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
2 changes: 1 addition & 1 deletion packages/cloudflare/src/handler.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -38,7 +38,7 @@ export function withSentry<
QueueHandlerMessage,
CfHostMetadata
>,
>(optionsCallback: (env: Env) => CloudflareOptions, handler: T): T {
>(optionsCallback: (env: Env) => CloudflareOptions | undefined, handler: T): T {

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note: I made this return undefined as it was also working before theoretically. I adapted the types to also allow undefined just to have this case also handled

setAsyncLocalStorageAsyncContextStrategy();

try {
Expand Down
22 changes: 20 additions & 2 deletions packages/cloudflare/src/options.ts
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
import { envToBool } from '@sentry/core';
import type { CloudflareOptions } from './client';

/**
Expand All@@ -17,6 +18,12 @@ function isVersionMetadata(value: unknown): value is CfVersionMetadata {
return typeof value === 'object' && value !== null && 'id' in value && typeof value.id === 'string';
}

function getEnvVar<T extends Record<string, unknown>>(env: unknown, varName: keyof T): string | undefined {
return typeof env === 'object' && env !== null && varName in env && typeof (env as T)[varName] === 'string'
? ((env as T)[varName] as string)
: undefined;
}

/**
* Merges the options passed in from the user with the options we read from
* the Cloudflare `env` environment variable object.
Expand All@@ -31,7 +38,7 @@ function isVersionMetadata(value: unknown): value is CfVersionMetadata {
*
* @returns The final options.
*/
export function getFinalOptions(userOptions: CloudflareOptions, env: unknown): CloudflareOptions {
export function getFinalOptions(userOptions: CloudflareOptions = {}, env: unknown): CloudflareOptions {
if (typeof env !== 'object' || env === null) {
return userOptions;
}
Expand All@@ -44,5 +51,16 @@ export function getFinalOptions(userOptions: CloudflareOptions, env: unknown): C
? env.CF_VERSION_METADATA.id
: undefined;

return { release, ...userOptions };
const tracesSampleRate =
userOptions.tracesSampleRate ?? parseFloat(getEnvVar(env, 'SENTRY_TRACES_SAMPLE_RATE') ?? '');

return {
release,
...userOptions,
dsn: userOptions.dsn ?? getEnvVar(env, 'SENTRY_DSN'),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

l: can we also add SENTRY_DEBUG? You could use envToBool for that from node-core, maybe lift that up into @sentry/core?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great idea. Added.

I also added tunnel to it

environment: userOptions.environment ?? getEnvVar(env, 'SENTRY_ENVIRONMENT'),
tracesSampleRate: isFinite(tracesSampleRate) ? tracesSampleRate : undefined,
debug: userOptions.debug ?? envToBool(getEnvVar(env, 'SENTRY_DEBUG')),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

envToBool(undefined) forces debug: false when unset

Medium Severity

When SENTRY_DEBUG is absent from env, getEnvVar returns undefined, and envToBool(undefined) returns false (since Boolean(undefined) is false in loose mode). This means debug is always explicitly set to false in the returned options, even when neither the user nor the env specified it. This breaks multiple existing toEqual assertions (e.g., lines 29, 120, 132) that don't expect a debug property.

Fix in CursorFix in Web

tunnel: userOptions.tunnel ?? getEnvVar(env, 'SENTRY_TUNNEL'),
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spread operator overrides env-derived fallback values

Medium Severity

The ...userOptions spread at the end of the return object overrides the env-derived fallback values for dsn, environment, and tracesSampleRate whenever those keys exist in userOptions — even if their values are undefined. This means if a user passes { dsn: undefined }, the SENTRY_DSN env fallback is silently ignored. The ?? fallback logic and the isFinite guard are both bypassed by the spread.

Fix in CursorFix in Web

}
25 changes: 15 additions & 10 deletions packages/cloudflare/test/handler.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -31,6 +31,11 @@ const MOCK_ENV = {
SENTRY_RELEASE: '1.1.1',
};

// Mock env without DSN for tests that should not initialize the SDK
const MOCK_ENV_WITHOUT_DSN = {

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note: It seems that the SDK was not initialized for the tests where this is used. By now also allowing SENTRY_DSN from the variables these tests fail. I'll create a follow up PR and remove this, so the SDK is actually initialized (I don't want to blow up this PR with other code changes)

SENTRY_RELEASE: '1.1.1',
};

function addDelayedWaitUntil(context: ExecutionContext) {
context.waitUntil(new Promise<void>(resolve => setTimeout(() => resolve())));
}
Expand DownExpand Up@@ -149,12 +154,12 @@ describe('withSentry', () => {
addDelayedWaitUntil(_context);
return new Response('test');
},
} satisfies ExportedHandler<typeof MOCK_ENV>;
} satisfies ExportedHandler<typeof MOCK_ENV_WITHOUT_DSN>;

const wrappedHandler = withSentry(vi.fn(), handler);
const waits: Promise<unknown>[] = [];
const waitUntil = vi.fn(promise => waits.push(promise));
await wrappedHandler.fetch?.(new Request('https://example.com'), MOCK_ENV, {
await wrappedHandler.fetch?.(new Request('https://example.com'), MOCK_ENV_WITHOUT_DSN, {
waitUntil,
} as unknown as ExecutionContext);
expect(flush).not.toBeCalled();
Expand DownExpand Up@@ -389,12 +394,12 @@ describe('withSentry', () => {
addDelayedWaitUntil(_context);
return;
},
} satisfies ExportedHandler<typeof MOCK_ENV>;
} satisfies ExportedHandler<typeof MOCK_ENV_WITHOUT_DSN>;

const wrappedHandler = withSentry(vi.fn(), handler);
const waits: Promise<unknown>[] = [];
const waitUntil = vi.fn(promise => waits.push(promise));
await wrappedHandler.scheduled?.(createMockScheduledController(), MOCK_ENV, {
await wrappedHandler.scheduled?.(createMockScheduledController(), MOCK_ENV_WITHOUT_DSN, {
waitUntil,
} as unknown as ExecutionContext);
expect(flush).not.toBeCalled();
Expand DownExpand Up@@ -628,12 +633,12 @@ describe('withSentry', () => {
addDelayedWaitUntil(_context);
return;
},
} satisfies ExportedHandler<typeof MOCK_ENV>;
} satisfies ExportedHandler<typeof MOCK_ENV_WITHOUT_DSN>;

const wrappedHandler = withSentry(vi.fn(), handler);
const waits: Promise<unknown>[] = [];
const waitUntil = vi.fn(promise => waits.push(promise));
await wrappedHandler.email?.(createMockEmailMessage(), MOCK_ENV, {
await wrappedHandler.email?.(createMockEmailMessage(), MOCK_ENV_WITHOUT_DSN, {
waitUntil,
} as unknown as ExecutionContext);
expect(flush).not.toBeCalled();
Expand DownExpand Up@@ -871,12 +876,12 @@ describe('withSentry', () => {
addDelayedWaitUntil(_context);
return;
},
} satisfies ExportedHandler<typeof MOCK_ENV>;
} satisfies ExportedHandler<typeof MOCK_ENV_WITHOUT_DSN>;

const wrappedHandler = withSentry(vi.fn(), handler);
const waits: Promise<unknown>[] = [];
const waitUntil = vi.fn(promise => waits.push(promise));
await wrappedHandler.queue?.(createMockQueueBatch(), MOCK_ENV, {
await wrappedHandler.queue?.(createMockQueueBatch(), MOCK_ENV_WITHOUT_DSN, {
waitUntil,
} as unknown as ExecutionContext);
expect(flush).not.toBeCalled();
Expand DownExpand Up@@ -1069,12 +1074,12 @@ describe('withSentry', () => {
addDelayedWaitUntil(_context);
return;
},
} satisfies ExportedHandler<typeof MOCK_ENV>;
} satisfies ExportedHandler<typeof MOCK_ENV_WITHOUT_DSN>;

const wrappedHandler = withSentry(vi.fn(), handler);
const waits: Promise<unknown>[] = [];
const waitUntil = vi.fn(promise => waits.push(promise));
await wrappedHandler.tail?.(createMockTailEvent(), MOCK_ENV, {
await wrappedHandler.tail?.(createMockTailEvent(), MOCK_ENV_WITHOUT_DSN, {
waitUntil,
} as unknown as ExecutionContext);
expect(flush).not.toBeCalled();
Expand Down
87 changes: 75 additions & 12 deletions packages/cloudflare/test/options.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -17,7 +17,7 @@ describe('getFinalOptions', () => {

const result = getFinalOptions(userOptions, env);

expect(result).toEqual(userOptions);
expect(result).toEqual(expect.objectContaining(userOptions));
});

it('merges options from env with user options', () => {
Expand All@@ -26,7 +26,7 @@ describe('getFinalOptions', () => {

const result = getFinalOptions(userOptions, env);

expect(result).toEqual({ dsn: 'test-dsn', release: 'user-release' });
expect(result).toEqual(expect.objectContaining({ dsn: 'test-dsn', release: 'user-release' }));
});

it('uses user options when SENTRY_RELEASE exists but is not a string', () => {
Expand All@@ -35,7 +35,7 @@ describe('getFinalOptions', () => {

const result = getFinalOptions(userOptions, env);

expect(result).toEqual(userOptions);
expect(result).toEqual(expect.objectContaining(userOptions));
});

it('uses user options when SENTRY_RELEASE does not exist', () => {
Expand All@@ -44,7 +44,7 @@ describe('getFinalOptions', () => {

const result = getFinalOptions(userOptions, env);

expect(result).toEqual(userOptions);
expect(result).toEqual(expect.objectContaining(userOptions));
});

it('takes user options over env options', () => {
Expand All@@ -53,7 +53,70 @@ describe('getFinalOptions', () => {

const result = getFinalOptions(userOptions, env);

expect(result).toEqual(userOptions);
expect(result).toEqual(expect.objectContaining(userOptions));
});

it('adds debug from env when user debug is undefined', () => {
const userOptions = { dsn: 'test-dsn' };
const env = { SENTRY_DEBUG: '1' };

const result = getFinalOptions(userOptions, env);

expect(result.debug).toBe(true);
});

it('uses SENTRY_DSN from env when user dsn is undefined', () => {
const userOptions = { release: 'user-release' };
const env = { SENTRY_DSN: 'https://key@ingest.sentry.io/1' };

const result = getFinalOptions(userOptions, env);

expect(result.dsn).toBe('https://key@ingest.sentry.io/1');
expect(result.release).toBe('user-release');
});

it('uses SENTRY_ENVIRONMENT from env when user environment is undefined', () => {
const userOptions = { dsn: 'test-dsn' };
const env = { SENTRY_ENVIRONMENT: 'staging' };

const result = getFinalOptions(userOptions, env);

expect(result.environment).toBe('staging');
});

it('uses SENTRY_TRACES_SAMPLE_RATE from env when user tracesSampleRate is undefined', () => {
const userOptions = { dsn: 'test-dsn' };
const env = { SENTRY_TRACES_SAMPLE_RATE: '0.5' };

const result = getFinalOptions(userOptions, env);

expect(result.tracesSampleRate).toBe(0.5);
});

it('does not use SENTRY_TRACES_SAMPLE_RATE from env when it is gibberish', () => {
const env = { SENTRY_TRACES_SAMPLE_RATE: 'ʕっ•ᴥ•ʔっ' };

const result = getFinalOptions(undefined, env);

expect(result.tracesSampleRate).toBeUndefined();
});

it('prefers user dsn over env SENTRY_DSN', () => {
const userOptions = { dsn: 'user-dsn', release: 'user-release' };
const env = { SENTRY_DSN: 'https://env@ingest.sentry.io/1' };

const result = getFinalOptions(userOptions, env);

expect(result.dsn).toBe('user-dsn');
});

it('ignores SENTRY_DSN when not a string', () => {
const userOptions = { dsn: undefined };
const env = { SENTRY_DSN: 123 };

const result = getFinalOptions(userOptions, env);

expect(result.dsn).toBeUndefined();
});

describe('CF_VERSION_METADATA', () => {
Expand All@@ -63,7 +126,7 @@ describe('getFinalOptions', () => {

const result = getFinalOptions(userOptions, env);

expect(result).toEqual({ dsn: 'test-dsn', release: 'version-123' });
expect(result).toEqual(expect.objectContaining({ dsn: 'test-dsn', release: 'version-123' }));
});

it('prefers SENTRY_RELEASE over CF_VERSION_METADATA.id', () => {
Expand All@@ -75,7 +138,7 @@ describe('getFinalOptions', () => {

const result = getFinalOptions(userOptions, env);

expect(result).toEqual({ dsn: 'test-dsn', release: 'env-release' });
expect(result).toEqual(expect.objectContaining({ dsn: 'test-dsn', release: 'env-release' }));
});

it('prefers user release over CF_VERSION_METADATA.id', () => {
Expand All@@ -84,7 +147,7 @@ describe('getFinalOptions', () => {

const result = getFinalOptions(userOptions, env);

expect(result).toEqual({ dsn: 'test-dsn', release: 'user-release' });
expect(result).toEqual(expect.objectContaining({ dsn: 'test-dsn', release: 'user-release' }));
});

it('prefers user release over both SENTRY_RELEASE and CF_VERSION_METADATA.id', () => {
Expand All@@ -96,7 +159,7 @@ describe('getFinalOptions', () => {

const result = getFinalOptions(userOptions, env);

expect(result).toEqual({ dsn: 'test-dsn', release: 'user-release' });
expect(result).toEqual(expect.objectContaining({ dsn: 'test-dsn', release: 'user-release' }));
});

it('ignores CF_VERSION_METADATA when it is not an object', () => {
Expand All@@ -105,7 +168,7 @@ describe('getFinalOptions', () => {

const result = getFinalOptions(userOptions, env);

expect(result).toEqual({ dsn: 'test-dsn', release: undefined });
expect(result).toEqual(expect.objectContaining({ dsn: 'test-dsn', release: undefined }));
});

it('ignores CF_VERSION_METADATA when id is not a string', () => {
Expand All@@ -114,7 +177,7 @@ describe('getFinalOptions', () => {

const result = getFinalOptions(userOptions, env);

expect(result).toEqual({ dsn: 'test-dsn', release: undefined });
expect(result).toEqual(expect.objectContaining({ dsn: 'test-dsn', release: undefined }));
});

it('ignores CF_VERSION_METADATA when id is missing', () => {
Expand All@@ -123,7 +186,7 @@ describe('getFinalOptions', () => {

const result = getFinalOptions(userOptions, env);

expect(result).toEqual({ dsn: 'test-dsn', release: undefined });
expect(result).toEqual(expect.objectContaining({ dsn: 'test-dsn', release: undefined }));
});
});
});
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -62,6 +62,7 @@ export {
_INTERNAL_shouldSkipAiProviderWrapping,
_INTERNAL_clearAiProviderSkips,
} from './utils/ai/providerSkip';
export { envToBool } from './utils/envToBool';
export { applyScopeDataToEvent, mergeScopeData, getCombinedScopeData } from './utils/scopeData';
export { prepareEvent } from './utils/prepareEvent';
export type { ExclusiveEventHintOrCaptureContext } from './utils/prepareEvent';
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest';
import { envToBool } from '../../src/utils/envToBool';
import { envToBool } from '../../../src/utils/envToBool';

describe('envToBool', () => {
it.each([
Expand Down
2 changes: 1 addition & 1 deletion packages/node-core/src/common-exports.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -31,7 +31,6 @@ export { getRequestUrl } from './utils/getRequestUrl';
export { initializeEsmLoader } from './sdk/esmLoader';
export { isCjs } from './utils/detection';
export { createMissingInstrumentationContext } from './utils/createMissingInstrumentationContext';
export { envToBool } from './utils/envToBool';
export { makeNodeTransport, type NodeTransportOptions } from './transports';
export type { HTTPModuleRequestIncomingMessage } from './transports/http-module';
export { cron } from './cron';
Expand DownExpand Up@@ -117,6 +116,7 @@ export {
wrapMcpServerWithSentry,
featureFlagsIntegration,
metrics,
envToBool,
} from '@sentry/core';

export type {
Expand Down
2 changes: 1 addition & 1 deletion packages/node-core/src/light/sdk.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,6 +4,7 @@ import {
consoleIntegration,
consoleSandbox,
debug,
envToBool,
eventFiltersIntegration,
functionToStringIntegration,
getCurrentScope,
Expand All@@ -29,7 +30,6 @@ import { defaultStackParser, getSentryRelease } from '../sdk/api';
import { makeNodeTransport } from '../transports';
import type { NodeClientOptions, NodeOptions } from '../types';
import { isCjs } from '../utils/detection';
import { envToBool } from '../utils/envToBool';
import { getSpotlightConfig } from '../utils/spotlight';
import { setAsyncLocalStorageAsyncContextStrategy } from './asyncLocalStorageStrategy';
import { LightNodeClient } from './client';
Expand Down
2 changes: 1 addition & 1 deletion packages/node-core/src/sdk/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,7 @@ import {
consoleSandbox,
conversationIdIntegration,
debug,
envToBool,
functionToStringIntegration,
getCurrentScope,
getIntegrationsToSetup,
Expand DownExpand Up@@ -38,7 +39,6 @@ import { systemErrorIntegration } from '../integrations/systemError';
import { makeNodeTransport } from '../transports';
import type { NodeClientOptions, NodeOptions } from '../types';
import { isCjs } from '../utils/detection';
import { envToBool } from '../utils/envToBool';
import { getSpotlightConfig } from '../utils/spotlight';
import { defaultStackParser, getSentryRelease } from './api';
import { NodeClient } from './client';
Expand Down
2 changes: 1 addition & 1 deletion packages/node-core/src/utils/spotlight.ts
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
import { envToBool } from './envToBool';
import { envToBool } from '@sentry/core';

/**
* Parse the spotlight option with proper precedence:
Expand Down
Loading
, '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
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
2 changes: 1 addition & 1 deletion packages/cloudflare/src/handler.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -38,7 +38,7 @@ export function withSentry<
QueueHandlerMessage,
CfHostMetadata
>,
>(optionsCallback: (env: Env) => CloudflareOptions, handler: T): T {
>(optionsCallback: (env: Env) => CloudflareOptions | undefined, handler: T): T {

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note: I made this return undefined as it was also working before theoretically. I adapted the types to also allow undefined just to have this case also handled

setAsyncLocalStorageAsyncContextStrategy();

try {
Expand Down
22 changes: 20 additions & 2 deletions packages/cloudflare/src/options.ts
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
import { envToBool } from '@sentry/core';
import type { CloudflareOptions } from './client';

/**
Expand All@@ -17,6 +18,12 @@ function isVersionMetadata(value: unknown): value is CfVersionMetadata {
return typeof value === 'object' && value !== null && 'id' in value && typeof value.id === 'string';
}

function getEnvVar<T extends Record<string, unknown>>(env: unknown, varName: keyof T): string | undefined {
return typeof env === 'object' && env !== null && varName in env && typeof (env as T)[varName] === 'string'
? ((env as T)[varName] as string)
: undefined;
}

/**
* Merges the options passed in from the user with the options we read from
* the Cloudflare `env` environment variable object.
Expand All@@ -31,7 +38,7 @@ function isVersionMetadata(value: unknown): value is CfVersionMetadata {
*
* @returns The final options.
*/
export function getFinalOptions(userOptions: CloudflareOptions, env: unknown): CloudflareOptions {
export function getFinalOptions(userOptions: CloudflareOptions = {}, env: unknown): CloudflareOptions {
if (typeof env !== 'object' || env === null) {
return userOptions;
}
Expand All@@ -44,5 +51,16 @@ export function getFinalOptions(userOptions: CloudflareOptions, env: unknown): C
? env.CF_VERSION_METADATA.id
: undefined;

return { release, ...userOptions };
const tracesSampleRate =
userOptions.tracesSampleRate ?? parseFloat(getEnvVar(env, 'SENTRY_TRACES_SAMPLE_RATE') ?? '');

return {
release,
...userOptions,
dsn: userOptions.dsn ?? getEnvVar(env, 'SENTRY_DSN'),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

l: can we also add SENTRY_DEBUG? You could use envToBool for that from node-core, maybe lift that up into @sentry/core?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great idea. Added.

I also added tunnel to it

environment: userOptions.environment ?? getEnvVar(env, 'SENTRY_ENVIRONMENT'),
tracesSampleRate: isFinite(tracesSampleRate) ? tracesSampleRate : undefined,
debug: userOptions.debug ?? envToBool(getEnvVar(env, 'SENTRY_DEBUG')),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

envToBool(undefined) forces debug: false when unset

Medium Severity

When SENTRY_DEBUG is absent from env, getEnvVar returns undefined, and envToBool(undefined) returns false (since Boolean(undefined) is false in loose mode). This means debug is always explicitly set to false in the returned options, even when neither the user nor the env specified it. This breaks multiple existing toEqual assertions (e.g., lines 29, 120, 132) that don't expect a debug property.

Fix in CursorFix in Web

tunnel: userOptions.tunnel ?? getEnvVar(env, 'SENTRY_TUNNEL'),
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spread operator overrides env-derived fallback values

Medium Severity

The ...userOptions spread at the end of the return object overrides the env-derived fallback values for dsn, environment, and tracesSampleRate whenever those keys exist in userOptions — even if their values are undefined. This means if a user passes { dsn: undefined }, the SENTRY_DSN env fallback is silently ignored. The ?? fallback logic and the isFinite guard are both bypassed by the spread.

Fix in CursorFix in Web

}
25 changes: 15 additions & 10 deletions packages/cloudflare/test/handler.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -31,6 +31,11 @@ const MOCK_ENV = {
SENTRY_RELEASE: '1.1.1',
};

// Mock env without DSN for tests that should not initialize the SDK
const MOCK_ENV_WITHOUT_DSN = {

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note: It seems that the SDK was not initialized for the tests where this is used. By now also allowing SENTRY_DSN from the variables these tests fail. I'll create a follow up PR and remove this, so the SDK is actually initialized (I don't want to blow up this PR with other code changes)

SENTRY_RELEASE: '1.1.1',
};

function addDelayedWaitUntil(context: ExecutionContext) {
context.waitUntil(new Promise<void>(resolve => setTimeout(() => resolve())));
}
Expand DownExpand Up@@ -149,12 +154,12 @@ describe('withSentry', () => {
addDelayedWaitUntil(_context);
return new Response('test');
},
} satisfies ExportedHandler<typeof MOCK_ENV>;
} satisfies ExportedHandler<typeof MOCK_ENV_WITHOUT_DSN>;

const wrappedHandler = withSentry(vi.fn(), handler);
const waits: Promise<unknown>[] = [];
const waitUntil = vi.fn(promise => waits.push(promise));
await wrappedHandler.fetch?.(new Request('https://example.com'), MOCK_ENV, {
await wrappedHandler.fetch?.(new Request('https://example.com'), MOCK_ENV_WITHOUT_DSN, {
waitUntil,
} as unknown as ExecutionContext);
expect(flush).not.toBeCalled();
Expand DownExpand Up@@ -389,12 +394,12 @@ describe('withSentry', () => {
addDelayedWaitUntil(_context);
return;
},
} satisfies ExportedHandler<typeof MOCK_ENV>;
} satisfies ExportedHandler<typeof MOCK_ENV_WITHOUT_DSN>;

const wrappedHandler = withSentry(vi.fn(), handler);
const waits: Promise<unknown>[] = [];
const waitUntil = vi.fn(promise => waits.push(promise));
await wrappedHandler.scheduled?.(createMockScheduledController(), MOCK_ENV, {
await wrappedHandler.scheduled?.(createMockScheduledController(), MOCK_ENV_WITHOUT_DSN, {
waitUntil,
} as unknown as ExecutionContext);
expect(flush).not.toBeCalled();
Expand DownExpand Up@@ -628,12 +633,12 @@ describe('withSentry', () => {
addDelayedWaitUntil(_context);
return;
},
} satisfies ExportedHandler<typeof MOCK_ENV>;
} satisfies ExportedHandler<typeof MOCK_ENV_WITHOUT_DSN>;

const wrappedHandler = withSentry(vi.fn(), handler);
const waits: Promise<unknown>[] = [];
const waitUntil = vi.fn(promise => waits.push(promise));
await wrappedHandler.email?.(createMockEmailMessage(), MOCK_ENV, {
await wrappedHandler.email?.(createMockEmailMessage(), MOCK_ENV_WITHOUT_DSN, {
waitUntil,
} as unknown as ExecutionContext);
expect(flush).not.toBeCalled();
Expand DownExpand Up@@ -871,12 +876,12 @@ describe('withSentry', () => {
addDelayedWaitUntil(_context);
return;
},
} satisfies ExportedHandler<typeof MOCK_ENV>;
} satisfies ExportedHandler<typeof MOCK_ENV_WITHOUT_DSN>;

const wrappedHandler = withSentry(vi.fn(), handler);
const waits: Promise<unknown>[] = [];
const waitUntil = vi.fn(promise => waits.push(promise));
await wrappedHandler.queue?.(createMockQueueBatch(), MOCK_ENV, {
await wrappedHandler.queue?.(createMockQueueBatch(), MOCK_ENV_WITHOUT_DSN, {
waitUntil,
} as unknown as ExecutionContext);
expect(flush).not.toBeCalled();
Expand DownExpand Up@@ -1069,12 +1074,12 @@ describe('withSentry', () => {
addDelayedWaitUntil(_context);
return;
},
} satisfies ExportedHandler<typeof MOCK_ENV>;
} satisfies ExportedHandler<typeof MOCK_ENV_WITHOUT_DSN>;

const wrappedHandler = withSentry(vi.fn(), handler);
const waits: Promise<unknown>[] = [];
const waitUntil = vi.fn(promise => waits.push(promise));
await wrappedHandler.tail?.(createMockTailEvent(), MOCK_ENV, {
await wrappedHandler.tail?.(createMockTailEvent(), MOCK_ENV_WITHOUT_DSN, {
waitUntil,
} as unknown as ExecutionContext);
expect(flush).not.toBeCalled();
Expand Down
87 changes: 75 additions & 12 deletions packages/cloudflare/test/options.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -17,7 +17,7 @@ describe('getFinalOptions', () => {

const result = getFinalOptions(userOptions, env);

expect(result).toEqual(userOptions);
expect(result).toEqual(expect.objectContaining(userOptions));
});

it('merges options from env with user options', () => {
Expand All@@ -26,7 +26,7 @@ describe('getFinalOptions', () => {

const result = getFinalOptions(userOptions, env);

expect(result).toEqual({ dsn: 'test-dsn', release: 'user-release' });
expect(result).toEqual(expect.objectContaining({ dsn: 'test-dsn', release: 'user-release' }));
});

it('uses user options when SENTRY_RELEASE exists but is not a string', () => {
Expand All@@ -35,7 +35,7 @@ describe('getFinalOptions', () => {

const result = getFinalOptions(userOptions, env);

expect(result).toEqual(userOptions);
expect(result).toEqual(expect.objectContaining(userOptions));
});

it('uses user options when SENTRY_RELEASE does not exist', () => {
Expand All@@ -44,7 +44,7 @@ describe('getFinalOptions', () => {

const result = getFinalOptions(userOptions, env);

expect(result).toEqual(userOptions);
expect(result).toEqual(expect.objectContaining(userOptions));
});

it('takes user options over env options', () => {
Expand All@@ -53,7 +53,70 @@ describe('getFinalOptions', () => {

const result = getFinalOptions(userOptions, env);

expect(result).toEqual(userOptions);
expect(result).toEqual(expect.objectContaining(userOptions));
});

it('adds debug from env when user debug is undefined', () => {
const userOptions = { dsn: 'test-dsn' };
const env = { SENTRY_DEBUG: '1' };

const result = getFinalOptions(userOptions, env);

expect(result.debug).toBe(true);
});

it('uses SENTRY_DSN from env when user dsn is undefined', () => {
const userOptions = { release: 'user-release' };
const env = { SENTRY_DSN: 'https://key@ingest.sentry.io/1' };

const result = getFinalOptions(userOptions, env);

expect(result.dsn).toBe('https://key@ingest.sentry.io/1');
expect(result.release).toBe('user-release');
});

it('uses SENTRY_ENVIRONMENT from env when user environment is undefined', () => {
const userOptions = { dsn: 'test-dsn' };
const env = { SENTRY_ENVIRONMENT: 'staging' };

const result = getFinalOptions(userOptions, env);

expect(result.environment).toBe('staging');
});

it('uses SENTRY_TRACES_SAMPLE_RATE from env when user tracesSampleRate is undefined', () => {
const userOptions = { dsn: 'test-dsn' };
const env = { SENTRY_TRACES_SAMPLE_RATE: '0.5' };

const result = getFinalOptions(userOptions, env);

expect(result.tracesSampleRate).toBe(0.5);
});

it('does not use SENTRY_TRACES_SAMPLE_RATE from env when it is gibberish', () => {
const env = { SENTRY_TRACES_SAMPLE_RATE: 'ʕっ•ᴥ•ʔっ' };

const result = getFinalOptions(undefined, env);

expect(result.tracesSampleRate).toBeUndefined();
});

it('prefers user dsn over env SENTRY_DSN', () => {
const userOptions = { dsn: 'user-dsn', release: 'user-release' };
const env = { SENTRY_DSN: 'https://env@ingest.sentry.io/1' };

const result = getFinalOptions(userOptions, env);

expect(result.dsn).toBe('user-dsn');
});

it('ignores SENTRY_DSN when not a string', () => {
const userOptions = { dsn: undefined };
const env = { SENTRY_DSN: 123 };

const result = getFinalOptions(userOptions, env);

expect(result.dsn).toBeUndefined();
});

describe('CF_VERSION_METADATA', () => {
Expand All@@ -63,7 +126,7 @@ describe('getFinalOptions', () => {

const result = getFinalOptions(userOptions, env);

expect(result).toEqual({ dsn: 'test-dsn', release: 'version-123' });
expect(result).toEqual(expect.objectContaining({ dsn: 'test-dsn', release: 'version-123' }));
});

it('prefers SENTRY_RELEASE over CF_VERSION_METADATA.id', () => {
Expand All@@ -75,7 +138,7 @@ describe('getFinalOptions', () => {

const result = getFinalOptions(userOptions, env);

expect(result).toEqual({ dsn: 'test-dsn', release: 'env-release' });
expect(result).toEqual(expect.objectContaining({ dsn: 'test-dsn', release: 'env-release' }));
});

it('prefers user release over CF_VERSION_METADATA.id', () => {
Expand All@@ -84,7 +147,7 @@ describe('getFinalOptions', () => {

const result = getFinalOptions(userOptions, env);

expect(result).toEqual({ dsn: 'test-dsn', release: 'user-release' });
expect(result).toEqual(expect.objectContaining({ dsn: 'test-dsn', release: 'user-release' }));
});

it('prefers user release over both SENTRY_RELEASE and CF_VERSION_METADATA.id', () => {
Expand All@@ -96,7 +159,7 @@ describe('getFinalOptions', () => {

const result = getFinalOptions(userOptions, env);

expect(result).toEqual({ dsn: 'test-dsn', release: 'user-release' });
expect(result).toEqual(expect.objectContaining({ dsn: 'test-dsn', release: 'user-release' }));
});

it('ignores CF_VERSION_METADATA when it is not an object', () => {
Expand All@@ -105,7 +168,7 @@ describe('getFinalOptions', () => {

const result = getFinalOptions(userOptions, env);

expect(result).toEqual({ dsn: 'test-dsn', release: undefined });
expect(result).toEqual(expect.objectContaining({ dsn: 'test-dsn', release: undefined }));
});

it('ignores CF_VERSION_METADATA when id is not a string', () => {
Expand All@@ -114,7 +177,7 @@ describe('getFinalOptions', () => {

const result = getFinalOptions(userOptions, env);

expect(result).toEqual({ dsn: 'test-dsn', release: undefined });
expect(result).toEqual(expect.objectContaining({ dsn: 'test-dsn', release: undefined }));
});

it('ignores CF_VERSION_METADATA when id is missing', () => {
Expand All@@ -123,7 +186,7 @@ describe('getFinalOptions', () => {

const result = getFinalOptions(userOptions, env);

expect(result).toEqual({ dsn: 'test-dsn', release: undefined });
expect(result).toEqual(expect.objectContaining({ dsn: 'test-dsn', release: undefined }));
});
});
});
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -62,6 +62,7 @@ export {
_INTERNAL_shouldSkipAiProviderWrapping,
_INTERNAL_clearAiProviderSkips,
} from './utils/ai/providerSkip';
export { envToBool } from './utils/envToBool';
export { applyScopeDataToEvent, mergeScopeData, getCombinedScopeData } from './utils/scopeData';
export { prepareEvent } from './utils/prepareEvent';
export type { ExclusiveEventHintOrCaptureContext } from './utils/prepareEvent';
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest';
import { envToBool } from '../../src/utils/envToBool';
import { envToBool } from '../../../src/utils/envToBool';

describe('envToBool', () => {
it.each([
Expand Down
2 changes: 1 addition & 1 deletion packages/node-core/src/common-exports.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -31,7 +31,6 @@ export { getRequestUrl } from './utils/getRequestUrl';
export { initializeEsmLoader } from './sdk/esmLoader';
export { isCjs } from './utils/detection';
export { createMissingInstrumentationContext } from './utils/createMissingInstrumentationContext';
export { envToBool } from './utils/envToBool';
export { makeNodeTransport, type NodeTransportOptions } from './transports';
export type { HTTPModuleRequestIncomingMessage } from './transports/http-module';
export { cron } from './cron';
Expand DownExpand Up@@ -117,6 +116,7 @@ export {
wrapMcpServerWithSentry,
featureFlagsIntegration,
metrics,
envToBool,
} from '@sentry/core';

export type {
Expand Down
2 changes: 1 addition & 1 deletion packages/node-core/src/light/sdk.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,6 +4,7 @@ import {
consoleIntegration,
consoleSandbox,
debug,
envToBool,
eventFiltersIntegration,
functionToStringIntegration,
getCurrentScope,
Expand All@@ -29,7 +30,6 @@ import { defaultStackParser, getSentryRelease } from '../sdk/api';
import { makeNodeTransport } from '../transports';
import type { NodeClientOptions, NodeOptions } from '../types';
import { isCjs } from '../utils/detection';
import { envToBool } from '../utils/envToBool';
import { getSpotlightConfig } from '../utils/spotlight';
import { setAsyncLocalStorageAsyncContextStrategy } from './asyncLocalStorageStrategy';
import { LightNodeClient } from './client';
Expand Down
2 changes: 1 addition & 1 deletion packages/node-core/src/sdk/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,7 @@ import {
consoleSandbox,
conversationIdIntegration,
debug,
envToBool,
functionToStringIntegration,
getCurrentScope,
getIntegrationsToSetup,
Expand DownExpand Up@@ -38,7 +39,6 @@ import { systemErrorIntegration } from '../integrations/systemError';
import { makeNodeTransport } from '../transports';
import type { NodeClientOptions, NodeOptions } from '../types';
import { isCjs } from '../utils/detection';
import { envToBool } from '../utils/envToBool';
import { getSpotlightConfig } from '../utils/spotlight';
import { defaultStackParser, getSentryRelease } from './api';
import { NodeClient } from './client';
Expand Down
2 changes: 1 addition & 1 deletion packages/node-core/src/utils/spotlight.ts
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
import { envToBool } from './envToBool';
import { envToBool } from '@sentry/core';

/**
* Parse the spotlight option with proper precedence:
Expand Down
Loading
, '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
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
2 changes: 1 addition & 1 deletion packages/cloudflare/src/handler.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -38,7 +38,7 @@ export function withSentry<
QueueHandlerMessage,
CfHostMetadata
>,
>(optionsCallback: (env: Env) => CloudflareOptions, handler: T): T {
>(optionsCallback: (env: Env) => CloudflareOptions | undefined, handler: T): T {

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note: I made this return undefined as it was also working before theoretically. I adapted the types to also allow undefined just to have this case also handled

setAsyncLocalStorageAsyncContextStrategy();

try {
Expand Down
22 changes: 20 additions & 2 deletions packages/cloudflare/src/options.ts
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
import { envToBool } from '@sentry/core';
import type { CloudflareOptions } from './client';

/**
Expand All@@ -17,6 +18,12 @@ function isVersionMetadata(value: unknown): value is CfVersionMetadata {
return typeof value === 'object' && value !== null && 'id' in value && typeof value.id === 'string';
}

function getEnvVar<T extends Record<string, unknown>>(env: unknown, varName: keyof T): string | undefined {
return typeof env === 'object' && env !== null && varName in env && typeof (env as T)[varName] === 'string'
? ((env as T)[varName] as string)
: undefined;
}

/**
* Merges the options passed in from the user with the options we read from
* the Cloudflare `env` environment variable object.
Expand All@@ -31,7 +38,7 @@ function isVersionMetadata(value: unknown): value is CfVersionMetadata {
*
* @returns The final options.
*/
export function getFinalOptions(userOptions: CloudflareOptions, env: unknown): CloudflareOptions {
export function getFinalOptions(userOptions: CloudflareOptions = {}, env: unknown): CloudflareOptions {
if (typeof env !== 'object' || env === null) {
return userOptions;
}
Expand All@@ -44,5 +51,16 @@ export function getFinalOptions(userOptions: CloudflareOptions, env: unknown): C
? env.CF_VERSION_METADATA.id
: undefined;

return { release, ...userOptions };
const tracesSampleRate =
userOptions.tracesSampleRate ?? parseFloat(getEnvVar(env, 'SENTRY_TRACES_SAMPLE_RATE') ?? '');

return {
release,
...userOptions,
dsn: userOptions.dsn ?? getEnvVar(env, 'SENTRY_DSN'),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

l: can we also add SENTRY_DEBUG? You could use envToBool for that from node-core, maybe lift that up into @sentry/core?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great idea. Added.

I also added tunnel to it

environment: userOptions.environment ?? getEnvVar(env, 'SENTRY_ENVIRONMENT'),
tracesSampleRate: isFinite(tracesSampleRate) ? tracesSampleRate : undefined,
debug: userOptions.debug ?? envToBool(getEnvVar(env, 'SENTRY_DEBUG')),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

envToBool(undefined) forces debug: false when unset

Medium Severity

When SENTRY_DEBUG is absent from env, getEnvVar returns undefined, and envToBool(undefined) returns false (since Boolean(undefined) is false in loose mode). This means debug is always explicitly set to false in the returned options, even when neither the user nor the env specified it. This breaks multiple existing toEqual assertions (e.g., lines 29, 120, 132) that don't expect a debug property.

Fix in CursorFix in Web

tunnel: userOptions.tunnel ?? getEnvVar(env, 'SENTRY_TUNNEL'),
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spread operator overrides env-derived fallback values

Medium Severity

The ...userOptions spread at the end of the return object overrides the env-derived fallback values for dsn, environment, and tracesSampleRate whenever those keys exist in userOptions — even if their values are undefined. This means if a user passes { dsn: undefined }, the SENTRY_DSN env fallback is silently ignored. The ?? fallback logic and the isFinite guard are both bypassed by the spread.

Fix in CursorFix in Web

}
25 changes: 15 additions & 10 deletions packages/cloudflare/test/handler.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -31,6 +31,11 @@ const MOCK_ENV = {
SENTRY_RELEASE: '1.1.1',
};

// Mock env without DSN for tests that should not initialize the SDK
const MOCK_ENV_WITHOUT_DSN = {

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note: It seems that the SDK was not initialized for the tests where this is used. By now also allowing SENTRY_DSN from the variables these tests fail. I'll create a follow up PR and remove this, so the SDK is actually initialized (I don't want to blow up this PR with other code changes)

SENTRY_RELEASE: '1.1.1',
};

function addDelayedWaitUntil(context: ExecutionContext) {
context.waitUntil(new Promise<void>(resolve => setTimeout(() => resolve())));
}
Expand DownExpand Up@@ -149,12 +154,12 @@ describe('withSentry', () => {
addDelayedWaitUntil(_context);
return new Response('test');
},
} satisfies ExportedHandler<typeof MOCK_ENV>;
} satisfies ExportedHandler<typeof MOCK_ENV_WITHOUT_DSN>;

const wrappedHandler = withSentry(vi.fn(), handler);
const waits: Promise<unknown>[] = [];
const waitUntil = vi.fn(promise => waits.push(promise));
await wrappedHandler.fetch?.(new Request('https://example.com'), MOCK_ENV, {
await wrappedHandler.fetch?.(new Request('https://example.com'), MOCK_ENV_WITHOUT_DSN, {
waitUntil,
} as unknown as ExecutionContext);
expect(flush).not.toBeCalled();
Expand DownExpand Up@@ -389,12 +394,12 @@ describe('withSentry', () => {
addDelayedWaitUntil(_context);
return;
},
} satisfies ExportedHandler<typeof MOCK_ENV>;
} satisfies ExportedHandler<typeof MOCK_ENV_WITHOUT_DSN>;

const wrappedHandler = withSentry(vi.fn(), handler);
const waits: Promise<unknown>[] = [];
const waitUntil = vi.fn(promise => waits.push(promise));
await wrappedHandler.scheduled?.(createMockScheduledController(), MOCK_ENV, {
await wrappedHandler.scheduled?.(createMockScheduledController(), MOCK_ENV_WITHOUT_DSN, {
waitUntil,
} as unknown as ExecutionContext);
expect(flush).not.toBeCalled();
Expand DownExpand Up@@ -628,12 +633,12 @@ describe('withSentry', () => {
addDelayedWaitUntil(_context);
return;
},
} satisfies ExportedHandler<typeof MOCK_ENV>;
} satisfies ExportedHandler<typeof MOCK_ENV_WITHOUT_DSN>;

const wrappedHandler = withSentry(vi.fn(), handler);
const waits: Promise<unknown>[] = [];
const waitUntil = vi.fn(promise => waits.push(promise));
await wrappedHandler.email?.(createMockEmailMessage(), MOCK_ENV, {
await wrappedHandler.email?.(createMockEmailMessage(), MOCK_ENV_WITHOUT_DSN, {
waitUntil,
} as unknown as ExecutionContext);
expect(flush).not.toBeCalled();
Expand DownExpand Up@@ -871,12 +876,12 @@ describe('withSentry', () => {
addDelayedWaitUntil(_context);
return;
},
} satisfies ExportedHandler<typeof MOCK_ENV>;
} satisfies ExportedHandler<typeof MOCK_ENV_WITHOUT_DSN>;

const wrappedHandler = withSentry(vi.fn(), handler);
const waits: Promise<unknown>[] = [];
const waitUntil = vi.fn(promise => waits.push(promise));
await wrappedHandler.queue?.(createMockQueueBatch(), MOCK_ENV, {
await wrappedHandler.queue?.(createMockQueueBatch(), MOCK_ENV_WITHOUT_DSN, {
waitUntil,
} as unknown as ExecutionContext);
expect(flush).not.toBeCalled();
Expand DownExpand Up@@ -1069,12 +1074,12 @@ describe('withSentry', () => {
addDelayedWaitUntil(_context);
return;
},
} satisfies ExportedHandler<typeof MOCK_ENV>;
} satisfies ExportedHandler<typeof MOCK_ENV_WITHOUT_DSN>;

const wrappedHandler = withSentry(vi.fn(), handler);
const waits: Promise<unknown>[] = [];
const waitUntil = vi.fn(promise => waits.push(promise));
await wrappedHandler.tail?.(createMockTailEvent(), MOCK_ENV, {
await wrappedHandler.tail?.(createMockTailEvent(), MOCK_ENV_WITHOUT_DSN, {
waitUntil,
} as unknown as ExecutionContext);
expect(flush).not.toBeCalled();
Expand Down
87 changes: 75 additions & 12 deletions packages/cloudflare/test/options.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -17,7 +17,7 @@ describe('getFinalOptions', () => {

const result = getFinalOptions(userOptions, env);

expect(result).toEqual(userOptions);
expect(result).toEqual(expect.objectContaining(userOptions));
});

it('merges options from env with user options', () => {
Expand All@@ -26,7 +26,7 @@ describe('getFinalOptions', () => {

const result = getFinalOptions(userOptions, env);

expect(result).toEqual({ dsn: 'test-dsn', release: 'user-release' });
expect(result).toEqual(expect.objectContaining({ dsn: 'test-dsn', release: 'user-release' }));
});

it('uses user options when SENTRY_RELEASE exists but is not a string', () => {
Expand All@@ -35,7 +35,7 @@ describe('getFinalOptions', () => {

const result = getFinalOptions(userOptions, env);

expect(result).toEqual(userOptions);
expect(result).toEqual(expect.objectContaining(userOptions));
});

it('uses user options when SENTRY_RELEASE does not exist', () => {
Expand All@@ -44,7 +44,7 @@ describe('getFinalOptions', () => {

const result = getFinalOptions(userOptions, env);

expect(result).toEqual(userOptions);
expect(result).toEqual(expect.objectContaining(userOptions));
});

it('takes user options over env options', () => {
Expand All@@ -53,7 +53,70 @@ describe('getFinalOptions', () => {

const result = getFinalOptions(userOptions, env);

expect(result).toEqual(userOptions);
expect(result).toEqual(expect.objectContaining(userOptions));
});

it('adds debug from env when user debug is undefined', () => {
const userOptions = { dsn: 'test-dsn' };
const env = { SENTRY_DEBUG: '1' };

const result = getFinalOptions(userOptions, env);

expect(result.debug).toBe(true);
});

it('uses SENTRY_DSN from env when user dsn is undefined', () => {
const userOptions = { release: 'user-release' };
const env = { SENTRY_DSN: 'https://key@ingest.sentry.io/1' };

const result = getFinalOptions(userOptions, env);

expect(result.dsn).toBe('https://key@ingest.sentry.io/1');
expect(result.release).toBe('user-release');
});

it('uses SENTRY_ENVIRONMENT from env when user environment is undefined', () => {
const userOptions = { dsn: 'test-dsn' };
const env = { SENTRY_ENVIRONMENT: 'staging' };

const result = getFinalOptions(userOptions, env);

expect(result.environment).toBe('staging');
});

it('uses SENTRY_TRACES_SAMPLE_RATE from env when user tracesSampleRate is undefined', () => {
const userOptions = { dsn: 'test-dsn' };
const env = { SENTRY_TRACES_SAMPLE_RATE: '0.5' };

const result = getFinalOptions(userOptions, env);

expect(result.tracesSampleRate).toBe(0.5);
});

it('does not use SENTRY_TRACES_SAMPLE_RATE from env when it is gibberish', () => {
const env = { SENTRY_TRACES_SAMPLE_RATE: 'ʕっ•ᴥ•ʔっ' };

const result = getFinalOptions(undefined, env);

expect(result.tracesSampleRate).toBeUndefined();
});

it('prefers user dsn over env SENTRY_DSN', () => {
const userOptions = { dsn: 'user-dsn', release: 'user-release' };
const env = { SENTRY_DSN: 'https://env@ingest.sentry.io/1' };

const result = getFinalOptions(userOptions, env);

expect(result.dsn).toBe('user-dsn');
});

it('ignores SENTRY_DSN when not a string', () => {
const userOptions = { dsn: undefined };
const env = { SENTRY_DSN: 123 };

const result = getFinalOptions(userOptions, env);

expect(result.dsn).toBeUndefined();
});

describe('CF_VERSION_METADATA', () => {
Expand All@@ -63,7 +126,7 @@ describe('getFinalOptions', () => {

const result = getFinalOptions(userOptions, env);

expect(result).toEqual({ dsn: 'test-dsn', release: 'version-123' });
expect(result).toEqual(expect.objectContaining({ dsn: 'test-dsn', release: 'version-123' }));
});

it('prefers SENTRY_RELEASE over CF_VERSION_METADATA.id', () => {
Expand All@@ -75,7 +138,7 @@ describe('getFinalOptions', () => {

const result = getFinalOptions(userOptions, env);

expect(result).toEqual({ dsn: 'test-dsn', release: 'env-release' });
expect(result).toEqual(expect.objectContaining({ dsn: 'test-dsn', release: 'env-release' }));
});

it('prefers user release over CF_VERSION_METADATA.id', () => {
Expand All@@ -84,7 +147,7 @@ describe('getFinalOptions', () => {

const result = getFinalOptions(userOptions, env);

expect(result).toEqual({ dsn: 'test-dsn', release: 'user-release' });
expect(result).toEqual(expect.objectContaining({ dsn: 'test-dsn', release: 'user-release' }));
});

it('prefers user release over both SENTRY_RELEASE and CF_VERSION_METADATA.id', () => {
Expand All@@ -96,7 +159,7 @@ describe('getFinalOptions', () => {

const result = getFinalOptions(userOptions, env);

expect(result).toEqual({ dsn: 'test-dsn', release: 'user-release' });
expect(result).toEqual(expect.objectContaining({ dsn: 'test-dsn', release: 'user-release' }));
});

it('ignores CF_VERSION_METADATA when it is not an object', () => {
Expand All@@ -105,7 +168,7 @@ describe('getFinalOptions', () => {

const result = getFinalOptions(userOptions, env);

expect(result).toEqual({ dsn: 'test-dsn', release: undefined });
expect(result).toEqual(expect.objectContaining({ dsn: 'test-dsn', release: undefined }));
});

it('ignores CF_VERSION_METADATA when id is not a string', () => {
Expand All@@ -114,7 +177,7 @@ describe('getFinalOptions', () => {

const result = getFinalOptions(userOptions, env);

expect(result).toEqual({ dsn: 'test-dsn', release: undefined });
expect(result).toEqual(expect.objectContaining({ dsn: 'test-dsn', release: undefined }));
});

it('ignores CF_VERSION_METADATA when id is missing', () => {
Expand All@@ -123,7 +186,7 @@ describe('getFinalOptions', () => {

const result = getFinalOptions(userOptions, env);

expect(result).toEqual({ dsn: 'test-dsn', release: undefined });
expect(result).toEqual(expect.objectContaining({ dsn: 'test-dsn', release: undefined }));
});
});
});
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -62,6 +62,7 @@ export {
_INTERNAL_shouldSkipAiProviderWrapping,
_INTERNAL_clearAiProviderSkips,
} from './utils/ai/providerSkip';
export { envToBool } from './utils/envToBool';
export { applyScopeDataToEvent, mergeScopeData, getCombinedScopeData } from './utils/scopeData';
export { prepareEvent } from './utils/prepareEvent';
export type { ExclusiveEventHintOrCaptureContext } from './utils/prepareEvent';
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest';
import { envToBool } from '../../src/utils/envToBool';
import { envToBool } from '../../../src/utils/envToBool';

describe('envToBool', () => {
it.each([
Expand Down
2 changes: 1 addition & 1 deletion packages/node-core/src/common-exports.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -31,7 +31,6 @@ export { getRequestUrl } from './utils/getRequestUrl';
export { initializeEsmLoader } from './sdk/esmLoader';
export { isCjs } from './utils/detection';
export { createMissingInstrumentationContext } from './utils/createMissingInstrumentationContext';
export { envToBool } from './utils/envToBool';
export { makeNodeTransport, type NodeTransportOptions } from './transports';
export type { HTTPModuleRequestIncomingMessage } from './transports/http-module';
export { cron } from './cron';
Expand DownExpand Up@@ -117,6 +116,7 @@ export {
wrapMcpServerWithSentry,
featureFlagsIntegration,
metrics,
envToBool,
} from '@sentry/core';

export type {
Expand Down
2 changes: 1 addition & 1 deletion packages/node-core/src/light/sdk.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,6 +4,7 @@ import {
consoleIntegration,
consoleSandbox,
debug,
envToBool,
eventFiltersIntegration,
functionToStringIntegration,
getCurrentScope,
Expand All@@ -29,7 +30,6 @@ import { defaultStackParser, getSentryRelease } from '../sdk/api';
import { makeNodeTransport } from '../transports';
import type { NodeClientOptions, NodeOptions } from '../types';
import { isCjs } from '../utils/detection';
import { envToBool } from '../utils/envToBool';
import { getSpotlightConfig } from '../utils/spotlight';
import { setAsyncLocalStorageAsyncContextStrategy } from './asyncLocalStorageStrategy';
import { LightNodeClient } from './client';
Expand Down
2 changes: 1 addition & 1 deletion packages/node-core/src/sdk/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,7 @@ import {
consoleSandbox,
conversationIdIntegration,
debug,
envToBool,
functionToStringIntegration,
getCurrentScope,
getIntegrationsToSetup,
Expand DownExpand Up@@ -38,7 +39,6 @@ import { systemErrorIntegration } from '../integrations/systemError';
import { makeNodeTransport } from '../transports';
import type { NodeClientOptions, NodeOptions } from '../types';
import { isCjs } from '../utils/detection';
import { envToBool } from '../utils/envToBool';
import { getSpotlightConfig } from '../utils/spotlight';
import { defaultStackParser, getSentryRelease } from './api';
import { NodeClient } from './client';
Expand Down
2 changes: 1 addition & 1 deletion packages/node-core/src/utils/spotlight.ts
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
import { envToBool } from './envToBool';
import { envToBool } from '@sentry/core';

/**
* Parse the spotlight option with proper precedence:
Expand Down
Loading
, '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
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
2 changes: 1 addition & 1 deletion packages/cloudflare/src/handler.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -38,7 +38,7 @@ export function withSentry<
QueueHandlerMessage,
CfHostMetadata
>,
>(optionsCallback: (env: Env) => CloudflareOptions, handler: T): T {
>(optionsCallback: (env: Env) => CloudflareOptions | undefined, handler: T): T {

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note: I made this return undefined as it was also working before theoretically. I adapted the types to also allow undefined just to have this case also handled

setAsyncLocalStorageAsyncContextStrategy();

try {
Expand Down
22 changes: 20 additions & 2 deletions packages/cloudflare/src/options.ts
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
import { envToBool } from '@sentry/core';
import type { CloudflareOptions } from './client';

/**
Expand All@@ -17,6 +18,12 @@ function isVersionMetadata(value: unknown): value is CfVersionMetadata {
return typeof value === 'object' && value !== null && 'id' in value && typeof value.id === 'string';
}

function getEnvVar<T extends Record<string, unknown>>(env: unknown, varName: keyof T): string | undefined {
return typeof env === 'object' && env !== null && varName in env && typeof (env as T)[varName] === 'string'
? ((env as T)[varName] as string)
: undefined;
}

/**
* Merges the options passed in from the user with the options we read from
* the Cloudflare `env` environment variable object.
Expand All@@ -31,7 +38,7 @@ function isVersionMetadata(value: unknown): value is CfVersionMetadata {
*
* @returns The final options.
*/
export function getFinalOptions(userOptions: CloudflareOptions, env: unknown): CloudflareOptions {
export function getFinalOptions(userOptions: CloudflareOptions = {}, env: unknown): CloudflareOptions {
if (typeof env !== 'object' || env === null) {
return userOptions;
}
Expand All@@ -44,5 +51,16 @@ export function getFinalOptions(userOptions: CloudflareOptions, env: unknown): C
? env.CF_VERSION_METADATA.id
: undefined;

return { release, ...userOptions };
const tracesSampleRate =
userOptions.tracesSampleRate ?? parseFloat(getEnvVar(env, 'SENTRY_TRACES_SAMPLE_RATE') ?? '');

return {
release,
...userOptions,
dsn: userOptions.dsn ?? getEnvVar(env, 'SENTRY_DSN'),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

l: can we also add SENTRY_DEBUG? You could use envToBool for that from node-core, maybe lift that up into @sentry/core?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great idea. Added.

I also added tunnel to it

environment: userOptions.environment ?? getEnvVar(env, 'SENTRY_ENVIRONMENT'),
tracesSampleRate: isFinite(tracesSampleRate) ? tracesSampleRate : undefined,
debug: userOptions.debug ?? envToBool(getEnvVar(env, 'SENTRY_DEBUG')),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

envToBool(undefined) forces debug: false when unset

Medium Severity

When SENTRY_DEBUG is absent from env, getEnvVar returns undefined, and envToBool(undefined) returns false (since Boolean(undefined) is false in loose mode). This means debug is always explicitly set to false in the returned options, even when neither the user nor the env specified it. This breaks multiple existing toEqual assertions (e.g., lines 29, 120, 132) that don't expect a debug property.

Fix in CursorFix in Web

tunnel: userOptions.tunnel ?? getEnvVar(env, 'SENTRY_TUNNEL'),
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spread operator overrides env-derived fallback values

Medium Severity

The ...userOptions spread at the end of the return object overrides the env-derived fallback values for dsn, environment, and tracesSampleRate whenever those keys exist in userOptions — even if their values are undefined. This means if a user passes { dsn: undefined }, the SENTRY_DSN env fallback is silently ignored. The ?? fallback logic and the isFinite guard are both bypassed by the spread.

Fix in CursorFix in Web

}
25 changes: 15 additions & 10 deletions packages/cloudflare/test/handler.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -31,6 +31,11 @@ const MOCK_ENV = {
SENTRY_RELEASE: '1.1.1',
};

// Mock env without DSN for tests that should not initialize the SDK
const MOCK_ENV_WITHOUT_DSN = {

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note: It seems that the SDK was not initialized for the tests where this is used. By now also allowing SENTRY_DSN from the variables these tests fail. I'll create a follow up PR and remove this, so the SDK is actually initialized (I don't want to blow up this PR with other code changes)

SENTRY_RELEASE: '1.1.1',
};

function addDelayedWaitUntil(context: ExecutionContext) {
context.waitUntil(new Promise<void>(resolve => setTimeout(() => resolve())));
}
Expand DownExpand Up@@ -149,12 +154,12 @@ describe('withSentry', () => {
addDelayedWaitUntil(_context);
return new Response('test');
},
} satisfies ExportedHandler<typeof MOCK_ENV>;
} satisfies ExportedHandler<typeof MOCK_ENV_WITHOUT_DSN>;

const wrappedHandler = withSentry(vi.fn(), handler);
const waits: Promise<unknown>[] = [];
const waitUntil = vi.fn(promise => waits.push(promise));
await wrappedHandler.fetch?.(new Request('https://example.com'), MOCK_ENV, {
await wrappedHandler.fetch?.(new Request('https://example.com'), MOCK_ENV_WITHOUT_DSN, {
waitUntil,
} as unknown as ExecutionContext);
expect(flush).not.toBeCalled();
Expand DownExpand Up@@ -389,12 +394,12 @@ describe('withSentry', () => {
addDelayedWaitUntil(_context);
return;
},
} satisfies ExportedHandler<typeof MOCK_ENV>;
} satisfies ExportedHandler<typeof MOCK_ENV_WITHOUT_DSN>;

const wrappedHandler = withSentry(vi.fn(), handler);
const waits: Promise<unknown>[] = [];
const waitUntil = vi.fn(promise => waits.push(promise));
await wrappedHandler.scheduled?.(createMockScheduledController(), MOCK_ENV, {
await wrappedHandler.scheduled?.(createMockScheduledController(), MOCK_ENV_WITHOUT_DSN, {
waitUntil,
} as unknown as ExecutionContext);
expect(flush).not.toBeCalled();
Expand DownExpand Up@@ -628,12 +633,12 @@ describe('withSentry', () => {
addDelayedWaitUntil(_context);
return;
},
} satisfies ExportedHandler<typeof MOCK_ENV>;
} satisfies ExportedHandler<typeof MOCK_ENV_WITHOUT_DSN>;

const wrappedHandler = withSentry(vi.fn(), handler);
const waits: Promise<unknown>[] = [];
const waitUntil = vi.fn(promise => waits.push(promise));
await wrappedHandler.email?.(createMockEmailMessage(), MOCK_ENV, {
await wrappedHandler.email?.(createMockEmailMessage(), MOCK_ENV_WITHOUT_DSN, {
waitUntil,
} as unknown as ExecutionContext);
expect(flush).not.toBeCalled();
Expand DownExpand Up@@ -871,12 +876,12 @@ describe('withSentry', () => {
addDelayedWaitUntil(_context);
return;
},
} satisfies ExportedHandler<typeof MOCK_ENV>;
} satisfies ExportedHandler<typeof MOCK_ENV_WITHOUT_DSN>;

const wrappedHandler = withSentry(vi.fn(), handler);
const waits: Promise<unknown>[] = [];
const waitUntil = vi.fn(promise => waits.push(promise));
await wrappedHandler.queue?.(createMockQueueBatch(), MOCK_ENV, {
await wrappedHandler.queue?.(createMockQueueBatch(), MOCK_ENV_WITHOUT_DSN, {
waitUntil,
} as unknown as ExecutionContext);
expect(flush).not.toBeCalled();
Expand DownExpand Up@@ -1069,12 +1074,12 @@ describe('withSentry', () => {
addDelayedWaitUntil(_context);
return;
},
} satisfies ExportedHandler<typeof MOCK_ENV>;
} satisfies ExportedHandler<typeof MOCK_ENV_WITHOUT_DSN>;

const wrappedHandler = withSentry(vi.fn(), handler);
const waits: Promise<unknown>[] = [];
const waitUntil = vi.fn(promise => waits.push(promise));
await wrappedHandler.tail?.(createMockTailEvent(), MOCK_ENV, {
await wrappedHandler.tail?.(createMockTailEvent(), MOCK_ENV_WITHOUT_DSN, {
waitUntil,
} as unknown as ExecutionContext);
expect(flush).not.toBeCalled();
Expand Down
87 changes: 75 additions & 12 deletions packages/cloudflare/test/options.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -17,7 +17,7 @@ describe('getFinalOptions', () => {

const result = getFinalOptions(userOptions, env);

expect(result).toEqual(userOptions);
expect(result).toEqual(expect.objectContaining(userOptions));
});

it('merges options from env with user options', () => {
Expand All@@ -26,7 +26,7 @@ describe('getFinalOptions', () => {

const result = getFinalOptions(userOptions, env);

expect(result).toEqual({ dsn: 'test-dsn', release: 'user-release' });
expect(result).toEqual(expect.objectContaining({ dsn: 'test-dsn', release: 'user-release' }));
});

it('uses user options when SENTRY_RELEASE exists but is not a string', () => {
Expand All@@ -35,7 +35,7 @@ describe('getFinalOptions', () => {

const result = getFinalOptions(userOptions, env);

expect(result).toEqual(userOptions);
expect(result).toEqual(expect.objectContaining(userOptions));
});

it('uses user options when SENTRY_RELEASE does not exist', () => {
Expand All@@ -44,7 +44,7 @@ describe('getFinalOptions', () => {

const result = getFinalOptions(userOptions, env);

expect(result).toEqual(userOptions);
expect(result).toEqual(expect.objectContaining(userOptions));
});

it('takes user options over env options', () => {
Expand All@@ -53,7 +53,70 @@ describe('getFinalOptions', () => {

const result = getFinalOptions(userOptions, env);

expect(result).toEqual(userOptions);
expect(result).toEqual(expect.objectContaining(userOptions));
});

it('adds debug from env when user debug is undefined', () => {
const userOptions = { dsn: 'test-dsn' };
const env = { SENTRY_DEBUG: '1' };

const result = getFinalOptions(userOptions, env);

expect(result.debug).toBe(true);
});

it('uses SENTRY_DSN from env when user dsn is undefined', () => {
const userOptions = { release: 'user-release' };
const env = { SENTRY_DSN: 'https://key@ingest.sentry.io/1' };

const result = getFinalOptions(userOptions, env);

expect(result.dsn).toBe('https://key@ingest.sentry.io/1');
expect(result.release).toBe('user-release');
});

it('uses SENTRY_ENVIRONMENT from env when user environment is undefined', () => {
const userOptions = { dsn: 'test-dsn' };
const env = { SENTRY_ENVIRONMENT: 'staging' };

const result = getFinalOptions(userOptions, env);

expect(result.environment).toBe('staging');
});

it('uses SENTRY_TRACES_SAMPLE_RATE from env when user tracesSampleRate is undefined', () => {
const userOptions = { dsn: 'test-dsn' };
const env = { SENTRY_TRACES_SAMPLE_RATE: '0.5' };

const result = getFinalOptions(userOptions, env);

expect(result.tracesSampleRate).toBe(0.5);
});

it('does not use SENTRY_TRACES_SAMPLE_RATE from env when it is gibberish', () => {
const env = { SENTRY_TRACES_SAMPLE_RATE: 'ʕっ•ᴥ•ʔっ' };

const result = getFinalOptions(undefined, env);

expect(result.tracesSampleRate).toBeUndefined();
});

it('prefers user dsn over env SENTRY_DSN', () => {
const userOptions = { dsn: 'user-dsn', release: 'user-release' };
const env = { SENTRY_DSN: 'https://env@ingest.sentry.io/1' };

const result = getFinalOptions(userOptions, env);

expect(result.dsn).toBe('user-dsn');
});

it('ignores SENTRY_DSN when not a string', () => {
const userOptions = { dsn: undefined };
const env = { SENTRY_DSN: 123 };

const result = getFinalOptions(userOptions, env);

expect(result.dsn).toBeUndefined();
});

describe('CF_VERSION_METADATA', () => {
Expand All@@ -63,7 +126,7 @@ describe('getFinalOptions', () => {

const result = getFinalOptions(userOptions, env);

expect(result).toEqual({ dsn: 'test-dsn', release: 'version-123' });
expect(result).toEqual(expect.objectContaining({ dsn: 'test-dsn', release: 'version-123' }));
});

it('prefers SENTRY_RELEASE over CF_VERSION_METADATA.id', () => {
Expand All@@ -75,7 +138,7 @@ describe('getFinalOptions', () => {

const result = getFinalOptions(userOptions, env);

expect(result).toEqual({ dsn: 'test-dsn', release: 'env-release' });
expect(result).toEqual(expect.objectContaining({ dsn: 'test-dsn', release: 'env-release' }));
});

it('prefers user release over CF_VERSION_METADATA.id', () => {
Expand All@@ -84,7 +147,7 @@ describe('getFinalOptions', () => {

const result = getFinalOptions(userOptions, env);

expect(result).toEqual({ dsn: 'test-dsn', release: 'user-release' });
expect(result).toEqual(expect.objectContaining({ dsn: 'test-dsn', release: 'user-release' }));
});

it('prefers user release over both SENTRY_RELEASE and CF_VERSION_METADATA.id', () => {
Expand All@@ -96,7 +159,7 @@ describe('getFinalOptions', () => {

const result = getFinalOptions(userOptions, env);

expect(result).toEqual({ dsn: 'test-dsn', release: 'user-release' });
expect(result).toEqual(expect.objectContaining({ dsn: 'test-dsn', release: 'user-release' }));
});

it('ignores CF_VERSION_METADATA when it is not an object', () => {
Expand All@@ -105,7 +168,7 @@ describe('getFinalOptions', () => {

const result = getFinalOptions(userOptions, env);

expect(result).toEqual({ dsn: 'test-dsn', release: undefined });
expect(result).toEqual(expect.objectContaining({ dsn: 'test-dsn', release: undefined }));
});

it('ignores CF_VERSION_METADATA when id is not a string', () => {
Expand All@@ -114,7 +177,7 @@ describe('getFinalOptions', () => {

const result = getFinalOptions(userOptions, env);

expect(result).toEqual({ dsn: 'test-dsn', release: undefined });
expect(result).toEqual(expect.objectContaining({ dsn: 'test-dsn', release: undefined }));
});

it('ignores CF_VERSION_METADATA when id is missing', () => {
Expand All@@ -123,7 +186,7 @@ describe('getFinalOptions', () => {

const result = getFinalOptions(userOptions, env);

expect(result).toEqual({ dsn: 'test-dsn', release: undefined });
expect(result).toEqual(expect.objectContaining({ dsn: 'test-dsn', release: undefined }));
});
});
});
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -62,6 +62,7 @@ export {
_INTERNAL_shouldSkipAiProviderWrapping,
_INTERNAL_clearAiProviderSkips,
} from './utils/ai/providerSkip';
export { envToBool } from './utils/envToBool';
export { applyScopeDataToEvent, mergeScopeData, getCombinedScopeData } from './utils/scopeData';
export { prepareEvent } from './utils/prepareEvent';
export type { ExclusiveEventHintOrCaptureContext } from './utils/prepareEvent';
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest';
import { envToBool } from '../../src/utils/envToBool';
import { envToBool } from '../../../src/utils/envToBool';

describe('envToBool', () => {
it.each([
Expand Down
2 changes: 1 addition & 1 deletion packages/node-core/src/common-exports.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -31,7 +31,6 @@ export { getRequestUrl } from './utils/getRequestUrl';
export { initializeEsmLoader } from './sdk/esmLoader';
export { isCjs } from './utils/detection';
export { createMissingInstrumentationContext } from './utils/createMissingInstrumentationContext';
export { envToBool } from './utils/envToBool';
export { makeNodeTransport, type NodeTransportOptions } from './transports';
export type { HTTPModuleRequestIncomingMessage } from './transports/http-module';
export { cron } from './cron';
Expand DownExpand Up@@ -117,6 +116,7 @@ export {
wrapMcpServerWithSentry,
featureFlagsIntegration,
metrics,
envToBool,
} from '@sentry/core';

export type {
Expand Down
2 changes: 1 addition & 1 deletion packages/node-core/src/light/sdk.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,6 +4,7 @@ import {
consoleIntegration,
consoleSandbox,
debug,
envToBool,
eventFiltersIntegration,
functionToStringIntegration,
getCurrentScope,
Expand All@@ -29,7 +30,6 @@ import { defaultStackParser, getSentryRelease } from '../sdk/api';
import { makeNodeTransport } from '../transports';
import type { NodeClientOptions, NodeOptions } from '../types';
import { isCjs } from '../utils/detection';
import { envToBool } from '../utils/envToBool';
import { getSpotlightConfig } from '../utils/spotlight';
import { setAsyncLocalStorageAsyncContextStrategy } from './asyncLocalStorageStrategy';
import { LightNodeClient } from './client';
Expand Down
2 changes: 1 addition & 1 deletion packages/node-core/src/sdk/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,7 @@ import {
consoleSandbox,
conversationIdIntegration,
debug,
envToBool,
functionToStringIntegration,
getCurrentScope,
getIntegrationsToSetup,
Expand DownExpand Up@@ -38,7 +39,6 @@ import { systemErrorIntegration } from '../integrations/systemError';
import { makeNodeTransport } from '../transports';
import type { NodeClientOptions, NodeOptions } from '../types';
import { isCjs } from '../utils/detection';
import { envToBool } from '../utils/envToBool';
import { getSpotlightConfig } from '../utils/spotlight';
import { defaultStackParser, getSentryRelease } from './api';
import { NodeClient } from './client';
Expand Down
2 changes: 1 addition & 1 deletion packages/node-core/src/utils/spotlight.ts
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
import { envToBool } from './envToBool';
import { envToBool } from '@sentry/core';

/**
* Parse the spotlight option with proper precedence:
Expand Down
Loading
, '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
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
2 changes: 1 addition & 1 deletion packages/cloudflare/src/handler.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -38,7 +38,7 @@ export function withSentry<
QueueHandlerMessage,
CfHostMetadata
>,
>(optionsCallback: (env: Env) => CloudflareOptions, handler: T): T {
>(optionsCallback: (env: Env) => CloudflareOptions | undefined, handler: T): T {

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note: I made this return undefined as it was also working before theoretically. I adapted the types to also allow undefined just to have this case also handled

setAsyncLocalStorageAsyncContextStrategy();

try {
Expand Down
22 changes: 20 additions & 2 deletions packages/cloudflare/src/options.ts
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
import { envToBool } from '@sentry/core';
import type { CloudflareOptions } from './client';

/**
Expand All@@ -17,6 +18,12 @@ function isVersionMetadata(value: unknown): value is CfVersionMetadata {
return typeof value === 'object' && value !== null && 'id' in value && typeof value.id === 'string';
}

function getEnvVar<T extends Record<string, unknown>>(env: unknown, varName: keyof T): string | undefined {
return typeof env === 'object' && env !== null && varName in env && typeof (env as T)[varName] === 'string'
? ((env as T)[varName] as string)
: undefined;
}

/**
* Merges the options passed in from the user with the options we read from
* the Cloudflare `env` environment variable object.
Expand All@@ -31,7 +38,7 @@ function isVersionMetadata(value: unknown): value is CfVersionMetadata {
*
* @returns The final options.
*/
export function getFinalOptions(userOptions: CloudflareOptions, env: unknown): CloudflareOptions {
export function getFinalOptions(userOptions: CloudflareOptions = {}, env: unknown): CloudflareOptions {
if (typeof env !== 'object' || env === null) {
return userOptions;
}
Expand All@@ -44,5 +51,16 @@ export function getFinalOptions(userOptions: CloudflareOptions, env: unknown): C
? env.CF_VERSION_METADATA.id
: undefined;

return { release, ...userOptions };
const tracesSampleRate =
userOptions.tracesSampleRate ?? parseFloat(getEnvVar(env, 'SENTRY_TRACES_SAMPLE_RATE') ?? '');

return {
release,
...userOptions,
dsn: userOptions.dsn ?? getEnvVar(env, 'SENTRY_DSN'),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

l: can we also add SENTRY_DEBUG? You could use envToBool for that from node-core, maybe lift that up into @sentry/core?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great idea. Added.

I also added tunnel to it

environment: userOptions.environment ?? getEnvVar(env, 'SENTRY_ENVIRONMENT'),
tracesSampleRate: isFinite(tracesSampleRate) ? tracesSampleRate : undefined,
debug: userOptions.debug ?? envToBool(getEnvVar(env, 'SENTRY_DEBUG')),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

envToBool(undefined) forces debug: false when unset

Medium Severity

When SENTRY_DEBUG is absent from env, getEnvVar returns undefined, and envToBool(undefined) returns false (since Boolean(undefined) is false in loose mode). This means debug is always explicitly set to false in the returned options, even when neither the user nor the env specified it. This breaks multiple existing toEqual assertions (e.g., lines 29, 120, 132) that don't expect a debug property.

Fix in CursorFix in Web

tunnel: userOptions.tunnel ?? getEnvVar(env, 'SENTRY_TUNNEL'),
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spread operator overrides env-derived fallback values

Medium Severity

The ...userOptions spread at the end of the return object overrides the env-derived fallback values for dsn, environment, and tracesSampleRate whenever those keys exist in userOptions — even if their values are undefined. This means if a user passes { dsn: undefined }, the SENTRY_DSN env fallback is silently ignored. The ?? fallback logic and the isFinite guard are both bypassed by the spread.

Fix in CursorFix in Web

}
25 changes: 15 additions & 10 deletions packages/cloudflare/test/handler.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -31,6 +31,11 @@ const MOCK_ENV = {
SENTRY_RELEASE: '1.1.1',
};

// Mock env without DSN for tests that should not initialize the SDK
const MOCK_ENV_WITHOUT_DSN = {

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note: It seems that the SDK was not initialized for the tests where this is used. By now also allowing SENTRY_DSN from the variables these tests fail. I'll create a follow up PR and remove this, so the SDK is actually initialized (I don't want to blow up this PR with other code changes)

SENTRY_RELEASE: '1.1.1',
};

function addDelayedWaitUntil(context: ExecutionContext) {
context.waitUntil(new Promise<void>(resolve => setTimeout(() => resolve())));
}
Expand DownExpand Up@@ -149,12 +154,12 @@ describe('withSentry', () => {
addDelayedWaitUntil(_context);
return new Response('test');
},
} satisfies ExportedHandler<typeof MOCK_ENV>;
} satisfies ExportedHandler<typeof MOCK_ENV_WITHOUT_DSN>;

const wrappedHandler = withSentry(vi.fn(), handler);
const waits: Promise<unknown>[] = [];
const waitUntil = vi.fn(promise => waits.push(promise));
await wrappedHandler.fetch?.(new Request('https://example.com'), MOCK_ENV, {
await wrappedHandler.fetch?.(new Request('https://example.com'), MOCK_ENV_WITHOUT_DSN, {
waitUntil,
} as unknown as ExecutionContext);
expect(flush).not.toBeCalled();
Expand DownExpand Up@@ -389,12 +394,12 @@ describe('withSentry', () => {
addDelayedWaitUntil(_context);
return;
},
} satisfies ExportedHandler<typeof MOCK_ENV>;
} satisfies ExportedHandler<typeof MOCK_ENV_WITHOUT_DSN>;

const wrappedHandler = withSentry(vi.fn(), handler);
const waits: Promise<unknown>[] = [];
const waitUntil = vi.fn(promise => waits.push(promise));
await wrappedHandler.scheduled?.(createMockScheduledController(), MOCK_ENV, {
await wrappedHandler.scheduled?.(createMockScheduledController(), MOCK_ENV_WITHOUT_DSN, {
waitUntil,
} as unknown as ExecutionContext);
expect(flush).not.toBeCalled();
Expand DownExpand Up@@ -628,12 +633,12 @@ describe('withSentry', () => {
addDelayedWaitUntil(_context);
return;
},
} satisfies ExportedHandler<typeof MOCK_ENV>;
} satisfies ExportedHandler<typeof MOCK_ENV_WITHOUT_DSN>;

const wrappedHandler = withSentry(vi.fn(), handler);
const waits: Promise<unknown>[] = [];
const waitUntil = vi.fn(promise => waits.push(promise));
await wrappedHandler.email?.(createMockEmailMessage(), MOCK_ENV, {
await wrappedHandler.email?.(createMockEmailMessage(), MOCK_ENV_WITHOUT_DSN, {
waitUntil,
} as unknown as ExecutionContext);
expect(flush).not.toBeCalled();
Expand DownExpand Up@@ -871,12 +876,12 @@ describe('withSentry', () => {
addDelayedWaitUntil(_context);
return;
},
} satisfies ExportedHandler<typeof MOCK_ENV>;
} satisfies ExportedHandler<typeof MOCK_ENV_WITHOUT_DSN>;

const wrappedHandler = withSentry(vi.fn(), handler);
const waits: Promise<unknown>[] = [];
const waitUntil = vi.fn(promise => waits.push(promise));
await wrappedHandler.queue?.(createMockQueueBatch(), MOCK_ENV, {
await wrappedHandler.queue?.(createMockQueueBatch(), MOCK_ENV_WITHOUT_DSN, {
waitUntil,
} as unknown as ExecutionContext);
expect(flush).not.toBeCalled();
Expand DownExpand Up@@ -1069,12 +1074,12 @@ describe('withSentry', () => {
addDelayedWaitUntil(_context);
return;
},
} satisfies ExportedHandler<typeof MOCK_ENV>;
} satisfies ExportedHandler<typeof MOCK_ENV_WITHOUT_DSN>;

const wrappedHandler = withSentry(vi.fn(), handler);
const waits: Promise<unknown>[] = [];
const waitUntil = vi.fn(promise => waits.push(promise));
await wrappedHandler.tail?.(createMockTailEvent(), MOCK_ENV, {
await wrappedHandler.tail?.(createMockTailEvent(), MOCK_ENV_WITHOUT_DSN, {
waitUntil,
} as unknown as ExecutionContext);
expect(flush).not.toBeCalled();
Expand Down
87 changes: 75 additions & 12 deletions packages/cloudflare/test/options.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -17,7 +17,7 @@ describe('getFinalOptions', () => {

const result = getFinalOptions(userOptions, env);

expect(result).toEqual(userOptions);
expect(result).toEqual(expect.objectContaining(userOptions));
});

it('merges options from env with user options', () => {
Expand All@@ -26,7 +26,7 @@ describe('getFinalOptions', () => {

const result = getFinalOptions(userOptions, env);

expect(result).toEqual({ dsn: 'test-dsn', release: 'user-release' });
expect(result).toEqual(expect.objectContaining({ dsn: 'test-dsn', release: 'user-release' }));
});

it('uses user options when SENTRY_RELEASE exists but is not a string', () => {
Expand All@@ -35,7 +35,7 @@ describe('getFinalOptions', () => {

const result = getFinalOptions(userOptions, env);

expect(result).toEqual(userOptions);
expect(result).toEqual(expect.objectContaining(userOptions));
});

it('uses user options when SENTRY_RELEASE does not exist', () => {
Expand All@@ -44,7 +44,7 @@ describe('getFinalOptions', () => {

const result = getFinalOptions(userOptions, env);

expect(result).toEqual(userOptions);
expect(result).toEqual(expect.objectContaining(userOptions));
});

it('takes user options over env options', () => {
Expand All@@ -53,7 +53,70 @@ describe('getFinalOptions', () => {

const result = getFinalOptions(userOptions, env);

expect(result).toEqual(userOptions);
expect(result).toEqual(expect.objectContaining(userOptions));
});

it('adds debug from env when user debug is undefined', () => {
const userOptions = { dsn: 'test-dsn' };
const env = { SENTRY_DEBUG: '1' };

const result = getFinalOptions(userOptions, env);

expect(result.debug).toBe(true);
});

it('uses SENTRY_DSN from env when user dsn is undefined', () => {
const userOptions = { release: 'user-release' };
const env = { SENTRY_DSN: 'https://key@ingest.sentry.io/1' };

const result = getFinalOptions(userOptions, env);

expect(result.dsn).toBe('https://key@ingest.sentry.io/1');
expect(result.release).toBe('user-release');
});

it('uses SENTRY_ENVIRONMENT from env when user environment is undefined', () => {
const userOptions = { dsn: 'test-dsn' };
const env = { SENTRY_ENVIRONMENT: 'staging' };

const result = getFinalOptions(userOptions, env);

expect(result.environment).toBe('staging');
});

it('uses SENTRY_TRACES_SAMPLE_RATE from env when user tracesSampleRate is undefined', () => {
const userOptions = { dsn: 'test-dsn' };
const env = { SENTRY_TRACES_SAMPLE_RATE: '0.5' };

const result = getFinalOptions(userOptions, env);

expect(result.tracesSampleRate).toBe(0.5);
});

it('does not use SENTRY_TRACES_SAMPLE_RATE from env when it is gibberish', () => {
const env = { SENTRY_TRACES_SAMPLE_RATE: 'ʕっ•ᴥ•ʔっ' };

const result = getFinalOptions(undefined, env);

expect(result.tracesSampleRate).toBeUndefined();
});

it('prefers user dsn over env SENTRY_DSN', () => {
const userOptions = { dsn: 'user-dsn', release: 'user-release' };
const env = { SENTRY_DSN: 'https://env@ingest.sentry.io/1' };

const result = getFinalOptions(userOptions, env);

expect(result.dsn).toBe('user-dsn');
});

it('ignores SENTRY_DSN when not a string', () => {
const userOptions = { dsn: undefined };
const env = { SENTRY_DSN: 123 };

const result = getFinalOptions(userOptions, env);

expect(result.dsn).toBeUndefined();
});

describe('CF_VERSION_METADATA', () => {
Expand All@@ -63,7 +126,7 @@ describe('getFinalOptions', () => {

const result = getFinalOptions(userOptions, env);

expect(result).toEqual({ dsn: 'test-dsn', release: 'version-123' });
expect(result).toEqual(expect.objectContaining({ dsn: 'test-dsn', release: 'version-123' }));
});

it('prefers SENTRY_RELEASE over CF_VERSION_METADATA.id', () => {
Expand All@@ -75,7 +138,7 @@ describe('getFinalOptions', () => {

const result = getFinalOptions(userOptions, env);

expect(result).toEqual({ dsn: 'test-dsn', release: 'env-release' });
expect(result).toEqual(expect.objectContaining({ dsn: 'test-dsn', release: 'env-release' }));
});

it('prefers user release over CF_VERSION_METADATA.id', () => {
Expand All@@ -84,7 +147,7 @@ describe('getFinalOptions', () => {

const result = getFinalOptions(userOptions, env);

expect(result).toEqual({ dsn: 'test-dsn', release: 'user-release' });
expect(result).toEqual(expect.objectContaining({ dsn: 'test-dsn', release: 'user-release' }));
});

it('prefers user release over both SENTRY_RELEASE and CF_VERSION_METADATA.id', () => {
Expand All@@ -96,7 +159,7 @@ describe('getFinalOptions', () => {

const result = getFinalOptions(userOptions, env);

expect(result).toEqual({ dsn: 'test-dsn', release: 'user-release' });
expect(result).toEqual(expect.objectContaining({ dsn: 'test-dsn', release: 'user-release' }));
});

it('ignores CF_VERSION_METADATA when it is not an object', () => {
Expand All@@ -105,7 +168,7 @@ describe('getFinalOptions', () => {

const result = getFinalOptions(userOptions, env);

expect(result).toEqual({ dsn: 'test-dsn', release: undefined });
expect(result).toEqual(expect.objectContaining({ dsn: 'test-dsn', release: undefined }));
});

it('ignores CF_VERSION_METADATA when id is not a string', () => {
Expand All@@ -114,7 +177,7 @@ describe('getFinalOptions', () => {

const result = getFinalOptions(userOptions, env);

expect(result).toEqual({ dsn: 'test-dsn', release: undefined });
expect(result).toEqual(expect.objectContaining({ dsn: 'test-dsn', release: undefined }));
});

it('ignores CF_VERSION_METADATA when id is missing', () => {
Expand All@@ -123,7 +186,7 @@ describe('getFinalOptions', () => {

const result = getFinalOptions(userOptions, env);

expect(result).toEqual({ dsn: 'test-dsn', release: undefined });
expect(result).toEqual(expect.objectContaining({ dsn: 'test-dsn', release: undefined }));
});
});
});
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -62,6 +62,7 @@ export {
_INTERNAL_shouldSkipAiProviderWrapping,
_INTERNAL_clearAiProviderSkips,
} from './utils/ai/providerSkip';
export { envToBool } from './utils/envToBool';
export { applyScopeDataToEvent, mergeScopeData, getCombinedScopeData } from './utils/scopeData';
export { prepareEvent } from './utils/prepareEvent';
export type { ExclusiveEventHintOrCaptureContext } from './utils/prepareEvent';
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest';
import { envToBool } from '../../src/utils/envToBool';
import { envToBool } from '../../../src/utils/envToBool';

describe('envToBool', () => {
it.each([
Expand Down
2 changes: 1 addition & 1 deletion packages/node-core/src/common-exports.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -31,7 +31,6 @@ export { getRequestUrl } from './utils/getRequestUrl';
export { initializeEsmLoader } from './sdk/esmLoader';
export { isCjs } from './utils/detection';
export { createMissingInstrumentationContext } from './utils/createMissingInstrumentationContext';
export { envToBool } from './utils/envToBool';
export { makeNodeTransport, type NodeTransportOptions } from './transports';
export type { HTTPModuleRequestIncomingMessage } from './transports/http-module';
export { cron } from './cron';
Expand DownExpand Up@@ -117,6 +116,7 @@ export {
wrapMcpServerWithSentry,
featureFlagsIntegration,
metrics,
envToBool,
} from '@sentry/core';

export type {
Expand Down
2 changes: 1 addition & 1 deletion packages/node-core/src/light/sdk.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,6 +4,7 @@ import {
consoleIntegration,
consoleSandbox,
debug,
envToBool,
eventFiltersIntegration,
functionToStringIntegration,
getCurrentScope,
Expand All@@ -29,7 +30,6 @@ import { defaultStackParser, getSentryRelease } from '../sdk/api';
import { makeNodeTransport } from '../transports';
import type { NodeClientOptions, NodeOptions } from '../types';
import { isCjs } from '../utils/detection';
import { envToBool } from '../utils/envToBool';
import { getSpotlightConfig } from '../utils/spotlight';
import { setAsyncLocalStorageAsyncContextStrategy } from './asyncLocalStorageStrategy';
import { LightNodeClient } from './client';
Expand Down
2 changes: 1 addition & 1 deletion packages/node-core/src/sdk/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,7 @@ import {
consoleSandbox,
conversationIdIntegration,
debug,
envToBool,
functionToStringIntegration,
getCurrentScope,
getIntegrationsToSetup,
Expand DownExpand Up@@ -38,7 +39,6 @@ import { systemErrorIntegration } from '../integrations/systemError';
import { makeNodeTransport } from '../transports';
import type { NodeClientOptions, NodeOptions } from '../types';
import { isCjs } from '../utils/detection';
import { envToBool } from '../utils/envToBool';
import { getSpotlightConfig } from '../utils/spotlight';
import { defaultStackParser, getSentryRelease } from './api';
import { NodeClient } from './client';
Expand Down
2 changes: 1 addition & 1 deletion packages/node-core/src/utils/spotlight.ts
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
import { envToBool } from './envToBool';
import { envToBool } from '@sentry/core';

/**
* Parse the spotlight option with proper precedence:
Expand Down
Loading
, '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
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
2 changes: 1 addition & 1 deletion packages/cloudflare/src/handler.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -38,7 +38,7 @@ export function withSentry<
QueueHandlerMessage,
CfHostMetadata
>,
>(optionsCallback: (env: Env) => CloudflareOptions, handler: T): T {
>(optionsCallback: (env: Env) => CloudflareOptions | undefined, handler: T): T {

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note: I made this return undefined as it was also working before theoretically. I adapted the types to also allow undefined just to have this case also handled

setAsyncLocalStorageAsyncContextStrategy();

try {
Expand Down
22 changes: 20 additions & 2 deletions packages/cloudflare/src/options.ts
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
import { envToBool } from '@sentry/core';
import type { CloudflareOptions } from './client';

/**
Expand All@@ -17,6 +18,12 @@ function isVersionMetadata(value: unknown): value is CfVersionMetadata {
return typeof value === 'object' && value !== null && 'id' in value && typeof value.id === 'string';
}

function getEnvVar<T extends Record<string, unknown>>(env: unknown, varName: keyof T): string | undefined {
return typeof env === 'object' && env !== null && varName in env && typeof (env as T)[varName] === 'string'
? ((env as T)[varName] as string)
: undefined;
}

/**
* Merges the options passed in from the user with the options we read from
* the Cloudflare `env` environment variable object.
Expand All@@ -31,7 +38,7 @@ function isVersionMetadata(value: unknown): value is CfVersionMetadata {
*
* @returns The final options.
*/
export function getFinalOptions(userOptions: CloudflareOptions, env: unknown): CloudflareOptions {
export function getFinalOptions(userOptions: CloudflareOptions = {}, env: unknown): CloudflareOptions {
if (typeof env !== 'object' || env === null) {
return userOptions;
}
Expand All@@ -44,5 +51,16 @@ export function getFinalOptions(userOptions: CloudflareOptions, env: unknown): C
? env.CF_VERSION_METADATA.id
: undefined;

return { release, ...userOptions };
const tracesSampleRate =
userOptions.tracesSampleRate ?? parseFloat(getEnvVar(env, 'SENTRY_TRACES_SAMPLE_RATE') ?? '');

return {
release,
...userOptions,
dsn: userOptions.dsn ?? getEnvVar(env, 'SENTRY_DSN'),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

l: can we also add SENTRY_DEBUG? You could use envToBool for that from node-core, maybe lift that up into @sentry/core?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great idea. Added.

I also added tunnel to it

environment: userOptions.environment ?? getEnvVar(env, 'SENTRY_ENVIRONMENT'),
tracesSampleRate: isFinite(tracesSampleRate) ? tracesSampleRate : undefined,
debug: userOptions.debug ?? envToBool(getEnvVar(env, 'SENTRY_DEBUG')),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

envToBool(undefined) forces debug: false when unset

Medium Severity

When SENTRY_DEBUG is absent from env, getEnvVar returns undefined, and envToBool(undefined) returns false (since Boolean(undefined) is false in loose mode). This means debug is always explicitly set to false in the returned options, even when neither the user nor the env specified it. This breaks multiple existing toEqual assertions (e.g., lines 29, 120, 132) that don't expect a debug property.

Fix in CursorFix in Web

tunnel: userOptions.tunnel ?? getEnvVar(env, 'SENTRY_TUNNEL'),
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spread operator overrides env-derived fallback values

Medium Severity

The ...userOptions spread at the end of the return object overrides the env-derived fallback values for dsn, environment, and tracesSampleRate whenever those keys exist in userOptions — even if their values are undefined. This means if a user passes { dsn: undefined }, the SENTRY_DSN env fallback is silently ignored. The ?? fallback logic and the isFinite guard are both bypassed by the spread.

Fix in CursorFix in Web

}
25 changes: 15 additions & 10 deletions packages/cloudflare/test/handler.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -31,6 +31,11 @@ const MOCK_ENV = {
SENTRY_RELEASE: '1.1.1',
};

// Mock env without DSN for tests that should not initialize the SDK
const MOCK_ENV_WITHOUT_DSN = {

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note: It seems that the SDK was not initialized for the tests where this is used. By now also allowing SENTRY_DSN from the variables these tests fail. I'll create a follow up PR and remove this, so the SDK is actually initialized (I don't want to blow up this PR with other code changes)

SENTRY_RELEASE: '1.1.1',
};

function addDelayedWaitUntil(context: ExecutionContext) {
context.waitUntil(new Promise<void>(resolve => setTimeout(() => resolve())));
}
Expand DownExpand Up@@ -149,12 +154,12 @@ describe('withSentry', () => {
addDelayedWaitUntil(_context);
return new Response('test');
},
} satisfies ExportedHandler<typeof MOCK_ENV>;
} satisfies ExportedHandler<typeof MOCK_ENV_WITHOUT_DSN>;

const wrappedHandler = withSentry(vi.fn(), handler);
const waits: Promise<unknown>[] = [];
const waitUntil = vi.fn(promise => waits.push(promise));
await wrappedHandler.fetch?.(new Request('https://example.com'), MOCK_ENV, {
await wrappedHandler.fetch?.(new Request('https://example.com'), MOCK_ENV_WITHOUT_DSN, {
waitUntil,
} as unknown as ExecutionContext);
expect(flush).not.toBeCalled();
Expand DownExpand Up@@ -389,12 +394,12 @@ describe('withSentry', () => {
addDelayedWaitUntil(_context);
return;
},
} satisfies ExportedHandler<typeof MOCK_ENV>;
} satisfies ExportedHandler<typeof MOCK_ENV_WITHOUT_DSN>;

const wrappedHandler = withSentry(vi.fn(), handler);
const waits: Promise<unknown>[] = [];
const waitUntil = vi.fn(promise => waits.push(promise));
await wrappedHandler.scheduled?.(createMockScheduledController(), MOCK_ENV, {
await wrappedHandler.scheduled?.(createMockScheduledController(), MOCK_ENV_WITHOUT_DSN, {
waitUntil,
} as unknown as ExecutionContext);
expect(flush).not.toBeCalled();
Expand DownExpand Up@@ -628,12 +633,12 @@ describe('withSentry', () => {
addDelayedWaitUntil(_context);
return;
},
} satisfies ExportedHandler<typeof MOCK_ENV>;
} satisfies ExportedHandler<typeof MOCK_ENV_WITHOUT_DSN>;

const wrappedHandler = withSentry(vi.fn(), handler);
const waits: Promise<unknown>[] = [];
const waitUntil = vi.fn(promise => waits.push(promise));
await wrappedHandler.email?.(createMockEmailMessage(), MOCK_ENV, {
await wrappedHandler.email?.(createMockEmailMessage(), MOCK_ENV_WITHOUT_DSN, {
waitUntil,
} as unknown as ExecutionContext);
expect(flush).not.toBeCalled();
Expand DownExpand Up@@ -871,12 +876,12 @@ describe('withSentry', () => {
addDelayedWaitUntil(_context);
return;
},
} satisfies ExportedHandler<typeof MOCK_ENV>;
} satisfies ExportedHandler<typeof MOCK_ENV_WITHOUT_DSN>;

const wrappedHandler = withSentry(vi.fn(), handler);
const waits: Promise<unknown>[] = [];
const waitUntil = vi.fn(promise => waits.push(promise));
await wrappedHandler.queue?.(createMockQueueBatch(), MOCK_ENV, {
await wrappedHandler.queue?.(createMockQueueBatch(), MOCK_ENV_WITHOUT_DSN, {
waitUntil,
} as unknown as ExecutionContext);
expect(flush).not.toBeCalled();
Expand DownExpand Up@@ -1069,12 +1074,12 @@ describe('withSentry', () => {
addDelayedWaitUntil(_context);
return;
},
} satisfies ExportedHandler<typeof MOCK_ENV>;
} satisfies ExportedHandler<typeof MOCK_ENV_WITHOUT_DSN>;

const wrappedHandler = withSentry(vi.fn(), handler);
const waits: Promise<unknown>[] = [];
const waitUntil = vi.fn(promise => waits.push(promise));
await wrappedHandler.tail?.(createMockTailEvent(), MOCK_ENV, {
await wrappedHandler.tail?.(createMockTailEvent(), MOCK_ENV_WITHOUT_DSN, {
waitUntil,
} as unknown as ExecutionContext);
expect(flush).not.toBeCalled();
Expand Down
87 changes: 75 additions & 12 deletions packages/cloudflare/test/options.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -17,7 +17,7 @@ describe('getFinalOptions', () => {

const result = getFinalOptions(userOptions, env);

expect(result).toEqual(userOptions);
expect(result).toEqual(expect.objectContaining(userOptions));
});

it('merges options from env with user options', () => {
Expand All@@ -26,7 +26,7 @@ describe('getFinalOptions', () => {

const result = getFinalOptions(userOptions, env);

expect(result).toEqual({ dsn: 'test-dsn', release: 'user-release' });
expect(result).toEqual(expect.objectContaining({ dsn: 'test-dsn', release: 'user-release' }));
});

it('uses user options when SENTRY_RELEASE exists but is not a string', () => {
Expand All@@ -35,7 +35,7 @@ describe('getFinalOptions', () => {

const result = getFinalOptions(userOptions, env);

expect(result).toEqual(userOptions);
expect(result).toEqual(expect.objectContaining(userOptions));
});

it('uses user options when SENTRY_RELEASE does not exist', () => {
Expand All@@ -44,7 +44,7 @@ describe('getFinalOptions', () => {

const result = getFinalOptions(userOptions, env);

expect(result).toEqual(userOptions);
expect(result).toEqual(expect.objectContaining(userOptions));
});

it('takes user options over env options', () => {
Expand All@@ -53,7 +53,70 @@ describe('getFinalOptions', () => {

const result = getFinalOptions(userOptions, env);

expect(result).toEqual(userOptions);
expect(result).toEqual(expect.objectContaining(userOptions));
});

it('adds debug from env when user debug is undefined', () => {
const userOptions = { dsn: 'test-dsn' };
const env = { SENTRY_DEBUG: '1' };

const result = getFinalOptions(userOptions, env);

expect(result.debug).toBe(true);
});

it('uses SENTRY_DSN from env when user dsn is undefined', () => {
const userOptions = { release: 'user-release' };
const env = { SENTRY_DSN: 'https://key@ingest.sentry.io/1' };

const result = getFinalOptions(userOptions, env);

expect(result.dsn).toBe('https://key@ingest.sentry.io/1');
expect(result.release).toBe('user-release');
});

it('uses SENTRY_ENVIRONMENT from env when user environment is undefined', () => {
const userOptions = { dsn: 'test-dsn' };
const env = { SENTRY_ENVIRONMENT: 'staging' };

const result = getFinalOptions(userOptions, env);

expect(result.environment).toBe('staging');
});

it('uses SENTRY_TRACES_SAMPLE_RATE from env when user tracesSampleRate is undefined', () => {
const userOptions = { dsn: 'test-dsn' };
const env = { SENTRY_TRACES_SAMPLE_RATE: '0.5' };

const result = getFinalOptions(userOptions, env);

expect(result.tracesSampleRate).toBe(0.5);
});

it('does not use SENTRY_TRACES_SAMPLE_RATE from env when it is gibberish', () => {
const env = { SENTRY_TRACES_SAMPLE_RATE: 'ʕっ•ᴥ•ʔっ' };

const result = getFinalOptions(undefined, env);

expect(result.tracesSampleRate).toBeUndefined();
});

it('prefers user dsn over env SENTRY_DSN', () => {
const userOptions = { dsn: 'user-dsn', release: 'user-release' };
const env = { SENTRY_DSN: 'https://env@ingest.sentry.io/1' };

const result = getFinalOptions(userOptions, env);

expect(result.dsn).toBe('user-dsn');
});

it('ignores SENTRY_DSN when not a string', () => {
const userOptions = { dsn: undefined };
const env = { SENTRY_DSN: 123 };

const result = getFinalOptions(userOptions, env);

expect(result.dsn).toBeUndefined();
});

describe('CF_VERSION_METADATA', () => {
Expand All@@ -63,7 +126,7 @@ describe('getFinalOptions', () => {

const result = getFinalOptions(userOptions, env);

expect(result).toEqual({ dsn: 'test-dsn', release: 'version-123' });
expect(result).toEqual(expect.objectContaining({ dsn: 'test-dsn', release: 'version-123' }));
});

it('prefers SENTRY_RELEASE over CF_VERSION_METADATA.id', () => {
Expand All@@ -75,7 +138,7 @@ describe('getFinalOptions', () => {

const result = getFinalOptions(userOptions, env);

expect(result).toEqual({ dsn: 'test-dsn', release: 'env-release' });
expect(result).toEqual(expect.objectContaining({ dsn: 'test-dsn', release: 'env-release' }));
});

it('prefers user release over CF_VERSION_METADATA.id', () => {
Expand All@@ -84,7 +147,7 @@ describe('getFinalOptions', () => {

const result = getFinalOptions(userOptions, env);

expect(result).toEqual({ dsn: 'test-dsn', release: 'user-release' });
expect(result).toEqual(expect.objectContaining({ dsn: 'test-dsn', release: 'user-release' }));
});

it('prefers user release over both SENTRY_RELEASE and CF_VERSION_METADATA.id', () => {
Expand All@@ -96,7 +159,7 @@ describe('getFinalOptions', () => {

const result = getFinalOptions(userOptions, env);

expect(result).toEqual({ dsn: 'test-dsn', release: 'user-release' });
expect(result).toEqual(expect.objectContaining({ dsn: 'test-dsn', release: 'user-release' }));
});

it('ignores CF_VERSION_METADATA when it is not an object', () => {
Expand All@@ -105,7 +168,7 @@ describe('getFinalOptions', () => {

const result = getFinalOptions(userOptions, env);

expect(result).toEqual({ dsn: 'test-dsn', release: undefined });
expect(result).toEqual(expect.objectContaining({ dsn: 'test-dsn', release: undefined }));
});

it('ignores CF_VERSION_METADATA when id is not a string', () => {
Expand All@@ -114,7 +177,7 @@ describe('getFinalOptions', () => {

const result = getFinalOptions(userOptions, env);

expect(result).toEqual({ dsn: 'test-dsn', release: undefined });
expect(result).toEqual(expect.objectContaining({ dsn: 'test-dsn', release: undefined }));
});

it('ignores CF_VERSION_METADATA when id is missing', () => {
Expand All@@ -123,7 +186,7 @@ describe('getFinalOptions', () => {

const result = getFinalOptions(userOptions, env);

expect(result).toEqual({ dsn: 'test-dsn', release: undefined });
expect(result).toEqual(expect.objectContaining({ dsn: 'test-dsn', release: undefined }));
});
});
});
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -62,6 +62,7 @@ export {
_INTERNAL_shouldSkipAiProviderWrapping,
_INTERNAL_clearAiProviderSkips,
} from './utils/ai/providerSkip';
export { envToBool } from './utils/envToBool';
export { applyScopeDataToEvent, mergeScopeData, getCombinedScopeData } from './utils/scopeData';
export { prepareEvent } from './utils/prepareEvent';
export type { ExclusiveEventHintOrCaptureContext } from './utils/prepareEvent';
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest';
import { envToBool } from '../../src/utils/envToBool';
import { envToBool } from '../../../src/utils/envToBool';

describe('envToBool', () => {
it.each([
Expand Down
2 changes: 1 addition & 1 deletion packages/node-core/src/common-exports.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -31,7 +31,6 @@ export { getRequestUrl } from './utils/getRequestUrl';
export { initializeEsmLoader } from './sdk/esmLoader';
export { isCjs } from './utils/detection';
export { createMissingInstrumentationContext } from './utils/createMissingInstrumentationContext';
export { envToBool } from './utils/envToBool';
export { makeNodeTransport, type NodeTransportOptions } from './transports';
export type { HTTPModuleRequestIncomingMessage } from './transports/http-module';
export { cron } from './cron';
Expand DownExpand Up@@ -117,6 +116,7 @@ export {
wrapMcpServerWithSentry,
featureFlagsIntegration,
metrics,
envToBool,
} from '@sentry/core';

export type {
Expand Down
2 changes: 1 addition & 1 deletion packages/node-core/src/light/sdk.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,6 +4,7 @@ import {
consoleIntegration,
consoleSandbox,
debug,
envToBool,
eventFiltersIntegration,
functionToStringIntegration,
getCurrentScope,
Expand All@@ -29,7 +30,6 @@ import { defaultStackParser, getSentryRelease } from '../sdk/api';
import { makeNodeTransport } from '../transports';
import type { NodeClientOptions, NodeOptions } from '../types';
import { isCjs } from '../utils/detection';
import { envToBool } from '../utils/envToBool';
import { getSpotlightConfig } from '../utils/spotlight';
import { setAsyncLocalStorageAsyncContextStrategy } from './asyncLocalStorageStrategy';
import { LightNodeClient } from './client';
Expand Down
2 changes: 1 addition & 1 deletion packages/node-core/src/sdk/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,7 @@ import {
consoleSandbox,
conversationIdIntegration,
debug,
envToBool,
functionToStringIntegration,
getCurrentScope,
getIntegrationsToSetup,
Expand DownExpand Up@@ -38,7 +39,6 @@ import { systemErrorIntegration } from '../integrations/systemError';
import { makeNodeTransport } from '../transports';
import type { NodeClientOptions, NodeOptions } from '../types';
import { isCjs } from '../utils/detection';
import { envToBool } from '../utils/envToBool';
import { getSpotlightConfig } from '../utils/spotlight';
import { defaultStackParser, getSentryRelease } from './api';
import { NodeClient } from './client';
Expand Down
2 changes: 1 addition & 1 deletion packages/node-core/src/utils/spotlight.ts
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
import { envToBool } from './envToBool';
import { envToBool } from '@sentry/core';

/**
* Parse the spotlight option with proper precedence:
Expand Down
Loading
, '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
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
2 changes: 1 addition & 1 deletion packages/cloudflare/src/handler.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -38,7 +38,7 @@ export function withSentry<
QueueHandlerMessage,
CfHostMetadata
>,
>(optionsCallback: (env: Env) => CloudflareOptions, handler: T): T {
>(optionsCallback: (env: Env) => CloudflareOptions | undefined, handler: T): T {

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note: I made this return undefined as it was also working before theoretically. I adapted the types to also allow undefined just to have this case also handled

setAsyncLocalStorageAsyncContextStrategy();

try {
Expand Down
22 changes: 20 additions & 2 deletions packages/cloudflare/src/options.ts
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
import { envToBool } from '@sentry/core';
import type { CloudflareOptions } from './client';

/**
Expand All@@ -17,6 +18,12 @@ function isVersionMetadata(value: unknown): value is CfVersionMetadata {
return typeof value === 'object' && value !== null && 'id' in value && typeof value.id === 'string';
}

function getEnvVar<T extends Record<string, unknown>>(env: unknown, varName: keyof T): string | undefined {
return typeof env === 'object' && env !== null && varName in env && typeof (env as T)[varName] === 'string'
? ((env as T)[varName] as string)
: undefined;
}

/**
* Merges the options passed in from the user with the options we read from
* the Cloudflare `env` environment variable object.
Expand All@@ -31,7 +38,7 @@ function isVersionMetadata(value: unknown): value is CfVersionMetadata {
*
* @returns The final options.
*/
export function getFinalOptions(userOptions: CloudflareOptions, env: unknown): CloudflareOptions {
export function getFinalOptions(userOptions: CloudflareOptions = {}, env: unknown): CloudflareOptions {
if (typeof env !== 'object' || env === null) {
return userOptions;
}
Expand All@@ -44,5 +51,16 @@ export function getFinalOptions(userOptions: CloudflareOptions, env: unknown): C
? env.CF_VERSION_METADATA.id
: undefined;

return { release, ...userOptions };
const tracesSampleRate =
userOptions.tracesSampleRate ?? parseFloat(getEnvVar(env, 'SENTRY_TRACES_SAMPLE_RATE') ?? '');

return {
release,
...userOptions,
dsn: userOptions.dsn ?? getEnvVar(env, 'SENTRY_DSN'),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

l: can we also add SENTRY_DEBUG? You could use envToBool for that from node-core, maybe lift that up into @sentry/core?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great idea. Added.

I also added tunnel to it

environment: userOptions.environment ?? getEnvVar(env, 'SENTRY_ENVIRONMENT'),
tracesSampleRate: isFinite(tracesSampleRate) ? tracesSampleRate : undefined,
debug: userOptions.debug ?? envToBool(getEnvVar(env, 'SENTRY_DEBUG')),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

envToBool(undefined) forces debug: false when unset

Medium Severity

When SENTRY_DEBUG is absent from env, getEnvVar returns undefined, and envToBool(undefined) returns false (since Boolean(undefined) is false in loose mode). This means debug is always explicitly set to false in the returned options, even when neither the user nor the env specified it. This breaks multiple existing toEqual assertions (e.g., lines 29, 120, 132) that don't expect a debug property.

Fix in CursorFix in Web

tunnel: userOptions.tunnel ?? getEnvVar(env, 'SENTRY_TUNNEL'),
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spread operator overrides env-derived fallback values

Medium Severity

The ...userOptions spread at the end of the return object overrides the env-derived fallback values for dsn, environment, and tracesSampleRate whenever those keys exist in userOptions — even if their values are undefined. This means if a user passes { dsn: undefined }, the SENTRY_DSN env fallback is silently ignored. The ?? fallback logic and the isFinite guard are both bypassed by the spread.

Fix in CursorFix in Web

}
25 changes: 15 additions & 10 deletions packages/cloudflare/test/handler.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -31,6 +31,11 @@ const MOCK_ENV = {
SENTRY_RELEASE: '1.1.1',
};

// Mock env without DSN for tests that should not initialize the SDK
const MOCK_ENV_WITHOUT_DSN = {

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note: It seems that the SDK was not initialized for the tests where this is used. By now also allowing SENTRY_DSN from the variables these tests fail. I'll create a follow up PR and remove this, so the SDK is actually initialized (I don't want to blow up this PR with other code changes)

SENTRY_RELEASE: '1.1.1',
};

function addDelayedWaitUntil(context: ExecutionContext) {
context.waitUntil(new Promise<void>(resolve => setTimeout(() => resolve())));
}
Expand DownExpand Up@@ -149,12 +154,12 @@ describe('withSentry', () => {
addDelayedWaitUntil(_context);
return new Response('test');
},
} satisfies ExportedHandler<typeof MOCK_ENV>;
} satisfies ExportedHandler<typeof MOCK_ENV_WITHOUT_DSN>;

const wrappedHandler = withSentry(vi.fn(), handler);
const waits: Promise<unknown>[] = [];
const waitUntil = vi.fn(promise => waits.push(promise));
await wrappedHandler.fetch?.(new Request('https://example.com'), MOCK_ENV, {
await wrappedHandler.fetch?.(new Request('https://example.com'), MOCK_ENV_WITHOUT_DSN, {
waitUntil,
} as unknown as ExecutionContext);
expect(flush).not.toBeCalled();
Expand DownExpand Up@@ -389,12 +394,12 @@ describe('withSentry', () => {
addDelayedWaitUntil(_context);
return;
},
} satisfies ExportedHandler<typeof MOCK_ENV>;
} satisfies ExportedHandler<typeof MOCK_ENV_WITHOUT_DSN>;

const wrappedHandler = withSentry(vi.fn(), handler);
const waits: Promise<unknown>[] = [];
const waitUntil = vi.fn(promise => waits.push(promise));
await wrappedHandler.scheduled?.(createMockScheduledController(), MOCK_ENV, {
await wrappedHandler.scheduled?.(createMockScheduledController(), MOCK_ENV_WITHOUT_DSN, {
waitUntil,
} as unknown as ExecutionContext);
expect(flush).not.toBeCalled();
Expand DownExpand Up@@ -628,12 +633,12 @@ describe('withSentry', () => {
addDelayedWaitUntil(_context);
return;
},
} satisfies ExportedHandler<typeof MOCK_ENV>;
} satisfies ExportedHandler<typeof MOCK_ENV_WITHOUT_DSN>;

const wrappedHandler = withSentry(vi.fn(), handler);
const waits: Promise<unknown>[] = [];
const waitUntil = vi.fn(promise => waits.push(promise));
await wrappedHandler.email?.(createMockEmailMessage(), MOCK_ENV, {
await wrappedHandler.email?.(createMockEmailMessage(), MOCK_ENV_WITHOUT_DSN, {
waitUntil,
} as unknown as ExecutionContext);
expect(flush).not.toBeCalled();
Expand DownExpand Up@@ -871,12 +876,12 @@ describe('withSentry', () => {
addDelayedWaitUntil(_context);
return;
},
} satisfies ExportedHandler<typeof MOCK_ENV>;
} satisfies ExportedHandler<typeof MOCK_ENV_WITHOUT_DSN>;

const wrappedHandler = withSentry(vi.fn(), handler);
const waits: Promise<unknown>[] = [];
const waitUntil = vi.fn(promise => waits.push(promise));
await wrappedHandler.queue?.(createMockQueueBatch(), MOCK_ENV, {
await wrappedHandler.queue?.(createMockQueueBatch(), MOCK_ENV_WITHOUT_DSN, {
waitUntil,
} as unknown as ExecutionContext);
expect(flush).not.toBeCalled();
Expand DownExpand Up@@ -1069,12 +1074,12 @@ describe('withSentry', () => {
addDelayedWaitUntil(_context);
return;
},
} satisfies ExportedHandler<typeof MOCK_ENV>;
} satisfies ExportedHandler<typeof MOCK_ENV_WITHOUT_DSN>;

const wrappedHandler = withSentry(vi.fn(), handler);
const waits: Promise<unknown>[] = [];
const waitUntil = vi.fn(promise => waits.push(promise));
await wrappedHandler.tail?.(createMockTailEvent(), MOCK_ENV, {
await wrappedHandler.tail?.(createMockTailEvent(), MOCK_ENV_WITHOUT_DSN, {
waitUntil,
} as unknown as ExecutionContext);
expect(flush).not.toBeCalled();
Expand Down
87 changes: 75 additions & 12 deletions packages/cloudflare/test/options.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -17,7 +17,7 @@ describe('getFinalOptions', () => {

const result = getFinalOptions(userOptions, env);

expect(result).toEqual(userOptions);
expect(result).toEqual(expect.objectContaining(userOptions));
});

it('merges options from env with user options', () => {
Expand All@@ -26,7 +26,7 @@ describe('getFinalOptions', () => {

const result = getFinalOptions(userOptions, env);

expect(result).toEqual({ dsn: 'test-dsn', release: 'user-release' });
expect(result).toEqual(expect.objectContaining({ dsn: 'test-dsn', release: 'user-release' }));
});

it('uses user options when SENTRY_RELEASE exists but is not a string', () => {
Expand All@@ -35,7 +35,7 @@ describe('getFinalOptions', () => {

const result = getFinalOptions(userOptions, env);

expect(result).toEqual(userOptions);
expect(result).toEqual(expect.objectContaining(userOptions));
});

it('uses user options when SENTRY_RELEASE does not exist', () => {
Expand All@@ -44,7 +44,7 @@ describe('getFinalOptions', () => {

const result = getFinalOptions(userOptions, env);

expect(result).toEqual(userOptions);
expect(result).toEqual(expect.objectContaining(userOptions));
});

it('takes user options over env options', () => {
Expand All@@ -53,7 +53,70 @@ describe('getFinalOptions', () => {

const result = getFinalOptions(userOptions, env);

expect(result).toEqual(userOptions);
expect(result).toEqual(expect.objectContaining(userOptions));
});

it('adds debug from env when user debug is undefined', () => {
const userOptions = { dsn: 'test-dsn' };
const env = { SENTRY_DEBUG: '1' };

const result = getFinalOptions(userOptions, env);

expect(result.debug).toBe(true);
});

it('uses SENTRY_DSN from env when user dsn is undefined', () => {
const userOptions = { release: 'user-release' };
const env = { SENTRY_DSN: 'https://key@ingest.sentry.io/1' };

const result = getFinalOptions(userOptions, env);

expect(result.dsn).toBe('https://key@ingest.sentry.io/1');
expect(result.release).toBe('user-release');
});

it('uses SENTRY_ENVIRONMENT from env when user environment is undefined', () => {
const userOptions = { dsn: 'test-dsn' };
const env = { SENTRY_ENVIRONMENT: 'staging' };

const result = getFinalOptions(userOptions, env);

expect(result.environment).toBe('staging');
});

it('uses SENTRY_TRACES_SAMPLE_RATE from env when user tracesSampleRate is undefined', () => {
const userOptions = { dsn: 'test-dsn' };
const env = { SENTRY_TRACES_SAMPLE_RATE: '0.5' };

const result = getFinalOptions(userOptions, env);

expect(result.tracesSampleRate).toBe(0.5);
});

it('does not use SENTRY_TRACES_SAMPLE_RATE from env when it is gibberish', () => {
const env = { SENTRY_TRACES_SAMPLE_RATE: 'ʕっ•ᴥ•ʔっ' };

const result = getFinalOptions(undefined, env);

expect(result.tracesSampleRate).toBeUndefined();
});

it('prefers user dsn over env SENTRY_DSN', () => {
const userOptions = { dsn: 'user-dsn', release: 'user-release' };
const env = { SENTRY_DSN: 'https://env@ingest.sentry.io/1' };

const result = getFinalOptions(userOptions, env);

expect(result.dsn).toBe('user-dsn');
});

it('ignores SENTRY_DSN when not a string', () => {
const userOptions = { dsn: undefined };
const env = { SENTRY_DSN: 123 };

const result = getFinalOptions(userOptions, env);

expect(result.dsn).toBeUndefined();
});

describe('CF_VERSION_METADATA', () => {
Expand All@@ -63,7 +126,7 @@ describe('getFinalOptions', () => {

const result = getFinalOptions(userOptions, env);

expect(result).toEqual({ dsn: 'test-dsn', release: 'version-123' });
expect(result).toEqual(expect.objectContaining({ dsn: 'test-dsn', release: 'version-123' }));
});

it('prefers SENTRY_RELEASE over CF_VERSION_METADATA.id', () => {
Expand All@@ -75,7 +138,7 @@ describe('getFinalOptions', () => {

const result = getFinalOptions(userOptions, env);

expect(result).toEqual({ dsn: 'test-dsn', release: 'env-release' });
expect(result).toEqual(expect.objectContaining({ dsn: 'test-dsn', release: 'env-release' }));
});

it('prefers user release over CF_VERSION_METADATA.id', () => {
Expand All@@ -84,7 +147,7 @@ describe('getFinalOptions', () => {

const result = getFinalOptions(userOptions, env);

expect(result).toEqual({ dsn: 'test-dsn', release: 'user-release' });
expect(result).toEqual(expect.objectContaining({ dsn: 'test-dsn', release: 'user-release' }));
});

it('prefers user release over both SENTRY_RELEASE and CF_VERSION_METADATA.id', () => {
Expand All@@ -96,7 +159,7 @@ describe('getFinalOptions', () => {

const result = getFinalOptions(userOptions, env);

expect(result).toEqual({ dsn: 'test-dsn', release: 'user-release' });
expect(result).toEqual(expect.objectContaining({ dsn: 'test-dsn', release: 'user-release' }));
});

it('ignores CF_VERSION_METADATA when it is not an object', () => {
Expand All@@ -105,7 +168,7 @@ describe('getFinalOptions', () => {

const result = getFinalOptions(userOptions, env);

expect(result).toEqual({ dsn: 'test-dsn', release: undefined });
expect(result).toEqual(expect.objectContaining({ dsn: 'test-dsn', release: undefined }));
});

it('ignores CF_VERSION_METADATA when id is not a string', () => {
Expand All@@ -114,7 +177,7 @@ describe('getFinalOptions', () => {

const result = getFinalOptions(userOptions, env);

expect(result).toEqual({ dsn: 'test-dsn', release: undefined });
expect(result).toEqual(expect.objectContaining({ dsn: 'test-dsn', release: undefined }));
});

it('ignores CF_VERSION_METADATA when id is missing', () => {
Expand All@@ -123,7 +186,7 @@ describe('getFinalOptions', () => {

const result = getFinalOptions(userOptions, env);

expect(result).toEqual({ dsn: 'test-dsn', release: undefined });
expect(result).toEqual(expect.objectContaining({ dsn: 'test-dsn', release: undefined }));
});
});
});
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -62,6 +62,7 @@ export {
_INTERNAL_shouldSkipAiProviderWrapping,
_INTERNAL_clearAiProviderSkips,
} from './utils/ai/providerSkip';
export { envToBool } from './utils/envToBool';
export { applyScopeDataToEvent, mergeScopeData, getCombinedScopeData } from './utils/scopeData';
export { prepareEvent } from './utils/prepareEvent';
export type { ExclusiveEventHintOrCaptureContext } from './utils/prepareEvent';
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest';
import { envToBool } from '../../src/utils/envToBool';
import { envToBool } from '../../../src/utils/envToBool';

describe('envToBool', () => {
it.each([
Expand Down
2 changes: 1 addition & 1 deletion packages/node-core/src/common-exports.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -31,7 +31,6 @@ export { getRequestUrl } from './utils/getRequestUrl';
export { initializeEsmLoader } from './sdk/esmLoader';
export { isCjs } from './utils/detection';
export { createMissingInstrumentationContext } from './utils/createMissingInstrumentationContext';
export { envToBool } from './utils/envToBool';
export { makeNodeTransport, type NodeTransportOptions } from './transports';
export type { HTTPModuleRequestIncomingMessage } from './transports/http-module';
export { cron } from './cron';
Expand DownExpand Up@@ -117,6 +116,7 @@ export {
wrapMcpServerWithSentry,
featureFlagsIntegration,
metrics,
envToBool,
} from '@sentry/core';

export type {
Expand Down
2 changes: 1 addition & 1 deletion packages/node-core/src/light/sdk.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,6 +4,7 @@ import {
consoleIntegration,
consoleSandbox,
debug,
envToBool,
eventFiltersIntegration,
functionToStringIntegration,
getCurrentScope,
Expand All@@ -29,7 +30,6 @@ import { defaultStackParser, getSentryRelease } from '../sdk/api';
import { makeNodeTransport } from '../transports';
import type { NodeClientOptions, NodeOptions } from '../types';
import { isCjs } from '../utils/detection';
import { envToBool } from '../utils/envToBool';
import { getSpotlightConfig } from '../utils/spotlight';
import { setAsyncLocalStorageAsyncContextStrategy } from './asyncLocalStorageStrategy';
import { LightNodeClient } from './client';
Expand Down
2 changes: 1 addition & 1 deletion packages/node-core/src/sdk/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,7 @@ import {
consoleSandbox,
conversationIdIntegration,
debug,
envToBool,
functionToStringIntegration,
getCurrentScope,
getIntegrationsToSetup,
Expand DownExpand Up@@ -38,7 +39,6 @@ import { systemErrorIntegration } from '../integrations/systemError';
import { makeNodeTransport } from '../transports';
import type { NodeClientOptions, NodeOptions } from '../types';
import { isCjs } from '../utils/detection';
import { envToBool } from '../utils/envToBool';
import { getSpotlightConfig } from '../utils/spotlight';
import { defaultStackParser, getSentryRelease } from './api';
import { NodeClient } from './client';
Expand Down
2 changes: 1 addition & 1 deletion packages/node-core/src/utils/spotlight.ts
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
import { envToBool } from './envToBool';
import { envToBool } from '@sentry/core';

/**
* Parse the spotlight option with proper precedence:
Expand Down
Loading
, '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
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
2 changes: 1 addition & 1 deletion packages/cloudflare/src/handler.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -38,7 +38,7 @@ export function withSentry<
QueueHandlerMessage,
CfHostMetadata
>,
>(optionsCallback: (env: Env) => CloudflareOptions, handler: T): T {
>(optionsCallback: (env: Env) => CloudflareOptions | undefined, handler: T): T {

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note: I made this return undefined as it was also working before theoretically. I adapted the types to also allow undefined just to have this case also handled

setAsyncLocalStorageAsyncContextStrategy();

try {
Expand Down
22 changes: 20 additions & 2 deletions packages/cloudflare/src/options.ts
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
import { envToBool } from '@sentry/core';
import type { CloudflareOptions } from './client';

/**
Expand All@@ -17,6 +18,12 @@ function isVersionMetadata(value: unknown): value is CfVersionMetadata {
return typeof value === 'object' && value !== null && 'id' in value && typeof value.id === 'string';
}

function getEnvVar<T extends Record<string, unknown>>(env: unknown, varName: keyof T): string | undefined {
return typeof env === 'object' && env !== null && varName in env && typeof (env as T)[varName] === 'string'
? ((env as T)[varName] as string)
: undefined;
}

/**
* Merges the options passed in from the user with the options we read from
* the Cloudflare `env` environment variable object.
Expand All@@ -31,7 +38,7 @@ function isVersionMetadata(value: unknown): value is CfVersionMetadata {
*
* @returns The final options.
*/
export function getFinalOptions(userOptions: CloudflareOptions, env: unknown): CloudflareOptions {
export function getFinalOptions(userOptions: CloudflareOptions = {}, env: unknown): CloudflareOptions {
if (typeof env !== 'object' || env === null) {
return userOptions;
}
Expand All@@ -44,5 +51,16 @@ export function getFinalOptions(userOptions: CloudflareOptions, env: unknown): C
? env.CF_VERSION_METADATA.id
: undefined;

return { release, ...userOptions };
const tracesSampleRate =
userOptions.tracesSampleRate ?? parseFloat(getEnvVar(env, 'SENTRY_TRACES_SAMPLE_RATE') ?? '');

return {
release,
...userOptions,
dsn: userOptions.dsn ?? getEnvVar(env, 'SENTRY_DSN'),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

l: can we also add SENTRY_DEBUG? You could use envToBool for that from node-core, maybe lift that up into @sentry/core?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great idea. Added.

I also added tunnel to it

environment: userOptions.environment ?? getEnvVar(env, 'SENTRY_ENVIRONMENT'),
tracesSampleRate: isFinite(tracesSampleRate) ? tracesSampleRate : undefined,
debug: userOptions.debug ?? envToBool(getEnvVar(env, 'SENTRY_DEBUG')),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

envToBool(undefined) forces debug: false when unset

Medium Severity

When SENTRY_DEBUG is absent from env, getEnvVar returns undefined, and envToBool(undefined) returns false (since Boolean(undefined) is false in loose mode). This means debug is always explicitly set to false in the returned options, even when neither the user nor the env specified it. This breaks multiple existing toEqual assertions (e.g., lines 29, 120, 132) that don't expect a debug property.

Fix in CursorFix in Web

tunnel: userOptions.tunnel ?? getEnvVar(env, 'SENTRY_TUNNEL'),
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spread operator overrides env-derived fallback values

Medium Severity

The ...userOptions spread at the end of the return object overrides the env-derived fallback values for dsn, environment, and tracesSampleRate whenever those keys exist in userOptions — even if their values are undefined. This means if a user passes { dsn: undefined }, the SENTRY_DSN env fallback is silently ignored. The ?? fallback logic and the isFinite guard are both bypassed by the spread.

Fix in CursorFix in Web

}
25 changes: 15 additions & 10 deletions packages/cloudflare/test/handler.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -31,6 +31,11 @@ const MOCK_ENV = {
SENTRY_RELEASE: '1.1.1',
};

// Mock env without DSN for tests that should not initialize the SDK
const MOCK_ENV_WITHOUT_DSN = {

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note: It seems that the SDK was not initialized for the tests where this is used. By now also allowing SENTRY_DSN from the variables these tests fail. I'll create a follow up PR and remove this, so the SDK is actually initialized (I don't want to blow up this PR with other code changes)

SENTRY_RELEASE: '1.1.1',
};

function addDelayedWaitUntil(context: ExecutionContext) {
context.waitUntil(new Promise<void>(resolve => setTimeout(() => resolve())));
}
Expand DownExpand Up@@ -149,12 +154,12 @@ describe('withSentry', () => {
addDelayedWaitUntil(_context);
return new Response('test');
},
} satisfies ExportedHandler<typeof MOCK_ENV>;
} satisfies ExportedHandler<typeof MOCK_ENV_WITHOUT_DSN>;

const wrappedHandler = withSentry(vi.fn(), handler);
const waits: Promise<unknown>[] = [];
const waitUntil = vi.fn(promise => waits.push(promise));
await wrappedHandler.fetch?.(new Request('https://example.com'), MOCK_ENV, {
await wrappedHandler.fetch?.(new Request('https://example.com'), MOCK_ENV_WITHOUT_DSN, {
waitUntil,
} as unknown as ExecutionContext);
expect(flush).not.toBeCalled();
Expand DownExpand Up@@ -389,12 +394,12 @@ describe('withSentry', () => {
addDelayedWaitUntil(_context);
return;
},
} satisfies ExportedHandler<typeof MOCK_ENV>;
} satisfies ExportedHandler<typeof MOCK_ENV_WITHOUT_DSN>;

const wrappedHandler = withSentry(vi.fn(), handler);
const waits: Promise<unknown>[] = [];
const waitUntil = vi.fn(promise => waits.push(promise));
await wrappedHandler.scheduled?.(createMockScheduledController(), MOCK_ENV, {
await wrappedHandler.scheduled?.(createMockScheduledController(), MOCK_ENV_WITHOUT_DSN, {
waitUntil,
} as unknown as ExecutionContext);
expect(flush).not.toBeCalled();
Expand DownExpand Up@@ -628,12 +633,12 @@ describe('withSentry', () => {
addDelayedWaitUntil(_context);
return;
},
} satisfies ExportedHandler<typeof MOCK_ENV>;
} satisfies ExportedHandler<typeof MOCK_ENV_WITHOUT_DSN>;

const wrappedHandler = withSentry(vi.fn(), handler);
const waits: Promise<unknown>[] = [];
const waitUntil = vi.fn(promise => waits.push(promise));
await wrappedHandler.email?.(createMockEmailMessage(), MOCK_ENV, {
await wrappedHandler.email?.(createMockEmailMessage(), MOCK_ENV_WITHOUT_DSN, {
waitUntil,
} as unknown as ExecutionContext);
expect(flush).not.toBeCalled();
Expand DownExpand Up@@ -871,12 +876,12 @@ describe('withSentry', () => {
addDelayedWaitUntil(_context);
return;
},
} satisfies ExportedHandler<typeof MOCK_ENV>;
} satisfies ExportedHandler<typeof MOCK_ENV_WITHOUT_DSN>;

const wrappedHandler = withSentry(vi.fn(), handler);
const waits: Promise<unknown>[] = [];
const waitUntil = vi.fn(promise => waits.push(promise));
await wrappedHandler.queue?.(createMockQueueBatch(), MOCK_ENV, {
await wrappedHandler.queue?.(createMockQueueBatch(), MOCK_ENV_WITHOUT_DSN, {
waitUntil,
} as unknown as ExecutionContext);
expect(flush).not.toBeCalled();
Expand DownExpand Up@@ -1069,12 +1074,12 @@ describe('withSentry', () => {
addDelayedWaitUntil(_context);
return;
},
} satisfies ExportedHandler<typeof MOCK_ENV>;
} satisfies ExportedHandler<typeof MOCK_ENV_WITHOUT_DSN>;

const wrappedHandler = withSentry(vi.fn(), handler);
const waits: Promise<unknown>[] = [];
const waitUntil = vi.fn(promise => waits.push(promise));
await wrappedHandler.tail?.(createMockTailEvent(), MOCK_ENV, {
await wrappedHandler.tail?.(createMockTailEvent(), MOCK_ENV_WITHOUT_DSN, {
waitUntil,
} as unknown as ExecutionContext);
expect(flush).not.toBeCalled();
Expand Down
87 changes: 75 additions & 12 deletions packages/cloudflare/test/options.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -17,7 +17,7 @@ describe('getFinalOptions', () => {

const result = getFinalOptions(userOptions, env);

expect(result).toEqual(userOptions);
expect(result).toEqual(expect.objectContaining(userOptions));
});

it('merges options from env with user options', () => {
Expand All@@ -26,7 +26,7 @@ describe('getFinalOptions', () => {

const result = getFinalOptions(userOptions, env);

expect(result).toEqual({ dsn: 'test-dsn', release: 'user-release' });
expect(result).toEqual(expect.objectContaining({ dsn: 'test-dsn', release: 'user-release' }));
});

it('uses user options when SENTRY_RELEASE exists but is not a string', () => {
Expand All@@ -35,7 +35,7 @@ describe('getFinalOptions', () => {

const result = getFinalOptions(userOptions, env);

expect(result).toEqual(userOptions);
expect(result).toEqual(expect.objectContaining(userOptions));
});

it('uses user options when SENTRY_RELEASE does not exist', () => {
Expand All@@ -44,7 +44,7 @@ describe('getFinalOptions', () => {

const result = getFinalOptions(userOptions, env);

expect(result).toEqual(userOptions);
expect(result).toEqual(expect.objectContaining(userOptions));
});

it('takes user options over env options', () => {
Expand All@@ -53,7 +53,70 @@ describe('getFinalOptions', () => {

const result = getFinalOptions(userOptions, env);

expect(result).toEqual(userOptions);
expect(result).toEqual(expect.objectContaining(userOptions));
});

it('adds debug from env when user debug is undefined', () => {
const userOptions = { dsn: 'test-dsn' };
const env = { SENTRY_DEBUG: '1' };

const result = getFinalOptions(userOptions, env);

expect(result.debug).toBe(true);
});

it('uses SENTRY_DSN from env when user dsn is undefined', () => {
const userOptions = { release: 'user-release' };
const env = { SENTRY_DSN: 'https://key@ingest.sentry.io/1' };

const result = getFinalOptions(userOptions, env);

expect(result.dsn).toBe('https://key@ingest.sentry.io/1');
expect(result.release).toBe('user-release');
});

it('uses SENTRY_ENVIRONMENT from env when user environment is undefined', () => {
const userOptions = { dsn: 'test-dsn' };
const env = { SENTRY_ENVIRONMENT: 'staging' };

const result = getFinalOptions(userOptions, env);

expect(result.environment).toBe('staging');
});

it('uses SENTRY_TRACES_SAMPLE_RATE from env when user tracesSampleRate is undefined', () => {
const userOptions = { dsn: 'test-dsn' };
const env = { SENTRY_TRACES_SAMPLE_RATE: '0.5' };

const result = getFinalOptions(userOptions, env);

expect(result.tracesSampleRate).toBe(0.5);
});

it('does not use SENTRY_TRACES_SAMPLE_RATE from env when it is gibberish', () => {
const env = { SENTRY_TRACES_SAMPLE_RATE: 'ʕっ•ᴥ•ʔっ' };

const result = getFinalOptions(undefined, env);

expect(result.tracesSampleRate).toBeUndefined();
});

it('prefers user dsn over env SENTRY_DSN', () => {
const userOptions = { dsn: 'user-dsn', release: 'user-release' };
const env = { SENTRY_DSN: 'https://env@ingest.sentry.io/1' };

const result = getFinalOptions(userOptions, env);

expect(result.dsn).toBe('user-dsn');
});

it('ignores SENTRY_DSN when not a string', () => {
const userOptions = { dsn: undefined };
const env = { SENTRY_DSN: 123 };

const result = getFinalOptions(userOptions, env);

expect(result.dsn).toBeUndefined();
});

describe('CF_VERSION_METADATA', () => {
Expand All@@ -63,7 +126,7 @@ describe('getFinalOptions', () => {

const result = getFinalOptions(userOptions, env);

expect(result).toEqual({ dsn: 'test-dsn', release: 'version-123' });
expect(result).toEqual(expect.objectContaining({ dsn: 'test-dsn', release: 'version-123' }));
});

it('prefers SENTRY_RELEASE over CF_VERSION_METADATA.id', () => {
Expand All@@ -75,7 +138,7 @@ describe('getFinalOptions', () => {

const result = getFinalOptions(userOptions, env);

expect(result).toEqual({ dsn: 'test-dsn', release: 'env-release' });
expect(result).toEqual(expect.objectContaining({ dsn: 'test-dsn', release: 'env-release' }));
});

it('prefers user release over CF_VERSION_METADATA.id', () => {
Expand All@@ -84,7 +147,7 @@ describe('getFinalOptions', () => {

const result = getFinalOptions(userOptions, env);

expect(result).toEqual({ dsn: 'test-dsn', release: 'user-release' });
expect(result).toEqual(expect.objectContaining({ dsn: 'test-dsn', release: 'user-release' }));
});

it('prefers user release over both SENTRY_RELEASE and CF_VERSION_METADATA.id', () => {
Expand All@@ -96,7 +159,7 @@ describe('getFinalOptions', () => {

const result = getFinalOptions(userOptions, env);

expect(result).toEqual({ dsn: 'test-dsn', release: 'user-release' });
expect(result).toEqual(expect.objectContaining({ dsn: 'test-dsn', release: 'user-release' }));
});

it('ignores CF_VERSION_METADATA when it is not an object', () => {
Expand All@@ -105,7 +168,7 @@ describe('getFinalOptions', () => {

const result = getFinalOptions(userOptions, env);

expect(result).toEqual({ dsn: 'test-dsn', release: undefined });
expect(result).toEqual(expect.objectContaining({ dsn: 'test-dsn', release: undefined }));
});

it('ignores CF_VERSION_METADATA when id is not a string', () => {
Expand All@@ -114,7 +177,7 @@ describe('getFinalOptions', () => {

const result = getFinalOptions(userOptions, env);

expect(result).toEqual({ dsn: 'test-dsn', release: undefined });
expect(result).toEqual(expect.objectContaining({ dsn: 'test-dsn', release: undefined }));
});

it('ignores CF_VERSION_METADATA when id is missing', () => {
Expand All@@ -123,7 +186,7 @@ describe('getFinalOptions', () => {

const result = getFinalOptions(userOptions, env);

expect(result).toEqual({ dsn: 'test-dsn', release: undefined });
expect(result).toEqual(expect.objectContaining({ dsn: 'test-dsn', release: undefined }));
});
});
});
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -62,6 +62,7 @@ export {
_INTERNAL_shouldSkipAiProviderWrapping,
_INTERNAL_clearAiProviderSkips,
} from './utils/ai/providerSkip';
export { envToBool } from './utils/envToBool';
export { applyScopeDataToEvent, mergeScopeData, getCombinedScopeData } from './utils/scopeData';
export { prepareEvent } from './utils/prepareEvent';
export type { ExclusiveEventHintOrCaptureContext } from './utils/prepareEvent';
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest';
import { envToBool } from '../../src/utils/envToBool';
import { envToBool } from '../../../src/utils/envToBool';

describe('envToBool', () => {
it.each([
Expand Down
2 changes: 1 addition & 1 deletion packages/node-core/src/common-exports.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -31,7 +31,6 @@ export { getRequestUrl } from './utils/getRequestUrl';
export { initializeEsmLoader } from './sdk/esmLoader';
export { isCjs } from './utils/detection';
export { createMissingInstrumentationContext } from './utils/createMissingInstrumentationContext';
export { envToBool } from './utils/envToBool';
export { makeNodeTransport, type NodeTransportOptions } from './transports';
export type { HTTPModuleRequestIncomingMessage } from './transports/http-module';
export { cron } from './cron';
Expand DownExpand Up@@ -117,6 +116,7 @@ export {
wrapMcpServerWithSentry,
featureFlagsIntegration,
metrics,
envToBool,
} from '@sentry/core';

export type {
Expand Down
2 changes: 1 addition & 1 deletion packages/node-core/src/light/sdk.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,6 +4,7 @@ import {
consoleIntegration,
consoleSandbox,
debug,
envToBool,
eventFiltersIntegration,
functionToStringIntegration,
getCurrentScope,
Expand All@@ -29,7 +30,6 @@ import { defaultStackParser, getSentryRelease } from '../sdk/api';
import { makeNodeTransport } from '../transports';
import type { NodeClientOptions, NodeOptions } from '../types';
import { isCjs } from '../utils/detection';
import { envToBool } from '../utils/envToBool';
import { getSpotlightConfig } from '../utils/spotlight';
import { setAsyncLocalStorageAsyncContextStrategy } from './asyncLocalStorageStrategy';
import { LightNodeClient } from './client';
Expand Down
2 changes: 1 addition & 1 deletion packages/node-core/src/sdk/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,7 @@ import {
consoleSandbox,
conversationIdIntegration,
debug,
envToBool,
functionToStringIntegration,
getCurrentScope,
getIntegrationsToSetup,
Expand DownExpand Up@@ -38,7 +39,6 @@ import { systemErrorIntegration } from '../integrations/systemError';
import { makeNodeTransport } from '../transports';
import type { NodeClientOptions, NodeOptions } from '../types';
import { isCjs } from '../utils/detection';
import { envToBool } from '../utils/envToBool';
import { getSpotlightConfig } from '../utils/spotlight';
import { defaultStackParser, getSentryRelease } from './api';
import { NodeClient } from './client';
Expand Down
2 changes: 1 addition & 1 deletion packages/node-core/src/utils/spotlight.ts
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
import { envToBool } from './envToBool';
import { envToBool } from '@sentry/core';

/**
* Parse the spotlight option with proper precedence:
Expand Down
Loading