Skip to content

Conversation

@chargome
Copy link
Member

@chargome chargome commented Dec 4, 2025

@chargome chargome self-assigned this Dec 4, 2025
@linear
Copy link

linear bot commented Dec 4, 2025

@chargome chargome requested a review from logaretm December 5, 2025 09:50
@chargome chargome marked this pull request as ready for review December 5, 2025 09:50
Comment on lines 40 to 45
() => originalFunction.apply(thisArg, args),
error => {
const isolationScope = getIsolationScope();
const span = getActiveSpan();
const { componentRoute, componentType } = context;
isolationScope.setTransactionName(`${componentType} Server Component (${componentRoute})`);

This comment was marked as outdated.

Comment on lines 58 to 68

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

This comment was marked as outdated.

() => {
waitUntil(flushSafelyWithTimeout());
},
);
Copy link

Choose a reason for hiding this comment

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

Bug: Missing isolation scope context breaks request metadata attachment

The refactored code retrieves an isolation scope via commonObjectToIsolationScope at line 29 and sets normalizedRequest metadata on it, but the handleCallbackErrors callback is not wrapped with withIsolationScope. When captureException is called in the error handler, getIsolationScope() at line 42 returns the current global isolation scope - not the one with the request metadata. This causes captured exceptions to be missing request context (like headers) that was set up earlier. The original code used withIsolationScope(isolationScope, ...) to ensure all nested code ran with the correct isolation scope.

Fix in Cursor Fix in Web

Copy link
Member Author

Choose a reason for hiding this comment

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

the scope already gets forked earlier

@github-actions
Copy link
Contributor

github-actions bot commented Dec 5, 2025

node-overhead report 🧳

Note: This is a synthetic benchmark with a minimal express app and does not necessarily reflect the real-world performance impact in an application.
⚠️ Warning: Base artifact is not the latest one, because the latest workflow run is not done yet. This may lead to incorrect results. Try to re-run all tests to get up to date results.

Scenario Requests/s % of Baseline Prev. Requests/s Change %
GET Baseline 9,209 - 8,964 +3%
GET With Sentry 1,599 17% 1,758 -9%
GET With Sentry (error only) 5,930 64% 6,183 -4%
POST Baseline 1,170 - 1,202 -3%
POST With Sentry 562 48% 595 -6%
POST With Sentry (error only) 1,014 87% 1,075 -6%
MYSQL Baseline 3,169 - 3,329 -5%
MYSQL With Sentry 418 13% 479 -13%
MYSQL With Sentry (error only) 2,581 81% 2,743 -6%

View base workflow run

Comment on lines 53 to 63
} 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, {

This comment was marked as outdated.

Comment on lines +64 to +74
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());
},
);

This comment was marked as outdated.

page,
}) => {
const serverTransactionEventPromise = waitForTransaction('nextjs-app-dir', async transactionEvent => {
console.log(transactionEvent?.transaction);
Copy link

Choose a reason for hiding this comment

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

Bug: Debug console.log left in test code

A console.log(transactionEvent?.transaction) statement appears to have been left in the test code, likely from debugging. While this is in test code, it will produce unnecessary output during test runs.

Fix in Cursor Fix in Web

Comment on lines +64 to +74
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());
},
);

This comment was marked as outdated.

Copy link
Member Author

Choose a reason for hiding this comment

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

this is just relevant for the error case

Comment on lines 67 to 77
},
},
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());
},
);
},
});
}

This comment was marked as outdated.

@github-actions
Copy link
Contributor

github-actions bot commented Dec 12, 2025

size-limit report 📦

Path Size % Change Change
@sentry/browser 24.81 kB - -
@sentry/browser - with treeshaking flags 23.3 kB - -
@sentry/browser (incl. Tracing) 41.56 kB - -
@sentry/browser (incl. Tracing, Profiling) 46.16 kB - -
@sentry/browser (incl. Tracing, Replay) 79.98 kB - -
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags 69.7 kB - -
@sentry/browser (incl. Tracing, Replay with Canvas) 84.65 kB - -
@sentry/browser (incl. Tracing, Replay, Feedback) 96.9 kB - -
@sentry/browser (incl. Feedback) 41.52 kB - -
@sentry/browser (incl. sendFeedback) 29.49 kB - -
@sentry/browser (incl. FeedbackAsync) 34.48 kB - -
@sentry/react 26.52 kB - -
@sentry/react (incl. Tracing) 43.76 kB - -
@sentry/vue 29.27 kB - -
@sentry/vue (incl. Tracing) 43.37 kB - -
@sentry/svelte 24.82 kB - -
CDN Bundle 27.24 kB - -
CDN Bundle (incl. Tracing) 42.23 kB - -
CDN Bundle (incl. Tracing, Replay) 78.75 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) 84.21 kB - -
CDN Bundle - uncompressed 80.04 kB - -
CDN Bundle (incl. Tracing) - uncompressed 125.39 kB - -
CDN Bundle (incl. Tracing, Replay) - uncompressed 241.42 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed 254.18 kB - -
@sentry/nextjs (client) 45.98 kB - -
@sentry/sveltekit (client) 41.93 kB - -
@sentry/node-core 51.5 kB -0.01% -1 B 🔽
@sentry/node 161.36 kB - -
@sentry/node - without tracing 92.91 kB - -
@sentry/aws-serverless 108.44 kB -0.01% -1 B 🔽

View base workflow run

Copy link
Collaborator

@logaretm logaretm left a comment

Choose a reason for hiding this comment

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

Nice one, looks like the only blocker is Next.js 13 not liking this

}
});

client?.on('spanStart', span => {
Copy link
Collaborator

Choose a reason for hiding this comment

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

l: Since args match we could do:

  client?.on('spanStart', handleOnSpanStart);

Unless you want to add more stuff to it later, just a nitpick.

Comment on lines +28 to 30
const isolationScope = getIsolationScope();

const headersDict = context.headers ? winterCGHeadersToDict(context.headers) : undefined;

This comment was marked as outdated.

Comment on lines 6 to 16
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';

/**
* Wraps an `app` directory server component with Sentry error instrumentation.

This comment was marked as outdated.

activeSpan.updateName(enhancedName);
activeSpan.setAttributes({
'sentry.nextjs.ssr.function.type': segment === PAGE_SEGMENT ? 'Page' : 'Layout',
'sentry.nextjs.ssr.function.route': route,
Copy link

Choose a reason for hiding this comment

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

Bug: Inconsistent type handling for route span attribute

The route variable from rootSpanAttributes[ATTR_HTTP_ROUTE] can be undefined or a non-string type. On line 174, this is properly handled with typeof route === 'string' ? route : '' for the span name. However, on line 178, route is passed directly to setAttributes as 'sentry.nextjs.ssr.function.route': route, which could set the attribute to undefined or an unexpected type instead of being omitted or defaulted consistently.

Fix in Cursor Fix in Web

} else {
span.setStatus({ code: SPAN_STATUS_ERROR, message: 'internal_error' });
}
}
Copy link

Choose a reason for hiding this comment

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

Bug: Navigation errors captured when no active span exists

The navigation error checks (isNotFoundNavigationError and isRedirectNavigationError) that set shouldCapture = false are inside the if (span) block. If getActiveSpan() returns null or undefined, the entire block is skipped and shouldCapture remains true. This causes navigation errors (not-found and redirect) to be incorrectly captured via captureException when there's no active span, whereas the old code never captured these navigation errors regardless of span existence.

Fix in Cursor Fix in Web

Comment on lines 65 to 75
},
},
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());
},
);
},
});
}
Copy link

Choose a reason for hiding this comment

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

Bug: The refactoring removes manual span creation for server components, but the new error handling logic still expects an active span, causing error statuses to be lost.
Severity: HIGH | Confidence: High

🔍 Detailed Analysis

The refactoring in wrapServerComponentWithSentry.ts removes the manual creation of a Sentry span that was previously done with startSpanManual(). The new implementation uses handleCallbackErrors(), which does not create spans. However, the error handling logic within the new code relies on retrieving an active span via getActiveSpan() to set the error status. Since the span is no longer being created for the server component execution, getActiveSpan() will return null. This results in a failure to record the error status on any span when an error occurs in a server component, leading to a loss of observability and degraded error monitoring for server components.

💡 Suggested Fix

Reintroduce span creation to wrap the server component execution. Before executing the component, create a new span using a function like startSpanManual(), ensuring it has the necessary attributes. This span will then be available to getActiveSpan() within the error handler, allowing the error status to be correctly recorded.

🤖 Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location: packages/nextjs/src/common/wrapServerComponentWithSentry.ts#L35-L75

Potential issue: The refactoring in `wrapServerComponentWithSentry.ts` removes the
manual creation of a Sentry span that was previously done with `startSpanManual()`. The
new implementation uses `handleCallbackErrors()`, which does not create spans. However,
the error handling logic within the new code relies on retrieving an active span via
`getActiveSpan()` to set the error status. Since the span is no longer being created for
the server component execution, `getActiveSpan()` will return `null`. This results in a
failure to record the error status on any span when an error occurs in a server
component, leading to a loss of observability and degraded error monitoring for server
components.

Did we get this right? 👍 / 👎 to inform future reviews.
Reference ID: 7586027

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Remove tracing from App Router Server Components templates

3 participants