Skip to content

feat(http): refactor node:http client instrumentation for portability#20393

Open
isaacs wants to merge 2 commits intodevelopfrom
isaacschlueter/portable-http-integration-client
Open

feat(http): refactor node:http client instrumentation for portability#20393
isaacs wants to merge 2 commits intodevelopfrom
isaacschlueter/portable-http-integration-client

Conversation

@isaacs
Copy link
Copy Markdown
Member

@isaacs isaacs commented Apr 20, 2026

Refactor the node:http outgoing request instrumentation so that it can
be applied to non-Node.js environments by patching the http module.

Also, refactor so that the diagnostics_channel and monkeypatching paths
can share code, and so that light and normal node-core instrumentations
can share more of the functionality as well.

To facilitate this, some portable minimal types are vendored in from the
node:http module.

  • If you've added code that should be tested, please add tests.
  • Ensure your code lints and the test suite passes (yarn lint) & (yarn test).
  • Link an issue if there is one related to your pull request. If no issue is linked, one will be auto-generated and linked.

Notes on test changes:

  • The test changes are mostly owing to the fact that more versions are covered by some of the conditions, so the test gets un-indented, because it was previously conditionalTest on a node version.
  • The origin span data changes from the vague (and now incorrect) auto.http.otel.http to auto.http.client for client spans. This also affects a lot of the tests.

Otherwise, all prior behavior should be unchanged, which is reflected in the integration and unit tests.

@isaacs isaacs force-pushed the isaacschlueter/portable-http-integration-client branch from 1f9cef2 to 5ab332a Compare April 20, 2026 01:47
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 20, 2026

size-limit report 📦

Path Size % Change Change
@sentry/browser 25.88 kB -0.01% -1 B 🔽
@sentry/browser - with treeshaking flags 24.35 kB - -
@sentry/browser (incl. Tracing) 43.8 kB -0.01% -1 B 🔽
@sentry/browser (incl. Tracing + Span Streaming) 45.5 kB - -
@sentry/browser (incl. Tracing, Profiling) 48.73 kB - -
@sentry/browser (incl. Tracing, Replay) 82.98 kB -0.01% -1 B 🔽
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags 72.5 kB - -
@sentry/browser (incl. Tracing, Replay with Canvas) 87.67 kB - -
@sentry/browser (incl. Tracing, Replay, Feedback) 99.93 kB -0.01% -1 B 🔽
@sentry/browser (incl. Feedback) 42.7 kB - -
@sentry/browser (incl. sendFeedback) 30.55 kB -0.01% -1 B 🔽
@sentry/browser (incl. FeedbackAsync) 35.55 kB -0.01% -1 B 🔽
@sentry/browser (incl. Metrics) 27.16 kB - -
@sentry/browser (incl. Logs) 27.29 kB -0.01% -1 B 🔽
@sentry/browser (incl. Metrics & Logs) 27.98 kB - -
@sentry/react 27.62 kB - -
@sentry/react (incl. Tracing) 46.05 kB - -
@sentry/vue 30.71 kB - -
@sentry/vue (incl. Tracing) 45.62 kB - -
@sentry/svelte 25.89 kB - -
CDN Bundle 28.57 kB -0.02% -3 B 🔽
CDN Bundle (incl. Tracing) 46.07 kB -0.01% -2 B 🔽
CDN Bundle (incl. Logs, Metrics) 29.95 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) 47.12 kB +0.01% +1 B 🔺
CDN Bundle (incl. Replay, Logs, Metrics) 68.92 kB - -
CDN Bundle (incl. Tracing, Replay) 83.13 kB -0.01% -5 B 🔽
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) 84.17 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) 88.61 kB -0.01% -1 B 🔽
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) 89.69 kB -0.01% -4 B 🔽
CDN Bundle - uncompressed 83.59 kB - -
CDN Bundle (incl. Tracing) - uncompressed 137.62 kB - -
CDN Bundle (incl. Logs, Metrics) - uncompressed 87.73 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed 141.03 kB - -
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed 211.31 kB - -
CDN Bundle (incl. Tracing, Replay) - uncompressed 255.06 kB - -
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed 258.46 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed 267.97 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed 271.36 kB - -
@sentry/nextjs (client) 48.58 kB - -
@sentry/sveltekit (client) 44.22 kB - -
@sentry/node-core 59.02 kB +1.16% +676 B 🔺
@sentry/node 168.01 kB -4.35% -7.64 kB 🔽
@sentry/node - without tracing 72.06 kB -26.69% -26.23 kB 🔽
@sentry/aws-serverless 107.63 kB -6.68% -7.7 kB 🔽

View base workflow run

@isaacs isaacs force-pushed the isaacschlueter/portable-http-integration-client branch 3 times, most recently from 18b93f6 to fd04ed5 Compare April 20, 2026 16:31
isaacs added a commit that referenced this pull request Apr 20, 2026
Refactor the `node:http` outgoing request instrumentation so that it can
be applied to non-Node.js environments by patching the http module.

Also, refactor so that the diagnostics_channel and monkeypatching paths
can share code, and so that light and normal node-core instrumentations
can share more of the functionality as well.

To facilitate this, some portable minimal types are vendored in from the
`node:http` module.
@isaacs isaacs force-pushed the isaacschlueter/portable-http-integration-client branch from 0877c6e to 1963717 Compare April 20, 2026 19:00
@isaacs isaacs marked this pull request as ready for review April 20, 2026 19:00
@isaacs isaacs requested review from andreiborza and mydea April 20, 2026 19:00
Comment thread packages/core/src/integrations/http/client-subscriptions.ts Outdated
isaacs added a commit that referenced this pull request Apr 20, 2026
Refactor the `node:http` outgoing request instrumentation so that it can
be applied to non-Node.js environments by patching the http module.

Also, refactor so that the diagnostics_channel and monkeypatching paths
can share code, and so that light and normal node-core instrumentations
can share more of the functionality as well.

To facilitate this, some portable minimal types are vendored in from the
`node:http` module.
@isaacs isaacs force-pushed the isaacschlueter/portable-http-integration-client branch from 1963717 to 3e7001c Compare April 20, 2026 19:06
Comment thread packages/core/src/integrations/http/client-subscriptions.ts Outdated
Comment thread packages/core/src/integrations/http/instrument-outgoing-request.ts Outdated
isaacs added a commit that referenced this pull request Apr 20, 2026
Refactor the `node:http` outgoing request instrumentation so that it can
be applied to non-Node.js environments by patching the http module.

Also, refactor so that the diagnostics_channel and monkeypatching paths
can share code, and so that light and normal node-core instrumentations
can share more of the functionality as well.

To facilitate this, some portable minimal types are vendored in from the
`node:http` module.
@isaacs isaacs force-pushed the isaacschlueter/portable-http-integration-client branch from 0307e3a to 640f8f0 Compare April 20, 2026 20:12
Comment thread packages/core/src/integrations/http/client-subscriptions.ts Outdated
Comment thread packages/node-core/src/integrations/http/SentryHttpInstrumentation.ts Outdated
Comment thread packages/core/src/integrations/http/client-subscriptions.ts
Comment thread packages/node-core/src/light/integrations/httpIntegration.ts
Comment thread packages/node-core/src/integrations/http/SentryHttpInstrumentation.ts Outdated
isaacs added a commit that referenced this pull request Apr 20, 2026
Refactor the `node:http` outgoing request instrumentation so that it can
be applied to non-Node.js environments by patching the http module.

Also, refactor so that the diagnostics_channel and monkeypatching paths
can share code, and so that light and normal node-core instrumentations
can share more of the functionality as well.

To facilitate this, some portable minimal types are vendored in from the
`node:http` module.
@isaacs isaacs force-pushed the isaacschlueter/portable-http-integration-client branch from 1b8377c to 1a8d51e Compare April 20, 2026 21:52
Comment thread packages/core/src/integrations/http/client-subscriptions.ts
Comment thread packages/node-core/src/utils/outgoingHttpRequest.ts
Comment thread packages/node-core/src/light/integrations/httpIntegration.ts Outdated
isaacs added a commit that referenced this pull request Apr 20, 2026
isaacs added a commit that referenced this pull request Apr 20, 2026
Comment thread .size-limit.js Outdated
isaacs added a commit that referenced this pull request Apr 20, 2026
Refactor the `node:http` outgoing request instrumentation so that it can
be applied to non-Node.js environments by patching the http module.

Also, refactor so that the diagnostics_channel and monkeypatching paths
can share code, and so that light and normal node-core instrumentations
can share more of the functionality as well.

To facilitate this, some portable minimal types are vendored in from the
`node:http` module.
@isaacs isaacs force-pushed the isaacschlueter/portable-http-integration-client branch from 953b012 to 1ef480a Compare April 20, 2026 23:54
Comment thread packages/node-core/src/integrations/http/SentryHttpInstrumentation.ts Outdated
isaacs added a commit that referenced this pull request Apr 21, 2026
Refactor the `node:http` outgoing request instrumentation so that it can
be applied to non-Node.js environments by patching the http module.

Also, refactor so that the diagnostics_channel and monkeypatching paths
can share code, and so that light and normal node-core instrumentations
can share more of the functionality as well.

To facilitate this, some portable minimal types are vendored in from the
`node:http` module.
@isaacs isaacs force-pushed the isaacschlueter/portable-http-integration-client branch from 1ef480a to 13607f2 Compare April 21, 2026 01:04
Comment thread packages/node-core/src/integrations/http/SentryHttpInstrumentation.ts Outdated
Comment thread packages/node-core/src/light/integrations/httpIntegration.ts Outdated
isaacs added a commit that referenced this pull request Apr 21, 2026
Refactor the `node:http` outgoing request instrumentation so that it can
be applied to non-Node.js environments by patching the http module.

Also, refactor so that the diagnostics_channel and monkeypatching paths
can share code, and so that light and normal node-core instrumentations
can share more of the functionality as well.

To facilitate this, some portable minimal types are vendored in from the
`node:http` module.
@isaacs isaacs force-pushed the isaacschlueter/portable-http-integration-client branch from 13607f2 to d8d225c Compare April 21, 2026 03:08
isaacs added a commit that referenced this pull request Apr 21, 2026
Refactor the `node:http` outgoing request instrumentation so that it can
be applied to non-Node.js environments by patching the http module.

Also, refactor so that the diagnostics_channel and monkeypatching paths
can share code, and so that light and normal node-core instrumentations
can share more of the functionality as well.

To facilitate this, some portable minimal types are vendored in from the
`node:http` module.
@isaacs isaacs force-pushed the isaacschlueter/portable-http-integration-client branch from d8d225c to 8ad3dd0 Compare April 21, 2026 16:19
@isaacs isaacs requested a review from JPeer264 April 22, 2026 15:17
isaacs added a commit that referenced this pull request Apr 22, 2026
Refactor the `node:http` outgoing request instrumentation so that it can
be applied to non-Node.js environments by patching the http module.

Also, refactor so that the diagnostics_channel and monkeypatching paths
can share code, and so that light and normal node-core instrumentations
can share more of the functionality as well.

To facilitate this, some portable minimal types are vendored in from the
`node:http` module.
isaacs added a commit that referenced this pull request Apr 22, 2026
Refactor the `node:http` outgoing request instrumentation so that it can
be applied to non-Node.js environments by patching the http module.

Also, refactor so that the diagnostics_channel and monkeypatching paths
can share code, and so that light and normal node-core instrumentations
can share more of the functionality as well.

To facilitate this, some portable minimal types are vendored in from the
`node:http` module.
Copy link
Copy Markdown
Member

@JPeer264 JPeer264 left a comment

Choose a reason for hiding this comment

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

Overall it looks great. I just want to clarify the OTel double instrumentation before approving (it might not be an issue but I want to be sure)

Comment thread packages/core/src/integrations/http/client-subscriptions.ts
// if we do not have earlier access to the request object at creation
// time. The http.client.request.error channel is only available on
// the same node versions as client.request.created, so no help.
if (_options.breadcrumbs && !FULLY_SUPPORTS_HTTP_DIAGNOSTICS_CHANNEL) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

q: Do I understand it correctly, that http.client.request.error has been removed, because http.client.response.finish was here first, so http.client.request.error is kinda useless in our case, as finish did the trick already?!

If so, I wonder what would happen on newer Node versions where http.client.response.finish is not getting triggered - I think I'm missing something

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Ok, I think you're actually noticing a valid point here. (It's very confusing!)

So, the flow is:

  • create ClientRequest object
  • fire http.client.request.created (node >=22.12 only)
  • pass object to user
  • user does things, calls .end() at some point.
  • Headers sent!
  • fire http.client.request.start (all node versions)
  • response arrives from server
  • user consumes response
  • fire http.client.response.finish (all node versions)

("All node versions" means >=18.6, so, all that we support anyway.)

If at some point, the request has an error, it'll fire http.client.request.error, but only on node >=22.6.0 || 20 >=20.17.0.

On node 22 >=22.12.0 || 23 >=23.2.0 || >=24 we have the http.client.request.created, and we attach an errorMonitor to the request object, so there's no need for http.client.request.error on those versions.

On nodes that don't support http.client.request.created, if breadcrumbs are enabled, we generate breadcrumbs on the http.client.response.finish. (This doesn't matter if we attached to request.created, because we do the errorMonitor and prependListener('response') listeners early enough.)

However, the valid point is that the response is not guaranteed, if the request has a socket hangup error or something before the response arrives.

I think the solution is that for old nodes, we attach a listener on the http.client.request.start diagnostics channel. This gets access to the request object, albeit too late to set headers, so no trace propagation. But it can attach the error listener to do breadcrumbs without a response. This covers more node versions than the http.client.request.error channel, and we don't need to have another version range to be checking.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

On newer node versions, we're relying on the fact that the request.created channel listener attaches a response event handler, and then we deal with the response at that point.

* Inject Sentry trace-propagation headers into an outgoing request if the
* target URL matches the configured `tracePropagationTargets`.
*/
export function injectTracePropagationHeaders(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

l: There is already the _INTERNAL_getTracingHeadersForFetchRequest which is quite similar, with the difference that it doesn't apply the headers, but returns them. Theoretically we can try to reuse it, but not really mandatory for this PR.

@@ -0,0 +1,28 @@
import { objectToBaggageHeader, parseBaggageHeader } from '../../utils/baggage';

// TODO: should this be in utils/baggage?
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

l: Yeah good idea, it might make sense there, as these utilities there are quite similar. I would then suggest to move mergeBaggage there and maybe call it mergeBaggageHeader?!

export function getOpenTelemetryInstrumentationToPreload(): (((options?: any) => void) & { id: string })[] {
return [
instrumentSentryHttp,
instrumentOtelHttp,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

q/l: Would that mean we can remove the @opentelemetry/instrumentation-http dependency as well?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Ah, yes, I had thought we needed that for the Server bits for now, but in fact we do not. Good catch!

export function getOpenTelemetryInstrumentationToPreload(): (((options?: any) => void) & { id: string })[] {
return [
instrumentSentryHttp,
instrumentOtelHttp,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

q/m: Do you know what will happen for users that already have an OTel setup? Usually the HTTP instrumentation is on by default when doing auto instrumentation. Before it wouldn't do double instrumentation, as we already registered it, but now we removed this and OTel can now instrument it.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This is a good point! Also, it's going to affect any instrumentations that we migrate from OTel to Sentry for use in other platforms, right?

This does add to the argument in favor of changing the span origin from auto.http.otel.http to auto.http.client (similarly for the express instrumentation). So, if users do start getting duplicate instrumentations, they'll know why.

I'll try to set up a demo app with this scenario, and see if there's a way we can detect the situation or somehow register with OTel in that case.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

this is a good point! IMHO it is not a blocker if we can't get this to work. but maybe we can find a way to at least warn if we detect this somehow… but this may be hard due to timing, as we may be the first or last to wrap. ideal world, if possible:

  1. warn if we wrap and detect that otel has already wrapped it
  2. mark our wrapped stuff in a way that otel will also detect it as wrapped (??? not sure if this is feasible and/or has other problems attached to it…)

Copy link
Copy Markdown
Member

@JPeer264 JPeer264 Apr 24, 2026

Choose a reason for hiding this comment

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

Also, it's going to affect any instrumentations that we migrate from OTel to Sentry for use in other platforms, right?

I think so yes.

This does add to the argument in favor of changing the span origin from auto.http.otel.http to auto.http.client (similarly for the express instrumentation). So, if users do start getting duplicate instrumentations, they'll know why.

Fair point. Maybe we can even set up a document somewhere where we basically have potential double instrumentation and would recommend to not do autoinstrumentation but allow list these instrumentations. Some "best practice" guide or something.

mark our wrapped stuff in a way that otel will also detect it as wrapped

Could work potentially - timing is an issue as you mentioned.

});

request.prependListener('response', response => {
// no longer need this, listen on response now
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

to clarify, if this is prepended then it will def. be called and close can no longer be called?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Well, it's more that once we have a response, the response will either emit end or error at some point, and the breadcrumbs will be emitted at that point. The value of removing the listener is somewhat debatable, though, now that I look at it. It might actually be preferable from a perf point of view to just leave it there, since it's not hurting anyone, and removing it is slightly more work than leaving it in place. 🤔

import type { HttpClientRequest } from './types';

/**
* Inject Sentry trace-propagation headers into an outgoing request if the
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

let's mention here somewhere that this has to be called before the request is started, for future reference?

Copy link
Copy Markdown
Member

@mydea mydea left a comment

Choose a reason for hiding this comment

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

scanned it a bit and it generally looks sweet to me, great work! I'd ask @JPeer264 , @andreiborza and/or @nicohrubec to take some time to review this as it would be ideal to have broader understanding in the team about how these things work together, possibly worth it to do a session to sit together and explain the changes etc!

isaacs added 2 commits April 23, 2026 07:28
This was implemented for the portable Express integration, but others
will need the same functionality, so make it a reusable util.
Refactor the `node:http` outgoing request instrumentation so that it can
be applied to non-Node.js environments by patching the http module.

Also, refactor so that the diagnostics_channel and monkeypatching paths
can share code, and so that light and normal node-core instrumentations
can share more of the functionality as well.

To facilitate this, some portable minimal types are vendored in from the
`node:http` module.
@isaacs isaacs force-pushed the isaacschlueter/portable-http-integration-client branch from 8ad3dd0 to c5a2b94 Compare April 23, 2026 14:33
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit c5a2b94. Configure here.

propagateTrace: options.propagateTraceInOutgoingRequests,
applyCustomAttributesOnSpan,
...options,
spans: options.createSpansForOutgoingRequests ?? options.spans,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Node-core unintentionally creates outgoing HTTP request spans

High Severity

The node-core httpIntegration never sets createSpansForOutgoingRequests or spans in its SentryHttpInstrumentationOptions. After the refactor, patchOptions.spans resolves to undefined ?? undefined, which causes getHttpClientSubscriptions to fall back to hasSpansEnabled(clientOptions). When tracing is enabled, this now creates outgoing request spans — a behavior that never existed before in node-core. The old code used createSpansForOutgoingRequests && (spans ?? true), which was always falsy when createSpansForOutgoingRequests was unset.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit c5a2b94. Configure here.

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.

3 participants