@himetrica/replaykit 0.1.0 → 0.2.0
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/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/replaykit.global.js +3 -3
- package/dist/replaykit.global.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/state.ts","../src/env.ts","../src/identity.ts","../src/match.ts","../src/buffer.ts","../src/detect.ts","../src/decide.ts","../src/transport.ts","../src/signal-queue.ts","../src/signals-dom.ts","../src/recorder.ts","../src/uploader.ts","../src/client.ts","../src/index.ts"],"sourcesContent":["// Per-session recording state, in sessionStorage so it follows the session across the\n// pages of a multi-page site (each page load is a new SDK instance).\n//\n// Keyed to the tracker session id: when the tracker rolls a new session (30 min idle,\n// new UTM...), the stored state belongs to the old one and is discarded.\n\nimport type { Env } from \"./env\";\nimport type { ReplayKitRecordingStats, ReplayKitTrigger } from \"./protocol\";\n\nexport type StopReason = \"quota\" | \"manual\" | \"limit\" | \"off\";\n\nexport interface SessionState {\n sid: string;\n /** Recording id, set once a trigger fired; absent while only buffering. */\n rid?: string;\n /** Next chunk sequence number to upload. */\n seq: number;\n trigger?: ReplayKitTrigger;\n /** Set when recording must not (re)start for the rest of the session. */\n stopped?: StopReason;\n stats?: ReplayKitRecordingStats;\n}\n\nconst KEY = \"hm_rk_state\";\n\nexport class StateStore {\n private cache: SessionState | null = null;\n\n constructor(private readonly env: Env) {}\n\n load(sid: string): SessionState {\n if (this.cache?.sid === sid) return this.cache;\n let s: SessionState | null = null;\n try {\n const raw = this.env.getSession(KEY);\n if (raw) s = JSON.parse(raw) as SessionState;\n } catch {\n s = null;\n }\n if (!s || s.sid !== sid || typeof s.seq !== \"number\") s = { sid, seq: 0 };\n this.cache = s;\n return s;\n }\n\n save(s: SessionState): void {\n this.cache = s;\n try {\n this.env.setSession(KEY, JSON.stringify(s));\n } catch {\n // In-memory copy still drives this page.\n }\n }\n}\n\nexport function emptyStats(now: number, entryUrl: string): ReplayKitRecordingStats {\n return { startedAt: now, endedAt: now, activeMs: 0, clicks: 0, rageClicks: 0, deadClicks: 0, errors: 0, pages: 1, entryUrl };\n}\n","// Browser globals behind one interface, like ErrorKit's env.ts.\n//\n// Network, storage, timers and the clock go through an Env, so the transport, the\n// uploader and the client run in plain Node under node:test with fakes. DOM wiring\n// (listeners, rrweb) stays in signals-dom.ts / recorder.ts and is not unit-tested.\n\nexport interface FetchResult {\n status: number;\n headers: { get(name: string): string | null };\n json(): Promise<unknown>;\n}\n\nexport interface Env {\n now(): number;\n uuid(): string;\n fetch?: (url: string, init: RequestInit) => Promise<FetchResult>;\n sendBeacon?: (url: string, body: string) => boolean;\n getLocal(key: string): string | null;\n setLocal(key: string, value: string | null): void;\n getSession(key: string): string | null;\n setSession(key: string, value: string | null): void;\n getCookie(name: string): string | null;\n /** Async key/value store that survives a page load (IndexedDB). Missing = nothing carried across pages. */\n store?: KvStore;\n /** gzip; undefined when CompressionStream is unavailable (recording is then skipped). */\n gzip?: (text: string) => Promise<Blob>;\n setTimer(fn: () => void, ms: number): unknown;\n clearTimer(handle: unknown): void;\n}\n\nexport interface KvStore {\n get<T>(key: string): Promise<T | undefined>;\n set(key: string, value: unknown): Promise<void>;\n del(key: string): Promise<void>;\n}\n\nexport const isBrowser = typeof window !== \"undefined\" && typeof document !== \"undefined\";\n\nexport function uuidV4(): string {\n const c = (globalThis as { crypto?: Crypto }).crypto;\n if (c && typeof c.randomUUID === \"function\") {\n try {\n return c.randomUUID();\n } catch {\n // randomUUID throws on insecure origins in some browsers; fall through.\n }\n }\n return \"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx\".replace(/[xy]/g, (ch) => {\n const r = (Math.random() * 16) | 0;\n return (ch === \"x\" ? r : (r & 0x3) | 0x8).toString(16);\n });\n}\n\n// Storage throws in Safari private mode, sandboxed iframes and under storage policies.\nfunction safeGet(storage: () => Storage, key: string): string | null {\n try {\n return storage().getItem(key);\n } catch {\n return null;\n }\n}\n\nfunction safeSet(storage: () => Storage, key: string, value: string | null): void {\n try {\n if (value === null) storage().removeItem(key);\n else storage().setItem(key, value);\n } catch {\n // Unavailable or full: in-memory state still works for this page.\n }\n}\n\n/** One IndexedDB object store used as a key/value map. Every failure degrades to \"nothing stored\". */\nfunction indexedDbStore(): KvStore | undefined {\n if (typeof indexedDB === \"undefined\") return undefined;\n let dbp: Promise<IDBDatabase> | null = null;\n const open = () =>\n (dbp ??= new Promise<IDBDatabase>((resolve, reject) => {\n const req = indexedDB.open(\"himetrica-replaykit\", 1);\n req.onupgradeneeded = () => req.result.createObjectStore(\"kv\");\n req.onsuccess = () => resolve(req.result);\n req.onerror = () => reject(req.error);\n }));\n const run = <T>(mode: IDBTransactionMode, fn: (s: IDBObjectStore) => IDBRequest | void): Promise<T | undefined> =>\n open()\n .then(\n (db) =>\n new Promise<T | undefined>((resolve) => {\n const tx = db.transaction(\"kv\", mode);\n const req = fn(tx.objectStore(\"kv\"));\n tx.oncomplete = () => resolve(req ? (req.result as T) : undefined);\n tx.onerror = tx.onabort = () => resolve(undefined);\n }),\n )\n .catch(() => undefined);\n return {\n get: <T>(key: string) => run<T>(\"readonly\", (s) => s.get(key)),\n set: (key, value) => run<void>(\"readwrite\", (s) => void s.put(value, key)).then(() => undefined),\n del: (key) => run<void>(\"readwrite\", (s) => void s.delete(key)).then(() => undefined),\n };\n}\n\nfunction gzipFn(): Env[\"gzip\"] {\n if (typeof CompressionStream === \"undefined\" || typeof Blob === \"undefined\" || typeof Response === \"undefined\") return undefined;\n return (text) => new Response(new Blob([text]).stream().pipeThrough(new CompressionStream(\"gzip\"))).blob();\n}\n\nexport function browserEnv(): Env {\n const originalFetch = typeof fetch === \"function\" ? fetch.bind(globalThis) : undefined;\n const nav = typeof navigator !== \"undefined\" ? navigator : undefined;\n return {\n now: () => Date.now(),\n uuid: () => uuidV4(),\n fetch: originalFetch as Env[\"fetch\"],\n sendBeacon:\n nav && typeof nav.sendBeacon === \"function\"\n ? (url, body) => {\n try {\n return nav.sendBeacon(url, new Blob([body], { type: \"text/plain\" }));\n } catch {\n return false;\n }\n }\n : undefined,\n getLocal: (key) => safeGet(() => localStorage, key),\n setLocal: (key, value) => safeSet(() => localStorage, key, value),\n getSession: (key) => safeGet(() => sessionStorage, key),\n setSession: (key, value) => safeSet(() => sessionStorage, key, value),\n getCookie: (name) => {\n try {\n const match = document.cookie.match(new RegExp(\"(?:^|; )\" + name.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\") + \"=([^;]*)\"));\n return match ? decodeURIComponent(match[1]) : null;\n } catch {\n return null;\n }\n },\n store: indexedDbStore(),\n gzip: gzipFn(),\n setTimer: (fn, ms) => setTimeout(fn, ms),\n clearTimer: (h) => clearTimeout(h as ReturnType<typeof setTimeout>),\n };\n}\n","// Identity bridge with the analytics tracker, strictly READ-ONLY (same approach as\n// ErrorKit's browser/identity.ts).\n//\n// The tracker (tracker-js / tracker.js) stores the ids in:\n// visitor cookie hm_vid (cookieDomain mode) or localStorage hm_visitor_id\n// session cookie hm_sid (cookieDomain mode) or sessionStorage hm_session_id\n// We read those keys and never write them. window.himetrica.getSessionId() is NOT\n// called: in the tracker it refreshes the session timestamp.\n//\n// A recording or a signal is only worth something tied to the tracker's visitor and\n// session, so without the tracker on the page ReplayKit stays idle.\n\nimport type { Env } from \"./env\";\n\nexport interface Identity {\n visitorId?: string;\n sessionId?: string;\n}\n\nexport function readIdentity(env: Env): Identity {\n const visitorId = env.getCookie(\"hm_vid\") || env.getLocal(\"hm_visitor_id\") || undefined;\n const sessionId = env.getCookie(\"hm_sid\") || env.getSession(\"hm_session_id\") || undefined;\n return { visitorId, sessionId };\n}\n","// Pure helpers the recording decision is made of: path globs and the session sample.\n\n/**\n * Path glob, as the project config writes them: `*` matches any run of characters\n * (slashes included), everything else is literal. A pattern without `*` is a prefix\n * on a segment boundary: `/checkout` matches `/checkout` and `/checkout/step-2`, not\n * `/checkouts`. Matched against the pathname only, case-insensitive.\n */\nexport function matchPath(pattern: string, pathname: string): boolean {\n const p = pattern.trim().toLowerCase();\n if (!p) return false;\n const path = (pathname || \"/\").toLowerCase();\n if (!p.includes(\"*\")) {\n const base = p.endsWith(\"/\") && p.length > 1 ? p.slice(0, -1) : p;\n return path === base || path.startsWith(base.endsWith(\"/\") ? base : base + \"/\");\n }\n const re = new RegExp(\"^\" + p.split(\"*\").map(escapeRegExp).join(\".*\") + \"$\");\n return re.test(path);\n}\n\nexport function matchAny(patterns: readonly string[], pathname: string): boolean {\n for (const p of patterns) if (matchPath(p, pathname)) return true;\n return false;\n}\n\nfunction escapeRegExp(s: string): string {\n return s.replace(/[.+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n}\n\n/**\n * Deterministic [0, 1) from the session id (cyrb53). Every page of a session gets the\n * same draw, so a sampled session is recorded across all its pages, and the server\n * could recompute it.\n */\nexport function sampleDraw(sessionId: string): number {\n let h1 = 0xdeadbeef;\n let h2 = 0x41c6ce57;\n for (let i = 0; i < sessionId.length; i++) {\n const ch = sessionId.charCodeAt(i);\n h1 = Math.imul(h1 ^ ch, 2654435761);\n h2 = Math.imul(h2 ^ ch, 1597334677);\n }\n h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ Math.imul(h2 ^ (h2 >>> 13), 3266489909);\n h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ Math.imul(h1 ^ (h1 >>> 13), 3266489909);\n const n = 4294967296 * (2097151 & h2) + (h1 >>> 0);\n return n / 9007199254740992;\n}\n\nexport function inSample(sessionId: string, rate: number): boolean {\n if (!(rate > 0)) return false;\n if (rate >= 1) return true;\n return sampleDraw(sessionId) < rate;\n}\n\n/** URL without query and hash: queries carry tokens and emails, never sent. */\nexport function stripUrl(href: string): string {\n const cut = href.search(/[?#]/);\n return cut === -1 ? href : href.slice(0, cut);\n}\n","// The pre-trigger ring buffer.\n//\n// rrweb can only replay from a full snapshot: a Meta event (type 4) followed by a\n// FullSnapshot (type 2). The recorder takes one every CHECKOUT_MS, so the buffer can\n// always be cut at the last checkout that is older than the window and still start\n// with a replayable page. The cut keeps BUFFER_WINDOW_MS..BUFFER_WINDOW_MS+CHECKOUT_MS\n// of history before the trigger.\n\nexport interface RrEvent {\n type: number;\n timestamp: number;\n data?: unknown;\n}\n\nexport const EVENT_FULL_SNAPSHOT = 2;\nexport const EVENT_META = 4;\nexport const EVENT_CUSTOM = 5;\n/** rrweb custom event the SDK adds on SPA URL changes (rrweb only emits Meta on page loads/checkouts). */\nexport const PAGE_TAG = \"replaykit:page\";\n\n/**\n * Pages shown in a run of events, in order: Meta hrefs (page loads, and every\n * checkout repeats the current one) plus PAGE_TAG custom events. Query/fragment\n * stripped, back-to-back repeats folded, `prev` = last page already reported for\n * this recording (so a checkout's repeated Meta is not a new page). Capped at `max`.\n */\nexport function pagesOf(events: RrEvent[], prev: string, max: number): Array<{ url: string; ts: number }> {\n const out: Array<{ url: string; ts: number }> = [];\n let last = prev;\n for (const e of events) {\n let href: unknown;\n if (e.type === EVENT_META) href = (e.data as { href?: unknown } | undefined)?.href;\n else if (e.type === EVENT_CUSTOM) {\n const d = e.data as { tag?: unknown; payload?: { href?: unknown } } | undefined;\n if (d?.tag === PAGE_TAG) href = d.payload?.href;\n }\n if (typeof href !== \"string\" || !href) continue;\n const cut = href.search(/[?#]/);\n const url = (cut === -1 ? href : href.slice(0, cut)).slice(0, 500);\n if (url === last) continue;\n last = url;\n out.push({ url, ts: e.timestamp });\n if (out.length >= max) break;\n }\n return out;\n}\n\nexport const BUFFER_WINDOW_MS = 30_000;\nexport const CHECKOUT_MS = 30_000;\n\n/** Index of the Meta event that opens the snapshot the event list may start from. */\nfunction snapshotStart(events: RrEvent[], i: number): number {\n // rrweb emits Meta right before FullSnapshot; keep the Meta so the player knows the viewport.\n return i > 0 && events[i - 1].type === EVENT_META ? i - 1 : i;\n}\n\n/**\n * Drops everything before the most recent full snapshot whose timestamp is at or\n * before `now - windowMs`. If no snapshot is that old, nothing is dropped (the buffer\n * is still shorter than the window). A list that does not start with a snapshot (the\n * head was lost) is cut at its first snapshot, since nothing before it can replay.\n */\nexport function trimBuffer(events: RrEvent[], now: number, windowMs = BUFFER_WINDOW_MS): RrEvent[] {\n const cutoff = now - windowMs;\n let keepFrom = -1;\n let firstSnapshot = -1;\n for (let i = 0; i < events.length; i++) {\n if (events[i].type !== EVENT_FULL_SNAPSHOT) continue;\n if (firstSnapshot === -1) firstSnapshot = i;\n if (events[i].timestamp <= cutoff) keepFrom = i;\n else break;\n }\n if (keepFrom === -1) keepFrom = firstSnapshot;\n if (keepFrom === -1) return [];\n const start = snapshotStart(events, keepFrom);\n return start === 0 ? events : events.slice(start);\n}\n\n/** Rough serialized size, to cut chunks without stringifying on every event. */\nexport function approxBytes(e: RrEvent): number {\n // Mutations and snapshots dominate; JSON.stringify per event is acceptable: events\n // are stringified once here and the chunk is re-stringified once at flush.\n try {\n return JSON.stringify(e).length;\n } catch {\n return 0;\n }\n}\n","// Friction detectors, DOM-free so node:test can drive them with plain numbers.\n// signals-dom.ts feeds them real clicks and reports what they decide.\n\nexport interface ClickPoint {\n x: number;\n y: number;\n ts: number;\n /** Stable key of the clicked element (its selector), so a burst on one button counts even if the mouse drifts. */\n target: string;\n}\n\n/** 3+ clicks within RAGE_WINDOW_MS, on the same element or within RAGE_RADIUS_PX of the first. */\nexport const RAGE_MIN_CLICKS = 3;\nexport const RAGE_WINDOW_MS = 1000;\nexport const RAGE_RADIUS_PX = 30;\n\nexport interface RageBurst {\n first: ClickPoint;\n count: number;\n lastTs: number;\n}\n\n/**\n * Feeds clicks in order. `onClick` returns \"trigger\" on the click that turns a burst\n * into a rage click (the 3rd), so recording starts right away; the finished burst\n * (with its final count) comes out of `onClick` of the next unrelated click or of\n * `flush(now)` once the window has passed.\n */\nexport class RageDetector {\n private burst: RageBurst | null = null;\n\n onClick(c: ClickPoint): { trigger: boolean; finished: RageBurst | null } {\n const b = this.burst;\n if (b && c.ts - b.lastTs <= RAGE_WINDOW_MS && near(b.first, c)) {\n b.count++;\n b.lastTs = c.ts;\n return { trigger: b.count === RAGE_MIN_CLICKS, finished: null };\n }\n const finished = b && b.count >= RAGE_MIN_CLICKS ? b : null;\n this.burst = { first: c, count: 1, lastTs: c.ts };\n return { trigger: false, finished };\n }\n\n /** The burst that ended (quiet for the window) before `now`, if it was a rage click. */\n flush(now: number): RageBurst | null {\n const b = this.burst;\n if (!b || now - b.lastTs <= RAGE_WINDOW_MS) return null;\n this.burst = null;\n return b.count >= RAGE_MIN_CLICKS ? b : null;\n }\n\n /** Clicks of the current burst so far (a rage burst is not also a dead click). */\n currentCount(): number {\n return this.burst?.count ?? 0;\n }\n}\n\nfunction near(a: ClickPoint, b: ClickPoint): boolean {\n if (a.target && a.target === b.target) return true;\n return Math.abs(a.x - b.x) <= RAGE_RADIUS_PX && Math.abs(a.y - b.y) <= RAGE_RADIUS_PX;\n}\n\n/** How long after a click something must happen for the click not to be \"dead\". */\nexport const DEAD_CLICK_WAIT_MS = 500;\n\nexport interface ClickTargetFacts {\n /** Lowercase tag name of the clicked element. */\n tag: string;\n /** The element or an ancestor is a link/button/[role=button…]/[onclick]/summary. */\n insideInteractive: boolean;\n /** Text fields, selects, labels, contenteditable: a click there focuses, nothing else to expect. */\n isFormField: boolean;\n /** Computed cursor is \"pointer\": the page tells the user it is clickable. */\n pointerCursor: boolean;\n /** Text is selected after the click (the user was selecting, not clicking). */\n hasSelection: boolean;\n}\n\n/**\n * A click can only be dead when the element promised something: it is (inside) an\n * interactive element or shows a pointer cursor. A click on plain text or on a\n * background is not friction, whatever happens after it.\n */\nexport function isDeadClickCandidate(f: ClickTargetFacts): boolean {\n if (f.isFormField || f.hasSelection) return false;\n if (f.tag === \"html\" || f.tag === \"body\") return false;\n return f.insideInteractive || f.pointerCursor;\n}\n\nexport interface AfterClickReaction {\n /** DOM mutations observed in the wait window. */\n mutations: number;\n urlChanged: boolean;\n scrolled: boolean;\n focusChanged: boolean;\n /** The page is unloading (a navigation or a download started). */\n unloading: boolean;\n}\n\n/** Nothing reacted within DEAD_CLICK_WAIT_MS: the click was dead. */\nexport function isDeadClick(r: AfterClickReaction): boolean {\n return r.mutations === 0 && !r.urlChanged && !r.scrolled && !r.focusChanged && !r.unloading;\n}\n\n/** Active time: input gaps up to ACTIVE_GAP_MS count, longer gaps are idle. */\nexport const ACTIVE_GAP_MS = 5000;\n\nexport function activeIncrement(lastInputTs: number, ts: number): number {\n if (!lastInputTs) return 0;\n const gap = ts - lastInputTs;\n return gap > 0 && gap <= ACTIVE_GAP_MS ? gap : 0;\n}\n","// What this page does about recording, from what /init said and the session state.\n// Pure, so the whole decision table is unit-tested.\n\nimport type { ReplayKitTrigger } from \"./protocol\";\nimport type { SessionState } from \"./state\";\n\nexport type PageMode =\n /** Nothing is recorded on this page (signals may still flow). */\n | { kind: \"off\" }\n /** requireConsent and the banner has not said yes yet. */\n | { kind: \"wait_consent\" }\n /** A recording of this session is already running: continue it. */\n | { kind: \"continue\" }\n /** Start a recording now, for this reason. */\n | { kind: \"start\"; trigger: ReplayKitTrigger }\n /** Keep the pre-trigger buffer and wait for a trigger. */\n | { kind: \"buffer\" };\n\nexport interface DecideInput {\n /** /init answered record: true. */\n record: boolean;\n /** CompressionStream exists (chunks are gzip'd in the browser). */\n canGzip: boolean;\n state: SessionState;\n /** Path is in excludePaths. */\n excluded: boolean;\n requireConsent: boolean;\n /** Consent stored by ReplayKit.consent(): true, false, or never given. */\n consent: boolean | null;\n forceTrigger: ReplayKitTrigger | null;\n sampled: boolean;\n /** Path matched checkoutPaths and \"checkout\" is an enabled trigger. */\n checkout: boolean;\n}\n\nexport function decidePage(i: DecideInput): PageMode {\n // An explicit ReplayKit.consent(false) wins even when the project does not require consent.\n if (!i.record || !i.canGzip || i.state.stopped || i.excluded || i.consent === false) return { kind: \"off\" };\n if (i.requireConsent && i.consent !== true) return { kind: \"wait_consent\" };\n if (i.state.rid) return { kind: \"continue\" };\n if (i.forceTrigger) return { kind: \"start\", trigger: i.forceTrigger };\n if (i.checkout) return { kind: \"start\", trigger: \"checkout\" };\n if (i.sampled) return { kind: \"start\", trigger: \"sample\" };\n return { kind: \"buffer\" };\n}\n","// Calls to the ingest (POST {apiUrl}/api/t/r/<op>?apiKey=...), text/plain JSON so\n// they are \"simple\" CORS requests with no preflight, plus the PUT of a chunk to R2.\n//\n// Every response is reduced to an outcome the client acts on (see the contract in\n// packages/shared/src/replaykit.ts):\n// ok 2xx\n// drop 400/413: this body is unusable, forget it\n// stop 401: bad key / origin not allowed, ReplayKit stops for this page\n// off 403: rollout, plan or this op is off for the project\n// quota 429: no more recordings this session (signals continue)\n// retry 5xx, network error, or no fetch at all\n\nimport type { Env } from \"./env\";\nimport type {\n ReplayKitChunkRequest,\n ReplayKitChunkResponse,\n ReplayKitInitRequest,\n ReplayKitInitResponse,\n ReplayKitSignalsRequest,\n} from \"./protocol\";\n\nexport type Outcome = \"ok\" | \"drop\" | \"stop\" | \"off\" | \"quota\" | \"retry\";\n\nexport function outcomeOf(status: number): Outcome {\n if (status >= 200 && status < 300) return \"ok\";\n if (status === 401) return \"stop\";\n if (status === 403) return \"off\";\n if (status === 429) return \"quota\";\n if (status === 400 || status === 413 || status === 404 || status === 422) return \"drop\";\n return \"retry\";\n}\n\nexport interface Result<T> {\n outcome: Outcome;\n data?: T;\n}\n\nexport class Api {\n constructor(\n private readonly env: Env,\n private readonly apiUrl: string,\n private readonly apiKey: string,\n ) {}\n\n url(op: \"init\" | \"chunk\" | \"signals\"): string {\n return `${this.apiUrl}/api/t/r/${op}?apiKey=${encodeURIComponent(this.apiKey)}`;\n }\n\n init(body: ReplayKitInitRequest): Promise<Result<ReplayKitInitResponse>> {\n return this.post<ReplayKitInitResponse>(\"init\", body, true);\n }\n\n chunk(body: ReplayKitChunkRequest): Promise<Result<ReplayKitChunkResponse>> {\n return this.post<ReplayKitChunkResponse>(\"chunk\", body, true);\n }\n\n signals(body: ReplayKitSignalsRequest): Promise<Result<void>> {\n return this.post<void>(\"signals\", body, false);\n }\n\n /** On page hide: fire-and-forget. False when the beacon could not be queued. */\n beaconSignals(body: ReplayKitSignalsRequest): boolean {\n return !!this.env.sendBeacon?.(this.url(\"signals\"), JSON.stringify(body));\n }\n\n /** PUT the gzip'd chunk to the presigned R2 URL. R2 answers 200; 403 = URL expired or bad signature. */\n async put(url: string, blob: Blob): Promise<Outcome> {\n if (!this.env.fetch) return \"retry\";\n try {\n const res = await this.env.fetch(url, { method: \"PUT\", body: blob, headers: { \"Content-Type\": \"application/gzip\" } });\n if (res.status >= 200 && res.status < 300) return \"ok\";\n // An expired/rejected signature is fixed by asking for a new URL, not by dropping the chunk.\n return res.status === 400 || res.status === 413 ? \"drop\" : \"retry\";\n } catch {\n return \"retry\";\n }\n }\n\n private async post<T>(op: \"init\" | \"chunk\" | \"signals\", body: unknown, wantJson: boolean): Promise<Result<T>> {\n if (!this.env.fetch) return { outcome: \"retry\" };\n try {\n const res = await this.env.fetch(this.url(op), {\n method: \"POST\",\n body: JSON.stringify(body),\n headers: { \"Content-Type\": \"text/plain\" },\n keepalive: false,\n credentials: \"omit\",\n });\n const outcome = outcomeOf(res.status);\n if (outcome !== \"ok\" || !wantJson) return { outcome };\n try {\n return { outcome, data: (await res.json()) as T };\n } catch {\n return { outcome: \"retry\" };\n }\n } catch {\n return { outcome: \"retry\" };\n }\n }\n}\n\nexport const BASE_BACKOFF_MS = 2_000;\nexport const MAX_BACKOFF_MS = 60_000;\n\nexport function backoff(failures: number): number {\n return Math.min(MAX_BACKOFF_MS, BASE_BACKOFF_MS * 2 ** Math.max(0, failures - 1));\n}\n","// Signals out: batched POST /api/t/r/signals every FLUSH_MS (or at a full batch),\n// sendBeacon for whatever is left when the page hides.\n//\n// Signals are cheap and never count against the quota, but a page in a rage loop\n// should not turn into a request loop: at most MAX_PER_PAGE signals per page load,\n// a full batch per request, and identical signals within DEDUPE_MS folded.\n\nimport type { Env } from \"./env\";\nimport { REPLAYKIT_MAX_SIGNALS_PER_BATCH, type ReplayKitSignal } from \"./protocol\";\nimport { backoff, type Api } from \"./transport\";\n\nexport const SIGNAL_FLUSH_MS = 5_000;\nexport const MAX_PER_PAGE = 200;\nconst MAX_QUEUED = 500;\nconst DEDUPE_MS = 2_000;\n\nexport class SignalQueue {\n private queue: ReplayKitSignal[] = [];\n private timer: unknown = null;\n private sentThisPage = 0;\n private failures = 0;\n private busy = false;\n private last = new Map<string, number>();\n /** Ingest said no (403 off, 401 bad key): nothing more this page. */\n disabled = false;\n /** Init has not answered yet: keep collecting, send nothing. */\n held = true;\n\n constructor(\n private readonly env: Env,\n private readonly api: Api,\n private readonly ids: () => { sid: string; vid: string; rid?: string },\n ) {}\n\n add(s: ReplayKitSignal): void {\n if (this.disabled || this.sentThisPage + this.queue.length >= MAX_PER_PAGE) return;\n const key = `${s.type}|${s.url}|${s.selector || \"\"}|${s.detail || \"\"}`;\n const prev = this.last.get(key);\n if (prev !== undefined && s.ts - prev < DEDUPE_MS) return;\n this.last.set(key, s.ts);\n this.queue.push(s);\n if (this.queue.length > MAX_QUEUED) this.queue.shift();\n if (this.held) return;\n if (this.queue.length >= REPLAYKIT_MAX_SIGNALS_PER_BATCH) this.schedule(0);\n else if (!this.timer) this.schedule(SIGNAL_FLUSH_MS);\n }\n\n /** Init answered: start sending (signals on) or drop everything (off). */\n release(enabled: boolean): void {\n this.held = false;\n if (!enabled) {\n this.disable();\n return;\n }\n if (this.queue.length) this.schedule(0);\n }\n\n disable(): void {\n this.disabled = true;\n this.queue = [];\n if (this.timer) this.env.clearTimer(this.timer);\n this.timer = null;\n }\n\n private schedule(ms: number): void {\n if (this.timer) this.env.clearTimer(this.timer);\n this.timer = this.env.setTimer(() => {\n this.timer = null;\n void this.flush();\n }, ms);\n }\n\n async flush(): Promise<void> {\n if (this.busy || this.held || this.disabled) return;\n this.busy = true;\n try {\n while (this.queue.length && !this.disabled) {\n const batch = this.queue.slice(0, REPLAYKIT_MAX_SIGNALS_PER_BATCH);\n const r = await this.api.signals({ ...this.ids(), signals: batch });\n if (r.outcome === \"ok\" || r.outcome === \"drop\" || r.outcome === \"quota\") {\n // 429 on signals would be a server bug (they never count): drop the batch, don't loop.\n this.queue.splice(0, batch.length);\n this.sentThisPage += batch.length;\n this.failures = 0;\n continue;\n }\n if (r.outcome === \"off\" || r.outcome === \"stop\") {\n this.disable();\n return;\n }\n this.failures++;\n this.schedule(backoff(this.failures));\n return;\n }\n } finally {\n this.busy = false;\n }\n }\n\n /** Page hide: beacon what is left (no status to read; a lost beacon loses a few signals). */\n beacon(): void {\n if (this.held || this.disabled || !this.queue.length) return;\n while (this.queue.length) {\n const batch = this.queue.slice(0, REPLAYKIT_MAX_SIGNALS_PER_BATCH);\n if (!this.api.beaconSignals({ ...this.ids(), signals: batch })) return;\n this.queue.splice(0, batch.length);\n this.sentThisPage += batch.length;\n }\n }\n\n get size(): number {\n return this.queue.length;\n }\n}\n","// DOM side of the friction signals: listeners that turn real clicks, errors,\n// navigations and forms into ReplayKitSignal values. The decisions themselves\n// (what is a rage click, what is a dead click) live in detect.ts.\n//\n// Passive capture-phase listeners only; nothing is patched. The MutationObserver a\n// dead-click check needs is attached for DEAD_CLICK_WAIT_MS after a candidate click\n// and disconnected right after, so a quiet page costs nothing.\n//\n// Never sent: what was typed. A clicked element's text is only read when it is not a\n// field and not masked (.hm-mask, [data-hm-mask], or maskAllText).\n\nimport {\n DEAD_CLICK_WAIT_MS,\n isDeadClick,\n isDeadClickCandidate,\n RAGE_WINDOW_MS,\n RageDetector,\n type ClickTargetFacts,\n} from \"./detect\";\nimport { stripUrl } from \"./match\";\nimport type { ReplayKitSignal } from \"./protocol\";\n\nexport interface SignalHooks {\n now(): number;\n /** Text of clicked elements is withheld (config maskAllText). */\n maskAllText(): boolean;\n signal(s: ReplayKitSignal): void;\n /** A trigger fired: the 3rd click of a rage burst, or a JS error. */\n trigger(t: \"rage_click\" | \"error\"): void;\n /** Any click / key / scroll, for active time and click counts. */\n input(kind: \"click\" | \"key\" | \"scroll\", ts: number): void;\n /** The URL changed without a page load (SPA navigation). */\n navigated(url: string): void;\n}\n\nconst INTERACTIVE = 'a[href],button,summary,[role=\"button\"],[role=\"link\"],[role=\"tab\"],[role=\"menuitem\"],[role=\"checkbox\"],[role=\"switch\"],[role=\"option\"],[onclick]';\nconst FIELD = \"input,textarea,select,label,option,[contenteditable],[contenteditable=true]\";\nconst MASKED = \".hm-mask,[data-hm-mask],[data-hm-block],.hm-block\";\nconst MAX_TEXT = 80;\nconst MAX_DETAIL = 200;\n\n/** Short CSS path: up to 3 levels, stops at an id. Hashed class names (css-1x2y3z) are skipped. */\nexport function selectorOf(el: Element | null): string {\n const parts: string[] = [];\n let cur: Element | null = el;\n for (let depth = 0; cur && depth < 3 && cur.nodeType === 1; depth++) {\n const tag = cur.tagName.toLowerCase();\n if (tag === \"html\" || tag === \"body\") break;\n if (cur.id && /^[A-Za-z][\\w-]{0,40}$/.test(cur.id) && !/\\d{3,}/.test(cur.id)) {\n parts.unshift(`${tag}#${cur.id}`);\n break;\n }\n const classes = (typeof cur.className === \"string\" ? cur.className : \"\")\n .split(/\\s+/)\n .filter((c) => c && c.length < 30 && !/\\d{3,}|^css-|^sc-|__/.test(c))\n .slice(0, 2);\n parts.unshift(tag + classes.map((c) => \".\" + c).join(\"\"));\n cur = cur.parentElement;\n }\n return parts.join(\" > \");\n}\n\nfunction visibleText(el: Element, maskAll: boolean): string | undefined {\n if (maskAll || el.closest(MASKED) || el.closest(FIELD)) return undefined;\n const t = ((el as HTMLElement).innerText ?? el.textContent ?? \"\").replace(/\\s+/g, \" \").trim();\n if (!t) return (el.getAttribute(\"aria-label\") || el.getAttribute(\"title\") || undefined)?.slice(0, MAX_TEXT);\n return t.slice(0, MAX_TEXT);\n}\n\nfunction targetFacts(el: Element): ClickTargetFacts {\n let pointer = false;\n try {\n pointer = getComputedStyle(el).cursor === \"pointer\";\n } catch {\n pointer = false;\n }\n const sel = typeof getSelection === \"function\" ? getSelection() : null;\n return {\n tag: el.tagName.toLowerCase(),\n insideInteractive: !!el.closest(INTERACTIVE),\n isFormField: !!el.closest(FIELD),\n pointerCursor: pointer,\n hasSelection: !!sel && !sel.isCollapsed && sel.toString().trim().length > 0,\n };\n}\n\n/** The element a click \"means\": the nearest interactive ancestor, else the target itself. */\nfunction clickedElement(target: EventTarget | null): Element | null {\n const el = target instanceof Element ? target : target instanceof Node ? target.parentElement : null;\n if (!el) return null;\n return el.closest(INTERACTIVE) || el;\n}\n\nexport class SignalCollector {\n private rage = new RageDetector();\n private rageEl: Element | null = null;\n private rageTimer: ReturnType<typeof setTimeout> | null = null;\n private lastHref = \"\";\n private hrefTimer: ReturnType<typeof setInterval> | null = null;\n private unloading = false;\n private lastScrollInput = 0;\n private forms = new Map<HTMLFormElement, Set<string>>();\n private errorsSeen = new Set<string>();\n private off: Array<() => void> = [];\n\n constructor(private readonly h: SignalHooks) {}\n\n start(): void {\n this.lastHref = location.href;\n const on = <K extends keyof WindowEventMap>(type: K, fn: (e: WindowEventMap[K]) => void, target: Window | Document = window) => {\n const opts = { capture: true, passive: true };\n target.addEventListener(type, fn as EventListener, opts);\n this.off.push(() => target.removeEventListener(type, fn as EventListener, opts));\n };\n on(\"click\", (e) => this.onClick(e));\n on(\"keydown\", () => this.h.input(\"key\", this.h.now()));\n on(\"scroll\", () => {\n const now = this.h.now();\n if (now - this.lastScrollInput > 250) {\n this.lastScrollInput = now;\n this.h.input(\"scroll\", now);\n }\n });\n on(\"error\", (e) => this.onError(e.message || (e.error as Error | undefined)?.message || \"\"));\n on(\"unhandledrejection\", (e) => {\n const r = e.reason as { message?: string } | string | undefined;\n this.onError(typeof r === \"string\" ? r : r?.message || \"Unhandled promise rejection\");\n });\n on(\"popstate\", () => this.backNav());\n on(\"input\", (e) => this.onField(e.target));\n on(\"change\", (e) => this.onField(e.target));\n on(\"submit\", (e) => {\n if (e.target instanceof HTMLFormElement) this.forms.delete(e.target);\n });\n on(\"pageshow\", () => {\n this.unloading = false;\n });\n // SPA navigations: no patching of history, a cheap href poll instead.\n this.hrefTimer = setInterval(() => this.checkHref(), 1000);\n // Arrived here with the back/forward button.\n try {\n const nav = performance.getEntriesByType(\"navigation\")[0] as PerformanceNavigationTiming | undefined;\n if (nav?.type === \"back_forward\") this.emit({ type: \"back_nav\", detail: stripUrl(location.href) });\n } catch {\n // Navigation Timing unavailable.\n }\n }\n\n stop(): void {\n for (const f of this.off) f();\n this.off = [];\n if (this.hrefTimer) clearInterval(this.hrefTimer);\n if (this.rageTimer) clearTimeout(this.rageTimer);\n this.hrefTimer = this.rageTimer = null;\n }\n\n /** Called by the client on pagehide, before it beacons the queue: report what the page leaves behind. */\n pageHide(): void {\n this.unloading = true;\n this.flushForms();\n this.flushRage(true);\n }\n\n private emit(s: Omit<ReplayKitSignal, \"ts\" | \"url\"> & { ts?: number }): void {\n this.h.signal({ ts: s.ts ?? this.h.now(), url: stripUrl(location.href), ...s } as ReplayKitSignal);\n }\n\n private checkHref(): void {\n const href = location.href;\n if (href === this.lastHref) return;\n const prev = this.lastHref;\n this.lastHref = href;\n if (stripUrl(href) !== stripUrl(prev)) this.h.navigated(stripUrl(href));\n }\n\n private onClick(e: MouseEvent): void {\n const ts = this.h.now();\n this.h.input(\"click\", ts);\n const el = clickedElement(e.target);\n if (!el) return;\n const selector = selectorOf(el);\n const r = this.rage.onClick({ x: e.clientX, y: e.clientY, ts, target: selector });\n if (r.finished) this.reportRage(r.finished.count, r.finished.first.ts);\n if (r.trigger) this.h.trigger(\"rage_click\");\n this.rageEl = this.rage.currentCount() === 1 ? el : this.rageEl;\n if (this.rageTimer) clearTimeout(this.rageTimer);\n this.rageTimer = setTimeout(() => this.flushRage(false), RAGE_WINDOW_MS + 50);\n if (this.rage.currentCount() === 1) this.watchDeadClick(el, selector, ts);\n }\n\n private flushRage(force: boolean): void {\n const b = this.rage.flush(force ? Number.MAX_SAFE_INTEGER : this.h.now());\n if (b) this.reportRage(b.count, b.first.ts);\n }\n\n private reportRage(count: number, ts: number): void {\n const el = this.rageEl;\n this.emit({ type: \"rage_click\", ts, count, selector: el ? selectorOf(el) : undefined, text: el ? visibleText(el, this.h.maskAllText()) : undefined });\n }\n\n private watchDeadClick(el: Element, selector: string, ts: number): void {\n if (!isDeadClickCandidate(targetFacts(el))) return;\n const hrefBefore = location.href;\n const scrollBefore = `${scrollX},${scrollY}`;\n const focusBefore = document.activeElement;\n let mutations = 0;\n let mo: MutationObserver | null = null;\n try {\n mo = new MutationObserver((list) => {\n mutations += list.length;\n });\n mo.observe(document.documentElement, { subtree: true, childList: true, attributes: true, characterData: true });\n } catch {\n return;\n }\n setTimeout(() => {\n mo?.disconnect();\n // Part of a rage burst: reported as a rage click, not also as a dead one.\n if (this.rage.currentCount() > 1) return;\n const dead = isDeadClick({\n mutations,\n urlChanged: location.href !== hrefBefore,\n scrolled: `${scrollX},${scrollY}` !== scrollBefore,\n focusChanged: document.activeElement !== focusBefore && document.activeElement !== el,\n unloading: this.unloading,\n });\n if (dead) this.emit({ type: \"dead_click\", ts, selector, text: visibleText(el, this.h.maskAllText()) });\n }, DEAD_CLICK_WAIT_MS);\n }\n\n private onError(message: string): void {\n const detail = (message || \"Error\").slice(0, MAX_DETAIL);\n if (this.errorsSeen.has(detail)) return;\n this.errorsSeen.add(detail);\n this.emit({ type: \"error\", detail });\n this.h.trigger(\"error\");\n }\n\n private backNav(): void {\n this.emit({ type: \"back_nav\", detail: stripUrl(location.href) });\n this.checkHref();\n }\n\n private onField(target: EventTarget | null): void {\n if (!(target instanceof Element)) return;\n const form = target.closest(\"form\");\n if (!form) return;\n const key = target.getAttribute(\"name\") || target.id || selectorOf(target);\n let set = this.forms.get(form);\n if (!set) this.forms.set(form, (set = new Set()));\n set.add(key);\n }\n\n /** Forms touched and never submitted, reported when the page goes away. */\n private flushForms(): void {\n for (const [form, fields] of this.forms) {\n if (!form.isConnected && fields.size === 0) continue;\n this.emit({ type: \"form_abandon\", selector: selectorOf(form), count: fields.size });\n }\n this.forms.clear();\n }\n}\n","// rrweb, loaded only when this page may record.\n//\n// The ESM build turns the dynamic import into a separate chunk, so a page that only\n// sends signals never downloads rrweb (~24 KB gzip). The IIFE build bundles it.\n//\n// Privacy defaults that config cannot turn off:\n// - every input value is masked (maskAllInputs), passwords and card fields included;\n// - card fields ([autocomplete^=\"cc-\"]) and [data-hm-block] / .hm-block are blocked:\n// replaced by an empty box of the same size, their content never leaves the page;\n// - text marked .hm-mask / [data-hm-mask] is masked; maskAllText masks every text node;\n// - canvas, cross-origin iframes and inlined images are not recorded.\n\nimport type { RrEvent } from \"./buffer\";\nimport { CHECKOUT_MS, PAGE_TAG } from \"./buffer\";\n\nexport interface RecorderOptions {\n blockSelectors: string[];\n maskAllText: boolean;\n emit(e: RrEvent): void;\n}\n\ntype RecordFn = typeof import(\"@rrweb/record\").record;\n\nconst BASE_BLOCK = ['[data-hm-block]', \".hm-block\", '[autocomplete^=\"cc-\"]', 'input[type=\"password\"]'];\nconst BASE_MASK = \".hm-mask,[data-hm-mask]\";\n\nlet recordFn: Promise<RecordFn> | null = null;\n\nfunction loadRecord(): Promise<RecordFn> {\n return (recordFn ??= import(\"@rrweb/record\").then((m) => m.record));\n}\n\n/** Drops selectors the browser rejects: one bad entry in the config must not stop rrweb. */\nexport function validSelectors(list: string[]): string[] {\n return list.filter((s) => {\n if (!s || typeof s !== \"string\") return false;\n try {\n document.createDocumentFragment().querySelector(s);\n return true;\n } catch {\n return false;\n }\n });\n}\n\nexport class Recorder {\n private stopFn: (() => void) | undefined;\n private record: RecordFn | null = null;\n private stopped = false;\n\n async start(o: RecorderOptions): Promise<boolean> {\n try {\n this.record = await loadRecord();\n } catch {\n return false;\n }\n if (this.stopped) return false;\n try {\n this.stopFn = this.record({\n emit: (e) => o.emit(e as unknown as RrEvent),\n checkoutEveryNms: CHECKOUT_MS,\n maskAllInputs: true,\n maskInputOptions: { password: true },\n maskTextSelector: o.maskAllText ? \"*\" : BASE_MASK,\n blockSelector: validSelectors([...BASE_BLOCK, ...o.blockSelectors]).join(\",\"),\n recordCanvas: false,\n recordCrossOriginIframes: false,\n inlineImages: false,\n collectFonts: false,\n slimDOMOptions: { script: true, comment: true, headFavicon: true, headWhitespace: true, headMetaSocial: true, headMetaRobots: true, headMetaHttpEquiv: true, headMetaVerification: true, headMetaAuthorship: true },\n sampling: { mousemove: 50, scroll: 150, input: \"last\" },\n }) ?? undefined;\n return !!this.stopFn;\n } catch {\n return false;\n }\n }\n\n /** New full snapshot right now (the first event of a fresh recording must be one). */\n snapshot(): void {\n try {\n this.record?.takeFullSnapshot(true);\n } catch {\n // Not recording.\n }\n }\n\n /** Marks an SPA URL change in the stream (the player and the page list read it). */\n page(href: string): void {\n try {\n this.record?.addCustomEvent(PAGE_TAG, { href });\n } catch {\n // Not recording.\n }\n }\n\n stop(): void {\n this.stopped = true;\n try {\n this.stopFn?.();\n } catch {\n // Already stopped.\n }\n this.stopFn = undefined;\n }\n}\n","// Live recording upload: rrweb events → gzip'd chunk → presigned PUT straight to R2.\n//\n// Per chunk: POST /api/t/r/chunk (the API answers with a presigned URL and, on seq 0,\n// opens the recording and counts it in the quota), then PUT the gzip to R2. Nothing\n// of the recording passes through our servers.\n//\n// A chunk goes out every FLUSH_MS or once FLUSH_BYTES of raw JSON piled up. The two\n// requests cannot be trusted to finish while a page unloads, so on page hide the\n// unsent events (queued and in flight) are written to IndexedDB instead, and the\n// next page of the same session uploads them before its own. A session that never\n// comes back loses its last few seconds; nothing else is lost.\n//\n// seq advances only after R2 accepted the PUT, so a retry re-uses the same seq and\n// overwrites the same object: retries are idempotent end to end.\n\nimport type { Env } from \"./env\";\nimport { approxBytes, pagesOf, type RrEvent } from \"./buffer\";\nimport { REPLAYKIT_MAX_CHUNK_BYTES, REPLAYKIT_MAX_CHUNKS, REPLAYKIT_MAX_PAGES_PER_CHUNK, type ReplayKitChunkFile } from \"./protocol\";\nimport type { StateStore, SessionState, StopReason } from \"./state\";\nimport { backoff, type Api } from \"./transport\";\n\nexport const FLUSH_MS = 30_000;\nexport const FLUSH_BYTES = 256 * 1024;\n\nconst pendingKey = (sid: string) => `rk:pending:${sid}`;\n\ninterface Pending {\n rid: string;\n events: RrEvent[];\n}\n\nexport interface UploaderHooks {\n /** Recording must stop (quota, ReplayKit off for the project, chunk limit, unusable chunk). */\n onStop(reason: StopReason): void;\n /** Right before a chunk is requested: write the running stats into the session state. */\n beforeChunk?(): void;\n}\n\nexport class Uploader {\n private queue: RrEvent[] = [];\n private inflight: RrEvent[] = [];\n private queuedBytes = 0;\n private timer: unknown = null;\n private retryTimer: unknown = null;\n private failures = 0;\n private busy = false;\n private stopped = false;\n /** Bumped by pause(); a flush that started before it must not touch the new state. */\n private epoch = 0;\n /** A copy of the unsent events was parked in IndexedDB (page hidden); keep it in step with what lands. */\n private parked = false;\n /** Last page reported for `lastPageRid`: a checkout's repeated Meta href is not a new page. */\n private lastPage = \"\";\n private lastPageRid = \"\";\n\n constructor(\n private readonly env: Env,\n private readonly api: Api,\n private readonly states: StateStore,\n private readonly ids: { sid: string; vid: string },\n private readonly hooks: UploaderHooks,\n ) {}\n\n private state(): SessionState {\n return this.states.load(this.ids.sid);\n }\n\n /** Prepends what the previous page of this session could not send. */\n async restorePending(): Promise<void> {\n const store = this.env.store;\n if (!store) return;\n const p = await store.get<Pending>(pendingKey(this.ids.sid));\n if (!p) return;\n await store.del(pendingKey(this.ids.sid));\n const s = this.state();\n if (!s.rid || p.rid !== s.rid || !Array.isArray(p.events) || p.events.length === 0) return;\n this.queue = p.events.concat(this.queue);\n this.queuedBytes += p.events.reduce((n, e) => n + approxBytes(e), 0);\n this.schedule(0);\n }\n\n add(e: RrEvent): void {\n if (this.stopped) return;\n this.queue.push(e);\n this.queuedBytes += approxBytes(e);\n if (this.queuedBytes >= FLUSH_BYTES) this.schedule(0);\n else if (!this.timer) this.schedule(FLUSH_MS);\n }\n\n addMany(events: RrEvent[]): void {\n for (const e of events) this.add(e);\n }\n\n private schedule(ms: number): void {\n if (this.timer) this.env.clearTimer(this.timer);\n this.timer = this.env.setTimer(() => {\n this.timer = null;\n void this.flush();\n }, ms);\n }\n\n /** Uploads everything queued, one chunk at a time. Resolves when the queue is empty or blocked. */\n async flush(): Promise<void> {\n if (this.busy || this.stopped) return;\n const epoch = this.epoch;\n this.busy = true;\n try {\n while (!this.stopped && epoch === this.epoch && (this.inflight.length > 0 || this.queue.length > 0)) {\n if (this.inflight.length === 0) {\n this.inflight = this.queue;\n this.queue = [];\n this.queuedBytes = 0;\n }\n const ok = await this.sendInflight();\n if (!ok) return;\n }\n } finally {\n this.busy = false;\n }\n }\n\n /** True when the in-flight events are done with (sent or dropped); false to wait for a retry. */\n private async sendInflight(): Promise<boolean> {\n this.hooks.beforeChunk?.();\n const s = this.state();\n if (!s.rid || !s.trigger || !s.stats) return false;\n if (s.seq >= REPLAYKIT_MAX_CHUNKS) {\n this.stop(\"limit\");\n return false;\n }\n const gzip = this.env.gzip;\n if (!gzip) {\n this.stop(\"off\");\n return false;\n }\n const file: ReplayKitChunkFile = { v: 1, rid: s.rid, seq: s.seq, events: this.inflight };\n const blob = await gzip(JSON.stringify(file));\n if (blob.size > REPLAYKIT_MAX_CHUNK_BYTES) {\n // Too big for one chunk: send the first half now, the rest goes back to the head of the queue.\n if (this.inflight.length > 1) {\n const half = Math.ceil(this.inflight.length / 2);\n this.queue = this.inflight.slice(half).concat(this.queue);\n this.inflight = this.inflight.slice(0, half);\n return true;\n }\n // One event over 2 MB gzip'd (a giant full snapshot): unreplayable either way.\n this.stop(\"limit\");\n return false;\n }\n const prevPage = this.lastPageRid === s.rid ? this.lastPage : \"\";\n const pages = pagesOf(this.inflight, prevPage, REPLAYKIT_MAX_PAGES_PER_CHUNK);\n const presign = await this.api.chunk({ rid: s.rid, sid: this.ids.sid, vid: this.ids.vid, seq: s.seq, bytes: blob.size, trigger: s.trigger, stats: s.stats, pages });\n if (presign.outcome === \"drop\") {\n this.inflight = [];\n return true;\n }\n if (presign.outcome === \"quota\") return this.stop(\"quota\"), false;\n if (presign.outcome === \"off\" || presign.outcome === \"stop\") return this.stop(\"off\"), false;\n if (presign.outcome !== \"ok\" || !presign.data?.url) return this.retryLater(), false;\n const put = await this.api.put(presign.data.url, blob);\n if (put === \"drop\") {\n this.inflight = [];\n return true;\n }\n if (put !== \"ok\") return this.retryLater(), false;\n this.failures = 0;\n this.inflight = [];\n if (pages.length) {\n this.lastPage = pages[pages.length - 1].url;\n this.lastPageRid = s.rid;\n }\n const cur = this.state();\n if (cur.rid === s.rid) this.states.save({ ...cur, seq: s.seq + 1 });\n // Hidden tab whose upload still went through: the parked copy must not replay it on the next page.\n if (this.parked) {\n if (this.queue.length) this.persist();\n else this.unpersist();\n }\n return true;\n }\n\n private retryLater(): void {\n this.failures++;\n if (this.retryTimer) this.env.clearTimer(this.retryTimer);\n this.retryTimer = this.env.setTimer(() => {\n this.retryTimer = null;\n void this.flush();\n }, backoff(this.failures));\n }\n\n private stop(reason: StopReason): void {\n this.stopped = true;\n this.queue = [];\n this.inflight = [];\n this.queuedBytes = 0;\n this.clearTimers();\n this.hooks.onStop(reason);\n }\n\n private clearTimers(): void {\n if (this.timer) this.env.clearTimer(this.timer);\n if (this.retryTimer) this.env.clearTimer(this.retryTimer);\n this.timer = this.retryTimer = null;\n }\n\n /** Page hide: park unsent events for the next page of this session. */\n persist(): void {\n const store = this.env.store;\n const s = this.state();\n const events = this.inflight.concat(this.queue);\n if (!store || !s.rid || events.length === 0 || this.stopped) return;\n this.parked = true;\n void store.set(pendingKey(this.ids.sid), { rid: s.rid, events } satisfies Pending);\n }\n\n /** The page came back into view: whatever was parked is still in memory, drop the copy. */\n unpersist(): void {\n if (!this.parked) return;\n this.parked = false;\n void this.env.store?.del(pendingKey(this.ids.sid));\n }\n\n /** Stop without a reason the server gave (manual stop, consent withdrawn): drop what is queued. */\n discard(): void {\n this.epoch++;\n this.stopped = true;\n this.queue = [];\n this.inflight = [];\n this.queuedBytes = 0;\n this.clearTimers();\n this.unpersist();\n }\n\n get pendingEvents(): number {\n return this.queue.length + this.inflight.length;\n }\n}\n","// The ReplayKit client: one per page load.\n//\n// 1. waits for the tracker's visitor and session ids (read-only, identity.ts);\n// 2. starts the signal listeners at once, so errors during startup are caught,\n// holding what they find until /init answers;\n// 3. POST /api/t/r/init: signals on/off, recordings on/off, the project's config and\n// a server-side trigger (\"paying\");\n// 4. decides the page (decide.ts): off, continue the session's recording, start\n// one, or keep a pre-trigger buffer and wait for a trigger.\n//\n// Conditional recording: in buffer mode rrweb runs into an in-memory ring buffer cut\n// at full snapshots (buffer.ts), parked in IndexedDB when the page hides so a trigger\n// on the next page of the session still shows the page before. A trigger promotes\n// the buffer into a recording: a recording id is minted, the buffer becomes the\n// first chunk and every later event streams through the uploader.\n\nimport { emptyStats, StateStore, type SessionState, type StopReason } from \"./state\";\nimport { browserEnv, isBrowser, type Env } from \"./env\";\nimport { readIdentity } from \"./identity\";\nimport { inSample, matchAny, stripUrl } from \"./match\";\nimport { trimBuffer, BUFFER_WINDOW_MS, CHECKOUT_MS, EVENT_FULL_SNAPSHOT, type RrEvent } from \"./buffer\";\nimport { activeIncrement } from \"./detect\";\nimport { decidePage } from \"./decide\";\nimport { Api, backoff } from \"./transport\";\nimport { SignalQueue } from \"./signal-queue\";\nimport { SignalCollector } from \"./signals-dom\";\nimport { Recorder } from \"./recorder\";\nimport { Uploader } from \"./uploader\";\nimport type { ReplayKitClientConfig, ReplayKitInitResponse, ReplayKitRecordingStats, ReplayKitSignal, ReplayKitTrigger } from \"./protocol\";\n\nexport interface ReplayKitOptions {\n /** Your project's public API key (the same one the tracker uses). */\n apiKey: string;\n /** Defaults to https://app.himetrica.com. */\n apiUrl?: string;\n /** Run on localhost (off by default, like the tracker). */\n allowLocalhost?: boolean;\n /** Log decisions to the console. */\n debug?: boolean;\n}\n\nconst CONSENT_KEY = \"hm_rk_consent\";\nconst bufferKey = (sid: string) => `rk:buf:${sid}`;\nconst IDENTITY_POLL_MS = 500;\nconst IDENTITY_WAIT_MS = 15_000;\nconst INIT_ATTEMPTS = 3;\n\ntype Mode = \"idle\" | \"off\" | \"wait_consent\" | \"buffer\" | \"live\";\n\ninterface ParkedBuffer {\n at: number;\n events: RrEvent[];\n}\n\nexport class ReplayKitClient {\n private readonly env: Env;\n private readonly api: Api;\n private readonly states: StateStore;\n private ids: { sid: string; vid: string } | null = null;\n private queue: SignalQueue | null = null;\n private collector: SignalCollector | null = null;\n private recorder: Recorder | null = null;\n private uploader: Uploader | null = null;\n private init: ReplayKitInitResponse | null = null;\n private config: ReplayKitClientConfig | null = null;\n private mode: Mode = \"idle\";\n private buffer: RrEvent[] = [];\n /** Clicks, signals and input times while buffering, folded into the stats on promotion. */\n private recent: Array<{ k: \"click\" | \"rage\" | \"dead\" | \"error\" | \"input\"; ts: number }> = [];\n private stats: ReplayKitRecordingStats | null = null;\n private lastInputTs = 0;\n /** A trigger that fired before /init answered. */\n private earlyTrigger: ReplayKitTrigger | null = null;\n private destroyed = false;\n private offWindow: Array<() => void> = [];\n\n constructor(private readonly options: ReplayKitOptions, env: Env = browserEnv()) {\n this.env = env;\n const apiUrl = (options.apiUrl || \"https://app.himetrica.com\").replace(/\\/+$/, \"\");\n this.api = new Api(env, apiUrl, options.apiKey);\n this.states = new StateStore(env);\n }\n\n private log(...args: unknown[]): void {\n if (this.options.debug) console.debug(\"[ReplayKit]\", ...args);\n }\n\n /** Starts everything. Safe to call once per page; no-op outside a browser. */\n start(): void {\n if (!isBrowser || !this.options.apiKey) return;\n const host = location.hostname;\n if (!this.options.allowLocalhost && (host === \"localhost\" || host === \"127.0.0.1\" || host === \"[::1]\" || host.endsWith(\".localhost\"))) {\n this.log(\"localhost: idle (allowLocalhost to enable)\");\n return;\n }\n const started = this.env.now();\n const tryIds = () => {\n if (this.destroyed) return;\n const id = readIdentity(this.env);\n if (id.sessionId && id.visitorId) {\n this.ids = { sid: id.sessionId, vid: id.visitorId };\n this.boot();\n } else if (this.env.now() - started < IDENTITY_WAIT_MS) {\n this.env.setTimer(tryIds, IDENTITY_POLL_MS);\n } else {\n this.log(\"no tracker ids on this page: idle\");\n }\n };\n tryIds();\n }\n\n private boot(): void {\n const ids = this.ids!;\n this.queue = new SignalQueue(this.env, this.api, () => ({ ...ids, rid: this.mode === \"live\" ? this.state().rid : undefined }));\n this.collector = new SignalCollector({\n now: () => this.env.now(),\n maskAllText: () => this.config?.maskAllText ?? true,\n signal: (s) => this.onSignal(s),\n trigger: (t) => this.trigger(t),\n input: (kind, ts) => this.onInput(kind, ts),\n navigated: (url) => this.onNavigate(url),\n });\n this.collector.start();\n this.listenPage();\n void this.callInit(1);\n }\n\n private state(): SessionState {\n return this.states.load(this.ids!.sid);\n }\n\n private async callInit(attempt: number): Promise<void> {\n const ids = this.ids!;\n const r = await this.api.init({ sid: ids.sid, vid: ids.vid, url: stripUrl(location.href) });\n if (this.destroyed) return;\n if (r.outcome === \"ok\" && r.data) return this.applyInit(r.data);\n if (r.outcome === \"retry\" && attempt < INIT_ATTEMPTS) {\n this.env.setTimer(() => void this.callInit(attempt + 1), backoff(attempt));\n return;\n }\n this.log(\"init refused:\", r.outcome);\n if (r.outcome === \"quota\") {\n // Quota is about recordings; signals never count.\n this.queue?.release(true);\n this.mode = \"off\";\n return;\n }\n this.shutdown();\n }\n\n private applyInit(res: ReplayKitInitResponse): void {\n this.init = res;\n this.config = res.config;\n this.queue!.release(res.signals);\n if (!res.signals && !res.record) {\n this.shutdown();\n return;\n }\n this.decide();\n }\n\n private consentValue(): boolean | null {\n const v = this.env.getLocal(CONSENT_KEY);\n return v === \"1\" ? true : v === \"0\" ? false : null;\n }\n\n private triggerEnabled(t: ReplayKitTrigger): boolean {\n if (t === \"manual\" || t === \"sample\") return true;\n return !!this.config?.triggers.includes(t as ReplayKitClientConfig[\"triggers\"][number]);\n }\n\n private pathExcluded(): boolean {\n return matchAny(this.config?.excludePaths ?? [], location.pathname);\n }\n\n private onCheckoutPath(): boolean {\n return this.triggerEnabled(\"checkout\") && matchAny(this.config?.checkoutPaths ?? [], location.pathname);\n }\n\n private decide(): void {\n if (!this.init || !this.config) return;\n const s = this.state();\n const mode = decidePage({\n record: this.init.record,\n canGzip: !!this.env.gzip,\n state: s,\n excluded: this.pathExcluded(),\n requireConsent: this.config.requireConsent,\n consent: this.consentValue(),\n forceTrigger: this.init.forceTrigger,\n sampled: inSample(this.ids!.sid, this.config.sampleRate),\n checkout: this.onCheckoutPath(),\n });\n this.log(\"page:\", mode.kind, mode.kind === \"start\" ? mode.trigger : \"\");\n if (mode.kind === \"off\" || mode.kind === \"wait_consent\") {\n this.mode = mode.kind;\n return;\n }\n if (mode.kind === \"continue\") {\n this.mode = \"live\";\n const st = s.stats ?? emptyStats(this.env.now(), stripUrl(location.href));\n this.stats = { ...st, pages: st.pages + 1 };\n this.makeUploader();\n void this.uploader!.restorePending();\n void this.startRecorder();\n return;\n }\n this.mode = \"buffer\";\n void this.restoreParkedBuffer().then(async () => {\n if (this.mode !== \"buffer\") return;\n // rrweb emits the page's full snapshot synchronously on start: it lands in the buffer\n // before a promotion, so the first chunk always opens with a replayable page.\n await this.startRecorder();\n if (this.mode !== \"buffer\") return;\n const t = mode.kind === \"start\" ? mode.trigger : this.earlyTrigger;\n this.earlyTrigger = null;\n if (t && this.triggerEnabled(t)) this.promote(t);\n });\n }\n\n private async restoreParkedBuffer(): Promise<void> {\n const store = this.env.store;\n if (!store) return;\n const key = bufferKey(this.ids!.sid);\n const parked = await store.get<ParkedBuffer>(key);\n if (!parked) return;\n void store.del(key);\n const fresh = this.env.now() - parked.at < BUFFER_WINDOW_MS + CHECKOUT_MS;\n if (fresh && Array.isArray(parked.events) && parked.events.length) this.buffer = parked.events.concat(this.buffer);\n }\n\n private async startRecorder(): Promise<void> {\n if (this.recorder) return;\n const rec = (this.recorder = new Recorder());\n const ok = await rec.start({\n blockSelectors: this.config?.blockSelectors ?? [],\n maskAllText: this.config?.maskAllText ?? false,\n emit: (e) => this.onEvent(e),\n });\n if (!ok && this.recorder === rec) {\n this.log(\"rrweb failed to start\");\n this.recorder = null;\n this.mode = \"off\";\n }\n }\n\n private onEvent(e: RrEvent): void {\n if (this.mode === \"buffer\") {\n this.buffer.push(e);\n // Trim only when a new checkout lands: the only moment a cut point appears.\n if (e.type === EVENT_FULL_SNAPSHOT) this.buffer = trimBuffer(this.buffer, this.env.now());\n return;\n }\n if (this.mode === \"live\" && this.uploader) {\n if (this.stats) this.stats.endedAt = Math.max(this.stats.endedAt, e.timestamp);\n this.uploader.add(e);\n }\n }\n\n private makeUploader(): void {\n this.uploader = new Uploader(this.env, this.api, this.states, this.ids!, {\n onStop: (reason) => this.stopRecording(reason),\n beforeChunk: () => this.saveStats(),\n });\n }\n\n private saveStats(): void {\n if (!this.stats || this.mode !== \"live\") return;\n const s = this.state();\n if (s.rid) this.states.save({ ...s, stats: this.stats });\n }\n\n /** A trigger fired: turn the buffer into a recording, or remember it until /init answers. */\n trigger(t: ReplayKitTrigger): void {\n if (this.destroyed || !this.ids) return;\n if (!this.init) {\n if (!this.earlyTrigger) this.earlyTrigger = t;\n return;\n }\n if (this.mode !== \"buffer\" || !this.triggerEnabled(t)) return;\n if (!this.recorder) {\n // Still restoring the parked buffer: the buffer setup promotes it when it is done.\n this.earlyTrigger ??= t;\n return;\n }\n this.promote(t);\n }\n\n private promote(t: ReplayKitTrigger): void {\n const now = this.env.now();\n // The buffer starts at a full snapshot; recordings begin there, so stats do too.\n const events = this.buffer;\n this.buffer = [];\n const startedAt = events.length ? events[0].timestamp : now;\n const stats = emptyStats(startedAt, stripUrl(location.href));\n stats.endedAt = events.length ? events[events.length - 1].timestamp : now;\n let last = 0;\n for (const r of this.recent) {\n if (r.ts < startedAt) continue;\n if (r.k === \"click\") stats.clicks++;\n else if (r.k === \"rage\") stats.rageClicks++;\n else if (r.k === \"dead\") stats.deadClicks++;\n else if (r.k === \"error\") stats.errors++;\n if (r.k === \"click\" || r.k === \"input\") {\n stats.activeMs += activeIncrement(last, r.ts);\n last = r.ts;\n }\n }\n this.recent = [];\n this.stats = stats;\n this.states.save({ ...this.state(), rid: this.env.uuid(), seq: 0, trigger: t, stats, stopped: undefined });\n this.mode = \"live\";\n this.log(\"recording:\", t);\n this.makeUploader();\n this.uploader!.addMany(events);\n if (!events.length) this.recorder?.snapshot();\n // Open the recording right away: short sessions that end soon after a trigger are the interesting ones.\n void this.uploader!.flush();\n }\n\n private stopRecording(reason: StopReason): void {\n this.log(\"recording stopped:\", reason);\n this.recorder?.stop();\n this.recorder = null;\n this.mode = \"off\";\n this.buffer = [];\n const s = this.state();\n this.states.save({ ...s, stopped: reason });\n }\n\n private onSignal(s: ReplayKitSignal): void {\n if (this.mode === \"live\" && this.stats) {\n if (s.type === \"rage_click\") this.stats.rageClicks++;\n else if (s.type === \"dead_click\") this.stats.deadClicks++;\n else if (s.type === \"error\") this.stats.errors++;\n } else if (this.mode === \"buffer\" || this.mode === \"idle\") {\n const k = s.type === \"rage_click\" ? \"rage\" : s.type === \"dead_click\" ? \"dead\" : s.type === \"error\" ? \"error\" : null;\n if (k) this.pushRecent(k, s.ts);\n }\n this.queue?.add(s);\n }\n\n private onInput(kind: \"click\" | \"key\" | \"scroll\", ts: number): void {\n if (this.mode === \"live\" && this.stats) {\n this.stats.activeMs += activeIncrement(this.lastInputTs, ts);\n if (kind === \"click\") this.stats.clicks++;\n } else if (this.mode === \"buffer\" || this.mode === \"idle\") {\n this.pushRecent(kind === \"click\" ? \"click\" : \"input\", ts);\n }\n this.lastInputTs = ts;\n }\n\n private pushRecent(k: \"click\" | \"rage\" | \"dead\" | \"error\" | \"input\", ts: number): void {\n this.recent.push({ k, ts });\n if (this.recent.length > 1000) this.recent.splice(0, this.recent.length - 1000);\n }\n\n private onNavigate(url: string): void {\n if (!this.init || !this.config) return;\n if (this.mode === \"live\" && this.stats) this.stats.pages++;\n const excluded = this.pathExcluded();\n if (excluded && (this.mode === \"live\" || this.mode === \"buffer\")) {\n // SPA route that must not be recorded: pause rrweb, keep the session's state.\n this.recorder?.stop();\n this.recorder = null;\n if (this.mode === \"buffer\") this.buffer = [];\n void this.uploader?.flush();\n this.mode = \"off\";\n return;\n }\n if (!excluded && this.mode === \"off\" && !this.state().stopped) {\n // Back from an excluded route: decide again (continues the recording or rebuffers).\n this.uploader = null;\n this.decide();\n return;\n }\n // Excluded routes never reach here: their URL is not written into the stream.\n this.recorder?.page(url);\n if (this.onCheckoutPath()) this.trigger(\"checkout\");\n }\n\n private listenPage(): void {\n const hide = (unloading: boolean) => {\n if (unloading) this.collector?.pageHide();\n this.queue?.beacon();\n if (this.mode === \"live\") {\n this.saveStats();\n this.uploader?.persist();\n } else if (this.mode === \"buffer\" && this.buffer.length && this.env.store) {\n const events = trimBuffer(this.buffer, this.env.now());\n void this.env.store.set(bufferKey(this.ids!.sid), { at: this.env.now(), events } satisfies ParkedBuffer);\n }\n };\n const onVis = () => {\n if (document.visibilityState === \"hidden\") hide(false);\n else this.uploader?.unpersist();\n };\n const onHide = () => hide(true);\n document.addEventListener(\"visibilitychange\", onVis);\n window.addEventListener(\"pagehide\", onHide);\n this.offWindow.push(\n () => document.removeEventListener(\"visibilitychange\", onVis),\n () => window.removeEventListener(\"pagehide\", onHide),\n );\n }\n\n // ── Public API ────────────────────────────────────────────\n\n /** Start recording this session now (ReplayKit.start()). Still subject to /init, consent and excludePaths. */\n manualStart(reason?: string): void {\n if (!this.ids) return;\n if (reason) this.log(\"manual trigger:\", reason);\n if (this.state().stopped === \"manual\") {\n // Started again after ReplayKit.stop(): a new recording, decided like a fresh page.\n this.states.save({ ...this.state(), stopped: undefined, rid: undefined, seq: 0, trigger: undefined, stats: undefined });\n this.uploader = null;\n this.earlyTrigger = \"manual\";\n this.decide();\n return;\n }\n this.trigger(\"manual\");\n }\n\n /** Stop recording for the rest of the session; what was captured is still uploaded. */\n manualStop(): void {\n if (!this.ids) return;\n this.recorder?.stop();\n this.recorder = null;\n this.buffer = [];\n if (this.mode === \"live\") {\n this.saveStats();\n void this.uploader?.flush();\n }\n this.mode = \"off\";\n this.states.save({ ...this.state(), stopped: \"manual\" });\n }\n\n /** The visitor's answer to the customer's consent banner. Stored on this device. */\n consent(granted: boolean): void {\n this.env.setLocal(CONSENT_KEY, granted ? \"1\" : \"0\");\n if (!this.ids) return;\n if (granted) {\n if (this.mode === \"wait_consent\") this.decide();\n return;\n }\n // Withdrawn: stop now and drop everything not uploaded yet.\n this.recorder?.stop();\n this.recorder = null;\n this.buffer = [];\n this.uploader?.discard();\n void this.env.store?.del(bufferKey(this.ids.sid));\n this.mode = \"off\";\n }\n\n recordingId(): string | null {\n return this.mode === \"live\" ? (this.ids ? this.state().rid ?? null : null) : null;\n }\n\n private shutdown(): void {\n this.collector?.stop();\n this.queue?.disable();\n this.recorder?.stop();\n this.recorder = null;\n this.mode = \"off\";\n }\n\n destroy(): void {\n this.destroyed = true;\n this.shutdown();\n for (const f of this.offWindow) f();\n this.offWindow = [];\n }\n}\n","// @himetrica/replaykit: session replay and friction signals for the web.\n//\n// import * as ReplayKit from \"@himetrica/replaykit\";\n// ReplayKit.init({ apiKey: \"hm_...\" });\n//\n// Needs the Himetrica tracker on the same page: recordings and signals belong to the\n// tracker's visitor and session. What is recorded, and when, is configured per\n// project in the dashboard; the SDK reads it from /init on every page.\n\nimport { ReplayKitClient, type ReplayKitOptions } from \"./client\";\n\nexport type { ReplayKitOptions } from \"./client\";\nexport type { ReplayKitSignal, ReplayKitSignalType, ReplayKitTrigger } from \"./protocol\";\n\nlet client: ReplayKitClient | null = null;\n\n/** Starts ReplayKit. A second call is ignored. */\nexport function init(options: ReplayKitOptions): void {\n if (client) return;\n client = new ReplayKitClient(options);\n client.start();\n}\n\n/** Record this session from now on (and keep the pre-trigger buffer). */\nexport function start(): void {\n client?.manualStart();\n}\n\n/**\n * Mark this moment as worth a recording, like an error would. `reason` is for your\n * own debugging (logged with `debug: true`); the recording is tagged \"manual\".\n */\nexport function trigger(reason?: string): void {\n client?.manualStart(reason);\n}\n\n/** Stop recording for the rest of the session. What was captured is still uploaded. */\nexport function stop(): void {\n client?.manualStop();\n}\n\n/**\n * The visitor's answer to your consent banner. With \"require consent\" on in the\n * project, nothing is recorded until consent(true). consent(false) stops and drops\n * what was not uploaded yet. Remembered on this device.\n */\nexport function consent(granted: boolean): void {\n if (client) client.consent(granted);\n else {\n try {\n localStorage.setItem(\"hm_rk_consent\", granted ? \"1\" : \"0\");\n } catch {\n // Storage unavailable.\n }\n }\n}\n\n/** Id of the recording running on this page, if any. */\nexport function getRecordingId(): string | null {\n return client?.recordingId() ?? null;\n}\n\n/** Stops everything and removes the listeners (tests, SPA teardown). */\nexport function shutdown(): void {\n client?.destroy();\n client = null;\n}\n"],"mappings":"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","names":["KEY","StateStore","env","sid","s","raw","emptyStats","now","entryUrl","isBrowser","uuidV4","c","ch","r","safeGet","storage","key","safeSet","value","indexedDbStore","dbp","open","resolve","reject","req","run","mode","fn","db","tx","s","gzipFn","text","browserEnv","originalFetch","nav","url","body","name","match","ms","h","readIdentity","env","visitorId","sessionId","matchPath","pattern","pathname","p","path","base","escapeRegExp","matchAny","patterns","s","sampleDraw","sessionId","h1","h2","i","ch","inSample","rate","stripUrl","href","cut","PAGE_TAG","pagesOf","events","prev","max","out","last","e","href","d","cut","url","BUFFER_WINDOW_MS","CHECKOUT_MS","snapshotStart","i","trimBuffer","now","windowMs","cutoff","keepFrom","firstSnapshot","start","approxBytes","RageDetector","c","b","near","finished","now","a","DEAD_CLICK_WAIT_MS","isDeadClickCandidate","f","isDeadClick","r","ACTIVE_GAP_MS","activeIncrement","lastInputTs","ts","gap","decidePage","outcomeOf","status","Api","env","apiUrl","apiKey","op","body","url","blob","res","wantJson","outcome","BASE_BACKOFF_MS","MAX_BACKOFF_MS","backoff","failures","SIGNAL_FLUSH_MS","MAX_PER_PAGE","MAX_QUEUED","DEDUPE_MS","SignalQueue","env","api","ids","s","key","prev","enabled","ms","batch","r","backoff","INTERACTIVE","FIELD","MASKED","MAX_TEXT","MAX_DETAIL","selectorOf","el","parts","cur","depth","tag","classes","c","visibleText","maskAll","t","targetFacts","pointer","sel","clickedElement","target","SignalCollector","h","RageDetector","on","type","fn","opts","now","stripUrl","f","s","href","prev","e","ts","selector","r","force","b","count","isDeadClickCandidate","hrefBefore","scrollBefore","focusBefore","mutations","mo","list","isDeadClick","DEAD_CLICK_WAIT_MS","message","detail","form","key","set","fields","BASE_BLOCK","BASE_MASK","recordFn","loadRecord","m","validSelectors","list","s","Recorder","o","CHECKOUT_MS","href","PAGE_TAG","FLUSH_MS","FLUSH_BYTES","pendingKey","sid","Uploader","env","api","states","ids","hooks","store","p","s","n","e","approxBytes","events","ms","epoch","gzip","file","blob","half","prevPage","pages","pagesOf","presign","put","cur","backoff","reason","CONSENT_KEY","bufferKey","sid","IDENTITY_POLL_MS","IDENTITY_WAIT_MS","INIT_ATTEMPTS","ReplayKitClient","options","env","browserEnv","apiUrl","Api","StateStore","args","isBrowser","host","started","tryIds","id","readIdentity","ids","SignalQueue","SignalCollector","s","t","kind","ts","url","attempt","stripUrl","backoff","res","v","matchAny","mode","decidePage","inSample","st","emptyStats","store","key","parked","BUFFER_WINDOW_MS","CHECKOUT_MS","rec","Recorder","e","trimBuffer","Uploader","reason","now","events","startedAt","stats","last","r","activeIncrement","k","excluded","hide","unloading","onVis","onHide","granted","f","client","init","options","ReplayKitClient","start","trigger","reason","stop","consent","granted","getRecordingId","shutdown"]}
|
|
1
|
+
{"version":3,"sources":["../src/state.ts","../src/env.ts","../src/identity.ts","../src/match.ts","../src/buffer.ts","../src/detect.ts","../src/decide.ts","../src/transport.ts","../src/signal-queue.ts","../src/signals-dom.ts","../src/recorder.ts","../src/uploader.ts","../src/client.ts","../src/index.ts"],"sourcesContent":["// Per-session recording state, in sessionStorage so it follows the session across the\n// pages of a multi-page site (each page load is a new SDK instance).\n//\n// Keyed to the tracker session id: when the tracker rolls a new session (30 min idle,\n// new UTM...), the stored state belongs to the old one and is discarded.\n\nimport type { Env } from \"./env\";\nimport type { ReplayKitRecordingStats, ReplayKitTrigger } from \"./protocol\";\n\nexport type StopReason = \"quota\" | \"manual\" | \"limit\" | \"off\";\n\nexport interface SessionState {\n sid: string;\n /** Recording id, set once a trigger fired; absent while only buffering. */\n rid?: string;\n /** Next chunk sequence number to upload. */\n seq: number;\n trigger?: ReplayKitTrigger;\n /** Set when recording must not (re)start for the rest of the session. */\n stopped?: StopReason;\n stats?: ReplayKitRecordingStats;\n /** Visible tab time of the session's earlier pages, for the minimum duration. */\n visibleMs?: number;\n}\n\nconst KEY = \"hm_rk_state\";\n\nexport class StateStore {\n private cache: SessionState | null = null;\n\n constructor(private readonly env: Env) {}\n\n load(sid: string): SessionState {\n if (this.cache?.sid === sid) return this.cache;\n let s: SessionState | null = null;\n try {\n const raw = this.env.getSession(KEY);\n if (raw) s = JSON.parse(raw) as SessionState;\n } catch {\n s = null;\n }\n if (!s || s.sid !== sid || typeof s.seq !== \"number\") s = { sid, seq: 0 };\n this.cache = s;\n return s;\n }\n\n save(s: SessionState): void {\n this.cache = s;\n try {\n this.env.setSession(KEY, JSON.stringify(s));\n } catch {\n // In-memory copy still drives this page.\n }\n }\n}\n\nexport function emptyStats(now: number, entryUrl: string): ReplayKitRecordingStats {\n return { startedAt: now, endedAt: now, activeMs: 0, clicks: 0, rageClicks: 0, deadClicks: 0, errors: 0, pages: 1, entryUrl };\n}\n","// Browser globals behind one interface, like ErrorKit's env.ts.\n//\n// Network, storage, timers and the clock go through an Env, so the transport, the\n// uploader and the client run in plain Node under node:test with fakes. DOM wiring\n// (listeners, rrweb) stays in signals-dom.ts / recorder.ts and is not unit-tested.\n\nexport interface FetchResult {\n status: number;\n headers: { get(name: string): string | null };\n json(): Promise<unknown>;\n}\n\nexport interface Env {\n now(): number;\n uuid(): string;\n fetch?: (url: string, init: RequestInit) => Promise<FetchResult>;\n sendBeacon?: (url: string, body: string) => boolean;\n getLocal(key: string): string | null;\n setLocal(key: string, value: string | null): void;\n getSession(key: string): string | null;\n setSession(key: string, value: string | null): void;\n getCookie(name: string): string | null;\n /** Async key/value store that survives a page load (IndexedDB). Missing = nothing carried across pages. */\n store?: KvStore;\n /** gzip; undefined when CompressionStream is unavailable (recording is then skipped). */\n gzip?: (text: string) => Promise<Blob>;\n setTimer(fn: () => void, ms: number): unknown;\n clearTimer(handle: unknown): void;\n}\n\nexport interface KvStore {\n get<T>(key: string): Promise<T | undefined>;\n set(key: string, value: unknown): Promise<void>;\n del(key: string): Promise<void>;\n}\n\nexport const isBrowser = typeof window !== \"undefined\" && typeof document !== \"undefined\";\n\nexport function uuidV4(): string {\n const c = (globalThis as { crypto?: Crypto }).crypto;\n if (c && typeof c.randomUUID === \"function\") {\n try {\n return c.randomUUID();\n } catch {\n // randomUUID throws on insecure origins in some browsers; fall through.\n }\n }\n return \"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx\".replace(/[xy]/g, (ch) => {\n const r = (Math.random() * 16) | 0;\n return (ch === \"x\" ? r : (r & 0x3) | 0x8).toString(16);\n });\n}\n\n// Storage throws in Safari private mode, sandboxed iframes and under storage policies.\nfunction safeGet(storage: () => Storage, key: string): string | null {\n try {\n return storage().getItem(key);\n } catch {\n return null;\n }\n}\n\nfunction safeSet(storage: () => Storage, key: string, value: string | null): void {\n try {\n if (value === null) storage().removeItem(key);\n else storage().setItem(key, value);\n } catch {\n // Unavailable or full: in-memory state still works for this page.\n }\n}\n\n/** One IndexedDB object store used as a key/value map. Every failure degrades to \"nothing stored\". */\nfunction indexedDbStore(): KvStore | undefined {\n if (typeof indexedDB === \"undefined\") return undefined;\n let dbp: Promise<IDBDatabase> | null = null;\n const open = () =>\n (dbp ??= new Promise<IDBDatabase>((resolve, reject) => {\n const req = indexedDB.open(\"himetrica-replaykit\", 1);\n req.onupgradeneeded = () => req.result.createObjectStore(\"kv\");\n req.onsuccess = () => resolve(req.result);\n req.onerror = () => reject(req.error);\n }));\n const run = <T>(mode: IDBTransactionMode, fn: (s: IDBObjectStore) => IDBRequest | void): Promise<T | undefined> =>\n open()\n .then(\n (db) =>\n new Promise<T | undefined>((resolve) => {\n const tx = db.transaction(\"kv\", mode);\n const req = fn(tx.objectStore(\"kv\"));\n tx.oncomplete = () => resolve(req ? (req.result as T) : undefined);\n tx.onerror = tx.onabort = () => resolve(undefined);\n }),\n )\n .catch(() => undefined);\n return {\n get: <T>(key: string) => run<T>(\"readonly\", (s) => s.get(key)),\n set: (key, value) => run<void>(\"readwrite\", (s) => void s.put(value, key)).then(() => undefined),\n del: (key) => run<void>(\"readwrite\", (s) => void s.delete(key)).then(() => undefined),\n };\n}\n\nfunction gzipFn(): Env[\"gzip\"] {\n if (typeof CompressionStream === \"undefined\" || typeof Blob === \"undefined\" || typeof Response === \"undefined\") return undefined;\n return (text) => new Response(new Blob([text]).stream().pipeThrough(new CompressionStream(\"gzip\"))).blob();\n}\n\nexport function browserEnv(): Env {\n const originalFetch = typeof fetch === \"function\" ? fetch.bind(globalThis) : undefined;\n const nav = typeof navigator !== \"undefined\" ? navigator : undefined;\n return {\n now: () => Date.now(),\n uuid: () => uuidV4(),\n fetch: originalFetch as Env[\"fetch\"],\n sendBeacon:\n nav && typeof nav.sendBeacon === \"function\"\n ? (url, body) => {\n try {\n return nav.sendBeacon(url, new Blob([body], { type: \"text/plain\" }));\n } catch {\n return false;\n }\n }\n : undefined,\n getLocal: (key) => safeGet(() => localStorage, key),\n setLocal: (key, value) => safeSet(() => localStorage, key, value),\n getSession: (key) => safeGet(() => sessionStorage, key),\n setSession: (key, value) => safeSet(() => sessionStorage, key, value),\n getCookie: (name) => {\n try {\n const match = document.cookie.match(new RegExp(\"(?:^|; )\" + name.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\") + \"=([^;]*)\"));\n return match ? decodeURIComponent(match[1]) : null;\n } catch {\n return null;\n }\n },\n store: indexedDbStore(),\n gzip: gzipFn(),\n setTimer: (fn, ms) => setTimeout(fn, ms),\n clearTimer: (h) => clearTimeout(h as ReturnType<typeof setTimeout>),\n };\n}\n","// Identity bridge with the analytics tracker, strictly READ-ONLY (same approach as\n// ErrorKit's browser/identity.ts).\n//\n// The tracker (tracker-js / tracker.js) stores the ids in:\n// visitor cookie hm_vid (cookieDomain mode) or localStorage hm_visitor_id\n// session cookie hm_sid (cookieDomain mode) or sessionStorage hm_session_id\n// We read those keys and never write them. window.himetrica.getSessionId() is NOT\n// called: in the tracker it refreshes the session timestamp.\n//\n// A recording or a signal is only worth something tied to the tracker's visitor and\n// session, so without the tracker on the page ReplayKit stays idle.\n\nimport type { Env } from \"./env\";\n\nexport interface Identity {\n visitorId?: string;\n sessionId?: string;\n}\n\nexport function readIdentity(env: Env): Identity {\n const visitorId = env.getCookie(\"hm_vid\") || env.getLocal(\"hm_visitor_id\") || undefined;\n const sessionId = env.getCookie(\"hm_sid\") || env.getSession(\"hm_session_id\") || undefined;\n return { visitorId, sessionId };\n}\n","// Pure helpers the recording decision is made of: path globs and the session sample.\n\n/**\n * Path glob, as the project config writes them: `*` matches any run of characters\n * (slashes included), everything else is literal. A pattern without `*` is a prefix\n * on a segment boundary: `/checkout` matches `/checkout` and `/checkout/step-2`, not\n * `/checkouts`. Matched against the pathname only, case-insensitive.\n */\nexport function matchPath(pattern: string, pathname: string): boolean {\n const p = pattern.trim().toLowerCase();\n if (!p) return false;\n const path = (pathname || \"/\").toLowerCase();\n if (!p.includes(\"*\")) {\n const base = p.endsWith(\"/\") && p.length > 1 ? p.slice(0, -1) : p;\n return path === base || path.startsWith(base.endsWith(\"/\") ? base : base + \"/\");\n }\n const re = new RegExp(\"^\" + p.split(\"*\").map(escapeRegExp).join(\".*\") + \"$\");\n return re.test(path);\n}\n\nexport function matchAny(patterns: readonly string[], pathname: string): boolean {\n for (const p of patterns) if (matchPath(p, pathname)) return true;\n return false;\n}\n\nfunction escapeRegExp(s: string): string {\n return s.replace(/[.+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n}\n\n/**\n * Deterministic [0, 1) from the session id (cyrb53). Every page of a session gets the\n * same draw, so a sampled session is recorded across all its pages, and the server\n * could recompute it.\n */\nexport function sampleDraw(sessionId: string): number {\n let h1 = 0xdeadbeef;\n let h2 = 0x41c6ce57;\n for (let i = 0; i < sessionId.length; i++) {\n const ch = sessionId.charCodeAt(i);\n h1 = Math.imul(h1 ^ ch, 2654435761);\n h2 = Math.imul(h2 ^ ch, 1597334677);\n }\n h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ Math.imul(h2 ^ (h2 >>> 13), 3266489909);\n h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ Math.imul(h1 ^ (h1 >>> 13), 3266489909);\n const n = 4294967296 * (2097151 & h2) + (h1 >>> 0);\n return n / 9007199254740992;\n}\n\nexport function inSample(sessionId: string, rate: number): boolean {\n if (!(rate > 0)) return false;\n if (rate >= 1) return true;\n return sampleDraw(sessionId) < rate;\n}\n\n/** URL without query and hash: queries carry tokens and emails, never sent. */\nexport function stripUrl(href: string): string {\n const cut = href.search(/[?#]/);\n return cut === -1 ? href : href.slice(0, cut);\n}\n","// The pre-trigger ring buffer.\n//\n// rrweb can only replay from a full snapshot: a Meta event (type 4) followed by a\n// FullSnapshot (type 2). The recorder takes one every CHECKOUT_MS, so the buffer can\n// always be cut at the last checkout that is older than the window and still start\n// with a replayable page. The cut keeps BUFFER_WINDOW_MS..BUFFER_WINDOW_MS+CHECKOUT_MS\n// of history before the trigger.\n\nexport interface RrEvent {\n type: number;\n timestamp: number;\n data?: unknown;\n}\n\nexport const EVENT_FULL_SNAPSHOT = 2;\nexport const EVENT_META = 4;\nexport const EVENT_CUSTOM = 5;\n/** rrweb custom event the SDK adds on SPA URL changes (rrweb only emits Meta on page loads/checkouts). */\nexport const PAGE_TAG = \"replaykit:page\";\n\n/**\n * Pages shown in a run of events, in order: Meta hrefs (page loads, and every\n * checkout repeats the current one) plus PAGE_TAG custom events. Query/fragment\n * stripped, back-to-back repeats folded, `prev` = last page already reported for\n * this recording (so a checkout's repeated Meta is not a new page). Capped at `max`.\n */\nexport function pagesOf(events: RrEvent[], prev: string, max: number): Array<{ url: string; ts: number }> {\n const out: Array<{ url: string; ts: number }> = [];\n let last = prev;\n for (const e of events) {\n let href: unknown;\n if (e.type === EVENT_META) href = (e.data as { href?: unknown } | undefined)?.href;\n else if (e.type === EVENT_CUSTOM) {\n const d = e.data as { tag?: unknown; payload?: { href?: unknown } } | undefined;\n if (d?.tag === PAGE_TAG) href = d.payload?.href;\n }\n if (typeof href !== \"string\" || !href) continue;\n const cut = href.search(/[?#]/);\n const url = (cut === -1 ? href : href.slice(0, cut)).slice(0, 500);\n if (url === last) continue;\n last = url;\n out.push({ url, ts: e.timestamp });\n if (out.length >= max) break;\n }\n return out;\n}\n\nexport const BUFFER_WINDOW_MS = 30_000;\nexport const CHECKOUT_MS = 30_000;\n\n/** Index of the Meta event that opens the snapshot the event list may start from. */\nfunction snapshotStart(events: RrEvent[], i: number): number {\n // rrweb emits Meta right before FullSnapshot; keep the Meta so the player knows the viewport.\n return i > 0 && events[i - 1].type === EVENT_META ? i - 1 : i;\n}\n\n/**\n * Drops everything before the most recent full snapshot whose timestamp is at or\n * before `now - windowMs`. If no snapshot is that old, nothing is dropped (the buffer\n * is still shorter than the window). A list that does not start with a snapshot (the\n * head was lost) is cut at its first snapshot, since nothing before it can replay.\n */\nexport function trimBuffer(events: RrEvent[], now: number, windowMs = BUFFER_WINDOW_MS): RrEvent[] {\n const cutoff = now - windowMs;\n let keepFrom = -1;\n let firstSnapshot = -1;\n for (let i = 0; i < events.length; i++) {\n if (events[i].type !== EVENT_FULL_SNAPSHOT) continue;\n if (firstSnapshot === -1) firstSnapshot = i;\n if (events[i].timestamp <= cutoff) keepFrom = i;\n else break;\n }\n if (keepFrom === -1) keepFrom = firstSnapshot;\n if (keepFrom === -1) return [];\n const start = snapshotStart(events, keepFrom);\n return start === 0 ? events : events.slice(start);\n}\n\n/** Rough serialized size, to cut chunks without stringifying on every event. */\nexport function approxBytes(e: RrEvent): number {\n // Mutations and snapshots dominate; JSON.stringify per event is acceptable: events\n // are stringified once here and the chunk is re-stringified once at flush.\n try {\n return JSON.stringify(e).length;\n } catch {\n return 0;\n }\n}\n","// Friction detectors, DOM-free so node:test can drive them with plain numbers.\n// signals-dom.ts feeds them real clicks and reports what they decide.\n\nexport interface ClickPoint {\n x: number;\n y: number;\n ts: number;\n /** Stable key of the clicked element (its selector), so a burst on one button counts even if the mouse drifts. */\n target: string;\n}\n\n/** 3+ clicks within RAGE_WINDOW_MS, on the same element or within RAGE_RADIUS_PX of the first. */\nexport const RAGE_MIN_CLICKS = 3;\nexport const RAGE_WINDOW_MS = 1000;\nexport const RAGE_RADIUS_PX = 30;\n\nexport interface RageBurst {\n first: ClickPoint;\n count: number;\n lastTs: number;\n}\n\n/**\n * Feeds clicks in order. `onClick` returns \"trigger\" on the click that turns a burst\n * into a rage click (the 3rd), so recording starts right away; the finished burst\n * (with its final count) comes out of `onClick` of the next unrelated click or of\n * `flush(now)` once the window has passed.\n */\nexport class RageDetector {\n private burst: RageBurst | null = null;\n\n onClick(c: ClickPoint): { trigger: boolean; finished: RageBurst | null } {\n const b = this.burst;\n if (b && c.ts - b.lastTs <= RAGE_WINDOW_MS && near(b.first, c)) {\n b.count++;\n b.lastTs = c.ts;\n return { trigger: b.count === RAGE_MIN_CLICKS, finished: null };\n }\n const finished = b && b.count >= RAGE_MIN_CLICKS ? b : null;\n this.burst = { first: c, count: 1, lastTs: c.ts };\n return { trigger: false, finished };\n }\n\n /** The burst that ended (quiet for the window) before `now`, if it was a rage click. */\n flush(now: number): RageBurst | null {\n const b = this.burst;\n if (!b || now - b.lastTs <= RAGE_WINDOW_MS) return null;\n this.burst = null;\n return b.count >= RAGE_MIN_CLICKS ? b : null;\n }\n\n /** Clicks of the current burst so far (a rage burst is not also a dead click). */\n currentCount(): number {\n return this.burst?.count ?? 0;\n }\n}\n\nfunction near(a: ClickPoint, b: ClickPoint): boolean {\n if (a.target && a.target === b.target) return true;\n return Math.abs(a.x - b.x) <= RAGE_RADIUS_PX && Math.abs(a.y - b.y) <= RAGE_RADIUS_PX;\n}\n\n/** How long after a click something must happen for the click not to be \"dead\". */\nexport const DEAD_CLICK_WAIT_MS = 500;\n\nexport interface ClickTargetFacts {\n /** Lowercase tag name of the clicked element. */\n tag: string;\n /** The element or an ancestor is a link/button/[role=button…]/[onclick]/summary. */\n insideInteractive: boolean;\n /** Text fields, selects, labels, contenteditable: a click there focuses, nothing else to expect. */\n isFormField: boolean;\n /** Computed cursor is \"pointer\": the page tells the user it is clickable. */\n pointerCursor: boolean;\n /** Text is selected after the click (the user was selecting, not clicking). */\n hasSelection: boolean;\n}\n\n/**\n * A click can only be dead when the element promised something: it is (inside) an\n * interactive element or shows a pointer cursor. A click on plain text or on a\n * background is not friction, whatever happens after it.\n */\nexport function isDeadClickCandidate(f: ClickTargetFacts): boolean {\n if (f.isFormField || f.hasSelection) return false;\n if (f.tag === \"html\" || f.tag === \"body\") return false;\n return f.insideInteractive || f.pointerCursor;\n}\n\nexport interface AfterClickReaction {\n /** DOM mutations observed in the wait window. */\n mutations: number;\n urlChanged: boolean;\n scrolled: boolean;\n focusChanged: boolean;\n /** The page is unloading (a navigation or a download started). */\n unloading: boolean;\n}\n\n/** Nothing reacted within DEAD_CLICK_WAIT_MS: the click was dead. */\nexport function isDeadClick(r: AfterClickReaction): boolean {\n return r.mutations === 0 && !r.urlChanged && !r.scrolled && !r.focusChanged && !r.unloading;\n}\n\n/** Active time: input gaps up to ACTIVE_GAP_MS count, longer gaps are idle. */\nexport const ACTIVE_GAP_MS = 5000;\n\nexport function activeIncrement(lastInputTs: number, ts: number): number {\n if (!lastInputTs) return 0;\n const gap = ts - lastInputTs;\n return gap > 0 && gap <= ACTIVE_GAP_MS ? gap : 0;\n}\n","// What this page does about recording, from what /init said and the session state.\n// Pure, so the whole decision table is unit-tested.\n\nimport type { ReplayKitTrigger } from \"./protocol\";\nimport type { SessionState } from \"./state\";\n\nexport type PageMode =\n /** Nothing is recorded on this page (signals may still flow). */\n | { kind: \"off\" }\n /** requireConsent and the banner has not said yes yet. */\n | { kind: \"wait_consent\" }\n /** A recording of this session is already running: continue it. */\n | { kind: \"continue\" }\n /** Start a recording now, for this reason. */\n | { kind: \"start\"; trigger: ReplayKitTrigger }\n /** Keep the pre-trigger buffer and wait for a trigger. */\n | { kind: \"buffer\" };\n\nexport interface DecideInput {\n /** /init answered record: true. */\n record: boolean;\n /** CompressionStream exists (chunks are gzip'd in the browser). */\n canGzip: boolean;\n state: SessionState;\n /** Path is in excludePaths. */\n excluded: boolean;\n requireConsent: boolean;\n /** Consent stored by ReplayKit.consent(): true, false, or never given. */\n consent: boolean | null;\n forceTrigger: ReplayKitTrigger | null;\n sampled: boolean;\n /** Path matched checkoutPaths and \"checkout\" is an enabled trigger. */\n checkout: boolean;\n}\n\n/** Triggers that open a recording however short the visit is (same list as the API's). */\nexport const MIN_DURATION_EXEMPT: readonly ReplayKitTrigger[] = [\"error\", \"rage_click\", \"manual\"];\n\n/**\n * How long trigger `t` still has to wait before it may open a recording, given how long the\n * session has been on screen. 0 = open it now. A bounce never reaches the minimum, so it never\n * opens a recording nor costs quota; an error or a rage click does not wait.\n */\nexport function waitBeforeRecording(t: ReplayKitTrigger, visibleMs: number, minDurationMs: number): number {\n if (minDurationMs <= 0 || MIN_DURATION_EXEMPT.includes(t)) return 0;\n return Math.max(0, minDurationMs - visibleMs);\n}\n\nexport function decidePage(i: DecideInput): PageMode {\n // An explicit ReplayKit.consent(false) wins even when the project does not require consent.\n if (!i.record || !i.canGzip || i.state.stopped || i.excluded || i.consent === false) return { kind: \"off\" };\n if (i.requireConsent && i.consent !== true) return { kind: \"wait_consent\" };\n if (i.state.rid) return { kind: \"continue\" };\n if (i.forceTrigger) return { kind: \"start\", trigger: i.forceTrigger };\n if (i.checkout) return { kind: \"start\", trigger: \"checkout\" };\n if (i.sampled) return { kind: \"start\", trigger: \"sample\" };\n return { kind: \"buffer\" };\n}\n","// Calls to the ingest (POST {apiUrl}/api/t/r/<op>?apiKey=...), text/plain JSON so\n// they are \"simple\" CORS requests with no preflight, plus the PUT of a chunk to R2.\n//\n// Every response is reduced to an outcome the client acts on (see the contract in\n// packages/shared/src/replaykit.ts):\n// ok 2xx\n// drop 400/413: this body is unusable, forget it\n// stop 401: bad key / origin not allowed, ReplayKit stops for this page\n// off 403: rollout, plan or this op is off for the project\n// quota 429: no more recordings this session (signals continue)\n// retry 5xx, network error, or no fetch at all\n\nimport type { Env } from \"./env\";\nimport type {\n ReplayKitChunkRequest,\n ReplayKitChunkResponse,\n ReplayKitInitRequest,\n ReplayKitInitResponse,\n ReplayKitSignalsRequest,\n} from \"./protocol\";\n\nexport type Outcome = \"ok\" | \"drop\" | \"stop\" | \"off\" | \"quota\" | \"retry\";\n\nexport function outcomeOf(status: number): Outcome {\n if (status >= 200 && status < 300) return \"ok\";\n if (status === 401) return \"stop\";\n if (status === 403) return \"off\";\n if (status === 429) return \"quota\";\n if (status === 400 || status === 413 || status === 404 || status === 422) return \"drop\";\n return \"retry\";\n}\n\nexport interface Result<T> {\n outcome: Outcome;\n data?: T;\n}\n\nexport class Api {\n constructor(\n private readonly env: Env,\n private readonly apiUrl: string,\n private readonly apiKey: string,\n ) {}\n\n url(op: \"init\" | \"chunk\" | \"signals\"): string {\n return `${this.apiUrl}/api/t/r/${op}?apiKey=${encodeURIComponent(this.apiKey)}`;\n }\n\n init(body: ReplayKitInitRequest): Promise<Result<ReplayKitInitResponse>> {\n return this.post<ReplayKitInitResponse>(\"init\", body, true);\n }\n\n chunk(body: ReplayKitChunkRequest): Promise<Result<ReplayKitChunkResponse>> {\n return this.post<ReplayKitChunkResponse>(\"chunk\", body, true);\n }\n\n signals(body: ReplayKitSignalsRequest): Promise<Result<void>> {\n return this.post<void>(\"signals\", body, false);\n }\n\n /** On page hide: fire-and-forget. False when the beacon could not be queued. */\n beaconSignals(body: ReplayKitSignalsRequest): boolean {\n return !!this.env.sendBeacon?.(this.url(\"signals\"), JSON.stringify(body));\n }\n\n /** PUT the gzip'd chunk to the presigned R2 URL. R2 answers 200; 403 = URL expired or bad signature. */\n async put(url: string, blob: Blob): Promise<Outcome> {\n if (!this.env.fetch) return \"retry\";\n try {\n const res = await this.env.fetch(url, { method: \"PUT\", body: blob, headers: { \"Content-Type\": \"application/gzip\" } });\n if (res.status >= 200 && res.status < 300) return \"ok\";\n // An expired/rejected signature is fixed by asking for a new URL, not by dropping the chunk.\n return res.status === 400 || res.status === 413 ? \"drop\" : \"retry\";\n } catch {\n return \"retry\";\n }\n }\n\n private async post<T>(op: \"init\" | \"chunk\" | \"signals\", body: unknown, wantJson: boolean): Promise<Result<T>> {\n if (!this.env.fetch) return { outcome: \"retry\" };\n try {\n const res = await this.env.fetch(this.url(op), {\n method: \"POST\",\n body: JSON.stringify(body),\n headers: { \"Content-Type\": \"text/plain\" },\n keepalive: false,\n credentials: \"omit\",\n });\n const outcome = outcomeOf(res.status);\n if (outcome !== \"ok\" || !wantJson) return { outcome };\n try {\n return { outcome, data: (await res.json()) as T };\n } catch {\n return { outcome: \"retry\" };\n }\n } catch {\n return { outcome: \"retry\" };\n }\n }\n}\n\nexport const BASE_BACKOFF_MS = 2_000;\nexport const MAX_BACKOFF_MS = 60_000;\n\nexport function backoff(failures: number): number {\n return Math.min(MAX_BACKOFF_MS, BASE_BACKOFF_MS * 2 ** Math.max(0, failures - 1));\n}\n","// Signals out: batched POST /api/t/r/signals every FLUSH_MS (or at a full batch),\n// sendBeacon for whatever is left when the page hides.\n//\n// Signals are cheap and never count against the quota, but a page in a rage loop\n// should not turn into a request loop: at most MAX_PER_PAGE signals per page load,\n// a full batch per request, and identical signals within DEDUPE_MS folded.\n\nimport type { Env } from \"./env\";\nimport { REPLAYKIT_MAX_SIGNALS_PER_BATCH, type ReplayKitSignal } from \"./protocol\";\nimport { backoff, type Api } from \"./transport\";\n\nexport const SIGNAL_FLUSH_MS = 5_000;\nexport const MAX_PER_PAGE = 200;\nconst MAX_QUEUED = 500;\nconst DEDUPE_MS = 2_000;\n\nexport class SignalQueue {\n private queue: ReplayKitSignal[] = [];\n private timer: unknown = null;\n private sentThisPage = 0;\n private failures = 0;\n private busy = false;\n private last = new Map<string, number>();\n /** Ingest said no (403 off, 401 bad key): nothing more this page. */\n disabled = false;\n /** Init has not answered yet: keep collecting, send nothing. */\n held = true;\n\n constructor(\n private readonly env: Env,\n private readonly api: Api,\n private readonly ids: () => { sid: string; vid: string; rid?: string },\n ) {}\n\n add(s: ReplayKitSignal): void {\n if (this.disabled || this.sentThisPage + this.queue.length >= MAX_PER_PAGE) return;\n const key = `${s.type}|${s.url}|${s.selector || \"\"}|${s.detail || \"\"}`;\n const prev = this.last.get(key);\n if (prev !== undefined && s.ts - prev < DEDUPE_MS) return;\n this.last.set(key, s.ts);\n this.queue.push(s);\n if (this.queue.length > MAX_QUEUED) this.queue.shift();\n if (this.held) return;\n if (this.queue.length >= REPLAYKIT_MAX_SIGNALS_PER_BATCH) this.schedule(0);\n else if (!this.timer) this.schedule(SIGNAL_FLUSH_MS);\n }\n\n /** Init answered: start sending (signals on) or drop everything (off). */\n release(enabled: boolean): void {\n this.held = false;\n if (!enabled) {\n this.disable();\n return;\n }\n if (this.queue.length) this.schedule(0);\n }\n\n disable(): void {\n this.disabled = true;\n this.queue = [];\n if (this.timer) this.env.clearTimer(this.timer);\n this.timer = null;\n }\n\n private schedule(ms: number): void {\n if (this.timer) this.env.clearTimer(this.timer);\n this.timer = this.env.setTimer(() => {\n this.timer = null;\n void this.flush();\n }, ms);\n }\n\n async flush(): Promise<void> {\n if (this.busy || this.held || this.disabled) return;\n this.busy = true;\n try {\n while (this.queue.length && !this.disabled) {\n const batch = this.queue.slice(0, REPLAYKIT_MAX_SIGNALS_PER_BATCH);\n const r = await this.api.signals({ ...this.ids(), signals: batch });\n if (r.outcome === \"ok\" || r.outcome === \"drop\" || r.outcome === \"quota\") {\n // 429 on signals would be a server bug (they never count): drop the batch, don't loop.\n this.queue.splice(0, batch.length);\n this.sentThisPage += batch.length;\n this.failures = 0;\n continue;\n }\n if (r.outcome === \"off\" || r.outcome === \"stop\") {\n this.disable();\n return;\n }\n this.failures++;\n this.schedule(backoff(this.failures));\n return;\n }\n } finally {\n this.busy = false;\n }\n }\n\n /** Page hide: beacon what is left (no status to read; a lost beacon loses a few signals). */\n beacon(): void {\n if (this.held || this.disabled || !this.queue.length) return;\n while (this.queue.length) {\n const batch = this.queue.slice(0, REPLAYKIT_MAX_SIGNALS_PER_BATCH);\n if (!this.api.beaconSignals({ ...this.ids(), signals: batch })) return;\n this.queue.splice(0, batch.length);\n this.sentThisPage += batch.length;\n }\n }\n\n get size(): number {\n return this.queue.length;\n }\n}\n","// DOM side of the friction signals: listeners that turn real clicks, errors,\n// navigations and forms into ReplayKitSignal values. The decisions themselves\n// (what is a rage click, what is a dead click) live in detect.ts.\n//\n// Passive capture-phase listeners only; nothing is patched. The MutationObserver a\n// dead-click check needs is attached for DEAD_CLICK_WAIT_MS after a candidate click\n// and disconnected right after, so a quiet page costs nothing.\n//\n// Never sent: what was typed. A clicked element's text is only read when it is not a\n// field and not masked (.hm-mask, [data-hm-mask], or maskAllText).\n\nimport {\n DEAD_CLICK_WAIT_MS,\n isDeadClick,\n isDeadClickCandidate,\n RAGE_WINDOW_MS,\n RageDetector,\n type ClickTargetFacts,\n} from \"./detect\";\nimport { stripUrl } from \"./match\";\nimport type { ReplayKitSignal } from \"./protocol\";\n\nexport interface SignalHooks {\n now(): number;\n /** Text of clicked elements is withheld (config maskAllText). */\n maskAllText(): boolean;\n signal(s: ReplayKitSignal): void;\n /** A trigger fired: the 3rd click of a rage burst, or a JS error. */\n trigger(t: \"rage_click\" | \"error\"): void;\n /** Any click / key / scroll, for active time and click counts. */\n input(kind: \"click\" | \"key\" | \"scroll\", ts: number): void;\n /** The URL changed without a page load (SPA navigation). */\n navigated(url: string): void;\n}\n\nconst INTERACTIVE = 'a[href],button,summary,[role=\"button\"],[role=\"link\"],[role=\"tab\"],[role=\"menuitem\"],[role=\"checkbox\"],[role=\"switch\"],[role=\"option\"],[onclick]';\nconst FIELD = \"input,textarea,select,label,option,[contenteditable],[contenteditable=true]\";\nconst MASKED = \".hm-mask,[data-hm-mask],[data-hm-block],.hm-block\";\nconst MAX_TEXT = 80;\nconst MAX_DETAIL = 200;\n\n/** Short CSS path: up to 3 levels, stops at an id. Hashed class names (css-1x2y3z) are skipped. */\nexport function selectorOf(el: Element | null): string {\n const parts: string[] = [];\n let cur: Element | null = el;\n for (let depth = 0; cur && depth < 3 && cur.nodeType === 1; depth++) {\n const tag = cur.tagName.toLowerCase();\n if (tag === \"html\" || tag === \"body\") break;\n if (cur.id && /^[A-Za-z][\\w-]{0,40}$/.test(cur.id) && !/\\d{3,}/.test(cur.id)) {\n parts.unshift(`${tag}#${cur.id}`);\n break;\n }\n const classes = (typeof cur.className === \"string\" ? cur.className : \"\")\n .split(/\\s+/)\n .filter((c) => c && c.length < 30 && !/\\d{3,}|^css-|^sc-|__/.test(c))\n .slice(0, 2);\n parts.unshift(tag + classes.map((c) => \".\" + c).join(\"\"));\n cur = cur.parentElement;\n }\n return parts.join(\" > \");\n}\n\nfunction visibleText(el: Element, maskAll: boolean): string | undefined {\n if (maskAll || el.closest(MASKED) || el.closest(FIELD)) return undefined;\n const t = ((el as HTMLElement).innerText ?? el.textContent ?? \"\").replace(/\\s+/g, \" \").trim();\n if (!t) return (el.getAttribute(\"aria-label\") || el.getAttribute(\"title\") || undefined)?.slice(0, MAX_TEXT);\n return t.slice(0, MAX_TEXT);\n}\n\nfunction targetFacts(el: Element): ClickTargetFacts {\n let pointer = false;\n try {\n pointer = getComputedStyle(el).cursor === \"pointer\";\n } catch {\n pointer = false;\n }\n const sel = typeof getSelection === \"function\" ? getSelection() : null;\n return {\n tag: el.tagName.toLowerCase(),\n insideInteractive: !!el.closest(INTERACTIVE),\n isFormField: !!el.closest(FIELD),\n pointerCursor: pointer,\n hasSelection: !!sel && !sel.isCollapsed && sel.toString().trim().length > 0,\n };\n}\n\n/** The element a click \"means\": the nearest interactive ancestor, else the target itself. */\nfunction clickedElement(target: EventTarget | null): Element | null {\n const el = target instanceof Element ? target : target instanceof Node ? target.parentElement : null;\n if (!el) return null;\n return el.closest(INTERACTIVE) || el;\n}\n\nexport class SignalCollector {\n private rage = new RageDetector();\n private rageEl: Element | null = null;\n private rageTimer: ReturnType<typeof setTimeout> | null = null;\n private lastHref = \"\";\n private hrefTimer: ReturnType<typeof setInterval> | null = null;\n private unloading = false;\n private lastScrollInput = 0;\n private forms = new Map<HTMLFormElement, Set<string>>();\n private errorsSeen = new Set<string>();\n private off: Array<() => void> = [];\n\n constructor(private readonly h: SignalHooks) {}\n\n start(): void {\n this.lastHref = location.href;\n const on = <K extends keyof WindowEventMap>(type: K, fn: (e: WindowEventMap[K]) => void, target: Window | Document = window) => {\n const opts = { capture: true, passive: true };\n target.addEventListener(type, fn as EventListener, opts);\n this.off.push(() => target.removeEventListener(type, fn as EventListener, opts));\n };\n on(\"click\", (e) => this.onClick(e));\n on(\"keydown\", () => this.h.input(\"key\", this.h.now()));\n on(\"scroll\", () => {\n const now = this.h.now();\n if (now - this.lastScrollInput > 250) {\n this.lastScrollInput = now;\n this.h.input(\"scroll\", now);\n }\n });\n on(\"error\", (e) => this.onError(e.message || (e.error as Error | undefined)?.message || \"\"));\n on(\"unhandledrejection\", (e) => {\n const r = e.reason as { message?: string } | string | undefined;\n this.onError(typeof r === \"string\" ? r : r?.message || \"Unhandled promise rejection\");\n });\n on(\"popstate\", () => this.backNav());\n on(\"input\", (e) => this.onField(e.target));\n on(\"change\", (e) => this.onField(e.target));\n on(\"submit\", (e) => {\n if (e.target instanceof HTMLFormElement) this.forms.delete(e.target);\n });\n on(\"pageshow\", () => {\n this.unloading = false;\n });\n // SPA navigations: no patching of history, a cheap href poll instead.\n this.hrefTimer = setInterval(() => this.checkHref(), 1000);\n // Arrived here with the back/forward button.\n try {\n const nav = performance.getEntriesByType(\"navigation\")[0] as PerformanceNavigationTiming | undefined;\n if (nav?.type === \"back_forward\") this.emit({ type: \"back_nav\", detail: stripUrl(location.href) });\n } catch {\n // Navigation Timing unavailable.\n }\n }\n\n stop(): void {\n for (const f of this.off) f();\n this.off = [];\n if (this.hrefTimer) clearInterval(this.hrefTimer);\n if (this.rageTimer) clearTimeout(this.rageTimer);\n this.hrefTimer = this.rageTimer = null;\n }\n\n /** Called by the client on pagehide, before it beacons the queue: report what the page leaves behind. */\n pageHide(): void {\n this.unloading = true;\n this.flushForms();\n this.flushRage(true);\n }\n\n private emit(s: Omit<ReplayKitSignal, \"ts\" | \"url\"> & { ts?: number }): void {\n this.h.signal({ ts: s.ts ?? this.h.now(), url: stripUrl(location.href), ...s } as ReplayKitSignal);\n }\n\n private checkHref(): void {\n const href = location.href;\n if (href === this.lastHref) return;\n const prev = this.lastHref;\n this.lastHref = href;\n if (stripUrl(href) !== stripUrl(prev)) this.h.navigated(stripUrl(href));\n }\n\n private onClick(e: MouseEvent): void {\n const ts = this.h.now();\n this.h.input(\"click\", ts);\n const el = clickedElement(e.target);\n if (!el) return;\n const selector = selectorOf(el);\n const r = this.rage.onClick({ x: e.clientX, y: e.clientY, ts, target: selector });\n if (r.finished) this.reportRage(r.finished.count, r.finished.first.ts);\n if (r.trigger) this.h.trigger(\"rage_click\");\n this.rageEl = this.rage.currentCount() === 1 ? el : this.rageEl;\n if (this.rageTimer) clearTimeout(this.rageTimer);\n this.rageTimer = setTimeout(() => this.flushRage(false), RAGE_WINDOW_MS + 50);\n if (this.rage.currentCount() === 1) this.watchDeadClick(el, selector, ts);\n }\n\n private flushRage(force: boolean): void {\n const b = this.rage.flush(force ? Number.MAX_SAFE_INTEGER : this.h.now());\n if (b) this.reportRage(b.count, b.first.ts);\n }\n\n private reportRage(count: number, ts: number): void {\n const el = this.rageEl;\n this.emit({ type: \"rage_click\", ts, count, selector: el ? selectorOf(el) : undefined, text: el ? visibleText(el, this.h.maskAllText()) : undefined });\n }\n\n private watchDeadClick(el: Element, selector: string, ts: number): void {\n if (!isDeadClickCandidate(targetFacts(el))) return;\n const hrefBefore = location.href;\n const scrollBefore = `${scrollX},${scrollY}`;\n const focusBefore = document.activeElement;\n let mutations = 0;\n let mo: MutationObserver | null = null;\n try {\n mo = new MutationObserver((list) => {\n mutations += list.length;\n });\n mo.observe(document.documentElement, { subtree: true, childList: true, attributes: true, characterData: true });\n } catch {\n return;\n }\n setTimeout(() => {\n mo?.disconnect();\n // Part of a rage burst: reported as a rage click, not also as a dead one.\n if (this.rage.currentCount() > 1) return;\n const dead = isDeadClick({\n mutations,\n urlChanged: location.href !== hrefBefore,\n scrolled: `${scrollX},${scrollY}` !== scrollBefore,\n focusChanged: document.activeElement !== focusBefore && document.activeElement !== el,\n unloading: this.unloading,\n });\n if (dead) this.emit({ type: \"dead_click\", ts, selector, text: visibleText(el, this.h.maskAllText()) });\n }, DEAD_CLICK_WAIT_MS);\n }\n\n private onError(message: string): void {\n const detail = (message || \"Error\").slice(0, MAX_DETAIL);\n if (this.errorsSeen.has(detail)) return;\n this.errorsSeen.add(detail);\n this.emit({ type: \"error\", detail });\n this.h.trigger(\"error\");\n }\n\n private backNav(): void {\n this.emit({ type: \"back_nav\", detail: stripUrl(location.href) });\n this.checkHref();\n }\n\n private onField(target: EventTarget | null): void {\n if (!(target instanceof Element)) return;\n const form = target.closest(\"form\");\n if (!form) return;\n const key = target.getAttribute(\"name\") || target.id || selectorOf(target);\n let set = this.forms.get(form);\n if (!set) this.forms.set(form, (set = new Set()));\n set.add(key);\n }\n\n /** Forms touched and never submitted, reported when the page goes away. */\n private flushForms(): void {\n for (const [form, fields] of this.forms) {\n if (!form.isConnected && fields.size === 0) continue;\n this.emit({ type: \"form_abandon\", selector: selectorOf(form), count: fields.size });\n }\n this.forms.clear();\n }\n}\n","// rrweb, loaded only when this page may record.\n//\n// The ESM build turns the dynamic import into a separate chunk, so a page that only\n// sends signals never downloads rrweb (~24 KB gzip). The IIFE build bundles it.\n//\n// Privacy defaults that config cannot turn off:\n// - every input value is masked (maskAllInputs), passwords and card fields included;\n// - card fields ([autocomplete^=\"cc-\"]) and [data-hm-block] / .hm-block are blocked:\n// replaced by an empty box of the same size, their content never leaves the page;\n// - text marked .hm-mask / [data-hm-mask] is masked; maskAllText masks every text node;\n// - canvas, cross-origin iframes and inlined images are not recorded.\n\nimport type { RrEvent } from \"./buffer\";\nimport { CHECKOUT_MS, PAGE_TAG } from \"./buffer\";\n\nexport interface RecorderOptions {\n blockSelectors: string[];\n maskAllText: boolean;\n emit(e: RrEvent): void;\n}\n\ntype RecordFn = typeof import(\"@rrweb/record\").record;\n\nconst BASE_BLOCK = ['[data-hm-block]', \".hm-block\", '[autocomplete^=\"cc-\"]', 'input[type=\"password\"]'];\nconst BASE_MASK = \".hm-mask,[data-hm-mask]\";\n\nlet recordFn: Promise<RecordFn> | null = null;\n\nfunction loadRecord(): Promise<RecordFn> {\n return (recordFn ??= import(\"@rrweb/record\").then((m) => m.record));\n}\n\n/** Drops selectors the browser rejects: one bad entry in the config must not stop rrweb. */\nexport function validSelectors(list: string[]): string[] {\n return list.filter((s) => {\n if (!s || typeof s !== \"string\") return false;\n try {\n document.createDocumentFragment().querySelector(s);\n return true;\n } catch {\n return false;\n }\n });\n}\n\nexport class Recorder {\n private stopFn: (() => void) | undefined;\n private record: RecordFn | null = null;\n private stopped = false;\n\n async start(o: RecorderOptions): Promise<boolean> {\n try {\n this.record = await loadRecord();\n } catch {\n return false;\n }\n if (this.stopped) return false;\n try {\n this.stopFn = this.record({\n emit: (e) => o.emit(e as unknown as RrEvent),\n checkoutEveryNms: CHECKOUT_MS,\n maskAllInputs: true,\n maskInputOptions: { password: true },\n maskTextSelector: o.maskAllText ? \"*\" : BASE_MASK,\n blockSelector: validSelectors([...BASE_BLOCK, ...o.blockSelectors]).join(\",\"),\n recordCanvas: false,\n recordCrossOriginIframes: false,\n inlineImages: false,\n collectFonts: false,\n slimDOMOptions: { script: true, comment: true, headFavicon: true, headWhitespace: true, headMetaSocial: true, headMetaRobots: true, headMetaHttpEquiv: true, headMetaVerification: true, headMetaAuthorship: true },\n sampling: { mousemove: 50, scroll: 150, input: \"last\" },\n }) ?? undefined;\n return !!this.stopFn;\n } catch {\n return false;\n }\n }\n\n /** New full snapshot right now (the first event of a fresh recording must be one). */\n snapshot(): void {\n try {\n this.record?.takeFullSnapshot(true);\n } catch {\n // Not recording.\n }\n }\n\n /** Marks an SPA URL change in the stream (the player and the page list read it). */\n page(href: string): void {\n try {\n this.record?.addCustomEvent(PAGE_TAG, { href });\n } catch {\n // Not recording.\n }\n }\n\n stop(): void {\n this.stopped = true;\n try {\n this.stopFn?.();\n } catch {\n // Already stopped.\n }\n this.stopFn = undefined;\n }\n}\n","// Live recording upload: rrweb events → gzip'd chunk → presigned PUT straight to R2.\n//\n// Per chunk: POST /api/t/r/chunk (the API answers with a presigned URL and, on seq 0,\n// opens the recording and counts it in the quota), then PUT the gzip to R2. Nothing\n// of the recording passes through our servers.\n//\n// A chunk goes out every FLUSH_MS or once FLUSH_BYTES of raw JSON piled up. The two\n// requests cannot be trusted to finish while a page unloads, so on page hide the\n// unsent events (queued and in flight) are written to IndexedDB instead, and the\n// next page of the same session uploads them before its own. A session that never\n// comes back loses its last few seconds; nothing else is lost.\n//\n// seq advances only after R2 accepted the PUT, so a retry re-uses the same seq and\n// overwrites the same object: retries are idempotent end to end.\n\nimport type { Env } from \"./env\";\nimport { approxBytes, pagesOf, type RrEvent } from \"./buffer\";\nimport { REPLAYKIT_MAX_CHUNK_BYTES, REPLAYKIT_MAX_CHUNKS, REPLAYKIT_MAX_PAGES_PER_CHUNK, type ReplayKitChunkFile } from \"./protocol\";\nimport type { StateStore, SessionState, StopReason } from \"./state\";\nimport { backoff, type Api } from \"./transport\";\n\nexport const FLUSH_MS = 30_000;\nexport const FLUSH_BYTES = 256 * 1024;\n\nconst pendingKey = (sid: string) => `rk:pending:${sid}`;\n\ninterface Pending {\n rid: string;\n events: RrEvent[];\n}\n\nexport interface UploaderHooks {\n /** Recording must stop (quota, ReplayKit off for the project, chunk limit, unusable chunk). */\n onStop(reason: StopReason): void;\n /** Right before a chunk is requested: write the running stats into the session state. */\n beforeChunk?(): void;\n}\n\nexport class Uploader {\n private queue: RrEvent[] = [];\n private inflight: RrEvent[] = [];\n private queuedBytes = 0;\n private timer: unknown = null;\n private retryTimer: unknown = null;\n private failures = 0;\n private busy = false;\n private stopped = false;\n /** Bumped by pause(); a flush that started before it must not touch the new state. */\n private epoch = 0;\n /** A copy of the unsent events was parked in IndexedDB (page hidden); keep it in step with what lands. */\n private parked = false;\n /** Last page reported for `lastPageRid`: a checkout's repeated Meta href is not a new page. */\n private lastPage = \"\";\n private lastPageRid = \"\";\n\n constructor(\n private readonly env: Env,\n private readonly api: Api,\n private readonly states: StateStore,\n private readonly ids: { sid: string; vid: string },\n private readonly hooks: UploaderHooks,\n ) {}\n\n private state(): SessionState {\n return this.states.load(this.ids.sid);\n }\n\n /** Prepends what the previous page of this session could not send. */\n async restorePending(): Promise<void> {\n const store = this.env.store;\n if (!store) return;\n const p = await store.get<Pending>(pendingKey(this.ids.sid));\n if (!p) return;\n await store.del(pendingKey(this.ids.sid));\n const s = this.state();\n if (!s.rid || p.rid !== s.rid || !Array.isArray(p.events) || p.events.length === 0) return;\n this.queue = p.events.concat(this.queue);\n this.queuedBytes += p.events.reduce((n, e) => n + approxBytes(e), 0);\n this.schedule(0);\n }\n\n add(e: RrEvent): void {\n if (this.stopped) return;\n this.queue.push(e);\n this.queuedBytes += approxBytes(e);\n if (this.queuedBytes >= FLUSH_BYTES) this.schedule(0);\n else if (!this.timer) this.schedule(FLUSH_MS);\n }\n\n addMany(events: RrEvent[]): void {\n for (const e of events) this.add(e);\n }\n\n private schedule(ms: number): void {\n if (this.timer) this.env.clearTimer(this.timer);\n this.timer = this.env.setTimer(() => {\n this.timer = null;\n void this.flush();\n }, ms);\n }\n\n /** Uploads everything queued, one chunk at a time. Resolves when the queue is empty or blocked. */\n async flush(): Promise<void> {\n if (this.busy || this.stopped) return;\n const epoch = this.epoch;\n this.busy = true;\n try {\n while (!this.stopped && epoch === this.epoch && (this.inflight.length > 0 || this.queue.length > 0)) {\n if (this.inflight.length === 0) {\n this.inflight = this.queue;\n this.queue = [];\n this.queuedBytes = 0;\n }\n const ok = await this.sendInflight();\n if (!ok) return;\n }\n } finally {\n this.busy = false;\n }\n }\n\n /** True when the in-flight events are done with (sent or dropped); false to wait for a retry. */\n private async sendInflight(): Promise<boolean> {\n this.hooks.beforeChunk?.();\n const s = this.state();\n if (!s.rid || !s.trigger || !s.stats) return false;\n if (s.seq >= REPLAYKIT_MAX_CHUNKS) {\n this.stop(\"limit\");\n return false;\n }\n const gzip = this.env.gzip;\n if (!gzip) {\n this.stop(\"off\");\n return false;\n }\n const file: ReplayKitChunkFile = { v: 1, rid: s.rid, seq: s.seq, events: this.inflight };\n const blob = await gzip(JSON.stringify(file));\n if (blob.size > REPLAYKIT_MAX_CHUNK_BYTES) {\n // Too big for one chunk: send the first half now, the rest goes back to the head of the queue.\n if (this.inflight.length > 1) {\n const half = Math.ceil(this.inflight.length / 2);\n this.queue = this.inflight.slice(half).concat(this.queue);\n this.inflight = this.inflight.slice(0, half);\n return true;\n }\n // One event over 2 MB gzip'd (a giant full snapshot): unreplayable either way.\n this.stop(\"limit\");\n return false;\n }\n const prevPage = this.lastPageRid === s.rid ? this.lastPage : \"\";\n const pages = pagesOf(this.inflight, prevPage, REPLAYKIT_MAX_PAGES_PER_CHUNK);\n const presign = await this.api.chunk({ rid: s.rid, sid: this.ids.sid, vid: this.ids.vid, seq: s.seq, bytes: blob.size, trigger: s.trigger, stats: s.stats, pages });\n if (presign.outcome === \"drop\") {\n this.inflight = [];\n return true;\n }\n if (presign.outcome === \"quota\") return this.stop(\"quota\"), false;\n if (presign.outcome === \"off\" || presign.outcome === \"stop\") return this.stop(\"off\"), false;\n if (presign.outcome !== \"ok\" || !presign.data?.url) return this.retryLater(), false;\n const put = await this.api.put(presign.data.url, blob);\n if (put === \"drop\") {\n this.inflight = [];\n return true;\n }\n if (put !== \"ok\") return this.retryLater(), false;\n this.failures = 0;\n this.inflight = [];\n if (pages.length) {\n this.lastPage = pages[pages.length - 1].url;\n this.lastPageRid = s.rid;\n }\n const cur = this.state();\n if (cur.rid === s.rid) this.states.save({ ...cur, seq: s.seq + 1 });\n // Hidden tab whose upload still went through: the parked copy must not replay it on the next page.\n if (this.parked) {\n if (this.queue.length) this.persist();\n else this.unpersist();\n }\n return true;\n }\n\n private retryLater(): void {\n this.failures++;\n if (this.retryTimer) this.env.clearTimer(this.retryTimer);\n this.retryTimer = this.env.setTimer(() => {\n this.retryTimer = null;\n void this.flush();\n }, backoff(this.failures));\n }\n\n private stop(reason: StopReason): void {\n this.stopped = true;\n this.queue = [];\n this.inflight = [];\n this.queuedBytes = 0;\n this.clearTimers();\n this.hooks.onStop(reason);\n }\n\n private clearTimers(): void {\n if (this.timer) this.env.clearTimer(this.timer);\n if (this.retryTimer) this.env.clearTimer(this.retryTimer);\n this.timer = this.retryTimer = null;\n }\n\n /** Page hide: park unsent events for the next page of this session. */\n persist(): void {\n const store = this.env.store;\n const s = this.state();\n const events = this.inflight.concat(this.queue);\n if (!store || !s.rid || events.length === 0 || this.stopped) return;\n this.parked = true;\n void store.set(pendingKey(this.ids.sid), { rid: s.rid, events } satisfies Pending);\n }\n\n /** The page came back into view: whatever was parked is still in memory, drop the copy. */\n unpersist(): void {\n if (!this.parked) return;\n this.parked = false;\n void this.env.store?.del(pendingKey(this.ids.sid));\n }\n\n /** Stop without a reason the server gave (manual stop, consent withdrawn): drop what is queued. */\n discard(): void {\n this.epoch++;\n this.stopped = true;\n this.queue = [];\n this.inflight = [];\n this.queuedBytes = 0;\n this.clearTimers();\n this.unpersist();\n }\n\n get pendingEvents(): number {\n return this.queue.length + this.inflight.length;\n }\n}\n","// The ReplayKit client: one per page load.\n//\n// 1. waits for the tracker's visitor and session ids (read-only, identity.ts);\n// 2. starts the signal listeners at once, so errors during startup are caught,\n// holding what they find until /init answers;\n// 3. POST /api/t/r/init: signals on/off, recordings on/off, the project's config and\n// a server-side trigger (\"paying\");\n// 4. decides the page (decide.ts): off, continue the session's recording, start\n// one, or keep a pre-trigger buffer and wait for a trigger.\n//\n// Conditional recording: in buffer mode rrweb runs into an in-memory ring buffer cut\n// at full snapshots (buffer.ts), parked in IndexedDB when the page hides so a trigger\n// on the next page of the session still shows the page before. A trigger promotes\n// the buffer into a recording: a recording id is minted, the buffer becomes the\n// first chunk and every later event streams through the uploader.\n\nimport { emptyStats, StateStore, type SessionState, type StopReason } from \"./state\";\nimport { browserEnv, isBrowser, type Env } from \"./env\";\nimport { readIdentity } from \"./identity\";\nimport { inSample, matchAny, stripUrl } from \"./match\";\nimport { trimBuffer, BUFFER_WINDOW_MS, CHECKOUT_MS, EVENT_FULL_SNAPSHOT, type RrEvent } from \"./buffer\";\nimport { activeIncrement } from \"./detect\";\nimport { decidePage, waitBeforeRecording } from \"./decide\";\nimport { Api, backoff } from \"./transport\";\nimport { SignalQueue } from \"./signal-queue\";\nimport { SignalCollector } from \"./signals-dom\";\nimport { Recorder } from \"./recorder\";\nimport { Uploader } from \"./uploader\";\nimport type { ReplayKitClientConfig, ReplayKitInitResponse, ReplayKitRecordingStats, ReplayKitSignal, ReplayKitTrigger } from \"./protocol\";\n\nexport interface ReplayKitOptions {\n /** Your project's public API key (the same one the tracker uses). */\n apiKey: string;\n /** Defaults to https://app.himetrica.com. */\n apiUrl?: string;\n /** Run on localhost (off by default, like the tracker). */\n allowLocalhost?: boolean;\n /** Log decisions to the console. */\n debug?: boolean;\n}\n\nconst CONSENT_KEY = \"hm_rk_consent\";\nconst bufferKey = (sid: string) => `rk:buf:${sid}`;\nconst IDENTITY_POLL_MS = 500;\nconst IDENTITY_WAIT_MS = 15_000;\nconst INIT_ATTEMPTS = 3;\n\ntype Mode = \"idle\" | \"off\" | \"wait_consent\" | \"buffer\" | \"live\";\n\ninterface ParkedBuffer {\n at: number;\n events: RrEvent[];\n}\n\nexport class ReplayKitClient {\n private readonly env: Env;\n private readonly api: Api;\n private readonly states: StateStore;\n private ids: { sid: string; vid: string } | null = null;\n private queue: SignalQueue | null = null;\n private collector: SignalCollector | null = null;\n private recorder: Recorder | null = null;\n private uploader: Uploader | null = null;\n private init: ReplayKitInitResponse | null = null;\n private config: ReplayKitClientConfig | null = null;\n private mode: Mode = \"idle\";\n private buffer: RrEvent[] = [];\n /** Clicks, signals and input times while buffering, folded into the stats on promotion. */\n private recent: Array<{ k: \"click\" | \"rage\" | \"dead\" | \"error\" | \"input\"; ts: number }> = [];\n private stats: ReplayKitRecordingStats | null = null;\n private lastInputTs = 0;\n /** A trigger that fired before /init answered. */\n private earlyTrigger: ReplayKitTrigger | null = null;\n /** A trigger waiting for the session to reach minDurationMs on screen. */\n private pending: ReplayKitTrigger | null = null;\n /** Bumped on every wait scheduled, so only the latest timer acts. */\n private pendingGen = 0;\n /** Visible time of the session's earlier pages (state.visibleMs, read once ids are known). */\n private baseVisibleMs = 0;\n /** Visible time of this page so far, excluding the current visible stretch. */\n private pageVisibleMs = 0;\n /** Start of the current visible stretch, null while the tab is hidden. */\n private visibleSince: number | null = null;\n private destroyed = false;\n private offWindow: Array<() => void> = [];\n\n constructor(private readonly options: ReplayKitOptions, env: Env = browserEnv()) {\n this.env = env;\n const apiUrl = (options.apiUrl || \"https://app.himetrica.com\").replace(/\\/+$/, \"\");\n this.api = new Api(env, apiUrl, options.apiKey);\n this.states = new StateStore(env);\n }\n\n private log(...args: unknown[]): void {\n if (this.options.debug) console.debug(\"[ReplayKit]\", ...args);\n }\n\n /** Starts everything. Safe to call once per page; no-op outside a browser. */\n start(): void {\n if (!isBrowser || !this.options.apiKey) return;\n const host = location.hostname;\n if (!this.options.allowLocalhost && (host === \"localhost\" || host === \"127.0.0.1\" || host === \"[::1]\" || host.endsWith(\".localhost\"))) {\n this.log(\"localhost: idle (allowLocalhost to enable)\");\n return;\n }\n this.trackVisibility();\n const started = this.env.now();\n const tryIds = () => {\n if (this.destroyed) return;\n const id = readIdentity(this.env);\n if (id.sessionId && id.visitorId) {\n this.ids = { sid: id.sessionId, vid: id.visitorId };\n this.boot();\n } else if (this.env.now() - started < IDENTITY_WAIT_MS) {\n this.env.setTimer(tryIds, IDENTITY_POLL_MS);\n } else {\n this.log(\"no tracker ids on this page: idle\");\n }\n };\n tryIds();\n }\n\n private boot(): void {\n const ids = this.ids!;\n this.baseVisibleMs = this.states.load(ids.sid).visibleMs ?? 0;\n this.queue = new SignalQueue(this.env, this.api, () => ({ ...ids, rid: this.mode === \"live\" ? this.state().rid : undefined }));\n this.collector = new SignalCollector({\n now: () => this.env.now(),\n maskAllText: () => this.config?.maskAllText ?? true,\n signal: (s) => this.onSignal(s),\n trigger: (t) => this.trigger(t),\n input: (kind, ts) => this.onInput(kind, ts),\n navigated: (url) => this.onNavigate(url),\n });\n this.collector.start();\n this.listenPage();\n void this.callInit(1);\n }\n\n private state(): SessionState {\n return this.states.load(this.ids!.sid);\n }\n\n private async callInit(attempt: number): Promise<void> {\n const ids = this.ids!;\n const r = await this.api.init({ sid: ids.sid, vid: ids.vid, url: stripUrl(location.href) });\n if (this.destroyed) return;\n if (r.outcome === \"ok\" && r.data) return this.applyInit(r.data);\n if (r.outcome === \"retry\" && attempt < INIT_ATTEMPTS) {\n this.env.setTimer(() => void this.callInit(attempt + 1), backoff(attempt));\n return;\n }\n this.log(\"init refused:\", r.outcome);\n if (r.outcome === \"quota\") {\n // Quota is about recordings; signals never count.\n this.queue?.release(true);\n this.mode = \"off\";\n return;\n }\n this.shutdown();\n }\n\n private applyInit(res: ReplayKitInitResponse): void {\n this.init = res;\n this.config = res.config;\n this.queue!.release(res.signals);\n if (!res.signals && !res.record) {\n this.shutdown();\n return;\n }\n this.decide();\n }\n\n private consentValue(): boolean | null {\n const v = this.env.getLocal(CONSENT_KEY);\n return v === \"1\" ? true : v === \"0\" ? false : null;\n }\n\n private triggerEnabled(t: ReplayKitTrigger): boolean {\n if (t === \"manual\" || t === \"sample\") return true;\n return !!this.config?.triggers.includes(t as ReplayKitClientConfig[\"triggers\"][number]);\n }\n\n private pathExcluded(): boolean {\n return matchAny(this.config?.excludePaths ?? [], location.pathname);\n }\n\n private onCheckoutPath(): boolean {\n return this.triggerEnabled(\"checkout\") && matchAny(this.config?.checkoutPaths ?? [], location.pathname);\n }\n\n private decide(): void {\n if (!this.init || !this.config) return;\n const s = this.state();\n const mode = decidePage({\n record: this.init.record,\n canGzip: !!this.env.gzip,\n state: s,\n excluded: this.pathExcluded(),\n requireConsent: this.config.requireConsent,\n consent: this.consentValue(),\n forceTrigger: this.init.forceTrigger,\n sampled: inSample(this.ids!.sid, this.config.sampleRate),\n checkout: this.onCheckoutPath(),\n });\n this.log(\"page:\", mode.kind, mode.kind === \"start\" ? mode.trigger : \"\");\n if (mode.kind === \"off\" || mode.kind === \"wait_consent\") {\n this.mode = mode.kind;\n return;\n }\n if (mode.kind === \"continue\") {\n this.mode = \"live\";\n const st = s.stats ?? emptyStats(this.env.now(), stripUrl(location.href));\n this.stats = { ...st, pages: st.pages + 1 };\n this.makeUploader();\n void this.uploader!.restorePending();\n void this.startRecorder();\n return;\n }\n this.mode = \"buffer\";\n void this.restoreParkedBuffer().then(async () => {\n if (this.mode !== \"buffer\") return;\n // rrweb emits the page's full snapshot synchronously on start: it lands in the buffer\n // before a promotion, so the first chunk always opens with a replayable page.\n await this.startRecorder();\n if (this.mode !== \"buffer\") return;\n const t = mode.kind === \"start\" ? mode.trigger : this.earlyTrigger;\n this.earlyTrigger = null;\n if (t && this.triggerEnabled(t)) this.requestPromote(t);\n });\n }\n\n private async restoreParkedBuffer(): Promise<void> {\n const store = this.env.store;\n if (!store) return;\n const key = bufferKey(this.ids!.sid);\n const parked = await store.get<ParkedBuffer>(key);\n if (!parked) return;\n void store.del(key);\n const fresh = this.env.now() - parked.at < BUFFER_WINDOW_MS + CHECKOUT_MS;\n if (fresh && Array.isArray(parked.events) && parked.events.length) this.buffer = parked.events.concat(this.buffer);\n }\n\n private async startRecorder(): Promise<void> {\n if (this.recorder) return;\n const rec = (this.recorder = new Recorder());\n const ok = await rec.start({\n blockSelectors: this.config?.blockSelectors ?? [],\n maskAllText: this.config?.maskAllText ?? false,\n emit: (e) => this.onEvent(e),\n });\n if (!ok && this.recorder === rec) {\n this.log(\"rrweb failed to start\");\n this.recorder = null;\n this.mode = \"off\";\n }\n }\n\n private onEvent(e: RrEvent): void {\n if (this.mode === \"buffer\") {\n this.buffer.push(e);\n // Trim only when a new checkout lands: the only moment a cut point appears.\n if (e.type === EVENT_FULL_SNAPSHOT) this.buffer = trimBuffer(this.buffer, this.env.now());\n return;\n }\n if (this.mode === \"live\" && this.uploader) {\n if (this.stats) this.stats.endedAt = Math.max(this.stats.endedAt, e.timestamp);\n this.uploader.add(e);\n }\n }\n\n private makeUploader(): void {\n this.uploader = new Uploader(this.env, this.api, this.states, this.ids!, {\n onStop: (reason) => this.stopRecording(reason),\n beforeChunk: () => this.saveStats(),\n });\n }\n\n private saveStats(): void {\n if (!this.stats || this.mode !== \"live\") return;\n const s = this.state();\n if (s.rid) this.states.save({ ...s, stats: this.stats });\n }\n\n /** A trigger fired: turn the buffer into a recording, or remember it until /init answers. */\n trigger(t: ReplayKitTrigger): void {\n if (this.destroyed || !this.ids) return;\n if (!this.init) {\n if (!this.earlyTrigger) this.earlyTrigger = t;\n return;\n }\n if (this.mode !== \"buffer\" || !this.triggerEnabled(t)) return;\n if (!this.recorder) {\n // Still restoring the parked buffer: the buffer setup promotes it when it is done.\n this.earlyTrigger ??= t;\n return;\n }\n this.requestPromote(t);\n }\n\n // ── Minimum duration ──────────────────────────────────────\n\n private trackVisibility(): void {\n if (document.visibilityState === \"visible\") this.visibleSince = this.env.now();\n const onVis = () => {\n const now = this.env.now();\n if (document.visibilityState === \"hidden\") {\n if (this.visibleSince !== null) this.pageVisibleMs += now - this.visibleSince;\n this.visibleSince = null;\n } else if (this.visibleSince === null) {\n this.visibleSince = now;\n // Hidden tabs do not count: the wait resumes where it stopped.\n if (this.pending) this.schedulePending();\n }\n };\n document.addEventListener(\"visibilitychange\", onVis);\n this.offWindow.push(() => document.removeEventListener(\"visibilitychange\", onVis));\n }\n\n /** Session time on screen: earlier pages + this one, the current visible stretch included. */\n private visibleMs(): number {\n const live = this.visibleSince !== null ? this.env.now() - this.visibleSince : 0;\n return this.baseVisibleMs + this.pageVisibleMs + live;\n }\n\n /** Open the recording now, or once the session has been on screen for minDurationMs. */\n private requestPromote(t: ReplayKitTrigger): void {\n const wait = waitBeforeRecording(t, this.visibleMs(), this.config?.minDurationMs ?? 0);\n if (wait === 0) {\n this.pending = null;\n this.promote(t);\n return;\n }\n // The first waiting trigger is the one the recording reports.\n this.pending ??= t;\n this.log(\"waiting for the minimum duration:\", t, wait, \"ms\");\n this.schedulePending();\n }\n\n private schedulePending(): void {\n const t = this.pending;\n if (!t) return;\n const gen = ++this.pendingGen;\n const wait = waitBeforeRecording(t, this.visibleMs(), this.config?.minDurationMs ?? 0);\n this.env.setTimer(() => {\n if (gen !== this.pendingGen || this.pending !== t || this.mode !== \"buffer\" || this.destroyed) return;\n // Hidden meanwhile: the visibilitychange handler reschedules when the tab comes back.\n if (this.visibleSince === null) return;\n this.requestPromote(t);\n }, Math.max(wait, 50));\n }\n\n private promote(t: ReplayKitTrigger): void {\n const now = this.env.now();\n // The buffer starts at a full snapshot; recordings begin there, so stats do too.\n const events = this.buffer;\n this.buffer = [];\n const startedAt = events.length ? events[0].timestamp : now;\n const stats = emptyStats(startedAt, stripUrl(location.href));\n // The visit lasted until now even if the page was quiet: a recording opened after the\n // minimum duration must not read as shorter than it (the API hides short ones).\n stats.endedAt = Math.max(events.length ? events[events.length - 1].timestamp : now, now);\n this.pending = null;\n let last = 0;\n for (const r of this.recent) {\n if (r.ts < startedAt) continue;\n if (r.k === \"click\") stats.clicks++;\n else if (r.k === \"rage\") stats.rageClicks++;\n else if (r.k === \"dead\") stats.deadClicks++;\n else if (r.k === \"error\") stats.errors++;\n if (r.k === \"click\" || r.k === \"input\") {\n stats.activeMs += activeIncrement(last, r.ts);\n last = r.ts;\n }\n }\n this.recent = [];\n this.stats = stats;\n this.states.save({ ...this.state(), rid: this.env.uuid(), seq: 0, trigger: t, stats, stopped: undefined });\n this.mode = \"live\";\n this.log(\"recording:\", t);\n this.makeUploader();\n this.uploader!.addMany(events);\n if (!events.length) this.recorder?.snapshot();\n // Open the recording right away: short sessions that end soon after a trigger are the interesting ones.\n void this.uploader!.flush();\n }\n\n private stopRecording(reason: StopReason): void {\n this.log(\"recording stopped:\", reason);\n this.recorder?.stop();\n this.recorder = null;\n this.mode = \"off\";\n this.buffer = [];\n const s = this.state();\n this.states.save({ ...s, stopped: reason });\n }\n\n private onSignal(s: ReplayKitSignal): void {\n if (this.mode === \"live\" && this.stats) {\n if (s.type === \"rage_click\") this.stats.rageClicks++;\n else if (s.type === \"dead_click\") this.stats.deadClicks++;\n else if (s.type === \"error\") this.stats.errors++;\n } else if (this.mode === \"buffer\" || this.mode === \"idle\") {\n const k = s.type === \"rage_click\" ? \"rage\" : s.type === \"dead_click\" ? \"dead\" : s.type === \"error\" ? \"error\" : null;\n if (k) this.pushRecent(k, s.ts);\n }\n this.queue?.add(s);\n }\n\n private onInput(kind: \"click\" | \"key\" | \"scroll\", ts: number): void {\n if (this.mode === \"live\" && this.stats) {\n this.stats.activeMs += activeIncrement(this.lastInputTs, ts);\n if (kind === \"click\") this.stats.clicks++;\n } else if (this.mode === \"buffer\" || this.mode === \"idle\") {\n this.pushRecent(kind === \"click\" ? \"click\" : \"input\", ts);\n }\n this.lastInputTs = ts;\n }\n\n private pushRecent(k: \"click\" | \"rage\" | \"dead\" | \"error\" | \"input\", ts: number): void {\n this.recent.push({ k, ts });\n if (this.recent.length > 1000) this.recent.splice(0, this.recent.length - 1000);\n }\n\n private onNavigate(url: string): void {\n if (!this.init || !this.config) return;\n if (this.mode === \"live\" && this.stats) this.stats.pages++;\n const excluded = this.pathExcluded();\n if (excluded && (this.mode === \"live\" || this.mode === \"buffer\")) {\n // SPA route that must not be recorded: pause rrweb, keep the session's state.\n this.recorder?.stop();\n this.recorder = null;\n if (this.mode === \"buffer\") this.buffer = [];\n void this.uploader?.flush();\n this.mode = \"off\";\n return;\n }\n if (!excluded && this.mode === \"off\" && !this.state().stopped) {\n // Back from an excluded route: decide again (continues the recording or rebuffers).\n this.uploader = null;\n this.decide();\n return;\n }\n // Excluded routes never reach here: their URL is not written into the stream.\n this.recorder?.page(url);\n if (this.onCheckoutPath()) this.trigger(\"checkout\");\n }\n\n private listenPage(): void {\n const hide = (unloading: boolean) => {\n if (unloading) this.collector?.pageHide();\n // The next page of the session continues the minimum-duration count from here.\n this.states.save({ ...this.state(), visibleMs: this.visibleMs() });\n this.queue?.beacon();\n if (this.mode === \"live\") {\n this.saveStats();\n this.uploader?.persist();\n } else if (this.mode === \"buffer\" && this.buffer.length && this.env.store) {\n const events = trimBuffer(this.buffer, this.env.now());\n void this.env.store.set(bufferKey(this.ids!.sid), { at: this.env.now(), events } satisfies ParkedBuffer);\n }\n };\n const onVis = () => {\n if (document.visibilityState === \"hidden\") hide(false);\n else this.uploader?.unpersist();\n };\n const onHide = () => hide(true);\n document.addEventListener(\"visibilitychange\", onVis);\n window.addEventListener(\"pagehide\", onHide);\n this.offWindow.push(\n () => document.removeEventListener(\"visibilitychange\", onVis),\n () => window.removeEventListener(\"pagehide\", onHide),\n );\n }\n\n // ── Public API ────────────────────────────────────────────\n\n /** Start recording this session now (ReplayKit.start()). Still subject to /init, consent and excludePaths. */\n manualStart(reason?: string): void {\n if (!this.ids) return;\n if (reason) this.log(\"manual trigger:\", reason);\n if (this.state().stopped === \"manual\") {\n // Started again after ReplayKit.stop(): a new recording, decided like a fresh page.\n this.states.save({ ...this.state(), stopped: undefined, rid: undefined, seq: 0, trigger: undefined, stats: undefined });\n this.uploader = null;\n this.earlyTrigger = \"manual\";\n this.decide();\n return;\n }\n this.trigger(\"manual\");\n }\n\n /** Stop recording for the rest of the session; what was captured is still uploaded. */\n manualStop(): void {\n if (!this.ids) return;\n this.recorder?.stop();\n this.recorder = null;\n this.buffer = [];\n if (this.mode === \"live\") {\n this.saveStats();\n void this.uploader?.flush();\n }\n this.mode = \"off\";\n this.states.save({ ...this.state(), stopped: \"manual\" });\n }\n\n /** The visitor's answer to the customer's consent banner. Stored on this device. */\n consent(granted: boolean): void {\n this.env.setLocal(CONSENT_KEY, granted ? \"1\" : \"0\");\n if (!this.ids) return;\n if (granted) {\n if (this.mode === \"wait_consent\") this.decide();\n return;\n }\n // Withdrawn: stop now and drop everything not uploaded yet.\n this.recorder?.stop();\n this.recorder = null;\n this.buffer = [];\n this.uploader?.discard();\n void this.env.store?.del(bufferKey(this.ids.sid));\n this.mode = \"off\";\n }\n\n recordingId(): string | null {\n return this.mode === \"live\" ? (this.ids ? this.state().rid ?? null : null) : null;\n }\n\n private shutdown(): void {\n this.collector?.stop();\n this.queue?.disable();\n this.recorder?.stop();\n this.recorder = null;\n this.mode = \"off\";\n }\n\n destroy(): void {\n this.destroyed = true;\n this.shutdown();\n for (const f of this.offWindow) f();\n this.offWindow = [];\n }\n}\n","// @himetrica/replaykit: session replay and friction signals for the web.\n//\n// import * as ReplayKit from \"@himetrica/replaykit\";\n// ReplayKit.init({ apiKey: \"hm_...\" });\n//\n// Needs the Himetrica tracker on the same page: recordings and signals belong to the\n// tracker's visitor and session. What is recorded, and when, is configured per\n// project in the dashboard; the SDK reads it from /init on every page.\n\nimport { ReplayKitClient, type ReplayKitOptions } from \"./client\";\n\nexport type { ReplayKitOptions } from \"./client\";\nexport type { ReplayKitSignal, ReplayKitSignalType, ReplayKitTrigger } from \"./protocol\";\n\nlet client: ReplayKitClient | null = null;\n\n/** Starts ReplayKit. A second call is ignored. */\nexport function init(options: ReplayKitOptions): void {\n if (client) return;\n client = new ReplayKitClient(options);\n client.start();\n}\n\n/** Record this session from now on (and keep the pre-trigger buffer). */\nexport function start(): void {\n client?.manualStart();\n}\n\n/**\n * Mark this moment as worth a recording, like an error would. `reason` is for your\n * own debugging (logged with `debug: true`); the recording is tagged \"manual\".\n */\nexport function trigger(reason?: string): void {\n client?.manualStart(reason);\n}\n\n/** Stop recording for the rest of the session. What was captured is still uploaded. */\nexport function stop(): void {\n client?.manualStop();\n}\n\n/**\n * The visitor's answer to your consent banner. With \"require consent\" on in the\n * project, nothing is recorded until consent(true). consent(false) stops and drops\n * what was not uploaded yet. Remembered on this device.\n */\nexport function consent(granted: boolean): void {\n if (client) client.consent(granted);\n else {\n try {\n localStorage.setItem(\"hm_rk_consent\", granted ? \"1\" : \"0\");\n } catch {\n // Storage unavailable.\n }\n }\n}\n\n/** Id of the recording running on this page, if any. */\nexport function getRecordingId(): string | null {\n return client?.recordingId() ?? null;\n}\n\n/** Stops everything and removes the listeners (tests, SPA teardown). */\nexport function shutdown(): void {\n client?.destroy();\n client = null;\n}\n"],"mappings":"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","names":["KEY","StateStore","env","sid","s","raw","emptyStats","now","entryUrl","isBrowser","uuidV4","c","ch","r","safeGet","storage","key","safeSet","value","indexedDbStore","dbp","open","resolve","reject","req","run","mode","fn","db","tx","s","gzipFn","text","browserEnv","originalFetch","nav","url","body","name","match","ms","h","readIdentity","env","visitorId","sessionId","matchPath","pattern","pathname","p","path","base","escapeRegExp","matchAny","patterns","s","sampleDraw","sessionId","h1","h2","i","ch","inSample","rate","stripUrl","href","cut","PAGE_TAG","pagesOf","events","prev","max","out","last","e","href","d","cut","url","BUFFER_WINDOW_MS","CHECKOUT_MS","snapshotStart","i","trimBuffer","now","windowMs","cutoff","keepFrom","firstSnapshot","start","approxBytes","RageDetector","c","b","near","finished","now","a","DEAD_CLICK_WAIT_MS","isDeadClickCandidate","f","isDeadClick","r","ACTIVE_GAP_MS","activeIncrement","lastInputTs","ts","gap","MIN_DURATION_EXEMPT","waitBeforeRecording","t","visibleMs","minDurationMs","decidePage","outcomeOf","status","Api","env","apiUrl","apiKey","op","body","url","blob","res","wantJson","outcome","BASE_BACKOFF_MS","MAX_BACKOFF_MS","backoff","failures","SIGNAL_FLUSH_MS","MAX_PER_PAGE","MAX_QUEUED","DEDUPE_MS","SignalQueue","env","api","ids","s","key","prev","enabled","ms","batch","r","backoff","INTERACTIVE","FIELD","MASKED","MAX_TEXT","MAX_DETAIL","selectorOf","el","parts","cur","depth","tag","classes","c","visibleText","maskAll","targetFacts","pointer","sel","clickedElement","target","SignalCollector","h","RageDetector","on","type","fn","opts","e","now","r","stripUrl","f","s","href","prev","ts","selector","force","b","count","isDeadClickCandidate","hrefBefore","scrollBefore","focusBefore","mutations","mo","list","isDeadClick","DEAD_CLICK_WAIT_MS","message","detail","form","key","set","fields","BASE_BLOCK","BASE_MASK","recordFn","loadRecord","m","validSelectors","list","s","Recorder","o","e","CHECKOUT_MS","href","PAGE_TAG","FLUSH_MS","FLUSH_BYTES","pendingKey","sid","Uploader","env","api","states","ids","hooks","store","p","n","e","approxBytes","events","ms","epoch","s","gzip","file","blob","half","prevPage","pages","pagesOf","presign","put","cur","backoff","reason","CONSENT_KEY","bufferKey","sid","IDENTITY_POLL_MS","IDENTITY_WAIT_MS","INIT_ATTEMPTS","ReplayKitClient","options","env","browserEnv","apiUrl","Api","StateStore","args","isBrowser","host","started","tryIds","id","readIdentity","ids","SignalQueue","SignalCollector","s","kind","ts","url","attempt","r","stripUrl","backoff","res","v","t","matchAny","mode","decidePage","inSample","st","emptyStats","store","key","parked","BUFFER_WINDOW_MS","CHECKOUT_MS","rec","Recorder","e","trimBuffer","Uploader","reason","onVis","now","live","wait","waitBeforeRecording","gen","events","startedAt","stats","last","activeIncrement","k","excluded","hide","unloading","onHide","granted","f","client","init","options","ReplayKitClient","start","trigger","reason","stop","consent","granted","getRecordingId","shutdown"]}
|