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
8 changes: 7 additions & 1 deletion dev-packages/bun-integration-tests/expect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,11 @@ export function expectedEvent(event: Event, { sdk }: { sdk: 'bun' | 'hono' }): E

export function eventEnvelope(
event: Event,
{ includeSampleRand = false, sdk = 'bun' }: { includeSampleRand?: boolean; sdk?: 'bun' | 'hono' } = {},
{
includeSampleRand = false,
includeTransaction = true,
sdk = 'bun',
}: { includeSampleRand?: boolean; includeTransaction?: boolean; sdk?: 'bun' | 'hono' } = {},
): Envelope {
return [
{
Expand All @@ -79,11 +83,13 @@ export function eventEnvelope(
environment: event.environment || 'production',
public_key: 'public',
trace_id: UUID_MATCHER,

sample_rate: expect.any(String),
sampled: expect.any(String),
// release is auto-detected from GitHub CI env vars, so only expect it if we know it will be there
...(process.env.GITHUB_SHA ? { release: expect.any(String) } : {}),
...(includeSampleRand && { sample_rand: expect.stringMatching(/^[01](\.\d+)?$/) }),
...(includeTransaction && { transaction: expect.any(String) }),
},
},
[[{ type: 'event' }, expectedEvent(event, { sdk })]],
Expand Down
2 changes: 1 addition & 1 deletion dev-packages/bun-integration-tests/suites/basic/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ it('captures an error thrown in Bun.serve fetch handler', async ({ signal }) =>
url: expect.stringContaining('/error'),
}),
},
{ includeSampleRand: true },
{ includeSampleRand: true, includeTransaction: false },
),
)
.ignore('transaction')
Expand Down
31 changes: 31 additions & 0 deletions dev-packages/bun-integration-tests/suites/hono-sdk/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { sentry } from '@sentry/hono/bun';
import { Hono } from 'hono';

const app = new Hono();

app.use(
sentry(app, {
dsn: process.env.SENTRY_DSN,
tracesSampleRate: 1.0,
}),
);

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

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

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

const server = Bun.serve({
port: 0,
fetch: app.fetch,
});

process.send?.(JSON.stringify({ event: 'READY', port: server.port }));
131 changes: 131 additions & 0 deletions dev-packages/bun-integration-tests/suites/hono-sdk/test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import { expect, it } from 'vitest';
import { eventEnvelope, SHORT_UUID_MATCHER, UUID_MATCHER } from '../../expect';
import { createRunner } from '../../runner';

it('Hono app captures parametrized errors (Hono SDK on Bun)', async ({ signal }) => {
const runner = createRunner(__dirname)
.expect(envelope => {
const [, envelopeItems] = envelope;
const [itemHeader, itemPayload] = envelopeItems[0];

expect(itemHeader.type).toBe('transaction');

expect(itemPayload).toMatchObject({
type: 'transaction',
platform: 'node',
transaction: 'GET /error/:param',
transaction_info: {
source: 'route',
},
contexts: {
trace: {
span_id: expect.any(String),
trace_id: expect.any(String),
op: 'http.server',
status: 'internal_error',
origin: 'auto.http.bun.serve',
},
response: {
status_code: 500,
},
},
request: expect.objectContaining({
method: 'GET',
url: expect.stringContaining('/error/param-123'),
}),
breadcrumbs: [
{
timestamp: expect.any(Number),
category: 'console',
level: 'error',
message: 'Error: Test error from Hono app',
data: expect.objectContaining({
logger: 'console',
arguments: [{ message: 'Test error from Hono app', name: 'Error', stack: expect.any(String) }],
}),
},
],
});
})

.expect(
eventEnvelope(
{
level: 'error',
transaction: 'GET /error/:param',
exception: {
values: [
{
type: 'Error',
value: 'Test error from Hono app',
stacktrace: {
frames: expect.any(Array),
},
mechanism: { type: 'auto.http.hono.context_error', handled: false },
},
],
},
request: {
cookies: {},
headers: expect.any(Object),
method: 'GET',
url: expect.stringContaining('/error/param-123'),
},
breadcrumbs: [
{
timestamp: expect.any(Number),
category: 'console',
level: 'error',
message: 'Error: Test error from Hono app',
data: expect.objectContaining({
logger: 'console',
arguments: [{ message: 'Test error from Hono app', name: 'Error', stack: expect.any(String) }],
}),
},
],
},
{ sdk: 'hono', includeSampleRand: true, includeTransaction: true },
),
)
.unordered()
.start(signal);

await runner.makeRequest('get', '/error/param-123', { expectError: true });
await runner.completed();
});

it('Hono app captures parametrized route names on Bun', async ({ signal }) => {
const runner = createRunner(__dirname)
.expect(envelope => {
const [, envelopeItems] = envelope;
const [itemHeader, itemPayload] = envelopeItems[0];

expect(itemHeader.type).toBe('transaction');

expect(itemPayload).toMatchObject({
type: 'transaction',
platform: 'node',
transaction: 'GET /hello/:name',
transaction_info: {
source: 'route',
},
contexts: {
trace: {
span_id: SHORT_UUID_MATCHER,
trace_id: UUID_MATCHER,
op: 'http.server',
status: 'ok',
origin: 'auto.http.bun.serve',
},
},
request: expect.objectContaining({
method: 'GET',
url: expect.stringContaining('/hello/world'),
}),
});
})
.start(signal);

await runner.makeRequest('get', '/hello/world');
await runner.completed();
});
14 changes: 14 additions & 0 deletions packages/hono/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,16 @@
"types": "./build/types/index.node.d.ts",
"default": "./build/cjs/index.node.js"
}
},
"./bun": {
"import": {
"types": "./build/types/index.bun.d.ts",
"default": "./build/esm/index.bun.js"
},
"require": {
"types": "./build/types/index.bun.d.ts",
"default": "./build/cjs/index.bun.js"
}
}
},
"typesVersions": {
Expand All @@ -58,6 +68,9 @@
],
"build/types/index.node.d.ts": [
"build/types-ts3.8/index.node.d.ts"
],
"build/types/index.bun.d.ts": [
"build/types-ts3.8/index.bun.d.ts"
]
}
},
Expand All @@ -66,6 +79,7 @@
},
"dependencies": {
"@opentelemetry/api": "^1.9.1",
"@sentry/bun": "10.49.0",
"@sentry/cloudflare": "10.49.0",
"@sentry/core": "10.49.0",
"@sentry/node": "10.49.0"
Expand Down
2 changes: 1 addition & 1 deletion packages/hono/rollup.npm.config.mjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { makeBaseNPMConfig, makeNPMConfigVariants } from '@sentry-internal/rollup-utils';

const baseConfig = makeBaseNPMConfig({
entrypoints: ['src/index.ts', 'src/index.cloudflare.ts', 'src/index.node.ts'],
entrypoints: ['src/index.ts', 'src/index.cloudflare.ts', 'src/index.node.ts', 'src/index.bun.ts'],
packageSpecificConfig: {
output: {
preserveModulesRoot: 'src',
Expand Down
28 changes: 28 additions & 0 deletions packages/hono/src/bun/middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { type BaseTransportOptions, debug, type Options } from '@sentry/core';
import { init } from './sdk';
import type { Hono, MiddlewareHandler } from 'hono';
import { patchAppUse } from '../shared/patchAppUse';
import { requestHandler, responseHandler } from '../shared/middlewareHandlers';

export interface HonoBunOptions extends Options<BaseTransportOptions> {}

/**
* Sentry middleware for Hono running in a Bun runtime environment.
*/
export const sentry = (app: Hono, options: HonoBunOptions | undefined = {}): MiddlewareHandler => {
const isDebug = options.debug;

isDebug && debug.log('Initialized Sentry Hono middleware (Bun)');

init(options);

patchAppUse(app);

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

await next(); // Handler runs in between Request above ⤴ and Response below ⤵

responseHandler(context);
};
};
24 changes: 24 additions & 0 deletions packages/hono/src/bun/sdk.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type { Client } from '@sentry/core';
import { applySdkMetadata } from '@sentry/core';
import { init as initBun } from '@sentry/bun';
import type { HonoBunOptions } from './middleware';
import { buildFilteredIntegrations } from '../shared/buildFilteredIntegrations';

/**
* Initializes Sentry for Hono running in a Bun runtime environment.
*
* In general, it is recommended to initialize Sentry via the `sentry()` middleware, as it sets up everything by default and calls `init` internally.
*
* When manually calling `init`, add the `honoIntegration` to the `integrations` array to set up the Hono integration.
*/
export function init(options: HonoBunOptions): Client | undefined {
applySdkMetadata(options, 'hono', ['hono', 'bun']);

// Remove Hono from the SDK defaults to prevent double instrumentation: @sentry/bun
const filteredOptions: HonoBunOptions = {
...options,
integrations: buildFilteredIntegrations(options.integrations, false),
};

return initBun(filteredOptions);
}
Comment thread
cursor[bot] marked this conversation as resolved.
15 changes: 3 additions & 12 deletions packages/hono/src/cloudflare/middleware.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { withSentry } from '@sentry/cloudflare';
import { applySdkMetadata, type BaseTransportOptions, debug, getIntegrationsToSetup, type Options } from '@sentry/core';
import { applySdkMetadata, type BaseTransportOptions, debug, type Options } from '@sentry/core';
import type { Env, Hono, MiddlewareHandler } from 'hono';
import { buildFilteredIntegrations } from '../shared/buildFilteredIntegrations';
import { requestHandler, responseHandler } from '../shared/middlewareHandlers';
import { patchAppUse } from '../shared/patchAppUse';
import { filterHonoIntegration } from '../shared/filterHonoIntegration';

export interface HonoCloudflareOptions extends Options<BaseTransportOptions> {}

Expand All @@ -22,20 +22,11 @@ export function sentry<E extends Env>(

honoOptions.debug && debug.log('Initialized Sentry Hono middleware (Cloudflare)');

const { integrations: userIntegrations } = honoOptions;
return {
...honoOptions,
// Always filter out the Hono integration from defaults and user integrations.
// 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.
integrations: Array.isArray(userIntegrations)
? defaults =>
getIntegrationsToSetup({
defaultIntegrations: defaults.filter(filterHonoIntegration),
integrations: userIntegrations.filter(filterHonoIntegration),
})
: typeof userIntegrations === 'function'
? defaults => userIntegrations(defaults).filter(filterHonoIntegration)
: defaults => defaults.filter(filterHonoIntegration),
integrations: buildFilteredIntegrations(honoOptions.integrations, true),
};
},
// Cast needed because Hono<E> exposes a narrower fetch signature than ExportedHandler<unknown>
Expand Down
5 changes: 5 additions & 0 deletions packages/hono/src/index.bun.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export { sentry } from './bun/middleware';

export * from '@sentry/bun';

export { init } from './bun/sdk';
18 changes: 4 additions & 14 deletions packages/hono/src/node/sdk.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import type { Client, Integration } from '@sentry/core';
import { applySdkMetadata, getIntegrationsToSetup } from '@sentry/core';
import type { Client } from '@sentry/core';
import { applySdkMetadata } from '@sentry/core';
import { init as initNode } from '@sentry/node';
import type { HonoNodeOptions } from './middleware';
import { filterHonoIntegration } from '../shared/filterHonoIntegration';
import { buildFilteredIntegrations } from '../shared/buildFilteredIntegrations';

/**
* Initializes Sentry for Hono running in a Node runtime environment.
Expand All @@ -14,20 +14,10 @@ import { filterHonoIntegration } from '../shared/filterHonoIntegration';
export function init(options: HonoNodeOptions): Client | undefined {
applySdkMetadata(options, 'hono', ['hono', 'node']);

const { integrations: userIntegrations } = options;

// Remove Hono from the SDK defaults to prevent double instrumentation: @sentry/node
const filteredOptions: HonoNodeOptions = {
...options,
integrations: Array.isArray(userIntegrations)
? (defaults: Integration[]) =>
getIntegrationsToSetup({
defaultIntegrations: defaults.filter(filterHonoIntegration),
integrations: userIntegrations, // user's explicit Hono integration is preserved
})
: typeof userIntegrations === 'function'
? (defaults: Integration[]) => userIntegrations(defaults.filter(filterHonoIntegration))
: (defaults: Integration[]) => defaults.filter(filterHonoIntegration),
integrations: buildFilteredIntegrations(options.integrations, false),
};

return initNode(filteredOptions);
Expand Down
Loading
Loading