Uh oh!
There was an error while loading. Please reload this page.
runtime-config: serve the client error-reporting DSN from the server - #12697
Conversation
ObjectStack's users consume a prebuilt Console and cannot set build-time keys, so the two-key gate shipped in #10805 -- a build-time VITE_SENTRY_DSN AND a runtime permission -- left a self-hosting operator unable to enable client error reporting at all. The DSN now travels on GET /api/v1/runtime/config together with the closed set of knobs that must accompany it, and the permission boolean it replaces is removed rather than paralleled. The DSN's presence IS the grant: no second boolean, so the two silent dead states the split shape had ("permission on, no DSN" / "DSN in, permission off") cannot exist. Fail-closed survives the collapse for free, because absence of a source is not a value that can be misread. Malformed values are refused at mount and never coerced, with every refusal landing on the safer value: a bad DSN withholds the whole block, a bad sample rate falls back to its default. A DSN carrying a secret after the public key is refused outright -- this payload is read by every browser that loads the Console. OS_CLOUD_URL=off still refuses to serve any sink. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DKWDdUJ2XNRESVVWUvcpnh
📓 Docs Drift CheckThis PR changes 1 package(s): 1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
What this run could not see
Coarse fallback — 3 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 2e46570f0ca71a6993ea5ec0d303d92cb6b00eb6 && git checkout 2e46570f0ca71a6993ea5ec0d303d92cb6b00eb6
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 284fc22d8834f2c9a3530592d6614e0eb6a28590 a44e75fec735a3a333f7783517baa4592217f614 && git checkout -B drift-repro 284fc22d8834f2c9a3530592d6614e0eb6a28590 && git merge --no-ff a44e75fec735a3a333f7783517baa4592217f614
node scripts/docs-audit/affected-docs.mjs --json 284fc22d8834f2c9a3530592d6614e0eb6a28590
|
Cross-repo pair for #12681.
Both are draft, and neither closes the card on its own — the card is done when both land. Landing ORDER does not matter: an old client meeting this server reads an absent Generated by Claude Code Generated by Claude Code |
os-zhuang
left a comment
There was a problem hiding this comment.
PM contract-tier verification record (dispatching seat for #12681; this seat shares the PR's author identity, so this is a COMMENT review — the queue does not require an approval, and packages/cloud-connection is not a governed surface; the maintainer's ruling on the card IS the design authority here, transcribed verbatim in the diff's own doc comments).
Read the full 2113-line diff. Contract-facing findings, all positive:
- The ruled shape is implemented exactly:
telemetry.errorReportingas one closed object, DSN presence = grant, boolean REPLACED in the same change (code, payload, reader, env var, plugin option, tests, docs row, README, pending changeset superseded with an adr-0087 not-required annotation reasoned from its unpublished status). The{"telemetry":{}}vs no-telemetry-key distinction gives one curl the diagnosis. - Three judgments beyond the card's text, each correct: (1) a secret-bearing DSN is refused outright — this payload is public to every browser, so a legacy secret DSN would be published to every visitor; (2) quoted DSNs in boot logs are key-redacted; (3)
VITE_SENTRY_RELEASEstays build-time with the right reason (a release must match the build's uploaded source maps — a fact no server can know). - Per-field precedence (host option for one knob cannot discard the operator's DSN) is pinned — the exact quiet two-knob failure class this card deletes.
- All refusals reported, not only the first, and knob refusals still reported when the DSN is missing too — the operator with two mistakes hears about both.
- The consumer reader deliberately does NOT re-run the producer's shape check (avoids two-places-disagreeing one layer down), keeping only the secret-DSN guard as the last line against a third-party host. Right trade.
- Sub-decisions flagged in the dev's report are ratified as within the ruled scope: the env family keeps the
CLIENT_ERROR_REPORTINGconvention (the card's own "per repo convention" deferral; vendor-neutral), andreplaysOnErrorSampleRatejoins the closed set (the more privacy-bearing rate could not stay stranded build-time). Both trivially renameable before release if the maintainer prefers otherwise.
Cross-repo pair: objectui#6603 (verified in its own review). Landing order safe in both directions, pinned by test on both sides. Landing via the normal queue.
Generated by Claude Code
Uh oh!
There was an error while loading. Please reload this page.
Part of #12681 — the server half. The client half is objectstack-ai/objectui, branch
claude/issue-12681-runtime-dsn; neither PR closes the card on its own.What this changes
GET /api/v1/runtime/confignow serves the Console's client error-reporting sink — the DSN itself plus the closed set of knobs that must travel with it — so a self-hosting operator configures telemetry on the server, in one place, with no frontend rebuild.{ "telemetry": { "errorReporting": { "dsn": "https://PUBLIC_KEY@o1.ingest.sentry.io/42", "sendDefaultPii": false, "environment": "production", "tracesSampleRate": 0.1, "replaysOnErrorSampleRate": 0 } } }Unconfigured, the runtime serves
{"telemetry":{}}.The maintainer's ruling on the card, verbatim and untranslated:
The DSN's presence IS the grant
No second boolean. That is not shorthand — it removes the failure mode the two-key shape had: with a permission and a source configured in different places, "permission on, no DSN" and "DSN in, permission off" are two silent dead states that look identical from the browser. One knob cannot disagree with itself.
Fail-closed survives the collapse for free, and more robustly than the boolean managed. The grant is now "a non-empty DSN reached me", so an older runtime, a third-party host, a 404, a network error, a malformed body and a not-yet-arrived payload all carry no DSN and therefore deny. A boolean needed
=== trueplus a written argument about why a negativedisabledkey would have been vacuous; absence of a source is not a value that can be misread.Two absences stay distinguishable with one
curl, which is why thetelemetryblock is always emitted:{"telemetry":{}}means "this runtime knows the key and has no DSN"; notelemetrykey at all means the payload did not come from a runtime that knows it. Both deny; only one is fixable by the operator reading it.The env knobs
OS_TELEMETRY_CLIENT_ERROR_REPORTING_DSNOS_TELEMETRY_CLIENT_ERROR_REPORTING_SEND_DEFAULT_PIIfalseOS_TELEMETRY_CLIENT_ERROR_REPORTING_ENVIRONMENTOS_TELEMETRY_CLIENT_ERROR_REPORTING_TRACES_SAMPLE_RATE0.1OS_TELEMETRY_CLIENT_ERROR_REPORTING_REPLAY_SAMPLE_RATE0Named for the narrow thing they configure rather than for the vendor, keeping the
..._CLIENT_ERROR_REPORTING_...family the retired boolean established — so the replacement reads as the same knob rather than a new one, and a self-hosted or DSN-compatible sink is not misdescribed. The card suggested anOS_TELEMETRY_SENTRY_DSN-shaped name "per repo convention"; the convention in this namespace is the existing family, so that is what was followed.Host option
clientErrorReporting: { … }wins over the matching env var per field, matching every branding key above it in the constructor. Whole-object replacement was rejected: a host passing onlysendDefaultPiiwould silently discard the operator's DSN, which is the class of quiet two-knob failure this card deletes.Malformed is refused at mount, never coerced
Every refusal lands on the safer value, and that is what decides whether a bad knob takes down the block or only itself:
https://PUBLIC_KEY@HOST/PROJECT_IDURL withholds the whole block — there is no safe default for a source;false. Silencing error reporting over a typo in a volume knob would be strictness pointed away from the hazard.Quoted DSNs in the warnings are key-redacted (
redactDsn): boot logs travel further than the configuration they quote, and the operator needs the shape of what they typed, never the key. Every wrong knob is reported, not just the first.OS_CLOUD_URL=off(ornone/local/disabled) still refuses to serve any sink and warns about it — the copied-hosted-config-onto-an-air-gapped-box shape.Replaced, not paralleled
telemetry.allowClientErrorReportingis removed in this same change — code, payload, tests, theenvironment-variables.mdxrow, the README section, and its pending changeset. No dual-spelling window. It landed days ago, is unreleased (@objectstack/cloud-connection@17.2.0mentions it nowhere and its changeset was still pending), and no deployment consumes it, so its changeset is superseded rather than shipping a feature and its removal in the same release notes.OS_TELEMETRY_CLIENT_ERROR_REPORTING_ENABLED=trueOS_TELEMETRY_CLIENT_ERROR_REPORTING_DSN=https://PUBLIC_KEY@HOST/PROJECT_IDnew RuntimeConfigPlugin({ allowClientErrorReporting: true })new RuntimeConfigPlugin({ clientErrorReporting: { dsn: '…' } })isClientErrorReportingAllowed(payload): booleanreadClientErrorReporting(payload): ClientErrorReportingConfig or nullCLIENT_ERROR_REPORTING_ENVCLIENT_ERROR_REPORTING_DSN_ENVand four siblingsThe ADR-0087 disposition is recorded in the changeset:
not-required (unpublished).No
.objectui-shapin bump is needed. objectui has no dependency on@objectstack/cloud-connection— measured, not assumed: nopackage.jsonthere names it, so removing these exports cannot break the pinned Console build.Compatibility — any landing order is safe
telemetry.allowClientErrorReporting, its=== truetest denies ⇒ off.Neither half can turn reporting on by itself, so the two PRs can land in either order with no window in which anything sends unexpectedly. Pinned by test on both sides.
Validation
All of the following ran on
a44e75fe(this branch's head), exit codes captured before any pipe.pnpm --filter @objectstack/cloud-connection build && … test— 27 files, 340 tests passed; the build's DTS emission is this package's real type gate (it has notypecheckscript —ERR_PNPM_RECURSIVE_RUN_NO_SCRIPTis a missing script, not a failure).pnpm --filter @objectstack/cli exec vitest run test/serve-marketplace-offline-runtime-config.test.ts— 14 passed (after building the CLI dependency closure; the first run'sFailed to resolve entry for @objectstack/plugin-emailwas an unbuilt dependency, not a verdict).node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack(no hand-written path list), then run:check:adr-0087-registration,check:empty-changeset,check:changeset-no-major,check:changeset-gate-self-tests,check:objectui-changeset,check:pm-half-states,check:nul-bytes,check:route-envelope,release-rehearsal-clone --self-test, all twelve doc families, all sixteen test-kind/package families,check:skill-examples,check:i18n— all green.--format json, 5 files judged). The narrowing is a measurement, not a skip: this repo's ESLint config sets noparserOptions.projectand no typed rules (it says so ateslint.config.mjs:328), so no untouched file's verdict can move because of this diff.Reverse-verified — both new pins were shown able to fail, each mutation confirmed on disk by before/after
grep -con the anchored text plus agit hash-objectdifference, and each restore proved by an emptygit diff HEADand a blob hash matchingHEAD:OS_CLOUD_URL=offceiling inert ⇒ 8 failed / 85 passed.Declared NOT MEASURED
node scripts/pm/check-half-states.mjs— exit 3,PREREQUISITE NOT MET: no valid GitHub credential in this container. Its own output states nothing was swept. (pnpm check:pm-half-states, the self-test form, passed.)pnpm check:i18n-coverage—COULD NOT MEASURE: 1 of 12 configs could not lint because@objectstack/connector-mcphas no build output in this worktree; it needs a whole-repopnpm build. Unrelated to this diff (no i18n keys change).pnpm check:type-check-debt— needs the full workspace closure built; not run.Generated by Claude Code
Generated by Claude Code