# 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:

1. Supply `apps/<slug>/analytics.json` using `rifflet.analytics/0.2`, with a documented first meaningful action, up to 12 honest intermediate milestones, a true final completion or `null`, reset/stop boundaries and bounded interaction mappings. Each event has one semantic role. A finite completion must match `content.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.
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.
3. Keep play, editing and accessibility working with analytics off, blocked, unavailable, storage denied, timed out or at capacity. Never await delivery to advance play.
4. 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_mismatch` diagnostic 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 preserving `unknown` when evidence is insufficient.
5. 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.
6. 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:null` display “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

```sh
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.
