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
1 change: 1 addition & 0 deletions dev-packages/node-integration-tests/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"@prisma/client": "6.15.0",
"@sentry/aws-serverless": "10.43.0",
"@sentry/core": "10.43.0",
"@sentry/hono": "10.43.0",
"@sentry/node": "10.43.0",
"@types/mongodb": "^3.6.20",
"@types/mysql": "^2.15.21",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
// Sentry is initialized by the @sentry/hono/node middleware in scenario.mjs
31 changes: 31 additions & 0 deletions dev-packages/node-integration-tests/suites/hono-sdk/scenario.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { serve } from '@hono/node-server';
import { sentry } from '@sentry/hono/node';
import { loggingTransport, sendPortToRunner } from '@sentry-internal/node-integration-tests';
import { Hono } from 'hono';

const app = new Hono();

app.use(
sentry(app, {
dsn: 'https://public@dsn.ingest.sentry.io/1337',
tracesSampleRate: 1.0,
transport: loggingTransport,
}),
);

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

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');
});

serve({ fetch: app.fetch, port: 0 }, info => {
sendPortToRunner(info.port);
});
96 changes: 96 additions & 0 deletions dev-packages/node-integration-tests/suites/hono-sdk/test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { afterAll, describe, expect } from 'vitest';
import { cleanupChildProcesses, createEsmAndCjsTests } from '../../utils/runner';

describe('hono-sdk (Node)', () => {
afterAll(() => {
cleanupChildProcesses();
});

createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createRunner, test) => {
test('creates a transaction for a basic GET request', async () => {
const runner = createRunner()
.expect({
transaction: {
transaction: 'GET /',
contexts: {
trace: {
op: 'http.server',
status: 'ok',
},
},
},
})
.start();
runner.makeRequest('get', '/');
await runner.completed();
});

test('creates a transaction with a parametrized route name', async () => {
const runner = createRunner()
.expect({
transaction: {
transaction: 'GET /hello/:name',
transaction_info: {
source: 'route',
},
contexts: {
trace: {
op: 'http.server',
status: 'ok',
},
},
},
})
.start();
runner.makeRequest('get', '/hello/world');
await runner.completed();
});

test('captures an error with the correct mechanism', async () => {
const runner = createRunner()
.ignore('transaction')
.expect({
event: {
exception: {
values: [
{
type: 'Error',
value: 'Test error from Hono app',
mechanism: {
type: 'auto.faas.hono.error_handler',
handled: false,
},
},
],
},
transaction: 'GET /error/:param',
},
})
.start();
runner.makeRequest('get', '/error/param-123', { expectError: true });
await runner.completed();
});

test('creates a transaction with internal_error status when an error occurs', async () => {
const runner = createRunner()
.ignore('event')
.expect({
transaction: {
transaction: 'GET /error/:param',
contexts: {
trace: {
op: 'http.server',
status: 'internal_error',
data: expect.objectContaining({
'http.response.status_code': 500,
}),
},
},
},
})
.start();
runner.makeRequest('get', '/error/param-456', { expectError: true });
await runner.completed();
});
});
});
46 changes: 45 additions & 1 deletion packages/hono/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ const app = new Hono();
// Initialize Sentry middleware right after creating the app
app.use(
sentry(app, {
dsn: 'your-sentry-dsn',
dsn: '__DSN__',
// ...other Sentry options
}),
);
Expand All @@ -82,3 +82,47 @@ app.use(sentry(app, env => ({ dsn: env.SENTRY_DSN })));

export default app;
```

## Setup (Node)

### 1. Initialize Sentry in your Hono app

Initialize the Sentry Hono middleware as early as possible in your app:

```ts
import { Hono } from 'hono';
import { serve } from '@hono/node-server';
import { sentry } from '@sentry/hono/node';

const app = new Hono();

// Initialize Sentry middleware right after creating the app
app.use(
sentry(app, {
dsn: '__DSN__', // or process.env.SENTRY_DSN
tracesSampleRate: 1.0,
}),
);

// ... your routes and other middleware

serve(app);
```

### 2. Add `preload` script to start command

To ensure that Sentry can capture spans from third-party libraries (e.g. database clients) used in your Hono app, Sentry needs to wrap these libraries as early as possible.

When starting the Hono Node application, use the `@sentry/node/preload` hook with the `--import` CLI option to ensure modules are wrapped before the application code runs:

```bash
node --import @sentry/node/preload index.js
```

This option can also be added to the `NODE_OPTIONS` environment variable:

```bash
NODE_OPTIONS="--import @sentry/node/preload"
```

Read more about this preload script in the docs: https://docs.sentry.io/platforms/javascript/guides/hono/install/late-initialization/#late-initialization-with-esm
13 changes: 13 additions & 0 deletions packages/hono/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,16 @@
"types": "./build/types/index.cloudflare.d.ts",
"default": "./build/cjs/index.cloudflare.js"
}
},
"./node": {
"import": {
"types": "./build/types/index.node.d.ts",
"default": "./build/esm/index.node.js"
},
"require": {
"types": "./build/types/index.node.d.ts",
"default": "./build/cjs/index.node.js"
}
}
},
"typesVersions": {
Expand All @@ -45,6 +55,9 @@
],
"build/types/index.cloudflare.d.ts": [
"build/types-ts3.8/index.cloudflare.d.ts"
],
"build/types/index.node.d.ts": [
"build/types-ts3.8/index.node.d.ts"
]
}
},
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'],
entrypoints: ['src/index.ts', 'src/index.cloudflare.ts', 'src/index.node.ts'],
packageSpecificConfig: {
output: {
preserveModulesRoot: 'src',
Expand Down
16 changes: 4 additions & 12 deletions packages/hono/src/cloudflare/middleware.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,18 @@
import { withSentry } from '@sentry/cloudflare';
import {
applySdkMetadata,
type BaseTransportOptions,
debug,
getIntegrationsToSetup,
type Integration,
type Options,
} from '@sentry/core';
import { applySdkMetadata, type BaseTransportOptions, debug, getIntegrationsToSetup, type Options } from '@sentry/core';
import type { Env, Hono, MiddlewareHandler } from 'hono';
import { requestHandler, responseHandler } from '../shared/middlewareHandlers';
import { patchAppUse } from '../shared/patchAppUse';
import { filterHonoIntegration } from '../shared/filterHonoIntegration';

export interface HonoOptions extends Options<BaseTransportOptions> {}

const filterHonoIntegration = (integration: Integration): boolean => integration.name !== 'Hono';
export interface HonoCloudflareOptions extends Options<BaseTransportOptions> {}

/**
* Sentry middleware for Hono on Cloudflare Workers.
*/
export function sentry<E extends Env>(
app: Hono<E>,
options: HonoOptions | ((env: E['Bindings']) => HonoOptions),
options: HonoCloudflareOptions | ((env: E['Bindings']) => HonoCloudflareOptions),
): MiddlewareHandler {
withSentry(
env => {
Expand Down
5 changes: 5 additions & 0 deletions packages/hono/src/index.node.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export { sentry } from './node/middleware';

export * from '@sentry/node';
Copy link
Member

Choose a reason for hiding this comment

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

l/q: Do you think it would make sense to re-export init and add applySdkMetadata(opts, 'hono', ['hono', 'node']); here on top?

Copy link
Member Author

Choose a reason for hiding this comment

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

We currently don't document this way of setting it up (calling init manually) but that's a good idea!


export { init } from './node/sdk';
28 changes: 28 additions & 0 deletions packages/hono/src/node/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 HonoNodeOptions extends Options<BaseTransportOptions> {}

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

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

init(options);

patchAppUse(app);

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

Choose a reason for hiding this comment

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

Bug: The hasFetchEvent() check incorrectly returns true in Node.js because accessing the undefined c.event property doesn't throw, leading to a TypeError when context.event.request is accessed.
Severity: CRITICAL

Suggested Fix

The hasFetchEvent utility should be updated to correctly identify the Node.js environment. Instead of relying on a try-catch block, it should perform a truthiness check on c.event (e.g., return !!c.event;). This will ensure it returns false for Node.js, causing the code to correctly fall back to using context.req.raw and avoid the TypeError.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location: packages/hono/src/node/middleware.ts#L22

Potential issue: The Node.js Hono middleware will crash on every request due to a
`TypeError`. The shared `requestHandler` calls `hasFetchEvent()` to determine the
runtime environment. This function uses a `try-catch` block around `c.event` access,
assuming an error is thrown if `c.event` is unavailable. However, in the Node.js
environment, accessing `c.event` returns `undefined` without throwing an error. This
causes `hasFetchEvent()` to incorrectly return `true`, leading the subsequent code to
attempt accessing `context.event.request`, which fails because `context.event` is
`undefined`.

Did we get this right? 👍 / 👎 to inform future reviews.


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

responseHandler(context);
};
Copy link

Choose a reason for hiding this comment

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

Wrong faas mechanism type used for Node runtime

Medium Severity

The Node middleware calls shared responseHandler, which uses mechanism type auto.faas.hono.error_handler for captureException. The faas (Function as a Service) prefix is specific to Cloudflare/serverless environments in this codebase. Every other Node framework uses auto.middleware.*, auto.function.*, or auto.http.* (e.g., Express uses auto.middleware.express, Fastify uses auto.function.fastify). By reusing the shared handler for Node, the mechanism type misidentifies the runtime context. Additionally, since the sentry middleware gets wrapped by wrapMiddlewareWithSpan (due to patchAppUse patching app.use before registration), the wrapping span has origin auto.middleware.hono, which doesn't align with the auto.faas.hono.error_handler mechanism type.

Additional Locations (1)
Fix in Cursor Fix in Web

Triggered by project rule: PR Review Guidelines for Cursor Bot

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

/**
* Initializes Sentry for Hono running in a Node 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: 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),
};

return initNode(filteredOptions);
}
3 changes: 3 additions & 0 deletions packages/hono/src/shared/filterHonoIntegration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import type { Integration } from '@sentry/core';

export const filterHonoIntegration = (integration: Integration): boolean => integration.name !== 'Hono';
9 changes: 7 additions & 2 deletions packages/hono/src/shared/middlewareHandlers.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { getIsolationScope } from '@sentry/cloudflare';
import {
getActiveSpan,
getClient,
getDefaultIsolationScope,
getIsolationScope,
getRootSpan,
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
updateSpanName,
winterCGRequestToRequestData,
} from '@sentry/core';
Expand Down Expand Up @@ -32,7 +33,11 @@ export function responseHandler(context: Context): void {
const activeSpan = getActiveSpan();
if (activeSpan) {
activeSpan.updateName(`${context.req.method} ${routePath(context)}`);
updateSpanName(getRootSpan(activeSpan), `${context.req.method} ${routePath(context)}`);
activeSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route');

const rootSpan = getRootSpan(activeSpan);
updateSpanName(rootSpan, `${context.req.method} ${routePath(context)}`);
rootSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route');
Copy link

Choose a reason for hiding this comment

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

Incorrect faas mechanism type for Node runtime

Medium Severity

The captureException call uses mechanism type auto.faas.hono.error_handler, where faas (Function as a Service) is specific to serverless environments like Cloudflare Workers. Now that this shared code is used for the Node runtime too, the mechanism type is semantically incorrect for Node HTTP server contexts. The existing @sentry/node Hono error handler uses auto.middleware.hono for its mechanism type. The mechanism type needs to vary by runtime or use a runtime-agnostic value.

Fix in Cursor Fix in Web

Triggered by project rule: PR Review Guidelines for Cursor Bot

Copy link
Member Author

@s1gr1d s1gr1d Mar 17, 2026

Choose a reason for hiding this comment

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

Will be done in another PR as this affects not just Node

}

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