Skip to content

Commit 492c578

Browse files
author
Luca Forstner
authored
ref(nextjs): Switch edge wrapping to OTEL (#13937)
Emit proper spans for the edge runtime, even if we don't have any build-time instrumentation.
1 parent 74b68c7 commit 492c578

File tree

14 files changed

+243
-350
lines changed

14 files changed

+243
-350
lines changed

dev-packages/e2e-tests/test-applications/nextjs-app-dir/middleware.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,5 +20,5 @@ export async function middleware(request: NextRequest) {
2020

2121
// See "Matching Paths" below to learn more
2222
export const config = {
23-
matcher: ['/api/endpoint-behind-middleware'],
23+
matcher: ['/api/endpoint-behind-middleware', '/api/endpoint-behind-faulty-middleware'],
2424
};

dev-packages/e2e-tests/test-applications/nextjs-app-dir/next-env.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@
33
/// <reference types="next/navigation-types/compat/navigation" />
44

55
// NOTE: This file should not be edited
6-
// see https://nextjs.org/docs/basic-features/typescript for more information.
6+
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.

dev-packages/e2e-tests/test-applications/nextjs-app-dir/pages/api/async-context-edge-endpoint.ts

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,22 @@ export const config = {
77
export default async function handler() {
88
// Without a working async context strategy the two spans created by `Sentry.startSpan()` would be nested.
99

10-
const outerSpanPromise = Sentry.withIsolationScope(() => {
11-
return Sentry.startSpan({ name: 'outer-span' }, () => {
12-
return new Promise<void>(resolve => setTimeout(resolve, 300));
13-
});
10+
const outerSpanPromise = Sentry.startSpan({ name: 'outer-span' }, () => {
11+
return new Promise<void>(resolve => setTimeout(resolve, 300));
1412
});
1513

16-
setTimeout(() => {
17-
Sentry.withIsolationScope(() => {
18-
return Sentry.startSpan({ name: 'inner-span' }, () => {
14+
const innerSpanPromise = new Promise<void>(resolve => {
15+
setTimeout(() => {
16+
Sentry.startSpan({ name: 'inner-span' }, () => {
1917
return new Promise<void>(resolve => setTimeout(resolve, 100));
18+
}).then(() => {
19+
resolve();
2020
});
21-
});
22-
}, 100);
21+
}, 100);
22+
});
2323

2424
await outerSpanPromise;
25+
await innerSpanPromise;
2526

2627
return new Response('ok', { status: 200 });
2728
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import type { NextApiRequest, NextApiResponse } from 'next';
2+
3+
type Data = {
4+
name: string;
5+
};
6+
7+
export default function handler(req: NextApiRequest, res: NextApiResponse<Data>) {
8+
res.status(200).json({ name: 'John Doe' });
9+
}

dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/async-context-edge.test.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@ import { waitForTransaction } from '@sentry-internal/test-utils';
33

44
test('Should allow for async context isolation in the edge SDK', async ({ request }) => {
55
const edgerouteTransactionPromise = waitForTransaction('nextjs-app-dir', async transactionEvent => {
6-
return transactionEvent?.transaction === 'GET /api/async-context-edge-endpoint';
6+
return (
7+
transactionEvent?.transaction === 'GET /api/async-context-edge-endpoint' &&
8+
transactionEvent.contexts?.runtime?.name === 'vercel-edge'
9+
);
710
});
811

912
await request.get('/api/async-context-edge-endpoint');
@@ -13,8 +16,5 @@ test('Should allow for async context isolation in the edge SDK', async ({ reques
1316
const outerSpan = asyncContextEdgerouteTransaction.spans?.find(span => span.description === 'outer-span');
1417
const innerSpan = asyncContextEdgerouteTransaction.spans?.find(span => span.description === 'inner-span');
1518

16-
// @ts-expect-error parent_span_id exists
17-
expect(outerSpan?.parent_span_id).toStrictEqual(asyncContextEdgerouteTransaction.contexts?.trace?.span_id);
18-
// @ts-expect-error parent_span_id exists
19-
expect(innerSpan?.parent_span_id).toStrictEqual(asyncContextEdgerouteTransaction.contexts?.trace?.span_id);
19+
expect(outerSpan?.parent_span_id).toStrictEqual(innerSpan?.parent_span_id);
2020
});

dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/edge-route.test.ts

Lines changed: 17 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ test('Should create a transaction for edge routes', async ({ request }) => {
55
const edgerouteTransactionPromise = waitForTransaction('nextjs-app-dir', async transactionEvent => {
66
return (
77
transactionEvent?.transaction === 'GET /api/edge-endpoint' &&
8-
transactionEvent?.contexts?.trace?.status === 'ok' &&
98
transactionEvent.contexts?.runtime?.name === 'vercel-edge'
109
);
1110
});
@@ -24,31 +23,11 @@ test('Should create a transaction for edge routes', async ({ request }) => {
2423
expect(edgerouteTransaction.request?.headers?.['x-yeet']).toBe('test-value');
2524
});
2625

27-
test('Should create a transaction with error status for faulty edge routes', async ({ request }) => {
26+
test('Faulty edge routes', async ({ request }) => {
2827
const edgerouteTransactionPromise = waitForTransaction('nextjs-app-dir', async transactionEvent => {
29-
return (
30-
transactionEvent?.transaction === 'GET /api/error-edge-endpoint' &&
31-
transactionEvent?.contexts?.trace?.status === 'unknown_error'
32-
);
28+
return transactionEvent?.transaction === 'GET /api/error-edge-endpoint';
3329
});
3430

35-
request.get('/api/error-edge-endpoint').catch(() => {
36-
// Noop
37-
});
38-
39-
const edgerouteTransaction = await edgerouteTransactionPromise;
40-
41-
expect(edgerouteTransaction.contexts?.trace?.status).toBe('unknown_error');
42-
expect(edgerouteTransaction.contexts?.trace?.op).toBe('http.server');
43-
expect(edgerouteTransaction.contexts?.runtime?.name).toBe('vercel-edge');
44-
45-
// Assert that isolation scope works properly
46-
expect(edgerouteTransaction.tags?.['my-isolated-tag']).toBe(true);
47-
expect(edgerouteTransaction.tags?.['my-global-scope-isolated-tag']).not.toBeDefined();
48-
});
49-
50-
// TODO(lforst): This cannot make it into production - Make sure to fix this test
51-
test.skip('Should record exceptions for faulty edge routes', async ({ request }) => {
5231
const errorEventPromise = waitForError('nextjs-app-dir', errorEvent => {
5332
return errorEvent?.exception?.values?.[0]?.value === 'Edge Route Error';
5433
});
@@ -57,11 +36,21 @@ test.skip('Should record exceptions for faulty edge routes', async ({ request })
5736
// Noop
5837
});
5938

60-
const errorEvent = await errorEventPromise;
39+
const [edgerouteTransaction, errorEvent] = await Promise.all([
40+
test.step('should create a transaction', () => edgerouteTransactionPromise),
41+
test.step('should create an error event', () => errorEventPromise),
42+
]);
6143

62-
// Assert that isolation scope works properly
63-
expect(errorEvent.tags?.['my-isolated-tag']).toBe(true);
64-
expect(errorEvent.tags?.['my-global-scope-isolated-tag']).not.toBeDefined();
44+
test.step('should create transactions with the right fields', () => {
45+
expect(edgerouteTransaction.contexts?.trace?.status).toBe('unknown_error');
46+
expect(edgerouteTransaction.contexts?.trace?.op).toBe('http.server');
47+
expect(edgerouteTransaction.contexts?.runtime?.name).toBe('vercel-edge');
48+
});
6549

66-
expect(errorEvent.transaction).toBe('GET /api/error-edge-endpoint');
50+
test.step('should have scope isolation', () => {
51+
expect(edgerouteTransaction.tags?.['my-isolated-tag']).toBe(true);
52+
expect(edgerouteTransaction.tags?.['my-global-scope-isolated-tag']).not.toBeDefined();
53+
expect(errorEvent.tags?.['my-isolated-tag']).toBe(true);
54+
expect(errorEvent.tags?.['my-global-scope-isolated-tag']).not.toBeDefined();
55+
});
6756
});

dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/edge.test.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,19 @@ test('Should record exceptions for faulty edge server components', async ({ page
1919
expect(errorEvent.transaction).toBe(`Page Server Component (/edge-server-components/error)`);
2020
});
2121

22-
// TODO(lforst): This test skip cannot make it into production - make sure to fix this test before merging into develop branch
22+
// TODO(lforst): Fix this test
2323
test.skip('Should record transaction for edge server components', async ({ page }) => {
2424
const serverComponentTransactionPromise = waitForTransaction('nextjs-app-dir', async transactionEvent => {
25-
return transactionEvent?.transaction === 'GET /edge-server-components';
25+
return (
26+
transactionEvent?.transaction === 'GET /edge-server-components' &&
27+
transactionEvent.contexts?.runtime?.name === 'vercel-edge'
28+
);
2629
});
2730

2831
await page.goto('/edge-server-components');
2932

3033
const serverComponentTransaction = await serverComponentTransactionPromise;
3134

32-
expect(serverComponentTransaction).toBe(1);
3335
expect(serverComponentTransaction).toBeDefined();
3436
expect(serverComponentTransaction.request?.headers).toBeDefined();
3537

dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/middleware.test.ts

Lines changed: 20 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { waitForError, waitForTransaction } from '@sentry-internal/test-utils';
33

44
test('Should create a transaction for middleware', async ({ request }) => {
55
const middlewareTransactionPromise = waitForTransaction('nextjs-app-dir', async transactionEvent => {
6-
return transactionEvent?.transaction === 'middleware' && transactionEvent?.contexts?.trace?.status === 'ok';
6+
return transactionEvent?.transaction === 'middleware GET /api/endpoint-behind-middleware';
77
});
88

99
const response = await request.get('/api/endpoint-behind-middleware');
@@ -12,54 +12,48 @@ test('Should create a transaction for middleware', async ({ request }) => {
1212
const middlewareTransaction = await middlewareTransactionPromise;
1313

1414
expect(middlewareTransaction.contexts?.trace?.status).toBe('ok');
15-
expect(middlewareTransaction.contexts?.trace?.op).toBe('middleware.nextjs');
15+
expect(middlewareTransaction.contexts?.trace?.op).toBe('http.server.middleware');
1616
expect(middlewareTransaction.contexts?.runtime?.name).toBe('vercel-edge');
1717

1818
// Assert that isolation scope works properly
1919
expect(middlewareTransaction.tags?.['my-isolated-tag']).toBe(true);
2020
expect(middlewareTransaction.tags?.['my-global-scope-isolated-tag']).not.toBeDefined();
2121
});
2222

23-
test('Should create a transaction with error status for faulty middleware', async ({ request }) => {
23+
test('Faulty middlewares', async ({ request }) => {
2424
const middlewareTransactionPromise = waitForTransaction('nextjs-app-dir', async transactionEvent => {
25-
return (
26-
transactionEvent?.transaction === 'middleware' && transactionEvent?.contexts?.trace?.status === 'unknown_error'
27-
);
25+
return transactionEvent?.transaction === 'middleware GET /api/endpoint-behind-faulty-middleware';
2826
});
2927

30-
request.get('/api/endpoint-behind-middleware', { headers: { 'x-should-throw': '1' } }).catch(() => {
31-
// Noop
32-
});
33-
34-
const middlewareTransaction = await middlewareTransactionPromise;
35-
36-
expect(middlewareTransaction.contexts?.trace?.status).toBe('unknown_error');
37-
expect(middlewareTransaction.contexts?.trace?.op).toBe('middleware.nextjs');
38-
expect(middlewareTransaction.contexts?.runtime?.name).toBe('vercel-edge');
39-
});
40-
41-
// TODO(lforst): This cannot make it into production - Make sure to fix this test
42-
test.skip('Records exceptions happening in middleware', async ({ request }) => {
4328
const errorEventPromise = waitForError('nextjs-app-dir', errorEvent => {
4429
return errorEvent?.exception?.values?.[0]?.value === 'Middleware Error';
4530
});
4631

47-
request.get('/api/endpoint-behind-middleware', { headers: { 'x-should-throw': '1' } }).catch(() => {
32+
request.get('/api/endpoint-behind-faulty-middleware', { headers: { 'x-should-throw': '1' } }).catch(() => {
4833
// Noop
4934
});
5035

51-
const errorEvent = await errorEventPromise;
36+
await test.step('should record transactions', async () => {
37+
const middlewareTransaction = await middlewareTransactionPromise;
38+
expect(middlewareTransaction.contexts?.trace?.status).toBe('unknown_error');
39+
expect(middlewareTransaction.contexts?.trace?.op).toBe('http.server.middleware');
40+
expect(middlewareTransaction.contexts?.runtime?.name).toBe('vercel-edge');
41+
});
5242

53-
// Assert that isolation scope works properly
54-
expect(errorEvent.tags?.['my-isolated-tag']).toBe(true);
55-
expect(errorEvent.tags?.['my-global-scope-isolated-tag']).not.toBeDefined();
56-
expect(errorEvent.transaction).toBe('middleware');
43+
await test.step('should record exceptions', async () => {
44+
const errorEvent = await errorEventPromise;
45+
46+
// Assert that isolation scope works properly
47+
expect(errorEvent.tags?.['my-isolated-tag']).toBe(true);
48+
expect(errorEvent.tags?.['my-global-scope-isolated-tag']).not.toBeDefined();
49+
expect(errorEvent.transaction).toBe('middleware GET /api/endpoint-behind-faulty-middleware');
50+
});
5751
});
5852

5953
test('Should trace outgoing fetch requests inside middleware and create breadcrumbs for it', async ({ request }) => {
6054
const middlewareTransactionPromise = waitForTransaction('nextjs-app-dir', async transactionEvent => {
6155
return (
62-
transactionEvent?.transaction === 'middleware' &&
56+
transactionEvent?.transaction === 'middleware GET /api/endpoint-behind-middleware' &&
6357
!!transactionEvent.spans?.find(span => span.op === 'http.client')
6458
);
6559
});

packages/nextjs/src/common/utils/edgeWrapperUtils.ts

Lines changed: 0 additions & 91 deletions
This file was deleted.

0 commit comments

Comments
 (0)