diff --git a/dev-packages/cloudflare-integration-tests/expect.ts b/dev-packages/cloudflare-integration-tests/expect.ts index b33926ffce11..c3e2bd007436 100644 --- a/dev-packages/cloudflare-integration-tests/expect.ts +++ b/dev-packages/cloudflare-integration-tests/expect.ts @@ -28,6 +28,7 @@ function getSdk(sdk: 'cloudflare' | 'hono'): SdkInfo { name: `npm:@sentry/${sdk}`, version: SDK_VERSION, }, + ...(sdk === 'hono' ? [{ name: 'npm:@sentry/cloudflare', version: SDK_VERSION }] : []), ], version: SDK_VERSION, }; diff --git a/packages/hono/README.md b/packages/hono/README.md index cc5edbd30a02..b1b9e07760f9 100644 --- a/packages/hono/README.md +++ b/packages/hono/README.md @@ -48,6 +48,7 @@ compatibility_flags = ["nodejs_compat"] Initialize the Sentry Hono middleware as early as possible in your app: ```typescript +import { Hono } from 'hono'; import { sentry } from '@sentry/hono/cloudflare'; const app = new Hono(); @@ -64,3 +65,20 @@ app.use( export default app; ``` + +#### Access `env` from Cloudflare Worker bindings + +Pass the options as a callback instead of a plain options object. The function receives the Cloudflare Worker `env` as defined in the Worker's `Bindings`: + +```typescript +import { Hono } from 'hono'; +import { sentry } from '@sentry/hono/cloudflare'; + +type Bindings = { SENTRY_DSN: string }; + +const app = new Hono<{ Bindings: Bindings }>(); + +app.use(sentry(app, env => ({ dsn: env.SENTRY_DSN }))); + +export default app; +``` diff --git a/packages/hono/src/cloudflare/middleware.ts b/packages/hono/src/cloudflare/middleware.ts index ffcdf5e40346..76d571d2cda7 100644 --- a/packages/hono/src/cloudflare/middleware.ts +++ b/packages/hono/src/cloudflare/middleware.ts @@ -7,40 +7,47 @@ import { type Integration, type Options, } from '@sentry/core'; -import type { Context, Hono, MiddlewareHandler } from 'hono'; +import type { Env, Hono, MiddlewareHandler } from 'hono'; import { requestHandler, responseHandler } from '../shared/middlewareHandlers'; import { patchAppUse } from '../shared/patchAppUse'; -export interface HonoOptions extends Options { - context?: Context; -} +export interface HonoOptions extends Options {} const filterHonoIntegration = (integration: Integration): boolean => integration.name !== 'Hono'; -export const sentry = (app: Hono, options: HonoOptions | undefined = {}): MiddlewareHandler => { - const isDebug = options.debug; - - isDebug && debug.log('Initialized Sentry Hono middleware (Cloudflare)'); - - applySdkMetadata(options, 'hono'); - - const { integrations: userIntegrations } = options; +/** + * Sentry middleware for Hono on Cloudflare Workers. + */ +export function sentry( + app: Hono, + options: HonoOptions | ((env: E['Bindings']) => HonoOptions), +): MiddlewareHandler { withSentry( - () => ({ - ...options, - // Always filter out the Hono integration from defaults and user integrations. - // The Hono integration is already set up by withSentry, so adding it again would cause capturing too early (in Cloudflare SDK) and non-parametrized URLs. - integrations: Array.isArray(userIntegrations) - ? defaults => - getIntegrationsToSetup({ - defaultIntegrations: defaults.filter(filterHonoIntegration), - integrations: userIntegrations.filter(filterHonoIntegration), - }) - : typeof userIntegrations === 'function' - ? defaults => userIntegrations(defaults).filter(filterHonoIntegration) - : defaults => defaults.filter(filterHonoIntegration), - }), - app, + env => { + const honoOptions = typeof options === 'function' ? options(env as E['Bindings']) : options; + + applySdkMetadata(honoOptions, 'hono', ['hono', 'cloudflare']); + + honoOptions.debug && debug.log('Initialized Sentry Hono middleware (Cloudflare)'); + + const { integrations: userIntegrations } = honoOptions; + return { + ...honoOptions, + // Always filter out the Hono integration from defaults and user integrations. + // The Hono integration is already set up by withSentry, so adding it again would cause capturing too early (in Cloudflare SDK) and non-parametrized URLs. + integrations: Array.isArray(userIntegrations) + ? defaults => + getIntegrationsToSetup({ + defaultIntegrations: defaults.filter(filterHonoIntegration), + integrations: userIntegrations.filter(filterHonoIntegration), + }) + : typeof userIntegrations === 'function' + ? defaults => userIntegrations(defaults).filter(filterHonoIntegration) + : defaults => defaults.filter(filterHonoIntegration), + }; + }, + // Cast needed because Hono exposes a narrower fetch signature than ExportedHandler + app as unknown as ExportedHandler, ); patchAppUse(app); @@ -52,4 +59,4 @@ export const sentry = (app: Hono, options: HonoOptions | undefined = {}): Middle responseHandler(context); }; -}; +} diff --git a/packages/hono/src/index.cloudflare.ts b/packages/hono/src/index.cloudflare.ts index cba517e1d295..99c04597a98f 100644 --- a/packages/hono/src/index.cloudflare.ts +++ b/packages/hono/src/index.cloudflare.ts @@ -1 +1,3 @@ export { sentry } from './cloudflare/middleware'; + +export * from '@sentry/cloudflare'; diff --git a/packages/hono/src/shared/patchAppUse.ts b/packages/hono/src/shared/patchAppUse.ts index dfcd186dc38a..28c3c49e7193 100644 --- a/packages/hono/src/shared/patchAppUse.ts +++ b/packages/hono/src/shared/patchAppUse.ts @@ -6,7 +6,7 @@ import { SPAN_STATUS_OK, startInactiveSpan, } from '@sentry/core'; -import type { Hono, MiddlewareHandler } from 'hono'; +import type { Env, Hono, MiddlewareHandler } from 'hono'; const MIDDLEWARE_ORIGIN = 'auto.middleware.hono'; @@ -14,7 +14,7 @@ const MIDDLEWARE_ORIGIN = 'auto.middleware.hono'; * Patches `app.use` so that every middleware registered through it is automatically * wrapped in a Sentry span. Supports both forms: `app.use(...handlers)` and `app.use(path, ...handlers)`. */ -export function patchAppUse(app: Hono): void { +export function patchAppUse(app: Hono): void { app.use = new Proxy(app.use, { apply(target: typeof app.use, thisArg: typeof app, args: Parameters): ReturnType { const [first, ...rest] = args as [unknown, ...MiddlewareHandler[]]; diff --git a/packages/hono/test/cloudflare/middleware.test.ts b/packages/hono/test/cloudflare/middleware.test.ts index 08629d706e8b..f46192b0ac87 100644 --- a/packages/hono/test/cloudflare/middleware.test.ts +++ b/packages/hono/test/cloudflare/middleware.test.ts @@ -25,7 +25,7 @@ describe('Hono Cloudflare Middleware', () => { }); describe('sentry middleware', () => { - it('calls applySdkMetadata with "hono"', () => { + it('calls applySdkMetadata with "hono" when the options callback is invoked', () => { const app = new Hono(); const options = { dsn: 'https://public@dsn.ingest.sentry.io/1337', @@ -33,8 +33,11 @@ describe('Hono Cloudflare Middleware', () => { sentry(app, options); + const optionsCallback = withSentryMock.mock.calls[0]?.[0]; + optionsCallback(); + expect(applySdkMetadataMock).toHaveBeenCalledTimes(1); - expect(applySdkMetadataMock).toHaveBeenCalledWith(options, 'hono'); + expect(applySdkMetadataMock).toHaveBeenCalledWith(options, 'hono', ['hono', 'cloudflare']); }); it('calls withSentry with modified options', () => { @@ -63,24 +66,13 @@ describe('Hono Cloudflare Middleware', () => { name: 'npm:@sentry/hono', version: SDK_VERSION, }, + { + name: 'npm:@sentry/cloudflare', + version: SDK_VERSION, + }, ]); }); - it('calls applySdkMetadata before withSentry', () => { - const app = new Hono(); - const options = { - dsn: 'https://public@dsn.ingest.sentry.io/1337', - }; - - sentry(app, options); - - // Verify applySdkMetadata was called before withSentry - const applySdkMetadataCallOrder = applySdkMetadataMock.mock.invocationCallOrder[0]; - const withSentryCallOrder = withSentryMock.mock.invocationCallOrder[0]; - - expect(applySdkMetadataCallOrder).toBeLessThan(withSentryCallOrder as number); - }); - it('preserves all user options', () => { const app = new Hono(); const options = {