Skip to content

Commit 141bfa9

Browse files
committed
feat(hono): Add @sentry/hono/bun for Bun runtime
1 parent 5f72df5 commit 141bfa9

10 files changed

Lines changed: 510 additions & 3 deletions

File tree

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,11 @@ export function expectedEvent(event: Event, { sdk }: { sdk: 'bun' | 'hono' }): E
6868

6969
export function eventEnvelope(
7070
event: Event,
71-
{ includeSampleRand = false, sdk = 'bun' }: { includeSampleRand?: boolean; sdk?: 'bun' | 'hono' } = {},
71+
{
72+
includeSampleRand = false,
73+
includeTransaction = true,
74+
sdk = 'bun',
75+
}: { includeSampleRand?: boolean; includeTransaction?: boolean; sdk?: 'bun' | 'hono' } = {},
7276
): Envelope {
7377
return [
7478
{
@@ -79,11 +83,13 @@ export function eventEnvelope(
7983
environment: event.environment || 'production',
8084
public_key: 'public',
8185
trace_id: UUID_MATCHER,
86+
8287
sample_rate: expect.any(String),
8388
sampled: expect.any(String),
8489
// release is auto-detected from GitHub CI env vars, so only expect it if we know it will be there
8590
...(process.env.GITHUB_SHA ? { release: expect.any(String) } : {}),
8691
...(includeSampleRand && { sample_rand: expect.stringMatching(/^[01](\.\d+)?$/) }),
92+
...(includeTransaction && { transaction: expect.any(String) }),
8793
},
8894
},
8995
[[{ type: 'event' }, expectedEvent(event, { sdk })]],

dev-packages/bun-integration-tests/suites/basic/test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ it('captures an error thrown in Bun.serve fetch handler', async ({ signal }) =>
2525
url: expect.stringContaining('/error'),
2626
}),
2727
},
28-
{ includeSampleRand: true },
28+
{ includeSampleRand: true, includeTransaction: false },
2929
),
3030
)
3131
.ignore('transaction')
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { sentry } from '@sentry/hono/bun';
2+
import { Hono } from 'hono';
3+
4+
const app = new Hono();
5+
6+
app.use(
7+
sentry(app, {
8+
dsn: process.env.SENTRY_DSN,
9+
tracesSampleRate: 1.0,
10+
}),
11+
);
12+
13+
app.get('/', c => {
14+
return c.text('Hello from Hono on Bun!');
15+
});
16+
17+
app.get('/hello/:name', c => {
18+
const name = c.req.param('name');
19+
return c.text(`Hello, ${name}!`);
20+
});
21+
22+
app.get('/error/:param', () => {
23+
throw new Error('Test error from Hono app');
24+
});
25+
26+
const server = Bun.serve({
27+
port: 0,
28+
fetch: app.fetch,
29+
});
30+
31+
process.send?.(JSON.stringify({ event: 'READY', port: server.port }));
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import { expect, it } from 'vitest';
2+
import { eventEnvelope, SHORT_UUID_MATCHER, UUID_MATCHER } from '../../expect';
3+
import { createRunner } from '../../runner';
4+
5+
it('Hono app captures parametrized errors (Hono SDK on Bun)', async ({ signal }) => {
6+
const runner = createRunner(__dirname)
7+
.expect(envelope => {
8+
const [, envelopeItems] = envelope;
9+
const [itemHeader, itemPayload] = envelopeItems[0];
10+
11+
expect(itemHeader.type).toBe('transaction');
12+
13+
expect(itemPayload).toMatchObject({
14+
type: 'transaction',
15+
platform: 'node',
16+
transaction: 'GET /error/:param',
17+
transaction_info: {
18+
source: 'route',
19+
},
20+
contexts: {
21+
trace: {
22+
span_id: expect.any(String),
23+
trace_id: expect.any(String),
24+
op: 'http.server',
25+
status: 'internal_error',
26+
origin: 'auto.http.bun.serve',
27+
},
28+
response: {
29+
status_code: 500,
30+
},
31+
},
32+
request: expect.objectContaining({
33+
method: 'GET',
34+
url: expect.stringContaining('/error/param-123'),
35+
}),
36+
breadcrumbs: [
37+
{
38+
timestamp: expect.any(Number),
39+
category: 'console',
40+
level: 'error',
41+
message: 'Error: Test error from Hono app',
42+
data: expect.objectContaining({
43+
logger: 'console',
44+
arguments: [{ message: 'Test error from Hono app', name: 'Error', stack: expect.any(String) }],
45+
}),
46+
},
47+
],
48+
});
49+
})
50+
51+
.expect(
52+
eventEnvelope(
53+
{
54+
level: 'error',
55+
transaction: 'GET /error/:param',
56+
exception: {
57+
values: [
58+
{
59+
type: 'Error',
60+
value: 'Test error from Hono app',
61+
stacktrace: {
62+
frames: expect.any(Array),
63+
},
64+
mechanism: { type: 'auto.http.hono.context_error', handled: false },
65+
},
66+
],
67+
},
68+
request: {
69+
cookies: {},
70+
headers: expect.any(Object),
71+
method: 'GET',
72+
url: expect.stringContaining('/error/param-123'),
73+
},
74+
breadcrumbs: [
75+
{
76+
timestamp: expect.any(Number),
77+
category: 'console',
78+
level: 'error',
79+
message: 'Error: Test error from Hono app',
80+
data: expect.objectContaining({
81+
logger: 'console',
82+
arguments: [{ message: 'Test error from Hono app', name: 'Error', stack: expect.any(String) }],
83+
}),
84+
},
85+
],
86+
},
87+
{ sdk: 'hono', includeSampleRand: true, includeTransaction: true },
88+
),
89+
)
90+
.unordered()
91+
.start(signal);
92+
93+
await runner.makeRequest('get', '/error/param-123', { expectError: true });
94+
await runner.completed();
95+
});
96+
97+
it('Hono app captures parametrized route names on Bun', async ({ signal }) => {
98+
const runner = createRunner(__dirname)
99+
.expect(envelope => {
100+
const [, envelopeItems] = envelope;
101+
const [itemHeader, itemPayload] = envelopeItems[0];
102+
103+
expect(itemHeader.type).toBe('transaction');
104+
105+
expect(itemPayload).toMatchObject({
106+
type: 'transaction',
107+
platform: 'node',
108+
transaction: 'GET /hello/:name',
109+
transaction_info: {
110+
source: 'route',
111+
},
112+
contexts: {
113+
trace: {
114+
span_id: SHORT_UUID_MATCHER,
115+
trace_id: UUID_MATCHER,
116+
op: 'http.server',
117+
status: 'ok',
118+
origin: 'auto.http.bun.serve',
119+
},
120+
},
121+
request: expect.objectContaining({
122+
method: 'GET',
123+
url: expect.stringContaining('/hello/world'),
124+
}),
125+
});
126+
})
127+
.start(signal);
128+
129+
await runner.makeRequest('get', '/hello/world');
130+
await runner.completed();
131+
});

packages/hono/package.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,16 @@
4646
"types": "./build/types/index.node.d.ts",
4747
"default": "./build/cjs/index.node.js"
4848
}
49+
},
50+
"./bun": {
51+
"import": {
52+
"types": "./build/types/index.bun.d.ts",
53+
"default": "./build/esm/index.bun.js"
54+
},
55+
"require": {
56+
"types": "./build/types/index.bun.d.ts",
57+
"default": "./build/cjs/index.bun.js"
58+
}
4959
}
5060
},
5161
"typesVersions": {
@@ -58,6 +68,9 @@
5868
],
5969
"build/types/index.node.d.ts": [
6070
"build/types-ts3.8/index.node.d.ts"
71+
],
72+
"build/types/index.bun.d.ts": [
73+
"build/types-ts3.8/index.bun.d.ts"
6174
]
6275
}
6376
},
@@ -66,6 +79,7 @@
6679
},
6780
"dependencies": {
6881
"@opentelemetry/api": "^1.9.1",
82+
"@sentry/bun": "10.48.0",
6983
"@sentry/cloudflare": "10.48.0",
7084
"@sentry/core": "10.48.0",
7185
"@sentry/node": "10.48.0"

packages/hono/rollup.npm.config.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { makeBaseNPMConfig, makeNPMConfigVariants } from '@sentry-internal/rollup-utils';
22

33
const baseConfig = makeBaseNPMConfig({
4-
entrypoints: ['src/index.ts', 'src/index.cloudflare.ts', 'src/index.node.ts'],
4+
entrypoints: ['src/index.ts', 'src/index.cloudflare.ts', 'src/index.node.ts', 'src/index.bun.ts'],
55
packageSpecificConfig: {
66
output: {
77
preserveModulesRoot: 'src',
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { type BaseTransportOptions, debug, type Options } from '@sentry/core';
2+
import { init } from './sdk';
3+
import type { Hono, MiddlewareHandler } from 'hono';
4+
import { patchAppUse } from '../shared/patchAppUse';
5+
import { requestHandler, responseHandler } from '../shared/middlewareHandlers';
6+
7+
export interface HonoBunOptions extends Options<BaseTransportOptions> {}
8+
9+
/**
10+
* Sentry middleware for Hono running in a Bun runtime environment.
11+
*/
12+
export const sentry = (app: Hono, options: HonoBunOptions | undefined = {}): MiddlewareHandler => {
13+
const isDebug = options.debug;
14+
15+
isDebug && debug.log('Initialized Sentry Hono middleware (Bun)');
16+
17+
init(options);
18+
19+
patchAppUse(app);
20+
21+
return async (context, next) => {
22+
requestHandler(context);
23+
24+
await next(); // Handler runs in between Request above ⤴ and Response below ⤵
25+
26+
responseHandler(context);
27+
};
28+
};

packages/hono/src/bun/sdk.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import type { Client, Integration } from '@sentry/core';
2+
import { applySdkMetadata, getIntegrationsToSetup } from '@sentry/core';
3+
import { init as initBun } from '@sentry/bun';
4+
import type { HonoBunOptions } from './middleware';
5+
import { filterHonoIntegration } from '../shared/filterHonoIntegration';
6+
7+
/**
8+
* Initializes Sentry for Hono running in a Bun runtime environment.
9+
*
10+
* In general, it is recommended to initialize Sentry via the `sentry()` middleware, as it sets up everything by default and calls `init` internally.
11+
*
12+
* When manually calling `init`, add the `honoIntegration` to the `integrations` array to set up the Hono integration.
13+
*/
14+
export function init(options: HonoBunOptions): Client | undefined {
15+
applySdkMetadata(options, 'hono', ['hono', 'bun']);
16+
17+
const { integrations: userIntegrations } = options;
18+
19+
// Remove Hono from the SDK defaults to prevent double instrumentation: @sentry/bun
20+
const filteredOptions: HonoBunOptions = {
21+
...options,
22+
integrations: Array.isArray(userIntegrations)
23+
? (defaults: Integration[]) =>
24+
getIntegrationsToSetup({
25+
defaultIntegrations: defaults.filter(filterHonoIntegration),
26+
integrations: userIntegrations, // user's explicit Hono integration is preserved
27+
})
28+
: typeof userIntegrations === 'function'
29+
? (defaults: Integration[]) => userIntegrations(defaults.filter(filterHonoIntegration))
30+
: (defaults: Integration[]) => defaults.filter(filterHonoIntegration),
31+
};
32+
33+
return initBun(filteredOptions);
34+
}

packages/hono/src/index.bun.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export { sentry } from './bun/middleware';
2+
3+
export * from '@sentry/bun';
4+
5+
export { init } from './bun/sdk';

0 commit comments

Comments
 (0)