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 e14573254dfb..8616aafadba8 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 @@ -17,7 +17,6 @@ test('Will create a transaction with spans for every server component and metada expect(spanDescriptions).toContainEqual('render route (app) /nested-layout'); expect(spanDescriptions).toContainEqual('generateMetadata /(nested-layout)/nested-layout/page'); - expect(spanDescriptions).toContainEqual('Page.generateMetadata (/(nested-layout)/nested-layout)'); // Next.js 13 has limited OTEL support for server components, so we don't expect to see the following spans if (!isNext13) { @@ -46,7 +45,6 @@ test('Will create a transaction with spans for every server component and metada expect(spanDescriptions).toContainEqual('render route (app) /nested-layout/[dynamic]'); expect(spanDescriptions).toContainEqual('generateMetadata /(nested-layout)/nested-layout/[dynamic]/page'); - expect(spanDescriptions).toContainEqual('Page.generateMetadata (/(nested-layout)/nested-layout/[dynamic])'); // Next.js 13 has limited OTEL support for server components, so we don't expect to see the following spans if (!isNext13) { diff --git a/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts b/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts index 85969ef1064d..295b06548af4 100644 --- a/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts +++ b/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts @@ -1,133 +1,83 @@ -import type { RequestEventData, WebFetchHeaders } from '@sentry/core'; +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 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'; +import { flushSafelyWithTimeout, waitUntil } from './utils/responseEnd'; + /** - * Wraps a generation function (e.g. generateMetadata) with Sentry error and performance instrumentation. + * Wraps a generation function (e.g. generateMetadata) with Sentry error instrumentation. */ // eslint-disable-next-line @typescript-eslint/no-explicit-any export function wrapGenerationFunctionWithSentry any>( generationFunction: F, context: GenerationFunctionContext, ): F { - const { requestAsyncStorage, componentRoute, componentType, generationFunctionIdentifier } = context; return new Proxy(generationFunction, { apply: (originalFunction, thisArg, args) => { - const requestTraceId = getActiveSpan()?.spanContext().traceId; - let headers: WebFetchHeaders | undefined = undefined; - // We try-catch here just in case anything goes wrong with the async storage here goes wrong since it is Next.js internal API + const isolationScope = getIsolationScope(); + + let headers = undefined; + // We try-catch here just in case anything goes wrong with the async storage since it is Next.js internal API try { - headers = requestAsyncStorage?.getStore()?.headers; + headers = context.requestAsyncStorage?.getStore()?.headers; } catch { /** empty */ } - const isolationScope = commonObjectToIsolationScope(headers); - - const activeSpan = getActiveSpan(); - if (activeSpan) { - const rootSpan = getRootSpan(activeSpan); - const { scope } = getCapturedScopesOnSpan(rootSpan); - setCapturedScopesOnSpan(rootSpan, scope ?? new Scope(), isolationScope); - } - const headersDict = headers ? winterCGHeadersToDict(headers) : undefined; - return withIsolationScope(isolationScope, () => { - return withScope(scope => { - scope.setTransactionName(`${componentType}.${generationFunctionIdentifier} (${componentRoute})`); + isolationScope.setSDKProcessingMetadata({ + normalizedRequest: { + headers: headersDict, + } satisfies RequestEventData, + }); - isolationScope.setSDKProcessingMetadata({ - normalizedRequest: { - headers: headersDict, - } satisfies RequestEventData, - }); + return handleCallbackErrors( + () => originalFunction.apply(thisArg, args), + error => { + const span = getActiveSpan(); + const { componentRoute, componentType, generationFunctionIdentifier } = context; + let shouldCapture = true; + isolationScope.setTransactionName(`${componentType}.${generationFunctionIdentifier} (${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)) { + // We don't want to report "not-found"s + shouldCapture = false; + span.setStatus({ code: SPAN_STATUS_ERROR, message: 'not_found' }); + } else if (isRedirectNavigationError(error)) { + // We don't want to report redirects + shouldCapture = false; + span.setStatus({ code: SPAN_STATUS_OK }); + } else { + span.setStatus({ code: SPAN_STATUS_ERROR, message: 'internal_error' }); } } - const propagationContext = commonObjectToPropagationContext( - headers, - propagationContextFromHeaders(headersDict?.['sentry-trace'], headersDict?.['baggage']), - ); - - if (requestTraceId) { - propagationContext.traceId = requestTraceId; - } - - scope.setPropagationContext(propagationContext); - - return startSpanManual( - { - op: 'function.nextjs', - name: `${componentType}.${generationFunctionIdentifier} (${componentRoute})`, - attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.nextjs', - 'sentry.nextjs.ssr.function.type': generationFunctionIdentifier, - 'sentry.nextjs.ssr.function.route': componentRoute, - }, - }, - span => { - return handleCallbackErrors( - () => originalFunction.apply(thisArg, args), - err => { - // 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(err)) { - // We don't want to report "not-found"s - span.setStatus({ code: SPAN_STATUS_ERROR, message: 'not_found' }); - getRootSpan(span).setStatus({ code: SPAN_STATUS_ERROR, message: 'not_found' }); - } else if (isRedirectNavigationError(err)) { - // We don't want to report redirects - span.setStatus({ code: SPAN_STATUS_OK }); - } else { - span.setStatus({ code: SPAN_STATUS_ERROR, message: 'internal_error' }); - getRootSpan(span).setStatus({ code: SPAN_STATUS_ERROR, message: 'internal_error' }); - captureException(err, { - mechanism: { - handled: false, - type: 'auto.function.nextjs.generation_function', - data: { - function: generationFunctionIdentifier, - }, - }, - }); - } - }, - () => { - span.end(); + if (shouldCapture) { + captureException(error, { + mechanism: { + handled: false, + type: 'auto.function.nextjs.generation_function', + data: { + function: generationFunctionIdentifier, }, - ); - }, - ); - }); - }); + }, + }); + } + }, + () => { + waitUntil(flushSafelyWithTimeout()); + }, + ); }, }); }