Drop the story a reader has passed over this visit
A reader is on their fourth article since they arrived, and the same story has sat in the rail under every one of them. They have not opened it. The model learns about the skip after this reader has moved on, so the rail keeps offering it. Four shows without a click is an answer, and the slot could hold something they have not been offered yet.
rail_impressions_session- How many times each story was shown in the rail this visit
rail_clicked_session- The stories opened from the rail this visit
Removes any story shown three times this visit without a click from the candidate set, and fills the slot with one this reader has not been offered.
from datetime import timedelta
from snowplow_signals import Attribute, Event, EventProperty, StreamAttributeGroup, domain_sessionid
rec_shown = Event(name="recommendation_shown", vendor="com.example", version="1-0-0")
rec_clicked = Event(name="recommendation_clicked", vendor="com.example", version="1-0-0")
# Keyed on the visit, so no rolling window; a ttl lets the row expire after it.
rail_impressions_session = Attribute(
name="rail_impressions_session",
type="dict",
events=[rec_shown],
aggregation="category_count",
property=EventProperty(vendor="com.example", name="recommendation_shown", major_version=1, path="article_id"),
ttl=timedelta(hours=6),
)
rail_clicked_session = Attribute(
name="rail_clicked_session",
type="string_list",
events=[rec_clicked],
aggregation="unique_list",
property=EventProperty(vendor="com.example", name="recommendation_clicked", major_version=1, path="article_id"),
ttl=timedelta(hours=6),
)
recs_skipped = StreamAttributeGroup(
name="recs_skipped",
version=1,
attribute_key=domain_sessionid,
owner="you@example.com",
attributes=[rail_impressions_session, rail_clicked_session],
)// In the code that renders the article page
const reader = await signals.getServiceAttributes({
name: "recs_skipped",
attribute_key: "domain_sessionid",
identifier: sessionId,
});
// { rail_impressions_session: { c04: 4, f17: 2, a81: 1 }, rail_clicked_session: ["a81"] }
const skipped = Object.entries(reader.rail_impressions_session)
.filter(([id, shows]) => shows >= 3 && !reader.rail_clicked_session.includes(id))
.map(([id]) => id);
recommender.rank({ exclude: skipped });