@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.cjs CHANGED
@@ -57,7 +57,7 @@ __export(index_exports, {
57
57
  module.exports = __toCommonJS(index_exports);
58
58
 
59
59
  // src/client.ts
60
- var import_runtime13 = require("@guidelytech/shared/runtime");
60
+ var import_runtime15 = require("@guidelytech/shared/runtime");
61
61
  var import_guide_runtime4 = require("@guidelytech/guide-runtime");
62
62
  var import_guide_runtime5 = require("@guidelytech/guide-runtime");
63
63
  var import_flags = require("@guidelytech/flags");
@@ -206,6 +206,8 @@ var KEYS = {
206
206
  state: (key) => `guidely:state:${key}`,
207
207
  user: (key) => `guidely:user:${key}`,
208
208
  queue: (key) => `guidely:queue:${key}`,
209
+ /** Survey answers and guide/checklist progress not yet accepted by the server. */
210
+ outbox: (key) => `guidely:outbox:${key}`,
209
211
  session: "guidely:session",
210
212
  active: "guidely:active",
211
213
  authoring: "guidely:authoring",
@@ -304,7 +306,7 @@ var AuthoringBridge = class {
304
306
  masked(el) {
305
307
  return this.opts.maskSelectors.some((sel) => {
306
308
  try {
307
- return !!el.closest(sel);
309
+ return !!(0, import_guide_runtime.closestComposed)(el, sel);
308
310
  } catch {
309
311
  return false;
310
312
  }
@@ -505,11 +507,11 @@ function validSelectors(list) {
505
507
  return out;
506
508
  }
507
509
  function isIgnoredElement(el) {
508
- return (0, import_guide_runtime2.isGuidelyElement)(el) || !!el.closest("[data-guidely-ignore]");
510
+ return (0, import_guide_runtime2.isGuidelyElement)(el) || !!(0, import_guide_runtime2.closestComposed)(el, "[data-guidely-ignore]");
509
511
  }
510
512
  function interactiveElement(target) {
511
513
  try {
512
- return target.closest(INTERACTIVE_SELECTOR);
514
+ return (0, import_guide_runtime2.closestComposed)(target, INTERACTIVE_SELECTOR);
513
515
  } catch {
514
516
  return null;
515
517
  }
@@ -520,14 +522,19 @@ function esc(value) {
520
522
  }
521
523
  var attr = (tag, name, value) => value ? `${tag}[${name}="${value.replace(/["\\]/g, "\\$&")}"]` : `${tag}[${name}]`;
522
524
  function elementSelector(el) {
525
+ const own = ownSelector(el);
526
+ const chain = (0, import_guide_runtime2.shadowHostChain)(el);
527
+ return (chain ? chain + import_guide_runtime2.SHADOW_SEPARATOR + own : own).slice(0, 300);
528
+ }
529
+ function ownSelector(el) {
523
530
  const tag = el.tagName.toLowerCase();
524
531
  if (el.id && (0, import_guide_runtime2.isStableId)(el.id)) return `#${esc(el.id)}`;
525
- for (const name of PREFERRED_ATTRS) if (el.hasAttribute(name)) return attr(tag, name, el.getAttribute(name) || "").slice(0, 300);
532
+ for (const name of PREFERRED_ATTRS) if (el.hasAttribute(name)) return attr(tag, name, el.getAttribute(name) || "");
526
533
  for (const a of Array.from(el.attributes)) {
527
- if (a.name.startsWith("data-") && !NOISY_DATA_ATTR.test(a.name) && a.value && a.value.length <= 60) return attr(tag, a.name, a.value).slice(0, 300);
534
+ if (a.name.startsWith("data-") && !NOISY_DATA_ATTR.test(a.name) && a.value && a.value.length <= 60) return attr(tag, a.name, a.value);
528
535
  }
529
536
  const label = el.getAttribute("aria-label");
530
- if (label && label.length <= 80) return attr(tag, "aria-label", label).slice(0, 300);
537
+ if (label && label.length <= 80) return attr(tag, "aria-label", label);
531
538
  const parts = [];
532
539
  let cur = el;
533
540
  while (cur && parts.length < 4) {
@@ -543,12 +550,12 @@ function elementSelector(el) {
543
550
  parts.unshift(same.length > 1 ? `${t}:nth-of-type(${same.indexOf(node) + 1})` : t);
544
551
  cur = parent;
545
552
  }
546
- return (parts.join(" > ") || tag).slice(0, 300);
553
+ return parts.join(" > ") || tag;
547
554
  }
548
555
  function matches(el, selector, deep) {
549
556
  if (!selector) return false;
550
557
  try {
551
- return !!el.closest(selector) || deep && !!el.querySelector(selector);
558
+ return !!(0, import_guide_runtime2.closestComposed)(el, selector) || deep && !!el.querySelector(selector);
552
559
  } catch {
553
560
  return false;
554
561
  }
@@ -566,7 +573,7 @@ function elementText(el, mask, maskAll) {
566
573
  return text ? text.slice(0, 100) : void 0;
567
574
  }
568
575
  function linkHref(el) {
569
- const a = el.closest("a[href]");
576
+ const a = (0, import_guide_runtime2.closestComposed)(el, "a[href]");
570
577
  if (!a) return void 0;
571
578
  try {
572
579
  const u = new URL(a.getAttribute("href") || "", location.href);
@@ -893,13 +900,113 @@ function isWithinSchedule(guide, now = Date.now()) {
893
900
  if (s.endAt && now > Date.parse(s.endAt)) return false;
894
901
  return true;
895
902
  }
903
+ function dismissedAfterMerge(local, server) {
904
+ if (server.dismissResetAt && server.dismissResetAt !== local?.dismissResetAt) return server.dismissedAt ?? null;
905
+ return local?.dismissedAt ?? server.dismissedAt ?? null;
906
+ }
907
+
908
+ // src/health.ts
909
+ var MAX_RETRY_AFTER_MS = 60 * 60 * 1e3;
910
+ var OFFLINE_WAIT_MS = 3e4;
911
+ var ServerHealth = class {
912
+ constructor(opts = {}) {
913
+ __publicField(this, "failures", 0);
914
+ __publicField(this, "openUntil", 0);
915
+ __publicField(this, "lastOkAt", 0);
916
+ __publicField(this, "listeners", /* @__PURE__ */ new Set());
917
+ __publicField(this, "threshold");
918
+ __publicField(this, "baseMs");
919
+ __publicField(this, "maxMs");
920
+ __publicField(this, "now");
921
+ __publicField(this, "random");
922
+ this.threshold = opts.threshold ?? 3;
923
+ this.baseMs = opts.baseMs ?? 5e3;
924
+ this.maxMs = opts.maxMs ?? 5 * 60 * 1e3;
925
+ this.now = opts.now ?? Date.now;
926
+ this.random = opts.random ?? Math.random;
927
+ }
928
+ /** The server answered (2xx, 3xx, or a 4xx other than 429): it is up. */
929
+ success() {
930
+ const wasDown = this.failures > 0 || this.openUntil > 0;
931
+ this.failures = 0;
932
+ this.openUntil = 0;
933
+ this.lastOkAt = this.now();
934
+ if (wasDown) this.notify();
935
+ }
936
+ /** The request failed or the server is overloaded. `retryAfterMs` comes from a `Retry-After` header. */
937
+ failure(retryAfterMs) {
938
+ this.failures += 1;
939
+ const now = this.now();
940
+ if (retryAfterMs != null && retryAfterMs > 0) {
941
+ this.openUntil = Math.max(this.openUntil, now + Math.min(retryAfterMs, MAX_RETRY_AFTER_MS));
942
+ return;
943
+ }
944
+ if (this.failures < this.threshold) return;
945
+ const cooldown = Math.min(this.maxMs, this.baseMs * 2 ** (this.failures - this.threshold));
946
+ this.openUntil = Math.max(this.openUntil, now + cooldown * (0.8 + this.random() * 0.4));
947
+ }
948
+ /** Milliseconds background traffic should wait before trying again (0: send now). */
949
+ waitMs() {
950
+ if (typeof navigator !== "undefined" && navigator.onLine === false) return OFFLINE_WAIT_MS;
951
+ return Math.max(0, this.openUntil - this.now());
952
+ }
953
+ canSend() {
954
+ return this.waitMs() === 0;
955
+ }
956
+ /**
957
+ * The last request got an answer. Unload beacons can't report back, so what they carry is only
958
+ * forgotten while this holds; otherwise it stays queued and is sent again (the server drops duplicates).
959
+ */
960
+ get reachable() {
961
+ return this.failures === 0 && this.lastOkAt > 0;
962
+ }
963
+ /** The browser came back online: end the cooldown and let queued traffic try straight away. */
964
+ online() {
965
+ this.openUntil = 0;
966
+ this.notify();
967
+ }
968
+ /** Called when the server answers again after failures, or the browser comes back online. */
969
+ onRecover(cb) {
970
+ this.listeners.add(cb);
971
+ return () => this.listeners.delete(cb);
972
+ }
973
+ notify() {
974
+ for (const cb of Array.from(this.listeners)) {
975
+ try {
976
+ cb();
977
+ } catch {
978
+ }
979
+ }
980
+ }
981
+ };
982
+ function parseRetryAfter(value, now = Date.now()) {
983
+ if (!value) return null;
984
+ const trimmed = value.trim();
985
+ if (/^\d+$/.test(trimmed)) return Number(trimmed) * 1e3;
986
+ const at = Date.parse(trimmed);
987
+ return Number.isNaN(at) ? null : Math.max(0, at - now);
988
+ }
989
+ function isServerFailure(status) {
990
+ return status === 429 || status >= 500;
991
+ }
896
992
 
897
993
  // src/http.ts
994
+ var STREAM_RESPONSE_TIMEOUT_MS = 3e4;
995
+ var STREAM_IDLE_TIMEOUT_MS = 6e4;
898
996
  var Api = class {
899
- constructor(baseUrl, key, fetchImpl) {
997
+ constructor(baseUrl, key, fetchImpl, health) {
900
998
  __publicField(this, "baseUrl", baseUrl);
901
999
  __publicField(this, "key", key);
902
1000
  __publicField(this, "fetchImpl", fetchImpl);
1001
+ /** Reachability of the API, fed by every answer (see {@link ServerHealth}). */
1002
+ __publicField(this, "health");
1003
+ this.health = health ?? new ServerHealth();
1004
+ }
1005
+ /** Feeds an answer (or a failed request, `null`) into {@link health}. */
1006
+ observe(res) {
1007
+ if (!res) this.health.failure();
1008
+ else if (isServerFailure(res.status)) this.health.failure(parseRetryAfter(res.headers?.get?.("retry-after")));
1009
+ else this.health.success();
903
1010
  }
904
1011
  url(path, params = {}) {
905
1012
  const query = Object.entries({ key: this.key, ...params }).filter((e) => e[1] != null).map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join("&");
@@ -919,15 +1026,22 @@ var Api = class {
919
1026
  const ac = typeof AbortController !== "undefined" ? new AbortController() : null;
920
1027
  const timer = ac ? setTimeout(() => ac.abort(), options.timeoutMs ?? 1e4) : null;
921
1028
  try {
922
- const res = await f(this.url(path, options.params), {
923
- method,
924
- headers,
925
- body: options.body === void 0 ? void 0 : form ? options.body : JSON.stringify(options.body),
926
- keepalive: options.keepalive,
927
- signal: ac?.signal,
928
- credentials: "omit",
929
- mode: "cors"
930
- });
1029
+ let res;
1030
+ try {
1031
+ res = await f(this.url(path, options.params), {
1032
+ method,
1033
+ headers,
1034
+ body: options.body === void 0 ? void 0 : form ? options.body : JSON.stringify(options.body),
1035
+ keepalive: options.keepalive,
1036
+ signal: ac?.signal,
1037
+ credentials: "omit",
1038
+ mode: "cors"
1039
+ });
1040
+ } catch (err) {
1041
+ this.observe(null);
1042
+ throw err;
1043
+ }
1044
+ this.observe(res);
931
1045
  let data = null;
932
1046
  if (res.status !== 304 && res.status !== 204) {
933
1047
  try {
@@ -953,30 +1067,53 @@ var Api = class {
953
1067
  }
954
1068
  /**
955
1069
  * POST that may answer with server-sent events. Each event is passed to `onEvent`; a JSON answer is
956
- * returned instead. No timeout: Scout replies can take a while and the server keeps the stream alive.
1070
+ * returned instead. Scout replies can take a while, so there is no overall timeout: the response
1071
+ * must start within 30 s and then never go quiet for more than 60 s (the server sends keepalives),
1072
+ * otherwise the request is aborted and rejects.
957
1073
  */
958
1074
  async stream(path, body, onEvent, headers = {}) {
959
1075
  const f = this.fetcher;
960
1076
  if (!f) throw new Error("fetch is not available");
961
- const res = await f(this.url(path), {
962
- method: "POST",
963
- headers: { ...headers, "X-Guidely-Key": this.key, Accept: "text/event-stream, application/json", "Content-Type": "application/json" },
964
- body: JSON.stringify(body),
965
- credentials: "omit",
966
- mode: "cors"
967
- });
968
- const type = res.headers?.get?.("content-type") ?? "";
969
- if (!type.includes("text/event-stream") || !res.body) {
970
- let data = null;
1077
+ const ac = typeof AbortController !== "undefined" ? new AbortController() : null;
1078
+ let timer = null;
1079
+ const arm = (ms) => {
1080
+ if (!ac) return;
1081
+ if (timer) clearTimeout(timer);
1082
+ timer = setTimeout(() => ac.abort(), ms);
1083
+ };
1084
+ arm(STREAM_RESPONSE_TIMEOUT_MS);
1085
+ try {
1086
+ let res;
971
1087
  try {
972
- data = await res.json();
973
- } catch {
974
- data = null;
1088
+ res = await f(this.url(path), {
1089
+ method: "POST",
1090
+ headers: { ...headers, "X-Guidely-Key": this.key, Accept: "text/event-stream, application/json", "Content-Type": "application/json" },
1091
+ body: JSON.stringify(body),
1092
+ signal: ac?.signal,
1093
+ credentials: "omit",
1094
+ mode: "cors"
1095
+ });
1096
+ } catch (err) {
1097
+ this.observe(null);
1098
+ throw err;
1099
+ }
1100
+ this.observe(res);
1101
+ const type = res.headers?.get?.("content-type") ?? "";
1102
+ if (!type.includes("text/event-stream") || !res.body) {
1103
+ let data = null;
1104
+ try {
1105
+ data = await res.json();
1106
+ } catch {
1107
+ data = null;
1108
+ }
1109
+ return { status: res.status, ok: res.ok, data, etag: null };
975
1110
  }
976
- return { status: res.status, ok: res.ok, data, etag: null };
1111
+ arm(STREAM_IDLE_TIMEOUT_MS);
1112
+ await readEventStream(res.body, onEvent, () => arm(STREAM_IDLE_TIMEOUT_MS));
1113
+ return { status: res.status, ok: res.ok, data: null, etag: null };
1114
+ } finally {
1115
+ if (timer) clearTimeout(timer);
977
1116
  }
978
- await readEventStream(res.body, onEvent);
979
- return { status: res.status, ok: res.ok, data: null, etag: null };
980
1117
  }
981
1118
  /** `navigator.sendBeacon` when available (page unload); returns false when it could not be queued. */
982
1119
  beacon(path, body) {
@@ -1007,7 +1144,7 @@ async function withRetry(send, attempts = 3, baseMs = 500) {
1007
1144
  function postWithRetry(api, path, body, attempts = 3, baseMs = 500) {
1008
1145
  return withRetry(() => api.post(path, body), attempts, baseMs);
1009
1146
  }
1010
- async function readEventStream(body, onEvent) {
1147
+ async function readEventStream(body, onEvent, onChunk) {
1011
1148
  const reader = body.getReader();
1012
1149
  const decoder = new TextDecoder();
1013
1150
  let buffer = "";
@@ -1030,6 +1167,7 @@ async function readEventStream(body, onEvent) {
1030
1167
  };
1031
1168
  for (; ; ) {
1032
1169
  const { done, value } = await reader.read();
1170
+ if (value) onChunk?.();
1033
1171
  if (value) buffer += decoder.decode(value, { stream: true }).replace(/\r\n?/g, "\n");
1034
1172
  let sep;
1035
1173
  while ((sep = buffer.indexOf("\n\n")) >= 0) {
@@ -1061,11 +1199,15 @@ function forgetConversations(store, projectId) {
1061
1199
  store.remove(KEYS.conversations(projectId));
1062
1200
  }
1063
1201
  var HttpError = class extends Error {
1064
- constructor(status) {
1065
- super(`Request failed (${status})`);
1202
+ constructor(status, message) {
1203
+ super(message || `Request failed (${status})`);
1066
1204
  __publicField(this, "status", status);
1067
1205
  }
1068
1206
  };
1207
+ function serverMessage(data) {
1208
+ const m = data?.error?.message;
1209
+ return typeof m === "string" && m !== "Request validation failed" ? m : void 0;
1210
+ }
1069
1211
  function createMessengerTransport(opts) {
1070
1212
  const { api, store, projectId } = opts;
1071
1213
  const tokenFor = (id) => storedConversations(store, projectId).find((c) => c.id === id)?.token ?? "";
@@ -1147,6 +1289,16 @@ function createMessengerTransport(opts) {
1147
1289
  const res = await api.get(`/articles/${encodeURIComponent(id)}`);
1148
1290
  return res.ok && res.data ? res.data.article : null;
1149
1291
  },
1292
+ async fileComplaint(input) {
1293
+ const headers = input.conversationId ? { [TOKEN_HEADER]: tokenFor(input.conversationId) } : {};
1294
+ const res = await api.post("/complaints", { ...input, anonymousId: opts.anonymousId(), userId: opts.userId(), pageUrl: opts.pageUrl() }, { headers });
1295
+ if (!res.ok || !res.data || !("complaint" in res.data)) {
1296
+ if (res.status === 429) throw new HttpError(429, "You've filed several complaints just now. Please try again later.");
1297
+ throw new HttpError(res.status, serverMessage(res.data) ?? "Your complaint couldn't be sent. Please try again.");
1298
+ }
1299
+ if (res.data.token) remember(res.data.conversation.id, res.data.token);
1300
+ return res.data;
1301
+ },
1150
1302
  async pageContent(path) {
1151
1303
  const res = await api.post("/messenger/page", { anonymousId: opts.anonymousId(), userId: opts.userId(), path: path.split(/[?#]/)[0] || "/" });
1152
1304
  if (!res.ok || !res.data) throw new HttpError(res.status);
@@ -1169,7 +1321,7 @@ function createMessengerTransport(opts) {
1169
1321
  }
1170
1322
 
1171
1323
  // src/messenger-visitor.ts
1172
- var import_runtime10 = require("@guidelytech/shared/runtime");
1324
+ var import_runtime11 = require("@guidelytech/shared/runtime");
1173
1325
  var import_guide_runtime3 = require("@guidelytech/guide-runtime");
1174
1326
  var PAGE_GUIDE_KINDS = /* @__PURE__ */ new Set(["flow", "tooltip", "modal"]);
1175
1327
  var CATCH_ALL = /* @__PURE__ */ new Set(["", "*", "**", "/*", "/**", "/"]);
@@ -1236,7 +1388,7 @@ function buildMessengerVisitor(input) {
1236
1388
  unread: !seen.has(g.id) && !(state.guides[g.id]?.impressions ?? 0)
1237
1389
  };
1238
1390
  });
1239
- return { firstName: (0, import_runtime10.firstNameFromTraits)(input.traits), path: pathOf(input.url), pageGuides, resumeGuide, checklist, news };
1391
+ return { firstName: (0, import_runtime11.firstNameFromTraits)(input.traits), path: pathOf(input.url), pageGuides, resumeGuide, checklist, news };
1240
1392
  }
1241
1393
 
1242
1394
  // src/navigation.ts
@@ -1339,6 +1491,90 @@ var PageTracker = class {
1339
1491
  }
1340
1492
  };
1341
1493
 
1494
+ // src/outbox.ts
1495
+ var Outbox = class {
1496
+ constructor(opts) {
1497
+ __publicField(this, "opts", opts);
1498
+ __publicField(this, "items");
1499
+ __publicField(this, "sending", false);
1500
+ __publicField(this, "attempt", 0);
1501
+ __publicField(this, "timer", null);
1502
+ __publicField(this, "destroyed", false);
1503
+ __publicField(this, "offRecover");
1504
+ __publicField(this, "maxItems");
1505
+ __publicField(this, "maxAgeMs");
1506
+ this.maxItems = opts.maxItems ?? 50;
1507
+ this.maxAgeMs = opts.maxAgeMs ?? 7 * 24 * 60 * 60 * 1e3;
1508
+ const stored = readJson(opts.store, opts.storageKey, []);
1509
+ const cutoff = Date.now() - this.maxAgeMs;
1510
+ this.items = Array.isArray(stored) ? stored.filter((i) => i && typeof i.path === "string" && typeof i.key === "string" && i.createdAt > cutoff).slice(-this.maxItems) : [];
1511
+ this.offRecover = opts.health?.onRecover(() => {
1512
+ this.attempt = 0;
1513
+ this.retryIn(0);
1514
+ }) ?? null;
1515
+ if (this.items.length) this.retryIn(0);
1516
+ }
1517
+ get size() {
1518
+ return this.items.length;
1519
+ }
1520
+ /** Queues `POST path` and sends it now (or as soon as the API is reachable). */
1521
+ add(key, path, body) {
1522
+ if (this.destroyed) return;
1523
+ this.items = this.items.filter((i) => i.key !== key);
1524
+ this.items.push({ key, path, body, createdAt: Date.now() });
1525
+ if (this.items.length > this.maxItems) this.items.splice(0, this.items.length - this.maxItems);
1526
+ this.persist();
1527
+ void this.flush();
1528
+ }
1529
+ persist() {
1530
+ writeJson(this.opts.store, this.opts.storageKey, this.items);
1531
+ }
1532
+ retryIn(ms) {
1533
+ if (this.destroyed) return;
1534
+ if (this.timer) clearTimeout(this.timer);
1535
+ this.timer = setTimeout(() => {
1536
+ this.timer = null;
1537
+ void this.flush();
1538
+ }, ms);
1539
+ }
1540
+ /** Sends queued requests in order, one at a time. */
1541
+ async flush() {
1542
+ if (this.destroyed || this.sending || !this.items.length) return;
1543
+ const wait = this.opts.health?.waitMs() ?? 0;
1544
+ if (wait > 0) return this.retryIn(wait);
1545
+ this.sending = true;
1546
+ try {
1547
+ while (this.items.length && !this.destroyed) {
1548
+ const item = this.items[0];
1549
+ let retry = false;
1550
+ try {
1551
+ const res = await this.opts.api.post(item.path, item.body);
1552
+ retry = !res.ok && (res.status === 429 || res.status >= 500);
1553
+ } catch (err) {
1554
+ retry = true;
1555
+ this.opts.onError?.(err);
1556
+ }
1557
+ if (retry) {
1558
+ this.attempt += 1;
1559
+ this.retryIn(Math.max(backoffMs(this.attempt - 1, 1e3, 6e4), this.opts.health?.waitMs() ?? 0));
1560
+ return;
1561
+ }
1562
+ this.attempt = 0;
1563
+ this.items = this.items.filter((i) => i !== item);
1564
+ this.persist();
1565
+ }
1566
+ } finally {
1567
+ this.sending = false;
1568
+ }
1569
+ }
1570
+ destroy() {
1571
+ this.destroyed = true;
1572
+ this.offRecover?.();
1573
+ if (this.timer) clearTimeout(this.timer);
1574
+ this.timer = null;
1575
+ }
1576
+ };
1577
+
1342
1578
  // src/queue.ts
1343
1579
  var EventQueue = class {
1344
1580
  constructor(opts) {
@@ -1350,17 +1586,34 @@ var EventQueue = class {
1350
1586
  __publicField(this, "inflightIds", /* @__PURE__ */ new Set());
1351
1587
  __publicField(this, "attempt", 0);
1352
1588
  __publicField(this, "destroyed", false);
1589
+ __publicField(this, "offRecover", null);
1353
1590
  __publicField(this, "flushIntervalMs");
1354
1591
  __publicField(this, "maxQueue");
1355
1592
  __publicField(this, "batchSize");
1356
1593
  __publicField(this, "maxAttempts");
1357
1594
  this.flushIntervalMs = opts.flushIntervalMs ?? 2e3;
1358
- this.maxQueue = opts.maxQueue ?? 200;
1595
+ this.maxQueue = opts.maxQueue ?? 500;
1359
1596
  this.batchSize = opts.batchSize ?? 50;
1360
1597
  this.maxAttempts = opts.maxAttempts ?? 5;
1361
1598
  const persisted = readJson(opts.store, opts.storageKey, []);
1362
1599
  if (Array.isArray(persisted)) this.events = persisted.filter((e) => e && typeof e.eventId === "string").slice(-this.maxQueue);
1363
1600
  if (this.events.length) this.schedule();
1601
+ this.offRecover = opts.health?.onRecover(() => this.retryNow()) ?? null;
1602
+ }
1603
+ /** The API is back: drop any pending backoff and send what is queued. */
1604
+ retryNow() {
1605
+ if (this.destroyed) return;
1606
+ if (this.retryTimer) clearTimeout(this.retryTimer);
1607
+ this.retryTimer = null;
1608
+ this.attempt = 0;
1609
+ void this.flush();
1610
+ }
1611
+ retryIn(ms) {
1612
+ if (this.retryTimer || this.destroyed) return;
1613
+ this.retryTimer = setTimeout(() => {
1614
+ this.retryTimer = null;
1615
+ void this.flush();
1616
+ }, ms);
1364
1617
  }
1365
1618
  get size() {
1366
1619
  return this.events.length;
@@ -1394,6 +1647,8 @@ var EventQueue = class {
1394
1647
  clearTimeout(this.timer);
1395
1648
  this.timer = null;
1396
1649
  }
1650
+ const wait = this.opts.health?.waitMs() ?? 0;
1651
+ if (wait > 0) return this.retryIn(wait);
1397
1652
  this.inflight = true;
1398
1653
  const batch = this.events.slice(0, this.batchSize);
1399
1654
  this.inflightIds = new Set(batch.map((e) => e.eventId));
@@ -1417,26 +1672,20 @@ var EventQueue = class {
1417
1672
  if (this.events.length) this.schedule();
1418
1673
  return;
1419
1674
  }
1420
- this.attempt += 1;
1421
- if (this.attempt >= this.maxAttempts) {
1422
- this.attempt = 0;
1423
- return;
1424
- }
1425
- this.retryTimer = setTimeout(() => {
1426
- this.retryTimer = null;
1427
- void this.flush();
1428
- }, backoffMs(this.attempt - 1));
1675
+ this.attempt = Math.min(this.attempt + 1, this.maxAttempts);
1676
+ const delay = this.attempt >= this.maxAttempts ? 6e4 : backoffMs(this.attempt - 1);
1677
+ this.retryIn(Math.max(delay, this.opts.health?.waitMs() ?? 0));
1429
1678
  }
1430
1679
  /** Best-effort synchronous flush on unload using `sendBeacon`. */
1431
1680
  flushSync() {
1432
1681
  if (this.destroyed || !this.opts.enabled() || !this.opts.beacon) return;
1433
1682
  const batch = this.events.filter((e) => !this.inflightIds.has(e.eventId)).slice(0, this.batchSize);
1434
1683
  if (!batch.length) return;
1435
- if (this.opts.beacon(batch)) {
1684
+ if (this.opts.beacon(batch) && (this.opts.health?.reachable ?? true)) {
1436
1685
  const ids = new Set(batch.map((e) => e.eventId));
1437
1686
  this.events = this.events.filter((e) => !ids.has(e.eventId));
1438
- this.persist();
1439
1687
  }
1688
+ this.persist();
1440
1689
  }
1441
1690
  clear() {
1442
1691
  this.events = [];
@@ -1444,6 +1693,7 @@ var EventQueue = class {
1444
1693
  }
1445
1694
  destroy() {
1446
1695
  this.destroyed = true;
1696
+ this.offRecover?.();
1447
1697
  if (this.timer) clearTimeout(this.timer);
1448
1698
  if (this.retryTimer) clearTimeout(this.retryTimer);
1449
1699
  this.timer = null;
@@ -1457,11 +1707,22 @@ var loadRecorder = async () => (await import("./recorder.cjs")).recorder;
1457
1707
 
1458
1708
  // src/client.ts
1459
1709
  var SDK_NAME = "@guidelytech/sdk-core";
1460
- var SDK_VERSION = "0.8.0";
1710
+ var SDK_VERSION = "0.10.0";
1461
1711
  var DEFAULT_API_URL = "https://api.guidely.app";
1462
1712
  var PAGE_SCOPED = /* @__PURE__ */ new Set(["page_load", "url_match", "element_click", "element_visible", "delay"]);
1463
1713
  var DAY_MS = 24 * 60 * 60 * 1e3;
1464
1714
  var MAX_SESSION_EXCEPTIONS = 100;
1715
+ var CACHED_START_MS = 1500;
1716
+ function settlesWithin(promise, ms) {
1717
+ return new Promise((resolve) => {
1718
+ const timer = setTimeout(() => resolve(false), ms);
1719
+ const done = () => {
1720
+ clearTimeout(timer);
1721
+ resolve(true);
1722
+ };
1723
+ promise.then(done, done);
1724
+ });
1725
+ }
1465
1726
  function isBrowser() {
1466
1727
  return typeof window !== "undefined" && typeof document !== "undefined";
1467
1728
  }
@@ -1472,7 +1733,7 @@ function emptyState() {
1472
1733
  return { guides: {}, checklists: {}, surveysSubmitted: [], audienceIds: [] };
1473
1734
  }
1474
1735
  function newSession(now = Date.now()) {
1475
- return { id: `s_${(0, import_runtime13.randomId)(16)}`, startedAt: new Date(now).toISOString(), shownGuides: [], events: [], lastActivityAt: now };
1736
+ return { id: `s_${(0, import_runtime15.randomId)(16)}`, startedAt: new Date(now).toISOString(), shownGuides: [], events: [], lastActivityAt: now };
1476
1737
  }
1477
1738
  var GuidelyClient = class _GuidelyClient {
1478
1739
  constructor(options) {
@@ -1482,6 +1743,8 @@ var GuidelyClient = class _GuidelyClient {
1482
1743
  __publicField(this, "sessionStore");
1483
1744
  __publicField(this, "emitter");
1484
1745
  __publicField(this, "queue");
1746
+ /** Survey answers and guide/checklist progress, kept until the server accepts them. */
1747
+ __publicField(this, "outbox");
1485
1748
  __publicField(this, "config", null);
1486
1749
  __publicField(this, "etag", null);
1487
1750
  __publicField(this, "anonymousId");
@@ -1565,7 +1828,7 @@ var GuidelyClient = class _GuidelyClient {
1565
1828
  this.sessionStore = this.options.storage === "memory" ? this.store : createStorage("session");
1566
1829
  this.anonymousId = this.store.get(KEYS.anon) || "";
1567
1830
  if (!this.anonymousId) {
1568
- this.anonymousId = `anon_${(0, import_runtime13.randomId)(20)}`;
1831
+ this.anonymousId = `anon_${(0, import_runtime15.randomId)(20)}`;
1569
1832
  this.store.set(KEYS.anon, this.anonymousId);
1570
1833
  }
1571
1834
  this.firstVisit = !this.store.get(KEYS.seen);
@@ -1581,6 +1844,7 @@ var GuidelyClient = class _GuidelyClient {
1581
1844
  store: this.store,
1582
1845
  storageKey: KEYS.queue(this.options.projectId),
1583
1846
  enabled: () => this.analyticsEnabled(),
1847
+ health: this.api.health,
1584
1848
  send: async (events) => {
1585
1849
  const res = await this.api.post("/events", { events, sentAt: nowIso(), sdk: { name: SDK_NAME, version: SDK_VERSION } }, { keepalive: true });
1586
1850
  return { ok: res.ok, retry: !(res.status >= 400 && res.status < 500 && res.status !== 429) };
@@ -1588,6 +1852,7 @@ var GuidelyClient = class _GuidelyClient {
1588
1852
  beacon: (events) => this.api.beacon("/events", { events, sentAt: nowIso(), sdk: { name: SDK_NAME, version: SDK_VERSION } }),
1589
1853
  onError: (err) => this.reportError(err)
1590
1854
  });
1855
+ this.outbox = new Outbox({ api: this.api, store: this.store, storageKey: KEYS.outbox(this.options.projectId), health: this.api.health });
1591
1856
  if (options.user) this.user = { ...options.user };
1592
1857
  this.flags = this.createFlags(options.flags);
1593
1858
  }
@@ -1645,12 +1910,15 @@ var GuidelyClient = class _GuidelyClient {
1645
1910
  const cached = readJson(this.store, KEYS.config(this.options.projectId), null);
1646
1911
  this.etag = this.store.get(KEYS.etag(this.options.projectId));
1647
1912
  if (cached && cached.guides) this.config = webOnly(cached);
1648
- await this.fetchConfig();
1913
+ const before = this.config?.configVersion;
1914
+ const fresh = this.fetchConfig();
1915
+ if (!this.config) await fresh;
1916
+ else if (!await settlesWithin(fresh, CACHED_START_MS)) void fresh.then(() => this.applyConfigChange(before));
1649
1917
  if (this.destroyed) return;
1650
1918
  this.wireListeners();
1651
1919
  if (!this.identitySettled) {
1652
- if (this.options.user) await this.identify(this.options.user.id, this.options.user.traits, this.options.user.account);
1653
- else if (this.user?.id) await this.identify(this.user.id, this.user.traits, this.user.account);
1920
+ const known = this.options.user ?? (this.user?.id ? this.user : null);
1921
+ if (known) await settlesWithin(this.identify(known.id, known.traits, known.account), CACHED_START_MS);
1654
1922
  }
1655
1923
  if (this.destroyed) return;
1656
1924
  this.isReady = true;
@@ -1755,7 +2023,12 @@ var GuidelyClient = class _GuidelyClient {
1755
2023
  win.removeEventListener("scroll", onScroll);
1756
2024
  doc.removeEventListener("keydown", onKey, true);
1757
2025
  });
1758
- const refresh = setInterval(() => void this.refresh(), this.options.configRefreshMs);
2026
+ const refresh = setInterval(() => {
2027
+ if (this.api.health.canSend()) void this.refresh();
2028
+ }, this.options.configRefreshMs);
2029
+ const onOnline = () => this.api.health.online();
2030
+ win.addEventListener("online", onOnline);
2031
+ this.cleanups.push(() => win.removeEventListener("online", onOnline));
1759
2032
  this.cleanups.push(() => clearInterval(refresh));
1760
2033
  const poll = setInterval(() => {
1761
2034
  if (!this.isReady || !this.config || this.editing() || doc.visibilityState === "hidden") return;
@@ -1788,6 +2061,11 @@ var GuidelyClient = class _GuidelyClient {
1788
2061
  if (this.destroyed) return;
1789
2062
  const before = this.config?.configVersion;
1790
2063
  await this.fetchConfig();
2064
+ this.applyConfigChange(before);
2065
+ }
2066
+ /** Re-renders after a config change (refresh, or a config that arrived after start). */
2067
+ applyConfigChange(before) {
2068
+ if (this.destroyed || !this.isReady) return;
1791
2069
  if (this.config && this.config.configVersion !== before) {
1792
2070
  this.renderPersistentWidgets();
1793
2071
  this.evaluate("ready");
@@ -1822,6 +2100,7 @@ var GuidelyClient = class _GuidelyClient {
1822
2100
  this.replay = null;
1823
2101
  this.queue.flushSync();
1824
2102
  this.queue.destroy();
2103
+ this.outbox.destroy();
1825
2104
  this.flags.destroy();
1826
2105
  this.emitter.clear();
1827
2106
  (0, import_guide_runtime4.destroyHost)();
@@ -1837,7 +2116,7 @@ var GuidelyClient = class _GuidelyClient {
1837
2116
  this.user = { id: String(userId), traits: { ...this.user?.id === String(userId) ? this.user.traits : {}, ...traits || {} }, account: account ?? (this.user?.id === String(userId) ? this.user.account : void 0) };
1838
2117
  writeJson(this.store, KEYS.user(this.options.projectId), this.user);
1839
2118
  this.syncFlagAttributes();
1840
- this.enqueue("identify", { properties: { traits: (0, import_runtime13.sanitizeProperties)(this.user.traits) } });
2119
+ this.enqueue("identify", { properties: { traits: (0, import_runtime15.sanitizeProperties)(this.user.traits) } });
1841
2120
  await this.syncIdentify();
1842
2121
  this.emitter.emit("identify", { userId: this.user.id, anonymousId: this.anonymousId });
1843
2122
  if (this.isReady) this.evaluate("identify");
@@ -1865,7 +2144,8 @@ var GuidelyClient = class _GuidelyClient {
1865
2144
  checklists[id] = {
1866
2145
  completedTaskIds: Array.from(/* @__PURE__ */ new Set([...local?.completedTaskIds ?? [], ...p.completedTaskIds ?? []])),
1867
2146
  completedAt: local?.completedAt ?? p.completedAt ?? null,
1868
- dismissedAt: local?.dismissedAt ?? p.dismissedAt ?? null
2147
+ dismissedAt: dismissedAfterMerge(local, p),
2148
+ dismissResetAt: p.dismissResetAt ?? local?.dismissResetAt ?? null
1869
2149
  };
1870
2150
  }
1871
2151
  this.state = {
@@ -1909,7 +2189,7 @@ var GuidelyClient = class _GuidelyClient {
1909
2189
  this.stopGuide();
1910
2190
  this.user = null;
1911
2191
  this.store.remove(KEYS.user(this.options.projectId));
1912
- this.anonymousId = `anon_${(0, import_runtime13.randomId)(20)}`;
2192
+ this.anonymousId = `anon_${(0, import_runtime15.randomId)(20)}`;
1913
2193
  this.store.set(KEYS.anon, this.anonymousId);
1914
2194
  this.state = emptyState();
1915
2195
  this.persistState();
@@ -1953,7 +2233,7 @@ var GuidelyClient = class _GuidelyClient {
1953
2233
  if (this.consent === "denied" || !this.options.analytics) return null;
1954
2234
  if (name !== "page_leave" && name !== "heatmap") this.touchSession();
1955
2235
  const event = {
1956
- eventId: (0, import_runtime13.randomId)(24),
2236
+ eventId: (0, import_runtime15.randomId)(24),
1957
2237
  name,
1958
2238
  occurredAt: nowIso(),
1959
2239
  anonymousId: this.anonymousId,
@@ -1963,7 +2243,7 @@ var GuidelyClient = class _GuidelyClient {
1963
2243
  url: this.collectUrls() ? location.href : void 0,
1964
2244
  ...extra
1965
2245
  };
1966
- if (extra.properties) event.properties = (0, import_runtime13.sanitizeProperties)(extra.properties);
2246
+ if (extra.properties) event.properties = (0, import_runtime15.sanitizeProperties)(extra.properties);
1967
2247
  this.queue.enqueue(event);
1968
2248
  this.replay?.addCustomEvent("guidely", { name, customName: event.customName, guideId: event.guideId });
1969
2249
  this.emitter.emit("track", event);
@@ -2023,7 +2303,7 @@ var GuidelyClient = class _GuidelyClient {
2023
2303
  if (changed) void this.queue.flush();
2024
2304
  }
2025
2305
  handleDocumentClick(e) {
2026
- const target = e.target;
2306
+ const target = (0, import_guide_runtime4.composedTarget)(e);
2027
2307
  if (!target || typeof target.closest !== "function") return;
2028
2308
  this.autocaptureClick(e, target);
2029
2309
  this.completeChecklistTasksBy("click", target);
@@ -2118,7 +2398,7 @@ var GuidelyClient = class _GuidelyClient {
2118
2398
  }
2119
2399
  /** Bins one pointer sample into the heatmap buffer. Not session activity. */
2120
2400
  sampleMove(e) {
2121
- const target = e.target;
2401
+ const target = (0, import_guide_runtime4.composedTarget)(e);
2122
2402
  if (document.visibilityState === "hidden" || !target?.closest || !this.autocaptureOn("mouseMovement") || isIgnoredElement(target)) return;
2123
2403
  const url = location.href;
2124
2404
  if (this.heatmap.url !== url) this.flushHeatmap();
@@ -2209,7 +2489,7 @@ var GuidelyClient = class _GuidelyClient {
2209
2489
  syncReplay() {
2210
2490
  const cfg = this.isReady && this.captureAllowed() ? this.replayConfig() : null;
2211
2491
  if (cfg && this.session.replay === void 0) {
2212
- this.session.replay = (0, import_runtime13.rolloutBucket)(this.session.id, "replay") < cfg.sampleRate;
2492
+ this.session.replay = (0, import_runtime15.rolloutBucket)(this.session.id, "replay") < cfg.sampleRate;
2213
2493
  this.persistSession();
2214
2494
  }
2215
2495
  if (!cfg || !this.session.replay) {
@@ -2243,7 +2523,9 @@ var GuidelyClient = class _GuidelyClient {
2243
2523
  this.persistSession();
2244
2524
  this.replay?.stop({ discard: true });
2245
2525
  },
2246
- onError: (err) => this.reportError(err)
2526
+ onError: (err) => this.reportError(err),
2527
+ health: this.api.health,
2528
+ stash: this.sessionStore
2247
2529
  });
2248
2530
  this.syncReplay();
2249
2531
  },
@@ -2309,7 +2591,7 @@ var GuidelyClient = class _GuidelyClient {
2309
2591
  if (!isWithinSchedule(guide)) return false;
2310
2592
  if (guide.kind === "survey" && (this.state.surveysSubmitted.includes(guide.id) || this.state.surveysSubmitted.includes(guide.key))) return false;
2311
2593
  ctx.rolloutSalt = guide.id;
2312
- return (0, import_runtime13.evaluateRules)(c.targeting, ctx);
2594
+ return (0, import_runtime15.evaluateRules)(c.targeting, ctx);
2313
2595
  }
2314
2596
  isEligible(guide, ctx, url) {
2315
2597
  if (!this.isTargeted(guide, ctx, url)) return false;
@@ -2333,7 +2615,7 @@ var GuidelyClient = class _GuidelyClient {
2333
2615
  case "element_click":
2334
2616
  if (reason !== "click" || !t.value || !detail.target) return false;
2335
2617
  try {
2336
- return !!detail.target.closest(t.value);
2618
+ return !!(0, import_guide_runtime4.closestComposed)(detail.target, t.value);
2337
2619
  } catch {
2338
2620
  return false;
2339
2621
  }
@@ -2484,7 +2766,9 @@ var GuidelyClient = class _GuidelyClient {
2484
2766
  if (reason === "completed") this.completeChecklistTasksBy("guide_completed", guide);
2485
2767
  }
2486
2768
  syncGuideState(guideId, update) {
2487
- void postWithRetry(this.api, "/guide-state", { anonymousId: this.anonymousId, userId: this.user?.id || null, updates: [{ guideId, ...update }] });
2769
+ const body = { anonymousId: this.anonymousId, userId: this.user?.id || null, updates: [{ guideId, ...update }] };
2770
+ if (update.impression) void postWithRetry(this.api, "/guide-state", body);
2771
+ else this.outbox.add(`guide:${guideId}:${update.completed ? "completed" : "dismissed"}:${this.anonymousId}`, "/guide-state", body);
2488
2772
  }
2489
2773
  show(guide, manual, stepId) {
2490
2774
  try {
@@ -2504,6 +2788,12 @@ var GuidelyClient = class _GuidelyClient {
2504
2788
  ctx: () => this.getContext(),
2505
2789
  startAtStep: stepId,
2506
2790
  eventBus: this.eventBus,
2791
+ // Started from Help, Scout or startGuide(): steps on other pages offer to take the user there.
2792
+ promptForUrl: manual,
2793
+ // Also while a step waits for its page, so a link that loads that page resumes on it.
2794
+ onPosition: (id) => {
2795
+ if (exclusive) writeJson(this.sessionStore, KEYS.active, { guideId: guide.id, stepId: id, manual });
2796
+ },
2507
2797
  onError: (err) => this.reportError(err),
2508
2798
  onEvent: (e) => {
2509
2799
  const step = e.stepId ? guide.config.steps.find((s) => s.id === e.stepId) : void 0;
@@ -2601,12 +2891,12 @@ var GuidelyClient = class _GuidelyClient {
2601
2891
  title: guide.name,
2602
2892
  onDismiss: () => finish(this.state.surveysSubmitted.includes(guide.id) ? "submitted" : "dismissed"),
2603
2893
  onSubmit: (answers) => {
2604
- const responseId = (0, import_runtime13.randomId)(24);
2894
+ const responseId = (0, import_runtime15.randomId)(24);
2605
2895
  this.state.surveysSubmitted.push(guide.id);
2606
2896
  this.persistState();
2607
2897
  this.recordEnd(guide, "completed");
2608
2898
  this.enqueue("survey_submitted", { guideId: guide.id, guideVersion: guide.version, properties: { responseId } });
2609
- 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 });
2899
+ 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 });
2610
2900
  this.emitter.emit("survey:submit", { guide, answers, responseId });
2611
2901
  this.emitter.emit("guide:complete", guide);
2612
2902
  widget.showThankYou();
@@ -2727,7 +3017,7 @@ var GuidelyClient = class _GuidelyClient {
2727
3017
  const ctx = this.getContext();
2728
3018
  const url = location.href;
2729
3019
  const seen = readJson(this.store, KEYS.newsSeen(this.options.projectId), []);
2730
- return buildMessengerVisitor({
3020
+ const visitor = buildMessengerVisitor({
2731
3021
  guides: this.config?.guides ?? [],
2732
3022
  state: this.state,
2733
3023
  traits: this.user?.traits,
@@ -2736,10 +3026,11 @@ var GuidelyClient = class _GuidelyClient {
2736
3026
  isAudienceTargeted: (g) => {
2737
3027
  if (!isWithinSchedule(g)) return false;
2738
3028
  ctx.rolloutSalt = g.id;
2739
- return (0, import_runtime13.evaluateRules)(g.config.targeting, ctx);
3029
+ return (0, import_runtime15.evaluateRules)(g.config.targeting, ctx);
2740
3030
  },
2741
3031
  newsSeen: Array.isArray(seen) ? seen : []
2742
3032
  });
3033
+ return { ...visitor, identified: !!this.user?.id };
2743
3034
  }
2744
3035
  destroyMessenger() {
2745
3036
  this.messenger?.destroy();
@@ -2794,7 +3085,7 @@ var GuidelyClient = class _GuidelyClient {
2794
3085
  }
2795
3086
  this.persistState();
2796
3087
  this.widgets.get(guide.id)?.checklist?.setProgress(this.getChecklistProgress(guide.id));
2797
- 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 });
3088
+ 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 });
2798
3089
  this.emitter.emit("checklist:task", { guide, task, progress: this.getChecklistProgress(guide.id) });
2799
3090
  if (complete) {
2800
3091
  this.emitter.emit("checklist:complete", guide);
@@ -2814,7 +3105,7 @@ var GuidelyClient = class _GuidelyClient {
2814
3105
  else if (kind === "guide_completed") hit = value.id === task.completionValue || value.key === task.completionValue;
2815
3106
  else if (kind === "click") {
2816
3107
  try {
2817
- hit = !!value.closest(task.completionValue);
3108
+ hit = !!(0, import_guide_runtime4.closestComposed)(value, task.completionValue);
2818
3109
  } catch {
2819
3110
  hit = false;
2820
3111
  }
@@ -2834,7 +3125,7 @@ var GuidelyClient = class _GuidelyClient {
2834
3125
  progress.dismissedAt = nowIso();
2835
3126
  this.persistState();
2836
3127
  this.enqueue("checklist_dismissed", { guideId: guide.id, guideVersion: guide.version });
2837
- 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 });
3128
+ 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 });
2838
3129
  this.widgets.get(guide.id)?.destroy();
2839
3130
  this.widgets.delete(guide.id);
2840
3131
  }
@@ -2925,6 +3216,14 @@ var GuidelyClient = class _GuidelyClient {
2925
3216
  closeMessenger() {
2926
3217
  this.messenger?.close();
2927
3218
  }
3219
+ /**
3220
+ * Opens the complaint form in the messenger, optionally with a category picked (its id from
3221
+ * Complaints → Categories). Does nothing when the project doesn't take complaints.
3222
+ */
3223
+ openComplaint(categoryId) {
3224
+ if (!this.config?.messenger?.enabled || !this.config.messenger.complaints?.categories.length) return;
3225
+ void this.loadMessenger().then((m) => m?.openComplaint(categoryId));
3226
+ }
2928
3227
  /** Opens a new messenger conversation, optionally pre-filling the message. */
2929
3228
  showNewMessage(text) {
2930
3229
  if (!this.config?.messenger?.enabled) return;
@@ -3134,7 +3433,7 @@ var GuidelyClient = class _GuidelyClient {
3134
3433
  };
3135
3434
  var current = null;
3136
3435
  function webOnly(config) {
3137
- return config.guides.every((g) => (0, import_runtime13.supportsPlatform)(g.config, "web")) ? config : { ...config, guides: config.guides.filter((g) => (0, import_runtime13.supportsPlatform)(g.config, "web")) };
3436
+ return config.guides.every((g) => (0, import_runtime15.supportsPlatform)(g.config, "web")) ? config : { ...config, guides: config.guides.filter((g) => (0, import_runtime15.supportsPlatform)(g.config, "web")) };
3138
3437
  }
3139
3438
  function pathOf2(url) {
3140
3439
  try {