Skip to content

console/app-shell: stop shipping an un-disableable Sentry DSN, and make sendDefaultPii opt-in (#5522) - #5559

Merged
os-sales merged 2 commits into
mainfrom
claude/issue-5522-sentry-telemetry-reopen
Aug 21, 2026
Merged

console/app-shell: stop shipping an un-disableable Sentry DSN, and make sendDefaultPii opt-in (#5522)#5559
os-sales merged 2 commits into
mainfrom
claude/issue-5522-sentry-telemetry-reopen

Conversation

@os-sales

Copy link
Copy Markdown
Collaborator

Part of #5522notFixes. See "What this does not close" at the end.

⚠️Why this PR exists on a second branch. PR #5546 was opened for
claude/issue-5522-sentry-disableable-telemetry and reached 22/22 green gates, but it
is not viewable — it returns 404 for the maintainer and for this session, and a head-filtered
query returns nothing in any state, while GitHub still refuses to open another PR for that
branch ("a pull request already exists"). So the record exists and cannot be reached.
Rather than leave a p0 security fix parked behind an unreachable PR, the identical
commit
f751c0288 was pushed to a second branch and opened here.
Nothing was rebuilt, rebased or re-verified — this is the same object that went green.
If #5546 becomes reachable again, close whichever of the two is redundant; they are the same tree.

@object-ui/console publishes a pre-built SPA, so ONE artifact — built once from
apps/console/.env.production — is what the hosted SaaS console and the on-premises /
air-gapped EE images all embed. Vite inlines every VITE_* from that file into the bundle
as a frozen object literal, so the DSN committed there was a live third-party telemetry
endpoint compiled into artifacts that land inside customer networks.

It could not be switched off afterwards either: the VITE_SENTRY_ENABLED kill switch is
read off that same frozen literal, so on a shipped bundle it is undefined forever and
editing env vars on the deployed host does nothing. Upstream (objectstack-ai/cloud#1508,
graded p0/security) an air-gapped deployment was measured sending 14 envelopes per session
to sentry.io carrying IP + User-Agent PII, unstoppable by the customer.

What changed

  • apps/console/.env.production no longer defines VITE_SENTRY_DSN,
    VITE_SENTRY_ENVIRONMENT or VITE_SENTRY_SEND_DEFAULT_PII. A build with no DSN never
    imports @sentry/react, so the vendor-sentry chunk is not even fetched.
  • sendDefaultPii changed from opt-out (!== 'false') to opt-in (=== 'true'), so IP
    address and User-Agent are never the inherited default of a build that did not ask.
  • The gate is extracted as a pure resolveSentryGate(env) and now fails closed: absent,
    empty, whitespace-only or non-string DSN, and a null/undefined env object, all return
    enabled: falsebefore the dynamic import. Every withheld verdict also carries
    sendDefaultPii: false, so no disabled branch is one refactor away from leaking.

The fail direction is deliberately inverted from the usual: an unreported error is
recoverable, PII leaving an air-gapped deployment is not.

Why the gate became a pure function, and why that is not incidental

The first draft stubbed environment variables and silently tested nothing. This repo's
Vitest exposes only BASE_URL/DEV/MODE/PROD/SSR on import.meta.env, and
vi.stubEnv writes to process.env without reaching import.meta.env — so every case
landed on the no-DSN branch, which is exactly why the three "absence" cases appeared to
pass. Making the decision reachable is the fix; no assertion was relaxed.

Verification (all on f751c0288, the commit this branch points at)

  • Reproduce, real vite build on main: the live DSN appears 6 times across 3 chunks,
    and the compiled gate reads e.VITE_SENTRY_ENABLED from a literal ending
    …VITE_SENTRY_SEND_DEFAULT_PII:"true",VITE_SERVER_URL:"",VITE_USE_MOCK_SERVER:"false"}
    the key is provably absent from the literal it is read from.
  • Gone, same build on this tree: live DSN occurrences 0; files containing any
    sentry.io host 0.
  • Counter-probe (the reason that zero is evidence): with a DSN injected at build time on
    this same tree, the injected DSN inlines 6 times and the vendor-sentry chunk is
    emitted — identical in shape to the defect. So the zero is a gate, not a build that wired
    nothing up. The SaaS path is intact.
  • Reverse verification, direction predicted before running, both matched: re-committing
    the DSN turns the ratchet red on that file only (Tests 1 failed | 6 passed);
    reverting sendDefaultPii to the opt-out spelling turns exactly the two PII-default cases
    red (Tests 2 failed | 12 passed). Each mutation was proved on disk by grep counts before
    any output was read, under a restoring trap.
  • Near-miss spellings are pinned as not enabling PII: 'TRUE', 'True', '1', 'yes',
    'on', ''.
  • CI: 22/22 gates green on this commit, including all four test shards.

The ratchet test reads .env.production through Node's fs, not a Vite ?raw import —
Vite's own server.fs.deny covers .env* by design, and that protection was not weakened to
make a test pass. It lives in packages/app-shell because apps/console's tsconfig is
browser-only while app-shell's carries ["node", "vite/client"].

⚠️ Action required before/with merge — hosted SaaS only

The hosted SaaS/demo console must inject VITE_SENTRY_DSN from its Vercel project
environment, the same way VITE_SERVER_URL already is, or it builds with error reporting
off. Add VITE_SENTRY_SEND_DEFAULT_PII=true if IP/User-Agent on events is still wanted —
that one is a genuine behaviour change, opt-out became opt-in. It is a deploy-dashboard
action, outside this repo.

What this does not close

An opted-in build still has no post-build off switch, which is the literal wording of the
card's invariant. A runtime-configurable gate needs a new key on /api/v1/runtime/config
an objectstack contract change, filed upstream with the measurement. The existing payload was
read key by key first and carries nothing usable: reading cloudUrl === '' as "air-gapped"
would be inferring a posture from an unrelated signal, which is the anti-pattern
runtime-config.ts warns against in isMarketplaceEnabled()'s own doc.

So #5522 stays open after this merges; the shipped artifact no longer carrying an endpoint
at all is the stop-the-bleed.


Generated by Claude Code

os-salesand others added 2 commits August 21, 2026 09:46
…ed bundle
`@object-ui/console` publishes a PRE-BUILT SPA, so one artifact built from
`apps/console/.env.production` is what the hosted SaaS console and the
on-premises / air-gapped EE images all embed. Vite inlines every `VITE_*`
from that file into the bundle as a frozen object literal, which made the
committed `VITE_SENTRY_DSN` a live third-party telemetry endpoint compiled
into artifacts that land inside customer networks — and un-disableable,
because the `VITE_SENTRY_ENABLED` kill switch is read off that same frozen
literal and is `undefined` forever on a build that never defined it.
- drop the DSN, environment and `SEND_DEFAULT_PII=true` from `.env.production`;
builds that want reporting inject the DSN from their own deploy environment
- `sendDefaultPii` becomes opt-in (`=== 'true'`), so IP + User-Agent are never
the inherited default of a build that did not ask for them
- make the gate fail CLOSED and document why the direction is inverted here
- ratchet test on `.env.production`, plus gate tests with a counter-probe
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014zHsbJoTkTZeJQ5DLbRXrE
The gate's decision was unreachable from tests: this repo's Vitest exposes
only BASE_URL/DEV/MODE/PROD/SSR on `import.meta.env`, and `vi.stubEnv` writes
to `process.env` without reaching `import.meta.env` — measured, after a first
draft whose five positive cases all failed while its three absence cases
"passed". An untestable gate is how the previous one stayed broken.
Also moves the `.env` ratchet next to `sentry.ts`: Vite's `server.fs.deny`
blocks `.env*` from `?raw` imports, and the console's tsconfig is browser-only
so it cannot use `node:fs` either — app-shell's carries `types: [node, ...]`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014zHsbJoTkTZeJQ5DLbRXrE
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 52 chunks)3784.4 KB3867.2 KB
Main entry chunk (gzip)151.2 KB350 KB
Entry fileindex-DoMxXYl_.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 (index.js)10.04KB3.72KB
app-shell (runtime-config.js)8.91KB2.99KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)1.17KB0.53KB
auth (AuthProvider.js)29.34KB7.05KB
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)6.35KB2.43KB
auth (index.js)2.77KB1.22KB
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.02KB0.89KB
auth (useIsWorkspaceAdmin.js)3.04KB1.45KB
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.94KB113.63KB
core (index.js)4.51KB1.80KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)159.80KB44.33KB
fields (index.js)237.52KB59.62KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.22KB3.08KB
i18n (provider.js)23.13KB7.63KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)30.51KB7.57KB
i18n (useSafeTranslation.js)7.77KB3.13KB
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.35KB3.31KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)4.42KB1.42KB
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.81KB0.83KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.62KB12.83KB
plugin-charts (index.js)64.72KB18.35KB
plugin-chatbot (index.js)181.21KB43.14KB
plugin-dashboard (index.js)128.51KB32.94KB
plugin-designer (index.js)212.39KB42.83KB
plugin-detail (index.js)242.15KB60.89KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)124.40KB30.26KB
plugin-gantt (index.js)164.10KB39.87KB
plugin-grid (index.js)200.79KB54.26KB
plugin-kanban (index.js)52.93KB14.60KB
plugin-list (index.js)111.70KB27.17KB
plugin-map (index.js)20.06KB6.62KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.49KB11.93KB
plugin-timeline (index.js)26.68KB7.66KB
plugin-tree (index.js)8.50KB2.88KB
plugin-view (index.js)84.52KB20.67KB
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)3.77KB1.33KB
react (SchemaRenderer.js)43.66KB14.77KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.33KB0.69KB
react (schema-input.js)1.45KB0.83KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (index.js)4.77KB2.16KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)10.76KB3.17KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.29KB0.24KB
sdui-parser (validate.js)6.92KB2.40KB
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)0.20KB0.18KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)0.20KB0.18KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.87KB0.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-retry.js)4.32KB2.02KB
types (index.js)3.08KB1.53KB
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 (system-fields.js)3.33KB1.54KB
types (theme.js)0.20KB0.18KB
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

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.

1 participant

@os-sales