@guidelytech/sdk-core 0.10.0 → 0.12.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 (46) hide show
  1. package/dist/{capture-DVJyQ2JZ.d.cts → capture-4EgpjK6Z.d.cts} +2 -2
  2. package/dist/{capture-369s4YYT.d.ts → capture-C-4R56G0.d.ts} +2 -2
  3. package/dist/capture-CgN69Lxq.d.cts +100 -0
  4. package/dist/capture-Csz721-A.d.ts +100 -0
  5. package/dist/capture.cjs +5 -1
  6. package/dist/capture.cjs.map +1 -1
  7. package/dist/capture.d.cts +3 -3
  8. package/dist/capture.d.ts +3 -3
  9. package/dist/capture.js +1 -1
  10. package/dist/{chunk-GDJB466J.js → chunk-2E2YUQAG.js} +29 -6
  11. package/dist/chunk-2E2YUQAG.js.map +1 -0
  12. package/dist/{chunk-C4WWSF67.js → chunk-JUSM7J5N.js} +3 -1
  13. package/dist/{chunk-C4WWSF67.js.map → chunk-JUSM7J5N.js.map} +1 -1
  14. package/dist/chunk-XML74K2P.js +378 -0
  15. package/dist/chunk-XML74K2P.js.map +1 -0
  16. package/dist/chunk-ZAF2KRJA.js +334 -0
  17. package/dist/chunk-ZAF2KRJA.js.map +1 -0
  18. package/dist/headless.cjs +35 -5
  19. package/dist/headless.cjs.map +1 -1
  20. package/dist/headless.d.cts +6 -5
  21. package/dist/headless.d.ts +6 -5
  22. package/dist/headless.js +9 -3
  23. package/dist/health-DjZ2gwYa.d.cts +92 -0
  24. package/dist/health-DjZ2gwYa.d.ts +92 -0
  25. package/dist/{health-DFHUstWd.d.cts → health-sbP9JwTE.d.cts} +2 -0
  26. package/dist/{health-DFHUstWd.d.ts → health-sbP9JwTE.d.ts} +2 -0
  27. package/dist/{http-DJd8jRhK.d.ts → http-BXys2d-j.d.ts} +1 -1
  28. package/dist/{http-D0dCcpLN.d.cts → http-Cb6KYYFv.d.cts} +1 -1
  29. package/dist/http-D047Ngxk.d.ts +61 -0
  30. package/dist/http-zOI8d2c2.d.cts +61 -0
  31. package/dist/index.cjs +1638 -1047
  32. package/dist/index.cjs.map +1 -1
  33. package/dist/index.d.cts +77 -9
  34. package/dist/index.d.ts +77 -9
  35. package/dist/index.js +763 -202
  36. package/dist/index.js.map +1 -1
  37. package/dist/recorder.cjs.map +1 -1
  38. package/dist/recorder.d.cts +1 -1
  39. package/dist/recorder.d.ts +1 -1
  40. package/dist/recorder.js +1 -1
  41. package/dist/{types-B5ecgHUV.d.cts → types-BDlebXrm.d.ts} +59 -5
  42. package/dist/types-Bkw8ErmO.d.cts +314 -0
  43. package/dist/{types-BtPovimB.d.ts → types-Dy_uPu6S.d.ts} +59 -5
  44. package/dist/types-Mtlmlj5C.d.cts +314 -0
  45. package/package.json +4 -4
  46. package/dist/chunk-GDJB466J.js.map +0 -1
package/dist/index.cjs CHANGED
@@ -33,165 +33,370 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
33
33
  var index_exports = {};
34
34
  __export(index_exports, {
35
35
  AUTHORING_PARAM: () => AUTHORING_PARAM,
36
- CAPTURE_PARAM: () => import_runtime2.CAPTURE_PARAM,
36
+ CAPTURE_PARAM: () => import_runtime9.CAPTURE_PARAM,
37
37
  DEFAULT_API_URL: () => DEFAULT_API_URL,
38
38
  EventQueue: () => EventQueue,
39
39
  Guidely: () => Guidely,
40
40
  GuidelyClient: () => GuidelyClient,
41
41
  HEARTBEAT_MS: () => HEARTBEAT_MS,
42
42
  KEYS: () => KEYS,
43
+ PROTOTYPE_CLOSE_MESSAGE: () => PROTOTYPE_CLOSE_MESSAGE,
43
44
  SDK_NAME: () => SDK_NAME,
44
45
  SDK_VERSION: () => SDK_VERSION,
45
46
  areMutuallyExclusive: () => areMutuallyExclusive,
46
47
  buildContext: () => buildContext,
47
48
  captureSnapshot: () => captureSnapshot,
49
+ countsTowardThrottle: () => countsTowardThrottle,
48
50
  createGuidely: () => createGuidely,
49
51
  createStorage: () => createStorage,
50
52
  default: () => index_default,
51
53
  detectDevice: () => detectDevice,
54
+ isAppUrl: () => isAppUrl,
52
55
  isExclusiveGuide: () => isExclusiveGuide,
53
56
  isFrequencyEligible: () => isFrequencyEligible,
57
+ isThrottleEligible: () => isThrottleEligible,
54
58
  isWithinSchedule: () => isWithinSchedule,
59
+ matchPrototypeTestUrl: () => matchPrototypeTestUrl,
60
+ normalizeAppUrl: () => normalizeAppUrl,
61
+ pruneLaunches: () => pruneLaunches,
55
62
  watchNavigation: () => watchNavigation
56
63
  });
57
64
  module.exports = __toCommonJS(index_exports);
58
65
 
59
66
  // src/client.ts
60
- var import_runtime15 = require("@guidelytech/shared/runtime");
61
- var import_guide_runtime4 = require("@guidelytech/guide-runtime");
67
+ var import_runtime18 = require("@guidelytech/shared/runtime");
62
68
  var import_guide_runtime5 = require("@guidelytech/guide-runtime");
69
+ var import_guide_runtime6 = require("@guidelytech/guide-runtime");
63
70
  var import_flags = require("@guidelytech/flags");
64
71
 
65
- // src/authoring.ts
66
- var import_guide_runtime = require("@guidelytech/guide-runtime");
72
+ // src/agent-turns.ts
73
+ var import_runtime4 = require("@guidelytech/shared/runtime");
67
74
 
68
- // src/snapshot.ts
69
- var MAX_SNAPSHOT_BYTES = 6 * 1024 * 1024;
70
- var STRIP = "script, noscript, template, #guidely-root, [data-guidely], meta[http-equiv], link[rel~='preload'], link[rel~='modulepreload'], link[rel~='prefetch'], link[rel~='manifest'], link[rel~='serviceworker']";
71
- function rulesText(sheet) {
72
- if (!sheet) return null;
73
- try {
74
- return Array.from(sheet.cssRules, (r) => r.cssText).join("\n");
75
- } catch {
76
- return null;
75
+ // src/health.ts
76
+ var MAX_RETRY_AFTER_MS = 60 * 60 * 1e3;
77
+ var OFFLINE_WAIT_MS = 3e4;
78
+ var ServerHealth = class {
79
+ constructor(opts = {}) {
80
+ __publicField(this, "failures", 0);
81
+ __publicField(this, "openUntil", 0);
82
+ __publicField(this, "lastOkAt", 0);
83
+ __publicField(this, "listeners", /* @__PURE__ */ new Set());
84
+ __publicField(this, "threshold");
85
+ __publicField(this, "baseMs");
86
+ __publicField(this, "maxMs");
87
+ __publicField(this, "now");
88
+ __publicField(this, "random");
89
+ this.threshold = opts.threshold ?? 3;
90
+ this.baseMs = opts.baseMs ?? 5e3;
91
+ this.maxMs = opts.maxMs ?? 5 * 60 * 1e3;
92
+ this.now = opts.now ?? Date.now;
93
+ this.random = opts.random ?? Math.random;
77
94
  }
78
- }
79
- function maskText(root) {
80
- const walker = root.ownerDocument.createTreeWalker(root, NodeFilter.SHOW_TEXT);
81
- for (let n = walker.nextNode(); n; n = walker.nextNode()) {
82
- n.textContent = (n.textContent ?? "").replace(/\S/g, "\u2022");
95
+ /** The server answered (2xx, 3xx, or a 4xx other than 429): it is up. */
96
+ success() {
97
+ const wasDown = this.failures > 0 || this.openUntil > 0;
98
+ this.failures = 0;
99
+ this.openUntil = 0;
100
+ this.lastOkAt = this.now();
101
+ if (wasDown) this.notify();
83
102
  }
84
- }
85
- function captureSnapshot(doc, maskSelectors, maxBytes = MAX_SNAPSHOT_BYTES) {
86
- const win = doc.defaultView;
87
- if (!win || !doc.documentElement) return null;
88
- const clone = doc.documentElement.cloneNode(true);
89
- const origStyles = doc.documentElement.querySelectorAll("style");
90
- clone.querySelectorAll("style").forEach((el, i) => {
91
- const css = rulesText(origStyles[i]?.sheet);
92
- if (css !== null && css.length > (el.textContent ?? "").length) el.textContent = css;
93
- });
94
- const origFrames = doc.documentElement.querySelectorAll("iframe, canvas, video, embed, object");
95
- clone.querySelectorAll("iframe, canvas, video, embed, object").forEach((el, i) => {
96
- const r = origFrames[i]?.getBoundingClientRect();
97
- const box = doc.createElement("div");
98
- box.setAttribute("style", `${el.getAttribute("style") ?? ""};width:${r?.width ?? 0}px;height:${r?.height ?? 0}px;background:#e2e8f0;border-radius:4px`);
99
- if (el.getAttribute("class")) box.setAttribute("class", el.getAttribute("class"));
100
- el.replaceWith(box);
101
- });
102
- clone.querySelectorAll(STRIP).forEach((el) => el.remove());
103
- for (const sel of maskSelectors) {
104
- try {
105
- clone.querySelectorAll(sel).forEach(maskText);
106
- } catch {
103
+ /** The request failed or the server is overloaded. `retryAfterMs` comes from a `Retry-After` header. */
104
+ failure(retryAfterMs) {
105
+ this.failures += 1;
106
+ const now = this.now();
107
+ if (retryAfterMs != null && retryAfterMs > 0) {
108
+ this.openUntil = Math.max(this.openUntil, now + Math.min(retryAfterMs, MAX_RETRY_AFTER_MS));
109
+ return;
107
110
  }
111
+ if (this.failures < this.threshold) return;
112
+ const cooldown = Math.min(this.maxMs, this.baseMs * 2 ** (this.failures - this.threshold));
113
+ this.openUntil = Math.max(this.openUntil, now + cooldown * (0.8 + this.random() * 0.4));
108
114
  }
109
- clone.querySelectorAll("*").forEach((el) => {
110
- for (const attr2 of Array.from(el.attributes)) {
111
- const name = attr2.name.toLowerCase();
112
- if (name.startsWith("on") || (name === "href" || name === "src" || name === "action") && /^\s*javascript:/i.test(attr2.value)) el.removeAttribute(attr2.name);
113
- }
114
- if (el instanceof HTMLInputElement) {
115
- el.removeAttribute("value");
116
- el.removeAttribute("checked");
117
- } else if (el instanceof HTMLTextAreaElement) {
118
- el.textContent = "";
119
- }
120
- el.removeAttribute("autofocus");
121
- });
122
- const adopted = Array.from(doc.adoptedStyleSheets ?? []).map(rulesText).filter(Boolean).join("\n");
123
- let head = clone.querySelector("head");
124
- if (!head) {
125
- head = doc.createElement("head");
126
- clone.prepend(head);
115
+ /** Milliseconds background traffic should wait before trying again (0: send now). */
116
+ waitMs() {
117
+ if (typeof navigator !== "undefined" && navigator.onLine === false) return OFFLINE_WAIT_MS;
118
+ return Math.max(0, this.openUntil - this.now());
127
119
  }
128
- if (adopted) {
129
- const style = doc.createElement("style");
130
- style.textContent = adopted;
131
- head.append(style);
120
+ canSend() {
121
+ return this.waitMs() === 0;
132
122
  }
133
- clone.querySelectorAll("base").forEach((el) => el.remove());
134
- const base = doc.createElement("base");
135
- base.setAttribute("href", doc.baseURI || win.location.href);
136
- base.setAttribute("target", "_blank");
137
- head.prepend(base);
138
- const html = `<!DOCTYPE html>
139
- ${clone.outerHTML}`;
140
- if (html.length > maxBytes) return null;
141
- return { html, url: win.location.href, title: doc.title, width: win.innerWidth, height: win.innerHeight, capturedAt: (/* @__PURE__ */ new Date()).toISOString() };
123
+ /**
124
+ * The last request got an answer. Unload beacons can't report back, so what they carry is only
125
+ * forgotten while this holds; otherwise it stays queued and is sent again (the server drops duplicates).
126
+ */
127
+ get reachable() {
128
+ return this.failures === 0 && this.lastOkAt > 0;
129
+ }
130
+ /** The browser came back online: end the cooldown and let queued traffic try straight away. */
131
+ online() {
132
+ this.openUntil = 0;
133
+ this.notify();
134
+ }
135
+ /** Called when the server answers again after failures, or the browser comes back online. */
136
+ onRecover(cb) {
137
+ this.listeners.add(cb);
138
+ return () => this.listeners.delete(cb);
139
+ }
140
+ notify() {
141
+ for (const cb of Array.from(this.listeners)) {
142
+ try {
143
+ cb();
144
+ } catch {
145
+ }
146
+ }
147
+ }
148
+ };
149
+ function parseRetryAfter(value, now = Date.now()) {
150
+ if (!value) return null;
151
+ const trimmed = value.trim();
152
+ if (/^\d+$/.test(trimmed)) return Number(trimmed) * 1e3;
153
+ const at = Date.parse(trimmed);
154
+ return Number.isNaN(at) ? null : Math.max(0, at - now);
142
155
  }
143
-
144
- // src/storage.ts
145
- function memoryStore() {
146
- const map = /* @__PURE__ */ new Map();
147
- return { get: (k) => map.get(k) ?? null, set: (k, v) => void map.set(k, v), remove: (k) => void map.delete(k) };
156
+ function isServerFailure(status) {
157
+ return status === 429 || status >= 500;
148
158
  }
149
- function webStore(area) {
150
- try {
151
- if (typeof window === "undefined") return null;
152
- const s = window[area];
153
- if (!s) return null;
154
- const probe = "__guidely_probe__";
155
- s.setItem(probe, "1");
156
- s.removeItem(probe);
157
- return {
158
- get: (k) => {
159
- try {
160
- return s.getItem(k);
161
- } catch {
162
- return null;
163
- }
164
- },
165
- set: (k, v) => {
166
- try {
167
- s.setItem(k, v);
168
- } catch {
169
- }
170
- },
171
- remove: (k) => {
159
+
160
+ // src/http.ts
161
+ var STREAM_RESPONSE_TIMEOUT_MS = 3e4;
162
+ var STREAM_IDLE_TIMEOUT_MS = 6e4;
163
+ var Api = class {
164
+ constructor(baseUrl, key, fetchImpl, health) {
165
+ __publicField(this, "baseUrl", baseUrl);
166
+ __publicField(this, "key", key);
167
+ __publicField(this, "fetchImpl", fetchImpl);
168
+ /** Reachability of the API, fed by every answer (see {@link ServerHealth}). */
169
+ __publicField(this, "health");
170
+ this.health = health ?? new ServerHealth();
171
+ }
172
+ /** Feeds an answer (or a failed request, `null`) into {@link health}. */
173
+ observe(res) {
174
+ if (!res) this.health.failure();
175
+ else if (isServerFailure(res.status)) this.health.failure(parseRetryAfter(res.headers?.get?.("retry-after")));
176
+ else this.health.success();
177
+ }
178
+ url(path, params = {}) {
179
+ const query = Object.entries({ key: this.key, ...params }).filter((e) => e[1] != null).map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join("&");
180
+ return `${this.baseUrl.replace(/\/$/, "")}/api/v1/sdk${path}?${query}`;
181
+ }
182
+ get fetcher() {
183
+ if (this.fetchImpl) return this.fetchImpl;
184
+ return typeof fetch === "function" ? fetch.bind(globalThis) : null;
185
+ }
186
+ async request(method, path, options = {}) {
187
+ const f = this.fetcher;
188
+ if (!f) throw new Error("fetch is not available");
189
+ const headers = { ...options.headers, "X-Guidely-Key": this.key, Accept: "application/json" };
190
+ const form = typeof FormData !== "undefined" && options.body instanceof FormData;
191
+ if (options.body !== void 0 && !form) headers["Content-Type"] = "application/json";
192
+ if (options.etag) headers["If-None-Match"] = options.etag;
193
+ const ac = typeof AbortController !== "undefined" ? new AbortController() : null;
194
+ const timer = ac ? setTimeout(() => ac.abort(), options.timeoutMs ?? 1e4) : null;
195
+ try {
196
+ let res;
197
+ try {
198
+ res = await f(this.url(path, options.params), {
199
+ method,
200
+ headers,
201
+ body: options.body === void 0 ? void 0 : form ? options.body : JSON.stringify(options.body),
202
+ keepalive: options.keepalive,
203
+ signal: ac?.signal,
204
+ credentials: "omit",
205
+ mode: "cors"
206
+ });
207
+ } catch (err) {
208
+ this.observe(null);
209
+ throw err;
210
+ }
211
+ this.observe(res);
212
+ let data = null;
213
+ if (res.status !== 304 && res.status !== 204) {
172
214
  try {
173
- s.removeItem(k);
215
+ data = await res.json();
174
216
  } catch {
217
+ data = null;
175
218
  }
176
219
  }
177
- };
178
- } catch {
179
- return null;
220
+ return { status: res.status, ok: res.ok, data, etag: res.headers?.get?.("etag") ?? null };
221
+ } finally {
222
+ if (timer) clearTimeout(timer);
223
+ }
180
224
  }
181
- }
182
- function createStorage(kind = "local") {
183
- if (kind === "memory") return memoryStore();
184
- return webStore(kind === "local" ? "localStorage" : "sessionStorage") ?? memoryStore();
185
- }
186
- function readJson(store, key, fallback) {
187
- const raw = store.get(key);
188
- if (raw == null) return fallback;
189
- try {
190
- return JSON.parse(raw);
191
- } catch {
192
- return fallback;
225
+ get(path, options) {
226
+ return this.request("GET", path, options);
193
227
  }
194
- }
228
+ post(path, body, options) {
229
+ return this.request("POST", path, { body, ...options });
230
+ }
231
+ /** Multipart POST (file uploads). No Content-Type header: the browser adds it with the boundary. */
232
+ postForm(path, form, options) {
233
+ return this.request("POST", path, { body: form, ...options });
234
+ }
235
+ /**
236
+ * POST that may answer with server-sent events. Each event is passed to `onEvent`; a JSON answer is
237
+ * returned instead. Scout replies can take a while, so there is no overall timeout: the response
238
+ * must start within 30 s and then never go quiet for more than 60 s (the server sends keepalives),
239
+ * otherwise the request is aborted and rejects.
240
+ */
241
+ async stream(path, body, onEvent, headers = {}) {
242
+ const f = this.fetcher;
243
+ if (!f) throw new Error("fetch is not available");
244
+ const ac = typeof AbortController !== "undefined" ? new AbortController() : null;
245
+ let timer = null;
246
+ const arm = (ms) => {
247
+ if (!ac) return;
248
+ if (timer) clearTimeout(timer);
249
+ timer = setTimeout(() => ac.abort(), ms);
250
+ };
251
+ arm(STREAM_RESPONSE_TIMEOUT_MS);
252
+ try {
253
+ let res;
254
+ try {
255
+ res = await f(this.url(path), {
256
+ method: "POST",
257
+ headers: { ...headers, "X-Guidely-Key": this.key, Accept: "text/event-stream, application/json", "Content-Type": "application/json" },
258
+ body: JSON.stringify(body),
259
+ signal: ac?.signal,
260
+ credentials: "omit",
261
+ mode: "cors"
262
+ });
263
+ } catch (err) {
264
+ this.observe(null);
265
+ throw err;
266
+ }
267
+ this.observe(res);
268
+ const type = res.headers?.get?.("content-type") ?? "";
269
+ if (!type.includes("text/event-stream") || !res.body) {
270
+ let data = null;
271
+ try {
272
+ data = await res.json();
273
+ } catch {
274
+ data = null;
275
+ }
276
+ return { status: res.status, ok: res.ok, data, etag: null };
277
+ }
278
+ arm(STREAM_IDLE_TIMEOUT_MS);
279
+ await readEventStream(res.body, onEvent, () => arm(STREAM_IDLE_TIMEOUT_MS));
280
+ return { status: res.status, ok: res.ok, data: null, etag: null };
281
+ } finally {
282
+ if (timer) clearTimeout(timer);
283
+ }
284
+ }
285
+ /** `navigator.sendBeacon` when available (page unload); returns false when it could not be queued. */
286
+ beacon(path, body) {
287
+ try {
288
+ if (typeof navigator === "undefined" || typeof navigator.sendBeacon !== "function") return false;
289
+ const blob = new Blob([JSON.stringify(body)], { type: "text/plain;charset=UTF-8" });
290
+ return navigator.sendBeacon(this.url(path), blob);
291
+ } catch {
292
+ return false;
293
+ }
294
+ }
295
+ };
296
+ var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
297
+ function backoffMs(attempt, baseMs = 1e3, maxMs = 3e4) {
298
+ return Math.min(maxMs, baseMs * 2 ** Math.max(0, attempt));
299
+ }
300
+ async function withRetry(send, attempts = 3, baseMs = 500) {
301
+ for (let i = 0; i < attempts; i++) {
302
+ try {
303
+ const res = await send();
304
+ if (res.ok || res.status >= 400 && res.status < 500 && res.status !== 429) return res;
305
+ } catch {
306
+ }
307
+ if (i < attempts - 1) await sleep(backoffMs(i, baseMs, 8e3));
308
+ }
309
+ return null;
310
+ }
311
+ function postWithRetry(api, path, body, attempts = 3, baseMs = 500) {
312
+ return withRetry(() => api.post(path, body), attempts, baseMs);
313
+ }
314
+ async function readEventStream(body, onEvent, onChunk) {
315
+ const reader = body.getReader();
316
+ const decoder = new TextDecoder();
317
+ let buffer = "";
318
+ const dispatch = (raw) => {
319
+ let event = "message";
320
+ const data = [];
321
+ for (const line of raw.split("\n")) {
322
+ if (!line || line.startsWith(":")) continue;
323
+ const i = line.indexOf(":");
324
+ const field = i < 0 ? line : line.slice(0, i);
325
+ const value = i < 0 ? "" : line.slice(i + 1).replace(/^ /, "");
326
+ if (field === "event") event = value;
327
+ else if (field === "data") data.push(value);
328
+ }
329
+ if (!data.length) return;
330
+ try {
331
+ onEvent(event, JSON.parse(data.join("\n")));
332
+ } catch {
333
+ }
334
+ };
335
+ for (; ; ) {
336
+ const { done, value } = await reader.read();
337
+ if (value) onChunk?.();
338
+ if (value) buffer += decoder.decode(value, { stream: true }).replace(/\r\n?/g, "\n");
339
+ let sep;
340
+ while ((sep = buffer.indexOf("\n\n")) >= 0) {
341
+ dispatch(buffer.slice(0, sep));
342
+ buffer = buffer.slice(sep + 2);
343
+ }
344
+ if (done) break;
345
+ }
346
+ if (buffer.trim()) dispatch(buffer);
347
+ }
348
+
349
+ // src/storage.ts
350
+ function memoryStore() {
351
+ const map = /* @__PURE__ */ new Map();
352
+ return { get: (k) => map.get(k) ?? null, set: (k, v) => void map.set(k, v), remove: (k) => void map.delete(k) };
353
+ }
354
+ function webStore(area) {
355
+ try {
356
+ if (typeof window === "undefined") return null;
357
+ const s = window[area];
358
+ if (!s) return null;
359
+ const probe = "__guidely_probe__";
360
+ s.setItem(probe, "1");
361
+ s.removeItem(probe);
362
+ return {
363
+ get: (k) => {
364
+ try {
365
+ return s.getItem(k);
366
+ } catch {
367
+ return null;
368
+ }
369
+ },
370
+ set: (k, v) => {
371
+ try {
372
+ s.setItem(k, v);
373
+ } catch {
374
+ }
375
+ },
376
+ remove: (k) => {
377
+ try {
378
+ s.removeItem(k);
379
+ } catch {
380
+ }
381
+ }
382
+ };
383
+ } catch {
384
+ return null;
385
+ }
386
+ }
387
+ function createStorage(kind = "local") {
388
+ if (kind === "memory") return memoryStore();
389
+ return webStore(kind === "local" ? "localStorage" : "sessionStorage") ?? memoryStore();
390
+ }
391
+ function readJson(store, key, fallback) {
392
+ const raw = store.get(key);
393
+ if (raw == null) return fallback;
394
+ try {
395
+ return JSON.parse(raw);
396
+ } catch {
397
+ return fallback;
398
+ }
399
+ }
195
400
  function writeJson(store, key, value) {
196
401
  try {
197
402
  store.set(key, JSON.stringify(value));
@@ -208,6 +413,8 @@ var KEYS = {
208
413
  queue: (key) => `guidely:queue:${key}`,
209
414
  /** Survey answers and guide/checklist progress not yet accepted by the server. */
210
415
  outbox: (key) => `guidely:outbox:${key}`,
416
+ /** Agent Analytics turns not yet accepted by the server. */
417
+ agentOutbox: (key) => `guidely:agents:${key}`,
211
418
  session: "guidely:session",
212
419
  active: "guidely:active",
213
420
  authoring: "guidely:authoring",
@@ -217,285 +424,261 @@ var KEYS = {
217
424
  /** Messenger conversations this browser can read: [{ id, token }]. */
218
425
  conversations: (key) => `guidely:conversations:${key}`,
219
426
  /** Announcement ids opened in the messenger's News tab. */
220
- newsSeen: (key) => `guidely:news:${key}`
427
+ newsSeen: (key) => `guidely:news:${key}`,
428
+ /** Where the visitor dragged the messenger launcher ({ side, y }). */
429
+ launcher: (key) => `guidely:launcher:${key}`
221
430
  };
222
431
 
223
- // src/authoring.ts
224
- async function loadSchemas() {
225
- const m = await import("@guidelytech/shared");
226
- return { StepSchema: m.StepSchema, GuideConfigSchema: m.GuideConfigSchema };
227
- }
228
- var AUTHORING_PARAM = "guidely_authoring";
229
- var HEARTBEAT_MS = 15e3;
230
- var SNAPSHOT_DEBOUNCE_MS = 800;
231
- var SNAPSHOT_MAX_WAIT_MS = 4e3;
232
- var AuthoringBridge = class {
432
+ // src/outbox.ts
433
+ var Outbox = class {
233
434
  constructor(opts) {
234
435
  __publicField(this, "opts", opts);
235
- __publicField(this, "token", null);
236
- __publicField(this, "info", null);
237
- __publicField(this, "session", null);
238
- __publicField(this, "heartbeat", null);
239
- __publicField(this, "preview", null);
240
- __publicField(this, "previewGuide", null);
241
- __publicField(this, "active", false);
242
- __publicField(this, "observer", null);
243
- __publicField(this, "snapshotTimer", null);
244
- __publicField(this, "snapshotDue", 0);
245
- __publicField(this, "lastSnapshot", "");
436
+ __publicField(this, "items");
437
+ __publicField(this, "sending", false);
438
+ __publicField(this, "attempt", 0);
439
+ __publicField(this, "timer", null);
440
+ __publicField(this, "destroyed", false);
441
+ __publicField(this, "offRecover");
442
+ __publicField(this, "maxItems");
443
+ __publicField(this, "maxAgeMs");
444
+ this.maxItems = opts.maxItems ?? 50;
445
+ this.maxAgeMs = opts.maxAgeMs ?? 7 * 24 * 60 * 60 * 1e3;
446
+ const stored = readJson(opts.store, opts.storageKey, []);
447
+ const cutoff = Date.now() - this.maxAgeMs;
448
+ this.items = Array.isArray(stored) ? stored.filter((i) => i && typeof i.path === "string" && typeof i.key === "string" && i.createdAt > cutoff).slice(-this.maxItems) : [];
449
+ this.offRecover = opts.health?.onRecover(() => {
450
+ this.attempt = 0;
451
+ this.retryIn(0);
452
+ }) ?? null;
453
+ if (this.items.length) this.retryIn(0);
246
454
  }
247
- isActive() {
248
- return this.active;
455
+ get size() {
456
+ return this.items.length;
249
457
  }
250
- /** Returns true when a token was found (in the URL or the session marker) and verification started. */
251
- detect() {
252
- if (typeof window === "undefined") return false;
253
- let token = null;
254
- try {
255
- const url = new URL(window.location.href);
256
- token = url.searchParams.get(AUTHORING_PARAM);
257
- if (token) {
258
- url.searchParams.delete(AUTHORING_PARAM);
259
- window.history.replaceState(window.history.state, "", url.toString());
260
- }
261
- } catch {
262
- token = null;
263
- }
264
- if (!token) {
265
- const marker = readJson(this.opts.sessionStore, KEYS.authoring, null);
266
- token = marker?.token ?? null;
267
- }
268
- if (!token) return false;
269
- this.token = token;
270
- writeJson(this.opts.sessionStore, KEYS.authoring, { token });
271
- void this.start();
272
- return true;
458
+ /** Queues `POST path` and sends it now (or as soon as the API is reachable). */
459
+ add(key, path, body) {
460
+ if (this.destroyed) return;
461
+ this.items = this.items.filter((i) => i.key !== key);
462
+ this.items.push({ key, path, body, createdAt: Date.now() });
463
+ if (this.items.length > this.maxItems) this.items.splice(0, this.items.length - this.maxItems);
464
+ this.persist();
465
+ void this.flush();
273
466
  }
274
- async start() {
275
- if (!this.token) return;
276
- try {
277
- const res = await this.opts.api.get("/authoring/session", { params: { token: this.token } });
278
- if (!res.ok || !res.data) {
279
- this.opts.sessionStore.remove(KEYS.authoring);
280
- this.opts.onError(new Error(`[Guidely] authoring session invalid or expired (${res.status})`));
281
- return;
282
- }
283
- this.info = res.data;
284
- if (Date.parse(this.info.expiresAt) < Date.now()) {
285
- this.opts.sessionStore.remove(KEYS.authoring);
286
- this.opts.onError(new Error("[Guidely] authoring session expired"));
287
- return;
288
- }
289
- this.active = true;
290
- this.opts.onActive(true);
291
- this.session = (0, import_guide_runtime.startAuthoring)({
292
- dashboardOrigin: this.info.dashboardOrigin,
293
- onPick: (pick) => this.sendPick(pick),
294
- onExit: () => this.stop()
295
- });
296
- this.heartbeat = setInterval(() => {
297
- void this.opts.api.post("/authoring/session/heartbeat", {}, { params: { token: this.token ?? void 0 } }).catch(() => {
298
- });
299
- }, HEARTBEAT_MS);
300
- this.post({ type: "guidely:authoring-ready", sessionId: this.info.id, url: window.location.href });
301
- this.watchPage();
302
- } catch (err) {
303
- this.opts.onError(err);
304
- }
305
- }
306
- masked(el) {
307
- return this.opts.maskSelectors.some((sel) => {
308
- try {
309
- return !!(0, import_guide_runtime.closestComposed)(el, sel);
310
- } catch {
311
- return false;
312
- }
313
- });
467
+ persist() {
468
+ writeJson(this.opts.store, this.opts.storageKey, this.items);
314
469
  }
315
- sendPick(raw) {
316
- if (!this.info || !this.token) return;
317
- const { element, ...rest } = raw;
318
- const pick = this.masked(element) ? { ...rest, textSnapshot: "", selectors: rest.selectors.filter((s) => s.kind !== "text") } : rest;
319
- pick.url = window.location.href;
320
- this.post({ type: "guidely:pick", sessionId: this.info.id, pick });
321
- void this.opts.api.post("/authoring/session/picks", pick, { params: { token: this.token } }).catch((err) => this.opts.onError(err));
470
+ retryIn(ms) {
471
+ if (this.destroyed) return;
472
+ if (this.timer) clearTimeout(this.timer);
473
+ this.timer = setTimeout(() => {
474
+ this.timer = null;
475
+ void this.flush();
476
+ }, ms);
322
477
  }
323
- /** Sends a snapshot now and again whenever the page settles after DOM changes or navigation. */
324
- watchPage() {
325
- this.scheduleSnapshot();
326
- if (typeof MutationObserver === "undefined") return;
327
- this.observer = new MutationObserver(() => this.scheduleSnapshot());
328
- this.observer.observe(document.documentElement, { subtree: true, childList: true, attributes: true, characterData: true });
478
+ /** Sends queued requests in order, one at a time. */
479
+ async flush() {
480
+ if (this.destroyed || this.sending || !this.items.length) return;
481
+ const wait = this.opts.health?.waitMs() ?? 0;
482
+ if (wait > 0) return this.retryIn(wait);
483
+ this.sending = true;
484
+ try {
485
+ while (this.items.length && !this.destroyed) {
486
+ const item = this.items[0];
487
+ let retry = false;
488
+ try {
489
+ const res = await this.opts.api.post(item.path, item.body);
490
+ retry = !res.ok && (res.status === 429 || res.status >= 500);
491
+ } catch (err) {
492
+ retry = true;
493
+ this.opts.onError?.(err);
494
+ }
495
+ if (retry) {
496
+ this.attempt += 1;
497
+ this.retryIn(Math.max(backoffMs(this.attempt - 1, 1e3, 6e4), this.opts.health?.waitMs() ?? 0));
498
+ return;
499
+ }
500
+ this.attempt = 0;
501
+ this.items = this.items.filter((i) => i !== item);
502
+ this.persist();
503
+ }
504
+ } finally {
505
+ this.sending = false;
506
+ }
329
507
  }
330
- scheduleSnapshot() {
331
- const now = Date.now();
332
- if (!this.snapshotTimer) this.snapshotDue = now + SNAPSHOT_MAX_WAIT_MS;
333
- else clearTimeout(this.snapshotTimer);
334
- this.snapshotTimer = setTimeout(() => this.sendSnapshot(), Math.max(0, Math.min(SNAPSHOT_DEBOUNCE_MS, this.snapshotDue - now)));
508
+ destroy() {
509
+ this.destroyed = true;
510
+ this.offRecover?.();
511
+ if (this.timer) clearTimeout(this.timer);
512
+ this.timer = null;
335
513
  }
336
- sendSnapshot() {
337
- if (this.snapshotTimer) clearTimeout(this.snapshotTimer);
338
- this.snapshotTimer = null;
339
- if (!this.info || !this.active) return;
514
+ };
515
+
516
+ // src/agent-turns.ts
517
+ var PENDING_MAX = 500;
518
+ function toolCalls(calls) {
519
+ if (!Array.isArray(calls)) return void 0;
520
+ return calls.filter((c) => !!c && typeof c.name === "string" && !!c.name).slice(0, import_runtime4.AGENT_TOOL_CALLS_PER_TURN).map((c) => ({
521
+ name: c.name.slice(0, 100),
522
+ ...c.status === "ok" || c.status === "error" ? { status: c.status } : {},
523
+ ...typeof c.latencyMs === "number" && Number.isFinite(c.latencyMs) && c.latencyMs >= 0 ? { latencyMs: Math.round(c.latencyMs) } : {}
524
+ }));
525
+ }
526
+ function prepareAgentTurn(turn, identity, redact, onError) {
527
+ if (!turn || typeof turn !== "object" || !turn.conversationId || !turn.agentId || !turn.role) return null;
528
+ let out = {
529
+ ...turn,
530
+ type: "agent.conversation",
531
+ turnId: turn.turnId || `t_${(0, import_runtime4.randomId)(20)}`,
532
+ occurredAt: turn.occurredAt || (/* @__PURE__ */ new Date()).toISOString(),
533
+ anonymousId: turn.anonymousId ?? identity.anonymousId,
534
+ userId: turn.userId !== void 0 ? turn.userId : identity.userId,
535
+ accountId: turn.accountId !== void 0 ? turn.accountId : identity.accountId,
536
+ sessionId: turn.sessionId ?? identity.sessionId
537
+ };
538
+ if (redact) {
340
539
  try {
341
- const snapshot = captureSnapshot(document, this.opts.maskSelectors);
342
- if (!snapshot || snapshot.html === this.lastSnapshot) return;
343
- this.lastSnapshot = snapshot.html;
344
- this.post({ type: "guidely:snapshot", sessionId: this.info.id, snapshot });
540
+ const r = redact({ ...out, toolCalls: out.toolCalls ? out.toolCalls.map((c) => ({ ...c })) : void 0, metadata: out.metadata ? { ...out.metadata } : void 0 });
541
+ if (!r) return null;
542
+ out = { ...r, type: "agent.conversation", conversationId: out.conversationId, agentId: out.agentId, turnId: out.turnId, role: out.role };
345
543
  } catch (err) {
346
- this.opts.onError(err);
544
+ onError?.(err);
545
+ return null;
347
546
  }
348
547
  }
349
- post(message) {
350
- if (!this.info) return;
351
- try {
352
- window.opener?.postMessage(message, this.info.dashboardOrigin);
353
- } catch {
548
+ if (typeof out.text === "string" && out.text.length > import_runtime4.AGENT_TURN_MAX_TEXT) out.text = out.text.slice(0, import_runtime4.AGENT_TURN_MAX_TEXT);
549
+ if (out.text !== void 0 && typeof out.text !== "string") out.text = String(out.text);
550
+ const calls = toolCalls(out.toolCalls);
551
+ if (calls) out.toolCalls = calls;
552
+ else delete out.toolCalls;
553
+ return out;
554
+ }
555
+ var AgentTurns = class {
556
+ constructor(opts) {
557
+ __publicField(this, "opts", opts);
558
+ __publicField(this, "outbox");
559
+ __publicField(this, "pending", []);
560
+ this.outbox = new Outbox({ api: opts.api, store: opts.store, storageKey: opts.storageKey, health: opts.health, maxItems: 20, maxAgeMs: 24 * 60 * 60 * 1e3 });
561
+ }
562
+ get queued() {
563
+ return this.outbox.size + this.pending.length;
564
+ }
565
+ /** Queues turns to send now, or (`hold`) until consent is granted. */
566
+ add(turns, hold = false) {
567
+ if (!turns.length) return;
568
+ if (hold) {
569
+ this.pending.push(...turns);
570
+ if (this.pending.length > PENDING_MAX) this.pending.splice(0, this.pending.length - PENDING_MAX);
571
+ return;
354
572
  }
355
- }
356
- stopPreview() {
357
- this.preview?.destroy();
358
- this.preview = null;
359
- this.previewGuide?.destroy();
360
- this.previewGuide = null;
361
- this.session?.setPicking(true);
362
- }
363
- /** Handles `message` events; ignores anything not from `dashboardOrigin`. */
364
- async handleMessage(e) {
365
- if (!this.active || !this.info) return;
366
- if (e.origin !== this.info.dashboardOrigin) return;
367
- const msg = e.data;
368
- if (!msg || typeof msg !== "object" || typeof msg.type !== "string") return;
369
- try {
370
- switch (msg.type) {
371
- case "guidely:preview-step": {
372
- this.stopPreview();
373
- const { StepSchema, GuideConfigSchema } = await loadSchemas();
374
- const step = StepSchema.parse(msg.step);
375
- const appearance = msg.appearance ? GuideConfigSchema.shape.appearance.parse(msg.appearance) : void 0;
376
- this.session?.setPicking(false);
377
- this.session?.setStatus(`Previewing step \u201C${step.title || step.id}\u201D \u2014 close it to continue picking`);
378
- this.preview = (0, import_guide_runtime.previewStep)(step, { ...this.opts.theme(), ...msg.theme || {} }, {
379
- appearance,
380
- onEnd: () => {
381
- this.preview = null;
382
- this.session?.setPicking(true);
383
- this.session?.setStatus("Guidely authoring \u2014 hover an element and click to select \xB7 Esc to cancel");
384
- this.post({ type: "guidely:preview-ended", sessionId: this.info.id, reason: "step" });
385
- }
386
- });
387
- break;
388
- }
389
- case "guidely:preview-guide": {
390
- this.stopPreview();
391
- const g = msg.guide;
392
- const guide = { id: g.id || "preview", key: g.key || "preview", name: g.name || "Preview", kind: g.kind || "flow", version: g.version ?? 0, config: (await loadSchemas()).GuideConfigSchema.parse(g.config) };
393
- this.session?.setPicking(false);
394
- this.session?.setStatus(`Previewing \u201C${guide.name}\u201D`);
395
- this.previewGuide = (0, import_guide_runtime.runGuide)(guide, {
396
- theme: this.opts.theme(),
397
- ctx: this.opts.ctx,
398
- onNavigate: (url, newTab) => newTab ? window.open(url, "_blank", "noopener") : window.location.assign(url),
399
- onEnd: (reason) => {
400
- this.previewGuide = null;
401
- this.session?.setPicking(true);
402
- this.post({ type: "guidely:preview-ended", sessionId: this.info.id, reason });
403
- }
404
- });
405
- this.previewGuide.start();
406
- break;
407
- }
408
- case "guidely:highlight": {
409
- const result = (0, import_guide_runtime.highlightSelectors)(Array.isArray(msg.selectors) ? msg.selectors : [], { durationMs: 3e3 });
410
- this.post({ type: "guidely:highlight-result", sessionId: this.info.id, counts: result.counts, resolved: !!result.resolved });
411
- break;
412
- }
413
- case "guidely:stop-preview":
414
- this.stopPreview();
415
- break;
416
- case "guidely:request-snapshot":
417
- this.lastSnapshot = "";
418
- this.sendSnapshot();
419
- break;
420
- case "guidely:ping":
421
- this.post({ type: "guidely:pong", sessionId: this.info.id, url: window.location.href });
422
- break;
423
- case "guidely:exit":
424
- this.stop();
425
- break;
426
- default:
427
- break;
428
- }
429
- } catch (err) {
430
- this.opts.onError(err);
573
+ for (let i = 0; i < turns.length; i += import_runtime4.AGENT_TURNS_PER_BATCH) {
574
+ const batch = turns.slice(i, i + import_runtime4.AGENT_TURNS_PER_BATCH);
575
+ this.outbox.add(`agent:${batch[0].turnId}`, "/agent-events", { turns: batch, sdk: this.opts.sdk });
431
576
  }
432
577
  }
433
- /** Leaves authoring mode and clears the session marker. */
434
- stop() {
435
- if (!this.active) return;
436
- this.active = false;
437
- this.stopPreview();
438
- this.observer?.disconnect();
439
- this.observer = null;
440
- if (this.snapshotTimer) clearTimeout(this.snapshotTimer);
441
- this.snapshotTimer = null;
442
- if (this.heartbeat) clearInterval(this.heartbeat);
443
- this.heartbeat = null;
444
- this.session?.stop();
445
- this.session = null;
446
- this.opts.sessionStore.remove(KEYS.authoring);
447
- if (this.info) this.post({ type: "guidely:exit", sessionId: this.info.id });
448
- this.opts.onActive(false);
578
+ /** Consent changed: granted sends what was held, anything else drops it. */
579
+ consentChanged(granted) {
580
+ const held = this.pending.splice(0);
581
+ if (granted) this.add(held);
582
+ }
583
+ destroy() {
584
+ this.pending = [];
585
+ this.outbox.destroy();
449
586
  }
450
587
  };
451
588
 
452
- // src/capture-token.ts
453
- var import_runtime2 = require("@guidelytech/shared/runtime");
454
- function takeCaptureToken(sessionStore) {
455
- if (typeof window === "undefined") return null;
456
- let token = null;
457
- let authoringLink = false;
589
+ // src/authoring.ts
590
+ var import_guide_runtime3 = require("@guidelytech/guide-runtime");
591
+
592
+ // src/snapshot.ts
593
+ var MAX_SNAPSHOT_BYTES = 6 * 1024 * 1024;
594
+ var STRIP = "script, noscript, template, #guidely-root, [data-guidely], meta[http-equiv], link[rel~='preload'], link[rel~='modulepreload'], link[rel~='prefetch'], link[rel~='manifest'], link[rel~='serviceworker']";
595
+ function rulesText(sheet) {
596
+ if (!sheet) return null;
458
597
  try {
459
- const url = new URL(window.location.href);
460
- token = url.searchParams.get(import_runtime2.CAPTURE_PARAM);
461
- authoringLink = url.searchParams.has(AUTHORING_PARAM);
462
- if (token) {
463
- url.searchParams.delete(import_runtime2.CAPTURE_PARAM);
464
- window.history.replaceState(window.history.state, "", url.toString());
465
- }
598
+ return Array.from(sheet.cssRules, (r) => r.cssText).join("\n");
466
599
  } catch {
467
- token = null;
468
- }
469
- const saved = readJson(sessionStore, KEYS.capture, null);
470
- let marker = null;
471
- if (token) marker = saved?.token === token ? saved : { token };
472
- else if (saved?.token && !authoringLink) marker = saved;
473
- if (!marker) {
474
- if (saved) {
475
- sessionStore.remove(KEYS.capture);
476
- sessionStore.remove(KEYS.capturePending);
477
- }
478
600
  return null;
479
601
  }
480
- if (token && saved?.token !== token) sessionStore.remove(KEYS.capturePending);
481
- writeJson(sessionStore, KEYS.capture, marker);
482
- sessionStore.remove(KEYS.authoring);
483
- return marker;
484
602
  }
485
-
486
- // src/load-capture.ts
487
- var loadCapture = async () => (await import("./capture.cjs")).capture;
488
-
489
- // src/autocapture.ts
490
- var import_guide_runtime2 = require("@guidelytech/guide-runtime");
491
- var INTERACTIVE_SELECTOR = "a,button,input[type=button],input[type=submit],input[type=reset],input[type=checkbox],input[type=radio],select,summary,label,[role=button],[role=link],[role=tab],[role=menuitem],[role=checkbox],[role=switch],[role=option],[onclick],[data-guidely-track]";
492
- var DEFAULT_MASK_SELECTORS = ["[data-guidely-mask]", ".guidely-mask"];
493
- var PREFERRED_ATTRS = ["data-testid", "data-test-id", "data-test", "data-cy", "data-qa", "data-guidely-track", "data-tour"];
494
- var NOISY_DATA_ATTR = /^data-(state|v-|react|radix|headlessui|svelte|orientation|highlighted|focus|key|index|side|align|disabled|selected|active|open|guidely-(mask|ignore|block))/i;
495
- function validSelectors(list) {
496
- const out = [];
497
- const probe = typeof document !== "undefined" ? document.createDocumentFragment() : null;
498
- for (const raw of list) {
603
+ function maskText(root) {
604
+ const walker = root.ownerDocument.createTreeWalker(root, NodeFilter.SHOW_TEXT);
605
+ for (let n = walker.nextNode(); n; n = walker.nextNode()) {
606
+ n.textContent = (n.textContent ?? "").replace(/\S/g, "\u2022");
607
+ }
608
+ }
609
+ function captureSnapshot(doc, maskSelectors, maxBytes = MAX_SNAPSHOT_BYTES) {
610
+ const win = doc.defaultView;
611
+ if (!win || !doc.documentElement) return null;
612
+ const clone = doc.documentElement.cloneNode(true);
613
+ const origStyles = doc.documentElement.querySelectorAll("style");
614
+ clone.querySelectorAll("style").forEach((el, i) => {
615
+ const css = rulesText(origStyles[i]?.sheet);
616
+ if (css !== null && css.length > (el.textContent ?? "").length) el.textContent = css;
617
+ });
618
+ const origFrames = doc.documentElement.querySelectorAll("iframe, canvas, video, embed, object");
619
+ clone.querySelectorAll("iframe, canvas, video, embed, object").forEach((el, i) => {
620
+ const r = origFrames[i]?.getBoundingClientRect();
621
+ const box = doc.createElement("div");
622
+ box.setAttribute("style", `${el.getAttribute("style") ?? ""};width:${r?.width ?? 0}px;height:${r?.height ?? 0}px;background:#e2e8f0;border-radius:4px`);
623
+ if (el.getAttribute("class")) box.setAttribute("class", el.getAttribute("class"));
624
+ el.replaceWith(box);
625
+ });
626
+ clone.querySelectorAll(STRIP).forEach((el) => el.remove());
627
+ for (const sel of maskSelectors) {
628
+ try {
629
+ clone.querySelectorAll(sel).forEach(maskText);
630
+ } catch {
631
+ }
632
+ }
633
+ clone.querySelectorAll("*").forEach((el) => {
634
+ for (const attr2 of Array.from(el.attributes)) {
635
+ const name = attr2.name.toLowerCase();
636
+ if (name.startsWith("on") || (name === "href" || name === "src" || name === "action") && /^\s*javascript:/i.test(attr2.value)) el.removeAttribute(attr2.name);
637
+ }
638
+ if (el instanceof HTMLInputElement) {
639
+ el.removeAttribute("value");
640
+ el.removeAttribute("checked");
641
+ } else if (el instanceof HTMLTextAreaElement) {
642
+ el.textContent = "";
643
+ }
644
+ el.removeAttribute("autofocus");
645
+ });
646
+ const adopted = Array.from(doc.adoptedStyleSheets ?? []).map(rulesText).filter(Boolean).join("\n");
647
+ let head = clone.querySelector("head");
648
+ if (!head) {
649
+ head = doc.createElement("head");
650
+ clone.prepend(head);
651
+ }
652
+ if (adopted) {
653
+ const style = doc.createElement("style");
654
+ style.textContent = adopted;
655
+ head.append(style);
656
+ }
657
+ clone.querySelectorAll("base").forEach((el) => el.remove());
658
+ const base = doc.createElement("base");
659
+ base.setAttribute("href", doc.baseURI || win.location.href);
660
+ base.setAttribute("target", "_blank");
661
+ head.prepend(base);
662
+ const html = `<!DOCTYPE html>
663
+ ${clone.outerHTML}`;
664
+ if (html.length > maxBytes) return null;
665
+ return { html, url: win.location.href, title: doc.title, width: win.innerWidth, height: win.innerHeight, capturedAt: (/* @__PURE__ */ new Date()).toISOString() };
666
+ }
667
+
668
+ // src/tagging.ts
669
+ var import_runtime7 = require("@guidelytech/shared/runtime");
670
+ var import_guide_runtime2 = require("@guidelytech/guide-runtime");
671
+
672
+ // src/autocapture.ts
673
+ var import_guide_runtime = require("@guidelytech/guide-runtime");
674
+ var INTERACTIVE_SELECTOR = "a,button,input[type=button],input[type=submit],input[type=reset],input[type=checkbox],input[type=radio],select,summary,label,[role=button],[role=link],[role=tab],[role=menuitem],[role=checkbox],[role=switch],[role=option],[onclick],[data-guidely-track]";
675
+ var DEFAULT_MASK_SELECTORS = ["[data-guidely-mask]", ".guidely-mask"];
676
+ var PREFERRED_ATTRS = ["data-testid", "data-test-id", "data-test", "data-cy", "data-qa", "data-guidely-track", "data-tour"];
677
+ var NOISY_DATA_ATTR = /^data-(state|v-|react|radix|headlessui|svelte|orientation|highlighted|focus|key|index|side|align|disabled|selected|active|open|guidely-(mask|ignore|block))/i;
678
+ function validSelectors(list) {
679
+ const out = [];
680
+ const probe = typeof document !== "undefined" ? document.createDocumentFragment() : null;
681
+ for (const raw of list) {
499
682
  const s = (raw || "").trim();
500
683
  if (!s || out.includes(s)) continue;
501
684
  try {
@@ -507,11 +690,11 @@ function validSelectors(list) {
507
690
  return out;
508
691
  }
509
692
  function isIgnoredElement(el) {
510
- return (0, import_guide_runtime2.isGuidelyElement)(el) || !!(0, import_guide_runtime2.closestComposed)(el, "[data-guidely-ignore]");
693
+ return (0, import_guide_runtime.isGuidelyElement)(el) || !!(0, import_guide_runtime.closestComposed)(el, "[data-guidely-ignore]");
511
694
  }
512
695
  function interactiveElement(target) {
513
696
  try {
514
- return (0, import_guide_runtime2.closestComposed)(target, INTERACTIVE_SELECTOR);
697
+ return (0, import_guide_runtime.closestComposed)(target, INTERACTIVE_SELECTOR);
515
698
  } catch {
516
699
  return null;
517
700
  }
@@ -523,12 +706,12 @@ function esc(value) {
523
706
  var attr = (tag, name, value) => value ? `${tag}[${name}="${value.replace(/["\\]/g, "\\$&")}"]` : `${tag}[${name}]`;
524
707
  function elementSelector(el) {
525
708
  const own = ownSelector(el);
526
- const chain = (0, import_guide_runtime2.shadowHostChain)(el);
527
- return (chain ? chain + import_guide_runtime2.SHADOW_SEPARATOR + own : own).slice(0, 300);
709
+ const chain = (0, import_guide_runtime.shadowHostChain)(el);
710
+ return (chain ? chain + import_guide_runtime.SHADOW_SEPARATOR + own : own).slice(0, 300);
528
711
  }
529
712
  function ownSelector(el) {
530
713
  const tag = el.tagName.toLowerCase();
531
- if (el.id && (0, import_guide_runtime2.isStableId)(el.id)) return `#${esc(el.id)}`;
714
+ if (el.id && (0, import_guide_runtime.isStableId)(el.id)) return `#${esc(el.id)}`;
532
715
  for (const name of PREFERRED_ATTRS) if (el.hasAttribute(name)) return attr(tag, name, el.getAttribute(name) || "");
533
716
  for (const a of Array.from(el.attributes)) {
534
717
  if (a.name.startsWith("data-") && !NOISY_DATA_ATTR.test(a.name) && a.value && a.value.length <= 60) return attr(tag, a.name, a.value);
@@ -540,7 +723,7 @@ function ownSelector(el) {
540
723
  while (cur && parts.length < 4) {
541
724
  const t = cur.tagName.toLowerCase();
542
725
  if (t === "html" || t === "body") break;
543
- if (cur !== el && cur.id && (0, import_guide_runtime2.isStableId)(cur.id)) {
726
+ if (cur !== el && cur.id && (0, import_guide_runtime.isStableId)(cur.id)) {
544
727
  parts.unshift(`#${esc(cur.id)}`);
545
728
  break;
546
729
  }
@@ -555,7 +738,7 @@ function ownSelector(el) {
555
738
  function matches(el, selector, deep) {
556
739
  if (!selector) return false;
557
740
  try {
558
- return !!(0, import_guide_runtime2.closestComposed)(el, selector) || deep && !!el.querySelector(selector);
741
+ return !!(0, import_guide_runtime.closestComposed)(el, selector) || deep && !!el.querySelector(selector);
559
742
  } catch {
560
743
  return false;
561
744
  }
@@ -573,7 +756,7 @@ function elementText(el, mask, maskAll) {
573
756
  return text ? text.slice(0, 100) : void 0;
574
757
  }
575
758
  function linkHref(el) {
576
- const a = (0, import_guide_runtime2.closestComposed)(el, "a[href]");
759
+ const a = (0, import_guide_runtime.closestComposed)(el, "a[href]");
577
760
  if (!a) return void 0;
578
761
  try {
579
762
  const u = new URL(a.getAttribute("href") || "", location.href);
@@ -619,564 +802,713 @@ var RageClickDetector = class {
619
802
  }
620
803
  };
621
804
 
622
- // src/context.ts
623
- function detectDevice(ua, width) {
624
- const u = ua || "";
625
- let type = "desktop";
626
- if (/iPad|Tablet|PlayBook|Silk/i.test(u) || /Android/i.test(u) && !/Mobile/i.test(u)) type = "tablet";
627
- else if (/Mobi|iPhone|iPod|Android.*Mobile|Windows Phone/i.test(u)) type = "mobile";
628
- else if (width > 0 && width < 768) type = "mobile";
629
- else if (width > 0 && width < 1024) type = "tablet";
630
- let browser;
631
- if (/Edg\//i.test(u)) browser = "edge";
632
- else if (/OPR\/|Opera/i.test(u)) browser = "opera";
633
- else if (/SamsungBrowser/i.test(u)) browser = "samsung";
634
- else if (/Chrome\/|CriOS/i.test(u)) browser = "chrome";
635
- else if (/Firefox\/|FxiOS/i.test(u)) browser = "firefox";
636
- else if (/Safari\//i.test(u) && /Version\//i.test(u)) browser = "safari";
637
- else if (/MSIE|Trident/i.test(u)) browser = "ie";
638
- let os;
639
- if (/Windows/i.test(u)) os = "windows";
640
- else if (/iPhone|iPad|iPod/i.test(u)) os = "ios";
641
- else if (/Mac OS X|Macintosh/i.test(u)) os = "macos";
642
- else if (/Android/i.test(u)) os = "android";
643
- else if (/CrOS/i.test(u)) os = "chromeos";
644
- else if (/Linux/i.test(u)) os = "linux";
645
- return { type, browser, os };
805
+ // src/tagging.ts
806
+ var TEXT_ATTRS = /* @__PURE__ */ new Set(["aria-label", "title", "alt"]);
807
+ var SAFE_ATTRS = ["role", "name", "type", "aria-label", "title", "alt", "for"];
808
+ var STATE_CLASS = /^(is-|has-)?(active|hover|hovered|focus|focused|focus-visible|selected|open|opened|closed|disabled|visible|hidden|show|shown|collapsed|expanded|checked|pressed|loading|current)$/i;
809
+ function nthOfType(el) {
810
+ const parent = el.parentElement;
811
+ if (!parent) return void 0;
812
+ let n = 0;
813
+ for (const c of Array.from(parent.children)) {
814
+ if (c.tagName === el.tagName) n++;
815
+ if (c === el) return n;
816
+ }
817
+ return void 0;
646
818
  }
647
- function buildContext(input) {
648
- let path = "";
649
- const query = {};
819
+ function nodeOf(el, masked, featureAttribute) {
820
+ const node = { t: el.tagName.toLowerCase().slice(0, import_runtime7.SNAPSHOT_LIMITS.tag) };
821
+ if (el.id && (0, import_guide_runtime2.isStableId)(el.id) && el.id.length <= import_runtime7.SNAPSHOT_LIMITS.id) node.id = el.id;
822
+ const classes = Array.from(el.classList).filter((c) => (0, import_guide_runtime2.isStableClass)(c) && !STATE_CLASS.test(c) && c.length <= import_runtime7.SNAPSHOT_LIMITS.className);
823
+ if (classes.length) node.c = classes.slice(0, import_runtime7.SNAPSHOT_LIMITS.classes);
824
+ const a = {};
825
+ const put = (name, value) => {
826
+ if (value === null || Object.keys(a).length >= import_runtime7.SNAPSHOT_LIMITS.attrs || name in a) return;
827
+ if (masked && TEXT_ATTRS.has(name)) return;
828
+ a[name] = value.slice(0, import_runtime7.SNAPSHOT_LIMITS.attrValue);
829
+ };
830
+ if (featureAttribute && el.hasAttribute(featureAttribute)) put(featureAttribute, el.getAttribute(featureAttribute));
831
+ for (const attr2 of Array.from(el.attributes)) {
832
+ if (attr2.name.startsWith("data-") && !import_runtime7.NOISY_DATA_ATTR.test(attr2.name) && attr2.value.length <= import_runtime7.SNAPSHOT_LIMITS.attrValue) put(attr2.name, attr2.value);
833
+ }
834
+ for (const name of SAFE_ATTRS) if (el.hasAttribute(name)) put(name, el.getAttribute(name));
835
+ if (Object.keys(a).length) node.a = a;
836
+ const n = nthOfType(el);
837
+ if (n) node.n = n;
838
+ return node;
839
+ }
840
+ function elementSnapshot(el, opts) {
841
+ const masked = opts.maskAll || isMasked(el, opts.mask);
842
+ const snap = nodeOf(el, masked, opts.featureAttribute);
843
+ const p = [];
844
+ for (let cur = el.parentElement; cur && p.length < import_runtime7.SNAPSHOT_LIMITS.ancestors; cur = cur.parentElement) {
845
+ if (cur.tagName === "BODY" || cur.tagName === "HTML") break;
846
+ p.push(nodeOf(cur, masked, opts.featureAttribute));
847
+ }
848
+ if (p.length) snap.p = p;
849
+ const h = [];
850
+ let root = el.getRootNode?.();
851
+ while (root && root.nodeType === 11 && root.host && h.length < import_runtime7.SNAPSHOT_LIMITS.hosts) {
852
+ h.push(nodeOf(root.host, masked, opts.featureAttribute));
853
+ root = root.host.getRootNode?.();
854
+ }
855
+ if (h.length) snap.h = h;
856
+ return snap;
857
+ }
858
+ function isMasked(el, mask) {
859
+ if (!mask) return false;
650
860
  try {
651
- const u = new URL(input.url, "http://localhost");
652
- path = u.pathname;
653
- u.searchParams.forEach((v, k) => {
654
- query[k] = v;
655
- });
861
+ return !!(0, import_guide_runtime2.closestComposed)(el, mask);
656
862
  } catch {
657
- path = input.url;
863
+ return false;
658
864
  }
659
- const device = input.device ?? (typeof navigator !== "undefined" ? detectDevice(navigator.userAgent, typeof window !== "undefined" ? window.innerWidth : 0) : { type: "desktop" });
660
- return {
661
- anonymousId: input.anonymousId,
662
- userId: input.userId ?? null,
663
- userAttributes: input.userAttributes,
664
- accountAttributes: input.accountAttributes,
665
- url: input.url,
666
- path,
667
- query,
668
- device,
669
- firstVisit: input.firstVisit,
670
- eventsSeen: new Set(input.eventsSeen),
671
- completedGuides: new Set(input.completedGuides),
672
- dismissedGuides: new Set(input.dismissedGuides),
673
- seenGuides: new Set(input.seenGuides),
674
- completedChecklists: new Set(input.completedChecklists),
675
- audienceIds: new Set(input.audienceIds),
676
- now: input.now ?? /* @__PURE__ */ new Date(),
677
- environment: input.environment,
678
- locale: input.locale ?? (typeof navigator !== "undefined" ? navigator.language : void 0),
679
- platform: input.platform ?? "web",
680
- ...input.screen !== void 0 ? { screen: input.screen } : {},
681
- ...input.appVersion !== void 0 ? { appVersion: input.appVersion } : {}
682
- };
683
865
  }
684
-
685
- // src/heatmap.ts
686
- var HEATMAP_MAX_POINTS = 40;
687
- var HEATMAP_MAX_BYTES = 6e3;
688
- var HEATMAP_MAX_BINS = 60;
689
- var r2 = (n) => Math.round(Math.min(1, Math.max(0, n)) * 100) / 100;
690
- function heatmapPoint(el, e, rage) {
691
- const p = { k: rage ? "r" : "c", x: Math.round(e.pageX ?? 0) || 0, y: Math.round(e.pageY ?? 0) || 0 };
692
- const s = elementSelector(el).slice(0, 120);
693
- if (s) p.s = s;
694
- const rect = el.getBoundingClientRect();
695
- if (rect.width > 0 && rect.height > 0) {
696
- p.rx = r2((e.clientX - rect.left) / rect.width);
697
- p.ry = r2((e.clientY - rect.top) / rect.height);
866
+ function featureAttributeOf(target, attribute) {
867
+ if (!attribute) return null;
868
+ try {
869
+ const el = (0, import_guide_runtime2.closestComposed)(target, `[${attribute}]`);
870
+ const value = el?.getAttribute(attribute)?.replace(/\s+/g, " ").trim();
871
+ return el && value ? { el, value: value.slice(0, 100) } : null;
872
+ } catch {
873
+ return null;
698
874
  }
699
- return p;
700
- }
701
- var selectors = /* @__PURE__ */ new WeakMap();
702
- var r1 = (n) => Math.round(Math.min(1, Math.max(0, n)) * 10) / 10;
703
- function movementBin(el, e) {
704
- const p = { k: "m", x: Math.round(e.pageX ?? 0) || 0, y: Math.round(e.pageY ?? 0) || 0 };
705
- let s = /^(HTML|BODY)$/.test(el.tagName) ? "" : selectors.get(el);
706
- if (s === void 0) selectors.set(el, s = elementSelector(el).slice(0, 120));
707
- const rect = s ? el.getBoundingClientRect() : null;
708
- if (!rect || !(rect.width > 0 && rect.height > 0)) return [`${p.x >> 4},${p.y >> 4}`, p];
709
- p.s = s;
710
- p.rx = r1((e.clientX - rect.left) / rect.width);
711
- p.ry = r1((e.clientY - rect.top) / rect.height);
712
- return [`${s} ${p.rx},${p.ry}`, p];
713
875
  }
714
- var HeatmapBuffer = class {
715
- constructor() {
716
- /** The page the buffered points belong to. */
717
- __publicField(this, "url", "");
718
- __publicField(this, "points", []);
719
- __publicField(this, "bins", /* @__PURE__ */ new Map());
720
- __publicField(this, "bytes", 0);
721
- }
722
- /** Adds a click (`burst` > 0 marks it and the burst's previous clicks as rage); true when the batch is full. */
723
- add(url, point, burst) {
724
- this.url = url;
725
- for (let i = Math.max(0, this.points.length - burst + 1); i < this.points.length; i++) this.points[i].k = "r";
726
- this.points.push(point);
727
- this.bytes += JSON.stringify(point).length + 1;
728
- return this.full();
729
- }
730
- /** Adds a movement sample to its bin (weight +1); true when the batch is full. */
731
- move(url, [key, point]) {
732
- this.url = url;
733
- const bin = this.bins.get(key);
734
- if (bin) bin.w = (bin.w ?? 1) + 1;
735
- else {
736
- this.bins.set(key, point);
737
- this.bytes += JSON.stringify(point).length + 8;
876
+ function readProperty(def, base, opts) {
877
+ let el = base;
878
+ if (def.selector) {
879
+ try {
880
+ el = (0, import_guide_runtime2.querySelectorStrategy)({ kind: "css", value: def.selector }, document)[0] ?? null;
881
+ } catch {
882
+ el = null;
738
883
  }
739
- return this.full();
740
- }
741
- full() {
742
- return this.points.length >= HEATMAP_MAX_POINTS || this.bins.size > HEATMAP_MAX_BINS || this.bytes >= HEATMAP_MAX_BYTES;
743
884
  }
744
- /** Empties the buffer, returning what was in it (clicks first, then movement bins). */
745
- take() {
746
- const points = [...this.points, ...this.bins.values()];
747
- this.points = [];
748
- this.bins = /* @__PURE__ */ new Map();
749
- this.bytes = 0;
750
- return points.length ? { url: this.url, points } : null;
885
+ if (!el) return void 0;
886
+ if (def.source === "attribute") {
887
+ const name = (def.attribute ?? "").toLowerCase();
888
+ if (!name || name === "value" || name.startsWith("on")) return void 0;
889
+ if (opts.maskAll || isMasked(el, opts.mask)) return void 0;
890
+ const v = el.getAttribute(name);
891
+ return v ? v.replace(/\s+/g, " ").trim().slice(0, import_runtime7.TAG_PROPERTY_LIMITS.value) || void 0 : void 0;
751
892
  }
752
- };
753
-
754
- // src/errors.ts
755
- var import_runtime5 = require("@guidelytech/shared/runtime");
756
- var MAX_FRAMES_BYTES = 6e3;
757
- function fitFrames(frames) {
758
- const out = frames.slice(0, 30);
759
- while (out.length > 1 && JSON.stringify(out).length > MAX_FRAMES_BYTES) out.pop();
760
- return out;
893
+ return elementText(el, opts.mask, opts.maskAll);
761
894
  }
762
- function describe(value) {
763
- if (typeof value === "string") return value;
895
+ function clickPropertyValues(config, el, click, opts) {
896
+ if (!config?.properties.length) return void 0;
897
+ const out = {};
898
+ const css = (selector) => {
899
+ try {
900
+ return !!(0, import_guide_runtime2.closestComposed)(el, selector);
901
+ } catch {
902
+ return false;
903
+ }
904
+ };
905
+ const here = { url: location.href, path: location.pathname, title: document.title };
906
+ for (const tag of config.properties) {
907
+ if (tag.kind !== "feature" || !tag.featureRules) continue;
908
+ const rules = tag.featureRules;
909
+ const matches2 = (r) => (0, import_runtime7.featureRuleMatchesClick)(r, click, css) && (!r.page?.pattern || (0, import_runtime7.pageRuleMatches)({ type: "url", value: r.page.pattern }, here));
910
+ if (!rules.some((r) => !r.exclude && matches2(r)) || rules.some((r) => r.exclude && matches2(r))) continue;
911
+ for (const def of tag.properties) {
912
+ const v = readProperty(def, el, opts);
913
+ if (v) out[def.id] = v;
914
+ }
915
+ }
916
+ return Object.keys(out).length ? out : void 0;
917
+ }
918
+ function pagePropertyValues(config, url, opts) {
919
+ if (!config?.properties.length) return void 0;
920
+ const out = {};
921
+ let path = "/";
764
922
  try {
765
- return JSON.stringify(value) ?? String(value);
923
+ path = new URL(url, location.href).pathname;
766
924
  } catch {
767
- return String(value);
768
925
  }
926
+ const ctx = { url, path, title: document.title };
927
+ for (const tag of config.properties) {
928
+ if (tag.kind !== "page" || !tag.pageRules || !(0, import_runtime7.pageRulesMatch)(tag.pageRules, ctx)) continue;
929
+ for (const def of tag.properties) {
930
+ if (!def.selector) continue;
931
+ const v = readProperty(def, null, opts);
932
+ if (v) out[def.id] = v;
933
+ }
934
+ }
935
+ return Object.keys(out).length ? out : void 0;
769
936
  }
770
- function exceptionFromError(error, source, handled, fallbackType = "Error") {
771
- const isError = error instanceof Error || !!error && typeof error === "object" && typeof error.message === "string" && typeof error.stack === "string";
772
- const e = error;
773
- return {
774
- type: isError && e.name || fallbackType,
775
- message: (isError ? e.message : describe(error)).slice(0, 1e3),
776
- frames: isError ? fitFrames((0, import_runtime5.parseStack)(e.stack)) : [],
777
- source,
778
- handled
779
- };
780
- }
781
- function errorEventToException(e) {
782
- const target = e.target;
783
- if (target && typeof Element !== "undefined" && target instanceof Element) return null;
784
- if (e.error != null) return exceptionFromError(e.error, "onerror", false);
785
- const message = e.message || "";
786
- if (!message || /^script error\.?$/i.test(message.trim())) return null;
787
- const m = /^(?:Uncaught )?([A-Z]\w*(?:Error|Exception)):\s*([\s\S]*)$/.exec(message);
788
- return {
789
- type: m?.[1] ?? "Error",
790
- message: (m?.[2] ?? message).slice(0, 1e3),
791
- frames: e.filename ? [{ file: e.filename.slice(0, 500), line: e.lineno || void 0, col: e.colno || void 0 }] : [],
792
- source: "onerror",
793
- handled: false
794
- };
937
+
938
+ // src/authoring.ts
939
+ async function loadSchemas() {
940
+ const m = await import("@guidelytech/shared");
941
+ return { StepSchema: m.StepSchema, GuideConfigSchema: m.GuideConfigSchema };
795
942
  }
796
- function rejectionToException(e) {
797
- return exceptionFromError(e.reason, "unhandledrejection", false, "UnhandledRejection");
798
- }
799
- var GUIDELY_SCRIPT = /\/guidely(-recorder)?(\.min)?\.js$/i;
800
- function isGuidelyException(props) {
801
- if (props.message.startsWith("[Guidely]")) return true;
802
- const file = props.frames[0]?.file;
803
- if (!file) return false;
804
- try {
805
- return GUIDELY_SCRIPT.test(new URL(file).pathname);
806
- } catch {
807
- return GUIDELY_SCRIPT.test(file.replace(/[?#].*$/, ""));
943
+ var AUTHORING_PARAM = "guidely_authoring";
944
+ var HEARTBEAT_MS = 15e3;
945
+ var SNAPSHOT_DEBOUNCE_MS = 800;
946
+ var SNAPSHOT_MAX_WAIT_MS = 4e3;
947
+ var AuthoringBridge = class {
948
+ constructor(opts) {
949
+ __publicField(this, "opts", opts);
950
+ __publicField(this, "token", null);
951
+ __publicField(this, "info", null);
952
+ __publicField(this, "session", null);
953
+ __publicField(this, "heartbeat", null);
954
+ __publicField(this, "preview", null);
955
+ __publicField(this, "previewGuide", null);
956
+ __publicField(this, "active", false);
957
+ __publicField(this, "observer", null);
958
+ __publicField(this, "snapshotTimer", null);
959
+ __publicField(this, "snapshotDue", 0);
960
+ __publicField(this, "lastSnapshot", "");
808
961
  }
809
- }
810
- var ExceptionThrottle = class {
811
- constructor(max = 5) {
812
- __publicField(this, "max", max);
813
- __publicField(this, "seen", /* @__PURE__ */ new Map());
962
+ isActive() {
963
+ return this.active;
814
964
  }
815
- allow(props) {
816
- const top = props.frames[0];
817
- const key = `${props.type}|${props.message}|${top ? `${top.file}:${top.line}:${top.col}` : ""}`;
818
- const n = this.seen.get(key) ?? 0;
819
- if (n >= this.max) return false;
820
- if (this.seen.size > 500) this.seen.clear();
821
- this.seen.set(key, n + 1);
965
+ /** Returns true when a token was found (in the URL or the session marker) and verification started. */
966
+ detect() {
967
+ if (typeof window === "undefined") return false;
968
+ let token = null;
969
+ try {
970
+ const url = new URL(window.location.href);
971
+ token = url.searchParams.get(AUTHORING_PARAM);
972
+ if (token) {
973
+ url.searchParams.delete(AUTHORING_PARAM);
974
+ window.history.replaceState(window.history.state, "", url.toString());
975
+ }
976
+ } catch {
977
+ token = null;
978
+ }
979
+ if (!token) {
980
+ const marker = readJson(this.opts.sessionStore, KEYS.authoring, null);
981
+ token = marker?.token ?? null;
982
+ }
983
+ if (!token) return false;
984
+ this.token = token;
985
+ writeJson(this.opts.sessionStore, KEYS.authoring, { token });
986
+ void this.start();
822
987
  return true;
823
988
  }
824
- };
825
-
826
- // src/emitter.ts
827
- var Emitter = class {
828
- constructor(onError) {
829
- __publicField(this, "onError", onError);
830
- __publicField(this, "listeners", /* @__PURE__ */ new Map());
831
- }
832
- on(event, cb) {
833
- let set = this.listeners.get(event);
834
- if (!set) {
835
- set = /* @__PURE__ */ new Set();
836
- this.listeners.set(event, set);
989
+ async start() {
990
+ if (!this.token) return;
991
+ try {
992
+ const res = await this.opts.api.get("/authoring/session", { params: { token: this.token } });
993
+ if (!res.ok || !res.data) {
994
+ this.opts.sessionStore.remove(KEYS.authoring);
995
+ this.opts.onError(new Error(`[Guidely] authoring session invalid or expired (${res.status})`));
996
+ return;
997
+ }
998
+ this.info = res.data;
999
+ if (Date.parse(this.info.expiresAt) < Date.now()) {
1000
+ this.opts.sessionStore.remove(KEYS.authoring);
1001
+ this.opts.onError(new Error("[Guidely] authoring session expired"));
1002
+ return;
1003
+ }
1004
+ this.active = true;
1005
+ this.opts.onActive(true);
1006
+ this.session = (0, import_guide_runtime3.startAuthoring)({
1007
+ dashboardOrigin: this.info.dashboardOrigin,
1008
+ // Tag mode (Pages & Features): picks become feature rules; the page itself is tagged from the dashboard.
1009
+ message: this.info.purpose === "tag" ? "Guidely tagging \u2014 click an element to tag it as a feature \xB7 tag the page from the dashboard \xB7 Esc to finish" : void 0,
1010
+ onPick: (pick) => this.sendPick(pick),
1011
+ onExit: () => this.stop()
1012
+ });
1013
+ this.heartbeat = setInterval(() => {
1014
+ void this.opts.api.post("/authoring/session/heartbeat", {}, { params: { token: this.token ?? void 0 } }).catch(() => {
1015
+ });
1016
+ }, HEARTBEAT_MS);
1017
+ this.post({ type: "guidely:authoring-ready", sessionId: this.info.id, url: window.location.href });
1018
+ this.watchPage();
1019
+ } catch (err) {
1020
+ this.opts.onError(err);
837
1021
  }
838
- set.add(cb);
839
- return () => this.off(event, cb);
840
1022
  }
841
- off(event, cb) {
842
- this.listeners.get(event)?.delete(cb);
843
- }
844
- emit(event, ...args) {
845
- const set = this.listeners.get(event);
846
- if (!set) return;
847
- for (const cb of Array.from(set)) {
1023
+ masked(el) {
1024
+ return this.opts.maskSelectors.some((sel) => {
848
1025
  try {
849
- cb(...args);
850
- } catch (err) {
851
- this.onError?.(err);
1026
+ return !!(0, import_guide_runtime3.closestComposed)(el, sel);
1027
+ } catch {
1028
+ return false;
852
1029
  }
853
- }
854
- }
855
- clear() {
856
- this.listeners.clear();
857
- }
858
- };
859
-
860
- // src/frequency.ts
861
- var HOUR = 3600 * 1e3;
862
- function isFrequencyEligible(freq, state, shownThisSession, now = Date.now()) {
863
- const s = state ?? { impressions: 0 };
864
- if (freq.maxImpressions != null && s.impressions >= freq.maxImpressions) return false;
865
- if (s.completedAt && freq.mode !== "every_time") return false;
866
- if (s.dismissedAt) {
867
- if (!freq.reshowAfterDismiss) return false;
868
- if (freq.cooldownHours > 0 && now - Date.parse(s.dismissedAt) < freq.cooldownHours * HOUR) return false;
1030
+ });
869
1031
  }
870
- switch (freq.mode) {
871
- case "once":
872
- return s.impressions === 0 || !!s.dismissedAt && freq.reshowAfterDismiss;
873
- case "once_per_session":
874
- return !shownThisSession;
875
- case "interval": {
876
- if (!s.lastShownAt) return true;
877
- const hours = freq.intervalHours ?? 24;
878
- return now - Date.parse(s.lastShownAt) >= hours * HOUR;
879
- }
880
- case "every_time":
881
- default:
882
- return true;
1032
+ sendPick(raw) {
1033
+ if (!this.info || !this.token) return;
1034
+ const { element, ...rest } = raw;
1035
+ const pick = this.masked(element) ? { ...rest, textSnapshot: "", selectors: rest.selectors.filter((s) => s.kind !== "text") } : rest;
1036
+ pick.url = window.location.href;
1037
+ if (this.info.purpose === "tag") pick.elementSnapshot = elementSnapshot(element, { mask: this.opts.maskSelectors.join(","), maskAll: false, featureAttribute: null });
1038
+ this.post({ type: "guidely:pick", sessionId: this.info.id, pick });
1039
+ void this.opts.api.post("/authoring/session/picks", pick, { params: { token: this.token } }).catch((err) => this.opts.onError(err));
883
1040
  }
884
- }
885
- function isExclusiveGuide(guide) {
886
- if (guide.kind === "banner" || guide.kind === "checklist") return false;
887
- const steps = guide.config.steps;
888
- if (steps.length > 0 && steps.every((s) => s.type === "hotspot" || s.type === "banner")) return false;
889
- return true;
890
- }
891
- function areMutuallyExclusive(a, b) {
892
- const la = a.config.frequency.mutuallyExclusiveWith;
893
- const lb = b.config.frequency.mutuallyExclusiveWith;
894
- return la.includes(b.id) || la.includes(b.key) || lb.includes(a.id) || lb.includes(a.key);
895
- }
896
- function isWithinSchedule(guide, now = Date.now()) {
897
- const s = guide.config.survey;
898
- if (!s) return true;
899
- if (s.startAt && now < Date.parse(s.startAt)) return false;
900
- if (s.endAt && now > Date.parse(s.endAt)) return false;
901
- return true;
902
- }
903
- function dismissedAfterMerge(local, server) {
904
- if (server.dismissResetAt && server.dismissResetAt !== local?.dismissResetAt) return server.dismissedAt ?? null;
905
- return local?.dismissedAt ?? server.dismissedAt ?? null;
906
- }
907
-
908
- // src/health.ts
909
- var MAX_RETRY_AFTER_MS = 60 * 60 * 1e3;
910
- var OFFLINE_WAIT_MS = 3e4;
911
- var ServerHealth = class {
912
- constructor(opts = {}) {
913
- __publicField(this, "failures", 0);
914
- __publicField(this, "openUntil", 0);
915
- __publicField(this, "lastOkAt", 0);
916
- __publicField(this, "listeners", /* @__PURE__ */ new Set());
917
- __publicField(this, "threshold");
918
- __publicField(this, "baseMs");
919
- __publicField(this, "maxMs");
920
- __publicField(this, "now");
921
- __publicField(this, "random");
922
- this.threshold = opts.threshold ?? 3;
923
- this.baseMs = opts.baseMs ?? 5e3;
924
- this.maxMs = opts.maxMs ?? 5 * 60 * 1e3;
925
- this.now = opts.now ?? Date.now;
926
- this.random = opts.random ?? Math.random;
1041
+ /** Sends a snapshot now and again whenever the page settles after DOM changes or navigation. */
1042
+ watchPage() {
1043
+ this.scheduleSnapshot();
1044
+ if (typeof MutationObserver === "undefined") return;
1045
+ this.observer = new MutationObserver(() => this.scheduleSnapshot());
1046
+ this.observer.observe(document.documentElement, { subtree: true, childList: true, attributes: true, characterData: true });
927
1047
  }
928
- /** The server answered (2xx, 3xx, or a 4xx other than 429): it is up. */
929
- success() {
930
- const wasDown = this.failures > 0 || this.openUntil > 0;
931
- this.failures = 0;
932
- this.openUntil = 0;
933
- this.lastOkAt = this.now();
934
- if (wasDown) this.notify();
1048
+ scheduleSnapshot() {
1049
+ const now = Date.now();
1050
+ if (!this.snapshotTimer) this.snapshotDue = now + SNAPSHOT_MAX_WAIT_MS;
1051
+ else clearTimeout(this.snapshotTimer);
1052
+ this.snapshotTimer = setTimeout(() => this.sendSnapshot(), Math.max(0, Math.min(SNAPSHOT_DEBOUNCE_MS, this.snapshotDue - now)));
935
1053
  }
936
- /** The request failed or the server is overloaded. `retryAfterMs` comes from a `Retry-After` header. */
937
- failure(retryAfterMs) {
938
- this.failures += 1;
939
- const now = this.now();
940
- if (retryAfterMs != null && retryAfterMs > 0) {
941
- this.openUntil = Math.max(this.openUntil, now + Math.min(retryAfterMs, MAX_RETRY_AFTER_MS));
942
- return;
1054
+ sendSnapshot() {
1055
+ if (this.snapshotTimer) clearTimeout(this.snapshotTimer);
1056
+ this.snapshotTimer = null;
1057
+ if (!this.info || !this.active) return;
1058
+ try {
1059
+ const snapshot = captureSnapshot(document, this.opts.maskSelectors);
1060
+ if (!snapshot || snapshot.html === this.lastSnapshot) return;
1061
+ this.lastSnapshot = snapshot.html;
1062
+ this.post({ type: "guidely:snapshot", sessionId: this.info.id, snapshot });
1063
+ } catch (err) {
1064
+ this.opts.onError(err);
943
1065
  }
944
- if (this.failures < this.threshold) return;
945
- const cooldown = Math.min(this.maxMs, this.baseMs * 2 ** (this.failures - this.threshold));
946
- this.openUntil = Math.max(this.openUntil, now + cooldown * (0.8 + this.random() * 0.4));
947
1066
  }
948
- /** Milliseconds background traffic should wait before trying again (0: send now). */
949
- waitMs() {
950
- if (typeof navigator !== "undefined" && navigator.onLine === false) return OFFLINE_WAIT_MS;
951
- return Math.max(0, this.openUntil - this.now());
1067
+ post(message) {
1068
+ if (!this.info) return;
1069
+ try {
1070
+ window.opener?.postMessage(message, this.info.dashboardOrigin);
1071
+ } catch {
1072
+ }
952
1073
  }
953
- canSend() {
954
- return this.waitMs() === 0;
1074
+ stopPreview() {
1075
+ this.preview?.destroy();
1076
+ this.preview = null;
1077
+ this.previewGuide?.destroy();
1078
+ this.previewGuide = null;
1079
+ this.session?.setPicking(true);
955
1080
  }
956
- /**
957
- * The last request got an answer. Unload beacons can't report back, so what they carry is only
958
- * forgotten while this holds; otherwise it stays queued and is sent again (the server drops duplicates).
959
- */
960
- get reachable() {
961
- return this.failures === 0 && this.lastOkAt > 0;
1081
+ /** Handles `message` events; ignores anything not from `dashboardOrigin`. */
1082
+ async handleMessage(e) {
1083
+ if (!this.active || !this.info) return;
1084
+ if (e.origin !== this.info.dashboardOrigin) return;
1085
+ const msg = e.data;
1086
+ if (!msg || typeof msg !== "object" || typeof msg.type !== "string") return;
1087
+ try {
1088
+ switch (msg.type) {
1089
+ case "guidely:preview-step": {
1090
+ this.stopPreview();
1091
+ const { StepSchema, GuideConfigSchema } = await loadSchemas();
1092
+ const step = StepSchema.parse(msg.step);
1093
+ const appearance = msg.appearance ? GuideConfigSchema.shape.appearance.parse(msg.appearance) : void 0;
1094
+ this.session?.setPicking(false);
1095
+ this.session?.setStatus(`Previewing step \u201C${step.title || step.id}\u201D \u2014 close it to continue picking`);
1096
+ this.preview = (0, import_guide_runtime3.previewStep)(step, { ...this.opts.theme(), ...msg.theme || {} }, {
1097
+ appearance,
1098
+ onEnd: () => {
1099
+ this.preview = null;
1100
+ this.session?.setPicking(true);
1101
+ this.session?.setStatus("Guidely authoring \u2014 hover an element and click to select \xB7 Esc to cancel");
1102
+ this.post({ type: "guidely:preview-ended", sessionId: this.info.id, reason: "step" });
1103
+ }
1104
+ });
1105
+ break;
1106
+ }
1107
+ case "guidely:preview-guide": {
1108
+ this.stopPreview();
1109
+ const g = msg.guide;
1110
+ const guide = { id: g.id || "preview", key: g.key || "preview", name: g.name || "Preview", kind: g.kind || "flow", version: g.version ?? 0, config: (await loadSchemas()).GuideConfigSchema.parse(g.config) };
1111
+ this.session?.setPicking(false);
1112
+ this.session?.setStatus(`Previewing \u201C${guide.name}\u201D`);
1113
+ this.previewGuide = (0, import_guide_runtime3.runGuide)(guide, {
1114
+ theme: this.opts.theme(),
1115
+ ctx: this.opts.ctx,
1116
+ onNavigate: (url, newTab) => newTab ? window.open(url, "_blank", "noopener") : window.location.assign(url),
1117
+ onEnd: (reason) => {
1118
+ this.previewGuide = null;
1119
+ this.session?.setPicking(true);
1120
+ this.post({ type: "guidely:preview-ended", sessionId: this.info.id, reason });
1121
+ }
1122
+ });
1123
+ this.previewGuide.start();
1124
+ break;
1125
+ }
1126
+ case "guidely:highlight": {
1127
+ const result = (0, import_guide_runtime3.highlightSelectors)(Array.isArray(msg.selectors) ? msg.selectors : [], { durationMs: 3e3 });
1128
+ this.post({ type: "guidely:highlight-result", sessionId: this.info.id, counts: result.counts, resolved: !!result.resolved });
1129
+ break;
1130
+ }
1131
+ case "guidely:stop-preview":
1132
+ this.stopPreview();
1133
+ break;
1134
+ case "guidely:request-snapshot":
1135
+ this.lastSnapshot = "";
1136
+ this.sendSnapshot();
1137
+ break;
1138
+ case "guidely:ping":
1139
+ this.post({ type: "guidely:pong", sessionId: this.info.id, url: window.location.href });
1140
+ break;
1141
+ case "guidely:exit":
1142
+ this.stop();
1143
+ break;
1144
+ default:
1145
+ break;
1146
+ }
1147
+ } catch (err) {
1148
+ this.opts.onError(err);
1149
+ }
962
1150
  }
963
- /** The browser came back online: end the cooldown and let queued traffic try straight away. */
964
- online() {
965
- this.openUntil = 0;
966
- this.notify();
1151
+ /** Leaves authoring mode and clears the session marker. */
1152
+ stop() {
1153
+ if (!this.active) return;
1154
+ this.active = false;
1155
+ this.stopPreview();
1156
+ this.observer?.disconnect();
1157
+ this.observer = null;
1158
+ if (this.snapshotTimer) clearTimeout(this.snapshotTimer);
1159
+ this.snapshotTimer = null;
1160
+ if (this.heartbeat) clearInterval(this.heartbeat);
1161
+ this.heartbeat = null;
1162
+ this.session?.stop();
1163
+ this.session = null;
1164
+ this.opts.sessionStore.remove(KEYS.authoring);
1165
+ if (this.info) this.post({ type: "guidely:exit", sessionId: this.info.id });
1166
+ this.opts.onActive(false);
967
1167
  }
968
- /** Called when the server answers again after failures, or the browser comes back online. */
969
- onRecover(cb) {
970
- this.listeners.add(cb);
971
- return () => this.listeners.delete(cb);
1168
+ };
1169
+
1170
+ // src/capture-token.ts
1171
+ var import_runtime9 = require("@guidelytech/shared/runtime");
1172
+ function takeCaptureToken(sessionStore) {
1173
+ if (typeof window === "undefined") return null;
1174
+ let token = null;
1175
+ let authoringLink = false;
1176
+ try {
1177
+ const url = new URL(window.location.href);
1178
+ token = url.searchParams.get(import_runtime9.CAPTURE_PARAM);
1179
+ authoringLink = url.searchParams.has(AUTHORING_PARAM);
1180
+ if (token) {
1181
+ url.searchParams.delete(import_runtime9.CAPTURE_PARAM);
1182
+ window.history.replaceState(window.history.state, "", url.toString());
1183
+ }
1184
+ } catch {
1185
+ token = null;
972
1186
  }
973
- notify() {
974
- for (const cb of Array.from(this.listeners)) {
975
- try {
976
- cb();
977
- } catch {
978
- }
1187
+ const saved = readJson(sessionStore, KEYS.capture, null);
1188
+ let marker = null;
1189
+ if (token) marker = saved?.token === token ? saved : { token };
1190
+ else if (saved?.token && !authoringLink) marker = saved;
1191
+ if (!marker) {
1192
+ if (saved) {
1193
+ sessionStore.remove(KEYS.capture);
1194
+ sessionStore.remove(KEYS.capturePending);
979
1195
  }
1196
+ return null;
980
1197
  }
981
- };
982
- function parseRetryAfter(value, now = Date.now()) {
983
- if (!value) return null;
984
- const trimmed = value.trim();
985
- if (/^\d+$/.test(trimmed)) return Number(trimmed) * 1e3;
986
- const at = Date.parse(trimmed);
987
- return Number.isNaN(at) ? null : Math.max(0, at - now);
988
- }
989
- function isServerFailure(status) {
990
- return status === 429 || status >= 500;
1198
+ if (token && saved?.token !== token) sessionStore.remove(KEYS.capturePending);
1199
+ writeJson(sessionStore, KEYS.capture, marker);
1200
+ sessionStore.remove(KEYS.authoring);
1201
+ return marker;
991
1202
  }
992
1203
 
993
- // src/http.ts
994
- var STREAM_RESPONSE_TIMEOUT_MS = 3e4;
995
- var STREAM_IDLE_TIMEOUT_MS = 6e4;
996
- var Api = class {
997
- constructor(baseUrl, key, fetchImpl, health) {
998
- __publicField(this, "baseUrl", baseUrl);
999
- __publicField(this, "key", key);
1000
- __publicField(this, "fetchImpl", fetchImpl);
1001
- /** Reachability of the API, fed by every answer (see {@link ServerHealth}). */
1002
- __publicField(this, "health");
1003
- this.health = health ?? new ServerHealth();
1204
+ // src/load-capture.ts
1205
+ var loadCapture = async () => (await import("./capture.cjs")).capture;
1206
+
1207
+ // src/context.ts
1208
+ function detectDevice(ua, width) {
1209
+ const u = ua || "";
1210
+ let type = "desktop";
1211
+ if (/iPad|Tablet|PlayBook|Silk/i.test(u) || /Android/i.test(u) && !/Mobile/i.test(u)) type = "tablet";
1212
+ else if (/Mobi|iPhone|iPod|Android.*Mobile|Windows Phone/i.test(u)) type = "mobile";
1213
+ else if (width > 0 && width < 768) type = "mobile";
1214
+ else if (width > 0 && width < 1024) type = "tablet";
1215
+ let browser;
1216
+ if (/Electron\//i.test(u)) browser = "electron";
1217
+ else if (/Edg\//i.test(u)) browser = "edge";
1218
+ else if (/OPR\/|Opera/i.test(u)) browser = "opera";
1219
+ else if (/SamsungBrowser/i.test(u)) browser = "samsung";
1220
+ else if (/Chrome\/|CriOS/i.test(u)) browser = "chrome";
1221
+ else if (/Firefox\/|FxiOS/i.test(u)) browser = "firefox";
1222
+ else if (/Safari\//i.test(u) && /Version\//i.test(u)) browser = "safari";
1223
+ else if (/MSIE|Trident/i.test(u)) browser = "ie";
1224
+ let os;
1225
+ if (/Windows/i.test(u)) os = "windows";
1226
+ else if (/iPhone|iPad|iPod/i.test(u)) os = "ios";
1227
+ else if (/Mac OS X|Macintosh/i.test(u)) os = "macos";
1228
+ else if (/Android/i.test(u)) os = "android";
1229
+ else if (/CrOS/i.test(u)) os = "chromeos";
1230
+ else if (/Linux/i.test(u)) os = "linux";
1231
+ return { type, browser, os };
1232
+ }
1233
+ function buildContext(input) {
1234
+ let path = "";
1235
+ const query = {};
1236
+ try {
1237
+ const u = new URL(input.url, "http://localhost");
1238
+ path = u.pathname;
1239
+ u.searchParams.forEach((v, k) => {
1240
+ query[k] = v;
1241
+ });
1242
+ } catch {
1243
+ path = input.url;
1004
1244
  }
1005
- /** Feeds an answer (or a failed request, `null`) into {@link health}. */
1006
- observe(res) {
1007
- if (!res) this.health.failure();
1008
- else if (isServerFailure(res.status)) this.health.failure(parseRetryAfter(res.headers?.get?.("retry-after")));
1009
- else this.health.success();
1245
+ const device = input.device ?? (typeof navigator !== "undefined" ? detectDevice(navigator.userAgent, typeof window !== "undefined" ? window.innerWidth : 0) : { type: "desktop" });
1246
+ return {
1247
+ anonymousId: input.anonymousId,
1248
+ userId: input.userId ?? null,
1249
+ userAttributes: input.userAttributes,
1250
+ accountAttributes: input.accountAttributes,
1251
+ url: input.url,
1252
+ path,
1253
+ query,
1254
+ device,
1255
+ firstVisit: input.firstVisit,
1256
+ eventsSeen: new Set(input.eventsSeen),
1257
+ completedGuides: new Set(input.completedGuides),
1258
+ dismissedGuides: new Set(input.dismissedGuides),
1259
+ seenGuides: new Set(input.seenGuides),
1260
+ completedChecklists: new Set(input.completedChecklists),
1261
+ audienceIds: new Set(input.audienceIds),
1262
+ now: input.now ?? /* @__PURE__ */ new Date(),
1263
+ environment: input.environment,
1264
+ locale: input.locale ?? (typeof navigator !== "undefined" ? navigator.language : void 0),
1265
+ platform: input.platform ?? "web",
1266
+ ...input.screen !== void 0 ? { screen: input.screen } : {},
1267
+ ...input.appVersion !== void 0 ? { appVersion: input.appVersion } : {}
1268
+ };
1269
+ }
1270
+
1271
+ // src/heatmap.ts
1272
+ var HEATMAP_MAX_POINTS = 40;
1273
+ var HEATMAP_MAX_BYTES = 6e3;
1274
+ var HEATMAP_MAX_BINS = 60;
1275
+ var r2 = (n) => Math.round(Math.min(1, Math.max(0, n)) * 100) / 100;
1276
+ function heatmapPoint(el, e, rage) {
1277
+ const p = { k: rage ? "r" : "c", x: Math.round(e.pageX ?? 0) || 0, y: Math.round(e.pageY ?? 0) || 0 };
1278
+ const s = elementSelector(el).slice(0, 120);
1279
+ if (s) p.s = s;
1280
+ const rect = el.getBoundingClientRect();
1281
+ if (rect.width > 0 && rect.height > 0) {
1282
+ p.rx = r2((e.clientX - rect.left) / rect.width);
1283
+ p.ry = r2((e.clientY - rect.top) / rect.height);
1010
1284
  }
1011
- url(path, params = {}) {
1012
- const query = Object.entries({ key: this.key, ...params }).filter((e) => e[1] != null).map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join("&");
1013
- return `${this.baseUrl.replace(/\/$/, "")}/api/v1/sdk${path}?${query}`;
1285
+ return p;
1286
+ }
1287
+ var selectors = /* @__PURE__ */ new WeakMap();
1288
+ var r1 = (n) => Math.round(Math.min(1, Math.max(0, n)) * 10) / 10;
1289
+ function movementBin(el, e) {
1290
+ const p = { k: "m", x: Math.round(e.pageX ?? 0) || 0, y: Math.round(e.pageY ?? 0) || 0 };
1291
+ let s = /^(HTML|BODY)$/.test(el.tagName) ? "" : selectors.get(el);
1292
+ if (s === void 0) selectors.set(el, s = elementSelector(el).slice(0, 120));
1293
+ const rect = s ? el.getBoundingClientRect() : null;
1294
+ if (!rect || !(rect.width > 0 && rect.height > 0)) return [`${p.x >> 4},${p.y >> 4}`, p];
1295
+ p.s = s;
1296
+ p.rx = r1((e.clientX - rect.left) / rect.width);
1297
+ p.ry = r1((e.clientY - rect.top) / rect.height);
1298
+ return [`${s} ${p.rx},${p.ry}`, p];
1299
+ }
1300
+ var HeatmapBuffer = class {
1301
+ constructor() {
1302
+ /** The page the buffered points belong to. */
1303
+ __publicField(this, "url", "");
1304
+ __publicField(this, "points", []);
1305
+ __publicField(this, "bins", /* @__PURE__ */ new Map());
1306
+ __publicField(this, "bytes", 0);
1014
1307
  }
1015
- get fetcher() {
1016
- if (this.fetchImpl) return this.fetchImpl;
1017
- return typeof fetch === "function" ? fetch.bind(globalThis) : null;
1308
+ /** Adds a click (`burst` > 0 marks it and the burst's previous clicks as rage); true when the batch is full. */
1309
+ add(url, point, burst) {
1310
+ this.url = url;
1311
+ for (let i = Math.max(0, this.points.length - burst + 1); i < this.points.length; i++) this.points[i].k = "r";
1312
+ this.points.push(point);
1313
+ this.bytes += JSON.stringify(point).length + 1;
1314
+ return this.full();
1018
1315
  }
1019
- async request(method, path, options = {}) {
1020
- const f = this.fetcher;
1021
- if (!f) throw new Error("fetch is not available");
1022
- const headers = { ...options.headers, "X-Guidely-Key": this.key, Accept: "application/json" };
1023
- const form = typeof FormData !== "undefined" && options.body instanceof FormData;
1024
- if (options.body !== void 0 && !form) headers["Content-Type"] = "application/json";
1025
- if (options.etag) headers["If-None-Match"] = options.etag;
1026
- const ac = typeof AbortController !== "undefined" ? new AbortController() : null;
1027
- const timer = ac ? setTimeout(() => ac.abort(), options.timeoutMs ?? 1e4) : null;
1028
- try {
1029
- let res;
1030
- try {
1031
- res = await f(this.url(path, options.params), {
1032
- method,
1033
- headers,
1034
- body: options.body === void 0 ? void 0 : form ? options.body : JSON.stringify(options.body),
1035
- keepalive: options.keepalive,
1036
- signal: ac?.signal,
1037
- credentials: "omit",
1038
- mode: "cors"
1039
- });
1040
- } catch (err) {
1041
- this.observe(null);
1042
- throw err;
1043
- }
1044
- this.observe(res);
1045
- let data = null;
1046
- if (res.status !== 304 && res.status !== 204) {
1047
- try {
1048
- data = await res.json();
1049
- } catch {
1050
- data = null;
1051
- }
1052
- }
1053
- return { status: res.status, ok: res.ok, data, etag: res.headers?.get?.("etag") ?? null };
1054
- } finally {
1055
- if (timer) clearTimeout(timer);
1316
+ /** Adds a movement sample to its bin (weight +1); true when the batch is full. */
1317
+ move(url, [key, point]) {
1318
+ this.url = url;
1319
+ const bin = this.bins.get(key);
1320
+ if (bin) bin.w = (bin.w ?? 1) + 1;
1321
+ else {
1322
+ this.bins.set(key, point);
1323
+ this.bytes += JSON.stringify(point).length + 8;
1056
1324
  }
1325
+ return this.full();
1057
1326
  }
1058
- get(path, options) {
1059
- return this.request("GET", path, options);
1327
+ full() {
1328
+ return this.points.length >= HEATMAP_MAX_POINTS || this.bins.size > HEATMAP_MAX_BINS || this.bytes >= HEATMAP_MAX_BYTES;
1060
1329
  }
1061
- post(path, body, options) {
1062
- return this.request("POST", path, { body, ...options });
1330
+ /** Empties the buffer, returning what was in it (clicks first, then movement bins). */
1331
+ take() {
1332
+ const points = [...this.points, ...this.bins.values()];
1333
+ this.points = [];
1334
+ this.bins = /* @__PURE__ */ new Map();
1335
+ this.bytes = 0;
1336
+ return points.length ? { url: this.url, points } : null;
1063
1337
  }
1064
- /** Multipart POST (file uploads). No Content-Type header: the browser adds it with the boundary. */
1065
- postForm(path, form, options) {
1066
- return this.request("POST", path, { body: form, ...options });
1338
+ };
1339
+
1340
+ // src/errors.ts
1341
+ var import_runtime11 = require("@guidelytech/shared/runtime");
1342
+ var MAX_FRAMES_BYTES = 6e3;
1343
+ function fitFrames(frames) {
1344
+ const out = frames.slice(0, 30);
1345
+ while (out.length > 1 && JSON.stringify(out).length > MAX_FRAMES_BYTES) out.pop();
1346
+ return out;
1347
+ }
1348
+ function describe(value) {
1349
+ if (typeof value === "string") return value;
1350
+ try {
1351
+ return JSON.stringify(value) ?? String(value);
1352
+ } catch {
1353
+ return String(value);
1067
1354
  }
1068
- /**
1069
- * POST that may answer with server-sent events. Each event is passed to `onEvent`; a JSON answer is
1070
- * returned instead. Scout replies can take a while, so there is no overall timeout: the response
1071
- * must start within 30 s and then never go quiet for more than 60 s (the server sends keepalives),
1072
- * otherwise the request is aborted and rejects.
1073
- */
1074
- async stream(path, body, onEvent, headers = {}) {
1075
- const f = this.fetcher;
1076
- if (!f) throw new Error("fetch is not available");
1077
- const ac = typeof AbortController !== "undefined" ? new AbortController() : null;
1078
- let timer = null;
1079
- const arm = (ms) => {
1080
- if (!ac) return;
1081
- if (timer) clearTimeout(timer);
1082
- timer = setTimeout(() => ac.abort(), ms);
1083
- };
1084
- arm(STREAM_RESPONSE_TIMEOUT_MS);
1085
- try {
1086
- let res;
1087
- try {
1088
- res = await f(this.url(path), {
1089
- method: "POST",
1090
- headers: { ...headers, "X-Guidely-Key": this.key, Accept: "text/event-stream, application/json", "Content-Type": "application/json" },
1091
- body: JSON.stringify(body),
1092
- signal: ac?.signal,
1093
- credentials: "omit",
1094
- mode: "cors"
1095
- });
1096
- } catch (err) {
1097
- this.observe(null);
1098
- throw err;
1099
- }
1100
- this.observe(res);
1101
- const type = res.headers?.get?.("content-type") ?? "";
1102
- if (!type.includes("text/event-stream") || !res.body) {
1103
- let data = null;
1104
- try {
1105
- data = await res.json();
1106
- } catch {
1107
- data = null;
1108
- }
1109
- return { status: res.status, ok: res.ok, data, etag: null };
1110
- }
1111
- arm(STREAM_IDLE_TIMEOUT_MS);
1112
- await readEventStream(res.body, onEvent, () => arm(STREAM_IDLE_TIMEOUT_MS));
1113
- return { status: res.status, ok: res.ok, data: null, etag: null };
1114
- } finally {
1115
- if (timer) clearTimeout(timer);
1355
+ }
1356
+ function exceptionFromError(error, source, handled, fallbackType = "Error") {
1357
+ const isError = error instanceof Error || !!error && typeof error === "object" && typeof error.message === "string" && typeof error.stack === "string";
1358
+ const e = error;
1359
+ return {
1360
+ type: isError && e.name || fallbackType,
1361
+ message: (isError ? e.message : describe(error)).slice(0, 1e3),
1362
+ frames: isError ? fitFrames((0, import_runtime11.parseStack)(e.stack)) : [],
1363
+ source,
1364
+ handled
1365
+ };
1366
+ }
1367
+ function errorEventToException(e) {
1368
+ const target = e.target;
1369
+ if (target && typeof Element !== "undefined" && target instanceof Element) return null;
1370
+ if (e.error != null) return exceptionFromError(e.error, "onerror", false);
1371
+ const message = e.message || "";
1372
+ if (!message || /^script error\.?$/i.test(message.trim())) return null;
1373
+ const m = /^(?:Uncaught )?([A-Z]\w*(?:Error|Exception)):\s*([\s\S]*)$/.exec(message);
1374
+ return {
1375
+ type: m?.[1] ?? "Error",
1376
+ message: (m?.[2] ?? message).slice(0, 1e3),
1377
+ frames: e.filename ? [{ file: e.filename.slice(0, 500), line: e.lineno || void 0, col: e.colno || void 0 }] : [],
1378
+ source: "onerror",
1379
+ handled: false
1380
+ };
1381
+ }
1382
+ function rejectionToException(e) {
1383
+ return exceptionFromError(e.reason, "unhandledrejection", false, "UnhandledRejection");
1384
+ }
1385
+ var GUIDELY_SCRIPT = /\/guidely(-recorder)?(\.min)?\.js$/i;
1386
+ function isGuidelyException(props) {
1387
+ if (props.message.startsWith("[Guidely]")) return true;
1388
+ const file = props.frames[0]?.file;
1389
+ if (!file) return false;
1390
+ try {
1391
+ return GUIDELY_SCRIPT.test(new URL(file).pathname);
1392
+ } catch {
1393
+ return GUIDELY_SCRIPT.test(file.replace(/[?#].*$/, ""));
1394
+ }
1395
+ }
1396
+ var ExceptionThrottle = class {
1397
+ constructor(max = 5) {
1398
+ __publicField(this, "max", max);
1399
+ __publicField(this, "seen", /* @__PURE__ */ new Map());
1400
+ }
1401
+ allow(props) {
1402
+ const top = props.frames[0];
1403
+ const key = `${props.type}|${props.message}|${top ? `${top.file}:${top.line}:${top.col}` : ""}`;
1404
+ const n = this.seen.get(key) ?? 0;
1405
+ if (n >= this.max) return false;
1406
+ if (this.seen.size > 500) this.seen.clear();
1407
+ this.seen.set(key, n + 1);
1408
+ return true;
1409
+ }
1410
+ };
1411
+
1412
+ // src/emitter.ts
1413
+ var Emitter = class {
1414
+ constructor(onError) {
1415
+ __publicField(this, "onError", onError);
1416
+ __publicField(this, "listeners", /* @__PURE__ */ new Map());
1417
+ }
1418
+ on(event, cb) {
1419
+ let set = this.listeners.get(event);
1420
+ if (!set) {
1421
+ set = /* @__PURE__ */ new Set();
1422
+ this.listeners.set(event, set);
1116
1423
  }
1424
+ set.add(cb);
1425
+ return () => this.off(event, cb);
1117
1426
  }
1118
- /** `navigator.sendBeacon` when available (page unload); returns false when it could not be queued. */
1119
- beacon(path, body) {
1120
- try {
1121
- if (typeof navigator === "undefined" || typeof navigator.sendBeacon !== "function") return false;
1122
- const blob = new Blob([JSON.stringify(body)], { type: "text/plain;charset=UTF-8" });
1123
- return navigator.sendBeacon(this.url(path), blob);
1124
- } catch {
1125
- return false;
1427
+ off(event, cb) {
1428
+ this.listeners.get(event)?.delete(cb);
1429
+ }
1430
+ emit(event, ...args) {
1431
+ const set = this.listeners.get(event);
1432
+ if (!set) return;
1433
+ for (const cb of Array.from(set)) {
1434
+ try {
1435
+ cb(...args);
1436
+ } catch (err) {
1437
+ this.onError?.(err);
1438
+ }
1126
1439
  }
1127
1440
  }
1441
+ clear() {
1442
+ this.listeners.clear();
1443
+ }
1128
1444
  };
1129
- var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
1130
- function backoffMs(attempt, baseMs = 1e3, maxMs = 3e4) {
1131
- return Math.min(maxMs, baseMs * 2 ** Math.max(0, attempt));
1132
- }
1133
- async function withRetry(send, attempts = 3, baseMs = 500) {
1134
- for (let i = 0; i < attempts; i++) {
1135
- try {
1136
- const res = await send();
1137
- if (res.ok || res.status >= 400 && res.status < 500 && res.status !== 429) return res;
1138
- } catch {
1445
+
1446
+ // src/frequency.ts
1447
+ var HOUR = 3600 * 1e3;
1448
+ function isFrequencyEligible(freq, state, shownThisSession, now = Date.now()) {
1449
+ const s = state ?? { impressions: 0 };
1450
+ if (freq.maxImpressions != null && s.impressions >= freq.maxImpressions) return false;
1451
+ if (s.completedAt && freq.mode !== "every_time") return false;
1452
+ if (s.dismissedAt) {
1453
+ if (!freq.reshowAfterDismiss) return false;
1454
+ if (freq.cooldownHours > 0 && now - Date.parse(s.dismissedAt) < freq.cooldownHours * HOUR) return false;
1455
+ }
1456
+ switch (freq.mode) {
1457
+ case "once":
1458
+ return s.impressions === 0 || !!s.dismissedAt && freq.reshowAfterDismiss;
1459
+ case "once_per_session":
1460
+ return !shownThisSession;
1461
+ case "interval": {
1462
+ if (!s.lastShownAt) return true;
1463
+ const hours = freq.intervalHours ?? 24;
1464
+ return now - Date.parse(s.lastShownAt) >= hours * HOUR;
1139
1465
  }
1140
- if (i < attempts - 1) await sleep(backoffMs(i, baseMs, 8e3));
1466
+ case "every_time":
1467
+ default:
1468
+ return true;
1141
1469
  }
1142
- return null;
1143
1470
  }
1144
- function postWithRetry(api, path, body, attempts = 3, baseMs = 500) {
1145
- return withRetry(() => api.post(path, body), attempts, baseMs);
1471
+ function isExclusiveGuide(guide) {
1472
+ if (guide.kind === "banner" || guide.kind === "checklist") return false;
1473
+ const steps = guide.config.steps;
1474
+ if (steps.length > 0 && steps.every((s) => s.type === "hotspot" || s.type === "banner")) return false;
1475
+ return true;
1146
1476
  }
1147
- async function readEventStream(body, onEvent, onChunk) {
1148
- const reader = body.getReader();
1149
- const decoder = new TextDecoder();
1150
- let buffer = "";
1151
- const dispatch = (raw) => {
1152
- let event = "message";
1153
- const data = [];
1154
- for (const line of raw.split("\n")) {
1155
- if (!line || line.startsWith(":")) continue;
1156
- const i = line.indexOf(":");
1157
- const field = i < 0 ? line : line.slice(0, i);
1158
- const value = i < 0 ? "" : line.slice(i + 1).replace(/^ /, "");
1159
- if (field === "event") event = value;
1160
- else if (field === "data") data.push(value);
1161
- }
1162
- if (!data.length) return;
1163
- try {
1164
- onEvent(event, JSON.parse(data.join("\n")));
1165
- } catch {
1166
- }
1167
- };
1168
- for (; ; ) {
1169
- const { done, value } = await reader.read();
1170
- if (value) onChunk?.();
1171
- if (value) buffer += decoder.decode(value, { stream: true }).replace(/\r\n?/g, "\n");
1172
- let sep;
1173
- while ((sep = buffer.indexOf("\n\n")) >= 0) {
1174
- dispatch(buffer.slice(0, sep));
1175
- buffer = buffer.slice(sep + 2);
1176
- }
1177
- if (done) break;
1477
+ function areMutuallyExclusive(a, b) {
1478
+ const la = a.config.frequency.mutuallyExclusiveWith;
1479
+ const lb = b.config.frequency.mutuallyExclusiveWith;
1480
+ return la.includes(b.id) || la.includes(b.key) || lb.includes(a.id) || lb.includes(a.key);
1481
+ }
1482
+ function isWithinSchedule(guide, now = Date.now()) {
1483
+ for (const w of [guide.config.schedule, guide.config.survey]) {
1484
+ if (!w) continue;
1485
+ if (w.startAt && now < Date.parse(w.startAt)) return false;
1486
+ if (w.endAt && now > Date.parse(w.endAt)) return false;
1178
1487
  }
1179
- if (buffer.trim()) dispatch(buffer);
1488
+ return true;
1489
+ }
1490
+ var MINUTE = 60 * 1e3;
1491
+ function countsTowardThrottle(guide) {
1492
+ return isExclusiveGuide(guide) && !guide.config.frequency.ignoreThrottling;
1493
+ }
1494
+ function isThrottleEligible(settings, launches, now = Date.now()) {
1495
+ if (!settings || !settings.enabled) return true;
1496
+ const times = launches.map((l) => Date.parse(l)).filter((t) => !Number.isNaN(t));
1497
+ const windowMs = Math.max(1, settings.windowHours) * HOUR;
1498
+ if (times.filter((t) => now - t < windowMs).length >= Math.max(1, settings.maxGuides)) return false;
1499
+ if (settings.minGapMinutes > 0 && times.some((t) => now - t < settings.minGapMinutes * MINUTE)) return false;
1500
+ return true;
1501
+ }
1502
+ function pruneLaunches(launches, settings, now = Date.now()) {
1503
+ const keepMs = Math.max(settings?.windowHours ?? 24, 1) * HOUR + (settings?.minGapMinutes ?? 0) * MINUTE;
1504
+ return launches.filter((l) => {
1505
+ const t = Date.parse(l);
1506
+ return !Number.isNaN(t) && now - t < keepMs;
1507
+ }).slice(-100);
1508
+ }
1509
+ function dismissedAfterMerge(local, server) {
1510
+ if (server.dismissResetAt && server.dismissResetAt !== local?.dismissResetAt) return server.dismissedAt ?? null;
1511
+ return local?.dismissedAt ?? server.dismissedAt ?? null;
1180
1512
  }
1181
1513
 
1182
1514
  // src/messenger-api.ts
@@ -1250,6 +1582,14 @@ function createMessengerTransport(opts) {
1250
1582
  h.onDone(d.messages.at(-1) ?? null, d.conversation);
1251
1583
  } else if (!finished) h.onError("The reply was interrupted");
1252
1584
  }
1585
+ async function feedback(path, body, fallback) {
1586
+ const res = await api.post(`/feedback${path}`, { ...body, anonymousId: opts.anonymousId(), userId: opts.userId() });
1587
+ if (!res.ok || !res.data) {
1588
+ if (res.status === 429) throw new HttpError(429, "You're doing that a lot. Please try again in a minute.");
1589
+ throw new HttpError(res.status, serverMessage(res.data) ?? fallback);
1590
+ }
1591
+ return res.data;
1592
+ }
1253
1593
  return {
1254
1594
  async listConversations() {
1255
1595
  const tokens = storedConversations(store, projectId).map((c) => c.token);
@@ -1286,7 +1626,8 @@ function createMessengerTransport(opts) {
1286
1626
  return res.data;
1287
1627
  },
1288
1628
  async getArticle(id) {
1289
- const res = await api.get(`/articles/${encodeURIComponent(id)}`);
1629
+ const locale = opts.locale?.();
1630
+ const res = await api.get(`/articles/${encodeURIComponent(id)}`, locale ? { params: { locale } } : void 0);
1290
1631
  return res.ok && res.data ? res.data.article : null;
1291
1632
  },
1292
1633
  async fileComplaint(input) {
@@ -1316,13 +1657,25 @@ function createMessengerTransport(opts) {
1316
1657
  async getHowTo(id) {
1317
1658
  const res = await api.get(`/how-tos/${encodeURIComponent(id)}`);
1318
1659
  return res.ok && res.data ? res.data.howTo : null;
1319
- }
1660
+ },
1661
+ // Ideas (feature requests). The visitor's ids go in every body; the server decides what they may do.
1662
+ listIdeas: (query) => feedback("/requests/query", query, "Ideas couldn't be loaded."),
1663
+ getIdea: (id) => feedback(`/requests/${encodeURIComponent(id)}/view`, {}, "This idea couldn't be loaded."),
1664
+ submitIdea: (input) => feedback("/requests", { ...input, pageUrl: opts.pageUrl() }, "Your idea couldn't be sent. Please try again."),
1665
+ voteIdea: (id, vote) => feedback(`/requests/${encodeURIComponent(id)}/vote`, vote, "Your vote couldn't be saved."),
1666
+ unvoteIdea: (id) => feedback(`/requests/${encodeURIComponent(id)}/unvote`, {}, "Your vote couldn't be removed."),
1667
+ followIdea: (id, follow) => feedback(`/requests/${encodeURIComponent(id)}/follow`, { follow }, "That didn't work. Please try again."),
1668
+ commentIdea: (id, body) => feedback(`/requests/${encodeURIComponent(id)}/comments`, { body }, "Your comment couldn't be posted."),
1669
+ similarIdeas: async (title) => (await feedback("/similar", { title }, "Similar ideas couldn't be loaded.")).requests ?? [],
1670
+ ideaUpdates: () => feedback("/updates", {}, "Updates couldn't be loaded."),
1671
+ markIdeaUpdatesRead: (ids) => feedback("/updates/read", ids ? { ids } : {}, "Updates couldn't be marked read."),
1672
+ ideaPortalLink: async () => (await feedback("/portal-link", {}, "The feedback portal couldn't be opened.")).url
1320
1673
  };
1321
1674
  }
1322
1675
 
1323
1676
  // src/messenger-visitor.ts
1324
- var import_runtime11 = require("@guidelytech/shared/runtime");
1325
- var import_guide_runtime3 = require("@guidelytech/guide-runtime");
1677
+ var import_runtime15 = require("@guidelytech/shared/runtime");
1678
+ var import_guide_runtime4 = require("@guidelytech/guide-runtime");
1326
1679
  var PAGE_GUIDE_KINDS = /* @__PURE__ */ new Set(["flow", "tooltip", "modal"]);
1327
1680
  var CATCH_ALL = /* @__PURE__ */ new Set(["", "*", "**", "/*", "/**", "/"]);
1328
1681
  var RESUME_WITHIN_MS = 14 * 864e5;
@@ -1339,7 +1692,7 @@ function targetsPage(guide, url) {
1339
1692
  const c = guide.config;
1340
1693
  const checks = [...c.urlPatterns, ...c.trigger.urlPattern ? [c.trigger.urlPattern] : [], ...c.trigger.type === "url_match" && c.trigger.value ? [c.trigger.value] : [], ...c.steps[0]?.url ? [c.steps[0].url] : []];
1341
1694
  const specific = checks.filter((p) => !CATCH_ALL.has(p.trim()));
1342
- return specific.length > 0 && checks.every((p) => (0, import_guide_runtime3.urlMatches)(p, url));
1695
+ return specific.length > 0 && checks.every((p) => (0, import_guide_runtime4.urlMatches)(p, url));
1343
1696
  }
1344
1697
  function plainText(html, max = 280) {
1345
1698
  const text = html.replace(/<(br|\/p|\/div|\/li|\/h[1-6])\s*\/?>/gi, " ").replace(/<[^>]*>/g, "").replace(/&nbsp;/g, " ").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&amp;/g, "&").replace(/\s+/g, " ").trim();
@@ -1388,7 +1741,7 @@ function buildMessengerVisitor(input) {
1388
1741
  unread: !seen.has(g.id) && !(state.guides[g.id]?.impressions ?? 0)
1389
1742
  };
1390
1743
  });
1391
- return { firstName: (0, import_runtime11.firstNameFromTraits)(input.traits), path: pathOf(input.url), pageGuides, resumeGuide, checklist, news };
1744
+ return { firstName: (0, import_runtime15.firstNameFromTraits)(input.traits), path: pathOf(input.url), pageGuides, resumeGuide, checklist, news };
1392
1745
  }
1393
1746
 
1394
1747
  // src/navigation.ts
@@ -1446,6 +1799,48 @@ function watchNavigation(listener, debounceMs = 50) {
1446
1799
  };
1447
1800
  }
1448
1801
 
1802
+ // src/page-url.ts
1803
+ var WEB_SCHEMES = /* @__PURE__ */ new Set(["http", "https", "about", "data", "blob", "javascript"]);
1804
+ function appScheme(href) {
1805
+ let u;
1806
+ try {
1807
+ u = new URL(href);
1808
+ } catch {
1809
+ return null;
1810
+ }
1811
+ const scheme = u.protocol.replace(/:$/, "").toLowerCase();
1812
+ return WEB_SCHEMES.has(scheme) ? null : { u, scheme };
1813
+ }
1814
+ function normalizeAppUrl(href) {
1815
+ const app = appScheme(href);
1816
+ if (!app) return href;
1817
+ const { u, scheme } = app;
1818
+ const host = scheme === "file" ? "app" : u.host || "app";
1819
+ const hash = /^#!?(\/.*)$/.exec(u.hash);
1820
+ if (hash) {
1821
+ const route = hash[1];
1822
+ return `${scheme}://${host}${route.startsWith("/") ? route : `/${route}`}`;
1823
+ }
1824
+ let path = safeDecode(u.pathname);
1825
+ if (scheme === "file" || /\.html?$/i.test(path)) {
1826
+ const file = path.split("/").filter(Boolean).pop() ?? "";
1827
+ const name = file.replace(/\.html?$/i, "");
1828
+ path = !name || name.toLowerCase() === "index" ? "/" : `/${name}`;
1829
+ }
1830
+ if (!path.startsWith("/")) path = `/${path}`;
1831
+ return `${scheme}://${host}${path}${u.search}`;
1832
+ }
1833
+ function safeDecode(s) {
1834
+ try {
1835
+ return decodeURI(s);
1836
+ } catch {
1837
+ return s;
1838
+ }
1839
+ }
1840
+ function isAppUrl(href) {
1841
+ return appScheme(href) !== null;
1842
+ }
1843
+
1449
1844
  // src/page-tracking.ts
1450
1845
  function scrollDepth() {
1451
1846
  const de = document.documentElement;
@@ -1491,89 +1886,105 @@ var PageTracker = class {
1491
1886
  }
1492
1887
  };
1493
1888
 
1494
- // src/outbox.ts
1495
- var Outbox = class {
1496
- constructor(opts) {
1497
- __publicField(this, "opts", opts);
1498
- __publicField(this, "items");
1499
- __publicField(this, "sending", false);
1500
- __publicField(this, "attempt", 0);
1501
- __publicField(this, "timer", null);
1502
- __publicField(this, "destroyed", false);
1503
- __publicField(this, "offRecover");
1504
- __publicField(this, "maxItems");
1505
- __publicField(this, "maxAgeMs");
1506
- this.maxItems = opts.maxItems ?? 50;
1507
- this.maxAgeMs = opts.maxAgeMs ?? 7 * 24 * 60 * 60 * 1e3;
1508
- const stored = readJson(opts.store, opts.storageKey, []);
1509
- const cutoff = Date.now() - this.maxAgeMs;
1510
- this.items = Array.isArray(stored) ? stored.filter((i) => i && typeof i.path === "string" && typeof i.key === "string" && i.createdAt > cutoff).slice(-this.maxItems) : [];
1511
- this.offRecover = opts.health?.onRecover(() => {
1512
- this.attempt = 0;
1513
- this.retryIn(0);
1514
- }) ?? null;
1515
- if (this.items.length) this.retryIn(0);
1516
- }
1517
- get size() {
1518
- return this.items.length;
1519
- }
1520
- /** Queues `POST path` and sends it now (or as soon as the API is reachable). */
1521
- add(key, path, body) {
1522
- if (this.destroyed) return;
1523
- this.items = this.items.filter((i) => i.key !== key);
1524
- this.items.push({ key, path, body, createdAt: Date.now() });
1525
- if (this.items.length > this.maxItems) this.items.splice(0, this.items.length - this.maxItems);
1526
- this.persist();
1527
- void this.flush();
1528
- }
1529
- persist() {
1530
- writeJson(this.opts.store, this.opts.storageKey, this.items);
1889
+ // src/prototype-overlay.ts
1890
+ var PROTOTYPE_CLOSE_MESSAGE = "guidely:prototype:close";
1891
+ var TOKEN = /^[A-Za-z0-9_-]{16,64}$/;
1892
+ var OVERLAY_ATTR = "data-guidely-prototype-overlay";
1893
+ function matchPrototypeTestUrl(urlOrToken, apiUrl) {
1894
+ const value = (urlOrToken || "").trim();
1895
+ let api;
1896
+ try {
1897
+ api = new URL(apiUrl);
1898
+ } catch {
1899
+ return null;
1531
1900
  }
1532
- retryIn(ms) {
1533
- if (this.destroyed) return;
1534
- if (this.timer) clearTimeout(this.timer);
1535
- this.timer = setTimeout(() => {
1536
- this.timer = null;
1537
- void this.flush();
1538
- }, ms);
1901
+ if (TOKEN.test(value)) return new URL(`/validate/${value}`, api.origin);
1902
+ let url;
1903
+ try {
1904
+ url = new URL(value, typeof location !== "undefined" ? location.href : api.origin);
1905
+ } catch {
1906
+ return null;
1539
1907
  }
1540
- /** Sends queued requests in order, one at a time. */
1541
- async flush() {
1542
- if (this.destroyed || this.sending || !this.items.length) return;
1543
- const wait = this.opts.health?.waitMs() ?? 0;
1544
- if (wait > 0) return this.retryIn(wait);
1545
- this.sending = true;
1908
+ if (url.origin !== api.origin) return null;
1909
+ const m = /^\/validate\/([^/]+)\/?$/.exec(url.pathname);
1910
+ if (!m || !TOKEN.test(m[1])) return null;
1911
+ return new URL(`/validate/${m[1]}`, api.origin);
1912
+ }
1913
+ var current = null;
1914
+ function openPrototypeOverlay(testUrl, opts) {
1915
+ current?.close();
1916
+ const doc = document;
1917
+ const previous = doc.activeElement;
1918
+ const src = new URL(testUrl.href);
1919
+ const hash = new URLSearchParams();
1920
+ hash.set("key", opts.key);
1921
+ if (opts.anonymousId) hash.set("anon", opts.anonymousId);
1922
+ if (opts.userId) hash.set("uid", opts.userId);
1923
+ src.hash = hash.toString();
1924
+ const root = doc.createElement("div");
1925
+ root.setAttribute(OVERLAY_ATTR, "");
1926
+ root.setAttribute("role", "dialog");
1927
+ root.setAttribute("aria-modal", "true");
1928
+ root.setAttribute("aria-label", "Prototype test");
1929
+ root.style.cssText = `position:fixed;inset:0;z-index:${opts.zIndex ?? 2147483646};background:rgba(15,23,42,.55);display:flex;align-items:stretch;justify-content:center;padding:0;margin:0;`;
1930
+ const frame = doc.createElement("iframe");
1931
+ frame.src = src.toString();
1932
+ frame.title = "Prototype test";
1933
+ frame.setAttribute("allow", "fullscreen");
1934
+ frame.setAttribute("referrerpolicy", "no-referrer");
1935
+ frame.style.cssText = "border:0;width:100%;height:100%;background:#fff;display:block;";
1936
+ const close = doc.createElement("button");
1937
+ close.type = "button";
1938
+ close.setAttribute("aria-label", "Close prototype test");
1939
+ close.textContent = "\xD7";
1940
+ close.style.cssText = "position:absolute;top:10px;right:10px;width:36px;height:36px;border-radius:999px;border:0;background:rgba(15,23,42,.75);color:#fff;font:600 22px/36px system-ui,sans-serif;text-align:center;cursor:pointer;padding:0;z-index:1;";
1941
+ root.append(frame, close);
1942
+ const overflow = doc.documentElement.style.overflow;
1943
+ doc.documentElement.style.overflow = "hidden";
1944
+ let closed = false;
1945
+ const shut = () => {
1946
+ if (closed) return;
1947
+ closed = true;
1948
+ window.removeEventListener("message", onMessage);
1949
+ doc.removeEventListener("keydown", onKey, true);
1950
+ doc.removeEventListener("focusin", onFocus, true);
1951
+ root.remove();
1952
+ doc.documentElement.style.overflow = overflow;
1953
+ if (current === overlay) current = null;
1546
1954
  try {
1547
- while (this.items.length && !this.destroyed) {
1548
- const item = this.items[0];
1549
- let retry = false;
1550
- try {
1551
- const res = await this.opts.api.post(item.path, item.body);
1552
- retry = !res.ok && (res.status === 429 || res.status >= 500);
1553
- } catch (err) {
1554
- retry = true;
1555
- this.opts.onError?.(err);
1556
- }
1557
- if (retry) {
1558
- this.attempt += 1;
1559
- this.retryIn(Math.max(backoffMs(this.attempt - 1, 1e3, 6e4), this.opts.health?.waitMs() ?? 0));
1560
- return;
1561
- }
1562
- this.attempt = 0;
1563
- this.items = this.items.filter((i) => i !== item);
1564
- this.persist();
1565
- }
1566
- } finally {
1567
- this.sending = false;
1955
+ previous?.focus?.();
1956
+ } catch {
1568
1957
  }
1569
- }
1570
- destroy() {
1571
- this.destroyed = true;
1572
- this.offRecover?.();
1573
- if (this.timer) clearTimeout(this.timer);
1574
- this.timer = null;
1575
- }
1576
- };
1958
+ opts.onClose?.();
1959
+ };
1960
+ const onMessage = (e) => {
1961
+ if (e.origin !== testUrl.origin || e.source !== frame.contentWindow) return;
1962
+ const data = e.data;
1963
+ if (data && typeof data === "object" && data.type === PROTOTYPE_CLOSE_MESSAGE) shut();
1964
+ };
1965
+ const onKey = (e) => {
1966
+ if (e.key === "Escape") {
1967
+ e.preventDefault();
1968
+ e.stopPropagation();
1969
+ shut();
1970
+ } else if (e.key === "Tab") {
1971
+ e.preventDefault();
1972
+ (doc.activeElement === close ? frame : close).focus();
1973
+ }
1974
+ };
1975
+ const onFocus = (e) => {
1976
+ if (!root.contains(e.target)) close.focus();
1977
+ };
1978
+ close.addEventListener("click", shut);
1979
+ window.addEventListener("message", onMessage);
1980
+ doc.addEventListener("keydown", onKey, true);
1981
+ doc.addEventListener("focusin", onFocus, true);
1982
+ doc.body.appendChild(root);
1983
+ frame.focus();
1984
+ const overlay = { element: root, close: shut };
1985
+ current = overlay;
1986
+ return overlay;
1987
+ }
1577
1988
 
1578
1989
  // src/queue.ts
1579
1990
  var EventQueue = class {
@@ -1707,7 +2118,7 @@ var loadRecorder = async () => (await import("./recorder.cjs")).recorder;
1707
2118
 
1708
2119
  // src/client.ts
1709
2120
  var SDK_NAME = "@guidelytech/sdk-core";
1710
- var SDK_VERSION = "0.10.0";
2121
+ var SDK_VERSION = "0.12.0";
1711
2122
  var DEFAULT_API_URL = "https://api.guidely.app";
1712
2123
  var PAGE_SCOPED = /* @__PURE__ */ new Set(["page_load", "url_match", "element_click", "element_visible", "delay"]);
1713
2124
  var DAY_MS = 24 * 60 * 60 * 1e3;
@@ -1733,7 +2144,7 @@ function emptyState() {
1733
2144
  return { guides: {}, checklists: {}, surveysSubmitted: [], audienceIds: [] };
1734
2145
  }
1735
2146
  function newSession(now = Date.now()) {
1736
- return { id: `s_${(0, import_runtime15.randomId)(16)}`, startedAt: new Date(now).toISOString(), shownGuides: [], events: [], lastActivityAt: now };
2147
+ return { id: `s_${(0, import_runtime18.randomId)(16)}`, startedAt: new Date(now).toISOString(), shownGuides: [], events: [], lastActivityAt: now };
1737
2148
  }
1738
2149
  var GuidelyClient = class _GuidelyClient {
1739
2150
  constructor(options) {
@@ -1745,6 +2156,8 @@ var GuidelyClient = class _GuidelyClient {
1745
2156
  __publicField(this, "queue");
1746
2157
  /** Survey answers and guide/checklist progress, kept until the server accepts them. */
1747
2158
  __publicField(this, "outbox");
2159
+ /** Agent Analytics turns (agentConversation), in their own outbox. */
2160
+ __publicField(this, "agentTurns");
1748
2161
  __publicField(this, "config", null);
1749
2162
  __publicField(this, "etag", null);
1750
2163
  __publicField(this, "anonymousId");
@@ -1794,6 +2207,10 @@ var GuidelyClient = class _GuidelyClient {
1794
2207
  __publicField(this, "flags");
1795
2208
  /** Experiments already reported this session (`experiment_viewed` is sent once per session). */
1796
2209
  __publicField(this, "exposures", /* @__PURE__ */ new Set());
2210
+ /** Language set with `setLocale()`. */
2211
+ __publicField(this, "locale", null);
2212
+ /** Language each shown guide was rendered in; added to its events as `locale`. */
2213
+ __publicField(this, "guideLocales", /* @__PURE__ */ new Map());
1797
2214
  __publicField(this, "customListeners", /* @__PURE__ */ new Map());
1798
2215
  __publicField(this, "eventBus", {
1799
2216
  on: (name, cb) => {
@@ -1828,7 +2245,7 @@ var GuidelyClient = class _GuidelyClient {
1828
2245
  this.sessionStore = this.options.storage === "memory" ? this.store : createStorage("session");
1829
2246
  this.anonymousId = this.store.get(KEYS.anon) || "";
1830
2247
  if (!this.anonymousId) {
1831
- this.anonymousId = `anon_${(0, import_runtime15.randomId)(20)}`;
2248
+ this.anonymousId = `anon_${(0, import_runtime18.randomId)(20)}`;
1832
2249
  this.store.set(KEYS.anon, this.anonymousId);
1833
2250
  }
1834
2251
  this.firstVisit = !this.store.get(KEYS.seen);
@@ -1838,7 +2255,7 @@ var GuidelyClient = class _GuidelyClient {
1838
2255
  const session = readJson(this.sessionStore, KEYS.session, null);
1839
2256
  this.session = session && session.id ? session : newSession();
1840
2257
  this.persistSession();
1841
- this.lastUrl = location.href;
2258
+ this.lastUrl = this.pageHref();
1842
2259
  this.lastAudiences = new Set(this.state.audienceIds);
1843
2260
  this.queue = new EventQueue({
1844
2261
  store: this.store,
@@ -1853,6 +2270,7 @@ var GuidelyClient = class _GuidelyClient {
1853
2270
  onError: (err) => this.reportError(err)
1854
2271
  });
1855
2272
  this.outbox = new Outbox({ api: this.api, store: this.store, storageKey: KEYS.outbox(this.options.projectId), health: this.api.health });
2273
+ this.agentTurns = new AgentTurns({ api: this.api, store: this.store, storageKey: KEYS.agentOutbox(this.options.projectId), health: this.api.health, sdk: { name: SDK_NAME, version: SDK_VERSION } });
1856
2274
  if (options.user) this.user = { ...options.user };
1857
2275
  this.flags = this.createFlags(options.flags);
1858
2276
  }
@@ -1929,7 +2347,7 @@ var GuidelyClient = class _GuidelyClient {
1929
2347
  this.resumeActiveGuide();
1930
2348
  this.evaluate("ready");
1931
2349
  if (!this.options.disableAuthoring && !capturing) this.maybeStartAuthoring();
1932
- this.pageTracker.start(location.href);
2350
+ this.pageTracker.start(this.pageHref());
1933
2351
  this.touchSession();
1934
2352
  this.syncCapture();
1935
2353
  } catch (err) {
@@ -1962,7 +2380,7 @@ var GuidelyClient = class _GuidelyClient {
1962
2380
  wireListeners() {
1963
2381
  const doc = document;
1964
2382
  const win = window;
1965
- this.cleanups.push(watchNavigation((url) => this.handleNavigation(url)));
2383
+ this.cleanups.push(watchNavigation((url) => this.handleNavigation(this.pageHref(url))));
1966
2384
  const onClick = (e) => this.handleDocumentClick(e);
1967
2385
  doc.addEventListener("click", onClick, true);
1968
2386
  this.cleanups.push(() => doc.removeEventListener("click", onClick, true));
@@ -1983,7 +2401,7 @@ var GuidelyClient = class _GuidelyClient {
1983
2401
  const onPageShow = (e) => {
1984
2402
  if (!e.persisted || !this.pageLeft) return;
1985
2403
  this.pageLeft = false;
1986
- this.pageTracker.start(location.href);
2404
+ this.pageTracker.start(this.pageHref());
1987
2405
  };
1988
2406
  let lastScroll = 0;
1989
2407
  const onScroll = () => {
@@ -2032,7 +2450,7 @@ var GuidelyClient = class _GuidelyClient {
2032
2450
  this.cleanups.push(() => clearInterval(refresh));
2033
2451
  const poll = setInterval(() => {
2034
2452
  if (!this.isReady || !this.config || this.editing() || doc.visibilityState === "hidden") return;
2035
- const url = location.href;
2453
+ const url = this.pageHref();
2036
2454
  const ctx = this.getContext();
2037
2455
  for (const guide of this.config.guides) {
2038
2456
  if (guide.config.trigger.type === "element_visible" && guide.config.trigger.value) this.armVisibility(guide, ctx, url);
@@ -2101,10 +2519,11 @@ var GuidelyClient = class _GuidelyClient {
2101
2519
  this.queue.flushSync();
2102
2520
  this.queue.destroy();
2103
2521
  this.outbox.destroy();
2522
+ this.agentTurns.destroy();
2104
2523
  this.flags.destroy();
2105
2524
  this.emitter.clear();
2106
- (0, import_guide_runtime4.destroyHost)();
2107
- if (current === this) current = null;
2525
+ (0, import_guide_runtime5.destroyHost)();
2526
+ if (current2 === this) current2 = null;
2108
2527
  }
2109
2528
  // ---------------------------------------------------------------------------
2110
2529
  // Identity
@@ -2116,13 +2535,13 @@ var GuidelyClient = class _GuidelyClient {
2116
2535
  this.user = { id: String(userId), traits: { ...this.user?.id === String(userId) ? this.user.traits : {}, ...traits || {} }, account: account ?? (this.user?.id === String(userId) ? this.user.account : void 0) };
2117
2536
  writeJson(this.store, KEYS.user(this.options.projectId), this.user);
2118
2537
  this.syncFlagAttributes();
2119
- this.enqueue("identify", { properties: { traits: (0, import_runtime15.sanitizeProperties)(this.user.traits) } });
2538
+ this.enqueue("identify", { properties: { traits: (0, import_runtime18.sanitizeProperties)(this.user.traits) } });
2120
2539
  await this.syncIdentify();
2121
2540
  this.emitter.emit("identify", { userId: this.user.id, anonymousId: this.anonymousId });
2122
2541
  if (this.isReady) this.evaluate("identify");
2123
2542
  }
2124
2543
  async syncIdentify() {
2125
- const body = { anonymousId: this.anonymousId, userId: this.user?.id ?? null, traits: this.user?.traits, account: this.user?.account ? { id: this.user.account.id, traits: this.user.account.traits } : null, sessionId: this.session.id, url: this.collectUrls() ? location.href : void 0 };
2544
+ const body = { anonymousId: this.anonymousId, userId: this.user?.id ?? null, traits: this.user?.traits, account: this.user?.account ? { id: this.user.account.id, traits: this.user.account.traits } : null, sessionId: this.session.id, url: this.collectUrls() ? this.pageHref() : void 0 };
2126
2545
  const res = await postWithRetry(this.api, "/identify", body, 2);
2127
2546
  if (res?.ok && res.data) this.mergeServerState(res.data);
2128
2547
  }
@@ -2153,7 +2572,10 @@ var GuidelyClient = class _GuidelyClient {
2153
2572
  checklists,
2154
2573
  surveysSubmitted: Array.from(/* @__PURE__ */ new Set([...this.state.surveysSubmitted, ...data.surveysSubmitted || []])),
2155
2574
  audienceIds: data.audienceIds || [],
2156
- firstSeenAt: this.state.firstSeenAt ?? data.firstSeenAt
2575
+ firstSeenAt: this.state.firstSeenAt ?? data.firstSeenAt,
2576
+ // Device-local only (the server doesn't hold them).
2577
+ ...this.state.throttle ? { throttle: this.state.throttle } : {},
2578
+ ...this.state.announcementsSeenAt ? { announcementsSeenAt: this.state.announcementsSeenAt } : {}
2157
2579
  };
2158
2580
  if (data.firstSeenAt && Date.now() - Date.parse(data.firstSeenAt) > 6e4) this.firstVisit = false;
2159
2581
  this.persistState();
@@ -2189,7 +2611,7 @@ var GuidelyClient = class _GuidelyClient {
2189
2611
  this.stopGuide();
2190
2612
  this.user = null;
2191
2613
  this.store.remove(KEYS.user(this.options.projectId));
2192
- this.anonymousId = `anon_${(0, import_runtime15.randomId)(20)}`;
2614
+ this.anonymousId = `anon_${(0, import_runtime18.randomId)(20)}`;
2193
2615
  this.store.set(KEYS.anon, this.anonymousId);
2194
2616
  this.state = emptyState();
2195
2617
  this.persistState();
@@ -2221,6 +2643,7 @@ var GuidelyClient = class _GuidelyClient {
2221
2643
  if (consent === "denied") this.queue.clear();
2222
2644
  else if (consent === "granted") void this.queue.flush();
2223
2645
  if (consent !== "granted") this.replay?.stop({ discard: true });
2646
+ if (consent !== "pending") this.agentTurns.consentChanged(consent === "granted");
2224
2647
  this.syncCapture();
2225
2648
  }
2226
2649
  analyticsEnabled() {
@@ -2233,17 +2656,19 @@ var GuidelyClient = class _GuidelyClient {
2233
2656
  if (this.consent === "denied" || !this.options.analytics) return null;
2234
2657
  if (name !== "page_leave" && name !== "heatmap") this.touchSession();
2235
2658
  const event = {
2236
- eventId: (0, import_runtime15.randomId)(24),
2659
+ eventId: (0, import_runtime18.randomId)(24),
2237
2660
  name,
2238
2661
  occurredAt: nowIso(),
2239
2662
  anonymousId: this.anonymousId,
2240
2663
  userId: this.user?.id || null,
2241
2664
  accountId: this.user?.account?.id || null,
2242
2665
  sessionId: this.session.id,
2243
- url: this.collectUrls() ? location.href : void 0,
2666
+ url: this.collectUrls() ? this.pageHref() : void 0,
2244
2667
  ...extra
2245
2668
  };
2246
- if (extra.properties) event.properties = (0, import_runtime15.sanitizeProperties)(extra.properties);
2669
+ if (extra.properties) event.properties = (0, import_runtime18.sanitizeProperties)(extra.properties);
2670
+ const locale = extra.guideId ? this.guideLocales.get(extra.guideId) : void 0;
2671
+ if (locale) event.properties = { ...event.properties ?? {}, locale };
2247
2672
  this.queue.enqueue(event);
2248
2673
  this.replay?.addCustomEvent("guidely", { name, customName: event.customName, guideId: event.guideId });
2249
2674
  this.emitter.emit("track", event);
@@ -2267,9 +2692,30 @@ var GuidelyClient = class _GuidelyClient {
2267
2692
  this.completeChecklistTasksBy("event", name);
2268
2693
  if (this.isReady) this.evaluate("event", { eventName: name });
2269
2694
  }
2695
+ /**
2696
+ * Agent Analytics: records one or more turns of a conversation with your own AI agent or chatbot
2697
+ * (`{ conversationId, agentId, role, text, toolCalls?, latencyMs?, outcome?, … }`). The visitor's
2698
+ * anonymous id, user id, account id and session id are filled in, `agents.redact` from the init
2699
+ * options runs on each turn, and turns are retried until Guidely accepts them. Returns false when
2700
+ * nothing was queued (analytics off or consent denied, or every turn was invalid or dropped).
2701
+ */
2702
+ agentConversation(turns) {
2703
+ if (this.destroyed || !this.options.analytics || this.consent === "denied" || this.config?.settings?.analyticsEnabled === false) return false;
2704
+ const identity = { anonymousId: this.anonymousId, userId: this.user?.id || null, accountId: this.user?.account?.id || null, sessionId: this.session.id };
2705
+ const prepared = (Array.isArray(turns) ? turns : [turns]).map((t) => prepareAgentTurn(t, identity, this.options.agents?.redact, (err) => this.reportError(err))).filter((t) => !!t);
2706
+ if (!prepared.length) return false;
2707
+ this.touchSession();
2708
+ this.agentTurns.add(prepared, this.consent === "pending");
2709
+ return true;
2710
+ }
2711
+ /** Alias of {@link agentConversation}. */
2712
+ trackAgentTurn(turns) {
2713
+ return this.agentConversation(turns);
2714
+ }
2270
2715
  /** Records a page view and re-evaluates triggers. Called automatically on SPA navigation. */
2271
- page(url = location.href) {
2716
+ page(href) {
2272
2717
  if (this.destroyed) return;
2718
+ const url = this.pageHref(href);
2273
2719
  this.leavePage(url);
2274
2720
  this.trackPageView(url);
2275
2721
  this.handleNavigation(url, true);
@@ -2289,13 +2735,13 @@ var GuidelyClient = class _GuidelyClient {
2289
2735
  const st = this.active.controller.getState();
2290
2736
  if (st.status === "running" && !this.active.manual) {
2291
2737
  const g = this.active.guide;
2292
- if (g.config.urlPatterns.length && !g.config.urlPatterns.some((p) => (0, import_guide_runtime4.urlMatches)(p, url))) this.active.controller.destroy();
2738
+ if (g.config.urlPatterns.length && !g.config.urlPatterns.some((p) => (0, import_guide_runtime5.urlMatches)(p, url))) this.active.controller.destroy();
2293
2739
  }
2294
2740
  }
2295
2741
  this.widgets.forEach((w, id) => {
2296
2742
  if (!w.controller || w.manual) return;
2297
2743
  const g = this.findGuide(id);
2298
- if (g?.config.urlPatterns.length && !g.config.urlPatterns.some((p) => (0, import_guide_runtime4.urlMatches)(p, url))) w.controller.destroy();
2744
+ if (g?.config.urlPatterns.length && !g.config.urlPatterns.some((p) => (0, import_guide_runtime5.urlMatches)(p, url))) w.controller.destroy();
2299
2745
  });
2300
2746
  this.firedDelays.clear();
2301
2747
  if (this.isReady) this.evaluate("navigation");
@@ -2303,7 +2749,7 @@ var GuidelyClient = class _GuidelyClient {
2303
2749
  if (changed) void this.queue.flush();
2304
2750
  }
2305
2751
  handleDocumentClick(e) {
2306
- const target = (0, import_guide_runtime4.composedTarget)(e);
2752
+ const target = (0, import_guide_runtime5.composedTarget)(e);
2307
2753
  if (!target || typeof target.closest !== "function") return;
2308
2754
  this.autocaptureClick(e, target);
2309
2755
  this.completeChecklistTasksBy("click", target);
@@ -2361,6 +2807,8 @@ var GuidelyClient = class _GuidelyClient {
2361
2807
  const ref = !this.session.pageViews ? externalReferrer() : void 0;
2362
2808
  if (ref) props.referrer = ref;
2363
2809
  }
2810
+ const ep = this.captureAllowed() ? pagePropertyValues(this.config?.tagging, url, { mask: this.maskSelector(), maskAll: this.maskAllText() }) : void 0;
2811
+ if (ep) props.ep = ep;
2364
2812
  this.session.pageViews = (this.session.pageViews ?? 0) + 1;
2365
2813
  this.persistSession();
2366
2814
  this.enqueue("page_view", { url: collect ? url : void 0, properties: props });
@@ -2386,21 +2834,28 @@ var GuidelyClient = class _GuidelyClient {
2386
2834
  const heat = this.autocaptureOn("heatmaps");
2387
2835
  if (!clicks && !rage && !heat || isIgnoredElement(target)) return;
2388
2836
  this.touchSession();
2389
- const el = interactiveElement(target);
2837
+ const featureAttribute = this.config?.tagging?.featureAttribute ?? null;
2838
+ const feature = featureAttributeOf(target, featureAttribute);
2839
+ const el = interactiveElement(target) ?? feature?.el ?? null;
2390
2840
  const opts = { mask: this.maskSelector(), maskAll: this.maskAllText(), collectUrls: this.collectUrls() };
2391
- if (clicks && el) this.enqueue("click", { properties: { ...describeClick(el, e, opts) } });
2841
+ const clickProps = (on) => {
2842
+ const p = describeClick(on, e, opts);
2843
+ const ep = clickPropertyValues(this.config?.tagging, on, p, opts);
2844
+ return { ...p, el: elementSnapshot(on, { ...opts, featureAttribute }), ...feature ? { fa: feature.value } : {}, ...ep ? { ep } : {} };
2845
+ };
2846
+ if (clicks && el) this.enqueue("click", { properties: clickProps(el) });
2392
2847
  const count = this.rage.add(e.pageX ?? 0, e.pageY ?? 0, Date.now());
2393
- if (rage && count) this.enqueue("rage_click", { properties: { ...describeClick(el ?? target, e, opts), count } });
2848
+ if (rage && count) this.enqueue("rage_click", { properties: { ...clickProps(el ?? target), count } });
2394
2849
  if (!heat) return;
2395
- const url = location.href;
2850
+ const url = this.pageHref();
2396
2851
  if (this.heatmap.url !== url) this.flushHeatmap();
2397
2852
  if (this.heatmap.add(url, heatmapPoint(el ?? target, e, this.rage.burst > 0), this.rage.burst)) this.flushHeatmap();
2398
2853
  }
2399
2854
  /** Bins one pointer sample into the heatmap buffer. Not session activity. */
2400
2855
  sampleMove(e) {
2401
- const target = (0, import_guide_runtime4.composedTarget)(e);
2856
+ const target = (0, import_guide_runtime5.composedTarget)(e);
2402
2857
  if (document.visibilityState === "hidden" || !target?.closest || !this.autocaptureOn("mouseMovement") || isIgnoredElement(target)) return;
2403
- const url = location.href;
2858
+ const url = this.pageHref();
2404
2859
  if (this.heatmap.url !== url) this.flushHeatmap();
2405
2860
  if (this.heatmap.move(url, movementBin(interactiveElement(target) ?? target, e))) this.flushHeatmap();
2406
2861
  }
@@ -2476,7 +2931,7 @@ var GuidelyClient = class _GuidelyClient {
2476
2931
  if (!this.isReady || this.destroyed) return;
2477
2932
  if (this.config && !this.initialPageViewSent && this.config.settings?.autocapture) {
2478
2933
  this.initialPageViewSent = true;
2479
- this.trackPageView(location.href);
2934
+ this.trackPageView(this.pageHref());
2480
2935
  }
2481
2936
  if (this.config) {
2482
2937
  for (const [props, extra] of this.earlyExceptions.splice(0)) {
@@ -2489,7 +2944,7 @@ var GuidelyClient = class _GuidelyClient {
2489
2944
  syncReplay() {
2490
2945
  const cfg = this.isReady && this.captureAllowed() ? this.replayConfig() : null;
2491
2946
  if (cfg && this.session.replay === void 0) {
2492
- this.session.replay = (0, import_runtime15.rolloutBucket)(this.session.id, "replay") < cfg.sampleRate;
2947
+ this.session.replay = (0, import_runtime18.rolloutBucket)(this.session.id, "replay") < cfg.sampleRate;
2493
2948
  this.persistSession();
2494
2949
  }
2495
2950
  if (!cfg || !this.session.replay) {
@@ -2511,7 +2966,7 @@ var GuidelyClient = class _GuidelyClient {
2511
2966
  return {
2512
2967
  anonymousId: this.anonymousId,
2513
2968
  userId: this.user?.id || null,
2514
- url: this.collectUrls() ? location.href : void 0,
2969
+ url: this.collectUrls() ? this.pageHref() : void 0,
2515
2970
  device: { type: device.type, browser: device.browser, os: device.os },
2516
2971
  viewport: { width: window.innerWidth, height: window.innerHeight },
2517
2972
  sdk: { name: SDK_NAME, version: SDK_VERSION }
@@ -2562,6 +3017,14 @@ var GuidelyClient = class _GuidelyClient {
2562
3017
  // ---------------------------------------------------------------------------
2563
3018
  // Targeting + triggers
2564
3019
  // ---------------------------------------------------------------------------
3020
+ /** The page URL Guidely works with: `options.pageUrl`, else app-relative for `file:` / custom protocols (`normalizeAppUrl`). */
3021
+ pageHref(href = location.href) {
3022
+ try {
3023
+ return this.options.pageUrl ? this.options.pageUrl(href) || href : normalizeAppUrl(href);
3024
+ } catch {
3025
+ return href;
3026
+ }
3027
+ }
2565
3028
  /** Builds the targeting context for the current moment. */
2566
3029
  getContext() {
2567
3030
  const guides = this.state.guides;
@@ -2572,7 +3035,7 @@ var GuidelyClient = class _GuidelyClient {
2572
3035
  userId: this.user?.id || null,
2573
3036
  userAttributes: this.user?.traits ?? {},
2574
3037
  accountAttributes: this.user?.account?.traits ?? {},
2575
- url: location.href,
3038
+ url: this.pageHref(),
2576
3039
  firstVisit: this.firstVisit,
2577
3040
  eventsSeen: this.session.events,
2578
3041
  completedGuides: keysOf(Object.keys(guides).filter((id) => guides[id]?.completedAt)),
@@ -2581,17 +3044,64 @@ var GuidelyClient = class _GuidelyClient {
2581
3044
  completedChecklists: keysOf(completedChecklists),
2582
3045
  audienceIds: this.state.audienceIds,
2583
3046
  environment: this.options.environment,
2584
- platform: "web"
3047
+ platform: "web",
3048
+ ...this.options.appVersion ? { appVersion: this.options.appVersion } : {}
2585
3049
  });
2586
3050
  }
2587
3051
  isTargeted(guide, ctx, url) {
2588
3052
  const c = guide.config;
2589
- if (c.urlPatterns.length && !c.urlPatterns.some((p) => (0, import_guide_runtime4.urlMatches)(p, url))) return false;
2590
- if (c.trigger.urlPattern && !(0, import_guide_runtime4.urlMatches)(c.trigger.urlPattern, url)) return false;
3053
+ if (c.urlPatterns.length && !c.urlPatterns.some((p) => (0, import_guide_runtime5.urlMatches)(p, url))) return false;
3054
+ if (c.trigger.urlPattern && !(0, import_guide_runtime5.urlMatches)(c.trigger.urlPattern, url)) return false;
2591
3055
  if (!isWithinSchedule(guide)) return false;
2592
3056
  if (guide.kind === "survey" && (this.state.surveysSubmitted.includes(guide.id) || this.state.surveysSubmitted.includes(guide.key))) return false;
2593
3057
  ctx.rolloutSalt = guide.id;
2594
- return (0, import_runtime15.evaluateRules)(c.targeting, ctx);
3058
+ return (0, import_runtime18.evaluateRules)(c.targeting, ctx);
3059
+ }
3060
+ /** Guide throttling: may this guide launch automatically now? */
3061
+ throttleAllows(guide) {
3062
+ if (!countsTowardThrottle(guide)) return true;
3063
+ return isThrottleEligible(this.config?.settings.throttling, this.state.throttle ?? []);
3064
+ }
3065
+ recordLaunch(guide, manual) {
3066
+ if (manual || !countsTowardThrottle(guide) || !this.config?.settings.throttling?.enabled) return;
3067
+ this.state.throttle = pruneLaunches([...this.state.throttle ?? [], nowIso()], this.config.settings.throttling);
3068
+ this.persistState();
3069
+ }
3070
+ /** Guides and A/B test control groups, which are evaluated the same way but never render. */
3071
+ candidates() {
3072
+ const guides = this.config?.guides ?? [];
3073
+ const holdouts = this.config?.guideHoldouts ?? [];
3074
+ return holdouts.length ? [...guides, ...holdouts.map((h) => h.guide)] : guides;
3075
+ }
3076
+ holdoutFor(guide) {
3077
+ return this.config?.guideHoldouts?.find((h) => h.guide.id === guide.id) ?? null;
3078
+ }
3079
+ /** The visitor's language candidates, most preferred first. */
3080
+ locales() {
3081
+ const nav = typeof navigator !== "undefined" ? [...navigator.languages ?? [], navigator.language] : [];
3082
+ return (0, import_runtime18.visitorLocales)(this.locale ?? this.options.locale, this.user?.traits, nav);
3083
+ }
3084
+ defaultLocale() {
3085
+ return this.config?.settings.localization?.defaultLocale || "en";
3086
+ }
3087
+ /** The guide in the visitor's language (remembered so its events carry `locale`). */
3088
+ localize(guide) {
3089
+ const localized = (0, import_runtime18.localizeGuide)(guide, this.locales(), this.defaultLocale());
3090
+ this.guideLocales.set(guide.id, localized.locale);
3091
+ return localized;
3092
+ }
3093
+ /**
3094
+ * Sets the language for guides and the resource center (`null` = back to the traits / browser).
3095
+ * Guides already on screen keep their language; the resource center re-renders.
3096
+ */
3097
+ setLocale(locale) {
3098
+ this.locale = locale ? (0, import_runtime18.normalizeLocale)(locale) || null : null;
3099
+ if (this.resourceCenter) {
3100
+ this.resourceCenter.destroy();
3101
+ this.resourceCenterScope?.destroy();
3102
+ this.resourceCenter = null;
3103
+ this.renderPersistentWidgets();
3104
+ }
2595
3105
  }
2596
3106
  isEligible(guide, ctx, url) {
2597
3107
  if (!this.isTargeted(guide, ctx, url)) return false;
@@ -2609,13 +3119,13 @@ var GuidelyClient = class _GuidelyClient {
2609
3119
  if (t.delayMs > 0) return reason === "timer" && detail.guideId === guide.id;
2610
3120
  return reason === "ready" || reason === "navigation" || reason === "identify";
2611
3121
  case "url_match":
2612
- return (reason === "ready" || reason === "navigation" || reason === "identify") && (0, import_guide_runtime4.urlMatches)(t.value, location.href);
3122
+ return (reason === "ready" || reason === "navigation" || reason === "identify") && (0, import_guide_runtime5.urlMatches)(t.value, this.pageHref());
2613
3123
  case "event":
2614
3124
  return reason === "event" && !!t.value && detail.eventName === t.value;
2615
3125
  case "element_click":
2616
3126
  if (reason !== "click" || !t.value || !detail.target) return false;
2617
3127
  try {
2618
- return !!(0, import_guide_runtime4.closestComposed)(detail.target, t.value);
3128
+ return !!(0, import_guide_runtime5.closestComposed)(detail.target, t.value);
2619
3129
  } catch {
2620
3130
  return false;
2621
3131
  }
@@ -2634,15 +3144,15 @@ var GuidelyClient = class _GuidelyClient {
2634
3144
  }
2635
3145
  evaluate(reason, detail = {}) {
2636
3146
  if (!this.config || this.destroyed || this.editing()) return;
2637
- const url = location.href;
3147
+ const url = this.pageHref();
2638
3148
  const ctx = this.getContext();
2639
3149
  if (reason === "identify") {
2640
- const current2 = new Set(this.state.audienceIds);
2641
- detail.newAudiences = new Set(Array.from(current2).filter((a) => !this.lastAudiences.has(a)));
2642
- this.lastAudiences = current2;
3150
+ const current3 = new Set(this.state.audienceIds);
3151
+ detail.newAudiences = new Set(Array.from(current3).filter((a) => !this.lastAudiences.has(a)));
3152
+ this.lastAudiences = current3;
2643
3153
  }
2644
3154
  const candidates = [];
2645
- for (const guide of this.config.guides) {
3155
+ for (const guide of this.candidates()) {
2646
3156
  const t = guide.config.trigger;
2647
3157
  if (detail.endedGuide && detail.endedGuide.id === guide.id) continue;
2648
3158
  if (reason === "ready" || reason === "navigation") {
@@ -2658,6 +3168,10 @@ var GuidelyClient = class _GuidelyClient {
2658
3168
  if (t.type === "page_load" && t.delayMs > 0 && reason === "timer" && detail.guideId !== guide.id) continue;
2659
3169
  if (!this.triggerFires(guide, reason, detail)) continue;
2660
3170
  if (!this.isEligible(guide, ctx, url)) continue;
3171
+ if (!this.throttleAllows(guide)) {
3172
+ this.log("throttled", guide.key);
3173
+ continue;
3174
+ }
2661
3175
  candidates.push(guide);
2662
3176
  }
2663
3177
  if (reason === "after_guide" || reason === "navigation") this.drainQueue(url, ctx);
@@ -2685,7 +3199,7 @@ var GuidelyClient = class _GuidelyClient {
2685
3199
  this.visibilityObservers.delete(guide.id);
2686
3200
  };
2687
3201
  this.visibilityObservers.set(guide.id, stop);
2688
- (0, import_guide_runtime4.waitForTarget)([{ kind: "css", value: selector }], { timeoutMs: 0, signal: ac.signal }).then((el) => {
3202
+ (0, import_guide_runtime5.waitForTarget)([{ kind: "css", value: selector }], { timeoutMs: 0, signal: ac.signal }).then((el) => {
2689
3203
  if (ac.signal.aborted) return;
2690
3204
  const fire = () => {
2691
3205
  stop();
@@ -2708,6 +3222,13 @@ var GuidelyClient = class _GuidelyClient {
2708
3222
  /** Decides whether a guide can show now, otherwise queues it. */
2709
3223
  tryShow(guide, url, manual) {
2710
3224
  if (this.destroyed) return false;
3225
+ if (!manual && !this.throttleAllows(guide)) return false;
3226
+ const holdout = this.holdoutFor(guide);
3227
+ if (holdout) {
3228
+ if (this.active && !manual) return false;
3229
+ this.recordImpression(guide, manual);
3230
+ return true;
3231
+ }
2711
3232
  if (!isExclusiveGuide(guide)) {
2712
3233
  if (this.widgets.has(guide.id)) return false;
2713
3234
  this.show(guide, manual);
@@ -2748,14 +3269,18 @@ var GuidelyClient = class _GuidelyClient {
2748
3269
  guideState(id) {
2749
3270
  return this.state.guides[id] ?? (this.state.guides[id] = { impressions: 0 });
2750
3271
  }
2751
- recordImpression(guide) {
3272
+ recordImpression(guide, manual = false) {
2752
3273
  const s = this.guideState(guide.id);
2753
3274
  s.impressions += 1;
2754
3275
  s.lastShownAt = nowIso();
2755
3276
  if (!this.session.shownGuides.includes(guide.id)) this.session.shownGuides.push(guide.id);
3277
+ this.recordLaunch(guide, manual);
2756
3278
  this.persistState();
2757
3279
  this.persistSession();
2758
- this.syncGuideState(guide.id, { impression: true });
3280
+ const holdout = this.holdoutFor(guide);
3281
+ const experiment = holdout ? { id: holdout.experimentId, variantId: holdout.variantId } : guide.experiment;
3282
+ if (experiment) this.enqueue("experiment_viewed", { properties: { guideExperimentId: experiment.id, variantId: experiment.variantId, ...holdout ? {} : { guideId: guide.id } } });
3283
+ if (!holdout) this.syncGuideState(guide.id, { impression: true });
2759
3284
  }
2760
3285
  recordEnd(guide, reason) {
2761
3286
  const s = this.guideState(guide.id);
@@ -2779,13 +3304,17 @@ var GuidelyClient = class _GuidelyClient {
2779
3304
  this.reportError(err);
2780
3305
  }
2781
3306
  }
2782
- showFlow(guide, manual, stepId) {
3307
+ showFlow(raw, manual, stepId) {
2783
3308
  if (this.editing()) return;
3309
+ const guide = this.localize(raw);
2784
3310
  const exclusive = isExclusiveGuide(guide);
2785
- const entry = { guide, manual, startUrl: location.href };
2786
- const controller = (0, import_guide_runtime4.runGuide)(guide, {
3311
+ const entry = { guide, manual, startUrl: this.pageHref() };
3312
+ const controller = (0, import_guide_runtime5.runGuide)(guide, {
2787
3313
  theme: this.theme(),
3314
+ // Code blocks only arrive when a workspace admin allowed them (the server strips them otherwise).
3315
+ runCode: true,
2788
3316
  ctx: () => this.getContext(),
3317
+ currentUrl: () => this.pageHref(),
2789
3318
  startAtStep: stepId,
2790
3319
  eventBus: this.eventBus,
2791
3320
  // Started from Help, Scout or startGuide(): steps on other pages offer to take the user there.
@@ -2799,7 +3328,7 @@ var GuidelyClient = class _GuidelyClient {
2799
3328
  const step = e.stepId ? guide.config.steps.find((s) => s.id === e.stepId) : void 0;
2800
3329
  this.enqueue(e.name, { guideId: guide.id, guideVersion: guide.version, stepId: e.stepId, stepIndex: e.stepIndex, properties: e.properties });
2801
3330
  if (e.name === "guide_started") {
2802
- this.recordImpression(guide);
3331
+ this.recordImpression(guide, manual);
2803
3332
  this.emitter.emit("guide:start", guide);
2804
3333
  }
2805
3334
  if (e.name === "step_viewed" && step) {
@@ -2846,14 +3375,15 @@ var GuidelyClient = class _GuidelyClient {
2846
3375
  this.widgets.delete(guide.id);
2847
3376
  }
2848
3377
  }
2849
- showChecklist(guide) {
3378
+ showChecklist(raw) {
2850
3379
  if (this.editing()) return;
3380
+ const guide = this.localize(raw);
2851
3381
  const config = guide.config.checklist;
2852
3382
  if (!config || this.widgets.has(guide.id)) return;
2853
- const scope = (0, import_guide_runtime5.createScope)(this.theme(), guide.config.appearance);
3383
+ const scope = (0, import_guide_runtime6.createScope)(this.theme(), guide.config.appearance);
2854
3384
  const progress = this.getChecklistProgress(guide.id);
2855
3385
  const first = !this.state.guides[guide.id]?.impressions;
2856
- const widget = (0, import_guide_runtime4.renderChecklist)({
3386
+ const widget = (0, import_guide_runtime5.renderChecklist)({
2857
3387
  scope: scope.el,
2858
3388
  config,
2859
3389
  progress,
@@ -2868,12 +3398,13 @@ var GuidelyClient = class _GuidelyClient {
2868
3398
  }, checklist: widget, scope });
2869
3399
  if (first) this.recordImpression(guide);
2870
3400
  }
2871
- showSurvey(guide, manual) {
3401
+ showSurvey(raw, manual) {
2872
3402
  if (this.editing()) return;
3403
+ const guide = this.localize(raw);
2873
3404
  const config = guide.config.survey;
2874
3405
  if (!config) return;
2875
- const scope = (0, import_guide_runtime5.createScope)(this.theme(), guide.config.appearance);
2876
- const entry = { guide, manual, startUrl: location.href, scope };
3406
+ const scope = (0, import_guide_runtime6.createScope)(this.theme(), guide.config.appearance);
3407
+ const entry = { guide, manual, startUrl: this.pageHref(), scope };
2877
3408
  const finish = (reason) => {
2878
3409
  widget.destroy();
2879
3410
  scope.destroy();
@@ -2885,13 +3416,13 @@ var GuidelyClient = class _GuidelyClient {
2885
3416
  }
2886
3417
  this.setTimer(() => this.evaluate("after_guide", { endedGuide: guide }), 0);
2887
3418
  };
2888
- const widget = (0, import_guide_runtime4.renderSurvey)({
3419
+ const widget = (0, import_guide_runtime5.renderSurvey)({
2889
3420
  scope: scope.el,
2890
3421
  config,
2891
3422
  title: guide.name,
2892
3423
  onDismiss: () => finish(this.state.surveysSubmitted.includes(guide.id) ? "submitted" : "dismissed"),
2893
3424
  onSubmit: (answers) => {
2894
- const responseId = (0, import_runtime15.randomId)(24);
3425
+ const responseId = (0, import_runtime18.randomId)(24);
2895
3426
  this.state.surveysSubmitted.push(guide.id);
2896
3427
  this.persistState();
2897
3428
  this.recordEnd(guide, "completed");
@@ -2904,7 +3435,7 @@ var GuidelyClient = class _GuidelyClient {
2904
3435
  });
2905
3436
  entry.survey = widget;
2906
3437
  this.active = entry;
2907
- this.recordImpression(guide);
3438
+ this.recordImpression(guide, manual);
2908
3439
  this.enqueue("survey_displayed", { guideId: guide.id, guideVersion: guide.version });
2909
3440
  this.emitter.emit("guide:start", guide);
2910
3441
  }
@@ -2913,6 +3444,7 @@ var GuidelyClient = class _GuidelyClient {
2913
3444
  }
2914
3445
  navigate(url, newTab) {
2915
3446
  try {
3447
+ if (this.openPrototypeTest(url)) return;
2916
3448
  if (newTab) window.open(url, "_blank", "noopener");
2917
3449
  else location.assign(url);
2918
3450
  } catch (err) {
@@ -2933,11 +3465,17 @@ var GuidelyClient = class _GuidelyClient {
2933
3465
  }
2934
3466
  this.destroyMessenger();
2935
3467
  if (rc && rc.enabled && !this.resourceCenter) {
2936
- const scope = (0, import_guide_runtime5.createScope)(this.theme());
3468
+ const scope = (0, import_guide_runtime6.createScope)(this.theme());
2937
3469
  this.resourceCenterScope = scope;
2938
- this.resourceCenter = (0, import_guide_runtime4.renderResourceCenter)({
3470
+ this.resourceCenter = (0, import_guide_runtime5.renderResourceCenter)({
2939
3471
  scope: scope.el,
2940
- config: rc,
3472
+ config: (0, import_runtime18.localizeResourceCenter)(rc, this.locales(), this.defaultLocale()),
3473
+ evaluate: (rules) => (0, import_runtime18.evaluateRules)(rules, this.getContext()),
3474
+ announcementsSeenAt: () => this.state.announcementsSeenAt ?? null,
3475
+ onAnnouncementsSeen: () => {
3476
+ this.state.announcementsSeenAt = nowIso();
3477
+ this.persistState();
3478
+ },
2941
3479
  guides: this.config.guides.map((g) => ({ id: g.id, key: g.key, name: g.name, kind: g.kind })),
2942
3480
  search: (q) => this.searchResourceCenter(q),
2943
3481
  audienceIds: () => this.state.audienceIds,
@@ -2968,10 +3506,10 @@ var GuidelyClient = class _GuidelyClient {
2968
3506
  loadMessenger() {
2969
3507
  if (this.messenger) return Promise.resolve(this.messenger);
2970
3508
  if (this.messengerLoading) return this.messengerLoading;
2971
- this.messengerLoading = import("@guidelytech/guide-runtime/messenger").then(({ renderMessenger }) => {
3509
+ this.messengerLoading = import("@guidelytech/guide-runtime/messenger").then(({ renderMessenger, parseLauncherSpot }) => {
2972
3510
  const cfg = this.config?.messenger;
2973
3511
  if (this.destroyed || !this.config || !cfg?.enabled || this.messenger) return this.messenger;
2974
- const scope = (0, import_guide_runtime5.createScope)(this.theme());
3512
+ const scope = (0, import_guide_runtime6.createScope)(this.theme());
2975
3513
  this.messengerScope = scope;
2976
3514
  this.messenger = renderMessenger({
2977
3515
  scope: scope.el,
@@ -2982,16 +3520,20 @@ var GuidelyClient = class _GuidelyClient {
2982
3520
  projectId: this.options.projectId,
2983
3521
  anonymousId: () => this.anonymousId,
2984
3522
  userId: () => this.user?.id || null,
2985
- pageUrl: () => this.collectUrls() ? location.href : void 0
3523
+ pageUrl: () => this.collectUrls() ? this.pageHref() : void 0,
3524
+ locale: () => this.rcLocale()
2986
3525
  }),
2987
3526
  visitor: () => this.messengerVisitor(),
3527
+ launcherSpot: parseLauncherSpot(this.store.get(KEYS.launcher(this.options.projectId))),
3528
+ onLauncherMove: (spot) => writeJson(this.store, KEYS.launcher(this.options.projectId), spot),
2988
3529
  brand: { logoUrl: this.theme().logoUrl ?? null, name: this.theme().brandName ?? null },
2989
3530
  onResumeGuide: (guideId, stepId) => this.startGuide(guideId, { stepId }),
2990
3531
  onNewsSeen: (ids) => {
2991
3532
  const seen = readJson(this.store, KEYS.newsSeen(this.options.projectId), []);
2992
3533
  writeJson(this.store, KEYS.newsSeen(this.options.projectId), Array.from(/* @__PURE__ */ new Set([...ids, ...Array.isArray(seen) ? seen : []])).slice(0, 200));
2993
3534
  },
2994
- resourceCenter: this.config.resourceCenter,
3535
+ resourceCenter: this.config.resourceCenter ? (0, import_runtime18.localizeResourceCenter)(this.config.resourceCenter, this.locales(), this.defaultLocale()) : null,
3536
+ evaluate: (rules) => (0, import_runtime18.evaluateRules)(rules, this.getContext()),
2995
3537
  audienceIds: () => this.state.audienceIds,
2996
3538
  guides: this.config.guides.map((g) => ({ id: g.id, key: g.key, name: g.name, kind: g.kind })),
2997
3539
  onStartGuide: (idOrKey) => this.startGuide(idOrKey),
@@ -3015,7 +3557,7 @@ var GuidelyClient = class _GuidelyClient {
3015
3557
  /** What the messenger personalises from, computed fresh on every render. */
3016
3558
  messengerVisitor() {
3017
3559
  const ctx = this.getContext();
3018
- const url = location.href;
3560
+ const url = this.pageHref();
3019
3561
  const seen = readJson(this.store, KEYS.newsSeen(this.options.projectId), []);
3020
3562
  const visitor = buildMessengerVisitor({
3021
3563
  guides: this.config?.guides ?? [],
@@ -3026,11 +3568,24 @@ var GuidelyClient = class _GuidelyClient {
3026
3568
  isAudienceTargeted: (g) => {
3027
3569
  if (!isWithinSchedule(g)) return false;
3028
3570
  ctx.rolloutSalt = g.id;
3029
- return (0, import_runtime15.evaluateRules)(g.config.targeting, ctx);
3571
+ return (0, import_runtime18.evaluateRules)(g.config.targeting, ctx);
3030
3572
  },
3031
3573
  newsSeen: Array.isArray(seen) ? seen : []
3032
3574
  });
3033
- return { ...visitor, identified: !!this.user?.id };
3575
+ const rc = this.config?.resourceCenter;
3576
+ const rcNews = rc?.announcements?.length ? (0, import_runtime18.visibleAnnouncements)((0, import_runtime18.localizeResourceCenter)(rc, this.locales(), this.defaultLocale()).announcements, this.state.audienceIds).map((a) => ({
3577
+ id: `rc_${a.id}`,
3578
+ key: a.guideId ?? "",
3579
+ title: a.title,
3580
+ body: plainText(a.body),
3581
+ imageUrl: null,
3582
+ publishedAt: a.publishedAt,
3583
+ unread: !(Array.isArray(seen) && seen.includes(`rc_${a.id}`)),
3584
+ articleId: a.articleId ?? null,
3585
+ url: a.url ?? null
3586
+ })) : [];
3587
+ const news = rcNews.length ? [...visitor.news, ...rcNews].sort((a, b) => b.publishedAt.localeCompare(a.publishedAt)).slice(0, 10) : visitor.news;
3588
+ return { ...visitor, news, identified: !!this.user?.id };
3034
3589
  }
3035
3590
  destroyMessenger() {
3036
3591
  this.messenger?.destroy();
@@ -3038,9 +3593,17 @@ var GuidelyClient = class _GuidelyClient {
3038
3593
  this.messenger = null;
3039
3594
  this.messengerScope = null;
3040
3595
  }
3596
+ /** The resource center's language for this visitor, or null for the default language. */
3597
+ rcLocale() {
3598
+ const rc = this.config?.resourceCenter;
3599
+ if (!rc) return null;
3600
+ const locale = (0, import_runtime18.localizeResourceCenter)(rc, this.locales(), this.defaultLocale()).locale;
3601
+ return locale.toLowerCase() === this.defaultLocale().toLowerCase() ? null : locale;
3602
+ }
3041
3603
  async loadArticle(id) {
3042
3604
  try {
3043
- const res = await this.api.get(`/articles/${encodeURIComponent(id)}`);
3605
+ const locale = this.rcLocale();
3606
+ const res = await this.api.get(`/articles/${encodeURIComponent(id)}`, locale ? { params: { locale } } : void 0);
3044
3607
  return res.ok && res.data ? res.data.article : null;
3045
3608
  } catch {
3046
3609
  return null;
@@ -3078,7 +3641,7 @@ var GuidelyClient = class _GuidelyClient {
3078
3641
  if (progress.completedTaskIds.includes(task.id)) return;
3079
3642
  progress.completedTaskIds.push(task.id);
3080
3643
  this.enqueue("checklist_task_completed", { guideId: guide.id, guideVersion: guide.version, properties: { taskId: task.id } });
3081
- const complete = (0, import_guide_runtime4.isChecklistComplete)(config, progress) && !progress.completedAt;
3644
+ const complete = (0, import_guide_runtime5.isChecklistComplete)(config, progress) && !progress.completedAt;
3082
3645
  if (complete) {
3083
3646
  progress.completedAt = nowIso();
3084
3647
  this.enqueue("checklist_completed", { guideId: guide.id, guideVersion: guide.version });
@@ -3101,11 +3664,11 @@ var GuidelyClient = class _GuidelyClient {
3101
3664
  if (task.completion !== kind || !task.completionValue) continue;
3102
3665
  let hit = false;
3103
3666
  if (kind === "event") hit = value === task.completionValue;
3104
- else if (kind === "url_visited") hit = (0, import_guide_runtime4.urlMatches)(task.completionValue, value);
3667
+ else if (kind === "url_visited") hit = (0, import_guide_runtime5.urlMatches)(task.completionValue, value);
3105
3668
  else if (kind === "guide_completed") hit = value.id === task.completionValue || value.key === task.completionValue;
3106
3669
  else if (kind === "click") {
3107
3670
  try {
3108
- hit = !!(0, import_guide_runtime4.closestComposed)(value, task.completionValue);
3671
+ hit = !!(0, import_guide_runtime5.closestComposed)(value, task.completionValue);
3109
3672
  } catch {
3110
3673
  hit = false;
3111
3674
  }
@@ -3216,6 +3779,18 @@ var GuidelyClient = class _GuidelyClient {
3216
3779
  closeMessenger() {
3217
3780
  this.messenger?.close();
3218
3781
  }
3782
+ /**
3783
+ * Opens a Guidely prototype test (its `/validate/<token>` URL on this SDK's API origin, or the bare
3784
+ * token) in a full-screen overlay, attributed to the current user. Returns false, doing nothing, for
3785
+ * any other URL. Guide buttons that open a test URL use this automatically.
3786
+ */
3787
+ openPrototypeTest(urlOrToken) {
3788
+ if (typeof document === "undefined") return false;
3789
+ const url = matchPrototypeTestUrl(urlOrToken, this.options.apiUrl);
3790
+ if (!url) return false;
3791
+ openPrototypeOverlay(url, { key: this.options.projectId, anonymousId: this.anonymousId, userId: this.user?.id ?? null });
3792
+ return true;
3793
+ }
3219
3794
  /**
3220
3795
  * Opens the complaint form in the messenger, optionally with a category picked (its id from
3221
3796
  * Complaints → Categories). Does nothing when the project doesn't take complaints.
@@ -3224,6 +3799,14 @@ var GuidelyClient = class _GuidelyClient {
3224
3799
  if (!this.config?.messenger?.enabled || !this.config.messenger.complaints?.categories.length) return;
3225
3800
  void this.loadMessenger().then((m) => m?.openComplaint(categoryId));
3226
3801
  }
3802
+ /**
3803
+ * Opens the Ideas tab in the messenger (feature requests and voting), or one idea by its id.
3804
+ * Does nothing when the project hasn't turned on Ideas.
3805
+ */
3806
+ openIdeas(requestId) {
3807
+ if (!this.config?.messenger?.enabled || !this.config.messenger.ideas) return;
3808
+ void this.loadMessenger().then((m) => m?.openIdeas(requestId));
3809
+ }
3227
3810
  /** Opens a new messenger conversation, optionally pre-filling the message. */
3228
3811
  showNewMessage(text) {
3229
3812
  if (!this.config?.messenger?.enabled) return;
@@ -3253,14 +3836,15 @@ var GuidelyClient = class _GuidelyClient {
3253
3836
  const device = detectDevice(navigator.userAgent, window.innerWidth);
3254
3837
  const account = this.user?.account;
3255
3838
  return {
3256
- url: location.href,
3257
- path: location.pathname,
3839
+ url: this.pageHref(),
3840
+ path: pathOf2(this.pageHref()),
3258
3841
  deviceType: device.type,
3259
3842
  browser: device.browser,
3260
3843
  os: device.os,
3261
3844
  locale: navigator.language || void 0,
3262
3845
  environment: this.options.environment,
3263
3846
  platform: "web",
3847
+ ...this.options.appVersion ? { appVersion: this.options.appVersion } : {},
3264
3848
  ...this.user?.traits ?? {},
3265
3849
  id: this.user?.id || this.anonymousId,
3266
3850
  anonymousId: this.anonymousId,
@@ -3420,20 +4004,20 @@ var GuidelyClient = class _GuidelyClient {
3420
4004
  // ---------------------------------------------------------------------------
3421
4005
  /** Creates (or returns the existing) client and starts initialisation. */
3422
4006
  static init(options) {
3423
- if (current && !current.destroyed && current.options.projectId === options.projectId) return current;
3424
- current?.destroy();
3425
- current = new _GuidelyClient(options);
3426
- void current.init();
3427
- return current;
4007
+ if (current2 && !current2.destroyed && current2.options.projectId === options.projectId) return current2;
4008
+ current2?.destroy();
4009
+ current2 = new _GuidelyClient(options);
4010
+ void current2.init();
4011
+ return current2;
3428
4012
  }
3429
4013
  /** The most recently created client, if any. */
3430
4014
  static get() {
3431
- return current && !current.destroyed ? current : null;
4015
+ return current2 && !current2.destroyed ? current2 : null;
3432
4016
  }
3433
4017
  };
3434
- var current = null;
4018
+ var current2 = null;
3435
4019
  function webOnly(config) {
3436
- return config.guides.every((g) => (0, import_runtime15.supportsPlatform)(g.config, "web")) ? config : { ...config, guides: config.guides.filter((g) => (0, import_runtime15.supportsPlatform)(g.config, "web")) };
4020
+ return config.guides.every((g) => (0, import_runtime18.supportsPlatform)(g.config, "web")) ? config : { ...config, guides: config.guides.filter((g) => (0, import_runtime18.supportsPlatform)(g.config, "web")) };
3437
4021
  }
3438
4022
  function pathOf2(url) {
3439
4023
  try {
@@ -3453,7 +4037,7 @@ function externalReferrer() {
3453
4037
  }
3454
4038
  function createGuidely(options) {
3455
4039
  const client = new GuidelyClient(options);
3456
- current = client;
4040
+ current2 = client;
3457
4041
  void client.init();
3458
4042
  return client;
3459
4043
  }
@@ -3476,17 +4060,24 @@ var index_default = Guidely;
3476
4060
  GuidelyClient,
3477
4061
  HEARTBEAT_MS,
3478
4062
  KEYS,
4063
+ PROTOTYPE_CLOSE_MESSAGE,
3479
4064
  SDK_NAME,
3480
4065
  SDK_VERSION,
3481
4066
  areMutuallyExclusive,
3482
4067
  buildContext,
3483
4068
  captureSnapshot,
4069
+ countsTowardThrottle,
3484
4070
  createGuidely,
3485
4071
  createStorage,
3486
4072
  detectDevice,
4073
+ isAppUrl,
3487
4074
  isExclusiveGuide,
3488
4075
  isFrequencyEligible,
4076
+ isThrottleEligible,
3489
4077
  isWithinSchedule,
4078
+ matchPrototypeTestUrl,
4079
+ normalizeAppUrl,
4080
+ pruneLaunches,
3490
4081
  watchNavigation
3491
4082
  });
3492
4083
  //# sourceMappingURL=index.cjs.map