diff --git a/packages/cloudflare/src/handler.ts b/packages/cloudflare/src/handler.ts index f7e8353906ec..9d2e2728af82 100644 --- a/packages/cloudflare/src/handler.ts +++ b/packages/cloudflare/src/handler.ts @@ -38,7 +38,7 @@ export function withSentry< QueueHandlerMessage, CfHostMetadata >, ->(optionsCallback: (env: Env) => CloudflareOptions, handler: T): T { +>(optionsCallback: (env: Env) => CloudflareOptions | undefined, handler: T): T { setAsyncLocalStorageAsyncContextStrategy(); try { diff --git a/packages/cloudflare/src/options.ts b/packages/cloudflare/src/options.ts index 3c62f88f25ed..b4808ed7c125 100644 --- a/packages/cloudflare/src/options.ts +++ b/packages/cloudflare/src/options.ts @@ -1,3 +1,4 @@ +import { envToBool } from '@sentry/core'; import type { CloudflareOptions } from './client'; /** @@ -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>(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. @@ -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; } @@ -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'), + environment: userOptions.environment ?? getEnvVar(env, 'SENTRY_ENVIRONMENT'), + tracesSampleRate: isFinite(tracesSampleRate) ? tracesSampleRate : undefined, + debug: userOptions.debug ?? envToBool(getEnvVar(env, 'SENTRY_DEBUG')), + tunnel: userOptions.tunnel ?? getEnvVar(env, 'SENTRY_TUNNEL'), + }; } diff --git a/packages/cloudflare/test/handler.test.ts b/packages/cloudflare/test/handler.test.ts index 24ceeeded151..52ed02d07ee1 100644 --- a/packages/cloudflare/test/handler.test.ts +++ b/packages/cloudflare/test/handler.test.ts @@ -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 = { + SENTRY_RELEASE: '1.1.1', +}; + function addDelayedWaitUntil(context: ExecutionContext) { context.waitUntil(new Promise(resolve => setTimeout(() => resolve()))); } @@ -149,12 +154,12 @@ describe('withSentry', () => { addDelayedWaitUntil(_context); return new Response('test'); }, - } satisfies ExportedHandler; + } satisfies ExportedHandler; const wrappedHandler = withSentry(vi.fn(), handler); const waits: Promise[] = []; 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(); @@ -389,12 +394,12 @@ describe('withSentry', () => { addDelayedWaitUntil(_context); return; }, - } satisfies ExportedHandler; + } satisfies ExportedHandler; const wrappedHandler = withSentry(vi.fn(), handler); const waits: Promise[] = []; 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(); @@ -628,12 +633,12 @@ describe('withSentry', () => { addDelayedWaitUntil(_context); return; }, - } satisfies ExportedHandler; + } satisfies ExportedHandler; const wrappedHandler = withSentry(vi.fn(), handler); const waits: Promise[] = []; 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(); @@ -871,12 +876,12 @@ describe('withSentry', () => { addDelayedWaitUntil(_context); return; }, - } satisfies ExportedHandler; + } satisfies ExportedHandler; const wrappedHandler = withSentry(vi.fn(), handler); const waits: Promise[] = []; 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(); @@ -1069,12 +1074,12 @@ describe('withSentry', () => { addDelayedWaitUntil(_context); return; }, - } satisfies ExportedHandler; + } satisfies ExportedHandler; const wrappedHandler = withSentry(vi.fn(), handler); const waits: Promise[] = []; 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(); diff --git a/packages/cloudflare/test/options.test.ts b/packages/cloudflare/test/options.test.ts index 6efcf18688c0..9dc21606b445 100644 --- a/packages/cloudflare/test/options.test.ts +++ b/packages/cloudflare/test/options.test.ts @@ -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', () => { @@ -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', () => { @@ -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', () => { @@ -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', () => { @@ -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', () => { @@ -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', () => { @@ -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', () => { @@ -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', () => { @@ -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', () => { @@ -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', () => { @@ -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', () => { @@ -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 })); }); }); }); diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 30ace1803b1a..91d1317ded6e 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -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'; diff --git a/packages/node-core/src/utils/envToBool.ts b/packages/core/src/utils/envToBool.ts similarity index 100% rename from packages/node-core/src/utils/envToBool.ts rename to packages/core/src/utils/envToBool.ts diff --git a/packages/node-core/test/utils/envToBool.test.ts b/packages/core/test/lib/utils/envToBool.test.ts similarity index 96% rename from packages/node-core/test/utils/envToBool.test.ts rename to packages/core/test/lib/utils/envToBool.test.ts index aa3c73fe1e8f..08a273b1754e 100644 --- a/packages/node-core/test/utils/envToBool.test.ts +++ b/packages/core/test/lib/utils/envToBool.test.ts @@ -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([ diff --git a/packages/node-core/src/common-exports.ts b/packages/node-core/src/common-exports.ts index 5fd9df08c808..3fff4100b352 100644 --- a/packages/node-core/src/common-exports.ts +++ b/packages/node-core/src/common-exports.ts @@ -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'; @@ -117,6 +116,7 @@ export { wrapMcpServerWithSentry, featureFlagsIntegration, metrics, + envToBool, } from '@sentry/core'; export type { diff --git a/packages/node-core/src/light/sdk.ts b/packages/node-core/src/light/sdk.ts index acbea4649ceb..6f0e453a21d7 100644 --- a/packages/node-core/src/light/sdk.ts +++ b/packages/node-core/src/light/sdk.ts @@ -4,6 +4,7 @@ import { consoleIntegration, consoleSandbox, debug, + envToBool, eventFiltersIntegration, functionToStringIntegration, getCurrentScope, @@ -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'; diff --git a/packages/node-core/src/sdk/index.ts b/packages/node-core/src/sdk/index.ts index 22dd7b38d657..8b5fead854f1 100644 --- a/packages/node-core/src/sdk/index.ts +++ b/packages/node-core/src/sdk/index.ts @@ -5,6 +5,7 @@ import { consoleSandbox, conversationIdIntegration, debug, + envToBool, functionToStringIntegration, getCurrentScope, getIntegrationsToSetup, @@ -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'; diff --git a/packages/node-core/src/utils/spotlight.ts b/packages/node-core/src/utils/spotlight.ts index 1aa01e57b4e7..2311d2195f24 100644 --- a/packages/node-core/src/utils/spotlight.ts +++ b/packages/node-core/src/utils/spotlight.ts @@ -1,4 +1,4 @@ -import { envToBool } from './envToBool'; +import { envToBool } from '@sentry/core'; /** * Parse the spotlight option with proper precedence: