Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

fix(astro): Ensure traces are correctly propagated for static routes #17536

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
mydea merged 7 commits into develop from fn/astro-fixes
Sep 8, 2025

Conversation

Copy link
Member

@mydea mydea commented Sep 5, 2025

This refactors the astro middleware a bit to be more correct & future proof.

Right now, http.server spans are not emitted by the node http integration because import-in-the-middle does not work with Astro. So we emit our own. This PR makes astro ready to also handle the base http.server spans and enhance them with routing information.

It also handles static routes better: these do not emit spans anymore now, and do not continue traces, but instead only propagate the parametrized route to the client, no trace data.

One fundamental problem remains that is not fixed in this PR: Sentry.init() is only injected via page-ssr into SSR pages. This means that only once any SSR page is hit, the server-side part of Sentry is initialized. If you hit a prerendered (static) page first, sentry will not be initialized and thus neither errors are captured nor spans created. For regular prerendered pages this should be fine because we do not need to run anything at runtime there. However, when you hit a prerendered page that has a server-island, the server island (which is dynamic) will not be instrumented either because Sentry is not initialized yet.

I don't think we can fix this with the current set of primitives we have, so this is a future problem to look into...

@mydea mydea requested review from Lms24 and s1gr1d September 5, 2025 09:21
const rootSpan = activeSpan ? getRootSpan(activeSpan) : undefined;

// if there is an active span, we just want to enhance it with routing data etc.
if (rootSpan && spanToJSON(rootSpan).op === 'http.server') {
Copy link
Member Author

Choose a reason for hiding this comment

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

today this path is never hit as the http integration does not work, but this will become relevant when we adjust this to not need an otel instrumentation anymore. I opted to still make this forwards-compatible change here, for simplicity...

Copy link
Member

Choose a reason for hiding this comment

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

Sounds good to me! Though see my other comment about sub requests. Again, not sure if this is really a concern so we can also just do this for the moment and adjust if necessary.

cursor[bot]

This comment was marked as outdated.

// if there is an active span, we know that this handle call is nested and hence
// we don't create a new domain for it. If we created one, nested server calls would
// create new transactions instead of adding a child span to the currently active span.
if (getActiveSpan()) {
Copy link
Member Author

Choose a reason for hiding this comment

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

I don't know if/where this would happen that the http.server span would be nested, could not find any case of this. Even then, I suppose it is not too bad if the isolation scope would be forked again for the sub-request 🤔

Copy link
Member

Choose a reason for hiding this comment

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

The idea here is that Astro doesn't actually make another network request when you call fetch during SSR and the request targets e.g. an API route on the Astro server. So to avoid the network roundtrip, the framework just programatically re-enters the request lifecycle as a "sub request".

^ now that being said, I don't know if it works like that anymore and the comment suggests that it hasn't been updated in a long time ("domains"). I does work like that still in Sveltekit but obviously that doesn't mean anything 😅

Copy link
Member Author

Choose a reason for hiding this comment

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

yeah, I think that makes sense/could be, but arguably even then it would be OK to fork the isolation scope I'd say and have a dedicated isolation scope for the "sub-request"? 🤔 e.g. if I had (theoretically):

  • GET /page --> isolation scope forked
    • GET /sub-request --> fork isolation scope again here, as that could have it's own tags etc. that should not leak to the general /page request, I suppose

Copy link
Member

Choose a reason for hiding this comment

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

yeah sounds good!

yarn.lock Outdated
@@ -6944,7 +6944,7 @@
mitt "^3.0.0"
Copy link
Member Author

Choose a reason for hiding this comment

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

no idea where this changes come from...

Copy link
Contributor

github-actions bot commented Sep 5, 2025
edited
Loading

size-limit report 📦

⚠️ 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.

Path Size % Change Change
@sentry/browser 24.17 kB - -
@sentry/browser - with treeshaking flags 22.75 kB - -
@sentry/browser (incl. Tracing) 40.14 kB - -
@sentry/browser (incl. Tracing, Replay) 78.5 kB - -
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags 68.26 kB - -
@sentry/browser (incl. Tracing, Replay with Canvas) 83.18 kB - -
@sentry/browser (incl. Tracing, Replay, Feedback) 95.38 kB - -
@sentry/browser (incl. Feedback) 40.91 kB - -
@sentry/browser (incl. sendFeedback) 28.82 kB - -
@sentry/browser (incl. FeedbackAsync) 33.77 kB - -
@sentry/react 25.89 kB - -
@sentry/react (incl. Tracing) 42.11 kB - -
@sentry/vue 28.66 kB - -
@sentry/vue (incl. Tracing) 41.95 kB - -
@sentry/svelte 24.2 kB - -
CDN Bundle 25.76 kB - -
CDN Bundle (incl. Tracing) 40.01 kB - -
CDN Bundle (incl. Tracing, Replay) 76.29 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) 81.75 kB - -
CDN Bundle - uncompressed 75.2 kB - -
CDN Bundle (incl. Tracing) - uncompressed 118.31 kB - -
CDN Bundle (incl. Tracing, Replay) - uncompressed 233.4 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed 246.16 kB - -
@sentry/nextjs (client) 44.14 kB - -
@sentry/sveltekit (client) 40.58 kB - -
@sentry/node-core 49.66 kB - -
@sentry/node 150.7 kB - -
@sentry/node - without tracing 92.23 kB - -
@sentry/aws-serverless 105.53 kB -0.01% -2 B 🔽

View base workflow run

Copy link
Contributor

github-actions bot commented Sep 5, 2025
edited
Loading

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.

Scenario Requests/s % of Baseline Prev. Requests/s Change %
GET Baseline 8,955 - 8,890 +1%
GET With Sentry 1,326 15% 1,331 -0%
GET With Sentry (error only) 5,910 66% 6,018 -2%
POST Baseline 1,214 - 1,208 +0%
POST With Sentry 480 40% 534 -10%
POST With Sentry (error only) 1,060 87% 1,063 -0%
MYSQL Baseline 3,370 - 3,348 +1%
MYSQL With Sentry 450 13% 474 -5%
MYSQL With Sentry (error only) 2,728 81% 2,728 -

View base workflow run

Copy link
Member

@Lms24 Lms24 left a comment

Choose a reason for hiding this comment

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

LGTM! Thanks for refactoring the middleware! Left some remarks but nothing too actionable. RE sub requests: If we don't have tests that cover this scenario I think it's also fine to just live with it right now.

// if there is an active span, we know that this handle call is nested and hence
// we don't create a new domain for it. If we created one, nested server calls would
// create new transactions instead of adding a child span to the currently active span.
if (getActiveSpan()) {
Copy link
Member

Choose a reason for hiding this comment

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

The idea here is that Astro doesn't actually make another network request when you call fetch during SSR and the request targets e.g. an API route on the Astro server. So to avoid the network roundtrip, the framework just programatically re-enters the request lifecycle as a "sub request".

^ now that being said, I don't know if it works like that anymore and the comment suggests that it hasn't been updated in a long time ("domains"). I does work like that still in Sveltekit but obviously that doesn't mean anything 😅

const rootSpan = activeSpan ? getRootSpan(activeSpan) : undefined;

// if there is an active span, we just want to enhance it with routing data etc.
if (rootSpan && spanToJSON(rootSpan).op === 'http.server') {
Copy link
Member

Choose a reason for hiding this comment

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

Sounds good to me! Though see my other comment about sub requests. Again, not sure if this is really a concern so we can also just do this for the moment and adjust if necessary.

Comment on lines +144 to +146
// This is here for backwards compatibility, we used to set this here before
method,
Copy link
Member

Choose a reason for hiding this comment

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

l: shouldnt this be http.method? (likely was wrong beforehand already)

Copy link
Member Author

Choose a reason for hiding this comment

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

so http.method will already be there from the http.server span of http. this is added because we used to have this before, for backwards compatibility 😅

Lms24 reacted with thumbs up emoji
if (options.trackClientIp) {
isolationScope.setUser({ ip_address: ctx.clientAddress });
Copy link
Member

Choose a reason for hiding this comment

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

side-note (not relevant for this PR ): Should we remove this? I don't think we ever set the user in other SDKs, right? This is something, users could easily achieve with a middleware on their own. It's also kinda weird with sendDefaultPii usually gating IP address storing.

Copy link
Member Author

Choose a reason for hiding this comment

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

we do set the ip address in the base http requestDataIntegration handling!

Copy link
Member

Choose a reason for hiding this comment

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

right, I blanked and thought we only set event.request but you're right! Though still, since requestDataIntegration does this anyway, having this logic is redundant and still confusing with trackClientIp vs sendDefaultPii, IMHO

Copy link
Member Author

Choose a reason for hiding this comment

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

it probably makes sense to adjust this in a follow up and look at sendDefaultPii here as well!

@mydea mydea force-pushed the fn/astro-fixes branch 2 times, most recently from 3083981 to 93ec015 Compare September 8, 2025 06:39
cursor[bot]

This comment was marked as outdated.

mydea and others added 7 commits September 8, 2025 09:17
This refactors the astro middleware a bit to be more correct & future proof.
Right now, http.server spans are not emitted by the node http integration because import-in-the-middle does not work with Astro. So we emit our own. This PR makes astro ready to also handle the base http.server spans and enhance them with routing information.
It also handles static routes better: these do not emit spans anymore now, and do not continue traces, but instead only propagate the parametrized route to the client, no trace data.
Co-authored-by: Lukas Stracke <lukas.stracke@sentry.io>
} finally {
await flushIfServerless();
}
}
Copy link

@cursor cursor bot Sep 8, 2025

Choose a reason for hiding this comment

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

Bug: Refactoring Breaks Sentry Data Capture

The refactoring introduced inconsistencies in Sentry data capture. enhanceHttpServerSpan and handleStaticRoute paths are missing client IP and SDK processing metadata (request data) settings. enhanceHttpServerSpan also lacks consistent fallback transaction naming and sentry.source attribute setting when a parametrized route is unavailable, leading to incomplete or inconsistent Sentry events.

Fix in Cursor Fix in Web

@mydea mydea merged commit 5441df9 into develop Sep 8, 2025
163 checks passed
@mydea mydea deleted the fn/astro-fixes branch September 8, 2025 07:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Reviewers

@Lms24 Lms24 Lms24 approved these changes

@seer-by-sentry seer-by-sentry[bot] seer-by-sentry[bot] left review comments

@cursor cursor[bot] cursor[bot] left review comments

@s1gr1d s1gr1d Awaiting requested review from s1gr1d

Assignees
No one assigned
Labels
None yet
Projects
None yet
Milestone
No milestone
Development

Successfully merging this pull request may close these issues.

2 participants

AltStyle によって変換されたページ (->オリジナル) /