Add tutorial: live viewer profiles with Signals media tracking - #1893
Add tutorial: live viewer profiles with Signals media tracking#1893jborlase-snowplow wants to merge 7 commits into
Conversation
A Signals counterpart to the Kafka live viewer profiles accelerator. The Kafka version needs Snowbridge, Kafka, a Java consumer, DynamoDB, a WebSocket back-end, and local Docker or Terraform; this one needs a tracker, a Signals attribute group, and a thin dashboard. Five pages: the comparison and prerequisites, the React video page with the media plugin, the `viewer_profile` stream attribute group and its service, and the dashboard built on the Node.js SDK's `getBatchServiceAttributes`. Verified against a live trial pipeline with real media events driven in a browser. Notable behaviours documented in the pages: * `seconds_watched` reads `last` of the media `session` entity's `timePlayed`, which already accumulates, so summing would multiply-count every ping. It is scoped to a media session and resets on reload, while the `ads_skipped` counter accumulates over the `domain_sessionid`. * The media plugin's `MediaType` enum is declared in its type definitions but not exported at runtime, so the page passes the `'video'` string. * `getBatchServiceAttributes` returns a columnar response with no identifier column, so rows map from the request's `identifiers` order. * Publishing is not instant and a published attribute group version is immutable. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
documentation | 0a36cba | Commit Preview URL Branch Preview URL |
Jul 31 2026, 10:43 AM |
Style and writing reviewOverall this is a well-written accelerator: terminology is accurate ( 1. The companion repo link is a 404 (
|
Aggregate viewer metrics at the video level as well as the session level. A custom attribute key reads the media player's label, which the video page sets to the video's ID, and a second attribute group counts active viewers, distinct viewers, and skipped ads across every session watching a title. A service can only reference attribute groups that share an attribute key, so the two groups get a service each and the back-end makes one batch call per service. The Python definitions are now a single script that publishes the key, both groups, and both services together, passing the group objects straight to Service rather than hand-written name and version dicts. Also, from review: - Drop the Kafka comparison table for an Architecture section with a Mermaid diagram, and drop the companion repository link: every file is inline - Define the attribute group with the Python SDK directly, instead of starting in Console and cancelling, and remove the screenshots of the flow that is no longer instructed - Move the session registration step ahead of the back-end section, and fold the credentials into it - Style fixes: capitalized prerequisites, bold reserved for UI elements, lead-ins before bullet lists, and neutral phrasing Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Recreating an attribute group at the same name and version resumes the existing counters rather than starting from zero, which is worth knowing before you test the counts repeatedly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Pushed 🤖 Generated with Claude Code |
Offer a single paste-able prompt that produces the same Signals configuration as the Python SDK path: the custom video_id attribute key, both attribute groups, and one service per attribute key. The SDK path stays as written, since the dashboard reads the names it creates. The prompt works with any MCP-capable assistant connected to the Snowplow MCP server, or with the Snowplow Assistant in Console. A note explains that new definitions are saved as drafts, so the reader reviews the configuration before publishing it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Pushed Equivalence was verified for real, two ways: the MCP-built and SDK-built configurations' canonical registry JSON diff to identical (sole exception: Reviewer notes: the Console Assistant variant is mentioned, not walked (no UI claims made); no cross-link to the in-flight signals-mcp tutorial yet — add one in the new section and the conclusion when that PR merges; page 3 now sits at 7 H2s against the 3–5 guideline (declared, restructure if you prefer). 🤖 Generated with Claude Code |
Published tutorials shouldn't narrate the Console and API errors hit while verifying them, or how long anything took in those runs. * Page 2: state the media type strings as an instruction instead of an import failure, without the package version. * Page 3: keep one-service-per-attribute-key as the design statement it is, minus the 422 text; state group immutability without the 400 text; drop the delete-and-republish aside about stored values, which nothing else on the pages relies on; drop the publish-wait timing while keeping the wait itself, since values are empty until the definitions land. * Page 4: turn the registration note into the ordering rule the reader needs, and the troubleshooting list into positive checks on service names, credentials, and event ordering. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Pushed 🤖 Generated with Claude Code |
|
Claude (@claude) review please |
Writing quality reviewComments below are limited to things worth changing; the voice, second-person framing, and terminology are otherwise in good shape. Page structure
Headings
Wording and formatting
|
Product-owner directives: - Page 1 no longer frames the requirement as a "full pipeline" that rules out Snowplow Micro. It now asks for a Snowplow account and pipeline, keeping the free-trial pointer. - Removes the media-type-strings, "Why this group is defined in code", and "The media player entity has no ID field" panels. The label-to-video-ID fact survives as one prose sentence, since the custom attribute key depends on it. - Moves the AI-assistant section above the Python on page 3, and drops the two rationale paragraphs after the prompt. Review round: - Splits the attribute verification steps into their own page, renumbering the positions after it, and converts the last two page-3 notes to prose. - Adds a Snowplow implementation summary to the architecture section, listing the events and entities the demo generates. - Renames two headings, links the Console URL on first mention only, splits the over-length frontmatter descriptions, and drops a duplicated instruction to start the back-end first. - Adds Node.js and Python to the tutorial's technologies. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Pushed
🤖 Generated with Claude Code |
Page 3 mixed the two ways of creating the definitions into one linear flow: the AI-assistant section sat between the shared design and the Python, and ended by telling the reader to skip the rest of the page. Restructure it into two parallel route sections. The shared design now comes first in full, including the per-video conceptual paragraphs that were interleaved with the Python, so that the AI route's review step has something to check against. Then `## Define using the AI assistant` and `## Define using the Python SDK`, with the SDK flow's four steps nested as H3s and each route publishing on its own. The lead-in link points at the renamed AI section, and the page drops from 6 H2s to 4. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Pushed 🤖 Generated with Claude Code |
| "label": "Solution accelerator", | ||
| "useCase": "Real-time personalization", | ||
| "technologies": ["React", "Node.js", "Python"], | ||
| "snowplowTech": ["Signals"] |
There was a problem hiding this comment.
I first tried to find this accelerator by "Filter by topic: Signals implementation" but it didn't show up. It's under "Filter by Snowplow technology: Signals" but that's quite hidden on the bottom. Maybe worth adding it to the Signals implementation section so people looking for signals tutorials can find it easier?
|
|
||
| * The video page sends `play_event`, `pause_event`, `end_event`, `seek_start_event`, and `seek_end_event` as the viewer controls playback, a `ping_event` every 10 seconds while the video plays, and `ad_break_start_event`, `ad_start_event`, and `ad_skip_event` or `ad_complete_event` for a simulated pre-roll | ||
| * Every one of those events carries two entities: `media_player`, whose `label` holds the video's ID, and the media `session` entity, whose `timePlayed` is a running total of seconds played | ||
| * The `viewer_profile` attribute group folds the playback events into each session's state, watch time, and skipped ads, keyed on the built-in `domain_sessionid` attribute key |
There was a problem hiding this comment.
This section combines Snowplow tracking and Signals concepts into one without distinguishing them properly so is a bit harder to follow. I think it could help to separate the bullet points into tracking and signals.
| type="int32", | ||
| events=[ad_skip], | ||
| aggregation="counter", | ||
| default_value=0, |
There was a problem hiding this comment.
The default value is something we have only half-implemented and it's not handled well on the API (it's not really applied on the Go server on retrieval). I wouldn't recommend users to rely on it for now and we need to reconsider how it should be handled and fill the gaps we have. There is this task: https://snplow.atlassian.net/browse/AISP-678
|
|
||
| In this section you'll build the dashboard: a page that lists every live viewer with their state, watch time, and skipped ads, and every video with its audience metrics, polling for fresh values every few seconds. | ||
|
|
||
| There's one design problem to solve first. Signals is a lookup store: given an attribute key value such as a `domain_sessionid`, it returns that profile's attributes. It doesn't provide a way to enumerate all sessions that have profiles. The dashboard therefore needs to learn what to look up. The simplest solution is for the video page to announce itself: on load, it sends its `domain_sessionid` and the video's ID to a small back-end, which keeps the set of live sessions in memory. The dashboard asks that back-end for one profile row per registered session, and one audience row per video anyone is watching. |
There was a problem hiding this comment.
Wouldn't it be better to create a Signals attribute like last_viewing_sessions keyed by either the app_id or by the video_id (maybe better to have a viewers list by video). It could be a unique list with the last 100 session ids of viewers.
It's still not something someone could do in production, but it seems less hacky than this?
There was a problem hiding this comment.
The video ids could be a constant on the backend – probably in a real app, there could be a way to retrieve these ids (from database).
Note
The companion-repo merge blocker is resolved: the accelerator is now fully self-contained — every file is provided inline and the (formerly 404) repository link has been removed. One process point for reviewers:
tutorial-requirements/README.mdsays all source code must be on GitHub; the product owner has waived this for an inline-complete accelerator — confirm, or amend the requirement text.What changed?
New tutorial
tutorials/signals-live-viewer-profiles/(5 pages + 3 images): instrument video playback with the media tracking plugin, define session-level viewer attributes AND per-video audience metrics (customvideo_idattribute key) with the Python SDK, and build a live-viewers dashboard ongetBatchServiceAttributes.Why?
Fills the media-tracking + real-time-profiles gap in Signals tutorial coverage.
Reviewer guidance
active_viewers: 2; the dashboard screenshot matches a 3-session/2-video assertion round).media_player2-0-0 has no content ID —labelcarries the stable video ID (alternative documented).seconds_watcheduseslastnotsum(the session entity already accumulatestimePlayed); the push-state page folded into the conclusion (Node SDK can't subscribe to interventions).🤖 Generated with Claude Code