@visitor-analytics-sdk/core 1.0.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.js ADDED
@@ -0,0 +1,1682 @@
1
+ // src/event-bus.ts
2
+ var EventBus = class {
3
+ listeners = /* @__PURE__ */ new Map();
4
+ on(event, handler) {
5
+ let set = this.listeners.get(event);
6
+ if (!set) {
7
+ set = /* @__PURE__ */ new Set();
8
+ this.listeners.set(event, set);
9
+ }
10
+ set.add(handler);
11
+ }
12
+ off(event, handler) {
13
+ this.listeners.get(event)?.delete(handler);
14
+ }
15
+ emit(event, ...args) {
16
+ const set = this.listeners.get(event);
17
+ if (!set) return;
18
+ for (const handler of set) {
19
+ try {
20
+ handler(...args);
21
+ } catch (_err) {
22
+ }
23
+ }
24
+ }
25
+ removeAllListeners(event) {
26
+ if (event) {
27
+ this.listeners.delete(event);
28
+ } else {
29
+ this.listeners.clear();
30
+ }
31
+ }
32
+ listenerCount(event) {
33
+ return this.listeners.get(event)?.size ?? 0;
34
+ }
35
+ };
36
+
37
+ // ../plugins/src/plugin-manager.ts
38
+ var PluginManager = class {
39
+ installedPlugins = /* @__PURE__ */ new Map();
40
+ collectors = /* @__PURE__ */ new Map();
41
+ eventBus;
42
+ constructor(eventBus) {
43
+ this.eventBus = eventBus;
44
+ }
45
+ install(plugin, context) {
46
+ if (this.installedPlugins.has(plugin.name)) {
47
+ return;
48
+ }
49
+ plugin.install(context);
50
+ this.installedPlugins.set(plugin.name, plugin);
51
+ this.eventBus.emit("plugin-installed", plugin.name);
52
+ }
53
+ uninstall(pluginName, context) {
54
+ const plugin = this.installedPlugins.get(pluginName);
55
+ if (!plugin) return;
56
+ plugin.uninstall?.(context);
57
+ this.installedPlugins.delete(pluginName);
58
+ this.eventBus.emit("plugin-uninstalled", pluginName);
59
+ }
60
+ has(pluginName) {
61
+ return this.installedPlugins.has(pluginName);
62
+ }
63
+ get(pluginName) {
64
+ return this.installedPlugins.get(pluginName);
65
+ }
66
+ getAll() {
67
+ return [...this.installedPlugins.values()];
68
+ }
69
+ addCollector(collector) {
70
+ if (this.collectors.has(collector.name)) {
71
+ return;
72
+ }
73
+ this.collectors.set(collector.name, collector);
74
+ this.eventBus.emit("collector-registered", collector.name);
75
+ }
76
+ removeCollector(name) {
77
+ if (!this.collectors.has(name)) return;
78
+ this.collectors.delete(name);
79
+ this.eventBus.emit("collector-removed", name);
80
+ }
81
+ getCollectors() {
82
+ return [...this.collectors.values()];
83
+ }
84
+ getCollector(name) {
85
+ return this.collectors.get(name);
86
+ }
87
+ };
88
+ function createPluginContext(addCollector, removeCollector, eventBus, getConfig) {
89
+ return {
90
+ addCollector,
91
+ removeCollector,
92
+ on: (event, handler) => {
93
+ eventBus.on(event, handler);
94
+ },
95
+ off: (event, handler) => {
96
+ eventBus.off(event, handler);
97
+ },
98
+ getConfig
99
+ };
100
+ }
101
+
102
+ // ../utils/src/index.ts
103
+ var SDK_VERSION = "1.0.0";
104
+ function generateId() {
105
+ if (typeof crypto !== "undefined" && crypto.randomUUID) {
106
+ return crypto.randomUUID();
107
+ }
108
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
109
+ const r = Math.random() * 16 | 0;
110
+ const v = c === "x" ? r : r & 3 | 8;
111
+ return v.toString(16);
112
+ });
113
+ }
114
+ function generateShortId() {
115
+ if (typeof crypto !== "undefined" && crypto.getRandomValues) {
116
+ const bytes = new Uint8Array(8);
117
+ crypto.getRandomValues(bytes);
118
+ return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
119
+ }
120
+ return generateId().replace(/-/g, "").slice(0, 16);
121
+ }
122
+ function safeCall(fn, fallback) {
123
+ try {
124
+ return fn();
125
+ } catch {
126
+ return fallback;
127
+ }
128
+ }
129
+ function requestIdle(callback, timeout = 5e3) {
130
+ if (typeof window !== "undefined" && "requestIdleCallback" in window) {
131
+ window.requestIdleCallback(callback, { timeout });
132
+ } else {
133
+ setTimeout(callback, 0);
134
+ }
135
+ }
136
+ function deepFreeze(obj) {
137
+ Object.freeze(obj);
138
+ for (const key of Object.keys(obj)) {
139
+ const val = obj[key];
140
+ if (val !== null && typeof val === "object" && !Object.isFrozen(val)) {
141
+ deepFreeze(val);
142
+ }
143
+ }
144
+ return obj;
145
+ }
146
+ function cloneRecord(record) {
147
+ return JSON.parse(JSON.stringify(record));
148
+ }
149
+ function isBrowser() {
150
+ return typeof window !== "undefined" && typeof document !== "undefined" && typeof navigator !== "undefined";
151
+ }
152
+
153
+ // ../uploader/src/uploader.ts
154
+ var DEFAULT_CONFIG = {
155
+ endpoint: "",
156
+ batchSize: 50,
157
+ flushInterval: 3e4,
158
+ maxRetries: 5,
159
+ retryBaseDelay: 1e3,
160
+ retryMaxDelay: 3e4,
161
+ compressionEnabled: false,
162
+ deduplicationEnabled: true,
163
+ headers: {},
164
+ timeout: 1e4
165
+ };
166
+ var Uploader = class {
167
+ config;
168
+ storage;
169
+ flushTimer = null;
170
+ retryQueue = /* @__PURE__ */ new Map();
171
+ retryTimer = null;
172
+ eventHandlers = [];
173
+ seenBatchIds = /* @__PURE__ */ new Set();
174
+ isUploading = false;
175
+ isStopped = false;
176
+ constructor(storage, config) {
177
+ this.config = { ...DEFAULT_CONFIG, ...config };
178
+ this.storage = storage;
179
+ }
180
+ start() {
181
+ this.isStopped = false;
182
+ if (this.config.flushInterval > 0) {
183
+ this.flushTimer = setInterval(() => {
184
+ requestIdle(() => this.flush());
185
+ }, this.config.flushInterval);
186
+ }
187
+ }
188
+ stop() {
189
+ this.isStopped = true;
190
+ if (this.flushTimer !== null) {
191
+ clearInterval(this.flushTimer);
192
+ this.flushTimer = null;
193
+ }
194
+ if (this.retryTimer !== null) {
195
+ clearTimeout(this.retryTimer);
196
+ this.retryTimer = null;
197
+ }
198
+ this.retryQueue.clear();
199
+ }
200
+ onEvent(handler) {
201
+ this.eventHandlers.push(handler);
202
+ return () => {
203
+ const idx = this.eventHandlers.indexOf(handler);
204
+ if (idx !== -1) this.eventHandlers.splice(idx, 1);
205
+ };
206
+ }
207
+ emitEvent(event) {
208
+ for (const handler of this.eventHandlers) {
209
+ try {
210
+ handler(event);
211
+ } catch {
212
+ }
213
+ }
214
+ }
215
+ async flush() {
216
+ if (this.isUploading || this.isStopped) return;
217
+ const count = await this.storage.count();
218
+ if (count === 0) return;
219
+ this.isUploading = true;
220
+ try {
221
+ const records = await this.storage.loadBatch(this.config.batchSize);
222
+ if (records.length === 0) return;
223
+ const batchId = generateShortId();
224
+ const payload = {
225
+ records,
226
+ batchId,
227
+ timestamp: Date.now(),
228
+ sdkVersion: SDK_VERSION
229
+ };
230
+ if (this.config.deduplicationEnabled && this.seenBatchIds.has(batchId)) {
231
+ return;
232
+ }
233
+ this.seenBatchIds.add(batchId);
234
+ if (this.seenBatchIds.size > 1e3) {
235
+ const ids = [...this.seenBatchIds];
236
+ const toRemove = ids.slice(0, ids.length - 1e3);
237
+ for (const id of toRemove) {
238
+ this.seenBatchIds.delete(id);
239
+ }
240
+ }
241
+ this.emitEvent({
242
+ type: "batch-sent",
243
+ batchId,
244
+ recordCount: records.length,
245
+ timestamp: Date.now()
246
+ });
247
+ const result = await this.sendBatch(payload);
248
+ if (result.success) {
249
+ this.emitEvent({
250
+ type: "batch-success",
251
+ batchId,
252
+ recordCount: records.length,
253
+ timestamp: Date.now()
254
+ });
255
+ } else if (result.retryable) {
256
+ this.scheduleRetry(payload);
257
+ } else {
258
+ this.emitEvent({
259
+ type: "batch-failed",
260
+ batchId,
261
+ recordCount: records.length,
262
+ timestamp: Date.now(),
263
+ error: result.error
264
+ });
265
+ }
266
+ } finally {
267
+ this.isUploading = false;
268
+ }
269
+ }
270
+ async sendBatch(payload) {
271
+ if (!this.config.endpoint) {
272
+ return { success: false, batchId: payload.batchId, error: "No endpoint configured", retryable: false };
273
+ }
274
+ const body = this.config.compressionEnabled ? await this.compress(JSON.stringify(payload)) : JSON.stringify(payload);
275
+ const headers = {
276
+ "Content-Type": "application/json",
277
+ "X-Batch-Id": payload.batchId,
278
+ "X-Record-Count": String(payload.records.length),
279
+ ...this.config.headers
280
+ };
281
+ if (this.config.compressionEnabled) {
282
+ headers["Content-Encoding"] = "gzip";
283
+ }
284
+ const controller = new AbortController();
285
+ const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
286
+ try {
287
+ const response = await fetch(this.config.endpoint, {
288
+ method: "POST",
289
+ headers,
290
+ body,
291
+ signal: controller.signal,
292
+ keepalive: true
293
+ });
294
+ clearTimeout(timeoutId);
295
+ if (response.ok) {
296
+ return { success: true, batchId: payload.batchId, statusCode: response.status, retryable: false };
297
+ }
298
+ const retryable = response.status >= 500 || response.status === 429;
299
+ return {
300
+ success: false,
301
+ batchId: payload.batchId,
302
+ statusCode: response.status,
303
+ error: `HTTP ${response.status}: ${response.statusText}`,
304
+ retryable
305
+ };
306
+ } catch (err) {
307
+ clearTimeout(timeoutId);
308
+ const error = err instanceof Error ? err.message : "Unknown error";
309
+ return { success: false, batchId: payload.batchId, error, retryable: true };
310
+ }
311
+ }
312
+ scheduleRetry(payload) {
313
+ const existing = this.retryQueue.get(payload.batchId);
314
+ const retryCount = (existing?.retryCount ?? 0) + 1;
315
+ if (retryCount > this.config.maxRetries) {
316
+ this.retryQueue.delete(payload.batchId);
317
+ this.emitEvent({
318
+ type: "batch-failed",
319
+ batchId: payload.batchId,
320
+ recordCount: payload.records.length,
321
+ timestamp: Date.now(),
322
+ error: "Max retries exceeded",
323
+ retryCount
324
+ });
325
+ return;
326
+ }
327
+ const delay = Math.min(
328
+ this.config.retryBaseDelay * Math.pow(2, retryCount - 1),
329
+ this.config.retryMaxDelay
330
+ );
331
+ const jitter = delay * (0.5 + Math.random() * 0.5);
332
+ const nextRetryAt = Date.now() + jitter;
333
+ this.retryQueue.set(payload.batchId, { payload, retryCount, nextRetryAt });
334
+ this.emitEvent({
335
+ type: "retry-scheduled",
336
+ batchId: payload.batchId,
337
+ recordCount: payload.records.length,
338
+ timestamp: Date.now(),
339
+ retryCount,
340
+ nextRetryAt
341
+ });
342
+ this.processRetryQueue();
343
+ }
344
+ processRetryQueue() {
345
+ if (this.retryTimer !== null) return;
346
+ const now = Date.now();
347
+ let earliest = Infinity;
348
+ for (const [batchId, entry] of this.retryQueue) {
349
+ if (entry.nextRetryAt <= now) {
350
+ this.retryQueue.delete(batchId);
351
+ this.sendBatch(entry.payload).then((result) => {
352
+ if (!result.success && result.retryable) {
353
+ this.scheduleRetry(entry.payload);
354
+ } else if (result.success) {
355
+ this.emitEvent({
356
+ type: "batch-success",
357
+ batchId: entry.payload.batchId,
358
+ recordCount: entry.payload.records.length,
359
+ timestamp: Date.now()
360
+ });
361
+ }
362
+ });
363
+ } else if (entry.nextRetryAt < earliest) {
364
+ earliest = entry.nextRetryAt;
365
+ }
366
+ }
367
+ if (earliest < Infinity) {
368
+ this.retryTimer = setTimeout(() => {
369
+ this.retryTimer = null;
370
+ this.processRetryQueue();
371
+ }, earliest - Date.now());
372
+ }
373
+ }
374
+ async retryFailed() {
375
+ const entries = [...this.retryQueue.values()];
376
+ this.retryQueue.clear();
377
+ for (const entry of entries) {
378
+ this.scheduleRetry(entry.payload);
379
+ }
380
+ }
381
+ async sync() {
382
+ await this.flush();
383
+ }
384
+ async compress(data) {
385
+ if (typeof CompressionStream === "undefined") {
386
+ return data;
387
+ }
388
+ try {
389
+ const stream = new Blob([data]).stream().pipeThrough(new CompressionStream("gzip"));
390
+ return new Response(stream).blob();
391
+ } catch {
392
+ return data;
393
+ }
394
+ }
395
+ destroy() {
396
+ this.stop();
397
+ this.retryQueue.clear();
398
+ this.eventHandlers = [];
399
+ this.seenBatchIds.clear();
400
+ }
401
+ };
402
+
403
+ // ../storage/src/memory-storage.ts
404
+ var MemoryStorage = class {
405
+ records = [];
406
+ async save(record) {
407
+ this.records.push(cloneRecord(record));
408
+ }
409
+ async saveBatch(records) {
410
+ for (const record of records) {
411
+ this.records.push(cloneRecord(record));
412
+ }
413
+ }
414
+ async load() {
415
+ return this.records.map((r) => cloneRecord(r));
416
+ }
417
+ async loadBatch(limit) {
418
+ return this.records.splice(0, limit);
419
+ }
420
+ async remove(ids) {
421
+ const idSet = new Set(ids);
422
+ this.records = this.records.filter((r) => !idSet.has(r.id));
423
+ }
424
+ async count() {
425
+ return this.records.length;
426
+ }
427
+ async clear() {
428
+ this.records = [];
429
+ }
430
+ async export() {
431
+ return JSON.stringify(this.records);
432
+ }
433
+ };
434
+
435
+ // ../storage/src/localstorage-adapter.ts
436
+ var STORAGE_KEY = "va_records";
437
+ var LocalStorageAdapter = class {
438
+ key;
439
+ constructor(key = STORAGE_KEY) {
440
+ this.key = key;
441
+ }
442
+ async save(record) {
443
+ const records = this.readAll();
444
+ records.push(cloneRecord(record));
445
+ this.writeAll(records);
446
+ }
447
+ async saveBatch(records) {
448
+ const existing = this.readAll();
449
+ for (const record of records) {
450
+ existing.push(cloneRecord(record));
451
+ }
452
+ this.writeAll(existing);
453
+ }
454
+ async load() {
455
+ return this.readAll();
456
+ }
457
+ async loadBatch(limit) {
458
+ const records = this.readAll();
459
+ const batch = records.slice(0, limit);
460
+ this.writeAll(records.slice(limit));
461
+ return batch;
462
+ }
463
+ async remove(ids) {
464
+ const idSet = new Set(ids);
465
+ const records = this.readAll().filter((r) => !idSet.has(r.id));
466
+ this.writeAll(records);
467
+ }
468
+ async count() {
469
+ return this.readAll().length;
470
+ }
471
+ async clear() {
472
+ try {
473
+ localStorage.removeItem(this.key);
474
+ } catch {
475
+ }
476
+ }
477
+ async export() {
478
+ return JSON.stringify(this.readAll());
479
+ }
480
+ readAll() {
481
+ try {
482
+ const raw = localStorage.getItem(this.key);
483
+ if (!raw) return [];
484
+ const parsed = JSON.parse(raw);
485
+ if (!Array.isArray(parsed)) return [];
486
+ return parsed;
487
+ } catch {
488
+ return [];
489
+ }
490
+ }
491
+ writeAll(records) {
492
+ try {
493
+ localStorage.setItem(this.key, JSON.stringify(records));
494
+ } catch {
495
+ }
496
+ }
497
+ };
498
+
499
+ // ../storage/src/indexeddb-adapter.ts
500
+ var DB_NAME = "visitor-analytics";
501
+ var DB_VERSION = 1;
502
+ var STORE_NAME = "records";
503
+ var IndexedDBAdapter = class {
504
+ db = null;
505
+ dbName;
506
+ storeName;
507
+ initPromise = null;
508
+ constructor(dbName = DB_NAME, storeName = STORE_NAME) {
509
+ this.dbName = dbName;
510
+ this.storeName = storeName;
511
+ }
512
+ async getDB() {
513
+ if (this.db) return this.db;
514
+ if (this.initPromise) return this.initPromise;
515
+ this.initPromise = new Promise((resolve, reject) => {
516
+ if (typeof indexedDB === "undefined") {
517
+ reject(new Error("IndexedDB is not available"));
518
+ return;
519
+ }
520
+ const request = indexedDB.open(this.dbName, DB_VERSION);
521
+ request.onerror = () => {
522
+ this.initPromise = null;
523
+ reject(request.error);
524
+ };
525
+ request.onsuccess = () => {
526
+ this.db = request.result;
527
+ resolve(request.result);
528
+ };
529
+ request.onupgradeneeded = (event) => {
530
+ const db = event.target.result;
531
+ if (!db.objectStoreNames.contains(this.storeName)) {
532
+ db.createObjectStore(this.storeName, { keyPath: "id" });
533
+ }
534
+ };
535
+ }).catch((err) => {
536
+ this.initPromise = null;
537
+ throw err;
538
+ });
539
+ return this.initPromise;
540
+ }
541
+ async save(record) {
542
+ const db = await this.getDB();
543
+ return new Promise((resolve, reject) => {
544
+ const tx = db.transaction(this.storeName, "readwrite");
545
+ const store = tx.objectStore(this.storeName);
546
+ const request = store.put(cloneRecord(record));
547
+ request.onsuccess = () => resolve();
548
+ request.onerror = () => reject(request.error);
549
+ });
550
+ }
551
+ async saveBatch(records) {
552
+ const db = await this.getDB();
553
+ return new Promise((resolve, reject) => {
554
+ const tx = db.transaction(this.storeName, "readwrite");
555
+ const store = tx.objectStore(this.storeName);
556
+ for (const record of records) {
557
+ store.put(cloneRecord(record));
558
+ }
559
+ tx.oncomplete = () => resolve();
560
+ tx.onerror = () => reject(tx.error);
561
+ });
562
+ }
563
+ async load() {
564
+ const db = await this.getDB();
565
+ return new Promise((resolve, reject) => {
566
+ const tx = db.transaction(this.storeName, "readonly");
567
+ const store = tx.objectStore(this.storeName);
568
+ const request = store.getAll();
569
+ request.onsuccess = () => resolve(request.result);
570
+ request.onerror = () => reject(request.error);
571
+ });
572
+ }
573
+ async loadBatch(limit) {
574
+ const db = await this.getDB();
575
+ return new Promise((resolve, reject) => {
576
+ const tx = db.transaction(this.storeName, "readwrite");
577
+ const store = tx.objectStore(this.storeName);
578
+ const results = [];
579
+ let count = 0;
580
+ const cursorRequest = store.openCursor();
581
+ cursorRequest.onsuccess = (event) => {
582
+ const cursor = event.target.result;
583
+ if (cursor && count < limit) {
584
+ results.push(cursor.value);
585
+ count++;
586
+ cursor.delete();
587
+ cursor.continue();
588
+ } else {
589
+ resolve(results);
590
+ }
591
+ };
592
+ cursorRequest.onerror = () => reject(cursorRequest.error);
593
+ });
594
+ }
595
+ async remove(ids) {
596
+ const db = await this.getDB();
597
+ return new Promise((resolve, reject) => {
598
+ const tx = db.transaction(this.storeName, "readwrite");
599
+ const store = tx.objectStore(this.storeName);
600
+ for (const id of ids) {
601
+ store.delete(id);
602
+ }
603
+ tx.oncomplete = () => resolve();
604
+ tx.onerror = () => reject(tx.error);
605
+ });
606
+ }
607
+ async count() {
608
+ const db = await this.getDB();
609
+ return new Promise((resolve, reject) => {
610
+ const tx = db.transaction(this.storeName, "readonly");
611
+ const store = tx.objectStore(this.storeName);
612
+ const request = store.count();
613
+ request.onsuccess = () => resolve(request.result);
614
+ request.onerror = () => reject(request.error);
615
+ });
616
+ }
617
+ async clear() {
618
+ const db = await this.getDB();
619
+ return new Promise((resolve, reject) => {
620
+ const tx = db.transaction(this.storeName, "readwrite");
621
+ const store = tx.objectStore(this.storeName);
622
+ const request = store.clear();
623
+ request.onsuccess = () => resolve();
624
+ request.onerror = () => reject(request.error);
625
+ });
626
+ }
627
+ async export() {
628
+ const records = await this.load();
629
+ return JSON.stringify(records);
630
+ }
631
+ async close() {
632
+ if (this.db) {
633
+ this.db.close();
634
+ this.db = null;
635
+ this.initPromise = null;
636
+ }
637
+ }
638
+ };
639
+
640
+ // ../storage/src/factory.ts
641
+ var StorageAdapterFactory = class {
642
+ static create(type) {
643
+ if (typeof type === "object" && typeof type.save === "function") {
644
+ return type;
645
+ }
646
+ switch (type) {
647
+ case "memory":
648
+ return new MemoryStorage();
649
+ case "localstorage":
650
+ return new LocalStorageAdapter();
651
+ case "indexeddb":
652
+ return new IndexedDBAdapter();
653
+ default:
654
+ throw new Error(`Unknown storage type: ${String(type)}`);
655
+ }
656
+ }
657
+ };
658
+
659
+ // ../collectors/src/browser/browser-collector.ts
660
+ var BrowserCollector = class {
661
+ name = "browser";
662
+ category = "browser";
663
+ version = SDK_VERSION;
664
+ enabled = true;
665
+ includeUserAgent;
666
+ constructor(options) {
667
+ this.includeUserAgent = options?.includeUserAgent ?? false;
668
+ }
669
+ async collect(context) {
670
+ const nav = context.navigator;
671
+ const ua = nav.userAgent;
672
+ const parsed = this.parseUA(ua);
673
+ return {
674
+ browser: {
675
+ name: parsed.name,
676
+ version: parsed.version,
677
+ engine: parsed.engine,
678
+ engineVersion: parsed.engineVersion,
679
+ userAgent: this.includeUserAgent ? ua : "",
680
+ language: nav.language,
681
+ cookiesEnabled: nav.cookieEnabled,
682
+ javaScriptEnabled: true,
683
+ doNotTrack: nav.doNotTrack
684
+ }
685
+ };
686
+ }
687
+ parseUA(ua) {
688
+ let name = "unknown";
689
+ let version = "0";
690
+ let engine = "unknown";
691
+ let engineVersion = "0";
692
+ if (ua.includes("Firefox/") && !ua.includes("Seamonkey")) {
693
+ name = "Firefox";
694
+ const match = ua.match(/Firefox\/([\d.]+)/);
695
+ version = match?.[1] ?? "0";
696
+ engine = "Gecko";
697
+ } else if (ua.includes("Edg/")) {
698
+ name = "Edge";
699
+ const match = ua.match(/Edg\/([\d.]+)/);
700
+ version = match?.[1] ?? "0";
701
+ engine = "Blink";
702
+ } else if (ua.includes("OPR/") || ua.includes("Opera")) {
703
+ name = "Opera";
704
+ const match = ua.match(/(?:OPR|Opera)\/([\d.]+)/);
705
+ version = match?.[1] ?? "0";
706
+ engine = "Blink";
707
+ } else if (ua.includes("Chrome/") && !ua.includes("Edg/")) {
708
+ name = "Chrome";
709
+ const match = ua.match(/Chrome\/([\d.]+)/);
710
+ version = match?.[1] ?? "0";
711
+ engine = "Blink";
712
+ } else if (ua.includes("Safari/") && !ua.includes("Chrome/") && !ua.includes("Chromium")) {
713
+ name = "Safari";
714
+ const match = ua.match(/Version\/([\d.]+)/);
715
+ version = match?.[1] ?? "0";
716
+ engine = "WebKit";
717
+ } else if (ua.includes("MSIE") || ua.includes("Trident/")) {
718
+ name = "Internet Explorer";
719
+ const match = ua.match(/(?:MSIE |Trident\/.*rv:)([\d.]+)/);
720
+ version = match?.[1] ?? "0";
721
+ engine = "Trident";
722
+ }
723
+ if (engine === "Blink") {
724
+ const match = ua.match(/AppleWebKit\/([\d.]+)/);
725
+ engineVersion = match?.[1] ?? "0";
726
+ } else if (engine === "Gecko") {
727
+ const match = ua.match(/Gecko\/([\d.]+)/);
728
+ engineVersion = match?.[1] ?? "20100101";
729
+ } else if (engine === "WebKit") {
730
+ const match = ua.match(/AppleWebKit\/([\d.]+)/);
731
+ engineVersion = match?.[1] ?? "0";
732
+ } else if (engine === "Trident") {
733
+ const match = ua.match(/Trident\/([\d.]+)/);
734
+ engineVersion = match?.[1] ?? "0";
735
+ }
736
+ return { name, version, engine, engineVersion };
737
+ }
738
+ async destroy() {
739
+ }
740
+ };
741
+
742
+ // ../collectors/src/device/device-collector.ts
743
+ var DeviceCollector = class {
744
+ name = "device";
745
+ category = "device";
746
+ version = SDK_VERSION;
747
+ enabled = true;
748
+ async collect(context) {
749
+ const nav = context.navigator;
750
+ const win = context.window;
751
+ const scr = context.screen;
752
+ return {
753
+ device: {
754
+ os: this.detectOS(nav.platform, nav.userAgent),
755
+ osVersion: this.detectOSVersion(nav.userAgent),
756
+ platform: nav.platform,
757
+ architecture: this.detectArch(nav.userAgent),
758
+ formFactor: this.detectFormFactor(nav.userAgent, scr),
759
+ screenWidth: scr.width,
760
+ screenHeight: scr.height,
761
+ viewportWidth: win.innerWidth,
762
+ viewportHeight: win.innerHeight,
763
+ devicePixelRatio: win.devicePixelRatio,
764
+ colorDepth: scr.colorDepth,
765
+ orientation: scr.orientation?.type ?? "portrait-primary",
766
+ touchSupport: this.detectTouch(nav.maxTouchPoints),
767
+ hardwareConcurrency: nav.hardwareConcurrency,
768
+ maxTouchPoints: nav.maxTouchPoints
769
+ }
770
+ };
771
+ }
772
+ detectOS(platform, ua) {
773
+ if (platform.includes("Win")) return "Windows";
774
+ if (platform.includes("Mac")) return "macOS";
775
+ if (platform.includes("Linux")) return "Linux";
776
+ if (platform.includes("Android")) return "Android";
777
+ if (/iPhone|iPad|iPod/.test(ua)) return "iOS";
778
+ if (ua.includes("Chrome OS")) return "Chrome OS";
779
+ if (ua.includes("X11") || ua.includes("Linux")) return "Linux";
780
+ return "Unknown";
781
+ }
782
+ detectOSVersion(ua) {
783
+ const windowsMatch = ua.match(/Windows NT ([\d.]+)/);
784
+ if (windowsMatch) {
785
+ const versions = {
786
+ "10.0": "10/11",
787
+ "6.3": "8.1",
788
+ "6.2": "8",
789
+ "6.1": "7",
790
+ "6.0": "Vista"
791
+ };
792
+ const ver = windowsMatch[1] ?? "";
793
+ return versions[ver] ?? ver;
794
+ }
795
+ const macMatch = ua.match(/Mac OS X ([\d_]+)/);
796
+ if (macMatch) return (macMatch[1] ?? "").replace(/_/g, ".");
797
+ const androidMatch = ua.match(/Android ([\d.]+)/);
798
+ if (androidMatch) return androidMatch[1] ?? "";
799
+ const iosMatch = ua.match(/OS ([\d_]+)/);
800
+ if (iosMatch) return (iosMatch[1] ?? "").replace(/_/g, ".");
801
+ return "unknown";
802
+ }
803
+ detectArch(ua) {
804
+ if (ua.includes("x86_64") || ua.includes("x64") || ua.includes("Win64") || ua.includes("x86-64")) {
805
+ return "x86_64";
806
+ }
807
+ if (ua.includes("arm64") || ua.includes("aarch64")) return "arm64";
808
+ if (ua.includes("arm")) return "arm";
809
+ if (ua.includes("x86")) return "x86";
810
+ if (ua.includes("WOW64")) return "x86_64";
811
+ return "unknown";
812
+ }
813
+ detectFormFactor(ua, scr) {
814
+ if (/iPhone|iPod/.test(ua)) return "mobile";
815
+ if (/iPad/.test(ua)) return "tablet";
816
+ if (/Android/.test(ua)) {
817
+ if (scr.width < 768) return "mobile";
818
+ if (scr.width < 1024) return "tablet";
819
+ return "mobile";
820
+ }
821
+ if (/SmartTV|AppleTV|Chromecast|FireTV|Roku/.test(ua)) return "smarttv";
822
+ if (/Watch|Wearable|Fitbit/.test(ua)) return "wearable";
823
+ if (/PlayStation|Xbox|Nintendo/.test(ua)) return "console";
824
+ if (scr.width >= 1024 && !("ontouchstart" in globalThis)) return "desktop";
825
+ return "unknown";
826
+ }
827
+ detectTouch(maxTouchPoints) {
828
+ if (maxTouchPoints === 0) return "none";
829
+ if (maxTouchPoints <= 5) return "coarse";
830
+ return "fine";
831
+ }
832
+ async destroy() {
833
+ }
834
+ };
835
+
836
+ // ../collectors/src/performance/performance-collector.ts
837
+ var PerformanceCollector = class {
838
+ name = "performance";
839
+ category = "performance";
840
+ version = SDK_VERSION;
841
+ enabled = true;
842
+ async collect(context) {
843
+ const perf = context.performance;
844
+ const nav = context.navigator;
845
+ const connection = nav.connection;
846
+ return {
847
+ performance: {
848
+ navigationTiming: this.collectNavigationTiming(perf),
849
+ paintTiming: this.collectPaintTiming(perf),
850
+ largestContentfulPaint: this.collectLCP(perf),
851
+ firstContentfulPaint: this.collectFCP(perf),
852
+ cumulativeLayoutShift: this.collectCLS(perf),
853
+ interactionToNextPaint: this.collectINP(perf),
854
+ deviceMemory: nav.deviceMemory ?? null,
855
+ networkType: connection?.type ?? "unknown",
856
+ effectiveType: connection?.effectiveType ?? "unknown",
857
+ downlink: connection?.downlink ?? null,
858
+ rtt: connection?.rtt ?? null,
859
+ saveData: connection?.saveData ?? false
860
+ }
861
+ };
862
+ }
863
+ collectNavigationTiming(perf) {
864
+ const timing = perf.timing;
865
+ if (!timing) return null;
866
+ return safeCall(() => ({
867
+ redirectTime: timing.redirectEnd - timing.redirectStart,
868
+ dnsLookupTime: timing.domainLookupEnd - timing.domainLookupStart,
869
+ tcpConnectTime: timing.connectEnd - timing.connectStart,
870
+ requestTime: timing.responseStart - timing.requestStart,
871
+ responseTime: timing.responseEnd - timing.responseStart,
872
+ domInteractiveTime: timing.domInteractive - timing.navigationStart,
873
+ domContentLoadedTime: timing.domContentLoadedEventEnd - timing.navigationStart,
874
+ domCompleteTime: timing.domComplete - timing.navigationStart,
875
+ loadTime: timing.loadEventEnd - timing.navigationStart,
876
+ duration: timing.loadEventEnd - timing.navigationStart
877
+ }), null);
878
+ }
879
+ collectPaintTiming(perf) {
880
+ const entries = perf.getEntriesByType("paint");
881
+ if (!entries.length) return null;
882
+ const fp = entries.find((e) => e.name === "first-paint");
883
+ const fcp = entries.find((e) => e.name === "first-contentful-paint");
884
+ if (!fp && !fcp) return null;
885
+ return {
886
+ firstPaint: fp?.startTime ?? 0,
887
+ firstContentfulPaint: fcp?.startTime ?? 0
888
+ };
889
+ }
890
+ collectLCP(perf) {
891
+ const entries = perf.getEntriesByType("largest-contentful-paint");
892
+ if (!entries.length) return null;
893
+ const last = entries[entries.length - 1];
894
+ return last ? last.startTime : null;
895
+ }
896
+ collectFCP(perf) {
897
+ const entries = perf.getEntriesByType("paint");
898
+ const fcp = entries.find((e) => e.name === "first-contentful-paint");
899
+ return fcp?.startTime ?? null;
900
+ }
901
+ collectCLS(perf) {
902
+ const entries = perf.getEntriesByType("layout-shift");
903
+ if (!entries.length) return null;
904
+ let cls = 0;
905
+ for (const entry of entries) {
906
+ if (!entry.hadRecentInput && entry.value !== void 0) {
907
+ cls += entry.value;
908
+ }
909
+ }
910
+ return cls;
911
+ }
912
+ collectINP(perf) {
913
+ const entries = perf.getEntriesByType("event");
914
+ if (!entries.length) return null;
915
+ const durations = entries.map((e) => e.duration).sort((a, b) => b - a);
916
+ return durations[0] ?? null;
917
+ }
918
+ async destroy() {
919
+ }
920
+ };
921
+
922
+ // ../collectors/src/environment/environment-collector.ts
923
+ var EnvironmentCollector = class {
924
+ name = "environment";
925
+ category = "environment";
926
+ version = SDK_VERSION;
927
+ enabled = true;
928
+ async collect(context) {
929
+ const win = context.window;
930
+ const nav = context.navigator;
931
+ const doc = context.document;
932
+ return {
933
+ environment: {
934
+ timezone: this.getTimezone(),
935
+ timezoneOffset: (/* @__PURE__ */ new Date()).getTimezoneOffset(),
936
+ languages: [...nav.languages],
937
+ language: nav.language,
938
+ locale: this.getLocale(),
939
+ prefersColorScheme: this.getColorScheme(win),
940
+ prefersReducedMotion: this.getReducedMotion(win),
941
+ prefersContrast: this.getContrast(win),
942
+ colorGamut: this.getColorGamut(win),
943
+ hdr: this.getHDR(win),
944
+ localStorageSupport: this.checkStorage(win.localStorage),
945
+ sessionStorageSupport: this.checkStorage(win.sessionStorage),
946
+ indexedDBSupport: this.checkIndexedDB(win.indexedDB),
947
+ cookieSupport: doc.cookie !== void 0,
948
+ cacheAPISupport: this.checkCacheAPI(win)
949
+ }
950
+ };
951
+ }
952
+ getTimezone() {
953
+ try {
954
+ return Intl.DateTimeFormat().resolvedOptions().timeZone;
955
+ } catch {
956
+ return "UTC";
957
+ }
958
+ }
959
+ getLocale() {
960
+ try {
961
+ return Intl.DateTimeFormat().resolvedOptions().locale;
962
+ } catch {
963
+ return "en-US";
964
+ }
965
+ }
966
+ getColorScheme(win) {
967
+ if (typeof win.matchMedia !== "function") return "no-preference";
968
+ const mql = win.matchMedia("(prefers-color-scheme: light)");
969
+ if (mql?.matches) return "light";
970
+ const mqlDark = win.matchMedia("(prefers-color-scheme: dark)");
971
+ if (mqlDark?.matches) return "dark";
972
+ return "no-preference";
973
+ }
974
+ getReducedMotion(win) {
975
+ if (typeof win.matchMedia !== "function") return false;
976
+ return win.matchMedia("(prefers-reduced-motion: reduce)")?.matches ?? false;
977
+ }
978
+ getContrast(win) {
979
+ if (typeof win.matchMedia !== "function") return "no-preference";
980
+ const mql = win.matchMedia("(prefers-contrast: more)");
981
+ if (mql?.matches) return "more";
982
+ const mqlLess = win.matchMedia("(prefers-contrast: less)");
983
+ if (mqlLess?.matches) return "less";
984
+ const mqlCustom = win.matchMedia("(prefers-contrast: custom)");
985
+ if (mqlCustom?.matches) return "custom";
986
+ return "no-preference";
987
+ }
988
+ getColorGamut(win) {
989
+ if (typeof win.matchMedia !== "function") return "unknown";
990
+ const mqlRec2020 = win.matchMedia("(color-gamut: rec2020)");
991
+ if (mqlRec2020?.matches) return "rec2020";
992
+ const mqlP3 = win.matchMedia("(color-gamut: p3)");
993
+ if (mqlP3?.matches) return "p3";
994
+ const mqlSrgb = win.matchMedia("(color-gamut: srgb)");
995
+ if (mqlSrgb?.matches) return "srgb";
996
+ return "unknown";
997
+ }
998
+ getHDR(win) {
999
+ if (typeof win.matchMedia !== "function") return false;
1000
+ return win.matchMedia("(dynamic-range: high)")?.matches ?? false;
1001
+ }
1002
+ checkStorage(storage) {
1003
+ if (!storage) return false;
1004
+ return safeCall(() => typeof storage.length === "number", false);
1005
+ }
1006
+ checkIndexedDB(idb) {
1007
+ if (!idb) return false;
1008
+ return typeof idb.open === "function";
1009
+ }
1010
+ checkCacheAPI(win) {
1011
+ return safeCall(() => {
1012
+ const w = win;
1013
+ return typeof w.Cache !== "undefined" && typeof w.caches !== "undefined";
1014
+ }, false);
1015
+ }
1016
+ async destroy() {
1017
+ }
1018
+ };
1019
+
1020
+ // ../collectors/src/features/feature-collector.ts
1021
+ var FeatureCollector = class {
1022
+ name = "features";
1023
+ category = "features";
1024
+ version = SDK_VERSION;
1025
+ enabled = true;
1026
+ async collect(context) {
1027
+ const win = context.window;
1028
+ const nav = context.navigator;
1029
+ const doc = context.document;
1030
+ return {
1031
+ features: {
1032
+ webgl: this.checkWebGL(doc),
1033
+ webgl2: this.checkWebGL2(doc),
1034
+ webgpu: this.checkWebGPU(nav),
1035
+ wasm: this.checkWASM(),
1036
+ webrtc: this.checkWebRTC(win),
1037
+ websockets: this.checkWebSockets(win),
1038
+ broadcastChannel: this.checkBroadcastChannel(win),
1039
+ sharedWorker: this.checkSharedWorker(win),
1040
+ serviceWorker: this.checkServiceWorker(nav),
1041
+ notifications: this.checkNotifications(win),
1042
+ clipboard: this.checkClipboard(nav),
1043
+ fileSystemAccess: this.checkFileSystemAccess(win),
1044
+ webShare: this.checkWebShare(nav),
1045
+ webAuthn: this.checkWebAuthn(win),
1046
+ pushManager: this.checkPushManager(win),
1047
+ geolocation: this.checkGeolocation(nav),
1048
+ bluetooth: this.checkBluetooth(nav),
1049
+ usb: this.checkUSB(nav),
1050
+ serial: this.checkSerial(nav),
1051
+ gamepad: this.checkGamepad(win),
1052
+ pictureInPicture: this.checkPictureInPicture(win),
1053
+ fullscreen: this.checkFullscreen(doc)
1054
+ }
1055
+ };
1056
+ }
1057
+ checkWebGL(doc) {
1058
+ return safeCall(() => {
1059
+ const canvas = doc.createElement("canvas");
1060
+ const gl = canvas.getContext("webgl") ?? canvas.getContext("experimental-webgl");
1061
+ return gl !== null;
1062
+ }, false);
1063
+ }
1064
+ checkWebGL2(doc) {
1065
+ return safeCall(() => {
1066
+ const canvas = doc.createElement("canvas");
1067
+ const gl = canvas.getContext("webgl2");
1068
+ return gl !== null;
1069
+ }, false);
1070
+ }
1071
+ checkWebGPU(nav) {
1072
+ return safeCall(() => {
1073
+ const gpu = nav.gpu;
1074
+ return typeof gpu === "object" && typeof gpu?.requestAdapter === "function";
1075
+ }, false);
1076
+ }
1077
+ checkWASM() {
1078
+ return safeCall(() => {
1079
+ return typeof WebAssembly === "object" && typeof WebAssembly.instantiate === "function";
1080
+ }, false);
1081
+ }
1082
+ checkWebRTC(win) {
1083
+ return safeCall(() => {
1084
+ return typeof win.RTCPeerConnection !== "undefined";
1085
+ }, false);
1086
+ }
1087
+ checkWebSockets(win) {
1088
+ return typeof win.WebSocket !== "undefined";
1089
+ }
1090
+ checkBroadcastChannel(win) {
1091
+ return typeof win.BroadcastChannel !== "undefined";
1092
+ }
1093
+ checkSharedWorker(win) {
1094
+ return typeof win.SharedWorker !== "undefined";
1095
+ }
1096
+ checkServiceWorker(nav) {
1097
+ return typeof nav !== "undefined" && "serviceWorker" in nav;
1098
+ }
1099
+ checkNotifications(win) {
1100
+ return safeCall(() => {
1101
+ const w = win;
1102
+ return typeof w.Notification === "object" && typeof w.Notification.permission === "string";
1103
+ }, false);
1104
+ }
1105
+ checkClipboard(nav) {
1106
+ return safeCall(() => {
1107
+ const clipboard = nav.clipboard;
1108
+ return typeof clipboard?.readText === "function";
1109
+ }, false);
1110
+ }
1111
+ checkFileSystemAccess(win) {
1112
+ return typeof win.showOpenFilePicker === "function";
1113
+ }
1114
+ checkWebShare(nav) {
1115
+ return safeCall(() => {
1116
+ return typeof nav.share === "function";
1117
+ }, false);
1118
+ }
1119
+ checkWebAuthn(win) {
1120
+ return safeCall(() => {
1121
+ return typeof win.PublicKeyCredential !== "undefined";
1122
+ }, false);
1123
+ }
1124
+ checkPushManager(win) {
1125
+ return safeCall(() => {
1126
+ return typeof win.PushManager !== "undefined";
1127
+ }, false);
1128
+ }
1129
+ checkGeolocation(nav) {
1130
+ return safeCall(() => {
1131
+ return "geolocation" in nav;
1132
+ }, false);
1133
+ }
1134
+ checkBluetooth(nav) {
1135
+ return safeCall(() => {
1136
+ return "bluetooth" in nav;
1137
+ }, false);
1138
+ }
1139
+ checkUSB(nav) {
1140
+ return safeCall(() => {
1141
+ return "usb" in nav;
1142
+ }, false);
1143
+ }
1144
+ checkSerial(nav) {
1145
+ return safeCall(() => {
1146
+ return "serial" in nav;
1147
+ }, false);
1148
+ }
1149
+ checkGamepad(win) {
1150
+ return safeCall(() => {
1151
+ return typeof win.navigator?.getGamepads === "function";
1152
+ }, false);
1153
+ }
1154
+ checkPictureInPicture(win) {
1155
+ return safeCall(() => {
1156
+ const Video = win.HTMLVideoElement;
1157
+ return typeof Video?.prototype?.requestPictureInPicture === "function";
1158
+ }, false);
1159
+ }
1160
+ checkFullscreen(doc) {
1161
+ return safeCall(() => {
1162
+ const el = doc;
1163
+ return typeof el.documentElement?.requestFullscreen === "function";
1164
+ }, false);
1165
+ }
1166
+ async destroy() {
1167
+ }
1168
+ };
1169
+
1170
+ // ../collectors/src/interaction/interaction-collector.ts
1171
+ var InteractionCollector = class {
1172
+ name = "interaction";
1173
+ category = "interaction";
1174
+ version = SDK_VERSION;
1175
+ enabled = true;
1176
+ state;
1177
+ cleanupFns = [];
1178
+ sessionTimeout = null;
1179
+ sessionTimeoutMs;
1180
+ constructor(options) {
1181
+ this.sessionTimeoutMs = options?.sessionTimeout ?? 30 * 60 * 1e3;
1182
+ this.state = this.createInitialState();
1183
+ }
1184
+ createInitialState() {
1185
+ return {
1186
+ sessionStart: Date.now(),
1187
+ pageStart: Date.now(),
1188
+ routeChanges: 0,
1189
+ scrollDepth: 0,
1190
+ clickCount: 0,
1191
+ resizeCount: 0,
1192
+ visibilityChanges: 0,
1193
+ focusChanges: 0,
1194
+ landingPage: typeof location !== "undefined" ? location.href : "",
1195
+ lastPage: typeof location !== "undefined" ? location.href : "",
1196
+ utmParams: this.parseUTMParams()
1197
+ };
1198
+ }
1199
+ parseUTMParams() {
1200
+ if (typeof location === "undefined") {
1201
+ return { source: null, medium: null, campaign: null, term: null, content: null };
1202
+ }
1203
+ const params = new URLSearchParams(location.search);
1204
+ return {
1205
+ source: params.get("utm_source"),
1206
+ medium: params.get("utm_medium"),
1207
+ campaign: params.get("utm_campaign"),
1208
+ term: params.get("utm_term"),
1209
+ content: params.get("utm_content")
1210
+ };
1211
+ }
1212
+ async init(context) {
1213
+ const win = context.window;
1214
+ const doc = context.document;
1215
+ const clickHandler = () => {
1216
+ this.state.clickCount++;
1217
+ this.resetSessionTimeout();
1218
+ };
1219
+ doc.addEventListener("click", clickHandler, { passive: true });
1220
+ this.cleanupFns.push(() => doc.removeEventListener("click", clickHandler));
1221
+ let scrollRaf = null;
1222
+ const scrollHandler = () => {
1223
+ if (scrollRaf !== null) return;
1224
+ scrollRaf = requestAnimationFrame(() => {
1225
+ const maxScroll = Math.max(
1226
+ doc.documentElement.scrollHeight - win.innerHeight,
1227
+ 1
1228
+ );
1229
+ const currentScroll = win.scrollY ?? 0;
1230
+ const depth = Math.min(Math.round(currentScroll / maxScroll * 100), 100);
1231
+ if (depth > this.state.scrollDepth) {
1232
+ this.state.scrollDepth = depth;
1233
+ }
1234
+ scrollRaf = null;
1235
+ });
1236
+ };
1237
+ win.addEventListener("scroll", scrollHandler, { passive: true });
1238
+ this.cleanupFns.push(() => win.removeEventListener("scroll", scrollHandler));
1239
+ let resizeTimer = null;
1240
+ const resizeHandler = () => {
1241
+ if (resizeTimer !== null) return;
1242
+ resizeTimer = setTimeout(() => {
1243
+ this.state.resizeCount++;
1244
+ resizeTimer = null;
1245
+ }, 200);
1246
+ };
1247
+ win.addEventListener("resize", resizeHandler, { passive: true });
1248
+ this.cleanupFns.push(() => {
1249
+ win.removeEventListener("resize", resizeHandler);
1250
+ if (resizeTimer !== null) clearTimeout(resizeTimer);
1251
+ });
1252
+ const visHandler = () => {
1253
+ this.state.visibilityChanges++;
1254
+ };
1255
+ doc.addEventListener("visibilitychange", visHandler, { passive: true });
1256
+ this.cleanupFns.push(() => doc.removeEventListener("visibilitychange", visHandler));
1257
+ const focusHandler = () => {
1258
+ this.state.focusChanges++;
1259
+ };
1260
+ win.addEventListener("focus", focusHandler, { passive: true });
1261
+ win.addEventListener("blur", focusHandler, { passive: true });
1262
+ this.cleanupFns.push(() => {
1263
+ win.removeEventListener("focus", focusHandler);
1264
+ win.removeEventListener("blur", focusHandler);
1265
+ });
1266
+ const onPopState = () => {
1267
+ this.state.routeChanges++;
1268
+ this.state.lastPage = location.href;
1269
+ };
1270
+ win.addEventListener("popstate", onPopState, { passive: true });
1271
+ this.cleanupFns.push(() => {
1272
+ win.removeEventListener("popstate", onPopState);
1273
+ });
1274
+ this.startSessionTimeout();
1275
+ }
1276
+ startSessionTimeout() {
1277
+ this.sessionTimeout = setTimeout(() => {
1278
+ this.state.sessionStart = Date.now();
1279
+ this.state.routeChanges = 0;
1280
+ this.state.clickCount = 0;
1281
+ this.state.resizeCount = 0;
1282
+ this.state.visibilityChanges = 0;
1283
+ this.state.focusChanges = 0;
1284
+ this.state.scrollDepth = 0;
1285
+ }, this.sessionTimeoutMs);
1286
+ }
1287
+ resetSessionTimeout() {
1288
+ if (this.sessionTimeout !== null) {
1289
+ clearTimeout(this.sessionTimeout);
1290
+ }
1291
+ this.startSessionTimeout();
1292
+ }
1293
+ async collect(_context) {
1294
+ const now = Date.now();
1295
+ return {
1296
+ interaction: {
1297
+ sessionDuration: now - this.state.sessionStart,
1298
+ timeOnPage: now - this.state.pageStart,
1299
+ routeChanges: this.state.routeChanges,
1300
+ scrollDepth: this.state.scrollDepth,
1301
+ clickCount: this.state.clickCount,
1302
+ resizeCount: this.state.resizeCount,
1303
+ visibilityChanges: this.state.visibilityChanges,
1304
+ focusChanges: this.state.focusChanges,
1305
+ landingPage: this.state.landingPage,
1306
+ exitPage: this.state.lastPage,
1307
+ utmSource: this.state.utmParams.source,
1308
+ utmMedium: this.state.utmParams.medium,
1309
+ utmCampaign: this.state.utmParams.campaign,
1310
+ utmTerm: this.state.utmParams.term,
1311
+ utmContent: this.state.utmParams.content
1312
+ }
1313
+ };
1314
+ }
1315
+ async destroy() {
1316
+ if (this.sessionTimeout !== null) {
1317
+ clearTimeout(this.sessionTimeout);
1318
+ }
1319
+ for (const fn of this.cleanupFns) {
1320
+ fn();
1321
+ }
1322
+ this.cleanupFns = [];
1323
+ }
1324
+ };
1325
+
1326
+ // src/visitor-analytics.ts
1327
+ var DEFAULT_CONFIG2 = {
1328
+ endpoint: "",
1329
+ storage: "memory",
1330
+ autoStart: true,
1331
+ batchSize: 50,
1332
+ flushInterval: 3e4,
1333
+ maxRetries: 5,
1334
+ retryBaseDelay: 1e3,
1335
+ retryMaxDelay: 3e4,
1336
+ compressionEnabled: false,
1337
+ deduplicationEnabled: true,
1338
+ headers: {},
1339
+ timeout: 1e4,
1340
+ collectBrowser: true,
1341
+ collectDevice: true,
1342
+ collectPerformance: true,
1343
+ collectEnvironment: true,
1344
+ collectFeatures: true,
1345
+ collectInteraction: true,
1346
+ includeUserAgent: false,
1347
+ includeReferrer: true,
1348
+ sessionTimeout: 30 * 60 * 1e3,
1349
+ customData: {}
1350
+ };
1351
+ var VisitorAnalytics = class {
1352
+ config;
1353
+ eventBus;
1354
+ pluginManager;
1355
+ storage;
1356
+ uploader;
1357
+ collectors = [];
1358
+ collectorContext = null;
1359
+ isRunning = false;
1360
+ sessionId;
1361
+ collectTimer = null;
1362
+ constructor(config = {}) {
1363
+ const merged = { ...DEFAULT_CONFIG2, ...config };
1364
+ if (config.customData) {
1365
+ merged.customData = { ...DEFAULT_CONFIG2.customData, ...config.customData };
1366
+ }
1367
+ if (config.headers) {
1368
+ merged.headers = { ...DEFAULT_CONFIG2.headers, ...config.headers };
1369
+ }
1370
+ this.config = merged;
1371
+ this.eventBus = new EventBus();
1372
+ this.sessionId = generateId();
1373
+ if (typeof this.config.storage === "object" && typeof this.config.storage.save === "function") {
1374
+ this.storage = this.config.storage;
1375
+ } else {
1376
+ this.storage = StorageAdapterFactory.create(this.config.storage);
1377
+ }
1378
+ this.uploader = new Uploader(this.storage, {
1379
+ endpoint: this.config.endpoint,
1380
+ batchSize: this.config.batchSize,
1381
+ flushInterval: this.config.flushInterval,
1382
+ maxRetries: this.config.maxRetries,
1383
+ retryBaseDelay: this.config.retryBaseDelay,
1384
+ retryMaxDelay: this.config.retryMaxDelay,
1385
+ compressionEnabled: this.config.compressionEnabled,
1386
+ deduplicationEnabled: this.config.deduplicationEnabled,
1387
+ headers: this.config.headers,
1388
+ timeout: this.config.timeout
1389
+ });
1390
+ this.pluginManager = new PluginManager(this.eventBus);
1391
+ this.registerBuiltInCollectors();
1392
+ if (this.config.autoStart && isBrowser()) {
1393
+ requestIdle(() => this.start());
1394
+ }
1395
+ }
1396
+ registerBuiltInCollectors() {
1397
+ if (this.config.collectBrowser) {
1398
+ this.addCollector(new BrowserCollector({ includeUserAgent: this.config.includeUserAgent }));
1399
+ }
1400
+ if (this.config.collectDevice) {
1401
+ this.addCollector(new DeviceCollector());
1402
+ }
1403
+ if (this.config.collectPerformance) {
1404
+ this.addCollector(new PerformanceCollector());
1405
+ }
1406
+ if (this.config.collectEnvironment) {
1407
+ this.addCollector(new EnvironmentCollector());
1408
+ }
1409
+ if (this.config.collectFeatures) {
1410
+ this.addCollector(new FeatureCollector());
1411
+ }
1412
+ if (this.config.collectInteraction) {
1413
+ this.addCollector(
1414
+ new InteractionCollector({ sessionTimeout: this.config.sessionTimeout })
1415
+ );
1416
+ }
1417
+ }
1418
+ buildCollectorContext() {
1419
+ if (this.collectorContext) return this.collectorContext;
1420
+ const ctx = {
1421
+ document,
1422
+ navigator,
1423
+ window,
1424
+ performance,
1425
+ location,
1426
+ history,
1427
+ screen
1428
+ };
1429
+ this.collectorContext = ctx;
1430
+ return ctx;
1431
+ }
1432
+ start() {
1433
+ if (this.isRunning) return;
1434
+ this.isRunning = true;
1435
+ this.uploader.start();
1436
+ this.eventBus.emit("start");
1437
+ for (const collector of this.collectors) {
1438
+ if (collector.init && isBrowser()) {
1439
+ requestIdle(() => {
1440
+ collector.init(this.buildCollectorContext()).catch((err) => {
1441
+ console.debug("[VisitorAnalytics] Collector init failed:", collector.name, err);
1442
+ });
1443
+ });
1444
+ }
1445
+ }
1446
+ this.collectTimer = setInterval(() => {
1447
+ requestIdle(() => this.collectAndStore());
1448
+ }, this.config.flushInterval);
1449
+ }
1450
+ stop() {
1451
+ if (!this.isRunning) return;
1452
+ this.isRunning = false;
1453
+ this.uploader.stop();
1454
+ if (this.collectTimer !== null) {
1455
+ clearInterval(this.collectTimer);
1456
+ this.collectTimer = null;
1457
+ }
1458
+ this.eventBus.emit("stop");
1459
+ }
1460
+ async flush() {
1461
+ await this.collectAndStore();
1462
+ await this.uploader.flush();
1463
+ this.eventBus.emit("flush");
1464
+ }
1465
+ async sync() {
1466
+ await this.collectAndStore();
1467
+ await this.uploader.sync();
1468
+ this.eventBus.emit("sync");
1469
+ }
1470
+ async retryFailed() {
1471
+ await this.uploader.retryFailed();
1472
+ }
1473
+ use(plugin) {
1474
+ const pluginContext = this.createPluginContext();
1475
+ this.pluginManager.install(plugin, pluginContext);
1476
+ }
1477
+ addCollector(collector) {
1478
+ if (this.collectors.some((c) => c.name === collector.name)) {
1479
+ return;
1480
+ }
1481
+ this.collectors.push(collector);
1482
+ this.pluginManager.addCollector(collector);
1483
+ this.eventBus.emit("collector-registered", collector.name);
1484
+ }
1485
+ removeCollector(name) {
1486
+ const idx = this.collectors.findIndex((c) => c.name === name);
1487
+ if (idx === -1) return;
1488
+ const collector = this.collectors[idx];
1489
+ if (collector?.destroy) {
1490
+ collector.destroy().catch(() => {
1491
+ });
1492
+ }
1493
+ this.collectors.splice(idx, 1);
1494
+ this.pluginManager.removeCollector(name);
1495
+ this.eventBus.emit("collector-removed", name);
1496
+ }
1497
+ async getCollectedData() {
1498
+ return this.storage.load();
1499
+ }
1500
+ async export() {
1501
+ return this.storage.export();
1502
+ }
1503
+ on(event, handler) {
1504
+ this.eventBus.on(event, handler);
1505
+ }
1506
+ off(event, handler) {
1507
+ this.eventBus.off(event, handler);
1508
+ }
1509
+ async destroy() {
1510
+ this.stop();
1511
+ for (const collector of this.collectors) {
1512
+ if (collector.destroy) {
1513
+ await collector.destroy();
1514
+ }
1515
+ }
1516
+ this.uploader.destroy();
1517
+ this.eventBus.removeAllListeners();
1518
+ this.collectors = [];
1519
+ this.collectorContext = null;
1520
+ }
1521
+ async collectAndStore() {
1522
+ if (!isBrowser()) return;
1523
+ const context = this.buildCollectorContext();
1524
+ const record = await this.collectRecord(context);
1525
+ if (record) {
1526
+ await this.storage.save(record);
1527
+ this.eventBus.emit("record-collected", record);
1528
+ }
1529
+ }
1530
+ async collectRecord(context) {
1531
+ const now = Date.now();
1532
+ const partials = [];
1533
+ for (const collector of this.collectors) {
1534
+ if (!collector.enabled) continue;
1535
+ try {
1536
+ const data = await collector.collect(context);
1537
+ partials.push(data);
1538
+ } catch (err) {
1539
+ console.debug("[VisitorAnalytics] Collector collect failed:", collector.name, err);
1540
+ }
1541
+ }
1542
+ if (partials.length === 0) return null;
1543
+ const merged = structuredClone(Object.assign({}, ...partials));
1544
+ const record = {
1545
+ id: generateId(),
1546
+ timestamp: now,
1547
+ sessionId: this.sessionId,
1548
+ pageUrl: location.href,
1549
+ pagePath: location.pathname,
1550
+ referrer: this.config.includeReferrer ? document.referrer : "",
1551
+ screenWidth: screen.width,
1552
+ screenHeight: screen.height,
1553
+ viewportWidth: window.innerWidth,
1554
+ viewportHeight: window.innerHeight,
1555
+ devicePixelRatio: window.devicePixelRatio,
1556
+ browser: merged.browser ?? {
1557
+ name: "unknown",
1558
+ version: "0",
1559
+ engine: "unknown",
1560
+ engineVersion: "0",
1561
+ userAgent: "",
1562
+ language: "",
1563
+ cookiesEnabled: false,
1564
+ javaScriptEnabled: true,
1565
+ doNotTrack: null
1566
+ },
1567
+ device: merged.device ?? {
1568
+ os: "unknown",
1569
+ osVersion: "unknown",
1570
+ platform: "",
1571
+ architecture: "unknown",
1572
+ formFactor: "unknown",
1573
+ screenWidth: 0,
1574
+ screenHeight: 0,
1575
+ viewportWidth: 0,
1576
+ viewportHeight: 0,
1577
+ devicePixelRatio: 1,
1578
+ colorDepth: 24,
1579
+ orientation: "portrait-primary",
1580
+ touchSupport: "none",
1581
+ hardwareConcurrency: 0,
1582
+ maxTouchPoints: 0
1583
+ },
1584
+ performance: merged.performance ?? {
1585
+ navigationTiming: null,
1586
+ paintTiming: null,
1587
+ largestContentfulPaint: null,
1588
+ firstContentfulPaint: null,
1589
+ cumulativeLayoutShift: null,
1590
+ interactionToNextPaint: null,
1591
+ deviceMemory: null,
1592
+ networkType: "unknown",
1593
+ effectiveType: "unknown",
1594
+ downlink: null,
1595
+ rtt: null,
1596
+ saveData: false
1597
+ },
1598
+ environment: merged.environment ?? {
1599
+ timezone: "UTC",
1600
+ timezoneOffset: 0,
1601
+ languages: [],
1602
+ language: "",
1603
+ locale: "",
1604
+ prefersColorScheme: "no-preference",
1605
+ prefersReducedMotion: false,
1606
+ prefersContrast: "no-preference",
1607
+ colorGamut: "unknown",
1608
+ hdr: false,
1609
+ localStorageSupport: false,
1610
+ sessionStorageSupport: false,
1611
+ indexedDBSupport: false,
1612
+ cookieSupport: false,
1613
+ cacheAPISupport: false
1614
+ },
1615
+ features: merged.features ?? {
1616
+ webgl: false,
1617
+ webgl2: false,
1618
+ webgpu: false,
1619
+ wasm: false,
1620
+ webrtc: false,
1621
+ websockets: false,
1622
+ broadcastChannel: false,
1623
+ sharedWorker: false,
1624
+ serviceWorker: false,
1625
+ notifications: false,
1626
+ clipboard: false,
1627
+ fileSystemAccess: false,
1628
+ webShare: false,
1629
+ webAuthn: false,
1630
+ pushManager: false,
1631
+ geolocation: false,
1632
+ bluetooth: false,
1633
+ usb: false,
1634
+ serial: false,
1635
+ gamepad: false,
1636
+ pictureInPicture: false,
1637
+ fullscreen: false
1638
+ },
1639
+ interaction: merged.interaction ?? {
1640
+ sessionDuration: 0,
1641
+ timeOnPage: 0,
1642
+ routeChanges: 0,
1643
+ scrollDepth: 0,
1644
+ clickCount: 0,
1645
+ resizeCount: 0,
1646
+ visibilityChanges: 0,
1647
+ focusChanges: 0,
1648
+ landingPage: "",
1649
+ exitPage: null,
1650
+ utmSource: null,
1651
+ utmMedium: null,
1652
+ utmCampaign: null,
1653
+ utmTerm: null,
1654
+ utmContent: null
1655
+ },
1656
+ metadata: {
1657
+ sdkVersion: SDK_VERSION,
1658
+ buildTarget: "browser",
1659
+ collectorVersion: SDK_VERSION,
1660
+ customData: this.config.customData
1661
+ }
1662
+ };
1663
+ deepFreeze(record);
1664
+ return record;
1665
+ }
1666
+ createPluginContext() {
1667
+ return createPluginContext(
1668
+ (c) => this.addCollector(c),
1669
+ (name) => this.removeCollector(name),
1670
+ this.eventBus,
1671
+ () => this.config
1672
+ );
1673
+ }
1674
+ };
1675
+ function createAnalytics(config) {
1676
+ return new VisitorAnalytics(config);
1677
+ }
1678
+ export {
1679
+ EventBus,
1680
+ VisitorAnalytics,
1681
+ createAnalytics
1682
+ };