@obsrviq/tracker 0.3.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/dist/index.d.ts +13 -0
- package/dist/index.js +99 -5
- package/dist/index.js.map +1 -1
- package/dist/obsrviq.global.js +24 -24
- package/dist/obsrviq.global.js.map +1 -1
- package/package.json +2 -2
- package/src/context.ts +2 -0
- package/src/forms.ts +75 -0
- package/src/global.ts +5 -1
- package/src/index.ts +24 -0
- package/src/interactions.ts +24 -0
- package/src/scroll.ts +10 -4
package/dist/index.d.ts
CHANGED
|
@@ -18,6 +18,9 @@ declare class ObsrviqClient {
|
|
|
18
18
|
private recording;
|
|
19
19
|
private consent;
|
|
20
20
|
private sampledIn;
|
|
21
|
+
/** Manual-control intent. In autoStart:false mode, recording only begins once
|
|
22
|
+
* the host calls startRecording() (which sets this); stopRecording() clears it. */
|
|
23
|
+
private manualStartRequested;
|
|
21
24
|
private lastActivity;
|
|
22
25
|
/** The userId this session is attributed to (for the shared-device split). */
|
|
23
26
|
private currentUserId;
|
|
@@ -84,6 +87,16 @@ declare class ObsrviqClient {
|
|
|
84
87
|
/** Gate recording (PRIV-2). true starts; false stops + flushes. Dismisses the
|
|
85
88
|
* built-in consent prompt if one is showing. */
|
|
86
89
|
setConsent(granted: boolean): void;
|
|
90
|
+
/** Begin recording now. The trigger for `autoStart: false` (manual) mode —
|
|
91
|
+
* nothing records until this is called. Idempotent: a no-op while already
|
|
92
|
+
* recording. Consent, GPC/DNT and sampling still apply (so a consent-gated or
|
|
93
|
+
* sampled-out session won't start just because this was called). After a
|
|
94
|
+
* stopRecording(), this resumes the SAME session with the idle gap shown. */
|
|
95
|
+
startRecording(): void;
|
|
96
|
+
/** Stop recording and flush the final batch. Recording stays off until
|
|
97
|
+
* startRecording() is called again. (Friendlier name for stop(), and it also
|
|
98
|
+
* clears the manual-start intent so recording won't silently resume.) */
|
|
99
|
+
stopRecording(): void;
|
|
87
100
|
/** Hard stop + flush. */
|
|
88
101
|
stop(): void;
|
|
89
102
|
/** Force a flush now (returns when send is attempted). */
|
package/dist/index.js
CHANGED
|
@@ -1025,6 +1025,26 @@ function instrumentInteractions(ctx) {
|
|
|
1025
1025
|
if (!target || !(target instanceof Element)) return;
|
|
1026
1026
|
const t = ctx.clock.now();
|
|
1027
1027
|
const selector = cssPath(target);
|
|
1028
|
+
{
|
|
1029
|
+
const rect = target.getBoundingClientRect();
|
|
1030
|
+
const rx = rect.width > 0 ? Math.min(1, Math.max(0, (ev.clientX - rect.left) / rect.width)) : 0.5;
|
|
1031
|
+
const ry = rect.height > 0 ? Math.min(1, Math.max(0, (ev.clientY - rect.top) / rect.height)) : 0.5;
|
|
1032
|
+
const label = (target.textContent || "").replace(/\s+/g, " ").trim().slice(0, 40);
|
|
1033
|
+
emitCustom(
|
|
1034
|
+
"_click",
|
|
1035
|
+
{
|
|
1036
|
+
selector,
|
|
1037
|
+
path: location.pathname,
|
|
1038
|
+
vw: window.innerWidth,
|
|
1039
|
+
vh: window.innerHeight,
|
|
1040
|
+
rx: Math.round(rx * 1e3) / 1e3,
|
|
1041
|
+
ry: Math.round(ry * 1e3) / 1e3,
|
|
1042
|
+
tag: target.tagName.toLowerCase(),
|
|
1043
|
+
label
|
|
1044
|
+
},
|
|
1045
|
+
t
|
|
1046
|
+
);
|
|
1047
|
+
}
|
|
1028
1048
|
recent = recent.filter((r) => t - r.t <= RAGE_WINDOW_MS);
|
|
1029
1049
|
recent.push({ el: target, t });
|
|
1030
1050
|
const sameTarget = recent.filter((r) => r.el === target);
|
|
@@ -1049,6 +1069,7 @@ function instrumentInteractions(ctx) {
|
|
|
1049
1069
|
var TRACKED = /* @__PURE__ */ new Set(["INPUT", "TEXTAREA", "SELECT"]);
|
|
1050
1070
|
var SKIP_INPUT_TYPES = /* @__PURE__ */ new Set(["hidden", "submit", "button", "reset", "image", "file"]);
|
|
1051
1071
|
var MAX_KEY = 80;
|
|
1072
|
+
var MAX_HINT = 60;
|
|
1052
1073
|
var MAX_FOCUS_MS = 3e5;
|
|
1053
1074
|
var MIN_FOCUS_MS = 100;
|
|
1054
1075
|
function isTracked(el) {
|
|
@@ -1092,6 +1113,56 @@ function fieldKey(el) {
|
|
|
1092
1113
|
if (ph) return clip(ph);
|
|
1093
1114
|
return clip(cssPath(el));
|
|
1094
1115
|
}
|
|
1116
|
+
function clipHint(s) {
|
|
1117
|
+
if (!s) return void 0;
|
|
1118
|
+
const t = s.trim().replace(/\s+/g, " ");
|
|
1119
|
+
if (!t) return void 0;
|
|
1120
|
+
return t.length > MAX_HINT ? t.slice(0, MAX_HINT) : t;
|
|
1121
|
+
}
|
|
1122
|
+
function nearbyText(el) {
|
|
1123
|
+
if (el.labels && el.labels.length) {
|
|
1124
|
+
const t = clipHint(el.labels[0].textContent);
|
|
1125
|
+
if (t) return t;
|
|
1126
|
+
}
|
|
1127
|
+
const aria = clipHint(el.getAttribute("aria-label"));
|
|
1128
|
+
if (aria) return aria;
|
|
1129
|
+
let sib = el.previousElementSibling;
|
|
1130
|
+
for (let hops = 0; sib && hops < 3; hops++, sib = sib.previousElementSibling) {
|
|
1131
|
+
if (TRACKED.has(sib.tagName) || /^(BUTTON|SCRIPT|STYLE|SVG)$/.test(sib.tagName)) continue;
|
|
1132
|
+
const t = clipHint(sib.textContent);
|
|
1133
|
+
if (t) return t;
|
|
1134
|
+
}
|
|
1135
|
+
const parent = el.parentElement;
|
|
1136
|
+
if (parent) {
|
|
1137
|
+
const direct = clipHint(
|
|
1138
|
+
Array.from(parent.childNodes).filter((n) => n.nodeType === 3).map((n) => n.textContent || "").join(" ")
|
|
1139
|
+
);
|
|
1140
|
+
if (direct) return direct;
|
|
1141
|
+
const lbl = clipHint(parent.querySelector("label,legend")?.textContent);
|
|
1142
|
+
if (lbl) return lbl;
|
|
1143
|
+
}
|
|
1144
|
+
return clipHint(el.getAttribute("placeholder"));
|
|
1145
|
+
}
|
|
1146
|
+
function formTitleHint(el) {
|
|
1147
|
+
const scope = el.closest('form,fieldset,section,[role="form"],[role="group"]');
|
|
1148
|
+
if (scope) {
|
|
1149
|
+
const heading = clipHint(scope.querySelector('legend,h1,h2,h3,h4,[role="heading"]')?.textContent);
|
|
1150
|
+
if (heading) return heading;
|
|
1151
|
+
const al = clipHint(scope.getAttribute("aria-label"));
|
|
1152
|
+
if (al) return al;
|
|
1153
|
+
}
|
|
1154
|
+
return clipHint(typeof document !== "undefined" ? document.title : void 0);
|
|
1155
|
+
}
|
|
1156
|
+
function namingHints(el) {
|
|
1157
|
+
const out = {};
|
|
1158
|
+
const ac = (el.getAttribute("autocomplete") || "").trim().toLowerCase();
|
|
1159
|
+
if (ac && ac !== "off" && ac !== "on") out.autocomplete = ac.slice(0, MAX_HINT);
|
|
1160
|
+
const near = nearbyText(el);
|
|
1161
|
+
if (near) out.near = near;
|
|
1162
|
+
const ft = formTitleHint(el);
|
|
1163
|
+
if (ft) out.formTitle = ft;
|
|
1164
|
+
return out;
|
|
1165
|
+
}
|
|
1095
1166
|
function formKey(el) {
|
|
1096
1167
|
const form = el.form;
|
|
1097
1168
|
if (form) {
|
|
@@ -1145,7 +1216,8 @@ function instrumentForms(ctx) {
|
|
|
1145
1216
|
type: controlType(el),
|
|
1146
1217
|
ms,
|
|
1147
1218
|
changed,
|
|
1148
|
-
filled: isFilled(el)
|
|
1219
|
+
filled: isFilled(el),
|
|
1220
|
+
...namingHints(el)
|
|
1149
1221
|
});
|
|
1150
1222
|
};
|
|
1151
1223
|
const onFocusIn = (ev) => {
|
|
@@ -1188,7 +1260,7 @@ function instrumentScrollDepth(ctx) {
|
|
|
1188
1260
|
let path = location.pathname;
|
|
1189
1261
|
let maxBand = 0;
|
|
1190
1262
|
let scheduled = false;
|
|
1191
|
-
const emit = (depth) => {
|
|
1263
|
+
const emit = (depth, h, vh) => {
|
|
1192
1264
|
const e = {
|
|
1193
1265
|
id: uuid(),
|
|
1194
1266
|
sessionId: ctx.sessionId,
|
|
@@ -1196,7 +1268,7 @@ function instrumentScrollDepth(ctx) {
|
|
|
1196
1268
|
t: ctx.clock.now(),
|
|
1197
1269
|
ts: ctx.clock.wall(),
|
|
1198
1270
|
name: "_scroll",
|
|
1199
|
-
props: { path, depth }
|
|
1271
|
+
props: { path, depth, h, vh }
|
|
1200
1272
|
};
|
|
1201
1273
|
ctx.emit(e);
|
|
1202
1274
|
};
|
|
@@ -1212,11 +1284,12 @@ function instrumentScrollDepth(ctx) {
|
|
|
1212
1284
|
maxBand = 0;
|
|
1213
1285
|
}
|
|
1214
1286
|
const vh = window.innerHeight || document.documentElement.clientHeight || 1;
|
|
1215
|
-
const
|
|
1287
|
+
const h = docHeight();
|
|
1288
|
+
const reached = Math.min(1, (window.scrollY + vh) / h);
|
|
1216
1289
|
const band = Math.min(100, Math.round(reached * 10) * 10);
|
|
1217
1290
|
if (band > maxBand) {
|
|
1218
1291
|
maxBand = band;
|
|
1219
|
-
emit(band);
|
|
1292
|
+
emit(band, h, vh);
|
|
1220
1293
|
}
|
|
1221
1294
|
};
|
|
1222
1295
|
const onScroll = () => {
|
|
@@ -1498,6 +1571,7 @@ var DEFAULTS = {
|
|
|
1498
1571
|
requireConsent: false,
|
|
1499
1572
|
askPermission: false,
|
|
1500
1573
|
showRecording: false,
|
|
1574
|
+
autoStart: true,
|
|
1501
1575
|
maxArgBytes: 8 * 1024
|
|
1502
1576
|
};
|
|
1503
1577
|
var ObsrviqClient = class {
|
|
@@ -1510,6 +1584,9 @@ var ObsrviqClient = class {
|
|
|
1510
1584
|
this.recording = false;
|
|
1511
1585
|
this.consent = null;
|
|
1512
1586
|
this.sampledIn = true;
|
|
1587
|
+
/** Manual-control intent. In autoStart:false mode, recording only begins once
|
|
1588
|
+
* the host calls startRecording() (which sets this); stopRecording() clears it. */
|
|
1589
|
+
this.manualStartRequested = false;
|
|
1513
1590
|
this.lastActivity = 0;
|
|
1514
1591
|
/** Next batch seq — mirrored from the transport so we can persist continuation. */
|
|
1515
1592
|
this.nextSeq = 0;
|
|
@@ -1630,6 +1707,7 @@ var ObsrviqClient = class {
|
|
|
1630
1707
|
}
|
|
1631
1708
|
maybeStart() {
|
|
1632
1709
|
if (this.recording || !this.config || !this.clock || !this.transport) return;
|
|
1710
|
+
if (this.config.autoStart === false && !this.manualStartRequested) return;
|
|
1633
1711
|
if (this.config.requireConsent && this.consent !== true) return;
|
|
1634
1712
|
if (this.consent === false) return;
|
|
1635
1713
|
if (!this.sampledIn) return;
|
|
@@ -1787,6 +1865,22 @@ var ObsrviqClient = class {
|
|
|
1787
1865
|
if (granted) this.maybeStart();
|
|
1788
1866
|
else if (this.recording) this.teardown();
|
|
1789
1867
|
}
|
|
1868
|
+
/** Begin recording now. The trigger for `autoStart: false` (manual) mode —
|
|
1869
|
+
* nothing records until this is called. Idempotent: a no-op while already
|
|
1870
|
+
* recording. Consent, GPC/DNT and sampling still apply (so a consent-gated or
|
|
1871
|
+
* sampled-out session won't start just because this was called). After a
|
|
1872
|
+
* stopRecording(), this resumes the SAME session with the idle gap shown. */
|
|
1873
|
+
startRecording() {
|
|
1874
|
+
this.manualStartRequested = true;
|
|
1875
|
+
this.maybeStart();
|
|
1876
|
+
}
|
|
1877
|
+
/** Stop recording and flush the final batch. Recording stays off until
|
|
1878
|
+
* startRecording() is called again. (Friendlier name for stop(), and it also
|
|
1879
|
+
* clears the manual-start intent so recording won't silently resume.) */
|
|
1880
|
+
stopRecording() {
|
|
1881
|
+
this.manualStartRequested = false;
|
|
1882
|
+
if (this.recording) this.teardown();
|
|
1883
|
+
}
|
|
1790
1884
|
/** Hard stop + flush. */
|
|
1791
1885
|
stop() {
|
|
1792
1886
|
this.teardown();
|