Console error reporting: consume the DSN from runtime config - #6603

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

Console error reporting: consume the DSN from runtime config#6603
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 objectstack-ai/objectstack#12681 — the client half. The server half is objectstack-ai/objectstack#12697; neither PR closes the card on its own.

What this changes

The Console's error-reporting DSN — and every knob that travels with it — now arrives from the runtime, on telemetry.errorReporting of GET /api/v1/runtime/config. The build-time VITE_SENTRY_DSN path is retired rather than kept as a second source.

send ⇔ the runtime served a DSN

The maintainer's ruling on the card, verbatim and untranslated:

「我是一个开发平台呀,我的用户并不会去构建我的前端,我理解这种应该在服务端传进去。」

ObjectStack's users consume a prebuilt Console. Under the two-key gate a build-time key was unreachable for them, so a self-hosting operator could not enable client error reporting at all — the runtime permission was reachable and the source was not. They also could not turn sendDefaultPii off, which is the knob deciding whether IP and User-Agent leave their network.

The gate collapsed to one input, and that is the fix

resolveSentryGate() now takes the runtime payload alone — no env argument. That absence is the point: nothing a build was compiled with can influence whether reporting happens.

reason drops from four values to two. forced-off retired with VITE_SENTRY_ENABLED; runtime-deniedcollapsed intono-dsn, because once the DSN is the grant, "the runtime declined" and "no DSN arrived" are the same state — described from the one place an operator has to look. Turning reporting off is unsetting the server DSN; there is deliberately no build-time force-off left, because nobody consuming a prebuilt console could reach one.

The fail-closed posture is unchanged and structurally stronger: absence of a source is not a value that can be misread, where the boolean needed a strict === true plus a written argument about why a negative disabled flag would have been vacuous on exactly the runtimes that were leaking.

What moved to the runtime, and the one knob that did not

Moved into the payload: sendDefaultPii, environment, tracesSampleRate, replaysOnErrorSampleRate. Not new surface — the same surface relocated to the side that can operate it.

VITE_SENTRY_RELEASE stays build-time, and is now the only VITE_SENTRY_* variable that exists. A release identifies which bundle produced a stack trace and must match the source maps that bundle's pipeline uploaded; a server cannot know which Console build it is serving. It is a label on the events, never a gate, so it is read at the Sentry.init call site rather than inside the gate. VITE_SENTRY_ENABLED, VITE_SENTRY_ENVIRONMENT, VITE_SENTRY_TRACES_SAMPLE_RATE and VITE_SENTRY_REPLAY retire alongside VITE_SENTRY_DSN.

replaysSessionSampleRate stays hard-coded to 0 and is deliberately not authorable: whole-session replay is a strictly larger surface than error-session replay and nothing pulls it.

Ordering, and the ratchet

initSentry() stays sequenced after initRuntimeConfig() in apps/console/src/main.tsx, and the ordering is now more load-bearing, not less: the DSN itself is server-pushed, so a call at module-eval time freezes "no sink" for the session and turns the operator's only switch into a permanent removal. The comment there says so.

committed-telemetry-endpoint.test.ts keeps its rules unchanged — its job ("nothing endpoint-shaped is committed to this repo") is unchanged, and a committed DSN is still inlined into the published bundle. Its rules key on the variable's suffix and on the value, never on the VITE_ prefix, so they already covered the runtime-side spelling; two counter-probe assertions now pin that, so a later tidy-up cannot narrow the rules to the retired names and reopen the hole under a new one. Prose updated to describe the new recipe.

Docs

apps/console/docs/error-tracking.md (rewritten last by #6601) is updated to describe only the final shape.

Its CSP section is kept accurate and nothing CSP-related was added to code, per the PM's premise correction on the card: the Console ships no CSPapps/console/index.html sets none and the repo defines no default policy — so there is no in-repo CSP obstacle. The section remains the hosting-layer note, with one addition earned by this change: a runtime-served DSN can be repointed at a different ingest origin with no Console rebuild, so a hosting CSP that was correct before can start dropping events with nothing else having moved.

Breaking

FROMTO
VITE_SENTRY_DSN in the Console build environmentOS_TELEMETRY_CLIENT_ERROR_REPORTING_DSN on the ObjectStack runtime
VITE_SENTRY_SEND_DEFAULT_PII=trueOS_TELEMETRY_CLIENT_ERROR_REPORTING_SEND_DEFAULT_PII=true
VITE_SENTRY_ENVIRONMENT / VITE_SENTRY_TRACES_SAMPLE_RATEthe matching OS_TELEMETRY_CLIENT_ERROR_REPORTING_* variables
VITE_SENTRY_REPLAY=trueOS_TELEMETRY_CLIENT_ERROR_REPORTING_REPLAY_SAMPLE_RATE=0.1
VITE_SENTRY_ENABLED=falseunset the runtime DSN
isClientErrorReportingAllowed(): booleangetClientErrorReporting(): RuntimeClientErrorReporting or null
resolveSentryGate(env, runtimeAllows)resolveSentryGate(runtimeErrorReporting)
RuntimeTelemetry.allowClientErrorReportingRuntimeTelemetry.errorReporting?

Changeset declares minor on @object-ui/app-shell and @object-ui/console — per this repo's rule that breaking changes ship as minor with the semantics spelled out in the body, never as major.

Compatibility — any landing order is safe

  • Old client + new server: a Console built before this change reads an absent telemetry.allowClientErrorReporting, its === true test denies ⇒ off.
  • New client + old server: this Console reads an absent DSN ⇒ off. Pinned by a test that feeds it exactly the intermediate runtime's payload, { telemetry: { allowClientErrorReporting: true } }, and asserts null.

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.

Validation

All of the following ran on 0657139 (this branch's head, working tree clean), exit codes captured before any pipe.

  • pnpm exec vitest run packages/app-shell/src/runtime-config.test.ts packages/app-shell/src/observability/sentry.test.ts packages/app-shell/src/observability/committed-telemetry-endpoint.test.ts3 files, 55 tests passed. Run from the repo root: the repo's own guard rejected the pnpm --filter form, which would have reported 22 passed from a different package entirely.
  • Full package suite pnpm exec vitest run packages/app-shell/558 files, 5400 passed, 1 skipped.
  • pnpm --filter @object-ui/app-shell run type-check (tsc --noEmit && tsc -p tsconfig.test.json) — clean. Verified with --listFiles that all 3 edited test files are inside that program, so "type-check clean" actually covers them.
  • pnpm --filter @object-ui/console run type-check — clean (after building the console dependency closure; the first run's Cannot find module '@object-ui/plugin-*' errors were unbuilt packages, not verdicts).
  • Gates derived from this repo's own package.json and .github/workflows/: check-control-bytes, check-changeset-presence, check-changeset-no-major, check:doc-fences, check:doc-snippets, check:doc-types, check:readme-exports, check:vi-mock-specifiers, check:self-import, check:phantom-deps, check:shell-escape-residue, check:spec-symbolsall green.
  • ESLint on the nine changed TS/TSX files: 0 errors, 7 warnings, all pre-existing no-explicit-any spellings — the per-file count is flat or lower than the merge base (sentry.test.ts went 2 to 0), and eslint . carries no --max-warnings. The narrowing is a measurement, not a skip: this repo's ESLint config sets no parserOptions.project and no typed rules, 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:

  • making the mirrored reader fail open on an empty DSN ⇒ 1 failed / 47 passed;
  • letting a truthy lookalike open the PII flow (=== true to !!) ⇒ 1 failed / 17 passed.

Declared NOT MEASURED

  • Repo-wide pnpm test and pnpm lint were not run; CI runs both. The affected package's full suite and the derived gate families above are the local half.
  • 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 never run a build of it,
so the build-time VITE_SENTRY_DSN half of the two-key gate was unreachable
for them -- they could neither turn reporting on nor turn PII collection
off. app-shell now reads the sink and its knobs from telemetry.errorReporting
on /api/v1/runtime/config, and the build-time DSN path is retired rather
than kept as a second source.
The gate collapses to one input, which is the fix rather than a side effect:
resolveSentryGate() takes the runtime payload alone, and its `reason` drops
from four values to two because "the runtime declined" and "no DSN arrived"
are now the same state, described from the one place an operator looks.
VITE_SENTRY_RELEASE survives as the only build-time knob: a release
identifies which bundle produced a stack trace and must match that build's
uploaded source maps, which no server can know.
initSentry() stays sequenced after initRuntimeConfig() and the ordering is
now more load-bearing, not less -- the DSN itself arrives from the server.
The committed-telemetry-endpoint ratchet keeps its rules unchanged; they key
on the variable suffix and the value, never on the VITE_ prefix, so they
already cover the runtime-side spelling. That is now pinned.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DKWDdUJ2XNRESVVWUvcpnh
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 52 chunks)3235.6 KB3266.6 KB
Main entry chunk (gzip)157.0 KB350 KB
Entry fileindex-DvWnMsNg.js
StatusPASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)11.89KB4.50KB
app-shell (runtime-config.js)20.61KB7.35KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)506.01KB114.64KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)173.10KB47.96KB
fields (index.js)238.89KB60.02KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)9.53KB3.38KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)4.64KB1.50KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)1.93KB0.88KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.85KB12.89KB
plugin-charts (index.js)64.66KB18.32KB
plugin-chatbot (index.js)188.60KB44.82KB
plugin-dashboard (index.js)133.48KB34.49KB
plugin-designer (index.js)212.80KB43.15KB
plugin-detail (index.js)245.29KB62.39KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)131.78KB32.19KB
plugin-gantt (index.js)165.16KB40.33KB
plugin-grid (index.js)201.66KB54.58KB
plugin-kanban (index.js)53.11KB14.62KB
plugin-list (index.js)112.86KB27.54KB
plugin-map (index.js)20.09KB6.62KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)26.72KB7.71KB
plugin-tree (index.js)9.26KB3.13KB
plugin-view (index.js)85.87KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)63.21KB21.05KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)2.44KB1.21KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)12.13KB3.65KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)7.54KB2.63KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@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 objectstack#12681; this seat shares the PR's author identity, so this is a COMMENT review — not a governed surface).

Read the full 1885-line diff. Findings, all positive:

  • The gate collapses to one input and the collapse is complete: resolveSentryGate(runtimeErrorReporting) takes no env argument any more — nothing a build was compiled with can influence whether reporting happens; reason honestly shrinks from four values to two, with runtime-denied collapsing INTO no-dsn because they are now the same state. The withheld verdict carries no PII and zero sample rates.
  • Every VITE_SENTRY_* retires except VITE_SENTRY_RELEASE, which is a label read at the call site, never a gate — with the source-maps reasoning recorded in three places a future editor would look.
  • The ratchet did not narrow: committed-telemetry-endpoint.test.ts rules are keyed on key-suffix and value, and the new counter-probes pin that a committed OS_TELEMETRY_CLIENT_ERROR_REPORTING_DSN in a .env file is caught exactly like the retired spelling — a later "tidy-up" cannot reopen the hole under the new name.
  • The mirror discipline is preserved: readClientErrorReporting is a declared line-for-line hand copy of the producer's export (this repo deliberately takes no dependency on @objectstack/cloud-connection), the secret-DSN guard is kept on this side as the last line against a third-party host, knobs are re-derived defensively (=== true for PII, finite 0..1 for rates, 0 kept as a real answer), and telemetry stays replace-not-merge so a withdrawn DSN withdraws.
  • The landing-order pin exists on this side too: a payload carrying only the replaced #10805 boolean reads as null.
  • Docs and comments (error-tracking.md, .env.production, main.tsx ordering note) describe only the final shape; the error-tracking CSP note gains the right new caveat — the DSN can now be repointed without a rebuild, so a hosting-layer CSP can silently go stale.

Cross-repo pair: objectstack#12697 (verified in its own review). Landing via the normal queue; the card closes when both halves are merged.


Generated by Claude Code

@os-zhuang
os-zhuang marked this pull request as ready for review August 27, 2026 10:37
@os-zhuang
os-zhuang enabled auto-merge August 27, 2026 10:37
@os-zhuang
os-zhuang added this pull request to the merge queueAug 27, 2026
Merged via the queue into main with commit 14ef9f5Aug 27, 2026
30 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-12681-runtime-dsn branch August 27, 2026 10:56
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Console error reporting: consume the DSN from runtime config - #6603

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

Console error reporting: consume the DSN from runtime config#6603
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 objectstack-ai/objectstack#12681 — the client half. The server half is objectstack-ai/objectstack#12697; neither PR closes the card on its own.

What this changes

The Console's error-reporting DSN — and every knob that travels with it — now arrives from the runtime, on telemetry.errorReporting of GET /api/v1/runtime/config. The build-time VITE_SENTRY_DSN path is retired rather than kept as a second source.

send ⇔ the runtime served a DSN

The maintainer's ruling on the card, verbatim and untranslated:

「我是一个开发平台呀,我的用户并不会去构建我的前端,我理解这种应该在服务端传进去。」

ObjectStack's users consume a prebuilt Console. Under the two-key gate a build-time key was unreachable for them, so a self-hosting operator could not enable client error reporting at all — the runtime permission was reachable and the source was not. They also could not turn sendDefaultPii off, which is the knob deciding whether IP and User-Agent leave their network.

The gate collapsed to one input, and that is the fix

resolveSentryGate() now takes the runtime payload alone — no env argument. That absence is the point: nothing a build was compiled with can influence whether reporting happens.

reason drops from four values to two. forced-off retired with VITE_SENTRY_ENABLED; runtime-deniedcollapsed intono-dsn, because once the DSN is the grant, "the runtime declined" and "no DSN arrived" are the same state — described from the one place an operator has to look. Turning reporting off is unsetting the server DSN; there is deliberately no build-time force-off left, because nobody consuming a prebuilt console could reach one.

The fail-closed posture is unchanged and structurally stronger: absence of a source is not a value that can be misread, where the boolean needed a strict === true plus a written argument about why a negative disabled flag would have been vacuous on exactly the runtimes that were leaking.

What moved to the runtime, and the one knob that did not

Moved into the payload: sendDefaultPii, environment, tracesSampleRate, replaysOnErrorSampleRate. Not new surface — the same surface relocated to the side that can operate it.

VITE_SENTRY_RELEASE stays build-time, and is now the only VITE_SENTRY_* variable that exists. A release identifies which bundle produced a stack trace and must match the source maps that bundle's pipeline uploaded; a server cannot know which Console build it is serving. It is a label on the events, never a gate, so it is read at the Sentry.init call site rather than inside the gate. VITE_SENTRY_ENABLED, VITE_SENTRY_ENVIRONMENT, VITE_SENTRY_TRACES_SAMPLE_RATE and VITE_SENTRY_REPLAY retire alongside VITE_SENTRY_DSN.

replaysSessionSampleRate stays hard-coded to 0 and is deliberately not authorable: whole-session replay is a strictly larger surface than error-session replay and nothing pulls it.

Ordering, and the ratchet

initSentry() stays sequenced after initRuntimeConfig() in apps/console/src/main.tsx, and the ordering is now more load-bearing, not less: the DSN itself is server-pushed, so a call at module-eval time freezes "no sink" for the session and turns the operator's only switch into a permanent removal. The comment there says so.

committed-telemetry-endpoint.test.ts keeps its rules unchanged — its job ("nothing endpoint-shaped is committed to this repo") is unchanged, and a committed DSN is still inlined into the published bundle. Its rules key on the variable's suffix and on the value, never on the VITE_ prefix, so they already covered the runtime-side spelling; two counter-probe assertions now pin that, so a later tidy-up cannot narrow the rules to the retired names and reopen the hole under a new one. Prose updated to describe the new recipe.

Docs

apps/console/docs/error-tracking.md (rewritten last by #6601) is updated to describe only the final shape.

Its CSP section is kept accurate and nothing CSP-related was added to code, per the PM's premise correction on the card: the Console ships no CSPapps/console/index.html sets none and the repo defines no default policy — so there is no in-repo CSP obstacle. The section remains the hosting-layer note, with one addition earned by this change: a runtime-served DSN can be repointed at a different ingest origin with no Console rebuild, so a hosting CSP that was correct before can start dropping events with nothing else having moved.

Breaking

FROMTO
VITE_SENTRY_DSN in the Console build environmentOS_TELEMETRY_CLIENT_ERROR_REPORTING_DSN on the ObjectStack runtime
VITE_SENTRY_SEND_DEFAULT_PII=trueOS_TELEMETRY_CLIENT_ERROR_REPORTING_SEND_DEFAULT_PII=true
VITE_SENTRY_ENVIRONMENT / VITE_SENTRY_TRACES_SAMPLE_RATEthe matching OS_TELEMETRY_CLIENT_ERROR_REPORTING_* variables
VITE_SENTRY_REPLAY=trueOS_TELEMETRY_CLIENT_ERROR_REPORTING_REPLAY_SAMPLE_RATE=0.1
VITE_SENTRY_ENABLED=falseunset the runtime DSN
isClientErrorReportingAllowed(): booleangetClientErrorReporting(): RuntimeClientErrorReporting or null
resolveSentryGate(env, runtimeAllows)resolveSentryGate(runtimeErrorReporting)
RuntimeTelemetry.allowClientErrorReportingRuntimeTelemetry.errorReporting?

Changeset declares minor on @object-ui/app-shell and @object-ui/console — per this repo's rule that breaking changes ship as minor with the semantics spelled out in the body, never as major.

Compatibility — any landing order is safe

  • Old client + new server: a Console built before this change reads an absent telemetry.allowClientErrorReporting, its === true test denies ⇒ off.
  • New client + old server: this Console reads an absent DSN ⇒ off. Pinned by a test that feeds it exactly the intermediate runtime's payload, { telemetry: { allowClientErrorReporting: true } }, and asserts null.

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.

Validation

All of the following ran on 0657139 (this branch's head, working tree clean), exit codes captured before any pipe.

  • pnpm exec vitest run packages/app-shell/src/runtime-config.test.ts packages/app-shell/src/observability/sentry.test.ts packages/app-shell/src/observability/committed-telemetry-endpoint.test.ts3 files, 55 tests passed. Run from the repo root: the repo's own guard rejected the pnpm --filter form, which would have reported 22 passed from a different package entirely.
  • Full package suite pnpm exec vitest run packages/app-shell/558 files, 5400 passed, 1 skipped.
  • pnpm --filter @object-ui/app-shell run type-check (tsc --noEmit && tsc -p tsconfig.test.json) — clean. Verified with --listFiles that all 3 edited test files are inside that program, so "type-check clean" actually covers them.
  • pnpm --filter @object-ui/console run type-check — clean (after building the console dependency closure; the first run's Cannot find module '@object-ui/plugin-*' errors were unbuilt packages, not verdicts).
  • Gates derived from this repo's own package.json and .github/workflows/: check-control-bytes, check-changeset-presence, check-changeset-no-major, check:doc-fences, check:doc-snippets, check:doc-types, check:readme-exports, check:vi-mock-specifiers, check:self-import, check:phantom-deps, check:shell-escape-residue, check:spec-symbolsall green.
  • ESLint on the nine changed TS/TSX files: 0 errors, 7 warnings, all pre-existing no-explicit-any spellings — the per-file count is flat or lower than the merge base (sentry.test.ts went 2 to 0), and eslint . carries no --max-warnings. The narrowing is a measurement, not a skip: this repo's ESLint config sets no parserOptions.project and no typed rules, 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:

  • making the mirrored reader fail open on an empty DSN ⇒ 1 failed / 47 passed;
  • letting a truthy lookalike open the PII flow (=== true to !!) ⇒ 1 failed / 17 passed.

Declared NOT MEASURED

  • Repo-wide pnpm test and pnpm lint were not run; CI runs both. The affected package's full suite and the derived gate families above are the local half.
  • 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 never run a build of it,
so the build-time VITE_SENTRY_DSN half of the two-key gate was unreachable
for them -- they could neither turn reporting on nor turn PII collection
off. app-shell now reads the sink and its knobs from telemetry.errorReporting
on /api/v1/runtime/config, and the build-time DSN path is retired rather
than kept as a second source.
The gate collapses to one input, which is the fix rather than a side effect:
resolveSentryGate() takes the runtime payload alone, and its `reason` drops
from four values to two because "the runtime declined" and "no DSN arrived"
are now the same state, described from the one place an operator looks.
VITE_SENTRY_RELEASE survives as the only build-time knob: a release
identifies which bundle produced a stack trace and must match that build's
uploaded source maps, which no server can know.
initSentry() stays sequenced after initRuntimeConfig() and the ordering is
now more load-bearing, not less -- the DSN itself arrives from the server.
The committed-telemetry-endpoint ratchet keeps its rules unchanged; they key
on the variable suffix and the value, never on the VITE_ prefix, so they
already cover the runtime-side spelling. That is now pinned.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DKWDdUJ2XNRESVVWUvcpnh
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 52 chunks)3235.6 KB3266.6 KB
Main entry chunk (gzip)157.0 KB350 KB
Entry fileindex-DvWnMsNg.js
StatusPASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)11.89KB4.50KB
app-shell (runtime-config.js)20.61KB7.35KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)506.01KB114.64KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)173.10KB47.96KB
fields (index.js)238.89KB60.02KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)9.53KB3.38KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)4.64KB1.50KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)1.93KB0.88KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.85KB12.89KB
plugin-charts (index.js)64.66KB18.32KB
plugin-chatbot (index.js)188.60KB44.82KB
plugin-dashboard (index.js)133.48KB34.49KB
plugin-designer (index.js)212.80KB43.15KB
plugin-detail (index.js)245.29KB62.39KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)131.78KB32.19KB
plugin-gantt (index.js)165.16KB40.33KB
plugin-grid (index.js)201.66KB54.58KB
plugin-kanban (index.js)53.11KB14.62KB
plugin-list (index.js)112.86KB27.54KB
plugin-map (index.js)20.09KB6.62KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)26.72KB7.71KB
plugin-tree (index.js)9.26KB3.13KB
plugin-view (index.js)85.87KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)63.21KB21.05KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)2.44KB1.21KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)12.13KB3.65KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)7.54KB2.63KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@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 objectstack#12681; this seat shares the PR's author identity, so this is a COMMENT review — not a governed surface).

Read the full 1885-line diff. Findings, all positive:

  • The gate collapses to one input and the collapse is complete: resolveSentryGate(runtimeErrorReporting) takes no env argument any more — nothing a build was compiled with can influence whether reporting happens; reason honestly shrinks from four values to two, with runtime-denied collapsing INTO no-dsn because they are now the same state. The withheld verdict carries no PII and zero sample rates.
  • Every VITE_SENTRY_* retires except VITE_SENTRY_RELEASE, which is a label read at the call site, never a gate — with the source-maps reasoning recorded in three places a future editor would look.
  • The ratchet did not narrow: committed-telemetry-endpoint.test.ts rules are keyed on key-suffix and value, and the new counter-probes pin that a committed OS_TELEMETRY_CLIENT_ERROR_REPORTING_DSN in a .env file is caught exactly like the retired spelling — a later "tidy-up" cannot reopen the hole under the new name.
  • The mirror discipline is preserved: readClientErrorReporting is a declared line-for-line hand copy of the producer's export (this repo deliberately takes no dependency on @objectstack/cloud-connection), the secret-DSN guard is kept on this side as the last line against a third-party host, knobs are re-derived defensively (=== true for PII, finite 0..1 for rates, 0 kept as a real answer), and telemetry stays replace-not-merge so a withdrawn DSN withdraws.
  • The landing-order pin exists on this side too: a payload carrying only the replaced #10805 boolean reads as null.
  • Docs and comments (error-tracking.md, .env.production, main.tsx ordering note) describe only the final shape; the error-tracking CSP note gains the right new caveat — the DSN can now be repointed without a rebuild, so a hosting-layer CSP can silently go stale.

Cross-repo pair: objectstack#12697 (verified in its own review). Landing via the normal queue; the card closes when both halves are merged.


Generated by Claude Code

@os-zhuang
os-zhuang marked this pull request as ready for review August 27, 2026 10:37
@os-zhuang
os-zhuang enabled auto-merge August 27, 2026 10:37
@os-zhuang
os-zhuang added this pull request to the merge queueAug 27, 2026
Merged via the queue into main with commit 14ef9f5Aug 27, 2026
30 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-12681-runtime-dsn branch August 27, 2026 10:56
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Console error reporting: consume the DSN from runtime config - #6603

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

Console error reporting: consume the DSN from runtime config#6603
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 objectstack-ai/objectstack#12681 — the client half. The server half is objectstack-ai/objectstack#12697; neither PR closes the card on its own.

What this changes

The Console's error-reporting DSN — and every knob that travels with it — now arrives from the runtime, on telemetry.errorReporting of GET /api/v1/runtime/config. The build-time VITE_SENTRY_DSN path is retired rather than kept as a second source.

send ⇔ the runtime served a DSN

The maintainer's ruling on the card, verbatim and untranslated:

「我是一个开发平台呀,我的用户并不会去构建我的前端,我理解这种应该在服务端传进去。」

ObjectStack's users consume a prebuilt Console. Under the two-key gate a build-time key was unreachable for them, so a self-hosting operator could not enable client error reporting at all — the runtime permission was reachable and the source was not. They also could not turn sendDefaultPii off, which is the knob deciding whether IP and User-Agent leave their network.

The gate collapsed to one input, and that is the fix

resolveSentryGate() now takes the runtime payload alone — no env argument. That absence is the point: nothing a build was compiled with can influence whether reporting happens.

reason drops from four values to two. forced-off retired with VITE_SENTRY_ENABLED; runtime-deniedcollapsed intono-dsn, because once the DSN is the grant, "the runtime declined" and "no DSN arrived" are the same state — described from the one place an operator has to look. Turning reporting off is unsetting the server DSN; there is deliberately no build-time force-off left, because nobody consuming a prebuilt console could reach one.

The fail-closed posture is unchanged and structurally stronger: absence of a source is not a value that can be misread, where the boolean needed a strict === true plus a written argument about why a negative disabled flag would have been vacuous on exactly the runtimes that were leaking.

What moved to the runtime, and the one knob that did not

Moved into the payload: sendDefaultPii, environment, tracesSampleRate, replaysOnErrorSampleRate. Not new surface — the same surface relocated to the side that can operate it.

VITE_SENTRY_RELEASE stays build-time, and is now the only VITE_SENTRY_* variable that exists. A release identifies which bundle produced a stack trace and must match the source maps that bundle's pipeline uploaded; a server cannot know which Console build it is serving. It is a label on the events, never a gate, so it is read at the Sentry.init call site rather than inside the gate. VITE_SENTRY_ENABLED, VITE_SENTRY_ENVIRONMENT, VITE_SENTRY_TRACES_SAMPLE_RATE and VITE_SENTRY_REPLAY retire alongside VITE_SENTRY_DSN.

replaysSessionSampleRate stays hard-coded to 0 and is deliberately not authorable: whole-session replay is a strictly larger surface than error-session replay and nothing pulls it.

Ordering, and the ratchet

initSentry() stays sequenced after initRuntimeConfig() in apps/console/src/main.tsx, and the ordering is now more load-bearing, not less: the DSN itself is server-pushed, so a call at module-eval time freezes "no sink" for the session and turns the operator's only switch into a permanent removal. The comment there says so.

committed-telemetry-endpoint.test.ts keeps its rules unchanged — its job ("nothing endpoint-shaped is committed to this repo") is unchanged, and a committed DSN is still inlined into the published bundle. Its rules key on the variable's suffix and on the value, never on the VITE_ prefix, so they already covered the runtime-side spelling; two counter-probe assertions now pin that, so a later tidy-up cannot narrow the rules to the retired names and reopen the hole under a new one. Prose updated to describe the new recipe.

Docs

apps/console/docs/error-tracking.md (rewritten last by #6601) is updated to describe only the final shape.

Its CSP section is kept accurate and nothing CSP-related was added to code, per the PM's premise correction on the card: the Console ships no CSPapps/console/index.html sets none and the repo defines no default policy — so there is no in-repo CSP obstacle. The section remains the hosting-layer note, with one addition earned by this change: a runtime-served DSN can be repointed at a different ingest origin with no Console rebuild, so a hosting CSP that was correct before can start dropping events with nothing else having moved.

Breaking

FROMTO
VITE_SENTRY_DSN in the Console build environmentOS_TELEMETRY_CLIENT_ERROR_REPORTING_DSN on the ObjectStack runtime
VITE_SENTRY_SEND_DEFAULT_PII=trueOS_TELEMETRY_CLIENT_ERROR_REPORTING_SEND_DEFAULT_PII=true
VITE_SENTRY_ENVIRONMENT / VITE_SENTRY_TRACES_SAMPLE_RATEthe matching OS_TELEMETRY_CLIENT_ERROR_REPORTING_* variables
VITE_SENTRY_REPLAY=trueOS_TELEMETRY_CLIENT_ERROR_REPORTING_REPLAY_SAMPLE_RATE=0.1
VITE_SENTRY_ENABLED=falseunset the runtime DSN
isClientErrorReportingAllowed(): booleangetClientErrorReporting(): RuntimeClientErrorReporting or null
resolveSentryGate(env, runtimeAllows)resolveSentryGate(runtimeErrorReporting)
RuntimeTelemetry.allowClientErrorReportingRuntimeTelemetry.errorReporting?

Changeset declares minor on @object-ui/app-shell and @object-ui/console — per this repo's rule that breaking changes ship as minor with the semantics spelled out in the body, never as major.

Compatibility — any landing order is safe

  • Old client + new server: a Console built before this change reads an absent telemetry.allowClientErrorReporting, its === true test denies ⇒ off.
  • New client + old server: this Console reads an absent DSN ⇒ off. Pinned by a test that feeds it exactly the intermediate runtime's payload, { telemetry: { allowClientErrorReporting: true } }, and asserts null.

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.

Validation

All of the following ran on 0657139 (this branch's head, working tree clean), exit codes captured before any pipe.

  • pnpm exec vitest run packages/app-shell/src/runtime-config.test.ts packages/app-shell/src/observability/sentry.test.ts packages/app-shell/src/observability/committed-telemetry-endpoint.test.ts3 files, 55 tests passed. Run from the repo root: the repo's own guard rejected the pnpm --filter form, which would have reported 22 passed from a different package entirely.
  • Full package suite pnpm exec vitest run packages/app-shell/558 files, 5400 passed, 1 skipped.
  • pnpm --filter @object-ui/app-shell run type-check (tsc --noEmit && tsc -p tsconfig.test.json) — clean. Verified with --listFiles that all 3 edited test files are inside that program, so "type-check clean" actually covers them.
  • pnpm --filter @object-ui/console run type-check — clean (after building the console dependency closure; the first run's Cannot find module '@object-ui/plugin-*' errors were unbuilt packages, not verdicts).
  • Gates derived from this repo's own package.json and .github/workflows/: check-control-bytes, check-changeset-presence, check-changeset-no-major, check:doc-fences, check:doc-snippets, check:doc-types, check:readme-exports, check:vi-mock-specifiers, check:self-import, check:phantom-deps, check:shell-escape-residue, check:spec-symbolsall green.
  • ESLint on the nine changed TS/TSX files: 0 errors, 7 warnings, all pre-existing no-explicit-any spellings — the per-file count is flat or lower than the merge base (sentry.test.ts went 2 to 0), and eslint . carries no --max-warnings. The narrowing is a measurement, not a skip: this repo's ESLint config sets no parserOptions.project and no typed rules, 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:

  • making the mirrored reader fail open on an empty DSN ⇒ 1 failed / 47 passed;
  • letting a truthy lookalike open the PII flow (=== true to !!) ⇒ 1 failed / 17 passed.

Declared NOT MEASURED

  • Repo-wide pnpm test and pnpm lint were not run; CI runs both. The affected package's full suite and the derived gate families above are the local half.
  • 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 never run a build of it,
so the build-time VITE_SENTRY_DSN half of the two-key gate was unreachable
for them -- they could neither turn reporting on nor turn PII collection
off. app-shell now reads the sink and its knobs from telemetry.errorReporting
on /api/v1/runtime/config, and the build-time DSN path is retired rather
than kept as a second source.
The gate collapses to one input, which is the fix rather than a side effect:
resolveSentryGate() takes the runtime payload alone, and its `reason` drops
from four values to two because "the runtime declined" and "no DSN arrived"
are now the same state, described from the one place an operator looks.
VITE_SENTRY_RELEASE survives as the only build-time knob: a release
identifies which bundle produced a stack trace and must match that build's
uploaded source maps, which no server can know.
initSentry() stays sequenced after initRuntimeConfig() and the ordering is
now more load-bearing, not less -- the DSN itself arrives from the server.
The committed-telemetry-endpoint ratchet keeps its rules unchanged; they key
on the variable suffix and the value, never on the VITE_ prefix, so they
already cover the runtime-side spelling. That is now pinned.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DKWDdUJ2XNRESVVWUvcpnh
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 52 chunks)3235.6 KB3266.6 KB
Main entry chunk (gzip)157.0 KB350 KB
Entry fileindex-DvWnMsNg.js
StatusPASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)11.89KB4.50KB
app-shell (runtime-config.js)20.61KB7.35KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)506.01KB114.64KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)173.10KB47.96KB
fields (index.js)238.89KB60.02KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)9.53KB3.38KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)4.64KB1.50KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)1.93KB0.88KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.85KB12.89KB
plugin-charts (index.js)64.66KB18.32KB
plugin-chatbot (index.js)188.60KB44.82KB
plugin-dashboard (index.js)133.48KB34.49KB
plugin-designer (index.js)212.80KB43.15KB
plugin-detail (index.js)245.29KB62.39KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)131.78KB32.19KB
plugin-gantt (index.js)165.16KB40.33KB
plugin-grid (index.js)201.66KB54.58KB
plugin-kanban (index.js)53.11KB14.62KB
plugin-list (index.js)112.86KB27.54KB
plugin-map (index.js)20.09KB6.62KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)26.72KB7.71KB
plugin-tree (index.js)9.26KB3.13KB
plugin-view (index.js)85.87KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)63.21KB21.05KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)2.44KB1.21KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)12.13KB3.65KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)7.54KB2.63KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@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 objectstack#12681; this seat shares the PR's author identity, so this is a COMMENT review — not a governed surface).

Read the full 1885-line diff. Findings, all positive:

  • The gate collapses to one input and the collapse is complete: resolveSentryGate(runtimeErrorReporting) takes no env argument any more — nothing a build was compiled with can influence whether reporting happens; reason honestly shrinks from four values to two, with runtime-denied collapsing INTO no-dsn because they are now the same state. The withheld verdict carries no PII and zero sample rates.
  • Every VITE_SENTRY_* retires except VITE_SENTRY_RELEASE, which is a label read at the call site, never a gate — with the source-maps reasoning recorded in three places a future editor would look.
  • The ratchet did not narrow: committed-telemetry-endpoint.test.ts rules are keyed on key-suffix and value, and the new counter-probes pin that a committed OS_TELEMETRY_CLIENT_ERROR_REPORTING_DSN in a .env file is caught exactly like the retired spelling — a later "tidy-up" cannot reopen the hole under the new name.
  • The mirror discipline is preserved: readClientErrorReporting is a declared line-for-line hand copy of the producer's export (this repo deliberately takes no dependency on @objectstack/cloud-connection), the secret-DSN guard is kept on this side as the last line against a third-party host, knobs are re-derived defensively (=== true for PII, finite 0..1 for rates, 0 kept as a real answer), and telemetry stays replace-not-merge so a withdrawn DSN withdraws.
  • The landing-order pin exists on this side too: a payload carrying only the replaced #10805 boolean reads as null.
  • Docs and comments (error-tracking.md, .env.production, main.tsx ordering note) describe only the final shape; the error-tracking CSP note gains the right new caveat — the DSN can now be repointed without a rebuild, so a hosting-layer CSP can silently go stale.

Cross-repo pair: objectstack#12697 (verified in its own review). Landing via the normal queue; the card closes when both halves are merged.


Generated by Claude Code

@os-zhuang
os-zhuang marked this pull request as ready for review August 27, 2026 10:37
@os-zhuang
os-zhuang enabled auto-merge August 27, 2026 10:37
@os-zhuang
os-zhuang added this pull request to the merge queueAug 27, 2026
Merged via the queue into main with commit 14ef9f5Aug 27, 2026
30 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-12681-runtime-dsn branch August 27, 2026 10:56
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Console error reporting: consume the DSN from runtime config - #6603

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

Console error reporting: consume the DSN from runtime config#6603
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 objectstack-ai/objectstack#12681 — the client half. The server half is objectstack-ai/objectstack#12697; neither PR closes the card on its own.

What this changes

The Console's error-reporting DSN — and every knob that travels with it — now arrives from the runtime, on telemetry.errorReporting of GET /api/v1/runtime/config. The build-time VITE_SENTRY_DSN path is retired rather than kept as a second source.

send ⇔ the runtime served a DSN

The maintainer's ruling on the card, verbatim and untranslated:

「我是一个开发平台呀,我的用户并不会去构建我的前端,我理解这种应该在服务端传进去。」

ObjectStack's users consume a prebuilt Console. Under the two-key gate a build-time key was unreachable for them, so a self-hosting operator could not enable client error reporting at all — the runtime permission was reachable and the source was not. They also could not turn sendDefaultPii off, which is the knob deciding whether IP and User-Agent leave their network.

The gate collapsed to one input, and that is the fix

resolveSentryGate() now takes the runtime payload alone — no env argument. That absence is the point: nothing a build was compiled with can influence whether reporting happens.

reason drops from four values to two. forced-off retired with VITE_SENTRY_ENABLED; runtime-deniedcollapsed intono-dsn, because once the DSN is the grant, "the runtime declined" and "no DSN arrived" are the same state — described from the one place an operator has to look. Turning reporting off is unsetting the server DSN; there is deliberately no build-time force-off left, because nobody consuming a prebuilt console could reach one.

The fail-closed posture is unchanged and structurally stronger: absence of a source is not a value that can be misread, where the boolean needed a strict === true plus a written argument about why a negative disabled flag would have been vacuous on exactly the runtimes that were leaking.

What moved to the runtime, and the one knob that did not

Moved into the payload: sendDefaultPii, environment, tracesSampleRate, replaysOnErrorSampleRate. Not new surface — the same surface relocated to the side that can operate it.

VITE_SENTRY_RELEASE stays build-time, and is now the only VITE_SENTRY_* variable that exists. A release identifies which bundle produced a stack trace and must match the source maps that bundle's pipeline uploaded; a server cannot know which Console build it is serving. It is a label on the events, never a gate, so it is read at the Sentry.init call site rather than inside the gate. VITE_SENTRY_ENABLED, VITE_SENTRY_ENVIRONMENT, VITE_SENTRY_TRACES_SAMPLE_RATE and VITE_SENTRY_REPLAY retire alongside VITE_SENTRY_DSN.

replaysSessionSampleRate stays hard-coded to 0 and is deliberately not authorable: whole-session replay is a strictly larger surface than error-session replay and nothing pulls it.

Ordering, and the ratchet

initSentry() stays sequenced after initRuntimeConfig() in apps/console/src/main.tsx, and the ordering is now more load-bearing, not less: the DSN itself is server-pushed, so a call at module-eval time freezes "no sink" for the session and turns the operator's only switch into a permanent removal. The comment there says so.

committed-telemetry-endpoint.test.ts keeps its rules unchanged — its job ("nothing endpoint-shaped is committed to this repo") is unchanged, and a committed DSN is still inlined into the published bundle. Its rules key on the variable's suffix and on the value, never on the VITE_ prefix, so they already covered the runtime-side spelling; two counter-probe assertions now pin that, so a later tidy-up cannot narrow the rules to the retired names and reopen the hole under a new one. Prose updated to describe the new recipe.

Docs

apps/console/docs/error-tracking.md (rewritten last by #6601) is updated to describe only the final shape.

Its CSP section is kept accurate and nothing CSP-related was added to code, per the PM's premise correction on the card: the Console ships no CSPapps/console/index.html sets none and the repo defines no default policy — so there is no in-repo CSP obstacle. The section remains the hosting-layer note, with one addition earned by this change: a runtime-served DSN can be repointed at a different ingest origin with no Console rebuild, so a hosting CSP that was correct before can start dropping events with nothing else having moved.

Breaking

FROMTO
VITE_SENTRY_DSN in the Console build environmentOS_TELEMETRY_CLIENT_ERROR_REPORTING_DSN on the ObjectStack runtime
VITE_SENTRY_SEND_DEFAULT_PII=trueOS_TELEMETRY_CLIENT_ERROR_REPORTING_SEND_DEFAULT_PII=true
VITE_SENTRY_ENVIRONMENT / VITE_SENTRY_TRACES_SAMPLE_RATEthe matching OS_TELEMETRY_CLIENT_ERROR_REPORTING_* variables
VITE_SENTRY_REPLAY=trueOS_TELEMETRY_CLIENT_ERROR_REPORTING_REPLAY_SAMPLE_RATE=0.1
VITE_SENTRY_ENABLED=falseunset the runtime DSN
isClientErrorReportingAllowed(): booleangetClientErrorReporting(): RuntimeClientErrorReporting or null
resolveSentryGate(env, runtimeAllows)resolveSentryGate(runtimeErrorReporting)
RuntimeTelemetry.allowClientErrorReportingRuntimeTelemetry.errorReporting?

Changeset declares minor on @object-ui/app-shell and @object-ui/console — per this repo's rule that breaking changes ship as minor with the semantics spelled out in the body, never as major.

Compatibility — any landing order is safe

  • Old client + new server: a Console built before this change reads an absent telemetry.allowClientErrorReporting, its === true test denies ⇒ off.
  • New client + old server: this Console reads an absent DSN ⇒ off. Pinned by a test that feeds it exactly the intermediate runtime's payload, { telemetry: { allowClientErrorReporting: true } }, and asserts null.

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.

Validation

All of the following ran on 0657139 (this branch's head, working tree clean), exit codes captured before any pipe.

  • pnpm exec vitest run packages/app-shell/src/runtime-config.test.ts packages/app-shell/src/observability/sentry.test.ts packages/app-shell/src/observability/committed-telemetry-endpoint.test.ts3 files, 55 tests passed. Run from the repo root: the repo's own guard rejected the pnpm --filter form, which would have reported 22 passed from a different package entirely.
  • Full package suite pnpm exec vitest run packages/app-shell/558 files, 5400 passed, 1 skipped.
  • pnpm --filter @object-ui/app-shell run type-check (tsc --noEmit && tsc -p tsconfig.test.json) — clean. Verified with --listFiles that all 3 edited test files are inside that program, so "type-check clean" actually covers them.
  • pnpm --filter @object-ui/console run type-check — clean (after building the console dependency closure; the first run's Cannot find module '@object-ui/plugin-*' errors were unbuilt packages, not verdicts).
  • Gates derived from this repo's own package.json and .github/workflows/: check-control-bytes, check-changeset-presence, check-changeset-no-major, check:doc-fences, check:doc-snippets, check:doc-types, check:readme-exports, check:vi-mock-specifiers, check:self-import, check:phantom-deps, check:shell-escape-residue, check:spec-symbolsall green.
  • ESLint on the nine changed TS/TSX files: 0 errors, 7 warnings, all pre-existing no-explicit-any spellings — the per-file count is flat or lower than the merge base (sentry.test.ts went 2 to 0), and eslint . carries no --max-warnings. The narrowing is a measurement, not a skip: this repo's ESLint config sets no parserOptions.project and no typed rules, 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:

  • making the mirrored reader fail open on an empty DSN ⇒ 1 failed / 47 passed;
  • letting a truthy lookalike open the PII flow (=== true to !!) ⇒ 1 failed / 17 passed.

Declared NOT MEASURED

  • Repo-wide pnpm test and pnpm lint were not run; CI runs both. The affected package's full suite and the derived gate families above are the local half.
  • 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 never run a build of it,
so the build-time VITE_SENTRY_DSN half of the two-key gate was unreachable
for them -- they could neither turn reporting on nor turn PII collection
off. app-shell now reads the sink and its knobs from telemetry.errorReporting
on /api/v1/runtime/config, and the build-time DSN path is retired rather
than kept as a second source.
The gate collapses to one input, which is the fix rather than a side effect:
resolveSentryGate() takes the runtime payload alone, and its `reason` drops
from four values to two because "the runtime declined" and "no DSN arrived"
are now the same state, described from the one place an operator looks.
VITE_SENTRY_RELEASE survives as the only build-time knob: a release
identifies which bundle produced a stack trace and must match that build's
uploaded source maps, which no server can know.
initSentry() stays sequenced after initRuntimeConfig() and the ordering is
now more load-bearing, not less -- the DSN itself arrives from the server.
The committed-telemetry-endpoint ratchet keeps its rules unchanged; they key
on the variable suffix and the value, never on the VITE_ prefix, so they
already cover the runtime-side spelling. That is now pinned.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DKWDdUJ2XNRESVVWUvcpnh
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 52 chunks)3235.6 KB3266.6 KB
Main entry chunk (gzip)157.0 KB350 KB
Entry fileindex-DvWnMsNg.js
StatusPASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)11.89KB4.50KB
app-shell (runtime-config.js)20.61KB7.35KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)506.01KB114.64KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)173.10KB47.96KB
fields (index.js)238.89KB60.02KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)9.53KB3.38KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)4.64KB1.50KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)1.93KB0.88KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.85KB12.89KB
plugin-charts (index.js)64.66KB18.32KB
plugin-chatbot (index.js)188.60KB44.82KB
plugin-dashboard (index.js)133.48KB34.49KB
plugin-designer (index.js)212.80KB43.15KB
plugin-detail (index.js)245.29KB62.39KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)131.78KB32.19KB
plugin-gantt (index.js)165.16KB40.33KB
plugin-grid (index.js)201.66KB54.58KB
plugin-kanban (index.js)53.11KB14.62KB
plugin-list (index.js)112.86KB27.54KB
plugin-map (index.js)20.09KB6.62KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)26.72KB7.71KB
plugin-tree (index.js)9.26KB3.13KB
plugin-view (index.js)85.87KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)63.21KB21.05KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)2.44KB1.21KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)12.13KB3.65KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)7.54KB2.63KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@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 objectstack#12681; this seat shares the PR's author identity, so this is a COMMENT review — not a governed surface).

Read the full 1885-line diff. Findings, all positive:

  • The gate collapses to one input and the collapse is complete: resolveSentryGate(runtimeErrorReporting) takes no env argument any more — nothing a build was compiled with can influence whether reporting happens; reason honestly shrinks from four values to two, with runtime-denied collapsing INTO no-dsn because they are now the same state. The withheld verdict carries no PII and zero sample rates.
  • Every VITE_SENTRY_* retires except VITE_SENTRY_RELEASE, which is a label read at the call site, never a gate — with the source-maps reasoning recorded in three places a future editor would look.
  • The ratchet did not narrow: committed-telemetry-endpoint.test.ts rules are keyed on key-suffix and value, and the new counter-probes pin that a committed OS_TELEMETRY_CLIENT_ERROR_REPORTING_DSN in a .env file is caught exactly like the retired spelling — a later "tidy-up" cannot reopen the hole under the new name.
  • The mirror discipline is preserved: readClientErrorReporting is a declared line-for-line hand copy of the producer's export (this repo deliberately takes no dependency on @objectstack/cloud-connection), the secret-DSN guard is kept on this side as the last line against a third-party host, knobs are re-derived defensively (=== true for PII, finite 0..1 for rates, 0 kept as a real answer), and telemetry stays replace-not-merge so a withdrawn DSN withdraws.
  • The landing-order pin exists on this side too: a payload carrying only the replaced #10805 boolean reads as null.
  • Docs and comments (error-tracking.md, .env.production, main.tsx ordering note) describe only the final shape; the error-tracking CSP note gains the right new caveat — the DSN can now be repointed without a rebuild, so a hosting-layer CSP can silently go stale.

Cross-repo pair: objectstack#12697 (verified in its own review). Landing via the normal queue; the card closes when both halves are merged.


Generated by Claude Code

@os-zhuang
os-zhuang marked this pull request as ready for review August 27, 2026 10:37
@os-zhuang
os-zhuang enabled auto-merge August 27, 2026 10:37
@os-zhuang
os-zhuang added this pull request to the merge queueAug 27, 2026
Merged via the queue into main with commit 14ef9f5Aug 27, 2026
30 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-12681-runtime-dsn branch August 27, 2026 10:56
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Console error reporting: consume the DSN from runtime config - #6603

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

Console error reporting: consume the DSN from runtime config#6603
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 objectstack-ai/objectstack#12681 — the client half. The server half is objectstack-ai/objectstack#12697; neither PR closes the card on its own.

What this changes

The Console's error-reporting DSN — and every knob that travels with it — now arrives from the runtime, on telemetry.errorReporting of GET /api/v1/runtime/config. The build-time VITE_SENTRY_DSN path is retired rather than kept as a second source.

send ⇔ the runtime served a DSN

The maintainer's ruling on the card, verbatim and untranslated:

「我是一个开发平台呀,我的用户并不会去构建我的前端,我理解这种应该在服务端传进去。」

ObjectStack's users consume a prebuilt Console. Under the two-key gate a build-time key was unreachable for them, so a self-hosting operator could not enable client error reporting at all — the runtime permission was reachable and the source was not. They also could not turn sendDefaultPii off, which is the knob deciding whether IP and User-Agent leave their network.

The gate collapsed to one input, and that is the fix

resolveSentryGate() now takes the runtime payload alone — no env argument. That absence is the point: nothing a build was compiled with can influence whether reporting happens.

reason drops from four values to two. forced-off retired with VITE_SENTRY_ENABLED; runtime-deniedcollapsed intono-dsn, because once the DSN is the grant, "the runtime declined" and "no DSN arrived" are the same state — described from the one place an operator has to look. Turning reporting off is unsetting the server DSN; there is deliberately no build-time force-off left, because nobody consuming a prebuilt console could reach one.

The fail-closed posture is unchanged and structurally stronger: absence of a source is not a value that can be misread, where the boolean needed a strict === true plus a written argument about why a negative disabled flag would have been vacuous on exactly the runtimes that were leaking.

What moved to the runtime, and the one knob that did not

Moved into the payload: sendDefaultPii, environment, tracesSampleRate, replaysOnErrorSampleRate. Not new surface — the same surface relocated to the side that can operate it.

VITE_SENTRY_RELEASE stays build-time, and is now the only VITE_SENTRY_* variable that exists. A release identifies which bundle produced a stack trace and must match the source maps that bundle's pipeline uploaded; a server cannot know which Console build it is serving. It is a label on the events, never a gate, so it is read at the Sentry.init call site rather than inside the gate. VITE_SENTRY_ENABLED, VITE_SENTRY_ENVIRONMENT, VITE_SENTRY_TRACES_SAMPLE_RATE and VITE_SENTRY_REPLAY retire alongside VITE_SENTRY_DSN.

replaysSessionSampleRate stays hard-coded to 0 and is deliberately not authorable: whole-session replay is a strictly larger surface than error-session replay and nothing pulls it.

Ordering, and the ratchet

initSentry() stays sequenced after initRuntimeConfig() in apps/console/src/main.tsx, and the ordering is now more load-bearing, not less: the DSN itself is server-pushed, so a call at module-eval time freezes "no sink" for the session and turns the operator's only switch into a permanent removal. The comment there says so.

committed-telemetry-endpoint.test.ts keeps its rules unchanged — its job ("nothing endpoint-shaped is committed to this repo") is unchanged, and a committed DSN is still inlined into the published bundle. Its rules key on the variable's suffix and on the value, never on the VITE_ prefix, so they already covered the runtime-side spelling; two counter-probe assertions now pin that, so a later tidy-up cannot narrow the rules to the retired names and reopen the hole under a new one. Prose updated to describe the new recipe.

Docs

apps/console/docs/error-tracking.md (rewritten last by #6601) is updated to describe only the final shape.

Its CSP section is kept accurate and nothing CSP-related was added to code, per the PM's premise correction on the card: the Console ships no CSPapps/console/index.html sets none and the repo defines no default policy — so there is no in-repo CSP obstacle. The section remains the hosting-layer note, with one addition earned by this change: a runtime-served DSN can be repointed at a different ingest origin with no Console rebuild, so a hosting CSP that was correct before can start dropping events with nothing else having moved.

Breaking

FROMTO
VITE_SENTRY_DSN in the Console build environmentOS_TELEMETRY_CLIENT_ERROR_REPORTING_DSN on the ObjectStack runtime
VITE_SENTRY_SEND_DEFAULT_PII=trueOS_TELEMETRY_CLIENT_ERROR_REPORTING_SEND_DEFAULT_PII=true
VITE_SENTRY_ENVIRONMENT / VITE_SENTRY_TRACES_SAMPLE_RATEthe matching OS_TELEMETRY_CLIENT_ERROR_REPORTING_* variables
VITE_SENTRY_REPLAY=trueOS_TELEMETRY_CLIENT_ERROR_REPORTING_REPLAY_SAMPLE_RATE=0.1
VITE_SENTRY_ENABLED=falseunset the runtime DSN
isClientErrorReportingAllowed(): booleangetClientErrorReporting(): RuntimeClientErrorReporting or null
resolveSentryGate(env, runtimeAllows)resolveSentryGate(runtimeErrorReporting)
RuntimeTelemetry.allowClientErrorReportingRuntimeTelemetry.errorReporting?

Changeset declares minor on @object-ui/app-shell and @object-ui/console — per this repo's rule that breaking changes ship as minor with the semantics spelled out in the body, never as major.

Compatibility — any landing order is safe

  • Old client + new server: a Console built before this change reads an absent telemetry.allowClientErrorReporting, its === true test denies ⇒ off.
  • New client + old server: this Console reads an absent DSN ⇒ off. Pinned by a test that feeds it exactly the intermediate runtime's payload, { telemetry: { allowClientErrorReporting: true } }, and asserts null.

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.

Validation

All of the following ran on 0657139 (this branch's head, working tree clean), exit codes captured before any pipe.

  • pnpm exec vitest run packages/app-shell/src/runtime-config.test.ts packages/app-shell/src/observability/sentry.test.ts packages/app-shell/src/observability/committed-telemetry-endpoint.test.ts3 files, 55 tests passed. Run from the repo root: the repo's own guard rejected the pnpm --filter form, which would have reported 22 passed from a different package entirely.
  • Full package suite pnpm exec vitest run packages/app-shell/558 files, 5400 passed, 1 skipped.
  • pnpm --filter @object-ui/app-shell run type-check (tsc --noEmit && tsc -p tsconfig.test.json) — clean. Verified with --listFiles that all 3 edited test files are inside that program, so "type-check clean" actually covers them.
  • pnpm --filter @object-ui/console run type-check — clean (after building the console dependency closure; the first run's Cannot find module '@object-ui/plugin-*' errors were unbuilt packages, not verdicts).
  • Gates derived from this repo's own package.json and .github/workflows/: check-control-bytes, check-changeset-presence, check-changeset-no-major, check:doc-fences, check:doc-snippets, check:doc-types, check:readme-exports, check:vi-mock-specifiers, check:self-import, check:phantom-deps, check:shell-escape-residue, check:spec-symbolsall green.
  • ESLint on the nine changed TS/TSX files: 0 errors, 7 warnings, all pre-existing no-explicit-any spellings — the per-file count is flat or lower than the merge base (sentry.test.ts went 2 to 0), and eslint . carries no --max-warnings. The narrowing is a measurement, not a skip: this repo's ESLint config sets no parserOptions.project and no typed rules, 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:

  • making the mirrored reader fail open on an empty DSN ⇒ 1 failed / 47 passed;
  • letting a truthy lookalike open the PII flow (=== true to !!) ⇒ 1 failed / 17 passed.

Declared NOT MEASURED

  • Repo-wide pnpm test and pnpm lint were not run; CI runs both. The affected package's full suite and the derived gate families above are the local half.
  • 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 never run a build of it,
so the build-time VITE_SENTRY_DSN half of the two-key gate was unreachable
for them -- they could neither turn reporting on nor turn PII collection
off. app-shell now reads the sink and its knobs from telemetry.errorReporting
on /api/v1/runtime/config, and the build-time DSN path is retired rather
than kept as a second source.
The gate collapses to one input, which is the fix rather than a side effect:
resolveSentryGate() takes the runtime payload alone, and its `reason` drops
from four values to two because "the runtime declined" and "no DSN arrived"
are now the same state, described from the one place an operator looks.
VITE_SENTRY_RELEASE survives as the only build-time knob: a release
identifies which bundle produced a stack trace and must match that build's
uploaded source maps, which no server can know.
initSentry() stays sequenced after initRuntimeConfig() and the ordering is
now more load-bearing, not less -- the DSN itself arrives from the server.
The committed-telemetry-endpoint ratchet keeps its rules unchanged; they key
on the variable suffix and the value, never on the VITE_ prefix, so they
already cover the runtime-side spelling. That is now pinned.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DKWDdUJ2XNRESVVWUvcpnh
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 52 chunks)3235.6 KB3266.6 KB
Main entry chunk (gzip)157.0 KB350 KB
Entry fileindex-DvWnMsNg.js
StatusPASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)11.89KB4.50KB
app-shell (runtime-config.js)20.61KB7.35KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)506.01KB114.64KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)173.10KB47.96KB
fields (index.js)238.89KB60.02KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)9.53KB3.38KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)4.64KB1.50KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)1.93KB0.88KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.85KB12.89KB
plugin-charts (index.js)64.66KB18.32KB
plugin-chatbot (index.js)188.60KB44.82KB
plugin-dashboard (index.js)133.48KB34.49KB
plugin-designer (index.js)212.80KB43.15KB
plugin-detail (index.js)245.29KB62.39KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)131.78KB32.19KB
plugin-gantt (index.js)165.16KB40.33KB
plugin-grid (index.js)201.66KB54.58KB
plugin-kanban (index.js)53.11KB14.62KB
plugin-list (index.js)112.86KB27.54KB
plugin-map (index.js)20.09KB6.62KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)26.72KB7.71KB
plugin-tree (index.js)9.26KB3.13KB
plugin-view (index.js)85.87KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)63.21KB21.05KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)2.44KB1.21KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)12.13KB3.65KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)7.54KB2.63KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@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 objectstack#12681; this seat shares the PR's author identity, so this is a COMMENT review — not a governed surface).

Read the full 1885-line diff. Findings, all positive:

  • The gate collapses to one input and the collapse is complete: resolveSentryGate(runtimeErrorReporting) takes no env argument any more — nothing a build was compiled with can influence whether reporting happens; reason honestly shrinks from four values to two, with runtime-denied collapsing INTO no-dsn because they are now the same state. The withheld verdict carries no PII and zero sample rates.
  • Every VITE_SENTRY_* retires except VITE_SENTRY_RELEASE, which is a label read at the call site, never a gate — with the source-maps reasoning recorded in three places a future editor would look.
  • The ratchet did not narrow: committed-telemetry-endpoint.test.ts rules are keyed on key-suffix and value, and the new counter-probes pin that a committed OS_TELEMETRY_CLIENT_ERROR_REPORTING_DSN in a .env file is caught exactly like the retired spelling — a later "tidy-up" cannot reopen the hole under the new name.
  • The mirror discipline is preserved: readClientErrorReporting is a declared line-for-line hand copy of the producer's export (this repo deliberately takes no dependency on @objectstack/cloud-connection), the secret-DSN guard is kept on this side as the last line against a third-party host, knobs are re-derived defensively (=== true for PII, finite 0..1 for rates, 0 kept as a real answer), and telemetry stays replace-not-merge so a withdrawn DSN withdraws.
  • The landing-order pin exists on this side too: a payload carrying only the replaced #10805 boolean reads as null.
  • Docs and comments (error-tracking.md, .env.production, main.tsx ordering note) describe only the final shape; the error-tracking CSP note gains the right new caveat — the DSN can now be repointed without a rebuild, so a hosting-layer CSP can silently go stale.

Cross-repo pair: objectstack#12697 (verified in its own review). Landing via the normal queue; the card closes when both halves are merged.


Generated by Claude Code

@os-zhuang
os-zhuang marked this pull request as ready for review August 27, 2026 10:37
@os-zhuang
os-zhuang enabled auto-merge August 27, 2026 10:37
@os-zhuang
os-zhuang added this pull request to the merge queueAug 27, 2026
Merged via the queue into main with commit 14ef9f5Aug 27, 2026
30 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-12681-runtime-dsn branch August 27, 2026 10:56
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Console error reporting: consume the DSN from runtime config - #6603

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

Console error reporting: consume the DSN from runtime config#6603
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 objectstack-ai/objectstack#12681 — the client half. The server half is objectstack-ai/objectstack#12697; neither PR closes the card on its own.

What this changes

The Console's error-reporting DSN — and every knob that travels with it — now arrives from the runtime, on telemetry.errorReporting of GET /api/v1/runtime/config. The build-time VITE_SENTRY_DSN path is retired rather than kept as a second source.

send ⇔ the runtime served a DSN

The maintainer's ruling on the card, verbatim and untranslated:

「我是一个开发平台呀,我的用户并不会去构建我的前端,我理解这种应该在服务端传进去。」

ObjectStack's users consume a prebuilt Console. Under the two-key gate a build-time key was unreachable for them, so a self-hosting operator could not enable client error reporting at all — the runtime permission was reachable and the source was not. They also could not turn sendDefaultPii off, which is the knob deciding whether IP and User-Agent leave their network.

The gate collapsed to one input, and that is the fix

resolveSentryGate() now takes the runtime payload alone — no env argument. That absence is the point: nothing a build was compiled with can influence whether reporting happens.

reason drops from four values to two. forced-off retired with VITE_SENTRY_ENABLED; runtime-deniedcollapsed intono-dsn, because once the DSN is the grant, "the runtime declined" and "no DSN arrived" are the same state — described from the one place an operator has to look. Turning reporting off is unsetting the server DSN; there is deliberately no build-time force-off left, because nobody consuming a prebuilt console could reach one.

The fail-closed posture is unchanged and structurally stronger: absence of a source is not a value that can be misread, where the boolean needed a strict === true plus a written argument about why a negative disabled flag would have been vacuous on exactly the runtimes that were leaking.

What moved to the runtime, and the one knob that did not

Moved into the payload: sendDefaultPii, environment, tracesSampleRate, replaysOnErrorSampleRate. Not new surface — the same surface relocated to the side that can operate it.

VITE_SENTRY_RELEASE stays build-time, and is now the only VITE_SENTRY_* variable that exists. A release identifies which bundle produced a stack trace and must match the source maps that bundle's pipeline uploaded; a server cannot know which Console build it is serving. It is a label on the events, never a gate, so it is read at the Sentry.init call site rather than inside the gate. VITE_SENTRY_ENABLED, VITE_SENTRY_ENVIRONMENT, VITE_SENTRY_TRACES_SAMPLE_RATE and VITE_SENTRY_REPLAY retire alongside VITE_SENTRY_DSN.

replaysSessionSampleRate stays hard-coded to 0 and is deliberately not authorable: whole-session replay is a strictly larger surface than error-session replay and nothing pulls it.

Ordering, and the ratchet

initSentry() stays sequenced after initRuntimeConfig() in apps/console/src/main.tsx, and the ordering is now more load-bearing, not less: the DSN itself is server-pushed, so a call at module-eval time freezes "no sink" for the session and turns the operator's only switch into a permanent removal. The comment there says so.

committed-telemetry-endpoint.test.ts keeps its rules unchanged — its job ("nothing endpoint-shaped is committed to this repo") is unchanged, and a committed DSN is still inlined into the published bundle. Its rules key on the variable's suffix and on the value, never on the VITE_ prefix, so they already covered the runtime-side spelling; two counter-probe assertions now pin that, so a later tidy-up cannot narrow the rules to the retired names and reopen the hole under a new one. Prose updated to describe the new recipe.

Docs

apps/console/docs/error-tracking.md (rewritten last by #6601) is updated to describe only the final shape.

Its CSP section is kept accurate and nothing CSP-related was added to code, per the PM's premise correction on the card: the Console ships no CSPapps/console/index.html sets none and the repo defines no default policy — so there is no in-repo CSP obstacle. The section remains the hosting-layer note, with one addition earned by this change: a runtime-served DSN can be repointed at a different ingest origin with no Console rebuild, so a hosting CSP that was correct before can start dropping events with nothing else having moved.

Breaking

FROMTO
VITE_SENTRY_DSN in the Console build environmentOS_TELEMETRY_CLIENT_ERROR_REPORTING_DSN on the ObjectStack runtime
VITE_SENTRY_SEND_DEFAULT_PII=trueOS_TELEMETRY_CLIENT_ERROR_REPORTING_SEND_DEFAULT_PII=true
VITE_SENTRY_ENVIRONMENT / VITE_SENTRY_TRACES_SAMPLE_RATEthe matching OS_TELEMETRY_CLIENT_ERROR_REPORTING_* variables
VITE_SENTRY_REPLAY=trueOS_TELEMETRY_CLIENT_ERROR_REPORTING_REPLAY_SAMPLE_RATE=0.1
VITE_SENTRY_ENABLED=falseunset the runtime DSN
isClientErrorReportingAllowed(): booleangetClientErrorReporting(): RuntimeClientErrorReporting or null
resolveSentryGate(env, runtimeAllows)resolveSentryGate(runtimeErrorReporting)
RuntimeTelemetry.allowClientErrorReportingRuntimeTelemetry.errorReporting?

Changeset declares minor on @object-ui/app-shell and @object-ui/console — per this repo's rule that breaking changes ship as minor with the semantics spelled out in the body, never as major.

Compatibility — any landing order is safe

  • Old client + new server: a Console built before this change reads an absent telemetry.allowClientErrorReporting, its === true test denies ⇒ off.
  • New client + old server: this Console reads an absent DSN ⇒ off. Pinned by a test that feeds it exactly the intermediate runtime's payload, { telemetry: { allowClientErrorReporting: true } }, and asserts null.

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.

Validation

All of the following ran on 0657139 (this branch's head, working tree clean), exit codes captured before any pipe.

  • pnpm exec vitest run packages/app-shell/src/runtime-config.test.ts packages/app-shell/src/observability/sentry.test.ts packages/app-shell/src/observability/committed-telemetry-endpoint.test.ts3 files, 55 tests passed. Run from the repo root: the repo's own guard rejected the pnpm --filter form, which would have reported 22 passed from a different package entirely.
  • Full package suite pnpm exec vitest run packages/app-shell/558 files, 5400 passed, 1 skipped.
  • pnpm --filter @object-ui/app-shell run type-check (tsc --noEmit && tsc -p tsconfig.test.json) — clean. Verified with --listFiles that all 3 edited test files are inside that program, so "type-check clean" actually covers them.
  • pnpm --filter @object-ui/console run type-check — clean (after building the console dependency closure; the first run's Cannot find module '@object-ui/plugin-*' errors were unbuilt packages, not verdicts).
  • Gates derived from this repo's own package.json and .github/workflows/: check-control-bytes, check-changeset-presence, check-changeset-no-major, check:doc-fences, check:doc-snippets, check:doc-types, check:readme-exports, check:vi-mock-specifiers, check:self-import, check:phantom-deps, check:shell-escape-residue, check:spec-symbolsall green.
  • ESLint on the nine changed TS/TSX files: 0 errors, 7 warnings, all pre-existing no-explicit-any spellings — the per-file count is flat or lower than the merge base (sentry.test.ts went 2 to 0), and eslint . carries no --max-warnings. The narrowing is a measurement, not a skip: this repo's ESLint config sets no parserOptions.project and no typed rules, 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:

  • making the mirrored reader fail open on an empty DSN ⇒ 1 failed / 47 passed;
  • letting a truthy lookalike open the PII flow (=== true to !!) ⇒ 1 failed / 17 passed.

Declared NOT MEASURED

  • Repo-wide pnpm test and pnpm lint were not run; CI runs both. The affected package's full suite and the derived gate families above are the local half.
  • 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 never run a build of it,
so the build-time VITE_SENTRY_DSN half of the two-key gate was unreachable
for them -- they could neither turn reporting on nor turn PII collection
off. app-shell now reads the sink and its knobs from telemetry.errorReporting
on /api/v1/runtime/config, and the build-time DSN path is retired rather
than kept as a second source.
The gate collapses to one input, which is the fix rather than a side effect:
resolveSentryGate() takes the runtime payload alone, and its `reason` drops
from four values to two because "the runtime declined" and "no DSN arrived"
are now the same state, described from the one place an operator looks.
VITE_SENTRY_RELEASE survives as the only build-time knob: a release
identifies which bundle produced a stack trace and must match that build's
uploaded source maps, which no server can know.
initSentry() stays sequenced after initRuntimeConfig() and the ordering is
now more load-bearing, not less -- the DSN itself arrives from the server.
The committed-telemetry-endpoint ratchet keeps its rules unchanged; they key
on the variable suffix and the value, never on the VITE_ prefix, so they
already cover the runtime-side spelling. That is now pinned.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DKWDdUJ2XNRESVVWUvcpnh
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 52 chunks)3235.6 KB3266.6 KB
Main entry chunk (gzip)157.0 KB350 KB
Entry fileindex-DvWnMsNg.js
StatusPASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)11.89KB4.50KB
app-shell (runtime-config.js)20.61KB7.35KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)506.01KB114.64KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)173.10KB47.96KB
fields (index.js)238.89KB60.02KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)9.53KB3.38KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)4.64KB1.50KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)1.93KB0.88KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.85KB12.89KB
plugin-charts (index.js)64.66KB18.32KB
plugin-chatbot (index.js)188.60KB44.82KB
plugin-dashboard (index.js)133.48KB34.49KB
plugin-designer (index.js)212.80KB43.15KB
plugin-detail (index.js)245.29KB62.39KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)131.78KB32.19KB
plugin-gantt (index.js)165.16KB40.33KB
plugin-grid (index.js)201.66KB54.58KB
plugin-kanban (index.js)53.11KB14.62KB
plugin-list (index.js)112.86KB27.54KB
plugin-map (index.js)20.09KB6.62KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)26.72KB7.71KB
plugin-tree (index.js)9.26KB3.13KB
plugin-view (index.js)85.87KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)63.21KB21.05KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)2.44KB1.21KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)12.13KB3.65KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)7.54KB2.63KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@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 objectstack#12681; this seat shares the PR's author identity, so this is a COMMENT review — not a governed surface).

Read the full 1885-line diff. Findings, all positive:

  • The gate collapses to one input and the collapse is complete: resolveSentryGate(runtimeErrorReporting) takes no env argument any more — nothing a build was compiled with can influence whether reporting happens; reason honestly shrinks from four values to two, with runtime-denied collapsing INTO no-dsn because they are now the same state. The withheld verdict carries no PII and zero sample rates.
  • Every VITE_SENTRY_* retires except VITE_SENTRY_RELEASE, which is a label read at the call site, never a gate — with the source-maps reasoning recorded in three places a future editor would look.
  • The ratchet did not narrow: committed-telemetry-endpoint.test.ts rules are keyed on key-suffix and value, and the new counter-probes pin that a committed OS_TELEMETRY_CLIENT_ERROR_REPORTING_DSN in a .env file is caught exactly like the retired spelling — a later "tidy-up" cannot reopen the hole under the new name.
  • The mirror discipline is preserved: readClientErrorReporting is a declared line-for-line hand copy of the producer's export (this repo deliberately takes no dependency on @objectstack/cloud-connection), the secret-DSN guard is kept on this side as the last line against a third-party host, knobs are re-derived defensively (=== true for PII, finite 0..1 for rates, 0 kept as a real answer), and telemetry stays replace-not-merge so a withdrawn DSN withdraws.
  • The landing-order pin exists on this side too: a payload carrying only the replaced #10805 boolean reads as null.
  • Docs and comments (error-tracking.md, .env.production, main.tsx ordering note) describe only the final shape; the error-tracking CSP note gains the right new caveat — the DSN can now be repointed without a rebuild, so a hosting-layer CSP can silently go stale.

Cross-repo pair: objectstack#12697 (verified in its own review). Landing via the normal queue; the card closes when both halves are merged.


Generated by Claude Code

@os-zhuang
os-zhuang marked this pull request as ready for review August 27, 2026 10:37
@os-zhuang
os-zhuang enabled auto-merge August 27, 2026 10:37
@os-zhuang
os-zhuang added this pull request to the merge queueAug 27, 2026
Merged via the queue into main with commit 14ef9f5Aug 27, 2026
30 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-12681-runtime-dsn branch August 27, 2026 10:56
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Console error reporting: consume the DSN from runtime config - #6603

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

Console error reporting: consume the DSN from runtime config#6603
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 objectstack-ai/objectstack#12681 — the client half. The server half is objectstack-ai/objectstack#12697; neither PR closes the card on its own.

What this changes

The Console's error-reporting DSN — and every knob that travels with it — now arrives from the runtime, on telemetry.errorReporting of GET /api/v1/runtime/config. The build-time VITE_SENTRY_DSN path is retired rather than kept as a second source.

send ⇔ the runtime served a DSN

The maintainer's ruling on the card, verbatim and untranslated:

「我是一个开发平台呀,我的用户并不会去构建我的前端,我理解这种应该在服务端传进去。」

ObjectStack's users consume a prebuilt Console. Under the two-key gate a build-time key was unreachable for them, so a self-hosting operator could not enable client error reporting at all — the runtime permission was reachable and the source was not. They also could not turn sendDefaultPii off, which is the knob deciding whether IP and User-Agent leave their network.

The gate collapsed to one input, and that is the fix

resolveSentryGate() now takes the runtime payload alone — no env argument. That absence is the point: nothing a build was compiled with can influence whether reporting happens.

reason drops from four values to two. forced-off retired with VITE_SENTRY_ENABLED; runtime-deniedcollapsed intono-dsn, because once the DSN is the grant, "the runtime declined" and "no DSN arrived" are the same state — described from the one place an operator has to look. Turning reporting off is unsetting the server DSN; there is deliberately no build-time force-off left, because nobody consuming a prebuilt console could reach one.

The fail-closed posture is unchanged and structurally stronger: absence of a source is not a value that can be misread, where the boolean needed a strict === true plus a written argument about why a negative disabled flag would have been vacuous on exactly the runtimes that were leaking.

What moved to the runtime, and the one knob that did not

Moved into the payload: sendDefaultPii, environment, tracesSampleRate, replaysOnErrorSampleRate. Not new surface — the same surface relocated to the side that can operate it.

VITE_SENTRY_RELEASE stays build-time, and is now the only VITE_SENTRY_* variable that exists. A release identifies which bundle produced a stack trace and must match the source maps that bundle's pipeline uploaded; a server cannot know which Console build it is serving. It is a label on the events, never a gate, so it is read at the Sentry.init call site rather than inside the gate. VITE_SENTRY_ENABLED, VITE_SENTRY_ENVIRONMENT, VITE_SENTRY_TRACES_SAMPLE_RATE and VITE_SENTRY_REPLAY retire alongside VITE_SENTRY_DSN.

replaysSessionSampleRate stays hard-coded to 0 and is deliberately not authorable: whole-session replay is a strictly larger surface than error-session replay and nothing pulls it.

Ordering, and the ratchet

initSentry() stays sequenced after initRuntimeConfig() in apps/console/src/main.tsx, and the ordering is now more load-bearing, not less: the DSN itself is server-pushed, so a call at module-eval time freezes "no sink" for the session and turns the operator's only switch into a permanent removal. The comment there says so.

committed-telemetry-endpoint.test.ts keeps its rules unchanged — its job ("nothing endpoint-shaped is committed to this repo") is unchanged, and a committed DSN is still inlined into the published bundle. Its rules key on the variable's suffix and on the value, never on the VITE_ prefix, so they already covered the runtime-side spelling; two counter-probe assertions now pin that, so a later tidy-up cannot narrow the rules to the retired names and reopen the hole under a new one. Prose updated to describe the new recipe.

Docs

apps/console/docs/error-tracking.md (rewritten last by #6601) is updated to describe only the final shape.

Its CSP section is kept accurate and nothing CSP-related was added to code, per the PM's premise correction on the card: the Console ships no CSPapps/console/index.html sets none and the repo defines no default policy — so there is no in-repo CSP obstacle. The section remains the hosting-layer note, with one addition earned by this change: a runtime-served DSN can be repointed at a different ingest origin with no Console rebuild, so a hosting CSP that was correct before can start dropping events with nothing else having moved.

Breaking

FROMTO
VITE_SENTRY_DSN in the Console build environmentOS_TELEMETRY_CLIENT_ERROR_REPORTING_DSN on the ObjectStack runtime
VITE_SENTRY_SEND_DEFAULT_PII=trueOS_TELEMETRY_CLIENT_ERROR_REPORTING_SEND_DEFAULT_PII=true
VITE_SENTRY_ENVIRONMENT / VITE_SENTRY_TRACES_SAMPLE_RATEthe matching OS_TELEMETRY_CLIENT_ERROR_REPORTING_* variables
VITE_SENTRY_REPLAY=trueOS_TELEMETRY_CLIENT_ERROR_REPORTING_REPLAY_SAMPLE_RATE=0.1
VITE_SENTRY_ENABLED=falseunset the runtime DSN
isClientErrorReportingAllowed(): booleangetClientErrorReporting(): RuntimeClientErrorReporting or null
resolveSentryGate(env, runtimeAllows)resolveSentryGate(runtimeErrorReporting)
RuntimeTelemetry.allowClientErrorReportingRuntimeTelemetry.errorReporting?

Changeset declares minor on @object-ui/app-shell and @object-ui/console — per this repo's rule that breaking changes ship as minor with the semantics spelled out in the body, never as major.

Compatibility — any landing order is safe

  • Old client + new server: a Console built before this change reads an absent telemetry.allowClientErrorReporting, its === true test denies ⇒ off.
  • New client + old server: this Console reads an absent DSN ⇒ off. Pinned by a test that feeds it exactly the intermediate runtime's payload, { telemetry: { allowClientErrorReporting: true } }, and asserts null.

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.

Validation

All of the following ran on 0657139 (this branch's head, working tree clean), exit codes captured before any pipe.

  • pnpm exec vitest run packages/app-shell/src/runtime-config.test.ts packages/app-shell/src/observability/sentry.test.ts packages/app-shell/src/observability/committed-telemetry-endpoint.test.ts3 files, 55 tests passed. Run from the repo root: the repo's own guard rejected the pnpm --filter form, which would have reported 22 passed from a different package entirely.
  • Full package suite pnpm exec vitest run packages/app-shell/558 files, 5400 passed, 1 skipped.
  • pnpm --filter @object-ui/app-shell run type-check (tsc --noEmit && tsc -p tsconfig.test.json) — clean. Verified with --listFiles that all 3 edited test files are inside that program, so "type-check clean" actually covers them.
  • pnpm --filter @object-ui/console run type-check — clean (after building the console dependency closure; the first run's Cannot find module '@object-ui/plugin-*' errors were unbuilt packages, not verdicts).
  • Gates derived from this repo's own package.json and .github/workflows/: check-control-bytes, check-changeset-presence, check-changeset-no-major, check:doc-fences, check:doc-snippets, check:doc-types, check:readme-exports, check:vi-mock-specifiers, check:self-import, check:phantom-deps, check:shell-escape-residue, check:spec-symbolsall green.
  • ESLint on the nine changed TS/TSX files: 0 errors, 7 warnings, all pre-existing no-explicit-any spellings — the per-file count is flat or lower than the merge base (sentry.test.ts went 2 to 0), and eslint . carries no --max-warnings. The narrowing is a measurement, not a skip: this repo's ESLint config sets no parserOptions.project and no typed rules, 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:

  • making the mirrored reader fail open on an empty DSN ⇒ 1 failed / 47 passed;
  • letting a truthy lookalike open the PII flow (=== true to !!) ⇒ 1 failed / 17 passed.

Declared NOT MEASURED

  • Repo-wide pnpm test and pnpm lint were not run; CI runs both. The affected package's full suite and the derived gate families above are the local half.
  • 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 never run a build of it,
so the build-time VITE_SENTRY_DSN half of the two-key gate was unreachable
for them -- they could neither turn reporting on nor turn PII collection
off. app-shell now reads the sink and its knobs from telemetry.errorReporting
on /api/v1/runtime/config, and the build-time DSN path is retired rather
than kept as a second source.
The gate collapses to one input, which is the fix rather than a side effect:
resolveSentryGate() takes the runtime payload alone, and its `reason` drops
from four values to two because "the runtime declined" and "no DSN arrived"
are now the same state, described from the one place an operator looks.
VITE_SENTRY_RELEASE survives as the only build-time knob: a release
identifies which bundle produced a stack trace and must match that build's
uploaded source maps, which no server can know.
initSentry() stays sequenced after initRuntimeConfig() and the ordering is
now more load-bearing, not less -- the DSN itself arrives from the server.
The committed-telemetry-endpoint ratchet keeps its rules unchanged; they key
on the variable suffix and the value, never on the VITE_ prefix, so they
already cover the runtime-side spelling. That is now pinned.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DKWDdUJ2XNRESVVWUvcpnh
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 52 chunks)3235.6 KB3266.6 KB
Main entry chunk (gzip)157.0 KB350 KB
Entry fileindex-DvWnMsNg.js
StatusPASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)11.89KB4.50KB
app-shell (runtime-config.js)20.61KB7.35KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)506.01KB114.64KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)173.10KB47.96KB
fields (index.js)238.89KB60.02KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)9.53KB3.38KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)4.64KB1.50KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)1.93KB0.88KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.85KB12.89KB
plugin-charts (index.js)64.66KB18.32KB
plugin-chatbot (index.js)188.60KB44.82KB
plugin-dashboard (index.js)133.48KB34.49KB
plugin-designer (index.js)212.80KB43.15KB
plugin-detail (index.js)245.29KB62.39KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)131.78KB32.19KB
plugin-gantt (index.js)165.16KB40.33KB
plugin-grid (index.js)201.66KB54.58KB
plugin-kanban (index.js)53.11KB14.62KB
plugin-list (index.js)112.86KB27.54KB
plugin-map (index.js)20.09KB6.62KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)26.72KB7.71KB
plugin-tree (index.js)9.26KB3.13KB
plugin-view (index.js)85.87KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)63.21KB21.05KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)2.44KB1.21KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)12.13KB3.65KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)7.54KB2.63KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@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 objectstack#12681; this seat shares the PR's author identity, so this is a COMMENT review — not a governed surface).

Read the full 1885-line diff. Findings, all positive:

  • The gate collapses to one input and the collapse is complete: resolveSentryGate(runtimeErrorReporting) takes no env argument any more — nothing a build was compiled with can influence whether reporting happens; reason honestly shrinks from four values to two, with runtime-denied collapsing INTO no-dsn because they are now the same state. The withheld verdict carries no PII and zero sample rates.
  • Every VITE_SENTRY_* retires except VITE_SENTRY_RELEASE, which is a label read at the call site, never a gate — with the source-maps reasoning recorded in three places a future editor would look.
  • The ratchet did not narrow: committed-telemetry-endpoint.test.ts rules are keyed on key-suffix and value, and the new counter-probes pin that a committed OS_TELEMETRY_CLIENT_ERROR_REPORTING_DSN in a .env file is caught exactly like the retired spelling — a later "tidy-up" cannot reopen the hole under the new name.
  • The mirror discipline is preserved: readClientErrorReporting is a declared line-for-line hand copy of the producer's export (this repo deliberately takes no dependency on @objectstack/cloud-connection), the secret-DSN guard is kept on this side as the last line against a third-party host, knobs are re-derived defensively (=== true for PII, finite 0..1 for rates, 0 kept as a real answer), and telemetry stays replace-not-merge so a withdrawn DSN withdraws.
  • The landing-order pin exists on this side too: a payload carrying only the replaced #10805 boolean reads as null.
  • Docs and comments (error-tracking.md, .env.production, main.tsx ordering note) describe only the final shape; the error-tracking CSP note gains the right new caveat — the DSN can now be repointed without a rebuild, so a hosting-layer CSP can silently go stale.

Cross-repo pair: objectstack#12697 (verified in its own review). Landing via the normal queue; the card closes when both halves are merged.


Generated by Claude Code

@os-zhuang
os-zhuang marked this pull request as ready for review August 27, 2026 10:37
@os-zhuang
os-zhuang enabled auto-merge August 27, 2026 10:37
@os-zhuang
os-zhuang added this pull request to the merge queueAug 27, 2026
Merged via the queue into main with commit 14ef9f5Aug 27, 2026
30 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-12681-runtime-dsn branch August 27, 2026 10:56
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Console error reporting: consume the DSN from runtime config - #6603

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

Console error reporting: consume the DSN from runtime config#6603
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 objectstack-ai/objectstack#12681 — the client half. The server half is objectstack-ai/objectstack#12697; neither PR closes the card on its own.

What this changes

The Console's error-reporting DSN — and every knob that travels with it — now arrives from the runtime, on telemetry.errorReporting of GET /api/v1/runtime/config. The build-time VITE_SENTRY_DSN path is retired rather than kept as a second source.

send ⇔ the runtime served a DSN

The maintainer's ruling on the card, verbatim and untranslated:

「我是一个开发平台呀,我的用户并不会去构建我的前端,我理解这种应该在服务端传进去。」

ObjectStack's users consume a prebuilt Console. Under the two-key gate a build-time key was unreachable for them, so a self-hosting operator could not enable client error reporting at all — the runtime permission was reachable and the source was not. They also could not turn sendDefaultPii off, which is the knob deciding whether IP and User-Agent leave their network.

The gate collapsed to one input, and that is the fix

resolveSentryGate() now takes the runtime payload alone — no env argument. That absence is the point: nothing a build was compiled with can influence whether reporting happens.

reason drops from four values to two. forced-off retired with VITE_SENTRY_ENABLED; runtime-deniedcollapsed intono-dsn, because once the DSN is the grant, "the runtime declined" and "no DSN arrived" are the same state — described from the one place an operator has to look. Turning reporting off is unsetting the server DSN; there is deliberately no build-time force-off left, because nobody consuming a prebuilt console could reach one.

The fail-closed posture is unchanged and structurally stronger: absence of a source is not a value that can be misread, where the boolean needed a strict === true plus a written argument about why a negative disabled flag would have been vacuous on exactly the runtimes that were leaking.

What moved to the runtime, and the one knob that did not

Moved into the payload: sendDefaultPii, environment, tracesSampleRate, replaysOnErrorSampleRate. Not new surface — the same surface relocated to the side that can operate it.

VITE_SENTRY_RELEASE stays build-time, and is now the only VITE_SENTRY_* variable that exists. A release identifies which bundle produced a stack trace and must match the source maps that bundle's pipeline uploaded; a server cannot know which Console build it is serving. It is a label on the events, never a gate, so it is read at the Sentry.init call site rather than inside the gate. VITE_SENTRY_ENABLED, VITE_SENTRY_ENVIRONMENT, VITE_SENTRY_TRACES_SAMPLE_RATE and VITE_SENTRY_REPLAY retire alongside VITE_SENTRY_DSN.

replaysSessionSampleRate stays hard-coded to 0 and is deliberately not authorable: whole-session replay is a strictly larger surface than error-session replay and nothing pulls it.

Ordering, and the ratchet

initSentry() stays sequenced after initRuntimeConfig() in apps/console/src/main.tsx, and the ordering is now more load-bearing, not less: the DSN itself is server-pushed, so a call at module-eval time freezes "no sink" for the session and turns the operator's only switch into a permanent removal. The comment there says so.

committed-telemetry-endpoint.test.ts keeps its rules unchanged — its job ("nothing endpoint-shaped is committed to this repo") is unchanged, and a committed DSN is still inlined into the published bundle. Its rules key on the variable's suffix and on the value, never on the VITE_ prefix, so they already covered the runtime-side spelling; two counter-probe assertions now pin that, so a later tidy-up cannot narrow the rules to the retired names and reopen the hole under a new one. Prose updated to describe the new recipe.

Docs

apps/console/docs/error-tracking.md (rewritten last by #6601) is updated to describe only the final shape.

Its CSP section is kept accurate and nothing CSP-related was added to code, per the PM's premise correction on the card: the Console ships no CSPapps/console/index.html sets none and the repo defines no default policy — so there is no in-repo CSP obstacle. The section remains the hosting-layer note, with one addition earned by this change: a runtime-served DSN can be repointed at a different ingest origin with no Console rebuild, so a hosting CSP that was correct before can start dropping events with nothing else having moved.

Breaking

FROMTO
VITE_SENTRY_DSN in the Console build environmentOS_TELEMETRY_CLIENT_ERROR_REPORTING_DSN on the ObjectStack runtime
VITE_SENTRY_SEND_DEFAULT_PII=trueOS_TELEMETRY_CLIENT_ERROR_REPORTING_SEND_DEFAULT_PII=true
VITE_SENTRY_ENVIRONMENT / VITE_SENTRY_TRACES_SAMPLE_RATEthe matching OS_TELEMETRY_CLIENT_ERROR_REPORTING_* variables
VITE_SENTRY_REPLAY=trueOS_TELEMETRY_CLIENT_ERROR_REPORTING_REPLAY_SAMPLE_RATE=0.1
VITE_SENTRY_ENABLED=falseunset the runtime DSN
isClientErrorReportingAllowed(): booleangetClientErrorReporting(): RuntimeClientErrorReporting or null
resolveSentryGate(env, runtimeAllows)resolveSentryGate(runtimeErrorReporting)
RuntimeTelemetry.allowClientErrorReportingRuntimeTelemetry.errorReporting?

Changeset declares minor on @object-ui/app-shell and @object-ui/console — per this repo's rule that breaking changes ship as minor with the semantics spelled out in the body, never as major.

Compatibility — any landing order is safe

  • Old client + new server: a Console built before this change reads an absent telemetry.allowClientErrorReporting, its === true test denies ⇒ off.
  • New client + old server: this Console reads an absent DSN ⇒ off. Pinned by a test that feeds it exactly the intermediate runtime's payload, { telemetry: { allowClientErrorReporting: true } }, and asserts null.

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.

Validation

All of the following ran on 0657139 (this branch's head, working tree clean), exit codes captured before any pipe.

  • pnpm exec vitest run packages/app-shell/src/runtime-config.test.ts packages/app-shell/src/observability/sentry.test.ts packages/app-shell/src/observability/committed-telemetry-endpoint.test.ts3 files, 55 tests passed. Run from the repo root: the repo's own guard rejected the pnpm --filter form, which would have reported 22 passed from a different package entirely.
  • Full package suite pnpm exec vitest run packages/app-shell/558 files, 5400 passed, 1 skipped.
  • pnpm --filter @object-ui/app-shell run type-check (tsc --noEmit && tsc -p tsconfig.test.json) — clean. Verified with --listFiles that all 3 edited test files are inside that program, so "type-check clean" actually covers them.
  • pnpm --filter @object-ui/console run type-check — clean (after building the console dependency closure; the first run's Cannot find module '@object-ui/plugin-*' errors were unbuilt packages, not verdicts).
  • Gates derived from this repo's own package.json and .github/workflows/: check-control-bytes, check-changeset-presence, check-changeset-no-major, check:doc-fences, check:doc-snippets, check:doc-types, check:readme-exports, check:vi-mock-specifiers, check:self-import, check:phantom-deps, check:shell-escape-residue, check:spec-symbolsall green.
  • ESLint on the nine changed TS/TSX files: 0 errors, 7 warnings, all pre-existing no-explicit-any spellings — the per-file count is flat or lower than the merge base (sentry.test.ts went 2 to 0), and eslint . carries no --max-warnings. The narrowing is a measurement, not a skip: this repo's ESLint config sets no parserOptions.project and no typed rules, 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:

  • making the mirrored reader fail open on an empty DSN ⇒ 1 failed / 47 passed;
  • letting a truthy lookalike open the PII flow (=== true to !!) ⇒ 1 failed / 17 passed.

Declared NOT MEASURED

  • Repo-wide pnpm test and pnpm lint were not run; CI runs both. The affected package's full suite and the derived gate families above are the local half.
  • 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 never run a build of it,
so the build-time VITE_SENTRY_DSN half of the two-key gate was unreachable
for them -- they could neither turn reporting on nor turn PII collection
off. app-shell now reads the sink and its knobs from telemetry.errorReporting
on /api/v1/runtime/config, and the build-time DSN path is retired rather
than kept as a second source.
The gate collapses to one input, which is the fix rather than a side effect:
resolveSentryGate() takes the runtime payload alone, and its `reason` drops
from four values to two because "the runtime declined" and "no DSN arrived"
are now the same state, described from the one place an operator looks.
VITE_SENTRY_RELEASE survives as the only build-time knob: a release
identifies which bundle produced a stack trace and must match that build's
uploaded source maps, which no server can know.
initSentry() stays sequenced after initRuntimeConfig() and the ordering is
now more load-bearing, not less -- the DSN itself arrives from the server.
The committed-telemetry-endpoint ratchet keeps its rules unchanged; they key
on the variable suffix and the value, never on the VITE_ prefix, so they
already cover the runtime-side spelling. That is now pinned.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DKWDdUJ2XNRESVVWUvcpnh
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 52 chunks)3235.6 KB3266.6 KB
Main entry chunk (gzip)157.0 KB350 KB
Entry fileindex-DvWnMsNg.js
StatusPASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)11.89KB4.50KB
app-shell (runtime-config.js)20.61KB7.35KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)506.01KB114.64KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)173.10KB47.96KB
fields (index.js)238.89KB60.02KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)9.53KB3.38KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)4.64KB1.50KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)1.93KB0.88KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.85KB12.89KB
plugin-charts (index.js)64.66KB18.32KB
plugin-chatbot (index.js)188.60KB44.82KB
plugin-dashboard (index.js)133.48KB34.49KB
plugin-designer (index.js)212.80KB43.15KB
plugin-detail (index.js)245.29KB62.39KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)131.78KB32.19KB
plugin-gantt (index.js)165.16KB40.33KB
plugin-grid (index.js)201.66KB54.58KB
plugin-kanban (index.js)53.11KB14.62KB
plugin-list (index.js)112.86KB27.54KB
plugin-map (index.js)20.09KB6.62KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)26.72KB7.71KB
plugin-tree (index.js)9.26KB3.13KB
plugin-view (index.js)85.87KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)63.21KB21.05KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)2.44KB1.21KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)12.13KB3.65KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)7.54KB2.63KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@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 objectstack#12681; this seat shares the PR's author identity, so this is a COMMENT review — not a governed surface).

Read the full 1885-line diff. Findings, all positive:

  • The gate collapses to one input and the collapse is complete: resolveSentryGate(runtimeErrorReporting) takes no env argument any more — nothing a build was compiled with can influence whether reporting happens; reason honestly shrinks from four values to two, with runtime-denied collapsing INTO no-dsn because they are now the same state. The withheld verdict carries no PII and zero sample rates.
  • Every VITE_SENTRY_* retires except VITE_SENTRY_RELEASE, which is a label read at the call site, never a gate — with the source-maps reasoning recorded in three places a future editor would look.
  • The ratchet did not narrow: committed-telemetry-endpoint.test.ts rules are keyed on key-suffix and value, and the new counter-probes pin that a committed OS_TELEMETRY_CLIENT_ERROR_REPORTING_DSN in a .env file is caught exactly like the retired spelling — a later "tidy-up" cannot reopen the hole under the new name.
  • The mirror discipline is preserved: readClientErrorReporting is a declared line-for-line hand copy of the producer's export (this repo deliberately takes no dependency on @objectstack/cloud-connection), the secret-DSN guard is kept on this side as the last line against a third-party host, knobs are re-derived defensively (=== true for PII, finite 0..1 for rates, 0 kept as a real answer), and telemetry stays replace-not-merge so a withdrawn DSN withdraws.
  • The landing-order pin exists on this side too: a payload carrying only the replaced #10805 boolean reads as null.
  • Docs and comments (error-tracking.md, .env.production, main.tsx ordering note) describe only the final shape; the error-tracking CSP note gains the right new caveat — the DSN can now be repointed without a rebuild, so a hosting-layer CSP can silently go stale.

Cross-repo pair: objectstack#12697 (verified in its own review). Landing via the normal queue; the card closes when both halves are merged.


Generated by Claude Code

@os-zhuang
os-zhuang marked this pull request as ready for review August 27, 2026 10:37
@os-zhuang
os-zhuang enabled auto-merge August 27, 2026 10:37
@os-zhuang
os-zhuang added this pull request to the merge queueAug 27, 2026
Merged via the queue into main with commit 14ef9f5Aug 27, 2026
30 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-12681-runtime-dsn branch August 27, 2026 10:56
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-zhuang@claude