@guidelytech/sdk-core 0.9.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -4
- package/dist/{capture-Dqd7up2n.d.ts → capture-CgN69Lxq.d.cts} +2 -1
- package/dist/{capture-C3faAYb7.d.cts → capture-Csz721-A.d.ts} +2 -1
- package/dist/capture.cjs +4 -0
- package/dist/capture.cjs.map +1 -1
- package/dist/capture.d.cts +3 -2
- package/dist/capture.d.ts +3 -2
- package/dist/capture.js +4 -6
- package/dist/capture.js.map +1 -1
- package/dist/chunk-JUSM7J5N.js +376 -0
- package/dist/chunk-JUSM7J5N.js.map +1 -0
- package/dist/{chunk-544KSOC3.js → chunk-ZAF2KRJA.js} +142 -22
- package/dist/chunk-ZAF2KRJA.js.map +1 -0
- package/dist/headless.cjs +307 -44
- package/dist/headless.cjs.map +1 -1
- package/dist/headless.d.cts +44 -3
- package/dist/headless.d.ts +44 -3
- package/dist/headless.js +21 -9
- package/dist/health-sbP9JwTE.d.cts +90 -0
- package/dist/health-sbP9JwTE.d.ts +90 -0
- package/dist/{http-47tiAlOM.d.ts → http-Cb6KYYFv.d.cts} +10 -35
- package/dist/{http-47tiAlOM.d.cts → http-D047Ngxk.d.ts} +10 -35
- package/dist/index.cjs +1352 -500
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +81 -8
- package/dist/index.d.ts +81 -8
- package/dist/index.js +809 -211
- package/dist/index.js.map +1 -1
- package/dist/recorder.cjs +70 -9
- package/dist/recorder.cjs.map +1 -1
- package/dist/recorder.d.cts +9 -0
- package/dist/recorder.d.ts +9 -0
- package/dist/recorder.js +57 -11
- package/dist/recorder.js.map +1 -1
- package/dist/{types-DnJW_4ck.d.ts → types-BDlebXrm.d.ts} +68 -7
- package/dist/{types-zM5e3loY.d.cts → types-Mtlmlj5C.d.cts} +68 -7
- package/package.json +4 -4
- package/dist/chunk-544KSOC3.js.map +0 -1
- package/dist/chunk-AKNE7D5M.js +0 -160
- package/dist/chunk-AKNE7D5M.js.map +0 -1
- package/dist/chunk-CYAG6ZYX.js +0 -85
- package/dist/chunk-CYAG6ZYX.js.map +0 -1
- package/dist/chunk-GDF6RCHQ.js +0 -209
- package/dist/chunk-GDF6RCHQ.js.map +0 -1
- package/dist/types-DbKQPvFt.d.cts +0 -238
- package/dist/types-DqhNl5Am.d.ts +0 -238
- package/dist/types-RyeRQd9f.d.ts +0 -226
- package/dist/types-d4eOerxi.d.cts +0 -226
package/dist/index.cjs
CHANGED
|
@@ -33,37 +33,559 @@ 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
|
|
|
72
|
+
// src/agent-turns.ts
|
|
73
|
+
var import_runtime4 = require("@guidelytech/shared/runtime");
|
|
74
|
+
|
|
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;
|
|
94
|
+
}
|
|
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();
|
|
102
|
+
}
|
|
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;
|
|
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));
|
|
114
|
+
}
|
|
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());
|
|
119
|
+
}
|
|
120
|
+
canSend() {
|
|
121
|
+
return this.waitMs() === 0;
|
|
122
|
+
}
|
|
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);
|
|
155
|
+
}
|
|
156
|
+
function isServerFailure(status) {
|
|
157
|
+
return status === 429 || status >= 500;
|
|
158
|
+
}
|
|
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) {
|
|
214
|
+
try {
|
|
215
|
+
data = await res.json();
|
|
216
|
+
} catch {
|
|
217
|
+
data = null;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return { status: res.status, ok: res.ok, data, etag: res.headers?.get?.("etag") ?? null };
|
|
221
|
+
} finally {
|
|
222
|
+
if (timer) clearTimeout(timer);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
get(path, options) {
|
|
226
|
+
return this.request("GET", path, options);
|
|
227
|
+
}
|
|
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
|
+
}
|
|
400
|
+
function writeJson(store, key, value) {
|
|
401
|
+
try {
|
|
402
|
+
store.set(key, JSON.stringify(value));
|
|
403
|
+
} catch {
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
var KEYS = {
|
|
407
|
+
anon: "guidely:anon",
|
|
408
|
+
seen: "guidely:seen",
|
|
409
|
+
config: (key) => `guidely:config:${key}`,
|
|
410
|
+
etag: (key) => `guidely:etag:${key}`,
|
|
411
|
+
state: (key) => `guidely:state:${key}`,
|
|
412
|
+
user: (key) => `guidely:user:${key}`,
|
|
413
|
+
queue: (key) => `guidely:queue:${key}`,
|
|
414
|
+
/** Survey answers and guide/checklist progress not yet accepted by the server. */
|
|
415
|
+
outbox: (key) => `guidely:outbox:${key}`,
|
|
416
|
+
/** Agent Analytics turns not yet accepted by the server. */
|
|
417
|
+
agentOutbox: (key) => `guidely:agents:${key}`,
|
|
418
|
+
session: "guidely:session",
|
|
419
|
+
active: "guidely:active",
|
|
420
|
+
authoring: "guidely:authoring",
|
|
421
|
+
/** How-to capture session marker ({ token, … }) and its not-yet-uploaded steps. */
|
|
422
|
+
capture: "guidely:capture",
|
|
423
|
+
capturePending: "guidely:capture:pending",
|
|
424
|
+
/** Messenger conversations this browser can read: [{ id, token }]. */
|
|
425
|
+
conversations: (key) => `guidely:conversations:${key}`,
|
|
426
|
+
/** Announcement ids opened in the messenger's News tab. */
|
|
427
|
+
newsSeen: (key) => `guidely:news:${key}`
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
// src/outbox.ts
|
|
431
|
+
var Outbox = class {
|
|
432
|
+
constructor(opts) {
|
|
433
|
+
__publicField(this, "opts", opts);
|
|
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);
|
|
452
|
+
}
|
|
453
|
+
get size() {
|
|
454
|
+
return this.items.length;
|
|
455
|
+
}
|
|
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();
|
|
464
|
+
}
|
|
465
|
+
persist() {
|
|
466
|
+
writeJson(this.opts.store, this.opts.storageKey, this.items);
|
|
467
|
+
}
|
|
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);
|
|
475
|
+
}
|
|
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
|
+
}
|
|
505
|
+
}
|
|
506
|
+
destroy() {
|
|
507
|
+
this.destroyed = true;
|
|
508
|
+
this.offRecover?.();
|
|
509
|
+
if (this.timer) clearTimeout(this.timer);
|
|
510
|
+
this.timer = null;
|
|
511
|
+
}
|
|
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) {
|
|
537
|
+
try {
|
|
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 };
|
|
541
|
+
} catch (err) {
|
|
542
|
+
onError?.(err);
|
|
543
|
+
return null;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
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;
|
|
570
|
+
}
|
|
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 });
|
|
574
|
+
}
|
|
575
|
+
}
|
|
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();
|
|
584
|
+
}
|
|
585
|
+
};
|
|
586
|
+
|
|
65
587
|
// src/authoring.ts
|
|
66
|
-
var
|
|
588
|
+
var import_guide_runtime3 = require("@guidelytech/guide-runtime");
|
|
67
589
|
|
|
68
590
|
// src/snapshot.ts
|
|
69
591
|
var MAX_SNAPSHOT_BYTES = 6 * 1024 * 1024;
|
|
@@ -125,98 +647,291 @@ function captureSnapshot(doc, maskSelectors, maxBytes = MAX_SNAPSHOT_BYTES) {
|
|
|
125
647
|
head = doc.createElement("head");
|
|
126
648
|
clone.prepend(head);
|
|
127
649
|
}
|
|
128
|
-
if (adopted) {
|
|
129
|
-
const style = doc.createElement("style");
|
|
130
|
-
style.textContent = adopted;
|
|
131
|
-
head.append(style);
|
|
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) {
|
|
680
|
+
const s = (raw || "").trim();
|
|
681
|
+
if (!s || out.includes(s)) continue;
|
|
682
|
+
try {
|
|
683
|
+
probe?.querySelector(s);
|
|
684
|
+
out.push(s);
|
|
685
|
+
} catch {
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
return out;
|
|
689
|
+
}
|
|
690
|
+
function isIgnoredElement(el) {
|
|
691
|
+
return (0, import_guide_runtime.isGuidelyElement)(el) || !!(0, import_guide_runtime.closestComposed)(el, "[data-guidely-ignore]");
|
|
692
|
+
}
|
|
693
|
+
function interactiveElement(target) {
|
|
694
|
+
try {
|
|
695
|
+
return (0, import_guide_runtime.closestComposed)(target, INTERACTIVE_SELECTOR);
|
|
696
|
+
} catch {
|
|
697
|
+
return null;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
function esc(value) {
|
|
701
|
+
const g = globalThis;
|
|
702
|
+
return g.CSS?.escape ? g.CSS.escape(value) : value.replace(/[^\w-]/g, (c) => `\\${c}`);
|
|
703
|
+
}
|
|
704
|
+
var attr = (tag, name, value) => value ? `${tag}[${name}="${value.replace(/["\\]/g, "\\$&")}"]` : `${tag}[${name}]`;
|
|
705
|
+
function elementSelector(el) {
|
|
706
|
+
const own = ownSelector(el);
|
|
707
|
+
const chain = (0, import_guide_runtime.shadowHostChain)(el);
|
|
708
|
+
return (chain ? chain + import_guide_runtime.SHADOW_SEPARATOR + own : own).slice(0, 300);
|
|
709
|
+
}
|
|
710
|
+
function ownSelector(el) {
|
|
711
|
+
const tag = el.tagName.toLowerCase();
|
|
712
|
+
if (el.id && (0, import_guide_runtime.isStableId)(el.id)) return `#${esc(el.id)}`;
|
|
713
|
+
for (const name of PREFERRED_ATTRS) if (el.hasAttribute(name)) return attr(tag, name, el.getAttribute(name) || "");
|
|
714
|
+
for (const a of Array.from(el.attributes)) {
|
|
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);
|
|
716
|
+
}
|
|
717
|
+
const label = el.getAttribute("aria-label");
|
|
718
|
+
if (label && label.length <= 80) return attr(tag, "aria-label", label);
|
|
719
|
+
const parts = [];
|
|
720
|
+
let cur = el;
|
|
721
|
+
while (cur && parts.length < 4) {
|
|
722
|
+
const t = cur.tagName.toLowerCase();
|
|
723
|
+
if (t === "html" || t === "body") break;
|
|
724
|
+
if (cur !== el && cur.id && (0, import_guide_runtime.isStableId)(cur.id)) {
|
|
725
|
+
parts.unshift(`#${esc(cur.id)}`);
|
|
726
|
+
break;
|
|
727
|
+
}
|
|
728
|
+
const parent = cur.parentElement;
|
|
729
|
+
const node = cur;
|
|
730
|
+
const same = parent ? Array.from(parent.children).filter((c) => c.tagName === node.tagName) : [];
|
|
731
|
+
parts.unshift(same.length > 1 ? `${t}:nth-of-type(${same.indexOf(node) + 1})` : t);
|
|
732
|
+
cur = parent;
|
|
733
|
+
}
|
|
734
|
+
return parts.join(" > ") || tag;
|
|
735
|
+
}
|
|
736
|
+
function matches(el, selector, deep) {
|
|
737
|
+
if (!selector) return false;
|
|
738
|
+
try {
|
|
739
|
+
return !!(0, import_guide_runtime.closestComposed)(el, selector) || deep && !!el.querySelector(selector);
|
|
740
|
+
} catch {
|
|
741
|
+
return false;
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
function elementText(el, mask, maskAll) {
|
|
745
|
+
if (maskAll || matches(el, mask, true)) return void 0;
|
|
746
|
+
const tag = el.tagName.toLowerCase();
|
|
747
|
+
let text = "";
|
|
748
|
+
if (tag === "input") {
|
|
749
|
+
if (!/^(button|submit|reset)$/i.test(el.type || "")) return void 0;
|
|
750
|
+
text = el.value;
|
|
751
|
+
} else if (tag === "select" || tag === "textarea") return void 0;
|
|
752
|
+
else text = el.innerText ?? el.textContent ?? "";
|
|
753
|
+
text = (text || el.getAttribute("aria-label") || el.getAttribute("title") || "").replace(/\s+/g, " ").trim();
|
|
754
|
+
return text ? text.slice(0, 100) : void 0;
|
|
755
|
+
}
|
|
756
|
+
function linkHref(el) {
|
|
757
|
+
const a = (0, import_guide_runtime.closestComposed)(el, "a[href]");
|
|
758
|
+
if (!a) return void 0;
|
|
759
|
+
try {
|
|
760
|
+
const u = new URL(a.getAttribute("href") || "", location.href);
|
|
761
|
+
if (!/^https?:$/.test(u.protocol)) return void 0;
|
|
762
|
+
return u.origin === location.origin ? u.pathname : u.origin;
|
|
763
|
+
} catch {
|
|
764
|
+
return void 0;
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
function describeClick(el, e, opts) {
|
|
768
|
+
const props = { tag: el.tagName.toLowerCase(), selector: elementSelector(el), x: Math.round(e.pageX ?? 0) || 0, y: Math.round(e.pageY ?? 0) || 0 };
|
|
769
|
+
const text = elementText(el, opts.mask, opts.maskAll);
|
|
770
|
+
if (text) props.text = text;
|
|
771
|
+
const href = opts.collectUrls ? linkHref(el) : void 0;
|
|
772
|
+
if (href) props.href = href;
|
|
773
|
+
return props;
|
|
774
|
+
}
|
|
775
|
+
var RageClickDetector = class {
|
|
776
|
+
constructor(threshold = 3, windowMs = 1e3, radius = 30) {
|
|
777
|
+
__publicField(this, "threshold", threshold);
|
|
778
|
+
__publicField(this, "windowMs", windowMs);
|
|
779
|
+
__publicField(this, "radius", radius);
|
|
780
|
+
__publicField(this, "clicks", []);
|
|
781
|
+
__publicField(this, "fired", false);
|
|
782
|
+
}
|
|
783
|
+
/** Clicks in the current burst once it has fired (rage), otherwise 0. */
|
|
784
|
+
get burst() {
|
|
785
|
+
return this.fired ? this.clicks.length : 0;
|
|
132
786
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
}
|
|
787
|
+
/** Records a click; returns the click count when it completes a burst, otherwise 0. */
|
|
788
|
+
add(x, y, t) {
|
|
789
|
+
const first = this.clicks[0];
|
|
790
|
+
const last = this.clicks[this.clicks.length - 1];
|
|
791
|
+
if (!first || !last || t - last.t > this.windowMs || Math.hypot(x - first.x, y - first.y) > this.radius) {
|
|
792
|
+
this.clicks = [];
|
|
793
|
+
this.fired = false;
|
|
794
|
+
}
|
|
795
|
+
this.clicks = this.clicks.filter((c) => t - c.t <= this.windowMs);
|
|
796
|
+
this.clicks.push({ x, y, t });
|
|
797
|
+
if (this.fired || this.clicks.length < this.threshold) return 0;
|
|
798
|
+
this.fired = true;
|
|
799
|
+
return this.clicks.length;
|
|
800
|
+
}
|
|
801
|
+
};
|
|
143
802
|
|
|
144
|
-
// src/
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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;
|
|
148
816
|
}
|
|
149
|
-
function
|
|
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;
|
|
150
858
|
try {
|
|
151
|
-
|
|
152
|
-
const s = window[area];
|
|
153
|
-
if (!s) return null;
|
|
154
|
-
const probe = "__guidely_probe__";
|
|
155
|
-
s.setItem(probe, "1");
|
|
156
|
-
s.removeItem(probe);
|
|
157
|
-
return {
|
|
158
|
-
get: (k) => {
|
|
159
|
-
try {
|
|
160
|
-
return s.getItem(k);
|
|
161
|
-
} catch {
|
|
162
|
-
return null;
|
|
163
|
-
}
|
|
164
|
-
},
|
|
165
|
-
set: (k, v) => {
|
|
166
|
-
try {
|
|
167
|
-
s.setItem(k, v);
|
|
168
|
-
} catch {
|
|
169
|
-
}
|
|
170
|
-
},
|
|
171
|
-
remove: (k) => {
|
|
172
|
-
try {
|
|
173
|
-
s.removeItem(k);
|
|
174
|
-
} catch {
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
};
|
|
859
|
+
return !!(0, import_guide_runtime2.closestComposed)(el, mask);
|
|
178
860
|
} catch {
|
|
179
|
-
return
|
|
861
|
+
return false;
|
|
180
862
|
}
|
|
181
863
|
}
|
|
182
|
-
function
|
|
183
|
-
if (
|
|
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;
|
|
864
|
+
function featureAttributeOf(target, attribute) {
|
|
865
|
+
if (!attribute) return null;
|
|
189
866
|
try {
|
|
190
|
-
|
|
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;
|
|
191
870
|
} catch {
|
|
192
|
-
return
|
|
871
|
+
return null;
|
|
193
872
|
}
|
|
194
873
|
}
|
|
195
|
-
function
|
|
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;
|
|
881
|
+
}
|
|
882
|
+
}
|
|
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;
|
|
890
|
+
}
|
|
891
|
+
return elementText(el, opts.mask, opts.maskAll);
|
|
892
|
+
}
|
|
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 = "/";
|
|
196
920
|
try {
|
|
197
|
-
|
|
921
|
+
path = new URL(url, location.href).pathname;
|
|
198
922
|
} catch {
|
|
199
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;
|
|
200
934
|
}
|
|
201
|
-
var KEYS = {
|
|
202
|
-
anon: "guidely:anon",
|
|
203
|
-
seen: "guidely:seen",
|
|
204
|
-
config: (key) => `guidely:config:${key}`,
|
|
205
|
-
etag: (key) => `guidely:etag:${key}`,
|
|
206
|
-
state: (key) => `guidely:state:${key}`,
|
|
207
|
-
user: (key) => `guidely:user:${key}`,
|
|
208
|
-
queue: (key) => `guidely:queue:${key}`,
|
|
209
|
-
session: "guidely:session",
|
|
210
|
-
active: "guidely:active",
|
|
211
|
-
authoring: "guidely:authoring",
|
|
212
|
-
/** How-to capture session marker ({ token, … }) and its not-yet-uploaded steps. */
|
|
213
|
-
capture: "guidely:capture",
|
|
214
|
-
capturePending: "guidely:capture:pending",
|
|
215
|
-
/** Messenger conversations this browser can read: [{ id, token }]. */
|
|
216
|
-
conversations: (key) => `guidely:conversations:${key}`,
|
|
217
|
-
/** Announcement ids opened in the messenger's News tab. */
|
|
218
|
-
newsSeen: (key) => `guidely:news:${key}`
|
|
219
|
-
};
|
|
220
935
|
|
|
221
936
|
// src/authoring.ts
|
|
222
937
|
async function loadSchemas() {
|
|
@@ -286,8 +1001,10 @@ var AuthoringBridge = class {
|
|
|
286
1001
|
}
|
|
287
1002
|
this.active = true;
|
|
288
1003
|
this.opts.onActive(true);
|
|
289
|
-
this.session = (0,
|
|
1004
|
+
this.session = (0, import_guide_runtime3.startAuthoring)({
|
|
290
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,
|
|
291
1008
|
onPick: (pick) => this.sendPick(pick),
|
|
292
1009
|
onExit: () => this.stop()
|
|
293
1010
|
});
|
|
@@ -304,7 +1021,7 @@ var AuthoringBridge = class {
|
|
|
304
1021
|
masked(el) {
|
|
305
1022
|
return this.opts.maskSelectors.some((sel) => {
|
|
306
1023
|
try {
|
|
307
|
-
return !!
|
|
1024
|
+
return !!(0, import_guide_runtime3.closestComposed)(el, sel);
|
|
308
1025
|
} catch {
|
|
309
1026
|
return false;
|
|
310
1027
|
}
|
|
@@ -315,6 +1032,7 @@ var AuthoringBridge = class {
|
|
|
315
1032
|
const { element, ...rest } = raw;
|
|
316
1033
|
const pick = this.masked(element) ? { ...rest, textSnapshot: "", selectors: rest.selectors.filter((s) => s.kind !== "text") } : rest;
|
|
317
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 });
|
|
318
1036
|
this.post({ type: "guidely:pick", sessionId: this.info.id, pick });
|
|
319
1037
|
void this.opts.api.post("/authoring/session/picks", pick, { params: { token: this.token } }).catch((err) => this.opts.onError(err));
|
|
320
1038
|
}
|
|
@@ -373,7 +1091,7 @@ var AuthoringBridge = class {
|
|
|
373
1091
|
const appearance = msg.appearance ? GuideConfigSchema.shape.appearance.parse(msg.appearance) : void 0;
|
|
374
1092
|
this.session?.setPicking(false);
|
|
375
1093
|
this.session?.setStatus(`Previewing step \u201C${step.title || step.id}\u201D \u2014 close it to continue picking`);
|
|
376
|
-
this.preview = (0,
|
|
1094
|
+
this.preview = (0, import_guide_runtime3.previewStep)(step, { ...this.opts.theme(), ...msg.theme || {} }, {
|
|
377
1095
|
appearance,
|
|
378
1096
|
onEnd: () => {
|
|
379
1097
|
this.preview = null;
|
|
@@ -390,7 +1108,7 @@ var AuthoringBridge = class {
|
|
|
390
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) };
|
|
391
1109
|
this.session?.setPicking(false);
|
|
392
1110
|
this.session?.setStatus(`Previewing \u201C${guide.name}\u201D`);
|
|
393
|
-
this.previewGuide = (0,
|
|
1111
|
+
this.previewGuide = (0, import_guide_runtime3.runGuide)(guide, {
|
|
394
1112
|
theme: this.opts.theme(),
|
|
395
1113
|
ctx: this.opts.ctx,
|
|
396
1114
|
onNavigate: (url, newTab) => newTab ? window.open(url, "_blank", "noopener") : window.location.assign(url),
|
|
@@ -404,7 +1122,7 @@ var AuthoringBridge = class {
|
|
|
404
1122
|
break;
|
|
405
1123
|
}
|
|
406
1124
|
case "guidely:highlight": {
|
|
407
|
-
const result = (0,
|
|
1125
|
+
const result = (0, import_guide_runtime3.highlightSelectors)(Array.isArray(msg.selectors) ? msg.selectors : [], { durationMs: 3e3 });
|
|
408
1126
|
this.post({ type: "guidely:highlight-result", sessionId: this.info.id, counts: result.counts, resolved: !!result.resolved });
|
|
409
1127
|
break;
|
|
410
1128
|
}
|
|
@@ -448,169 +1166,41 @@ var AuthoringBridge = class {
|
|
|
448
1166
|
};
|
|
449
1167
|
|
|
450
1168
|
// src/capture-token.ts
|
|
451
|
-
var
|
|
1169
|
+
var import_runtime9 = require("@guidelytech/shared/runtime");
|
|
452
1170
|
function takeCaptureToken(sessionStore) {
|
|
453
1171
|
if (typeof window === "undefined") return null;
|
|
454
1172
|
let token = null;
|
|
455
1173
|
let authoringLink = false;
|
|
456
1174
|
try {
|
|
457
1175
|
const url = new URL(window.location.href);
|
|
458
|
-
token = url.searchParams.get(
|
|
1176
|
+
token = url.searchParams.get(import_runtime9.CAPTURE_PARAM);
|
|
459
1177
|
authoringLink = url.searchParams.has(AUTHORING_PARAM);
|
|
460
1178
|
if (token) {
|
|
461
|
-
url.searchParams.delete(
|
|
1179
|
+
url.searchParams.delete(import_runtime9.CAPTURE_PARAM);
|
|
462
1180
|
window.history.replaceState(window.history.state, "", url.toString());
|
|
463
1181
|
}
|
|
464
|
-
} catch {
|
|
465
|
-
token = null;
|
|
466
|
-
}
|
|
467
|
-
const saved = readJson(sessionStore, KEYS.capture, null);
|
|
468
|
-
let marker = null;
|
|
469
|
-
if (token) marker = saved?.token === token ? saved : { token };
|
|
470
|
-
else if (saved?.token && !authoringLink) marker = saved;
|
|
471
|
-
if (!marker) {
|
|
472
|
-
if (saved) {
|
|
473
|
-
sessionStore.remove(KEYS.capture);
|
|
474
|
-
sessionStore.remove(KEYS.capturePending);
|
|
475
|
-
}
|
|
476
|
-
return null;
|
|
477
|
-
}
|
|
478
|
-
if (token && saved?.token !== token) sessionStore.remove(KEYS.capturePending);
|
|
479
|
-
writeJson(sessionStore, KEYS.capture, marker);
|
|
480
|
-
sessionStore.remove(KEYS.authoring);
|
|
481
|
-
return marker;
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
// src/load-capture.ts
|
|
485
|
-
var loadCapture = async () => (await import("./capture.cjs")).capture;
|
|
486
|
-
|
|
487
|
-
// src/autocapture.ts
|
|
488
|
-
var import_guide_runtime2 = require("@guidelytech/guide-runtime");
|
|
489
|
-
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]";
|
|
490
|
-
var DEFAULT_MASK_SELECTORS = ["[data-guidely-mask]", ".guidely-mask"];
|
|
491
|
-
var PREFERRED_ATTRS = ["data-testid", "data-test-id", "data-test", "data-cy", "data-qa", "data-guidely-track", "data-tour"];
|
|
492
|
-
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;
|
|
493
|
-
function validSelectors(list) {
|
|
494
|
-
const out = [];
|
|
495
|
-
const probe = typeof document !== "undefined" ? document.createDocumentFragment() : null;
|
|
496
|
-
for (const raw of list) {
|
|
497
|
-
const s = (raw || "").trim();
|
|
498
|
-
if (!s || out.includes(s)) continue;
|
|
499
|
-
try {
|
|
500
|
-
probe?.querySelector(s);
|
|
501
|
-
out.push(s);
|
|
502
|
-
} catch {
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
return out;
|
|
506
|
-
}
|
|
507
|
-
function isIgnoredElement(el) {
|
|
508
|
-
return (0, import_guide_runtime2.isGuidelyElement)(el) || !!el.closest("[data-guidely-ignore]");
|
|
509
|
-
}
|
|
510
|
-
function interactiveElement(target) {
|
|
511
|
-
try {
|
|
512
|
-
return target.closest(INTERACTIVE_SELECTOR);
|
|
513
|
-
} catch {
|
|
514
|
-
return null;
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
function esc(value) {
|
|
518
|
-
const g = globalThis;
|
|
519
|
-
return g.CSS?.escape ? g.CSS.escape(value) : value.replace(/[^\w-]/g, (c) => `\\${c}`);
|
|
520
|
-
}
|
|
521
|
-
var attr = (tag, name, value) => value ? `${tag}[${name}="${value.replace(/["\\]/g, "\\$&")}"]` : `${tag}[${name}]`;
|
|
522
|
-
function elementSelector(el) {
|
|
523
|
-
const tag = el.tagName.toLowerCase();
|
|
524
|
-
if (el.id && (0, import_guide_runtime2.isStableId)(el.id)) return `#${esc(el.id)}`;
|
|
525
|
-
for (const name of PREFERRED_ATTRS) if (el.hasAttribute(name)) return attr(tag, name, el.getAttribute(name) || "").slice(0, 300);
|
|
526
|
-
for (const a of Array.from(el.attributes)) {
|
|
527
|
-
if (a.name.startsWith("data-") && !NOISY_DATA_ATTR.test(a.name) && a.value && a.value.length <= 60) return attr(tag, a.name, a.value).slice(0, 300);
|
|
528
|
-
}
|
|
529
|
-
const label = el.getAttribute("aria-label");
|
|
530
|
-
if (label && label.length <= 80) return attr(tag, "aria-label", label).slice(0, 300);
|
|
531
|
-
const parts = [];
|
|
532
|
-
let cur = el;
|
|
533
|
-
while (cur && parts.length < 4) {
|
|
534
|
-
const t = cur.tagName.toLowerCase();
|
|
535
|
-
if (t === "html" || t === "body") break;
|
|
536
|
-
if (cur !== el && cur.id && (0, import_guide_runtime2.isStableId)(cur.id)) {
|
|
537
|
-
parts.unshift(`#${esc(cur.id)}`);
|
|
538
|
-
break;
|
|
539
|
-
}
|
|
540
|
-
const parent = cur.parentElement;
|
|
541
|
-
const node = cur;
|
|
542
|
-
const same = parent ? Array.from(parent.children).filter((c) => c.tagName === node.tagName) : [];
|
|
543
|
-
parts.unshift(same.length > 1 ? `${t}:nth-of-type(${same.indexOf(node) + 1})` : t);
|
|
544
|
-
cur = parent;
|
|
545
|
-
}
|
|
546
|
-
return (parts.join(" > ") || tag).slice(0, 300);
|
|
547
|
-
}
|
|
548
|
-
function matches(el, selector, deep) {
|
|
549
|
-
if (!selector) return false;
|
|
550
|
-
try {
|
|
551
|
-
return !!el.closest(selector) || deep && !!el.querySelector(selector);
|
|
552
|
-
} catch {
|
|
553
|
-
return false;
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
function elementText(el, mask, maskAll) {
|
|
557
|
-
if (maskAll || matches(el, mask, true)) return void 0;
|
|
558
|
-
const tag = el.tagName.toLowerCase();
|
|
559
|
-
let text = "";
|
|
560
|
-
if (tag === "input") {
|
|
561
|
-
if (!/^(button|submit|reset)$/i.test(el.type || "")) return void 0;
|
|
562
|
-
text = el.value;
|
|
563
|
-
} else if (tag === "select" || tag === "textarea") return void 0;
|
|
564
|
-
else text = el.innerText ?? el.textContent ?? "";
|
|
565
|
-
text = (text || el.getAttribute("aria-label") || el.getAttribute("title") || "").replace(/\s+/g, " ").trim();
|
|
566
|
-
return text ? text.slice(0, 100) : void 0;
|
|
567
|
-
}
|
|
568
|
-
function linkHref(el) {
|
|
569
|
-
const a = el.closest("a[href]");
|
|
570
|
-
if (!a) return void 0;
|
|
571
|
-
try {
|
|
572
|
-
const u = new URL(a.getAttribute("href") || "", location.href);
|
|
573
|
-
if (!/^https?:$/.test(u.protocol)) return void 0;
|
|
574
|
-
return u.origin === location.origin ? u.pathname : u.origin;
|
|
575
|
-
} catch {
|
|
576
|
-
return void 0;
|
|
577
|
-
}
|
|
578
|
-
}
|
|
579
|
-
function describeClick(el, e, opts) {
|
|
580
|
-
const props = { tag: el.tagName.toLowerCase(), selector: elementSelector(el), x: Math.round(e.pageX ?? 0) || 0, y: Math.round(e.pageY ?? 0) || 0 };
|
|
581
|
-
const text = elementText(el, opts.mask, opts.maskAll);
|
|
582
|
-
if (text) props.text = text;
|
|
583
|
-
const href = opts.collectUrls ? linkHref(el) : void 0;
|
|
584
|
-
if (href) props.href = href;
|
|
585
|
-
return props;
|
|
586
|
-
}
|
|
587
|
-
var RageClickDetector = class {
|
|
588
|
-
constructor(threshold = 3, windowMs = 1e3, radius = 30) {
|
|
589
|
-
__publicField(this, "threshold", threshold);
|
|
590
|
-
__publicField(this, "windowMs", windowMs);
|
|
591
|
-
__publicField(this, "radius", radius);
|
|
592
|
-
__publicField(this, "clicks", []);
|
|
593
|
-
__publicField(this, "fired", false);
|
|
594
|
-
}
|
|
595
|
-
/** Clicks in the current burst once it has fired (rage), otherwise 0. */
|
|
596
|
-
get burst() {
|
|
597
|
-
return this.fired ? this.clicks.length : 0;
|
|
598
|
-
}
|
|
599
|
-
/** Records a click; returns the click count when it completes a burst, otherwise 0. */
|
|
600
|
-
add(x, y, t) {
|
|
601
|
-
const first = this.clicks[0];
|
|
602
|
-
const last = this.clicks[this.clicks.length - 1];
|
|
603
|
-
if (!first || !last || t - last.t > this.windowMs || Math.hypot(x - first.x, y - first.y) > this.radius) {
|
|
604
|
-
this.clicks = [];
|
|
605
|
-
this.fired = false;
|
|
1182
|
+
} catch {
|
|
1183
|
+
token = null;
|
|
1184
|
+
}
|
|
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);
|
|
606
1193
|
}
|
|
607
|
-
|
|
608
|
-
this.clicks.push({ x, y, t });
|
|
609
|
-
if (this.fired || this.clicks.length < this.threshold) return 0;
|
|
610
|
-
this.fired = true;
|
|
611
|
-
return this.clicks.length;
|
|
1194
|
+
return null;
|
|
612
1195
|
}
|
|
613
|
-
|
|
1196
|
+
if (token && saved?.token !== token) sessionStore.remove(KEYS.capturePending);
|
|
1197
|
+
writeJson(sessionStore, KEYS.capture, marker);
|
|
1198
|
+
sessionStore.remove(KEYS.authoring);
|
|
1199
|
+
return marker;
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
// src/load-capture.ts
|
|
1203
|
+
var loadCapture = async () => (await import("./capture.cjs")).capture;
|
|
614
1204
|
|
|
615
1205
|
// src/context.ts
|
|
616
1206
|
function detectDevice(ua, width) {
|
|
@@ -621,7 +1211,8 @@ function detectDevice(ua, width) {
|
|
|
621
1211
|
else if (width > 0 && width < 768) type = "mobile";
|
|
622
1212
|
else if (width > 0 && width < 1024) type = "tablet";
|
|
623
1213
|
let browser;
|
|
624
|
-
if (/
|
|
1214
|
+
if (/Electron\//i.test(u)) browser = "electron";
|
|
1215
|
+
else if (/Edg\//i.test(u)) browser = "edge";
|
|
625
1216
|
else if (/OPR\/|Opera/i.test(u)) browser = "opera";
|
|
626
1217
|
else if (/SamsungBrowser/i.test(u)) browser = "samsung";
|
|
627
1218
|
else if (/Chrome\/|CriOS/i.test(u)) browser = "chrome";
|
|
@@ -745,7 +1336,7 @@ var HeatmapBuffer = class {
|
|
|
745
1336
|
};
|
|
746
1337
|
|
|
747
1338
|
// src/errors.ts
|
|
748
|
-
var
|
|
1339
|
+
var import_runtime11 = require("@guidelytech/shared/runtime");
|
|
749
1340
|
var MAX_FRAMES_BYTES = 6e3;
|
|
750
1341
|
function fitFrames(frames) {
|
|
751
1342
|
const out = frames.slice(0, 30);
|
|
@@ -766,7 +1357,7 @@ function exceptionFromError(error, source, handled, fallbackType = "Error") {
|
|
|
766
1357
|
return {
|
|
767
1358
|
type: isError && e.name || fallbackType,
|
|
768
1359
|
message: (isError ? e.message : describe(error)).slice(0, 1e3),
|
|
769
|
-
frames: isError ? fitFrames((0,
|
|
1360
|
+
frames: isError ? fitFrames((0, import_runtime11.parseStack)(e.stack)) : [],
|
|
770
1361
|
source,
|
|
771
1362
|
handled
|
|
772
1363
|
};
|
|
@@ -887,162 +1478,35 @@ function areMutuallyExclusive(a, b) {
|
|
|
887
1478
|
return la.includes(b.id) || la.includes(b.key) || lb.includes(a.id) || lb.includes(a.key);
|
|
888
1479
|
}
|
|
889
1480
|
function isWithinSchedule(guide, now = Date.now()) {
|
|
890
|
-
const
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
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;
|
|
1485
|
+
}
|
|
894
1486
|
return true;
|
|
895
1487
|
}
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
return
|
|
899
|
-
}
|
|
900
|
-
|
|
901
|
-
// src/http.ts
|
|
902
|
-
var Api = class {
|
|
903
|
-
constructor(baseUrl, key, fetchImpl) {
|
|
904
|
-
__publicField(this, "baseUrl", baseUrl);
|
|
905
|
-
__publicField(this, "key", key);
|
|
906
|
-
__publicField(this, "fetchImpl", fetchImpl);
|
|
907
|
-
}
|
|
908
|
-
url(path, params = {}) {
|
|
909
|
-
const query = Object.entries({ key: this.key, ...params }).filter((e) => e[1] != null).map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join("&");
|
|
910
|
-
return `${this.baseUrl.replace(/\/$/, "")}/api/v1/sdk${path}?${query}`;
|
|
911
|
-
}
|
|
912
|
-
get fetcher() {
|
|
913
|
-
if (this.fetchImpl) return this.fetchImpl;
|
|
914
|
-
return typeof fetch === "function" ? fetch.bind(globalThis) : null;
|
|
915
|
-
}
|
|
916
|
-
async request(method, path, options = {}) {
|
|
917
|
-
const f = this.fetcher;
|
|
918
|
-
if (!f) throw new Error("fetch is not available");
|
|
919
|
-
const headers = { ...options.headers, "X-Guidely-Key": this.key, Accept: "application/json" };
|
|
920
|
-
const form = typeof FormData !== "undefined" && options.body instanceof FormData;
|
|
921
|
-
if (options.body !== void 0 && !form) headers["Content-Type"] = "application/json";
|
|
922
|
-
if (options.etag) headers["If-None-Match"] = options.etag;
|
|
923
|
-
const ac = typeof AbortController !== "undefined" ? new AbortController() : null;
|
|
924
|
-
const timer = ac ? setTimeout(() => ac.abort(), options.timeoutMs ?? 1e4) : null;
|
|
925
|
-
try {
|
|
926
|
-
const res = await f(this.url(path, options.params), {
|
|
927
|
-
method,
|
|
928
|
-
headers,
|
|
929
|
-
body: options.body === void 0 ? void 0 : form ? options.body : JSON.stringify(options.body),
|
|
930
|
-
keepalive: options.keepalive,
|
|
931
|
-
signal: ac?.signal,
|
|
932
|
-
credentials: "omit",
|
|
933
|
-
mode: "cors"
|
|
934
|
-
});
|
|
935
|
-
let data = null;
|
|
936
|
-
if (res.status !== 304 && res.status !== 204) {
|
|
937
|
-
try {
|
|
938
|
-
data = await res.json();
|
|
939
|
-
} catch {
|
|
940
|
-
data = null;
|
|
941
|
-
}
|
|
942
|
-
}
|
|
943
|
-
return { status: res.status, ok: res.ok, data, etag: res.headers?.get?.("etag") ?? null };
|
|
944
|
-
} finally {
|
|
945
|
-
if (timer) clearTimeout(timer);
|
|
946
|
-
}
|
|
947
|
-
}
|
|
948
|
-
get(path, options) {
|
|
949
|
-
return this.request("GET", path, options);
|
|
950
|
-
}
|
|
951
|
-
post(path, body, options) {
|
|
952
|
-
return this.request("POST", path, { body, ...options });
|
|
953
|
-
}
|
|
954
|
-
/** Multipart POST (file uploads). No Content-Type header: the browser adds it with the boundary. */
|
|
955
|
-
postForm(path, form, options) {
|
|
956
|
-
return this.request("POST", path, { body: form, ...options });
|
|
957
|
-
}
|
|
958
|
-
/**
|
|
959
|
-
* POST that may answer with server-sent events. Each event is passed to `onEvent`; a JSON answer is
|
|
960
|
-
* returned instead. No timeout: Scout replies can take a while and the server keeps the stream alive.
|
|
961
|
-
*/
|
|
962
|
-
async stream(path, body, onEvent, headers = {}) {
|
|
963
|
-
const f = this.fetcher;
|
|
964
|
-
if (!f) throw new Error("fetch is not available");
|
|
965
|
-
const res = await f(this.url(path), {
|
|
966
|
-
method: "POST",
|
|
967
|
-
headers: { ...headers, "X-Guidely-Key": this.key, Accept: "text/event-stream, application/json", "Content-Type": "application/json" },
|
|
968
|
-
body: JSON.stringify(body),
|
|
969
|
-
credentials: "omit",
|
|
970
|
-
mode: "cors"
|
|
971
|
-
});
|
|
972
|
-
const type = res.headers?.get?.("content-type") ?? "";
|
|
973
|
-
if (!type.includes("text/event-stream") || !res.body) {
|
|
974
|
-
let data = null;
|
|
975
|
-
try {
|
|
976
|
-
data = await res.json();
|
|
977
|
-
} catch {
|
|
978
|
-
data = null;
|
|
979
|
-
}
|
|
980
|
-
return { status: res.status, ok: res.ok, data, etag: null };
|
|
981
|
-
}
|
|
982
|
-
await readEventStream(res.body, onEvent);
|
|
983
|
-
return { status: res.status, ok: res.ok, data: null, etag: null };
|
|
984
|
-
}
|
|
985
|
-
/** `navigator.sendBeacon` when available (page unload); returns false when it could not be queued. */
|
|
986
|
-
beacon(path, body) {
|
|
987
|
-
try {
|
|
988
|
-
if (typeof navigator === "undefined" || typeof navigator.sendBeacon !== "function") return false;
|
|
989
|
-
const blob = new Blob([JSON.stringify(body)], { type: "text/plain;charset=UTF-8" });
|
|
990
|
-
return navigator.sendBeacon(this.url(path), blob);
|
|
991
|
-
} catch {
|
|
992
|
-
return false;
|
|
993
|
-
}
|
|
994
|
-
}
|
|
995
|
-
};
|
|
996
|
-
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
997
|
-
function backoffMs(attempt, baseMs = 1e3, maxMs = 3e4) {
|
|
998
|
-
return Math.min(maxMs, baseMs * 2 ** Math.max(0, attempt));
|
|
1488
|
+
var MINUTE = 60 * 1e3;
|
|
1489
|
+
function countsTowardThrottle(guide) {
|
|
1490
|
+
return isExclusiveGuide(guide) && !guide.config.frequency.ignoreThrottling;
|
|
999
1491
|
}
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
if (i < attempts - 1) await sleep(backoffMs(i, baseMs, 8e3));
|
|
1008
|
-
}
|
|
1009
|
-
return null;
|
|
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;
|
|
1010
1499
|
}
|
|
1011
|
-
function
|
|
1012
|
-
|
|
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);
|
|
1013
1506
|
}
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
let buffer = "";
|
|
1018
|
-
const dispatch = (raw) => {
|
|
1019
|
-
let event = "message";
|
|
1020
|
-
const data = [];
|
|
1021
|
-
for (const line of raw.split("\n")) {
|
|
1022
|
-
if (!line || line.startsWith(":")) continue;
|
|
1023
|
-
const i = line.indexOf(":");
|
|
1024
|
-
const field = i < 0 ? line : line.slice(0, i);
|
|
1025
|
-
const value = i < 0 ? "" : line.slice(i + 1).replace(/^ /, "");
|
|
1026
|
-
if (field === "event") event = value;
|
|
1027
|
-
else if (field === "data") data.push(value);
|
|
1028
|
-
}
|
|
1029
|
-
if (!data.length) return;
|
|
1030
|
-
try {
|
|
1031
|
-
onEvent(event, JSON.parse(data.join("\n")));
|
|
1032
|
-
} catch {
|
|
1033
|
-
}
|
|
1034
|
-
};
|
|
1035
|
-
for (; ; ) {
|
|
1036
|
-
const { done, value } = await reader.read();
|
|
1037
|
-
if (value) buffer += decoder.decode(value, { stream: true }).replace(/\r\n?/g, "\n");
|
|
1038
|
-
let sep;
|
|
1039
|
-
while ((sep = buffer.indexOf("\n\n")) >= 0) {
|
|
1040
|
-
dispatch(buffer.slice(0, sep));
|
|
1041
|
-
buffer = buffer.slice(sep + 2);
|
|
1042
|
-
}
|
|
1043
|
-
if (done) break;
|
|
1044
|
-
}
|
|
1045
|
-
if (buffer.trim()) dispatch(buffer);
|
|
1507
|
+
function dismissedAfterMerge(local, server) {
|
|
1508
|
+
if (server.dismissResetAt && server.dismissResetAt !== local?.dismissResetAt) return server.dismissedAt ?? null;
|
|
1509
|
+
return local?.dismissedAt ?? server.dismissedAt ?? null;
|
|
1046
1510
|
}
|
|
1047
1511
|
|
|
1048
1512
|
// src/messenger-api.ts
|
|
@@ -1116,6 +1580,14 @@ function createMessengerTransport(opts) {
|
|
|
1116
1580
|
h.onDone(d.messages.at(-1) ?? null, d.conversation);
|
|
1117
1581
|
} else if (!finished) h.onError("The reply was interrupted");
|
|
1118
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
|
+
}
|
|
1119
1591
|
return {
|
|
1120
1592
|
async listConversations() {
|
|
1121
1593
|
const tokens = storedConversations(store, projectId).map((c) => c.token);
|
|
@@ -1152,7 +1624,8 @@ function createMessengerTransport(opts) {
|
|
|
1152
1624
|
return res.data;
|
|
1153
1625
|
},
|
|
1154
1626
|
async getArticle(id) {
|
|
1155
|
-
const
|
|
1627
|
+
const locale = opts.locale?.();
|
|
1628
|
+
const res = await api.get(`/articles/${encodeURIComponent(id)}`, locale ? { params: { locale } } : void 0);
|
|
1156
1629
|
return res.ok && res.data ? res.data.article : null;
|
|
1157
1630
|
},
|
|
1158
1631
|
async fileComplaint(input) {
|
|
@@ -1182,13 +1655,25 @@ function createMessengerTransport(opts) {
|
|
|
1182
1655
|
async getHowTo(id) {
|
|
1183
1656
|
const res = await api.get(`/how-tos/${encodeURIComponent(id)}`);
|
|
1184
1657
|
return res.ok && res.data ? res.data.howTo : null;
|
|
1185
|
-
}
|
|
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
|
|
1186
1671
|
};
|
|
1187
1672
|
}
|
|
1188
1673
|
|
|
1189
1674
|
// src/messenger-visitor.ts
|
|
1190
|
-
var
|
|
1191
|
-
var
|
|
1675
|
+
var import_runtime15 = require("@guidelytech/shared/runtime");
|
|
1676
|
+
var import_guide_runtime4 = require("@guidelytech/guide-runtime");
|
|
1192
1677
|
var PAGE_GUIDE_KINDS = /* @__PURE__ */ new Set(["flow", "tooltip", "modal"]);
|
|
1193
1678
|
var CATCH_ALL = /* @__PURE__ */ new Set(["", "*", "**", "/*", "/**", "/"]);
|
|
1194
1679
|
var RESUME_WITHIN_MS = 14 * 864e5;
|
|
@@ -1205,7 +1690,7 @@ function targetsPage(guide, url) {
|
|
|
1205
1690
|
const c = guide.config;
|
|
1206
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] : []];
|
|
1207
1692
|
const specific = checks.filter((p) => !CATCH_ALL.has(p.trim()));
|
|
1208
|
-
return specific.length > 0 && checks.every((p) => (0,
|
|
1693
|
+
return specific.length > 0 && checks.every((p) => (0, import_guide_runtime4.urlMatches)(p, url));
|
|
1209
1694
|
}
|
|
1210
1695
|
function plainText(html, max = 280) {
|
|
1211
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();
|
|
@@ -1254,7 +1739,7 @@ function buildMessengerVisitor(input) {
|
|
|
1254
1739
|
unread: !seen.has(g.id) && !(state.guides[g.id]?.impressions ?? 0)
|
|
1255
1740
|
};
|
|
1256
1741
|
});
|
|
1257
|
-
return { firstName: (0,
|
|
1742
|
+
return { firstName: (0, import_runtime15.firstNameFromTraits)(input.traits), path: pathOf(input.url), pageGuides, resumeGuide, checklist, news };
|
|
1258
1743
|
}
|
|
1259
1744
|
|
|
1260
1745
|
// src/navigation.ts
|
|
@@ -1312,6 +1797,48 @@ function watchNavigation(listener, debounceMs = 50) {
|
|
|
1312
1797
|
};
|
|
1313
1798
|
}
|
|
1314
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
|
+
|
|
1315
1842
|
// src/page-tracking.ts
|
|
1316
1843
|
function scrollDepth() {
|
|
1317
1844
|
const de = document.documentElement;
|
|
@@ -1357,6 +1884,106 @@ var PageTracker = class {
|
|
|
1357
1884
|
}
|
|
1358
1885
|
};
|
|
1359
1886
|
|
|
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;
|
|
1898
|
+
}
|
|
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;
|
|
1905
|
+
}
|
|
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;
|
|
1952
|
+
try {
|
|
1953
|
+
previous?.focus?.();
|
|
1954
|
+
} catch {
|
|
1955
|
+
}
|
|
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
|
+
}
|
|
1986
|
+
|
|
1360
1987
|
// src/queue.ts
|
|
1361
1988
|
var EventQueue = class {
|
|
1362
1989
|
constructor(opts) {
|
|
@@ -1368,17 +1995,34 @@ var EventQueue = class {
|
|
|
1368
1995
|
__publicField(this, "inflightIds", /* @__PURE__ */ new Set());
|
|
1369
1996
|
__publicField(this, "attempt", 0);
|
|
1370
1997
|
__publicField(this, "destroyed", false);
|
|
1998
|
+
__publicField(this, "offRecover", null);
|
|
1371
1999
|
__publicField(this, "flushIntervalMs");
|
|
1372
2000
|
__publicField(this, "maxQueue");
|
|
1373
2001
|
__publicField(this, "batchSize");
|
|
1374
2002
|
__publicField(this, "maxAttempts");
|
|
1375
2003
|
this.flushIntervalMs = opts.flushIntervalMs ?? 2e3;
|
|
1376
|
-
this.maxQueue = opts.maxQueue ??
|
|
2004
|
+
this.maxQueue = opts.maxQueue ?? 500;
|
|
1377
2005
|
this.batchSize = opts.batchSize ?? 50;
|
|
1378
2006
|
this.maxAttempts = opts.maxAttempts ?? 5;
|
|
1379
2007
|
const persisted = readJson(opts.store, opts.storageKey, []);
|
|
1380
2008
|
if (Array.isArray(persisted)) this.events = persisted.filter((e) => e && typeof e.eventId === "string").slice(-this.maxQueue);
|
|
1381
2009
|
if (this.events.length) this.schedule();
|
|
2010
|
+
this.offRecover = opts.health?.onRecover(() => this.retryNow()) ?? null;
|
|
2011
|
+
}
|
|
2012
|
+
/** The API is back: drop any pending backoff and send what is queued. */
|
|
2013
|
+
retryNow() {
|
|
2014
|
+
if (this.destroyed) return;
|
|
2015
|
+
if (this.retryTimer) clearTimeout(this.retryTimer);
|
|
2016
|
+
this.retryTimer = null;
|
|
2017
|
+
this.attempt = 0;
|
|
2018
|
+
void this.flush();
|
|
2019
|
+
}
|
|
2020
|
+
retryIn(ms) {
|
|
2021
|
+
if (this.retryTimer || this.destroyed) return;
|
|
2022
|
+
this.retryTimer = setTimeout(() => {
|
|
2023
|
+
this.retryTimer = null;
|
|
2024
|
+
void this.flush();
|
|
2025
|
+
}, ms);
|
|
1382
2026
|
}
|
|
1383
2027
|
get size() {
|
|
1384
2028
|
return this.events.length;
|
|
@@ -1412,6 +2056,8 @@ var EventQueue = class {
|
|
|
1412
2056
|
clearTimeout(this.timer);
|
|
1413
2057
|
this.timer = null;
|
|
1414
2058
|
}
|
|
2059
|
+
const wait = this.opts.health?.waitMs() ?? 0;
|
|
2060
|
+
if (wait > 0) return this.retryIn(wait);
|
|
1415
2061
|
this.inflight = true;
|
|
1416
2062
|
const batch = this.events.slice(0, this.batchSize);
|
|
1417
2063
|
this.inflightIds = new Set(batch.map((e) => e.eventId));
|
|
@@ -1435,26 +2081,20 @@ var EventQueue = class {
|
|
|
1435
2081
|
if (this.events.length) this.schedule();
|
|
1436
2082
|
return;
|
|
1437
2083
|
}
|
|
1438
|
-
this.attempt
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
return;
|
|
1442
|
-
}
|
|
1443
|
-
this.retryTimer = setTimeout(() => {
|
|
1444
|
-
this.retryTimer = null;
|
|
1445
|
-
void this.flush();
|
|
1446
|
-
}, backoffMs(this.attempt - 1));
|
|
2084
|
+
this.attempt = Math.min(this.attempt + 1, this.maxAttempts);
|
|
2085
|
+
const delay = this.attempt >= this.maxAttempts ? 6e4 : backoffMs(this.attempt - 1);
|
|
2086
|
+
this.retryIn(Math.max(delay, this.opts.health?.waitMs() ?? 0));
|
|
1447
2087
|
}
|
|
1448
2088
|
/** Best-effort synchronous flush on unload using `sendBeacon`. */
|
|
1449
2089
|
flushSync() {
|
|
1450
2090
|
if (this.destroyed || !this.opts.enabled() || !this.opts.beacon) return;
|
|
1451
2091
|
const batch = this.events.filter((e) => !this.inflightIds.has(e.eventId)).slice(0, this.batchSize);
|
|
1452
2092
|
if (!batch.length) return;
|
|
1453
|
-
if (this.opts.beacon(batch)) {
|
|
2093
|
+
if (this.opts.beacon(batch) && (this.opts.health?.reachable ?? true)) {
|
|
1454
2094
|
const ids = new Set(batch.map((e) => e.eventId));
|
|
1455
2095
|
this.events = this.events.filter((e) => !ids.has(e.eventId));
|
|
1456
|
-
this.persist();
|
|
1457
2096
|
}
|
|
2097
|
+
this.persist();
|
|
1458
2098
|
}
|
|
1459
2099
|
clear() {
|
|
1460
2100
|
this.events = [];
|
|
@@ -1462,6 +2102,7 @@ var EventQueue = class {
|
|
|
1462
2102
|
}
|
|
1463
2103
|
destroy() {
|
|
1464
2104
|
this.destroyed = true;
|
|
2105
|
+
this.offRecover?.();
|
|
1465
2106
|
if (this.timer) clearTimeout(this.timer);
|
|
1466
2107
|
if (this.retryTimer) clearTimeout(this.retryTimer);
|
|
1467
2108
|
this.timer = null;
|
|
@@ -1475,11 +2116,22 @@ var loadRecorder = async () => (await import("./recorder.cjs")).recorder;
|
|
|
1475
2116
|
|
|
1476
2117
|
// src/client.ts
|
|
1477
2118
|
var SDK_NAME = "@guidelytech/sdk-core";
|
|
1478
|
-
var SDK_VERSION = "0.
|
|
2119
|
+
var SDK_VERSION = "0.11.0";
|
|
1479
2120
|
var DEFAULT_API_URL = "https://api.guidely.app";
|
|
1480
2121
|
var PAGE_SCOPED = /* @__PURE__ */ new Set(["page_load", "url_match", "element_click", "element_visible", "delay"]);
|
|
1481
2122
|
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
1482
2123
|
var MAX_SESSION_EXCEPTIONS = 100;
|
|
2124
|
+
var CACHED_START_MS = 1500;
|
|
2125
|
+
function settlesWithin(promise, ms) {
|
|
2126
|
+
return new Promise((resolve) => {
|
|
2127
|
+
const timer = setTimeout(() => resolve(false), ms);
|
|
2128
|
+
const done = () => {
|
|
2129
|
+
clearTimeout(timer);
|
|
2130
|
+
resolve(true);
|
|
2131
|
+
};
|
|
2132
|
+
promise.then(done, done);
|
|
2133
|
+
});
|
|
2134
|
+
}
|
|
1483
2135
|
function isBrowser() {
|
|
1484
2136
|
return typeof window !== "undefined" && typeof document !== "undefined";
|
|
1485
2137
|
}
|
|
@@ -1490,7 +2142,7 @@ function emptyState() {
|
|
|
1490
2142
|
return { guides: {}, checklists: {}, surveysSubmitted: [], audienceIds: [] };
|
|
1491
2143
|
}
|
|
1492
2144
|
function newSession(now = Date.now()) {
|
|
1493
|
-
return { id: `s_${(0,
|
|
2145
|
+
return { id: `s_${(0, import_runtime18.randomId)(16)}`, startedAt: new Date(now).toISOString(), shownGuides: [], events: [], lastActivityAt: now };
|
|
1494
2146
|
}
|
|
1495
2147
|
var GuidelyClient = class _GuidelyClient {
|
|
1496
2148
|
constructor(options) {
|
|
@@ -1500,6 +2152,10 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
1500
2152
|
__publicField(this, "sessionStore");
|
|
1501
2153
|
__publicField(this, "emitter");
|
|
1502
2154
|
__publicField(this, "queue");
|
|
2155
|
+
/** Survey answers and guide/checklist progress, kept until the server accepts them. */
|
|
2156
|
+
__publicField(this, "outbox");
|
|
2157
|
+
/** Agent Analytics turns (agentConversation), in their own outbox. */
|
|
2158
|
+
__publicField(this, "agentTurns");
|
|
1503
2159
|
__publicField(this, "config", null);
|
|
1504
2160
|
__publicField(this, "etag", null);
|
|
1505
2161
|
__publicField(this, "anonymousId");
|
|
@@ -1549,6 +2205,10 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
1549
2205
|
__publicField(this, "flags");
|
|
1550
2206
|
/** Experiments already reported this session (`experiment_viewed` is sent once per session). */
|
|
1551
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());
|
|
1552
2212
|
__publicField(this, "customListeners", /* @__PURE__ */ new Map());
|
|
1553
2213
|
__publicField(this, "eventBus", {
|
|
1554
2214
|
on: (name, cb) => {
|
|
@@ -1583,7 +2243,7 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
1583
2243
|
this.sessionStore = this.options.storage === "memory" ? this.store : createStorage("session");
|
|
1584
2244
|
this.anonymousId = this.store.get(KEYS.anon) || "";
|
|
1585
2245
|
if (!this.anonymousId) {
|
|
1586
|
-
this.anonymousId = `anon_${(0,
|
|
2246
|
+
this.anonymousId = `anon_${(0, import_runtime18.randomId)(20)}`;
|
|
1587
2247
|
this.store.set(KEYS.anon, this.anonymousId);
|
|
1588
2248
|
}
|
|
1589
2249
|
this.firstVisit = !this.store.get(KEYS.seen);
|
|
@@ -1593,12 +2253,13 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
1593
2253
|
const session = readJson(this.sessionStore, KEYS.session, null);
|
|
1594
2254
|
this.session = session && session.id ? session : newSession();
|
|
1595
2255
|
this.persistSession();
|
|
1596
|
-
this.lastUrl =
|
|
2256
|
+
this.lastUrl = this.pageHref();
|
|
1597
2257
|
this.lastAudiences = new Set(this.state.audienceIds);
|
|
1598
2258
|
this.queue = new EventQueue({
|
|
1599
2259
|
store: this.store,
|
|
1600
2260
|
storageKey: KEYS.queue(this.options.projectId),
|
|
1601
2261
|
enabled: () => this.analyticsEnabled(),
|
|
2262
|
+
health: this.api.health,
|
|
1602
2263
|
send: async (events) => {
|
|
1603
2264
|
const res = await this.api.post("/events", { events, sentAt: nowIso(), sdk: { name: SDK_NAME, version: SDK_VERSION } }, { keepalive: true });
|
|
1604
2265
|
return { ok: res.ok, retry: !(res.status >= 400 && res.status < 500 && res.status !== 429) };
|
|
@@ -1606,6 +2267,8 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
1606
2267
|
beacon: (events) => this.api.beacon("/events", { events, sentAt: nowIso(), sdk: { name: SDK_NAME, version: SDK_VERSION } }),
|
|
1607
2268
|
onError: (err) => this.reportError(err)
|
|
1608
2269
|
});
|
|
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 } });
|
|
1609
2272
|
if (options.user) this.user = { ...options.user };
|
|
1610
2273
|
this.flags = this.createFlags(options.flags);
|
|
1611
2274
|
}
|
|
@@ -1663,12 +2326,15 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
1663
2326
|
const cached = readJson(this.store, KEYS.config(this.options.projectId), null);
|
|
1664
2327
|
this.etag = this.store.get(KEYS.etag(this.options.projectId));
|
|
1665
2328
|
if (cached && cached.guides) this.config = webOnly(cached);
|
|
1666
|
-
|
|
2329
|
+
const before = this.config?.configVersion;
|
|
2330
|
+
const fresh = this.fetchConfig();
|
|
2331
|
+
if (!this.config) await fresh;
|
|
2332
|
+
else if (!await settlesWithin(fresh, CACHED_START_MS)) void fresh.then(() => this.applyConfigChange(before));
|
|
1667
2333
|
if (this.destroyed) return;
|
|
1668
2334
|
this.wireListeners();
|
|
1669
2335
|
if (!this.identitySettled) {
|
|
1670
|
-
|
|
1671
|
-
|
|
2336
|
+
const known = this.options.user ?? (this.user?.id ? this.user : null);
|
|
2337
|
+
if (known) await settlesWithin(this.identify(known.id, known.traits, known.account), CACHED_START_MS);
|
|
1672
2338
|
}
|
|
1673
2339
|
if (this.destroyed) return;
|
|
1674
2340
|
this.isReady = true;
|
|
@@ -1679,7 +2345,7 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
1679
2345
|
this.resumeActiveGuide();
|
|
1680
2346
|
this.evaluate("ready");
|
|
1681
2347
|
if (!this.options.disableAuthoring && !capturing) this.maybeStartAuthoring();
|
|
1682
|
-
this.pageTracker.start(
|
|
2348
|
+
this.pageTracker.start(this.pageHref());
|
|
1683
2349
|
this.touchSession();
|
|
1684
2350
|
this.syncCapture();
|
|
1685
2351
|
} catch (err) {
|
|
@@ -1712,7 +2378,7 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
1712
2378
|
wireListeners() {
|
|
1713
2379
|
const doc = document;
|
|
1714
2380
|
const win = window;
|
|
1715
|
-
this.cleanups.push(watchNavigation((url) => this.handleNavigation(url)));
|
|
2381
|
+
this.cleanups.push(watchNavigation((url) => this.handleNavigation(this.pageHref(url))));
|
|
1716
2382
|
const onClick = (e) => this.handleDocumentClick(e);
|
|
1717
2383
|
doc.addEventListener("click", onClick, true);
|
|
1718
2384
|
this.cleanups.push(() => doc.removeEventListener("click", onClick, true));
|
|
@@ -1733,7 +2399,7 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
1733
2399
|
const onPageShow = (e) => {
|
|
1734
2400
|
if (!e.persisted || !this.pageLeft) return;
|
|
1735
2401
|
this.pageLeft = false;
|
|
1736
|
-
this.pageTracker.start(
|
|
2402
|
+
this.pageTracker.start(this.pageHref());
|
|
1737
2403
|
};
|
|
1738
2404
|
let lastScroll = 0;
|
|
1739
2405
|
const onScroll = () => {
|
|
@@ -1773,11 +2439,16 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
1773
2439
|
win.removeEventListener("scroll", onScroll);
|
|
1774
2440
|
doc.removeEventListener("keydown", onKey, true);
|
|
1775
2441
|
});
|
|
1776
|
-
const refresh = setInterval(() =>
|
|
2442
|
+
const refresh = setInterval(() => {
|
|
2443
|
+
if (this.api.health.canSend()) void this.refresh();
|
|
2444
|
+
}, this.options.configRefreshMs);
|
|
2445
|
+
const onOnline = () => this.api.health.online();
|
|
2446
|
+
win.addEventListener("online", onOnline);
|
|
2447
|
+
this.cleanups.push(() => win.removeEventListener("online", onOnline));
|
|
1777
2448
|
this.cleanups.push(() => clearInterval(refresh));
|
|
1778
2449
|
const poll = setInterval(() => {
|
|
1779
2450
|
if (!this.isReady || !this.config || this.editing() || doc.visibilityState === "hidden") return;
|
|
1780
|
-
const url =
|
|
2451
|
+
const url = this.pageHref();
|
|
1781
2452
|
const ctx = this.getContext();
|
|
1782
2453
|
for (const guide of this.config.guides) {
|
|
1783
2454
|
if (guide.config.trigger.type === "element_visible" && guide.config.trigger.value) this.armVisibility(guide, ctx, url);
|
|
@@ -1806,6 +2477,11 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
1806
2477
|
if (this.destroyed) return;
|
|
1807
2478
|
const before = this.config?.configVersion;
|
|
1808
2479
|
await this.fetchConfig();
|
|
2480
|
+
this.applyConfigChange(before);
|
|
2481
|
+
}
|
|
2482
|
+
/** Re-renders after a config change (refresh, or a config that arrived after start). */
|
|
2483
|
+
applyConfigChange(before) {
|
|
2484
|
+
if (this.destroyed || !this.isReady) return;
|
|
1809
2485
|
if (this.config && this.config.configVersion !== before) {
|
|
1810
2486
|
this.renderPersistentWidgets();
|
|
1811
2487
|
this.evaluate("ready");
|
|
@@ -1840,10 +2516,12 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
1840
2516
|
this.replay = null;
|
|
1841
2517
|
this.queue.flushSync();
|
|
1842
2518
|
this.queue.destroy();
|
|
2519
|
+
this.outbox.destroy();
|
|
2520
|
+
this.agentTurns.destroy();
|
|
1843
2521
|
this.flags.destroy();
|
|
1844
2522
|
this.emitter.clear();
|
|
1845
|
-
(0,
|
|
1846
|
-
if (
|
|
2523
|
+
(0, import_guide_runtime5.destroyHost)();
|
|
2524
|
+
if (current2 === this) current2 = null;
|
|
1847
2525
|
}
|
|
1848
2526
|
// ---------------------------------------------------------------------------
|
|
1849
2527
|
// Identity
|
|
@@ -1855,13 +2533,13 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
1855
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) };
|
|
1856
2534
|
writeJson(this.store, KEYS.user(this.options.projectId), this.user);
|
|
1857
2535
|
this.syncFlagAttributes();
|
|
1858
|
-
this.enqueue("identify", { properties: { traits: (0,
|
|
2536
|
+
this.enqueue("identify", { properties: { traits: (0, import_runtime18.sanitizeProperties)(this.user.traits) } });
|
|
1859
2537
|
await this.syncIdentify();
|
|
1860
2538
|
this.emitter.emit("identify", { userId: this.user.id, anonymousId: this.anonymousId });
|
|
1861
2539
|
if (this.isReady) this.evaluate("identify");
|
|
1862
2540
|
}
|
|
1863
2541
|
async syncIdentify() {
|
|
1864
|
-
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 };
|
|
1865
2543
|
const res = await postWithRetry(this.api, "/identify", body, 2);
|
|
1866
2544
|
if (res?.ok && res.data) this.mergeServerState(res.data);
|
|
1867
2545
|
}
|
|
@@ -1892,7 +2570,10 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
1892
2570
|
checklists,
|
|
1893
2571
|
surveysSubmitted: Array.from(/* @__PURE__ */ new Set([...this.state.surveysSubmitted, ...data.surveysSubmitted || []])),
|
|
1894
2572
|
audienceIds: data.audienceIds || [],
|
|
1895
|
-
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 } : {}
|
|
1896
2577
|
};
|
|
1897
2578
|
if (data.firstSeenAt && Date.now() - Date.parse(data.firstSeenAt) > 6e4) this.firstVisit = false;
|
|
1898
2579
|
this.persistState();
|
|
@@ -1928,7 +2609,7 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
1928
2609
|
this.stopGuide();
|
|
1929
2610
|
this.user = null;
|
|
1930
2611
|
this.store.remove(KEYS.user(this.options.projectId));
|
|
1931
|
-
this.anonymousId = `anon_${(0,
|
|
2612
|
+
this.anonymousId = `anon_${(0, import_runtime18.randomId)(20)}`;
|
|
1932
2613
|
this.store.set(KEYS.anon, this.anonymousId);
|
|
1933
2614
|
this.state = emptyState();
|
|
1934
2615
|
this.persistState();
|
|
@@ -1960,6 +2641,7 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
1960
2641
|
if (consent === "denied") this.queue.clear();
|
|
1961
2642
|
else if (consent === "granted") void this.queue.flush();
|
|
1962
2643
|
if (consent !== "granted") this.replay?.stop({ discard: true });
|
|
2644
|
+
if (consent !== "pending") this.agentTurns.consentChanged(consent === "granted");
|
|
1963
2645
|
this.syncCapture();
|
|
1964
2646
|
}
|
|
1965
2647
|
analyticsEnabled() {
|
|
@@ -1972,17 +2654,19 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
1972
2654
|
if (this.consent === "denied" || !this.options.analytics) return null;
|
|
1973
2655
|
if (name !== "page_leave" && name !== "heatmap") this.touchSession();
|
|
1974
2656
|
const event = {
|
|
1975
|
-
eventId: (0,
|
|
2657
|
+
eventId: (0, import_runtime18.randomId)(24),
|
|
1976
2658
|
name,
|
|
1977
2659
|
occurredAt: nowIso(),
|
|
1978
2660
|
anonymousId: this.anonymousId,
|
|
1979
2661
|
userId: this.user?.id || null,
|
|
1980
2662
|
accountId: this.user?.account?.id || null,
|
|
1981
2663
|
sessionId: this.session.id,
|
|
1982
|
-
url: this.collectUrls() ?
|
|
2664
|
+
url: this.collectUrls() ? this.pageHref() : void 0,
|
|
1983
2665
|
...extra
|
|
1984
2666
|
};
|
|
1985
|
-
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 };
|
|
1986
2670
|
this.queue.enqueue(event);
|
|
1987
2671
|
this.replay?.addCustomEvent("guidely", { name, customName: event.customName, guideId: event.guideId });
|
|
1988
2672
|
this.emitter.emit("track", event);
|
|
@@ -2006,9 +2690,30 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2006
2690
|
this.completeChecklistTasksBy("event", name);
|
|
2007
2691
|
if (this.isReady) this.evaluate("event", { eventName: name });
|
|
2008
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
|
+
}
|
|
2009
2713
|
/** Records a page view and re-evaluates triggers. Called automatically on SPA navigation. */
|
|
2010
|
-
page(
|
|
2714
|
+
page(href) {
|
|
2011
2715
|
if (this.destroyed) return;
|
|
2716
|
+
const url = this.pageHref(href);
|
|
2012
2717
|
this.leavePage(url);
|
|
2013
2718
|
this.trackPageView(url);
|
|
2014
2719
|
this.handleNavigation(url, true);
|
|
@@ -2028,13 +2733,13 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2028
2733
|
const st = this.active.controller.getState();
|
|
2029
2734
|
if (st.status === "running" && !this.active.manual) {
|
|
2030
2735
|
const g = this.active.guide;
|
|
2031
|
-
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();
|
|
2032
2737
|
}
|
|
2033
2738
|
}
|
|
2034
2739
|
this.widgets.forEach((w, id) => {
|
|
2035
2740
|
if (!w.controller || w.manual) return;
|
|
2036
2741
|
const g = this.findGuide(id);
|
|
2037
|
-
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();
|
|
2038
2743
|
});
|
|
2039
2744
|
this.firedDelays.clear();
|
|
2040
2745
|
if (this.isReady) this.evaluate("navigation");
|
|
@@ -2042,7 +2747,7 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2042
2747
|
if (changed) void this.queue.flush();
|
|
2043
2748
|
}
|
|
2044
2749
|
handleDocumentClick(e) {
|
|
2045
|
-
const target = e
|
|
2750
|
+
const target = (0, import_guide_runtime5.composedTarget)(e);
|
|
2046
2751
|
if (!target || typeof target.closest !== "function") return;
|
|
2047
2752
|
this.autocaptureClick(e, target);
|
|
2048
2753
|
this.completeChecklistTasksBy("click", target);
|
|
@@ -2100,6 +2805,8 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2100
2805
|
const ref = !this.session.pageViews ? externalReferrer() : void 0;
|
|
2101
2806
|
if (ref) props.referrer = ref;
|
|
2102
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;
|
|
2103
2810
|
this.session.pageViews = (this.session.pageViews ?? 0) + 1;
|
|
2104
2811
|
this.persistSession();
|
|
2105
2812
|
this.enqueue("page_view", { url: collect ? url : void 0, properties: props });
|
|
@@ -2125,21 +2832,28 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2125
2832
|
const heat = this.autocaptureOn("heatmaps");
|
|
2126
2833
|
if (!clicks && !rage && !heat || isIgnoredElement(target)) return;
|
|
2127
2834
|
this.touchSession();
|
|
2128
|
-
const
|
|
2835
|
+
const featureAttribute = this.config?.tagging?.featureAttribute ?? null;
|
|
2836
|
+
const feature = featureAttributeOf(target, featureAttribute);
|
|
2837
|
+
const el = interactiveElement(target) ?? feature?.el ?? null;
|
|
2129
2838
|
const opts = { mask: this.maskSelector(), maskAll: this.maskAllText(), collectUrls: this.collectUrls() };
|
|
2130
|
-
|
|
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) });
|
|
2131
2845
|
const count = this.rage.add(e.pageX ?? 0, e.pageY ?? 0, Date.now());
|
|
2132
|
-
if (rage && count) this.enqueue("rage_click", { properties: { ...
|
|
2846
|
+
if (rage && count) this.enqueue("rage_click", { properties: { ...clickProps(el ?? target), count } });
|
|
2133
2847
|
if (!heat) return;
|
|
2134
|
-
const url =
|
|
2848
|
+
const url = this.pageHref();
|
|
2135
2849
|
if (this.heatmap.url !== url) this.flushHeatmap();
|
|
2136
2850
|
if (this.heatmap.add(url, heatmapPoint(el ?? target, e, this.rage.burst > 0), this.rage.burst)) this.flushHeatmap();
|
|
2137
2851
|
}
|
|
2138
2852
|
/** Bins one pointer sample into the heatmap buffer. Not session activity. */
|
|
2139
2853
|
sampleMove(e) {
|
|
2140
|
-
const target = e
|
|
2854
|
+
const target = (0, import_guide_runtime5.composedTarget)(e);
|
|
2141
2855
|
if (document.visibilityState === "hidden" || !target?.closest || !this.autocaptureOn("mouseMovement") || isIgnoredElement(target)) return;
|
|
2142
|
-
const url =
|
|
2856
|
+
const url = this.pageHref();
|
|
2143
2857
|
if (this.heatmap.url !== url) this.flushHeatmap();
|
|
2144
2858
|
if (this.heatmap.move(url, movementBin(interactiveElement(target) ?? target, e))) this.flushHeatmap();
|
|
2145
2859
|
}
|
|
@@ -2215,7 +2929,7 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2215
2929
|
if (!this.isReady || this.destroyed) return;
|
|
2216
2930
|
if (this.config && !this.initialPageViewSent && this.config.settings?.autocapture) {
|
|
2217
2931
|
this.initialPageViewSent = true;
|
|
2218
|
-
this.trackPageView(
|
|
2932
|
+
this.trackPageView(this.pageHref());
|
|
2219
2933
|
}
|
|
2220
2934
|
if (this.config) {
|
|
2221
2935
|
for (const [props, extra] of this.earlyExceptions.splice(0)) {
|
|
@@ -2228,7 +2942,7 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2228
2942
|
syncReplay() {
|
|
2229
2943
|
const cfg = this.isReady && this.captureAllowed() ? this.replayConfig() : null;
|
|
2230
2944
|
if (cfg && this.session.replay === void 0) {
|
|
2231
|
-
this.session.replay = (0,
|
|
2945
|
+
this.session.replay = (0, import_runtime18.rolloutBucket)(this.session.id, "replay") < cfg.sampleRate;
|
|
2232
2946
|
this.persistSession();
|
|
2233
2947
|
}
|
|
2234
2948
|
if (!cfg || !this.session.replay) {
|
|
@@ -2250,7 +2964,7 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2250
2964
|
return {
|
|
2251
2965
|
anonymousId: this.anonymousId,
|
|
2252
2966
|
userId: this.user?.id || null,
|
|
2253
|
-
url: this.collectUrls() ?
|
|
2967
|
+
url: this.collectUrls() ? this.pageHref() : void 0,
|
|
2254
2968
|
device: { type: device.type, browser: device.browser, os: device.os },
|
|
2255
2969
|
viewport: { width: window.innerWidth, height: window.innerHeight },
|
|
2256
2970
|
sdk: { name: SDK_NAME, version: SDK_VERSION }
|
|
@@ -2262,7 +2976,9 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2262
2976
|
this.persistSession();
|
|
2263
2977
|
this.replay?.stop({ discard: true });
|
|
2264
2978
|
},
|
|
2265
|
-
onError: (err) => this.reportError(err)
|
|
2979
|
+
onError: (err) => this.reportError(err),
|
|
2980
|
+
health: this.api.health,
|
|
2981
|
+
stash: this.sessionStore
|
|
2266
2982
|
});
|
|
2267
2983
|
this.syncReplay();
|
|
2268
2984
|
},
|
|
@@ -2299,6 +3015,14 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2299
3015
|
// ---------------------------------------------------------------------------
|
|
2300
3016
|
// Targeting + triggers
|
|
2301
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
|
+
}
|
|
2302
3026
|
/** Builds the targeting context for the current moment. */
|
|
2303
3027
|
getContext() {
|
|
2304
3028
|
const guides = this.state.guides;
|
|
@@ -2309,7 +3033,7 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2309
3033
|
userId: this.user?.id || null,
|
|
2310
3034
|
userAttributes: this.user?.traits ?? {},
|
|
2311
3035
|
accountAttributes: this.user?.account?.traits ?? {},
|
|
2312
|
-
url:
|
|
3036
|
+
url: this.pageHref(),
|
|
2313
3037
|
firstVisit: this.firstVisit,
|
|
2314
3038
|
eventsSeen: this.session.events,
|
|
2315
3039
|
completedGuides: keysOf(Object.keys(guides).filter((id) => guides[id]?.completedAt)),
|
|
@@ -2318,17 +3042,64 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2318
3042
|
completedChecklists: keysOf(completedChecklists),
|
|
2319
3043
|
audienceIds: this.state.audienceIds,
|
|
2320
3044
|
environment: this.options.environment,
|
|
2321
|
-
platform: "web"
|
|
3045
|
+
platform: "web",
|
|
3046
|
+
...this.options.appVersion ? { appVersion: this.options.appVersion } : {}
|
|
2322
3047
|
});
|
|
2323
3048
|
}
|
|
2324
3049
|
isTargeted(guide, ctx, url) {
|
|
2325
3050
|
const c = guide.config;
|
|
2326
|
-
if (c.urlPatterns.length && !c.urlPatterns.some((p) => (0,
|
|
2327
|
-
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;
|
|
2328
3053
|
if (!isWithinSchedule(guide)) return false;
|
|
2329
3054
|
if (guide.kind === "survey" && (this.state.surveysSubmitted.includes(guide.id) || this.state.surveysSubmitted.includes(guide.key))) return false;
|
|
2330
3055
|
ctx.rolloutSalt = guide.id;
|
|
2331
|
-
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
|
+
}
|
|
2332
3103
|
}
|
|
2333
3104
|
isEligible(guide, ctx, url) {
|
|
2334
3105
|
if (!this.isTargeted(guide, ctx, url)) return false;
|
|
@@ -2346,13 +3117,13 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2346
3117
|
if (t.delayMs > 0) return reason === "timer" && detail.guideId === guide.id;
|
|
2347
3118
|
return reason === "ready" || reason === "navigation" || reason === "identify";
|
|
2348
3119
|
case "url_match":
|
|
2349
|
-
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());
|
|
2350
3121
|
case "event":
|
|
2351
3122
|
return reason === "event" && !!t.value && detail.eventName === t.value;
|
|
2352
3123
|
case "element_click":
|
|
2353
3124
|
if (reason !== "click" || !t.value || !detail.target) return false;
|
|
2354
3125
|
try {
|
|
2355
|
-
return !!detail.target
|
|
3126
|
+
return !!(0, import_guide_runtime5.closestComposed)(detail.target, t.value);
|
|
2356
3127
|
} catch {
|
|
2357
3128
|
return false;
|
|
2358
3129
|
}
|
|
@@ -2371,15 +3142,15 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2371
3142
|
}
|
|
2372
3143
|
evaluate(reason, detail = {}) {
|
|
2373
3144
|
if (!this.config || this.destroyed || this.editing()) return;
|
|
2374
|
-
const url =
|
|
3145
|
+
const url = this.pageHref();
|
|
2375
3146
|
const ctx = this.getContext();
|
|
2376
3147
|
if (reason === "identify") {
|
|
2377
|
-
const
|
|
2378
|
-
detail.newAudiences = new Set(Array.from(
|
|
2379
|
-
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;
|
|
2380
3151
|
}
|
|
2381
3152
|
const candidates = [];
|
|
2382
|
-
for (const guide of this.
|
|
3153
|
+
for (const guide of this.candidates()) {
|
|
2383
3154
|
const t = guide.config.trigger;
|
|
2384
3155
|
if (detail.endedGuide && detail.endedGuide.id === guide.id) continue;
|
|
2385
3156
|
if (reason === "ready" || reason === "navigation") {
|
|
@@ -2395,6 +3166,10 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2395
3166
|
if (t.type === "page_load" && t.delayMs > 0 && reason === "timer" && detail.guideId !== guide.id) continue;
|
|
2396
3167
|
if (!this.triggerFires(guide, reason, detail)) continue;
|
|
2397
3168
|
if (!this.isEligible(guide, ctx, url)) continue;
|
|
3169
|
+
if (!this.throttleAllows(guide)) {
|
|
3170
|
+
this.log("throttled", guide.key);
|
|
3171
|
+
continue;
|
|
3172
|
+
}
|
|
2398
3173
|
candidates.push(guide);
|
|
2399
3174
|
}
|
|
2400
3175
|
if (reason === "after_guide" || reason === "navigation") this.drainQueue(url, ctx);
|
|
@@ -2422,7 +3197,7 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2422
3197
|
this.visibilityObservers.delete(guide.id);
|
|
2423
3198
|
};
|
|
2424
3199
|
this.visibilityObservers.set(guide.id, stop);
|
|
2425
|
-
(0,
|
|
3200
|
+
(0, import_guide_runtime5.waitForTarget)([{ kind: "css", value: selector }], { timeoutMs: 0, signal: ac.signal }).then((el) => {
|
|
2426
3201
|
if (ac.signal.aborted) return;
|
|
2427
3202
|
const fire = () => {
|
|
2428
3203
|
stop();
|
|
@@ -2445,6 +3220,13 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2445
3220
|
/** Decides whether a guide can show now, otherwise queues it. */
|
|
2446
3221
|
tryShow(guide, url, manual) {
|
|
2447
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
|
+
}
|
|
2448
3230
|
if (!isExclusiveGuide(guide)) {
|
|
2449
3231
|
if (this.widgets.has(guide.id)) return false;
|
|
2450
3232
|
this.show(guide, manual);
|
|
@@ -2485,14 +3267,18 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2485
3267
|
guideState(id) {
|
|
2486
3268
|
return this.state.guides[id] ?? (this.state.guides[id] = { impressions: 0 });
|
|
2487
3269
|
}
|
|
2488
|
-
recordImpression(guide) {
|
|
3270
|
+
recordImpression(guide, manual = false) {
|
|
2489
3271
|
const s = this.guideState(guide.id);
|
|
2490
3272
|
s.impressions += 1;
|
|
2491
3273
|
s.lastShownAt = nowIso();
|
|
2492
3274
|
if (!this.session.shownGuides.includes(guide.id)) this.session.shownGuides.push(guide.id);
|
|
3275
|
+
this.recordLaunch(guide, manual);
|
|
2493
3276
|
this.persistState();
|
|
2494
3277
|
this.persistSession();
|
|
2495
|
-
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 });
|
|
2496
3282
|
}
|
|
2497
3283
|
recordEnd(guide, reason) {
|
|
2498
3284
|
const s = this.guideState(guide.id);
|
|
@@ -2503,7 +3289,9 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2503
3289
|
if (reason === "completed") this.completeChecklistTasksBy("guide_completed", guide);
|
|
2504
3290
|
}
|
|
2505
3291
|
syncGuideState(guideId, update) {
|
|
2506
|
-
|
|
3292
|
+
const body = { anonymousId: this.anonymousId, userId: this.user?.id || null, updates: [{ guideId, ...update }] };
|
|
3293
|
+
if (update.impression) void postWithRetry(this.api, "/guide-state", body);
|
|
3294
|
+
else this.outbox.add(`guide:${guideId}:${update.completed ? "completed" : "dismissed"}:${this.anonymousId}`, "/guide-state", body);
|
|
2507
3295
|
}
|
|
2508
3296
|
show(guide, manual, stepId) {
|
|
2509
3297
|
try {
|
|
@@ -2514,13 +3302,17 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2514
3302
|
this.reportError(err);
|
|
2515
3303
|
}
|
|
2516
3304
|
}
|
|
2517
|
-
showFlow(
|
|
3305
|
+
showFlow(raw, manual, stepId) {
|
|
2518
3306
|
if (this.editing()) return;
|
|
3307
|
+
const guide = this.localize(raw);
|
|
2519
3308
|
const exclusive = isExclusiveGuide(guide);
|
|
2520
|
-
const entry = { guide, manual, startUrl:
|
|
2521
|
-
const controller = (0,
|
|
3309
|
+
const entry = { guide, manual, startUrl: this.pageHref() };
|
|
3310
|
+
const controller = (0, import_guide_runtime5.runGuide)(guide, {
|
|
2522
3311
|
theme: this.theme(),
|
|
3312
|
+
// Code blocks only arrive when a workspace admin allowed them (the server strips them otherwise).
|
|
3313
|
+
runCode: true,
|
|
2523
3314
|
ctx: () => this.getContext(),
|
|
3315
|
+
currentUrl: () => this.pageHref(),
|
|
2524
3316
|
startAtStep: stepId,
|
|
2525
3317
|
eventBus: this.eventBus,
|
|
2526
3318
|
// Started from Help, Scout or startGuide(): steps on other pages offer to take the user there.
|
|
@@ -2534,7 +3326,7 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2534
3326
|
const step = e.stepId ? guide.config.steps.find((s) => s.id === e.stepId) : void 0;
|
|
2535
3327
|
this.enqueue(e.name, { guideId: guide.id, guideVersion: guide.version, stepId: e.stepId, stepIndex: e.stepIndex, properties: e.properties });
|
|
2536
3328
|
if (e.name === "guide_started") {
|
|
2537
|
-
this.recordImpression(guide);
|
|
3329
|
+
this.recordImpression(guide, manual);
|
|
2538
3330
|
this.emitter.emit("guide:start", guide);
|
|
2539
3331
|
}
|
|
2540
3332
|
if (e.name === "step_viewed" && step) {
|
|
@@ -2581,14 +3373,15 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2581
3373
|
this.widgets.delete(guide.id);
|
|
2582
3374
|
}
|
|
2583
3375
|
}
|
|
2584
|
-
showChecklist(
|
|
3376
|
+
showChecklist(raw) {
|
|
2585
3377
|
if (this.editing()) return;
|
|
3378
|
+
const guide = this.localize(raw);
|
|
2586
3379
|
const config = guide.config.checklist;
|
|
2587
3380
|
if (!config || this.widgets.has(guide.id)) return;
|
|
2588
|
-
const scope = (0,
|
|
3381
|
+
const scope = (0, import_guide_runtime6.createScope)(this.theme(), guide.config.appearance);
|
|
2589
3382
|
const progress = this.getChecklistProgress(guide.id);
|
|
2590
3383
|
const first = !this.state.guides[guide.id]?.impressions;
|
|
2591
|
-
const widget = (0,
|
|
3384
|
+
const widget = (0, import_guide_runtime5.renderChecklist)({
|
|
2592
3385
|
scope: scope.el,
|
|
2593
3386
|
config,
|
|
2594
3387
|
progress,
|
|
@@ -2603,12 +3396,13 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2603
3396
|
}, checklist: widget, scope });
|
|
2604
3397
|
if (first) this.recordImpression(guide);
|
|
2605
3398
|
}
|
|
2606
|
-
showSurvey(
|
|
3399
|
+
showSurvey(raw, manual) {
|
|
2607
3400
|
if (this.editing()) return;
|
|
3401
|
+
const guide = this.localize(raw);
|
|
2608
3402
|
const config = guide.config.survey;
|
|
2609
3403
|
if (!config) return;
|
|
2610
|
-
const scope = (0,
|
|
2611
|
-
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 };
|
|
2612
3406
|
const finish = (reason) => {
|
|
2613
3407
|
widget.destroy();
|
|
2614
3408
|
scope.destroy();
|
|
@@ -2620,18 +3414,18 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2620
3414
|
}
|
|
2621
3415
|
this.setTimer(() => this.evaluate("after_guide", { endedGuide: guide }), 0);
|
|
2622
3416
|
};
|
|
2623
|
-
const widget = (0,
|
|
3417
|
+
const widget = (0, import_guide_runtime5.renderSurvey)({
|
|
2624
3418
|
scope: scope.el,
|
|
2625
3419
|
config,
|
|
2626
3420
|
title: guide.name,
|
|
2627
3421
|
onDismiss: () => finish(this.state.surveysSubmitted.includes(guide.id) ? "submitted" : "dismissed"),
|
|
2628
3422
|
onSubmit: (answers) => {
|
|
2629
|
-
const responseId = (0,
|
|
3423
|
+
const responseId = (0, import_runtime18.randomId)(24);
|
|
2630
3424
|
this.state.surveysSubmitted.push(guide.id);
|
|
2631
3425
|
this.persistState();
|
|
2632
3426
|
this.recordEnd(guide, "completed");
|
|
2633
3427
|
this.enqueue("survey_submitted", { guideId: guide.id, guideVersion: guide.version, properties: { responseId } });
|
|
2634
|
-
|
|
3428
|
+
this.outbox.add(`survey:${responseId}`, `/surveys/${encodeURIComponent(guide.id)}/responses`, { anonymousId: this.anonymousId, userId: this.user?.id || null, accountId: this.user?.account?.id || null, guideVersion: guide.version, answers, responseId });
|
|
2635
3429
|
this.emitter.emit("survey:submit", { guide, answers, responseId });
|
|
2636
3430
|
this.emitter.emit("guide:complete", guide);
|
|
2637
3431
|
widget.showThankYou();
|
|
@@ -2639,7 +3433,7 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2639
3433
|
});
|
|
2640
3434
|
entry.survey = widget;
|
|
2641
3435
|
this.active = entry;
|
|
2642
|
-
this.recordImpression(guide);
|
|
3436
|
+
this.recordImpression(guide, manual);
|
|
2643
3437
|
this.enqueue("survey_displayed", { guideId: guide.id, guideVersion: guide.version });
|
|
2644
3438
|
this.emitter.emit("guide:start", guide);
|
|
2645
3439
|
}
|
|
@@ -2648,6 +3442,7 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2648
3442
|
}
|
|
2649
3443
|
navigate(url, newTab) {
|
|
2650
3444
|
try {
|
|
3445
|
+
if (this.openPrototypeTest(url)) return;
|
|
2651
3446
|
if (newTab) window.open(url, "_blank", "noopener");
|
|
2652
3447
|
else location.assign(url);
|
|
2653
3448
|
} catch (err) {
|
|
@@ -2668,11 +3463,17 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2668
3463
|
}
|
|
2669
3464
|
this.destroyMessenger();
|
|
2670
3465
|
if (rc && rc.enabled && !this.resourceCenter) {
|
|
2671
|
-
const scope = (0,
|
|
3466
|
+
const scope = (0, import_guide_runtime6.createScope)(this.theme());
|
|
2672
3467
|
this.resourceCenterScope = scope;
|
|
2673
|
-
this.resourceCenter = (0,
|
|
3468
|
+
this.resourceCenter = (0, import_guide_runtime5.renderResourceCenter)({
|
|
2674
3469
|
scope: scope.el,
|
|
2675
|
-
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
|
+
},
|
|
2676
3477
|
guides: this.config.guides.map((g) => ({ id: g.id, key: g.key, name: g.name, kind: g.kind })),
|
|
2677
3478
|
search: (q) => this.searchResourceCenter(q),
|
|
2678
3479
|
audienceIds: () => this.state.audienceIds,
|
|
@@ -2706,7 +3507,7 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2706
3507
|
this.messengerLoading = import("@guidelytech/guide-runtime/messenger").then(({ renderMessenger }) => {
|
|
2707
3508
|
const cfg = this.config?.messenger;
|
|
2708
3509
|
if (this.destroyed || !this.config || !cfg?.enabled || this.messenger) return this.messenger;
|
|
2709
|
-
const scope = (0,
|
|
3510
|
+
const scope = (0, import_guide_runtime6.createScope)(this.theme());
|
|
2710
3511
|
this.messengerScope = scope;
|
|
2711
3512
|
this.messenger = renderMessenger({
|
|
2712
3513
|
scope: scope.el,
|
|
@@ -2717,7 +3518,8 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2717
3518
|
projectId: this.options.projectId,
|
|
2718
3519
|
anonymousId: () => this.anonymousId,
|
|
2719
3520
|
userId: () => this.user?.id || null,
|
|
2720
|
-
pageUrl: () => this.collectUrls() ?
|
|
3521
|
+
pageUrl: () => this.collectUrls() ? this.pageHref() : void 0,
|
|
3522
|
+
locale: () => this.rcLocale()
|
|
2721
3523
|
}),
|
|
2722
3524
|
visitor: () => this.messengerVisitor(),
|
|
2723
3525
|
brand: { logoUrl: this.theme().logoUrl ?? null, name: this.theme().brandName ?? null },
|
|
@@ -2726,7 +3528,8 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2726
3528
|
const seen = readJson(this.store, KEYS.newsSeen(this.options.projectId), []);
|
|
2727
3529
|
writeJson(this.store, KEYS.newsSeen(this.options.projectId), Array.from(/* @__PURE__ */ new Set([...ids, ...Array.isArray(seen) ? seen : []])).slice(0, 200));
|
|
2728
3530
|
},
|
|
2729
|
-
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()),
|
|
2730
3533
|
audienceIds: () => this.state.audienceIds,
|
|
2731
3534
|
guides: this.config.guides.map((g) => ({ id: g.id, key: g.key, name: g.name, kind: g.kind })),
|
|
2732
3535
|
onStartGuide: (idOrKey) => this.startGuide(idOrKey),
|
|
@@ -2750,7 +3553,7 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2750
3553
|
/** What the messenger personalises from, computed fresh on every render. */
|
|
2751
3554
|
messengerVisitor() {
|
|
2752
3555
|
const ctx = this.getContext();
|
|
2753
|
-
const url =
|
|
3556
|
+
const url = this.pageHref();
|
|
2754
3557
|
const seen = readJson(this.store, KEYS.newsSeen(this.options.projectId), []);
|
|
2755
3558
|
const visitor = buildMessengerVisitor({
|
|
2756
3559
|
guides: this.config?.guides ?? [],
|
|
@@ -2761,11 +3564,24 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2761
3564
|
isAudienceTargeted: (g) => {
|
|
2762
3565
|
if (!isWithinSchedule(g)) return false;
|
|
2763
3566
|
ctx.rolloutSalt = g.id;
|
|
2764
|
-
return (0,
|
|
3567
|
+
return (0, import_runtime18.evaluateRules)(g.config.targeting, ctx);
|
|
2765
3568
|
},
|
|
2766
3569
|
newsSeen: Array.isArray(seen) ? seen : []
|
|
2767
3570
|
});
|
|
2768
|
-
|
|
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 };
|
|
2769
3585
|
}
|
|
2770
3586
|
destroyMessenger() {
|
|
2771
3587
|
this.messenger?.destroy();
|
|
@@ -2773,9 +3589,17 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2773
3589
|
this.messenger = null;
|
|
2774
3590
|
this.messengerScope = null;
|
|
2775
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
|
+
}
|
|
2776
3599
|
async loadArticle(id) {
|
|
2777
3600
|
try {
|
|
2778
|
-
const
|
|
3601
|
+
const locale = this.rcLocale();
|
|
3602
|
+
const res = await this.api.get(`/articles/${encodeURIComponent(id)}`, locale ? { params: { locale } } : void 0);
|
|
2779
3603
|
return res.ok && res.data ? res.data.article : null;
|
|
2780
3604
|
} catch {
|
|
2781
3605
|
return null;
|
|
@@ -2813,14 +3637,14 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2813
3637
|
if (progress.completedTaskIds.includes(task.id)) return;
|
|
2814
3638
|
progress.completedTaskIds.push(task.id);
|
|
2815
3639
|
this.enqueue("checklist_task_completed", { guideId: guide.id, guideVersion: guide.version, properties: { taskId: task.id } });
|
|
2816
|
-
const complete = (0,
|
|
3640
|
+
const complete = (0, import_guide_runtime5.isChecklistComplete)(config, progress) && !progress.completedAt;
|
|
2817
3641
|
if (complete) {
|
|
2818
3642
|
progress.completedAt = nowIso();
|
|
2819
3643
|
this.enqueue("checklist_completed", { guideId: guide.id, guideVersion: guide.version });
|
|
2820
3644
|
}
|
|
2821
3645
|
this.persistState();
|
|
2822
3646
|
this.widgets.get(guide.id)?.checklist?.setProgress(this.getChecklistProgress(guide.id));
|
|
2823
|
-
|
|
3647
|
+
this.outbox.add(`checklist:${guide.id}:progress:${this.anonymousId}`, `/checklists/${encodeURIComponent(guide.id)}/progress`, { anonymousId: this.anonymousId, userId: this.user?.id || null, guideVersion: guide.version, completedTaskIds: progress.completedTaskIds, completed: !!progress.completedAt });
|
|
2824
3648
|
this.emitter.emit("checklist:task", { guide, task, progress: this.getChecklistProgress(guide.id) });
|
|
2825
3649
|
if (complete) {
|
|
2826
3650
|
this.emitter.emit("checklist:complete", guide);
|
|
@@ -2836,11 +3660,11 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2836
3660
|
if (task.completion !== kind || !task.completionValue) continue;
|
|
2837
3661
|
let hit = false;
|
|
2838
3662
|
if (kind === "event") hit = value === task.completionValue;
|
|
2839
|
-
else if (kind === "url_visited") hit = (0,
|
|
3663
|
+
else if (kind === "url_visited") hit = (0, import_guide_runtime5.urlMatches)(task.completionValue, value);
|
|
2840
3664
|
else if (kind === "guide_completed") hit = value.id === task.completionValue || value.key === task.completionValue;
|
|
2841
3665
|
else if (kind === "click") {
|
|
2842
3666
|
try {
|
|
2843
|
-
hit = !!
|
|
3667
|
+
hit = !!(0, import_guide_runtime5.closestComposed)(value, task.completionValue);
|
|
2844
3668
|
} catch {
|
|
2845
3669
|
hit = false;
|
|
2846
3670
|
}
|
|
@@ -2860,7 +3684,7 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2860
3684
|
progress.dismissedAt = nowIso();
|
|
2861
3685
|
this.persistState();
|
|
2862
3686
|
this.enqueue("checklist_dismissed", { guideId: guide.id, guideVersion: guide.version });
|
|
2863
|
-
|
|
3687
|
+
this.outbox.add(`checklist:${guide.id}:dismissed:${this.anonymousId}`, `/checklists/${encodeURIComponent(guide.id)}/progress`, { anonymousId: this.anonymousId, userId: this.user?.id || null, guideVersion: guide.version, completedTaskIds: progress.completedTaskIds, dismissed: true });
|
|
2864
3688
|
this.widgets.get(guide.id)?.destroy();
|
|
2865
3689
|
this.widgets.delete(guide.id);
|
|
2866
3690
|
}
|
|
@@ -2951,6 +3775,18 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2951
3775
|
closeMessenger() {
|
|
2952
3776
|
this.messenger?.close();
|
|
2953
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
|
+
}
|
|
2954
3790
|
/**
|
|
2955
3791
|
* Opens the complaint form in the messenger, optionally with a category picked (its id from
|
|
2956
3792
|
* Complaints → Categories). Does nothing when the project doesn't take complaints.
|
|
@@ -2959,6 +3795,14 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2959
3795
|
if (!this.config?.messenger?.enabled || !this.config.messenger.complaints?.categories.length) return;
|
|
2960
3796
|
void this.loadMessenger().then((m) => m?.openComplaint(categoryId));
|
|
2961
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
|
+
}
|
|
2962
3806
|
/** Opens a new messenger conversation, optionally pre-filling the message. */
|
|
2963
3807
|
showNewMessage(text) {
|
|
2964
3808
|
if (!this.config?.messenger?.enabled) return;
|
|
@@ -2988,14 +3832,15 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2988
3832
|
const device = detectDevice(navigator.userAgent, window.innerWidth);
|
|
2989
3833
|
const account = this.user?.account;
|
|
2990
3834
|
return {
|
|
2991
|
-
url:
|
|
2992
|
-
path:
|
|
3835
|
+
url: this.pageHref(),
|
|
3836
|
+
path: pathOf2(this.pageHref()),
|
|
2993
3837
|
deviceType: device.type,
|
|
2994
3838
|
browser: device.browser,
|
|
2995
3839
|
os: device.os,
|
|
2996
3840
|
locale: navigator.language || void 0,
|
|
2997
3841
|
environment: this.options.environment,
|
|
2998
3842
|
platform: "web",
|
|
3843
|
+
...this.options.appVersion ? { appVersion: this.options.appVersion } : {},
|
|
2999
3844
|
...this.user?.traits ?? {},
|
|
3000
3845
|
id: this.user?.id || this.anonymousId,
|
|
3001
3846
|
anonymousId: this.anonymousId,
|
|
@@ -3155,20 +4000,20 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
3155
4000
|
// ---------------------------------------------------------------------------
|
|
3156
4001
|
/** Creates (or returns the existing) client and starts initialisation. */
|
|
3157
4002
|
static init(options) {
|
|
3158
|
-
if (
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
void
|
|
3162
|
-
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;
|
|
3163
4008
|
}
|
|
3164
4009
|
/** The most recently created client, if any. */
|
|
3165
4010
|
static get() {
|
|
3166
|
-
return
|
|
4011
|
+
return current2 && !current2.destroyed ? current2 : null;
|
|
3167
4012
|
}
|
|
3168
4013
|
};
|
|
3169
|
-
var
|
|
4014
|
+
var current2 = null;
|
|
3170
4015
|
function webOnly(config) {
|
|
3171
|
-
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")) };
|
|
3172
4017
|
}
|
|
3173
4018
|
function pathOf2(url) {
|
|
3174
4019
|
try {
|
|
@@ -3188,7 +4033,7 @@ function externalReferrer() {
|
|
|
3188
4033
|
}
|
|
3189
4034
|
function createGuidely(options) {
|
|
3190
4035
|
const client = new GuidelyClient(options);
|
|
3191
|
-
|
|
4036
|
+
current2 = client;
|
|
3192
4037
|
void client.init();
|
|
3193
4038
|
return client;
|
|
3194
4039
|
}
|
|
@@ -3211,17 +4056,24 @@ var index_default = Guidely;
|
|
|
3211
4056
|
GuidelyClient,
|
|
3212
4057
|
HEARTBEAT_MS,
|
|
3213
4058
|
KEYS,
|
|
4059
|
+
PROTOTYPE_CLOSE_MESSAGE,
|
|
3214
4060
|
SDK_NAME,
|
|
3215
4061
|
SDK_VERSION,
|
|
3216
4062
|
areMutuallyExclusive,
|
|
3217
4063
|
buildContext,
|
|
3218
4064
|
captureSnapshot,
|
|
4065
|
+
countsTowardThrottle,
|
|
3219
4066
|
createGuidely,
|
|
3220
4067
|
createStorage,
|
|
3221
4068
|
detectDevice,
|
|
4069
|
+
isAppUrl,
|
|
3222
4070
|
isExclusiveGuide,
|
|
3223
4071
|
isFrequencyEligible,
|
|
4072
|
+
isThrottleEligible,
|
|
3224
4073
|
isWithinSchedule,
|
|
4074
|
+
matchPrototypeTestUrl,
|
|
4075
|
+
normalizeAppUrl,
|
|
4076
|
+
pruneLaunches,
|
|
3225
4077
|
watchNavigation
|
|
3226
4078
|
});
|
|
3227
4079
|
//# sourceMappingURL=index.cjs.map
|