0% прочитано

Laravel Seen vs Read State: Showing Unseen News Without Guest Fingerprinting

Showing a News card on Home, opening it, reading it, and recording an analytics view are different events. This article separates product state from analytics and explains why persistent News exposure history belongs to authenticated users rather than anonymous fingerprint profiles.

25 вересня 2026 р. 9 хв читанняLaravel

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.

In this article, seen is a product-personalization state. It is not an ICanUp Analytics metric and should not be inferred from a deduplicated view.

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.

PHP - News view starts in ShowNewsAction
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.

PHP - Views and deduplicated views are separate measurements
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 seen state 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 seen either.

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 and guest personalization boundary
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 history
This does not mean ICanUp never uses a privacy-safe fingerprint in Analytics. It exists for bounded deduplication. The important boundary is not turning an analytics deduplication key into durable guest identity for personalization.

The 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.

PHP - Analytics fingerprint is used for bounded deduplication
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.