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