diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/client-app-routing-instrumentation.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/client-app-routing-instrumentation.test.ts index 35984640bcf6..abfe9b323d0f 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/client-app-routing-instrumentation.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/client-app-routing-instrumentation.test.ts @@ -42,9 +42,7 @@ test('Creates a navigation transaction for app router routes', async ({ page }) // It seems to differ between Next.js versions whether the route is parameterized or not (transactionEvent?.transaction === 'GET /server-component/parameter/foo/bar/baz' || transactionEvent?.transaction === 'GET /server-component/parameter/[...parameters]') && - transactionEvent.contexts?.trace?.data?.['http.target'].startsWith('/server-component/parameter/foo/bar/baz') && - (await clientNavigationTransactionPromise).contexts?.trace?.trace_id === - transactionEvent.contexts?.trace?.trace_id + transactionEvent.contexts?.trace?.data?.['http.target'].startsWith('/server-component/parameter/foo/bar/baz') ); }); @@ -52,6 +50,10 @@ test('Creates a navigation transaction for app router routes', async ({ page }) expect(await clientNavigationTransactionPromise).toBeDefined(); expect(await serverComponentTransactionPromise).toBeDefined(); + + expect((await serverComponentTransactionPromise).contexts?.trace?.trace_id).toBe( + (await clientNavigationTransactionPromise).contexts?.trace?.trace_id, + ); }); test('Creates a navigation transaction for `router.push()`', async ({ page }) => { diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/route-handlers.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/route-handlers.test.ts index 3e8006ae21f8..90d71865dd3b 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/route-handlers.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/route-handlers.test.ts @@ -56,7 +56,7 @@ test('Should record exceptions and transactions for faulty route handlers', asyn expect(routehandlerTransaction.contexts?.trace?.status).toBe('internal_error'); expect(routehandlerTransaction.contexts?.trace?.op).toBe('http.server'); - expect(routehandlerTransaction.contexts?.trace?.origin).toContain('auto.http.otel.http'); + expect(routehandlerTransaction.contexts?.trace?.origin).toContain('auto'); expect(routehandlerError.exception?.values?.[0].value).toBe('route-handler-error'); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/server-components.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/server-components.test.ts index 49afe791328f..75f30075a47f 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/server-components.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/server-components.test.ts @@ -16,7 +16,7 @@ test('Sends a transaction for a request to app router', async ({ page }) => { expect(transactionEvent.contexts?.trace).toEqual({ data: expect.objectContaining({ 'sentry.op': 'http.server', - 'sentry.origin': 'auto.http.otel.http', + 'sentry.origin': 'auto', 'sentry.sample_rate': 1, 'sentry.source': 'route', 'http.method': 'GET', @@ -27,7 +27,7 @@ test('Sends a transaction for a request to app router', async ({ page }) => { 'otel.kind': 'SERVER', }), op: 'http.server', - origin: 'auto.http.otel.http', + origin: 'auto', span_id: expect.any(String), status: 'ok', trace_id: expect.any(String), diff --git a/packages/nextjs/src/common/span-attributes-with-logic-attached.ts b/packages/nextjs/src/common/span-attributes-with-logic-attached.ts index f6b5b46fdcac..593ea61b4e28 100644 --- a/packages/nextjs/src/common/span-attributes-with-logic-attached.ts +++ b/packages/nextjs/src/common/span-attributes-with-logic-attached.ts @@ -2,3 +2,5 @@ * If this attribute is attached to a transaction, the Next.js SDK will drop that transaction. */ export const TRANSACTION_ATTR_SHOULD_DROP_TRANSACTION = 'sentry.drop_transaction'; + +export const TRANSACTION_ATTR_SENTRY_TRACE_BACKFILL = 'sentry.sentry_trace_backfill'; diff --git a/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts b/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts index 5944b520f6ea..ec2636ce2aac 100644 --- a/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts +++ b/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts @@ -20,6 +20,7 @@ import { propagationContextFromHeaders, uuid4, winterCGHeadersToDict } from '@se import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import type { GenerationFunctionContext } from '../common/types'; import { isNotFoundNavigationError, isRedirectNavigationError } from './nextNavigationErrorUtils'; +import { TRANSACTION_ATTR_SENTRY_TRACE_BACKFILL } from './span-attributes-with-logic-attached'; import { commonObjectToIsolationScope, commonObjectToPropagationContext } from './utils/tracingUtils'; /** @@ -75,6 +76,15 @@ export function wrapGenerationFunctionWithSentry a }, }); + const activeSpan = getActiveSpan(); + if (activeSpan) { + const rootSpan = getRootSpan(activeSpan); + const sentryTrace = headersDict?.['sentry-trace']; + if (sentryTrace) { + rootSpan.setAttribute(TRANSACTION_ATTR_SENTRY_TRACE_BACKFILL, sentryTrace); + } + } + const propagationContext = commonObjectToPropagationContext( headers, headersDict?.['sentry-trace'] diff --git a/packages/nextjs/src/common/wrapServerComponentWithSentry.ts b/packages/nextjs/src/common/wrapServerComponentWithSentry.ts index 079722dad76d..8ea7fdededf2 100644 --- a/packages/nextjs/src/common/wrapServerComponentWithSentry.ts +++ b/packages/nextjs/src/common/wrapServerComponentWithSentry.ts @@ -18,6 +18,7 @@ import { propagationContextFromHeaders, uuid4, vercelWaitUntil, winterCGHeadersT import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import { isNotFoundNavigationError, isRedirectNavigationError } from '../common/nextNavigationErrorUtils'; import type { ServerComponentContext } from '../common/types'; +import { TRANSACTION_ATTR_SENTRY_TRACE_BACKFILL } from './span-attributes-with-logic-attached'; import { flushSafelyWithTimeout } from './utils/responseEnd'; import { commonObjectToIsolationScope, commonObjectToPropagationContext } from './utils/tracingUtils'; @@ -74,6 +75,15 @@ export function wrapServerComponentWithSentry any> scope.setPropagationContext(propagationContext); } + const activeSpan = getActiveSpan(); + if (activeSpan) { + const rootSpan = getRootSpan(activeSpan); + const sentryTrace = headersDict?.['sentry-trace']; + if (sentryTrace) { + rootSpan.setAttribute(TRANSACTION_ATTR_SENTRY_TRACE_BACKFILL, sentryTrace); + } + } + return startSpanManual( { op: 'function.nextjs', diff --git a/packages/nextjs/src/server/index.ts b/packages/nextjs/src/server/index.ts index 8e00a325f5f5..66fbfa1e4c4f 100644 --- a/packages/nextjs/src/server/index.ts +++ b/packages/nextjs/src/server/index.ts @@ -1,15 +1,16 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, applySdkMetadata, getClient, getGlobalScope, getRootSpan, spanToJSON, } from '@sentry/core'; -import { getDefaultIntegrations, init as nodeInit } from '@sentry/node'; +import { getDefaultIntegrations, httpIntegration, init as nodeInit } from '@sentry/node'; import type { NodeClient, NodeOptions } from '@sentry/node'; -import { GLOBAL_OBJ, logger } from '@sentry/utils'; +import { GLOBAL_OBJ, extractTraceparentData, logger, stripUrlQueryAndFragment } from '@sentry/utils'; import { ATTR_HTTP_REQUEST_METHOD, @@ -22,7 +23,10 @@ import type { EventProcessor } from '@sentry/types'; import { DEBUG_BUILD } from '../common/debug-build'; import { devErrorSymbolicationEventProcessor } from '../common/devErrorSymbolicationEventProcessor'; import { getVercelEnv } from '../common/getVercelEnv'; -import { TRANSACTION_ATTR_SHOULD_DROP_TRANSACTION } from '../common/span-attributes-with-logic-attached'; +import { + TRANSACTION_ATTR_SENTRY_TRACE_BACKFILL, + TRANSACTION_ATTR_SHOULD_DROP_TRANSACTION, +} from '../common/span-attributes-with-logic-attached'; import { isBuild } from '../common/utils/isBuild'; import { distDirRewriteFramesIntegration } from './distDirRewriteFramesIntegration'; @@ -99,7 +103,18 @@ export function init(options: NodeOptions): NodeClient | undefined { return; } - const customDefaultIntegrations = getDefaultIntegrations(options); + const customDefaultIntegrations = getDefaultIntegrations(options) + .filter(integration => integration.name !== 'Http') + .concat( + // We are using the HTTP integration without instrumenting incoming HTTP requests because Next.js does that by itself. + httpIntegration({ + instrumentation: { + _experimentalConfig: { + disableIncomingRequestInstrumentation: true, + }, + }, + }), + ); // Turn off Next.js' own fetch instrumentation // https://github.com/lforst/nextjs-fork/blob/1994fd186defda77ad971c36dc3163db263c993f/packages/next/src/server/lib/patch-fetch.ts#L245 @@ -319,15 +334,47 @@ export function init(options: NodeOptions): NodeClient | undefined { } // Enhance route handler transactions - if (event.type === 'transaction' && event.contexts?.trace?.data?.['sentry.route_handler'] === true) { + if ( + event.type === 'transaction' && + (event.contexts?.trace?.data?.['sentry.route_handler'] === true || + event.contexts?.trace?.data?.['sentry.rsc'] === true) + ) { event.contexts.trace.data = event.contexts.trace.data || {}; event.contexts.trace.data[SEMANTIC_ATTRIBUTE_SENTRY_OP] = 'http.server'; event.contexts.trace.op = 'http.server'; + + if (event.transaction) { + event.transaction = stripUrlQueryAndFragment(event.transaction); + } + if (typeof event.contexts.trace.data[ATTR_HTTP_ROUTE] === 'string') { // eslint-disable-next-line deprecation/deprecation event.transaction = `${event.contexts.trace.data[SEMATTRS_HTTP_METHOD]} ${event.contexts.trace.data[ ATTR_HTTP_ROUTE ].replace(/\/route$/, '')}`; + event.contexts.trace.data[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] = 'route'; + } + } + + // Next.js 13 is not correctly picking up tracing data for trace propagation so we use a back-fill strategy + if ( + event.type === 'transaction' && + typeof event.contexts?.trace?.data?.[TRANSACTION_ATTR_SENTRY_TRACE_BACKFILL] === 'string' + ) { + const traceparentData = extractTraceparentData( + event.contexts.trace.data[TRANSACTION_ATTR_SENTRY_TRACE_BACKFILL], + ); + + if (traceparentData?.parentSampled === false) { + return null; + } + + if (traceparentData?.traceId) { + event.contexts.trace.trace_id = traceparentData.traceId; + } + + if (traceparentData?.parentSpanId) { + event.contexts.trace.parent_span_id = traceparentData.parentSpanId; } }