Repository navigation
Conversation
…and refusal code One Analytics Engine data point per rise_present call: rise.current.admitted with its beats and scenes, rise.current.refused with the refusal's code from the door's closed set. Only what telemetry/events.json names is written, and every answer is byte-identical with the binding absent, present or throwing. EVENTS is bound in wrangler.production.jsonc alone. PRIVACY.md says so in the ChatGPT and Claude list and in section 11. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VHrJ4hmTtR1RFNpc215EK7
A reading's begin and end, the first word after Home's Begin, and four failure classes, each one beacon from rise.syberlabs.io (never from a frame) to worker/count.mjs. It takes only same-origin JSON of at most 512 bytes within COUNT_LIMITER, derives the browser and the device from the User-Agent, fits the numbers, writes only what telemetry/events.json names, and always answers 204. PRIVACY.md names the counts in sections 4, 6, 10 and 11. Builds on #762's writer and allowlist. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VHrJ4hmTtR1RFNpc215EK7
✅ Deploy Preview for rise-v2-symbolic-experience ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
Bring this branch up to date with main's dead-code sweeps (#765, #766, #767) so it can merge. Two files conflicted: - worker/mcp-server.mjs: main deleted Gate 0 and the witness log, made dispatch take the card as its third argument (dispatch(message, origin, card = null)) and deleted handleLive. All of that is kept as main has it. This branch's count callback becomes dispatch's fourth argument (count = () => {}): dispatch passes it to call() for rise_present, and handleMcp still derives the host with hostOf and writes through countEvent. No response changes. - vitest.fast.config.js: main lowered FAST_TEST_FILES from 169 to 157 after deleting test files, and this branch adds worker/telemetry.test.js. The merged list collects 158 files (vitest list --filesOnly, and the fast run's JSON report), so FAST_TEST_FILES is 158. PRIVACY.md, public/PRIVACY.md, public/privacy.html, worker/production-config.test.js and wrangler.production.jsonc merged without conflict; npm run verify:legal confirms the generated copies still match PRIVACY.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VHrJ4hmTtR1RFNpc215EK7
Bring this branch up to date with main's dead-code sweeps (#765, #766, #767). It merges the updated telemetry/composer-counts (8f45346), which already merges main at bce75dc, so this one merge brings in both. Two files conflicted: - vitest.fast.config.js: #762's merge set FAST_TEST_FILES to 158, and this branch had raised it from 170 to 174 for its four test files. The merged list collects 162 files (vitest list --filesOnly, and the fast run's JSON report), so FAST_TEST_FILES is 162. - docs/specs/ARCHITECTURE.md: only the generated diagram conflicted. Took main's and regenerated it with npm run docs:diagram, which adds this branch's src/core/telemetry.js to core's module count and its imports to the audio, components and live edges into core. One break git could not see: main (#767) deleted worker/live-realtime.mjs, whose readText worker/count.mjs imports, and moved readText unchanged into worker/mcp-server.mjs as a private function. worker/mcp-server.mjs now exports it and worker/count.mjs imports it from there. It is the same function count.mjs used before, so the 512-byte and UTF-8 limits on a beacon are unchanged. PRIVACY.md, public/PRIVACY.md, public/privacy.html, src/audio/engine.js, src/audio/engine.lifecycle.test.js, src/components/Home.js, src/components/read/Chamber.js, src/core/error-boundary.js, src/live/host/LiveHost.js, src/live/host/LiveHost.test.js, worker/index.mjs, worker/production-config.test.js and wrangler.production.jsonc merged without conflict; npm run verify:legal confirms the generated legal copies still match PRIVACY.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VHrJ4hmTtR1RFNpc215EK7
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Draft until Mateo's privacy decision, Seth's handle and Mateo's sign-off. RISE's codex-feedback workflow would otherwise auto-merge it.
This builds on #762 (
telemetry/composer-counts), because it reuses that branch'sworker/telemetry.mjswriter andtelemetry/events.jsonallowlist. Until #762 merges, this diff also contains its commitc64f1bb. This packet is the second commit.Update 2026-11-04:
mainmerged in (00ef244), through the updated #762 branch. Main deletedworker/live-realtime.mjs, whichworker/count.mjsimportedreadTextfrom, and movedreadTextunchanged intoworker/mcp-server.mjsas an unexported function.mcp-server.mjsnow exports it andcount.mjsimports it from there; without that the Worker does not bundle. (A small shared module would do as well; this is the smallest change.)FAST_TEST_FILESis now 162, and the architecture diagram was regenerated withnpm run docs:diagram.Checks on
00ef244: fast suite 162 files, 3821 tests (3817 passed, 4 skipped, 0 failed); full unit suite 608 files, 9301 tests (9293 passed, 8 skipped, 0 failed);vite build; first load 62.8 of 64.0 KB brotli; e2e gate 72 passed, 3 skipped; both wrangler dry runs, production listingEVENTSandCOUNT_LIMITER;CIgreen on GitHub. PRIVACY.md's "Last updated" is 2 November 2026 on main and still needs the date these sentences publish. The test counts below describe the PR as first opened.What changed
Server
worker/count.mjs(new):POST /api/count, dispatched fromworker/index.mjs. A beacon is written only when every one of these holds:Originequals the request's own origin;CF-Connecting-IPand is withinCOUNT_LIMITER, which is charged before the body is read;{ "v": 1, "e": <event>, "f": { ... } };browseranddevice), so the connector's events are refused;fholds exactly that event's other fields.The server derives
browser(chrome|safari|firefox|edge|other) anddevice(mobile|desktop) from the User-Agent and keeps nothing else of it. Numbers are fitted:minutesgoes to the least of 1, 2, 5, 10, 20, 30, 60 or 120 at or above it, and 120 for anything longer;msis rounded to 50 and held within 0..60000.Anything else is dropped. Every request is answered 204 with no body, and nothing is logged. With no limiter or a failing limiter, nothing is written.
worker/telemetry.mjs: the writer now holds a number to what the allowlist gives its field (one of itsvalues, or a multiple ofstepup tomax), so a fitted number off its list cannot be written either.telemetry/events.json: addsrise.reading.begun{browser, device, source, mode},rise.reading.ended{browser, device, end; minutes},rise.app.failed{browser, device, class} andrise.reading.first_word{browser, device; ms}, all owned by Reader.browseranddeviceare markedderived.wrangler.production.jsonc: aratelimitsbindingCOUNT_LIMITER, 60 per 60 seconds, namespace2026103102. No other configuration gets it;worker/production-config.test.jspins that.Client
src/core/telemetry.js(new):count(event, fields)callsnavigator.sendBeacon('/api/count', new Blob([json], { type: 'application/json' })). It sends only whenlocation.hostname === 'rise.syberlabs.io'andtop === globalThis, which rules out frames, so nothing is sent from inside ChatGPT, Claude or any embed. It uses no storage, cookie or queue, and never throws.src/core/begin-steps.js:markBegin()andtakeBegin(), aperformancemark that Home's Begin leaves for the first word to be measured from. It is read once and then cleared.Call sites wired
src/components/read/Chamber.js:rise.reading.begunat the firstplayingstate, which covers autostart, the Chamber's own Begin and a host that plays. A reading that opens as a page is counted as it opens.source:importwhen the provenance islocal-text;publicwhen the session keeps apublicPath(route-url.js's own definition);libraryforlibrary-work,library-division,school,chapel-bookorkeystone;other.rise.reading.ended:yeswhen the Player completes;nowhen the Chamber is destroyed or the page is hidden for good (pagehide, added at begin and removed on destroy). Counted once per reading.minutesis wall time since begin.rise.reading.first_word: from the press of Home's Begin ([data-home="enter"]) to the first atom spoken or shown, meaning not concealed, not an authored boundary, and with text. This is R2's interval, ase2e/home-begin.spec.jsmeasures it. The mark is cleared when Home's launch fails, when a Home reading is left before its first word, and when the reading waits for a second press (no audio clock).unhandled: the error boundary's windowerrorandunhandledrejectionlisteners, counted once per page load. An error that recurs every frame would otherwise be a Worker request every frame, which the per-address limiter drops but still pays for.audio_failed: the audio engine'sinit()failure, beside its existingonUnavailablecall.speech_unavailableandwebgl_unavailable: the Live venue's capability record, read once perLiveHost(speechOutput === 'none',webgl2 === false).Call sites left out, and why
kev_load_failed: Kev loads only on the EnterpRise page.src/enterprise/boundary.test.jsholds that EnterpRise imports nothing outsidesrc/enterprise. Wiring it needs either that boundary opened tosrc/core/telemetry.jsor a second copy of the sender inside EnterpRise, and that is the owner's decision. The value stays in the allowlist; nothing sends it, and the policy does not list it.WEBGL2_UNAVAILABLE, the Home atmosphere'shardwareWebGL2()also rejects software renderers and runs on every Home visit, and the wormhole drops to 2D. Counting those would count engines rather than browsers.audio_failed.LiveHost.Policy
PRIVACY.md: one paragraph at the end of section 4's "Page-view counts", naming the events, the derived browser and device, no cookie, no storage, no identifier, no reading text or typing, the per-address limit, nothing sent from frames, and three months' retention.public/PRIVACY.mdandpublic/privacy.htmlwere regenerated withnpm run build:legal, anddocs/specs/ARCHITECTURE.md's generated diagram withnpm run docs:diagram.Why
This is the approved SyberLabs telemetry design (telemetry.v1): reader-side counts as closed enums and bucketed numbers, never text or identifiers. They show where readings start, whether they finish, how long Begin takes and what fails.
How
release,browseranddeviceare derivedreleaseis'unknown', as in Composer: count each Current the door admits or refuses, by host app and refusal code #762. CI writes the commit intodist/release.txtafter wrangler bundles the Worker, so naming it needs a deploy-time variable, which is a workflow change.browseranddevicecome from the User-Agent, read inworker/count.mjs; the string is never written.Edg/,EdgA/,EdgiOS/); Opera, Samsung, Yandex or UC asother; Firefox (Firefox/,FxiOS/); Chrome (Chrome/,CriOS/); then Safari (Version/…Safari/, not Android).mobileforMobi,Android,iPhone,iPadoriPod; otherwisedesktop.Tests
CIcheck runs (npx vitest run --config vitest.fast.config.js): 174 files collected, equal toFAST_TEST_FILES. 3958 tests: 3954 passed, 4 skipped, 0 failed. On Composer: count each Current the door admits or refuses, by host app and refusal code #762's branch the same suite was 170 files and 3927 tests (3923 passed, 4 skipped).worker/count.test.js(11 tests): written fields and order; reader events only; User-Agent table; number fitting; malformed and unlisted bodies dropped; foreign, absent and near Origins dropped; POST only; 512-byte limit and non-JSON or non-UTF-8 bodies; the limiter charged before the body, over-limit, missing, throwing and no-address cases; a byte-identical 204 with the binding present, absent and throwing, with nothing logged; the route reached through the Worker.src/core/telemetry.test.js(7 tests): the beacon's shape; no send off-host or in a frame; no storage, cookie or queue; never throws; the Begin mark taken once, and absent without a timeline.src/components/read/Chamber.counts.test.js(9 tests): begin once, with source and mode; end yes, no and onpagehide, once each; no end without a begin; first word only after Home's Begin; no stale interval left behind.src/core/error-boundary.test.js(1 test): unhandled errors counted once per page.worker/telemetry.test.js: the allowlist's six events; numbers with values or a step; numbers off their list refused (24 tests now).src/audio/engine.lifecycle.test.js: the init failure is counted.src/live/host/LiveHost.test.js: the capability counts.worker/production-config.test.js:COUNT_LIMITERin production only.node scripts/ci-hygiene.mjs,npm run roadmap:check,npm run test:roadmap,npm run security:compat;npm run docs:diagram, unchanged on a re-run;npm run verify:legal,npx vite build;npm run measure:first-load: 63.5 of 64.0 KB brotli, from 63.4;wrangler deploy --dry-runbundles. Production listsenv.COUNT_LIMITER (60 requests/60s)besideenv.EVENTS.wrangler dev --localof the production configuration answered a valid beacon, a foreign-origin one, a 2 KB one and a GET with 204 and an empty body, with no runtime error.npm run test:e2e:gate), run locally on this tree, which includes Composer: count each Current the door admits or refuses, by host app and refusal code #762: 72 passed, 3 skipped, 0 failed, in 3.6 minutes. This session's image ships Chromium build 1243, while Playwright 1.61.1 looks for build 1228. The run therefore pointedPLAYWRIGHT_BROWSERS_PATHat a scratch folder that links 1228 to the installed 1243; nothing in the repository changed for it. CI runs the gate with its own browser.npm run security:audit.ratelimitsshape (period10 or 60) was checked against the installed wrangler 4.144.0 schema and its dry run instead.Needs a human key
wrangler.production.jsonc(COUNT_LIMITER, plusEVENTSfrom Composer: count each Current the door admits or refuses, by host app and refusal code #762) floors at keys. Merging deploys rise.syberlabs.io.Deliberately not done, and limitations
Originfrom outside a browser and write allowlisted values. No personal data is at stake, and the per-address limit bounds it. Every beacon is a Worker request, even when it is dropped.minutesincludes time the tab was hidden.end: yesmeans the Player reached the last word, so a reading finished in the Page view is counted asno.unhandledcounts page loads that met an error, not errors. It includes benign browser errors (a ResizeObserver loop, for example) and extensions' errors.desktop; Android tablets count asmobile; Brave and webviews count aschrome./api/countexists on every configuration, but writes only whereEVENTSandCOUNT_LIMITERare both bound (production).Generated by Claude Code