Summary
On a navigation entry, addSpanNetworkEvents intentionally includes zero-valued timings. That decision was made deliberately in #5332. What doesn't appear to have been evaluated is where the resulting event lands relative to the span carrying it: @opentelemetry/instrumentation-document-load starts its documentFetch span at fetchStart, not at startTime, so a zero-valued timing is emitted at timeOrigin — before the span begins.
#5332 explicitly flagged this consumer as unknown territory:
It is also unclear if these utilities are intended for anything other than the internal fetch/XHR instrumentation, but they are public API, so if someone uses these functions on the initial page navigation event, then it is possible for the function to see legitimate 0-value inputs.
instrumentation-document-load is that caller. This report is the missing half of that question, not a claim that the compromise was wrong.
Mechanism
addSpanNetworkEvents (packages/opentelemetry-sdk-trace-web/src/utils.ts) defaults:
if (ignoreZeros === undefined) {
ignoreZeros = resource[PTN.START_TIME] !== 0;
}
- For a
PerformanceNavigationTiming entry, startTime is unconditionally 0 — Navigation Timing L2 §3.1: "The startTime getter step is to return a DOMHighResTimeStamp with a time value of 0." So ignoreZeros is false for every navigation entry, which is the intended branch.
instrumentation-document-load calls addSpanNetworkEvents(fetchSpan, entries, ignoreNetworkEvents) with no explicit ignoreZeros. (entries is a copy built by getPerformanceNavigationEntries; hasKey uses in, so the prototype getter startTime is copied as 0 rather than being absent.)
- That span is started at
PTN.FETCH_START. Since fetchStart > 0 while the zero-valued event resolves to timeOrigin + 0, the event precedes the span start by exactly fetchStart.
Note the reference time for the #4486 ordering check (perfTime >= refTime) was changed from fetchStart to startTime in #5332. For navigation entries that comparison is now always true, so it no longer constrains these events.
Reproduction
Plain-HTTP page load of the OpenTelemetry Demo 2.2.0 (frontend-web), Chromium.
Browser-side input, read directly from the page:
entryType : "navigation"
protocol : "http:"
startTime : 0
fetchStart : 0.1 ms
secureConnectionStart : 0
timeOrigin : 1787203601492.6 ms
Collector-side output, taken from the collector's debug exporter at verbosity: detailed — i.e. before any backend ingest, storage or UI:
Name : documentFetch
Start time : 2026-08-20 05:26:41.492700098 +0000 UTC (= timeOrigin + fetchStart)
SpanEvent #4
-> Name : secureConnectionStart
-> Timestamp : 2026-08-20 05:26:41.492600098 +0000 UTC (= timeOrigin + 0)
The event is 100µs before its span's start, exactly equal to fetchStart. A second capture with fetchStart = 0.2 ms produced a 200µs offset. Since the magnitude is fetchStart, it is inherently small — on a navigation entry it will always be in this range — and it cannot originate downstream of the SDK.
Observed on 3 of 3 documentFetch spans. Across the same traces, 544 events were checked and every other one falls inside its span; a subresource that genuinely used TLS reported a correctly-ordered secureConnectionStart, so the zero-filtering behaves as intended when startTime is non-zero.
Why report it
Narrowly: a span event carries a timestamp outside the span it belongs to, so the emitted trace is internally inconsistent on its face.
Additionally, 0 in secureConnectionStart is overloaded — no TLS, a cached resource, a resource failing the timing-allow check, and per Fetch's clamp-and-coarsen step a reused connection can produce a non-zero value even on plain HTTP. So a 0 cannot be read as "no secure connection happened", which makes converting it to an absolute instant hard to interpret regardless of ordering.
I am deliberately not claiming downstream breakage. I have not demonstrated a consumer that computes durations from these events, and I am not asking to reverse the cross-origin behaviour restored by #5332 (#3199).
Possible directions
instrumentation-document-load passes ignoreZeros: true explicitly for navigation entries.
- Key the ordering check on the span's own start rather than on the entry's
startTime, so an event cannot precede the span regardless of entry type.
Happy to open a PR for either if a preference is indicated.
Environment
@opentelemetry/sdk-trace-web 2.2.0
@opentelemetry/auto-instrumentations-web 0.54.0 → @opentelemetry/instrumentation-document-load 0.54.x
- Chromium, plain-HTTP origin
- The defaulting shown above is present on
main at time of writing
Filed here because the heuristic and the ordering check live in the core web SDK; the affected caller is instrumentation-document-load in opentelemetry-js-contrib, so please transfer if ownership sits there.
Summary
On a navigation entry,
addSpanNetworkEventsintentionally includes zero-valued timings. That decision was made deliberately in #5332. What doesn't appear to have been evaluated is where the resulting event lands relative to the span carrying it:@opentelemetry/instrumentation-document-loadstarts itsdocumentFetchspan atfetchStart, not atstartTime, so a zero-valued timing is emitted attimeOrigin— before the span begins.#5332 explicitly flagged this consumer as unknown territory:
instrumentation-document-loadis that caller. This report is the missing half of that question, not a claim that the compromise was wrong.Mechanism
addSpanNetworkEvents(packages/opentelemetry-sdk-trace-web/src/utils.ts) defaults:PerformanceNavigationTimingentry,startTimeis unconditionally0— Navigation Timing L2 §3.1: "ThestartTimegetter step is to return aDOMHighResTimeStampwith a time value of 0." SoignoreZerosisfalsefor every navigation entry, which is the intended branch.instrumentation-document-loadcallsaddSpanNetworkEvents(fetchSpan, entries, ignoreNetworkEvents)with no explicitignoreZeros. (entriesis a copy built bygetPerformanceNavigationEntries;hasKeyusesin, so the prototype getterstartTimeis copied as0rather than being absent.)PTN.FETCH_START. SincefetchStart > 0while the zero-valued event resolves totimeOrigin + 0, the event precedes the span start by exactlyfetchStart.Note the reference time for the #4486 ordering check (
perfTime >= refTime) was changed fromfetchStarttostartTimein #5332. For navigation entries that comparison is now always true, so it no longer constrains these events.Reproduction
Plain-HTTP page load of the OpenTelemetry Demo 2.2.0 (
frontend-web), Chromium.Browser-side input, read directly from the page:
Collector-side output, taken from the collector's
debugexporter atverbosity: detailed— i.e. before any backend ingest, storage or UI:The event is 100µs before its span's start, exactly equal to
fetchStart. A second capture withfetchStart = 0.2 msproduced a 200µs offset. Since the magnitude isfetchStart, it is inherently small — on a navigation entry it will always be in this range — and it cannot originate downstream of the SDK.Observed on 3 of 3
documentFetchspans. Across the same traces, 544 events were checked and every other one falls inside its span; a subresource that genuinely used TLS reported a correctly-orderedsecureConnectionStart, so the zero-filtering behaves as intended whenstartTimeis non-zero.Why report it
Narrowly: a span event carries a timestamp outside the span it belongs to, so the emitted trace is internally inconsistent on its face.
Additionally,
0insecureConnectionStartis overloaded — no TLS, a cached resource, a resource failing the timing-allow check, and per Fetch's clamp-and-coarsen step a reused connection can produce a non-zero value even on plain HTTP. So a0cannot be read as "no secure connection happened", which makes converting it to an absolute instant hard to interpret regardless of ordering.I am deliberately not claiming downstream breakage. I have not demonstrated a consumer that computes durations from these events, and I am not asking to reverse the cross-origin behaviour restored by #5332 (#3199).
Possible directions
instrumentation-document-loadpassesignoreZeros: trueexplicitly for navigation entries.startTime, so an event cannot precede the span regardless of entry type.Happy to open a PR for either if a preference is indicated.
Environment
@opentelemetry/sdk-trace-web2.2.0@opentelemetry/auto-instrumentations-web0.54.0 →@opentelemetry/instrumentation-document-load0.54.xmainat time of writingFiled here because the heuristic and the ordering check live in the core web SDK; the affected caller is
instrumentation-document-loadinopentelemetry-js-contrib, so please transfer if ownership sits there.