Repository navigation
Description
Provenance: maintainer, 2026-08-27, PM chat (session session_01DKWDdUJ2XNRESVVWUvcpnh), reviewing the #5522 (objectui) operating procedure. Verbatim, untranslated:
「我是一个开发平台呀,我的用户并不会去构建我的前端,我理解这种应该在服务端传进去。」
The gap the ruling names
The shipped two-key gate (objectui #5559 + #5982, this repo's PR #11382) is: build-time VITE_SENTRY_DSN AND runtime telemetry.allowClientErrorReporting. That shape silently assumes the deployer builds the console. ObjectStack's users don't — they consume a prebuilt console, and after #5559 the prebuilt artifact deliberately carries no DSN. Consequence, stated plainly:
- A self-hosting operator cannot enable client error reporting at all today — the runtime permission exists, but the source half is compiled-in and absent, and rebuilding the frontend is not a thing platform users do.
- The maintainer's own SaaS can work around it (he controls the Vercel build), but only by configuring telemetry in two places (build panel + server env) — two knobs with two silent dead states ("permission on, no DSN" / "DSN in, permission off"), the same shape-class of quiet failure docs(pm-dispatch): 协调模型改版 —— 分诊/执行纵向拆分 + 一人一车道双射 + 登记表正文即真相 + 座位 Routine 化 (#5472) #5522 was about.
Ruled direction, and the shape the standing rules pick
The DSN is served by the operator's server at runtime — the direction is the maintainer's ruling above. The shape follows from the startup-stage no-gradualism rule (no dual paths, no grace windows; the boolean shipped days ago, is in an unreleased changeset, and no deployment consumes it):
/api/v1/runtime/config'stelemetrypayload carries the source itself: the DSN (plus a closed enumeration of the client knobs that must travel with it — PII opt-in, environment, sample rates — designed as one object, not scattered fields). The DSN's presence IS the grant: no DSN ⇒ reporting off. Fail-closed posture unchanged.- The server's knowledge source is operator env /
RuntimeConfigPluginconfig (name per repo convention, e.g.OS_TELEMETRY_SENTRY_DSN-shaped; the existing truthy-vocabulary strictness precedent applies — malformed value refused loudly at mount, never coerced). - The
allowClientErrorReportingboolean is replaced, not paralleled — remove it in the same change (unreleased changeset superseded per changeset conventions; thecontent/docs/deployment/environment-variables.mdxrow andpackages/cloud-connection/README.mdmove with it, sync-guarded docs updated in the same PR). - objectui half:
app-shell's runtime-config consumer reads the DSN object;initSentry()already runs afterinitRuntimeConfig()(test(service-sms): sms settings provider 表 ↔ transports 双向契约测试 (#5773) #5982 sequenced exactly this), so the delivery point exists. TheVITE_SENTRY_DSNbuild-time source path retires (no-gradualism); thecommitted-telemetry-endpoint.test.tsratchet stays — its job (nothing endpoint-shaped in the repo) is unchanged. - CSP note for the dev: the console CSP's
connect-srcallowshttps://*.sentry.io; a runtime-delivered DSN on a custom/self-hosted Sentry domain needs the CSP story stated in the docs, not silently broken.
Sequencing
- objectui#6599 (in flight) rewrites
apps/console/docs/error-tracking.mdto describe the current two-key shape — correct to land first; this card updates that doc again to the server-delivered shape as part of its own change. Expected churn, accepted. ⚠️ Contract tier: this changes the accept/reject surface of a public endpoint's payload (/api/v1/runtime/config) — the standing contract-review tiering clause applies to construction and review.
Acceptance
- A self-hosting operator enables client error reporting with server-side configuration only — one place, no frontend rebuild; unset ⇒ off, malformed ⇒ loud refusal at mount.
- The boolean is gone from code, payload, docs, and pending changeset in the same change; no dual-spelling window.
- Both repos' halves land coherently (cross-repo PRs referencing this card); docs (
environment-variables.mdx, cloud-connection README, error-tracking.md) describe only the final shape.
Refs: objectui#5522 (the saga card, closed by maintainer ruling 2026-08-27) · objectui#5559 · objectui#5982 · #10805 / PR #11382 · objectui#6599.
Activity
os-zhuang commentedon Aug 27, 2026
Premise correction to item 5 (CSP note) — the measurement from objectui#6599's rewrite (PR objectui#6601) falsified what this card's item 5 assumed.
This card said: "the console CSP's
connect-srcallowshttps://*.sentry.io". Measured reality: the Console ships no CSP at all —apps/console/index.htmlsets no CSP meta tag, the repo defines no default policy, and the only mention of CSP anywhere in objectui was the fabricated section of the olderror-tracking.md(now rewritten to say so honestly).Item 5 therefore reduces to: a runtime-delivered DSN has no in-repo CSP obstacle; the only CSP story is the one the rewritten doc already carries — an operator whose hosting layer serves CSP headers must allow their own DSN's ingest origin in
connect-src. The dev implementing this card should keep that doc section accurate for the server-delivered shape and add nothing CSP-related to the code.Generated by Claude Code
claude commentedon Aug 27, 2026
Claim — starting work on this card.
session_01DKWDdUJ2XNRESVVWUvcpnhclaude/issue-12681-runtime-dsnobjectstack-ai/objectstack(server half:RuntimeConfigPlugin, env knob, docs row, README, changesets) andobjectstack-ai/objectui(client half:app-shellruntime-config consumer, Sentry gate,apps/consolewiring,apps/console/docs/error-tracking.md).Acknowledged: the PM comment above corrects item 5 — the Console ships no CSP, so there is no in-repo CSP work; the rewritten doc's hosting-layer CSP note is kept accurate for the server-delivered shape and nothing CSP-related is added to code.
Generated by Claude Code
Generated by Claude Code
claude commentedon Aug 27, 2026
os-dev-report
{ "issue": 12681, "status": "done", "branch": "claude/issue-12681-runtime-dsn", "pr": "https://github.com/objectstack-ai/objectstack/pull/12697 + https://github.com/objectstack-ai/objectui/pull/6603", "premise_still_valid": true, "summary": "Both halves implemented and pushed as draft PRs. SERVER (objectstack#12697): GET /api/v1/runtime/config now serves telemetry.errorReporting = { dsn, sendDefaultPii, environment?, tracesSampleRate, replaysOnErrorSampleRate } as one object; the telemetry block is always emitted (empty when unconfigured) so `{\"telemetry\":{}}` stays distinguishable from a runtime that never heard of the key. Env knobs: OS_TELEMETRY_CLIENT_ERROR_REPORTING_DSN plus _SEND_DEFAULT_PII / _ENVIRONMENT / _TRACES_SAMPLE_RATE / _REPLAY_SAMPLE_RATE; host option clientErrorReporting wins PER FIELD. Malformed refused loudly at mount, each refusal landing on the safer value (bad DSN withholds the whole block, bad rate falls back to its default), and a secret-bearing DSN is refused outright because this payload is public to every browser; quoted DSNs are key-redacted. OS_CLOUD_URL=off still refuses to serve a sink. CLIENT (objectui#6603): app-shell reads the sink from runtime config, resolveSentryGate() now takes that ONE input (no env argument) and its `reason` collapses from four values to two; VITE_SENTRY_DSN and every sibling retire except VITE_SENTRY_RELEASE; initSentry() stays after initRuntimeConfig(); the committed-telemetry-endpoint ratchet keeps its rules and gains two counter-probes for the runtime-side spelling. The allowClientErrorReporting boolean is REMOVED everywhere in the same change (code, payload, tests, docs row, README, pending changeset superseded) with no dual-spelling window.", "tests": "objectstack, all on branch head a44e75fe (exit codes captured before any pipe): `pnpm --filter @objectstack/cloud-connection build && ... test` -> 'Test Files 27 passed (27) / Tests 340 passed (340)', build DTS emitted clean (this package has no typecheck script -- the ERR_PNPM_RECURSIVE_RUN_NO_SCRIPT is a missing script, not a failure, so the DTS emission is the type gate). `pnpm --filter @objectstack/cli exec vitest run test/serve-marketplace-offline-runtime-config.test.ts` -> 'Tests 14 passed (14)' (first attempt hit an unbuilt @objectstack/plugin-email -- not a verdict; re-run after building the CLI closure). Gate families derived with `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack` (no hand-written path list; it reported 9 paths vs merge base 07e646565): all 12 doc families, all 16 test-kind/package families, the 6 changeset families, check:nul-bytes, check:route-envelope, check:skill-examples and check:i18n all PASS (0). ESLint on the 5 changed TS files: 0 errors, 0 warnings, 5 files judged from --format json. objectui, all on branch head 0657139 with a clean working tree: targeted suite -> 'Test Files 3 passed (3) / Tests 55 passed (55)'; full package suite `pnpm exec vitest run packages/app-shell/` -> 'Test Files 558 passed (558) / Tests 5400 passed | 1 skipped (5401)'; app-shell type-check (tsc --noEmit && tsc -p tsconfig.test.json) clean AND --listFiles confirms all 3 edited test files are inside that program (so 'type-check clean' really covers them); console type-check clean after building its closure; 12 derived gates all PASS. ESLint on the 9 changed TS/TSX files: 0 errors, 7 warnings, all pre-existing no-explicit-any -- per-file counts flat or lower than the merge base (sentry.test.ts 2 -> 0) and `eslint .` carries no --max-warnings. ESLint narrowing is a measurement, not a skip: neither repo sets parserOptions.project or any typed rule (objectstack states it at eslint.config.mjs:328), so no untouched file's verdict can move because of these diffs. REVERSE VERIFICATION -- 4 ablations, all on COMMITTED trees, each mutation confirmed on disk by before/after `grep -c` on the anchored text plus a git hash-object difference (never an editor exit code), each restore proved by an empty `git diff HEAD` plus a blob hash matching HEAD (non-empty), absolute paths in every trap: (A) removing the secret-DSN guard from the server reader -> 1 failed / 92 passed; (B) making the OS_CLOUD_URL=off ceiling inert -> 8 failed / 85 passed; (C) making the client mirror fail open on an empty DSN -> 1 failed / 47 passed; (D) letting a truthy lookalike open the PII flow (=== true -> !!) -> 1 failed / 17 passed. All four went red in the predicted direction. No dist/build was involved in any ablation: every suite imports the mutated module by relative source path, so there is no stale-dist false green here. NOT MEASURED, declared: (1) `node scripts/pm/check-half-states.mjs` exit 3 PREREQUISITE NOT MET -- no valid GitHub credential in this container; its own output says nothing was swept (the self-test form `pnpm check:pm-half-states` passed). (2) `pnpm check:i18n-coverage` COULD NOT MEASURE -- 1 of 12 configs cannot lint because @objectstack/connector-mcp has no build output here; needs a whole-repo pnpm build, and is unrelated to this diff. (3) `pnpm check:type-check-debt` needs the full workspace closure built; not run. (4) Repo-wide `pnpm lint` / `pnpm test` not run in either repo; CI runs both. (5) CI convergence not awaited, per the standing dispatch contract.", "mcp_calls": "0 -- the repo-scoped REST probe returned HTTP 200 authed on this seat, so every GitHub read and write (issue body, comments, both PR creations, all read-backs) went over REST; no MCP GitHub call was made.", "open_questions": [ { "question": "Should replaysOnErrorSampleRate be inside the closed enumeration, or left out as scope the card did not explicitly name?", "options": [ "A -- include it (implemented): the ruling says 'sample rates' (plural), the build had exactly two, and replay is the MORE privacy-bearing of them, so leaving it build-time-only would strand a privacy knob out of the platform user's reach -- this card's defect class exactly.", "B -- leave it build-time and keep only tracesSampleRate: strictly smaller new surface, but a prebuilt-console consumer could then never turn error-session replay on or off." ], "recommendation": "A, implemented. Not new surface: it is an existing knob relocated to the side that can operate it, and it stays OFF by default. Whole-session replay (replaysSessionSampleRate) is deliberately NOT authorable -- hard-coded 0, strictly larger surface, nothing pulls it. Flagging it because the card named 'PII opt-in, environment, sample rates' without enumerating this one; trivially removable if the maintainer wants the tighter set." }, { "question": "Env var family: OS_TELEMETRY_CLIENT_ERROR_REPORTING_DSN (implemented) vs the card's suggested OS_TELEMETRY_SENTRY_DSN shape.", "options": [ "A -- keep the CLIENT_ERROR_REPORTING family (implemented): the card said 'name per repo convention, e.g. OS_TELEMETRY_SENTRY_DSN-shaped', and the convention already established in this exact namespace one PR ago is the CLIENT_ERROR_REPORTING family. It also keeps the vendor out of an operator-facing contract that a self-hosted or DSN-compatible sink satisfies equally well, and makes the replacement read as the same knob rather than a new one.", "B -- OS_TELEMETRY_SENTRY_DSN: shorter, and the DSN format is a Sentry construct." ], "recommendation": "A, implemented. The card's wording was 'e.g.' and '-shaped' and explicitly deferred to repo convention, so this is not a deviation from the ruling. Cheap to rename before merge if the maintainer prefers the vendor spelling." } ], "out_of_scope_findings": [] }Generated by Claude Code
Generated by Claude Code
os-zhuang commentedon Aug 27, 2026
Both halves are merged — closing as completed.
/api/v1/runtime/configservestelemetry.errorReportingas one closed object; DSN presence is the grant; the Runtime config has no telemetry key, so an opted-in console build still has no post-build off switch (upstream half of objectui#5522, cloud#1508 p0) #10805 boolean removed in the same change with its pending changeset superseded; malformed refused loudly at mount; secret-bearing DSNs refused;OS_CLOUD_URL=offstill refuses to serve a sink.VITE_SENTRY_*retired exceptVITE_SENTRY_RELEASE; the committed-telemetry ratchet extended with counter-probes for the runtime-side spelling.The acceptance holds end to end: a self-hosting operator enables client error reporting with one server-side environment variable (
OS_TELEMETRY_CLIENT_ERROR_REPORTING_DSN), no frontend rebuild; unset ⇒ off; malformed ⇒ loud refusal; no dual-spelling window in either landing order (both directions were pinned by test and the order ended up irrelevant — both merged within ten minutes).Ratified sub-decisions recorded in both PM reviews: the env family keeps the vendor-neutral
CLIENT_ERROR_REPORTINGconvention, andreplaysOnErrorSampleRatejoins the closed set (privacy-bearing knobs may not stay stranded on the build side). Both are one-line changes if the maintainer ever prefers otherwise.Generated by Claude Code
docs(cloud-connection): re-anchor the dead tracker citations in packa…