Skip to content

Commit da758e3

Browse files
author
Luca Forstner
authored
test(e2e): Assert correct isolation scopes in Next.js (#11480)
1 parent 7383f8a commit da758e3

File tree

20 files changed

+251
-92
lines changed

20 files changed

+251
-92
lines changed

dev-packages/e2e-tests/test-applications/nextjs-14/app/generation-functions/page.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import { getDefaultIsolationScope } from '@sentry/core';
2+
import * as Sentry from '@sentry/nextjs';
3+
14
export const dynamic = 'force-dynamic';
25

36
export default function Page() {
@@ -9,6 +12,9 @@ export async function generateMetadata({
912
}: {
1013
searchParams: { [key: string]: string | string[] | undefined };
1114
}) {
15+
Sentry.setTag('my-isolated-tag', true);
16+
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
17+
1218
if (searchParams['shouldThrowInGenerateMetadata']) {
1319
throw new Error('generateMetadata Error');
1420
}

dev-packages/e2e-tests/test-applications/nextjs-14/tests/generation-functions.test.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,14 @@ test('Should send a transaction and an error event for a faulty generateMetadata
3737

3838
await page.goto(`/generation-functions?metadataTitle=${testTitle}&shouldThrowInGenerateMetadata=1`);
3939

40-
expect(await transactionPromise).toBeDefined();
41-
expect(await errorEventPromise).toBeDefined();
40+
const errorEvent = await errorEventPromise;
41+
const transactionEvent = await transactionPromise;
42+
43+
// Assert that isolation scope works properly
44+
expect(errorEvent.tags?.['my-isolated-tag']).toBe(true);
45+
expect(errorEvent.tags?.['my-global-scope-isolated-tag']).not.toBeDefined();
46+
expect(transactionEvent.tags?.['my-isolated-tag']).toBe(true);
47+
expect(transactionEvent.tags?.['my-global-scope-isolated-tag']).not.toBeDefined();
4248
});
4349

4450
test('Should send a transaction event for a generateViewport() function invokation', async ({ page }) => {
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
1+
import { getDefaultIsolationScope } from '@sentry/core';
2+
import * as Sentry from '@sentry/nextjs';
3+
14
export const dynamic = 'force-dynamic';
25

36
export const runtime = 'edge';
47

58
export default async function Page() {
9+
Sentry.setTag('my-isolated-tag', true);
10+
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
611
throw new Error('Edge Server Component Error');
712
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
1+
import { getDefaultIsolationScope } from '@sentry/core';
2+
import * as Sentry from '@sentry/nextjs';
3+
14
export const dynamic = 'force-dynamic';
25

36
export const runtime = 'edge';
47

58
export default async function Page() {
9+
Sentry.setTag('my-isolated-tag', true);
10+
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
11+
612
return <h1>Hello world!</h1>;
713
}

dev-packages/e2e-tests/test-applications/nextjs-app-dir/app/route-handlers/[param]/edge/route.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { getDefaultIsolationScope } from '@sentry/core';
2+
import * as Sentry from '@sentry/nextjs';
13
import { NextResponse } from 'next/server';
24

35
export const runtime = 'edge';
@@ -7,5 +9,8 @@ export async function PATCH() {
79
}
810

911
export async function DELETE(): Promise<Response> {
12+
Sentry.setTag('my-isolated-tag', true);
13+
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
14+
1015
throw new Error('route-handler-edge-error');
1116
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
import { getDefaultIsolationScope } from '@sentry/core';
2+
import * as Sentry from '@sentry/nextjs';
3+
14
export async function PUT(): Promise<Response> {
5+
Sentry.setTag('my-isolated-tag', true);
6+
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
7+
28
throw new Error('route-handler-error');
39
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { getDefaultIsolationScope } from '@sentry/core';
2+
import * as Sentry from '@sentry/nextjs';
3+
4+
export const dynamic = 'force-dynamic';
5+
6+
export default async function FaultyServerComponent() {
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 (Math.random() + 1 > 0) {
11+
throw new Error('I am a faulty server component');
12+
}
13+
14+
return null;
15+
}

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
1+
import { getDefaultIsolationScope } from '@sentry/core';
2+
import * as Sentry from '@sentry/nextjs';
13
import { NextResponse } from 'next/server';
24
import type { NextRequest } from 'next/server';
35

46
export async function middleware(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+
510
if (request.headers.has('x-should-throw')) {
611
throw new Error('Middleware Error');
712
}

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
1+
import { getDefaultIsolationScope } from '@sentry/core';
2+
import * as Sentry from '@sentry/nextjs';
3+
14
export const config = {
25
runtime: 'edge',
36
};
47

58
export default async function handler() {
9+
Sentry.setTag('my-isolated-tag', true);
10+
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
11+
612
return new Response(
713
JSON.stringify({
814
name: 'Jim Halpert',
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
1+
import { getDefaultIsolationScope } from '@sentry/core';
2+
import * as Sentry from '@sentry/nextjs';
3+
14
export const config = { runtime: 'edge' };
25

36
export default () => {
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
49
throw new Error('Edge Route Error');
510
};

dev-packages/e2e-tests/test-applications/nextjs-app-dir/pages/pages-router/ssr-error-fc.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1+
import { getDefaultIsolationScope } from '@sentry/core';
2+
import * as Sentry from '@sentry/nextjs';
3+
14
export default function Page() {
5+
Sentry.setTag('my-isolated-tag', true);
6+
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
7+
28
throw new Error('Pages SSR Error FC');
39
return <div>Hello world!</div>;
410
}

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ test('Should create a transaction with error status for faulty edge routes', asy
4040
expect(edgerouteTransaction.contexts?.trace?.status).toBe('internal_error');
4141
expect(edgerouteTransaction.contexts?.trace?.op).toBe('http.server');
4242
expect(edgerouteTransaction.contexts?.runtime?.name).toBe('vercel-edge');
43+
44+
// Assert that isolation scope works properly
45+
expect(edgerouteTransaction.tags?.['my-isolated-tag']).toBe(true);
46+
expect(edgerouteTransaction.tags?.['my-global-scope-isolated-tag']).not.toBeDefined();
4347
});
4448

4549
test('Should record exceptions for faulty edge routes', async ({ request }) => {
@@ -51,5 +55,9 @@ test('Should record exceptions for faulty edge routes', async ({ request }) => {
5155
// Noop
5256
});
5357

54-
expect(await errorEventPromise).toBeDefined();
58+
const errorEvent = await errorEventPromise;
59+
60+
// Assert that isolation scope works properly
61+
expect(errorEvent.tags?.['my-isolated-tag']).toBe(true);
62+
expect(errorEvent.tags?.['my-global-scope-isolated-tag']).not.toBeDefined();
5563
});

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

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,13 @@ test('Should record exceptions for faulty edge server components', async ({ page
88

99
await page.goto('/edge-server-components/error');
1010

11-
expect(await errorEventPromise).toBeDefined();
11+
const errorEvent = await errorEventPromise;
12+
13+
expect(errorEvent).toBeDefined();
14+
15+
// Assert that isolation scope works properly
16+
expect(errorEvent.tags?.['my-isolated-tag']).toBe(true);
17+
expect(errorEvent.tags?.['my-global-scope-isolated-tag']).not.toBeDefined();
1218
});
1319

1420
test('Should record transaction for edge server components', async ({ page }) => {
@@ -22,4 +28,8 @@ test('Should record transaction for edge server components', async ({ page }) =>
2228

2329
expect(serverComponentTransaction).toBeDefined();
2430
expect(serverComponentTransaction.request?.headers).toBeDefined();
31+
32+
// Assert that isolation scope works properly
33+
expect(serverComponentTransaction.tags?.['my-isolated-tag']).toBe(true);
34+
expect(serverComponentTransaction.tags?.['my-global-scope-isolated-tag']).not.toBeDefined();
2535
});

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ test('Should create a transaction for middleware', async ({ request }) => {
1414
expect(middlewareTransaction.contexts?.trace?.status).toBe('ok');
1515
expect(middlewareTransaction.contexts?.trace?.op).toBe('middleware.nextjs');
1616
expect(middlewareTransaction.contexts?.runtime?.name).toBe('vercel-edge');
17+
18+
// Assert that isolation scope works properly
19+
expect(middlewareTransaction.tags?.['my-isolated-tag']).toBe(true);
20+
expect(middlewareTransaction.tags?.['my-global-scope-isolated-tag']).not.toBeDefined();
1721
});
1822

1923
test('Should create a transaction with error status for faulty middleware', async ({ request }) => {
@@ -43,7 +47,11 @@ test('Records exceptions happening in middleware', async ({ request }) => {
4347
// Noop
4448
});
4549

46-
expect(await errorEventPromise).toBeDefined();
50+
const errorEvent = await errorEventPromise;
51+
52+
// Assert that isolation scope works properly
53+
expect(errorEvent.tags?.['my-isolated-tag']).toBe(true);
54+
expect(errorEvent.tags?.['my-global-scope-isolated-tag']).not.toBeDefined();
4755
});
4856

4957
test('Should trace outgoing fetch requests inside middleware and create breadcrumbs for it', async ({ request }) => {

dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/pages-ssr-errors.test.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ test('Will capture error for SSR rendering error with a connected trace (Functio
2424
return errorEvent?.exception?.values?.[0]?.value === 'Pages SSR Error FC';
2525
});
2626

27-
const serverComponentTransaction = waitForTransaction('nextjs-13-app-dir', async transactionEvent => {
27+
const ssrTransactionPromise = waitForTransaction('nextjs-13-app-dir', async transactionEvent => {
2828
return (
2929
transactionEvent?.transaction === '/pages-router/ssr-error-fc' &&
3030
(await errorEventPromise).contexts?.trace?.trace_id === transactionEvent.contexts?.trace?.trace_id
@@ -33,6 +33,14 @@ test('Will capture error for SSR rendering error with a connected trace (Functio
3333

3434
await page.goto('/pages-router/ssr-error-fc');
3535

36-
expect(await errorEventPromise).toBeDefined();
37-
expect(await serverComponentTransaction).toBeDefined();
36+
const errorEvent = await errorEventPromise;
37+
const ssrTransaction = await ssrTransactionPromise;
38+
39+
// Assert that isolation scope works properly
40+
expect(errorEvent.tags?.['my-isolated-tag']).toBe(true);
41+
expect(errorEvent.tags?.['my-global-scope-isolated-tag']).not.toBeDefined();
42+
43+
// TODO(lforst): Reuse SSR request span isolation scope to fix the following two assertions
44+
// expect(ssrTransaction.tags?.['my-isolated-tag']).toBe(true);
45+
// expect(ssrTransaction.tags?.['my-global-scope-isolated-tag']).not.toBeDefined();
3846
});

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,12 @@ test('Should record exceptions and transactions for faulty route handlers', asyn
4848
const routehandlerTransaction = await routehandlerTransactionPromise;
4949
const routehandlerError = await errorEventPromise;
5050

51+
// Assert that isolation scope works properly
52+
expect(routehandlerTransaction.tags?.['my-isolated-tag']).toBe(true);
53+
expect(routehandlerTransaction.tags?.['my-global-scope-isolated-tag']).not.toBeDefined();
54+
expect(routehandlerError.tags?.['my-isolated-tag']).toBe(true);
55+
expect(routehandlerError.tags?.['my-global-scope-isolated-tag']).not.toBeDefined();
56+
5157
expect(routehandlerTransaction.contexts?.trace?.status).toBe('unknown_error');
5258
expect(routehandlerTransaction.contexts?.trace?.op).toBe('http.server');
5359

@@ -87,6 +93,12 @@ test.describe('Edge runtime', () => {
8793
const routehandlerTransaction = await routehandlerTransactionPromise;
8894
const routehandlerError = await errorEventPromise;
8995

96+
// Assert that isolation scope works properly
97+
expect(routehandlerTransaction.tags?.['my-isolated-tag']).toBe(true);
98+
expect(routehandlerTransaction.tags?.['my-global-scope-isolated-tag']).not.toBeDefined();
99+
expect(routehandlerError.tags?.['my-isolated-tag']).toBe(true);
100+
expect(routehandlerError.tags?.['my-global-scope-isolated-tag']).not.toBeDefined();
101+
90102
expect(routehandlerTransaction.contexts?.trace?.status).toBe('internal_error');
91103
expect(routehandlerTransaction.contexts?.trace?.op).toBe('http.server');
92104
expect(routehandlerTransaction.contexts?.runtime?.name).toBe('vercel-edge');
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/event-proxy-server';
3+
import axios, { AxiosError } from 'axios';
4+
5+
const authToken = process.env.E2E_TEST_AUTH_TOKEN;
6+
const sentryTestOrgSlug = process.env.E2E_TEST_SENTRY_ORG_SLUG;
7+
const sentryTestProject = process.env.E2E_TEST_SENTRY_TEST_PROJECT;
8+
const EVENT_POLLING_TIMEOUT = 90_000;
9+
10+
test('Sends a transaction for a server component', async ({ page }) => {
11+
// TODO: Fix that this is flakey on dev server - might be an SDK bug
12+
test.skip(process.env.TEST_ENV === 'production', 'Flakey on dev-server');
13+
14+
const serverComponentTransactionPromise = waitForTransaction('nextjs-13-app-dir', transactionEvent => {
15+
return (
16+
transactionEvent?.contexts?.trace?.op === 'function.nextjs' &&
17+
transactionEvent?.transaction === 'Page Server Component (/server-component/parameter/[...parameters])'
18+
);
19+
});
20+
21+
await page.goto('/server-component/parameter/1337/42');
22+
23+
const transactionEvent = await serverComponentTransactionPromise;
24+
const transactionEventId = transactionEvent.event_id;
25+
26+
expect(transactionEvent.request?.headers).toBeDefined();
27+
28+
await expect
29+
.poll(
30+
async () => {
31+
try {
32+
const response = await axios.get(
33+
`https://sentry.io/api/0/projects/${sentryTestOrgSlug}/${sentryTestProject}/events/${transactionEventId}/`,
34+
{ headers: { Authorization: `Bearer ${authToken}` } },
35+
);
36+
37+
return response.status;
38+
} catch (e) {
39+
if (e instanceof AxiosError && e.response) {
40+
if (e.response.status !== 404) {
41+
throw e;
42+
} else {
43+
return e.response.status;
44+
}
45+
} else {
46+
throw e;
47+
}
48+
}
49+
},
50+
{
51+
timeout: EVENT_POLLING_TIMEOUT,
52+
},
53+
)
54+
.toBe(200);
55+
});
56+
57+
test('Should not set an error status on a server component transaction when it redirects', async ({ page }) => {
58+
// TODO: Fix that this is flakey on dev server - might be an SDK bug
59+
test.skip(process.env.TEST_ENV === 'production', 'Flakey on dev-server');
60+
61+
const serverComponentTransactionPromise = waitForTransaction('nextjs-13-app-dir', async transactionEvent => {
62+
return transactionEvent?.transaction === 'Page Server Component (/server-component/redirect)';
63+
});
64+
65+
await page.goto('/server-component/redirect');
66+
67+
expect((await serverComponentTransactionPromise).contexts?.trace?.status).not.toBe('internal_error');
68+
});
69+
70+
test('Should set a "not_found" status on a server component transaction when notFound() is called', async ({
71+
page,
72+
}) => {
73+
// TODO: Fix that this is flakey on dev server - might be an SDK bug
74+
test.skip(process.env.TEST_ENV === 'production', 'Flakey on dev-server');
75+
76+
const serverComponentTransactionPromise = waitForTransaction('nextjs-13-app-dir', async transactionEvent => {
77+
return transactionEvent?.transaction === 'Page Server Component (/server-component/not-found)';
78+
});
79+
80+
await page.goto('/server-component/not-found');
81+
82+
expect((await serverComponentTransactionPromise).contexts?.trace?.status).toBe('not_found');
83+
});
84+
85+
test('Should capture an error and transaction with correct status for a faulty server component', async ({ page }) => {
86+
const transactionEventPromise = waitForTransaction('nextjs-13-app-dir', async transactionEvent => {
87+
return transactionEvent?.transaction === 'Page Server Component (/server-component/faulty)';
88+
});
89+
90+
const errorEventPromise = waitForError('nextjs-13-app-dir', errorEvent => {
91+
return errorEvent?.exception?.values?.[0]?.value === 'I am a faulty server component';
92+
});
93+
94+
await page.goto('/server-component/faulty');
95+
96+
const transactionEvent = await transactionEventPromise;
97+
const errorEvent = await errorEventPromise;
98+
99+
expect(transactionEvent.contexts?.trace?.status).toBe('internal_error');
100+
101+
expect(errorEvent.tags?.['my-isolated-tag']).toBe(true);
102+
expect(errorEvent.tags?.['my-global-scope-isolated-tag']).not.toBeDefined();
103+
expect(transactionEvent.tags?.['my-isolated-tag']).toBe(true);
104+
expect(transactionEvent.tags?.['my-global-scope-isolated-tag']).not.toBeDefined();
105+
});

0 commit comments

Comments
 (0)