cookie-banner: keep flush entries until delivery is confirmed
flushSync() previously deleted pending entries unconditionally once a transport accepted the payload. That dropped data when the page was restored from bfcache after visibilitychange:hidden, and it allowed concurrent flushes to take overlapping batches. Now items are only removed from `pending` once the transport confirms delivery: synchronously for sendBeacon, in the keepalive fetch's `.then` on an `ok` response. Failed fetches leave entries queued so the next flush can retry, and a `flushing` guard prevents overlapping in-flight batches. Signed-off-by: Émile Ré <emile@getprobo.com>
This commit is contained in:
@@ -238,23 +238,38 @@ export class ReportQueue {
|
|||||||
// and accept losing the tail on pages with > 100 pending items at
|
// and accept losing the tail on pages with > 100 pending items at
|
||||||
// unload time -- still strictly better than the previous behaviour,
|
// unload time -- still strictly better than the previous behaviour,
|
||||||
// which lost the entire last debounce window.
|
// which lost the entire last debounce window.
|
||||||
|
//
|
||||||
|
// Items are only removed from `pending` once the transport
|
||||||
|
// confirms delivery: sendBeacon synchronously returns true when
|
||||||
|
// the browser has accepted ownership of the request, and the
|
||||||
|
// keepalive fetch removes items in its `.then` on an `ok`
|
||||||
|
// response. On async fetch failure we leave the entries queued so
|
||||||
|
// they can be retried by the next flush -- this matters when
|
||||||
|
// `visibilitychange:hidden` fires but the page is restored from
|
||||||
|
// bfcache rather than truly unloading.
|
||||||
private flushSync(): void {
|
private flushSync(): void {
|
||||||
|
if (this.flushing) return;
|
||||||
if (this.pending.size === 0) return;
|
if (this.pending.size === 0) return;
|
||||||
|
|
||||||
const { keys, body } = this.takeBatch();
|
const { keys, body } = this.takeBatch();
|
||||||
const payload = JSON.stringify(body);
|
const payload = JSON.stringify(body);
|
||||||
|
|
||||||
let sent = false;
|
|
||||||
if (typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function") {
|
if (typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function") {
|
||||||
const blob = new Blob([payload], { type: "application/json" });
|
const blob = new Blob([payload], { type: "application/json" });
|
||||||
|
let queued = false;
|
||||||
try {
|
try {
|
||||||
sent = navigator.sendBeacon(this.reportUrl.toString(), blob);
|
queued = navigator.sendBeacon(this.reportUrl.toString(), blob);
|
||||||
} catch {
|
} catch {
|
||||||
sent = false;
|
queued = false;
|
||||||
|
}
|
||||||
|
if (queued) {
|
||||||
|
for (const key of keys) this.pending.delete(key);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!sent && typeof fetch === "function") {
|
if (typeof fetch === "function") {
|
||||||
|
this.flushing = true;
|
||||||
try {
|
try {
|
||||||
void fetch(this.reportUrl.toString(), {
|
void fetch(this.reportUrl.toString(), {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -267,17 +282,27 @@ export class ReportQueue {
|
|||||||
"X-SDK-Version": __SDK_VERSION__,
|
"X-SDK-Version": __SDK_VERSION__,
|
||||||
},
|
},
|
||||||
body: payload,
|
body: payload,
|
||||||
}).catch(() => {
|
})
|
||||||
// fire-and-forget: the page is unloading, no retry possible.
|
.then((res) => {
|
||||||
});
|
if (res.ok) {
|
||||||
sent = true;
|
|
||||||
} catch {
|
|
||||||
sent = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sent) {
|
|
||||||
for (const key of keys) this.pending.delete(key);
|
for (const key of keys) this.pending.delete(key);
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// Leave entries in `pending`; if the page is restored
|
||||||
|
// from bfcache the next flush retries them, and on a
|
||||||
|
// true unload the queue itself is discarded with the
|
||||||
|
// page.
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.flushing = false;
|
||||||
|
if (!this.stopped && this.pending.size > 0) {
|
||||||
|
this.scheduleFlush();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
this.flushing = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user