diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/middleware.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/middleware.ts
index 6096fcfb1493..abc565f438b4 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/middleware.ts
+++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/middleware.ts
@@ -20,5 +20,5 @@ export async function middleware(request: NextRequest) {
// See "Matching Paths" below to learn more
export const config = {
- matcher: ['/api/endpoint-behind-middleware'],
+ matcher: ['/api/endpoint-behind-middleware', '/api/endpoint-behind-faulty-middleware'],
};
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/next-env.d.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/next-env.d.ts
index fd36f9494e2c..725dd6f24515 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/next-env.d.ts
+++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/next-env.d.ts
@@ -3,4 +3,4 @@
///
// NOTE: This file should not be edited
-// see https://nextjs.org/docs/basic-features/typescript for more information.
+// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/pages/api/async-context-edge-endpoint.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/pages/api/async-context-edge-endpoint.ts
index 6dc023fdf1ed..d6a129f9e056 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/pages/api/async-context-edge-endpoint.ts
+++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/pages/api/async-context-edge-endpoint.ts
@@ -7,21 +7,22 @@ export const config = {
export default async function handler() {
// Without a working async context strategy the two spans created by `Sentry.startSpan()` would be nested.
- const outerSpanPromise = Sentry.withIsolationScope(() => {
- return Sentry.startSpan({ name: 'outer-span' }, () => {
- return new Promise(resolve => setTimeout(resolve, 300));
- });
+ const outerSpanPromise = Sentry.startSpan({ name: 'outer-span' }, () => {
+ return new Promise(resolve => setTimeout(resolve, 300));
});
- setTimeout(() => {
- Sentry.withIsolationScope(() => {
- return Sentry.startSpan({ name: 'inner-span' }, () => {
+ const innerSpanPromise = new Promise(resolve => {
+ setTimeout(() => {
+ Sentry.startSpan({ name: 'inner-span' }, () => {
return new Promise(resolve => setTimeout(resolve, 100));
+ }).then(() => {
+ resolve();
});
- });
- }, 100);
+ }, 100);
+ });
await outerSpanPromise;
+ await innerSpanPromise;
return new Response('ok', { status: 200 });
}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/pages/api/endpoint-behind-faulty-middleware.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/pages/api/endpoint-behind-faulty-middleware.ts
new file mode 100644
index 000000000000..2ca75a33ba7e
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/pages/api/endpoint-behind-faulty-middleware.ts
@@ -0,0 +1,9 @@
+import type { NextApiRequest, NextApiResponse } from 'next';
+
+type Data = {
+ name: string;
+};
+
+export default function handler(req: NextApiRequest, res: NextApiResponse) {
+ res.status(200).json({ name: 'John Doe' });
+}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/async-context-edge.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/async-context-edge.test.ts
index ecce719f0656..cb92cb2bab49 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/async-context-edge.test.ts
+++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/async-context-edge.test.ts
@@ -3,7 +3,10 @@ import { waitForTransaction } from '@sentry-internal/test-utils';
test('Should allow for async context isolation in the edge SDK', async ({ request }) => {
const edgerouteTransactionPromise = waitForTransaction('nextjs-app-dir', async transactionEvent => {
- return transactionEvent?.transaction === 'GET /api/async-context-edge-endpoint';
+ return (
+ transactionEvent?.transaction === 'GET /api/async-context-edge-endpoint' &&
+ transactionEvent.contexts?.runtime?.name === 'vercel-edge'
+ );
});
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
const outerSpan = asyncContextEdgerouteTransaction.spans?.find(span => span.description === 'outer-span');
const innerSpan = asyncContextEdgerouteTransaction.spans?.find(span => span.description === 'inner-span');
- // @ts-expect-error parent_span_id exists
- expect(outerSpan?.parent_span_id).toStrictEqual(asyncContextEdgerouteTransaction.contexts?.trace?.span_id);
- // @ts-expect-error parent_span_id exists
- expect(innerSpan?.parent_span_id).toStrictEqual(asyncContextEdgerouteTransaction.contexts?.trace?.span_id);
+ expect(outerSpan?.parent_span_id).toStrictEqual(innerSpan?.parent_span_id);
});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/edge-route.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/edge-route.test.ts
index 810e76eaa690..6233688317e4 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/edge-route.test.ts
+++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/edge-route.test.ts
@@ -5,7 +5,6 @@ test('Should create a transaction for edge routes', async ({ request }) => {
const edgerouteTransactionPromise = waitForTransaction('nextjs-app-dir', async transactionEvent => {
return (
transactionEvent?.transaction === 'GET /api/edge-endpoint' &&
- transactionEvent?.contexts?.trace?.status === 'ok' &&
transactionEvent.contexts?.runtime?.name === 'vercel-edge'
);
});
@@ -24,31 +23,11 @@ test('Should create a transaction for edge routes', async ({ request }) => {
expect(edgerouteTransaction.request?.headers?.['x-yeet']).toBe('test-value');
});
-test('Should create a transaction with error status for faulty edge routes', async ({ request }) => {
+test('Faulty edge routes', async ({ request }) => {
const edgerouteTransactionPromise = waitForTransaction('nextjs-app-dir', async transactionEvent => {
- return (
- transactionEvent?.transaction === 'GET /api/error-edge-endpoint' &&
- transactionEvent?.contexts?.trace?.status === 'unknown_error'
- );
+ return transactionEvent?.transaction === 'GET /api/error-edge-endpoint';
});
- request.get('/api/error-edge-endpoint').catch(() => {
- // Noop
- });
-
- const edgerouteTransaction = await edgerouteTransactionPromise;
-
- expect(edgerouteTransaction.contexts?.trace?.status).toBe('unknown_error');
- expect(edgerouteTransaction.contexts?.trace?.op).toBe('http.server');
- expect(edgerouteTransaction.contexts?.runtime?.name).toBe('vercel-edge');
-
- // Assert that isolation scope works properly
- expect(edgerouteTransaction.tags?.['my-isolated-tag']).toBe(true);
- expect(edgerouteTransaction.tags?.['my-global-scope-isolated-tag']).not.toBeDefined();
-});
-
-// TODO(lforst): This cannot make it into production - Make sure to fix this test
-test.skip('Should record exceptions for faulty edge routes', async ({ request }) => {
const errorEventPromise = waitForError('nextjs-app-dir', errorEvent => {
return errorEvent?.exception?.values?.[0]?.value === 'Edge Route Error';
});
@@ -57,11 +36,21 @@ test.skip('Should record exceptions for faulty edge routes', async ({ request })
// Noop
});
- const errorEvent = await errorEventPromise;
+ const [edgerouteTransaction, errorEvent] = await Promise.all([
+ test.step('should create a transaction', () => edgerouteTransactionPromise),
+ test.step('should create an error event', () => errorEventPromise),
+ ]);
- // Assert that isolation scope works properly
- expect(errorEvent.tags?.['my-isolated-tag']).toBe(true);
- expect(errorEvent.tags?.['my-global-scope-isolated-tag']).not.toBeDefined();
+ test.step('should create transactions with the right fields', () => {
+ expect(edgerouteTransaction.contexts?.trace?.status).toBe('unknown_error');
+ expect(edgerouteTransaction.contexts?.trace?.op).toBe('http.server');
+ expect(edgerouteTransaction.contexts?.runtime?.name).toBe('vercel-edge');
+ });
- expect(errorEvent.transaction).toBe('GET /api/error-edge-endpoint');
+ test.step('should have scope isolation', () => {
+ expect(edgerouteTransaction.tags?.['my-isolated-tag']).toBe(true);
+ expect(edgerouteTransaction.tags?.['my-global-scope-isolated-tag']).not.toBeDefined();
+ expect(errorEvent.tags?.['my-isolated-tag']).toBe(true);
+ expect(errorEvent.tags?.['my-global-scope-isolated-tag']).not.toBeDefined();
+ });
});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/edge.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/edge.test.ts
index de4e2f45ed37..a34d415ee4bf 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/edge.test.ts
+++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/edge.test.ts
@@ -19,17 +19,19 @@ test('Should record exceptions for faulty edge server components', async ({ page
expect(errorEvent.transaction).toBe(`Page Server Component (/edge-server-components/error)`);
});
-// TODO(lforst): This test skip cannot make it into production - make sure to fix this test before merging into develop branch
+// TODO(lforst): Fix this test
test.skip('Should record transaction for edge server components', async ({ page }) => {
const serverComponentTransactionPromise = waitForTransaction('nextjs-app-dir', async transactionEvent => {
- return transactionEvent?.transaction === 'GET /edge-server-components';
+ return (
+ transactionEvent?.transaction === 'GET /edge-server-components' &&
+ transactionEvent.contexts?.runtime?.name === 'vercel-edge'
+ );
});
await page.goto('/edge-server-components');
const serverComponentTransaction = await serverComponentTransactionPromise;
- expect(serverComponentTransaction).toBe(1);
expect(serverComponentTransaction).toBeDefined();
expect(serverComponentTransaction.request?.headers).toBeDefined();
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/middleware.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/middleware.test.ts
index 2fb31bba13a7..5501f9a13b33 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/middleware.test.ts
+++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/middleware.test.ts
@@ -3,7 +3,7 @@ import { waitForError, waitForTransaction } from '@sentry-internal/test-utils';
test('Should create a transaction for middleware', async ({ request }) => {
const middlewareTransactionPromise = waitForTransaction('nextjs-app-dir', async transactionEvent => {
- return transactionEvent?.transaction === 'middleware' && transactionEvent?.contexts?.trace?.status === 'ok';
+ return transactionEvent?.transaction === 'middleware GET /api/endpoint-behind-middleware';
});
const response = await request.get('/api/endpoint-behind-middleware');
@@ -12,7 +12,7 @@ test('Should create a transaction for middleware', async ({ request }) => {
const middlewareTransaction = await middlewareTransactionPromise;
expect(middlewareTransaction.contexts?.trace?.status).toBe('ok');
- expect(middlewareTransaction.contexts?.trace?.op).toBe('middleware.nextjs');
+ expect(middlewareTransaction.contexts?.trace?.op).toBe('http.server.middleware');
expect(middlewareTransaction.contexts?.runtime?.name).toBe('vercel-edge');
// Assert that isolation scope works properly
@@ -20,46 +20,40 @@ test('Should create a transaction for middleware', async ({ request }) => {
expect(middlewareTransaction.tags?.['my-global-scope-isolated-tag']).not.toBeDefined();
});
-test('Should create a transaction with error status for faulty middleware', async ({ request }) => {
+test('Faulty middlewares', async ({ request }) => {
const middlewareTransactionPromise = waitForTransaction('nextjs-app-dir', async transactionEvent => {
- return (
- transactionEvent?.transaction === 'middleware' && transactionEvent?.contexts?.trace?.status === 'unknown_error'
- );
+ return transactionEvent?.transaction === 'middleware GET /api/endpoint-behind-faulty-middleware';
});
- request.get('/api/endpoint-behind-middleware', { headers: { 'x-should-throw': '1' } }).catch(() => {
- // Noop
- });
-
- const middlewareTransaction = await middlewareTransactionPromise;
-
- expect(middlewareTransaction.contexts?.trace?.status).toBe('unknown_error');
- expect(middlewareTransaction.contexts?.trace?.op).toBe('middleware.nextjs');
- expect(middlewareTransaction.contexts?.runtime?.name).toBe('vercel-edge');
-});
-
-// TODO(lforst): This cannot make it into production - Make sure to fix this test
-test.skip('Records exceptions happening in middleware', async ({ request }) => {
const errorEventPromise = waitForError('nextjs-app-dir', errorEvent => {
return errorEvent?.exception?.values?.[0]?.value === 'Middleware Error';
});
- request.get('/api/endpoint-behind-middleware', { headers: { 'x-should-throw': '1' } }).catch(() => {
+ request.get('/api/endpoint-behind-faulty-middleware', { headers: { 'x-should-throw': '1' } }).catch(() => {
// Noop
});
- const errorEvent = await errorEventPromise;
+ await test.step('should record transactions', async () => {
+ const middlewareTransaction = await middlewareTransactionPromise;
+ expect(middlewareTransaction.contexts?.trace?.status).toBe('unknown_error');
+ expect(middlewareTransaction.contexts?.trace?.op).toBe('http.server.middleware');
+ expect(middlewareTransaction.contexts?.runtime?.name).toBe('vercel-edge');
+ });
- // Assert that isolation scope works properly
- expect(errorEvent.tags?.['my-isolated-tag']).toBe(true);
- expect(errorEvent.tags?.['my-global-scope-isolated-tag']).not.toBeDefined();
- expect(errorEvent.transaction).toBe('middleware');
+ await test.step('should record exceptions', async () => {
+ const errorEvent = await errorEventPromise;
+
+ // Assert that isolation scope works properly
+ expect(errorEvent.tags?.['my-isolated-tag']).toBe(true);
+ expect(errorEvent.tags?.['my-global-scope-isolated-tag']).not.toBeDefined();
+ expect(errorEvent.transaction).toBe('middleware GET /api/endpoint-behind-faulty-middleware');
+ });
});
test('Should trace outgoing fetch requests inside middleware and create breadcrumbs for it', async ({ request }) => {
const middlewareTransactionPromise = waitForTransaction('nextjs-app-dir', async transactionEvent => {
return (
- transactionEvent?.transaction === 'middleware' &&
+ transactionEvent?.transaction === 'middleware GET /api/endpoint-behind-middleware' &&
!!transactionEvent.spans?.find(span => span.op === 'http.client')
);
});
diff --git a/packages/nextjs/src/common/utils/edgeWrapperUtils.ts b/packages/nextjs/src/common/utils/edgeWrapperUtils.ts
deleted file mode 100644
index 5eed59aca0a3..000000000000
--- a/packages/nextjs/src/common/utils/edgeWrapperUtils.ts
+++ /dev/null
@@ -1,91 +0,0 @@
-import {
- SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
- SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
- SPAN_STATUS_OK,
- captureException,
- continueTrace,
- handleCallbackErrors,
- setHttpStatus,
- startSpan,
- withIsolationScope,
-} from '@sentry/core';
-import { vercelWaitUntil, winterCGRequestToRequestData } from '@sentry/utils';
-
-import type { EdgeRouteHandler } from '../../edge/types';
-import { flushSafelyWithTimeout } from './responseEnd';
-import { commonObjectToIsolationScope, escapeNextjsTracing } from './tracingUtils';
-
-/**
- * Wraps a function on the edge runtime with error and performance monitoring.
- */
-export function withEdgeWrapping(
- handler: H,
- options: { spanDescription: string; spanOp: string; mechanismFunctionName: string },
-): (...params: Parameters) => Promise> {
- return async function (this: unknown, ...args) {
- return escapeNextjsTracing(() => {
- const req: unknown = args[0];
- return withIsolationScope(commonObjectToIsolationScope(req), isolationScope => {
- let sentryTrace;
- let baggage;
-
- if (req instanceof Request) {
- sentryTrace = req.headers.get('sentry-trace') || '';
- baggage = req.headers.get('baggage');
-
- isolationScope.setSDKProcessingMetadata({
- request: winterCGRequestToRequestData(req),
- });
- }
-
- isolationScope.setTransactionName(options.spanDescription);
-
- return continueTrace(
- {
- sentryTrace,
- baggage,
- },
- () => {
- return startSpan(
- {
- name: options.spanDescription,
- op: options.spanOp,
- forceTransaction: true,
- attributes: {
- [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route',
- [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.nextjs.withEdgeWrapping',
- },
- },
- async span => {
- const handlerResult = await handleCallbackErrors(
- () => handler.apply(this, args),
- error => {
- captureException(error, {
- mechanism: {
- type: 'instrument',
- handled: false,
- data: {
- function: options.mechanismFunctionName,
- },
- },
- });
- },
- );
-
- if (handlerResult instanceof Response) {
- setHttpStatus(span, handlerResult.status);
- } else {
- span.setStatus({ code: SPAN_STATUS_OK });
- }
-
- return handlerResult;
- },
- );
- },
- ).finally(() => {
- vercelWaitUntil(flushSafelyWithTimeout());
- });
- });
- });
- };
-}
diff --git a/packages/nextjs/src/common/wrapMiddlewareWithSentry.ts b/packages/nextjs/src/common/wrapMiddlewareWithSentry.ts
index 66cbbb046300..9f0903e86984 100644
--- a/packages/nextjs/src/common/wrapMiddlewareWithSentry.ts
+++ b/packages/nextjs/src/common/wrapMiddlewareWithSentry.ts
@@ -1,5 +1,19 @@
+import {
+ SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
+ SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
+ captureException,
+ getActiveSpan,
+ getCurrentScope,
+ getRootSpan,
+ handleCallbackErrors,
+ setCapturedScopesOnSpan,
+ startSpan,
+ withIsolationScope,
+} from '@sentry/core';
+import type { TransactionSource } from '@sentry/types';
+import { vercelWaitUntil, winterCGRequestToRequestData } from '@sentry/utils';
import type { EdgeRouteHandler } from '../edge/types';
-import { withEdgeWrapping } from './utils/edgeWrapperUtils';
+import { flushSafelyWithTimeout } from './utils/responseEnd';
/**
* Wraps Next.js middleware with Sentry error and performance instrumentation.
@@ -11,12 +25,69 @@ export function wrapMiddlewareWithSentry(
middleware: H,
): (...params: Parameters) => Promise> {
return new Proxy(middleware, {
- apply: (wrappingTarget, thisArg, args: Parameters) => {
- return withEdgeWrapping(wrappingTarget, {
- spanDescription: 'middleware',
- spanOp: 'middleware.nextjs',
- mechanismFunctionName: 'withSentryMiddleware',
- }).apply(thisArg, args);
+ apply: async (wrappingTarget, thisArg, args: Parameters) => {
+ // TODO: We still should add central isolation scope creation for when our build-time instrumentation does not work anymore with turbopack.
+ return withIsolationScope(isolationScope => {
+ const req: unknown = args[0];
+ const currentScope = getCurrentScope();
+
+ let spanName: string;
+ let spanOrigin: TransactionSource;
+
+ if (req instanceof Request) {
+ isolationScope.setSDKProcessingMetadata({
+ request: winterCGRequestToRequestData(req),
+ });
+ spanName = `middleware ${req.method} ${new URL(req.url).pathname}`;
+ spanOrigin = 'url';
+ } else {
+ spanName = 'middleware';
+ spanOrigin = 'component';
+ }
+
+ currentScope.setTransactionName(spanName);
+
+ const activeSpan = getActiveSpan();
+
+ if (activeSpan) {
+ // If there is an active span, it likely means that the automatic Next.js OTEL instrumentation worked and we can
+ // rely on that for parameterization.
+ spanName = 'middleware';
+ spanOrigin = 'component';
+
+ const rootSpan = getRootSpan(activeSpan);
+ if (rootSpan) {
+ setCapturedScopesOnSpan(rootSpan, currentScope, isolationScope);
+ }
+ }
+
+ return startSpan(
+ {
+ name: spanName,
+ op: 'http.server.middleware',
+ attributes: {
+ [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: spanOrigin,
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.nextjs.wrapMiddlewareWithSentry',
+ },
+ },
+ () => {
+ return handleCallbackErrors(
+ () => wrappingTarget.apply(thisArg, args),
+ error => {
+ captureException(error, {
+ mechanism: {
+ type: 'instrument',
+ handled: false,
+ },
+ });
+ },
+ () => {
+ vercelWaitUntil(flushSafelyWithTimeout());
+ },
+ );
+ },
+ );
+ });
},
});
}
diff --git a/packages/nextjs/src/edge/index.ts b/packages/nextjs/src/edge/index.ts
index cf0e571e67cb..0a63118ada46 100644
--- a/packages/nextjs/src/edge/index.ts
+++ b/packages/nextjs/src/edge/index.ts
@@ -1,20 +1,17 @@
-import { context } from '@opentelemetry/api';
import {
+ SEMANTIC_ATTRIBUTE_SENTRY_OP,
applySdkMetadata,
- getCapturedScopesOnSpan,
- getCurrentScope,
- getIsolationScope,
getRootSpan,
registerSpanErrorInstrumentation,
- setCapturedScopesOnSpan,
+ spanToJSON,
} from '@sentry/core';
-import { GLOBAL_OBJ } from '@sentry/utils';
+import { GLOBAL_OBJ, vercelWaitUntil } from '@sentry/utils';
import type { VercelEdgeOptions } from '@sentry/vercel-edge';
import { getDefaultIntegrations, init as vercelEdgeInit } from '@sentry/vercel-edge';
-import { getScopesFromContext } from '@sentry/opentelemetry';
import { isBuild } from '../common/utils/isBuild';
+import { flushSafelyWithTimeout } from '../common/utils/responseEnd';
import { distDirRewriteFramesIntegration } from './distDirRewriteFramesIntegration';
export { captureUnderscoreErrorException } from '../common/pages-router-instrumentation/_error';
@@ -52,20 +49,18 @@ export function init(options: VercelEdgeOptions = {}): void {
const client = vercelEdgeInit(opts);
- // Create/fork an isolation whenever we create root spans. This is ok because in Next.js we only create root spans on the edge for incoming requests.
client?.on('spanStart', span => {
- if (span === getRootSpan(span)) {
- const scopes = getCapturedScopesOnSpan(span);
-
- const isolationScope = (scopes.isolationScope || getIsolationScope()).clone();
- const scope = scopes.scope || getCurrentScope();
+ const spanAttributes = spanToJSON(span).data;
- const currentScopesPointer = getScopesFromContext(context.active());
- if (currentScopesPointer) {
- currentScopesPointer.isolationScope = isolationScope;
- }
+ // Make sure middleware spans get the right op
+ if (spanAttributes?.['next.span_type'] === 'Middleware.execute') {
+ span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'http.server.middleware');
+ }
+ });
- setCapturedScopesOnSpan(span, scope, isolationScope);
+ client?.on('spanEnd', span => {
+ if (span === getRootSpan(span)) {
+ vercelWaitUntil(flushSafelyWithTimeout());
}
});
}
diff --git a/packages/nextjs/src/edge/wrapApiHandlerWithSentry.ts b/packages/nextjs/src/edge/wrapApiHandlerWithSentry.ts
index e5191ea27dbe..5c8ce043ecb8 100644
--- a/packages/nextjs/src/edge/wrapApiHandlerWithSentry.ts
+++ b/packages/nextjs/src/edge/wrapApiHandlerWithSentry.ts
@@ -1,4 +1,18 @@
-import { withEdgeWrapping } from '../common/utils/edgeWrapperUtils';
+import {
+ SEMANTIC_ATTRIBUTE_SENTRY_OP,
+ SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
+ SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
+ captureException,
+ getActiveSpan,
+ getCurrentScope,
+ getRootSpan,
+ handleCallbackErrors,
+ setCapturedScopesOnSpan,
+ startSpan,
+ withIsolationScope,
+} from '@sentry/core';
+import { vercelWaitUntil, winterCGRequestToRequestData } from '@sentry/utils';
+import { flushSafelyWithTimeout } from '../common/utils/responseEnd';
import type { EdgeRouteHandler } from './types';
/**
@@ -9,18 +23,76 @@ export function wrapApiHandlerWithSentry(
parameterizedRoute: string,
): (...params: Parameters) => Promise> {
return new Proxy(handler, {
- apply: (wrappingTarget, thisArg, args: Parameters) => {
- const req = args[0];
+ apply: async (wrappingTarget, thisArg, args: Parameters) => {
+ // TODO: We still should add central isolation scope creation for when our build-time instrumentation does not work anymore with turbopack.
- const wrappedHandler = withEdgeWrapping(wrappingTarget, {
- spanDescription: !(req instanceof Request)
- ? `handler (${parameterizedRoute})`
- : `${req.method} ${parameterizedRoute}`,
- spanOp: 'http.server',
- mechanismFunctionName: 'wrapApiHandlerWithSentry',
- });
+ return withIsolationScope(isolationScope => {
+ const req: unknown = args[0];
+ const currentScope = getCurrentScope();
+
+ if (req instanceof Request) {
+ isolationScope.setSDKProcessingMetadata({
+ request: winterCGRequestToRequestData(req),
+ });
+ currentScope.setTransactionName(`${req.method} ${parameterizedRoute}`);
+ } else {
+ currentScope.setTransactionName(`handler (${parameterizedRoute})`);
+ }
+
+ let spanName: string;
+ let op: string | undefined = 'http.server';
- return wrappedHandler.apply(thisArg, args);
+ // If there is an active span, it likely means that the automatic Next.js OTEL instrumentation worked and we can
+ // rely on that for parameterization.
+ const activeSpan = getActiveSpan();
+ if (activeSpan) {
+ spanName = `handler (${parameterizedRoute})`;
+ op = undefined;
+
+ const rootSpan = getRootSpan(activeSpan);
+ if (rootSpan) {
+ rootSpan.updateName(
+ req instanceof Request ? `${req.method} ${parameterizedRoute}` : `handler ${parameterizedRoute}`,
+ );
+ rootSpan.setAttributes({
+ [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.server',
+ [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route',
+ });
+ setCapturedScopesOnSpan(rootSpan, currentScope, isolationScope);
+ }
+ } else if (req instanceof Request) {
+ spanName = `${req.method} ${parameterizedRoute}`;
+ } else {
+ spanName = `handler ${parameterizedRoute}`;
+ }
+
+ return startSpan(
+ {
+ name: spanName,
+ op: op,
+ attributes: {
+ [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route',
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.nextjs.wrapApiHandlerWithSentry',
+ },
+ },
+ () => {
+ return handleCallbackErrors(
+ () => wrappingTarget.apply(thisArg, args),
+ error => {
+ captureException(error, {
+ mechanism: {
+ type: 'instrument',
+ handled: false,
+ },
+ });
+ },
+ () => {
+ vercelWaitUntil(flushSafelyWithTimeout());
+ },
+ );
+ },
+ );
+ });
},
});
}
diff --git a/packages/nextjs/test/edge/edgeWrapperUtils.test.ts b/packages/nextjs/test/edge/edgeWrapperUtils.test.ts
deleted file mode 100644
index 029ee9d97fce..000000000000
--- a/packages/nextjs/test/edge/edgeWrapperUtils.test.ts
+++ /dev/null
@@ -1,109 +0,0 @@
-import * as coreSdk from '@sentry/core';
-import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core';
-
-import { withEdgeWrapping } from '../../src/common/utils/edgeWrapperUtils';
-
-const origRequest = global.Request;
-const origResponse = global.Response;
-
-// @ts-expect-error Request does not exist on type Global
-global.Request = class Request {
- headers = {
- get() {
- return null;
- },
- };
-};
-
-// @ts-expect-error Response does not exist on type Global
-global.Response = class Request {};
-
-afterAll(() => {
- global.Request = origRequest;
- global.Response = origResponse;
-});
-
-beforeEach(() => {
- jest.clearAllMocks();
-});
-
-describe('withEdgeWrapping', () => {
- it('should return a function that calls the passed function', async () => {
- const origFunctionReturnValue = new Response();
- const origFunction = jest.fn(_req => origFunctionReturnValue);
-
- const wrappedFunction = withEdgeWrapping(origFunction, {
- spanDescription: 'some label',
- mechanismFunctionName: 'some name',
- spanOp: 'some op',
- });
-
- const returnValue = await wrappedFunction(new Request('https://sentry.io/'));
-
- expect(returnValue).toBe(origFunctionReturnValue);
- expect(origFunction).toHaveBeenCalledTimes(1);
- });
-
- it('should return a function that calls captureException on error', async () => {
- const captureExceptionSpy = jest.spyOn(coreSdk, 'captureException');
- const error = new Error();
- const origFunction = jest.fn(_req => {
- throw error;
- });
-
- const wrappedFunction = withEdgeWrapping(origFunction, {
- spanDescription: 'some label',
- mechanismFunctionName: 'some name',
- spanOp: 'some op',
- });
-
- await expect(wrappedFunction(new Request('https://sentry.io/'))).rejects.toBe(error);
- expect(captureExceptionSpy).toHaveBeenCalledTimes(1);
- });
-
- it('should return a function that calls trace', async () => {
- const startSpanSpy = jest.spyOn(coreSdk, 'startSpan');
-
- const request = new Request('https://sentry.io/');
- const origFunction = jest.fn(_req => new Response());
-
- const wrappedFunction = withEdgeWrapping(origFunction, {
- spanDescription: 'some label',
- mechanismFunctionName: 'some name',
- spanOp: 'some op',
- });
-
- await wrappedFunction(request);
-
- expect(startSpanSpy).toHaveBeenCalledTimes(1);
- expect(startSpanSpy).toHaveBeenCalledWith(
- expect.objectContaining({
- attributes: {
- [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route',
- [coreSdk.SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.nextjs.withEdgeWrapping',
- },
- name: 'some label',
- op: 'some op',
- }),
- expect.any(Function),
- );
-
- expect(coreSdk.getIsolationScope().getScopeData().sdkProcessingMetadata).toEqual({
- request: { headers: {} },
- });
- });
-
- it("should return a function that doesn't crash when req isn't passed", async () => {
- const origFunctionReturnValue = new Response();
- const origFunction = jest.fn(() => origFunctionReturnValue);
-
- const wrappedFunction = withEdgeWrapping(origFunction, {
- spanDescription: 'some label',
- mechanismFunctionName: 'some name',
- spanOp: 'some op',
- });
-
- await expect(wrappedFunction()).resolves.toBe(origFunctionReturnValue);
- expect(origFunction).toHaveBeenCalledTimes(1);
- });
-});
diff --git a/packages/nextjs/test/edge/withSentryAPI.test.ts b/packages/nextjs/test/edge/withSentryAPI.test.ts
index 6e24eca21bfe..11449da0e1ef 100644
--- a/packages/nextjs/test/edge/withSentryAPI.test.ts
+++ b/packages/nextjs/test/edge/withSentryAPI.test.ts
@@ -1,6 +1,3 @@
-import * as coreSdk from '@sentry/core';
-import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core';
-
import { wrapApiHandlerWithSentry } from '../../src/edge';
const origRequest = global.Request;
@@ -31,53 +28,16 @@ afterAll(() => {
global.Response = origResponse;
});
-const startSpanSpy = jest.spyOn(coreSdk, 'startSpan');
-
afterEach(() => {
jest.clearAllMocks();
});
describe('wrapApiHandlerWithSentry', () => {
- it('should return a function that calls trace', async () => {
- const request = new Request('https://sentry.io/');
- const origFunction = jest.fn(_req => new Response());
-
- const wrappedFunction = wrapApiHandlerWithSentry(origFunction, '/user/[userId]/post/[postId]');
-
- await wrappedFunction(request);
-
- expect(startSpanSpy).toHaveBeenCalledTimes(1);
- expect(startSpanSpy).toHaveBeenCalledWith(
- expect.objectContaining({
- attributes: {
- [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route',
- [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.nextjs.withEdgeWrapping',
- },
- name: 'POST /user/[userId]/post/[postId]',
- op: 'http.server',
- }),
- expect.any(Function),
- );
- });
-
- it('should return a function that calls trace without throwing when no request is passed', async () => {
+ it('should return a function that does not throw when no request is passed', async () => {
const origFunction = jest.fn(() => new Response());
const wrappedFunction = wrapApiHandlerWithSentry(origFunction, '/user/[userId]/post/[postId]');
await wrappedFunction();
-
- expect(startSpanSpy).toHaveBeenCalledTimes(1);
- expect(startSpanSpy).toHaveBeenCalledWith(
- expect.objectContaining({
- attributes: {
- [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route',
- [coreSdk.SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.nextjs.withEdgeWrapping',
- },
- name: 'handler (/user/[userId]/post/[postId])',
- op: 'http.server',
- }),
- expect.any(Function),
- );
});
});