@guidelytech/sdk-core 0.10.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/dist/{capture-DVJyQ2JZ.d.cts → capture-CgN69Lxq.d.cts} +2 -2
- package/dist/{capture-369s4YYT.d.ts → capture-Csz721-A.d.ts} +2 -2
- package/dist/capture.cjs +2 -0
- package/dist/capture.cjs.map +1 -1
- package/dist/capture.d.cts +3 -3
- package/dist/capture.d.ts +3 -3
- package/dist/capture.js +1 -1
- package/dist/{chunk-C4WWSF67.js → chunk-JUSM7J5N.js} +3 -1
- package/dist/{chunk-C4WWSF67.js.map → chunk-JUSM7J5N.js.map} +1 -1
- package/dist/{chunk-GDJB466J.js → chunk-ZAF2KRJA.js} +29 -6
- package/dist/chunk-ZAF2KRJA.js.map +1 -0
- package/dist/headless.cjs +32 -4
- package/dist/headless.cjs.map +1 -1
- package/dist/headless.d.cts +6 -5
- package/dist/headless.d.ts +6 -5
- package/dist/headless.js +9 -3
- package/dist/{health-DFHUstWd.d.cts → health-sbP9JwTE.d.cts} +2 -0
- package/dist/{health-DFHUstWd.d.ts → health-sbP9JwTE.d.ts} +2 -0
- package/dist/{http-D0dCcpLN.d.cts → http-Cb6KYYFv.d.cts} +1 -1
- package/dist/{http-DJd8jRhK.d.ts → http-D047Ngxk.d.ts} +1 -1
- package/dist/index.cjs +1632 -1045
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +77 -9
- package/dist/index.d.ts +77 -9
- package/dist/index.js +760 -201
- package/dist/index.js.map +1 -1
- package/dist/recorder.cjs.map +1 -1
- package/dist/recorder.d.cts +1 -1
- package/dist/recorder.d.ts +1 -1
- package/dist/recorder.js +1 -1
- package/dist/{types-B5ecgHUV.d.cts → types-BDlebXrm.d.ts} +59 -5
- package/dist/{types-BtPovimB.d.ts → types-Mtlmlj5C.d.cts} +59 -5
- package/package.json +4 -4
- package/dist/chunk-GDJB466J.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -33,165 +33,370 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
|
|
|
33
33
|
var index_exports = {};
|
|
34
34
|
__export(index_exports, {
|
|
35
35
|
AUTHORING_PARAM: () => AUTHORING_PARAM,
|
|
36
|
-
CAPTURE_PARAM: () =>
|
|
36
|
+
CAPTURE_PARAM: () => import_runtime9.CAPTURE_PARAM,
|
|
37
37
|
DEFAULT_API_URL: () => DEFAULT_API_URL,
|
|
38
38
|
EventQueue: () => EventQueue,
|
|
39
39
|
Guidely: () => Guidely,
|
|
40
40
|
GuidelyClient: () => GuidelyClient,
|
|
41
41
|
HEARTBEAT_MS: () => HEARTBEAT_MS,
|
|
42
42
|
KEYS: () => KEYS,
|
|
43
|
+
PROTOTYPE_CLOSE_MESSAGE: () => PROTOTYPE_CLOSE_MESSAGE,
|
|
43
44
|
SDK_NAME: () => SDK_NAME,
|
|
44
45
|
SDK_VERSION: () => SDK_VERSION,
|
|
45
46
|
areMutuallyExclusive: () => areMutuallyExclusive,
|
|
46
47
|
buildContext: () => buildContext,
|
|
47
48
|
captureSnapshot: () => captureSnapshot,
|
|
49
|
+
countsTowardThrottle: () => countsTowardThrottle,
|
|
48
50
|
createGuidely: () => createGuidely,
|
|
49
51
|
createStorage: () => createStorage,
|
|
50
52
|
default: () => index_default,
|
|
51
53
|
detectDevice: () => detectDevice,
|
|
54
|
+
isAppUrl: () => isAppUrl,
|
|
52
55
|
isExclusiveGuide: () => isExclusiveGuide,
|
|
53
56
|
isFrequencyEligible: () => isFrequencyEligible,
|
|
57
|
+
isThrottleEligible: () => isThrottleEligible,
|
|
54
58
|
isWithinSchedule: () => isWithinSchedule,
|
|
59
|
+
matchPrototypeTestUrl: () => matchPrototypeTestUrl,
|
|
60
|
+
normalizeAppUrl: () => normalizeAppUrl,
|
|
61
|
+
pruneLaunches: () => pruneLaunches,
|
|
55
62
|
watchNavigation: () => watchNavigation
|
|
56
63
|
});
|
|
57
64
|
module.exports = __toCommonJS(index_exports);
|
|
58
65
|
|
|
59
66
|
// src/client.ts
|
|
60
|
-
var
|
|
61
|
-
var import_guide_runtime4 = require("@guidelytech/guide-runtime");
|
|
67
|
+
var import_runtime18 = require("@guidelytech/shared/runtime");
|
|
62
68
|
var import_guide_runtime5 = require("@guidelytech/guide-runtime");
|
|
69
|
+
var import_guide_runtime6 = require("@guidelytech/guide-runtime");
|
|
63
70
|
var import_flags = require("@guidelytech/flags");
|
|
64
71
|
|
|
65
|
-
// src/
|
|
66
|
-
var
|
|
72
|
+
// src/agent-turns.ts
|
|
73
|
+
var import_runtime4 = require("@guidelytech/shared/runtime");
|
|
67
74
|
|
|
68
|
-
// src/
|
|
69
|
-
var
|
|
70
|
-
var
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
75
|
+
// src/health.ts
|
|
76
|
+
var MAX_RETRY_AFTER_MS = 60 * 60 * 1e3;
|
|
77
|
+
var OFFLINE_WAIT_MS = 3e4;
|
|
78
|
+
var ServerHealth = class {
|
|
79
|
+
constructor(opts = {}) {
|
|
80
|
+
__publicField(this, "failures", 0);
|
|
81
|
+
__publicField(this, "openUntil", 0);
|
|
82
|
+
__publicField(this, "lastOkAt", 0);
|
|
83
|
+
__publicField(this, "listeners", /* @__PURE__ */ new Set());
|
|
84
|
+
__publicField(this, "threshold");
|
|
85
|
+
__publicField(this, "baseMs");
|
|
86
|
+
__publicField(this, "maxMs");
|
|
87
|
+
__publicField(this, "now");
|
|
88
|
+
__publicField(this, "random");
|
|
89
|
+
this.threshold = opts.threshold ?? 3;
|
|
90
|
+
this.baseMs = opts.baseMs ?? 5e3;
|
|
91
|
+
this.maxMs = opts.maxMs ?? 5 * 60 * 1e3;
|
|
92
|
+
this.now = opts.now ?? Date.now;
|
|
93
|
+
this.random = opts.random ?? Math.random;
|
|
77
94
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
95
|
+
/** The server answered (2xx, 3xx, or a 4xx other than 429): it is up. */
|
|
96
|
+
success() {
|
|
97
|
+
const wasDown = this.failures > 0 || this.openUntil > 0;
|
|
98
|
+
this.failures = 0;
|
|
99
|
+
this.openUntil = 0;
|
|
100
|
+
this.lastOkAt = this.now();
|
|
101
|
+
if (wasDown) this.notify();
|
|
83
102
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const css = rulesText(origStyles[i]?.sheet);
|
|
92
|
-
if (css !== null && css.length > (el.textContent ?? "").length) el.textContent = css;
|
|
93
|
-
});
|
|
94
|
-
const origFrames = doc.documentElement.querySelectorAll("iframe, canvas, video, embed, object");
|
|
95
|
-
clone.querySelectorAll("iframe, canvas, video, embed, object").forEach((el, i) => {
|
|
96
|
-
const r = origFrames[i]?.getBoundingClientRect();
|
|
97
|
-
const box = doc.createElement("div");
|
|
98
|
-
box.setAttribute("style", `${el.getAttribute("style") ?? ""};width:${r?.width ?? 0}px;height:${r?.height ?? 0}px;background:#e2e8f0;border-radius:4px`);
|
|
99
|
-
if (el.getAttribute("class")) box.setAttribute("class", el.getAttribute("class"));
|
|
100
|
-
el.replaceWith(box);
|
|
101
|
-
});
|
|
102
|
-
clone.querySelectorAll(STRIP).forEach((el) => el.remove());
|
|
103
|
-
for (const sel of maskSelectors) {
|
|
104
|
-
try {
|
|
105
|
-
clone.querySelectorAll(sel).forEach(maskText);
|
|
106
|
-
} catch {
|
|
103
|
+
/** The request failed or the server is overloaded. `retryAfterMs` comes from a `Retry-After` header. */
|
|
104
|
+
failure(retryAfterMs) {
|
|
105
|
+
this.failures += 1;
|
|
106
|
+
const now = this.now();
|
|
107
|
+
if (retryAfterMs != null && retryAfterMs > 0) {
|
|
108
|
+
this.openUntil = Math.max(this.openUntil, now + Math.min(retryAfterMs, MAX_RETRY_AFTER_MS));
|
|
109
|
+
return;
|
|
107
110
|
}
|
|
111
|
+
if (this.failures < this.threshold) return;
|
|
112
|
+
const cooldown = Math.min(this.maxMs, this.baseMs * 2 ** (this.failures - this.threshold));
|
|
113
|
+
this.openUntil = Math.max(this.openUntil, now + cooldown * (0.8 + this.random() * 0.4));
|
|
108
114
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
}
|
|
114
|
-
if (el instanceof HTMLInputElement) {
|
|
115
|
-
el.removeAttribute("value");
|
|
116
|
-
el.removeAttribute("checked");
|
|
117
|
-
} else if (el instanceof HTMLTextAreaElement) {
|
|
118
|
-
el.textContent = "";
|
|
119
|
-
}
|
|
120
|
-
el.removeAttribute("autofocus");
|
|
121
|
-
});
|
|
122
|
-
const adopted = Array.from(doc.adoptedStyleSheets ?? []).map(rulesText).filter(Boolean).join("\n");
|
|
123
|
-
let head = clone.querySelector("head");
|
|
124
|
-
if (!head) {
|
|
125
|
-
head = doc.createElement("head");
|
|
126
|
-
clone.prepend(head);
|
|
115
|
+
/** Milliseconds background traffic should wait before trying again (0: send now). */
|
|
116
|
+
waitMs() {
|
|
117
|
+
if (typeof navigator !== "undefined" && navigator.onLine === false) return OFFLINE_WAIT_MS;
|
|
118
|
+
return Math.max(0, this.openUntil - this.now());
|
|
127
119
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
style.textContent = adopted;
|
|
131
|
-
head.append(style);
|
|
120
|
+
canSend() {
|
|
121
|
+
return this.waitMs() === 0;
|
|
132
122
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
123
|
+
/**
|
|
124
|
+
* The last request got an answer. Unload beacons can't report back, so what they carry is only
|
|
125
|
+
* forgotten while this holds; otherwise it stays queued and is sent again (the server drops duplicates).
|
|
126
|
+
*/
|
|
127
|
+
get reachable() {
|
|
128
|
+
return this.failures === 0 && this.lastOkAt > 0;
|
|
129
|
+
}
|
|
130
|
+
/** The browser came back online: end the cooldown and let queued traffic try straight away. */
|
|
131
|
+
online() {
|
|
132
|
+
this.openUntil = 0;
|
|
133
|
+
this.notify();
|
|
134
|
+
}
|
|
135
|
+
/** Called when the server answers again after failures, or the browser comes back online. */
|
|
136
|
+
onRecover(cb) {
|
|
137
|
+
this.listeners.add(cb);
|
|
138
|
+
return () => this.listeners.delete(cb);
|
|
139
|
+
}
|
|
140
|
+
notify() {
|
|
141
|
+
for (const cb of Array.from(this.listeners)) {
|
|
142
|
+
try {
|
|
143
|
+
cb();
|
|
144
|
+
} catch {
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
function parseRetryAfter(value, now = Date.now()) {
|
|
150
|
+
if (!value) return null;
|
|
151
|
+
const trimmed = value.trim();
|
|
152
|
+
if (/^\d+$/.test(trimmed)) return Number(trimmed) * 1e3;
|
|
153
|
+
const at = Date.parse(trimmed);
|
|
154
|
+
return Number.isNaN(at) ? null : Math.max(0, at - now);
|
|
142
155
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
function memoryStore() {
|
|
146
|
-
const map = /* @__PURE__ */ new Map();
|
|
147
|
-
return { get: (k) => map.get(k) ?? null, set: (k, v) => void map.set(k, v), remove: (k) => void map.delete(k) };
|
|
156
|
+
function isServerFailure(status) {
|
|
157
|
+
return status === 429 || status >= 500;
|
|
148
158
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
159
|
+
|
|
160
|
+
// src/http.ts
|
|
161
|
+
var STREAM_RESPONSE_TIMEOUT_MS = 3e4;
|
|
162
|
+
var STREAM_IDLE_TIMEOUT_MS = 6e4;
|
|
163
|
+
var Api = class {
|
|
164
|
+
constructor(baseUrl, key, fetchImpl, health) {
|
|
165
|
+
__publicField(this, "baseUrl", baseUrl);
|
|
166
|
+
__publicField(this, "key", key);
|
|
167
|
+
__publicField(this, "fetchImpl", fetchImpl);
|
|
168
|
+
/** Reachability of the API, fed by every answer (see {@link ServerHealth}). */
|
|
169
|
+
__publicField(this, "health");
|
|
170
|
+
this.health = health ?? new ServerHealth();
|
|
171
|
+
}
|
|
172
|
+
/** Feeds an answer (or a failed request, `null`) into {@link health}. */
|
|
173
|
+
observe(res) {
|
|
174
|
+
if (!res) this.health.failure();
|
|
175
|
+
else if (isServerFailure(res.status)) this.health.failure(parseRetryAfter(res.headers?.get?.("retry-after")));
|
|
176
|
+
else this.health.success();
|
|
177
|
+
}
|
|
178
|
+
url(path, params = {}) {
|
|
179
|
+
const query = Object.entries({ key: this.key, ...params }).filter((e) => e[1] != null).map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join("&");
|
|
180
|
+
return `${this.baseUrl.replace(/\/$/, "")}/api/v1/sdk${path}?${query}`;
|
|
181
|
+
}
|
|
182
|
+
get fetcher() {
|
|
183
|
+
if (this.fetchImpl) return this.fetchImpl;
|
|
184
|
+
return typeof fetch === "function" ? fetch.bind(globalThis) : null;
|
|
185
|
+
}
|
|
186
|
+
async request(method, path, options = {}) {
|
|
187
|
+
const f = this.fetcher;
|
|
188
|
+
if (!f) throw new Error("fetch is not available");
|
|
189
|
+
const headers = { ...options.headers, "X-Guidely-Key": this.key, Accept: "application/json" };
|
|
190
|
+
const form = typeof FormData !== "undefined" && options.body instanceof FormData;
|
|
191
|
+
if (options.body !== void 0 && !form) headers["Content-Type"] = "application/json";
|
|
192
|
+
if (options.etag) headers["If-None-Match"] = options.etag;
|
|
193
|
+
const ac = typeof AbortController !== "undefined" ? new AbortController() : null;
|
|
194
|
+
const timer = ac ? setTimeout(() => ac.abort(), options.timeoutMs ?? 1e4) : null;
|
|
195
|
+
try {
|
|
196
|
+
let res;
|
|
197
|
+
try {
|
|
198
|
+
res = await f(this.url(path, options.params), {
|
|
199
|
+
method,
|
|
200
|
+
headers,
|
|
201
|
+
body: options.body === void 0 ? void 0 : form ? options.body : JSON.stringify(options.body),
|
|
202
|
+
keepalive: options.keepalive,
|
|
203
|
+
signal: ac?.signal,
|
|
204
|
+
credentials: "omit",
|
|
205
|
+
mode: "cors"
|
|
206
|
+
});
|
|
207
|
+
} catch (err) {
|
|
208
|
+
this.observe(null);
|
|
209
|
+
throw err;
|
|
210
|
+
}
|
|
211
|
+
this.observe(res);
|
|
212
|
+
let data = null;
|
|
213
|
+
if (res.status !== 304 && res.status !== 204) {
|
|
172
214
|
try {
|
|
173
|
-
|
|
215
|
+
data = await res.json();
|
|
174
216
|
} catch {
|
|
217
|
+
data = null;
|
|
175
218
|
}
|
|
176
219
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
220
|
+
return { status: res.status, ok: res.ok, data, etag: res.headers?.get?.("etag") ?? null };
|
|
221
|
+
} finally {
|
|
222
|
+
if (timer) clearTimeout(timer);
|
|
223
|
+
}
|
|
180
224
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
if (kind === "memory") return memoryStore();
|
|
184
|
-
return webStore(kind === "local" ? "localStorage" : "sessionStorage") ?? memoryStore();
|
|
185
|
-
}
|
|
186
|
-
function readJson(store, key, fallback) {
|
|
187
|
-
const raw = store.get(key);
|
|
188
|
-
if (raw == null) return fallback;
|
|
189
|
-
try {
|
|
190
|
-
return JSON.parse(raw);
|
|
191
|
-
} catch {
|
|
192
|
-
return fallback;
|
|
225
|
+
get(path, options) {
|
|
226
|
+
return this.request("GET", path, options);
|
|
193
227
|
}
|
|
194
|
-
|
|
228
|
+
post(path, body, options) {
|
|
229
|
+
return this.request("POST", path, { body, ...options });
|
|
230
|
+
}
|
|
231
|
+
/** Multipart POST (file uploads). No Content-Type header: the browser adds it with the boundary. */
|
|
232
|
+
postForm(path, form, options) {
|
|
233
|
+
return this.request("POST", path, { body: form, ...options });
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* POST that may answer with server-sent events. Each event is passed to `onEvent`; a JSON answer is
|
|
237
|
+
* returned instead. Scout replies can take a while, so there is no overall timeout: the response
|
|
238
|
+
* must start within 30 s and then never go quiet for more than 60 s (the server sends keepalives),
|
|
239
|
+
* otherwise the request is aborted and rejects.
|
|
240
|
+
*/
|
|
241
|
+
async stream(path, body, onEvent, headers = {}) {
|
|
242
|
+
const f = this.fetcher;
|
|
243
|
+
if (!f) throw new Error("fetch is not available");
|
|
244
|
+
const ac = typeof AbortController !== "undefined" ? new AbortController() : null;
|
|
245
|
+
let timer = null;
|
|
246
|
+
const arm = (ms) => {
|
|
247
|
+
if (!ac) return;
|
|
248
|
+
if (timer) clearTimeout(timer);
|
|
249
|
+
timer = setTimeout(() => ac.abort(), ms);
|
|
250
|
+
};
|
|
251
|
+
arm(STREAM_RESPONSE_TIMEOUT_MS);
|
|
252
|
+
try {
|
|
253
|
+
let res;
|
|
254
|
+
try {
|
|
255
|
+
res = await f(this.url(path), {
|
|
256
|
+
method: "POST",
|
|
257
|
+
headers: { ...headers, "X-Guidely-Key": this.key, Accept: "text/event-stream, application/json", "Content-Type": "application/json" },
|
|
258
|
+
body: JSON.stringify(body),
|
|
259
|
+
signal: ac?.signal,
|
|
260
|
+
credentials: "omit",
|
|
261
|
+
mode: "cors"
|
|
262
|
+
});
|
|
263
|
+
} catch (err) {
|
|
264
|
+
this.observe(null);
|
|
265
|
+
throw err;
|
|
266
|
+
}
|
|
267
|
+
this.observe(res);
|
|
268
|
+
const type = res.headers?.get?.("content-type") ?? "";
|
|
269
|
+
if (!type.includes("text/event-stream") || !res.body) {
|
|
270
|
+
let data = null;
|
|
271
|
+
try {
|
|
272
|
+
data = await res.json();
|
|
273
|
+
} catch {
|
|
274
|
+
data = null;
|
|
275
|
+
}
|
|
276
|
+
return { status: res.status, ok: res.ok, data, etag: null };
|
|
277
|
+
}
|
|
278
|
+
arm(STREAM_IDLE_TIMEOUT_MS);
|
|
279
|
+
await readEventStream(res.body, onEvent, () => arm(STREAM_IDLE_TIMEOUT_MS));
|
|
280
|
+
return { status: res.status, ok: res.ok, data: null, etag: null };
|
|
281
|
+
} finally {
|
|
282
|
+
if (timer) clearTimeout(timer);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
/** `navigator.sendBeacon` when available (page unload); returns false when it could not be queued. */
|
|
286
|
+
beacon(path, body) {
|
|
287
|
+
try {
|
|
288
|
+
if (typeof navigator === "undefined" || typeof navigator.sendBeacon !== "function") return false;
|
|
289
|
+
const blob = new Blob([JSON.stringify(body)], { type: "text/plain;charset=UTF-8" });
|
|
290
|
+
return navigator.sendBeacon(this.url(path), blob);
|
|
291
|
+
} catch {
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
};
|
|
296
|
+
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
297
|
+
function backoffMs(attempt, baseMs = 1e3, maxMs = 3e4) {
|
|
298
|
+
return Math.min(maxMs, baseMs * 2 ** Math.max(0, attempt));
|
|
299
|
+
}
|
|
300
|
+
async function withRetry(send, attempts = 3, baseMs = 500) {
|
|
301
|
+
for (let i = 0; i < attempts; i++) {
|
|
302
|
+
try {
|
|
303
|
+
const res = await send();
|
|
304
|
+
if (res.ok || res.status >= 400 && res.status < 500 && res.status !== 429) return res;
|
|
305
|
+
} catch {
|
|
306
|
+
}
|
|
307
|
+
if (i < attempts - 1) await sleep(backoffMs(i, baseMs, 8e3));
|
|
308
|
+
}
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
311
|
+
function postWithRetry(api, path, body, attempts = 3, baseMs = 500) {
|
|
312
|
+
return withRetry(() => api.post(path, body), attempts, baseMs);
|
|
313
|
+
}
|
|
314
|
+
async function readEventStream(body, onEvent, onChunk) {
|
|
315
|
+
const reader = body.getReader();
|
|
316
|
+
const decoder = new TextDecoder();
|
|
317
|
+
let buffer = "";
|
|
318
|
+
const dispatch = (raw) => {
|
|
319
|
+
let event = "message";
|
|
320
|
+
const data = [];
|
|
321
|
+
for (const line of raw.split("\n")) {
|
|
322
|
+
if (!line || line.startsWith(":")) continue;
|
|
323
|
+
const i = line.indexOf(":");
|
|
324
|
+
const field = i < 0 ? line : line.slice(0, i);
|
|
325
|
+
const value = i < 0 ? "" : line.slice(i + 1).replace(/^ /, "");
|
|
326
|
+
if (field === "event") event = value;
|
|
327
|
+
else if (field === "data") data.push(value);
|
|
328
|
+
}
|
|
329
|
+
if (!data.length) return;
|
|
330
|
+
try {
|
|
331
|
+
onEvent(event, JSON.parse(data.join("\n")));
|
|
332
|
+
} catch {
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
for (; ; ) {
|
|
336
|
+
const { done, value } = await reader.read();
|
|
337
|
+
if (value) onChunk?.();
|
|
338
|
+
if (value) buffer += decoder.decode(value, { stream: true }).replace(/\r\n?/g, "\n");
|
|
339
|
+
let sep;
|
|
340
|
+
while ((sep = buffer.indexOf("\n\n")) >= 0) {
|
|
341
|
+
dispatch(buffer.slice(0, sep));
|
|
342
|
+
buffer = buffer.slice(sep + 2);
|
|
343
|
+
}
|
|
344
|
+
if (done) break;
|
|
345
|
+
}
|
|
346
|
+
if (buffer.trim()) dispatch(buffer);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// src/storage.ts
|
|
350
|
+
function memoryStore() {
|
|
351
|
+
const map = /* @__PURE__ */ new Map();
|
|
352
|
+
return { get: (k) => map.get(k) ?? null, set: (k, v) => void map.set(k, v), remove: (k) => void map.delete(k) };
|
|
353
|
+
}
|
|
354
|
+
function webStore(area) {
|
|
355
|
+
try {
|
|
356
|
+
if (typeof window === "undefined") return null;
|
|
357
|
+
const s = window[area];
|
|
358
|
+
if (!s) return null;
|
|
359
|
+
const probe = "__guidely_probe__";
|
|
360
|
+
s.setItem(probe, "1");
|
|
361
|
+
s.removeItem(probe);
|
|
362
|
+
return {
|
|
363
|
+
get: (k) => {
|
|
364
|
+
try {
|
|
365
|
+
return s.getItem(k);
|
|
366
|
+
} catch {
|
|
367
|
+
return null;
|
|
368
|
+
}
|
|
369
|
+
},
|
|
370
|
+
set: (k, v) => {
|
|
371
|
+
try {
|
|
372
|
+
s.setItem(k, v);
|
|
373
|
+
} catch {
|
|
374
|
+
}
|
|
375
|
+
},
|
|
376
|
+
remove: (k) => {
|
|
377
|
+
try {
|
|
378
|
+
s.removeItem(k);
|
|
379
|
+
} catch {
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
};
|
|
383
|
+
} catch {
|
|
384
|
+
return null;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
function createStorage(kind = "local") {
|
|
388
|
+
if (kind === "memory") return memoryStore();
|
|
389
|
+
return webStore(kind === "local" ? "localStorage" : "sessionStorage") ?? memoryStore();
|
|
390
|
+
}
|
|
391
|
+
function readJson(store, key, fallback) {
|
|
392
|
+
const raw = store.get(key);
|
|
393
|
+
if (raw == null) return fallback;
|
|
394
|
+
try {
|
|
395
|
+
return JSON.parse(raw);
|
|
396
|
+
} catch {
|
|
397
|
+
return fallback;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
195
400
|
function writeJson(store, key, value) {
|
|
196
401
|
try {
|
|
197
402
|
store.set(key, JSON.stringify(value));
|
|
@@ -208,6 +413,8 @@ var KEYS = {
|
|
|
208
413
|
queue: (key) => `guidely:queue:${key}`,
|
|
209
414
|
/** Survey answers and guide/checklist progress not yet accepted by the server. */
|
|
210
415
|
outbox: (key) => `guidely:outbox:${key}`,
|
|
416
|
+
/** Agent Analytics turns not yet accepted by the server. */
|
|
417
|
+
agentOutbox: (key) => `guidely:agents:${key}`,
|
|
211
418
|
session: "guidely:session",
|
|
212
419
|
active: "guidely:active",
|
|
213
420
|
authoring: "guidely:authoring",
|
|
@@ -220,282 +427,256 @@ var KEYS = {
|
|
|
220
427
|
newsSeen: (key) => `guidely:news:${key}`
|
|
221
428
|
};
|
|
222
429
|
|
|
223
|
-
// src/
|
|
224
|
-
|
|
225
|
-
const m = await import("@guidelytech/shared");
|
|
226
|
-
return { StepSchema: m.StepSchema, GuideConfigSchema: m.GuideConfigSchema };
|
|
227
|
-
}
|
|
228
|
-
var AUTHORING_PARAM = "guidely_authoring";
|
|
229
|
-
var HEARTBEAT_MS = 15e3;
|
|
230
|
-
var SNAPSHOT_DEBOUNCE_MS = 800;
|
|
231
|
-
var SNAPSHOT_MAX_WAIT_MS = 4e3;
|
|
232
|
-
var AuthoringBridge = class {
|
|
430
|
+
// src/outbox.ts
|
|
431
|
+
var Outbox = class {
|
|
233
432
|
constructor(opts) {
|
|
234
433
|
__publicField(this, "opts", opts);
|
|
235
|
-
__publicField(this, "
|
|
236
|
-
__publicField(this, "
|
|
237
|
-
__publicField(this, "
|
|
238
|
-
__publicField(this, "
|
|
239
|
-
__publicField(this, "
|
|
240
|
-
__publicField(this, "
|
|
241
|
-
__publicField(this, "
|
|
242
|
-
__publicField(this, "
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
434
|
+
__publicField(this, "items");
|
|
435
|
+
__publicField(this, "sending", false);
|
|
436
|
+
__publicField(this, "attempt", 0);
|
|
437
|
+
__publicField(this, "timer", null);
|
|
438
|
+
__publicField(this, "destroyed", false);
|
|
439
|
+
__publicField(this, "offRecover");
|
|
440
|
+
__publicField(this, "maxItems");
|
|
441
|
+
__publicField(this, "maxAgeMs");
|
|
442
|
+
this.maxItems = opts.maxItems ?? 50;
|
|
443
|
+
this.maxAgeMs = opts.maxAgeMs ?? 7 * 24 * 60 * 60 * 1e3;
|
|
444
|
+
const stored = readJson(opts.store, opts.storageKey, []);
|
|
445
|
+
const cutoff = Date.now() - this.maxAgeMs;
|
|
446
|
+
this.items = Array.isArray(stored) ? stored.filter((i) => i && typeof i.path === "string" && typeof i.key === "string" && i.createdAt > cutoff).slice(-this.maxItems) : [];
|
|
447
|
+
this.offRecover = opts.health?.onRecover(() => {
|
|
448
|
+
this.attempt = 0;
|
|
449
|
+
this.retryIn(0);
|
|
450
|
+
}) ?? null;
|
|
451
|
+
if (this.items.length) this.retryIn(0);
|
|
246
452
|
}
|
|
247
|
-
|
|
248
|
-
return this.
|
|
453
|
+
get size() {
|
|
454
|
+
return this.items.length;
|
|
249
455
|
}
|
|
250
|
-
/**
|
|
251
|
-
|
|
252
|
-
if (
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
url.searchParams.delete(AUTHORING_PARAM);
|
|
259
|
-
window.history.replaceState(window.history.state, "", url.toString());
|
|
260
|
-
}
|
|
261
|
-
} catch {
|
|
262
|
-
token = null;
|
|
263
|
-
}
|
|
264
|
-
if (!token) {
|
|
265
|
-
const marker = readJson(this.opts.sessionStore, KEYS.authoring, null);
|
|
266
|
-
token = marker?.token ?? null;
|
|
267
|
-
}
|
|
268
|
-
if (!token) return false;
|
|
269
|
-
this.token = token;
|
|
270
|
-
writeJson(this.opts.sessionStore, KEYS.authoring, { token });
|
|
271
|
-
void this.start();
|
|
272
|
-
return true;
|
|
456
|
+
/** Queues `POST path` and sends it now (or as soon as the API is reachable). */
|
|
457
|
+
add(key, path, body) {
|
|
458
|
+
if (this.destroyed) return;
|
|
459
|
+
this.items = this.items.filter((i) => i.key !== key);
|
|
460
|
+
this.items.push({ key, path, body, createdAt: Date.now() });
|
|
461
|
+
if (this.items.length > this.maxItems) this.items.splice(0, this.items.length - this.maxItems);
|
|
462
|
+
this.persist();
|
|
463
|
+
void this.flush();
|
|
273
464
|
}
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
try {
|
|
277
|
-
const res = await this.opts.api.get("/authoring/session", { params: { token: this.token } });
|
|
278
|
-
if (!res.ok || !res.data) {
|
|
279
|
-
this.opts.sessionStore.remove(KEYS.authoring);
|
|
280
|
-
this.opts.onError(new Error(`[Guidely] authoring session invalid or expired (${res.status})`));
|
|
281
|
-
return;
|
|
282
|
-
}
|
|
283
|
-
this.info = res.data;
|
|
284
|
-
if (Date.parse(this.info.expiresAt) < Date.now()) {
|
|
285
|
-
this.opts.sessionStore.remove(KEYS.authoring);
|
|
286
|
-
this.opts.onError(new Error("[Guidely] authoring session expired"));
|
|
287
|
-
return;
|
|
288
|
-
}
|
|
289
|
-
this.active = true;
|
|
290
|
-
this.opts.onActive(true);
|
|
291
|
-
this.session = (0, import_guide_runtime.startAuthoring)({
|
|
292
|
-
dashboardOrigin: this.info.dashboardOrigin,
|
|
293
|
-
onPick: (pick) => this.sendPick(pick),
|
|
294
|
-
onExit: () => this.stop()
|
|
295
|
-
});
|
|
296
|
-
this.heartbeat = setInterval(() => {
|
|
297
|
-
void this.opts.api.post("/authoring/session/heartbeat", {}, { params: { token: this.token ?? void 0 } }).catch(() => {
|
|
298
|
-
});
|
|
299
|
-
}, HEARTBEAT_MS);
|
|
300
|
-
this.post({ type: "guidely:authoring-ready", sessionId: this.info.id, url: window.location.href });
|
|
301
|
-
this.watchPage();
|
|
302
|
-
} catch (err) {
|
|
303
|
-
this.opts.onError(err);
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
masked(el) {
|
|
307
|
-
return this.opts.maskSelectors.some((sel) => {
|
|
308
|
-
try {
|
|
309
|
-
return !!(0, import_guide_runtime.closestComposed)(el, sel);
|
|
310
|
-
} catch {
|
|
311
|
-
return false;
|
|
312
|
-
}
|
|
313
|
-
});
|
|
465
|
+
persist() {
|
|
466
|
+
writeJson(this.opts.store, this.opts.storageKey, this.items);
|
|
314
467
|
}
|
|
315
|
-
|
|
316
|
-
if (
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
468
|
+
retryIn(ms) {
|
|
469
|
+
if (this.destroyed) return;
|
|
470
|
+
if (this.timer) clearTimeout(this.timer);
|
|
471
|
+
this.timer = setTimeout(() => {
|
|
472
|
+
this.timer = null;
|
|
473
|
+
void this.flush();
|
|
474
|
+
}, ms);
|
|
322
475
|
}
|
|
323
|
-
/** Sends
|
|
324
|
-
|
|
325
|
-
this.
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
this.
|
|
476
|
+
/** Sends queued requests in order, one at a time. */
|
|
477
|
+
async flush() {
|
|
478
|
+
if (this.destroyed || this.sending || !this.items.length) return;
|
|
479
|
+
const wait = this.opts.health?.waitMs() ?? 0;
|
|
480
|
+
if (wait > 0) return this.retryIn(wait);
|
|
481
|
+
this.sending = true;
|
|
482
|
+
try {
|
|
483
|
+
while (this.items.length && !this.destroyed) {
|
|
484
|
+
const item = this.items[0];
|
|
485
|
+
let retry = false;
|
|
486
|
+
try {
|
|
487
|
+
const res = await this.opts.api.post(item.path, item.body);
|
|
488
|
+
retry = !res.ok && (res.status === 429 || res.status >= 500);
|
|
489
|
+
} catch (err) {
|
|
490
|
+
retry = true;
|
|
491
|
+
this.opts.onError?.(err);
|
|
492
|
+
}
|
|
493
|
+
if (retry) {
|
|
494
|
+
this.attempt += 1;
|
|
495
|
+
this.retryIn(Math.max(backoffMs(this.attempt - 1, 1e3, 6e4), this.opts.health?.waitMs() ?? 0));
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
this.attempt = 0;
|
|
499
|
+
this.items = this.items.filter((i) => i !== item);
|
|
500
|
+
this.persist();
|
|
501
|
+
}
|
|
502
|
+
} finally {
|
|
503
|
+
this.sending = false;
|
|
504
|
+
}
|
|
329
505
|
}
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
this.
|
|
506
|
+
destroy() {
|
|
507
|
+
this.destroyed = true;
|
|
508
|
+
this.offRecover?.();
|
|
509
|
+
if (this.timer) clearTimeout(this.timer);
|
|
510
|
+
this.timer = null;
|
|
335
511
|
}
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
512
|
+
};
|
|
513
|
+
|
|
514
|
+
// src/agent-turns.ts
|
|
515
|
+
var PENDING_MAX = 500;
|
|
516
|
+
function toolCalls(calls) {
|
|
517
|
+
if (!Array.isArray(calls)) return void 0;
|
|
518
|
+
return calls.filter((c) => !!c && typeof c.name === "string" && !!c.name).slice(0, import_runtime4.AGENT_TOOL_CALLS_PER_TURN).map((c) => ({
|
|
519
|
+
name: c.name.slice(0, 100),
|
|
520
|
+
...c.status === "ok" || c.status === "error" ? { status: c.status } : {},
|
|
521
|
+
...typeof c.latencyMs === "number" && Number.isFinite(c.latencyMs) && c.latencyMs >= 0 ? { latencyMs: Math.round(c.latencyMs) } : {}
|
|
522
|
+
}));
|
|
523
|
+
}
|
|
524
|
+
function prepareAgentTurn(turn, identity, redact, onError) {
|
|
525
|
+
if (!turn || typeof turn !== "object" || !turn.conversationId || !turn.agentId || !turn.role) return null;
|
|
526
|
+
let out = {
|
|
527
|
+
...turn,
|
|
528
|
+
type: "agent.conversation",
|
|
529
|
+
turnId: turn.turnId || `t_${(0, import_runtime4.randomId)(20)}`,
|
|
530
|
+
occurredAt: turn.occurredAt || (/* @__PURE__ */ new Date()).toISOString(),
|
|
531
|
+
anonymousId: turn.anonymousId ?? identity.anonymousId,
|
|
532
|
+
userId: turn.userId !== void 0 ? turn.userId : identity.userId,
|
|
533
|
+
accountId: turn.accountId !== void 0 ? turn.accountId : identity.accountId,
|
|
534
|
+
sessionId: turn.sessionId ?? identity.sessionId
|
|
535
|
+
};
|
|
536
|
+
if (redact) {
|
|
340
537
|
try {
|
|
341
|
-
const
|
|
342
|
-
if (!
|
|
343
|
-
|
|
344
|
-
this.post({ type: "guidely:snapshot", sessionId: this.info.id, snapshot });
|
|
538
|
+
const r = redact({ ...out, toolCalls: out.toolCalls ? out.toolCalls.map((c) => ({ ...c })) : void 0, metadata: out.metadata ? { ...out.metadata } : void 0 });
|
|
539
|
+
if (!r) return null;
|
|
540
|
+
out = { ...r, type: "agent.conversation", conversationId: out.conversationId, agentId: out.agentId, turnId: out.turnId, role: out.role };
|
|
345
541
|
} catch (err) {
|
|
346
|
-
|
|
542
|
+
onError?.(err);
|
|
543
|
+
return null;
|
|
347
544
|
}
|
|
348
545
|
}
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
546
|
+
if (typeof out.text === "string" && out.text.length > import_runtime4.AGENT_TURN_MAX_TEXT) out.text = out.text.slice(0, import_runtime4.AGENT_TURN_MAX_TEXT);
|
|
547
|
+
if (out.text !== void 0 && typeof out.text !== "string") out.text = String(out.text);
|
|
548
|
+
const calls = toolCalls(out.toolCalls);
|
|
549
|
+
if (calls) out.toolCalls = calls;
|
|
550
|
+
else delete out.toolCalls;
|
|
551
|
+
return out;
|
|
552
|
+
}
|
|
553
|
+
var AgentTurns = class {
|
|
554
|
+
constructor(opts) {
|
|
555
|
+
__publicField(this, "opts", opts);
|
|
556
|
+
__publicField(this, "outbox");
|
|
557
|
+
__publicField(this, "pending", []);
|
|
558
|
+
this.outbox = new Outbox({ api: opts.api, store: opts.store, storageKey: opts.storageKey, health: opts.health, maxItems: 20, maxAgeMs: 24 * 60 * 60 * 1e3 });
|
|
559
|
+
}
|
|
560
|
+
get queued() {
|
|
561
|
+
return this.outbox.size + this.pending.length;
|
|
562
|
+
}
|
|
563
|
+
/** Queues turns to send now, or (`hold`) until consent is granted. */
|
|
564
|
+
add(turns, hold = false) {
|
|
565
|
+
if (!turns.length) return;
|
|
566
|
+
if (hold) {
|
|
567
|
+
this.pending.push(...turns);
|
|
568
|
+
if (this.pending.length > PENDING_MAX) this.pending.splice(0, this.pending.length - PENDING_MAX);
|
|
569
|
+
return;
|
|
354
570
|
}
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
this.preview = null;
|
|
359
|
-
this.previewGuide?.destroy();
|
|
360
|
-
this.previewGuide = null;
|
|
361
|
-
this.session?.setPicking(true);
|
|
362
|
-
}
|
|
363
|
-
/** Handles `message` events; ignores anything not from `dashboardOrigin`. */
|
|
364
|
-
async handleMessage(e) {
|
|
365
|
-
if (!this.active || !this.info) return;
|
|
366
|
-
if (e.origin !== this.info.dashboardOrigin) return;
|
|
367
|
-
const msg = e.data;
|
|
368
|
-
if (!msg || typeof msg !== "object" || typeof msg.type !== "string") return;
|
|
369
|
-
try {
|
|
370
|
-
switch (msg.type) {
|
|
371
|
-
case "guidely:preview-step": {
|
|
372
|
-
this.stopPreview();
|
|
373
|
-
const { StepSchema, GuideConfigSchema } = await loadSchemas();
|
|
374
|
-
const step = StepSchema.parse(msg.step);
|
|
375
|
-
const appearance = msg.appearance ? GuideConfigSchema.shape.appearance.parse(msg.appearance) : void 0;
|
|
376
|
-
this.session?.setPicking(false);
|
|
377
|
-
this.session?.setStatus(`Previewing step \u201C${step.title || step.id}\u201D \u2014 close it to continue picking`);
|
|
378
|
-
this.preview = (0, import_guide_runtime.previewStep)(step, { ...this.opts.theme(), ...msg.theme || {} }, {
|
|
379
|
-
appearance,
|
|
380
|
-
onEnd: () => {
|
|
381
|
-
this.preview = null;
|
|
382
|
-
this.session?.setPicking(true);
|
|
383
|
-
this.session?.setStatus("Guidely authoring \u2014 hover an element and click to select \xB7 Esc to cancel");
|
|
384
|
-
this.post({ type: "guidely:preview-ended", sessionId: this.info.id, reason: "step" });
|
|
385
|
-
}
|
|
386
|
-
});
|
|
387
|
-
break;
|
|
388
|
-
}
|
|
389
|
-
case "guidely:preview-guide": {
|
|
390
|
-
this.stopPreview();
|
|
391
|
-
const g = msg.guide;
|
|
392
|
-
const guide = { id: g.id || "preview", key: g.key || "preview", name: g.name || "Preview", kind: g.kind || "flow", version: g.version ?? 0, config: (await loadSchemas()).GuideConfigSchema.parse(g.config) };
|
|
393
|
-
this.session?.setPicking(false);
|
|
394
|
-
this.session?.setStatus(`Previewing \u201C${guide.name}\u201D`);
|
|
395
|
-
this.previewGuide = (0, import_guide_runtime.runGuide)(guide, {
|
|
396
|
-
theme: this.opts.theme(),
|
|
397
|
-
ctx: this.opts.ctx,
|
|
398
|
-
onNavigate: (url, newTab) => newTab ? window.open(url, "_blank", "noopener") : window.location.assign(url),
|
|
399
|
-
onEnd: (reason) => {
|
|
400
|
-
this.previewGuide = null;
|
|
401
|
-
this.session?.setPicking(true);
|
|
402
|
-
this.post({ type: "guidely:preview-ended", sessionId: this.info.id, reason });
|
|
403
|
-
}
|
|
404
|
-
});
|
|
405
|
-
this.previewGuide.start();
|
|
406
|
-
break;
|
|
407
|
-
}
|
|
408
|
-
case "guidely:highlight": {
|
|
409
|
-
const result = (0, import_guide_runtime.highlightSelectors)(Array.isArray(msg.selectors) ? msg.selectors : [], { durationMs: 3e3 });
|
|
410
|
-
this.post({ type: "guidely:highlight-result", sessionId: this.info.id, counts: result.counts, resolved: !!result.resolved });
|
|
411
|
-
break;
|
|
412
|
-
}
|
|
413
|
-
case "guidely:stop-preview":
|
|
414
|
-
this.stopPreview();
|
|
415
|
-
break;
|
|
416
|
-
case "guidely:request-snapshot":
|
|
417
|
-
this.lastSnapshot = "";
|
|
418
|
-
this.sendSnapshot();
|
|
419
|
-
break;
|
|
420
|
-
case "guidely:ping":
|
|
421
|
-
this.post({ type: "guidely:pong", sessionId: this.info.id, url: window.location.href });
|
|
422
|
-
break;
|
|
423
|
-
case "guidely:exit":
|
|
424
|
-
this.stop();
|
|
425
|
-
break;
|
|
426
|
-
default:
|
|
427
|
-
break;
|
|
428
|
-
}
|
|
429
|
-
} catch (err) {
|
|
430
|
-
this.opts.onError(err);
|
|
571
|
+
for (let i = 0; i < turns.length; i += import_runtime4.AGENT_TURNS_PER_BATCH) {
|
|
572
|
+
const batch = turns.slice(i, i + import_runtime4.AGENT_TURNS_PER_BATCH);
|
|
573
|
+
this.outbox.add(`agent:${batch[0].turnId}`, "/agent-events", { turns: batch, sdk: this.opts.sdk });
|
|
431
574
|
}
|
|
432
575
|
}
|
|
433
|
-
/**
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
this.
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
this.
|
|
440
|
-
|
|
441
|
-
this.snapshotTimer = null;
|
|
442
|
-
if (this.heartbeat) clearInterval(this.heartbeat);
|
|
443
|
-
this.heartbeat = null;
|
|
444
|
-
this.session?.stop();
|
|
445
|
-
this.session = null;
|
|
446
|
-
this.opts.sessionStore.remove(KEYS.authoring);
|
|
447
|
-
if (this.info) this.post({ type: "guidely:exit", sessionId: this.info.id });
|
|
448
|
-
this.opts.onActive(false);
|
|
576
|
+
/** Consent changed: granted sends what was held, anything else drops it. */
|
|
577
|
+
consentChanged(granted) {
|
|
578
|
+
const held = this.pending.splice(0);
|
|
579
|
+
if (granted) this.add(held);
|
|
580
|
+
}
|
|
581
|
+
destroy() {
|
|
582
|
+
this.pending = [];
|
|
583
|
+
this.outbox.destroy();
|
|
449
584
|
}
|
|
450
585
|
};
|
|
451
586
|
|
|
452
|
-
// src/
|
|
453
|
-
var
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
587
|
+
// src/authoring.ts
|
|
588
|
+
var import_guide_runtime3 = require("@guidelytech/guide-runtime");
|
|
589
|
+
|
|
590
|
+
// src/snapshot.ts
|
|
591
|
+
var MAX_SNAPSHOT_BYTES = 6 * 1024 * 1024;
|
|
592
|
+
var STRIP = "script, noscript, template, #guidely-root, [data-guidely], meta[http-equiv], link[rel~='preload'], link[rel~='modulepreload'], link[rel~='prefetch'], link[rel~='manifest'], link[rel~='serviceworker']";
|
|
593
|
+
function rulesText(sheet) {
|
|
594
|
+
if (!sheet) return null;
|
|
458
595
|
try {
|
|
459
|
-
|
|
460
|
-
token = url.searchParams.get(import_runtime2.CAPTURE_PARAM);
|
|
461
|
-
authoringLink = url.searchParams.has(AUTHORING_PARAM);
|
|
462
|
-
if (token) {
|
|
463
|
-
url.searchParams.delete(import_runtime2.CAPTURE_PARAM);
|
|
464
|
-
window.history.replaceState(window.history.state, "", url.toString());
|
|
465
|
-
}
|
|
596
|
+
return Array.from(sheet.cssRules, (r) => r.cssText).join("\n");
|
|
466
597
|
} catch {
|
|
467
|
-
token = null;
|
|
468
|
-
}
|
|
469
|
-
const saved = readJson(sessionStore, KEYS.capture, null);
|
|
470
|
-
let marker = null;
|
|
471
|
-
if (token) marker = saved?.token === token ? saved : { token };
|
|
472
|
-
else if (saved?.token && !authoringLink) marker = saved;
|
|
473
|
-
if (!marker) {
|
|
474
|
-
if (saved) {
|
|
475
|
-
sessionStore.remove(KEYS.capture);
|
|
476
|
-
sessionStore.remove(KEYS.capturePending);
|
|
477
|
-
}
|
|
478
598
|
return null;
|
|
479
599
|
}
|
|
480
|
-
if (token && saved?.token !== token) sessionStore.remove(KEYS.capturePending);
|
|
481
|
-
writeJson(sessionStore, KEYS.capture, marker);
|
|
482
|
-
sessionStore.remove(KEYS.authoring);
|
|
483
|
-
return marker;
|
|
484
600
|
}
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
601
|
+
function maskText(root) {
|
|
602
|
+
const walker = root.ownerDocument.createTreeWalker(root, NodeFilter.SHOW_TEXT);
|
|
603
|
+
for (let n = walker.nextNode(); n; n = walker.nextNode()) {
|
|
604
|
+
n.textContent = (n.textContent ?? "").replace(/\S/g, "\u2022");
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
function captureSnapshot(doc, maskSelectors, maxBytes = MAX_SNAPSHOT_BYTES) {
|
|
608
|
+
const win = doc.defaultView;
|
|
609
|
+
if (!win || !doc.documentElement) return null;
|
|
610
|
+
const clone = doc.documentElement.cloneNode(true);
|
|
611
|
+
const origStyles = doc.documentElement.querySelectorAll("style");
|
|
612
|
+
clone.querySelectorAll("style").forEach((el, i) => {
|
|
613
|
+
const css = rulesText(origStyles[i]?.sheet);
|
|
614
|
+
if (css !== null && css.length > (el.textContent ?? "").length) el.textContent = css;
|
|
615
|
+
});
|
|
616
|
+
const origFrames = doc.documentElement.querySelectorAll("iframe, canvas, video, embed, object");
|
|
617
|
+
clone.querySelectorAll("iframe, canvas, video, embed, object").forEach((el, i) => {
|
|
618
|
+
const r = origFrames[i]?.getBoundingClientRect();
|
|
619
|
+
const box = doc.createElement("div");
|
|
620
|
+
box.setAttribute("style", `${el.getAttribute("style") ?? ""};width:${r?.width ?? 0}px;height:${r?.height ?? 0}px;background:#e2e8f0;border-radius:4px`);
|
|
621
|
+
if (el.getAttribute("class")) box.setAttribute("class", el.getAttribute("class"));
|
|
622
|
+
el.replaceWith(box);
|
|
623
|
+
});
|
|
624
|
+
clone.querySelectorAll(STRIP).forEach((el) => el.remove());
|
|
625
|
+
for (const sel of maskSelectors) {
|
|
626
|
+
try {
|
|
627
|
+
clone.querySelectorAll(sel).forEach(maskText);
|
|
628
|
+
} catch {
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
clone.querySelectorAll("*").forEach((el) => {
|
|
632
|
+
for (const attr2 of Array.from(el.attributes)) {
|
|
633
|
+
const name = attr2.name.toLowerCase();
|
|
634
|
+
if (name.startsWith("on") || (name === "href" || name === "src" || name === "action") && /^\s*javascript:/i.test(attr2.value)) el.removeAttribute(attr2.name);
|
|
635
|
+
}
|
|
636
|
+
if (el instanceof HTMLInputElement) {
|
|
637
|
+
el.removeAttribute("value");
|
|
638
|
+
el.removeAttribute("checked");
|
|
639
|
+
} else if (el instanceof HTMLTextAreaElement) {
|
|
640
|
+
el.textContent = "";
|
|
641
|
+
}
|
|
642
|
+
el.removeAttribute("autofocus");
|
|
643
|
+
});
|
|
644
|
+
const adopted = Array.from(doc.adoptedStyleSheets ?? []).map(rulesText).filter(Boolean).join("\n");
|
|
645
|
+
let head = clone.querySelector("head");
|
|
646
|
+
if (!head) {
|
|
647
|
+
head = doc.createElement("head");
|
|
648
|
+
clone.prepend(head);
|
|
649
|
+
}
|
|
650
|
+
if (adopted) {
|
|
651
|
+
const style = doc.createElement("style");
|
|
652
|
+
style.textContent = adopted;
|
|
653
|
+
head.append(style);
|
|
654
|
+
}
|
|
655
|
+
clone.querySelectorAll("base").forEach((el) => el.remove());
|
|
656
|
+
const base = doc.createElement("base");
|
|
657
|
+
base.setAttribute("href", doc.baseURI || win.location.href);
|
|
658
|
+
base.setAttribute("target", "_blank");
|
|
659
|
+
head.prepend(base);
|
|
660
|
+
const html = `<!DOCTYPE html>
|
|
661
|
+
${clone.outerHTML}`;
|
|
662
|
+
if (html.length > maxBytes) return null;
|
|
663
|
+
return { html, url: win.location.href, title: doc.title, width: win.innerWidth, height: win.innerHeight, capturedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// src/tagging.ts
|
|
667
|
+
var import_runtime7 = require("@guidelytech/shared/runtime");
|
|
668
|
+
var import_guide_runtime2 = require("@guidelytech/guide-runtime");
|
|
669
|
+
|
|
670
|
+
// src/autocapture.ts
|
|
671
|
+
var import_guide_runtime = require("@guidelytech/guide-runtime");
|
|
672
|
+
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]";
|
|
673
|
+
var DEFAULT_MASK_SELECTORS = ["[data-guidely-mask]", ".guidely-mask"];
|
|
674
|
+
var PREFERRED_ATTRS = ["data-testid", "data-test-id", "data-test", "data-cy", "data-qa", "data-guidely-track", "data-tour"];
|
|
675
|
+
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;
|
|
676
|
+
function validSelectors(list) {
|
|
677
|
+
const out = [];
|
|
678
|
+
const probe = typeof document !== "undefined" ? document.createDocumentFragment() : null;
|
|
679
|
+
for (const raw of list) {
|
|
499
680
|
const s = (raw || "").trim();
|
|
500
681
|
if (!s || out.includes(s)) continue;
|
|
501
682
|
try {
|
|
@@ -507,11 +688,11 @@ function validSelectors(list) {
|
|
|
507
688
|
return out;
|
|
508
689
|
}
|
|
509
690
|
function isIgnoredElement(el) {
|
|
510
|
-
return (0,
|
|
691
|
+
return (0, import_guide_runtime.isGuidelyElement)(el) || !!(0, import_guide_runtime.closestComposed)(el, "[data-guidely-ignore]");
|
|
511
692
|
}
|
|
512
693
|
function interactiveElement(target) {
|
|
513
694
|
try {
|
|
514
|
-
return (0,
|
|
695
|
+
return (0, import_guide_runtime.closestComposed)(target, INTERACTIVE_SELECTOR);
|
|
515
696
|
} catch {
|
|
516
697
|
return null;
|
|
517
698
|
}
|
|
@@ -523,12 +704,12 @@ function esc(value) {
|
|
|
523
704
|
var attr = (tag, name, value) => value ? `${tag}[${name}="${value.replace(/["\\]/g, "\\$&")}"]` : `${tag}[${name}]`;
|
|
524
705
|
function elementSelector(el) {
|
|
525
706
|
const own = ownSelector(el);
|
|
526
|
-
const chain = (0,
|
|
527
|
-
return (chain ? chain +
|
|
707
|
+
const chain = (0, import_guide_runtime.shadowHostChain)(el);
|
|
708
|
+
return (chain ? chain + import_guide_runtime.SHADOW_SEPARATOR + own : own).slice(0, 300);
|
|
528
709
|
}
|
|
529
710
|
function ownSelector(el) {
|
|
530
711
|
const tag = el.tagName.toLowerCase();
|
|
531
|
-
if (el.id && (0,
|
|
712
|
+
if (el.id && (0, import_guide_runtime.isStableId)(el.id)) return `#${esc(el.id)}`;
|
|
532
713
|
for (const name of PREFERRED_ATTRS) if (el.hasAttribute(name)) return attr(tag, name, el.getAttribute(name) || "");
|
|
533
714
|
for (const a of Array.from(el.attributes)) {
|
|
534
715
|
if (a.name.startsWith("data-") && !NOISY_DATA_ATTR.test(a.name) && a.value && a.value.length <= 60) return attr(tag, a.name, a.value);
|
|
@@ -540,7 +721,7 @@ function ownSelector(el) {
|
|
|
540
721
|
while (cur && parts.length < 4) {
|
|
541
722
|
const t = cur.tagName.toLowerCase();
|
|
542
723
|
if (t === "html" || t === "body") break;
|
|
543
|
-
if (cur !== el && cur.id && (0,
|
|
724
|
+
if (cur !== el && cur.id && (0, import_guide_runtime.isStableId)(cur.id)) {
|
|
544
725
|
parts.unshift(`#${esc(cur.id)}`);
|
|
545
726
|
break;
|
|
546
727
|
}
|
|
@@ -555,7 +736,7 @@ function ownSelector(el) {
|
|
|
555
736
|
function matches(el, selector, deep) {
|
|
556
737
|
if (!selector) return false;
|
|
557
738
|
try {
|
|
558
|
-
return !!(0,
|
|
739
|
+
return !!(0, import_guide_runtime.closestComposed)(el, selector) || deep && !!el.querySelector(selector);
|
|
559
740
|
} catch {
|
|
560
741
|
return false;
|
|
561
742
|
}
|
|
@@ -573,7 +754,7 @@ function elementText(el, mask, maskAll) {
|
|
|
573
754
|
return text ? text.slice(0, 100) : void 0;
|
|
574
755
|
}
|
|
575
756
|
function linkHref(el) {
|
|
576
|
-
const a = (0,
|
|
757
|
+
const a = (0, import_guide_runtime.closestComposed)(el, "a[href]");
|
|
577
758
|
if (!a) return void 0;
|
|
578
759
|
try {
|
|
579
760
|
const u = new URL(a.getAttribute("href") || "", location.href);
|
|
@@ -619,564 +800,713 @@ var RageClickDetector = class {
|
|
|
619
800
|
}
|
|
620
801
|
};
|
|
621
802
|
|
|
622
|
-
// src/
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
else if (/Firefox\/|FxiOS/i.test(u)) browser = "firefox";
|
|
636
|
-
else if (/Safari\//i.test(u) && /Version\//i.test(u)) browser = "safari";
|
|
637
|
-
else if (/MSIE|Trident/i.test(u)) browser = "ie";
|
|
638
|
-
let os;
|
|
639
|
-
if (/Windows/i.test(u)) os = "windows";
|
|
640
|
-
else if (/iPhone|iPad|iPod/i.test(u)) os = "ios";
|
|
641
|
-
else if (/Mac OS X|Macintosh/i.test(u)) os = "macos";
|
|
642
|
-
else if (/Android/i.test(u)) os = "android";
|
|
643
|
-
else if (/CrOS/i.test(u)) os = "chromeos";
|
|
644
|
-
else if (/Linux/i.test(u)) os = "linux";
|
|
645
|
-
return { type, browser, os };
|
|
803
|
+
// src/tagging.ts
|
|
804
|
+
var TEXT_ATTRS = /* @__PURE__ */ new Set(["aria-label", "title", "alt"]);
|
|
805
|
+
var SAFE_ATTRS = ["role", "name", "type", "aria-label", "title", "alt", "for"];
|
|
806
|
+
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;
|
|
807
|
+
function nthOfType(el) {
|
|
808
|
+
const parent = el.parentElement;
|
|
809
|
+
if (!parent) return void 0;
|
|
810
|
+
let n = 0;
|
|
811
|
+
for (const c of Array.from(parent.children)) {
|
|
812
|
+
if (c.tagName === el.tagName) n++;
|
|
813
|
+
if (c === el) return n;
|
|
814
|
+
}
|
|
815
|
+
return void 0;
|
|
646
816
|
}
|
|
647
|
-
function
|
|
648
|
-
|
|
649
|
-
|
|
817
|
+
function nodeOf(el, masked, featureAttribute) {
|
|
818
|
+
const node = { t: el.tagName.toLowerCase().slice(0, import_runtime7.SNAPSHOT_LIMITS.tag) };
|
|
819
|
+
if (el.id && (0, import_guide_runtime2.isStableId)(el.id) && el.id.length <= import_runtime7.SNAPSHOT_LIMITS.id) node.id = el.id;
|
|
820
|
+
const classes = Array.from(el.classList).filter((c) => (0, import_guide_runtime2.isStableClass)(c) && !STATE_CLASS.test(c) && c.length <= import_runtime7.SNAPSHOT_LIMITS.className);
|
|
821
|
+
if (classes.length) node.c = classes.slice(0, import_runtime7.SNAPSHOT_LIMITS.classes);
|
|
822
|
+
const a = {};
|
|
823
|
+
const put = (name, value) => {
|
|
824
|
+
if (value === null || Object.keys(a).length >= import_runtime7.SNAPSHOT_LIMITS.attrs || name in a) return;
|
|
825
|
+
if (masked && TEXT_ATTRS.has(name)) return;
|
|
826
|
+
a[name] = value.slice(0, import_runtime7.SNAPSHOT_LIMITS.attrValue);
|
|
827
|
+
};
|
|
828
|
+
if (featureAttribute && el.hasAttribute(featureAttribute)) put(featureAttribute, el.getAttribute(featureAttribute));
|
|
829
|
+
for (const attr2 of Array.from(el.attributes)) {
|
|
830
|
+
if (attr2.name.startsWith("data-") && !import_runtime7.NOISY_DATA_ATTR.test(attr2.name) && attr2.value.length <= import_runtime7.SNAPSHOT_LIMITS.attrValue) put(attr2.name, attr2.value);
|
|
831
|
+
}
|
|
832
|
+
for (const name of SAFE_ATTRS) if (el.hasAttribute(name)) put(name, el.getAttribute(name));
|
|
833
|
+
if (Object.keys(a).length) node.a = a;
|
|
834
|
+
const n = nthOfType(el);
|
|
835
|
+
if (n) node.n = n;
|
|
836
|
+
return node;
|
|
837
|
+
}
|
|
838
|
+
function elementSnapshot(el, opts) {
|
|
839
|
+
const masked = opts.maskAll || isMasked(el, opts.mask);
|
|
840
|
+
const snap = nodeOf(el, masked, opts.featureAttribute);
|
|
841
|
+
const p = [];
|
|
842
|
+
for (let cur = el.parentElement; cur && p.length < import_runtime7.SNAPSHOT_LIMITS.ancestors; cur = cur.parentElement) {
|
|
843
|
+
if (cur.tagName === "BODY" || cur.tagName === "HTML") break;
|
|
844
|
+
p.push(nodeOf(cur, masked, opts.featureAttribute));
|
|
845
|
+
}
|
|
846
|
+
if (p.length) snap.p = p;
|
|
847
|
+
const h = [];
|
|
848
|
+
let root = el.getRootNode?.();
|
|
849
|
+
while (root && root.nodeType === 11 && root.host && h.length < import_runtime7.SNAPSHOT_LIMITS.hosts) {
|
|
850
|
+
h.push(nodeOf(root.host, masked, opts.featureAttribute));
|
|
851
|
+
root = root.host.getRootNode?.();
|
|
852
|
+
}
|
|
853
|
+
if (h.length) snap.h = h;
|
|
854
|
+
return snap;
|
|
855
|
+
}
|
|
856
|
+
function isMasked(el, mask) {
|
|
857
|
+
if (!mask) return false;
|
|
650
858
|
try {
|
|
651
|
-
|
|
652
|
-
path = u.pathname;
|
|
653
|
-
u.searchParams.forEach((v, k) => {
|
|
654
|
-
query[k] = v;
|
|
655
|
-
});
|
|
859
|
+
return !!(0, import_guide_runtime2.closestComposed)(el, mask);
|
|
656
860
|
} catch {
|
|
657
|
-
|
|
861
|
+
return false;
|
|
658
862
|
}
|
|
659
|
-
const device = input.device ?? (typeof navigator !== "undefined" ? detectDevice(navigator.userAgent, typeof window !== "undefined" ? window.innerWidth : 0) : { type: "desktop" });
|
|
660
|
-
return {
|
|
661
|
-
anonymousId: input.anonymousId,
|
|
662
|
-
userId: input.userId ?? null,
|
|
663
|
-
userAttributes: input.userAttributes,
|
|
664
|
-
accountAttributes: input.accountAttributes,
|
|
665
|
-
url: input.url,
|
|
666
|
-
path,
|
|
667
|
-
query,
|
|
668
|
-
device,
|
|
669
|
-
firstVisit: input.firstVisit,
|
|
670
|
-
eventsSeen: new Set(input.eventsSeen),
|
|
671
|
-
completedGuides: new Set(input.completedGuides),
|
|
672
|
-
dismissedGuides: new Set(input.dismissedGuides),
|
|
673
|
-
seenGuides: new Set(input.seenGuides),
|
|
674
|
-
completedChecklists: new Set(input.completedChecklists),
|
|
675
|
-
audienceIds: new Set(input.audienceIds),
|
|
676
|
-
now: input.now ?? /* @__PURE__ */ new Date(),
|
|
677
|
-
environment: input.environment,
|
|
678
|
-
locale: input.locale ?? (typeof navigator !== "undefined" ? navigator.language : void 0),
|
|
679
|
-
platform: input.platform ?? "web",
|
|
680
|
-
...input.screen !== void 0 ? { screen: input.screen } : {},
|
|
681
|
-
...input.appVersion !== void 0 ? { appVersion: input.appVersion } : {}
|
|
682
|
-
};
|
|
683
863
|
}
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
const s = elementSelector(el).slice(0, 120);
|
|
693
|
-
if (s) p.s = s;
|
|
694
|
-
const rect = el.getBoundingClientRect();
|
|
695
|
-
if (rect.width > 0 && rect.height > 0) {
|
|
696
|
-
p.rx = r2((e.clientX - rect.left) / rect.width);
|
|
697
|
-
p.ry = r2((e.clientY - rect.top) / rect.height);
|
|
864
|
+
function featureAttributeOf(target, attribute) {
|
|
865
|
+
if (!attribute) return null;
|
|
866
|
+
try {
|
|
867
|
+
const el = (0, import_guide_runtime2.closestComposed)(target, `[${attribute}]`);
|
|
868
|
+
const value = el?.getAttribute(attribute)?.replace(/\s+/g, " ").trim();
|
|
869
|
+
return el && value ? { el, value: value.slice(0, 100) } : null;
|
|
870
|
+
} catch {
|
|
871
|
+
return null;
|
|
698
872
|
}
|
|
699
|
-
return p;
|
|
700
|
-
}
|
|
701
|
-
var selectors = /* @__PURE__ */ new WeakMap();
|
|
702
|
-
var r1 = (n) => Math.round(Math.min(1, Math.max(0, n)) * 10) / 10;
|
|
703
|
-
function movementBin(el, e) {
|
|
704
|
-
const p = { k: "m", x: Math.round(e.pageX ?? 0) || 0, y: Math.round(e.pageY ?? 0) || 0 };
|
|
705
|
-
let s = /^(HTML|BODY)$/.test(el.tagName) ? "" : selectors.get(el);
|
|
706
|
-
if (s === void 0) selectors.set(el, s = elementSelector(el).slice(0, 120));
|
|
707
|
-
const rect = s ? el.getBoundingClientRect() : null;
|
|
708
|
-
if (!rect || !(rect.width > 0 && rect.height > 0)) return [`${p.x >> 4},${p.y >> 4}`, p];
|
|
709
|
-
p.s = s;
|
|
710
|
-
p.rx = r1((e.clientX - rect.left) / rect.width);
|
|
711
|
-
p.ry = r1((e.clientY - rect.top) / rect.height);
|
|
712
|
-
return [`${s} ${p.rx},${p.ry}`, p];
|
|
713
873
|
}
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
}
|
|
722
|
-
/** Adds a click (`burst` > 0 marks it and the burst's previous clicks as rage); true when the batch is full. */
|
|
723
|
-
add(url, point, burst) {
|
|
724
|
-
this.url = url;
|
|
725
|
-
for (let i = Math.max(0, this.points.length - burst + 1); i < this.points.length; i++) this.points[i].k = "r";
|
|
726
|
-
this.points.push(point);
|
|
727
|
-
this.bytes += JSON.stringify(point).length + 1;
|
|
728
|
-
return this.full();
|
|
729
|
-
}
|
|
730
|
-
/** Adds a movement sample to its bin (weight +1); true when the batch is full. */
|
|
731
|
-
move(url, [key, point]) {
|
|
732
|
-
this.url = url;
|
|
733
|
-
const bin = this.bins.get(key);
|
|
734
|
-
if (bin) bin.w = (bin.w ?? 1) + 1;
|
|
735
|
-
else {
|
|
736
|
-
this.bins.set(key, point);
|
|
737
|
-
this.bytes += JSON.stringify(point).length + 8;
|
|
874
|
+
function readProperty(def, base, opts) {
|
|
875
|
+
let el = base;
|
|
876
|
+
if (def.selector) {
|
|
877
|
+
try {
|
|
878
|
+
el = (0, import_guide_runtime2.querySelectorStrategy)({ kind: "css", value: def.selector }, document)[0] ?? null;
|
|
879
|
+
} catch {
|
|
880
|
+
el = null;
|
|
738
881
|
}
|
|
739
|
-
return this.full();
|
|
740
|
-
}
|
|
741
|
-
full() {
|
|
742
|
-
return this.points.length >= HEATMAP_MAX_POINTS || this.bins.size > HEATMAP_MAX_BINS || this.bytes >= HEATMAP_MAX_BYTES;
|
|
743
882
|
}
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
const
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
return
|
|
883
|
+
if (!el) return void 0;
|
|
884
|
+
if (def.source === "attribute") {
|
|
885
|
+
const name = (def.attribute ?? "").toLowerCase();
|
|
886
|
+
if (!name || name === "value" || name.startsWith("on")) return void 0;
|
|
887
|
+
if (opts.maskAll || isMasked(el, opts.mask)) return void 0;
|
|
888
|
+
const v = el.getAttribute(name);
|
|
889
|
+
return v ? v.replace(/\s+/g, " ").trim().slice(0, import_runtime7.TAG_PROPERTY_LIMITS.value) || void 0 : void 0;
|
|
751
890
|
}
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
// src/errors.ts
|
|
755
|
-
var import_runtime5 = require("@guidelytech/shared/runtime");
|
|
756
|
-
var MAX_FRAMES_BYTES = 6e3;
|
|
757
|
-
function fitFrames(frames) {
|
|
758
|
-
const out = frames.slice(0, 30);
|
|
759
|
-
while (out.length > 1 && JSON.stringify(out).length > MAX_FRAMES_BYTES) out.pop();
|
|
760
|
-
return out;
|
|
891
|
+
return elementText(el, opts.mask, opts.maskAll);
|
|
761
892
|
}
|
|
762
|
-
function
|
|
763
|
-
if (
|
|
893
|
+
function clickPropertyValues(config, el, click, opts) {
|
|
894
|
+
if (!config?.properties.length) return void 0;
|
|
895
|
+
const out = {};
|
|
896
|
+
const css = (selector) => {
|
|
897
|
+
try {
|
|
898
|
+
return !!(0, import_guide_runtime2.closestComposed)(el, selector);
|
|
899
|
+
} catch {
|
|
900
|
+
return false;
|
|
901
|
+
}
|
|
902
|
+
};
|
|
903
|
+
const here = { url: location.href, path: location.pathname, title: document.title };
|
|
904
|
+
for (const tag of config.properties) {
|
|
905
|
+
if (tag.kind !== "feature" || !tag.featureRules) continue;
|
|
906
|
+
const rules = tag.featureRules;
|
|
907
|
+
const matches2 = (r) => (0, import_runtime7.featureRuleMatchesClick)(r, click, css) && (!r.page?.pattern || (0, import_runtime7.pageRuleMatches)({ type: "url", value: r.page.pattern }, here));
|
|
908
|
+
if (!rules.some((r) => !r.exclude && matches2(r)) || rules.some((r) => r.exclude && matches2(r))) continue;
|
|
909
|
+
for (const def of tag.properties) {
|
|
910
|
+
const v = readProperty(def, el, opts);
|
|
911
|
+
if (v) out[def.id] = v;
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
return Object.keys(out).length ? out : void 0;
|
|
915
|
+
}
|
|
916
|
+
function pagePropertyValues(config, url, opts) {
|
|
917
|
+
if (!config?.properties.length) return void 0;
|
|
918
|
+
const out = {};
|
|
919
|
+
let path = "/";
|
|
764
920
|
try {
|
|
765
|
-
|
|
921
|
+
path = new URL(url, location.href).pathname;
|
|
766
922
|
} catch {
|
|
767
|
-
return String(value);
|
|
768
923
|
}
|
|
924
|
+
const ctx = { url, path, title: document.title };
|
|
925
|
+
for (const tag of config.properties) {
|
|
926
|
+
if (tag.kind !== "page" || !tag.pageRules || !(0, import_runtime7.pageRulesMatch)(tag.pageRules, ctx)) continue;
|
|
927
|
+
for (const def of tag.properties) {
|
|
928
|
+
if (!def.selector) continue;
|
|
929
|
+
const v = readProperty(def, null, opts);
|
|
930
|
+
if (v) out[def.id] = v;
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
return Object.keys(out).length ? out : void 0;
|
|
769
934
|
}
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
message: (isError ? e.message : describe(error)).slice(0, 1e3),
|
|
776
|
-
frames: isError ? fitFrames((0, import_runtime5.parseStack)(e.stack)) : [],
|
|
777
|
-
source,
|
|
778
|
-
handled
|
|
779
|
-
};
|
|
780
|
-
}
|
|
781
|
-
function errorEventToException(e) {
|
|
782
|
-
const target = e.target;
|
|
783
|
-
if (target && typeof Element !== "undefined" && target instanceof Element) return null;
|
|
784
|
-
if (e.error != null) return exceptionFromError(e.error, "onerror", false);
|
|
785
|
-
const message = e.message || "";
|
|
786
|
-
if (!message || /^script error\.?$/i.test(message.trim())) return null;
|
|
787
|
-
const m = /^(?:Uncaught )?([A-Z]\w*(?:Error|Exception)):\s*([\s\S]*)$/.exec(message);
|
|
788
|
-
return {
|
|
789
|
-
type: m?.[1] ?? "Error",
|
|
790
|
-
message: (m?.[2] ?? message).slice(0, 1e3),
|
|
791
|
-
frames: e.filename ? [{ file: e.filename.slice(0, 500), line: e.lineno || void 0, col: e.colno || void 0 }] : [],
|
|
792
|
-
source: "onerror",
|
|
793
|
-
handled: false
|
|
794
|
-
};
|
|
935
|
+
|
|
936
|
+
// src/authoring.ts
|
|
937
|
+
async function loadSchemas() {
|
|
938
|
+
const m = await import("@guidelytech/shared");
|
|
939
|
+
return { StepSchema: m.StepSchema, GuideConfigSchema: m.GuideConfigSchema };
|
|
795
940
|
}
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
var
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
941
|
+
var AUTHORING_PARAM = "guidely_authoring";
|
|
942
|
+
var HEARTBEAT_MS = 15e3;
|
|
943
|
+
var SNAPSHOT_DEBOUNCE_MS = 800;
|
|
944
|
+
var SNAPSHOT_MAX_WAIT_MS = 4e3;
|
|
945
|
+
var AuthoringBridge = class {
|
|
946
|
+
constructor(opts) {
|
|
947
|
+
__publicField(this, "opts", opts);
|
|
948
|
+
__publicField(this, "token", null);
|
|
949
|
+
__publicField(this, "info", null);
|
|
950
|
+
__publicField(this, "session", null);
|
|
951
|
+
__publicField(this, "heartbeat", null);
|
|
952
|
+
__publicField(this, "preview", null);
|
|
953
|
+
__publicField(this, "previewGuide", null);
|
|
954
|
+
__publicField(this, "active", false);
|
|
955
|
+
__publicField(this, "observer", null);
|
|
956
|
+
__publicField(this, "snapshotTimer", null);
|
|
957
|
+
__publicField(this, "snapshotDue", 0);
|
|
958
|
+
__publicField(this, "lastSnapshot", "");
|
|
808
959
|
}
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
constructor(max = 5) {
|
|
812
|
-
__publicField(this, "max", max);
|
|
813
|
-
__publicField(this, "seen", /* @__PURE__ */ new Map());
|
|
960
|
+
isActive() {
|
|
961
|
+
return this.active;
|
|
814
962
|
}
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
963
|
+
/** Returns true when a token was found (in the URL or the session marker) and verification started. */
|
|
964
|
+
detect() {
|
|
965
|
+
if (typeof window === "undefined") return false;
|
|
966
|
+
let token = null;
|
|
967
|
+
try {
|
|
968
|
+
const url = new URL(window.location.href);
|
|
969
|
+
token = url.searchParams.get(AUTHORING_PARAM);
|
|
970
|
+
if (token) {
|
|
971
|
+
url.searchParams.delete(AUTHORING_PARAM);
|
|
972
|
+
window.history.replaceState(window.history.state, "", url.toString());
|
|
973
|
+
}
|
|
974
|
+
} catch {
|
|
975
|
+
token = null;
|
|
976
|
+
}
|
|
977
|
+
if (!token) {
|
|
978
|
+
const marker = readJson(this.opts.sessionStore, KEYS.authoring, null);
|
|
979
|
+
token = marker?.token ?? null;
|
|
980
|
+
}
|
|
981
|
+
if (!token) return false;
|
|
982
|
+
this.token = token;
|
|
983
|
+
writeJson(this.opts.sessionStore, KEYS.authoring, { token });
|
|
984
|
+
void this.start();
|
|
822
985
|
return true;
|
|
823
986
|
}
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
987
|
+
async start() {
|
|
988
|
+
if (!this.token) return;
|
|
989
|
+
try {
|
|
990
|
+
const res = await this.opts.api.get("/authoring/session", { params: { token: this.token } });
|
|
991
|
+
if (!res.ok || !res.data) {
|
|
992
|
+
this.opts.sessionStore.remove(KEYS.authoring);
|
|
993
|
+
this.opts.onError(new Error(`[Guidely] authoring session invalid or expired (${res.status})`));
|
|
994
|
+
return;
|
|
995
|
+
}
|
|
996
|
+
this.info = res.data;
|
|
997
|
+
if (Date.parse(this.info.expiresAt) < Date.now()) {
|
|
998
|
+
this.opts.sessionStore.remove(KEYS.authoring);
|
|
999
|
+
this.opts.onError(new Error("[Guidely] authoring session expired"));
|
|
1000
|
+
return;
|
|
1001
|
+
}
|
|
1002
|
+
this.active = true;
|
|
1003
|
+
this.opts.onActive(true);
|
|
1004
|
+
this.session = (0, import_guide_runtime3.startAuthoring)({
|
|
1005
|
+
dashboardOrigin: this.info.dashboardOrigin,
|
|
1006
|
+
// Tag mode (Pages & Features): picks become feature rules; the page itself is tagged from the dashboard.
|
|
1007
|
+
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,
|
|
1008
|
+
onPick: (pick) => this.sendPick(pick),
|
|
1009
|
+
onExit: () => this.stop()
|
|
1010
|
+
});
|
|
1011
|
+
this.heartbeat = setInterval(() => {
|
|
1012
|
+
void this.opts.api.post("/authoring/session/heartbeat", {}, { params: { token: this.token ?? void 0 } }).catch(() => {
|
|
1013
|
+
});
|
|
1014
|
+
}, HEARTBEAT_MS);
|
|
1015
|
+
this.post({ type: "guidely:authoring-ready", sessionId: this.info.id, url: window.location.href });
|
|
1016
|
+
this.watchPage();
|
|
1017
|
+
} catch (err) {
|
|
1018
|
+
this.opts.onError(err);
|
|
837
1019
|
}
|
|
838
|
-
set.add(cb);
|
|
839
|
-
return () => this.off(event, cb);
|
|
840
1020
|
}
|
|
841
|
-
|
|
842
|
-
this.
|
|
843
|
-
}
|
|
844
|
-
emit(event, ...args) {
|
|
845
|
-
const set = this.listeners.get(event);
|
|
846
|
-
if (!set) return;
|
|
847
|
-
for (const cb of Array.from(set)) {
|
|
1021
|
+
masked(el) {
|
|
1022
|
+
return this.opts.maskSelectors.some((sel) => {
|
|
848
1023
|
try {
|
|
849
|
-
|
|
850
|
-
} catch
|
|
851
|
-
|
|
1024
|
+
return !!(0, import_guide_runtime3.closestComposed)(el, sel);
|
|
1025
|
+
} catch {
|
|
1026
|
+
return false;
|
|
852
1027
|
}
|
|
853
|
-
}
|
|
854
|
-
}
|
|
855
|
-
clear() {
|
|
856
|
-
this.listeners.clear();
|
|
857
|
-
}
|
|
858
|
-
};
|
|
859
|
-
|
|
860
|
-
// src/frequency.ts
|
|
861
|
-
var HOUR = 3600 * 1e3;
|
|
862
|
-
function isFrequencyEligible(freq, state, shownThisSession, now = Date.now()) {
|
|
863
|
-
const s = state ?? { impressions: 0 };
|
|
864
|
-
if (freq.maxImpressions != null && s.impressions >= freq.maxImpressions) return false;
|
|
865
|
-
if (s.completedAt && freq.mode !== "every_time") return false;
|
|
866
|
-
if (s.dismissedAt) {
|
|
867
|
-
if (!freq.reshowAfterDismiss) return false;
|
|
868
|
-
if (freq.cooldownHours > 0 && now - Date.parse(s.dismissedAt) < freq.cooldownHours * HOUR) return false;
|
|
1028
|
+
});
|
|
869
1029
|
}
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
return now - Date.parse(s.lastShownAt) >= hours * HOUR;
|
|
879
|
-
}
|
|
880
|
-
case "every_time":
|
|
881
|
-
default:
|
|
882
|
-
return true;
|
|
1030
|
+
sendPick(raw) {
|
|
1031
|
+
if (!this.info || !this.token) return;
|
|
1032
|
+
const { element, ...rest } = raw;
|
|
1033
|
+
const pick = this.masked(element) ? { ...rest, textSnapshot: "", selectors: rest.selectors.filter((s) => s.kind !== "text") } : rest;
|
|
1034
|
+
pick.url = window.location.href;
|
|
1035
|
+
if (this.info.purpose === "tag") pick.elementSnapshot = elementSnapshot(element, { mask: this.opts.maskSelectors.join(","), maskAll: false, featureAttribute: null });
|
|
1036
|
+
this.post({ type: "guidely:pick", sessionId: this.info.id, pick });
|
|
1037
|
+
void this.opts.api.post("/authoring/session/picks", pick, { params: { token: this.token } }).catch((err) => this.opts.onError(err));
|
|
883
1038
|
}
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
}
|
|
891
|
-
function areMutuallyExclusive(a, b) {
|
|
892
|
-
const la = a.config.frequency.mutuallyExclusiveWith;
|
|
893
|
-
const lb = b.config.frequency.mutuallyExclusiveWith;
|
|
894
|
-
return la.includes(b.id) || la.includes(b.key) || lb.includes(a.id) || lb.includes(a.key);
|
|
895
|
-
}
|
|
896
|
-
function isWithinSchedule(guide, now = Date.now()) {
|
|
897
|
-
const s = guide.config.survey;
|
|
898
|
-
if (!s) return true;
|
|
899
|
-
if (s.startAt && now < Date.parse(s.startAt)) return false;
|
|
900
|
-
if (s.endAt && now > Date.parse(s.endAt)) return false;
|
|
901
|
-
return true;
|
|
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;
|
|
1039
|
+
/** Sends a snapshot now and again whenever the page settles after DOM changes or navigation. */
|
|
1040
|
+
watchPage() {
|
|
1041
|
+
this.scheduleSnapshot();
|
|
1042
|
+
if (typeof MutationObserver === "undefined") return;
|
|
1043
|
+
this.observer = new MutationObserver(() => this.scheduleSnapshot());
|
|
1044
|
+
this.observer.observe(document.documentElement, { subtree: true, childList: true, attributes: true, characterData: true });
|
|
927
1045
|
}
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
this.
|
|
932
|
-
this.
|
|
933
|
-
this.lastOkAt = this.now();
|
|
934
|
-
if (wasDown) this.notify();
|
|
1046
|
+
scheduleSnapshot() {
|
|
1047
|
+
const now = Date.now();
|
|
1048
|
+
if (!this.snapshotTimer) this.snapshotDue = now + SNAPSHOT_MAX_WAIT_MS;
|
|
1049
|
+
else clearTimeout(this.snapshotTimer);
|
|
1050
|
+
this.snapshotTimer = setTimeout(() => this.sendSnapshot(), Math.max(0, Math.min(SNAPSHOT_DEBOUNCE_MS, this.snapshotDue - now)));
|
|
935
1051
|
}
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
this.
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
return;
|
|
1052
|
+
sendSnapshot() {
|
|
1053
|
+
if (this.snapshotTimer) clearTimeout(this.snapshotTimer);
|
|
1054
|
+
this.snapshotTimer = null;
|
|
1055
|
+
if (!this.info || !this.active) return;
|
|
1056
|
+
try {
|
|
1057
|
+
const snapshot = captureSnapshot(document, this.opts.maskSelectors);
|
|
1058
|
+
if (!snapshot || snapshot.html === this.lastSnapshot) return;
|
|
1059
|
+
this.lastSnapshot = snapshot.html;
|
|
1060
|
+
this.post({ type: "guidely:snapshot", sessionId: this.info.id, snapshot });
|
|
1061
|
+
} catch (err) {
|
|
1062
|
+
this.opts.onError(err);
|
|
943
1063
|
}
|
|
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
1064
|
}
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
1065
|
+
post(message) {
|
|
1066
|
+
if (!this.info) return;
|
|
1067
|
+
try {
|
|
1068
|
+
window.opener?.postMessage(message, this.info.dashboardOrigin);
|
|
1069
|
+
} catch {
|
|
1070
|
+
}
|
|
952
1071
|
}
|
|
953
|
-
|
|
954
|
-
|
|
1072
|
+
stopPreview() {
|
|
1073
|
+
this.preview?.destroy();
|
|
1074
|
+
this.preview = null;
|
|
1075
|
+
this.previewGuide?.destroy();
|
|
1076
|
+
this.previewGuide = null;
|
|
1077
|
+
this.session?.setPicking(true);
|
|
955
1078
|
}
|
|
956
|
-
/**
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
1079
|
+
/** Handles `message` events; ignores anything not from `dashboardOrigin`. */
|
|
1080
|
+
async handleMessage(e) {
|
|
1081
|
+
if (!this.active || !this.info) return;
|
|
1082
|
+
if (e.origin !== this.info.dashboardOrigin) return;
|
|
1083
|
+
const msg = e.data;
|
|
1084
|
+
if (!msg || typeof msg !== "object" || typeof msg.type !== "string") return;
|
|
1085
|
+
try {
|
|
1086
|
+
switch (msg.type) {
|
|
1087
|
+
case "guidely:preview-step": {
|
|
1088
|
+
this.stopPreview();
|
|
1089
|
+
const { StepSchema, GuideConfigSchema } = await loadSchemas();
|
|
1090
|
+
const step = StepSchema.parse(msg.step);
|
|
1091
|
+
const appearance = msg.appearance ? GuideConfigSchema.shape.appearance.parse(msg.appearance) : void 0;
|
|
1092
|
+
this.session?.setPicking(false);
|
|
1093
|
+
this.session?.setStatus(`Previewing step \u201C${step.title || step.id}\u201D \u2014 close it to continue picking`);
|
|
1094
|
+
this.preview = (0, import_guide_runtime3.previewStep)(step, { ...this.opts.theme(), ...msg.theme || {} }, {
|
|
1095
|
+
appearance,
|
|
1096
|
+
onEnd: () => {
|
|
1097
|
+
this.preview = null;
|
|
1098
|
+
this.session?.setPicking(true);
|
|
1099
|
+
this.session?.setStatus("Guidely authoring \u2014 hover an element and click to select \xB7 Esc to cancel");
|
|
1100
|
+
this.post({ type: "guidely:preview-ended", sessionId: this.info.id, reason: "step" });
|
|
1101
|
+
}
|
|
1102
|
+
});
|
|
1103
|
+
break;
|
|
1104
|
+
}
|
|
1105
|
+
case "guidely:preview-guide": {
|
|
1106
|
+
this.stopPreview();
|
|
1107
|
+
const g = msg.guide;
|
|
1108
|
+
const guide = { id: g.id || "preview", key: g.key || "preview", name: g.name || "Preview", kind: g.kind || "flow", version: g.version ?? 0, config: (await loadSchemas()).GuideConfigSchema.parse(g.config) };
|
|
1109
|
+
this.session?.setPicking(false);
|
|
1110
|
+
this.session?.setStatus(`Previewing \u201C${guide.name}\u201D`);
|
|
1111
|
+
this.previewGuide = (0, import_guide_runtime3.runGuide)(guide, {
|
|
1112
|
+
theme: this.opts.theme(),
|
|
1113
|
+
ctx: this.opts.ctx,
|
|
1114
|
+
onNavigate: (url, newTab) => newTab ? window.open(url, "_blank", "noopener") : window.location.assign(url),
|
|
1115
|
+
onEnd: (reason) => {
|
|
1116
|
+
this.previewGuide = null;
|
|
1117
|
+
this.session?.setPicking(true);
|
|
1118
|
+
this.post({ type: "guidely:preview-ended", sessionId: this.info.id, reason });
|
|
1119
|
+
}
|
|
1120
|
+
});
|
|
1121
|
+
this.previewGuide.start();
|
|
1122
|
+
break;
|
|
1123
|
+
}
|
|
1124
|
+
case "guidely:highlight": {
|
|
1125
|
+
const result = (0, import_guide_runtime3.highlightSelectors)(Array.isArray(msg.selectors) ? msg.selectors : [], { durationMs: 3e3 });
|
|
1126
|
+
this.post({ type: "guidely:highlight-result", sessionId: this.info.id, counts: result.counts, resolved: !!result.resolved });
|
|
1127
|
+
break;
|
|
1128
|
+
}
|
|
1129
|
+
case "guidely:stop-preview":
|
|
1130
|
+
this.stopPreview();
|
|
1131
|
+
break;
|
|
1132
|
+
case "guidely:request-snapshot":
|
|
1133
|
+
this.lastSnapshot = "";
|
|
1134
|
+
this.sendSnapshot();
|
|
1135
|
+
break;
|
|
1136
|
+
case "guidely:ping":
|
|
1137
|
+
this.post({ type: "guidely:pong", sessionId: this.info.id, url: window.location.href });
|
|
1138
|
+
break;
|
|
1139
|
+
case "guidely:exit":
|
|
1140
|
+
this.stop();
|
|
1141
|
+
break;
|
|
1142
|
+
default:
|
|
1143
|
+
break;
|
|
1144
|
+
}
|
|
1145
|
+
} catch (err) {
|
|
1146
|
+
this.opts.onError(err);
|
|
1147
|
+
}
|
|
962
1148
|
}
|
|
963
|
-
/**
|
|
964
|
-
|
|
965
|
-
this.
|
|
966
|
-
this.
|
|
1149
|
+
/** Leaves authoring mode and clears the session marker. */
|
|
1150
|
+
stop() {
|
|
1151
|
+
if (!this.active) return;
|
|
1152
|
+
this.active = false;
|
|
1153
|
+
this.stopPreview();
|
|
1154
|
+
this.observer?.disconnect();
|
|
1155
|
+
this.observer = null;
|
|
1156
|
+
if (this.snapshotTimer) clearTimeout(this.snapshotTimer);
|
|
1157
|
+
this.snapshotTimer = null;
|
|
1158
|
+
if (this.heartbeat) clearInterval(this.heartbeat);
|
|
1159
|
+
this.heartbeat = null;
|
|
1160
|
+
this.session?.stop();
|
|
1161
|
+
this.session = null;
|
|
1162
|
+
this.opts.sessionStore.remove(KEYS.authoring);
|
|
1163
|
+
if (this.info) this.post({ type: "guidely:exit", sessionId: this.info.id });
|
|
1164
|
+
this.opts.onActive(false);
|
|
967
1165
|
}
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
1166
|
+
};
|
|
1167
|
+
|
|
1168
|
+
// src/capture-token.ts
|
|
1169
|
+
var import_runtime9 = require("@guidelytech/shared/runtime");
|
|
1170
|
+
function takeCaptureToken(sessionStore) {
|
|
1171
|
+
if (typeof window === "undefined") return null;
|
|
1172
|
+
let token = null;
|
|
1173
|
+
let authoringLink = false;
|
|
1174
|
+
try {
|
|
1175
|
+
const url = new URL(window.location.href);
|
|
1176
|
+
token = url.searchParams.get(import_runtime9.CAPTURE_PARAM);
|
|
1177
|
+
authoringLink = url.searchParams.has(AUTHORING_PARAM);
|
|
1178
|
+
if (token) {
|
|
1179
|
+
url.searchParams.delete(import_runtime9.CAPTURE_PARAM);
|
|
1180
|
+
window.history.replaceState(window.history.state, "", url.toString());
|
|
1181
|
+
}
|
|
1182
|
+
} catch {
|
|
1183
|
+
token = null;
|
|
972
1184
|
}
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
1185
|
+
const saved = readJson(sessionStore, KEYS.capture, null);
|
|
1186
|
+
let marker = null;
|
|
1187
|
+
if (token) marker = saved?.token === token ? saved : { token };
|
|
1188
|
+
else if (saved?.token && !authoringLink) marker = saved;
|
|
1189
|
+
if (!marker) {
|
|
1190
|
+
if (saved) {
|
|
1191
|
+
sessionStore.remove(KEYS.capture);
|
|
1192
|
+
sessionStore.remove(KEYS.capturePending);
|
|
979
1193
|
}
|
|
1194
|
+
return null;
|
|
980
1195
|
}
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
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;
|
|
1196
|
+
if (token && saved?.token !== token) sessionStore.remove(KEYS.capturePending);
|
|
1197
|
+
writeJson(sessionStore, KEYS.capture, marker);
|
|
1198
|
+
sessionStore.remove(KEYS.authoring);
|
|
1199
|
+
return marker;
|
|
991
1200
|
}
|
|
992
1201
|
|
|
993
|
-
// src/
|
|
994
|
-
var
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1202
|
+
// src/load-capture.ts
|
|
1203
|
+
var loadCapture = async () => (await import("./capture.cjs")).capture;
|
|
1204
|
+
|
|
1205
|
+
// src/context.ts
|
|
1206
|
+
function detectDevice(ua, width) {
|
|
1207
|
+
const u = ua || "";
|
|
1208
|
+
let type = "desktop";
|
|
1209
|
+
if (/iPad|Tablet|PlayBook|Silk/i.test(u) || /Android/i.test(u) && !/Mobile/i.test(u)) type = "tablet";
|
|
1210
|
+
else if (/Mobi|iPhone|iPod|Android.*Mobile|Windows Phone/i.test(u)) type = "mobile";
|
|
1211
|
+
else if (width > 0 && width < 768) type = "mobile";
|
|
1212
|
+
else if (width > 0 && width < 1024) type = "tablet";
|
|
1213
|
+
let browser;
|
|
1214
|
+
if (/Electron\//i.test(u)) browser = "electron";
|
|
1215
|
+
else if (/Edg\//i.test(u)) browser = "edge";
|
|
1216
|
+
else if (/OPR\/|Opera/i.test(u)) browser = "opera";
|
|
1217
|
+
else if (/SamsungBrowser/i.test(u)) browser = "samsung";
|
|
1218
|
+
else if (/Chrome\/|CriOS/i.test(u)) browser = "chrome";
|
|
1219
|
+
else if (/Firefox\/|FxiOS/i.test(u)) browser = "firefox";
|
|
1220
|
+
else if (/Safari\//i.test(u) && /Version\//i.test(u)) browser = "safari";
|
|
1221
|
+
else if (/MSIE|Trident/i.test(u)) browser = "ie";
|
|
1222
|
+
let os;
|
|
1223
|
+
if (/Windows/i.test(u)) os = "windows";
|
|
1224
|
+
else if (/iPhone|iPad|iPod/i.test(u)) os = "ios";
|
|
1225
|
+
else if (/Mac OS X|Macintosh/i.test(u)) os = "macos";
|
|
1226
|
+
else if (/Android/i.test(u)) os = "android";
|
|
1227
|
+
else if (/CrOS/i.test(u)) os = "chromeos";
|
|
1228
|
+
else if (/Linux/i.test(u)) os = "linux";
|
|
1229
|
+
return { type, browser, os };
|
|
1230
|
+
}
|
|
1231
|
+
function buildContext(input) {
|
|
1232
|
+
let path = "";
|
|
1233
|
+
const query = {};
|
|
1234
|
+
try {
|
|
1235
|
+
const u = new URL(input.url, "http://localhost");
|
|
1236
|
+
path = u.pathname;
|
|
1237
|
+
u.searchParams.forEach((v, k) => {
|
|
1238
|
+
query[k] = v;
|
|
1239
|
+
});
|
|
1240
|
+
} catch {
|
|
1241
|
+
path = input.url;
|
|
1004
1242
|
}
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1243
|
+
const device = input.device ?? (typeof navigator !== "undefined" ? detectDevice(navigator.userAgent, typeof window !== "undefined" ? window.innerWidth : 0) : { type: "desktop" });
|
|
1244
|
+
return {
|
|
1245
|
+
anonymousId: input.anonymousId,
|
|
1246
|
+
userId: input.userId ?? null,
|
|
1247
|
+
userAttributes: input.userAttributes,
|
|
1248
|
+
accountAttributes: input.accountAttributes,
|
|
1249
|
+
url: input.url,
|
|
1250
|
+
path,
|
|
1251
|
+
query,
|
|
1252
|
+
device,
|
|
1253
|
+
firstVisit: input.firstVisit,
|
|
1254
|
+
eventsSeen: new Set(input.eventsSeen),
|
|
1255
|
+
completedGuides: new Set(input.completedGuides),
|
|
1256
|
+
dismissedGuides: new Set(input.dismissedGuides),
|
|
1257
|
+
seenGuides: new Set(input.seenGuides),
|
|
1258
|
+
completedChecklists: new Set(input.completedChecklists),
|
|
1259
|
+
audienceIds: new Set(input.audienceIds),
|
|
1260
|
+
now: input.now ?? /* @__PURE__ */ new Date(),
|
|
1261
|
+
environment: input.environment,
|
|
1262
|
+
locale: input.locale ?? (typeof navigator !== "undefined" ? navigator.language : void 0),
|
|
1263
|
+
platform: input.platform ?? "web",
|
|
1264
|
+
...input.screen !== void 0 ? { screen: input.screen } : {},
|
|
1265
|
+
...input.appVersion !== void 0 ? { appVersion: input.appVersion } : {}
|
|
1266
|
+
};
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
// src/heatmap.ts
|
|
1270
|
+
var HEATMAP_MAX_POINTS = 40;
|
|
1271
|
+
var HEATMAP_MAX_BYTES = 6e3;
|
|
1272
|
+
var HEATMAP_MAX_BINS = 60;
|
|
1273
|
+
var r2 = (n) => Math.round(Math.min(1, Math.max(0, n)) * 100) / 100;
|
|
1274
|
+
function heatmapPoint(el, e, rage) {
|
|
1275
|
+
const p = { k: rage ? "r" : "c", x: Math.round(e.pageX ?? 0) || 0, y: Math.round(e.pageY ?? 0) || 0 };
|
|
1276
|
+
const s = elementSelector(el).slice(0, 120);
|
|
1277
|
+
if (s) p.s = s;
|
|
1278
|
+
const rect = el.getBoundingClientRect();
|
|
1279
|
+
if (rect.width > 0 && rect.height > 0) {
|
|
1280
|
+
p.rx = r2((e.clientX - rect.left) / rect.width);
|
|
1281
|
+
p.ry = r2((e.clientY - rect.top) / rect.height);
|
|
1010
1282
|
}
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1283
|
+
return p;
|
|
1284
|
+
}
|
|
1285
|
+
var selectors = /* @__PURE__ */ new WeakMap();
|
|
1286
|
+
var r1 = (n) => Math.round(Math.min(1, Math.max(0, n)) * 10) / 10;
|
|
1287
|
+
function movementBin(el, e) {
|
|
1288
|
+
const p = { k: "m", x: Math.round(e.pageX ?? 0) || 0, y: Math.round(e.pageY ?? 0) || 0 };
|
|
1289
|
+
let s = /^(HTML|BODY)$/.test(el.tagName) ? "" : selectors.get(el);
|
|
1290
|
+
if (s === void 0) selectors.set(el, s = elementSelector(el).slice(0, 120));
|
|
1291
|
+
const rect = s ? el.getBoundingClientRect() : null;
|
|
1292
|
+
if (!rect || !(rect.width > 0 && rect.height > 0)) return [`${p.x >> 4},${p.y >> 4}`, p];
|
|
1293
|
+
p.s = s;
|
|
1294
|
+
p.rx = r1((e.clientX - rect.left) / rect.width);
|
|
1295
|
+
p.ry = r1((e.clientY - rect.top) / rect.height);
|
|
1296
|
+
return [`${s} ${p.rx},${p.ry}`, p];
|
|
1297
|
+
}
|
|
1298
|
+
var HeatmapBuffer = class {
|
|
1299
|
+
constructor() {
|
|
1300
|
+
/** The page the buffered points belong to. */
|
|
1301
|
+
__publicField(this, "url", "");
|
|
1302
|
+
__publicField(this, "points", []);
|
|
1303
|
+
__publicField(this, "bins", /* @__PURE__ */ new Map());
|
|
1304
|
+
__publicField(this, "bytes", 0);
|
|
1014
1305
|
}
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1306
|
+
/** Adds a click (`burst` > 0 marks it and the burst's previous clicks as rage); true when the batch is full. */
|
|
1307
|
+
add(url, point, burst) {
|
|
1308
|
+
this.url = url;
|
|
1309
|
+
for (let i = Math.max(0, this.points.length - burst + 1); i < this.points.length; i++) this.points[i].k = "r";
|
|
1310
|
+
this.points.push(point);
|
|
1311
|
+
this.bytes += JSON.stringify(point).length + 1;
|
|
1312
|
+
return this.full();
|
|
1018
1313
|
}
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
const
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
const timer = ac ? setTimeout(() => ac.abort(), options.timeoutMs ?? 1e4) : null;
|
|
1028
|
-
try {
|
|
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);
|
|
1045
|
-
let data = null;
|
|
1046
|
-
if (res.status !== 304 && res.status !== 204) {
|
|
1047
|
-
try {
|
|
1048
|
-
data = await res.json();
|
|
1049
|
-
} catch {
|
|
1050
|
-
data = null;
|
|
1051
|
-
}
|
|
1052
|
-
}
|
|
1053
|
-
return { status: res.status, ok: res.ok, data, etag: res.headers?.get?.("etag") ?? null };
|
|
1054
|
-
} finally {
|
|
1055
|
-
if (timer) clearTimeout(timer);
|
|
1314
|
+
/** Adds a movement sample to its bin (weight +1); true when the batch is full. */
|
|
1315
|
+
move(url, [key, point]) {
|
|
1316
|
+
this.url = url;
|
|
1317
|
+
const bin = this.bins.get(key);
|
|
1318
|
+
if (bin) bin.w = (bin.w ?? 1) + 1;
|
|
1319
|
+
else {
|
|
1320
|
+
this.bins.set(key, point);
|
|
1321
|
+
this.bytes += JSON.stringify(point).length + 8;
|
|
1056
1322
|
}
|
|
1323
|
+
return this.full();
|
|
1057
1324
|
}
|
|
1058
|
-
|
|
1059
|
-
return this.
|
|
1325
|
+
full() {
|
|
1326
|
+
return this.points.length >= HEATMAP_MAX_POINTS || this.bins.size > HEATMAP_MAX_BINS || this.bytes >= HEATMAP_MAX_BYTES;
|
|
1060
1327
|
}
|
|
1061
|
-
|
|
1062
|
-
|
|
1328
|
+
/** Empties the buffer, returning what was in it (clicks first, then movement bins). */
|
|
1329
|
+
take() {
|
|
1330
|
+
const points = [...this.points, ...this.bins.values()];
|
|
1331
|
+
this.points = [];
|
|
1332
|
+
this.bins = /* @__PURE__ */ new Map();
|
|
1333
|
+
this.bytes = 0;
|
|
1334
|
+
return points.length ? { url: this.url, points } : null;
|
|
1063
1335
|
}
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1336
|
+
};
|
|
1337
|
+
|
|
1338
|
+
// src/errors.ts
|
|
1339
|
+
var import_runtime11 = require("@guidelytech/shared/runtime");
|
|
1340
|
+
var MAX_FRAMES_BYTES = 6e3;
|
|
1341
|
+
function fitFrames(frames) {
|
|
1342
|
+
const out = frames.slice(0, 30);
|
|
1343
|
+
while (out.length > 1 && JSON.stringify(out).length > MAX_FRAMES_BYTES) out.pop();
|
|
1344
|
+
return out;
|
|
1345
|
+
}
|
|
1346
|
+
function describe(value) {
|
|
1347
|
+
if (typeof value === "string") return value;
|
|
1348
|
+
try {
|
|
1349
|
+
return JSON.stringify(value) ?? String(value);
|
|
1350
|
+
} catch {
|
|
1351
|
+
return String(value);
|
|
1067
1352
|
}
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1353
|
+
}
|
|
1354
|
+
function exceptionFromError(error, source, handled, fallbackType = "Error") {
|
|
1355
|
+
const isError = error instanceof Error || !!error && typeof error === "object" && typeof error.message === "string" && typeof error.stack === "string";
|
|
1356
|
+
const e = error;
|
|
1357
|
+
return {
|
|
1358
|
+
type: isError && e.name || fallbackType,
|
|
1359
|
+
message: (isError ? e.message : describe(error)).slice(0, 1e3),
|
|
1360
|
+
frames: isError ? fitFrames((0, import_runtime11.parseStack)(e.stack)) : [],
|
|
1361
|
+
source,
|
|
1362
|
+
handled
|
|
1363
|
+
};
|
|
1364
|
+
}
|
|
1365
|
+
function errorEventToException(e) {
|
|
1366
|
+
const target = e.target;
|
|
1367
|
+
if (target && typeof Element !== "undefined" && target instanceof Element) return null;
|
|
1368
|
+
if (e.error != null) return exceptionFromError(e.error, "onerror", false);
|
|
1369
|
+
const message = e.message || "";
|
|
1370
|
+
if (!message || /^script error\.?$/i.test(message.trim())) return null;
|
|
1371
|
+
const m = /^(?:Uncaught )?([A-Z]\w*(?:Error|Exception)):\s*([\s\S]*)$/.exec(message);
|
|
1372
|
+
return {
|
|
1373
|
+
type: m?.[1] ?? "Error",
|
|
1374
|
+
message: (m?.[2] ?? message).slice(0, 1e3),
|
|
1375
|
+
frames: e.filename ? [{ file: e.filename.slice(0, 500), line: e.lineno || void 0, col: e.colno || void 0 }] : [],
|
|
1376
|
+
source: "onerror",
|
|
1377
|
+
handled: false
|
|
1378
|
+
};
|
|
1379
|
+
}
|
|
1380
|
+
function rejectionToException(e) {
|
|
1381
|
+
return exceptionFromError(e.reason, "unhandledrejection", false, "UnhandledRejection");
|
|
1382
|
+
}
|
|
1383
|
+
var GUIDELY_SCRIPT = /\/guidely(-recorder)?(\.min)?\.js$/i;
|
|
1384
|
+
function isGuidelyException(props) {
|
|
1385
|
+
if (props.message.startsWith("[Guidely]")) return true;
|
|
1386
|
+
const file = props.frames[0]?.file;
|
|
1387
|
+
if (!file) return false;
|
|
1388
|
+
try {
|
|
1389
|
+
return GUIDELY_SCRIPT.test(new URL(file).pathname);
|
|
1390
|
+
} catch {
|
|
1391
|
+
return GUIDELY_SCRIPT.test(file.replace(/[?#].*$/, ""));
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
var ExceptionThrottle = class {
|
|
1395
|
+
constructor(max = 5) {
|
|
1396
|
+
__publicField(this, "max", max);
|
|
1397
|
+
__publicField(this, "seen", /* @__PURE__ */ new Map());
|
|
1398
|
+
}
|
|
1399
|
+
allow(props) {
|
|
1400
|
+
const top = props.frames[0];
|
|
1401
|
+
const key = `${props.type}|${props.message}|${top ? `${top.file}:${top.line}:${top.col}` : ""}`;
|
|
1402
|
+
const n = this.seen.get(key) ?? 0;
|
|
1403
|
+
if (n >= this.max) return false;
|
|
1404
|
+
if (this.seen.size > 500) this.seen.clear();
|
|
1405
|
+
this.seen.set(key, n + 1);
|
|
1406
|
+
return true;
|
|
1407
|
+
}
|
|
1408
|
+
};
|
|
1409
|
+
|
|
1410
|
+
// src/emitter.ts
|
|
1411
|
+
var Emitter = class {
|
|
1412
|
+
constructor(onError) {
|
|
1413
|
+
__publicField(this, "onError", onError);
|
|
1414
|
+
__publicField(this, "listeners", /* @__PURE__ */ new Map());
|
|
1415
|
+
}
|
|
1416
|
+
on(event, cb) {
|
|
1417
|
+
let set = this.listeners.get(event);
|
|
1418
|
+
if (!set) {
|
|
1419
|
+
set = /* @__PURE__ */ new Set();
|
|
1420
|
+
this.listeners.set(event, set);
|
|
1116
1421
|
}
|
|
1422
|
+
set.add(cb);
|
|
1423
|
+
return () => this.off(event, cb);
|
|
1117
1424
|
}
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1425
|
+
off(event, cb) {
|
|
1426
|
+
this.listeners.get(event)?.delete(cb);
|
|
1427
|
+
}
|
|
1428
|
+
emit(event, ...args) {
|
|
1429
|
+
const set = this.listeners.get(event);
|
|
1430
|
+
if (!set) return;
|
|
1431
|
+
for (const cb of Array.from(set)) {
|
|
1432
|
+
try {
|
|
1433
|
+
cb(...args);
|
|
1434
|
+
} catch (err) {
|
|
1435
|
+
this.onError?.(err);
|
|
1436
|
+
}
|
|
1126
1437
|
}
|
|
1127
1438
|
}
|
|
1439
|
+
clear() {
|
|
1440
|
+
this.listeners.clear();
|
|
1441
|
+
}
|
|
1128
1442
|
};
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1443
|
+
|
|
1444
|
+
// src/frequency.ts
|
|
1445
|
+
var HOUR = 3600 * 1e3;
|
|
1446
|
+
function isFrequencyEligible(freq, state, shownThisSession, now = Date.now()) {
|
|
1447
|
+
const s = state ?? { impressions: 0 };
|
|
1448
|
+
if (freq.maxImpressions != null && s.impressions >= freq.maxImpressions) return false;
|
|
1449
|
+
if (s.completedAt && freq.mode !== "every_time") return false;
|
|
1450
|
+
if (s.dismissedAt) {
|
|
1451
|
+
if (!freq.reshowAfterDismiss) return false;
|
|
1452
|
+
if (freq.cooldownHours > 0 && now - Date.parse(s.dismissedAt) < freq.cooldownHours * HOUR) return false;
|
|
1453
|
+
}
|
|
1454
|
+
switch (freq.mode) {
|
|
1455
|
+
case "once":
|
|
1456
|
+
return s.impressions === 0 || !!s.dismissedAt && freq.reshowAfterDismiss;
|
|
1457
|
+
case "once_per_session":
|
|
1458
|
+
return !shownThisSession;
|
|
1459
|
+
case "interval": {
|
|
1460
|
+
if (!s.lastShownAt) return true;
|
|
1461
|
+
const hours = freq.intervalHours ?? 24;
|
|
1462
|
+
return now - Date.parse(s.lastShownAt) >= hours * HOUR;
|
|
1139
1463
|
}
|
|
1140
|
-
|
|
1464
|
+
case "every_time":
|
|
1465
|
+
default:
|
|
1466
|
+
return true;
|
|
1141
1467
|
}
|
|
1142
|
-
return null;
|
|
1143
1468
|
}
|
|
1144
|
-
function
|
|
1145
|
-
|
|
1469
|
+
function isExclusiveGuide(guide) {
|
|
1470
|
+
if (guide.kind === "banner" || guide.kind === "checklist") return false;
|
|
1471
|
+
const steps = guide.config.steps;
|
|
1472
|
+
if (steps.length > 0 && steps.every((s) => s.type === "hotspot" || s.type === "banner")) return false;
|
|
1473
|
+
return true;
|
|
1146
1474
|
}
|
|
1147
|
-
|
|
1148
|
-
const
|
|
1149
|
-
const
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
const field = i < 0 ? line : line.slice(0, i);
|
|
1158
|
-
const value = i < 0 ? "" : line.slice(i + 1).replace(/^ /, "");
|
|
1159
|
-
if (field === "event") event = value;
|
|
1160
|
-
else if (field === "data") data.push(value);
|
|
1161
|
-
}
|
|
1162
|
-
if (!data.length) return;
|
|
1163
|
-
try {
|
|
1164
|
-
onEvent(event, JSON.parse(data.join("\n")));
|
|
1165
|
-
} catch {
|
|
1166
|
-
}
|
|
1167
|
-
};
|
|
1168
|
-
for (; ; ) {
|
|
1169
|
-
const { done, value } = await reader.read();
|
|
1170
|
-
if (value) onChunk?.();
|
|
1171
|
-
if (value) buffer += decoder.decode(value, { stream: true }).replace(/\r\n?/g, "\n");
|
|
1172
|
-
let sep;
|
|
1173
|
-
while ((sep = buffer.indexOf("\n\n")) >= 0) {
|
|
1174
|
-
dispatch(buffer.slice(0, sep));
|
|
1175
|
-
buffer = buffer.slice(sep + 2);
|
|
1176
|
-
}
|
|
1177
|
-
if (done) break;
|
|
1475
|
+
function areMutuallyExclusive(a, b) {
|
|
1476
|
+
const la = a.config.frequency.mutuallyExclusiveWith;
|
|
1477
|
+
const lb = b.config.frequency.mutuallyExclusiveWith;
|
|
1478
|
+
return la.includes(b.id) || la.includes(b.key) || lb.includes(a.id) || lb.includes(a.key);
|
|
1479
|
+
}
|
|
1480
|
+
function isWithinSchedule(guide, now = Date.now()) {
|
|
1481
|
+
for (const w of [guide.config.schedule, guide.config.survey]) {
|
|
1482
|
+
if (!w) continue;
|
|
1483
|
+
if (w.startAt && now < Date.parse(w.startAt)) return false;
|
|
1484
|
+
if (w.endAt && now > Date.parse(w.endAt)) return false;
|
|
1178
1485
|
}
|
|
1179
|
-
|
|
1486
|
+
return true;
|
|
1487
|
+
}
|
|
1488
|
+
var MINUTE = 60 * 1e3;
|
|
1489
|
+
function countsTowardThrottle(guide) {
|
|
1490
|
+
return isExclusiveGuide(guide) && !guide.config.frequency.ignoreThrottling;
|
|
1491
|
+
}
|
|
1492
|
+
function isThrottleEligible(settings, launches, now = Date.now()) {
|
|
1493
|
+
if (!settings || !settings.enabled) return true;
|
|
1494
|
+
const times = launches.map((l) => Date.parse(l)).filter((t) => !Number.isNaN(t));
|
|
1495
|
+
const windowMs = Math.max(1, settings.windowHours) * HOUR;
|
|
1496
|
+
if (times.filter((t) => now - t < windowMs).length >= Math.max(1, settings.maxGuides)) return false;
|
|
1497
|
+
if (settings.minGapMinutes > 0 && times.some((t) => now - t < settings.minGapMinutes * MINUTE)) return false;
|
|
1498
|
+
return true;
|
|
1499
|
+
}
|
|
1500
|
+
function pruneLaunches(launches, settings, now = Date.now()) {
|
|
1501
|
+
const keepMs = Math.max(settings?.windowHours ?? 24, 1) * HOUR + (settings?.minGapMinutes ?? 0) * MINUTE;
|
|
1502
|
+
return launches.filter((l) => {
|
|
1503
|
+
const t = Date.parse(l);
|
|
1504
|
+
return !Number.isNaN(t) && now - t < keepMs;
|
|
1505
|
+
}).slice(-100);
|
|
1506
|
+
}
|
|
1507
|
+
function dismissedAfterMerge(local, server) {
|
|
1508
|
+
if (server.dismissResetAt && server.dismissResetAt !== local?.dismissResetAt) return server.dismissedAt ?? null;
|
|
1509
|
+
return local?.dismissedAt ?? server.dismissedAt ?? null;
|
|
1180
1510
|
}
|
|
1181
1511
|
|
|
1182
1512
|
// src/messenger-api.ts
|
|
@@ -1250,6 +1580,14 @@ function createMessengerTransport(opts) {
|
|
|
1250
1580
|
h.onDone(d.messages.at(-1) ?? null, d.conversation);
|
|
1251
1581
|
} else if (!finished) h.onError("The reply was interrupted");
|
|
1252
1582
|
}
|
|
1583
|
+
async function feedback(path, body, fallback) {
|
|
1584
|
+
const res = await api.post(`/feedback${path}`, { ...body, anonymousId: opts.anonymousId(), userId: opts.userId() });
|
|
1585
|
+
if (!res.ok || !res.data) {
|
|
1586
|
+
if (res.status === 429) throw new HttpError(429, "You're doing that a lot. Please try again in a minute.");
|
|
1587
|
+
throw new HttpError(res.status, serverMessage(res.data) ?? fallback);
|
|
1588
|
+
}
|
|
1589
|
+
return res.data;
|
|
1590
|
+
}
|
|
1253
1591
|
return {
|
|
1254
1592
|
async listConversations() {
|
|
1255
1593
|
const tokens = storedConversations(store, projectId).map((c) => c.token);
|
|
@@ -1286,7 +1624,8 @@ function createMessengerTransport(opts) {
|
|
|
1286
1624
|
return res.data;
|
|
1287
1625
|
},
|
|
1288
1626
|
async getArticle(id) {
|
|
1289
|
-
const
|
|
1627
|
+
const locale = opts.locale?.();
|
|
1628
|
+
const res = await api.get(`/articles/${encodeURIComponent(id)}`, locale ? { params: { locale } } : void 0);
|
|
1290
1629
|
return res.ok && res.data ? res.data.article : null;
|
|
1291
1630
|
},
|
|
1292
1631
|
async fileComplaint(input) {
|
|
@@ -1316,13 +1655,25 @@ function createMessengerTransport(opts) {
|
|
|
1316
1655
|
async getHowTo(id) {
|
|
1317
1656
|
const res = await api.get(`/how-tos/${encodeURIComponent(id)}`);
|
|
1318
1657
|
return res.ok && res.data ? res.data.howTo : null;
|
|
1319
|
-
}
|
|
1658
|
+
},
|
|
1659
|
+
// Ideas (feature requests). The visitor's ids go in every body; the server decides what they may do.
|
|
1660
|
+
listIdeas: (query) => feedback("/requests/query", query, "Ideas couldn't be loaded."),
|
|
1661
|
+
getIdea: (id) => feedback(`/requests/${encodeURIComponent(id)}/view`, {}, "This idea couldn't be loaded."),
|
|
1662
|
+
submitIdea: (input) => feedback("/requests", { ...input, pageUrl: opts.pageUrl() }, "Your idea couldn't be sent. Please try again."),
|
|
1663
|
+
voteIdea: (id, vote) => feedback(`/requests/${encodeURIComponent(id)}/vote`, vote, "Your vote couldn't be saved."),
|
|
1664
|
+
unvoteIdea: (id) => feedback(`/requests/${encodeURIComponent(id)}/unvote`, {}, "Your vote couldn't be removed."),
|
|
1665
|
+
followIdea: (id, follow) => feedback(`/requests/${encodeURIComponent(id)}/follow`, { follow }, "That didn't work. Please try again."),
|
|
1666
|
+
commentIdea: (id, body) => feedback(`/requests/${encodeURIComponent(id)}/comments`, { body }, "Your comment couldn't be posted."),
|
|
1667
|
+
similarIdeas: async (title) => (await feedback("/similar", { title }, "Similar ideas couldn't be loaded.")).requests ?? [],
|
|
1668
|
+
ideaUpdates: () => feedback("/updates", {}, "Updates couldn't be loaded."),
|
|
1669
|
+
markIdeaUpdatesRead: (ids) => feedback("/updates/read", ids ? { ids } : {}, "Updates couldn't be marked read."),
|
|
1670
|
+
ideaPortalLink: async () => (await feedback("/portal-link", {}, "The feedback portal couldn't be opened.")).url
|
|
1320
1671
|
};
|
|
1321
1672
|
}
|
|
1322
1673
|
|
|
1323
1674
|
// src/messenger-visitor.ts
|
|
1324
|
-
var
|
|
1325
|
-
var
|
|
1675
|
+
var import_runtime15 = require("@guidelytech/shared/runtime");
|
|
1676
|
+
var import_guide_runtime4 = require("@guidelytech/guide-runtime");
|
|
1326
1677
|
var PAGE_GUIDE_KINDS = /* @__PURE__ */ new Set(["flow", "tooltip", "modal"]);
|
|
1327
1678
|
var CATCH_ALL = /* @__PURE__ */ new Set(["", "*", "**", "/*", "/**", "/"]);
|
|
1328
1679
|
var RESUME_WITHIN_MS = 14 * 864e5;
|
|
@@ -1339,7 +1690,7 @@ function targetsPage(guide, url) {
|
|
|
1339
1690
|
const c = guide.config;
|
|
1340
1691
|
const checks = [...c.urlPatterns, ...c.trigger.urlPattern ? [c.trigger.urlPattern] : [], ...c.trigger.type === "url_match" && c.trigger.value ? [c.trigger.value] : [], ...c.steps[0]?.url ? [c.steps[0].url] : []];
|
|
1341
1692
|
const specific = checks.filter((p) => !CATCH_ALL.has(p.trim()));
|
|
1342
|
-
return specific.length > 0 && checks.every((p) => (0,
|
|
1693
|
+
return specific.length > 0 && checks.every((p) => (0, import_guide_runtime4.urlMatches)(p, url));
|
|
1343
1694
|
}
|
|
1344
1695
|
function plainText(html, max = 280) {
|
|
1345
1696
|
const text = html.replace(/<(br|\/p|\/div|\/li|\/h[1-6])\s*\/?>/gi, " ").replace(/<[^>]*>/g, "").replace(/ /g, " ").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/&/g, "&").replace(/\s+/g, " ").trim();
|
|
@@ -1388,7 +1739,7 @@ function buildMessengerVisitor(input) {
|
|
|
1388
1739
|
unread: !seen.has(g.id) && !(state.guides[g.id]?.impressions ?? 0)
|
|
1389
1740
|
};
|
|
1390
1741
|
});
|
|
1391
|
-
return { firstName: (0,
|
|
1742
|
+
return { firstName: (0, import_runtime15.firstNameFromTraits)(input.traits), path: pathOf(input.url), pageGuides, resumeGuide, checklist, news };
|
|
1392
1743
|
}
|
|
1393
1744
|
|
|
1394
1745
|
// src/navigation.ts
|
|
@@ -1446,6 +1797,48 @@ function watchNavigation(listener, debounceMs = 50) {
|
|
|
1446
1797
|
};
|
|
1447
1798
|
}
|
|
1448
1799
|
|
|
1800
|
+
// src/page-url.ts
|
|
1801
|
+
var WEB_SCHEMES = /* @__PURE__ */ new Set(["http", "https", "about", "data", "blob", "javascript"]);
|
|
1802
|
+
function appScheme(href) {
|
|
1803
|
+
let u;
|
|
1804
|
+
try {
|
|
1805
|
+
u = new URL(href);
|
|
1806
|
+
} catch {
|
|
1807
|
+
return null;
|
|
1808
|
+
}
|
|
1809
|
+
const scheme = u.protocol.replace(/:$/, "").toLowerCase();
|
|
1810
|
+
return WEB_SCHEMES.has(scheme) ? null : { u, scheme };
|
|
1811
|
+
}
|
|
1812
|
+
function normalizeAppUrl(href) {
|
|
1813
|
+
const app = appScheme(href);
|
|
1814
|
+
if (!app) return href;
|
|
1815
|
+
const { u, scheme } = app;
|
|
1816
|
+
const host = scheme === "file" ? "app" : u.host || "app";
|
|
1817
|
+
const hash = /^#!?(\/.*)$/.exec(u.hash);
|
|
1818
|
+
if (hash) {
|
|
1819
|
+
const route = hash[1];
|
|
1820
|
+
return `${scheme}://${host}${route.startsWith("/") ? route : `/${route}`}`;
|
|
1821
|
+
}
|
|
1822
|
+
let path = safeDecode(u.pathname);
|
|
1823
|
+
if (scheme === "file" || /\.html?$/i.test(path)) {
|
|
1824
|
+
const file = path.split("/").filter(Boolean).pop() ?? "";
|
|
1825
|
+
const name = file.replace(/\.html?$/i, "");
|
|
1826
|
+
path = !name || name.toLowerCase() === "index" ? "/" : `/${name}`;
|
|
1827
|
+
}
|
|
1828
|
+
if (!path.startsWith("/")) path = `/${path}`;
|
|
1829
|
+
return `${scheme}://${host}${path}${u.search}`;
|
|
1830
|
+
}
|
|
1831
|
+
function safeDecode(s) {
|
|
1832
|
+
try {
|
|
1833
|
+
return decodeURI(s);
|
|
1834
|
+
} catch {
|
|
1835
|
+
return s;
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
function isAppUrl(href) {
|
|
1839
|
+
return appScheme(href) !== null;
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1449
1842
|
// src/page-tracking.ts
|
|
1450
1843
|
function scrollDepth() {
|
|
1451
1844
|
const de = document.documentElement;
|
|
@@ -1491,89 +1884,105 @@ var PageTracker = class {
|
|
|
1491
1884
|
}
|
|
1492
1885
|
};
|
|
1493
1886
|
|
|
1494
|
-
// src/
|
|
1495
|
-
var
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
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);
|
|
1887
|
+
// src/prototype-overlay.ts
|
|
1888
|
+
var PROTOTYPE_CLOSE_MESSAGE = "guidely:prototype:close";
|
|
1889
|
+
var TOKEN = /^[A-Za-z0-9_-]{16,64}$/;
|
|
1890
|
+
var OVERLAY_ATTR = "data-guidely-prototype-overlay";
|
|
1891
|
+
function matchPrototypeTestUrl(urlOrToken, apiUrl) {
|
|
1892
|
+
const value = (urlOrToken || "").trim();
|
|
1893
|
+
let api;
|
|
1894
|
+
try {
|
|
1895
|
+
api = new URL(apiUrl);
|
|
1896
|
+
} catch {
|
|
1897
|
+
return null;
|
|
1531
1898
|
}
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
}, ms);
|
|
1899
|
+
if (TOKEN.test(value)) return new URL(`/validate/${value}`, api.origin);
|
|
1900
|
+
let url;
|
|
1901
|
+
try {
|
|
1902
|
+
url = new URL(value, typeof location !== "undefined" ? location.href : api.origin);
|
|
1903
|
+
} catch {
|
|
1904
|
+
return null;
|
|
1539
1905
|
}
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1906
|
+
if (url.origin !== api.origin) return null;
|
|
1907
|
+
const m = /^\/validate\/([^/]+)\/?$/.exec(url.pathname);
|
|
1908
|
+
if (!m || !TOKEN.test(m[1])) return null;
|
|
1909
|
+
return new URL(`/validate/${m[1]}`, api.origin);
|
|
1910
|
+
}
|
|
1911
|
+
var current = null;
|
|
1912
|
+
function openPrototypeOverlay(testUrl, opts) {
|
|
1913
|
+
current?.close();
|
|
1914
|
+
const doc = document;
|
|
1915
|
+
const previous = doc.activeElement;
|
|
1916
|
+
const src = new URL(testUrl.href);
|
|
1917
|
+
const hash = new URLSearchParams();
|
|
1918
|
+
hash.set("key", opts.key);
|
|
1919
|
+
if (opts.anonymousId) hash.set("anon", opts.anonymousId);
|
|
1920
|
+
if (opts.userId) hash.set("uid", opts.userId);
|
|
1921
|
+
src.hash = hash.toString();
|
|
1922
|
+
const root = doc.createElement("div");
|
|
1923
|
+
root.setAttribute(OVERLAY_ATTR, "");
|
|
1924
|
+
root.setAttribute("role", "dialog");
|
|
1925
|
+
root.setAttribute("aria-modal", "true");
|
|
1926
|
+
root.setAttribute("aria-label", "Prototype test");
|
|
1927
|
+
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;`;
|
|
1928
|
+
const frame = doc.createElement("iframe");
|
|
1929
|
+
frame.src = src.toString();
|
|
1930
|
+
frame.title = "Prototype test";
|
|
1931
|
+
frame.setAttribute("allow", "fullscreen");
|
|
1932
|
+
frame.setAttribute("referrerpolicy", "no-referrer");
|
|
1933
|
+
frame.style.cssText = "border:0;width:100%;height:100%;background:#fff;display:block;";
|
|
1934
|
+
const close = doc.createElement("button");
|
|
1935
|
+
close.type = "button";
|
|
1936
|
+
close.setAttribute("aria-label", "Close prototype test");
|
|
1937
|
+
close.textContent = "\xD7";
|
|
1938
|
+
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;";
|
|
1939
|
+
root.append(frame, close);
|
|
1940
|
+
const overflow = doc.documentElement.style.overflow;
|
|
1941
|
+
doc.documentElement.style.overflow = "hidden";
|
|
1942
|
+
let closed = false;
|
|
1943
|
+
const shut = () => {
|
|
1944
|
+
if (closed) return;
|
|
1945
|
+
closed = true;
|
|
1946
|
+
window.removeEventListener("message", onMessage);
|
|
1947
|
+
doc.removeEventListener("keydown", onKey, true);
|
|
1948
|
+
doc.removeEventListener("focusin", onFocus, true);
|
|
1949
|
+
root.remove();
|
|
1950
|
+
doc.documentElement.style.overflow = overflow;
|
|
1951
|
+
if (current === overlay) current = null;
|
|
1546
1952
|
try {
|
|
1547
|
-
|
|
1548
|
-
|
|
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;
|
|
1953
|
+
previous?.focus?.();
|
|
1954
|
+
} catch {
|
|
1568
1955
|
}
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
}
|
|
1576
|
-
|
|
1956
|
+
opts.onClose?.();
|
|
1957
|
+
};
|
|
1958
|
+
const onMessage = (e) => {
|
|
1959
|
+
if (e.origin !== testUrl.origin || e.source !== frame.contentWindow) return;
|
|
1960
|
+
const data = e.data;
|
|
1961
|
+
if (data && typeof data === "object" && data.type === PROTOTYPE_CLOSE_MESSAGE) shut();
|
|
1962
|
+
};
|
|
1963
|
+
const onKey = (e) => {
|
|
1964
|
+
if (e.key === "Escape") {
|
|
1965
|
+
e.preventDefault();
|
|
1966
|
+
e.stopPropagation();
|
|
1967
|
+
shut();
|
|
1968
|
+
} else if (e.key === "Tab") {
|
|
1969
|
+
e.preventDefault();
|
|
1970
|
+
(doc.activeElement === close ? frame : close).focus();
|
|
1971
|
+
}
|
|
1972
|
+
};
|
|
1973
|
+
const onFocus = (e) => {
|
|
1974
|
+
if (!root.contains(e.target)) close.focus();
|
|
1975
|
+
};
|
|
1976
|
+
close.addEventListener("click", shut);
|
|
1977
|
+
window.addEventListener("message", onMessage);
|
|
1978
|
+
doc.addEventListener("keydown", onKey, true);
|
|
1979
|
+
doc.addEventListener("focusin", onFocus, true);
|
|
1980
|
+
doc.body.appendChild(root);
|
|
1981
|
+
frame.focus();
|
|
1982
|
+
const overlay = { element: root, close: shut };
|
|
1983
|
+
current = overlay;
|
|
1984
|
+
return overlay;
|
|
1985
|
+
}
|
|
1577
1986
|
|
|
1578
1987
|
// src/queue.ts
|
|
1579
1988
|
var EventQueue = class {
|
|
@@ -1707,7 +2116,7 @@ var loadRecorder = async () => (await import("./recorder.cjs")).recorder;
|
|
|
1707
2116
|
|
|
1708
2117
|
// src/client.ts
|
|
1709
2118
|
var SDK_NAME = "@guidelytech/sdk-core";
|
|
1710
|
-
var SDK_VERSION = "0.
|
|
2119
|
+
var SDK_VERSION = "0.11.0";
|
|
1711
2120
|
var DEFAULT_API_URL = "https://api.guidely.app";
|
|
1712
2121
|
var PAGE_SCOPED = /* @__PURE__ */ new Set(["page_load", "url_match", "element_click", "element_visible", "delay"]);
|
|
1713
2122
|
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
@@ -1733,7 +2142,7 @@ function emptyState() {
|
|
|
1733
2142
|
return { guides: {}, checklists: {}, surveysSubmitted: [], audienceIds: [] };
|
|
1734
2143
|
}
|
|
1735
2144
|
function newSession(now = Date.now()) {
|
|
1736
|
-
return { id: `s_${(0,
|
|
2145
|
+
return { id: `s_${(0, import_runtime18.randomId)(16)}`, startedAt: new Date(now).toISOString(), shownGuides: [], events: [], lastActivityAt: now };
|
|
1737
2146
|
}
|
|
1738
2147
|
var GuidelyClient = class _GuidelyClient {
|
|
1739
2148
|
constructor(options) {
|
|
@@ -1745,6 +2154,8 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
1745
2154
|
__publicField(this, "queue");
|
|
1746
2155
|
/** Survey answers and guide/checklist progress, kept until the server accepts them. */
|
|
1747
2156
|
__publicField(this, "outbox");
|
|
2157
|
+
/** Agent Analytics turns (agentConversation), in their own outbox. */
|
|
2158
|
+
__publicField(this, "agentTurns");
|
|
1748
2159
|
__publicField(this, "config", null);
|
|
1749
2160
|
__publicField(this, "etag", null);
|
|
1750
2161
|
__publicField(this, "anonymousId");
|
|
@@ -1794,6 +2205,10 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
1794
2205
|
__publicField(this, "flags");
|
|
1795
2206
|
/** Experiments already reported this session (`experiment_viewed` is sent once per session). */
|
|
1796
2207
|
__publicField(this, "exposures", /* @__PURE__ */ new Set());
|
|
2208
|
+
/** Language set with `setLocale()`. */
|
|
2209
|
+
__publicField(this, "locale", null);
|
|
2210
|
+
/** Language each shown guide was rendered in; added to its events as `locale`. */
|
|
2211
|
+
__publicField(this, "guideLocales", /* @__PURE__ */ new Map());
|
|
1797
2212
|
__publicField(this, "customListeners", /* @__PURE__ */ new Map());
|
|
1798
2213
|
__publicField(this, "eventBus", {
|
|
1799
2214
|
on: (name, cb) => {
|
|
@@ -1828,7 +2243,7 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
1828
2243
|
this.sessionStore = this.options.storage === "memory" ? this.store : createStorage("session");
|
|
1829
2244
|
this.anonymousId = this.store.get(KEYS.anon) || "";
|
|
1830
2245
|
if (!this.anonymousId) {
|
|
1831
|
-
this.anonymousId = `anon_${(0,
|
|
2246
|
+
this.anonymousId = `anon_${(0, import_runtime18.randomId)(20)}`;
|
|
1832
2247
|
this.store.set(KEYS.anon, this.anonymousId);
|
|
1833
2248
|
}
|
|
1834
2249
|
this.firstVisit = !this.store.get(KEYS.seen);
|
|
@@ -1838,7 +2253,7 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
1838
2253
|
const session = readJson(this.sessionStore, KEYS.session, null);
|
|
1839
2254
|
this.session = session && session.id ? session : newSession();
|
|
1840
2255
|
this.persistSession();
|
|
1841
|
-
this.lastUrl =
|
|
2256
|
+
this.lastUrl = this.pageHref();
|
|
1842
2257
|
this.lastAudiences = new Set(this.state.audienceIds);
|
|
1843
2258
|
this.queue = new EventQueue({
|
|
1844
2259
|
store: this.store,
|
|
@@ -1853,6 +2268,7 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
1853
2268
|
onError: (err) => this.reportError(err)
|
|
1854
2269
|
});
|
|
1855
2270
|
this.outbox = new Outbox({ api: this.api, store: this.store, storageKey: KEYS.outbox(this.options.projectId), health: this.api.health });
|
|
2271
|
+
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 } });
|
|
1856
2272
|
if (options.user) this.user = { ...options.user };
|
|
1857
2273
|
this.flags = this.createFlags(options.flags);
|
|
1858
2274
|
}
|
|
@@ -1929,7 +2345,7 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
1929
2345
|
this.resumeActiveGuide();
|
|
1930
2346
|
this.evaluate("ready");
|
|
1931
2347
|
if (!this.options.disableAuthoring && !capturing) this.maybeStartAuthoring();
|
|
1932
|
-
this.pageTracker.start(
|
|
2348
|
+
this.pageTracker.start(this.pageHref());
|
|
1933
2349
|
this.touchSession();
|
|
1934
2350
|
this.syncCapture();
|
|
1935
2351
|
} catch (err) {
|
|
@@ -1962,7 +2378,7 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
1962
2378
|
wireListeners() {
|
|
1963
2379
|
const doc = document;
|
|
1964
2380
|
const win = window;
|
|
1965
|
-
this.cleanups.push(watchNavigation((url) => this.handleNavigation(url)));
|
|
2381
|
+
this.cleanups.push(watchNavigation((url) => this.handleNavigation(this.pageHref(url))));
|
|
1966
2382
|
const onClick = (e) => this.handleDocumentClick(e);
|
|
1967
2383
|
doc.addEventListener("click", onClick, true);
|
|
1968
2384
|
this.cleanups.push(() => doc.removeEventListener("click", onClick, true));
|
|
@@ -1983,7 +2399,7 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
1983
2399
|
const onPageShow = (e) => {
|
|
1984
2400
|
if (!e.persisted || !this.pageLeft) return;
|
|
1985
2401
|
this.pageLeft = false;
|
|
1986
|
-
this.pageTracker.start(
|
|
2402
|
+
this.pageTracker.start(this.pageHref());
|
|
1987
2403
|
};
|
|
1988
2404
|
let lastScroll = 0;
|
|
1989
2405
|
const onScroll = () => {
|
|
@@ -2032,7 +2448,7 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2032
2448
|
this.cleanups.push(() => clearInterval(refresh));
|
|
2033
2449
|
const poll = setInterval(() => {
|
|
2034
2450
|
if (!this.isReady || !this.config || this.editing() || doc.visibilityState === "hidden") return;
|
|
2035
|
-
const url =
|
|
2451
|
+
const url = this.pageHref();
|
|
2036
2452
|
const ctx = this.getContext();
|
|
2037
2453
|
for (const guide of this.config.guides) {
|
|
2038
2454
|
if (guide.config.trigger.type === "element_visible" && guide.config.trigger.value) this.armVisibility(guide, ctx, url);
|
|
@@ -2101,10 +2517,11 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2101
2517
|
this.queue.flushSync();
|
|
2102
2518
|
this.queue.destroy();
|
|
2103
2519
|
this.outbox.destroy();
|
|
2520
|
+
this.agentTurns.destroy();
|
|
2104
2521
|
this.flags.destroy();
|
|
2105
2522
|
this.emitter.clear();
|
|
2106
|
-
(0,
|
|
2107
|
-
if (
|
|
2523
|
+
(0, import_guide_runtime5.destroyHost)();
|
|
2524
|
+
if (current2 === this) current2 = null;
|
|
2108
2525
|
}
|
|
2109
2526
|
// ---------------------------------------------------------------------------
|
|
2110
2527
|
// Identity
|
|
@@ -2116,13 +2533,13 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2116
2533
|
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) };
|
|
2117
2534
|
writeJson(this.store, KEYS.user(this.options.projectId), this.user);
|
|
2118
2535
|
this.syncFlagAttributes();
|
|
2119
|
-
this.enqueue("identify", { properties: { traits: (0,
|
|
2536
|
+
this.enqueue("identify", { properties: { traits: (0, import_runtime18.sanitizeProperties)(this.user.traits) } });
|
|
2120
2537
|
await this.syncIdentify();
|
|
2121
2538
|
this.emitter.emit("identify", { userId: this.user.id, anonymousId: this.anonymousId });
|
|
2122
2539
|
if (this.isReady) this.evaluate("identify");
|
|
2123
2540
|
}
|
|
2124
2541
|
async syncIdentify() {
|
|
2125
|
-
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() ?
|
|
2542
|
+
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 };
|
|
2126
2543
|
const res = await postWithRetry(this.api, "/identify", body, 2);
|
|
2127
2544
|
if (res?.ok && res.data) this.mergeServerState(res.data);
|
|
2128
2545
|
}
|
|
@@ -2153,7 +2570,10 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2153
2570
|
checklists,
|
|
2154
2571
|
surveysSubmitted: Array.from(/* @__PURE__ */ new Set([...this.state.surveysSubmitted, ...data.surveysSubmitted || []])),
|
|
2155
2572
|
audienceIds: data.audienceIds || [],
|
|
2156
|
-
firstSeenAt: this.state.firstSeenAt ?? data.firstSeenAt
|
|
2573
|
+
firstSeenAt: this.state.firstSeenAt ?? data.firstSeenAt,
|
|
2574
|
+
// Device-local only (the server doesn't hold them).
|
|
2575
|
+
...this.state.throttle ? { throttle: this.state.throttle } : {},
|
|
2576
|
+
...this.state.announcementsSeenAt ? { announcementsSeenAt: this.state.announcementsSeenAt } : {}
|
|
2157
2577
|
};
|
|
2158
2578
|
if (data.firstSeenAt && Date.now() - Date.parse(data.firstSeenAt) > 6e4) this.firstVisit = false;
|
|
2159
2579
|
this.persistState();
|
|
@@ -2189,7 +2609,7 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2189
2609
|
this.stopGuide();
|
|
2190
2610
|
this.user = null;
|
|
2191
2611
|
this.store.remove(KEYS.user(this.options.projectId));
|
|
2192
|
-
this.anonymousId = `anon_${(0,
|
|
2612
|
+
this.anonymousId = `anon_${(0, import_runtime18.randomId)(20)}`;
|
|
2193
2613
|
this.store.set(KEYS.anon, this.anonymousId);
|
|
2194
2614
|
this.state = emptyState();
|
|
2195
2615
|
this.persistState();
|
|
@@ -2221,6 +2641,7 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2221
2641
|
if (consent === "denied") this.queue.clear();
|
|
2222
2642
|
else if (consent === "granted") void this.queue.flush();
|
|
2223
2643
|
if (consent !== "granted") this.replay?.stop({ discard: true });
|
|
2644
|
+
if (consent !== "pending") this.agentTurns.consentChanged(consent === "granted");
|
|
2224
2645
|
this.syncCapture();
|
|
2225
2646
|
}
|
|
2226
2647
|
analyticsEnabled() {
|
|
@@ -2233,17 +2654,19 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2233
2654
|
if (this.consent === "denied" || !this.options.analytics) return null;
|
|
2234
2655
|
if (name !== "page_leave" && name !== "heatmap") this.touchSession();
|
|
2235
2656
|
const event = {
|
|
2236
|
-
eventId: (0,
|
|
2657
|
+
eventId: (0, import_runtime18.randomId)(24),
|
|
2237
2658
|
name,
|
|
2238
2659
|
occurredAt: nowIso(),
|
|
2239
2660
|
anonymousId: this.anonymousId,
|
|
2240
2661
|
userId: this.user?.id || null,
|
|
2241
2662
|
accountId: this.user?.account?.id || null,
|
|
2242
2663
|
sessionId: this.session.id,
|
|
2243
|
-
url: this.collectUrls() ?
|
|
2664
|
+
url: this.collectUrls() ? this.pageHref() : void 0,
|
|
2244
2665
|
...extra
|
|
2245
2666
|
};
|
|
2246
|
-
if (extra.properties) event.properties = (0,
|
|
2667
|
+
if (extra.properties) event.properties = (0, import_runtime18.sanitizeProperties)(extra.properties);
|
|
2668
|
+
const locale = extra.guideId ? this.guideLocales.get(extra.guideId) : void 0;
|
|
2669
|
+
if (locale) event.properties = { ...event.properties ?? {}, locale };
|
|
2247
2670
|
this.queue.enqueue(event);
|
|
2248
2671
|
this.replay?.addCustomEvent("guidely", { name, customName: event.customName, guideId: event.guideId });
|
|
2249
2672
|
this.emitter.emit("track", event);
|
|
@@ -2267,9 +2690,30 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2267
2690
|
this.completeChecklistTasksBy("event", name);
|
|
2268
2691
|
if (this.isReady) this.evaluate("event", { eventName: name });
|
|
2269
2692
|
}
|
|
2693
|
+
/**
|
|
2694
|
+
* Agent Analytics: records one or more turns of a conversation with your own AI agent or chatbot
|
|
2695
|
+
* (`{ conversationId, agentId, role, text, toolCalls?, latencyMs?, outcome?, … }`). The visitor's
|
|
2696
|
+
* anonymous id, user id, account id and session id are filled in, `agents.redact` from the init
|
|
2697
|
+
* options runs on each turn, and turns are retried until Guidely accepts them. Returns false when
|
|
2698
|
+
* nothing was queued (analytics off or consent denied, or every turn was invalid or dropped).
|
|
2699
|
+
*/
|
|
2700
|
+
agentConversation(turns) {
|
|
2701
|
+
if (this.destroyed || !this.options.analytics || this.consent === "denied" || this.config?.settings?.analyticsEnabled === false) return false;
|
|
2702
|
+
const identity = { anonymousId: this.anonymousId, userId: this.user?.id || null, accountId: this.user?.account?.id || null, sessionId: this.session.id };
|
|
2703
|
+
const prepared = (Array.isArray(turns) ? turns : [turns]).map((t) => prepareAgentTurn(t, identity, this.options.agents?.redact, (err) => this.reportError(err))).filter((t) => !!t);
|
|
2704
|
+
if (!prepared.length) return false;
|
|
2705
|
+
this.touchSession();
|
|
2706
|
+
this.agentTurns.add(prepared, this.consent === "pending");
|
|
2707
|
+
return true;
|
|
2708
|
+
}
|
|
2709
|
+
/** Alias of {@link agentConversation}. */
|
|
2710
|
+
trackAgentTurn(turns) {
|
|
2711
|
+
return this.agentConversation(turns);
|
|
2712
|
+
}
|
|
2270
2713
|
/** Records a page view and re-evaluates triggers. Called automatically on SPA navigation. */
|
|
2271
|
-
page(
|
|
2714
|
+
page(href) {
|
|
2272
2715
|
if (this.destroyed) return;
|
|
2716
|
+
const url = this.pageHref(href);
|
|
2273
2717
|
this.leavePage(url);
|
|
2274
2718
|
this.trackPageView(url);
|
|
2275
2719
|
this.handleNavigation(url, true);
|
|
@@ -2289,13 +2733,13 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2289
2733
|
const st = this.active.controller.getState();
|
|
2290
2734
|
if (st.status === "running" && !this.active.manual) {
|
|
2291
2735
|
const g = this.active.guide;
|
|
2292
|
-
if (g.config.urlPatterns.length && !g.config.urlPatterns.some((p) => (0,
|
|
2736
|
+
if (g.config.urlPatterns.length && !g.config.urlPatterns.some((p) => (0, import_guide_runtime5.urlMatches)(p, url))) this.active.controller.destroy();
|
|
2293
2737
|
}
|
|
2294
2738
|
}
|
|
2295
2739
|
this.widgets.forEach((w, id) => {
|
|
2296
2740
|
if (!w.controller || w.manual) return;
|
|
2297
2741
|
const g = this.findGuide(id);
|
|
2298
|
-
if (g?.config.urlPatterns.length && !g.config.urlPatterns.some((p) => (0,
|
|
2742
|
+
if (g?.config.urlPatterns.length && !g.config.urlPatterns.some((p) => (0, import_guide_runtime5.urlMatches)(p, url))) w.controller.destroy();
|
|
2299
2743
|
});
|
|
2300
2744
|
this.firedDelays.clear();
|
|
2301
2745
|
if (this.isReady) this.evaluate("navigation");
|
|
@@ -2303,7 +2747,7 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2303
2747
|
if (changed) void this.queue.flush();
|
|
2304
2748
|
}
|
|
2305
2749
|
handleDocumentClick(e) {
|
|
2306
|
-
const target = (0,
|
|
2750
|
+
const target = (0, import_guide_runtime5.composedTarget)(e);
|
|
2307
2751
|
if (!target || typeof target.closest !== "function") return;
|
|
2308
2752
|
this.autocaptureClick(e, target);
|
|
2309
2753
|
this.completeChecklistTasksBy("click", target);
|
|
@@ -2361,6 +2805,8 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2361
2805
|
const ref = !this.session.pageViews ? externalReferrer() : void 0;
|
|
2362
2806
|
if (ref) props.referrer = ref;
|
|
2363
2807
|
}
|
|
2808
|
+
const ep = this.captureAllowed() ? pagePropertyValues(this.config?.tagging, url, { mask: this.maskSelector(), maskAll: this.maskAllText() }) : void 0;
|
|
2809
|
+
if (ep) props.ep = ep;
|
|
2364
2810
|
this.session.pageViews = (this.session.pageViews ?? 0) + 1;
|
|
2365
2811
|
this.persistSession();
|
|
2366
2812
|
this.enqueue("page_view", { url: collect ? url : void 0, properties: props });
|
|
@@ -2386,21 +2832,28 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2386
2832
|
const heat = this.autocaptureOn("heatmaps");
|
|
2387
2833
|
if (!clicks && !rage && !heat || isIgnoredElement(target)) return;
|
|
2388
2834
|
this.touchSession();
|
|
2389
|
-
const
|
|
2835
|
+
const featureAttribute = this.config?.tagging?.featureAttribute ?? null;
|
|
2836
|
+
const feature = featureAttributeOf(target, featureAttribute);
|
|
2837
|
+
const el = interactiveElement(target) ?? feature?.el ?? null;
|
|
2390
2838
|
const opts = { mask: this.maskSelector(), maskAll: this.maskAllText(), collectUrls: this.collectUrls() };
|
|
2391
|
-
|
|
2839
|
+
const clickProps = (on) => {
|
|
2840
|
+
const p = describeClick(on, e, opts);
|
|
2841
|
+
const ep = clickPropertyValues(this.config?.tagging, on, p, opts);
|
|
2842
|
+
return { ...p, el: elementSnapshot(on, { ...opts, featureAttribute }), ...feature ? { fa: feature.value } : {}, ...ep ? { ep } : {} };
|
|
2843
|
+
};
|
|
2844
|
+
if (clicks && el) this.enqueue("click", { properties: clickProps(el) });
|
|
2392
2845
|
const count = this.rage.add(e.pageX ?? 0, e.pageY ?? 0, Date.now());
|
|
2393
|
-
if (rage && count) this.enqueue("rage_click", { properties: { ...
|
|
2846
|
+
if (rage && count) this.enqueue("rage_click", { properties: { ...clickProps(el ?? target), count } });
|
|
2394
2847
|
if (!heat) return;
|
|
2395
|
-
const url =
|
|
2848
|
+
const url = this.pageHref();
|
|
2396
2849
|
if (this.heatmap.url !== url) this.flushHeatmap();
|
|
2397
2850
|
if (this.heatmap.add(url, heatmapPoint(el ?? target, e, this.rage.burst > 0), this.rage.burst)) this.flushHeatmap();
|
|
2398
2851
|
}
|
|
2399
2852
|
/** Bins one pointer sample into the heatmap buffer. Not session activity. */
|
|
2400
2853
|
sampleMove(e) {
|
|
2401
|
-
const target = (0,
|
|
2854
|
+
const target = (0, import_guide_runtime5.composedTarget)(e);
|
|
2402
2855
|
if (document.visibilityState === "hidden" || !target?.closest || !this.autocaptureOn("mouseMovement") || isIgnoredElement(target)) return;
|
|
2403
|
-
const url =
|
|
2856
|
+
const url = this.pageHref();
|
|
2404
2857
|
if (this.heatmap.url !== url) this.flushHeatmap();
|
|
2405
2858
|
if (this.heatmap.move(url, movementBin(interactiveElement(target) ?? target, e))) this.flushHeatmap();
|
|
2406
2859
|
}
|
|
@@ -2476,7 +2929,7 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2476
2929
|
if (!this.isReady || this.destroyed) return;
|
|
2477
2930
|
if (this.config && !this.initialPageViewSent && this.config.settings?.autocapture) {
|
|
2478
2931
|
this.initialPageViewSent = true;
|
|
2479
|
-
this.trackPageView(
|
|
2932
|
+
this.trackPageView(this.pageHref());
|
|
2480
2933
|
}
|
|
2481
2934
|
if (this.config) {
|
|
2482
2935
|
for (const [props, extra] of this.earlyExceptions.splice(0)) {
|
|
@@ -2489,7 +2942,7 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2489
2942
|
syncReplay() {
|
|
2490
2943
|
const cfg = this.isReady && this.captureAllowed() ? this.replayConfig() : null;
|
|
2491
2944
|
if (cfg && this.session.replay === void 0) {
|
|
2492
|
-
this.session.replay = (0,
|
|
2945
|
+
this.session.replay = (0, import_runtime18.rolloutBucket)(this.session.id, "replay") < cfg.sampleRate;
|
|
2493
2946
|
this.persistSession();
|
|
2494
2947
|
}
|
|
2495
2948
|
if (!cfg || !this.session.replay) {
|
|
@@ -2511,7 +2964,7 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2511
2964
|
return {
|
|
2512
2965
|
anonymousId: this.anonymousId,
|
|
2513
2966
|
userId: this.user?.id || null,
|
|
2514
|
-
url: this.collectUrls() ?
|
|
2967
|
+
url: this.collectUrls() ? this.pageHref() : void 0,
|
|
2515
2968
|
device: { type: device.type, browser: device.browser, os: device.os },
|
|
2516
2969
|
viewport: { width: window.innerWidth, height: window.innerHeight },
|
|
2517
2970
|
sdk: { name: SDK_NAME, version: SDK_VERSION }
|
|
@@ -2562,6 +3015,14 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2562
3015
|
// ---------------------------------------------------------------------------
|
|
2563
3016
|
// Targeting + triggers
|
|
2564
3017
|
// ---------------------------------------------------------------------------
|
|
3018
|
+
/** The page URL Guidely works with: `options.pageUrl`, else app-relative for `file:` / custom protocols (`normalizeAppUrl`). */
|
|
3019
|
+
pageHref(href = location.href) {
|
|
3020
|
+
try {
|
|
3021
|
+
return this.options.pageUrl ? this.options.pageUrl(href) || href : normalizeAppUrl(href);
|
|
3022
|
+
} catch {
|
|
3023
|
+
return href;
|
|
3024
|
+
}
|
|
3025
|
+
}
|
|
2565
3026
|
/** Builds the targeting context for the current moment. */
|
|
2566
3027
|
getContext() {
|
|
2567
3028
|
const guides = this.state.guides;
|
|
@@ -2572,7 +3033,7 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2572
3033
|
userId: this.user?.id || null,
|
|
2573
3034
|
userAttributes: this.user?.traits ?? {},
|
|
2574
3035
|
accountAttributes: this.user?.account?.traits ?? {},
|
|
2575
|
-
url:
|
|
3036
|
+
url: this.pageHref(),
|
|
2576
3037
|
firstVisit: this.firstVisit,
|
|
2577
3038
|
eventsSeen: this.session.events,
|
|
2578
3039
|
completedGuides: keysOf(Object.keys(guides).filter((id) => guides[id]?.completedAt)),
|
|
@@ -2581,17 +3042,64 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2581
3042
|
completedChecklists: keysOf(completedChecklists),
|
|
2582
3043
|
audienceIds: this.state.audienceIds,
|
|
2583
3044
|
environment: this.options.environment,
|
|
2584
|
-
platform: "web"
|
|
3045
|
+
platform: "web",
|
|
3046
|
+
...this.options.appVersion ? { appVersion: this.options.appVersion } : {}
|
|
2585
3047
|
});
|
|
2586
3048
|
}
|
|
2587
3049
|
isTargeted(guide, ctx, url) {
|
|
2588
3050
|
const c = guide.config;
|
|
2589
|
-
if (c.urlPatterns.length && !c.urlPatterns.some((p) => (0,
|
|
2590
|
-
if (c.trigger.urlPattern && !(0,
|
|
3051
|
+
if (c.urlPatterns.length && !c.urlPatterns.some((p) => (0, import_guide_runtime5.urlMatches)(p, url))) return false;
|
|
3052
|
+
if (c.trigger.urlPattern && !(0, import_guide_runtime5.urlMatches)(c.trigger.urlPattern, url)) return false;
|
|
2591
3053
|
if (!isWithinSchedule(guide)) return false;
|
|
2592
3054
|
if (guide.kind === "survey" && (this.state.surveysSubmitted.includes(guide.id) || this.state.surveysSubmitted.includes(guide.key))) return false;
|
|
2593
3055
|
ctx.rolloutSalt = guide.id;
|
|
2594
|
-
return (0,
|
|
3056
|
+
return (0, import_runtime18.evaluateRules)(c.targeting, ctx);
|
|
3057
|
+
}
|
|
3058
|
+
/** Guide throttling: may this guide launch automatically now? */
|
|
3059
|
+
throttleAllows(guide) {
|
|
3060
|
+
if (!countsTowardThrottle(guide)) return true;
|
|
3061
|
+
return isThrottleEligible(this.config?.settings.throttling, this.state.throttle ?? []);
|
|
3062
|
+
}
|
|
3063
|
+
recordLaunch(guide, manual) {
|
|
3064
|
+
if (manual || !countsTowardThrottle(guide) || !this.config?.settings.throttling?.enabled) return;
|
|
3065
|
+
this.state.throttle = pruneLaunches([...this.state.throttle ?? [], nowIso()], this.config.settings.throttling);
|
|
3066
|
+
this.persistState();
|
|
3067
|
+
}
|
|
3068
|
+
/** Guides and A/B test control groups, which are evaluated the same way but never render. */
|
|
3069
|
+
candidates() {
|
|
3070
|
+
const guides = this.config?.guides ?? [];
|
|
3071
|
+
const holdouts = this.config?.guideHoldouts ?? [];
|
|
3072
|
+
return holdouts.length ? [...guides, ...holdouts.map((h) => h.guide)] : guides;
|
|
3073
|
+
}
|
|
3074
|
+
holdoutFor(guide) {
|
|
3075
|
+
return this.config?.guideHoldouts?.find((h) => h.guide.id === guide.id) ?? null;
|
|
3076
|
+
}
|
|
3077
|
+
/** The visitor's language candidates, most preferred first. */
|
|
3078
|
+
locales() {
|
|
3079
|
+
const nav = typeof navigator !== "undefined" ? [...navigator.languages ?? [], navigator.language] : [];
|
|
3080
|
+
return (0, import_runtime18.visitorLocales)(this.locale ?? this.options.locale, this.user?.traits, nav);
|
|
3081
|
+
}
|
|
3082
|
+
defaultLocale() {
|
|
3083
|
+
return this.config?.settings.localization?.defaultLocale || "en";
|
|
3084
|
+
}
|
|
3085
|
+
/** The guide in the visitor's language (remembered so its events carry `locale`). */
|
|
3086
|
+
localize(guide) {
|
|
3087
|
+
const localized = (0, import_runtime18.localizeGuide)(guide, this.locales(), this.defaultLocale());
|
|
3088
|
+
this.guideLocales.set(guide.id, localized.locale);
|
|
3089
|
+
return localized;
|
|
3090
|
+
}
|
|
3091
|
+
/**
|
|
3092
|
+
* Sets the language for guides and the resource center (`null` = back to the traits / browser).
|
|
3093
|
+
* Guides already on screen keep their language; the resource center re-renders.
|
|
3094
|
+
*/
|
|
3095
|
+
setLocale(locale) {
|
|
3096
|
+
this.locale = locale ? (0, import_runtime18.normalizeLocale)(locale) || null : null;
|
|
3097
|
+
if (this.resourceCenter) {
|
|
3098
|
+
this.resourceCenter.destroy();
|
|
3099
|
+
this.resourceCenterScope?.destroy();
|
|
3100
|
+
this.resourceCenter = null;
|
|
3101
|
+
this.renderPersistentWidgets();
|
|
3102
|
+
}
|
|
2595
3103
|
}
|
|
2596
3104
|
isEligible(guide, ctx, url) {
|
|
2597
3105
|
if (!this.isTargeted(guide, ctx, url)) return false;
|
|
@@ -2609,13 +3117,13 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2609
3117
|
if (t.delayMs > 0) return reason === "timer" && detail.guideId === guide.id;
|
|
2610
3118
|
return reason === "ready" || reason === "navigation" || reason === "identify";
|
|
2611
3119
|
case "url_match":
|
|
2612
|
-
return (reason === "ready" || reason === "navigation" || reason === "identify") && (0,
|
|
3120
|
+
return (reason === "ready" || reason === "navigation" || reason === "identify") && (0, import_guide_runtime5.urlMatches)(t.value, this.pageHref());
|
|
2613
3121
|
case "event":
|
|
2614
3122
|
return reason === "event" && !!t.value && detail.eventName === t.value;
|
|
2615
3123
|
case "element_click":
|
|
2616
3124
|
if (reason !== "click" || !t.value || !detail.target) return false;
|
|
2617
3125
|
try {
|
|
2618
|
-
return !!(0,
|
|
3126
|
+
return !!(0, import_guide_runtime5.closestComposed)(detail.target, t.value);
|
|
2619
3127
|
} catch {
|
|
2620
3128
|
return false;
|
|
2621
3129
|
}
|
|
@@ -2634,15 +3142,15 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2634
3142
|
}
|
|
2635
3143
|
evaluate(reason, detail = {}) {
|
|
2636
3144
|
if (!this.config || this.destroyed || this.editing()) return;
|
|
2637
|
-
const url =
|
|
3145
|
+
const url = this.pageHref();
|
|
2638
3146
|
const ctx = this.getContext();
|
|
2639
3147
|
if (reason === "identify") {
|
|
2640
|
-
const
|
|
2641
|
-
detail.newAudiences = new Set(Array.from(
|
|
2642
|
-
this.lastAudiences =
|
|
3148
|
+
const current3 = new Set(this.state.audienceIds);
|
|
3149
|
+
detail.newAudiences = new Set(Array.from(current3).filter((a) => !this.lastAudiences.has(a)));
|
|
3150
|
+
this.lastAudiences = current3;
|
|
2643
3151
|
}
|
|
2644
3152
|
const candidates = [];
|
|
2645
|
-
for (const guide of this.
|
|
3153
|
+
for (const guide of this.candidates()) {
|
|
2646
3154
|
const t = guide.config.trigger;
|
|
2647
3155
|
if (detail.endedGuide && detail.endedGuide.id === guide.id) continue;
|
|
2648
3156
|
if (reason === "ready" || reason === "navigation") {
|
|
@@ -2658,6 +3166,10 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2658
3166
|
if (t.type === "page_load" && t.delayMs > 0 && reason === "timer" && detail.guideId !== guide.id) continue;
|
|
2659
3167
|
if (!this.triggerFires(guide, reason, detail)) continue;
|
|
2660
3168
|
if (!this.isEligible(guide, ctx, url)) continue;
|
|
3169
|
+
if (!this.throttleAllows(guide)) {
|
|
3170
|
+
this.log("throttled", guide.key);
|
|
3171
|
+
continue;
|
|
3172
|
+
}
|
|
2661
3173
|
candidates.push(guide);
|
|
2662
3174
|
}
|
|
2663
3175
|
if (reason === "after_guide" || reason === "navigation") this.drainQueue(url, ctx);
|
|
@@ -2685,7 +3197,7 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2685
3197
|
this.visibilityObservers.delete(guide.id);
|
|
2686
3198
|
};
|
|
2687
3199
|
this.visibilityObservers.set(guide.id, stop);
|
|
2688
|
-
(0,
|
|
3200
|
+
(0, import_guide_runtime5.waitForTarget)([{ kind: "css", value: selector }], { timeoutMs: 0, signal: ac.signal }).then((el) => {
|
|
2689
3201
|
if (ac.signal.aborted) return;
|
|
2690
3202
|
const fire = () => {
|
|
2691
3203
|
stop();
|
|
@@ -2708,6 +3220,13 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2708
3220
|
/** Decides whether a guide can show now, otherwise queues it. */
|
|
2709
3221
|
tryShow(guide, url, manual) {
|
|
2710
3222
|
if (this.destroyed) return false;
|
|
3223
|
+
if (!manual && !this.throttleAllows(guide)) return false;
|
|
3224
|
+
const holdout = this.holdoutFor(guide);
|
|
3225
|
+
if (holdout) {
|
|
3226
|
+
if (this.active && !manual) return false;
|
|
3227
|
+
this.recordImpression(guide, manual);
|
|
3228
|
+
return true;
|
|
3229
|
+
}
|
|
2711
3230
|
if (!isExclusiveGuide(guide)) {
|
|
2712
3231
|
if (this.widgets.has(guide.id)) return false;
|
|
2713
3232
|
this.show(guide, manual);
|
|
@@ -2748,14 +3267,18 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2748
3267
|
guideState(id) {
|
|
2749
3268
|
return this.state.guides[id] ?? (this.state.guides[id] = { impressions: 0 });
|
|
2750
3269
|
}
|
|
2751
|
-
recordImpression(guide) {
|
|
3270
|
+
recordImpression(guide, manual = false) {
|
|
2752
3271
|
const s = this.guideState(guide.id);
|
|
2753
3272
|
s.impressions += 1;
|
|
2754
3273
|
s.lastShownAt = nowIso();
|
|
2755
3274
|
if (!this.session.shownGuides.includes(guide.id)) this.session.shownGuides.push(guide.id);
|
|
3275
|
+
this.recordLaunch(guide, manual);
|
|
2756
3276
|
this.persistState();
|
|
2757
3277
|
this.persistSession();
|
|
2758
|
-
this.
|
|
3278
|
+
const holdout = this.holdoutFor(guide);
|
|
3279
|
+
const experiment = holdout ? { id: holdout.experimentId, variantId: holdout.variantId } : guide.experiment;
|
|
3280
|
+
if (experiment) this.enqueue("experiment_viewed", { properties: { guideExperimentId: experiment.id, variantId: experiment.variantId, ...holdout ? {} : { guideId: guide.id } } });
|
|
3281
|
+
if (!holdout) this.syncGuideState(guide.id, { impression: true });
|
|
2759
3282
|
}
|
|
2760
3283
|
recordEnd(guide, reason) {
|
|
2761
3284
|
const s = this.guideState(guide.id);
|
|
@@ -2779,13 +3302,17 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2779
3302
|
this.reportError(err);
|
|
2780
3303
|
}
|
|
2781
3304
|
}
|
|
2782
|
-
showFlow(
|
|
3305
|
+
showFlow(raw, manual, stepId) {
|
|
2783
3306
|
if (this.editing()) return;
|
|
3307
|
+
const guide = this.localize(raw);
|
|
2784
3308
|
const exclusive = isExclusiveGuide(guide);
|
|
2785
|
-
const entry = { guide, manual, startUrl:
|
|
2786
|
-
const controller = (0,
|
|
3309
|
+
const entry = { guide, manual, startUrl: this.pageHref() };
|
|
3310
|
+
const controller = (0, import_guide_runtime5.runGuide)(guide, {
|
|
2787
3311
|
theme: this.theme(),
|
|
3312
|
+
// Code blocks only arrive when a workspace admin allowed them (the server strips them otherwise).
|
|
3313
|
+
runCode: true,
|
|
2788
3314
|
ctx: () => this.getContext(),
|
|
3315
|
+
currentUrl: () => this.pageHref(),
|
|
2789
3316
|
startAtStep: stepId,
|
|
2790
3317
|
eventBus: this.eventBus,
|
|
2791
3318
|
// Started from Help, Scout or startGuide(): steps on other pages offer to take the user there.
|
|
@@ -2799,7 +3326,7 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2799
3326
|
const step = e.stepId ? guide.config.steps.find((s) => s.id === e.stepId) : void 0;
|
|
2800
3327
|
this.enqueue(e.name, { guideId: guide.id, guideVersion: guide.version, stepId: e.stepId, stepIndex: e.stepIndex, properties: e.properties });
|
|
2801
3328
|
if (e.name === "guide_started") {
|
|
2802
|
-
this.recordImpression(guide);
|
|
3329
|
+
this.recordImpression(guide, manual);
|
|
2803
3330
|
this.emitter.emit("guide:start", guide);
|
|
2804
3331
|
}
|
|
2805
3332
|
if (e.name === "step_viewed" && step) {
|
|
@@ -2846,14 +3373,15 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2846
3373
|
this.widgets.delete(guide.id);
|
|
2847
3374
|
}
|
|
2848
3375
|
}
|
|
2849
|
-
showChecklist(
|
|
3376
|
+
showChecklist(raw) {
|
|
2850
3377
|
if (this.editing()) return;
|
|
3378
|
+
const guide = this.localize(raw);
|
|
2851
3379
|
const config = guide.config.checklist;
|
|
2852
3380
|
if (!config || this.widgets.has(guide.id)) return;
|
|
2853
|
-
const scope = (0,
|
|
3381
|
+
const scope = (0, import_guide_runtime6.createScope)(this.theme(), guide.config.appearance);
|
|
2854
3382
|
const progress = this.getChecklistProgress(guide.id);
|
|
2855
3383
|
const first = !this.state.guides[guide.id]?.impressions;
|
|
2856
|
-
const widget = (0,
|
|
3384
|
+
const widget = (0, import_guide_runtime5.renderChecklist)({
|
|
2857
3385
|
scope: scope.el,
|
|
2858
3386
|
config,
|
|
2859
3387
|
progress,
|
|
@@ -2868,12 +3396,13 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2868
3396
|
}, checklist: widget, scope });
|
|
2869
3397
|
if (first) this.recordImpression(guide);
|
|
2870
3398
|
}
|
|
2871
|
-
showSurvey(
|
|
3399
|
+
showSurvey(raw, manual) {
|
|
2872
3400
|
if (this.editing()) return;
|
|
3401
|
+
const guide = this.localize(raw);
|
|
2873
3402
|
const config = guide.config.survey;
|
|
2874
3403
|
if (!config) return;
|
|
2875
|
-
const scope = (0,
|
|
2876
|
-
const entry = { guide, manual, startUrl:
|
|
3404
|
+
const scope = (0, import_guide_runtime6.createScope)(this.theme(), guide.config.appearance);
|
|
3405
|
+
const entry = { guide, manual, startUrl: this.pageHref(), scope };
|
|
2877
3406
|
const finish = (reason) => {
|
|
2878
3407
|
widget.destroy();
|
|
2879
3408
|
scope.destroy();
|
|
@@ -2885,13 +3414,13 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2885
3414
|
}
|
|
2886
3415
|
this.setTimer(() => this.evaluate("after_guide", { endedGuide: guide }), 0);
|
|
2887
3416
|
};
|
|
2888
|
-
const widget = (0,
|
|
3417
|
+
const widget = (0, import_guide_runtime5.renderSurvey)({
|
|
2889
3418
|
scope: scope.el,
|
|
2890
3419
|
config,
|
|
2891
3420
|
title: guide.name,
|
|
2892
3421
|
onDismiss: () => finish(this.state.surveysSubmitted.includes(guide.id) ? "submitted" : "dismissed"),
|
|
2893
3422
|
onSubmit: (answers) => {
|
|
2894
|
-
const responseId = (0,
|
|
3423
|
+
const responseId = (0, import_runtime18.randomId)(24);
|
|
2895
3424
|
this.state.surveysSubmitted.push(guide.id);
|
|
2896
3425
|
this.persistState();
|
|
2897
3426
|
this.recordEnd(guide, "completed");
|
|
@@ -2904,7 +3433,7 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2904
3433
|
});
|
|
2905
3434
|
entry.survey = widget;
|
|
2906
3435
|
this.active = entry;
|
|
2907
|
-
this.recordImpression(guide);
|
|
3436
|
+
this.recordImpression(guide, manual);
|
|
2908
3437
|
this.enqueue("survey_displayed", { guideId: guide.id, guideVersion: guide.version });
|
|
2909
3438
|
this.emitter.emit("guide:start", guide);
|
|
2910
3439
|
}
|
|
@@ -2913,6 +3442,7 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2913
3442
|
}
|
|
2914
3443
|
navigate(url, newTab) {
|
|
2915
3444
|
try {
|
|
3445
|
+
if (this.openPrototypeTest(url)) return;
|
|
2916
3446
|
if (newTab) window.open(url, "_blank", "noopener");
|
|
2917
3447
|
else location.assign(url);
|
|
2918
3448
|
} catch (err) {
|
|
@@ -2933,11 +3463,17 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2933
3463
|
}
|
|
2934
3464
|
this.destroyMessenger();
|
|
2935
3465
|
if (rc && rc.enabled && !this.resourceCenter) {
|
|
2936
|
-
const scope = (0,
|
|
3466
|
+
const scope = (0, import_guide_runtime6.createScope)(this.theme());
|
|
2937
3467
|
this.resourceCenterScope = scope;
|
|
2938
|
-
this.resourceCenter = (0,
|
|
3468
|
+
this.resourceCenter = (0, import_guide_runtime5.renderResourceCenter)({
|
|
2939
3469
|
scope: scope.el,
|
|
2940
|
-
config: rc,
|
|
3470
|
+
config: (0, import_runtime18.localizeResourceCenter)(rc, this.locales(), this.defaultLocale()),
|
|
3471
|
+
evaluate: (rules) => (0, import_runtime18.evaluateRules)(rules, this.getContext()),
|
|
3472
|
+
announcementsSeenAt: () => this.state.announcementsSeenAt ?? null,
|
|
3473
|
+
onAnnouncementsSeen: () => {
|
|
3474
|
+
this.state.announcementsSeenAt = nowIso();
|
|
3475
|
+
this.persistState();
|
|
3476
|
+
},
|
|
2941
3477
|
guides: this.config.guides.map((g) => ({ id: g.id, key: g.key, name: g.name, kind: g.kind })),
|
|
2942
3478
|
search: (q) => this.searchResourceCenter(q),
|
|
2943
3479
|
audienceIds: () => this.state.audienceIds,
|
|
@@ -2971,7 +3507,7 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2971
3507
|
this.messengerLoading = import("@guidelytech/guide-runtime/messenger").then(({ renderMessenger }) => {
|
|
2972
3508
|
const cfg = this.config?.messenger;
|
|
2973
3509
|
if (this.destroyed || !this.config || !cfg?.enabled || this.messenger) return this.messenger;
|
|
2974
|
-
const scope = (0,
|
|
3510
|
+
const scope = (0, import_guide_runtime6.createScope)(this.theme());
|
|
2975
3511
|
this.messengerScope = scope;
|
|
2976
3512
|
this.messenger = renderMessenger({
|
|
2977
3513
|
scope: scope.el,
|
|
@@ -2982,7 +3518,8 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2982
3518
|
projectId: this.options.projectId,
|
|
2983
3519
|
anonymousId: () => this.anonymousId,
|
|
2984
3520
|
userId: () => this.user?.id || null,
|
|
2985
|
-
pageUrl: () => this.collectUrls() ?
|
|
3521
|
+
pageUrl: () => this.collectUrls() ? this.pageHref() : void 0,
|
|
3522
|
+
locale: () => this.rcLocale()
|
|
2986
3523
|
}),
|
|
2987
3524
|
visitor: () => this.messengerVisitor(),
|
|
2988
3525
|
brand: { logoUrl: this.theme().logoUrl ?? null, name: this.theme().brandName ?? null },
|
|
@@ -2991,7 +3528,8 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2991
3528
|
const seen = readJson(this.store, KEYS.newsSeen(this.options.projectId), []);
|
|
2992
3529
|
writeJson(this.store, KEYS.newsSeen(this.options.projectId), Array.from(/* @__PURE__ */ new Set([...ids, ...Array.isArray(seen) ? seen : []])).slice(0, 200));
|
|
2993
3530
|
},
|
|
2994
|
-
resourceCenter: this.config.resourceCenter,
|
|
3531
|
+
resourceCenter: this.config.resourceCenter ? (0, import_runtime18.localizeResourceCenter)(this.config.resourceCenter, this.locales(), this.defaultLocale()) : null,
|
|
3532
|
+
evaluate: (rules) => (0, import_runtime18.evaluateRules)(rules, this.getContext()),
|
|
2995
3533
|
audienceIds: () => this.state.audienceIds,
|
|
2996
3534
|
guides: this.config.guides.map((g) => ({ id: g.id, key: g.key, name: g.name, kind: g.kind })),
|
|
2997
3535
|
onStartGuide: (idOrKey) => this.startGuide(idOrKey),
|
|
@@ -3015,7 +3553,7 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
3015
3553
|
/** What the messenger personalises from, computed fresh on every render. */
|
|
3016
3554
|
messengerVisitor() {
|
|
3017
3555
|
const ctx = this.getContext();
|
|
3018
|
-
const url =
|
|
3556
|
+
const url = this.pageHref();
|
|
3019
3557
|
const seen = readJson(this.store, KEYS.newsSeen(this.options.projectId), []);
|
|
3020
3558
|
const visitor = buildMessengerVisitor({
|
|
3021
3559
|
guides: this.config?.guides ?? [],
|
|
@@ -3026,11 +3564,24 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
3026
3564
|
isAudienceTargeted: (g) => {
|
|
3027
3565
|
if (!isWithinSchedule(g)) return false;
|
|
3028
3566
|
ctx.rolloutSalt = g.id;
|
|
3029
|
-
return (0,
|
|
3567
|
+
return (0, import_runtime18.evaluateRules)(g.config.targeting, ctx);
|
|
3030
3568
|
},
|
|
3031
3569
|
newsSeen: Array.isArray(seen) ? seen : []
|
|
3032
3570
|
});
|
|
3033
|
-
|
|
3571
|
+
const rc = this.config?.resourceCenter;
|
|
3572
|
+
const rcNews = rc?.announcements?.length ? (0, import_runtime18.visibleAnnouncements)((0, import_runtime18.localizeResourceCenter)(rc, this.locales(), this.defaultLocale()).announcements, this.state.audienceIds).map((a) => ({
|
|
3573
|
+
id: `rc_${a.id}`,
|
|
3574
|
+
key: a.guideId ?? "",
|
|
3575
|
+
title: a.title,
|
|
3576
|
+
body: plainText(a.body),
|
|
3577
|
+
imageUrl: null,
|
|
3578
|
+
publishedAt: a.publishedAt,
|
|
3579
|
+
unread: !(Array.isArray(seen) && seen.includes(`rc_${a.id}`)),
|
|
3580
|
+
articleId: a.articleId ?? null,
|
|
3581
|
+
url: a.url ?? null
|
|
3582
|
+
})) : [];
|
|
3583
|
+
const news = rcNews.length ? [...visitor.news, ...rcNews].sort((a, b) => b.publishedAt.localeCompare(a.publishedAt)).slice(0, 10) : visitor.news;
|
|
3584
|
+
return { ...visitor, news, identified: !!this.user?.id };
|
|
3034
3585
|
}
|
|
3035
3586
|
destroyMessenger() {
|
|
3036
3587
|
this.messenger?.destroy();
|
|
@@ -3038,9 +3589,17 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
3038
3589
|
this.messenger = null;
|
|
3039
3590
|
this.messengerScope = null;
|
|
3040
3591
|
}
|
|
3592
|
+
/** The resource center's language for this visitor, or null for the default language. */
|
|
3593
|
+
rcLocale() {
|
|
3594
|
+
const rc = this.config?.resourceCenter;
|
|
3595
|
+
if (!rc) return null;
|
|
3596
|
+
const locale = (0, import_runtime18.localizeResourceCenter)(rc, this.locales(), this.defaultLocale()).locale;
|
|
3597
|
+
return locale.toLowerCase() === this.defaultLocale().toLowerCase() ? null : locale;
|
|
3598
|
+
}
|
|
3041
3599
|
async loadArticle(id) {
|
|
3042
3600
|
try {
|
|
3043
|
-
const
|
|
3601
|
+
const locale = this.rcLocale();
|
|
3602
|
+
const res = await this.api.get(`/articles/${encodeURIComponent(id)}`, locale ? { params: { locale } } : void 0);
|
|
3044
3603
|
return res.ok && res.data ? res.data.article : null;
|
|
3045
3604
|
} catch {
|
|
3046
3605
|
return null;
|
|
@@ -3078,7 +3637,7 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
3078
3637
|
if (progress.completedTaskIds.includes(task.id)) return;
|
|
3079
3638
|
progress.completedTaskIds.push(task.id);
|
|
3080
3639
|
this.enqueue("checklist_task_completed", { guideId: guide.id, guideVersion: guide.version, properties: { taskId: task.id } });
|
|
3081
|
-
const complete = (0,
|
|
3640
|
+
const complete = (0, import_guide_runtime5.isChecklistComplete)(config, progress) && !progress.completedAt;
|
|
3082
3641
|
if (complete) {
|
|
3083
3642
|
progress.completedAt = nowIso();
|
|
3084
3643
|
this.enqueue("checklist_completed", { guideId: guide.id, guideVersion: guide.version });
|
|
@@ -3101,11 +3660,11 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
3101
3660
|
if (task.completion !== kind || !task.completionValue) continue;
|
|
3102
3661
|
let hit = false;
|
|
3103
3662
|
if (kind === "event") hit = value === task.completionValue;
|
|
3104
|
-
else if (kind === "url_visited") hit = (0,
|
|
3663
|
+
else if (kind === "url_visited") hit = (0, import_guide_runtime5.urlMatches)(task.completionValue, value);
|
|
3105
3664
|
else if (kind === "guide_completed") hit = value.id === task.completionValue || value.key === task.completionValue;
|
|
3106
3665
|
else if (kind === "click") {
|
|
3107
3666
|
try {
|
|
3108
|
-
hit = !!(0,
|
|
3667
|
+
hit = !!(0, import_guide_runtime5.closestComposed)(value, task.completionValue);
|
|
3109
3668
|
} catch {
|
|
3110
3669
|
hit = false;
|
|
3111
3670
|
}
|
|
@@ -3216,6 +3775,18 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
3216
3775
|
closeMessenger() {
|
|
3217
3776
|
this.messenger?.close();
|
|
3218
3777
|
}
|
|
3778
|
+
/**
|
|
3779
|
+
* Opens a Guidely prototype test (its `/validate/<token>` URL on this SDK's API origin, or the bare
|
|
3780
|
+
* token) in a full-screen overlay, attributed to the current user. Returns false, doing nothing, for
|
|
3781
|
+
* any other URL. Guide buttons that open a test URL use this automatically.
|
|
3782
|
+
*/
|
|
3783
|
+
openPrototypeTest(urlOrToken) {
|
|
3784
|
+
if (typeof document === "undefined") return false;
|
|
3785
|
+
const url = matchPrototypeTestUrl(urlOrToken, this.options.apiUrl);
|
|
3786
|
+
if (!url) return false;
|
|
3787
|
+
openPrototypeOverlay(url, { key: this.options.projectId, anonymousId: this.anonymousId, userId: this.user?.id ?? null });
|
|
3788
|
+
return true;
|
|
3789
|
+
}
|
|
3219
3790
|
/**
|
|
3220
3791
|
* Opens the complaint form in the messenger, optionally with a category picked (its id from
|
|
3221
3792
|
* Complaints → Categories). Does nothing when the project doesn't take complaints.
|
|
@@ -3224,6 +3795,14 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
3224
3795
|
if (!this.config?.messenger?.enabled || !this.config.messenger.complaints?.categories.length) return;
|
|
3225
3796
|
void this.loadMessenger().then((m) => m?.openComplaint(categoryId));
|
|
3226
3797
|
}
|
|
3798
|
+
/**
|
|
3799
|
+
* Opens the Ideas tab in the messenger (feature requests and voting), or one idea by its id.
|
|
3800
|
+
* Does nothing when the project hasn't turned on Ideas.
|
|
3801
|
+
*/
|
|
3802
|
+
openIdeas(requestId) {
|
|
3803
|
+
if (!this.config?.messenger?.enabled || !this.config.messenger.ideas) return;
|
|
3804
|
+
void this.loadMessenger().then((m) => m?.openIdeas(requestId));
|
|
3805
|
+
}
|
|
3227
3806
|
/** Opens a new messenger conversation, optionally pre-filling the message. */
|
|
3228
3807
|
showNewMessage(text) {
|
|
3229
3808
|
if (!this.config?.messenger?.enabled) return;
|
|
@@ -3253,14 +3832,15 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
3253
3832
|
const device = detectDevice(navigator.userAgent, window.innerWidth);
|
|
3254
3833
|
const account = this.user?.account;
|
|
3255
3834
|
return {
|
|
3256
|
-
url:
|
|
3257
|
-
path:
|
|
3835
|
+
url: this.pageHref(),
|
|
3836
|
+
path: pathOf2(this.pageHref()),
|
|
3258
3837
|
deviceType: device.type,
|
|
3259
3838
|
browser: device.browser,
|
|
3260
3839
|
os: device.os,
|
|
3261
3840
|
locale: navigator.language || void 0,
|
|
3262
3841
|
environment: this.options.environment,
|
|
3263
3842
|
platform: "web",
|
|
3843
|
+
...this.options.appVersion ? { appVersion: this.options.appVersion } : {},
|
|
3264
3844
|
...this.user?.traits ?? {},
|
|
3265
3845
|
id: this.user?.id || this.anonymousId,
|
|
3266
3846
|
anonymousId: this.anonymousId,
|
|
@@ -3420,20 +4000,20 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
3420
4000
|
// ---------------------------------------------------------------------------
|
|
3421
4001
|
/** Creates (or returns the existing) client and starts initialisation. */
|
|
3422
4002
|
static init(options) {
|
|
3423
|
-
if (
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
void
|
|
3427
|
-
return
|
|
4003
|
+
if (current2 && !current2.destroyed && current2.options.projectId === options.projectId) return current2;
|
|
4004
|
+
current2?.destroy();
|
|
4005
|
+
current2 = new _GuidelyClient(options);
|
|
4006
|
+
void current2.init();
|
|
4007
|
+
return current2;
|
|
3428
4008
|
}
|
|
3429
4009
|
/** The most recently created client, if any. */
|
|
3430
4010
|
static get() {
|
|
3431
|
-
return
|
|
4011
|
+
return current2 && !current2.destroyed ? current2 : null;
|
|
3432
4012
|
}
|
|
3433
4013
|
};
|
|
3434
|
-
var
|
|
4014
|
+
var current2 = null;
|
|
3435
4015
|
function webOnly(config) {
|
|
3436
|
-
return config.guides.every((g) => (0,
|
|
4016
|
+
return config.guides.every((g) => (0, import_runtime18.supportsPlatform)(g.config, "web")) ? config : { ...config, guides: config.guides.filter((g) => (0, import_runtime18.supportsPlatform)(g.config, "web")) };
|
|
3437
4017
|
}
|
|
3438
4018
|
function pathOf2(url) {
|
|
3439
4019
|
try {
|
|
@@ -3453,7 +4033,7 @@ function externalReferrer() {
|
|
|
3453
4033
|
}
|
|
3454
4034
|
function createGuidely(options) {
|
|
3455
4035
|
const client = new GuidelyClient(options);
|
|
3456
|
-
|
|
4036
|
+
current2 = client;
|
|
3457
4037
|
void client.init();
|
|
3458
4038
|
return client;
|
|
3459
4039
|
}
|
|
@@ -3476,17 +4056,24 @@ var index_default = Guidely;
|
|
|
3476
4056
|
GuidelyClient,
|
|
3477
4057
|
HEARTBEAT_MS,
|
|
3478
4058
|
KEYS,
|
|
4059
|
+
PROTOTYPE_CLOSE_MESSAGE,
|
|
3479
4060
|
SDK_NAME,
|
|
3480
4061
|
SDK_VERSION,
|
|
3481
4062
|
areMutuallyExclusive,
|
|
3482
4063
|
buildContext,
|
|
3483
4064
|
captureSnapshot,
|
|
4065
|
+
countsTowardThrottle,
|
|
3484
4066
|
createGuidely,
|
|
3485
4067
|
createStorage,
|
|
3486
4068
|
detectDevice,
|
|
4069
|
+
isAppUrl,
|
|
3487
4070
|
isExclusiveGuide,
|
|
3488
4071
|
isFrequencyEligible,
|
|
4072
|
+
isThrottleEligible,
|
|
3489
4073
|
isWithinSchedule,
|
|
4074
|
+
matchPrototypeTestUrl,
|
|
4075
|
+
normalizeAppUrl,
|
|
4076
|
+
pruneLaunches,
|
|
3490
4077
|
watchNavigation
|
|
3491
4078
|
});
|
|
3492
4079
|
//# sourceMappingURL=index.cjs.map
|