@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.
- package/README.md +15 -4
- package/dist/{capture-C3faAYb7.d.cts → capture-369s4YYT.d.ts} +2 -1
- package/dist/{capture-Dqd7up2n.d.ts → capture-DVJyQ2JZ.d.cts} +2 -1
- package/dist/capture.cjs +2 -0
- package/dist/capture.cjs.map +1 -1
- package/dist/capture.d.cts +3 -2
- package/dist/capture.d.ts +3 -2
- package/dist/capture.js +4 -6
- package/dist/capture.js.map +1 -1
- package/dist/chunk-C4WWSF67.js +374 -0
- package/dist/chunk-C4WWSF67.js.map +1 -0
- package/dist/{chunk-GDF6RCHQ.js → chunk-GDJB466J.js} +121 -19
- package/dist/chunk-GDJB466J.js.map +1 -0
- package/dist/headless.cjs +281 -40
- package/dist/headless.cjs.map +1 -1
- package/dist/headless.d.cts +43 -3
- package/dist/headless.d.ts +43 -3
- package/dist/headless.js +16 -8
- package/dist/health-DFHUstWd.d.cts +88 -0
- package/dist/health-DFHUstWd.d.ts +88 -0
- package/dist/{http-47tiAlOM.d.ts → http-D0dCcpLN.d.cts} +10 -35
- package/dist/{http-47tiAlOM.d.cts → http-DJd8jRhK.d.ts} +10 -35
- package/dist/index.cjs +380 -81
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +15 -5
- package/dist/index.d.ts +15 -5
- package/dist/index.js +106 -36
- package/dist/index.js.map +1 -1
- package/dist/recorder.cjs +70 -9
- package/dist/recorder.cjs.map +1 -1
- package/dist/recorder.d.cts +9 -0
- package/dist/recorder.d.ts +9 -0
- package/dist/recorder.js +57 -11
- package/dist/recorder.js.map +1 -1
- package/dist/{types-DqhNl5Am.d.ts → types-B5ecgHUV.d.cts} +26 -4
- package/dist/{types-DbKQPvFt.d.cts → types-BtPovimB.d.ts} +26 -4
- package/package.json +7 -7
- package/dist/chunk-AKNE7D5M.js +0 -160
- package/dist/chunk-AKNE7D5M.js.map +0 -1
- package/dist/chunk-CYAG6ZYX.js +0 -85
- package/dist/chunk-CYAG6ZYX.js.map +0 -1
- package/dist/chunk-GDF6RCHQ.js.map +0 -1
- package/dist/types-RyeRQd9f.d.ts +0 -226
- 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
|
|
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 !!
|
|
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) || !!
|
|
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
|
|
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) || "")
|
|
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)
|
|
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)
|
|
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
|
|
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 !!
|
|
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 =
|
|
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
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
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.
|
|
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
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
}
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
let
|
|
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
|
-
|
|
973
|
-
|
|
974
|
-
|
|
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
|
-
|
|
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
|
|
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,
|
|
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 ??
|
|
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
|
|
1421
|
-
|
|
1422
|
-
|
|
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.
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
1653
|
-
|
|
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(() =>
|
|
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,
|
|
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
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
|
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
|
|
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,
|
|
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,
|
|
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
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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 = !!
|
|
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
|
-
|
|
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,
|
|
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 {
|