Skip to content

document-load: zero-valued navigation timing events are emitted before their own span's start #7017

Description

@ssarma-hub

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

  1. addSpanNetworkEvents (packages/opentelemetry-sdk-trace-web/src/utils.ts) defaults:
    if (ignoreZeros === undefined) {
      ignoreZeros = resource[PTN.START_TIME] !== 0;
    }
  2. For a PerformanceNavigationTiming entry, startTime is unconditionally 0Navigation 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.
  3. 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.)
  4. 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

  1. instrumentation-document-load passes ignoreZeros: true explicitly for navigation entries.
  2. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions