@reopt-ai/data-sdk-client 0.4.0 → 0.5.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.
Files changed (38) hide show
  1. package/README.md +50 -2
  2. package/dist/{client-BCrui21p.d.cts → client-BrtY8ZQB.d.cts} +72 -12
  3. package/dist/client-BrtY8ZQB.d.cts.map +1 -0
  4. package/dist/{client-B1GlWwVq.d.ts → client-BvSE7ErY.d.ts} +72 -12
  5. package/dist/client-BvSE7ErY.d.ts.map +1 -0
  6. package/dist/{exceptions-CuhGo9A5.cjs → exceptions-BPmC4TTt.cjs} +2 -2
  7. package/dist/exceptions-BPmC4TTt.cjs.map +1 -0
  8. package/dist/{exceptions-C9Fh0BIR.js → exceptions-CorACVsv.js} +2 -2
  9. package/dist/exceptions-CorACVsv.js.map +1 -0
  10. package/dist/index.cjs +1 -1
  11. package/dist/index.cjs.map +1 -1
  12. package/dist/index.d.cts +2 -2
  13. package/dist/index.d.cts.map +1 -1
  14. package/dist/index.d.ts +2 -2
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +1 -1
  17. package/dist/index.js.map +1 -1
  18. package/dist/next.d.cts +1 -1
  19. package/dist/next.d.ts +1 -1
  20. package/dist/react.cjs +1 -1
  21. package/dist/react.d.cts +1 -1
  22. package/dist/react.d.ts +1 -1
  23. package/dist/react.js +1 -1
  24. package/dist/recorder-B993wv6b.js +390 -0
  25. package/dist/recorder-B993wv6b.js.map +1 -0
  26. package/dist/recorder-BSzzSd3Q.cjs +390 -0
  27. package/dist/recorder-BSzzSd3Q.cjs.map +1 -0
  28. package/dist/{registry-CGqrOtf4.cjs → registry-DR1N7wD5.cjs} +17 -4
  29. package/dist/registry-DR1N7wD5.cjs.map +1 -0
  30. package/dist/{registry-BPhiH_l9.js → registry-IzNqE02g.js} +18 -5
  31. package/dist/registry-IzNqE02g.js.map +1 -0
  32. package/package.json +7 -5
  33. package/dist/client-B1GlWwVq.d.ts.map +0 -1
  34. package/dist/client-BCrui21p.d.cts.map +0 -1
  35. package/dist/exceptions-C9Fh0BIR.js.map +0 -1
  36. package/dist/exceptions-CuhGo9A5.cjs.map +0 -1
  37. package/dist/registry-BPhiH_l9.js.map +0 -1
  38. package/dist/registry-CGqrOtf4.cjs.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recorder-B993wv6b.js","names":[],"sources":["../src/replay/recorder.ts"],"sourcesContent":["import type { record as recordFn } from \"@rrweb/record\";\nimport {\n REPLAY_LIMITS,\n createReplayAssetResolver,\n REPLAY_PATHS,\n REPLAY_TOKEN_HEADER,\n type ReplayGrant,\n} from \"@reopt-ai/data-contract/replay\";\nimport { DEVICE_ID_HEADER, WRITE_KEY_HEADER } from \"@reopt-ai/data-contract/identity\";\nimport { sanitizeReplayEvent } from \"@reopt-ai/data-contract/replay\";\nimport type { ReplayController, ReplayRuntime, SessionReplayConfig } from \"./types.js\";\n\ntype RecordFunction = typeof recordFn;\ninterface PendingChunk {\n sequence: number;\n body: string;\n bytes: number;\n}\n/** An event is serialized once, when it is measured; sealing joins the strings. */\ninterface BufferedEvent {\n type: number;\n json: string;\n}\nconst utf8 = (value: string) => new TextEncoder().encode(value).byteLength;\n\nlet gzipSupported = typeof CompressionStream === \"function\";\n/** Native gzip when the browser has it; a failure disables compression for this page rather than a chunk. */\nasync function gzipBody(body: string): Promise<Uint8Array | null> {\n if (!gzipSupported) return null;\n try {\n const stream = new Blob([body]).stream().pipeThrough(new CompressionStream(\"gzip\"));\n return new Uint8Array(await new Response(stream).arrayBuffer());\n } catch {\n gzipSupported = false;\n return null;\n }\n}\n// rrweb owns a document-wide mirror. Multiple analytics clients must not run\n// competing recorders against it; another eligible client can claim it later.\nlet recordingOwner: symbol | null = null;\n\n/** Keep the opt-in adapter out of the analytics SDK's initial bundle. */\nexport default function createClientReplay(\n client: Pick<import(\"../client.js\").ReoptClient, \"getConsent\" | \"getDeviceId\">,\n config: import(\"../config.js\").ReoptClientConfig,\n fetch: ReplayRuntime[\"fetch\"],\n session: { header: string | null },\n canRun: () => boolean,\n path: () => string\n): ReplayController {\n const replay = config.sessionReplay!;\n return createReplayController(\n {\n writeKey: config.writeKey,\n baseUrl: config.baseUrl,\n fetch,\n path,\n allowed: () =>\n canRun() && client.getConsent(\"analytics\") && client.getConsent(replay.consentCategory ?? \"replay\"),\n deviceId: () => client.getDeviceId(),\n sessionId: () => session.header?.split(\".\")[0] ?? null,\n },\n replay\n );\n}\n\n/** One controller per SDK instance; rrweb is loaded only after consent and grant. */\nexport function createReplayController(\n runtime: ReplayRuntime,\n config: SessionReplayConfig,\n load: () => Promise<{ record: RecordFunction }> = () => import(\"@rrweb/record\")\n): ReplayController {\n const owner = Symbol();\n const asset = createReplayAssetResolver(config.publicAssets ?? [], location.href);\n let generation = 0;\n let active = false;\n let starting = false;\n let permanentlyStopped = false;\n let pageHidden = false;\n let grant: ReplayGrant | null = null;\n let stopRecord: (() => void) | undefined;\n let buffer: BufferedEvent[] = [];\n let bufferBytes = 2;\n let queue: PendingChunk[] = [];\n let queuedBytes = 0;\n let sequence = 0;\n let lastTimestamp = 0;\n let totalBytes = 0;\n let lastActivity = Date.now();\n let capturedDevice = \"\";\n let capturedSession: string | null = null;\n let inFlight: Promise<void> | null = null;\n let requestAbort = new AbortController();\n let retryAt = 0;\n let attempts = 0;\n // Consecutive stream failures on this page. A page whose DOM can never fit\n // the event limit must not re-serialize its snapshot every 30 seconds forever.\n let failures = 0;\n let exhausted = false;\n let rotating = false;\n // Sampling is deterministic per analytics session, so a \"not sampled\" answer\n // stays true until the session changes; polling /start every minute is waste.\n let unsampledSession: string | null = null;\n let flushingAt = Date.now();\n const flushInterval = Math.min(Math.max(config.flushIntervalMs ?? 20_000, 1000), 30_000);\n const idleTimeout = Math.min(Math.max(config.idleTimeoutMs ?? 60_000, 10_000), 5 * 60_000);\n const shouldRun = () =>\n !permanentlyStopped &&\n !exhausted &&\n !pageHidden &&\n runtime.allowed() &&\n !document.hidden &&\n Date.now() - lastActivity < idleTimeout;\n\n function discard(): void {\n generation++;\n active = false;\n starting = false;\n stopRecord?.();\n stopRecord = undefined;\n if (recordingOwner === owner) recordingOwner = null;\n requestAbort.abort();\n requestAbort = new AbortController();\n buffer = [];\n bufferBytes = 2;\n queue = [];\n queuedBytes = 0;\n grant = null;\n sequence = 0;\n totalBytes = 0;\n inFlight = null;\n lastTimestamp = 0;\n }\n\n function fail(): void {\n discard();\n failures++;\n if (failures >= 3) exhausted = true;\n retryAt = Date.now() + Math.min(10 * 60_000, 30_000 * 2 ** (failures - 1));\n }\n\n function seal(): void {\n if (!buffer.length || !grant) return;\n const body = `{\"version\":1,\"streamId\":${JSON.stringify(grant.streamId)},\"sequence\":${sequence++},\"events\":[${buffer\n .map((item) => item.json)\n .join(\",\")}]}`;\n const bytes = utf8(body);\n buffer = [];\n bufferBytes = 2;\n if (bytes > REPLAY_LIMITS.requestBytes || queuedBytes + bytes > REPLAY_LIMITS.queueBytes) {\n fail();\n return;\n }\n queuedBytes += bytes;\n queue.push({ sequence: sequence - 1, body, bytes });\n }\n\n function emit(raw: import(\"@rrweb/types\").eventWithTime): void {\n if (!active || !shouldRun()) return;\n try {\n const event = sanitizeReplayEvent(raw, runtime.path(), asset);\n if (!event) return;\n // Anchor the rrweb clock to the start response, never accepting a device\n // clock jump that would turn one short recording into a multi-day stream.\n event.timestamp = Math.max(lastTimestamp, event.timestamp + clockOffset);\n lastTimestamp = event.timestamp;\n const json = JSON.stringify(event);\n const bytes = utf8(json);\n if (bytes > REPLAY_LIMITS.eventBytes || totalBytes + bytes > REPLAY_LIMITS.streamBytes) {\n fail();\n return;\n }\n // Start each checkout chunk with Meta then FullSnapshot, independently of\n // earlier chunks. Mutations after overflow never get appended to a broken mirror.\n if (\n event.type === 4 ||\n (bufferBytes + bytes > REPLAY_LIMITS.targetChunkBytes &&\n !(event.type === 2 && buffer.length === 1 && buffer[0]?.type === 4)) ||\n buffer.length >= REPLAY_LIMITS.chunkEvents\n )\n seal();\n if (!active) return;\n buffer.push({ type: event.type, json });\n bufferBytes += bytes + 1;\n totalBytes += bytes;\n if (bufferBytes + queuedBytes > REPLAY_LIMITS.queueBytes) fail();\n } catch {\n fail();\n }\n }\n\n let clockOffset = 0;\n async function request(\n path: string,\n body: string,\n token?: string\n ): Promise<Awaited<ReturnType<ReplayRuntime[\"fetch\"]>>> {\n const abort = requestAbort;\n const timeout = new AbortController();\n const onAbort = () => timeout.abort();\n abort.signal.addEventListener(\"abort\", onAbort, { once: true });\n const timer = setTimeout(() => timeout.abort(), 15_000);\n let rejectAbort: (() => void) | undefined;\n try {\n // DOM chunks compress 8-12x. Compressing on the visitor's side cuts their\n // upload and keeps more chunks under the keepalive size for unload flushes.\n const compressed = token ? await gzipBody(body) : null;\n if (abort.signal.aborted) throw new Error(\"Replay request cancelled\");\n const pending = runtime.fetch(`${runtime.baseUrl.replace(/\\/+$/, \"\")}${path}`, {\n method: \"POST\",\n credentials: \"omit\",\n headers: {\n \"Content-Type\": \"application/json\",\n [WRITE_KEY_HEADER]: runtime.writeKey,\n [DEVICE_ID_HEADER]: capturedDevice,\n ...(token ? { [REPLAY_TOKEN_HEADER]: token } : {}),\n ...(compressed ? { \"Content-Encoding\": \"gzip\" } : {}),\n },\n body: compressed ?? body,\n signal: timeout.signal,\n keepalive: (compressed ? compressed.byteLength : utf8(body)) <= REPLAY_LIMITS.keepaliveBytes,\n });\n return await Promise.race([\n pending,\n new Promise<never>((_resolve, reject) => {\n rejectAbort = () => reject(new Error(\"Replay request cancelled\"));\n timeout.signal.addEventListener(\"abort\", rejectAbort, { once: true });\n if (timeout.signal.aborted) rejectAbort();\n }),\n ]);\n } finally {\n clearTimeout(timer);\n abort.signal.removeEventListener(\"abort\", onAbort);\n if (rejectAbort) timeout.signal.removeEventListener(\"abort\", rejectAbort);\n }\n }\n\n async function flush(): Promise<void> {\n if (!runtime.allowed()) {\n discard();\n return;\n }\n seal();\n if (inFlight) return inFlight;\n if (!grant || !queue.length || Date.now() < retryAt) return;\n const epoch = generation;\n const currentGrant = grant;\n inFlight = (async () => {\n while (queue.length && generation === epoch && runtime.allowed()) {\n const chunk = queue[0]!;\n try {\n const response = await request(REPLAY_PATHS.chunk, chunk.body, currentGrant.token);\n if (generation !== epoch) return;\n if (!response.ok) {\n if (response.status >= 500 || response.status === 429 || response.status === 408) {\n attempts++;\n if (attempts >= 5) {\n fail();\n return;\n }\n const retryAfter = Number(response.headers.get(\"retry-after\")) * 1000;\n retryAt =\n Date.now() + Math.min(60_000, Math.max(retryAfter || 0, 1000 * 2 ** attempts + Math.random() * 500));\n return;\n }\n fail();\n return;\n }\n queue.shift();\n queuedBytes -= chunk.bytes;\n attempts = 0;\n failures = 0;\n retryAt = 0;\n } catch {\n if (generation === epoch) {\n attempts++;\n if (attempts >= 5) fail();\n else retryAt = Date.now() + Math.min(30_000, 1000 * 2 ** attempts);\n }\n return;\n }\n }\n })().finally(() => {\n if (generation === epoch) inFlight = null;\n });\n return inFlight;\n }\n\n async function start(): Promise<void> {\n if (\n starting ||\n active ||\n rotating ||\n (recordingOwner && recordingOwner !== owner) ||\n !shouldRun() ||\n Date.now() < retryAt ||\n (unsampledSession !== null && runtime.sessionId() === unsampledSession)\n )\n return;\n starting = true;\n const epoch = generation;\n capturedDevice = runtime.deviceId();\n capturedSession = runtime.sessionId();\n const streamId = crypto.randomUUID();\n try {\n const response = await request(REPLAY_PATHS.start, JSON.stringify({ version: 1, streamId }));\n if (generation !== epoch || !shouldRun()) return;\n if (!response.ok) {\n // 409: the analytics session is still materializing. 403: the project\n // has capture off, which an operator changes on the order of minutes.\n retryAt = Date.now() + (response.status === 409 ? 2000 : response.status === 403 ? 10 * 60_000 : 60_000);\n return;\n }\n const result = JSON.parse(await response.text()) as ReplayGrant;\n if (generation !== epoch || !shouldRun()) return;\n if (!result.sampled) {\n unsampledSession = result.sessionId || runtime.sessionId();\n retryAt = Date.now() + 10 * 60_000;\n return;\n }\n if (\n result.version !== 1 ||\n result.streamId !== streamId ||\n !result.token ||\n !Number.isFinite(result.serverTime) ||\n !Number.isFinite(result.expiresAt)\n ) {\n fail();\n return;\n }\n const { record } = await load();\n if (generation !== epoch || !shouldRun()) return;\n if (recordingOwner && recordingOwner !== owner) return;\n recordingOwner = owner;\n grant = result;\n capturedSession = result.sessionId;\n clockOffset = result.serverTime - Date.now();\n active = true;\n const blockSelector = [\n \"[data-reopt-replay-block]\",\n \"[data-reopt-private]\",\n \"iframe\",\n \"object\",\n \"embed\",\n \"input[type=password]\",\n config.blockSelector,\n ]\n .filter(Boolean)\n .join(\",\");\n // Invalid selectors fail before observing the page.\n document.querySelector(blockSelector);\n const stop = record({\n emit: (event) => {\n if (generation === epoch) emit(event);\n },\n maskAllInputs: true,\n maskTextSelector: \"*\",\n blockSelector,\n blockClass: \"rr-block\",\n inlineStylesheet: true,\n collectFonts: false,\n inlineImages: false,\n recordCanvas: false,\n recordCrossOriginIframes: false,\n // Full snapshots dominate recorded bytes (about 90% measured with\n // minute checkouts, ~440 KiB each against ~1 KiB/s of mutations), so\n // checkouts are rare. Playback still seeks inside a window from its\n // snapshot; a five-minute window stays far under the 8 MiB read cap.\n checkoutEveryNms: 5 * 60_000,\n sampling: { mousemove: 100, scroll: 150, input: \"last\" },\n slimDOMOptions: {\n script: true,\n comment: true,\n headMetaDescKeywords: true,\n headMetaSocial: true,\n headMetaRobots: true,\n headMetaHttpEquiv: true,\n headMetaAuthorship: true,\n headMetaVerification: true,\n },\n errorHandler: () => {\n queueMicrotask(() => {\n if (generation === epoch) fail();\n });\n return true;\n },\n });\n if (!active || generation !== epoch) {\n stop?.();\n return;\n }\n stopRecord = stop;\n if (!stopRecord) fail();\n else void flush();\n } catch {\n if (generation === epoch) fail();\n } finally {\n if (generation === epoch) starting = false;\n }\n }\n\n /** The grant is still valid for five minutes: deliver what is buffered, then start a fresh stream. */\n function rotate(): void {\n rotating = true;\n const epoch = generation;\n stopRecord?.();\n stopRecord = undefined;\n active = false;\n void flush().finally(() => {\n rotating = false;\n if (generation === epoch) discard();\n sync();\n });\n }\n\n function sync(): void {\n const identityChanged =\n (active || starting) &&\n (capturedDevice !== runtime.deviceId() ||\n (capturedSession && runtime.sessionId() && capturedSession !== runtime.sessionId()));\n if ((!shouldRun() || identityChanged) && (active || starting || grant || queue.length || inFlight)) discard();\n else if (grant && active && !rotating && Date.now() + clockOffset >= grant.expiresAt - 5 * 60_000) rotate();\n if (shouldRun()) void start();\n }\n const onActivity = () => {\n lastActivity = Date.now();\n sync();\n };\n const onHidden = () => {\n if (document.hidden || pageHidden) {\n stopRecord?.();\n stopRecord = undefined;\n active = false;\n // A final, bounded best-effort flush. Consent withdrawal always discards.\n void flush().finally(() => {\n if (document.hidden || pageHidden) discard();\n });\n } else {\n discard();\n lastActivity = Date.now();\n sync();\n }\n };\n const onPageHide = () => {\n pageHidden = true;\n onHidden();\n };\n const onPageShow = () => {\n pageHidden = false;\n onHidden();\n };\n for (const name of [\"pointerdown\", \"keydown\", \"scroll\"])\n document.addEventListener(name, onActivity, { passive: true });\n document.addEventListener(\"visibilitychange\", onHidden);\n window.addEventListener(\"pagehide\", onPageHide);\n window.addEventListener(\"pageshow\", onPageShow);\n const timer = setInterval(() => {\n sync();\n // A retry whose backoff has elapsed goes out now rather than waiting for\n // the next scheduled flush; the flush interval paces new chunks, not retries.\n const retryDue = queue.length > 0 && retryAt > 0 && Date.now() >= retryAt;\n if (retryDue || Date.now() - flushingAt >= flushInterval) {\n flushingAt = Date.now();\n void flush();\n }\n }, 1000);\n sync();\n return {\n sync,\n flush,\n stop() {\n permanentlyStopped = true;\n discard();\n clearInterval(timer);\n for (const name of [\"pointerdown\", \"keydown\", \"scroll\"]) document.removeEventListener(name, onActivity);\n document.removeEventListener(\"visibilitychange\", onHidden);\n window.removeEventListener(\"pagehide\", onPageHide);\n window.removeEventListener(\"pageshow\", onPageShow);\n },\n };\n}\n"],"mappings":"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"}
@@ -0,0 +1,390 @@
1
+ let _reopt_ai_data_contract_identity = require("@reopt-ai/data-contract/identity");
2
+ let _reopt_ai_data_contract_replay = require("@reopt-ai/data-contract/replay");
3
+ //#region src/replay/recorder.ts
4
+ const utf8 = (value) => new TextEncoder().encode(value).byteLength;
5
+ let gzipSupported = typeof CompressionStream === "function";
6
+ async function gzipBody(body) {
7
+ if (!gzipSupported) return null;
8
+ try {
9
+ const stream = new Blob([body]).stream().pipeThrough(new CompressionStream("gzip"));
10
+ return new Uint8Array(await new Response(stream).arrayBuffer());
11
+ } catch {
12
+ gzipSupported = false;
13
+ return null;
14
+ }
15
+ }
16
+ let recordingOwner = null;
17
+ function createClientReplay(client, config, fetch, session, canRun, path) {
18
+ const replay = config.sessionReplay;
19
+ return createReplayController({
20
+ writeKey: config.writeKey,
21
+ baseUrl: config.baseUrl,
22
+ fetch,
23
+ path,
24
+ allowed: () => canRun() && client.getConsent("analytics") && client.getConsent(replay.consentCategory ?? "replay"),
25
+ deviceId: () => client.getDeviceId(),
26
+ sessionId: () => session.header?.split(".")[0] ?? null
27
+ }, replay);
28
+ }
29
+ function createReplayController(runtime, config, load = () => import("@rrweb/record")) {
30
+ const owner = Symbol();
31
+ const asset = (0, _reopt_ai_data_contract_replay.createReplayAssetResolver)(config.publicAssets ?? [], location.href);
32
+ let generation = 0;
33
+ let active = false;
34
+ let starting = false;
35
+ let permanentlyStopped = false;
36
+ let pageHidden = false;
37
+ let grant = null;
38
+ let stopRecord;
39
+ let buffer = [];
40
+ let bufferBytes = 2;
41
+ let queue = [];
42
+ let queuedBytes = 0;
43
+ let sequence = 0;
44
+ let lastTimestamp = 0;
45
+ let totalBytes = 0;
46
+ let lastActivity = Date.now();
47
+ let capturedDevice = "";
48
+ let capturedSession = null;
49
+ let inFlight = null;
50
+ let requestAbort = new AbortController();
51
+ let retryAt = 0;
52
+ let attempts = 0;
53
+ let failures = 0;
54
+ let exhausted = false;
55
+ let rotating = false;
56
+ let unsampledSession = null;
57
+ let flushingAt = Date.now();
58
+ const flushInterval = Math.min(Math.max(config.flushIntervalMs ?? 2e4, 1e3), 3e4);
59
+ const idleTimeout = Math.min(Math.max(config.idleTimeoutMs ?? 6e4, 1e4), 3e5);
60
+ const shouldRun = () => !permanentlyStopped && !exhausted && !pageHidden && runtime.allowed() && !document.hidden && Date.now() - lastActivity < idleTimeout;
61
+ function discard() {
62
+ generation++;
63
+ active = false;
64
+ starting = false;
65
+ stopRecord?.();
66
+ stopRecord = void 0;
67
+ if (recordingOwner === owner) recordingOwner = null;
68
+ requestAbort.abort();
69
+ requestAbort = new AbortController();
70
+ buffer = [];
71
+ bufferBytes = 2;
72
+ queue = [];
73
+ queuedBytes = 0;
74
+ grant = null;
75
+ sequence = 0;
76
+ totalBytes = 0;
77
+ inFlight = null;
78
+ lastTimestamp = 0;
79
+ }
80
+ function fail() {
81
+ discard();
82
+ failures++;
83
+ if (failures >= 3) exhausted = true;
84
+ retryAt = Date.now() + Math.min(6e5, 3e4 * 2 ** (failures - 1));
85
+ }
86
+ function seal() {
87
+ if (!buffer.length || !grant) return;
88
+ const body = `{"version":1,"streamId":${JSON.stringify(grant.streamId)},"sequence":${sequence++},"events":[${buffer.map((item) => item.json).join(",")}]}`;
89
+ const bytes = utf8(body);
90
+ buffer = [];
91
+ bufferBytes = 2;
92
+ if (bytes > _reopt_ai_data_contract_replay.REPLAY_LIMITS.requestBytes || queuedBytes + bytes > _reopt_ai_data_contract_replay.REPLAY_LIMITS.queueBytes) {
93
+ fail();
94
+ return;
95
+ }
96
+ queuedBytes += bytes;
97
+ queue.push({
98
+ sequence: sequence - 1,
99
+ body,
100
+ bytes
101
+ });
102
+ }
103
+ function emit(raw) {
104
+ if (!active || !shouldRun()) return;
105
+ try {
106
+ const event = (0, _reopt_ai_data_contract_replay.sanitizeReplayEvent)(raw, runtime.path(), asset);
107
+ if (!event) return;
108
+ event.timestamp = Math.max(lastTimestamp, event.timestamp + clockOffset);
109
+ lastTimestamp = event.timestamp;
110
+ const json = JSON.stringify(event);
111
+ const bytes = utf8(json);
112
+ if (bytes > _reopt_ai_data_contract_replay.REPLAY_LIMITS.eventBytes || totalBytes + bytes > _reopt_ai_data_contract_replay.REPLAY_LIMITS.streamBytes) {
113
+ fail();
114
+ return;
115
+ }
116
+ if (event.type === 4 || bufferBytes + bytes > _reopt_ai_data_contract_replay.REPLAY_LIMITS.targetChunkBytes && !(event.type === 2 && buffer.length === 1 && buffer[0]?.type === 4) || buffer.length >= _reopt_ai_data_contract_replay.REPLAY_LIMITS.chunkEvents) seal();
117
+ if (!active) return;
118
+ buffer.push({
119
+ type: event.type,
120
+ json
121
+ });
122
+ bufferBytes += bytes + 1;
123
+ totalBytes += bytes;
124
+ if (bufferBytes + queuedBytes > _reopt_ai_data_contract_replay.REPLAY_LIMITS.queueBytes) fail();
125
+ } catch {
126
+ fail();
127
+ }
128
+ }
129
+ let clockOffset = 0;
130
+ async function request(path, body, token) {
131
+ const abort = requestAbort;
132
+ const timeout = new AbortController();
133
+ const onAbort = () => timeout.abort();
134
+ abort.signal.addEventListener("abort", onAbort, { once: true });
135
+ const timer = setTimeout(() => timeout.abort(), 15e3);
136
+ let rejectAbort;
137
+ try {
138
+ const compressed = token ? await gzipBody(body) : null;
139
+ if (abort.signal.aborted) throw new Error("Replay request cancelled");
140
+ const pending = runtime.fetch(`${runtime.baseUrl.replace(/\/+$/, "")}${path}`, {
141
+ method: "POST",
142
+ credentials: "omit",
143
+ headers: {
144
+ "Content-Type": "application/json",
145
+ [_reopt_ai_data_contract_identity.WRITE_KEY_HEADER]: runtime.writeKey,
146
+ [_reopt_ai_data_contract_identity.DEVICE_ID_HEADER]: capturedDevice,
147
+ ...token ? { [_reopt_ai_data_contract_replay.REPLAY_TOKEN_HEADER]: token } : {},
148
+ ...compressed ? { "Content-Encoding": "gzip" } : {}
149
+ },
150
+ body: compressed ?? body,
151
+ signal: timeout.signal,
152
+ keepalive: (compressed ? compressed.byteLength : utf8(body)) <= _reopt_ai_data_contract_replay.REPLAY_LIMITS.keepaliveBytes
153
+ });
154
+ return await Promise.race([pending, new Promise((_resolve, reject) => {
155
+ rejectAbort = () => reject(new Error("Replay request cancelled"));
156
+ timeout.signal.addEventListener("abort", rejectAbort, { once: true });
157
+ if (timeout.signal.aborted) rejectAbort();
158
+ })]);
159
+ } finally {
160
+ clearTimeout(timer);
161
+ abort.signal.removeEventListener("abort", onAbort);
162
+ if (rejectAbort) timeout.signal.removeEventListener("abort", rejectAbort);
163
+ }
164
+ }
165
+ async function flush() {
166
+ if (!runtime.allowed()) {
167
+ discard();
168
+ return;
169
+ }
170
+ seal();
171
+ if (inFlight) return inFlight;
172
+ if (!grant || !queue.length || Date.now() < retryAt) return;
173
+ const epoch = generation;
174
+ const currentGrant = grant;
175
+ inFlight = (async () => {
176
+ while (queue.length && generation === epoch && runtime.allowed()) {
177
+ const chunk = queue[0];
178
+ try {
179
+ const response = await request(_reopt_ai_data_contract_replay.REPLAY_PATHS.chunk, chunk.body, currentGrant.token);
180
+ if (generation !== epoch) return;
181
+ if (!response.ok) {
182
+ if (response.status >= 500 || response.status === 429 || response.status === 408) {
183
+ attempts++;
184
+ if (attempts >= 5) {
185
+ fail();
186
+ return;
187
+ }
188
+ const retryAfter = Number(response.headers.get("retry-after")) * 1e3;
189
+ retryAt = Date.now() + Math.min(6e4, Math.max(retryAfter || 0, 1e3 * 2 ** attempts + Math.random() * 500));
190
+ return;
191
+ }
192
+ fail();
193
+ return;
194
+ }
195
+ queue.shift();
196
+ queuedBytes -= chunk.bytes;
197
+ attempts = 0;
198
+ failures = 0;
199
+ retryAt = 0;
200
+ } catch {
201
+ if (generation === epoch) {
202
+ attempts++;
203
+ if (attempts >= 5) fail();
204
+ else retryAt = Date.now() + Math.min(3e4, 1e3 * 2 ** attempts);
205
+ }
206
+ return;
207
+ }
208
+ }
209
+ })().finally(() => {
210
+ if (generation === epoch) inFlight = null;
211
+ });
212
+ return inFlight;
213
+ }
214
+ async function start() {
215
+ if (starting || active || rotating || recordingOwner && recordingOwner !== owner || !shouldRun() || Date.now() < retryAt || unsampledSession !== null && runtime.sessionId() === unsampledSession) return;
216
+ starting = true;
217
+ const epoch = generation;
218
+ capturedDevice = runtime.deviceId();
219
+ capturedSession = runtime.sessionId();
220
+ const streamId = crypto.randomUUID();
221
+ try {
222
+ const response = await request(_reopt_ai_data_contract_replay.REPLAY_PATHS.start, JSON.stringify({
223
+ version: 1,
224
+ streamId
225
+ }));
226
+ if (generation !== epoch || !shouldRun()) return;
227
+ if (!response.ok) {
228
+ retryAt = Date.now() + (response.status === 409 ? 2e3 : response.status === 403 ? 6e5 : 6e4);
229
+ return;
230
+ }
231
+ const result = JSON.parse(await response.text());
232
+ if (generation !== epoch || !shouldRun()) return;
233
+ if (!result.sampled) {
234
+ unsampledSession = result.sessionId || runtime.sessionId();
235
+ retryAt = Date.now() + 6e5;
236
+ return;
237
+ }
238
+ if (result.version !== 1 || result.streamId !== streamId || !result.token || !Number.isFinite(result.serverTime) || !Number.isFinite(result.expiresAt)) {
239
+ fail();
240
+ return;
241
+ }
242
+ const { record } = await load();
243
+ if (generation !== epoch || !shouldRun()) return;
244
+ if (recordingOwner && recordingOwner !== owner) return;
245
+ recordingOwner = owner;
246
+ grant = result;
247
+ capturedSession = result.sessionId;
248
+ clockOffset = result.serverTime - Date.now();
249
+ active = true;
250
+ const blockSelector = [
251
+ "[data-reopt-replay-block]",
252
+ "[data-reopt-private]",
253
+ "iframe",
254
+ "object",
255
+ "embed",
256
+ "input[type=password]",
257
+ config.blockSelector
258
+ ].filter(Boolean).join(",");
259
+ document.querySelector(blockSelector);
260
+ const stop = record({
261
+ emit: (event) => {
262
+ if (generation === epoch) emit(event);
263
+ },
264
+ maskAllInputs: true,
265
+ maskTextSelector: "*",
266
+ blockSelector,
267
+ blockClass: "rr-block",
268
+ inlineStylesheet: true,
269
+ collectFonts: false,
270
+ inlineImages: false,
271
+ recordCanvas: false,
272
+ recordCrossOriginIframes: false,
273
+ checkoutEveryNms: 3e5,
274
+ sampling: {
275
+ mousemove: 100,
276
+ scroll: 150,
277
+ input: "last"
278
+ },
279
+ slimDOMOptions: {
280
+ script: true,
281
+ comment: true,
282
+ headMetaDescKeywords: true,
283
+ headMetaSocial: true,
284
+ headMetaRobots: true,
285
+ headMetaHttpEquiv: true,
286
+ headMetaAuthorship: true,
287
+ headMetaVerification: true
288
+ },
289
+ errorHandler: () => {
290
+ queueMicrotask(() => {
291
+ if (generation === epoch) fail();
292
+ });
293
+ return true;
294
+ }
295
+ });
296
+ if (!active || generation !== epoch) {
297
+ stop?.();
298
+ return;
299
+ }
300
+ stopRecord = stop;
301
+ if (!stopRecord) fail();
302
+ else flush();
303
+ } catch {
304
+ if (generation === epoch) fail();
305
+ } finally {
306
+ if (generation === epoch) starting = false;
307
+ }
308
+ }
309
+ function rotate() {
310
+ rotating = true;
311
+ const epoch = generation;
312
+ stopRecord?.();
313
+ stopRecord = void 0;
314
+ active = false;
315
+ flush().finally(() => {
316
+ rotating = false;
317
+ if (generation === epoch) discard();
318
+ sync();
319
+ });
320
+ }
321
+ function sync() {
322
+ const identityChanged = (active || starting) && (capturedDevice !== runtime.deviceId() || capturedSession && runtime.sessionId() && capturedSession !== runtime.sessionId());
323
+ if ((!shouldRun() || identityChanged) && (active || starting || grant || queue.length || inFlight)) discard();
324
+ else if (grant && active && !rotating && Date.now() + clockOffset >= grant.expiresAt - 3e5) rotate();
325
+ if (shouldRun()) start();
326
+ }
327
+ const onActivity = () => {
328
+ lastActivity = Date.now();
329
+ sync();
330
+ };
331
+ const onHidden = () => {
332
+ if (document.hidden || pageHidden) {
333
+ stopRecord?.();
334
+ stopRecord = void 0;
335
+ active = false;
336
+ flush().finally(() => {
337
+ if (document.hidden || pageHidden) discard();
338
+ });
339
+ } else {
340
+ discard();
341
+ lastActivity = Date.now();
342
+ sync();
343
+ }
344
+ };
345
+ const onPageHide = () => {
346
+ pageHidden = true;
347
+ onHidden();
348
+ };
349
+ const onPageShow = () => {
350
+ pageHidden = false;
351
+ onHidden();
352
+ };
353
+ for (const name of [
354
+ "pointerdown",
355
+ "keydown",
356
+ "scroll"
357
+ ]) document.addEventListener(name, onActivity, { passive: true });
358
+ document.addEventListener("visibilitychange", onHidden);
359
+ window.addEventListener("pagehide", onPageHide);
360
+ window.addEventListener("pageshow", onPageShow);
361
+ const timer = setInterval(() => {
362
+ sync();
363
+ if (queue.length > 0 && retryAt > 0 && Date.now() >= retryAt || Date.now() - flushingAt >= flushInterval) {
364
+ flushingAt = Date.now();
365
+ flush();
366
+ }
367
+ }, 1e3);
368
+ sync();
369
+ return {
370
+ sync,
371
+ flush,
372
+ stop() {
373
+ permanentlyStopped = true;
374
+ discard();
375
+ clearInterval(timer);
376
+ for (const name of [
377
+ "pointerdown",
378
+ "keydown",
379
+ "scroll"
380
+ ]) document.removeEventListener(name, onActivity);
381
+ document.removeEventListener("visibilitychange", onHidden);
382
+ window.removeEventListener("pagehide", onPageHide);
383
+ window.removeEventListener("pageshow", onPageShow);
384
+ }
385
+ };
386
+ }
387
+ //#endregion
388
+ exports.default = createClientReplay;
389
+
390
+ //# sourceMappingURL=recorder-BSzzSd3Q.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recorder-BSzzSd3Q.cjs","names":["createReplayAssetResolver","REPLAY_LIMITS","sanitizeReplayEvent","WRITE_KEY_HEADER","DEVICE_ID_HEADER","REPLAY_TOKEN_HEADER","REPLAY_PATHS"],"sources":["../src/replay/recorder.ts"],"sourcesContent":["import type { record as recordFn } from \"@rrweb/record\";\nimport {\n REPLAY_LIMITS,\n createReplayAssetResolver,\n REPLAY_PATHS,\n REPLAY_TOKEN_HEADER,\n type ReplayGrant,\n} from \"@reopt-ai/data-contract/replay\";\nimport { DEVICE_ID_HEADER, WRITE_KEY_HEADER } from \"@reopt-ai/data-contract/identity\";\nimport { sanitizeReplayEvent } from \"@reopt-ai/data-contract/replay\";\nimport type { ReplayController, ReplayRuntime, SessionReplayConfig } from \"./types.js\";\n\ntype RecordFunction = typeof recordFn;\ninterface PendingChunk {\n sequence: number;\n body: string;\n bytes: number;\n}\n/** An event is serialized once, when it is measured; sealing joins the strings. */\ninterface BufferedEvent {\n type: number;\n json: string;\n}\nconst utf8 = (value: string) => new TextEncoder().encode(value).byteLength;\n\nlet gzipSupported = typeof CompressionStream === \"function\";\n/** Native gzip when the browser has it; a failure disables compression for this page rather than a chunk. */\nasync function gzipBody(body: string): Promise<Uint8Array | null> {\n if (!gzipSupported) return null;\n try {\n const stream = new Blob([body]).stream().pipeThrough(new CompressionStream(\"gzip\"));\n return new Uint8Array(await new Response(stream).arrayBuffer());\n } catch {\n gzipSupported = false;\n return null;\n }\n}\n// rrweb owns a document-wide mirror. Multiple analytics clients must not run\n// competing recorders against it; another eligible client can claim it later.\nlet recordingOwner: symbol | null = null;\n\n/** Keep the opt-in adapter out of the analytics SDK's initial bundle. */\nexport default function createClientReplay(\n client: Pick<import(\"../client.js\").ReoptClient, \"getConsent\" | \"getDeviceId\">,\n config: import(\"../config.js\").ReoptClientConfig,\n fetch: ReplayRuntime[\"fetch\"],\n session: { header: string | null },\n canRun: () => boolean,\n path: () => string\n): ReplayController {\n const replay = config.sessionReplay!;\n return createReplayController(\n {\n writeKey: config.writeKey,\n baseUrl: config.baseUrl,\n fetch,\n path,\n allowed: () =>\n canRun() && client.getConsent(\"analytics\") && client.getConsent(replay.consentCategory ?? \"replay\"),\n deviceId: () => client.getDeviceId(),\n sessionId: () => session.header?.split(\".\")[0] ?? null,\n },\n replay\n );\n}\n\n/** One controller per SDK instance; rrweb is loaded only after consent and grant. */\nexport function createReplayController(\n runtime: ReplayRuntime,\n config: SessionReplayConfig,\n load: () => Promise<{ record: RecordFunction }> = () => import(\"@rrweb/record\")\n): ReplayController {\n const owner = Symbol();\n const asset = createReplayAssetResolver(config.publicAssets ?? [], location.href);\n let generation = 0;\n let active = false;\n let starting = false;\n let permanentlyStopped = false;\n let pageHidden = false;\n let grant: ReplayGrant | null = null;\n let stopRecord: (() => void) | undefined;\n let buffer: BufferedEvent[] = [];\n let bufferBytes = 2;\n let queue: PendingChunk[] = [];\n let queuedBytes = 0;\n let sequence = 0;\n let lastTimestamp = 0;\n let totalBytes = 0;\n let lastActivity = Date.now();\n let capturedDevice = \"\";\n let capturedSession: string | null = null;\n let inFlight: Promise<void> | null = null;\n let requestAbort = new AbortController();\n let retryAt = 0;\n let attempts = 0;\n // Consecutive stream failures on this page. A page whose DOM can never fit\n // the event limit must not re-serialize its snapshot every 30 seconds forever.\n let failures = 0;\n let exhausted = false;\n let rotating = false;\n // Sampling is deterministic per analytics session, so a \"not sampled\" answer\n // stays true until the session changes; polling /start every minute is waste.\n let unsampledSession: string | null = null;\n let flushingAt = Date.now();\n const flushInterval = Math.min(Math.max(config.flushIntervalMs ?? 20_000, 1000), 30_000);\n const idleTimeout = Math.min(Math.max(config.idleTimeoutMs ?? 60_000, 10_000), 5 * 60_000);\n const shouldRun = () =>\n !permanentlyStopped &&\n !exhausted &&\n !pageHidden &&\n runtime.allowed() &&\n !document.hidden &&\n Date.now() - lastActivity < idleTimeout;\n\n function discard(): void {\n generation++;\n active = false;\n starting = false;\n stopRecord?.();\n stopRecord = undefined;\n if (recordingOwner === owner) recordingOwner = null;\n requestAbort.abort();\n requestAbort = new AbortController();\n buffer = [];\n bufferBytes = 2;\n queue = [];\n queuedBytes = 0;\n grant = null;\n sequence = 0;\n totalBytes = 0;\n inFlight = null;\n lastTimestamp = 0;\n }\n\n function fail(): void {\n discard();\n failures++;\n if (failures >= 3) exhausted = true;\n retryAt = Date.now() + Math.min(10 * 60_000, 30_000 * 2 ** (failures - 1));\n }\n\n function seal(): void {\n if (!buffer.length || !grant) return;\n const body = `{\"version\":1,\"streamId\":${JSON.stringify(grant.streamId)},\"sequence\":${sequence++},\"events\":[${buffer\n .map((item) => item.json)\n .join(\",\")}]}`;\n const bytes = utf8(body);\n buffer = [];\n bufferBytes = 2;\n if (bytes > REPLAY_LIMITS.requestBytes || queuedBytes + bytes > REPLAY_LIMITS.queueBytes) {\n fail();\n return;\n }\n queuedBytes += bytes;\n queue.push({ sequence: sequence - 1, body, bytes });\n }\n\n function emit(raw: import(\"@rrweb/types\").eventWithTime): void {\n if (!active || !shouldRun()) return;\n try {\n const event = sanitizeReplayEvent(raw, runtime.path(), asset);\n if (!event) return;\n // Anchor the rrweb clock to the start response, never accepting a device\n // clock jump that would turn one short recording into a multi-day stream.\n event.timestamp = Math.max(lastTimestamp, event.timestamp + clockOffset);\n lastTimestamp = event.timestamp;\n const json = JSON.stringify(event);\n const bytes = utf8(json);\n if (bytes > REPLAY_LIMITS.eventBytes || totalBytes + bytes > REPLAY_LIMITS.streamBytes) {\n fail();\n return;\n }\n // Start each checkout chunk with Meta then FullSnapshot, independently of\n // earlier chunks. Mutations after overflow never get appended to a broken mirror.\n if (\n event.type === 4 ||\n (bufferBytes + bytes > REPLAY_LIMITS.targetChunkBytes &&\n !(event.type === 2 && buffer.length === 1 && buffer[0]?.type === 4)) ||\n buffer.length >= REPLAY_LIMITS.chunkEvents\n )\n seal();\n if (!active) return;\n buffer.push({ type: event.type, json });\n bufferBytes += bytes + 1;\n totalBytes += bytes;\n if (bufferBytes + queuedBytes > REPLAY_LIMITS.queueBytes) fail();\n } catch {\n fail();\n }\n }\n\n let clockOffset = 0;\n async function request(\n path: string,\n body: string,\n token?: string\n ): Promise<Awaited<ReturnType<ReplayRuntime[\"fetch\"]>>> {\n const abort = requestAbort;\n const timeout = new AbortController();\n const onAbort = () => timeout.abort();\n abort.signal.addEventListener(\"abort\", onAbort, { once: true });\n const timer = setTimeout(() => timeout.abort(), 15_000);\n let rejectAbort: (() => void) | undefined;\n try {\n // DOM chunks compress 8-12x. Compressing on the visitor's side cuts their\n // upload and keeps more chunks under the keepalive size for unload flushes.\n const compressed = token ? await gzipBody(body) : null;\n if (abort.signal.aborted) throw new Error(\"Replay request cancelled\");\n const pending = runtime.fetch(`${runtime.baseUrl.replace(/\\/+$/, \"\")}${path}`, {\n method: \"POST\",\n credentials: \"omit\",\n headers: {\n \"Content-Type\": \"application/json\",\n [WRITE_KEY_HEADER]: runtime.writeKey,\n [DEVICE_ID_HEADER]: capturedDevice,\n ...(token ? { [REPLAY_TOKEN_HEADER]: token } : {}),\n ...(compressed ? { \"Content-Encoding\": \"gzip\" } : {}),\n },\n body: compressed ?? body,\n signal: timeout.signal,\n keepalive: (compressed ? compressed.byteLength : utf8(body)) <= REPLAY_LIMITS.keepaliveBytes,\n });\n return await Promise.race([\n pending,\n new Promise<never>((_resolve, reject) => {\n rejectAbort = () => reject(new Error(\"Replay request cancelled\"));\n timeout.signal.addEventListener(\"abort\", rejectAbort, { once: true });\n if (timeout.signal.aborted) rejectAbort();\n }),\n ]);\n } finally {\n clearTimeout(timer);\n abort.signal.removeEventListener(\"abort\", onAbort);\n if (rejectAbort) timeout.signal.removeEventListener(\"abort\", rejectAbort);\n }\n }\n\n async function flush(): Promise<void> {\n if (!runtime.allowed()) {\n discard();\n return;\n }\n seal();\n if (inFlight) return inFlight;\n if (!grant || !queue.length || Date.now() < retryAt) return;\n const epoch = generation;\n const currentGrant = grant;\n inFlight = (async () => {\n while (queue.length && generation === epoch && runtime.allowed()) {\n const chunk = queue[0]!;\n try {\n const response = await request(REPLAY_PATHS.chunk, chunk.body, currentGrant.token);\n if (generation !== epoch) return;\n if (!response.ok) {\n if (response.status >= 500 || response.status === 429 || response.status === 408) {\n attempts++;\n if (attempts >= 5) {\n fail();\n return;\n }\n const retryAfter = Number(response.headers.get(\"retry-after\")) * 1000;\n retryAt =\n Date.now() + Math.min(60_000, Math.max(retryAfter || 0, 1000 * 2 ** attempts + Math.random() * 500));\n return;\n }\n fail();\n return;\n }\n queue.shift();\n queuedBytes -= chunk.bytes;\n attempts = 0;\n failures = 0;\n retryAt = 0;\n } catch {\n if (generation === epoch) {\n attempts++;\n if (attempts >= 5) fail();\n else retryAt = Date.now() + Math.min(30_000, 1000 * 2 ** attempts);\n }\n return;\n }\n }\n })().finally(() => {\n if (generation === epoch) inFlight = null;\n });\n return inFlight;\n }\n\n async function start(): Promise<void> {\n if (\n starting ||\n active ||\n rotating ||\n (recordingOwner && recordingOwner !== owner) ||\n !shouldRun() ||\n Date.now() < retryAt ||\n (unsampledSession !== null && runtime.sessionId() === unsampledSession)\n )\n return;\n starting = true;\n const epoch = generation;\n capturedDevice = runtime.deviceId();\n capturedSession = runtime.sessionId();\n const streamId = crypto.randomUUID();\n try {\n const response = await request(REPLAY_PATHS.start, JSON.stringify({ version: 1, streamId }));\n if (generation !== epoch || !shouldRun()) return;\n if (!response.ok) {\n // 409: the analytics session is still materializing. 403: the project\n // has capture off, which an operator changes on the order of minutes.\n retryAt = Date.now() + (response.status === 409 ? 2000 : response.status === 403 ? 10 * 60_000 : 60_000);\n return;\n }\n const result = JSON.parse(await response.text()) as ReplayGrant;\n if (generation !== epoch || !shouldRun()) return;\n if (!result.sampled) {\n unsampledSession = result.sessionId || runtime.sessionId();\n retryAt = Date.now() + 10 * 60_000;\n return;\n }\n if (\n result.version !== 1 ||\n result.streamId !== streamId ||\n !result.token ||\n !Number.isFinite(result.serverTime) ||\n !Number.isFinite(result.expiresAt)\n ) {\n fail();\n return;\n }\n const { record } = await load();\n if (generation !== epoch || !shouldRun()) return;\n if (recordingOwner && recordingOwner !== owner) return;\n recordingOwner = owner;\n grant = result;\n capturedSession = result.sessionId;\n clockOffset = result.serverTime - Date.now();\n active = true;\n const blockSelector = [\n \"[data-reopt-replay-block]\",\n \"[data-reopt-private]\",\n \"iframe\",\n \"object\",\n \"embed\",\n \"input[type=password]\",\n config.blockSelector,\n ]\n .filter(Boolean)\n .join(\",\");\n // Invalid selectors fail before observing the page.\n document.querySelector(blockSelector);\n const stop = record({\n emit: (event) => {\n if (generation === epoch) emit(event);\n },\n maskAllInputs: true,\n maskTextSelector: \"*\",\n blockSelector,\n blockClass: \"rr-block\",\n inlineStylesheet: true,\n collectFonts: false,\n inlineImages: false,\n recordCanvas: false,\n recordCrossOriginIframes: false,\n // Full snapshots dominate recorded bytes (about 90% measured with\n // minute checkouts, ~440 KiB each against ~1 KiB/s of mutations), so\n // checkouts are rare. Playback still seeks inside a window from its\n // snapshot; a five-minute window stays far under the 8 MiB read cap.\n checkoutEveryNms: 5 * 60_000,\n sampling: { mousemove: 100, scroll: 150, input: \"last\" },\n slimDOMOptions: {\n script: true,\n comment: true,\n headMetaDescKeywords: true,\n headMetaSocial: true,\n headMetaRobots: true,\n headMetaHttpEquiv: true,\n headMetaAuthorship: true,\n headMetaVerification: true,\n },\n errorHandler: () => {\n queueMicrotask(() => {\n if (generation === epoch) fail();\n });\n return true;\n },\n });\n if (!active || generation !== epoch) {\n stop?.();\n return;\n }\n stopRecord = stop;\n if (!stopRecord) fail();\n else void flush();\n } catch {\n if (generation === epoch) fail();\n } finally {\n if (generation === epoch) starting = false;\n }\n }\n\n /** The grant is still valid for five minutes: deliver what is buffered, then start a fresh stream. */\n function rotate(): void {\n rotating = true;\n const epoch = generation;\n stopRecord?.();\n stopRecord = undefined;\n active = false;\n void flush().finally(() => {\n rotating = false;\n if (generation === epoch) discard();\n sync();\n });\n }\n\n function sync(): void {\n const identityChanged =\n (active || starting) &&\n (capturedDevice !== runtime.deviceId() ||\n (capturedSession && runtime.sessionId() && capturedSession !== runtime.sessionId()));\n if ((!shouldRun() || identityChanged) && (active || starting || grant || queue.length || inFlight)) discard();\n else if (grant && active && !rotating && Date.now() + clockOffset >= grant.expiresAt - 5 * 60_000) rotate();\n if (shouldRun()) void start();\n }\n const onActivity = () => {\n lastActivity = Date.now();\n sync();\n };\n const onHidden = () => {\n if (document.hidden || pageHidden) {\n stopRecord?.();\n stopRecord = undefined;\n active = false;\n // A final, bounded best-effort flush. Consent withdrawal always discards.\n void flush().finally(() => {\n if (document.hidden || pageHidden) discard();\n });\n } else {\n discard();\n lastActivity = Date.now();\n sync();\n }\n };\n const onPageHide = () => {\n pageHidden = true;\n onHidden();\n };\n const onPageShow = () => {\n pageHidden = false;\n onHidden();\n };\n for (const name of [\"pointerdown\", \"keydown\", \"scroll\"])\n document.addEventListener(name, onActivity, { passive: true });\n document.addEventListener(\"visibilitychange\", onHidden);\n window.addEventListener(\"pagehide\", onPageHide);\n window.addEventListener(\"pageshow\", onPageShow);\n const timer = setInterval(() => {\n sync();\n // A retry whose backoff has elapsed goes out now rather than waiting for\n // the next scheduled flush; the flush interval paces new chunks, not retries.\n const retryDue = queue.length > 0 && retryAt > 0 && Date.now() >= retryAt;\n if (retryDue || Date.now() - flushingAt >= flushInterval) {\n flushingAt = Date.now();\n void flush();\n }\n }, 1000);\n sync();\n return {\n sync,\n flush,\n stop() {\n permanentlyStopped = true;\n discard();\n clearInterval(timer);\n for (const name of [\"pointerdown\", \"keydown\", \"scroll\"]) document.removeEventListener(name, onActivity);\n document.removeEventListener(\"visibilitychange\", onHidden);\n window.removeEventListener(\"pagehide\", onPageHide);\n window.removeEventListener(\"pageshow\", onPageShow);\n },\n };\n}\n"],"mappings":"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"}
@@ -1115,7 +1115,6 @@ var PageLeaveTracker = class {
1115
1115
  } : {}
1116
1116
  };
1117
1117
  }
1118
- resume() {}
1119
1118
  peek() {
1120
1119
  return this.current;
1121
1120
  }
@@ -1271,7 +1270,6 @@ function makeStore(kind, writeKey, backend) {
1271
1270
  writeDevice: (state) => backend.setItem(deviceName, (0, _reopt_ai_data_contract_identity.serializeDeviceCookie)(state)),
1272
1271
  clearDevice: () => backend.removeItem(deviceName),
1273
1272
  readConsent,
1274
- writeConsent: (state) => backend.setItem(consentName, (0, _reopt_ai_data_contract_identity.serializeConsentCookie)(state)),
1275
1273
  consentBackend: () => ({
1276
1274
  getItem: () => {
1277
1275
  const state = readConsent();
@@ -1376,6 +1374,7 @@ function readCaptureExceptionOptions(value) {
1376
1374
  return { properties: value };
1377
1375
  }
1378
1376
  var ReoptClient = class extends ReoptCore {
1377
+ replay;
1379
1378
  writeKey;
1380
1379
  capture;
1381
1380
  store;
@@ -1423,6 +1422,7 @@ var ReoptClient = class extends ReoptCore {
1423
1422
  deviceId: current?.deviceId ?? identity.deviceId,
1424
1423
  sessionId: session.header
1425
1424
  });
1425
+ box.replay?.();
1426
1426
  },
1427
1427
  storage: routedStorage(`${storagePrefix}consent`, store.consentBackend(), queueStorage),
1428
1428
  now,
@@ -1498,8 +1498,13 @@ var ReoptClient = class extends ReoptCore {
1498
1498
  const release = (config.release ?? globalThis.__REOPT_RELEASE__)?.trim();
1499
1499
  if (release) this.register({ $release_id: release });
1500
1500
  box.leave = () => this.emitPageLeave();
1501
+ box.replay = () => this.replay?.sync();
1501
1502
  this.log(`Device ${identity.deviceId} (${identity.source}, ${store.kind})`);
1502
1503
  if (config.bootstrap?.consent && !store.readConsent() && Object.keys(config.bootstrap.consent).length > 0) this.replaceConsentState(config.bootstrap.consent);
1504
+ if (config.sessionReplay?.enabled && transportFetch && typeof document !== "undefined") Promise.resolve().then(() => require("./recorder-BSzzSd3Q.cjs")).then(({ default: createClientReplay }) => {
1505
+ if (this.torndown) return;
1506
+ this.replay = createClientReplay(this, config, transportFetch, session, () => !this.torndown && !this.consent.isPaused(), () => this.currentPath().path);
1507
+ }).catch(() => {});
1503
1508
  const hosts = resolveTracingHosts(config.tracingHeaders);
1504
1509
  if (hosts.length > 0) Promise.resolve().then(() => require("./tracing-DAhaKkl0.cjs")).then(({ installTracingHeaders }) => {
1505
1510
  if (!this.torndown) this.uninstallers.push(installTracingHeaders({
@@ -1608,6 +1613,7 @@ var ReoptClient = class extends ReoptCore {
1608
1613
  }
1609
1614
  pauseTracking() {
1610
1615
  super.pauseTracking();
1616
+ this.replay?.sync();
1611
1617
  this.observe([
1612
1618
  "tracking",
1613
1619
  Date.now(),
@@ -1616,6 +1622,7 @@ var ReoptClient = class extends ReoptCore {
1616
1622
  }
1617
1623
  resumeTracking() {
1618
1624
  super.resumeTracking();
1625
+ this.replay?.sync();
1619
1626
  this.observe([
1620
1627
  "tracking",
1621
1628
  Date.now(),
@@ -1627,6 +1634,7 @@ var ReoptClient = class extends ReoptCore {
1627
1634
  this.observeIdentity("profile_changed");
1628
1635
  }
1629
1636
  syncOptOut() {
1637
+ this.replay?.sync();
1630
1638
  const now = (0, _reopt_ai_data_contract_identity.isOptedOut)(this.getConsentState());
1631
1639
  if (now === this.optedOut.value) return;
1632
1640
  this.optedOut.value = now;
@@ -1685,7 +1693,7 @@ var ReoptClient = class extends ReoptCore {
1685
1693
  addExceptionStep(step);
1686
1694
  }
1687
1695
  loadExceptionChunk() {
1688
- this.exceptionChunkPromise ??= Promise.resolve().then(() => require("./exceptions-CuhGo9A5.cjs")).then((chunk) => {
1696
+ this.exceptionChunkPromise ??= Promise.resolve().then(() => require("./exceptions-BPmC4TTt.cjs")).then((chunk) => {
1689
1697
  this.exceptionChunk = chunk;
1690
1698
  return chunk;
1691
1699
  });
@@ -1716,10 +1724,15 @@ var ReoptClient = class extends ReoptCore {
1716
1724
  super.reset();
1717
1725
  this.session.header = null;
1718
1726
  this.store.writeDevice({ deviceId: this.getDeviceId() });
1727
+ this.replay?.sync();
1719
1728
  this.observeIdentity("reset");
1720
1729
  }
1730
+ flushReplay() {
1731
+ return this.replay?.flush() ?? Promise.resolve();
1732
+ }
1721
1733
  async close() {
1722
1734
  this.torndown = true;
1735
+ this.replay?.stop();
1723
1736
  this.emitPageLeave();
1724
1737
  this.scroll?.stop();
1725
1738
  for (const uninstall of this.uninstallers.splice(0)) uninstall();
@@ -1807,4 +1820,4 @@ Object.defineProperty(exports, "getOrCreateClient", {
1807
1820
  }
1808
1821
  });
1809
1822
 
1810
- //# sourceMappingURL=registry-CGqrOtf4.cjs.map
1823
+ //# sourceMappingURL=registry-DR1N7wD5.cjs.map