@tamsensedev/dataclient 0.1.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.cjs ADDED
@@ -0,0 +1,979 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ DataClient: () => DataClient
24
+ });
25
+ module.exports = __toCommonJS(index_exports);
26
+
27
+ // src/constants.ts
28
+ var MASK_ATTR = "dataclient-mask";
29
+ var MASK_SELECTOR = `[${MASK_ATTR}]`;
30
+ var SKIP_TAGS = /* @__PURE__ */ new Set(["script", "style", "noscript", "svg", "link", "meta", "head"]);
31
+ var RECORD_ATTRS = /* @__PURE__ */ new Set([
32
+ "class",
33
+ "role",
34
+ "href",
35
+ "type",
36
+ "placeholder",
37
+ "disabled",
38
+ "hidden",
39
+ "aria-label",
40
+ "aria-selected",
41
+ "aria-expanded",
42
+ "aria-invalid",
43
+ "aria-busy",
44
+ "aria-checked",
45
+ "aria-hidden",
46
+ "value",
47
+ "name",
48
+ "id",
49
+ "for",
50
+ "target",
51
+ "data-state",
52
+ MASK_ATTR
53
+ ]);
54
+ var WATCH_ATTRS = [
55
+ "class",
56
+ "role",
57
+ "href",
58
+ "disabled",
59
+ "hidden",
60
+ "placeholder",
61
+ "aria-label",
62
+ "aria-selected",
63
+ "aria-expanded",
64
+ "aria-invalid",
65
+ "aria-busy",
66
+ "aria-checked",
67
+ "aria-hidden",
68
+ "data-state",
69
+ "value"
70
+ ];
71
+ var TEXT_INPUT_TYPES = /* @__PURE__ */ new Set(["text", "email", "password", "search", "tel", "url", "number"]);
72
+ var INTERACTIVE_TAGS = /* @__PURE__ */ new Set(["button", "a", "input", "select", "textarea"]);
73
+ var INTERACTIVE_ROLES = /* @__PURE__ */ new Set(["button", "link", "tab", "menuitem", "checkbox", "radio", "switch", "option"]);
74
+
75
+ // src/dom/mask.ts
76
+ function isMasked(el) {
77
+ if (!el)
78
+ return false;
79
+ const element = el.nodeType === Node.ELEMENT_NODE ? el : el.parentElement;
80
+ return !!element?.closest(MASK_SELECTOR);
81
+ }
82
+ function maskText(text) {
83
+ if (!text) {
84
+ return text;
85
+ }
86
+ const visible = Math.max(1, Math.ceil(text.length * 0.2));
87
+ return text.slice(0, visible) + "*".repeat(text.length - visible);
88
+ }
89
+
90
+ // src/dom/icon.ts
91
+ var ICON_CLASS_PATTERNS = [
92
+ /\bfa-([a-z0-9-]+)\b/,
93
+ /\bmdi-([a-z0-9-]+)\b/,
94
+ /\bbi-([a-z0-9-]+)\b/,
95
+ /\bicon-([a-z0-9-]+)\b/,
96
+ /\blucide-([a-z0-9-]+)\b/,
97
+ /\bri-([a-z0-9-]+)\b/,
98
+ /\btabler-icon-([a-z0-9-]+)\b/,
99
+ /\bi-[a-z0-9-]+[:/]([a-z0-9-]+)\b/
100
+ ];
101
+ var MATERIAL_CLASS_PATTERN = /\b(?:material-icons|material-symbols-[a-z]+)\b/;
102
+ var SVG_USE_HREF_PATTERN = /#(.+)/;
103
+ var MAX_ICON_IMG_SIZE = 48;
104
+ function getElementIcon(el) {
105
+ if (!el) {
106
+ return null;
107
+ }
108
+ const direct = detectIcon(el);
109
+ if (direct) {
110
+ return direct;
111
+ }
112
+ for (const child of el.children) {
113
+ const icon = detectIcon(child);
114
+ if (icon) {
115
+ return icon;
116
+ }
117
+ }
118
+ return null;
119
+ }
120
+ function detectIcon(el) {
121
+ if (!el?.tagName) {
122
+ return null;
123
+ }
124
+ const tag = el.tagName.toLowerCase();
125
+ if (tag === "svg") {
126
+ return detectSvgIcon(el);
127
+ }
128
+ if (tag === "i" || tag === "span" || el.classList.contains("iconify")) {
129
+ return detectFontIcon(el);
130
+ }
131
+ if (tag === "img") {
132
+ return detectImgIcon(el);
133
+ }
134
+ const svg = el.querySelector("svg");
135
+ if (svg) {
136
+ return detectSvgIcon(svg);
137
+ }
138
+ return null;
139
+ }
140
+ function detectSvgIcon(svg) {
141
+ const lucide = svg.getAttribute("data-lucide");
142
+ if (lucide) {
143
+ return lucide;
144
+ }
145
+ const dataIcon = svg.getAttribute("data-icon");
146
+ if (dataIcon) {
147
+ return dataIcon;
148
+ }
149
+ const use = svg.querySelector("use");
150
+ if (use) {
151
+ const href = use.getAttribute("href") || use.getAttribute("xlink:href");
152
+ if (href) {
153
+ const match = href.match(SVG_USE_HREF_PATTERN);
154
+ if (match) {
155
+ return match[1];
156
+ }
157
+ }
158
+ }
159
+ const ariaLabel = svg.getAttribute("aria-label");
160
+ if (ariaLabel) {
161
+ return ariaLabel;
162
+ }
163
+ const cls = typeof svg.className === "string" ? svg.className : svg.getAttribute("class") || "";
164
+ const fromClass = extractIconNameFromClass(cls);
165
+ if (fromClass) {
166
+ return fromClass;
167
+ }
168
+ return null;
169
+ }
170
+ function detectFontIcon(el) {
171
+ const cls = typeof el.className === "string" ? el.className : el.getAttribute("class") || "";
172
+ if (MATERIAL_CLASS_PATTERN.test(cls)) {
173
+ const text = el.textContent?.trim();
174
+ if (text) {
175
+ return text;
176
+ }
177
+ }
178
+ return extractIconNameFromClass(cls);
179
+ }
180
+ function detectImgIcon(img) {
181
+ if (img.width > MAX_ICON_IMG_SIZE || img.height > MAX_ICON_IMG_SIZE) {
182
+ return null;
183
+ }
184
+ if (img.naturalWidth > MAX_ICON_IMG_SIZE || img.naturalHeight > MAX_ICON_IMG_SIZE) {
185
+ return null;
186
+ }
187
+ const alt = img.alt?.trim();
188
+ if (alt) {
189
+ return alt;
190
+ }
191
+ const src = img.getAttribute("src");
192
+ if (src) {
193
+ const filename = src.split("/").pop()?.split("?")[0]?.split(".")[0];
194
+ if (filename) {
195
+ return filename;
196
+ }
197
+ }
198
+ return null;
199
+ }
200
+ function extractIconNameFromClass(cls) {
201
+ for (const pattern of ICON_CLASS_PATTERNS) {
202
+ const match = cls.match(pattern);
203
+ if (match) {
204
+ return match[1];
205
+ }
206
+ }
207
+ return null;
208
+ }
209
+
210
+ // src/dom/serializer.ts
211
+ var nextId = 1;
212
+ var nodeToId = /* @__PURE__ */ new WeakMap();
213
+ var idToNode = /* @__PURE__ */ new Map();
214
+ function resetIds() {
215
+ nextId = 1;
216
+ idToNode.clear();
217
+ }
218
+ function assignId(node) {
219
+ const existing = nodeToId.get(node);
220
+ if (existing) {
221
+ return existing;
222
+ }
223
+ const id = nextId++;
224
+ nodeToId.set(node, id);
225
+ idToNode.set(id, node);
226
+ return id;
227
+ }
228
+ function getNodeId(node) {
229
+ return nodeToId.get(node) ?? null;
230
+ }
231
+ function removeNodeId(id) {
232
+ const node = idToNode.get(id);
233
+ if (node) {
234
+ nodeToId.delete(node);
235
+ idToNode.delete(id);
236
+ }
237
+ }
238
+ function isVisible(el) {
239
+ if (el.hidden)
240
+ return false;
241
+ if (el.getAttribute("aria-hidden") === "true")
242
+ return false;
243
+ if (!el.offsetParent && el.tagName !== "BODY" && getComputedStyle(el).position !== "fixed") {
244
+ return false;
245
+ }
246
+ return true;
247
+ }
248
+ function getDirectText(el) {
249
+ let text = "";
250
+ for (const child of el.childNodes) {
251
+ if (child.nodeType === Node.TEXT_NODE) {
252
+ const t = child.textContent?.trim();
253
+ if (t) {
254
+ text += (text ? " " : "") + t;
255
+ }
256
+ }
257
+ }
258
+ const truncated = text.slice(0, 200);
259
+ return isMasked(el) ? maskText(truncated) : truncated;
260
+ }
261
+ function getAttrs(el) {
262
+ const attrs = {};
263
+ let hasAttrs = false;
264
+ const masked = isMasked(el);
265
+ for (const name of RECORD_ATTRS) {
266
+ const value = el.getAttribute(name);
267
+ if (value !== null && value !== "") {
268
+ let v = value.slice(0, 200);
269
+ if (masked && (name === "value" || name === "placeholder")) {
270
+ v = maskText(v);
271
+ }
272
+ attrs[name] = v;
273
+ hasAttrs = true;
274
+ }
275
+ }
276
+ return hasAttrs ? attrs : void 0;
277
+ }
278
+ function getRect(el) {
279
+ const r = el.getBoundingClientRect();
280
+ if (r.width === 0 && r.height === 0) {
281
+ return void 0;
282
+ }
283
+ return {
284
+ x: Math.round(r.left + window.scrollX),
285
+ y: Math.round(r.top + window.scrollY),
286
+ w: Math.round(r.width),
287
+ h: Math.round(r.height)
288
+ };
289
+ }
290
+ function serializeNode(el) {
291
+ const tag = el.tagName?.toLowerCase();
292
+ if (!tag || SKIP_TAGS.has(tag)) {
293
+ return null;
294
+ }
295
+ if (!isVisible(el)) {
296
+ return null;
297
+ }
298
+ const id = assignId(el);
299
+ const text = getDirectText(el);
300
+ const icon = getElementIcon(el);
301
+ const attrs = getAttrs(el);
302
+ const rect = getRect(el);
303
+ const children = [];
304
+ for (const child of el.children) {
305
+ const serialized = serializeNode(child);
306
+ if (serialized) {
307
+ children.push(serialized.id);
308
+ }
309
+ }
310
+ const node = { id, tag };
311
+ if (text) {
312
+ node.text = text;
313
+ }
314
+ if (icon) {
315
+ node.icon = icon;
316
+ }
317
+ if (attrs) {
318
+ node.attrs = attrs;
319
+ }
320
+ if (rect) {
321
+ node.rect = rect;
322
+ }
323
+ if (children.length > 0) {
324
+ node.children = children;
325
+ }
326
+ return node;
327
+ }
328
+ function serializeTree(root) {
329
+ const nodes = [];
330
+ function walk(el) {
331
+ const node = serializeNode(el);
332
+ if (!node) {
333
+ return;
334
+ }
335
+ nodes.push(node);
336
+ for (const child of el.children) {
337
+ walk(child);
338
+ }
339
+ }
340
+ walk(root);
341
+ return nodes;
342
+ }
343
+
344
+ // src/dom/viewport.ts
345
+ function getViewport() {
346
+ return {
347
+ scrollX: Math.round(window.scrollX),
348
+ scrollY: Math.round(window.scrollY),
349
+ width: window.innerWidth,
350
+ height: window.innerHeight
351
+ };
352
+ }
353
+
354
+ // src/trackers/rrweb.ts
355
+ var import_rrweb = require("rrweb");
356
+ var RrwebTracker = class {
357
+ constructor(config, sender) {
358
+ this.config = config;
359
+ this.sender = sender;
360
+ }
361
+ config;
362
+ sender;
363
+ stopFn = null;
364
+ start() {
365
+ this.stopFn = (0, import_rrweb.record)({
366
+ emit: (event) => {
367
+ const rrwebEvent = {
368
+ event: "rrweb",
369
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
370
+ rrwebEvent: event
371
+ };
372
+ this.sender.add(rrwebEvent);
373
+ },
374
+ recordCrossOriginIframes: false,
375
+ recordCanvas: false,
376
+ maskTextSelector: MASK_SELECTOR,
377
+ maskInputOptions: { password: true },
378
+ maskTextFn: (text) => "*".repeat(text.length),
379
+ sampling: {
380
+ mousemove: false,
381
+ mouseInteraction: true,
382
+ scroll: 500,
383
+ input: "last"
384
+ }
385
+ }) ?? null;
386
+ if (this.config.debug)
387
+ console.log("[dataclient] rrweb recording started");
388
+ }
389
+ stop() {
390
+ if (this.stopFn) {
391
+ this.stopFn();
392
+ this.stopFn = null;
393
+ }
394
+ }
395
+ static addMarker(tag, payload) {
396
+ import_rrweb.record.addCustomEvent(tag, payload);
397
+ }
398
+ };
399
+
400
+ // src/trackers/action.ts
401
+ var ActionTracker = class {
402
+ constructor(config, sender) {
403
+ this.config = config;
404
+ this.sender = sender;
405
+ }
406
+ config;
407
+ sender;
408
+ inputTimers = /* @__PURE__ */ new WeakMap();
409
+ pendingInputs = /* @__PURE__ */ new Set();
410
+ handleClick = (e) => this.onClick(e);
411
+ handleInput = (e) => this.onInput(e);
412
+ handleChange = (e) => this.onChange(e);
413
+ start() {
414
+ document.addEventListener("click", this.handleClick, true);
415
+ document.addEventListener("input", this.handleInput, true);
416
+ document.addEventListener("change", this.handleChange, true);
417
+ }
418
+ stop() {
419
+ document.removeEventListener("click", this.handleClick, true);
420
+ document.removeEventListener("input", this.handleInput, true);
421
+ document.removeEventListener("change", this.handleChange, true);
422
+ }
423
+ beforeUnload() {
424
+ for (const el of this.pendingInputs) {
425
+ const timer = this.inputTimers.get(el);
426
+ if (timer) {
427
+ clearTimeout(timer);
428
+ }
429
+ this.inputTimers.delete(el);
430
+ this.recordInput(el);
431
+ }
432
+ this.pendingInputs.clear();
433
+ }
434
+ onClick(e) {
435
+ const raw = e.target;
436
+ if (!raw) {
437
+ return;
438
+ }
439
+ const tag = raw.tagName?.toLowerCase();
440
+ if (tag === "body" || tag === "html") {
441
+ return;
442
+ }
443
+ const el = this.findMeaningfulElement(raw);
444
+ const info = this.getElementInfo(el);
445
+ const action = {
446
+ event: "action",
447
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
448
+ type: "click",
449
+ targetId: info.targetId,
450
+ tag: info.tag,
451
+ text: info.text,
452
+ url: location.href,
453
+ state: info.state,
454
+ viewport: getViewport()
455
+ };
456
+ if (this.config.debug) {
457
+ console.log(`[dataclient] click: ${info.tag} "${info.text}"`);
458
+ }
459
+ this.sender.add(action);
460
+ RrwebTracker.addMarker("click", { tag: info.tag, text: info.text, label: `Click: ${info.text || info.tag}` });
461
+ }
462
+ onInput(e) {
463
+ const target = e.target;
464
+ if (!target) {
465
+ return;
466
+ }
467
+ const tag = target.tagName?.toLowerCase();
468
+ if (tag === "input") {
469
+ const type = target.type?.toLowerCase() || "text";
470
+ if (!TEXT_INPUT_TYPES.has(type)) {
471
+ return;
472
+ }
473
+ } else if (tag !== "textarea") {
474
+ return;
475
+ }
476
+ this.pendingInputs.add(target);
477
+ const prev = this.inputTimers.get(target);
478
+ if (prev) {
479
+ clearTimeout(prev);
480
+ }
481
+ this.inputTimers.set(target, setTimeout(() => {
482
+ this.inputTimers.delete(target);
483
+ this.pendingInputs.delete(target);
484
+ this.recordInput(target);
485
+ }, this.config.inputDebounce));
486
+ }
487
+ recordInput(target) {
488
+ const rawValue = target.value || "";
489
+ if (!rawValue) {
490
+ return;
491
+ }
492
+ const info = this.getElementInfo(target);
493
+ const type = target.type?.toLowerCase() || "";
494
+ const value = info.masked || type === "password" ? maskText(rawValue) : rawValue;
495
+ const action = {
496
+ event: "action",
497
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
498
+ type: "input",
499
+ targetId: info.targetId,
500
+ tag: info.tag,
501
+ text: info.text,
502
+ url: location.href,
503
+ value,
504
+ length: rawValue.length,
505
+ viewport: getViewport()
506
+ };
507
+ if (this.config.debug) {
508
+ console.log(`[dataclient] input: ${info.tag} "${info.text}" \u2192 ${value.length} chars`);
509
+ }
510
+ this.sender.add(action);
511
+ RrwebTracker.addMarker("input", { tag: info.tag, text: info.text, label: `Input: ${info.text || info.tag}` });
512
+ }
513
+ onChange(e) {
514
+ const target = e.target;
515
+ if (!target) {
516
+ return;
517
+ }
518
+ const info = this.getElementInfo(target);
519
+ const tag = target.tagName?.toLowerCase();
520
+ const action = {
521
+ event: "action",
522
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
523
+ type: "change",
524
+ targetId: info.targetId,
525
+ tag: info.tag,
526
+ text: info.text,
527
+ url: location.href,
528
+ viewport: getViewport()
529
+ };
530
+ if (tag === "select") {
531
+ const selectValue = target.value;
532
+ action.value = info.masked ? maskText(selectValue) : selectValue;
533
+ } else if (tag === "input") {
534
+ const type = target.type;
535
+ if (type === "checkbox" || type === "radio") {
536
+ action.checked = target.checked;
537
+ }
538
+ }
539
+ if (this.config.debug) {
540
+ console.log(`[dataclient] change: ${info.tag} "${info.text}"`);
541
+ }
542
+ this.sender.add(action);
543
+ RrwebTracker.addMarker("change", { tag: info.tag, text: info.text, label: `Change: ${info.text || info.tag}` });
544
+ }
545
+ getElementInfo(el) {
546
+ const tag = el.tagName?.toLowerCase() || "";
547
+ let text = "";
548
+ if (tag === "input" || tag === "textarea") {
549
+ text = el.placeholder || el.getAttribute("aria-label") || "";
550
+ } else {
551
+ for (const child of el.childNodes) {
552
+ if (child.nodeType === Node.TEXT_NODE) {
553
+ const t = child.textContent?.trim();
554
+ if (t) {
555
+ text += (text ? " " : "") + t;
556
+ }
557
+ }
558
+ }
559
+ if (!text) {
560
+ text = el.textContent?.trim().slice(0, 100) || "";
561
+ }
562
+ }
563
+ const masked = isMasked(el);
564
+ const finalText = masked ? maskText(text) : text;
565
+ return {
566
+ tag,
567
+ text: finalText.slice(0, 100),
568
+ targetId: getNodeId(el),
569
+ state: el.disabled ? "disabled" : "enabled",
570
+ masked
571
+ };
572
+ }
573
+ findMeaningfulElement(el) {
574
+ let current = el;
575
+ while (current && current !== document.body) {
576
+ if (INTERACTIVE_TAGS.has(current.tagName?.toLowerCase())) {
577
+ return current;
578
+ }
579
+ const role = current.getAttribute("role");
580
+ if (role && INTERACTIVE_ROLES.has(role)) {
581
+ return current;
582
+ }
583
+ current = current.parentElement;
584
+ }
585
+ return el;
586
+ }
587
+ };
588
+
589
+ // src/trackers/mutation.ts
590
+ var MutationTracker = class {
591
+ constructor(config, sender, onMutation) {
592
+ this.config = config;
593
+ this.sender = sender;
594
+ this.onMutation = onMutation;
595
+ }
596
+ config;
597
+ sender;
598
+ onMutation;
599
+ observer = null;
600
+ debounceTimer = null;
601
+ pendingAdds = [];
602
+ pendingRemoves = [];
603
+ pendingTextChanges = [];
604
+ pendingAttrChanges = [];
605
+ start() {
606
+ this.observer = new MutationObserver((mutations) => this.handleMutations(mutations));
607
+ this.observer.observe(document.body, {
608
+ childList: true,
609
+ subtree: true,
610
+ characterData: true,
611
+ attributes: true,
612
+ attributeFilter: WATCH_ATTRS
613
+ });
614
+ }
615
+ stop() {
616
+ this.observer?.disconnect();
617
+ this.observer = null;
618
+ if (this.debounceTimer) {
619
+ clearTimeout(this.debounceTimer);
620
+ }
621
+ }
622
+ beforeUnload() {
623
+ if (this.debounceTimer) {
624
+ clearTimeout(this.debounceTimer);
625
+ this.flush();
626
+ }
627
+ }
628
+ flush() {
629
+ this.debounceTimer = null;
630
+ if (this.pendingAdds.length === 0 && this.pendingRemoves.length === 0 && this.pendingTextChanges.length === 0 && this.pendingAttrChanges.length === 0) {
631
+ return;
632
+ }
633
+ const mutation = {
634
+ event: "mutation",
635
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
636
+ adds: this.pendingAdds,
637
+ removes: this.pendingRemoves,
638
+ text_changes: this.pendingTextChanges,
639
+ attr_changes: this.pendingAttrChanges
640
+ };
641
+ if (this.config.debug) {
642
+ console.log(`[dataclient] mutation: +${this.pendingAdds.length} -${this.pendingRemoves.length} text:${this.pendingTextChanges.length} attr:${this.pendingAttrChanges.length}`);
643
+ }
644
+ this.sender.add(mutation);
645
+ this.onMutation();
646
+ this.pendingAdds = [];
647
+ this.pendingRemoves = [];
648
+ this.pendingTextChanges = [];
649
+ this.pendingAttrChanges = [];
650
+ }
651
+ scheduleFlush() {
652
+ if (this.debounceTimer) {
653
+ clearTimeout(this.debounceTimer);
654
+ }
655
+ this.debounceTimer = setTimeout(() => this.flush(), this.config.mutationDebounce);
656
+ }
657
+ handleMutations(mutations) {
658
+ for (const m of mutations) {
659
+ for (const node of m.addedNodes) {
660
+ if (node.nodeType !== Node.ELEMENT_NODE) {
661
+ continue;
662
+ }
663
+ const el = node;
664
+ const serialized = serializeNode(el);
665
+ if (!serialized) {
666
+ continue;
667
+ }
668
+ const parentId = getNodeId(el.parentElement);
669
+ if (parentId === null) {
670
+ continue;
671
+ }
672
+ this.pendingAdds.push({ parentId, node: serialized });
673
+ }
674
+ for (const node of m.removedNodes) {
675
+ if (node.nodeType !== Node.ELEMENT_NODE) {
676
+ continue;
677
+ }
678
+ const id = getNodeId(node);
679
+ if (id !== null) {
680
+ this.pendingRemoves.push(id);
681
+ removeNodeId(id);
682
+ }
683
+ }
684
+ if (m.type === "characterData" && m.target.parentElement) {
685
+ const parentId = getNodeId(m.target.parentElement);
686
+ if (parentId !== null) {
687
+ const text = m.target.textContent?.trim().slice(0, 200) || "";
688
+ this.pendingTextChanges.push({ id: parentId, text });
689
+ }
690
+ }
691
+ if (m.type === "attributes" && m.attributeName) {
692
+ const id = getNodeId(m.target);
693
+ if (id !== null) {
694
+ const value = m.target.getAttribute(m.attributeName);
695
+ this.pendingAttrChanges.push({
696
+ id,
697
+ attr: m.attributeName,
698
+ value: value?.slice(0, 200) ?? null
699
+ });
700
+ }
701
+ }
702
+ }
703
+ if (this.pendingAdds.length > 0 || this.pendingRemoves.length > 0 || this.pendingTextChanges.length > 0 || this.pendingAttrChanges.length > 0) {
704
+ this.scheduleFlush();
705
+ }
706
+ }
707
+ };
708
+
709
+ // src/trackers/snapshot.ts
710
+ var SnapshotTracker = class {
711
+ constructor(config, sender) {
712
+ this.config = config;
713
+ this.sender = sender;
714
+ }
715
+ config;
716
+ sender;
717
+ lastUrl = "";
718
+ urlPollTimer = null;
719
+ checkpointTimer = null;
720
+ hasMutations = false;
721
+ start() {
722
+ this.lastUrl = location.href;
723
+ this.recordSnapshot();
724
+ this.urlPollTimer = setInterval(() => this.pollUrl(), 500);
725
+ this.checkpointTimer = setInterval(() => this.checkpoint(), this.config.checkpointInterval);
726
+ }
727
+ stop() {
728
+ if (this.urlPollTimer) {
729
+ clearInterval(this.urlPollTimer);
730
+ }
731
+ if (this.checkpointTimer) {
732
+ clearInterval(this.checkpointTimer);
733
+ }
734
+ }
735
+ beforeUnload() {
736
+ if (this.hasMutations) {
737
+ this.recordSnapshot();
738
+ }
739
+ }
740
+ markMutation() {
741
+ this.hasMutations = true;
742
+ }
743
+ recordSnapshot() {
744
+ resetIds();
745
+ const snapshot = {
746
+ event: "snapshot",
747
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
748
+ url: location.href,
749
+ title: document.title,
750
+ tree: serializeTree(document.body),
751
+ viewport: getViewport()
752
+ };
753
+ if (this.config.debug) {
754
+ console.log(`[dataclient] snapshot: ${location.href} (${snapshot.tree.length} nodes)`);
755
+ }
756
+ this.sender.add(snapshot);
757
+ this.sender.flush();
758
+ this.hasMutations = false;
759
+ }
760
+ pollUrl() {
761
+ if (location.href !== this.lastUrl) {
762
+ const prev = this.lastUrl;
763
+ this.lastUrl = location.href;
764
+ if (prev) {
765
+ if (this.config.debug) {
766
+ console.log(`[dataclient] URL changed: ${prev} \u2192 ${location.href}`);
767
+ }
768
+ this.recordSnapshot();
769
+ }
770
+ }
771
+ }
772
+ checkpoint() {
773
+ if (this.hasMutations) {
774
+ if (this.config.debug) {
775
+ console.log("[dataclient] checkpoint snapshot");
776
+ }
777
+ this.recordSnapshot();
778
+ }
779
+ }
780
+ };
781
+
782
+ // src/utils/identity.ts
783
+ function getDeviceId(key) {
784
+ let id = localStorage.getItem(key);
785
+ if (!id) {
786
+ id = generateId();
787
+ localStorage.setItem(key, id);
788
+ }
789
+ return id;
790
+ }
791
+ function generateId() {
792
+ const ts = Date.now().toString(36);
793
+ const rand = Math.random().toString(36).substring(2, 12);
794
+ return `${ts}-${rand}`;
795
+ }
796
+
797
+ // src/utils/sender.ts
798
+ var STORAGE_KEY = "sc2_pending";
799
+ var MAX_RETRIES = 2;
800
+ var Sender = class {
801
+ constructor(endpoint, apiKey, batchSize, sessionId, deviceId, flushInterval) {
802
+ this.endpoint = endpoint;
803
+ this.apiKey = apiKey;
804
+ this.batchSize = batchSize;
805
+ this.sessionId = sessionId;
806
+ this.deviceId = deviceId;
807
+ this.restoreFromStorage();
808
+ this.timer = setInterval(() => this.flush(), flushInterval);
809
+ document.addEventListener("visibilitychange", () => {
810
+ if (document.visibilityState === "hidden") {
811
+ this.flush();
812
+ }
813
+ });
814
+ }
815
+ endpoint;
816
+ apiKey;
817
+ batchSize;
818
+ sessionId;
819
+ deviceId;
820
+ queue = [];
821
+ timer = null;
822
+ isFlushing = false;
823
+ restoreFromStorage() {
824
+ try {
825
+ localStorage.removeItem(STORAGE_KEY);
826
+ } catch {
827
+ }
828
+ }
829
+ saveToStorage() {
830
+ if (this.queue.length === 0) {
831
+ return;
832
+ }
833
+ try {
834
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(this.queue));
835
+ } catch {
836
+ }
837
+ }
838
+ add(event) {
839
+ this.queue.push(event);
840
+ if (this.queue.length >= this.batchSize) {
841
+ this.flush();
842
+ }
843
+ }
844
+ async flush() {
845
+ if (this.queue.length === 0 || this.isFlushing) {
846
+ return;
847
+ }
848
+ this.isFlushing = true;
849
+ const events = this.queue.splice(0);
850
+ const batch = this.buildBatch(events);
851
+ const json = JSON.stringify(batch);
852
+ const url = `${this.endpoint}?key=${encodeURIComponent(this.apiKey)}`;
853
+ const success = await this.send(json, url);
854
+ if (!success) {
855
+ this.queue.unshift(...events);
856
+ this.saveToStorage();
857
+ }
858
+ this.isFlushing = false;
859
+ }
860
+ flushSync() {
861
+ if (this.queue.length === 0) {
862
+ return;
863
+ }
864
+ const events = this.queue.splice(0);
865
+ const batch = this.buildBatch(events);
866
+ const json = JSON.stringify(batch);
867
+ const url = `${this.endpoint}?key=${encodeURIComponent(this.apiKey)}`;
868
+ const blob = new Blob([json], { type: "application/json" });
869
+ const sent = navigator.sendBeacon(url, blob);
870
+ if (!sent) {
871
+ this.queue.unshift(...events);
872
+ this.saveToStorage();
873
+ }
874
+ }
875
+ buildBatch(events) {
876
+ return {
877
+ session_id: this.sessionId,
878
+ device_id: this.deviceId,
879
+ events,
880
+ sent_at: (/* @__PURE__ */ new Date()).toISOString(),
881
+ page_url: location.href,
882
+ user_agent: navigator.userAgent,
883
+ screen: {
884
+ width: screen.width,
885
+ height: screen.height,
886
+ viewport_width: window.innerWidth,
887
+ viewport_height: window.innerHeight
888
+ }
889
+ };
890
+ }
891
+ async send(json, url) {
892
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
893
+ try {
894
+ const response = await fetch(url, {
895
+ method: "POST",
896
+ headers: { "Content-Type": "application/json" },
897
+ body: json,
898
+ keepalive: true
899
+ });
900
+ if (response.ok) {
901
+ return true;
902
+ }
903
+ } catch {
904
+ }
905
+ if (attempt < MAX_RETRIES) {
906
+ await new Promise((r) => setTimeout(r, (attempt + 1) * 200));
907
+ }
908
+ }
909
+ return false;
910
+ }
911
+ destroy() {
912
+ if (this.timer) {
913
+ clearInterval(this.timer);
914
+ }
915
+ this.flushSync();
916
+ }
917
+ };
918
+
919
+ // src/index.ts
920
+ var defaults = {
921
+ endpoint: "https://my.tamsense.com/api/scenes2",
922
+ debug: false,
923
+ batchSize: 10,
924
+ flushInterval: 1e4,
925
+ checkpointInterval: 3e4,
926
+ mutationDebounce: 200,
927
+ inputDebounce: 1e3,
928
+ sessionIdKey: "sc2_sid",
929
+ deviceIdKey: "sc2_did",
930
+ apiKey: ""
931
+ };
932
+ var DataClient = class {
933
+ sender;
934
+ trackers = [];
935
+ config;
936
+ constructor(options) {
937
+ this.config = { ...defaults, ...options };
938
+ const sessionId = generateId();
939
+ const deviceId = getDeviceId(this.config.deviceIdKey);
940
+ this.sender = new Sender(
941
+ this.config.endpoint,
942
+ this.config.apiKey,
943
+ this.config.batchSize,
944
+ sessionId,
945
+ deviceId,
946
+ this.config.flushInterval
947
+ );
948
+ const snapshotTracker = new SnapshotTracker(this.config, this.sender);
949
+ const mutationTracker = new MutationTracker(this.config, this.sender, () => snapshotTracker.markMutation());
950
+ const actionTracker = new ActionTracker(this.config, this.sender);
951
+ const rrwebTracker = new RrwebTracker(this.config, this.sender);
952
+ this.trackers = [snapshotTracker, mutationTracker, actionTracker, rrwebTracker];
953
+ this.trackers.forEach((t) => t.start());
954
+ const onUnload = () => {
955
+ this.trackers.forEach((t) => t.beforeUnload?.());
956
+ this.sender.flushSync();
957
+ };
958
+ window.addEventListener("beforeunload", onUnload);
959
+ window.addEventListener("pagehide", onUnload);
960
+ console.log(`[dataclient] Initialized. Session: ${sessionId}, Device: ${deviceId}`);
961
+ }
962
+ setUser(userId) {
963
+ this.sender.add({ event: "identify", timestamp: (/* @__PURE__ */ new Date()).toISOString(), user_id: userId });
964
+ }
965
+ excludeSession(reason = "") {
966
+ this.sender.add({ event: "exclude", timestamp: (/* @__PURE__ */ new Date()).toISOString(), reason });
967
+ this.destroy();
968
+ }
969
+ destroy() {
970
+ this.trackers.forEach((t) => t.stop());
971
+ this.trackers = [];
972
+ this.sender.destroy();
973
+ }
974
+ };
975
+ // Annotate the CommonJS export names for ESM import in node:
976
+ 0 && (module.exports = {
977
+ DataClient
978
+ });
979
+ //# sourceMappingURL=index.cjs.map