A true nem azt jelenti, hogy megérkezett: a sendBeacon() 64 KiB-os csapdája

A sendBeacon() true értéke csak sikeres sorba állítást jelent. A 64 KiB ráadásul közös keepalive keret – és ettől még a szerver semmit sem kapott meg biztosan.

A monitoring SDK boldogan kiírja, hogy navigator.sendBeacon() visszatért true-val. A dashboardon mégis lyukas a session vége. A hálózat lassú volt, a felhasználó bezárta a laptopot, a mobil OS pedig kilőtte a böngészőt. A boolean közben végig igazat mondott – csak mi kérdeztünk tőle olyat, amire nem válaszol.

A sendBeacon() nem kézbesítési bizonylat. Arra jó, hogy egy kis POST törzset a böngésző gondjaira bízzunk anélkül, hogy feltartanánk a navigációt. Telemetriához remek. Fizetés véglegesítéséhez nagyjából olyan megnyugtató, mint a „feladtam postán” mondat nyomkövetési szám nélkül.

Diagram a böngésző közös 64 KiB-os keepalive soráról: a sorba állítás sikeres lehet, miközben a szerveres kézbesítés még bizonytalan

A true csak annyi: befért a sorba

A Beacon specifikáció meglepően egyértelmű: a visszatérési érték azt jelzi, hogy a user agent sorba tudta-e állítani az adatot. A tényleges hálózati átvitel aszinkron, eredményéről az API semmit nem mond. Nincs response, nincs callback, nincs retry-ígéret. Offline állapothoz sem ad külön tárolást; attól, hogy beaconnek hívják, még nem növeszt saját postagalambot.

Ha false-ot kapsz, legalább biztosan tudod, hogy az adat nem került a sorba. A tipikus ok a méretkorlát. A true viszont csak egy köztes állapot: a böngésző vállalta a próbálkozást, de hálózati hiba, processzkilövés vagy szerverhiba után nem fogja neked utólag bevallani, hogy nem sikerült.

Nem 64 KiB kérésenként, hanem közösen

Itt jön a „na, ezt nem tudtam” rész. A sendBeacon() a háttérben keepalive jelzővel indít fetch-et. A Fetch algoritmusa összeadja az adott fetch grouphoz tartozó, még be nem fejezett keepalive kérések törzsméretét. Ha az új törzzsel együtt az összeg nagyobb 64 KiB-nál, hálózati hibát ad. Vagyis ez nem kényelmes 64 KiB minden hívásra, hanem közös, gördülő keret.

Hat darab, egyenként 10 000 bájtos beacon még elfoglalhat 60 000 bájtot. A hetedik ugyanekkora már nem fér be, amíg az előzőek közül nem fejeződik be valamelyik. Ráadásul a kereten a sendBeacon() és a fetch(..., { keepalive: true }) is osztozik. A szabványos webplatformteszt ezt szó szerint próbálja: 65 536 bájt után még egy üres törzs befér, egyetlen további bájtnak viszont már false jár.

És bájtról beszélünk, nem JavaScript-karakterről. Az ASCII többnyire egy bájt, egy magyar ékezet vagy emoji UTF-8-ban több. A payload.length ezért rossz mérce; a Blob.size vagy a TextEncoder eredményének byteLength értéke kell.

const MAX_BATCH_BYTES = 48 * 1024; // saját, óvatos keret
let queue = [];

function makePayload(events) {
  return new Blob([JSON.stringify(events)], {
    type: "text/plain;charset=UTF-8",
  });
}

function track(event) {
  queue.push(event);
}

function flushTelemetry() {
  if (queue.length === 0) return;

  const body = makePayload(queue);
  if (body.size > MAX_BATCH_BYTES) return false;

  const queued = navigator.sendBeacon("/telemetry", body);
  if (queued) queue = [];
  // false esetén az adat maradjon meg a következő próbához
  return queued;
}

A 48 KiB nem webszabvány, hanem alkalmazásszintű biztonsági tartalék: ugyanabban a dokumentumban más mérőkód is fogyaszthatja a közös keretet. A fontos rész az, hogy a batch méretét bájtban korlátozzuk, és csak sikeres sorba állítás után ürítsük a memóriabeli sort.

Mikor küldjük el?

Az unload eseményre épített „utolsó pillanat” régóta rossz fogadás: mobilon simán elmaradhat, és az unload handler a bfcache-t is ronthatja. A Page Visibility ajánlása szerint a hidden állapot az utolsó megbízhatóan megfigyelhető pont. Ekkor érdemes flusholni – de ne kizárólag ekkor. Egy hosszú életű SPA közben is küldj kisebb batch-eket, például idő- vagy méretküszöbnél.

document.addEventListener("visibilitychange", () => {
  if (document.visibilityState === "hidden") flushTelemetry();
});

Ez sem varázslat. Ha az OS úgy lövi ki a háttérben lévő processzt, hogy JavaScript már nem futhat, nincs esemény, amit elkaphatnánk. Ami nem veszhet el, azt már használat közben tedd tartós kliensoldali outboxba, a szerveren pedig kezelj egyedi eseményazonosítót és deduplikációt. A „küldd el még egyszer” retry nélkül adatvesztés, idempotencia nélkül duplikáció. A hálózat ilyen udvarias.

És a fetch keepalive?

Ha más HTTP metódus, egyedi header vagy response kell, a fetch() keepalive opciója rugalmasabb. A Promise-ból még választ is kaphatsz, amíg az oldal él. Csakhogy ugyanaz a 64 KiB-os keret vonatkozik rá, és a bezárt dokumentum már nem fogja feldolgozni a Promise eredményét. A keepalive tehát nem tartós háttérmunka, csak engedély arra, hogy a kérés túlélhesse az oldalt.

A sendBeacon() ráadásul mindig POST, saját headert nem állíthatsz. Cross-origin célpontnál egy nem CORS-safelisted Content-Type – például application/json-os Blob – preflightot válthat ki. Telemetriánál ezért egyszerűbb a saját origin egyik végpontja és a text/plain törzs; a szerver attól még nyugodtan parse-olhatja JSON-ként. Nem elegáns? Talán. Legalább működés közben nem kér még egy engedélykérő kérést a kérés elé.

Production-ítélet

Használnám productionben analitikára, teljesítménymérésre és diagnosztikára, ahol néhány elveszett esemény elfogadható. Kis batch-eket küldenék menet közben, visibilitychange-nél még egyszer flusholnék, figyelném a false arányát, és a backend minden eseményt azonosító alapján deduplikálna.

Nem használnám rendelés, fizetés, jogosultságmódosítás vagy auditnapló egyetlen kézbesítési csatornájaként. Ott szerveres visszaigazolás, tartós outbox és retry kell. A beacon jó futár az apró, részben eldobható csomagokra. Csak tértivevényt ne képzeljünk a kezébe.

Források

Leave a Reply

Az e-mail címet nem tesszük közzé. A kötelező mezőket * karakterrel jelöltük