@guidelytech/sdk-core 0.8.0 → 0.10.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 (44) hide show
  1. package/README.md +15 -4
  2. package/dist/{capture-C3faAYb7.d.cts → capture-369s4YYT.d.ts} +2 -1
  3. package/dist/{capture-Dqd7up2n.d.ts → capture-DVJyQ2JZ.d.cts} +2 -1
  4. package/dist/capture.cjs +2 -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-C4WWSF67.js +374 -0
  11. package/dist/chunk-C4WWSF67.js.map +1 -0
  12. package/dist/{chunk-GDF6RCHQ.js → chunk-GDJB466J.js} +121 -19
  13. package/dist/chunk-GDJB466J.js.map +1 -0
  14. package/dist/headless.cjs +281 -40
  15. package/dist/headless.cjs.map +1 -1
  16. package/dist/headless.d.cts +43 -3
  17. package/dist/headless.d.ts +43 -3
  18. package/dist/headless.js +16 -8
  19. package/dist/health-DFHUstWd.d.cts +88 -0
  20. package/dist/health-DFHUstWd.d.ts +88 -0
  21. package/dist/{http-47tiAlOM.d.ts → http-D0dCcpLN.d.cts} +10 -35
  22. package/dist/{http-47tiAlOM.d.cts → http-DJd8jRhK.d.ts} +10 -35
  23. package/dist/index.cjs +380 -81
  24. package/dist/index.cjs.map +1 -1
  25. package/dist/index.d.cts +15 -5
  26. package/dist/index.d.ts +15 -5
  27. package/dist/index.js +106 -36
  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-DqhNl5Am.d.ts → types-B5ecgHUV.d.cts} +26 -4
  36. package/dist/{types-DbKQPvFt.d.cts → types-BtPovimB.d.ts} +26 -4
  37. package/package.json +7 -7
  38. package/dist/chunk-AKNE7D5M.js +0 -160
  39. package/dist/chunk-AKNE7D5M.js.map +0 -1
  40. package/dist/chunk-CYAG6ZYX.js +0 -85
  41. package/dist/chunk-CYAG6ZYX.js.map +0 -1
  42. package/dist/chunk-GDF6RCHQ.js.map +0 -1
  43. package/dist/types-RyeRQd9f.d.ts +0 -226
  44. package/dist/types-d4eOerxi.d.cts +0 -226
package/dist/index.d.cts CHANGED
@@ -4,14 +4,15 @@ export { AppSnapshot, AutocaptureSettings, ExceptionProperties, GuideConfig, Gui
4
4
  import { FlagsClient, JsonValue, FlagValue, FlagDetails } from '@guidelytech/flags';
5
5
  export { Attributes, EvaluationResult, FlagDetails, FlagsBootstrap, FlagsClient, FlagsClientOptions, FlagsPayload, JsonValue } from '@guidelytech/flags';
6
6
  import { MessengerTab } from '@guidelytech/guide-runtime/messenger';
7
- import { j as GuidelyOptions, c as GuidelyAccount, a as ConsentState, C as ChecklistProgressState, k as GuidelyEvents, e as GuidelyUser, G as GuideLocalState, S as SessionState } from './types-DbKQPvFt.cjs';
8
- export { E as EventQueue, b as EventQueueOptions, l as GuideStepInfo, d as GuidelyEnvironment, m as GuidelyEventName, P as PersistedState, f as areMutuallyExclusive, i as isExclusiveGuide, g as isFrequencyEligible, h as isWithinSchedule } from './types-DbKQPvFt.cjs';
9
- export { K as KEYS, b as KeyValueStore, S as StorageKind, d as createStorage } from './http-47tiAlOM.cjs';
10
- export { C as CaptureMarker, a as CaptureOutgoingMessage } from './capture-C3faAYb7.cjs';
7
+ import { k as GuidelyOptions, c as GuidelyAccount, a as ConsentState, C as ChecklistProgressState, l as GuidelyEvents, e as GuidelyUser, G as GuideLocalState, S as SessionState } from './types-B5ecgHUV.cjs';
8
+ export { E as EventQueue, b as EventQueueOptions, m as GuideStepInfo, d as GuidelyEnvironment, n as GuidelyEventName, P as PersistedState, f as areMutuallyExclusive, i as isExclusiveGuide, h as isFrequencyEligible, j as isWithinSchedule } from './types-B5ecgHUV.cjs';
9
+ export { a as KEYS, K as KeyValueStore, c as StorageKind, d as createStorage } from './health-DFHUstWd.cjs';
10
+ export { C as CaptureMarker, a as CaptureOutgoingMessage } from './capture-DVJyQ2JZ.cjs';
11
11
  export { CAPTURE_PARAM } from '@guidelytech/shared/runtime';
12
+ import './http-D0dCcpLN.cjs';
12
13
 
13
14
  declare const SDK_NAME = "@guidelytech/sdk-core";
14
- declare const SDK_VERSION = "0.8.0";
15
+ declare const SDK_VERSION = "0.10.0";
15
16
  declare const DEFAULT_API_URL = "https://api.guidely.app";
16
17
  /**
17
18
  * Framework-independent Guidely client. Create one per page with {@link createGuidely} or
@@ -24,6 +25,8 @@ declare class GuidelyClient {
24
25
  private readonly sessionStore;
25
26
  private readonly emitter;
26
27
  private readonly queue;
28
+ /** Survey answers and guide/checklist progress, kept until the server accepts them. */
29
+ private readonly outbox;
27
30
  private config;
28
31
  private etag;
29
32
  private anonymousId;
@@ -85,6 +88,8 @@ declare class GuidelyClient {
85
88
  private wireErrorListeners;
86
89
  /** Re-fetches the published config and re-evaluates triggers. */
87
90
  refresh(): Promise<void>;
91
+ /** Re-renders after a config change (refresh, or a config that arrived after start). */
92
+ private applyConfigChange;
88
93
  /** Tears everything down: guides, widgets, listeners, timers, host element. Re-init is allowed via a new client. */
89
94
  destroy(): void;
90
95
  /** Identifies the end user and loads server-held state (audiences, guide history, checklist progress). */
@@ -214,6 +219,11 @@ declare class GuidelyClient {
214
219
  /** Opens the Scout messenger (when enabled for the project), optionally on a tab. */
215
220
  openMessenger(tab?: MessengerTab): void;
216
221
  closeMessenger(): void;
222
+ /**
223
+ * Opens the complaint form in the messenger, optionally with a category picked (its id from
224
+ * Complaints → Categories). Does nothing when the project doesn't take complaints.
225
+ */
226
+ openComplaint(categoryId?: string): void;
217
227
  /** Opens a new messenger conversation, optionally pre-filling the message. */
218
228
  showNewMessage(text?: string): void;
219
229
  /** Boolean flag value for the current user; `fallback` (default `false`) until loaded or when missing. */
package/dist/index.d.ts CHANGED
@@ -4,14 +4,15 @@ export { AppSnapshot, AutocaptureSettings, ExceptionProperties, GuideConfig, Gui
4
4
  import { FlagsClient, JsonValue, FlagValue, FlagDetails } from '@guidelytech/flags';
5
5
  export { Attributes, EvaluationResult, FlagDetails, FlagsBootstrap, FlagsClient, FlagsClientOptions, FlagsPayload, JsonValue } from '@guidelytech/flags';
6
6
  import { MessengerTab } from '@guidelytech/guide-runtime/messenger';
7
- import { j as GuidelyOptions, c as GuidelyAccount, a as ConsentState, C as ChecklistProgressState, k as GuidelyEvents, e as GuidelyUser, G as GuideLocalState, S as SessionState } from './types-DqhNl5Am.js';
8
- export { E as EventQueue, b as EventQueueOptions, l as GuideStepInfo, d as GuidelyEnvironment, m as GuidelyEventName, P as PersistedState, f as areMutuallyExclusive, i as isExclusiveGuide, g as isFrequencyEligible, h as isWithinSchedule } from './types-DqhNl5Am.js';
9
- export { K as KEYS, b as KeyValueStore, S as StorageKind, d as createStorage } from './http-47tiAlOM.js';
10
- export { C as CaptureMarker, a as CaptureOutgoingMessage } from './capture-Dqd7up2n.js';
7
+ import { k as GuidelyOptions, c as GuidelyAccount, a as ConsentState, C as ChecklistProgressState, l as GuidelyEvents, e as GuidelyUser, G as GuideLocalState, S as SessionState } from './types-BtPovimB.js';
8
+ export { E as EventQueue, b as EventQueueOptions, m as GuideStepInfo, d as GuidelyEnvironment, n as GuidelyEventName, P as PersistedState, f as areMutuallyExclusive, i as isExclusiveGuide, h as isFrequencyEligible, j as isWithinSchedule } from './types-BtPovimB.js';
9
+ export { a as KEYS, K as KeyValueStore, c as StorageKind, d as createStorage } from './health-DFHUstWd.js';
10
+ export { C as CaptureMarker, a as CaptureOutgoingMessage } from './capture-369s4YYT.js';
11
11
  export { CAPTURE_PARAM } from '@guidelytech/shared/runtime';
12
+ import './http-DJd8jRhK.js';
12
13
 
13
14
  declare const SDK_NAME = "@guidelytech/sdk-core";
14
- declare const SDK_VERSION = "0.8.0";
15
+ declare const SDK_VERSION = "0.10.0";
15
16
  declare const DEFAULT_API_URL = "https://api.guidely.app";
16
17
  /**
17
18
  * Framework-independent Guidely client. Create one per page with {@link createGuidely} or
@@ -24,6 +25,8 @@ declare class GuidelyClient {
24
25
  private readonly sessionStore;
25
26
  private readonly emitter;
26
27
  private readonly queue;
28
+ /** Survey answers and guide/checklist progress, kept until the server accepts them. */
29
+ private readonly outbox;
27
30
  private config;
28
31
  private etag;
29
32
  private anonymousId;
@@ -85,6 +88,8 @@ declare class GuidelyClient {
85
88
  private wireErrorListeners;
86
89
  /** Re-fetches the published config and re-evaluates triggers. */
87
90
  refresh(): Promise<void>;
91
+ /** Re-renders after a config change (refresh, or a config that arrived after start). */
92
+ private applyConfigChange;
88
93
  /** Tears everything down: guides, widgets, listeners, timers, host element. Re-init is allowed via a new client. */
89
94
  destroy(): void;
90
95
  /** Identifies the end user and loads server-held state (audiences, guide history, checklist progress). */
@@ -214,6 +219,11 @@ declare class GuidelyClient {
214
219
  /** Opens the Scout messenger (when enabled for the project), optionally on a tab. */
215
220
  openMessenger(tab?: MessengerTab): void;
216
221
  closeMessenger(): void;
222
+ /**
223
+ * Opens the complaint form in the messenger, optionally with a category picked (its id from
224
+ * Complaints → Categories). Does nothing when the project doesn't take complaints.
225
+ */
226
+ openComplaint(categoryId?: string): void;
217
227
  /** Opens a new messenger conversation, optionally pre-filling the message. */
218
228
  showNewMessage(text?: string): void;
219
229
  /** Boolean flag value for the current user; `fallback` (default `false`) until loaded or when missing. */
package/dist/index.js CHANGED
@@ -1,26 +1,28 @@
1
1
  import {
2
2
  Emitter,
3
3
  EventQueue,
4
+ Outbox,
4
5
  areMutuallyExclusive,
6
+ dismissedAfterMerge,
5
7
  isExclusiveGuide,
6
8
  isFrequencyEligible,
7
9
  isWithinSchedule
8
- } from "./chunk-GDF6RCHQ.js";
10
+ } from "./chunk-GDJB466J.js";
9
11
  import {
12
+ Api,
10
13
  KEYS,
14
+ __publicField,
11
15
  createStorage,
16
+ postWithRetry,
12
17
  readJson,
13
18
  writeJson
14
- } from "./chunk-CYAG6ZYX.js";
15
- import {
16
- Api,
17
- __publicField,
18
- postWithRetry
19
- } from "./chunk-AKNE7D5M.js";
19
+ } from "./chunk-C4WWSF67.js";
20
20
 
21
21
  // src/client.ts
22
22
  import { evaluateRules, randomId, rolloutBucket, sanitizeProperties, supportsPlatform } from "@guidelytech/shared/runtime";
23
23
  import {
24
+ closestComposed as closestComposed3,
25
+ composedTarget,
24
26
  destroyHost,
25
27
  isChecklistComplete,
26
28
  renderChecklist,
@@ -34,7 +36,7 @@ import { createScope } from "@guidelytech/guide-runtime";
34
36
  import { createFlagsClient } from "@guidelytech/flags";
35
37
 
36
38
  // src/authoring.ts
37
- import { highlightSelectors, previewStep, runGuide, startAuthoring } from "@guidelytech/guide-runtime";
39
+ import { closestComposed, highlightSelectors, previewStep, runGuide, startAuthoring } from "@guidelytech/guide-runtime";
38
40
 
39
41
  // src/snapshot.ts
40
42
  var MAX_SNAPSHOT_BYTES = 6 * 1024 * 1024;
@@ -198,7 +200,7 @@ var AuthoringBridge = class {
198
200
  masked(el) {
199
201
  return this.opts.maskSelectors.some((sel) => {
200
202
  try {
201
- return !!el.closest(sel);
203
+ return !!closestComposed(el, sel);
202
204
  } catch {
203
205
  return false;
204
206
  }
@@ -379,7 +381,7 @@ function takeCaptureToken(sessionStore) {
379
381
  var loadCapture = async () => (await import("./capture.js")).capture;
380
382
 
381
383
  // src/autocapture.ts
382
- import { isGuidelyElement, isStableId } from "@guidelytech/guide-runtime";
384
+ import { closestComposed as closestComposed2, isGuidelyElement, isStableId, SHADOW_SEPARATOR, shadowHostChain } from "@guidelytech/guide-runtime";
383
385
  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]";
384
386
  var DEFAULT_MASK_SELECTORS = ["[data-guidely-mask]", ".guidely-mask"];
385
387
  var PREFERRED_ATTRS = ["data-testid", "data-test-id", "data-test", "data-cy", "data-qa", "data-guidely-track", "data-tour"];
@@ -399,11 +401,11 @@ function validSelectors(list) {
399
401
  return out;
400
402
  }
401
403
  function isIgnoredElement(el) {
402
- return isGuidelyElement(el) || !!el.closest("[data-guidely-ignore]");
404
+ return isGuidelyElement(el) || !!closestComposed2(el, "[data-guidely-ignore]");
403
405
  }
404
406
  function interactiveElement(target) {
405
407
  try {
406
- return target.closest(INTERACTIVE_SELECTOR);
408
+ return closestComposed2(target, INTERACTIVE_SELECTOR);
407
409
  } catch {
408
410
  return null;
409
411
  }
@@ -414,14 +416,19 @@ function esc(value) {
414
416
  }
415
417
  var attr = (tag, name, value) => value ? `${tag}[${name}="${value.replace(/["\\]/g, "\\$&")}"]` : `${tag}[${name}]`;
416
418
  function elementSelector(el) {
419
+ const own = ownSelector(el);
420
+ const chain = shadowHostChain(el);
421
+ return (chain ? chain + SHADOW_SEPARATOR + own : own).slice(0, 300);
422
+ }
423
+ function ownSelector(el) {
417
424
  const tag = el.tagName.toLowerCase();
418
425
  if (el.id && isStableId(el.id)) return `#${esc(el.id)}`;
419
- for (const name of PREFERRED_ATTRS) if (el.hasAttribute(name)) return attr(tag, name, el.getAttribute(name) || "").slice(0, 300);
426
+ for (const name of PREFERRED_ATTRS) if (el.hasAttribute(name)) return attr(tag, name, el.getAttribute(name) || "");
420
427
  for (const a of Array.from(el.attributes)) {
421
- 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);
428
+ if (a.name.startsWith("data-") && !NOISY_DATA_ATTR.test(a.name) && a.value && a.value.length <= 60) return attr(tag, a.name, a.value);
422
429
  }
423
430
  const label = el.getAttribute("aria-label");
424
- if (label && label.length <= 80) return attr(tag, "aria-label", label).slice(0, 300);
431
+ if (label && label.length <= 80) return attr(tag, "aria-label", label);
425
432
  const parts = [];
426
433
  let cur = el;
427
434
  while (cur && parts.length < 4) {
@@ -437,12 +444,12 @@ function elementSelector(el) {
437
444
  parts.unshift(same.length > 1 ? `${t}:nth-of-type(${same.indexOf(node) + 1})` : t);
438
445
  cur = parent;
439
446
  }
440
- return (parts.join(" > ") || tag).slice(0, 300);
447
+ return parts.join(" > ") || tag;
441
448
  }
442
449
  function matches(el, selector, deep) {
443
450
  if (!selector) return false;
444
451
  try {
445
- return !!el.closest(selector) || deep && !!el.querySelector(selector);
452
+ return !!closestComposed2(el, selector) || deep && !!el.querySelector(selector);
446
453
  } catch {
447
454
  return false;
448
455
  }
@@ -460,7 +467,7 @@ function elementText(el, mask, maskAll) {
460
467
  return text ? text.slice(0, 100) : void 0;
461
468
  }
462
469
  function linkHref(el) {
463
- const a = el.closest("a[href]");
470
+ const a = closestComposed2(el, "a[href]");
464
471
  if (!a) return void 0;
465
472
  try {
466
473
  const u = new URL(a.getAttribute("href") || "", location.href);
@@ -730,11 +737,15 @@ function forgetConversations(store, projectId) {
730
737
  store.remove(KEYS.conversations(projectId));
731
738
  }
732
739
  var HttpError = class extends Error {
733
- constructor(status) {
734
- super(`Request failed (${status})`);
740
+ constructor(status, message) {
741
+ super(message || `Request failed (${status})`);
735
742
  __publicField(this, "status", status);
736
743
  }
737
744
  };
745
+ function serverMessage(data) {
746
+ const m = data?.error?.message;
747
+ return typeof m === "string" && m !== "Request validation failed" ? m : void 0;
748
+ }
738
749
  function createMessengerTransport(opts) {
739
750
  const { api, store, projectId } = opts;
740
751
  const tokenFor = (id) => storedConversations(store, projectId).find((c) => c.id === id)?.token ?? "";
@@ -816,6 +827,16 @@ function createMessengerTransport(opts) {
816
827
  const res = await api.get(`/articles/${encodeURIComponent(id)}`);
817
828
  return res.ok && res.data ? res.data.article : null;
818
829
  },
830
+ async fileComplaint(input) {
831
+ const headers = input.conversationId ? { [TOKEN_HEADER]: tokenFor(input.conversationId) } : {};
832
+ const res = await api.post("/complaints", { ...input, anonymousId: opts.anonymousId(), userId: opts.userId(), pageUrl: opts.pageUrl() }, { headers });
833
+ if (!res.ok || !res.data || !("complaint" in res.data)) {
834
+ if (res.status === 429) throw new HttpError(429, "You've filed several complaints just now. Please try again later.");
835
+ throw new HttpError(res.status, serverMessage(res.data) ?? "Your complaint couldn't be sent. Please try again.");
836
+ }
837
+ if (res.data.token) remember(res.data.conversation.id, res.data.token);
838
+ return res.data;
839
+ },
819
840
  async pageContent(path) {
820
841
  const res = await api.post("/messenger/page", { anonymousId: opts.anonymousId(), userId: opts.userId(), path: path.split(/[?#]/)[0] || "/" });
821
842
  if (!res.ok || !res.data) throw new HttpError(res.status);
@@ -1013,11 +1034,22 @@ var loadRecorder = async () => (await import("./recorder.js")).recorder;
1013
1034
 
1014
1035
  // src/client.ts
1015
1036
  var SDK_NAME = "@guidelytech/sdk-core";
1016
- var SDK_VERSION = "0.8.0";
1037
+ var SDK_VERSION = "0.10.0";
1017
1038
  var DEFAULT_API_URL = "https://api.guidely.app";
1018
1039
  var PAGE_SCOPED = /* @__PURE__ */ new Set(["page_load", "url_match", "element_click", "element_visible", "delay"]);
1019
1040
  var DAY_MS = 24 * 60 * 60 * 1e3;
1020
1041
  var MAX_SESSION_EXCEPTIONS = 100;
1042
+ var CACHED_START_MS = 1500;
1043
+ function settlesWithin(promise, ms) {
1044
+ return new Promise((resolve) => {
1045
+ const timer = setTimeout(() => resolve(false), ms);
1046
+ const done = () => {
1047
+ clearTimeout(timer);
1048
+ resolve(true);
1049
+ };
1050
+ promise.then(done, done);
1051
+ });
1052
+ }
1021
1053
  function isBrowser() {
1022
1054
  return typeof window !== "undefined" && typeof document !== "undefined";
1023
1055
  }
@@ -1038,6 +1070,8 @@ var GuidelyClient = class _GuidelyClient {
1038
1070
  __publicField(this, "sessionStore");
1039
1071
  __publicField(this, "emitter");
1040
1072
  __publicField(this, "queue");
1073
+ /** Survey answers and guide/checklist progress, kept until the server accepts them. */
1074
+ __publicField(this, "outbox");
1041
1075
  __publicField(this, "config", null);
1042
1076
  __publicField(this, "etag", null);
1043
1077
  __publicField(this, "anonymousId");
@@ -1137,6 +1171,7 @@ var GuidelyClient = class _GuidelyClient {
1137
1171
  store: this.store,
1138
1172
  storageKey: KEYS.queue(this.options.projectId),
1139
1173
  enabled: () => this.analyticsEnabled(),
1174
+ health: this.api.health,
1140
1175
  send: async (events) => {
1141
1176
  const res = await this.api.post("/events", { events, sentAt: nowIso(), sdk: { name: SDK_NAME, version: SDK_VERSION } }, { keepalive: true });
1142
1177
  return { ok: res.ok, retry: !(res.status >= 400 && res.status < 500 && res.status !== 429) };
@@ -1144,6 +1179,7 @@ var GuidelyClient = class _GuidelyClient {
1144
1179
  beacon: (events) => this.api.beacon("/events", { events, sentAt: nowIso(), sdk: { name: SDK_NAME, version: SDK_VERSION } }),
1145
1180
  onError: (err) => this.reportError(err)
1146
1181
  });
1182
+ this.outbox = new Outbox({ api: this.api, store: this.store, storageKey: KEYS.outbox(this.options.projectId), health: this.api.health });
1147
1183
  if (options.user) this.user = { ...options.user };
1148
1184
  this.flags = this.createFlags(options.flags);
1149
1185
  }
@@ -1201,12 +1237,15 @@ var GuidelyClient = class _GuidelyClient {
1201
1237
  const cached = readJson(this.store, KEYS.config(this.options.projectId), null);
1202
1238
  this.etag = this.store.get(KEYS.etag(this.options.projectId));
1203
1239
  if (cached && cached.guides) this.config = webOnly(cached);
1204
- await this.fetchConfig();
1240
+ const before = this.config?.configVersion;
1241
+ const fresh = this.fetchConfig();
1242
+ if (!this.config) await fresh;
1243
+ else if (!await settlesWithin(fresh, CACHED_START_MS)) void fresh.then(() => this.applyConfigChange(before));
1205
1244
  if (this.destroyed) return;
1206
1245
  this.wireListeners();
1207
1246
  if (!this.identitySettled) {
1208
- if (this.options.user) await this.identify(this.options.user.id, this.options.user.traits, this.options.user.account);
1209
- else if (this.user?.id) await this.identify(this.user.id, this.user.traits, this.user.account);
1247
+ const known = this.options.user ?? (this.user?.id ? this.user : null);
1248
+ if (known) await settlesWithin(this.identify(known.id, known.traits, known.account), CACHED_START_MS);
1210
1249
  }
1211
1250
  if (this.destroyed) return;
1212
1251
  this.isReady = true;
@@ -1311,7 +1350,12 @@ var GuidelyClient = class _GuidelyClient {
1311
1350
  win.removeEventListener("scroll", onScroll);
1312
1351
  doc.removeEventListener("keydown", onKey, true);
1313
1352
  });
1314
- const refresh = setInterval(() => void this.refresh(), this.options.configRefreshMs);
1353
+ const refresh = setInterval(() => {
1354
+ if (this.api.health.canSend()) void this.refresh();
1355
+ }, this.options.configRefreshMs);
1356
+ const onOnline = () => this.api.health.online();
1357
+ win.addEventListener("online", onOnline);
1358
+ this.cleanups.push(() => win.removeEventListener("online", onOnline));
1315
1359
  this.cleanups.push(() => clearInterval(refresh));
1316
1360
  const poll = setInterval(() => {
1317
1361
  if (!this.isReady || !this.config || this.editing() || doc.visibilityState === "hidden") return;
@@ -1344,6 +1388,11 @@ var GuidelyClient = class _GuidelyClient {
1344
1388
  if (this.destroyed) return;
1345
1389
  const before = this.config?.configVersion;
1346
1390
  await this.fetchConfig();
1391
+ this.applyConfigChange(before);
1392
+ }
1393
+ /** Re-renders after a config change (refresh, or a config that arrived after start). */
1394
+ applyConfigChange(before) {
1395
+ if (this.destroyed || !this.isReady) return;
1347
1396
  if (this.config && this.config.configVersion !== before) {
1348
1397
  this.renderPersistentWidgets();
1349
1398
  this.evaluate("ready");
@@ -1378,6 +1427,7 @@ var GuidelyClient = class _GuidelyClient {
1378
1427
  this.replay = null;
1379
1428
  this.queue.flushSync();
1380
1429
  this.queue.destroy();
1430
+ this.outbox.destroy();
1381
1431
  this.flags.destroy();
1382
1432
  this.emitter.clear();
1383
1433
  destroyHost();
@@ -1421,7 +1471,8 @@ var GuidelyClient = class _GuidelyClient {
1421
1471
  checklists[id] = {
1422
1472
  completedTaskIds: Array.from(/* @__PURE__ */ new Set([...local?.completedTaskIds ?? [], ...p.completedTaskIds ?? []])),
1423
1473
  completedAt: local?.completedAt ?? p.completedAt ?? null,
1424
- dismissedAt: local?.dismissedAt ?? p.dismissedAt ?? null
1474
+ dismissedAt: dismissedAfterMerge(local, p),
1475
+ dismissResetAt: p.dismissResetAt ?? local?.dismissResetAt ?? null
1425
1476
  };
1426
1477
  }
1427
1478
  this.state = {
@@ -1579,7 +1630,7 @@ var GuidelyClient = class _GuidelyClient {
1579
1630
  if (changed) void this.queue.flush();
1580
1631
  }
1581
1632
  handleDocumentClick(e) {
1582
- const target = e.target;
1633
+ const target = composedTarget(e);
1583
1634
  if (!target || typeof target.closest !== "function") return;
1584
1635
  this.autocaptureClick(e, target);
1585
1636
  this.completeChecklistTasksBy("click", target);
@@ -1674,7 +1725,7 @@ var GuidelyClient = class _GuidelyClient {
1674
1725
  }
1675
1726
  /** Bins one pointer sample into the heatmap buffer. Not session activity. */
1676
1727
  sampleMove(e) {
1677
- const target = e.target;
1728
+ const target = composedTarget(e);
1678
1729
  if (document.visibilityState === "hidden" || !target?.closest || !this.autocaptureOn("mouseMovement") || isIgnoredElement(target)) return;
1679
1730
  const url = location.href;
1680
1731
  if (this.heatmap.url !== url) this.flushHeatmap();
@@ -1799,7 +1850,9 @@ var GuidelyClient = class _GuidelyClient {
1799
1850
  this.persistSession();
1800
1851
  this.replay?.stop({ discard: true });
1801
1852
  },
1802
- onError: (err) => this.reportError(err)
1853
+ onError: (err) => this.reportError(err),
1854
+ health: this.api.health,
1855
+ stash: this.sessionStore
1803
1856
  });
1804
1857
  this.syncReplay();
1805
1858
  },
@@ -1889,7 +1942,7 @@ var GuidelyClient = class _GuidelyClient {
1889
1942
  case "element_click":
1890
1943
  if (reason !== "click" || !t.value || !detail.target) return false;
1891
1944
  try {
1892
- return !!detail.target.closest(t.value);
1945
+ return !!closestComposed3(detail.target, t.value);
1893
1946
  } catch {
1894
1947
  return false;
1895
1948
  }
@@ -2040,7 +2093,9 @@ var GuidelyClient = class _GuidelyClient {
2040
2093
  if (reason === "completed") this.completeChecklistTasksBy("guide_completed", guide);
2041
2094
  }
2042
2095
  syncGuideState(guideId, update) {
2043
- void postWithRetry(this.api, "/guide-state", { anonymousId: this.anonymousId, userId: this.user?.id || null, updates: [{ guideId, ...update }] });
2096
+ const body = { anonymousId: this.anonymousId, userId: this.user?.id || null, updates: [{ guideId, ...update }] };
2097
+ if (update.impression) void postWithRetry(this.api, "/guide-state", body);
2098
+ else this.outbox.add(`guide:${guideId}:${update.completed ? "completed" : "dismissed"}:${this.anonymousId}`, "/guide-state", body);
2044
2099
  }
2045
2100
  show(guide, manual, stepId) {
2046
2101
  try {
@@ -2060,6 +2115,12 @@ var GuidelyClient = class _GuidelyClient {
2060
2115
  ctx: () => this.getContext(),
2061
2116
  startAtStep: stepId,
2062
2117
  eventBus: this.eventBus,
2118
+ // Started from Help, Scout or startGuide(): steps on other pages offer to take the user there.
2119
+ promptForUrl: manual,
2120
+ // Also while a step waits for its page, so a link that loads that page resumes on it.
2121
+ onPosition: (id) => {
2122
+ if (exclusive) writeJson(this.sessionStore, KEYS.active, { guideId: guide.id, stepId: id, manual });
2123
+ },
2063
2124
  onError: (err) => this.reportError(err),
2064
2125
  onEvent: (e) => {
2065
2126
  const step = e.stepId ? guide.config.steps.find((s) => s.id === e.stepId) : void 0;
@@ -2162,7 +2223,7 @@ var GuidelyClient = class _GuidelyClient {
2162
2223
  this.persistState();
2163
2224
  this.recordEnd(guide, "completed");
2164
2225
  this.enqueue("survey_submitted", { guideId: guide.id, guideVersion: guide.version, properties: { responseId } });
2165
- 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 });
2226
+ 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 });
2166
2227
  this.emitter.emit("survey:submit", { guide, answers, responseId });
2167
2228
  this.emitter.emit("guide:complete", guide);
2168
2229
  widget.showThankYou();
@@ -2283,7 +2344,7 @@ var GuidelyClient = class _GuidelyClient {
2283
2344
  const ctx = this.getContext();
2284
2345
  const url = location.href;
2285
2346
  const seen = readJson(this.store, KEYS.newsSeen(this.options.projectId), []);
2286
- return buildMessengerVisitor({
2347
+ const visitor = buildMessengerVisitor({
2287
2348
  guides: this.config?.guides ?? [],
2288
2349
  state: this.state,
2289
2350
  traits: this.user?.traits,
@@ -2296,6 +2357,7 @@ var GuidelyClient = class _GuidelyClient {
2296
2357
  },
2297
2358
  newsSeen: Array.isArray(seen) ? seen : []
2298
2359
  });
2360
+ return { ...visitor, identified: !!this.user?.id };
2299
2361
  }
2300
2362
  destroyMessenger() {
2301
2363
  this.messenger?.destroy();
@@ -2350,7 +2412,7 @@ var GuidelyClient = class _GuidelyClient {
2350
2412
  }
2351
2413
  this.persistState();
2352
2414
  this.widgets.get(guide.id)?.checklist?.setProgress(this.getChecklistProgress(guide.id));
2353
- 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 });
2415
+ 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 });
2354
2416
  this.emitter.emit("checklist:task", { guide, task, progress: this.getChecklistProgress(guide.id) });
2355
2417
  if (complete) {
2356
2418
  this.emitter.emit("checklist:complete", guide);
@@ -2370,7 +2432,7 @@ var GuidelyClient = class _GuidelyClient {
2370
2432
  else if (kind === "guide_completed") hit = value.id === task.completionValue || value.key === task.completionValue;
2371
2433
  else if (kind === "click") {
2372
2434
  try {
2373
- hit = !!value.closest(task.completionValue);
2435
+ hit = !!closestComposed3(value, task.completionValue);
2374
2436
  } catch {
2375
2437
  hit = false;
2376
2438
  }
@@ -2390,7 +2452,7 @@ var GuidelyClient = class _GuidelyClient {
2390
2452
  progress.dismissedAt = nowIso();
2391
2453
  this.persistState();
2392
2454
  this.enqueue("checklist_dismissed", { guideId: guide.id, guideVersion: guide.version });
2393
- 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 });
2455
+ 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 });
2394
2456
  this.widgets.get(guide.id)?.destroy();
2395
2457
  this.widgets.delete(guide.id);
2396
2458
  }
@@ -2481,6 +2543,14 @@ var GuidelyClient = class _GuidelyClient {
2481
2543
  closeMessenger() {
2482
2544
  this.messenger?.close();
2483
2545
  }
2546
+ /**
2547
+ * Opens the complaint form in the messenger, optionally with a category picked (its id from
2548
+ * Complaints → Categories). Does nothing when the project doesn't take complaints.
2549
+ */
2550
+ openComplaint(categoryId) {
2551
+ if (!this.config?.messenger?.enabled || !this.config.messenger.complaints?.categories.length) return;
2552
+ void this.loadMessenger().then((m) => m?.openComplaint(categoryId));
2553
+ }
2484
2554
  /** Opens a new messenger conversation, optionally pre-filling the message. */
2485
2555
  showNewMessage(text) {
2486
2556
  if (!this.config?.messenger?.enabled) return;