@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.
- package/LICENSE +21 -0
- package/README.md +727 -0
- package/dist/attribution.d.ts +68 -0
- package/dist/attribution.d.ts.map +1 -0
- package/dist/container.d.ts +140 -0
- package/dist/container.d.ts.map +1 -0
- package/dist/datalyr.cjs.js +2835 -0
- package/dist/datalyr.cjs.js.map +1 -0
- package/dist/datalyr.esm.js +2833 -0
- package/dist/datalyr.esm.js.map +1 -0
- package/dist/datalyr.esm.min.js +2 -0
- package/dist/datalyr.esm.min.js.map +1 -0
- package/dist/datalyr.js +2838 -0
- package/dist/datalyr.js.map +1 -0
- package/dist/datalyr.min.js +2 -0
- package/dist/datalyr.min.js.map +1 -0
- package/dist/fingerprint.d.ts +79 -0
- package/dist/fingerprint.d.ts.map +1 -0
- package/dist/identity.d.ts +62 -0
- package/dist/identity.d.ts.map +1 -0
- package/dist/index.d.ts +175 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/queue.d.ts +99 -0
- package/dist/queue.d.ts.map +1 -0
- package/dist/session.d.ts +87 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/storage.d.ts +44 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/types.d.ts +211 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/utils.d.ts +44 -0
- package/dist/utils.d.ts.map +1 -0
- package/package.json +62 -0
|
@@ -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
|