ref(node): Streamline undici (node-fetch) instrumentation - #21650

Merged
logaretm merged 3 commits into
developfrom
awad/js-2395-streamline-opentelemetryinstrumentation-undici
Jun 19, 2026
Merged

ref(node): Streamline undici (node-fetch) instrumentation#21650
logaretm merged 3 commits into
developfrom
awad/js-2395-streamline-opentelemetryinstrumentation-undici

Conversation

@logaretm

@logaretmlogaretm commented Jun 19, 2026

Copy link
Copy Markdown
Member

Streamlines the vendored UndiciInstrumentation (the span-emitting half of the NodeFetch integration) onto Sentry's span APIs.

closes#20743

Refactor the vendored `UndiciInstrumentation` (the span-emitting half of the
NodeFetch integration) to use Sentry's span APIs instead of OpenTelemetry's
tracing/propagation APIs.
- Span creation: `tracer.startSpan` -> `startInactiveSpan({ kind: CLIENT })`
(the span is created on `undici:request:create` and ended later in
`onDone`/`onError`, so it must be inactive).
- Status: `SpanStatusCode` -> `SPAN_STATUS_ERROR`; drop the no-op
`recordException`.
- Trace propagation: replace `propagation.inject(trace.setSpan(...))` with
`withActiveSpan(span, () => getTraceData(...))` gated by
`shouldPropagateTraceForUrl`. This keeps the propagated `sentry-trace`
referencing the http.client span (not the parent), matching prior output.
Passing `{ span }` to `getTraceData` alone is insufficient: for an inactive
span it resolves to the span's captured scope, whose active span is the
parent.
- Drop the OTel metrics (no MeterProvider is wired up) and the dead
`requireParentforSpans` code path (the SDK always passes `false`).
- Replace the diag logger with `debug` + `DEBUG_BUILD`, swap OTel type imports
for `@sentry/core`, inline the semantic-convention constants into a local
`semconv.ts`, and drop the blanket `eslint-disable`s.
The only remaining `@opentelemetry/api` usage is `SpanKind`; the
`@opentelemetry/instrumentation` patching base is kept.
Adds integration coverage for the error path (`internal_error` span) and for
`headersToSpanAttributes` (request + response header capture).
@linear-code

Copy link
Copy Markdown

JS-2395

@github-actions

github-actionsBot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

size-limit report 📦

PathSize% ChangeChange
@sentry/browser27.45 kB--
@sentry/browser - with treeshaking flags25.88 kB--
@sentry/browser (incl. Tracing)45.94 kB--
@sentry/browser (incl. Tracing + Span Streaming)47.7 kB--
@sentry/browser (incl. Tracing, Profiling)50.73 kB--
@sentry/browser (incl. Tracing, Replay)85.14 kB--
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags74.73 kB--
@sentry/browser (incl. Tracing, Replay with Canvas)89.83 kB--
@sentry/browser (incl. Tracing, Replay, Feedback)102.49 kB--
@sentry/browser (incl. Feedback)44.62 kB--
@sentry/browser (incl. sendFeedback)32.25 kB--
@sentry/browser (incl. FeedbackAsync)37.38 kB--
@sentry/browser (incl. Metrics)28.52 kB--
@sentry/browser (incl. Logs)28.76 kB--
@sentry/browser (incl. Metrics & Logs)29.45 kB--
@sentry/react29.25 kB--
@sentry/react (incl. Tracing)48.24 kB--
@sentry/vue32.61 kB--
@sentry/vue (incl. Tracing)47.8 kB--
@sentry/svelte27.48 kB--
CDN Bundle29.84 kB--
CDN Bundle (incl. Tracing)47.85 kB--
CDN Bundle (incl. Logs, Metrics)31.39 kB--
CDN Bundle (incl. Tracing, Logs, Metrics)49.19 kB--
CDN Bundle (incl. Replay, Logs, Metrics)70.7 kB--
CDN Bundle (incl. Tracing, Replay)85.21 kB--
CDN Bundle (incl. Tracing, Replay, Logs, Metrics)86.48 kB--
CDN Bundle (incl. Tracing, Replay, Feedback)91.05 kB--
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics)92.31 kB--
CDN Bundle - uncompressed88.8 kB--
CDN Bundle (incl. Tracing) - uncompressed144.84 kB--
CDN Bundle (incl. Logs, Metrics) - uncompressed93.5 kB--
CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed148.81 kB--
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed218.33 kB--
CDN Bundle (incl. Tracing, Replay) - uncompressed263.7 kB--
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed267.66 kB--
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed277.4 kB--
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed281.35 kB--
@sentry/nextjs (client)50.63 kB--
@sentry/sveltekit (client)46.33 kB--
@sentry/core/server75.85 kB--
@sentry/core/browser63.01 kB--
@sentry/node-core61.63 kB--
@sentry/node123.61 kB-0.24%-285 B 🔽
@sentry/node/import (ESM hook with diagnostics-channel injection)70.05 kB--
@sentry/node/light50.55 kB--
@sentry/node - without tracing73.69 kB-0.59%-435 B 🔽
@sentry/aws-serverless84.89 kB-0.41%-341 B 🔽
@sentry/cloudflare (withSentry) - minified172.9 kB--
@sentry/cloudflare (withSentry)432.48 kB--

View base workflow run

@logaretm
logaretm marked this pull request as ready for review June 19, 2026 00:57
@logaretm
logaretm requested a review from a team as a code ownerJune 19, 2026 00:57
@logaretm
logaretm requested review from JPeer264, andreiborza, isaacs, mydea and nicohrubec and removed request for a team and mydeaJune 19, 2026 00:57
name: requestMethod === '_OTHER' ? 'HTTP' : requestMethod,
kind: SpanKind.CLIENT,
attributes,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Missing http.client sentry.op

High Severity

startInactiveSpan is called without sentry.op (http.client) on the span or in merged attributes. The integration tests and prior OTel export path expect outgoing fetch spans to use op http.client, but native SentrySpan JSON only reads op from attributes and does not infer it from kind or http.request.method.

Fix in CursorFix in Web

Triggered by project rule: PR Review Guidelines for Cursor Bot

Reviewed by Cursor Bugbot for commit 2b3dbd0. Configure here.

name: requestMethod === '_OTHER' ? 'HTTP' : requestMethod,
kind: SpanKind.CLIENT,
attributes,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Fetch span description missing URL

Medium Severity

Outgoing fetch spans are started with name set to the HTTP method only (e.g. GET), while integration tests expect descriptions like GET http://host/path. The OTel exporter used to derive that description from URL attributes and CLIENT kind; that inference no longer runs for these native spans.

Additional Locations (1)
Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit 2b3dbd0. Configure here.

span.recordException(error);
span.setStatus({
code: SpanStatusCode.ERROR,
code: SPAN_STATUS_ERROR,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Error status not internal_error

Medium Severity

On undici:request:error, the span status message is set to the raw error.message, so serialized status is typically the system error string rather than internal_error. The new fetch-error integration test expects status: 'internal_error', consistent with @sentry/core fetch instrumentation.

Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit 2b3dbd0. Configure here.

@mydeamydea left a comment

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 good IMHO, for this case we can actually already de-otelify this right? And just make this a regular integration that registers the channel hooks...? but we can do this in a follow up 👍

@suzunn

Copy link
Copy Markdown
Contributor

I noticed the new coverage exercises the error path and header-to-attribute mapping, but the riskiest behavior in this refactor is the propagation handoff from OTel context APIs to withActiveSpan(span, () => getTraceData(...)). I would add one integration assertion that captures the outgoing sentry-trace/traceparent headers and verifies their parent span id is the newly-created http.client span, not the surrounding transaction span. That would lock down the inactive-span behavior called out in the PR description and make future cleanup around getTraceData({ span }) much less likely to regress distributed trace parenting.

@logaretm

Copy link
Copy Markdown
MemberAuthor

@mydea I think I might as well de-otelify it if the same tests pass

@suzunn I added a test case for what you raised and it passes, no worries.

Drop the @opentelemetry/instrumentation base from the vendored undici
instrumentation. Since undici reports via diagnostics_channel rather than
module patching, InstrumentationBase provided nothing here — it's now a plain
class that the integration subscribes/unsubscribes directly, off OTel's
registerInstrumentations path. Also drops the remaining @opentelemetry/api
usage (SpanKind, inlined) and safeExecuteInTheMiddle (local helper).
Pure refactor: span emission, trace propagation, and breadcrumbs are unchanged.

@cursorcursorBot left a comment

Copy link
Copy Markdown

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.

There are 4 total unresolved issues (including 3 from previous reviews).

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 3962e7f. Configure here.

if (!_undiciInstrumentation) {
_undiciInstrumentation = new UndiciInstrumentation(_getConfigWithDefaults(options));
}
_undiciInstrumentation.enable();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Undici config frozen at first init

Low Severity

UndiciInstrumentation is constructed only when _undiciInstrumentation is first created; later instrumentNodeFetchSpans(options) calls only invoke enable(). Updated integration options (hooks, header mapping, ignore callbacks) from a subsequent setup path are not applied to the singleton instance.

Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit 3962e7f. Configure here.

Comment on lines +60 to +64
function instrumentNodeFetchSpans(options: NodeFetchOptions): void {
if (!_undiciInstrumentation) {
_undiciInstrumentation = new UndiciInstrumentation(_getConfigWithDefaults(options));
}
_undiciInstrumentation.enable();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Bug: The nativeNodeFetchIntegration configuration is frozen after the first initialization. Subsequent calls to Sentry.init() with different options for this integration are silently ignored.
Severity: MEDIUM

Suggested Fix

To allow for configuration updates, implement a setConfig() method on the UndiciInstrumentation class. Then, modify instrumentNodeFetchSpans to call this setConfig() method on the existing singleton instance whenever new options are provided, similar to how generateInstrumentOnce handles configuration updates.

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/node/src/integrations/node-fetch/index.ts#L60-L64
Potential issue: The `nativeNodeFetchIntegration` uses a module-level singleton
`_undiciInstrumentation` that is initialized only once. The `UndiciInstrumentation`
class does not have a method to update its configuration after instantiation.
Consequently, if `Sentry.init()` is called multiple times in the same process with
different options for this integration (e.g., different `headersToSpanAttributes`), the
configuration from the first call is permanently used, and all subsequent configuration
changes are silently ignored. This primarily affects testing scenarios or applications
that re-initialize Sentry.

Did we get this right? 👍 / 👎 to inform future reviews.

@logaretm
logaretm merged commit beb181a into developJun 19, 2026
549 of 552 checks passed
@logaretm
logaretm deleted the awad/js-2395-streamline-opentelemetryinstrumentation-undici branch June 19, 2026 19:55
Sign up for freeto 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.

Streamline @opentelemetry/instrumentation-undici

4 participants

@logaretm@suzunn@mydea@nicohrubec
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

ref(node): Streamline undici (node-fetch) instrumentation - #21650

Merged
logaretm merged 3 commits into
developfrom
awad/js-2395-streamline-opentelemetryinstrumentation-undici
Jun 19, 2026
Merged

ref(node): Streamline undici (node-fetch) instrumentation#21650
logaretm merged 3 commits into
developfrom
awad/js-2395-streamline-opentelemetryinstrumentation-undici

Conversation

@logaretm

@logaretmlogaretm commented Jun 19, 2026

Copy link
Copy Markdown
Member

Streamlines the vendored UndiciInstrumentation (the span-emitting half of the NodeFetch integration) onto Sentry's span APIs.

closes#20743

Refactor the vendored `UndiciInstrumentation` (the span-emitting half of the
NodeFetch integration) to use Sentry's span APIs instead of OpenTelemetry's
tracing/propagation APIs.
- Span creation: `tracer.startSpan` -> `startInactiveSpan({ kind: CLIENT })`
(the span is created on `undici:request:create` and ended later in
`onDone`/`onError`, so it must be inactive).
- Status: `SpanStatusCode` -> `SPAN_STATUS_ERROR`; drop the no-op
`recordException`.
- Trace propagation: replace `propagation.inject(trace.setSpan(...))` with
`withActiveSpan(span, () => getTraceData(...))` gated by
`shouldPropagateTraceForUrl`. This keeps the propagated `sentry-trace`
referencing the http.client span (not the parent), matching prior output.
Passing `{ span }` to `getTraceData` alone is insufficient: for an inactive
span it resolves to the span's captured scope, whose active span is the
parent.
- Drop the OTel metrics (no MeterProvider is wired up) and the dead
`requireParentforSpans` code path (the SDK always passes `false`).
- Replace the diag logger with `debug` + `DEBUG_BUILD`, swap OTel type imports
for `@sentry/core`, inline the semantic-convention constants into a local
`semconv.ts`, and drop the blanket `eslint-disable`s.
The only remaining `@opentelemetry/api` usage is `SpanKind`; the
`@opentelemetry/instrumentation` patching base is kept.
Adds integration coverage for the error path (`internal_error` span) and for
`headersToSpanAttributes` (request + response header capture).
@linear-code

Copy link
Copy Markdown

JS-2395

@github-actions

github-actionsBot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

size-limit report 📦

PathSize% ChangeChange
@sentry/browser27.45 kB--
@sentry/browser - with treeshaking flags25.88 kB--
@sentry/browser (incl. Tracing)45.94 kB--
@sentry/browser (incl. Tracing + Span Streaming)47.7 kB--
@sentry/browser (incl. Tracing, Profiling)50.73 kB--
@sentry/browser (incl. Tracing, Replay)85.14 kB--
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags74.73 kB--
@sentry/browser (incl. Tracing, Replay with Canvas)89.83 kB--
@sentry/browser (incl. Tracing, Replay, Feedback)102.49 kB--
@sentry/browser (incl. Feedback)44.62 kB--
@sentry/browser (incl. sendFeedback)32.25 kB--
@sentry/browser (incl. FeedbackAsync)37.38 kB--
@sentry/browser (incl. Metrics)28.52 kB--
@sentry/browser (incl. Logs)28.76 kB--
@sentry/browser (incl. Metrics & Logs)29.45 kB--
@sentry/react29.25 kB--
@sentry/react (incl. Tracing)48.24 kB--
@sentry/vue32.61 kB--
@sentry/vue (incl. Tracing)47.8 kB--
@sentry/svelte27.48 kB--
CDN Bundle29.84 kB--
CDN Bundle (incl. Tracing)47.85 kB--
CDN Bundle (incl. Logs, Metrics)31.39 kB--
CDN Bundle (incl. Tracing, Logs, Metrics)49.19 kB--
CDN Bundle (incl. Replay, Logs, Metrics)70.7 kB--
CDN Bundle (incl. Tracing, Replay)85.21 kB--
CDN Bundle (incl. Tracing, Replay, Logs, Metrics)86.48 kB--
CDN Bundle (incl. Tracing, Replay, Feedback)91.05 kB--
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics)92.31 kB--
CDN Bundle - uncompressed88.8 kB--
CDN Bundle (incl. Tracing) - uncompressed144.84 kB--
CDN Bundle (incl. Logs, Metrics) - uncompressed93.5 kB--
CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed148.81 kB--
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed218.33 kB--
CDN Bundle (incl. Tracing, Replay) - uncompressed263.7 kB--
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed267.66 kB--
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed277.4 kB--
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed281.35 kB--
@sentry/nextjs (client)50.63 kB--
@sentry/sveltekit (client)46.33 kB--
@sentry/core/server75.85 kB--
@sentry/core/browser63.01 kB--
@sentry/node-core61.63 kB--
@sentry/node123.61 kB-0.24%-285 B 🔽
@sentry/node/import (ESM hook with diagnostics-channel injection)70.05 kB--
@sentry/node/light50.55 kB--
@sentry/node - without tracing73.69 kB-0.59%-435 B 🔽
@sentry/aws-serverless84.89 kB-0.41%-341 B 🔽
@sentry/cloudflare (withSentry) - minified172.9 kB--
@sentry/cloudflare (withSentry)432.48 kB--

View base workflow run

@logaretm
logaretm marked this pull request as ready for review June 19, 2026 00:57
@logaretm
logaretm requested a review from a team as a code ownerJune 19, 2026 00:57
@logaretm
logaretm requested review from JPeer264, andreiborza, isaacs, mydea and nicohrubec and removed request for a team and mydeaJune 19, 2026 00:57
name: requestMethod === '_OTHER' ? 'HTTP' : requestMethod,
kind: SpanKind.CLIENT,
attributes,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Missing http.client sentry.op

High Severity

startInactiveSpan is called without sentry.op (http.client) on the span or in merged attributes. The integration tests and prior OTel export path expect outgoing fetch spans to use op http.client, but native SentrySpan JSON only reads op from attributes and does not infer it from kind or http.request.method.

Fix in CursorFix in Web

Triggered by project rule: PR Review Guidelines for Cursor Bot

Reviewed by Cursor Bugbot for commit 2b3dbd0. Configure here.

name: requestMethod === '_OTHER' ? 'HTTP' : requestMethod,
kind: SpanKind.CLIENT,
attributes,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Fetch span description missing URL

Medium Severity

Outgoing fetch spans are started with name set to the HTTP method only (e.g. GET), while integration tests expect descriptions like GET http://host/path. The OTel exporter used to derive that description from URL attributes and CLIENT kind; that inference no longer runs for these native spans.

Additional Locations (1)
Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit 2b3dbd0. Configure here.

span.recordException(error);
span.setStatus({
code: SpanStatusCode.ERROR,
code: SPAN_STATUS_ERROR,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Error status not internal_error

Medium Severity

On undici:request:error, the span status message is set to the raw error.message, so serialized status is typically the system error string rather than internal_error. The new fetch-error integration test expects status: 'internal_error', consistent with @sentry/core fetch instrumentation.

Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit 2b3dbd0. Configure here.

@mydeamydea left a comment

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 good IMHO, for this case we can actually already de-otelify this right? And just make this a regular integration that registers the channel hooks...? but we can do this in a follow up 👍

@suzunn

Copy link
Copy Markdown
Contributor

I noticed the new coverage exercises the error path and header-to-attribute mapping, but the riskiest behavior in this refactor is the propagation handoff from OTel context APIs to withActiveSpan(span, () => getTraceData(...)). I would add one integration assertion that captures the outgoing sentry-trace/traceparent headers and verifies their parent span id is the newly-created http.client span, not the surrounding transaction span. That would lock down the inactive-span behavior called out in the PR description and make future cleanup around getTraceData({ span }) much less likely to regress distributed trace parenting.

@logaretm

Copy link
Copy Markdown
MemberAuthor

@mydea I think I might as well de-otelify it if the same tests pass

@suzunn I added a test case for what you raised and it passes, no worries.

Drop the @opentelemetry/instrumentation base from the vendored undici
instrumentation. Since undici reports via diagnostics_channel rather than
module patching, InstrumentationBase provided nothing here — it's now a plain
class that the integration subscribes/unsubscribes directly, off OTel's
registerInstrumentations path. Also drops the remaining @opentelemetry/api
usage (SpanKind, inlined) and safeExecuteInTheMiddle (local helper).
Pure refactor: span emission, trace propagation, and breadcrumbs are unchanged.

@cursorcursorBot left a comment

Copy link
Copy Markdown

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.

There are 4 total unresolved issues (including 3 from previous reviews).

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 3962e7f. Configure here.

if (!_undiciInstrumentation) {
_undiciInstrumentation = new UndiciInstrumentation(_getConfigWithDefaults(options));
}
_undiciInstrumentation.enable();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Undici config frozen at first init

Low Severity

UndiciInstrumentation is constructed only when _undiciInstrumentation is first created; later instrumentNodeFetchSpans(options) calls only invoke enable(). Updated integration options (hooks, header mapping, ignore callbacks) from a subsequent setup path are not applied to the singleton instance.

Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit 3962e7f. Configure here.

Comment on lines +60 to +64
function instrumentNodeFetchSpans(options: NodeFetchOptions): void {
if (!_undiciInstrumentation) {
_undiciInstrumentation = new UndiciInstrumentation(_getConfigWithDefaults(options));
}
_undiciInstrumentation.enable();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Bug: The nativeNodeFetchIntegration configuration is frozen after the first initialization. Subsequent calls to Sentry.init() with different options for this integration are silently ignored.
Severity: MEDIUM

Suggested Fix

To allow for configuration updates, implement a setConfig() method on the UndiciInstrumentation class. Then, modify instrumentNodeFetchSpans to call this setConfig() method on the existing singleton instance whenever new options are provided, similar to how generateInstrumentOnce handles configuration updates.

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/node/src/integrations/node-fetch/index.ts#L60-L64
Potential issue: The `nativeNodeFetchIntegration` uses a module-level singleton
`_undiciInstrumentation` that is initialized only once. The `UndiciInstrumentation`
class does not have a method to update its configuration after instantiation.
Consequently, if `Sentry.init()` is called multiple times in the same process with
different options for this integration (e.g., different `headersToSpanAttributes`), the
configuration from the first call is permanently used, and all subsequent configuration
changes are silently ignored. This primarily affects testing scenarios or applications
that re-initialize Sentry.

Did we get this right? 👍 / 👎 to inform future reviews.

@logaretm
logaretm merged commit beb181a into developJun 19, 2026
549 of 552 checks passed
@logaretm
logaretm deleted the awad/js-2395-streamline-opentelemetryinstrumentation-undici branch June 19, 2026 19:55
Sign up for freeto 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.

Streamline @opentelemetry/instrumentation-undici

4 participants

@logaretm@suzunn@mydea@nicohrubec
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

ref(node): Streamline undici (node-fetch) instrumentation - #21650

Merged
logaretm merged 3 commits into
developfrom
awad/js-2395-streamline-opentelemetryinstrumentation-undici
Jun 19, 2026
Merged

ref(node): Streamline undici (node-fetch) instrumentation#21650
logaretm merged 3 commits into
developfrom
awad/js-2395-streamline-opentelemetryinstrumentation-undici

Conversation

@logaretm

@logaretmlogaretm commented Jun 19, 2026

Copy link
Copy Markdown
Member

Streamlines the vendored UndiciInstrumentation (the span-emitting half of the NodeFetch integration) onto Sentry's span APIs.

closes#20743

Refactor the vendored `UndiciInstrumentation` (the span-emitting half of the
NodeFetch integration) to use Sentry's span APIs instead of OpenTelemetry's
tracing/propagation APIs.
- Span creation: `tracer.startSpan` -> `startInactiveSpan({ kind: CLIENT })`
(the span is created on `undici:request:create` and ended later in
`onDone`/`onError`, so it must be inactive).
- Status: `SpanStatusCode` -> `SPAN_STATUS_ERROR`; drop the no-op
`recordException`.
- Trace propagation: replace `propagation.inject(trace.setSpan(...))` with
`withActiveSpan(span, () => getTraceData(...))` gated by
`shouldPropagateTraceForUrl`. This keeps the propagated `sentry-trace`
referencing the http.client span (not the parent), matching prior output.
Passing `{ span }` to `getTraceData` alone is insufficient: for an inactive
span it resolves to the span's captured scope, whose active span is the
parent.
- Drop the OTel metrics (no MeterProvider is wired up) and the dead
`requireParentforSpans` code path (the SDK always passes `false`).
- Replace the diag logger with `debug` + `DEBUG_BUILD`, swap OTel type imports
for `@sentry/core`, inline the semantic-convention constants into a local
`semconv.ts`, and drop the blanket `eslint-disable`s.
The only remaining `@opentelemetry/api` usage is `SpanKind`; the
`@opentelemetry/instrumentation` patching base is kept.
Adds integration coverage for the error path (`internal_error` span) and for
`headersToSpanAttributes` (request + response header capture).
@linear-code

Copy link
Copy Markdown

JS-2395

@github-actions

github-actionsBot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

size-limit report 📦

PathSize% ChangeChange
@sentry/browser27.45 kB--
@sentry/browser - with treeshaking flags25.88 kB--
@sentry/browser (incl. Tracing)45.94 kB--
@sentry/browser (incl. Tracing + Span Streaming)47.7 kB--
@sentry/browser (incl. Tracing, Profiling)50.73 kB--
@sentry/browser (incl. Tracing, Replay)85.14 kB--
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags74.73 kB--
@sentry/browser (incl. Tracing, Replay with Canvas)89.83 kB--
@sentry/browser (incl. Tracing, Replay, Feedback)102.49 kB--
@sentry/browser (incl. Feedback)44.62 kB--
@sentry/browser (incl. sendFeedback)32.25 kB--
@sentry/browser (incl. FeedbackAsync)37.38 kB--
@sentry/browser (incl. Metrics)28.52 kB--
@sentry/browser (incl. Logs)28.76 kB--
@sentry/browser (incl. Metrics & Logs)29.45 kB--
@sentry/react29.25 kB--
@sentry/react (incl. Tracing)48.24 kB--
@sentry/vue32.61 kB--
@sentry/vue (incl. Tracing)47.8 kB--
@sentry/svelte27.48 kB--
CDN Bundle29.84 kB--
CDN Bundle (incl. Tracing)47.85 kB--
CDN Bundle (incl. Logs, Metrics)31.39 kB--
CDN Bundle (incl. Tracing, Logs, Metrics)49.19 kB--
CDN Bundle (incl. Replay, Logs, Metrics)70.7 kB--
CDN Bundle (incl. Tracing, Replay)85.21 kB--
CDN Bundle (incl. Tracing, Replay, Logs, Metrics)86.48 kB--
CDN Bundle (incl. Tracing, Replay, Feedback)91.05 kB--
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics)92.31 kB--
CDN Bundle - uncompressed88.8 kB--
CDN Bundle (incl. Tracing) - uncompressed144.84 kB--
CDN Bundle (incl. Logs, Metrics) - uncompressed93.5 kB--
CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed148.81 kB--
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed218.33 kB--
CDN Bundle (incl. Tracing, Replay) - uncompressed263.7 kB--
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed267.66 kB--
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed277.4 kB--
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed281.35 kB--
@sentry/nextjs (client)50.63 kB--
@sentry/sveltekit (client)46.33 kB--
@sentry/core/server75.85 kB--
@sentry/core/browser63.01 kB--
@sentry/node-core61.63 kB--
@sentry/node123.61 kB-0.24%-285 B 🔽
@sentry/node/import (ESM hook with diagnostics-channel injection)70.05 kB--
@sentry/node/light50.55 kB--
@sentry/node - without tracing73.69 kB-0.59%-435 B 🔽
@sentry/aws-serverless84.89 kB-0.41%-341 B 🔽
@sentry/cloudflare (withSentry) - minified172.9 kB--
@sentry/cloudflare (withSentry)432.48 kB--

View base workflow run

@logaretm
logaretm marked this pull request as ready for review June 19, 2026 00:57
@logaretm
logaretm requested a review from a team as a code ownerJune 19, 2026 00:57
@logaretm
logaretm requested review from JPeer264, andreiborza, isaacs, mydea and nicohrubec and removed request for a team and mydeaJune 19, 2026 00:57
name: requestMethod === '_OTHER' ? 'HTTP' : requestMethod,
kind: SpanKind.CLIENT,
attributes,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Missing http.client sentry.op

High Severity

startInactiveSpan is called without sentry.op (http.client) on the span or in merged attributes. The integration tests and prior OTel export path expect outgoing fetch spans to use op http.client, but native SentrySpan JSON only reads op from attributes and does not infer it from kind or http.request.method.

Fix in CursorFix in Web

Triggered by project rule: PR Review Guidelines for Cursor Bot

Reviewed by Cursor Bugbot for commit 2b3dbd0. Configure here.

name: requestMethod === '_OTHER' ? 'HTTP' : requestMethod,
kind: SpanKind.CLIENT,
attributes,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Fetch span description missing URL

Medium Severity

Outgoing fetch spans are started with name set to the HTTP method only (e.g. GET), while integration tests expect descriptions like GET http://host/path. The OTel exporter used to derive that description from URL attributes and CLIENT kind; that inference no longer runs for these native spans.

Additional Locations (1)
Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit 2b3dbd0. Configure here.

span.recordException(error);
span.setStatus({
code: SpanStatusCode.ERROR,
code: SPAN_STATUS_ERROR,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Error status not internal_error

Medium Severity

On undici:request:error, the span status message is set to the raw error.message, so serialized status is typically the system error string rather than internal_error. The new fetch-error integration test expects status: 'internal_error', consistent with @sentry/core fetch instrumentation.

Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit 2b3dbd0. Configure here.

@mydeamydea left a comment

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 good IMHO, for this case we can actually already de-otelify this right? And just make this a regular integration that registers the channel hooks...? but we can do this in a follow up 👍

@suzunn

Copy link
Copy Markdown
Contributor

I noticed the new coverage exercises the error path and header-to-attribute mapping, but the riskiest behavior in this refactor is the propagation handoff from OTel context APIs to withActiveSpan(span, () => getTraceData(...)). I would add one integration assertion that captures the outgoing sentry-trace/traceparent headers and verifies their parent span id is the newly-created http.client span, not the surrounding transaction span. That would lock down the inactive-span behavior called out in the PR description and make future cleanup around getTraceData({ span }) much less likely to regress distributed trace parenting.

@logaretm

Copy link
Copy Markdown
MemberAuthor

@mydea I think I might as well de-otelify it if the same tests pass

@suzunn I added a test case for what you raised and it passes, no worries.

Drop the @opentelemetry/instrumentation base from the vendored undici
instrumentation. Since undici reports via diagnostics_channel rather than
module patching, InstrumentationBase provided nothing here — it's now a plain
class that the integration subscribes/unsubscribes directly, off OTel's
registerInstrumentations path. Also drops the remaining @opentelemetry/api
usage (SpanKind, inlined) and safeExecuteInTheMiddle (local helper).
Pure refactor: span emission, trace propagation, and breadcrumbs are unchanged.

@cursorcursorBot left a comment

Copy link
Copy Markdown

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.

There are 4 total unresolved issues (including 3 from previous reviews).

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 3962e7f. Configure here.

if (!_undiciInstrumentation) {
_undiciInstrumentation = new UndiciInstrumentation(_getConfigWithDefaults(options));
}
_undiciInstrumentation.enable();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Undici config frozen at first init

Low Severity

UndiciInstrumentation is constructed only when _undiciInstrumentation is first created; later instrumentNodeFetchSpans(options) calls only invoke enable(). Updated integration options (hooks, header mapping, ignore callbacks) from a subsequent setup path are not applied to the singleton instance.

Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit 3962e7f. Configure here.

Comment on lines +60 to +64
function instrumentNodeFetchSpans(options: NodeFetchOptions): void {
if (!_undiciInstrumentation) {
_undiciInstrumentation = new UndiciInstrumentation(_getConfigWithDefaults(options));
}
_undiciInstrumentation.enable();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Bug: The nativeNodeFetchIntegration configuration is frozen after the first initialization. Subsequent calls to Sentry.init() with different options for this integration are silently ignored.
Severity: MEDIUM

Suggested Fix

To allow for configuration updates, implement a setConfig() method on the UndiciInstrumentation class. Then, modify instrumentNodeFetchSpans to call this setConfig() method on the existing singleton instance whenever new options are provided, similar to how generateInstrumentOnce handles configuration updates.

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/node/src/integrations/node-fetch/index.ts#L60-L64
Potential issue: The `nativeNodeFetchIntegration` uses a module-level singleton
`_undiciInstrumentation` that is initialized only once. The `UndiciInstrumentation`
class does not have a method to update its configuration after instantiation.
Consequently, if `Sentry.init()` is called multiple times in the same process with
different options for this integration (e.g., different `headersToSpanAttributes`), the
configuration from the first call is permanently used, and all subsequent configuration
changes are silently ignored. This primarily affects testing scenarios or applications
that re-initialize Sentry.

Did we get this right? 👍 / 👎 to inform future reviews.

@logaretm
logaretm merged commit beb181a into developJun 19, 2026
549 of 552 checks passed
@logaretm
logaretm deleted the awad/js-2395-streamline-opentelemetryinstrumentation-undici branch June 19, 2026 19:55
Sign up for freeto 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.

Streamline @opentelemetry/instrumentation-undici

4 participants

@logaretm@suzunn@mydea@nicohrubec
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

ref(node): Streamline undici (node-fetch) instrumentation - #21650

Merged
logaretm merged 3 commits into
developfrom
awad/js-2395-streamline-opentelemetryinstrumentation-undici
Jun 19, 2026
Merged

ref(node): Streamline undici (node-fetch) instrumentation#21650
logaretm merged 3 commits into
developfrom
awad/js-2395-streamline-opentelemetryinstrumentation-undici

Conversation

@logaretm

@logaretmlogaretm commented Jun 19, 2026

Copy link
Copy Markdown
Member

Streamlines the vendored UndiciInstrumentation (the span-emitting half of the NodeFetch integration) onto Sentry's span APIs.

closes#20743

Refactor the vendored `UndiciInstrumentation` (the span-emitting half of the
NodeFetch integration) to use Sentry's span APIs instead of OpenTelemetry's
tracing/propagation APIs.
- Span creation: `tracer.startSpan` -> `startInactiveSpan({ kind: CLIENT })`
(the span is created on `undici:request:create` and ended later in
`onDone`/`onError`, so it must be inactive).
- Status: `SpanStatusCode` -> `SPAN_STATUS_ERROR`; drop the no-op
`recordException`.
- Trace propagation: replace `propagation.inject(trace.setSpan(...))` with
`withActiveSpan(span, () => getTraceData(...))` gated by
`shouldPropagateTraceForUrl`. This keeps the propagated `sentry-trace`
referencing the http.client span (not the parent), matching prior output.
Passing `{ span }` to `getTraceData` alone is insufficient: for an inactive
span it resolves to the span's captured scope, whose active span is the
parent.
- Drop the OTel metrics (no MeterProvider is wired up) and the dead
`requireParentforSpans` code path (the SDK always passes `false`).
- Replace the diag logger with `debug` + `DEBUG_BUILD`, swap OTel type imports
for `@sentry/core`, inline the semantic-convention constants into a local
`semconv.ts`, and drop the blanket `eslint-disable`s.
The only remaining `@opentelemetry/api` usage is `SpanKind`; the
`@opentelemetry/instrumentation` patching base is kept.
Adds integration coverage for the error path (`internal_error` span) and for
`headersToSpanAttributes` (request + response header capture).
@linear-code

Copy link
Copy Markdown

JS-2395

@github-actions

github-actionsBot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

size-limit report 📦

PathSize% ChangeChange
@sentry/browser27.45 kB--
@sentry/browser - with treeshaking flags25.88 kB--
@sentry/browser (incl. Tracing)45.94 kB--
@sentry/browser (incl. Tracing + Span Streaming)47.7 kB--
@sentry/browser (incl. Tracing, Profiling)50.73 kB--
@sentry/browser (incl. Tracing, Replay)85.14 kB--
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags74.73 kB--
@sentry/browser (incl. Tracing, Replay with Canvas)89.83 kB--
@sentry/browser (incl. Tracing, Replay, Feedback)102.49 kB--
@sentry/browser (incl. Feedback)44.62 kB--
@sentry/browser (incl. sendFeedback)32.25 kB--
@sentry/browser (incl. FeedbackAsync)37.38 kB--
@sentry/browser (incl. Metrics)28.52 kB--
@sentry/browser (incl. Logs)28.76 kB--
@sentry/browser (incl. Metrics & Logs)29.45 kB--
@sentry/react29.25 kB--
@sentry/react (incl. Tracing)48.24 kB--
@sentry/vue32.61 kB--
@sentry/vue (incl. Tracing)47.8 kB--
@sentry/svelte27.48 kB--
CDN Bundle29.84 kB--
CDN Bundle (incl. Tracing)47.85 kB--
CDN Bundle (incl. Logs, Metrics)31.39 kB--
CDN Bundle (incl. Tracing, Logs, Metrics)49.19 kB--
CDN Bundle (incl. Replay, Logs, Metrics)70.7 kB--
CDN Bundle (incl. Tracing, Replay)85.21 kB--
CDN Bundle (incl. Tracing, Replay, Logs, Metrics)86.48 kB--
CDN Bundle (incl. Tracing, Replay, Feedback)91.05 kB--
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics)92.31 kB--
CDN Bundle - uncompressed88.8 kB--
CDN Bundle (incl. Tracing) - uncompressed144.84 kB--
CDN Bundle (incl. Logs, Metrics) - uncompressed93.5 kB--
CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed148.81 kB--
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed218.33 kB--
CDN Bundle (incl. Tracing, Replay) - uncompressed263.7 kB--
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed267.66 kB--
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed277.4 kB--
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed281.35 kB--
@sentry/nextjs (client)50.63 kB--
@sentry/sveltekit (client)46.33 kB--
@sentry/core/server75.85 kB--
@sentry/core/browser63.01 kB--
@sentry/node-core61.63 kB--
@sentry/node123.61 kB-0.24%-285 B 🔽
@sentry/node/import (ESM hook with diagnostics-channel injection)70.05 kB--
@sentry/node/light50.55 kB--
@sentry/node - without tracing73.69 kB-0.59%-435 B 🔽
@sentry/aws-serverless84.89 kB-0.41%-341 B 🔽
@sentry/cloudflare (withSentry) - minified172.9 kB--
@sentry/cloudflare (withSentry)432.48 kB--

View base workflow run

@logaretm
logaretm marked this pull request as ready for review June 19, 2026 00:57
@logaretm
logaretm requested a review from a team as a code ownerJune 19, 2026 00:57
@logaretm
logaretm requested review from JPeer264, andreiborza, isaacs, mydea and nicohrubec and removed request for a team and mydeaJune 19, 2026 00:57
name: requestMethod === '_OTHER' ? 'HTTP' : requestMethod,
kind: SpanKind.CLIENT,
attributes,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Missing http.client sentry.op

High Severity

startInactiveSpan is called without sentry.op (http.client) on the span or in merged attributes. The integration tests and prior OTel export path expect outgoing fetch spans to use op http.client, but native SentrySpan JSON only reads op from attributes and does not infer it from kind or http.request.method.

Fix in CursorFix in Web

Triggered by project rule: PR Review Guidelines for Cursor Bot

Reviewed by Cursor Bugbot for commit 2b3dbd0. Configure here.

name: requestMethod === '_OTHER' ? 'HTTP' : requestMethod,
kind: SpanKind.CLIENT,
attributes,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Fetch span description missing URL

Medium Severity

Outgoing fetch spans are started with name set to the HTTP method only (e.g. GET), while integration tests expect descriptions like GET http://host/path. The OTel exporter used to derive that description from URL attributes and CLIENT kind; that inference no longer runs for these native spans.

Additional Locations (1)
Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit 2b3dbd0. Configure here.

span.recordException(error);
span.setStatus({
code: SpanStatusCode.ERROR,
code: SPAN_STATUS_ERROR,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Error status not internal_error

Medium Severity

On undici:request:error, the span status message is set to the raw error.message, so serialized status is typically the system error string rather than internal_error. The new fetch-error integration test expects status: 'internal_error', consistent with @sentry/core fetch instrumentation.

Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit 2b3dbd0. Configure here.

@mydeamydea left a comment

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 good IMHO, for this case we can actually already de-otelify this right? And just make this a regular integration that registers the channel hooks...? but we can do this in a follow up 👍

@suzunn

Copy link
Copy Markdown
Contributor

I noticed the new coverage exercises the error path and header-to-attribute mapping, but the riskiest behavior in this refactor is the propagation handoff from OTel context APIs to withActiveSpan(span, () => getTraceData(...)). I would add one integration assertion that captures the outgoing sentry-trace/traceparent headers and verifies their parent span id is the newly-created http.client span, not the surrounding transaction span. That would lock down the inactive-span behavior called out in the PR description and make future cleanup around getTraceData({ span }) much less likely to regress distributed trace parenting.

@logaretm

Copy link
Copy Markdown
MemberAuthor

@mydea I think I might as well de-otelify it if the same tests pass

@suzunn I added a test case for what you raised and it passes, no worries.

Drop the @opentelemetry/instrumentation base from the vendored undici
instrumentation. Since undici reports via diagnostics_channel rather than
module patching, InstrumentationBase provided nothing here — it's now a plain
class that the integration subscribes/unsubscribes directly, off OTel's
registerInstrumentations path. Also drops the remaining @opentelemetry/api
usage (SpanKind, inlined) and safeExecuteInTheMiddle (local helper).
Pure refactor: span emission, trace propagation, and breadcrumbs are unchanged.

@cursorcursorBot left a comment

Copy link
Copy Markdown

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.

There are 4 total unresolved issues (including 3 from previous reviews).

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 3962e7f. Configure here.

if (!_undiciInstrumentation) {
_undiciInstrumentation = new UndiciInstrumentation(_getConfigWithDefaults(options));
}
_undiciInstrumentation.enable();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Undici config frozen at first init

Low Severity

UndiciInstrumentation is constructed only when _undiciInstrumentation is first created; later instrumentNodeFetchSpans(options) calls only invoke enable(). Updated integration options (hooks, header mapping, ignore callbacks) from a subsequent setup path are not applied to the singleton instance.

Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit 3962e7f. Configure here.

Comment on lines +60 to +64
function instrumentNodeFetchSpans(options: NodeFetchOptions): void {
if (!_undiciInstrumentation) {
_undiciInstrumentation = new UndiciInstrumentation(_getConfigWithDefaults(options));
}
_undiciInstrumentation.enable();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Bug: The nativeNodeFetchIntegration configuration is frozen after the first initialization. Subsequent calls to Sentry.init() with different options for this integration are silently ignored.
Severity: MEDIUM

Suggested Fix

To allow for configuration updates, implement a setConfig() method on the UndiciInstrumentation class. Then, modify instrumentNodeFetchSpans to call this setConfig() method on the existing singleton instance whenever new options are provided, similar to how generateInstrumentOnce handles configuration updates.

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/node/src/integrations/node-fetch/index.ts#L60-L64
Potential issue: The `nativeNodeFetchIntegration` uses a module-level singleton
`_undiciInstrumentation` that is initialized only once. The `UndiciInstrumentation`
class does not have a method to update its configuration after instantiation.
Consequently, if `Sentry.init()` is called multiple times in the same process with
different options for this integration (e.g., different `headersToSpanAttributes`), the
configuration from the first call is permanently used, and all subsequent configuration
changes are silently ignored. This primarily affects testing scenarios or applications
that re-initialize Sentry.

Did we get this right? 👍 / 👎 to inform future reviews.

@logaretm
logaretm merged commit beb181a into developJun 19, 2026
549 of 552 checks passed
@logaretm
logaretm deleted the awad/js-2395-streamline-opentelemetryinstrumentation-undici branch June 19, 2026 19:55
Sign up for freeto 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.

Streamline @opentelemetry/instrumentation-undici

4 participants

@logaretm@suzunn@mydea@nicohrubec
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

ref(node): Streamline undici (node-fetch) instrumentation - #21650

Merged
logaretm merged 3 commits into
developfrom
awad/js-2395-streamline-opentelemetryinstrumentation-undici
Jun 19, 2026
Merged

ref(node): Streamline undici (node-fetch) instrumentation#21650
logaretm merged 3 commits into
developfrom
awad/js-2395-streamline-opentelemetryinstrumentation-undici

Conversation

@logaretm

@logaretmlogaretm commented Jun 19, 2026

Copy link
Copy Markdown
Member

Streamlines the vendored UndiciInstrumentation (the span-emitting half of the NodeFetch integration) onto Sentry's span APIs.

closes#20743

Refactor the vendored `UndiciInstrumentation` (the span-emitting half of the
NodeFetch integration) to use Sentry's span APIs instead of OpenTelemetry's
tracing/propagation APIs.
- Span creation: `tracer.startSpan` -> `startInactiveSpan({ kind: CLIENT })`
(the span is created on `undici:request:create` and ended later in
`onDone`/`onError`, so it must be inactive).
- Status: `SpanStatusCode` -> `SPAN_STATUS_ERROR`; drop the no-op
`recordException`.
- Trace propagation: replace `propagation.inject(trace.setSpan(...))` with
`withActiveSpan(span, () => getTraceData(...))` gated by
`shouldPropagateTraceForUrl`. This keeps the propagated `sentry-trace`
referencing the http.client span (not the parent), matching prior output.
Passing `{ span }` to `getTraceData` alone is insufficient: for an inactive
span it resolves to the span's captured scope, whose active span is the
parent.
- Drop the OTel metrics (no MeterProvider is wired up) and the dead
`requireParentforSpans` code path (the SDK always passes `false`).
- Replace the diag logger with `debug` + `DEBUG_BUILD`, swap OTel type imports
for `@sentry/core`, inline the semantic-convention constants into a local
`semconv.ts`, and drop the blanket `eslint-disable`s.
The only remaining `@opentelemetry/api` usage is `SpanKind`; the
`@opentelemetry/instrumentation` patching base is kept.
Adds integration coverage for the error path (`internal_error` span) and for
`headersToSpanAttributes` (request + response header capture).
@linear-code

Copy link
Copy Markdown

JS-2395

@github-actions

github-actionsBot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

size-limit report 📦

PathSize% ChangeChange
@sentry/browser27.45 kB--
@sentry/browser - with treeshaking flags25.88 kB--
@sentry/browser (incl. Tracing)45.94 kB--
@sentry/browser (incl. Tracing + Span Streaming)47.7 kB--
@sentry/browser (incl. Tracing, Profiling)50.73 kB--
@sentry/browser (incl. Tracing, Replay)85.14 kB--
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags74.73 kB--
@sentry/browser (incl. Tracing, Replay with Canvas)89.83 kB--
@sentry/browser (incl. Tracing, Replay, Feedback)102.49 kB--
@sentry/browser (incl. Feedback)44.62 kB--
@sentry/browser (incl. sendFeedback)32.25 kB--
@sentry/browser (incl. FeedbackAsync)37.38 kB--
@sentry/browser (incl. Metrics)28.52 kB--
@sentry/browser (incl. Logs)28.76 kB--
@sentry/browser (incl. Metrics & Logs)29.45 kB--
@sentry/react29.25 kB--
@sentry/react (incl. Tracing)48.24 kB--
@sentry/vue32.61 kB--
@sentry/vue (incl. Tracing)47.8 kB--
@sentry/svelte27.48 kB--
CDN Bundle29.84 kB--
CDN Bundle (incl. Tracing)47.85 kB--
CDN Bundle (incl. Logs, Metrics)31.39 kB--
CDN Bundle (incl. Tracing, Logs, Metrics)49.19 kB--
CDN Bundle (incl. Replay, Logs, Metrics)70.7 kB--
CDN Bundle (incl. Tracing, Replay)85.21 kB--
CDN Bundle (incl. Tracing, Replay, Logs, Metrics)86.48 kB--
CDN Bundle (incl. Tracing, Replay, Feedback)91.05 kB--
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics)92.31 kB--
CDN Bundle - uncompressed88.8 kB--
CDN Bundle (incl. Tracing) - uncompressed144.84 kB--
CDN Bundle (incl. Logs, Metrics) - uncompressed93.5 kB--
CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed148.81 kB--
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed218.33 kB--
CDN Bundle (incl. Tracing, Replay) - uncompressed263.7 kB--
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed267.66 kB--
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed277.4 kB--
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed281.35 kB--
@sentry/nextjs (client)50.63 kB--
@sentry/sveltekit (client)46.33 kB--
@sentry/core/server75.85 kB--
@sentry/core/browser63.01 kB--
@sentry/node-core61.63 kB--
@sentry/node123.61 kB-0.24%-285 B 🔽
@sentry/node/import (ESM hook with diagnostics-channel injection)70.05 kB--
@sentry/node/light50.55 kB--
@sentry/node - without tracing73.69 kB-0.59%-435 B 🔽
@sentry/aws-serverless84.89 kB-0.41%-341 B 🔽
@sentry/cloudflare (withSentry) - minified172.9 kB--
@sentry/cloudflare (withSentry)432.48 kB--

View base workflow run

@logaretm
logaretm marked this pull request as ready for review June 19, 2026 00:57
@logaretm
logaretm requested a review from a team as a code ownerJune 19, 2026 00:57
@logaretm
logaretm requested review from JPeer264, andreiborza, isaacs, mydea and nicohrubec and removed request for a team and mydeaJune 19, 2026 00:57
name: requestMethod === '_OTHER' ? 'HTTP' : requestMethod,
kind: SpanKind.CLIENT,
attributes,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Missing http.client sentry.op

High Severity

startInactiveSpan is called without sentry.op (http.client) on the span or in merged attributes. The integration tests and prior OTel export path expect outgoing fetch spans to use op http.client, but native SentrySpan JSON only reads op from attributes and does not infer it from kind or http.request.method.

Fix in CursorFix in Web

Triggered by project rule: PR Review Guidelines for Cursor Bot

Reviewed by Cursor Bugbot for commit 2b3dbd0. Configure here.

name: requestMethod === '_OTHER' ? 'HTTP' : requestMethod,
kind: SpanKind.CLIENT,
attributes,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Fetch span description missing URL

Medium Severity

Outgoing fetch spans are started with name set to the HTTP method only (e.g. GET), while integration tests expect descriptions like GET http://host/path. The OTel exporter used to derive that description from URL attributes and CLIENT kind; that inference no longer runs for these native spans.

Additional Locations (1)
Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit 2b3dbd0. Configure here.

span.recordException(error);
span.setStatus({
code: SpanStatusCode.ERROR,
code: SPAN_STATUS_ERROR,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Error status not internal_error

Medium Severity

On undici:request:error, the span status message is set to the raw error.message, so serialized status is typically the system error string rather than internal_error. The new fetch-error integration test expects status: 'internal_error', consistent with @sentry/core fetch instrumentation.

Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit 2b3dbd0. Configure here.

@mydeamydea left a comment

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 good IMHO, for this case we can actually already de-otelify this right? And just make this a regular integration that registers the channel hooks...? but we can do this in a follow up 👍

@suzunn

Copy link
Copy Markdown
Contributor

I noticed the new coverage exercises the error path and header-to-attribute mapping, but the riskiest behavior in this refactor is the propagation handoff from OTel context APIs to withActiveSpan(span, () => getTraceData(...)). I would add one integration assertion that captures the outgoing sentry-trace/traceparent headers and verifies their parent span id is the newly-created http.client span, not the surrounding transaction span. That would lock down the inactive-span behavior called out in the PR description and make future cleanup around getTraceData({ span }) much less likely to regress distributed trace parenting.

@logaretm

Copy link
Copy Markdown
MemberAuthor

@mydea I think I might as well de-otelify it if the same tests pass

@suzunn I added a test case for what you raised and it passes, no worries.

Drop the @opentelemetry/instrumentation base from the vendored undici
instrumentation. Since undici reports via diagnostics_channel rather than
module patching, InstrumentationBase provided nothing here — it's now a plain
class that the integration subscribes/unsubscribes directly, off OTel's
registerInstrumentations path. Also drops the remaining @opentelemetry/api
usage (SpanKind, inlined) and safeExecuteInTheMiddle (local helper).
Pure refactor: span emission, trace propagation, and breadcrumbs are unchanged.

@cursorcursorBot left a comment

Copy link
Copy Markdown

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.

There are 4 total unresolved issues (including 3 from previous reviews).

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 3962e7f. Configure here.

if (!_undiciInstrumentation) {
_undiciInstrumentation = new UndiciInstrumentation(_getConfigWithDefaults(options));
}
_undiciInstrumentation.enable();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Undici config frozen at first init

Low Severity

UndiciInstrumentation is constructed only when _undiciInstrumentation is first created; later instrumentNodeFetchSpans(options) calls only invoke enable(). Updated integration options (hooks, header mapping, ignore callbacks) from a subsequent setup path are not applied to the singleton instance.

Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit 3962e7f. Configure here.

Comment on lines +60 to +64
function instrumentNodeFetchSpans(options: NodeFetchOptions): void {
if (!_undiciInstrumentation) {
_undiciInstrumentation = new UndiciInstrumentation(_getConfigWithDefaults(options));
}
_undiciInstrumentation.enable();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Bug: The nativeNodeFetchIntegration configuration is frozen after the first initialization. Subsequent calls to Sentry.init() with different options for this integration are silently ignored.
Severity: MEDIUM

Suggested Fix

To allow for configuration updates, implement a setConfig() method on the UndiciInstrumentation class. Then, modify instrumentNodeFetchSpans to call this setConfig() method on the existing singleton instance whenever new options are provided, similar to how generateInstrumentOnce handles configuration updates.

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/node/src/integrations/node-fetch/index.ts#L60-L64
Potential issue: The `nativeNodeFetchIntegration` uses a module-level singleton
`_undiciInstrumentation` that is initialized only once. The `UndiciInstrumentation`
class does not have a method to update its configuration after instantiation.
Consequently, if `Sentry.init()` is called multiple times in the same process with
different options for this integration (e.g., different `headersToSpanAttributes`), the
configuration from the first call is permanently used, and all subsequent configuration
changes are silently ignored. This primarily affects testing scenarios or applications
that re-initialize Sentry.

Did we get this right? 👍 / 👎 to inform future reviews.

@logaretm
logaretm merged commit beb181a into developJun 19, 2026
549 of 552 checks passed
@logaretm
logaretm deleted the awad/js-2395-streamline-opentelemetryinstrumentation-undici branch June 19, 2026 19:55
Sign up for freeto 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.

Streamline @opentelemetry/instrumentation-undici

4 participants

@logaretm@suzunn@mydea@nicohrubec
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

ref(node): Streamline undici (node-fetch) instrumentation - #21650

Merged
logaretm merged 3 commits into
developfrom
awad/js-2395-streamline-opentelemetryinstrumentation-undici
Jun 19, 2026
Merged

ref(node): Streamline undici (node-fetch) instrumentation#21650
logaretm merged 3 commits into
developfrom
awad/js-2395-streamline-opentelemetryinstrumentation-undici

Conversation

@logaretm

@logaretmlogaretm commented Jun 19, 2026

Copy link
Copy Markdown
Member

Streamlines the vendored UndiciInstrumentation (the span-emitting half of the NodeFetch integration) onto Sentry's span APIs.

closes#20743

Refactor the vendored `UndiciInstrumentation` (the span-emitting half of the
NodeFetch integration) to use Sentry's span APIs instead of OpenTelemetry's
tracing/propagation APIs.
- Span creation: `tracer.startSpan` -> `startInactiveSpan({ kind: CLIENT })`
(the span is created on `undici:request:create` and ended later in
`onDone`/`onError`, so it must be inactive).
- Status: `SpanStatusCode` -> `SPAN_STATUS_ERROR`; drop the no-op
`recordException`.
- Trace propagation: replace `propagation.inject(trace.setSpan(...))` with
`withActiveSpan(span, () => getTraceData(...))` gated by
`shouldPropagateTraceForUrl`. This keeps the propagated `sentry-trace`
referencing the http.client span (not the parent), matching prior output.
Passing `{ span }` to `getTraceData` alone is insufficient: for an inactive
span it resolves to the span's captured scope, whose active span is the
parent.
- Drop the OTel metrics (no MeterProvider is wired up) and the dead
`requireParentforSpans` code path (the SDK always passes `false`).
- Replace the diag logger with `debug` + `DEBUG_BUILD`, swap OTel type imports
for `@sentry/core`, inline the semantic-convention constants into a local
`semconv.ts`, and drop the blanket `eslint-disable`s.
The only remaining `@opentelemetry/api` usage is `SpanKind`; the
`@opentelemetry/instrumentation` patching base is kept.
Adds integration coverage for the error path (`internal_error` span) and for
`headersToSpanAttributes` (request + response header capture).
@linear-code

Copy link
Copy Markdown

JS-2395

@github-actions

github-actionsBot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

size-limit report 📦

PathSize% ChangeChange
@sentry/browser27.45 kB--
@sentry/browser - with treeshaking flags25.88 kB--
@sentry/browser (incl. Tracing)45.94 kB--
@sentry/browser (incl. Tracing + Span Streaming)47.7 kB--
@sentry/browser (incl. Tracing, Profiling)50.73 kB--
@sentry/browser (incl. Tracing, Replay)85.14 kB--
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags74.73 kB--
@sentry/browser (incl. Tracing, Replay with Canvas)89.83 kB--
@sentry/browser (incl. Tracing, Replay, Feedback)102.49 kB--
@sentry/browser (incl. Feedback)44.62 kB--
@sentry/browser (incl. sendFeedback)32.25 kB--
@sentry/browser (incl. FeedbackAsync)37.38 kB--
@sentry/browser (incl. Metrics)28.52 kB--
@sentry/browser (incl. Logs)28.76 kB--
@sentry/browser (incl. Metrics & Logs)29.45 kB--
@sentry/react29.25 kB--
@sentry/react (incl. Tracing)48.24 kB--
@sentry/vue32.61 kB--
@sentry/vue (incl. Tracing)47.8 kB--
@sentry/svelte27.48 kB--
CDN Bundle29.84 kB--
CDN Bundle (incl. Tracing)47.85 kB--
CDN Bundle (incl. Logs, Metrics)31.39 kB--
CDN Bundle (incl. Tracing, Logs, Metrics)49.19 kB--
CDN Bundle (incl. Replay, Logs, Metrics)70.7 kB--
CDN Bundle (incl. Tracing, Replay)85.21 kB--
CDN Bundle (incl. Tracing, Replay, Logs, Metrics)86.48 kB--
CDN Bundle (incl. Tracing, Replay, Feedback)91.05 kB--
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics)92.31 kB--
CDN Bundle - uncompressed88.8 kB--
CDN Bundle (incl. Tracing) - uncompressed144.84 kB--
CDN Bundle (incl. Logs, Metrics) - uncompressed93.5 kB--
CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed148.81 kB--
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed218.33 kB--
CDN Bundle (incl. Tracing, Replay) - uncompressed263.7 kB--
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed267.66 kB--
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed277.4 kB--
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed281.35 kB--
@sentry/nextjs (client)50.63 kB--
@sentry/sveltekit (client)46.33 kB--
@sentry/core/server75.85 kB--
@sentry/core/browser63.01 kB--
@sentry/node-core61.63 kB--
@sentry/node123.61 kB-0.24%-285 B 🔽
@sentry/node/import (ESM hook with diagnostics-channel injection)70.05 kB--
@sentry/node/light50.55 kB--
@sentry/node - without tracing73.69 kB-0.59%-435 B 🔽
@sentry/aws-serverless84.89 kB-0.41%-341 B 🔽
@sentry/cloudflare (withSentry) - minified172.9 kB--
@sentry/cloudflare (withSentry)432.48 kB--

View base workflow run

@logaretm
logaretm marked this pull request as ready for review June 19, 2026 00:57
@logaretm
logaretm requested a review from a team as a code ownerJune 19, 2026 00:57
@logaretm
logaretm requested review from JPeer264, andreiborza, isaacs, mydea and nicohrubec and removed request for a team and mydeaJune 19, 2026 00:57
name: requestMethod === '_OTHER' ? 'HTTP' : requestMethod,
kind: SpanKind.CLIENT,
attributes,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Missing http.client sentry.op

High Severity

startInactiveSpan is called without sentry.op (http.client) on the span or in merged attributes. The integration tests and prior OTel export path expect outgoing fetch spans to use op http.client, but native SentrySpan JSON only reads op from attributes and does not infer it from kind or http.request.method.

Fix in CursorFix in Web

Triggered by project rule: PR Review Guidelines for Cursor Bot

Reviewed by Cursor Bugbot for commit 2b3dbd0. Configure here.

name: requestMethod === '_OTHER' ? 'HTTP' : requestMethod,
kind: SpanKind.CLIENT,
attributes,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Fetch span description missing URL

Medium Severity

Outgoing fetch spans are started with name set to the HTTP method only (e.g. GET), while integration tests expect descriptions like GET http://host/path. The OTel exporter used to derive that description from URL attributes and CLIENT kind; that inference no longer runs for these native spans.

Additional Locations (1)
Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit 2b3dbd0. Configure here.

span.recordException(error);
span.setStatus({
code: SpanStatusCode.ERROR,
code: SPAN_STATUS_ERROR,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Error status not internal_error

Medium Severity

On undici:request:error, the span status message is set to the raw error.message, so serialized status is typically the system error string rather than internal_error. The new fetch-error integration test expects status: 'internal_error', consistent with @sentry/core fetch instrumentation.

Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit 2b3dbd0. Configure here.

@mydeamydea left a comment

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 good IMHO, for this case we can actually already de-otelify this right? And just make this a regular integration that registers the channel hooks...? but we can do this in a follow up 👍

@suzunn

Copy link
Copy Markdown
Contributor

I noticed the new coverage exercises the error path and header-to-attribute mapping, but the riskiest behavior in this refactor is the propagation handoff from OTel context APIs to withActiveSpan(span, () => getTraceData(...)). I would add one integration assertion that captures the outgoing sentry-trace/traceparent headers and verifies their parent span id is the newly-created http.client span, not the surrounding transaction span. That would lock down the inactive-span behavior called out in the PR description and make future cleanup around getTraceData({ span }) much less likely to regress distributed trace parenting.

@logaretm

Copy link
Copy Markdown
MemberAuthor

@mydea I think I might as well de-otelify it if the same tests pass

@suzunn I added a test case for what you raised and it passes, no worries.

Drop the @opentelemetry/instrumentation base from the vendored undici
instrumentation. Since undici reports via diagnostics_channel rather than
module patching, InstrumentationBase provided nothing here — it's now a plain
class that the integration subscribes/unsubscribes directly, off OTel's
registerInstrumentations path. Also drops the remaining @opentelemetry/api
usage (SpanKind, inlined) and safeExecuteInTheMiddle (local helper).
Pure refactor: span emission, trace propagation, and breadcrumbs are unchanged.

@cursorcursorBot left a comment

Copy link
Copy Markdown

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.

There are 4 total unresolved issues (including 3 from previous reviews).

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 3962e7f. Configure here.

if (!_undiciInstrumentation) {
_undiciInstrumentation = new UndiciInstrumentation(_getConfigWithDefaults(options));
}
_undiciInstrumentation.enable();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Undici config frozen at first init

Low Severity

UndiciInstrumentation is constructed only when _undiciInstrumentation is first created; later instrumentNodeFetchSpans(options) calls only invoke enable(). Updated integration options (hooks, header mapping, ignore callbacks) from a subsequent setup path are not applied to the singleton instance.

Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit 3962e7f. Configure here.

Comment on lines +60 to +64
function instrumentNodeFetchSpans(options: NodeFetchOptions): void {
if (!_undiciInstrumentation) {
_undiciInstrumentation = new UndiciInstrumentation(_getConfigWithDefaults(options));
}
_undiciInstrumentation.enable();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Bug: The nativeNodeFetchIntegration configuration is frozen after the first initialization. Subsequent calls to Sentry.init() with different options for this integration are silently ignored.
Severity: MEDIUM

Suggested Fix

To allow for configuration updates, implement a setConfig() method on the UndiciInstrumentation class. Then, modify instrumentNodeFetchSpans to call this setConfig() method on the existing singleton instance whenever new options are provided, similar to how generateInstrumentOnce handles configuration updates.

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/node/src/integrations/node-fetch/index.ts#L60-L64
Potential issue: The `nativeNodeFetchIntegration` uses a module-level singleton
`_undiciInstrumentation` that is initialized only once. The `UndiciInstrumentation`
class does not have a method to update its configuration after instantiation.
Consequently, if `Sentry.init()` is called multiple times in the same process with
different options for this integration (e.g., different `headersToSpanAttributes`), the
configuration from the first call is permanently used, and all subsequent configuration
changes are silently ignored. This primarily affects testing scenarios or applications
that re-initialize Sentry.

Did we get this right? 👍 / 👎 to inform future reviews.

@logaretm
logaretm merged commit beb181a into developJun 19, 2026
549 of 552 checks passed
@logaretm
logaretm deleted the awad/js-2395-streamline-opentelemetryinstrumentation-undici branch June 19, 2026 19:55
Sign up for freeto 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.

Streamline @opentelemetry/instrumentation-undici

4 participants

@logaretm@suzunn@mydea@nicohrubec
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

ref(node): Streamline undici (node-fetch) instrumentation - #21650

Merged
logaretm merged 3 commits into
developfrom
awad/js-2395-streamline-opentelemetryinstrumentation-undici
Jun 19, 2026
Merged

ref(node): Streamline undici (node-fetch) instrumentation#21650
logaretm merged 3 commits into
developfrom
awad/js-2395-streamline-opentelemetryinstrumentation-undici

Conversation

@logaretm

@logaretmlogaretm commented Jun 19, 2026

Copy link
Copy Markdown
Member

Streamlines the vendored UndiciInstrumentation (the span-emitting half of the NodeFetch integration) onto Sentry's span APIs.

closes#20743

Refactor the vendored `UndiciInstrumentation` (the span-emitting half of the
NodeFetch integration) to use Sentry's span APIs instead of OpenTelemetry's
tracing/propagation APIs.
- Span creation: `tracer.startSpan` -> `startInactiveSpan({ kind: CLIENT })`
(the span is created on `undici:request:create` and ended later in
`onDone`/`onError`, so it must be inactive).
- Status: `SpanStatusCode` -> `SPAN_STATUS_ERROR`; drop the no-op
`recordException`.
- Trace propagation: replace `propagation.inject(trace.setSpan(...))` with
`withActiveSpan(span, () => getTraceData(...))` gated by
`shouldPropagateTraceForUrl`. This keeps the propagated `sentry-trace`
referencing the http.client span (not the parent), matching prior output.
Passing `{ span }` to `getTraceData` alone is insufficient: for an inactive
span it resolves to the span's captured scope, whose active span is the
parent.
- Drop the OTel metrics (no MeterProvider is wired up) and the dead
`requireParentforSpans` code path (the SDK always passes `false`).
- Replace the diag logger with `debug` + `DEBUG_BUILD`, swap OTel type imports
for `@sentry/core`, inline the semantic-convention constants into a local
`semconv.ts`, and drop the blanket `eslint-disable`s.
The only remaining `@opentelemetry/api` usage is `SpanKind`; the
`@opentelemetry/instrumentation` patching base is kept.
Adds integration coverage for the error path (`internal_error` span) and for
`headersToSpanAttributes` (request + response header capture).
@linear-code

Copy link
Copy Markdown

JS-2395

@github-actions

github-actionsBot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

size-limit report 📦

PathSize% ChangeChange
@sentry/browser27.45 kB--
@sentry/browser - with treeshaking flags25.88 kB--
@sentry/browser (incl. Tracing)45.94 kB--
@sentry/browser (incl. Tracing + Span Streaming)47.7 kB--
@sentry/browser (incl. Tracing, Profiling)50.73 kB--
@sentry/browser (incl. Tracing, Replay)85.14 kB--
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags74.73 kB--
@sentry/browser (incl. Tracing, Replay with Canvas)89.83 kB--
@sentry/browser (incl. Tracing, Replay, Feedback)102.49 kB--
@sentry/browser (incl. Feedback)44.62 kB--
@sentry/browser (incl. sendFeedback)32.25 kB--
@sentry/browser (incl. FeedbackAsync)37.38 kB--
@sentry/browser (incl. Metrics)28.52 kB--
@sentry/browser (incl. Logs)28.76 kB--
@sentry/browser (incl. Metrics & Logs)29.45 kB--
@sentry/react29.25 kB--
@sentry/react (incl. Tracing)48.24 kB--
@sentry/vue32.61 kB--
@sentry/vue (incl. Tracing)47.8 kB--
@sentry/svelte27.48 kB--
CDN Bundle29.84 kB--
CDN Bundle (incl. Tracing)47.85 kB--
CDN Bundle (incl. Logs, Metrics)31.39 kB--
CDN Bundle (incl. Tracing, Logs, Metrics)49.19 kB--
CDN Bundle (incl. Replay, Logs, Metrics)70.7 kB--
CDN Bundle (incl. Tracing, Replay)85.21 kB--
CDN Bundle (incl. Tracing, Replay, Logs, Metrics)86.48 kB--
CDN Bundle (incl. Tracing, Replay, Feedback)91.05 kB--
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics)92.31 kB--
CDN Bundle - uncompressed88.8 kB--
CDN Bundle (incl. Tracing) - uncompressed144.84 kB--
CDN Bundle (incl. Logs, Metrics) - uncompressed93.5 kB--
CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed148.81 kB--
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed218.33 kB--
CDN Bundle (incl. Tracing, Replay) - uncompressed263.7 kB--
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed267.66 kB--
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed277.4 kB--
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed281.35 kB--
@sentry/nextjs (client)50.63 kB--
@sentry/sveltekit (client)46.33 kB--
@sentry/core/server75.85 kB--
@sentry/core/browser63.01 kB--
@sentry/node-core61.63 kB--
@sentry/node123.61 kB-0.24%-285 B 🔽
@sentry/node/import (ESM hook with diagnostics-channel injection)70.05 kB--
@sentry/node/light50.55 kB--
@sentry/node - without tracing73.69 kB-0.59%-435 B 🔽
@sentry/aws-serverless84.89 kB-0.41%-341 B 🔽
@sentry/cloudflare (withSentry) - minified172.9 kB--
@sentry/cloudflare (withSentry)432.48 kB--

View base workflow run

@logaretm
logaretm marked this pull request as ready for review June 19, 2026 00:57
@logaretm
logaretm requested a review from a team as a code ownerJune 19, 2026 00:57
@logaretm
logaretm requested review from JPeer264, andreiborza, isaacs, mydea and nicohrubec and removed request for a team and mydeaJune 19, 2026 00:57
name: requestMethod === '_OTHER' ? 'HTTP' : requestMethod,
kind: SpanKind.CLIENT,
attributes,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Missing http.client sentry.op

High Severity

startInactiveSpan is called without sentry.op (http.client) on the span or in merged attributes. The integration tests and prior OTel export path expect outgoing fetch spans to use op http.client, but native SentrySpan JSON only reads op from attributes and does not infer it from kind or http.request.method.

Fix in CursorFix in Web

Triggered by project rule: PR Review Guidelines for Cursor Bot

Reviewed by Cursor Bugbot for commit 2b3dbd0. Configure here.

name: requestMethod === '_OTHER' ? 'HTTP' : requestMethod,
kind: SpanKind.CLIENT,
attributes,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Fetch span description missing URL

Medium Severity

Outgoing fetch spans are started with name set to the HTTP method only (e.g. GET), while integration tests expect descriptions like GET http://host/path. The OTel exporter used to derive that description from URL attributes and CLIENT kind; that inference no longer runs for these native spans.

Additional Locations (1)
Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit 2b3dbd0. Configure here.

span.recordException(error);
span.setStatus({
code: SpanStatusCode.ERROR,
code: SPAN_STATUS_ERROR,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Error status not internal_error

Medium Severity

On undici:request:error, the span status message is set to the raw error.message, so serialized status is typically the system error string rather than internal_error. The new fetch-error integration test expects status: 'internal_error', consistent with @sentry/core fetch instrumentation.

Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit 2b3dbd0. Configure here.

@mydeamydea left a comment

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 good IMHO, for this case we can actually already de-otelify this right? And just make this a regular integration that registers the channel hooks...? but we can do this in a follow up 👍

@suzunn

Copy link
Copy Markdown
Contributor

I noticed the new coverage exercises the error path and header-to-attribute mapping, but the riskiest behavior in this refactor is the propagation handoff from OTel context APIs to withActiveSpan(span, () => getTraceData(...)). I would add one integration assertion that captures the outgoing sentry-trace/traceparent headers and verifies their parent span id is the newly-created http.client span, not the surrounding transaction span. That would lock down the inactive-span behavior called out in the PR description and make future cleanup around getTraceData({ span }) much less likely to regress distributed trace parenting.

@logaretm

Copy link
Copy Markdown
MemberAuthor

@mydea I think I might as well de-otelify it if the same tests pass

@suzunn I added a test case for what you raised and it passes, no worries.

Drop the @opentelemetry/instrumentation base from the vendored undici
instrumentation. Since undici reports via diagnostics_channel rather than
module patching, InstrumentationBase provided nothing here — it's now a plain
class that the integration subscribes/unsubscribes directly, off OTel's
registerInstrumentations path. Also drops the remaining @opentelemetry/api
usage (SpanKind, inlined) and safeExecuteInTheMiddle (local helper).
Pure refactor: span emission, trace propagation, and breadcrumbs are unchanged.

@cursorcursorBot left a comment

Copy link
Copy Markdown

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.

There are 4 total unresolved issues (including 3 from previous reviews).

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 3962e7f. Configure here.

if (!_undiciInstrumentation) {
_undiciInstrumentation = new UndiciInstrumentation(_getConfigWithDefaults(options));
}
_undiciInstrumentation.enable();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Undici config frozen at first init

Low Severity

UndiciInstrumentation is constructed only when _undiciInstrumentation is first created; later instrumentNodeFetchSpans(options) calls only invoke enable(). Updated integration options (hooks, header mapping, ignore callbacks) from a subsequent setup path are not applied to the singleton instance.

Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit 3962e7f. Configure here.

Comment on lines +60 to +64
function instrumentNodeFetchSpans(options: NodeFetchOptions): void {
if (!_undiciInstrumentation) {
_undiciInstrumentation = new UndiciInstrumentation(_getConfigWithDefaults(options));
}
_undiciInstrumentation.enable();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Bug: The nativeNodeFetchIntegration configuration is frozen after the first initialization. Subsequent calls to Sentry.init() with different options for this integration are silently ignored.
Severity: MEDIUM

Suggested Fix

To allow for configuration updates, implement a setConfig() method on the UndiciInstrumentation class. Then, modify instrumentNodeFetchSpans to call this setConfig() method on the existing singleton instance whenever new options are provided, similar to how generateInstrumentOnce handles configuration updates.

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/node/src/integrations/node-fetch/index.ts#L60-L64
Potential issue: The `nativeNodeFetchIntegration` uses a module-level singleton
`_undiciInstrumentation` that is initialized only once. The `UndiciInstrumentation`
class does not have a method to update its configuration after instantiation.
Consequently, if `Sentry.init()` is called multiple times in the same process with
different options for this integration (e.g., different `headersToSpanAttributes`), the
configuration from the first call is permanently used, and all subsequent configuration
changes are silently ignored. This primarily affects testing scenarios or applications
that re-initialize Sentry.

Did we get this right? 👍 / 👎 to inform future reviews.

@logaretm
logaretm merged commit beb181a into developJun 19, 2026
549 of 552 checks passed
@logaretm
logaretm deleted the awad/js-2395-streamline-opentelemetryinstrumentation-undici branch June 19, 2026 19:55
Sign up for freeto 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.

Streamline @opentelemetry/instrumentation-undici

4 participants

@logaretm@suzunn@mydea@nicohrubec
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

ref(node): Streamline undici (node-fetch) instrumentation - #21650

Merged
logaretm merged 3 commits into
developfrom
awad/js-2395-streamline-opentelemetryinstrumentation-undici
Jun 19, 2026
Merged

ref(node): Streamline undici (node-fetch) instrumentation#21650
logaretm merged 3 commits into
developfrom
awad/js-2395-streamline-opentelemetryinstrumentation-undici

Conversation

@logaretm

@logaretmlogaretm commented Jun 19, 2026

Copy link
Copy Markdown
Member

Streamlines the vendored UndiciInstrumentation (the span-emitting half of the NodeFetch integration) onto Sentry's span APIs.

closes#20743

Refactor the vendored `UndiciInstrumentation` (the span-emitting half of the
NodeFetch integration) to use Sentry's span APIs instead of OpenTelemetry's
tracing/propagation APIs.
- Span creation: `tracer.startSpan` -> `startInactiveSpan({ kind: CLIENT })`
(the span is created on `undici:request:create` and ended later in
`onDone`/`onError`, so it must be inactive).
- Status: `SpanStatusCode` -> `SPAN_STATUS_ERROR`; drop the no-op
`recordException`.
- Trace propagation: replace `propagation.inject(trace.setSpan(...))` with
`withActiveSpan(span, () => getTraceData(...))` gated by
`shouldPropagateTraceForUrl`. This keeps the propagated `sentry-trace`
referencing the http.client span (not the parent), matching prior output.
Passing `{ span }` to `getTraceData` alone is insufficient: for an inactive
span it resolves to the span's captured scope, whose active span is the
parent.
- Drop the OTel metrics (no MeterProvider is wired up) and the dead
`requireParentforSpans` code path (the SDK always passes `false`).
- Replace the diag logger with `debug` + `DEBUG_BUILD`, swap OTel type imports
for `@sentry/core`, inline the semantic-convention constants into a local
`semconv.ts`, and drop the blanket `eslint-disable`s.
The only remaining `@opentelemetry/api` usage is `SpanKind`; the
`@opentelemetry/instrumentation` patching base is kept.
Adds integration coverage for the error path (`internal_error` span) and for
`headersToSpanAttributes` (request + response header capture).
@linear-code

Copy link
Copy Markdown

JS-2395

@github-actions

github-actionsBot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

size-limit report 📦

PathSize% ChangeChange
@sentry/browser27.45 kB--
@sentry/browser - with treeshaking flags25.88 kB--
@sentry/browser (incl. Tracing)45.94 kB--
@sentry/browser (incl. Tracing + Span Streaming)47.7 kB--
@sentry/browser (incl. Tracing, Profiling)50.73 kB--
@sentry/browser (incl. Tracing, Replay)85.14 kB--
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags74.73 kB--
@sentry/browser (incl. Tracing, Replay with Canvas)89.83 kB--
@sentry/browser (incl. Tracing, Replay, Feedback)102.49 kB--
@sentry/browser (incl. Feedback)44.62 kB--
@sentry/browser (incl. sendFeedback)32.25 kB--
@sentry/browser (incl. FeedbackAsync)37.38 kB--
@sentry/browser (incl. Metrics)28.52 kB--
@sentry/browser (incl. Logs)28.76 kB--
@sentry/browser (incl. Metrics & Logs)29.45 kB--
@sentry/react29.25 kB--
@sentry/react (incl. Tracing)48.24 kB--
@sentry/vue32.61 kB--
@sentry/vue (incl. Tracing)47.8 kB--
@sentry/svelte27.48 kB--
CDN Bundle29.84 kB--
CDN Bundle (incl. Tracing)47.85 kB--
CDN Bundle (incl. Logs, Metrics)31.39 kB--
CDN Bundle (incl. Tracing, Logs, Metrics)49.19 kB--
CDN Bundle (incl. Replay, Logs, Metrics)70.7 kB--
CDN Bundle (incl. Tracing, Replay)85.21 kB--
CDN Bundle (incl. Tracing, Replay, Logs, Metrics)86.48 kB--
CDN Bundle (incl. Tracing, Replay, Feedback)91.05 kB--
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics)92.31 kB--
CDN Bundle - uncompressed88.8 kB--
CDN Bundle (incl. Tracing) - uncompressed144.84 kB--
CDN Bundle (incl. Logs, Metrics) - uncompressed93.5 kB--
CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed148.81 kB--
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed218.33 kB--
CDN Bundle (incl. Tracing, Replay) - uncompressed263.7 kB--
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed267.66 kB--
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed277.4 kB--
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed281.35 kB--
@sentry/nextjs (client)50.63 kB--
@sentry/sveltekit (client)46.33 kB--
@sentry/core/server75.85 kB--
@sentry/core/browser63.01 kB--
@sentry/node-core61.63 kB--
@sentry/node123.61 kB-0.24%-285 B 🔽
@sentry/node/import (ESM hook with diagnostics-channel injection)70.05 kB--
@sentry/node/light50.55 kB--
@sentry/node - without tracing73.69 kB-0.59%-435 B 🔽
@sentry/aws-serverless84.89 kB-0.41%-341 B 🔽
@sentry/cloudflare (withSentry) - minified172.9 kB--
@sentry/cloudflare (withSentry)432.48 kB--

View base workflow run

@logaretm
logaretm marked this pull request as ready for review June 19, 2026 00:57
@logaretm
logaretm requested a review from a team as a code ownerJune 19, 2026 00:57
@logaretm
logaretm requested review from JPeer264, andreiborza, isaacs, mydea and nicohrubec and removed request for a team and mydeaJune 19, 2026 00:57
name: requestMethod === '_OTHER' ? 'HTTP' : requestMethod,
kind: SpanKind.CLIENT,
attributes,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Missing http.client sentry.op

High Severity

startInactiveSpan is called without sentry.op (http.client) on the span or in merged attributes. The integration tests and prior OTel export path expect outgoing fetch spans to use op http.client, but native SentrySpan JSON only reads op from attributes and does not infer it from kind or http.request.method.

Fix in CursorFix in Web

Triggered by project rule: PR Review Guidelines for Cursor Bot

Reviewed by Cursor Bugbot for commit 2b3dbd0. Configure here.

name: requestMethod === '_OTHER' ? 'HTTP' : requestMethod,
kind: SpanKind.CLIENT,
attributes,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Fetch span description missing URL

Medium Severity

Outgoing fetch spans are started with name set to the HTTP method only (e.g. GET), while integration tests expect descriptions like GET http://host/path. The OTel exporter used to derive that description from URL attributes and CLIENT kind; that inference no longer runs for these native spans.

Additional Locations (1)
Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit 2b3dbd0. Configure here.

span.recordException(error);
span.setStatus({
code: SpanStatusCode.ERROR,
code: SPAN_STATUS_ERROR,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Error status not internal_error

Medium Severity

On undici:request:error, the span status message is set to the raw error.message, so serialized status is typically the system error string rather than internal_error. The new fetch-error integration test expects status: 'internal_error', consistent with @sentry/core fetch instrumentation.

Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit 2b3dbd0. Configure here.

@mydeamydea left a comment

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 good IMHO, for this case we can actually already de-otelify this right? And just make this a regular integration that registers the channel hooks...? but we can do this in a follow up 👍

@suzunn

Copy link
Copy Markdown
Contributor

I noticed the new coverage exercises the error path and header-to-attribute mapping, but the riskiest behavior in this refactor is the propagation handoff from OTel context APIs to withActiveSpan(span, () => getTraceData(...)). I would add one integration assertion that captures the outgoing sentry-trace/traceparent headers and verifies their parent span id is the newly-created http.client span, not the surrounding transaction span. That would lock down the inactive-span behavior called out in the PR description and make future cleanup around getTraceData({ span }) much less likely to regress distributed trace parenting.

@logaretm

Copy link
Copy Markdown
MemberAuthor

@mydea I think I might as well de-otelify it if the same tests pass

@suzunn I added a test case for what you raised and it passes, no worries.

Drop the @opentelemetry/instrumentation base from the vendored undici
instrumentation. Since undici reports via diagnostics_channel rather than
module patching, InstrumentationBase provided nothing here — it's now a plain
class that the integration subscribes/unsubscribes directly, off OTel's
registerInstrumentations path. Also drops the remaining @opentelemetry/api
usage (SpanKind, inlined) and safeExecuteInTheMiddle (local helper).
Pure refactor: span emission, trace propagation, and breadcrumbs are unchanged.

@cursorcursorBot left a comment

Copy link
Copy Markdown

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.

There are 4 total unresolved issues (including 3 from previous reviews).

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 3962e7f. Configure here.

if (!_undiciInstrumentation) {
_undiciInstrumentation = new UndiciInstrumentation(_getConfigWithDefaults(options));
}
_undiciInstrumentation.enable();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Undici config frozen at first init

Low Severity

UndiciInstrumentation is constructed only when _undiciInstrumentation is first created; later instrumentNodeFetchSpans(options) calls only invoke enable(). Updated integration options (hooks, header mapping, ignore callbacks) from a subsequent setup path are not applied to the singleton instance.

Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit 3962e7f. Configure here.

Comment on lines +60 to +64
function instrumentNodeFetchSpans(options: NodeFetchOptions): void {
if (!_undiciInstrumentation) {
_undiciInstrumentation = new UndiciInstrumentation(_getConfigWithDefaults(options));
}
_undiciInstrumentation.enable();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Bug: The nativeNodeFetchIntegration configuration is frozen after the first initialization. Subsequent calls to Sentry.init() with different options for this integration are silently ignored.
Severity: MEDIUM

Suggested Fix

To allow for configuration updates, implement a setConfig() method on the UndiciInstrumentation class. Then, modify instrumentNodeFetchSpans to call this setConfig() method on the existing singleton instance whenever new options are provided, similar to how generateInstrumentOnce handles configuration updates.

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/node/src/integrations/node-fetch/index.ts#L60-L64
Potential issue: The `nativeNodeFetchIntegration` uses a module-level singleton
`_undiciInstrumentation` that is initialized only once. The `UndiciInstrumentation`
class does not have a method to update its configuration after instantiation.
Consequently, if `Sentry.init()` is called multiple times in the same process with
different options for this integration (e.g., different `headersToSpanAttributes`), the
configuration from the first call is permanently used, and all subsequent configuration
changes are silently ignored. This primarily affects testing scenarios or applications
that re-initialize Sentry.

Did we get this right? 👍 / 👎 to inform future reviews.

@logaretm
logaretm merged commit beb181a into developJun 19, 2026
549 of 552 checks passed
@logaretm
logaretm deleted the awad/js-2395-streamline-opentelemetryinstrumentation-undici branch June 19, 2026 19:55
Sign up for freeto 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.

Streamline @opentelemetry/instrumentation-undici

4 participants

@logaretm@suzunn@mydea@nicohrubec