@datalyr/web 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.
@@ -0,0 +1,2835 @@
1
+ /**
2
+ * @datalyr/web v1.0.0
3
+ * Datalyr Web SDK - Modern attribution tracking for web applications
4
+ * (c) 2025 Datalyr Inc.
5
+ * Released under the MIT License
6
+ */
7
+ 'use strict';
8
+
9
+ /******************************************************************************
10
+ Copyright (c) Microsoft Corporation.
11
+
12
+ Permission to use, copy, modify, and/or distribute this software for any
13
+ purpose with or without fee is hereby granted.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
16
+ REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
17
+ AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
18
+ INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
19
+ LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
20
+ OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
21
+ PERFORMANCE OF THIS SOFTWARE.
22
+ ***************************************************************************** */
23
+ /* global Reflect, Promise, SuppressedError, Symbol, Iterator */
24
+
25
+
26
+ function __awaiter(thisArg, _arguments, P, generator) {
27
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
28
+ return new (P || (P = Promise))(function (resolve, reject) {
29
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
30
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
31
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
32
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
33
+ });
34
+ }
35
+
36
+ typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
37
+ var e = new Error(message);
38
+ return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
39
+ };
40
+
41
+ /**
42
+ * Storage Module
43
+ * Safe storage wrapper with fallbacks for Safari private mode
44
+ */
45
+ class SafeStorage {
46
+ constructor(storage) {
47
+ this.memory = new Map();
48
+ this.prefix = '__dl_';
49
+ // Test if storage is available
50
+ try {
51
+ const testKey = '__dl_test__' + Math.random();
52
+ storage.setItem(testKey, '1');
53
+ storage.removeItem(testKey);
54
+ this.storage = storage;
55
+ }
56
+ catch (_a) {
57
+ // Storage not available (Safari private mode, etc.)
58
+ this.storage = null;
59
+ console.warn('[Datalyr] Storage not available, using memory fallback');
60
+ }
61
+ }
62
+ get(key, defaultValue = null) {
63
+ const fullKey = this.prefix + key;
64
+ try {
65
+ if (this.storage) {
66
+ const value = this.storage.getItem(fullKey);
67
+ if (value === null)
68
+ return defaultValue;
69
+ // Try to parse JSON
70
+ try {
71
+ return JSON.parse(value);
72
+ }
73
+ catch (_a) {
74
+ return value;
75
+ }
76
+ }
77
+ else {
78
+ const value = this.memory.get(fullKey);
79
+ if (value === undefined)
80
+ return defaultValue;
81
+ try {
82
+ return JSON.parse(value);
83
+ }
84
+ catch (_b) {
85
+ return value;
86
+ }
87
+ }
88
+ }
89
+ catch (_c) {
90
+ return defaultValue;
91
+ }
92
+ }
93
+ set(key, value) {
94
+ const fullKey = this.prefix + key;
95
+ const stringValue = typeof value === 'string' ? value : JSON.stringify(value);
96
+ try {
97
+ if (this.storage) {
98
+ this.storage.setItem(fullKey, stringValue);
99
+ return true;
100
+ }
101
+ else {
102
+ this.memory.set(fullKey, stringValue);
103
+ return true;
104
+ }
105
+ }
106
+ catch (e) {
107
+ // Quota exceeded or other error
108
+ console.warn('[Datalyr] Failed to store:', key, e);
109
+ // Try memory fallback
110
+ this.memory.set(fullKey, stringValue);
111
+ return false;
112
+ }
113
+ }
114
+ remove(key) {
115
+ const fullKey = this.prefix + key;
116
+ try {
117
+ if (this.storage) {
118
+ this.storage.removeItem(fullKey);
119
+ return true;
120
+ }
121
+ else {
122
+ this.memory.delete(fullKey);
123
+ return true;
124
+ }
125
+ }
126
+ catch (_a) {
127
+ return false;
128
+ }
129
+ }
130
+ keys() {
131
+ try {
132
+ if (this.storage) {
133
+ const keys = [];
134
+ for (let i = 0; i < this.storage.length; i++) {
135
+ const key = this.storage.key(i);
136
+ if (key && key.startsWith(this.prefix)) {
137
+ keys.push(key.slice(this.prefix.length));
138
+ }
139
+ }
140
+ return keys;
141
+ }
142
+ else {
143
+ return Array.from(this.memory.keys())
144
+ .filter(k => k.startsWith(this.prefix))
145
+ .map(k => k.slice(this.prefix.length));
146
+ }
147
+ }
148
+ catch (_a) {
149
+ return [];
150
+ }
151
+ }
152
+ }
153
+ // Cookie operations
154
+ class CookieStorage {
155
+ constructor(options = {}) {
156
+ this.domain = options.domain || 'auto';
157
+ this.maxAge = options.maxAge || 365;
158
+ this.sameSite = options.sameSite || 'Lax';
159
+ this.secure = options.secure || 'auto';
160
+ }
161
+ get(name) {
162
+ var _a;
163
+ const value = `; ${document.cookie}`;
164
+ const parts = value.split(`; ${name}=`);
165
+ if (parts.length === 2) {
166
+ return ((_a = parts.pop()) === null || _a === void 0 ? void 0 : _a.split(';').shift()) || null;
167
+ }
168
+ return null;
169
+ }
170
+ set(name, value, days) {
171
+ try {
172
+ const maxAge = (days || this.maxAge) * 86400; // Convert to seconds
173
+ const secure = this.secure === 'auto'
174
+ ? location.protocol === 'https:'
175
+ : this.secure;
176
+ let domain = '';
177
+ if (this.domain === 'auto') {
178
+ // Auto-detect domain for cross-subdomain tracking
179
+ domain = this.getAutoDomain();
180
+ }
181
+ else if (this.domain) {
182
+ domain = `;domain=${this.domain}`;
183
+ }
184
+ const cookie = [
185
+ `${name}=${encodeURIComponent(value)}`,
186
+ `max-age=${maxAge}`,
187
+ 'path=/',
188
+ `SameSite=${this.sameSite}`,
189
+ secure ? 'Secure' : '',
190
+ domain
191
+ ].filter(Boolean).join(';');
192
+ document.cookie = cookie;
193
+ return true;
194
+ }
195
+ catch (e) {
196
+ console.warn('[Datalyr] Failed to set cookie:', name, e);
197
+ return false;
198
+ }
199
+ }
200
+ remove(name) {
201
+ try {
202
+ // Try to remove with various domain settings
203
+ const domains = ['', location.hostname];
204
+ // Add parent domain variations
205
+ const parts = location.hostname.split('.');
206
+ if (parts.length > 2) {
207
+ domains.push(`.${parts.slice(-2).join('.')}`);
208
+ domains.push(`.${location.hostname}`);
209
+ }
210
+ domains.forEach(domain => {
211
+ const domainStr = domain ? `;domain=${domain}` : '';
212
+ document.cookie = `${name}=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/${domainStr}`;
213
+ });
214
+ return true;
215
+ }
216
+ catch (_a) {
217
+ return false;
218
+ }
219
+ }
220
+ /**
221
+ * Auto-detect the best domain for cross-subdomain tracking
222
+ */
223
+ getAutoDomain() {
224
+ const hostname = location.hostname;
225
+ // Don't set domain for localhost or IP addresses
226
+ if (hostname === 'localhost' || /^[\d.]+$/.test(hostname) || /^\[[\d:]+\]$/.test(hostname)) {
227
+ return '';
228
+ }
229
+ // Try setting cookie at different domain levels to find the highest allowed
230
+ const parts = hostname.split('.');
231
+ // Start from the root domain and work up
232
+ for (let i = parts.length - 2; i >= 0; i--) {
233
+ const testDomain = '.' + parts.slice(i).join('.');
234
+ const testName = '__dl_test_' + Math.random();
235
+ // Try to set a test cookie
236
+ document.cookie = `${testName}=1;domain=${testDomain};path=/`;
237
+ // Check if it was set successfully
238
+ if (document.cookie.indexOf(testName) !== -1) {
239
+ // Remove test cookie
240
+ document.cookie = `${testName}=;expires=Thu, 01 Jan 1970 00:00:00 GMT;domain=${testDomain};path=/`;
241
+ return `;domain=${testDomain}`;
242
+ }
243
+ }
244
+ // If nothing worked, don't set domain (will default to current subdomain)
245
+ return '';
246
+ }
247
+ }
248
+ // Export singleton instances for storage
249
+ const storage = new SafeStorage(window.localStorage);
250
+ new SafeStorage(window.sessionStorage);
251
+ // Default cookie instance for backwards compatibility
252
+ const cookies = new CookieStorage();
253
+
254
+ /**
255
+ * Utility Functions
256
+ */
257
+ /**
258
+ * Generate UUID v4
259
+ */
260
+ function generateUUID() {
261
+ // Use crypto.randomUUID if available
262
+ if (typeof crypto !== 'undefined' && crypto.randomUUID) {
263
+ return crypto.randomUUID();
264
+ }
265
+ // Fallback implementation
266
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
267
+ const r = Math.random() * 16 | 0;
268
+ const v = c === 'x' ? r : (r & 0x3 | 0x8);
269
+ return v.toString(16);
270
+ });
271
+ }
272
+ /**
273
+ * Get all URL query parameters
274
+ */
275
+ function getAllQueryParams(search = window.location.search) {
276
+ const params = {};
277
+ try {
278
+ if ('URLSearchParams' in window) {
279
+ const searchParams = new URLSearchParams(search);
280
+ searchParams.forEach((value, key) => {
281
+ params[key] = value;
282
+ });
283
+ return params;
284
+ }
285
+ }
286
+ catch (_a) { }
287
+ // Manual fallback
288
+ const query = (search || '').replace(/^\?/, '').split('&');
289
+ for (const part of query) {
290
+ const [key, value = ''] = part.split('=');
291
+ try {
292
+ const decodedKey = decodeURIComponent((key || '').replace(/\+/g, ' '));
293
+ const decodedValue = decodeURIComponent((value || '').replace(/\+/g, ' '));
294
+ if (decodedKey) {
295
+ params[decodedKey] = decodedValue;
296
+ }
297
+ }
298
+ catch (_b) {
299
+ // Ignore bad encoding
300
+ }
301
+ }
302
+ return params;
303
+ }
304
+ /**
305
+ * Sanitize event data (remove sensitive keys, DOM elements, functions)
306
+ */
307
+ function sanitizeEventData(data, maxDepth = 5, currentDepth = 0) {
308
+ if (currentDepth >= maxDepth)
309
+ return '[Max depth reached]';
310
+ if (data === null || data === undefined)
311
+ return data;
312
+ // Remove DOM elements and functions
313
+ if ((typeof Element !== 'undefined' && data instanceof Element) ||
314
+ (typeof Document !== 'undefined' && data instanceof Document) ||
315
+ typeof data === 'function') {
316
+ return '[Removed]';
317
+ }
318
+ // Handle arrays
319
+ if (Array.isArray(data)) {
320
+ return data.map(item => sanitizeEventData(item, maxDepth, currentDepth + 1));
321
+ }
322
+ // Handle objects
323
+ if (typeof data === 'object') {
324
+ const sanitized = {};
325
+ const sensitiveKeys = /pass|pwd|token|secret|auth|bearer|session|cookie|signature|api[-_]?key|private[-_]?key|access[-_]?token|refresh[-_]?token/i;
326
+ for (const key in data) {
327
+ if (Object.prototype.hasOwnProperty.call(data, key)) {
328
+ // Skip sensitive keys
329
+ if (sensitiveKeys.test(key)) {
330
+ continue;
331
+ }
332
+ // Sanitize value recursively
333
+ sanitized[key] = sanitizeEventData(data[key], maxDepth, currentDepth + 1);
334
+ }
335
+ }
336
+ return sanitized;
337
+ }
338
+ // Handle strings
339
+ if (typeof data === 'string') {
340
+ // Truncate very long strings
341
+ if (data.length > 1000) {
342
+ return data.slice(0, 1000) + '...[truncated]';
343
+ }
344
+ // Remove potential JWT tokens or API keys
345
+ if (data.match(/^[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+$/) || // JWT
346
+ data.match(/^[a-f0-9]{32,}$/i)) { // Hex tokens
347
+ return '[Redacted]';
348
+ }
349
+ return data;
350
+ }
351
+ return data;
352
+ }
353
+ /**
354
+ * Deep merge objects
355
+ */
356
+ function deepMerge(target, ...sources) {
357
+ if (!sources.length)
358
+ return target;
359
+ const source = sources.shift();
360
+ if (isObject(target) && isObject(source)) {
361
+ for (const key in source) {
362
+ if (isObject(source[key])) {
363
+ if (!target[key])
364
+ Object.assign(target, { [key]: {} });
365
+ deepMerge(target[key], source[key]);
366
+ }
367
+ else {
368
+ Object.assign(target, { [key]: source[key] });
369
+ }
370
+ }
371
+ }
372
+ return deepMerge(target, ...sources);
373
+ }
374
+ function isObject(item) {
375
+ return item && typeof item === 'object' && !Array.isArray(item);
376
+ }
377
+ /**
378
+ * Calculate retry delay with exponential backoff and jitter
379
+ */
380
+ function calculateRetryDelay(attempt, baseDelay = 1000) {
381
+ const maxDelay = 30000; // 30 seconds max
382
+ const jitter = Math.random() * 0.1; // 10% jitter
383
+ return Math.min(baseDelay * Math.pow(2, attempt) * (1 + jitter), maxDelay);
384
+ }
385
+ /**
386
+ * Check if browser Do Not Track is enabled
387
+ */
388
+ function isDoNotTrackEnabled() {
389
+ return navigator.doNotTrack === '1' ||
390
+ window.doNotTrack === '1' ||
391
+ navigator.doNotTrack === 'yes';
392
+ }
393
+ /**
394
+ * Check if Global Privacy Control is enabled
395
+ */
396
+ function isGlobalPrivacyControlEnabled() {
397
+ return navigator.globalPrivacyControl === true ||
398
+ window.globalPrivacyControl === true;
399
+ }
400
+ /**
401
+ * Get referrer data
402
+ */
403
+ function getReferrerData() {
404
+ const referrer = document.referrer;
405
+ if (!referrer)
406
+ return {};
407
+ try {
408
+ const url = new URL(referrer);
409
+ return {
410
+ referrer,
411
+ referrer_host: url.hostname,
412
+ referrer_path: url.pathname,
413
+ referrer_search: url.search,
414
+ referrer_source: detectReferrerSource(url.hostname)
415
+ };
416
+ }
417
+ catch (_a) {
418
+ return { referrer };
419
+ }
420
+ }
421
+ /**
422
+ * Detect referrer source
423
+ */
424
+ function detectReferrerSource(hostname) {
425
+ const sources = {
426
+ google: ['google.com', 'google.'],
427
+ facebook: ['facebook.com', 'fb.com'],
428
+ twitter: ['twitter.com', 't.co', 'x.com'],
429
+ linkedin: ['linkedin.com', 'lnkd.in'],
430
+ instagram: ['instagram.com'],
431
+ youtube: ['youtube.com', 'youtu.be'],
432
+ tiktok: ['tiktok.com'],
433
+ reddit: ['reddit.com'],
434
+ pinterest: ['pinterest.com'],
435
+ bing: ['bing.com'],
436
+ yahoo: ['yahoo.com'],
437
+ duckduckgo: ['duckduckgo.com'],
438
+ baidu: ['baidu.com']
439
+ };
440
+ for (const [source, domains] of Object.entries(sources)) {
441
+ if (domains.some(domain => hostname.includes(domain))) {
442
+ return source;
443
+ }
444
+ }
445
+ return 'other';
446
+ }
447
+
448
+ /**
449
+ * Identity Management Module
450
+ * Handles anonymous_id, user_id, and identity resolution
451
+ */
452
+ class IdentityManager {
453
+ constructor() {
454
+ this.userId = null;
455
+ this.sessionId = null;
456
+ this.anonymousId = this.getOrCreateAnonymousId();
457
+ this.userId = this.getStoredUserId();
458
+ }
459
+ /**
460
+ * Get or create anonymous ID (device/browser identifier)
461
+ */
462
+ getOrCreateAnonymousId() {
463
+ let anonymousId = storage.get('dl_anonymous_id');
464
+ if (!anonymousId) {
465
+ anonymousId = `anon_${generateUUID()}`;
466
+ storage.set('dl_anonymous_id', anonymousId);
467
+ }
468
+ return anonymousId;
469
+ }
470
+ /**
471
+ * Get stored user ID from previous session
472
+ */
473
+ getStoredUserId() {
474
+ return storage.get('dl_user_id');
475
+ }
476
+ /**
477
+ * Get the anonymous ID
478
+ */
479
+ getAnonymousId() {
480
+ return this.anonymousId;
481
+ }
482
+ /**
483
+ * Get the user ID (if identified)
484
+ */
485
+ getUserId() {
486
+ return this.userId;
487
+ }
488
+ /**
489
+ * Get the distinct ID (primary identifier)
490
+ * Returns user_id if identified, otherwise anonymous_id
491
+ */
492
+ getDistinctId() {
493
+ return this.userId || this.anonymousId;
494
+ }
495
+ /**
496
+ * Get canonical ID (alias for distinct_id)
497
+ */
498
+ getCanonicalId() {
499
+ return this.getDistinctId();
500
+ }
501
+ /**
502
+ * Set the session ID
503
+ */
504
+ setSessionId(sessionId) {
505
+ this.sessionId = sessionId;
506
+ }
507
+ /**
508
+ * Get the session ID
509
+ */
510
+ getSessionId() {
511
+ return this.sessionId;
512
+ }
513
+ /**
514
+ * Identify a user
515
+ * Links anonymous_id to user_id
516
+ */
517
+ identify(userId, traits = {}) {
518
+ if (!userId) {
519
+ console.warn('[Datalyr] identify() called without userId');
520
+ return {};
521
+ }
522
+ const previousUserId = this.userId;
523
+ this.userId = userId;
524
+ // Persist for future sessions
525
+ storage.set('dl_user_id', userId);
526
+ // Return identity link data (will be sent as $identify event)
527
+ return {
528
+ anonymous_id: this.anonymousId,
529
+ user_id: userId,
530
+ previous_id: previousUserId,
531
+ traits: traits,
532
+ identified_at: new Date().toISOString(),
533
+ resolution_method: 'identify_call'
534
+ };
535
+ }
536
+ /**
537
+ * Alias one ID to another
538
+ */
539
+ alias(userId, previousId) {
540
+ const aliasData = {
541
+ userId,
542
+ previousId: previousId || this.anonymousId,
543
+ aliased_at: new Date().toISOString()
544
+ };
545
+ // Update current user ID if aliasing to current anonymous ID
546
+ if (!previousId || previousId === this.anonymousId) {
547
+ this.userId = userId;
548
+ storage.set('dl_user_id', userId);
549
+ }
550
+ return aliasData;
551
+ }
552
+ /**
553
+ * Reset the current user (on logout)
554
+ * Clears user_id but keeps anonymous_id
555
+ */
556
+ reset() {
557
+ this.userId = null;
558
+ storage.remove('dl_user_id');
559
+ storage.remove('dl_user_traits');
560
+ // Generate new anonymous ID for privacy
561
+ this.anonymousId = `anon_${generateUUID()}`;
562
+ storage.set('dl_anonymous_id', this.anonymousId);
563
+ }
564
+ /**
565
+ * Get all identity fields for event payload
566
+ */
567
+ getIdentityFields() {
568
+ return {
569
+ // Modern fields
570
+ distinct_id: this.getDistinctId(),
571
+ anonymous_id: this.anonymousId,
572
+ user_id: this.userId,
573
+ // Legacy compatibility
574
+ visitor_id: this.anonymousId,
575
+ visitorId: this.anonymousId,
576
+ canonical_id: this.getCanonicalId(),
577
+ // Session
578
+ session_id: this.sessionId,
579
+ sessionId: this.sessionId,
580
+ // Identity resolution
581
+ resolution_method: 'browser_sdk',
582
+ resolution_confidence: 1.0
583
+ };
584
+ }
585
+ }
586
+
587
+ /**
588
+ * Session Management Module
589
+ */
590
+ class SessionManager {
591
+ constructor(timeout = 30 * 60 * 1000) {
592
+ this.sessionId = null;
593
+ this.sessionData = null;
594
+ this.lastActivity = Date.now();
595
+ this.SESSION_KEY = 'dl_session_data';
596
+ this.activityCheckInterval = null;
597
+ this.activityListeners = [];
598
+ this.sessionTimeout = timeout;
599
+ this.initSession();
600
+ this.setupActivityMonitor();
601
+ }
602
+ /**
603
+ * Initialize or restore session
604
+ */
605
+ initSession() {
606
+ const storedSession = storage.get(this.SESSION_KEY);
607
+ const now = Date.now();
608
+ if (storedSession && this.isSessionValid(storedSession, now)) {
609
+ // Restore existing session
610
+ this.sessionData = storedSession;
611
+ this.sessionId = storedSession.id;
612
+ this.lastActivity = now;
613
+ }
614
+ else {
615
+ // Create new session
616
+ this.createNewSession();
617
+ }
618
+ }
619
+ /**
620
+ * Check if session is still valid
621
+ */
622
+ isSessionValid(session, now) {
623
+ const timeSinceActivity = now - session.lastActivity;
624
+ return timeSinceActivity < this.sessionTimeout && session.isActive;
625
+ }
626
+ /**
627
+ * Create a new session
628
+ */
629
+ createNewSession() {
630
+ const now = Date.now();
631
+ this.sessionId = `sess_${generateUUID()}`;
632
+ this.sessionData = {
633
+ id: this.sessionId,
634
+ startTime: now,
635
+ lastActivity: now,
636
+ pageViews: 0,
637
+ events: 0,
638
+ duration: 0,
639
+ isActive: true
640
+ };
641
+ this.incrementSessionCount();
642
+ this.saveSession();
643
+ return this.sessionId;
644
+ }
645
+ /**
646
+ * Get current session ID
647
+ */
648
+ getSessionId() {
649
+ if (!this.sessionId || !this.isSessionActive()) {
650
+ this.createNewSession();
651
+ }
652
+ return this.sessionId;
653
+ }
654
+ /**
655
+ * Get session data
656
+ */
657
+ getSessionData() {
658
+ return this.sessionData;
659
+ }
660
+ /**
661
+ * Update session activity
662
+ */
663
+ updateActivity(eventType) {
664
+ const now = Date.now();
665
+ // Check if we need a new session
666
+ if (!this.sessionData || !this.isSessionValid(this.sessionData, now)) {
667
+ this.createNewSession();
668
+ return;
669
+ }
670
+ this.lastActivity = now;
671
+ this.sessionData.lastActivity = now;
672
+ this.sessionData.duration = now - this.sessionData.startTime;
673
+ // Update counters
674
+ if (eventType === 'pageview' || eventType === 'page_view') {
675
+ this.sessionData.pageViews++;
676
+ }
677
+ this.sessionData.events++;
678
+ this.saveSession();
679
+ }
680
+ /**
681
+ * Check if session is active
682
+ */
683
+ isSessionActive() {
684
+ if (!this.sessionData)
685
+ return false;
686
+ const now = Date.now();
687
+ return this.isSessionValid(this.sessionData, now);
688
+ }
689
+ /**
690
+ * End the current session
691
+ */
692
+ endSession() {
693
+ if (this.sessionData) {
694
+ this.sessionData.isActive = false;
695
+ this.saveSession();
696
+ }
697
+ this.sessionId = null;
698
+ this.sessionData = null;
699
+ }
700
+ /**
701
+ * Save session to storage
702
+ */
703
+ saveSession() {
704
+ if (this.sessionData) {
705
+ storage.set(this.SESSION_KEY, this.sessionData);
706
+ }
707
+ }
708
+ /**
709
+ * Get session timeout
710
+ */
711
+ getTimeout() {
712
+ return this.sessionTimeout;
713
+ }
714
+ /**
715
+ * Set session timeout
716
+ */
717
+ setTimeout(timeout) {
718
+ this.sessionTimeout = timeout;
719
+ }
720
+ /**
721
+ * Store session attribution
722
+ */
723
+ storeAttribution(attribution) {
724
+ const key = `dl_session_${this.sessionId}_attribution`;
725
+ storage.set(key, Object.assign(Object.assign({}, attribution), { sessionId: this.sessionId, timestamp: Date.now() }));
726
+ }
727
+ /**
728
+ * Get session attribution
729
+ */
730
+ getAttribution() {
731
+ if (!this.sessionId)
732
+ return null;
733
+ const key = `dl_session_${this.sessionId}_attribution`;
734
+ return storage.get(key);
735
+ }
736
+ /**
737
+ * Get session metrics
738
+ */
739
+ getMetrics() {
740
+ if (!this.sessionData)
741
+ return {};
742
+ return {
743
+ session_id: this.sessionId,
744
+ session_duration: this.sessionData.duration,
745
+ session_page_views: this.sessionData.pageViews,
746
+ session_events: this.sessionData.events,
747
+ session_start: this.sessionData.startTime,
748
+ time_since_session_start: Date.now() - this.sessionData.startTime
749
+ };
750
+ }
751
+ /**
752
+ * Setup activity monitor for automatic session timeout
753
+ */
754
+ setupActivityMonitor() {
755
+ // Monitor user activity
756
+ const activityEvents = ['mousedown', 'keydown', 'scroll', 'touchstart'];
757
+ const handleActivity = () => {
758
+ const now = Date.now();
759
+ if (this.sessionData && now - this.lastActivity > 1000) { // Debounce 1 second
760
+ this.updateActivity();
761
+ }
762
+ };
763
+ activityEvents.forEach(event => {
764
+ window.addEventListener(event, handleActivity, { passive: true, capture: true });
765
+ this.activityListeners.push({ event, handler: handleActivity });
766
+ });
767
+ // Check for session timeout periodically
768
+ this.activityCheckInterval = setInterval(() => {
769
+ if (this.sessionData && !this.isSessionActive()) {
770
+ this.createNewSession();
771
+ }
772
+ }, 60000); // Check every minute
773
+ }
774
+ /**
775
+ * Cleanup listeners and timers
776
+ */
777
+ destroy() {
778
+ // Remove activity listeners
779
+ this.activityListeners.forEach(({ event, handler }) => {
780
+ window.removeEventListener(event, handler);
781
+ });
782
+ this.activityListeners = [];
783
+ // Clear interval
784
+ if (this.activityCheckInterval) {
785
+ clearInterval(this.activityCheckInterval);
786
+ this.activityCheckInterval = null;
787
+ }
788
+ }
789
+ /**
790
+ * Get session number (count of sessions)
791
+ */
792
+ getSessionNumber() {
793
+ const count = storage.get('dl_session_count', 0);
794
+ return count + 1;
795
+ }
796
+ /**
797
+ * Increment session count
798
+ */
799
+ incrementSessionCount() {
800
+ const count = storage.get('dl_session_count', 0);
801
+ storage.set('dl_session_count', count + 1);
802
+ }
803
+ }
804
+
805
+ /**
806
+ * Attribution Tracking Module
807
+ * Handles UTM parameters, click IDs, and customer journey
808
+ */
809
+ class AttributionManager {
810
+ constructor(options = {}) {
811
+ this.UTM_PARAMS = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content'];
812
+ this.CLICK_IDS = ['fbclid', 'gclid', 'ttclid', 'msclkid', 'twclid', 'li_fat_id', 'sclid', 'dclid', 'epik'];
813
+ this.attributionWindow = options.attributionWindow || 30 * 24 * 60 * 60 * 1000; // 30 days
814
+ this.trackedParams = options.trackedParams || [];
815
+ }
816
+ /**
817
+ * Capture current attribution from URL
818
+ */
819
+ captureAttribution() {
820
+ const params = getAllQueryParams();
821
+ const attribution = {
822
+ timestamp: Date.now()
823
+ };
824
+ // Capture UTM parameters
825
+ for (const utm of this.UTM_PARAMS) {
826
+ const value = params[utm];
827
+ if (value) {
828
+ const key = utm.replace('utm_', '');
829
+ attribution[key] = value;
830
+ }
831
+ }
832
+ // Capture click IDs
833
+ for (const clickId of this.CLICK_IDS) {
834
+ const value = params[clickId];
835
+ if (value) {
836
+ attribution.clickId = value;
837
+ attribution.clickIdType = clickId;
838
+ break; // Use first found click ID
839
+ }
840
+ }
841
+ // Capture custom tracked parameters
842
+ for (const param of this.trackedParams) {
843
+ const value = params[param];
844
+ if (value) {
845
+ attribution[param] = value;
846
+ }
847
+ }
848
+ // Capture referrer
849
+ if (document.referrer) {
850
+ attribution.referrer = document.referrer;
851
+ attribution.referrerHost = this.extractHostname(document.referrer);
852
+ }
853
+ // Capture landing page
854
+ attribution.landingPage = window.location.href;
855
+ attribution.landingPath = window.location.pathname;
856
+ // Determine source if not explicitly set
857
+ if (!attribution.source) {
858
+ attribution.source = this.determineSource(attribution);
859
+ }
860
+ // Determine medium if not explicitly set
861
+ if (!attribution.medium) {
862
+ attribution.medium = this.determineMedium(attribution);
863
+ }
864
+ return attribution;
865
+ }
866
+ /**
867
+ * Store first touch attribution
868
+ */
869
+ storeFirstTouch(attribution) {
870
+ const existing = storage.get('dl_first_touch');
871
+ if (!existing) {
872
+ storage.set('dl_first_touch', Object.assign(Object.assign({}, attribution), { timestamp: Date.now() }));
873
+ }
874
+ }
875
+ /**
876
+ * Get first touch attribution
877
+ */
878
+ getFirstTouch() {
879
+ return storage.get('dl_first_touch');
880
+ }
881
+ /**
882
+ * Store last touch attribution
883
+ */
884
+ storeLastTouch(attribution) {
885
+ storage.set('dl_last_touch', Object.assign(Object.assign({}, attribution), { timestamp: Date.now() }));
886
+ }
887
+ /**
888
+ * Get last touch attribution
889
+ */
890
+ getLastTouch() {
891
+ return storage.get('dl_last_touch');
892
+ }
893
+ /**
894
+ * Add touchpoint to customer journey
895
+ */
896
+ addTouchpoint(sessionId, attribution) {
897
+ const journey = this.getJourney();
898
+ const touchpoint = {
899
+ timestamp: Date.now(),
900
+ sessionId,
901
+ source: attribution.source,
902
+ medium: attribution.medium,
903
+ campaign: attribution.campaign
904
+ };
905
+ journey.push(touchpoint);
906
+ // Keep last 30 touchpoints
907
+ if (journey.length > 30) {
908
+ journey.shift();
909
+ }
910
+ storage.set('dl_journey', journey);
911
+ }
912
+ /**
913
+ * Get customer journey
914
+ */
915
+ getJourney() {
916
+ return storage.get('dl_journey', []);
917
+ }
918
+ /**
919
+ * Get attribution data for event
920
+ */
921
+ getAttributionData() {
922
+ const firstTouch = this.getFirstTouch();
923
+ const lastTouch = this.getLastTouch();
924
+ const journey = this.getJourney();
925
+ const current = this.captureAttribution();
926
+ // Update first/last touch if needed
927
+ if (!firstTouch && Object.keys(current).length > 1) {
928
+ this.storeFirstTouch(current);
929
+ }
930
+ if (Object.keys(current).length > 1) {
931
+ this.storeLastTouch(current);
932
+ }
933
+ return Object.assign(Object.assign({}, current), {
934
+ // First touch (with snake_case aliases)
935
+ first_touch_source: firstTouch === null || firstTouch === void 0 ? void 0 : firstTouch.source, first_touch_medium: firstTouch === null || firstTouch === void 0 ? void 0 : firstTouch.medium, first_touch_campaign: firstTouch === null || firstTouch === void 0 ? void 0 : firstTouch.campaign, first_touch_timestamp: firstTouch === null || firstTouch === void 0 ? void 0 : firstTouch.timestamp, firstTouchSource: firstTouch === null || firstTouch === void 0 ? void 0 : firstTouch.source, firstTouchMedium: firstTouch === null || firstTouch === void 0 ? void 0 : firstTouch.medium, firstTouchCampaign: firstTouch === null || firstTouch === void 0 ? void 0 : firstTouch.campaign,
936
+ // Last touch (with snake_case aliases)
937
+ last_touch_source: lastTouch === null || lastTouch === void 0 ? void 0 : lastTouch.source, last_touch_medium: lastTouch === null || lastTouch === void 0 ? void 0 : lastTouch.medium, last_touch_campaign: lastTouch === null || lastTouch === void 0 ? void 0 : lastTouch.campaign, last_touch_timestamp: lastTouch === null || lastTouch === void 0 ? void 0 : lastTouch.timestamp, lastTouchSource: lastTouch === null || lastTouch === void 0 ? void 0 : lastTouch.source, lastTouchMedium: lastTouch === null || lastTouch === void 0 ? void 0 : lastTouch.medium, lastTouchCampaign: lastTouch === null || lastTouch === void 0 ? void 0 : lastTouch.campaign,
938
+ // Journey metrics
939
+ touchpoint_count: journey.length, touchpointCount: journey.length, days_since_first_touch: (firstTouch === null || firstTouch === void 0 ? void 0 : firstTouch.timestamp)
940
+ ? Math.floor((Date.now() - firstTouch.timestamp) / 86400000)
941
+ : 0, daysSinceFirstTouch: (firstTouch === null || firstTouch === void 0 ? void 0 : firstTouch.timestamp)
942
+ ? Math.floor((Date.now() - firstTouch.timestamp) / 86400000)
943
+ : 0 });
944
+ }
945
+ /**
946
+ * Determine source from attribution data
947
+ */
948
+ determineSource(attribution) {
949
+ // If we have a click ID, determine source from that
950
+ if (attribution.clickIdType) {
951
+ const clickIdSources = {
952
+ fbclid: 'facebook',
953
+ gclid: 'google',
954
+ ttclid: 'tiktok',
955
+ msclkid: 'bing',
956
+ twclid: 'twitter',
957
+ li_fat_id: 'linkedin',
958
+ sclid: 'snapchat',
959
+ dclid: 'doubleclick',
960
+ epik: 'pinterest'
961
+ };
962
+ return clickIdSources[attribution.clickIdType] || 'paid';
963
+ }
964
+ // Check referrer
965
+ if (attribution.referrerHost) {
966
+ const host = attribution.referrerHost.toLowerCase();
967
+ // Social sources
968
+ if (host.includes('facebook.com') || host.includes('fb.com'))
969
+ return 'facebook';
970
+ if (host.includes('twitter.com') || host.includes('t.co') || host.includes('x.com'))
971
+ return 'twitter';
972
+ if (host.includes('linkedin.com') || host.includes('lnkd.in'))
973
+ return 'linkedin';
974
+ if (host.includes('instagram.com'))
975
+ return 'instagram';
976
+ if (host.includes('youtube.com') || host.includes('youtu.be'))
977
+ return 'youtube';
978
+ if (host.includes('tiktok.com'))
979
+ return 'tiktok';
980
+ if (host.includes('reddit.com'))
981
+ return 'reddit';
982
+ if (host.includes('pinterest.com'))
983
+ return 'pinterest';
984
+ // Search engines
985
+ if (host.includes('google.'))
986
+ return 'google';
987
+ if (host.includes('bing.com'))
988
+ return 'bing';
989
+ if (host.includes('yahoo.com'))
990
+ return 'yahoo';
991
+ if (host.includes('duckduckgo.com'))
992
+ return 'duckduckgo';
993
+ if (host.includes('baidu.com'))
994
+ return 'baidu';
995
+ return 'referral';
996
+ }
997
+ return 'direct';
998
+ }
999
+ /**
1000
+ * Determine medium from attribution data
1001
+ */
1002
+ determineMedium(attribution) {
1003
+ // If we have a click ID, it's paid
1004
+ if (attribution.clickId) {
1005
+ return 'cpc'; // Cost per click
1006
+ }
1007
+ // Check source
1008
+ const source = attribution.source;
1009
+ if (!source || source === 'direct') {
1010
+ return 'none';
1011
+ }
1012
+ // Social sources typically organic unless paid
1013
+ const socialSources = ['facebook', 'twitter', 'linkedin', 'instagram', 'youtube', 'tiktok', 'reddit', 'pinterest'];
1014
+ if (socialSources.includes(source)) {
1015
+ return 'social';
1016
+ }
1017
+ // Search engines
1018
+ const searchSources = ['google', 'bing', 'yahoo', 'duckduckgo', 'baidu'];
1019
+ if (searchSources.includes(source)) {
1020
+ return 'organic';
1021
+ }
1022
+ return 'referral';
1023
+ }
1024
+ /**
1025
+ * Extract hostname from URL
1026
+ */
1027
+ extractHostname(url) {
1028
+ try {
1029
+ return new URL(url).hostname;
1030
+ }
1031
+ catch (_a) {
1032
+ return '';
1033
+ }
1034
+ }
1035
+ /**
1036
+ * Check if attribution has expired
1037
+ */
1038
+ isAttributionExpired(attribution) {
1039
+ if (!attribution.timestamp)
1040
+ return true;
1041
+ return Date.now() - attribution.timestamp > this.attributionWindow;
1042
+ }
1043
+ /**
1044
+ * Clear expired attribution
1045
+ */
1046
+ clearExpiredAttribution() {
1047
+ const firstTouch = this.getFirstTouch();
1048
+ const lastTouch = this.getLastTouch();
1049
+ if (firstTouch && this.isAttributionExpired(firstTouch)) {
1050
+ storage.remove('dl_first_touch');
1051
+ }
1052
+ if (lastTouch && this.isAttributionExpired(lastTouch)) {
1053
+ storage.remove('dl_last_touch');
1054
+ }
1055
+ }
1056
+ }
1057
+
1058
+ /**
1059
+ * Event Queue and Batching Module
1060
+ */
1061
+ // Default critical events that bypass batching
1062
+ const DEFAULT_CRITICAL_EVENTS = ['purchase', 'signup', 'subscribe', 'lead', 'conversion'];
1063
+ // Default high priority events that use faster batching
1064
+ const DEFAULT_HIGH_PRIORITY_EVENTS = ['add_to_cart', 'begin_checkout', 'view_item', 'search'];
1065
+ class EventQueue {
1066
+ constructor(config) {
1067
+ this.queue = [];
1068
+ this.offlineQueue = [];
1069
+ this.batchTimer = null;
1070
+ this.periodicFlushInterval = null;
1071
+ this.flushPromise = null;
1072
+ this.recentEventIds = new Set();
1073
+ this.MAX_RECENT_EVENT_IDS = 1000;
1074
+ this.OFFLINE_QUEUE_KEY = 'dl_offline_queue';
1075
+ this.currentEndpointIndex = 0;
1076
+ this.config = {
1077
+ batchSize: config.batchSize || 10,
1078
+ flushInterval: config.flushInterval || 5000,
1079
+ maxRetries: config.maxRetries || 5,
1080
+ retryDelay: config.retryDelay || 1000,
1081
+ endpoint: config.endpoint || 'https://ingest.datalyr.com',
1082
+ fallbackEndpoints: config.fallbackEndpoints || [],
1083
+ workspaceId: config.workspaceId,
1084
+ debug: config.debug || false,
1085
+ criticalEvents: config.criticalEvents || DEFAULT_CRITICAL_EVENTS,
1086
+ highPriorityEvents: config.highPriorityEvents || DEFAULT_HIGH_PRIORITY_EVENTS,
1087
+ maxOfflineQueueSize: config.maxOfflineQueueSize || 100
1088
+ };
1089
+ this.networkStatus = {
1090
+ isOnline: navigator.onLine !== false,
1091
+ lastOfflineAt: null,
1092
+ lastOnlineAt: null
1093
+ };
1094
+ this.loadOfflineQueue();
1095
+ this.setupNetworkListeners();
1096
+ this.startPeriodicFlush();
1097
+ }
1098
+ /**
1099
+ * Add event to queue
1100
+ */
1101
+ enqueue(event) {
1102
+ const eventName = event.eventName;
1103
+ // Check for duplicates (within 500ms window)
1104
+ if (this.isDuplicateEvent(event)) {
1105
+ this.log('Duplicate event suppressed:', eventName);
1106
+ return;
1107
+ }
1108
+ // Critical events bypass queue
1109
+ if (this.config.criticalEvents.includes(eventName)) {
1110
+ this.log('Critical event, sending immediately:', eventName);
1111
+ this.sendBatch([event]);
1112
+ return;
1113
+ }
1114
+ // Add to queue
1115
+ this.queue.push(event);
1116
+ this.log('Event queued:', eventName);
1117
+ // Check if we should flush
1118
+ if (this.shouldFlush(eventName)) {
1119
+ this.flush();
1120
+ }
1121
+ }
1122
+ /**
1123
+ * Check if event is duplicate
1124
+ */
1125
+ isDuplicateEvent(event) {
1126
+ const eventId = event.eventId;
1127
+ if (this.recentEventIds.has(eventId)) {
1128
+ return true;
1129
+ }
1130
+ this.recentEventIds.add(eventId);
1131
+ // Clean up old event IDs
1132
+ if (this.recentEventIds.size > this.MAX_RECENT_EVENT_IDS) {
1133
+ const toDelete = this.recentEventIds.size - this.MAX_RECENT_EVENT_IDS;
1134
+ const iterator = this.recentEventIds.values();
1135
+ for (let i = 0; i < toDelete; i++) {
1136
+ this.recentEventIds.delete(iterator.next().value);
1137
+ }
1138
+ }
1139
+ return false;
1140
+ }
1141
+ /**
1142
+ * Check if we should flush the queue
1143
+ */
1144
+ shouldFlush(eventName) {
1145
+ // Check queue size
1146
+ if (this.queue.length >= this.config.batchSize) {
1147
+ return true;
1148
+ }
1149
+ // Check for high priority events
1150
+ if (eventName && this.config.highPriorityEvents.includes(eventName)) {
1151
+ // Use faster flush for high priority
1152
+ if (this.batchTimer) {
1153
+ clearTimeout(this.batchTimer);
1154
+ }
1155
+ this.batchTimer = setTimeout(() => this.flush(), 1000);
1156
+ return false;
1157
+ }
1158
+ // Set normal batch timer if not already set
1159
+ if (!this.batchTimer) {
1160
+ this.batchTimer = setTimeout(() => this.flush(), this.config.flushInterval);
1161
+ }
1162
+ return false;
1163
+ }
1164
+ /**
1165
+ * Flush the queue
1166
+ */
1167
+ flush() {
1168
+ return __awaiter(this, void 0, void 0, function* () {
1169
+ // Prevent concurrent flushes
1170
+ if (this.flushPromise) {
1171
+ return this.flushPromise;
1172
+ }
1173
+ this.flushPromise = this._flush();
1174
+ yield this.flushPromise;
1175
+ this.flushPromise = null;
1176
+ });
1177
+ }
1178
+ /**
1179
+ * Internal flush implementation
1180
+ */
1181
+ _flush() {
1182
+ return __awaiter(this, void 0, void 0, function* () {
1183
+ // Clear timer
1184
+ if (this.batchTimer) {
1185
+ clearTimeout(this.batchTimer);
1186
+ this.batchTimer = null;
1187
+ }
1188
+ // Check if we have events
1189
+ if (this.queue.length === 0) {
1190
+ return;
1191
+ }
1192
+ // Check network status
1193
+ if (!this.networkStatus.isOnline) {
1194
+ this.log('Network offline, queuing events');
1195
+ this.moveToOfflineQueue();
1196
+ return;
1197
+ }
1198
+ // Get events to send
1199
+ const events = this.queue.splice(0, this.config.batchSize);
1200
+ try {
1201
+ yield this.sendBatch(events);
1202
+ }
1203
+ catch (error) {
1204
+ this.log('Failed to send batch:', error);
1205
+ // Move to offline queue for retry
1206
+ this.offlineQueue.push(...events);
1207
+ this.saveOfflineQueue();
1208
+ }
1209
+ });
1210
+ }
1211
+ /**
1212
+ * Send batch of events
1213
+ */
1214
+ sendBatch(events_1) {
1215
+ return __awaiter(this, arguments, void 0, function* (events, retries = 0, endpointIndex = 0) {
1216
+ const batchPayload = {
1217
+ events,
1218
+ batchId: generateUUID(),
1219
+ timestamp: new Date().toISOString()
1220
+ };
1221
+ // Get current endpoint (main or fallback)
1222
+ const endpoints = [this.config.endpoint, ...this.config.fallbackEndpoints];
1223
+ const currentEndpoint = endpoints[endpointIndex] || this.config.endpoint;
1224
+ try {
1225
+ const response = yield fetch(currentEndpoint, {
1226
+ method: 'POST',
1227
+ headers: {
1228
+ 'Content-Type': 'application/json',
1229
+ 'X-Batch-Size': events.length.toString()
1230
+ },
1231
+ body: JSON.stringify(batchPayload),
1232
+ keepalive: true
1233
+ });
1234
+ if (!response.ok) {
1235
+ // Handle rate limiting
1236
+ if (response.status === 429) {
1237
+ const retryAfter = parseInt(response.headers.get('Retry-After') || '60');
1238
+ this.log(`Rate limited, retrying after ${retryAfter}s`);
1239
+ setTimeout(() => {
1240
+ this.queue.unshift(...events);
1241
+ }, retryAfter * 1000);
1242
+ return;
1243
+ }
1244
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
1245
+ }
1246
+ this.log(`Batch sent successfully to ${currentEndpoint}: ${events.length} events`);
1247
+ this.currentEndpointIndex = 0; // Reset to primary on success
1248
+ }
1249
+ catch (error) {
1250
+ // Try next fallback endpoint if available
1251
+ if (endpointIndex < endpoints.length - 1) {
1252
+ this.log(`Failed on ${currentEndpoint}, trying fallback ${endpointIndex + 1}`);
1253
+ return this.sendBatch(events, 0, endpointIndex + 1);
1254
+ }
1255
+ // Retry with exponential backoff on current endpoint
1256
+ if (retries < this.config.maxRetries) {
1257
+ const delay = calculateRetryDelay(retries, this.config.retryDelay);
1258
+ this.log(`Retrying batch in ${delay}ms (attempt ${retries + 1}/${this.config.maxRetries})`);
1259
+ yield new Promise(resolve => setTimeout(resolve, delay));
1260
+ return this.sendBatch(events, retries + 1, endpointIndex);
1261
+ }
1262
+ throw error;
1263
+ }
1264
+ });
1265
+ }
1266
+ /**
1267
+ * Setup network status listeners
1268
+ */
1269
+ setupNetworkListeners() {
1270
+ window.addEventListener('online', () => {
1271
+ this.networkStatus.isOnline = true;
1272
+ this.networkStatus.lastOnlineAt = Date.now();
1273
+ this.log('Network connection restored');
1274
+ // Process offline queue
1275
+ setTimeout(() => this.processOfflineQueue(), 1000);
1276
+ });
1277
+ window.addEventListener('offline', () => {
1278
+ this.networkStatus.isOnline = false;
1279
+ this.networkStatus.lastOfflineAt = Date.now();
1280
+ this.log('Network connection lost');
1281
+ });
1282
+ }
1283
+ /**
1284
+ * Start periodic flush timer
1285
+ */
1286
+ startPeriodicFlush() {
1287
+ this.periodicFlushInterval = setInterval(() => {
1288
+ if (this.queue.length > 0) {
1289
+ this.flush();
1290
+ }
1291
+ }, this.config.flushInterval);
1292
+ }
1293
+ /**
1294
+ * Stop periodic flush timer
1295
+ */
1296
+ stopPeriodicFlush() {
1297
+ if (this.periodicFlushInterval) {
1298
+ clearInterval(this.periodicFlushInterval);
1299
+ this.periodicFlushInterval = null;
1300
+ }
1301
+ }
1302
+ /**
1303
+ * Move events to offline queue
1304
+ */
1305
+ moveToOfflineQueue() {
1306
+ this.offlineQueue.push(...this.queue);
1307
+ this.queue = [];
1308
+ this.saveOfflineQueue();
1309
+ }
1310
+ /**
1311
+ * Load offline queue from storage
1312
+ */
1313
+ loadOfflineQueue() {
1314
+ const stored = storage.get(this.OFFLINE_QUEUE_KEY, []);
1315
+ if (Array.isArray(stored)) {
1316
+ this.offlineQueue = stored;
1317
+ this.log(`Loaded ${this.offlineQueue.length} offline events`);
1318
+ }
1319
+ }
1320
+ /**
1321
+ * Save offline queue to storage
1322
+ */
1323
+ saveOfflineQueue() {
1324
+ // Keep max events based on config
1325
+ const toSave = this.offlineQueue.slice(-this.config.maxOfflineQueueSize);
1326
+ storage.set(this.OFFLINE_QUEUE_KEY, toSave);
1327
+ }
1328
+ /**
1329
+ * Process offline queue
1330
+ */
1331
+ processOfflineQueue() {
1332
+ return __awaiter(this, void 0, void 0, function* () {
1333
+ if (this.offlineQueue.length === 0)
1334
+ return;
1335
+ this.log(`Processing ${this.offlineQueue.length} offline events`);
1336
+ while (this.offlineQueue.length > 0) {
1337
+ const batch = this.offlineQueue.splice(0, this.config.batchSize);
1338
+ try {
1339
+ yield this.sendBatch(batch);
1340
+ this.saveOfflineQueue();
1341
+ }
1342
+ catch (error) {
1343
+ this.log('Failed to send offline batch:', error);
1344
+ // Put back in queue
1345
+ this.offlineQueue.unshift(...batch);
1346
+ this.saveOfflineQueue();
1347
+ break;
1348
+ }
1349
+ }
1350
+ if (this.offlineQueue.length === 0) {
1351
+ storage.remove(this.OFFLINE_QUEUE_KEY);
1352
+ }
1353
+ });
1354
+ }
1355
+ /**
1356
+ * Get queue size
1357
+ */
1358
+ getQueueSize() {
1359
+ return this.queue.length;
1360
+ }
1361
+ /**
1362
+ * Get offline queue size
1363
+ */
1364
+ getOfflineQueueSize() {
1365
+ return this.offlineQueue.length;
1366
+ }
1367
+ /**
1368
+ * Get network status
1369
+ */
1370
+ getNetworkStatus() {
1371
+ return Object.assign({}, this.networkStatus);
1372
+ }
1373
+ /**
1374
+ * Force flush (for page unload)
1375
+ */
1376
+ forceFlush() {
1377
+ return __awaiter(this, void 0, void 0, function* () {
1378
+ // Try sendBeacon first for reliability
1379
+ if (navigator.sendBeacon && this.queue.length > 0) {
1380
+ const batchPayload = {
1381
+ events: this.queue,
1382
+ batchId: generateUUID(),
1383
+ timestamp: new Date().toISOString()
1384
+ };
1385
+ const blob = new Blob([JSON.stringify(batchPayload)], {
1386
+ type: 'application/json'
1387
+ });
1388
+ const success = navigator.sendBeacon(this.config.endpoint, blob);
1389
+ if (success) {
1390
+ this.log('Events sent via sendBeacon');
1391
+ this.queue = [];
1392
+ return;
1393
+ }
1394
+ }
1395
+ // Fallback to regular flush
1396
+ yield this.flush();
1397
+ });
1398
+ }
1399
+ /**
1400
+ * Clear queue
1401
+ */
1402
+ clear() {
1403
+ this.queue = [];
1404
+ if (this.batchTimer) {
1405
+ clearTimeout(this.batchTimer);
1406
+ this.batchTimer = null;
1407
+ }
1408
+ }
1409
+ /**
1410
+ * Debug logging
1411
+ */
1412
+ log(...args) {
1413
+ if (this.config.debug) {
1414
+ console.log('[Datalyr Queue]', ...args);
1415
+ }
1416
+ }
1417
+ /**
1418
+ * Cleanup resources
1419
+ */
1420
+ destroy() {
1421
+ this.stopPeriodicFlush();
1422
+ if (this.batchTimer) {
1423
+ clearTimeout(this.batchTimer);
1424
+ this.batchTimer = null;
1425
+ }
1426
+ // Save any remaining events to offline queue
1427
+ if (this.queue.length > 0) {
1428
+ this.moveToOfflineQueue();
1429
+ }
1430
+ }
1431
+ }
1432
+
1433
+ /**
1434
+ * Fingerprint Collection Module
1435
+ * Collects device fingerprint data for identification
1436
+ * Privacy-conscious: respects privacy mode settings
1437
+ */
1438
+ class FingerprintCollector {
1439
+ constructor(options = {}) {
1440
+ this.heavyFingerprintDone = false;
1441
+ this.fingerprintCache = {};
1442
+ this.privacyMode = options.privacyMode || 'standard';
1443
+ this.enableFingerprinting = options.enableFingerprinting !== false;
1444
+ }
1445
+ /**
1446
+ * Collect fingerprint data
1447
+ * Returns minimal data in strict mode, full data in standard mode
1448
+ */
1449
+ collect() {
1450
+ // Strict privacy mode - minimal fingerprinting only
1451
+ if (this.privacyMode === 'strict' || !this.enableFingerprinting) {
1452
+ return this.collectMinimal();
1453
+ }
1454
+ // Standard mode - collect more data
1455
+ return this.collectStandard();
1456
+ }
1457
+ /**
1458
+ * Collect minimal fingerprint data (privacy-friendly)
1459
+ */
1460
+ collectMinimal() {
1461
+ return {
1462
+ timezone: this.getTimezone(),
1463
+ language: navigator.language || null,
1464
+ platform: navigator.platform || null,
1465
+ canvasEnabled: false,
1466
+ localStorageAvailable: this.testStorage('localStorage'),
1467
+ sessionStorageAvailable: this.testStorage('sessionStorage')
1468
+ };
1469
+ }
1470
+ /**
1471
+ * Collect standard fingerprint data
1472
+ */
1473
+ collectStandard() {
1474
+ const fingerprint = {};
1475
+ try {
1476
+ // Basic browser data
1477
+ fingerprint.userAgent = navigator.userAgent || null;
1478
+ // User-Agent Client Hints (modern browsers)
1479
+ if ('userAgentData' in navigator) {
1480
+ const uaData = navigator.userAgentData;
1481
+ fingerprint.userAgentData = {
1482
+ brands: uaData.brands || [],
1483
+ mobile: uaData.mobile || false,
1484
+ platform: uaData.platform || null
1485
+ };
1486
+ }
1487
+ // Language settings
1488
+ fingerprint.language = navigator.language || null;
1489
+ fingerprint.languages = navigator.languages ?
1490
+ navigator.languages.slice(0, 2) : null; // Limit to 2 for privacy
1491
+ // Platform and browser features
1492
+ fingerprint.platform = navigator.platform || null;
1493
+ fingerprint.cookieEnabled = navigator.cookieEnabled || null;
1494
+ fingerprint.doNotTrack = navigator.doNotTrack || null;
1495
+ // Hardware (coarsened for privacy)
1496
+ fingerprint.hardwareConcurrency = this.coarsenHardwareConcurrency();
1497
+ fingerprint.deviceMemory = this.coarsenDeviceMemory();
1498
+ fingerprint.maxTouchPoints = navigator.maxTouchPoints > 0 ? 'touch' : 'no-touch';
1499
+ // Screen (coarsened)
1500
+ fingerprint.screenResolution = this.getScreenResolution();
1501
+ fingerprint.colorDepth = screen.colorDepth || null;
1502
+ fingerprint.pixelRatio = this.coarsenPixelRatio();
1503
+ // Timezone
1504
+ fingerprint.timezone = this.getTimezone();
1505
+ fingerprint.timezoneOffset = this.coarsenTimezoneOffset();
1506
+ // Canvas fingerprinting disabled for privacy
1507
+ fingerprint.canvasEnabled = false;
1508
+ // Plugins count only (not details for privacy)
1509
+ fingerprint.pluginsCount = navigator.plugins ? navigator.plugins.length : null;
1510
+ // Storage availability
1511
+ fingerprint.localStorageAvailable = this.testStorage('localStorage');
1512
+ fingerprint.sessionStorageAvailable = this.testStorage('sessionStorage');
1513
+ fingerprint.indexedDBAvailable = this.testIndexedDB();
1514
+ // Add cached heavy fingerprint data if available
1515
+ if (this.heavyFingerprintDone) {
1516
+ Object.assign(fingerprint, this.fingerprintCache);
1517
+ }
1518
+ }
1519
+ catch (e) {
1520
+ console.warn('[Datalyr] Error collecting fingerprint:', e);
1521
+ }
1522
+ return fingerprint;
1523
+ }
1524
+ /**
1525
+ * Collect heavy fingerprint data (WebGL, Audio)
1526
+ * Called lazily on first event to improve page load performance
1527
+ */
1528
+ collectHeavyFingerprint() {
1529
+ return __awaiter(this, void 0, void 0, function* () {
1530
+ if (this.heavyFingerprintDone || this.privacyMode === 'strict') {
1531
+ return;
1532
+ }
1533
+ try {
1534
+ // WebGL fingerprinting
1535
+ const webglData = this.getWebGLFingerprint();
1536
+ if (webglData) {
1537
+ this.fingerprintCache.webglVendor = webglData.vendor;
1538
+ this.fingerprintCache.webglRenderer = webglData.renderer;
1539
+ }
1540
+ // Audio fingerprinting
1541
+ const audioData = yield this.getAudioFingerprint();
1542
+ if (audioData) {
1543
+ this.fingerprintCache.audioSampleRate = audioData.sampleRate;
1544
+ this.fingerprintCache.audioState = audioData.state;
1545
+ this.fingerprintCache.audioMaxChannels = audioData.maxChannels;
1546
+ }
1547
+ this.heavyFingerprintDone = true;
1548
+ }
1549
+ catch (e) {
1550
+ console.warn('[Datalyr] Heavy fingerprinting failed:', e);
1551
+ }
1552
+ });
1553
+ }
1554
+ /**
1555
+ * Get WebGL fingerprint
1556
+ */
1557
+ getWebGLFingerprint() {
1558
+ try {
1559
+ const canvas = document.createElement('canvas');
1560
+ const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
1561
+ if (!gl)
1562
+ return null;
1563
+ const debugInfo = gl.getExtension('WEBGL_debug_renderer_info');
1564
+ if (!debugInfo) {
1565
+ return {
1566
+ vendor: gl.getParameter(gl.VENDOR) || 'unknown',
1567
+ renderer: gl.getParameter(gl.RENDERER) || 'unknown'
1568
+ };
1569
+ }
1570
+ return {
1571
+ vendor: gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL) || 'unknown',
1572
+ renderer: gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL) || 'unknown'
1573
+ };
1574
+ }
1575
+ catch (_a) {
1576
+ return null;
1577
+ }
1578
+ }
1579
+ /**
1580
+ * Get audio fingerprint
1581
+ */
1582
+ getAudioFingerprint() {
1583
+ return __awaiter(this, void 0, void 0, function* () {
1584
+ var _a;
1585
+ try {
1586
+ const AudioContext = window.AudioContext || window.webkitAudioContext;
1587
+ if (!AudioContext)
1588
+ return null;
1589
+ const audioCtx = new AudioContext();
1590
+ const result = {
1591
+ sampleRate: audioCtx.sampleRate || null,
1592
+ state: audioCtx.state || null,
1593
+ maxChannels: ((_a = audioCtx.destination) === null || _a === void 0 ? void 0 : _a.maxChannelCount) || null
1594
+ };
1595
+ // Close context to free resources
1596
+ if (audioCtx.close) {
1597
+ yield audioCtx.close();
1598
+ }
1599
+ return result;
1600
+ }
1601
+ catch (_b) {
1602
+ return null;
1603
+ }
1604
+ });
1605
+ }
1606
+ /**
1607
+ * Get timezone
1608
+ */
1609
+ getTimezone() {
1610
+ try {
1611
+ return Intl.DateTimeFormat().resolvedOptions().timeZone || null;
1612
+ }
1613
+ catch (_a) {
1614
+ return null;
1615
+ }
1616
+ }
1617
+ /**
1618
+ * Get screen resolution (coarsened)
1619
+ */
1620
+ getScreenResolution() {
1621
+ try {
1622
+ const width = Math.round(screen.width / 100) * 100;
1623
+ const height = Math.round(screen.height / 100) * 100;
1624
+ return `${width}x${height}`;
1625
+ }
1626
+ catch (_a) {
1627
+ return null;
1628
+ }
1629
+ }
1630
+ /**
1631
+ * Coarsen hardware concurrency for privacy
1632
+ */
1633
+ coarsenHardwareConcurrency() {
1634
+ try {
1635
+ const cores = navigator.hardwareConcurrency;
1636
+ if (!cores)
1637
+ return null;
1638
+ return cores > 8 ? '8+' : cores;
1639
+ }
1640
+ catch (_a) {
1641
+ return null;
1642
+ }
1643
+ }
1644
+ /**
1645
+ * Coarsen device memory for privacy
1646
+ */
1647
+ coarsenDeviceMemory() {
1648
+ try {
1649
+ const memory = navigator.deviceMemory;
1650
+ if (!memory)
1651
+ return null;
1652
+ return memory > 4 ? '4+' : memory;
1653
+ }
1654
+ catch (_a) {
1655
+ return null;
1656
+ }
1657
+ }
1658
+ /**
1659
+ * Coarsen pixel ratio for privacy
1660
+ */
1661
+ coarsenPixelRatio() {
1662
+ try {
1663
+ const ratio = window.devicePixelRatio;
1664
+ if (!ratio)
1665
+ return null;
1666
+ // Round to common values
1667
+ if (ratio <= 1)
1668
+ return '1';
1669
+ if (ratio <= 1.5)
1670
+ return '1.5';
1671
+ if (ratio <= 2)
1672
+ return '2';
1673
+ if (ratio <= 3)
1674
+ return '3';
1675
+ return '3+';
1676
+ }
1677
+ catch (_a) {
1678
+ return null;
1679
+ }
1680
+ }
1681
+ /**
1682
+ * Coarsen timezone offset for privacy
1683
+ */
1684
+ coarsenTimezoneOffset() {
1685
+ try {
1686
+ const offset = new Date().getTimezoneOffset();
1687
+ // Round to nearest 30 minutes
1688
+ return Math.round(offset / 30) * 30;
1689
+ }
1690
+ catch (_a) {
1691
+ return null;
1692
+ }
1693
+ }
1694
+ /**
1695
+ * Test if storage is available
1696
+ */
1697
+ testStorage(type) {
1698
+ try {
1699
+ const storage = window[type];
1700
+ const testKey = '__dl_test__';
1701
+ storage.setItem(testKey, '1');
1702
+ storage.removeItem(testKey);
1703
+ return true;
1704
+ }
1705
+ catch (_a) {
1706
+ return false;
1707
+ }
1708
+ }
1709
+ /**
1710
+ * Test if IndexedDB is available
1711
+ */
1712
+ testIndexedDB() {
1713
+ try {
1714
+ return !!window.indexedDB;
1715
+ }
1716
+ catch (_a) {
1717
+ return false;
1718
+ }
1719
+ }
1720
+ /**
1721
+ * Generate fingerprint hash
1722
+ */
1723
+ generateHash(data) {
1724
+ return __awaiter(this, void 0, void 0, function* () {
1725
+ try {
1726
+ // Sort keys for consistent hashing
1727
+ const sortedData = Object.keys(data)
1728
+ .sort()
1729
+ .reduce((obj, key) => {
1730
+ obj[key] = data[key];
1731
+ return obj;
1732
+ }, {});
1733
+ const str = JSON.stringify(sortedData);
1734
+ // Use Web Crypto API if available
1735
+ if (window.crypto && window.crypto.subtle) {
1736
+ const encoder = new TextEncoder();
1737
+ const data = encoder.encode(str);
1738
+ const hashBuffer = yield crypto.subtle.digest('SHA-256', data);
1739
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
1740
+ return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
1741
+ }
1742
+ // Fallback to simple hash
1743
+ let hash = 0;
1744
+ for (let i = 0; i < str.length; i++) {
1745
+ const char = str.charCodeAt(i);
1746
+ hash = ((hash << 5) - hash) + char;
1747
+ hash = hash & hash; // Convert to 32bit integer
1748
+ }
1749
+ return Math.abs(hash).toString(16);
1750
+ }
1751
+ catch (_a) {
1752
+ return '';
1753
+ }
1754
+ });
1755
+ }
1756
+ }
1757
+
1758
+ /**
1759
+ * Container Script Manager
1760
+ * Loads and manages third-party tracking scripts and pixels
1761
+ */
1762
+ class ContainerManager {
1763
+ constructor(options) {
1764
+ this.scripts = [];
1765
+ this.loadedScripts = new Set();
1766
+ this.sessionLoadedScripts = new Set();
1767
+ this.pixels = null;
1768
+ this.initialized = false;
1769
+ this.workspaceId = options.workspaceId;
1770
+ // Container scripts always use the app endpoint, not ingest
1771
+ this.endpoint = this.extractAppEndpoint(options.endpoint);
1772
+ this.debug = options.debug || false;
1773
+ // Load session scripts from storage
1774
+ const sessionScripts = storage.get('dl_session_scripts', []);
1775
+ this.sessionLoadedScripts = new Set(sessionScripts);
1776
+ }
1777
+ /**
1778
+ * Initialize container and load scripts
1779
+ */
1780
+ init() {
1781
+ return __awaiter(this, void 0, void 0, function* () {
1782
+ if (this.initialized)
1783
+ return;
1784
+ try {
1785
+ // Fetch container configuration
1786
+ const response = yield fetch(`${this.endpoint}/container-scripts`, {
1787
+ method: 'POST',
1788
+ headers: {
1789
+ 'Content-Type': 'application/json',
1790
+ 'X-Container-Version': '1.0'
1791
+ },
1792
+ body: JSON.stringify({
1793
+ workspaceId: this.workspaceId
1794
+ })
1795
+ });
1796
+ if (!response.ok) {
1797
+ throw new Error(`Failed to fetch container scripts: ${response.status}`);
1798
+ }
1799
+ const data = yield response.json();
1800
+ // Store scripts and pixels
1801
+ this.scripts = data.scripts || [];
1802
+ this.pixels = data.pixels || null;
1803
+ // Initialize pixels if configured
1804
+ if (this.pixels) {
1805
+ this.initializePixels();
1806
+ }
1807
+ // Load scripts based on trigger
1808
+ this.loadScriptsByTrigger('page_load');
1809
+ // Setup DOM ready listener
1810
+ if (document.readyState === 'loading') {
1811
+ document.addEventListener('DOMContentLoaded', () => {
1812
+ this.loadScriptsByTrigger('dom_ready');
1813
+ });
1814
+ }
1815
+ else {
1816
+ this.loadScriptsByTrigger('dom_ready');
1817
+ }
1818
+ // Setup window load listener
1819
+ window.addEventListener('load', () => {
1820
+ this.loadScriptsByTrigger('window_load');
1821
+ });
1822
+ this.initialized = true;
1823
+ this.log('Container manager initialized with', this.scripts.length, 'scripts');
1824
+ }
1825
+ catch (error) {
1826
+ this.log('Error initializing container:', error);
1827
+ }
1828
+ });
1829
+ }
1830
+ /**
1831
+ * Load scripts by trigger type
1832
+ */
1833
+ loadScriptsByTrigger(trigger) {
1834
+ const scriptsToLoad = this.scripts.filter(script => script.enabled &&
1835
+ script.trigger === trigger &&
1836
+ this.shouldLoadScript(script));
1837
+ scriptsToLoad.forEach(script => this.loadScript(script));
1838
+ }
1839
+ /**
1840
+ * Check if script should be loaded based on frequency and conditions
1841
+ */
1842
+ shouldLoadScript(script) {
1843
+ // Check frequency
1844
+ if (script.frequency === 'once_per_page' && this.loadedScripts.has(script.id)) {
1845
+ return false;
1846
+ }
1847
+ if (script.frequency === 'once_per_session' && this.sessionLoadedScripts.has(script.id)) {
1848
+ return false;
1849
+ }
1850
+ // Check conditions
1851
+ if (script.conditions && script.conditions.length > 0) {
1852
+ return this.evaluateConditions(script.conditions);
1853
+ }
1854
+ return true;
1855
+ }
1856
+ /**
1857
+ * Evaluate script conditions
1858
+ */
1859
+ evaluateConditions(conditions) {
1860
+ return conditions.every(condition => {
1861
+ try {
1862
+ const { type, operator, value } = condition;
1863
+ switch (type) {
1864
+ case 'url_path':
1865
+ return this.evaluateStringCondition(window.location.pathname, operator, value);
1866
+ case 'url_host':
1867
+ return this.evaluateStringCondition(window.location.hostname, operator, value);
1868
+ case 'url_parameter':
1869
+ const params = new URLSearchParams(window.location.search);
1870
+ return this.evaluateStringCondition(params.get(condition.parameter) || '', operator, value);
1871
+ case 'referrer':
1872
+ return this.evaluateStringCondition(document.referrer, operator, value);
1873
+ case 'device_type':
1874
+ const isMobile = /Mobile|Android|iPhone|iPad/i.test(navigator.userAgent);
1875
+ return this.evaluateStringCondition(isMobile ? 'mobile' : 'desktop', operator, value);
1876
+ default:
1877
+ return true;
1878
+ }
1879
+ }
1880
+ catch (_a) {
1881
+ return false;
1882
+ }
1883
+ });
1884
+ }
1885
+ /**
1886
+ * Evaluate string condition
1887
+ */
1888
+ evaluateStringCondition(actual, operator, expected) {
1889
+ switch (operator) {
1890
+ case 'equals':
1891
+ return actual === expected;
1892
+ case 'not_equals':
1893
+ return actual !== expected;
1894
+ case 'contains':
1895
+ return actual.includes(expected);
1896
+ case 'not_contains':
1897
+ return !actual.includes(expected);
1898
+ case 'starts_with':
1899
+ return actual.startsWith(expected);
1900
+ case 'ends_with':
1901
+ return actual.endsWith(expected);
1902
+ case 'matches_regex':
1903
+ try {
1904
+ return new RegExp(expected).test(actual);
1905
+ }
1906
+ catch (_a) {
1907
+ return false;
1908
+ }
1909
+ default:
1910
+ return false;
1911
+ }
1912
+ }
1913
+ /**
1914
+ * Load a single script
1915
+ */
1916
+ loadScript(script) {
1917
+ try {
1918
+ switch (script.type) {
1919
+ case 'inline':
1920
+ this.loadInlineScript(script);
1921
+ break;
1922
+ case 'external':
1923
+ this.loadExternalScript(script);
1924
+ break;
1925
+ case 'pixel':
1926
+ this.loadPixel(script);
1927
+ break;
1928
+ }
1929
+ // Mark as loaded
1930
+ this.loadedScripts.add(script.id);
1931
+ // Update session scripts if needed
1932
+ if (script.frequency === 'once_per_session') {
1933
+ this.sessionLoadedScripts.add(script.id);
1934
+ storage.set('dl_session_scripts', Array.from(this.sessionLoadedScripts));
1935
+ }
1936
+ this.log('Loaded script:', script.name);
1937
+ }
1938
+ catch (error) {
1939
+ this.log('Error loading script:', script.name, error);
1940
+ }
1941
+ }
1942
+ /**
1943
+ * Load inline JavaScript
1944
+ */
1945
+ loadInlineScript(script) {
1946
+ // Basic XSS protection - ensure content doesn't contain obvious malicious patterns
1947
+ if (this.containsMaliciousPatterns(script.content)) {
1948
+ this.log('Blocked potentially malicious inline script:', script.id);
1949
+ return;
1950
+ }
1951
+ const scriptElement = document.createElement('script');
1952
+ scriptElement.textContent = script.content;
1953
+ scriptElement.dataset.datalyrScript = script.id;
1954
+ scriptElement.setAttribute('data-nonce', this.generateNonce());
1955
+ document.head.appendChild(scriptElement);
1956
+ }
1957
+ /**
1958
+ * Load external JavaScript
1959
+ */
1960
+ loadExternalScript(script) {
1961
+ // Validate URL before loading
1962
+ if (!this.isValidScriptUrl(script.content)) {
1963
+ this.log('Blocked invalid script URL:', script.content);
1964
+ return;
1965
+ }
1966
+ const scriptElement = document.createElement('script');
1967
+ scriptElement.src = script.content;
1968
+ scriptElement.dataset.datalyrScript = script.id;
1969
+ // Apply settings
1970
+ if (script.settings) {
1971
+ if (script.settings.async !== false)
1972
+ scriptElement.async = true;
1973
+ if (script.settings.defer)
1974
+ scriptElement.defer = true;
1975
+ if (script.settings.integrity)
1976
+ scriptElement.integrity = script.settings.integrity;
1977
+ if (script.settings.crossorigin)
1978
+ scriptElement.crossOrigin = script.settings.crossorigin;
1979
+ }
1980
+ else {
1981
+ // Default to async for better performance
1982
+ scriptElement.async = true;
1983
+ }
1984
+ document.head.appendChild(scriptElement);
1985
+ }
1986
+ /**
1987
+ * Load tracking pixel
1988
+ */
1989
+ loadPixel(script) {
1990
+ const img = new Image();
1991
+ img.src = script.content;
1992
+ img.style.display = 'none';
1993
+ img.dataset.datalyrPixel = script.id;
1994
+ document.body.appendChild(img);
1995
+ }
1996
+ /**
1997
+ * Initialize third-party pixels (Meta, Google, TikTok)
1998
+ */
1999
+ initializePixels() {
2000
+ var _a, _b, _c;
2001
+ if (!this.pixels)
2002
+ return;
2003
+ // Initialize Meta Pixel
2004
+ if (((_a = this.pixels.meta) === null || _a === void 0 ? void 0 : _a.enabled) && this.pixels.meta.pixel_id) {
2005
+ this.initializeMetaPixel(this.pixels.meta);
2006
+ }
2007
+ // Initialize Google Tag
2008
+ if (((_b = this.pixels.google) === null || _b === void 0 ? void 0 : _b.enabled) && this.pixels.google.tag_id) {
2009
+ this.initializeGoogleTag(this.pixels.google);
2010
+ }
2011
+ // Initialize TikTok Pixel
2012
+ if (((_c = this.pixels.tiktok) === null || _c === void 0 ? void 0 : _c.enabled) && this.pixels.tiktok.pixel_id) {
2013
+ this.initializeTikTokPixel(this.pixels.tiktok);
2014
+ }
2015
+ }
2016
+ /**
2017
+ * Initialize Meta (Facebook) Pixel
2018
+ */
2019
+ initializeMetaPixel(config) {
2020
+ try {
2021
+ // Load Meta Pixel script
2022
+ (function (f, b, e, v, n, t, s) {
2023
+ if (f.fbq)
2024
+ return;
2025
+ n = f.fbq = function () {
2026
+ n.callMethod ? n.callMethod.apply(n, arguments) : n.queue.push(arguments);
2027
+ };
2028
+ if (!f._fbq)
2029
+ f._fbq = n;
2030
+ n.push = n;
2031
+ n.loaded = !0;
2032
+ n.version = '2.0';
2033
+ n.queue = [];
2034
+ t = b.createElement(e);
2035
+ t.async = !0;
2036
+ t.src = v;
2037
+ s = b.getElementsByTagName(e)[0];
2038
+ s.parentNode.insertBefore(t, s);
2039
+ })(window, document, 'script', 'https://connect.facebook.net/en_US/fbevents.js');
2040
+ // Initialize pixel
2041
+ window.fbq('init', config.pixel_id);
2042
+ window.fbq('track', 'PageView');
2043
+ this.log('Meta Pixel initialized:', config.pixel_id);
2044
+ }
2045
+ catch (error) {
2046
+ this.log('Error initializing Meta Pixel:', error);
2047
+ }
2048
+ }
2049
+ /**
2050
+ * Initialize Google Tag
2051
+ */
2052
+ initializeGoogleTag(config) {
2053
+ try {
2054
+ // Load Google Tag script
2055
+ const script = document.createElement('script');
2056
+ script.async = true;
2057
+ script.src = `https://www.googletagmanager.com/gtag/js?id=${config.tag_id}`;
2058
+ document.head.appendChild(script);
2059
+ // Initialize gtag
2060
+ window.dataLayer = window.dataLayer || [];
2061
+ function gtag() {
2062
+ window.dataLayer.push(arguments);
2063
+ }
2064
+ window.gtag = gtag;
2065
+ gtag('js', new Date());
2066
+ gtag('config', config.tag_id, {
2067
+ allow_enhanced_conversions: config.enhanced_conversions !== false
2068
+ });
2069
+ this.log('Google Tag initialized:', config.tag_id);
2070
+ }
2071
+ catch (error) {
2072
+ this.log('Error initializing Google Tag:', error);
2073
+ }
2074
+ }
2075
+ /**
2076
+ * Initialize TikTok Pixel
2077
+ */
2078
+ initializeTikTokPixel(config) {
2079
+ try {
2080
+ // Load TikTok Pixel script
2081
+ (function (w, d, t) {
2082
+ w.TiktokAnalyticsObject = t;
2083
+ var ttq = w[t] = w[t] || [];
2084
+ ttq.methods = ['page', 'track', 'identify', 'instances', 'debug', 'on', 'off', 'once', 'ready', 'alias', 'group', 'enableCookie', 'disableCookie'];
2085
+ ttq.setAndDefer = function (t, e) {
2086
+ t[e] = function () {
2087
+ t.push([e].concat(Array.prototype.slice.call(arguments, 0)));
2088
+ };
2089
+ };
2090
+ for (var i = 0; i < ttq.methods.length; i++)
2091
+ ttq.setAndDefer(ttq, ttq.methods[i]);
2092
+ ttq.instance = function (t) {
2093
+ for (var e = ttq._i[t] || [], n = 0; n < ttq.methods.length; n++)
2094
+ ttq.setAndDefer(e, ttq.methods[n]);
2095
+ return e;
2096
+ };
2097
+ ttq.load = function (e, n) {
2098
+ var i = 'https://analytics.tiktok.com/i18n/pixel/events.js';
2099
+ ttq._i = ttq._i || {};
2100
+ ttq._i[e] = [];
2101
+ ttq._o = ttq._o || {};
2102
+ ttq._o[e] = n || {};
2103
+ var o = document.createElement('script');
2104
+ o.type = 'text/javascript';
2105
+ o.async = true;
2106
+ o.src = i + '?sdkid=' + e + '&lib=' + t;
2107
+ var a = document.getElementsByTagName('script')[0];
2108
+ a.parentNode.insertBefore(o, a);
2109
+ };
2110
+ })(window, document, 'ttq');
2111
+ // Initialize pixel
2112
+ window.ttq.load(config.pixel_id);
2113
+ window.ttq.page();
2114
+ this.log('TikTok Pixel initialized:', config.pixel_id);
2115
+ }
2116
+ catch (error) {
2117
+ this.log('Error initializing TikTok Pixel:', error);
2118
+ }
2119
+ }
2120
+ /**
2121
+ * Track event to all initialized pixels
2122
+ */
2123
+ trackToPixels(eventName, properties = {}) {
2124
+ var _a, _b, _c, _d, _e, _f;
2125
+ // Track to Meta Pixel
2126
+ if (((_b = (_a = this.pixels) === null || _a === void 0 ? void 0 : _a.meta) === null || _b === void 0 ? void 0 : _b.enabled) && window.fbq) {
2127
+ try {
2128
+ window.fbq('track', eventName, properties);
2129
+ }
2130
+ catch (error) {
2131
+ this.log('Error tracking Meta Pixel event:', error);
2132
+ }
2133
+ }
2134
+ // Track to Google Tag
2135
+ if (((_d = (_c = this.pixels) === null || _c === void 0 ? void 0 : _c.google) === null || _d === void 0 ? void 0 : _d.enabled) && window.gtag) {
2136
+ try {
2137
+ window.gtag('event', eventName, properties);
2138
+ }
2139
+ catch (error) {
2140
+ this.log('Error tracking Google Tag event:', error);
2141
+ }
2142
+ }
2143
+ // Track to TikTok Pixel
2144
+ if (((_f = (_e = this.pixels) === null || _e === void 0 ? void 0 : _e.tiktok) === null || _f === void 0 ? void 0 : _f.enabled) && window.ttq) {
2145
+ try {
2146
+ // Map common events to TikTok names
2147
+ const tiktokEventMap = {
2148
+ 'Purchase': 'CompletePayment',
2149
+ 'AddToCart': 'AddToCart',
2150
+ 'InitiateCheckout': 'InitiateCheckout',
2151
+ 'ViewContent': 'ViewContent',
2152
+ 'Search': 'Search',
2153
+ 'Lead': 'SubmitForm'
2154
+ };
2155
+ const tiktokEvent = tiktokEventMap[eventName] || eventName;
2156
+ window.ttq.track(tiktokEvent, properties);
2157
+ }
2158
+ catch (error) {
2159
+ this.log('Error tracking TikTok Pixel event:', error);
2160
+ }
2161
+ }
2162
+ }
2163
+ /**
2164
+ * Manually trigger a custom script
2165
+ */
2166
+ triggerCustomScript(scriptId) {
2167
+ const script = this.scripts.find(s => s.id === scriptId && s.trigger === 'custom');
2168
+ if (script && this.shouldLoadScript(script)) {
2169
+ this.loadScript(script);
2170
+ }
2171
+ }
2172
+ /**
2173
+ * Get loaded scripts
2174
+ */
2175
+ getLoadedScripts() {
2176
+ return Array.from(this.loadedScripts);
2177
+ }
2178
+ /**
2179
+ * Extract app endpoint from ingest endpoint
2180
+ */
2181
+ extractAppEndpoint(endpoint) {
2182
+ if (!endpoint) {
2183
+ return 'https://app.datalyr.com';
2184
+ }
2185
+ // If it's already an app endpoint, use it
2186
+ if (endpoint.includes('app.datalyr.com')) {
2187
+ return endpoint;
2188
+ }
2189
+ // Convert ingest endpoint to app endpoint
2190
+ if (endpoint.includes('ingest.datalyr.com')) {
2191
+ return 'https://app.datalyr.com';
2192
+ }
2193
+ // For local development
2194
+ if (endpoint.includes('localhost') || endpoint.includes('127.0.0.1')) {
2195
+ // Assume app is on port 3000 if ingest is on 3001
2196
+ return endpoint.replace(':3001', ':3000').replace('/ingest', '');
2197
+ }
2198
+ // For custom endpoints, try to extract the base domain
2199
+ try {
2200
+ const url = new URL(endpoint);
2201
+ return `${url.protocol}//${url.hostname}${url.port ? ':' + url.port : ''}`;
2202
+ }
2203
+ catch (_a) {
2204
+ return 'https://app.datalyr.com';
2205
+ }
2206
+ }
2207
+ /**
2208
+ * Check for malicious patterns in inline scripts
2209
+ */
2210
+ containsMaliciousPatterns(content) {
2211
+ // Basic patterns that might indicate malicious content
2212
+ const dangerousPatterns = [
2213
+ /<script[^>]*>/gi, // Script tags within content
2214
+ /document\.cookie/gi, // Direct cookie access
2215
+ /eval\s*\(/gi, // eval usage
2216
+ /Function\s*\(/gi, // Function constructor
2217
+ /innerHTML\s*=/gi, // Direct innerHTML assignment
2218
+ /document\.write/gi, // document.write usage
2219
+ ];
2220
+ return dangerousPatterns.some(pattern => pattern.test(content));
2221
+ }
2222
+ /**
2223
+ * Validate script URL
2224
+ */
2225
+ isValidScriptUrl(url) {
2226
+ try {
2227
+ const parsed = new URL(url);
2228
+ // Only allow HTTPS in production (allow HTTP for localhost)
2229
+ if (parsed.protocol !== 'https:' && !parsed.hostname.includes('localhost')) {
2230
+ return false;
2231
+ }
2232
+ // Block data: and javascript: protocols
2233
+ if (['data:', 'javascript:', 'file:'].includes(parsed.protocol)) {
2234
+ return false;
2235
+ }
2236
+ return true;
2237
+ }
2238
+ catch (_a) {
2239
+ return false;
2240
+ }
2241
+ }
2242
+ /**
2243
+ * Generate a nonce for CSP
2244
+ */
2245
+ generateNonce() {
2246
+ const array = new Uint8Array(16);
2247
+ crypto.getRandomValues(array);
2248
+ return btoa(String.fromCharCode(...array));
2249
+ }
2250
+ /**
2251
+ * Debug logging
2252
+ */
2253
+ log(...args) {
2254
+ if (this.debug) {
2255
+ console.log('[Datalyr Container]', ...args);
2256
+ }
2257
+ }
2258
+ }
2259
+
2260
+ /**
2261
+ * Datalyr Web SDK
2262
+ * Modern attribution tracking for web applications
2263
+ */
2264
+ class Datalyr {
2265
+ constructor() {
2266
+ this.superProperties = {};
2267
+ this.userProperties = {};
2268
+ this.optedOut = false;
2269
+ this.initialized = false;
2270
+ this.errors = [];
2271
+ this.MAX_ERRORS = 50;
2272
+ this.heavyFingerprintCollected = false;
2273
+ // Check for opt-out cookie on instantiation using default cookie instance
2274
+ this.optedOut = cookies.get('__dl_opt_out') === 'true';
2275
+ }
2276
+ /**
2277
+ * Initialize the SDK
2278
+ */
2279
+ init(config) {
2280
+ if (this.initialized) {
2281
+ console.warn('[Datalyr] SDK already initialized');
2282
+ return;
2283
+ }
2284
+ // Validate config
2285
+ if (!config.workspaceId) {
2286
+ throw new Error('[Datalyr] workspaceId is required');
2287
+ }
2288
+ // Set default config values
2289
+ this.config = Object.assign({ endpoint: 'https://ingest.datalyr.com', debug: false, batchSize: 10, flushInterval: 5000, flushAt: 10, criticalEvents: undefined, highPriorityEvents: undefined, sessionTimeout: 30 * 60 * 1000, trackSessions: true, attributionWindow: 30 * 24 * 60 * 60 * 1000, trackedParams: [], respectDoNotTrack: false, respectGlobalPrivacyControl: true, privacyMode: 'standard', cookieDomain: 'auto', cookieExpires: 365, secureCookie: 'auto', sameSite: 'Lax', cookiePrefix: '__dl_', enablePerformanceTracking: true, enableFingerprinting: true, maxRetries: 5, retryDelay: 1000, maxOfflineQueueSize: 100, trackSPA: true, trackPageViews: true, fallbackEndpoints: [], plugins: [] }, config);
2290
+ // Initialize cookie storage with config
2291
+ this.cookies = new CookieStorage({
2292
+ domain: this.config.cookieDomain,
2293
+ maxAge: this.config.cookieExpires,
2294
+ sameSite: this.config.sameSite,
2295
+ secure: this.config.secureCookie
2296
+ });
2297
+ // Initialize modules
2298
+ this.identity = new IdentityManager();
2299
+ this.session = new SessionManager(this.config.sessionTimeout);
2300
+ this.attribution = new AttributionManager({
2301
+ attributionWindow: this.config.attributionWindow,
2302
+ trackedParams: this.config.trackedParams
2303
+ });
2304
+ this.queue = new EventQueue(this.config);
2305
+ this.fingerprint = new FingerprintCollector({
2306
+ privacyMode: this.config.privacyMode,
2307
+ enableFingerprinting: this.config.enableFingerprinting
2308
+ });
2309
+ // Set session ID in identity manager
2310
+ const sessionId = this.session.getSessionId();
2311
+ this.identity.setSessionId(sessionId);
2312
+ // Load stored user properties
2313
+ this.userProperties = storage.get('dl_user_traits', {});
2314
+ // Setup SPA tracking if enabled
2315
+ if (this.config.trackSPA) {
2316
+ this.setupSPATracking();
2317
+ }
2318
+ // Initialize container manager if enabled
2319
+ if (this.config.enableContainer !== false) {
2320
+ this.container = new ContainerManager({
2321
+ workspaceId: this.config.workspaceId,
2322
+ endpoint: this.config.endpoint,
2323
+ debug: this.config.debug
2324
+ });
2325
+ // Initialize container asynchronously
2326
+ this.container.init().catch(error => {
2327
+ this.log('Container initialization failed:', error);
2328
+ });
2329
+ }
2330
+ // Track initial page view if enabled
2331
+ if (this.config.trackPageViews) {
2332
+ this.page();
2333
+ }
2334
+ // Setup page unload handler
2335
+ this.setupUnloadHandler();
2336
+ // Initialize plugins
2337
+ if (this.config.plugins) {
2338
+ for (const plugin of this.config.plugins) {
2339
+ try {
2340
+ plugin.initialize(this);
2341
+ this.log(`Plugin initialized: ${plugin.name}`);
2342
+ }
2343
+ catch (error) {
2344
+ this.trackError(error, { plugin: plugin.name });
2345
+ }
2346
+ }
2347
+ }
2348
+ this.initialized = true;
2349
+ this.log('SDK initialized');
2350
+ }
2351
+ /**
2352
+ * Track an event
2353
+ */
2354
+ track(eventName, properties = {}) {
2355
+ if (!this.shouldTrack())
2356
+ return;
2357
+ try {
2358
+ // Collect heavy fingerprint on first event (lazy loading)
2359
+ if (!this.heavyFingerprintCollected && this.config.enableFingerprinting) {
2360
+ this.heavyFingerprintCollected = true;
2361
+ this.fingerprint.collectHeavyFingerprint().catch(err => {
2362
+ this.log('Heavy fingerprint collection failed:', err);
2363
+ });
2364
+ }
2365
+ // Update session activity
2366
+ this.session.updateActivity(eventName);
2367
+ // Create event payload
2368
+ const payload = this.createEventPayload(eventName, properties);
2369
+ // Queue event
2370
+ this.queue.enqueue(payload);
2371
+ // Track to third-party pixels if container is initialized
2372
+ if (this.container) {
2373
+ this.container.trackToPixels(eventName, properties);
2374
+ }
2375
+ // Call plugin handlers
2376
+ if (this.config.plugins) {
2377
+ for (const plugin of this.config.plugins) {
2378
+ if (plugin.track) {
2379
+ try {
2380
+ plugin.track(eventName, properties);
2381
+ }
2382
+ catch (error) {
2383
+ this.trackError(error, { plugin: plugin.name, event: eventName });
2384
+ }
2385
+ }
2386
+ }
2387
+ }
2388
+ this.log('Event tracked:', eventName);
2389
+ }
2390
+ catch (error) {
2391
+ this.trackError(error, { event: eventName });
2392
+ }
2393
+ }
2394
+ /**
2395
+ * Identify a user
2396
+ */
2397
+ identify(userId, traits = {}) {
2398
+ if (!this.shouldTrack())
2399
+ return;
2400
+ if (!userId) {
2401
+ console.warn('[Datalyr] identify() called without userId');
2402
+ return;
2403
+ }
2404
+ try {
2405
+ // Update identity
2406
+ const identityLink = this.identity.identify(userId, traits);
2407
+ // Store user properties
2408
+ this.userProperties = Object.assign(Object.assign({}, this.userProperties), traits);
2409
+ storage.set('dl_user_traits', this.userProperties);
2410
+ // Track $identify event
2411
+ this.track('$identify', Object.assign(Object.assign({}, identityLink), { traits }));
2412
+ // Call plugin handlers
2413
+ if (this.config.plugins) {
2414
+ for (const plugin of this.config.plugins) {
2415
+ if (plugin.identify) {
2416
+ try {
2417
+ plugin.identify(userId, traits);
2418
+ }
2419
+ catch (error) {
2420
+ this.trackError(error, { plugin: plugin.name });
2421
+ }
2422
+ }
2423
+ }
2424
+ }
2425
+ this.log('User identified:', userId);
2426
+ }
2427
+ catch (error) {
2428
+ this.trackError(error, { userId });
2429
+ }
2430
+ }
2431
+ /**
2432
+ * Track a page view
2433
+ */
2434
+ page(properties = {}) {
2435
+ if (!this.shouldTrack())
2436
+ return;
2437
+ const pageData = Object.assign({ title: document.title, url: window.location.href, path: window.location.pathname, search: window.location.search, referrer: document.referrer }, properties);
2438
+ // Add referrer data
2439
+ const referrerData = getReferrerData();
2440
+ Object.assign(pageData, referrerData);
2441
+ // Add performance metrics if enabled
2442
+ if (this.config.enablePerformanceTracking) {
2443
+ const metrics = this.getPerformanceMetrics();
2444
+ if (metrics) {
2445
+ pageData.performance = metrics;
2446
+ }
2447
+ }
2448
+ this.track('pageview', pageData);
2449
+ // Call plugin handlers
2450
+ if (this.config.plugins) {
2451
+ for (const plugin of this.config.plugins) {
2452
+ if (plugin.page) {
2453
+ try {
2454
+ plugin.page(pageData);
2455
+ }
2456
+ catch (error) {
2457
+ this.trackError(error, { plugin: plugin.name });
2458
+ }
2459
+ }
2460
+ }
2461
+ }
2462
+ }
2463
+ /**
2464
+ * Track a screen view (for SPAs)
2465
+ */
2466
+ screen(screenName, properties = {}) {
2467
+ this.track('screen_view', Object.assign({ screen_name: screenName }, properties));
2468
+ }
2469
+ /**
2470
+ * Associate user with a group/account
2471
+ */
2472
+ group(groupId, traits = {}) {
2473
+ this.track('$group', {
2474
+ group_id: groupId,
2475
+ traits
2476
+ });
2477
+ }
2478
+ /**
2479
+ * Alias one ID to another
2480
+ */
2481
+ alias(userId, previousId) {
2482
+ const aliasData = this.identity.alias(userId, previousId);
2483
+ this.track('$alias', aliasData);
2484
+ }
2485
+ /**
2486
+ * Reset the current user
2487
+ */
2488
+ reset() {
2489
+ this.identity.reset();
2490
+ this.userProperties = {};
2491
+ storage.remove('dl_user_traits');
2492
+ this.session.createNewSession();
2493
+ this.log('User reset');
2494
+ }
2495
+ /**
2496
+ * Get the current anonymous ID
2497
+ */
2498
+ getAnonymousId() {
2499
+ return this.identity.getAnonymousId();
2500
+ }
2501
+ /**
2502
+ * Get the current user ID
2503
+ */
2504
+ getUserId() {
2505
+ return this.identity.getUserId();
2506
+ }
2507
+ /**
2508
+ * Get the distinct ID
2509
+ */
2510
+ getDistinctId() {
2511
+ return this.identity.getDistinctId();
2512
+ }
2513
+ /**
2514
+ * Get the current session ID
2515
+ */
2516
+ getSessionId() {
2517
+ return this.session.getSessionId();
2518
+ }
2519
+ /**
2520
+ * Start a new session manually
2521
+ */
2522
+ startNewSession() {
2523
+ const sessionId = this.session.createNewSession();
2524
+ this.identity.setSessionId(sessionId);
2525
+ return sessionId;
2526
+ }
2527
+ /**
2528
+ * Get session data
2529
+ */
2530
+ getSessionData() {
2531
+ return this.session.getSessionData();
2532
+ }
2533
+ /**
2534
+ * Get current attribution data
2535
+ */
2536
+ getAttribution() {
2537
+ return this.attribution.captureAttribution();
2538
+ }
2539
+ /**
2540
+ * Get customer journey
2541
+ */
2542
+ getJourney() {
2543
+ return this.attribution.getJourney();
2544
+ }
2545
+ /**
2546
+ * Set attribution manually
2547
+ */
2548
+ setAttribution(attribution) {
2549
+ const current = this.attribution.captureAttribution();
2550
+ const merged = Object.assign(Object.assign({}, current), attribution);
2551
+ this.session.storeAttribution(merged);
2552
+ }
2553
+ /**
2554
+ * Opt out of tracking
2555
+ */
2556
+ optOut() {
2557
+ this.optedOut = true;
2558
+ cookies.set('__dl_opt_out', 'true', this.config.cookieExpires);
2559
+ this.queue.clear();
2560
+ this.log('User opted out');
2561
+ }
2562
+ /**
2563
+ * Opt in to tracking
2564
+ */
2565
+ optIn() {
2566
+ this.optedOut = false;
2567
+ cookies.set('__dl_opt_out', 'false', this.config.cookieExpires);
2568
+ this.log('User opted in');
2569
+ }
2570
+ /**
2571
+ * Check if user has opted out
2572
+ */
2573
+ isOptedOut() {
2574
+ return this.optedOut;
2575
+ }
2576
+ /**
2577
+ * Set consent preferences
2578
+ */
2579
+ setConsent(consent) {
2580
+ storage.set('dl_consent', consent);
2581
+ this.log('Consent updated:', consent);
2582
+ }
2583
+ /**
2584
+ * Manually flush the event queue
2585
+ */
2586
+ flush() {
2587
+ return __awaiter(this, void 0, void 0, function* () {
2588
+ yield this.queue.flush();
2589
+ });
2590
+ }
2591
+ /**
2592
+ * Set super properties
2593
+ */
2594
+ setSuperProperties(properties) {
2595
+ this.superProperties = Object.assign(Object.assign({}, this.superProperties), properties);
2596
+ this.log('Super properties set:', properties);
2597
+ }
2598
+ /**
2599
+ * Unset a super property
2600
+ */
2601
+ unsetSuperProperty(propertyName) {
2602
+ delete this.superProperties[propertyName];
2603
+ this.log('Super property unset:', propertyName);
2604
+ }
2605
+ /**
2606
+ * Get super properties
2607
+ */
2608
+ getSuperProperties() {
2609
+ return Object.assign({}, this.superProperties);
2610
+ }
2611
+ /**
2612
+ * Create event payload
2613
+ */
2614
+ createEventPayload(eventName, properties) {
2615
+ // Sanitize and merge properties
2616
+ const sanitizedProperties = sanitizeEventData(properties);
2617
+ const eventData = deepMerge({}, this.superProperties, sanitizedProperties);
2618
+ // Add attribution data
2619
+ const attributionData = this.attribution.getAttributionData();
2620
+ Object.assign(eventData, attributionData);
2621
+ // Add session metrics
2622
+ const sessionMetrics = this.session.getMetrics();
2623
+ Object.assign(eventData, sessionMetrics);
2624
+ // Add fingerprint data if enabled
2625
+ if (this.config.enableFingerprinting) {
2626
+ const fingerprintData = this.fingerprint.collect();
2627
+ Object.assign(eventData, {
2628
+ fingerprint: fingerprintData,
2629
+ device_fingerprint: fingerprintData // Snake case alias
2630
+ });
2631
+ }
2632
+ // Add browser context
2633
+ Object.assign(eventData, {
2634
+ url: window.location.href,
2635
+ path: window.location.pathname,
2636
+ referrer: document.referrer,
2637
+ title: document.title,
2638
+ screen_width: screen.width,
2639
+ screen_height: screen.height,
2640
+ viewport_width: window.innerWidth,
2641
+ viewport_height: window.innerHeight
2642
+ });
2643
+ // Create payload with both camelCase and snake_case fields
2644
+ const identityFields = this.identity.getIdentityFields();
2645
+ const eventId = generateUUID();
2646
+ const payload = Object.assign(Object.assign({
2647
+ // Required fields
2648
+ workspaceId: this.config.workspaceId, workspace_id: this.config.workspaceId, eventId: eventId, event_id: eventId, // Snake case alias (same ID)
2649
+ eventName, event_name: eventName, // Snake case alias
2650
+ eventData, event_data: eventData, source: 'web', timestamp: new Date().toISOString() }, identityFields), {
2651
+ // SDK metadata
2652
+ sdk_version: '1.0.0', sdk_name: 'datalyr-web-sdk' });
2653
+ return payload;
2654
+ }
2655
+ /**
2656
+ * Check if we should track
2657
+ */
2658
+ shouldTrack() {
2659
+ // Check opt-out
2660
+ if (this.optedOut) {
2661
+ return false;
2662
+ }
2663
+ // Check Do Not Track
2664
+ if (this.config.respectDoNotTrack && isDoNotTrackEnabled()) {
2665
+ return false;
2666
+ }
2667
+ // Check Global Privacy Control
2668
+ if (this.config.respectGlobalPrivacyControl && isGlobalPrivacyControlEnabled()) {
2669
+ return false;
2670
+ }
2671
+ return true;
2672
+ }
2673
+ /**
2674
+ * Setup SPA tracking
2675
+ */
2676
+ setupSPATracking() {
2677
+ // Store original methods
2678
+ const originalPushState = history.pushState;
2679
+ const originalReplaceState = history.replaceState;
2680
+ const self = this;
2681
+ // Override pushState
2682
+ history.pushState = function (...args) {
2683
+ originalPushState.apply(history, args);
2684
+ setTimeout(() => {
2685
+ self.page();
2686
+ }, 0);
2687
+ };
2688
+ // Override replaceState
2689
+ history.replaceState = function (...args) {
2690
+ originalReplaceState.apply(history, args);
2691
+ setTimeout(() => {
2692
+ self.page();
2693
+ }, 0);
2694
+ };
2695
+ // Listen for popstate
2696
+ window.addEventListener('popstate', () => {
2697
+ setTimeout(() => {
2698
+ this.page();
2699
+ }, 0);
2700
+ });
2701
+ // Listen for hashchange
2702
+ window.addEventListener('hashchange', () => {
2703
+ this.page();
2704
+ });
2705
+ }
2706
+ /**
2707
+ * Setup page unload handler
2708
+ */
2709
+ setupUnloadHandler() {
2710
+ // Use both events for maximum compatibility
2711
+ const handleUnload = () => {
2712
+ this.queue.forceFlush();
2713
+ };
2714
+ window.addEventListener('beforeunload', handleUnload);
2715
+ window.addEventListener('pagehide', handleUnload);
2716
+ window.addEventListener('visibilitychange', () => {
2717
+ if (document.visibilityState === 'hidden') {
2718
+ handleUnload();
2719
+ }
2720
+ });
2721
+ }
2722
+ /**
2723
+ * Get performance metrics
2724
+ */
2725
+ getPerformanceMetrics() {
2726
+ if (!this.config.enablePerformanceTracking)
2727
+ return null;
2728
+ const metrics = {};
2729
+ try {
2730
+ // Try Navigation Timing v2
2731
+ if (performance && typeof performance.getEntriesByType === 'function') {
2732
+ const entries = performance.getEntriesByType('navigation');
2733
+ const nav = entries && entries[0];
2734
+ if (nav) {
2735
+ metrics.pageLoadTime = Math.round(nav.loadEventEnd);
2736
+ metrics.domReadyTime = Math.round(nav.domContentLoadedEventEnd);
2737
+ metrics.firstByteTime = Math.round(nav.responseStart);
2738
+ metrics.dnsTime = Math.round(nav.domainLookupEnd - nav.domainLookupStart);
2739
+ metrics.tcpTime = Math.round(nav.connectEnd - nav.connectStart);
2740
+ metrics.requestTime = Math.round(nav.responseEnd - nav.requestStart);
2741
+ metrics.timeOnPage = Math.round(performance.now());
2742
+ }
2743
+ }
2744
+ }
2745
+ catch (error) {
2746
+ this.trackError(error, { context: 'performance_metrics' });
2747
+ }
2748
+ return Object.keys(metrics).length > 0 ? metrics : null;
2749
+ }
2750
+ /**
2751
+ * Track error
2752
+ */
2753
+ trackError(error, context) {
2754
+ const errorInfo = {
2755
+ message: error.message || String(error),
2756
+ stack: error.stack,
2757
+ context,
2758
+ timestamp: new Date().toISOString(),
2759
+ url: window.location.href
2760
+ };
2761
+ this.errors.push(errorInfo);
2762
+ // Keep only recent errors
2763
+ if (this.errors.length > this.MAX_ERRORS) {
2764
+ this.errors = this.errors.slice(-this.MAX_ERRORS);
2765
+ }
2766
+ if (this.config.debug) {
2767
+ console.error('[Datalyr Error]', errorInfo);
2768
+ }
2769
+ }
2770
+ /**
2771
+ * Get errors
2772
+ */
2773
+ getErrors() {
2774
+ return [...this.errors];
2775
+ }
2776
+ /**
2777
+ * Get network status
2778
+ */
2779
+ getNetworkStatus() {
2780
+ return this.queue.getNetworkStatus();
2781
+ }
2782
+ /**
2783
+ * Load a container script by ID
2784
+ */
2785
+ loadScript(scriptId) {
2786
+ if (this.container) {
2787
+ this.container.triggerCustomScript(scriptId);
2788
+ }
2789
+ }
2790
+ /**
2791
+ * Get loaded container scripts
2792
+ */
2793
+ getLoadedScripts() {
2794
+ if (this.container) {
2795
+ return this.container.getLoadedScripts();
2796
+ }
2797
+ return [];
2798
+ }
2799
+ /**
2800
+ * Debug logging
2801
+ */
2802
+ log(...args) {
2803
+ if (this.config.debug) {
2804
+ console.log('[Datalyr]', ...args);
2805
+ }
2806
+ }
2807
+ /**
2808
+ * Destroy the SDK instance and cleanup resources
2809
+ */
2810
+ destroy() {
2811
+ // Clean up queue
2812
+ if (this.queue) {
2813
+ this.queue.destroy();
2814
+ }
2815
+ // Clean up session
2816
+ if (this.session) {
2817
+ this.session.destroy();
2818
+ }
2819
+ // Clear any remaining data
2820
+ this.superProperties = {};
2821
+ this.userProperties = {};
2822
+ this.errors = [];
2823
+ this.initialized = false;
2824
+ this.log('SDK destroyed');
2825
+ }
2826
+ }
2827
+ // Create singleton instance
2828
+ const datalyr = new Datalyr();
2829
+ // Expose global API
2830
+ if (typeof window !== 'undefined') {
2831
+ window.datalyr = datalyr;
2832
+ }
2833
+
2834
+ module.exports = datalyr;
2835
+ //# sourceMappingURL=datalyr.cjs.js.map