Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/browser/src/sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ export function init(options: BrowserOptions = {}): Client | undefined {
/* rollup-include-development-only */
// Resolve Spotlight configuration with proper precedence
const envSpotlight = getSpotlightConfig();
// resolveSpotlightOptions is the single source of truth that ensures empty strings are never used
const spotlightValue = resolveSpotlightOptions(options.spotlight, envSpotlight);

if (spotlightValue) {
Expand Down
4 changes: 3 additions & 1 deletion packages/browser/src/utils/spotlightConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,9 @@ export function getSpotlightConfig(): boolean | string | undefined {
}

// Not a boolean, treat as custom URL string
// Note: empty/whitespace strings are filtered by resolveSpotlightOptions
// Note: Empty/whitespace strings are intentionally returned as-is. The resolveSpotlightOptions
// function is the single source of truth for filtering these and ensuring empty strings are
// NEVER used (returns undefined instead).
if (DEBUG_BUILD) {
debug.log(`[Spotlight] Found ${key}=${value} (custom URL) in environment variables`);
}
Expand Down
8 changes: 4 additions & 4 deletions packages/browser/test/utils/spotlightConfig.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,24 +59,24 @@ describe('getSpotlightConfig', () => {
expect(getSpotlightConfig()).toBe(customUrl);
});

it('returns undefined when SENTRY_SPOTLIGHT is an empty string', () => {
it('returns empty string when SENTRY_SPOTLIGHT is an empty string (filtered by resolveSpotlightOptions)', () => {
globalThis.process = {
env: {
SENTRY_SPOTLIGHT: '',
} as Record<string, string>,
} as NodeJS.Process;

expect(getSpotlightConfig()).toBeUndefined();
expect(getSpotlightConfig()).toBe('');
});

it('returns undefined when SENTRY_SPOTLIGHT is whitespace only', () => {
it('returns whitespace string when SENTRY_SPOTLIGHT is whitespace only (filtered by resolveSpotlightOptions)', () => {
globalThis.process = {
env: {
SENTRY_SPOTLIGHT: ' ',
} as Record<string, string>,
} as NodeJS.Process;

expect(getSpotlightConfig()).toBeUndefined();
expect(getSpotlightConfig()).toBe(' ');
});

it('parses various truthy values correctly', () => {
Expand Down
6 changes: 5 additions & 1 deletion packages/core/src/utils/resolveSpotlightOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@
* Resolves the final spotlight configuration based on options and environment variables.
* Implements the precedence rules from the Spotlight spec.
*
* This is the single source of truth for filtering empty/whitespace strings - it ensures that
* empty strings are NEVER returned (returns undefined instead). All callers can rely on this
* guarantee when handling spotlight configuration.
*
* @param optionsSpotlight - The spotlight option from user config (false | true | string | undefined)
* @param envSpotlight - The spotlight value from environment variables (false | true | string | undefined)
* @returns The resolved spotlight configuration
* @returns The resolved spotlight configuration (false | true | string | undefined) - NEVER an empty string
*/
export function resolveSpotlightOptions(
optionsSpotlight: boolean | string | undefined,
Expand Down
44 changes: 37 additions & 7 deletions packages/core/test/utils/resolveSpotlightOptions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,36 +55,66 @@ describe('resolveSpotlightOptions', () => {
expect(resolveSpotlightOptions(undefined, envUrl)).toBe(envUrl);
});

describe('empty string handling', () => {
it('returns undefined when options.spotlight is an empty string', () => {
describe('empty string handling - NEVER returns empty strings', () => {
it('returns undefined (never empty string) when options.spotlight is an empty string', () => {
expect(resolveSpotlightOptions('', undefined)).toBeUndefined();
expect(resolveSpotlightOptions('', true)).toBeUndefined();
expect(resolveSpotlightOptions('', 'http://env:8969')).toBeUndefined();
});

it('returns undefined when options.spotlight is whitespace only', () => {
it('returns undefined (never empty string) when options.spotlight is whitespace only', () => {
expect(resolveSpotlightOptions(' ', undefined)).toBeUndefined();
expect(resolveSpotlightOptions('\t\n', true)).toBeUndefined();
});

it('returns undefined when env is an empty string and options.spotlight is undefined', () => {
it('returns undefined (never empty string) when env is an empty string and options.spotlight is undefined', () => {
expect(resolveSpotlightOptions(undefined, '')).toBeUndefined();
});

it('returns undefined when env is whitespace only and options.spotlight is undefined', () => {
it('returns undefined (never empty string) when env is whitespace only and options.spotlight is undefined', () => {
expect(resolveSpotlightOptions(undefined, ' ')).toBeUndefined();
expect(resolveSpotlightOptions(undefined, '\t\n')).toBeUndefined();
});

it('returns true when options.spotlight is true and env is empty string', () => {
it('returns true when options.spotlight is true and env is empty string (filters out empty env)', () => {
expect(resolveSpotlightOptions(true, '')).toBe(true);
expect(resolveSpotlightOptions(true, ' ')).toBe(true);
});

it('returns valid URL when options.spotlight is valid URL even if env is empty', () => {
it('returns valid URL when options.spotlight is valid URL even if env is empty (filters out empty env)', () => {
const validUrl = 'http://localhost:8969/stream';
expect(resolveSpotlightOptions(validUrl, '')).toBe(validUrl);
expect(resolveSpotlightOptions(validUrl, ' ')).toBe(validUrl);
});

it('NEVER returns empty string - comprehensive check of all combinations', () => {
// Test all possible combinations to ensure empty strings are never returned
const emptyValues = ['', ' ', '\t\n', ' \t \n '];
const nonEmptyValues = [false, true, undefined, 'http://localhost:8969'];

// Empty options.spotlight with any env
for (const emptyOption of emptyValues) {
for (const env of [...emptyValues, ...nonEmptyValues]) {
const result = resolveSpotlightOptions(emptyOption, env);
expect(result).not.toBe('');
// Only test regex on strings
if (typeof result === 'string') {
expect(result).not.toMatch(/^\s+$/);
}
}
}

// Any options.spotlight with empty env
for (const option of [...emptyValues, ...nonEmptyValues]) {
for (const emptyEnv of emptyValues) {
const result = resolveSpotlightOptions(option, emptyEnv);
expect(result).not.toBe('');
// Only test regex on strings
if (typeof result === 'string') {
expect(result).not.toMatch(/^\s+$/);
}
}
}
});
});
});
3 changes: 2 additions & 1 deletion packages/node-core/src/sdk/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,8 @@ function getClientOptions(
// Parse spotlight configuration with proper precedence per spec
const envBool = envToBool(process.env.SENTRY_SPOTLIGHT, { strict: true });
const envSpotlight = envBool !== null ? envBool : process.env.SENTRY_SPOTLIGHT;
// Note: resolveSpotlightOptions handles empty/whitespace string filtering
// Note: resolveSpotlightOptions is the single source of truth for filtering empty/whitespace strings
// and ensures that empty strings are NEVER returned (returns undefined instead)
const spotlight = resolveSpotlightOptions(options.spotlight, envSpotlight);

const tracesSampleRate = getTracesSampleRate(options.tracesSampleRate);
Expand Down
Binary file added packages/replay-canvas/core
Binary file not shown.
Loading