Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions dev-packages/cloudflare-integration-tests/expect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand Down
18 changes: 18 additions & 0 deletions packages/hono/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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;
```
63 changes: 35 additions & 28 deletions packages/hono/src/cloudflare/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<BaseTransportOptions> {
context?: Context;
}
export interface HonoOptions extends Options<BaseTransportOptions> {}

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<E extends Env>(
app: Hono<E>,
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<E> exposes a narrower fetch signature than ExportedHandler<unknown>
app as unknown as ExportedHandler<unknown>,
);

patchAppUse(app);
Expand All @@ -52,4 +59,4 @@ export const sentry = (app: Hono, options: HonoOptions | undefined = {}): Middle

responseHandler(context);
};
};
}
2 changes: 2 additions & 0 deletions packages/hono/src/index.cloudflare.ts
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
export { sentry } from './cloudflare/middleware';

export * from '@sentry/cloudflare';
4 changes: 2 additions & 2 deletions packages/hono/src/shared/patchAppUse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@ 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';

/**
* 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<E extends Env>(app: Hono<E>): void {
app.use = new Proxy(app.use, {
apply(target: typeof app.use, thisArg: typeof app, args: Parameters<typeof app.use>): ReturnType<typeof app.use> {
const [first, ...rest] = args as [unknown, ...MiddlewareHandler[]];
Expand Down
26 changes: 9 additions & 17 deletions packages/hono/test/cloudflare/middleware.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,19 @@ 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',
};

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']);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No test for the new callback options feature

Low Severity

The primary feature of this fix PR — passing options as a callback function receiving env (the (env: E['Bindings']) => HonoOptions branch) — has no unit, integration, or e2e test covering it. All existing tests pass options as a plain object. There's no test verifying that when options is a function, it gets called with the env argument and its return value is used correctly.

Additional Locations (1)
Fix in Cursor Fix in Web

Triggered by project rule: PR Review Guidelines for Cursor Bot

});

it('calls withSentry with modified options', () => {
Expand Down Expand Up @@ -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 = {
Expand Down
Loading