Skip to content

Commit 5767e4a

Browse files
authored
feat(hono): Use parametrized names for errors (#19577)
The SDK now only uses the hono integration for error capturing. Before, the Hono integration from the Cloudflare SDK wrapping was used which caused unparametrized transaction names. Addtionally, the mechanism `auto.faas.hono.error_handler` was added to the error. Closes #19578 (added automatically)
1 parent cca214a commit 5767e4a

5 files changed

Lines changed: 162 additions & 14 deletions

File tree

dev-packages/cloudflare-integration-tests/suites/hono-sdk/index.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,6 @@ app.use(
1212
sentry(app, {
1313
dsn: process.env.SENTRY_DSN,
1414
tracesSampleRate: 1.0,
15-
debug: true,
16-
// fixme - check out what removing this integration changes
17-
// integrations: integrations => integrations.filter(integration => integration.name !== 'Hono'),
1815
}),
1916
);
2017

@@ -26,7 +23,7 @@ app.get('/json', c => {
2623
return c.json({ message: 'Hello from Hono', framework: 'hono', platform: 'cloudflare' });
2724
});
2825

29-
app.get('/error', () => {
26+
app.get('/error/:param', () => {
3027
throw new Error('Test error from Hono app');
3128
});
3229

dev-packages/cloudflare-integration-tests/suites/hono-sdk/test.ts

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@ import { expect, it } from 'vitest';
22
import { eventEnvelope, SHORT_UUID_MATCHER, UUID_MATCHER } from '../../expect';
33
import { createRunner } from '../../runner';
44

5-
it('Hono app captures errors (Hono SDK)', async ({ signal }) => {
5+
it('Hono app captures parametrized errors (Hono SDK)', async ({ signal }) => {
66
const runner = createRunner(__dirname)
77
.expect(
88
eventEnvelope(
99
{
1010
level: 'error',
11-
transaction: 'GET /error',
11+
transaction: 'GET /error/:param',
1212
exception: {
1313
values: [
1414
{
@@ -24,12 +24,25 @@ it('Hono app captures errors (Hono SDK)', async ({ signal }) => {
2424
request: {
2525
headers: expect.any(Object),
2626
method: 'GET',
27-
url: expect.any(String),
27+
url: expect.stringContaining('/error/param-123'),
2828
},
29+
breadcrumbs: [
30+
{
31+
timestamp: expect.any(Number),
32+
category: 'console',
33+
level: 'error',
34+
message: 'Error: Test error from Hono app',
35+
data: expect.objectContaining({
36+
logger: 'console',
37+
arguments: [{ message: 'Test error from Hono app', name: 'Error', stack: expect.any(String) }],
38+
}),
39+
},
40+
],
2941
},
3042
{ includeSampleRand: true, sdk: 'hono' },
3143
),
3244
)
45+
3346
.expect(envelope => {
3447
const [, envelopeItems] = envelope;
3548
const [itemHeader, itemPayload] = envelopeItems[0];
@@ -39,7 +52,7 @@ it('Hono app captures errors (Hono SDK)', async ({ signal }) => {
3952
expect(itemPayload).toMatchObject({
4053
type: 'transaction',
4154
platform: 'javascript',
42-
transaction: 'GET /error',
55+
transaction: 'GET /error/:param',
4356
contexts: {
4457
trace: {
4558
span_id: expect.any(String),
@@ -51,15 +64,26 @@ it('Hono app captures errors (Hono SDK)', async ({ signal }) => {
5164
},
5265
request: expect.objectContaining({
5366
method: 'GET',
54-
url: expect.stringContaining('/error'),
67+
url: expect.stringContaining('/error/param-123'),
5568
}),
69+
breadcrumbs: [
70+
{
71+
timestamp: expect.any(Number),
72+
category: 'console',
73+
level: 'error',
74+
message: 'Error: Test error from Hono app',
75+
data: expect.objectContaining({
76+
logger: 'console',
77+
arguments: [{ message: 'Test error from Hono app', name: 'Error', stack: expect.any(String) }],
78+
}),
79+
},
80+
],
5681
});
5782
})
58-
5983
.unordered()
6084
.start(signal);
6185

62-
await runner.makeRequest('get', '/error', { expectError: true });
86+
await runner.makeRequest('get', '/error/param-123', { expectError: true });
6387
await runner.completed();
6488
});
6589

packages/hono/src/cloudflare/middleware.ts

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,46 @@
11
import { withSentry } from '@sentry/cloudflare';
2-
import { applySdkMetadata, type BaseTransportOptions, debug, type Options } from '@sentry/core';
2+
import {
3+
applySdkMetadata,
4+
type BaseTransportOptions,
5+
debug,
6+
getIntegrationsToSetup,
7+
type Integration,
8+
type Options,
9+
} from '@sentry/core';
310
import type { Context, Hono, MiddlewareHandler } from 'hono';
411
import { requestHandler, responseHandler } from '../shared/middlewareHandlers';
512

613
export interface HonoOptions extends Options<BaseTransportOptions> {
714
context?: Context;
815
}
916

17+
const filterHonoIntegration = (integration: Integration): boolean => integration.name !== 'Hono';
18+
1019
export const sentry = (app: Hono, options: HonoOptions | undefined = {}): MiddlewareHandler => {
1120
const isDebug = options.debug;
1221

1322
isDebug && debug.log('Initialized Sentry Hono middleware (Cloudflare)');
1423

1524
applySdkMetadata(options, 'hono');
16-
withSentry(() => options, app);
25+
26+
const { integrations: userIntegrations } = options;
27+
withSentry(
28+
() => ({
29+
...options,
30+
// Always filter out the Hono integration from defaults and user integrations.
31+
// 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.
32+
integrations: Array.isArray(userIntegrations)
33+
? defaults =>
34+
getIntegrationsToSetup({
35+
defaultIntegrations: defaults.filter(filterHonoIntegration),
36+
integrations: userIntegrations.filter(filterHonoIntegration),
37+
})
38+
: typeof userIntegrations === 'function'
39+
? defaults => userIntegrations(defaults).filter(filterHonoIntegration)
40+
: defaults => defaults.filter(filterHonoIntegration),
41+
}),
42+
app,
43+
);
1744

1845
return async (context, next) => {
1946
requestHandler(context);

packages/hono/src/shared/middlewareHandlers.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ export function responseHandler(context: Context): void {
3838
getIsolationScope().setTransactionName(`${context.req.method} ${routePath(context)}`);
3939

4040
if (context.error) {
41-
getClient()?.captureException(context.error);
41+
getClient()?.captureException(context.error, {
42+
mechanism: { handled: false, type: 'auto.faas.hono.error_handler' },
43+
});
4244
}
4345
}

packages/hono/test/cloudflare/middleware.test.ts

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,4 +125,102 @@ describe('Hono Cloudflare Middleware', () => {
125125
expect(middleware.constructor.name).toBe('AsyncFunction');
126126
});
127127
});
128+
129+
describe('filters Hono integration from user-provided integrations', () => {
130+
const honoIntegration = { name: 'Hono' } as SentryCore.Integration;
131+
const otherIntegration = { name: 'Other' } as SentryCore.Integration;
132+
133+
const getIntegrationsResult = () => {
134+
const optionsCallback = withSentryMock.mock.calls[0]?.[0];
135+
return optionsCallback().integrations;
136+
};
137+
138+
it.each([
139+
['filters Hono integration out', [honoIntegration, otherIntegration], [otherIntegration]],
140+
['keeps non-Hono integrations', [otherIntegration], [otherIntegration]],
141+
['returns empty array when only Hono integration provided', [honoIntegration], []],
142+
])('%s (array)', (_name, input, expected) => {
143+
const app = new Hono();
144+
sentry(app, { integrations: input });
145+
146+
const integrationsFn = getIntegrationsResult() as (
147+
defaults: SentryCore.Integration[],
148+
) => SentryCore.Integration[];
149+
expect(integrationsFn([])).toEqual(expected);
150+
});
151+
152+
it('filters Hono from defaults when user provides an array', () => {
153+
const app = new Hono();
154+
sentry(app, { integrations: [otherIntegration] });
155+
156+
const integrationsFn = getIntegrationsResult() as (
157+
defaults: SentryCore.Integration[],
158+
) => SentryCore.Integration[];
159+
// Defaults (from Cloudflare) include Hono; result must exclude it and deduplicate (user + defaults overlap)
160+
const defaultsWithHono = [honoIntegration, otherIntegration];
161+
expect(integrationsFn(defaultsWithHono)).toEqual([otherIntegration]);
162+
});
163+
164+
it('deduplicates when user integrations overlap with defaults (by name)', () => {
165+
const app = new Hono();
166+
const duplicateIntegration = { name: 'Other' } as SentryCore.Integration;
167+
sentry(app, { integrations: [duplicateIntegration] });
168+
169+
const integrationsFn = getIntegrationsResult() as (
170+
defaults: SentryCore.Integration[],
171+
) => SentryCore.Integration[];
172+
const defaultsWithOverlap = [
173+
honoIntegration,
174+
otherIntegration, // same name as duplicateIntegration
175+
];
176+
const result = integrationsFn(defaultsWithOverlap);
177+
expect(result).toHaveLength(1);
178+
expect(result[0]?.name).toBe('Other');
179+
});
180+
181+
it('filters Hono integration out of a function result', () => {
182+
const app = new Hono();
183+
sentry(app, { integrations: () => [honoIntegration, otherIntegration] });
184+
185+
const integrationsFn = getIntegrationsResult() as unknown as (
186+
defaults: SentryCore.Integration[],
187+
) => SentryCore.Integration[];
188+
expect(integrationsFn([])).toEqual([otherIntegration]);
189+
});
190+
191+
it('passes defaults through to the user-provided integrations function', () => {
192+
const app = new Hono();
193+
const userFn = vi.fn((_defaults: SentryCore.Integration[]) => [otherIntegration]);
194+
const defaults = [{ name: 'Default' } as SentryCore.Integration];
195+
196+
sentry(app, { integrations: userFn });
197+
198+
const integrationsFn = getIntegrationsResult() as unknown as (
199+
defaults: SentryCore.Integration[],
200+
) => SentryCore.Integration[];
201+
integrationsFn(defaults);
202+
203+
expect(userFn).toHaveBeenCalledWith(defaults);
204+
});
205+
206+
it('filters Hono integration returned by the user-provided integrations function', () => {
207+
const app = new Hono();
208+
sentry(app, { integrations: (_defaults: SentryCore.Integration[]) => [honoIntegration] });
209+
210+
const integrationsFn = getIntegrationsResult() as unknown as (
211+
defaults: SentryCore.Integration[],
212+
) => SentryCore.Integration[];
213+
expect(integrationsFn([])).toEqual([]);
214+
});
215+
216+
it('filters Hono integration from defaults when integrations is undefined', () => {
217+
const app = new Hono();
218+
sentry(app, {});
219+
220+
const integrationsFn = getIntegrationsResult() as unknown as (
221+
defaults: SentryCore.Integration[],
222+
) => SentryCore.Integration[];
223+
expect(integrationsFn([honoIntegration, otherIntegration])).toEqual([otherIntegration]);
224+
});
225+
});
128226
});

0 commit comments

Comments
 (0)