Skip to content

fix: route spans into Dynatrace OneAgent's provider instead of an undefined span processor (#502) - #504

Open
sjvans wants to merge 3 commits into
developfrom
fix/502-oneagent-undefined-processor
Open

fix: route spans into Dynatrace OneAgent's provider instead of an undefined span processor (#502)#504
sjvans wants to merge 3 commits into
developfrom
fix/502-oneagent-undefined-processor

Conversation

@sjvans

@sjvans sjvans commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

Fixes #502

Startup crash-loop (CF exit 137) when Dynatrace OneAgent is active, hit by two independent reporters upgrading @cap-js/telemetry 1.6.0 → 2.x.

Root cause

On the OneAgent path — DT_NODE_PRELOAD_OPTIONS set, kind *-to-dynatrace, and no @opentelemetry/exporter-trace-otlp-proto dependency — the 2.x rewrite left processor undefined but still ran new NodeTracerProvider({ spanProcessors: [processor] }). [undefined] was handed to the provider, and MultiSpanProcessor.onStart dereferenced it on the first span → crash. The CALM addDelegate(processor) branch had the same hole.

This is a regression: up to 1.6.0 the factory reused OneAgent's already-registered global tracer provider (via getDelegateTracer()/getDelegate()) instead of building its own, so CDS spans flowed into OneAgent (preserving PurePath/service topology).

Fix (restores 1.6.0 behavior — "Option A")

On the OneAgent path, don't build or register our own provider at all, and create no span processor — return nothing:

if (via_one_agent) {
  LOG._info && LOG.info('Dynatrace OneAgent detected, routing spans into its tracer provider')
  return
}

CDS spans obtain their tracer via the global OpenTelemetry API (lib/tracing/trace.jsotel.trace.getTracer(...)), so they route into OneAgent's provider automatically. registerInstrumentations (the only consumer of this return value) falls back to trace.getTracerProvider() — the global ProxyTracerProvider — when the argument is falsy, so the auto-instrumentations attach to OneAgent too. This removes both undefined-processor crash sites at once; all other kinds/paths are unchanged (the processor-construction block just moved out of the else).

Why return nothing rather than trace.getTracerProvider().getDelegate()? The proxy is late-binding — a delegate registered after tracers have been handed out is picked up retroactively. getDelegate() snapshots the current delegate, which would pin the auto-instrumentations to the Noop provider forever if OneAgent's provider weren't registered yet. Letting registerInstrumentations supply the live proxy is both safer and less code. (Verified under OTel 2.0: @opentelemetry/api@1.9.x's ProxyTracerProvider still exposes this late-binding behavior; the 1.6.0 provider.addSpanProcessor(...) call is gone in 2.0, but the OneAgent path never added a processor, so Option A doesn't need it.)

Tests

New test/tracing-one-agent.test.js:

  1. Reproduces the pre-fix crash — new NodeTracerProvider({ spanProcessors: [undefined] }) throws /onStart/ on the first span.
  2. Registers a stand-in global provider (simulating OneAgent), drives the factory on the via_one_agent path, and asserts it doesn't throw, returns undefined (registers no provider of its own), leaves OneAgent as the global delegate, and a span created via the global API lands in OneAgent's exporter.

22 passed | 2 skipped across the provider-setup suites; format:check clean; package.json/package-lock.json untouched.

Behavior note (docs)

Because Option A registers no provider on the OneAgent path, tracing.sampler (incl. ignoreIncomingPaths) and tracing.propagators are governed by OneAgent, not @cap-js/telemetry — the same contract as 1.6.0. This is architecturally unavoidable: under OneAgent there is a single global TracerProvider and it must be OneAgent's for spans to reach Dynatrace. Documented in the README (Leveraging Dynatrace OneAgent, with cross-references from the Sampler and Propagators sections).

Follow-up

Worth a 2.0.x backport — both reporters are dead-on-boot on 2.0.1 (what's on main today).

@sjvans
sjvans requested a review from a team as a code owner September 10, 2026 12:11
@hyperspace-pr-bot

Copy link
Copy Markdown
Contributor

Summary

The following content is AI-generated and provides a summary of the pull request:


Fix: Route Spans into Dynatrace OneAgent's Provider Instead of Crashing on Undefined Span Processor

Bug Fix

🐛 Resolves a startup crash-loop (CF exit 137) when Dynatrace OneAgent is active and @cap-js/telemetry 2.x is used. The 2.x rewrite left the processor variable undefined on the OneAgent path, then passed [undefined] to new NodeTracerProvider({ spanProcessors: [processor] }). This caused MultiSpanProcessor.onStart to dereference undefined on the very first CDS span, crashing the application immediately after boot.

The fix restores the 1.6.0 behavior: on the OneAgent path, skip building a custom tracer provider entirely and instead return OneAgent's already-registered global delegate. CDS spans flow into OneAgent's provider automatically via the global OpenTelemetry API.

Changes

  • lib/tracing/index.js: Added an early-return guard for the via_one_agent path. When DT_NODE_PRELOAD_OPTIONS is set, the kind matches *-to-dynatrace, and @opentelemetry/exporter-trace-otlp-proto is absent, the factory now calls trace.getTracerProvider().getDelegate() and returns immediately — no exporter, no processor, no custom provider registration. The processor-construction block was moved out of the former else branch so all other paths are unaffected. The @opentelemetry/api trace object is now imported at the top of the file.

  • test/tracing-one-agent.test.js: New regression test suite. Includes a test that reproduces the pre-fix crash (passing [undefined] as a span processor), and a test that simulates OneAgent registering the global provider, drives the factory on the via_one_agent path, and asserts: no crash, the factory returns OneAgent's provider without clobbering the global, and a span created via the global API lands in OneAgent's exporter.

  • CHANGELOG.md: Added a fix entry for version 2.1.0 describing the OneAgent crash and the corrected behavior.

Jira Issues

  • #502: Cannot read properties of undefined (reading 'onStart') — startup crash when upgrading to @cap-js/telemetry 2.x with Dynatrace OneAgent active

  • 🔄 Regenerate and Update Summary
  • ✏️ Insert as PR Description (deletes this comment)
  • 🗑️ Delete comment
PR Bot Information

Version: 1.31.20

@hyperspace-pr-bot hyperspace-pr-bot Bot left a comment

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.

I didn’t find any additional blocking issues in the provided changes. The fix is narrowly scoped to the Dynatrace OneAgent path, and the added regression coverage exercises both the prior crash mode and the intended routing behavior.

PR Bot Information

Version: 1.31.20

  • Correlation ID: b411bf60-ad10-11f1-8f90-4351429b0cde
  • Experiment Variant: treatment
  • Event Trigger: pull_request.opened
  • File Content Strategy: Full file content
  • LLM: gpt-5.5

…ll back to the live global proxy

Instead of returning trace.getTracerProvider().getDelegate() on the
via_one_agent path, return nothing. registerInstrumentations() in
lib/index.js already falls back to trace.getTracerProvider() (the global
ProxyTracerProvider) when its tracerProvider argument is falsy, and the
proxy is late-binding: a delegate registered after tracers are handed
out is picked up retroactively. getDelegate() snapshots the current
delegate, which would pin the auto-instrumentations to the Noop provider
forever if OneAgent's provider were not yet registered.

Drops the now-unused @opentelemetry/api import; updates the regression
test to assert the factory returns undefined and leaves OneAgent as the
global delegate.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant