@holdyapp/sdk 1.5.0 → 1.6.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 +1 -0
- package/dist/holdy.js +2 -2
- package/dist/holdy.js.map +4 -4
- package/dist/index.cjs +126 -3
- package/dist/index.cjs.map +4 -4
- package/dist/index.mjs +126 -3
- package/dist/index.mjs.map +4 -4
- package/dist/skeleton.d.ts +50 -0
- package/dist/skeleton.d.ts.map +1 -0
- package/dist/web.d.ts +8 -0
- package/dist/web.d.ts.map +1 -1
- package/dist/web.mjs +126 -3
- package/dist/web.mjs.map +4 -4
- package/package.json +1 -1
- package/src/skeleton.test.ts +60 -0
- package/src/skeleton.ts +173 -0
- package/src/web.ts +20 -1
package/dist/index.mjs
CHANGED
|
@@ -600,6 +600,119 @@ function installFrictionCapture(opts) {
|
|
|
600
600
|
};
|
|
601
601
|
}
|
|
602
602
|
|
|
603
|
+
// src/skeleton.ts
|
|
604
|
+
var SKELETON_SELECTOR = 'a[href], button, input, select, textarea, [role="button"], [role="link"], [role="tab"], [role="menuitem"], [role="checkbox"], [role="radio"], [role="switch"], [data-testid]';
|
|
605
|
+
var SKELETON_MAX_ELEMENTS = 80;
|
|
606
|
+
var SKELETON_DELAY_MS = 1500;
|
|
607
|
+
var SENT_STORAGE_KEY = "holdy_skeletons";
|
|
608
|
+
var SENT_STORAGE_MAX = 200;
|
|
609
|
+
function fnv1a(input) {
|
|
610
|
+
let hash = 2166136261;
|
|
611
|
+
for (let i = 0; i < input.length; i++) {
|
|
612
|
+
hash ^= input.charCodeAt(i);
|
|
613
|
+
hash = Math.imul(hash, 16777619) >>> 0;
|
|
614
|
+
}
|
|
615
|
+
return hash.toString(16).padStart(8, "0");
|
|
616
|
+
}
|
|
617
|
+
function attr(el, name, max) {
|
|
618
|
+
const value = el.getAttribute(name);
|
|
619
|
+
if (!value) return void 0;
|
|
620
|
+
const trimmed = value.replace(/\s+/g, " ").trim().slice(0, max);
|
|
621
|
+
return trimmed || void 0;
|
|
622
|
+
}
|
|
623
|
+
function isVisible(el) {
|
|
624
|
+
if (!el.getBoundingClientRect) return true;
|
|
625
|
+
const rect = el.getBoundingClientRect();
|
|
626
|
+
return rect.width > 0 && rect.height > 0;
|
|
627
|
+
}
|
|
628
|
+
function relativeBox(rect, viewport) {
|
|
629
|
+
const pct = (value, base) => base > 0 ? Math.max(0, Math.min(100, Math.round(value / base * 100))) : 0;
|
|
630
|
+
return [pct(rect.x, viewport.width), pct(rect.y + viewport.scrollY, viewport.height), pct(rect.width, viewport.width), pct(rect.height, viewport.height)];
|
|
631
|
+
}
|
|
632
|
+
function describeSkeletonElement(el, viewport) {
|
|
633
|
+
const out = { t: el.tagName.toLowerCase() };
|
|
634
|
+
const role = attr(el, "role", 40);
|
|
635
|
+
if (role) out.r = role;
|
|
636
|
+
const testid = attr(el, "data-testid", 80);
|
|
637
|
+
if (testid) out.id = testid;
|
|
638
|
+
const name = attr(el, "name", 80);
|
|
639
|
+
if (name) out.n = name;
|
|
640
|
+
const label = attr(el, "aria-label", 80);
|
|
641
|
+
if (label) out.l = label;
|
|
642
|
+
if (out.t === "input" || out.t === "button") {
|
|
643
|
+
const type = attr(el, "type", 20);
|
|
644
|
+
if (type) out.k = type.toLowerCase();
|
|
645
|
+
}
|
|
646
|
+
if (viewport && el.getBoundingClientRect) out.b = relativeBox(el.getBoundingClientRect(), viewport);
|
|
647
|
+
return out;
|
|
648
|
+
}
|
|
649
|
+
function collectSkeleton(root, path, viewport) {
|
|
650
|
+
const found = root.querySelectorAll(SKELETON_SELECTOR);
|
|
651
|
+
const visible = [];
|
|
652
|
+
for (let i = 0; i < found.length; i++) {
|
|
653
|
+
const el = found[i];
|
|
654
|
+
if (el.tagName.toLowerCase() === "input" && (el.getAttribute("type") ?? "").toLowerCase() === "hidden") continue;
|
|
655
|
+
if (!isVisible(el)) continue;
|
|
656
|
+
visible.push(el);
|
|
657
|
+
}
|
|
658
|
+
const elements = visible.slice(0, SKELETON_MAX_ELEMENTS).map((el) => describeSkeletonElement(el, viewport));
|
|
659
|
+
const structural = elements.map(({ b: _b, ...rest }) => rest);
|
|
660
|
+
return { path, hash: fnv1a(JSON.stringify(structural)), count: visible.length, elements };
|
|
661
|
+
}
|
|
662
|
+
function loadSent() {
|
|
663
|
+
try {
|
|
664
|
+
const raw = sessionStorage.getItem(SENT_STORAGE_KEY);
|
|
665
|
+
const list = raw ? JSON.parse(raw) : [];
|
|
666
|
+
return new Set(Array.isArray(list) ? list.filter((x) => typeof x === "string") : []);
|
|
667
|
+
} catch {
|
|
668
|
+
return /* @__PURE__ */ new Set();
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
function saveSent(sent) {
|
|
672
|
+
try {
|
|
673
|
+
sessionStorage.setItem(SENT_STORAGE_KEY, JSON.stringify([...sent].slice(-SENT_STORAGE_MAX)));
|
|
674
|
+
} catch {
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
function installSkeletonCapture(opts) {
|
|
679
|
+
if (typeof window === "undefined" || typeof document === "undefined") {
|
|
680
|
+
return { onNavigate: () => void 0, teardown: () => void 0 };
|
|
681
|
+
}
|
|
682
|
+
const delay = opts.delayMs ?? SKELETON_DELAY_MS;
|
|
683
|
+
const sent = loadSent();
|
|
684
|
+
let pending = null;
|
|
685
|
+
const capture = (path) => {
|
|
686
|
+
pending = null;
|
|
687
|
+
if (location.pathname !== path) return;
|
|
688
|
+
try {
|
|
689
|
+
const viewport = {
|
|
690
|
+
width: window.innerWidth,
|
|
691
|
+
height: Math.max(document.documentElement.scrollHeight, window.innerHeight),
|
|
692
|
+
scrollY: window.scrollY
|
|
693
|
+
};
|
|
694
|
+
const skeleton = collectSkeleton(document, path, viewport);
|
|
695
|
+
const key = `${path}:${skeleton.hash}`;
|
|
696
|
+
if (sent.has(key)) return;
|
|
697
|
+
sent.add(key);
|
|
698
|
+
saveSent(sent);
|
|
699
|
+
opts.onSkeleton(skeleton);
|
|
700
|
+
} catch {
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
};
|
|
704
|
+
return {
|
|
705
|
+
onNavigate: (path) => {
|
|
706
|
+
if (pending) clearTimeout(pending);
|
|
707
|
+
pending = setTimeout(() => capture(path), delay);
|
|
708
|
+
},
|
|
709
|
+
teardown: () => {
|
|
710
|
+
if (pending) clearTimeout(pending);
|
|
711
|
+
pending = null;
|
|
712
|
+
}
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
|
|
603
716
|
// src/web-context.ts
|
|
604
717
|
var UTM_KEYS = [
|
|
605
718
|
"utm_source",
|
|
@@ -718,7 +831,7 @@ function buildWebContext() {
|
|
|
718
831
|
}
|
|
719
832
|
|
|
720
833
|
// src/arm.ts
|
|
721
|
-
function
|
|
834
|
+
function fnv1a2(input) {
|
|
722
835
|
let hash = 2166136261;
|
|
723
836
|
for (let i = 0; i < input.length; i++) {
|
|
724
837
|
hash ^= input.charCodeAt(i);
|
|
@@ -727,7 +840,7 @@ function fnv1a(input) {
|
|
|
727
840
|
return hash >>> 0;
|
|
728
841
|
}
|
|
729
842
|
function changeBucket(changeId, accountKey) {
|
|
730
|
-
return
|
|
843
|
+
return fnv1a2(`${changeId}:${accountKey}`) % 100;
|
|
731
844
|
}
|
|
732
845
|
function changeArm(changeId, accountKey, holdoutPct) {
|
|
733
846
|
return changeBucket(changeId, accountKey) < holdoutPct ? "holdout" : "change";
|
|
@@ -1017,6 +1130,7 @@ var HoldyWeb = class extends HoldyCore {
|
|
|
1017
1130
|
teardownAutoCapture = null;
|
|
1018
1131
|
friction = null;
|
|
1019
1132
|
changes = null;
|
|
1133
|
+
skeleton = null;
|
|
1020
1134
|
constructor(opts) {
|
|
1021
1135
|
super("sdk_web", opts);
|
|
1022
1136
|
this.anonymousId = resolveAnonymousId();
|
|
@@ -1034,12 +1148,19 @@ var HoldyWeb = class extends HoldyCore {
|
|
|
1034
1148
|
onFriction: (signal) => this.track(`$${signal.kind}`, signal.properties)
|
|
1035
1149
|
});
|
|
1036
1150
|
}
|
|
1151
|
+
if (opts.skeleton !== false) {
|
|
1152
|
+
this.skeleton = installSkeletonCapture({
|
|
1153
|
+
onSkeleton: (skeleton) => this.track("$page_skeleton", { ...skeleton })
|
|
1154
|
+
});
|
|
1155
|
+
}
|
|
1037
1156
|
if (opts.autoCapture !== false) {
|
|
1038
1157
|
this.teardownAutoCapture = installAutoCapture({
|
|
1039
1158
|
onNavigate: (kind) => {
|
|
1040
1159
|
this.page(void 0, { navigation: kind });
|
|
1041
|
-
|
|
1160
|
+
const path = typeof location !== "undefined" ? location.pathname : "/";
|
|
1161
|
+
this.friction?.onNavigate(path);
|
|
1042
1162
|
this.changes?.onNavigate();
|
|
1163
|
+
this.skeleton?.onNavigate(path);
|
|
1043
1164
|
}
|
|
1044
1165
|
});
|
|
1045
1166
|
}
|
|
@@ -1089,6 +1210,8 @@ var HoldyWeb = class extends HoldyCore {
|
|
|
1089
1210
|
this.friction = null;
|
|
1090
1211
|
this.changes?.teardown();
|
|
1091
1212
|
this.changes = null;
|
|
1213
|
+
this.skeleton?.teardown();
|
|
1214
|
+
this.skeleton = null;
|
|
1092
1215
|
await super.shutdown();
|
|
1093
1216
|
}
|
|
1094
1217
|
environmentContext() {
|