fix(browser): send unbatched captures with sendBeacon when a navigation can cancel them - #4791
Conversation
…on can cancel them
An unbatched send, such as a `{ send_instantly: true }` capture, skips the
request queue and therefore the `sendBeacon` drain the queue runs on pagehide.
Without `fetch`, it went out over an async XHR that the navigation cancels, and
nothing retried it.
Unbatched captures now use `sendBeacon` when `fetch` is not available or the
page unloads, unless the caller chose a transport or needs the response. A
rejected beacon now falls back to XHR when `fetch` is missing, so the promotion
can never deliver less than before. The `send_instantly` doc comment now
describes the trade-off.
Generated-By: PostHog Desktop
Task-Id: 2ffe93c1-cf12-4f5e-8098-f5040f7c3997
🦔 PostHog Review reviewed this pull requestFound 0 must fix, 1 should fix, 2 consider. Published 3 findings (view the review). Resolved comments: 3 fixed |
Replay incident risk checkThis diff touches code involved in past incidents. This is a heads-up, not a verdict: read the matched sections of INCIDENTS.md and answer their review questions before merging. For a judgment on whether this diff has the same failure mode, run the |
posthog-js Compliance ReportDate: 2026-09-04 20:05:48 UTC ✅ All Tests Passed!26/26 tests passed Capture Tests✅ 26/26 tests passed View Details
|
|
Size Change: +3.39 kB (+0.02%) Total Size: 20.5 MB 📦 View Changed
ℹ️ View Unchanged
|
|
PostHog Review alpha 🦔 If you find any issues helpful - please reply "valid", "invalid", etc., for evaluation purposes 🙏 |
…rs are set sendBeacon cannot carry the headers that _send_request merges from config.request_headers, so promoting a capture to a beacon would strip proxy or authentication headers from a request that fetch or XHR used to deliver with them. A beacon also never reports a rejection, so a 401 from such a proxy would be silent. Skip the promotion when request_headers are configured; the default empty object leaves it active everywhere else. Generated-By: PostHog Desktop Task-Id: ce964698-8912-47fd-9bd2-66f0a3c09103
fetch only sets keepalive for POST bodies below KEEP_ALIVE_THRESHOLD, so a large immediate event can still be cancelled by a navigation. The doc also now records that a project setting request_headers keeps its normal transport, since a beacon cannot carry them. Generated-By: PostHog Desktop Task-Id: ce964698-8912-47fd-9bd2-66f0a3c09103
…bfcache pagehide also fires when the browser freezes a page into the back-forward cache, and the same PostHog instance resumes on pageshow. Without a reset, _isPageUnloading stayed true for the rest of the restored document, so every later unbatched capture took the beacon path on a fully active page, losing the response callback that drives rate limiting and retries. Generated-By: PostHog Desktop Task-Id: ce964698-8912-47fd-9bd2-66f0a3c09103
Problem
{ send_instantly: true }right before a navigation can lose it silently, and a lost conversion looks exactly like a conversion that never happened.send_instantlyskips the batched queue, and the queue is the only place that forcessendBeaconon pagehide, so the event has no navigation safety net.fetch, the event then went out over an async XHR that the navigation cancels. Nothing retried it.sendBeaconon pagehidefetchavailablefetchwithkeepalivefetchsendBeaconapi_transportsayssendBeaconfetchRefs #205, refs #2289 — both report this gap.
Changes
sendBeaconwhenfetchis not available, or when the page is already unloading.callback)._handle_unloadsets a new_isPageUnloadingflag, so a capture made during unload also uses the beacon.fetchis missing. Before, that payload was lost. This makes the promotion safe: the worst case is the transport used today.send_instantlydoc comment now states what the option does and points at{ transport: 'sendBeacon' }._batchKeyalso skips the queue, sosend_instantlychanges nothing for most real calls.Note
Behaviour on a modern browser with
fetchis unchanged, unless the page is unloading.Release info Sub-libraries affected
Libraries affected
Checklist
If releasing new changes
pnpm changesetto generate a changeset file🤖 Agent context
Autonomy: Fully autonomous
posthog-core.instant-send.test.tsmocks thefetchglobal and checks the transport of the request thatcaptureproduces, for four cases:fetchpresent,fetchmissing, page unloading, and a caller-chosen transport. It also checks that a batched event still goes to the queue.request-beacon-fallback.test.tsdrivesrequest()with nofetchand a rejectingsendBeacon, and checks that XHR sends the payload. Each new test was confirmed to fail with the source change reverted.packages/browserunit suite (6048 pass), the functional suite,tsc,oxlint, andoxfmt. One suite,entrypoints/module.test.ts, fails on a cleanmaincheckout as well, so it is not from this change. No manual browser testing was done._send_request, which every network call funnels through. That would cover more senders, but most of them need the response that a beacon cannot give, so the change was kept on the capture path.sendBeaconfor every unbatched send, including large session recording payloads. The beacon fallback to XHR removes the risk, but the wider change was not needed for the reported problem.Created with PostHog Desktop from this inbox report.