Skip to content

Commit b47e68c

Browse files
committed
wip
1 parent 7c84ee5 commit b47e68c

File tree

8 files changed

+157
-0
lines changed

8 files changed

+157
-0
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
@sentry:registry=http://127.0.0.1:4873
2+
@sentry-internal:registry=http://127.0.0.1:4873
3+
public-hoist-pattern[]=*import-in-the-middle*
4+
public-hoist-pattern[]=*require-in-the-middle*
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export function GET() {
2+
return Response.json({ name: 'John Doe' });
3+
}

dev-packages/e2e-tests/test-applications/nextjs-16/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
},
2323
"dependencies": {
2424
"@sentry/nextjs": "latest || *",
25+
"@sentry/core": "latest || *",
2526
"ai": "^3.0.0",
2627
"import-in-the-middle": "^1",
2728
"next": "16.0.0-beta.0",
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { getDefaultIsolationScope } from '@sentry/core';
2+
import * as Sentry from '@sentry/nextjs';
3+
import { NextResponse } from 'next/server';
4+
import type { NextRequest } from 'next/server';
5+
6+
export async function proxy(request: NextRequest) {
7+
Sentry.setTag('my-isolated-tag', true);
8+
Sentry.setTag('my-global-scope-isolated-tag', getDefaultIsolationScope().getScopeData().tags['my-isolated-tag']); // We set this tag to be able to assert that the previously set tag has not leaked into the global isolation scope
9+
10+
if (request.headers.has('x-should-throw')) {
11+
throw new Error('Middleware Error');
12+
}
13+
14+
if (request.headers.has('x-should-make-request')) {
15+
await fetch('http://localhost:3030/');
16+
}
17+
18+
return NextResponse.next();
19+
}
20+
21+
// See "Matching Paths" below to learn more
22+
export const config = {
23+
matcher: ['/api/endpoint-behind-middleware', '/api/endpoint-behind-faulty-middleware'],
24+
};

dev-packages/e2e-tests/test-applications/nextjs-16/sentry.edge.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,5 @@ Sentry.init({
66
tunnel: `http://localhost:3031/`, // proxy server
77
tracesSampleRate: 1.0,
88
sendDefaultPii: true,
9+
// debug: true,
910
});

dev-packages/e2e-tests/test-applications/nextjs-16/sentry.server.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,6 @@ Sentry.init({
66
tunnel: `http://localhost:3031/`, // proxy server
77
tracesSampleRate: 1.0,
88
sendDefaultPii: true,
9+
// debug: true,
910
integrations: [Sentry.vercelAIIntegration()],
1011
});
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import { expect, test } from '@playwright/test';
2+
import { waitForError, waitForTransaction } from '@sentry-internal/test-utils';
3+
4+
test('Should create a transaction for middleware', async ({ request }) => {
5+
const middlewareTransactionPromise = waitForTransaction('nextjs-16', async transactionEvent => {
6+
return transactionEvent?.transaction === 'middleware GET';
7+
});
8+
9+
const response = await request.get('/api/endpoint-behind-middleware');
10+
expect(await response.json()).toStrictEqual({ name: 'John Doe' });
11+
12+
const middlewareTransaction = await middlewareTransactionPromise;
13+
14+
expect(middlewareTransaction.contexts?.trace?.status).toBe('ok');
15+
expect(middlewareTransaction.contexts?.trace?.op).toBe('http.server.middleware');
16+
expect(middlewareTransaction.contexts?.runtime?.name).toBe('vercel-edge');
17+
expect(middlewareTransaction.transaction_info?.source).toBe('url');
18+
19+
// Assert that isolation scope works properly
20+
expect(middlewareTransaction.tags?.['my-isolated-tag']).toBe(true);
21+
expect(middlewareTransaction.tags?.['my-global-scope-isolated-tag']).not.toBeDefined();
22+
});
23+
24+
test('Faulty middlewares', async ({ request }) => {
25+
const middlewareTransactionPromise = waitForTransaction('nextjs-16', async transactionEvent => {
26+
return transactionEvent?.transaction === 'middleware GET';
27+
});
28+
29+
const errorEventPromise = waitForError('nextjs-16', errorEvent => {
30+
return errorEvent?.exception?.values?.[0]?.value === 'Middleware Error';
31+
});
32+
33+
request.get('/api/endpoint-behind-middleware', { headers: { 'x-should-throw': '1' } }).catch(() => {
34+
// Noop
35+
});
36+
37+
await test.step('should record transactions', async () => {
38+
const middlewareTransaction = await middlewareTransactionPromise;
39+
expect(middlewareTransaction.contexts?.trace?.status).toBe('unknown_error');
40+
expect(middlewareTransaction.contexts?.trace?.op).toBe('http.server.middleware');
41+
expect(middlewareTransaction.contexts?.runtime?.name).toBe('vercel-edge');
42+
expect(middlewareTransaction.transaction_info?.source).toBe('url');
43+
});
44+
45+
await test.step('should record exceptions', async () => {
46+
const errorEvent = await errorEventPromise;
47+
48+
// Assert that isolation scope works properly
49+
expect(errorEvent.tags?.['my-isolated-tag']).toBe(true);
50+
expect(errorEvent.tags?.['my-global-scope-isolated-tag']).not.toBeDefined();
51+
console.log('errorEvent', errorEvent.transaction);
52+
expect(errorEvent.transaction).toBe('middleware GET');
53+
});
54+
});
55+
56+
test('Should trace outgoing fetch requests inside middleware and create breadcrumbs for it', async ({ request }) => {
57+
const middlewareTransactionPromise = waitForTransaction('nextjs-16', async transactionEvent => {
58+
return (
59+
transactionEvent?.transaction === 'middleware GET /api/endpoint-behind-middleware' &&
60+
!!transactionEvent.spans?.find(span => span.op === 'http.client')
61+
);
62+
});
63+
64+
request.get('/api/endpoint-behind-middleware', { headers: { 'x-should-make-request': '1' } }).catch(() => {
65+
// Noop
66+
});
67+
68+
const middlewareTransaction = await middlewareTransactionPromise;
69+
70+
expect(middlewareTransaction.spans).toEqual(
71+
expect.arrayContaining([
72+
{
73+
data: {
74+
'http.method': 'GET',
75+
'http.response.status_code': 200,
76+
type: 'fetch',
77+
url: 'http://localhost:3030/',
78+
'http.url': 'http://localhost:3030/',
79+
'server.address': 'localhost:3030',
80+
'sentry.op': 'http.client',
81+
'sentry.origin': 'auto.http.wintercg_fetch',
82+
},
83+
description: 'GET http://localhost:3030/',
84+
op: 'http.client',
85+
origin: 'auto.http.wintercg_fetch',
86+
parent_span_id: expect.stringMatching(/[a-f0-9]{16}/),
87+
span_id: expect.stringMatching(/[a-f0-9]{16}/),
88+
start_timestamp: expect.any(Number),
89+
status: 'ok',
90+
timestamp: expect.any(Number),
91+
trace_id: expect.stringMatching(/[a-f0-9]{32}/),
92+
},
93+
]),
94+
);
95+
expect(middlewareTransaction.breadcrumbs).toEqual(
96+
expect.arrayContaining([
97+
{
98+
category: 'fetch',
99+
data: { method: 'GET', status_code: 200, url: 'http://localhost:3030/' },
100+
timestamp: expect.any(Number),
101+
type: 'http',
102+
},
103+
]),
104+
);
105+
});

packages/nextjs/src/edge/index.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import {
22
applySdkMetadata,
3+
getCapturedScopesOnSpan,
4+
getCurrentScope,
35
getGlobalScope,
46
getIsolationScope,
57
getRootSpan,
@@ -8,6 +10,7 @@ import {
810
SEMANTIC_ATTRIBUTE_SENTRY_OP,
911
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
1012
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
13+
setCapturedScopesOnSpan,
1114
spanToJSON,
1215
stripUrlQueryAndFragment,
1316
vercelWaitUntil,
@@ -18,6 +21,8 @@ import { addHeadersAsAttributes } from '../common/utils/addHeadersAsAttributes';
1821
import { isBuild } from '../common/utils/isBuild';
1922
import { flushSafelyWithTimeout } from '../common/utils/responseEnd';
2023
import { distDirRewriteFramesIntegration } from './distDirRewriteFramesIntegration';
24+
import { getScopesFromContext } from '@sentry/opentelemetry';
25+
import { context } from '@opentelemetry/api';
2126

2227
export * from '@sentry/vercel-edge';
2328
export * from '../common';
@@ -73,6 +78,19 @@ export function init(options: VercelEdgeOptions = {}): void {
7378
if (spanAttributes?.['next.span_type'] === 'Middleware.execute') {
7479
span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'http.server.middleware');
7580
span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'url');
81+
82+
if (isRootSpan) {
83+
// Fork isolation scope for middleware requests
84+
const scopes = getCapturedScopesOnSpan(span);
85+
const isolationScope = (scopes.isolationScope || getIsolationScope()).clone();
86+
const scope = scopes.scope || getCurrentScope();
87+
const currentScopesPointer = getScopesFromContext(context.active());
88+
if (currentScopesPointer) {
89+
currentScopesPointer.isolationScope = isolationScope;
90+
}
91+
92+
setCapturedScopesOnSpan(span, scope, isolationScope);
93+
}
7694
}
7795

7896
if (isRootSpan) {

0 commit comments

Comments
 (0)