@guidelytech/sdk-core 0.10.0 → 0.11.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/{capture-DVJyQ2JZ.d.cts → capture-CgN69Lxq.d.cts} +2 -2
- package/dist/{capture-369s4YYT.d.ts → capture-Csz721-A.d.ts} +2 -2
- package/dist/capture.cjs +2 -0
- package/dist/capture.cjs.map +1 -1
- package/dist/capture.d.cts +3 -3
- package/dist/capture.d.ts +3 -3
- package/dist/capture.js +1 -1
- package/dist/{chunk-C4WWSF67.js → chunk-JUSM7J5N.js} +3 -1
- package/dist/{chunk-C4WWSF67.js.map → chunk-JUSM7J5N.js.map} +1 -1
- package/dist/{chunk-GDJB466J.js → chunk-ZAF2KRJA.js} +29 -6
- package/dist/chunk-ZAF2KRJA.js.map +1 -0
- package/dist/headless.cjs +32 -4
- package/dist/headless.cjs.map +1 -1
- package/dist/headless.d.cts +6 -5
- package/dist/headless.d.ts +6 -5
- package/dist/headless.js +9 -3
- package/dist/{health-DFHUstWd.d.cts → health-sbP9JwTE.d.cts} +2 -0
- package/dist/{health-DFHUstWd.d.ts → health-sbP9JwTE.d.ts} +2 -0
- package/dist/{http-D0dCcpLN.d.cts → http-Cb6KYYFv.d.cts} +1 -1
- package/dist/{http-DJd8jRhK.d.ts → http-D047Ngxk.d.ts} +1 -1
- package/dist/index.cjs +1632 -1045
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +77 -9
- package/dist/index.d.ts +77 -9
- package/dist/index.js +760 -201
- package/dist/index.js.map +1 -1
- package/dist/recorder.cjs.map +1 -1
- package/dist/recorder.d.cts +1 -1
- package/dist/recorder.d.ts +1 -1
- package/dist/recorder.js +1 -1
- package/dist/{types-B5ecgHUV.d.cts → types-BDlebXrm.d.ts} +59 -5
- package/dist/{types-BtPovimB.d.ts → types-Mtlmlj5C.d.cts} +59 -5
- package/package.json +4 -4
- package/dist/chunk-GDJB466J.js.map +0 -1
package/dist/recorder.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/recorder.ts","../src/network.ts","../src/replay.ts","../src/http.ts","../src/storage.ts"],"sourcesContent":["/**\n * Session-replay chunk: the only module that imports rrweb. Built as its own entry and loaded on\n * demand (`import(\"./recorder\")`) together with the replay controller, so the base SDK pays for\n * neither. The script-tag build ships it as `guidely-recorder.js` (assigns `window.__guidelyRecorder`).\n */\nimport { record } from \"@rrweb/record\";\nimport { getRecordConsolePlugin } from \"@rrweb/rrweb-plugin-console-record\";\nimport type { RecordPlugin } from \"@rrweb/types\";\nimport type { RecorderApi, RecorderHandle, RecorderStartOptions } from \"./recorder-types\";\nimport { networkRecordPlugin } from \"./network\";\nimport { ReplayController } from \"./replay\";\n\nexport type { RecorderApi, RecorderHandle, RecorderStartOptions } from \"./recorder-types\";\nexport { ReplayController } from \"./replay\";\nexport type { ReplayControllerOptions, ReplayRecordingConfig, ReplayChunkMeta } from \"./replay\";\nexport type { NetworkCaptureOptions } from \"./network\";\n\n/** Starts an rrweb recording with Guidely's privacy defaults (inputs always masked, no canvas, no fonts). */\nexport function startRecorder(options: RecorderStartOptions): RecorderHandle | null {\n const onError = (err: unknown) => {\n try {\n options.onError?.(err);\n } catch {\n /* ignore */\n }\n return true; // handled: never rethrow into the host page\n };\n try {\n const network = options.network ? networkRecordPlugin(options.network) : null;\n const plugins: RecordPlugin[] = [];\n if (options.recordConsole) plugins.push(getRecordConsolePlugin({ level: [\"error\", \"warn\", \"info\", \"log\"], lengthThreshold: 200 }));\n if (network) plugins.push(network.plugin as unknown as RecordPlugin);\n const stop = record({\n emit: (event) => options.emit(event as { type: number; timestamp: number }),\n maskAllInputs: true,\n maskTextSelector: options.maskTextSelector || undefined,\n blockSelector: options.blockSelector || undefined,\n slimDOMOptions: { script: true, comment: true, headFavicon: true, headWhitespace: true, headMetaSocial: true, headMetaRobots: true, headMetaHttpEquiv: true, headMetaVerification: true, headMetaAuthorship: true, headMetaDescKeywords: true },\n sampling: { mousemove: 50, scroll: 150, input: \"last\" },\n inlineStylesheet: true,\n recordCanvas: false,\n collectFonts: false,\n inlineImages: false,\n plugins,\n errorHandler: onError,\n });\n if (!stop) return null;\n return {\n stop: () => {\n try {\n stop();\n } catch (err) {\n onError(err);\n }\n },\n addCustomEvent: (tag, payload) => {\n try {\n record.addCustomEvent(tag, payload);\n } catch {\n /* not recording */\n }\n },\n flush: () => network?.flush(),\n };\n } catch (err) {\n onError(err);\n return null;\n }\n}\n\n/** The recorder chunk's API (the script-tag build substitutes a loader that resolves to it). */\nexport const recorder: RecorderApi = { startRecorder, createReplay: (options) => new ReplayController({ ...options, startRecorder }) };\n","/**\n * Network recording for session replays (part of the lazily loaded recorder chunk, never the base\n * SDK): patches `fetch`, `XMLHttpRequest` and `WebSocket` while recording, observes resource timing,\n * and emits batched `NetworkPluginPayload`s as rrweb plugin events (`NETWORK_PLUGIN`).\n * The page's own requests behave exactly as before: the original promise / XHR / socket is returned\n * untouched, bodies are read from clones, and every recording step is wrapped in try/catch.\n */\nimport { NETWORK_PLUGIN, SENSITIVE_HEADER_PATTERN, SENSITIVE_PARAM_PATTERN, WS_MAX_MESSAGE_BYTES, WS_MAX_MESSAGES_PER_SOCKET, randomId, type NetworkPluginPayload, type NetworkRequest, type NetworkRequestType, type WebSocketMessage } from \"@guidelytech/shared/runtime\";\n\n/** Network recording settings (merged from the project setting and SDK options by the client). */\nexport interface NetworkCaptureOptions {\n /** Record headers and text bodies (`recordNetworkPayloads`). */\n payloads: boolean;\n /** Guidely API base URL: requests to `<origin>/api/v1/sdk/` are never recorded. */\n apiUrl?: string;\n /** Last say over each request: return a modified copy, or null to drop it (exceptions drop it too). */\n mask?: (request: NetworkRequest) => NetworkRequest | null;\n}\n\n/** Bodies above this many characters are cut and marked `[truncated]`. */\nexport const MAX_BODY_CHARS = 10 * 1024;\nexport const MAX_REQUESTS_PER_PAGE = 500;\nconst BATCH_SIZE = 20;\nconst BATCH_MS = 1000;\nconst BODY_TIMEOUT_MS = 2000;\nconst REDACTED = \"[redacted]\";\n\n// Per page load (the chunk is loaded once per page).\nlet requestCount = 0;\n/** Resource-timing entries that started before this time were already reported by an earlier recording. */\nlet perfCutoff = 0;\n\nconst now = () => Date.now();\n\n/** Absolute URL with sensitive query parameter values replaced by `[redacted]`. */\nexport function redactUrl(raw: string, base = typeof location !== \"undefined\" ? location.href : \"http://localhost/\"): string {\n try {\n const u = new URL(raw, base);\n u.searchParams.forEach((_v, k) => {\n if (SENSITIVE_PARAM_PATTERN.test(k)) u.searchParams.set(k, REDACTED);\n });\n return u.toString().replace(/%5Bredacted%5D/g, REDACTED);\n } catch {\n return String(raw);\n }\n}\n\n/** Recursively masks values of sensitive-looking keys. */\nexport function maskJson(value: unknown, depth = 0): unknown {\n if (depth > 20 || !value || typeof value !== \"object\") return value;\n if (Array.isArray(value)) return value.map((v) => maskJson(v, depth + 1));\n const out: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(value)) out[k] = SENSITIVE_PARAM_PATTERN.test(k) ? REDACTED : maskJson(v, depth + 1);\n return out;\n}\n\nfunction cap(text: string, max: number): string {\n return text.length > max ? `${text.slice(0, max)}[truncated]` : text;\n}\n\n/** Body as recorded: text/JSON/form only (JSON keys masked), ≤10KB; `[binary]` otherwise. */\nexport function formatBody(body: unknown, contentType: string | null | undefined): string | undefined {\n if (body == null || body === \"\") return undefined;\n const ct = (contentType || \"\").toLowerCase();\n if (typeof URLSearchParams !== \"undefined\" && body instanceof URLSearchParams) return formatBody(body.toString(), \"application/x-www-form-urlencoded\");\n if (typeof body !== \"string\") return \"[binary]\";\n if (ct && !/^text\\/|json|x-www-form-urlencoded/.test(ct)) return \"[binary]\";\n if (ct.includes(\"x-www-form-urlencoded\")) {\n const params = new URLSearchParams(body);\n params.forEach((_v, k) => {\n if (SENSITIVE_PARAM_PATTERN.test(k)) params.set(k, REDACTED);\n });\n return cap(params.toString().replace(/%5Bredacted%5D/g, REDACTED), MAX_BODY_CHARS);\n }\n if (ct.includes(\"json\") || (!ct && /^\\s*[[{]/.test(body))) {\n try {\n return cap(JSON.stringify(maskJson(JSON.parse(body))), MAX_BODY_CHARS);\n } catch {\n /* not JSON after all */\n }\n }\n return cap(body, MAX_BODY_CHARS);\n}\n\n/** Lower-cased header names; sensitive values `[redacted]`. */\nexport function formatHeaders(entries: Iterable<[string, string]>): Record<string, string> {\n const out: Record<string, string> = {};\n for (const [k, v] of entries) {\n const name = k.toLowerCase();\n out[name] = SENSITIVE_HEADER_PATTERN.test(name) ? REDACTED : String(v);\n }\n return out;\n}\n\nfunction headerEntries(h: unknown): Array<[string, string]> {\n if (!h) return [];\n if (Array.isArray(h)) return h.map(([k, v]) => [String(k), String(v)]);\n if (typeof (h as Headers).forEach === \"function\") {\n const out: Array<[string, string]> = [];\n (h as Headers).forEach((v, k) => out.push([k, v]));\n return out;\n }\n return Object.entries(h as Record<string, string>).map(([k, v]) => [k, String(v)]);\n}\n\n/** UTF-8 byte length of a string, or the size of binary data. */\nexport function byteSize(data: unknown): number {\n if (typeof data === \"string\") return typeof TextEncoder !== \"undefined\" ? new TextEncoder().encode(data).length : data.length;\n if (data instanceof ArrayBuffer) return data.byteLength;\n if (ArrayBuffer.isView(data)) return data.byteLength;\n if (typeof Blob !== \"undefined\" && data instanceof Blob) return data.size;\n return 0;\n}\n\nfunction frameData(data: unknown, size: number): string {\n if (typeof data !== \"string\") return `[binary ${size} B]`;\n let text = data;\n try {\n if (/^\\s*[[{]/.test(text)) text = JSON.stringify(maskJson(JSON.parse(text)));\n } catch {\n /* plain text */\n }\n return cap(text, WS_MAX_MESSAGE_BYTES);\n}\n\nconst RESOURCE_TYPES: Record<string, NetworkRequestType> = { script: \"script\", link: \"stylesheet\", css: \"stylesheet\", img: \"image\", image: \"image\", audio: \"media\", video: \"media\", track: \"media\", beacon: \"beacon\", navigation: \"document\", iframe: \"document\", frame: \"document\" };\n\n/** A running capture: `flush()` emits pending items now; `stop()` flushes and restores everything. */\nexport interface NetworkCapture {\n flush(): void;\n stop(): void;\n}\n\ntype Win = Window & typeof globalThis;\n\n/** Starts recording the window's network activity; `emit` receives batched payloads. */\nexport function startNetworkCapture(win: Win, opts: NetworkCaptureOptions, emit: (payload: NetworkPluginPayload) => void): NetworkCapture {\n let active = true;\n const startedAt = now();\n let requests: NetworkRequest[] = [];\n let messages: WebSocketMessage[] = [];\n let droppedRequests = 0;\n let droppedMessages = 0;\n let timer: ReturnType<typeof setTimeout> | null = null;\n /** Recent fetch/XHR requests, for enriching with resource timing. */\n const recent: NetworkRequest[] = [];\n const cleanups: Array<() => void> = [];\n let apiPrefix = \"\";\n try {\n if (opts.apiUrl) apiPrefix = `${new URL(opts.apiUrl).origin}/api/v1/sdk/`;\n } catch {\n /* invalid apiUrl */\n }\n\n const excluded = (url: string) => /^(data|blob|about|chrome-extension):/i.test(url) || (!!apiPrefix && url.replace(/^ws(s?):/i, \"http$1:\").startsWith(apiPrefix)) || /\\/guidely-(recorder|capture)(\\.min)?\\.js(\\?|$)/.test(url);\n\n const flush = () => {\n if (timer) clearTimeout(timer);\n timer = null;\n if (!requests.length && !messages.length && !droppedRequests && !droppedMessages) return;\n const payload: NetworkPluginPayload = {};\n if (requests.length) payload.requests = requests;\n if (messages.length) payload.messages = messages;\n if (droppedRequests || droppedMessages) payload.dropped = { ...(droppedRequests ? { requests: droppedRequests } : {}), ...(droppedMessages ? { messages: droppedMessages } : {}) };\n requests = [];\n messages = [];\n droppedRequests = 0;\n droppedMessages = 0;\n try {\n emit(payload);\n } catch {\n /* recorder stopped */\n }\n };\n const schedule = () => {\n if (requests.length + messages.length >= BATCH_SIZE) flush();\n else timer ??= setTimeout(flush, BATCH_MS);\n };\n\n /** Applies the mask hook; null drops. `counted` requests (re-emissions) skip the per-page cap. */\n const push = (req: NetworkRequest, counted = false): boolean => {\n if (!active) return false;\n if (!counted) {\n if (requestCount >= MAX_REQUESTS_PER_PAGE) {\n droppedRequests += 1;\n schedule();\n return false;\n }\n requestCount += 1;\n }\n // Pushed as-is (resource timing may still enrich it before the batch is sent); the hook gets a copy.\n let out: NetworkRequest | null = req;\n if (opts.mask) {\n try {\n out = opts.mask({ ...req });\n } catch {\n out = null;\n }\n }\n if (!out) return false;\n requests.push(out);\n schedule();\n return true;\n };\n const finish = (req: NetworkRequest, fields: Partial<NetworkRequest>) => {\n Object.assign(req, fields);\n req.endTime = now();\n req.durationMs = req.endTime - req.startTime;\n };\n const remember = (req: NetworkRequest) => {\n recent.push(req);\n if (recent.length > 100) recent.shift();\n };\n\n // --- fetch -------------------------------------------------------------------------------------\n const origFetch = win.fetch;\n if (typeof origFetch === \"function\") {\n const patched = function (this: unknown, input: RequestInfo | URL, init?: RequestInit): Promise<Response> {\n const p = origFetch.call(this === undefined || this === patched ? win : (this as Win), input, init);\n if (!active) return p;\n try {\n const isReq = typeof Request !== \"undefined\" && input instanceof Request;\n const rawUrl = isReq ? (input as Request).url : String(input instanceof URL ? input.href : input);\n const url = redactUrl(rawUrl);\n if (excluded(url) || excluded(rawUrl)) return p;\n const req: NetworkRequest = { id: randomId(12), type: \"fetch\", method: String(init?.method || (isReq ? (input as Request).method : \"GET\")).toUpperCase(), url, startTime: now() };\n if (opts.payloads) {\n const headers = headerEntries(init?.headers ?? (isReq ? (input as Request).headers : undefined));\n req.requestHeaders = formatHeaders(headers);\n const body = formatBody(init?.body, req.requestHeaders[\"content-type\"] ?? (typeof init?.body === \"string\" ? \"text/plain\" : undefined));\n if (body !== undefined) req.requestBody = body;\n }\n remember(req);\n p.then(\n (res) => {\n try {\n finish(req, { status: res.status });\n if (!opts.payloads) return void push(req);\n req.responseHeaders = formatHeaders(headerEntries(res.headers));\n const ct = res.headers.get(\"content-type\");\n if (!ct || !/^text\\/(?!event-stream)|json|x-www-form-urlencoded/i.test(ct) || !res.body) {\n if (ct && res.body) req.responseBody = \"[binary]\";\n return void push(req);\n }\n // Read a clone (never the app's stream), at most ~10KB and 2s, then send.\n const reader = res.clone().body!.getReader();\n const decoder = new TextDecoder();\n let text = \"\";\n const done = () => {\n req.responseBody = formatBody(text, ct);\n push(req);\n };\n const t = setTimeout(() => void reader.cancel().catch(() => undefined), BODY_TIMEOUT_MS);\n const read = (): Promise<void> =>\n reader.read().then(({ done: end, value }) => {\n if (value) text += decoder.decode(value, { stream: true });\n if (end) return;\n if (text.length > MAX_BODY_CHARS * 4) return void reader.cancel().catch(() => undefined);\n return read();\n });\n read()\n .catch(() => undefined)\n .then(() => {\n clearTimeout(t);\n done();\n });\n } catch {\n /* never affect the app */\n }\n },\n (err: unknown) => {\n try {\n finish(req, { status: 0, error: err instanceof Error && err.name === \"AbortError\" ? \"aborted\" : String((err as Error)?.message || err) });\n push(req);\n } catch {\n /* ignore */\n }\n },\n );\n } catch {\n /* never affect the app */\n }\n return p;\n };\n win.fetch = patched as typeof fetch;\n cleanups.push(() => {\n if (win.fetch === (patched as typeof fetch)) win.fetch = origFetch;\n });\n }\n\n // --- XMLHttpRequest ----------------------------------------------------------------------------\n const XHR = win.XMLHttpRequest;\n if (XHR?.prototype) {\n const proto = XHR.prototype;\n const { open, send, setRequestHeader } = proto;\n const state = new WeakMap<XMLHttpRequest, { method: string; url: string; headers: Array<[string, string]> }>();\n const pOpen = function (this: XMLHttpRequest, method: string, url: string | URL, ...rest: unknown[]) {\n try {\n if (active) state.set(this, { method: String(method).toUpperCase(), url: String(url), headers: [] });\n } catch {\n /* ignore */\n }\n return (open as (...a: unknown[]) => void).call(this, method, url, ...rest);\n };\n const pSetHeader = function (this: XMLHttpRequest, name: string, value: string) {\n try {\n state.get(this)?.headers.push([name, value]);\n } catch {\n /* ignore */\n }\n return setRequestHeader.call(this, name, value);\n };\n const pSend = function (this: XMLHttpRequest, body?: Document | XMLHttpRequestBodyInit | null) {\n try {\n const s = state.get(this);\n const url = s ? redactUrl(s.url) : \"\";\n if (active && s && !excluded(url) && !excluded(s.url)) {\n const req: NetworkRequest = { id: randomId(12), type: \"xhr\", method: s.method, url, startTime: now() };\n if (opts.payloads) {\n req.requestHeaders = formatHeaders(s.headers);\n const b = formatBody(body, req.requestHeaders[\"content-type\"] ?? (typeof body === \"string\" ? \"text/plain\" : undefined));\n if (b !== undefined) req.requestBody = b;\n }\n remember(req);\n let ended = false;\n const end = (error?: string) => () => {\n if (ended) return;\n ended = true;\n try {\n finish(req, { status: error ? 0 : this.status, ...(error ? { error } : {}) });\n if (opts.payloads && !error) {\n const raw = this.getAllResponseHeaders() || \"\";\n req.responseHeaders = formatHeaders(raw.trim().split(/[\\r\\n]+/).filter(Boolean).map((line) => [line.slice(0, line.indexOf(\":\")).trim(), line.slice(line.indexOf(\":\") + 1).trim()] as [string, string]));\n const ct = this.getResponseHeader(\"content-type\");\n const type = this.responseType;\n const text = type === \"\" || type === \"text\" ? this.responseText : type === \"json\" ? JSON.stringify(this.response) : null;\n const formatted = text == null ? (this.response ? \"[binary]\" : undefined) : formatBody(text, ct ?? (type === \"json\" ? \"application/json\" : \"text/plain\"));\n if (formatted !== undefined) req.responseBody = formatted;\n }\n push(req);\n } catch {\n /* ignore */\n }\n };\n this.addEventListener(\"load\", end());\n this.addEventListener(\"error\", end(\"network error\"));\n this.addEventListener(\"abort\", end(\"aborted\"));\n this.addEventListener(\"timeout\", end(\"timeout\"));\n }\n } catch {\n /* never affect the app */\n }\n return send.call(this, body);\n };\n proto.open = pOpen as typeof open;\n proto.setRequestHeader = pSetHeader;\n proto.send = pSend;\n cleanups.push(() => {\n if (proto.open === (pOpen as typeof open)) proto.open = open;\n if (proto.setRequestHeader === pSetHeader) proto.setRequestHeader = setRequestHeader;\n if (proto.send === pSend) proto.send = send;\n });\n }\n\n // --- WebSocket ---------------------------------------------------------------------------------\n const WS = win.WebSocket;\n if (typeof WS === \"function\") {\n const track = (ws: WebSocket, rawUrl: string) => {\n const url = redactUrl(rawUrl);\n if (excluded(url)) return;\n const req: NetworkRequest = { id: randomId(12), type: \"websocket\", method: \"GET\", url, startTime: now() };\n const totals = { sent: 0, received: 0, sentBytes: 0, receivedBytes: 0, recorded: 0 };\n let dropped = false;\n let counted = false;\n const emitConn = (fields: Partial<NetworkRequest>) => {\n if (dropped || !active) return;\n Object.assign(req, fields, { messages: { ...totals } });\n dropped = !push({ ...req }, counted);\n counted = true;\n };\n const frame = (dir: \"send\" | \"recv\", data: unknown) => {\n try {\n const size = byteSize(data);\n if (dir === \"send\") {\n totals.sent += 1;\n totals.sentBytes += size;\n } else {\n totals.received += 1;\n totals.receivedBytes += size;\n }\n if (dropped || !active) return;\n if (totals.recorded >= WS_MAX_MESSAGES_PER_SOCKET) {\n droppedMessages += 1;\n return schedule();\n }\n totals.recorded += 1;\n const m: WebSocketMessage = { socketId: req.id, dir, time: now(), size };\n if (typeof data !== \"string\") m.binary = true;\n if (opts.payloads) m.data = frameData(data, size);\n messages.push(m);\n schedule();\n } catch {\n /* ignore */\n }\n };\n const nativeSend = ws.send;\n // Instance-level wrapper: the socket keeps working after recording stops (it just stops being recorded).\n ws.send = function (this: WebSocket, data: string | ArrayBufferLike | Blob | ArrayBufferView) {\n const r = nativeSend.call(this, data); // throws (unrecorded) when the socket is not open\n frame(\"send\", data);\n return r;\n };\n ws.addEventListener(\"open\", () => emitConn({ protocol: ws.protocol || undefined }));\n ws.addEventListener(\"message\", (e: MessageEvent) => frame(\"recv\", e.data));\n let errored = false;\n ws.addEventListener(\"error\", () => {\n errored = true;\n });\n ws.addEventListener(\"close\", (e: CloseEvent) => {\n const endTime = now();\n emitConn({ endTime, durationMs: endTime - req.startTime, closeCode: e.code, closeReason: e.reason || undefined, ...(errored || e.code === 1006 ? { error: \"connection error\" } : {}) });\n });\n };\n const Patched = class extends WS {\n constructor(url: string | URL, protocols?: string | string[]) {\n super(url, protocols);\n try {\n if (active) track(this, String(url instanceof URL ? url.href : url));\n } catch {\n /* never affect the app */\n }\n }\n static override [Symbol.hasInstance](x: unknown): boolean {\n return x instanceof WS;\n }\n };\n Object.defineProperty(Patched, \"name\", { value: WS.name });\n win.WebSocket = Patched as typeof WebSocket;\n cleanups.push(() => {\n if (win.WebSocket === (Patched as typeof WebSocket)) win.WebSocket = WS;\n });\n }\n\n // --- Resource timing -------------------------------------------------------------------------\n const PO = win.PerformanceObserver;\n if (typeof PO === \"function\") {\n const origin = win.performance?.timeOrigin ?? startedAt - (win.performance?.now?.() ?? 0);\n const cutoff = perfCutoff;\n const seen = new Set<string>(); // browsers re-deliver the navigation entry once the load completes\n const onEntries = (list: PerformanceObserverEntryList) => {\n for (const e of list.getEntries() as PerformanceResourceTiming[]) {\n try {\n const start = Math.round(origin + e.startTime);\n const key = `${e.entryType}|${e.name}|${e.startTime}`;\n if (start < cutoff || seen.has(key)) continue;\n seen.add(key);\n const url = redactUrl(e.name);\n if (excluded(url) || excluded(e.name)) continue;\n const status = (e as { responseStatus?: number }).responseStatus;\n const timing = { transferSize: e.transferSize, responseSize: e.decodedBodySize, ...(status ? { status } : {}) };\n if (e.initiatorType === \"fetch\" || e.initiatorType === \"xmlhttprequest\") {\n const match = recent.find((r) => r.url === url && Math.abs(r.startTime - start) <= 50);\n if (match) Object.assign(match, { transferSize: timing.transferSize, responseSize: timing.responseSize }, match.status === undefined && status ? { status } : {});\n continue;\n }\n const type = e.entryType === \"navigation\" ? \"document\" : /\\.(woff2?|ttf|otf|eot)(\\?|$)/i.test(e.name) ? \"font\" : (RESOURCE_TYPES[e.initiatorType] ?? \"other\");\n push({ id: randomId(12), type, method: \"GET\", url, startTime: start, endTime: Math.round(origin + e.responseEnd), durationMs: Math.round(e.duration), ...timing });\n } catch {\n /* ignore */\n }\n }\n };\n for (const type of [\"resource\", \"navigation\"]) {\n try {\n const po = new PO(onEntries);\n po.observe({ type, buffered: true });\n cleanups.push(() => po.disconnect());\n } catch {\n /* unsupported entry type */\n }\n }\n }\n\n return {\n flush,\n stop: () => {\n if (!active) return;\n flush();\n active = false;\n perfCutoff = now();\n cleanups.splice(0).forEach((fn) => {\n try {\n fn();\n } catch {\n /* ignore */\n }\n });\n },\n };\n}\n\n/** The rrweb plugin that records network activity as `{ type: 6, data: { plugin: NETWORK_PLUGIN, payload } }`. */\nexport function networkRecordPlugin(opts: NetworkCaptureOptions): { plugin: { name: string; observer: (cb: (payload: unknown) => void, win: Win) => () => void; options: unknown }; flush: () => void } {\n let capture: NetworkCapture | null = null;\n return {\n plugin: {\n name: NETWORK_PLUGIN,\n options: {},\n observer: (cb, win) => {\n // rrweb also calls plugin observers for same-origin iframes: only the top window is patched.\n if (capture || win !== (typeof window !== \"undefined\" ? window : win)) return () => {};\n const c = startNetworkCapture(win, opts, cb);\n capture = c;\n return () => {\n c.stop();\n if (capture === c) capture = null;\n };\n },\n },\n flush: () => capture?.flush(),\n };\n}\n\n/** Test hook: resets the per-page counters. */\nexport function resetNetworkCounters(): void {\n requestCount = 0;\n perfCutoff = 0;\n}\n","import { randomId } from \"@guidelytech/shared/runtime\";\nimport type { ServerHealth } from \"./health\";\nimport { backoffMs } from \"./http\";\nimport type { NetworkCaptureOptions } from \"./network\";\nimport type { RecorderHandle, RecorderStartOptions } from \"./recorder-types\";\nimport { readJson, writeJson, type KeyValueStore } from \"./storage\";\n\n/** Recorder settings for one session (already merged from the project setting and SDK options). */\nexport interface ReplayRecordingConfig {\n maskTextSelector: string;\n blockSelector: string;\n recordConsole: boolean;\n network?: NetworkCaptureOptions | null;\n}\n\n/** Per-upload metadata (`ReplayChunk` minus `sessionId`/`windowId`/`seq`/`events`). */\nexport interface ReplayChunkMeta {\n anonymousId: string;\n userId?: string | null;\n url?: string;\n device?: { type: string; browser?: string; os?: string };\n viewport?: { width: number; height: number };\n sdk?: { name: string; version: string };\n}\n\nexport interface ReplayControllerOptions {\n /** Upload URL for `POST /api/v1/sdk/replay` (with `?key=` and the given params). */\n url: (params: Record<string, string | undefined>) => string;\n fetch: () => typeof fetch | null;\n meta: () => ReplayChunkMeta;\n /** Starts rrweb (`startRecorder` from the recorder chunk; injectable for tests). */\n startRecorder: (options: RecorderStartOptions) => RecorderHandle | null;\n /** The server answered `{ disabled: true }`: recording must stop for this session. */\n onDisabled: (sessionId: string) => void;\n onError: (err: unknown) => void;\n flushIntervalMs?: number;\n /** Buffered bytes that trigger an early upload (default ~1MB). */\n maxBufferBytes?: number;\n /** Upload backlog cap; the oldest chunks are dropped above it (default ~5MB). */\n maxQueuedBytes?: number;\n /** API reachability: uploads wait while its circuit is open. */\n health?: ServerHealth;\n /**\n * Per-tab storage (sessionStorage) for what could not be uploaded when the page is hidden while\n * the API is unreachable; the next page in the tab uploads it. Up to {@link STASH_MAX_BYTES}.\n */\n stash?: KeyValueStore;\n}\n\ninterface Chunk {\n sessionId: string;\n /** A stashed upload body from an earlier page of this tab (its window id and seq are inside). */\n raw?: string;\n meta: ReplayChunkMeta;\n events: string[];\n bytes: number;\n /** Assigned on the first upload attempt and kept for retries (the server dedupes by window + seq). */\n seq?: number;\n}\n\n/** Largest body sent with `sendBeacon` / keepalive fetch while the page is being hidden. */\nexport const UNLOAD_MAX_BYTES = 60 * 1024;\n/** Uploads still running after this long are aborted and retried. */\nexport const UPLOAD_TIMEOUT_MS = 30_000;\n/** Most recording kept in the tab's sessionStorage for the next page while the API is unreachable. */\nexport const STASH_MAX_BYTES = 1_000_000;\nconst STASH_KEY = \"guidely:replay:stash\";\n\ninterface StashedChunk {\n sessionId: string;\n body: string;\n}\n\nasync function gzip(text: string): Promise<Blob> {\n const stream = new Blob([text]).stream().pipeThrough(new CompressionStream(\"gzip\"));\n return new Response(stream).blob();\n}\n\n/**\n * Session-replay controller (part of the lazily loaded recorder chunk): records with rrweb, buffers events, and uploads them\n * every 5s (or once ~1MB is buffered) as gzip JSON (`?enc=gzip`; plain JSON without\n * `CompressionStream`). On page hide it sends what is left with `sendBeacon` / keepalive fetch.\n */\nexport class ReplayController {\n readonly windowId = randomId(16);\n private handle: RecorderHandle | null = null;\n private sessionId: string | null = null;\n private configKey = \"\";\n private buffer: Chunk | null = null;\n private queue: Chunk[] = [];\n private queuedBytes = 0;\n private seqs = new Map<string, number>();\n private interval: ReturnType<typeof setInterval> | null = null;\n private retryTimer: ReturnType<typeof setTimeout> | null = null;\n private attempt = 0;\n private uploading = false;\n private flushing: Promise<void> | null = null;\n private destroyed = false;\n\n constructor(private readonly opts: ReplayControllerOptions) {\n // Recording an earlier page of this tab could not upload (the API was unreachable): send it first.\n if (opts.stash) {\n const stashed = readJson<StashedChunk[]>(opts.stash, STASH_KEY, []);\n opts.stash.remove(STASH_KEY);\n for (const c of Array.isArray(stashed) ? stashed : []) {\n if (!c || typeof c.body !== \"string\" || typeof c.sessionId !== \"string\") continue;\n this.queue.push({ sessionId: c.sessionId, raw: c.body, meta: opts.meta(), events: [], bytes: c.body.length });\n this.queuedBytes += c.body.length;\n }\n if (this.queue.length) void this.flush();\n }\n }\n\n /** True while rrweb is recording. */\n isRecording(): boolean {\n return !!this.handle;\n }\n\n /** The session currently being recorded, if any. */\n get activeSessionId(): string | null {\n return this.sessionId;\n }\n\n /** Starts (or keeps) recording for `sessionId`. Restarts when the session or settings changed. */\n start(sessionId: string, config: ReplayRecordingConfig): void {\n if (this.destroyed) return;\n const key = JSON.stringify(config);\n if (this.sessionId === sessionId && this.configKey === key && this.handle) return;\n this.stop();\n this.handle = this.opts.startRecorder({ ...config, emit: (e) => this.push(sessionId, e), onError: this.opts.onError });\n if (!this.handle) return;\n this.sessionId = sessionId;\n this.configKey = key;\n this.interval = setInterval(() => void this.flush(), this.opts.flushIntervalMs ?? 5000);\n }\n\n /** Stops recording. Buffered events are uploaded unless `discard` (consent withdrawn, authoring). */\n stop(options: { discard?: boolean } = {}): void {\n const handle = this.handle;\n this.handle = null;\n this.sessionId = null;\n this.configKey = \"\";\n if (this.interval) clearInterval(this.interval);\n this.interval = null;\n handle?.stop();\n if (options.discard) {\n this.buffer = null;\n this.queue = [];\n this.queuedBytes = 0;\n } else void this.flush();\n }\n\n /** Adds an rrweb custom event (Guidely event markers) while recording. */\n addCustomEvent(tag: string, payload: unknown): void {\n this.handle?.addCustomEvent(tag, payload);\n }\n\n private push(sessionId: string, event: unknown): void {\n let json: string;\n try {\n json = JSON.stringify(event);\n } catch {\n return;\n }\n if (this.buffer && this.buffer.sessionId !== sessionId) this.seal();\n const chunk = this.buffer ?? (this.buffer = { sessionId, meta: this.opts.meta(), events: [], bytes: 0 });\n chunk.events.push(json);\n chunk.bytes += json.length;\n if (chunk.bytes > (this.opts.maxBufferBytes ?? 1_000_000)) void this.flush();\n }\n\n /** Moves the buffer to the upload queue, dropping the oldest chunks above the backlog cap. */\n private seal(): void {\n const chunk = this.buffer;\n this.buffer = null;\n if (!chunk || (!chunk.events.length && !chunk.raw)) return;\n this.queue.push(chunk);\n this.queuedBytes += chunk.bytes;\n const max = this.opts.maxQueuedBytes ?? 5_000_000;\n while (this.queuedBytes > max && this.queue.length > 1) {\n const i = this.uploading ? 1 : 0;\n const [dropped] = this.queue.splice(i, 1);\n if (dropped) this.queuedBytes -= dropped.bytes;\n }\n }\n\n private nextSeq(chunk: Chunk): number {\n if (chunk.seq === undefined) {\n const seq = this.seqs.get(chunk.sessionId) ?? 0;\n this.seqs.set(chunk.sessionId, seq + 1);\n chunk.seq = seq;\n }\n return chunk.seq;\n }\n\n private body(chunk: Chunk): string {\n if (chunk.raw) return chunk.raw;\n const head = JSON.stringify({ sessionId: chunk.sessionId, windowId: this.windowId, seq: this.nextSeq(chunk), ...chunk.meta });\n return `${head.slice(0, -1)},\"events\":[${chunk.events.join(\",\")}]}`;\n }\n\n private remove(chunk: Chunk): void {\n const i = this.queue.indexOf(chunk);\n if (i >= 0) {\n this.queue.splice(i, 1);\n this.queuedBytes -= chunk.bytes;\n }\n }\n\n /** Uploads the buffer and the backlog, one chunk at a time; retries with backoff on failure. */\n flush(): Promise<void> {\n this.seal();\n if (this.retryTimer || !this.queue.length) return Promise.resolve();\n // An upload loop already running may have finished its last chunk: queue another pass after it.\n if (this.flushing) return this.flushing.then(() => this.flush());\n return (this.flushing = this.drain().finally(() => {\n this.flushing = null;\n }));\n }\n\n private retryIn(ms: number): void {\n if (this.destroyed || this.retryTimer) return;\n this.retryTimer = setTimeout(() => {\n this.retryTimer = null;\n void this.flush();\n }, ms);\n }\n\n private async drain(): Promise<void> {\n this.uploading = true;\n try {\n while (this.queue.length) {\n // The API is down: wait for its circuit instead of uploading into the void.\n const wait = this.opts.health?.waitMs() ?? 0;\n if (wait > 0) return this.retryIn(wait);\n const chunk = this.queue[0] as Chunk;\n const result = await this.send(chunk);\n if (result === \"retry\") {\n // After six quick retries the 5s flush interval tries again, paced by the circuit.\n this.attempt += 1;\n if (this.attempt < 6) this.retryIn(Math.max(backoffMs(this.attempt - 1), this.opts.health?.waitMs() ?? 0));\n else this.attempt = 0;\n return;\n }\n this.attempt = 0;\n this.remove(chunk);\n if (result === \"disabled\") {\n for (const c of this.queue.slice()) if (c.sessionId === chunk.sessionId) this.remove(c);\n this.opts.onDisabled(chunk.sessionId);\n }\n }\n } finally {\n this.uploading = false;\n }\n }\n\n private async send(chunk: Chunk): Promise<\"ok\" | \"drop\" | \"retry\" | \"disabled\"> {\n const f = this.opts.fetch();\n if (!f) return \"drop\";\n try {\n const json = this.body(chunk);\n let body: Blob | string = json;\n let enc: string | undefined;\n if (typeof CompressionStream === \"function\") {\n try {\n body = await gzip(json);\n enc = \"gzip\";\n } catch {\n body = json;\n }\n }\n const size = typeof body === \"string\" ? body.length : body.size;\n const ac = typeof AbortController !== \"undefined\" ? new AbortController() : null;\n const timer = ac ? setTimeout(() => ac.abort(), UPLOAD_TIMEOUT_MS) : null;\n let res: Response;\n try {\n res = await f(this.opts.url({ enc }), { method: \"POST\", body, headers: { \"Content-Type\": \"text/plain\" }, keepalive: size < UNLOAD_MAX_BYTES, signal: ac?.signal, credentials: \"omit\", mode: \"cors\" });\n } catch (err) {\n this.opts.health?.failure();\n throw err;\n } finally {\n if (timer) clearTimeout(timer);\n }\n if (res.status === 429 || res.status >= 500) this.opts.health?.failure();\n else this.opts.health?.success();\n if (res.ok) {\n const data = (await res.json().catch(() => null)) as { disabled?: boolean } | null;\n return data?.disabled ? \"disabled\" : \"ok\";\n }\n return res.status >= 400 && res.status < 500 && res.status !== 429 ? \"drop\" : \"retry\";\n } catch {\n return \"retry\";\n }\n }\n\n /**\n * Page is being hidden / unloaded: sends whatever is buffered synchronously as uncompressed JSON\n * (`sendBeacon` up to 60KB, keepalive fetch otherwise), splitting chunks that never went out.\n */\n flushOnHide(): void {\n this.handle?.flush?.();\n this.seal();\n const pending = this.queue.slice(this.uploading ? 1 : 0);\n // The API isn't answering: a beacon would most likely be lost. Keep the recording in the tab\n // for the next page to upload (it may still beacon what doesn't fit).\n if (this.opts.stash && this.opts.health && !this.opts.health.reachable && pending.length) {\n const kept: StashedChunk[] = [];\n let bytes = 0;\n for (const chunk of pending) {\n const body = this.body(chunk);\n if (bytes + body.length > STASH_MAX_BYTES) break;\n bytes += body.length;\n kept.push({ sessionId: chunk.sessionId, body });\n this.remove(chunk);\n }\n if (kept.length) writeJson(this.opts.stash, STASH_KEY, [...readJson<StashedChunk[]>(this.opts.stash, STASH_KEY, []), ...kept]);\n pending.splice(0, kept.length);\n }\n for (const chunk of pending) {\n const parts = chunk.seq === undefined && !chunk.raw ? splitChunk(chunk, UNLOAD_MAX_BYTES - 2048) : [chunk];\n let sent = true;\n for (const part of parts) {\n if (!this.sendNow(this.body(part))) {\n sent = false;\n break;\n }\n if (part !== chunk) {\n chunk.events.splice(0, part.events.length);\n chunk.bytes -= part.bytes;\n this.queuedBytes -= part.bytes;\n }\n }\n if (!sent) break;\n this.remove(chunk);\n }\n }\n\n private sendNow(json: string): boolean {\n const url = this.opts.url({});\n try {\n if (json.length <= UNLOAD_MAX_BYTES && typeof navigator !== \"undefined\" && typeof navigator.sendBeacon === \"function\" && navigator.sendBeacon(url, new Blob([json], { type: \"text/plain;charset=UTF-8\" }))) return true;\n const f = this.opts.fetch();\n if (!f || json.length > 64 * 1024) return false;\n void f(url, { method: \"POST\", body: json, headers: { \"Content-Type\": \"text/plain\" }, keepalive: true, credentials: \"omit\", mode: \"cors\" }).catch(() => undefined);\n return true;\n } catch {\n return false;\n }\n }\n\n /** Stops recording and uploads what is left; no retries afterwards. */\n destroy(): void {\n this.stop();\n this.destroyed = true;\n if (this.retryTimer) clearTimeout(this.retryTimer);\n this.retryTimer = null;\n }\n}\n\n/** Splits a chunk into parts of at most `max` bytes of events (a single larger event stays whole). */\nfunction splitChunk(chunk: Chunk, max: number): Chunk[] {\n if (chunk.bytes <= max) return [chunk];\n const parts: Chunk[] = [];\n let cur: Chunk | null = null;\n for (const e of chunk.events) {\n if (!cur || (cur.bytes + e.length > max && cur.events.length)) parts.push((cur = { sessionId: chunk.sessionId, meta: chunk.meta, events: [], bytes: 0 }));\n cur.events.push(e);\n cur.bytes += e.length;\n }\n return parts;\n}\n","import { isServerFailure, parseRetryAfter, ServerHealth } from \"./health\";\n\nexport interface ApiResponse<T> {\n status: number;\n ok: boolean;\n data: T | null;\n etag: string | null;\n}\n\nexport type FetchLike = typeof fetch;\n\n/** Reply streams: the response must start within this long, and then never go quiet longer than the idle limit (the server sends a keepalive every 15 s). */\nexport const STREAM_RESPONSE_TIMEOUT_MS = 30_000;\nexport const STREAM_IDLE_TIMEOUT_MS = 60_000;\n\n/** Thin HTTP client for the public SDK API. Adds the public key as query + header. */\nexport class Api {\n /** Reachability of the API, fed by every answer (see {@link ServerHealth}). */\n readonly health: ServerHealth;\n\n constructor(\n private readonly baseUrl: string,\n private readonly key: string,\n private readonly fetchImpl?: FetchLike,\n health?: ServerHealth,\n ) {\n this.health = health ?? new ServerHealth();\n }\n\n /** Feeds an answer (or a failed request, `null`) into {@link health}. */\n private observe(res: Response | null): void {\n if (!res) this.health.failure();\n else if (isServerFailure(res.status)) this.health.failure(parseRetryAfter(res.headers?.get?.(\"retry-after\")));\n else this.health.success();\n }\n\n url(path: string, params: Record<string, string | undefined> = {}): string {\n // Built by hand rather than with URL/searchParams, which React Native does not implement.\n const query = Object.entries({ key: this.key, ...params })\n .filter((e): e is [string, string] => e[1] != null)\n .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)\n .join(\"&\");\n return `${this.baseUrl.replace(/\\/$/, \"\")}/api/v1/sdk${path}?${query}`;\n }\n\n private get fetcher(): FetchLike | null {\n if (this.fetchImpl) return this.fetchImpl;\n return typeof fetch === \"function\" ? fetch.bind(globalThis) : null;\n }\n\n async request<T>(method: \"GET\" | \"POST\", path: string, options: { body?: unknown; etag?: string | null; params?: Record<string, string | undefined>; keepalive?: boolean; timeoutMs?: number; headers?: Record<string, string> } = {}): Promise<ApiResponse<T>> {\n const f = this.fetcher;\n if (!f) throw new Error(\"fetch is not available\");\n const headers: Record<string, string> = { ...options.headers, \"X-Guidely-Key\": this.key, Accept: \"application/json\" };\n // Multipart bodies set their own Content-Type (with the boundary).\n const form = typeof FormData !== \"undefined\" && options.body instanceof FormData;\n if (options.body !== undefined && !form) headers[\"Content-Type\"] = \"application/json\";\n if (options.etag) headers[\"If-None-Match\"] = options.etag;\n const ac = typeof AbortController !== \"undefined\" ? new AbortController() : null;\n const timer = ac ? setTimeout(() => ac.abort(), options.timeoutMs ?? 10000) : null;\n try {\n let res: Response;\n try {\n res = await f(this.url(path, options.params), {\n method,\n headers,\n body: options.body === undefined ? undefined : form ? (options.body as FormData) : JSON.stringify(options.body),\n keepalive: options.keepalive,\n signal: ac?.signal,\n credentials: \"omit\",\n mode: \"cors\",\n });\n } catch (err) {\n this.observe(null);\n throw err;\n }\n this.observe(res);\n let data: T | null = null;\n if (res.status !== 304 && res.status !== 204) {\n try {\n data = (await res.json()) as T;\n } catch {\n data = null;\n }\n }\n return { status: res.status, ok: res.ok, data, etag: res.headers?.get?.(\"etag\") ?? null };\n } finally {\n if (timer) clearTimeout(timer);\n }\n }\n\n get<T>(path: string, options?: { etag?: string | null; params?: Record<string, string | undefined>; headers?: Record<string, string> }): Promise<ApiResponse<T>> {\n return this.request<T>(\"GET\", path, options);\n }\n\n post<T>(path: string, body: unknown, options?: { params?: Record<string, string | undefined>; keepalive?: boolean; headers?: Record<string, string> }): Promise<ApiResponse<T>> {\n return this.request<T>(\"POST\", path, { body, ...options });\n }\n\n /** Multipart POST (file uploads). No Content-Type header: the browser adds it with the boundary. */\n postForm<T>(path: string, form: FormData, options?: { params?: Record<string, string | undefined>; timeoutMs?: number }): Promise<ApiResponse<T>> {\n return this.request<T>(\"POST\", path, { body: form, ...options });\n }\n\n /**\n * POST that may answer with server-sent events. Each event is passed to `onEvent`; a JSON answer is\n * returned instead. Scout replies can take a while, so there is no overall timeout: the response\n * must start within 30 s and then never go quiet for more than 60 s (the server sends keepalives),\n * otherwise the request is aborted and rejects.\n */\n async stream<T>(path: string, body: unknown, onEvent: (event: string, data: unknown) => void, headers: Record<string, string> = {}): Promise<ApiResponse<T>> {\n const f = this.fetcher;\n if (!f) throw new Error(\"fetch is not available\");\n const ac = typeof AbortController !== \"undefined\" ? new AbortController() : null;\n let timer: ReturnType<typeof setTimeout> | null = null;\n const arm = (ms: number) => {\n if (!ac) return;\n if (timer) clearTimeout(timer);\n timer = setTimeout(() => ac.abort(), ms);\n };\n arm(STREAM_RESPONSE_TIMEOUT_MS);\n try {\n let res: Response;\n try {\n res = await f(this.url(path), {\n method: \"POST\",\n headers: { ...headers, \"X-Guidely-Key\": this.key, Accept: \"text/event-stream, application/json\", \"Content-Type\": \"application/json\" },\n body: JSON.stringify(body),\n signal: ac?.signal,\n credentials: \"omit\",\n mode: \"cors\",\n });\n } catch (err) {\n this.observe(null);\n throw err;\n }\n this.observe(res);\n const type = res.headers?.get?.(\"content-type\") ?? \"\";\n if (!type.includes(\"text/event-stream\") || !res.body) {\n let data: T | null = null;\n try {\n data = (await res.json()) as T;\n } catch {\n data = null;\n }\n return { status: res.status, ok: res.ok, data, etag: null };\n }\n arm(STREAM_IDLE_TIMEOUT_MS);\n await readEventStream(res.body, onEvent, () => arm(STREAM_IDLE_TIMEOUT_MS));\n return { status: res.status, ok: res.ok, data: null, etag: null };\n } finally {\n if (timer) clearTimeout(timer);\n }\n }\n\n /** `navigator.sendBeacon` when available (page unload); returns false when it could not be queued. */\n beacon(path: string, body: unknown): boolean {\n try {\n if (typeof navigator === \"undefined\" || typeof navigator.sendBeacon !== \"function\") return false;\n // text/plain is a CORS-safelisted type: cross-origin beacons need no preflight (which\n // browsers drop during unload). The API parses the body regardless of content type.\n const blob = new Blob([JSON.stringify(body)], { type: \"text/plain;charset=UTF-8\" });\n return navigator.sendBeacon(this.url(path), blob);\n } catch {\n return false;\n }\n }\n}\n\nexport const sleep = (ms: number) => new Promise<void>((r) => setTimeout(r, ms));\n\n/** Exponential backoff helper: 1s → 2s → 4s … capped. */\nexport function backoffMs(attempt: number, baseMs = 1000, maxMs = 30000): number {\n return Math.min(maxMs, baseMs * 2 ** Math.max(0, attempt));\n}\n\n/**\n * Sends with bounded retries: network errors, 429 and 5xx are retried with backoff; success and\n * other 4xx answers are returned as they are. Null when every attempt failed.\n */\nexport async function withRetry<T>(send: () => Promise<ApiResponse<T>>, attempts = 3, baseMs = 500): Promise<ApiResponse<T> | null> {\n for (let i = 0; i < attempts; i++) {\n try {\n const res = await send();\n if (res.ok || (res.status >= 400 && res.status < 500 && res.status !== 429)) return res;\n } catch {\n /* network error → retry */\n }\n if (i < attempts - 1) await sleep(backoffMs(i, baseMs, 8000));\n }\n return null;\n}\n\n/** Fire-and-forget POST with bounded retries (used for state sync; never blocks UI). */\nexport function postWithRetry<T>(api: Api, path: string, body: unknown, attempts = 3, baseMs = 500): Promise<ApiResponse<T> | null> {\n return withRetry(() => api.post<T>(path, body), attempts, baseMs);\n}\n\n/** Parses a `text/event-stream` body (`event:` + single-line JSON `data:`; comment lines ignored). `onChunk` runs whenever bytes arrive. */\nexport async function readEventStream(body: ReadableStream<Uint8Array>, onEvent: (event: string, data: unknown) => void, onChunk?: () => void): Promise<void> {\n const reader = body.getReader();\n const decoder = new TextDecoder();\n let buffer = \"\";\n const dispatch = (raw: string) => {\n let event = \"message\";\n const data: string[] = [];\n for (const line of raw.split(\"\\n\")) {\n if (!line || line.startsWith(\":\")) continue;\n const i = line.indexOf(\":\");\n const field = i < 0 ? line : line.slice(0, i);\n const value = i < 0 ? \"\" : line.slice(i + 1).replace(/^ /, \"\");\n if (field === \"event\") event = value;\n else if (field === \"data\") data.push(value);\n }\n if (!data.length) return;\n try {\n onEvent(event, JSON.parse(data.join(\"\\n\")));\n } catch {\n /* ignore malformed events */\n }\n };\n for (;;) {\n const { done, value } = await reader.read();\n if (value) onChunk?.();\n if (value) buffer += decoder.decode(value, { stream: true }).replace(/\\r\\n?/g, \"\\n\");\n let sep: number;\n while ((sep = buffer.indexOf(\"\\n\\n\")) >= 0) {\n dispatch(buffer.slice(0, sep));\n buffer = buffer.slice(sep + 2);\n }\n if (done) break;\n }\n if (buffer.trim()) dispatch(buffer);\n}\n","export type StorageKind = \"local\" | \"session\" | \"memory\";\n\n/** Minimal synchronous key/value store; never throws (private mode, quota, SSR). */\nexport interface KeyValueStore {\n get(key: string): string | null;\n set(key: string, value: string): void;\n remove(key: string): void;\n}\n\nexport function memoryStore(): KeyValueStore {\n const map = new Map<string, string>();\n return { get: (k) => map.get(k) ?? null, set: (k, v) => void map.set(k, v), remove: (k) => void map.delete(k) };\n}\n\nfunction webStore(area: \"localStorage\" | \"sessionStorage\"): KeyValueStore | null {\n try {\n if (typeof window === \"undefined\") return null;\n const s = window[area];\n if (!s) return null;\n const probe = \"__guidely_probe__\";\n s.setItem(probe, \"1\");\n s.removeItem(probe);\n return {\n get: (k) => {\n try {\n return s.getItem(k);\n } catch {\n return null;\n }\n },\n set: (k, v) => {\n try {\n s.setItem(k, v);\n } catch {\n /* quota / private mode */\n }\n },\n remove: (k) => {\n try {\n s.removeItem(k);\n } catch {\n /* ignore */\n }\n },\n };\n } catch {\n return null;\n }\n}\n\n/** Creates a store of the requested kind, degrading to memory when unavailable. */\nexport function createStorage(kind: StorageKind = \"local\"): KeyValueStore {\n if (kind === \"memory\") return memoryStore();\n return webStore(kind === \"local\" ? \"localStorage\" : \"sessionStorage\") ?? memoryStore();\n}\n\nexport function readJson<T>(store: KeyValueStore, key: string, fallback: T): T {\n const raw = store.get(key);\n if (raw == null) return fallback;\n try {\n return JSON.parse(raw) as T;\n } catch {\n return fallback;\n }\n}\n\nexport function writeJson(store: KeyValueStore, key: string, value: unknown): void {\n try {\n store.set(key, JSON.stringify(value));\n } catch {\n /* ignore */\n }\n}\n\n/** Storage keys used by the SDK (documented for integrators). */\nexport const KEYS = {\n anon: \"guidely:anon\",\n seen: \"guidely:seen\",\n config: (key: string) => `guidely:config:${key}`,\n etag: (key: string) => `guidely:etag:${key}`,\n state: (key: string) => `guidely:state:${key}`,\n user: (key: string) => `guidely:user:${key}`,\n queue: (key: string) => `guidely:queue:${key}`,\n /** Survey answers and guide/checklist progress not yet accepted by the server. */\n outbox: (key: string) => `guidely:outbox:${key}`,\n session: \"guidely:session\",\n active: \"guidely:active\",\n authoring: \"guidely:authoring\",\n /** How-to capture session marker ({ token, … }) and its not-yet-uploaded steps. */\n capture: \"guidely:capture\",\n capturePending: \"guidely:capture:pending\",\n /** Messenger conversations this browser can read: [{ id, token }]. */\n conversations: (key: string) => `guidely:conversations:${key}`,\n /** Announcement ids opened in the messenger's News tab. */\n newsSeen: (key: string) => `guidely:news:${key}`,\n} as const;\n"],"mappings":"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","names":["import_runtime"]}
|
|
1
|
+
{"version":3,"sources":["../src/recorder.ts","../src/network.ts","../src/replay.ts","../src/http.ts","../src/storage.ts"],"sourcesContent":["/**\n * Session-replay chunk: the only module that imports rrweb. Built as its own entry and loaded on\n * demand (`import(\"./recorder\")`) together with the replay controller, so the base SDK pays for\n * neither. The script-tag build ships it as `guidely-recorder.js` (assigns `window.__guidelyRecorder`).\n */\nimport { record } from \"@rrweb/record\";\nimport { getRecordConsolePlugin } from \"@rrweb/rrweb-plugin-console-record\";\nimport type { RecordPlugin } from \"@rrweb/types\";\nimport type { RecorderApi, RecorderHandle, RecorderStartOptions } from \"./recorder-types\";\nimport { networkRecordPlugin } from \"./network\";\nimport { ReplayController } from \"./replay\";\n\nexport type { RecorderApi, RecorderHandle, RecorderStartOptions } from \"./recorder-types\";\nexport { ReplayController } from \"./replay\";\nexport type { ReplayControllerOptions, ReplayRecordingConfig, ReplayChunkMeta } from \"./replay\";\nexport type { NetworkCaptureOptions } from \"./network\";\n\n/** Starts an rrweb recording with Guidely's privacy defaults (inputs always masked, no canvas, no fonts). */\nexport function startRecorder(options: RecorderStartOptions): RecorderHandle | null {\n const onError = (err: unknown) => {\n try {\n options.onError?.(err);\n } catch {\n /* ignore */\n }\n return true; // handled: never rethrow into the host page\n };\n try {\n const network = options.network ? networkRecordPlugin(options.network) : null;\n const plugins: RecordPlugin[] = [];\n if (options.recordConsole) plugins.push(getRecordConsolePlugin({ level: [\"error\", \"warn\", \"info\", \"log\"], lengthThreshold: 200 }));\n if (network) plugins.push(network.plugin as unknown as RecordPlugin);\n const stop = record({\n emit: (event) => options.emit(event as { type: number; timestamp: number }),\n maskAllInputs: true,\n maskTextSelector: options.maskTextSelector || undefined,\n blockSelector: options.blockSelector || undefined,\n slimDOMOptions: { script: true, comment: true, headFavicon: true, headWhitespace: true, headMetaSocial: true, headMetaRobots: true, headMetaHttpEquiv: true, headMetaVerification: true, headMetaAuthorship: true, headMetaDescKeywords: true },\n sampling: { mousemove: 50, scroll: 150, input: \"last\" },\n inlineStylesheet: true,\n recordCanvas: false,\n collectFonts: false,\n inlineImages: false,\n plugins,\n errorHandler: onError,\n });\n if (!stop) return null;\n return {\n stop: () => {\n try {\n stop();\n } catch (err) {\n onError(err);\n }\n },\n addCustomEvent: (tag, payload) => {\n try {\n record.addCustomEvent(tag, payload);\n } catch {\n /* not recording */\n }\n },\n flush: () => network?.flush(),\n };\n } catch (err) {\n onError(err);\n return null;\n }\n}\n\n/** The recorder chunk's API (the script-tag build substitutes a loader that resolves to it). */\nexport const recorder: RecorderApi = { startRecorder, createReplay: (options) => new ReplayController({ ...options, startRecorder }) };\n","/**\n * Network recording for session replays (part of the lazily loaded recorder chunk, never the base\n * SDK): patches `fetch`, `XMLHttpRequest` and `WebSocket` while recording, observes resource timing,\n * and emits batched `NetworkPluginPayload`s as rrweb plugin events (`NETWORK_PLUGIN`).\n * The page's own requests behave exactly as before: the original promise / XHR / socket is returned\n * untouched, bodies are read from clones, and every recording step is wrapped in try/catch.\n */\nimport { NETWORK_PLUGIN, SENSITIVE_HEADER_PATTERN, SENSITIVE_PARAM_PATTERN, WS_MAX_MESSAGE_BYTES, WS_MAX_MESSAGES_PER_SOCKET, randomId, type NetworkPluginPayload, type NetworkRequest, type NetworkRequestType, type WebSocketMessage } from \"@guidelytech/shared/runtime\";\n\n/** Network recording settings (merged from the project setting and SDK options by the client). */\nexport interface NetworkCaptureOptions {\n /** Record headers and text bodies (`recordNetworkPayloads`). */\n payloads: boolean;\n /** Guidely API base URL: requests to `<origin>/api/v1/sdk/` are never recorded. */\n apiUrl?: string;\n /** Last say over each request: return a modified copy, or null to drop it (exceptions drop it too). */\n mask?: (request: NetworkRequest) => NetworkRequest | null;\n}\n\n/** Bodies above this many characters are cut and marked `[truncated]`. */\nexport const MAX_BODY_CHARS = 10 * 1024;\nexport const MAX_REQUESTS_PER_PAGE = 500;\nconst BATCH_SIZE = 20;\nconst BATCH_MS = 1000;\nconst BODY_TIMEOUT_MS = 2000;\nconst REDACTED = \"[redacted]\";\n\n// Per page load (the chunk is loaded once per page).\nlet requestCount = 0;\n/** Resource-timing entries that started before this time were already reported by an earlier recording. */\nlet perfCutoff = 0;\n\nconst now = () => Date.now();\n\n/** Absolute URL with sensitive query parameter values replaced by `[redacted]`. */\nexport function redactUrl(raw: string, base = typeof location !== \"undefined\" ? location.href : \"http://localhost/\"): string {\n try {\n const u = new URL(raw, base);\n u.searchParams.forEach((_v, k) => {\n if (SENSITIVE_PARAM_PATTERN.test(k)) u.searchParams.set(k, REDACTED);\n });\n return u.toString().replace(/%5Bredacted%5D/g, REDACTED);\n } catch {\n return String(raw);\n }\n}\n\n/** Recursively masks values of sensitive-looking keys. */\nexport function maskJson(value: unknown, depth = 0): unknown {\n if (depth > 20 || !value || typeof value !== \"object\") return value;\n if (Array.isArray(value)) return value.map((v) => maskJson(v, depth + 1));\n const out: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(value)) out[k] = SENSITIVE_PARAM_PATTERN.test(k) ? REDACTED : maskJson(v, depth + 1);\n return out;\n}\n\nfunction cap(text: string, max: number): string {\n return text.length > max ? `${text.slice(0, max)}[truncated]` : text;\n}\n\n/** Body as recorded: text/JSON/form only (JSON keys masked), ≤10KB; `[binary]` otherwise. */\nexport function formatBody(body: unknown, contentType: string | null | undefined): string | undefined {\n if (body == null || body === \"\") return undefined;\n const ct = (contentType || \"\").toLowerCase();\n if (typeof URLSearchParams !== \"undefined\" && body instanceof URLSearchParams) return formatBody(body.toString(), \"application/x-www-form-urlencoded\");\n if (typeof body !== \"string\") return \"[binary]\";\n if (ct && !/^text\\/|json|x-www-form-urlencoded/.test(ct)) return \"[binary]\";\n if (ct.includes(\"x-www-form-urlencoded\")) {\n const params = new URLSearchParams(body);\n params.forEach((_v, k) => {\n if (SENSITIVE_PARAM_PATTERN.test(k)) params.set(k, REDACTED);\n });\n return cap(params.toString().replace(/%5Bredacted%5D/g, REDACTED), MAX_BODY_CHARS);\n }\n if (ct.includes(\"json\") || (!ct && /^\\s*[[{]/.test(body))) {\n try {\n return cap(JSON.stringify(maskJson(JSON.parse(body))), MAX_BODY_CHARS);\n } catch {\n /* not JSON after all */\n }\n }\n return cap(body, MAX_BODY_CHARS);\n}\n\n/** Lower-cased header names; sensitive values `[redacted]`. */\nexport function formatHeaders(entries: Iterable<[string, string]>): Record<string, string> {\n const out: Record<string, string> = {};\n for (const [k, v] of entries) {\n const name = k.toLowerCase();\n out[name] = SENSITIVE_HEADER_PATTERN.test(name) ? REDACTED : String(v);\n }\n return out;\n}\n\nfunction headerEntries(h: unknown): Array<[string, string]> {\n if (!h) return [];\n if (Array.isArray(h)) return h.map(([k, v]) => [String(k), String(v)]);\n if (typeof (h as Headers).forEach === \"function\") {\n const out: Array<[string, string]> = [];\n (h as Headers).forEach((v, k) => out.push([k, v]));\n return out;\n }\n return Object.entries(h as Record<string, string>).map(([k, v]) => [k, String(v)]);\n}\n\n/** UTF-8 byte length of a string, or the size of binary data. */\nexport function byteSize(data: unknown): number {\n if (typeof data === \"string\") return typeof TextEncoder !== \"undefined\" ? new TextEncoder().encode(data).length : data.length;\n if (data instanceof ArrayBuffer) return data.byteLength;\n if (ArrayBuffer.isView(data)) return data.byteLength;\n if (typeof Blob !== \"undefined\" && data instanceof Blob) return data.size;\n return 0;\n}\n\nfunction frameData(data: unknown, size: number): string {\n if (typeof data !== \"string\") return `[binary ${size} B]`;\n let text = data;\n try {\n if (/^\\s*[[{]/.test(text)) text = JSON.stringify(maskJson(JSON.parse(text)));\n } catch {\n /* plain text */\n }\n return cap(text, WS_MAX_MESSAGE_BYTES);\n}\n\nconst RESOURCE_TYPES: Record<string, NetworkRequestType> = { script: \"script\", link: \"stylesheet\", css: \"stylesheet\", img: \"image\", image: \"image\", audio: \"media\", video: \"media\", track: \"media\", beacon: \"beacon\", navigation: \"document\", iframe: \"document\", frame: \"document\" };\n\n/** A running capture: `flush()` emits pending items now; `stop()` flushes and restores everything. */\nexport interface NetworkCapture {\n flush(): void;\n stop(): void;\n}\n\ntype Win = Window & typeof globalThis;\n\n/** Starts recording the window's network activity; `emit` receives batched payloads. */\nexport function startNetworkCapture(win: Win, opts: NetworkCaptureOptions, emit: (payload: NetworkPluginPayload) => void): NetworkCapture {\n let active = true;\n const startedAt = now();\n let requests: NetworkRequest[] = [];\n let messages: WebSocketMessage[] = [];\n let droppedRequests = 0;\n let droppedMessages = 0;\n let timer: ReturnType<typeof setTimeout> | null = null;\n /** Recent fetch/XHR requests, for enriching with resource timing. */\n const recent: NetworkRequest[] = [];\n const cleanups: Array<() => void> = [];\n let apiPrefix = \"\";\n try {\n if (opts.apiUrl) apiPrefix = `${new URL(opts.apiUrl).origin}/api/v1/sdk/`;\n } catch {\n /* invalid apiUrl */\n }\n\n const excluded = (url: string) => /^(data|blob|about|chrome-extension):/i.test(url) || (!!apiPrefix && url.replace(/^ws(s?):/i, \"http$1:\").startsWith(apiPrefix)) || /\\/guidely-(recorder|capture)(\\.min)?\\.js(\\?|$)/.test(url);\n\n const flush = () => {\n if (timer) clearTimeout(timer);\n timer = null;\n if (!requests.length && !messages.length && !droppedRequests && !droppedMessages) return;\n const payload: NetworkPluginPayload = {};\n if (requests.length) payload.requests = requests;\n if (messages.length) payload.messages = messages;\n if (droppedRequests || droppedMessages) payload.dropped = { ...(droppedRequests ? { requests: droppedRequests } : {}), ...(droppedMessages ? { messages: droppedMessages } : {}) };\n requests = [];\n messages = [];\n droppedRequests = 0;\n droppedMessages = 0;\n try {\n emit(payload);\n } catch {\n /* recorder stopped */\n }\n };\n const schedule = () => {\n if (requests.length + messages.length >= BATCH_SIZE) flush();\n else timer ??= setTimeout(flush, BATCH_MS);\n };\n\n /** Applies the mask hook; null drops. `counted` requests (re-emissions) skip the per-page cap. */\n const push = (req: NetworkRequest, counted = false): boolean => {\n if (!active) return false;\n if (!counted) {\n if (requestCount >= MAX_REQUESTS_PER_PAGE) {\n droppedRequests += 1;\n schedule();\n return false;\n }\n requestCount += 1;\n }\n // Pushed as-is (resource timing may still enrich it before the batch is sent); the hook gets a copy.\n let out: NetworkRequest | null = req;\n if (opts.mask) {\n try {\n out = opts.mask({ ...req });\n } catch {\n out = null;\n }\n }\n if (!out) return false;\n requests.push(out);\n schedule();\n return true;\n };\n const finish = (req: NetworkRequest, fields: Partial<NetworkRequest>) => {\n Object.assign(req, fields);\n req.endTime = now();\n req.durationMs = req.endTime - req.startTime;\n };\n const remember = (req: NetworkRequest) => {\n recent.push(req);\n if (recent.length > 100) recent.shift();\n };\n\n // --- fetch -------------------------------------------------------------------------------------\n const origFetch = win.fetch;\n if (typeof origFetch === \"function\") {\n const patched = function (this: unknown, input: RequestInfo | URL, init?: RequestInit): Promise<Response> {\n const p = origFetch.call(this === undefined || this === patched ? win : (this as Win), input, init);\n if (!active) return p;\n try {\n const isReq = typeof Request !== \"undefined\" && input instanceof Request;\n const rawUrl = isReq ? (input as Request).url : String(input instanceof URL ? input.href : input);\n const url = redactUrl(rawUrl);\n if (excluded(url) || excluded(rawUrl)) return p;\n const req: NetworkRequest = { id: randomId(12), type: \"fetch\", method: String(init?.method || (isReq ? (input as Request).method : \"GET\")).toUpperCase(), url, startTime: now() };\n if (opts.payloads) {\n const headers = headerEntries(init?.headers ?? (isReq ? (input as Request).headers : undefined));\n req.requestHeaders = formatHeaders(headers);\n const body = formatBody(init?.body, req.requestHeaders[\"content-type\"] ?? (typeof init?.body === \"string\" ? \"text/plain\" : undefined));\n if (body !== undefined) req.requestBody = body;\n }\n remember(req);\n p.then(\n (res) => {\n try {\n finish(req, { status: res.status });\n if (!opts.payloads) return void push(req);\n req.responseHeaders = formatHeaders(headerEntries(res.headers));\n const ct = res.headers.get(\"content-type\");\n if (!ct || !/^text\\/(?!event-stream)|json|x-www-form-urlencoded/i.test(ct) || !res.body) {\n if (ct && res.body) req.responseBody = \"[binary]\";\n return void push(req);\n }\n // Read a clone (never the app's stream), at most ~10KB and 2s, then send.\n const reader = res.clone().body!.getReader();\n const decoder = new TextDecoder();\n let text = \"\";\n const done = () => {\n req.responseBody = formatBody(text, ct);\n push(req);\n };\n const t = setTimeout(() => void reader.cancel().catch(() => undefined), BODY_TIMEOUT_MS);\n const read = (): Promise<void> =>\n reader.read().then(({ done: end, value }) => {\n if (value) text += decoder.decode(value, { stream: true });\n if (end) return;\n if (text.length > MAX_BODY_CHARS * 4) return void reader.cancel().catch(() => undefined);\n return read();\n });\n read()\n .catch(() => undefined)\n .then(() => {\n clearTimeout(t);\n done();\n });\n } catch {\n /* never affect the app */\n }\n },\n (err: unknown) => {\n try {\n finish(req, { status: 0, error: err instanceof Error && err.name === \"AbortError\" ? \"aborted\" : String((err as Error)?.message || err) });\n push(req);\n } catch {\n /* ignore */\n }\n },\n );\n } catch {\n /* never affect the app */\n }\n return p;\n };\n win.fetch = patched as typeof fetch;\n cleanups.push(() => {\n if (win.fetch === (patched as typeof fetch)) win.fetch = origFetch;\n });\n }\n\n // --- XMLHttpRequest ----------------------------------------------------------------------------\n const XHR = win.XMLHttpRequest;\n if (XHR?.prototype) {\n const proto = XHR.prototype;\n const { open, send, setRequestHeader } = proto;\n const state = new WeakMap<XMLHttpRequest, { method: string; url: string; headers: Array<[string, string]> }>();\n const pOpen = function (this: XMLHttpRequest, method: string, url: string | URL, ...rest: unknown[]) {\n try {\n if (active) state.set(this, { method: String(method).toUpperCase(), url: String(url), headers: [] });\n } catch {\n /* ignore */\n }\n return (open as (...a: unknown[]) => void).call(this, method, url, ...rest);\n };\n const pSetHeader = function (this: XMLHttpRequest, name: string, value: string) {\n try {\n state.get(this)?.headers.push([name, value]);\n } catch {\n /* ignore */\n }\n return setRequestHeader.call(this, name, value);\n };\n const pSend = function (this: XMLHttpRequest, body?: Document | XMLHttpRequestBodyInit | null) {\n try {\n const s = state.get(this);\n const url = s ? redactUrl(s.url) : \"\";\n if (active && s && !excluded(url) && !excluded(s.url)) {\n const req: NetworkRequest = { id: randomId(12), type: \"xhr\", method: s.method, url, startTime: now() };\n if (opts.payloads) {\n req.requestHeaders = formatHeaders(s.headers);\n const b = formatBody(body, req.requestHeaders[\"content-type\"] ?? (typeof body === \"string\" ? \"text/plain\" : undefined));\n if (b !== undefined) req.requestBody = b;\n }\n remember(req);\n let ended = false;\n const end = (error?: string) => () => {\n if (ended) return;\n ended = true;\n try {\n finish(req, { status: error ? 0 : this.status, ...(error ? { error } : {}) });\n if (opts.payloads && !error) {\n const raw = this.getAllResponseHeaders() || \"\";\n req.responseHeaders = formatHeaders(raw.trim().split(/[\\r\\n]+/).filter(Boolean).map((line) => [line.slice(0, line.indexOf(\":\")).trim(), line.slice(line.indexOf(\":\") + 1).trim()] as [string, string]));\n const ct = this.getResponseHeader(\"content-type\");\n const type = this.responseType;\n const text = type === \"\" || type === \"text\" ? this.responseText : type === \"json\" ? JSON.stringify(this.response) : null;\n const formatted = text == null ? (this.response ? \"[binary]\" : undefined) : formatBody(text, ct ?? (type === \"json\" ? \"application/json\" : \"text/plain\"));\n if (formatted !== undefined) req.responseBody = formatted;\n }\n push(req);\n } catch {\n /* ignore */\n }\n };\n this.addEventListener(\"load\", end());\n this.addEventListener(\"error\", end(\"network error\"));\n this.addEventListener(\"abort\", end(\"aborted\"));\n this.addEventListener(\"timeout\", end(\"timeout\"));\n }\n } catch {\n /* never affect the app */\n }\n return send.call(this, body);\n };\n proto.open = pOpen as typeof open;\n proto.setRequestHeader = pSetHeader;\n proto.send = pSend;\n cleanups.push(() => {\n if (proto.open === (pOpen as typeof open)) proto.open = open;\n if (proto.setRequestHeader === pSetHeader) proto.setRequestHeader = setRequestHeader;\n if (proto.send === pSend) proto.send = send;\n });\n }\n\n // --- WebSocket ---------------------------------------------------------------------------------\n const WS = win.WebSocket;\n if (typeof WS === \"function\") {\n const track = (ws: WebSocket, rawUrl: string) => {\n const url = redactUrl(rawUrl);\n if (excluded(url)) return;\n const req: NetworkRequest = { id: randomId(12), type: \"websocket\", method: \"GET\", url, startTime: now() };\n const totals = { sent: 0, received: 0, sentBytes: 0, receivedBytes: 0, recorded: 0 };\n let dropped = false;\n let counted = false;\n const emitConn = (fields: Partial<NetworkRequest>) => {\n if (dropped || !active) return;\n Object.assign(req, fields, { messages: { ...totals } });\n dropped = !push({ ...req }, counted);\n counted = true;\n };\n const frame = (dir: \"send\" | \"recv\", data: unknown) => {\n try {\n const size = byteSize(data);\n if (dir === \"send\") {\n totals.sent += 1;\n totals.sentBytes += size;\n } else {\n totals.received += 1;\n totals.receivedBytes += size;\n }\n if (dropped || !active) return;\n if (totals.recorded >= WS_MAX_MESSAGES_PER_SOCKET) {\n droppedMessages += 1;\n return schedule();\n }\n totals.recorded += 1;\n const m: WebSocketMessage = { socketId: req.id, dir, time: now(), size };\n if (typeof data !== \"string\") m.binary = true;\n if (opts.payloads) m.data = frameData(data, size);\n messages.push(m);\n schedule();\n } catch {\n /* ignore */\n }\n };\n const nativeSend = ws.send;\n // Instance-level wrapper: the socket keeps working after recording stops (it just stops being recorded).\n ws.send = function (this: WebSocket, data: string | ArrayBufferLike | Blob | ArrayBufferView) {\n const r = nativeSend.call(this, data); // throws (unrecorded) when the socket is not open\n frame(\"send\", data);\n return r;\n };\n ws.addEventListener(\"open\", () => emitConn({ protocol: ws.protocol || undefined }));\n ws.addEventListener(\"message\", (e: MessageEvent) => frame(\"recv\", e.data));\n let errored = false;\n ws.addEventListener(\"error\", () => {\n errored = true;\n });\n ws.addEventListener(\"close\", (e: CloseEvent) => {\n const endTime = now();\n emitConn({ endTime, durationMs: endTime - req.startTime, closeCode: e.code, closeReason: e.reason || undefined, ...(errored || e.code === 1006 ? { error: \"connection error\" } : {}) });\n });\n };\n const Patched = class extends WS {\n constructor(url: string | URL, protocols?: string | string[]) {\n super(url, protocols);\n try {\n if (active) track(this, String(url instanceof URL ? url.href : url));\n } catch {\n /* never affect the app */\n }\n }\n static override [Symbol.hasInstance](x: unknown): boolean {\n return x instanceof WS;\n }\n };\n Object.defineProperty(Patched, \"name\", { value: WS.name });\n win.WebSocket = Patched as typeof WebSocket;\n cleanups.push(() => {\n if (win.WebSocket === (Patched as typeof WebSocket)) win.WebSocket = WS;\n });\n }\n\n // --- Resource timing -------------------------------------------------------------------------\n const PO = win.PerformanceObserver;\n if (typeof PO === \"function\") {\n const origin = win.performance?.timeOrigin ?? startedAt - (win.performance?.now?.() ?? 0);\n const cutoff = perfCutoff;\n const seen = new Set<string>(); // browsers re-deliver the navigation entry once the load completes\n const onEntries = (list: PerformanceObserverEntryList) => {\n for (const e of list.getEntries() as PerformanceResourceTiming[]) {\n try {\n const start = Math.round(origin + e.startTime);\n const key = `${e.entryType}|${e.name}|${e.startTime}`;\n if (start < cutoff || seen.has(key)) continue;\n seen.add(key);\n const url = redactUrl(e.name);\n if (excluded(url) || excluded(e.name)) continue;\n const status = (e as { responseStatus?: number }).responseStatus;\n const timing = { transferSize: e.transferSize, responseSize: e.decodedBodySize, ...(status ? { status } : {}) };\n if (e.initiatorType === \"fetch\" || e.initiatorType === \"xmlhttprequest\") {\n const match = recent.find((r) => r.url === url && Math.abs(r.startTime - start) <= 50);\n if (match) Object.assign(match, { transferSize: timing.transferSize, responseSize: timing.responseSize }, match.status === undefined && status ? { status } : {});\n continue;\n }\n const type = e.entryType === \"navigation\" ? \"document\" : /\\.(woff2?|ttf|otf|eot)(\\?|$)/i.test(e.name) ? \"font\" : (RESOURCE_TYPES[e.initiatorType] ?? \"other\");\n push({ id: randomId(12), type, method: \"GET\", url, startTime: start, endTime: Math.round(origin + e.responseEnd), durationMs: Math.round(e.duration), ...timing });\n } catch {\n /* ignore */\n }\n }\n };\n for (const type of [\"resource\", \"navigation\"]) {\n try {\n const po = new PO(onEntries);\n po.observe({ type, buffered: true });\n cleanups.push(() => po.disconnect());\n } catch {\n /* unsupported entry type */\n }\n }\n }\n\n return {\n flush,\n stop: () => {\n if (!active) return;\n flush();\n active = false;\n perfCutoff = now();\n cleanups.splice(0).forEach((fn) => {\n try {\n fn();\n } catch {\n /* ignore */\n }\n });\n },\n };\n}\n\n/** The rrweb plugin that records network activity as `{ type: 6, data: { plugin: NETWORK_PLUGIN, payload } }`. */\nexport function networkRecordPlugin(opts: NetworkCaptureOptions): { plugin: { name: string; observer: (cb: (payload: unknown) => void, win: Win) => () => void; options: unknown }; flush: () => void } {\n let capture: NetworkCapture | null = null;\n return {\n plugin: {\n name: NETWORK_PLUGIN,\n options: {},\n observer: (cb, win) => {\n // rrweb also calls plugin observers for same-origin iframes: only the top window is patched.\n if (capture || win !== (typeof window !== \"undefined\" ? window : win)) return () => {};\n const c = startNetworkCapture(win, opts, cb);\n capture = c;\n return () => {\n c.stop();\n if (capture === c) capture = null;\n };\n },\n },\n flush: () => capture?.flush(),\n };\n}\n\n/** Test hook: resets the per-page counters. */\nexport function resetNetworkCounters(): void {\n requestCount = 0;\n perfCutoff = 0;\n}\n","import { randomId } from \"@guidelytech/shared/runtime\";\nimport type { ServerHealth } from \"./health\";\nimport { backoffMs } from \"./http\";\nimport type { NetworkCaptureOptions } from \"./network\";\nimport type { RecorderHandle, RecorderStartOptions } from \"./recorder-types\";\nimport { readJson, writeJson, type KeyValueStore } from \"./storage\";\n\n/** Recorder settings for one session (already merged from the project setting and SDK options). */\nexport interface ReplayRecordingConfig {\n maskTextSelector: string;\n blockSelector: string;\n recordConsole: boolean;\n network?: NetworkCaptureOptions | null;\n}\n\n/** Per-upload metadata (`ReplayChunk` minus `sessionId`/`windowId`/`seq`/`events`). */\nexport interface ReplayChunkMeta {\n anonymousId: string;\n userId?: string | null;\n url?: string;\n device?: { type: string; browser?: string; os?: string };\n viewport?: { width: number; height: number };\n sdk?: { name: string; version: string };\n}\n\nexport interface ReplayControllerOptions {\n /** Upload URL for `POST /api/v1/sdk/replay` (with `?key=` and the given params). */\n url: (params: Record<string, string | undefined>) => string;\n fetch: () => typeof fetch | null;\n meta: () => ReplayChunkMeta;\n /** Starts rrweb (`startRecorder` from the recorder chunk; injectable for tests). */\n startRecorder: (options: RecorderStartOptions) => RecorderHandle | null;\n /** The server answered `{ disabled: true }`: recording must stop for this session. */\n onDisabled: (sessionId: string) => void;\n onError: (err: unknown) => void;\n flushIntervalMs?: number;\n /** Buffered bytes that trigger an early upload (default ~1MB). */\n maxBufferBytes?: number;\n /** Upload backlog cap; the oldest chunks are dropped above it (default ~5MB). */\n maxQueuedBytes?: number;\n /** API reachability: uploads wait while its circuit is open. */\n health?: ServerHealth;\n /**\n * Per-tab storage (sessionStorage) for what could not be uploaded when the page is hidden while\n * the API is unreachable; the next page in the tab uploads it. Up to {@link STASH_MAX_BYTES}.\n */\n stash?: KeyValueStore;\n}\n\ninterface Chunk {\n sessionId: string;\n /** A stashed upload body from an earlier page of this tab (its window id and seq are inside). */\n raw?: string;\n meta: ReplayChunkMeta;\n events: string[];\n bytes: number;\n /** Assigned on the first upload attempt and kept for retries (the server dedupes by window + seq). */\n seq?: number;\n}\n\n/** Largest body sent with `sendBeacon` / keepalive fetch while the page is being hidden. */\nexport const UNLOAD_MAX_BYTES = 60 * 1024;\n/** Uploads still running after this long are aborted and retried. */\nexport const UPLOAD_TIMEOUT_MS = 30_000;\n/** Most recording kept in the tab's sessionStorage for the next page while the API is unreachable. */\nexport const STASH_MAX_BYTES = 1_000_000;\nconst STASH_KEY = \"guidely:replay:stash\";\n\ninterface StashedChunk {\n sessionId: string;\n body: string;\n}\n\nasync function gzip(text: string): Promise<Blob> {\n const stream = new Blob([text]).stream().pipeThrough(new CompressionStream(\"gzip\"));\n return new Response(stream).blob();\n}\n\n/**\n * Session-replay controller (part of the lazily loaded recorder chunk): records with rrweb, buffers events, and uploads them\n * every 5s (or once ~1MB is buffered) as gzip JSON (`?enc=gzip`; plain JSON without\n * `CompressionStream`). On page hide it sends what is left with `sendBeacon` / keepalive fetch.\n */\nexport class ReplayController {\n readonly windowId = randomId(16);\n private handle: RecorderHandle | null = null;\n private sessionId: string | null = null;\n private configKey = \"\";\n private buffer: Chunk | null = null;\n private queue: Chunk[] = [];\n private queuedBytes = 0;\n private seqs = new Map<string, number>();\n private interval: ReturnType<typeof setInterval> | null = null;\n private retryTimer: ReturnType<typeof setTimeout> | null = null;\n private attempt = 0;\n private uploading = false;\n private flushing: Promise<void> | null = null;\n private destroyed = false;\n\n constructor(private readonly opts: ReplayControllerOptions) {\n // Recording an earlier page of this tab could not upload (the API was unreachable): send it first.\n if (opts.stash) {\n const stashed = readJson<StashedChunk[]>(opts.stash, STASH_KEY, []);\n opts.stash.remove(STASH_KEY);\n for (const c of Array.isArray(stashed) ? stashed : []) {\n if (!c || typeof c.body !== \"string\" || typeof c.sessionId !== \"string\") continue;\n this.queue.push({ sessionId: c.sessionId, raw: c.body, meta: opts.meta(), events: [], bytes: c.body.length });\n this.queuedBytes += c.body.length;\n }\n if (this.queue.length) void this.flush();\n }\n }\n\n /** True while rrweb is recording. */\n isRecording(): boolean {\n return !!this.handle;\n }\n\n /** The session currently being recorded, if any. */\n get activeSessionId(): string | null {\n return this.sessionId;\n }\n\n /** Starts (or keeps) recording for `sessionId`. Restarts when the session or settings changed. */\n start(sessionId: string, config: ReplayRecordingConfig): void {\n if (this.destroyed) return;\n const key = JSON.stringify(config);\n if (this.sessionId === sessionId && this.configKey === key && this.handle) return;\n this.stop();\n this.handle = this.opts.startRecorder({ ...config, emit: (e) => this.push(sessionId, e), onError: this.opts.onError });\n if (!this.handle) return;\n this.sessionId = sessionId;\n this.configKey = key;\n this.interval = setInterval(() => void this.flush(), this.opts.flushIntervalMs ?? 5000);\n }\n\n /** Stops recording. Buffered events are uploaded unless `discard` (consent withdrawn, authoring). */\n stop(options: { discard?: boolean } = {}): void {\n const handle = this.handle;\n this.handle = null;\n this.sessionId = null;\n this.configKey = \"\";\n if (this.interval) clearInterval(this.interval);\n this.interval = null;\n handle?.stop();\n if (options.discard) {\n this.buffer = null;\n this.queue = [];\n this.queuedBytes = 0;\n } else void this.flush();\n }\n\n /** Adds an rrweb custom event (Guidely event markers) while recording. */\n addCustomEvent(tag: string, payload: unknown): void {\n this.handle?.addCustomEvent(tag, payload);\n }\n\n private push(sessionId: string, event: unknown): void {\n let json: string;\n try {\n json = JSON.stringify(event);\n } catch {\n return;\n }\n if (this.buffer && this.buffer.sessionId !== sessionId) this.seal();\n const chunk = this.buffer ?? (this.buffer = { sessionId, meta: this.opts.meta(), events: [], bytes: 0 });\n chunk.events.push(json);\n chunk.bytes += json.length;\n if (chunk.bytes > (this.opts.maxBufferBytes ?? 1_000_000)) void this.flush();\n }\n\n /** Moves the buffer to the upload queue, dropping the oldest chunks above the backlog cap. */\n private seal(): void {\n const chunk = this.buffer;\n this.buffer = null;\n if (!chunk || (!chunk.events.length && !chunk.raw)) return;\n this.queue.push(chunk);\n this.queuedBytes += chunk.bytes;\n const max = this.opts.maxQueuedBytes ?? 5_000_000;\n while (this.queuedBytes > max && this.queue.length > 1) {\n const i = this.uploading ? 1 : 0;\n const [dropped] = this.queue.splice(i, 1);\n if (dropped) this.queuedBytes -= dropped.bytes;\n }\n }\n\n private nextSeq(chunk: Chunk): number {\n if (chunk.seq === undefined) {\n const seq = this.seqs.get(chunk.sessionId) ?? 0;\n this.seqs.set(chunk.sessionId, seq + 1);\n chunk.seq = seq;\n }\n return chunk.seq;\n }\n\n private body(chunk: Chunk): string {\n if (chunk.raw) return chunk.raw;\n const head = JSON.stringify({ sessionId: chunk.sessionId, windowId: this.windowId, seq: this.nextSeq(chunk), ...chunk.meta });\n return `${head.slice(0, -1)},\"events\":[${chunk.events.join(\",\")}]}`;\n }\n\n private remove(chunk: Chunk): void {\n const i = this.queue.indexOf(chunk);\n if (i >= 0) {\n this.queue.splice(i, 1);\n this.queuedBytes -= chunk.bytes;\n }\n }\n\n /** Uploads the buffer and the backlog, one chunk at a time; retries with backoff on failure. */\n flush(): Promise<void> {\n this.seal();\n if (this.retryTimer || !this.queue.length) return Promise.resolve();\n // An upload loop already running may have finished its last chunk: queue another pass after it.\n if (this.flushing) return this.flushing.then(() => this.flush());\n return (this.flushing = this.drain().finally(() => {\n this.flushing = null;\n }));\n }\n\n private retryIn(ms: number): void {\n if (this.destroyed || this.retryTimer) return;\n this.retryTimer = setTimeout(() => {\n this.retryTimer = null;\n void this.flush();\n }, ms);\n }\n\n private async drain(): Promise<void> {\n this.uploading = true;\n try {\n while (this.queue.length) {\n // The API is down: wait for its circuit instead of uploading into the void.\n const wait = this.opts.health?.waitMs() ?? 0;\n if (wait > 0) return this.retryIn(wait);\n const chunk = this.queue[0] as Chunk;\n const result = await this.send(chunk);\n if (result === \"retry\") {\n // After six quick retries the 5s flush interval tries again, paced by the circuit.\n this.attempt += 1;\n if (this.attempt < 6) this.retryIn(Math.max(backoffMs(this.attempt - 1), this.opts.health?.waitMs() ?? 0));\n else this.attempt = 0;\n return;\n }\n this.attempt = 0;\n this.remove(chunk);\n if (result === \"disabled\") {\n for (const c of this.queue.slice()) if (c.sessionId === chunk.sessionId) this.remove(c);\n this.opts.onDisabled(chunk.sessionId);\n }\n }\n } finally {\n this.uploading = false;\n }\n }\n\n private async send(chunk: Chunk): Promise<\"ok\" | \"drop\" | \"retry\" | \"disabled\"> {\n const f = this.opts.fetch();\n if (!f) return \"drop\";\n try {\n const json = this.body(chunk);\n let body: Blob | string = json;\n let enc: string | undefined;\n if (typeof CompressionStream === \"function\") {\n try {\n body = await gzip(json);\n enc = \"gzip\";\n } catch {\n body = json;\n }\n }\n const size = typeof body === \"string\" ? body.length : body.size;\n const ac = typeof AbortController !== \"undefined\" ? new AbortController() : null;\n const timer = ac ? setTimeout(() => ac.abort(), UPLOAD_TIMEOUT_MS) : null;\n let res: Response;\n try {\n res = await f(this.opts.url({ enc }), { method: \"POST\", body, headers: { \"Content-Type\": \"text/plain\" }, keepalive: size < UNLOAD_MAX_BYTES, signal: ac?.signal, credentials: \"omit\", mode: \"cors\" });\n } catch (err) {\n this.opts.health?.failure();\n throw err;\n } finally {\n if (timer) clearTimeout(timer);\n }\n if (res.status === 429 || res.status >= 500) this.opts.health?.failure();\n else this.opts.health?.success();\n if (res.ok) {\n const data = (await res.json().catch(() => null)) as { disabled?: boolean } | null;\n return data?.disabled ? \"disabled\" : \"ok\";\n }\n return res.status >= 400 && res.status < 500 && res.status !== 429 ? \"drop\" : \"retry\";\n } catch {\n return \"retry\";\n }\n }\n\n /**\n * Page is being hidden / unloaded: sends whatever is buffered synchronously as uncompressed JSON\n * (`sendBeacon` up to 60KB, keepalive fetch otherwise), splitting chunks that never went out.\n */\n flushOnHide(): void {\n this.handle?.flush?.();\n this.seal();\n const pending = this.queue.slice(this.uploading ? 1 : 0);\n // The API isn't answering: a beacon would most likely be lost. Keep the recording in the tab\n // for the next page to upload (it may still beacon what doesn't fit).\n if (this.opts.stash && this.opts.health && !this.opts.health.reachable && pending.length) {\n const kept: StashedChunk[] = [];\n let bytes = 0;\n for (const chunk of pending) {\n const body = this.body(chunk);\n if (bytes + body.length > STASH_MAX_BYTES) break;\n bytes += body.length;\n kept.push({ sessionId: chunk.sessionId, body });\n this.remove(chunk);\n }\n if (kept.length) writeJson(this.opts.stash, STASH_KEY, [...readJson<StashedChunk[]>(this.opts.stash, STASH_KEY, []), ...kept]);\n pending.splice(0, kept.length);\n }\n for (const chunk of pending) {\n const parts = chunk.seq === undefined && !chunk.raw ? splitChunk(chunk, UNLOAD_MAX_BYTES - 2048) : [chunk];\n let sent = true;\n for (const part of parts) {\n if (!this.sendNow(this.body(part))) {\n sent = false;\n break;\n }\n if (part !== chunk) {\n chunk.events.splice(0, part.events.length);\n chunk.bytes -= part.bytes;\n this.queuedBytes -= part.bytes;\n }\n }\n if (!sent) break;\n this.remove(chunk);\n }\n }\n\n private sendNow(json: string): boolean {\n const url = this.opts.url({});\n try {\n if (json.length <= UNLOAD_MAX_BYTES && typeof navigator !== \"undefined\" && typeof navigator.sendBeacon === \"function\" && navigator.sendBeacon(url, new Blob([json], { type: \"text/plain;charset=UTF-8\" }))) return true;\n const f = this.opts.fetch();\n if (!f || json.length > 64 * 1024) return false;\n void f(url, { method: \"POST\", body: json, headers: { \"Content-Type\": \"text/plain\" }, keepalive: true, credentials: \"omit\", mode: \"cors\" }).catch(() => undefined);\n return true;\n } catch {\n return false;\n }\n }\n\n /** Stops recording and uploads what is left; no retries afterwards. */\n destroy(): void {\n this.stop();\n this.destroyed = true;\n if (this.retryTimer) clearTimeout(this.retryTimer);\n this.retryTimer = null;\n }\n}\n\n/** Splits a chunk into parts of at most `max` bytes of events (a single larger event stays whole). */\nfunction splitChunk(chunk: Chunk, max: number): Chunk[] {\n if (chunk.bytes <= max) return [chunk];\n const parts: Chunk[] = [];\n let cur: Chunk | null = null;\n for (const e of chunk.events) {\n if (!cur || (cur.bytes + e.length > max && cur.events.length)) parts.push((cur = { sessionId: chunk.sessionId, meta: chunk.meta, events: [], bytes: 0 }));\n cur.events.push(e);\n cur.bytes += e.length;\n }\n return parts;\n}\n","import { isServerFailure, parseRetryAfter, ServerHealth } from \"./health\";\n\nexport interface ApiResponse<T> {\n status: number;\n ok: boolean;\n data: T | null;\n etag: string | null;\n}\n\nexport type FetchLike = typeof fetch;\n\n/** Reply streams: the response must start within this long, and then never go quiet longer than the idle limit (the server sends a keepalive every 15 s). */\nexport const STREAM_RESPONSE_TIMEOUT_MS = 30_000;\nexport const STREAM_IDLE_TIMEOUT_MS = 60_000;\n\n/** Thin HTTP client for the public SDK API. Adds the public key as query + header. */\nexport class Api {\n /** Reachability of the API, fed by every answer (see {@link ServerHealth}). */\n readonly health: ServerHealth;\n\n constructor(\n private readonly baseUrl: string,\n private readonly key: string,\n private readonly fetchImpl?: FetchLike,\n health?: ServerHealth,\n ) {\n this.health = health ?? new ServerHealth();\n }\n\n /** Feeds an answer (or a failed request, `null`) into {@link health}. */\n private observe(res: Response | null): void {\n if (!res) this.health.failure();\n else if (isServerFailure(res.status)) this.health.failure(parseRetryAfter(res.headers?.get?.(\"retry-after\")));\n else this.health.success();\n }\n\n url(path: string, params: Record<string, string | undefined> = {}): string {\n // Built by hand rather than with URL/searchParams, which React Native does not implement.\n const query = Object.entries({ key: this.key, ...params })\n .filter((e): e is [string, string] => e[1] != null)\n .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)\n .join(\"&\");\n return `${this.baseUrl.replace(/\\/$/, \"\")}/api/v1/sdk${path}?${query}`;\n }\n\n private get fetcher(): FetchLike | null {\n if (this.fetchImpl) return this.fetchImpl;\n return typeof fetch === \"function\" ? fetch.bind(globalThis) : null;\n }\n\n async request<T>(method: \"GET\" | \"POST\", path: string, options: { body?: unknown; etag?: string | null; params?: Record<string, string | undefined>; keepalive?: boolean; timeoutMs?: number; headers?: Record<string, string> } = {}): Promise<ApiResponse<T>> {\n const f = this.fetcher;\n if (!f) throw new Error(\"fetch is not available\");\n const headers: Record<string, string> = { ...options.headers, \"X-Guidely-Key\": this.key, Accept: \"application/json\" };\n // Multipart bodies set their own Content-Type (with the boundary).\n const form = typeof FormData !== \"undefined\" && options.body instanceof FormData;\n if (options.body !== undefined && !form) headers[\"Content-Type\"] = \"application/json\";\n if (options.etag) headers[\"If-None-Match\"] = options.etag;\n const ac = typeof AbortController !== \"undefined\" ? new AbortController() : null;\n const timer = ac ? setTimeout(() => ac.abort(), options.timeoutMs ?? 10000) : null;\n try {\n let res: Response;\n try {\n res = await f(this.url(path, options.params), {\n method,\n headers,\n body: options.body === undefined ? undefined : form ? (options.body as FormData) : JSON.stringify(options.body),\n keepalive: options.keepalive,\n signal: ac?.signal,\n credentials: \"omit\",\n mode: \"cors\",\n });\n } catch (err) {\n this.observe(null);\n throw err;\n }\n this.observe(res);\n let data: T | null = null;\n if (res.status !== 304 && res.status !== 204) {\n try {\n data = (await res.json()) as T;\n } catch {\n data = null;\n }\n }\n return { status: res.status, ok: res.ok, data, etag: res.headers?.get?.(\"etag\") ?? null };\n } finally {\n if (timer) clearTimeout(timer);\n }\n }\n\n get<T>(path: string, options?: { etag?: string | null; params?: Record<string, string | undefined>; headers?: Record<string, string> }): Promise<ApiResponse<T>> {\n return this.request<T>(\"GET\", path, options);\n }\n\n post<T>(path: string, body: unknown, options?: { params?: Record<string, string | undefined>; keepalive?: boolean; headers?: Record<string, string> }): Promise<ApiResponse<T>> {\n return this.request<T>(\"POST\", path, { body, ...options });\n }\n\n /** Multipart POST (file uploads). No Content-Type header: the browser adds it with the boundary. */\n postForm<T>(path: string, form: FormData, options?: { params?: Record<string, string | undefined>; timeoutMs?: number }): Promise<ApiResponse<T>> {\n return this.request<T>(\"POST\", path, { body: form, ...options });\n }\n\n /**\n * POST that may answer with server-sent events. Each event is passed to `onEvent`; a JSON answer is\n * returned instead. Scout replies can take a while, so there is no overall timeout: the response\n * must start within 30 s and then never go quiet for more than 60 s (the server sends keepalives),\n * otherwise the request is aborted and rejects.\n */\n async stream<T>(path: string, body: unknown, onEvent: (event: string, data: unknown) => void, headers: Record<string, string> = {}): Promise<ApiResponse<T>> {\n const f = this.fetcher;\n if (!f) throw new Error(\"fetch is not available\");\n const ac = typeof AbortController !== \"undefined\" ? new AbortController() : null;\n let timer: ReturnType<typeof setTimeout> | null = null;\n const arm = (ms: number) => {\n if (!ac) return;\n if (timer) clearTimeout(timer);\n timer = setTimeout(() => ac.abort(), ms);\n };\n arm(STREAM_RESPONSE_TIMEOUT_MS);\n try {\n let res: Response;\n try {\n res = await f(this.url(path), {\n method: \"POST\",\n headers: { ...headers, \"X-Guidely-Key\": this.key, Accept: \"text/event-stream, application/json\", \"Content-Type\": \"application/json\" },\n body: JSON.stringify(body),\n signal: ac?.signal,\n credentials: \"omit\",\n mode: \"cors\",\n });\n } catch (err) {\n this.observe(null);\n throw err;\n }\n this.observe(res);\n const type = res.headers?.get?.(\"content-type\") ?? \"\";\n if (!type.includes(\"text/event-stream\") || !res.body) {\n let data: T | null = null;\n try {\n data = (await res.json()) as T;\n } catch {\n data = null;\n }\n return { status: res.status, ok: res.ok, data, etag: null };\n }\n arm(STREAM_IDLE_TIMEOUT_MS);\n await readEventStream(res.body, onEvent, () => arm(STREAM_IDLE_TIMEOUT_MS));\n return { status: res.status, ok: res.ok, data: null, etag: null };\n } finally {\n if (timer) clearTimeout(timer);\n }\n }\n\n /** `navigator.sendBeacon` when available (page unload); returns false when it could not be queued. */\n beacon(path: string, body: unknown): boolean {\n try {\n if (typeof navigator === \"undefined\" || typeof navigator.sendBeacon !== \"function\") return false;\n // text/plain is a CORS-safelisted type: cross-origin beacons need no preflight (which\n // browsers drop during unload). The API parses the body regardless of content type.\n const blob = new Blob([JSON.stringify(body)], { type: \"text/plain;charset=UTF-8\" });\n return navigator.sendBeacon(this.url(path), blob);\n } catch {\n return false;\n }\n }\n}\n\nexport const sleep = (ms: number) => new Promise<void>((r) => setTimeout(r, ms));\n\n/** Exponential backoff helper: 1s → 2s → 4s … capped. */\nexport function backoffMs(attempt: number, baseMs = 1000, maxMs = 30000): number {\n return Math.min(maxMs, baseMs * 2 ** Math.max(0, attempt));\n}\n\n/**\n * Sends with bounded retries: network errors, 429 and 5xx are retried with backoff; success and\n * other 4xx answers are returned as they are. Null when every attempt failed.\n */\nexport async function withRetry<T>(send: () => Promise<ApiResponse<T>>, attempts = 3, baseMs = 500): Promise<ApiResponse<T> | null> {\n for (let i = 0; i < attempts; i++) {\n try {\n const res = await send();\n if (res.ok || (res.status >= 400 && res.status < 500 && res.status !== 429)) return res;\n } catch {\n /* network error → retry */\n }\n if (i < attempts - 1) await sleep(backoffMs(i, baseMs, 8000));\n }\n return null;\n}\n\n/** Fire-and-forget POST with bounded retries (used for state sync; never blocks UI). */\nexport function postWithRetry<T>(api: Api, path: string, body: unknown, attempts = 3, baseMs = 500): Promise<ApiResponse<T> | null> {\n return withRetry(() => api.post<T>(path, body), attempts, baseMs);\n}\n\n/** Parses a `text/event-stream` body (`event:` + single-line JSON `data:`; comment lines ignored). `onChunk` runs whenever bytes arrive. */\nexport async function readEventStream(body: ReadableStream<Uint8Array>, onEvent: (event: string, data: unknown) => void, onChunk?: () => void): Promise<void> {\n const reader = body.getReader();\n const decoder = new TextDecoder();\n let buffer = \"\";\n const dispatch = (raw: string) => {\n let event = \"message\";\n const data: string[] = [];\n for (const line of raw.split(\"\\n\")) {\n if (!line || line.startsWith(\":\")) continue;\n const i = line.indexOf(\":\");\n const field = i < 0 ? line : line.slice(0, i);\n const value = i < 0 ? \"\" : line.slice(i + 1).replace(/^ /, \"\");\n if (field === \"event\") event = value;\n else if (field === \"data\") data.push(value);\n }\n if (!data.length) return;\n try {\n onEvent(event, JSON.parse(data.join(\"\\n\")));\n } catch {\n /* ignore malformed events */\n }\n };\n for (;;) {\n const { done, value } = await reader.read();\n if (value) onChunk?.();\n if (value) buffer += decoder.decode(value, { stream: true }).replace(/\\r\\n?/g, \"\\n\");\n let sep: number;\n while ((sep = buffer.indexOf(\"\\n\\n\")) >= 0) {\n dispatch(buffer.slice(0, sep));\n buffer = buffer.slice(sep + 2);\n }\n if (done) break;\n }\n if (buffer.trim()) dispatch(buffer);\n}\n","export type StorageKind = \"local\" | \"session\" | \"memory\";\n\n/** Minimal synchronous key/value store; never throws (private mode, quota, SSR). */\nexport interface KeyValueStore {\n get(key: string): string | null;\n set(key: string, value: string): void;\n remove(key: string): void;\n}\n\nexport function memoryStore(): KeyValueStore {\n const map = new Map<string, string>();\n return { get: (k) => map.get(k) ?? null, set: (k, v) => void map.set(k, v), remove: (k) => void map.delete(k) };\n}\n\nfunction webStore(area: \"localStorage\" | \"sessionStorage\"): KeyValueStore | null {\n try {\n if (typeof window === \"undefined\") return null;\n const s = window[area];\n if (!s) return null;\n const probe = \"__guidely_probe__\";\n s.setItem(probe, \"1\");\n s.removeItem(probe);\n return {\n get: (k) => {\n try {\n return s.getItem(k);\n } catch {\n return null;\n }\n },\n set: (k, v) => {\n try {\n s.setItem(k, v);\n } catch {\n /* quota / private mode */\n }\n },\n remove: (k) => {\n try {\n s.removeItem(k);\n } catch {\n /* ignore */\n }\n },\n };\n } catch {\n return null;\n }\n}\n\n/** Creates a store of the requested kind, degrading to memory when unavailable. */\nexport function createStorage(kind: StorageKind = \"local\"): KeyValueStore {\n if (kind === \"memory\") return memoryStore();\n return webStore(kind === \"local\" ? \"localStorage\" : \"sessionStorage\") ?? memoryStore();\n}\n\nexport function readJson<T>(store: KeyValueStore, key: string, fallback: T): T {\n const raw = store.get(key);\n if (raw == null) return fallback;\n try {\n return JSON.parse(raw) as T;\n } catch {\n return fallback;\n }\n}\n\nexport function writeJson(store: KeyValueStore, key: string, value: unknown): void {\n try {\n store.set(key, JSON.stringify(value));\n } catch {\n /* ignore */\n }\n}\n\n/** Storage keys used by the SDK (documented for integrators). */\nexport const KEYS = {\n anon: \"guidely:anon\",\n seen: \"guidely:seen\",\n config: (key: string) => `guidely:config:${key}`,\n etag: (key: string) => `guidely:etag:${key}`,\n state: (key: string) => `guidely:state:${key}`,\n user: (key: string) => `guidely:user:${key}`,\n queue: (key: string) => `guidely:queue:${key}`,\n /** Survey answers and guide/checklist progress not yet accepted by the server. */\n outbox: (key: string) => `guidely:outbox:${key}`,\n /** Agent Analytics turns not yet accepted by the server. */\n agentOutbox: (key: string) => `guidely:agents:${key}`,\n session: \"guidely:session\",\n active: \"guidely:active\",\n authoring: \"guidely:authoring\",\n /** How-to capture session marker ({ token, … }) and its not-yet-uploaded steps. */\n capture: \"guidely:capture\",\n capturePending: \"guidely:capture:pending\",\n /** Messenger conversations this browser can read: [{ id, token }]. */\n conversations: (key: string) => `guidely:conversations:${key}`,\n /** Announcement ids opened in the messenger's News tab. */\n newsSeen: (key: string) => `guidely:news:${key}`,\n} as const;\n"],"mappings":"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","names":["import_runtime"]}
|
package/dist/recorder.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { NetworkRequest } from '@guidelytech/shared/runtime';
|
|
2
|
-
import { S as ServerHealth, K as KeyValueStore } from './health-
|
|
2
|
+
import { S as ServerHealth, K as KeyValueStore } from './health-sbP9JwTE.cjs';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Network recording for session replays (part of the lazily loaded recorder chunk, never the base
|
package/dist/recorder.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { NetworkRequest } from '@guidelytech/shared/runtime';
|
|
2
|
-
import { S as ServerHealth, K as KeyValueStore } from './health-
|
|
2
|
+
import { S as ServerHealth, K as KeyValueStore } from './health-sbP9JwTE.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Network recording for session replays (part of the lazily loaded recorder chunk, never the base
|
package/dist/recorder.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { SdkEvent, PublishedGuide, FrequencyConfig, AutocaptureSettings, NetworkRequest, ProjectTheme, SdkConfigResponse, ChecklistTask, MessengerMessage, MessengerConversation } from '@guidelytech/shared';
|
|
2
|
-
import { K as KeyValueStore, S as ServerHealth, c as StorageKind } from './health-
|
|
1
|
+
import { SdkEvent, PublishedGuide, FrequencyConfig, GuideThrottlingSettings, AutocaptureSettings, NetworkRequest, ProjectTheme, SdkConfigResponse, ChecklistTask, MessengerMessage, MessengerConversation } from '@guidelytech/shared';
|
|
2
|
+
import { K as KeyValueStore, S as ServerHealth, c as StorageKind } from './health-sbP9JwTE.js';
|
|
3
3
|
import { FlagsClientOptions } from '@guidelytech/flags';
|
|
4
|
+
import { AgentTurnInput } from '@guidelytech/shared/runtime';
|
|
4
5
|
|
|
5
6
|
interface EventQueueOptions {
|
|
6
7
|
store: KeyValueStore;
|
|
@@ -86,8 +87,25 @@ declare function isFrequencyEligible(freq: FrequencyConfig, state: GuideLocalSta
|
|
|
86
87
|
declare function isExclusiveGuide(guide: PublishedGuide): boolean;
|
|
87
88
|
/** Is `a` mutually exclusive with `b` (by id or key, in either direction)? */
|
|
88
89
|
declare function areMutuallyExclusive(a: PublishedGuide, b: PublishedGuide): boolean;
|
|
89
|
-
/**
|
|
90
|
-
|
|
90
|
+
/**
|
|
91
|
+
* Scheduling window: the guide's `schedule` (any kind) and, for surveys, `survey.startAt` / `endAt`.
|
|
92
|
+
* Unparsable dates are ignored.
|
|
93
|
+
*/
|
|
94
|
+
declare function isWithinSchedule(guide: Pick<PublishedGuide, "config">, now?: number): boolean;
|
|
95
|
+
/**
|
|
96
|
+
* Does showing this guide automatically count toward (and get limited by) the project's guide
|
|
97
|
+
* throttling? Only exclusive guides (flows, modals, tooltips, announcements, surveys) interrupt;
|
|
98
|
+
* banners, checklists and hotspot-only guides never count, nor does a guide set to ignore throttling.
|
|
99
|
+
* Guides started by the visitor or the app (`startGuide`, buttons, Help) are never throttled.
|
|
100
|
+
*/
|
|
101
|
+
declare function countsTowardThrottle(guide: PublishedGuide): boolean;
|
|
102
|
+
/**
|
|
103
|
+
* Pure throttling check. `launches` = ISO times of this visitor's earlier automatic launches of
|
|
104
|
+
* counted guides (any order). Disabled or missing settings never throttle.
|
|
105
|
+
*/
|
|
106
|
+
declare function isThrottleEligible(settings: GuideThrottlingSettings | null | undefined, launches: readonly string[], now?: number): boolean;
|
|
107
|
+
/** Launch times worth keeping: those still inside the window or the minimum gap (at most 100). */
|
|
108
|
+
declare function pruneLaunches(launches: readonly string[], settings: GuideThrottlingSettings | null | undefined, now?: number): string[];
|
|
91
109
|
/**
|
|
92
110
|
* A checklist's dismissal after merging server state. A local dismissal normally wins (it may not have
|
|
93
111
|
* synced yet), but when the server reports a reset this device hasn't applied, the server's value wins:
|
|
@@ -102,6 +120,15 @@ declare function dismissedAfterMerge(local: {
|
|
|
102
120
|
dismissResetAt?: string | null;
|
|
103
121
|
}): string | null;
|
|
104
122
|
|
|
123
|
+
/**
|
|
124
|
+
* Agent Analytics in the browser: turns of conversations with the app's own AI agent, sent to
|
|
125
|
+
* `POST /api/v1/sdk/agent-events`. Turns are filled in with the visitor's ids and session, passed through
|
|
126
|
+
* the app's `agents.redact` callback, and kept in a persisted outbox until the server accepts them
|
|
127
|
+
* (retried with backoff while it is unreachable; the server ignores turns it already has).
|
|
128
|
+
*/
|
|
129
|
+
/** Last say over each turn before it leaves the browser: return a changed copy, or null to drop it. */
|
|
130
|
+
type AgentTurnRedactor = (turn: AgentTurnInput) => AgentTurnInput | null | undefined;
|
|
131
|
+
|
|
105
132
|
type GuidelyEnvironment = "development" | "staging" | "production";
|
|
106
133
|
type ConsentState = "granted" | "denied" | "pending";
|
|
107
134
|
interface GuidelyAccount {
|
|
@@ -141,6 +168,17 @@ interface GuidelyOptions {
|
|
|
141
168
|
autocapture?: boolean | Partial<AutocaptureSettings>;
|
|
142
169
|
/** App version or commit SHA (≤100 chars), sent with every `exception` to pick the matching uploaded source maps. */
|
|
143
170
|
release?: string;
|
|
171
|
+
/**
|
|
172
|
+
* Version of the host application (e.g. an Electron app's `app.getVersion()`), used by **App version**
|
|
173
|
+
* targeting rules and as the `appVersion` flag attribute.
|
|
174
|
+
*/
|
|
175
|
+
appVersion?: string;
|
|
176
|
+
/**
|
|
177
|
+
* Maps the browser URL to the page URL Guidely sees (targeting, URL patterns, page analytics). By
|
|
178
|
+
* default http(s) URLs are used as they are and `file:` / custom-protocol URLs (Electron, desktop
|
|
179
|
+
* shells) are made app-relative with `normalizeAppUrl`: `file:///…/index.html#/settings` → `file://app/settings`.
|
|
180
|
+
*/
|
|
181
|
+
pageUrl?: (href: string) => string;
|
|
144
182
|
/** `false` stops uncaught errors / unhandled rejections from being reported (`captureException` still works). */
|
|
145
183
|
captureErrors?: boolean;
|
|
146
184
|
/**
|
|
@@ -158,6 +196,11 @@ interface GuidelyOptions {
|
|
|
158
196
|
maskNetworkRequest?: (request: NetworkRequest) => NetworkRequest | null;
|
|
159
197
|
};
|
|
160
198
|
onError?: (error: Error) => void;
|
|
199
|
+
/**
|
|
200
|
+
* Language for guides and the resource center (BCP 47, e.g. `fr` or `pt-BR`). Defaults to the
|
|
201
|
+
* user's `language` / `locale` trait, then the browser language; see also `setLocale()`.
|
|
202
|
+
*/
|
|
203
|
+
locale?: string;
|
|
161
204
|
/** Overrides for the project theme (colors, radius, font). */
|
|
162
205
|
theme?: Partial<ProjectTheme>;
|
|
163
206
|
/** Identify this user as soon as the client initialises. */
|
|
@@ -173,6 +216,13 @@ interface GuidelyOptions {
|
|
|
173
216
|
* `{ pollingMode: "stream" }`. Identity, page and device attributes are maintained for you.
|
|
174
217
|
*/
|
|
175
218
|
flags?: false | Partial<FlagsClientOptions>;
|
|
219
|
+
/**
|
|
220
|
+
* Agent Analytics (`agentConversation()`): `redact` gets each turn before it leaves the browser and
|
|
221
|
+
* returns a changed copy (e.g. with personal details removed from `text`), or null to drop it.
|
|
222
|
+
*/
|
|
223
|
+
agents?: {
|
|
224
|
+
redact?: AgentTurnRedactor;
|
|
225
|
+
};
|
|
176
226
|
}
|
|
177
227
|
interface ChecklistProgressState {
|
|
178
228
|
completedTaskIds: string[];
|
|
@@ -188,6 +238,10 @@ interface PersistedState {
|
|
|
188
238
|
surveysSubmitted: string[];
|
|
189
239
|
audienceIds: string[];
|
|
190
240
|
firstSeenAt?: string;
|
|
241
|
+
/** ISO times of automatic launches that count toward guide throttling (pruned to the window). */
|
|
242
|
+
throttle?: string[];
|
|
243
|
+
/** When the visitor last opened the resource center's "What's new" (unread badge). */
|
|
244
|
+
announcementsSeenAt?: string;
|
|
191
245
|
}
|
|
192
246
|
interface SessionState {
|
|
193
247
|
id: string;
|
|
@@ -257,4 +311,4 @@ interface GuidelyEvents {
|
|
|
257
311
|
}
|
|
258
312
|
type GuidelyEventName = keyof GuidelyEvents;
|
|
259
313
|
|
|
260
|
-
export { type ChecklistProgressState as C, EventQueue as E, type GuideLocalState as G, type PersistedState as P, type SessionState as S, type ConsentState as a, type EventQueueOptions as b, type GuidelyAccount as c, type GuidelyEnvironment as d, type GuidelyUser as e, areMutuallyExclusive as f,
|
|
314
|
+
export { type AgentTurnRedactor as A, type ChecklistProgressState as C, EventQueue as E, type GuideLocalState as G, type PersistedState as P, type SessionState as S, type ConsentState as a, type EventQueueOptions as b, type GuidelyAccount as c, type GuidelyEnvironment as d, type GuidelyUser as e, areMutuallyExclusive as f, countsTowardThrottle as g, dismissedAfterMerge as h, isExclusiveGuide as i, isFrequencyEligible as j, isThrottleEligible as k, isWithinSchedule as l, type GuidelyOptions as m, type GuidelyEvents as n, type GuideStepInfo as o, pruneLaunches as p, type GuidelyEventName as q };
|