feat(remix): Server Timing Headers Trace Propagation#18653
Conversation
There was a problem hiding this comment.
Pull request overview
This PR implements a proof-of-concept for propagating Sentry trace context from server to client using the Server-Timing HTTP header and the browser Performance API. This provides an alternative to meta tag-based trace propagation, particularly useful for streaming SSR responses and edge runtimes.
Key changes:
- Added utilities for generating and injecting Server-Timing headers with Sentry trace data
- Implemented client-side parsing of Server-Timing headers via the Performance API
- Updated server instrumentation to capture and propagate trace context via Server-Timing headers
- Added comprehensive E2E test coverage for both Node.js and Cloudflare environments
Reviewed changes
Copilot reviewed 25 out of 27 changed files in this pull request and generated 16 comments.
Show a summary per file
| File | Description |
|---|---|
packages/remix/src/server/serverTimingTracePropagation.ts |
New utility module for generating Server-Timing headers with trace context |
packages/remix/src/client/serverTimingTracePropagation.ts |
New client-side utilities for parsing trace data from Server-Timing headers |
packages/remix/src/server/instrumentServer.ts |
Updated server instrumentation to inject Server-Timing headers and refactored trace propagation logic |
packages/remix/src/client/performance.tsx |
Updated pageload span initialization to use Server-Timing trace propagation |
packages/remix/src/server/index.ts |
Exported new Server-Timing utilities for public API |
packages/remix/src/client/index.ts |
Exported new client-side Server-Timing utilities |
packages/remix/src/cloudflare/index.ts |
Exported Server-Timing utilities for Cloudflare runtime |
dev-packages/e2e-tests/test-applications/remix-server-timing/* |
New E2E test application validating Server-Timing trace propagation |
dev-packages/e2e-tests/test-applications/remix-hydrogen/* |
Updated Hydrogen test app to demonstrate Cloudflare support |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
...2e-tests/test-applications/remix-server-timing/tests/server-timing-trace-propagation.test.ts
Outdated
Show resolved
Hide resolved
1f07bc8 to
67ec468
Compare
3e06c9b to
48e03dd
Compare
d882ca3 to
982c420
Compare
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.
|
1b1481d to
8a43e90
Compare
...2e-tests/test-applications/remix-server-timing/tests/server-timing-trace-propagation.test.ts
Outdated
Show resolved
Hide resolved
fa61b3c to
7518ec6
Compare
Lms24
left a comment
There was a problem hiding this comment.
@onurtemizkan sorry for the delay on this! I merged in #18673 to get support for server-timing trace continuation on the client side in the general browser SDK. Could you update the PR to use this instead of the remix-specific approach? The server side looks fine to me, though I'll give this a more proper look once the PR is updated.
7518ec6 to
1f7c85c
Compare
Codecov Results 📊Generated by Codecov Action |
dev-packages/e2e-tests/test-applications/remix-hydrogen/app/entry.server.tsx
Outdated
Show resolved
Hide resolved
25fa718 to
1033440
Compare
size-limit report 📦
|
4ae90bf to
4d1a53e
Compare
| } | ||
|
|
||
| return parts.join(', '); | ||
| } catch (e) { |
There was a problem hiding this comment.
Unescaped quotes in Server-Timing desc
Medium Severity
generateSentryServerTimingHeader interpolates sentryTrace and baggage into Server-Timing desc="..." without escaping. If either value ever contains " or \ (for example via non-standard baggage values), the header becomes syntactically invalid and may be dropped or parsed incorrectly by proxies/browsers, breaking trace propagation.
e16a926 to
c88fee9
Compare
|
This pull request has gone three weeks without activity. In another week, I will close it. But! If you comment or otherwise update it, I will reset the clock, and if you apply the label |
c88fee9 to
15fface
Compare
Semver Impact of This PR🟡 Minor (new features) 📋 Changelog PreviewThis is how your changes will appear in the changelog. New Features ✨
Bug Fixes 🐛Deps
Other
Internal Changes 🔧
🤖 This preview updates automatically when you update the PR. |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 2 total unresolved issues (including 1 from previous review).
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Reversed fallback makes scope context path unreachable dead code
- Restored correct order of OR expression to check scope context first before falling back to setSpan, making the fallback path reachable and the scope variable properly utilized.
Or push these changes by commenting:
@cursor push 061834c332
Preview (061834c332)
diff --git a/packages/opentelemetry/src/utils/getTraceData.ts b/packages/opentelemetry/src/utils/getTraceData.ts
--- a/packages/opentelemetry/src/utils/getTraceData.ts
+++ b/packages/opentelemetry/src/utils/getTraceData.ts
@@ -24,7 +24,7 @@
if (span) {
const { scope } = getCapturedScopesOnSpan(span);
// fall back to current context if for whatever reason we can't find the one of the span
- ctx = api.trace.setSpan(api.context.active(), span) || (scope && getContextFromScope(scope));
+ ctx = (scope && getContextFromScope(scope)) || api.trace.setSpan(api.context.active(), span);
}
const { traceId, spanId, sampled, dynamicSamplingContext } = getInjectionData(ctx, { scope, client });This Bugbot Autofix run was free. To enable autofix for future PRs, go to the Cursor dashboard.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 2 total unresolved issues (including 1 from previous review).
Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
| response = await origDocumentRequestFunction.call(this, request, ...args); | ||
| } | ||
|
|
||
| if (serverTimingHeader && response instanceof Response) { |
There was a problem hiding this comment.
Uses instanceof Response instead of duck-typed isResponse
Medium Severity
The new check at line 148 uses response instanceof Response to guard Server-Timing injection in the document request wrapper. Every other response check in this file (lines 73, 221, 263, 388) uses the duck-typed isResponse() helper from ../utils/vendor/response, which is already imported on line 40. The isResponse function was adopted from Remix's own source to handle cross-realm and polyfill cases where instanceof fails. Using instanceof Response could silently skip Server-Timing header injection on document responses in Node.js environments with polyfilled or differently-sourced Response constructors.



Adds automatic trace propagation from server to client via the Server-Timing HTTP header for Remix applications. The client-side reading of Server-Timing headers via the Performance API was added in #18673.
Adds:
generateSentryServerTimingHeader(span)public utility that generates a Server-Timing header value containing Sentry trace contextgenerateSentryServerTimingHeader()manually and append the value to the response'sServer-Timingheader in entry.server.tsx (see remix-hydrogen e2e test for example)Works on both Node.js and Cloudflare Workers environments.
Closes #18696