diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/app/(nested-layout)/nested-layout/[dynamic]/layout.tsx b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/app/(nested-layout)/nested-layout/[dynamic]/layout.tsx new file mode 100644 index 000000000000..dbdc60adadc2 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/app/(nested-layout)/nested-layout/[dynamic]/layout.tsx @@ -0,0 +1,12 @@ +import { PropsWithChildren } from 'react'; + +export const dynamic = 'force-dynamic'; + +export default function Layout({ children }: PropsWithChildren<{}>) { + return ( +
+

DynamicLayout

+ {children} +
+ ); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/app/(nested-layout)/nested-layout/[dynamic]/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/app/(nested-layout)/nested-layout/[dynamic]/page.tsx new file mode 100644 index 000000000000..3eaddda2a1df --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/app/(nested-layout)/nested-layout/[dynamic]/page.tsx @@ -0,0 +1,15 @@ +export const dynamic = 'force-dynamic'; + +export default async function Page() { + return ( +
+

Dynamic Page

+
+ ); +} + +export async function generateMetadata() { + return { + title: 'I am dynamic page generated metadata', + }; +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/connected-servercomponent-trace.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/connected-servercomponent-trace.test.ts index 3d2f29358d54..1657e94af1a4 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/connected-servercomponent-trace.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/connected-servercomponent-trace.test.ts @@ -14,8 +14,43 @@ test('Will create a transaction with spans for every server component and metada return span.description; }); - expect(spanDescriptions).toContainEqual('Layout Server Component (/(nested-layout)/nested-layout)'); - expect(spanDescriptions).toContainEqual('Layout Server Component (/(nested-layout))'); - expect(spanDescriptions).toContainEqual('Page Server Component (/(nested-layout)/nested-layout)'); + expect(spanDescriptions).toContainEqual('resolve page components'); + expect(spanDescriptions).toContainEqual('render route (app) /nested-layout'); + expect(spanDescriptions).toContainEqual('build component tree'); + expect(spanDescriptions).toContainEqual('resolve root layout server component'); + expect(spanDescriptions).toContainEqual('resolve layout server component "(nested-layout)"'); + expect(spanDescriptions).toContainEqual('resolve layout server component "nested-layout"'); + expect(spanDescriptions).toContainEqual('resolve page server component "/nested-layout"'); + expect(spanDescriptions).toContainEqual('generateMetadata /(nested-layout)/nested-layout/page'); expect(spanDescriptions).toContainEqual('Page.generateMetadata (/(nested-layout)/nested-layout)'); + expect(spanDescriptions).toContainEqual('start response'); + expect(spanDescriptions).toContainEqual('NextNodeServer.clientComponentLoading'); +}); + +test('Will create a transaction with spans for every server component and metadata generation functions when visiting a dynamic page', async ({ + page, +}) => { + const serverTransactionEventPromise = waitForTransaction('nextjs-app-dir', async transactionEvent => { + console.log(transactionEvent?.transaction); + return transactionEvent?.transaction === 'GET /nested-layout/[dynamic]'; + }); + + await page.goto('/nested-layout/123'); + + const spanDescriptions = (await serverTransactionEventPromise).spans?.map(span => { + return span.description; + }); + + expect(spanDescriptions).toContainEqual('resolve page components'); + expect(spanDescriptions).toContainEqual('render route (app) /nested-layout/[dynamic]'); + expect(spanDescriptions).toContainEqual('build component tree'); + expect(spanDescriptions).toContainEqual('resolve root layout server component'); + expect(spanDescriptions).toContainEqual('resolve layout server component "(nested-layout)"'); + expect(spanDescriptions).toContainEqual('resolve layout server component "nested-layout"'); + expect(spanDescriptions).toContainEqual('resolve layout server component "[dynamic]"'); + expect(spanDescriptions).toContainEqual('resolve page server component "/nested-layout/[dynamic]"'); + expect(spanDescriptions).toContainEqual('generateMetadata /(nested-layout)/nested-layout/[dynamic]/page'); + expect(spanDescriptions).toContainEqual('Page.generateMetadata (/(nested-layout)/nested-layout/[dynamic])'); + expect(spanDescriptions).toContainEqual('start response'); + expect(spanDescriptions).toContainEqual('NextNodeServer.clientComponentLoading'); }); 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 eedb702715de..5aca8a0b1688 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 @@ -70,13 +70,20 @@ test('Should set a "not_found" status on a server component span when notFound() const transactionEvent = await serverComponentTransactionPromise; - // Transaction should have status ok, because the http status is ok, but the server component span should be not_found + // Transaction should have status ok, because the http status is ok, but the render component span should be not_found expect(transactionEvent.contexts?.trace?.status).toBe('ok'); expect(transactionEvent.spans).toContainEqual( expect.objectContaining({ - description: 'Page Server Component (/server-component/not-found)', - op: 'function.nextjs', + description: 'render route (app) /server-component/not-found', status: 'not_found', + }), + ); + + // Page server component span should have the right name and attributes + expect(transactionEvent.spans).toContainEqual( + expect.objectContaining({ + description: 'resolve page server component "/server-component/not-found"', + op: 'function.nextjs', data: expect.objectContaining({ 'sentry.nextjs.ssr.function.type': 'Page', 'sentry.nextjs.ssr.function.route': '/server-component/not-found', @@ -102,13 +109,20 @@ test('Should capture an error and transaction for a app router page', async ({ p // Error event should have the right transaction name expect(errorEvent.transaction).toBe(`Page Server Component (/server-component/faulty)`); - // Transaction should have status ok, because the http status is ok, but the server component span should be internal_error + // Transaction should have status ok, because the http status is ok, but the render component span should be internal_error expect(transactionEvent.contexts?.trace?.status).toBe('ok'); expect(transactionEvent.spans).toContainEqual( expect.objectContaining({ - description: 'Page Server Component (/server-component/faulty)', - op: 'function.nextjs', + description: 'render route (app) /server-component/faulty', status: 'internal_error', + }), + ); + + // The page server component span should have the right name and attributes + expect(transactionEvent.spans).toContainEqual( + expect.objectContaining({ + description: 'resolve page server component "/server-component/faulty"', + op: 'function.nextjs', data: expect.objectContaining({ 'sentry.nextjs.ssr.function.type': 'Page', 'sentry.nextjs.ssr.function.route': '/server-component/faulty', diff --git a/packages/nextjs/src/common/nextSpanAttributes.ts b/packages/nextjs/src/common/nextSpanAttributes.ts index 8b9f4a9d1374..2f412d6ce7db 100644 --- a/packages/nextjs/src/common/nextSpanAttributes.ts +++ b/packages/nextjs/src/common/nextSpanAttributes.ts @@ -1,3 +1,5 @@ export const ATTR_NEXT_SPAN_TYPE = 'next.span_type'; export const ATTR_NEXT_SPAN_NAME = 'next.span_name'; export const ATTR_NEXT_ROUTE = 'next.route'; +export const ATTR_NEXT_SPAN_DESCRIPTION = 'next.span_description'; +export const ATTR_NEXT_SEGMENT = 'next.segment'; diff --git a/packages/nextjs/src/common/utils/tracingUtils.ts b/packages/nextjs/src/common/utils/tracingUtils.ts index bda3049fbc78..efa3ac4fdbf6 100644 --- a/packages/nextjs/src/common/utils/tracingUtils.ts +++ b/packages/nextjs/src/common/utils/tracingUtils.ts @@ -1,10 +1,23 @@ -import type { PropagationContext } from '@sentry/core'; -import { debug, getActiveSpan, getRootSpan, GLOBAL_OBJ, Scope, spanToJSON, startNewTrace } from '@sentry/core'; +import { ATTR_HTTP_ROUTE } from '@opentelemetry/semantic-conventions'; +import type { PropagationContext, Span, SpanAttributes } from '@sentry/core'; +import { + debug, + getActiveSpan, + getRootSpan, + GLOBAL_OBJ, + Scope, + SEMANTIC_ATTRIBUTE_SENTRY_OP, + spanToJSON, + startNewTrace, +} from '@sentry/core'; import { DEBUG_BUILD } from '../debug-build'; +import { ATTR_NEXT_SEGMENT, ATTR_NEXT_SPAN_NAME, ATTR_NEXT_SPAN_TYPE } from '../nextSpanAttributes'; import { TRANSACTION_ATTR_SHOULD_DROP_TRANSACTION } from '../span-attributes-with-logic-attached'; const commonPropagationContextMap = new WeakMap(); +const PAGE_SEGMENT = '__PAGE__'; + /** * Takes a shared (garbage collectable) object between resources, e.g. a headers object shared between Next.js server components and returns a common propagation context. * @@ -108,3 +121,61 @@ export function dropNextjsRootContext(): void { } } } + +/** + * Checks if the span is a resolve segment span. + * @param spanAttributes The attributes of the span to check. + * @returns True if the span is a resolve segment span, false otherwise. + */ +export function isResolveSegmentSpan(spanAttributes: SpanAttributes): boolean { + return ( + spanAttributes[ATTR_NEXT_SPAN_TYPE] === 'NextNodeServer.getLayoutOrPageModule' && + spanAttributes[ATTR_NEXT_SPAN_NAME] === 'resolve segment modules' && + typeof spanAttributes[ATTR_NEXT_SEGMENT] === 'string' + ); +} + +/** + * Returns the enhanced name for a resolve segment span. + * @param segment The segment of the resolve segment span. + * @param route The route of the resolve segment span. + * @returns The enhanced name for the resolve segment span. + */ +export function getEnhancedResolveSegmentSpanName({ segment, route }: { segment: string; route: string }): string { + if (segment === PAGE_SEGMENT) { + return `resolve page server component "${route}"`; + } + + if (segment === '') { + return 'resolve root layout server component'; + } + + return `resolve layout server component "${segment}"`; +} + +/** + * Maybe enhances the span name for a resolve segment span. + * If the span is not a resolve segment span, this function does nothing. + * @param activeSpan The active span. + * @param spanAttributes The attributes of the span to check. + * @param rootSpanAttributes The attributes of the according root span. + */ +export function maybeEnhanceServerComponentSpanName( + activeSpan: Span, + spanAttributes: SpanAttributes, + rootSpanAttributes: SpanAttributes, +): void { + if (!isResolveSegmentSpan(spanAttributes)) { + return; + } + + const segment = spanAttributes[ATTR_NEXT_SEGMENT] as string; + const route = rootSpanAttributes[ATTR_HTTP_ROUTE]; + const enhancedName = getEnhancedResolveSegmentSpanName({ segment, route: typeof route === 'string' ? route : '' }); + activeSpan.updateName(enhancedName); + activeSpan.setAttributes({ + 'sentry.nextjs.ssr.function.type': segment === PAGE_SEGMENT ? 'Page' : 'Layout', + 'sentry.nextjs.ssr.function.route': route, + }); + activeSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'function.nextjs'); +} diff --git a/packages/nextjs/src/common/wrapServerComponentWithSentry.ts b/packages/nextjs/src/common/wrapServerComponentWithSentry.ts index bdf8f77f4f97..f3a855dfae13 100644 --- a/packages/nextjs/src/common/wrapServerComponentWithSentry.ts +++ b/packages/nextjs/src/common/wrapServerComponentWithSentry.ts @@ -2,26 +2,16 @@ import type { RequestEventData } from '@sentry/core'; import { captureException, getActiveSpan, - getCapturedScopesOnSpan, - getRootSpan, + getIsolationScope, handleCallbackErrors, - propagationContextFromHeaders, - Scope, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, - setCapturedScopesOnSpan, SPAN_STATUS_ERROR, SPAN_STATUS_OK, - startSpanManual, winterCGHeadersToDict, - withIsolationScope, - withScope, } from '@sentry/core'; import { isNotFoundNavigationError, isRedirectNavigationError } from '../common/nextNavigationErrorUtils'; import type { ServerComponentContext } from '../common/types'; import { flushSafelyWithTimeout, waitUntil } from '../common/utils/responseEnd'; -import { TRANSACTION_ATTR_SENTRY_TRACE_BACKFILL } from './span-attributes-with-logic-attached'; -import { commonObjectToIsolationScope, commonObjectToPropagationContext } from './utils/tracingUtils'; +import { commonObjectToIsolationScope } from './utils/tracingUtils'; /** * Wraps an `app` directory server component with Sentry error instrumentation. @@ -31,22 +21,13 @@ export function wrapServerComponentWithSentry any> appDirComponent: F, context: ServerComponentContext, ): F { - const { componentRoute, componentType } = context; // Even though users may define server components as async functions, for the client bundles // Next.js will turn them into synchronous functions and it will transform any `await`s into instances of the `use` // hook. 🤯 return new Proxy(appDirComponent, { apply: (originalFunction, thisArg, args) => { - const requestTraceId = getActiveSpan()?.spanContext().traceId; const isolationScope = commonObjectToIsolationScope(context.headers); - const activeSpan = getActiveSpan(); - if (activeSpan) { - const rootSpan = getRootSpan(activeSpan); - const { scope } = getCapturedScopesOnSpan(rootSpan); - setCapturedScopesOnSpan(rootSpan, scope ?? new Scope(), isolationScope); - } - const headersDict = context.headers ? winterCGHeadersToDict(context.headers) : undefined; isolationScope.setSDKProcessingMetadata({ @@ -55,74 +36,42 @@ export function wrapServerComponentWithSentry any> } satisfies RequestEventData, }); - return withIsolationScope(isolationScope, () => { - return withScope(scope => { - scope.setTransactionName(`${componentType} Server Component (${componentRoute})`); - - if (process.env.NEXT_RUNTIME === 'edge') { - const propagationContext = commonObjectToPropagationContext( - context.headers, - propagationContextFromHeaders(headersDict?.['sentry-trace'], headersDict?.['baggage']), - ); - - if (requestTraceId) { - propagationContext.traceId = requestTraceId; - } - - scope.setPropagationContext(propagationContext); - } + return handleCallbackErrors( + () => originalFunction.apply(thisArg, args), + error => { + const isolationScope = getIsolationScope(); + const span = getActiveSpan(); + const { componentRoute, componentType } = context; + let shouldCapture = false; + isolationScope.setTransactionName(`${componentType} Server Component (${componentRoute})`); - const activeSpan = getActiveSpan(); - if (activeSpan) { - const rootSpan = getRootSpan(activeSpan); - const sentryTrace = headersDict?.['sentry-trace']; - if (sentryTrace) { - rootSpan.setAttribute(TRANSACTION_ATTR_SENTRY_TRACE_BACKFILL, sentryTrace); + if (span) { + if (isNotFoundNavigationError(error)) { + shouldCapture = false; + // We don't want to report "not-found"s + span.setStatus({ code: SPAN_STATUS_ERROR, message: 'not_found' }); + } else if (isRedirectNavigationError(error)) { + shouldCapture = false; + // We don't want to report redirects + span.setStatus({ code: SPAN_STATUS_OK }); + } else { + span.setStatus({ code: SPAN_STATUS_ERROR, message: 'internal_error' }); } } - return startSpanManual( - { - op: 'function.nextjs', - name: `${componentType} Server Component (${componentRoute})`, - attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.nextjs.server_component', - 'sentry.nextjs.ssr.function.type': componentType, - 'sentry.nextjs.ssr.function.route': componentRoute, + if (shouldCapture) { + captureException(error, { + mechanism: { + handled: false, + type: 'auto.function.nextjs.server_component', }, - }, - span => { - return handleCallbackErrors( - () => originalFunction.apply(thisArg, args), - error => { - // When you read this code you might think: "Wait a minute, shouldn't we set the status on the root span too?" - // The answer is: "No." - The status of the root span is determined by whatever status code Next.js decides to put on the response. - if (isNotFoundNavigationError(error)) { - // We don't want to report "not-found"s - span.setStatus({ code: SPAN_STATUS_ERROR, message: 'not_found' }); - } else if (isRedirectNavigationError(error)) { - // We don't want to report redirects - span.setStatus({ code: SPAN_STATUS_OK }); - } else { - span.setStatus({ code: SPAN_STATUS_ERROR, message: 'internal_error' }); - captureException(error, { - mechanism: { - handled: false, - type: 'auto.function.nextjs.server_component', - }, - }); - } - }, - () => { - span.end(); - waitUntil(flushSafelyWithTimeout()); - }, - ); - }, - ); - }); - }); + }); + } + }, + () => { + waitUntil(flushSafelyWithTimeout()); + }, + ); }, }); } diff --git a/packages/nextjs/src/edge/index.ts b/packages/nextjs/src/edge/index.ts index 27d42f616727..fcaad178b9fa 100644 --- a/packages/nextjs/src/edge/index.ts +++ b/packages/nextjs/src/edge/index.ts @@ -22,6 +22,7 @@ import { import { getScopesFromContext } from '@sentry/opentelemetry'; import type { VercelEdgeOptions } from '@sentry/vercel-edge'; import { getDefaultIntegrations, init as vercelEdgeInit } from '@sentry/vercel-edge'; +import { ATTR_NEXT_SPAN_TYPE } from '../common/nextSpanAttributes'; import { TRANSACTION_ATTR_SHOULD_DROP_TRANSACTION } from '../common/span-attributes-with-logic-attached'; import { addHeadersAsAttributes } from '../common/utils/addHeadersAsAttributes'; import { dropMiddlewareTunnelRequests } from '../common/utils/dropMiddlewareTunnelRequests'; @@ -90,19 +91,21 @@ export function init(options: VercelEdgeOptions = {}): void { if (spanAttributes?.['next.span_type'] === 'Middleware.execute') { span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'http.server.middleware'); span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'url'); + } - if (isRootSpan) { - // Fork isolation scope for middleware requests - const scopes = getCapturedScopesOnSpan(span); - const isolationScope = (scopes.isolationScope || getIsolationScope()).clone(); - const scope = scopes.scope || getCurrentScope(); - const currentScopesPointer = getScopesFromContext(context.active()); - if (currentScopesPointer) { - currentScopesPointer.isolationScope = isolationScope; - } + // We want to fork the isolation scope for incoming requests + if (spanAttributes?.[ATTR_NEXT_SPAN_TYPE] === 'BaseServer.handleRequest' && isRootSpan) { + const scopes = getCapturedScopesOnSpan(span); - setCapturedScopesOnSpan(span, scope, isolationScope); + const isolationScope = (scopes.isolationScope || getIsolationScope()).clone(); + const scope = scopes.scope || getCurrentScope(); + + const currentScopesPointer = getScopesFromContext(context.active()); + if (currentScopesPointer) { + currentScopesPointer.isolationScope = isolationScope; } + + setCapturedScopesOnSpan(span, scope, isolationScope); } if (isRootSpan) { diff --git a/packages/nextjs/src/server/handleOnSpanStart.ts b/packages/nextjs/src/server/handleOnSpanStart.ts new file mode 100644 index 000000000000..de5dd2a1f3d5 --- /dev/null +++ b/packages/nextjs/src/server/handleOnSpanStart.ts @@ -0,0 +1,87 @@ +import { context } from '@opentelemetry/api'; +import { ATTR_HTTP_REQUEST_METHOD, ATTR_HTTP_ROUTE, SEMATTRS_HTTP_METHOD } from '@opentelemetry/semantic-conventions'; +import type { Span } from '@sentry/core'; +import { + getCapturedScopesOnSpan, + getCurrentScope, + getIsolationScope, + getRootSpan, + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + setCapturedScopesOnSpan, + spanToJSON, +} from '@sentry/core'; +import { getScopesFromContext } from '@sentry/opentelemetry'; +import { ATTR_NEXT_ROUTE, ATTR_NEXT_SPAN_NAME, ATTR_NEXT_SPAN_TYPE } from '../common/nextSpanAttributes'; +import { addHeadersAsAttributes } from '../common/utils/addHeadersAsAttributes'; +import { dropMiddlewareTunnelRequests } from '../common/utils/dropMiddlewareTunnelRequests'; +import { maybeEnhanceServerComponentSpanName } from '../common/utils/tracingUtils'; + +/** + * Handles the on span start event for Next.js spans. + * This function is used to enhance the span with additional information such as the route, the method, the headers, etc. + * It is called for every span that is started by Next.js. + * @param span The span that is starting. + */ +export function handleOnSpanStart(span: Span): void { + const spanAttributes = spanToJSON(span).data; + const rootSpan = getRootSpan(span); + const rootSpanAttributes = spanToJSON(rootSpan).data; + const isRootSpan = span === rootSpan; + + dropMiddlewareTunnelRequests(span, spanAttributes); + + // What we do in this glorious piece of code, is hoist any information about parameterized routes from spans emitted + // by Next.js via the `next.route` attribute, up to the transaction by setting the http.route attribute. + if (typeof spanAttributes?.[ATTR_NEXT_ROUTE] === 'string') { + // Only hoist the http.route attribute if the transaction doesn't already have it + if ( + // eslint-disable-next-line deprecation/deprecation + (rootSpanAttributes?.[ATTR_HTTP_REQUEST_METHOD] || rootSpanAttributes?.[SEMATTRS_HTTP_METHOD]) && + !rootSpanAttributes?.[ATTR_HTTP_ROUTE] + ) { + const route = spanAttributes[ATTR_NEXT_ROUTE].replace(/\/route$/, ''); + rootSpan.updateName(route); + rootSpan.setAttribute(ATTR_HTTP_ROUTE, route); + // Preserving the original attribute despite internally not depending on it + rootSpan.setAttribute(ATTR_NEXT_ROUTE, route); + } + } + + if (spanAttributes?.[ATTR_NEXT_SPAN_TYPE] === 'Middleware.execute') { + const middlewareName = spanAttributes[ATTR_NEXT_SPAN_NAME]; + if (typeof middlewareName === 'string') { + rootSpan.updateName(middlewareName); + rootSpan.setAttribute(ATTR_HTTP_ROUTE, middlewareName); + rootSpan.setAttribute(ATTR_NEXT_SPAN_NAME, middlewareName); + } + span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, 'auto'); + } + + // We want to skip span data inference for any spans generated by Next.js. Reason being that Next.js emits spans + // with patterns (e.g. http.server spans) that will produce confusing data. + if (spanAttributes?.[ATTR_NEXT_SPAN_TYPE] !== undefined) { + span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, 'auto'); + } + + if (isRootSpan) { + const headers = getIsolationScope().getScopeData().sdkProcessingMetadata?.normalizedRequest?.headers; + addHeadersAsAttributes(headers, rootSpan); + } + + // We want to fork the isolation scope for incoming requests + if (spanAttributes?.[ATTR_NEXT_SPAN_TYPE] === 'BaseServer.handleRequest' && isRootSpan) { + const scopes = getCapturedScopesOnSpan(span); + + const isolationScope = (scopes.isolationScope || getIsolationScope()).clone(); + const scope = scopes.scope || getCurrentScope(); + + const currentScopesPointer = getScopesFromContext(context.active()); + if (currentScopesPointer) { + currentScopesPointer.isolationScope = isolationScope; + } + + setCapturedScopesOnSpan(span, scope, isolationScope); + } + + maybeEnhanceServerComponentSpanName(span, spanAttributes, rootSpanAttributes); +} diff --git a/packages/nextjs/src/server/index.ts b/packages/nextjs/src/server/index.ts index bc5372274ad6..d95a9693bd76 100644 --- a/packages/nextjs/src/server/index.ts +++ b/packages/nextjs/src/server/index.ts @@ -3,7 +3,6 @@ /* eslint-disable import/export */ import { context } from '@opentelemetry/api'; import { - ATTR_HTTP_REQUEST_METHOD, ATTR_HTTP_ROUTE, ATTR_URL_QUERY, SEMATTRS_HTTP_METHOD, @@ -14,23 +13,15 @@ import { applySdkMetadata, debug, extractTraceparentData, - getCapturedScopesOnSpan, getClient, - getCurrentScope, getGlobalScope, - getIsolationScope, - getRootSpan, GLOBAL_OBJ, SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, - setCapturedScopesOnSpan, - spanToJSON, stripUrlQueryAndFragment, } from '@sentry/core'; import type { NodeClient, NodeOptions } from '@sentry/node'; import { getDefaultIntegrations, httpIntegration, init as nodeInit } from '@sentry/node'; -import { getScopesFromContext } from '@sentry/opentelemetry'; import { DEBUG_BUILD } from '../common/debug-build'; import { devErrorSymbolicationEventProcessor } from '../common/devErrorSymbolicationEventProcessor'; import { getVercelEnv } from '../common/getVercelEnv'; @@ -40,11 +31,10 @@ import { TRANSACTION_ATTR_SENTRY_TRACE_BACKFILL, TRANSACTION_ATTR_SHOULD_DROP_TRANSACTION, } from '../common/span-attributes-with-logic-attached'; -import { addHeadersAsAttributes } from '../common/utils/addHeadersAsAttributes'; -import { dropMiddlewareTunnelRequests } from '../common/utils/dropMiddlewareTunnelRequests'; import { isBuild } from '../common/utils/isBuild'; import { setUrlProcessingMetadata } from '../common/utils/setUrlProcessingMetadata'; import { distDirRewriteFramesIntegration } from './distDirRewriteFramesIntegration'; +import { handleOnSpanStart } from './handleOnSpanStart'; export * from '@sentry/node'; @@ -172,65 +162,7 @@ export function init(options: NodeOptions): NodeClient | undefined { }); client?.on('spanStart', span => { - const spanAttributes = spanToJSON(span).data; - const rootSpan = getRootSpan(span); - const isRootSpan = span === rootSpan; - - dropMiddlewareTunnelRequests(span, spanAttributes); - - // What we do in this glorious piece of code, is hoist any information about parameterized routes from spans emitted - // by Next.js via the `next.route` attribute, up to the transaction by setting the http.route attribute. - if (typeof spanAttributes?.[ATTR_NEXT_ROUTE] === 'string') { - const rootSpanAttributes = spanToJSON(rootSpan).data; - // Only hoist the http.route attribute if the transaction doesn't already have it - if ( - // eslint-disable-next-line deprecation/deprecation - (rootSpanAttributes?.[ATTR_HTTP_REQUEST_METHOD] || rootSpanAttributes?.[SEMATTRS_HTTP_METHOD]) && - !rootSpanAttributes?.[ATTR_HTTP_ROUTE] - ) { - const route = spanAttributes[ATTR_NEXT_ROUTE].replace(/\/route$/, ''); - rootSpan.updateName(route); - rootSpan.setAttribute(ATTR_HTTP_ROUTE, route); - // Preserving the original attribute despite internally not depending on it - rootSpan.setAttribute(ATTR_NEXT_ROUTE, route); - } - } - - if (spanAttributes?.[ATTR_NEXT_SPAN_TYPE] === 'Middleware.execute') { - const middlewareName = spanAttributes[ATTR_NEXT_SPAN_NAME]; - if (typeof middlewareName === 'string') { - rootSpan.updateName(middlewareName); - rootSpan.setAttribute(ATTR_HTTP_ROUTE, middlewareName); - rootSpan.setAttribute(ATTR_NEXT_SPAN_NAME, middlewareName); - } - span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, 'auto'); - } - - // We want to skip span data inference for any spans generated by Next.js. Reason being that Next.js emits spans - // with patterns (e.g. http.server spans) that will produce confusing data. - if (spanAttributes?.[ATTR_NEXT_SPAN_TYPE] !== undefined) { - span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, 'auto'); - } - - if (isRootSpan) { - const headers = getIsolationScope().getScopeData().sdkProcessingMetadata?.normalizedRequest?.headers; - addHeadersAsAttributes(headers, rootSpan); - } - - // We want to fork the isolation scope for incoming requests - if (spanAttributes?.[ATTR_NEXT_SPAN_TYPE] === 'BaseServer.handleRequest' && isRootSpan) { - const scopes = getCapturedScopesOnSpan(span); - - const isolationScope = (scopes.isolationScope || getIsolationScope()).clone(); - const scope = scopes.scope || getCurrentScope(); - - const currentScopesPointer = getScopesFromContext(context.active()); - if (currentScopesPointer) { - currentScopesPointer.isolationScope = isolationScope; - } - - setCapturedScopesOnSpan(span, scope, isolationScope); - } + handleOnSpanStart(span); }); getGlobalScope().addEventProcessor(