From 78b542d58714f85015e7b6e3c80c786a8c104967 Mon Sep 17 00:00:00 2001 From: Eric Dahl Date: Wed, 23 Sep 2026 16:24:08 -0400 Subject: [PATCH] fix: inline the public Sentry DSN into the client bundle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #94 read the DSN through a helper that indexes process.env dynamically. Next only substitutes literal `process.env.NEXT_PUBLIC_*` expressions when building the client bundle, so the dynamic read survived into the browser as undefined and Sentry disabled itself with no error. Verified against the deployed site: the SDK shipped but the DSN string was absent, while PostHog's token was inlined because it is referenced literally. Server and edge runtimes were unaffected — they read process.env at runtime. The client now passes literal references explicitly, and a guard test asserts the literal is present and that the no-argument form is not used, since a unit test cannot observe bundler substitution. Confirmed with a real build: a probe DSN now appears in a client chunk. --- instrumentation-client.ts | 10 ++++++- .../observability/client-dsn-inlining.test.ts | 28 +++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 tests/observability/client-dsn-inlining.test.ts diff --git a/instrumentation-client.ts b/instrumentation-client.ts index 57d4d0c..539beb5 100644 --- a/instrumentation-client.ts +++ b/instrumentation-client.ts @@ -11,7 +11,15 @@ posthog.init(process.env.NEXT_PUBLIC_POSTHOG_PROJECT_TOKEN!, { debug: process.env.NODE_ENV === 'development', }); -const sentryOptions = buildSentryOptions('client'); +// These must be written as literal `process.env.X` expressions: that is the +// only form Next substitutes when building the client bundle. Reading them +// dynamically leaves `undefined` in the browser and silently disables Sentry. +const sentryOptions = buildSentryOptions('client', { + NEXT_PUBLIC_SENTRY_DSN: process.env.NEXT_PUBLIC_SENTRY_DSN, + NODE_ENV: process.env.NODE_ENV, + SENTRY_ENVIRONMENT: process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT, + SENTRY_RELEASE: process.env.NEXT_PUBLIC_SENTRY_RELEASE, +}); Sentry.init({ enabled: sentryOptions.enabled, diff --git a/tests/observability/client-dsn-inlining.test.ts b/tests/observability/client-dsn-inlining.test.ts new file mode 100644 index 0000000..81f85e8 --- /dev/null +++ b/tests/observability/client-dsn-inlining.test.ts @@ -0,0 +1,28 @@ +import { readFileSync } from 'node:fs'; +import path from 'node:path'; + +import { describe, expect, test } from 'vitest'; + +/** + * Next only substitutes *literal* `process.env.NEXT_PUBLIC_*` references when + * it builds the client bundle. Reading the same name dynamically — say + * `env['NEXT_PUBLIC_SENTRY_DSN']` inside a helper — is left untouched, so the + * value is `undefined` in the browser and Sentry disables itself without any + * error. That shipped once already; these tests keep it from shipping again. + */ +describe('instrumentation-client.ts', () => { + const source = readFileSync( + path.resolve(__dirname, '../../instrumentation-client.ts'), + 'utf8', + ); + + test('references the public DSN literally so the bundler can inline it', () => { + expect(source).toContain('process.env.NEXT_PUBLIC_SENTRY_DSN'); + }); + + test('does not hand the raw process.env object to the options builder', () => { + // buildSentryOptions('client') with no second argument falls back to + // process.env, which is exactly the non-inlinable path. + expect(source).not.toMatch(/buildSentryOptions\(\s*'client'\s*\)/); + }); +});