Skip to content

Commit 7cd3590

Browse files
chargomeclaude
andauthored
test(e2e): Restore scope isolation coverage on spans under span streaming (#24055)
Restores the isolation-scope assertions the span streaming port dropped from the Next.js 16 middleware specs by having the test apps report isolation state as span attributes instead of scope tags. Fixes #23941 Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
1 parent 212ddeb commit 7cd3590

7 files changed

Lines changed: 66 additions & 0 deletions

File tree

‎dev-packages/e2e-tests/test-applications/nextjs-16-bun/proxy.ts‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,15 @@ export async function proxy(request: NextRequest) {
77
Sentry.setTag('my-isolated-tag', true);
88
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
99

10+
// Streamed spans carry no scope tags, so the tests read the isolation state from this attribute instead
11+
const activeSpan = Sentry.getActiveSpan();
12+
if (activeSpan) {
13+
Sentry.getRootSpan(activeSpan).setAttribute(
14+
'isolation_scope.is_default',
15+
Sentry.getIsolationScope() === getDefaultIsolationScope(),
16+
);
17+
}
18+
1019
if (request.headers.has('x-should-throw')) {
1120
throw new Error('Middleware Error');
1221
}

‎dev-packages/e2e-tests/test-applications/nextjs-16-bun/tests/middleware.test.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,7 @@ test('Should create a span for middleware', async ({ request }) => {
1414
expect(middlewareSpan.status).toBe('ok');
1515
expect(getSpanOp(middlewareSpan)).toBe('middleware');
1616
expect(middlewareSpan.attributes['sentry.segment.name.source']?.value).toBe('route');
17+
18+
// Assert that isolation scope works properly
19+
expect(middlewareSpan.attributes['isolation_scope.is_default']).toEqual({ value: false, type: 'boolean' });
1720
});

‎dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/middleware.ts‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,15 @@ export async function middleware(request: NextRequest) {
77
Sentry.setTag('my-isolated-tag', true);
88
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
99

10+
// Streamed spans carry no scope tags, so the tests read the isolation state from this attribute instead
11+
const activeSpan = Sentry.getActiveSpan();
12+
if (activeSpan) {
13+
Sentry.getRootSpan(activeSpan).setAttribute(
14+
'isolation_scope.is_default',
15+
Sentry.getIsolationScope() === getDefaultIsolationScope(),
16+
);
17+
}
18+
1019
if (request.headers.has('x-should-throw')) {
1120
throw new Error('Middleware Error');
1221
}

‎dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/middleware.test.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ test.skip('Should create a span for middleware', async ({ request }) => {
3030
expect(middlewareSpan.status).toBe('ok');
3131
expect(getSpanOp(middlewareSpan)).toBe('middleware');
3232
expect(middlewareSpan.attributes['sentry.segment.name.source']?.value).toBe('route');
33+
34+
// Assert that isolation scope works properly
35+
// expect(middlewareSpan.attributes['isolation_scope.is_default']).toEqual({ value: false, type: 'boolean' });
3336
});
3437

3538
// TODO: Middleware tests need SDK adjustments for Cloudflare Workers edge runtime
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,17 @@
1+
import { getDefaultIsolationScope } from '@sentry/core';
2+
import * as Sentry from '@sentry/nextjs';
3+
14
export function GET() {
5+
// Streamed spans carry no scope tags, so the middleware test reads from these attributes whether the
6+
// isolation scope marked in `proxy.ts` leaked into this request
7+
const activeSpan = Sentry.getActiveSpan();
8+
if (activeSpan) {
9+
const isolationScope = Sentry.getIsolationScope();
10+
Sentry.getRootSpan(activeSpan).setAttributes({
11+
'isolation_scope.is_default': isolationScope === getDefaultIsolationScope(),
12+
'isolation_scope.has_proxy_marker': 'proxy-marker' in isolationScope.getScopeData().contexts,
13+
});
14+
}
15+
216
return Response.json({ name: 'John Doe' });
317
}

‎dev-packages/e2e-tests/test-applications/nextjs-16/proxy.ts‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,19 @@ export async function proxy(request: NextRequest) {
77
Sentry.setTag('my-isolated-tag', true);
88
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
99

10+
const isolationScope = Sentry.getIsolationScope();
11+
// Marks this request's isolation scope so the route handler behind the proxy can check it did not leak there
12+
isolationScope.setContext('proxy-marker', { set: true });
13+
14+
// Streamed spans carry no scope tags, so the tests read the isolation state from these attributes instead
15+
const activeSpan = Sentry.getActiveSpan();
16+
if (activeSpan) {
17+
Sentry.getRootSpan(activeSpan).setAttributes({
18+
'isolation_scope.is_default': isolationScope === getDefaultIsolationScope(),
19+
'isolation_scope.has_proxy_marker': 'proxy-marker' in isolationScope.getScopeData().contexts,
20+
});
21+
}
22+
1023
if (request.headers.has('x-should-throw')) {
1124
throw new Error('Middleware Error');
1225
}

‎dev-packages/e2e-tests/test-applications/nextjs-16/tests/middleware.test.ts‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ test('Should create a span for middleware', async ({ request }) => {
77
spans.some(span => span.name === 'middleware GET' && span.is_segment),
88
);
99

10+
const routeSpanPromise = waitForStreamedSpan('nextjs-16', span => {
11+
return span.name === 'GET /api/endpoint-behind-middleware' && span.is_segment;
12+
});
13+
1014
const response = await request.get('/api/endpoint-behind-middleware');
1115
expect(await response.json()).toStrictEqual({ name: 'John Doe' });
1216

@@ -24,6 +28,17 @@ test('Should create a span for middleware', async ({ request }) => {
2428
// `wrapMiddlewareWithSentry` wrapper used to start a second, redundant one nested inside it.
2529
const nestedMiddlewareSpans = spans.filter(span => getSpanOp(span) === 'middleware' && !span.is_segment);
2630
expect(nestedMiddlewareSpans).toHaveLength(0);
31+
32+
// Assert that isolation scope works properly
33+
expect(middlewareSpan.attributes['isolation_scope.is_default']).toEqual({ value: false, type: 'boolean' });
34+
expect(middlewareSpan.attributes['isolation_scope.has_proxy_marker']).toEqual({ value: true, type: 'boolean' });
35+
36+
// Scope data set in middleware must not leak into other requests (e.g. via a shared scope when the middleware
37+
// runs in a detached context - https://github.com/vercel/next.js/pull/95306). The route handler exposes it
38+
// via the same attributes.
39+
const routeSpan = await routeSpanPromise;
40+
expect(routeSpan.attributes['isolation_scope.is_default']).toEqual({ value: false, type: 'boolean' });
41+
expect(routeSpan.attributes['isolation_scope.has_proxy_marker']).toEqual({ value: false, type: 'boolean' });
2742
});
2843

2944
test('Faulty middlewares', async ({ request }) => {

0 commit comments

Comments
 (0)