After adding News to ICanUp, a simple product question appeared: which News item should receive the most prominent position on Home?
For an authenticated user, repeatedly presenting the same item as "new" is not very useful. But that creates an important semantic distinction: showing the card does not mean the user read the News.
It is equally important not to extend the same personalization to guests by silently turning an IP address, cookie, or fingerprint into persistent identity. The absence of an account identity is not automatically a problem that needs hidden identification.
Shown, Seen, Opened, Read and View Answer Different Questions
The problem starts when all of these words are treated as synonyms.
News state name | What it means | What it does not prove |
|---|---|---|
shown | The News item was included in the response or rendered as a card. | That the user actually noticed it. |
seen | The product considers that News item already presented to a specific known user. | That the user opened or read it. |
opened | The user navigated to the News detail page. | That the article was read to completion. |
read | A stronger product state representing completed reading when the product explicitly defines it. | It cannot safely be inferred only from opening the page. |
view | Analytics accepted and recorded a public-content view. | That it represents a unique person or completed reading. |
If two states change different product behavior, they deserve different names and different rules.
An ICanUp News View Starts After Opening the Public News Page
In the current ICanUp implementation, public News tracking happens on the News detail page itself.
ShowNewsAction first resolves an accessible News entity through the public service and only sends a genuinely public News subject to PublicContentViewTracker.
The mere presence of a News card on Home is therefore not a News page view.
public function __invoke( Request $request, News $news,): Response { $news = $this->newsService->getNews( $news, $request->user()?->can( PermissionEnum::NEWS_PREVIEW->value ) ?? false, ); if ($news->isPubliclyVisible()) { $this->publicContentViewTracker->track( $request, AnalyticsSubject::entity( ContentSubjectTypeEnum::NEWS->definition(),Seen Should Not Be an Alias for an Analytics View
Analytics and personalization solve different problems.
Analytics answers aggregate questions: how many views occurred, how activity changed, which content types are visited, and which channels bring traffic.
Personalization answers a different question: what should this specific user see now?
If analytics history becomes product state, the two contracts start leaking into each other. Changing the deduplication window could unexpectedly change Home behavior, while clearing analytics history could make previously presented items appear new again.
A Deduplicated View Still Does Not Mean Read
In ICanUp, every accepted view is first recorded as a normal measurement. A separate deduplication strategy then decides whether a companion deduplicated_views measurement should also be recorded.
This is useful for analytics, but the semantics remain analytical. A deduplicated view does not become evidence that a specific person read the News item.
public function track( TrackAnalyticsInput $input,): ?AnalyticsDeduplicationDecisionEnum { $this->history->record( $input->measurement() ); $context = $input->deduplication(); if ($context === null) { return null; } $decision = $this->deduplication->claim(An Authenticated User Already Has Stable Identity
The situation is different for an authenticated user. The application already knows who owns the personalized state.
If the product should avoid repeatedly promoting the same News item on Home, it can maintain separate exposure state for user + news. That state exists only for personalization.
- A News item is presented in the prominent position - its
seenstate may change. - The user opens the News item - that is a separate event.
- Analytics records a view - that belongs to a separate measurement system.
- If a real read contract is introduced later, it should not automatically follow from
seeneither.
This allows the product to select the next unseen News item without mixing personalization state with Analytics.
For a Guest I Do Not Want a Persistent News History Built From a Fingerprint
A guest has no account identity. It would be possible to manufacture a replacement with a cookie, IP-derived key, browser fingerprint, or another durable identifier.
For the requirement "show another News item", that is too high a privacy cost.
I prefer weaker personalization instead: a guest can receive an eligible current News item without a durable history of everything previously shown to that anonymous browser. After authentication, the application has a legitimate owner for persistent personalized state.
Authenticated user | +--> stable account identity exists | +--> product may remember News exposure | +--> next Home request can prefer unseen News Guest | +--> no account identity | +--> do not create persistent News-seen profile | +--> choose eligible News without durable guest historyThe Analytics Fingerprint Has a Narrow Responsibility
PublicContentViewTracker passes a privacy-safe visitor value into the deduplication context together with the subject and locale.
That mechanism exists so repeated requests do not automatically become new deduplicated views inside the deduplication window.
It should not answer the question "which News item was shown to this guest last week?". That is a different product contract.
return $this->tracker->track( TrackAnalyticsInput::withDeduplication( measurement: $measurement, deduplicatedMetric: ContentMetricEnum::DEDUPLICATED_VIEWS->metric(), visitor: $this->fingerprints->forRequest($request), scope: AnalyticsDeduplicationScope::subjectAndLocale( $locale ), ));Why Seen Should Not Be Written During the Selection Query
There is another useful boundary between selecting News and changing its state.
A query that finds the "latest unseen News" should not automatically mean the user has already seen it.
There are several reasons:
- SSR may generate a response that the user never actually sees.
- Prefetch does not prove exposure.
- A frontend failure after the response does not mean successful presentation.
- Repeated server rendering should not silently consume the unseen queue.
Selection and exposure are therefore better treated as separate operations even when a particular UI performs them very close together.
Read Needs Its Own Explicit Definition
The word read sounds obvious, but technically it is the most ambiguous state.
Is opening the page enough? Ten seconds on the page? Scrolling to 80 percent? Reaching the final ContentBlock? Explicitly pressing "mark as read"?
Until the product has a clear answer, I would not create an automatic News read state. A precise opened or analytics view is better than calling something "read" when the system does not actually know that.
A system should not give a strong name to a state it cannot actually prove.
The Boundary I Keep Between Product State and Analytics
Requirement | Where it belongs |
|---|---|
Avoid repeatedly promoting the same News item to an authenticated user | Separate user-owned personalization state |
Select public News for a guest | Public selection without a durable guest profile |
Count News page openings | Public Content Analytics |
Reduce repetition in the analytics metric | Bounded analytics deduplication |
Know that News was genuinely read | A separate read contract only when the product can define it precisely |
Conclusion: Privacy Is Part of Product Architecture
I like this case because a small requirement around a News card forces several responsibilities to become explicit.
seen belongs to personalization for a known user. view belongs to Analytics measurement. read should exist only when the product can define what it genuinely means.
For a guest, the product does not need to manufacture hidden identity merely because a personalization algorithm could consume one more signal.
Sometimes good personalization architecture is not about collecting more state. It is about deliberately refusing to create state that the product does not actually need.



