@guidelytech/sdk-core 0.2.1 → 0.4.1
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/README.md +37 -4
- package/dist/chunk-GJT7CFAC.js +151 -0
- package/dist/chunk-GJT7CFAC.js.map +1 -0
- package/dist/chunk-HLR3W63Z.js +282 -0
- package/dist/chunk-HLR3W63Z.js.map +1 -0
- package/dist/headless.cjs +423 -0
- package/dist/headless.cjs.map +1 -0
- package/dist/headless.d.cts +65 -0
- package/dist/headless.d.ts +65 -0
- package/dist/headless.js +35 -0
- package/dist/headless.js.map +1 -0
- package/dist/index.cjs +731 -53
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +86 -213
- package/dist/index.d.ts +86 -213
- package/dist/index.js +701 -411
- package/dist/index.js.map +1 -1
- package/dist/recorder.cjs +766 -0
- package/dist/recorder.cjs.map +1 -0
- package/dist/recorder.d.cts +155 -0
- package/dist/recorder.d.ts +155 -0
- package/dist/recorder.js +735 -0
- package/dist/recorder.js.map +1 -0
- package/dist/types-BZ9r3Fs9.d.cts +247 -0
- package/dist/types-BZ9r3Fs9.d.ts +247 -0
- package/package.json +21 -8
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/recorder.ts","../src/network.ts","../src/replay.ts","../src/http.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(\\.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 { backoffMs } from \"./http\";\nimport type { NetworkCaptureOptions } from \"./network\";\nimport type { RecorderHandle, RecorderStartOptions } from \"./recorder-types\";\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}\n\ninterface Chunk {\n sessionId: 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\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\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) 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 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 async drain(): Promise<void> {\n this.uploading = true;\n try {\n while (this.queue.length) {\n const chunk = this.queue[0] as Chunk;\n const result = await this.send(chunk);\n if (result === \"retry\") {\n this.attempt += 1;\n if (!this.destroyed && this.attempt < 6) {\n this.retryTimer = setTimeout(() => {\n this.retryTimer = null;\n void this.flush();\n }, backoffMs(this.attempt - 1));\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 res = await f(this.opts.url({ enc }), { method: \"POST\", body, headers: { \"Content-Type\": \"text/plain\" }, keepalive: size < UNLOAD_MAX_BYTES, credentials: \"omit\", mode: \"cors\" });\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 for (const chunk of pending) {\n const parts = chunk.seq === undefined ? 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","export 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/** Thin HTTP client for the public SDK API. Adds the public key as query + header. */\nexport class Api {\n constructor(\n private readonly baseUrl: string,\n private readonly key: string,\n private readonly fetchImpl?: FetchLike,\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 if (options.body !== undefined) 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 const res = await f(this.url(path, options.params), {\n method,\n headers,\n body: options.body === undefined ? undefined : JSON.stringify(options.body),\n keepalive: options.keepalive,\n signal: ac?.signal,\n credentials: \"omit\",\n mode: \"cors\",\n });\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 /**\n * POST that may answer with server-sent events. Each event is passed to `onEvent`; a JSON answer is\n * returned instead. No timeout: Scout replies can take a while and the server keeps the stream alive.\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 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 credentials: \"omit\",\n mode: \"cors\",\n });\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 await readEventStream(res.body, onEvent);\n return { status: res.status, ok: res.ok, data: null, etag: null };\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/** Fire-and-forget POST with bounded retries (used for state sync; never blocks UI). */\nexport async function postWithRetry<T>(api: Api, path: string, body: unknown, attempts = 3, baseMs = 500): Promise<ApiResponse<T> | null> {\n for (let i = 0; i < attempts; i++) {\n try {\n const res = await api.post<T>(path, body);\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/** Parses a `text/event-stream` body (`event:` + single-line JSON `data:`; comment lines ignored). */\nexport async function readEventStream(body: ReadableStream<Uint8Array>, onEvent: (event: string, data: unknown) => 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) 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"],"mappings":"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","names":["import_runtime"]}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { NetworkRequest } from '@guidelytech/shared/runtime';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Network recording for session replays (part of the lazily loaded recorder chunk, never the base
|
|
5
|
+
* SDK): patches `fetch`, `XMLHttpRequest` and `WebSocket` while recording, observes resource timing,
|
|
6
|
+
* and emits batched `NetworkPluginPayload`s as rrweb plugin events (`NETWORK_PLUGIN`).
|
|
7
|
+
* The page's own requests behave exactly as before: the original promise / XHR / socket is returned
|
|
8
|
+
* untouched, bodies are read from clones, and every recording step is wrapped in try/catch.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** Network recording settings (merged from the project setting and SDK options by the client). */
|
|
12
|
+
interface NetworkCaptureOptions {
|
|
13
|
+
/** Record headers and text bodies (`recordNetworkPayloads`). */
|
|
14
|
+
payloads: boolean;
|
|
15
|
+
/** Guidely API base URL: requests to `<origin>/api/v1/sdk/` are never recorded. */
|
|
16
|
+
apiUrl?: string;
|
|
17
|
+
/** Last say over each request: return a modified copy, or null to drop it (exceptions drop it too). */
|
|
18
|
+
mask?: (request: NetworkRequest) => NetworkRequest | null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Recorder settings for one session (already merged from the project setting and SDK options). */
|
|
22
|
+
interface ReplayRecordingConfig {
|
|
23
|
+
maskTextSelector: string;
|
|
24
|
+
blockSelector: string;
|
|
25
|
+
recordConsole: boolean;
|
|
26
|
+
network?: NetworkCaptureOptions | null;
|
|
27
|
+
}
|
|
28
|
+
/** Per-upload metadata (`ReplayChunk` minus `sessionId`/`windowId`/`seq`/`events`). */
|
|
29
|
+
interface ReplayChunkMeta {
|
|
30
|
+
anonymousId: string;
|
|
31
|
+
userId?: string | null;
|
|
32
|
+
url?: string;
|
|
33
|
+
device?: {
|
|
34
|
+
type: string;
|
|
35
|
+
browser?: string;
|
|
36
|
+
os?: string;
|
|
37
|
+
};
|
|
38
|
+
viewport?: {
|
|
39
|
+
width: number;
|
|
40
|
+
height: number;
|
|
41
|
+
};
|
|
42
|
+
sdk?: {
|
|
43
|
+
name: string;
|
|
44
|
+
version: string;
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
interface ReplayControllerOptions {
|
|
48
|
+
/** Upload URL for `POST /api/v1/sdk/replay` (with `?key=` and the given params). */
|
|
49
|
+
url: (params: Record<string, string | undefined>) => string;
|
|
50
|
+
fetch: () => typeof fetch | null;
|
|
51
|
+
meta: () => ReplayChunkMeta;
|
|
52
|
+
/** Starts rrweb (`startRecorder` from the recorder chunk; injectable for tests). */
|
|
53
|
+
startRecorder: (options: RecorderStartOptions) => RecorderHandle | null;
|
|
54
|
+
/** The server answered `{ disabled: true }`: recording must stop for this session. */
|
|
55
|
+
onDisabled: (sessionId: string) => void;
|
|
56
|
+
onError: (err: unknown) => void;
|
|
57
|
+
flushIntervalMs?: number;
|
|
58
|
+
/** Buffered bytes that trigger an early upload (default ~1MB). */
|
|
59
|
+
maxBufferBytes?: number;
|
|
60
|
+
/** Upload backlog cap; the oldest chunks are dropped above it (default ~5MB). */
|
|
61
|
+
maxQueuedBytes?: number;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Session-replay controller (part of the lazily loaded recorder chunk): records with rrweb, buffers events, and uploads them
|
|
65
|
+
* every 5s (or once ~1MB is buffered) as gzip JSON (`?enc=gzip`; plain JSON without
|
|
66
|
+
* `CompressionStream`). On page hide it sends what is left with `sendBeacon` / keepalive fetch.
|
|
67
|
+
*/
|
|
68
|
+
declare class ReplayController {
|
|
69
|
+
private readonly opts;
|
|
70
|
+
readonly windowId: string;
|
|
71
|
+
private handle;
|
|
72
|
+
private sessionId;
|
|
73
|
+
private configKey;
|
|
74
|
+
private buffer;
|
|
75
|
+
private queue;
|
|
76
|
+
private queuedBytes;
|
|
77
|
+
private seqs;
|
|
78
|
+
private interval;
|
|
79
|
+
private retryTimer;
|
|
80
|
+
private attempt;
|
|
81
|
+
private uploading;
|
|
82
|
+
private flushing;
|
|
83
|
+
private destroyed;
|
|
84
|
+
constructor(opts: ReplayControllerOptions);
|
|
85
|
+
/** True while rrweb is recording. */
|
|
86
|
+
isRecording(): boolean;
|
|
87
|
+
/** The session currently being recorded, if any. */
|
|
88
|
+
get activeSessionId(): string | null;
|
|
89
|
+
/** Starts (or keeps) recording for `sessionId`. Restarts when the session or settings changed. */
|
|
90
|
+
start(sessionId: string, config: ReplayRecordingConfig): void;
|
|
91
|
+
/** Stops recording. Buffered events are uploaded unless `discard` (consent withdrawn, authoring). */
|
|
92
|
+
stop(options?: {
|
|
93
|
+
discard?: boolean;
|
|
94
|
+
}): void;
|
|
95
|
+
/** Adds an rrweb custom event (Guidely event markers) while recording. */
|
|
96
|
+
addCustomEvent(tag: string, payload: unknown): void;
|
|
97
|
+
private push;
|
|
98
|
+
/** Moves the buffer to the upload queue, dropping the oldest chunks above the backlog cap. */
|
|
99
|
+
private seal;
|
|
100
|
+
private nextSeq;
|
|
101
|
+
private body;
|
|
102
|
+
private remove;
|
|
103
|
+
/** Uploads the buffer and the backlog, one chunk at a time; retries with backoff on failure. */
|
|
104
|
+
flush(): Promise<void>;
|
|
105
|
+
private drain;
|
|
106
|
+
private send;
|
|
107
|
+
/**
|
|
108
|
+
* Page is being hidden / unloaded: sends whatever is buffered synchronously as uncompressed JSON
|
|
109
|
+
* (`sendBeacon` up to 60KB, keepalive fetch otherwise), splitting chunks that never went out.
|
|
110
|
+
*/
|
|
111
|
+
flushOnHide(): void;
|
|
112
|
+
private sendNow;
|
|
113
|
+
/** Stops recording and uploads what is left; no retries afterwards. */
|
|
114
|
+
destroy(): void;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Options for one rrweb recording (see `recorder.ts`). */
|
|
118
|
+
interface RecorderStartOptions {
|
|
119
|
+
/** Called for every rrweb event (`eventWithTime`). */
|
|
120
|
+
emit: (event: {
|
|
121
|
+
type: number;
|
|
122
|
+
timestamp: number;
|
|
123
|
+
}) => void;
|
|
124
|
+
/** CSS selector of elements whose text is masked (`*` masks everything). */
|
|
125
|
+
maskTextSelector: string;
|
|
126
|
+
/** CSS selector of elements replaced by a placeholder box. */
|
|
127
|
+
blockSelector: string;
|
|
128
|
+
/** Record console output (rrweb console plugin). */
|
|
129
|
+
recordConsole: boolean;
|
|
130
|
+
/** Record network requests and WebSockets (`guidely/network@1` plugin events); null/absent = off. */
|
|
131
|
+
network?: NetworkCaptureOptions | null;
|
|
132
|
+
/** Errors raised inside rrweb (never rethrown into the page). */
|
|
133
|
+
onError?: (error: unknown) => void;
|
|
134
|
+
}
|
|
135
|
+
/** A running recording. */
|
|
136
|
+
interface RecorderHandle {
|
|
137
|
+
stop(): void;
|
|
138
|
+
addCustomEvent(tag: string, payload: unknown): void;
|
|
139
|
+
/** Emits buffered plugin data (network) now, e.g. before the page is hidden. */
|
|
140
|
+
flush?(): void;
|
|
141
|
+
}
|
|
142
|
+
/** What the lazily loaded recorder chunk exposes (`window.__guidelyRecorder` in the script-tag build). */
|
|
143
|
+
interface RecorderApi {
|
|
144
|
+
/** Starts recording; `null` when rrweb could not start (unsupported browser). */
|
|
145
|
+
startRecorder(options: RecorderStartOptions): RecorderHandle | null;
|
|
146
|
+
/** Creates the replay controller (buffering + uploads) bound to this recorder. */
|
|
147
|
+
createReplay(options: Omit<ReplayControllerOptions, "startRecorder">): ReplayController;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Starts an rrweb recording with Guidely's privacy defaults (inputs always masked, no canvas, no fonts). */
|
|
151
|
+
declare function startRecorder(options: RecorderStartOptions): RecorderHandle | null;
|
|
152
|
+
/** The recorder chunk's API (the script-tag build substitutes a loader that resolves to it). */
|
|
153
|
+
declare const recorder: RecorderApi;
|
|
154
|
+
|
|
155
|
+
export { type NetworkCaptureOptions, type RecorderApi, type RecorderHandle, type RecorderStartOptions, type ReplayChunkMeta, ReplayController, type ReplayControllerOptions, type ReplayRecordingConfig, recorder, startRecorder };
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { NetworkRequest } from '@guidelytech/shared/runtime';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Network recording for session replays (part of the lazily loaded recorder chunk, never the base
|
|
5
|
+
* SDK): patches `fetch`, `XMLHttpRequest` and `WebSocket` while recording, observes resource timing,
|
|
6
|
+
* and emits batched `NetworkPluginPayload`s as rrweb plugin events (`NETWORK_PLUGIN`).
|
|
7
|
+
* The page's own requests behave exactly as before: the original promise / XHR / socket is returned
|
|
8
|
+
* untouched, bodies are read from clones, and every recording step is wrapped in try/catch.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** Network recording settings (merged from the project setting and SDK options by the client). */
|
|
12
|
+
interface NetworkCaptureOptions {
|
|
13
|
+
/** Record headers and text bodies (`recordNetworkPayloads`). */
|
|
14
|
+
payloads: boolean;
|
|
15
|
+
/** Guidely API base URL: requests to `<origin>/api/v1/sdk/` are never recorded. */
|
|
16
|
+
apiUrl?: string;
|
|
17
|
+
/** Last say over each request: return a modified copy, or null to drop it (exceptions drop it too). */
|
|
18
|
+
mask?: (request: NetworkRequest) => NetworkRequest | null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Recorder settings for one session (already merged from the project setting and SDK options). */
|
|
22
|
+
interface ReplayRecordingConfig {
|
|
23
|
+
maskTextSelector: string;
|
|
24
|
+
blockSelector: string;
|
|
25
|
+
recordConsole: boolean;
|
|
26
|
+
network?: NetworkCaptureOptions | null;
|
|
27
|
+
}
|
|
28
|
+
/** Per-upload metadata (`ReplayChunk` minus `sessionId`/`windowId`/`seq`/`events`). */
|
|
29
|
+
interface ReplayChunkMeta {
|
|
30
|
+
anonymousId: string;
|
|
31
|
+
userId?: string | null;
|
|
32
|
+
url?: string;
|
|
33
|
+
device?: {
|
|
34
|
+
type: string;
|
|
35
|
+
browser?: string;
|
|
36
|
+
os?: string;
|
|
37
|
+
};
|
|
38
|
+
viewport?: {
|
|
39
|
+
width: number;
|
|
40
|
+
height: number;
|
|
41
|
+
};
|
|
42
|
+
sdk?: {
|
|
43
|
+
name: string;
|
|
44
|
+
version: string;
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
interface ReplayControllerOptions {
|
|
48
|
+
/** Upload URL for `POST /api/v1/sdk/replay` (with `?key=` and the given params). */
|
|
49
|
+
url: (params: Record<string, string | undefined>) => string;
|
|
50
|
+
fetch: () => typeof fetch | null;
|
|
51
|
+
meta: () => ReplayChunkMeta;
|
|
52
|
+
/** Starts rrweb (`startRecorder` from the recorder chunk; injectable for tests). */
|
|
53
|
+
startRecorder: (options: RecorderStartOptions) => RecorderHandle | null;
|
|
54
|
+
/** The server answered `{ disabled: true }`: recording must stop for this session. */
|
|
55
|
+
onDisabled: (sessionId: string) => void;
|
|
56
|
+
onError: (err: unknown) => void;
|
|
57
|
+
flushIntervalMs?: number;
|
|
58
|
+
/** Buffered bytes that trigger an early upload (default ~1MB). */
|
|
59
|
+
maxBufferBytes?: number;
|
|
60
|
+
/** Upload backlog cap; the oldest chunks are dropped above it (default ~5MB). */
|
|
61
|
+
maxQueuedBytes?: number;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Session-replay controller (part of the lazily loaded recorder chunk): records with rrweb, buffers events, and uploads them
|
|
65
|
+
* every 5s (or once ~1MB is buffered) as gzip JSON (`?enc=gzip`; plain JSON without
|
|
66
|
+
* `CompressionStream`). On page hide it sends what is left with `sendBeacon` / keepalive fetch.
|
|
67
|
+
*/
|
|
68
|
+
declare class ReplayController {
|
|
69
|
+
private readonly opts;
|
|
70
|
+
readonly windowId: string;
|
|
71
|
+
private handle;
|
|
72
|
+
private sessionId;
|
|
73
|
+
private configKey;
|
|
74
|
+
private buffer;
|
|
75
|
+
private queue;
|
|
76
|
+
private queuedBytes;
|
|
77
|
+
private seqs;
|
|
78
|
+
private interval;
|
|
79
|
+
private retryTimer;
|
|
80
|
+
private attempt;
|
|
81
|
+
private uploading;
|
|
82
|
+
private flushing;
|
|
83
|
+
private destroyed;
|
|
84
|
+
constructor(opts: ReplayControllerOptions);
|
|
85
|
+
/** True while rrweb is recording. */
|
|
86
|
+
isRecording(): boolean;
|
|
87
|
+
/** The session currently being recorded, if any. */
|
|
88
|
+
get activeSessionId(): string | null;
|
|
89
|
+
/** Starts (or keeps) recording for `sessionId`. Restarts when the session or settings changed. */
|
|
90
|
+
start(sessionId: string, config: ReplayRecordingConfig): void;
|
|
91
|
+
/** Stops recording. Buffered events are uploaded unless `discard` (consent withdrawn, authoring). */
|
|
92
|
+
stop(options?: {
|
|
93
|
+
discard?: boolean;
|
|
94
|
+
}): void;
|
|
95
|
+
/** Adds an rrweb custom event (Guidely event markers) while recording. */
|
|
96
|
+
addCustomEvent(tag: string, payload: unknown): void;
|
|
97
|
+
private push;
|
|
98
|
+
/** Moves the buffer to the upload queue, dropping the oldest chunks above the backlog cap. */
|
|
99
|
+
private seal;
|
|
100
|
+
private nextSeq;
|
|
101
|
+
private body;
|
|
102
|
+
private remove;
|
|
103
|
+
/** Uploads the buffer and the backlog, one chunk at a time; retries with backoff on failure. */
|
|
104
|
+
flush(): Promise<void>;
|
|
105
|
+
private drain;
|
|
106
|
+
private send;
|
|
107
|
+
/**
|
|
108
|
+
* Page is being hidden / unloaded: sends whatever is buffered synchronously as uncompressed JSON
|
|
109
|
+
* (`sendBeacon` up to 60KB, keepalive fetch otherwise), splitting chunks that never went out.
|
|
110
|
+
*/
|
|
111
|
+
flushOnHide(): void;
|
|
112
|
+
private sendNow;
|
|
113
|
+
/** Stops recording and uploads what is left; no retries afterwards. */
|
|
114
|
+
destroy(): void;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Options for one rrweb recording (see `recorder.ts`). */
|
|
118
|
+
interface RecorderStartOptions {
|
|
119
|
+
/** Called for every rrweb event (`eventWithTime`). */
|
|
120
|
+
emit: (event: {
|
|
121
|
+
type: number;
|
|
122
|
+
timestamp: number;
|
|
123
|
+
}) => void;
|
|
124
|
+
/** CSS selector of elements whose text is masked (`*` masks everything). */
|
|
125
|
+
maskTextSelector: string;
|
|
126
|
+
/** CSS selector of elements replaced by a placeholder box. */
|
|
127
|
+
blockSelector: string;
|
|
128
|
+
/** Record console output (rrweb console plugin). */
|
|
129
|
+
recordConsole: boolean;
|
|
130
|
+
/** Record network requests and WebSockets (`guidely/network@1` plugin events); null/absent = off. */
|
|
131
|
+
network?: NetworkCaptureOptions | null;
|
|
132
|
+
/** Errors raised inside rrweb (never rethrown into the page). */
|
|
133
|
+
onError?: (error: unknown) => void;
|
|
134
|
+
}
|
|
135
|
+
/** A running recording. */
|
|
136
|
+
interface RecorderHandle {
|
|
137
|
+
stop(): void;
|
|
138
|
+
addCustomEvent(tag: string, payload: unknown): void;
|
|
139
|
+
/** Emits buffered plugin data (network) now, e.g. before the page is hidden. */
|
|
140
|
+
flush?(): void;
|
|
141
|
+
}
|
|
142
|
+
/** What the lazily loaded recorder chunk exposes (`window.__guidelyRecorder` in the script-tag build). */
|
|
143
|
+
interface RecorderApi {
|
|
144
|
+
/** Starts recording; `null` when rrweb could not start (unsupported browser). */
|
|
145
|
+
startRecorder(options: RecorderStartOptions): RecorderHandle | null;
|
|
146
|
+
/** Creates the replay controller (buffering + uploads) bound to this recorder. */
|
|
147
|
+
createReplay(options: Omit<ReplayControllerOptions, "startRecorder">): ReplayController;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Starts an rrweb recording with Guidely's privacy defaults (inputs always masked, no canvas, no fonts). */
|
|
151
|
+
declare function startRecorder(options: RecorderStartOptions): RecorderHandle | null;
|
|
152
|
+
/** The recorder chunk's API (the script-tag build substitutes a loader that resolves to it). */
|
|
153
|
+
declare const recorder: RecorderApi;
|
|
154
|
+
|
|
155
|
+
export { type NetworkCaptureOptions, type RecorderApi, type RecorderHandle, type RecorderStartOptions, type ReplayChunkMeta, ReplayController, type ReplayControllerOptions, type ReplayRecordingConfig, recorder, startRecorder };
|