Skip to content

Commit 6cb5513

Browse files
committed
feat(browser): Add environment variable support for Spotlight configuration
- Add support for multiple framework/bundler-specific environment variables with proper precedence - SENTRY_SPOTLIGHT (highest priority - base name, supported natively by many bundlers) - PUBLIC_SENTRY_SPOTLIGHT (SvelteKit, Astro, Qwik) - NEXT_PUBLIC_SENTRY_SPOTLIGHT (Next.js) - VITE_SENTRY_SPOTLIGHT (Vite) - NUXT_PUBLIC_SENTRY_SPOTLIGHT (Nuxt.js) - REACT_APP_SENTRY_SPOTLIGHT (Create React App) - VUE_APP_SENTRY_SPOTLIGHT (Vue CLI) - GATSBY_SENTRY_SPOTLIGHT (Gatsby) - Add defensive environment variable access via process.env (transformed by all major bundlers) - Move envToBool utility from node-core to core for shared usage - Add resolveSpotlightOptions utility for consistent precedence rules - Update node-core and aws-serverless to use shared utilities - Add comprehensive tests for all new utilities and SDK integration Note: import.meta.env is intentionally not checked because bundlers only replace static references (e.g., import.meta.env.VITE_VAR) at build time, not dynamic access. All major bundlers transform process.env references, making it the universal solution.
1 parent c8ca286 commit 6cb5513

File tree

17 files changed

+681
-22
lines changed

17 files changed

+681
-22
lines changed

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,17 @@
22

33
## Unreleased
44

5+
- feat(browser): Add environment variable support for Spotlight configuration
6+
- Supports `SENTRY_SPOTLIGHT`, `PUBLIC_SENTRY_SPOTLIGHT`, `NEXT_PUBLIC_SENTRY_SPOTLIGHT`, `VITE_SENTRY_SPOTLIGHT`, `NUXT_PUBLIC_SENTRY_SPOTLIGHT`, `REACT_APP_SENTRY_SPOTLIGHT`, `VUE_APP_SENTRY_SPOTLIGHT`, and `GATSBY_SENTRY_SPOTLIGHT` with proper precedence
7+
- Adds defensive environment variable access for both `process.env` and `import.meta.env`
8+
- feat(core): Add shared utilities for Spotlight configuration
9+
- Moves `envToBool` from `node-core` to `core` for shared usage across SDKs
10+
- Adds `resolveSpotlightOptions` utility for consistent precedence rules
11+
- feat(node): Update Spotlight configuration to use shared utilities
512
- fix(node): Fix Spotlight configuration precedence to match specification (#18195)
613

14+
- "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott
15+
716
## 10.25.0
817

918
- feat(browser): Include Spotlight in development bundles ([#18078](https://github.com/getsentry/sentry-javascript/pull/18078))

packages/aws-serverless/src/init.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import type { Integration, Options } from '@sentry/core';
2-
import { applySdkMetadata, debug, getSDKSource } from '@sentry/core';
2+
import { applySdkMetadata, debug, envToBool, getSDKSource } from '@sentry/core';
33
import type { NodeClient, NodeOptions } from '@sentry/node';
44
import { getDefaultIntegrationsWithoutPerformance, initWithoutDefaultIntegrations } from '@sentry/node';
5-
import { envToBool } from '@sentry/node-core';
65
import { DEBUG_BUILD } from './debug-build';
76
import { awsIntegration } from './integration/aws';
87
import { awsLambdaIntegration } from './integration/awslambda';

packages/browser/src/client.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,9 +70,26 @@ type BrowserSpecificOptions = BrowserClientReplayOptions &
7070
*
7171
* Either set it to true, or provide a specific Spotlight Sidecar URL.
7272
*
73+
* Alternatively, you can configure Spotlight using environment variables:
74+
* - SENTRY_SPOTLIGHT (base/official name)
75+
* - PUBLIC_SENTRY_SPOTLIGHT (SvelteKit, Astro, Qwik)
76+
* - NEXT_PUBLIC_SENTRY_SPOTLIGHT (Next.js)
77+
* - VITE_SENTRY_SPOTLIGHT (Vite)
78+
* - NUXT_PUBLIC_SENTRY_SPOTLIGHT (Nuxt)
79+
* - REACT_APP_SENTRY_SPOTLIGHT (Create React App)
80+
* - VUE_APP_SENTRY_SPOTLIGHT (Vue CLI)
81+
* - GATSBY_SENTRY_SPOTLIGHT (Gatsby)
82+
*
83+
* Precedence rules:
84+
* - If this option is `false`, Spotlight is disabled (env vars ignored)
85+
* - If this option is a string URL, that URL is used (env vars ignored)
86+
* - If this option is `true` and env var is a URL, the env var URL is used
87+
* - If this option is `undefined`, the env var value is used (if set)
88+
*
7389
* More details: https://spotlightjs.com/
7490
*
7591
* IMPORTANT: Only set this option to `true` while developing, not in production!
92+
* Spotlight is automatically excluded from production bundles.
7693
*/
7794
spotlight?: boolean | string;
7895
};

packages/browser/src/sdk.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
getIntegrationsToSetup,
66
inboundFiltersIntegration,
77
initAndBind,
8+
resolveSpotlightOptions,
89
stackParserFromStackParserOptions,
910
} from '@sentry/core';
1011
import type { BrowserClientOptions, BrowserOptions } from './client';
@@ -19,6 +20,7 @@ import { spotlightBrowserIntegration } from './integrations/spotlight';
1920
import { defaultStackParser } from './stack-parsers';
2021
import { makeFetchTransport } from './transports/fetch';
2122
import { checkAndWarnIfIsEmbeddedBrowserExtension } from './utils/detectBrowserExtension';
23+
import { getSpotlightConfig } from './utils/spotlightConfig';
2224

2325
/** Get the default integrations for the browser SDK. */
2426
export function getDefaultIntegrations(_options: Options): Integration[] {
@@ -95,11 +97,15 @@ export function init(options: BrowserOptions = {}): Client | undefined {
9597
options.defaultIntegrations == null ? getDefaultIntegrations(options) : options.defaultIntegrations;
9698

9799
/* rollup-include-development-only */
98-
if (options.spotlight) {
100+
// Resolve Spotlight configuration with proper precedence
101+
const envSpotlight = getSpotlightConfig();
102+
const spotlightValue = resolveSpotlightOptions(options.spotlight, envSpotlight);
103+
104+
if (spotlightValue) {
99105
if (!defaultIntegrations) {
100106
defaultIntegrations = [];
101107
}
102-
const args = typeof options.spotlight === 'string' ? { sidecarUrl: options.spotlight } : undefined;
108+
const args = typeof spotlightValue === 'string' ? { sidecarUrl: spotlightValue } : undefined;
103109
defaultIntegrations.push(spotlightBrowserIntegration(args));
104110
}
105111
/* rollup-include-development-only-end */

packages/browser/src/utils/env.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/**
2+
* Safely gets an environment variable value with defensive guards for browser environments.
3+
* Checks process.env which is transformed by most bundlers (Webpack, Vite, Rollup, Rspack, Parcel, etc.)
4+
* at build time.
5+
*
6+
* Note: We don't check import.meta.env because:
7+
* 1. Bundlers only replace static references like `import.meta.env.VITE_VAR`, not dynamic access
8+
* 2. Dynamic access causes syntax errors in unsupported environments
9+
* 3. Most bundlers transform process.env references anyway
10+
*
11+
* @param key - The environment variable key to look up
12+
* @returns The value of the environment variable or undefined if not found
13+
*/
14+
export function getEnvValue(key: string): string | undefined {
15+
try {
16+
if (typeof process !== 'undefined' && process.env) {
17+
const value = process.env[key];
18+
if (value !== undefined) {
19+
return value;
20+
}
21+
}
22+
} catch (e) {
23+
// Silently ignore - process might not be accessible or might throw in some environments
24+
}
25+
26+
return undefined;
27+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { debug, envToBool } from '@sentry/core';
2+
import { DEBUG_BUILD } from '../debug-build';
3+
import { getEnvValue } from './env';
4+
5+
/**
6+
* Environment variable keys to check for Spotlight configuration, in priority order.
7+
* The first one found with a value will be used.
8+
*/
9+
const SPOTLIGHT_ENV_KEYS = [
10+
'SENTRY_SPOTLIGHT', // Base/official name - works in Parcel, Webpack, Rspack, Rollup, Rolldown, Node.js
11+
'PUBLIC_SENTRY_SPOTLIGHT', // SvelteKit, Astro, Qwik
12+
'NEXT_PUBLIC_SENTRY_SPOTLIGHT', // Next.js
13+
'VITE_SENTRY_SPOTLIGHT', // Vite
14+
'NUXT_PUBLIC_SENTRY_SPOTLIGHT', // Nuxt
15+
'REACT_APP_SENTRY_SPOTLIGHT', // Create React App
16+
'VUE_APP_SENTRY_SPOTLIGHT', // Vue CLI
17+
'GATSBY_SENTRY_SPOTLIGHT', // Gatsby
18+
] as const;
19+
20+
/**
21+
* Gets the Spotlight configuration from environment variables.
22+
* Checks multiple environment variable prefixes in priority order to support
23+
* different bundlers and frameworks.
24+
*
25+
* @returns The resolved Spotlight configuration (boolean | string | undefined)
26+
*/
27+
export function getSpotlightConfig(): boolean | string | undefined {
28+
for (const key of SPOTLIGHT_ENV_KEYS) {
29+
const value = getEnvValue(key);
30+
31+
if (value !== undefined) {
32+
// Try to parse as boolean first (strict mode)
33+
const boolValue = envToBool(value, { strict: true });
34+
35+
if (boolValue !== null) {
36+
// It's a valid boolean value
37+
if (DEBUG_BUILD) {
38+
debug.log(`[Spotlight] Found ${key}=${String(boolValue)} in environment variables`);
39+
}
40+
return boolValue;
41+
}
42+
43+
// Not a boolean, treat as custom URL string
44+
if (DEBUG_BUILD) {
45+
debug.log(`[Spotlight] Found ${key}=${value} (custom URL) in environment variables`);
46+
}
47+
return value;
48+
}
49+
}
50+
51+
// No Spotlight configuration found in environment
52+
return undefined;
53+
}

packages/browser/test/sdk.test.ts

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,161 @@ describe('init', () => {
234234
});
235235
});
236236

237+
describe('Spotlight environment variable support', () => {
238+
let originalProcess: typeof globalThis.process | undefined;
239+
240+
afterEach(() => {
241+
if (originalProcess !== undefined) {
242+
globalThis.process = originalProcess;
243+
} else {
244+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
245+
delete (globalThis as any).process;
246+
}
247+
});
248+
249+
it('uses environment variable when options.spotlight is undefined', () => {
250+
originalProcess = globalThis.process;
251+
globalThis.process = {
252+
env: {
253+
SENTRY_SPOTLIGHT: 'true',
254+
},
255+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
256+
} as any;
257+
258+
// @ts-expect-error this is fine for testing
259+
const initAndBindSpy = vi.spyOn(SentryCore, 'initAndBind').mockImplementationOnce(() => {});
260+
const options = getDefaultBrowserOptions({ dsn: PUBLIC_DSN, spotlight: undefined });
261+
init(options);
262+
263+
const optionsPassed = initAndBindSpy.mock.calls[0]?.[1];
264+
// Spotlight integration should be added
265+
const spotlightIntegration = optionsPassed?.integrations.find((i: Integration) => i.name === 'SpotlightBrowser');
266+
expect(spotlightIntegration).toBeDefined();
267+
});
268+
269+
it('does not add Spotlight when environment variable is false', () => {
270+
originalProcess = globalThis.process;
271+
globalThis.process = {
272+
env: {
273+
SENTRY_SPOTLIGHT: 'false',
274+
},
275+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
276+
} as any;
277+
278+
// @ts-expect-error this is fine for testing
279+
const initAndBindSpy = vi.spyOn(SentryCore, 'initAndBind').mockImplementationOnce(() => {});
280+
const options = getDefaultBrowserOptions({ dsn: PUBLIC_DSN, spotlight: undefined });
281+
init(options);
282+
283+
const optionsPassed = initAndBindSpy.mock.calls[0]?.[1];
284+
// Spotlight integration should NOT be added
285+
const spotlightIntegration = optionsPassed?.integrations.find((i: Integration) => i.name === 'SpotlightBrowser');
286+
expect(spotlightIntegration).toBeUndefined();
287+
});
288+
289+
it('options.spotlight=false takes precedence over environment variable', () => {
290+
originalProcess = globalThis.process;
291+
globalThis.process = {
292+
env: {
293+
SENTRY_SPOTLIGHT: 'true',
294+
},
295+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
296+
} as any;
297+
298+
// @ts-expect-error this is fine for testing
299+
const initAndBindSpy = vi.spyOn(SentryCore, 'initAndBind').mockImplementationOnce(() => {});
300+
const options = getDefaultBrowserOptions({ dsn: PUBLIC_DSN, spotlight: false });
301+
init(options);
302+
303+
const optionsPassed = initAndBindSpy.mock.calls[0]?.[1];
304+
// Spotlight integration should NOT be added even though env var is true
305+
const spotlightIntegration = optionsPassed?.integrations.find((i: Integration) => i.name === 'SpotlightBrowser');
306+
expect(spotlightIntegration).toBeUndefined();
307+
});
308+
309+
it('options.spotlight=url takes precedence over environment variable', () => {
310+
originalProcess = globalThis.process;
311+
const customUrl = 'http://custom:1234/stream';
312+
globalThis.process = {
313+
env: {
314+
SENTRY_SPOTLIGHT: 'http://env:5678/stream',
315+
},
316+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
317+
} as any;
318+
319+
// @ts-expect-error this is fine for testing
320+
const initAndBindSpy = vi.spyOn(SentryCore, 'initAndBind').mockImplementationOnce(() => {});
321+
const options = getDefaultBrowserOptions({ dsn: PUBLIC_DSN, spotlight: customUrl });
322+
init(options);
323+
324+
const optionsPassed = initAndBindSpy.mock.calls[0]?.[1];
325+
// Spotlight integration should be added (we can't easily check the URL here without deeper inspection)
326+
const spotlightIntegration = optionsPassed?.integrations.find((i: Integration) => i.name === 'SpotlightBrowser');
327+
expect(spotlightIntegration).toBeDefined();
328+
});
329+
330+
it('uses environment variable URL when options.spotlight=true', () => {
331+
originalProcess = globalThis.process;
332+
globalThis.process = {
333+
env: {
334+
SENTRY_SPOTLIGHT: 'http://env:5678/stream',
335+
},
336+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
337+
} as any;
338+
339+
// @ts-expect-error this is fine for testing
340+
const initAndBindSpy = vi.spyOn(SentryCore, 'initAndBind').mockImplementationOnce(() => {});
341+
const options = getDefaultBrowserOptions({ dsn: PUBLIC_DSN, spotlight: true });
342+
init(options);
343+
344+
const optionsPassed = initAndBindSpy.mock.calls[0]?.[1];
345+
// Spotlight integration should be added
346+
const spotlightIntegration = optionsPassed?.integrations.find((i: Integration) => i.name === 'SpotlightBrowser');
347+
expect(spotlightIntegration).toBeDefined();
348+
});
349+
350+
it('respects priority order: SENTRY_SPOTLIGHT over PUBLIC_SENTRY_SPOTLIGHT', () => {
351+
originalProcess = globalThis.process;
352+
globalThis.process = {
353+
env: {
354+
SENTRY_SPOTLIGHT: 'true',
355+
PUBLIC_SENTRY_SPOTLIGHT: 'false',
356+
},
357+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
358+
} as any;
359+
360+
// @ts-expect-error this is fine for testing
361+
const initAndBindSpy = vi.spyOn(SentryCore, 'initAndBind').mockImplementationOnce(() => {});
362+
const options = getDefaultBrowserOptions({ dsn: PUBLIC_DSN, spotlight: undefined });
363+
init(options);
364+
365+
const optionsPassed = initAndBindSpy.mock.calls[0]?.[1];
366+
// Spotlight integration should be added (SENTRY_SPOTLIGHT=true wins)
367+
const spotlightIntegration = optionsPassed?.integrations.find((i: Integration) => i.name === 'SpotlightBrowser');
368+
expect(spotlightIntegration).toBeDefined();
369+
});
370+
371+
it('uses framework-specific prefix when base is not set', () => {
372+
originalProcess = globalThis.process;
373+
globalThis.process = {
374+
env: {
375+
NEXT_PUBLIC_SENTRY_SPOTLIGHT: 'true',
376+
},
377+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
378+
} as any;
379+
380+
// @ts-expect-error this is fine for testing
381+
const initAndBindSpy = vi.spyOn(SentryCore, 'initAndBind').mockImplementationOnce(() => {});
382+
const options = getDefaultBrowserOptions({ dsn: PUBLIC_DSN, spotlight: undefined });
383+
init(options);
384+
385+
const optionsPassed = initAndBindSpy.mock.calls[0]?.[1];
386+
// Spotlight integration should be added
387+
const spotlightIntegration = optionsPassed?.integrations.find((i: Integration) => i.name === 'SpotlightBrowser');
388+
expect(spotlightIntegration).toBeDefined();
389+
});
390+
});
391+
237392
it('returns a client from init', () => {
238393
const client = init();
239394
expect(client).not.toBeUndefined();

0 commit comments

Comments
 (0)