@guidelytech/sdk-core 0.10.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/dist/{capture-DVJyQ2JZ.d.cts → capture-CgN69Lxq.d.cts} +2 -2
  2. package/dist/{capture-369s4YYT.d.ts → capture-Csz721-A.d.ts} +2 -2
  3. package/dist/capture.cjs +2 -0
  4. package/dist/capture.cjs.map +1 -1
  5. package/dist/capture.d.cts +3 -3
  6. package/dist/capture.d.ts +3 -3
  7. package/dist/capture.js +1 -1
  8. package/dist/{chunk-C4WWSF67.js → chunk-JUSM7J5N.js} +3 -1
  9. package/dist/{chunk-C4WWSF67.js.map → chunk-JUSM7J5N.js.map} +1 -1
  10. package/dist/{chunk-GDJB466J.js → chunk-ZAF2KRJA.js} +29 -6
  11. package/dist/chunk-ZAF2KRJA.js.map +1 -0
  12. package/dist/headless.cjs +32 -4
  13. package/dist/headless.cjs.map +1 -1
  14. package/dist/headless.d.cts +6 -5
  15. package/dist/headless.d.ts +6 -5
  16. package/dist/headless.js +9 -3
  17. package/dist/{health-DFHUstWd.d.cts → health-sbP9JwTE.d.cts} +2 -0
  18. package/dist/{health-DFHUstWd.d.ts → health-sbP9JwTE.d.ts} +2 -0
  19. package/dist/{http-D0dCcpLN.d.cts → http-Cb6KYYFv.d.cts} +1 -1
  20. package/dist/{http-DJd8jRhK.d.ts → http-D047Ngxk.d.ts} +1 -1
  21. package/dist/index.cjs +1632 -1045
  22. package/dist/index.cjs.map +1 -1
  23. package/dist/index.d.cts +77 -9
  24. package/dist/index.d.ts +77 -9
  25. package/dist/index.js +760 -201
  26. package/dist/index.js.map +1 -1
  27. package/dist/recorder.cjs.map +1 -1
  28. package/dist/recorder.d.cts +1 -1
  29. package/dist/recorder.d.ts +1 -1
  30. package/dist/recorder.js +1 -1
  31. package/dist/{types-B5ecgHUV.d.cts → types-BDlebXrm.d.ts} +59 -5
  32. package/dist/{types-BtPovimB.d.ts → types-Mtlmlj5C.d.cts} +59 -5
  33. package/package.json +4 -4
  34. 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",
@@ -220,282 +427,256 @@ var KEYS = {
220
427
  newsSeen: (key) => `guidely:news:${key}`
221
428
  };
222
429
 
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 {
430
+ // src/outbox.ts
431
+ var Outbox = class {
233
432
  constructor(opts) {
234
433
  __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", "");
434
+ __publicField(this, "items");
435
+ __publicField(this, "sending", false);
436
+ __publicField(this, "attempt", 0);
437
+ __publicField(this, "timer", null);
438
+ __publicField(this, "destroyed", false);
439
+ __publicField(this, "offRecover");
440
+ __publicField(this, "maxItems");
441
+ __publicField(this, "maxAgeMs");
442
+ this.maxItems = opts.maxItems ?? 50;
443
+ this.maxAgeMs = opts.maxAgeMs ?? 7 * 24 * 60 * 60 * 1e3;
444
+ const stored = readJson(opts.store, opts.storageKey, []);
445
+ const cutoff = Date.now() - this.maxAgeMs;
446
+ this.items = Array.isArray(stored) ? stored.filter((i) => i && typeof i.path === "string" && typeof i.key === "string" && i.createdAt > cutoff).slice(-this.maxItems) : [];
447
+ this.offRecover = opts.health?.onRecover(() => {
448
+ this.attempt = 0;
449
+ this.retryIn(0);
450
+ }) ?? null;
451
+ if (this.items.length) this.retryIn(0);
246
452
  }
247
- isActive() {
248
- return this.active;
453
+ get size() {
454
+ return this.items.length;
249
455
  }
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;
456
+ /** Queues `POST path` and sends it now (or as soon as the API is reachable). */
457
+ add(key, path, body) {
458
+ if (this.destroyed) return;
459
+ this.items = this.items.filter((i) => i.key !== key);
460
+ this.items.push({ key, path, body, createdAt: Date.now() });
461
+ if (this.items.length > this.maxItems) this.items.splice(0, this.items.length - this.maxItems);
462
+ this.persist();
463
+ void this.flush();
273
464
  }
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
- });
465
+ persist() {
466
+ writeJson(this.opts.store, this.opts.storageKey, this.items);
314
467
  }
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));
468
+ retryIn(ms) {
469
+ if (this.destroyed) return;
470
+ if (this.timer) clearTimeout(this.timer);
471
+ this.timer = setTimeout(() => {
472
+ this.timer = null;
473
+ void this.flush();
474
+ }, ms);
322
475
  }
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 });
476
+ /** Sends queued requests in order, one at a time. */
477
+ async flush() {
478
+ if (this.destroyed || this.sending || !this.items.length) return;
479
+ const wait = this.opts.health?.waitMs() ?? 0;
480
+ if (wait > 0) return this.retryIn(wait);
481
+ this.sending = true;
482
+ try {
483
+ while (this.items.length && !this.destroyed) {
484
+ const item = this.items[0];
485
+ let retry = false;
486
+ try {
487
+ const res = await this.opts.api.post(item.path, item.body);
488
+ retry = !res.ok && (res.status === 429 || res.status >= 500);
489
+ } catch (err) {
490
+ retry = true;
491
+ this.opts.onError?.(err);
492
+ }
493
+ if (retry) {
494
+ this.attempt += 1;
495
+ this.retryIn(Math.max(backoffMs(this.attempt - 1, 1e3, 6e4), this.opts.health?.waitMs() ?? 0));
496
+ return;
497
+ }
498
+ this.attempt = 0;
499
+ this.items = this.items.filter((i) => i !== item);
500
+ this.persist();
501
+ }
502
+ } finally {
503
+ this.sending = false;
504
+ }
329
505
  }
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)));
506
+ destroy() {
507
+ this.destroyed = true;
508
+ this.offRecover?.();
509
+ if (this.timer) clearTimeout(this.timer);
510
+ this.timer = null;
335
511
  }
336
- sendSnapshot() {
337
- if (this.snapshotTimer) clearTimeout(this.snapshotTimer);
338
- this.snapshotTimer = null;
339
- if (!this.info || !this.active) return;
512
+ };
513
+
514
+ // src/agent-turns.ts
515
+ var PENDING_MAX = 500;
516
+ function toolCalls(calls) {
517
+ if (!Array.isArray(calls)) return void 0;
518
+ return calls.filter((c) => !!c && typeof c.name === "string" && !!c.name).slice(0, import_runtime4.AGENT_TOOL_CALLS_PER_TURN).map((c) => ({
519
+ name: c.name.slice(0, 100),
520
+ ...c.status === "ok" || c.status === "error" ? { status: c.status } : {},
521
+ ...typeof c.latencyMs === "number" && Number.isFinite(c.latencyMs) && c.latencyMs >= 0 ? { latencyMs: Math.round(c.latencyMs) } : {}
522
+ }));
523
+ }
524
+ function prepareAgentTurn(turn, identity, redact, onError) {
525
+ if (!turn || typeof turn !== "object" || !turn.conversationId || !turn.agentId || !turn.role) return null;
526
+ let out = {
527
+ ...turn,
528
+ type: "agent.conversation",
529
+ turnId: turn.turnId || `t_${(0, import_runtime4.randomId)(20)}`,
530
+ occurredAt: turn.occurredAt || (/* @__PURE__ */ new Date()).toISOString(),
531
+ anonymousId: turn.anonymousId ?? identity.anonymousId,
532
+ userId: turn.userId !== void 0 ? turn.userId : identity.userId,
533
+ accountId: turn.accountId !== void 0 ? turn.accountId : identity.accountId,
534
+ sessionId: turn.sessionId ?? identity.sessionId
535
+ };
536
+ if (redact) {
340
537
  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 });
538
+ const r = redact({ ...out, toolCalls: out.toolCalls ? out.toolCalls.map((c) => ({ ...c })) : void 0, metadata: out.metadata ? { ...out.metadata } : void 0 });
539
+ if (!r) return null;
540
+ out = { ...r, type: "agent.conversation", conversationId: out.conversationId, agentId: out.agentId, turnId: out.turnId, role: out.role };
345
541
  } catch (err) {
346
- this.opts.onError(err);
542
+ onError?.(err);
543
+ return null;
347
544
  }
348
545
  }
349
- post(message) {
350
- if (!this.info) return;
351
- try {
352
- window.opener?.postMessage(message, this.info.dashboardOrigin);
353
- } catch {
546
+ 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);
547
+ if (out.text !== void 0 && typeof out.text !== "string") out.text = String(out.text);
548
+ const calls = toolCalls(out.toolCalls);
549
+ if (calls) out.toolCalls = calls;
550
+ else delete out.toolCalls;
551
+ return out;
552
+ }
553
+ var AgentTurns = class {
554
+ constructor(opts) {
555
+ __publicField(this, "opts", opts);
556
+ __publicField(this, "outbox");
557
+ __publicField(this, "pending", []);
558
+ this.outbox = new Outbox({ api: opts.api, store: opts.store, storageKey: opts.storageKey, health: opts.health, maxItems: 20, maxAgeMs: 24 * 60 * 60 * 1e3 });
559
+ }
560
+ get queued() {
561
+ return this.outbox.size + this.pending.length;
562
+ }
563
+ /** Queues turns to send now, or (`hold`) until consent is granted. */
564
+ add(turns, hold = false) {
565
+ if (!turns.length) return;
566
+ if (hold) {
567
+ this.pending.push(...turns);
568
+ if (this.pending.length > PENDING_MAX) this.pending.splice(0, this.pending.length - PENDING_MAX);
569
+ return;
354
570
  }
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);
571
+ for (let i = 0; i < turns.length; i += import_runtime4.AGENT_TURNS_PER_BATCH) {
572
+ const batch = turns.slice(i, i + import_runtime4.AGENT_TURNS_PER_BATCH);
573
+ this.outbox.add(`agent:${batch[0].turnId}`, "/agent-events", { turns: batch, sdk: this.opts.sdk });
431
574
  }
432
575
  }
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);
576
+ /** Consent changed: granted sends what was held, anything else drops it. */
577
+ consentChanged(granted) {
578
+ const held = this.pending.splice(0);
579
+ if (granted) this.add(held);
580
+ }
581
+ destroy() {
582
+ this.pending = [];
583
+ this.outbox.destroy();
449
584
  }
450
585
  };
451
586
 
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;
587
+ // src/authoring.ts
588
+ var import_guide_runtime3 = require("@guidelytech/guide-runtime");
589
+
590
+ // src/snapshot.ts
591
+ var MAX_SNAPSHOT_BYTES = 6 * 1024 * 1024;
592
+ 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']";
593
+ function rulesText(sheet) {
594
+ if (!sheet) return null;
458
595
  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
- }
596
+ return Array.from(sheet.cssRules, (r) => r.cssText).join("\n");
466
597
  } 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
598
  return null;
479
599
  }
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
600
  }
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) {
601
+ function maskText(root) {
602
+ const walker = root.ownerDocument.createTreeWalker(root, NodeFilter.SHOW_TEXT);
603
+ for (let n = walker.nextNode(); n; n = walker.nextNode()) {
604
+ n.textContent = (n.textContent ?? "").replace(/\S/g, "\u2022");
605
+ }
606
+ }
607
+ function captureSnapshot(doc, maskSelectors, maxBytes = MAX_SNAPSHOT_BYTES) {
608
+ const win = doc.defaultView;
609
+ if (!win || !doc.documentElement) return null;
610
+ const clone = doc.documentElement.cloneNode(true);
611
+ const origStyles = doc.documentElement.querySelectorAll("style");
612
+ clone.querySelectorAll("style").forEach((el, i) => {
613
+ const css = rulesText(origStyles[i]?.sheet);
614
+ if (css !== null && css.length > (el.textContent ?? "").length) el.textContent = css;
615
+ });
616
+ const origFrames = doc.documentElement.querySelectorAll("iframe, canvas, video, embed, object");
617
+ clone.querySelectorAll("iframe, canvas, video, embed, object").forEach((el, i) => {
618
+ const r = origFrames[i]?.getBoundingClientRect();
619
+ const box = doc.createElement("div");
620
+ box.setAttribute("style", `${el.getAttribute("style") ?? ""};width:${r?.width ?? 0}px;height:${r?.height ?? 0}px;background:#e2e8f0;border-radius:4px`);
621
+ if (el.getAttribute("class")) box.setAttribute("class", el.getAttribute("class"));
622
+ el.replaceWith(box);
623
+ });
624
+ clone.querySelectorAll(STRIP).forEach((el) => el.remove());
625
+ for (const sel of maskSelectors) {
626
+ try {
627
+ clone.querySelectorAll(sel).forEach(maskText);
628
+ } catch {
629
+ }
630
+ }
631
+ clone.querySelectorAll("*").forEach((el) => {
632
+ for (const attr2 of Array.from(el.attributes)) {
633
+ const name = attr2.name.toLowerCase();
634
+ if (name.startsWith("on") || (name === "href" || name === "src" || name === "action") && /^\s*javascript:/i.test(attr2.value)) el.removeAttribute(attr2.name);
635
+ }
636
+ if (el instanceof HTMLInputElement) {
637
+ el.removeAttribute("value");
638
+ el.removeAttribute("checked");
639
+ } else if (el instanceof HTMLTextAreaElement) {
640
+ el.textContent = "";
641
+ }
642
+ el.removeAttribute("autofocus");
643
+ });
644
+ const adopted = Array.from(doc.adoptedStyleSheets ?? []).map(rulesText).filter(Boolean).join("\n");
645
+ let head = clone.querySelector("head");
646
+ if (!head) {
647
+ head = doc.createElement("head");
648
+ clone.prepend(head);
649
+ }
650
+ if (adopted) {
651
+ const style = doc.createElement("style");
652
+ style.textContent = adopted;
653
+ head.append(style);
654
+ }
655
+ clone.querySelectorAll("base").forEach((el) => el.remove());
656
+ const base = doc.createElement("base");
657
+ base.setAttribute("href", doc.baseURI || win.location.href);
658
+ base.setAttribute("target", "_blank");
659
+ head.prepend(base);
660
+ const html = `<!DOCTYPE html>
661
+ ${clone.outerHTML}`;
662
+ if (html.length > maxBytes) return null;
663
+ return { html, url: win.location.href, title: doc.title, width: win.innerWidth, height: win.innerHeight, capturedAt: (/* @__PURE__ */ new Date()).toISOString() };
664
+ }
665
+
666
+ // src/tagging.ts
667
+ var import_runtime7 = require("@guidelytech/shared/runtime");
668
+ var import_guide_runtime2 = require("@guidelytech/guide-runtime");
669
+
670
+ // src/autocapture.ts
671
+ var import_guide_runtime = require("@guidelytech/guide-runtime");
672
+ 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]";
673
+ var DEFAULT_MASK_SELECTORS = ["[data-guidely-mask]", ".guidely-mask"];
674
+ var PREFERRED_ATTRS = ["data-testid", "data-test-id", "data-test", "data-cy", "data-qa", "data-guidely-track", "data-tour"];
675
+ 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;
676
+ function validSelectors(list) {
677
+ const out = [];
678
+ const probe = typeof document !== "undefined" ? document.createDocumentFragment() : null;
679
+ for (const raw of list) {
499
680
  const s = (raw || "").trim();
500
681
  if (!s || out.includes(s)) continue;
501
682
  try {
@@ -507,11 +688,11 @@ function validSelectors(list) {
507
688
  return out;
508
689
  }
509
690
  function isIgnoredElement(el) {
510
- return (0, import_guide_runtime2.isGuidelyElement)(el) || !!(0, import_guide_runtime2.closestComposed)(el, "[data-guidely-ignore]");
691
+ return (0, import_guide_runtime.isGuidelyElement)(el) || !!(0, import_guide_runtime.closestComposed)(el, "[data-guidely-ignore]");
511
692
  }
512
693
  function interactiveElement(target) {
513
694
  try {
514
- return (0, import_guide_runtime2.closestComposed)(target, INTERACTIVE_SELECTOR);
695
+ return (0, import_guide_runtime.closestComposed)(target, INTERACTIVE_SELECTOR);
515
696
  } catch {
516
697
  return null;
517
698
  }
@@ -523,12 +704,12 @@ function esc(value) {
523
704
  var attr = (tag, name, value) => value ? `${tag}[${name}="${value.replace(/["\\]/g, "\\$&")}"]` : `${tag}[${name}]`;
524
705
  function elementSelector(el) {
525
706
  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);
707
+ const chain = (0, import_guide_runtime.shadowHostChain)(el);
708
+ return (chain ? chain + import_guide_runtime.SHADOW_SEPARATOR + own : own).slice(0, 300);
528
709
  }
529
710
  function ownSelector(el) {
530
711
  const tag = el.tagName.toLowerCase();
531
- if (el.id && (0, import_guide_runtime2.isStableId)(el.id)) return `#${esc(el.id)}`;
712
+ if (el.id && (0, import_guide_runtime.isStableId)(el.id)) return `#${esc(el.id)}`;
532
713
  for (const name of PREFERRED_ATTRS) if (el.hasAttribute(name)) return attr(tag, name, el.getAttribute(name) || "");
533
714
  for (const a of Array.from(el.attributes)) {
534
715
  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 +721,7 @@ function ownSelector(el) {
540
721
  while (cur && parts.length < 4) {
541
722
  const t = cur.tagName.toLowerCase();
542
723
  if (t === "html" || t === "body") break;
543
- if (cur !== el && cur.id && (0, import_guide_runtime2.isStableId)(cur.id)) {
724
+ if (cur !== el && cur.id && (0, import_guide_runtime.isStableId)(cur.id)) {
544
725
  parts.unshift(`#${esc(cur.id)}`);
545
726
  break;
546
727
  }
@@ -555,7 +736,7 @@ function ownSelector(el) {
555
736
  function matches(el, selector, deep) {
556
737
  if (!selector) return false;
557
738
  try {
558
- return !!(0, import_guide_runtime2.closestComposed)(el, selector) || deep && !!el.querySelector(selector);
739
+ return !!(0, import_guide_runtime.closestComposed)(el, selector) || deep && !!el.querySelector(selector);
559
740
  } catch {
560
741
  return false;
561
742
  }
@@ -573,7 +754,7 @@ function elementText(el, mask, maskAll) {
573
754
  return text ? text.slice(0, 100) : void 0;
574
755
  }
575
756
  function linkHref(el) {
576
- const a = (0, import_guide_runtime2.closestComposed)(el, "a[href]");
757
+ const a = (0, import_guide_runtime.closestComposed)(el, "a[href]");
577
758
  if (!a) return void 0;
578
759
  try {
579
760
  const u = new URL(a.getAttribute("href") || "", location.href);
@@ -619,564 +800,713 @@ var RageClickDetector = class {
619
800
  }
620
801
  };
621
802
 
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 };
803
+ // src/tagging.ts
804
+ var TEXT_ATTRS = /* @__PURE__ */ new Set(["aria-label", "title", "alt"]);
805
+ var SAFE_ATTRS = ["role", "name", "type", "aria-label", "title", "alt", "for"];
806
+ 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;
807
+ function nthOfType(el) {
808
+ const parent = el.parentElement;
809
+ if (!parent) return void 0;
810
+ let n = 0;
811
+ for (const c of Array.from(parent.children)) {
812
+ if (c.tagName === el.tagName) n++;
813
+ if (c === el) return n;
814
+ }
815
+ return void 0;
646
816
  }
647
- function buildContext(input) {
648
- let path = "";
649
- const query = {};
817
+ function nodeOf(el, masked, featureAttribute) {
818
+ const node = { t: el.tagName.toLowerCase().slice(0, import_runtime7.SNAPSHOT_LIMITS.tag) };
819
+ if (el.id && (0, import_guide_runtime2.isStableId)(el.id) && el.id.length <= import_runtime7.SNAPSHOT_LIMITS.id) node.id = el.id;
820
+ 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);
821
+ if (classes.length) node.c = classes.slice(0, import_runtime7.SNAPSHOT_LIMITS.classes);
822
+ const a = {};
823
+ const put = (name, value) => {
824
+ if (value === null || Object.keys(a).length >= import_runtime7.SNAPSHOT_LIMITS.attrs || name in a) return;
825
+ if (masked && TEXT_ATTRS.has(name)) return;
826
+ a[name] = value.slice(0, import_runtime7.SNAPSHOT_LIMITS.attrValue);
827
+ };
828
+ if (featureAttribute && el.hasAttribute(featureAttribute)) put(featureAttribute, el.getAttribute(featureAttribute));
829
+ for (const attr2 of Array.from(el.attributes)) {
830
+ 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);
831
+ }
832
+ for (const name of SAFE_ATTRS) if (el.hasAttribute(name)) put(name, el.getAttribute(name));
833
+ if (Object.keys(a).length) node.a = a;
834
+ const n = nthOfType(el);
835
+ if (n) node.n = n;
836
+ return node;
837
+ }
838
+ function elementSnapshot(el, opts) {
839
+ const masked = opts.maskAll || isMasked(el, opts.mask);
840
+ const snap = nodeOf(el, masked, opts.featureAttribute);
841
+ const p = [];
842
+ for (let cur = el.parentElement; cur && p.length < import_runtime7.SNAPSHOT_LIMITS.ancestors; cur = cur.parentElement) {
843
+ if (cur.tagName === "BODY" || cur.tagName === "HTML") break;
844
+ p.push(nodeOf(cur, masked, opts.featureAttribute));
845
+ }
846
+ if (p.length) snap.p = p;
847
+ const h = [];
848
+ let root = el.getRootNode?.();
849
+ while (root && root.nodeType === 11 && root.host && h.length < import_runtime7.SNAPSHOT_LIMITS.hosts) {
850
+ h.push(nodeOf(root.host, masked, opts.featureAttribute));
851
+ root = root.host.getRootNode?.();
852
+ }
853
+ if (h.length) snap.h = h;
854
+ return snap;
855
+ }
856
+ function isMasked(el, mask) {
857
+ if (!mask) return false;
650
858
  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
- });
859
+ return !!(0, import_guide_runtime2.closestComposed)(el, mask);
656
860
  } catch {
657
- path = input.url;
861
+ return false;
658
862
  }
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
863
  }
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);
864
+ function featureAttributeOf(target, attribute) {
865
+ if (!attribute) return null;
866
+ try {
867
+ const el = (0, import_guide_runtime2.closestComposed)(target, `[${attribute}]`);
868
+ const value = el?.getAttribute(attribute)?.replace(/\s+/g, " ").trim();
869
+ return el && value ? { el, value: value.slice(0, 100) } : null;
870
+ } catch {
871
+ return null;
698
872
  }
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
873
  }
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;
874
+ function readProperty(def, base, opts) {
875
+ let el = base;
876
+ if (def.selector) {
877
+ try {
878
+ el = (0, import_guide_runtime2.querySelectorStrategy)({ kind: "css", value: def.selector }, document)[0] ?? null;
879
+ } catch {
880
+ el = null;
738
881
  }
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
882
  }
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;
883
+ if (!el) return void 0;
884
+ if (def.source === "attribute") {
885
+ const name = (def.attribute ?? "").toLowerCase();
886
+ if (!name || name === "value" || name.startsWith("on")) return void 0;
887
+ if (opts.maskAll || isMasked(el, opts.mask)) return void 0;
888
+ const v = el.getAttribute(name);
889
+ return v ? v.replace(/\s+/g, " ").trim().slice(0, import_runtime7.TAG_PROPERTY_LIMITS.value) || void 0 : void 0;
751
890
  }
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;
891
+ return elementText(el, opts.mask, opts.maskAll);
761
892
  }
762
- function describe(value) {
763
- if (typeof value === "string") return value;
893
+ function clickPropertyValues(config, el, click, opts) {
894
+ if (!config?.properties.length) return void 0;
895
+ const out = {};
896
+ const css = (selector) => {
897
+ try {
898
+ return !!(0, import_guide_runtime2.closestComposed)(el, selector);
899
+ } catch {
900
+ return false;
901
+ }
902
+ };
903
+ const here = { url: location.href, path: location.pathname, title: document.title };
904
+ for (const tag of config.properties) {
905
+ if (tag.kind !== "feature" || !tag.featureRules) continue;
906
+ const rules = tag.featureRules;
907
+ const matches2 = (r) => (0, import_runtime7.featureRuleMatchesClick)(r, click, css) && (!r.page?.pattern || (0, import_runtime7.pageRuleMatches)({ type: "url", value: r.page.pattern }, here));
908
+ if (!rules.some((r) => !r.exclude && matches2(r)) || rules.some((r) => r.exclude && matches2(r))) continue;
909
+ for (const def of tag.properties) {
910
+ const v = readProperty(def, el, opts);
911
+ if (v) out[def.id] = v;
912
+ }
913
+ }
914
+ return Object.keys(out).length ? out : void 0;
915
+ }
916
+ function pagePropertyValues(config, url, opts) {
917
+ if (!config?.properties.length) return void 0;
918
+ const out = {};
919
+ let path = "/";
764
920
  try {
765
- return JSON.stringify(value) ?? String(value);
921
+ path = new URL(url, location.href).pathname;
766
922
  } catch {
767
- return String(value);
768
923
  }
924
+ const ctx = { url, path, title: document.title };
925
+ for (const tag of config.properties) {
926
+ if (tag.kind !== "page" || !tag.pageRules || !(0, import_runtime7.pageRulesMatch)(tag.pageRules, ctx)) continue;
927
+ for (const def of tag.properties) {
928
+ if (!def.selector) continue;
929
+ const v = readProperty(def, null, opts);
930
+ if (v) out[def.id] = v;
931
+ }
932
+ }
933
+ return Object.keys(out).length ? out : void 0;
769
934
  }
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
- };
935
+
936
+ // src/authoring.ts
937
+ async function loadSchemas() {
938
+ const m = await import("@guidelytech/shared");
939
+ return { StepSchema: m.StepSchema, GuideConfigSchema: m.GuideConfigSchema };
795
940
  }
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(/[?#].*$/, ""));
941
+ var AUTHORING_PARAM = "guidely_authoring";
942
+ var HEARTBEAT_MS = 15e3;
943
+ var SNAPSHOT_DEBOUNCE_MS = 800;
944
+ var SNAPSHOT_MAX_WAIT_MS = 4e3;
945
+ var AuthoringBridge = class {
946
+ constructor(opts) {
947
+ __publicField(this, "opts", opts);
948
+ __publicField(this, "token", null);
949
+ __publicField(this, "info", null);
950
+ __publicField(this, "session", null);
951
+ __publicField(this, "heartbeat", null);
952
+ __publicField(this, "preview", null);
953
+ __publicField(this, "previewGuide", null);
954
+ __publicField(this, "active", false);
955
+ __publicField(this, "observer", null);
956
+ __publicField(this, "snapshotTimer", null);
957
+ __publicField(this, "snapshotDue", 0);
958
+ __publicField(this, "lastSnapshot", "");
808
959
  }
809
- }
810
- var ExceptionThrottle = class {
811
- constructor(max = 5) {
812
- __publicField(this, "max", max);
813
- __publicField(this, "seen", /* @__PURE__ */ new Map());
960
+ isActive() {
961
+ return this.active;
814
962
  }
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);
963
+ /** Returns true when a token was found (in the URL or the session marker) and verification started. */
964
+ detect() {
965
+ if (typeof window === "undefined") return false;
966
+ let token = null;
967
+ try {
968
+ const url = new URL(window.location.href);
969
+ token = url.searchParams.get(AUTHORING_PARAM);
970
+ if (token) {
971
+ url.searchParams.delete(AUTHORING_PARAM);
972
+ window.history.replaceState(window.history.state, "", url.toString());
973
+ }
974
+ } catch {
975
+ token = null;
976
+ }
977
+ if (!token) {
978
+ const marker = readJson(this.opts.sessionStore, KEYS.authoring, null);
979
+ token = marker?.token ?? null;
980
+ }
981
+ if (!token) return false;
982
+ this.token = token;
983
+ writeJson(this.opts.sessionStore, KEYS.authoring, { token });
984
+ void this.start();
822
985
  return true;
823
986
  }
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);
987
+ async start() {
988
+ if (!this.token) return;
989
+ try {
990
+ const res = await this.opts.api.get("/authoring/session", { params: { token: this.token } });
991
+ if (!res.ok || !res.data) {
992
+ this.opts.sessionStore.remove(KEYS.authoring);
993
+ this.opts.onError(new Error(`[Guidely] authoring session invalid or expired (${res.status})`));
994
+ return;
995
+ }
996
+ this.info = res.data;
997
+ if (Date.parse(this.info.expiresAt) < Date.now()) {
998
+ this.opts.sessionStore.remove(KEYS.authoring);
999
+ this.opts.onError(new Error("[Guidely] authoring session expired"));
1000
+ return;
1001
+ }
1002
+ this.active = true;
1003
+ this.opts.onActive(true);
1004
+ this.session = (0, import_guide_runtime3.startAuthoring)({
1005
+ dashboardOrigin: this.info.dashboardOrigin,
1006
+ // Tag mode (Pages & Features): picks become feature rules; the page itself is tagged from the dashboard.
1007
+ 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,
1008
+ onPick: (pick) => this.sendPick(pick),
1009
+ onExit: () => this.stop()
1010
+ });
1011
+ this.heartbeat = setInterval(() => {
1012
+ void this.opts.api.post("/authoring/session/heartbeat", {}, { params: { token: this.token ?? void 0 } }).catch(() => {
1013
+ });
1014
+ }, HEARTBEAT_MS);
1015
+ this.post({ type: "guidely:authoring-ready", sessionId: this.info.id, url: window.location.href });
1016
+ this.watchPage();
1017
+ } catch (err) {
1018
+ this.opts.onError(err);
837
1019
  }
838
- set.add(cb);
839
- return () => this.off(event, cb);
840
1020
  }
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)) {
1021
+ masked(el) {
1022
+ return this.opts.maskSelectors.some((sel) => {
848
1023
  try {
849
- cb(...args);
850
- } catch (err) {
851
- this.onError?.(err);
1024
+ return !!(0, import_guide_runtime3.closestComposed)(el, sel);
1025
+ } catch {
1026
+ return false;
852
1027
  }
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;
1028
+ });
869
1029
  }
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;
1030
+ sendPick(raw) {
1031
+ if (!this.info || !this.token) return;
1032
+ const { element, ...rest } = raw;
1033
+ const pick = this.masked(element) ? { ...rest, textSnapshot: "", selectors: rest.selectors.filter((s) => s.kind !== "text") } : rest;
1034
+ pick.url = window.location.href;
1035
+ if (this.info.purpose === "tag") pick.elementSnapshot = elementSnapshot(element, { mask: this.opts.maskSelectors.join(","), maskAll: false, featureAttribute: null });
1036
+ this.post({ type: "guidely:pick", sessionId: this.info.id, pick });
1037
+ void this.opts.api.post("/authoring/session/picks", pick, { params: { token: this.token } }).catch((err) => this.opts.onError(err));
883
1038
  }
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;
1039
+ /** Sends a snapshot now and again whenever the page settles after DOM changes or navigation. */
1040
+ watchPage() {
1041
+ this.scheduleSnapshot();
1042
+ if (typeof MutationObserver === "undefined") return;
1043
+ this.observer = new MutationObserver(() => this.scheduleSnapshot());
1044
+ this.observer.observe(document.documentElement, { subtree: true, childList: true, attributes: true, characterData: true });
927
1045
  }
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();
1046
+ scheduleSnapshot() {
1047
+ const now = Date.now();
1048
+ if (!this.snapshotTimer) this.snapshotDue = now + SNAPSHOT_MAX_WAIT_MS;
1049
+ else clearTimeout(this.snapshotTimer);
1050
+ this.snapshotTimer = setTimeout(() => this.sendSnapshot(), Math.max(0, Math.min(SNAPSHOT_DEBOUNCE_MS, this.snapshotDue - now)));
935
1051
  }
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;
1052
+ sendSnapshot() {
1053
+ if (this.snapshotTimer) clearTimeout(this.snapshotTimer);
1054
+ this.snapshotTimer = null;
1055
+ if (!this.info || !this.active) return;
1056
+ try {
1057
+ const snapshot = captureSnapshot(document, this.opts.maskSelectors);
1058
+ if (!snapshot || snapshot.html === this.lastSnapshot) return;
1059
+ this.lastSnapshot = snapshot.html;
1060
+ this.post({ type: "guidely:snapshot", sessionId: this.info.id, snapshot });
1061
+ } catch (err) {
1062
+ this.opts.onError(err);
943
1063
  }
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
1064
  }
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());
1065
+ post(message) {
1066
+ if (!this.info) return;
1067
+ try {
1068
+ window.opener?.postMessage(message, this.info.dashboardOrigin);
1069
+ } catch {
1070
+ }
952
1071
  }
953
- canSend() {
954
- return this.waitMs() === 0;
1072
+ stopPreview() {
1073
+ this.preview?.destroy();
1074
+ this.preview = null;
1075
+ this.previewGuide?.destroy();
1076
+ this.previewGuide = null;
1077
+ this.session?.setPicking(true);
955
1078
  }
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;
1079
+ /** Handles `message` events; ignores anything not from `dashboardOrigin`. */
1080
+ async handleMessage(e) {
1081
+ if (!this.active || !this.info) return;
1082
+ if (e.origin !== this.info.dashboardOrigin) return;
1083
+ const msg = e.data;
1084
+ if (!msg || typeof msg !== "object" || typeof msg.type !== "string") return;
1085
+ try {
1086
+ switch (msg.type) {
1087
+ case "guidely:preview-step": {
1088
+ this.stopPreview();
1089
+ const { StepSchema, GuideConfigSchema } = await loadSchemas();
1090
+ const step = StepSchema.parse(msg.step);
1091
+ const appearance = msg.appearance ? GuideConfigSchema.shape.appearance.parse(msg.appearance) : void 0;
1092
+ this.session?.setPicking(false);
1093
+ this.session?.setStatus(`Previewing step \u201C${step.title || step.id}\u201D \u2014 close it to continue picking`);
1094
+ this.preview = (0, import_guide_runtime3.previewStep)(step, { ...this.opts.theme(), ...msg.theme || {} }, {
1095
+ appearance,
1096
+ onEnd: () => {
1097
+ this.preview = null;
1098
+ this.session?.setPicking(true);
1099
+ this.session?.setStatus("Guidely authoring \u2014 hover an element and click to select \xB7 Esc to cancel");
1100
+ this.post({ type: "guidely:preview-ended", sessionId: this.info.id, reason: "step" });
1101
+ }
1102
+ });
1103
+ break;
1104
+ }
1105
+ case "guidely:preview-guide": {
1106
+ this.stopPreview();
1107
+ const g = msg.guide;
1108
+ 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) };
1109
+ this.session?.setPicking(false);
1110
+ this.session?.setStatus(`Previewing \u201C${guide.name}\u201D`);
1111
+ this.previewGuide = (0, import_guide_runtime3.runGuide)(guide, {
1112
+ theme: this.opts.theme(),
1113
+ ctx: this.opts.ctx,
1114
+ onNavigate: (url, newTab) => newTab ? window.open(url, "_blank", "noopener") : window.location.assign(url),
1115
+ onEnd: (reason) => {
1116
+ this.previewGuide = null;
1117
+ this.session?.setPicking(true);
1118
+ this.post({ type: "guidely:preview-ended", sessionId: this.info.id, reason });
1119
+ }
1120
+ });
1121
+ this.previewGuide.start();
1122
+ break;
1123
+ }
1124
+ case "guidely:highlight": {
1125
+ const result = (0, import_guide_runtime3.highlightSelectors)(Array.isArray(msg.selectors) ? msg.selectors : [], { durationMs: 3e3 });
1126
+ this.post({ type: "guidely:highlight-result", sessionId: this.info.id, counts: result.counts, resolved: !!result.resolved });
1127
+ break;
1128
+ }
1129
+ case "guidely:stop-preview":
1130
+ this.stopPreview();
1131
+ break;
1132
+ case "guidely:request-snapshot":
1133
+ this.lastSnapshot = "";
1134
+ this.sendSnapshot();
1135
+ break;
1136
+ case "guidely:ping":
1137
+ this.post({ type: "guidely:pong", sessionId: this.info.id, url: window.location.href });
1138
+ break;
1139
+ case "guidely:exit":
1140
+ this.stop();
1141
+ break;
1142
+ default:
1143
+ break;
1144
+ }
1145
+ } catch (err) {
1146
+ this.opts.onError(err);
1147
+ }
962
1148
  }
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();
1149
+ /** Leaves authoring mode and clears the session marker. */
1150
+ stop() {
1151
+ if (!this.active) return;
1152
+ this.active = false;
1153
+ this.stopPreview();
1154
+ this.observer?.disconnect();
1155
+ this.observer = null;
1156
+ if (this.snapshotTimer) clearTimeout(this.snapshotTimer);
1157
+ this.snapshotTimer = null;
1158
+ if (this.heartbeat) clearInterval(this.heartbeat);
1159
+ this.heartbeat = null;
1160
+ this.session?.stop();
1161
+ this.session = null;
1162
+ this.opts.sessionStore.remove(KEYS.authoring);
1163
+ if (this.info) this.post({ type: "guidely:exit", sessionId: this.info.id });
1164
+ this.opts.onActive(false);
967
1165
  }
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);
1166
+ };
1167
+
1168
+ // src/capture-token.ts
1169
+ var import_runtime9 = require("@guidelytech/shared/runtime");
1170
+ function takeCaptureToken(sessionStore) {
1171
+ if (typeof window === "undefined") return null;
1172
+ let token = null;
1173
+ let authoringLink = false;
1174
+ try {
1175
+ const url = new URL(window.location.href);
1176
+ token = url.searchParams.get(import_runtime9.CAPTURE_PARAM);
1177
+ authoringLink = url.searchParams.has(AUTHORING_PARAM);
1178
+ if (token) {
1179
+ url.searchParams.delete(import_runtime9.CAPTURE_PARAM);
1180
+ window.history.replaceState(window.history.state, "", url.toString());
1181
+ }
1182
+ } catch {
1183
+ token = null;
972
1184
  }
973
- notify() {
974
- for (const cb of Array.from(this.listeners)) {
975
- try {
976
- cb();
977
- } catch {
978
- }
1185
+ const saved = readJson(sessionStore, KEYS.capture, null);
1186
+ let marker = null;
1187
+ if (token) marker = saved?.token === token ? saved : { token };
1188
+ else if (saved?.token && !authoringLink) marker = saved;
1189
+ if (!marker) {
1190
+ if (saved) {
1191
+ sessionStore.remove(KEYS.capture);
1192
+ sessionStore.remove(KEYS.capturePending);
979
1193
  }
1194
+ return null;
980
1195
  }
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;
1196
+ if (token && saved?.token !== token) sessionStore.remove(KEYS.capturePending);
1197
+ writeJson(sessionStore, KEYS.capture, marker);
1198
+ sessionStore.remove(KEYS.authoring);
1199
+ return marker;
991
1200
  }
992
1201
 
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();
1202
+ // src/load-capture.ts
1203
+ var loadCapture = async () => (await import("./capture.cjs")).capture;
1204
+
1205
+ // src/context.ts
1206
+ function detectDevice(ua, width) {
1207
+ const u = ua || "";
1208
+ let type = "desktop";
1209
+ if (/iPad|Tablet|PlayBook|Silk/i.test(u) || /Android/i.test(u) && !/Mobile/i.test(u)) type = "tablet";
1210
+ else if (/Mobi|iPhone|iPod|Android.*Mobile|Windows Phone/i.test(u)) type = "mobile";
1211
+ else if (width > 0 && width < 768) type = "mobile";
1212
+ else if (width > 0 && width < 1024) type = "tablet";
1213
+ let browser;
1214
+ if (/Electron\//i.test(u)) browser = "electron";
1215
+ else if (/Edg\//i.test(u)) browser = "edge";
1216
+ else if (/OPR\/|Opera/i.test(u)) browser = "opera";
1217
+ else if (/SamsungBrowser/i.test(u)) browser = "samsung";
1218
+ else if (/Chrome\/|CriOS/i.test(u)) browser = "chrome";
1219
+ else if (/Firefox\/|FxiOS/i.test(u)) browser = "firefox";
1220
+ else if (/Safari\//i.test(u) && /Version\//i.test(u)) browser = "safari";
1221
+ else if (/MSIE|Trident/i.test(u)) browser = "ie";
1222
+ let os;
1223
+ if (/Windows/i.test(u)) os = "windows";
1224
+ else if (/iPhone|iPad|iPod/i.test(u)) os = "ios";
1225
+ else if (/Mac OS X|Macintosh/i.test(u)) os = "macos";
1226
+ else if (/Android/i.test(u)) os = "android";
1227
+ else if (/CrOS/i.test(u)) os = "chromeos";
1228
+ else if (/Linux/i.test(u)) os = "linux";
1229
+ return { type, browser, os };
1230
+ }
1231
+ function buildContext(input) {
1232
+ let path = "";
1233
+ const query = {};
1234
+ try {
1235
+ const u = new URL(input.url, "http://localhost");
1236
+ path = u.pathname;
1237
+ u.searchParams.forEach((v, k) => {
1238
+ query[k] = v;
1239
+ });
1240
+ } catch {
1241
+ path = input.url;
1004
1242
  }
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();
1243
+ const device = input.device ?? (typeof navigator !== "undefined" ? detectDevice(navigator.userAgent, typeof window !== "undefined" ? window.innerWidth : 0) : { type: "desktop" });
1244
+ return {
1245
+ anonymousId: input.anonymousId,
1246
+ userId: input.userId ?? null,
1247
+ userAttributes: input.userAttributes,
1248
+ accountAttributes: input.accountAttributes,
1249
+ url: input.url,
1250
+ path,
1251
+ query,
1252
+ device,
1253
+ firstVisit: input.firstVisit,
1254
+ eventsSeen: new Set(input.eventsSeen),
1255
+ completedGuides: new Set(input.completedGuides),
1256
+ dismissedGuides: new Set(input.dismissedGuides),
1257
+ seenGuides: new Set(input.seenGuides),
1258
+ completedChecklists: new Set(input.completedChecklists),
1259
+ audienceIds: new Set(input.audienceIds),
1260
+ now: input.now ?? /* @__PURE__ */ new Date(),
1261
+ environment: input.environment,
1262
+ locale: input.locale ?? (typeof navigator !== "undefined" ? navigator.language : void 0),
1263
+ platform: input.platform ?? "web",
1264
+ ...input.screen !== void 0 ? { screen: input.screen } : {},
1265
+ ...input.appVersion !== void 0 ? { appVersion: input.appVersion } : {}
1266
+ };
1267
+ }
1268
+
1269
+ // src/heatmap.ts
1270
+ var HEATMAP_MAX_POINTS = 40;
1271
+ var HEATMAP_MAX_BYTES = 6e3;
1272
+ var HEATMAP_MAX_BINS = 60;
1273
+ var r2 = (n) => Math.round(Math.min(1, Math.max(0, n)) * 100) / 100;
1274
+ function heatmapPoint(el, e, rage) {
1275
+ const p = { k: rage ? "r" : "c", x: Math.round(e.pageX ?? 0) || 0, y: Math.round(e.pageY ?? 0) || 0 };
1276
+ const s = elementSelector(el).slice(0, 120);
1277
+ if (s) p.s = s;
1278
+ const rect = el.getBoundingClientRect();
1279
+ if (rect.width > 0 && rect.height > 0) {
1280
+ p.rx = r2((e.clientX - rect.left) / rect.width);
1281
+ p.ry = r2((e.clientY - rect.top) / rect.height);
1010
1282
  }
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}`;
1283
+ return p;
1284
+ }
1285
+ var selectors = /* @__PURE__ */ new WeakMap();
1286
+ var r1 = (n) => Math.round(Math.min(1, Math.max(0, n)) * 10) / 10;
1287
+ function movementBin(el, e) {
1288
+ const p = { k: "m", x: Math.round(e.pageX ?? 0) || 0, y: Math.round(e.pageY ?? 0) || 0 };
1289
+ let s = /^(HTML|BODY)$/.test(el.tagName) ? "" : selectors.get(el);
1290
+ if (s === void 0) selectors.set(el, s = elementSelector(el).slice(0, 120));
1291
+ const rect = s ? el.getBoundingClientRect() : null;
1292
+ if (!rect || !(rect.width > 0 && rect.height > 0)) return [`${p.x >> 4},${p.y >> 4}`, p];
1293
+ p.s = s;
1294
+ p.rx = r1((e.clientX - rect.left) / rect.width);
1295
+ p.ry = r1((e.clientY - rect.top) / rect.height);
1296
+ return [`${s} ${p.rx},${p.ry}`, p];
1297
+ }
1298
+ var HeatmapBuffer = class {
1299
+ constructor() {
1300
+ /** The page the buffered points belong to. */
1301
+ __publicField(this, "url", "");
1302
+ __publicField(this, "points", []);
1303
+ __publicField(this, "bins", /* @__PURE__ */ new Map());
1304
+ __publicField(this, "bytes", 0);
1014
1305
  }
1015
- get fetcher() {
1016
- if (this.fetchImpl) return this.fetchImpl;
1017
- return typeof fetch === "function" ? fetch.bind(globalThis) : null;
1306
+ /** Adds a click (`burst` > 0 marks it and the burst's previous clicks as rage); true when the batch is full. */
1307
+ add(url, point, burst) {
1308
+ this.url = url;
1309
+ for (let i = Math.max(0, this.points.length - burst + 1); i < this.points.length; i++) this.points[i].k = "r";
1310
+ this.points.push(point);
1311
+ this.bytes += JSON.stringify(point).length + 1;
1312
+ return this.full();
1018
1313
  }
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);
1314
+ /** Adds a movement sample to its bin (weight +1); true when the batch is full. */
1315
+ move(url, [key, point]) {
1316
+ this.url = url;
1317
+ const bin = this.bins.get(key);
1318
+ if (bin) bin.w = (bin.w ?? 1) + 1;
1319
+ else {
1320
+ this.bins.set(key, point);
1321
+ this.bytes += JSON.stringify(point).length + 8;
1056
1322
  }
1323
+ return this.full();
1057
1324
  }
1058
- get(path, options) {
1059
- return this.request("GET", path, options);
1325
+ full() {
1326
+ return this.points.length >= HEATMAP_MAX_POINTS || this.bins.size > HEATMAP_MAX_BINS || this.bytes >= HEATMAP_MAX_BYTES;
1060
1327
  }
1061
- post(path, body, options) {
1062
- return this.request("POST", path, { body, ...options });
1328
+ /** Empties the buffer, returning what was in it (clicks first, then movement bins). */
1329
+ take() {
1330
+ const points = [...this.points, ...this.bins.values()];
1331
+ this.points = [];
1332
+ this.bins = /* @__PURE__ */ new Map();
1333
+ this.bytes = 0;
1334
+ return points.length ? { url: this.url, points } : null;
1063
1335
  }
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 });
1336
+ };
1337
+
1338
+ // src/errors.ts
1339
+ var import_runtime11 = require("@guidelytech/shared/runtime");
1340
+ var MAX_FRAMES_BYTES = 6e3;
1341
+ function fitFrames(frames) {
1342
+ const out = frames.slice(0, 30);
1343
+ while (out.length > 1 && JSON.stringify(out).length > MAX_FRAMES_BYTES) out.pop();
1344
+ return out;
1345
+ }
1346
+ function describe(value) {
1347
+ if (typeof value === "string") return value;
1348
+ try {
1349
+ return JSON.stringify(value) ?? String(value);
1350
+ } catch {
1351
+ return String(value);
1067
1352
  }
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);
1353
+ }
1354
+ function exceptionFromError(error, source, handled, fallbackType = "Error") {
1355
+ const isError = error instanceof Error || !!error && typeof error === "object" && typeof error.message === "string" && typeof error.stack === "string";
1356
+ const e = error;
1357
+ return {
1358
+ type: isError && e.name || fallbackType,
1359
+ message: (isError ? e.message : describe(error)).slice(0, 1e3),
1360
+ frames: isError ? fitFrames((0, import_runtime11.parseStack)(e.stack)) : [],
1361
+ source,
1362
+ handled
1363
+ };
1364
+ }
1365
+ function errorEventToException(e) {
1366
+ const target = e.target;
1367
+ if (target && typeof Element !== "undefined" && target instanceof Element) return null;
1368
+ if (e.error != null) return exceptionFromError(e.error, "onerror", false);
1369
+ const message = e.message || "";
1370
+ if (!message || /^script error\.?$/i.test(message.trim())) return null;
1371
+ const m = /^(?:Uncaught )?([A-Z]\w*(?:Error|Exception)):\s*([\s\S]*)$/.exec(message);
1372
+ return {
1373
+ type: m?.[1] ?? "Error",
1374
+ message: (m?.[2] ?? message).slice(0, 1e3),
1375
+ frames: e.filename ? [{ file: e.filename.slice(0, 500), line: e.lineno || void 0, col: e.colno || void 0 }] : [],
1376
+ source: "onerror",
1377
+ handled: false
1378
+ };
1379
+ }
1380
+ function rejectionToException(e) {
1381
+ return exceptionFromError(e.reason, "unhandledrejection", false, "UnhandledRejection");
1382
+ }
1383
+ var GUIDELY_SCRIPT = /\/guidely(-recorder)?(\.min)?\.js$/i;
1384
+ function isGuidelyException(props) {
1385
+ if (props.message.startsWith("[Guidely]")) return true;
1386
+ const file = props.frames[0]?.file;
1387
+ if (!file) return false;
1388
+ try {
1389
+ return GUIDELY_SCRIPT.test(new URL(file).pathname);
1390
+ } catch {
1391
+ return GUIDELY_SCRIPT.test(file.replace(/[?#].*$/, ""));
1392
+ }
1393
+ }
1394
+ var ExceptionThrottle = class {
1395
+ constructor(max = 5) {
1396
+ __publicField(this, "max", max);
1397
+ __publicField(this, "seen", /* @__PURE__ */ new Map());
1398
+ }
1399
+ allow(props) {
1400
+ const top = props.frames[0];
1401
+ const key = `${props.type}|${props.message}|${top ? `${top.file}:${top.line}:${top.col}` : ""}`;
1402
+ const n = this.seen.get(key) ?? 0;
1403
+ if (n >= this.max) return false;
1404
+ if (this.seen.size > 500) this.seen.clear();
1405
+ this.seen.set(key, n + 1);
1406
+ return true;
1407
+ }
1408
+ };
1409
+
1410
+ // src/emitter.ts
1411
+ var Emitter = class {
1412
+ constructor(onError) {
1413
+ __publicField(this, "onError", onError);
1414
+ __publicField(this, "listeners", /* @__PURE__ */ new Map());
1415
+ }
1416
+ on(event, cb) {
1417
+ let set = this.listeners.get(event);
1418
+ if (!set) {
1419
+ set = /* @__PURE__ */ new Set();
1420
+ this.listeners.set(event, set);
1116
1421
  }
1422
+ set.add(cb);
1423
+ return () => this.off(event, cb);
1117
1424
  }
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;
1425
+ off(event, cb) {
1426
+ this.listeners.get(event)?.delete(cb);
1427
+ }
1428
+ emit(event, ...args) {
1429
+ const set = this.listeners.get(event);
1430
+ if (!set) return;
1431
+ for (const cb of Array.from(set)) {
1432
+ try {
1433
+ cb(...args);
1434
+ } catch (err) {
1435
+ this.onError?.(err);
1436
+ }
1126
1437
  }
1127
1438
  }
1439
+ clear() {
1440
+ this.listeners.clear();
1441
+ }
1128
1442
  };
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 {
1443
+
1444
+ // src/frequency.ts
1445
+ var HOUR = 3600 * 1e3;
1446
+ function isFrequencyEligible(freq, state, shownThisSession, now = Date.now()) {
1447
+ const s = state ?? { impressions: 0 };
1448
+ if (freq.maxImpressions != null && s.impressions >= freq.maxImpressions) return false;
1449
+ if (s.completedAt && freq.mode !== "every_time") return false;
1450
+ if (s.dismissedAt) {
1451
+ if (!freq.reshowAfterDismiss) return false;
1452
+ if (freq.cooldownHours > 0 && now - Date.parse(s.dismissedAt) < freq.cooldownHours * HOUR) return false;
1453
+ }
1454
+ switch (freq.mode) {
1455
+ case "once":
1456
+ return s.impressions === 0 || !!s.dismissedAt && freq.reshowAfterDismiss;
1457
+ case "once_per_session":
1458
+ return !shownThisSession;
1459
+ case "interval": {
1460
+ if (!s.lastShownAt) return true;
1461
+ const hours = freq.intervalHours ?? 24;
1462
+ return now - Date.parse(s.lastShownAt) >= hours * HOUR;
1139
1463
  }
1140
- if (i < attempts - 1) await sleep(backoffMs(i, baseMs, 8e3));
1464
+ case "every_time":
1465
+ default:
1466
+ return true;
1141
1467
  }
1142
- return null;
1143
1468
  }
1144
- function postWithRetry(api, path, body, attempts = 3, baseMs = 500) {
1145
- return withRetry(() => api.post(path, body), attempts, baseMs);
1469
+ function isExclusiveGuide(guide) {
1470
+ if (guide.kind === "banner" || guide.kind === "checklist") return false;
1471
+ const steps = guide.config.steps;
1472
+ if (steps.length > 0 && steps.every((s) => s.type === "hotspot" || s.type === "banner")) return false;
1473
+ return true;
1146
1474
  }
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;
1475
+ function areMutuallyExclusive(a, b) {
1476
+ const la = a.config.frequency.mutuallyExclusiveWith;
1477
+ const lb = b.config.frequency.mutuallyExclusiveWith;
1478
+ return la.includes(b.id) || la.includes(b.key) || lb.includes(a.id) || lb.includes(a.key);
1479
+ }
1480
+ function isWithinSchedule(guide, now = Date.now()) {
1481
+ for (const w of [guide.config.schedule, guide.config.survey]) {
1482
+ if (!w) continue;
1483
+ if (w.startAt && now < Date.parse(w.startAt)) return false;
1484
+ if (w.endAt && now > Date.parse(w.endAt)) return false;
1178
1485
  }
1179
- if (buffer.trim()) dispatch(buffer);
1486
+ return true;
1487
+ }
1488
+ var MINUTE = 60 * 1e3;
1489
+ function countsTowardThrottle(guide) {
1490
+ return isExclusiveGuide(guide) && !guide.config.frequency.ignoreThrottling;
1491
+ }
1492
+ function isThrottleEligible(settings, launches, now = Date.now()) {
1493
+ if (!settings || !settings.enabled) return true;
1494
+ const times = launches.map((l) => Date.parse(l)).filter((t) => !Number.isNaN(t));
1495
+ const windowMs = Math.max(1, settings.windowHours) * HOUR;
1496
+ if (times.filter((t) => now - t < windowMs).length >= Math.max(1, settings.maxGuides)) return false;
1497
+ if (settings.minGapMinutes > 0 && times.some((t) => now - t < settings.minGapMinutes * MINUTE)) return false;
1498
+ return true;
1499
+ }
1500
+ function pruneLaunches(launches, settings, now = Date.now()) {
1501
+ const keepMs = Math.max(settings?.windowHours ?? 24, 1) * HOUR + (settings?.minGapMinutes ?? 0) * MINUTE;
1502
+ return launches.filter((l) => {
1503
+ const t = Date.parse(l);
1504
+ return !Number.isNaN(t) && now - t < keepMs;
1505
+ }).slice(-100);
1506
+ }
1507
+ function dismissedAfterMerge(local, server) {
1508
+ if (server.dismissResetAt && server.dismissResetAt !== local?.dismissResetAt) return server.dismissedAt ?? null;
1509
+ return local?.dismissedAt ?? server.dismissedAt ?? null;
1180
1510
  }
1181
1511
 
1182
1512
  // src/messenger-api.ts
@@ -1250,6 +1580,14 @@ function createMessengerTransport(opts) {
1250
1580
  h.onDone(d.messages.at(-1) ?? null, d.conversation);
1251
1581
  } else if (!finished) h.onError("The reply was interrupted");
1252
1582
  }
1583
+ async function feedback(path, body, fallback) {
1584
+ const res = await api.post(`/feedback${path}`, { ...body, anonymousId: opts.anonymousId(), userId: opts.userId() });
1585
+ if (!res.ok || !res.data) {
1586
+ if (res.status === 429) throw new HttpError(429, "You're doing that a lot. Please try again in a minute.");
1587
+ throw new HttpError(res.status, serverMessage(res.data) ?? fallback);
1588
+ }
1589
+ return res.data;
1590
+ }
1253
1591
  return {
1254
1592
  async listConversations() {
1255
1593
  const tokens = storedConversations(store, projectId).map((c) => c.token);
@@ -1286,7 +1624,8 @@ function createMessengerTransport(opts) {
1286
1624
  return res.data;
1287
1625
  },
1288
1626
  async getArticle(id) {
1289
- const res = await api.get(`/articles/${encodeURIComponent(id)}`);
1627
+ const locale = opts.locale?.();
1628
+ const res = await api.get(`/articles/${encodeURIComponent(id)}`, locale ? { params: { locale } } : void 0);
1290
1629
  return res.ok && res.data ? res.data.article : null;
1291
1630
  },
1292
1631
  async fileComplaint(input) {
@@ -1316,13 +1655,25 @@ function createMessengerTransport(opts) {
1316
1655
  async getHowTo(id) {
1317
1656
  const res = await api.get(`/how-tos/${encodeURIComponent(id)}`);
1318
1657
  return res.ok && res.data ? res.data.howTo : null;
1319
- }
1658
+ },
1659
+ // Ideas (feature requests). The visitor's ids go in every body; the server decides what they may do.
1660
+ listIdeas: (query) => feedback("/requests/query", query, "Ideas couldn't be loaded."),
1661
+ getIdea: (id) => feedback(`/requests/${encodeURIComponent(id)}/view`, {}, "This idea couldn't be loaded."),
1662
+ submitIdea: (input) => feedback("/requests", { ...input, pageUrl: opts.pageUrl() }, "Your idea couldn't be sent. Please try again."),
1663
+ voteIdea: (id, vote) => feedback(`/requests/${encodeURIComponent(id)}/vote`, vote, "Your vote couldn't be saved."),
1664
+ unvoteIdea: (id) => feedback(`/requests/${encodeURIComponent(id)}/unvote`, {}, "Your vote couldn't be removed."),
1665
+ followIdea: (id, follow) => feedback(`/requests/${encodeURIComponent(id)}/follow`, { follow }, "That didn't work. Please try again."),
1666
+ commentIdea: (id, body) => feedback(`/requests/${encodeURIComponent(id)}/comments`, { body }, "Your comment couldn't be posted."),
1667
+ similarIdeas: async (title) => (await feedback("/similar", { title }, "Similar ideas couldn't be loaded.")).requests ?? [],
1668
+ ideaUpdates: () => feedback("/updates", {}, "Updates couldn't be loaded."),
1669
+ markIdeaUpdatesRead: (ids) => feedback("/updates/read", ids ? { ids } : {}, "Updates couldn't be marked read."),
1670
+ ideaPortalLink: async () => (await feedback("/portal-link", {}, "The feedback portal couldn't be opened.")).url
1320
1671
  };
1321
1672
  }
1322
1673
 
1323
1674
  // src/messenger-visitor.ts
1324
- var import_runtime11 = require("@guidelytech/shared/runtime");
1325
- var import_guide_runtime3 = require("@guidelytech/guide-runtime");
1675
+ var import_runtime15 = require("@guidelytech/shared/runtime");
1676
+ var import_guide_runtime4 = require("@guidelytech/guide-runtime");
1326
1677
  var PAGE_GUIDE_KINDS = /* @__PURE__ */ new Set(["flow", "tooltip", "modal"]);
1327
1678
  var CATCH_ALL = /* @__PURE__ */ new Set(["", "*", "**", "/*", "/**", "/"]);
1328
1679
  var RESUME_WITHIN_MS = 14 * 864e5;
@@ -1339,7 +1690,7 @@ function targetsPage(guide, url) {
1339
1690
  const c = guide.config;
1340
1691
  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
1692
  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));
1693
+ return specific.length > 0 && checks.every((p) => (0, import_guide_runtime4.urlMatches)(p, url));
1343
1694
  }
1344
1695
  function plainText(html, max = 280) {
1345
1696
  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 +1739,7 @@ function buildMessengerVisitor(input) {
1388
1739
  unread: !seen.has(g.id) && !(state.guides[g.id]?.impressions ?? 0)
1389
1740
  };
1390
1741
  });
1391
- return { firstName: (0, import_runtime11.firstNameFromTraits)(input.traits), path: pathOf(input.url), pageGuides, resumeGuide, checklist, news };
1742
+ return { firstName: (0, import_runtime15.firstNameFromTraits)(input.traits), path: pathOf(input.url), pageGuides, resumeGuide, checklist, news };
1392
1743
  }
1393
1744
 
1394
1745
  // src/navigation.ts
@@ -1446,6 +1797,48 @@ function watchNavigation(listener, debounceMs = 50) {
1446
1797
  };
1447
1798
  }
1448
1799
 
1800
+ // src/page-url.ts
1801
+ var WEB_SCHEMES = /* @__PURE__ */ new Set(["http", "https", "about", "data", "blob", "javascript"]);
1802
+ function appScheme(href) {
1803
+ let u;
1804
+ try {
1805
+ u = new URL(href);
1806
+ } catch {
1807
+ return null;
1808
+ }
1809
+ const scheme = u.protocol.replace(/:$/, "").toLowerCase();
1810
+ return WEB_SCHEMES.has(scheme) ? null : { u, scheme };
1811
+ }
1812
+ function normalizeAppUrl(href) {
1813
+ const app = appScheme(href);
1814
+ if (!app) return href;
1815
+ const { u, scheme } = app;
1816
+ const host = scheme === "file" ? "app" : u.host || "app";
1817
+ const hash = /^#!?(\/.*)$/.exec(u.hash);
1818
+ if (hash) {
1819
+ const route = hash[1];
1820
+ return `${scheme}://${host}${route.startsWith("/") ? route : `/${route}`}`;
1821
+ }
1822
+ let path = safeDecode(u.pathname);
1823
+ if (scheme === "file" || /\.html?$/i.test(path)) {
1824
+ const file = path.split("/").filter(Boolean).pop() ?? "";
1825
+ const name = file.replace(/\.html?$/i, "");
1826
+ path = !name || name.toLowerCase() === "index" ? "/" : `/${name}`;
1827
+ }
1828
+ if (!path.startsWith("/")) path = `/${path}`;
1829
+ return `${scheme}://${host}${path}${u.search}`;
1830
+ }
1831
+ function safeDecode(s) {
1832
+ try {
1833
+ return decodeURI(s);
1834
+ } catch {
1835
+ return s;
1836
+ }
1837
+ }
1838
+ function isAppUrl(href) {
1839
+ return appScheme(href) !== null;
1840
+ }
1841
+
1449
1842
  // src/page-tracking.ts
1450
1843
  function scrollDepth() {
1451
1844
  const de = document.documentElement;
@@ -1491,89 +1884,105 @@ var PageTracker = class {
1491
1884
  }
1492
1885
  };
1493
1886
 
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);
1887
+ // src/prototype-overlay.ts
1888
+ var PROTOTYPE_CLOSE_MESSAGE = "guidely:prototype:close";
1889
+ var TOKEN = /^[A-Za-z0-9_-]{16,64}$/;
1890
+ var OVERLAY_ATTR = "data-guidely-prototype-overlay";
1891
+ function matchPrototypeTestUrl(urlOrToken, apiUrl) {
1892
+ const value = (urlOrToken || "").trim();
1893
+ let api;
1894
+ try {
1895
+ api = new URL(apiUrl);
1896
+ } catch {
1897
+ return null;
1531
1898
  }
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);
1899
+ if (TOKEN.test(value)) return new URL(`/validate/${value}`, api.origin);
1900
+ let url;
1901
+ try {
1902
+ url = new URL(value, typeof location !== "undefined" ? location.href : api.origin);
1903
+ } catch {
1904
+ return null;
1539
1905
  }
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;
1906
+ if (url.origin !== api.origin) return null;
1907
+ const m = /^\/validate\/([^/]+)\/?$/.exec(url.pathname);
1908
+ if (!m || !TOKEN.test(m[1])) return null;
1909
+ return new URL(`/validate/${m[1]}`, api.origin);
1910
+ }
1911
+ var current = null;
1912
+ function openPrototypeOverlay(testUrl, opts) {
1913
+ current?.close();
1914
+ const doc = document;
1915
+ const previous = doc.activeElement;
1916
+ const src = new URL(testUrl.href);
1917
+ const hash = new URLSearchParams();
1918
+ hash.set("key", opts.key);
1919
+ if (opts.anonymousId) hash.set("anon", opts.anonymousId);
1920
+ if (opts.userId) hash.set("uid", opts.userId);
1921
+ src.hash = hash.toString();
1922
+ const root = doc.createElement("div");
1923
+ root.setAttribute(OVERLAY_ATTR, "");
1924
+ root.setAttribute("role", "dialog");
1925
+ root.setAttribute("aria-modal", "true");
1926
+ root.setAttribute("aria-label", "Prototype test");
1927
+ 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;`;
1928
+ const frame = doc.createElement("iframe");
1929
+ frame.src = src.toString();
1930
+ frame.title = "Prototype test";
1931
+ frame.setAttribute("allow", "fullscreen");
1932
+ frame.setAttribute("referrerpolicy", "no-referrer");
1933
+ frame.style.cssText = "border:0;width:100%;height:100%;background:#fff;display:block;";
1934
+ const close = doc.createElement("button");
1935
+ close.type = "button";
1936
+ close.setAttribute("aria-label", "Close prototype test");
1937
+ close.textContent = "\xD7";
1938
+ 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;";
1939
+ root.append(frame, close);
1940
+ const overflow = doc.documentElement.style.overflow;
1941
+ doc.documentElement.style.overflow = "hidden";
1942
+ let closed = false;
1943
+ const shut = () => {
1944
+ if (closed) return;
1945
+ closed = true;
1946
+ window.removeEventListener("message", onMessage);
1947
+ doc.removeEventListener("keydown", onKey, true);
1948
+ doc.removeEventListener("focusin", onFocus, true);
1949
+ root.remove();
1950
+ doc.documentElement.style.overflow = overflow;
1951
+ if (current === overlay) current = null;
1546
1952
  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;
1953
+ previous?.focus?.();
1954
+ } catch {
1568
1955
  }
1569
- }
1570
- destroy() {
1571
- this.destroyed = true;
1572
- this.offRecover?.();
1573
- if (this.timer) clearTimeout(this.timer);
1574
- this.timer = null;
1575
- }
1576
- };
1956
+ opts.onClose?.();
1957
+ };
1958
+ const onMessage = (e) => {
1959
+ if (e.origin !== testUrl.origin || e.source !== frame.contentWindow) return;
1960
+ const data = e.data;
1961
+ if (data && typeof data === "object" && data.type === PROTOTYPE_CLOSE_MESSAGE) shut();
1962
+ };
1963
+ const onKey = (e) => {
1964
+ if (e.key === "Escape") {
1965
+ e.preventDefault();
1966
+ e.stopPropagation();
1967
+ shut();
1968
+ } else if (e.key === "Tab") {
1969
+ e.preventDefault();
1970
+ (doc.activeElement === close ? frame : close).focus();
1971
+ }
1972
+ };
1973
+ const onFocus = (e) => {
1974
+ if (!root.contains(e.target)) close.focus();
1975
+ };
1976
+ close.addEventListener("click", shut);
1977
+ window.addEventListener("message", onMessage);
1978
+ doc.addEventListener("keydown", onKey, true);
1979
+ doc.addEventListener("focusin", onFocus, true);
1980
+ doc.body.appendChild(root);
1981
+ frame.focus();
1982
+ const overlay = { element: root, close: shut };
1983
+ current = overlay;
1984
+ return overlay;
1985
+ }
1577
1986
 
1578
1987
  // src/queue.ts
1579
1988
  var EventQueue = class {
@@ -1707,7 +2116,7 @@ var loadRecorder = async () => (await import("./recorder.cjs")).recorder;
1707
2116
 
1708
2117
  // src/client.ts
1709
2118
  var SDK_NAME = "@guidelytech/sdk-core";
1710
- var SDK_VERSION = "0.10.0";
2119
+ var SDK_VERSION = "0.11.0";
1711
2120
  var DEFAULT_API_URL = "https://api.guidely.app";
1712
2121
  var PAGE_SCOPED = /* @__PURE__ */ new Set(["page_load", "url_match", "element_click", "element_visible", "delay"]);
1713
2122
  var DAY_MS = 24 * 60 * 60 * 1e3;
@@ -1733,7 +2142,7 @@ function emptyState() {
1733
2142
  return { guides: {}, checklists: {}, surveysSubmitted: [], audienceIds: [] };
1734
2143
  }
1735
2144
  function newSession(now = Date.now()) {
1736
- return { id: `s_${(0, import_runtime15.randomId)(16)}`, startedAt: new Date(now).toISOString(), shownGuides: [], events: [], lastActivityAt: now };
2145
+ return { id: `s_${(0, import_runtime18.randomId)(16)}`, startedAt: new Date(now).toISOString(), shownGuides: [], events: [], lastActivityAt: now };
1737
2146
  }
1738
2147
  var GuidelyClient = class _GuidelyClient {
1739
2148
  constructor(options) {
@@ -1745,6 +2154,8 @@ var GuidelyClient = class _GuidelyClient {
1745
2154
  __publicField(this, "queue");
1746
2155
  /** Survey answers and guide/checklist progress, kept until the server accepts them. */
1747
2156
  __publicField(this, "outbox");
2157
+ /** Agent Analytics turns (agentConversation), in their own outbox. */
2158
+ __publicField(this, "agentTurns");
1748
2159
  __publicField(this, "config", null);
1749
2160
  __publicField(this, "etag", null);
1750
2161
  __publicField(this, "anonymousId");
@@ -1794,6 +2205,10 @@ var GuidelyClient = class _GuidelyClient {
1794
2205
  __publicField(this, "flags");
1795
2206
  /** Experiments already reported this session (`experiment_viewed` is sent once per session). */
1796
2207
  __publicField(this, "exposures", /* @__PURE__ */ new Set());
2208
+ /** Language set with `setLocale()`. */
2209
+ __publicField(this, "locale", null);
2210
+ /** Language each shown guide was rendered in; added to its events as `locale`. */
2211
+ __publicField(this, "guideLocales", /* @__PURE__ */ new Map());
1797
2212
  __publicField(this, "customListeners", /* @__PURE__ */ new Map());
1798
2213
  __publicField(this, "eventBus", {
1799
2214
  on: (name, cb) => {
@@ -1828,7 +2243,7 @@ var GuidelyClient = class _GuidelyClient {
1828
2243
  this.sessionStore = this.options.storage === "memory" ? this.store : createStorage("session");
1829
2244
  this.anonymousId = this.store.get(KEYS.anon) || "";
1830
2245
  if (!this.anonymousId) {
1831
- this.anonymousId = `anon_${(0, import_runtime15.randomId)(20)}`;
2246
+ this.anonymousId = `anon_${(0, import_runtime18.randomId)(20)}`;
1832
2247
  this.store.set(KEYS.anon, this.anonymousId);
1833
2248
  }
1834
2249
  this.firstVisit = !this.store.get(KEYS.seen);
@@ -1838,7 +2253,7 @@ var GuidelyClient = class _GuidelyClient {
1838
2253
  const session = readJson(this.sessionStore, KEYS.session, null);
1839
2254
  this.session = session && session.id ? session : newSession();
1840
2255
  this.persistSession();
1841
- this.lastUrl = location.href;
2256
+ this.lastUrl = this.pageHref();
1842
2257
  this.lastAudiences = new Set(this.state.audienceIds);
1843
2258
  this.queue = new EventQueue({
1844
2259
  store: this.store,
@@ -1853,6 +2268,7 @@ var GuidelyClient = class _GuidelyClient {
1853
2268
  onError: (err) => this.reportError(err)
1854
2269
  });
1855
2270
  this.outbox = new Outbox({ api: this.api, store: this.store, storageKey: KEYS.outbox(this.options.projectId), health: this.api.health });
2271
+ 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
2272
  if (options.user) this.user = { ...options.user };
1857
2273
  this.flags = this.createFlags(options.flags);
1858
2274
  }
@@ -1929,7 +2345,7 @@ var GuidelyClient = class _GuidelyClient {
1929
2345
  this.resumeActiveGuide();
1930
2346
  this.evaluate("ready");
1931
2347
  if (!this.options.disableAuthoring && !capturing) this.maybeStartAuthoring();
1932
- this.pageTracker.start(location.href);
2348
+ this.pageTracker.start(this.pageHref());
1933
2349
  this.touchSession();
1934
2350
  this.syncCapture();
1935
2351
  } catch (err) {
@@ -1962,7 +2378,7 @@ var GuidelyClient = class _GuidelyClient {
1962
2378
  wireListeners() {
1963
2379
  const doc = document;
1964
2380
  const win = window;
1965
- this.cleanups.push(watchNavigation((url) => this.handleNavigation(url)));
2381
+ this.cleanups.push(watchNavigation((url) => this.handleNavigation(this.pageHref(url))));
1966
2382
  const onClick = (e) => this.handleDocumentClick(e);
1967
2383
  doc.addEventListener("click", onClick, true);
1968
2384
  this.cleanups.push(() => doc.removeEventListener("click", onClick, true));
@@ -1983,7 +2399,7 @@ var GuidelyClient = class _GuidelyClient {
1983
2399
  const onPageShow = (e) => {
1984
2400
  if (!e.persisted || !this.pageLeft) return;
1985
2401
  this.pageLeft = false;
1986
- this.pageTracker.start(location.href);
2402
+ this.pageTracker.start(this.pageHref());
1987
2403
  };
1988
2404
  let lastScroll = 0;
1989
2405
  const onScroll = () => {
@@ -2032,7 +2448,7 @@ var GuidelyClient = class _GuidelyClient {
2032
2448
  this.cleanups.push(() => clearInterval(refresh));
2033
2449
  const poll = setInterval(() => {
2034
2450
  if (!this.isReady || !this.config || this.editing() || doc.visibilityState === "hidden") return;
2035
- const url = location.href;
2451
+ const url = this.pageHref();
2036
2452
  const ctx = this.getContext();
2037
2453
  for (const guide of this.config.guides) {
2038
2454
  if (guide.config.trigger.type === "element_visible" && guide.config.trigger.value) this.armVisibility(guide, ctx, url);
@@ -2101,10 +2517,11 @@ var GuidelyClient = class _GuidelyClient {
2101
2517
  this.queue.flushSync();
2102
2518
  this.queue.destroy();
2103
2519
  this.outbox.destroy();
2520
+ this.agentTurns.destroy();
2104
2521
  this.flags.destroy();
2105
2522
  this.emitter.clear();
2106
- (0, import_guide_runtime4.destroyHost)();
2107
- if (current === this) current = null;
2523
+ (0, import_guide_runtime5.destroyHost)();
2524
+ if (current2 === this) current2 = null;
2108
2525
  }
2109
2526
  // ---------------------------------------------------------------------------
2110
2527
  // Identity
@@ -2116,13 +2533,13 @@ var GuidelyClient = class _GuidelyClient {
2116
2533
  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
2534
  writeJson(this.store, KEYS.user(this.options.projectId), this.user);
2118
2535
  this.syncFlagAttributes();
2119
- this.enqueue("identify", { properties: { traits: (0, import_runtime15.sanitizeProperties)(this.user.traits) } });
2536
+ this.enqueue("identify", { properties: { traits: (0, import_runtime18.sanitizeProperties)(this.user.traits) } });
2120
2537
  await this.syncIdentify();
2121
2538
  this.emitter.emit("identify", { userId: this.user.id, anonymousId: this.anonymousId });
2122
2539
  if (this.isReady) this.evaluate("identify");
2123
2540
  }
2124
2541
  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 };
2542
+ 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
2543
  const res = await postWithRetry(this.api, "/identify", body, 2);
2127
2544
  if (res?.ok && res.data) this.mergeServerState(res.data);
2128
2545
  }
@@ -2153,7 +2570,10 @@ var GuidelyClient = class _GuidelyClient {
2153
2570
  checklists,
2154
2571
  surveysSubmitted: Array.from(/* @__PURE__ */ new Set([...this.state.surveysSubmitted, ...data.surveysSubmitted || []])),
2155
2572
  audienceIds: data.audienceIds || [],
2156
- firstSeenAt: this.state.firstSeenAt ?? data.firstSeenAt
2573
+ firstSeenAt: this.state.firstSeenAt ?? data.firstSeenAt,
2574
+ // Device-local only (the server doesn't hold them).
2575
+ ...this.state.throttle ? { throttle: this.state.throttle } : {},
2576
+ ...this.state.announcementsSeenAt ? { announcementsSeenAt: this.state.announcementsSeenAt } : {}
2157
2577
  };
2158
2578
  if (data.firstSeenAt && Date.now() - Date.parse(data.firstSeenAt) > 6e4) this.firstVisit = false;
2159
2579
  this.persistState();
@@ -2189,7 +2609,7 @@ var GuidelyClient = class _GuidelyClient {
2189
2609
  this.stopGuide();
2190
2610
  this.user = null;
2191
2611
  this.store.remove(KEYS.user(this.options.projectId));
2192
- this.anonymousId = `anon_${(0, import_runtime15.randomId)(20)}`;
2612
+ this.anonymousId = `anon_${(0, import_runtime18.randomId)(20)}`;
2193
2613
  this.store.set(KEYS.anon, this.anonymousId);
2194
2614
  this.state = emptyState();
2195
2615
  this.persistState();
@@ -2221,6 +2641,7 @@ var GuidelyClient = class _GuidelyClient {
2221
2641
  if (consent === "denied") this.queue.clear();
2222
2642
  else if (consent === "granted") void this.queue.flush();
2223
2643
  if (consent !== "granted") this.replay?.stop({ discard: true });
2644
+ if (consent !== "pending") this.agentTurns.consentChanged(consent === "granted");
2224
2645
  this.syncCapture();
2225
2646
  }
2226
2647
  analyticsEnabled() {
@@ -2233,17 +2654,19 @@ var GuidelyClient = class _GuidelyClient {
2233
2654
  if (this.consent === "denied" || !this.options.analytics) return null;
2234
2655
  if (name !== "page_leave" && name !== "heatmap") this.touchSession();
2235
2656
  const event = {
2236
- eventId: (0, import_runtime15.randomId)(24),
2657
+ eventId: (0, import_runtime18.randomId)(24),
2237
2658
  name,
2238
2659
  occurredAt: nowIso(),
2239
2660
  anonymousId: this.anonymousId,
2240
2661
  userId: this.user?.id || null,
2241
2662
  accountId: this.user?.account?.id || null,
2242
2663
  sessionId: this.session.id,
2243
- url: this.collectUrls() ? location.href : void 0,
2664
+ url: this.collectUrls() ? this.pageHref() : void 0,
2244
2665
  ...extra
2245
2666
  };
2246
- if (extra.properties) event.properties = (0, import_runtime15.sanitizeProperties)(extra.properties);
2667
+ if (extra.properties) event.properties = (0, import_runtime18.sanitizeProperties)(extra.properties);
2668
+ const locale = extra.guideId ? this.guideLocales.get(extra.guideId) : void 0;
2669
+ if (locale) event.properties = { ...event.properties ?? {}, locale };
2247
2670
  this.queue.enqueue(event);
2248
2671
  this.replay?.addCustomEvent("guidely", { name, customName: event.customName, guideId: event.guideId });
2249
2672
  this.emitter.emit("track", event);
@@ -2267,9 +2690,30 @@ var GuidelyClient = class _GuidelyClient {
2267
2690
  this.completeChecklistTasksBy("event", name);
2268
2691
  if (this.isReady) this.evaluate("event", { eventName: name });
2269
2692
  }
2693
+ /**
2694
+ * Agent Analytics: records one or more turns of a conversation with your own AI agent or chatbot
2695
+ * (`{ conversationId, agentId, role, text, toolCalls?, latencyMs?, outcome?, … }`). The visitor's
2696
+ * anonymous id, user id, account id and session id are filled in, `agents.redact` from the init
2697
+ * options runs on each turn, and turns are retried until Guidely accepts them. Returns false when
2698
+ * nothing was queued (analytics off or consent denied, or every turn was invalid or dropped).
2699
+ */
2700
+ agentConversation(turns) {
2701
+ if (this.destroyed || !this.options.analytics || this.consent === "denied" || this.config?.settings?.analyticsEnabled === false) return false;
2702
+ const identity = { anonymousId: this.anonymousId, userId: this.user?.id || null, accountId: this.user?.account?.id || null, sessionId: this.session.id };
2703
+ const prepared = (Array.isArray(turns) ? turns : [turns]).map((t) => prepareAgentTurn(t, identity, this.options.agents?.redact, (err) => this.reportError(err))).filter((t) => !!t);
2704
+ if (!prepared.length) return false;
2705
+ this.touchSession();
2706
+ this.agentTurns.add(prepared, this.consent === "pending");
2707
+ return true;
2708
+ }
2709
+ /** Alias of {@link agentConversation}. */
2710
+ trackAgentTurn(turns) {
2711
+ return this.agentConversation(turns);
2712
+ }
2270
2713
  /** Records a page view and re-evaluates triggers. Called automatically on SPA navigation. */
2271
- page(url = location.href) {
2714
+ page(href) {
2272
2715
  if (this.destroyed) return;
2716
+ const url = this.pageHref(href);
2273
2717
  this.leavePage(url);
2274
2718
  this.trackPageView(url);
2275
2719
  this.handleNavigation(url, true);
@@ -2289,13 +2733,13 @@ var GuidelyClient = class _GuidelyClient {
2289
2733
  const st = this.active.controller.getState();
2290
2734
  if (st.status === "running" && !this.active.manual) {
2291
2735
  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();
2736
+ if (g.config.urlPatterns.length && !g.config.urlPatterns.some((p) => (0, import_guide_runtime5.urlMatches)(p, url))) this.active.controller.destroy();
2293
2737
  }
2294
2738
  }
2295
2739
  this.widgets.forEach((w, id) => {
2296
2740
  if (!w.controller || w.manual) return;
2297
2741
  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();
2742
+ if (g?.config.urlPatterns.length && !g.config.urlPatterns.some((p) => (0, import_guide_runtime5.urlMatches)(p, url))) w.controller.destroy();
2299
2743
  });
2300
2744
  this.firedDelays.clear();
2301
2745
  if (this.isReady) this.evaluate("navigation");
@@ -2303,7 +2747,7 @@ var GuidelyClient = class _GuidelyClient {
2303
2747
  if (changed) void this.queue.flush();
2304
2748
  }
2305
2749
  handleDocumentClick(e) {
2306
- const target = (0, import_guide_runtime4.composedTarget)(e);
2750
+ const target = (0, import_guide_runtime5.composedTarget)(e);
2307
2751
  if (!target || typeof target.closest !== "function") return;
2308
2752
  this.autocaptureClick(e, target);
2309
2753
  this.completeChecklistTasksBy("click", target);
@@ -2361,6 +2805,8 @@ var GuidelyClient = class _GuidelyClient {
2361
2805
  const ref = !this.session.pageViews ? externalReferrer() : void 0;
2362
2806
  if (ref) props.referrer = ref;
2363
2807
  }
2808
+ const ep = this.captureAllowed() ? pagePropertyValues(this.config?.tagging, url, { mask: this.maskSelector(), maskAll: this.maskAllText() }) : void 0;
2809
+ if (ep) props.ep = ep;
2364
2810
  this.session.pageViews = (this.session.pageViews ?? 0) + 1;
2365
2811
  this.persistSession();
2366
2812
  this.enqueue("page_view", { url: collect ? url : void 0, properties: props });
@@ -2386,21 +2832,28 @@ var GuidelyClient = class _GuidelyClient {
2386
2832
  const heat = this.autocaptureOn("heatmaps");
2387
2833
  if (!clicks && !rage && !heat || isIgnoredElement(target)) return;
2388
2834
  this.touchSession();
2389
- const el = interactiveElement(target);
2835
+ const featureAttribute = this.config?.tagging?.featureAttribute ?? null;
2836
+ const feature = featureAttributeOf(target, featureAttribute);
2837
+ const el = interactiveElement(target) ?? feature?.el ?? null;
2390
2838
  const opts = { mask: this.maskSelector(), maskAll: this.maskAllText(), collectUrls: this.collectUrls() };
2391
- if (clicks && el) this.enqueue("click", { properties: { ...describeClick(el, e, opts) } });
2839
+ const clickProps = (on) => {
2840
+ const p = describeClick(on, e, opts);
2841
+ const ep = clickPropertyValues(this.config?.tagging, on, p, opts);
2842
+ return { ...p, el: elementSnapshot(on, { ...opts, featureAttribute }), ...feature ? { fa: feature.value } : {}, ...ep ? { ep } : {} };
2843
+ };
2844
+ if (clicks && el) this.enqueue("click", { properties: clickProps(el) });
2392
2845
  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 } });
2846
+ if (rage && count) this.enqueue("rage_click", { properties: { ...clickProps(el ?? target), count } });
2394
2847
  if (!heat) return;
2395
- const url = location.href;
2848
+ const url = this.pageHref();
2396
2849
  if (this.heatmap.url !== url) this.flushHeatmap();
2397
2850
  if (this.heatmap.add(url, heatmapPoint(el ?? target, e, this.rage.burst > 0), this.rage.burst)) this.flushHeatmap();
2398
2851
  }
2399
2852
  /** Bins one pointer sample into the heatmap buffer. Not session activity. */
2400
2853
  sampleMove(e) {
2401
- const target = (0, import_guide_runtime4.composedTarget)(e);
2854
+ const target = (0, import_guide_runtime5.composedTarget)(e);
2402
2855
  if (document.visibilityState === "hidden" || !target?.closest || !this.autocaptureOn("mouseMovement") || isIgnoredElement(target)) return;
2403
- const url = location.href;
2856
+ const url = this.pageHref();
2404
2857
  if (this.heatmap.url !== url) this.flushHeatmap();
2405
2858
  if (this.heatmap.move(url, movementBin(interactiveElement(target) ?? target, e))) this.flushHeatmap();
2406
2859
  }
@@ -2476,7 +2929,7 @@ var GuidelyClient = class _GuidelyClient {
2476
2929
  if (!this.isReady || this.destroyed) return;
2477
2930
  if (this.config && !this.initialPageViewSent && this.config.settings?.autocapture) {
2478
2931
  this.initialPageViewSent = true;
2479
- this.trackPageView(location.href);
2932
+ this.trackPageView(this.pageHref());
2480
2933
  }
2481
2934
  if (this.config) {
2482
2935
  for (const [props, extra] of this.earlyExceptions.splice(0)) {
@@ -2489,7 +2942,7 @@ var GuidelyClient = class _GuidelyClient {
2489
2942
  syncReplay() {
2490
2943
  const cfg = this.isReady && this.captureAllowed() ? this.replayConfig() : null;
2491
2944
  if (cfg && this.session.replay === void 0) {
2492
- this.session.replay = (0, import_runtime15.rolloutBucket)(this.session.id, "replay") < cfg.sampleRate;
2945
+ this.session.replay = (0, import_runtime18.rolloutBucket)(this.session.id, "replay") < cfg.sampleRate;
2493
2946
  this.persistSession();
2494
2947
  }
2495
2948
  if (!cfg || !this.session.replay) {
@@ -2511,7 +2964,7 @@ var GuidelyClient = class _GuidelyClient {
2511
2964
  return {
2512
2965
  anonymousId: this.anonymousId,
2513
2966
  userId: this.user?.id || null,
2514
- url: this.collectUrls() ? location.href : void 0,
2967
+ url: this.collectUrls() ? this.pageHref() : void 0,
2515
2968
  device: { type: device.type, browser: device.browser, os: device.os },
2516
2969
  viewport: { width: window.innerWidth, height: window.innerHeight },
2517
2970
  sdk: { name: SDK_NAME, version: SDK_VERSION }
@@ -2562,6 +3015,14 @@ var GuidelyClient = class _GuidelyClient {
2562
3015
  // ---------------------------------------------------------------------------
2563
3016
  // Targeting + triggers
2564
3017
  // ---------------------------------------------------------------------------
3018
+ /** The page URL Guidely works with: `options.pageUrl`, else app-relative for `file:` / custom protocols (`normalizeAppUrl`). */
3019
+ pageHref(href = location.href) {
3020
+ try {
3021
+ return this.options.pageUrl ? this.options.pageUrl(href) || href : normalizeAppUrl(href);
3022
+ } catch {
3023
+ return href;
3024
+ }
3025
+ }
2565
3026
  /** Builds the targeting context for the current moment. */
2566
3027
  getContext() {
2567
3028
  const guides = this.state.guides;
@@ -2572,7 +3033,7 @@ var GuidelyClient = class _GuidelyClient {
2572
3033
  userId: this.user?.id || null,
2573
3034
  userAttributes: this.user?.traits ?? {},
2574
3035
  accountAttributes: this.user?.account?.traits ?? {},
2575
- url: location.href,
3036
+ url: this.pageHref(),
2576
3037
  firstVisit: this.firstVisit,
2577
3038
  eventsSeen: this.session.events,
2578
3039
  completedGuides: keysOf(Object.keys(guides).filter((id) => guides[id]?.completedAt)),
@@ -2581,17 +3042,64 @@ var GuidelyClient = class _GuidelyClient {
2581
3042
  completedChecklists: keysOf(completedChecklists),
2582
3043
  audienceIds: this.state.audienceIds,
2583
3044
  environment: this.options.environment,
2584
- platform: "web"
3045
+ platform: "web",
3046
+ ...this.options.appVersion ? { appVersion: this.options.appVersion } : {}
2585
3047
  });
2586
3048
  }
2587
3049
  isTargeted(guide, ctx, url) {
2588
3050
  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;
3051
+ if (c.urlPatterns.length && !c.urlPatterns.some((p) => (0, import_guide_runtime5.urlMatches)(p, url))) return false;
3052
+ if (c.trigger.urlPattern && !(0, import_guide_runtime5.urlMatches)(c.trigger.urlPattern, url)) return false;
2591
3053
  if (!isWithinSchedule(guide)) return false;
2592
3054
  if (guide.kind === "survey" && (this.state.surveysSubmitted.includes(guide.id) || this.state.surveysSubmitted.includes(guide.key))) return false;
2593
3055
  ctx.rolloutSalt = guide.id;
2594
- return (0, import_runtime15.evaluateRules)(c.targeting, ctx);
3056
+ return (0, import_runtime18.evaluateRules)(c.targeting, ctx);
3057
+ }
3058
+ /** Guide throttling: may this guide launch automatically now? */
3059
+ throttleAllows(guide) {
3060
+ if (!countsTowardThrottle(guide)) return true;
3061
+ return isThrottleEligible(this.config?.settings.throttling, this.state.throttle ?? []);
3062
+ }
3063
+ recordLaunch(guide, manual) {
3064
+ if (manual || !countsTowardThrottle(guide) || !this.config?.settings.throttling?.enabled) return;
3065
+ this.state.throttle = pruneLaunches([...this.state.throttle ?? [], nowIso()], this.config.settings.throttling);
3066
+ this.persistState();
3067
+ }
3068
+ /** Guides and A/B test control groups, which are evaluated the same way but never render. */
3069
+ candidates() {
3070
+ const guides = this.config?.guides ?? [];
3071
+ const holdouts = this.config?.guideHoldouts ?? [];
3072
+ return holdouts.length ? [...guides, ...holdouts.map((h) => h.guide)] : guides;
3073
+ }
3074
+ holdoutFor(guide) {
3075
+ return this.config?.guideHoldouts?.find((h) => h.guide.id === guide.id) ?? null;
3076
+ }
3077
+ /** The visitor's language candidates, most preferred first. */
3078
+ locales() {
3079
+ const nav = typeof navigator !== "undefined" ? [...navigator.languages ?? [], navigator.language] : [];
3080
+ return (0, import_runtime18.visitorLocales)(this.locale ?? this.options.locale, this.user?.traits, nav);
3081
+ }
3082
+ defaultLocale() {
3083
+ return this.config?.settings.localization?.defaultLocale || "en";
3084
+ }
3085
+ /** The guide in the visitor's language (remembered so its events carry `locale`). */
3086
+ localize(guide) {
3087
+ const localized = (0, import_runtime18.localizeGuide)(guide, this.locales(), this.defaultLocale());
3088
+ this.guideLocales.set(guide.id, localized.locale);
3089
+ return localized;
3090
+ }
3091
+ /**
3092
+ * Sets the language for guides and the resource center (`null` = back to the traits / browser).
3093
+ * Guides already on screen keep their language; the resource center re-renders.
3094
+ */
3095
+ setLocale(locale) {
3096
+ this.locale = locale ? (0, import_runtime18.normalizeLocale)(locale) || null : null;
3097
+ if (this.resourceCenter) {
3098
+ this.resourceCenter.destroy();
3099
+ this.resourceCenterScope?.destroy();
3100
+ this.resourceCenter = null;
3101
+ this.renderPersistentWidgets();
3102
+ }
2595
3103
  }
2596
3104
  isEligible(guide, ctx, url) {
2597
3105
  if (!this.isTargeted(guide, ctx, url)) return false;
@@ -2609,13 +3117,13 @@ var GuidelyClient = class _GuidelyClient {
2609
3117
  if (t.delayMs > 0) return reason === "timer" && detail.guideId === guide.id;
2610
3118
  return reason === "ready" || reason === "navigation" || reason === "identify";
2611
3119
  case "url_match":
2612
- return (reason === "ready" || reason === "navigation" || reason === "identify") && (0, import_guide_runtime4.urlMatches)(t.value, location.href);
3120
+ return (reason === "ready" || reason === "navigation" || reason === "identify") && (0, import_guide_runtime5.urlMatches)(t.value, this.pageHref());
2613
3121
  case "event":
2614
3122
  return reason === "event" && !!t.value && detail.eventName === t.value;
2615
3123
  case "element_click":
2616
3124
  if (reason !== "click" || !t.value || !detail.target) return false;
2617
3125
  try {
2618
- return !!(0, import_guide_runtime4.closestComposed)(detail.target, t.value);
3126
+ return !!(0, import_guide_runtime5.closestComposed)(detail.target, t.value);
2619
3127
  } catch {
2620
3128
  return false;
2621
3129
  }
@@ -2634,15 +3142,15 @@ var GuidelyClient = class _GuidelyClient {
2634
3142
  }
2635
3143
  evaluate(reason, detail = {}) {
2636
3144
  if (!this.config || this.destroyed || this.editing()) return;
2637
- const url = location.href;
3145
+ const url = this.pageHref();
2638
3146
  const ctx = this.getContext();
2639
3147
  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;
3148
+ const current3 = new Set(this.state.audienceIds);
3149
+ detail.newAudiences = new Set(Array.from(current3).filter((a) => !this.lastAudiences.has(a)));
3150
+ this.lastAudiences = current3;
2643
3151
  }
2644
3152
  const candidates = [];
2645
- for (const guide of this.config.guides) {
3153
+ for (const guide of this.candidates()) {
2646
3154
  const t = guide.config.trigger;
2647
3155
  if (detail.endedGuide && detail.endedGuide.id === guide.id) continue;
2648
3156
  if (reason === "ready" || reason === "navigation") {
@@ -2658,6 +3166,10 @@ var GuidelyClient = class _GuidelyClient {
2658
3166
  if (t.type === "page_load" && t.delayMs > 0 && reason === "timer" && detail.guideId !== guide.id) continue;
2659
3167
  if (!this.triggerFires(guide, reason, detail)) continue;
2660
3168
  if (!this.isEligible(guide, ctx, url)) continue;
3169
+ if (!this.throttleAllows(guide)) {
3170
+ this.log("throttled", guide.key);
3171
+ continue;
3172
+ }
2661
3173
  candidates.push(guide);
2662
3174
  }
2663
3175
  if (reason === "after_guide" || reason === "navigation") this.drainQueue(url, ctx);
@@ -2685,7 +3197,7 @@ var GuidelyClient = class _GuidelyClient {
2685
3197
  this.visibilityObservers.delete(guide.id);
2686
3198
  };
2687
3199
  this.visibilityObservers.set(guide.id, stop);
2688
- (0, import_guide_runtime4.waitForTarget)([{ kind: "css", value: selector }], { timeoutMs: 0, signal: ac.signal }).then((el) => {
3200
+ (0, import_guide_runtime5.waitForTarget)([{ kind: "css", value: selector }], { timeoutMs: 0, signal: ac.signal }).then((el) => {
2689
3201
  if (ac.signal.aborted) return;
2690
3202
  const fire = () => {
2691
3203
  stop();
@@ -2708,6 +3220,13 @@ var GuidelyClient = class _GuidelyClient {
2708
3220
  /** Decides whether a guide can show now, otherwise queues it. */
2709
3221
  tryShow(guide, url, manual) {
2710
3222
  if (this.destroyed) return false;
3223
+ if (!manual && !this.throttleAllows(guide)) return false;
3224
+ const holdout = this.holdoutFor(guide);
3225
+ if (holdout) {
3226
+ if (this.active && !manual) return false;
3227
+ this.recordImpression(guide, manual);
3228
+ return true;
3229
+ }
2711
3230
  if (!isExclusiveGuide(guide)) {
2712
3231
  if (this.widgets.has(guide.id)) return false;
2713
3232
  this.show(guide, manual);
@@ -2748,14 +3267,18 @@ var GuidelyClient = class _GuidelyClient {
2748
3267
  guideState(id) {
2749
3268
  return this.state.guides[id] ?? (this.state.guides[id] = { impressions: 0 });
2750
3269
  }
2751
- recordImpression(guide) {
3270
+ recordImpression(guide, manual = false) {
2752
3271
  const s = this.guideState(guide.id);
2753
3272
  s.impressions += 1;
2754
3273
  s.lastShownAt = nowIso();
2755
3274
  if (!this.session.shownGuides.includes(guide.id)) this.session.shownGuides.push(guide.id);
3275
+ this.recordLaunch(guide, manual);
2756
3276
  this.persistState();
2757
3277
  this.persistSession();
2758
- this.syncGuideState(guide.id, { impression: true });
3278
+ const holdout = this.holdoutFor(guide);
3279
+ const experiment = holdout ? { id: holdout.experimentId, variantId: holdout.variantId } : guide.experiment;
3280
+ if (experiment) this.enqueue("experiment_viewed", { properties: { guideExperimentId: experiment.id, variantId: experiment.variantId, ...holdout ? {} : { guideId: guide.id } } });
3281
+ if (!holdout) this.syncGuideState(guide.id, { impression: true });
2759
3282
  }
2760
3283
  recordEnd(guide, reason) {
2761
3284
  const s = this.guideState(guide.id);
@@ -2779,13 +3302,17 @@ var GuidelyClient = class _GuidelyClient {
2779
3302
  this.reportError(err);
2780
3303
  }
2781
3304
  }
2782
- showFlow(guide, manual, stepId) {
3305
+ showFlow(raw, manual, stepId) {
2783
3306
  if (this.editing()) return;
3307
+ const guide = this.localize(raw);
2784
3308
  const exclusive = isExclusiveGuide(guide);
2785
- const entry = { guide, manual, startUrl: location.href };
2786
- const controller = (0, import_guide_runtime4.runGuide)(guide, {
3309
+ const entry = { guide, manual, startUrl: this.pageHref() };
3310
+ const controller = (0, import_guide_runtime5.runGuide)(guide, {
2787
3311
  theme: this.theme(),
3312
+ // Code blocks only arrive when a workspace admin allowed them (the server strips them otherwise).
3313
+ runCode: true,
2788
3314
  ctx: () => this.getContext(),
3315
+ currentUrl: () => this.pageHref(),
2789
3316
  startAtStep: stepId,
2790
3317
  eventBus: this.eventBus,
2791
3318
  // Started from Help, Scout or startGuide(): steps on other pages offer to take the user there.
@@ -2799,7 +3326,7 @@ var GuidelyClient = class _GuidelyClient {
2799
3326
  const step = e.stepId ? guide.config.steps.find((s) => s.id === e.stepId) : void 0;
2800
3327
  this.enqueue(e.name, { guideId: guide.id, guideVersion: guide.version, stepId: e.stepId, stepIndex: e.stepIndex, properties: e.properties });
2801
3328
  if (e.name === "guide_started") {
2802
- this.recordImpression(guide);
3329
+ this.recordImpression(guide, manual);
2803
3330
  this.emitter.emit("guide:start", guide);
2804
3331
  }
2805
3332
  if (e.name === "step_viewed" && step) {
@@ -2846,14 +3373,15 @@ var GuidelyClient = class _GuidelyClient {
2846
3373
  this.widgets.delete(guide.id);
2847
3374
  }
2848
3375
  }
2849
- showChecklist(guide) {
3376
+ showChecklist(raw) {
2850
3377
  if (this.editing()) return;
3378
+ const guide = this.localize(raw);
2851
3379
  const config = guide.config.checklist;
2852
3380
  if (!config || this.widgets.has(guide.id)) return;
2853
- const scope = (0, import_guide_runtime5.createScope)(this.theme(), guide.config.appearance);
3381
+ const scope = (0, import_guide_runtime6.createScope)(this.theme(), guide.config.appearance);
2854
3382
  const progress = this.getChecklistProgress(guide.id);
2855
3383
  const first = !this.state.guides[guide.id]?.impressions;
2856
- const widget = (0, import_guide_runtime4.renderChecklist)({
3384
+ const widget = (0, import_guide_runtime5.renderChecklist)({
2857
3385
  scope: scope.el,
2858
3386
  config,
2859
3387
  progress,
@@ -2868,12 +3396,13 @@ var GuidelyClient = class _GuidelyClient {
2868
3396
  }, checklist: widget, scope });
2869
3397
  if (first) this.recordImpression(guide);
2870
3398
  }
2871
- showSurvey(guide, manual) {
3399
+ showSurvey(raw, manual) {
2872
3400
  if (this.editing()) return;
3401
+ const guide = this.localize(raw);
2873
3402
  const config = guide.config.survey;
2874
3403
  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 };
3404
+ const scope = (0, import_guide_runtime6.createScope)(this.theme(), guide.config.appearance);
3405
+ const entry = { guide, manual, startUrl: this.pageHref(), scope };
2877
3406
  const finish = (reason) => {
2878
3407
  widget.destroy();
2879
3408
  scope.destroy();
@@ -2885,13 +3414,13 @@ var GuidelyClient = class _GuidelyClient {
2885
3414
  }
2886
3415
  this.setTimer(() => this.evaluate("after_guide", { endedGuide: guide }), 0);
2887
3416
  };
2888
- const widget = (0, import_guide_runtime4.renderSurvey)({
3417
+ const widget = (0, import_guide_runtime5.renderSurvey)({
2889
3418
  scope: scope.el,
2890
3419
  config,
2891
3420
  title: guide.name,
2892
3421
  onDismiss: () => finish(this.state.surveysSubmitted.includes(guide.id) ? "submitted" : "dismissed"),
2893
3422
  onSubmit: (answers) => {
2894
- const responseId = (0, import_runtime15.randomId)(24);
3423
+ const responseId = (0, import_runtime18.randomId)(24);
2895
3424
  this.state.surveysSubmitted.push(guide.id);
2896
3425
  this.persistState();
2897
3426
  this.recordEnd(guide, "completed");
@@ -2904,7 +3433,7 @@ var GuidelyClient = class _GuidelyClient {
2904
3433
  });
2905
3434
  entry.survey = widget;
2906
3435
  this.active = entry;
2907
- this.recordImpression(guide);
3436
+ this.recordImpression(guide, manual);
2908
3437
  this.enqueue("survey_displayed", { guideId: guide.id, guideVersion: guide.version });
2909
3438
  this.emitter.emit("guide:start", guide);
2910
3439
  }
@@ -2913,6 +3442,7 @@ var GuidelyClient = class _GuidelyClient {
2913
3442
  }
2914
3443
  navigate(url, newTab) {
2915
3444
  try {
3445
+ if (this.openPrototypeTest(url)) return;
2916
3446
  if (newTab) window.open(url, "_blank", "noopener");
2917
3447
  else location.assign(url);
2918
3448
  } catch (err) {
@@ -2933,11 +3463,17 @@ var GuidelyClient = class _GuidelyClient {
2933
3463
  }
2934
3464
  this.destroyMessenger();
2935
3465
  if (rc && rc.enabled && !this.resourceCenter) {
2936
- const scope = (0, import_guide_runtime5.createScope)(this.theme());
3466
+ const scope = (0, import_guide_runtime6.createScope)(this.theme());
2937
3467
  this.resourceCenterScope = scope;
2938
- this.resourceCenter = (0, import_guide_runtime4.renderResourceCenter)({
3468
+ this.resourceCenter = (0, import_guide_runtime5.renderResourceCenter)({
2939
3469
  scope: scope.el,
2940
- config: rc,
3470
+ config: (0, import_runtime18.localizeResourceCenter)(rc, this.locales(), this.defaultLocale()),
3471
+ evaluate: (rules) => (0, import_runtime18.evaluateRules)(rules, this.getContext()),
3472
+ announcementsSeenAt: () => this.state.announcementsSeenAt ?? null,
3473
+ onAnnouncementsSeen: () => {
3474
+ this.state.announcementsSeenAt = nowIso();
3475
+ this.persistState();
3476
+ },
2941
3477
  guides: this.config.guides.map((g) => ({ id: g.id, key: g.key, name: g.name, kind: g.kind })),
2942
3478
  search: (q) => this.searchResourceCenter(q),
2943
3479
  audienceIds: () => this.state.audienceIds,
@@ -2971,7 +3507,7 @@ var GuidelyClient = class _GuidelyClient {
2971
3507
  this.messengerLoading = import("@guidelytech/guide-runtime/messenger").then(({ renderMessenger }) => {
2972
3508
  const cfg = this.config?.messenger;
2973
3509
  if (this.destroyed || !this.config || !cfg?.enabled || this.messenger) return this.messenger;
2974
- const scope = (0, import_guide_runtime5.createScope)(this.theme());
3510
+ const scope = (0, import_guide_runtime6.createScope)(this.theme());
2975
3511
  this.messengerScope = scope;
2976
3512
  this.messenger = renderMessenger({
2977
3513
  scope: scope.el,
@@ -2982,7 +3518,8 @@ var GuidelyClient = class _GuidelyClient {
2982
3518
  projectId: this.options.projectId,
2983
3519
  anonymousId: () => this.anonymousId,
2984
3520
  userId: () => this.user?.id || null,
2985
- pageUrl: () => this.collectUrls() ? location.href : void 0
3521
+ pageUrl: () => this.collectUrls() ? this.pageHref() : void 0,
3522
+ locale: () => this.rcLocale()
2986
3523
  }),
2987
3524
  visitor: () => this.messengerVisitor(),
2988
3525
  brand: { logoUrl: this.theme().logoUrl ?? null, name: this.theme().brandName ?? null },
@@ -2991,7 +3528,8 @@ var GuidelyClient = class _GuidelyClient {
2991
3528
  const seen = readJson(this.store, KEYS.newsSeen(this.options.projectId), []);
2992
3529
  writeJson(this.store, KEYS.newsSeen(this.options.projectId), Array.from(/* @__PURE__ */ new Set([...ids, ...Array.isArray(seen) ? seen : []])).slice(0, 200));
2993
3530
  },
2994
- resourceCenter: this.config.resourceCenter,
3531
+ resourceCenter: this.config.resourceCenter ? (0, import_runtime18.localizeResourceCenter)(this.config.resourceCenter, this.locales(), this.defaultLocale()) : null,
3532
+ evaluate: (rules) => (0, import_runtime18.evaluateRules)(rules, this.getContext()),
2995
3533
  audienceIds: () => this.state.audienceIds,
2996
3534
  guides: this.config.guides.map((g) => ({ id: g.id, key: g.key, name: g.name, kind: g.kind })),
2997
3535
  onStartGuide: (idOrKey) => this.startGuide(idOrKey),
@@ -3015,7 +3553,7 @@ var GuidelyClient = class _GuidelyClient {
3015
3553
  /** What the messenger personalises from, computed fresh on every render. */
3016
3554
  messengerVisitor() {
3017
3555
  const ctx = this.getContext();
3018
- const url = location.href;
3556
+ const url = this.pageHref();
3019
3557
  const seen = readJson(this.store, KEYS.newsSeen(this.options.projectId), []);
3020
3558
  const visitor = buildMessengerVisitor({
3021
3559
  guides: this.config?.guides ?? [],
@@ -3026,11 +3564,24 @@ var GuidelyClient = class _GuidelyClient {
3026
3564
  isAudienceTargeted: (g) => {
3027
3565
  if (!isWithinSchedule(g)) return false;
3028
3566
  ctx.rolloutSalt = g.id;
3029
- return (0, import_runtime15.evaluateRules)(g.config.targeting, ctx);
3567
+ return (0, import_runtime18.evaluateRules)(g.config.targeting, ctx);
3030
3568
  },
3031
3569
  newsSeen: Array.isArray(seen) ? seen : []
3032
3570
  });
3033
- return { ...visitor, identified: !!this.user?.id };
3571
+ const rc = this.config?.resourceCenter;
3572
+ const rcNews = rc?.announcements?.length ? (0, import_runtime18.visibleAnnouncements)((0, import_runtime18.localizeResourceCenter)(rc, this.locales(), this.defaultLocale()).announcements, this.state.audienceIds).map((a) => ({
3573
+ id: `rc_${a.id}`,
3574
+ key: a.guideId ?? "",
3575
+ title: a.title,
3576
+ body: plainText(a.body),
3577
+ imageUrl: null,
3578
+ publishedAt: a.publishedAt,
3579
+ unread: !(Array.isArray(seen) && seen.includes(`rc_${a.id}`)),
3580
+ articleId: a.articleId ?? null,
3581
+ url: a.url ?? null
3582
+ })) : [];
3583
+ const news = rcNews.length ? [...visitor.news, ...rcNews].sort((a, b) => b.publishedAt.localeCompare(a.publishedAt)).slice(0, 10) : visitor.news;
3584
+ return { ...visitor, news, identified: !!this.user?.id };
3034
3585
  }
3035
3586
  destroyMessenger() {
3036
3587
  this.messenger?.destroy();
@@ -3038,9 +3589,17 @@ var GuidelyClient = class _GuidelyClient {
3038
3589
  this.messenger = null;
3039
3590
  this.messengerScope = null;
3040
3591
  }
3592
+ /** The resource center's language for this visitor, or null for the default language. */
3593
+ rcLocale() {
3594
+ const rc = this.config?.resourceCenter;
3595
+ if (!rc) return null;
3596
+ const locale = (0, import_runtime18.localizeResourceCenter)(rc, this.locales(), this.defaultLocale()).locale;
3597
+ return locale.toLowerCase() === this.defaultLocale().toLowerCase() ? null : locale;
3598
+ }
3041
3599
  async loadArticle(id) {
3042
3600
  try {
3043
- const res = await this.api.get(`/articles/${encodeURIComponent(id)}`);
3601
+ const locale = this.rcLocale();
3602
+ const res = await this.api.get(`/articles/${encodeURIComponent(id)}`, locale ? { params: { locale } } : void 0);
3044
3603
  return res.ok && res.data ? res.data.article : null;
3045
3604
  } catch {
3046
3605
  return null;
@@ -3078,7 +3637,7 @@ var GuidelyClient = class _GuidelyClient {
3078
3637
  if (progress.completedTaskIds.includes(task.id)) return;
3079
3638
  progress.completedTaskIds.push(task.id);
3080
3639
  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;
3640
+ const complete = (0, import_guide_runtime5.isChecklistComplete)(config, progress) && !progress.completedAt;
3082
3641
  if (complete) {
3083
3642
  progress.completedAt = nowIso();
3084
3643
  this.enqueue("checklist_completed", { guideId: guide.id, guideVersion: guide.version });
@@ -3101,11 +3660,11 @@ var GuidelyClient = class _GuidelyClient {
3101
3660
  if (task.completion !== kind || !task.completionValue) continue;
3102
3661
  let hit = false;
3103
3662
  if (kind === "event") hit = value === task.completionValue;
3104
- else if (kind === "url_visited") hit = (0, import_guide_runtime4.urlMatches)(task.completionValue, value);
3663
+ else if (kind === "url_visited") hit = (0, import_guide_runtime5.urlMatches)(task.completionValue, value);
3105
3664
  else if (kind === "guide_completed") hit = value.id === task.completionValue || value.key === task.completionValue;
3106
3665
  else if (kind === "click") {
3107
3666
  try {
3108
- hit = !!(0, import_guide_runtime4.closestComposed)(value, task.completionValue);
3667
+ hit = !!(0, import_guide_runtime5.closestComposed)(value, task.completionValue);
3109
3668
  } catch {
3110
3669
  hit = false;
3111
3670
  }
@@ -3216,6 +3775,18 @@ var GuidelyClient = class _GuidelyClient {
3216
3775
  closeMessenger() {
3217
3776
  this.messenger?.close();
3218
3777
  }
3778
+ /**
3779
+ * Opens a Guidely prototype test (its `/validate/<token>` URL on this SDK's API origin, or the bare
3780
+ * token) in a full-screen overlay, attributed to the current user. Returns false, doing nothing, for
3781
+ * any other URL. Guide buttons that open a test URL use this automatically.
3782
+ */
3783
+ openPrototypeTest(urlOrToken) {
3784
+ if (typeof document === "undefined") return false;
3785
+ const url = matchPrototypeTestUrl(urlOrToken, this.options.apiUrl);
3786
+ if (!url) return false;
3787
+ openPrototypeOverlay(url, { key: this.options.projectId, anonymousId: this.anonymousId, userId: this.user?.id ?? null });
3788
+ return true;
3789
+ }
3219
3790
  /**
3220
3791
  * Opens the complaint form in the messenger, optionally with a category picked (its id from
3221
3792
  * Complaints → Categories). Does nothing when the project doesn't take complaints.
@@ -3224,6 +3795,14 @@ var GuidelyClient = class _GuidelyClient {
3224
3795
  if (!this.config?.messenger?.enabled || !this.config.messenger.complaints?.categories.length) return;
3225
3796
  void this.loadMessenger().then((m) => m?.openComplaint(categoryId));
3226
3797
  }
3798
+ /**
3799
+ * Opens the Ideas tab in the messenger (feature requests and voting), or one idea by its id.
3800
+ * Does nothing when the project hasn't turned on Ideas.
3801
+ */
3802
+ openIdeas(requestId) {
3803
+ if (!this.config?.messenger?.enabled || !this.config.messenger.ideas) return;
3804
+ void this.loadMessenger().then((m) => m?.openIdeas(requestId));
3805
+ }
3227
3806
  /** Opens a new messenger conversation, optionally pre-filling the message. */
3228
3807
  showNewMessage(text) {
3229
3808
  if (!this.config?.messenger?.enabled) return;
@@ -3253,14 +3832,15 @@ var GuidelyClient = class _GuidelyClient {
3253
3832
  const device = detectDevice(navigator.userAgent, window.innerWidth);
3254
3833
  const account = this.user?.account;
3255
3834
  return {
3256
- url: location.href,
3257
- path: location.pathname,
3835
+ url: this.pageHref(),
3836
+ path: pathOf2(this.pageHref()),
3258
3837
  deviceType: device.type,
3259
3838
  browser: device.browser,
3260
3839
  os: device.os,
3261
3840
  locale: navigator.language || void 0,
3262
3841
  environment: this.options.environment,
3263
3842
  platform: "web",
3843
+ ...this.options.appVersion ? { appVersion: this.options.appVersion } : {},
3264
3844
  ...this.user?.traits ?? {},
3265
3845
  id: this.user?.id || this.anonymousId,
3266
3846
  anonymousId: this.anonymousId,
@@ -3420,20 +4000,20 @@ var GuidelyClient = class _GuidelyClient {
3420
4000
  // ---------------------------------------------------------------------------
3421
4001
  /** Creates (or returns the existing) client and starts initialisation. */
3422
4002
  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;
4003
+ if (current2 && !current2.destroyed && current2.options.projectId === options.projectId) return current2;
4004
+ current2?.destroy();
4005
+ current2 = new _GuidelyClient(options);
4006
+ void current2.init();
4007
+ return current2;
3428
4008
  }
3429
4009
  /** The most recently created client, if any. */
3430
4010
  static get() {
3431
- return current && !current.destroyed ? current : null;
4011
+ return current2 && !current2.destroyed ? current2 : null;
3432
4012
  }
3433
4013
  };
3434
- var current = null;
4014
+ var current2 = null;
3435
4015
  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")) };
4016
+ 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
4017
  }
3438
4018
  function pathOf2(url) {
3439
4019
  try {
@@ -3453,7 +4033,7 @@ function externalReferrer() {
3453
4033
  }
3454
4034
  function createGuidely(options) {
3455
4035
  const client = new GuidelyClient(options);
3456
- current = client;
4036
+ current2 = client;
3457
4037
  void client.init();
3458
4038
  return client;
3459
4039
  }
@@ -3476,17 +4056,24 @@ var index_default = Guidely;
3476
4056
  GuidelyClient,
3477
4057
  HEARTBEAT_MS,
3478
4058
  KEYS,
4059
+ PROTOTYPE_CLOSE_MESSAGE,
3479
4060
  SDK_NAME,
3480
4061
  SDK_VERSION,
3481
4062
  areMutuallyExclusive,
3482
4063
  buildContext,
3483
4064
  captureSnapshot,
4065
+ countsTowardThrottle,
3484
4066
  createGuidely,
3485
4067
  createStorage,
3486
4068
  detectDevice,
4069
+ isAppUrl,
3487
4070
  isExclusiveGuide,
3488
4071
  isFrequencyEligible,
4072
+ isThrottleEligible,
3489
4073
  isWithinSchedule,
4074
+ matchPrototypeTestUrl,
4075
+ normalizeAppUrl,
4076
+ pruneLaunches,
3490
4077
  watchNavigation
3491
4078
  });
3492
4079
  //# sourceMappingURL=index.cjs.map