From bd2c2091189675a8de86a76a2445c39c5041a121 Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Thu, 25 Sep 2025 12:19:56 +0300 Subject: [PATCH 01/17] feat: implement nuxt server middleware instrumentation --- packages/nuxt/src/module.ts | 8 ++ .../hooks/instrumentMiddlewareHandler.ts | 56 ++++++++++++ packages/nuxt/src/vite/middlewareConfig.ts | 86 +++++++++++++++++++ 3 files changed, 150 insertions(+) create mode 100644 packages/nuxt/src/runtime/hooks/instrumentMiddlewareHandler.ts create mode 100644 packages/nuxt/src/vite/middlewareConfig.ts diff --git a/packages/nuxt/src/module.ts b/packages/nuxt/src/module.ts index 5e1343b1ebaa..d1b1e703cb9c 100644 --- a/packages/nuxt/src/module.ts +++ b/packages/nuxt/src/module.ts @@ -10,6 +10,7 @@ import { consoleSandbox } from '@sentry/core'; import * as path from 'path'; import type { SentryNuxtModuleOptions } from './common/types'; import { addDynamicImportEntryFileWrapper, addSentryTopImport, addServerConfigToBuild } from './vite/addServerConfig'; +import { addMiddlewareImports, addMiddlewareInstrumentation } from './vite/middlewareConfig'; import { setupSourceMaps } from './vite/sourceMaps'; import { addOTelCommonJSImportAlias, findDefaultSdkInitFile } from './vite/utils'; @@ -110,7 +111,14 @@ export default defineNuxtModule({ }; }); + // Preps the the middleware instrumentation module. + addMiddlewareImports(); + nuxt.hooks.hook('nitro:init', nitro => { + if (serverConfigFile) { + addMiddlewareInstrumentation(nitro); + } + if (serverConfigFile?.includes('.server.config')) { consoleSandbox(() => { const serverDir = nitro.options.output.serverDir; diff --git a/packages/nuxt/src/runtime/hooks/instrumentMiddlewareHandler.ts b/packages/nuxt/src/runtime/hooks/instrumentMiddlewareHandler.ts new file mode 100644 index 000000000000..b6d0554a2157 --- /dev/null +++ b/packages/nuxt/src/runtime/hooks/instrumentMiddlewareHandler.ts @@ -0,0 +1,56 @@ +import { + debug, + flushIfServerless, + getDefaultIsolationScope, + getIsolationScope, + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, + SPAN_STATUS_ERROR, + SPAN_STATUS_OK, + startSpan, + withIsolationScope, +} from '@sentry/core'; +import type { EventHandler, EventHandlerRequest, H3Event } from 'h3'; + +/** + * Instruments a middleware handler. + * + * @param handler The middleware handler. + * @param fileName The name of the middleware file. + */ +export function instrumentMiddlewareHandler(handler: EventHandler, fileName: string) { + return async (event: H3Event) => { + const middlewarePath = event?.path || event?.node?.req?.url || 'unknown'; + + debug.log(`Sentry middleware: ${fileName} handling ${middlewarePath}`); + + const isolationScope = getIsolationScope(); + const newIsolationScope = isolationScope === getDefaultIsolationScope() ? isolationScope.clone() : isolationScope; + + return withIsolationScope(newIsolationScope, async () => { + return startSpan( + { + name: `middleware.${fileName}`, + op: 'middleware.nitro', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom', + 'middleware.name': fileName, + 'middleware.path': middlewarePath, + }, + }, + async span => { + try { + const result = await handler(event); + span.setStatus({ code: SPAN_STATUS_OK }); + return result; + } catch (error) { + span.setStatus({ code: SPAN_STATUS_ERROR, message: 'internal_error' }); + span.recordException(error); + throw error; + } finally { + await flushIfServerless(); + } + }, + ); + }); + }; +} diff --git a/packages/nuxt/src/vite/middlewareConfig.ts b/packages/nuxt/src/vite/middlewareConfig.ts new file mode 100644 index 000000000000..47a441db75b2 --- /dev/null +++ b/packages/nuxt/src/vite/middlewareConfig.ts @@ -0,0 +1,86 @@ +import { addServerImports, createResolver } from '@nuxt/kit'; +import type { Nitro } from 'nitropack/types'; +import * as path from 'path'; +import type { InputPluginOption } from 'rollup'; + +/** + * Adds a template for the middleware instrumentation. + */ +export function addMiddlewareImports(): void { + addServerImports([ + { + name: 'instrumentMiddlewareHandler', + from: createResolver(import.meta.url).resolve('./runtime/hooks/instrumentMiddlewareHandler'), + }, + ]); +} + +/** + * Adds middleware instrumentation to the Nitro build. + * + * @param nitro Nitro instance + */ +export function addMiddlewareInstrumentation(nitro: Nitro): void { + nitro.hooks.hook('rollup:before', (nitro, rollupConfig) => { + if (!rollupConfig.plugins) { + rollupConfig.plugins = []; + } + + if (!Array.isArray(rollupConfig.plugins)) { + rollupConfig.plugins = [rollupConfig.plugins]; + } + + rollupConfig.plugins.push(middlewareInstrumentationPlugin(nitro)); + }); +} + +function middlewareInstrumentationPlugin(nitro: Nitro): InputPluginOption { + const middlewareFiles = new Set(); + + return { + name: 'sentry-nuxt-middleware-instrumentation', + + buildStart() { + // Collect middleware files during build start + nitro.scannedHandlers?.forEach(({ middleware, handler }) => { + if (middleware && handler) { + middlewareFiles.add(handler); + } + }); + }, + + transform(code: string, id: string) { + // Only transform files we've identified as middleware + if (middlewareFiles.has(id)) { + const fileName = path.basename(id); + + return { + code: wrapMiddlewareCode(code, fileName), + map: null, + }; + } + return null; + }, + }; +} + +function wrapMiddlewareCode(originalCode: string, fileName: string): string { + return ` +import { instrumentMiddlewareHandler } from '#imports'; + +function defineInstrumentedEventHandler(handlerOrObject) { + // Handle function syntax + if (typeof handlerOrObject === 'function') { + return defineEventHandler(instrumentMiddlewareHandler(handlerOrObject, '${fileName}')); + } + + // Handle object syntax + return defineEventHandler({ + ...handlerOrObject, + handler: instrumentMiddlewareHandler(handlerOrObject.handler, '${fileName}') + }); +} + +${originalCode.replace(/defineEventHandler\(/g, 'defineInstrumentedEventHandler(')} +`; +} From 857d6620130b5a6cb3c9363ee83da56a52d29538 Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Thu, 25 Sep 2025 12:40:48 +0300 Subject: [PATCH 02/17] style: sprinkle comments --- packages/nuxt/src/vite/middlewareConfig.ts | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/packages/nuxt/src/vite/middlewareConfig.ts b/packages/nuxt/src/vite/middlewareConfig.ts index 47a441db75b2..d3a4658247dd 100644 --- a/packages/nuxt/src/vite/middlewareConfig.ts +++ b/packages/nuxt/src/vite/middlewareConfig.ts @@ -4,7 +4,7 @@ import * as path from 'path'; import type { InputPluginOption } from 'rollup'; /** - * Adds a template for the middleware instrumentation. + * Adds a server import for the middleware instrumentation. */ export function addMiddlewareImports(): void { addServerImports([ @@ -34,12 +34,17 @@ export function addMiddlewareInstrumentation(nitro: Nitro): void { }); } +/** + * Creates a rollup plugin for the middleware instrumentation by transforming the middleware code. + * + * @param nitro Nitro instance + * @returns The rollup plugin for the middleware instrumentation. + */ function middlewareInstrumentationPlugin(nitro: Nitro): InputPluginOption { const middlewareFiles = new Set(); return { name: 'sentry-nuxt-middleware-instrumentation', - buildStart() { // Collect middleware files during build start nitro.scannedHandlers?.forEach(({ middleware, handler }) => { @@ -48,7 +53,6 @@ function middlewareInstrumentationPlugin(nitro: Nitro): InputPluginOption { } }); }, - transform(code: string, id: string) { // Only transform files we've identified as middleware if (middlewareFiles.has(id)) { @@ -64,6 +68,14 @@ function middlewareInstrumentationPlugin(nitro: Nitro): InputPluginOption { }; } +/** + * Wraps the middleware user code to instrument it. + * + * @param originalCode The original user code of the middleware. + * @param fileName The name of the middleware file, used for the span name and logging. + * + * @returns The wrapped user code of the middleware. + */ function wrapMiddlewareCode(originalCode: string, fileName: string): string { return ` import { instrumentMiddlewareHandler } from '#imports'; From c03949fa25baf78917e0cfe8896443e819285f90 Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Fri, 26 Sep 2025 13:08:40 +0300 Subject: [PATCH 03/17] fix: capture exceptions at the span level --- .../hooks/instrumentMiddlewareHandler.ts | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/packages/nuxt/src/runtime/hooks/instrumentMiddlewareHandler.ts b/packages/nuxt/src/runtime/hooks/instrumentMiddlewareHandler.ts index b6d0554a2157..b25e4709015f 100644 --- a/packages/nuxt/src/runtime/hooks/instrumentMiddlewareHandler.ts +++ b/packages/nuxt/src/runtime/hooks/instrumentMiddlewareHandler.ts @@ -1,8 +1,11 @@ import { + captureException, debug, flushIfServerless, getDefaultIsolationScope, getIsolationScope, + SEMANTIC_ATTRIBUTE_SENTRY_OP, + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, SPAN_STATUS_ERROR, SPAN_STATUS_OK, @@ -23,18 +26,18 @@ export function instrumentMiddlewareHandler(handler: EventHandler, fileName: str debug.log(`Sentry middleware: ${fileName} handling ${middlewarePath}`); + const origin = 'auto.http.nuxt'; const isolationScope = getIsolationScope(); const newIsolationScope = isolationScope === getDefaultIsolationScope() ? isolationScope.clone() : isolationScope; return withIsolationScope(newIsolationScope, async () => { return startSpan( { - name: `middleware.${fileName}`, - op: 'middleware.nitro', + name: `${fileName}`, attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom', - 'middleware.name': fileName, - 'middleware.path': middlewarePath, + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.server.middleware', + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: origin, }, }, async span => { @@ -45,6 +48,15 @@ export function instrumentMiddlewareHandler(handler: EventHandler, fileName: str } catch (error) { span.setStatus({ code: SPAN_STATUS_ERROR, message: 'internal_error' }); span.recordException(error); + captureException(error, { + mechanism: { + handled: false, + type: origin, + }, + }); + + span.end(); + // Re-throw the error to be handled by the caller throw error; } finally { await flushIfServerless(); From 2325676b5fc6d58edbb81a420f994cf67cb7bc36 Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Fri, 26 Sep 2025 13:43:12 +0300 Subject: [PATCH 04/17] refactor: rename wrapping fn --- ...ntMiddlewareHandler.ts => wrapMiddlewareHandler.ts} | 4 ++-- packages/nuxt/src/vite/middlewareConfig.ts | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) rename packages/nuxt/src/runtime/hooks/{instrumentMiddlewareHandler.ts => wrapMiddlewareHandler.ts} (93%) diff --git a/packages/nuxt/src/runtime/hooks/instrumentMiddlewareHandler.ts b/packages/nuxt/src/runtime/hooks/wrapMiddlewareHandler.ts similarity index 93% rename from packages/nuxt/src/runtime/hooks/instrumentMiddlewareHandler.ts rename to packages/nuxt/src/runtime/hooks/wrapMiddlewareHandler.ts index b25e4709015f..0d7bf49ef394 100644 --- a/packages/nuxt/src/runtime/hooks/instrumentMiddlewareHandler.ts +++ b/packages/nuxt/src/runtime/hooks/wrapMiddlewareHandler.ts @@ -15,12 +15,12 @@ import { import type { EventHandler, EventHandlerRequest, H3Event } from 'h3'; /** - * Instruments a middleware handler. + * Wraps a middleware handler with Sentry instrumentation. * * @param handler The middleware handler. * @param fileName The name of the middleware file. */ -export function instrumentMiddlewareHandler(handler: EventHandler, fileName: string) { +export function wrapMiddlewareHandler(handler: EventHandler, fileName: string) { return async (event: H3Event) => { const middlewarePath = event?.path || event?.node?.req?.url || 'unknown'; diff --git a/packages/nuxt/src/vite/middlewareConfig.ts b/packages/nuxt/src/vite/middlewareConfig.ts index d3a4658247dd..392236cff8f4 100644 --- a/packages/nuxt/src/vite/middlewareConfig.ts +++ b/packages/nuxt/src/vite/middlewareConfig.ts @@ -9,8 +9,8 @@ import type { InputPluginOption } from 'rollup'; export function addMiddlewareImports(): void { addServerImports([ { - name: 'instrumentMiddlewareHandler', - from: createResolver(import.meta.url).resolve('./runtime/hooks/instrumentMiddlewareHandler'), + name: 'wrapMiddlewareHandler', + from: createResolver(import.meta.url).resolve('./runtime/hooks/wrapMiddlewareHandler'), }, ]); } @@ -78,18 +78,18 @@ function middlewareInstrumentationPlugin(nitro: Nitro): InputPluginOption { */ function wrapMiddlewareCode(originalCode: string, fileName: string): string { return ` -import { instrumentMiddlewareHandler } from '#imports'; +import { wrapMiddlewareHandler } from '#imports'; function defineInstrumentedEventHandler(handlerOrObject) { // Handle function syntax if (typeof handlerOrObject === 'function') { - return defineEventHandler(instrumentMiddlewareHandler(handlerOrObject, '${fileName}')); + return defineEventHandler(wrapMiddlewareHandler(handlerOrObject, '${fileName}')); } // Handle object syntax return defineEventHandler({ ...handlerOrObject, - handler: instrumentMiddlewareHandler(handlerOrObject.handler, '${fileName}') + handler: wrapMiddlewareHandler(handlerOrObject.handler, '${fileName}') }); } From 1280c43d85875fdf0160d724ff3175b14eec85d3 Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Fri, 26 Sep 2025 14:08:06 +0300 Subject: [PATCH 05/17] feat: enrich span attributes with request data --- .../runtime/hooks/wrapMiddlewareHandler.ts | 77 ++++++++++++++++--- 1 file changed, 65 insertions(+), 12 deletions(-) diff --git a/packages/nuxt/src/runtime/hooks/wrapMiddlewareHandler.ts b/packages/nuxt/src/runtime/hooks/wrapMiddlewareHandler.ts index 0d7bf49ef394..d7a8f634ea3d 100644 --- a/packages/nuxt/src/runtime/hooks/wrapMiddlewareHandler.ts +++ b/packages/nuxt/src/runtime/hooks/wrapMiddlewareHandler.ts @@ -1,9 +1,14 @@ import { + type RequestEventData, + type SpanAttributes, captureException, debug, flushIfServerless, + getClient, getDefaultIsolationScope, getIsolationScope, + httpHeadersToSpanAttributes, + httpRequestToRequestData, SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, @@ -22,28 +27,28 @@ import type { EventHandler, EventHandlerRequest, H3Event } from 'h3'; */ export function wrapMiddlewareHandler(handler: EventHandler, fileName: string) { return async (event: H3Event) => { - const middlewarePath = event?.path || event?.node?.req?.url || 'unknown'; + debug.log(`Sentry middleware: ${fileName} handling ${event.path}`); - debug.log(`Sentry middleware: ${fileName} handling ${middlewarePath}`); - - const origin = 'auto.http.nuxt'; const isolationScope = getIsolationScope(); const newIsolationScope = isolationScope === getDefaultIsolationScope() ? isolationScope.clone() : isolationScope; + const normalizedRequest = createNormalizedRequestData(event); + newIsolationScope.setSDKProcessingMetadata({ + normalizedRequest, + }); + + const attributes = getSpanAttributes(event); return withIsolationScope(newIsolationScope, async () => { return startSpan( { - name: `${fileName}`, - attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.server.middleware', - [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: origin, - }, + name: fileName, + attributes, }, async span => { try { const result = await handler(event); span.setStatus({ code: SPAN_STATUS_OK }); + return result; } catch (error) { span.setStatus({ code: SPAN_STATUS_ERROR, message: 'internal_error' }); @@ -51,11 +56,11 @@ export function wrapMiddlewareHandler(handler: EventHandler, fileName: string) { captureException(error, { mechanism: { handled: false, - type: origin, + type: attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN], }, }); - span.end(); + // Re-throw the error to be handled by the caller throw error; } finally { @@ -66,3 +71,51 @@ export function wrapMiddlewareHandler(handler: EventHandler, fileName: string) { }); }; } + +/** + * Creates the normalized request data for the middleware handler based on the event. + */ +function createNormalizedRequestData(event: H3Event): RequestEventData { + // Extract headers from the Node.js request object + const headers = event.node?.req?.headers || {}; + + return httpRequestToRequestData({ + method: event.method, + url: event.path || event.node?.req?.url, + headers, + }); +} + +/** + * Gets the span attributes for the middleware handler based on the event. + */ +function getSpanAttributes(event: H3Event): SpanAttributes { + const attributes: SpanAttributes = { + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.server.middleware', + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.nuxt', + }; + + // Add HTTP method + if (event.method) { + attributes['http.request.method'] = event.method; + } + + // Add route information + if (event.path) { + attributes['http.route'] = event.path; + } + + // Extract and add HTTP headers as span attributes + const client = getClient(); + const sendDefaultPii = client?.getOptions().sendDefaultPii ?? false; + + // Get headers from the Node.js request object + const headers = event.node?.req?.headers || {}; + const headerAttributes = httpHeadersToSpanAttributes(headers, sendDefaultPii); + + // Merge header attributes with existing attributes + Object.assign(attributes, headerAttributes); + + return attributes; +} From 2e2833554dd941b35bf588914a5ce8b111a26553 Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Fri, 26 Sep 2025 14:33:44 +0300 Subject: [PATCH 06/17] test: added basic tests --- .../runtime/hooks/wrapMiddlewareHandler.ts | 5 +- .../hooks/wrapMiddlewareHandler.test.ts | 201 ++++++++++++++++++ 2 files changed, 204 insertions(+), 2 deletions(-) create mode 100644 packages/nuxt/test/runtime/hooks/wrapMiddlewareHandler.test.ts diff --git a/packages/nuxt/src/runtime/hooks/wrapMiddlewareHandler.ts b/packages/nuxt/src/runtime/hooks/wrapMiddlewareHandler.ts index d7a8f634ea3d..457e6629a72f 100644 --- a/packages/nuxt/src/runtime/hooks/wrapMiddlewareHandler.ts +++ b/packages/nuxt/src/runtime/hooks/wrapMiddlewareHandler.ts @@ -36,7 +36,7 @@ export function wrapMiddlewareHandler(handler: EventHandler, fileName: string) { normalizedRequest, }); - const attributes = getSpanAttributes(event); + const attributes = getSpanAttributes(event, fileName); return withIsolationScope(newIsolationScope, async () => { return startSpan( @@ -89,11 +89,12 @@ function createNormalizedRequestData(event: H3Event): Reque /** * Gets the span attributes for the middleware handler based on the event. */ -function getSpanAttributes(event: H3Event): SpanAttributes { +function getSpanAttributes(event: H3Event, fileName: string): SpanAttributes { const attributes: SpanAttributes = { [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.server.middleware', [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.nuxt', + 'nuxt.middleware.name': fileName, }; // Add HTTP method diff --git a/packages/nuxt/test/runtime/hooks/wrapMiddlewareHandler.test.ts b/packages/nuxt/test/runtime/hooks/wrapMiddlewareHandler.test.ts new file mode 100644 index 000000000000..7efb1067d464 --- /dev/null +++ b/packages/nuxt/test/runtime/hooks/wrapMiddlewareHandler.test.ts @@ -0,0 +1,201 @@ +import * as SentryCore from '@sentry/core'; +import type { EventHandler, EventHandlerRequest, H3Event } from 'h3'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { wrapMiddlewareHandler } from '../../../src/runtime/hooks/wrapMiddlewareHandler'; + +// Only mock the Sentry APIs we need to verify +vi.mock('@sentry/core', async importOriginal => { + const mod = await importOriginal(); + return { + ...(mod as any), + debug: { log: vi.fn() }, + startSpan: vi.fn(), + withIsolationScope: vi.fn(), + getIsolationScope: vi.fn(), + getDefaultIsolationScope: vi.fn(), + getClient: vi.fn(), + httpHeadersToSpanAttributes: vi.fn(), + httpRequestToRequestData: vi.fn(), + captureException: vi.fn(), + flushIfServerless: vi.fn(), + }; +}); + +describe('wrapMiddlewareHandler', () => { + const mockEvent: H3Event = { + path: '/test-path', + method: 'GET', + node: { + req: { + headers: { 'user-agent': 'test-agent' }, + url: '/test-url', + }, + }, + } as any; + + const mockSpan = { + setStatus: vi.fn(), + recordException: vi.fn(), + end: vi.fn(), + }; + + const mockIsolationScope = { + clone: vi.fn().mockReturnValue('cloned-scope'), + setSDKProcessingMetadata: vi.fn(), + }; + + beforeEach(() => { + vi.clearAllMocks(); + + // Setup minimal required mocks + (SentryCore.getIsolationScope as any).mockReturnValue(mockIsolationScope); + (SentryCore.getDefaultIsolationScope as any).mockReturnValue('default-scope'); + (SentryCore.withIsolationScope as any).mockImplementation((_scope: any, callback: any) => callback()); + (SentryCore.startSpan as any).mockImplementation((_config: any, callback: any) => callback(mockSpan)); + (SentryCore.getClient as any).mockReturnValue({ getOptions: () => ({ sendDefaultPii: false }) }); + (SentryCore.httpHeadersToSpanAttributes as any).mockReturnValue({ 'http.request.header.user_agent': 'test-agent' }); + (SentryCore.httpRequestToRequestData as any).mockReturnValue({ url: '/test-path', method: 'GET' }); + (SentryCore.flushIfServerless as any).mockResolvedValue(undefined); + }); + + describe('function handler wrapping', () => { + it('should wrap function handlers correctly and preserve return values', async () => { + const functionHandler: EventHandler = vi.fn().mockResolvedValue('success'); + + const wrapped = wrapMiddlewareHandler(functionHandler, 'test-middleware'); + const result = await wrapped(mockEvent); + + expect(functionHandler).toHaveBeenCalledWith(mockEvent); + expect(result).toBe('success'); + expect(typeof wrapped).toBe('function'); + }); + + it('should preserve sync return values from function handlers', async () => { + const syncHandler: EventHandler = vi.fn().mockReturnValue('sync-result'); + + const wrapped = wrapMiddlewareHandler(syncHandler, 'sync-middleware'); + const result = await wrapped(mockEvent); + + expect(syncHandler).toHaveBeenCalledWith(mockEvent); + expect(result).toBe('sync-result'); + }); + }); + + describe('different handler types', () => { + it('should handle async function handlers', async () => { + const asyncHandler: EventHandler = vi.fn().mockResolvedValue('async-success'); + + const wrapped = wrapMiddlewareHandler(asyncHandler, 'async-middleware'); + const result = await wrapped(mockEvent); + + expect(asyncHandler).toHaveBeenCalledWith(mockEvent); + expect(result).toBe('async-success'); + }); + }); + + describe('error propagation without masking', () => { + it('should propagate async errors without modification', async () => { + const originalError = new Error('Original async error'); + originalError.stack = 'original-stack-trace'; + const failingHandler: EventHandler = vi.fn().mockRejectedValue(originalError); + + const wrapped = wrapMiddlewareHandler(failingHandler, 'failing-middleware'); + + await expect(wrapped(mockEvent)).rejects.toThrow('Original async error'); + await expect(wrapped(mockEvent)).rejects.toMatchObject({ + message: 'Original async error', + stack: 'original-stack-trace', + }); + + // Verify Sentry APIs were called but error was not masked + expect(SentryCore.captureException).toHaveBeenCalledWith(originalError, expect.any(Object)); + expect(mockSpan.recordException).toHaveBeenCalledWith(originalError); + }); + + it('should propagate sync errors without modification', async () => { + const originalError = new Error('Original sync error'); + const failingHandler: EventHandler = vi.fn().mockImplementation(() => { + throw originalError; + }); + + const wrapped = wrapMiddlewareHandler(failingHandler, 'sync-failing-middleware'); + + await expect(wrapped(mockEvent)).rejects.toThrow('Original sync error'); + await expect(wrapped(mockEvent)).rejects.toBe(originalError); + + expect(SentryCore.captureException).toHaveBeenCalledWith(originalError, expect.any(Object)); + }); + + it('should handle non-Error thrown values', async () => { + const stringError = 'String error'; + const failingHandler: EventHandler = vi.fn().mockRejectedValue(stringError); + + const wrapped = wrapMiddlewareHandler(failingHandler, 'string-error-middleware'); + + await expect(wrapped(mockEvent)).rejects.toBe(stringError); + expect(SentryCore.captureException).toHaveBeenCalledWith(stringError, expect.any(Object)); + }); + }); + + describe('user code isolation', () => { + it('should not affect user code when Sentry APIs fail', async () => { + // Simulate Sentry API failures + (SentryCore.startSpan as any).mockImplementation(() => { + throw new Error('Sentry API failure'); + }); + + const userHandler: EventHandler = vi.fn().mockResolvedValue('user-result'); + + // Should not throw despite Sentry failure + const wrapped = wrapMiddlewareHandler(userHandler, 'isolated-middleware'); + + // This should handle the Sentry error gracefully and still call user code + await expect(wrapped(mockEvent)).rejects.toThrow('Sentry API failure'); + + // But user handler should still have been attempted to be called + // (this tests that we don't fail before reaching user code) + }); + }); + + describe('Sentry API integration', () => { + it('should call Sentry APIs with correct parameters', async () => { + const userHandler: EventHandler = vi.fn().mockResolvedValue('api-test-result'); + + const wrapped = wrapMiddlewareHandler(userHandler, 'api-middleware'); + await wrapped(mockEvent); + + // Verify key Sentry APIs are called correctly + expect(SentryCore.startSpan).toHaveBeenCalledWith( + expect.objectContaining({ + name: 'api-middleware', + attributes: expect.objectContaining({ + [SentryCore.SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.server.middleware', + 'nuxt.middleware.name': 'api-middleware', + 'http.request.method': 'GET', + 'http.route': '/test-path', + }), + }), + expect.any(Function), + ); + + expect(SentryCore.httpRequestToRequestData).toHaveBeenCalledWith({ + method: 'GET', + url: '/test-path', + headers: { 'user-agent': 'test-agent' }, + }); + }); + + it('should handle missing optional data gracefully', async () => { + const minimalEvent = { path: '/minimal' } as H3Event; + const userHandler: EventHandler = vi.fn().mockResolvedValue('minimal-result'); + + const wrapped = wrapMiddlewareHandler(userHandler, 'minimal-middleware'); + const result = await wrapped(minimalEvent); + + expect(result).toBe('minimal-result'); + expect(userHandler).toHaveBeenCalledWith(minimalEvent); + // Should still create span even with minimal data + expect(SentryCore.startSpan).toHaveBeenCalled(); + }); + }); +}); From a99d92aeb89cffc133cb4eb989666384baf9fc99 Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Fri, 26 Sep 2025 16:33:21 +0300 Subject: [PATCH 07/17] tests: added middleware e2e tests --- .../nuxt-3/server/api/middleware-test.ts | 15 ++++ .../nuxt-3/server/middleware/01.first.ts | 6 ++ .../nuxt-3/server/middleware/02.second.ts | 6 ++ .../nuxt-3/server/middleware/03.auth.ts | 6 ++ .../nuxt-3/tests/middleware.test.ts | 78 +++++++++++++++++++ .../nuxt-4/server/api/middleware-test.ts | 15 ++++ .../nuxt-4/server/middleware/01.first.ts | 6 ++ .../nuxt-4/server/middleware/02.second.ts | 6 ++ .../nuxt-4/server/middleware/03.auth.ts | 6 ++ .../nuxt-4/tests/middleware.test.ts | 78 +++++++++++++++++++ 10 files changed, 222 insertions(+) create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-3/server/api/middleware-test.ts create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-3/server/middleware/01.first.ts create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-3/server/middleware/02.second.ts create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-3/server/middleware/03.auth.ts create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-3/tests/middleware.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-4/server/api/middleware-test.ts create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-4/server/middleware/01.first.ts create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-4/server/middleware/02.second.ts create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-4/server/middleware/03.auth.ts create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-4/tests/middleware.test.ts diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3/server/api/middleware-test.ts b/dev-packages/e2e-tests/test-applications/nuxt-3/server/api/middleware-test.ts new file mode 100644 index 000000000000..8973690e6adb --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3/server/api/middleware-test.ts @@ -0,0 +1,15 @@ +import { defineEventHandler, getHeader } from '#imports'; + +export default defineEventHandler(async event => { + // Simple API endpoint that will trigger all server middleware + return { + message: 'Server middleware test endpoint', + path: event.path, + method: event.method, + headers: { + 'x-first-middleware': getHeader(event, 'x-first-middleware'), + 'x-second-middleware': getHeader(event, 'x-second-middleware'), + 'x-auth-middleware': getHeader(event, 'x-auth-middleware'), + }, + }; +}); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3/server/middleware/01.first.ts b/dev-packages/e2e-tests/test-applications/nuxt-3/server/middleware/01.first.ts new file mode 100644 index 000000000000..b146c42e3483 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3/server/middleware/01.first.ts @@ -0,0 +1,6 @@ +import { defineEventHandler, setHeader } from '#imports'; + +export default defineEventHandler(async event => { + // Set a header to indicate this middleware ran + setHeader(event, 'x-first-middleware', 'executed'); +}); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3/server/middleware/02.second.ts b/dev-packages/e2e-tests/test-applications/nuxt-3/server/middleware/02.second.ts new file mode 100644 index 000000000000..7534051af316 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3/server/middleware/02.second.ts @@ -0,0 +1,6 @@ +import { defineEventHandler, setHeader } from '#imports'; + +export default defineEventHandler(async event => { + // Set a header to indicate this middleware ran + setHeader(event, 'x-second-middleware', 'executed'); +}); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3/server/middleware/03.auth.ts b/dev-packages/e2e-tests/test-applications/nuxt-3/server/middleware/03.auth.ts new file mode 100644 index 000000000000..47a63827f65e --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3/server/middleware/03.auth.ts @@ -0,0 +1,6 @@ +import { defineEventHandler, setHeader } from '#imports'; + +export default defineEventHandler(async event => { + // Set a header to indicate this middleware ran + setHeader(event, 'x-auth-middleware', 'executed'); +}); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3/tests/middleware.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-3/tests/middleware.test.ts new file mode 100644 index 000000000000..c5e2d88afc7d --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3/tests/middleware.test.ts @@ -0,0 +1,78 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; + +test.describe('Server Middleware Instrumentation', () => { + test('should create separate spans for each server middleware', async ({ request }) => { + const serverTxnEventPromise = waitForTransaction('nuxt-3', txnEvent => { + return txnEvent.transaction?.includes('GET /api/middleware-test') ?? false; + }); + + // Make request to the API endpoint that will trigger all server middleware + const response = await request.get('/api/middleware-test'); + expect(response.status()).toBe(200); + + const responseData = await response.json(); + expect(responseData.message).toBe('Server middleware test endpoint'); + + const serverTxnEvent = await serverTxnEventPromise; + + // Verify that we have spans for each middleware + const middlewareSpans = serverTxnEvent.spans?.filter(span => span.op === 'http.server.middleware') || []; + + expect(middlewareSpans).toHaveLength(3); + + // Check for specific middleware spans + const firstMiddlewareSpan = middlewareSpans.find(span => span.data?.['nuxt.middleware.name'] === '01.first.ts'); + const secondMiddlewareSpan = middlewareSpans.find(span => span.data?.['nuxt.middleware.name'] === '02.second.ts'); + const authMiddlewareSpan = middlewareSpans.find(span => span.data?.['nuxt.middleware.name'] === '03.auth.ts'); + + expect(firstMiddlewareSpan).toBeDefined(); + expect(secondMiddlewareSpan).toBeDefined(); + expect(authMiddlewareSpan).toBeDefined(); + + // Verify each span has the correct attributes + [firstMiddlewareSpan, secondMiddlewareSpan, authMiddlewareSpan].forEach(span => { + expect(span).toEqual( + expect.objectContaining({ + op: 'http.server.middleware', + data: expect.objectContaining({ + 'sentry.op': 'http.server.middleware', + 'sentry.origin': 'auto.http.nuxt', + 'sentry.source': 'custom', + 'http.request.method': 'GET', + 'http.route': '/api/middleware-test', + }), + parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), + span_id: expect.stringMatching(/[a-f0-9]{16}/), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + }), + ); + }); + + // Verify spans have different span IDs (each middleware gets its own span) + const spanIds = middlewareSpans.map(span => span.span_id); + const uniqueSpanIds = new Set(spanIds); + expect(uniqueSpanIds.size).toBe(3); + + // Verify spans share the same trace ID + const traceIds = middlewareSpans.map(span => span.trace_id); + const uniqueTraceIds = new Set(traceIds); + expect(uniqueTraceIds.size).toBe(1); + }); + + test('middleware spans should have proper parent-child relationship', async ({ request }) => { + const serverTxnEventPromise = waitForTransaction('nuxt-3', txnEvent => { + return txnEvent.transaction?.includes('GET /api/middleware-test') ?? false; + }); + + await request.get('/api/middleware-test'); + const serverTxnEvent = await serverTxnEventPromise; + + const middlewareSpans = serverTxnEvent.spans?.filter(span => span.op === 'http.server.middleware') || []; + + // All middleware spans should be children of the main transaction + middlewareSpans.forEach(span => { + expect(span.parent_span_id).toBe(serverTxnEvent.contexts?.trace?.span_id); + }); + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/server/api/middleware-test.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/server/api/middleware-test.ts new file mode 100644 index 000000000000..8973690e6adb --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-4/server/api/middleware-test.ts @@ -0,0 +1,15 @@ +import { defineEventHandler, getHeader } from '#imports'; + +export default defineEventHandler(async event => { + // Simple API endpoint that will trigger all server middleware + return { + message: 'Server middleware test endpoint', + path: event.path, + method: event.method, + headers: { + 'x-first-middleware': getHeader(event, 'x-first-middleware'), + 'x-second-middleware': getHeader(event, 'x-second-middleware'), + 'x-auth-middleware': getHeader(event, 'x-auth-middleware'), + }, + }; +}); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/server/middleware/01.first.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/server/middleware/01.first.ts new file mode 100644 index 000000000000..b146c42e3483 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-4/server/middleware/01.first.ts @@ -0,0 +1,6 @@ +import { defineEventHandler, setHeader } from '#imports'; + +export default defineEventHandler(async event => { + // Set a header to indicate this middleware ran + setHeader(event, 'x-first-middleware', 'executed'); +}); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/server/middleware/02.second.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/server/middleware/02.second.ts new file mode 100644 index 000000000000..7534051af316 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-4/server/middleware/02.second.ts @@ -0,0 +1,6 @@ +import { defineEventHandler, setHeader } from '#imports'; + +export default defineEventHandler(async event => { + // Set a header to indicate this middleware ran + setHeader(event, 'x-second-middleware', 'executed'); +}); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/server/middleware/03.auth.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/server/middleware/03.auth.ts new file mode 100644 index 000000000000..47a63827f65e --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-4/server/middleware/03.auth.ts @@ -0,0 +1,6 @@ +import { defineEventHandler, setHeader } from '#imports'; + +export default defineEventHandler(async event => { + // Set a header to indicate this middleware ran + setHeader(event, 'x-auth-middleware', 'executed'); +}); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/tests/middleware.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/middleware.test.ts new file mode 100644 index 000000000000..334045dc8900 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/middleware.test.ts @@ -0,0 +1,78 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; + +test.describe('Server Middleware Instrumentation', () => { + test('should create separate spans for each server middleware', async ({ request }) => { + const serverTxnEventPromise = waitForTransaction('nuxt-4', txnEvent => { + return txnEvent.transaction?.includes('GET /api/middleware-test') ?? false; + }); + + // Make request to the API endpoint that will trigger all server middleware + const response = await request.get('/api/middleware-test'); + expect(response.status()).toBe(200); + + const responseData = await response.json(); + expect(responseData.message).toBe('Server middleware test endpoint'); + + const serverTxnEvent = await serverTxnEventPromise; + + // Verify that we have spans for each middleware + const middlewareSpans = serverTxnEvent.spans?.filter(span => span.op === 'http.server.middleware') || []; + + expect(middlewareSpans).toHaveLength(3); + + // Check for specific middleware spans + const firstMiddlewareSpan = middlewareSpans.find(span => span.data?.['nuxt.middleware.name'] === '01.first.ts'); + const secondMiddlewareSpan = middlewareSpans.find(span => span.data?.['nuxt.middleware.name'] === '02.second.ts'); + const authMiddlewareSpan = middlewareSpans.find(span => span.data?.['nuxt.middleware.name'] === '03.auth.ts'); + + expect(firstMiddlewareSpan).toBeDefined(); + expect(secondMiddlewareSpan).toBeDefined(); + expect(authMiddlewareSpan).toBeDefined(); + + // Verify each span has the correct attributes + [firstMiddlewareSpan, secondMiddlewareSpan, authMiddlewareSpan].forEach(span => { + expect(span).toEqual( + expect.objectContaining({ + op: 'http.server.middleware', + data: expect.objectContaining({ + 'sentry.op': 'http.server.middleware', + 'sentry.origin': 'auto.http.nuxt', + 'sentry.source': 'custom', + 'http.request.method': 'GET', + 'http.route': '/api/middleware-test', + }), + parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), + span_id: expect.stringMatching(/[a-f0-9]{16}/), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + }), + ); + }); + + // Verify spans have different span IDs (each middleware gets its own span) + const spanIds = middlewareSpans.map(span => span.span_id); + const uniqueSpanIds = new Set(spanIds); + expect(uniqueSpanIds.size).toBe(3); + + // Verify spans share the same trace ID + const traceIds = middlewareSpans.map(span => span.trace_id); + const uniqueTraceIds = new Set(traceIds); + expect(uniqueTraceIds.size).toBe(1); + }); + + test('middleware spans should have proper parent-child relationship', async ({ request }) => { + const serverTxnEventPromise = waitForTransaction('nuxt-4', txnEvent => { + return txnEvent.transaction?.includes('GET /api/middleware-test') ?? false; + }); + + await request.get('/api/middleware-test'); + const serverTxnEvent = await serverTxnEventPromise; + + const middlewareSpans = serverTxnEvent.spans?.filter(span => span.op === 'http.server.middleware') || []; + + // All middleware spans should be children of the main transaction + middlewareSpans.forEach(span => { + expect(span.parent_span_id).toBe(serverTxnEvent.contexts?.trace?.span_id); + }); + }); +}); From 36cfbc07c7cf14e035c1e959964f5c2046bf5c9a Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Fri, 26 Sep 2025 16:38:35 +0300 Subject: [PATCH 08/17] test: added middleware span error test --- .../nuxt-3/server/middleware/03.auth.ts | 8 +++- .../nuxt-3/tests/middleware.test.ts | 45 ++++++++++++++++++- .../nuxt-4/server/middleware/03.auth.ts | 8 +++- .../nuxt-4/tests/middleware.test.ts | 45 ++++++++++++++++++- 4 files changed, 102 insertions(+), 4 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3/server/middleware/03.auth.ts b/dev-packages/e2e-tests/test-applications/nuxt-3/server/middleware/03.auth.ts index 47a63827f65e..6dcd9a075589 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-3/server/middleware/03.auth.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-3/server/middleware/03.auth.ts @@ -1,6 +1,12 @@ -import { defineEventHandler, setHeader } from '#imports'; +import { defineEventHandler, setHeader, getQuery } from '#imports'; export default defineEventHandler(async event => { + // Check if we should throw an error + const query = getQuery(event); + if (query.throwError === 'true') { + throw new Error('Auth middleware error'); + } + // Set a header to indicate this middleware ran setHeader(event, 'x-auth-middleware', 'executed'); }); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3/tests/middleware.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-3/tests/middleware.test.ts index c5e2d88afc7d..d08c36ebba98 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-3/tests/middleware.test.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-3/tests/middleware.test.ts @@ -1,5 +1,5 @@ import { expect, test } from '@playwright/test'; -import { waitForTransaction } from '@sentry-internal/test-utils'; +import { waitForTransaction, waitForError } from '@sentry-internal/test-utils'; test.describe('Server Middleware Instrumentation', () => { test('should create separate spans for each server middleware', async ({ request }) => { @@ -75,4 +75,47 @@ test.describe('Server Middleware Instrumentation', () => { expect(span.parent_span_id).toBe(serverTxnEvent.contexts?.trace?.span_id); }); }); + + test('should capture errors thrown in middleware and associate them with the span', async ({ request }) => { + const serverTxnEventPromise = waitForTransaction('nuxt-3', txnEvent => { + return txnEvent.transaction?.includes('GET /api/middleware-test') ?? false; + }); + + const errorEventPromise = waitForError('nuxt-3', errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === 'Auth middleware error'; + }); + + // Make request with query param to trigger error in auth middleware + const response = await request.get('/api/middleware-test?throwError=true'); + + // The request should fail due to the middleware error + expect(response.status()).toBe(500); + + const [serverTxnEvent, errorEvent] = await Promise.all([serverTxnEventPromise, errorEventPromise]); + + // Find the auth middleware span + const authMiddlewareSpan = serverTxnEvent.spans?.find( + span => span.op === 'http.server.middleware' && span.data?.['nuxt.middleware.name'] === '03.auth.ts', + ); + + expect(authMiddlewareSpan).toBeDefined(); + + // Verify the span has error status + expect(authMiddlewareSpan?.status).toBe('internal_error'); + + // Verify the error event is associated with the correct transaction + expect(errorEvent.transaction).toContain('GET /api/middleware-test'); + + // Verify the error has the correct mechanism + expect(errorEvent.exception?.values?.[0]).toEqual( + expect.objectContaining({ + value: 'Auth middleware error', + type: 'Error', + mechanism: expect.objectContaining({ + handled: false, + type: 'auto.http.nuxt', + }), + }), + ); + }); }); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/server/middleware/03.auth.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/server/middleware/03.auth.ts index 47a63827f65e..6dcd9a075589 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-4/server/middleware/03.auth.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-4/server/middleware/03.auth.ts @@ -1,6 +1,12 @@ -import { defineEventHandler, setHeader } from '#imports'; +import { defineEventHandler, setHeader, getQuery } from '#imports'; export default defineEventHandler(async event => { + // Check if we should throw an error + const query = getQuery(event); + if (query.throwError === 'true') { + throw new Error('Auth middleware error'); + } + // Set a header to indicate this middleware ran setHeader(event, 'x-auth-middleware', 'executed'); }); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/tests/middleware.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/middleware.test.ts index 334045dc8900..9b46d3e1f955 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-4/tests/middleware.test.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/middleware.test.ts @@ -1,5 +1,5 @@ import { expect, test } from '@playwright/test'; -import { waitForTransaction } from '@sentry-internal/test-utils'; +import { waitForTransaction, waitForError } from '@sentry-internal/test-utils'; test.describe('Server Middleware Instrumentation', () => { test('should create separate spans for each server middleware', async ({ request }) => { @@ -75,4 +75,47 @@ test.describe('Server Middleware Instrumentation', () => { expect(span.parent_span_id).toBe(serverTxnEvent.contexts?.trace?.span_id); }); }); + + test('should capture errors thrown in middleware and associate them with the span', async ({ request }) => { + const serverTxnEventPromise = waitForTransaction('nuxt-4', txnEvent => { + return txnEvent.transaction?.includes('GET /api/middleware-test') ?? false; + }); + + const errorEventPromise = waitForError('nuxt-4', errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === 'Auth middleware error'; + }); + + // Make request with query param to trigger error in auth middleware + const response = await request.get('/api/middleware-test?throwError=true'); + + // The request should fail due to the middleware error + expect(response.status()).toBe(500); + + const [serverTxnEvent, errorEvent] = await Promise.all([serverTxnEventPromise, errorEventPromise]); + + // Find the auth middleware span + const authMiddlewareSpan = serverTxnEvent.spans?.find( + span => span.op === 'http.server.middleware' && span.data?.['nuxt.middleware.name'] === '03.auth.ts', + ); + + expect(authMiddlewareSpan).toBeDefined(); + + // Verify the span has error status + expect(authMiddlewareSpan?.status).toBe('internal_error'); + + // Verify the error event is associated with the correct transaction + expect(errorEvent.transaction).toContain('GET /api/middleware-test'); + + // Verify the error has the correct mechanism + expect(errorEvent.exception?.values?.[0]).toEqual( + expect.objectContaining({ + value: 'Auth middleware error', + type: 'Error', + mechanism: expect.objectContaining({ + handled: false, + type: 'auto.http.nuxt', + }), + }), + ); + }); }); From 5bf3e30f44c6bd242d017483760a0546fff1a9f8 Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Mon, 29 Sep 2025 13:03:41 +0300 Subject: [PATCH 09/17] fix: remove uneeded isolation scope calls --- .../runtime/hooks/wrapMiddlewareHandler.ts | 80 ++++++------------- .../hooks/wrapMiddlewareHandler.test.ts | 25 +----- 2 files changed, 25 insertions(+), 80 deletions(-) diff --git a/packages/nuxt/src/runtime/hooks/wrapMiddlewareHandler.ts b/packages/nuxt/src/runtime/hooks/wrapMiddlewareHandler.ts index 457e6629a72f..e990a5fff631 100644 --- a/packages/nuxt/src/runtime/hooks/wrapMiddlewareHandler.ts +++ b/packages/nuxt/src/runtime/hooks/wrapMiddlewareHandler.ts @@ -1,21 +1,15 @@ import { - type RequestEventData, type SpanAttributes, captureException, debug, flushIfServerless, getClient, - getDefaultIsolationScope, - getIsolationScope, httpHeadersToSpanAttributes, - httpRequestToRequestData, SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, - SPAN_STATUS_ERROR, SPAN_STATUS_OK, startSpan, - withIsolationScope, } from '@sentry/core'; import type { EventHandler, EventHandlerRequest, H3Event } from 'h3'; @@ -29,63 +23,37 @@ export function wrapMiddlewareHandler(handler: EventHandler, fileName: string) { return async (event: H3Event) => { debug.log(`Sentry middleware: ${fileName} handling ${event.path}`); - const isolationScope = getIsolationScope(); - const newIsolationScope = isolationScope === getDefaultIsolationScope() ? isolationScope.clone() : isolationScope; - const normalizedRequest = createNormalizedRequestData(event); - newIsolationScope.setSDKProcessingMetadata({ - normalizedRequest, - }); - const attributes = getSpanAttributes(event, fileName); - return withIsolationScope(newIsolationScope, async () => { - return startSpan( - { - name: fileName, - attributes, - }, - async span => { - try { - const result = await handler(event); - span.setStatus({ code: SPAN_STATUS_OK }); + return startSpan( + { + name: fileName, + attributes, + }, + async span => { + try { + const result = await handler(event); + span.setStatus({ code: SPAN_STATUS_OK }); - return result; - } catch (error) { - span.setStatus({ code: SPAN_STATUS_ERROR, message: 'internal_error' }); - span.recordException(error); - captureException(error, { - mechanism: { - handled: false, - type: attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN], - }, - }); - span.end(); + return result; + } catch (error) { + captureException(error, { + mechanism: { + handled: false, + type: attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN], + }, + }); - // Re-throw the error to be handled by the caller - throw error; - } finally { - await flushIfServerless(); - } - }, - ); - }); + // Re-throw the error to be handled by the caller + throw error; + } finally { + await flushIfServerless(); + } + }, + ); }; } -/** - * Creates the normalized request data for the middleware handler based on the event. - */ -function createNormalizedRequestData(event: H3Event): RequestEventData { - // Extract headers from the Node.js request object - const headers = event.node?.req?.headers || {}; - - return httpRequestToRequestData({ - method: event.method, - url: event.path || event.node?.req?.url, - headers, - }); -} - /** * Gets the span attributes for the middleware handler based on the event. */ diff --git a/packages/nuxt/test/runtime/hooks/wrapMiddlewareHandler.test.ts b/packages/nuxt/test/runtime/hooks/wrapMiddlewareHandler.test.ts index 7efb1067d464..e334ecde33bc 100644 --- a/packages/nuxt/test/runtime/hooks/wrapMiddlewareHandler.test.ts +++ b/packages/nuxt/test/runtime/hooks/wrapMiddlewareHandler.test.ts @@ -10,12 +10,8 @@ vi.mock('@sentry/core', async importOriginal => { ...(mod as any), debug: { log: vi.fn() }, startSpan: vi.fn(), - withIsolationScope: vi.fn(), - getIsolationScope: vi.fn(), - getDefaultIsolationScope: vi.fn(), getClient: vi.fn(), httpHeadersToSpanAttributes: vi.fn(), - httpRequestToRequestData: vi.fn(), captureException: vi.fn(), flushIfServerless: vi.fn(), }; @@ -39,22 +35,13 @@ describe('wrapMiddlewareHandler', () => { end: vi.fn(), }; - const mockIsolationScope = { - clone: vi.fn().mockReturnValue('cloned-scope'), - setSDKProcessingMetadata: vi.fn(), - }; - beforeEach(() => { vi.clearAllMocks(); // Setup minimal required mocks - (SentryCore.getIsolationScope as any).mockReturnValue(mockIsolationScope); - (SentryCore.getDefaultIsolationScope as any).mockReturnValue('default-scope'); - (SentryCore.withIsolationScope as any).mockImplementation((_scope: any, callback: any) => callback()); (SentryCore.startSpan as any).mockImplementation((_config: any, callback: any) => callback(mockSpan)); (SentryCore.getClient as any).mockReturnValue({ getOptions: () => ({ sendDefaultPii: false }) }); (SentryCore.httpHeadersToSpanAttributes as any).mockReturnValue({ 'http.request.header.user_agent': 'test-agent' }); - (SentryCore.httpRequestToRequestData as any).mockReturnValue({ url: '/test-path', method: 'GET' }); (SentryCore.flushIfServerless as any).mockResolvedValue(undefined); }); @@ -109,7 +96,6 @@ describe('wrapMiddlewareHandler', () => { // Verify Sentry APIs were called but error was not masked expect(SentryCore.captureException).toHaveBeenCalledWith(originalError, expect.any(Object)); - expect(mockSpan.recordException).toHaveBeenCalledWith(originalError); }); it('should propagate sync errors without modification', async () => { @@ -151,9 +137,6 @@ describe('wrapMiddlewareHandler', () => { // This should handle the Sentry error gracefully and still call user code await expect(wrapped(mockEvent)).rejects.toThrow('Sentry API failure'); - - // But user handler should still have been attempted to be called - // (this tests that we don't fail before reaching user code) }); }); @@ -169,7 +152,7 @@ describe('wrapMiddlewareHandler', () => { expect.objectContaining({ name: 'api-middleware', attributes: expect.objectContaining({ - [SentryCore.SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.server.middleware', + [SentryCore.SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'middleware.nuxt', 'nuxt.middleware.name': 'api-middleware', 'http.request.method': 'GET', 'http.route': '/test-path', @@ -177,12 +160,6 @@ describe('wrapMiddlewareHandler', () => { }), expect.any(Function), ); - - expect(SentryCore.httpRequestToRequestData).toHaveBeenCalledWith({ - method: 'GET', - url: '/test-path', - headers: { 'user-agent': 'test-agent' }, - }); }); it('should handle missing optional data gracefully', async () => { From 9537a683657da9c90537f3a1068006531f53da0d Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Mon, 29 Sep 2025 13:10:00 +0300 Subject: [PATCH 10/17] fix: follow span op conventions --- .../test-applications/nuxt-3/tests/middleware.test.ts | 10 +++++----- .../test-applications/nuxt-4/tests/middleware.test.ts | 10 +++++----- .../nuxt/src/runtime/hooks/wrapMiddlewareHandler.ts | 2 +- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3/tests/middleware.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-3/tests/middleware.test.ts index d08c36ebba98..7edbe94184d8 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-3/tests/middleware.test.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-3/tests/middleware.test.ts @@ -17,7 +17,7 @@ test.describe('Server Middleware Instrumentation', () => { const serverTxnEvent = await serverTxnEventPromise; // Verify that we have spans for each middleware - const middlewareSpans = serverTxnEvent.spans?.filter(span => span.op === 'http.server.middleware') || []; + const middlewareSpans = serverTxnEvent.spans?.filter(span => span.op === 'middleware.nuxt') || []; expect(middlewareSpans).toHaveLength(3); @@ -34,9 +34,9 @@ test.describe('Server Middleware Instrumentation', () => { [firstMiddlewareSpan, secondMiddlewareSpan, authMiddlewareSpan].forEach(span => { expect(span).toEqual( expect.objectContaining({ - op: 'http.server.middleware', + op: 'middleware.nuxt', data: expect.objectContaining({ - 'sentry.op': 'http.server.middleware', + 'sentry.op': 'middleware.nuxt', 'sentry.origin': 'auto.http.nuxt', 'sentry.source': 'custom', 'http.request.method': 'GET', @@ -68,7 +68,7 @@ test.describe('Server Middleware Instrumentation', () => { await request.get('/api/middleware-test'); const serverTxnEvent = await serverTxnEventPromise; - const middlewareSpans = serverTxnEvent.spans?.filter(span => span.op === 'http.server.middleware') || []; + const middlewareSpans = serverTxnEvent.spans?.filter(span => span.op === 'middleware.nuxt') || []; // All middleware spans should be children of the main transaction middlewareSpans.forEach(span => { @@ -95,7 +95,7 @@ test.describe('Server Middleware Instrumentation', () => { // Find the auth middleware span const authMiddlewareSpan = serverTxnEvent.spans?.find( - span => span.op === 'http.server.middleware' && span.data?.['nuxt.middleware.name'] === '03.auth.ts', + span => span.op === 'middleware.nuxt' && span.data?.['nuxt.middleware.name'] === '03.auth.ts', ); expect(authMiddlewareSpan).toBeDefined(); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/tests/middleware.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/middleware.test.ts index 9b46d3e1f955..d265a996955e 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-4/tests/middleware.test.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/middleware.test.ts @@ -17,7 +17,7 @@ test.describe('Server Middleware Instrumentation', () => { const serverTxnEvent = await serverTxnEventPromise; // Verify that we have spans for each middleware - const middlewareSpans = serverTxnEvent.spans?.filter(span => span.op === 'http.server.middleware') || []; + const middlewareSpans = serverTxnEvent.spans?.filter(span => span.op === 'middleware.nuxt') || []; expect(middlewareSpans).toHaveLength(3); @@ -34,9 +34,9 @@ test.describe('Server Middleware Instrumentation', () => { [firstMiddlewareSpan, secondMiddlewareSpan, authMiddlewareSpan].forEach(span => { expect(span).toEqual( expect.objectContaining({ - op: 'http.server.middleware', + op: 'middleware.nuxt', data: expect.objectContaining({ - 'sentry.op': 'http.server.middleware', + 'sentry.op': 'middleware.nuxt', 'sentry.origin': 'auto.http.nuxt', 'sentry.source': 'custom', 'http.request.method': 'GET', @@ -68,7 +68,7 @@ test.describe('Server Middleware Instrumentation', () => { await request.get('/api/middleware-test'); const serverTxnEvent = await serverTxnEventPromise; - const middlewareSpans = serverTxnEvent.spans?.filter(span => span.op === 'http.server.middleware') || []; + const middlewareSpans = serverTxnEvent.spans?.filter(span => span.op === 'middleware.nuxt') || []; // All middleware spans should be children of the main transaction middlewareSpans.forEach(span => { @@ -95,7 +95,7 @@ test.describe('Server Middleware Instrumentation', () => { // Find the auth middleware span const authMiddlewareSpan = serverTxnEvent.spans?.find( - span => span.op === 'http.server.middleware' && span.data?.['nuxt.middleware.name'] === '03.auth.ts', + span => span.op === 'middleware.nuxt' && span.data?.['nuxt.middleware.name'] === '03.auth.ts', ); expect(authMiddlewareSpan).toBeDefined(); diff --git a/packages/nuxt/src/runtime/hooks/wrapMiddlewareHandler.ts b/packages/nuxt/src/runtime/hooks/wrapMiddlewareHandler.ts index e990a5fff631..5c304f49e75b 100644 --- a/packages/nuxt/src/runtime/hooks/wrapMiddlewareHandler.ts +++ b/packages/nuxt/src/runtime/hooks/wrapMiddlewareHandler.ts @@ -59,7 +59,7 @@ export function wrapMiddlewareHandler(handler: EventHandler, fileName: string) { */ function getSpanAttributes(event: H3Event, fileName: string): SpanAttributes { const attributes: SpanAttributes = { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.server.middleware', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'middleware.nuxt', [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.nuxt', 'nuxt.middleware.name': fileName, From f1257b3ade5441ea0a2ca38e78ff91b049392978 Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Mon, 29 Sep 2025 15:02:38 +0300 Subject: [PATCH 11/17] feat: instrument onBeforeResponse and onRequest handlers --- .../runtime/hooks/wrapMiddlewareHandler.ts | 171 +++++++-- packages/nuxt/src/vite/middlewareConfig.ts | 14 +- .../hooks/wrapMiddlewareHandler.test.ts | 328 ++++++++++++++++++ 3 files changed, 468 insertions(+), 45 deletions(-) diff --git a/packages/nuxt/src/runtime/hooks/wrapMiddlewareHandler.ts b/packages/nuxt/src/runtime/hooks/wrapMiddlewareHandler.ts index 5c304f49e75b..a3a787ca3964 100644 --- a/packages/nuxt/src/runtime/hooks/wrapMiddlewareHandler.ts +++ b/packages/nuxt/src/runtime/hooks/wrapMiddlewareHandler.ts @@ -11,7 +11,14 @@ import { SPAN_STATUS_OK, startSpan, } from '@sentry/core'; -import type { EventHandler, EventHandlerRequest, H3Event } from 'h3'; +import type { + _ResponseMiddleware as ResponseMiddleware, + EventHandler, + EventHandlerObject, + EventHandlerRequest, + EventHandlerResponse, + H3Event, +} from 'h3'; /** * Wraps a middleware handler with Sentry instrumentation. @@ -19,52 +26,139 @@ import type { EventHandler, EventHandlerRequest, H3Event } from 'h3'; * @param handler The middleware handler. * @param fileName The name of the middleware file. */ -export function wrapMiddlewareHandler(handler: EventHandler, fileName: string) { - return async (event: H3Event) => { - debug.log(`Sentry middleware: ${fileName} handling ${event.path}`); - - const attributes = getSpanAttributes(event, fileName); - - return startSpan( - { - name: fileName, - attributes, - }, - async span => { - try { - const result = await handler(event); - span.setStatus({ code: SPAN_STATUS_OK }); - - return result; - } catch (error) { - captureException(error, { - mechanism: { - handled: false, - type: attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN], - }, - }); - - // Re-throw the error to be handled by the caller - throw error; - } finally { - await flushIfServerless(); - } - }, +export function wrapMiddlewareHandler( + handler: THandler, + fileName: string, +): THandler { + if (!isEventHandlerObject(handler)) { + return wrapEventHandler(handler, fileName) as THandler; + } + + const handlerObj = { + ...handler, + handler: wrapEventHandler(handler.handler, fileName), + }; + + if (handlerObj.onRequest) { + handlerObj.onRequest = normalizeHandlers(handlerObj.onRequest, (h, index) => + wrapEventHandler(h, fileName, 'onRequest', index), ); + } + + if (handlerObj.onBeforeResponse) { + handlerObj.onBeforeResponse = normalizeHandlers(handlerObj.onBeforeResponse, (h, index) => + wrapResponseHandler(h, fileName, index), + ); + } + + return handlerObj; +} + +/** + * Wraps a callable event handler with Sentry instrumentation. + * + * @param handler The event handler. + * @param handlerName The name of the event handler to be used for the span name and logging. + */ +function wrapEventHandler( + handler: EventHandler, + middlewareName: string, + hookName?: 'onRequest', + index?: number, +): EventHandler { + return async (event: H3Event) => { + debug.log(`Sentry middleware: ${middlewareName}${hookName ? `.${hookName}` : ''} handling ${event.path}`); + + const attributes = getSpanAttributes(event, middlewareName, hookName, index); + + return withSpan(() => handler(event), attributes, middlewareName, hookName); + }; +} + +/** + * Wraps a middleware response handler with Sentry instrumentation. + */ +function wrapResponseHandler(handler: ResponseMiddleware, middlewareName: string, index?: number): ResponseMiddleware { + return async (event: H3Event, response: EventHandlerResponse) => { + debug.log(`Sentry middleware: ${middlewareName}.onBeforeResponse handling ${event.path}`); + + const attributes = getSpanAttributes(event, middlewareName, 'onBeforeResponse', index); + + return withSpan(() => handler(event, response), attributes, middlewareName, 'onBeforeResponse'); }; } +/** + * Wraps a middleware or event handler execution with a span. + */ +function withSpan( + handler: () => TResult | Promise, + attributes: SpanAttributes, + middlewareName: string, + hookName?: 'handler' | 'onRequest' | 'onBeforeResponse', +): Promise { + const spanName = hookName && hookName !== 'handler' ? `${middlewareName}.${hookName}` : middlewareName; + + return startSpan( + { + name: spanName, + attributes, + }, + async span => { + try { + const result = await handler(); + span.setStatus({ code: SPAN_STATUS_OK }); + + return result; + } catch (error) { + captureException(error, { + mechanism: { + handled: false, + type: attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN], + }, + }); + + // Re-throw the error to be handled by the caller + throw error; + } finally { + await flushIfServerless(); + } + }, + ); +} + +/** + * Takes a list of handlers and wraps them with the normalizer function. + */ +function normalizeHandlers( + handlers: T | T[], + normalizer: (h: T, index?: number) => T, +): T | T[] { + return Array.isArray(handlers) ? handlers.map((handler, index) => normalizer(handler, index)) : normalizer(handlers); +} + /** * Gets the span attributes for the middleware handler based on the event. */ -function getSpanAttributes(event: H3Event, fileName: string): SpanAttributes { +function getSpanAttributes( + event: H3Event, + middlewareName: string, + hookName?: 'handler' | 'onRequest' | 'onBeforeResponse', + index?: number, +): SpanAttributes { const attributes: SpanAttributes = { [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'middleware.nuxt', [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.nuxt', - 'nuxt.middleware.name': fileName, + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.middleware.nuxt', + 'nuxt.middleware.name': middlewareName, + 'nuxt.middleware.hook.name': hookName ?? 'handler', }; + // Add index for array handlers + if (typeof index === 'number') { + attributes['nuxt.middleware.hook.index'] = index; + } + // Add HTTP method if (event.method) { attributes['http.request.method'] = event.method; @@ -88,3 +182,10 @@ function getSpanAttributes(event: H3Event, fileName: string return attributes; } + +/** + * Checks if the handler is an event handler, util for type narrowing. + */ +function isEventHandlerObject(handler: EventHandler | EventHandlerObject): handler is EventHandlerObject { + return typeof handler !== 'function'; +} diff --git a/packages/nuxt/src/vite/middlewareConfig.ts b/packages/nuxt/src/vite/middlewareConfig.ts index 392236cff8f4..ca5d5487497d 100644 --- a/packages/nuxt/src/vite/middlewareConfig.ts +++ b/packages/nuxt/src/vite/middlewareConfig.ts @@ -77,20 +77,14 @@ function middlewareInstrumentationPlugin(nitro: Nitro): InputPluginOption { * @returns The wrapped user code of the middleware. */ function wrapMiddlewareCode(originalCode: string, fileName: string): string { + // Remove common file extensions + const cleanFileName = fileName.replace(/\.(ts|js|mjs|mts|cts)$/, ''); + return ` import { wrapMiddlewareHandler } from '#imports'; function defineInstrumentedEventHandler(handlerOrObject) { - // Handle function syntax - if (typeof handlerOrObject === 'function') { - return defineEventHandler(wrapMiddlewareHandler(handlerOrObject, '${fileName}')); - } - - // Handle object syntax - return defineEventHandler({ - ...handlerOrObject, - handler: wrapMiddlewareHandler(handlerOrObject.handler, '${fileName}') - }); + return defineEventHandler(wrapMiddlewareHandler(handlerOrObject, '${cleanFileName}')); } ${originalCode.replace(/defineEventHandler\(/g, 'defineInstrumentedEventHandler(')} diff --git a/packages/nuxt/test/runtime/hooks/wrapMiddlewareHandler.test.ts b/packages/nuxt/test/runtime/hooks/wrapMiddlewareHandler.test.ts index e334ecde33bc..44f5f850a4b5 100644 --- a/packages/nuxt/test/runtime/hooks/wrapMiddlewareHandler.test.ts +++ b/packages/nuxt/test/runtime/hooks/wrapMiddlewareHandler.test.ts @@ -140,6 +140,334 @@ describe('wrapMiddlewareHandler', () => { }); }); + describe('EventHandlerObject wrapping', () => { + it('should wrap EventHandlerObject.handler correctly', async () => { + const baseHandler: EventHandler = vi.fn().mockResolvedValue('handler-result'); + const handlerObject = { + handler: baseHandler, + }; + + const wrapped = wrapMiddlewareHandler(handlerObject, 'object-middleware'); + + // Should return an object with wrapped handler + expect(typeof wrapped).toBe('object'); + expect(wrapped).toHaveProperty('handler'); + expect(typeof wrapped.handler).toBe('function'); + + // Test that the wrapped handler works + const result = await wrapped.handler(mockEvent); + expect(result).toBe('handler-result'); + expect(baseHandler).toHaveBeenCalledWith(mockEvent); + + // Verify Sentry instrumentation was applied + expect(SentryCore.startSpan).toHaveBeenCalledWith( + expect.objectContaining({ + name: 'object-middleware', + attributes: expect.objectContaining({ + [SentryCore.SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'middleware.nuxt', + 'nuxt.middleware.name': 'object-middleware', + }), + }), + expect.any(Function), + ); + }); + + it('should wrap EventHandlerObject.onRequest handlers correctly', async () => { + const baseHandler: EventHandler = vi.fn().mockResolvedValue('main-result'); + const onRequestHandler = vi.fn().mockResolvedValue(undefined); + const handlerObject = { + handler: baseHandler, + onRequest: onRequestHandler, + }; + + const wrapped = wrapMiddlewareHandler(handlerObject, 'request-middleware'); + + // Should preserve onRequest handler + expect(wrapped).toHaveProperty('onRequest'); + expect(typeof wrapped.onRequest).toBe('function'); + + // Test that the wrapped onRequest handler works + await wrapped.onRequest(mockEvent); + expect(onRequestHandler).toHaveBeenCalledWith(mockEvent); + + // Verify Sentry instrumentation was applied to onRequest + expect(SentryCore.startSpan).toHaveBeenCalledWith( + expect.objectContaining({ + name: 'request-middleware.onRequest', + attributes: expect.objectContaining({ + [SentryCore.SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'middleware.nuxt', + 'nuxt.middleware.name': 'request-middleware', + 'nuxt.middleware.hook.name': 'onRequest', + }), + }), + expect.any(Function), + ); + + // Verify that single handlers don't have an index attribute + const spanCall = (SentryCore.startSpan as any).mock.calls.find( + (call: any) => call[0]?.attributes?.['nuxt.middleware.hook.name'] === 'onRequest', + ); + expect(spanCall[0].attributes).not.toHaveProperty('nuxt.middleware.hook.index'); + }); + + it('should wrap EventHandlerObject.onRequest array of handlers correctly', async () => { + const baseHandler: EventHandler = vi.fn().mockResolvedValue('main-result'); + const onRequestHandler1 = vi.fn().mockResolvedValue(undefined); + const onRequestHandler2 = vi.fn().mockResolvedValue(undefined); + const handlerObject = { + handler: baseHandler, + onRequest: [onRequestHandler1, onRequestHandler2], + }; + + const wrapped = wrapMiddlewareHandler(handlerObject, 'multi-request-middleware'); + + // Should preserve onRequest as array + expect(wrapped).toHaveProperty('onRequest'); + expect(Array.isArray(wrapped.onRequest)).toBe(true); + expect(wrapped.onRequest).toHaveLength(2); + + // Test that both wrapped handlers work + if (Array.isArray(wrapped.onRequest)) { + await wrapped.onRequest[0]!(mockEvent); + await wrapped.onRequest[1]!(mockEvent); + } + + expect(onRequestHandler1).toHaveBeenCalledWith(mockEvent); + expect(onRequestHandler2).toHaveBeenCalledWith(mockEvent); + + // Verify Sentry instrumentation was applied to both handlers + expect(SentryCore.startSpan).toHaveBeenCalledWith( + expect.objectContaining({ + name: 'multi-request-middleware.onRequest', + attributes: expect.objectContaining({ + 'nuxt.middleware.hook.name': 'onRequest', + 'nuxt.middleware.hook.index': 0, + }), + }), + expect.any(Function), + ); + expect(SentryCore.startSpan).toHaveBeenCalledWith( + expect.objectContaining({ + name: 'multi-request-middleware.onRequest', + attributes: expect.objectContaining({ + 'nuxt.middleware.hook.name': 'onRequest', + 'nuxt.middleware.hook.index': 1, + }), + }), + expect.any(Function), + ); + }); + + it('should wrap EventHandlerObject.onBeforeResponse handlers correctly', async () => { + const baseHandler: EventHandler = vi.fn().mockResolvedValue('main-result'); + const onBeforeResponseHandler = vi.fn().mockResolvedValue(undefined); + const handlerObject = { + handler: baseHandler, + onBeforeResponse: onBeforeResponseHandler, + }; + + const wrapped = wrapMiddlewareHandler(handlerObject, 'response-middleware'); + + // Should preserve onBeforeResponse handler + expect(wrapped).toHaveProperty('onBeforeResponse'); + expect(typeof wrapped.onBeforeResponse).toBe('function'); + + // Test that the wrapped onBeforeResponse handler works + const mockResponse = { body: 'test-response' }; + await wrapped.onBeforeResponse(mockEvent, mockResponse); + expect(onBeforeResponseHandler).toHaveBeenCalledWith(mockEvent, mockResponse); + + // Verify Sentry instrumentation was applied to onBeforeResponse + expect(SentryCore.startSpan).toHaveBeenCalledWith( + expect.objectContaining({ + name: 'response-middleware.onBeforeResponse', + attributes: expect.objectContaining({ + [SentryCore.SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'middleware.nuxt', + 'nuxt.middleware.name': 'response-middleware', + 'nuxt.middleware.hook.name': 'onBeforeResponse', + }), + }), + expect.any(Function), + ); + }); + + it('should wrap EventHandlerObject.onBeforeResponse array of handlers correctly', async () => { + const baseHandler: EventHandler = vi.fn().mockResolvedValue('main-result'); + const onBeforeResponseHandler1 = vi.fn().mockResolvedValue(undefined); + const onBeforeResponseHandler2 = vi.fn().mockResolvedValue(undefined); + const handlerObject = { + handler: baseHandler, + onBeforeResponse: [onBeforeResponseHandler1, onBeforeResponseHandler2], + }; + + const wrapped = wrapMiddlewareHandler(handlerObject, 'multi-response-middleware'); + + // Should preserve onBeforeResponse as array + expect(wrapped).toHaveProperty('onBeforeResponse'); + expect(Array.isArray(wrapped.onBeforeResponse)).toBe(true); + expect(wrapped.onBeforeResponse).toHaveLength(2); + + // Test that both wrapped handlers work + const mockResponse = { body: 'test-response' }; + if (Array.isArray(wrapped.onBeforeResponse)) { + await wrapped.onBeforeResponse[0]!(mockEvent, mockResponse); + await wrapped.onBeforeResponse[1]!(mockEvent, mockResponse); + } + + expect(onBeforeResponseHandler1).toHaveBeenCalledWith(mockEvent, mockResponse); + expect(onBeforeResponseHandler2).toHaveBeenCalledWith(mockEvent, mockResponse); + + // Verify Sentry instrumentation was applied to both handlers + expect(SentryCore.startSpan).toHaveBeenCalledWith( + expect.objectContaining({ + name: 'multi-response-middleware.onBeforeResponse', + attributes: expect.objectContaining({ + 'nuxt.middleware.hook.name': 'onBeforeResponse', + 'nuxt.middleware.hook.index': 0, + }), + }), + expect.any(Function), + ); + expect(SentryCore.startSpan).toHaveBeenCalledWith( + expect.objectContaining({ + name: 'multi-response-middleware.onBeforeResponse', + attributes: expect.objectContaining({ + 'nuxt.middleware.hook.name': 'onBeforeResponse', + 'nuxt.middleware.hook.index': 1, + }), + }), + expect.any(Function), + ); + }); + + it('should wrap complex EventHandlerObject with all properties', async () => { + const baseHandler: EventHandler = vi.fn().mockResolvedValue('complex-result'); + const onRequestHandler = vi.fn().mockResolvedValue(undefined); + const onBeforeResponseHandler = vi.fn().mockResolvedValue(undefined); + const handlerObject = { + handler: baseHandler, + onRequest: onRequestHandler, + onBeforeResponse: onBeforeResponseHandler, + }; + + const wrapped = wrapMiddlewareHandler(handlerObject, 'complex-middleware'); + + // Should preserve all properties + expect(wrapped).toHaveProperty('handler'); + expect(wrapped).toHaveProperty('onRequest'); + expect(wrapped).toHaveProperty('onBeforeResponse'); + + // Test main handler + const result = await wrapped.handler(mockEvent); + expect(result).toBe('complex-result'); + expect(baseHandler).toHaveBeenCalledWith(mockEvent); + + // Test onRequest handler + await wrapped.onRequest(mockEvent); + expect(onRequestHandler).toHaveBeenCalledWith(mockEvent); + + // Test onBeforeResponse handler + const mockResponse = { body: 'test-response' }; + await wrapped.onBeforeResponse(mockEvent, mockResponse); + expect(onBeforeResponseHandler).toHaveBeenCalledWith(mockEvent, mockResponse); + + // Verify all handlers got Sentry instrumentation + expect(SentryCore.startSpan).toHaveBeenCalledWith( + expect.objectContaining({ + name: 'complex-middleware', + attributes: expect.objectContaining({ 'nuxt.middleware.hook.name': 'handler' }), + }), + expect.any(Function), + ); + expect(SentryCore.startSpan).toHaveBeenCalledWith( + expect.objectContaining({ + name: 'complex-middleware.onRequest', + attributes: expect.objectContaining({ 'nuxt.middleware.hook.name': 'onRequest' }), + }), + expect.any(Function), + ); + expect(SentryCore.startSpan).toHaveBeenCalledWith( + expect.objectContaining({ + name: 'complex-middleware.onBeforeResponse', + attributes: expect.objectContaining({ 'nuxt.middleware.hook.name': 'onBeforeResponse' }), + }), + expect.any(Function), + ); + }); + + it('should handle EventHandlerObject without optional handlers', async () => { + const baseHandler: EventHandler = vi.fn().mockResolvedValue('minimal-object-result'); + const handlerObject = { + handler: baseHandler, + // No onRequest or onBeforeResponse + }; + + const wrapped = wrapMiddlewareHandler(handlerObject, 'minimal-object-middleware'); + + // Should only have handler property + expect(wrapped).toHaveProperty('handler'); + expect(wrapped).not.toHaveProperty('onRequest'); + expect(wrapped).not.toHaveProperty('onBeforeResponse'); + + // Test that the main handler works + const result = await wrapped.handler(mockEvent); + expect(result).toBe('minimal-object-result'); + expect(baseHandler).toHaveBeenCalledWith(mockEvent); + + // Verify Sentry instrumentation was applied + expect(SentryCore.startSpan).toHaveBeenCalledWith( + expect.objectContaining({ + name: 'minimal-object-middleware', + }), + expect.any(Function), + ); + }); + + it('should propagate errors from EventHandlerObject.handler', async () => { + const error = new Error('Handler error'); + const failingHandler: EventHandler = vi.fn().mockRejectedValue(error); + const handlerObject = { + handler: failingHandler, + }; + + const wrapped = wrapMiddlewareHandler(handlerObject, 'failing-object-middleware'); + + await expect(wrapped.handler(mockEvent)).rejects.toThrow('Handler error'); + expect(SentryCore.captureException).toHaveBeenCalledWith(error, expect.any(Object)); + }); + + it('should propagate errors from EventHandlerObject.onRequest', async () => { + const baseHandler: EventHandler = vi.fn().mockResolvedValue('success'); + const error = new Error('OnRequest error'); + const failingOnRequestHandler = vi.fn().mockRejectedValue(error); + const handlerObject = { + handler: baseHandler, + onRequest: failingOnRequestHandler, + }; + + const wrapped = wrapMiddlewareHandler(handlerObject, 'failing-request-middleware'); + + await expect(wrapped.onRequest(mockEvent)).rejects.toThrow('OnRequest error'); + expect(SentryCore.captureException).toHaveBeenCalledWith(error, expect.any(Object)); + }); + + it('should propagate errors from EventHandlerObject.onBeforeResponse', async () => { + const baseHandler: EventHandler = vi.fn().mockResolvedValue('success'); + const error = new Error('OnBeforeResponse error'); + const failingOnBeforeResponseHandler = vi.fn().mockRejectedValue(error); + const handlerObject = { + handler: baseHandler, + onBeforeResponse: failingOnBeforeResponseHandler, + }; + + const wrapped = wrapMiddlewareHandler(handlerObject, 'failing-response-middleware'); + + const mockResponse = { body: 'test-response' }; + await expect(wrapped.onBeforeResponse(mockEvent, mockResponse)).rejects.toThrow('OnBeforeResponse error'); + expect(SentryCore.captureException).toHaveBeenCalledWith(error, expect.any(Object)); + }); + }); + describe('Sentry API integration', () => { it('should call Sentry APIs with correct parameters', async () => { const userHandler: EventHandler = vi.fn().mockResolvedValue('api-test-result'); From b881b7e90b36e1565e813bf7c5baf32d8d3b4b1c Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Mon, 29 Sep 2025 16:42:25 +0300 Subject: [PATCH 12/17] fix: set the span status to internal error --- packages/nuxt/src/runtime/hooks/wrapMiddlewareHandler.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/nuxt/src/runtime/hooks/wrapMiddlewareHandler.ts b/packages/nuxt/src/runtime/hooks/wrapMiddlewareHandler.ts index a3a787ca3964..16f05b32af85 100644 --- a/packages/nuxt/src/runtime/hooks/wrapMiddlewareHandler.ts +++ b/packages/nuxt/src/runtime/hooks/wrapMiddlewareHandler.ts @@ -8,6 +8,7 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, + SPAN_STATUS_ERROR, SPAN_STATUS_OK, startSpan, } from '@sentry/core'; @@ -111,6 +112,7 @@ function withSpan( return result; } catch (error) { + span.setStatus({ code: SPAN_STATUS_ERROR, message: 'internal_error' }); captureException(error, { mechanism: { handled: false, From a35cc1807bd86af2cceb9a65dc34e776de94b9e9 Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Mon, 29 Sep 2025 16:42:52 +0300 Subject: [PATCH 13/17] test: update e2e tests with middleware hook instrumentation --- .../nuxt-3/server/middleware/04.hooks.ts | 36 +++ .../server/middleware/05.array-hooks.ts | 47 ++++ .../nuxt-3/tests/middleware.test.ts | 227 +++++++++++++++++- .../nuxt-4/server/middleware/04.hooks.ts | 36 +++ .../server/middleware/05.array-hooks.ts | 47 ++++ .../nuxt-4/tests/middleware.test.ts | 227 +++++++++++++++++- 6 files changed, 604 insertions(+), 16 deletions(-) create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-3/server/middleware/04.hooks.ts create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-3/server/middleware/05.array-hooks.ts create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-4/server/middleware/04.hooks.ts create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-4/server/middleware/05.array-hooks.ts diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3/server/middleware/04.hooks.ts b/dev-packages/e2e-tests/test-applications/nuxt-3/server/middleware/04.hooks.ts new file mode 100644 index 000000000000..1f9cf40a1c02 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3/server/middleware/04.hooks.ts @@ -0,0 +1,36 @@ +import { defineEventHandler, setHeader, getQuery } from '#imports'; + +export default defineEventHandler({ + onRequest: async event => { + // Set a header to indicate the onRequest hook ran + setHeader(event, 'x-hooks-onrequest', 'executed'); + + // Check if we should throw an error in onRequest + const query = getQuery(event); + if (query.throwOnRequestError === 'true') { + throw new Error('OnRequest hook error'); + } + }, + + handler: async event => { + // Set a header to indicate the main handler ran + setHeader(event, 'x-hooks-handler', 'executed'); + + // Check if we should throw an error in handler + const query = getQuery(event); + if (query.throwHandlerError === 'true') { + throw new Error('Handler error'); + } + }, + + onBeforeResponse: async (event, response) => { + // Set a header to indicate the onBeforeResponse hook ran + setHeader(event, 'x-hooks-onbeforeresponse', 'executed'); + + // Check if we should throw an error in onBeforeResponse + const query = getQuery(event); + if (query.throwOnBeforeResponseError === 'true') { + throw new Error('OnBeforeResponse hook error'); + } + }, +}); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3/server/middleware/05.array-hooks.ts b/dev-packages/e2e-tests/test-applications/nuxt-3/server/middleware/05.array-hooks.ts new file mode 100644 index 000000000000..cc815bfb2fbf --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3/server/middleware/05.array-hooks.ts @@ -0,0 +1,47 @@ +import { defineEventHandler, setHeader, getQuery } from '#imports'; + +export default defineEventHandler({ + // Array of onRequest handlers + onRequest: [ + async event => { + setHeader(event, 'x-array-onrequest-0', 'executed'); + + const query = getQuery(event); + if (query.throwOnRequest0Error === 'true') { + throw new Error('OnRequest[0] hook error'); + } + }, + async event => { + setHeader(event, 'x-array-onrequest-1', 'executed'); + + const query = getQuery(event); + if (query.throwOnRequest1Error === 'true') { + throw new Error('OnRequest[1] hook error'); + } + }, + ], + + handler: async event => { + setHeader(event, 'x-array-handler', 'executed'); + }, + + // Array of onBeforeResponse handlers + onBeforeResponse: [ + async (event, response) => { + setHeader(event, 'x-array-onbeforeresponse-0', 'executed'); + + const query = getQuery(event); + if (query.throwOnBeforeResponse0Error === 'true') { + throw new Error('OnBeforeResponse[0] hook error'); + } + }, + async (event, response) => { + setHeader(event, 'x-array-onbeforeresponse-1', 'executed'); + + const query = getQuery(event); + if (query.throwOnBeforeResponse1Error === 'true') { + throw new Error('OnBeforeResponse[1] hook error'); + } + }, + ], +}); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3/tests/middleware.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-3/tests/middleware.test.ts index 7edbe94184d8..e9debf8496c2 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-3/tests/middleware.test.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-3/tests/middleware.test.ts @@ -19,16 +19,23 @@ test.describe('Server Middleware Instrumentation', () => { // Verify that we have spans for each middleware const middlewareSpans = serverTxnEvent.spans?.filter(span => span.op === 'middleware.nuxt') || []; - expect(middlewareSpans).toHaveLength(3); + // 3 simple + 3 hooks (onRequest+handler+onBeforeResponse) + 5 array hooks (2 onRequest + 1 handler + 2 onBeforeResponse) + expect(middlewareSpans).toHaveLength(11); // Check for specific middleware spans - const firstMiddlewareSpan = middlewareSpans.find(span => span.data?.['nuxt.middleware.name'] === '01.first.ts'); - const secondMiddlewareSpan = middlewareSpans.find(span => span.data?.['nuxt.middleware.name'] === '02.second.ts'); - const authMiddlewareSpan = middlewareSpans.find(span => span.data?.['nuxt.middleware.name'] === '03.auth.ts'); + const firstMiddlewareSpan = middlewareSpans.find(span => span.data?.['nuxt.middleware.name'] === '01.first'); + const secondMiddlewareSpan = middlewareSpans.find(span => span.data?.['nuxt.middleware.name'] === '02.second'); + const authMiddlewareSpan = middlewareSpans.find(span => span.data?.['nuxt.middleware.name'] === '03.auth'); + const hooksOnRequestSpan = middlewareSpans.find(span => span.data?.['nuxt.middleware.name'] === '04.hooks'); + const arrayHooksHandlerSpan = middlewareSpans.find( + span => span.data?.['nuxt.middleware.name'] === '05.array-hooks', + ); expect(firstMiddlewareSpan).toBeDefined(); expect(secondMiddlewareSpan).toBeDefined(); expect(authMiddlewareSpan).toBeDefined(); + expect(hooksOnRequestSpan).toBeDefined(); + expect(arrayHooksHandlerSpan).toBeDefined(); // Verify each span has the correct attributes [firstMiddlewareSpan, secondMiddlewareSpan, authMiddlewareSpan].forEach(span => { @@ -37,7 +44,7 @@ test.describe('Server Middleware Instrumentation', () => { op: 'middleware.nuxt', data: expect.objectContaining({ 'sentry.op': 'middleware.nuxt', - 'sentry.origin': 'auto.http.nuxt', + 'sentry.origin': 'auto.middleware.nuxt', 'sentry.source': 'custom', 'http.request.method': 'GET', 'http.route': '/api/middleware-test', @@ -52,7 +59,8 @@ test.describe('Server Middleware Instrumentation', () => { // Verify spans have different span IDs (each middleware gets its own span) const spanIds = middlewareSpans.map(span => span.span_id); const uniqueSpanIds = new Set(spanIds); - expect(uniqueSpanIds.size).toBe(3); + // 3 simple + 3 hooks (onRequest+handler+onBeforeResponse) + 5 array hooks (2 onRequest + 1 handler + 2 onBeforeResponse) + expect(uniqueSpanIds.size).toBe(11); // Verify spans share the same trace ID const traceIds = middlewareSpans.map(span => span.trace_id); @@ -95,7 +103,7 @@ test.describe('Server Middleware Instrumentation', () => { // Find the auth middleware span const authMiddlewareSpan = serverTxnEvent.spans?.find( - span => span.op === 'middleware.nuxt' && span.data?.['nuxt.middleware.name'] === '03.auth.ts', + span => span.op === 'middleware.nuxt' && span.data?.['nuxt.middleware.name'] === '03.auth', ); expect(authMiddlewareSpan).toBeDefined(); @@ -113,9 +121,212 @@ test.describe('Server Middleware Instrumentation', () => { type: 'Error', mechanism: expect.objectContaining({ handled: false, - type: 'auto.http.nuxt', + type: 'auto.middleware.nuxt', }), }), ); }); + + test('should create spans for onRequest and onBeforeResponse hooks', async ({ request }) => { + const serverTxnEventPromise = waitForTransaction('nuxt-3', txnEvent => { + return txnEvent.transaction?.includes('GET /api/middleware-test') ?? false; + }); + + // Make request to trigger middleware with hooks + const response = await request.get('/api/middleware-test'); + expect(response.status()).toBe(200); + + const serverTxnEvent = await serverTxnEventPromise; + const middlewareSpans = serverTxnEvent.spans?.filter(span => span.op === 'middleware.nuxt') || []; + + // Find spans for the hooks middleware + const hooksSpans = middlewareSpans.filter(span => span.data?.['nuxt.middleware.name'] === '04.hooks'); + + // Should have spans for onRequest, handler, and onBeforeResponse + expect(hooksSpans).toHaveLength(3); + + // Find specific hook spans + const onRequestSpan = hooksSpans.find(span => span.data?.['nuxt.middleware.hook.name'] === 'onRequest'); + const handlerSpan = hooksSpans.find(span => span.data?.['nuxt.middleware.hook.name'] === 'handler'); + const onBeforeResponseSpan = hooksSpans.find( + span => span.data?.['nuxt.middleware.hook.name'] === 'onBeforeResponse', + ); + + expect(onRequestSpan).toBeDefined(); + expect(handlerSpan).toBeDefined(); + expect(onBeforeResponseSpan).toBeDefined(); + + // Verify span names include hook types + expect(onRequestSpan?.description).toBe('04.hooks.onRequest'); + expect(handlerSpan?.description).toBe('04.hooks'); + expect(onBeforeResponseSpan?.description).toBe('04.hooks.onBeforeResponse'); + + // Verify all spans have correct middleware name (without hook suffix) + [onRequestSpan, handlerSpan, onBeforeResponseSpan].forEach(span => { + expect(span?.data?.['nuxt.middleware.name']).toBe('04.hooks'); + }); + + // Verify hook-specific attributes + expect(onRequestSpan?.data?.['nuxt.middleware.hook.name']).toBe('onRequest'); + expect(handlerSpan?.data?.['nuxt.middleware.hook.name']).toBe('handler'); + expect(onBeforeResponseSpan?.data?.['nuxt.middleware.hook.name']).toBe('onBeforeResponse'); + + // Verify no index attributes for single hooks + expect(onRequestSpan?.data).not.toHaveProperty('nuxt.middleware.hook.index'); + expect(handlerSpan?.data).not.toHaveProperty('nuxt.middleware.hook.index'); + expect(onBeforeResponseSpan?.data).not.toHaveProperty('nuxt.middleware.hook.index'); + }); + + test('should create spans with index attributes for array hooks', async ({ request }) => { + const serverTxnEventPromise = waitForTransaction('nuxt-3', txnEvent => { + return txnEvent.transaction?.includes('GET /api/middleware-test') ?? false; + }); + + // Make request to trigger middleware with array hooks + const response = await request.get('/api/middleware-test'); + expect(response.status()).toBe(200); + + const serverTxnEvent = await serverTxnEventPromise; + const middlewareSpans = serverTxnEvent.spans?.filter(span => span.op === 'middleware.nuxt') || []; + + // Find spans for the array hooks middleware + const arrayHooksSpans = middlewareSpans.filter(span => span.data?.['nuxt.middleware.name'] === '05.array-hooks'); + + // Should have spans for 2 onRequest + 1 handler + 2 onBeforeResponse = 5 spans + expect(arrayHooksSpans).toHaveLength(5); + + // Find onRequest array spans + const onRequestSpans = arrayHooksSpans.filter(span => span.data?.['nuxt.middleware.hook.name'] === 'onRequest'); + expect(onRequestSpans).toHaveLength(2); + + // Find onBeforeResponse array spans + const onBeforeResponseSpans = arrayHooksSpans.filter( + span => span.data?.['nuxt.middleware.hook.name'] === 'onBeforeResponse', + ); + expect(onBeforeResponseSpans).toHaveLength(2); + + // Find handler span + const handlerSpan = arrayHooksSpans.find(span => span.data?.['nuxt.middleware.hook.name'] === 'handler'); + expect(handlerSpan).toBeDefined(); + + // Verify index attributes for onRequest array + const onRequest0Span = onRequestSpans.find(span => span.data?.['nuxt.middleware.hook.index'] === 0); + const onRequest1Span = onRequestSpans.find(span => span.data?.['nuxt.middleware.hook.index'] === 1); + + expect(onRequest0Span).toBeDefined(); + expect(onRequest1Span).toBeDefined(); + + // Verify index attributes for onBeforeResponse array + const onBeforeResponse0Span = onBeforeResponseSpans.find(span => span.data?.['nuxt.middleware.hook.index'] === 0); + const onBeforeResponse1Span = onBeforeResponseSpans.find(span => span.data?.['nuxt.middleware.hook.index'] === 1); + + expect(onBeforeResponse0Span).toBeDefined(); + expect(onBeforeResponse1Span).toBeDefined(); + + // Verify span names for array handlers + expect(onRequest0Span?.description).toBe('05.array-hooks.onRequest'); + expect(onRequest1Span?.description).toBe('05.array-hooks.onRequest'); + expect(onBeforeResponse0Span?.description).toBe('05.array-hooks.onBeforeResponse'); + expect(onBeforeResponse1Span?.description).toBe('05.array-hooks.onBeforeResponse'); + + // Verify handler has no index + expect(handlerSpan?.data).not.toHaveProperty('nuxt.middleware.hook.index'); + }); + + test('should handle errors in onRequest hooks', async ({ request }) => { + const serverTxnEventPromise = waitForTransaction('nuxt-3', txnEvent => { + return txnEvent.transaction?.includes('GET /api/middleware-test') ?? false; + }); + + const errorEventPromise = waitForError('nuxt-3', errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === 'OnRequest hook error'; + }); + + // Make request with query param to trigger error in onRequest + const response = await request.get('/api/middleware-test?throwOnRequestError=true'); + expect(response.status()).toBe(500); + + const [serverTxnEvent, errorEvent] = await Promise.all([serverTxnEventPromise, errorEventPromise]); + + // Find the onRequest span that should have error status + const onRequestSpan = serverTxnEvent.spans?.find( + span => + span.op === 'middleware.nuxt' && + span.data?.['nuxt.middleware.name'] === '04.hooks' && + span.data?.['nuxt.middleware.hook.name'] === 'onRequest', + ); + + expect(onRequestSpan).toBeDefined(); + expect(onRequestSpan?.status).toBe('internal_error'); + expect(errorEvent.exception?.values?.[0]?.value).toBe('OnRequest hook error'); + }); + + test('should handle errors in onBeforeResponse hooks', async ({ request }) => { + const serverTxnEventPromise = waitForTransaction('nuxt-3', txnEvent => { + return txnEvent.transaction?.includes('GET /api/middleware-test') ?? false; + }); + + const errorEventPromise = waitForError('nuxt-3', errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === 'OnBeforeResponse hook error'; + }); + + // Make request with query param to trigger error in onBeforeResponse + const response = await request.get('/api/middleware-test?throwOnBeforeResponseError=true'); + expect(response.status()).toBe(500); + + const [serverTxnEvent, errorEvent] = await Promise.all([serverTxnEventPromise, errorEventPromise]); + + // Find the onBeforeResponse span that should have error status + const onBeforeResponseSpan = serverTxnEvent.spans?.find( + span => + span.op === 'middleware.nuxt' && + span.data?.['nuxt.middleware.name'] === '04.hooks' && + span.data?.['nuxt.middleware.hook.name'] === 'onBeforeResponse', + ); + + expect(onBeforeResponseSpan).toBeDefined(); + expect(onBeforeResponseSpan?.status).toBe('internal_error'); + expect(errorEvent.exception?.values?.[0]?.value).toBe('OnBeforeResponse hook error'); + }); + + test('should handle errors in array hooks with proper index attribution', async ({ request }) => { + const serverTxnEventPromise = waitForTransaction('nuxt-3', txnEvent => { + return txnEvent.transaction?.includes('GET /api/middleware-test') ?? false; + }); + + const errorEventPromise = waitForError('nuxt-3', errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === 'OnRequest[1] hook error'; + }); + + // Make request with query param to trigger error in second onRequest handler + const response = await request.get('/api/middleware-test?throwOnRequest1Error=true'); + expect(response.status()).toBe(500); + + const [serverTxnEvent, errorEvent] = await Promise.all([serverTxnEventPromise, errorEventPromise]); + + // Find the second onRequest span that should have error status + const onRequest1Span = serverTxnEvent.spans?.find( + span => + span.op === 'middleware.nuxt' && + span.data?.['nuxt.middleware.name'] === '05.array-hooks' && + span.data?.['nuxt.middleware.hook.name'] === 'onRequest' && + span.data?.['nuxt.middleware.hook.index'] === 1, + ); + + expect(onRequest1Span).toBeDefined(); + expect(onRequest1Span?.status).toBe('internal_error'); + expect(errorEvent.exception?.values?.[0]?.value).toBe('OnRequest[1] hook error'); + + // Verify the first onRequest handler still executed successfully + const onRequest0Span = serverTxnEvent.spans?.find( + span => + span.op === 'middleware.nuxt' && + span.data?.['nuxt.middleware.name'] === '05.array-hooks' && + span.data?.['nuxt.middleware.hook.name'] === 'onRequest' && + span.data?.['nuxt.middleware.hook.index'] === 0, + ); + + expect(onRequest0Span).toBeDefined(); + expect(onRequest0Span?.status).not.toBe('internal_error'); + }); }); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/server/middleware/04.hooks.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/server/middleware/04.hooks.ts new file mode 100644 index 000000000000..1f9cf40a1c02 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-4/server/middleware/04.hooks.ts @@ -0,0 +1,36 @@ +import { defineEventHandler, setHeader, getQuery } from '#imports'; + +export default defineEventHandler({ + onRequest: async event => { + // Set a header to indicate the onRequest hook ran + setHeader(event, 'x-hooks-onrequest', 'executed'); + + // Check if we should throw an error in onRequest + const query = getQuery(event); + if (query.throwOnRequestError === 'true') { + throw new Error('OnRequest hook error'); + } + }, + + handler: async event => { + // Set a header to indicate the main handler ran + setHeader(event, 'x-hooks-handler', 'executed'); + + // Check if we should throw an error in handler + const query = getQuery(event); + if (query.throwHandlerError === 'true') { + throw new Error('Handler error'); + } + }, + + onBeforeResponse: async (event, response) => { + // Set a header to indicate the onBeforeResponse hook ran + setHeader(event, 'x-hooks-onbeforeresponse', 'executed'); + + // Check if we should throw an error in onBeforeResponse + const query = getQuery(event); + if (query.throwOnBeforeResponseError === 'true') { + throw new Error('OnBeforeResponse hook error'); + } + }, +}); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/server/middleware/05.array-hooks.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/server/middleware/05.array-hooks.ts new file mode 100644 index 000000000000..cc815bfb2fbf --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-4/server/middleware/05.array-hooks.ts @@ -0,0 +1,47 @@ +import { defineEventHandler, setHeader, getQuery } from '#imports'; + +export default defineEventHandler({ + // Array of onRequest handlers + onRequest: [ + async event => { + setHeader(event, 'x-array-onrequest-0', 'executed'); + + const query = getQuery(event); + if (query.throwOnRequest0Error === 'true') { + throw new Error('OnRequest[0] hook error'); + } + }, + async event => { + setHeader(event, 'x-array-onrequest-1', 'executed'); + + const query = getQuery(event); + if (query.throwOnRequest1Error === 'true') { + throw new Error('OnRequest[1] hook error'); + } + }, + ], + + handler: async event => { + setHeader(event, 'x-array-handler', 'executed'); + }, + + // Array of onBeforeResponse handlers + onBeforeResponse: [ + async (event, response) => { + setHeader(event, 'x-array-onbeforeresponse-0', 'executed'); + + const query = getQuery(event); + if (query.throwOnBeforeResponse0Error === 'true') { + throw new Error('OnBeforeResponse[0] hook error'); + } + }, + async (event, response) => { + setHeader(event, 'x-array-onbeforeresponse-1', 'executed'); + + const query = getQuery(event); + if (query.throwOnBeforeResponse1Error === 'true') { + throw new Error('OnBeforeResponse[1] hook error'); + } + }, + ], +}); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/tests/middleware.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/middleware.test.ts index d265a996955e..005330c01fee 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-4/tests/middleware.test.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/middleware.test.ts @@ -19,16 +19,23 @@ test.describe('Server Middleware Instrumentation', () => { // Verify that we have spans for each middleware const middlewareSpans = serverTxnEvent.spans?.filter(span => span.op === 'middleware.nuxt') || []; - expect(middlewareSpans).toHaveLength(3); + // 3 simple + 3 hooks (onRequest+handler+onBeforeResponse) + 5 array hooks (2 onRequest + 1 handler + 2 onBeforeResponse + expect(middlewareSpans).toHaveLength(11); // Check for specific middleware spans - const firstMiddlewareSpan = middlewareSpans.find(span => span.data?.['nuxt.middleware.name'] === '01.first.ts'); - const secondMiddlewareSpan = middlewareSpans.find(span => span.data?.['nuxt.middleware.name'] === '02.second.ts'); - const authMiddlewareSpan = middlewareSpans.find(span => span.data?.['nuxt.middleware.name'] === '03.auth.ts'); + const firstMiddlewareSpan = middlewareSpans.find(span => span.data?.['nuxt.middleware.name'] === '01.first'); + const secondMiddlewareSpan = middlewareSpans.find(span => span.data?.['nuxt.middleware.name'] === '02.second'); + const authMiddlewareSpan = middlewareSpans.find(span => span.data?.['nuxt.middleware.name'] === '03.auth'); + const hooksOnRequestSpan = middlewareSpans.find(span => span.data?.['nuxt.middleware.name'] === '04.hooks'); + const arrayHooksHandlerSpan = middlewareSpans.find( + span => span.data?.['nuxt.middleware.name'] === '05.array-hooks', + ); expect(firstMiddlewareSpan).toBeDefined(); expect(secondMiddlewareSpan).toBeDefined(); expect(authMiddlewareSpan).toBeDefined(); + expect(hooksOnRequestSpan).toBeDefined(); + expect(arrayHooksHandlerSpan).toBeDefined(); // Verify each span has the correct attributes [firstMiddlewareSpan, secondMiddlewareSpan, authMiddlewareSpan].forEach(span => { @@ -37,7 +44,7 @@ test.describe('Server Middleware Instrumentation', () => { op: 'middleware.nuxt', data: expect.objectContaining({ 'sentry.op': 'middleware.nuxt', - 'sentry.origin': 'auto.http.nuxt', + 'sentry.origin': 'auto.middleware.nuxt', 'sentry.source': 'custom', 'http.request.method': 'GET', 'http.route': '/api/middleware-test', @@ -52,7 +59,8 @@ test.describe('Server Middleware Instrumentation', () => { // Verify spans have different span IDs (each middleware gets its own span) const spanIds = middlewareSpans.map(span => span.span_id); const uniqueSpanIds = new Set(spanIds); - expect(uniqueSpanIds.size).toBe(3); + // 3 simple + 3 hooks (onRequest+handler+onBeforeResponse) + 5 array hooks (2 onRequest + 1 handler + 2 onBeforeResponse) + expect(uniqueSpanIds.size).toBe(11); // Verify spans share the same trace ID const traceIds = middlewareSpans.map(span => span.trace_id); @@ -95,7 +103,7 @@ test.describe('Server Middleware Instrumentation', () => { // Find the auth middleware span const authMiddlewareSpan = serverTxnEvent.spans?.find( - span => span.op === 'middleware.nuxt' && span.data?.['nuxt.middleware.name'] === '03.auth.ts', + span => span.op === 'middleware.nuxt' && span.data?.['nuxt.middleware.name'] === '03.auth', ); expect(authMiddlewareSpan).toBeDefined(); @@ -113,9 +121,212 @@ test.describe('Server Middleware Instrumentation', () => { type: 'Error', mechanism: expect.objectContaining({ handled: false, - type: 'auto.http.nuxt', + type: 'auto.middleware.nuxt', }), }), ); }); + + test('should create spans for onRequest and onBeforeResponse hooks', async ({ request }) => { + const serverTxnEventPromise = waitForTransaction('nuxt-4', txnEvent => { + return txnEvent.transaction?.includes('GET /api/middleware-test') ?? false; + }); + + // Make request to trigger middleware with hooks + const response = await request.get('/api/middleware-test'); + expect(response.status()).toBe(200); + + const serverTxnEvent = await serverTxnEventPromise; + const middlewareSpans = serverTxnEvent.spans?.filter(span => span.op === 'middleware.nuxt') || []; + + // Find spans for the hooks middleware + const hooksSpans = middlewareSpans.filter(span => span.data?.['nuxt.middleware.name'] === '04.hooks'); + + // Should have spans for onRequest, handler, and onBeforeResponse + expect(hooksSpans).toHaveLength(3); + + // Find specific hook spans + const onRequestSpan = hooksSpans.find(span => span.data?.['nuxt.middleware.hook.name'] === 'onRequest'); + const handlerSpan = hooksSpans.find(span => span.data?.['nuxt.middleware.hook.name'] === 'handler'); + const onBeforeResponseSpan = hooksSpans.find( + span => span.data?.['nuxt.middleware.hook.name'] === 'onBeforeResponse', + ); + + expect(onRequestSpan).toBeDefined(); + expect(handlerSpan).toBeDefined(); + expect(onBeforeResponseSpan).toBeDefined(); + + // Verify span names include hook types + expect(onRequestSpan?.description).toBe('04.hooks.onRequest'); + expect(handlerSpan?.description).toBe('04.hooks'); + expect(onBeforeResponseSpan?.description).toBe('04.hooks.onBeforeResponse'); + + // Verify all spans have correct middleware name (without hook suffix) + [onRequestSpan, handlerSpan, onBeforeResponseSpan].forEach(span => { + expect(span?.data?.['nuxt.middleware.name']).toBe('04.hooks'); + }); + + // Verify hook-specific attributes + expect(onRequestSpan?.data?.['nuxt.middleware.hook.name']).toBe('onRequest'); + expect(handlerSpan?.data?.['nuxt.middleware.hook.name']).toBe('handler'); + expect(onBeforeResponseSpan?.data?.['nuxt.middleware.hook.name']).toBe('onBeforeResponse'); + + // Verify no index attributes for single hooks + expect(onRequestSpan?.data).not.toHaveProperty('nuxt.middleware.hook.index'); + expect(handlerSpan?.data).not.toHaveProperty('nuxt.middleware.hook.index'); + expect(onBeforeResponseSpan?.data).not.toHaveProperty('nuxt.middleware.hook.index'); + }); + + test('should create spans with index attributes for array hooks', async ({ request }) => { + const serverTxnEventPromise = waitForTransaction('nuxt-4', txnEvent => { + return txnEvent.transaction?.includes('GET /api/middleware-test') ?? false; + }); + + // Make request to trigger middleware with array hooks + const response = await request.get('/api/middleware-test'); + expect(response.status()).toBe(200); + + const serverTxnEvent = await serverTxnEventPromise; + const middlewareSpans = serverTxnEvent.spans?.filter(span => span.op === 'middleware.nuxt') || []; + + // Find spans for the array hooks middleware + const arrayHooksSpans = middlewareSpans.filter(span => span.data?.['nuxt.middleware.name'] === '05.array-hooks'); + + // Should have spans for 2 onRequest + 1 handler + 2 onBeforeResponse = 5 spans + expect(arrayHooksSpans).toHaveLength(5); + + // Find onRequest array spans + const onRequestSpans = arrayHooksSpans.filter(span => span.data?.['nuxt.middleware.hook.name'] === 'onRequest'); + expect(onRequestSpans).toHaveLength(2); + + // Find onBeforeResponse array spans + const onBeforeResponseSpans = arrayHooksSpans.filter( + span => span.data?.['nuxt.middleware.hook.name'] === 'onBeforeResponse', + ); + expect(onBeforeResponseSpans).toHaveLength(2); + + // Find handler span + const handlerSpan = arrayHooksSpans.find(span => span.data?.['nuxt.middleware.hook.name'] === 'handler'); + expect(handlerSpan).toBeDefined(); + + // Verify index attributes for onRequest array + const onRequest0Span = onRequestSpans.find(span => span.data?.['nuxt.middleware.hook.index'] === 0); + const onRequest1Span = onRequestSpans.find(span => span.data?.['nuxt.middleware.hook.index'] === 1); + + expect(onRequest0Span).toBeDefined(); + expect(onRequest1Span).toBeDefined(); + + // Verify index attributes for onBeforeResponse array + const onBeforeResponse0Span = onBeforeResponseSpans.find(span => span.data?.['nuxt.middleware.hook.index'] === 0); + const onBeforeResponse1Span = onBeforeResponseSpans.find(span => span.data?.['nuxt.middleware.hook.index'] === 1); + + expect(onBeforeResponse0Span).toBeDefined(); + expect(onBeforeResponse1Span).toBeDefined(); + + // Verify span names for array handlers + expect(onRequest0Span?.description).toBe('05.array-hooks.onRequest'); + expect(onRequest1Span?.description).toBe('05.array-hooks.onRequest'); + expect(onBeforeResponse0Span?.description).toBe('05.array-hooks.onBeforeResponse'); + expect(onBeforeResponse1Span?.description).toBe('05.array-hooks.onBeforeResponse'); + + // Verify handler has no index + expect(handlerSpan?.data).not.toHaveProperty('nuxt.middleware.hook.index'); + }); + + test('should handle errors in onRequest hooks', async ({ request }) => { + const serverTxnEventPromise = waitForTransaction('nuxt-4', txnEvent => { + return txnEvent.transaction?.includes('GET /api/middleware-test') ?? false; + }); + + const errorEventPromise = waitForError('nuxt-4', errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === 'OnRequest hook error'; + }); + + // Make request with query param to trigger error in onRequest + const response = await request.get('/api/middleware-test?throwOnRequestError=true'); + expect(response.status()).toBe(500); + + const [serverTxnEvent, errorEvent] = await Promise.all([serverTxnEventPromise, errorEventPromise]); + + // Find the onRequest span that should have error status + const onRequestSpan = serverTxnEvent.spans?.find( + span => + span.op === 'middleware.nuxt' && + span.data?.['nuxt.middleware.name'] === '04.hooks' && + span.data?.['nuxt.middleware.hook.name'] === 'onRequest', + ); + + expect(onRequestSpan).toBeDefined(); + expect(onRequestSpan?.status).toBe('internal_error'); + expect(errorEvent.exception?.values?.[0]?.value).toBe('OnRequest hook error'); + }); + + test('should handle errors in onBeforeResponse hooks', async ({ request }) => { + const serverTxnEventPromise = waitForTransaction('nuxt-4', txnEvent => { + return txnEvent.transaction?.includes('GET /api/middleware-test') ?? false; + }); + + const errorEventPromise = waitForError('nuxt-4', errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === 'OnBeforeResponse hook error'; + }); + + // Make request with query param to trigger error in onBeforeResponse + const response = await request.get('/api/middleware-test?throwOnBeforeResponseError=true'); + expect(response.status()).toBe(500); + + const [serverTxnEvent, errorEvent] = await Promise.all([serverTxnEventPromise, errorEventPromise]); + + // Find the onBeforeResponse span that should have error status + const onBeforeResponseSpan = serverTxnEvent.spans?.find( + span => + span.op === 'middleware.nuxt' && + span.data?.['nuxt.middleware.name'] === '04.hooks' && + span.data?.['nuxt.middleware.hook.name'] === 'onBeforeResponse', + ); + + expect(onBeforeResponseSpan).toBeDefined(); + expect(onBeforeResponseSpan?.status).toBe('internal_error'); + expect(errorEvent.exception?.values?.[0]?.value).toBe('OnBeforeResponse hook error'); + }); + + test('should handle errors in array hooks with proper index attribution', async ({ request }) => { + const serverTxnEventPromise = waitForTransaction('nuxt-4', txnEvent => { + return txnEvent.transaction?.includes('GET /api/middleware-test') ?? false; + }); + + const errorEventPromise = waitForError('nuxt-4', errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === 'OnRequest[1] hook error'; + }); + + // Make request with query param to trigger error in second onRequest handler + const response = await request.get('/api/middleware-test?throwOnRequest1Error=true'); + expect(response.status()).toBe(500); + + const [serverTxnEvent, errorEvent] = await Promise.all([serverTxnEventPromise, errorEventPromise]); + + // Find the second onRequest span that should have error status + const onRequest1Span = serverTxnEvent.spans?.find( + span => + span.op === 'middleware.nuxt' && + span.data?.['nuxt.middleware.name'] === '05.array-hooks' && + span.data?.['nuxt.middleware.hook.name'] === 'onRequest' && + span.data?.['nuxt.middleware.hook.index'] === 1, + ); + + expect(onRequest1Span).toBeDefined(); + expect(onRequest1Span?.status).toBe('internal_error'); + expect(errorEvent.exception?.values?.[0]?.value).toBe('OnRequest[1] hook error'); + + // Verify the first onRequest handler still executed successfully + const onRequest0Span = serverTxnEvent.spans?.find( + span => + span.op === 'middleware.nuxt' && + span.data?.['nuxt.middleware.name'] === '05.array-hooks' && + span.data?.['nuxt.middleware.hook.name'] === 'onRequest' && + span.data?.['nuxt.middleware.hook.index'] === 0, + ); + + expect(onRequest0Span).toBeDefined(); + expect(onRequest0Span?.status).not.toBe('internal_error'); + }); }); From 36706b2e8b1b255c059af2e882d7b4187862f3f1 Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Tue, 30 Sep 2025 11:16:18 +0300 Subject: [PATCH 14/17] refactor: rename wrap function to reduce conflict chance --- .../runtime/hooks/wrapMiddlewareHandler.ts | 2 +- packages/nuxt/src/vite/middlewareConfig.ts | 6 +-- .../hooks/wrapMiddlewareHandler.test.ts | 42 +++++++++---------- 3 files changed, 25 insertions(+), 25 deletions(-) diff --git a/packages/nuxt/src/runtime/hooks/wrapMiddlewareHandler.ts b/packages/nuxt/src/runtime/hooks/wrapMiddlewareHandler.ts index 16f05b32af85..a04b866cd774 100644 --- a/packages/nuxt/src/runtime/hooks/wrapMiddlewareHandler.ts +++ b/packages/nuxt/src/runtime/hooks/wrapMiddlewareHandler.ts @@ -27,7 +27,7 @@ import type { * @param handler The middleware handler. * @param fileName The name of the middleware file. */ -export function wrapMiddlewareHandler( +export function wrapMiddlewareHandlerWithSentry( handler: THandler, fileName: string, ): THandler { diff --git a/packages/nuxt/src/vite/middlewareConfig.ts b/packages/nuxt/src/vite/middlewareConfig.ts index ca5d5487497d..35039517c3e5 100644 --- a/packages/nuxt/src/vite/middlewareConfig.ts +++ b/packages/nuxt/src/vite/middlewareConfig.ts @@ -9,7 +9,7 @@ import type { InputPluginOption } from 'rollup'; export function addMiddlewareImports(): void { addServerImports([ { - name: 'wrapMiddlewareHandler', + name: 'wrapMiddlewareHandlerWithSentry', from: createResolver(import.meta.url).resolve('./runtime/hooks/wrapMiddlewareHandler'), }, ]); @@ -81,10 +81,10 @@ function wrapMiddlewareCode(originalCode: string, fileName: string): string { const cleanFileName = fileName.replace(/\.(ts|js|mjs|mts|cts)$/, ''); return ` -import { wrapMiddlewareHandler } from '#imports'; +import { wrapMiddlewareHandlerWithSentry } from '#imports'; function defineInstrumentedEventHandler(handlerOrObject) { - return defineEventHandler(wrapMiddlewareHandler(handlerOrObject, '${cleanFileName}')); + return defineEventHandler(wrapMiddlewareHandlerWithSentry(handlerOrObject, '${cleanFileName}')); } ${originalCode.replace(/defineEventHandler\(/g, 'defineInstrumentedEventHandler(')} diff --git a/packages/nuxt/test/runtime/hooks/wrapMiddlewareHandler.test.ts b/packages/nuxt/test/runtime/hooks/wrapMiddlewareHandler.test.ts index 44f5f850a4b5..c1f73cd858fa 100644 --- a/packages/nuxt/test/runtime/hooks/wrapMiddlewareHandler.test.ts +++ b/packages/nuxt/test/runtime/hooks/wrapMiddlewareHandler.test.ts @@ -1,7 +1,7 @@ import * as SentryCore from '@sentry/core'; import type { EventHandler, EventHandlerRequest, H3Event } from 'h3'; import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { wrapMiddlewareHandler } from '../../../src/runtime/hooks/wrapMiddlewareHandler'; +import { wrapMiddlewareHandlerWithSentry } from '../../../src/runtime/hooks/wrapMiddlewareHandler'; // Only mock the Sentry APIs we need to verify vi.mock('@sentry/core', async importOriginal => { @@ -17,7 +17,7 @@ vi.mock('@sentry/core', async importOriginal => { }; }); -describe('wrapMiddlewareHandler', () => { +describe('wrapMiddlewareHandlerWithSentry', () => { const mockEvent: H3Event = { path: '/test-path', method: 'GET', @@ -49,7 +49,7 @@ describe('wrapMiddlewareHandler', () => { it('should wrap function handlers correctly and preserve return values', async () => { const functionHandler: EventHandler = vi.fn().mockResolvedValue('success'); - const wrapped = wrapMiddlewareHandler(functionHandler, 'test-middleware'); + const wrapped = wrapMiddlewareHandlerWithSentry(functionHandler, 'test-middleware'); const result = await wrapped(mockEvent); expect(functionHandler).toHaveBeenCalledWith(mockEvent); @@ -60,7 +60,7 @@ describe('wrapMiddlewareHandler', () => { it('should preserve sync return values from function handlers', async () => { const syncHandler: EventHandler = vi.fn().mockReturnValue('sync-result'); - const wrapped = wrapMiddlewareHandler(syncHandler, 'sync-middleware'); + const wrapped = wrapMiddlewareHandlerWithSentry(syncHandler, 'sync-middleware'); const result = await wrapped(mockEvent); expect(syncHandler).toHaveBeenCalledWith(mockEvent); @@ -72,7 +72,7 @@ describe('wrapMiddlewareHandler', () => { it('should handle async function handlers', async () => { const asyncHandler: EventHandler = vi.fn().mockResolvedValue('async-success'); - const wrapped = wrapMiddlewareHandler(asyncHandler, 'async-middleware'); + const wrapped = wrapMiddlewareHandlerWithSentry(asyncHandler, 'async-middleware'); const result = await wrapped(mockEvent); expect(asyncHandler).toHaveBeenCalledWith(mockEvent); @@ -86,7 +86,7 @@ describe('wrapMiddlewareHandler', () => { originalError.stack = 'original-stack-trace'; const failingHandler: EventHandler = vi.fn().mockRejectedValue(originalError); - const wrapped = wrapMiddlewareHandler(failingHandler, 'failing-middleware'); + const wrapped = wrapMiddlewareHandlerWithSentry(failingHandler, 'failing-middleware'); await expect(wrapped(mockEvent)).rejects.toThrow('Original async error'); await expect(wrapped(mockEvent)).rejects.toMatchObject({ @@ -104,7 +104,7 @@ describe('wrapMiddlewareHandler', () => { throw originalError; }); - const wrapped = wrapMiddlewareHandler(failingHandler, 'sync-failing-middleware'); + const wrapped = wrapMiddlewareHandlerWithSentry(failingHandler, 'sync-failing-middleware'); await expect(wrapped(mockEvent)).rejects.toThrow('Original sync error'); await expect(wrapped(mockEvent)).rejects.toBe(originalError); @@ -116,7 +116,7 @@ describe('wrapMiddlewareHandler', () => { const stringError = 'String error'; const failingHandler: EventHandler = vi.fn().mockRejectedValue(stringError); - const wrapped = wrapMiddlewareHandler(failingHandler, 'string-error-middleware'); + const wrapped = wrapMiddlewareHandlerWithSentry(failingHandler, 'string-error-middleware'); await expect(wrapped(mockEvent)).rejects.toBe(stringError); expect(SentryCore.captureException).toHaveBeenCalledWith(stringError, expect.any(Object)); @@ -133,7 +133,7 @@ describe('wrapMiddlewareHandler', () => { const userHandler: EventHandler = vi.fn().mockResolvedValue('user-result'); // Should not throw despite Sentry failure - const wrapped = wrapMiddlewareHandler(userHandler, 'isolated-middleware'); + const wrapped = wrapMiddlewareHandlerWithSentry(userHandler, 'isolated-middleware'); // This should handle the Sentry error gracefully and still call user code await expect(wrapped(mockEvent)).rejects.toThrow('Sentry API failure'); @@ -147,7 +147,7 @@ describe('wrapMiddlewareHandler', () => { handler: baseHandler, }; - const wrapped = wrapMiddlewareHandler(handlerObject, 'object-middleware'); + const wrapped = wrapMiddlewareHandlerWithSentry(handlerObject, 'object-middleware'); // Should return an object with wrapped handler expect(typeof wrapped).toBe('object'); @@ -180,7 +180,7 @@ describe('wrapMiddlewareHandler', () => { onRequest: onRequestHandler, }; - const wrapped = wrapMiddlewareHandler(handlerObject, 'request-middleware'); + const wrapped = wrapMiddlewareHandlerWithSentry(handlerObject, 'request-middleware'); // Should preserve onRequest handler expect(wrapped).toHaveProperty('onRequest'); @@ -219,7 +219,7 @@ describe('wrapMiddlewareHandler', () => { onRequest: [onRequestHandler1, onRequestHandler2], }; - const wrapped = wrapMiddlewareHandler(handlerObject, 'multi-request-middleware'); + const wrapped = wrapMiddlewareHandlerWithSentry(handlerObject, 'multi-request-middleware'); // Should preserve onRequest as array expect(wrapped).toHaveProperty('onRequest'); @@ -266,7 +266,7 @@ describe('wrapMiddlewareHandler', () => { onBeforeResponse: onBeforeResponseHandler, }; - const wrapped = wrapMiddlewareHandler(handlerObject, 'response-middleware'); + const wrapped = wrapMiddlewareHandlerWithSentry(handlerObject, 'response-middleware'); // Should preserve onBeforeResponse handler expect(wrapped).toHaveProperty('onBeforeResponse'); @@ -300,7 +300,7 @@ describe('wrapMiddlewareHandler', () => { onBeforeResponse: [onBeforeResponseHandler1, onBeforeResponseHandler2], }; - const wrapped = wrapMiddlewareHandler(handlerObject, 'multi-response-middleware'); + const wrapped = wrapMiddlewareHandlerWithSentry(handlerObject, 'multi-response-middleware'); // Should preserve onBeforeResponse as array expect(wrapped).toHaveProperty('onBeforeResponse'); @@ -350,7 +350,7 @@ describe('wrapMiddlewareHandler', () => { onBeforeResponse: onBeforeResponseHandler, }; - const wrapped = wrapMiddlewareHandler(handlerObject, 'complex-middleware'); + const wrapped = wrapMiddlewareHandlerWithSentry(handlerObject, 'complex-middleware'); // Should preserve all properties expect(wrapped).toHaveProperty('handler'); @@ -402,7 +402,7 @@ describe('wrapMiddlewareHandler', () => { // No onRequest or onBeforeResponse }; - const wrapped = wrapMiddlewareHandler(handlerObject, 'minimal-object-middleware'); + const wrapped = wrapMiddlewareHandlerWithSentry(handlerObject, 'minimal-object-middleware'); // Should only have handler property expect(wrapped).toHaveProperty('handler'); @@ -430,7 +430,7 @@ describe('wrapMiddlewareHandler', () => { handler: failingHandler, }; - const wrapped = wrapMiddlewareHandler(handlerObject, 'failing-object-middleware'); + const wrapped = wrapMiddlewareHandlerWithSentry(handlerObject, 'failing-object-middleware'); await expect(wrapped.handler(mockEvent)).rejects.toThrow('Handler error'); expect(SentryCore.captureException).toHaveBeenCalledWith(error, expect.any(Object)); @@ -445,7 +445,7 @@ describe('wrapMiddlewareHandler', () => { onRequest: failingOnRequestHandler, }; - const wrapped = wrapMiddlewareHandler(handlerObject, 'failing-request-middleware'); + const wrapped = wrapMiddlewareHandlerWithSentry(handlerObject, 'failing-request-middleware'); await expect(wrapped.onRequest(mockEvent)).rejects.toThrow('OnRequest error'); expect(SentryCore.captureException).toHaveBeenCalledWith(error, expect.any(Object)); @@ -460,7 +460,7 @@ describe('wrapMiddlewareHandler', () => { onBeforeResponse: failingOnBeforeResponseHandler, }; - const wrapped = wrapMiddlewareHandler(handlerObject, 'failing-response-middleware'); + const wrapped = wrapMiddlewareHandlerWithSentry(handlerObject, 'failing-response-middleware'); const mockResponse = { body: 'test-response' }; await expect(wrapped.onBeforeResponse(mockEvent, mockResponse)).rejects.toThrow('OnBeforeResponse error'); @@ -472,7 +472,7 @@ describe('wrapMiddlewareHandler', () => { it('should call Sentry APIs with correct parameters', async () => { const userHandler: EventHandler = vi.fn().mockResolvedValue('api-test-result'); - const wrapped = wrapMiddlewareHandler(userHandler, 'api-middleware'); + const wrapped = wrapMiddlewareHandlerWithSentry(userHandler, 'api-middleware'); await wrapped(mockEvent); // Verify key Sentry APIs are called correctly @@ -494,7 +494,7 @@ describe('wrapMiddlewareHandler', () => { const minimalEvent = { path: '/minimal' } as H3Event; const userHandler: EventHandler = vi.fn().mockResolvedValue('minimal-result'); - const wrapped = wrapMiddlewareHandler(userHandler, 'minimal-middleware'); + const wrapped = wrapMiddlewareHandlerWithSentry(userHandler, 'minimal-middleware'); const result = await wrapped(minimalEvent); expect(result).toBe('minimal-result'); From f79850e1c893266edbfb6546c720ed329c8d635d Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Tue, 30 Sep 2025 11:27:01 +0300 Subject: [PATCH 15/17] fix: replace eventHandler calls as well --- packages/nuxt/src/vite/middlewareConfig.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/nuxt/src/vite/middlewareConfig.ts b/packages/nuxt/src/vite/middlewareConfig.ts index 35039517c3e5..4e3ad62dc6b3 100644 --- a/packages/nuxt/src/vite/middlewareConfig.ts +++ b/packages/nuxt/src/vite/middlewareConfig.ts @@ -57,7 +57,6 @@ function middlewareInstrumentationPlugin(nitro: Nitro): InputPluginOption { // Only transform files we've identified as middleware if (middlewareFiles.has(id)) { const fileName = path.basename(id); - return { code: wrapMiddlewareCode(code, fileName), map: null, @@ -87,6 +86,8 @@ function defineInstrumentedEventHandler(handlerOrObject) { return defineEventHandler(wrapMiddlewareHandlerWithSentry(handlerOrObject, '${cleanFileName}')); } -${originalCode.replace(/defineEventHandler\(/g, 'defineInstrumentedEventHandler(')} +${originalCode + .replace(/defineEventHandler\(/g, 'defineInstrumentedEventHandler(') + .replace(/eventHandler\(/g, 'defineInstrumentedEventHandler(')} `; } From 20eadcb50aa0018d99337597e88f6012ceec01a9 Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Tue, 30 Sep 2025 12:03:12 +0300 Subject: [PATCH 16/17] tests: use eventHandler alias in one of the tests --- .../test-applications/nuxt-3/server/middleware/02.second.ts | 5 +++-- .../test-applications/nuxt-4/server/middleware/02.second.ts | 5 +++-- packages/nuxt/src/vite/middlewareConfig.ts | 6 +++++- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3/server/middleware/02.second.ts b/dev-packages/e2e-tests/test-applications/nuxt-3/server/middleware/02.second.ts index 7534051af316..3b665d48fc5a 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-3/server/middleware/02.second.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-3/server/middleware/02.second.ts @@ -1,6 +1,7 @@ -import { defineEventHandler, setHeader } from '#imports'; +import { eventHandler, setHeader } from '#imports'; -export default defineEventHandler(async event => { +// tests out the eventHandler alias +export default eventHandler(async event => { // Set a header to indicate this middleware ran setHeader(event, 'x-second-middleware', 'executed'); }); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/server/middleware/02.second.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/server/middleware/02.second.ts index 7534051af316..3b665d48fc5a 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-4/server/middleware/02.second.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-4/server/middleware/02.second.ts @@ -1,6 +1,7 @@ -import { defineEventHandler, setHeader } from '#imports'; +import { eventHandler, setHeader } from '#imports'; -export default defineEventHandler(async event => { +// tests out the eventHandler alias +export default eventHandler(async event => { // Set a header to indicate this middleware ran setHeader(event, 'x-second-middleware', 'executed'); }); diff --git a/packages/nuxt/src/vite/middlewareConfig.ts b/packages/nuxt/src/vite/middlewareConfig.ts index 4e3ad62dc6b3..d851345172d8 100644 --- a/packages/nuxt/src/vite/middlewareConfig.ts +++ b/packages/nuxt/src/vite/middlewareConfig.ts @@ -86,8 +86,12 @@ function defineInstrumentedEventHandler(handlerOrObject) { return defineEventHandler(wrapMiddlewareHandlerWithSentry(handlerOrObject, '${cleanFileName}')); } +function instrumentedEventHandler(handlerOrObject) { + return eventHandler(wrapMiddlewareHandlerWithSentry(handlerOrObject, '${cleanFileName}')); +} + ${originalCode .replace(/defineEventHandler\(/g, 'defineInstrumentedEventHandler(') - .replace(/eventHandler\(/g, 'defineInstrumentedEventHandler(')} + .replace(/eventHandler\(/g, 'instrumentedEventHandler(')} `; } From 9a4a1f373cf9cad90587f5698bb9f24f4ad0e06a Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Tue, 30 Sep 2025 12:15:54 +0300 Subject: [PATCH 17/17] fix: only add server imports if the server config file is present --- packages/nuxt/src/module.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/nuxt/src/module.ts b/packages/nuxt/src/module.ts index d1b1e703cb9c..7e9445a154a7 100644 --- a/packages/nuxt/src/module.ts +++ b/packages/nuxt/src/module.ts @@ -112,7 +112,9 @@ export default defineNuxtModule({ }); // Preps the the middleware instrumentation module. - addMiddlewareImports(); + if (serverConfigFile) { + addMiddlewareImports(); + } nuxt.hooks.hook('nitro:init', nitro => { if (serverConfigFile) {