Skip to content

Commit b44ff35

Browse files
authored
feat(hono)!: Change setup for @sentry/hono/node (init in external file) (#20497)
Let's users set up Sentry in an external file. This makes it easier to filter any integrations. The setup is now slightly different than it is for Bun and Cloudflare, but the Node runtime just works differently. Closes #20460 BREAKING CHANGE: `sentry` from `@sentry/hono/node` does not accept `options` anymore as those need to be passed in a separate `Sentry.init()` that is called in an external file: ```ts // instrument.mjs (or instrument.ts) import * as Sentry from '@sentry/hono/node'; Sentry.init({ dsn: '__DSN__', tracesSampleRate: 1.0, }); ```
1 parent cd1b022 commit b44ff35

12 files changed

Lines changed: 309 additions & 221 deletions

File tree

dev-packages/e2e-tests/test-applications/hono-4/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"private": true,
66
"scripts": {
77
"dev:cf": "wrangler dev --var \"E2E_TEST_DSN:$E2E_TEST_DSN\" --log-level=$(test $CI && echo 'none' || echo 'log')",
8-
"dev:node": "node --import tsx/esm --import @sentry/node/preload src/entry.node.ts",
8+
"dev:node": "node --import tsx/esm --import ./src/instrument.node.ts src/entry.node.ts",
99
"dev:bun": "bun src/entry.bun.ts",
1010
"build": "wrangler deploy --dry-run",
1111
"test:build": "pnpm install && pnpm build",

dev-packages/e2e-tests/test-applications/hono-4/src/entry.node.ts

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,9 @@ import { sentry } from '@sentry/hono/node';
33
import { serve } from '@hono/node-server';
44
import { addRoutes } from './routes';
55

6-
const app = new Hono<{ Bindings: { E2E_TEST_DSN: string } }>();
6+
const app = new Hono();
77

8-
app.use(
9-
// @ts-expect-error - Env is not yet in type
10-
sentry(app, {
11-
dsn: process.env.E2E_TEST_DSN,
12-
environment: 'qa',
13-
tracesSampleRate: 1.0,
14-
tunnel: 'http://localhost:3031/',
15-
}),
16-
);
8+
app.use(sentry(app));
179

1810
addRoutes(app);
1911

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import * as Sentry from '@sentry/hono/node';
2+
3+
Sentry.init({
4+
dsn: process.env.E2E_TEST_DSN,
5+
environment: 'qa',
6+
tracesSampleRate: 1.0,
7+
tunnel: 'http://localhost:3031/',
8+
});
Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
export type Runtime = 'cloudflare' | 'node' | 'bun';
22

33
export const RUNTIME = (process.env.RUNTIME || 'cloudflare') as Runtime;
4-
export const isNode = RUNTIME === 'node';
54

65
export const APP_NAME = 'hono-4';

dev-packages/e2e-tests/test-applications/hono-4/tests/middleware.test.ts

Lines changed: 7 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,20 @@
11
import { expect, test } from '@playwright/test';
22
import { waitForError, waitForTransaction } from '@sentry-internal/test-utils';
33
import { type SpanJSON } from '@sentry/core';
4-
import { APP_NAME, isNode } from './constants';
5-
6-
// In Node, @sentry/node/preload eagerly activates the OTel HonoInstrumentation,
7-
// which wraps all Hono instance methods at construction time via WrappedHono.
8-
const MIDDLEWARE_ORIGIN = 'auto.middleware.hono';
9-
const OTEL_ORIGIN = 'auto.http.otel.hono';
4+
import { APP_NAME } from './constants';
105

116
const SCENARIOS = [
127
{
138
name: 'root app middleware',
149
prefix: '/test-middleware',
15-
origin: MIDDLEWARE_ORIGIN,
1610
},
1711
{
1812
name: 'sub-app middleware (route group)',
1913
prefix: '/test-subapp-middleware',
20-
origin: isNode ? OTEL_ORIGIN : MIDDLEWARE_ORIGIN,
2114
},
2215
] as const;
2316

24-
for (const { name, prefix, origin } of SCENARIOS) {
17+
for (const { name, prefix } of SCENARIOS) {
2518
test.describe(name, () => {
2619
test('creates a span for named middleware', async ({ baseURL }) => {
2720
const transactionPromise = waitForTransaction(APP_NAME, event => {
@@ -43,7 +36,7 @@ for (const { name, prefix, origin } of SCENARIOS) {
4336
expect.objectContaining({
4437
description: 'middlewareA',
4538
op: 'middleware.hono',
46-
origin,
39+
origin: 'auto.middleware.hono',
4740
status: 'ok',
4841
}),
4942
);
@@ -68,18 +61,13 @@ for (const { name, prefix, origin } of SCENARIOS) {
6861
expect.objectContaining({
6962
description: '<anonymous>',
7063
op: 'middleware.hono',
71-
origin: MIDDLEWARE_ORIGIN,
64+
origin: 'auto.middleware.hono',
7265
status: 'ok',
7366
}),
7467
);
7568
});
7669

7770
test('multiple middleware are sibling spans under the same parent', async ({ baseURL }) => {
78-
test.skip(
79-
isNode,
80-
'Node double-instruments middleware (too many spans) - TODO: fix this in the SDK and re-enable the test',
81-
);
82-
8371
const transactionPromise = waitForTransaction(APP_NAME, event => {
8472
return event.contexts?.trace?.op === 'http.server' && event.transaction === `GET ${prefix}/multi`;
8573
});
@@ -90,7 +78,6 @@ for (const { name, prefix, origin } of SCENARIOS) {
9078
const transaction = await transactionPromise;
9179
const spans = transaction.spans || [];
9280

93-
// Sort spans because they are in a different order in Node/Bun (OTel-based)
9481
const middlewareSpans = spans.sort((a, b) => (a.start_timestamp ?? 0) - (b.start_timestamp ?? 0));
9582

9683
expect(middlewareSpans).toHaveLength(2);
@@ -139,10 +126,6 @@ for (const { name, prefix, origin } of SCENARIOS) {
139126
const transaction = await transactionPromise;
140127
const spans = transaction.spans || [];
141128

142-
// On the /error path only one middleware (failingMiddleware) is registered,
143-
// so we can find the error span by status alone. On Node for sub-apps, the
144-
// OTel layer wraps before patchRoute, so the function name may be lost in
145-
// the patchRoute span — but the error status is always set.
146129
const failingSpan = spans.find(
147130
(span: SpanJSON) => span.op === 'middleware.hono' && span.status === 'internal_error',
148131
);
@@ -169,36 +152,8 @@ for (const { name, prefix, origin } of SCENARIOS) {
169152
});
170153
}
171154

172-
test.describe('.all() handler on sub-app (method ALL edge case)', () => {
173-
test('Node: OTel wraps .all() and produces a hono span', async ({ baseURL }) => {
174-
test.skip(!isNode, 'Node-specific: OTel wraps .all() at construction time');
175-
176-
const transactionPromise = waitForTransaction(APP_NAME, event => {
177-
return (
178-
event.contexts?.trace?.op === 'http.server' && event.transaction === 'GET /test-subapp-middleware/all-handler'
179-
);
180-
});
181-
182-
const response = await fetch(`${baseURL}/test-subapp-middleware/all-handler`);
183-
expect(response.status).toBe(200);
184-
185-
const body = await response.json();
186-
expect(body).toEqual({ handler: 'all' });
187-
188-
const transaction = await transactionPromise;
189-
const spans = transaction.spans || [];
190-
191-
// On Node, OTel wraps .all() at construction time. Since the handler
192-
// returns a Response, OTel classifies it as 'request_handler' (not
193-
// middleware). patchRoute also wraps it but sees the anonymous OTel wrapper.
194-
// Either way, the handler IS instrumented — verify any hono span exists.
195-
const honoSpan = spans.find((span: SpanJSON) => span.op?.endsWith('.hono'));
196-
expect(honoSpan).toBeDefined();
197-
});
198-
199-
test('Bun/Cloudflare: patchRoute wraps .all() as middleware span', async ({ baseURL }) => {
200-
test.skip(isNode, 'Bun/Cloudflare-specific: patchRoute is the sole wrapper');
201-
155+
test.describe('patchRoute wraps .all() as middleware span (in sub-app)', () => {
156+
test('patchRoute wraps .all() as middleware span', async ({ baseURL }) => {
202157
const transactionPromise = waitForTransaction(APP_NAME, event => {
203158
return (
204159
event.contexts?.trace?.op === 'http.server' && event.transaction === 'GET /test-subapp-middleware/all-handler'
@@ -225,7 +180,7 @@ test.describe('.all() handler on sub-app (method ALL edge case)', () => {
225180
expect.objectContaining({
226181
description: 'allCatchAll',
227182
op: 'middleware.hono',
228-
origin: MIDDLEWARE_ORIGIN,
183+
origin: 'auto.middleware.hono',
229184
status: 'ok',
230185
}),
231186
);
Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,8 @@
1-
// Sentry is initialized by the @sentry/hono/node middleware in scenario.mjs
1+
import * as Sentry from '@sentry/hono/node';
2+
import { loggingTransport } from '@sentry-internal/node-integration-tests';
3+
4+
Sentry.init({
5+
dsn: 'https://public@dsn.ingest.sentry.io/1337',
6+
tracesSampleRate: 1.0,
7+
transport: loggingTransport,
8+
});

dev-packages/node-integration-tests/suites/hono-sdk/scenario.mjs

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,11 @@
11
import { serve } from '@hono/node-server';
22
import { sentry } from '@sentry/hono/node';
3-
import { loggingTransport, sendPortToRunner } from '@sentry-internal/node-integration-tests';
3+
import { sendPortToRunner } from '@sentry-internal/node-integration-tests';
44
import { Hono } from 'hono';
55

66
const app = new Hono();
77

8-
app.use(
9-
sentry(app, {
10-
dsn: 'https://public@dsn.ingest.sentry.io/1337',
11-
tracesSampleRate: 1.0,
12-
transport: loggingTransport,
13-
}),
14-
);
8+
app.use(sentry(app));
159

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

packages/hono/README.md

Lines changed: 34 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ npm install @sentry/hono
3333

3434
Additionally to `@sentry/hono`, install the `@sentry/cloudflare` package:
3535

36-
```bashbash
36+
```bash
3737
npm install --save @sentry/cloudflare
3838
```
3939

@@ -100,62 +100,68 @@ export default app;
100100

101101
Additionally to `@sentry/hono`, install the `@sentry/node` package:
102102

103-
```bashbash
103+
```bash
104104
npm install --save @sentry/node
105105
```
106106

107107
Make sure the installed version always stays in sync. The `@sentry/node` package is a required peer dependency when using `@sentry/hono/node`.
108108
You won't import `@sentry/node` directly in your code, but it needs to be installed in your project.
109109

110-
### 2. Initialize Sentry in your Hono app
110+
### 2. Initialize Sentry in a separate file
111111

112-
Initialize the Sentry Hono middleware as early as possible in your app:
112+
Create an `instrument.mjs` (or `instrument.ts`) file that initializes Sentry before the rest of your application runs.
113+
This ensures Sentry can wrap third-party libraries (e.g. database clients) as early as possible:
113114

114115
```ts
115-
import { Hono } from 'hono';
116-
import { serve } from '@hono/node-server';
117-
import { sentry } from '@sentry/hono/node';
118-
119-
const app = new Hono();
120-
121-
// Initialize Sentry middleware right after creating the app
122-
app.use(
123-
sentry(app, {
124-
dsn: '__DSN__', // or process.env.SENTRY_DSN
125-
tracesSampleRate: 1.0,
126-
}),
127-
);
128-
129-
// ... your routes and other middleware
116+
// instrument.mjs (or instrument.ts)
117+
import * as Sentry from '@sentry/hono/node';
130118

131-
serve(app);
119+
Sentry.init({
120+
dsn: '__DSN__',
121+
tracesSampleRate: 1.0,
122+
});
132123
```
133124

134-
### 3. Add `preload` script to start command
135-
136-
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.
125+
### 3. Load the instrument file with `--import`
137126

138-
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:
127+
When starting your Hono Node application, use the `--import` CLI flag to load `instrument.mjs` before your app code:
139128

140129
```bash
141-
node --import @sentry/node/preload index.js
130+
node --import ./instrument.mjs app.js
142131
```
143132

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

146135
```bash
147-
NODE_OPTIONS="--import @sentry/node/preload"
136+
NODE_OPTIONS="--import ./instrument.mjs"
148137
```
149138

150-
Read more about this preload script in the docs: https://docs.sentry.io/platforms/javascript/guides/hono/install/late-initialization/#late-initialization-with-esm
139+
### 4. Add the Sentry middleware to your Hono app
140+
141+
Add the `sentry` middleware to your Hono app. Since Sentry was already initialized in the instrument file, no options are passed here:
142+
143+
```ts
144+
import { Hono } from 'hono';
145+
import { serve } from '@hono/node-server';
146+
import { sentry } from '@sentry/hono/node';
147+
148+
const app = new Hono();
149+
150+
// Add Sentry middleware right after creating the app
151+
app.use(sentry(app));
152+
153+
// ... your routes and other middleware
154+
155+
serve(app);
156+
```
151157

152158
## Setup (Bun)
153159

154160
### 1. Install Peer Dependency
155161

156162
Additionally to `@sentry/hono`, install the `@sentry/bun` package:
157163

158-
```bashbash
164+
```bash
159165
npm install --save @sentry/bun
160166
```
161167

packages/hono/src/node/middleware.ts

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,28 @@
1-
import { type BaseTransportOptions, debug, type Options } from '@sentry/core';
2-
import { init } from './sdk';
1+
import { type BaseTransportOptions, debug, type Options, getClient } from '@sentry/core';
32
import type { Hono, MiddlewareHandler } from 'hono';
43
import { requestHandler, responseHandler } from '../shared/middlewareHandlers';
54
import { applyPatches } from '../shared/applyPatches';
65

76
export interface HonoNodeOptions extends Options<BaseTransportOptions> {}
87

98
/**
10-
* Sentry middleware for Hono running in a Node runtime environment.
9+
* Sentry middleware for Hono applications running in a Node.js environment.
10+
*
11+
* This middleware enhances your Hono application by automatically instrumenting incoming requests and outgoing responses.
12+
* It also applies the necessary patches to ensure Sentry captures execution context correctly in Node.js.
13+
*
14+
* **Note:** You must initialize Sentry separately before using this middleware. Typically, this is done by calling `Sentry.init()` in an `instrument.ts` file and loading it via the Node `--import` flag.
1115
*/
12-
export const sentry = (app: Hono, options: HonoNodeOptions): MiddlewareHandler => {
13-
const isDebug = options.debug;
14-
15-
isDebug && debug.log('Initialized Sentry Hono middleware (Node)');
16-
17-
init(options);
16+
export const sentry = (app: Hono): MiddlewareHandler => {
17+
const sentryClient = getClient();
18+
if (sentryClient === undefined) {
19+
debug.warn(
20+
'Sentry is not initialized. Call `init()` from @sentry/hono/node in an `instrument.ts` file loaded via `--import` to set up Sentry for your application.',
21+
);
22+
} else {
23+
sentryClient.getOptions().debug &&
24+
debug.log('Sentry is initialized, proceeding to set up Hono `sentry` middleware.');
25+
}
1826

1927
applyPatches(app);
2028

packages/hono/src/node/sdk.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,23 @@
11
import type { Client } from '@sentry/core';
2-
import { applySdkMetadata } from '@sentry/core';
2+
import { applySdkMetadata, debug, getClient } from '@sentry/core';
33
import { init as initNode } from '@sentry/node';
44
import type { HonoNodeOptions } from './middleware';
55
import { buildFilteredIntegrations } from '../shared/buildFilteredIntegrations';
66

77
/**
88
* Initializes Sentry for Hono running in a Node runtime environment.
99
*
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.
10+
* This function should be called in an `instrument.ts` file loaded via `--import` to set up Sentry globally for the application.
1311
*/
1412
export function init(options: HonoNodeOptions): Client | undefined {
13+
const existingClient = getClient();
14+
if (existingClient) {
15+
existingClient.getOptions().debug && debug.log('Sentry is already initialized, skipping re-initialization.');
16+
return existingClient;
17+
}
18+
1519
applySdkMetadata(options, 'hono', ['hono', 'node']);
1620

17-
// Remove Hono from the SDK defaults to prevent double instrumentation: @sentry/node
1821
const filteredOptions: HonoNodeOptions = {
1922
...options,
2023
integrations: buildFilteredIntegrations(options.integrations, false),

0 commit comments

Comments
 (0)