-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
feat(nuxt): implement server middleware instrumentation #17796
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
+749
−0
Draft
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
83e3690
feat: implement nuxt server middleware instrumentation
logaretm 24bf047
style: sprinkle comments
logaretm e0b3cc7
fix: capture exceptions at the span level
logaretm 4ef4f7e
refactor: rename wrapping fn
logaretm 4bc0d10
feat: enrich span attributes with request data
logaretm ccdaba8
test: added basic tests
logaretm a8d51ca
tests: added middleware e2e tests
logaretm a8ce604
test: added middleware span error test
logaretm File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
15 changes: 15 additions & 0 deletions
dev-packages/e2e-tests/test-applications/nuxt-3/server/api/middleware-test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'), | ||
}, | ||
}; | ||
}); |
6 changes: 6 additions & 0 deletions
dev-packages/e2e-tests/test-applications/nuxt-3/server/middleware/01.first.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'); | ||
}); |
6 changes: 6 additions & 0 deletions
dev-packages/e2e-tests/test-applications/nuxt-3/server/middleware/02.second.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'); | ||
}); |
12 changes: 12 additions & 0 deletions
dev-packages/e2e-tests/test-applications/nuxt-3/server/middleware/03.auth.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'); | ||
}); |
121 changes: 121 additions & 0 deletions
dev-packages/e2e-tests/test-applications/nuxt-3/tests/middleware.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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', | ||
}), | ||
}), | ||
); | ||
}); | ||
}); |
15 changes: 15 additions & 0 deletions
dev-packages/e2e-tests/test-applications/nuxt-4/server/api/middleware-test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'), | ||
}, | ||
}; | ||
}); |
6 changes: 6 additions & 0 deletions
dev-packages/e2e-tests/test-applications/nuxt-4/server/middleware/01.first.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'); | ||
}); |
6 changes: 6 additions & 0 deletions
dev-packages/e2e-tests/test-applications/nuxt-4/server/middleware/02.second.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'); | ||
}); |
12 changes: 12 additions & 0 deletions
dev-packages/e2e-tests/test-applications/nuxt-4/server/middleware/03.auth.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'); | ||
}); |
121 changes: 121 additions & 0 deletions
dev-packages/e2e-tests/test-applications/nuxt-4/tests/middleware.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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', | ||
}), | ||
}), | ||
); | ||
}); | ||
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.