diff --git a/packages/cloudflare/src/handler.ts b/packages/cloudflare/src/handler.ts index 62956cff62cf..d3d1f80dbbd5 100644 --- a/packages/cloudflare/src/handler.ts +++ b/packages/cloudflare/src/handler.ts @@ -26,6 +26,7 @@ import { init } from './sdk'; * @param handler {ExportedHandler} The handler to wrap. * @returns The wrapped handler. */ +// eslint-disable-next-line complexity export function withSentry( optionsCallback: (env: Env) => CloudflareOptions, handler: ExportedHandler, @@ -47,6 +48,26 @@ export function withSentry>) { diff --git a/packages/cloudflare/test/handler.test.ts b/packages/cloudflare/test/handler.test.ts index 6ae688f316f9..bced0fdbe277 100644 --- a/packages/cloudflare/test/handler.test.ts +++ b/packages/cloudflare/test/handler.test.ts @@ -7,6 +7,17 @@ import * as SentryCore from '@sentry/core'; import { beforeEach, describe, expect, test, vi } from 'vitest'; import { CloudflareClient } from '../src/client'; import { withSentry } from '../src/handler'; +import { markAsInstrumented } from '../src/instrument'; + +// Custom type for hono-like apps (cloudflare handlers) that include errorHandler and onError +type HonoLikeApp = ExportedHandler< + Env, + QueueHandlerMessage, + CfHostMetadata +> & { + onError?: () => void; + errorHandler?: (err: Error) => Response; +}; const MOCK_ENV = { SENTRY_DSN: 'https://public@dsn.ingest.sentry.io/1337', @@ -931,6 +942,86 @@ describe('withSentry', () => { }); }); }); + + describe('hono errorHandler', () => { + test('captures errors handled by the errorHandler', async () => { + const captureExceptionSpy = vi.spyOn(SentryCore, 'captureException'); + const error = new Error('test hono error'); + + const honoApp = { + fetch(_request, _env, _context) { + return new Response('test'); + }, + onError() {}, // hono-like onError + errorHandler(err: Error) { + return new Response(`Error: ${err.message}`, { status: 500 }); + }, + } satisfies HonoLikeApp; + + withSentry(env => ({ dsn: env.SENTRY_DSN }), honoApp); + + // simulates hono's error handling + const errorHandlerResponse = honoApp.errorHandler?.(error); + + expect(captureExceptionSpy).toHaveBeenCalledTimes(1); + expect(captureExceptionSpy).toHaveBeenLastCalledWith(error, { + mechanism: { handled: false, type: 'cloudflare' }, + }); + expect(errorHandlerResponse?.status).toBe(500); + }); + + test('preserves the original errorHandler functionality', async () => { + const originalErrorHandlerSpy = vi.fn().mockImplementation((err: Error) => { + return new Response(`Error: ${err.message}`, { status: 500 }); + }); + + const error = new Error('test hono error'); + + const honoApp = { + fetch(_request, _env, _context) { + return new Response('test'); + }, + onError() {}, // hono-like onError + errorHandler: originalErrorHandlerSpy, + } satisfies HonoLikeApp; + + withSentry(env => ({ dsn: env.SENTRY_DSN }), honoApp); + + // Call the errorHandler directly to simulate Hono's error handling + const errorHandlerResponse = honoApp.errorHandler?.(error); + + expect(originalErrorHandlerSpy).toHaveBeenCalledTimes(1); + expect(originalErrorHandlerSpy).toHaveBeenLastCalledWith(error); + expect(errorHandlerResponse?.status).toBe(500); + }); + + test('does not instrument an already instrumented errorHandler', async () => { + const captureExceptionSpy = vi.spyOn(SentryCore, 'captureException'); + const error = new Error('test hono error'); + + // Create a handler with an errorHandler that's already been instrumented + const originalErrorHandler = (err: Error) => { + return new Response(`Error: ${err.message}`, { status: 500 }); + }; + + // Mark as instrumented before wrapping + markAsInstrumented(originalErrorHandler); + + const honoApp = { + fetch(_request, _env, _context) { + return new Response('test'); + }, + onError() {}, // hono-like onError + errorHandler: originalErrorHandler, + } satisfies HonoLikeApp; + + withSentry(env => ({ dsn: env.SENTRY_DSN }), honoApp); + + // The errorHandler should not have been wrapped again + honoApp.errorHandler?.(error); + expect(captureExceptionSpy).not.toHaveBeenCalled(); + }); + }); }); function createMockExecutionContext(): ExecutionContext {