Skip to content

Commit 3fda84d

Browse files
authored
fix(cloudflare): Add hono transaction name when error is thrown (#18529)
If the error is caught with the hono error handler, no `trace` data was sent alongside the event. This PR fixed this. Before: <img width="216" height="80" alt="image" src="https://github.com/user-attachments/assets/35bc99b7-4a59-40e3-aad8-10130f816d68" /> After: <img width="248" height="85" alt="image" src="https://github.com/user-attachments/assets/dbc89577-0832-4166-8532-5210dc5e5d99" />
1 parent a538901 commit 3fda84d

File tree

10 files changed

+159
-16
lines changed

10 files changed

+159
-16
lines changed

dev-packages/cloudflare-integration-tests/expect.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ export function expectedEvent(event: Event): Event {
5858
});
5959
}
6060

61-
export function eventEnvelope(event: Event): Envelope {
61+
export function eventEnvelope(event: Event, includeSampleRand = false): Envelope {
6262
return [
6363
{
6464
event_id: UUID_MATCHER,
@@ -69,6 +69,7 @@ export function eventEnvelope(event: Event): Envelope {
6969
public_key: 'public',
7070
trace_id: UUID_MATCHER,
7171
sample_rate: expect.any(String),
72+
...(includeSampleRand && { sample_rand: expect.stringMatching(/^[01](\.\d+)?$/) }),
7273
sampled: expect.any(String),
7374
transaction: expect.any(String),
7475
},

dev-packages/cloudflare-integration-tests/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414
},
1515
"dependencies": {
1616
"@langchain/langgraph": "^1.0.1",
17-
"@sentry/cloudflare": "10.32.0"
17+
"@sentry/cloudflare": "10.32.0",
18+
"hono": "^4.0.0"
1819
},
1920
"devDependencies": {
2021
"@cloudflare/workers-types": "^4.20250922.0",
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import * as Sentry from '@sentry/cloudflare';
2+
import { Hono } from 'hono';
3+
4+
interface Env {
5+
SENTRY_DSN: string;
6+
}
7+
8+
const app = new Hono<{ Bindings: Env }>();
9+
10+
app.get('/', c => {
11+
return c.text('Hello from Hono on Cloudflare!');
12+
});
13+
14+
app.get('/json', c => {
15+
return c.json({ message: 'Hello from Hono', framework: 'hono', platform: 'cloudflare' });
16+
});
17+
18+
app.get('/error', () => {
19+
throw new Error('Test error from Hono app');
20+
});
21+
22+
app.get('/hello/:name', c => {
23+
const name = c.req.param('name');
24+
return c.text(`Hello, ${name}!`);
25+
});
26+
27+
export default Sentry.withSentry(
28+
(env: Env) => ({
29+
dsn: env.SENTRY_DSN,
30+
tracesSampleRate: 1.0,
31+
}),
32+
app,
33+
);
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { expect, it } from 'vitest';
2+
import { eventEnvelope } from '../../../expect';
3+
import { createRunner } from '../../../runner';
4+
5+
it('Hono app captures errors', async ({ signal }) => {
6+
const runner = createRunner(__dirname)
7+
// First envelope: error event from Hono error handler
8+
.expect(
9+
eventEnvelope(
10+
{
11+
level: 'error',
12+
transaction: 'GET /error',
13+
exception: {
14+
values: [
15+
{
16+
type: 'Error',
17+
value: 'Test error from Hono app',
18+
stacktrace: {
19+
frames: expect.any(Array),
20+
},
21+
mechanism: { type: 'auto.faas.hono.error_handler', handled: false },
22+
},
23+
],
24+
},
25+
request: {
26+
headers: expect.any(Object),
27+
method: 'GET',
28+
url: expect.any(String),
29+
},
30+
},
31+
true,
32+
),
33+
)
34+
// Second envelope: transaction event
35+
.expect(envelope => {
36+
const transactionEvent = envelope[1]?.[0]?.[1];
37+
expect(transactionEvent).toEqual(
38+
expect.objectContaining({
39+
type: 'transaction',
40+
transaction: 'GET /error',
41+
contexts: expect.objectContaining({
42+
trace: expect.objectContaining({
43+
op: 'http.server',
44+
status: 'internal_error',
45+
}),
46+
}),
47+
}),
48+
);
49+
})
50+
.start(signal);
51+
await runner.makeRequest('get', '/error', { expectError: true });
52+
await runner.completed();
53+
});
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"name": "hono-basic-worker",
3+
"compatibility_date": "2025-06-17",
4+
"main": "index.ts",
5+
"compatibility_flags": ["nodejs_compat"]
6+
}
7+

packages/cloudflare/src/handler.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,9 @@ export function withSentry<
6767
) {
6868
handler.errorHandler = new Proxy(handler.errorHandler, {
6969
apply(target, thisArg, args) {
70-
const [err] = args;
70+
const [err, context] = args;
7171

72-
getHonoIntegration()?.handleHonoException(err);
72+
getHonoIntegration()?.handleHonoException(err, context);
7373

7474
return Reflect.apply(target, thisArg, args);
7575
},

packages/cloudflare/src/integrations/hono.ts

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
import type { IntegrationFn } from '@sentry/core';
2-
import { captureException, debug, defineIntegration, getClient } from '@sentry/core';
2+
import {
3+
captureException,
4+
debug,
5+
defineIntegration,
6+
getActiveSpan,
7+
getClient,
8+
getIsolationScope,
9+
getRootSpan,
10+
updateSpanName,
11+
} from '@sentry/core';
312
import { DEBUG_BUILD } from '../debug-build';
413

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

20+
// Minimal type - only exported for tests
21+
export interface HonoContext {
22+
req: { method: string; path?: string };
23+
}
24+
1125
export interface Options {
1226
/**
1327
* Callback method deciding whether error should be captured and sent to Sentry
@@ -28,10 +42,14 @@ function isHonoError(err: unknown): err is HonoError {
2842
return typeof err === 'object' && err !== null && 'status' in (err as Record<string, unknown>);
2943
}
3044

45+
// Vendored from https://github.com/honojs/hono/blob/d3abeb1f801aaa1b334285c73da5f5f022dbcadb/src/helper/route/index.ts#L58-L59
46+
const routePath = (c: HonoContext): string => c.req?.path ?? '';
47+
3148
const _honoIntegration = ((options: Partial<Options> = {}) => {
3249
return {
3350
name: INTEGRATION_NAME,
34-
handleHonoException(err: HonoError): void {
51+
// Hono error handler: https://github.com/honojs/hono/blob/d3abeb1f801aaa1b334285c73da5f5f022dbcadb/src/hono-base.ts#L35
52+
handleHonoException(err: HonoError, context: HonoContext): void {
3553
const shouldHandleError = options.shouldHandleError || defaultShouldHandleError;
3654

3755
if (!isHonoError(err)) {
@@ -40,6 +58,18 @@ const _honoIntegration = ((options: Partial<Options> = {}) => {
4058
}
4159

4260
if (shouldHandleError(err)) {
61+
if (context) {
62+
const activeSpan = getActiveSpan();
63+
const spanName = `${context.req.method} ${routePath(context)}`;
64+
65+
if (activeSpan) {
66+
activeSpan.updateName(spanName);
67+
updateSpanName(getRootSpan(activeSpan), spanName);
68+
}
69+
70+
getIsolationScope().setTransactionName(spanName);
71+
}
72+
4373
captureException(err, { mechanism: { handled: false, type: 'auto.faas.hono.error_handler' } });
4474
} else {
4575
DEBUG_BUILD && debug.log('[Hono] Not capturing exception because `shouldHandleError` returned `false`.', err);

packages/cloudflare/test/handler.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1108,7 +1108,8 @@ describe('withSentry', () => {
11081108
const errorHandlerResponse = honoApp.errorHandler?.(error);
11091109

11101110
expect(handleHonoException).toHaveBeenCalledTimes(1);
1111-
expect(handleHonoException).toHaveBeenLastCalledWith(error);
1111+
// 2nd param is context, which is undefined here
1112+
expect(handleHonoException).toHaveBeenLastCalledWith(error, undefined);
11121113
expect(errorHandlerResponse?.status).toBe(500);
11131114
});
11141115

packages/cloudflare/test/integrations/hono.test.ts

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import * as sentryCore from '@sentry/core';
22
import { type Client, createStackParser } from '@sentry/core';
33
import { beforeEach, describe, expect, it, vi } from 'vitest';
44
import { CloudflareClient } from '../../src/client';
5+
import type { HonoContext } from '../../src/integrations/hono';
56
import { honoIntegration } from '../../src/integrations/hono';
67

78
class FakeClient extends CloudflareClient {
@@ -10,7 +11,11 @@ class FakeClient extends CloudflareClient {
1011
}
1112
}
1213

13-
type MockHonoIntegrationType = { handleHonoException: (err: Error) => void };
14+
type MockHonoIntegrationType = { handleHonoException: (err: Error, ctx: HonoContext) => void };
15+
16+
const sampleContext: HonoContext = {
17+
req: { method: 'GET', path: '/vitest-sample' },
18+
};
1419

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

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

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

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

6167
(integration as unknown as MockHonoIntegrationType).handleHonoException(
6268
Object.assign(new Error('redirect'), { status: 302 }),
69+
sampleContext,
6370
);
6471
expect(captureExceptionSpy).not.toHaveBeenCalled();
6572
});
@@ -70,7 +77,7 @@ describe('Hono integration', () => {
7077
integration.setupOnce?.();
7178

7279
const err = Object.assign(new Error('server err'), { status: 500 });
73-
(integration as unknown as MockHonoIntegrationType).handleHonoException(err);
80+
(integration as unknown as MockHonoIntegrationType).handleHonoException(err, sampleContext);
7481
expect(captureExceptionSpy).toHaveBeenCalledTimes(1);
7582
});
7683

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

82-
(integration as unknown as MockHonoIntegrationType).handleHonoException(new Error('no status'));
89+
(integration as unknown as MockHonoIntegrationType).handleHonoException(new Error('no status'), sampleContext);
8390
expect(captureExceptionSpy).toHaveBeenCalledTimes(1);
8491
});
8592

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

91-
(integration as unknown as MockHonoIntegrationType).handleHonoException(new Error('blocked'));
98+
(integration as unknown as MockHonoIntegrationType).handleHonoException(new Error('blocked'), sampleContext);
9299
expect(captureExceptionSpy).not.toHaveBeenCalled();
93100
});
101+
102+
it('does not throw error without passed context and still captures', () => {
103+
const captureExceptionSpy = vi.spyOn(sentryCore, 'captureException');
104+
const integration = honoIntegration();
105+
integration.setupOnce?.();
106+
107+
// @ts-expect-error context is not passed
108+
(integration as unknown as MockHonoIntegrationType).handleHonoException(new Error());
109+
expect(captureExceptionSpy).toHaveBeenCalledTimes(1);
110+
});
94111
});

yarn.lock

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18848,10 +18848,10 @@ homedir-polyfill@^1.0.1:
1884818848
dependencies:
1884918849
parse-passwd "^1.0.0"
1885018850

18851-
hono@^4.9.8:
18852-
version "4.9.8"
18853-
resolved "https://registry.yarnpkg.com/hono/-/hono-4.9.8.tgz#1710981135ec775fe26fab5ea6535b403e92bcc3"
18854-
integrity sha512-JW8Bb4RFWD9iOKxg5PbUarBYGM99IcxFl2FPBo2gSJO11jjUDqlP1Bmfyqt8Z/dGhIQ63PMA9LdcLefXyIasyg==
18851+
hono@^4.0.0, hono@^4.9.8:
18852+
version "4.11.1"
18853+
resolved "https://registry.yarnpkg.com/hono/-/hono-4.11.1.tgz#cb1b0c045fc74a96c693927234c95a45fb46ab0b"
18854+
integrity sha512-KsFcH0xxHes0J4zaQgWbYwmz3UPOOskdqZmItstUG93+Wk1ePBLkLGwbP9zlmh1BFUiL8Qp+Xfu9P7feJWpGNg==
1885518855

1885618856
hookable@^5.5.3:
1885718857
version "5.5.3"

0 commit comments

Comments
 (0)