Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
3 changes: 2 additions & 1 deletion dev-packages/cloudflare-integration-tests/expect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export function expectedEvent(event: Event): Event {
});
}

export function eventEnvelope(event: Event): Envelope {
export function eventEnvelope(event: Event, includeSampleRand = false): Envelope {
return [
{
event_id: UUID_MATCHER,
Expand All @@ -69,6 +69,7 @@ export function eventEnvelope(event: Event): Envelope {
public_key: 'public',
trace_id: UUID_MATCHER,
sample_rate: expect.any(String),
...(includeSampleRand && { sample_rand: expect.stringMatching(/^[01](\.\d+)?$/) }),
sampled: expect.any(String),
transaction: expect.any(String),
},
Expand Down
3 changes: 2 additions & 1 deletion dev-packages/cloudflare-integration-tests/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
},
"dependencies": {
"@langchain/langgraph": "^1.0.1",
"@sentry/cloudflare": "10.31.0"
"@sentry/cloudflare": "10.31.0",
"hono": "^4.0.0"
},
"devDependencies": {
"@cloudflare/workers-types": "^4.20250922.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import * as Sentry from '@sentry/cloudflare';
import { Hono } from 'hono';

interface Env {
SENTRY_DSN: string;
}

const app = new Hono<{ Bindings: Env }>();

app.get('/', c => {
return c.text('Hello from Hono on Cloudflare!');
});

app.get('/json', c => {
return c.json({ message: 'Hello from Hono', framework: 'hono', platform: 'cloudflare' });
});

app.get('/error', () => {
throw new Error('Test error from Hono app');
});

app.get('/hello/:name', c => {
const name = c.req.param('name');
return c.text(`Hello, ${name}!`);
});

export default Sentry.withSentry(
(env: Env) => ({
dsn: env.SENTRY_DSN,
tracesSampleRate: 1.0,
}),
app,
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { expect, it } from 'vitest';
import { eventEnvelope } from '../../../expect';
import { createRunner } from '../../../runner';

it('Hono app captures errors', async ({ signal }) => {
const runner = createRunner(__dirname)
// First envelope: error event from Hono error handler
.expect(
eventEnvelope(
{
level: 'error',
transaction: 'GET /error',
exception: {
values: [
{
type: 'Error',
value: 'Test error from Hono app',
stacktrace: {
frames: expect.any(Array),
},
mechanism: { type: 'auto.faas.hono.error_handler', handled: false },
},
],
},
request: {
headers: expect.any(Object),
method: 'GET',
url: expect.any(String),
},
},
true,
),
)
// Second envelope: transaction event
.expect(envelope => {
const transactionEvent = envelope[1]?.[0]?.[1];
expect(transactionEvent).toEqual(
expect.objectContaining({
type: 'transaction',
transaction: 'GET /error',
contexts: expect.objectContaining({
trace: expect.objectContaining({
op: 'http.server',
status: 'internal_error',
}),
}),
}),
);
})
.start(signal);
await runner.makeRequest('get', '/error', { expectError: true });
await runner.completed();
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"name": "hono-basic-worker",
"compatibility_date": "2025-06-17",
"main": "index.ts",
"compatibility_flags": ["nodejs_compat"]
}

4 changes: 2 additions & 2 deletions packages/cloudflare/src/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,9 @@ export function withSentry<
) {
handler.errorHandler = new Proxy(handler.errorHandler, {
apply(target, thisArg, args) {
const [err] = args;
const [err, context] = args;

getHonoIntegration()?.handleHonoException(err);
getHonoIntegration()?.handleHonoException(err, context);

return Reflect.apply(target, thisArg, args);
},
Expand Down
30 changes: 28 additions & 2 deletions packages/cloudflare/src/integrations/hono.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
import type { IntegrationFn } from '@sentry/core';
import { captureException, debug, defineIntegration, getClient } from '@sentry/core';
import {
captureException,
debug,
defineIntegration,
getActiveSpan,
getClient,
getIsolationScope,
getRootSpan,
updateSpanName,
} from '@sentry/core';
import { DEBUG_BUILD } from '../debug-build';

const INTEGRATION_NAME = 'Hono';
Expand All @@ -8,6 +17,11 @@ interface HonoError extends Error {
status?: number;
}

// Minimal type - only exported for tests
export interface HonoContext {
req: { method: string; path?: string };
}

export interface Options {
/**
* Callback method deciding whether error should be captured and sent to Sentry
Expand All @@ -28,10 +42,14 @@ function isHonoError(err: unknown): err is HonoError {
return typeof err === 'object' && err !== null && 'status' in (err as Record<string, unknown>);
}

// Vendored from https://github.com/honojs/hono/blob/d3abeb1f801aaa1b334285c73da5f5f022dbcadb/src/helper/route/index.ts#L58-L59
const routePath = (c: HonoContext): string => c.req?.path ?? '';

const _honoIntegration = ((options: Partial<Options> = {}) => {
return {
name: INTEGRATION_NAME,
handleHonoException(err: HonoError): void {
// Hono error handler: https://github.com/honojs/hono/blob/d3abeb1f801aaa1b334285c73da5f5f022dbcadb/src/hono-base.ts#L35
handleHonoException(err: HonoError, context: HonoContext): void {
const shouldHandleError = options.shouldHandleError || defaultShouldHandleError;

if (!isHonoError(err)) {
Expand All @@ -40,6 +58,14 @@ const _honoIntegration = ((options: Partial<Options> = {}) => {
}

if (shouldHandleError(err)) {
const activeSpan = getActiveSpan();
if (activeSpan) {
activeSpan.updateName(`${context.req.method} ${routePath(context)}`);
Copy link
Member

Choose a reason for hiding this comment

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

l: "${context.req.method} ${routePath(context)}" is used 3 times. We could use a const at the beginning

updateSpanName(getRootSpan(activeSpan), `${context.req.method} ${routePath(context)}`);
}

getIsolationScope().setTransactionName(`${context.req.method} ${routePath(context)}`);

This comment was marked as outdated.


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);
Expand Down
3 changes: 2 additions & 1 deletion packages/cloudflare/test/handler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1108,7 +1108,8 @@ describe('withSentry', () => {
const errorHandlerResponse = honoApp.errorHandler?.(error);

expect(handleHonoException).toHaveBeenCalledTimes(1);
expect(handleHonoException).toHaveBeenLastCalledWith(error);
// 2nd param is context, which is undefined here
expect(handleHonoException).toHaveBeenLastCalledWith(error, undefined);
expect(errorHandlerResponse?.status).toBe(500);
});

Expand Down
17 changes: 12 additions & 5 deletions packages/cloudflare/test/integrations/hono.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ 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 type { HonoContext } from '../../src/integrations/hono';
import { honoIntegration } from '../../src/integrations/hono';

class FakeClient extends CloudflareClient {
Expand All @@ -10,7 +11,11 @@ class FakeClient extends CloudflareClient {
}
}

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

const sampleContext: HonoContext = {
req: { method: 'GET', path: '/vitest-sample' },
};

describe('Hono integration', () => {
let client: FakeClient;
Expand All @@ -34,7 +39,7 @@ describe('Hono integration', () => {

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

expect(captureExceptionSpy).toHaveBeenCalledTimes(1);
expect(captureExceptionSpy).toHaveBeenLastCalledWith(error, {
Expand All @@ -49,6 +54,7 @@ describe('Hono integration', () => {

(integration as unknown as MockHonoIntegrationType).handleHonoException(
Object.assign(new Error('client err'), { status: 404 }),
sampleContext,
);
expect(captureExceptionSpy).not.toHaveBeenCalled();
});
Expand All @@ -60,6 +66,7 @@ describe('Hono integration', () => {

(integration as unknown as MockHonoIntegrationType).handleHonoException(
Object.assign(new Error('redirect'), { status: 302 }),
sampleContext,
);
expect(captureExceptionSpy).not.toHaveBeenCalled();
});
Expand All @@ -70,7 +77,7 @@ describe('Hono integration', () => {
integration.setupOnce?.();

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

Expand All @@ -79,7 +86,7 @@ describe('Hono integration', () => {
const integration = honoIntegration();
integration.setupOnce?.();

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

Expand All @@ -88,7 +95,7 @@ describe('Hono integration', () => {
const integration = honoIntegration({ shouldHandleError: () => false });
integration.setupOnce?.();

(integration as unknown as MockHonoIntegrationType).handleHonoException(new Error('blocked'));
(integration as unknown as MockHonoIntegrationType).handleHonoException(new Error('blocked'), sampleContext);
expect(captureExceptionSpy).not.toHaveBeenCalled();
});
});
Loading