Skip to content
Merged
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
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,21 @@

- "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott

- **feat(cloudflare): Add honoIntegration with error-filtering function ([#17743](https://github.com/getsentry/sentry-javascript/pull/17743))**

This release adds a `honoIntegration` to `@sentry/cloudflare`, which exposes a `shouldHandleError` function that lets you define which errors in `onError` should be captured.
By default, Sentry captures exceptions with `error.status >= 500 || error.status <= 299`.

The integration is added by default, and it's possible to modify this behavior like this:

```js
integrations: [
honoIntegration({
shouldHandleError: (err) => true; // always capture exceptions in onError
})
]
```

Work in this release was contributed by @Karibash. Thank you for your contribution!

## 10.14.0
Expand Down
5 changes: 3 additions & 2 deletions packages/cloudflare/src/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
import { setAsyncLocalStorageAsyncContextStrategy } from './async';
import type { CloudflareOptions } from './client';
import { isInstrumented, markAsInstrumented } from './instrument';
import { getHonoIntegration } from './integrations/hono';
import { getFinalOptions } from './options';
import { wrapRequestHandler } from './request';
import { addCloudResourceContext } from './scope-utils';
Expand Down Expand Up @@ -48,7 +49,7 @@ export function withSentry<Env = unknown, QueueHandlerMessage = unknown, CfHostM
markAsInstrumented(handler.fetch);
}

/* hono does not reach the catch block of the fetch handler and captureException needs to be called in the hono errorHandler */
/* Hono does not reach the catch block of the fetch handler and captureException needs to be called in the hono errorHandler */
if (
'onError' in handler &&
'errorHandler' in handler &&
Expand All @@ -59,7 +60,7 @@ export function withSentry<Env = unknown, QueueHandlerMessage = unknown, CfHostM
apply(target, thisArg, args) {
const [err] = args;

captureException(err, { mechanism: { handled: false, type: 'auto.faas.cloudflare.error_handler' } });
getHonoIntegration()?.handleHonoException(err);
Copy link

Choose a reason for hiding this comment

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

Bug: Error Handling Regression in SDK

The errorHandler proxy now uses getHonoIntegration()?.handleHonoException(err). This change can silently drop exceptions if getHonoIntegration() returns undefined (e.g., SDK not initialized or Hono integration is missing), a regression from the previous guaranteed error capture.

Fix in Cursor Fix in Web

Copy link
Member Author

@s1gr1d s1gr1d Sep 23, 2025

Choose a reason for hiding this comment

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

this is the reason of the PR...

The client is defined at this point. It was the same before. If there was no client, it did not capture.


return Reflect.apply(target, thisArg, args);
},
Expand Down
1 change: 1 addition & 0 deletions packages/cloudflare/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ export { getDefaultIntegrations } from './sdk';

export { fetchIntegration } from './integrations/fetch';
export { vercelAIIntegration } from './integrations/tracing/vercelai';
export { honoIntegration } from './integrations/hono';

export { instrumentD1WithSentry } from './d1';

Expand Down
74 changes: 74 additions & 0 deletions packages/cloudflare/src/integrations/hono.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import type { IntegrationFn } from '@sentry/core';
import { captureException, debug, defineIntegration, getClient } from '@sentry/core';
import { DEBUG_BUILD } from '../debug-build';

const INTEGRATION_NAME = 'Hono';

interface HonoError extends Error {
status?: number;
}

export interface Options {
/**
* Callback method deciding whether error should be captured and sent to Sentry
* @param error Captured middleware error
*/
shouldHandleError?(this: void, error: HonoError): boolean;
}

/** Only exported for internal use */
export function getHonoIntegration(): ReturnType<typeof _honoIntegration> | undefined {
return getClient()?.getIntegrationByName(INTEGRATION_NAME);
}

function isHonoError(err: unknown): err is HonoError {
if (err instanceof Error) {
return true;
}
return typeof err === 'object' && err !== null && 'status' in (err as Record<string, unknown>);
}

const _honoIntegration = ((options: Partial<Options> = {}) => {
return {
name: INTEGRATION_NAME,
handleHonoException(err: HonoError): void {
const shouldHandleError = options.shouldHandleError || defaultShouldHandleError;

if (!isHonoError(err)) {
DEBUG_BUILD && debug.log("[Hono] Won't capture exception in `onError` because it's not a Hono error.", err);
return;
}

if (shouldHandleError(err)) {
captureException(err, { mechanism: { handled: false, type: 'auto.faas.hono.error_handler' } });
} else {
DEBUG_BUILD && debug.log('[Hono] Not capturing exception because `shouldHandleError` returned `false`.', err);
}
},
};
}) satisfies IntegrationFn;

/**
* Automatically captures exceptions caught with the `onError` handler in Hono.
*
* The integration is enabled by default.
*
* @example
* integrations: [
* honoIntegration({
* shouldHandleError: (err) => true; // always capture exceptions in onError
* })
* ]
*/
export const honoIntegration = defineIntegration(_honoIntegration);

/**
* Default function to determine if an error should be sent to Sentry
*
* 3xx and 4xx errors are not sent by default.
*/
function defaultShouldHandleError(error: HonoError): boolean {
const statusCode = error?.status;
// 3xx and 4xx errors are not sent by default.
return statusCode ? statusCode >= 500 || statusCode <= 299 : true;
}
2 changes: 2 additions & 0 deletions packages/cloudflare/src/sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import type { CloudflareClientOptions, CloudflareOptions } from './client';
import { CloudflareClient } from './client';
import { makeFlushLock } from './flush';
import { fetchIntegration } from './integrations/fetch';
import { honoIntegration } from './integrations/hono';
import { setupOpenTelemetryTracer } from './opentelemetry/tracer';
import { makeCloudflareTransport } from './transport';
import { defaultStackParser } from './vendor/stacktrace';
Expand All @@ -31,6 +32,7 @@ export function getDefaultIntegrations(options: CloudflareOptions): Integration[
functionToStringIntegration(),
linkedErrorsIntegration(),
fetchIntegration(),
honoIntegration(),
// TODO(v11): the `include` object should be defined directly in the integration based on `sendDefaultPii`
requestDataIntegration(sendDefaultPii ? undefined : { include: { cookies: false } }),
consoleIntegration(),
Expand Down
13 changes: 7 additions & 6 deletions packages/cloudflare/test/handler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { beforeEach, describe, expect, onTestFinished, test, vi } from 'vitest';
import { CloudflareClient } from '../src/client';
import { withSentry } from '../src/handler';
import { markAsInstrumented } from '../src/instrument';
import * as HonoIntegration from '../src/integrations/hono';

// Custom type for hono-like apps (cloudflare handlers) that include errorHandler and onError
type HonoLikeApp<Env = unknown, QueueHandlerMessage = unknown, CfHostMetadata = unknown> = ExportedHandler<
Expand Down Expand Up @@ -1081,10 +1082,12 @@ describe('withSentry', () => {
});

describe('hono errorHandler', () => {
test('captures errors handled by the errorHandler', async () => {
const captureExceptionSpy = vi.spyOn(SentryCore, 'captureException');
test('calls Hono Integration to handle error captured by the errorHandler', async () => {
const error = new Error('test hono error');

const handleHonoException = vi.fn();
vi.spyOn(HonoIntegration, 'getHonoIntegration').mockReturnValue({ handleHonoException } as any);

const honoApp = {
fetch(_request, _env, _context) {
return new Response('test');
Expand All @@ -1100,10 +1103,8 @@ describe('withSentry', () => {
// simulates hono's error handling
const errorHandlerResponse = honoApp.errorHandler?.(error);

expect(captureExceptionSpy).toHaveBeenCalledTimes(1);
expect(captureExceptionSpy).toHaveBeenLastCalledWith(error, {
mechanism: { handled: false, type: 'auto.faas.cloudflare.error_handler' },
});
expect(handleHonoException).toHaveBeenCalledTimes(1);
expect(handleHonoException).toHaveBeenLastCalledWith(error);
expect(errorHandlerResponse?.status).toBe(500);
});

Expand Down
94 changes: 94 additions & 0 deletions packages/cloudflare/test/integrations/hono.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import * as sentryCore from '@sentry/core';
import { type Client, createStackParser } from '@sentry/core';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { CloudflareClient } from '../../src/client';
import { honoIntegration } from '../../src/integrations/hono';

class FakeClient extends CloudflareClient {
public getIntegrationByName(name: string) {
return name === 'Hono' ? (honoIntegration() as any) : undefined;
}
}

type MockHonoIntegrationType = { handleHonoException: (err: Error) => void };

describe('Hono integration', () => {
let client: FakeClient;

beforeEach(() => {
vi.clearAllMocks();
client = new FakeClient({
dsn: 'https://[email protected]/1337',
integrations: [],
transport: () => ({ send: () => Promise.resolve({}), flush: () => Promise.resolve(true) }),
stackParser: createStackParser(),
});

vi.spyOn(sentryCore, 'getClient').mockImplementation(() => client as Client);
});

it('captures in errorHandler when onError exists', () => {
const captureExceptionSpy = vi.spyOn(sentryCore, 'captureException');
const integration = honoIntegration();
integration.setupOnce?.();

const error = new Error('hono boom');
// simulate withSentry wrapping of errorHandler calling back into integration
(integration as unknown as MockHonoIntegrationType).handleHonoException(error);

expect(captureExceptionSpy).toHaveBeenCalledTimes(1);
expect(captureExceptionSpy).toHaveBeenLastCalledWith(error, {
mechanism: { handled: false, type: 'auto.faas.hono.error_handler' },
});
});

it('does not capture for 4xx status', () => {
const captureExceptionSpy = vi.spyOn(sentryCore, 'captureException');
const integration = honoIntegration();
integration.setupOnce?.();

(integration as unknown as MockHonoIntegrationType).handleHonoException(
Object.assign(new Error('client err'), { status: 404 }),
);
expect(captureExceptionSpy).not.toHaveBeenCalled();
});

it('does not capture for 3xx status', () => {
const captureExceptionSpy = vi.spyOn(sentryCore, 'captureException');
const integration = honoIntegration();
integration.setupOnce?.();

(integration as unknown as MockHonoIntegrationType).handleHonoException(
Object.assign(new Error('redirect'), { status: 302 }),
);
expect(captureExceptionSpy).not.toHaveBeenCalled();
});

it('captures for 5xx status', () => {
const captureExceptionSpy = vi.spyOn(sentryCore, 'captureException');
const integration = honoIntegration();
integration.setupOnce?.();

const err = Object.assign(new Error('server err'), { status: 500 });
(integration as unknown as MockHonoIntegrationType).handleHonoException(err);
expect(captureExceptionSpy).toHaveBeenCalledTimes(1);
});

it('captures if no status is present on Error', () => {
const captureExceptionSpy = vi.spyOn(sentryCore, 'captureException');
const integration = honoIntegration();
integration.setupOnce?.();

(integration as unknown as MockHonoIntegrationType).handleHonoException(new Error('no status'));
expect(captureExceptionSpy).toHaveBeenCalledTimes(1);
});

it('supports custom shouldHandleError option', () => {
const captureExceptionSpy = vi.spyOn(sentryCore, 'captureException');
const integration = honoIntegration({ shouldHandleError: () => false });
integration.setupOnce?.();

(integration as unknown as MockHonoIntegrationType).handleHonoException(new Error('blocked'));
expect(captureExceptionSpy).not.toHaveBeenCalled();
});
});
2 changes: 1 addition & 1 deletion packages/cloudflare/tsconfig.test.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"extends": "./tsconfig.json",

"include": ["test/**/*"],
"include": ["test/**/*", "vite.config.ts"],

"compilerOptions": {
// other package-specific, test-specific options
Expand Down
6 changes: 6 additions & 0 deletions packages/cloudflare/vite.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { defineConfig } from 'vitest/config';
import baseConfig from '../../vite/vite.config';

export default defineConfig({
...baseConfig,
Copy link
Member Author

Choose a reason for hiding this comment

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

I needed to add a vite.config.ts file because the baseConfig defines this:

  define: {
    __DEBUG_BUILD__: true,
  },

...and this was needed to make the unit tests work.

});