diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-bun/proxy.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-bun/proxy.ts index 60722f329fa0..e2d263c4d80c 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-bun/proxy.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-bun/proxy.ts @@ -7,6 +7,15 @@ export async function proxy(request: NextRequest) { Sentry.setTag('my-isolated-tag', true); 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 + // Streamed spans carry no scope tags, so the tests read the isolation state from this attribute instead + const activeSpan = Sentry.getActiveSpan(); + if (activeSpan) { + Sentry.getRootSpan(activeSpan).setAttribute( + 'isolation_scope.is_default', + Sentry.getIsolationScope() === getDefaultIsolationScope(), + ); + } + if (request.headers.has('x-should-throw')) { throw new Error('Middleware Error'); } diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-bun/tests/middleware.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-bun/tests/middleware.test.ts index 068c5454176e..2b56d36de0ce 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-bun/tests/middleware.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-bun/tests/middleware.test.ts @@ -14,4 +14,7 @@ test('Should create a span for middleware', async ({ request }) => { expect(middlewareSpan.status).toBe('ok'); expect(getSpanOp(middlewareSpan)).toBe('middleware'); expect(middlewareSpan.attributes['sentry.segment.name.source']?.value).toBe('route'); + + // Assert that isolation scope works properly + expect(middlewareSpan.attributes['isolation_scope.is_default']).toEqual({ value: false, type: 'boolean' }); }); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/middleware.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/middleware.ts index f5980e4231c1..a6d31cc8c66f 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/middleware.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/middleware.ts @@ -7,6 +7,15 @@ export async function middleware(request: NextRequest) { Sentry.setTag('my-isolated-tag', true); 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 + // Streamed spans carry no scope tags, so the tests read the isolation state from this attribute instead + const activeSpan = Sentry.getActiveSpan(); + if (activeSpan) { + Sentry.getRootSpan(activeSpan).setAttribute( + 'isolation_scope.is_default', + Sentry.getIsolationScope() === getDefaultIsolationScope(), + ); + } + if (request.headers.has('x-should-throw')) { throw new Error('Middleware Error'); } diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/middleware.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/middleware.test.ts index c677115cd682..78b27c8427d9 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/middleware.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/middleware.test.ts @@ -30,6 +30,9 @@ test.skip('Should create a span for middleware', async ({ request }) => { expect(middlewareSpan.status).toBe('ok'); expect(getSpanOp(middlewareSpan)).toBe('middleware'); expect(middlewareSpan.attributes['sentry.segment.name.source']?.value).toBe('route'); + + // Assert that isolation scope works properly + // expect(middlewareSpan.attributes['isolation_scope.is_default']).toEqual({ value: false, type: 'boolean' }); }); // TODO: Middleware tests need SDK adjustments for Cloudflare Workers edge runtime diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16/app/api/endpoint-behind-middleware/route.ts b/dev-packages/e2e-tests/test-applications/nextjs-16/app/api/endpoint-behind-middleware/route.ts index 2733cc918f44..e2e27470d279 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16/app/api/endpoint-behind-middleware/route.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-16/app/api/endpoint-behind-middleware/route.ts @@ -1,3 +1,17 @@ +import { getDefaultIsolationScope } from '@sentry/core'; +import * as Sentry from '@sentry/nextjs'; + export function GET() { + // Streamed spans carry no scope tags, so the middleware test reads from these attributes whether the + // isolation scope marked in `proxy.ts` leaked into this request + const activeSpan = Sentry.getActiveSpan(); + if (activeSpan) { + const isolationScope = Sentry.getIsolationScope(); + Sentry.getRootSpan(activeSpan).setAttributes({ + 'isolation_scope.is_default': isolationScope === getDefaultIsolationScope(), + 'isolation_scope.has_proxy_marker': 'proxy-marker' in isolationScope.getScopeData().contexts, + }); + } + return Response.json({ name: 'John Doe' }); } diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16/proxy.ts b/dev-packages/e2e-tests/test-applications/nextjs-16/proxy.ts index 60722f329fa0..2daf05c0c50d 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16/proxy.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-16/proxy.ts @@ -7,6 +7,19 @@ export async function proxy(request: NextRequest) { Sentry.setTag('my-isolated-tag', true); 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 + const isolationScope = Sentry.getIsolationScope(); + // Marks this request's isolation scope so the route handler behind the proxy can check it did not leak there + isolationScope.setContext('proxy-marker', { set: true }); + + // Streamed spans carry no scope tags, so the tests read the isolation state from these attributes instead + const activeSpan = Sentry.getActiveSpan(); + if (activeSpan) { + Sentry.getRootSpan(activeSpan).setAttributes({ + 'isolation_scope.is_default': isolationScope === getDefaultIsolationScope(), + 'isolation_scope.has_proxy_marker': 'proxy-marker' in isolationScope.getScopeData().contexts, + }); + } + if (request.headers.has('x-should-throw')) { throw new Error('Middleware Error'); } diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16/tests/middleware.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-16/tests/middleware.test.ts index 9a0214980632..765d3961238f 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16/tests/middleware.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-16/tests/middleware.test.ts @@ -7,6 +7,10 @@ test('Should create a span for middleware', async ({ request }) => { spans.some(span => span.name === 'middleware GET' && span.is_segment), ); + const routeSpanPromise = waitForStreamedSpan('nextjs-16', span => { + return span.name === 'GET /api/endpoint-behind-middleware' && span.is_segment; + }); + const response = await request.get('/api/endpoint-behind-middleware'); expect(await response.json()).toStrictEqual({ name: 'John Doe' }); @@ -24,6 +28,17 @@ test('Should create a span for middleware', async ({ request }) => { // `wrapMiddlewareWithSentry` wrapper used to start a second, redundant one nested inside it. const nestedMiddlewareSpans = spans.filter(span => getSpanOp(span) === 'middleware' && !span.is_segment); expect(nestedMiddlewareSpans).toHaveLength(0); + + // Assert that isolation scope works properly + expect(middlewareSpan.attributes['isolation_scope.is_default']).toEqual({ value: false, type: 'boolean' }); + expect(middlewareSpan.attributes['isolation_scope.has_proxy_marker']).toEqual({ value: true, type: 'boolean' }); + + // Scope data set in middleware must not leak into other requests (e.g. via a shared scope when the middleware + // runs in a detached context - https://github.com/vercel/next.js/pull/95306). The route handler exposes it + // via the same attributes. + const routeSpan = await routeSpanPromise; + expect(routeSpan.attributes['isolation_scope.is_default']).toEqual({ value: false, type: 'boolean' }); + expect(routeSpan.attributes['isolation_scope.has_proxy_marker']).toEqual({ value: false, type: 'boolean' }); }); test('Faulty middlewares', async ({ request }) => {