@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.cjs
CHANGED
|
@@ -56,9 +56,9 @@ __export(index_exports, {
|
|
|
56
56
|
module.exports = __toCommonJS(index_exports);
|
|
57
57
|
|
|
58
58
|
// src/client.ts
|
|
59
|
-
var
|
|
60
|
-
var import_guide_runtime2 = require("@guidelytech/guide-runtime");
|
|
59
|
+
var import_runtime11 = require("@guidelytech/shared/runtime");
|
|
61
60
|
var import_guide_runtime3 = require("@guidelytech/guide-runtime");
|
|
61
|
+
var import_guide_runtime4 = require("@guidelytech/guide-runtime");
|
|
62
62
|
|
|
63
63
|
// src/authoring.ts
|
|
64
64
|
var import_guide_runtime = require("@guidelytech/guide-runtime");
|
|
@@ -105,9 +105,9 @@ function captureSnapshot(doc, maskSelectors, maxBytes = MAX_SNAPSHOT_BYTES) {
|
|
|
105
105
|
}
|
|
106
106
|
}
|
|
107
107
|
clone.querySelectorAll("*").forEach((el) => {
|
|
108
|
-
for (const
|
|
109
|
-
const name =
|
|
110
|
-
if (name.startsWith("on") || (name === "href" || name === "src" || name === "action") && /^\s*javascript:/i.test(
|
|
108
|
+
for (const attr2 of Array.from(el.attributes)) {
|
|
109
|
+
const name = attr2.name.toLowerCase();
|
|
110
|
+
if (name.startsWith("on") || (name === "href" || name === "src" || name === "action") && /^\s*javascript:/i.test(attr2.value)) el.removeAttribute(attr2.name);
|
|
111
111
|
}
|
|
112
112
|
if (el instanceof HTMLInputElement) {
|
|
113
113
|
el.removeAttribute("value");
|
|
@@ -440,6 +440,134 @@ var AuthoringBridge = class {
|
|
|
440
440
|
}
|
|
441
441
|
};
|
|
442
442
|
|
|
443
|
+
// src/autocapture.ts
|
|
444
|
+
var import_guide_runtime2 = require("@guidelytech/guide-runtime");
|
|
445
|
+
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]";
|
|
446
|
+
var DEFAULT_MASK_SELECTORS = ["[data-guidely-mask]", ".guidely-mask"];
|
|
447
|
+
var PREFERRED_ATTRS = ["data-testid", "data-test-id", "data-test", "data-cy", "data-qa", "data-guidely-track", "data-tour"];
|
|
448
|
+
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;
|
|
449
|
+
function validSelectors(list) {
|
|
450
|
+
const out = [];
|
|
451
|
+
const probe = typeof document !== "undefined" ? document.createDocumentFragment() : null;
|
|
452
|
+
for (const raw of list) {
|
|
453
|
+
const s = (raw || "").trim();
|
|
454
|
+
if (!s || out.includes(s)) continue;
|
|
455
|
+
try {
|
|
456
|
+
probe?.querySelector(s);
|
|
457
|
+
out.push(s);
|
|
458
|
+
} catch {
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
return out;
|
|
462
|
+
}
|
|
463
|
+
function isIgnoredElement(el) {
|
|
464
|
+
return (0, import_guide_runtime2.isGuidelyElement)(el) || !!el.closest("[data-guidely-ignore]");
|
|
465
|
+
}
|
|
466
|
+
function interactiveElement(target) {
|
|
467
|
+
try {
|
|
468
|
+
return target.closest(INTERACTIVE_SELECTOR);
|
|
469
|
+
} catch {
|
|
470
|
+
return null;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
function esc(value) {
|
|
474
|
+
const g = globalThis;
|
|
475
|
+
return g.CSS?.escape ? g.CSS.escape(value) : value.replace(/[^\w-]/g, (c) => `\\${c}`);
|
|
476
|
+
}
|
|
477
|
+
var attr = (tag, name, value) => value ? `${tag}[${name}="${value.replace(/["\\]/g, "\\$&")}"]` : `${tag}[${name}]`;
|
|
478
|
+
function elementSelector(el) {
|
|
479
|
+
const tag = el.tagName.toLowerCase();
|
|
480
|
+
if (el.id && (0, import_guide_runtime2.isStableId)(el.id)) return `#${esc(el.id)}`;
|
|
481
|
+
for (const name of PREFERRED_ATTRS) if (el.hasAttribute(name)) return attr(tag, name, el.getAttribute(name) || "").slice(0, 300);
|
|
482
|
+
for (const a of Array.from(el.attributes)) {
|
|
483
|
+
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);
|
|
484
|
+
}
|
|
485
|
+
const label = el.getAttribute("aria-label");
|
|
486
|
+
if (label && label.length <= 80) return attr(tag, "aria-label", label).slice(0, 300);
|
|
487
|
+
const parts = [];
|
|
488
|
+
let cur = el;
|
|
489
|
+
while (cur && parts.length < 4) {
|
|
490
|
+
const t = cur.tagName.toLowerCase();
|
|
491
|
+
if (t === "html" || t === "body") break;
|
|
492
|
+
if (cur !== el && cur.id && (0, import_guide_runtime2.isStableId)(cur.id)) {
|
|
493
|
+
parts.unshift(`#${esc(cur.id)}`);
|
|
494
|
+
break;
|
|
495
|
+
}
|
|
496
|
+
const parent = cur.parentElement;
|
|
497
|
+
const node = cur;
|
|
498
|
+
const same = parent ? Array.from(parent.children).filter((c) => c.tagName === node.tagName) : [];
|
|
499
|
+
parts.unshift(same.length > 1 ? `${t}:nth-of-type(${same.indexOf(node) + 1})` : t);
|
|
500
|
+
cur = parent;
|
|
501
|
+
}
|
|
502
|
+
return (parts.join(" > ") || tag).slice(0, 300);
|
|
503
|
+
}
|
|
504
|
+
function matches(el, selector, deep) {
|
|
505
|
+
if (!selector) return false;
|
|
506
|
+
try {
|
|
507
|
+
return !!el.closest(selector) || deep && !!el.querySelector(selector);
|
|
508
|
+
} catch {
|
|
509
|
+
return false;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
function elementText(el, mask, maskAll) {
|
|
513
|
+
if (maskAll || matches(el, mask, true)) return void 0;
|
|
514
|
+
const tag = el.tagName.toLowerCase();
|
|
515
|
+
let text = "";
|
|
516
|
+
if (tag === "input") {
|
|
517
|
+
if (!/^(button|submit|reset)$/i.test(el.type || "")) return void 0;
|
|
518
|
+
text = el.value;
|
|
519
|
+
} else if (tag === "select" || tag === "textarea") return void 0;
|
|
520
|
+
else text = el.innerText ?? el.textContent ?? "";
|
|
521
|
+
text = (text || el.getAttribute("aria-label") || el.getAttribute("title") || "").replace(/\s+/g, " ").trim();
|
|
522
|
+
return text ? text.slice(0, 100) : void 0;
|
|
523
|
+
}
|
|
524
|
+
function linkHref(el) {
|
|
525
|
+
const a = el.closest("a[href]");
|
|
526
|
+
if (!a) return void 0;
|
|
527
|
+
try {
|
|
528
|
+
const u = new URL(a.getAttribute("href") || "", location.href);
|
|
529
|
+
if (!/^https?:$/.test(u.protocol)) return void 0;
|
|
530
|
+
return u.origin === location.origin ? u.pathname : u.origin;
|
|
531
|
+
} catch {
|
|
532
|
+
return void 0;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
function describeClick(el, e, opts) {
|
|
536
|
+
const props = { tag: el.tagName.toLowerCase(), selector: elementSelector(el), x: Math.round(e.pageX ?? 0) || 0, y: Math.round(e.pageY ?? 0) || 0 };
|
|
537
|
+
const text = elementText(el, opts.mask, opts.maskAll);
|
|
538
|
+
if (text) props.text = text;
|
|
539
|
+
const href = opts.collectUrls ? linkHref(el) : void 0;
|
|
540
|
+
if (href) props.href = href;
|
|
541
|
+
return props;
|
|
542
|
+
}
|
|
543
|
+
var RageClickDetector = class {
|
|
544
|
+
constructor(threshold = 3, windowMs = 1e3, radius = 30) {
|
|
545
|
+
__publicField(this, "threshold", threshold);
|
|
546
|
+
__publicField(this, "windowMs", windowMs);
|
|
547
|
+
__publicField(this, "radius", radius);
|
|
548
|
+
__publicField(this, "clicks", []);
|
|
549
|
+
__publicField(this, "fired", false);
|
|
550
|
+
}
|
|
551
|
+
/** Clicks in the current burst once it has fired (rage), otherwise 0. */
|
|
552
|
+
get burst() {
|
|
553
|
+
return this.fired ? this.clicks.length : 0;
|
|
554
|
+
}
|
|
555
|
+
/** Records a click; returns the click count when it completes a burst, otherwise 0. */
|
|
556
|
+
add(x, y, t) {
|
|
557
|
+
const first = this.clicks[0];
|
|
558
|
+
const last = this.clicks[this.clicks.length - 1];
|
|
559
|
+
if (!first || !last || t - last.t > this.windowMs || Math.hypot(x - first.x, y - first.y) > this.radius) {
|
|
560
|
+
this.clicks = [];
|
|
561
|
+
this.fired = false;
|
|
562
|
+
}
|
|
563
|
+
this.clicks = this.clicks.filter((c) => t - c.t <= this.windowMs);
|
|
564
|
+
this.clicks.push({ x, y, t });
|
|
565
|
+
if (this.fired || this.clicks.length < this.threshold) return 0;
|
|
566
|
+
this.fired = true;
|
|
567
|
+
return this.clicks.length;
|
|
568
|
+
}
|
|
569
|
+
};
|
|
570
|
+
|
|
443
571
|
// src/context.ts
|
|
444
572
|
function detectDevice(ua, width) {
|
|
445
573
|
const u = ua || "";
|
|
@@ -496,9 +624,153 @@ function buildContext(input) {
|
|
|
496
624
|
audienceIds: new Set(input.audienceIds),
|
|
497
625
|
now: input.now ?? /* @__PURE__ */ new Date(),
|
|
498
626
|
environment: input.environment,
|
|
499
|
-
locale: input.locale ?? (typeof navigator !== "undefined" ? navigator.language : void 0)
|
|
627
|
+
locale: input.locale ?? (typeof navigator !== "undefined" ? navigator.language : void 0),
|
|
628
|
+
platform: input.platform ?? "web",
|
|
629
|
+
...input.screen !== void 0 ? { screen: input.screen } : {},
|
|
630
|
+
...input.appVersion !== void 0 ? { appVersion: input.appVersion } : {}
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// src/heatmap.ts
|
|
635
|
+
var HEATMAP_MAX_POINTS = 40;
|
|
636
|
+
var HEATMAP_MAX_BYTES = 6e3;
|
|
637
|
+
var HEATMAP_MAX_BINS = 60;
|
|
638
|
+
var r2 = (n) => Math.round(Math.min(1, Math.max(0, n)) * 100) / 100;
|
|
639
|
+
function heatmapPoint(el, e, rage) {
|
|
640
|
+
const p = { k: rage ? "r" : "c", x: Math.round(e.pageX ?? 0) || 0, y: Math.round(e.pageY ?? 0) || 0 };
|
|
641
|
+
const s = elementSelector(el).slice(0, 120);
|
|
642
|
+
if (s) p.s = s;
|
|
643
|
+
const rect = el.getBoundingClientRect();
|
|
644
|
+
if (rect.width > 0 && rect.height > 0) {
|
|
645
|
+
p.rx = r2((e.clientX - rect.left) / rect.width);
|
|
646
|
+
p.ry = r2((e.clientY - rect.top) / rect.height);
|
|
647
|
+
}
|
|
648
|
+
return p;
|
|
649
|
+
}
|
|
650
|
+
var selectors = /* @__PURE__ */ new WeakMap();
|
|
651
|
+
var r1 = (n) => Math.round(Math.min(1, Math.max(0, n)) * 10) / 10;
|
|
652
|
+
function movementBin(el, e) {
|
|
653
|
+
const p = { k: "m", x: Math.round(e.pageX ?? 0) || 0, y: Math.round(e.pageY ?? 0) || 0 };
|
|
654
|
+
let s = /^(HTML|BODY)$/.test(el.tagName) ? "" : selectors.get(el);
|
|
655
|
+
if (s === void 0) selectors.set(el, s = elementSelector(el).slice(0, 120));
|
|
656
|
+
const rect = s ? el.getBoundingClientRect() : null;
|
|
657
|
+
if (!rect || !(rect.width > 0 && rect.height > 0)) return [`${p.x >> 4},${p.y >> 4}`, p];
|
|
658
|
+
p.s = s;
|
|
659
|
+
p.rx = r1((e.clientX - rect.left) / rect.width);
|
|
660
|
+
p.ry = r1((e.clientY - rect.top) / rect.height);
|
|
661
|
+
return [`${s} ${p.rx},${p.ry}`, p];
|
|
662
|
+
}
|
|
663
|
+
var HeatmapBuffer = class {
|
|
664
|
+
constructor() {
|
|
665
|
+
/** The page the buffered points belong to. */
|
|
666
|
+
__publicField(this, "url", "");
|
|
667
|
+
__publicField(this, "points", []);
|
|
668
|
+
__publicField(this, "bins", /* @__PURE__ */ new Map());
|
|
669
|
+
__publicField(this, "bytes", 0);
|
|
670
|
+
}
|
|
671
|
+
/** Adds a click (`burst` > 0 marks it and the burst's previous clicks as rage); true when the batch is full. */
|
|
672
|
+
add(url, point, burst) {
|
|
673
|
+
this.url = url;
|
|
674
|
+
for (let i = Math.max(0, this.points.length - burst + 1); i < this.points.length; i++) this.points[i].k = "r";
|
|
675
|
+
this.points.push(point);
|
|
676
|
+
this.bytes += JSON.stringify(point).length + 1;
|
|
677
|
+
return this.full();
|
|
678
|
+
}
|
|
679
|
+
/** Adds a movement sample to its bin (weight +1); true when the batch is full. */
|
|
680
|
+
move(url, [key, point]) {
|
|
681
|
+
this.url = url;
|
|
682
|
+
const bin = this.bins.get(key);
|
|
683
|
+
if (bin) bin.w = (bin.w ?? 1) + 1;
|
|
684
|
+
else {
|
|
685
|
+
this.bins.set(key, point);
|
|
686
|
+
this.bytes += JSON.stringify(point).length + 8;
|
|
687
|
+
}
|
|
688
|
+
return this.full();
|
|
689
|
+
}
|
|
690
|
+
full() {
|
|
691
|
+
return this.points.length >= HEATMAP_MAX_POINTS || this.bins.size > HEATMAP_MAX_BINS || this.bytes >= HEATMAP_MAX_BYTES;
|
|
692
|
+
}
|
|
693
|
+
/** Empties the buffer, returning what was in it (clicks first, then movement bins). */
|
|
694
|
+
take() {
|
|
695
|
+
const points = [...this.points, ...this.bins.values()];
|
|
696
|
+
this.points = [];
|
|
697
|
+
this.bins = /* @__PURE__ */ new Map();
|
|
698
|
+
this.bytes = 0;
|
|
699
|
+
return points.length ? { url: this.url, points } : null;
|
|
700
|
+
}
|
|
701
|
+
};
|
|
702
|
+
|
|
703
|
+
// src/errors.ts
|
|
704
|
+
var import_runtime4 = require("@guidelytech/shared/runtime");
|
|
705
|
+
var MAX_FRAMES_BYTES = 6e3;
|
|
706
|
+
function fitFrames(frames) {
|
|
707
|
+
const out = frames.slice(0, 30);
|
|
708
|
+
while (out.length > 1 && JSON.stringify(out).length > MAX_FRAMES_BYTES) out.pop();
|
|
709
|
+
return out;
|
|
710
|
+
}
|
|
711
|
+
function describe(value) {
|
|
712
|
+
if (typeof value === "string") return value;
|
|
713
|
+
try {
|
|
714
|
+
return JSON.stringify(value) ?? String(value);
|
|
715
|
+
} catch {
|
|
716
|
+
return String(value);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
function exceptionFromError(error, source, handled, fallbackType = "Error") {
|
|
720
|
+
const isError = error instanceof Error || !!error && typeof error === "object" && typeof error.message === "string" && typeof error.stack === "string";
|
|
721
|
+
const e = error;
|
|
722
|
+
return {
|
|
723
|
+
type: isError && e.name || fallbackType,
|
|
724
|
+
message: (isError ? e.message : describe(error)).slice(0, 1e3),
|
|
725
|
+
frames: isError ? fitFrames((0, import_runtime4.parseStack)(e.stack)) : [],
|
|
726
|
+
source,
|
|
727
|
+
handled
|
|
500
728
|
};
|
|
501
729
|
}
|
|
730
|
+
function errorEventToException(e) {
|
|
731
|
+
const target = e.target;
|
|
732
|
+
if (target && typeof Element !== "undefined" && target instanceof Element) return null;
|
|
733
|
+
if (e.error != null) return exceptionFromError(e.error, "onerror", false);
|
|
734
|
+
const message = e.message || "";
|
|
735
|
+
if (!message || /^script error\.?$/i.test(message.trim())) return null;
|
|
736
|
+
const m = /^(?:Uncaught )?([A-Z]\w*(?:Error|Exception)):\s*([\s\S]*)$/.exec(message);
|
|
737
|
+
return {
|
|
738
|
+
type: m?.[1] ?? "Error",
|
|
739
|
+
message: (m?.[2] ?? message).slice(0, 1e3),
|
|
740
|
+
frames: e.filename ? [{ file: e.filename.slice(0, 500), line: e.lineno || void 0, col: e.colno || void 0 }] : [],
|
|
741
|
+
source: "onerror",
|
|
742
|
+
handled: false
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
function rejectionToException(e) {
|
|
746
|
+
return exceptionFromError(e.reason, "unhandledrejection", false, "UnhandledRejection");
|
|
747
|
+
}
|
|
748
|
+
var GUIDELY_SCRIPT = /\/guidely(-recorder)?(\.min)?\.js$/i;
|
|
749
|
+
function isGuidelyException(props) {
|
|
750
|
+
if (props.message.startsWith("[Guidely]")) return true;
|
|
751
|
+
const file = props.frames[0]?.file;
|
|
752
|
+
if (!file) return false;
|
|
753
|
+
try {
|
|
754
|
+
return GUIDELY_SCRIPT.test(new URL(file).pathname);
|
|
755
|
+
} catch {
|
|
756
|
+
return GUIDELY_SCRIPT.test(file.replace(/[?#].*$/, ""));
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
var ExceptionThrottle = class {
|
|
760
|
+
constructor(max = 5) {
|
|
761
|
+
__publicField(this, "max", max);
|
|
762
|
+
__publicField(this, "seen", /* @__PURE__ */ new Map());
|
|
763
|
+
}
|
|
764
|
+
allow(props) {
|
|
765
|
+
const top = props.frames[0];
|
|
766
|
+
const key = `${props.type}|${props.message}|${top ? `${top.file}:${top.line}:${top.col}` : ""}`;
|
|
767
|
+
const n = this.seen.get(key) ?? 0;
|
|
768
|
+
if (n >= this.max) return false;
|
|
769
|
+
if (this.seen.size > 500) this.seen.clear();
|
|
770
|
+
this.seen.set(key, n + 1);
|
|
771
|
+
return true;
|
|
772
|
+
}
|
|
773
|
+
};
|
|
502
774
|
|
|
503
775
|
// src/emitter.ts
|
|
504
776
|
var Emitter = class {
|
|
@@ -586,10 +858,8 @@ var Api = class {
|
|
|
586
858
|
__publicField(this, "fetchImpl", fetchImpl);
|
|
587
859
|
}
|
|
588
860
|
url(path, params = {}) {
|
|
589
|
-
const
|
|
590
|
-
|
|
591
|
-
for (const [k, v] of Object.entries(params)) if (v != null) u.searchParams.set(k, v);
|
|
592
|
-
return u.toString();
|
|
861
|
+
const query = Object.entries({ key: this.key, ...params }).filter((e) => e[1] != null).map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join("&");
|
|
862
|
+
return `${this.baseUrl.replace(/\/$/, "")}/api/v1/sdk${path}?${query}`;
|
|
593
863
|
}
|
|
594
864
|
get fetcher() {
|
|
595
865
|
if (this.fetchImpl) return this.fetchImpl;
|
|
@@ -878,6 +1148,51 @@ function watchNavigation(listener, debounceMs = 50) {
|
|
|
878
1148
|
};
|
|
879
1149
|
}
|
|
880
1150
|
|
|
1151
|
+
// src/page-tracking.ts
|
|
1152
|
+
function scrollDepth() {
|
|
1153
|
+
const de = document.documentElement;
|
|
1154
|
+
const height = Math.max(de?.scrollHeight || 0, document.body?.scrollHeight || 0);
|
|
1155
|
+
const view = window.innerHeight || de?.clientHeight || 0;
|
|
1156
|
+
if (height <= 0 || height <= view) return 100;
|
|
1157
|
+
const y = window.scrollY || de?.scrollTop || 0;
|
|
1158
|
+
return Math.max(0, Math.min(100, Math.round((y + view) / height * 100)));
|
|
1159
|
+
}
|
|
1160
|
+
var PageTracker = class {
|
|
1161
|
+
constructor() {
|
|
1162
|
+
__publicField(this, "url", "");
|
|
1163
|
+
__publicField(this, "visibleMs", 0);
|
|
1164
|
+
__publicField(this, "visibleSince", null);
|
|
1165
|
+
__publicField(this, "maxScroll", 0);
|
|
1166
|
+
}
|
|
1167
|
+
/** Starts measuring a new page. */
|
|
1168
|
+
start(url, now = Date.now()) {
|
|
1169
|
+
this.url = url;
|
|
1170
|
+
this.visibleMs = 0;
|
|
1171
|
+
this.visibleSince = typeof document !== "undefined" && document.visibilityState === "hidden" ? null : now;
|
|
1172
|
+
this.maxScroll = 0;
|
|
1173
|
+
this.measureScroll();
|
|
1174
|
+
}
|
|
1175
|
+
/** Pauses / resumes the visible-time clock. */
|
|
1176
|
+
setHidden(hidden, now = Date.now()) {
|
|
1177
|
+
if (hidden && this.visibleSince !== null) {
|
|
1178
|
+
this.visibleMs += Math.max(0, now - this.visibleSince);
|
|
1179
|
+
this.visibleSince = null;
|
|
1180
|
+
} else if (!hidden && this.visibleSince === null) this.visibleSince = now;
|
|
1181
|
+
}
|
|
1182
|
+
measureScroll() {
|
|
1183
|
+
try {
|
|
1184
|
+
this.maxScroll = Math.max(this.maxScroll, scrollDepth());
|
|
1185
|
+
} catch {
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
/** Final numbers for the current page. */
|
|
1189
|
+
leave(now = Date.now()) {
|
|
1190
|
+
this.measureScroll();
|
|
1191
|
+
const running = this.visibleSince !== null ? Math.max(0, now - this.visibleSince) : 0;
|
|
1192
|
+
return { url: this.url, durationMs: Math.round(this.visibleMs + running), scrollDepth: this.maxScroll };
|
|
1193
|
+
}
|
|
1194
|
+
};
|
|
1195
|
+
|
|
881
1196
|
// src/queue.ts
|
|
882
1197
|
var EventQueue = class {
|
|
883
1198
|
constructor(opts) {
|
|
@@ -991,11 +1306,16 @@ var EventQueue = class {
|
|
|
991
1306
|
}
|
|
992
1307
|
};
|
|
993
1308
|
|
|
1309
|
+
// src/load-recorder.ts
|
|
1310
|
+
var loadRecorder = async () => (await import("./recorder.cjs")).recorder;
|
|
1311
|
+
|
|
994
1312
|
// src/client.ts
|
|
995
1313
|
var SDK_NAME = "@guidelytech/sdk-core";
|
|
996
|
-
var SDK_VERSION = "0.
|
|
1314
|
+
var SDK_VERSION = "0.4.0";
|
|
997
1315
|
var DEFAULT_API_URL = "https://api.guidely.app";
|
|
998
1316
|
var PAGE_SCOPED = /* @__PURE__ */ new Set(["page_load", "url_match", "element_click", "element_visible", "delay"]);
|
|
1317
|
+
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
1318
|
+
var MAX_SESSION_EXCEPTIONS = 100;
|
|
999
1319
|
function isBrowser() {
|
|
1000
1320
|
return typeof window !== "undefined" && typeof document !== "undefined";
|
|
1001
1321
|
}
|
|
@@ -1005,6 +1325,9 @@ function nowIso() {
|
|
|
1005
1325
|
function emptyState() {
|
|
1006
1326
|
return { guides: {}, checklists: {}, surveysSubmitted: [], audienceIds: [] };
|
|
1007
1327
|
}
|
|
1328
|
+
function newSession(now = Date.now()) {
|
|
1329
|
+
return { id: `s_${(0, import_runtime11.randomId)(16)}`, startedAt: new Date(now).toISOString(), shownGuides: [], events: [], lastActivityAt: now };
|
|
1330
|
+
}
|
|
1008
1331
|
var GuidelyClient = class _GuidelyClient {
|
|
1009
1332
|
constructor(options) {
|
|
1010
1333
|
__publicField(this, "options");
|
|
@@ -1042,6 +1365,19 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
1042
1365
|
__publicField(this, "lastAudiences", /* @__PURE__ */ new Set());
|
|
1043
1366
|
/** Set once the app calls identify()/reset(); bootstrap then leaves identity alone instead of re-identifying. */
|
|
1044
1367
|
__publicField(this, "identitySettled", false);
|
|
1368
|
+
// Product analytics (autocapture, error tracking, session replay)
|
|
1369
|
+
/** Created once the recorder chunk has loaded (it carries the replay controller). */
|
|
1370
|
+
__publicField(this, "replay", null);
|
|
1371
|
+
__publicField(this, "replayLoading", false);
|
|
1372
|
+
__publicField(this, "pageTracker", new PageTracker());
|
|
1373
|
+
__publicField(this, "rage", new RageClickDetector());
|
|
1374
|
+
__publicField(this, "heatmap", new HeatmapBuffer());
|
|
1375
|
+
__publicField(this, "exceptionThrottle", new ExceptionThrottle());
|
|
1376
|
+
/** Exceptions seen before the config was loaded (reported once it is known whether the server accepts them). */
|
|
1377
|
+
__publicField(this, "earlyExceptions", []);
|
|
1378
|
+
__publicField(this, "initialPageViewSent", false);
|
|
1379
|
+
__publicField(this, "pageViewUrl", null);
|
|
1380
|
+
__publicField(this, "pageLeft", false);
|
|
1045
1381
|
__publicField(this, "customListeners", /* @__PURE__ */ new Map());
|
|
1046
1382
|
__publicField(this, "eventBus", {
|
|
1047
1383
|
on: (name, cb) => {
|
|
@@ -1076,7 +1412,7 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
1076
1412
|
this.sessionStore = this.options.storage === "memory" ? this.store : createStorage("session");
|
|
1077
1413
|
this.anonymousId = this.store.get(KEYS.anon) || "";
|
|
1078
1414
|
if (!this.anonymousId) {
|
|
1079
|
-
this.anonymousId = `anon_${(0,
|
|
1415
|
+
this.anonymousId = `anon_${(0, import_runtime11.randomId)(20)}`;
|
|
1080
1416
|
this.store.set(KEYS.anon, this.anonymousId);
|
|
1081
1417
|
}
|
|
1082
1418
|
this.firstVisit = !this.store.get(KEYS.seen);
|
|
@@ -1084,7 +1420,7 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
1084
1420
|
this.state = { ...emptyState(), ...readJson(this.store, KEYS.state(this.options.projectId), {}) };
|
|
1085
1421
|
this.user = readJson(this.store, KEYS.user(this.options.projectId), null);
|
|
1086
1422
|
const session = readJson(this.sessionStore, KEYS.session, null);
|
|
1087
|
-
this.session = session && session.id ? session :
|
|
1423
|
+
this.session = session && session.id ? session : newSession();
|
|
1088
1424
|
this.persistSession();
|
|
1089
1425
|
this.lastUrl = location.href;
|
|
1090
1426
|
this.lastAudiences = new Set(this.state.audienceIds);
|
|
@@ -1118,10 +1454,11 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
1118
1454
|
return this.init();
|
|
1119
1455
|
}
|
|
1120
1456
|
async bootstrap() {
|
|
1457
|
+
this.wireErrorListeners();
|
|
1121
1458
|
try {
|
|
1122
1459
|
const cached = readJson(this.store, KEYS.config(this.options.projectId), null);
|
|
1123
1460
|
this.etag = this.store.get(KEYS.etag(this.options.projectId));
|
|
1124
|
-
if (cached && cached.guides) this.config = cached;
|
|
1461
|
+
if (cached && cached.guides) this.config = webOnly(cached);
|
|
1125
1462
|
await this.fetchConfig();
|
|
1126
1463
|
if (this.destroyed) return;
|
|
1127
1464
|
this.wireListeners();
|
|
@@ -1137,6 +1474,9 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
1137
1474
|
this.resumeActiveGuide();
|
|
1138
1475
|
this.evaluate("ready");
|
|
1139
1476
|
if (!this.options.disableAuthoring) this.maybeStartAuthoring();
|
|
1477
|
+
this.pageTracker.start(location.href);
|
|
1478
|
+
this.touchSession();
|
|
1479
|
+
this.syncCapture();
|
|
1140
1480
|
} catch (err) {
|
|
1141
1481
|
this.reportError(err);
|
|
1142
1482
|
this.isReady = true;
|
|
@@ -1145,10 +1485,10 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
1145
1485
|
}
|
|
1146
1486
|
async fetchConfig() {
|
|
1147
1487
|
try {
|
|
1148
|
-
const res = await this.api.get("/config", { etag: this.etag });
|
|
1488
|
+
const res = await this.api.get("/config", { etag: this.etag, params: { platform: "web" } });
|
|
1149
1489
|
if (res.status === 304) return true;
|
|
1150
1490
|
if (res.ok && res.data && Array.isArray(res.data.guides)) {
|
|
1151
|
-
this.config = res.data;
|
|
1491
|
+
this.config = webOnly(res.data);
|
|
1152
1492
|
this.etag = res.etag;
|
|
1153
1493
|
writeJson(this.store, KEYS.config(this.options.projectId), res.data);
|
|
1154
1494
|
if (res.etag) this.store.set(KEYS.etag(this.options.projectId), res.etag);
|
|
@@ -1171,14 +1511,62 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
1171
1511
|
const onClick = (e) => this.handleDocumentClick(e);
|
|
1172
1512
|
doc.addEventListener("click", onClick, true);
|
|
1173
1513
|
this.cleanups.push(() => doc.removeEventListener("click", onClick, true));
|
|
1174
|
-
const
|
|
1175
|
-
|
|
1514
|
+
const onVisibility = () => {
|
|
1515
|
+
const hidden = doc.visibilityState === "hidden";
|
|
1516
|
+
this.pageTracker.setHidden(hidden);
|
|
1517
|
+
if (!hidden) return;
|
|
1518
|
+
this.replay?.flushOnHide();
|
|
1519
|
+
this.queue.flushSync();
|
|
1520
|
+
};
|
|
1521
|
+
const onPageHide = () => {
|
|
1522
|
+
this.flushHeatmap();
|
|
1523
|
+
if (!this.pageLeft) this.leavePage(null);
|
|
1524
|
+
this.pageLeft = true;
|
|
1525
|
+
this.replay?.flushOnHide();
|
|
1526
|
+
this.queue.flushSync();
|
|
1527
|
+
};
|
|
1528
|
+
const onPageShow = (e) => {
|
|
1529
|
+
if (!e.persisted || !this.pageLeft) return;
|
|
1530
|
+
this.pageLeft = false;
|
|
1531
|
+
this.pageTracker.start(location.href);
|
|
1176
1532
|
};
|
|
1177
|
-
|
|
1178
|
-
|
|
1533
|
+
let lastScroll = 0;
|
|
1534
|
+
const onScroll = () => {
|
|
1535
|
+
const now = Date.now();
|
|
1536
|
+
if (now - lastScroll < 250) return;
|
|
1537
|
+
lastScroll = now;
|
|
1538
|
+
this.pageTracker.measureScroll();
|
|
1539
|
+
this.touchSession();
|
|
1540
|
+
};
|
|
1541
|
+
const onKey = () => this.touchSession();
|
|
1542
|
+
let move = null;
|
|
1543
|
+
let moveTimer = null;
|
|
1544
|
+
const onMove = (e) => {
|
|
1545
|
+
move = e;
|
|
1546
|
+
moveTimer ?? (moveTimer = setTimeout(() => {
|
|
1547
|
+
moveTimer = null;
|
|
1548
|
+
if (move) this.sampleMove(move);
|
|
1549
|
+
move = null;
|
|
1550
|
+
}, 200));
|
|
1551
|
+
};
|
|
1552
|
+
if (win.matchMedia?.("(pointer: fine)").matches) {
|
|
1553
|
+
doc.addEventListener("mousemove", onMove, { passive: true });
|
|
1554
|
+
this.cleanups.push(() => {
|
|
1555
|
+
doc.removeEventListener("mousemove", onMove);
|
|
1556
|
+
if (moveTimer) clearTimeout(moveTimer);
|
|
1557
|
+
});
|
|
1558
|
+
}
|
|
1559
|
+
doc.addEventListener("visibilitychange", onVisibility);
|
|
1560
|
+
win.addEventListener("pagehide", onPageHide);
|
|
1561
|
+
win.addEventListener("pageshow", onPageShow);
|
|
1562
|
+
win.addEventListener("scroll", onScroll, { passive: true });
|
|
1563
|
+
doc.addEventListener("keydown", onKey, true);
|
|
1179
1564
|
this.cleanups.push(() => {
|
|
1180
|
-
doc.removeEventListener("visibilitychange",
|
|
1181
|
-
win.removeEventListener("pagehide",
|
|
1565
|
+
doc.removeEventListener("visibilitychange", onVisibility);
|
|
1566
|
+
win.removeEventListener("pagehide", onPageHide);
|
|
1567
|
+
win.removeEventListener("pageshow", onPageShow);
|
|
1568
|
+
win.removeEventListener("scroll", onScroll);
|
|
1569
|
+
doc.removeEventListener("keydown", onKey, true);
|
|
1182
1570
|
});
|
|
1183
1571
|
const refresh = setInterval(() => void this.refresh(), this.options.configRefreshMs);
|
|
1184
1572
|
this.cleanups.push(() => clearInterval(refresh));
|
|
@@ -1196,6 +1584,18 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
1196
1584
|
win.addEventListener("message", onMessage);
|
|
1197
1585
|
this.cleanups.push(() => win.removeEventListener("message", onMessage));
|
|
1198
1586
|
}
|
|
1587
|
+
/** Uncaught errors + unhandled rejections; attached before the config loads so boot errors are kept. */
|
|
1588
|
+
wireErrorListeners() {
|
|
1589
|
+
const win = window;
|
|
1590
|
+
const onError = (e) => this.captureGlobalException(errorEventToException(e));
|
|
1591
|
+
const onRejection = (e) => this.captureGlobalException(rejectionToException(e));
|
|
1592
|
+
win.addEventListener("error", onError);
|
|
1593
|
+
win.addEventListener("unhandledrejection", onRejection);
|
|
1594
|
+
this.cleanups.push(() => {
|
|
1595
|
+
win.removeEventListener("error", onError);
|
|
1596
|
+
win.removeEventListener("unhandledrejection", onRejection);
|
|
1597
|
+
});
|
|
1598
|
+
}
|
|
1199
1599
|
/** Re-fetches the published config and re-evaluates triggers. */
|
|
1200
1600
|
async refresh() {
|
|
1201
1601
|
if (this.destroyed) return;
|
|
@@ -1205,6 +1605,7 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
1205
1605
|
this.renderPersistentWidgets();
|
|
1206
1606
|
this.evaluate("ready");
|
|
1207
1607
|
}
|
|
1608
|
+
this.syncCapture();
|
|
1208
1609
|
}
|
|
1209
1610
|
/** Tears everything down: guides, widgets, listeners, timers, host element. Re-init is allowed via a new client. */
|
|
1210
1611
|
destroy() {
|
|
@@ -1228,10 +1629,12 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
1228
1629
|
this.cleanups.splice(0).forEach((fn) => fn());
|
|
1229
1630
|
this.authoring?.stop();
|
|
1230
1631
|
this.authoring = null;
|
|
1632
|
+
this.replay?.destroy();
|
|
1633
|
+
this.replay = null;
|
|
1231
1634
|
this.queue.flushSync();
|
|
1232
1635
|
this.queue.destroy();
|
|
1233
1636
|
this.emitter.clear();
|
|
1234
|
-
(0,
|
|
1637
|
+
(0, import_guide_runtime3.destroyHost)();
|
|
1235
1638
|
if (current === this) current = null;
|
|
1236
1639
|
}
|
|
1237
1640
|
// ---------------------------------------------------------------------------
|
|
@@ -1243,7 +1646,7 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
1243
1646
|
this.identitySettled = true;
|
|
1244
1647
|
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) };
|
|
1245
1648
|
writeJson(this.store, KEYS.user(this.options.projectId), this.user);
|
|
1246
|
-
this.enqueue("identify", { properties: { traits: (0,
|
|
1649
|
+
this.enqueue("identify", { properties: { traits: (0, import_runtime11.sanitizeProperties)(this.user.traits) } });
|
|
1247
1650
|
await this.syncIdentify();
|
|
1248
1651
|
this.emitter.emit("identify", { userId: this.user.id, anonymousId: this.anonymousId });
|
|
1249
1652
|
if (this.isReady) this.evaluate("identify");
|
|
@@ -1310,13 +1713,15 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
1310
1713
|
this.stopGuide();
|
|
1311
1714
|
this.user = null;
|
|
1312
1715
|
this.store.remove(KEYS.user(this.options.projectId));
|
|
1313
|
-
this.anonymousId = `anon_${(0,
|
|
1716
|
+
this.anonymousId = `anon_${(0, import_runtime11.randomId)(20)}`;
|
|
1314
1717
|
this.store.set(KEYS.anon, this.anonymousId);
|
|
1315
1718
|
this.state = emptyState();
|
|
1316
1719
|
this.persistState();
|
|
1317
|
-
this.session =
|
|
1720
|
+
this.session = newSession();
|
|
1318
1721
|
this.persistSession();
|
|
1319
1722
|
this.sessionStore.remove(KEYS.active);
|
|
1723
|
+
this.replay?.stop();
|
|
1724
|
+
this.syncCapture();
|
|
1320
1725
|
this.lastAudiences = /* @__PURE__ */ new Set();
|
|
1321
1726
|
this.firedDelays.clear();
|
|
1322
1727
|
this.widgets.forEach((w) => w.destroy());
|
|
@@ -1337,6 +1742,8 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
1337
1742
|
this.consent = consent;
|
|
1338
1743
|
if (consent === "denied") this.queue.clear();
|
|
1339
1744
|
else if (consent === "granted") void this.queue.flush();
|
|
1745
|
+
if (consent !== "granted") this.replay?.stop({ discard: true });
|
|
1746
|
+
this.syncCapture();
|
|
1340
1747
|
}
|
|
1341
1748
|
analyticsEnabled() {
|
|
1342
1749
|
return this.options.analytics && this.consent === "granted" && (this.config?.settings?.analyticsEnabled ?? true);
|
|
@@ -1346,8 +1753,9 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
1346
1753
|
}
|
|
1347
1754
|
enqueue(name, extra = {}) {
|
|
1348
1755
|
if (this.consent === "denied" || !this.options.analytics) return null;
|
|
1756
|
+
if (name !== "page_leave" && name !== "heatmap") this.touchSession();
|
|
1349
1757
|
const event = {
|
|
1350
|
-
eventId: (0,
|
|
1758
|
+
eventId: (0, import_runtime11.randomId)(24),
|
|
1351
1759
|
name,
|
|
1352
1760
|
occurredAt: nowIso(),
|
|
1353
1761
|
anonymousId: this.anonymousId,
|
|
@@ -1357,8 +1765,9 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
1357
1765
|
url: this.collectUrls() ? location.href : void 0,
|
|
1358
1766
|
...extra
|
|
1359
1767
|
};
|
|
1360
|
-
if (extra.properties) event.properties = (0,
|
|
1768
|
+
if (extra.properties) event.properties = (0, import_runtime11.sanitizeProperties)(extra.properties);
|
|
1361
1769
|
this.queue.enqueue(event);
|
|
1770
|
+
this.replay?.addCustomEvent("guidely", { name, customName: event.customName, guideId: event.guideId });
|
|
1362
1771
|
this.emitter.emit("track", event);
|
|
1363
1772
|
return event;
|
|
1364
1773
|
}
|
|
@@ -1383,23 +1792,32 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
1383
1792
|
/** Records a page view and re-evaluates triggers. Called automatically on SPA navigation. */
|
|
1384
1793
|
page(url = location.href) {
|
|
1385
1794
|
if (this.destroyed) return;
|
|
1386
|
-
this.
|
|
1795
|
+
this.leavePage(url);
|
|
1796
|
+
this.trackPageView(url);
|
|
1387
1797
|
this.handleNavigation(url, true);
|
|
1388
1798
|
}
|
|
1389
1799
|
handleNavigation(url, explicit = false) {
|
|
1390
1800
|
if (this.destroyed) return;
|
|
1391
1801
|
const changed = url !== this.lastUrl;
|
|
1392
1802
|
this.lastUrl = url;
|
|
1393
|
-
if (!explicit && changed)
|
|
1803
|
+
if (!explicit && changed) {
|
|
1804
|
+
this.leavePage(url);
|
|
1805
|
+
this.trackPageView(url);
|
|
1806
|
+
}
|
|
1394
1807
|
this.completeChecklistTasksBy("url_visited", url);
|
|
1395
1808
|
this.active?.controller?.resume();
|
|
1396
1809
|
if (this.active?.controller) {
|
|
1397
1810
|
const st = this.active.controller.getState();
|
|
1398
1811
|
if (st.status === "running" && !this.active.manual) {
|
|
1399
1812
|
const g = this.active.guide;
|
|
1400
|
-
if (g.config.urlPatterns.length && !g.config.urlPatterns.some((p) => (0,
|
|
1813
|
+
if (g.config.urlPatterns.length && !g.config.urlPatterns.some((p) => (0, import_guide_runtime3.urlMatches)(p, url))) this.active.controller.destroy();
|
|
1401
1814
|
}
|
|
1402
1815
|
}
|
|
1816
|
+
this.widgets.forEach((w, id) => {
|
|
1817
|
+
if (!w.controller || w.manual) return;
|
|
1818
|
+
const g = this.findGuide(id);
|
|
1819
|
+
if (g?.config.urlPatterns.length && !g.config.urlPatterns.some((p) => (0, import_guide_runtime3.urlMatches)(p, url))) w.controller.destroy();
|
|
1820
|
+
});
|
|
1403
1821
|
this.firedDelays.clear();
|
|
1404
1822
|
if (this.isReady) this.evaluate("navigation");
|
|
1405
1823
|
if (changed) void this.queue.flush();
|
|
@@ -1407,10 +1825,255 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
1407
1825
|
handleDocumentClick(e) {
|
|
1408
1826
|
const target = e.target;
|
|
1409
1827
|
if (!target || typeof target.closest !== "function") return;
|
|
1828
|
+
this.autocaptureClick(e, target);
|
|
1410
1829
|
this.completeChecklistTasksBy("click", target);
|
|
1411
1830
|
if (this.isReady) this.evaluate("click", { target });
|
|
1412
1831
|
}
|
|
1413
1832
|
// ---------------------------------------------------------------------------
|
|
1833
|
+
// Product analytics: sessions, page views, clicks, errors, session replay
|
|
1834
|
+
// ---------------------------------------------------------------------------
|
|
1835
|
+
/** Records activity; starts a new session after `sessionTimeoutMinutes` of inactivity or 24h. */
|
|
1836
|
+
touchSession() {
|
|
1837
|
+
const now = Date.now();
|
|
1838
|
+
const s = this.session;
|
|
1839
|
+
const timeout = Math.max(1, this.config?.settings?.sessionTimeoutMinutes ?? 30) * 6e4;
|
|
1840
|
+
const last = s.lastActivityAt ?? now;
|
|
1841
|
+
if (now - last > timeout || now - (Date.parse(s.startedAt) || now) > DAY_MS) return this.rotateSession(now);
|
|
1842
|
+
if (now - (s.lastActivityAt ?? 0) < 1e3) return;
|
|
1843
|
+
s.lastActivityAt = now;
|
|
1844
|
+
this.persistSession();
|
|
1845
|
+
}
|
|
1846
|
+
rotateSession(now) {
|
|
1847
|
+
this.session = newSession(now);
|
|
1848
|
+
this.persistSession();
|
|
1849
|
+
this.log("session rotated", this.session.id);
|
|
1850
|
+
if (this.replay?.activeSessionId) this.replay.stop();
|
|
1851
|
+
this.syncReplay();
|
|
1852
|
+
}
|
|
1853
|
+
/** Autocapture, error tracking and replay need analytics on, consent granted and no authoring session. */
|
|
1854
|
+
captureAllowed() {
|
|
1855
|
+
return !this.destroyed && this.analyticsEnabled() && !this.authoring?.isActive();
|
|
1856
|
+
}
|
|
1857
|
+
/** A capability is on when the project enables it (absent on old servers → off) and no option turns it off. */
|
|
1858
|
+
autocaptureOn(key) {
|
|
1859
|
+
const o = this.options.autocapture;
|
|
1860
|
+
return !!this.config?.settings?.autocapture?.[key] && o !== false && !(typeof o === "object" && o[key] === false) && this.captureAllowed();
|
|
1861
|
+
}
|
|
1862
|
+
/** Page views on SPA navigation predate autocapture: only an explicit opt-out stops them. */
|
|
1863
|
+
pageviewsOn() {
|
|
1864
|
+
const o = this.options.autocapture;
|
|
1865
|
+
return this.config?.settings?.autocapture?.pageviews !== false && o !== false && !(typeof o === "object" && o.pageviews === false) && !this.authoring?.isActive();
|
|
1866
|
+
}
|
|
1867
|
+
trackPageView(url) {
|
|
1868
|
+
if (url === this.pageViewUrl || !this.pageviewsOn()) return;
|
|
1869
|
+
this.pageViewUrl = url;
|
|
1870
|
+
this.touchSession();
|
|
1871
|
+
const collect = this.collectUrls();
|
|
1872
|
+
const device = detectDevice(navigator.userAgent, window.innerWidth);
|
|
1873
|
+
const props = { deviceType: device.type, browser: device.browser, os: device.os, screenWidth: window.screen?.width, viewportWidth: window.innerWidth };
|
|
1874
|
+
if (collect) {
|
|
1875
|
+
props.path = pathOf(url);
|
|
1876
|
+
props.title = (document.title || "").slice(0, 200);
|
|
1877
|
+
const ref = !this.session.pageViews ? externalReferrer() : void 0;
|
|
1878
|
+
if (ref) props.referrer = ref;
|
|
1879
|
+
}
|
|
1880
|
+
this.session.pageViews = (this.session.pageViews ?? 0) + 1;
|
|
1881
|
+
this.persistSession();
|
|
1882
|
+
this.enqueue("page_view", { url: collect ? url : void 0, properties: props });
|
|
1883
|
+
}
|
|
1884
|
+
/** Emits `page_leave` for the page being left and starts measuring `nextUrl` (null: page unload). */
|
|
1885
|
+
leavePage(nextUrl) {
|
|
1886
|
+
const t = this.pageTracker;
|
|
1887
|
+
if (!t.url || t.url === nextUrl) return;
|
|
1888
|
+
this.flushHeatmap();
|
|
1889
|
+
if (!this.pageLeft && this.autocaptureOn("pageLeave")) {
|
|
1890
|
+
const info = t.leave();
|
|
1891
|
+
const collect = this.collectUrls();
|
|
1892
|
+
this.enqueue("page_leave", { url: collect ? info.url : void 0, properties: { ...collect ? { path: pathOf(info.url) } : {}, durationMs: info.durationMs, scrollDepth: info.scrollDepth } });
|
|
1893
|
+
}
|
|
1894
|
+
if (nextUrl !== null) {
|
|
1895
|
+
t.start(nextUrl);
|
|
1896
|
+
this.pageLeft = false;
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1899
|
+
autocaptureClick(e, target) {
|
|
1900
|
+
const clicks = this.autocaptureOn("clicks");
|
|
1901
|
+
const rage = this.autocaptureOn("rageClicks");
|
|
1902
|
+
const heat = this.autocaptureOn("heatmaps");
|
|
1903
|
+
if (!clicks && !rage && !heat || isIgnoredElement(target)) return;
|
|
1904
|
+
this.touchSession();
|
|
1905
|
+
const el = interactiveElement(target);
|
|
1906
|
+
const opts = { mask: this.maskSelector(), maskAll: this.maskAllText(), collectUrls: this.collectUrls() };
|
|
1907
|
+
if (clicks && el) this.enqueue("click", { properties: { ...describeClick(el, e, opts) } });
|
|
1908
|
+
const count = this.rage.add(e.pageX ?? 0, e.pageY ?? 0, Date.now());
|
|
1909
|
+
if (rage && count) this.enqueue("rage_click", { properties: { ...describeClick(el ?? target, e, opts), count } });
|
|
1910
|
+
if (!heat) return;
|
|
1911
|
+
const url = location.href;
|
|
1912
|
+
if (this.heatmap.url !== url) this.flushHeatmap();
|
|
1913
|
+
if (this.heatmap.add(url, heatmapPoint(el ?? target, e, this.rage.burst > 0), this.rage.burst)) this.flushHeatmap();
|
|
1914
|
+
}
|
|
1915
|
+
/** Bins one pointer sample into the heatmap buffer. Not session activity. */
|
|
1916
|
+
sampleMove(e) {
|
|
1917
|
+
const target = e.target;
|
|
1918
|
+
if (document.visibilityState === "hidden" || !target?.closest || !this.autocaptureOn("mouseMovement") || isIgnoredElement(target)) return;
|
|
1919
|
+
const url = location.href;
|
|
1920
|
+
if (this.heatmap.url !== url) this.flushHeatmap();
|
|
1921
|
+
if (this.heatmap.move(url, movementBin(interactiveElement(target) ?? target, e))) this.flushHeatmap();
|
|
1922
|
+
}
|
|
1923
|
+
/** Sends the buffered heatmap clicks as one `heatmap` event attributed to the page they happened on. */
|
|
1924
|
+
flushHeatmap() {
|
|
1925
|
+
const batch = this.heatmap.take();
|
|
1926
|
+
if (!batch || !this.captureAllowed()) return;
|
|
1927
|
+
const collect = this.collectUrls();
|
|
1928
|
+
const de = document.documentElement;
|
|
1929
|
+
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 };
|
|
1930
|
+
if (!collect) delete properties.path;
|
|
1931
|
+
this.enqueue("heatmap", { url: collect ? batch.url : void 0, properties });
|
|
1932
|
+
}
|
|
1933
|
+
captureGlobalException(props) {
|
|
1934
|
+
if (!props || isGuidelyException(props) || this.options.captureErrors === false) return;
|
|
1935
|
+
if (!this.config) {
|
|
1936
|
+
if (this.earlyExceptions.length < 10) this.earlyExceptions.push([props, void 0]);
|
|
1937
|
+
return;
|
|
1938
|
+
}
|
|
1939
|
+
this.sendException(props);
|
|
1940
|
+
}
|
|
1941
|
+
/**
|
|
1942
|
+
* Reports a caught error as an `exception` event (`source: "manual"`, `handled: true`), grouped
|
|
1943
|
+
* into an issue on the Errors page. Extra properties are attached to the occurrence. Needs the
|
|
1944
|
+
* project's error tracking on (plus analytics + consent); `captureErrors: false` does not stop it.
|
|
1945
|
+
*/
|
|
1946
|
+
captureException(error, properties) {
|
|
1947
|
+
if (this.destroyed) return;
|
|
1948
|
+
const props = exceptionFromError(error, "manual", true);
|
|
1949
|
+
if (!this.config) {
|
|
1950
|
+
if (this.earlyExceptions.length < 10) this.earlyExceptions.push([props, properties]);
|
|
1951
|
+
return;
|
|
1952
|
+
}
|
|
1953
|
+
this.sendException(props, properties);
|
|
1954
|
+
}
|
|
1955
|
+
sendException(props, extra) {
|
|
1956
|
+
if (!this.config?.settings?.errorTracking?.enabled || !this.captureAllowed() || !this.exceptionThrottle.allow(props)) return;
|
|
1957
|
+
const count = this.session.exceptions ?? 0;
|
|
1958
|
+
if (count >= MAX_SESSION_EXCEPTIONS) return;
|
|
1959
|
+
const properties = { ...props };
|
|
1960
|
+
if (this.options.release) properties.release = String(this.options.release).slice(0, 100);
|
|
1961
|
+
for (const [k, v] of Object.entries(extra ?? {})) if (!(k in properties)) properties[k] = v;
|
|
1962
|
+
this.enqueue("exception", { properties });
|
|
1963
|
+
this.session.exceptions = count + 1;
|
|
1964
|
+
this.persistSession();
|
|
1965
|
+
}
|
|
1966
|
+
maskSelector() {
|
|
1967
|
+
return validSelectors([...DEFAULT_MASK_SELECTORS, ...this.config?.settings?.maskSelectors || [], ...this.options.maskSelectors || []]).join(",");
|
|
1968
|
+
}
|
|
1969
|
+
maskAllText() {
|
|
1970
|
+
const o = this.options.sessionReplay;
|
|
1971
|
+
return !!this.config?.settings?.replay?.maskAllText || typeof o === "object" && !!o.maskAllText;
|
|
1972
|
+
}
|
|
1973
|
+
/** Replay settings for this installation, or null when the project (or an option) disables replay. */
|
|
1974
|
+
replayConfig() {
|
|
1975
|
+
const r = this.config?.settings?.replay;
|
|
1976
|
+
const o = this.options.sessionReplay;
|
|
1977
|
+
if (!r?.enabled || o === false) return null;
|
|
1978
|
+
const opt = typeof o === "object" ? o : {};
|
|
1979
|
+
const extra = opt.blockSelectors || [];
|
|
1980
|
+
return {
|
|
1981
|
+
sampleRate: r.sampleRate ?? 100,
|
|
1982
|
+
config: {
|
|
1983
|
+
recordConsole: r.recordConsole !== false,
|
|
1984
|
+
maskTextSelector: this.maskAllText() ? "*" : this.maskSelector(),
|
|
1985
|
+
blockSelector: validSelectors(["[data-guidely-block]", ...r.blockSelectors || [], ...extra]).join(","),
|
|
1986
|
+
network: r.recordNetwork && opt.recordNetwork !== false ? { payloads: !!r.recordNetworkPayloads && opt.recordNetworkPayloads !== false, apiUrl: this.options.apiUrl, mask: opt.maskNetworkRequest } : null
|
|
1987
|
+
}
|
|
1988
|
+
};
|
|
1989
|
+
}
|
|
1990
|
+
/** Reconciles capture with the current config / consent / authoring state. Idempotent. */
|
|
1991
|
+
syncCapture() {
|
|
1992
|
+
if (!this.isReady || this.destroyed) return;
|
|
1993
|
+
if (this.config && !this.initialPageViewSent && this.config.settings?.autocapture) {
|
|
1994
|
+
this.initialPageViewSent = true;
|
|
1995
|
+
this.trackPageView(location.href);
|
|
1996
|
+
}
|
|
1997
|
+
if (this.config) {
|
|
1998
|
+
for (const [props, extra] of this.earlyExceptions.splice(0)) {
|
|
1999
|
+
if (props.handled) this.sendException(props, extra);
|
|
2000
|
+
else this.captureGlobalException(props);
|
|
2001
|
+
}
|
|
2002
|
+
}
|
|
2003
|
+
this.syncReplay();
|
|
2004
|
+
}
|
|
2005
|
+
syncReplay() {
|
|
2006
|
+
const cfg = this.isReady && this.captureAllowed() ? this.replayConfig() : null;
|
|
2007
|
+
if (cfg && this.session.replay === void 0) {
|
|
2008
|
+
this.session.replay = (0, import_runtime11.rolloutBucket)(this.session.id, "replay") < cfg.sampleRate;
|
|
2009
|
+
this.persistSession();
|
|
2010
|
+
}
|
|
2011
|
+
if (!cfg || !this.session.replay) {
|
|
2012
|
+
if (this.replay?.activeSessionId) this.replay.stop({ discard: !this.analyticsEnabled() });
|
|
2013
|
+
return;
|
|
2014
|
+
}
|
|
2015
|
+
if (this.replay) return this.replay.start(this.session.id, cfg.config);
|
|
2016
|
+
if (this.replayLoading) return;
|
|
2017
|
+
this.replayLoading = true;
|
|
2018
|
+
loadRecorder().then(
|
|
2019
|
+
(api) => {
|
|
2020
|
+
this.replayLoading = false;
|
|
2021
|
+
if (this.destroyed || this.replay) return;
|
|
2022
|
+
this.replay = api.createReplay({
|
|
2023
|
+
url: (params) => this.api.url("/replay", params),
|
|
2024
|
+
fetch: () => this.options.fetch ?? (typeof fetch === "function" ? fetch.bind(globalThis) : null),
|
|
2025
|
+
meta: () => {
|
|
2026
|
+
const device = detectDevice(navigator.userAgent, window.innerWidth);
|
|
2027
|
+
return {
|
|
2028
|
+
anonymousId: this.anonymousId,
|
|
2029
|
+
userId: this.user?.id || null,
|
|
2030
|
+
url: this.collectUrls() ? location.href : void 0,
|
|
2031
|
+
device: { type: device.type, browser: device.browser, os: device.os },
|
|
2032
|
+
viewport: { width: window.innerWidth, height: window.innerHeight },
|
|
2033
|
+
sdk: { name: SDK_NAME, version: SDK_VERSION }
|
|
2034
|
+
};
|
|
2035
|
+
},
|
|
2036
|
+
onDisabled: (sessionId) => {
|
|
2037
|
+
if (sessionId !== this.session.id) return;
|
|
2038
|
+
this.session.replay = false;
|
|
2039
|
+
this.persistSession();
|
|
2040
|
+
this.replay?.stop({ discard: true });
|
|
2041
|
+
},
|
|
2042
|
+
onError: (err) => this.reportError(err)
|
|
2043
|
+
});
|
|
2044
|
+
this.syncReplay();
|
|
2045
|
+
},
|
|
2046
|
+
(err) => {
|
|
2047
|
+
this.replayLoading = false;
|
|
2048
|
+
this.reportError(err);
|
|
2049
|
+
}
|
|
2050
|
+
);
|
|
2051
|
+
}
|
|
2052
|
+
/**
|
|
2053
|
+
* Records this session even when it was not sampled, provided the project has session replay
|
|
2054
|
+
* enabled (and analytics + consent allow it). The decision sticks for the rest of the session.
|
|
2055
|
+
*/
|
|
2056
|
+
startSessionRecording() {
|
|
2057
|
+
if (this.destroyed) return;
|
|
2058
|
+
this.session.replay = true;
|
|
2059
|
+
this.persistSession();
|
|
2060
|
+
this.syncReplay();
|
|
2061
|
+
}
|
|
2062
|
+
/** Stops recording for the rest of this session (what was recorded so far is uploaded). */
|
|
2063
|
+
stopSessionRecording() {
|
|
2064
|
+
this.session.replay = false;
|
|
2065
|
+
this.persistSession();
|
|
2066
|
+
this.replay?.stop();
|
|
2067
|
+
}
|
|
2068
|
+
/** True while the session is being recorded. */
|
|
2069
|
+
isRecording() {
|
|
2070
|
+
return !!this.replay?.isRecording();
|
|
2071
|
+
}
|
|
2072
|
+
/** The current analytics session id (rotates after inactivity; also the replay id). */
|
|
2073
|
+
getSessionId() {
|
|
2074
|
+
return this.session.id;
|
|
2075
|
+
}
|
|
2076
|
+
// ---------------------------------------------------------------------------
|
|
1414
2077
|
// Targeting + triggers
|
|
1415
2078
|
// ---------------------------------------------------------------------------
|
|
1416
2079
|
/** Builds the targeting context for the current moment. */
|
|
@@ -1431,17 +2094,18 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
1431
2094
|
seenGuides: keysOf(Object.keys(guides).filter((id) => (guides[id]?.impressions ?? 0) > 0)),
|
|
1432
2095
|
completedChecklists: keysOf(completedChecklists),
|
|
1433
2096
|
audienceIds: this.state.audienceIds,
|
|
1434
|
-
environment: this.options.environment
|
|
2097
|
+
environment: this.options.environment,
|
|
2098
|
+
platform: "web"
|
|
1435
2099
|
});
|
|
1436
2100
|
}
|
|
1437
2101
|
isTargeted(guide, ctx, url) {
|
|
1438
2102
|
const c = guide.config;
|
|
1439
|
-
if (c.urlPatterns.length && !c.urlPatterns.some((p) => (0,
|
|
1440
|
-
if (c.trigger.urlPattern && !(0,
|
|
2103
|
+
if (c.urlPatterns.length && !c.urlPatterns.some((p) => (0, import_guide_runtime3.urlMatches)(p, url))) return false;
|
|
2104
|
+
if (c.trigger.urlPattern && !(0, import_guide_runtime3.urlMatches)(c.trigger.urlPattern, url)) return false;
|
|
1441
2105
|
if (!isWithinSchedule(guide)) return false;
|
|
1442
2106
|
if (guide.kind === "survey" && (this.state.surveysSubmitted.includes(guide.id) || this.state.surveysSubmitted.includes(guide.key))) return false;
|
|
1443
2107
|
ctx.rolloutSalt = guide.id;
|
|
1444
|
-
return (0,
|
|
2108
|
+
return (0, import_runtime11.evaluateRules)(c.targeting, ctx);
|
|
1445
2109
|
}
|
|
1446
2110
|
isEligible(guide, ctx, url) {
|
|
1447
2111
|
if (!this.isTargeted(guide, ctx, url)) return false;
|
|
@@ -1456,9 +2120,10 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
1456
2120
|
const t = guide.config.trigger;
|
|
1457
2121
|
switch (t.type) {
|
|
1458
2122
|
case "page_load":
|
|
2123
|
+
if (t.delayMs > 0) return reason === "timer" && detail.guideId === guide.id;
|
|
1459
2124
|
return reason === "ready" || reason === "navigation" || reason === "identify";
|
|
1460
2125
|
case "url_match":
|
|
1461
|
-
return (reason === "ready" || reason === "navigation" || reason === "identify") && (0,
|
|
2126
|
+
return (reason === "ready" || reason === "navigation" || reason === "identify") && (0, import_guide_runtime3.urlMatches)(t.value, location.href);
|
|
1462
2127
|
case "event":
|
|
1463
2128
|
return reason === "event" && !!t.value && detail.eventName === t.value;
|
|
1464
2129
|
case "element_click":
|
|
@@ -1534,7 +2199,7 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
1534
2199
|
this.visibilityObservers.delete(guide.id);
|
|
1535
2200
|
};
|
|
1536
2201
|
this.visibilityObservers.set(guide.id, stop);
|
|
1537
|
-
(0,
|
|
2202
|
+
(0, import_guide_runtime3.waitForTarget)([{ kind: "css", value: selector }], { timeoutMs: 0, signal: ac.signal }).then((el) => {
|
|
1538
2203
|
if (ac.signal.aborted) return;
|
|
1539
2204
|
const fire = () => {
|
|
1540
2205
|
stop();
|
|
@@ -1630,7 +2295,7 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
1630
2295
|
if (this.authoring?.isActive()) return;
|
|
1631
2296
|
const exclusive = isExclusiveGuide(guide);
|
|
1632
2297
|
const entry = { guide, manual, startUrl: location.href };
|
|
1633
|
-
const controller = (0,
|
|
2298
|
+
const controller = (0, import_guide_runtime3.runGuide)(guide, {
|
|
1634
2299
|
theme: this.theme(),
|
|
1635
2300
|
ctx: () => this.getContext(),
|
|
1636
2301
|
startAtStep: stepId,
|
|
@@ -1676,7 +2341,7 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
1676
2341
|
});
|
|
1677
2342
|
entry.controller = controller;
|
|
1678
2343
|
if (exclusive) this.active = entry;
|
|
1679
|
-
else this.widgets.set(guide.id, { destroy: () => controller.destroy(), controller });
|
|
2344
|
+
else this.widgets.set(guide.id, { destroy: () => controller.destroy(), controller, manual });
|
|
1680
2345
|
controller.start();
|
|
1681
2346
|
if (controller.getState().status === "ended") {
|
|
1682
2347
|
if (exclusive && this.active === entry) this.active = null;
|
|
@@ -1687,10 +2352,10 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
1687
2352
|
if (this.authoring?.isActive()) return;
|
|
1688
2353
|
const config = guide.config.checklist;
|
|
1689
2354
|
if (!config || this.widgets.has(guide.id)) return;
|
|
1690
|
-
const scope = (0,
|
|
2355
|
+
const scope = (0, import_guide_runtime4.createScope)(this.theme(), guide.config.appearance);
|
|
1691
2356
|
const progress = this.getChecklistProgress(guide.id);
|
|
1692
2357
|
const first = !this.state.guides[guide.id]?.impressions;
|
|
1693
|
-
const widget = (0,
|
|
2358
|
+
const widget = (0, import_guide_runtime3.renderChecklist)({
|
|
1694
2359
|
scope: scope.el,
|
|
1695
2360
|
config,
|
|
1696
2361
|
progress,
|
|
@@ -1709,7 +2374,7 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
1709
2374
|
if (this.authoring?.isActive()) return;
|
|
1710
2375
|
const config = guide.config.survey;
|
|
1711
2376
|
if (!config) return;
|
|
1712
|
-
const scope = (0,
|
|
2377
|
+
const scope = (0, import_guide_runtime4.createScope)(this.theme(), guide.config.appearance);
|
|
1713
2378
|
const entry = { guide, manual, startUrl: location.href, scope };
|
|
1714
2379
|
const finish = (reason) => {
|
|
1715
2380
|
widget.destroy();
|
|
@@ -1722,13 +2387,13 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
1722
2387
|
}
|
|
1723
2388
|
this.setTimer(() => this.evaluate("after_guide", { endedGuide: guide }), 0);
|
|
1724
2389
|
};
|
|
1725
|
-
const widget = (0,
|
|
2390
|
+
const widget = (0, import_guide_runtime3.renderSurvey)({
|
|
1726
2391
|
scope: scope.el,
|
|
1727
2392
|
config,
|
|
1728
2393
|
title: guide.name,
|
|
1729
2394
|
onDismiss: () => finish(this.state.surveysSubmitted.includes(guide.id) ? "submitted" : "dismissed"),
|
|
1730
2395
|
onSubmit: (answers) => {
|
|
1731
|
-
const responseId = (0,
|
|
2396
|
+
const responseId = (0, import_runtime11.randomId)(24);
|
|
1732
2397
|
this.state.surveysSubmitted.push(guide.id);
|
|
1733
2398
|
this.persistState();
|
|
1734
2399
|
this.recordEnd(guide, "completed");
|
|
@@ -1770,9 +2435,9 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
1770
2435
|
}
|
|
1771
2436
|
this.destroyMessenger();
|
|
1772
2437
|
if (rc && rc.enabled && !this.resourceCenter) {
|
|
1773
|
-
const scope = (0,
|
|
2438
|
+
const scope = (0, import_guide_runtime4.createScope)(this.theme());
|
|
1774
2439
|
this.resourceCenterScope = scope;
|
|
1775
|
-
this.resourceCenter = (0,
|
|
2440
|
+
this.resourceCenter = (0, import_guide_runtime3.renderResourceCenter)({
|
|
1776
2441
|
scope: scope.el,
|
|
1777
2442
|
config: rc,
|
|
1778
2443
|
guides: this.config.guides.map((g) => ({ id: g.id, key: g.key, name: g.name, kind: g.kind })),
|
|
@@ -1807,7 +2472,7 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
1807
2472
|
this.messengerLoading = import("@guidelytech/guide-runtime/messenger").then(({ renderMessenger }) => {
|
|
1808
2473
|
const cfg = this.config?.messenger;
|
|
1809
2474
|
if (this.destroyed || !this.config || !cfg?.enabled || this.messenger) return this.messenger;
|
|
1810
|
-
const scope = (0,
|
|
2475
|
+
const scope = (0, import_guide_runtime4.createScope)(this.theme());
|
|
1811
2476
|
this.messengerScope = scope;
|
|
1812
2477
|
this.messenger = renderMessenger({
|
|
1813
2478
|
scope: scope.el,
|
|
@@ -1886,7 +2551,7 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
1886
2551
|
if (progress.completedTaskIds.includes(task.id)) return;
|
|
1887
2552
|
progress.completedTaskIds.push(task.id);
|
|
1888
2553
|
this.enqueue("checklist_task_completed", { guideId: guide.id, guideVersion: guide.version, properties: { taskId: task.id } });
|
|
1889
|
-
const complete = (0,
|
|
2554
|
+
const complete = (0, import_guide_runtime3.isChecklistComplete)(config, progress) && !progress.completedAt;
|
|
1890
2555
|
if (complete) {
|
|
1891
2556
|
progress.completedAt = nowIso();
|
|
1892
2557
|
this.enqueue("checklist_completed", { guideId: guide.id, guideVersion: guide.version });
|
|
@@ -1909,7 +2574,7 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
1909
2574
|
if (task.completion !== kind || !task.completionValue) continue;
|
|
1910
2575
|
let hit = false;
|
|
1911
2576
|
if (kind === "event") hit = value === task.completionValue;
|
|
1912
|
-
else if (kind === "url_visited") hit = (0,
|
|
2577
|
+
else if (kind === "url_visited") hit = (0, import_guide_runtime3.urlMatches)(task.completionValue, value);
|
|
1913
2578
|
else if (kind === "guide_completed") hit = value.id === task.completionValue || value.key === task.completionValue;
|
|
1914
2579
|
else if (kind === "click") {
|
|
1915
2580
|
try {
|
|
@@ -2070,6 +2735,8 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2070
2735
|
onError: (err) => this.reportError(err),
|
|
2071
2736
|
onActive: (active) => {
|
|
2072
2737
|
this.emitter.emit("authoring", { active });
|
|
2738
|
+
if (active) this.replay?.stop({ discard: true });
|
|
2739
|
+
else this.syncCapture();
|
|
2073
2740
|
if (active) {
|
|
2074
2741
|
this.stopGuide();
|
|
2075
2742
|
this.widgets.forEach((w) => w.destroy());
|
|
@@ -2120,12 +2787,23 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2120
2787
|
}
|
|
2121
2788
|
};
|
|
2122
2789
|
var current = null;
|
|
2123
|
-
function
|
|
2790
|
+
function webOnly(config) {
|
|
2791
|
+
return config.guides.every((g) => (0, import_runtime11.supportsPlatform)(g.config, "web")) ? config : { ...config, guides: config.guides.filter((g) => (0, import_runtime11.supportsPlatform)(g.config, "web")) };
|
|
2792
|
+
}
|
|
2793
|
+
function pathOf(url) {
|
|
2794
|
+
try {
|
|
2795
|
+
return new URL(url, "http://localhost").pathname;
|
|
2796
|
+
} catch {
|
|
2797
|
+
return url.replace(/[?#].*$/, "");
|
|
2798
|
+
}
|
|
2799
|
+
}
|
|
2800
|
+
function externalReferrer() {
|
|
2124
2801
|
try {
|
|
2125
|
-
|
|
2126
|
-
|
|
2802
|
+
if (!document.referrer) return void 0;
|
|
2803
|
+
const u = new URL(document.referrer);
|
|
2804
|
+
return u.origin === location.origin ? void 0 : u.origin + u.pathname;
|
|
2127
2805
|
} catch {
|
|
2128
|
-
return
|
|
2806
|
+
return void 0;
|
|
2129
2807
|
}
|
|
2130
2808
|
}
|
|
2131
2809
|
function createGuidely(options) {
|