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