Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down
146 changes: 48 additions & 98 deletions packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts
Original file line number Diff line number Diff line change
@@ -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<F extends (...args: any[]) => 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,
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Navigation errors incorrectly captured when no span exists

High Severity

The shouldCapture variable defaults to true, but the logic that sets it to false for not-found and redirect navigation errors is nested inside if (span). When getActiveSpan() returns undefined (no active span), the navigation error checks are skipped entirely, causing captureException to be called for these expected navigation behaviors. This results in erroneous error reports to Sentry for redirects and 404s that should be silently ignored.

Fix in Cursor Fix in Web

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());
},
);
},
});
}
Loading