Dozor

Edge cases & browser support

The SDK is designed to be defensive — no method throws unexpectedly, no-op'd calls return cleanly, and transport failures retry on a predictable schedule. This page is the reference matrix for all of that.

No-op matrix

Every method has a "do nothing" path for invalid state, by design. Apps don't have to track which lifecycle phase the recorder is in before calling.

CallWhenWhat happens
Dozor.init(...)Already initialisedReturns the existing singleton, ignores new options
start()Not in idle (already recording or paused)No-op
pause()Not recordingNo-op
resume()Not pausedNo-op
stop() / cancel()Already in idleNo-op
hold()Already heldNo-op
release()Not heldNo-op

Lifecycle interactions

ScenarioBehaviour
stop() while heldFlushes everything (including held events), tells the server the session is over, returns to idle. No data loss. Held flag is reset.
cancel() while heldDrops the buffer without flushing, deletes the session row server-side, returns to idle. Held flag is reset.
Tab hidden + pauseOnHidden: trueAuto-pause with pauseReason: "visibility"; auto-resume on visibility return.
Tab hidden after manual pause()The manual pause carries pauseReason: "user"; auto-resume only triggers for "visibility" pauses, so the manual pause survives the tab switch.
Tab hidden + pauseOnHidden: falseNo auto-pause; recording continues in background tab.
identify() after metadata sentTriggers metadata re-send on next flush — server picks up the new identity.

Page unload behaviour

ScenarioBehaviour
Page unload while recordingFinal events sent via fetch() with keepalive: true. The body is sync-gzipped via fflate so a CSS-heavy page's bootstrap (Meta + FullSnapshot, often 200–400 KB raw) lands well under the browser's ~64 KB keepalive cap. The bootstrap pair has typically already shipped via the eager flush after start(), so keepalive carries only the incremental tail.
Page unload while pausedNo final flush — paused means "don't transport".
Page unload while heldHeld events are not sent. The hold semantics apply through unload.

Storage fallbacks

ScenarioBehaviour
sessionStorage unavailable (private mode, embedded iframe, disabled by user)Session ID generated in memory only — does not persist across reloads. Each reload starts a fresh session.
CompressionStream unavailable (older Safari, older Firefox)Falls back to uncompressed JSON. Bandwidth higher (gzip ratios on rrweb event streams are typically large), everything else identical.

Network + retry semantics

Server responseSDK behaviour
204 No ContentSuccess — proceed to next batch.
4xxNo retry. Invalid key (401) or bad payload (400) — retry won't help, drop the batch.
5xx or network errorRetried up to 3 times with exponential backoff (1s → 2s → 4s).
All retries exhaustedEvents re-queued to buffer, retried on the next flush cycle.
Buffer cap reached during outageBuffer is capped at 10,000 events; oldest dropped first.
Fetch timeout (default 10s)Counts as network error — triggers retry.

The fetchTimeout option does not apply to page-unload keepalive flushes — those are fire-and-forget.

Browser support

Works in any modern browser supporting:

In practical terms: Chrome 89+, Edge 89+, Firefox 113+, Safari 16.4+.

The version floor is set by CompressionStream (Safari 16.4 was the last to ship it). Without it the SDK still works, just sends uncompressed JSON.

TypeScript types

The package exports types for backend and tooling code that needs to understand SDK payloads:

import { DOZOR_MARKER_TAG } from "@kharko/dozor";
import type {
  DozorMarkerTag,
  DozorOptions,
  DozorState,
  DozorUrlMarker,
  IngestPayload,
  Logger,
  SessionMetadata,
  UserIdentity,
  UserTraits,
} from "@kharko/dozor";

The dashboard's ingest endpoint validates incoming payloads against IngestPayload (via Zod) — if you're writing a custom proxy or test harness, importing the type keeps your shape aligned with what the server expects.

On this page