@guidelytech/sdk-core 0.9.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +15 -4
  2. package/dist/{capture-Dqd7up2n.d.ts → capture-CgN69Lxq.d.cts} +2 -1
  3. package/dist/{capture-C3faAYb7.d.cts → capture-Csz721-A.d.ts} +2 -1
  4. package/dist/capture.cjs +4 -0
  5. package/dist/capture.cjs.map +1 -1
  6. package/dist/capture.d.cts +3 -2
  7. package/dist/capture.d.ts +3 -2
  8. package/dist/capture.js +4 -6
  9. package/dist/capture.js.map +1 -1
  10. package/dist/chunk-JUSM7J5N.js +376 -0
  11. package/dist/chunk-JUSM7J5N.js.map +1 -0
  12. package/dist/{chunk-544KSOC3.js → chunk-ZAF2KRJA.js} +142 -22
  13. package/dist/chunk-ZAF2KRJA.js.map +1 -0
  14. package/dist/headless.cjs +307 -44
  15. package/dist/headless.cjs.map +1 -1
  16. package/dist/headless.d.cts +44 -3
  17. package/dist/headless.d.ts +44 -3
  18. package/dist/headless.js +21 -9
  19. package/dist/health-sbP9JwTE.d.cts +90 -0
  20. package/dist/health-sbP9JwTE.d.ts +90 -0
  21. package/dist/{http-47tiAlOM.d.ts → http-Cb6KYYFv.d.cts} +10 -35
  22. package/dist/{http-47tiAlOM.d.cts → http-D047Ngxk.d.ts} +10 -35
  23. package/dist/index.cjs +1352 -500
  24. package/dist/index.cjs.map +1 -1
  25. package/dist/index.d.cts +81 -8
  26. package/dist/index.d.ts +81 -8
  27. package/dist/index.js +809 -211
  28. package/dist/index.js.map +1 -1
  29. package/dist/recorder.cjs +70 -9
  30. package/dist/recorder.cjs.map +1 -1
  31. package/dist/recorder.d.cts +9 -0
  32. package/dist/recorder.d.ts +9 -0
  33. package/dist/recorder.js +57 -11
  34. package/dist/recorder.js.map +1 -1
  35. package/dist/{types-DnJW_4ck.d.ts → types-BDlebXrm.d.ts} +68 -7
  36. package/dist/{types-zM5e3loY.d.cts → types-Mtlmlj5C.d.cts} +68 -7
  37. package/package.json +4 -4
  38. package/dist/chunk-544KSOC3.js.map +0 -1
  39. package/dist/chunk-AKNE7D5M.js +0 -160
  40. package/dist/chunk-AKNE7D5M.js.map +0 -1
  41. package/dist/chunk-CYAG6ZYX.js +0 -85
  42. package/dist/chunk-CYAG6ZYX.js.map +0 -1
  43. package/dist/chunk-GDF6RCHQ.js +0 -209
  44. package/dist/chunk-GDF6RCHQ.js.map +0 -1
  45. package/dist/types-DbKQPvFt.d.cts +0 -238
  46. package/dist/types-DqhNl5Am.d.ts +0 -238
  47. package/dist/types-RyeRQd9f.d.ts +0 -226
  48. package/dist/types-d4eOerxi.d.cts +0 -226
package/dist/index.cjs CHANGED
@@ -33,37 +33,559 @@ 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_runtime13 = 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
 
72
+ // src/agent-turns.ts
73
+ var import_runtime4 = require("@guidelytech/shared/runtime");
74
+
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;
94
+ }
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();
102
+ }
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;
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));
114
+ }
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());
119
+ }
120
+ canSend() {
121
+ return this.waitMs() === 0;
122
+ }
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);
155
+ }
156
+ function isServerFailure(status) {
157
+ return status === 429 || status >= 500;
158
+ }
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) {
214
+ try {
215
+ data = await res.json();
216
+ } catch {
217
+ data = null;
218
+ }
219
+ }
220
+ return { status: res.status, ok: res.ok, data, etag: res.headers?.get?.("etag") ?? null };
221
+ } finally {
222
+ if (timer) clearTimeout(timer);
223
+ }
224
+ }
225
+ get(path, options) {
226
+ return this.request("GET", path, options);
227
+ }
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
+ }
400
+ function writeJson(store, key, value) {
401
+ try {
402
+ store.set(key, JSON.stringify(value));
403
+ } catch {
404
+ }
405
+ }
406
+ var KEYS = {
407
+ anon: "guidely:anon",
408
+ seen: "guidely:seen",
409
+ config: (key) => `guidely:config:${key}`,
410
+ etag: (key) => `guidely:etag:${key}`,
411
+ state: (key) => `guidely:state:${key}`,
412
+ user: (key) => `guidely:user:${key}`,
413
+ queue: (key) => `guidely:queue:${key}`,
414
+ /** Survey answers and guide/checklist progress not yet accepted by the server. */
415
+ outbox: (key) => `guidely:outbox:${key}`,
416
+ /** Agent Analytics turns not yet accepted by the server. */
417
+ agentOutbox: (key) => `guidely:agents:${key}`,
418
+ session: "guidely:session",
419
+ active: "guidely:active",
420
+ authoring: "guidely:authoring",
421
+ /** How-to capture session marker ({ token, … }) and its not-yet-uploaded steps. */
422
+ capture: "guidely:capture",
423
+ capturePending: "guidely:capture:pending",
424
+ /** Messenger conversations this browser can read: [{ id, token }]. */
425
+ conversations: (key) => `guidely:conversations:${key}`,
426
+ /** Announcement ids opened in the messenger's News tab. */
427
+ newsSeen: (key) => `guidely:news:${key}`
428
+ };
429
+
430
+ // src/outbox.ts
431
+ var Outbox = class {
432
+ constructor(opts) {
433
+ __publicField(this, "opts", opts);
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);
452
+ }
453
+ get size() {
454
+ return this.items.length;
455
+ }
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();
464
+ }
465
+ persist() {
466
+ writeJson(this.opts.store, this.opts.storageKey, this.items);
467
+ }
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);
475
+ }
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
+ }
505
+ }
506
+ destroy() {
507
+ this.destroyed = true;
508
+ this.offRecover?.();
509
+ if (this.timer) clearTimeout(this.timer);
510
+ this.timer = null;
511
+ }
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) {
537
+ try {
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 };
541
+ } catch (err) {
542
+ onError?.(err);
543
+ return null;
544
+ }
545
+ }
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;
570
+ }
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 });
574
+ }
575
+ }
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();
584
+ }
585
+ };
586
+
65
587
  // src/authoring.ts
66
- var import_guide_runtime = require("@guidelytech/guide-runtime");
588
+ var import_guide_runtime3 = require("@guidelytech/guide-runtime");
67
589
 
68
590
  // src/snapshot.ts
69
591
  var MAX_SNAPSHOT_BYTES = 6 * 1024 * 1024;
@@ -125,98 +647,291 @@ function captureSnapshot(doc, maskSelectors, maxBytes = MAX_SNAPSHOT_BYTES) {
125
647
  head = doc.createElement("head");
126
648
  clone.prepend(head);
127
649
  }
128
- if (adopted) {
129
- const style = doc.createElement("style");
130
- style.textContent = adopted;
131
- head.append(style);
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) {
680
+ const s = (raw || "").trim();
681
+ if (!s || out.includes(s)) continue;
682
+ try {
683
+ probe?.querySelector(s);
684
+ out.push(s);
685
+ } catch {
686
+ }
687
+ }
688
+ return out;
689
+ }
690
+ function isIgnoredElement(el) {
691
+ return (0, import_guide_runtime.isGuidelyElement)(el) || !!(0, import_guide_runtime.closestComposed)(el, "[data-guidely-ignore]");
692
+ }
693
+ function interactiveElement(target) {
694
+ try {
695
+ return (0, import_guide_runtime.closestComposed)(target, INTERACTIVE_SELECTOR);
696
+ } catch {
697
+ return null;
698
+ }
699
+ }
700
+ function esc(value) {
701
+ const g = globalThis;
702
+ return g.CSS?.escape ? g.CSS.escape(value) : value.replace(/[^\w-]/g, (c) => `\\${c}`);
703
+ }
704
+ var attr = (tag, name, value) => value ? `${tag}[${name}="${value.replace(/["\\]/g, "\\$&")}"]` : `${tag}[${name}]`;
705
+ function elementSelector(el) {
706
+ const own = ownSelector(el);
707
+ const chain = (0, import_guide_runtime.shadowHostChain)(el);
708
+ return (chain ? chain + import_guide_runtime.SHADOW_SEPARATOR + own : own).slice(0, 300);
709
+ }
710
+ function ownSelector(el) {
711
+ const tag = el.tagName.toLowerCase();
712
+ if (el.id && (0, import_guide_runtime.isStableId)(el.id)) return `#${esc(el.id)}`;
713
+ for (const name of PREFERRED_ATTRS) if (el.hasAttribute(name)) return attr(tag, name, el.getAttribute(name) || "");
714
+ for (const a of Array.from(el.attributes)) {
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);
716
+ }
717
+ const label = el.getAttribute("aria-label");
718
+ if (label && label.length <= 80) return attr(tag, "aria-label", label);
719
+ const parts = [];
720
+ let cur = el;
721
+ while (cur && parts.length < 4) {
722
+ const t = cur.tagName.toLowerCase();
723
+ if (t === "html" || t === "body") break;
724
+ if (cur !== el && cur.id && (0, import_guide_runtime.isStableId)(cur.id)) {
725
+ parts.unshift(`#${esc(cur.id)}`);
726
+ break;
727
+ }
728
+ const parent = cur.parentElement;
729
+ const node = cur;
730
+ const same = parent ? Array.from(parent.children).filter((c) => c.tagName === node.tagName) : [];
731
+ parts.unshift(same.length > 1 ? `${t}:nth-of-type(${same.indexOf(node) + 1})` : t);
732
+ cur = parent;
733
+ }
734
+ return parts.join(" > ") || tag;
735
+ }
736
+ function matches(el, selector, deep) {
737
+ if (!selector) return false;
738
+ try {
739
+ return !!(0, import_guide_runtime.closestComposed)(el, selector) || deep && !!el.querySelector(selector);
740
+ } catch {
741
+ return false;
742
+ }
743
+ }
744
+ function elementText(el, mask, maskAll) {
745
+ if (maskAll || matches(el, mask, true)) return void 0;
746
+ const tag = el.tagName.toLowerCase();
747
+ let text = "";
748
+ if (tag === "input") {
749
+ if (!/^(button|submit|reset)$/i.test(el.type || "")) return void 0;
750
+ text = el.value;
751
+ } else if (tag === "select" || tag === "textarea") return void 0;
752
+ else text = el.innerText ?? el.textContent ?? "";
753
+ text = (text || el.getAttribute("aria-label") || el.getAttribute("title") || "").replace(/\s+/g, " ").trim();
754
+ return text ? text.slice(0, 100) : void 0;
755
+ }
756
+ function linkHref(el) {
757
+ const a = (0, import_guide_runtime.closestComposed)(el, "a[href]");
758
+ if (!a) return void 0;
759
+ try {
760
+ const u = new URL(a.getAttribute("href") || "", location.href);
761
+ if (!/^https?:$/.test(u.protocol)) return void 0;
762
+ return u.origin === location.origin ? u.pathname : u.origin;
763
+ } catch {
764
+ return void 0;
765
+ }
766
+ }
767
+ function describeClick(el, e, opts) {
768
+ const props = { tag: el.tagName.toLowerCase(), selector: elementSelector(el), x: Math.round(e.pageX ?? 0) || 0, y: Math.round(e.pageY ?? 0) || 0 };
769
+ const text = elementText(el, opts.mask, opts.maskAll);
770
+ if (text) props.text = text;
771
+ const href = opts.collectUrls ? linkHref(el) : void 0;
772
+ if (href) props.href = href;
773
+ return props;
774
+ }
775
+ var RageClickDetector = class {
776
+ constructor(threshold = 3, windowMs = 1e3, radius = 30) {
777
+ __publicField(this, "threshold", threshold);
778
+ __publicField(this, "windowMs", windowMs);
779
+ __publicField(this, "radius", radius);
780
+ __publicField(this, "clicks", []);
781
+ __publicField(this, "fired", false);
782
+ }
783
+ /** Clicks in the current burst once it has fired (rage), otherwise 0. */
784
+ get burst() {
785
+ return this.fired ? this.clicks.length : 0;
132
786
  }
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() };
142
- }
787
+ /** Records a click; returns the click count when it completes a burst, otherwise 0. */
788
+ add(x, y, t) {
789
+ const first = this.clicks[0];
790
+ const last = this.clicks[this.clicks.length - 1];
791
+ if (!first || !last || t - last.t > this.windowMs || Math.hypot(x - first.x, y - first.y) > this.radius) {
792
+ this.clicks = [];
793
+ this.fired = false;
794
+ }
795
+ this.clicks = this.clicks.filter((c) => t - c.t <= this.windowMs);
796
+ this.clicks.push({ x, y, t });
797
+ if (this.fired || this.clicks.length < this.threshold) return 0;
798
+ this.fired = true;
799
+ return this.clicks.length;
800
+ }
801
+ };
143
802
 
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) };
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;
148
816
  }
149
- function webStore(area) {
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;
150
858
  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) => {
172
- try {
173
- s.removeItem(k);
174
- } catch {
175
- }
176
- }
177
- };
859
+ return !!(0, import_guide_runtime2.closestComposed)(el, mask);
178
860
  } catch {
179
- return null;
861
+ return false;
180
862
  }
181
863
  }
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;
864
+ function featureAttributeOf(target, attribute) {
865
+ if (!attribute) return null;
189
866
  try {
190
- return JSON.parse(raw);
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;
191
870
  } catch {
192
- return fallback;
871
+ return null;
193
872
  }
194
873
  }
195
- function writeJson(store, key, value) {
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;
881
+ }
882
+ }
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;
890
+ }
891
+ return elementText(el, opts.mask, opts.maskAll);
892
+ }
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 = "/";
196
920
  try {
197
- store.set(key, JSON.stringify(value));
921
+ path = new URL(url, location.href).pathname;
198
922
  } catch {
199
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;
200
934
  }
201
- var KEYS = {
202
- anon: "guidely:anon",
203
- seen: "guidely:seen",
204
- config: (key) => `guidely:config:${key}`,
205
- etag: (key) => `guidely:etag:${key}`,
206
- state: (key) => `guidely:state:${key}`,
207
- user: (key) => `guidely:user:${key}`,
208
- queue: (key) => `guidely:queue:${key}`,
209
- session: "guidely:session",
210
- active: "guidely:active",
211
- authoring: "guidely:authoring",
212
- /** How-to capture session marker ({ token, … }) and its not-yet-uploaded steps. */
213
- capture: "guidely:capture",
214
- capturePending: "guidely:capture:pending",
215
- /** Messenger conversations this browser can read: [{ id, token }]. */
216
- conversations: (key) => `guidely:conversations:${key}`,
217
- /** Announcement ids opened in the messenger's News tab. */
218
- newsSeen: (key) => `guidely:news:${key}`
219
- };
220
935
 
221
936
  // src/authoring.ts
222
937
  async function loadSchemas() {
@@ -286,8 +1001,10 @@ var AuthoringBridge = class {
286
1001
  }
287
1002
  this.active = true;
288
1003
  this.opts.onActive(true);
289
- this.session = (0, import_guide_runtime.startAuthoring)({
1004
+ this.session = (0, import_guide_runtime3.startAuthoring)({
290
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,
291
1008
  onPick: (pick) => this.sendPick(pick),
292
1009
  onExit: () => this.stop()
293
1010
  });
@@ -304,7 +1021,7 @@ var AuthoringBridge = class {
304
1021
  masked(el) {
305
1022
  return this.opts.maskSelectors.some((sel) => {
306
1023
  try {
307
- return !!el.closest(sel);
1024
+ return !!(0, import_guide_runtime3.closestComposed)(el, sel);
308
1025
  } catch {
309
1026
  return false;
310
1027
  }
@@ -315,6 +1032,7 @@ var AuthoringBridge = class {
315
1032
  const { element, ...rest } = raw;
316
1033
  const pick = this.masked(element) ? { ...rest, textSnapshot: "", selectors: rest.selectors.filter((s) => s.kind !== "text") } : rest;
317
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 });
318
1036
  this.post({ type: "guidely:pick", sessionId: this.info.id, pick });
319
1037
  void this.opts.api.post("/authoring/session/picks", pick, { params: { token: this.token } }).catch((err) => this.opts.onError(err));
320
1038
  }
@@ -373,7 +1091,7 @@ var AuthoringBridge = class {
373
1091
  const appearance = msg.appearance ? GuideConfigSchema.shape.appearance.parse(msg.appearance) : void 0;
374
1092
  this.session?.setPicking(false);
375
1093
  this.session?.setStatus(`Previewing step \u201C${step.title || step.id}\u201D \u2014 close it to continue picking`);
376
- this.preview = (0, import_guide_runtime.previewStep)(step, { ...this.opts.theme(), ...msg.theme || {} }, {
1094
+ this.preview = (0, import_guide_runtime3.previewStep)(step, { ...this.opts.theme(), ...msg.theme || {} }, {
377
1095
  appearance,
378
1096
  onEnd: () => {
379
1097
  this.preview = null;
@@ -390,7 +1108,7 @@ var AuthoringBridge = class {
390
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) };
391
1109
  this.session?.setPicking(false);
392
1110
  this.session?.setStatus(`Previewing \u201C${guide.name}\u201D`);
393
- this.previewGuide = (0, import_guide_runtime.runGuide)(guide, {
1111
+ this.previewGuide = (0, import_guide_runtime3.runGuide)(guide, {
394
1112
  theme: this.opts.theme(),
395
1113
  ctx: this.opts.ctx,
396
1114
  onNavigate: (url, newTab) => newTab ? window.open(url, "_blank", "noopener") : window.location.assign(url),
@@ -404,7 +1122,7 @@ var AuthoringBridge = class {
404
1122
  break;
405
1123
  }
406
1124
  case "guidely:highlight": {
407
- const result = (0, import_guide_runtime.highlightSelectors)(Array.isArray(msg.selectors) ? msg.selectors : [], { durationMs: 3e3 });
1125
+ const result = (0, import_guide_runtime3.highlightSelectors)(Array.isArray(msg.selectors) ? msg.selectors : [], { durationMs: 3e3 });
408
1126
  this.post({ type: "guidely:highlight-result", sessionId: this.info.id, counts: result.counts, resolved: !!result.resolved });
409
1127
  break;
410
1128
  }
@@ -448,169 +1166,41 @@ var AuthoringBridge = class {
448
1166
  };
449
1167
 
450
1168
  // src/capture-token.ts
451
- var import_runtime2 = require("@guidelytech/shared/runtime");
1169
+ var import_runtime9 = require("@guidelytech/shared/runtime");
452
1170
  function takeCaptureToken(sessionStore) {
453
1171
  if (typeof window === "undefined") return null;
454
1172
  let token = null;
455
1173
  let authoringLink = false;
456
1174
  try {
457
1175
  const url = new URL(window.location.href);
458
- token = url.searchParams.get(import_runtime2.CAPTURE_PARAM);
1176
+ token = url.searchParams.get(import_runtime9.CAPTURE_PARAM);
459
1177
  authoringLink = url.searchParams.has(AUTHORING_PARAM);
460
1178
  if (token) {
461
- url.searchParams.delete(import_runtime2.CAPTURE_PARAM);
1179
+ url.searchParams.delete(import_runtime9.CAPTURE_PARAM);
462
1180
  window.history.replaceState(window.history.state, "", url.toString());
463
1181
  }
464
- } catch {
465
- token = null;
466
- }
467
- const saved = readJson(sessionStore, KEYS.capture, null);
468
- let marker = null;
469
- if (token) marker = saved?.token === token ? saved : { token };
470
- else if (saved?.token && !authoringLink) marker = saved;
471
- if (!marker) {
472
- if (saved) {
473
- sessionStore.remove(KEYS.capture);
474
- sessionStore.remove(KEYS.capturePending);
475
- }
476
- return null;
477
- }
478
- if (token && saved?.token !== token) sessionStore.remove(KEYS.capturePending);
479
- writeJson(sessionStore, KEYS.capture, marker);
480
- sessionStore.remove(KEYS.authoring);
481
- return marker;
482
- }
483
-
484
- // src/load-capture.ts
485
- var loadCapture = async () => (await import("./capture.cjs")).capture;
486
-
487
- // src/autocapture.ts
488
- var import_guide_runtime2 = require("@guidelytech/guide-runtime");
489
- 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]";
490
- var DEFAULT_MASK_SELECTORS = ["[data-guidely-mask]", ".guidely-mask"];
491
- var PREFERRED_ATTRS = ["data-testid", "data-test-id", "data-test", "data-cy", "data-qa", "data-guidely-track", "data-tour"];
492
- 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;
493
- function validSelectors(list) {
494
- const out = [];
495
- const probe = typeof document !== "undefined" ? document.createDocumentFragment() : null;
496
- for (const raw of list) {
497
- const s = (raw || "").trim();
498
- if (!s || out.includes(s)) continue;
499
- try {
500
- probe?.querySelector(s);
501
- out.push(s);
502
- } catch {
503
- }
504
- }
505
- return out;
506
- }
507
- function isIgnoredElement(el) {
508
- return (0, import_guide_runtime2.isGuidelyElement)(el) || !!el.closest("[data-guidely-ignore]");
509
- }
510
- function interactiveElement(target) {
511
- try {
512
- return target.closest(INTERACTIVE_SELECTOR);
513
- } catch {
514
- return null;
515
- }
516
- }
517
- function esc(value) {
518
- const g = globalThis;
519
- return g.CSS?.escape ? g.CSS.escape(value) : value.replace(/[^\w-]/g, (c) => `\\${c}`);
520
- }
521
- var attr = (tag, name, value) => value ? `${tag}[${name}="${value.replace(/["\\]/g, "\\$&")}"]` : `${tag}[${name}]`;
522
- function elementSelector(el) {
523
- const tag = el.tagName.toLowerCase();
524
- if (el.id && (0, import_guide_runtime2.isStableId)(el.id)) return `#${esc(el.id)}`;
525
- for (const name of PREFERRED_ATTRS) if (el.hasAttribute(name)) return attr(tag, name, el.getAttribute(name) || "").slice(0, 300);
526
- for (const a of Array.from(el.attributes)) {
527
- if (a.name.startsWith("data-") && !NOISY_DATA_ATTR.test(a.name) && a.value && a.value.length <= 60) return attr(tag, a.name, a.value).slice(0, 300);
528
- }
529
- const label = el.getAttribute("aria-label");
530
- if (label && label.length <= 80) return attr(tag, "aria-label", label).slice(0, 300);
531
- const parts = [];
532
- let cur = el;
533
- while (cur && parts.length < 4) {
534
- const t = cur.tagName.toLowerCase();
535
- if (t === "html" || t === "body") break;
536
- if (cur !== el && cur.id && (0, import_guide_runtime2.isStableId)(cur.id)) {
537
- parts.unshift(`#${esc(cur.id)}`);
538
- break;
539
- }
540
- const parent = cur.parentElement;
541
- const node = cur;
542
- const same = parent ? Array.from(parent.children).filter((c) => c.tagName === node.tagName) : [];
543
- parts.unshift(same.length > 1 ? `${t}:nth-of-type(${same.indexOf(node) + 1})` : t);
544
- cur = parent;
545
- }
546
- return (parts.join(" > ") || tag).slice(0, 300);
547
- }
548
- function matches(el, selector, deep) {
549
- if (!selector) return false;
550
- try {
551
- return !!el.closest(selector) || deep && !!el.querySelector(selector);
552
- } catch {
553
- return false;
554
- }
555
- }
556
- function elementText(el, mask, maskAll) {
557
- if (maskAll || matches(el, mask, true)) return void 0;
558
- const tag = el.tagName.toLowerCase();
559
- let text = "";
560
- if (tag === "input") {
561
- if (!/^(button|submit|reset)$/i.test(el.type || "")) return void 0;
562
- text = el.value;
563
- } else if (tag === "select" || tag === "textarea") return void 0;
564
- else text = el.innerText ?? el.textContent ?? "";
565
- text = (text || el.getAttribute("aria-label") || el.getAttribute("title") || "").replace(/\s+/g, " ").trim();
566
- return text ? text.slice(0, 100) : void 0;
567
- }
568
- function linkHref(el) {
569
- const a = el.closest("a[href]");
570
- if (!a) return void 0;
571
- try {
572
- const u = new URL(a.getAttribute("href") || "", location.href);
573
- if (!/^https?:$/.test(u.protocol)) return void 0;
574
- return u.origin === location.origin ? u.pathname : u.origin;
575
- } catch {
576
- return void 0;
577
- }
578
- }
579
- function describeClick(el, e, opts) {
580
- const props = { tag: el.tagName.toLowerCase(), selector: elementSelector(el), x: Math.round(e.pageX ?? 0) || 0, y: Math.round(e.pageY ?? 0) || 0 };
581
- const text = elementText(el, opts.mask, opts.maskAll);
582
- if (text) props.text = text;
583
- const href = opts.collectUrls ? linkHref(el) : void 0;
584
- if (href) props.href = href;
585
- return props;
586
- }
587
- var RageClickDetector = class {
588
- constructor(threshold = 3, windowMs = 1e3, radius = 30) {
589
- __publicField(this, "threshold", threshold);
590
- __publicField(this, "windowMs", windowMs);
591
- __publicField(this, "radius", radius);
592
- __publicField(this, "clicks", []);
593
- __publicField(this, "fired", false);
594
- }
595
- /** Clicks in the current burst once it has fired (rage), otherwise 0. */
596
- get burst() {
597
- return this.fired ? this.clicks.length : 0;
598
- }
599
- /** Records a click; returns the click count when it completes a burst, otherwise 0. */
600
- add(x, y, t) {
601
- const first = this.clicks[0];
602
- const last = this.clicks[this.clicks.length - 1];
603
- if (!first || !last || t - last.t > this.windowMs || Math.hypot(x - first.x, y - first.y) > this.radius) {
604
- this.clicks = [];
605
- this.fired = false;
1182
+ } catch {
1183
+ token = null;
1184
+ }
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);
606
1193
  }
607
- this.clicks = this.clicks.filter((c) => t - c.t <= this.windowMs);
608
- this.clicks.push({ x, y, t });
609
- if (this.fired || this.clicks.length < this.threshold) return 0;
610
- this.fired = true;
611
- return this.clicks.length;
1194
+ return null;
612
1195
  }
613
- };
1196
+ if (token && saved?.token !== token) sessionStore.remove(KEYS.capturePending);
1197
+ writeJson(sessionStore, KEYS.capture, marker);
1198
+ sessionStore.remove(KEYS.authoring);
1199
+ return marker;
1200
+ }
1201
+
1202
+ // src/load-capture.ts
1203
+ var loadCapture = async () => (await import("./capture.cjs")).capture;
614
1204
 
615
1205
  // src/context.ts
616
1206
  function detectDevice(ua, width) {
@@ -621,7 +1211,8 @@ function detectDevice(ua, width) {
621
1211
  else if (width > 0 && width < 768) type = "mobile";
622
1212
  else if (width > 0 && width < 1024) type = "tablet";
623
1213
  let browser;
624
- if (/Edg\//i.test(u)) browser = "edge";
1214
+ if (/Electron\//i.test(u)) browser = "electron";
1215
+ else if (/Edg\//i.test(u)) browser = "edge";
625
1216
  else if (/OPR\/|Opera/i.test(u)) browser = "opera";
626
1217
  else if (/SamsungBrowser/i.test(u)) browser = "samsung";
627
1218
  else if (/Chrome\/|CriOS/i.test(u)) browser = "chrome";
@@ -745,7 +1336,7 @@ var HeatmapBuffer = class {
745
1336
  };
746
1337
 
747
1338
  // src/errors.ts
748
- var import_runtime5 = require("@guidelytech/shared/runtime");
1339
+ var import_runtime11 = require("@guidelytech/shared/runtime");
749
1340
  var MAX_FRAMES_BYTES = 6e3;
750
1341
  function fitFrames(frames) {
751
1342
  const out = frames.slice(0, 30);
@@ -766,7 +1357,7 @@ function exceptionFromError(error, source, handled, fallbackType = "Error") {
766
1357
  return {
767
1358
  type: isError && e.name || fallbackType,
768
1359
  message: (isError ? e.message : describe(error)).slice(0, 1e3),
769
- frames: isError ? fitFrames((0, import_runtime5.parseStack)(e.stack)) : [],
1360
+ frames: isError ? fitFrames((0, import_runtime11.parseStack)(e.stack)) : [],
770
1361
  source,
771
1362
  handled
772
1363
  };
@@ -887,162 +1478,35 @@ function areMutuallyExclusive(a, b) {
887
1478
  return la.includes(b.id) || la.includes(b.key) || lb.includes(a.id) || lb.includes(a.key);
888
1479
  }
889
1480
  function isWithinSchedule(guide, now = Date.now()) {
890
- const s = guide.config.survey;
891
- if (!s) return true;
892
- if (s.startAt && now < Date.parse(s.startAt)) return false;
893
- if (s.endAt && now > Date.parse(s.endAt)) return false;
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;
1485
+ }
894
1486
  return true;
895
1487
  }
896
- function dismissedAfterMerge(local, server) {
897
- if (server.dismissResetAt && server.dismissResetAt !== local?.dismissResetAt) return server.dismissedAt ?? null;
898
- return local?.dismissedAt ?? server.dismissedAt ?? null;
899
- }
900
-
901
- // src/http.ts
902
- var Api = class {
903
- constructor(baseUrl, key, fetchImpl) {
904
- __publicField(this, "baseUrl", baseUrl);
905
- __publicField(this, "key", key);
906
- __publicField(this, "fetchImpl", fetchImpl);
907
- }
908
- url(path, params = {}) {
909
- const query = Object.entries({ key: this.key, ...params }).filter((e) => e[1] != null).map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join("&");
910
- return `${this.baseUrl.replace(/\/$/, "")}/api/v1/sdk${path}?${query}`;
911
- }
912
- get fetcher() {
913
- if (this.fetchImpl) return this.fetchImpl;
914
- return typeof fetch === "function" ? fetch.bind(globalThis) : null;
915
- }
916
- async request(method, path, options = {}) {
917
- const f = this.fetcher;
918
- if (!f) throw new Error("fetch is not available");
919
- const headers = { ...options.headers, "X-Guidely-Key": this.key, Accept: "application/json" };
920
- const form = typeof FormData !== "undefined" && options.body instanceof FormData;
921
- if (options.body !== void 0 && !form) headers["Content-Type"] = "application/json";
922
- if (options.etag) headers["If-None-Match"] = options.etag;
923
- const ac = typeof AbortController !== "undefined" ? new AbortController() : null;
924
- const timer = ac ? setTimeout(() => ac.abort(), options.timeoutMs ?? 1e4) : null;
925
- try {
926
- const res = await f(this.url(path, options.params), {
927
- method,
928
- headers,
929
- body: options.body === void 0 ? void 0 : form ? options.body : JSON.stringify(options.body),
930
- keepalive: options.keepalive,
931
- signal: ac?.signal,
932
- credentials: "omit",
933
- mode: "cors"
934
- });
935
- let data = null;
936
- if (res.status !== 304 && res.status !== 204) {
937
- try {
938
- data = await res.json();
939
- } catch {
940
- data = null;
941
- }
942
- }
943
- return { status: res.status, ok: res.ok, data, etag: res.headers?.get?.("etag") ?? null };
944
- } finally {
945
- if (timer) clearTimeout(timer);
946
- }
947
- }
948
- get(path, options) {
949
- return this.request("GET", path, options);
950
- }
951
- post(path, body, options) {
952
- return this.request("POST", path, { body, ...options });
953
- }
954
- /** Multipart POST (file uploads). No Content-Type header: the browser adds it with the boundary. */
955
- postForm(path, form, options) {
956
- return this.request("POST", path, { body: form, ...options });
957
- }
958
- /**
959
- * POST that may answer with server-sent events. Each event is passed to `onEvent`; a JSON answer is
960
- * returned instead. No timeout: Scout replies can take a while and the server keeps the stream alive.
961
- */
962
- async stream(path, body, onEvent, headers = {}) {
963
- const f = this.fetcher;
964
- if (!f) throw new Error("fetch is not available");
965
- const res = await f(this.url(path), {
966
- method: "POST",
967
- headers: { ...headers, "X-Guidely-Key": this.key, Accept: "text/event-stream, application/json", "Content-Type": "application/json" },
968
- body: JSON.stringify(body),
969
- credentials: "omit",
970
- mode: "cors"
971
- });
972
- const type = res.headers?.get?.("content-type") ?? "";
973
- if (!type.includes("text/event-stream") || !res.body) {
974
- let data = null;
975
- try {
976
- data = await res.json();
977
- } catch {
978
- data = null;
979
- }
980
- return { status: res.status, ok: res.ok, data, etag: null };
981
- }
982
- await readEventStream(res.body, onEvent);
983
- return { status: res.status, ok: res.ok, data: null, etag: null };
984
- }
985
- /** `navigator.sendBeacon` when available (page unload); returns false when it could not be queued. */
986
- beacon(path, body) {
987
- try {
988
- if (typeof navigator === "undefined" || typeof navigator.sendBeacon !== "function") return false;
989
- const blob = new Blob([JSON.stringify(body)], { type: "text/plain;charset=UTF-8" });
990
- return navigator.sendBeacon(this.url(path), blob);
991
- } catch {
992
- return false;
993
- }
994
- }
995
- };
996
- var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
997
- function backoffMs(attempt, baseMs = 1e3, maxMs = 3e4) {
998
- return Math.min(maxMs, baseMs * 2 ** Math.max(0, attempt));
1488
+ var MINUTE = 60 * 1e3;
1489
+ function countsTowardThrottle(guide) {
1490
+ return isExclusiveGuide(guide) && !guide.config.frequency.ignoreThrottling;
999
1491
  }
1000
- async function withRetry(send, attempts = 3, baseMs = 500) {
1001
- for (let i = 0; i < attempts; i++) {
1002
- try {
1003
- const res = await send();
1004
- if (res.ok || res.status >= 400 && res.status < 500 && res.status !== 429) return res;
1005
- } catch {
1006
- }
1007
- if (i < attempts - 1) await sleep(backoffMs(i, baseMs, 8e3));
1008
- }
1009
- return null;
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;
1010
1499
  }
1011
- function postWithRetry(api, path, body, attempts = 3, baseMs = 500) {
1012
- return withRetry(() => api.post(path, body), attempts, baseMs);
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);
1013
1506
  }
1014
- async function readEventStream(body, onEvent) {
1015
- const reader = body.getReader();
1016
- const decoder = new TextDecoder();
1017
- let buffer = "";
1018
- const dispatch = (raw) => {
1019
- let event = "message";
1020
- const data = [];
1021
- for (const line of raw.split("\n")) {
1022
- if (!line || line.startsWith(":")) continue;
1023
- const i = line.indexOf(":");
1024
- const field = i < 0 ? line : line.slice(0, i);
1025
- const value = i < 0 ? "" : line.slice(i + 1).replace(/^ /, "");
1026
- if (field === "event") event = value;
1027
- else if (field === "data") data.push(value);
1028
- }
1029
- if (!data.length) return;
1030
- try {
1031
- onEvent(event, JSON.parse(data.join("\n")));
1032
- } catch {
1033
- }
1034
- };
1035
- for (; ; ) {
1036
- const { done, value } = await reader.read();
1037
- if (value) buffer += decoder.decode(value, { stream: true }).replace(/\r\n?/g, "\n");
1038
- let sep;
1039
- while ((sep = buffer.indexOf("\n\n")) >= 0) {
1040
- dispatch(buffer.slice(0, sep));
1041
- buffer = buffer.slice(sep + 2);
1042
- }
1043
- if (done) break;
1044
- }
1045
- if (buffer.trim()) dispatch(buffer);
1507
+ function dismissedAfterMerge(local, server) {
1508
+ if (server.dismissResetAt && server.dismissResetAt !== local?.dismissResetAt) return server.dismissedAt ?? null;
1509
+ return local?.dismissedAt ?? server.dismissedAt ?? null;
1046
1510
  }
1047
1511
 
1048
1512
  // src/messenger-api.ts
@@ -1116,6 +1580,14 @@ function createMessengerTransport(opts) {
1116
1580
  h.onDone(d.messages.at(-1) ?? null, d.conversation);
1117
1581
  } else if (!finished) h.onError("The reply was interrupted");
1118
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
+ }
1119
1591
  return {
1120
1592
  async listConversations() {
1121
1593
  const tokens = storedConversations(store, projectId).map((c) => c.token);
@@ -1152,7 +1624,8 @@ function createMessengerTransport(opts) {
1152
1624
  return res.data;
1153
1625
  },
1154
1626
  async getArticle(id) {
1155
- 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);
1156
1629
  return res.ok && res.data ? res.data.article : null;
1157
1630
  },
1158
1631
  async fileComplaint(input) {
@@ -1182,13 +1655,25 @@ function createMessengerTransport(opts) {
1182
1655
  async getHowTo(id) {
1183
1656
  const res = await api.get(`/how-tos/${encodeURIComponent(id)}`);
1184
1657
  return res.ok && res.data ? res.data.howTo : null;
1185
- }
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
1186
1671
  };
1187
1672
  }
1188
1673
 
1189
1674
  // src/messenger-visitor.ts
1190
- var import_runtime10 = require("@guidelytech/shared/runtime");
1191
- 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");
1192
1677
  var PAGE_GUIDE_KINDS = /* @__PURE__ */ new Set(["flow", "tooltip", "modal"]);
1193
1678
  var CATCH_ALL = /* @__PURE__ */ new Set(["", "*", "**", "/*", "/**", "/"]);
1194
1679
  var RESUME_WITHIN_MS = 14 * 864e5;
@@ -1205,7 +1690,7 @@ function targetsPage(guide, url) {
1205
1690
  const c = guide.config;
1206
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] : []];
1207
1692
  const specific = checks.filter((p) => !CATCH_ALL.has(p.trim()));
1208
- 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));
1209
1694
  }
1210
1695
  function plainText(html, max = 280) {
1211
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();
@@ -1254,7 +1739,7 @@ function buildMessengerVisitor(input) {
1254
1739
  unread: !seen.has(g.id) && !(state.guides[g.id]?.impressions ?? 0)
1255
1740
  };
1256
1741
  });
1257
- return { firstName: (0, import_runtime10.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 };
1258
1743
  }
1259
1744
 
1260
1745
  // src/navigation.ts
@@ -1312,6 +1797,48 @@ function watchNavigation(listener, debounceMs = 50) {
1312
1797
  };
1313
1798
  }
1314
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
+
1315
1842
  // src/page-tracking.ts
1316
1843
  function scrollDepth() {
1317
1844
  const de = document.documentElement;
@@ -1357,6 +1884,106 @@ var PageTracker = class {
1357
1884
  }
1358
1885
  };
1359
1886
 
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;
1898
+ }
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;
1905
+ }
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;
1952
+ try {
1953
+ previous?.focus?.();
1954
+ } catch {
1955
+ }
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
+ }
1986
+
1360
1987
  // src/queue.ts
1361
1988
  var EventQueue = class {
1362
1989
  constructor(opts) {
@@ -1368,17 +1995,34 @@ var EventQueue = class {
1368
1995
  __publicField(this, "inflightIds", /* @__PURE__ */ new Set());
1369
1996
  __publicField(this, "attempt", 0);
1370
1997
  __publicField(this, "destroyed", false);
1998
+ __publicField(this, "offRecover", null);
1371
1999
  __publicField(this, "flushIntervalMs");
1372
2000
  __publicField(this, "maxQueue");
1373
2001
  __publicField(this, "batchSize");
1374
2002
  __publicField(this, "maxAttempts");
1375
2003
  this.flushIntervalMs = opts.flushIntervalMs ?? 2e3;
1376
- this.maxQueue = opts.maxQueue ?? 200;
2004
+ this.maxQueue = opts.maxQueue ?? 500;
1377
2005
  this.batchSize = opts.batchSize ?? 50;
1378
2006
  this.maxAttempts = opts.maxAttempts ?? 5;
1379
2007
  const persisted = readJson(opts.store, opts.storageKey, []);
1380
2008
  if (Array.isArray(persisted)) this.events = persisted.filter((e) => e && typeof e.eventId === "string").slice(-this.maxQueue);
1381
2009
  if (this.events.length) this.schedule();
2010
+ this.offRecover = opts.health?.onRecover(() => this.retryNow()) ?? null;
2011
+ }
2012
+ /** The API is back: drop any pending backoff and send what is queued. */
2013
+ retryNow() {
2014
+ if (this.destroyed) return;
2015
+ if (this.retryTimer) clearTimeout(this.retryTimer);
2016
+ this.retryTimer = null;
2017
+ this.attempt = 0;
2018
+ void this.flush();
2019
+ }
2020
+ retryIn(ms) {
2021
+ if (this.retryTimer || this.destroyed) return;
2022
+ this.retryTimer = setTimeout(() => {
2023
+ this.retryTimer = null;
2024
+ void this.flush();
2025
+ }, ms);
1382
2026
  }
1383
2027
  get size() {
1384
2028
  return this.events.length;
@@ -1412,6 +2056,8 @@ var EventQueue = class {
1412
2056
  clearTimeout(this.timer);
1413
2057
  this.timer = null;
1414
2058
  }
2059
+ const wait = this.opts.health?.waitMs() ?? 0;
2060
+ if (wait > 0) return this.retryIn(wait);
1415
2061
  this.inflight = true;
1416
2062
  const batch = this.events.slice(0, this.batchSize);
1417
2063
  this.inflightIds = new Set(batch.map((e) => e.eventId));
@@ -1435,26 +2081,20 @@ var EventQueue = class {
1435
2081
  if (this.events.length) this.schedule();
1436
2082
  return;
1437
2083
  }
1438
- this.attempt += 1;
1439
- if (this.attempt >= this.maxAttempts) {
1440
- this.attempt = 0;
1441
- return;
1442
- }
1443
- this.retryTimer = setTimeout(() => {
1444
- this.retryTimer = null;
1445
- void this.flush();
1446
- }, backoffMs(this.attempt - 1));
2084
+ this.attempt = Math.min(this.attempt + 1, this.maxAttempts);
2085
+ const delay = this.attempt >= this.maxAttempts ? 6e4 : backoffMs(this.attempt - 1);
2086
+ this.retryIn(Math.max(delay, this.opts.health?.waitMs() ?? 0));
1447
2087
  }
1448
2088
  /** Best-effort synchronous flush on unload using `sendBeacon`. */
1449
2089
  flushSync() {
1450
2090
  if (this.destroyed || !this.opts.enabled() || !this.opts.beacon) return;
1451
2091
  const batch = this.events.filter((e) => !this.inflightIds.has(e.eventId)).slice(0, this.batchSize);
1452
2092
  if (!batch.length) return;
1453
- if (this.opts.beacon(batch)) {
2093
+ if (this.opts.beacon(batch) && (this.opts.health?.reachable ?? true)) {
1454
2094
  const ids = new Set(batch.map((e) => e.eventId));
1455
2095
  this.events = this.events.filter((e) => !ids.has(e.eventId));
1456
- this.persist();
1457
2096
  }
2097
+ this.persist();
1458
2098
  }
1459
2099
  clear() {
1460
2100
  this.events = [];
@@ -1462,6 +2102,7 @@ var EventQueue = class {
1462
2102
  }
1463
2103
  destroy() {
1464
2104
  this.destroyed = true;
2105
+ this.offRecover?.();
1465
2106
  if (this.timer) clearTimeout(this.timer);
1466
2107
  if (this.retryTimer) clearTimeout(this.retryTimer);
1467
2108
  this.timer = null;
@@ -1475,11 +2116,22 @@ var loadRecorder = async () => (await import("./recorder.cjs")).recorder;
1475
2116
 
1476
2117
  // src/client.ts
1477
2118
  var SDK_NAME = "@guidelytech/sdk-core";
1478
- var SDK_VERSION = "0.9.0";
2119
+ var SDK_VERSION = "0.11.0";
1479
2120
  var DEFAULT_API_URL = "https://api.guidely.app";
1480
2121
  var PAGE_SCOPED = /* @__PURE__ */ new Set(["page_load", "url_match", "element_click", "element_visible", "delay"]);
1481
2122
  var DAY_MS = 24 * 60 * 60 * 1e3;
1482
2123
  var MAX_SESSION_EXCEPTIONS = 100;
2124
+ var CACHED_START_MS = 1500;
2125
+ function settlesWithin(promise, ms) {
2126
+ return new Promise((resolve) => {
2127
+ const timer = setTimeout(() => resolve(false), ms);
2128
+ const done = () => {
2129
+ clearTimeout(timer);
2130
+ resolve(true);
2131
+ };
2132
+ promise.then(done, done);
2133
+ });
2134
+ }
1483
2135
  function isBrowser() {
1484
2136
  return typeof window !== "undefined" && typeof document !== "undefined";
1485
2137
  }
@@ -1490,7 +2142,7 @@ function emptyState() {
1490
2142
  return { guides: {}, checklists: {}, surveysSubmitted: [], audienceIds: [] };
1491
2143
  }
1492
2144
  function newSession(now = Date.now()) {
1493
- return { id: `s_${(0, import_runtime13.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 };
1494
2146
  }
1495
2147
  var GuidelyClient = class _GuidelyClient {
1496
2148
  constructor(options) {
@@ -1500,6 +2152,10 @@ var GuidelyClient = class _GuidelyClient {
1500
2152
  __publicField(this, "sessionStore");
1501
2153
  __publicField(this, "emitter");
1502
2154
  __publicField(this, "queue");
2155
+ /** Survey answers and guide/checklist progress, kept until the server accepts them. */
2156
+ __publicField(this, "outbox");
2157
+ /** Agent Analytics turns (agentConversation), in their own outbox. */
2158
+ __publicField(this, "agentTurns");
1503
2159
  __publicField(this, "config", null);
1504
2160
  __publicField(this, "etag", null);
1505
2161
  __publicField(this, "anonymousId");
@@ -1549,6 +2205,10 @@ var GuidelyClient = class _GuidelyClient {
1549
2205
  __publicField(this, "flags");
1550
2206
  /** Experiments already reported this session (`experiment_viewed` is sent once per session). */
1551
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());
1552
2212
  __publicField(this, "customListeners", /* @__PURE__ */ new Map());
1553
2213
  __publicField(this, "eventBus", {
1554
2214
  on: (name, cb) => {
@@ -1583,7 +2243,7 @@ var GuidelyClient = class _GuidelyClient {
1583
2243
  this.sessionStore = this.options.storage === "memory" ? this.store : createStorage("session");
1584
2244
  this.anonymousId = this.store.get(KEYS.anon) || "";
1585
2245
  if (!this.anonymousId) {
1586
- this.anonymousId = `anon_${(0, import_runtime13.randomId)(20)}`;
2246
+ this.anonymousId = `anon_${(0, import_runtime18.randomId)(20)}`;
1587
2247
  this.store.set(KEYS.anon, this.anonymousId);
1588
2248
  }
1589
2249
  this.firstVisit = !this.store.get(KEYS.seen);
@@ -1593,12 +2253,13 @@ var GuidelyClient = class _GuidelyClient {
1593
2253
  const session = readJson(this.sessionStore, KEYS.session, null);
1594
2254
  this.session = session && session.id ? session : newSession();
1595
2255
  this.persistSession();
1596
- this.lastUrl = location.href;
2256
+ this.lastUrl = this.pageHref();
1597
2257
  this.lastAudiences = new Set(this.state.audienceIds);
1598
2258
  this.queue = new EventQueue({
1599
2259
  store: this.store,
1600
2260
  storageKey: KEYS.queue(this.options.projectId),
1601
2261
  enabled: () => this.analyticsEnabled(),
2262
+ health: this.api.health,
1602
2263
  send: async (events) => {
1603
2264
  const res = await this.api.post("/events", { events, sentAt: nowIso(), sdk: { name: SDK_NAME, version: SDK_VERSION } }, { keepalive: true });
1604
2265
  return { ok: res.ok, retry: !(res.status >= 400 && res.status < 500 && res.status !== 429) };
@@ -1606,6 +2267,8 @@ var GuidelyClient = class _GuidelyClient {
1606
2267
  beacon: (events) => this.api.beacon("/events", { events, sentAt: nowIso(), sdk: { name: SDK_NAME, version: SDK_VERSION } }),
1607
2268
  onError: (err) => this.reportError(err)
1608
2269
  });
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 } });
1609
2272
  if (options.user) this.user = { ...options.user };
1610
2273
  this.flags = this.createFlags(options.flags);
1611
2274
  }
@@ -1663,12 +2326,15 @@ var GuidelyClient = class _GuidelyClient {
1663
2326
  const cached = readJson(this.store, KEYS.config(this.options.projectId), null);
1664
2327
  this.etag = this.store.get(KEYS.etag(this.options.projectId));
1665
2328
  if (cached && cached.guides) this.config = webOnly(cached);
1666
- await this.fetchConfig();
2329
+ const before = this.config?.configVersion;
2330
+ const fresh = this.fetchConfig();
2331
+ if (!this.config) await fresh;
2332
+ else if (!await settlesWithin(fresh, CACHED_START_MS)) void fresh.then(() => this.applyConfigChange(before));
1667
2333
  if (this.destroyed) return;
1668
2334
  this.wireListeners();
1669
2335
  if (!this.identitySettled) {
1670
- if (this.options.user) await this.identify(this.options.user.id, this.options.user.traits, this.options.user.account);
1671
- else if (this.user?.id) await this.identify(this.user.id, this.user.traits, this.user.account);
2336
+ const known = this.options.user ?? (this.user?.id ? this.user : null);
2337
+ if (known) await settlesWithin(this.identify(known.id, known.traits, known.account), CACHED_START_MS);
1672
2338
  }
1673
2339
  if (this.destroyed) return;
1674
2340
  this.isReady = true;
@@ -1679,7 +2345,7 @@ var GuidelyClient = class _GuidelyClient {
1679
2345
  this.resumeActiveGuide();
1680
2346
  this.evaluate("ready");
1681
2347
  if (!this.options.disableAuthoring && !capturing) this.maybeStartAuthoring();
1682
- this.pageTracker.start(location.href);
2348
+ this.pageTracker.start(this.pageHref());
1683
2349
  this.touchSession();
1684
2350
  this.syncCapture();
1685
2351
  } catch (err) {
@@ -1712,7 +2378,7 @@ var GuidelyClient = class _GuidelyClient {
1712
2378
  wireListeners() {
1713
2379
  const doc = document;
1714
2380
  const win = window;
1715
- this.cleanups.push(watchNavigation((url) => this.handleNavigation(url)));
2381
+ this.cleanups.push(watchNavigation((url) => this.handleNavigation(this.pageHref(url))));
1716
2382
  const onClick = (e) => this.handleDocumentClick(e);
1717
2383
  doc.addEventListener("click", onClick, true);
1718
2384
  this.cleanups.push(() => doc.removeEventListener("click", onClick, true));
@@ -1733,7 +2399,7 @@ var GuidelyClient = class _GuidelyClient {
1733
2399
  const onPageShow = (e) => {
1734
2400
  if (!e.persisted || !this.pageLeft) return;
1735
2401
  this.pageLeft = false;
1736
- this.pageTracker.start(location.href);
2402
+ this.pageTracker.start(this.pageHref());
1737
2403
  };
1738
2404
  let lastScroll = 0;
1739
2405
  const onScroll = () => {
@@ -1773,11 +2439,16 @@ var GuidelyClient = class _GuidelyClient {
1773
2439
  win.removeEventListener("scroll", onScroll);
1774
2440
  doc.removeEventListener("keydown", onKey, true);
1775
2441
  });
1776
- const refresh = setInterval(() => void this.refresh(), this.options.configRefreshMs);
2442
+ const refresh = setInterval(() => {
2443
+ if (this.api.health.canSend()) void this.refresh();
2444
+ }, this.options.configRefreshMs);
2445
+ const onOnline = () => this.api.health.online();
2446
+ win.addEventListener("online", onOnline);
2447
+ this.cleanups.push(() => win.removeEventListener("online", onOnline));
1777
2448
  this.cleanups.push(() => clearInterval(refresh));
1778
2449
  const poll = setInterval(() => {
1779
2450
  if (!this.isReady || !this.config || this.editing() || doc.visibilityState === "hidden") return;
1780
- const url = location.href;
2451
+ const url = this.pageHref();
1781
2452
  const ctx = this.getContext();
1782
2453
  for (const guide of this.config.guides) {
1783
2454
  if (guide.config.trigger.type === "element_visible" && guide.config.trigger.value) this.armVisibility(guide, ctx, url);
@@ -1806,6 +2477,11 @@ var GuidelyClient = class _GuidelyClient {
1806
2477
  if (this.destroyed) return;
1807
2478
  const before = this.config?.configVersion;
1808
2479
  await this.fetchConfig();
2480
+ this.applyConfigChange(before);
2481
+ }
2482
+ /** Re-renders after a config change (refresh, or a config that arrived after start). */
2483
+ applyConfigChange(before) {
2484
+ if (this.destroyed || !this.isReady) return;
1809
2485
  if (this.config && this.config.configVersion !== before) {
1810
2486
  this.renderPersistentWidgets();
1811
2487
  this.evaluate("ready");
@@ -1840,10 +2516,12 @@ var GuidelyClient = class _GuidelyClient {
1840
2516
  this.replay = null;
1841
2517
  this.queue.flushSync();
1842
2518
  this.queue.destroy();
2519
+ this.outbox.destroy();
2520
+ this.agentTurns.destroy();
1843
2521
  this.flags.destroy();
1844
2522
  this.emitter.clear();
1845
- (0, import_guide_runtime4.destroyHost)();
1846
- if (current === this) current = null;
2523
+ (0, import_guide_runtime5.destroyHost)();
2524
+ if (current2 === this) current2 = null;
1847
2525
  }
1848
2526
  // ---------------------------------------------------------------------------
1849
2527
  // Identity
@@ -1855,13 +2533,13 @@ var GuidelyClient = class _GuidelyClient {
1855
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) };
1856
2534
  writeJson(this.store, KEYS.user(this.options.projectId), this.user);
1857
2535
  this.syncFlagAttributes();
1858
- this.enqueue("identify", { properties: { traits: (0, import_runtime13.sanitizeProperties)(this.user.traits) } });
2536
+ this.enqueue("identify", { properties: { traits: (0, import_runtime18.sanitizeProperties)(this.user.traits) } });
1859
2537
  await this.syncIdentify();
1860
2538
  this.emitter.emit("identify", { userId: this.user.id, anonymousId: this.anonymousId });
1861
2539
  if (this.isReady) this.evaluate("identify");
1862
2540
  }
1863
2541
  async syncIdentify() {
1864
- 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 };
1865
2543
  const res = await postWithRetry(this.api, "/identify", body, 2);
1866
2544
  if (res?.ok && res.data) this.mergeServerState(res.data);
1867
2545
  }
@@ -1892,7 +2570,10 @@ var GuidelyClient = class _GuidelyClient {
1892
2570
  checklists,
1893
2571
  surveysSubmitted: Array.from(/* @__PURE__ */ new Set([...this.state.surveysSubmitted, ...data.surveysSubmitted || []])),
1894
2572
  audienceIds: data.audienceIds || [],
1895
- 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 } : {}
1896
2577
  };
1897
2578
  if (data.firstSeenAt && Date.now() - Date.parse(data.firstSeenAt) > 6e4) this.firstVisit = false;
1898
2579
  this.persistState();
@@ -1928,7 +2609,7 @@ var GuidelyClient = class _GuidelyClient {
1928
2609
  this.stopGuide();
1929
2610
  this.user = null;
1930
2611
  this.store.remove(KEYS.user(this.options.projectId));
1931
- this.anonymousId = `anon_${(0, import_runtime13.randomId)(20)}`;
2612
+ this.anonymousId = `anon_${(0, import_runtime18.randomId)(20)}`;
1932
2613
  this.store.set(KEYS.anon, this.anonymousId);
1933
2614
  this.state = emptyState();
1934
2615
  this.persistState();
@@ -1960,6 +2641,7 @@ var GuidelyClient = class _GuidelyClient {
1960
2641
  if (consent === "denied") this.queue.clear();
1961
2642
  else if (consent === "granted") void this.queue.flush();
1962
2643
  if (consent !== "granted") this.replay?.stop({ discard: true });
2644
+ if (consent !== "pending") this.agentTurns.consentChanged(consent === "granted");
1963
2645
  this.syncCapture();
1964
2646
  }
1965
2647
  analyticsEnabled() {
@@ -1972,17 +2654,19 @@ var GuidelyClient = class _GuidelyClient {
1972
2654
  if (this.consent === "denied" || !this.options.analytics) return null;
1973
2655
  if (name !== "page_leave" && name !== "heatmap") this.touchSession();
1974
2656
  const event = {
1975
- eventId: (0, import_runtime13.randomId)(24),
2657
+ eventId: (0, import_runtime18.randomId)(24),
1976
2658
  name,
1977
2659
  occurredAt: nowIso(),
1978
2660
  anonymousId: this.anonymousId,
1979
2661
  userId: this.user?.id || null,
1980
2662
  accountId: this.user?.account?.id || null,
1981
2663
  sessionId: this.session.id,
1982
- url: this.collectUrls() ? location.href : void 0,
2664
+ url: this.collectUrls() ? this.pageHref() : void 0,
1983
2665
  ...extra
1984
2666
  };
1985
- if (extra.properties) event.properties = (0, import_runtime13.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 };
1986
2670
  this.queue.enqueue(event);
1987
2671
  this.replay?.addCustomEvent("guidely", { name, customName: event.customName, guideId: event.guideId });
1988
2672
  this.emitter.emit("track", event);
@@ -2006,9 +2690,30 @@ var GuidelyClient = class _GuidelyClient {
2006
2690
  this.completeChecklistTasksBy("event", name);
2007
2691
  if (this.isReady) this.evaluate("event", { eventName: name });
2008
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
+ }
2009
2713
  /** Records a page view and re-evaluates triggers. Called automatically on SPA navigation. */
2010
- page(url = location.href) {
2714
+ page(href) {
2011
2715
  if (this.destroyed) return;
2716
+ const url = this.pageHref(href);
2012
2717
  this.leavePage(url);
2013
2718
  this.trackPageView(url);
2014
2719
  this.handleNavigation(url, true);
@@ -2028,13 +2733,13 @@ var GuidelyClient = class _GuidelyClient {
2028
2733
  const st = this.active.controller.getState();
2029
2734
  if (st.status === "running" && !this.active.manual) {
2030
2735
  const g = this.active.guide;
2031
- 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();
2032
2737
  }
2033
2738
  }
2034
2739
  this.widgets.forEach((w, id) => {
2035
2740
  if (!w.controller || w.manual) return;
2036
2741
  const g = this.findGuide(id);
2037
- 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();
2038
2743
  });
2039
2744
  this.firedDelays.clear();
2040
2745
  if (this.isReady) this.evaluate("navigation");
@@ -2042,7 +2747,7 @@ var GuidelyClient = class _GuidelyClient {
2042
2747
  if (changed) void this.queue.flush();
2043
2748
  }
2044
2749
  handleDocumentClick(e) {
2045
- const target = e.target;
2750
+ const target = (0, import_guide_runtime5.composedTarget)(e);
2046
2751
  if (!target || typeof target.closest !== "function") return;
2047
2752
  this.autocaptureClick(e, target);
2048
2753
  this.completeChecklistTasksBy("click", target);
@@ -2100,6 +2805,8 @@ var GuidelyClient = class _GuidelyClient {
2100
2805
  const ref = !this.session.pageViews ? externalReferrer() : void 0;
2101
2806
  if (ref) props.referrer = ref;
2102
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;
2103
2810
  this.session.pageViews = (this.session.pageViews ?? 0) + 1;
2104
2811
  this.persistSession();
2105
2812
  this.enqueue("page_view", { url: collect ? url : void 0, properties: props });
@@ -2125,21 +2832,28 @@ var GuidelyClient = class _GuidelyClient {
2125
2832
  const heat = this.autocaptureOn("heatmaps");
2126
2833
  if (!clicks && !rage && !heat || isIgnoredElement(target)) return;
2127
2834
  this.touchSession();
2128
- 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;
2129
2838
  const opts = { mask: this.maskSelector(), maskAll: this.maskAllText(), collectUrls: this.collectUrls() };
2130
- 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) });
2131
2845
  const count = this.rage.add(e.pageX ?? 0, e.pageY ?? 0, Date.now());
2132
- 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 } });
2133
2847
  if (!heat) return;
2134
- const url = location.href;
2848
+ const url = this.pageHref();
2135
2849
  if (this.heatmap.url !== url) this.flushHeatmap();
2136
2850
  if (this.heatmap.add(url, heatmapPoint(el ?? target, e, this.rage.burst > 0), this.rage.burst)) this.flushHeatmap();
2137
2851
  }
2138
2852
  /** Bins one pointer sample into the heatmap buffer. Not session activity. */
2139
2853
  sampleMove(e) {
2140
- const target = e.target;
2854
+ const target = (0, import_guide_runtime5.composedTarget)(e);
2141
2855
  if (document.visibilityState === "hidden" || !target?.closest || !this.autocaptureOn("mouseMovement") || isIgnoredElement(target)) return;
2142
- const url = location.href;
2856
+ const url = this.pageHref();
2143
2857
  if (this.heatmap.url !== url) this.flushHeatmap();
2144
2858
  if (this.heatmap.move(url, movementBin(interactiveElement(target) ?? target, e))) this.flushHeatmap();
2145
2859
  }
@@ -2215,7 +2929,7 @@ var GuidelyClient = class _GuidelyClient {
2215
2929
  if (!this.isReady || this.destroyed) return;
2216
2930
  if (this.config && !this.initialPageViewSent && this.config.settings?.autocapture) {
2217
2931
  this.initialPageViewSent = true;
2218
- this.trackPageView(location.href);
2932
+ this.trackPageView(this.pageHref());
2219
2933
  }
2220
2934
  if (this.config) {
2221
2935
  for (const [props, extra] of this.earlyExceptions.splice(0)) {
@@ -2228,7 +2942,7 @@ var GuidelyClient = class _GuidelyClient {
2228
2942
  syncReplay() {
2229
2943
  const cfg = this.isReady && this.captureAllowed() ? this.replayConfig() : null;
2230
2944
  if (cfg && this.session.replay === void 0) {
2231
- this.session.replay = (0, import_runtime13.rolloutBucket)(this.session.id, "replay") < cfg.sampleRate;
2945
+ this.session.replay = (0, import_runtime18.rolloutBucket)(this.session.id, "replay") < cfg.sampleRate;
2232
2946
  this.persistSession();
2233
2947
  }
2234
2948
  if (!cfg || !this.session.replay) {
@@ -2250,7 +2964,7 @@ var GuidelyClient = class _GuidelyClient {
2250
2964
  return {
2251
2965
  anonymousId: this.anonymousId,
2252
2966
  userId: this.user?.id || null,
2253
- url: this.collectUrls() ? location.href : void 0,
2967
+ url: this.collectUrls() ? this.pageHref() : void 0,
2254
2968
  device: { type: device.type, browser: device.browser, os: device.os },
2255
2969
  viewport: { width: window.innerWidth, height: window.innerHeight },
2256
2970
  sdk: { name: SDK_NAME, version: SDK_VERSION }
@@ -2262,7 +2976,9 @@ var GuidelyClient = class _GuidelyClient {
2262
2976
  this.persistSession();
2263
2977
  this.replay?.stop({ discard: true });
2264
2978
  },
2265
- onError: (err) => this.reportError(err)
2979
+ onError: (err) => this.reportError(err),
2980
+ health: this.api.health,
2981
+ stash: this.sessionStore
2266
2982
  });
2267
2983
  this.syncReplay();
2268
2984
  },
@@ -2299,6 +3015,14 @@ var GuidelyClient = class _GuidelyClient {
2299
3015
  // ---------------------------------------------------------------------------
2300
3016
  // Targeting + triggers
2301
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
+ }
2302
3026
  /** Builds the targeting context for the current moment. */
2303
3027
  getContext() {
2304
3028
  const guides = this.state.guides;
@@ -2309,7 +3033,7 @@ var GuidelyClient = class _GuidelyClient {
2309
3033
  userId: this.user?.id || null,
2310
3034
  userAttributes: this.user?.traits ?? {},
2311
3035
  accountAttributes: this.user?.account?.traits ?? {},
2312
- url: location.href,
3036
+ url: this.pageHref(),
2313
3037
  firstVisit: this.firstVisit,
2314
3038
  eventsSeen: this.session.events,
2315
3039
  completedGuides: keysOf(Object.keys(guides).filter((id) => guides[id]?.completedAt)),
@@ -2318,17 +3042,64 @@ var GuidelyClient = class _GuidelyClient {
2318
3042
  completedChecklists: keysOf(completedChecklists),
2319
3043
  audienceIds: this.state.audienceIds,
2320
3044
  environment: this.options.environment,
2321
- platform: "web"
3045
+ platform: "web",
3046
+ ...this.options.appVersion ? { appVersion: this.options.appVersion } : {}
2322
3047
  });
2323
3048
  }
2324
3049
  isTargeted(guide, ctx, url) {
2325
3050
  const c = guide.config;
2326
- if (c.urlPatterns.length && !c.urlPatterns.some((p) => (0, import_guide_runtime4.urlMatches)(p, url))) return false;
2327
- 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;
2328
3053
  if (!isWithinSchedule(guide)) return false;
2329
3054
  if (guide.kind === "survey" && (this.state.surveysSubmitted.includes(guide.id) || this.state.surveysSubmitted.includes(guide.key))) return false;
2330
3055
  ctx.rolloutSalt = guide.id;
2331
- return (0, import_runtime13.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
+ }
2332
3103
  }
2333
3104
  isEligible(guide, ctx, url) {
2334
3105
  if (!this.isTargeted(guide, ctx, url)) return false;
@@ -2346,13 +3117,13 @@ var GuidelyClient = class _GuidelyClient {
2346
3117
  if (t.delayMs > 0) return reason === "timer" && detail.guideId === guide.id;
2347
3118
  return reason === "ready" || reason === "navigation" || reason === "identify";
2348
3119
  case "url_match":
2349
- 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());
2350
3121
  case "event":
2351
3122
  return reason === "event" && !!t.value && detail.eventName === t.value;
2352
3123
  case "element_click":
2353
3124
  if (reason !== "click" || !t.value || !detail.target) return false;
2354
3125
  try {
2355
- return !!detail.target.closest(t.value);
3126
+ return !!(0, import_guide_runtime5.closestComposed)(detail.target, t.value);
2356
3127
  } catch {
2357
3128
  return false;
2358
3129
  }
@@ -2371,15 +3142,15 @@ var GuidelyClient = class _GuidelyClient {
2371
3142
  }
2372
3143
  evaluate(reason, detail = {}) {
2373
3144
  if (!this.config || this.destroyed || this.editing()) return;
2374
- const url = location.href;
3145
+ const url = this.pageHref();
2375
3146
  const ctx = this.getContext();
2376
3147
  if (reason === "identify") {
2377
- const current2 = new Set(this.state.audienceIds);
2378
- detail.newAudiences = new Set(Array.from(current2).filter((a) => !this.lastAudiences.has(a)));
2379
- 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;
2380
3151
  }
2381
3152
  const candidates = [];
2382
- for (const guide of this.config.guides) {
3153
+ for (const guide of this.candidates()) {
2383
3154
  const t = guide.config.trigger;
2384
3155
  if (detail.endedGuide && detail.endedGuide.id === guide.id) continue;
2385
3156
  if (reason === "ready" || reason === "navigation") {
@@ -2395,6 +3166,10 @@ var GuidelyClient = class _GuidelyClient {
2395
3166
  if (t.type === "page_load" && t.delayMs > 0 && reason === "timer" && detail.guideId !== guide.id) continue;
2396
3167
  if (!this.triggerFires(guide, reason, detail)) continue;
2397
3168
  if (!this.isEligible(guide, ctx, url)) continue;
3169
+ if (!this.throttleAllows(guide)) {
3170
+ this.log("throttled", guide.key);
3171
+ continue;
3172
+ }
2398
3173
  candidates.push(guide);
2399
3174
  }
2400
3175
  if (reason === "after_guide" || reason === "navigation") this.drainQueue(url, ctx);
@@ -2422,7 +3197,7 @@ var GuidelyClient = class _GuidelyClient {
2422
3197
  this.visibilityObservers.delete(guide.id);
2423
3198
  };
2424
3199
  this.visibilityObservers.set(guide.id, stop);
2425
- (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) => {
2426
3201
  if (ac.signal.aborted) return;
2427
3202
  const fire = () => {
2428
3203
  stop();
@@ -2445,6 +3220,13 @@ var GuidelyClient = class _GuidelyClient {
2445
3220
  /** Decides whether a guide can show now, otherwise queues it. */
2446
3221
  tryShow(guide, url, manual) {
2447
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
+ }
2448
3230
  if (!isExclusiveGuide(guide)) {
2449
3231
  if (this.widgets.has(guide.id)) return false;
2450
3232
  this.show(guide, manual);
@@ -2485,14 +3267,18 @@ var GuidelyClient = class _GuidelyClient {
2485
3267
  guideState(id) {
2486
3268
  return this.state.guides[id] ?? (this.state.guides[id] = { impressions: 0 });
2487
3269
  }
2488
- recordImpression(guide) {
3270
+ recordImpression(guide, manual = false) {
2489
3271
  const s = this.guideState(guide.id);
2490
3272
  s.impressions += 1;
2491
3273
  s.lastShownAt = nowIso();
2492
3274
  if (!this.session.shownGuides.includes(guide.id)) this.session.shownGuides.push(guide.id);
3275
+ this.recordLaunch(guide, manual);
2493
3276
  this.persistState();
2494
3277
  this.persistSession();
2495
- 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 });
2496
3282
  }
2497
3283
  recordEnd(guide, reason) {
2498
3284
  const s = this.guideState(guide.id);
@@ -2503,7 +3289,9 @@ var GuidelyClient = class _GuidelyClient {
2503
3289
  if (reason === "completed") this.completeChecklistTasksBy("guide_completed", guide);
2504
3290
  }
2505
3291
  syncGuideState(guideId, update) {
2506
- void postWithRetry(this.api, "/guide-state", { anonymousId: this.anonymousId, userId: this.user?.id || null, updates: [{ guideId, ...update }] });
3292
+ const body = { anonymousId: this.anonymousId, userId: this.user?.id || null, updates: [{ guideId, ...update }] };
3293
+ if (update.impression) void postWithRetry(this.api, "/guide-state", body);
3294
+ else this.outbox.add(`guide:${guideId}:${update.completed ? "completed" : "dismissed"}:${this.anonymousId}`, "/guide-state", body);
2507
3295
  }
2508
3296
  show(guide, manual, stepId) {
2509
3297
  try {
@@ -2514,13 +3302,17 @@ var GuidelyClient = class _GuidelyClient {
2514
3302
  this.reportError(err);
2515
3303
  }
2516
3304
  }
2517
- showFlow(guide, manual, stepId) {
3305
+ showFlow(raw, manual, stepId) {
2518
3306
  if (this.editing()) return;
3307
+ const guide = this.localize(raw);
2519
3308
  const exclusive = isExclusiveGuide(guide);
2520
- const entry = { guide, manual, startUrl: location.href };
2521
- 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, {
2522
3311
  theme: this.theme(),
3312
+ // Code blocks only arrive when a workspace admin allowed them (the server strips them otherwise).
3313
+ runCode: true,
2523
3314
  ctx: () => this.getContext(),
3315
+ currentUrl: () => this.pageHref(),
2524
3316
  startAtStep: stepId,
2525
3317
  eventBus: this.eventBus,
2526
3318
  // Started from Help, Scout or startGuide(): steps on other pages offer to take the user there.
@@ -2534,7 +3326,7 @@ var GuidelyClient = class _GuidelyClient {
2534
3326
  const step = e.stepId ? guide.config.steps.find((s) => s.id === e.stepId) : void 0;
2535
3327
  this.enqueue(e.name, { guideId: guide.id, guideVersion: guide.version, stepId: e.stepId, stepIndex: e.stepIndex, properties: e.properties });
2536
3328
  if (e.name === "guide_started") {
2537
- this.recordImpression(guide);
3329
+ this.recordImpression(guide, manual);
2538
3330
  this.emitter.emit("guide:start", guide);
2539
3331
  }
2540
3332
  if (e.name === "step_viewed" && step) {
@@ -2581,14 +3373,15 @@ var GuidelyClient = class _GuidelyClient {
2581
3373
  this.widgets.delete(guide.id);
2582
3374
  }
2583
3375
  }
2584
- showChecklist(guide) {
3376
+ showChecklist(raw) {
2585
3377
  if (this.editing()) return;
3378
+ const guide = this.localize(raw);
2586
3379
  const config = guide.config.checklist;
2587
3380
  if (!config || this.widgets.has(guide.id)) return;
2588
- 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);
2589
3382
  const progress = this.getChecklistProgress(guide.id);
2590
3383
  const first = !this.state.guides[guide.id]?.impressions;
2591
- const widget = (0, import_guide_runtime4.renderChecklist)({
3384
+ const widget = (0, import_guide_runtime5.renderChecklist)({
2592
3385
  scope: scope.el,
2593
3386
  config,
2594
3387
  progress,
@@ -2603,12 +3396,13 @@ var GuidelyClient = class _GuidelyClient {
2603
3396
  }, checklist: widget, scope });
2604
3397
  if (first) this.recordImpression(guide);
2605
3398
  }
2606
- showSurvey(guide, manual) {
3399
+ showSurvey(raw, manual) {
2607
3400
  if (this.editing()) return;
3401
+ const guide = this.localize(raw);
2608
3402
  const config = guide.config.survey;
2609
3403
  if (!config) return;
2610
- const scope = (0, import_guide_runtime5.createScope)(this.theme(), guide.config.appearance);
2611
- 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 };
2612
3406
  const finish = (reason) => {
2613
3407
  widget.destroy();
2614
3408
  scope.destroy();
@@ -2620,18 +3414,18 @@ var GuidelyClient = class _GuidelyClient {
2620
3414
  }
2621
3415
  this.setTimer(() => this.evaluate("after_guide", { endedGuide: guide }), 0);
2622
3416
  };
2623
- const widget = (0, import_guide_runtime4.renderSurvey)({
3417
+ const widget = (0, import_guide_runtime5.renderSurvey)({
2624
3418
  scope: scope.el,
2625
3419
  config,
2626
3420
  title: guide.name,
2627
3421
  onDismiss: () => finish(this.state.surveysSubmitted.includes(guide.id) ? "submitted" : "dismissed"),
2628
3422
  onSubmit: (answers) => {
2629
- const responseId = (0, import_runtime13.randomId)(24);
3423
+ const responseId = (0, import_runtime18.randomId)(24);
2630
3424
  this.state.surveysSubmitted.push(guide.id);
2631
3425
  this.persistState();
2632
3426
  this.recordEnd(guide, "completed");
2633
3427
  this.enqueue("survey_submitted", { guideId: guide.id, guideVersion: guide.version, properties: { responseId } });
2634
- void postWithRetry(this.api, `/surveys/${encodeURIComponent(guide.id)}/responses`, { anonymousId: this.anonymousId, userId: this.user?.id || null, accountId: this.user?.account?.id || null, guideVersion: guide.version, answers, responseId });
3428
+ this.outbox.add(`survey:${responseId}`, `/surveys/${encodeURIComponent(guide.id)}/responses`, { anonymousId: this.anonymousId, userId: this.user?.id || null, accountId: this.user?.account?.id || null, guideVersion: guide.version, answers, responseId });
2635
3429
  this.emitter.emit("survey:submit", { guide, answers, responseId });
2636
3430
  this.emitter.emit("guide:complete", guide);
2637
3431
  widget.showThankYou();
@@ -2639,7 +3433,7 @@ var GuidelyClient = class _GuidelyClient {
2639
3433
  });
2640
3434
  entry.survey = widget;
2641
3435
  this.active = entry;
2642
- this.recordImpression(guide);
3436
+ this.recordImpression(guide, manual);
2643
3437
  this.enqueue("survey_displayed", { guideId: guide.id, guideVersion: guide.version });
2644
3438
  this.emitter.emit("guide:start", guide);
2645
3439
  }
@@ -2648,6 +3442,7 @@ var GuidelyClient = class _GuidelyClient {
2648
3442
  }
2649
3443
  navigate(url, newTab) {
2650
3444
  try {
3445
+ if (this.openPrototypeTest(url)) return;
2651
3446
  if (newTab) window.open(url, "_blank", "noopener");
2652
3447
  else location.assign(url);
2653
3448
  } catch (err) {
@@ -2668,11 +3463,17 @@ var GuidelyClient = class _GuidelyClient {
2668
3463
  }
2669
3464
  this.destroyMessenger();
2670
3465
  if (rc && rc.enabled && !this.resourceCenter) {
2671
- const scope = (0, import_guide_runtime5.createScope)(this.theme());
3466
+ const scope = (0, import_guide_runtime6.createScope)(this.theme());
2672
3467
  this.resourceCenterScope = scope;
2673
- this.resourceCenter = (0, import_guide_runtime4.renderResourceCenter)({
3468
+ this.resourceCenter = (0, import_guide_runtime5.renderResourceCenter)({
2674
3469
  scope: scope.el,
2675
- 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
+ },
2676
3477
  guides: this.config.guides.map((g) => ({ id: g.id, key: g.key, name: g.name, kind: g.kind })),
2677
3478
  search: (q) => this.searchResourceCenter(q),
2678
3479
  audienceIds: () => this.state.audienceIds,
@@ -2706,7 +3507,7 @@ var GuidelyClient = class _GuidelyClient {
2706
3507
  this.messengerLoading = import("@guidelytech/guide-runtime/messenger").then(({ renderMessenger }) => {
2707
3508
  const cfg = this.config?.messenger;
2708
3509
  if (this.destroyed || !this.config || !cfg?.enabled || this.messenger) return this.messenger;
2709
- const scope = (0, import_guide_runtime5.createScope)(this.theme());
3510
+ const scope = (0, import_guide_runtime6.createScope)(this.theme());
2710
3511
  this.messengerScope = scope;
2711
3512
  this.messenger = renderMessenger({
2712
3513
  scope: scope.el,
@@ -2717,7 +3518,8 @@ var GuidelyClient = class _GuidelyClient {
2717
3518
  projectId: this.options.projectId,
2718
3519
  anonymousId: () => this.anonymousId,
2719
3520
  userId: () => this.user?.id || null,
2720
- pageUrl: () => this.collectUrls() ? location.href : void 0
3521
+ pageUrl: () => this.collectUrls() ? this.pageHref() : void 0,
3522
+ locale: () => this.rcLocale()
2721
3523
  }),
2722
3524
  visitor: () => this.messengerVisitor(),
2723
3525
  brand: { logoUrl: this.theme().logoUrl ?? null, name: this.theme().brandName ?? null },
@@ -2726,7 +3528,8 @@ var GuidelyClient = class _GuidelyClient {
2726
3528
  const seen = readJson(this.store, KEYS.newsSeen(this.options.projectId), []);
2727
3529
  writeJson(this.store, KEYS.newsSeen(this.options.projectId), Array.from(/* @__PURE__ */ new Set([...ids, ...Array.isArray(seen) ? seen : []])).slice(0, 200));
2728
3530
  },
2729
- 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()),
2730
3533
  audienceIds: () => this.state.audienceIds,
2731
3534
  guides: this.config.guides.map((g) => ({ id: g.id, key: g.key, name: g.name, kind: g.kind })),
2732
3535
  onStartGuide: (idOrKey) => this.startGuide(idOrKey),
@@ -2750,7 +3553,7 @@ var GuidelyClient = class _GuidelyClient {
2750
3553
  /** What the messenger personalises from, computed fresh on every render. */
2751
3554
  messengerVisitor() {
2752
3555
  const ctx = this.getContext();
2753
- const url = location.href;
3556
+ const url = this.pageHref();
2754
3557
  const seen = readJson(this.store, KEYS.newsSeen(this.options.projectId), []);
2755
3558
  const visitor = buildMessengerVisitor({
2756
3559
  guides: this.config?.guides ?? [],
@@ -2761,11 +3564,24 @@ var GuidelyClient = class _GuidelyClient {
2761
3564
  isAudienceTargeted: (g) => {
2762
3565
  if (!isWithinSchedule(g)) return false;
2763
3566
  ctx.rolloutSalt = g.id;
2764
- return (0, import_runtime13.evaluateRules)(g.config.targeting, ctx);
3567
+ return (0, import_runtime18.evaluateRules)(g.config.targeting, ctx);
2765
3568
  },
2766
3569
  newsSeen: Array.isArray(seen) ? seen : []
2767
3570
  });
2768
- 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 };
2769
3585
  }
2770
3586
  destroyMessenger() {
2771
3587
  this.messenger?.destroy();
@@ -2773,9 +3589,17 @@ var GuidelyClient = class _GuidelyClient {
2773
3589
  this.messenger = null;
2774
3590
  this.messengerScope = null;
2775
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
+ }
2776
3599
  async loadArticle(id) {
2777
3600
  try {
2778
- 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);
2779
3603
  return res.ok && res.data ? res.data.article : null;
2780
3604
  } catch {
2781
3605
  return null;
@@ -2813,14 +3637,14 @@ var GuidelyClient = class _GuidelyClient {
2813
3637
  if (progress.completedTaskIds.includes(task.id)) return;
2814
3638
  progress.completedTaskIds.push(task.id);
2815
3639
  this.enqueue("checklist_task_completed", { guideId: guide.id, guideVersion: guide.version, properties: { taskId: task.id } });
2816
- const complete = (0, import_guide_runtime4.isChecklistComplete)(config, progress) && !progress.completedAt;
3640
+ const complete = (0, import_guide_runtime5.isChecklistComplete)(config, progress) && !progress.completedAt;
2817
3641
  if (complete) {
2818
3642
  progress.completedAt = nowIso();
2819
3643
  this.enqueue("checklist_completed", { guideId: guide.id, guideVersion: guide.version });
2820
3644
  }
2821
3645
  this.persistState();
2822
3646
  this.widgets.get(guide.id)?.checklist?.setProgress(this.getChecklistProgress(guide.id));
2823
- void postWithRetry(this.api, `/checklists/${encodeURIComponent(guide.id)}/progress`, { anonymousId: this.anonymousId, userId: this.user?.id || null, guideVersion: guide.version, completedTaskIds: progress.completedTaskIds, completed: !!progress.completedAt });
3647
+ this.outbox.add(`checklist:${guide.id}:progress:${this.anonymousId}`, `/checklists/${encodeURIComponent(guide.id)}/progress`, { anonymousId: this.anonymousId, userId: this.user?.id || null, guideVersion: guide.version, completedTaskIds: progress.completedTaskIds, completed: !!progress.completedAt });
2824
3648
  this.emitter.emit("checklist:task", { guide, task, progress: this.getChecklistProgress(guide.id) });
2825
3649
  if (complete) {
2826
3650
  this.emitter.emit("checklist:complete", guide);
@@ -2836,11 +3660,11 @@ var GuidelyClient = class _GuidelyClient {
2836
3660
  if (task.completion !== kind || !task.completionValue) continue;
2837
3661
  let hit = false;
2838
3662
  if (kind === "event") hit = value === task.completionValue;
2839
- 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);
2840
3664
  else if (kind === "guide_completed") hit = value.id === task.completionValue || value.key === task.completionValue;
2841
3665
  else if (kind === "click") {
2842
3666
  try {
2843
- hit = !!value.closest(task.completionValue);
3667
+ hit = !!(0, import_guide_runtime5.closestComposed)(value, task.completionValue);
2844
3668
  } catch {
2845
3669
  hit = false;
2846
3670
  }
@@ -2860,7 +3684,7 @@ var GuidelyClient = class _GuidelyClient {
2860
3684
  progress.dismissedAt = nowIso();
2861
3685
  this.persistState();
2862
3686
  this.enqueue("checklist_dismissed", { guideId: guide.id, guideVersion: guide.version });
2863
- void postWithRetry(this.api, `/checklists/${encodeURIComponent(guide.id)}/progress`, { anonymousId: this.anonymousId, userId: this.user?.id || null, guideVersion: guide.version, completedTaskIds: progress.completedTaskIds, dismissed: true });
3687
+ this.outbox.add(`checklist:${guide.id}:dismissed:${this.anonymousId}`, `/checklists/${encodeURIComponent(guide.id)}/progress`, { anonymousId: this.anonymousId, userId: this.user?.id || null, guideVersion: guide.version, completedTaskIds: progress.completedTaskIds, dismissed: true });
2864
3688
  this.widgets.get(guide.id)?.destroy();
2865
3689
  this.widgets.delete(guide.id);
2866
3690
  }
@@ -2951,6 +3775,18 @@ var GuidelyClient = class _GuidelyClient {
2951
3775
  closeMessenger() {
2952
3776
  this.messenger?.close();
2953
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
+ }
2954
3790
  /**
2955
3791
  * Opens the complaint form in the messenger, optionally with a category picked (its id from
2956
3792
  * Complaints → Categories). Does nothing when the project doesn't take complaints.
@@ -2959,6 +3795,14 @@ var GuidelyClient = class _GuidelyClient {
2959
3795
  if (!this.config?.messenger?.enabled || !this.config.messenger.complaints?.categories.length) return;
2960
3796
  void this.loadMessenger().then((m) => m?.openComplaint(categoryId));
2961
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
+ }
2962
3806
  /** Opens a new messenger conversation, optionally pre-filling the message. */
2963
3807
  showNewMessage(text) {
2964
3808
  if (!this.config?.messenger?.enabled) return;
@@ -2988,14 +3832,15 @@ var GuidelyClient = class _GuidelyClient {
2988
3832
  const device = detectDevice(navigator.userAgent, window.innerWidth);
2989
3833
  const account = this.user?.account;
2990
3834
  return {
2991
- url: location.href,
2992
- path: location.pathname,
3835
+ url: this.pageHref(),
3836
+ path: pathOf2(this.pageHref()),
2993
3837
  deviceType: device.type,
2994
3838
  browser: device.browser,
2995
3839
  os: device.os,
2996
3840
  locale: navigator.language || void 0,
2997
3841
  environment: this.options.environment,
2998
3842
  platform: "web",
3843
+ ...this.options.appVersion ? { appVersion: this.options.appVersion } : {},
2999
3844
  ...this.user?.traits ?? {},
3000
3845
  id: this.user?.id || this.anonymousId,
3001
3846
  anonymousId: this.anonymousId,
@@ -3155,20 +4000,20 @@ var GuidelyClient = class _GuidelyClient {
3155
4000
  // ---------------------------------------------------------------------------
3156
4001
  /** Creates (or returns the existing) client and starts initialisation. */
3157
4002
  static init(options) {
3158
- if (current && !current.destroyed && current.options.projectId === options.projectId) return current;
3159
- current?.destroy();
3160
- current = new _GuidelyClient(options);
3161
- void current.init();
3162
- 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;
3163
4008
  }
3164
4009
  /** The most recently created client, if any. */
3165
4010
  static get() {
3166
- return current && !current.destroyed ? current : null;
4011
+ return current2 && !current2.destroyed ? current2 : null;
3167
4012
  }
3168
4013
  };
3169
- var current = null;
4014
+ var current2 = null;
3170
4015
  function webOnly(config) {
3171
- return config.guides.every((g) => (0, import_runtime13.supportsPlatform)(g.config, "web")) ? config : { ...config, guides: config.guides.filter((g) => (0, import_runtime13.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")) };
3172
4017
  }
3173
4018
  function pathOf2(url) {
3174
4019
  try {
@@ -3188,7 +4033,7 @@ function externalReferrer() {
3188
4033
  }
3189
4034
  function createGuidely(options) {
3190
4035
  const client = new GuidelyClient(options);
3191
- current = client;
4036
+ current2 = client;
3192
4037
  void client.init();
3193
4038
  return client;
3194
4039
  }
@@ -3211,17 +4056,24 @@ var index_default = Guidely;
3211
4056
  GuidelyClient,
3212
4057
  HEARTBEAT_MS,
3213
4058
  KEYS,
4059
+ PROTOTYPE_CLOSE_MESSAGE,
3214
4060
  SDK_NAME,
3215
4061
  SDK_VERSION,
3216
4062
  areMutuallyExclusive,
3217
4063
  buildContext,
3218
4064
  captureSnapshot,
4065
+ countsTowardThrottle,
3219
4066
  createGuidely,
3220
4067
  createStorage,
3221
4068
  detectDevice,
4069
+ isAppUrl,
3222
4070
  isExclusiveGuide,
3223
4071
  isFrequencyEligible,
4072
+ isThrottleEligible,
3224
4073
  isWithinSchedule,
4074
+ matchPrototypeTestUrl,
4075
+ normalizeAppUrl,
4076
+ pruneLaunches,
3225
4077
  watchNavigation
3226
4078
  });
3227
4079
  //# sourceMappingURL=index.cjs.map