@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.cjs
CHANGED
|
@@ -630,6 +630,119 @@ function installFrictionCapture(opts) {
|
|
|
630
630
|
};
|
|
631
631
|
}
|
|
632
632
|
|
|
633
|
+
// src/skeleton.ts
|
|
634
|
+
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]';
|
|
635
|
+
var SKELETON_MAX_ELEMENTS = 80;
|
|
636
|
+
var SKELETON_DELAY_MS = 1500;
|
|
637
|
+
var SENT_STORAGE_KEY = "holdy_skeletons";
|
|
638
|
+
var SENT_STORAGE_MAX = 200;
|
|
639
|
+
function fnv1a(input) {
|
|
640
|
+
let hash = 2166136261;
|
|
641
|
+
for (let i = 0; i < input.length; i++) {
|
|
642
|
+
hash ^= input.charCodeAt(i);
|
|
643
|
+
hash = Math.imul(hash, 16777619) >>> 0;
|
|
644
|
+
}
|
|
645
|
+
return hash.toString(16).padStart(8, "0");
|
|
646
|
+
}
|
|
647
|
+
function attr(el, name, max) {
|
|
648
|
+
const value = el.getAttribute(name);
|
|
649
|
+
if (!value) return void 0;
|
|
650
|
+
const trimmed = value.replace(/\s+/g, " ").trim().slice(0, max);
|
|
651
|
+
return trimmed || void 0;
|
|
652
|
+
}
|
|
653
|
+
function isVisible(el) {
|
|
654
|
+
if (!el.getBoundingClientRect) return true;
|
|
655
|
+
const rect = el.getBoundingClientRect();
|
|
656
|
+
return rect.width > 0 && rect.height > 0;
|
|
657
|
+
}
|
|
658
|
+
function relativeBox(rect, viewport) {
|
|
659
|
+
const pct = (value, base) => base > 0 ? Math.max(0, Math.min(100, Math.round(value / base * 100))) : 0;
|
|
660
|
+
return [pct(rect.x, viewport.width), pct(rect.y + viewport.scrollY, viewport.height), pct(rect.width, viewport.width), pct(rect.height, viewport.height)];
|
|
661
|
+
}
|
|
662
|
+
function describeSkeletonElement(el, viewport) {
|
|
663
|
+
const out = { t: el.tagName.toLowerCase() };
|
|
664
|
+
const role = attr(el, "role", 40);
|
|
665
|
+
if (role) out.r = role;
|
|
666
|
+
const testid = attr(el, "data-testid", 80);
|
|
667
|
+
if (testid) out.id = testid;
|
|
668
|
+
const name = attr(el, "name", 80);
|
|
669
|
+
if (name) out.n = name;
|
|
670
|
+
const label = attr(el, "aria-label", 80);
|
|
671
|
+
if (label) out.l = label;
|
|
672
|
+
if (out.t === "input" || out.t === "button") {
|
|
673
|
+
const type = attr(el, "type", 20);
|
|
674
|
+
if (type) out.k = type.toLowerCase();
|
|
675
|
+
}
|
|
676
|
+
if (viewport && el.getBoundingClientRect) out.b = relativeBox(el.getBoundingClientRect(), viewport);
|
|
677
|
+
return out;
|
|
678
|
+
}
|
|
679
|
+
function collectSkeleton(root, path, viewport) {
|
|
680
|
+
const found = root.querySelectorAll(SKELETON_SELECTOR);
|
|
681
|
+
const visible = [];
|
|
682
|
+
for (let i = 0; i < found.length; i++) {
|
|
683
|
+
const el = found[i];
|
|
684
|
+
if (el.tagName.toLowerCase() === "input" && (el.getAttribute("type") ?? "").toLowerCase() === "hidden") continue;
|
|
685
|
+
if (!isVisible(el)) continue;
|
|
686
|
+
visible.push(el);
|
|
687
|
+
}
|
|
688
|
+
const elements = visible.slice(0, SKELETON_MAX_ELEMENTS).map((el) => describeSkeletonElement(el, viewport));
|
|
689
|
+
const structural = elements.map(({ b: _b, ...rest }) => rest);
|
|
690
|
+
return { path, hash: fnv1a(JSON.stringify(structural)), count: visible.length, elements };
|
|
691
|
+
}
|
|
692
|
+
function loadSent() {
|
|
693
|
+
try {
|
|
694
|
+
const raw = sessionStorage.getItem(SENT_STORAGE_KEY);
|
|
695
|
+
const list = raw ? JSON.parse(raw) : [];
|
|
696
|
+
return new Set(Array.isArray(list) ? list.filter((x) => typeof x === "string") : []);
|
|
697
|
+
} catch {
|
|
698
|
+
return /* @__PURE__ */ new Set();
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
function saveSent(sent) {
|
|
702
|
+
try {
|
|
703
|
+
sessionStorage.setItem(SENT_STORAGE_KEY, JSON.stringify([...sent].slice(-SENT_STORAGE_MAX)));
|
|
704
|
+
} catch {
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
function installSkeletonCapture(opts) {
|
|
709
|
+
if (typeof window === "undefined" || typeof document === "undefined") {
|
|
710
|
+
return { onNavigate: () => void 0, teardown: () => void 0 };
|
|
711
|
+
}
|
|
712
|
+
const delay = opts.delayMs ?? SKELETON_DELAY_MS;
|
|
713
|
+
const sent = loadSent();
|
|
714
|
+
let pending = null;
|
|
715
|
+
const capture = (path) => {
|
|
716
|
+
pending = null;
|
|
717
|
+
if (location.pathname !== path) return;
|
|
718
|
+
try {
|
|
719
|
+
const viewport = {
|
|
720
|
+
width: window.innerWidth,
|
|
721
|
+
height: Math.max(document.documentElement.scrollHeight, window.innerHeight),
|
|
722
|
+
scrollY: window.scrollY
|
|
723
|
+
};
|
|
724
|
+
const skeleton = collectSkeleton(document, path, viewport);
|
|
725
|
+
const key = `${path}:${skeleton.hash}`;
|
|
726
|
+
if (sent.has(key)) return;
|
|
727
|
+
sent.add(key);
|
|
728
|
+
saveSent(sent);
|
|
729
|
+
opts.onSkeleton(skeleton);
|
|
730
|
+
} catch {
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
};
|
|
734
|
+
return {
|
|
735
|
+
onNavigate: (path) => {
|
|
736
|
+
if (pending) clearTimeout(pending);
|
|
737
|
+
pending = setTimeout(() => capture(path), delay);
|
|
738
|
+
},
|
|
739
|
+
teardown: () => {
|
|
740
|
+
if (pending) clearTimeout(pending);
|
|
741
|
+
pending = null;
|
|
742
|
+
}
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
|
|
633
746
|
// src/web-context.ts
|
|
634
747
|
var UTM_KEYS = [
|
|
635
748
|
"utm_source",
|
|
@@ -748,7 +861,7 @@ function buildWebContext() {
|
|
|
748
861
|
}
|
|
749
862
|
|
|
750
863
|
// src/arm.ts
|
|
751
|
-
function
|
|
864
|
+
function fnv1a2(input) {
|
|
752
865
|
let hash = 2166136261;
|
|
753
866
|
for (let i = 0; i < input.length; i++) {
|
|
754
867
|
hash ^= input.charCodeAt(i);
|
|
@@ -757,7 +870,7 @@ function fnv1a(input) {
|
|
|
757
870
|
return hash >>> 0;
|
|
758
871
|
}
|
|
759
872
|
function changeBucket(changeId, accountKey) {
|
|
760
|
-
return
|
|
873
|
+
return fnv1a2(`${changeId}:${accountKey}`) % 100;
|
|
761
874
|
}
|
|
762
875
|
function changeArm(changeId, accountKey, holdoutPct) {
|
|
763
876
|
return changeBucket(changeId, accountKey) < holdoutPct ? "holdout" : "change";
|
|
@@ -1047,6 +1160,7 @@ var HoldyWeb = class extends HoldyCore {
|
|
|
1047
1160
|
teardownAutoCapture = null;
|
|
1048
1161
|
friction = null;
|
|
1049
1162
|
changes = null;
|
|
1163
|
+
skeleton = null;
|
|
1050
1164
|
constructor(opts) {
|
|
1051
1165
|
super("sdk_web", opts);
|
|
1052
1166
|
this.anonymousId = resolveAnonymousId();
|
|
@@ -1064,12 +1178,19 @@ var HoldyWeb = class extends HoldyCore {
|
|
|
1064
1178
|
onFriction: (signal) => this.track(`$${signal.kind}`, signal.properties)
|
|
1065
1179
|
});
|
|
1066
1180
|
}
|
|
1181
|
+
if (opts.skeleton !== false) {
|
|
1182
|
+
this.skeleton = installSkeletonCapture({
|
|
1183
|
+
onSkeleton: (skeleton) => this.track("$page_skeleton", { ...skeleton })
|
|
1184
|
+
});
|
|
1185
|
+
}
|
|
1067
1186
|
if (opts.autoCapture !== false) {
|
|
1068
1187
|
this.teardownAutoCapture = installAutoCapture({
|
|
1069
1188
|
onNavigate: (kind) => {
|
|
1070
1189
|
this.page(void 0, { navigation: kind });
|
|
1071
|
-
|
|
1190
|
+
const path = typeof location !== "undefined" ? location.pathname : "/";
|
|
1191
|
+
this.friction?.onNavigate(path);
|
|
1072
1192
|
this.changes?.onNavigate();
|
|
1193
|
+
this.skeleton?.onNavigate(path);
|
|
1073
1194
|
}
|
|
1074
1195
|
});
|
|
1075
1196
|
}
|
|
@@ -1119,6 +1240,8 @@ var HoldyWeb = class extends HoldyCore {
|
|
|
1119
1240
|
this.friction = null;
|
|
1120
1241
|
this.changes?.teardown();
|
|
1121
1242
|
this.changes = null;
|
|
1243
|
+
this.skeleton?.teardown();
|
|
1244
|
+
this.skeleton = null;
|
|
1122
1245
|
await super.shutdown();
|
|
1123
1246
|
}
|
|
1124
1247
|
environmentContext() {
|