From 027b6a8944e07223f394a12b5b456fdecffbc6f0 Mon Sep 17 00:00:00 2001 From: JPeer264 Date: Tue, 10 Mar 2026 09:40:01 +0100 Subject: [PATCH 1/2] fix(cloudflare): Recreate client when previous one was disposed --- .../cloudflare/src/wrapMethodWithSentry.ts | 10 +- .../test/wrapMethodWithSentry.test.ts | 108 ++++++++++++++++-- 2 files changed, 108 insertions(+), 10 deletions(-) diff --git a/packages/cloudflare/src/wrapMethodWithSentry.ts b/packages/cloudflare/src/wrapMethodWithSentry.ts index 3c719e7da4b1..7bc01940286a 100644 --- a/packages/cloudflare/src/wrapMethodWithSentry.ts +++ b/packages/cloudflare/src/wrapMethodWithSentry.ts @@ -68,13 +68,17 @@ export function wrapMethodWithSentry( const waitUntil = context?.waitUntil?.bind?.(context); - const currentClient = scope.getClient(); - if (!currentClient) { + let currentClient = scope.getClient(); + // Check if client exists AND is still usable (transport not disposed) + // This handles the case where a previous handler disposed the client + // but the scope still holds a reference to it (e.g., alarm handlers in Durable Objects) + if (!currentClient || !currentClient.getTransport()) { const client = init({ ...wrapperOptions.options, ctx: context as unknown as ExecutionContext | undefined }); scope.setClient(client); + currentClient = client; } - const clientToDispose = currentClient || scope.getClient(); + const clientToDispose = currentClient; if (!wrapperOptions.spanName) { try { diff --git a/packages/cloudflare/test/wrapMethodWithSentry.test.ts b/packages/cloudflare/test/wrapMethodWithSentry.test.ts index a7e73a83cd39..1804172418fc 100644 --- a/packages/cloudflare/test/wrapMethodWithSentry.test.ts +++ b/packages/cloudflare/test/wrapMethodWithSentry.test.ts @@ -1,25 +1,31 @@ import * as sentryCore from '@sentry/core'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { isInstrumented } from '../src/instrument'; +import * as sdk from '../src/sdk'; import { wrapMethodWithSentry } from '../src/wrapMethodWithSentry'; -// Mock the SDK init to avoid actual SDK initialization -vi.mock('../src/sdk', () => ({ - init: vi.fn(() => ({ +function createMockClient(hasTransport: boolean = true) { + return { getOptions: () => ({}), on: vi.fn(), dispose: vi.fn(), - })), + getTransport: vi.fn().mockReturnValue(hasTransport ? { send: vi.fn() } : undefined), + }; +} + +// Mock the SDK init to avoid actual SDK initialization +vi.mock('../src/sdk', () => ({ + init: vi.fn(() => createMockClient(true)), })); // Mock sentry/core functions vi.mock('@sentry/core', async importOriginal => { - const actual = await importOriginal(); + const actual = await importOriginal(); return { ...actual, getClient: vi.fn(), - withIsolationScope: vi.fn((callback: (scope: any) => any) => callback(createMockScope())), - withScope: vi.fn((callback: (scope: any) => any) => callback(createMockScope())), + withIsolationScope: vi.fn((callback: (scope: unknown) => unknown) => callback(createMockScope())), + withScope: vi.fn((callback: (scope: unknown) => unknown) => callback(createMockScope())), startSpan: vi.fn((opts, callback) => callback(createMockSpan())), captureException: vi.fn(), flush: vi.fn().mockResolvedValue(true), @@ -27,6 +33,8 @@ vi.mock('@sentry/core', async importOriginal => { }; }); +const mockedWithIsolationScope = vi.mocked(sentryCore.withIsolationScope); + function createMockScope() { return { getClient: vi.fn(), @@ -307,4 +315,90 @@ describe('wrapMethodWithSentry', () => { expect(handler.mock.instances[0]).toBe(thisArg); }); }); + + describe('client re-initialization', () => { + it('creates a new client when scope has no client', async () => { + const scope = new sentryCore.Scope(); + + mockedWithIsolationScope.mockImplementation(vi.fn(callback => callback(scope))); + + const spyClient = vi.spyOn(scope, 'setClient'); + const handler = vi.fn().mockResolvedValue('result'); + const options = { + options: { dsn: 'https://test@sentry.io/123' }, + context: createMockContext(), + }; + + const wrapped = wrapMethodWithSentry(options, handler); + + await wrapped(); + + expect(sdk.init).toHaveBeenCalledWith( + expect.objectContaining({ + dsn: 'https://test@sentry.io/123', + }), + ); + expect(spyClient).toHaveBeenCalled(); + }); + + it('creates a new client when existing client has no transport (disposed)', async () => { + const disposedClient = { + getOptions: () => ({}), + on: vi.fn(), + dispose: vi.fn(), + getTransport: vi.fn().mockReturnValue(undefined), + } as unknown as sentryCore.Client; + + const scope = new sentryCore.Scope(); + + scope.setClient(disposedClient); + mockedWithIsolationScope.mockImplementation(vi.fn(callback => callback(scope))); + + const spyClient = vi.spyOn(scope, 'setClient'); + const handler = vi.fn().mockResolvedValue('result'); + const options = { + options: { dsn: 'https://test@sentry.io/123' }, + context: createMockContext(), + }; + + const wrapped = wrapMethodWithSentry(options, handler); + await wrapped(); + + expect(sdk.init).toHaveBeenCalledWith( + expect.objectContaining({ + dsn: 'https://test@sentry.io/123', + }), + ); + expect(spyClient).toHaveBeenCalled(); + }); + + it('does not create a new client when existing client has valid transport', async () => { + const validClient = { + getOptions: () => ({}), + on: vi.fn(), + dispose: vi.fn(), + getTransport: vi.fn().mockReturnValue({ send: vi.fn() }), + } as unknown as sentryCore.Client; + + const scope = new sentryCore.Scope(); + + scope.setClient(validClient); + mockedWithIsolationScope.mockImplementation(vi.fn(callback => callback(scope))); + vi.mocked(sdk.init).mockClear(); + + const spyClient = vi.spyOn(scope, 'setClient'); + const handler = vi.fn().mockResolvedValue('result'); + const options = { + options: { dsn: 'https://test@sentry.io/123' }, + context: createMockContext(), + }; + + const wrapped = wrapMethodWithSentry(options, handler); + + await wrapped(); + + expect(sdk.init).not.toHaveBeenCalled(); + expect(spyClient).not.toHaveBeenCalled(); + }); + }); }); From 0781263c06659f8fe2cf25b1419cff9126954bcb Mon Sep 17 00:00:00 2001 From: JPeer264 Date: Tue, 10 Mar 2026 10:28:33 +0100 Subject: [PATCH 2/2] fixup! fix(cloudflare): Recreate client when previous one was disposed --- packages/cloudflare/test/wrapMethodWithSentry.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cloudflare/test/wrapMethodWithSentry.test.ts b/packages/cloudflare/test/wrapMethodWithSentry.test.ts index 1804172418fc..c831bd01a6bb 100644 --- a/packages/cloudflare/test/wrapMethodWithSentry.test.ts +++ b/packages/cloudflare/test/wrapMethodWithSentry.test.ts @@ -20,7 +20,7 @@ vi.mock('../src/sdk', () => ({ // Mock sentry/core functions vi.mock('@sentry/core', async importOriginal => { - const actual = await importOriginal(); + const actual = await importOriginal(); return { ...actual, getClient: vi.fn(),