@guidelytech/sdk-core 0.2.1 → 0.4.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/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"],"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"],"mappings":"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","names":["randomId","randomId"]}
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { SdkEvent, PublishedGuide, FrequencyConfig, AutocaptureSettings, NetworkRequest, ProjectTheme, SdkConfigResponse, ChecklistTask, MessengerMessage, MessengerConversation } from '@guidelytech/shared';
|
|
2
|
+
|
|
3
|
+
type StorageKind = "local" | "session" | "memory";
|
|
4
|
+
/** Minimal synchronous key/value store; never throws (private mode, quota, SSR). */
|
|
5
|
+
interface KeyValueStore {
|
|
6
|
+
get(key: string): string | null;
|
|
7
|
+
set(key: string, value: string): void;
|
|
8
|
+
remove(key: string): void;
|
|
9
|
+
}
|
|
10
|
+
declare function memoryStore(): KeyValueStore;
|
|
11
|
+
/** Creates a store of the requested kind, degrading to memory when unavailable. */
|
|
12
|
+
declare function createStorage(kind?: StorageKind): KeyValueStore;
|
|
13
|
+
declare function readJson<T>(store: KeyValueStore, key: string, fallback: T): T;
|
|
14
|
+
declare function writeJson(store: KeyValueStore, key: string, value: unknown): void;
|
|
15
|
+
/** Storage keys used by the SDK (documented for integrators). */
|
|
16
|
+
declare const KEYS: {
|
|
17
|
+
readonly anon: "guidely:anon";
|
|
18
|
+
readonly seen: "guidely:seen";
|
|
19
|
+
readonly config: (key: string) => string;
|
|
20
|
+
readonly etag: (key: string) => string;
|
|
21
|
+
readonly state: (key: string) => string;
|
|
22
|
+
readonly user: (key: string) => string;
|
|
23
|
+
readonly queue: (key: string) => string;
|
|
24
|
+
readonly session: "guidely:session";
|
|
25
|
+
readonly active: "guidely:active";
|
|
26
|
+
readonly authoring: "guidely:authoring";
|
|
27
|
+
/** Messenger conversations this browser can read: [{ id, token }]. */
|
|
28
|
+
readonly conversations: (key: string) => string;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
interface EventQueueOptions {
|
|
32
|
+
store: KeyValueStore;
|
|
33
|
+
storageKey: string;
|
|
34
|
+
/** Sends one batch. Resolve `{ ok: true }` on success, `{ ok: false, retry }` otherwise. */
|
|
35
|
+
send: (events: SdkEvent[]) => Promise<{
|
|
36
|
+
ok: boolean;
|
|
37
|
+
retry?: boolean;
|
|
38
|
+
}>;
|
|
39
|
+
/** Synchronous best-effort send used on page hide. */
|
|
40
|
+
beacon?: (events: SdkEvent[]) => boolean;
|
|
41
|
+
/** Should events be flushed right now? (analytics enabled + consent granted). */
|
|
42
|
+
enabled: () => boolean;
|
|
43
|
+
flushIntervalMs?: number;
|
|
44
|
+
maxQueue?: number;
|
|
45
|
+
batchSize?: number;
|
|
46
|
+
maxAttempts?: number;
|
|
47
|
+
onError?: (err: unknown) => void;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Bounded, persisted event queue. Batches ≤50 events every 2s (or on demand), dedupes by
|
|
51
|
+
* `eventId`, retries with exponential backoff (1s→30s, 5 attempts) and keeps failed events for
|
|
52
|
+
* the next flush. Drops the oldest events once the queue exceeds 200.
|
|
53
|
+
*/
|
|
54
|
+
declare class EventQueue {
|
|
55
|
+
private readonly opts;
|
|
56
|
+
private events;
|
|
57
|
+
private timer;
|
|
58
|
+
private retryTimer;
|
|
59
|
+
private inflight;
|
|
60
|
+
private inflightIds;
|
|
61
|
+
private attempt;
|
|
62
|
+
private destroyed;
|
|
63
|
+
private readonly flushIntervalMs;
|
|
64
|
+
private readonly maxQueue;
|
|
65
|
+
private readonly batchSize;
|
|
66
|
+
private readonly maxAttempts;
|
|
67
|
+
constructor(opts: EventQueueOptions);
|
|
68
|
+
get size(): number;
|
|
69
|
+
/** Pending events (read-only copy, for debugging). */
|
|
70
|
+
peek(): SdkEvent[];
|
|
71
|
+
enqueue(event: SdkEvent): void;
|
|
72
|
+
private persist;
|
|
73
|
+
private schedule;
|
|
74
|
+
/** Sends queued events now (one batch at a time). Safe to call at any time. */
|
|
75
|
+
flush(): Promise<void>;
|
|
76
|
+
/** Best-effort synchronous flush on unload using `sendBeacon`. */
|
|
77
|
+
flushSync(): void;
|
|
78
|
+
clear(): void;
|
|
79
|
+
destroy(): void;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Per-guide state kept locally and merged with the server's `IdentifyResponse.guideState`. */
|
|
83
|
+
interface GuideLocalState {
|
|
84
|
+
impressions: number;
|
|
85
|
+
completedAt?: string | null;
|
|
86
|
+
dismissedAt?: string | null;
|
|
87
|
+
lastShownAt?: string | null;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Pure frequency evaluation. `shownThisSession` comes from session storage.
|
|
91
|
+
* - `once`: a single impression ever (unless `reshowAfterDismiss` and it was only dismissed).
|
|
92
|
+
* - `once_per_session`: at most once per browser session.
|
|
93
|
+
* - `every_time`: on every trigger (still blocked by completion/dismissal rules below).
|
|
94
|
+
* - `interval`: at most once every `intervalHours`.
|
|
95
|
+
* `maxImpressions` caps total impressions; dismissal blocks re-showing unless `reshowAfterDismiss`
|
|
96
|
+
* (then `cooldownHours` must have elapsed); completion blocks unless mode is `every_time`.
|
|
97
|
+
*/
|
|
98
|
+
declare function isFrequencyEligible(freq: FrequencyConfig, state: GuideLocalState | undefined, shownThisSession: boolean, now?: number): boolean;
|
|
99
|
+
/**
|
|
100
|
+
* Exclusive guides (flows, modals, tooltips, announcements, surveys) run one at a time.
|
|
101
|
+
* Banners, checklists and hotspot-only tooltips can coexist with an active flow.
|
|
102
|
+
*/
|
|
103
|
+
declare function isExclusiveGuide(guide: PublishedGuide): boolean;
|
|
104
|
+
/** Is `a` mutually exclusive with `b` (by id or key, in either direction)? */
|
|
105
|
+
declare function areMutuallyExclusive(a: PublishedGuide, b: PublishedGuide): boolean;
|
|
106
|
+
/** Survey scheduling window. */
|
|
107
|
+
declare function isWithinSchedule(guide: PublishedGuide, now?: number): boolean;
|
|
108
|
+
|
|
109
|
+
type GuidelyEnvironment = "development" | "staging" | "production";
|
|
110
|
+
type ConsentState = "granted" | "denied" | "pending";
|
|
111
|
+
interface GuidelyAccount {
|
|
112
|
+
id: string;
|
|
113
|
+
traits?: Record<string, unknown>;
|
|
114
|
+
}
|
|
115
|
+
interface GuidelyUser {
|
|
116
|
+
id: string;
|
|
117
|
+
traits?: Record<string, unknown>;
|
|
118
|
+
account?: GuidelyAccount;
|
|
119
|
+
}
|
|
120
|
+
interface GuidelyOptions {
|
|
121
|
+
/** Environment public key, e.g. `pk_dev_…`. Not a secret. */
|
|
122
|
+
projectId: string;
|
|
123
|
+
environment?: GuidelyEnvironment;
|
|
124
|
+
/** Base URL of the Guidely API (default `https://api.guidely.app`; use `http://localhost:3000` locally). */
|
|
125
|
+
apiUrl?: string;
|
|
126
|
+
/** Log trigger decisions and requests to the console. */
|
|
127
|
+
debug?: boolean;
|
|
128
|
+
/** Send analytics events (default `true`). Guides still render when disabled. */
|
|
129
|
+
analytics?: boolean;
|
|
130
|
+
/** `pending` buffers events until `setConsent("granted")`; `denied` disables analytics. */
|
|
131
|
+
consent?: ConsentState;
|
|
132
|
+
/** Where to persist identity, cached config and the event queue (default `local`). */
|
|
133
|
+
storage?: StorageKind;
|
|
134
|
+
/** Interval for re-checking element-visibility triggers while the page is idle (default 1000ms). */
|
|
135
|
+
pollIntervalMs?: number;
|
|
136
|
+
/** How often to refresh the published config (default 5 minutes). */
|
|
137
|
+
configRefreshMs?: number;
|
|
138
|
+
/** Elements whose text must never be captured (authoring snapshots, click text, session replays). Merged with the project setting. */
|
|
139
|
+
maskSelectors?: string[];
|
|
140
|
+
/**
|
|
141
|
+
* Page views, page leave, clicks and rage clicks are captured when the project enables them.
|
|
142
|
+
* `false` turns all of them off for this installation; an object turns individual ones off
|
|
143
|
+
* (`{ clicks: false }`). Options can only switch capture off, never on.
|
|
144
|
+
*/
|
|
145
|
+
autocapture?: boolean | Partial<AutocaptureSettings>;
|
|
146
|
+
/** App version or commit SHA (≤100 chars), sent with every `exception` to pick the matching uploaded source maps. */
|
|
147
|
+
release?: string;
|
|
148
|
+
/** `false` stops uncaught errors / unhandled rejections from being reported (`captureException` still works). */
|
|
149
|
+
captureErrors?: boolean;
|
|
150
|
+
/**
|
|
151
|
+
* `false` never records sessions on this installation. An object can only make masking stricter:
|
|
152
|
+
* `maskAllText` masks every text node, `blockSelectors` replaces matching elements with a placeholder.
|
|
153
|
+
*/
|
|
154
|
+
sessionReplay?: boolean | {
|
|
155
|
+
maskAllText?: boolean;
|
|
156
|
+
blockSelectors?: string[];
|
|
157
|
+
/** `false`: never record network requests / WebSockets in replays. */
|
|
158
|
+
recordNetwork?: false;
|
|
159
|
+
/** `false`: never record request/response headers, bodies or WebSocket frame data. */
|
|
160
|
+
recordNetworkPayloads?: false;
|
|
161
|
+
/** Last say over each recorded request (applied after redaction): return a modified copy, or null to drop it. */
|
|
162
|
+
maskNetworkRequest?: (request: NetworkRequest) => NetworkRequest | null;
|
|
163
|
+
};
|
|
164
|
+
onError?: (error: Error) => void;
|
|
165
|
+
/** Overrides for the project theme (colors, radius, font). */
|
|
166
|
+
theme?: Partial<ProjectTheme>;
|
|
167
|
+
/** Identify this user as soon as the client initialises. */
|
|
168
|
+
user?: GuidelyUser;
|
|
169
|
+
/** Custom fetch (tests / polyfills). */
|
|
170
|
+
fetch?: typeof fetch;
|
|
171
|
+
/** Disable the authoring-mode URL parameter detection. */
|
|
172
|
+
disableAuthoring?: boolean;
|
|
173
|
+
}
|
|
174
|
+
interface ChecklistProgressState {
|
|
175
|
+
completedTaskIds: string[];
|
|
176
|
+
completedAt?: string | null;
|
|
177
|
+
dismissedAt?: string | null;
|
|
178
|
+
}
|
|
179
|
+
/** Everything the client persists per project key (`guidely:state:<key>`). */
|
|
180
|
+
interface PersistedState {
|
|
181
|
+
guides: Record<string, GuideLocalState>;
|
|
182
|
+
checklists: Record<string, ChecklistProgressState>;
|
|
183
|
+
surveysSubmitted: string[];
|
|
184
|
+
audienceIds: string[];
|
|
185
|
+
firstSeenAt?: string;
|
|
186
|
+
}
|
|
187
|
+
interface SessionState {
|
|
188
|
+
id: string;
|
|
189
|
+
startedAt: string;
|
|
190
|
+
shownGuides: string[];
|
|
191
|
+
events: string[];
|
|
192
|
+
/** Epoch ms of the last tracked activity; the session rotates after `sessionTimeoutMinutes` of inactivity. */
|
|
193
|
+
lastActivityAt?: number;
|
|
194
|
+
/** Page views sent in this session (the first one carries the referrer). */
|
|
195
|
+
pageViews?: number;
|
|
196
|
+
/** Exceptions reported in this session (capped at 100). */
|
|
197
|
+
exceptions?: number;
|
|
198
|
+
/** Replay decision for this session: sampled in / forced (`true`), sampled out or disabled by the server (`false`). */
|
|
199
|
+
replay?: boolean;
|
|
200
|
+
}
|
|
201
|
+
interface GuideStepInfo {
|
|
202
|
+
guide: PublishedGuide;
|
|
203
|
+
stepId: string;
|
|
204
|
+
stepIndex: number;
|
|
205
|
+
}
|
|
206
|
+
interface GuidelyEvents {
|
|
207
|
+
/** Config loaded (from network or cache) and triggers evaluated. */
|
|
208
|
+
ready: (config: SdkConfigResponse) => void;
|
|
209
|
+
/** Config (re)loaded. */
|
|
210
|
+
config: (config: SdkConfigResponse) => void;
|
|
211
|
+
"guide:start": (guide: PublishedGuide) => void;
|
|
212
|
+
"guide:step": (info: GuideStepInfo) => void;
|
|
213
|
+
"guide:complete": (guide: PublishedGuide) => void;
|
|
214
|
+
"guide:dismiss": (guide: PublishedGuide) => void;
|
|
215
|
+
"guide:skip": (guide: PublishedGuide) => void;
|
|
216
|
+
"checklist:task": (info: {
|
|
217
|
+
guide: PublishedGuide;
|
|
218
|
+
task: ChecklistTask;
|
|
219
|
+
progress: ChecklistProgressState;
|
|
220
|
+
}) => void;
|
|
221
|
+
"checklist:complete": (guide: PublishedGuide) => void;
|
|
222
|
+
"survey:submit": (info: {
|
|
223
|
+
guide: PublishedGuide;
|
|
224
|
+
answers: Record<string, unknown>;
|
|
225
|
+
responseId: string;
|
|
226
|
+
}) => void;
|
|
227
|
+
"resource-center:open": () => void;
|
|
228
|
+
"messenger:open": () => void;
|
|
229
|
+
/** A Scout or teammate message arrived in the messenger. */
|
|
230
|
+
"conversation:message": (info: {
|
|
231
|
+
message: MessengerMessage;
|
|
232
|
+
conversation: MessengerConversation;
|
|
233
|
+
}) => void;
|
|
234
|
+
error: (error: Error) => void;
|
|
235
|
+
/** Every tracked event (custom and lifecycle). */
|
|
236
|
+
track: (event: SdkEvent) => void;
|
|
237
|
+
identify: (info: {
|
|
238
|
+
userId: string | null;
|
|
239
|
+
anonymousId: string;
|
|
240
|
+
}) => void;
|
|
241
|
+
authoring: (info: {
|
|
242
|
+
active: boolean;
|
|
243
|
+
}) => void;
|
|
244
|
+
}
|
|
245
|
+
type GuidelyEventName = keyof GuidelyEvents;
|
|
246
|
+
|
|
247
|
+
export { type ChecklistProgressState as C, EventQueue as E, type GuideLocalState as G, KEYS as K, 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, type KeyValueStore as f, areMutuallyExclusive as g, isFrequencyEligible as h, isExclusiveGuide as i, isWithinSchedule as j, type GuidelyOptions as k, type GuidelyEvents as l, memoryStore as m, type GuideStepInfo as n, type GuidelyEventName as o, type StorageKind as p, createStorage as q, readJson as r, writeJson as w };
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { SdkEvent, PublishedGuide, FrequencyConfig, AutocaptureSettings, NetworkRequest, ProjectTheme, SdkConfigResponse, ChecklistTask, MessengerMessage, MessengerConversation } from '@guidelytech/shared';
|
|
2
|
+
|
|
3
|
+
type StorageKind = "local" | "session" | "memory";
|
|
4
|
+
/** Minimal synchronous key/value store; never throws (private mode, quota, SSR). */
|
|
5
|
+
interface KeyValueStore {
|
|
6
|
+
get(key: string): string | null;
|
|
7
|
+
set(key: string, value: string): void;
|
|
8
|
+
remove(key: string): void;
|
|
9
|
+
}
|
|
10
|
+
declare function memoryStore(): KeyValueStore;
|
|
11
|
+
/** Creates a store of the requested kind, degrading to memory when unavailable. */
|
|
12
|
+
declare function createStorage(kind?: StorageKind): KeyValueStore;
|
|
13
|
+
declare function readJson<T>(store: KeyValueStore, key: string, fallback: T): T;
|
|
14
|
+
declare function writeJson(store: KeyValueStore, key: string, value: unknown): void;
|
|
15
|
+
/** Storage keys used by the SDK (documented for integrators). */
|
|
16
|
+
declare const KEYS: {
|
|
17
|
+
readonly anon: "guidely:anon";
|
|
18
|
+
readonly seen: "guidely:seen";
|
|
19
|
+
readonly config: (key: string) => string;
|
|
20
|
+
readonly etag: (key: string) => string;
|
|
21
|
+
readonly state: (key: string) => string;
|
|
22
|
+
readonly user: (key: string) => string;
|
|
23
|
+
readonly queue: (key: string) => string;
|
|
24
|
+
readonly session: "guidely:session";
|
|
25
|
+
readonly active: "guidely:active";
|
|
26
|
+
readonly authoring: "guidely:authoring";
|
|
27
|
+
/** Messenger conversations this browser can read: [{ id, token }]. */
|
|
28
|
+
readonly conversations: (key: string) => string;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
interface EventQueueOptions {
|
|
32
|
+
store: KeyValueStore;
|
|
33
|
+
storageKey: string;
|
|
34
|
+
/** Sends one batch. Resolve `{ ok: true }` on success, `{ ok: false, retry }` otherwise. */
|
|
35
|
+
send: (events: SdkEvent[]) => Promise<{
|
|
36
|
+
ok: boolean;
|
|
37
|
+
retry?: boolean;
|
|
38
|
+
}>;
|
|
39
|
+
/** Synchronous best-effort send used on page hide. */
|
|
40
|
+
beacon?: (events: SdkEvent[]) => boolean;
|
|
41
|
+
/** Should events be flushed right now? (analytics enabled + consent granted). */
|
|
42
|
+
enabled: () => boolean;
|
|
43
|
+
flushIntervalMs?: number;
|
|
44
|
+
maxQueue?: number;
|
|
45
|
+
batchSize?: number;
|
|
46
|
+
maxAttempts?: number;
|
|
47
|
+
onError?: (err: unknown) => void;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Bounded, persisted event queue. Batches ≤50 events every 2s (or on demand), dedupes by
|
|
51
|
+
* `eventId`, retries with exponential backoff (1s→30s, 5 attempts) and keeps failed events for
|
|
52
|
+
* the next flush. Drops the oldest events once the queue exceeds 200.
|
|
53
|
+
*/
|
|
54
|
+
declare class EventQueue {
|
|
55
|
+
private readonly opts;
|
|
56
|
+
private events;
|
|
57
|
+
private timer;
|
|
58
|
+
private retryTimer;
|
|
59
|
+
private inflight;
|
|
60
|
+
private inflightIds;
|
|
61
|
+
private attempt;
|
|
62
|
+
private destroyed;
|
|
63
|
+
private readonly flushIntervalMs;
|
|
64
|
+
private readonly maxQueue;
|
|
65
|
+
private readonly batchSize;
|
|
66
|
+
private readonly maxAttempts;
|
|
67
|
+
constructor(opts: EventQueueOptions);
|
|
68
|
+
get size(): number;
|
|
69
|
+
/** Pending events (read-only copy, for debugging). */
|
|
70
|
+
peek(): SdkEvent[];
|
|
71
|
+
enqueue(event: SdkEvent): void;
|
|
72
|
+
private persist;
|
|
73
|
+
private schedule;
|
|
74
|
+
/** Sends queued events now (one batch at a time). Safe to call at any time. */
|
|
75
|
+
flush(): Promise<void>;
|
|
76
|
+
/** Best-effort synchronous flush on unload using `sendBeacon`. */
|
|
77
|
+
flushSync(): void;
|
|
78
|
+
clear(): void;
|
|
79
|
+
destroy(): void;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Per-guide state kept locally and merged with the server's `IdentifyResponse.guideState`. */
|
|
83
|
+
interface GuideLocalState {
|
|
84
|
+
impressions: number;
|
|
85
|
+
completedAt?: string | null;
|
|
86
|
+
dismissedAt?: string | null;
|
|
87
|
+
lastShownAt?: string | null;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Pure frequency evaluation. `shownThisSession` comes from session storage.
|
|
91
|
+
* - `once`: a single impression ever (unless `reshowAfterDismiss` and it was only dismissed).
|
|
92
|
+
* - `once_per_session`: at most once per browser session.
|
|
93
|
+
* - `every_time`: on every trigger (still blocked by completion/dismissal rules below).
|
|
94
|
+
* - `interval`: at most once every `intervalHours`.
|
|
95
|
+
* `maxImpressions` caps total impressions; dismissal blocks re-showing unless `reshowAfterDismiss`
|
|
96
|
+
* (then `cooldownHours` must have elapsed); completion blocks unless mode is `every_time`.
|
|
97
|
+
*/
|
|
98
|
+
declare function isFrequencyEligible(freq: FrequencyConfig, state: GuideLocalState | undefined, shownThisSession: boolean, now?: number): boolean;
|
|
99
|
+
/**
|
|
100
|
+
* Exclusive guides (flows, modals, tooltips, announcements, surveys) run one at a time.
|
|
101
|
+
* Banners, checklists and hotspot-only tooltips can coexist with an active flow.
|
|
102
|
+
*/
|
|
103
|
+
declare function isExclusiveGuide(guide: PublishedGuide): boolean;
|
|
104
|
+
/** Is `a` mutually exclusive with `b` (by id or key, in either direction)? */
|
|
105
|
+
declare function areMutuallyExclusive(a: PublishedGuide, b: PublishedGuide): boolean;
|
|
106
|
+
/** Survey scheduling window. */
|
|
107
|
+
declare function isWithinSchedule(guide: PublishedGuide, now?: number): boolean;
|
|
108
|
+
|
|
109
|
+
type GuidelyEnvironment = "development" | "staging" | "production";
|
|
110
|
+
type ConsentState = "granted" | "denied" | "pending";
|
|
111
|
+
interface GuidelyAccount {
|
|
112
|
+
id: string;
|
|
113
|
+
traits?: Record<string, unknown>;
|
|
114
|
+
}
|
|
115
|
+
interface GuidelyUser {
|
|
116
|
+
id: string;
|
|
117
|
+
traits?: Record<string, unknown>;
|
|
118
|
+
account?: GuidelyAccount;
|
|
119
|
+
}
|
|
120
|
+
interface GuidelyOptions {
|
|
121
|
+
/** Environment public key, e.g. `pk_dev_…`. Not a secret. */
|
|
122
|
+
projectId: string;
|
|
123
|
+
environment?: GuidelyEnvironment;
|
|
124
|
+
/** Base URL of the Guidely API (default `https://api.guidely.app`; use `http://localhost:3000` locally). */
|
|
125
|
+
apiUrl?: string;
|
|
126
|
+
/** Log trigger decisions and requests to the console. */
|
|
127
|
+
debug?: boolean;
|
|
128
|
+
/** Send analytics events (default `true`). Guides still render when disabled. */
|
|
129
|
+
analytics?: boolean;
|
|
130
|
+
/** `pending` buffers events until `setConsent("granted")`; `denied` disables analytics. */
|
|
131
|
+
consent?: ConsentState;
|
|
132
|
+
/** Where to persist identity, cached config and the event queue (default `local`). */
|
|
133
|
+
storage?: StorageKind;
|
|
134
|
+
/** Interval for re-checking element-visibility triggers while the page is idle (default 1000ms). */
|
|
135
|
+
pollIntervalMs?: number;
|
|
136
|
+
/** How often to refresh the published config (default 5 minutes). */
|
|
137
|
+
configRefreshMs?: number;
|
|
138
|
+
/** Elements whose text must never be captured (authoring snapshots, click text, session replays). Merged with the project setting. */
|
|
139
|
+
maskSelectors?: string[];
|
|
140
|
+
/**
|
|
141
|
+
* Page views, page leave, clicks and rage clicks are captured when the project enables them.
|
|
142
|
+
* `false` turns all of them off for this installation; an object turns individual ones off
|
|
143
|
+
* (`{ clicks: false }`). Options can only switch capture off, never on.
|
|
144
|
+
*/
|
|
145
|
+
autocapture?: boolean | Partial<AutocaptureSettings>;
|
|
146
|
+
/** App version or commit SHA (≤100 chars), sent with every `exception` to pick the matching uploaded source maps. */
|
|
147
|
+
release?: string;
|
|
148
|
+
/** `false` stops uncaught errors / unhandled rejections from being reported (`captureException` still works). */
|
|
149
|
+
captureErrors?: boolean;
|
|
150
|
+
/**
|
|
151
|
+
* `false` never records sessions on this installation. An object can only make masking stricter:
|
|
152
|
+
* `maskAllText` masks every text node, `blockSelectors` replaces matching elements with a placeholder.
|
|
153
|
+
*/
|
|
154
|
+
sessionReplay?: boolean | {
|
|
155
|
+
maskAllText?: boolean;
|
|
156
|
+
blockSelectors?: string[];
|
|
157
|
+
/** `false`: never record network requests / WebSockets in replays. */
|
|
158
|
+
recordNetwork?: false;
|
|
159
|
+
/** `false`: never record request/response headers, bodies or WebSocket frame data. */
|
|
160
|
+
recordNetworkPayloads?: false;
|
|
161
|
+
/** Last say over each recorded request (applied after redaction): return a modified copy, or null to drop it. */
|
|
162
|
+
maskNetworkRequest?: (request: NetworkRequest) => NetworkRequest | null;
|
|
163
|
+
};
|
|
164
|
+
onError?: (error: Error) => void;
|
|
165
|
+
/** Overrides for the project theme (colors, radius, font). */
|
|
166
|
+
theme?: Partial<ProjectTheme>;
|
|
167
|
+
/** Identify this user as soon as the client initialises. */
|
|
168
|
+
user?: GuidelyUser;
|
|
169
|
+
/** Custom fetch (tests / polyfills). */
|
|
170
|
+
fetch?: typeof fetch;
|
|
171
|
+
/** Disable the authoring-mode URL parameter detection. */
|
|
172
|
+
disableAuthoring?: boolean;
|
|
173
|
+
}
|
|
174
|
+
interface ChecklistProgressState {
|
|
175
|
+
completedTaskIds: string[];
|
|
176
|
+
completedAt?: string | null;
|
|
177
|
+
dismissedAt?: string | null;
|
|
178
|
+
}
|
|
179
|
+
/** Everything the client persists per project key (`guidely:state:<key>`). */
|
|
180
|
+
interface PersistedState {
|
|
181
|
+
guides: Record<string, GuideLocalState>;
|
|
182
|
+
checklists: Record<string, ChecklistProgressState>;
|
|
183
|
+
surveysSubmitted: string[];
|
|
184
|
+
audienceIds: string[];
|
|
185
|
+
firstSeenAt?: string;
|
|
186
|
+
}
|
|
187
|
+
interface SessionState {
|
|
188
|
+
id: string;
|
|
189
|
+
startedAt: string;
|
|
190
|
+
shownGuides: string[];
|
|
191
|
+
events: string[];
|
|
192
|
+
/** Epoch ms of the last tracked activity; the session rotates after `sessionTimeoutMinutes` of inactivity. */
|
|
193
|
+
lastActivityAt?: number;
|
|
194
|
+
/** Page views sent in this session (the first one carries the referrer). */
|
|
195
|
+
pageViews?: number;
|
|
196
|
+
/** Exceptions reported in this session (capped at 100). */
|
|
197
|
+
exceptions?: number;
|
|
198
|
+
/** Replay decision for this session: sampled in / forced (`true`), sampled out or disabled by the server (`false`). */
|
|
199
|
+
replay?: boolean;
|
|
200
|
+
}
|
|
201
|
+
interface GuideStepInfo {
|
|
202
|
+
guide: PublishedGuide;
|
|
203
|
+
stepId: string;
|
|
204
|
+
stepIndex: number;
|
|
205
|
+
}
|
|
206
|
+
interface GuidelyEvents {
|
|
207
|
+
/** Config loaded (from network or cache) and triggers evaluated. */
|
|
208
|
+
ready: (config: SdkConfigResponse) => void;
|
|
209
|
+
/** Config (re)loaded. */
|
|
210
|
+
config: (config: SdkConfigResponse) => void;
|
|
211
|
+
"guide:start": (guide: PublishedGuide) => void;
|
|
212
|
+
"guide:step": (info: GuideStepInfo) => void;
|
|
213
|
+
"guide:complete": (guide: PublishedGuide) => void;
|
|
214
|
+
"guide:dismiss": (guide: PublishedGuide) => void;
|
|
215
|
+
"guide:skip": (guide: PublishedGuide) => void;
|
|
216
|
+
"checklist:task": (info: {
|
|
217
|
+
guide: PublishedGuide;
|
|
218
|
+
task: ChecklistTask;
|
|
219
|
+
progress: ChecklistProgressState;
|
|
220
|
+
}) => void;
|
|
221
|
+
"checklist:complete": (guide: PublishedGuide) => void;
|
|
222
|
+
"survey:submit": (info: {
|
|
223
|
+
guide: PublishedGuide;
|
|
224
|
+
answers: Record<string, unknown>;
|
|
225
|
+
responseId: string;
|
|
226
|
+
}) => void;
|
|
227
|
+
"resource-center:open": () => void;
|
|
228
|
+
"messenger:open": () => void;
|
|
229
|
+
/** A Scout or teammate message arrived in the messenger. */
|
|
230
|
+
"conversation:message": (info: {
|
|
231
|
+
message: MessengerMessage;
|
|
232
|
+
conversation: MessengerConversation;
|
|
233
|
+
}) => void;
|
|
234
|
+
error: (error: Error) => void;
|
|
235
|
+
/** Every tracked event (custom and lifecycle). */
|
|
236
|
+
track: (event: SdkEvent) => void;
|
|
237
|
+
identify: (info: {
|
|
238
|
+
userId: string | null;
|
|
239
|
+
anonymousId: string;
|
|
240
|
+
}) => void;
|
|
241
|
+
authoring: (info: {
|
|
242
|
+
active: boolean;
|
|
243
|
+
}) => void;
|
|
244
|
+
}
|
|
245
|
+
type GuidelyEventName = keyof GuidelyEvents;
|
|
246
|
+
|
|
247
|
+
export { type ChecklistProgressState as C, EventQueue as E, type GuideLocalState as G, KEYS as K, 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, type KeyValueStore as f, areMutuallyExclusive as g, isFrequencyEligible as h, isExclusiveGuide as i, isWithinSchedule as j, type GuidelyOptions as k, type GuidelyEvents as l, memoryStore as m, type GuideStepInfo as n, type GuidelyEventName as o, type StorageKind as p, createStorage as q, readJson as r, writeJson as w };
|