runtime-config: serve the client error-reporting DSN from the server - #12697

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-12681-runtime-dsn
Aug 27, 2026
Merged

runtime-config: serve the client error-reporting DSN from the server#12697
os-zhuang merged 1 commit into
mainfrom
claude/issue-12681-runtime-dsn

Conversation

@claude

@claudeclaudeBot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

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/config now 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 === true plus a written argument about why a negative disabled key 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 the telemetry block is always emitted: {"telemetry":{}} means "this runtime knows the key and has no DSN"; no telemetry key 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

VariableDefault
OS_TELEMETRY_CLIENT_ERROR_REPORTING_DSN— (unset ⇒ no reporting)
OS_TELEMETRY_CLIENT_ERROR_REPORTING_SEND_DEFAULT_PIIfalse
OS_TELEMETRY_CLIENT_ERROR_REPORTING_ENVIRONMENT
OS_TELEMETRY_CLIENT_ERROR_REPORTING_TRACES_SAMPLE_RATE0.1
OS_TELEMETRY_CLIENT_ERROR_REPORTING_REPLAY_SAMPLE_RATE0

Named 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 an OS_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 only sendDefaultPii would 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:

  • a DSN that is not an https://PUBLIC_KEY@HOST/PROJECT_ID URL withholds the whole block — there is no safe default for a source;
  • a DSN carrying a secret after the public key is refused outright. This payload is read by every browser that loads the Console, so a legacy secret-bearing DSN would publish that secret to every visitor while looking entirely ordinary. This one check is mirrored on the consumer side too, because its failure mode is a leak rather than a misconfiguration;
  • a bad sample rate falls back to its documented default, and a bad PII spelling to 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 (or none / 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.allowClientErrorReporting is removed in this same change — code, payload, tests, the environment-variables.mdx row, the README section, and its pending changeset. No dual-spelling window. It landed days ago, is unreleased (@objectstack/cloud-connection@17.2.0 mentions 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.

FROMTO
OS_TELEMETRY_CLIENT_ERROR_REPORTING_ENABLED=trueOS_TELEMETRY_CLIENT_ERROR_REPORTING_DSN=https://PUBLIC_KEY@HOST/PROJECT_ID
new RuntimeConfigPlugin({ allowClientErrorReporting: true })new RuntimeConfigPlugin({ clientErrorReporting: { dsn: '…' } })
isClientErrorReportingAllowed(payload): booleanreadClientErrorReporting(payload): ClientErrorReportingConfig or null
CLIENT_ERROR_REPORTING_ENVCLIENT_ERROR_REPORTING_DSN_ENV and four siblings

The ADR-0087 disposition is recorded in the changeset: not-required (unpublished).

No .objectui-sha pin bump is needed. objectui has no dependency on @objectstack/cloud-connection — measured, not assumed: no package.json there names it, so removing these exports cannot break the pinned Console build.

Compatibility — any landing order is safe

  • Old client + new server: the old Console reads an absent telemetry.allowClientErrorReporting, its === true test denies ⇒ off.
  • New client + old server: the new Console reads an absent DSN ⇒ 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 && … test27 files, 340 tests passed; the build's DTS emission is this package's real type gate (it has no typecheck script — ERR_PNPM_RECURSIVE_RUN_NO_SCRIPT is a missing script, not a failure).
  • pnpm --filter @objectstack/cli exec vitest run test/serve-marketplace-offline-runtime-config.test.ts14 passed (after building the CLI dependency closure; the first run's Failed to resolve entry for @objectstack/plugin-email was an unbuilt dependency, not a verdict).
  • Gate families derived with 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:i18nall green.
  • ESLint on the five changed TS files: 0 errors, 0 warnings (--format json, 5 files judged). The narrowing is a measurement, not a skip: this repo's ESLint config sets no parserOptions.project and no typed rules (it says so at eslint.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 -c on the anchored text plus a git hash-object difference, and each restore proved by an empty git diff HEAD and a blob hash matching HEAD:

  • removing the secret-bearing-DSN guard from the consumer reader ⇒ 1 failed / 92 passed;
  • making the OS_CLOUD_URL=off ceiling 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-coverageCOULD NOT MEASURE: 1 of 12 configs could not lint because @objectstack/connector-mcp has no build output in this worktree; it needs a whole-repo pnpm build. Unrelated to this diff (no i18n keys change).
  • pnpm check:type-check-debt — needs the full workspace closure built; not run.
  • CI convergence is not awaited, per the standing dispatch contract.

Generated by Claude Code


Generated by Claude Code

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
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/cloud-connection, touching 41 documentable anchor(s). ⚠️2 changed file(s) yielded no anchor (packages/cloud-connection/README.md, packages/cloud-connection/src/index.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/deployment/environment-variables.mdx(via /api/v1/runtime/config (route))
What this run could not see
  • 2 changed file(s) yielded no anchor (packages/cloud-connection/README.md, packages/cloud-connection/src/index.ts) — pages documenting those are invisible to this run
  • 10 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 3 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 284fc22d8834f2c9a3530592d6614e0eb6a28590packageMentionDocs.

Which tree this was computed on

This run read content/docs from 2e46570f0ca71a6993ea5ec0d303d92cb6b00eb6 — the merge of head a44e75fec735a3a333f7783517baa4592217f614 into base 284fc22d8834f2c9a3530592d6614e0eb6a28590, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 284fc22d8834f2c9a3530592d6614e0eb6a28590 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@claude

claudeBot commented Aug 27, 2026

Copy link
Copy Markdown
ContributorAuthor

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 allowClientErrorReporting and denies; a new client meeting an old server reads an absent DSN and stays off. Neither half can turn reporting on by itself, and both directions are pinned by test.

Generated by Claude Code


Generated by Claude Code

@os-zhuangos-zhuang left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.errorReporting as 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_RELEASE stays 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_REPORTING convention (the card's own "per repo convention" deferral; vendor-neutral), and replaysOnErrorSampleRate joins 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

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/xlteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-zhuang@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

runtime-config: serve the client error-reporting DSN from the server - #12697

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-12681-runtime-dsn
Aug 27, 2026
Merged

runtime-config: serve the client error-reporting DSN from the server#12697
os-zhuang merged 1 commit into
mainfrom
claude/issue-12681-runtime-dsn

Conversation

@claude

@claudeclaudeBot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

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/config now 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 === true plus a written argument about why a negative disabled key 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 the telemetry block is always emitted: {"telemetry":{}} means "this runtime knows the key and has no DSN"; no telemetry key 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

VariableDefault
OS_TELEMETRY_CLIENT_ERROR_REPORTING_DSN— (unset ⇒ no reporting)
OS_TELEMETRY_CLIENT_ERROR_REPORTING_SEND_DEFAULT_PIIfalse
OS_TELEMETRY_CLIENT_ERROR_REPORTING_ENVIRONMENT
OS_TELEMETRY_CLIENT_ERROR_REPORTING_TRACES_SAMPLE_RATE0.1
OS_TELEMETRY_CLIENT_ERROR_REPORTING_REPLAY_SAMPLE_RATE0

Named 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 an OS_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 only sendDefaultPii would 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:

  • a DSN that is not an https://PUBLIC_KEY@HOST/PROJECT_ID URL withholds the whole block — there is no safe default for a source;
  • a DSN carrying a secret after the public key is refused outright. This payload is read by every browser that loads the Console, so a legacy secret-bearing DSN would publish that secret to every visitor while looking entirely ordinary. This one check is mirrored on the consumer side too, because its failure mode is a leak rather than a misconfiguration;
  • a bad sample rate falls back to its documented default, and a bad PII spelling to 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 (or none / 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.allowClientErrorReporting is removed in this same change — code, payload, tests, the environment-variables.mdx row, the README section, and its pending changeset. No dual-spelling window. It landed days ago, is unreleased (@objectstack/cloud-connection@17.2.0 mentions 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.

FROMTO
OS_TELEMETRY_CLIENT_ERROR_REPORTING_ENABLED=trueOS_TELEMETRY_CLIENT_ERROR_REPORTING_DSN=https://PUBLIC_KEY@HOST/PROJECT_ID
new RuntimeConfigPlugin({ allowClientErrorReporting: true })new RuntimeConfigPlugin({ clientErrorReporting: { dsn: '…' } })
isClientErrorReportingAllowed(payload): booleanreadClientErrorReporting(payload): ClientErrorReportingConfig or null
CLIENT_ERROR_REPORTING_ENVCLIENT_ERROR_REPORTING_DSN_ENV and four siblings

The ADR-0087 disposition is recorded in the changeset: not-required (unpublished).

No .objectui-sha pin bump is needed. objectui has no dependency on @objectstack/cloud-connection — measured, not assumed: no package.json there names it, so removing these exports cannot break the pinned Console build.

Compatibility — any landing order is safe

  • Old client + new server: the old Console reads an absent telemetry.allowClientErrorReporting, its === true test denies ⇒ off.
  • New client + old server: the new Console reads an absent DSN ⇒ 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 && … test27 files, 340 tests passed; the build's DTS emission is this package's real type gate (it has no typecheck script — ERR_PNPM_RECURSIVE_RUN_NO_SCRIPT is a missing script, not a failure).
  • pnpm --filter @objectstack/cli exec vitest run test/serve-marketplace-offline-runtime-config.test.ts14 passed (after building the CLI dependency closure; the first run's Failed to resolve entry for @objectstack/plugin-email was an unbuilt dependency, not a verdict).
  • Gate families derived with 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:i18nall green.
  • ESLint on the five changed TS files: 0 errors, 0 warnings (--format json, 5 files judged). The narrowing is a measurement, not a skip: this repo's ESLint config sets no parserOptions.project and no typed rules (it says so at eslint.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 -c on the anchored text plus a git hash-object difference, and each restore proved by an empty git diff HEAD and a blob hash matching HEAD:

  • removing the secret-bearing-DSN guard from the consumer reader ⇒ 1 failed / 92 passed;
  • making the OS_CLOUD_URL=off ceiling 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-coverageCOULD NOT MEASURE: 1 of 12 configs could not lint because @objectstack/connector-mcp has no build output in this worktree; it needs a whole-repo pnpm build. Unrelated to this diff (no i18n keys change).
  • pnpm check:type-check-debt — needs the full workspace closure built; not run.
  • CI convergence is not awaited, per the standing dispatch contract.

Generated by Claude Code


Generated by Claude Code

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
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/cloud-connection, touching 41 documentable anchor(s). ⚠️2 changed file(s) yielded no anchor (packages/cloud-connection/README.md, packages/cloud-connection/src/index.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/deployment/environment-variables.mdx(via /api/v1/runtime/config (route))
What this run could not see
  • 2 changed file(s) yielded no anchor (packages/cloud-connection/README.md, packages/cloud-connection/src/index.ts) — pages documenting those are invisible to this run
  • 10 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 3 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 284fc22d8834f2c9a3530592d6614e0eb6a28590packageMentionDocs.

Which tree this was computed on

This run read content/docs from 2e46570f0ca71a6993ea5ec0d303d92cb6b00eb6 — the merge of head a44e75fec735a3a333f7783517baa4592217f614 into base 284fc22d8834f2c9a3530592d6614e0eb6a28590, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 284fc22d8834f2c9a3530592d6614e0eb6a28590 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@claude

claudeBot commented Aug 27, 2026

Copy link
Copy Markdown
ContributorAuthor

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 allowClientErrorReporting and denies; a new client meeting an old server reads an absent DSN and stays off. Neither half can turn reporting on by itself, and both directions are pinned by test.

Generated by Claude Code


Generated by Claude Code

@os-zhuangos-zhuang left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.errorReporting as 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_RELEASE stays 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_REPORTING convention (the card's own "per repo convention" deferral; vendor-neutral), and replaysOnErrorSampleRate joins 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

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/xlteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-zhuang@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

runtime-config: serve the client error-reporting DSN from the server - #12697

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-12681-runtime-dsn
Aug 27, 2026
Merged

runtime-config: serve the client error-reporting DSN from the server#12697
os-zhuang merged 1 commit into
mainfrom
claude/issue-12681-runtime-dsn

Conversation

@claude

@claudeclaudeBot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

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/config now 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 === true plus a written argument about why a negative disabled key 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 the telemetry block is always emitted: {"telemetry":{}} means "this runtime knows the key and has no DSN"; no telemetry key 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

VariableDefault
OS_TELEMETRY_CLIENT_ERROR_REPORTING_DSN— (unset ⇒ no reporting)
OS_TELEMETRY_CLIENT_ERROR_REPORTING_SEND_DEFAULT_PIIfalse
OS_TELEMETRY_CLIENT_ERROR_REPORTING_ENVIRONMENT
OS_TELEMETRY_CLIENT_ERROR_REPORTING_TRACES_SAMPLE_RATE0.1
OS_TELEMETRY_CLIENT_ERROR_REPORTING_REPLAY_SAMPLE_RATE0

Named 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 an OS_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 only sendDefaultPii would 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:

  • a DSN that is not an https://PUBLIC_KEY@HOST/PROJECT_ID URL withholds the whole block — there is no safe default for a source;
  • a DSN carrying a secret after the public key is refused outright. This payload is read by every browser that loads the Console, so a legacy secret-bearing DSN would publish that secret to every visitor while looking entirely ordinary. This one check is mirrored on the consumer side too, because its failure mode is a leak rather than a misconfiguration;
  • a bad sample rate falls back to its documented default, and a bad PII spelling to 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 (or none / 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.allowClientErrorReporting is removed in this same change — code, payload, tests, the environment-variables.mdx row, the README section, and its pending changeset. No dual-spelling window. It landed days ago, is unreleased (@objectstack/cloud-connection@17.2.0 mentions 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.

FROMTO
OS_TELEMETRY_CLIENT_ERROR_REPORTING_ENABLED=trueOS_TELEMETRY_CLIENT_ERROR_REPORTING_DSN=https://PUBLIC_KEY@HOST/PROJECT_ID
new RuntimeConfigPlugin({ allowClientErrorReporting: true })new RuntimeConfigPlugin({ clientErrorReporting: { dsn: '…' } })
isClientErrorReportingAllowed(payload): booleanreadClientErrorReporting(payload): ClientErrorReportingConfig or null
CLIENT_ERROR_REPORTING_ENVCLIENT_ERROR_REPORTING_DSN_ENV and four siblings

The ADR-0087 disposition is recorded in the changeset: not-required (unpublished).

No .objectui-sha pin bump is needed. objectui has no dependency on @objectstack/cloud-connection — measured, not assumed: no package.json there names it, so removing these exports cannot break the pinned Console build.

Compatibility — any landing order is safe

  • Old client + new server: the old Console reads an absent telemetry.allowClientErrorReporting, its === true test denies ⇒ off.
  • New client + old server: the new Console reads an absent DSN ⇒ 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 && … test27 files, 340 tests passed; the build's DTS emission is this package's real type gate (it has no typecheck script — ERR_PNPM_RECURSIVE_RUN_NO_SCRIPT is a missing script, not a failure).
  • pnpm --filter @objectstack/cli exec vitest run test/serve-marketplace-offline-runtime-config.test.ts14 passed (after building the CLI dependency closure; the first run's Failed to resolve entry for @objectstack/plugin-email was an unbuilt dependency, not a verdict).
  • Gate families derived with 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:i18nall green.
  • ESLint on the five changed TS files: 0 errors, 0 warnings (--format json, 5 files judged). The narrowing is a measurement, not a skip: this repo's ESLint config sets no parserOptions.project and no typed rules (it says so at eslint.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 -c on the anchored text plus a git hash-object difference, and each restore proved by an empty git diff HEAD and a blob hash matching HEAD:

  • removing the secret-bearing-DSN guard from the consumer reader ⇒ 1 failed / 92 passed;
  • making the OS_CLOUD_URL=off ceiling 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-coverageCOULD NOT MEASURE: 1 of 12 configs could not lint because @objectstack/connector-mcp has no build output in this worktree; it needs a whole-repo pnpm build. Unrelated to this diff (no i18n keys change).
  • pnpm check:type-check-debt — needs the full workspace closure built; not run.
  • CI convergence is not awaited, per the standing dispatch contract.

Generated by Claude Code


Generated by Claude Code

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
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/cloud-connection, touching 41 documentable anchor(s). ⚠️2 changed file(s) yielded no anchor (packages/cloud-connection/README.md, packages/cloud-connection/src/index.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/deployment/environment-variables.mdx(via /api/v1/runtime/config (route))
What this run could not see
  • 2 changed file(s) yielded no anchor (packages/cloud-connection/README.md, packages/cloud-connection/src/index.ts) — pages documenting those are invisible to this run
  • 10 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 3 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 284fc22d8834f2c9a3530592d6614e0eb6a28590packageMentionDocs.

Which tree this was computed on

This run read content/docs from 2e46570f0ca71a6993ea5ec0d303d92cb6b00eb6 — the merge of head a44e75fec735a3a333f7783517baa4592217f614 into base 284fc22d8834f2c9a3530592d6614e0eb6a28590, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 284fc22d8834f2c9a3530592d6614e0eb6a28590 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@claude

claudeBot commented Aug 27, 2026

Copy link
Copy Markdown
ContributorAuthor

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 allowClientErrorReporting and denies; a new client meeting an old server reads an absent DSN and stays off. Neither half can turn reporting on by itself, and both directions are pinned by test.

Generated by Claude Code


Generated by Claude Code

@os-zhuangos-zhuang left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.errorReporting as 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_RELEASE stays 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_REPORTING convention (the card's own "per repo convention" deferral; vendor-neutral), and replaysOnErrorSampleRate joins 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

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/xlteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-zhuang@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

runtime-config: serve the client error-reporting DSN from the server - #12697

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-12681-runtime-dsn
Aug 27, 2026
Merged

runtime-config: serve the client error-reporting DSN from the server#12697
os-zhuang merged 1 commit into
mainfrom
claude/issue-12681-runtime-dsn

Conversation

@claude

@claudeclaudeBot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

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/config now 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 === true plus a written argument about why a negative disabled key 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 the telemetry block is always emitted: {"telemetry":{}} means "this runtime knows the key and has no DSN"; no telemetry key 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

VariableDefault
OS_TELEMETRY_CLIENT_ERROR_REPORTING_DSN— (unset ⇒ no reporting)
OS_TELEMETRY_CLIENT_ERROR_REPORTING_SEND_DEFAULT_PIIfalse
OS_TELEMETRY_CLIENT_ERROR_REPORTING_ENVIRONMENT
OS_TELEMETRY_CLIENT_ERROR_REPORTING_TRACES_SAMPLE_RATE0.1
OS_TELEMETRY_CLIENT_ERROR_REPORTING_REPLAY_SAMPLE_RATE0

Named 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 an OS_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 only sendDefaultPii would 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:

  • a DSN that is not an https://PUBLIC_KEY@HOST/PROJECT_ID URL withholds the whole block — there is no safe default for a source;
  • a DSN carrying a secret after the public key is refused outright. This payload is read by every browser that loads the Console, so a legacy secret-bearing DSN would publish that secret to every visitor while looking entirely ordinary. This one check is mirrored on the consumer side too, because its failure mode is a leak rather than a misconfiguration;
  • a bad sample rate falls back to its documented default, and a bad PII spelling to 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 (or none / 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.allowClientErrorReporting is removed in this same change — code, payload, tests, the environment-variables.mdx row, the README section, and its pending changeset. No dual-spelling window. It landed days ago, is unreleased (@objectstack/cloud-connection@17.2.0 mentions 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.

FROMTO
OS_TELEMETRY_CLIENT_ERROR_REPORTING_ENABLED=trueOS_TELEMETRY_CLIENT_ERROR_REPORTING_DSN=https://PUBLIC_KEY@HOST/PROJECT_ID
new RuntimeConfigPlugin({ allowClientErrorReporting: true })new RuntimeConfigPlugin({ clientErrorReporting: { dsn: '…' } })
isClientErrorReportingAllowed(payload): booleanreadClientErrorReporting(payload): ClientErrorReportingConfig or null
CLIENT_ERROR_REPORTING_ENVCLIENT_ERROR_REPORTING_DSN_ENV and four siblings

The ADR-0087 disposition is recorded in the changeset: not-required (unpublished).

No .objectui-sha pin bump is needed. objectui has no dependency on @objectstack/cloud-connection — measured, not assumed: no package.json there names it, so removing these exports cannot break the pinned Console build.

Compatibility — any landing order is safe

  • Old client + new server: the old Console reads an absent telemetry.allowClientErrorReporting, its === true test denies ⇒ off.
  • New client + old server: the new Console reads an absent DSN ⇒ 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 && … test27 files, 340 tests passed; the build's DTS emission is this package's real type gate (it has no typecheck script — ERR_PNPM_RECURSIVE_RUN_NO_SCRIPT is a missing script, not a failure).
  • pnpm --filter @objectstack/cli exec vitest run test/serve-marketplace-offline-runtime-config.test.ts14 passed (after building the CLI dependency closure; the first run's Failed to resolve entry for @objectstack/plugin-email was an unbuilt dependency, not a verdict).
  • Gate families derived with 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:i18nall green.
  • ESLint on the five changed TS files: 0 errors, 0 warnings (--format json, 5 files judged). The narrowing is a measurement, not a skip: this repo's ESLint config sets no parserOptions.project and no typed rules (it says so at eslint.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 -c on the anchored text plus a git hash-object difference, and each restore proved by an empty git diff HEAD and a blob hash matching HEAD:

  • removing the secret-bearing-DSN guard from the consumer reader ⇒ 1 failed / 92 passed;
  • making the OS_CLOUD_URL=off ceiling 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-coverageCOULD NOT MEASURE: 1 of 12 configs could not lint because @objectstack/connector-mcp has no build output in this worktree; it needs a whole-repo pnpm build. Unrelated to this diff (no i18n keys change).
  • pnpm check:type-check-debt — needs the full workspace closure built; not run.
  • CI convergence is not awaited, per the standing dispatch contract.

Generated by Claude Code


Generated by Claude Code

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
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/cloud-connection, touching 41 documentable anchor(s). ⚠️2 changed file(s) yielded no anchor (packages/cloud-connection/README.md, packages/cloud-connection/src/index.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/deployment/environment-variables.mdx(via /api/v1/runtime/config (route))
What this run could not see
  • 2 changed file(s) yielded no anchor (packages/cloud-connection/README.md, packages/cloud-connection/src/index.ts) — pages documenting those are invisible to this run
  • 10 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 3 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 284fc22d8834f2c9a3530592d6614e0eb6a28590packageMentionDocs.

Which tree this was computed on

This run read content/docs from 2e46570f0ca71a6993ea5ec0d303d92cb6b00eb6 — the merge of head a44e75fec735a3a333f7783517baa4592217f614 into base 284fc22d8834f2c9a3530592d6614e0eb6a28590, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 284fc22d8834f2c9a3530592d6614e0eb6a28590 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@claude

claudeBot commented Aug 27, 2026

Copy link
Copy Markdown
ContributorAuthor

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 allowClientErrorReporting and denies; a new client meeting an old server reads an absent DSN and stays off. Neither half can turn reporting on by itself, and both directions are pinned by test.

Generated by Claude Code


Generated by Claude Code

@os-zhuangos-zhuang left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.errorReporting as 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_RELEASE stays 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_REPORTING convention (the card's own "per repo convention" deferral; vendor-neutral), and replaysOnErrorSampleRate joins 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

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/xlteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-zhuang@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

runtime-config: serve the client error-reporting DSN from the server - #12697

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-12681-runtime-dsn
Aug 27, 2026
Merged

runtime-config: serve the client error-reporting DSN from the server#12697
os-zhuang merged 1 commit into
mainfrom
claude/issue-12681-runtime-dsn

Conversation

@claude

@claudeclaudeBot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

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/config now 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 === true plus a written argument about why a negative disabled key 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 the telemetry block is always emitted: {"telemetry":{}} means "this runtime knows the key and has no DSN"; no telemetry key 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

VariableDefault
OS_TELEMETRY_CLIENT_ERROR_REPORTING_DSN— (unset ⇒ no reporting)
OS_TELEMETRY_CLIENT_ERROR_REPORTING_SEND_DEFAULT_PIIfalse
OS_TELEMETRY_CLIENT_ERROR_REPORTING_ENVIRONMENT
OS_TELEMETRY_CLIENT_ERROR_REPORTING_TRACES_SAMPLE_RATE0.1
OS_TELEMETRY_CLIENT_ERROR_REPORTING_REPLAY_SAMPLE_RATE0

Named 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 an OS_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 only sendDefaultPii would 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:

  • a DSN that is not an https://PUBLIC_KEY@HOST/PROJECT_ID URL withholds the whole block — there is no safe default for a source;
  • a DSN carrying a secret after the public key is refused outright. This payload is read by every browser that loads the Console, so a legacy secret-bearing DSN would publish that secret to every visitor while looking entirely ordinary. This one check is mirrored on the consumer side too, because its failure mode is a leak rather than a misconfiguration;
  • a bad sample rate falls back to its documented default, and a bad PII spelling to 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 (or none / 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.allowClientErrorReporting is removed in this same change — code, payload, tests, the environment-variables.mdx row, the README section, and its pending changeset. No dual-spelling window. It landed days ago, is unreleased (@objectstack/cloud-connection@17.2.0 mentions 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.

FROMTO
OS_TELEMETRY_CLIENT_ERROR_REPORTING_ENABLED=trueOS_TELEMETRY_CLIENT_ERROR_REPORTING_DSN=https://PUBLIC_KEY@HOST/PROJECT_ID
new RuntimeConfigPlugin({ allowClientErrorReporting: true })new RuntimeConfigPlugin({ clientErrorReporting: { dsn: '…' } })
isClientErrorReportingAllowed(payload): booleanreadClientErrorReporting(payload): ClientErrorReportingConfig or null
CLIENT_ERROR_REPORTING_ENVCLIENT_ERROR_REPORTING_DSN_ENV and four siblings

The ADR-0087 disposition is recorded in the changeset: not-required (unpublished).

No .objectui-sha pin bump is needed. objectui has no dependency on @objectstack/cloud-connection — measured, not assumed: no package.json there names it, so removing these exports cannot break the pinned Console build.

Compatibility — any landing order is safe

  • Old client + new server: the old Console reads an absent telemetry.allowClientErrorReporting, its === true test denies ⇒ off.
  • New client + old server: the new Console reads an absent DSN ⇒ 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 && … test27 files, 340 tests passed; the build's DTS emission is this package's real type gate (it has no typecheck script — ERR_PNPM_RECURSIVE_RUN_NO_SCRIPT is a missing script, not a failure).
  • pnpm --filter @objectstack/cli exec vitest run test/serve-marketplace-offline-runtime-config.test.ts14 passed (after building the CLI dependency closure; the first run's Failed to resolve entry for @objectstack/plugin-email was an unbuilt dependency, not a verdict).
  • Gate families derived with 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:i18nall green.
  • ESLint on the five changed TS files: 0 errors, 0 warnings (--format json, 5 files judged). The narrowing is a measurement, not a skip: this repo's ESLint config sets no parserOptions.project and no typed rules (it says so at eslint.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 -c on the anchored text plus a git hash-object difference, and each restore proved by an empty git diff HEAD and a blob hash matching HEAD:

  • removing the secret-bearing-DSN guard from the consumer reader ⇒ 1 failed / 92 passed;
  • making the OS_CLOUD_URL=off ceiling 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-coverageCOULD NOT MEASURE: 1 of 12 configs could not lint because @objectstack/connector-mcp has no build output in this worktree; it needs a whole-repo pnpm build. Unrelated to this diff (no i18n keys change).
  • pnpm check:type-check-debt — needs the full workspace closure built; not run.
  • CI convergence is not awaited, per the standing dispatch contract.

Generated by Claude Code


Generated by Claude Code

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
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/cloud-connection, touching 41 documentable anchor(s). ⚠️2 changed file(s) yielded no anchor (packages/cloud-connection/README.md, packages/cloud-connection/src/index.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/deployment/environment-variables.mdx(via /api/v1/runtime/config (route))
What this run could not see
  • 2 changed file(s) yielded no anchor (packages/cloud-connection/README.md, packages/cloud-connection/src/index.ts) — pages documenting those are invisible to this run
  • 10 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 3 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 284fc22d8834f2c9a3530592d6614e0eb6a28590packageMentionDocs.

Which tree this was computed on

This run read content/docs from 2e46570f0ca71a6993ea5ec0d303d92cb6b00eb6 — the merge of head a44e75fec735a3a333f7783517baa4592217f614 into base 284fc22d8834f2c9a3530592d6614e0eb6a28590, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 284fc22d8834f2c9a3530592d6614e0eb6a28590 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@claude

claudeBot commented Aug 27, 2026

Copy link
Copy Markdown
ContributorAuthor

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 allowClientErrorReporting and denies; a new client meeting an old server reads an absent DSN and stays off. Neither half can turn reporting on by itself, and both directions are pinned by test.

Generated by Claude Code


Generated by Claude Code

@os-zhuangos-zhuang left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.errorReporting as 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_RELEASE stays 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_REPORTING convention (the card's own "per repo convention" deferral; vendor-neutral), and replaysOnErrorSampleRate joins 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

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/xlteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-zhuang@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

runtime-config: serve the client error-reporting DSN from the server - #12697

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-12681-runtime-dsn
Aug 27, 2026
Merged

runtime-config: serve the client error-reporting DSN from the server#12697
os-zhuang merged 1 commit into
mainfrom
claude/issue-12681-runtime-dsn

Conversation

@claude

@claudeclaudeBot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

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/config now 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 === true plus a written argument about why a negative disabled key 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 the telemetry block is always emitted: {"telemetry":{}} means "this runtime knows the key and has no DSN"; no telemetry key 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

VariableDefault
OS_TELEMETRY_CLIENT_ERROR_REPORTING_DSN— (unset ⇒ no reporting)
OS_TELEMETRY_CLIENT_ERROR_REPORTING_SEND_DEFAULT_PIIfalse
OS_TELEMETRY_CLIENT_ERROR_REPORTING_ENVIRONMENT
OS_TELEMETRY_CLIENT_ERROR_REPORTING_TRACES_SAMPLE_RATE0.1
OS_TELEMETRY_CLIENT_ERROR_REPORTING_REPLAY_SAMPLE_RATE0

Named 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 an OS_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 only sendDefaultPii would 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:

  • a DSN that is not an https://PUBLIC_KEY@HOST/PROJECT_ID URL withholds the whole block — there is no safe default for a source;
  • a DSN carrying a secret after the public key is refused outright. This payload is read by every browser that loads the Console, so a legacy secret-bearing DSN would publish that secret to every visitor while looking entirely ordinary. This one check is mirrored on the consumer side too, because its failure mode is a leak rather than a misconfiguration;
  • a bad sample rate falls back to its documented default, and a bad PII spelling to 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 (or none / 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.allowClientErrorReporting is removed in this same change — code, payload, tests, the environment-variables.mdx row, the README section, and its pending changeset. No dual-spelling window. It landed days ago, is unreleased (@objectstack/cloud-connection@17.2.0 mentions 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.

FROMTO
OS_TELEMETRY_CLIENT_ERROR_REPORTING_ENABLED=trueOS_TELEMETRY_CLIENT_ERROR_REPORTING_DSN=https://PUBLIC_KEY@HOST/PROJECT_ID
new RuntimeConfigPlugin({ allowClientErrorReporting: true })new RuntimeConfigPlugin({ clientErrorReporting: { dsn: '…' } })
isClientErrorReportingAllowed(payload): booleanreadClientErrorReporting(payload): ClientErrorReportingConfig or null
CLIENT_ERROR_REPORTING_ENVCLIENT_ERROR_REPORTING_DSN_ENV and four siblings

The ADR-0087 disposition is recorded in the changeset: not-required (unpublished).

No .objectui-sha pin bump is needed. objectui has no dependency on @objectstack/cloud-connection — measured, not assumed: no package.json there names it, so removing these exports cannot break the pinned Console build.

Compatibility — any landing order is safe

  • Old client + new server: the old Console reads an absent telemetry.allowClientErrorReporting, its === true test denies ⇒ off.
  • New client + old server: the new Console reads an absent DSN ⇒ 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 && … test27 files, 340 tests passed; the build's DTS emission is this package's real type gate (it has no typecheck script — ERR_PNPM_RECURSIVE_RUN_NO_SCRIPT is a missing script, not a failure).
  • pnpm --filter @objectstack/cli exec vitest run test/serve-marketplace-offline-runtime-config.test.ts14 passed (after building the CLI dependency closure; the first run's Failed to resolve entry for @objectstack/plugin-email was an unbuilt dependency, not a verdict).
  • Gate families derived with 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:i18nall green.
  • ESLint on the five changed TS files: 0 errors, 0 warnings (--format json, 5 files judged). The narrowing is a measurement, not a skip: this repo's ESLint config sets no parserOptions.project and no typed rules (it says so at eslint.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 -c on the anchored text plus a git hash-object difference, and each restore proved by an empty git diff HEAD and a blob hash matching HEAD:

  • removing the secret-bearing-DSN guard from the consumer reader ⇒ 1 failed / 92 passed;
  • making the OS_CLOUD_URL=off ceiling 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-coverageCOULD NOT MEASURE: 1 of 12 configs could not lint because @objectstack/connector-mcp has no build output in this worktree; it needs a whole-repo pnpm build. Unrelated to this diff (no i18n keys change).
  • pnpm check:type-check-debt — needs the full workspace closure built; not run.
  • CI convergence is not awaited, per the standing dispatch contract.

Generated by Claude Code


Generated by Claude Code

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
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/cloud-connection, touching 41 documentable anchor(s). ⚠️2 changed file(s) yielded no anchor (packages/cloud-connection/README.md, packages/cloud-connection/src/index.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/deployment/environment-variables.mdx(via /api/v1/runtime/config (route))
What this run could not see
  • 2 changed file(s) yielded no anchor (packages/cloud-connection/README.md, packages/cloud-connection/src/index.ts) — pages documenting those are invisible to this run
  • 10 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 3 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 284fc22d8834f2c9a3530592d6614e0eb6a28590packageMentionDocs.

Which tree this was computed on

This run read content/docs from 2e46570f0ca71a6993ea5ec0d303d92cb6b00eb6 — the merge of head a44e75fec735a3a333f7783517baa4592217f614 into base 284fc22d8834f2c9a3530592d6614e0eb6a28590, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 284fc22d8834f2c9a3530592d6614e0eb6a28590 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@claude

claudeBot commented Aug 27, 2026

Copy link
Copy Markdown
ContributorAuthor

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 allowClientErrorReporting and denies; a new client meeting an old server reads an absent DSN and stays off. Neither half can turn reporting on by itself, and both directions are pinned by test.

Generated by Claude Code


Generated by Claude Code

@os-zhuangos-zhuang left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.errorReporting as 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_RELEASE stays 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_REPORTING convention (the card's own "per repo convention" deferral; vendor-neutral), and replaysOnErrorSampleRate joins 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

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/xlteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-zhuang@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

runtime-config: serve the client error-reporting DSN from the server - #12697

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-12681-runtime-dsn
Aug 27, 2026
Merged

runtime-config: serve the client error-reporting DSN from the server#12697
os-zhuang merged 1 commit into
mainfrom
claude/issue-12681-runtime-dsn

Conversation

@claude

@claudeclaudeBot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

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/config now 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 === true plus a written argument about why a negative disabled key 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 the telemetry block is always emitted: {"telemetry":{}} means "this runtime knows the key and has no DSN"; no telemetry key 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

VariableDefault
OS_TELEMETRY_CLIENT_ERROR_REPORTING_DSN— (unset ⇒ no reporting)
OS_TELEMETRY_CLIENT_ERROR_REPORTING_SEND_DEFAULT_PIIfalse
OS_TELEMETRY_CLIENT_ERROR_REPORTING_ENVIRONMENT
OS_TELEMETRY_CLIENT_ERROR_REPORTING_TRACES_SAMPLE_RATE0.1
OS_TELEMETRY_CLIENT_ERROR_REPORTING_REPLAY_SAMPLE_RATE0

Named 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 an OS_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 only sendDefaultPii would 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:

  • a DSN that is not an https://PUBLIC_KEY@HOST/PROJECT_ID URL withholds the whole block — there is no safe default for a source;
  • a DSN carrying a secret after the public key is refused outright. This payload is read by every browser that loads the Console, so a legacy secret-bearing DSN would publish that secret to every visitor while looking entirely ordinary. This one check is mirrored on the consumer side too, because its failure mode is a leak rather than a misconfiguration;
  • a bad sample rate falls back to its documented default, and a bad PII spelling to 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 (or none / 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.allowClientErrorReporting is removed in this same change — code, payload, tests, the environment-variables.mdx row, the README section, and its pending changeset. No dual-spelling window. It landed days ago, is unreleased (@objectstack/cloud-connection@17.2.0 mentions 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.

FROMTO
OS_TELEMETRY_CLIENT_ERROR_REPORTING_ENABLED=trueOS_TELEMETRY_CLIENT_ERROR_REPORTING_DSN=https://PUBLIC_KEY@HOST/PROJECT_ID
new RuntimeConfigPlugin({ allowClientErrorReporting: true })new RuntimeConfigPlugin({ clientErrorReporting: { dsn: '…' } })
isClientErrorReportingAllowed(payload): booleanreadClientErrorReporting(payload): ClientErrorReportingConfig or null
CLIENT_ERROR_REPORTING_ENVCLIENT_ERROR_REPORTING_DSN_ENV and four siblings

The ADR-0087 disposition is recorded in the changeset: not-required (unpublished).

No .objectui-sha pin bump is needed. objectui has no dependency on @objectstack/cloud-connection — measured, not assumed: no package.json there names it, so removing these exports cannot break the pinned Console build.

Compatibility — any landing order is safe

  • Old client + new server: the old Console reads an absent telemetry.allowClientErrorReporting, its === true test denies ⇒ off.
  • New client + old server: the new Console reads an absent DSN ⇒ 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 && … test27 files, 340 tests passed; the build's DTS emission is this package's real type gate (it has no typecheck script — ERR_PNPM_RECURSIVE_RUN_NO_SCRIPT is a missing script, not a failure).
  • pnpm --filter @objectstack/cli exec vitest run test/serve-marketplace-offline-runtime-config.test.ts14 passed (after building the CLI dependency closure; the first run's Failed to resolve entry for @objectstack/plugin-email was an unbuilt dependency, not a verdict).
  • Gate families derived with 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:i18nall green.
  • ESLint on the five changed TS files: 0 errors, 0 warnings (--format json, 5 files judged). The narrowing is a measurement, not a skip: this repo's ESLint config sets no parserOptions.project and no typed rules (it says so at eslint.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 -c on the anchored text plus a git hash-object difference, and each restore proved by an empty git diff HEAD and a blob hash matching HEAD:

  • removing the secret-bearing-DSN guard from the consumer reader ⇒ 1 failed / 92 passed;
  • making the OS_CLOUD_URL=off ceiling 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-coverageCOULD NOT MEASURE: 1 of 12 configs could not lint because @objectstack/connector-mcp has no build output in this worktree; it needs a whole-repo pnpm build. Unrelated to this diff (no i18n keys change).
  • pnpm check:type-check-debt — needs the full workspace closure built; not run.
  • CI convergence is not awaited, per the standing dispatch contract.

Generated by Claude Code


Generated by Claude Code

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
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/cloud-connection, touching 41 documentable anchor(s). ⚠️2 changed file(s) yielded no anchor (packages/cloud-connection/README.md, packages/cloud-connection/src/index.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/deployment/environment-variables.mdx(via /api/v1/runtime/config (route))
What this run could not see
  • 2 changed file(s) yielded no anchor (packages/cloud-connection/README.md, packages/cloud-connection/src/index.ts) — pages documenting those are invisible to this run
  • 10 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 3 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 284fc22d8834f2c9a3530592d6614e0eb6a28590packageMentionDocs.

Which tree this was computed on

This run read content/docs from 2e46570f0ca71a6993ea5ec0d303d92cb6b00eb6 — the merge of head a44e75fec735a3a333f7783517baa4592217f614 into base 284fc22d8834f2c9a3530592d6614e0eb6a28590, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 284fc22d8834f2c9a3530592d6614e0eb6a28590 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@claude

claudeBot commented Aug 27, 2026

Copy link
Copy Markdown
ContributorAuthor

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 allowClientErrorReporting and denies; a new client meeting an old server reads an absent DSN and stays off. Neither half can turn reporting on by itself, and both directions are pinned by test.

Generated by Claude Code


Generated by Claude Code

@os-zhuangos-zhuang left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.errorReporting as 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_RELEASE stays 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_REPORTING convention (the card's own "per repo convention" deferral; vendor-neutral), and replaysOnErrorSampleRate joins 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

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/xlteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-zhuang@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

runtime-config: serve the client error-reporting DSN from the server - #12697

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-12681-runtime-dsn
Aug 27, 2026
Merged

runtime-config: serve the client error-reporting DSN from the server#12697
os-zhuang merged 1 commit into
mainfrom
claude/issue-12681-runtime-dsn

Conversation

@claude

@claudeclaudeBot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

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/config now 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 === true plus a written argument about why a negative disabled key 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 the telemetry block is always emitted: {"telemetry":{}} means "this runtime knows the key and has no DSN"; no telemetry key 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

VariableDefault
OS_TELEMETRY_CLIENT_ERROR_REPORTING_DSN— (unset ⇒ no reporting)
OS_TELEMETRY_CLIENT_ERROR_REPORTING_SEND_DEFAULT_PIIfalse
OS_TELEMETRY_CLIENT_ERROR_REPORTING_ENVIRONMENT
OS_TELEMETRY_CLIENT_ERROR_REPORTING_TRACES_SAMPLE_RATE0.1
OS_TELEMETRY_CLIENT_ERROR_REPORTING_REPLAY_SAMPLE_RATE0

Named 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 an OS_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 only sendDefaultPii would 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:

  • a DSN that is not an https://PUBLIC_KEY@HOST/PROJECT_ID URL withholds the whole block — there is no safe default for a source;
  • a DSN carrying a secret after the public key is refused outright. This payload is read by every browser that loads the Console, so a legacy secret-bearing DSN would publish that secret to every visitor while looking entirely ordinary. This one check is mirrored on the consumer side too, because its failure mode is a leak rather than a misconfiguration;
  • a bad sample rate falls back to its documented default, and a bad PII spelling to 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 (or none / 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.allowClientErrorReporting is removed in this same change — code, payload, tests, the environment-variables.mdx row, the README section, and its pending changeset. No dual-spelling window. It landed days ago, is unreleased (@objectstack/cloud-connection@17.2.0 mentions 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.

FROMTO
OS_TELEMETRY_CLIENT_ERROR_REPORTING_ENABLED=trueOS_TELEMETRY_CLIENT_ERROR_REPORTING_DSN=https://PUBLIC_KEY@HOST/PROJECT_ID
new RuntimeConfigPlugin({ allowClientErrorReporting: true })new RuntimeConfigPlugin({ clientErrorReporting: { dsn: '…' } })
isClientErrorReportingAllowed(payload): booleanreadClientErrorReporting(payload): ClientErrorReportingConfig or null
CLIENT_ERROR_REPORTING_ENVCLIENT_ERROR_REPORTING_DSN_ENV and four siblings

The ADR-0087 disposition is recorded in the changeset: not-required (unpublished).

No .objectui-sha pin bump is needed. objectui has no dependency on @objectstack/cloud-connection — measured, not assumed: no package.json there names it, so removing these exports cannot break the pinned Console build.

Compatibility — any landing order is safe

  • Old client + new server: the old Console reads an absent telemetry.allowClientErrorReporting, its === true test denies ⇒ off.
  • New client + old server: the new Console reads an absent DSN ⇒ 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 && … test27 files, 340 tests passed; the build's DTS emission is this package's real type gate (it has no typecheck script — ERR_PNPM_RECURSIVE_RUN_NO_SCRIPT is a missing script, not a failure).
  • pnpm --filter @objectstack/cli exec vitest run test/serve-marketplace-offline-runtime-config.test.ts14 passed (after building the CLI dependency closure; the first run's Failed to resolve entry for @objectstack/plugin-email was an unbuilt dependency, not a verdict).
  • Gate families derived with 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:i18nall green.
  • ESLint on the five changed TS files: 0 errors, 0 warnings (--format json, 5 files judged). The narrowing is a measurement, not a skip: this repo's ESLint config sets no parserOptions.project and no typed rules (it says so at eslint.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 -c on the anchored text plus a git hash-object difference, and each restore proved by an empty git diff HEAD and a blob hash matching HEAD:

  • removing the secret-bearing-DSN guard from the consumer reader ⇒ 1 failed / 92 passed;
  • making the OS_CLOUD_URL=off ceiling 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-coverageCOULD NOT MEASURE: 1 of 12 configs could not lint because @objectstack/connector-mcp has no build output in this worktree; it needs a whole-repo pnpm build. Unrelated to this diff (no i18n keys change).
  • pnpm check:type-check-debt — needs the full workspace closure built; not run.
  • CI convergence is not awaited, per the standing dispatch contract.

Generated by Claude Code


Generated by Claude Code

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
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/cloud-connection, touching 41 documentable anchor(s). ⚠️2 changed file(s) yielded no anchor (packages/cloud-connection/README.md, packages/cloud-connection/src/index.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/deployment/environment-variables.mdx(via /api/v1/runtime/config (route))
What this run could not see
  • 2 changed file(s) yielded no anchor (packages/cloud-connection/README.md, packages/cloud-connection/src/index.ts) — pages documenting those are invisible to this run
  • 10 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 3 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 284fc22d8834f2c9a3530592d6614e0eb6a28590packageMentionDocs.

Which tree this was computed on

This run read content/docs from 2e46570f0ca71a6993ea5ec0d303d92cb6b00eb6 — the merge of head a44e75fec735a3a333f7783517baa4592217f614 into base 284fc22d8834f2c9a3530592d6614e0eb6a28590, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 284fc22d8834f2c9a3530592d6614e0eb6a28590 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@claude

claudeBot commented Aug 27, 2026

Copy link
Copy Markdown
ContributorAuthor

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 allowClientErrorReporting and denies; a new client meeting an old server reads an absent DSN and stays off. Neither half can turn reporting on by itself, and both directions are pinned by test.

Generated by Claude Code


Generated by Claude Code

@os-zhuangos-zhuang left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.errorReporting as 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_RELEASE stays 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_REPORTING convention (the card's own "per repo convention" deferral; vendor-neutral), and replaysOnErrorSampleRate joins 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

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/xlteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-zhuang@claude