Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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'),
},
};
});
Original file line number Diff line number Diff line change
@@ -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');
});
Original file line number Diff line number Diff line change
@@ -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');
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
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');
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import { expect, test } from '@playwright/test';
import { waitForTransaction, waitForError } 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);
});
});

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',
}),
}),
);
});
});
Original file line number Diff line number Diff line change
@@ -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'),
},
};
});
Original file line number Diff line number Diff line change
@@ -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');
});
Original file line number Diff line number Diff line change
@@ -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');
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
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');
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import { expect, test } from '@playwright/test';
import { waitForTransaction, waitForError } 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);
});
});

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',
}),
}),
);
});
});
8 changes: 8 additions & 0 deletions packages/nuxt/src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -110,7 +111,14 @@ export default defineNuxtModule<ModuleOptions>({
};
});

// 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;
Expand Down
Loading