Rifflets analytics contract
Status: October 5, 2026. Dan authorized deployment for invited co-founders and friends, private nickname-to-browser reporting, and traffic classification that separates likely visitors, agents and registered internal tests. The invited-test release uses automatic collection; ordinary developer builds remain off. The verified deployment URL and live smoke results are recorded in releases and STATUS.md after rollout. No audience results are inferred from tests.
Purpose and architecture
Creators first need understandable engagement counts: encounters, opens, meaningful tries, active time, the number of different Rifflets tried, repeat attempts and return visits. App-specific milestones and a true final finish are secondary measures because progress means different things across experiences. Building agents also need release-specific action sequences, stages, controls, errors, performance and device context so they can propose and test improvements. Collect broadly during internal testing, then use measured volume and utility to narrow the schema.
The shared runtime and SDK send to one first-party collector, with separate private analytics tables in the existing Pages/D1 architecture. The dashboard and aggregate JSON serve creator-style analysis; authenticated raw samples serve internal agent diagnosis. Existing public feedback/history is a separate, explicitly submitted publication flow. Never append behavioral records or IPs to public feedback.
Feed and Lab use links; the viewer embeds one app. There is no autoplay fleet. The automatic bridge covers every published route, Feed and Lab card impressions. The current source upgrade migrates every published route to reviewed rifflet.analytics/0.2 semantics; see [the coverage inventory](docs/COMMON_INFRA_COVERAGE.md) for exact meanings and browser/source evidence. This source statement is not a deployment receipt. Historical releases may lack milestone events or use a broader legacy goal. Do not infer correctness or a full-experience finish from content.json.completionEvent alone.
Required creator contract
Every new or substantially revised Rifflet must:
- Supply
apps/<slug>/analytics.jsonusingrifflet.analytics/0.2, with a documented first meaningful action, up to 12 honest intermediate milestones, a true final completion ornull, reset/stop boundaries and bounded interaction mappings. Each event has one semantic role. A finite completion must matchcontent.json.completionEvent. For an open-ended app, explicitly map that legacy manifest event to one milestone or stop role; never rename an intermediate achievement as a final finish merely to fill a dashboard. The registry still reads 0.1 contracts for frozen source compatibility, but current published apps must use 0.2. - Emit those facts through
createRuntime().emit(type, detail). The common bridge supplies identity, device, release, transport and lifecycle. Do not install an independent analytics client or upload arbitrary detail objects. - Keep play, editing and accessibility working with analytics off, blocked, unavailable, storage denied, timed out or at capacity. Never await delivery to advance play.
- Use the build's actual app ID, version and immutable source commit. The build manifest is authoritative when a legacy runtime has stale version text; a bounded
runtime_version_mismatchdiagnostic flags it. Dirty previews are explicitly unpinned and accepted only by the local test server. Actor remains a backward-compatible declaration, not identity proof; traffic classification uses signed QA context and bounded browser/edge evidence while preservingunknownwhen evidence is insufficient. - Use reviewed bounded numeric, boolean and enum detail fields. Unknown fields are discarded before transport and rejected by the server. New fields require coordinated schema/client/server review and a statement of the question they answer.
- Test event meaning, not just shape, and submit source-linked evidence with the release. Report unavailable metrics explicitly. A plan finished is not food cooked; a copied link is not delivery; a displayed solution is not a player solving it. Test intermediate milestones separately from finishing the entire defined experience.
The 0.2 shape is start: {event,meaning}, required milestones: [{key,event,meaning,valueField?}], and completion: {event,meaning} | null, followed by the existing lifecycle and interaction mappings. Keys and events are bounded lowercase tokens. valueField, when present, names a reviewed emitted integer field whose values are 0–10000. A single-step experience may use an empty milestones array. Do not invent progress events.
The SDK method milestone(key, index?) emits a milestone inside the current attempt and keeps that attempt open. It deduplicates the same key/index; without an explicit index it assigns the next index for that key. complete() is reserved for the reviewed final hook, emits completed with {kind:'final'}, and closes the attempt. Previously stored completed records with {} remain historical goal evidence and must be reported as legacyGoalAttempts, not retroactively described as verified full-experience finishes.
Common data and ownership
| Owner | Collected facts |
|---|---|
| Feed / Lab | Qualified card impressions and card control interactions |
| Bootstrap / SDK | Visible opens; browser/session/view/attempt IDs; release; entry surface; activity time; controls; viewport, visibility, scroll-depth, performance and error categories |
| Rifflet author | Reviewed meaningful starts, intermediate milestones, final completion and attempt boundaries; emitted action names and bounded details |
| Collector | Trusted request IP, receipt time, validation, deduplication, capacity and rate limits, retention and private reads |
| Report / building agent | Scope, definitions, denominators, coverage, uncertainty, hypothesis and a reviewable proposed revision |
The exact event envelope is rifflet.event/0.2, the batch envelope rifflet.analytics-batch/0.2, and collection policy rifflet.internal-test/0.1. Executable field definitions live in shared/analytics-contract.js and shared/analytics-device.js.
Each event includes random event ID, optional persistent browser ID, session/view/attempt IDs, monotonic view sequence, client time, app ID/version/commit, surface, entry surface, actor, collection policy and a cached passive device snapshot. Browser identity lasts 30 days; sessions expire after 30 minutes idle. Attempts keep their original identity so long attempts are not split by rotation. Denied storage uses an ephemeral session and browserId:null. Clearing storage starts a new profile; the service does not relink it.
The authorized private browser directory may show an observed shared/identity.js nickname history beside the independent analytics browser ID. This is private operator evidence for recognizing a browser profile, not authentication or a public identity. Generated aliases can repeat. Never use an alias as a key, upload the identity module's opaque local ID to public scoreboards, infer a MAC address, fingerprint a device, join profiles across devices, or resurrect a profile after storage is cleared. Public scoreboard names remain explicit, consented submissions and are not backfilled from analytics. Existing alias reset semantics remain: confirm the exact old/new name, preserve progress, and leave earlier public posts under the earlier name.
Device context includes bounded user agent, platform, languages, timezone, screen and viewport dimensions, pixel ratio, touch capability, CPU count, available memory/network hints, reduced-motion/color/pointer preferences and API availability flags. These are passive observations; no permission requests or camera/microphone/sensor access. Missing properties are null or documented defaults. Request IP is read only from trusted CF-Connecting-IP, validated, and stored separately from the client payload. It is available in private raw exports; aggregates expose only the distinct address count. IPs are not people or stable identities.
No secrets, typed answers/free text, shared-link payloads, full URLs/referrers/query strings, DOM snapshots, images, audio, sensor samples, raw exception text or stacks are uploaded. Control identifiers use stable IDs or positional tokens, never text labels. Only range numeric values, checkbox/radio state and select option index are captured by the generic control observer. The raw trace contains IDs and IP and is private operational data; it must not be mistaken for a creator-safe aggregate.
| Event | Definition |
|---|---|
impression | At least half a card visible for one continuous second; once per app per browse view |
opened | Visible app view, once per view; not a meaningful try |
started | Reviewed first meaningful action; reason first-action, restart or replay |
milestone | Reviewed intermediate achievement {key,index} in a matching attempt; does not end it |
completed | Reviewed final condition {kind:'final'} once in a matching started attempt and closes it; historical {} records are legacy goal evidence |
attempt-ended | Explicit reset, exit, failure or stop; absence is not dislike |
active-time | Non-overlapping deltas up to 15 seconds, visible and within 15 seconds of input after start |
interaction | Reviewed move, hint, undo, choice, step, pause, resume, restart or error |
app-event | Runtime action name plus centrally allowlisted scalar details; useful even before semantic migration |
control | Trusted click/change, control token/type and bounded range/selection state |
performance | Navigation load/DOM readiness/FCP, navigation type and transfer bytes when available |
error | Script, promise or resource category; no message, stack or URL |
visibility, viewport, scroll-depth | Visibility transitions, debounced dimensions and increasing 10% page-depth buckets |
share-attempt, share-result | Attempt, native return, confirmed clipboard or fallback; no delivery claim |
skip | An explicit skip only, never inferred from silence or closing |
Generic diagnostics are capped at 1,000 accepted observations per document view. Runtime details are not bulk uploaded: allowed keys and ranges in sanitizeAppFields define the retained subset. All events are initially unsampled within these bounds. Custom sharing paths and canvas gestures without runtime events may remain partially observed; coverage review should name those gaps.
Metrics and agent evidence
- Unique players: distinct non-null analytics browser IDs with at least one meaningful start in the selected report window. These estimate playing browser profiles, not verified people. Passive impressions and opens do not count.
- Starts: attempts begun by the app’s reviewed first meaningful action, including a replay or restart. A start is neither a page load nor a milestone or final finish. The dashboard shows the current app-specific meaning beside its progress details; historical contracts remain release-specific.
- Observed browsers: persistent IDs, not people. Storage denial/clearing, shared devices, blockers and identity expiry affect counts.
- Observed UI interaction: all published apps can report trusted click/change controls. Count distinct interacted apps per browser and returns across sessions, excluding Feed/Lab card navigation. Settings and share controls count; this is not a meaningful-play claim.
- Tried breadth: distinct app IDs with reviewed meaningful starts per browser. Repeat attempts and releases do not count as additional apps; original editions have separate IDs.
- Open-to-start: opened views with a start divided by opened views, matched on browser/session/view/app/release.
- Milestones reached: milestone event count, attempts with milestones and per-key count/attempt/max-index. A milestone does not imply the app was finished.
- Finished the full experience: unique attempts started in the selected UTC window that emit the reviewed final completion by its end, divided by starts. Apps with
completion:nulldisplay “Open-ended — no final finish.” Historical releases without 0.2 evidence display “Not recorded in this release.” Orphans and completion after terminal events are excluded; in-flight attempts can be incomplete observations. - Legacy goal attempts: attempts containing historical
completed {}events. Retained for backward compatibility, never merged into verified final-finish counts. - Repeat attempts: another attempt of the same app in one session. Return: an earlier meaningful start in a different observed session, within retained history.
- D1/D7: first observed meaningful start in retained history, followed by a start on exactly the next/seventh UTC day. Only fully elapsed target days qualify; show eligible and returned counts. This is not lifetime acquisition retention.
- Active play time: recent-input foreground time from attempts started in the selected report window, including their valid samples through the report end. It is not enjoyment, attention or learning. It undercounts quiet reading/listening/breathing, and concurrent tabs can overlap. Total time includes null-browser sessions; play time per player uses only time linked to known playing browser IDs, divided by their unique count. With no known players the average is unavailable, not zero.
- Repeat visits per player (7d): count distinct playing sessions per known browser within
(report end − 7 days, report end], subtract the first session for each browser, then divide by unique players in that same rolling window. For example, three sessions for one player plus one session for another is two repeat visits / two players = 1.0. Multiple rounds or attempts in the same session do not add repeats. The session idle rule is 30 minutes; this is not next-day retention. Display repeat-session and repeated-player counts beside the average. - Diagnostics: event and control counts, runtime actions, error categories, device distribution, timing samples/median/p95 and estimated payload bytes. Device distributions count each observed browser or ephemeral session once per scope. Top groups are capped at 50. Payload bytes exclude transport/storage overhead.
Lead reports with engagement, then show per-app milestones and final finishes with the registry's actual meanings. Do not publish one aggregate completion percentage across incomparable apps. Always separate traffic classifications, actor declarations and release versions, show denominators and instrumentation coverage, and use unavailable for undefined rates. Synthetic tests are not human demand; before/after comparisons are not causal. No analytics results are fabricated by this implementation.
Private GET /api/analytics/summary returns rifflet.analytics-summary/0.1; GET /api/analytics/evidence requires an app filter and returns the same scoped structure. They include aggregate totals, per-app/release measures, entry surfaces, traffic evidence, coverage, definitions and limitations, without visitor/session/attempt IDs or nicknames. Dates are inclusive UTC, at most 30 days; earlier retained events support return calculations. Actor may be all; traffic may be all, visitors, human-likely, agent, internal-test or unknown. “Visitors” combines human-likely and unknown, and the UI must disclose that unknown can include automation.
Traffic classification is evidence, not verified human identity. A valid registered QA context takes priority, followed by automation evidence, explicit actor declaration, and limited positive interaction evidence; missing edge signals remain null rather than false. Never classify solely from IP, nickname, click trust, or the absence of webdriver. Preserve unknown when evidence is insufficient and expose the bounded reason codes used. Filter complete browser/session evidence consistently before attempt aggregation so late evidence does not selectively remove its earlier start.
Private GET /api/analytics/events?from=YYYY-MM-DD&to=YYYY-MM-DD&actor=all&traffic=visitors&app=water-jugs&limit=500 returns at most 500 retained raw observations with device, IDs, classification, receipt time and trusted IP, newest receipt first. Private GET /api/analytics/browsers returns at most 100 browser profiles with observed nickname history and per-app engagement. truncated:true means a sample is incomplete; no pagination/full-history export is promised. For agent use, scope to its app, inspect release and chronology, and avoid exposing other apps' traces. Access is currently operator-level, not creator-specific authorization.
For each proposed improvement an agent should record: question, app/release/window, sample and missing coverage, observed action or failure pattern, alternative explanations, hypothesis, proposed change, acceptance test and resulting release. The evidence supports tuning; it does not authorize an automatic production change or optimizing time spent at the expense of the experience.
Internal collection profile and access
Per Dan's October 2 instruction, the internal test build collects automatically and adds no disclosure, settings or opt-in UI. Ordinary builds initialize no analytics identity or transport. Browser GPC/DNT signals keep collection off. The subsequent user-authorized hosted rollout enables this profile for invited testing; no disclosure or opt-in UI is added.
The hosted dashboard at /analytics is password-protected operator access. It supplies creator-style reports, but creator accounts, ownership authorization and external distribution are not implemented. Before offering reports externally, add app-scoped authorization, reviewed cohort suppression and protection against filter differencing. Never distribute the operator credential or raw all-app traces to creators.
Events are retained for 30 days by server receipt time; reads exclude expired rows and requests perform cleanup. This is lazy purge, not a claim of exact physical deletion at day 30. Scheduled purge, deletion procedures and platform backup retention must be decided for a persistent hosted service.
Delivery and operations
POST /api/analytics/events accepts exact same-origin JSON, max 20 events / 32 KiB, known release and strict fields. Exact retries keep IDs and payloads. Changed payload under an existing ID conflicts; inserts are atomic and acknowledged only after persistence. Source labels and actor fields remain client assertions, not attestation.
Authenticated same-origin QA may create a four-hour signed, Secure, HttpOnly, SameSite=Strict __Host-riff_test context at POST /api/analytics/test-context with bounded purpose (qa or exploration) and agent. Clearing the same endpoint removes it. The signed context classifies subsequent anonymous analytics; it is not a user identity or broader authorization. Its run ID and metadata stay in private evidence, never URLs. Staff manual QA can register as human QA, while agent exploration registers as agent activity.
The browser queue is memory-only, max 100 events / 24 hours, 10-second transport timeout, a maximum 10-second ordinary batch delay and five retries with backoff. Full batches and semantic terminal events send immediately without blocking play. Hidden/page-exit flushes can start extra tail batches within a shared two-request (at most 64 KiB) keepalive budget. Claimed IDs prevent concurrent duplicate sends. Permanent failures drop the batch. Navigations, crashes, blocking and full queues can lose events; an unload send is not proof of receipt. Analytics failures cannot block gameplay. A loopback-only QA hook exposes queue/drop state; central delivery-loss accounting is not implemented.
Every automated builder, agent and browser-test framework must use a dedicated browser context and put test_agent=1 on the first Feed, Lab, viewer or app navigation. scripts/test-agent-browser.mjs supplies the Playwright URL/context helper. The enabled bootstrap captures a valid declaration and removes only test_agent with history.replaceState, preserving history state, all other query text and the hash before app code can copy the current URL into a recipient link. The declaration initializes a sessionStorage marker for that top-level tab, expires after four hours and propagates through same-origin navigation and same-origin viewer descendants. A bounded in-memory page reader preserves descendant inheritance when sessionStorage is blocked, without extending the original expiry; later top-level navigation cannot inherit through blocked storage. test_agent=0 removes the marker for events created afterward; it does not rewrite queued, delivered or earlier same-session events. The SDK also accepts testAgent: true and emits an immutable integer context.test_agent snapshot on every new event. Older rifflet.observation/0.1 records without the optional field remain valid. A declared tag classifies as internal-test with reason declared-test-agent; a signed QA/builder context retains precedence. Neither mechanism grants access, authenticates an agent or proves identity.
Clean traffic views and daily trends
The dashboard sends includeLegacy=0, nickname=all, and testAgent=exclude by default alongside the visitor-candidate traffic selection. Include legacy traffic is unchecked on a fresh load. It restores unknown sessions with no modern observation context; it does not remove records from storage or erase historical goals. Modern unknown sessions remain available because insufficient human/automation evidence is not itself legacy traffic. The separate nickname selector means a valid nickname was, or was not, observed somewhere in the retained session. Missing nicknames are missing coverage, not proof of an agent.
All private reads (summary, per-app evidence, browser directory, and raw export) support the same strict query parameters: includeLegacy=0|1, nickname=all|observed|missing, testAgent=all|exclude|only. Existing API consumers that omit them retain the earlier inclusive behavior (1, all, all). The dashboard explicitly supplies its narrower defaults. Every filter uses whole-session evidence before actor/app narrowing. The explicit test selector includes context.test_agent=1, legacy actor=agent-test, and registered QA/exploration sessions; automation detection remains separately selectable through the traffic control. A later test declaration excludes earlier events in the same session too. Use a fresh context for a separate human review.
summary.trends.days contains chronological UTC buckets through the report end or current time, whichever is earlier. It reports observed browser profiles, sessions, meaningful starts, returning playing profiles, distinct apps tried, and active seconds. Today is partial. Empty selected buckets mean no retained matching observations, not proven absence of use. Browser and session counts across days cannot be summed into unique users. Active time is attributed to the attempt's start day, including valid later samples through the report end. These trends retain the existing bounded read and retention limits; they are not historical backfill or a verified-person measure.
Server requirements: existing RIFF_DB binding, separate 0002_private_analytics.sql, ANALYTICS_ENABLED=true, and server-only RIFF_RATE_KEY; private reads also need ANALYTICS_READ_TOKEN. Local/default limits are 10,000 retained events, 200 write batches and 120 reads per trusted-IP hour. The hosted invited-test profile explicitly sets ANALYTICS_CAPACITY=100000 and ANALYTICS_WRITE_RATE=2000 for shared networks. Authenticated reports show exact storage use and warn at 90%; retained rows are never silently sampled or evicted. Summary reads remain capped at 10,000 relevant rows; select an app and shorter date range if a report exceeds that bound. Rate buckets use rotating HMAC; raw IP is stored only in the separate event column. A full store rejects new batches rather than silently evicting them. Review measured volume before raising capacity; this pilot uses bounded in-memory aggregation, not a production warehouse.
The Pages/D1 adapter and migration are deployed only through the approved rollout; consult the current release receipt for remote verification. Platform references: [Pages bindings](https://developers.cloudflare.com/pages/functions/bindings/), [D1 prepared statements](https://developers.cloudflare.com/d1/worker-api/prepared-statements/), [Workers practices](https://developers.cloudflare.com/workers/best-practices/workers-best-practices/).
View hosted analytics privately
Open https://riff.marantz.net/analytics and enter the operator dashboard password. The public sign-in page is /analytics-login. The server gates the dashboard and its raw/aggregate/evidence reads; a client-side password check is not sufficient. A successful login sets a persistent 30-day host-only Secure, HttpOnly, SameSite=Strict cookie. Returning to the dashboard or sign-in page reuses that cookie, including after a browser restart. It expires 30 days after login, and signing out or clearing browser cookies ends remembered access. Only a hash of its random session token is stored in D1, and signing out revokes the server record. Password-verifier rotation invalidates existing sessions. Login attempts are limited to ten per trusted IP per fifteen-minute window before password verification. Sessions are bounded to 64 active records per verifier and 128 total; expired rows are purged during auth requests. A password rotation should deliberately clear the session table as part of the rollout, so prior-verifier records do not temporarily consume capacity. Verify or retire older deployment aliases when rotating credentials.
The operator password is held only in the ignored, mode-600 .analytics/dashboard-password file for the owner. Pages stores a salted PBKDF2-SHA256 verifier in the secret ANALYTICS_PASSWORD_HASH; neither the password nor that verifier is shipped in assets, logs or URLs. Session endpoints use exact same-origin JSON and bounded request bodies. Protected responses are non-cacheable. This is shared operator access to all apps and private raw samples, not creator-specific accounts.
The build bundles dashboard HTML from ignored .generated/analytics.html into the authenticated Pages Function and removes it from public dist. Keep this separation: static-router URL normalization must never expose a dashboard page through an unguarded alias. Build before compiling Functions.
The original loopback viewer remains available as an alternative. After the authorized deployment, run npm run analytics:live-dashboard and open http://127.0.0.1:4287/analytics.html. This loopback-only viewer reads the real hosted database through authenticated APIs. The read capability is stored in the ignored, mode-600 .analytics/operator-token file and the Pages secret ANALYTICS_READ_TOKEN; it never enters dashboard JavaScript, browser storage or URLs. The viewer accepts only same-origin GET requests and serves only the exact built dashboard assets. Direct unauthenticated hosted API reads return 401; authenticated browser reads use the HttpOnly session and never receive the operator bearer capability. Restart the viewer after rebuilding its assets.
Run and validate
cd '/Users/dmarantz/Projects/Advanced Content/rifflets-analytics'
npm run analytics:build
PORT=4286 npm run analytics:preview
Open http://127.0.0.1:4286/feed and http://127.0.0.1:4286/analytics.html. Loopback preview validates Host/Origin, stores SQLite under ignored .analytics/, and supplies read authorization inside the server; no browser bearer token. ANALYTICS_DB=:memory: makes tests disposable. Set RIFF_ANALYTICS_ACTOR=agent-test during automated QA builds. npm run build restores an analytics-off artifact.
Acceptance covers first action/true completion, reset/replay/stop, incoming completed state, direct/viewer navigation, hidden documents, keyboard/touch, denied storage, no analytics on normal builds, GPC/DNT, failed/duplicate/conflicting delivery, bounded fields, trusted IP and private reads. Shape validation cannot replace source/browser validation of meaning. Desktop viewport checks are not physical-phone tests.
Remaining extensions are comprehensive per-app browser terminal-path verification, creator-specific access, hosted retention operations and a public-audience policy if the audience changes. The current rollout uses the existing authorized Pages/D1 service; creator report distribution is not enabled.
Independent Claude audit follow-up — October 4
Exact-release measurement: the registry records each reviewed release's analytics contract. Final rates use finalEligibleStarts only; historical releases without a recorded 0.2 contract are unavailable, never a zero-success cohort. Known open-ended finals and undeclared milestone keys are excluded and counted as data-quality exceptions. Unmatched finish reports stay visible as data-quality evidence. A legacy completed goal remains a legacy goal. Preserve the analytics contract with each production source pin when updating release history; never assign a current contract retroactively to an unknown historical release.
Open-to-start conversion follows the first retained open in the selected UTC window, matched by source/browser/view across idle-session rotations. A tab opened before the window does not add a denominator or conversion to the later window. Session-based returns can happen after 30 idle minutes; they are not necessarily next-day returns. The browser directory's repeat-session count covers retained history, while its session count covers the selected window. The default dashboard range is 14 inclusive days; D7 requires at least eight inclusive days and an eligible cohort.
Delivery prioritizes journey boundaries over diagnostics in the bounded 100-event client queue. In-flight records remain claimed until acknowledgement/retry. When even the queue's journey events exceed its finite capacity, loss remains possible and is exposed by the client's dropped count; this is not durable offline storage. New clients send X-Riff-Analytics-Ack: partial-v1. Only negotiated collector acknowledgements may include rejected:[{eventId,code}] with invalid_time or diagnostic_capacity; accepted, duplicate and rejected IDs must partition the sent batch exactly. Legacy clients without that header retain strict whole-batch timestamp/capacity failures and the original accepted/duplicate response shape; rejected records are never falsely reported as accepted or duplicates. Invalid timestamps are isolated only after the entire batch's structural, release, privacy and ID validation succeeds. Arbitrarily wrong device clocks remain untrusted; the server does not silently rewrite event times. Malformed/private payloads and conflicting IDs still fail atomically.
The collector reserves the top 30 percent of its existing event capacity for journey events: impressions, opens, starts, milestones, finals, endings, explicit skips, sharing outcomes and nickname observations. Lower-priority diagnostic samples receive explicit diagnostic_capacity rejections once total occupancy including incoming journey events would exceed the 70 percent boundary. Exact retries do not consume capacity. This protects against diagnostic saturation, not forged semantic traffic or distributed abuse. No new paid service is required.
Read scope for raw events and the browser directory begins with profiles present in the requested date/app/actor window and includes their retained history and complete session traffic evidence. Summary/evidence reads seed the union of that report window and its rolling seven-day window, so players seen only earlier in the week still contribute to repeat usage. Unrelated prior-only profiles no longer fill a narrowed report. A genuinely busy selected profile can still exceed the hard 10,000-row report cap even though storage allows more; this returns read_scope_too_large and no partial totals. Date narrowing is not guaranteed to make an oversized profile readable. Future-only ranges return a validation error. Indexed rollups/pagination and stronger per-profile admission remain separately scoped scaling work.
Privacy wording: the existing authorized private observation includes the generated nickname identifier as well as its alias. That identifier may persist across analytics browser-ID rotations and therefore permits correlation; rotation is not an unlinkability promise. The directory continues to key on the analytics browser ID and does not automatically merge profiles or relink cleared storage. Altering nickname collection or retention is a separate policy decision. Public feedback and score consent remain separate. The UI calls weak visitor evidence “human-like signals,” not verified humanity; real-browser agents can have those signals. Signed QA enrollment remains session-scoped and expires; it does not permanently label every future use of a browser as testing.
Feed likes and shares — October 5, 2026
The Feed uses the same private first-party collector, browser/session identity, pinned app release, observation context, traffic filters and delivery queue as the existing analytics framework. Heart transitions emit like-changed with exactly {liked: boolean} and no gameplay attempt. Restoring a favorite from storage does not emit another like; previous favorites are not backfilled. Like/unlike events are journey-priority facts, not diagnostic clicks, so a control diagnostic cannot double-count them.
Opening the share panel emits share-attempt. A resolved clipboard write emits share-result {method: 'clipboard'}; a resolved native-share promise emits native; a browser copy command over the full displayed link emits manual. Native cancellation emits cancelled, and failed clipboard/native operations emit failed. Selecting text, showing a fallback link or opening the panel is not a successful copy or delivery. Native returns and manual copy commands do not verify receipt. A late asynchronous result stays attributed to the Rifflet/version whose link was shared, even after the viewer moves to another card. Share URLs, recipients and clipboard contents are never sent as event detail.
Private summaries, per-app/release reports, daily JSON trends and browser-profile/per-app details include likes, unlikes, likingBrowsers, shareAttempts, copiedLinks, nativeShareReturns, manualCopies, cancelledShares and failedShares. These are actions during the selected dates, not a current worldwide favorite count or verified unique people. Existing date/app/actor/traffic/test filters apply unchanged. Events are idempotent by event ID; distinct repeat actions remain distinct. Older Feed social actions were not recorded and cannot be reconstructed from click diagnostics. Analytics may be unavailable because of privacy settings, blocked storage/network or finite retention/capacity; the controls still work. Favorites remain local and are not publicly published by these metrics.
Rifflet rankings and report pivots — October 5, 2026
The private dashboard includes a By rifflet view, with numeric sorting by unique players, total play time, play time per player and repeat visits per player over seven days. Every selected registry app remains listed, including apps with no observed starts. Undefined averages display as unavailable. Counts are scoped estimates, never verified people or lifetime totals.
Report dates apply to the first three metrics. The repeat window is independent of the report From date and ends at the earlier of the report To day’s end (UTC) or server now. The response supplies exact UTC repeatWindow.from and repeatWindow.to, durationDays:7, and retentionLimited. A historical window overlapping the 30-day retention cutoff is explicitly limited; no full seven-day history is promised then. This measure is separate from exact-day D7 retention and from repeat attempts within a single visit.
Aggregate metric objects add linkedActiveSeconds, unlinkedActiveSeconds, activeSecondsPerPlayer, playingSessions, repeatPlayingSessions, repeatPlayingBrowsers and repeatSessionsPerPlayer. Each app adds rolling7d with the five playing/repeat session metrics. These are aggregate numbers only; no browser IDs, session IDs or nickname histories enter this ranking payload. Existing traffic, test-agent, legacy and nickname filters apply to complete session evidence before aggregation, including sibling-app evidence through the report end. Later evidence cannot change an earlier report.
Summary-card links navigate to useful breakdowns using the server-reported scope of the displayed report. A returned scope that differs from the request is disclosed; dependent directory reads and app pivots retain that reported scope. Unapplied form edits cannot silently change a linked view. Rifflet detail actions scope the report to that app before opening progress or browser details. Private ranking data follows the existing logout, authentication-failure and page-exit clearing rules. See the current release receipt for deployment status; this contract is not itself a rollout receipt.
Analytics before publication and approval
Before publishing a preview or approving any new app/version, follow [Analytics publish and approval checks](docs/analytics/PUBLISH_APPROVAL_CHECKS.md). Review App Stats and Platform Health in the simulator, verify the exact release/submission identity and a marked server receipt (test_agent=1), and check meaningful starts, intermediate milestones and final completion separately. Unknown or local-only telemetry is not verified hosted analytics. Uploaded foreground time and historical family aggregates cannot prove semantic coverage or a new revision's correctness. Record gaps explicitly; preserve existing ownership and approval boundaries.