Skip to content

Commit 70cbbaa

Browse files
committed
feat: instrument onBeforeResponse and onRequest handlers
1 parent be16f5f commit 70cbbaa

File tree

3 files changed

+468
-45
lines changed

3 files changed

+468
-45
lines changed

packages/nuxt/src/runtime/hooks/wrapMiddlewareHandler.ts

Lines changed: 136 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -11,60 +11,154 @@ import {
1111
SPAN_STATUS_OK,
1212
startSpan,
1313
} from '@sentry/core';
14-
import type { EventHandler, EventHandlerRequest, H3Event } from 'h3';
14+
import type {
15+
_ResponseMiddleware as ResponseMiddleware,
16+
EventHandler,
17+
EventHandlerObject,
18+
EventHandlerRequest,
19+
EventHandlerResponse,
20+
H3Event,
21+
} from 'h3';
1522

1623
/**
1724
* Wraps a middleware handler with Sentry instrumentation.
1825
*
1926
* @param handler The middleware handler.
2027
* @param fileName The name of the middleware file.
2128
*/
22-
export function wrapMiddlewareHandler(handler: EventHandler, fileName: string) {
23-
return async (event: H3Event<EventHandlerRequest>) => {
24-
debug.log(`Sentry middleware: ${fileName} handling ${event.path}`);
25-
26-
const attributes = getSpanAttributes(event, fileName);
27-
28-
return startSpan(
29-
{
30-
name: fileName,
31-
attributes,
32-
},
33-
async span => {
34-
try {
35-
const result = await handler(event);
36-
span.setStatus({ code: SPAN_STATUS_OK });
37-
38-
return result;
39-
} catch (error) {
40-
captureException(error, {
41-
mechanism: {
42-
handled: false,
43-
type: attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN],
44-
},
45-
});
46-
47-
// Re-throw the error to be handled by the caller
48-
throw error;
49-
} finally {
50-
await flushIfServerless();
51-
}
52-
},
29+
export function wrapMiddlewareHandler<THandler extends EventHandler | EventHandlerObject>(
30+
handler: THandler,
31+
fileName: string,
32+
): THandler {
33+
if (!isEventHandlerObject(handler)) {
34+
return wrapEventHandler(handler, fileName) as THandler;
35+
}
36+
37+
const handlerObj = {
38+
...handler,
39+
handler: wrapEventHandler(handler.handler, fileName),
40+
};
41+
42+
if (handlerObj.onRequest) {
43+
handlerObj.onRequest = normalizeHandlers(handlerObj.onRequest, (h, index) =>
44+
wrapEventHandler(h, fileName, 'onRequest', index),
5345
);
46+
}
47+
48+
if (handlerObj.onBeforeResponse) {
49+
handlerObj.onBeforeResponse = normalizeHandlers(handlerObj.onBeforeResponse, (h, index) =>
50+
wrapResponseHandler(h, fileName, index),
51+
);
52+
}
53+
54+
return handlerObj;
55+
}
56+
57+
/**
58+
* Wraps a callable event handler with Sentry instrumentation.
59+
*
60+
* @param handler The event handler.
61+
* @param handlerName The name of the event handler to be used for the span name and logging.
62+
*/
63+
function wrapEventHandler(
64+
handler: EventHandler,
65+
middlewareName: string,
66+
hookName?: 'onRequest',
67+
index?: number,
68+
): EventHandler {
69+
return async (event: H3Event<EventHandlerRequest>) => {
70+
debug.log(`Sentry middleware: ${middlewareName}${hookName ? `.${hookName}` : ''} handling ${event.path}`);
71+
72+
const attributes = getSpanAttributes(event, middlewareName, hookName, index);
73+
74+
return withSpan(() => handler(event), attributes, middlewareName, hookName);
75+
};
76+
}
77+
78+
/**
79+
* Wraps a middleware response handler with Sentry instrumentation.
80+
*/
81+
function wrapResponseHandler(handler: ResponseMiddleware, middlewareName: string, index?: number): ResponseMiddleware {
82+
return async (event: H3Event<EventHandlerRequest>, response: EventHandlerResponse) => {
83+
debug.log(`Sentry middleware: ${middlewareName}.onBeforeResponse handling ${event.path}`);
84+
85+
const attributes = getSpanAttributes(event, middlewareName, 'onBeforeResponse', index);
86+
87+
return withSpan(() => handler(event, response), attributes, middlewareName, 'onBeforeResponse');
5488
};
5589
}
5690

91+
/**
92+
* Wraps a middleware or event handler execution with a span.
93+
*/
94+
function withSpan<TResult>(
95+
handler: () => TResult | Promise<TResult>,
96+
attributes: SpanAttributes,
97+
middlewareName: string,
98+
hookName?: 'handler' | 'onRequest' | 'onBeforeResponse',
99+
): Promise<TResult> {
100+
const spanName = hookName && hookName !== 'handler' ? `${middlewareName}.${hookName}` : middlewareName;
101+
102+
return startSpan(
103+
{
104+
name: spanName,
105+
attributes,
106+
},
107+
async span => {
108+
try {
109+
const result = await handler();
110+
span.setStatus({ code: SPAN_STATUS_OK });
111+
112+
return result;
113+
} catch (error) {
114+
captureException(error, {
115+
mechanism: {
116+
handled: false,
117+
type: attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN],
118+
},
119+
});
120+
121+
// Re-throw the error to be handled by the caller
122+
throw error;
123+
} finally {
124+
await flushIfServerless();
125+
}
126+
},
127+
);
128+
}
129+
130+
/**
131+
* Takes a list of handlers and wraps them with the normalizer function.
132+
*/
133+
function normalizeHandlers<T extends EventHandler | ResponseMiddleware>(
134+
handlers: T | T[],
135+
normalizer: (h: T, index?: number) => T,
136+
): T | T[] {
137+
return Array.isArray(handlers) ? handlers.map((handler, index) => normalizer(handler, index)) : normalizer(handlers);
138+
}
139+
57140
/**
58141
* Gets the span attributes for the middleware handler based on the event.
59142
*/
60-
function getSpanAttributes(event: H3Event<EventHandlerRequest>, fileName: string): SpanAttributes {
143+
function getSpanAttributes(
144+
event: H3Event<EventHandlerRequest>,
145+
middlewareName: string,
146+
hookName?: 'handler' | 'onRequest' | 'onBeforeResponse',
147+
index?: number,
148+
): SpanAttributes {
61149
const attributes: SpanAttributes = {
62150
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'middleware.nuxt',
63151
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom',
64-
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.nuxt',
65-
'nuxt.middleware.name': fileName,
152+
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.middleware.nuxt',
153+
'nuxt.middleware.name': middlewareName,
154+
'nuxt.middleware.hook.name': hookName ?? 'handler',
66155
};
67156

157+
// Add index for array handlers
158+
if (typeof index === 'number') {
159+
attributes['nuxt.middleware.hook.index'] = index;
160+
}
161+
68162
// Add HTTP method
69163
if (event.method) {
70164
attributes['http.request.method'] = event.method;
@@ -88,3 +182,10 @@ function getSpanAttributes(event: H3Event<EventHandlerRequest>, fileName: string
88182

89183
return attributes;
90184
}
185+
186+
/**
187+
* Checks if the handler is an event handler, util for type narrowing.
188+
*/
189+
function isEventHandlerObject(handler: EventHandler | EventHandlerObject): handler is EventHandlerObject {
190+
return typeof handler !== 'function';
191+
}

packages/nuxt/src/vite/middlewareConfig.ts

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -77,20 +77,14 @@ function middlewareInstrumentationPlugin(nitro: Nitro): InputPluginOption {
7777
* @returns The wrapped user code of the middleware.
7878
*/
7979
function wrapMiddlewareCode(originalCode: string, fileName: string): string {
80+
// Remove common file extensions
81+
const cleanFileName = fileName.replace(/\.(ts|js|mjs|mts|cts)$/, '');
82+
8083
return `
8184
import { wrapMiddlewareHandler } from '#imports';
8285
8386
function defineInstrumentedEventHandler(handlerOrObject) {
84-
// Handle function syntax
85-
if (typeof handlerOrObject === 'function') {
86-
return defineEventHandler(wrapMiddlewareHandler(handlerOrObject, '${fileName}'));
87-
}
88-
89-
// Handle object syntax
90-
return defineEventHandler({
91-
...handlerOrObject,
92-
handler: wrapMiddlewareHandler(handlerOrObject.handler, '${fileName}')
93-
});
87+
return defineEventHandler(wrapMiddlewareHandler(handlerOrObject, '${cleanFileName}'));
9488
}
9589
9690
${originalCode.replace(/defineEventHandler\(/g, 'defineInstrumentedEventHandler(')}

0 commit comments

Comments
 (0)