Skip to content

feat(browser): opt-in offline queue for spans and logs - #1156

Merged
Makisuo merged 11 commits into
feat/browser-sdk-error-replayfrom
feat/browser-sdk-offline-queue
Sep 29, 2026
Merged

Makisuo merged 11 commits into
feat/browser-sdk-error-replayfrom
feat/browser-sdk-offline-queue

fix(browser): one tab at a time resends the offline queue

e5c997c
Select commit
Loading
Failed to load commit list.
Maple Review Bot / Maple / review completed Sep 29, 2026 in 8m 43s

Confidence 3/5 · 1 issue to address

Confidence 3/5 · needs attention
The opt-in queue is well tested against real IndexedDB; the one defect is the origin-wide lock held across an untimed POST in drain.
quality 90/100 · 1 warning · tests covered · risk medium

Adds an opt-in (transport.offline, default off) offline queue: span and log batches the exports gave up on are serialized as OTLP JSON into IndexedDB and re-POSTed on online or the next page load, with consent and age limits. The design is sound and the store/resend/keep/drop paths are exercised by browser tests; one liveness defect in the resend lease is worth fixing before merge.

  • resolveConfig maps transport.offline to ResolvedConfig.offlineQueue, default false
  • OfflineSpanExporter/OfflineLogExporter hand failed batches to the deferred queue
  • startOfflineQueue stores OTLP JSON batches in IndexedDB and resends them under a navigator.locks lease
  • Consent revoke clears the stored queue; batches older than 24h or 100 batches are dropped

Findings

Warning · F3 · The resend lease is held across POSTs that have no timeout

correctness · packages/browser/src/deferred/offline.ts:112-116

run takes the origin-wide navigator.locks lease and holds it for the whole drain, whose fetch (line 93) has no timeout. One tab with a stalled connection (the flaky-network case this feature exists for) keeps the lease, so no other tab of the origin can resend, and every online event in that tab joins the same inflight drain (line 124) instead of starting a new one — the stored batches then wait for the next page load. Abort the POST after a short budget (AbortSignal.timeout), which the existing .catch(() => undefined) already turns into "keep it for next time".

Add `signal: AbortSignal.timeout(10_000)` to the drain `fetch`, or scope the lock to the store read/delete and not to the network call.
What was checked
  • tracing.ts:174 wraps inside the consent and HTTP-status exporters, so the stored batch is the one that would have been sent
  • Stored records are validated before resend (isStoredBatch), so batch.signal cannot escape into the /v1/<signal> URL
  • Resend drops pre-consent and expired batches and prunes past MAX_BATCHES (offline.ts:82,92), matching the documented limits

e5c997c · Updated on every push. Reply "won't fix" to dismiss a finding, or mention @maple to ask about one.

Annotations

Check warning on line 116 in packages/browser/src/deferred/offline.ts

See this annotation in the file changed.

@maple-review-bot maple-review-bot / Maple / review

correctness: The resend lease is held across POSTs that have no timeout

`run` takes the origin-wide `navigator.locks` lease and holds it for the whole `drain`, whose `fetch` (line 93) has no timeout. One tab with a stalled connection (the flaky-network case this feature exists for) keeps the lease, so no other tab of the origin can resend, and every `online` event in that tab joins the same `inflight` drain (line 124) instead of starting a new one — the stored batches then wait for the next page load. Abort the POST after a short budget (`AbortSignal.timeout`), which the existing `.catch(() => undefined)` already turns into "keep it for next time".