@usero/sdk 0.5.1 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/plugins/session-replay.cjs +11 -0
- package/dist/plugins/session-replay.cjs.map +1 -1
- package/dist/plugins/session-replay.d.cts +1 -0
- package/dist/plugins/session-replay.d.ts +1 -0
- package/dist/plugins/session-replay.js +11 -0
- package/dist/plugins/session-replay.js.map +1 -1
- package/package.json +1 -1
|
@@ -65,6 +65,8 @@ var SDK_SESSION_STORAGE_KEY = "usero:session-replay:sdk-session-id";
|
|
|
65
65
|
var HARD_CHUNK_BYTE_CAP = 4 * 1024 * 1024;
|
|
66
66
|
var MAX_PENDING_UPLOADS = 3;
|
|
67
67
|
var UPLOAD_DROP_WARN_INTERVAL_MS = 5e3;
|
|
68
|
+
var RRWEB_EVENT_TYPE_FULL_SNAPSHOT = 2;
|
|
69
|
+
var SNAPSHOT_ISOLATION_MIN_GAP_MS = 1500;
|
|
68
70
|
function uint8ToBase64(bytes) {
|
|
69
71
|
let binary = "";
|
|
70
72
|
const chunkSize = 32768;
|
|
@@ -251,6 +253,7 @@ function scheduleChunkUpload(store, ctx) {
|
|
|
251
253
|
ctx.logger.error(
|
|
252
254
|
`chunk ${seq} exceeds 4MB hard cap (${bytes.byteLength} bytes), dropping`
|
|
253
255
|
);
|
|
256
|
+
store.droppedSinceLastUpload += 1;
|
|
254
257
|
return;
|
|
255
258
|
}
|
|
256
259
|
const result = await uploadChunk(
|
|
@@ -314,6 +317,13 @@ function startRecording(store, ctx) {
|
|
|
314
317
|
emit: (event) => {
|
|
315
318
|
if (store.stopped || store.cancelled) return;
|
|
316
319
|
if (store.recordingStartedAt === null) store.recordingStartedAt = event.timestamp;
|
|
320
|
+
if (event.type === RRWEB_EVENT_TYPE_FULL_SNAPSHOT) {
|
|
321
|
+
const now = Date.now();
|
|
322
|
+
if (store.pendingEvents.length > 0 && now - store.lastSnapshotFlushAt >= SNAPSHOT_ISOLATION_MIN_GAP_MS) {
|
|
323
|
+
scheduleChunkUpload(store, ctx);
|
|
324
|
+
store.lastSnapshotFlushAt = now;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
317
327
|
store.pendingEvents.push(event);
|
|
318
328
|
store.pendingBytes += estimateEventBytes(event);
|
|
319
329
|
if (store.pendingFirstTs === null) store.pendingFirstTs = event.timestamp;
|
|
@@ -408,6 +418,7 @@ function sessionReplay(options = {}) {
|
|
|
408
418
|
pendingLastTs: null,
|
|
409
419
|
lastUploadDropWarnAt: 0,
|
|
410
420
|
droppedSinceLastUpload: 0,
|
|
421
|
+
lastSnapshotFlushAt: 0,
|
|
411
422
|
nextChunkSeq: 0,
|
|
412
423
|
uploadQueue: Promise.resolve(),
|
|
413
424
|
pendingUploads: 0,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/identity.ts","../../src/plugins/session-replay.ts"],"names":["generateRandomId"],"mappings":"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","file":"session-replay.cjs","sourcesContent":["// Identity layer for the Usero SDK.\n//\n// Two responsibilities:\n// 1. Mint and persist a stable per-browser `anonymousId` in localStorage\n// so cross-tab + cross-day replays from the same browser stitch\n// together server-side. Falls back to an in-memory id if storage is\n// blocked (sandboxed iframes, Safari Lockdown, full quota). Replay\n// still works in that case, you just lose stitching.\n// 2. Auto-fire POST /api/identify when the resolved user transitions\n// (null -> id, id -> id'). Deduped by an in-memory fingerprint so\n// re-renders with the same user are no-ops on the network.\n//\n// All storage access is wrapped in try/catch and gated behind a one-shot\n// init read. The hot path (replay chunk flush) never touches localStorage.\n\nconst ANON_STORAGE_KEY = 'usero:anonymous-id'\n\nlet cachedAnonymousId: string | null = null\n// Fingerprint of the last identify we POSTed. Same SDK instance + same\n// resolved user + same traits = no-op. Cleared on logout (anonymousId\n// rotation).\nlet lastIdentifyFingerprint: string | null = null\n\nexport type UserTraitValue = string | number | boolean | null\nexport type UserTraits = Record<string, UserTraitValue>\n\nexport interface UseroUser {\n\tid: string\n\temail?: string\n\tdisplayName?: string\n\ttraits?: UserTraits\n}\n\nfunction generateRandomId(): string {\n\tif (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {\n\t\treturn crypto.randomUUID()\n\t}\n\tconst bytes = new Uint8Array(16)\n\tif (typeof crypto !== 'undefined' && typeof crypto.getRandomValues === 'function') {\n\t\tcrypto.getRandomValues(bytes)\n\t} else {\n\t\tfor (let i = 0; i < bytes.length; i += 1) bytes[i] = Math.floor(Math.random() * 256)\n\t}\n\tlet out = ''\n\tfor (const b of bytes) out += b.toString(16).padStart(2, '0')\n\treturn out\n}\n\nfunction safeReadLocalStorage(key: string): string | null {\n\tif (typeof window === 'undefined') return null\n\ttry {\n\t\treturn window.localStorage?.getItem(key) ?? null\n\t} catch {\n\t\treturn null\n\t}\n}\n\nfunction safeWriteLocalStorage(key: string, value: string): void {\n\tif (typeof window === 'undefined') return\n\ttry {\n\t\twindow.localStorage?.setItem(key, value)\n\t} catch {\n\t\t// Sandboxed iframe / Safari Lockdown / quota. Fall back to memory.\n\t}\n}\n\n/**\n * Returns the stable per-browser anonymousId. Reads localStorage at most\n * once per SDK instance. Subsequent calls hit the in-memory cache, so\n * even hot paths (per-event in replay) are safe to call this.\n */\nexport function getOrMintAnonymousId(): string {\n\tif (cachedAnonymousId) return cachedAnonymousId\n\tconst existing = safeReadLocalStorage(ANON_STORAGE_KEY)\n\t// Sanity filter, not strict validation. We accept anything that looks\n\t// plausibly like an id (>=8 alphanumeric-or-hyphen) so older SDK\n\t// versions that wrote a slightly different shape still stitch. Fresh\n\t// mint is cheap, so we only reject obvious garbage; tightening this\n\t// would force rotation in customer browsers and split otherwise-good\n\t// sibling-session attribution.\n\tif (existing && /^[a-z0-9-]{8,}$/i.test(existing)) {\n\t\tcachedAnonymousId = existing\n\t\treturn existing\n\t}\n\tconst id = generateRandomId()\n\tsafeWriteLocalStorage(ANON_STORAGE_KEY, id)\n\tcachedAnonymousId = id\n\treturn id\n}\n\n/**\n * Rotate the anonymousId. Called on logout (user transitions from a\n * known id to null) so the next anonymous trail does not get auto-merged\n * into the previous person on the next identify().\n */\nexport function rotateAnonymousId(): string {\n\tconst id = generateRandomId()\n\tcachedAnonymousId = id\n\tsafeWriteLocalStorage(ANON_STORAGE_KEY, id)\n\tlastIdentifyFingerprint = null\n\treturn id\n}\n\nfunction fingerprintUser(anonymousId: string, user: UseroUser): string {\n\t// Stable across re-renders: keys sorted, traits canonicalised. Cheap\n\t// enough on the hot path (only runs when the SDK thinks the user might\n\t// have changed, never per-event).\n\tconst traits = user.traits ?? {}\n\tconst keys = Object.keys(traits).sort()\n\tconst canonical: Array<[string, UserTraitValue]> = keys.map(k => [k, traits[k] ?? null])\n\treturn JSON.stringify([anonymousId, user.id, user.email ?? null, user.displayName ?? null, canonical])\n}\n\nexport interface IdentifyTransport {\n\tapiUrl: string\n\tclientId: string\n}\n\n/**\n * POST to /api/identify if the (anonymousId, user) fingerprint differs\n * from the last call. Returns true if a network request actually fired.\n * Never throws; failures are best-effort and the caller (the widget /\n * provider) should not treat them as errors.\n *\n * Tab-unload safety: if the page is hidden when this fires (visibility\n * 'hidden' or a pagehide handler), we route the payload through\n * `navigator.sendBeacon` so the request survives unload. Otherwise we\n * use a normal fetch and only cache the fingerprint when the server\n * confirms `accepted: true`. A 200 `{ accepted: false }` (e.g.\n * `unknown_client` for a clientId that becomes valid mid-session) is\n * treated as retryable so the next call re-fires.\n */\nexport async function identifyIfChanged(transport: IdentifyTransport, user: UseroUser): Promise<boolean> {\n\tconst anonymousId = getOrMintAnonymousId()\n\tconst fp = fingerprintUser(anonymousId, user)\n\tif (fp === lastIdentifyFingerprint) return false\n\n\tconst url = `${transport.apiUrl.replace(/\\/$/, '')}/api/identify`\n\t// Body must stay under the browser's keepalive / sendBeacon cap\n\t// (~64KB across most engines) when this fires on pagehide. That\n\t// transitively caps trait payload size; in practice traits should be\n\t// small typed scalars, not blobs.\n\tconst body = JSON.stringify({\n\t\tclientId: transport.clientId,\n\t\tanonymousId,\n\t\texternalUserId: user.id,\n\t\temail: user.email,\n\t\tdisplayName: user.displayName,\n\t\ttraits: user.traits,\n\t})\n\n\t// If the document is hidden (pagehide / tab close in flight), best-effort\n\t// hand off to sendBeacon. We don't get a response back, so we optimistically\n\t// cache the fingerprint to avoid re-firing on the next page; the server is\n\t// idempotent if the page reload re-runs identify with the same payload.\n\tif (\n\t\ttypeof document !== 'undefined' &&\n\t\tdocument.visibilityState === 'hidden' &&\n\t\ttypeof navigator !== 'undefined' &&\n\t\ttypeof navigator.sendBeacon === 'function'\n\t) {\n\t\ttry {\n\t\t\tconst blob = new Blob([body], { type: 'application/json' })\n\t\t\t// sendBeacon returns false when the user agent refuses to queue\n\t\t\t// the request (size cap, or historically Safari rejecting\n\t\t\t// non-CORS-simple content types). Modern Safari accepts\n\t\t\t// application/json, but we keep a keepalive-fetch fallback so an\n\t\t\t// older WebKit that rejects the beacon still ships the identify.\n\t\t\tif (navigator.sendBeacon(url, blob)) {\n\t\t\t\tlastIdentifyFingerprint = fp\n\t\t\t\treturn true\n\t\t\t}\n\t\t} catch {\n\t\t\t// fall through to keepalive fetch below\n\t\t}\n\t}\n\n\ttry {\n\t\tconst res = await fetch(url, {\n\t\t\tmethod: 'POST',\n\t\t\theaders: { 'Content-Type': 'application/json' },\n\t\t\tbody,\n\t\t\t// keepalive lets the request survive a tab-close mid-flight on\n\t\t\t// browsers that support it; sendBeacon above is the primary path.\n\t\t\tkeepalive: true,\n\t\t})\n\t\tif (!res.ok) return true\n\t\t// Parse the response: a 200 with `accepted: false` (e.g. unknown\n\t\t// client) is retryable. Only cache the fingerprint when the server\n\t\t// confirmed it actually stored the identity.\n\t\ttry {\n\t\t\tconst json = (await res.json()) as { accepted?: unknown }\n\t\t\tif (json && json.accepted === true) lastIdentifyFingerprint = fp\n\t\t} catch {\n\t\t\t// Server returned 2xx but unparseable body: don't cache, let the\n\t\t\t// next call retry.\n\t\t}\n\t\treturn true\n\t} catch {\n\t\treturn false\n\t}\n}\n\n/**\n * Clear identify state and rotate anonymousId. Called when the resolved\n * user transitions from a known id to null (logout). The next anonymous\n * trail will get a fresh anonymousId so it does not merge into the\n * previous person.\n */\nexport function handleLogout(): void {\n\trotateAnonymousId()\n}\n\n// Test hooks (not exported from the package public surface).\nexport const __test__ = {\n\tANON_STORAGE_KEY,\n\tresetIdentityState: (): void => {\n\t\tcachedAnonymousId = null\n\t\tlastIdentifyFingerprint = null\n\t},\n}\n","// Session replay plugin for the Usero widget.\n//\n// Streams rrweb events to the SaaS side as gzipped chunks while the user\n// is on the page, instead of buffering in memory and attaching to a\n// feedback submission. This decouples session replay from feedback so we\n// capture every session (subject to bot-gate + sampling + engagement\n// gates), not just the ones that submit feedback.\n//\n// Lifecycle:\n// 1. onInit: dice-roll sample, optional engagement-time gate, mint a\n// stable per-tab `sdkSessionId` in sessionStorage, and POST to\n// /api/replay-sessions to create the row. If the server returns\n// `{accepted:false}` (bot-gated), the plugin no-ops the rest of the\n// session and getCurrentSession() returns null.\n// 2. Recording: lazy-load rrweb, append events to a buffer, flush a\n// chunk every `chunkSeconds` (or sooner if the buffer is large).\n// Each chunk is gzipped via CompressionStream and PUT to\n// /api/replay-sessions/:id/chunks/:seq with raw bytes + the three\n// X-Usero-* headers (Client-Id, Event-Count, Duration-Ms). Retries\n// with exponential backoff. R2 head-check makes retries idempotent\n// server-side. A chunk PUT returning 409 stops the session.\n// 3. onFeedbackSubmit: returns `{sessionReplayId, replayOffsetMs}` so\n// the feedback record can FK at the moment of submit. Does NOT\n// attach `replayEvents` (legacy field) — chunked uploads carry the\n// events out-of-band.\n// 4. onDestroy / pagehide: best-effort flush remaining buffer, then\n// sendBeacon to /api/replay-sessions/:id/finalise with the\n// end-timestamp. Idempotent server-side.\n//\n// Bundle hygiene: rrweb stays lazy via dynamic `import('rrweb')` behind\n// the engagement gate, so consumers who lose the dice roll or navigate\n// away inside the gate window pay zero rrweb bytes.\n\nimport { getOrMintAnonymousId } from '../identity'\nimport type { UseroPlugin, PluginContext } from '../plugin'\n\nexport interface ReplaySampling {\n\tmousemove?: number\n\tscroll?: number\n\tmedia?: number\n\tinput?: number | 'last'\n}\n\nexport interface SessionReplayOptions {\n\t// Wait this many ms after page load before loading rrweb and creating\n\t// the session row. If the user navigates away first, rrweb is never\n\t// loaded and no session row is created. Default 0 (start immediately).\n\tstartAfterMs?: number\n\t// Probability (0..1) that this session records at all. Decided once\n\t// at init via Math.random(). Default 1.\n\tsampleRate?: number\n\t// rrweb sampling rates per event type.\n\tsampling?: ReplaySampling\n\t// Mask all <input>/<textarea> values in the recording. Default true.\n\tmaskAllInputs?: boolean\n\t// CSS selector for nodes whose text content should be masked. Default\n\t// `[data-usero-mask]`.\n\tmaskTextSelector?: string\n\t// Inline external stylesheets so the replay viewer renders correctly\n\t// without network access. Default true.\n\tinlineStylesheet?: boolean\n\t// Block (entirely skip) DOM subtrees matching this selector. Default\n\t// `[data-usero-block]`.\n\tblockSelector?: string\n\t// Flush a chunk every N seconds. Default 3. Smaller = more PUTs but\n\t// less data lost on tab crash and less time event refs retain detached\n\t// DOM nodes in memory.\n\tchunkSeconds?: number\n\t// Soft cap on buffered events before forcing a flush, regardless of\n\t// time. Default 1000.\n\tchunkMaxEvents?: number\n\t// Soft cap on estimated buffered bytes before forcing a flush. Default\n\t// 512_000 (~500 KB pre-gzip). Keeps memory pressure bounded on event-heavy\n\t// pages even when chunkMaxEvents hasn't been hit.\n\tchunkMaxBytes?: number\n\t// Max attempts per chunk before giving up. Default 5.\n\tchunkMaxAttempts?: number\n\t// Force rrweb to take a fresh full snapshot every N ms. This resets\n\t// rrweb's internal mirror so detached DOM (e.g. SPA route changes)\n\t// becomes GC-eligible. Default 60_000.\n\tcheckoutEveryMs?: number\n\t// API origin. Override for self-hosted or local dev. Defaults to the\n\t// PluginContext baseUrl threaded through by the widget.\n\tapiUrl?: string\n}\n\ninterface RrwebEvent {\n\ttype: number\n\tdata: unknown\n\ttimestamp: number\n}\n\ninterface RrwebRecordOptions {\n\temit: (event: RrwebEvent) => void\n\tmaskAllInputs?: boolean\n\tmaskTextSelector?: string\n\tinlineStylesheet?: boolean\n\tblockSelector?: string\n\tsampling?: ReplaySampling\n\tcheckoutEveryNms?: number\n}\n\ninterface RrwebRecordFn {\n\t(opts: RrwebRecordOptions): () => void\n\ttakeFullSnapshot?: (isCheckout?: boolean) => void\n}\n\ntype RrwebRecord = RrwebRecordFn\n\ninterface ResolvedOptions {\n\tstartAfterMs: number\n\tsampleRate: number\n\tsampling: ReplaySampling\n\tmaskAllInputs: boolean\n\tmaskTextSelector: string\n\tinlineStylesheet: boolean\n\tblockSelector: string\n\tchunkSeconds: number\n\tchunkMaxEvents: number\n\tchunkMaxBytes: number\n\tchunkMaxAttempts: number\n\tcheckoutEveryMs: number\n\tapiUrl: string\n}\n\ninterface ReplayStore {\n\toptions: ResolvedOptions\n\tclientId: string\n\tsdkSessionId: string\n\tsessionReplayId: string | null\n\t// Wall-clock timestamp (ms) of the first event we ever recorded.\n\t// Used to compute replayOffsetMs at feedback-submit time.\n\trecordingStartedAt: number | null\n\tpendingEvents: RrwebEvent[]\n\tpendingBytes: number\n\tpendingFirstTs: number | null\n\tpendingLastTs: number | null\n\tlastUploadDropWarnAt: number\n\t// Count of chunks dropped (queue saturation) since the last successful\n\t// upload. Sent as a header on the next successful chunk PUT so the\n\t// viewer can show a \"gap here\" marker. Reset on success.\n\tdroppedSinceLastUpload: number\n\tnextChunkSeq: number\n\tuploadQueue: Promise<void>\n\tpendingUploads: number\n\tchunkFlushTimer: ReturnType<typeof setInterval> | null\n\tstartTimer: ReturnType<typeof setTimeout> | null\n\tpageHideHandler: (() => void) | null\n\tshadowUpdateHandler: ((event: Event) => void) | null\n\trecord: RrwebRecord | null\n\tstopRecording: (() => void) | null\n\tloadInProgress: boolean\n\tcancelled: boolean\n\t// True once the session is \"done\": bot-gated, finalised, or destroyed.\n\tstopped: boolean\n}\n\nconst DEFAULTS: ResolvedOptions = {\n\tstartAfterMs: 0,\n\tsampleRate: 1,\n\tsampling: { mousemove: 50, scroll: 100 },\n\tmaskAllInputs: true,\n\tmaskTextSelector: '[data-usero-mask]',\n\tinlineStylesheet: true,\n\tblockSelector: '[data-usero-block]',\n\tchunkSeconds: 3,\n\tchunkMaxEvents: 1000,\n\tchunkMaxBytes: 512_000,\n\tchunkMaxAttempts: 5,\n\tcheckoutEveryMs: 60_000,\n\tapiUrl: '',\n}\n\nconst SDK_SESSION_STORAGE_KEY = 'usero:session-replay:sdk-session-id'\nconst HARD_CHUNK_BYTE_CAP = 4 * 1024 * 1024\nconst MAX_PENDING_UPLOADS = 3\nconst UPLOAD_DROP_WARN_INTERVAL_MS = 5000\n\nfunction uint8ToBase64(bytes: Uint8Array): string {\n\tlet binary = ''\n\tconst chunkSize = 0x8000\n\tfor (let i = 0; i < bytes.length; i += chunkSize) {\n\t\tconst slice = bytes.subarray(i, i + chunkSize)\n\t\tbinary += String.fromCharCode.apply(null, Array.from(slice))\n\t}\n\treturn typeof btoa === 'function' ? btoa(binary) : ''\n}\n\nasync function gzipBytes(input: string): Promise<Uint8Array> {\n\tif (typeof CompressionStream === 'undefined') {\n\t\t// Old browsers: send uncompressed JSON. Acceptable degradation;\n\t\t// the server endpoint accepts raw application/octet-stream.\n\t\treturn new TextEncoder().encode(input)\n\t}\n\tconst stream = new Blob([input]).stream().pipeThrough(new CompressionStream('gzip'))\n\tconst buf = await new Response(stream).arrayBuffer()\n\treturn new Uint8Array(buf)\n}\n\nfunction generateRandomId(): string {\n\tif (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {\n\t\treturn crypto.randomUUID()\n\t}\n\tconst bytes = new Uint8Array(16)\n\tif (typeof crypto !== 'undefined' && typeof crypto.getRandomValues === 'function') {\n\t\tcrypto.getRandomValues(bytes)\n\t} else {\n\t\tfor (let i = 0; i < bytes.length; i += 1) bytes[i] = Math.floor(Math.random() * 256)\n\t}\n\tlet out = ''\n\tfor (const b of bytes) out += b.toString(16).padStart(2, '0')\n\treturn out\n}\n\nfunction mintSdkSessionId(): string {\n\ttry {\n\t\tconst existing = window.sessionStorage?.getItem(SDK_SESSION_STORAGE_KEY)\n\t\tif (existing && /^[a-z0-9-]{8,}$/i.test(existing)) return existing\n\t} catch {\n\t\t// sessionStorage can throw in sandboxed iframes — fall through.\n\t}\n\tconst id = generateRandomId()\n\ttry {\n\t\twindow.sessionStorage?.setItem(SDK_SESSION_STORAGE_KEY, id)\n\t} catch {\n\t\t// Ignore: we still return the freshly minted id.\n\t}\n\treturn id\n}\n\nfunction joinUrl(apiUrl: string, path: string): string {\n\treturn `${apiUrl.replace(/\\/$/, '')}${path}`\n}\n\n// Cheap per-event byte estimate. Avoids JSON.stringify on the hot emit path.\n// rrweb EventType: 0=DomContentLoaded, 1=Load, 2=FullSnapshot, 3=IncrementalSnapshot,\n// 4=Meta, 5=Custom, 6=Plugin. Full snapshots are the only event class that's\n// genuinely large; everything else is well under a KB on average. Numbers\n// chosen to over-estimate slightly so chunkMaxBytes stays a safety net.\nfunction estimateEventBytes(event: RrwebEvent): number {\n\tif (event.type === 2) return 50_000\n\tif (event.type === 3) return 256\n\treturn 128\n}\n\ninterface CreateSessionResult {\n\taccepted: boolean\n\tsessionReplayId?: string\n\tdropReason?: string\n}\n\nasync function createSession(\n\tapiUrl: string,\n\tclientId: string,\n\tsdkSessionId: string,\n\tanonymousId: string,\n): Promise<CreateSessionResult | null> {\n\ttry {\n\t\tconst startUrl =\n\t\t\ttypeof window !== 'undefined' && window.location ? window.location.href : undefined\n\t\tconst userAgent =\n\t\t\ttypeof navigator !== 'undefined' && navigator.userAgent ? navigator.userAgent : undefined\n\t\tconst res = await fetch(joinUrl(apiUrl, '/api/replay-sessions'), {\n\t\t\tmethod: 'POST',\n\t\t\theaders: { 'Content-Type': 'application/json' },\n\t\t\tbody: JSON.stringify({\n\t\t\t\tclientId,\n\t\t\t\tsdkSessionId,\n\t\t\t\tanonymousId,\n\t\t\t\tstartUrl,\n\t\t\t\tuserAgent,\n\t\t\t\tstartedAt: new Date().toISOString(),\n\t\t\t}),\n\t\t})\n\t\tif (!res.ok) return null\n\t\tconst json = (await res.json()) as {\n\t\t\taccepted?: unknown\n\t\t\tsessionReplayId?: unknown\n\t\t\tdropReason?: unknown\n\t\t}\n\t\tif (typeof json.accepted !== 'boolean') return null\n\t\tconst result: CreateSessionResult = { accepted: json.accepted }\n\t\tif (typeof json.sessionReplayId === 'string') result.sessionReplayId = json.sessionReplayId\n\t\tif (typeof json.dropReason === 'string') result.dropReason = json.dropReason\n\t\treturn result\n\t} catch {\n\t\treturn null\n\t}\n}\n\ninterface ChunkUploadResult {\n\tok: boolean\n\tstopSession: boolean\n}\n\nasync function uploadChunk(\n\tapiUrl: string,\n\tsessionReplayId: string,\n\tclientId: string,\n\tseq: number,\n\tbytes: Uint8Array,\n\teventCount: number,\n\tdurationMs: number,\n\tlogger: PluginContext['logger'],\n\tmaxAttempts: number,\n\tdroppedBefore: number,\n): Promise<ChunkUploadResult> {\n\tconst url = joinUrl(\n\t\tapiUrl,\n\t\t`/api/replay-sessions/${encodeURIComponent(sessionReplayId)}/chunks/${seq}`,\n\t)\n\tlet attempt = 0\n\twhile (attempt < maxAttempts) {\n\t\ttry {\n\t\t\t// Wrap in a Blob so the body type is unambiguously BodyInit; some\n\t\t\t// TS lib targets reject raw Uint8Array as fetch body. Slice off\n\t\t\t// the buffer to satisfy the BlobPart ArrayBuffer constraint\n\t\t\t// (Uint8Array<SharedArrayBuffer> is the alternative the lib\n\t\t\t// admits, which we never produce here).\n\t\t\tconst buffer = bytes.buffer.slice(\n\t\t\t\tbytes.byteOffset,\n\t\t\t\tbytes.byteOffset + bytes.byteLength,\n\t\t\t) as ArrayBuffer\n\t\t\tconst blob = new Blob([buffer], { type: 'application/octet-stream' })\n\t\t\tconst headers: Record<string, string> = {\n\t\t\t\t'Content-Type': 'application/octet-stream',\n\t\t\t\t'X-Usero-Client-Id': clientId,\n\t\t\t\t'X-Usero-Event-Count': String(eventCount),\n\t\t\t\t'X-Usero-Duration-Ms': String(Math.max(0, Math.round(durationMs))),\n\t\t\t}\n\t\t\t// Signal a playback gap: how many chunks were dropped (queue\n\t\t\t// saturation) between the previous successful upload and this one.\n\t\t\t// Server-side viewer will use this to render a \"missing data\" marker.\n\t\t\tif (droppedBefore > 0) headers['X-Usero-Dropped-Before'] = String(droppedBefore)\n\t\t\tconst res = await fetch(url, {\n\t\t\t\tmethod: 'PUT',\n\t\t\t\tbody: blob,\n\t\t\t\theaders,\n\t\t\t})\n\t\t\tif (res.ok) return { ok: true, stopSession: false }\n\t\t\t// 409: server told us to stop (bot-dropped, or session already\n\t\t\t// finalised). Don't retry, don't upload further chunks.\n\t\t\tif (res.status === 409) {\n\t\t\t\tlogger.warn(`chunk ${seq} rejected with 409, stopping session`)\n\t\t\t\treturn { ok: false, stopSession: true }\n\t\t\t}\n\t\t\t// Other 4xx (besides 408/429) won't get better with retry.\n\t\t\tif (res.status >= 400 && res.status < 500 && res.status !== 408 && res.status !== 429) {\n\t\t\t\tlogger.error(`chunk ${seq} rejected with ${res.status}`)\n\t\t\t\treturn { ok: false, stopSession: false }\n\t\t\t}\n\t\t} catch (err) {\n\t\t\tlogger.warn(`chunk ${seq} attempt ${attempt + 1} failed`, err)\n\t\t}\n\t\tattempt += 1\n\t\tconst backoff = Math.min(15_000, 500 * 2 ** attempt) + Math.floor(Math.random() * 250)\n\t\tawait new Promise(resolve => setTimeout(resolve, backoff))\n\t}\n\tlogger.error(`chunk ${seq} dropped after ${maxAttempts} attempts`)\n\treturn { ok: false, stopSession: false }\n}\n\nasync function loadRrwebRecord(): Promise<RrwebRecord | null> {\n\ttry {\n\t\tconst mod: unknown = await import(/* webpackChunkName: \"rrweb\" */ 'rrweb')\n\t\tif (\n\t\t\tmod &&\n\t\t\ttypeof mod === 'object' &&\n\t\t\t'record' in mod &&\n\t\t\ttypeof (mod as { record: unknown }).record === 'function'\n\t\t) {\n\t\t\treturn (mod as { record: RrwebRecord }).record\n\t\t}\n\t\treturn null\n\t} catch {\n\t\treturn null\n\t}\n}\n\nfunction scheduleChunkUpload(store: ReplayStore, ctx: PluginContext): void {\n\tif (!store.sessionReplayId) return\n\tif (store.pendingEvents.length === 0) return\n\tif (store.pendingUploads >= MAX_PENDING_UPLOADS) {\n\t\tconst now = Date.now()\n\t\tif (now - store.lastUploadDropWarnAt > UPLOAD_DROP_WARN_INTERVAL_MS) {\n\t\t\tstore.lastUploadDropWarnAt = now\n\t\t\tctx.logger.warn(\n\t\t\t\t`upload queue full (${store.pendingUploads} in-flight), dropping chunk to bound memory`,\n\t\t\t)\n\t\t}\n\t\tstore.pendingEvents = []\n\t\tstore.pendingBytes = 0\n\t\tstore.pendingFirstTs = null\n\t\tstore.pendingLastTs = null\n\t\t// Track for the next successful chunk so the viewer can render a gap.\n\t\tstore.droppedSinceLastUpload += 1\n\t\treturn\n\t}\n\t// Chunk boundary: re-resolve the user. Captures mid-session login on\n\t// replay-only installs that never open the widget. No-op via fingerprint\n\t// dedupe if nothing changed.\n\ttry {\n\t\tctx.resolveUser?.()\n\t} catch (err) {\n\t\tctx.logger.warn('resolveUser threw at chunk boundary', err)\n\t}\n\tconst events = store.pendingEvents\n\tconst eventCount = events.length\n\tconst firstTs = store.pendingFirstTs ?? 0\n\tconst lastTs = store.pendingLastTs ?? firstTs\n\tconst durationMs = Math.max(0, lastTs - firstTs)\n\tconst seq = store.nextChunkSeq\n\tstore.nextChunkSeq += 1\n\tstore.pendingEvents = []\n\tstore.pendingBytes = 0\n\tstore.pendingFirstTs = null\n\tstore.pendingLastTs = null\n\n\tconst sessionReplayId = store.sessionReplayId\n\tconst apiUrl = store.options.apiUrl\n\tconst clientId = store.clientId\n\tconst maxAttempts = store.options.chunkMaxAttempts\n\n\tconst droppedBefore = store.droppedSinceLastUpload\n\tstore.pendingUploads += 1\n\tstore.uploadQueue = store.uploadQueue.then(async () => {\n\t\ttry {\n\t\t\tif (store.cancelled) return\n\t\t\tconst json = JSON.stringify(events)\n\t\t\tconst bytes = await gzipBytes(json)\n\t\t\tif (bytes.byteLength > HARD_CHUNK_BYTE_CAP) {\n\t\t\t\tctx.logger.error(\n\t\t\t\t\t`chunk ${seq} exceeds 4MB hard cap (${bytes.byteLength} bytes), dropping`,\n\t\t\t\t)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst result = await uploadChunk(\n\t\t\t\tapiUrl,\n\t\t\t\tsessionReplayId,\n\t\t\t\tclientId,\n\t\t\t\tseq,\n\t\t\t\tbytes,\n\t\t\t\teventCount,\n\t\t\t\tdurationMs,\n\t\t\t\tctx.logger,\n\t\t\t\tmaxAttempts,\n\t\t\t\tdroppedBefore,\n\t\t\t)\n\t\t\tif (result.ok && droppedBefore > 0) {\n\t\t\t\t// Subtract what we just reported, rather than zeroing, so any\n\t\t\t\t// drops that happened while this chunk was in flight still\n\t\t\t\t// surface on the next successful upload.\n\t\t\t\tstore.droppedSinceLastUpload = Math.max(\n\t\t\t\t\t0,\n\t\t\t\t\tstore.droppedSinceLastUpload - droppedBefore,\n\t\t\t\t)\n\t\t\t}\n\t\t\tif (result.stopSession) {\n\t\t\t\tstore.stopped = true\n\t\t\t\tstopRrweb(store)\n\t\t\t}\n\t\t} catch (err) {\n\t\t\tctx.logger.error(`chunk ${seq} encode failed`, err)\n\t\t} finally {\n\t\t\tstore.pendingUploads -= 1\n\t\t}\n\t})\n}\n\nfunction flushPendingChunk(store: ReplayStore, ctx: PluginContext): void {\n\tif (store.stopped || store.cancelled) return\n\tif (store.pendingEvents.length === 0) return\n\tscheduleChunkUpload(store, ctx)\n}\n\nfunction stopRrweb(store: ReplayStore): void {\n\tif (store.stopRecording) {\n\t\ttry {\n\t\t\tstore.stopRecording()\n\t\t} catch {\n\t\t\t// Already stopped.\n\t\t}\n\t\tstore.stopRecording = null\n\t}\n\tif (store.chunkFlushTimer) {\n\t\tclearInterval(store.chunkFlushTimer)\n\t\tstore.chunkFlushTimer = null\n\t}\n}\n\nfunction startRecording(store: ReplayStore, ctx: PluginContext): void {\n\tif (store.cancelled || store.stopped || store.stopRecording || store.loadInProgress) return\n\tstore.loadInProgress = true\n\tvoid loadRrwebRecord().then(record => {\n\t\tstore.loadInProgress = false\n\t\tif (store.cancelled || store.stopped || !record) {\n\t\t\tif (!record) ctx.logger.warn('rrweb failed to load, replay disabled')\n\t\t\treturn\n\t\t}\n\t\ttry {\n\t\t\tconst stop = record({\n\t\t\t\temit: event => {\n\t\t\t\t\tif (store.stopped || store.cancelled) return\n\t\t\t\t\tif (store.recordingStartedAt === null) store.recordingStartedAt = event.timestamp\n\t\t\t\t\tstore.pendingEvents.push(event)\n\t\t\t\t\t// Hot path: rrweb fires hundreds of events/sec on busy SPAs.\n\t\t\t\t\t// JSON.stringify-per-event burns CPU we don't have, and .length\n\t\t\t\t\t// is UTF-16 units (under-counts non-ASCII by ~2x) so it was\n\t\t\t\t\t// never a real byte count anyway. Use a per-type heuristic:\n\t\t\t\t\t// full snapshots are huge, mutations are mid, everything else\n\t\t\t\t\t// is cheap. chunkMaxBytes is documented as approximate.\n\t\t\t\t\tstore.pendingBytes += estimateEventBytes(event)\n\t\t\t\t\tif (store.pendingFirstTs === null) store.pendingFirstTs = event.timestamp\n\t\t\t\t\tstore.pendingLastTs = event.timestamp\n\t\t\t\t\tif (\n\t\t\t\t\t\tstore.pendingEvents.length >= store.options.chunkMaxEvents ||\n\t\t\t\t\t\tstore.pendingBytes >= store.options.chunkMaxBytes\n\t\t\t\t\t) {\n\t\t\t\t\t\tscheduleChunkUpload(store, ctx)\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\tmaskAllInputs: store.options.maskAllInputs,\n\t\t\t\tmaskTextSelector: store.options.maskTextSelector || undefined,\n\t\t\t\tinlineStylesheet: store.options.inlineStylesheet,\n\t\t\t\tblockSelector: store.options.blockSelector,\n\t\t\t\tsampling: store.options.sampling,\n\t\t\t\tcheckoutEveryNms: store.options.checkoutEveryMs,\n\t\t\t})\n\t\t\tstore.stopRecording = stop\n\t\t\tstore.record = record\n\t\t\tscheduleShadowSnapshot(store, ctx)\n\n\t\t\tstore.chunkFlushTimer = setInterval(\n\t\t\t\t() => flushPendingChunk(store, ctx),\n\t\t\t\tstore.options.chunkSeconds * 1000,\n\t\t\t)\n\t\t} catch (err) {\n\t\t\tctx.logger.error('rrweb record() threw', err)\n\t\t}\n\t})\n}\n\nfunction scheduleShadowSnapshot(store: ReplayStore, ctx: PluginContext): void {\n\tif (store.cancelled || store.stopped || !store.record || !store.stopRecording) return\n\tconst fn = store.record.takeFullSnapshot\n\tif (typeof fn !== 'function') return\n\ttry {\n\t\tfn(true)\n\t} catch (err) {\n\t\tctx.logger.warn('takeFullSnapshot threw', err)\n\t}\n}\n\nfunction finalise(store: ReplayStore, ctx: PluginContext, opts: { useBeacon: boolean }): void {\n\tif (!store.sessionReplayId) return\n\tif (store.pendingEvents.length > 0) flushPendingChunk(store, ctx)\n\tconst url = joinUrl(\n\t\tstore.options.apiUrl,\n\t\t`/api/replay-sessions/${encodeURIComponent(store.sessionReplayId)}/finalise`,\n\t)\n\tconst body = JSON.stringify({ clientId: store.clientId, endedAt: new Date().toISOString() })\n\tif (opts.useBeacon && typeof navigator !== 'undefined' && navigator.sendBeacon) {\n\t\ttry {\n\t\t\tconst blob = new Blob([body], { type: 'application/json' })\n\t\t\tnavigator.sendBeacon(url, blob)\n\t\t\treturn\n\t\t} catch (err) {\n\t\t\tctx.logger.warn('finalise sendBeacon threw', err)\n\t\t}\n\t}\n\tvoid fetch(url, {\n\t\tmethod: 'POST',\n\t\tbody,\n\t\theaders: { 'Content-Type': 'application/json' },\n\t\tkeepalive: true,\n\t}).catch(err => ctx.logger.warn('finalise fetch failed', err))\n}\n\nexport interface CurrentSessionHandle {\n\tid: string\n\toffsetMs: number\n}\n\nexport function sessionReplay(options: SessionReplayOptions = {}): UseroPlugin {\n\tconst merged: ResolvedOptions = {\n\t\t...DEFAULTS,\n\t\t...options,\n\t\tsampling: { ...DEFAULTS.sampling, ...(options.sampling ?? {}) },\n\t}\n\n\treturn {\n\t\tname: 'session-replay',\n\t\tonInit(ctx) {\n\t\t\tif (typeof window === 'undefined') return\n\t\t\tif (merged.sampleRate < 1 && Math.random() >= merged.sampleRate) {\n\t\t\t\tctx.logger.debug('skipped by sampleRate')\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tconst apiUrl = merged.apiUrl || ctx.baseUrl\n\t\t\tif (!apiUrl) {\n\t\t\t\tctx.logger.error('session-replay needs an apiUrl (via options or PluginContext)')\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst sdkSessionId = mintSdkSessionId()\n\t\t\t// Mint or read the cross-session anonymousId. Cached in module\n\t\t\t// scope after the first call, so this stays O(1) on hot paths.\n\t\t\tconst anonymousId = getOrMintAnonymousId()\n\n\t\t\tconst store: ReplayStore = {\n\t\t\t\toptions: { ...merged, apiUrl },\n\t\t\t\tclientId: ctx.clientId,\n\t\t\t\tsdkSessionId,\n\t\t\t\tsessionReplayId: null,\n\t\t\t\trecordingStartedAt: null,\n\t\t\t\tpendingEvents: [],\n\t\t\t\tpendingBytes: 0,\n\t\t\t\tpendingFirstTs: null,\n\t\t\t\tpendingLastTs: null,\n\t\t\t\tlastUploadDropWarnAt: 0,\n\t\t\t\tdroppedSinceLastUpload: 0,\n\t\t\t\tnextChunkSeq: 0,\n\t\t\t\tuploadQueue: Promise.resolve(),\n\t\t\t\tpendingUploads: 0,\n\t\t\t\tchunkFlushTimer: null,\n\t\t\t\tstartTimer: null,\n\t\t\t\tpageHideHandler: null,\n\t\t\t\tshadowUpdateHandler: null,\n\t\t\t\trecord: null,\n\t\t\t\tstopRecording: null,\n\t\t\t\tloadInProgress: false,\n\t\t\t\tcancelled: false,\n\t\t\t\tstopped: false,\n\t\t\t}\n\t\t\tctx.setStore(store)\n\n\t\t\tconst onShadowUpdate = (): void => scheduleShadowSnapshot(store, ctx)\n\t\t\tstore.shadowUpdateHandler = onShadowUpdate\n\t\t\twindow.addEventListener('usero:shadow-update', onShadowUpdate)\n\n\t\t\tconst onPageHide = (): void => {\n\t\t\t\tfinalise(store, ctx, { useBeacon: true })\n\t\t\t\tstore.stopped = true\n\t\t\t\tstopRrweb(store)\n\t\t\t}\n\t\t\tstore.pageHideHandler = onPageHide\n\t\t\twindow.addEventListener('pagehide', onPageHide)\n\n\t\t\tconst begin = async (): Promise<void> => {\n\t\t\t\tif (store.cancelled) return\n\t\t\t\t// Replay-only customers may never open the widget, so the host's\n\t\t\t\t// user state never gets polled by the widget's interaction\n\t\t\t\t// boundaries. Re-resolve here so a mid-session login that\n\t\t\t\t// happened before session start is visible server-side before\n\t\t\t\t// the first chunk lands. Fingerprint dedupe inside\n\t\t\t\t// identifyIfChanged makes this effectively free when nothing\n\t\t\t\t// changed.\n\t\t\t\ttry {\n\t\t\t\t\tctx.resolveUser?.()\n\t\t\t\t} catch (err) {\n\t\t\t\t\tctx.logger.warn('resolveUser threw at session start', err)\n\t\t\t\t}\n\t\t\t\tconst created = await createSession(apiUrl, ctx.clientId, sdkSessionId, anonymousId)\n\t\t\t\tif (!created) {\n\t\t\t\t\tctx.logger.warn('session create failed, replay disabled')\n\t\t\t\t\tstore.stopped = true\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tif (!created.accepted) {\n\t\t\t\t\tctx.logger.info(`session-replay declined: ${created.dropReason ?? 'unknown'}`)\n\t\t\t\t\tstore.stopped = true\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tif (!created.sessionReplayId) {\n\t\t\t\t\tctx.logger.error('server accepted but returned no sessionReplayId')\n\t\t\t\t\tstore.stopped = true\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tstore.sessionReplayId = created.sessionReplayId\n\t\t\t\tstore.recordingStartedAt = Date.now()\n\t\t\t\tstartRecording(store, ctx)\n\t\t\t}\n\n\t\t\tif (merged.startAfterMs > 0) {\n\t\t\t\tconst cancelOnExit = (): void => {\n\t\t\t\t\tstore.cancelled = true\n\t\t\t\t\tif (store.startTimer) {\n\t\t\t\t\t\tclearTimeout(store.startTimer)\n\t\t\t\t\t\tstore.startTimer = null\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\twindow.addEventListener('pagehide', cancelOnExit, { once: true })\n\t\t\t\twindow.addEventListener('beforeunload', cancelOnExit, { once: true })\n\t\t\t\tstore.startTimer = setTimeout(() => {\n\t\t\t\t\tvoid begin()\n\t\t\t\t}, merged.startAfterMs)\n\t\t\t} else {\n\t\t\t\tvoid begin()\n\t\t\t}\n\t\t},\n\t\tonFeedbackSubmit(ctx) {\n\t\t\tconst store = ctx.getStore<ReplayStore>()\n\t\t\tif (!store || store.cancelled || store.stopped) return undefined\n\t\t\tif (!store.sessionReplayId) return undefined\n\t\t\tconst offsetMs =\n\t\t\t\tstore.recordingStartedAt !== null\n\t\t\t\t\t? Math.max(0, Date.now() - store.recordingStartedAt)\n\t\t\t\t\t: 0\n\t\t\treturn { sessionReplayId: store.sessionReplayId, replayOffsetMs: offsetMs }\n\t\t},\n\t\tonDestroy(ctx) {\n\t\t\tconst store = ctx.getStore<ReplayStore>()\n\t\t\tif (!store) return\n\t\t\tstore.cancelled = true\n\t\t\tif (store.startTimer) {\n\t\t\t\tclearTimeout(store.startTimer)\n\t\t\t\tstore.startTimer = null\n\t\t\t}\n\t\t\tif (store.pageHideHandler) {\n\t\t\t\twindow.removeEventListener('pagehide', store.pageHideHandler)\n\t\t\t\tstore.pageHideHandler = null\n\t\t\t}\n\t\t\tif (store.shadowUpdateHandler) {\n\t\t\t\twindow.removeEventListener('usero:shadow-update', store.shadowUpdateHandler)\n\t\t\t\tstore.shadowUpdateHandler = null\n\t\t\t}\n\t\t\t// SPA route change / React unmount: send a finalise so the\n\t\t\t// server stamps endedAt. fetch+keepalive is fine here since we\n\t\t\t// aren't necessarily in a pagehide path.\n\t\t\tif (store.sessionReplayId && !store.stopped) {\n\t\t\t\tfinalise(store, ctx, { useBeacon: false })\n\t\t\t}\n\t\t\tstore.stopped = true\n\t\t\tstopRrweb(store)\n\t\t\tstore.pendingEvents.length = 0\n\t\t\tstore.pendingBytes = 0\n\t\t\tstore.record = null\n\t\t},\n\t}\n}\n\n// Returns the live session-replay handle for a given plugin context, or\n// null if the session was bot-dropped, sample-skipped, or not yet\n// created. Other plugins (e.g. user-test) can call this to attach the\n// replay FK + offset to their own server-side records.\nexport function getCurrentSession(ctx: PluginContext): CurrentSessionHandle | null {\n\tconst store = ctx.getStore<ReplayStore>()\n\tif (!store || store.cancelled || store.stopped || !store.sessionReplayId) return null\n\tconst offsetMs =\n\t\tstore.recordingStartedAt !== null\n\t\t\t? Math.max(0, Date.now() - store.recordingStartedAt)\n\t\t\t: 0\n\treturn { id: store.sessionReplayId, offsetMs }\n}\n\n// Internal helper exports for testing only. Not part of the public API.\nexport const __test__ = {\n\tuint8ToBase64,\n\tgzipBytes,\n\tmintSdkSessionId,\n\tuploadChunk,\n\tcreateSession,\n\tjoinUrl,\n\tscheduleChunkUpload,\n\tHARD_CHUNK_BYTE_CAP,\n\tSDK_SESSION_STORAGE_KEY,\n\tMAX_PENDING_UPLOADS,\n\tUPLOAD_DROP_WARN_INTERVAL_MS,\n\tDEFAULTS,\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/identity.ts","../../src/plugins/session-replay.ts"],"names":["generateRandomId"],"mappings":";;;AAeA,IAAM,gBAAA,GAAmB,oBAAA;AAEzB,IAAI,iBAAA,GAAmC,IAAA;AAgBvC,SAAS,gBAAA,GAA2B;AACnC,EAAA,IAAI,OAAO,MAAA,KAAW,WAAA,IAAe,OAAO,MAAA,CAAO,eAAe,UAAA,EAAY;AAC7E,IAAA,OAAO,OAAO,UAAA,EAAW;AAAA,EAC1B;AACA,EAAA,MAAM,KAAA,GAAQ,IAAI,UAAA,CAAW,EAAE,CAAA;AAC/B,EAAA,IAAI,OAAO,MAAA,KAAW,WAAA,IAAe,OAAO,MAAA,CAAO,oBAAoB,UAAA,EAAY;AAClF,IAAA,MAAA,CAAO,gBAAgB,KAAK,CAAA;AAAA,EAC7B,CAAA,MAAO;AACN,IAAA,KAAA,IAAS,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,KAAA,CAAM,QAAQ,CAAA,IAAK,CAAA,EAAG,KAAA,CAAM,CAAC,IAAI,IAAA,CAAK,KAAA,CAAM,IAAA,CAAK,MAAA,KAAW,GAAG,CAAA;AAAA,EACpF;AACA,EAAA,IAAI,GAAA,GAAM,EAAA;AACV,EAAA,KAAA,MAAW,CAAA,IAAK,OAAO,GAAA,IAAO,CAAA,CAAE,SAAS,EAAE,CAAA,CAAE,QAAA,CAAS,CAAA,EAAG,GAAG,CAAA;AAC5D,EAAA,OAAO,GAAA;AACR;AAEA,SAAS,qBAAqB,GAAA,EAA4B;AACzD,EAAA,IAAI,OAAO,MAAA,KAAW,WAAA,EAAa,OAAO,IAAA;AAC1C,EAAA,IAAI;AACH,IAAA,OAAO,MAAA,CAAO,YAAA,EAAc,OAAA,CAAQ,GAAG,CAAA,IAAK,IAAA;AAAA,EAC7C,CAAA,CAAA,MAAQ;AACP,IAAA,OAAO,IAAA;AAAA,EACR;AACD;AAEA,SAAS,qBAAA,CAAsB,KAAa,KAAA,EAAqB;AAChE,EAAA,IAAI,OAAO,WAAW,WAAA,EAAa;AACnC,EAAA,IAAI;AACH,IAAA,MAAA,CAAO,YAAA,EAAc,OAAA,CAAQ,GAAA,EAAK,KAAK,CAAA;AAAA,EACxC,CAAA,CAAA,MAAQ;AAAA,EAER;AACD;AAOO,SAAS,oBAAA,GAA+B;AAC9C,EAAA,IAAI,mBAAmB,OAAO,iBAAA;AAC9B,EAAA,MAAM,QAAA,GAAW,qBAAqB,gBAAgB,CAAA;AAOtD,EAAA,IAAI,QAAA,IAAY,kBAAA,CAAmB,IAAA,CAAK,QAAQ,CAAA,EAAG;AAClD,IAAA,iBAAA,GAAoB,QAAA;AACpB,IAAA,OAAO,QAAA;AAAA,EACR;AACA,EAAA,MAAM,KAAK,gBAAA,EAAiB;AAC5B,EAAA,qBAAA,CAAsB,kBAAkB,EAAE,CAAA;AAC1C,EAAA,iBAAA,GAAoB,EAAA;AACpB,EAAA,OAAO,EAAA;AACR;;;ACyEA,IAAM,QAAA,GAA4B;AAAA,EACjC,YAAA,EAAc,CAAA;AAAA,EACd,UAAA,EAAY,CAAA;AAAA,EACZ,QAAA,EAAU,EAAE,SAAA,EAAW,EAAA,EAAI,QAAQ,GAAA,EAAI;AAAA,EACvC,aAAA,EAAe,IAAA;AAAA,EACf,gBAAA,EAAkB,mBAAA;AAAA,EAClB,gBAAA,EAAkB,IAAA;AAAA,EAClB,aAAA,EAAe,oBAAA;AAAA,EACf,YAAA,EAAc,CAAA;AAAA,EACd,cAAA,EAAgB,GAAA;AAAA,EAChB,aAAA,EAAe,KAAA;AAAA,EACf,gBAAA,EAAkB,CAAA;AAAA,EAClB,eAAA,EAAiB,GAAA;AAAA,EACjB,MAAA,EAAQ;AACT,CAAA;AAEA,IAAM,uBAAA,GAA0B,qCAAA;AAChC,IAAM,mBAAA,GAAsB,IAAI,IAAA,GAAO,IAAA;AACvC,IAAM,mBAAA,GAAsB,CAAA;AAC5B,IAAM,4BAAA,GAA+B,GAAA;AAKrC,IAAM,8BAAA,GAAiC,CAAA;AAOvC,IAAM,6BAAA,GAAgC,IAAA;AAEtC,SAAS,cAAc,KAAA,EAA2B;AACjD,EAAA,IAAI,MAAA,GAAS,EAAA;AACb,EAAA,MAAM,SAAA,GAAY,KAAA;AAClB,EAAA,KAAA,IAAS,IAAI,CAAA,EAAG,CAAA,GAAI,KAAA,CAAM,MAAA,EAAQ,KAAK,SAAA,EAAW;AACjD,IAAA,MAAM,KAAA,GAAQ,KAAA,CAAM,QAAA,CAAS,CAAA,EAAG,IAAI,SAAS,CAAA;AAC7C,IAAA,MAAA,IAAU,OAAO,YAAA,CAAa,KAAA,CAAM,MAAM,KAAA,CAAM,IAAA,CAAK,KAAK,CAAC,CAAA;AAAA,EAC5D;AACA,EAAA,OAAO,OAAO,IAAA,KAAS,UAAA,GAAa,IAAA,CAAK,MAAM,CAAA,GAAI,EAAA;AACpD;AAEA,eAAe,UAAU,KAAA,EAAoC;AAC5D,EAAA,IAAI,OAAO,sBAAsB,WAAA,EAAa;AAG7C,IAAA,OAAO,IAAI,WAAA,EAAY,CAAE,MAAA,CAAO,KAAK,CAAA;AAAA,EACtC;AACA,EAAA,MAAM,MAAA,GAAS,IAAI,IAAA,CAAK,CAAC,KAAK,CAAC,CAAA,CAAE,MAAA,EAAO,CAAE,WAAA,CAAY,IAAI,iBAAA,CAAkB,MAAM,CAAC,CAAA;AACnF,EAAA,MAAM,MAAM,MAAM,IAAI,QAAA,CAAS,MAAM,EAAE,WAAA,EAAY;AACnD,EAAA,OAAO,IAAI,WAAW,GAAG,CAAA;AAC1B;AAEA,SAASA,iBAAAA,GAA2B;AACnC,EAAA,IAAI,OAAO,MAAA,KAAW,WAAA,IAAe,OAAO,MAAA,CAAO,eAAe,UAAA,EAAY;AAC7E,IAAA,OAAO,OAAO,UAAA,EAAW;AAAA,EAC1B;AACA,EAAA,MAAM,KAAA,GAAQ,IAAI,UAAA,CAAW,EAAE,CAAA;AAC/B,EAAA,IAAI,OAAO,MAAA,KAAW,WAAA,IAAe,OAAO,MAAA,CAAO,oBAAoB,UAAA,EAAY;AAClF,IAAA,MAAA,CAAO,gBAAgB,KAAK,CAAA;AAAA,EAC7B,CAAA,MAAO;AACN,IAAA,KAAA,IAAS,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,KAAA,CAAM,QAAQ,CAAA,IAAK,CAAA,EAAG,KAAA,CAAM,CAAC,IAAI,IAAA,CAAK,KAAA,CAAM,IAAA,CAAK,MAAA,KAAW,GAAG,CAAA;AAAA,EACpF;AACA,EAAA,IAAI,GAAA,GAAM,EAAA;AACV,EAAA,KAAA,MAAW,CAAA,IAAK,OAAO,GAAA,IAAO,CAAA,CAAE,SAAS,EAAE,CAAA,CAAE,QAAA,CAAS,CAAA,EAAG,GAAG,CAAA;AAC5D,EAAA,OAAO,GAAA;AACR;AAEA,SAAS,gBAAA,GAA2B;AACnC,EAAA,IAAI;AACH,IAAA,MAAM,QAAA,GAAW,MAAA,CAAO,cAAA,EAAgB,OAAA,CAAQ,uBAAuB,CAAA;AACvE,IAAA,IAAI,QAAA,IAAY,kBAAA,CAAmB,IAAA,CAAK,QAAQ,GAAG,OAAO,QAAA;AAAA,EAC3D,CAAA,CAAA,MAAQ;AAAA,EAER;AACA,EAAA,MAAM,KAAKA,iBAAAA,EAAiB;AAC5B,EAAA,IAAI;AACH,IAAA,MAAA,CAAO,cAAA,EAAgB,OAAA,CAAQ,uBAAA,EAAyB,EAAE,CAAA;AAAA,EAC3D,CAAA,CAAA,MAAQ;AAAA,EAER;AACA,EAAA,OAAO,EAAA;AACR;AAEA,SAAS,OAAA,CAAQ,QAAgB,IAAA,EAAsB;AACtD,EAAA,OAAO,GAAG,MAAA,CAAO,OAAA,CAAQ,OAAO,EAAE,CAAC,GAAG,IAAI,CAAA,CAAA;AAC3C;AAOA,SAAS,mBAAmB,KAAA,EAA2B;AACtD,EAAA,IAAI,KAAA,CAAM,IAAA,KAAS,CAAA,EAAG,OAAO,GAAA;AAC7B,EAAA,IAAI,KAAA,CAAM,IAAA,KAAS,CAAA,EAAG,OAAO,GAAA;AAC7B,EAAA,OAAO,GAAA;AACR;AAQA,eAAe,aAAA,CACd,MAAA,EACA,QAAA,EACA,YAAA,EACA,WAAA,EACsC;AACtC,EAAA,IAAI;AACH,IAAA,MAAM,QAAA,GACL,OAAO,MAAA,KAAW,WAAA,IAAe,OAAO,QAAA,GAAW,MAAA,CAAO,SAAS,IAAA,GAAO,KAAA,CAAA;AAC3E,IAAA,MAAM,YACL,OAAO,SAAA,KAAc,eAAe,SAAA,CAAU,SAAA,GAAY,UAAU,SAAA,GAAY,KAAA,CAAA;AACjF,IAAA,MAAM,MAAM,MAAM,KAAA,CAAM,OAAA,CAAQ,MAAA,EAAQ,sBAAsB,CAAA,EAAG;AAAA,MAChE,MAAA,EAAQ,MAAA;AAAA,MACR,OAAA,EAAS,EAAE,cAAA,EAAgB,kBAAA,EAAmB;AAAA,MAC9C,IAAA,EAAM,KAAK,SAAA,CAAU;AAAA,QACpB,QAAA;AAAA,QACA,YAAA;AAAA,QACA,WAAA;AAAA,QACA,QAAA;AAAA,QACA,SAAA;AAAA,QACA,SAAA,EAAA,iBAAW,IAAI,IAAA,EAAK,EAAE,WAAA;AAAY,OAClC;AAAA,KACD,CAAA;AACD,IAAA,IAAI,CAAC,GAAA,CAAI,EAAA,EAAI,OAAO,IAAA;AACpB,IAAA,MAAM,IAAA,GAAQ,MAAM,GAAA,CAAI,IAAA,EAAK;AAK7B,IAAA,IAAI,OAAO,IAAA,CAAK,QAAA,KAAa,SAAA,EAAW,OAAO,IAAA;AAC/C,IAAA,MAAM,MAAA,GAA8B,EAAE,QAAA,EAAU,IAAA,CAAK,QAAA,EAAS;AAC9D,IAAA,IAAI,OAAO,IAAA,CAAK,eAAA,KAAoB,QAAA,EAAU,MAAA,CAAO,kBAAkB,IAAA,CAAK,eAAA;AAC5E,IAAA,IAAI,OAAO,IAAA,CAAK,UAAA,KAAe,QAAA,EAAU,MAAA,CAAO,aAAa,IAAA,CAAK,UAAA;AAClE,IAAA,OAAO,MAAA;AAAA,EACR,CAAA,CAAA,MAAQ;AACP,IAAA,OAAO,IAAA;AAAA,EACR;AACD;AAOA,eAAe,WAAA,CACd,MAAA,EACA,eAAA,EACA,QAAA,EACA,GAAA,EACA,OACA,UAAA,EACA,UAAA,EACA,MAAA,EACA,WAAA,EACA,aAAA,EAC6B;AAC7B,EAAA,MAAM,GAAA,GAAM,OAAA;AAAA,IACX,MAAA;AAAA,IACA,CAAA,qBAAA,EAAwB,kBAAA,CAAmB,eAAe,CAAC,WAAW,GAAG,CAAA;AAAA,GAC1E;AACA,EAAA,IAAI,OAAA,GAAU,CAAA;AACd,EAAA,OAAO,UAAU,WAAA,EAAa;AAC7B,IAAA,IAAI;AAMH,MAAA,MAAM,MAAA,GAAS,MAAM,MAAA,CAAO,KAAA;AAAA,QAC3B,KAAA,CAAM,UAAA;AAAA,QACN,KAAA,CAAM,aAAa,KAAA,CAAM;AAAA,OAC1B;AACA,MAAA,MAAM,IAAA,GAAO,IAAI,IAAA,CAAK,CAAC,MAAM,CAAA,EAAG,EAAE,IAAA,EAAM,0BAAA,EAA4B,CAAA;AACpE,MAAA,MAAM,OAAA,GAAkC;AAAA,QACvC,cAAA,EAAgB,0BAAA;AAAA,QAChB,mBAAA,EAAqB,QAAA;AAAA,QACrB,qBAAA,EAAuB,OAAO,UAAU,CAAA;AAAA,QACxC,qBAAA,EAAuB,OAAO,IAAA,CAAK,GAAA,CAAI,GAAG,IAAA,CAAK,KAAA,CAAM,UAAU,CAAC,CAAC;AAAA,OAClE;AAIA,MAAA,IAAI,gBAAgB,CAAA,EAAG,OAAA,CAAQ,wBAAwB,CAAA,GAAI,OAAO,aAAa,CAAA;AAC/E,MAAA,MAAM,GAAA,GAAM,MAAM,KAAA,CAAM,GAAA,EAAK;AAAA,QAC5B,MAAA,EAAQ,KAAA;AAAA,QACR,IAAA,EAAM,IAAA;AAAA,QACN;AAAA,OACA,CAAA;AACD,MAAA,IAAI,IAAI,EAAA,EAAI,OAAO,EAAE,EAAA,EAAI,IAAA,EAAM,aAAa,KAAA,EAAM;AAGlD,MAAA,IAAI,GAAA,CAAI,WAAW,GAAA,EAAK;AACvB,QAAA,MAAA,CAAO,IAAA,CAAK,CAAA,MAAA,EAAS,GAAG,CAAA,oCAAA,CAAsC,CAAA;AAC9D,QAAA,OAAO,EAAE,EAAA,EAAI,KAAA,EAAO,WAAA,EAAa,IAAA,EAAK;AAAA,MACvC;AAEA,MAAA,IAAI,GAAA,CAAI,MAAA,IAAU,GAAA,IAAO,GAAA,CAAI,MAAA,GAAS,GAAA,IAAO,GAAA,CAAI,MAAA,KAAW,GAAA,IAAO,GAAA,CAAI,MAAA,KAAW,GAAA,EAAK;AACtF,QAAA,MAAA,CAAO,MAAM,CAAA,MAAA,EAAS,GAAG,CAAA,eAAA,EAAkB,GAAA,CAAI,MAAM,CAAA,CAAE,CAAA;AACvD,QAAA,OAAO,EAAE,EAAA,EAAI,KAAA,EAAO,WAAA,EAAa,KAAA,EAAM;AAAA,MACxC;AAAA,IACD,SAAS,GAAA,EAAK;AACb,MAAA,MAAA,CAAO,KAAK,CAAA,MAAA,EAAS,GAAG,YAAY,OAAA,GAAU,CAAC,WAAW,GAAG,CAAA;AAAA,IAC9D;AACA,IAAA,OAAA,IAAW,CAAA;AACX,IAAA,MAAM,OAAA,GAAU,IAAA,CAAK,GAAA,CAAI,IAAA,EAAQ,GAAA,GAAM,CAAA,IAAK,OAAO,CAAA,GAAI,IAAA,CAAK,KAAA,CAAM,IAAA,CAAK,MAAA,KAAW,GAAG,CAAA;AACrF,IAAA,MAAM,IAAI,OAAA,CAAQ,CAAA,OAAA,KAAW,UAAA,CAAW,OAAA,EAAS,OAAO,CAAC,CAAA;AAAA,EAC1D;AACA,EAAA,MAAA,CAAO,KAAA,CAAM,CAAA,MAAA,EAAS,GAAG,CAAA,eAAA,EAAkB,WAAW,CAAA,SAAA,CAAW,CAAA;AACjE,EAAA,OAAO,EAAE,EAAA,EAAI,KAAA,EAAO,WAAA,EAAa,KAAA,EAAM;AACxC;AAEA,eAAe,eAAA,GAA+C;AAC7D,EAAA,IAAI;AACH,IAAA,MAAM,MAAe,MAAM;AAAA;AAAA,MAAuC;AAAA,KAAO;AACzE,IAAA,IACC,GAAA,IACA,OAAO,GAAA,KAAQ,QAAA,IACf,YAAY,GAAA,IACZ,OAAQ,GAAA,CAA4B,MAAA,KAAW,UAAA,EAC9C;AACD,MAAA,OAAQ,GAAA,CAAgC,MAAA;AAAA,IACzC;AACA,IAAA,OAAO,IAAA;AAAA,EACR,CAAA,CAAA,MAAQ;AACP,IAAA,OAAO,IAAA;AAAA,EACR;AACD;AAEA,SAAS,mBAAA,CAAoB,OAAoB,GAAA,EAA0B;AAC1E,EAAA,IAAI,CAAC,MAAM,eAAA,EAAiB;AAC5B,EAAA,IAAI,KAAA,CAAM,aAAA,CAAc,MAAA,KAAW,CAAA,EAAG;AACtC,EAAA,IAAI,KAAA,CAAM,kBAAkB,mBAAA,EAAqB;AAChD,IAAA,MAAM,GAAA,GAAM,KAAK,GAAA,EAAI;AACrB,IAAA,IAAI,GAAA,GAAM,KAAA,CAAM,oBAAA,GAAuB,4BAAA,EAA8B;AACpE,MAAA,KAAA,CAAM,oBAAA,GAAuB,GAAA;AAC7B,MAAA,GAAA,CAAI,MAAA,CAAO,IAAA;AAAA,QACV,CAAA,mBAAA,EAAsB,MAAM,cAAc,CAAA,2CAAA;AAAA,OAC3C;AAAA,IACD;AACA,IAAA,KAAA,CAAM,gBAAgB,EAAC;AACvB,IAAA,KAAA,CAAM,YAAA,GAAe,CAAA;AACrB,IAAA,KAAA,CAAM,cAAA,GAAiB,IAAA;AACvB,IAAA,KAAA,CAAM,aAAA,GAAgB,IAAA;AAEtB,IAAA,KAAA,CAAM,sBAAA,IAA0B,CAAA;AAChC,IAAA;AAAA,EACD;AAIA,EAAA,IAAI;AACH,IAAA,GAAA,CAAI,WAAA,IAAc;AAAA,EACnB,SAAS,GAAA,EAAK;AACb,IAAA,GAAA,CAAI,MAAA,CAAO,IAAA,CAAK,qCAAA,EAAuC,GAAG,CAAA;AAAA,EAC3D;AACA,EAAA,MAAM,SAAS,KAAA,CAAM,aAAA;AACrB,EAAA,MAAM,aAAa,MAAA,CAAO,MAAA;AAC1B,EAAA,MAAM,OAAA,GAAU,MAAM,cAAA,IAAkB,CAAA;AACxC,EAAA,MAAM,MAAA,GAAS,MAAM,aAAA,IAAiB,OAAA;AACtC,EAAA,MAAM,UAAA,GAAa,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,SAAS,OAAO,CAAA;AAC/C,EAAA,MAAM,MAAM,KAAA,CAAM,YAAA;AAClB,EAAA,KAAA,CAAM,YAAA,IAAgB,CAAA;AACtB,EAAA,KAAA,CAAM,gBAAgB,EAAC;AACvB,EAAA,KAAA,CAAM,YAAA,GAAe,CAAA;AACrB,EAAA,KAAA,CAAM,cAAA,GAAiB,IAAA;AACvB,EAAA,KAAA,CAAM,aAAA,GAAgB,IAAA;AAEtB,EAAA,MAAM,kBAAkB,KAAA,CAAM,eAAA;AAC9B,EAAA,MAAM,MAAA,GAAS,MAAM,OAAA,CAAQ,MAAA;AAC7B,EAAA,MAAM,WAAW,KAAA,CAAM,QAAA;AACvB,EAAA,MAAM,WAAA,GAAc,MAAM,OAAA,CAAQ,gBAAA;AAElC,EAAA,MAAM,gBAAgB,KAAA,CAAM,sBAAA;AAC5B,EAAA,KAAA,CAAM,cAAA,IAAkB,CAAA;AACxB,EAAA,KAAA,CAAM,WAAA,GAAc,KAAA,CAAM,WAAA,CAAY,IAAA,CAAK,YAAY;AACtD,IAAA,IAAI;AACH,MAAA,IAAI,MAAM,SAAA,EAAW;AACrB,MAAA,MAAM,IAAA,GAAO,IAAA,CAAK,SAAA,CAAU,MAAM,CAAA;AAClC,MAAA,MAAM,KAAA,GAAQ,MAAM,SAAA,CAAU,IAAI,CAAA;AAClC,MAAA,IAAI,KAAA,CAAM,aAAa,mBAAA,EAAqB;AAC3C,QAAA,GAAA,CAAI,MAAA,CAAO,KAAA;AAAA,UACV,CAAA,MAAA,EAAS,GAAG,CAAA,uBAAA,EAA0B,KAAA,CAAM,UAAU,CAAA,iBAAA;AAAA,SACvD;AAIA,QAAA,KAAA,CAAM,sBAAA,IAA0B,CAAA;AAChC,QAAA;AAAA,MACD;AACA,MAAA,MAAM,SAAS,MAAM,WAAA;AAAA,QACpB,MAAA;AAAA,QACA,eAAA;AAAA,QACA,QAAA;AAAA,QACA,GAAA;AAAA,QACA,KAAA;AAAA,QACA,UAAA;AAAA,QACA,UAAA;AAAA,QACA,GAAA,CAAI,MAAA;AAAA,QACJ,WAAA;AAAA,QACA;AAAA,OACD;AACA,MAAA,IAAI,MAAA,CAAO,EAAA,IAAM,aAAA,GAAgB,CAAA,EAAG;AAInC,QAAA,KAAA,CAAM,yBAAyB,IAAA,CAAK,GAAA;AAAA,UACnC,CAAA;AAAA,UACA,MAAM,sBAAA,GAAyB;AAAA,SAChC;AAAA,MACD;AACA,MAAA,IAAI,OAAO,WAAA,EAAa;AACvB,QAAA,KAAA,CAAM,OAAA,GAAU,IAAA;AAChB,QAAA,SAAA,CAAU,KAAK,CAAA;AAAA,MAChB;AAAA,IACD,SAAS,GAAA,EAAK;AACb,MAAA,GAAA,CAAI,MAAA,CAAO,KAAA,CAAM,CAAA,MAAA,EAAS,GAAG,kBAAkB,GAAG,CAAA;AAAA,IACnD,CAAA,SAAE;AACD,MAAA,KAAA,CAAM,cAAA,IAAkB,CAAA;AAAA,IACzB;AAAA,EACD,CAAC,CAAA;AACF;AAEA,SAAS,iBAAA,CAAkB,OAAoB,GAAA,EAA0B;AACxE,EAAA,IAAI,KAAA,CAAM,OAAA,IAAW,KAAA,CAAM,SAAA,EAAW;AACtC,EAAA,IAAI,KAAA,CAAM,aAAA,CAAc,MAAA,KAAW,CAAA,EAAG;AACtC,EAAA,mBAAA,CAAoB,OAAO,GAAG,CAAA;AAC/B;AAEA,SAAS,UAAU,KAAA,EAA0B;AAC5C,EAAA,IAAI,MAAM,aAAA,EAAe;AACxB,IAAA,IAAI;AACH,MAAA,KAAA,CAAM,aAAA,EAAc;AAAA,IACrB,CAAA,CAAA,MAAQ;AAAA,IAER;AACA,IAAA,KAAA,CAAM,aAAA,GAAgB,IAAA;AAAA,EACvB;AACA,EAAA,IAAI,MAAM,eAAA,EAAiB;AAC1B,IAAA,aAAA,CAAc,MAAM,eAAe,CAAA;AACnC,IAAA,KAAA,CAAM,eAAA,GAAkB,IAAA;AAAA,EACzB;AACD;AAEA,SAAS,cAAA,CAAe,OAAoB,GAAA,EAA0B;AACrE,EAAA,IAAI,MAAM,SAAA,IAAa,KAAA,CAAM,WAAW,KAAA,CAAM,aAAA,IAAiB,MAAM,cAAA,EAAgB;AACrF,EAAA,KAAA,CAAM,cAAA,GAAiB,IAAA;AACvB,EAAA,KAAK,eAAA,EAAgB,CAAE,IAAA,CAAK,CAAA,MAAA,KAAU;AACrC,IAAA,KAAA,CAAM,cAAA,GAAiB,KAAA;AACvB,IAAA,IAAI,KAAA,CAAM,SAAA,IAAa,KAAA,CAAM,OAAA,IAAW,CAAC,MAAA,EAAQ;AAChD,MAAA,IAAI,CAAC,MAAA,EAAQ,GAAA,CAAI,MAAA,CAAO,KAAK,uCAAuC,CAAA;AACpE,MAAA;AAAA,IACD;AACA,IAAA,IAAI;AACH,MAAA,MAAM,OAAO,MAAA,CAAO;AAAA,QACnB,MAAM,CAAA,KAAA,KAAS;AACd,UAAA,IAAI,KAAA,CAAM,OAAA,IAAW,KAAA,CAAM,SAAA,EAAW;AACtC,UAAA,IAAI,KAAA,CAAM,kBAAA,KAAuB,IAAA,EAAM,KAAA,CAAM,qBAAqB,KAAA,CAAM,SAAA;AASxE,UAAA,IAAI,KAAA,CAAM,SAAS,8BAAA,EAAgC;AAClD,YAAA,MAAM,GAAA,GAAM,KAAK,GAAA,EAAI;AACrB,YAAA,IACC,MAAM,aAAA,CAAc,MAAA,GAAS,KAC7B,GAAA,GAAM,KAAA,CAAM,uBAAuB,6BAAA,EAClC;AACD,cAAA,mBAAA,CAAoB,OAAO,GAAG,CAAA;AAC9B,cAAA,KAAA,CAAM,mBAAA,GAAsB,GAAA;AAAA,YAC7B;AAAA,UACD;AACA,UAAA,KAAA,CAAM,aAAA,CAAc,KAAK,KAAK,CAAA;AAO9B,UAAA,KAAA,CAAM,YAAA,IAAgB,mBAAmB,KAAK,CAAA;AAC9C,UAAA,IAAI,KAAA,CAAM,cAAA,KAAmB,IAAA,EAAM,KAAA,CAAM,iBAAiB,KAAA,CAAM,SAAA;AAChE,UAAA,KAAA,CAAM,gBAAgB,KAAA,CAAM,SAAA;AAC5B,UAAA,IACC,KAAA,CAAM,aAAA,CAAc,MAAA,IAAU,KAAA,CAAM,OAAA,CAAQ,kBAC5C,KAAA,CAAM,YAAA,IAAgB,KAAA,CAAM,OAAA,CAAQ,aAAA,EACnC;AACD,YAAA,mBAAA,CAAoB,OAAO,GAAG,CAAA;AAAA,UAC/B;AAAA,QACD,CAAA;AAAA,QACA,aAAA,EAAe,MAAM,OAAA,CAAQ,aAAA;AAAA,QAC7B,gBAAA,EAAkB,KAAA,CAAM,OAAA,CAAQ,gBAAA,IAAoB,KAAA,CAAA;AAAA,QACpD,gBAAA,EAAkB,MAAM,OAAA,CAAQ,gBAAA;AAAA,QAChC,aAAA,EAAe,MAAM,OAAA,CAAQ,aAAA;AAAA,QAC7B,QAAA,EAAU,MAAM,OAAA,CAAQ,QAAA;AAAA,QACxB,gBAAA,EAAkB,MAAM,OAAA,CAAQ;AAAA,OAChC,CAAA;AACD,MAAA,KAAA,CAAM,aAAA,GAAgB,IAAA;AACtB,MAAA,KAAA,CAAM,MAAA,GAAS,MAAA;AACf,MAAA,sBAAA,CAAuB,OAAO,GAAG,CAAA;AAEjC,MAAA,KAAA,CAAM,eAAA,GAAkB,WAAA;AAAA,QACvB,MAAM,iBAAA,CAAkB,KAAA,EAAO,GAAG,CAAA;AAAA,QAClC,KAAA,CAAM,QAAQ,YAAA,GAAe;AAAA,OAC9B;AAAA,IACD,SAAS,GAAA,EAAK;AACb,MAAA,GAAA,CAAI,MAAA,CAAO,KAAA,CAAM,sBAAA,EAAwB,GAAG,CAAA;AAAA,IAC7C;AAAA,EACD,CAAC,CAAA;AACF;AAEA,SAAS,sBAAA,CAAuB,OAAoB,GAAA,EAA0B;AAC7E,EAAA,IAAI,KAAA,CAAM,aAAa,KAAA,CAAM,OAAA,IAAW,CAAC,KAAA,CAAM,MAAA,IAAU,CAAC,KAAA,CAAM,aAAA,EAAe;AAC/E,EAAA,MAAM,EAAA,GAAK,MAAM,MAAA,CAAO,gBAAA;AACxB,EAAA,IAAI,OAAO,OAAO,UAAA,EAAY;AAC9B,EAAA,IAAI;AACH,IAAA,EAAA,CAAG,IAAI,CAAA;AAAA,EACR,SAAS,GAAA,EAAK;AACb,IAAA,GAAA,CAAI,MAAA,CAAO,IAAA,CAAK,wBAAA,EAA0B,GAAG,CAAA;AAAA,EAC9C;AACD;AAEA,SAAS,QAAA,CAAS,KAAA,EAAoB,GAAA,EAAoB,IAAA,EAAoC;AAC7F,EAAA,IAAI,CAAC,MAAM,eAAA,EAAiB;AAC5B,EAAA,IAAI,MAAM,aAAA,CAAc,MAAA,GAAS,CAAA,EAAG,iBAAA,CAAkB,OAAO,GAAG,CAAA;AAChE,EAAA,MAAM,GAAA,GAAM,OAAA;AAAA,IACX,MAAM,OAAA,CAAQ,MAAA;AAAA,IACd,CAAA,qBAAA,EAAwB,kBAAA,CAAmB,KAAA,CAAM,eAAe,CAAC,CAAA,SAAA;AAAA,GAClE;AACA,EAAA,MAAM,IAAA,GAAO,IAAA,CAAK,SAAA,CAAU,EAAE,QAAA,EAAU,KAAA,CAAM,QAAA,EAAU,OAAA,EAAA,iBAAS,IAAI,IAAA,EAAK,EAAE,WAAA,IAAe,CAAA;AAC3F,EAAA,IAAI,KAAK,SAAA,IAAa,OAAO,SAAA,KAAc,WAAA,IAAe,UAAU,UAAA,EAAY;AAC/E,IAAA,IAAI;AACH,MAAA,MAAM,IAAA,GAAO,IAAI,IAAA,CAAK,CAAC,IAAI,CAAA,EAAG,EAAE,IAAA,EAAM,kBAAA,EAAoB,CAAA;AAC1D,MAAA,SAAA,CAAU,UAAA,CAAW,KAAK,IAAI,CAAA;AAC9B,MAAA;AAAA,IACD,SAAS,GAAA,EAAK;AACb,MAAA,GAAA,CAAI,MAAA,CAAO,IAAA,CAAK,2BAAA,EAA6B,GAAG,CAAA;AAAA,IACjD;AAAA,EACD;AACA,EAAA,KAAK,MAAM,GAAA,EAAK;AAAA,IACf,MAAA,EAAQ,MAAA;AAAA,IACR,IAAA;AAAA,IACA,OAAA,EAAS,EAAE,cAAA,EAAgB,kBAAA,EAAmB;AAAA,IAC9C,SAAA,EAAW;AAAA,GACX,EAAE,KAAA,CAAM,CAAA,GAAA,KAAO,IAAI,MAAA,CAAO,IAAA,CAAK,uBAAA,EAAyB,GAAG,CAAC,CAAA;AAC9D;AAOO,SAAS,aAAA,CAAc,OAAA,GAAgC,EAAC,EAAgB;AAC9E,EAAA,MAAM,MAAA,GAA0B;AAAA,IAC/B,GAAG,QAAA;AAAA,IACH,GAAG,OAAA;AAAA,IACH,QAAA,EAAU,EAAE,GAAG,QAAA,CAAS,UAAU,GAAI,OAAA,CAAQ,QAAA,IAAY,EAAC;AAAG,GAC/D;AAEA,EAAA,OAAO;AAAA,IACN,IAAA,EAAM,gBAAA;AAAA,IACN,OAAO,GAAA,EAAK;AACX,MAAA,IAAI,OAAO,WAAW,WAAA,EAAa;AACnC,MAAA,IAAI,OAAO,UAAA,GAAa,CAAA,IAAK,KAAK,MAAA,EAAO,IAAK,OAAO,UAAA,EAAY;AAChE,QAAA,GAAA,CAAI,MAAA,CAAO,MAAM,uBAAuB,CAAA;AACxC,QAAA;AAAA,MACD;AAEA,MAAA,MAAM,MAAA,GAAS,MAAA,CAAO,MAAA,IAAU,GAAA,CAAI,OAAA;AACpC,MAAA,IAAI,CAAC,MAAA,EAAQ;AACZ,QAAA,GAAA,CAAI,MAAA,CAAO,MAAM,+DAA+D,CAAA;AAChF,QAAA;AAAA,MACD;AACA,MAAA,MAAM,eAAe,gBAAA,EAAiB;AAGtC,MAAA,MAAM,cAAc,oBAAA,EAAqB;AAEzC,MAAA,MAAM,KAAA,GAAqB;AAAA,QAC1B,OAAA,EAAS,EAAE,GAAG,MAAA,EAAQ,MAAA,EAAO;AAAA,QAC7B,UAAU,GAAA,CAAI,QAAA;AAAA,QACd,YAAA;AAAA,QACA,eAAA,EAAiB,IAAA;AAAA,QACjB,kBAAA,EAAoB,IAAA;AAAA,QACpB,eAAe,EAAC;AAAA,QAChB,YAAA,EAAc,CAAA;AAAA,QACd,cAAA,EAAgB,IAAA;AAAA,QAChB,aAAA,EAAe,IAAA;AAAA,QACf,oBAAA,EAAsB,CAAA;AAAA,QACtB,sBAAA,EAAwB,CAAA;AAAA,QACxB,mBAAA,EAAqB,CAAA;AAAA,QACrB,YAAA,EAAc,CAAA;AAAA,QACd,WAAA,EAAa,QAAQ,OAAA,EAAQ;AAAA,QAC7B,cAAA,EAAgB,CAAA;AAAA,QAChB,eAAA,EAAiB,IAAA;AAAA,QACjB,UAAA,EAAY,IAAA;AAAA,QACZ,eAAA,EAAiB,IAAA;AAAA,QACjB,mBAAA,EAAqB,IAAA;AAAA,QACrB,MAAA,EAAQ,IAAA;AAAA,QACR,aAAA,EAAe,IAAA;AAAA,QACf,cAAA,EAAgB,KAAA;AAAA,QAChB,SAAA,EAAW,KAAA;AAAA,QACX,OAAA,EAAS;AAAA,OACV;AACA,MAAA,GAAA,CAAI,SAAS,KAAK,CAAA;AAElB,MAAA,MAAM,cAAA,GAAiB,MAAY,sBAAA,CAAuB,KAAA,EAAO,GAAG,CAAA;AACpE,MAAA,KAAA,CAAM,mBAAA,GAAsB,cAAA;AAC5B,MAAA,MAAA,CAAO,gBAAA,CAAiB,uBAAuB,cAAc,CAAA;AAE7D,MAAA,MAAM,aAAa,MAAY;AAC9B,QAAA,QAAA,CAAS,KAAA,EAAO,GAAA,EAAK,EAAE,SAAA,EAAW,MAAM,CAAA;AACxC,QAAA,KAAA,CAAM,OAAA,GAAU,IAAA;AAChB,QAAA,SAAA,CAAU,KAAK,CAAA;AAAA,MAChB,CAAA;AACA,MAAA,KAAA,CAAM,eAAA,GAAkB,UAAA;AACxB,MAAA,MAAA,CAAO,gBAAA,CAAiB,YAAY,UAAU,CAAA;AAE9C,MAAA,MAAM,QAAQ,YAA2B;AACxC,QAAA,IAAI,MAAM,SAAA,EAAW;AAQrB,QAAA,IAAI;AACH,UAAA,GAAA,CAAI,WAAA,IAAc;AAAA,QACnB,SAAS,GAAA,EAAK;AACb,UAAA,GAAA,CAAI,MAAA,CAAO,IAAA,CAAK,oCAAA,EAAsC,GAAG,CAAA;AAAA,QAC1D;AACA,QAAA,MAAM,UAAU,MAAM,aAAA,CAAc,QAAQ,GAAA,CAAI,QAAA,EAAU,cAAc,WAAW,CAAA;AACnF,QAAA,IAAI,CAAC,OAAA,EAAS;AACb,UAAA,GAAA,CAAI,MAAA,CAAO,KAAK,wCAAwC,CAAA;AACxD,UAAA,KAAA,CAAM,OAAA,GAAU,IAAA;AAChB,UAAA;AAAA,QACD;AACA,QAAA,IAAI,CAAC,QAAQ,QAAA,EAAU;AACtB,UAAA,GAAA,CAAI,OAAO,IAAA,CAAK,CAAA,yBAAA,EAA4B,OAAA,CAAQ,UAAA,IAAc,SAAS,CAAA,CAAE,CAAA;AAC7E,UAAA,KAAA,CAAM,OAAA,GAAU,IAAA;AAChB,UAAA;AAAA,QACD;AACA,QAAA,IAAI,CAAC,QAAQ,eAAA,EAAiB;AAC7B,UAAA,GAAA,CAAI,MAAA,CAAO,MAAM,iDAAiD,CAAA;AAClE,UAAA,KAAA,CAAM,OAAA,GAAU,IAAA;AAChB,UAAA;AAAA,QACD;AACA,QAAA,KAAA,CAAM,kBAAkB,OAAA,CAAQ,eAAA;AAChC,QAAA,KAAA,CAAM,kBAAA,GAAqB,KAAK,GAAA,EAAI;AACpC,QAAA,cAAA,CAAe,OAAO,GAAG,CAAA;AAAA,MAC1B,CAAA;AAEA,MAAA,IAAI,MAAA,CAAO,eAAe,CAAA,EAAG;AAC5B,QAAA,MAAM,eAAe,MAAY;AAChC,UAAA,KAAA,CAAM,SAAA,GAAY,IAAA;AAClB,UAAA,IAAI,MAAM,UAAA,EAAY;AACrB,YAAA,YAAA,CAAa,MAAM,UAAU,CAAA;AAC7B,YAAA,KAAA,CAAM,UAAA,GAAa,IAAA;AAAA,UACpB;AAAA,QACD,CAAA;AACA,QAAA,MAAA,CAAO,iBAAiB,UAAA,EAAY,YAAA,EAAc,EAAE,IAAA,EAAM,MAAM,CAAA;AAChE,QAAA,MAAA,CAAO,iBAAiB,cAAA,EAAgB,YAAA,EAAc,EAAE,IAAA,EAAM,MAAM,CAAA;AACpE,QAAA,KAAA,CAAM,UAAA,GAAa,WAAW,MAAM;AACnC,UAAA,KAAK,KAAA,EAAM;AAAA,QACZ,CAAA,EAAG,OAAO,YAAY,CAAA;AAAA,MACvB,CAAA,MAAO;AACN,QAAA,KAAK,KAAA,EAAM;AAAA,MACZ;AAAA,IACD,CAAA;AAAA,IACA,iBAAiB,GAAA,EAAK;AACrB,MAAA,MAAM,KAAA,GAAQ,IAAI,QAAA,EAAsB;AACxC,MAAA,IAAI,CAAC,KAAA,IAAS,KAAA,CAAM,SAAA,IAAa,KAAA,CAAM,SAAS,OAAO,MAAA;AACvD,MAAA,IAAI,CAAC,KAAA,CAAM,eAAA,EAAiB,OAAO,MAAA;AACnC,MAAA,MAAM,QAAA,GACL,KAAA,CAAM,kBAAA,KAAuB,IAAA,GAC1B,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,IAAA,CAAK,GAAA,EAAI,GAAI,KAAA,CAAM,kBAAkB,CAAA,GACjD,CAAA;AACJ,MAAA,OAAO,EAAE,eAAA,EAAiB,KAAA,CAAM,eAAA,EAAiB,gBAAgB,QAAA,EAAS;AAAA,IAC3E,CAAA;AAAA,IACA,UAAU,GAAA,EAAK;AACd,MAAA,MAAM,KAAA,GAAQ,IAAI,QAAA,EAAsB;AACxC,MAAA,IAAI,CAAC,KAAA,EAAO;AACZ,MAAA,KAAA,CAAM,SAAA,GAAY,IAAA;AAClB,MAAA,IAAI,MAAM,UAAA,EAAY;AACrB,QAAA,YAAA,CAAa,MAAM,UAAU,CAAA;AAC7B,QAAA,KAAA,CAAM,UAAA,GAAa,IAAA;AAAA,MACpB;AACA,MAAA,IAAI,MAAM,eAAA,EAAiB;AAC1B,QAAA,MAAA,CAAO,mBAAA,CAAoB,UAAA,EAAY,KAAA,CAAM,eAAe,CAAA;AAC5D,QAAA,KAAA,CAAM,eAAA,GAAkB,IAAA;AAAA,MACzB;AACA,MAAA,IAAI,MAAM,mBAAA,EAAqB;AAC9B,QAAA,MAAA,CAAO,mBAAA,CAAoB,qBAAA,EAAuB,KAAA,CAAM,mBAAmB,CAAA;AAC3E,QAAA,KAAA,CAAM,mBAAA,GAAsB,IAAA;AAAA,MAC7B;AAIA,MAAA,IAAI,KAAA,CAAM,eAAA,IAAmB,CAAC,KAAA,CAAM,OAAA,EAAS;AAC5C,QAAA,QAAA,CAAS,KAAA,EAAO,GAAA,EAAK,EAAE,SAAA,EAAW,OAAO,CAAA;AAAA,MAC1C;AACA,MAAA,KAAA,CAAM,OAAA,GAAU,IAAA;AAChB,MAAA,SAAA,CAAU,KAAK,CAAA;AACf,MAAA,KAAA,CAAM,cAAc,MAAA,GAAS,CAAA;AAC7B,MAAA,KAAA,CAAM,YAAA,GAAe,CAAA;AACrB,MAAA,KAAA,CAAM,MAAA,GAAS,IAAA;AAAA,IAChB;AAAA,GACD;AACD;AAMO,SAAS,kBAAkB,GAAA,EAAiD;AAClF,EAAA,MAAM,KAAA,GAAQ,IAAI,QAAA,EAAsB;AACxC,EAAA,IAAI,CAAC,SAAS,KAAA,CAAM,SAAA,IAAa,MAAM,OAAA,IAAW,CAAC,KAAA,CAAM,eAAA,EAAiB,OAAO,IAAA;AACjF,EAAA,MAAM,QAAA,GACL,KAAA,CAAM,kBAAA,KAAuB,IAAA,GAC1B,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,IAAA,CAAK,GAAA,EAAI,GAAI,KAAA,CAAM,kBAAkB,CAAA,GACjD,CAAA;AACJ,EAAA,OAAO,EAAE,EAAA,EAAI,KAAA,CAAM,eAAA,EAAiB,QAAA,EAAS;AAC9C;AAGO,IAAM,QAAA,GAAW;AAAA,EACvB,aAAA;AAAA,EACA,SAAA;AAAA,EACA,gBAAA;AAAA,EACA,WAAA;AAAA,EACA,aAAA;AAAA,EACA,OAAA;AAAA,EACA,mBAAA;AAAA,EACA,mBAAA;AAAA,EACA,uBAAA;AAAA,EACA,mBAAA;AAAA,EACA,4BAAA;AAAA,EACA;AACD","file":"session-replay.cjs","sourcesContent":["// Identity layer for the Usero SDK.\n//\n// Two responsibilities:\n// 1. Mint and persist a stable per-browser `anonymousId` in localStorage\n// so cross-tab + cross-day replays from the same browser stitch\n// together server-side. Falls back to an in-memory id if storage is\n// blocked (sandboxed iframes, Safari Lockdown, full quota). Replay\n// still works in that case, you just lose stitching.\n// 2. Auto-fire POST /api/identify when the resolved user transitions\n// (null -> id, id -> id'). Deduped by an in-memory fingerprint so\n// re-renders with the same user are no-ops on the network.\n//\n// All storage access is wrapped in try/catch and gated behind a one-shot\n// init read. The hot path (replay chunk flush) never touches localStorage.\n\nconst ANON_STORAGE_KEY = 'usero:anonymous-id'\n\nlet cachedAnonymousId: string | null = null\n// Fingerprint of the last identify we POSTed. Same SDK instance + same\n// resolved user + same traits = no-op. Cleared on logout (anonymousId\n// rotation).\nlet lastIdentifyFingerprint: string | null = null\n\nexport type UserTraitValue = string | number | boolean | null\nexport type UserTraits = Record<string, UserTraitValue>\n\nexport interface UseroUser {\n\tid: string\n\temail?: string\n\tdisplayName?: string\n\ttraits?: UserTraits\n}\n\nfunction generateRandomId(): string {\n\tif (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {\n\t\treturn crypto.randomUUID()\n\t}\n\tconst bytes = new Uint8Array(16)\n\tif (typeof crypto !== 'undefined' && typeof crypto.getRandomValues === 'function') {\n\t\tcrypto.getRandomValues(bytes)\n\t} else {\n\t\tfor (let i = 0; i < bytes.length; i += 1) bytes[i] = Math.floor(Math.random() * 256)\n\t}\n\tlet out = ''\n\tfor (const b of bytes) out += b.toString(16).padStart(2, '0')\n\treturn out\n}\n\nfunction safeReadLocalStorage(key: string): string | null {\n\tif (typeof window === 'undefined') return null\n\ttry {\n\t\treturn window.localStorage?.getItem(key) ?? null\n\t} catch {\n\t\treturn null\n\t}\n}\n\nfunction safeWriteLocalStorage(key: string, value: string): void {\n\tif (typeof window === 'undefined') return\n\ttry {\n\t\twindow.localStorage?.setItem(key, value)\n\t} catch {\n\t\t// Sandboxed iframe / Safari Lockdown / quota. Fall back to memory.\n\t}\n}\n\n/**\n * Returns the stable per-browser anonymousId. Reads localStorage at most\n * once per SDK instance. Subsequent calls hit the in-memory cache, so\n * even hot paths (per-event in replay) are safe to call this.\n */\nexport function getOrMintAnonymousId(): string {\n\tif (cachedAnonymousId) return cachedAnonymousId\n\tconst existing = safeReadLocalStorage(ANON_STORAGE_KEY)\n\t// Sanity filter, not strict validation. We accept anything that looks\n\t// plausibly like an id (>=8 alphanumeric-or-hyphen) so older SDK\n\t// versions that wrote a slightly different shape still stitch. Fresh\n\t// mint is cheap, so we only reject obvious garbage; tightening this\n\t// would force rotation in customer browsers and split otherwise-good\n\t// sibling-session attribution.\n\tif (existing && /^[a-z0-9-]{8,}$/i.test(existing)) {\n\t\tcachedAnonymousId = existing\n\t\treturn existing\n\t}\n\tconst id = generateRandomId()\n\tsafeWriteLocalStorage(ANON_STORAGE_KEY, id)\n\tcachedAnonymousId = id\n\treturn id\n}\n\n/**\n * Rotate the anonymousId. Called on logout (user transitions from a\n * known id to null) so the next anonymous trail does not get auto-merged\n * into the previous person on the next identify().\n */\nexport function rotateAnonymousId(): string {\n\tconst id = generateRandomId()\n\tcachedAnonymousId = id\n\tsafeWriteLocalStorage(ANON_STORAGE_KEY, id)\n\tlastIdentifyFingerprint = null\n\treturn id\n}\n\nfunction fingerprintUser(anonymousId: string, user: UseroUser): string {\n\t// Stable across re-renders: keys sorted, traits canonicalised. Cheap\n\t// enough on the hot path (only runs when the SDK thinks the user might\n\t// have changed, never per-event).\n\tconst traits = user.traits ?? {}\n\tconst keys = Object.keys(traits).sort()\n\tconst canonical: Array<[string, UserTraitValue]> = keys.map(k => [k, traits[k] ?? null])\n\treturn JSON.stringify([anonymousId, user.id, user.email ?? null, user.displayName ?? null, canonical])\n}\n\nexport interface IdentifyTransport {\n\tapiUrl: string\n\tclientId: string\n}\n\n/**\n * POST to /api/identify if the (anonymousId, user) fingerprint differs\n * from the last call. Returns true if a network request actually fired.\n * Never throws; failures are best-effort and the caller (the widget /\n * provider) should not treat them as errors.\n *\n * Tab-unload safety: if the page is hidden when this fires (visibility\n * 'hidden' or a pagehide handler), we route the payload through\n * `navigator.sendBeacon` so the request survives unload. Otherwise we\n * use a normal fetch and only cache the fingerprint when the server\n * confirms `accepted: true`. A 200 `{ accepted: false }` (e.g.\n * `unknown_client` for a clientId that becomes valid mid-session) is\n * treated as retryable so the next call re-fires.\n */\nexport async function identifyIfChanged(transport: IdentifyTransport, user: UseroUser): Promise<boolean> {\n\tconst anonymousId = getOrMintAnonymousId()\n\tconst fp = fingerprintUser(anonymousId, user)\n\tif (fp === lastIdentifyFingerprint) return false\n\n\tconst url = `${transport.apiUrl.replace(/\\/$/, '')}/api/identify`\n\t// Body must stay under the browser's keepalive / sendBeacon cap\n\t// (~64KB across most engines) when this fires on pagehide. That\n\t// transitively caps trait payload size; in practice traits should be\n\t// small typed scalars, not blobs.\n\tconst body = JSON.stringify({\n\t\tclientId: transport.clientId,\n\t\tanonymousId,\n\t\texternalUserId: user.id,\n\t\temail: user.email,\n\t\tdisplayName: user.displayName,\n\t\ttraits: user.traits,\n\t})\n\n\t// If the document is hidden (pagehide / tab close in flight), best-effort\n\t// hand off to sendBeacon. We don't get a response back, so we optimistically\n\t// cache the fingerprint to avoid re-firing on the next page; the server is\n\t// idempotent if the page reload re-runs identify with the same payload.\n\tif (\n\t\ttypeof document !== 'undefined' &&\n\t\tdocument.visibilityState === 'hidden' &&\n\t\ttypeof navigator !== 'undefined' &&\n\t\ttypeof navigator.sendBeacon === 'function'\n\t) {\n\t\ttry {\n\t\t\tconst blob = new Blob([body], { type: 'application/json' })\n\t\t\t// sendBeacon returns false when the user agent refuses to queue\n\t\t\t// the request (size cap, or historically Safari rejecting\n\t\t\t// non-CORS-simple content types). Modern Safari accepts\n\t\t\t// application/json, but we keep a keepalive-fetch fallback so an\n\t\t\t// older WebKit that rejects the beacon still ships the identify.\n\t\t\tif (navigator.sendBeacon(url, blob)) {\n\t\t\t\tlastIdentifyFingerprint = fp\n\t\t\t\treturn true\n\t\t\t}\n\t\t} catch {\n\t\t\t// fall through to keepalive fetch below\n\t\t}\n\t}\n\n\ttry {\n\t\tconst res = await fetch(url, {\n\t\t\tmethod: 'POST',\n\t\t\theaders: { 'Content-Type': 'application/json' },\n\t\t\tbody,\n\t\t\t// keepalive lets the request survive a tab-close mid-flight on\n\t\t\t// browsers that support it; sendBeacon above is the primary path.\n\t\t\tkeepalive: true,\n\t\t})\n\t\tif (!res.ok) return true\n\t\t// Parse the response: a 200 with `accepted: false` (e.g. unknown\n\t\t// client) is retryable. Only cache the fingerprint when the server\n\t\t// confirmed it actually stored the identity.\n\t\ttry {\n\t\t\tconst json = (await res.json()) as { accepted?: unknown }\n\t\t\tif (json && json.accepted === true) lastIdentifyFingerprint = fp\n\t\t} catch {\n\t\t\t// Server returned 2xx but unparseable body: don't cache, let the\n\t\t\t// next call retry.\n\t\t}\n\t\treturn true\n\t} catch {\n\t\treturn false\n\t}\n}\n\n/**\n * Clear identify state and rotate anonymousId. Called when the resolved\n * user transitions from a known id to null (logout). The next anonymous\n * trail will get a fresh anonymousId so it does not merge into the\n * previous person.\n */\nexport function handleLogout(): void {\n\trotateAnonymousId()\n}\n\n// Test hooks (not exported from the package public surface).\nexport const __test__ = {\n\tANON_STORAGE_KEY,\n\tresetIdentityState: (): void => {\n\t\tcachedAnonymousId = null\n\t\tlastIdentifyFingerprint = null\n\t},\n}\n","// Session replay plugin for the Usero widget.\n//\n// Streams rrweb events to the SaaS side as gzipped chunks while the user\n// is on the page, instead of buffering in memory and attaching to a\n// feedback submission. This decouples session replay from feedback so we\n// capture every session (subject to bot-gate + sampling + engagement\n// gates), not just the ones that submit feedback.\n//\n// Lifecycle:\n// 1. onInit: dice-roll sample, optional engagement-time gate, mint a\n// stable per-tab `sdkSessionId` in sessionStorage, and POST to\n// /api/replay-sessions to create the row. If the server returns\n// `{accepted:false}` (bot-gated), the plugin no-ops the rest of the\n// session and getCurrentSession() returns null.\n// 2. Recording: lazy-load rrweb, append events to a buffer, flush a\n// chunk every `chunkSeconds` (or sooner if the buffer is large).\n// Each chunk is gzipped via CompressionStream and PUT to\n// /api/replay-sessions/:id/chunks/:seq with raw bytes + the three\n// X-Usero-* headers (Client-Id, Event-Count, Duration-Ms). Retries\n// with exponential backoff. R2 head-check makes retries idempotent\n// server-side. A chunk PUT returning 409 stops the session.\n// 3. onFeedbackSubmit: returns `{sessionReplayId, replayOffsetMs}` so\n// the feedback record can FK at the moment of submit. Does NOT\n// attach `replayEvents` (legacy field) — chunked uploads carry the\n// events out-of-band.\n// 4. onDestroy / pagehide: best-effort flush remaining buffer, then\n// sendBeacon to /api/replay-sessions/:id/finalise with the\n// end-timestamp. Idempotent server-side.\n//\n// Bundle hygiene: rrweb stays lazy via dynamic `import('rrweb')` behind\n// the engagement gate, so consumers who lose the dice roll or navigate\n// away inside the gate window pay zero rrweb bytes.\n\nimport { getOrMintAnonymousId } from '../identity'\nimport type { UseroPlugin, PluginContext } from '../plugin'\n\nexport interface ReplaySampling {\n\tmousemove?: number\n\tscroll?: number\n\tmedia?: number\n\tinput?: number | 'last'\n}\n\nexport interface SessionReplayOptions {\n\t// Wait this many ms after page load before loading rrweb and creating\n\t// the session row. If the user navigates away first, rrweb is never\n\t// loaded and no session row is created. Default 0 (start immediately).\n\tstartAfterMs?: number\n\t// Probability (0..1) that this session records at all. Decided once\n\t// at init via Math.random(). Default 1.\n\tsampleRate?: number\n\t// rrweb sampling rates per event type.\n\tsampling?: ReplaySampling\n\t// Mask all <input>/<textarea> values in the recording. Default true.\n\tmaskAllInputs?: boolean\n\t// CSS selector for nodes whose text content should be masked. Default\n\t// `[data-usero-mask]`.\n\tmaskTextSelector?: string\n\t// Inline external stylesheets so the replay viewer renders correctly\n\t// without network access. Default true.\n\tinlineStylesheet?: boolean\n\t// Block (entirely skip) DOM subtrees matching this selector. Default\n\t// `[data-usero-block]`.\n\tblockSelector?: string\n\t// Flush a chunk every N seconds. Default 3. Smaller = more PUTs but\n\t// less data lost on tab crash and less time event refs retain detached\n\t// DOM nodes in memory.\n\tchunkSeconds?: number\n\t// Soft cap on buffered events before forcing a flush, regardless of\n\t// time. Default 1000.\n\tchunkMaxEvents?: number\n\t// Soft cap on estimated buffered bytes before forcing a flush. Default\n\t// 512_000 (~500 KB pre-gzip). Keeps memory pressure bounded on event-heavy\n\t// pages even when chunkMaxEvents hasn't been hit.\n\tchunkMaxBytes?: number\n\t// Max attempts per chunk before giving up. Default 5.\n\tchunkMaxAttempts?: number\n\t// Force rrweb to take a fresh full snapshot every N ms. This resets\n\t// rrweb's internal mirror so detached DOM (e.g. SPA route changes)\n\t// becomes GC-eligible. Default 60_000.\n\tcheckoutEveryMs?: number\n\t// API origin. Override for self-hosted or local dev. Defaults to the\n\t// PluginContext baseUrl threaded through by the widget.\n\tapiUrl?: string\n}\n\ninterface RrwebEvent {\n\ttype: number\n\tdata: unknown\n\ttimestamp: number\n}\n\ninterface RrwebRecordOptions {\n\temit: (event: RrwebEvent) => void\n\tmaskAllInputs?: boolean\n\tmaskTextSelector?: string\n\tinlineStylesheet?: boolean\n\tblockSelector?: string\n\tsampling?: ReplaySampling\n\tcheckoutEveryNms?: number\n}\n\ninterface RrwebRecordFn {\n\t(opts: RrwebRecordOptions): () => void\n\ttakeFullSnapshot?: (isCheckout?: boolean) => void\n}\n\ntype RrwebRecord = RrwebRecordFn\n\ninterface ResolvedOptions {\n\tstartAfterMs: number\n\tsampleRate: number\n\tsampling: ReplaySampling\n\tmaskAllInputs: boolean\n\tmaskTextSelector: string\n\tinlineStylesheet: boolean\n\tblockSelector: string\n\tchunkSeconds: number\n\tchunkMaxEvents: number\n\tchunkMaxBytes: number\n\tchunkMaxAttempts: number\n\tcheckoutEveryMs: number\n\tapiUrl: string\n}\n\ninterface ReplayStore {\n\toptions: ResolvedOptions\n\tclientId: string\n\tsdkSessionId: string\n\tsessionReplayId: string | null\n\t// Wall-clock timestamp (ms) of the first event we ever recorded.\n\t// Used to compute replayOffsetMs at feedback-submit time.\n\trecordingStartedAt: number | null\n\tpendingEvents: RrwebEvent[]\n\tpendingBytes: number\n\tpendingFirstTs: number | null\n\tpendingLastTs: number | null\n\tlastUploadDropWarnAt: number\n\t// Count of chunks dropped (queue saturation) since the last successful\n\t// upload. Sent as a header on the next successful chunk PUT so the\n\t// viewer can show a \"gap here\" marker. Reset on success.\n\tdroppedSinceLastUpload: number\n\t// Wall-clock timestamp of the last snapshot-isolation flush. Used to\n\t// rate-limit pre-snapshot flushes so SPA route-change snapshot bursts\n\t// don't trigger a flush storm.\n\tlastSnapshotFlushAt: number\n\tnextChunkSeq: number\n\tuploadQueue: Promise<void>\n\tpendingUploads: number\n\tchunkFlushTimer: ReturnType<typeof setInterval> | null\n\tstartTimer: ReturnType<typeof setTimeout> | null\n\tpageHideHandler: (() => void) | null\n\tshadowUpdateHandler: ((event: Event) => void) | null\n\trecord: RrwebRecord | null\n\tstopRecording: (() => void) | null\n\tloadInProgress: boolean\n\tcancelled: boolean\n\t// True once the session is \"done\": bot-gated, finalised, or destroyed.\n\tstopped: boolean\n}\n\nconst DEFAULTS: ResolvedOptions = {\n\tstartAfterMs: 0,\n\tsampleRate: 1,\n\tsampling: { mousemove: 50, scroll: 100 },\n\tmaskAllInputs: true,\n\tmaskTextSelector: '[data-usero-mask]',\n\tinlineStylesheet: true,\n\tblockSelector: '[data-usero-block]',\n\tchunkSeconds: 3,\n\tchunkMaxEvents: 1000,\n\tchunkMaxBytes: 512_000,\n\tchunkMaxAttempts: 5,\n\tcheckoutEveryMs: 60_000,\n\tapiUrl: '',\n}\n\nconst SDK_SESSION_STORAGE_KEY = 'usero:session-replay:sdk-session-id'\nconst HARD_CHUNK_BYTE_CAP = 4 * 1024 * 1024\nconst MAX_PENDING_UPLOADS = 3\nconst UPLOAD_DROP_WARN_INTERVAL_MS = 5000\n// rrweb EventType.FullSnapshot. We don't import rrweb's enum because rrweb is\n// dynamically imported (bundle hygiene), so we'd have to pay the load cost\n// just to reference a constant. Magic number matches estimateEventBytes above\n// and rrweb's stable public event-type enum.\nconst RRWEB_EVENT_TYPE_FULL_SNAPSHOT = 2\n// Minimum gap between back-to-back snapshot-isolation flushes. Snapshots\n// normally fire every checkoutEveryMs (default 60s), but rrweb can emit\n// additional ones on SPA route changes via checkoutEveryNms. Keeping this\n// below chunkSeconds * 1000 / 2 of the default (3000ms) and well under\n// checkoutEveryMs ensures isolation still happens for back-to-back snapshots\n// while preventing pathological flush storms.\nconst SNAPSHOT_ISOLATION_MIN_GAP_MS = 1500\n\nfunction uint8ToBase64(bytes: Uint8Array): string {\n\tlet binary = ''\n\tconst chunkSize = 0x8000\n\tfor (let i = 0; i < bytes.length; i += chunkSize) {\n\t\tconst slice = bytes.subarray(i, i + chunkSize)\n\t\tbinary += String.fromCharCode.apply(null, Array.from(slice))\n\t}\n\treturn typeof btoa === 'function' ? btoa(binary) : ''\n}\n\nasync function gzipBytes(input: string): Promise<Uint8Array> {\n\tif (typeof CompressionStream === 'undefined') {\n\t\t// Old browsers: send uncompressed JSON. Acceptable degradation;\n\t\t// the server endpoint accepts raw application/octet-stream.\n\t\treturn new TextEncoder().encode(input)\n\t}\n\tconst stream = new Blob([input]).stream().pipeThrough(new CompressionStream('gzip'))\n\tconst buf = await new Response(stream).arrayBuffer()\n\treturn new Uint8Array(buf)\n}\n\nfunction generateRandomId(): string {\n\tif (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {\n\t\treturn crypto.randomUUID()\n\t}\n\tconst bytes = new Uint8Array(16)\n\tif (typeof crypto !== 'undefined' && typeof crypto.getRandomValues === 'function') {\n\t\tcrypto.getRandomValues(bytes)\n\t} else {\n\t\tfor (let i = 0; i < bytes.length; i += 1) bytes[i] = Math.floor(Math.random() * 256)\n\t}\n\tlet out = ''\n\tfor (const b of bytes) out += b.toString(16).padStart(2, '0')\n\treturn out\n}\n\nfunction mintSdkSessionId(): string {\n\ttry {\n\t\tconst existing = window.sessionStorage?.getItem(SDK_SESSION_STORAGE_KEY)\n\t\tif (existing && /^[a-z0-9-]{8,}$/i.test(existing)) return existing\n\t} catch {\n\t\t// sessionStorage can throw in sandboxed iframes — fall through.\n\t}\n\tconst id = generateRandomId()\n\ttry {\n\t\twindow.sessionStorage?.setItem(SDK_SESSION_STORAGE_KEY, id)\n\t} catch {\n\t\t// Ignore: we still return the freshly minted id.\n\t}\n\treturn id\n}\n\nfunction joinUrl(apiUrl: string, path: string): string {\n\treturn `${apiUrl.replace(/\\/$/, '')}${path}`\n}\n\n// Cheap per-event byte estimate. Avoids JSON.stringify on the hot emit path.\n// rrweb EventType: 0=DomContentLoaded, 1=Load, 2=FullSnapshot, 3=IncrementalSnapshot,\n// 4=Meta, 5=Custom, 6=Plugin. Full snapshots are the only event class that's\n// genuinely large; everything else is well under a KB on average. Numbers\n// chosen to over-estimate slightly so chunkMaxBytes stays a safety net.\nfunction estimateEventBytes(event: RrwebEvent): number {\n\tif (event.type === 2) return 50_000\n\tif (event.type === 3) return 256\n\treturn 128\n}\n\ninterface CreateSessionResult {\n\taccepted: boolean\n\tsessionReplayId?: string\n\tdropReason?: string\n}\n\nasync function createSession(\n\tapiUrl: string,\n\tclientId: string,\n\tsdkSessionId: string,\n\tanonymousId: string,\n): Promise<CreateSessionResult | null> {\n\ttry {\n\t\tconst startUrl =\n\t\t\ttypeof window !== 'undefined' && window.location ? window.location.href : undefined\n\t\tconst userAgent =\n\t\t\ttypeof navigator !== 'undefined' && navigator.userAgent ? navigator.userAgent : undefined\n\t\tconst res = await fetch(joinUrl(apiUrl, '/api/replay-sessions'), {\n\t\t\tmethod: 'POST',\n\t\t\theaders: { 'Content-Type': 'application/json' },\n\t\t\tbody: JSON.stringify({\n\t\t\t\tclientId,\n\t\t\t\tsdkSessionId,\n\t\t\t\tanonymousId,\n\t\t\t\tstartUrl,\n\t\t\t\tuserAgent,\n\t\t\t\tstartedAt: new Date().toISOString(),\n\t\t\t}),\n\t\t})\n\t\tif (!res.ok) return null\n\t\tconst json = (await res.json()) as {\n\t\t\taccepted?: unknown\n\t\t\tsessionReplayId?: unknown\n\t\t\tdropReason?: unknown\n\t\t}\n\t\tif (typeof json.accepted !== 'boolean') return null\n\t\tconst result: CreateSessionResult = { accepted: json.accepted }\n\t\tif (typeof json.sessionReplayId === 'string') result.sessionReplayId = json.sessionReplayId\n\t\tif (typeof json.dropReason === 'string') result.dropReason = json.dropReason\n\t\treturn result\n\t} catch {\n\t\treturn null\n\t}\n}\n\ninterface ChunkUploadResult {\n\tok: boolean\n\tstopSession: boolean\n}\n\nasync function uploadChunk(\n\tapiUrl: string,\n\tsessionReplayId: string,\n\tclientId: string,\n\tseq: number,\n\tbytes: Uint8Array,\n\teventCount: number,\n\tdurationMs: number,\n\tlogger: PluginContext['logger'],\n\tmaxAttempts: number,\n\tdroppedBefore: number,\n): Promise<ChunkUploadResult> {\n\tconst url = joinUrl(\n\t\tapiUrl,\n\t\t`/api/replay-sessions/${encodeURIComponent(sessionReplayId)}/chunks/${seq}`,\n\t)\n\tlet attempt = 0\n\twhile (attempt < maxAttempts) {\n\t\ttry {\n\t\t\t// Wrap in a Blob so the body type is unambiguously BodyInit; some\n\t\t\t// TS lib targets reject raw Uint8Array as fetch body. Slice off\n\t\t\t// the buffer to satisfy the BlobPart ArrayBuffer constraint\n\t\t\t// (Uint8Array<SharedArrayBuffer> is the alternative the lib\n\t\t\t// admits, which we never produce here).\n\t\t\tconst buffer = bytes.buffer.slice(\n\t\t\t\tbytes.byteOffset,\n\t\t\t\tbytes.byteOffset + bytes.byteLength,\n\t\t\t) as ArrayBuffer\n\t\t\tconst blob = new Blob([buffer], { type: 'application/octet-stream' })\n\t\t\tconst headers: Record<string, string> = {\n\t\t\t\t'Content-Type': 'application/octet-stream',\n\t\t\t\t'X-Usero-Client-Id': clientId,\n\t\t\t\t'X-Usero-Event-Count': String(eventCount),\n\t\t\t\t'X-Usero-Duration-Ms': String(Math.max(0, Math.round(durationMs))),\n\t\t\t}\n\t\t\t// Signal a playback gap: how many chunks were dropped (queue\n\t\t\t// saturation) between the previous successful upload and this one.\n\t\t\t// Server-side viewer will use this to render a \"missing data\" marker.\n\t\t\tif (droppedBefore > 0) headers['X-Usero-Dropped-Before'] = String(droppedBefore)\n\t\t\tconst res = await fetch(url, {\n\t\t\t\tmethod: 'PUT',\n\t\t\t\tbody: blob,\n\t\t\t\theaders,\n\t\t\t})\n\t\t\tif (res.ok) return { ok: true, stopSession: false }\n\t\t\t// 409: server told us to stop (bot-dropped, or session already\n\t\t\t// finalised). Don't retry, don't upload further chunks.\n\t\t\tif (res.status === 409) {\n\t\t\t\tlogger.warn(`chunk ${seq} rejected with 409, stopping session`)\n\t\t\t\treturn { ok: false, stopSession: true }\n\t\t\t}\n\t\t\t// Other 4xx (besides 408/429) won't get better with retry.\n\t\t\tif (res.status >= 400 && res.status < 500 && res.status !== 408 && res.status !== 429) {\n\t\t\t\tlogger.error(`chunk ${seq} rejected with ${res.status}`)\n\t\t\t\treturn { ok: false, stopSession: false }\n\t\t\t}\n\t\t} catch (err) {\n\t\t\tlogger.warn(`chunk ${seq} attempt ${attempt + 1} failed`, err)\n\t\t}\n\t\tattempt += 1\n\t\tconst backoff = Math.min(15_000, 500 * 2 ** attempt) + Math.floor(Math.random() * 250)\n\t\tawait new Promise(resolve => setTimeout(resolve, backoff))\n\t}\n\tlogger.error(`chunk ${seq} dropped after ${maxAttempts} attempts`)\n\treturn { ok: false, stopSession: false }\n}\n\nasync function loadRrwebRecord(): Promise<RrwebRecord | null> {\n\ttry {\n\t\tconst mod: unknown = await import(/* webpackChunkName: \"rrweb\" */ 'rrweb')\n\t\tif (\n\t\t\tmod &&\n\t\t\ttypeof mod === 'object' &&\n\t\t\t'record' in mod &&\n\t\t\ttypeof (mod as { record: unknown }).record === 'function'\n\t\t) {\n\t\t\treturn (mod as { record: RrwebRecord }).record\n\t\t}\n\t\treturn null\n\t} catch {\n\t\treturn null\n\t}\n}\n\nfunction scheduleChunkUpload(store: ReplayStore, ctx: PluginContext): void {\n\tif (!store.sessionReplayId) return\n\tif (store.pendingEvents.length === 0) return\n\tif (store.pendingUploads >= MAX_PENDING_UPLOADS) {\n\t\tconst now = Date.now()\n\t\tif (now - store.lastUploadDropWarnAt > UPLOAD_DROP_WARN_INTERVAL_MS) {\n\t\t\tstore.lastUploadDropWarnAt = now\n\t\t\tctx.logger.warn(\n\t\t\t\t`upload queue full (${store.pendingUploads} in-flight), dropping chunk to bound memory`,\n\t\t\t)\n\t\t}\n\t\tstore.pendingEvents = []\n\t\tstore.pendingBytes = 0\n\t\tstore.pendingFirstTs = null\n\t\tstore.pendingLastTs = null\n\t\t// Track for the next successful chunk so the viewer can render a gap.\n\t\tstore.droppedSinceLastUpload += 1\n\t\treturn\n\t}\n\t// Chunk boundary: re-resolve the user. Captures mid-session login on\n\t// replay-only installs that never open the widget. No-op via fingerprint\n\t// dedupe if nothing changed.\n\ttry {\n\t\tctx.resolveUser?.()\n\t} catch (err) {\n\t\tctx.logger.warn('resolveUser threw at chunk boundary', err)\n\t}\n\tconst events = store.pendingEvents\n\tconst eventCount = events.length\n\tconst firstTs = store.pendingFirstTs ?? 0\n\tconst lastTs = store.pendingLastTs ?? firstTs\n\tconst durationMs = Math.max(0, lastTs - firstTs)\n\tconst seq = store.nextChunkSeq\n\tstore.nextChunkSeq += 1\n\tstore.pendingEvents = []\n\tstore.pendingBytes = 0\n\tstore.pendingFirstTs = null\n\tstore.pendingLastTs = null\n\n\tconst sessionReplayId = store.sessionReplayId\n\tconst apiUrl = store.options.apiUrl\n\tconst clientId = store.clientId\n\tconst maxAttempts = store.options.chunkMaxAttempts\n\n\tconst droppedBefore = store.droppedSinceLastUpload\n\tstore.pendingUploads += 1\n\tstore.uploadQueue = store.uploadQueue.then(async () => {\n\t\ttry {\n\t\t\tif (store.cancelled) return\n\t\t\tconst json = JSON.stringify(events)\n\t\t\tconst bytes = await gzipBytes(json)\n\t\t\tif (bytes.byteLength > HARD_CHUNK_BYTE_CAP) {\n\t\t\t\tctx.logger.error(\n\t\t\t\t\t`chunk ${seq} exceeds 4MB hard cap (${bytes.byteLength} bytes), dropping`,\n\t\t\t\t)\n\t\t\t\t// Surface the drop on the next successful chunk so the viewer\n\t\t\t\t// can render a gap marker. Without this, oversized chunks\n\t\t\t\t// vanish without trace server-side.\n\t\t\t\tstore.droppedSinceLastUpload += 1\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst result = await uploadChunk(\n\t\t\t\tapiUrl,\n\t\t\t\tsessionReplayId,\n\t\t\t\tclientId,\n\t\t\t\tseq,\n\t\t\t\tbytes,\n\t\t\t\teventCount,\n\t\t\t\tdurationMs,\n\t\t\t\tctx.logger,\n\t\t\t\tmaxAttempts,\n\t\t\t\tdroppedBefore,\n\t\t\t)\n\t\t\tif (result.ok && droppedBefore > 0) {\n\t\t\t\t// Subtract what we just reported, rather than zeroing, so any\n\t\t\t\t// drops that happened while this chunk was in flight still\n\t\t\t\t// surface on the next successful upload.\n\t\t\t\tstore.droppedSinceLastUpload = Math.max(\n\t\t\t\t\t0,\n\t\t\t\t\tstore.droppedSinceLastUpload - droppedBefore,\n\t\t\t\t)\n\t\t\t}\n\t\t\tif (result.stopSession) {\n\t\t\t\tstore.stopped = true\n\t\t\t\tstopRrweb(store)\n\t\t\t}\n\t\t} catch (err) {\n\t\t\tctx.logger.error(`chunk ${seq} encode failed`, err)\n\t\t} finally {\n\t\t\tstore.pendingUploads -= 1\n\t\t}\n\t})\n}\n\nfunction flushPendingChunk(store: ReplayStore, ctx: PluginContext): void {\n\tif (store.stopped || store.cancelled) return\n\tif (store.pendingEvents.length === 0) return\n\tscheduleChunkUpload(store, ctx)\n}\n\nfunction stopRrweb(store: ReplayStore): void {\n\tif (store.stopRecording) {\n\t\ttry {\n\t\t\tstore.stopRecording()\n\t\t} catch {\n\t\t\t// Already stopped.\n\t\t}\n\t\tstore.stopRecording = null\n\t}\n\tif (store.chunkFlushTimer) {\n\t\tclearInterval(store.chunkFlushTimer)\n\t\tstore.chunkFlushTimer = null\n\t}\n}\n\nfunction startRecording(store: ReplayStore, ctx: PluginContext): void {\n\tif (store.cancelled || store.stopped || store.stopRecording || store.loadInProgress) return\n\tstore.loadInProgress = true\n\tvoid loadRrwebRecord().then(record => {\n\t\tstore.loadInProgress = false\n\t\tif (store.cancelled || store.stopped || !record) {\n\t\t\tif (!record) ctx.logger.warn('rrweb failed to load, replay disabled')\n\t\t\treturn\n\t\t}\n\t\ttry {\n\t\t\tconst stop = record({\n\t\t\t\temit: event => {\n\t\t\t\t\tif (store.stopped || store.cancelled) return\n\t\t\t\t\tif (store.recordingStartedAt === null) store.recordingStartedAt = event.timestamp\n\t\t\t\t\t// FullSnapshot isolation: the snapshot is the playback anchor\n\t\t\t\t\t// for every subsequent incremental event. If a chunk crosses\n\t\t\t\t\t// the 4MB gzipped hard cap, it's dropped wholesale, taking\n\t\t\t\t\t// the anchor with it and breaking playback for up to a full\n\t\t\t\t\t// checkoutEveryMs window, not just the chunk's own duration.\n\t\t\t\t\t// Mitigate by flushing the pre-snapshot buffer separately so\n\t\t\t\t\t// the snapshot lands in a near-empty chunk. Rate-limit to\n\t\t\t\t\t// avoid flush storms on SPA route-change snapshot bursts.\n\t\t\t\t\tif (event.type === RRWEB_EVENT_TYPE_FULL_SNAPSHOT) {\n\t\t\t\t\t\tconst now = Date.now()\n\t\t\t\t\t\tif (\n\t\t\t\t\t\t\tstore.pendingEvents.length > 0 &&\n\t\t\t\t\t\t\tnow - store.lastSnapshotFlushAt >= SNAPSHOT_ISOLATION_MIN_GAP_MS\n\t\t\t\t\t\t) {\n\t\t\t\t\t\t\tscheduleChunkUpload(store, ctx)\n\t\t\t\t\t\t\tstore.lastSnapshotFlushAt = now\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tstore.pendingEvents.push(event)\n\t\t\t\t\t// Hot path: rrweb fires hundreds of events/sec on busy SPAs.\n\t\t\t\t\t// JSON.stringify-per-event burns CPU we don't have, and .length\n\t\t\t\t\t// is UTF-16 units (under-counts non-ASCII by ~2x) so it was\n\t\t\t\t\t// never a real byte count anyway. Use a per-type heuristic:\n\t\t\t\t\t// full snapshots are huge, mutations are mid, everything else\n\t\t\t\t\t// is cheap. chunkMaxBytes is documented as approximate.\n\t\t\t\t\tstore.pendingBytes += estimateEventBytes(event)\n\t\t\t\t\tif (store.pendingFirstTs === null) store.pendingFirstTs = event.timestamp\n\t\t\t\t\tstore.pendingLastTs = event.timestamp\n\t\t\t\t\tif (\n\t\t\t\t\t\tstore.pendingEvents.length >= store.options.chunkMaxEvents ||\n\t\t\t\t\t\tstore.pendingBytes >= store.options.chunkMaxBytes\n\t\t\t\t\t) {\n\t\t\t\t\t\tscheduleChunkUpload(store, ctx)\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\tmaskAllInputs: store.options.maskAllInputs,\n\t\t\t\tmaskTextSelector: store.options.maskTextSelector || undefined,\n\t\t\t\tinlineStylesheet: store.options.inlineStylesheet,\n\t\t\t\tblockSelector: store.options.blockSelector,\n\t\t\t\tsampling: store.options.sampling,\n\t\t\t\tcheckoutEveryNms: store.options.checkoutEveryMs,\n\t\t\t})\n\t\t\tstore.stopRecording = stop\n\t\t\tstore.record = record\n\t\t\tscheduleShadowSnapshot(store, ctx)\n\n\t\t\tstore.chunkFlushTimer = setInterval(\n\t\t\t\t() => flushPendingChunk(store, ctx),\n\t\t\t\tstore.options.chunkSeconds * 1000,\n\t\t\t)\n\t\t} catch (err) {\n\t\t\tctx.logger.error('rrweb record() threw', err)\n\t\t}\n\t})\n}\n\nfunction scheduleShadowSnapshot(store: ReplayStore, ctx: PluginContext): void {\n\tif (store.cancelled || store.stopped || !store.record || !store.stopRecording) return\n\tconst fn = store.record.takeFullSnapshot\n\tif (typeof fn !== 'function') return\n\ttry {\n\t\tfn(true)\n\t} catch (err) {\n\t\tctx.logger.warn('takeFullSnapshot threw', err)\n\t}\n}\n\nfunction finalise(store: ReplayStore, ctx: PluginContext, opts: { useBeacon: boolean }): void {\n\tif (!store.sessionReplayId) return\n\tif (store.pendingEvents.length > 0) flushPendingChunk(store, ctx)\n\tconst url = joinUrl(\n\t\tstore.options.apiUrl,\n\t\t`/api/replay-sessions/${encodeURIComponent(store.sessionReplayId)}/finalise`,\n\t)\n\tconst body = JSON.stringify({ clientId: store.clientId, endedAt: new Date().toISOString() })\n\tif (opts.useBeacon && typeof navigator !== 'undefined' && navigator.sendBeacon) {\n\t\ttry {\n\t\t\tconst blob = new Blob([body], { type: 'application/json' })\n\t\t\tnavigator.sendBeacon(url, blob)\n\t\t\treturn\n\t\t} catch (err) {\n\t\t\tctx.logger.warn('finalise sendBeacon threw', err)\n\t\t}\n\t}\n\tvoid fetch(url, {\n\t\tmethod: 'POST',\n\t\tbody,\n\t\theaders: { 'Content-Type': 'application/json' },\n\t\tkeepalive: true,\n\t}).catch(err => ctx.logger.warn('finalise fetch failed', err))\n}\n\nexport interface CurrentSessionHandle {\n\tid: string\n\toffsetMs: number\n}\n\nexport function sessionReplay(options: SessionReplayOptions = {}): UseroPlugin {\n\tconst merged: ResolvedOptions = {\n\t\t...DEFAULTS,\n\t\t...options,\n\t\tsampling: { ...DEFAULTS.sampling, ...(options.sampling ?? {}) },\n\t}\n\n\treturn {\n\t\tname: 'session-replay',\n\t\tonInit(ctx) {\n\t\t\tif (typeof window === 'undefined') return\n\t\t\tif (merged.sampleRate < 1 && Math.random() >= merged.sampleRate) {\n\t\t\t\tctx.logger.debug('skipped by sampleRate')\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tconst apiUrl = merged.apiUrl || ctx.baseUrl\n\t\t\tif (!apiUrl) {\n\t\t\t\tctx.logger.error('session-replay needs an apiUrl (via options or PluginContext)')\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst sdkSessionId = mintSdkSessionId()\n\t\t\t// Mint or read the cross-session anonymousId. Cached in module\n\t\t\t// scope after the first call, so this stays O(1) on hot paths.\n\t\t\tconst anonymousId = getOrMintAnonymousId()\n\n\t\t\tconst store: ReplayStore = {\n\t\t\t\toptions: { ...merged, apiUrl },\n\t\t\t\tclientId: ctx.clientId,\n\t\t\t\tsdkSessionId,\n\t\t\t\tsessionReplayId: null,\n\t\t\t\trecordingStartedAt: null,\n\t\t\t\tpendingEvents: [],\n\t\t\t\tpendingBytes: 0,\n\t\t\t\tpendingFirstTs: null,\n\t\t\t\tpendingLastTs: null,\n\t\t\t\tlastUploadDropWarnAt: 0,\n\t\t\t\tdroppedSinceLastUpload: 0,\n\t\t\t\tlastSnapshotFlushAt: 0,\n\t\t\t\tnextChunkSeq: 0,\n\t\t\t\tuploadQueue: Promise.resolve(),\n\t\t\t\tpendingUploads: 0,\n\t\t\t\tchunkFlushTimer: null,\n\t\t\t\tstartTimer: null,\n\t\t\t\tpageHideHandler: null,\n\t\t\t\tshadowUpdateHandler: null,\n\t\t\t\trecord: null,\n\t\t\t\tstopRecording: null,\n\t\t\t\tloadInProgress: false,\n\t\t\t\tcancelled: false,\n\t\t\t\tstopped: false,\n\t\t\t}\n\t\t\tctx.setStore(store)\n\n\t\t\tconst onShadowUpdate = (): void => scheduleShadowSnapshot(store, ctx)\n\t\t\tstore.shadowUpdateHandler = onShadowUpdate\n\t\t\twindow.addEventListener('usero:shadow-update', onShadowUpdate)\n\n\t\t\tconst onPageHide = (): void => {\n\t\t\t\tfinalise(store, ctx, { useBeacon: true })\n\t\t\t\tstore.stopped = true\n\t\t\t\tstopRrweb(store)\n\t\t\t}\n\t\t\tstore.pageHideHandler = onPageHide\n\t\t\twindow.addEventListener('pagehide', onPageHide)\n\n\t\t\tconst begin = async (): Promise<void> => {\n\t\t\t\tif (store.cancelled) return\n\t\t\t\t// Replay-only customers may never open the widget, so the host's\n\t\t\t\t// user state never gets polled by the widget's interaction\n\t\t\t\t// boundaries. Re-resolve here so a mid-session login that\n\t\t\t\t// happened before session start is visible server-side before\n\t\t\t\t// the first chunk lands. Fingerprint dedupe inside\n\t\t\t\t// identifyIfChanged makes this effectively free when nothing\n\t\t\t\t// changed.\n\t\t\t\ttry {\n\t\t\t\t\tctx.resolveUser?.()\n\t\t\t\t} catch (err) {\n\t\t\t\t\tctx.logger.warn('resolveUser threw at session start', err)\n\t\t\t\t}\n\t\t\t\tconst created = await createSession(apiUrl, ctx.clientId, sdkSessionId, anonymousId)\n\t\t\t\tif (!created) {\n\t\t\t\t\tctx.logger.warn('session create failed, replay disabled')\n\t\t\t\t\tstore.stopped = true\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tif (!created.accepted) {\n\t\t\t\t\tctx.logger.info(`session-replay declined: ${created.dropReason ?? 'unknown'}`)\n\t\t\t\t\tstore.stopped = true\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tif (!created.sessionReplayId) {\n\t\t\t\t\tctx.logger.error('server accepted but returned no sessionReplayId')\n\t\t\t\t\tstore.stopped = true\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tstore.sessionReplayId = created.sessionReplayId\n\t\t\t\tstore.recordingStartedAt = Date.now()\n\t\t\t\tstartRecording(store, ctx)\n\t\t\t}\n\n\t\t\tif (merged.startAfterMs > 0) {\n\t\t\t\tconst cancelOnExit = (): void => {\n\t\t\t\t\tstore.cancelled = true\n\t\t\t\t\tif (store.startTimer) {\n\t\t\t\t\t\tclearTimeout(store.startTimer)\n\t\t\t\t\t\tstore.startTimer = null\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\twindow.addEventListener('pagehide', cancelOnExit, { once: true })\n\t\t\t\twindow.addEventListener('beforeunload', cancelOnExit, { once: true })\n\t\t\t\tstore.startTimer = setTimeout(() => {\n\t\t\t\t\tvoid begin()\n\t\t\t\t}, merged.startAfterMs)\n\t\t\t} else {\n\t\t\t\tvoid begin()\n\t\t\t}\n\t\t},\n\t\tonFeedbackSubmit(ctx) {\n\t\t\tconst store = ctx.getStore<ReplayStore>()\n\t\t\tif (!store || store.cancelled || store.stopped) return undefined\n\t\t\tif (!store.sessionReplayId) return undefined\n\t\t\tconst offsetMs =\n\t\t\t\tstore.recordingStartedAt !== null\n\t\t\t\t\t? Math.max(0, Date.now() - store.recordingStartedAt)\n\t\t\t\t\t: 0\n\t\t\treturn { sessionReplayId: store.sessionReplayId, replayOffsetMs: offsetMs }\n\t\t},\n\t\tonDestroy(ctx) {\n\t\t\tconst store = ctx.getStore<ReplayStore>()\n\t\t\tif (!store) return\n\t\t\tstore.cancelled = true\n\t\t\tif (store.startTimer) {\n\t\t\t\tclearTimeout(store.startTimer)\n\t\t\t\tstore.startTimer = null\n\t\t\t}\n\t\t\tif (store.pageHideHandler) {\n\t\t\t\twindow.removeEventListener('pagehide', store.pageHideHandler)\n\t\t\t\tstore.pageHideHandler = null\n\t\t\t}\n\t\t\tif (store.shadowUpdateHandler) {\n\t\t\t\twindow.removeEventListener('usero:shadow-update', store.shadowUpdateHandler)\n\t\t\t\tstore.shadowUpdateHandler = null\n\t\t\t}\n\t\t\t// SPA route change / React unmount: send a finalise so the\n\t\t\t// server stamps endedAt. fetch+keepalive is fine here since we\n\t\t\t// aren't necessarily in a pagehide path.\n\t\t\tif (store.sessionReplayId && !store.stopped) {\n\t\t\t\tfinalise(store, ctx, { useBeacon: false })\n\t\t\t}\n\t\t\tstore.stopped = true\n\t\t\tstopRrweb(store)\n\t\t\tstore.pendingEvents.length = 0\n\t\t\tstore.pendingBytes = 0\n\t\t\tstore.record = null\n\t\t},\n\t}\n}\n\n// Returns the live session-replay handle for a given plugin context, or\n// null if the session was bot-dropped, sample-skipped, or not yet\n// created. Other plugins (e.g. user-test) can call this to attach the\n// replay FK + offset to their own server-side records.\nexport function getCurrentSession(ctx: PluginContext): CurrentSessionHandle | null {\n\tconst store = ctx.getStore<ReplayStore>()\n\tif (!store || store.cancelled || store.stopped || !store.sessionReplayId) return null\n\tconst offsetMs =\n\t\tstore.recordingStartedAt !== null\n\t\t\t? Math.max(0, Date.now() - store.recordingStartedAt)\n\t\t\t: 0\n\treturn { id: store.sessionReplayId, offsetMs }\n}\n\n// Internal helper exports for testing only. Not part of the public API.\nexport const __test__ = {\n\tuint8ToBase64,\n\tgzipBytes,\n\tmintSdkSessionId,\n\tuploadChunk,\n\tcreateSession,\n\tjoinUrl,\n\tscheduleChunkUpload,\n\tHARD_CHUNK_BYTE_CAP,\n\tSDK_SESSION_STORAGE_KEY,\n\tMAX_PENDING_UPLOADS,\n\tUPLOAD_DROP_WARN_INTERVAL_MS,\n\tDEFAULTS,\n}\n"]}
|
|
@@ -63,6 +63,8 @@ var SDK_SESSION_STORAGE_KEY = "usero:session-replay:sdk-session-id";
|
|
|
63
63
|
var HARD_CHUNK_BYTE_CAP = 4 * 1024 * 1024;
|
|
64
64
|
var MAX_PENDING_UPLOADS = 3;
|
|
65
65
|
var UPLOAD_DROP_WARN_INTERVAL_MS = 5e3;
|
|
66
|
+
var RRWEB_EVENT_TYPE_FULL_SNAPSHOT = 2;
|
|
67
|
+
var SNAPSHOT_ISOLATION_MIN_GAP_MS = 1500;
|
|
66
68
|
function uint8ToBase64(bytes) {
|
|
67
69
|
let binary = "";
|
|
68
70
|
const chunkSize = 32768;
|
|
@@ -249,6 +251,7 @@ function scheduleChunkUpload(store, ctx) {
|
|
|
249
251
|
ctx.logger.error(
|
|
250
252
|
`chunk ${seq} exceeds 4MB hard cap (${bytes.byteLength} bytes), dropping`
|
|
251
253
|
);
|
|
254
|
+
store.droppedSinceLastUpload += 1;
|
|
252
255
|
return;
|
|
253
256
|
}
|
|
254
257
|
const result = await uploadChunk(
|
|
@@ -312,6 +315,13 @@ function startRecording(store, ctx) {
|
|
|
312
315
|
emit: (event) => {
|
|
313
316
|
if (store.stopped || store.cancelled) return;
|
|
314
317
|
if (store.recordingStartedAt === null) store.recordingStartedAt = event.timestamp;
|
|
318
|
+
if (event.type === RRWEB_EVENT_TYPE_FULL_SNAPSHOT) {
|
|
319
|
+
const now = Date.now();
|
|
320
|
+
if (store.pendingEvents.length > 0 && now - store.lastSnapshotFlushAt >= SNAPSHOT_ISOLATION_MIN_GAP_MS) {
|
|
321
|
+
scheduleChunkUpload(store, ctx);
|
|
322
|
+
store.lastSnapshotFlushAt = now;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
315
325
|
store.pendingEvents.push(event);
|
|
316
326
|
store.pendingBytes += estimateEventBytes(event);
|
|
317
327
|
if (store.pendingFirstTs === null) store.pendingFirstTs = event.timestamp;
|
|
@@ -406,6 +416,7 @@ function sessionReplay(options = {}) {
|
|
|
406
416
|
pendingLastTs: null,
|
|
407
417
|
lastUploadDropWarnAt: 0,
|
|
408
418
|
droppedSinceLastUpload: 0,
|
|
419
|
+
lastSnapshotFlushAt: 0,
|
|
409
420
|
nextChunkSeq: 0,
|
|
410
421
|
uploadQueue: Promise.resolve(),
|
|
411
422
|
pendingUploads: 0,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/identity.ts","../../src/plugins/session-replay.ts"],"names":["generateRandomId"],"mappings":"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","file":"session-replay.js","sourcesContent":["// Identity layer for the Usero SDK.\n//\n// Two responsibilities:\n// 1. Mint and persist a stable per-browser `anonymousId` in localStorage\n// so cross-tab + cross-day replays from the same browser stitch\n// together server-side. Falls back to an in-memory id if storage is\n// blocked (sandboxed iframes, Safari Lockdown, full quota). Replay\n// still works in that case, you just lose stitching.\n// 2. Auto-fire POST /api/identify when the resolved user transitions\n// (null -> id, id -> id'). Deduped by an in-memory fingerprint so\n// re-renders with the same user are no-ops on the network.\n//\n// All storage access is wrapped in try/catch and gated behind a one-shot\n// init read. The hot path (replay chunk flush) never touches localStorage.\n\nconst ANON_STORAGE_KEY = 'usero:anonymous-id'\n\nlet cachedAnonymousId: string | null = null\n// Fingerprint of the last identify we POSTed. Same SDK instance + same\n// resolved user + same traits = no-op. Cleared on logout (anonymousId\n// rotation).\nlet lastIdentifyFingerprint: string | null = null\n\nexport type UserTraitValue = string | number | boolean | null\nexport type UserTraits = Record<string, UserTraitValue>\n\nexport interface UseroUser {\n\tid: string\n\temail?: string\n\tdisplayName?: string\n\ttraits?: UserTraits\n}\n\nfunction generateRandomId(): string {\n\tif (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {\n\t\treturn crypto.randomUUID()\n\t}\n\tconst bytes = new Uint8Array(16)\n\tif (typeof crypto !== 'undefined' && typeof crypto.getRandomValues === 'function') {\n\t\tcrypto.getRandomValues(bytes)\n\t} else {\n\t\tfor (let i = 0; i < bytes.length; i += 1) bytes[i] = Math.floor(Math.random() * 256)\n\t}\n\tlet out = ''\n\tfor (const b of bytes) out += b.toString(16).padStart(2, '0')\n\treturn out\n}\n\nfunction safeReadLocalStorage(key: string): string | null {\n\tif (typeof window === 'undefined') return null\n\ttry {\n\t\treturn window.localStorage?.getItem(key) ?? null\n\t} catch {\n\t\treturn null\n\t}\n}\n\nfunction safeWriteLocalStorage(key: string, value: string): void {\n\tif (typeof window === 'undefined') return\n\ttry {\n\t\twindow.localStorage?.setItem(key, value)\n\t} catch {\n\t\t// Sandboxed iframe / Safari Lockdown / quota. Fall back to memory.\n\t}\n}\n\n/**\n * Returns the stable per-browser anonymousId. Reads localStorage at most\n * once per SDK instance. Subsequent calls hit the in-memory cache, so\n * even hot paths (per-event in replay) are safe to call this.\n */\nexport function getOrMintAnonymousId(): string {\n\tif (cachedAnonymousId) return cachedAnonymousId\n\tconst existing = safeReadLocalStorage(ANON_STORAGE_KEY)\n\t// Sanity filter, not strict validation. We accept anything that looks\n\t// plausibly like an id (>=8 alphanumeric-or-hyphen) so older SDK\n\t// versions that wrote a slightly different shape still stitch. Fresh\n\t// mint is cheap, so we only reject obvious garbage; tightening this\n\t// would force rotation in customer browsers and split otherwise-good\n\t// sibling-session attribution.\n\tif (existing && /^[a-z0-9-]{8,}$/i.test(existing)) {\n\t\tcachedAnonymousId = existing\n\t\treturn existing\n\t}\n\tconst id = generateRandomId()\n\tsafeWriteLocalStorage(ANON_STORAGE_KEY, id)\n\tcachedAnonymousId = id\n\treturn id\n}\n\n/**\n * Rotate the anonymousId. Called on logout (user transitions from a\n * known id to null) so the next anonymous trail does not get auto-merged\n * into the previous person on the next identify().\n */\nexport function rotateAnonymousId(): string {\n\tconst id = generateRandomId()\n\tcachedAnonymousId = id\n\tsafeWriteLocalStorage(ANON_STORAGE_KEY, id)\n\tlastIdentifyFingerprint = null\n\treturn id\n}\n\nfunction fingerprintUser(anonymousId: string, user: UseroUser): string {\n\t// Stable across re-renders: keys sorted, traits canonicalised. Cheap\n\t// enough on the hot path (only runs when the SDK thinks the user might\n\t// have changed, never per-event).\n\tconst traits = user.traits ?? {}\n\tconst keys = Object.keys(traits).sort()\n\tconst canonical: Array<[string, UserTraitValue]> = keys.map(k => [k, traits[k] ?? null])\n\treturn JSON.stringify([anonymousId, user.id, user.email ?? null, user.displayName ?? null, canonical])\n}\n\nexport interface IdentifyTransport {\n\tapiUrl: string\n\tclientId: string\n}\n\n/**\n * POST to /api/identify if the (anonymousId, user) fingerprint differs\n * from the last call. Returns true if a network request actually fired.\n * Never throws; failures are best-effort and the caller (the widget /\n * provider) should not treat them as errors.\n *\n * Tab-unload safety: if the page is hidden when this fires (visibility\n * 'hidden' or a pagehide handler), we route the payload through\n * `navigator.sendBeacon` so the request survives unload. Otherwise we\n * use a normal fetch and only cache the fingerprint when the server\n * confirms `accepted: true`. A 200 `{ accepted: false }` (e.g.\n * `unknown_client` for a clientId that becomes valid mid-session) is\n * treated as retryable so the next call re-fires.\n */\nexport async function identifyIfChanged(transport: IdentifyTransport, user: UseroUser): Promise<boolean> {\n\tconst anonymousId = getOrMintAnonymousId()\n\tconst fp = fingerprintUser(anonymousId, user)\n\tif (fp === lastIdentifyFingerprint) return false\n\n\tconst url = `${transport.apiUrl.replace(/\\/$/, '')}/api/identify`\n\t// Body must stay under the browser's keepalive / sendBeacon cap\n\t// (~64KB across most engines) when this fires on pagehide. That\n\t// transitively caps trait payload size; in practice traits should be\n\t// small typed scalars, not blobs.\n\tconst body = JSON.stringify({\n\t\tclientId: transport.clientId,\n\t\tanonymousId,\n\t\texternalUserId: user.id,\n\t\temail: user.email,\n\t\tdisplayName: user.displayName,\n\t\ttraits: user.traits,\n\t})\n\n\t// If the document is hidden (pagehide / tab close in flight), best-effort\n\t// hand off to sendBeacon. We don't get a response back, so we optimistically\n\t// cache the fingerprint to avoid re-firing on the next page; the server is\n\t// idempotent if the page reload re-runs identify with the same payload.\n\tif (\n\t\ttypeof document !== 'undefined' &&\n\t\tdocument.visibilityState === 'hidden' &&\n\t\ttypeof navigator !== 'undefined' &&\n\t\ttypeof navigator.sendBeacon === 'function'\n\t) {\n\t\ttry {\n\t\t\tconst blob = new Blob([body], { type: 'application/json' })\n\t\t\t// sendBeacon returns false when the user agent refuses to queue\n\t\t\t// the request (size cap, or historically Safari rejecting\n\t\t\t// non-CORS-simple content types). Modern Safari accepts\n\t\t\t// application/json, but we keep a keepalive-fetch fallback so an\n\t\t\t// older WebKit that rejects the beacon still ships the identify.\n\t\t\tif (navigator.sendBeacon(url, blob)) {\n\t\t\t\tlastIdentifyFingerprint = fp\n\t\t\t\treturn true\n\t\t\t}\n\t\t} catch {\n\t\t\t// fall through to keepalive fetch below\n\t\t}\n\t}\n\n\ttry {\n\t\tconst res = await fetch(url, {\n\t\t\tmethod: 'POST',\n\t\t\theaders: { 'Content-Type': 'application/json' },\n\t\t\tbody,\n\t\t\t// keepalive lets the request survive a tab-close mid-flight on\n\t\t\t// browsers that support it; sendBeacon above is the primary path.\n\t\t\tkeepalive: true,\n\t\t})\n\t\tif (!res.ok) return true\n\t\t// Parse the response: a 200 with `accepted: false` (e.g. unknown\n\t\t// client) is retryable. Only cache the fingerprint when the server\n\t\t// confirmed it actually stored the identity.\n\t\ttry {\n\t\t\tconst json = (await res.json()) as { accepted?: unknown }\n\t\t\tif (json && json.accepted === true) lastIdentifyFingerprint = fp\n\t\t} catch {\n\t\t\t// Server returned 2xx but unparseable body: don't cache, let the\n\t\t\t// next call retry.\n\t\t}\n\t\treturn true\n\t} catch {\n\t\treturn false\n\t}\n}\n\n/**\n * Clear identify state and rotate anonymousId. Called when the resolved\n * user transitions from a known id to null (logout). The next anonymous\n * trail will get a fresh anonymousId so it does not merge into the\n * previous person.\n */\nexport function handleLogout(): void {\n\trotateAnonymousId()\n}\n\n// Test hooks (not exported from the package public surface).\nexport const __test__ = {\n\tANON_STORAGE_KEY,\n\tresetIdentityState: (): void => {\n\t\tcachedAnonymousId = null\n\t\tlastIdentifyFingerprint = null\n\t},\n}\n","// Session replay plugin for the Usero widget.\n//\n// Streams rrweb events to the SaaS side as gzipped chunks while the user\n// is on the page, instead of buffering in memory and attaching to a\n// feedback submission. This decouples session replay from feedback so we\n// capture every session (subject to bot-gate + sampling + engagement\n// gates), not just the ones that submit feedback.\n//\n// Lifecycle:\n// 1. onInit: dice-roll sample, optional engagement-time gate, mint a\n// stable per-tab `sdkSessionId` in sessionStorage, and POST to\n// /api/replay-sessions to create the row. If the server returns\n// `{accepted:false}` (bot-gated), the plugin no-ops the rest of the\n// session and getCurrentSession() returns null.\n// 2. Recording: lazy-load rrweb, append events to a buffer, flush a\n// chunk every `chunkSeconds` (or sooner if the buffer is large).\n// Each chunk is gzipped via CompressionStream and PUT to\n// /api/replay-sessions/:id/chunks/:seq with raw bytes + the three\n// X-Usero-* headers (Client-Id, Event-Count, Duration-Ms). Retries\n// with exponential backoff. R2 head-check makes retries idempotent\n// server-side. A chunk PUT returning 409 stops the session.\n// 3. onFeedbackSubmit: returns `{sessionReplayId, replayOffsetMs}` so\n// the feedback record can FK at the moment of submit. Does NOT\n// attach `replayEvents` (legacy field) — chunked uploads carry the\n// events out-of-band.\n// 4. onDestroy / pagehide: best-effort flush remaining buffer, then\n// sendBeacon to /api/replay-sessions/:id/finalise with the\n// end-timestamp. Idempotent server-side.\n//\n// Bundle hygiene: rrweb stays lazy via dynamic `import('rrweb')` behind\n// the engagement gate, so consumers who lose the dice roll or navigate\n// away inside the gate window pay zero rrweb bytes.\n\nimport { getOrMintAnonymousId } from '../identity'\nimport type { UseroPlugin, PluginContext } from '../plugin'\n\nexport interface ReplaySampling {\n\tmousemove?: number\n\tscroll?: number\n\tmedia?: number\n\tinput?: number | 'last'\n}\n\nexport interface SessionReplayOptions {\n\t// Wait this many ms after page load before loading rrweb and creating\n\t// the session row. If the user navigates away first, rrweb is never\n\t// loaded and no session row is created. Default 0 (start immediately).\n\tstartAfterMs?: number\n\t// Probability (0..1) that this session records at all. Decided once\n\t// at init via Math.random(). Default 1.\n\tsampleRate?: number\n\t// rrweb sampling rates per event type.\n\tsampling?: ReplaySampling\n\t// Mask all <input>/<textarea> values in the recording. Default true.\n\tmaskAllInputs?: boolean\n\t// CSS selector for nodes whose text content should be masked. Default\n\t// `[data-usero-mask]`.\n\tmaskTextSelector?: string\n\t// Inline external stylesheets so the replay viewer renders correctly\n\t// without network access. Default true.\n\tinlineStylesheet?: boolean\n\t// Block (entirely skip) DOM subtrees matching this selector. Default\n\t// `[data-usero-block]`.\n\tblockSelector?: string\n\t// Flush a chunk every N seconds. Default 3. Smaller = more PUTs but\n\t// less data lost on tab crash and less time event refs retain detached\n\t// DOM nodes in memory.\n\tchunkSeconds?: number\n\t// Soft cap on buffered events before forcing a flush, regardless of\n\t// time. Default 1000.\n\tchunkMaxEvents?: number\n\t// Soft cap on estimated buffered bytes before forcing a flush. Default\n\t// 512_000 (~500 KB pre-gzip). Keeps memory pressure bounded on event-heavy\n\t// pages even when chunkMaxEvents hasn't been hit.\n\tchunkMaxBytes?: number\n\t// Max attempts per chunk before giving up. Default 5.\n\tchunkMaxAttempts?: number\n\t// Force rrweb to take a fresh full snapshot every N ms. This resets\n\t// rrweb's internal mirror so detached DOM (e.g. SPA route changes)\n\t// becomes GC-eligible. Default 60_000.\n\tcheckoutEveryMs?: number\n\t// API origin. Override for self-hosted or local dev. Defaults to the\n\t// PluginContext baseUrl threaded through by the widget.\n\tapiUrl?: string\n}\n\ninterface RrwebEvent {\n\ttype: number\n\tdata: unknown\n\ttimestamp: number\n}\n\ninterface RrwebRecordOptions {\n\temit: (event: RrwebEvent) => void\n\tmaskAllInputs?: boolean\n\tmaskTextSelector?: string\n\tinlineStylesheet?: boolean\n\tblockSelector?: string\n\tsampling?: ReplaySampling\n\tcheckoutEveryNms?: number\n}\n\ninterface RrwebRecordFn {\n\t(opts: RrwebRecordOptions): () => void\n\ttakeFullSnapshot?: (isCheckout?: boolean) => void\n}\n\ntype RrwebRecord = RrwebRecordFn\n\ninterface ResolvedOptions {\n\tstartAfterMs: number\n\tsampleRate: number\n\tsampling: ReplaySampling\n\tmaskAllInputs: boolean\n\tmaskTextSelector: string\n\tinlineStylesheet: boolean\n\tblockSelector: string\n\tchunkSeconds: number\n\tchunkMaxEvents: number\n\tchunkMaxBytes: number\n\tchunkMaxAttempts: number\n\tcheckoutEveryMs: number\n\tapiUrl: string\n}\n\ninterface ReplayStore {\n\toptions: ResolvedOptions\n\tclientId: string\n\tsdkSessionId: string\n\tsessionReplayId: string | null\n\t// Wall-clock timestamp (ms) of the first event we ever recorded.\n\t// Used to compute replayOffsetMs at feedback-submit time.\n\trecordingStartedAt: number | null\n\tpendingEvents: RrwebEvent[]\n\tpendingBytes: number\n\tpendingFirstTs: number | null\n\tpendingLastTs: number | null\n\tlastUploadDropWarnAt: number\n\t// Count of chunks dropped (queue saturation) since the last successful\n\t// upload. Sent as a header on the next successful chunk PUT so the\n\t// viewer can show a \"gap here\" marker. Reset on success.\n\tdroppedSinceLastUpload: number\n\tnextChunkSeq: number\n\tuploadQueue: Promise<void>\n\tpendingUploads: number\n\tchunkFlushTimer: ReturnType<typeof setInterval> | null\n\tstartTimer: ReturnType<typeof setTimeout> | null\n\tpageHideHandler: (() => void) | null\n\tshadowUpdateHandler: ((event: Event) => void) | null\n\trecord: RrwebRecord | null\n\tstopRecording: (() => void) | null\n\tloadInProgress: boolean\n\tcancelled: boolean\n\t// True once the session is \"done\": bot-gated, finalised, or destroyed.\n\tstopped: boolean\n}\n\nconst DEFAULTS: ResolvedOptions = {\n\tstartAfterMs: 0,\n\tsampleRate: 1,\n\tsampling: { mousemove: 50, scroll: 100 },\n\tmaskAllInputs: true,\n\tmaskTextSelector: '[data-usero-mask]',\n\tinlineStylesheet: true,\n\tblockSelector: '[data-usero-block]',\n\tchunkSeconds: 3,\n\tchunkMaxEvents: 1000,\n\tchunkMaxBytes: 512_000,\n\tchunkMaxAttempts: 5,\n\tcheckoutEveryMs: 60_000,\n\tapiUrl: '',\n}\n\nconst SDK_SESSION_STORAGE_KEY = 'usero:session-replay:sdk-session-id'\nconst HARD_CHUNK_BYTE_CAP = 4 * 1024 * 1024\nconst MAX_PENDING_UPLOADS = 3\nconst UPLOAD_DROP_WARN_INTERVAL_MS = 5000\n\nfunction uint8ToBase64(bytes: Uint8Array): string {\n\tlet binary = ''\n\tconst chunkSize = 0x8000\n\tfor (let i = 0; i < bytes.length; i += chunkSize) {\n\t\tconst slice = bytes.subarray(i, i + chunkSize)\n\t\tbinary += String.fromCharCode.apply(null, Array.from(slice))\n\t}\n\treturn typeof btoa === 'function' ? btoa(binary) : ''\n}\n\nasync function gzipBytes(input: string): Promise<Uint8Array> {\n\tif (typeof CompressionStream === 'undefined') {\n\t\t// Old browsers: send uncompressed JSON. Acceptable degradation;\n\t\t// the server endpoint accepts raw application/octet-stream.\n\t\treturn new TextEncoder().encode(input)\n\t}\n\tconst stream = new Blob([input]).stream().pipeThrough(new CompressionStream('gzip'))\n\tconst buf = await new Response(stream).arrayBuffer()\n\treturn new Uint8Array(buf)\n}\n\nfunction generateRandomId(): string {\n\tif (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {\n\t\treturn crypto.randomUUID()\n\t}\n\tconst bytes = new Uint8Array(16)\n\tif (typeof crypto !== 'undefined' && typeof crypto.getRandomValues === 'function') {\n\t\tcrypto.getRandomValues(bytes)\n\t} else {\n\t\tfor (let i = 0; i < bytes.length; i += 1) bytes[i] = Math.floor(Math.random() * 256)\n\t}\n\tlet out = ''\n\tfor (const b of bytes) out += b.toString(16).padStart(2, '0')\n\treturn out\n}\n\nfunction mintSdkSessionId(): string {\n\ttry {\n\t\tconst existing = window.sessionStorage?.getItem(SDK_SESSION_STORAGE_KEY)\n\t\tif (existing && /^[a-z0-9-]{8,}$/i.test(existing)) return existing\n\t} catch {\n\t\t// sessionStorage can throw in sandboxed iframes — fall through.\n\t}\n\tconst id = generateRandomId()\n\ttry {\n\t\twindow.sessionStorage?.setItem(SDK_SESSION_STORAGE_KEY, id)\n\t} catch {\n\t\t// Ignore: we still return the freshly minted id.\n\t}\n\treturn id\n}\n\nfunction joinUrl(apiUrl: string, path: string): string {\n\treturn `${apiUrl.replace(/\\/$/, '')}${path}`\n}\n\n// Cheap per-event byte estimate. Avoids JSON.stringify on the hot emit path.\n// rrweb EventType: 0=DomContentLoaded, 1=Load, 2=FullSnapshot, 3=IncrementalSnapshot,\n// 4=Meta, 5=Custom, 6=Plugin. Full snapshots are the only event class that's\n// genuinely large; everything else is well under a KB on average. Numbers\n// chosen to over-estimate slightly so chunkMaxBytes stays a safety net.\nfunction estimateEventBytes(event: RrwebEvent): number {\n\tif (event.type === 2) return 50_000\n\tif (event.type === 3) return 256\n\treturn 128\n}\n\ninterface CreateSessionResult {\n\taccepted: boolean\n\tsessionReplayId?: string\n\tdropReason?: string\n}\n\nasync function createSession(\n\tapiUrl: string,\n\tclientId: string,\n\tsdkSessionId: string,\n\tanonymousId: string,\n): Promise<CreateSessionResult | null> {\n\ttry {\n\t\tconst startUrl =\n\t\t\ttypeof window !== 'undefined' && window.location ? window.location.href : undefined\n\t\tconst userAgent =\n\t\t\ttypeof navigator !== 'undefined' && navigator.userAgent ? navigator.userAgent : undefined\n\t\tconst res = await fetch(joinUrl(apiUrl, '/api/replay-sessions'), {\n\t\t\tmethod: 'POST',\n\t\t\theaders: { 'Content-Type': 'application/json' },\n\t\t\tbody: JSON.stringify({\n\t\t\t\tclientId,\n\t\t\t\tsdkSessionId,\n\t\t\t\tanonymousId,\n\t\t\t\tstartUrl,\n\t\t\t\tuserAgent,\n\t\t\t\tstartedAt: new Date().toISOString(),\n\t\t\t}),\n\t\t})\n\t\tif (!res.ok) return null\n\t\tconst json = (await res.json()) as {\n\t\t\taccepted?: unknown\n\t\t\tsessionReplayId?: unknown\n\t\t\tdropReason?: unknown\n\t\t}\n\t\tif (typeof json.accepted !== 'boolean') return null\n\t\tconst result: CreateSessionResult = { accepted: json.accepted }\n\t\tif (typeof json.sessionReplayId === 'string') result.sessionReplayId = json.sessionReplayId\n\t\tif (typeof json.dropReason === 'string') result.dropReason = json.dropReason\n\t\treturn result\n\t} catch {\n\t\treturn null\n\t}\n}\n\ninterface ChunkUploadResult {\n\tok: boolean\n\tstopSession: boolean\n}\n\nasync function uploadChunk(\n\tapiUrl: string,\n\tsessionReplayId: string,\n\tclientId: string,\n\tseq: number,\n\tbytes: Uint8Array,\n\teventCount: number,\n\tdurationMs: number,\n\tlogger: PluginContext['logger'],\n\tmaxAttempts: number,\n\tdroppedBefore: number,\n): Promise<ChunkUploadResult> {\n\tconst url = joinUrl(\n\t\tapiUrl,\n\t\t`/api/replay-sessions/${encodeURIComponent(sessionReplayId)}/chunks/${seq}`,\n\t)\n\tlet attempt = 0\n\twhile (attempt < maxAttempts) {\n\t\ttry {\n\t\t\t// Wrap in a Blob so the body type is unambiguously BodyInit; some\n\t\t\t// TS lib targets reject raw Uint8Array as fetch body. Slice off\n\t\t\t// the buffer to satisfy the BlobPart ArrayBuffer constraint\n\t\t\t// (Uint8Array<SharedArrayBuffer> is the alternative the lib\n\t\t\t// admits, which we never produce here).\n\t\t\tconst buffer = bytes.buffer.slice(\n\t\t\t\tbytes.byteOffset,\n\t\t\t\tbytes.byteOffset + bytes.byteLength,\n\t\t\t) as ArrayBuffer\n\t\t\tconst blob = new Blob([buffer], { type: 'application/octet-stream' })\n\t\t\tconst headers: Record<string, string> = {\n\t\t\t\t'Content-Type': 'application/octet-stream',\n\t\t\t\t'X-Usero-Client-Id': clientId,\n\t\t\t\t'X-Usero-Event-Count': String(eventCount),\n\t\t\t\t'X-Usero-Duration-Ms': String(Math.max(0, Math.round(durationMs))),\n\t\t\t}\n\t\t\t// Signal a playback gap: how many chunks were dropped (queue\n\t\t\t// saturation) between the previous successful upload and this one.\n\t\t\t// Server-side viewer will use this to render a \"missing data\" marker.\n\t\t\tif (droppedBefore > 0) headers['X-Usero-Dropped-Before'] = String(droppedBefore)\n\t\t\tconst res = await fetch(url, {\n\t\t\t\tmethod: 'PUT',\n\t\t\t\tbody: blob,\n\t\t\t\theaders,\n\t\t\t})\n\t\t\tif (res.ok) return { ok: true, stopSession: false }\n\t\t\t// 409: server told us to stop (bot-dropped, or session already\n\t\t\t// finalised). Don't retry, don't upload further chunks.\n\t\t\tif (res.status === 409) {\n\t\t\t\tlogger.warn(`chunk ${seq} rejected with 409, stopping session`)\n\t\t\t\treturn { ok: false, stopSession: true }\n\t\t\t}\n\t\t\t// Other 4xx (besides 408/429) won't get better with retry.\n\t\t\tif (res.status >= 400 && res.status < 500 && res.status !== 408 && res.status !== 429) {\n\t\t\t\tlogger.error(`chunk ${seq} rejected with ${res.status}`)\n\t\t\t\treturn { ok: false, stopSession: false }\n\t\t\t}\n\t\t} catch (err) {\n\t\t\tlogger.warn(`chunk ${seq} attempt ${attempt + 1} failed`, err)\n\t\t}\n\t\tattempt += 1\n\t\tconst backoff = Math.min(15_000, 500 * 2 ** attempt) + Math.floor(Math.random() * 250)\n\t\tawait new Promise(resolve => setTimeout(resolve, backoff))\n\t}\n\tlogger.error(`chunk ${seq} dropped after ${maxAttempts} attempts`)\n\treturn { ok: false, stopSession: false }\n}\n\nasync function loadRrwebRecord(): Promise<RrwebRecord | null> {\n\ttry {\n\t\tconst mod: unknown = await import(/* webpackChunkName: \"rrweb\" */ 'rrweb')\n\t\tif (\n\t\t\tmod &&\n\t\t\ttypeof mod === 'object' &&\n\t\t\t'record' in mod &&\n\t\t\ttypeof (mod as { record: unknown }).record === 'function'\n\t\t) {\n\t\t\treturn (mod as { record: RrwebRecord }).record\n\t\t}\n\t\treturn null\n\t} catch {\n\t\treturn null\n\t}\n}\n\nfunction scheduleChunkUpload(store: ReplayStore, ctx: PluginContext): void {\n\tif (!store.sessionReplayId) return\n\tif (store.pendingEvents.length === 0) return\n\tif (store.pendingUploads >= MAX_PENDING_UPLOADS) {\n\t\tconst now = Date.now()\n\t\tif (now - store.lastUploadDropWarnAt > UPLOAD_DROP_WARN_INTERVAL_MS) {\n\t\t\tstore.lastUploadDropWarnAt = now\n\t\t\tctx.logger.warn(\n\t\t\t\t`upload queue full (${store.pendingUploads} in-flight), dropping chunk to bound memory`,\n\t\t\t)\n\t\t}\n\t\tstore.pendingEvents = []\n\t\tstore.pendingBytes = 0\n\t\tstore.pendingFirstTs = null\n\t\tstore.pendingLastTs = null\n\t\t// Track for the next successful chunk so the viewer can render a gap.\n\t\tstore.droppedSinceLastUpload += 1\n\t\treturn\n\t}\n\t// Chunk boundary: re-resolve the user. Captures mid-session login on\n\t// replay-only installs that never open the widget. No-op via fingerprint\n\t// dedupe if nothing changed.\n\ttry {\n\t\tctx.resolveUser?.()\n\t} catch (err) {\n\t\tctx.logger.warn('resolveUser threw at chunk boundary', err)\n\t}\n\tconst events = store.pendingEvents\n\tconst eventCount = events.length\n\tconst firstTs = store.pendingFirstTs ?? 0\n\tconst lastTs = store.pendingLastTs ?? firstTs\n\tconst durationMs = Math.max(0, lastTs - firstTs)\n\tconst seq = store.nextChunkSeq\n\tstore.nextChunkSeq += 1\n\tstore.pendingEvents = []\n\tstore.pendingBytes = 0\n\tstore.pendingFirstTs = null\n\tstore.pendingLastTs = null\n\n\tconst sessionReplayId = store.sessionReplayId\n\tconst apiUrl = store.options.apiUrl\n\tconst clientId = store.clientId\n\tconst maxAttempts = store.options.chunkMaxAttempts\n\n\tconst droppedBefore = store.droppedSinceLastUpload\n\tstore.pendingUploads += 1\n\tstore.uploadQueue = store.uploadQueue.then(async () => {\n\t\ttry {\n\t\t\tif (store.cancelled) return\n\t\t\tconst json = JSON.stringify(events)\n\t\t\tconst bytes = await gzipBytes(json)\n\t\t\tif (bytes.byteLength > HARD_CHUNK_BYTE_CAP) {\n\t\t\t\tctx.logger.error(\n\t\t\t\t\t`chunk ${seq} exceeds 4MB hard cap (${bytes.byteLength} bytes), dropping`,\n\t\t\t\t)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst result = await uploadChunk(\n\t\t\t\tapiUrl,\n\t\t\t\tsessionReplayId,\n\t\t\t\tclientId,\n\t\t\t\tseq,\n\t\t\t\tbytes,\n\t\t\t\teventCount,\n\t\t\t\tdurationMs,\n\t\t\t\tctx.logger,\n\t\t\t\tmaxAttempts,\n\t\t\t\tdroppedBefore,\n\t\t\t)\n\t\t\tif (result.ok && droppedBefore > 0) {\n\t\t\t\t// Subtract what we just reported, rather than zeroing, so any\n\t\t\t\t// drops that happened while this chunk was in flight still\n\t\t\t\t// surface on the next successful upload.\n\t\t\t\tstore.droppedSinceLastUpload = Math.max(\n\t\t\t\t\t0,\n\t\t\t\t\tstore.droppedSinceLastUpload - droppedBefore,\n\t\t\t\t)\n\t\t\t}\n\t\t\tif (result.stopSession) {\n\t\t\t\tstore.stopped = true\n\t\t\t\tstopRrweb(store)\n\t\t\t}\n\t\t} catch (err) {\n\t\t\tctx.logger.error(`chunk ${seq} encode failed`, err)\n\t\t} finally {\n\t\t\tstore.pendingUploads -= 1\n\t\t}\n\t})\n}\n\nfunction flushPendingChunk(store: ReplayStore, ctx: PluginContext): void {\n\tif (store.stopped || store.cancelled) return\n\tif (store.pendingEvents.length === 0) return\n\tscheduleChunkUpload(store, ctx)\n}\n\nfunction stopRrweb(store: ReplayStore): void {\n\tif (store.stopRecording) {\n\t\ttry {\n\t\t\tstore.stopRecording()\n\t\t} catch {\n\t\t\t// Already stopped.\n\t\t}\n\t\tstore.stopRecording = null\n\t}\n\tif (store.chunkFlushTimer) {\n\t\tclearInterval(store.chunkFlushTimer)\n\t\tstore.chunkFlushTimer = null\n\t}\n}\n\nfunction startRecording(store: ReplayStore, ctx: PluginContext): void {\n\tif (store.cancelled || store.stopped || store.stopRecording || store.loadInProgress) return\n\tstore.loadInProgress = true\n\tvoid loadRrwebRecord().then(record => {\n\t\tstore.loadInProgress = false\n\t\tif (store.cancelled || store.stopped || !record) {\n\t\t\tif (!record) ctx.logger.warn('rrweb failed to load, replay disabled')\n\t\t\treturn\n\t\t}\n\t\ttry {\n\t\t\tconst stop = record({\n\t\t\t\temit: event => {\n\t\t\t\t\tif (store.stopped || store.cancelled) return\n\t\t\t\t\tif (store.recordingStartedAt === null) store.recordingStartedAt = event.timestamp\n\t\t\t\t\tstore.pendingEvents.push(event)\n\t\t\t\t\t// Hot path: rrweb fires hundreds of events/sec on busy SPAs.\n\t\t\t\t\t// JSON.stringify-per-event burns CPU we don't have, and .length\n\t\t\t\t\t// is UTF-16 units (under-counts non-ASCII by ~2x) so it was\n\t\t\t\t\t// never a real byte count anyway. Use a per-type heuristic:\n\t\t\t\t\t// full snapshots are huge, mutations are mid, everything else\n\t\t\t\t\t// is cheap. chunkMaxBytes is documented as approximate.\n\t\t\t\t\tstore.pendingBytes += estimateEventBytes(event)\n\t\t\t\t\tif (store.pendingFirstTs === null) store.pendingFirstTs = event.timestamp\n\t\t\t\t\tstore.pendingLastTs = event.timestamp\n\t\t\t\t\tif (\n\t\t\t\t\t\tstore.pendingEvents.length >= store.options.chunkMaxEvents ||\n\t\t\t\t\t\tstore.pendingBytes >= store.options.chunkMaxBytes\n\t\t\t\t\t) {\n\t\t\t\t\t\tscheduleChunkUpload(store, ctx)\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\tmaskAllInputs: store.options.maskAllInputs,\n\t\t\t\tmaskTextSelector: store.options.maskTextSelector || undefined,\n\t\t\t\tinlineStylesheet: store.options.inlineStylesheet,\n\t\t\t\tblockSelector: store.options.blockSelector,\n\t\t\t\tsampling: store.options.sampling,\n\t\t\t\tcheckoutEveryNms: store.options.checkoutEveryMs,\n\t\t\t})\n\t\t\tstore.stopRecording = stop\n\t\t\tstore.record = record\n\t\t\tscheduleShadowSnapshot(store, ctx)\n\n\t\t\tstore.chunkFlushTimer = setInterval(\n\t\t\t\t() => flushPendingChunk(store, ctx),\n\t\t\t\tstore.options.chunkSeconds * 1000,\n\t\t\t)\n\t\t} catch (err) {\n\t\t\tctx.logger.error('rrweb record() threw', err)\n\t\t}\n\t})\n}\n\nfunction scheduleShadowSnapshot(store: ReplayStore, ctx: PluginContext): void {\n\tif (store.cancelled || store.stopped || !store.record || !store.stopRecording) return\n\tconst fn = store.record.takeFullSnapshot\n\tif (typeof fn !== 'function') return\n\ttry {\n\t\tfn(true)\n\t} catch (err) {\n\t\tctx.logger.warn('takeFullSnapshot threw', err)\n\t}\n}\n\nfunction finalise(store: ReplayStore, ctx: PluginContext, opts: { useBeacon: boolean }): void {\n\tif (!store.sessionReplayId) return\n\tif (store.pendingEvents.length > 0) flushPendingChunk(store, ctx)\n\tconst url = joinUrl(\n\t\tstore.options.apiUrl,\n\t\t`/api/replay-sessions/${encodeURIComponent(store.sessionReplayId)}/finalise`,\n\t)\n\tconst body = JSON.stringify({ clientId: store.clientId, endedAt: new Date().toISOString() })\n\tif (opts.useBeacon && typeof navigator !== 'undefined' && navigator.sendBeacon) {\n\t\ttry {\n\t\t\tconst blob = new Blob([body], { type: 'application/json' })\n\t\t\tnavigator.sendBeacon(url, blob)\n\t\t\treturn\n\t\t} catch (err) {\n\t\t\tctx.logger.warn('finalise sendBeacon threw', err)\n\t\t}\n\t}\n\tvoid fetch(url, {\n\t\tmethod: 'POST',\n\t\tbody,\n\t\theaders: { 'Content-Type': 'application/json' },\n\t\tkeepalive: true,\n\t}).catch(err => ctx.logger.warn('finalise fetch failed', err))\n}\n\nexport interface CurrentSessionHandle {\n\tid: string\n\toffsetMs: number\n}\n\nexport function sessionReplay(options: SessionReplayOptions = {}): UseroPlugin {\n\tconst merged: ResolvedOptions = {\n\t\t...DEFAULTS,\n\t\t...options,\n\t\tsampling: { ...DEFAULTS.sampling, ...(options.sampling ?? {}) },\n\t}\n\n\treturn {\n\t\tname: 'session-replay',\n\t\tonInit(ctx) {\n\t\t\tif (typeof window === 'undefined') return\n\t\t\tif (merged.sampleRate < 1 && Math.random() >= merged.sampleRate) {\n\t\t\t\tctx.logger.debug('skipped by sampleRate')\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tconst apiUrl = merged.apiUrl || ctx.baseUrl\n\t\t\tif (!apiUrl) {\n\t\t\t\tctx.logger.error('session-replay needs an apiUrl (via options or PluginContext)')\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst sdkSessionId = mintSdkSessionId()\n\t\t\t// Mint or read the cross-session anonymousId. Cached in module\n\t\t\t// scope after the first call, so this stays O(1) on hot paths.\n\t\t\tconst anonymousId = getOrMintAnonymousId()\n\n\t\t\tconst store: ReplayStore = {\n\t\t\t\toptions: { ...merged, apiUrl },\n\t\t\t\tclientId: ctx.clientId,\n\t\t\t\tsdkSessionId,\n\t\t\t\tsessionReplayId: null,\n\t\t\t\trecordingStartedAt: null,\n\t\t\t\tpendingEvents: [],\n\t\t\t\tpendingBytes: 0,\n\t\t\t\tpendingFirstTs: null,\n\t\t\t\tpendingLastTs: null,\n\t\t\t\tlastUploadDropWarnAt: 0,\n\t\t\t\tdroppedSinceLastUpload: 0,\n\t\t\t\tnextChunkSeq: 0,\n\t\t\t\tuploadQueue: Promise.resolve(),\n\t\t\t\tpendingUploads: 0,\n\t\t\t\tchunkFlushTimer: null,\n\t\t\t\tstartTimer: null,\n\t\t\t\tpageHideHandler: null,\n\t\t\t\tshadowUpdateHandler: null,\n\t\t\t\trecord: null,\n\t\t\t\tstopRecording: null,\n\t\t\t\tloadInProgress: false,\n\t\t\t\tcancelled: false,\n\t\t\t\tstopped: false,\n\t\t\t}\n\t\t\tctx.setStore(store)\n\n\t\t\tconst onShadowUpdate = (): void => scheduleShadowSnapshot(store, ctx)\n\t\t\tstore.shadowUpdateHandler = onShadowUpdate\n\t\t\twindow.addEventListener('usero:shadow-update', onShadowUpdate)\n\n\t\t\tconst onPageHide = (): void => {\n\t\t\t\tfinalise(store, ctx, { useBeacon: true })\n\t\t\t\tstore.stopped = true\n\t\t\t\tstopRrweb(store)\n\t\t\t}\n\t\t\tstore.pageHideHandler = onPageHide\n\t\t\twindow.addEventListener('pagehide', onPageHide)\n\n\t\t\tconst begin = async (): Promise<void> => {\n\t\t\t\tif (store.cancelled) return\n\t\t\t\t// Replay-only customers may never open the widget, so the host's\n\t\t\t\t// user state never gets polled by the widget's interaction\n\t\t\t\t// boundaries. Re-resolve here so a mid-session login that\n\t\t\t\t// happened before session start is visible server-side before\n\t\t\t\t// the first chunk lands. Fingerprint dedupe inside\n\t\t\t\t// identifyIfChanged makes this effectively free when nothing\n\t\t\t\t// changed.\n\t\t\t\ttry {\n\t\t\t\t\tctx.resolveUser?.()\n\t\t\t\t} catch (err) {\n\t\t\t\t\tctx.logger.warn('resolveUser threw at session start', err)\n\t\t\t\t}\n\t\t\t\tconst created = await createSession(apiUrl, ctx.clientId, sdkSessionId, anonymousId)\n\t\t\t\tif (!created) {\n\t\t\t\t\tctx.logger.warn('session create failed, replay disabled')\n\t\t\t\t\tstore.stopped = true\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tif (!created.accepted) {\n\t\t\t\t\tctx.logger.info(`session-replay declined: ${created.dropReason ?? 'unknown'}`)\n\t\t\t\t\tstore.stopped = true\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tif (!created.sessionReplayId) {\n\t\t\t\t\tctx.logger.error('server accepted but returned no sessionReplayId')\n\t\t\t\t\tstore.stopped = true\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tstore.sessionReplayId = created.sessionReplayId\n\t\t\t\tstore.recordingStartedAt = Date.now()\n\t\t\t\tstartRecording(store, ctx)\n\t\t\t}\n\n\t\t\tif (merged.startAfterMs > 0) {\n\t\t\t\tconst cancelOnExit = (): void => {\n\t\t\t\t\tstore.cancelled = true\n\t\t\t\t\tif (store.startTimer) {\n\t\t\t\t\t\tclearTimeout(store.startTimer)\n\t\t\t\t\t\tstore.startTimer = null\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\twindow.addEventListener('pagehide', cancelOnExit, { once: true })\n\t\t\t\twindow.addEventListener('beforeunload', cancelOnExit, { once: true })\n\t\t\t\tstore.startTimer = setTimeout(() => {\n\t\t\t\t\tvoid begin()\n\t\t\t\t}, merged.startAfterMs)\n\t\t\t} else {\n\t\t\t\tvoid begin()\n\t\t\t}\n\t\t},\n\t\tonFeedbackSubmit(ctx) {\n\t\t\tconst store = ctx.getStore<ReplayStore>()\n\t\t\tif (!store || store.cancelled || store.stopped) return undefined\n\t\t\tif (!store.sessionReplayId) return undefined\n\t\t\tconst offsetMs =\n\t\t\t\tstore.recordingStartedAt !== null\n\t\t\t\t\t? Math.max(0, Date.now() - store.recordingStartedAt)\n\t\t\t\t\t: 0\n\t\t\treturn { sessionReplayId: store.sessionReplayId, replayOffsetMs: offsetMs }\n\t\t},\n\t\tonDestroy(ctx) {\n\t\t\tconst store = ctx.getStore<ReplayStore>()\n\t\t\tif (!store) return\n\t\t\tstore.cancelled = true\n\t\t\tif (store.startTimer) {\n\t\t\t\tclearTimeout(store.startTimer)\n\t\t\t\tstore.startTimer = null\n\t\t\t}\n\t\t\tif (store.pageHideHandler) {\n\t\t\t\twindow.removeEventListener('pagehide', store.pageHideHandler)\n\t\t\t\tstore.pageHideHandler = null\n\t\t\t}\n\t\t\tif (store.shadowUpdateHandler) {\n\t\t\t\twindow.removeEventListener('usero:shadow-update', store.shadowUpdateHandler)\n\t\t\t\tstore.shadowUpdateHandler = null\n\t\t\t}\n\t\t\t// SPA route change / React unmount: send a finalise so the\n\t\t\t// server stamps endedAt. fetch+keepalive is fine here since we\n\t\t\t// aren't necessarily in a pagehide path.\n\t\t\tif (store.sessionReplayId && !store.stopped) {\n\t\t\t\tfinalise(store, ctx, { useBeacon: false })\n\t\t\t}\n\t\t\tstore.stopped = true\n\t\t\tstopRrweb(store)\n\t\t\tstore.pendingEvents.length = 0\n\t\t\tstore.pendingBytes = 0\n\t\t\tstore.record = null\n\t\t},\n\t}\n}\n\n// Returns the live session-replay handle for a given plugin context, or\n// null if the session was bot-dropped, sample-skipped, or not yet\n// created. Other plugins (e.g. user-test) can call this to attach the\n// replay FK + offset to their own server-side records.\nexport function getCurrentSession(ctx: PluginContext): CurrentSessionHandle | null {\n\tconst store = ctx.getStore<ReplayStore>()\n\tif (!store || store.cancelled || store.stopped || !store.sessionReplayId) return null\n\tconst offsetMs =\n\t\tstore.recordingStartedAt !== null\n\t\t\t? Math.max(0, Date.now() - store.recordingStartedAt)\n\t\t\t: 0\n\treturn { id: store.sessionReplayId, offsetMs }\n}\n\n// Internal helper exports for testing only. Not part of the public API.\nexport const __test__ = {\n\tuint8ToBase64,\n\tgzipBytes,\n\tmintSdkSessionId,\n\tuploadChunk,\n\tcreateSession,\n\tjoinUrl,\n\tscheduleChunkUpload,\n\tHARD_CHUNK_BYTE_CAP,\n\tSDK_SESSION_STORAGE_KEY,\n\tMAX_PENDING_UPLOADS,\n\tUPLOAD_DROP_WARN_INTERVAL_MS,\n\tDEFAULTS,\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/identity.ts","../../src/plugins/session-replay.ts"],"names":["generateRandomId"],"mappings":"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","file":"session-replay.js","sourcesContent":["// Identity layer for the Usero SDK.\n//\n// Two responsibilities:\n// 1. Mint and persist a stable per-browser `anonymousId` in localStorage\n// so cross-tab + cross-day replays from the same browser stitch\n// together server-side. Falls back to an in-memory id if storage is\n// blocked (sandboxed iframes, Safari Lockdown, full quota). Replay\n// still works in that case, you just lose stitching.\n// 2. Auto-fire POST /api/identify when the resolved user transitions\n// (null -> id, id -> id'). Deduped by an in-memory fingerprint so\n// re-renders with the same user are no-ops on the network.\n//\n// All storage access is wrapped in try/catch and gated behind a one-shot\n// init read. The hot path (replay chunk flush) never touches localStorage.\n\nconst ANON_STORAGE_KEY = 'usero:anonymous-id'\n\nlet cachedAnonymousId: string | null = null\n// Fingerprint of the last identify we POSTed. Same SDK instance + same\n// resolved user + same traits = no-op. Cleared on logout (anonymousId\n// rotation).\nlet lastIdentifyFingerprint: string | null = null\n\nexport type UserTraitValue = string | number | boolean | null\nexport type UserTraits = Record<string, UserTraitValue>\n\nexport interface UseroUser {\n\tid: string\n\temail?: string\n\tdisplayName?: string\n\ttraits?: UserTraits\n}\n\nfunction generateRandomId(): string {\n\tif (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {\n\t\treturn crypto.randomUUID()\n\t}\n\tconst bytes = new Uint8Array(16)\n\tif (typeof crypto !== 'undefined' && typeof crypto.getRandomValues === 'function') {\n\t\tcrypto.getRandomValues(bytes)\n\t} else {\n\t\tfor (let i = 0; i < bytes.length; i += 1) bytes[i] = Math.floor(Math.random() * 256)\n\t}\n\tlet out = ''\n\tfor (const b of bytes) out += b.toString(16).padStart(2, '0')\n\treturn out\n}\n\nfunction safeReadLocalStorage(key: string): string | null {\n\tif (typeof window === 'undefined') return null\n\ttry {\n\t\treturn window.localStorage?.getItem(key) ?? null\n\t} catch {\n\t\treturn null\n\t}\n}\n\nfunction safeWriteLocalStorage(key: string, value: string): void {\n\tif (typeof window === 'undefined') return\n\ttry {\n\t\twindow.localStorage?.setItem(key, value)\n\t} catch {\n\t\t// Sandboxed iframe / Safari Lockdown / quota. Fall back to memory.\n\t}\n}\n\n/**\n * Returns the stable per-browser anonymousId. Reads localStorage at most\n * once per SDK instance. Subsequent calls hit the in-memory cache, so\n * even hot paths (per-event in replay) are safe to call this.\n */\nexport function getOrMintAnonymousId(): string {\n\tif (cachedAnonymousId) return cachedAnonymousId\n\tconst existing = safeReadLocalStorage(ANON_STORAGE_KEY)\n\t// Sanity filter, not strict validation. We accept anything that looks\n\t// plausibly like an id (>=8 alphanumeric-or-hyphen) so older SDK\n\t// versions that wrote a slightly different shape still stitch. Fresh\n\t// mint is cheap, so we only reject obvious garbage; tightening this\n\t// would force rotation in customer browsers and split otherwise-good\n\t// sibling-session attribution.\n\tif (existing && /^[a-z0-9-]{8,}$/i.test(existing)) {\n\t\tcachedAnonymousId = existing\n\t\treturn existing\n\t}\n\tconst id = generateRandomId()\n\tsafeWriteLocalStorage(ANON_STORAGE_KEY, id)\n\tcachedAnonymousId = id\n\treturn id\n}\n\n/**\n * Rotate the anonymousId. Called on logout (user transitions from a\n * known id to null) so the next anonymous trail does not get auto-merged\n * into the previous person on the next identify().\n */\nexport function rotateAnonymousId(): string {\n\tconst id = generateRandomId()\n\tcachedAnonymousId = id\n\tsafeWriteLocalStorage(ANON_STORAGE_KEY, id)\n\tlastIdentifyFingerprint = null\n\treturn id\n}\n\nfunction fingerprintUser(anonymousId: string, user: UseroUser): string {\n\t// Stable across re-renders: keys sorted, traits canonicalised. Cheap\n\t// enough on the hot path (only runs when the SDK thinks the user might\n\t// have changed, never per-event).\n\tconst traits = user.traits ?? {}\n\tconst keys = Object.keys(traits).sort()\n\tconst canonical: Array<[string, UserTraitValue]> = keys.map(k => [k, traits[k] ?? null])\n\treturn JSON.stringify([anonymousId, user.id, user.email ?? null, user.displayName ?? null, canonical])\n}\n\nexport interface IdentifyTransport {\n\tapiUrl: string\n\tclientId: string\n}\n\n/**\n * POST to /api/identify if the (anonymousId, user) fingerprint differs\n * from the last call. Returns true if a network request actually fired.\n * Never throws; failures are best-effort and the caller (the widget /\n * provider) should not treat them as errors.\n *\n * Tab-unload safety: if the page is hidden when this fires (visibility\n * 'hidden' or a pagehide handler), we route the payload through\n * `navigator.sendBeacon` so the request survives unload. Otherwise we\n * use a normal fetch and only cache the fingerprint when the server\n * confirms `accepted: true`. A 200 `{ accepted: false }` (e.g.\n * `unknown_client` for a clientId that becomes valid mid-session) is\n * treated as retryable so the next call re-fires.\n */\nexport async function identifyIfChanged(transport: IdentifyTransport, user: UseroUser): Promise<boolean> {\n\tconst anonymousId = getOrMintAnonymousId()\n\tconst fp = fingerprintUser(anonymousId, user)\n\tif (fp === lastIdentifyFingerprint) return false\n\n\tconst url = `${transport.apiUrl.replace(/\\/$/, '')}/api/identify`\n\t// Body must stay under the browser's keepalive / sendBeacon cap\n\t// (~64KB across most engines) when this fires on pagehide. That\n\t// transitively caps trait payload size; in practice traits should be\n\t// small typed scalars, not blobs.\n\tconst body = JSON.stringify({\n\t\tclientId: transport.clientId,\n\t\tanonymousId,\n\t\texternalUserId: user.id,\n\t\temail: user.email,\n\t\tdisplayName: user.displayName,\n\t\ttraits: user.traits,\n\t})\n\n\t// If the document is hidden (pagehide / tab close in flight), best-effort\n\t// hand off to sendBeacon. We don't get a response back, so we optimistically\n\t// cache the fingerprint to avoid re-firing on the next page; the server is\n\t// idempotent if the page reload re-runs identify with the same payload.\n\tif (\n\t\ttypeof document !== 'undefined' &&\n\t\tdocument.visibilityState === 'hidden' &&\n\t\ttypeof navigator !== 'undefined' &&\n\t\ttypeof navigator.sendBeacon === 'function'\n\t) {\n\t\ttry {\n\t\t\tconst blob = new Blob([body], { type: 'application/json' })\n\t\t\t// sendBeacon returns false when the user agent refuses to queue\n\t\t\t// the request (size cap, or historically Safari rejecting\n\t\t\t// non-CORS-simple content types). Modern Safari accepts\n\t\t\t// application/json, but we keep a keepalive-fetch fallback so an\n\t\t\t// older WebKit that rejects the beacon still ships the identify.\n\t\t\tif (navigator.sendBeacon(url, blob)) {\n\t\t\t\tlastIdentifyFingerprint = fp\n\t\t\t\treturn true\n\t\t\t}\n\t\t} catch {\n\t\t\t// fall through to keepalive fetch below\n\t\t}\n\t}\n\n\ttry {\n\t\tconst res = await fetch(url, {\n\t\t\tmethod: 'POST',\n\t\t\theaders: { 'Content-Type': 'application/json' },\n\t\t\tbody,\n\t\t\t// keepalive lets the request survive a tab-close mid-flight on\n\t\t\t// browsers that support it; sendBeacon above is the primary path.\n\t\t\tkeepalive: true,\n\t\t})\n\t\tif (!res.ok) return true\n\t\t// Parse the response: a 200 with `accepted: false` (e.g. unknown\n\t\t// client) is retryable. Only cache the fingerprint when the server\n\t\t// confirmed it actually stored the identity.\n\t\ttry {\n\t\t\tconst json = (await res.json()) as { accepted?: unknown }\n\t\t\tif (json && json.accepted === true) lastIdentifyFingerprint = fp\n\t\t} catch {\n\t\t\t// Server returned 2xx but unparseable body: don't cache, let the\n\t\t\t// next call retry.\n\t\t}\n\t\treturn true\n\t} catch {\n\t\treturn false\n\t}\n}\n\n/**\n * Clear identify state and rotate anonymousId. Called when the resolved\n * user transitions from a known id to null (logout). The next anonymous\n * trail will get a fresh anonymousId so it does not merge into the\n * previous person.\n */\nexport function handleLogout(): void {\n\trotateAnonymousId()\n}\n\n// Test hooks (not exported from the package public surface).\nexport const __test__ = {\n\tANON_STORAGE_KEY,\n\tresetIdentityState: (): void => {\n\t\tcachedAnonymousId = null\n\t\tlastIdentifyFingerprint = null\n\t},\n}\n","// Session replay plugin for the Usero widget.\n//\n// Streams rrweb events to the SaaS side as gzipped chunks while the user\n// is on the page, instead of buffering in memory and attaching to a\n// feedback submission. This decouples session replay from feedback so we\n// capture every session (subject to bot-gate + sampling + engagement\n// gates), not just the ones that submit feedback.\n//\n// Lifecycle:\n// 1. onInit: dice-roll sample, optional engagement-time gate, mint a\n// stable per-tab `sdkSessionId` in sessionStorage, and POST to\n// /api/replay-sessions to create the row. If the server returns\n// `{accepted:false}` (bot-gated), the plugin no-ops the rest of the\n// session and getCurrentSession() returns null.\n// 2. Recording: lazy-load rrweb, append events to a buffer, flush a\n// chunk every `chunkSeconds` (or sooner if the buffer is large).\n// Each chunk is gzipped via CompressionStream and PUT to\n// /api/replay-sessions/:id/chunks/:seq with raw bytes + the three\n// X-Usero-* headers (Client-Id, Event-Count, Duration-Ms). Retries\n// with exponential backoff. R2 head-check makes retries idempotent\n// server-side. A chunk PUT returning 409 stops the session.\n// 3. onFeedbackSubmit: returns `{sessionReplayId, replayOffsetMs}` so\n// the feedback record can FK at the moment of submit. Does NOT\n// attach `replayEvents` (legacy field) — chunked uploads carry the\n// events out-of-band.\n// 4. onDestroy / pagehide: best-effort flush remaining buffer, then\n// sendBeacon to /api/replay-sessions/:id/finalise with the\n// end-timestamp. Idempotent server-side.\n//\n// Bundle hygiene: rrweb stays lazy via dynamic `import('rrweb')` behind\n// the engagement gate, so consumers who lose the dice roll or navigate\n// away inside the gate window pay zero rrweb bytes.\n\nimport { getOrMintAnonymousId } from '../identity'\nimport type { UseroPlugin, PluginContext } from '../plugin'\n\nexport interface ReplaySampling {\n\tmousemove?: number\n\tscroll?: number\n\tmedia?: number\n\tinput?: number | 'last'\n}\n\nexport interface SessionReplayOptions {\n\t// Wait this many ms after page load before loading rrweb and creating\n\t// the session row. If the user navigates away first, rrweb is never\n\t// loaded and no session row is created. Default 0 (start immediately).\n\tstartAfterMs?: number\n\t// Probability (0..1) that this session records at all. Decided once\n\t// at init via Math.random(). Default 1.\n\tsampleRate?: number\n\t// rrweb sampling rates per event type.\n\tsampling?: ReplaySampling\n\t// Mask all <input>/<textarea> values in the recording. Default true.\n\tmaskAllInputs?: boolean\n\t// CSS selector for nodes whose text content should be masked. Default\n\t// `[data-usero-mask]`.\n\tmaskTextSelector?: string\n\t// Inline external stylesheets so the replay viewer renders correctly\n\t// without network access. Default true.\n\tinlineStylesheet?: boolean\n\t// Block (entirely skip) DOM subtrees matching this selector. Default\n\t// `[data-usero-block]`.\n\tblockSelector?: string\n\t// Flush a chunk every N seconds. Default 3. Smaller = more PUTs but\n\t// less data lost on tab crash and less time event refs retain detached\n\t// DOM nodes in memory.\n\tchunkSeconds?: number\n\t// Soft cap on buffered events before forcing a flush, regardless of\n\t// time. Default 1000.\n\tchunkMaxEvents?: number\n\t// Soft cap on estimated buffered bytes before forcing a flush. Default\n\t// 512_000 (~500 KB pre-gzip). Keeps memory pressure bounded on event-heavy\n\t// pages even when chunkMaxEvents hasn't been hit.\n\tchunkMaxBytes?: number\n\t// Max attempts per chunk before giving up. Default 5.\n\tchunkMaxAttempts?: number\n\t// Force rrweb to take a fresh full snapshot every N ms. This resets\n\t// rrweb's internal mirror so detached DOM (e.g. SPA route changes)\n\t// becomes GC-eligible. Default 60_000.\n\tcheckoutEveryMs?: number\n\t// API origin. Override for self-hosted or local dev. Defaults to the\n\t// PluginContext baseUrl threaded through by the widget.\n\tapiUrl?: string\n}\n\ninterface RrwebEvent {\n\ttype: number\n\tdata: unknown\n\ttimestamp: number\n}\n\ninterface RrwebRecordOptions {\n\temit: (event: RrwebEvent) => void\n\tmaskAllInputs?: boolean\n\tmaskTextSelector?: string\n\tinlineStylesheet?: boolean\n\tblockSelector?: string\n\tsampling?: ReplaySampling\n\tcheckoutEveryNms?: number\n}\n\ninterface RrwebRecordFn {\n\t(opts: RrwebRecordOptions): () => void\n\ttakeFullSnapshot?: (isCheckout?: boolean) => void\n}\n\ntype RrwebRecord = RrwebRecordFn\n\ninterface ResolvedOptions {\n\tstartAfterMs: number\n\tsampleRate: number\n\tsampling: ReplaySampling\n\tmaskAllInputs: boolean\n\tmaskTextSelector: string\n\tinlineStylesheet: boolean\n\tblockSelector: string\n\tchunkSeconds: number\n\tchunkMaxEvents: number\n\tchunkMaxBytes: number\n\tchunkMaxAttempts: number\n\tcheckoutEveryMs: number\n\tapiUrl: string\n}\n\ninterface ReplayStore {\n\toptions: ResolvedOptions\n\tclientId: string\n\tsdkSessionId: string\n\tsessionReplayId: string | null\n\t// Wall-clock timestamp (ms) of the first event we ever recorded.\n\t// Used to compute replayOffsetMs at feedback-submit time.\n\trecordingStartedAt: number | null\n\tpendingEvents: RrwebEvent[]\n\tpendingBytes: number\n\tpendingFirstTs: number | null\n\tpendingLastTs: number | null\n\tlastUploadDropWarnAt: number\n\t// Count of chunks dropped (queue saturation) since the last successful\n\t// upload. Sent as a header on the next successful chunk PUT so the\n\t// viewer can show a \"gap here\" marker. Reset on success.\n\tdroppedSinceLastUpload: number\n\t// Wall-clock timestamp of the last snapshot-isolation flush. Used to\n\t// rate-limit pre-snapshot flushes so SPA route-change snapshot bursts\n\t// don't trigger a flush storm.\n\tlastSnapshotFlushAt: number\n\tnextChunkSeq: number\n\tuploadQueue: Promise<void>\n\tpendingUploads: number\n\tchunkFlushTimer: ReturnType<typeof setInterval> | null\n\tstartTimer: ReturnType<typeof setTimeout> | null\n\tpageHideHandler: (() => void) | null\n\tshadowUpdateHandler: ((event: Event) => void) | null\n\trecord: RrwebRecord | null\n\tstopRecording: (() => void) | null\n\tloadInProgress: boolean\n\tcancelled: boolean\n\t// True once the session is \"done\": bot-gated, finalised, or destroyed.\n\tstopped: boolean\n}\n\nconst DEFAULTS: ResolvedOptions = {\n\tstartAfterMs: 0,\n\tsampleRate: 1,\n\tsampling: { mousemove: 50, scroll: 100 },\n\tmaskAllInputs: true,\n\tmaskTextSelector: '[data-usero-mask]',\n\tinlineStylesheet: true,\n\tblockSelector: '[data-usero-block]',\n\tchunkSeconds: 3,\n\tchunkMaxEvents: 1000,\n\tchunkMaxBytes: 512_000,\n\tchunkMaxAttempts: 5,\n\tcheckoutEveryMs: 60_000,\n\tapiUrl: '',\n}\n\nconst SDK_SESSION_STORAGE_KEY = 'usero:session-replay:sdk-session-id'\nconst HARD_CHUNK_BYTE_CAP = 4 * 1024 * 1024\nconst MAX_PENDING_UPLOADS = 3\nconst UPLOAD_DROP_WARN_INTERVAL_MS = 5000\n// rrweb EventType.FullSnapshot. We don't import rrweb's enum because rrweb is\n// dynamically imported (bundle hygiene), so we'd have to pay the load cost\n// just to reference a constant. Magic number matches estimateEventBytes above\n// and rrweb's stable public event-type enum.\nconst RRWEB_EVENT_TYPE_FULL_SNAPSHOT = 2\n// Minimum gap between back-to-back snapshot-isolation flushes. Snapshots\n// normally fire every checkoutEveryMs (default 60s), but rrweb can emit\n// additional ones on SPA route changes via checkoutEveryNms. Keeping this\n// below chunkSeconds * 1000 / 2 of the default (3000ms) and well under\n// checkoutEveryMs ensures isolation still happens for back-to-back snapshots\n// while preventing pathological flush storms.\nconst SNAPSHOT_ISOLATION_MIN_GAP_MS = 1500\n\nfunction uint8ToBase64(bytes: Uint8Array): string {\n\tlet binary = ''\n\tconst chunkSize = 0x8000\n\tfor (let i = 0; i < bytes.length; i += chunkSize) {\n\t\tconst slice = bytes.subarray(i, i + chunkSize)\n\t\tbinary += String.fromCharCode.apply(null, Array.from(slice))\n\t}\n\treturn typeof btoa === 'function' ? btoa(binary) : ''\n}\n\nasync function gzipBytes(input: string): Promise<Uint8Array> {\n\tif (typeof CompressionStream === 'undefined') {\n\t\t// Old browsers: send uncompressed JSON. Acceptable degradation;\n\t\t// the server endpoint accepts raw application/octet-stream.\n\t\treturn new TextEncoder().encode(input)\n\t}\n\tconst stream = new Blob([input]).stream().pipeThrough(new CompressionStream('gzip'))\n\tconst buf = await new Response(stream).arrayBuffer()\n\treturn new Uint8Array(buf)\n}\n\nfunction generateRandomId(): string {\n\tif (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {\n\t\treturn crypto.randomUUID()\n\t}\n\tconst bytes = new Uint8Array(16)\n\tif (typeof crypto !== 'undefined' && typeof crypto.getRandomValues === 'function') {\n\t\tcrypto.getRandomValues(bytes)\n\t} else {\n\t\tfor (let i = 0; i < bytes.length; i += 1) bytes[i] = Math.floor(Math.random() * 256)\n\t}\n\tlet out = ''\n\tfor (const b of bytes) out += b.toString(16).padStart(2, '0')\n\treturn out\n}\n\nfunction mintSdkSessionId(): string {\n\ttry {\n\t\tconst existing = window.sessionStorage?.getItem(SDK_SESSION_STORAGE_KEY)\n\t\tif (existing && /^[a-z0-9-]{8,}$/i.test(existing)) return existing\n\t} catch {\n\t\t// sessionStorage can throw in sandboxed iframes — fall through.\n\t}\n\tconst id = generateRandomId()\n\ttry {\n\t\twindow.sessionStorage?.setItem(SDK_SESSION_STORAGE_KEY, id)\n\t} catch {\n\t\t// Ignore: we still return the freshly minted id.\n\t}\n\treturn id\n}\n\nfunction joinUrl(apiUrl: string, path: string): string {\n\treturn `${apiUrl.replace(/\\/$/, '')}${path}`\n}\n\n// Cheap per-event byte estimate. Avoids JSON.stringify on the hot emit path.\n// rrweb EventType: 0=DomContentLoaded, 1=Load, 2=FullSnapshot, 3=IncrementalSnapshot,\n// 4=Meta, 5=Custom, 6=Plugin. Full snapshots are the only event class that's\n// genuinely large; everything else is well under a KB on average. Numbers\n// chosen to over-estimate slightly so chunkMaxBytes stays a safety net.\nfunction estimateEventBytes(event: RrwebEvent): number {\n\tif (event.type === 2) return 50_000\n\tif (event.type === 3) return 256\n\treturn 128\n}\n\ninterface CreateSessionResult {\n\taccepted: boolean\n\tsessionReplayId?: string\n\tdropReason?: string\n}\n\nasync function createSession(\n\tapiUrl: string,\n\tclientId: string,\n\tsdkSessionId: string,\n\tanonymousId: string,\n): Promise<CreateSessionResult | null> {\n\ttry {\n\t\tconst startUrl =\n\t\t\ttypeof window !== 'undefined' && window.location ? window.location.href : undefined\n\t\tconst userAgent =\n\t\t\ttypeof navigator !== 'undefined' && navigator.userAgent ? navigator.userAgent : undefined\n\t\tconst res = await fetch(joinUrl(apiUrl, '/api/replay-sessions'), {\n\t\t\tmethod: 'POST',\n\t\t\theaders: { 'Content-Type': 'application/json' },\n\t\t\tbody: JSON.stringify({\n\t\t\t\tclientId,\n\t\t\t\tsdkSessionId,\n\t\t\t\tanonymousId,\n\t\t\t\tstartUrl,\n\t\t\t\tuserAgent,\n\t\t\t\tstartedAt: new Date().toISOString(),\n\t\t\t}),\n\t\t})\n\t\tif (!res.ok) return null\n\t\tconst json = (await res.json()) as {\n\t\t\taccepted?: unknown\n\t\t\tsessionReplayId?: unknown\n\t\t\tdropReason?: unknown\n\t\t}\n\t\tif (typeof json.accepted !== 'boolean') return null\n\t\tconst result: CreateSessionResult = { accepted: json.accepted }\n\t\tif (typeof json.sessionReplayId === 'string') result.sessionReplayId = json.sessionReplayId\n\t\tif (typeof json.dropReason === 'string') result.dropReason = json.dropReason\n\t\treturn result\n\t} catch {\n\t\treturn null\n\t}\n}\n\ninterface ChunkUploadResult {\n\tok: boolean\n\tstopSession: boolean\n}\n\nasync function uploadChunk(\n\tapiUrl: string,\n\tsessionReplayId: string,\n\tclientId: string,\n\tseq: number,\n\tbytes: Uint8Array,\n\teventCount: number,\n\tdurationMs: number,\n\tlogger: PluginContext['logger'],\n\tmaxAttempts: number,\n\tdroppedBefore: number,\n): Promise<ChunkUploadResult> {\n\tconst url = joinUrl(\n\t\tapiUrl,\n\t\t`/api/replay-sessions/${encodeURIComponent(sessionReplayId)}/chunks/${seq}`,\n\t)\n\tlet attempt = 0\n\twhile (attempt < maxAttempts) {\n\t\ttry {\n\t\t\t// Wrap in a Blob so the body type is unambiguously BodyInit; some\n\t\t\t// TS lib targets reject raw Uint8Array as fetch body. Slice off\n\t\t\t// the buffer to satisfy the BlobPart ArrayBuffer constraint\n\t\t\t// (Uint8Array<SharedArrayBuffer> is the alternative the lib\n\t\t\t// admits, which we never produce here).\n\t\t\tconst buffer = bytes.buffer.slice(\n\t\t\t\tbytes.byteOffset,\n\t\t\t\tbytes.byteOffset + bytes.byteLength,\n\t\t\t) as ArrayBuffer\n\t\t\tconst blob = new Blob([buffer], { type: 'application/octet-stream' })\n\t\t\tconst headers: Record<string, string> = {\n\t\t\t\t'Content-Type': 'application/octet-stream',\n\t\t\t\t'X-Usero-Client-Id': clientId,\n\t\t\t\t'X-Usero-Event-Count': String(eventCount),\n\t\t\t\t'X-Usero-Duration-Ms': String(Math.max(0, Math.round(durationMs))),\n\t\t\t}\n\t\t\t// Signal a playback gap: how many chunks were dropped (queue\n\t\t\t// saturation) between the previous successful upload and this one.\n\t\t\t// Server-side viewer will use this to render a \"missing data\" marker.\n\t\t\tif (droppedBefore > 0) headers['X-Usero-Dropped-Before'] = String(droppedBefore)\n\t\t\tconst res = await fetch(url, {\n\t\t\t\tmethod: 'PUT',\n\t\t\t\tbody: blob,\n\t\t\t\theaders,\n\t\t\t})\n\t\t\tif (res.ok) return { ok: true, stopSession: false }\n\t\t\t// 409: server told us to stop (bot-dropped, or session already\n\t\t\t// finalised). Don't retry, don't upload further chunks.\n\t\t\tif (res.status === 409) {\n\t\t\t\tlogger.warn(`chunk ${seq} rejected with 409, stopping session`)\n\t\t\t\treturn { ok: false, stopSession: true }\n\t\t\t}\n\t\t\t// Other 4xx (besides 408/429) won't get better with retry.\n\t\t\tif (res.status >= 400 && res.status < 500 && res.status !== 408 && res.status !== 429) {\n\t\t\t\tlogger.error(`chunk ${seq} rejected with ${res.status}`)\n\t\t\t\treturn { ok: false, stopSession: false }\n\t\t\t}\n\t\t} catch (err) {\n\t\t\tlogger.warn(`chunk ${seq} attempt ${attempt + 1} failed`, err)\n\t\t}\n\t\tattempt += 1\n\t\tconst backoff = Math.min(15_000, 500 * 2 ** attempt) + Math.floor(Math.random() * 250)\n\t\tawait new Promise(resolve => setTimeout(resolve, backoff))\n\t}\n\tlogger.error(`chunk ${seq} dropped after ${maxAttempts} attempts`)\n\treturn { ok: false, stopSession: false }\n}\n\nasync function loadRrwebRecord(): Promise<RrwebRecord | null> {\n\ttry {\n\t\tconst mod: unknown = await import(/* webpackChunkName: \"rrweb\" */ 'rrweb')\n\t\tif (\n\t\t\tmod &&\n\t\t\ttypeof mod === 'object' &&\n\t\t\t'record' in mod &&\n\t\t\ttypeof (mod as { record: unknown }).record === 'function'\n\t\t) {\n\t\t\treturn (mod as { record: RrwebRecord }).record\n\t\t}\n\t\treturn null\n\t} catch {\n\t\treturn null\n\t}\n}\n\nfunction scheduleChunkUpload(store: ReplayStore, ctx: PluginContext): void {\n\tif (!store.sessionReplayId) return\n\tif (store.pendingEvents.length === 0) return\n\tif (store.pendingUploads >= MAX_PENDING_UPLOADS) {\n\t\tconst now = Date.now()\n\t\tif (now - store.lastUploadDropWarnAt > UPLOAD_DROP_WARN_INTERVAL_MS) {\n\t\t\tstore.lastUploadDropWarnAt = now\n\t\t\tctx.logger.warn(\n\t\t\t\t`upload queue full (${store.pendingUploads} in-flight), dropping chunk to bound memory`,\n\t\t\t)\n\t\t}\n\t\tstore.pendingEvents = []\n\t\tstore.pendingBytes = 0\n\t\tstore.pendingFirstTs = null\n\t\tstore.pendingLastTs = null\n\t\t// Track for the next successful chunk so the viewer can render a gap.\n\t\tstore.droppedSinceLastUpload += 1\n\t\treturn\n\t}\n\t// Chunk boundary: re-resolve the user. Captures mid-session login on\n\t// replay-only installs that never open the widget. No-op via fingerprint\n\t// dedupe if nothing changed.\n\ttry {\n\t\tctx.resolveUser?.()\n\t} catch (err) {\n\t\tctx.logger.warn('resolveUser threw at chunk boundary', err)\n\t}\n\tconst events = store.pendingEvents\n\tconst eventCount = events.length\n\tconst firstTs = store.pendingFirstTs ?? 0\n\tconst lastTs = store.pendingLastTs ?? firstTs\n\tconst durationMs = Math.max(0, lastTs - firstTs)\n\tconst seq = store.nextChunkSeq\n\tstore.nextChunkSeq += 1\n\tstore.pendingEvents = []\n\tstore.pendingBytes = 0\n\tstore.pendingFirstTs = null\n\tstore.pendingLastTs = null\n\n\tconst sessionReplayId = store.sessionReplayId\n\tconst apiUrl = store.options.apiUrl\n\tconst clientId = store.clientId\n\tconst maxAttempts = store.options.chunkMaxAttempts\n\n\tconst droppedBefore = store.droppedSinceLastUpload\n\tstore.pendingUploads += 1\n\tstore.uploadQueue = store.uploadQueue.then(async () => {\n\t\ttry {\n\t\t\tif (store.cancelled) return\n\t\t\tconst json = JSON.stringify(events)\n\t\t\tconst bytes = await gzipBytes(json)\n\t\t\tif (bytes.byteLength > HARD_CHUNK_BYTE_CAP) {\n\t\t\t\tctx.logger.error(\n\t\t\t\t\t`chunk ${seq} exceeds 4MB hard cap (${bytes.byteLength} bytes), dropping`,\n\t\t\t\t)\n\t\t\t\t// Surface the drop on the next successful chunk so the viewer\n\t\t\t\t// can render a gap marker. Without this, oversized chunks\n\t\t\t\t// vanish without trace server-side.\n\t\t\t\tstore.droppedSinceLastUpload += 1\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst result = await uploadChunk(\n\t\t\t\tapiUrl,\n\t\t\t\tsessionReplayId,\n\t\t\t\tclientId,\n\t\t\t\tseq,\n\t\t\t\tbytes,\n\t\t\t\teventCount,\n\t\t\t\tdurationMs,\n\t\t\t\tctx.logger,\n\t\t\t\tmaxAttempts,\n\t\t\t\tdroppedBefore,\n\t\t\t)\n\t\t\tif (result.ok && droppedBefore > 0) {\n\t\t\t\t// Subtract what we just reported, rather than zeroing, so any\n\t\t\t\t// drops that happened while this chunk was in flight still\n\t\t\t\t// surface on the next successful upload.\n\t\t\t\tstore.droppedSinceLastUpload = Math.max(\n\t\t\t\t\t0,\n\t\t\t\t\tstore.droppedSinceLastUpload - droppedBefore,\n\t\t\t\t)\n\t\t\t}\n\t\t\tif (result.stopSession) {\n\t\t\t\tstore.stopped = true\n\t\t\t\tstopRrweb(store)\n\t\t\t}\n\t\t} catch (err) {\n\t\t\tctx.logger.error(`chunk ${seq} encode failed`, err)\n\t\t} finally {\n\t\t\tstore.pendingUploads -= 1\n\t\t}\n\t})\n}\n\nfunction flushPendingChunk(store: ReplayStore, ctx: PluginContext): void {\n\tif (store.stopped || store.cancelled) return\n\tif (store.pendingEvents.length === 0) return\n\tscheduleChunkUpload(store, ctx)\n}\n\nfunction stopRrweb(store: ReplayStore): void {\n\tif (store.stopRecording) {\n\t\ttry {\n\t\t\tstore.stopRecording()\n\t\t} catch {\n\t\t\t// Already stopped.\n\t\t}\n\t\tstore.stopRecording = null\n\t}\n\tif (store.chunkFlushTimer) {\n\t\tclearInterval(store.chunkFlushTimer)\n\t\tstore.chunkFlushTimer = null\n\t}\n}\n\nfunction startRecording(store: ReplayStore, ctx: PluginContext): void {\n\tif (store.cancelled || store.stopped || store.stopRecording || store.loadInProgress) return\n\tstore.loadInProgress = true\n\tvoid loadRrwebRecord().then(record => {\n\t\tstore.loadInProgress = false\n\t\tif (store.cancelled || store.stopped || !record) {\n\t\t\tif (!record) ctx.logger.warn('rrweb failed to load, replay disabled')\n\t\t\treturn\n\t\t}\n\t\ttry {\n\t\t\tconst stop = record({\n\t\t\t\temit: event => {\n\t\t\t\t\tif (store.stopped || store.cancelled) return\n\t\t\t\t\tif (store.recordingStartedAt === null) store.recordingStartedAt = event.timestamp\n\t\t\t\t\t// FullSnapshot isolation: the snapshot is the playback anchor\n\t\t\t\t\t// for every subsequent incremental event. If a chunk crosses\n\t\t\t\t\t// the 4MB gzipped hard cap, it's dropped wholesale, taking\n\t\t\t\t\t// the anchor with it and breaking playback for up to a full\n\t\t\t\t\t// checkoutEveryMs window, not just the chunk's own duration.\n\t\t\t\t\t// Mitigate by flushing the pre-snapshot buffer separately so\n\t\t\t\t\t// the snapshot lands in a near-empty chunk. Rate-limit to\n\t\t\t\t\t// avoid flush storms on SPA route-change snapshot bursts.\n\t\t\t\t\tif (event.type === RRWEB_EVENT_TYPE_FULL_SNAPSHOT) {\n\t\t\t\t\t\tconst now = Date.now()\n\t\t\t\t\t\tif (\n\t\t\t\t\t\t\tstore.pendingEvents.length > 0 &&\n\t\t\t\t\t\t\tnow - store.lastSnapshotFlushAt >= SNAPSHOT_ISOLATION_MIN_GAP_MS\n\t\t\t\t\t\t) {\n\t\t\t\t\t\t\tscheduleChunkUpload(store, ctx)\n\t\t\t\t\t\t\tstore.lastSnapshotFlushAt = now\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tstore.pendingEvents.push(event)\n\t\t\t\t\t// Hot path: rrweb fires hundreds of events/sec on busy SPAs.\n\t\t\t\t\t// JSON.stringify-per-event burns CPU we don't have, and .length\n\t\t\t\t\t// is UTF-16 units (under-counts non-ASCII by ~2x) so it was\n\t\t\t\t\t// never a real byte count anyway. Use a per-type heuristic:\n\t\t\t\t\t// full snapshots are huge, mutations are mid, everything else\n\t\t\t\t\t// is cheap. chunkMaxBytes is documented as approximate.\n\t\t\t\t\tstore.pendingBytes += estimateEventBytes(event)\n\t\t\t\t\tif (store.pendingFirstTs === null) store.pendingFirstTs = event.timestamp\n\t\t\t\t\tstore.pendingLastTs = event.timestamp\n\t\t\t\t\tif (\n\t\t\t\t\t\tstore.pendingEvents.length >= store.options.chunkMaxEvents ||\n\t\t\t\t\t\tstore.pendingBytes >= store.options.chunkMaxBytes\n\t\t\t\t\t) {\n\t\t\t\t\t\tscheduleChunkUpload(store, ctx)\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\tmaskAllInputs: store.options.maskAllInputs,\n\t\t\t\tmaskTextSelector: store.options.maskTextSelector || undefined,\n\t\t\t\tinlineStylesheet: store.options.inlineStylesheet,\n\t\t\t\tblockSelector: store.options.blockSelector,\n\t\t\t\tsampling: store.options.sampling,\n\t\t\t\tcheckoutEveryNms: store.options.checkoutEveryMs,\n\t\t\t})\n\t\t\tstore.stopRecording = stop\n\t\t\tstore.record = record\n\t\t\tscheduleShadowSnapshot(store, ctx)\n\n\t\t\tstore.chunkFlushTimer = setInterval(\n\t\t\t\t() => flushPendingChunk(store, ctx),\n\t\t\t\tstore.options.chunkSeconds * 1000,\n\t\t\t)\n\t\t} catch (err) {\n\t\t\tctx.logger.error('rrweb record() threw', err)\n\t\t}\n\t})\n}\n\nfunction scheduleShadowSnapshot(store: ReplayStore, ctx: PluginContext): void {\n\tif (store.cancelled || store.stopped || !store.record || !store.stopRecording) return\n\tconst fn = store.record.takeFullSnapshot\n\tif (typeof fn !== 'function') return\n\ttry {\n\t\tfn(true)\n\t} catch (err) {\n\t\tctx.logger.warn('takeFullSnapshot threw', err)\n\t}\n}\n\nfunction finalise(store: ReplayStore, ctx: PluginContext, opts: { useBeacon: boolean }): void {\n\tif (!store.sessionReplayId) return\n\tif (store.pendingEvents.length > 0) flushPendingChunk(store, ctx)\n\tconst url = joinUrl(\n\t\tstore.options.apiUrl,\n\t\t`/api/replay-sessions/${encodeURIComponent(store.sessionReplayId)}/finalise`,\n\t)\n\tconst body = JSON.stringify({ clientId: store.clientId, endedAt: new Date().toISOString() })\n\tif (opts.useBeacon && typeof navigator !== 'undefined' && navigator.sendBeacon) {\n\t\ttry {\n\t\t\tconst blob = new Blob([body], { type: 'application/json' })\n\t\t\tnavigator.sendBeacon(url, blob)\n\t\t\treturn\n\t\t} catch (err) {\n\t\t\tctx.logger.warn('finalise sendBeacon threw', err)\n\t\t}\n\t}\n\tvoid fetch(url, {\n\t\tmethod: 'POST',\n\t\tbody,\n\t\theaders: { 'Content-Type': 'application/json' },\n\t\tkeepalive: true,\n\t}).catch(err => ctx.logger.warn('finalise fetch failed', err))\n}\n\nexport interface CurrentSessionHandle {\n\tid: string\n\toffsetMs: number\n}\n\nexport function sessionReplay(options: SessionReplayOptions = {}): UseroPlugin {\n\tconst merged: ResolvedOptions = {\n\t\t...DEFAULTS,\n\t\t...options,\n\t\tsampling: { ...DEFAULTS.sampling, ...(options.sampling ?? {}) },\n\t}\n\n\treturn {\n\t\tname: 'session-replay',\n\t\tonInit(ctx) {\n\t\t\tif (typeof window === 'undefined') return\n\t\t\tif (merged.sampleRate < 1 && Math.random() >= merged.sampleRate) {\n\t\t\t\tctx.logger.debug('skipped by sampleRate')\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tconst apiUrl = merged.apiUrl || ctx.baseUrl\n\t\t\tif (!apiUrl) {\n\t\t\t\tctx.logger.error('session-replay needs an apiUrl (via options or PluginContext)')\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst sdkSessionId = mintSdkSessionId()\n\t\t\t// Mint or read the cross-session anonymousId. Cached in module\n\t\t\t// scope after the first call, so this stays O(1) on hot paths.\n\t\t\tconst anonymousId = getOrMintAnonymousId()\n\n\t\t\tconst store: ReplayStore = {\n\t\t\t\toptions: { ...merged, apiUrl },\n\t\t\t\tclientId: ctx.clientId,\n\t\t\t\tsdkSessionId,\n\t\t\t\tsessionReplayId: null,\n\t\t\t\trecordingStartedAt: null,\n\t\t\t\tpendingEvents: [],\n\t\t\t\tpendingBytes: 0,\n\t\t\t\tpendingFirstTs: null,\n\t\t\t\tpendingLastTs: null,\n\t\t\t\tlastUploadDropWarnAt: 0,\n\t\t\t\tdroppedSinceLastUpload: 0,\n\t\t\t\tlastSnapshotFlushAt: 0,\n\t\t\t\tnextChunkSeq: 0,\n\t\t\t\tuploadQueue: Promise.resolve(),\n\t\t\t\tpendingUploads: 0,\n\t\t\t\tchunkFlushTimer: null,\n\t\t\t\tstartTimer: null,\n\t\t\t\tpageHideHandler: null,\n\t\t\t\tshadowUpdateHandler: null,\n\t\t\t\trecord: null,\n\t\t\t\tstopRecording: null,\n\t\t\t\tloadInProgress: false,\n\t\t\t\tcancelled: false,\n\t\t\t\tstopped: false,\n\t\t\t}\n\t\t\tctx.setStore(store)\n\n\t\t\tconst onShadowUpdate = (): void => scheduleShadowSnapshot(store, ctx)\n\t\t\tstore.shadowUpdateHandler = onShadowUpdate\n\t\t\twindow.addEventListener('usero:shadow-update', onShadowUpdate)\n\n\t\t\tconst onPageHide = (): void => {\n\t\t\t\tfinalise(store, ctx, { useBeacon: true })\n\t\t\t\tstore.stopped = true\n\t\t\t\tstopRrweb(store)\n\t\t\t}\n\t\t\tstore.pageHideHandler = onPageHide\n\t\t\twindow.addEventListener('pagehide', onPageHide)\n\n\t\t\tconst begin = async (): Promise<void> => {\n\t\t\t\tif (store.cancelled) return\n\t\t\t\t// Replay-only customers may never open the widget, so the host's\n\t\t\t\t// user state never gets polled by the widget's interaction\n\t\t\t\t// boundaries. Re-resolve here so a mid-session login that\n\t\t\t\t// happened before session start is visible server-side before\n\t\t\t\t// the first chunk lands. Fingerprint dedupe inside\n\t\t\t\t// identifyIfChanged makes this effectively free when nothing\n\t\t\t\t// changed.\n\t\t\t\ttry {\n\t\t\t\t\tctx.resolveUser?.()\n\t\t\t\t} catch (err) {\n\t\t\t\t\tctx.logger.warn('resolveUser threw at session start', err)\n\t\t\t\t}\n\t\t\t\tconst created = await createSession(apiUrl, ctx.clientId, sdkSessionId, anonymousId)\n\t\t\t\tif (!created) {\n\t\t\t\t\tctx.logger.warn('session create failed, replay disabled')\n\t\t\t\t\tstore.stopped = true\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tif (!created.accepted) {\n\t\t\t\t\tctx.logger.info(`session-replay declined: ${created.dropReason ?? 'unknown'}`)\n\t\t\t\t\tstore.stopped = true\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tif (!created.sessionReplayId) {\n\t\t\t\t\tctx.logger.error('server accepted but returned no sessionReplayId')\n\t\t\t\t\tstore.stopped = true\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tstore.sessionReplayId = created.sessionReplayId\n\t\t\t\tstore.recordingStartedAt = Date.now()\n\t\t\t\tstartRecording(store, ctx)\n\t\t\t}\n\n\t\t\tif (merged.startAfterMs > 0) {\n\t\t\t\tconst cancelOnExit = (): void => {\n\t\t\t\t\tstore.cancelled = true\n\t\t\t\t\tif (store.startTimer) {\n\t\t\t\t\t\tclearTimeout(store.startTimer)\n\t\t\t\t\t\tstore.startTimer = null\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\twindow.addEventListener('pagehide', cancelOnExit, { once: true })\n\t\t\t\twindow.addEventListener('beforeunload', cancelOnExit, { once: true })\n\t\t\t\tstore.startTimer = setTimeout(() => {\n\t\t\t\t\tvoid begin()\n\t\t\t\t}, merged.startAfterMs)\n\t\t\t} else {\n\t\t\t\tvoid begin()\n\t\t\t}\n\t\t},\n\t\tonFeedbackSubmit(ctx) {\n\t\t\tconst store = ctx.getStore<ReplayStore>()\n\t\t\tif (!store || store.cancelled || store.stopped) return undefined\n\t\t\tif (!store.sessionReplayId) return undefined\n\t\t\tconst offsetMs =\n\t\t\t\tstore.recordingStartedAt !== null\n\t\t\t\t\t? Math.max(0, Date.now() - store.recordingStartedAt)\n\t\t\t\t\t: 0\n\t\t\treturn { sessionReplayId: store.sessionReplayId, replayOffsetMs: offsetMs }\n\t\t},\n\t\tonDestroy(ctx) {\n\t\t\tconst store = ctx.getStore<ReplayStore>()\n\t\t\tif (!store) return\n\t\t\tstore.cancelled = true\n\t\t\tif (store.startTimer) {\n\t\t\t\tclearTimeout(store.startTimer)\n\t\t\t\tstore.startTimer = null\n\t\t\t}\n\t\t\tif (store.pageHideHandler) {\n\t\t\t\twindow.removeEventListener('pagehide', store.pageHideHandler)\n\t\t\t\tstore.pageHideHandler = null\n\t\t\t}\n\t\t\tif (store.shadowUpdateHandler) {\n\t\t\t\twindow.removeEventListener('usero:shadow-update', store.shadowUpdateHandler)\n\t\t\t\tstore.shadowUpdateHandler = null\n\t\t\t}\n\t\t\t// SPA route change / React unmount: send a finalise so the\n\t\t\t// server stamps endedAt. fetch+keepalive is fine here since we\n\t\t\t// aren't necessarily in a pagehide path.\n\t\t\tif (store.sessionReplayId && !store.stopped) {\n\t\t\t\tfinalise(store, ctx, { useBeacon: false })\n\t\t\t}\n\t\t\tstore.stopped = true\n\t\t\tstopRrweb(store)\n\t\t\tstore.pendingEvents.length = 0\n\t\t\tstore.pendingBytes = 0\n\t\t\tstore.record = null\n\t\t},\n\t}\n}\n\n// Returns the live session-replay handle for a given plugin context, or\n// null if the session was bot-dropped, sample-skipped, or not yet\n// created. Other plugins (e.g. user-test) can call this to attach the\n// replay FK + offset to their own server-side records.\nexport function getCurrentSession(ctx: PluginContext): CurrentSessionHandle | null {\n\tconst store = ctx.getStore<ReplayStore>()\n\tif (!store || store.cancelled || store.stopped || !store.sessionReplayId) return null\n\tconst offsetMs =\n\t\tstore.recordingStartedAt !== null\n\t\t\t? Math.max(0, Date.now() - store.recordingStartedAt)\n\t\t\t: 0\n\treturn { id: store.sessionReplayId, offsetMs }\n}\n\n// Internal helper exports for testing only. Not part of the public API.\nexport const __test__ = {\n\tuint8ToBase64,\n\tgzipBytes,\n\tmintSdkSessionId,\n\tuploadChunk,\n\tcreateSession,\n\tjoinUrl,\n\tscheduleChunkUpload,\n\tHARD_CHUNK_BYTE_CAP,\n\tSDK_SESSION_STORAGE_KEY,\n\tMAX_PENDING_UPLOADS,\n\tUPLOAD_DROP_WARN_INTERVAL_MS,\n\tDEFAULTS,\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@usero/sdk",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.2",
|
|
4
4
|
"description": "Usero SDK. Drop-in feedback widget (vanilla JS, React, or script tag) plus future plugins like session replay. Zero config, framework-free, tiny.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Will Smith <will@willsmithte.com>",
|