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