@guidelytech/sdk-core 0.9.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +15 -4
  2. package/dist/{capture-Dqd7up2n.d.ts → capture-CgN69Lxq.d.cts} +2 -1
  3. package/dist/{capture-C3faAYb7.d.cts → capture-Csz721-A.d.ts} +2 -1
  4. package/dist/capture.cjs +4 -0
  5. package/dist/capture.cjs.map +1 -1
  6. package/dist/capture.d.cts +3 -2
  7. package/dist/capture.d.ts +3 -2
  8. package/dist/capture.js +4 -6
  9. package/dist/capture.js.map +1 -1
  10. package/dist/chunk-JUSM7J5N.js +376 -0
  11. package/dist/chunk-JUSM7J5N.js.map +1 -0
  12. package/dist/{chunk-544KSOC3.js → chunk-ZAF2KRJA.js} +142 -22
  13. package/dist/chunk-ZAF2KRJA.js.map +1 -0
  14. package/dist/headless.cjs +307 -44
  15. package/dist/headless.cjs.map +1 -1
  16. package/dist/headless.d.cts +44 -3
  17. package/dist/headless.d.ts +44 -3
  18. package/dist/headless.js +21 -9
  19. package/dist/health-sbP9JwTE.d.cts +90 -0
  20. package/dist/health-sbP9JwTE.d.ts +90 -0
  21. package/dist/{http-47tiAlOM.d.ts → http-Cb6KYYFv.d.cts} +10 -35
  22. package/dist/{http-47tiAlOM.d.cts → http-D047Ngxk.d.ts} +10 -35
  23. package/dist/index.cjs +1352 -500
  24. package/dist/index.cjs.map +1 -1
  25. package/dist/index.d.cts +81 -8
  26. package/dist/index.d.ts +81 -8
  27. package/dist/index.js +809 -211
  28. package/dist/index.js.map +1 -1
  29. package/dist/recorder.cjs +70 -9
  30. package/dist/recorder.cjs.map +1 -1
  31. package/dist/recorder.d.cts +9 -0
  32. package/dist/recorder.d.ts +9 -0
  33. package/dist/recorder.js +57 -11
  34. package/dist/recorder.js.map +1 -1
  35. package/dist/{types-DnJW_4ck.d.ts → types-BDlebXrm.d.ts} +68 -7
  36. package/dist/{types-zM5e3loY.d.cts → types-Mtlmlj5C.d.cts} +68 -7
  37. package/package.json +4 -4
  38. package/dist/chunk-544KSOC3.js.map +0 -1
  39. package/dist/chunk-AKNE7D5M.js +0 -160
  40. package/dist/chunk-AKNE7D5M.js.map +0 -1
  41. package/dist/chunk-CYAG6ZYX.js +0 -85
  42. package/dist/chunk-CYAG6ZYX.js.map +0 -1
  43. package/dist/chunk-GDF6RCHQ.js +0 -209
  44. package/dist/chunk-GDF6RCHQ.js.map +0 -1
  45. package/dist/types-DbKQPvFt.d.cts +0 -238
  46. package/dist/types-DqhNl5Am.d.ts +0 -238
  47. package/dist/types-RyeRQd9f.d.ts +0 -226
  48. package/dist/types-d4eOerxi.d.cts +0 -226
package/README.md CHANGED
@@ -122,11 +122,22 @@ through the event queue, so consent and `analytics: false` apply.
122
122
  ## Behaviour
123
123
 
124
124
  * **Config**: `GET /api/v1/sdk/config?key=…` with `If-None-Match`; the last good response is cached
125
- in storage (`guidely:config:<key>`) and used when offline. Refreshed every `configRefreshMs` and on `refresh()`.
126
- * **Events**: queued in memory + storage (max 200, oldest dropped), deduped by `eventId`, flushed in
125
+ in storage (`guidely:config:<key>`) and used when offline. With a cached config the client starts
126
+ from it if the server hasn't answered config or identify within 1.5 s, and applies the fresh
127
+ answer when it arrives. Refreshed every `configRefreshMs` and on `refresh()`.
128
+ * **Events**: queued in memory + storage (max 500, oldest dropped), deduped by `eventId`, flushed in
127
129
  batches of ≤50 every 2s, on navigation and on `visibilitychange`/`pagehide` (via `sendBeacon`,
128
- falling back to `fetch`). Failed batches retry with exponential backoff (1s → 30s, 5 attempts)
129
- and stay queued afterwards.
130
+ falling back to `fetch`). Failed batches retry with backoff (1s → 8s, then every 60s) and stay
131
+ queued, across page loads, until the server takes them. Beaconed events are only dropped while the
132
+ API is answering; otherwise they are sent again on the next load (the server drops duplicates).
133
+ * **Outages**: every API answer feeds a circuit breaker (`ServerHealth`). After 3 failures in a row
134
+ (network errors, timeouts, 429, 5xx) background traffic (events, replay uploads, config refreshes,
135
+ queued writes) waits 5 s → 5 min with jitter, or as long as `Retry-After` asks, and resumes as soon
136
+ as a request gets through or the browser fires `online`. Survey answers, guide completions and
137
+ checklist progress go through a persisted outbox (`guidely:outbox:<key>`, 50 requests, 7 days).
138
+ Replay uploads time out after 30 s; while the API is unreachable, what's left at page hide is kept
139
+ in `sessionStorage` (up to 1 MB) for the next page of the tab. Messenger reply streams abort after
140
+ 30 s without a response or 60 s of silence.
130
141
  * **Triggers**: `page_load`, `url_match`, `event`, `element_click` (delegated capture listener),
131
142
  `element_visible` (IntersectionObserver), `delay`, `after_guide`, `segment_enter`, `manual`.
132
143
  Filtered by `urlPatterns`, `trigger.urlPattern`, targeting rules (`rolloutSalt = guide.id`),
@@ -1,5 +1,6 @@
1
1
  import { CaptureStepInput } from '@guidelytech/shared';
2
- import { A as Api, b as KeyValueStore } from './http-47tiAlOM.js';
2
+ import { A as Api } from './http-Cb6KYYFv.cjs';
3
+ import { K as KeyValueStore } from './health-sbP9JwTE.cjs';
3
4
 
4
5
  /** What the SDK remembers about a capture session in this tab (`guidely:capture` in sessionStorage). */
5
6
  interface CaptureMarker {
@@ -1,5 +1,6 @@
1
1
  import { CaptureStepInput } from '@guidelytech/shared';
2
- import { A as Api, b as KeyValueStore } from './http-47tiAlOM.cjs';
2
+ import { A as Api } from './http-D047Ngxk.js';
3
+ import { K as KeyValueStore } from './health-sbP9JwTE.js';
3
4
 
4
5
  /** What the SDK remembers about a capture session in this tab (`guidely:capture` in sessionStorage). */
5
6
  interface CaptureMarker {
package/dist/capture.cjs CHANGED
@@ -76,6 +76,10 @@ var KEYS = {
76
76
  state: (key) => `guidely:state:${key}`,
77
77
  user: (key) => `guidely:user:${key}`,
78
78
  queue: (key) => `guidely:queue:${key}`,
79
+ /** Survey answers and guide/checklist progress not yet accepted by the server. */
80
+ outbox: (key) => `guidely:outbox:${key}`,
81
+ /** Agent Analytics turns not yet accepted by the server. */
82
+ agentOutbox: (key) => `guidely:agents:${key}`,
79
83
  session: "guidely:session",
80
84
  active: "guidely:active",
81
85
  authoring: "guidely:authoring",
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/capture.ts","../src/http.ts","../src/storage.ts"],"sourcesContent":["import type { CaptureSessionInfo, CaptureStepInput } from \"@guidelytech/shared\";\nimport { CAPTURE_SCREENSHOT_MAX_BYTES, randomId } from \"@guidelytech/shared/runtime\";\nimport { encodeScreenshot, screenshotDataUrl, startCapture, type CapturedStep, type CaptureSession } from \"@guidelytech/guide-runtime/capture\";\nimport type { CaptureMarker } from \"./capture-token\";\nimport { sleep, withRetry, type Api, type ApiResponse } from \"./http\";\nimport { KEYS, readJson, writeJson, type KeyValueStore } from \"./storage\";\n\n/**\n * How-to capture bridge (loaded on demand): verifies the capture session, runs the runtime's\n * recorder toolbar, uploads steps with their screenshots in capture order and tells the dashboard\n * window (`window.opener`) about progress.\n */\n\n/** Keeps the capture session alive on the server (same cadence as authoring). */\nexport const CAPTURE_HEARTBEAT_MS = 15000;\n\n/** A full page load this soon after a recorded interaction was caused by it: no navigate step. */\nexport const LOAD_AFTER_ACTION_MS = 8000;\n/** How long Done waits for queued uploads before finishing anyway. */\nexport const FINISH_DRAIN_MS = 20000;\n/** Pause before retrying uploads after every attempt failed (offline, server down). */\nexport const UPLOAD_RETRY_LATER_MS = 10000;\n\n/** Messages the SDK posts to the dashboard window (`window.opener`, target origin `dashboardOrigin`). */\nexport type CaptureOutgoingMessage =\n | { type: \"guidely:capture-ready\"; sessionId: string; url: string }\n | { type: \"guidely:capture-step\"; sessionId: string; stepCount: number }\n | { type: \"guidely:capture-finished\"; sessionId: string; howToId: string; cancelled: boolean };\n\nexport interface CaptureBridgeOptions {\n api: Api;\n sessionStore: KeyValueStore;\n marker: CaptureMarker;\n /** Comma-joined mask selectors (project setting + SDK option). */\n maskSelector: string;\n onError: (err: unknown) => void;\n onActive: (active: boolean) => void;\n}\n\ninterface QueuedStep {\n step: CaptureStepInput;\n frame: CapturedStep[\"frame\"];\n blob?: Blob | null;\n /** Screenshot saved while the page unloaded (uploaded after the next page load). */\n shot?: string;\n}\n\ninterface PendingStep {\n step: CaptureStepInput;\n shot?: string;\n}\n\n/** After a full page load: was it caused by the last recorded interaction (so no navigate step)? */\nexport function loadFollowsAction(marker: Pick<CaptureMarker, \"lastAction\" | \"lastStepAt\">, now = Date.now()): boolean {\n return !!marker.lastAction && marker.lastAction !== \"navigate\" && now - (marker.lastStepAt ?? 0) < LOAD_AFTER_ACTION_MS;\n}\n\nfunction dataUrlToBlob(url: string): Blob | null {\n try {\n const comma = url.indexOf(\",\");\n const type = /^data:([^;,]+)/.exec(url)?.[1] || \"image/jpeg\";\n const bin = atob(url.slice(comma + 1));\n const bytes = new Uint8Array(bin.length);\n for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);\n return new Blob([bytes], { type });\n } catch {\n return null;\n }\n}\n\nconst EXT: Record<string, string> = { \"image/webp\": \"webp\", \"image/jpeg\": \"jpg\", \"image/png\": \"png\" };\n\n/** Multipart body for `POST /capture/steps`: `step` (JSON) and optional `screenshot`. */\nexport function captureStepForm(step: CaptureStepInput, screenshot: Blob | null): FormData {\n const form = new FormData();\n form.append(\"step\", JSON.stringify(step));\n if (screenshot) form.append(\"screenshot\", screenshot, `step.${EXT[screenshot.type] ?? \"webp\"}`);\n return form;\n}\n\nexport class CaptureBridge {\n private info: CaptureSessionInfo | null = null;\n private session: CaptureSession | null = null;\n private heartbeat: ReturnType<typeof setInterval> | null = null;\n private retryTimer: ReturnType<typeof setTimeout> | null = null;\n private queue: QueuedStep[] = [];\n private pumping = false;\n private active = false;\n private finishing = false;\n private marker: CaptureMarker;\n private readonly onPageHide = () => this.saveForReload();\n\n constructor(private readonly opts: CaptureBridgeOptions) {\n this.marker = { ...opts.marker };\n }\n\n isActive(): boolean {\n return this.active;\n }\n\n private get token(): string {\n return this.marker.token;\n }\n\n /** Verifies the session and starts recording. Resolves once the toolbar is up (or the session was rejected). */\n async start(): Promise<void> {\n try {\n const res = await this.opts.api.get<CaptureSessionInfo>(\"/capture/session\", { params: { token: this.token } });\n if (!res.ok || !res.data) return this.reject(`[Guidely] capture session invalid or expired (${res.status})`);\n this.info = res.data;\n if (Date.parse(this.info.expiresAt) < Date.now()) return this.reject(\"[Guidely] capture session expired\");\n const pending = readJson<PendingStep[]>(this.opts.sessionStore, KEYS.capturePending, []);\n this.queue = (Array.isArray(pending) ? pending : []).filter((p) => p && p.step).map((p) => ({ step: p.step, frame: null, shot: p.shot }));\n this.active = true;\n this.opts.onActive(true);\n this.session = startCapture({\n title: this.info.howToTitle,\n stepCount: this.info.stepCount + this.queue.length,\n screenshots: this.info.screenshots,\n maskSelector: this.opts.maskSelector,\n resume: this.marker.started ? { navigate: !loadFollowsAction(this.marker), screenshots: !!this.marker.screenshots } : undefined,\n onStart: ({ screenshots }) => this.saveMarker({ started: true, screenshots }),\n onStep: (s) => this.enqueue(s),\n onFinish: () => void this.finish(false),\n onCancel: () => void this.finish(true),\n });\n this.heartbeat = setInterval(() => void this.beat(), CAPTURE_HEARTBEAT_MS);\n window.addEventListener(\"pagehide\", this.onPageHide);\n this.post({ type: \"guidely:capture-ready\", sessionId: this.info.id, url: window.location.href });\n void this.pump();\n } catch (err) {\n this.opts.onError(err);\n }\n }\n\n private reject(message: string): void {\n this.clearMarker();\n this.opts.onError(new Error(message));\n }\n\n private saveMarker(patch: Partial<CaptureMarker>): void {\n this.marker = { ...this.marker, ...patch };\n writeJson(this.opts.sessionStore, KEYS.capture, this.marker);\n }\n\n private clearMarker(): void {\n this.opts.sessionStore.remove(KEYS.capture);\n this.opts.sessionStore.remove(KEYS.capturePending);\n }\n\n /** Persists the steps still to upload (JSON only; screenshots are added on page hide). */\n private savePending(): void {\n if (!this.queue.length) this.opts.sessionStore.remove(KEYS.capturePending);\n else writeJson(this.opts.sessionStore, KEYS.capturePending, this.queue.map((q): PendingStep => (q.shot ? { step: q.step, shot: q.shot } : { step: q.step })));\n }\n\n /** The page is unloading with uploads in flight: keep their screenshots for the next page. */\n private saveForReload(): void {\n if (!this.active || !this.queue.length) return;\n for (const q of this.queue) if (!q.shot && q.frame && !(q.frame instanceof Promise)) q.shot = screenshotDataUrl(q.frame) ?? undefined;\n this.savePending();\n }\n\n private enqueue(captured: CapturedStep): void {\n if (this.finishing) return;\n // Same object: a deferred frame still updates its highlight and redaction count before upload.\n const step: CaptureStepInput = Object.assign(captured.step, { clientId: randomId(16) });\n this.queue.push({ step, frame: captured.frame });\n this.saveMarker({ lastStepAt: Date.now(), lastAction: step.action });\n this.savePending();\n void this.pump();\n }\n\n /** Uploads queued steps one at a time, in capture order. */\n private async pump(): Promise<void> {\n if (this.pumping || !this.info) return;\n this.pumping = true;\n try {\n while (this.queue.length && this.info) {\n const item = this.queue[0]!;\n if (item.frame instanceof Promise) item.frame = await item.frame.catch(() => null);\n if (item.blob === undefined) item.blob = item.shot ? dataUrlToBlob(item.shot) : item.frame ? await encodeScreenshot(item.frame, { maxBytes: CAPTURE_SCREENSHOT_MAX_BYTES }) : null;\n const res = await this.upload(item);\n if (!this.info) return;\n if (!res) {\n if (this.retryTimer) clearTimeout(this.retryTimer);\n this.retryTimer = setTimeout(() => void this.pump(), UPLOAD_RETRY_LATER_MS);\n return;\n }\n this.queue.shift();\n this.savePending();\n if (res.status === 401 || res.status === 403 || res.status === 404 || res.status === 410) {\n this.opts.onError(new Error(`[Guidely] capture session ended (${res.status})`));\n this.teardown(true);\n return;\n }\n if (res.ok && res.data) this.post({ type: \"guidely:capture-step\", sessionId: this.info.id, stepCount: res.data.stepCount });\n else if (!res.ok) this.opts.onError(new Error(`[Guidely] capture step rejected (${res.status})`));\n }\n } catch (err) {\n this.opts.onError(err);\n } finally {\n this.pumping = false;\n }\n }\n\n private async upload(item: QueuedStep): Promise<ApiResponse<{ id: string; position: number; stepCount: number }> | null> {\n const send = (shot: Blob | null) =>\n withRetry(() => this.opts.api.postForm<{ id: string; position: number; stepCount: number }>(\"/capture/steps\", captureStepForm(item.step, shot), { params: { token: this.token }, timeoutMs: 60000 }), 4, 1000);\n const res = await send(item.blob ?? null);\n // Too large or unreadable image: keep the step without it.\n if (res && item.blob && (res.status === 413 || res.status === 415)) return send(null);\n return res;\n }\n\n private async beat(): Promise<void> {\n try {\n const res = await this.opts.api.post(\"/capture/session/heartbeat\", {}, { params: { token: this.token } });\n if (res.status === 401 || res.status === 410) {\n this.opts.onError(new Error(`[Guidely] capture session ended (${res.status})`));\n this.teardown(true);\n }\n } catch {\n /* offline: the next beat retries */\n }\n }\n\n /** Done (waits for queued uploads) or Cancel (drops them); either way the dashboard is told. */\n private async finish(cancelled: boolean): Promise<void> {\n if (this.finishing || !this.info) return;\n this.finishing = true;\n const info = this.info;\n if (cancelled) {\n this.queue = [];\n this.savePending();\n } else {\n this.session?.setSaving(true);\n if (this.retryTimer) clearTimeout(this.retryTimer);\n void this.pump();\n const deadline = Date.now() + FINISH_DRAIN_MS;\n while ((this.queue.length || this.pumping) && this.info && Date.now() < deadline) await sleep(100);\n }\n let howToId = info.howToId;\n try {\n const res = await withRetry(() => this.opts.api.post<{ ok: boolean; howToId?: string }>(\"/capture/session/finish\", { cancelled }, { params: { token: this.token } }), 3, 500);\n if (res?.data?.howToId) howToId = res.data.howToId;\n } catch (err) {\n this.opts.onError(err);\n }\n this.post({ type: \"guidely:capture-finished\", sessionId: info.id, howToId, cancelled });\n this.teardown(true);\n }\n\n private post(message: CaptureOutgoingMessage): void {\n if (!this.info) return;\n try {\n window.opener?.postMessage(message, this.info.dashboardOrigin);\n } catch {\n /* opener may be gone */\n }\n }\n\n private teardown(clearMarker: boolean): void {\n if (this.heartbeat) clearInterval(this.heartbeat);\n this.heartbeat = null;\n if (this.retryTimer) clearTimeout(this.retryTimer);\n this.retryTimer = null;\n window.removeEventListener(\"pagehide\", this.onPageHide);\n this.session?.stop();\n this.session = null;\n if (clearMarker) this.clearMarker();\n this.info = null;\n this.queue = [];\n if (!this.active) return;\n this.active = false;\n this.opts.onActive(false);\n }\n\n /** Removes the toolbar and stops uploading but keeps the session marker (the SDK was destroyed; a new client resumes). */\n destroy(): void {\n this.saveForReload();\n this.teardown(false);\n }\n}\n\n/** What the on-demand capture chunk provides (`window.__guidelyCapture` in the script-tag build). */\nexport interface CaptureApi {\n createBridge(options: CaptureBridgeOptions): CaptureBridge;\n}\n\nexport const capture: CaptureApi = { createBridge: (options) => new CaptureBridge(options) };\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 // Multipart bodies set their own Content-Type (with the boundary).\n const form = typeof FormData !== \"undefined\" && options.body instanceof FormData;\n if (options.body !== undefined && !form) headers[\"Content-Type\"] = \"application/json\";\n if (options.etag) headers[\"If-None-Match\"] = options.etag;\n const ac = typeof AbortController !== \"undefined\" ? new AbortController() : null;\n const timer = ac ? setTimeout(() => ac.abort(), options.timeoutMs ?? 10000) : null;\n try {\n const res = await f(this.url(path, options.params), {\n method,\n headers,\n body: options.body === undefined ? undefined : form ? (options.body as FormData) : JSON.stringify(options.body),\n keepalive: options.keepalive,\n signal: ac?.signal,\n credentials: \"omit\",\n mode: \"cors\",\n });\n let data: T | null = null;\n if (res.status !== 304 && res.status !== 204) {\n try {\n data = (await res.json()) as T;\n } catch {\n data = null;\n }\n }\n return { status: res.status, ok: res.ok, data, etag: res.headers?.get?.(\"etag\") ?? null };\n } finally {\n if (timer) clearTimeout(timer);\n }\n }\n\n get<T>(path: string, options?: { etag?: string | null; params?: Record<string, string | undefined>; headers?: Record<string, string> }): Promise<ApiResponse<T>> {\n return this.request<T>(\"GET\", path, options);\n }\n\n post<T>(path: string, body: unknown, options?: { params?: Record<string, string | undefined>; keepalive?: boolean; headers?: Record<string, string> }): Promise<ApiResponse<T>> {\n return this.request<T>(\"POST\", path, { body, ...options });\n }\n\n /** Multipart POST (file uploads). No Content-Type header: the browser adds it with the boundary. */\n postForm<T>(path: string, form: FormData, options?: { params?: Record<string, string | undefined>; timeoutMs?: number }): Promise<ApiResponse<T>> {\n return this.request<T>(\"POST\", path, { body: form, ...options });\n }\n\n /**\n * POST that may answer with server-sent events. Each event is passed to `onEvent`; a JSON answer is\n * returned instead. 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/**\n * Sends with bounded retries: network errors, 429 and 5xx are retried with backoff; success and\n * other 4xx answers are returned as they are. Null when every attempt failed.\n */\nexport async function withRetry<T>(send: () => Promise<ApiResponse<T>>, attempts = 3, baseMs = 500): Promise<ApiResponse<T> | null> {\n for (let i = 0; i < attempts; i++) {\n try {\n const res = await send();\n if (res.ok || (res.status >= 400 && res.status < 500 && res.status !== 429)) return res;\n } catch {\n /* network error → retry */\n }\n if (i < attempts - 1) await sleep(backoffMs(i, baseMs, 8000));\n }\n return null;\n}\n\n/** Fire-and-forget POST with bounded retries (used for state sync; never blocks UI). */\nexport function postWithRetry<T>(api: Api, path: string, body: unknown, attempts = 3, baseMs = 500): Promise<ApiResponse<T> | null> {\n return withRetry(() => api.post<T>(path, body), attempts, baseMs);\n}\n\n/** Parses a `text/event-stream` body (`event:` + single-line JSON `data:`; comment lines ignored). */\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","export type StorageKind = \"local\" | \"session\" | \"memory\";\n\n/** Minimal synchronous key/value store; never throws (private mode, quota, SSR). */\nexport interface KeyValueStore {\n get(key: string): string | null;\n set(key: string, value: string): void;\n remove(key: string): void;\n}\n\nexport function memoryStore(): KeyValueStore {\n const map = new Map<string, string>();\n return { get: (k) => map.get(k) ?? null, set: (k, v) => void map.set(k, v), remove: (k) => void map.delete(k) };\n}\n\nfunction webStore(area: \"localStorage\" | \"sessionStorage\"): KeyValueStore | null {\n try {\n if (typeof window === \"undefined\") return null;\n const s = window[area];\n if (!s) return null;\n const probe = \"__guidely_probe__\";\n s.setItem(probe, \"1\");\n s.removeItem(probe);\n return {\n get: (k) => {\n try {\n return s.getItem(k);\n } catch {\n return null;\n }\n },\n set: (k, v) => {\n try {\n s.setItem(k, v);\n } catch {\n /* quota / private mode */\n }\n },\n remove: (k) => {\n try {\n s.removeItem(k);\n } catch {\n /* ignore */\n }\n },\n };\n } catch {\n return null;\n }\n}\n\n/** Creates a store of the requested kind, degrading to memory when unavailable. */\nexport function createStorage(kind: StorageKind = \"local\"): KeyValueStore {\n if (kind === \"memory\") return memoryStore();\n return webStore(kind === \"local\" ? \"localStorage\" : \"sessionStorage\") ?? memoryStore();\n}\n\nexport function readJson<T>(store: KeyValueStore, key: string, fallback: T): T {\n const raw = store.get(key);\n if (raw == null) return fallback;\n try {\n return JSON.parse(raw) as T;\n } catch {\n return fallback;\n }\n}\n\nexport function writeJson(store: KeyValueStore, key: string, value: unknown): void {\n try {\n store.set(key, JSON.stringify(value));\n } catch {\n /* ignore */\n }\n}\n\n/** Storage keys used by the SDK (documented for integrators). */\nexport const KEYS = {\n anon: \"guidely:anon\",\n seen: \"guidely:seen\",\n config: (key: string) => `guidely:config:${key}`,\n etag: (key: string) => `guidely:etag:${key}`,\n state: (key: string) => `guidely:state:${key}`,\n user: (key: string) => `guidely:user:${key}`,\n queue: (key: string) => `guidely:queue:${key}`,\n session: \"guidely:session\",\n active: \"guidely:active\",\n authoring: \"guidely:authoring\",\n /** How-to capture session marker ({ token, … }) and its not-yet-uploaded steps. */\n capture: \"guidely:capture\",\n capturePending: \"guidely:capture:pending\",\n /** Messenger conversations this browser can read: [{ id, token }]. */\n conversations: (key: string) => `guidely:conversations:${key}`,\n /** Announcement ids opened in the messenger's News tab. */\n newsSeen: (key: string) => `guidely:news:${key}`,\n} as const;\n"],"mappings":"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","names":["import_runtime"]}
1
+ {"version":3,"sources":["../src/capture.ts","../src/http.ts","../src/storage.ts"],"sourcesContent":["import type { CaptureSessionInfo, CaptureStepInput } from \"@guidelytech/shared\";\nimport { CAPTURE_SCREENSHOT_MAX_BYTES, randomId } from \"@guidelytech/shared/runtime\";\nimport { encodeScreenshot, screenshotDataUrl, startCapture, type CapturedStep, type CaptureSession } from \"@guidelytech/guide-runtime/capture\";\nimport type { CaptureMarker } from \"./capture-token\";\nimport { sleep, withRetry, type Api, type ApiResponse } from \"./http\";\nimport { KEYS, readJson, writeJson, type KeyValueStore } from \"./storage\";\n\n/**\n * How-to capture bridge (loaded on demand): verifies the capture session, runs the runtime's\n * recorder toolbar, uploads steps with their screenshots in capture order and tells the dashboard\n * window (`window.opener`) about progress.\n */\n\n/** Keeps the capture session alive on the server (same cadence as authoring). */\nexport const CAPTURE_HEARTBEAT_MS = 15000;\n\n/** A full page load this soon after a recorded interaction was caused by it: no navigate step. */\nexport const LOAD_AFTER_ACTION_MS = 8000;\n/** How long Done waits for queued uploads before finishing anyway. */\nexport const FINISH_DRAIN_MS = 20000;\n/** Pause before retrying uploads after every attempt failed (offline, server down). */\nexport const UPLOAD_RETRY_LATER_MS = 10000;\n\n/** Messages the SDK posts to the dashboard window (`window.opener`, target origin `dashboardOrigin`). */\nexport type CaptureOutgoingMessage =\n | { type: \"guidely:capture-ready\"; sessionId: string; url: string }\n | { type: \"guidely:capture-step\"; sessionId: string; stepCount: number }\n | { type: \"guidely:capture-finished\"; sessionId: string; howToId: string; cancelled: boolean };\n\nexport interface CaptureBridgeOptions {\n api: Api;\n sessionStore: KeyValueStore;\n marker: CaptureMarker;\n /** Comma-joined mask selectors (project setting + SDK option). */\n maskSelector: string;\n onError: (err: unknown) => void;\n onActive: (active: boolean) => void;\n}\n\ninterface QueuedStep {\n step: CaptureStepInput;\n frame: CapturedStep[\"frame\"];\n blob?: Blob | null;\n /** Screenshot saved while the page unloaded (uploaded after the next page load). */\n shot?: string;\n}\n\ninterface PendingStep {\n step: CaptureStepInput;\n shot?: string;\n}\n\n/** After a full page load: was it caused by the last recorded interaction (so no navigate step)? */\nexport function loadFollowsAction(marker: Pick<CaptureMarker, \"lastAction\" | \"lastStepAt\">, now = Date.now()): boolean {\n return !!marker.lastAction && marker.lastAction !== \"navigate\" && now - (marker.lastStepAt ?? 0) < LOAD_AFTER_ACTION_MS;\n}\n\nfunction dataUrlToBlob(url: string): Blob | null {\n try {\n const comma = url.indexOf(\",\");\n const type = /^data:([^;,]+)/.exec(url)?.[1] || \"image/jpeg\";\n const bin = atob(url.slice(comma + 1));\n const bytes = new Uint8Array(bin.length);\n for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);\n return new Blob([bytes], { type });\n } catch {\n return null;\n }\n}\n\nconst EXT: Record<string, string> = { \"image/webp\": \"webp\", \"image/jpeg\": \"jpg\", \"image/png\": \"png\" };\n\n/** Multipart body for `POST /capture/steps`: `step` (JSON) and optional `screenshot`. */\nexport function captureStepForm(step: CaptureStepInput, screenshot: Blob | null): FormData {\n const form = new FormData();\n form.append(\"step\", JSON.stringify(step));\n if (screenshot) form.append(\"screenshot\", screenshot, `step.${EXT[screenshot.type] ?? \"webp\"}`);\n return form;\n}\n\nexport class CaptureBridge {\n private info: CaptureSessionInfo | null = null;\n private session: CaptureSession | null = null;\n private heartbeat: ReturnType<typeof setInterval> | null = null;\n private retryTimer: ReturnType<typeof setTimeout> | null = null;\n private queue: QueuedStep[] = [];\n private pumping = false;\n private active = false;\n private finishing = false;\n private marker: CaptureMarker;\n private readonly onPageHide = () => this.saveForReload();\n\n constructor(private readonly opts: CaptureBridgeOptions) {\n this.marker = { ...opts.marker };\n }\n\n isActive(): boolean {\n return this.active;\n }\n\n private get token(): string {\n return this.marker.token;\n }\n\n /** Verifies the session and starts recording. Resolves once the toolbar is up (or the session was rejected). */\n async start(): Promise<void> {\n try {\n const res = await this.opts.api.get<CaptureSessionInfo>(\"/capture/session\", { params: { token: this.token } });\n if (!res.ok || !res.data) return this.reject(`[Guidely] capture session invalid or expired (${res.status})`);\n this.info = res.data;\n if (Date.parse(this.info.expiresAt) < Date.now()) return this.reject(\"[Guidely] capture session expired\");\n const pending = readJson<PendingStep[]>(this.opts.sessionStore, KEYS.capturePending, []);\n this.queue = (Array.isArray(pending) ? pending : []).filter((p) => p && p.step).map((p) => ({ step: p.step, frame: null, shot: p.shot }));\n this.active = true;\n this.opts.onActive(true);\n this.session = startCapture({\n title: this.info.howToTitle,\n stepCount: this.info.stepCount + this.queue.length,\n screenshots: this.info.screenshots,\n maskSelector: this.opts.maskSelector,\n resume: this.marker.started ? { navigate: !loadFollowsAction(this.marker), screenshots: !!this.marker.screenshots } : undefined,\n onStart: ({ screenshots }) => this.saveMarker({ started: true, screenshots }),\n onStep: (s) => this.enqueue(s),\n onFinish: () => void this.finish(false),\n onCancel: () => void this.finish(true),\n });\n this.heartbeat = setInterval(() => void this.beat(), CAPTURE_HEARTBEAT_MS);\n window.addEventListener(\"pagehide\", this.onPageHide);\n this.post({ type: \"guidely:capture-ready\", sessionId: this.info.id, url: window.location.href });\n void this.pump();\n } catch (err) {\n this.opts.onError(err);\n }\n }\n\n private reject(message: string): void {\n this.clearMarker();\n this.opts.onError(new Error(message));\n }\n\n private saveMarker(patch: Partial<CaptureMarker>): void {\n this.marker = { ...this.marker, ...patch };\n writeJson(this.opts.sessionStore, KEYS.capture, this.marker);\n }\n\n private clearMarker(): void {\n this.opts.sessionStore.remove(KEYS.capture);\n this.opts.sessionStore.remove(KEYS.capturePending);\n }\n\n /** Persists the steps still to upload (JSON only; screenshots are added on page hide). */\n private savePending(): void {\n if (!this.queue.length) this.opts.sessionStore.remove(KEYS.capturePending);\n else writeJson(this.opts.sessionStore, KEYS.capturePending, this.queue.map((q): PendingStep => (q.shot ? { step: q.step, shot: q.shot } : { step: q.step })));\n }\n\n /** The page is unloading with uploads in flight: keep their screenshots for the next page. */\n private saveForReload(): void {\n if (!this.active || !this.queue.length) return;\n for (const q of this.queue) if (!q.shot && q.frame && !(q.frame instanceof Promise)) q.shot = screenshotDataUrl(q.frame) ?? undefined;\n this.savePending();\n }\n\n private enqueue(captured: CapturedStep): void {\n if (this.finishing) return;\n // Same object: a deferred frame still updates its highlight and redaction count before upload.\n const step: CaptureStepInput = Object.assign(captured.step, { clientId: randomId(16) });\n this.queue.push({ step, frame: captured.frame });\n this.saveMarker({ lastStepAt: Date.now(), lastAction: step.action });\n this.savePending();\n void this.pump();\n }\n\n /** Uploads queued steps one at a time, in capture order. */\n private async pump(): Promise<void> {\n if (this.pumping || !this.info) return;\n this.pumping = true;\n try {\n while (this.queue.length && this.info) {\n const item = this.queue[0]!;\n if (item.frame instanceof Promise) item.frame = await item.frame.catch(() => null);\n if (item.blob === undefined) item.blob = item.shot ? dataUrlToBlob(item.shot) : item.frame ? await encodeScreenshot(item.frame, { maxBytes: CAPTURE_SCREENSHOT_MAX_BYTES }) : null;\n const res = await this.upload(item);\n if (!this.info) return;\n if (!res) {\n if (this.retryTimer) clearTimeout(this.retryTimer);\n this.retryTimer = setTimeout(() => void this.pump(), UPLOAD_RETRY_LATER_MS);\n return;\n }\n this.queue.shift();\n this.savePending();\n if (res.status === 401 || res.status === 403 || res.status === 404 || res.status === 410) {\n this.opts.onError(new Error(`[Guidely] capture session ended (${res.status})`));\n this.teardown(true);\n return;\n }\n if (res.ok && res.data) this.post({ type: \"guidely:capture-step\", sessionId: this.info.id, stepCount: res.data.stepCount });\n else if (!res.ok) this.opts.onError(new Error(`[Guidely] capture step rejected (${res.status})`));\n }\n } catch (err) {\n this.opts.onError(err);\n } finally {\n this.pumping = false;\n }\n }\n\n private async upload(item: QueuedStep): Promise<ApiResponse<{ id: string; position: number; stepCount: number }> | null> {\n const send = (shot: Blob | null) =>\n withRetry(() => this.opts.api.postForm<{ id: string; position: number; stepCount: number }>(\"/capture/steps\", captureStepForm(item.step, shot), { params: { token: this.token }, timeoutMs: 60000 }), 4, 1000);\n const res = await send(item.blob ?? null);\n // Too large or unreadable image: keep the step without it.\n if (res && item.blob && (res.status === 413 || res.status === 415)) return send(null);\n return res;\n }\n\n private async beat(): Promise<void> {\n try {\n const res = await this.opts.api.post(\"/capture/session/heartbeat\", {}, { params: { token: this.token } });\n if (res.status === 401 || res.status === 410) {\n this.opts.onError(new Error(`[Guidely] capture session ended (${res.status})`));\n this.teardown(true);\n }\n } catch {\n /* offline: the next beat retries */\n }\n }\n\n /** Done (waits for queued uploads) or Cancel (drops them); either way the dashboard is told. */\n private async finish(cancelled: boolean): Promise<void> {\n if (this.finishing || !this.info) return;\n this.finishing = true;\n const info = this.info;\n if (cancelled) {\n this.queue = [];\n this.savePending();\n } else {\n this.session?.setSaving(true);\n if (this.retryTimer) clearTimeout(this.retryTimer);\n void this.pump();\n const deadline = Date.now() + FINISH_DRAIN_MS;\n while ((this.queue.length || this.pumping) && this.info && Date.now() < deadline) await sleep(100);\n }\n let howToId = info.howToId;\n try {\n const res = await withRetry(() => this.opts.api.post<{ ok: boolean; howToId?: string }>(\"/capture/session/finish\", { cancelled }, { params: { token: this.token } }), 3, 500);\n if (res?.data?.howToId) howToId = res.data.howToId;\n } catch (err) {\n this.opts.onError(err);\n }\n this.post({ type: \"guidely:capture-finished\", sessionId: info.id, howToId, cancelled });\n this.teardown(true);\n }\n\n private post(message: CaptureOutgoingMessage): void {\n if (!this.info) return;\n try {\n window.opener?.postMessage(message, this.info.dashboardOrigin);\n } catch {\n /* opener may be gone */\n }\n }\n\n private teardown(clearMarker: boolean): void {\n if (this.heartbeat) clearInterval(this.heartbeat);\n this.heartbeat = null;\n if (this.retryTimer) clearTimeout(this.retryTimer);\n this.retryTimer = null;\n window.removeEventListener(\"pagehide\", this.onPageHide);\n this.session?.stop();\n this.session = null;\n if (clearMarker) this.clearMarker();\n this.info = null;\n this.queue = [];\n if (!this.active) return;\n this.active = false;\n this.opts.onActive(false);\n }\n\n /** Removes the toolbar and stops uploading but keeps the session marker (the SDK was destroyed; a new client resumes). */\n destroy(): void {\n this.saveForReload();\n this.teardown(false);\n }\n}\n\n/** What the on-demand capture chunk provides (`window.__guidelyCapture` in the script-tag build). */\nexport interface CaptureApi {\n createBridge(options: CaptureBridgeOptions): CaptureBridge;\n}\n\nexport const capture: CaptureApi = { createBridge: (options) => new CaptureBridge(options) };\n","import { isServerFailure, parseRetryAfter, ServerHealth } from \"./health\";\n\nexport interface ApiResponse<T> {\n status: number;\n ok: boolean;\n data: T | null;\n etag: string | null;\n}\n\nexport type FetchLike = typeof fetch;\n\n/** Reply streams: the response must start within this long, and then never go quiet longer than the idle limit (the server sends a keepalive every 15 s). */\nexport const STREAM_RESPONSE_TIMEOUT_MS = 30_000;\nexport const STREAM_IDLE_TIMEOUT_MS = 60_000;\n\n/** Thin HTTP client for the public SDK API. Adds the public key as query + header. */\nexport class Api {\n /** Reachability of the API, fed by every answer (see {@link ServerHealth}). */\n readonly health: ServerHealth;\n\n constructor(\n private readonly baseUrl: string,\n private readonly key: string,\n private readonly fetchImpl?: FetchLike,\n health?: ServerHealth,\n ) {\n this.health = health ?? new ServerHealth();\n }\n\n /** Feeds an answer (or a failed request, `null`) into {@link health}. */\n private observe(res: Response | null): void {\n if (!res) this.health.failure();\n else if (isServerFailure(res.status)) this.health.failure(parseRetryAfter(res.headers?.get?.(\"retry-after\")));\n else this.health.success();\n }\n\n url(path: string, params: Record<string, string | undefined> = {}): string {\n // Built by hand rather than with URL/searchParams, which React Native does not implement.\n const query = Object.entries({ key: this.key, ...params })\n .filter((e): e is [string, string] => e[1] != null)\n .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)\n .join(\"&\");\n return `${this.baseUrl.replace(/\\/$/, \"\")}/api/v1/sdk${path}?${query}`;\n }\n\n private get fetcher(): FetchLike | null {\n if (this.fetchImpl) return this.fetchImpl;\n return typeof fetch === \"function\" ? fetch.bind(globalThis) : null;\n }\n\n async request<T>(method: \"GET\" | \"POST\", path: string, options: { body?: unknown; etag?: string | null; params?: Record<string, string | undefined>; keepalive?: boolean; timeoutMs?: number; headers?: Record<string, string> } = {}): Promise<ApiResponse<T>> {\n const f = this.fetcher;\n if (!f) throw new Error(\"fetch is not available\");\n const headers: Record<string, string> = { ...options.headers, \"X-Guidely-Key\": this.key, Accept: \"application/json\" };\n // Multipart bodies set their own Content-Type (with the boundary).\n const form = typeof FormData !== \"undefined\" && options.body instanceof FormData;\n if (options.body !== undefined && !form) headers[\"Content-Type\"] = \"application/json\";\n if (options.etag) headers[\"If-None-Match\"] = options.etag;\n const ac = typeof AbortController !== \"undefined\" ? new AbortController() : null;\n const timer = ac ? setTimeout(() => ac.abort(), options.timeoutMs ?? 10000) : null;\n try {\n let res: Response;\n try {\n res = await f(this.url(path, options.params), {\n method,\n headers,\n body: options.body === undefined ? undefined : form ? (options.body as FormData) : JSON.stringify(options.body),\n keepalive: options.keepalive,\n signal: ac?.signal,\n credentials: \"omit\",\n mode: \"cors\",\n });\n } catch (err) {\n this.observe(null);\n throw err;\n }\n this.observe(res);\n let data: T | null = null;\n if (res.status !== 304 && res.status !== 204) {\n try {\n data = (await res.json()) as T;\n } catch {\n data = null;\n }\n }\n return { status: res.status, ok: res.ok, data, etag: res.headers?.get?.(\"etag\") ?? null };\n } finally {\n if (timer) clearTimeout(timer);\n }\n }\n\n get<T>(path: string, options?: { etag?: string | null; params?: Record<string, string | undefined>; headers?: Record<string, string> }): Promise<ApiResponse<T>> {\n return this.request<T>(\"GET\", path, options);\n }\n\n post<T>(path: string, body: unknown, options?: { params?: Record<string, string | undefined>; keepalive?: boolean; headers?: Record<string, string> }): Promise<ApiResponse<T>> {\n return this.request<T>(\"POST\", path, { body, ...options });\n }\n\n /** Multipart POST (file uploads). No Content-Type header: the browser adds it with the boundary. */\n postForm<T>(path: string, form: FormData, options?: { params?: Record<string, string | undefined>; timeoutMs?: number }): Promise<ApiResponse<T>> {\n return this.request<T>(\"POST\", path, { body: form, ...options });\n }\n\n /**\n * POST that may answer with server-sent events. Each event is passed to `onEvent`; a JSON answer is\n * returned instead. Scout replies can take a while, so there is no overall timeout: the response\n * must start within 30 s and then never go quiet for more than 60 s (the server sends keepalives),\n * otherwise the request is aborted and rejects.\n */\n async stream<T>(path: string, body: unknown, onEvent: (event: string, data: unknown) => void, headers: Record<string, string> = {}): Promise<ApiResponse<T>> {\n const f = this.fetcher;\n if (!f) throw new Error(\"fetch is not available\");\n const ac = typeof AbortController !== \"undefined\" ? new AbortController() : null;\n let timer: ReturnType<typeof setTimeout> | null = null;\n const arm = (ms: number) => {\n if (!ac) return;\n if (timer) clearTimeout(timer);\n timer = setTimeout(() => ac.abort(), ms);\n };\n arm(STREAM_RESPONSE_TIMEOUT_MS);\n try {\n let res: Response;\n try {\n res = await f(this.url(path), {\n method: \"POST\",\n headers: { ...headers, \"X-Guidely-Key\": this.key, Accept: \"text/event-stream, application/json\", \"Content-Type\": \"application/json\" },\n body: JSON.stringify(body),\n signal: ac?.signal,\n credentials: \"omit\",\n mode: \"cors\",\n });\n } catch (err) {\n this.observe(null);\n throw err;\n }\n this.observe(res);\n const type = res.headers?.get?.(\"content-type\") ?? \"\";\n if (!type.includes(\"text/event-stream\") || !res.body) {\n let data: T | null = null;\n try {\n data = (await res.json()) as T;\n } catch {\n data = null;\n }\n return { status: res.status, ok: res.ok, data, etag: null };\n }\n arm(STREAM_IDLE_TIMEOUT_MS);\n await readEventStream(res.body, onEvent, () => arm(STREAM_IDLE_TIMEOUT_MS));\n return { status: res.status, ok: res.ok, data: null, etag: null };\n } finally {\n if (timer) clearTimeout(timer);\n }\n }\n\n /** `navigator.sendBeacon` when available (page unload); returns false when it could not be queued. */\n beacon(path: string, body: unknown): boolean {\n try {\n if (typeof navigator === \"undefined\" || typeof navigator.sendBeacon !== \"function\") return false;\n // text/plain is a CORS-safelisted type: cross-origin beacons need no preflight (which\n // browsers drop during unload). The API parses the body regardless of content type.\n const blob = new Blob([JSON.stringify(body)], { type: \"text/plain;charset=UTF-8\" });\n return navigator.sendBeacon(this.url(path), blob);\n } catch {\n return false;\n }\n }\n}\n\nexport const sleep = (ms: number) => new Promise<void>((r) => setTimeout(r, ms));\n\n/** Exponential backoff helper: 1s → 2s → 4s … capped. */\nexport function backoffMs(attempt: number, baseMs = 1000, maxMs = 30000): number {\n return Math.min(maxMs, baseMs * 2 ** Math.max(0, attempt));\n}\n\n/**\n * Sends with bounded retries: network errors, 429 and 5xx are retried with backoff; success and\n * other 4xx answers are returned as they are. Null when every attempt failed.\n */\nexport async function withRetry<T>(send: () => Promise<ApiResponse<T>>, attempts = 3, baseMs = 500): Promise<ApiResponse<T> | null> {\n for (let i = 0; i < attempts; i++) {\n try {\n const res = await send();\n if (res.ok || (res.status >= 400 && res.status < 500 && res.status !== 429)) return res;\n } catch {\n /* network error → retry */\n }\n if (i < attempts - 1) await sleep(backoffMs(i, baseMs, 8000));\n }\n return null;\n}\n\n/** Fire-and-forget POST with bounded retries (used for state sync; never blocks UI). */\nexport function postWithRetry<T>(api: Api, path: string, body: unknown, attempts = 3, baseMs = 500): Promise<ApiResponse<T> | null> {\n return withRetry(() => api.post<T>(path, body), attempts, baseMs);\n}\n\n/** Parses a `text/event-stream` body (`event:` + single-line JSON `data:`; comment lines ignored). `onChunk` runs whenever bytes arrive. */\nexport async function readEventStream(body: ReadableStream<Uint8Array>, onEvent: (event: string, data: unknown) => void, onChunk?: () => void): Promise<void> {\n const reader = body.getReader();\n const decoder = new TextDecoder();\n let buffer = \"\";\n const dispatch = (raw: string) => {\n let event = \"message\";\n const data: string[] = [];\n for (const line of raw.split(\"\\n\")) {\n if (!line || line.startsWith(\":\")) continue;\n const i = line.indexOf(\":\");\n const field = i < 0 ? line : line.slice(0, i);\n const value = i < 0 ? \"\" : line.slice(i + 1).replace(/^ /, \"\");\n if (field === \"event\") event = value;\n else if (field === \"data\") data.push(value);\n }\n if (!data.length) return;\n try {\n onEvent(event, JSON.parse(data.join(\"\\n\")));\n } catch {\n /* ignore malformed events */\n }\n };\n for (;;) {\n const { done, value } = await reader.read();\n if (value) onChunk?.();\n if (value) buffer += decoder.decode(value, { stream: true }).replace(/\\r\\n?/g, \"\\n\");\n let sep: number;\n while ((sep = buffer.indexOf(\"\\n\\n\")) >= 0) {\n dispatch(buffer.slice(0, sep));\n buffer = buffer.slice(sep + 2);\n }\n if (done) break;\n }\n if (buffer.trim()) dispatch(buffer);\n}\n","export type StorageKind = \"local\" | \"session\" | \"memory\";\n\n/** Minimal synchronous key/value store; never throws (private mode, quota, SSR). */\nexport interface KeyValueStore {\n get(key: string): string | null;\n set(key: string, value: string): void;\n remove(key: string): void;\n}\n\nexport function memoryStore(): KeyValueStore {\n const map = new Map<string, string>();\n return { get: (k) => map.get(k) ?? null, set: (k, v) => void map.set(k, v), remove: (k) => void map.delete(k) };\n}\n\nfunction webStore(area: \"localStorage\" | \"sessionStorage\"): KeyValueStore | null {\n try {\n if (typeof window === \"undefined\") return null;\n const s = window[area];\n if (!s) return null;\n const probe = \"__guidely_probe__\";\n s.setItem(probe, \"1\");\n s.removeItem(probe);\n return {\n get: (k) => {\n try {\n return s.getItem(k);\n } catch {\n return null;\n }\n },\n set: (k, v) => {\n try {\n s.setItem(k, v);\n } catch {\n /* quota / private mode */\n }\n },\n remove: (k) => {\n try {\n s.removeItem(k);\n } catch {\n /* ignore */\n }\n },\n };\n } catch {\n return null;\n }\n}\n\n/** Creates a store of the requested kind, degrading to memory when unavailable. */\nexport function createStorage(kind: StorageKind = \"local\"): KeyValueStore {\n if (kind === \"memory\") return memoryStore();\n return webStore(kind === \"local\" ? \"localStorage\" : \"sessionStorage\") ?? memoryStore();\n}\n\nexport function readJson<T>(store: KeyValueStore, key: string, fallback: T): T {\n const raw = store.get(key);\n if (raw == null) return fallback;\n try {\n return JSON.parse(raw) as T;\n } catch {\n return fallback;\n }\n}\n\nexport function writeJson(store: KeyValueStore, key: string, value: unknown): void {\n try {\n store.set(key, JSON.stringify(value));\n } catch {\n /* ignore */\n }\n}\n\n/** Storage keys used by the SDK (documented for integrators). */\nexport const KEYS = {\n anon: \"guidely:anon\",\n seen: \"guidely:seen\",\n config: (key: string) => `guidely:config:${key}`,\n etag: (key: string) => `guidely:etag:${key}`,\n state: (key: string) => `guidely:state:${key}`,\n user: (key: string) => `guidely:user:${key}`,\n queue: (key: string) => `guidely:queue:${key}`,\n /** Survey answers and guide/checklist progress not yet accepted by the server. */\n outbox: (key: string) => `guidely:outbox:${key}`,\n /** Agent Analytics turns not yet accepted by the server. */\n agentOutbox: (key: string) => `guidely:agents:${key}`,\n session: \"guidely:session\",\n active: \"guidely:active\",\n authoring: \"guidely:authoring\",\n /** How-to capture session marker ({ token, … }) and its not-yet-uploaded steps. */\n capture: \"guidely:capture\",\n capturePending: \"guidely:capture:pending\",\n /** Messenger conversations this browser can read: [{ id, token }]. */\n conversations: (key: string) => `guidely:conversations:${key}`,\n /** Announcement ids opened in the messenger's News tab. */\n newsSeen: (key: string) => `guidely:news:${key}`,\n} as const;\n"],"mappings":"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","names":["import_runtime"]}
@@ -1,3 +1,4 @@
1
1
  import '@guidelytech/shared';
2
- export { b as CAPTURE_HEARTBEAT_MS, c as CaptureApi, d as CaptureBridge, e as CaptureBridgeOptions, a as CaptureOutgoingMessage, F as FINISH_DRAIN_MS, L as LOAD_AFTER_ACTION_MS, U as UPLOAD_RETRY_LATER_MS, f as capture, g as captureStepForm, l as loadFollowsAction } from './capture-C3faAYb7.cjs';
3
- import './http-47tiAlOM.cjs';
2
+ export { b as CAPTURE_HEARTBEAT_MS, c as CaptureApi, d as CaptureBridge, e as CaptureBridgeOptions, a as CaptureOutgoingMessage, F as FINISH_DRAIN_MS, L as LOAD_AFTER_ACTION_MS, U as UPLOAD_RETRY_LATER_MS, f as capture, g as captureStepForm, l as loadFollowsAction } from './capture-CgN69Lxq.cjs';
3
+ import './http-Cb6KYYFv.cjs';
4
+ import './health-sbP9JwTE.cjs';
package/dist/capture.d.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  import '@guidelytech/shared';
2
- export { b as CAPTURE_HEARTBEAT_MS, c as CaptureApi, d as CaptureBridge, e as CaptureBridgeOptions, a as CaptureOutgoingMessage, F as FINISH_DRAIN_MS, L as LOAD_AFTER_ACTION_MS, U as UPLOAD_RETRY_LATER_MS, f as capture, g as captureStepForm, l as loadFollowsAction } from './capture-Dqd7up2n.js';
3
- import './http-47tiAlOM.js';
2
+ export { b as CAPTURE_HEARTBEAT_MS, c as CaptureApi, d as CaptureBridge, e as CaptureBridgeOptions, a as CaptureOutgoingMessage, F as FINISH_DRAIN_MS, L as LOAD_AFTER_ACTION_MS, U as UPLOAD_RETRY_LATER_MS, f as capture, g as captureStepForm, l as loadFollowsAction } from './capture-Csz721-A.js';
3
+ import './http-D047Ngxk.js';
4
+ import './health-sbP9JwTE.js';
package/dist/capture.js CHANGED
@@ -1,13 +1,11 @@
1
1
  import {
2
2
  KEYS,
3
- readJson,
4
- writeJson
5
- } from "./chunk-CYAG6ZYX.js";
6
- import {
7
3
  __publicField,
4
+ readJson,
8
5
  sleep,
9
- withRetry
10
- } from "./chunk-AKNE7D5M.js";
6
+ withRetry,
7
+ writeJson
8
+ } from "./chunk-JUSM7J5N.js";
11
9
 
12
10
  // src/capture.ts
13
11
  import { CAPTURE_SCREENSHOT_MAX_BYTES, randomId } from "@guidelytech/shared/runtime";
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/capture.ts"],"sourcesContent":["import type { CaptureSessionInfo, CaptureStepInput } from \"@guidelytech/shared\";\nimport { CAPTURE_SCREENSHOT_MAX_BYTES, randomId } from \"@guidelytech/shared/runtime\";\nimport { encodeScreenshot, screenshotDataUrl, startCapture, type CapturedStep, type CaptureSession } from \"@guidelytech/guide-runtime/capture\";\nimport type { CaptureMarker } from \"./capture-token\";\nimport { sleep, withRetry, type Api, type ApiResponse } from \"./http\";\nimport { KEYS, readJson, writeJson, type KeyValueStore } from \"./storage\";\n\n/**\n * How-to capture bridge (loaded on demand): verifies the capture session, runs the runtime's\n * recorder toolbar, uploads steps with their screenshots in capture order and tells the dashboard\n * window (`window.opener`) about progress.\n */\n\n/** Keeps the capture session alive on the server (same cadence as authoring). */\nexport const CAPTURE_HEARTBEAT_MS = 15000;\n\n/** A full page load this soon after a recorded interaction was caused by it: no navigate step. */\nexport const LOAD_AFTER_ACTION_MS = 8000;\n/** How long Done waits for queued uploads before finishing anyway. */\nexport const FINISH_DRAIN_MS = 20000;\n/** Pause before retrying uploads after every attempt failed (offline, server down). */\nexport const UPLOAD_RETRY_LATER_MS = 10000;\n\n/** Messages the SDK posts to the dashboard window (`window.opener`, target origin `dashboardOrigin`). */\nexport type CaptureOutgoingMessage =\n | { type: \"guidely:capture-ready\"; sessionId: string; url: string }\n | { type: \"guidely:capture-step\"; sessionId: string; stepCount: number }\n | { type: \"guidely:capture-finished\"; sessionId: string; howToId: string; cancelled: boolean };\n\nexport interface CaptureBridgeOptions {\n api: Api;\n sessionStore: KeyValueStore;\n marker: CaptureMarker;\n /** Comma-joined mask selectors (project setting + SDK option). */\n maskSelector: string;\n onError: (err: unknown) => void;\n onActive: (active: boolean) => void;\n}\n\ninterface QueuedStep {\n step: CaptureStepInput;\n frame: CapturedStep[\"frame\"];\n blob?: Blob | null;\n /** Screenshot saved while the page unloaded (uploaded after the next page load). */\n shot?: string;\n}\n\ninterface PendingStep {\n step: CaptureStepInput;\n shot?: string;\n}\n\n/** After a full page load: was it caused by the last recorded interaction (so no navigate step)? */\nexport function loadFollowsAction(marker: Pick<CaptureMarker, \"lastAction\" | \"lastStepAt\">, now = Date.now()): boolean {\n return !!marker.lastAction && marker.lastAction !== \"navigate\" && now - (marker.lastStepAt ?? 0) < LOAD_AFTER_ACTION_MS;\n}\n\nfunction dataUrlToBlob(url: string): Blob | null {\n try {\n const comma = url.indexOf(\",\");\n const type = /^data:([^;,]+)/.exec(url)?.[1] || \"image/jpeg\";\n const bin = atob(url.slice(comma + 1));\n const bytes = new Uint8Array(bin.length);\n for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);\n return new Blob([bytes], { type });\n } catch {\n return null;\n }\n}\n\nconst EXT: Record<string, string> = { \"image/webp\": \"webp\", \"image/jpeg\": \"jpg\", \"image/png\": \"png\" };\n\n/** Multipart body for `POST /capture/steps`: `step` (JSON) and optional `screenshot`. */\nexport function captureStepForm(step: CaptureStepInput, screenshot: Blob | null): FormData {\n const form = new FormData();\n form.append(\"step\", JSON.stringify(step));\n if (screenshot) form.append(\"screenshot\", screenshot, `step.${EXT[screenshot.type] ?? \"webp\"}`);\n return form;\n}\n\nexport class CaptureBridge {\n private info: CaptureSessionInfo | null = null;\n private session: CaptureSession | null = null;\n private heartbeat: ReturnType<typeof setInterval> | null = null;\n private retryTimer: ReturnType<typeof setTimeout> | null = null;\n private queue: QueuedStep[] = [];\n private pumping = false;\n private active = false;\n private finishing = false;\n private marker: CaptureMarker;\n private readonly onPageHide = () => this.saveForReload();\n\n constructor(private readonly opts: CaptureBridgeOptions) {\n this.marker = { ...opts.marker };\n }\n\n isActive(): boolean {\n return this.active;\n }\n\n private get token(): string {\n return this.marker.token;\n }\n\n /** Verifies the session and starts recording. Resolves once the toolbar is up (or the session was rejected). */\n async start(): Promise<void> {\n try {\n const res = await this.opts.api.get<CaptureSessionInfo>(\"/capture/session\", { params: { token: this.token } });\n if (!res.ok || !res.data) return this.reject(`[Guidely] capture session invalid or expired (${res.status})`);\n this.info = res.data;\n if (Date.parse(this.info.expiresAt) < Date.now()) return this.reject(\"[Guidely] capture session expired\");\n const pending = readJson<PendingStep[]>(this.opts.sessionStore, KEYS.capturePending, []);\n this.queue = (Array.isArray(pending) ? pending : []).filter((p) => p && p.step).map((p) => ({ step: p.step, frame: null, shot: p.shot }));\n this.active = true;\n this.opts.onActive(true);\n this.session = startCapture({\n title: this.info.howToTitle,\n stepCount: this.info.stepCount + this.queue.length,\n screenshots: this.info.screenshots,\n maskSelector: this.opts.maskSelector,\n resume: this.marker.started ? { navigate: !loadFollowsAction(this.marker), screenshots: !!this.marker.screenshots } : undefined,\n onStart: ({ screenshots }) => this.saveMarker({ started: true, screenshots }),\n onStep: (s) => this.enqueue(s),\n onFinish: () => void this.finish(false),\n onCancel: () => void this.finish(true),\n });\n this.heartbeat = setInterval(() => void this.beat(), CAPTURE_HEARTBEAT_MS);\n window.addEventListener(\"pagehide\", this.onPageHide);\n this.post({ type: \"guidely:capture-ready\", sessionId: this.info.id, url: window.location.href });\n void this.pump();\n } catch (err) {\n this.opts.onError(err);\n }\n }\n\n private reject(message: string): void {\n this.clearMarker();\n this.opts.onError(new Error(message));\n }\n\n private saveMarker(patch: Partial<CaptureMarker>): void {\n this.marker = { ...this.marker, ...patch };\n writeJson(this.opts.sessionStore, KEYS.capture, this.marker);\n }\n\n private clearMarker(): void {\n this.opts.sessionStore.remove(KEYS.capture);\n this.opts.sessionStore.remove(KEYS.capturePending);\n }\n\n /** Persists the steps still to upload (JSON only; screenshots are added on page hide). */\n private savePending(): void {\n if (!this.queue.length) this.opts.sessionStore.remove(KEYS.capturePending);\n else writeJson(this.opts.sessionStore, KEYS.capturePending, this.queue.map((q): PendingStep => (q.shot ? { step: q.step, shot: q.shot } : { step: q.step })));\n }\n\n /** The page is unloading with uploads in flight: keep their screenshots for the next page. */\n private saveForReload(): void {\n if (!this.active || !this.queue.length) return;\n for (const q of this.queue) if (!q.shot && q.frame && !(q.frame instanceof Promise)) q.shot = screenshotDataUrl(q.frame) ?? undefined;\n this.savePending();\n }\n\n private enqueue(captured: CapturedStep): void {\n if (this.finishing) return;\n // Same object: a deferred frame still updates its highlight and redaction count before upload.\n const step: CaptureStepInput = Object.assign(captured.step, { clientId: randomId(16) });\n this.queue.push({ step, frame: captured.frame });\n this.saveMarker({ lastStepAt: Date.now(), lastAction: step.action });\n this.savePending();\n void this.pump();\n }\n\n /** Uploads queued steps one at a time, in capture order. */\n private async pump(): Promise<void> {\n if (this.pumping || !this.info) return;\n this.pumping = true;\n try {\n while (this.queue.length && this.info) {\n const item = this.queue[0]!;\n if (item.frame instanceof Promise) item.frame = await item.frame.catch(() => null);\n if (item.blob === undefined) item.blob = item.shot ? dataUrlToBlob(item.shot) : item.frame ? await encodeScreenshot(item.frame, { maxBytes: CAPTURE_SCREENSHOT_MAX_BYTES }) : null;\n const res = await this.upload(item);\n if (!this.info) return;\n if (!res) {\n if (this.retryTimer) clearTimeout(this.retryTimer);\n this.retryTimer = setTimeout(() => void this.pump(), UPLOAD_RETRY_LATER_MS);\n return;\n }\n this.queue.shift();\n this.savePending();\n if (res.status === 401 || res.status === 403 || res.status === 404 || res.status === 410) {\n this.opts.onError(new Error(`[Guidely] capture session ended (${res.status})`));\n this.teardown(true);\n return;\n }\n if (res.ok && res.data) this.post({ type: \"guidely:capture-step\", sessionId: this.info.id, stepCount: res.data.stepCount });\n else if (!res.ok) this.opts.onError(new Error(`[Guidely] capture step rejected (${res.status})`));\n }\n } catch (err) {\n this.opts.onError(err);\n } finally {\n this.pumping = false;\n }\n }\n\n private async upload(item: QueuedStep): Promise<ApiResponse<{ id: string; position: number; stepCount: number }> | null> {\n const send = (shot: Blob | null) =>\n withRetry(() => this.opts.api.postForm<{ id: string; position: number; stepCount: number }>(\"/capture/steps\", captureStepForm(item.step, shot), { params: { token: this.token }, timeoutMs: 60000 }), 4, 1000);\n const res = await send(item.blob ?? null);\n // Too large or unreadable image: keep the step without it.\n if (res && item.blob && (res.status === 413 || res.status === 415)) return send(null);\n return res;\n }\n\n private async beat(): Promise<void> {\n try {\n const res = await this.opts.api.post(\"/capture/session/heartbeat\", {}, { params: { token: this.token } });\n if (res.status === 401 || res.status === 410) {\n this.opts.onError(new Error(`[Guidely] capture session ended (${res.status})`));\n this.teardown(true);\n }\n } catch {\n /* offline: the next beat retries */\n }\n }\n\n /** Done (waits for queued uploads) or Cancel (drops them); either way the dashboard is told. */\n private async finish(cancelled: boolean): Promise<void> {\n if (this.finishing || !this.info) return;\n this.finishing = true;\n const info = this.info;\n if (cancelled) {\n this.queue = [];\n this.savePending();\n } else {\n this.session?.setSaving(true);\n if (this.retryTimer) clearTimeout(this.retryTimer);\n void this.pump();\n const deadline = Date.now() + FINISH_DRAIN_MS;\n while ((this.queue.length || this.pumping) && this.info && Date.now() < deadline) await sleep(100);\n }\n let howToId = info.howToId;\n try {\n const res = await withRetry(() => this.opts.api.post<{ ok: boolean; howToId?: string }>(\"/capture/session/finish\", { cancelled }, { params: { token: this.token } }), 3, 500);\n if (res?.data?.howToId) howToId = res.data.howToId;\n } catch (err) {\n this.opts.onError(err);\n }\n this.post({ type: \"guidely:capture-finished\", sessionId: info.id, howToId, cancelled });\n this.teardown(true);\n }\n\n private post(message: CaptureOutgoingMessage): void {\n if (!this.info) return;\n try {\n window.opener?.postMessage(message, this.info.dashboardOrigin);\n } catch {\n /* opener may be gone */\n }\n }\n\n private teardown(clearMarker: boolean): void {\n if (this.heartbeat) clearInterval(this.heartbeat);\n this.heartbeat = null;\n if (this.retryTimer) clearTimeout(this.retryTimer);\n this.retryTimer = null;\n window.removeEventListener(\"pagehide\", this.onPageHide);\n this.session?.stop();\n this.session = null;\n if (clearMarker) this.clearMarker();\n this.info = null;\n this.queue = [];\n if (!this.active) return;\n this.active = false;\n this.opts.onActive(false);\n }\n\n /** Removes the toolbar and stops uploading but keeps the session marker (the SDK was destroyed; a new client resumes). */\n destroy(): void {\n this.saveForReload();\n this.teardown(false);\n }\n}\n\n/** What the on-demand capture chunk provides (`window.__guidelyCapture` in the script-tag build). */\nexport interface CaptureApi {\n createBridge(options: CaptureBridgeOptions): CaptureBridge;\n}\n\nexport const capture: CaptureApi = { createBridge: (options) => new CaptureBridge(options) };\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/capture.ts"],"sourcesContent":["import type { CaptureSessionInfo, CaptureStepInput } from \"@guidelytech/shared\";\nimport { CAPTURE_SCREENSHOT_MAX_BYTES, randomId } from \"@guidelytech/shared/runtime\";\nimport { encodeScreenshot, screenshotDataUrl, startCapture, type CapturedStep, type CaptureSession } from \"@guidelytech/guide-runtime/capture\";\nimport type { CaptureMarker } from \"./capture-token\";\nimport { sleep, withRetry, type Api, type ApiResponse } from \"./http\";\nimport { KEYS, readJson, writeJson, type KeyValueStore } from \"./storage\";\n\n/**\n * How-to capture bridge (loaded on demand): verifies the capture session, runs the runtime's\n * recorder toolbar, uploads steps with their screenshots in capture order and tells the dashboard\n * window (`window.opener`) about progress.\n */\n\n/** Keeps the capture session alive on the server (same cadence as authoring). */\nexport const CAPTURE_HEARTBEAT_MS = 15000;\n\n/** A full page load this soon after a recorded interaction was caused by it: no navigate step. */\nexport const LOAD_AFTER_ACTION_MS = 8000;\n/** How long Done waits for queued uploads before finishing anyway. */\nexport const FINISH_DRAIN_MS = 20000;\n/** Pause before retrying uploads after every attempt failed (offline, server down). */\nexport const UPLOAD_RETRY_LATER_MS = 10000;\n\n/** Messages the SDK posts to the dashboard window (`window.opener`, target origin `dashboardOrigin`). */\nexport type CaptureOutgoingMessage =\n | { type: \"guidely:capture-ready\"; sessionId: string; url: string }\n | { type: \"guidely:capture-step\"; sessionId: string; stepCount: number }\n | { type: \"guidely:capture-finished\"; sessionId: string; howToId: string; cancelled: boolean };\n\nexport interface CaptureBridgeOptions {\n api: Api;\n sessionStore: KeyValueStore;\n marker: CaptureMarker;\n /** Comma-joined mask selectors (project setting + SDK option). */\n maskSelector: string;\n onError: (err: unknown) => void;\n onActive: (active: boolean) => void;\n}\n\ninterface QueuedStep {\n step: CaptureStepInput;\n frame: CapturedStep[\"frame\"];\n blob?: Blob | null;\n /** Screenshot saved while the page unloaded (uploaded after the next page load). */\n shot?: string;\n}\n\ninterface PendingStep {\n step: CaptureStepInput;\n shot?: string;\n}\n\n/** After a full page load: was it caused by the last recorded interaction (so no navigate step)? */\nexport function loadFollowsAction(marker: Pick<CaptureMarker, \"lastAction\" | \"lastStepAt\">, now = Date.now()): boolean {\n return !!marker.lastAction && marker.lastAction !== \"navigate\" && now - (marker.lastStepAt ?? 0) < LOAD_AFTER_ACTION_MS;\n}\n\nfunction dataUrlToBlob(url: string): Blob | null {\n try {\n const comma = url.indexOf(\",\");\n const type = /^data:([^;,]+)/.exec(url)?.[1] || \"image/jpeg\";\n const bin = atob(url.slice(comma + 1));\n const bytes = new Uint8Array(bin.length);\n for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);\n return new Blob([bytes], { type });\n } catch {\n return null;\n }\n}\n\nconst EXT: Record<string, string> = { \"image/webp\": \"webp\", \"image/jpeg\": \"jpg\", \"image/png\": \"png\" };\n\n/** Multipart body for `POST /capture/steps`: `step` (JSON) and optional `screenshot`. */\nexport function captureStepForm(step: CaptureStepInput, screenshot: Blob | null): FormData {\n const form = new FormData();\n form.append(\"step\", JSON.stringify(step));\n if (screenshot) form.append(\"screenshot\", screenshot, `step.${EXT[screenshot.type] ?? \"webp\"}`);\n return form;\n}\n\nexport class CaptureBridge {\n private info: CaptureSessionInfo | null = null;\n private session: CaptureSession | null = null;\n private heartbeat: ReturnType<typeof setInterval> | null = null;\n private retryTimer: ReturnType<typeof setTimeout> | null = null;\n private queue: QueuedStep[] = [];\n private pumping = false;\n private active = false;\n private finishing = false;\n private marker: CaptureMarker;\n private readonly onPageHide = () => this.saveForReload();\n\n constructor(private readonly opts: CaptureBridgeOptions) {\n this.marker = { ...opts.marker };\n }\n\n isActive(): boolean {\n return this.active;\n }\n\n private get token(): string {\n return this.marker.token;\n }\n\n /** Verifies the session and starts recording. Resolves once the toolbar is up (or the session was rejected). */\n async start(): Promise<void> {\n try {\n const res = await this.opts.api.get<CaptureSessionInfo>(\"/capture/session\", { params: { token: this.token } });\n if (!res.ok || !res.data) return this.reject(`[Guidely] capture session invalid or expired (${res.status})`);\n this.info = res.data;\n if (Date.parse(this.info.expiresAt) < Date.now()) return this.reject(\"[Guidely] capture session expired\");\n const pending = readJson<PendingStep[]>(this.opts.sessionStore, KEYS.capturePending, []);\n this.queue = (Array.isArray(pending) ? pending : []).filter((p) => p && p.step).map((p) => ({ step: p.step, frame: null, shot: p.shot }));\n this.active = true;\n this.opts.onActive(true);\n this.session = startCapture({\n title: this.info.howToTitle,\n stepCount: this.info.stepCount + this.queue.length,\n screenshots: this.info.screenshots,\n maskSelector: this.opts.maskSelector,\n resume: this.marker.started ? { navigate: !loadFollowsAction(this.marker), screenshots: !!this.marker.screenshots } : undefined,\n onStart: ({ screenshots }) => this.saveMarker({ started: true, screenshots }),\n onStep: (s) => this.enqueue(s),\n onFinish: () => void this.finish(false),\n onCancel: () => void this.finish(true),\n });\n this.heartbeat = setInterval(() => void this.beat(), CAPTURE_HEARTBEAT_MS);\n window.addEventListener(\"pagehide\", this.onPageHide);\n this.post({ type: \"guidely:capture-ready\", sessionId: this.info.id, url: window.location.href });\n void this.pump();\n } catch (err) {\n this.opts.onError(err);\n }\n }\n\n private reject(message: string): void {\n this.clearMarker();\n this.opts.onError(new Error(message));\n }\n\n private saveMarker(patch: Partial<CaptureMarker>): void {\n this.marker = { ...this.marker, ...patch };\n writeJson(this.opts.sessionStore, KEYS.capture, this.marker);\n }\n\n private clearMarker(): void {\n this.opts.sessionStore.remove(KEYS.capture);\n this.opts.sessionStore.remove(KEYS.capturePending);\n }\n\n /** Persists the steps still to upload (JSON only; screenshots are added on page hide). */\n private savePending(): void {\n if (!this.queue.length) this.opts.sessionStore.remove(KEYS.capturePending);\n else writeJson(this.opts.sessionStore, KEYS.capturePending, this.queue.map((q): PendingStep => (q.shot ? { step: q.step, shot: q.shot } : { step: q.step })));\n }\n\n /** The page is unloading with uploads in flight: keep their screenshots for the next page. */\n private saveForReload(): void {\n if (!this.active || !this.queue.length) return;\n for (const q of this.queue) if (!q.shot && q.frame && !(q.frame instanceof Promise)) q.shot = screenshotDataUrl(q.frame) ?? undefined;\n this.savePending();\n }\n\n private enqueue(captured: CapturedStep): void {\n if (this.finishing) return;\n // Same object: a deferred frame still updates its highlight and redaction count before upload.\n const step: CaptureStepInput = Object.assign(captured.step, { clientId: randomId(16) });\n this.queue.push({ step, frame: captured.frame });\n this.saveMarker({ lastStepAt: Date.now(), lastAction: step.action });\n this.savePending();\n void this.pump();\n }\n\n /** Uploads queued steps one at a time, in capture order. */\n private async pump(): Promise<void> {\n if (this.pumping || !this.info) return;\n this.pumping = true;\n try {\n while (this.queue.length && this.info) {\n const item = this.queue[0]!;\n if (item.frame instanceof Promise) item.frame = await item.frame.catch(() => null);\n if (item.blob === undefined) item.blob = item.shot ? dataUrlToBlob(item.shot) : item.frame ? await encodeScreenshot(item.frame, { maxBytes: CAPTURE_SCREENSHOT_MAX_BYTES }) : null;\n const res = await this.upload(item);\n if (!this.info) return;\n if (!res) {\n if (this.retryTimer) clearTimeout(this.retryTimer);\n this.retryTimer = setTimeout(() => void this.pump(), UPLOAD_RETRY_LATER_MS);\n return;\n }\n this.queue.shift();\n this.savePending();\n if (res.status === 401 || res.status === 403 || res.status === 404 || res.status === 410) {\n this.opts.onError(new Error(`[Guidely] capture session ended (${res.status})`));\n this.teardown(true);\n return;\n }\n if (res.ok && res.data) this.post({ type: \"guidely:capture-step\", sessionId: this.info.id, stepCount: res.data.stepCount });\n else if (!res.ok) this.opts.onError(new Error(`[Guidely] capture step rejected (${res.status})`));\n }\n } catch (err) {\n this.opts.onError(err);\n } finally {\n this.pumping = false;\n }\n }\n\n private async upload(item: QueuedStep): Promise<ApiResponse<{ id: string; position: number; stepCount: number }> | null> {\n const send = (shot: Blob | null) =>\n withRetry(() => this.opts.api.postForm<{ id: string; position: number; stepCount: number }>(\"/capture/steps\", captureStepForm(item.step, shot), { params: { token: this.token }, timeoutMs: 60000 }), 4, 1000);\n const res = await send(item.blob ?? null);\n // Too large or unreadable image: keep the step without it.\n if (res && item.blob && (res.status === 413 || res.status === 415)) return send(null);\n return res;\n }\n\n private async beat(): Promise<void> {\n try {\n const res = await this.opts.api.post(\"/capture/session/heartbeat\", {}, { params: { token: this.token } });\n if (res.status === 401 || res.status === 410) {\n this.opts.onError(new Error(`[Guidely] capture session ended (${res.status})`));\n this.teardown(true);\n }\n } catch {\n /* offline: the next beat retries */\n }\n }\n\n /** Done (waits for queued uploads) or Cancel (drops them); either way the dashboard is told. */\n private async finish(cancelled: boolean): Promise<void> {\n if (this.finishing || !this.info) return;\n this.finishing = true;\n const info = this.info;\n if (cancelled) {\n this.queue = [];\n this.savePending();\n } else {\n this.session?.setSaving(true);\n if (this.retryTimer) clearTimeout(this.retryTimer);\n void this.pump();\n const deadline = Date.now() + FINISH_DRAIN_MS;\n while ((this.queue.length || this.pumping) && this.info && Date.now() < deadline) await sleep(100);\n }\n let howToId = info.howToId;\n try {\n const res = await withRetry(() => this.opts.api.post<{ ok: boolean; howToId?: string }>(\"/capture/session/finish\", { cancelled }, { params: { token: this.token } }), 3, 500);\n if (res?.data?.howToId) howToId = res.data.howToId;\n } catch (err) {\n this.opts.onError(err);\n }\n this.post({ type: \"guidely:capture-finished\", sessionId: info.id, howToId, cancelled });\n this.teardown(true);\n }\n\n private post(message: CaptureOutgoingMessage): void {\n if (!this.info) return;\n try {\n window.opener?.postMessage(message, this.info.dashboardOrigin);\n } catch {\n /* opener may be gone */\n }\n }\n\n private teardown(clearMarker: boolean): void {\n if (this.heartbeat) clearInterval(this.heartbeat);\n this.heartbeat = null;\n if (this.retryTimer) clearTimeout(this.retryTimer);\n this.retryTimer = null;\n window.removeEventListener(\"pagehide\", this.onPageHide);\n this.session?.stop();\n this.session = null;\n if (clearMarker) this.clearMarker();\n this.info = null;\n this.queue = [];\n if (!this.active) return;\n this.active = false;\n this.opts.onActive(false);\n }\n\n /** Removes the toolbar and stops uploading but keeps the session marker (the SDK was destroyed; a new client resumes). */\n destroy(): void {\n this.saveForReload();\n this.teardown(false);\n }\n}\n\n/** What the on-demand capture chunk provides (`window.__guidelyCapture` in the script-tag build). */\nexport interface CaptureApi {\n createBridge(options: CaptureBridgeOptions): CaptureBridge;\n}\n\nexport const capture: CaptureApi = { createBridge: (options) => new CaptureBridge(options) };\n"],"mappings":"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","names":[]}