@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.js CHANGED
@@ -1,27 +1,31 @@
1
1
  import {
2
2
  Emitter,
3
3
  EventQueue,
4
+ Outbox,
4
5
  areMutuallyExclusive,
6
+ countsTowardThrottle,
5
7
  dismissedAfterMerge,
6
8
  isExclusiveGuide,
7
9
  isFrequencyEligible,
8
- isWithinSchedule
9
- } from "./chunk-544KSOC3.js";
10
+ isThrottleEligible,
11
+ isWithinSchedule,
12
+ pruneLaunches
13
+ } from "./chunk-ZAF2KRJA.js";
10
14
  import {
15
+ Api,
11
16
  KEYS,
17
+ __publicField,
12
18
  createStorage,
19
+ postWithRetry,
13
20
  readJson,
14
21
  writeJson
15
- } from "./chunk-CYAG6ZYX.js";
16
- import {
17
- Api,
18
- __publicField,
19
- postWithRetry
20
- } from "./chunk-AKNE7D5M.js";
22
+ } from "./chunk-JUSM7J5N.js";
21
23
 
22
24
  // src/client.ts
23
- import { evaluateRules, randomId, rolloutBucket, sanitizeProperties, supportsPlatform } from "@guidelytech/shared/runtime";
25
+ import { evaluateRules, localizeGuide, localizeResourceCenter, normalizeLocale, randomId as randomId2, rolloutBucket, sanitizeProperties, supportsPlatform, visibleAnnouncements, visitorLocales } from "@guidelytech/shared/runtime";
24
26
  import {
27
+ closestComposed as closestComposed4,
28
+ composedTarget,
25
29
  destroyHost,
26
30
  isChecklistComplete,
27
31
  renderChecklist,
@@ -34,8 +38,82 @@ import {
34
38
  import { createScope } from "@guidelytech/guide-runtime";
35
39
  import { createFlagsClient } from "@guidelytech/flags";
36
40
 
41
+ // src/agent-turns.ts
42
+ import { AGENT_TOOL_CALLS_PER_TURN, AGENT_TURN_MAX_TEXT, AGENT_TURNS_PER_BATCH, randomId } from "@guidelytech/shared/runtime";
43
+ var PENDING_MAX = 500;
44
+ function toolCalls(calls) {
45
+ if (!Array.isArray(calls)) return void 0;
46
+ return calls.filter((c) => !!c && typeof c.name === "string" && !!c.name).slice(0, AGENT_TOOL_CALLS_PER_TURN).map((c) => ({
47
+ name: c.name.slice(0, 100),
48
+ ...c.status === "ok" || c.status === "error" ? { status: c.status } : {},
49
+ ...typeof c.latencyMs === "number" && Number.isFinite(c.latencyMs) && c.latencyMs >= 0 ? { latencyMs: Math.round(c.latencyMs) } : {}
50
+ }));
51
+ }
52
+ function prepareAgentTurn(turn, identity, redact, onError) {
53
+ if (!turn || typeof turn !== "object" || !turn.conversationId || !turn.agentId || !turn.role) return null;
54
+ let out = {
55
+ ...turn,
56
+ type: "agent.conversation",
57
+ turnId: turn.turnId || `t_${randomId(20)}`,
58
+ occurredAt: turn.occurredAt || (/* @__PURE__ */ new Date()).toISOString(),
59
+ anonymousId: turn.anonymousId ?? identity.anonymousId,
60
+ userId: turn.userId !== void 0 ? turn.userId : identity.userId,
61
+ accountId: turn.accountId !== void 0 ? turn.accountId : identity.accountId,
62
+ sessionId: turn.sessionId ?? identity.sessionId
63
+ };
64
+ if (redact) {
65
+ try {
66
+ const r = redact({ ...out, toolCalls: out.toolCalls ? out.toolCalls.map((c) => ({ ...c })) : void 0, metadata: out.metadata ? { ...out.metadata } : void 0 });
67
+ if (!r) return null;
68
+ out = { ...r, type: "agent.conversation", conversationId: out.conversationId, agentId: out.agentId, turnId: out.turnId, role: out.role };
69
+ } catch (err) {
70
+ onError?.(err);
71
+ return null;
72
+ }
73
+ }
74
+ if (typeof out.text === "string" && out.text.length > AGENT_TURN_MAX_TEXT) out.text = out.text.slice(0, AGENT_TURN_MAX_TEXT);
75
+ if (out.text !== void 0 && typeof out.text !== "string") out.text = String(out.text);
76
+ const calls = toolCalls(out.toolCalls);
77
+ if (calls) out.toolCalls = calls;
78
+ else delete out.toolCalls;
79
+ return out;
80
+ }
81
+ var AgentTurns = class {
82
+ constructor(opts) {
83
+ __publicField(this, "opts", opts);
84
+ __publicField(this, "outbox");
85
+ __publicField(this, "pending", []);
86
+ this.outbox = new Outbox({ api: opts.api, store: opts.store, storageKey: opts.storageKey, health: opts.health, maxItems: 20, maxAgeMs: 24 * 60 * 60 * 1e3 });
87
+ }
88
+ get queued() {
89
+ return this.outbox.size + this.pending.length;
90
+ }
91
+ /** Queues turns to send now, or (`hold`) until consent is granted. */
92
+ add(turns, hold = false) {
93
+ if (!turns.length) return;
94
+ if (hold) {
95
+ this.pending.push(...turns);
96
+ if (this.pending.length > PENDING_MAX) this.pending.splice(0, this.pending.length - PENDING_MAX);
97
+ return;
98
+ }
99
+ for (let i = 0; i < turns.length; i += AGENT_TURNS_PER_BATCH) {
100
+ const batch = turns.slice(i, i + AGENT_TURNS_PER_BATCH);
101
+ this.outbox.add(`agent:${batch[0].turnId}`, "/agent-events", { turns: batch, sdk: this.opts.sdk });
102
+ }
103
+ }
104
+ /** Consent changed: granted sends what was held, anything else drops it. */
105
+ consentChanged(granted) {
106
+ const held = this.pending.splice(0);
107
+ if (granted) this.add(held);
108
+ }
109
+ destroy() {
110
+ this.pending = [];
111
+ this.outbox.destroy();
112
+ }
113
+ };
114
+
37
115
  // src/authoring.ts
38
- import { highlightSelectors, previewStep, runGuide, startAuthoring } from "@guidelytech/guide-runtime";
116
+ import { closestComposed as closestComposed3, highlightSelectors, previewStep, runGuide, startAuthoring } from "@guidelytech/guide-runtime";
39
117
 
40
118
  // src/snapshot.ts
41
119
  var MAX_SNAPSHOT_BYTES = 6 * 1024 * 1024;
@@ -113,6 +191,276 @@ ${clone.outerHTML}`;
113
191
  return { html, url: win.location.href, title: doc.title, width: win.innerWidth, height: win.innerHeight, capturedAt: (/* @__PURE__ */ new Date()).toISOString() };
114
192
  }
115
193
 
194
+ // src/tagging.ts
195
+ import { NOISY_DATA_ATTR as NOISY_DATA_ATTR2, SNAPSHOT_LIMITS, TAG_PROPERTY_LIMITS, featureRuleMatchesClick, pageRuleMatches, pageRulesMatch } from "@guidelytech/shared/runtime";
196
+ import { closestComposed as closestComposed2, isStableClass, isStableId as isStableId2, querySelectorStrategy } from "@guidelytech/guide-runtime";
197
+
198
+ // src/autocapture.ts
199
+ import { closestComposed, isGuidelyElement, isStableId, SHADOW_SEPARATOR, shadowHostChain } from "@guidelytech/guide-runtime";
200
+ 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]";
201
+ var DEFAULT_MASK_SELECTORS = ["[data-guidely-mask]", ".guidely-mask"];
202
+ var PREFERRED_ATTRS = ["data-testid", "data-test-id", "data-test", "data-cy", "data-qa", "data-guidely-track", "data-tour"];
203
+ 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;
204
+ function validSelectors(list) {
205
+ const out = [];
206
+ const probe = typeof document !== "undefined" ? document.createDocumentFragment() : null;
207
+ for (const raw of list) {
208
+ const s = (raw || "").trim();
209
+ if (!s || out.includes(s)) continue;
210
+ try {
211
+ probe?.querySelector(s);
212
+ out.push(s);
213
+ } catch {
214
+ }
215
+ }
216
+ return out;
217
+ }
218
+ function isIgnoredElement(el) {
219
+ return isGuidelyElement(el) || !!closestComposed(el, "[data-guidely-ignore]");
220
+ }
221
+ function interactiveElement(target) {
222
+ try {
223
+ return closestComposed(target, INTERACTIVE_SELECTOR);
224
+ } catch {
225
+ return null;
226
+ }
227
+ }
228
+ function esc(value) {
229
+ const g = globalThis;
230
+ return g.CSS?.escape ? g.CSS.escape(value) : value.replace(/[^\w-]/g, (c) => `\\${c}`);
231
+ }
232
+ var attr = (tag, name, value) => value ? `${tag}[${name}="${value.replace(/["\\]/g, "\\$&")}"]` : `${tag}[${name}]`;
233
+ function elementSelector(el) {
234
+ const own = ownSelector(el);
235
+ const chain = shadowHostChain(el);
236
+ return (chain ? chain + SHADOW_SEPARATOR + own : own).slice(0, 300);
237
+ }
238
+ function ownSelector(el) {
239
+ const tag = el.tagName.toLowerCase();
240
+ if (el.id && isStableId(el.id)) return `#${esc(el.id)}`;
241
+ for (const name of PREFERRED_ATTRS) if (el.hasAttribute(name)) return attr(tag, name, el.getAttribute(name) || "");
242
+ for (const a of Array.from(el.attributes)) {
243
+ if (a.name.startsWith("data-") && !NOISY_DATA_ATTR.test(a.name) && a.value && a.value.length <= 60) return attr(tag, a.name, a.value);
244
+ }
245
+ const label = el.getAttribute("aria-label");
246
+ if (label && label.length <= 80) return attr(tag, "aria-label", label);
247
+ const parts = [];
248
+ let cur = el;
249
+ while (cur && parts.length < 4) {
250
+ const t = cur.tagName.toLowerCase();
251
+ if (t === "html" || t === "body") break;
252
+ if (cur !== el && cur.id && isStableId(cur.id)) {
253
+ parts.unshift(`#${esc(cur.id)}`);
254
+ break;
255
+ }
256
+ const parent = cur.parentElement;
257
+ const node = cur;
258
+ const same = parent ? Array.from(parent.children).filter((c) => c.tagName === node.tagName) : [];
259
+ parts.unshift(same.length > 1 ? `${t}:nth-of-type(${same.indexOf(node) + 1})` : t);
260
+ cur = parent;
261
+ }
262
+ return parts.join(" > ") || tag;
263
+ }
264
+ function matches(el, selector, deep) {
265
+ if (!selector) return false;
266
+ try {
267
+ return !!closestComposed(el, selector) || deep && !!el.querySelector(selector);
268
+ } catch {
269
+ return false;
270
+ }
271
+ }
272
+ function elementText(el, mask, maskAll) {
273
+ if (maskAll || matches(el, mask, true)) return void 0;
274
+ const tag = el.tagName.toLowerCase();
275
+ let text = "";
276
+ if (tag === "input") {
277
+ if (!/^(button|submit|reset)$/i.test(el.type || "")) return void 0;
278
+ text = el.value;
279
+ } else if (tag === "select" || tag === "textarea") return void 0;
280
+ else text = el.innerText ?? el.textContent ?? "";
281
+ text = (text || el.getAttribute("aria-label") || el.getAttribute("title") || "").replace(/\s+/g, " ").trim();
282
+ return text ? text.slice(0, 100) : void 0;
283
+ }
284
+ function linkHref(el) {
285
+ const a = closestComposed(el, "a[href]");
286
+ if (!a) return void 0;
287
+ try {
288
+ const u = new URL(a.getAttribute("href") || "", location.href);
289
+ if (!/^https?:$/.test(u.protocol)) return void 0;
290
+ return u.origin === location.origin ? u.pathname : u.origin;
291
+ } catch {
292
+ return void 0;
293
+ }
294
+ }
295
+ function describeClick(el, e, opts) {
296
+ const props = { tag: el.tagName.toLowerCase(), selector: elementSelector(el), x: Math.round(e.pageX ?? 0) || 0, y: Math.round(e.pageY ?? 0) || 0 };
297
+ const text = elementText(el, opts.mask, opts.maskAll);
298
+ if (text) props.text = text;
299
+ const href = opts.collectUrls ? linkHref(el) : void 0;
300
+ if (href) props.href = href;
301
+ return props;
302
+ }
303
+ var RageClickDetector = class {
304
+ constructor(threshold = 3, windowMs = 1e3, radius = 30) {
305
+ __publicField(this, "threshold", threshold);
306
+ __publicField(this, "windowMs", windowMs);
307
+ __publicField(this, "radius", radius);
308
+ __publicField(this, "clicks", []);
309
+ __publicField(this, "fired", false);
310
+ }
311
+ /** Clicks in the current burst once it has fired (rage), otherwise 0. */
312
+ get burst() {
313
+ return this.fired ? this.clicks.length : 0;
314
+ }
315
+ /** Records a click; returns the click count when it completes a burst, otherwise 0. */
316
+ add(x, y, t) {
317
+ const first = this.clicks[0];
318
+ const last = this.clicks[this.clicks.length - 1];
319
+ if (!first || !last || t - last.t > this.windowMs || Math.hypot(x - first.x, y - first.y) > this.radius) {
320
+ this.clicks = [];
321
+ this.fired = false;
322
+ }
323
+ this.clicks = this.clicks.filter((c) => t - c.t <= this.windowMs);
324
+ this.clicks.push({ x, y, t });
325
+ if (this.fired || this.clicks.length < this.threshold) return 0;
326
+ this.fired = true;
327
+ return this.clicks.length;
328
+ }
329
+ };
330
+
331
+ // src/tagging.ts
332
+ var TEXT_ATTRS = /* @__PURE__ */ new Set(["aria-label", "title", "alt"]);
333
+ var SAFE_ATTRS = ["role", "name", "type", "aria-label", "title", "alt", "for"];
334
+ 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;
335
+ function nthOfType(el) {
336
+ const parent = el.parentElement;
337
+ if (!parent) return void 0;
338
+ let n = 0;
339
+ for (const c of Array.from(parent.children)) {
340
+ if (c.tagName === el.tagName) n++;
341
+ if (c === el) return n;
342
+ }
343
+ return void 0;
344
+ }
345
+ function nodeOf(el, masked, featureAttribute) {
346
+ const node = { t: el.tagName.toLowerCase().slice(0, SNAPSHOT_LIMITS.tag) };
347
+ if (el.id && isStableId2(el.id) && el.id.length <= SNAPSHOT_LIMITS.id) node.id = el.id;
348
+ const classes = Array.from(el.classList).filter((c) => isStableClass(c) && !STATE_CLASS.test(c) && c.length <= SNAPSHOT_LIMITS.className);
349
+ if (classes.length) node.c = classes.slice(0, SNAPSHOT_LIMITS.classes);
350
+ const a = {};
351
+ const put = (name, value) => {
352
+ if (value === null || Object.keys(a).length >= SNAPSHOT_LIMITS.attrs || name in a) return;
353
+ if (masked && TEXT_ATTRS.has(name)) return;
354
+ a[name] = value.slice(0, SNAPSHOT_LIMITS.attrValue);
355
+ };
356
+ if (featureAttribute && el.hasAttribute(featureAttribute)) put(featureAttribute, el.getAttribute(featureAttribute));
357
+ for (const attr2 of Array.from(el.attributes)) {
358
+ if (attr2.name.startsWith("data-") && !NOISY_DATA_ATTR2.test(attr2.name) && attr2.value.length <= SNAPSHOT_LIMITS.attrValue) put(attr2.name, attr2.value);
359
+ }
360
+ for (const name of SAFE_ATTRS) if (el.hasAttribute(name)) put(name, el.getAttribute(name));
361
+ if (Object.keys(a).length) node.a = a;
362
+ const n = nthOfType(el);
363
+ if (n) node.n = n;
364
+ return node;
365
+ }
366
+ function elementSnapshot(el, opts) {
367
+ const masked = opts.maskAll || isMasked(el, opts.mask);
368
+ const snap = nodeOf(el, masked, opts.featureAttribute);
369
+ const p = [];
370
+ for (let cur = el.parentElement; cur && p.length < SNAPSHOT_LIMITS.ancestors; cur = cur.parentElement) {
371
+ if (cur.tagName === "BODY" || cur.tagName === "HTML") break;
372
+ p.push(nodeOf(cur, masked, opts.featureAttribute));
373
+ }
374
+ if (p.length) snap.p = p;
375
+ const h = [];
376
+ let root = el.getRootNode?.();
377
+ while (root && root.nodeType === 11 && root.host && h.length < SNAPSHOT_LIMITS.hosts) {
378
+ h.push(nodeOf(root.host, masked, opts.featureAttribute));
379
+ root = root.host.getRootNode?.();
380
+ }
381
+ if (h.length) snap.h = h;
382
+ return snap;
383
+ }
384
+ function isMasked(el, mask) {
385
+ if (!mask) return false;
386
+ try {
387
+ return !!closestComposed2(el, mask);
388
+ } catch {
389
+ return false;
390
+ }
391
+ }
392
+ function featureAttributeOf(target, attribute) {
393
+ if (!attribute) return null;
394
+ try {
395
+ const el = closestComposed2(target, `[${attribute}]`);
396
+ const value = el?.getAttribute(attribute)?.replace(/\s+/g, " ").trim();
397
+ return el && value ? { el, value: value.slice(0, 100) } : null;
398
+ } catch {
399
+ return null;
400
+ }
401
+ }
402
+ function readProperty(def, base, opts) {
403
+ let el = base;
404
+ if (def.selector) {
405
+ try {
406
+ el = querySelectorStrategy({ kind: "css", value: def.selector }, document)[0] ?? null;
407
+ } catch {
408
+ el = null;
409
+ }
410
+ }
411
+ if (!el) return void 0;
412
+ if (def.source === "attribute") {
413
+ const name = (def.attribute ?? "").toLowerCase();
414
+ if (!name || name === "value" || name.startsWith("on")) return void 0;
415
+ if (opts.maskAll || isMasked(el, opts.mask)) return void 0;
416
+ const v = el.getAttribute(name);
417
+ return v ? v.replace(/\s+/g, " ").trim().slice(0, TAG_PROPERTY_LIMITS.value) || void 0 : void 0;
418
+ }
419
+ return elementText(el, opts.mask, opts.maskAll);
420
+ }
421
+ function clickPropertyValues(config, el, click, opts) {
422
+ if (!config?.properties.length) return void 0;
423
+ const out = {};
424
+ const css = (selector) => {
425
+ try {
426
+ return !!closestComposed2(el, selector);
427
+ } catch {
428
+ return false;
429
+ }
430
+ };
431
+ const here = { url: location.href, path: location.pathname, title: document.title };
432
+ for (const tag of config.properties) {
433
+ if (tag.kind !== "feature" || !tag.featureRules) continue;
434
+ const rules = tag.featureRules;
435
+ const matches2 = (r) => featureRuleMatchesClick(r, click, css) && (!r.page?.pattern || pageRuleMatches({ type: "url", value: r.page.pattern }, here));
436
+ if (!rules.some((r) => !r.exclude && matches2(r)) || rules.some((r) => r.exclude && matches2(r))) continue;
437
+ for (const def of tag.properties) {
438
+ const v = readProperty(def, el, opts);
439
+ if (v) out[def.id] = v;
440
+ }
441
+ }
442
+ return Object.keys(out).length ? out : void 0;
443
+ }
444
+ function pagePropertyValues(config, url, opts) {
445
+ if (!config?.properties.length) return void 0;
446
+ const out = {};
447
+ let path = "/";
448
+ try {
449
+ path = new URL(url, location.href).pathname;
450
+ } catch {
451
+ }
452
+ const ctx = { url, path, title: document.title };
453
+ for (const tag of config.properties) {
454
+ if (tag.kind !== "page" || !tag.pageRules || !pageRulesMatch(tag.pageRules, ctx)) continue;
455
+ for (const def of tag.properties) {
456
+ if (!def.selector) continue;
457
+ const v = readProperty(def, null, opts);
458
+ if (v) out[def.id] = v;
459
+ }
460
+ }
461
+ return Object.keys(out).length ? out : void 0;
462
+ }
463
+
116
464
  // src/authoring.ts
117
465
  async function loadSchemas() {
118
466
  const m = await import("@guidelytech/shared");
@@ -183,6 +531,8 @@ var AuthoringBridge = class {
183
531
  this.opts.onActive(true);
184
532
  this.session = startAuthoring({
185
533
  dashboardOrigin: this.info.dashboardOrigin,
534
+ // Tag mode (Pages & Features): picks become feature rules; the page itself is tagged from the dashboard.
535
+ 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,
186
536
  onPick: (pick) => this.sendPick(pick),
187
537
  onExit: () => this.stop()
188
538
  });
@@ -199,7 +549,7 @@ var AuthoringBridge = class {
199
549
  masked(el) {
200
550
  return this.opts.maskSelectors.some((sel) => {
201
551
  try {
202
- return !!el.closest(sel);
552
+ return !!closestComposed3(el, sel);
203
553
  } catch {
204
554
  return false;
205
555
  }
@@ -210,6 +560,7 @@ var AuthoringBridge = class {
210
560
  const { element, ...rest } = raw;
211
561
  const pick = this.masked(element) ? { ...rest, textSnapshot: "", selectors: rest.selectors.filter((s) => s.kind !== "text") } : rest;
212
562
  pick.url = window.location.href;
563
+ if (this.info.purpose === "tag") pick.elementSnapshot = elementSnapshot(element, { mask: this.opts.maskSelectors.join(","), maskAll: false, featureAttribute: null });
213
564
  this.post({ type: "guidely:pick", sessionId: this.info.id, pick });
214
565
  void this.opts.api.post("/authoring/session/picks", pick, { params: { token: this.token } }).catch((err) => this.opts.onError(err));
215
566
  }
@@ -379,134 +730,6 @@ function takeCaptureToken(sessionStore) {
379
730
  // src/load-capture.ts
380
731
  var loadCapture = async () => (await import("./capture.js")).capture;
381
732
 
382
- // src/autocapture.ts
383
- import { isGuidelyElement, isStableId } from "@guidelytech/guide-runtime";
384
- 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]";
385
- var DEFAULT_MASK_SELECTORS = ["[data-guidely-mask]", ".guidely-mask"];
386
- var PREFERRED_ATTRS = ["data-testid", "data-test-id", "data-test", "data-cy", "data-qa", "data-guidely-track", "data-tour"];
387
- 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;
388
- function validSelectors(list) {
389
- const out = [];
390
- const probe = typeof document !== "undefined" ? document.createDocumentFragment() : null;
391
- for (const raw of list) {
392
- const s = (raw || "").trim();
393
- if (!s || out.includes(s)) continue;
394
- try {
395
- probe?.querySelector(s);
396
- out.push(s);
397
- } catch {
398
- }
399
- }
400
- return out;
401
- }
402
- function isIgnoredElement(el) {
403
- return isGuidelyElement(el) || !!el.closest("[data-guidely-ignore]");
404
- }
405
- function interactiveElement(target) {
406
- try {
407
- return target.closest(INTERACTIVE_SELECTOR);
408
- } catch {
409
- return null;
410
- }
411
- }
412
- function esc(value) {
413
- const g = globalThis;
414
- return g.CSS?.escape ? g.CSS.escape(value) : value.replace(/[^\w-]/g, (c) => `\\${c}`);
415
- }
416
- var attr = (tag, name, value) => value ? `${tag}[${name}="${value.replace(/["\\]/g, "\\$&")}"]` : `${tag}[${name}]`;
417
- function elementSelector(el) {
418
- const tag = el.tagName.toLowerCase();
419
- if (el.id && isStableId(el.id)) return `#${esc(el.id)}`;
420
- for (const name of PREFERRED_ATTRS) if (el.hasAttribute(name)) return attr(tag, name, el.getAttribute(name) || "").slice(0, 300);
421
- for (const a of Array.from(el.attributes)) {
422
- 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);
423
- }
424
- const label = el.getAttribute("aria-label");
425
- if (label && label.length <= 80) return attr(tag, "aria-label", label).slice(0, 300);
426
- const parts = [];
427
- let cur = el;
428
- while (cur && parts.length < 4) {
429
- const t = cur.tagName.toLowerCase();
430
- if (t === "html" || t === "body") break;
431
- if (cur !== el && cur.id && isStableId(cur.id)) {
432
- parts.unshift(`#${esc(cur.id)}`);
433
- break;
434
- }
435
- const parent = cur.parentElement;
436
- const node = cur;
437
- const same = parent ? Array.from(parent.children).filter((c) => c.tagName === node.tagName) : [];
438
- parts.unshift(same.length > 1 ? `${t}:nth-of-type(${same.indexOf(node) + 1})` : t);
439
- cur = parent;
440
- }
441
- return (parts.join(" > ") || tag).slice(0, 300);
442
- }
443
- function matches(el, selector, deep) {
444
- if (!selector) return false;
445
- try {
446
- return !!el.closest(selector) || deep && !!el.querySelector(selector);
447
- } catch {
448
- return false;
449
- }
450
- }
451
- function elementText(el, mask, maskAll) {
452
- if (maskAll || matches(el, mask, true)) return void 0;
453
- const tag = el.tagName.toLowerCase();
454
- let text = "";
455
- if (tag === "input") {
456
- if (!/^(button|submit|reset)$/i.test(el.type || "")) return void 0;
457
- text = el.value;
458
- } else if (tag === "select" || tag === "textarea") return void 0;
459
- else text = el.innerText ?? el.textContent ?? "";
460
- text = (text || el.getAttribute("aria-label") || el.getAttribute("title") || "").replace(/\s+/g, " ").trim();
461
- return text ? text.slice(0, 100) : void 0;
462
- }
463
- function linkHref(el) {
464
- const a = el.closest("a[href]");
465
- if (!a) return void 0;
466
- try {
467
- const u = new URL(a.getAttribute("href") || "", location.href);
468
- if (!/^https?:$/.test(u.protocol)) return void 0;
469
- return u.origin === location.origin ? u.pathname : u.origin;
470
- } catch {
471
- return void 0;
472
- }
473
- }
474
- function describeClick(el, e, opts) {
475
- const props = { tag: el.tagName.toLowerCase(), selector: elementSelector(el), x: Math.round(e.pageX ?? 0) || 0, y: Math.round(e.pageY ?? 0) || 0 };
476
- const text = elementText(el, opts.mask, opts.maskAll);
477
- if (text) props.text = text;
478
- const href = opts.collectUrls ? linkHref(el) : void 0;
479
- if (href) props.href = href;
480
- return props;
481
- }
482
- var RageClickDetector = class {
483
- constructor(threshold = 3, windowMs = 1e3, radius = 30) {
484
- __publicField(this, "threshold", threshold);
485
- __publicField(this, "windowMs", windowMs);
486
- __publicField(this, "radius", radius);
487
- __publicField(this, "clicks", []);
488
- __publicField(this, "fired", false);
489
- }
490
- /** Clicks in the current burst once it has fired (rage), otherwise 0. */
491
- get burst() {
492
- return this.fired ? this.clicks.length : 0;
493
- }
494
- /** Records a click; returns the click count when it completes a burst, otherwise 0. */
495
- add(x, y, t) {
496
- const first = this.clicks[0];
497
- const last = this.clicks[this.clicks.length - 1];
498
- if (!first || !last || t - last.t > this.windowMs || Math.hypot(x - first.x, y - first.y) > this.radius) {
499
- this.clicks = [];
500
- this.fired = false;
501
- }
502
- this.clicks = this.clicks.filter((c) => t - c.t <= this.windowMs);
503
- this.clicks.push({ x, y, t });
504
- if (this.fired || this.clicks.length < this.threshold) return 0;
505
- this.fired = true;
506
- return this.clicks.length;
507
- }
508
- };
509
-
510
733
  // src/context.ts
511
734
  function detectDevice(ua, width) {
512
735
  const u = ua || "";
@@ -516,7 +739,8 @@ function detectDevice(ua, width) {
516
739
  else if (width > 0 && width < 768) type = "mobile";
517
740
  else if (width > 0 && width < 1024) type = "tablet";
518
741
  let browser;
519
- if (/Edg\//i.test(u)) browser = "edge";
742
+ if (/Electron\//i.test(u)) browser = "electron";
743
+ else if (/Edg\//i.test(u)) browser = "edge";
520
744
  else if (/OPR\/|Opera/i.test(u)) browser = "opera";
521
745
  else if (/SamsungBrowser/i.test(u)) browser = "samsung";
522
746
  else if (/Chrome\/|CriOS/i.test(u)) browser = "chrome";
@@ -782,6 +1006,14 @@ function createMessengerTransport(opts) {
782
1006
  h.onDone(d.messages.at(-1) ?? null, d.conversation);
783
1007
  } else if (!finished) h.onError("The reply was interrupted");
784
1008
  }
1009
+ async function feedback(path, body, fallback) {
1010
+ const res = await api.post(`/feedback${path}`, { ...body, anonymousId: opts.anonymousId(), userId: opts.userId() });
1011
+ if (!res.ok || !res.data) {
1012
+ if (res.status === 429) throw new HttpError(429, "You're doing that a lot. Please try again in a minute.");
1013
+ throw new HttpError(res.status, serverMessage(res.data) ?? fallback);
1014
+ }
1015
+ return res.data;
1016
+ }
785
1017
  return {
786
1018
  async listConversations() {
787
1019
  const tokens = storedConversations(store, projectId).map((c) => c.token);
@@ -818,7 +1050,8 @@ function createMessengerTransport(opts) {
818
1050
  return res.data;
819
1051
  },
820
1052
  async getArticle(id) {
821
- const res = await api.get(`/articles/${encodeURIComponent(id)}`);
1053
+ const locale = opts.locale?.();
1054
+ const res = await api.get(`/articles/${encodeURIComponent(id)}`, locale ? { params: { locale } } : void 0);
822
1055
  return res.ok && res.data ? res.data.article : null;
823
1056
  },
824
1057
  async fileComplaint(input) {
@@ -848,7 +1081,19 @@ function createMessengerTransport(opts) {
848
1081
  async getHowTo(id) {
849
1082
  const res = await api.get(`/how-tos/${encodeURIComponent(id)}`);
850
1083
  return res.ok && res.data ? res.data.howTo : null;
851
- }
1084
+ },
1085
+ // Ideas (feature requests). The visitor's ids go in every body; the server decides what they may do.
1086
+ listIdeas: (query) => feedback("/requests/query", query, "Ideas couldn't be loaded."),
1087
+ getIdea: (id) => feedback(`/requests/${encodeURIComponent(id)}/view`, {}, "This idea couldn't be loaded."),
1088
+ submitIdea: (input) => feedback("/requests", { ...input, pageUrl: opts.pageUrl() }, "Your idea couldn't be sent. Please try again."),
1089
+ voteIdea: (id, vote) => feedback(`/requests/${encodeURIComponent(id)}/vote`, vote, "Your vote couldn't be saved."),
1090
+ unvoteIdea: (id) => feedback(`/requests/${encodeURIComponent(id)}/unvote`, {}, "Your vote couldn't be removed."),
1091
+ followIdea: (id, follow) => feedback(`/requests/${encodeURIComponent(id)}/follow`, { follow }, "That didn't work. Please try again."),
1092
+ commentIdea: (id, body) => feedback(`/requests/${encodeURIComponent(id)}/comments`, { body }, "Your comment couldn't be posted."),
1093
+ similarIdeas: async (title) => (await feedback("/similar", { title }, "Similar ideas couldn't be loaded.")).requests ?? [],
1094
+ ideaUpdates: () => feedback("/updates", {}, "Updates couldn't be loaded."),
1095
+ markIdeaUpdatesRead: (ids) => feedback("/updates/read", ids ? { ids } : {}, "Updates couldn't be marked read."),
1096
+ ideaPortalLink: async () => (await feedback("/portal-link", {}, "The feedback portal couldn't be opened.")).url
852
1097
  };
853
1098
  }
854
1099
 
@@ -978,6 +1223,48 @@ function watchNavigation(listener, debounceMs = 50) {
978
1223
  };
979
1224
  }
980
1225
 
1226
+ // src/page-url.ts
1227
+ var WEB_SCHEMES = /* @__PURE__ */ new Set(["http", "https", "about", "data", "blob", "javascript"]);
1228
+ function appScheme(href) {
1229
+ let u;
1230
+ try {
1231
+ u = new URL(href);
1232
+ } catch {
1233
+ return null;
1234
+ }
1235
+ const scheme = u.protocol.replace(/:$/, "").toLowerCase();
1236
+ return WEB_SCHEMES.has(scheme) ? null : { u, scheme };
1237
+ }
1238
+ function normalizeAppUrl(href) {
1239
+ const app = appScheme(href);
1240
+ if (!app) return href;
1241
+ const { u, scheme } = app;
1242
+ const host = scheme === "file" ? "app" : u.host || "app";
1243
+ const hash = /^#!?(\/.*)$/.exec(u.hash);
1244
+ if (hash) {
1245
+ const route = hash[1];
1246
+ return `${scheme}://${host}${route.startsWith("/") ? route : `/${route}`}`;
1247
+ }
1248
+ let path = safeDecode(u.pathname);
1249
+ if (scheme === "file" || /\.html?$/i.test(path)) {
1250
+ const file = path.split("/").filter(Boolean).pop() ?? "";
1251
+ const name = file.replace(/\.html?$/i, "");
1252
+ path = !name || name.toLowerCase() === "index" ? "/" : `/${name}`;
1253
+ }
1254
+ if (!path.startsWith("/")) path = `/${path}`;
1255
+ return `${scheme}://${host}${path}${u.search}`;
1256
+ }
1257
+ function safeDecode(s) {
1258
+ try {
1259
+ return decodeURI(s);
1260
+ } catch {
1261
+ return s;
1262
+ }
1263
+ }
1264
+ function isAppUrl(href) {
1265
+ return appScheme(href) !== null;
1266
+ }
1267
+
981
1268
  // src/page-tracking.ts
982
1269
  function scrollDepth() {
983
1270
  const de = document.documentElement;
@@ -1023,16 +1310,127 @@ var PageTracker = class {
1023
1310
  }
1024
1311
  };
1025
1312
 
1313
+ // src/prototype-overlay.ts
1314
+ var PROTOTYPE_CLOSE_MESSAGE = "guidely:prototype:close";
1315
+ var TOKEN = /^[A-Za-z0-9_-]{16,64}$/;
1316
+ var OVERLAY_ATTR = "data-guidely-prototype-overlay";
1317
+ function matchPrototypeTestUrl(urlOrToken, apiUrl) {
1318
+ const value = (urlOrToken || "").trim();
1319
+ let api;
1320
+ try {
1321
+ api = new URL(apiUrl);
1322
+ } catch {
1323
+ return null;
1324
+ }
1325
+ if (TOKEN.test(value)) return new URL(`/validate/${value}`, api.origin);
1326
+ let url;
1327
+ try {
1328
+ url = new URL(value, typeof location !== "undefined" ? location.href : api.origin);
1329
+ } catch {
1330
+ return null;
1331
+ }
1332
+ if (url.origin !== api.origin) return null;
1333
+ const m = /^\/validate\/([^/]+)\/?$/.exec(url.pathname);
1334
+ if (!m || !TOKEN.test(m[1])) return null;
1335
+ return new URL(`/validate/${m[1]}`, api.origin);
1336
+ }
1337
+ var current = null;
1338
+ function openPrototypeOverlay(testUrl, opts) {
1339
+ current?.close();
1340
+ const doc = document;
1341
+ const previous = doc.activeElement;
1342
+ const src = new URL(testUrl.href);
1343
+ const hash = new URLSearchParams();
1344
+ hash.set("key", opts.key);
1345
+ if (opts.anonymousId) hash.set("anon", opts.anonymousId);
1346
+ if (opts.userId) hash.set("uid", opts.userId);
1347
+ src.hash = hash.toString();
1348
+ const root = doc.createElement("div");
1349
+ root.setAttribute(OVERLAY_ATTR, "");
1350
+ root.setAttribute("role", "dialog");
1351
+ root.setAttribute("aria-modal", "true");
1352
+ root.setAttribute("aria-label", "Prototype test");
1353
+ 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;`;
1354
+ const frame = doc.createElement("iframe");
1355
+ frame.src = src.toString();
1356
+ frame.title = "Prototype test";
1357
+ frame.setAttribute("allow", "fullscreen");
1358
+ frame.setAttribute("referrerpolicy", "no-referrer");
1359
+ frame.style.cssText = "border:0;width:100%;height:100%;background:#fff;display:block;";
1360
+ const close = doc.createElement("button");
1361
+ close.type = "button";
1362
+ close.setAttribute("aria-label", "Close prototype test");
1363
+ close.textContent = "\xD7";
1364
+ 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;";
1365
+ root.append(frame, close);
1366
+ const overflow = doc.documentElement.style.overflow;
1367
+ doc.documentElement.style.overflow = "hidden";
1368
+ let closed = false;
1369
+ const shut = () => {
1370
+ if (closed) return;
1371
+ closed = true;
1372
+ window.removeEventListener("message", onMessage);
1373
+ doc.removeEventListener("keydown", onKey, true);
1374
+ doc.removeEventListener("focusin", onFocus, true);
1375
+ root.remove();
1376
+ doc.documentElement.style.overflow = overflow;
1377
+ if (current === overlay) current = null;
1378
+ try {
1379
+ previous?.focus?.();
1380
+ } catch {
1381
+ }
1382
+ opts.onClose?.();
1383
+ };
1384
+ const onMessage = (e) => {
1385
+ if (e.origin !== testUrl.origin || e.source !== frame.contentWindow) return;
1386
+ const data = e.data;
1387
+ if (data && typeof data === "object" && data.type === PROTOTYPE_CLOSE_MESSAGE) shut();
1388
+ };
1389
+ const onKey = (e) => {
1390
+ if (e.key === "Escape") {
1391
+ e.preventDefault();
1392
+ e.stopPropagation();
1393
+ shut();
1394
+ } else if (e.key === "Tab") {
1395
+ e.preventDefault();
1396
+ (doc.activeElement === close ? frame : close).focus();
1397
+ }
1398
+ };
1399
+ const onFocus = (e) => {
1400
+ if (!root.contains(e.target)) close.focus();
1401
+ };
1402
+ close.addEventListener("click", shut);
1403
+ window.addEventListener("message", onMessage);
1404
+ doc.addEventListener("keydown", onKey, true);
1405
+ doc.addEventListener("focusin", onFocus, true);
1406
+ doc.body.appendChild(root);
1407
+ frame.focus();
1408
+ const overlay = { element: root, close: shut };
1409
+ current = overlay;
1410
+ return overlay;
1411
+ }
1412
+
1026
1413
  // src/load-recorder.ts
1027
1414
  var loadRecorder = async () => (await import("./recorder.js")).recorder;
1028
1415
 
1029
1416
  // src/client.ts
1030
1417
  var SDK_NAME = "@guidelytech/sdk-core";
1031
- var SDK_VERSION = "0.9.0";
1418
+ var SDK_VERSION = "0.11.0";
1032
1419
  var DEFAULT_API_URL = "https://api.guidely.app";
1033
1420
  var PAGE_SCOPED = /* @__PURE__ */ new Set(["page_load", "url_match", "element_click", "element_visible", "delay"]);
1034
1421
  var DAY_MS = 24 * 60 * 60 * 1e3;
1035
1422
  var MAX_SESSION_EXCEPTIONS = 100;
1423
+ var CACHED_START_MS = 1500;
1424
+ function settlesWithin(promise, ms) {
1425
+ return new Promise((resolve) => {
1426
+ const timer = setTimeout(() => resolve(false), ms);
1427
+ const done = () => {
1428
+ clearTimeout(timer);
1429
+ resolve(true);
1430
+ };
1431
+ promise.then(done, done);
1432
+ });
1433
+ }
1036
1434
  function isBrowser() {
1037
1435
  return typeof window !== "undefined" && typeof document !== "undefined";
1038
1436
  }
@@ -1043,7 +1441,7 @@ function emptyState() {
1043
1441
  return { guides: {}, checklists: {}, surveysSubmitted: [], audienceIds: [] };
1044
1442
  }
1045
1443
  function newSession(now = Date.now()) {
1046
- return { id: `s_${randomId(16)}`, startedAt: new Date(now).toISOString(), shownGuides: [], events: [], lastActivityAt: now };
1444
+ return { id: `s_${randomId2(16)}`, startedAt: new Date(now).toISOString(), shownGuides: [], events: [], lastActivityAt: now };
1047
1445
  }
1048
1446
  var GuidelyClient = class _GuidelyClient {
1049
1447
  constructor(options) {
@@ -1053,6 +1451,10 @@ var GuidelyClient = class _GuidelyClient {
1053
1451
  __publicField(this, "sessionStore");
1054
1452
  __publicField(this, "emitter");
1055
1453
  __publicField(this, "queue");
1454
+ /** Survey answers and guide/checklist progress, kept until the server accepts them. */
1455
+ __publicField(this, "outbox");
1456
+ /** Agent Analytics turns (agentConversation), in their own outbox. */
1457
+ __publicField(this, "agentTurns");
1056
1458
  __publicField(this, "config", null);
1057
1459
  __publicField(this, "etag", null);
1058
1460
  __publicField(this, "anonymousId");
@@ -1102,6 +1504,10 @@ var GuidelyClient = class _GuidelyClient {
1102
1504
  __publicField(this, "flags");
1103
1505
  /** Experiments already reported this session (`experiment_viewed` is sent once per session). */
1104
1506
  __publicField(this, "exposures", /* @__PURE__ */ new Set());
1507
+ /** Language set with `setLocale()`. */
1508
+ __publicField(this, "locale", null);
1509
+ /** Language each shown guide was rendered in; added to its events as `locale`. */
1510
+ __publicField(this, "guideLocales", /* @__PURE__ */ new Map());
1105
1511
  __publicField(this, "customListeners", /* @__PURE__ */ new Map());
1106
1512
  __publicField(this, "eventBus", {
1107
1513
  on: (name, cb) => {
@@ -1136,7 +1542,7 @@ var GuidelyClient = class _GuidelyClient {
1136
1542
  this.sessionStore = this.options.storage === "memory" ? this.store : createStorage("session");
1137
1543
  this.anonymousId = this.store.get(KEYS.anon) || "";
1138
1544
  if (!this.anonymousId) {
1139
- this.anonymousId = `anon_${randomId(20)}`;
1545
+ this.anonymousId = `anon_${randomId2(20)}`;
1140
1546
  this.store.set(KEYS.anon, this.anonymousId);
1141
1547
  }
1142
1548
  this.firstVisit = !this.store.get(KEYS.seen);
@@ -1146,12 +1552,13 @@ var GuidelyClient = class _GuidelyClient {
1146
1552
  const session = readJson(this.sessionStore, KEYS.session, null);
1147
1553
  this.session = session && session.id ? session : newSession();
1148
1554
  this.persistSession();
1149
- this.lastUrl = location.href;
1555
+ this.lastUrl = this.pageHref();
1150
1556
  this.lastAudiences = new Set(this.state.audienceIds);
1151
1557
  this.queue = new EventQueue({
1152
1558
  store: this.store,
1153
1559
  storageKey: KEYS.queue(this.options.projectId),
1154
1560
  enabled: () => this.analyticsEnabled(),
1561
+ health: this.api.health,
1155
1562
  send: async (events) => {
1156
1563
  const res = await this.api.post("/events", { events, sentAt: nowIso(), sdk: { name: SDK_NAME, version: SDK_VERSION } }, { keepalive: true });
1157
1564
  return { ok: res.ok, retry: !(res.status >= 400 && res.status < 500 && res.status !== 429) };
@@ -1159,6 +1566,8 @@ var GuidelyClient = class _GuidelyClient {
1159
1566
  beacon: (events) => this.api.beacon("/events", { events, sentAt: nowIso(), sdk: { name: SDK_NAME, version: SDK_VERSION } }),
1160
1567
  onError: (err) => this.reportError(err)
1161
1568
  });
1569
+ this.outbox = new Outbox({ api: this.api, store: this.store, storageKey: KEYS.outbox(this.options.projectId), health: this.api.health });
1570
+ 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 } });
1162
1571
  if (options.user) this.user = { ...options.user };
1163
1572
  this.flags = this.createFlags(options.flags);
1164
1573
  }
@@ -1216,12 +1625,15 @@ var GuidelyClient = class _GuidelyClient {
1216
1625
  const cached = readJson(this.store, KEYS.config(this.options.projectId), null);
1217
1626
  this.etag = this.store.get(KEYS.etag(this.options.projectId));
1218
1627
  if (cached && cached.guides) this.config = webOnly(cached);
1219
- await this.fetchConfig();
1628
+ const before = this.config?.configVersion;
1629
+ const fresh = this.fetchConfig();
1630
+ if (!this.config) await fresh;
1631
+ else if (!await settlesWithin(fresh, CACHED_START_MS)) void fresh.then(() => this.applyConfigChange(before));
1220
1632
  if (this.destroyed) return;
1221
1633
  this.wireListeners();
1222
1634
  if (!this.identitySettled) {
1223
- if (this.options.user) await this.identify(this.options.user.id, this.options.user.traits, this.options.user.account);
1224
- else if (this.user?.id) await this.identify(this.user.id, this.user.traits, this.user.account);
1635
+ const known = this.options.user ?? (this.user?.id ? this.user : null);
1636
+ if (known) await settlesWithin(this.identify(known.id, known.traits, known.account), CACHED_START_MS);
1225
1637
  }
1226
1638
  if (this.destroyed) return;
1227
1639
  this.isReady = true;
@@ -1232,7 +1644,7 @@ var GuidelyClient = class _GuidelyClient {
1232
1644
  this.resumeActiveGuide();
1233
1645
  this.evaluate("ready");
1234
1646
  if (!this.options.disableAuthoring && !capturing) this.maybeStartAuthoring();
1235
- this.pageTracker.start(location.href);
1647
+ this.pageTracker.start(this.pageHref());
1236
1648
  this.touchSession();
1237
1649
  this.syncCapture();
1238
1650
  } catch (err) {
@@ -1265,7 +1677,7 @@ var GuidelyClient = class _GuidelyClient {
1265
1677
  wireListeners() {
1266
1678
  const doc = document;
1267
1679
  const win = window;
1268
- this.cleanups.push(watchNavigation((url) => this.handleNavigation(url)));
1680
+ this.cleanups.push(watchNavigation((url) => this.handleNavigation(this.pageHref(url))));
1269
1681
  const onClick = (e) => this.handleDocumentClick(e);
1270
1682
  doc.addEventListener("click", onClick, true);
1271
1683
  this.cleanups.push(() => doc.removeEventListener("click", onClick, true));
@@ -1286,7 +1698,7 @@ var GuidelyClient = class _GuidelyClient {
1286
1698
  const onPageShow = (e) => {
1287
1699
  if (!e.persisted || !this.pageLeft) return;
1288
1700
  this.pageLeft = false;
1289
- this.pageTracker.start(location.href);
1701
+ this.pageTracker.start(this.pageHref());
1290
1702
  };
1291
1703
  let lastScroll = 0;
1292
1704
  const onScroll = () => {
@@ -1326,11 +1738,16 @@ var GuidelyClient = class _GuidelyClient {
1326
1738
  win.removeEventListener("scroll", onScroll);
1327
1739
  doc.removeEventListener("keydown", onKey, true);
1328
1740
  });
1329
- const refresh = setInterval(() => void this.refresh(), this.options.configRefreshMs);
1741
+ const refresh = setInterval(() => {
1742
+ if (this.api.health.canSend()) void this.refresh();
1743
+ }, this.options.configRefreshMs);
1744
+ const onOnline = () => this.api.health.online();
1745
+ win.addEventListener("online", onOnline);
1746
+ this.cleanups.push(() => win.removeEventListener("online", onOnline));
1330
1747
  this.cleanups.push(() => clearInterval(refresh));
1331
1748
  const poll = setInterval(() => {
1332
1749
  if (!this.isReady || !this.config || this.editing() || doc.visibilityState === "hidden") return;
1333
- const url = location.href;
1750
+ const url = this.pageHref();
1334
1751
  const ctx = this.getContext();
1335
1752
  for (const guide of this.config.guides) {
1336
1753
  if (guide.config.trigger.type === "element_visible" && guide.config.trigger.value) this.armVisibility(guide, ctx, url);
@@ -1359,6 +1776,11 @@ var GuidelyClient = class _GuidelyClient {
1359
1776
  if (this.destroyed) return;
1360
1777
  const before = this.config?.configVersion;
1361
1778
  await this.fetchConfig();
1779
+ this.applyConfigChange(before);
1780
+ }
1781
+ /** Re-renders after a config change (refresh, or a config that arrived after start). */
1782
+ applyConfigChange(before) {
1783
+ if (this.destroyed || !this.isReady) return;
1362
1784
  if (this.config && this.config.configVersion !== before) {
1363
1785
  this.renderPersistentWidgets();
1364
1786
  this.evaluate("ready");
@@ -1393,10 +1815,12 @@ var GuidelyClient = class _GuidelyClient {
1393
1815
  this.replay = null;
1394
1816
  this.queue.flushSync();
1395
1817
  this.queue.destroy();
1818
+ this.outbox.destroy();
1819
+ this.agentTurns.destroy();
1396
1820
  this.flags.destroy();
1397
1821
  this.emitter.clear();
1398
1822
  destroyHost();
1399
- if (current === this) current = null;
1823
+ if (current2 === this) current2 = null;
1400
1824
  }
1401
1825
  // ---------------------------------------------------------------------------
1402
1826
  // Identity
@@ -1414,7 +1838,7 @@ var GuidelyClient = class _GuidelyClient {
1414
1838
  if (this.isReady) this.evaluate("identify");
1415
1839
  }
1416
1840
  async syncIdentify() {
1417
- 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 };
1841
+ 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 };
1418
1842
  const res = await postWithRetry(this.api, "/identify", body, 2);
1419
1843
  if (res?.ok && res.data) this.mergeServerState(res.data);
1420
1844
  }
@@ -1445,7 +1869,10 @@ var GuidelyClient = class _GuidelyClient {
1445
1869
  checklists,
1446
1870
  surveysSubmitted: Array.from(/* @__PURE__ */ new Set([...this.state.surveysSubmitted, ...data.surveysSubmitted || []])),
1447
1871
  audienceIds: data.audienceIds || [],
1448
- firstSeenAt: this.state.firstSeenAt ?? data.firstSeenAt
1872
+ firstSeenAt: this.state.firstSeenAt ?? data.firstSeenAt,
1873
+ // Device-local only (the server doesn't hold them).
1874
+ ...this.state.throttle ? { throttle: this.state.throttle } : {},
1875
+ ...this.state.announcementsSeenAt ? { announcementsSeenAt: this.state.announcementsSeenAt } : {}
1449
1876
  };
1450
1877
  if (data.firstSeenAt && Date.now() - Date.parse(data.firstSeenAt) > 6e4) this.firstVisit = false;
1451
1878
  this.persistState();
@@ -1481,7 +1908,7 @@ var GuidelyClient = class _GuidelyClient {
1481
1908
  this.stopGuide();
1482
1909
  this.user = null;
1483
1910
  this.store.remove(KEYS.user(this.options.projectId));
1484
- this.anonymousId = `anon_${randomId(20)}`;
1911
+ this.anonymousId = `anon_${randomId2(20)}`;
1485
1912
  this.store.set(KEYS.anon, this.anonymousId);
1486
1913
  this.state = emptyState();
1487
1914
  this.persistState();
@@ -1513,6 +1940,7 @@ var GuidelyClient = class _GuidelyClient {
1513
1940
  if (consent === "denied") this.queue.clear();
1514
1941
  else if (consent === "granted") void this.queue.flush();
1515
1942
  if (consent !== "granted") this.replay?.stop({ discard: true });
1943
+ if (consent !== "pending") this.agentTurns.consentChanged(consent === "granted");
1516
1944
  this.syncCapture();
1517
1945
  }
1518
1946
  analyticsEnabled() {
@@ -1525,17 +1953,19 @@ var GuidelyClient = class _GuidelyClient {
1525
1953
  if (this.consent === "denied" || !this.options.analytics) return null;
1526
1954
  if (name !== "page_leave" && name !== "heatmap") this.touchSession();
1527
1955
  const event = {
1528
- eventId: randomId(24),
1956
+ eventId: randomId2(24),
1529
1957
  name,
1530
1958
  occurredAt: nowIso(),
1531
1959
  anonymousId: this.anonymousId,
1532
1960
  userId: this.user?.id || null,
1533
1961
  accountId: this.user?.account?.id || null,
1534
1962
  sessionId: this.session.id,
1535
- url: this.collectUrls() ? location.href : void 0,
1963
+ url: this.collectUrls() ? this.pageHref() : void 0,
1536
1964
  ...extra
1537
1965
  };
1538
1966
  if (extra.properties) event.properties = sanitizeProperties(extra.properties);
1967
+ const locale = extra.guideId ? this.guideLocales.get(extra.guideId) : void 0;
1968
+ if (locale) event.properties = { ...event.properties ?? {}, locale };
1539
1969
  this.queue.enqueue(event);
1540
1970
  this.replay?.addCustomEvent("guidely", { name, customName: event.customName, guideId: event.guideId });
1541
1971
  this.emitter.emit("track", event);
@@ -1559,9 +1989,30 @@ var GuidelyClient = class _GuidelyClient {
1559
1989
  this.completeChecklistTasksBy("event", name);
1560
1990
  if (this.isReady) this.evaluate("event", { eventName: name });
1561
1991
  }
1992
+ /**
1993
+ * Agent Analytics: records one or more turns of a conversation with your own AI agent or chatbot
1994
+ * (`{ conversationId, agentId, role, text, toolCalls?, latencyMs?, outcome?, … }`). The visitor's
1995
+ * anonymous id, user id, account id and session id are filled in, `agents.redact` from the init
1996
+ * options runs on each turn, and turns are retried until Guidely accepts them. Returns false when
1997
+ * nothing was queued (analytics off or consent denied, or every turn was invalid or dropped).
1998
+ */
1999
+ agentConversation(turns) {
2000
+ if (this.destroyed || !this.options.analytics || this.consent === "denied" || this.config?.settings?.analyticsEnabled === false) return false;
2001
+ const identity = { anonymousId: this.anonymousId, userId: this.user?.id || null, accountId: this.user?.account?.id || null, sessionId: this.session.id };
2002
+ const prepared = (Array.isArray(turns) ? turns : [turns]).map((t) => prepareAgentTurn(t, identity, this.options.agents?.redact, (err) => this.reportError(err))).filter((t) => !!t);
2003
+ if (!prepared.length) return false;
2004
+ this.touchSession();
2005
+ this.agentTurns.add(prepared, this.consent === "pending");
2006
+ return true;
2007
+ }
2008
+ /** Alias of {@link agentConversation}. */
2009
+ trackAgentTurn(turns) {
2010
+ return this.agentConversation(turns);
2011
+ }
1562
2012
  /** Records a page view and re-evaluates triggers. Called automatically on SPA navigation. */
1563
- page(url = location.href) {
2013
+ page(href) {
1564
2014
  if (this.destroyed) return;
2015
+ const url = this.pageHref(href);
1565
2016
  this.leavePage(url);
1566
2017
  this.trackPageView(url);
1567
2018
  this.handleNavigation(url, true);
@@ -1595,7 +2046,7 @@ var GuidelyClient = class _GuidelyClient {
1595
2046
  if (changed) void this.queue.flush();
1596
2047
  }
1597
2048
  handleDocumentClick(e) {
1598
- const target = e.target;
2049
+ const target = composedTarget(e);
1599
2050
  if (!target || typeof target.closest !== "function") return;
1600
2051
  this.autocaptureClick(e, target);
1601
2052
  this.completeChecklistTasksBy("click", target);
@@ -1653,6 +2104,8 @@ var GuidelyClient = class _GuidelyClient {
1653
2104
  const ref = !this.session.pageViews ? externalReferrer() : void 0;
1654
2105
  if (ref) props.referrer = ref;
1655
2106
  }
2107
+ const ep = this.captureAllowed() ? pagePropertyValues(this.config?.tagging, url, { mask: this.maskSelector(), maskAll: this.maskAllText() }) : void 0;
2108
+ if (ep) props.ep = ep;
1656
2109
  this.session.pageViews = (this.session.pageViews ?? 0) + 1;
1657
2110
  this.persistSession();
1658
2111
  this.enqueue("page_view", { url: collect ? url : void 0, properties: props });
@@ -1678,21 +2131,28 @@ var GuidelyClient = class _GuidelyClient {
1678
2131
  const heat = this.autocaptureOn("heatmaps");
1679
2132
  if (!clicks && !rage && !heat || isIgnoredElement(target)) return;
1680
2133
  this.touchSession();
1681
- const el = interactiveElement(target);
2134
+ const featureAttribute = this.config?.tagging?.featureAttribute ?? null;
2135
+ const feature = featureAttributeOf(target, featureAttribute);
2136
+ const el = interactiveElement(target) ?? feature?.el ?? null;
1682
2137
  const opts = { mask: this.maskSelector(), maskAll: this.maskAllText(), collectUrls: this.collectUrls() };
1683
- if (clicks && el) this.enqueue("click", { properties: { ...describeClick(el, e, opts) } });
2138
+ const clickProps = (on) => {
2139
+ const p = describeClick(on, e, opts);
2140
+ const ep = clickPropertyValues(this.config?.tagging, on, p, opts);
2141
+ return { ...p, el: elementSnapshot(on, { ...opts, featureAttribute }), ...feature ? { fa: feature.value } : {}, ...ep ? { ep } : {} };
2142
+ };
2143
+ if (clicks && el) this.enqueue("click", { properties: clickProps(el) });
1684
2144
  const count = this.rage.add(e.pageX ?? 0, e.pageY ?? 0, Date.now());
1685
- if (rage && count) this.enqueue("rage_click", { properties: { ...describeClick(el ?? target, e, opts), count } });
2145
+ if (rage && count) this.enqueue("rage_click", { properties: { ...clickProps(el ?? target), count } });
1686
2146
  if (!heat) return;
1687
- const url = location.href;
2147
+ const url = this.pageHref();
1688
2148
  if (this.heatmap.url !== url) this.flushHeatmap();
1689
2149
  if (this.heatmap.add(url, heatmapPoint(el ?? target, e, this.rage.burst > 0), this.rage.burst)) this.flushHeatmap();
1690
2150
  }
1691
2151
  /** Bins one pointer sample into the heatmap buffer. Not session activity. */
1692
2152
  sampleMove(e) {
1693
- const target = e.target;
2153
+ const target = composedTarget(e);
1694
2154
  if (document.visibilityState === "hidden" || !target?.closest || !this.autocaptureOn("mouseMovement") || isIgnoredElement(target)) return;
1695
- const url = location.href;
2155
+ const url = this.pageHref();
1696
2156
  if (this.heatmap.url !== url) this.flushHeatmap();
1697
2157
  if (this.heatmap.move(url, movementBin(interactiveElement(target) ?? target, e))) this.flushHeatmap();
1698
2158
  }
@@ -1768,7 +2228,7 @@ var GuidelyClient = class _GuidelyClient {
1768
2228
  if (!this.isReady || this.destroyed) return;
1769
2229
  if (this.config && !this.initialPageViewSent && this.config.settings?.autocapture) {
1770
2230
  this.initialPageViewSent = true;
1771
- this.trackPageView(location.href);
2231
+ this.trackPageView(this.pageHref());
1772
2232
  }
1773
2233
  if (this.config) {
1774
2234
  for (const [props, extra] of this.earlyExceptions.splice(0)) {
@@ -1803,7 +2263,7 @@ var GuidelyClient = class _GuidelyClient {
1803
2263
  return {
1804
2264
  anonymousId: this.anonymousId,
1805
2265
  userId: this.user?.id || null,
1806
- url: this.collectUrls() ? location.href : void 0,
2266
+ url: this.collectUrls() ? this.pageHref() : void 0,
1807
2267
  device: { type: device.type, browser: device.browser, os: device.os },
1808
2268
  viewport: { width: window.innerWidth, height: window.innerHeight },
1809
2269
  sdk: { name: SDK_NAME, version: SDK_VERSION }
@@ -1815,7 +2275,9 @@ var GuidelyClient = class _GuidelyClient {
1815
2275
  this.persistSession();
1816
2276
  this.replay?.stop({ discard: true });
1817
2277
  },
1818
- onError: (err) => this.reportError(err)
2278
+ onError: (err) => this.reportError(err),
2279
+ health: this.api.health,
2280
+ stash: this.sessionStore
1819
2281
  });
1820
2282
  this.syncReplay();
1821
2283
  },
@@ -1852,6 +2314,14 @@ var GuidelyClient = class _GuidelyClient {
1852
2314
  // ---------------------------------------------------------------------------
1853
2315
  // Targeting + triggers
1854
2316
  // ---------------------------------------------------------------------------
2317
+ /** The page URL Guidely works with: `options.pageUrl`, else app-relative for `file:` / custom protocols (`normalizeAppUrl`). */
2318
+ pageHref(href = location.href) {
2319
+ try {
2320
+ return this.options.pageUrl ? this.options.pageUrl(href) || href : normalizeAppUrl(href);
2321
+ } catch {
2322
+ return href;
2323
+ }
2324
+ }
1855
2325
  /** Builds the targeting context for the current moment. */
1856
2326
  getContext() {
1857
2327
  const guides = this.state.guides;
@@ -1862,7 +2332,7 @@ var GuidelyClient = class _GuidelyClient {
1862
2332
  userId: this.user?.id || null,
1863
2333
  userAttributes: this.user?.traits ?? {},
1864
2334
  accountAttributes: this.user?.account?.traits ?? {},
1865
- url: location.href,
2335
+ url: this.pageHref(),
1866
2336
  firstVisit: this.firstVisit,
1867
2337
  eventsSeen: this.session.events,
1868
2338
  completedGuides: keysOf(Object.keys(guides).filter((id) => guides[id]?.completedAt)),
@@ -1871,7 +2341,8 @@ var GuidelyClient = class _GuidelyClient {
1871
2341
  completedChecklists: keysOf(completedChecklists),
1872
2342
  audienceIds: this.state.audienceIds,
1873
2343
  environment: this.options.environment,
1874
- platform: "web"
2344
+ platform: "web",
2345
+ ...this.options.appVersion ? { appVersion: this.options.appVersion } : {}
1875
2346
  });
1876
2347
  }
1877
2348
  isTargeted(guide, ctx, url) {
@@ -1883,6 +2354,52 @@ var GuidelyClient = class _GuidelyClient {
1883
2354
  ctx.rolloutSalt = guide.id;
1884
2355
  return evaluateRules(c.targeting, ctx);
1885
2356
  }
2357
+ /** Guide throttling: may this guide launch automatically now? */
2358
+ throttleAllows(guide) {
2359
+ if (!countsTowardThrottle(guide)) return true;
2360
+ return isThrottleEligible(this.config?.settings.throttling, this.state.throttle ?? []);
2361
+ }
2362
+ recordLaunch(guide, manual) {
2363
+ if (manual || !countsTowardThrottle(guide) || !this.config?.settings.throttling?.enabled) return;
2364
+ this.state.throttle = pruneLaunches([...this.state.throttle ?? [], nowIso()], this.config.settings.throttling);
2365
+ this.persistState();
2366
+ }
2367
+ /** Guides and A/B test control groups, which are evaluated the same way but never render. */
2368
+ candidates() {
2369
+ const guides = this.config?.guides ?? [];
2370
+ const holdouts = this.config?.guideHoldouts ?? [];
2371
+ return holdouts.length ? [...guides, ...holdouts.map((h) => h.guide)] : guides;
2372
+ }
2373
+ holdoutFor(guide) {
2374
+ return this.config?.guideHoldouts?.find((h) => h.guide.id === guide.id) ?? null;
2375
+ }
2376
+ /** The visitor's language candidates, most preferred first. */
2377
+ locales() {
2378
+ const nav = typeof navigator !== "undefined" ? [...navigator.languages ?? [], navigator.language] : [];
2379
+ return visitorLocales(this.locale ?? this.options.locale, this.user?.traits, nav);
2380
+ }
2381
+ defaultLocale() {
2382
+ return this.config?.settings.localization?.defaultLocale || "en";
2383
+ }
2384
+ /** The guide in the visitor's language (remembered so its events carry `locale`). */
2385
+ localize(guide) {
2386
+ const localized = localizeGuide(guide, this.locales(), this.defaultLocale());
2387
+ this.guideLocales.set(guide.id, localized.locale);
2388
+ return localized;
2389
+ }
2390
+ /**
2391
+ * Sets the language for guides and the resource center (`null` = back to the traits / browser).
2392
+ * Guides already on screen keep their language; the resource center re-renders.
2393
+ */
2394
+ setLocale(locale) {
2395
+ this.locale = locale ? normalizeLocale(locale) || null : null;
2396
+ if (this.resourceCenter) {
2397
+ this.resourceCenter.destroy();
2398
+ this.resourceCenterScope?.destroy();
2399
+ this.resourceCenter = null;
2400
+ this.renderPersistentWidgets();
2401
+ }
2402
+ }
1886
2403
  isEligible(guide, ctx, url) {
1887
2404
  if (!this.isTargeted(guide, ctx, url)) return false;
1888
2405
  const shownThisSession = this.session.shownGuides.includes(guide.id);
@@ -1899,13 +2416,13 @@ var GuidelyClient = class _GuidelyClient {
1899
2416
  if (t.delayMs > 0) return reason === "timer" && detail.guideId === guide.id;
1900
2417
  return reason === "ready" || reason === "navigation" || reason === "identify";
1901
2418
  case "url_match":
1902
- return (reason === "ready" || reason === "navigation" || reason === "identify") && urlMatches2(t.value, location.href);
2419
+ return (reason === "ready" || reason === "navigation" || reason === "identify") && urlMatches2(t.value, this.pageHref());
1903
2420
  case "event":
1904
2421
  return reason === "event" && !!t.value && detail.eventName === t.value;
1905
2422
  case "element_click":
1906
2423
  if (reason !== "click" || !t.value || !detail.target) return false;
1907
2424
  try {
1908
- return !!detail.target.closest(t.value);
2425
+ return !!closestComposed4(detail.target, t.value);
1909
2426
  } catch {
1910
2427
  return false;
1911
2428
  }
@@ -1924,15 +2441,15 @@ var GuidelyClient = class _GuidelyClient {
1924
2441
  }
1925
2442
  evaluate(reason, detail = {}) {
1926
2443
  if (!this.config || this.destroyed || this.editing()) return;
1927
- const url = location.href;
2444
+ const url = this.pageHref();
1928
2445
  const ctx = this.getContext();
1929
2446
  if (reason === "identify") {
1930
- const current2 = new Set(this.state.audienceIds);
1931
- detail.newAudiences = new Set(Array.from(current2).filter((a) => !this.lastAudiences.has(a)));
1932
- this.lastAudiences = current2;
2447
+ const current3 = new Set(this.state.audienceIds);
2448
+ detail.newAudiences = new Set(Array.from(current3).filter((a) => !this.lastAudiences.has(a)));
2449
+ this.lastAudiences = current3;
1933
2450
  }
1934
2451
  const candidates = [];
1935
- for (const guide of this.config.guides) {
2452
+ for (const guide of this.candidates()) {
1936
2453
  const t = guide.config.trigger;
1937
2454
  if (detail.endedGuide && detail.endedGuide.id === guide.id) continue;
1938
2455
  if (reason === "ready" || reason === "navigation") {
@@ -1948,6 +2465,10 @@ var GuidelyClient = class _GuidelyClient {
1948
2465
  if (t.type === "page_load" && t.delayMs > 0 && reason === "timer" && detail.guideId !== guide.id) continue;
1949
2466
  if (!this.triggerFires(guide, reason, detail)) continue;
1950
2467
  if (!this.isEligible(guide, ctx, url)) continue;
2468
+ if (!this.throttleAllows(guide)) {
2469
+ this.log("throttled", guide.key);
2470
+ continue;
2471
+ }
1951
2472
  candidates.push(guide);
1952
2473
  }
1953
2474
  if (reason === "after_guide" || reason === "navigation") this.drainQueue(url, ctx);
@@ -1998,6 +2519,13 @@ var GuidelyClient = class _GuidelyClient {
1998
2519
  /** Decides whether a guide can show now, otherwise queues it. */
1999
2520
  tryShow(guide, url, manual) {
2000
2521
  if (this.destroyed) return false;
2522
+ if (!manual && !this.throttleAllows(guide)) return false;
2523
+ const holdout = this.holdoutFor(guide);
2524
+ if (holdout) {
2525
+ if (this.active && !manual) return false;
2526
+ this.recordImpression(guide, manual);
2527
+ return true;
2528
+ }
2001
2529
  if (!isExclusiveGuide(guide)) {
2002
2530
  if (this.widgets.has(guide.id)) return false;
2003
2531
  this.show(guide, manual);
@@ -2038,14 +2566,18 @@ var GuidelyClient = class _GuidelyClient {
2038
2566
  guideState(id) {
2039
2567
  return this.state.guides[id] ?? (this.state.guides[id] = { impressions: 0 });
2040
2568
  }
2041
- recordImpression(guide) {
2569
+ recordImpression(guide, manual = false) {
2042
2570
  const s = this.guideState(guide.id);
2043
2571
  s.impressions += 1;
2044
2572
  s.lastShownAt = nowIso();
2045
2573
  if (!this.session.shownGuides.includes(guide.id)) this.session.shownGuides.push(guide.id);
2574
+ this.recordLaunch(guide, manual);
2046
2575
  this.persistState();
2047
2576
  this.persistSession();
2048
- this.syncGuideState(guide.id, { impression: true });
2577
+ const holdout = this.holdoutFor(guide);
2578
+ const experiment = holdout ? { id: holdout.experimentId, variantId: holdout.variantId } : guide.experiment;
2579
+ if (experiment) this.enqueue("experiment_viewed", { properties: { guideExperimentId: experiment.id, variantId: experiment.variantId, ...holdout ? {} : { guideId: guide.id } } });
2580
+ if (!holdout) this.syncGuideState(guide.id, { impression: true });
2049
2581
  }
2050
2582
  recordEnd(guide, reason) {
2051
2583
  const s = this.guideState(guide.id);
@@ -2056,7 +2588,9 @@ var GuidelyClient = class _GuidelyClient {
2056
2588
  if (reason === "completed") this.completeChecklistTasksBy("guide_completed", guide);
2057
2589
  }
2058
2590
  syncGuideState(guideId, update) {
2059
- void postWithRetry(this.api, "/guide-state", { anonymousId: this.anonymousId, userId: this.user?.id || null, updates: [{ guideId, ...update }] });
2591
+ const body = { anonymousId: this.anonymousId, userId: this.user?.id || null, updates: [{ guideId, ...update }] };
2592
+ if (update.impression) void postWithRetry(this.api, "/guide-state", body);
2593
+ else this.outbox.add(`guide:${guideId}:${update.completed ? "completed" : "dismissed"}:${this.anonymousId}`, "/guide-state", body);
2060
2594
  }
2061
2595
  show(guide, manual, stepId) {
2062
2596
  try {
@@ -2067,13 +2601,17 @@ var GuidelyClient = class _GuidelyClient {
2067
2601
  this.reportError(err);
2068
2602
  }
2069
2603
  }
2070
- showFlow(guide, manual, stepId) {
2604
+ showFlow(raw, manual, stepId) {
2071
2605
  if (this.editing()) return;
2606
+ const guide = this.localize(raw);
2072
2607
  const exclusive = isExclusiveGuide(guide);
2073
- const entry = { guide, manual, startUrl: location.href };
2608
+ const entry = { guide, manual, startUrl: this.pageHref() };
2074
2609
  const controller = runGuide2(guide, {
2075
2610
  theme: this.theme(),
2611
+ // Code blocks only arrive when a workspace admin allowed them (the server strips them otherwise).
2612
+ runCode: true,
2076
2613
  ctx: () => this.getContext(),
2614
+ currentUrl: () => this.pageHref(),
2077
2615
  startAtStep: stepId,
2078
2616
  eventBus: this.eventBus,
2079
2617
  // Started from Help, Scout or startGuide(): steps on other pages offer to take the user there.
@@ -2087,7 +2625,7 @@ var GuidelyClient = class _GuidelyClient {
2087
2625
  const step = e.stepId ? guide.config.steps.find((s) => s.id === e.stepId) : void 0;
2088
2626
  this.enqueue(e.name, { guideId: guide.id, guideVersion: guide.version, stepId: e.stepId, stepIndex: e.stepIndex, properties: e.properties });
2089
2627
  if (e.name === "guide_started") {
2090
- this.recordImpression(guide);
2628
+ this.recordImpression(guide, manual);
2091
2629
  this.emitter.emit("guide:start", guide);
2092
2630
  }
2093
2631
  if (e.name === "step_viewed" && step) {
@@ -2134,8 +2672,9 @@ var GuidelyClient = class _GuidelyClient {
2134
2672
  this.widgets.delete(guide.id);
2135
2673
  }
2136
2674
  }
2137
- showChecklist(guide) {
2675
+ showChecklist(raw) {
2138
2676
  if (this.editing()) return;
2677
+ const guide = this.localize(raw);
2139
2678
  const config = guide.config.checklist;
2140
2679
  if (!config || this.widgets.has(guide.id)) return;
2141
2680
  const scope = createScope(this.theme(), guide.config.appearance);
@@ -2156,12 +2695,13 @@ var GuidelyClient = class _GuidelyClient {
2156
2695
  }, checklist: widget, scope });
2157
2696
  if (first) this.recordImpression(guide);
2158
2697
  }
2159
- showSurvey(guide, manual) {
2698
+ showSurvey(raw, manual) {
2160
2699
  if (this.editing()) return;
2700
+ const guide = this.localize(raw);
2161
2701
  const config = guide.config.survey;
2162
2702
  if (!config) return;
2163
2703
  const scope = createScope(this.theme(), guide.config.appearance);
2164
- const entry = { guide, manual, startUrl: location.href, scope };
2704
+ const entry = { guide, manual, startUrl: this.pageHref(), scope };
2165
2705
  const finish = (reason) => {
2166
2706
  widget.destroy();
2167
2707
  scope.destroy();
@@ -2179,12 +2719,12 @@ var GuidelyClient = class _GuidelyClient {
2179
2719
  title: guide.name,
2180
2720
  onDismiss: () => finish(this.state.surveysSubmitted.includes(guide.id) ? "submitted" : "dismissed"),
2181
2721
  onSubmit: (answers) => {
2182
- const responseId = randomId(24);
2722
+ const responseId = randomId2(24);
2183
2723
  this.state.surveysSubmitted.push(guide.id);
2184
2724
  this.persistState();
2185
2725
  this.recordEnd(guide, "completed");
2186
2726
  this.enqueue("survey_submitted", { guideId: guide.id, guideVersion: guide.version, properties: { responseId } });
2187
- 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 });
2727
+ 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 });
2188
2728
  this.emitter.emit("survey:submit", { guide, answers, responseId });
2189
2729
  this.emitter.emit("guide:complete", guide);
2190
2730
  widget.showThankYou();
@@ -2192,7 +2732,7 @@ var GuidelyClient = class _GuidelyClient {
2192
2732
  });
2193
2733
  entry.survey = widget;
2194
2734
  this.active = entry;
2195
- this.recordImpression(guide);
2735
+ this.recordImpression(guide, manual);
2196
2736
  this.enqueue("survey_displayed", { guideId: guide.id, guideVersion: guide.version });
2197
2737
  this.emitter.emit("guide:start", guide);
2198
2738
  }
@@ -2201,6 +2741,7 @@ var GuidelyClient = class _GuidelyClient {
2201
2741
  }
2202
2742
  navigate(url, newTab) {
2203
2743
  try {
2744
+ if (this.openPrototypeTest(url)) return;
2204
2745
  if (newTab) window.open(url, "_blank", "noopener");
2205
2746
  else location.assign(url);
2206
2747
  } catch (err) {
@@ -2225,7 +2766,13 @@ var GuidelyClient = class _GuidelyClient {
2225
2766
  this.resourceCenterScope = scope;
2226
2767
  this.resourceCenter = renderResourceCenter({
2227
2768
  scope: scope.el,
2228
- config: rc,
2769
+ config: localizeResourceCenter(rc, this.locales(), this.defaultLocale()),
2770
+ evaluate: (rules) => evaluateRules(rules, this.getContext()),
2771
+ announcementsSeenAt: () => this.state.announcementsSeenAt ?? null,
2772
+ onAnnouncementsSeen: () => {
2773
+ this.state.announcementsSeenAt = nowIso();
2774
+ this.persistState();
2775
+ },
2229
2776
  guides: this.config.guides.map((g) => ({ id: g.id, key: g.key, name: g.name, kind: g.kind })),
2230
2777
  search: (q) => this.searchResourceCenter(q),
2231
2778
  audienceIds: () => this.state.audienceIds,
@@ -2270,7 +2817,8 @@ var GuidelyClient = class _GuidelyClient {
2270
2817
  projectId: this.options.projectId,
2271
2818
  anonymousId: () => this.anonymousId,
2272
2819
  userId: () => this.user?.id || null,
2273
- pageUrl: () => this.collectUrls() ? location.href : void 0
2820
+ pageUrl: () => this.collectUrls() ? this.pageHref() : void 0,
2821
+ locale: () => this.rcLocale()
2274
2822
  }),
2275
2823
  visitor: () => this.messengerVisitor(),
2276
2824
  brand: { logoUrl: this.theme().logoUrl ?? null, name: this.theme().brandName ?? null },
@@ -2279,7 +2827,8 @@ var GuidelyClient = class _GuidelyClient {
2279
2827
  const seen = readJson(this.store, KEYS.newsSeen(this.options.projectId), []);
2280
2828
  writeJson(this.store, KEYS.newsSeen(this.options.projectId), Array.from(/* @__PURE__ */ new Set([...ids, ...Array.isArray(seen) ? seen : []])).slice(0, 200));
2281
2829
  },
2282
- resourceCenter: this.config.resourceCenter,
2830
+ resourceCenter: this.config.resourceCenter ? localizeResourceCenter(this.config.resourceCenter, this.locales(), this.defaultLocale()) : null,
2831
+ evaluate: (rules) => evaluateRules(rules, this.getContext()),
2283
2832
  audienceIds: () => this.state.audienceIds,
2284
2833
  guides: this.config.guides.map((g) => ({ id: g.id, key: g.key, name: g.name, kind: g.kind })),
2285
2834
  onStartGuide: (idOrKey) => this.startGuide(idOrKey),
@@ -2303,7 +2852,7 @@ var GuidelyClient = class _GuidelyClient {
2303
2852
  /** What the messenger personalises from, computed fresh on every render. */
2304
2853
  messengerVisitor() {
2305
2854
  const ctx = this.getContext();
2306
- const url = location.href;
2855
+ const url = this.pageHref();
2307
2856
  const seen = readJson(this.store, KEYS.newsSeen(this.options.projectId), []);
2308
2857
  const visitor = buildMessengerVisitor({
2309
2858
  guides: this.config?.guides ?? [],
@@ -2318,7 +2867,20 @@ var GuidelyClient = class _GuidelyClient {
2318
2867
  },
2319
2868
  newsSeen: Array.isArray(seen) ? seen : []
2320
2869
  });
2321
- return { ...visitor, identified: !!this.user?.id };
2870
+ const rc = this.config?.resourceCenter;
2871
+ const rcNews = rc?.announcements?.length ? visibleAnnouncements(localizeResourceCenter(rc, this.locales(), this.defaultLocale()).announcements, this.state.audienceIds).map((a) => ({
2872
+ id: `rc_${a.id}`,
2873
+ key: a.guideId ?? "",
2874
+ title: a.title,
2875
+ body: plainText(a.body),
2876
+ imageUrl: null,
2877
+ publishedAt: a.publishedAt,
2878
+ unread: !(Array.isArray(seen) && seen.includes(`rc_${a.id}`)),
2879
+ articleId: a.articleId ?? null,
2880
+ url: a.url ?? null
2881
+ })) : [];
2882
+ const news = rcNews.length ? [...visitor.news, ...rcNews].sort((a, b) => b.publishedAt.localeCompare(a.publishedAt)).slice(0, 10) : visitor.news;
2883
+ return { ...visitor, news, identified: !!this.user?.id };
2322
2884
  }
2323
2885
  destroyMessenger() {
2324
2886
  this.messenger?.destroy();
@@ -2326,9 +2888,17 @@ var GuidelyClient = class _GuidelyClient {
2326
2888
  this.messenger = null;
2327
2889
  this.messengerScope = null;
2328
2890
  }
2891
+ /** The resource center's language for this visitor, or null for the default language. */
2892
+ rcLocale() {
2893
+ const rc = this.config?.resourceCenter;
2894
+ if (!rc) return null;
2895
+ const locale = localizeResourceCenter(rc, this.locales(), this.defaultLocale()).locale;
2896
+ return locale.toLowerCase() === this.defaultLocale().toLowerCase() ? null : locale;
2897
+ }
2329
2898
  async loadArticle(id) {
2330
2899
  try {
2331
- const res = await this.api.get(`/articles/${encodeURIComponent(id)}`);
2900
+ const locale = this.rcLocale();
2901
+ const res = await this.api.get(`/articles/${encodeURIComponent(id)}`, locale ? { params: { locale } } : void 0);
2332
2902
  return res.ok && res.data ? res.data.article : null;
2333
2903
  } catch {
2334
2904
  return null;
@@ -2373,7 +2943,7 @@ var GuidelyClient = class _GuidelyClient {
2373
2943
  }
2374
2944
  this.persistState();
2375
2945
  this.widgets.get(guide.id)?.checklist?.setProgress(this.getChecklistProgress(guide.id));
2376
- 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 });
2946
+ 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 });
2377
2947
  this.emitter.emit("checklist:task", { guide, task, progress: this.getChecklistProgress(guide.id) });
2378
2948
  if (complete) {
2379
2949
  this.emitter.emit("checklist:complete", guide);
@@ -2393,7 +2963,7 @@ var GuidelyClient = class _GuidelyClient {
2393
2963
  else if (kind === "guide_completed") hit = value.id === task.completionValue || value.key === task.completionValue;
2394
2964
  else if (kind === "click") {
2395
2965
  try {
2396
- hit = !!value.closest(task.completionValue);
2966
+ hit = !!closestComposed4(value, task.completionValue);
2397
2967
  } catch {
2398
2968
  hit = false;
2399
2969
  }
@@ -2413,7 +2983,7 @@ var GuidelyClient = class _GuidelyClient {
2413
2983
  progress.dismissedAt = nowIso();
2414
2984
  this.persistState();
2415
2985
  this.enqueue("checklist_dismissed", { guideId: guide.id, guideVersion: guide.version });
2416
- 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 });
2986
+ 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 });
2417
2987
  this.widgets.get(guide.id)?.destroy();
2418
2988
  this.widgets.delete(guide.id);
2419
2989
  }
@@ -2504,6 +3074,18 @@ var GuidelyClient = class _GuidelyClient {
2504
3074
  closeMessenger() {
2505
3075
  this.messenger?.close();
2506
3076
  }
3077
+ /**
3078
+ * Opens a Guidely prototype test (its `/validate/<token>` URL on this SDK's API origin, or the bare
3079
+ * token) in a full-screen overlay, attributed to the current user. Returns false, doing nothing, for
3080
+ * any other URL. Guide buttons that open a test URL use this automatically.
3081
+ */
3082
+ openPrototypeTest(urlOrToken) {
3083
+ if (typeof document === "undefined") return false;
3084
+ const url = matchPrototypeTestUrl(urlOrToken, this.options.apiUrl);
3085
+ if (!url) return false;
3086
+ openPrototypeOverlay(url, { key: this.options.projectId, anonymousId: this.anonymousId, userId: this.user?.id ?? null });
3087
+ return true;
3088
+ }
2507
3089
  /**
2508
3090
  * Opens the complaint form in the messenger, optionally with a category picked (its id from
2509
3091
  * Complaints → Categories). Does nothing when the project doesn't take complaints.
@@ -2512,6 +3094,14 @@ var GuidelyClient = class _GuidelyClient {
2512
3094
  if (!this.config?.messenger?.enabled || !this.config.messenger.complaints?.categories.length) return;
2513
3095
  void this.loadMessenger().then((m) => m?.openComplaint(categoryId));
2514
3096
  }
3097
+ /**
3098
+ * Opens the Ideas tab in the messenger (feature requests and voting), or one idea by its id.
3099
+ * Does nothing when the project hasn't turned on Ideas.
3100
+ */
3101
+ openIdeas(requestId) {
3102
+ if (!this.config?.messenger?.enabled || !this.config.messenger.ideas) return;
3103
+ void this.loadMessenger().then((m) => m?.openIdeas(requestId));
3104
+ }
2515
3105
  /** Opens a new messenger conversation, optionally pre-filling the message. */
2516
3106
  showNewMessage(text) {
2517
3107
  if (!this.config?.messenger?.enabled) return;
@@ -2541,14 +3131,15 @@ var GuidelyClient = class _GuidelyClient {
2541
3131
  const device = detectDevice(navigator.userAgent, window.innerWidth);
2542
3132
  const account = this.user?.account;
2543
3133
  return {
2544
- url: location.href,
2545
- path: location.pathname,
3134
+ url: this.pageHref(),
3135
+ path: pathOf2(this.pageHref()),
2546
3136
  deviceType: device.type,
2547
3137
  browser: device.browser,
2548
3138
  os: device.os,
2549
3139
  locale: navigator.language || void 0,
2550
3140
  environment: this.options.environment,
2551
3141
  platform: "web",
3142
+ ...this.options.appVersion ? { appVersion: this.options.appVersion } : {},
2552
3143
  ...this.user?.traits ?? {},
2553
3144
  id: this.user?.id || this.anonymousId,
2554
3145
  anonymousId: this.anonymousId,
@@ -2708,18 +3299,18 @@ var GuidelyClient = class _GuidelyClient {
2708
3299
  // ---------------------------------------------------------------------------
2709
3300
  /** Creates (or returns the existing) client and starts initialisation. */
2710
3301
  static init(options) {
2711
- if (current && !current.destroyed && current.options.projectId === options.projectId) return current;
2712
- current?.destroy();
2713
- current = new _GuidelyClient(options);
2714
- void current.init();
2715
- return current;
3302
+ if (current2 && !current2.destroyed && current2.options.projectId === options.projectId) return current2;
3303
+ current2?.destroy();
3304
+ current2 = new _GuidelyClient(options);
3305
+ void current2.init();
3306
+ return current2;
2716
3307
  }
2717
3308
  /** The most recently created client, if any. */
2718
3309
  static get() {
2719
- return current && !current.destroyed ? current : null;
3310
+ return current2 && !current2.destroyed ? current2 : null;
2720
3311
  }
2721
3312
  };
2722
- var current = null;
3313
+ var current2 = null;
2723
3314
  function webOnly(config) {
2724
3315
  return config.guides.every((g) => supportsPlatform(g.config, "web")) ? config : { ...config, guides: config.guides.filter((g) => supportsPlatform(g.config, "web")) };
2725
3316
  }
@@ -2741,7 +3332,7 @@ function externalReferrer() {
2741
3332
  }
2742
3333
  function createGuidely(options) {
2743
3334
  const client = new GuidelyClient(options);
2744
- current = client;
3335
+ current2 = client;
2745
3336
  void client.init();
2746
3337
  return client;
2747
3338
  }
@@ -2763,18 +3354,25 @@ export {
2763
3354
  GuidelyClient,
2764
3355
  HEARTBEAT_MS,
2765
3356
  KEYS,
3357
+ PROTOTYPE_CLOSE_MESSAGE,
2766
3358
  SDK_NAME,
2767
3359
  SDK_VERSION,
2768
3360
  areMutuallyExclusive,
2769
3361
  buildContext,
2770
3362
  captureSnapshot,
3363
+ countsTowardThrottle,
2771
3364
  createGuidely,
2772
3365
  createStorage,
2773
3366
  index_default as default,
2774
3367
  detectDevice,
3368
+ isAppUrl,
2775
3369
  isExclusiveGuide,
2776
3370
  isFrequencyEligible,
3371
+ isThrottleEligible,
2777
3372
  isWithinSchedule,
3373
+ matchPrototypeTestUrl,
3374
+ normalizeAppUrl,
3375
+ pruneLaunches,
2778
3376
  watchNavigation
2779
3377
  };
2780
3378
  //# sourceMappingURL=index.js.map