@guidelytech/sdk-core 0.2.0 → 0.4.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 +37 -4
- package/dist/chunk-GJT7CFAC.js +151 -0
- package/dist/chunk-GJT7CFAC.js.map +1 -0
- package/dist/chunk-HLR3W63Z.js +282 -0
- package/dist/chunk-HLR3W63Z.js.map +1 -0
- package/dist/headless.cjs +423 -0
- package/dist/headless.cjs.map +1 -0
- package/dist/headless.d.cts +65 -0
- package/dist/headless.d.ts +65 -0
- package/dist/headless.js +35 -0
- package/dist/headless.js.map +1 -0
- package/dist/index.cjs +731 -53
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +86 -213
- package/dist/index.d.ts +86 -213
- package/dist/index.js +701 -411
- package/dist/index.js.map +1 -1
- package/dist/recorder.cjs +766 -0
- package/dist/recorder.cjs.map +1 -0
- package/dist/recorder.d.cts +155 -0
- package/dist/recorder.d.ts +155 -0
- package/dist/recorder.js +735 -0
- package/dist/recorder.js.map +1 -0
- package/dist/types-BZ9r3Fs9.d.cts +247 -0
- package/dist/types-BZ9r3Fs9.d.ts +247 -0
- package/package.json +21 -8
package/dist/index.js
CHANGED
|
@@ -1,9 +1,23 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import {
|
|
2
|
+
Emitter,
|
|
3
|
+
EventQueue,
|
|
4
|
+
KEYS,
|
|
5
|
+
areMutuallyExclusive,
|
|
6
|
+
createStorage,
|
|
7
|
+
isExclusiveGuide,
|
|
8
|
+
isFrequencyEligible,
|
|
9
|
+
isWithinSchedule,
|
|
10
|
+
readJson,
|
|
11
|
+
writeJson
|
|
12
|
+
} from "./chunk-HLR3W63Z.js";
|
|
13
|
+
import {
|
|
14
|
+
Api,
|
|
15
|
+
__publicField,
|
|
16
|
+
postWithRetry
|
|
17
|
+
} from "./chunk-GJT7CFAC.js";
|
|
4
18
|
|
|
5
19
|
// src/client.ts
|
|
6
|
-
import { evaluateRules, randomId, sanitizeProperties } from "@guidelytech/shared/runtime";
|
|
20
|
+
import { evaluateRules, randomId, rolloutBucket, sanitizeProperties, supportsPlatform } from "@guidelytech/shared/runtime";
|
|
7
21
|
import {
|
|
8
22
|
destroyHost,
|
|
9
23
|
isChecklistComplete,
|
|
@@ -61,9 +75,9 @@ function captureSnapshot(doc, maskSelectors, maxBytes = MAX_SNAPSHOT_BYTES) {
|
|
|
61
75
|
}
|
|
62
76
|
}
|
|
63
77
|
clone.querySelectorAll("*").forEach((el) => {
|
|
64
|
-
for (const
|
|
65
|
-
const name =
|
|
66
|
-
if (name.startsWith("on") || (name === "href" || name === "src" || name === "action") && /^\s*javascript:/i.test(
|
|
78
|
+
for (const attr2 of Array.from(el.attributes)) {
|
|
79
|
+
const name = attr2.name.toLowerCase();
|
|
80
|
+
if (name.startsWith("on") || (name === "href" || name === "src" || name === "action") && /^\s*javascript:/i.test(attr2.value)) el.removeAttribute(attr2.name);
|
|
67
81
|
}
|
|
68
82
|
if (el instanceof HTMLInputElement) {
|
|
69
83
|
el.removeAttribute("value");
|
|
@@ -95,78 +109,6 @@ ${clone.outerHTML}`;
|
|
|
95
109
|
return { html, url: win.location.href, title: doc.title, width: win.innerWidth, height: win.innerHeight, capturedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
96
110
|
}
|
|
97
111
|
|
|
98
|
-
// src/storage.ts
|
|
99
|
-
function memoryStore() {
|
|
100
|
-
const map = /* @__PURE__ */ new Map();
|
|
101
|
-
return { get: (k) => map.get(k) ?? null, set: (k, v) => void map.set(k, v), remove: (k) => void map.delete(k) };
|
|
102
|
-
}
|
|
103
|
-
function webStore(area) {
|
|
104
|
-
try {
|
|
105
|
-
if (typeof window === "undefined") return null;
|
|
106
|
-
const s = window[area];
|
|
107
|
-
if (!s) return null;
|
|
108
|
-
const probe = "__guidely_probe__";
|
|
109
|
-
s.setItem(probe, "1");
|
|
110
|
-
s.removeItem(probe);
|
|
111
|
-
return {
|
|
112
|
-
get: (k) => {
|
|
113
|
-
try {
|
|
114
|
-
return s.getItem(k);
|
|
115
|
-
} catch {
|
|
116
|
-
return null;
|
|
117
|
-
}
|
|
118
|
-
},
|
|
119
|
-
set: (k, v) => {
|
|
120
|
-
try {
|
|
121
|
-
s.setItem(k, v);
|
|
122
|
-
} catch {
|
|
123
|
-
}
|
|
124
|
-
},
|
|
125
|
-
remove: (k) => {
|
|
126
|
-
try {
|
|
127
|
-
s.removeItem(k);
|
|
128
|
-
} catch {
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
};
|
|
132
|
-
} catch {
|
|
133
|
-
return null;
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
function createStorage(kind = "local") {
|
|
137
|
-
if (kind === "memory") return memoryStore();
|
|
138
|
-
return webStore(kind === "local" ? "localStorage" : "sessionStorage") ?? memoryStore();
|
|
139
|
-
}
|
|
140
|
-
function readJson(store, key, fallback) {
|
|
141
|
-
const raw = store.get(key);
|
|
142
|
-
if (raw == null) return fallback;
|
|
143
|
-
try {
|
|
144
|
-
return JSON.parse(raw);
|
|
145
|
-
} catch {
|
|
146
|
-
return fallback;
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
function writeJson(store, key, value) {
|
|
150
|
-
try {
|
|
151
|
-
store.set(key, JSON.stringify(value));
|
|
152
|
-
} catch {
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
var KEYS = {
|
|
156
|
-
anon: "guidely:anon",
|
|
157
|
-
seen: "guidely:seen",
|
|
158
|
-
config: (key) => `guidely:config:${key}`,
|
|
159
|
-
etag: (key) => `guidely:etag:${key}`,
|
|
160
|
-
state: (key) => `guidely:state:${key}`,
|
|
161
|
-
user: (key) => `guidely:user:${key}`,
|
|
162
|
-
queue: (key) => `guidely:queue:${key}`,
|
|
163
|
-
session: "guidely:session",
|
|
164
|
-
active: "guidely:active",
|
|
165
|
-
authoring: "guidely:authoring",
|
|
166
|
-
/** Messenger conversations this browser can read: [{ id, token }]. */
|
|
167
|
-
conversations: (key) => `guidely:conversations:${key}`
|
|
168
|
-
};
|
|
169
|
-
|
|
170
112
|
// src/authoring.ts
|
|
171
113
|
async function loadSchemas() {
|
|
172
114
|
const m = await import("@guidelytech/shared");
|
|
@@ -396,6 +338,134 @@ var AuthoringBridge = class {
|
|
|
396
338
|
}
|
|
397
339
|
};
|
|
398
340
|
|
|
341
|
+
// src/autocapture.ts
|
|
342
|
+
import { isGuidelyElement, isStableId } from "@guidelytech/guide-runtime";
|
|
343
|
+
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]";
|
|
344
|
+
var DEFAULT_MASK_SELECTORS = ["[data-guidely-mask]", ".guidely-mask"];
|
|
345
|
+
var PREFERRED_ATTRS = ["data-testid", "data-test-id", "data-test", "data-cy", "data-qa", "data-guidely-track", "data-tour"];
|
|
346
|
+
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;
|
|
347
|
+
function validSelectors(list) {
|
|
348
|
+
const out = [];
|
|
349
|
+
const probe = typeof document !== "undefined" ? document.createDocumentFragment() : null;
|
|
350
|
+
for (const raw of list) {
|
|
351
|
+
const s = (raw || "").trim();
|
|
352
|
+
if (!s || out.includes(s)) continue;
|
|
353
|
+
try {
|
|
354
|
+
probe?.querySelector(s);
|
|
355
|
+
out.push(s);
|
|
356
|
+
} catch {
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
return out;
|
|
360
|
+
}
|
|
361
|
+
function isIgnoredElement(el) {
|
|
362
|
+
return isGuidelyElement(el) || !!el.closest("[data-guidely-ignore]");
|
|
363
|
+
}
|
|
364
|
+
function interactiveElement(target) {
|
|
365
|
+
try {
|
|
366
|
+
return target.closest(INTERACTIVE_SELECTOR);
|
|
367
|
+
} catch {
|
|
368
|
+
return null;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
function esc(value) {
|
|
372
|
+
const g = globalThis;
|
|
373
|
+
return g.CSS?.escape ? g.CSS.escape(value) : value.replace(/[^\w-]/g, (c) => `\\${c}`);
|
|
374
|
+
}
|
|
375
|
+
var attr = (tag, name, value) => value ? `${tag}[${name}="${value.replace(/["\\]/g, "\\$&")}"]` : `${tag}[${name}]`;
|
|
376
|
+
function elementSelector(el) {
|
|
377
|
+
const tag = el.tagName.toLowerCase();
|
|
378
|
+
if (el.id && isStableId(el.id)) return `#${esc(el.id)}`;
|
|
379
|
+
for (const name of PREFERRED_ATTRS) if (el.hasAttribute(name)) return attr(tag, name, el.getAttribute(name) || "").slice(0, 300);
|
|
380
|
+
for (const a of Array.from(el.attributes)) {
|
|
381
|
+
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);
|
|
382
|
+
}
|
|
383
|
+
const label = el.getAttribute("aria-label");
|
|
384
|
+
if (label && label.length <= 80) return attr(tag, "aria-label", label).slice(0, 300);
|
|
385
|
+
const parts = [];
|
|
386
|
+
let cur = el;
|
|
387
|
+
while (cur && parts.length < 4) {
|
|
388
|
+
const t = cur.tagName.toLowerCase();
|
|
389
|
+
if (t === "html" || t === "body") break;
|
|
390
|
+
if (cur !== el && cur.id && isStableId(cur.id)) {
|
|
391
|
+
parts.unshift(`#${esc(cur.id)}`);
|
|
392
|
+
break;
|
|
393
|
+
}
|
|
394
|
+
const parent = cur.parentElement;
|
|
395
|
+
const node = cur;
|
|
396
|
+
const same = parent ? Array.from(parent.children).filter((c) => c.tagName === node.tagName) : [];
|
|
397
|
+
parts.unshift(same.length > 1 ? `${t}:nth-of-type(${same.indexOf(node) + 1})` : t);
|
|
398
|
+
cur = parent;
|
|
399
|
+
}
|
|
400
|
+
return (parts.join(" > ") || tag).slice(0, 300);
|
|
401
|
+
}
|
|
402
|
+
function matches(el, selector, deep) {
|
|
403
|
+
if (!selector) return false;
|
|
404
|
+
try {
|
|
405
|
+
return !!el.closest(selector) || deep && !!el.querySelector(selector);
|
|
406
|
+
} catch {
|
|
407
|
+
return false;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
function elementText(el, mask, maskAll) {
|
|
411
|
+
if (maskAll || matches(el, mask, true)) return void 0;
|
|
412
|
+
const tag = el.tagName.toLowerCase();
|
|
413
|
+
let text = "";
|
|
414
|
+
if (tag === "input") {
|
|
415
|
+
if (!/^(button|submit|reset)$/i.test(el.type || "")) return void 0;
|
|
416
|
+
text = el.value;
|
|
417
|
+
} else if (tag === "select" || tag === "textarea") return void 0;
|
|
418
|
+
else text = el.innerText ?? el.textContent ?? "";
|
|
419
|
+
text = (text || el.getAttribute("aria-label") || el.getAttribute("title") || "").replace(/\s+/g, " ").trim();
|
|
420
|
+
return text ? text.slice(0, 100) : void 0;
|
|
421
|
+
}
|
|
422
|
+
function linkHref(el) {
|
|
423
|
+
const a = el.closest("a[href]");
|
|
424
|
+
if (!a) return void 0;
|
|
425
|
+
try {
|
|
426
|
+
const u = new URL(a.getAttribute("href") || "", location.href);
|
|
427
|
+
if (!/^https?:$/.test(u.protocol)) return void 0;
|
|
428
|
+
return u.origin === location.origin ? u.pathname : u.origin;
|
|
429
|
+
} catch {
|
|
430
|
+
return void 0;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
function describeClick(el, e, opts) {
|
|
434
|
+
const props = { tag: el.tagName.toLowerCase(), selector: elementSelector(el), x: Math.round(e.pageX ?? 0) || 0, y: Math.round(e.pageY ?? 0) || 0 };
|
|
435
|
+
const text = elementText(el, opts.mask, opts.maskAll);
|
|
436
|
+
if (text) props.text = text;
|
|
437
|
+
const href = opts.collectUrls ? linkHref(el) : void 0;
|
|
438
|
+
if (href) props.href = href;
|
|
439
|
+
return props;
|
|
440
|
+
}
|
|
441
|
+
var RageClickDetector = class {
|
|
442
|
+
constructor(threshold = 3, windowMs = 1e3, radius = 30) {
|
|
443
|
+
__publicField(this, "threshold", threshold);
|
|
444
|
+
__publicField(this, "windowMs", windowMs);
|
|
445
|
+
__publicField(this, "radius", radius);
|
|
446
|
+
__publicField(this, "clicks", []);
|
|
447
|
+
__publicField(this, "fired", false);
|
|
448
|
+
}
|
|
449
|
+
/** Clicks in the current burst once it has fired (rage), otherwise 0. */
|
|
450
|
+
get burst() {
|
|
451
|
+
return this.fired ? this.clicks.length : 0;
|
|
452
|
+
}
|
|
453
|
+
/** Records a click; returns the click count when it completes a burst, otherwise 0. */
|
|
454
|
+
add(x, y, t) {
|
|
455
|
+
const first = this.clicks[0];
|
|
456
|
+
const last = this.clicks[this.clicks.length - 1];
|
|
457
|
+
if (!first || !last || t - last.t > this.windowMs || Math.hypot(x - first.x, y - first.y) > this.radius) {
|
|
458
|
+
this.clicks = [];
|
|
459
|
+
this.fired = false;
|
|
460
|
+
}
|
|
461
|
+
this.clicks = this.clicks.filter((c) => t - c.t <= this.windowMs);
|
|
462
|
+
this.clicks.push({ x, y, t });
|
|
463
|
+
if (this.fired || this.clicks.length < this.threshold) return 0;
|
|
464
|
+
this.fired = true;
|
|
465
|
+
return this.clicks.length;
|
|
466
|
+
}
|
|
467
|
+
};
|
|
468
|
+
|
|
399
469
|
// src/context.ts
|
|
400
470
|
function detectDevice(ua, width) {
|
|
401
471
|
const u = ua || "";
|
|
@@ -452,228 +522,153 @@ function buildContext(input) {
|
|
|
452
522
|
audienceIds: new Set(input.audienceIds),
|
|
453
523
|
now: input.now ?? /* @__PURE__ */ new Date(),
|
|
454
524
|
environment: input.environment,
|
|
455
|
-
locale: input.locale ?? (typeof navigator !== "undefined" ? navigator.language : void 0)
|
|
525
|
+
locale: input.locale ?? (typeof navigator !== "undefined" ? navigator.language : void 0),
|
|
526
|
+
platform: input.platform ?? "web",
|
|
527
|
+
...input.screen !== void 0 ? { screen: input.screen } : {},
|
|
528
|
+
...input.appVersion !== void 0 ? { appVersion: input.appVersion } : {}
|
|
456
529
|
};
|
|
457
530
|
}
|
|
458
531
|
|
|
459
|
-
// src/
|
|
460
|
-
var
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
532
|
+
// src/heatmap.ts
|
|
533
|
+
var HEATMAP_MAX_POINTS = 40;
|
|
534
|
+
var HEATMAP_MAX_BYTES = 6e3;
|
|
535
|
+
var HEATMAP_MAX_BINS = 60;
|
|
536
|
+
var r2 = (n) => Math.round(Math.min(1, Math.max(0, n)) * 100) / 100;
|
|
537
|
+
function heatmapPoint(el, e, rage) {
|
|
538
|
+
const p = { k: rage ? "r" : "c", x: Math.round(e.pageX ?? 0) || 0, y: Math.round(e.pageY ?? 0) || 0 };
|
|
539
|
+
const s = elementSelector(el).slice(0, 120);
|
|
540
|
+
if (s) p.s = s;
|
|
541
|
+
const rect = el.getBoundingClientRect();
|
|
542
|
+
if (rect.width > 0 && rect.height > 0) {
|
|
543
|
+
p.rx = r2((e.clientX - rect.left) / rect.width);
|
|
544
|
+
p.ry = r2((e.clientY - rect.top) / rect.height);
|
|
545
|
+
}
|
|
546
|
+
return p;
|
|
547
|
+
}
|
|
548
|
+
var selectors = /* @__PURE__ */ new WeakMap();
|
|
549
|
+
var r1 = (n) => Math.round(Math.min(1, Math.max(0, n)) * 10) / 10;
|
|
550
|
+
function movementBin(el, e) {
|
|
551
|
+
const p = { k: "m", x: Math.round(e.pageX ?? 0) || 0, y: Math.round(e.pageY ?? 0) || 0 };
|
|
552
|
+
let s = /^(HTML|BODY)$/.test(el.tagName) ? "" : selectors.get(el);
|
|
553
|
+
if (s === void 0) selectors.set(el, s = elementSelector(el).slice(0, 120));
|
|
554
|
+
const rect = s ? el.getBoundingClientRect() : null;
|
|
555
|
+
if (!rect || !(rect.width > 0 && rect.height > 0)) return [`${p.x >> 4},${p.y >> 4}`, p];
|
|
556
|
+
p.s = s;
|
|
557
|
+
p.rx = r1((e.clientX - rect.left) / rect.width);
|
|
558
|
+
p.ry = r1((e.clientY - rect.top) / rect.height);
|
|
559
|
+
return [`${s} ${p.rx},${p.ry}`, p];
|
|
560
|
+
}
|
|
561
|
+
var HeatmapBuffer = class {
|
|
562
|
+
constructor() {
|
|
563
|
+
/** The page the buffered points belong to. */
|
|
564
|
+
__publicField(this, "url", "");
|
|
565
|
+
__publicField(this, "points", []);
|
|
566
|
+
__publicField(this, "bins", /* @__PURE__ */ new Map());
|
|
567
|
+
__publicField(this, "bytes", 0);
|
|
568
|
+
}
|
|
569
|
+
/** Adds a click (`burst` > 0 marks it and the burst's previous clicks as rage); true when the batch is full. */
|
|
570
|
+
add(url, point, burst) {
|
|
571
|
+
this.url = url;
|
|
572
|
+
for (let i = Math.max(0, this.points.length - burst + 1); i < this.points.length; i++) this.points[i].k = "r";
|
|
573
|
+
this.points.push(point);
|
|
574
|
+
this.bytes += JSON.stringify(point).length + 1;
|
|
575
|
+
return this.full();
|
|
576
|
+
}
|
|
577
|
+
/** Adds a movement sample to its bin (weight +1); true when the batch is full. */
|
|
578
|
+
move(url, [key, point]) {
|
|
579
|
+
this.url = url;
|
|
580
|
+
const bin = this.bins.get(key);
|
|
581
|
+
if (bin) bin.w = (bin.w ?? 1) + 1;
|
|
582
|
+
else {
|
|
583
|
+
this.bins.set(key, point);
|
|
584
|
+
this.bytes += JSON.stringify(point).length + 8;
|
|
470
585
|
}
|
|
471
|
-
|
|
472
|
-
return () => this.off(event, cb);
|
|
586
|
+
return this.full();
|
|
473
587
|
}
|
|
474
|
-
|
|
475
|
-
this.
|
|
476
|
-
}
|
|
477
|
-
emit(event, ...args) {
|
|
478
|
-
const set = this.listeners.get(event);
|
|
479
|
-
if (!set) return;
|
|
480
|
-
for (const cb of Array.from(set)) {
|
|
481
|
-
try {
|
|
482
|
-
cb(...args);
|
|
483
|
-
} catch (err) {
|
|
484
|
-
this.onError?.(err);
|
|
485
|
-
}
|
|
486
|
-
}
|
|
588
|
+
full() {
|
|
589
|
+
return this.points.length >= HEATMAP_MAX_POINTS || this.bins.size > HEATMAP_MAX_BINS || this.bytes >= HEATMAP_MAX_BYTES;
|
|
487
590
|
}
|
|
488
|
-
|
|
489
|
-
|
|
591
|
+
/** Empties the buffer, returning what was in it (clicks first, then movement bins). */
|
|
592
|
+
take() {
|
|
593
|
+
const points = [...this.points, ...this.bins.values()];
|
|
594
|
+
this.points = [];
|
|
595
|
+
this.bins = /* @__PURE__ */ new Map();
|
|
596
|
+
this.bytes = 0;
|
|
597
|
+
return points.length ? { url: this.url, points } : null;
|
|
490
598
|
}
|
|
491
599
|
};
|
|
492
600
|
|
|
493
|
-
// src/
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
return !shownThisSession;
|
|
508
|
-
case "interval": {
|
|
509
|
-
if (!s.lastShownAt) return true;
|
|
510
|
-
const hours = freq.intervalHours ?? 24;
|
|
511
|
-
return now - Date.parse(s.lastShownAt) >= hours * HOUR;
|
|
512
|
-
}
|
|
513
|
-
case "every_time":
|
|
514
|
-
default:
|
|
515
|
-
return true;
|
|
601
|
+
// src/errors.ts
|
|
602
|
+
import { parseStack } from "@guidelytech/shared/runtime";
|
|
603
|
+
var MAX_FRAMES_BYTES = 6e3;
|
|
604
|
+
function fitFrames(frames) {
|
|
605
|
+
const out = frames.slice(0, 30);
|
|
606
|
+
while (out.length > 1 && JSON.stringify(out).length > MAX_FRAMES_BYTES) out.pop();
|
|
607
|
+
return out;
|
|
608
|
+
}
|
|
609
|
+
function describe(value) {
|
|
610
|
+
if (typeof value === "string") return value;
|
|
611
|
+
try {
|
|
612
|
+
return JSON.stringify(value) ?? String(value);
|
|
613
|
+
} catch {
|
|
614
|
+
return String(value);
|
|
516
615
|
}
|
|
517
616
|
}
|
|
518
|
-
function
|
|
519
|
-
|
|
520
|
-
const
|
|
521
|
-
|
|
522
|
-
|
|
617
|
+
function exceptionFromError(error, source, handled, fallbackType = "Error") {
|
|
618
|
+
const isError = error instanceof Error || !!error && typeof error === "object" && typeof error.message === "string" && typeof error.stack === "string";
|
|
619
|
+
const e = error;
|
|
620
|
+
return {
|
|
621
|
+
type: isError && e.name || fallbackType,
|
|
622
|
+
message: (isError ? e.message : describe(error)).slice(0, 1e3),
|
|
623
|
+
frames: isError ? fitFrames(parseStack(e.stack)) : [],
|
|
624
|
+
source,
|
|
625
|
+
handled
|
|
626
|
+
};
|
|
523
627
|
}
|
|
524
|
-
function
|
|
525
|
-
const
|
|
526
|
-
|
|
527
|
-
|
|
628
|
+
function errorEventToException(e) {
|
|
629
|
+
const target = e.target;
|
|
630
|
+
if (target && typeof Element !== "undefined" && target instanceof Element) return null;
|
|
631
|
+
if (e.error != null) return exceptionFromError(e.error, "onerror", false);
|
|
632
|
+
const message = e.message || "";
|
|
633
|
+
if (!message || /^script error\.?$/i.test(message.trim())) return null;
|
|
634
|
+
const m = /^(?:Uncaught )?([A-Z]\w*(?:Error|Exception)):\s*([\s\S]*)$/.exec(message);
|
|
635
|
+
return {
|
|
636
|
+
type: m?.[1] ?? "Error",
|
|
637
|
+
message: (m?.[2] ?? message).slice(0, 1e3),
|
|
638
|
+
frames: e.filename ? [{ file: e.filename.slice(0, 500), line: e.lineno || void 0, col: e.colno || void 0 }] : [],
|
|
639
|
+
source: "onerror",
|
|
640
|
+
handled: false
|
|
641
|
+
};
|
|
528
642
|
}
|
|
529
|
-
function
|
|
530
|
-
|
|
531
|
-
if (!s) return true;
|
|
532
|
-
if (s.startAt && now < Date.parse(s.startAt)) return false;
|
|
533
|
-
if (s.endAt && now > Date.parse(s.endAt)) return false;
|
|
534
|
-
return true;
|
|
643
|
+
function rejectionToException(e) {
|
|
644
|
+
return exceptionFromError(e.reason, "unhandledrejection", false, "UnhandledRejection");
|
|
535
645
|
}
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
const u = new URL(`${this.baseUrl.replace(/\/$/, "")}/api/v1/sdk${path}`);
|
|
546
|
-
u.searchParams.set("key", this.key);
|
|
547
|
-
for (const [k, v] of Object.entries(params)) if (v != null) u.searchParams.set(k, v);
|
|
548
|
-
return u.toString();
|
|
549
|
-
}
|
|
550
|
-
get fetcher() {
|
|
551
|
-
if (this.fetchImpl) return this.fetchImpl;
|
|
552
|
-
return typeof fetch === "function" ? fetch.bind(globalThis) : null;
|
|
553
|
-
}
|
|
554
|
-
async request(method, path, options = {}) {
|
|
555
|
-
const f = this.fetcher;
|
|
556
|
-
if (!f) throw new Error("fetch is not available");
|
|
557
|
-
const headers = { ...options.headers, "X-Guidely-Key": this.key, Accept: "application/json" };
|
|
558
|
-
if (options.body !== void 0) headers["Content-Type"] = "application/json";
|
|
559
|
-
if (options.etag) headers["If-None-Match"] = options.etag;
|
|
560
|
-
const ac = typeof AbortController !== "undefined" ? new AbortController() : null;
|
|
561
|
-
const timer = ac ? setTimeout(() => ac.abort(), options.timeoutMs ?? 1e4) : null;
|
|
562
|
-
try {
|
|
563
|
-
const res = await f(this.url(path, options.params), {
|
|
564
|
-
method,
|
|
565
|
-
headers,
|
|
566
|
-
body: options.body === void 0 ? void 0 : JSON.stringify(options.body),
|
|
567
|
-
keepalive: options.keepalive,
|
|
568
|
-
signal: ac?.signal,
|
|
569
|
-
credentials: "omit",
|
|
570
|
-
mode: "cors"
|
|
571
|
-
});
|
|
572
|
-
let data = null;
|
|
573
|
-
if (res.status !== 304 && res.status !== 204) {
|
|
574
|
-
try {
|
|
575
|
-
data = await res.json();
|
|
576
|
-
} catch {
|
|
577
|
-
data = null;
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
return { status: res.status, ok: res.ok, data, etag: res.headers?.get?.("etag") ?? null };
|
|
581
|
-
} finally {
|
|
582
|
-
if (timer) clearTimeout(timer);
|
|
583
|
-
}
|
|
584
|
-
}
|
|
585
|
-
get(path, options) {
|
|
586
|
-
return this.request("GET", path, options);
|
|
587
|
-
}
|
|
588
|
-
post(path, body, options) {
|
|
589
|
-
return this.request("POST", path, { body, ...options });
|
|
590
|
-
}
|
|
591
|
-
/**
|
|
592
|
-
* POST that may answer with server-sent events. Each event is passed to `onEvent`; a JSON answer is
|
|
593
|
-
* returned instead. No timeout: Scout replies can take a while and the server keeps the stream alive.
|
|
594
|
-
*/
|
|
595
|
-
async stream(path, body, onEvent, headers = {}) {
|
|
596
|
-
const f = this.fetcher;
|
|
597
|
-
if (!f) throw new Error("fetch is not available");
|
|
598
|
-
const res = await f(this.url(path), {
|
|
599
|
-
method: "POST",
|
|
600
|
-
headers: { ...headers, "X-Guidely-Key": this.key, Accept: "text/event-stream, application/json", "Content-Type": "application/json" },
|
|
601
|
-
body: JSON.stringify(body),
|
|
602
|
-
credentials: "omit",
|
|
603
|
-
mode: "cors"
|
|
604
|
-
});
|
|
605
|
-
const type = res.headers?.get?.("content-type") ?? "";
|
|
606
|
-
if (!type.includes("text/event-stream") || !res.body) {
|
|
607
|
-
let data = null;
|
|
608
|
-
try {
|
|
609
|
-
data = await res.json();
|
|
610
|
-
} catch {
|
|
611
|
-
data = null;
|
|
612
|
-
}
|
|
613
|
-
return { status: res.status, ok: res.ok, data, etag: null };
|
|
614
|
-
}
|
|
615
|
-
await readEventStream(res.body, onEvent);
|
|
616
|
-
return { status: res.status, ok: res.ok, data: null, etag: null };
|
|
617
|
-
}
|
|
618
|
-
/** `navigator.sendBeacon` when available (page unload); returns false when it could not be queued. */
|
|
619
|
-
beacon(path, body) {
|
|
620
|
-
try {
|
|
621
|
-
if (typeof navigator === "undefined" || typeof navigator.sendBeacon !== "function") return false;
|
|
622
|
-
const blob = new Blob([JSON.stringify(body)], { type: "text/plain;charset=UTF-8" });
|
|
623
|
-
return navigator.sendBeacon(this.url(path), blob);
|
|
624
|
-
} catch {
|
|
625
|
-
return false;
|
|
626
|
-
}
|
|
627
|
-
}
|
|
628
|
-
};
|
|
629
|
-
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
630
|
-
function backoffMs(attempt, baseMs = 1e3, maxMs = 3e4) {
|
|
631
|
-
return Math.min(maxMs, baseMs * 2 ** Math.max(0, attempt));
|
|
632
|
-
}
|
|
633
|
-
async function postWithRetry(api, path, body, attempts = 3, baseMs = 500) {
|
|
634
|
-
for (let i = 0; i < attempts; i++) {
|
|
635
|
-
try {
|
|
636
|
-
const res = await api.post(path, body);
|
|
637
|
-
if (res.ok || res.status >= 400 && res.status < 500 && res.status !== 429) return res;
|
|
638
|
-
} catch {
|
|
639
|
-
}
|
|
640
|
-
if (i < attempts - 1) await sleep(backoffMs(i, baseMs, 8e3));
|
|
646
|
+
var GUIDELY_SCRIPT = /\/guidely(-recorder)?(\.min)?\.js$/i;
|
|
647
|
+
function isGuidelyException(props) {
|
|
648
|
+
if (props.message.startsWith("[Guidely]")) return true;
|
|
649
|
+
const file = props.frames[0]?.file;
|
|
650
|
+
if (!file) return false;
|
|
651
|
+
try {
|
|
652
|
+
return GUIDELY_SCRIPT.test(new URL(file).pathname);
|
|
653
|
+
} catch {
|
|
654
|
+
return GUIDELY_SCRIPT.test(file.replace(/[?#].*$/, ""));
|
|
641
655
|
}
|
|
642
|
-
return null;
|
|
643
656
|
}
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
const
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
else if (field === "data") data.push(value);
|
|
658
|
-
}
|
|
659
|
-
if (!data.length) return;
|
|
660
|
-
try {
|
|
661
|
-
onEvent(event, JSON.parse(data.join("\n")));
|
|
662
|
-
} catch {
|
|
663
|
-
}
|
|
664
|
-
};
|
|
665
|
-
for (; ; ) {
|
|
666
|
-
const { done, value } = await reader.read();
|
|
667
|
-
if (value) buffer += decoder.decode(value, { stream: true }).replace(/\r\n?/g, "\n");
|
|
668
|
-
let sep;
|
|
669
|
-
while ((sep = buffer.indexOf("\n\n")) >= 0) {
|
|
670
|
-
dispatch(buffer.slice(0, sep));
|
|
671
|
-
buffer = buffer.slice(sep + 2);
|
|
672
|
-
}
|
|
673
|
-
if (done) break;
|
|
657
|
+
var ExceptionThrottle = class {
|
|
658
|
+
constructor(max = 5) {
|
|
659
|
+
__publicField(this, "max", max);
|
|
660
|
+
__publicField(this, "seen", /* @__PURE__ */ new Map());
|
|
661
|
+
}
|
|
662
|
+
allow(props) {
|
|
663
|
+
const top = props.frames[0];
|
|
664
|
+
const key = `${props.type}|${props.message}|${top ? `${top.file}:${top.line}:${top.col}` : ""}`;
|
|
665
|
+
const n = this.seen.get(key) ?? 0;
|
|
666
|
+
if (n >= this.max) return false;
|
|
667
|
+
if (this.seen.size > 500) this.seen.clear();
|
|
668
|
+
this.seen.set(key, n + 1);
|
|
669
|
+
return true;
|
|
674
670
|
}
|
|
675
|
-
|
|
676
|
-
}
|
|
671
|
+
};
|
|
677
672
|
|
|
678
673
|
// src/messenger-api.ts
|
|
679
674
|
var MAX_STORED = 50;
|
|
@@ -834,124 +829,61 @@ function watchNavigation(listener, debounceMs = 50) {
|
|
|
834
829
|
};
|
|
835
830
|
}
|
|
836
831
|
|
|
837
|
-
// src/
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
__publicField(this, "
|
|
849
|
-
__publicField(this, "
|
|
850
|
-
__publicField(this, "
|
|
851
|
-
__publicField(this, "
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
this.
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
if (this.events.some((e) => e.eventId === event.eventId)) return;
|
|
870
|
-
this.events.push(event);
|
|
871
|
-
if (this.events.length > this.maxQueue) this.events.splice(0, this.events.length - this.maxQueue);
|
|
872
|
-
this.persist();
|
|
873
|
-
this.schedule();
|
|
874
|
-
}
|
|
875
|
-
persist() {
|
|
876
|
-
writeJson(this.opts.store, this.opts.storageKey, this.events);
|
|
877
|
-
}
|
|
878
|
-
schedule() {
|
|
879
|
-
if (this.timer || this.retryTimer || this.destroyed) return;
|
|
880
|
-
this.timer = setTimeout(() => {
|
|
881
|
-
this.timer = null;
|
|
882
|
-
void this.flush();
|
|
883
|
-
}, this.flushIntervalMs);
|
|
884
|
-
}
|
|
885
|
-
/** Sends queued events now (one batch at a time). Safe to call at any time. */
|
|
886
|
-
async flush() {
|
|
887
|
-
if (this.destroyed || this.inflight || !this.events.length || !this.opts.enabled()) return;
|
|
888
|
-
if (this.timer) {
|
|
889
|
-
clearTimeout(this.timer);
|
|
890
|
-
this.timer = null;
|
|
891
|
-
}
|
|
892
|
-
this.inflight = true;
|
|
893
|
-
const batch = this.events.slice(0, this.batchSize);
|
|
894
|
-
this.inflightIds = new Set(batch.map((e) => e.eventId));
|
|
895
|
-
let ok = false;
|
|
896
|
-
let retry = true;
|
|
832
|
+
// src/page-tracking.ts
|
|
833
|
+
function scrollDepth() {
|
|
834
|
+
const de = document.documentElement;
|
|
835
|
+
const height = Math.max(de?.scrollHeight || 0, document.body?.scrollHeight || 0);
|
|
836
|
+
const view = window.innerHeight || de?.clientHeight || 0;
|
|
837
|
+
if (height <= 0 || height <= view) return 100;
|
|
838
|
+
const y = window.scrollY || de?.scrollTop || 0;
|
|
839
|
+
return Math.max(0, Math.min(100, Math.round((y + view) / height * 100)));
|
|
840
|
+
}
|
|
841
|
+
var PageTracker = class {
|
|
842
|
+
constructor() {
|
|
843
|
+
__publicField(this, "url", "");
|
|
844
|
+
__publicField(this, "visibleMs", 0);
|
|
845
|
+
__publicField(this, "visibleSince", null);
|
|
846
|
+
__publicField(this, "maxScroll", 0);
|
|
847
|
+
}
|
|
848
|
+
/** Starts measuring a new page. */
|
|
849
|
+
start(url, now = Date.now()) {
|
|
850
|
+
this.url = url;
|
|
851
|
+
this.visibleMs = 0;
|
|
852
|
+
this.visibleSince = typeof document !== "undefined" && document.visibilityState === "hidden" ? null : now;
|
|
853
|
+
this.maxScroll = 0;
|
|
854
|
+
this.measureScroll();
|
|
855
|
+
}
|
|
856
|
+
/** Pauses / resumes the visible-time clock. */
|
|
857
|
+
setHidden(hidden, now = Date.now()) {
|
|
858
|
+
if (hidden && this.visibleSince !== null) {
|
|
859
|
+
this.visibleMs += Math.max(0, now - this.visibleSince);
|
|
860
|
+
this.visibleSince = null;
|
|
861
|
+
} else if (!hidden && this.visibleSince === null) this.visibleSince = now;
|
|
862
|
+
}
|
|
863
|
+
measureScroll() {
|
|
897
864
|
try {
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
retry = res.retry ?? !res.ok;
|
|
901
|
-
} catch (err) {
|
|
902
|
-
this.opts.onError?.(err);
|
|
903
|
-
}
|
|
904
|
-
this.inflight = false;
|
|
905
|
-
this.inflightIds.clear();
|
|
906
|
-
if (this.destroyed) return;
|
|
907
|
-
if (ok || !retry) {
|
|
908
|
-
const ids = new Set(batch.map((e) => e.eventId));
|
|
909
|
-
this.events = this.events.filter((e) => !ids.has(e.eventId));
|
|
910
|
-
this.attempt = 0;
|
|
911
|
-
this.persist();
|
|
912
|
-
if (this.events.length) this.schedule();
|
|
913
|
-
return;
|
|
914
|
-
}
|
|
915
|
-
this.attempt += 1;
|
|
916
|
-
if (this.attempt >= this.maxAttempts) {
|
|
917
|
-
this.attempt = 0;
|
|
918
|
-
return;
|
|
919
|
-
}
|
|
920
|
-
this.retryTimer = setTimeout(() => {
|
|
921
|
-
this.retryTimer = null;
|
|
922
|
-
void this.flush();
|
|
923
|
-
}, backoffMs(this.attempt - 1));
|
|
924
|
-
}
|
|
925
|
-
/** Best-effort synchronous flush on unload using `sendBeacon`. */
|
|
926
|
-
flushSync() {
|
|
927
|
-
if (this.destroyed || !this.opts.enabled() || !this.opts.beacon) return;
|
|
928
|
-
const batch = this.events.filter((e) => !this.inflightIds.has(e.eventId)).slice(0, this.batchSize);
|
|
929
|
-
if (!batch.length) return;
|
|
930
|
-
if (this.opts.beacon(batch)) {
|
|
931
|
-
const ids = new Set(batch.map((e) => e.eventId));
|
|
932
|
-
this.events = this.events.filter((e) => !ids.has(e.eventId));
|
|
933
|
-
this.persist();
|
|
865
|
+
this.maxScroll = Math.max(this.maxScroll, scrollDepth());
|
|
866
|
+
} catch {
|
|
934
867
|
}
|
|
935
868
|
}
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
this.
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
this.destroyed = true;
|
|
942
|
-
if (this.timer) clearTimeout(this.timer);
|
|
943
|
-
if (this.retryTimer) clearTimeout(this.retryTimer);
|
|
944
|
-
this.timer = null;
|
|
945
|
-
this.retryTimer = null;
|
|
946
|
-
this.persist();
|
|
869
|
+
/** Final numbers for the current page. */
|
|
870
|
+
leave(now = Date.now()) {
|
|
871
|
+
this.measureScroll();
|
|
872
|
+
const running = this.visibleSince !== null ? Math.max(0, now - this.visibleSince) : 0;
|
|
873
|
+
return { url: this.url, durationMs: Math.round(this.visibleMs + running), scrollDepth: this.maxScroll };
|
|
947
874
|
}
|
|
948
875
|
};
|
|
949
876
|
|
|
877
|
+
// src/load-recorder.ts
|
|
878
|
+
var loadRecorder = async () => (await import("./recorder.js")).recorder;
|
|
879
|
+
|
|
950
880
|
// src/client.ts
|
|
951
881
|
var SDK_NAME = "@guidelytech/sdk-core";
|
|
952
|
-
var SDK_VERSION = "0.
|
|
882
|
+
var SDK_VERSION = "0.4.0";
|
|
953
883
|
var DEFAULT_API_URL = "https://api.guidely.app";
|
|
954
884
|
var PAGE_SCOPED = /* @__PURE__ */ new Set(["page_load", "url_match", "element_click", "element_visible", "delay"]);
|
|
885
|
+
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
886
|
+
var MAX_SESSION_EXCEPTIONS = 100;
|
|
955
887
|
function isBrowser() {
|
|
956
888
|
return typeof window !== "undefined" && typeof document !== "undefined";
|
|
957
889
|
}
|
|
@@ -961,6 +893,9 @@ function nowIso() {
|
|
|
961
893
|
function emptyState() {
|
|
962
894
|
return { guides: {}, checklists: {}, surveysSubmitted: [], audienceIds: [] };
|
|
963
895
|
}
|
|
896
|
+
function newSession(now = Date.now()) {
|
|
897
|
+
return { id: `s_${randomId(16)}`, startedAt: new Date(now).toISOString(), shownGuides: [], events: [], lastActivityAt: now };
|
|
898
|
+
}
|
|
964
899
|
var GuidelyClient = class _GuidelyClient {
|
|
965
900
|
constructor(options) {
|
|
966
901
|
__publicField(this, "options");
|
|
@@ -998,6 +933,19 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
998
933
|
__publicField(this, "lastAudiences", /* @__PURE__ */ new Set());
|
|
999
934
|
/** Set once the app calls identify()/reset(); bootstrap then leaves identity alone instead of re-identifying. */
|
|
1000
935
|
__publicField(this, "identitySettled", false);
|
|
936
|
+
// Product analytics (autocapture, error tracking, session replay)
|
|
937
|
+
/** Created once the recorder chunk has loaded (it carries the replay controller). */
|
|
938
|
+
__publicField(this, "replay", null);
|
|
939
|
+
__publicField(this, "replayLoading", false);
|
|
940
|
+
__publicField(this, "pageTracker", new PageTracker());
|
|
941
|
+
__publicField(this, "rage", new RageClickDetector());
|
|
942
|
+
__publicField(this, "heatmap", new HeatmapBuffer());
|
|
943
|
+
__publicField(this, "exceptionThrottle", new ExceptionThrottle());
|
|
944
|
+
/** Exceptions seen before the config was loaded (reported once it is known whether the server accepts them). */
|
|
945
|
+
__publicField(this, "earlyExceptions", []);
|
|
946
|
+
__publicField(this, "initialPageViewSent", false);
|
|
947
|
+
__publicField(this, "pageViewUrl", null);
|
|
948
|
+
__publicField(this, "pageLeft", false);
|
|
1001
949
|
__publicField(this, "customListeners", /* @__PURE__ */ new Map());
|
|
1002
950
|
__publicField(this, "eventBus", {
|
|
1003
951
|
on: (name, cb) => {
|
|
@@ -1040,7 +988,7 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
1040
988
|
this.state = { ...emptyState(), ...readJson(this.store, KEYS.state(this.options.projectId), {}) };
|
|
1041
989
|
this.user = readJson(this.store, KEYS.user(this.options.projectId), null);
|
|
1042
990
|
const session = readJson(this.sessionStore, KEYS.session, null);
|
|
1043
|
-
this.session = session && session.id ? session :
|
|
991
|
+
this.session = session && session.id ? session : newSession();
|
|
1044
992
|
this.persistSession();
|
|
1045
993
|
this.lastUrl = location.href;
|
|
1046
994
|
this.lastAudiences = new Set(this.state.audienceIds);
|
|
@@ -1074,10 +1022,11 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
1074
1022
|
return this.init();
|
|
1075
1023
|
}
|
|
1076
1024
|
async bootstrap() {
|
|
1025
|
+
this.wireErrorListeners();
|
|
1077
1026
|
try {
|
|
1078
1027
|
const cached = readJson(this.store, KEYS.config(this.options.projectId), null);
|
|
1079
1028
|
this.etag = this.store.get(KEYS.etag(this.options.projectId));
|
|
1080
|
-
if (cached && cached.guides) this.config = cached;
|
|
1029
|
+
if (cached && cached.guides) this.config = webOnly(cached);
|
|
1081
1030
|
await this.fetchConfig();
|
|
1082
1031
|
if (this.destroyed) return;
|
|
1083
1032
|
this.wireListeners();
|
|
@@ -1093,6 +1042,9 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
1093
1042
|
this.resumeActiveGuide();
|
|
1094
1043
|
this.evaluate("ready");
|
|
1095
1044
|
if (!this.options.disableAuthoring) this.maybeStartAuthoring();
|
|
1045
|
+
this.pageTracker.start(location.href);
|
|
1046
|
+
this.touchSession();
|
|
1047
|
+
this.syncCapture();
|
|
1096
1048
|
} catch (err) {
|
|
1097
1049
|
this.reportError(err);
|
|
1098
1050
|
this.isReady = true;
|
|
@@ -1101,10 +1053,10 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
1101
1053
|
}
|
|
1102
1054
|
async fetchConfig() {
|
|
1103
1055
|
try {
|
|
1104
|
-
const res = await this.api.get("/config", { etag: this.etag });
|
|
1056
|
+
const res = await this.api.get("/config", { etag: this.etag, params: { platform: "web" } });
|
|
1105
1057
|
if (res.status === 304) return true;
|
|
1106
1058
|
if (res.ok && res.data && Array.isArray(res.data.guides)) {
|
|
1107
|
-
this.config = res.data;
|
|
1059
|
+
this.config = webOnly(res.data);
|
|
1108
1060
|
this.etag = res.etag;
|
|
1109
1061
|
writeJson(this.store, KEYS.config(this.options.projectId), res.data);
|
|
1110
1062
|
if (res.etag) this.store.set(KEYS.etag(this.options.projectId), res.etag);
|
|
@@ -1127,14 +1079,62 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
1127
1079
|
const onClick = (e) => this.handleDocumentClick(e);
|
|
1128
1080
|
doc.addEventListener("click", onClick, true);
|
|
1129
1081
|
this.cleanups.push(() => doc.removeEventListener("click", onClick, true));
|
|
1130
|
-
const
|
|
1131
|
-
|
|
1082
|
+
const onVisibility = () => {
|
|
1083
|
+
const hidden = doc.visibilityState === "hidden";
|
|
1084
|
+
this.pageTracker.setHidden(hidden);
|
|
1085
|
+
if (!hidden) return;
|
|
1086
|
+
this.replay?.flushOnHide();
|
|
1087
|
+
this.queue.flushSync();
|
|
1088
|
+
};
|
|
1089
|
+
const onPageHide = () => {
|
|
1090
|
+
this.flushHeatmap();
|
|
1091
|
+
if (!this.pageLeft) this.leavePage(null);
|
|
1092
|
+
this.pageLeft = true;
|
|
1093
|
+
this.replay?.flushOnHide();
|
|
1094
|
+
this.queue.flushSync();
|
|
1095
|
+
};
|
|
1096
|
+
const onPageShow = (e) => {
|
|
1097
|
+
if (!e.persisted || !this.pageLeft) return;
|
|
1098
|
+
this.pageLeft = false;
|
|
1099
|
+
this.pageTracker.start(location.href);
|
|
1132
1100
|
};
|
|
1133
|
-
|
|
1134
|
-
|
|
1101
|
+
let lastScroll = 0;
|
|
1102
|
+
const onScroll = () => {
|
|
1103
|
+
const now = Date.now();
|
|
1104
|
+
if (now - lastScroll < 250) return;
|
|
1105
|
+
lastScroll = now;
|
|
1106
|
+
this.pageTracker.measureScroll();
|
|
1107
|
+
this.touchSession();
|
|
1108
|
+
};
|
|
1109
|
+
const onKey = () => this.touchSession();
|
|
1110
|
+
let move = null;
|
|
1111
|
+
let moveTimer = null;
|
|
1112
|
+
const onMove = (e) => {
|
|
1113
|
+
move = e;
|
|
1114
|
+
moveTimer ?? (moveTimer = setTimeout(() => {
|
|
1115
|
+
moveTimer = null;
|
|
1116
|
+
if (move) this.sampleMove(move);
|
|
1117
|
+
move = null;
|
|
1118
|
+
}, 200));
|
|
1119
|
+
};
|
|
1120
|
+
if (win.matchMedia?.("(pointer: fine)").matches) {
|
|
1121
|
+
doc.addEventListener("mousemove", onMove, { passive: true });
|
|
1122
|
+
this.cleanups.push(() => {
|
|
1123
|
+
doc.removeEventListener("mousemove", onMove);
|
|
1124
|
+
if (moveTimer) clearTimeout(moveTimer);
|
|
1125
|
+
});
|
|
1126
|
+
}
|
|
1127
|
+
doc.addEventListener("visibilitychange", onVisibility);
|
|
1128
|
+
win.addEventListener("pagehide", onPageHide);
|
|
1129
|
+
win.addEventListener("pageshow", onPageShow);
|
|
1130
|
+
win.addEventListener("scroll", onScroll, { passive: true });
|
|
1131
|
+
doc.addEventListener("keydown", onKey, true);
|
|
1135
1132
|
this.cleanups.push(() => {
|
|
1136
|
-
doc.removeEventListener("visibilitychange",
|
|
1137
|
-
win.removeEventListener("pagehide",
|
|
1133
|
+
doc.removeEventListener("visibilitychange", onVisibility);
|
|
1134
|
+
win.removeEventListener("pagehide", onPageHide);
|
|
1135
|
+
win.removeEventListener("pageshow", onPageShow);
|
|
1136
|
+
win.removeEventListener("scroll", onScroll);
|
|
1137
|
+
doc.removeEventListener("keydown", onKey, true);
|
|
1138
1138
|
});
|
|
1139
1139
|
const refresh = setInterval(() => void this.refresh(), this.options.configRefreshMs);
|
|
1140
1140
|
this.cleanups.push(() => clearInterval(refresh));
|
|
@@ -1152,6 +1152,18 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
1152
1152
|
win.addEventListener("message", onMessage);
|
|
1153
1153
|
this.cleanups.push(() => win.removeEventListener("message", onMessage));
|
|
1154
1154
|
}
|
|
1155
|
+
/** Uncaught errors + unhandled rejections; attached before the config loads so boot errors are kept. */
|
|
1156
|
+
wireErrorListeners() {
|
|
1157
|
+
const win = window;
|
|
1158
|
+
const onError = (e) => this.captureGlobalException(errorEventToException(e));
|
|
1159
|
+
const onRejection = (e) => this.captureGlobalException(rejectionToException(e));
|
|
1160
|
+
win.addEventListener("error", onError);
|
|
1161
|
+
win.addEventListener("unhandledrejection", onRejection);
|
|
1162
|
+
this.cleanups.push(() => {
|
|
1163
|
+
win.removeEventListener("error", onError);
|
|
1164
|
+
win.removeEventListener("unhandledrejection", onRejection);
|
|
1165
|
+
});
|
|
1166
|
+
}
|
|
1155
1167
|
/** Re-fetches the published config and re-evaluates triggers. */
|
|
1156
1168
|
async refresh() {
|
|
1157
1169
|
if (this.destroyed) return;
|
|
@@ -1161,6 +1173,7 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
1161
1173
|
this.renderPersistentWidgets();
|
|
1162
1174
|
this.evaluate("ready");
|
|
1163
1175
|
}
|
|
1176
|
+
this.syncCapture();
|
|
1164
1177
|
}
|
|
1165
1178
|
/** Tears everything down: guides, widgets, listeners, timers, host element. Re-init is allowed via a new client. */
|
|
1166
1179
|
destroy() {
|
|
@@ -1184,6 +1197,8 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
1184
1197
|
this.cleanups.splice(0).forEach((fn) => fn());
|
|
1185
1198
|
this.authoring?.stop();
|
|
1186
1199
|
this.authoring = null;
|
|
1200
|
+
this.replay?.destroy();
|
|
1201
|
+
this.replay = null;
|
|
1187
1202
|
this.queue.flushSync();
|
|
1188
1203
|
this.queue.destroy();
|
|
1189
1204
|
this.emitter.clear();
|
|
@@ -1270,9 +1285,11 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
1270
1285
|
this.store.set(KEYS.anon, this.anonymousId);
|
|
1271
1286
|
this.state = emptyState();
|
|
1272
1287
|
this.persistState();
|
|
1273
|
-
this.session =
|
|
1288
|
+
this.session = newSession();
|
|
1274
1289
|
this.persistSession();
|
|
1275
1290
|
this.sessionStore.remove(KEYS.active);
|
|
1291
|
+
this.replay?.stop();
|
|
1292
|
+
this.syncCapture();
|
|
1276
1293
|
this.lastAudiences = /* @__PURE__ */ new Set();
|
|
1277
1294
|
this.firedDelays.clear();
|
|
1278
1295
|
this.widgets.forEach((w) => w.destroy());
|
|
@@ -1293,6 +1310,8 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
1293
1310
|
this.consent = consent;
|
|
1294
1311
|
if (consent === "denied") this.queue.clear();
|
|
1295
1312
|
else if (consent === "granted") void this.queue.flush();
|
|
1313
|
+
if (consent !== "granted") this.replay?.stop({ discard: true });
|
|
1314
|
+
this.syncCapture();
|
|
1296
1315
|
}
|
|
1297
1316
|
analyticsEnabled() {
|
|
1298
1317
|
return this.options.analytics && this.consent === "granted" && (this.config?.settings?.analyticsEnabled ?? true);
|
|
@@ -1302,6 +1321,7 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
1302
1321
|
}
|
|
1303
1322
|
enqueue(name, extra = {}) {
|
|
1304
1323
|
if (this.consent === "denied" || !this.options.analytics) return null;
|
|
1324
|
+
if (name !== "page_leave" && name !== "heatmap") this.touchSession();
|
|
1305
1325
|
const event = {
|
|
1306
1326
|
eventId: randomId(24),
|
|
1307
1327
|
name,
|
|
@@ -1315,6 +1335,7 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
1315
1335
|
};
|
|
1316
1336
|
if (extra.properties) event.properties = sanitizeProperties(extra.properties);
|
|
1317
1337
|
this.queue.enqueue(event);
|
|
1338
|
+
this.replay?.addCustomEvent("guidely", { name, customName: event.customName, guideId: event.guideId });
|
|
1318
1339
|
this.emitter.emit("track", event);
|
|
1319
1340
|
return event;
|
|
1320
1341
|
}
|
|
@@ -1339,14 +1360,18 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
1339
1360
|
/** Records a page view and re-evaluates triggers. Called automatically on SPA navigation. */
|
|
1340
1361
|
page(url = location.href) {
|
|
1341
1362
|
if (this.destroyed) return;
|
|
1342
|
-
this.
|
|
1363
|
+
this.leavePage(url);
|
|
1364
|
+
this.trackPageView(url);
|
|
1343
1365
|
this.handleNavigation(url, true);
|
|
1344
1366
|
}
|
|
1345
1367
|
handleNavigation(url, explicit = false) {
|
|
1346
1368
|
if (this.destroyed) return;
|
|
1347
1369
|
const changed = url !== this.lastUrl;
|
|
1348
1370
|
this.lastUrl = url;
|
|
1349
|
-
if (!explicit && changed)
|
|
1371
|
+
if (!explicit && changed) {
|
|
1372
|
+
this.leavePage(url);
|
|
1373
|
+
this.trackPageView(url);
|
|
1374
|
+
}
|
|
1350
1375
|
this.completeChecklistTasksBy("url_visited", url);
|
|
1351
1376
|
this.active?.controller?.resume();
|
|
1352
1377
|
if (this.active?.controller) {
|
|
@@ -1356,6 +1381,11 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
1356
1381
|
if (g.config.urlPatterns.length && !g.config.urlPatterns.some((p) => urlMatches(p, url))) this.active.controller.destroy();
|
|
1357
1382
|
}
|
|
1358
1383
|
}
|
|
1384
|
+
this.widgets.forEach((w, id) => {
|
|
1385
|
+
if (!w.controller || w.manual) return;
|
|
1386
|
+
const g = this.findGuide(id);
|
|
1387
|
+
if (g?.config.urlPatterns.length && !g.config.urlPatterns.some((p) => urlMatches(p, url))) w.controller.destroy();
|
|
1388
|
+
});
|
|
1359
1389
|
this.firedDelays.clear();
|
|
1360
1390
|
if (this.isReady) this.evaluate("navigation");
|
|
1361
1391
|
if (changed) void this.queue.flush();
|
|
@@ -1363,10 +1393,255 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
1363
1393
|
handleDocumentClick(e) {
|
|
1364
1394
|
const target = e.target;
|
|
1365
1395
|
if (!target || typeof target.closest !== "function") return;
|
|
1396
|
+
this.autocaptureClick(e, target);
|
|
1366
1397
|
this.completeChecklistTasksBy("click", target);
|
|
1367
1398
|
if (this.isReady) this.evaluate("click", { target });
|
|
1368
1399
|
}
|
|
1369
1400
|
// ---------------------------------------------------------------------------
|
|
1401
|
+
// Product analytics: sessions, page views, clicks, errors, session replay
|
|
1402
|
+
// ---------------------------------------------------------------------------
|
|
1403
|
+
/** Records activity; starts a new session after `sessionTimeoutMinutes` of inactivity or 24h. */
|
|
1404
|
+
touchSession() {
|
|
1405
|
+
const now = Date.now();
|
|
1406
|
+
const s = this.session;
|
|
1407
|
+
const timeout = Math.max(1, this.config?.settings?.sessionTimeoutMinutes ?? 30) * 6e4;
|
|
1408
|
+
const last = s.lastActivityAt ?? now;
|
|
1409
|
+
if (now - last > timeout || now - (Date.parse(s.startedAt) || now) > DAY_MS) return this.rotateSession(now);
|
|
1410
|
+
if (now - (s.lastActivityAt ?? 0) < 1e3) return;
|
|
1411
|
+
s.lastActivityAt = now;
|
|
1412
|
+
this.persistSession();
|
|
1413
|
+
}
|
|
1414
|
+
rotateSession(now) {
|
|
1415
|
+
this.session = newSession(now);
|
|
1416
|
+
this.persistSession();
|
|
1417
|
+
this.log("session rotated", this.session.id);
|
|
1418
|
+
if (this.replay?.activeSessionId) this.replay.stop();
|
|
1419
|
+
this.syncReplay();
|
|
1420
|
+
}
|
|
1421
|
+
/** Autocapture, error tracking and replay need analytics on, consent granted and no authoring session. */
|
|
1422
|
+
captureAllowed() {
|
|
1423
|
+
return !this.destroyed && this.analyticsEnabled() && !this.authoring?.isActive();
|
|
1424
|
+
}
|
|
1425
|
+
/** A capability is on when the project enables it (absent on old servers → off) and no option turns it off. */
|
|
1426
|
+
autocaptureOn(key) {
|
|
1427
|
+
const o = this.options.autocapture;
|
|
1428
|
+
return !!this.config?.settings?.autocapture?.[key] && o !== false && !(typeof o === "object" && o[key] === false) && this.captureAllowed();
|
|
1429
|
+
}
|
|
1430
|
+
/** Page views on SPA navigation predate autocapture: only an explicit opt-out stops them. */
|
|
1431
|
+
pageviewsOn() {
|
|
1432
|
+
const o = this.options.autocapture;
|
|
1433
|
+
return this.config?.settings?.autocapture?.pageviews !== false && o !== false && !(typeof o === "object" && o.pageviews === false) && !this.authoring?.isActive();
|
|
1434
|
+
}
|
|
1435
|
+
trackPageView(url) {
|
|
1436
|
+
if (url === this.pageViewUrl || !this.pageviewsOn()) return;
|
|
1437
|
+
this.pageViewUrl = url;
|
|
1438
|
+
this.touchSession();
|
|
1439
|
+
const collect = this.collectUrls();
|
|
1440
|
+
const device = detectDevice(navigator.userAgent, window.innerWidth);
|
|
1441
|
+
const props = { deviceType: device.type, browser: device.browser, os: device.os, screenWidth: window.screen?.width, viewportWidth: window.innerWidth };
|
|
1442
|
+
if (collect) {
|
|
1443
|
+
props.path = pathOf(url);
|
|
1444
|
+
props.title = (document.title || "").slice(0, 200);
|
|
1445
|
+
const ref = !this.session.pageViews ? externalReferrer() : void 0;
|
|
1446
|
+
if (ref) props.referrer = ref;
|
|
1447
|
+
}
|
|
1448
|
+
this.session.pageViews = (this.session.pageViews ?? 0) + 1;
|
|
1449
|
+
this.persistSession();
|
|
1450
|
+
this.enqueue("page_view", { url: collect ? url : void 0, properties: props });
|
|
1451
|
+
}
|
|
1452
|
+
/** Emits `page_leave` for the page being left and starts measuring `nextUrl` (null: page unload). */
|
|
1453
|
+
leavePage(nextUrl) {
|
|
1454
|
+
const t = this.pageTracker;
|
|
1455
|
+
if (!t.url || t.url === nextUrl) return;
|
|
1456
|
+
this.flushHeatmap();
|
|
1457
|
+
if (!this.pageLeft && this.autocaptureOn("pageLeave")) {
|
|
1458
|
+
const info = t.leave();
|
|
1459
|
+
const collect = this.collectUrls();
|
|
1460
|
+
this.enqueue("page_leave", { url: collect ? info.url : void 0, properties: { ...collect ? { path: pathOf(info.url) } : {}, durationMs: info.durationMs, scrollDepth: info.scrollDepth } });
|
|
1461
|
+
}
|
|
1462
|
+
if (nextUrl !== null) {
|
|
1463
|
+
t.start(nextUrl);
|
|
1464
|
+
this.pageLeft = false;
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
autocaptureClick(e, target) {
|
|
1468
|
+
const clicks = this.autocaptureOn("clicks");
|
|
1469
|
+
const rage = this.autocaptureOn("rageClicks");
|
|
1470
|
+
const heat = this.autocaptureOn("heatmaps");
|
|
1471
|
+
if (!clicks && !rage && !heat || isIgnoredElement(target)) return;
|
|
1472
|
+
this.touchSession();
|
|
1473
|
+
const el = interactiveElement(target);
|
|
1474
|
+
const opts = { mask: this.maskSelector(), maskAll: this.maskAllText(), collectUrls: this.collectUrls() };
|
|
1475
|
+
if (clicks && el) this.enqueue("click", { properties: { ...describeClick(el, e, opts) } });
|
|
1476
|
+
const count = this.rage.add(e.pageX ?? 0, e.pageY ?? 0, Date.now());
|
|
1477
|
+
if (rage && count) this.enqueue("rage_click", { properties: { ...describeClick(el ?? target, e, opts), count } });
|
|
1478
|
+
if (!heat) return;
|
|
1479
|
+
const url = location.href;
|
|
1480
|
+
if (this.heatmap.url !== url) this.flushHeatmap();
|
|
1481
|
+
if (this.heatmap.add(url, heatmapPoint(el ?? target, e, this.rage.burst > 0), this.rage.burst)) this.flushHeatmap();
|
|
1482
|
+
}
|
|
1483
|
+
/** Bins one pointer sample into the heatmap buffer. Not session activity. */
|
|
1484
|
+
sampleMove(e) {
|
|
1485
|
+
const target = e.target;
|
|
1486
|
+
if (document.visibilityState === "hidden" || !target?.closest || !this.autocaptureOn("mouseMovement") || isIgnoredElement(target)) return;
|
|
1487
|
+
const url = location.href;
|
|
1488
|
+
if (this.heatmap.url !== url) this.flushHeatmap();
|
|
1489
|
+
if (this.heatmap.move(url, movementBin(interactiveElement(target) ?? target, e))) this.flushHeatmap();
|
|
1490
|
+
}
|
|
1491
|
+
/** Sends the buffered heatmap clicks as one `heatmap` event attributed to the page they happened on. */
|
|
1492
|
+
flushHeatmap() {
|
|
1493
|
+
const batch = this.heatmap.take();
|
|
1494
|
+
if (!batch || !this.captureAllowed()) return;
|
|
1495
|
+
const collect = this.collectUrls();
|
|
1496
|
+
const de = document.documentElement;
|
|
1497
|
+
const properties = { path: collect ? pathOf(batch.url) : void 0, viewportWidth: window.innerWidth, viewportHeight: window.innerHeight, pageHeight: Math.max(de.scrollHeight, document.body?.scrollHeight || 0), points: batch.points };
|
|
1498
|
+
if (!collect) delete properties.path;
|
|
1499
|
+
this.enqueue("heatmap", { url: collect ? batch.url : void 0, properties });
|
|
1500
|
+
}
|
|
1501
|
+
captureGlobalException(props) {
|
|
1502
|
+
if (!props || isGuidelyException(props) || this.options.captureErrors === false) return;
|
|
1503
|
+
if (!this.config) {
|
|
1504
|
+
if (this.earlyExceptions.length < 10) this.earlyExceptions.push([props, void 0]);
|
|
1505
|
+
return;
|
|
1506
|
+
}
|
|
1507
|
+
this.sendException(props);
|
|
1508
|
+
}
|
|
1509
|
+
/**
|
|
1510
|
+
* Reports a caught error as an `exception` event (`source: "manual"`, `handled: true`), grouped
|
|
1511
|
+
* into an issue on the Errors page. Extra properties are attached to the occurrence. Needs the
|
|
1512
|
+
* project's error tracking on (plus analytics + consent); `captureErrors: false` does not stop it.
|
|
1513
|
+
*/
|
|
1514
|
+
captureException(error, properties) {
|
|
1515
|
+
if (this.destroyed) return;
|
|
1516
|
+
const props = exceptionFromError(error, "manual", true);
|
|
1517
|
+
if (!this.config) {
|
|
1518
|
+
if (this.earlyExceptions.length < 10) this.earlyExceptions.push([props, properties]);
|
|
1519
|
+
return;
|
|
1520
|
+
}
|
|
1521
|
+
this.sendException(props, properties);
|
|
1522
|
+
}
|
|
1523
|
+
sendException(props, extra) {
|
|
1524
|
+
if (!this.config?.settings?.errorTracking?.enabled || !this.captureAllowed() || !this.exceptionThrottle.allow(props)) return;
|
|
1525
|
+
const count = this.session.exceptions ?? 0;
|
|
1526
|
+
if (count >= MAX_SESSION_EXCEPTIONS) return;
|
|
1527
|
+
const properties = { ...props };
|
|
1528
|
+
if (this.options.release) properties.release = String(this.options.release).slice(0, 100);
|
|
1529
|
+
for (const [k, v] of Object.entries(extra ?? {})) if (!(k in properties)) properties[k] = v;
|
|
1530
|
+
this.enqueue("exception", { properties });
|
|
1531
|
+
this.session.exceptions = count + 1;
|
|
1532
|
+
this.persistSession();
|
|
1533
|
+
}
|
|
1534
|
+
maskSelector() {
|
|
1535
|
+
return validSelectors([...DEFAULT_MASK_SELECTORS, ...this.config?.settings?.maskSelectors || [], ...this.options.maskSelectors || []]).join(",");
|
|
1536
|
+
}
|
|
1537
|
+
maskAllText() {
|
|
1538
|
+
const o = this.options.sessionReplay;
|
|
1539
|
+
return !!this.config?.settings?.replay?.maskAllText || typeof o === "object" && !!o.maskAllText;
|
|
1540
|
+
}
|
|
1541
|
+
/** Replay settings for this installation, or null when the project (or an option) disables replay. */
|
|
1542
|
+
replayConfig() {
|
|
1543
|
+
const r = this.config?.settings?.replay;
|
|
1544
|
+
const o = this.options.sessionReplay;
|
|
1545
|
+
if (!r?.enabled || o === false) return null;
|
|
1546
|
+
const opt = typeof o === "object" ? o : {};
|
|
1547
|
+
const extra = opt.blockSelectors || [];
|
|
1548
|
+
return {
|
|
1549
|
+
sampleRate: r.sampleRate ?? 100,
|
|
1550
|
+
config: {
|
|
1551
|
+
recordConsole: r.recordConsole !== false,
|
|
1552
|
+
maskTextSelector: this.maskAllText() ? "*" : this.maskSelector(),
|
|
1553
|
+
blockSelector: validSelectors(["[data-guidely-block]", ...r.blockSelectors || [], ...extra]).join(","),
|
|
1554
|
+
network: r.recordNetwork && opt.recordNetwork !== false ? { payloads: !!r.recordNetworkPayloads && opt.recordNetworkPayloads !== false, apiUrl: this.options.apiUrl, mask: opt.maskNetworkRequest } : null
|
|
1555
|
+
}
|
|
1556
|
+
};
|
|
1557
|
+
}
|
|
1558
|
+
/** Reconciles capture with the current config / consent / authoring state. Idempotent. */
|
|
1559
|
+
syncCapture() {
|
|
1560
|
+
if (!this.isReady || this.destroyed) return;
|
|
1561
|
+
if (this.config && !this.initialPageViewSent && this.config.settings?.autocapture) {
|
|
1562
|
+
this.initialPageViewSent = true;
|
|
1563
|
+
this.trackPageView(location.href);
|
|
1564
|
+
}
|
|
1565
|
+
if (this.config) {
|
|
1566
|
+
for (const [props, extra] of this.earlyExceptions.splice(0)) {
|
|
1567
|
+
if (props.handled) this.sendException(props, extra);
|
|
1568
|
+
else this.captureGlobalException(props);
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
this.syncReplay();
|
|
1572
|
+
}
|
|
1573
|
+
syncReplay() {
|
|
1574
|
+
const cfg = this.isReady && this.captureAllowed() ? this.replayConfig() : null;
|
|
1575
|
+
if (cfg && this.session.replay === void 0) {
|
|
1576
|
+
this.session.replay = rolloutBucket(this.session.id, "replay") < cfg.sampleRate;
|
|
1577
|
+
this.persistSession();
|
|
1578
|
+
}
|
|
1579
|
+
if (!cfg || !this.session.replay) {
|
|
1580
|
+
if (this.replay?.activeSessionId) this.replay.stop({ discard: !this.analyticsEnabled() });
|
|
1581
|
+
return;
|
|
1582
|
+
}
|
|
1583
|
+
if (this.replay) return this.replay.start(this.session.id, cfg.config);
|
|
1584
|
+
if (this.replayLoading) return;
|
|
1585
|
+
this.replayLoading = true;
|
|
1586
|
+
loadRecorder().then(
|
|
1587
|
+
(api) => {
|
|
1588
|
+
this.replayLoading = false;
|
|
1589
|
+
if (this.destroyed || this.replay) return;
|
|
1590
|
+
this.replay = api.createReplay({
|
|
1591
|
+
url: (params) => this.api.url("/replay", params),
|
|
1592
|
+
fetch: () => this.options.fetch ?? (typeof fetch === "function" ? fetch.bind(globalThis) : null),
|
|
1593
|
+
meta: () => {
|
|
1594
|
+
const device = detectDevice(navigator.userAgent, window.innerWidth);
|
|
1595
|
+
return {
|
|
1596
|
+
anonymousId: this.anonymousId,
|
|
1597
|
+
userId: this.user?.id || null,
|
|
1598
|
+
url: this.collectUrls() ? location.href : void 0,
|
|
1599
|
+
device: { type: device.type, browser: device.browser, os: device.os },
|
|
1600
|
+
viewport: { width: window.innerWidth, height: window.innerHeight },
|
|
1601
|
+
sdk: { name: SDK_NAME, version: SDK_VERSION }
|
|
1602
|
+
};
|
|
1603
|
+
},
|
|
1604
|
+
onDisabled: (sessionId) => {
|
|
1605
|
+
if (sessionId !== this.session.id) return;
|
|
1606
|
+
this.session.replay = false;
|
|
1607
|
+
this.persistSession();
|
|
1608
|
+
this.replay?.stop({ discard: true });
|
|
1609
|
+
},
|
|
1610
|
+
onError: (err) => this.reportError(err)
|
|
1611
|
+
});
|
|
1612
|
+
this.syncReplay();
|
|
1613
|
+
},
|
|
1614
|
+
(err) => {
|
|
1615
|
+
this.replayLoading = false;
|
|
1616
|
+
this.reportError(err);
|
|
1617
|
+
}
|
|
1618
|
+
);
|
|
1619
|
+
}
|
|
1620
|
+
/**
|
|
1621
|
+
* Records this session even when it was not sampled, provided the project has session replay
|
|
1622
|
+
* enabled (and analytics + consent allow it). The decision sticks for the rest of the session.
|
|
1623
|
+
*/
|
|
1624
|
+
startSessionRecording() {
|
|
1625
|
+
if (this.destroyed) return;
|
|
1626
|
+
this.session.replay = true;
|
|
1627
|
+
this.persistSession();
|
|
1628
|
+
this.syncReplay();
|
|
1629
|
+
}
|
|
1630
|
+
/** Stops recording for the rest of this session (what was recorded so far is uploaded). */
|
|
1631
|
+
stopSessionRecording() {
|
|
1632
|
+
this.session.replay = false;
|
|
1633
|
+
this.persistSession();
|
|
1634
|
+
this.replay?.stop();
|
|
1635
|
+
}
|
|
1636
|
+
/** True while the session is being recorded. */
|
|
1637
|
+
isRecording() {
|
|
1638
|
+
return !!this.replay?.isRecording();
|
|
1639
|
+
}
|
|
1640
|
+
/** The current analytics session id (rotates after inactivity; also the replay id). */
|
|
1641
|
+
getSessionId() {
|
|
1642
|
+
return this.session.id;
|
|
1643
|
+
}
|
|
1644
|
+
// ---------------------------------------------------------------------------
|
|
1370
1645
|
// Targeting + triggers
|
|
1371
1646
|
// ---------------------------------------------------------------------------
|
|
1372
1647
|
/** Builds the targeting context for the current moment. */
|
|
@@ -1387,7 +1662,8 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
1387
1662
|
seenGuides: keysOf(Object.keys(guides).filter((id) => (guides[id]?.impressions ?? 0) > 0)),
|
|
1388
1663
|
completedChecklists: keysOf(completedChecklists),
|
|
1389
1664
|
audienceIds: this.state.audienceIds,
|
|
1390
|
-
environment: this.options.environment
|
|
1665
|
+
environment: this.options.environment,
|
|
1666
|
+
platform: "web"
|
|
1391
1667
|
});
|
|
1392
1668
|
}
|
|
1393
1669
|
isTargeted(guide, ctx, url) {
|
|
@@ -1412,6 +1688,7 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
1412
1688
|
const t = guide.config.trigger;
|
|
1413
1689
|
switch (t.type) {
|
|
1414
1690
|
case "page_load":
|
|
1691
|
+
if (t.delayMs > 0) return reason === "timer" && detail.guideId === guide.id;
|
|
1415
1692
|
return reason === "ready" || reason === "navigation" || reason === "identify";
|
|
1416
1693
|
case "url_match":
|
|
1417
1694
|
return (reason === "ready" || reason === "navigation" || reason === "identify") && urlMatches(t.value, location.href);
|
|
@@ -1632,7 +1909,7 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
1632
1909
|
});
|
|
1633
1910
|
entry.controller = controller;
|
|
1634
1911
|
if (exclusive) this.active = entry;
|
|
1635
|
-
else this.widgets.set(guide.id, { destroy: () => controller.destroy(), controller });
|
|
1912
|
+
else this.widgets.set(guide.id, { destroy: () => controller.destroy(), controller, manual });
|
|
1636
1913
|
controller.start();
|
|
1637
1914
|
if (controller.getState().status === "ended") {
|
|
1638
1915
|
if (exclusive && this.active === entry) this.active = null;
|
|
@@ -2026,6 +2303,8 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2026
2303
|
onError: (err) => this.reportError(err),
|
|
2027
2304
|
onActive: (active) => {
|
|
2028
2305
|
this.emitter.emit("authoring", { active });
|
|
2306
|
+
if (active) this.replay?.stop({ discard: true });
|
|
2307
|
+
else this.syncCapture();
|
|
2029
2308
|
if (active) {
|
|
2030
2309
|
this.stopGuide();
|
|
2031
2310
|
this.widgets.forEach((w) => w.destroy());
|
|
@@ -2076,12 +2355,23 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2076
2355
|
}
|
|
2077
2356
|
};
|
|
2078
2357
|
var current = null;
|
|
2079
|
-
function
|
|
2358
|
+
function webOnly(config) {
|
|
2359
|
+
return config.guides.every((g) => supportsPlatform(g.config, "web")) ? config : { ...config, guides: config.guides.filter((g) => supportsPlatform(g.config, "web")) };
|
|
2360
|
+
}
|
|
2361
|
+
function pathOf(url) {
|
|
2362
|
+
try {
|
|
2363
|
+
return new URL(url, "http://localhost").pathname;
|
|
2364
|
+
} catch {
|
|
2365
|
+
return url.replace(/[?#].*$/, "");
|
|
2366
|
+
}
|
|
2367
|
+
}
|
|
2368
|
+
function externalReferrer() {
|
|
2080
2369
|
try {
|
|
2081
|
-
|
|
2082
|
-
|
|
2370
|
+
if (!document.referrer) return void 0;
|
|
2371
|
+
const u = new URL(document.referrer);
|
|
2372
|
+
return u.origin === location.origin ? void 0 : u.origin + u.pathname;
|
|
2083
2373
|
} catch {
|
|
2084
|
-
return
|
|
2374
|
+
return void 0;
|
|
2085
2375
|
}
|
|
2086
2376
|
}
|
|
2087
2377
|
function createGuidely(options) {
|