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..c831bd01a6bb 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(); + }); + }); });