Skip to content

Commit be8604b

Browse files
committed
bugfix(node): Fix issue where middleware registered alone is treated as a route handler
1 parent 8c5a3ed commit be8604b

File tree

4 files changed

+190
-73
lines changed

4 files changed

+190
-73
lines changed

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

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,17 @@ basePaths.forEach(basePath => {
4343
},
4444
);
4545

46+
// anonymous middleware
47+
baseApp[method]('/middleware/separately', async (c, next) => {
48+
await next();
49+
});
50+
51+
baseApp[method]('/middleware/separately', async c => {
52+
const response = c.text('response 200');
53+
if (basePath === '/sync') return response;
54+
return Promise.resolve(response);
55+
});
56+
4657
baseApp.all('/all', c => {
4758
const response = c.text('response 200');
4859
if (basePath === '/sync') return response;
@@ -62,6 +73,17 @@ basePaths.forEach(basePath => {
6273
},
6374
);
6475

76+
// anonymous middleware
77+
baseApp.all('/all/middleware/separately', async (c, next) => {
78+
await next();
79+
});
80+
81+
baseApp.all('/all/middleware/separately', async c => {
82+
const response = c.text('response 200');
83+
if (basePath === '/sync') return response;
84+
return Promise.resolve(response);
85+
});
86+
6587
baseApp.on(method, '/on', c => {
6688
const response = c.text('response 200');
6789
if (basePath === '/sync') return response;
@@ -82,6 +104,17 @@ basePaths.forEach(basePath => {
82104
},
83105
);
84106

107+
// anonymous middleware
108+
baseApp.on(method, '/on/middleware/separately', async (c, next) => {
109+
await next();
110+
});
111+
112+
baseApp.on(method, '/on/middleware/separately', async c => {
113+
const response = c.text('response 200');
114+
if (basePath === '/sync') return response;
115+
return Promise.resolve(response);
116+
});
117+
85118
baseApp[method]('/401', () => {
86119
const response = new HTTPException(401, { message: 'response 401' });
87120
if (basePath === '/sync') throw response;

dev-packages/node-integration-tests/suites/tracing/hono/test.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,74 @@ describe('hono tracing', () => {
137137
await runner.completed();
138138
});
139139

140+
test('should handle transaction with separate middleware', async () => {
141+
const runner = createRunner()
142+
.expect({
143+
transaction: {
144+
transaction: `${method.toUpperCase()} ${route}${path === '/' ? '' : path}/middleware/separately`,
145+
spans: expect.arrayContaining([
146+
expect.objectContaining({
147+
data: expect.objectContaining({
148+
'hono.name': 'sentryRequestMiddleware',
149+
'hono.type': 'middleware',
150+
}),
151+
description: 'sentryRequestMiddleware',
152+
op: 'middleware.hono',
153+
origin: 'auto.http.otel.hono',
154+
}),
155+
expect.objectContaining({
156+
data: expect.objectContaining({
157+
'hono.name': 'sentryErrorMiddleware',
158+
'hono.type': 'middleware',
159+
}),
160+
description: 'sentryErrorMiddleware',
161+
op: 'middleware.hono',
162+
origin: 'auto.http.otel.hono',
163+
}),
164+
expect.objectContaining({
165+
data: expect.objectContaining({
166+
'hono.name': 'global',
167+
'hono.type': 'middleware',
168+
}),
169+
description: 'global',
170+
op: 'middleware.hono',
171+
origin: 'auto.http.otel.hono',
172+
}),
173+
expect.objectContaining({
174+
data: expect.objectContaining({
175+
'hono.name': 'base',
176+
'hono.type': 'middleware',
177+
}),
178+
description: 'base',
179+
op: 'middleware.hono',
180+
origin: 'auto.http.otel.hono',
181+
}),
182+
expect.objectContaining({
183+
data: expect.objectContaining({
184+
'hono.name': 'anonymous',
185+
'hono.type': 'middleware',
186+
}),
187+
description: 'anonymous',
188+
op: 'middleware.hono',
189+
origin: 'auto.http.otel.hono',
190+
}),
191+
expect.objectContaining({
192+
data: expect.objectContaining({
193+
'hono.name': `${route}${path === '/' ? '' : path}/middleware/separately`,
194+
'hono.type': 'request_handler',
195+
}),
196+
description: `${route}${path === '/' ? '' : path}/middleware/separately`,
197+
op: 'request_handler.hono',
198+
origin: 'auto.http.otel.hono',
199+
}),
200+
]),
201+
},
202+
})
203+
.start();
204+
runner.makeRequest(method, `${route}${path === '/' ? '' : path}/middleware/separately`);
205+
await runner.completed();
206+
});
207+
140208
test('should handle returned errors for %s path', async () => {
141209
const runner = createRunner()
142210
.ignore('transaction')

packages/node/src/integrations/tracing/hono/index.ts

Lines changed: 38 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import {
44
captureException,
55
debug,
66
defineIntegration,
7-
getClient,
87
getDefaultIsolationScope,
98
getIsolationScope,
109
httpRequestToRequestData,
@@ -20,7 +19,44 @@ import type { Context, MiddlewareHandler, MiddlewareHandlerInterface, Next } fro
2019

2120
const INTEGRATION_NAME = 'Hono';
2221

23-
export const instrumentHono = generateInstrumentOnce(INTEGRATION_NAME, () => new HonoInstrumentation());
22+
function addHonoSpanAttributes(span: Span): void {
23+
const attributes = spanToJSON(span).data;
24+
const type = attributes[AttributeNames.HONO_TYPE];
25+
if (attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP] || !type) {
26+
return;
27+
}
28+
29+
span.setAttributes({
30+
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.otel.hono',
31+
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: `${type}.hono`,
32+
});
33+
34+
const name = attributes[AttributeNames.HONO_NAME];
35+
if (typeof name === 'string') {
36+
span.updateName(name);
37+
}
38+
39+
if (getIsolationScope() === getDefaultIsolationScope()) {
40+
DEBUG_BUILD && debug.warn('Isolation scope is default isolation scope - skipping setting transactionName');
41+
return;
42+
}
43+
44+
const route = attributes[ATTR_HTTP_ROUTE];
45+
const method = attributes[ATTR_HTTP_REQUEST_METHOD];
46+
if (typeof route === 'string' && typeof method === 'string') {
47+
getIsolationScope().setTransactionName(`${method} ${route}`);
48+
}
49+
}
50+
51+
export const instrumentHono = generateInstrumentOnce(
52+
INTEGRATION_NAME,
53+
() =>
54+
new HonoInstrumentation({
55+
responseHook: span => {
56+
addHonoSpanAttributes(span);
57+
},
58+
}),
59+
);
2460

2561
const _honoIntegration = (() => {
2662
return {
@@ -86,35 +122,6 @@ function honoErrorHandler(options?: Partial<HonoHandlerOptions>): MiddlewareHand
86122
};
87123
}
88124

89-
function addHonoSpanAttributes(span: Span): void {
90-
const attributes = spanToJSON(span).data;
91-
const type = attributes[AttributeNames.HONO_TYPE];
92-
if (attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP] || !type) {
93-
return;
94-
}
95-
96-
span.setAttributes({
97-
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.otel.hono',
98-
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: `${type}.hono`,
99-
});
100-
101-
const name = attributes[AttributeNames.HONO_NAME];
102-
if (typeof name === 'string') {
103-
span.updateName(name);
104-
}
105-
106-
if (getIsolationScope() === getDefaultIsolationScope()) {
107-
DEBUG_BUILD && debug.warn('Isolation scope is default isolation scope - skipping setting transactionName');
108-
return;
109-
}
110-
111-
const route = attributes[ATTR_HTTP_ROUTE];
112-
const method = attributes[ATTR_HTTP_REQUEST_METHOD];
113-
if (typeof route === 'string' && typeof method === 'string') {
114-
getIsolationScope().setTransactionName(`${method} ${route}`);
115-
}
116-
}
117-
118125
/**
119126
* Add a Hono error handler to capture errors to Sentry.
120127
*
@@ -139,13 +146,5 @@ export function setupHonoErrorHandler(
139146
): void {
140147
app.use(honoRequestHandler());
141148
app.use(honoErrorHandler(options));
142-
143-
const client = getClient();
144-
if (client) {
145-
client.on('spanStart', span => {
146-
addHonoSpanAttributes(span);
147-
});
148-
}
149-
150149
ensureIsWrapped(app.use, 'hono');
151150
}

packages/node/src/integrations/tracing/hono/instrumentation.ts

Lines changed: 51 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { Span } from '@opentelemetry/api';
22
import { context, SpanStatusCode, trace } from '@opentelemetry/api';
3+
import type { InstrumentationConfig } from '@opentelemetry/instrumentation';
34
import { InstrumentationBase, InstrumentationNodeModuleDefinition } from '@opentelemetry/instrumentation';
45
import { isThenable } from '@sentry/core';
56
import { AttributeNames, HonoTypes } from './constants';
@@ -18,12 +19,21 @@ import type {
1819
const PACKAGE_NAME = '@sentry/instrumentation-hono';
1920
const PACKAGE_VERSION = '0.0.1';
2021

22+
export interface HonoResponseHookFunction {
23+
(span: Span): void;
24+
}
25+
26+
export interface HonoInstrumentationConfig extends InstrumentationConfig {
27+
/** Function for adding custom span attributes from the response */
28+
responseHook?: HonoResponseHookFunction;
29+
}
30+
2131
/**
2232
* Hono instrumentation for OpenTelemetry
2333
*/
24-
export class HonoInstrumentation extends InstrumentationBase {
25-
public constructor() {
26-
super(PACKAGE_NAME, PACKAGE_VERSION, {});
34+
export class HonoInstrumentation extends InstrumentationBase<HonoInstrumentationConfig> {
35+
public constructor(config: HonoInstrumentationConfig = {}) {
36+
super(PACKAGE_NAME, PACKAGE_VERSION, config);
2737
}
2838

2939
/**
@@ -86,23 +96,13 @@ export class HonoInstrumentation extends InstrumentationBase {
8696
const handlers = args.slice(1);
8797
return original.apply(this, [
8898
path,
89-
...handlers.map((handler, index) =>
90-
instrumentation._wrapHandler(
91-
index + 1 === handlers.length ? HonoTypes.REQUEST_HANDLER : HonoTypes.MIDDLEWARE,
92-
handler as Handler | MiddlewareHandler,
93-
),
94-
),
99+
...handlers.map(handler => instrumentation._wrapHandler(handler as Handler | MiddlewareHandler)),
95100
]);
96101
}
97102

98103
return original.apply(
99104
this,
100-
args.map((handler, index) =>
101-
instrumentation._wrapHandler(
102-
index + 1 === args.length ? HonoTypes.REQUEST_HANDLER : HonoTypes.MIDDLEWARE,
103-
handler as Handler | MiddlewareHandler,
104-
),
105-
),
105+
args.map(handler => instrumentation._wrapHandler(handler as Handler | MiddlewareHandler)),
106106
);
107107
};
108108
};
@@ -120,12 +120,7 @@ export class HonoInstrumentation extends InstrumentationBase {
120120
const handlers = args.slice(2);
121121
return original.apply(this, [
122122
...args.slice(0, 2),
123-
...handlers.map((handler, index) =>
124-
instrumentation._wrapHandler(
125-
index + 1 === handlers.length ? HonoTypes.REQUEST_HANDLER : HonoTypes.MIDDLEWARE,
126-
handler as Handler | MiddlewareHandler,
127-
),
128-
),
123+
...handlers.map(handler => instrumentation._wrapHandler(handler as Handler | MiddlewareHandler)),
129124
]);
130125
};
131126
};
@@ -149,15 +144,13 @@ export class HonoInstrumentation extends InstrumentationBase {
149144
const handlers = args.slice(1);
150145
return original.apply(this, [
151146
path,
152-
...handlers.map(handler =>
153-
instrumentation._wrapHandler(HonoTypes.MIDDLEWARE, handler as MiddlewareHandler),
154-
),
147+
...handlers.map(handler => instrumentation._wrapHandler(handler as MiddlewareHandler)),
155148
]);
156149
}
157150

158151
return original.apply(
159152
this,
160-
args.map(handler => instrumentation._wrapHandler(HonoTypes.MIDDLEWARE, handler as MiddlewareHandler)),
153+
args.map(handler => instrumentation._wrapHandler(handler as MiddlewareHandler)),
161154
);
162155
};
163156
};
@@ -166,7 +159,7 @@ export class HonoInstrumentation extends InstrumentationBase {
166159
/**
167160
* Wraps a handler or middleware handler to apply instrumentation.
168161
*/
169-
private _wrapHandler(type: HonoTypes, handler: Handler | MiddlewareHandler): Handler | MiddlewareHandler {
162+
private _wrapHandler(handler: Handler | MiddlewareHandler): Handler | MiddlewareHandler {
170163
// eslint-disable-next-line @typescript-eslint/no-this-alias
171164
const instrumentation = this;
172165

@@ -176,17 +169,32 @@ export class HonoInstrumentation extends InstrumentationBase {
176169
}
177170

178171
const path = c.req.path;
179-
const spanName = `${type.replace('_', ' ')} - ${path}`;
180-
const span = instrumentation.tracer.startSpan(spanName, {
181-
attributes: {
182-
[AttributeNames.HONO_TYPE]: type,
183-
[AttributeNames.HONO_NAME]: type === 'request_handler' ? path : handler.name || 'anonymous',
184-
},
185-
});
172+
const span = instrumentation.tracer.startSpan(path);
186173

187174
return context.with(trace.setSpan(context.active(), span), () => {
188175
return instrumentation._safeExecute(
189-
() => handler.apply(this, [c, next]),
176+
() => {
177+
const result = handler.apply(this, [c, next]);
178+
if (isThenable(result)) {
179+
return result.then(result => {
180+
const type = instrumentation._determineHandlerType(result);
181+
span.setAttributes({
182+
[AttributeNames.HONO_TYPE]: type,
183+
[AttributeNames.HONO_NAME]: type === HonoTypes.REQUEST_HANDLER ? path : handler.name || 'anonymous',
184+
});
185+
instrumentation.getConfig().responseHook?.(span);
186+
return result;
187+
});
188+
} else {
189+
const type = instrumentation._determineHandlerType(result);
190+
span.setAttributes({
191+
[AttributeNames.HONO_TYPE]: type,
192+
[AttributeNames.HONO_NAME]: type === HonoTypes.REQUEST_HANDLER ? path : handler.name || 'anonymous',
193+
});
194+
instrumentation.getConfig().responseHook?.(span);
195+
return result;
196+
}
197+
},
190198
() => span.end(),
191199
error => {
192200
instrumentation._handleError(span, error);
@@ -221,6 +229,15 @@ export class HonoInstrumentation extends InstrumentationBase {
221229
}
222230
}
223231

232+
/**
233+
* Determines the handler type based on the result.
234+
* @param result
235+
* @private
236+
*/
237+
private _determineHandlerType(result: unknown): HonoTypes {
238+
return result === undefined ? HonoTypes.MIDDLEWARE : HonoTypes.REQUEST_HANDLER;
239+
}
240+
224241
/**
225242
* Handles errors by setting the span status and recording the exception.
226243
*/

0 commit comments

Comments
 (0)