From fd592c682ceb3db74b6b0d65bab1383555358101 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Mon, 5 May 2025 18:13:11 +0200 Subject: [PATCH 1/5] feat(cloudflare): Read `SENTRY_RELEASE` from `env` --- packages/cloudflare/src/handler.ts | 20 ++++- packages/cloudflare/test/handler.test.ts | 109 +++++++++++++++++++++++ 2 files changed, 127 insertions(+), 2 deletions(-) diff --git a/packages/cloudflare/src/handler.ts b/packages/cloudflare/src/handler.ts index f2bb059ee071..d1aa47f30fb1 100644 --- a/packages/cloudflare/src/handler.ts +++ b/packages/cloudflare/src/handler.ts @@ -35,7 +35,10 @@ export function withSentry>) { const [request, env, context] = args; - const options = optionsCallback(env); + const callbackOptions = optionsCallback(env); + + const options = { ...getOptionsFromEnv(env), ...callbackOptions }; + return wrapRequestHandler({ options, request, context }, () => target.apply(thisArg, args)); }, }); @@ -48,7 +51,10 @@ export function withSentry>) { const [event, env, context] = args; return withIsolationScope(isolationScope => { - const options = optionsCallback(env); + const callbackOptions = optionsCallback(env); + + const options = { ...getOptionsFromEnv(env), ...callbackOptions }; + const client = init(options); isolationScope.setClient(client); @@ -91,3 +97,13 @@ export function withSentry { @@ -51,6 +52,65 @@ describe('withSentry', () => { expect(result).toBe(response); }); + + test('merges options from env and callback', async () => { + const handler = { + fetch(_request, _env, _context) { + throw new Error('test'); + }, + } satisfies ExportedHandler; + + let sentryEvent: Event = {}; + + const wrappedHandler = withSentry( + env => ({ + dsn: env.SENTRY_DSN, + beforeSend(event) { + sentryEvent = event; + return null; + }, + }), + handler, + ); + + try { + await wrappedHandler.fetch(new Request('https://example.com'), MOCK_ENV, createMockExecutionContext()); + } catch { + // ignore + } + + expect(sentryEvent.release).toEqual('1.1.1'); + }); + + test('callback options take precedence over env options', async () => { + const handler = { + fetch(_request, _env, _context) { + throw new Error('test'); + }, + } satisfies ExportedHandler; + + let sentryEvent: Event = {}; + + const wrappedHandler = withSentry( + env => ({ + dsn: env.SENTRY_DSN, + release: '2.0.0', + beforeSend(event) { + sentryEvent = event; + return null; + }, + }), + handler, + ); + + try { + await wrappedHandler.fetch(new Request('https://example.com'), MOCK_ENV, createMockExecutionContext()); + } catch { + // ignore + } + + expect(sentryEvent.release).toEqual('2.0.0'); + }); }); describe('scheduled handler', () => { @@ -70,6 +130,55 @@ describe('withSentry', () => { expect(optionsCallback).toHaveBeenLastCalledWith(MOCK_ENV); }); + test('merges options from env and callback', async () => { + const handler = { + scheduled(_controller, _env, _context) { + SentryCore.captureMessage('cloud_resource'); + return; + }, + } satisfies ExportedHandler; + + let sentryEvent: Event = {}; + const wrappedHandler = withSentry( + env => ({ + dsn: env.SENTRY_DSN, + beforeSend(event) { + sentryEvent = event; + return null; + }, + }), + handler, + ); + await wrappedHandler.scheduled(createMockScheduledController(), MOCK_ENV, createMockExecutionContext()); + + expect(sentryEvent.release).toBe('1.1.1'); + }); + + test('callback options take precedence over env options', async () => { + const handler = { + scheduled(_controller, _env, _context) { + SentryCore.captureMessage('cloud_resource'); + return; + }, + } satisfies ExportedHandler; + + let sentryEvent: Event = {}; + const wrappedHandler = withSentry( + env => ({ + dsn: env.SENTRY_DSN, + release: '2.0.0', + beforeSend(event) { + sentryEvent = event; + return null; + }, + }), + handler, + ); + await wrappedHandler.scheduled(createMockScheduledController(), MOCK_ENV, createMockExecutionContext()); + + expect(sentryEvent.release).toEqual('2.0.0'); + }); + test('flushes the event after the handler is done using the cloudflare context.waitUntil', async () => { const handler = { scheduled(_controller, _env, _context) { From 49f8f769760087e66b72dcd95c1aad33540e94f2 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Tue, 6 May 2025 11:43:42 +0200 Subject: [PATCH 2/5] ref and extract options merging helper, add to durable objects --- packages/cloudflare/src/durableobject.ts | 3 +- packages/cloudflare/src/handler.ts | 18 ++------ packages/cloudflare/src/options.ts | 23 ++++++++++ packages/cloudflare/test/options.test.ts | 58 ++++++++++++++++++++++++ packages/cloudflare/tsconfig.json | 2 +- 5 files changed, 87 insertions(+), 17 deletions(-) create mode 100644 packages/cloudflare/src/options.ts create mode 100644 packages/cloudflare/test/options.test.ts diff --git a/packages/cloudflare/src/durableobject.ts b/packages/cloudflare/src/durableobject.ts index ba00778bded5..aeddfed4dd91 100644 --- a/packages/cloudflare/src/durableobject.ts +++ b/packages/cloudflare/src/durableobject.ts @@ -15,6 +15,7 @@ import type { CloudflareOptions } from './client'; import { isInstrumented, markAsInstrumented } from './instrument'; import { wrapRequestHandler } from './request'; import { init } from './sdk'; +import { getFinalOptions } from './options'; type MethodWrapperOptions = { spanName?: string; @@ -140,7 +141,7 @@ export function instrumentDurableObjectWithSentry> construct(target, [context, env]) { setAsyncLocalStorageAsyncContextStrategy(); - const options = optionsCallback(env); + const options = getFinalOptions(optionsCallback(env), env); const obj = new target(context, env); diff --git a/packages/cloudflare/src/handler.ts b/packages/cloudflare/src/handler.ts index d1aa47f30fb1..7e1667d6dc56 100644 --- a/packages/cloudflare/src/handler.ts +++ b/packages/cloudflare/src/handler.ts @@ -9,6 +9,7 @@ import { import { setAsyncLocalStorageAsyncContextStrategy } from './async'; import type { CloudflareOptions } from './client'; import { isInstrumented, markAsInstrumented } from './instrument'; +import { getFinalOptions } from './options'; import { wrapRequestHandler } from './request'; import { addCloudResourceContext } from './scope-utils'; import { init } from './sdk'; @@ -35,9 +36,8 @@ export function withSentry>) { const [request, env, context] = args; - const callbackOptions = optionsCallback(env); - const options = { ...getOptionsFromEnv(env), ...callbackOptions }; + const options = getFinalOptions(optionsCallback(env), env); return wrapRequestHandler({ options, request, context }, () => target.apply(thisArg, args)); }, @@ -51,9 +51,7 @@ export function withSentry>) { const [event, env, context] = args; return withIsolationScope(isolationScope => { - const callbackOptions = optionsCallback(env); - - const options = { ...getOptionsFromEnv(env), ...callbackOptions }; + const options = getFinalOptions(optionsCallback(env), env); const client = init(options); isolationScope.setClient(client); @@ -97,13 +95,3 @@ export function withSentry { + it('returns user options when env is not an object', () => { + const userOptions = { dsn: 'test-dsn', release: 'test-release' }; + const env = 'not-an-object'; + + const result = getFinalOptions(userOptions, env); + + expect(result).toEqual(userOptions); + }); + + it('returns user options when env is null', () => { + const userOptions = { dsn: 'test-dsn', release: 'test-release' }; + const env = null; + + const result = getFinalOptions(userOptions, env); + + expect(result).toEqual(userOptions); + }); + + it('merges options from env with user options', () => { + const userOptions = { dsn: 'test-dsn', release: 'user-release' }; + const env = { SENTRY_RELEASE: 'env-release' }; + + const result = getFinalOptions(userOptions, env); + + expect(result).toEqual({ dsn: 'test-dsn', release: 'user-release' }); + }); + + it('uses user options when SENTRY_RELEASE exists but is not a string', () => { + const userOptions = { dsn: 'test-dsn', release: 'user-release' }; + const env = { SENTRY_RELEASE: 123 }; + + const result = getFinalOptions(userOptions, env); + + expect(result).toEqual(userOptions); + }); + + it('uses user options when SENTRY_RELEASE does not exist', () => { + const userOptions = { dsn: 'test-dsn', release: 'user-release' }; + const env = { OTHER_VAR: 'some-value' }; + + const result = getFinalOptions(userOptions, env); + + expect(result).toEqual(userOptions); + }); + + it('takes user options over env options', () => { + const userOptions = { dsn: 'test-dsn', release: 'user-release' }; + const env = { SENTRY_RELEASE: 'env-release' }; + + const result = getFinalOptions(userOptions, env); + + expect(result).toEqual(userOptions); + }); +}); diff --git a/packages/cloudflare/tsconfig.json b/packages/cloudflare/tsconfig.json index ff89f0feaa23..47f82e407551 100644 --- a/packages/cloudflare/tsconfig.json +++ b/packages/cloudflare/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "../../tsconfig.json", - "include": ["src/**/*"], + "include": ["src/**/*", "test/options.test.ts"], "compilerOptions": { "module": "esnext", From e49219f93393834dc5d0cccabc3d9ce0e2fca6d5 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Tue, 6 May 2025 11:50:42 +0200 Subject: [PATCH 3/5] simplify getFinalOptions --- packages/cloudflare/src/options.ts | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/packages/cloudflare/src/options.ts b/packages/cloudflare/src/options.ts index 9a205b654061..77a37ea51d31 100644 --- a/packages/cloudflare/src/options.ts +++ b/packages/cloudflare/src/options.ts @@ -10,14 +10,11 @@ import type { CloudflareOptions } from './client'; * @returns The final options. */ export function getFinalOptions(userOptions: CloudflareOptions, env: unknown): CloudflareOptions { - function getOptionsFromEnv(env: unknown): CloudflareOptions { - if (typeof env !== 'object' || env === null) { - return {}; - } - - return { - release: 'SENTRY_RELEASE' in env && typeof env.SENTRY_RELEASE === 'string' ? env.SENTRY_RELEASE : undefined, - }; + if (typeof env !== 'object' || env === null) { + return userOptions; } - return { ...getOptionsFromEnv(env), ...userOptions }; + + const release = 'SENTRY_RELEASE' in env && typeof env.SENTRY_RELEASE === 'string' ? env.SENTRY_RELEASE : undefined; + + return { release, ...userOptions }; } From 858134d4f09f063eb1f213a0688325b24fcad0ca Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Tue, 6 May 2025 15:14:25 +0200 Subject: [PATCH 4/5] lint --- packages/cloudflare/src/durableobject.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cloudflare/src/durableobject.ts b/packages/cloudflare/src/durableobject.ts index aeddfed4dd91..d595ccfa5985 100644 --- a/packages/cloudflare/src/durableobject.ts +++ b/packages/cloudflare/src/durableobject.ts @@ -13,9 +13,9 @@ import type { DurableObject } from 'cloudflare:workers'; import { setAsyncLocalStorageAsyncContextStrategy } from './async'; import type { CloudflareOptions } from './client'; import { isInstrumented, markAsInstrumented } from './instrument'; +import { getFinalOptions } from './options'; import { wrapRequestHandler } from './request'; import { init } from './sdk'; -import { getFinalOptions } from './options'; type MethodWrapperOptions = { spanName?: string; From e0c18f67cf9c5a1c93ddd6c46239a7dd5748f89b Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Tue, 6 May 2025 23:07:57 +0200 Subject: [PATCH 5/5] Update packages/cloudflare/tsconfig.json --- packages/cloudflare/tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cloudflare/tsconfig.json b/packages/cloudflare/tsconfig.json index 47f82e407551..ff89f0feaa23 100644 --- a/packages/cloudflare/tsconfig.json +++ b/packages/cloudflare/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "../../tsconfig.json", - "include": ["src/**/*", "test/options.test.ts"], + "include": ["src/**/*"], "compilerOptions": { "module": "esnext",