getu-attribution-v2-sdk 0.1.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/README.md +775 -0
- package/dist/core/AttributionSDK.d.ts +63 -0
- package/dist/core/AttributionSDK.d.ts.map +1 -0
- package/dist/core/AttributionSDK.js +709 -0
- package/dist/getuai-attribution.min.js +1 -0
- package/dist/index.d.ts +85 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.esm.js +1859 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/index.js +2017 -0
- package/dist/queue/index.d.ts +51 -0
- package/dist/queue/index.d.ts.map +1 -0
- package/dist/queue/index.js +215 -0
- package/dist/storage/index.d.ts +46 -0
- package/dist/storage/index.d.ts.map +1 -0
- package/dist/storage/index.js +363 -0
- package/dist/types/index.d.ts +101 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +32 -0
- package/dist/utils/index.d.ts +36 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +297 -0
- package/package.json +63 -0
|
@@ -0,0 +1,1859 @@
|
|
|
1
|
+
// Event types matching server-side enum exactly
|
|
2
|
+
var EventType;
|
|
3
|
+
(function (EventType) {
|
|
4
|
+
// Pre-conversion signals
|
|
5
|
+
EventType["PAGE_VIEW"] = "page_view";
|
|
6
|
+
EventType["VIDEO_PLAY"] = "video_play";
|
|
7
|
+
// Registration funnel
|
|
8
|
+
EventType["FORM_SUBMIT"] = "form_submit";
|
|
9
|
+
EventType["EMAIL_VERIFICATION"] = "email_verification";
|
|
10
|
+
// Login flow
|
|
11
|
+
EventType["LOGIN"] = "login";
|
|
12
|
+
// Signup flow
|
|
13
|
+
EventType["SIGNUP"] = "signup";
|
|
14
|
+
// Purchase funnel
|
|
15
|
+
EventType["PRODUCT_VIEW"] = "product_view";
|
|
16
|
+
EventType["ADD_TO_CART"] = "add_to_cart";
|
|
17
|
+
EventType["PURCHASE"] = "purchase";
|
|
18
|
+
})(EventType || (EventType = {}));
|
|
19
|
+
// Currency types
|
|
20
|
+
var Currency;
|
|
21
|
+
(function (Currency) {
|
|
22
|
+
Currency["USD"] = "USD";
|
|
23
|
+
})(Currency || (Currency = {}));
|
|
24
|
+
const defaultEndpoint = "https://attribution.getu.ai/attribution/api";
|
|
25
|
+
// Special events that should be sent immediately
|
|
26
|
+
const IMMEDIATE_EVENTS = [
|
|
27
|
+
EventType.PURCHASE,
|
|
28
|
+
EventType.LOGIN,
|
|
29
|
+
EventType.SIGNUP,
|
|
30
|
+
EventType.FORM_SUBMIT,
|
|
31
|
+
EventType.EMAIL_VERIFICATION,
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
// Generate unique ID
|
|
35
|
+
function generateId() {
|
|
36
|
+
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) {
|
|
37
|
+
const r = (Math.random() * 16) | 0;
|
|
38
|
+
const v = c === "x" ? r : (r & 0x3) | 0x8;
|
|
39
|
+
return v.toString(16);
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
// Generate timestamp in seconds
|
|
43
|
+
function getTimestamp() {
|
|
44
|
+
return Math.floor(Date.now() / 1000);
|
|
45
|
+
}
|
|
46
|
+
// Parse URL parameters
|
|
47
|
+
function parseUrlParams(url) {
|
|
48
|
+
const params = {};
|
|
49
|
+
try {
|
|
50
|
+
const urlObj = new URL(url);
|
|
51
|
+
urlObj.searchParams.forEach((value, key) => {
|
|
52
|
+
params[key] = value;
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
// Handle invalid URLs
|
|
57
|
+
}
|
|
58
|
+
return params;
|
|
59
|
+
}
|
|
60
|
+
// Extract UTM parameters from URL
|
|
61
|
+
function extractUTMParams(url) {
|
|
62
|
+
const params = parseUrlParams(url);
|
|
63
|
+
const utmParams = {};
|
|
64
|
+
const utmKeys = [
|
|
65
|
+
"utm_source",
|
|
66
|
+
"utm_medium",
|
|
67
|
+
"utm_campaign",
|
|
68
|
+
"utm_term",
|
|
69
|
+
"utm_content",
|
|
70
|
+
];
|
|
71
|
+
utmKeys.forEach((key) => {
|
|
72
|
+
if (params[key] && params[key].trim() !== "") {
|
|
73
|
+
utmParams[key] = params[key].trim();
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
return utmParams;
|
|
77
|
+
}
|
|
78
|
+
// Check if browser supports localStorage
|
|
79
|
+
function isLocalStorageSupported() {
|
|
80
|
+
try {
|
|
81
|
+
const test = "__localStorage_test__";
|
|
82
|
+
localStorage.setItem(test, test);
|
|
83
|
+
localStorage.removeItem(test);
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
// Check if browser supports IndexedDB
|
|
91
|
+
function isIndexedDBSupported() {
|
|
92
|
+
return "indexedDB" in window;
|
|
93
|
+
}
|
|
94
|
+
// Debounce function
|
|
95
|
+
function debounce(func, wait) {
|
|
96
|
+
let timeout;
|
|
97
|
+
return (...args) => {
|
|
98
|
+
clearTimeout(timeout);
|
|
99
|
+
timeout = setTimeout(() => func(...args), wait);
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
// Retry function with exponential backoff
|
|
103
|
+
async function retry(fn, maxRetries = 3, baseDelay = 1000) {
|
|
104
|
+
let lastError;
|
|
105
|
+
for (let i = 0; i <= maxRetries; i++) {
|
|
106
|
+
try {
|
|
107
|
+
return await fn();
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
lastError = error;
|
|
111
|
+
if (i === maxRetries) {
|
|
112
|
+
throw lastError;
|
|
113
|
+
}
|
|
114
|
+
const delay = baseDelay * Math.pow(2, i);
|
|
115
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
throw lastError;
|
|
119
|
+
}
|
|
120
|
+
// Console logger implementation
|
|
121
|
+
class ConsoleLogger {
|
|
122
|
+
constructor(enabled = true) {
|
|
123
|
+
this.enabled = enabled;
|
|
124
|
+
}
|
|
125
|
+
debug(message, ...args) {
|
|
126
|
+
if (this.enabled && console.debug) {
|
|
127
|
+
console.debug(`[GetuAI Debug] ${message}`, ...args);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
info(message, ...args) {
|
|
131
|
+
if (this.enabled && console.info) {
|
|
132
|
+
console.info(`[GetuAI Info] ${message}`, ...args);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
warn(message, ...args) {
|
|
136
|
+
if (this.enabled && console.warn) {
|
|
137
|
+
console.warn(`[GetuAI Warn] ${message}`, ...args);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
error(message, ...args) {
|
|
141
|
+
if (this.enabled && console.error) {
|
|
142
|
+
console.error(`[GetuAI Error] ${message}`, ...args);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
// Get user agent info
|
|
147
|
+
function getUserAgent() {
|
|
148
|
+
return navigator.userAgent || "";
|
|
149
|
+
}
|
|
150
|
+
// Get referrer
|
|
151
|
+
function getReferrer() {
|
|
152
|
+
return document.referrer || "";
|
|
153
|
+
}
|
|
154
|
+
// Get current URL
|
|
155
|
+
function getCurrentUrl() {
|
|
156
|
+
return window.location.href;
|
|
157
|
+
}
|
|
158
|
+
// Get page title
|
|
159
|
+
function getPageTitle() {
|
|
160
|
+
return document.title || "";
|
|
161
|
+
}
|
|
162
|
+
// Check if user is online
|
|
163
|
+
function isOnline() {
|
|
164
|
+
return navigator.onLine;
|
|
165
|
+
}
|
|
166
|
+
// Generate session ID
|
|
167
|
+
function generateSessionId() {
|
|
168
|
+
return `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
169
|
+
}
|
|
170
|
+
// Add UTM parameters to URL
|
|
171
|
+
function addUTMToURL$1(url, utmParams) {
|
|
172
|
+
try {
|
|
173
|
+
const urlObj = new URL(url);
|
|
174
|
+
// Add UTM parameters if they don't already exist
|
|
175
|
+
Object.entries(utmParams).forEach(([key, value]) => {
|
|
176
|
+
if (value && !urlObj.searchParams.has(key)) {
|
|
177
|
+
urlObj.searchParams.set(key, value);
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
return urlObj.toString();
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
// If URL parsing fails, return original URL
|
|
184
|
+
return url;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
// Check if domain should be excluded from UTM passing
|
|
188
|
+
function shouldExcludeDomain(url, excludeDomains = []) {
|
|
189
|
+
try {
|
|
190
|
+
const urlObj = new URL(url);
|
|
191
|
+
const hostname = urlObj.hostname.toLowerCase();
|
|
192
|
+
return excludeDomains.some((domain) => {
|
|
193
|
+
const excludeDomain = domain.toLowerCase();
|
|
194
|
+
return (hostname === excludeDomain || hostname.endsWith(`.${excludeDomain}`));
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
catch (error) {
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
// Check if URL is external (different domain)
|
|
202
|
+
function isExternalURL(url) {
|
|
203
|
+
try {
|
|
204
|
+
const urlObj = new URL(url);
|
|
205
|
+
const currentDomain = window.location.hostname;
|
|
206
|
+
return urlObj.hostname !== currentDomain;
|
|
207
|
+
}
|
|
208
|
+
catch (error) {
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
// Filter UTM parameters based on configuration
|
|
213
|
+
function filterUTMParams(utmData, allowedParams = ["utm_source", "utm_medium", "utm_campaign"]) {
|
|
214
|
+
const filtered = {};
|
|
215
|
+
allowedParams.forEach((param) => {
|
|
216
|
+
if (utmData[param]) {
|
|
217
|
+
filtered[param] = utmData[param];
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
return filtered;
|
|
221
|
+
}
|
|
222
|
+
// Get query string from URL
|
|
223
|
+
function getQueryString(url) {
|
|
224
|
+
try {
|
|
225
|
+
const targetUrl = url || window.location.href;
|
|
226
|
+
const urlObj = new URL(targetUrl);
|
|
227
|
+
return urlObj.search;
|
|
228
|
+
}
|
|
229
|
+
catch (error) {
|
|
230
|
+
return "";
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
// Get query parameters as object
|
|
234
|
+
function getQueryParams(url) {
|
|
235
|
+
try {
|
|
236
|
+
const targetUrl = url || window.location.href;
|
|
237
|
+
const urlObj = new URL(targetUrl);
|
|
238
|
+
const params = {};
|
|
239
|
+
urlObj.searchParams.forEach((value, key) => {
|
|
240
|
+
params[key] = value;
|
|
241
|
+
});
|
|
242
|
+
return params;
|
|
243
|
+
}
|
|
244
|
+
catch (error) {
|
|
245
|
+
return {};
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
// Clean URL by removing UTM parameters
|
|
249
|
+
function cleanURL() {
|
|
250
|
+
try {
|
|
251
|
+
const currentUrl = new URL(window.location.href);
|
|
252
|
+
const utmKeys = [
|
|
253
|
+
"utm_source",
|
|
254
|
+
"utm_medium",
|
|
255
|
+
"utm_campaign",
|
|
256
|
+
"utm_term",
|
|
257
|
+
"utm_content",
|
|
258
|
+
];
|
|
259
|
+
let hasParams = false;
|
|
260
|
+
utmKeys.forEach((key) => {
|
|
261
|
+
if (currentUrl.searchParams.has(key)) {
|
|
262
|
+
currentUrl.searchParams.delete(key);
|
|
263
|
+
hasParams = true;
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
if (hasParams) {
|
|
267
|
+
// Use replaceState to avoid page reload and history entry
|
|
268
|
+
window.history.replaceState({}, document.title, currentUrl.toString());
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
catch (error) {
|
|
272
|
+
// Handle invalid URLs silently
|
|
273
|
+
console.warn("Failed to clean URL:", error);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// LocalStorage implementation
|
|
278
|
+
class LocalStorageManager {
|
|
279
|
+
constructor(logger) {
|
|
280
|
+
this.logger = logger;
|
|
281
|
+
}
|
|
282
|
+
get(key) {
|
|
283
|
+
try {
|
|
284
|
+
if (!isLocalStorageSupported()) {
|
|
285
|
+
this.logger.warn("LocalStorage not supported");
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
const item = localStorage.getItem(key);
|
|
289
|
+
if (item === null) {
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
return JSON.parse(item);
|
|
293
|
+
}
|
|
294
|
+
catch (error) {
|
|
295
|
+
this.logger.error("Error reading from localStorage:", error);
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
set(key, value) {
|
|
300
|
+
try {
|
|
301
|
+
if (!isLocalStorageSupported()) {
|
|
302
|
+
this.logger.warn("LocalStorage not supported");
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
localStorage.setItem(key, JSON.stringify(value));
|
|
306
|
+
}
|
|
307
|
+
catch (error) {
|
|
308
|
+
this.logger.error("Error writing to localStorage:", error);
|
|
309
|
+
// Handle quota exceeded
|
|
310
|
+
this.handleQuotaExceeded();
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
remove(key) {
|
|
314
|
+
try {
|
|
315
|
+
if (isLocalStorageSupported()) {
|
|
316
|
+
localStorage.removeItem(key);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
catch (error) {
|
|
320
|
+
this.logger.error("Error removing from localStorage:", error);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
clear() {
|
|
324
|
+
try {
|
|
325
|
+
if (isLocalStorageSupported()) {
|
|
326
|
+
localStorage.clear();
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
catch (error) {
|
|
330
|
+
this.logger.error("Error clearing localStorage:", error);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
handleQuotaExceeded() {
|
|
334
|
+
// Try to clean up old data
|
|
335
|
+
try {
|
|
336
|
+
const keys = Object.keys(localStorage);
|
|
337
|
+
const attributionKeys = keys.filter((key) => key.startsWith("attribution_"));
|
|
338
|
+
if (attributionKeys.length > 0) {
|
|
339
|
+
// Remove oldest data first
|
|
340
|
+
attributionKeys.sort((a, b) => {
|
|
341
|
+
const aData = this.get(a);
|
|
342
|
+
const bData = this.get(b);
|
|
343
|
+
return (aData?.expiresAt || 0) - (bData?.expiresAt || 0);
|
|
344
|
+
});
|
|
345
|
+
// Remove oldest 20% of data
|
|
346
|
+
const toRemove = Math.ceil(attributionKeys.length * 0.2);
|
|
347
|
+
attributionKeys.slice(0, toRemove).forEach((key) => {
|
|
348
|
+
this.remove(key);
|
|
349
|
+
});
|
|
350
|
+
this.logger.info(`Cleaned up ${toRemove} old attribution records`);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
catch (error) {
|
|
354
|
+
this.logger.error("Error during quota cleanup:", error);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
// IndexedDB implementation
|
|
359
|
+
class IndexedDBManager {
|
|
360
|
+
constructor(logger) {
|
|
361
|
+
this.dbName = "attribution_events";
|
|
362
|
+
this.dbVersion = 1;
|
|
363
|
+
this.storeName = "events";
|
|
364
|
+
this.db = null;
|
|
365
|
+
this.logger = logger;
|
|
366
|
+
}
|
|
367
|
+
async init() {
|
|
368
|
+
if (!isIndexedDBSupported()) {
|
|
369
|
+
this.logger.warn("IndexedDB not supported");
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
return new Promise((resolve, reject) => {
|
|
373
|
+
const request = indexedDB.open(this.dbName, this.dbVersion);
|
|
374
|
+
request.onerror = () => {
|
|
375
|
+
this.logger.error("Failed to open IndexedDB:", request.error);
|
|
376
|
+
reject(request.error);
|
|
377
|
+
};
|
|
378
|
+
request.onsuccess = () => {
|
|
379
|
+
this.db = request.result;
|
|
380
|
+
this.logger.info("IndexedDB initialized successfully");
|
|
381
|
+
resolve();
|
|
382
|
+
};
|
|
383
|
+
request.onupgradeneeded = (event) => {
|
|
384
|
+
const db = event.target.result;
|
|
385
|
+
if (!db.objectStoreNames.contains(this.storeName)) {
|
|
386
|
+
const store = db.createObjectStore(this.storeName, {
|
|
387
|
+
keyPath: "id",
|
|
388
|
+
autoIncrement: true,
|
|
389
|
+
});
|
|
390
|
+
store.createIndex("timestamp", "timestamp", { unique: false });
|
|
391
|
+
store.createIndex("sent", "sent", { unique: false });
|
|
392
|
+
store.createIndex("queued_at", "queued_at", { unique: false });
|
|
393
|
+
}
|
|
394
|
+
};
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
async addEvent(event) {
|
|
398
|
+
if (!this.db) {
|
|
399
|
+
throw new Error("IndexedDB not initialized");
|
|
400
|
+
}
|
|
401
|
+
return new Promise((resolve, reject) => {
|
|
402
|
+
const transaction = this.db.transaction([this.storeName], "readwrite");
|
|
403
|
+
const store = transaction.objectStore(this.storeName);
|
|
404
|
+
const eventRecord = {
|
|
405
|
+
...event,
|
|
406
|
+
queued_at: Date.now(),
|
|
407
|
+
sent: false,
|
|
408
|
+
};
|
|
409
|
+
const request = store.add(eventRecord);
|
|
410
|
+
request.onsuccess = () => {
|
|
411
|
+
this.logger.debug("Event added to IndexedDB queue");
|
|
412
|
+
resolve();
|
|
413
|
+
};
|
|
414
|
+
request.onerror = () => {
|
|
415
|
+
this.logger.error("Failed to add event to IndexedDB:", request.error);
|
|
416
|
+
reject(request.error);
|
|
417
|
+
};
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
async getUnsentEvents(limit = 100) {
|
|
421
|
+
if (!this.db) {
|
|
422
|
+
return [];
|
|
423
|
+
}
|
|
424
|
+
return new Promise((resolve, reject) => {
|
|
425
|
+
const transaction = this.db.transaction([this.storeName], "readonly");
|
|
426
|
+
const store = transaction.objectStore(this.storeName);
|
|
427
|
+
const index = store.index("sent");
|
|
428
|
+
const request = index.getAll(IDBKeyRange.only(false), limit);
|
|
429
|
+
request.onsuccess = () => {
|
|
430
|
+
const events = request.result.map((record) => {
|
|
431
|
+
const { queued_at, sent, ...event } = record;
|
|
432
|
+
return event;
|
|
433
|
+
});
|
|
434
|
+
resolve(events);
|
|
435
|
+
};
|
|
436
|
+
request.onerror = () => {
|
|
437
|
+
this.logger.error("Failed to get unsent events:", request.error);
|
|
438
|
+
reject(request.error);
|
|
439
|
+
};
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
async markEventsAsSent(eventIds) {
|
|
443
|
+
if (!this.db || eventIds.length === 0) {
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
return new Promise((resolve, reject) => {
|
|
447
|
+
const transaction = this.db.transaction([this.storeName], "readwrite");
|
|
448
|
+
const store = transaction.objectStore(this.storeName);
|
|
449
|
+
let completed = 0;
|
|
450
|
+
let hasError = false;
|
|
451
|
+
eventIds.forEach((eventId) => {
|
|
452
|
+
const getRequest = store.get(eventId);
|
|
453
|
+
getRequest.onsuccess = () => {
|
|
454
|
+
if (getRequest.result) {
|
|
455
|
+
const record = { ...getRequest.result, sent: true };
|
|
456
|
+
const putRequest = store.put(record);
|
|
457
|
+
putRequest.onsuccess = () => {
|
|
458
|
+
completed++;
|
|
459
|
+
if (completed === eventIds.length && !hasError) {
|
|
460
|
+
resolve();
|
|
461
|
+
}
|
|
462
|
+
};
|
|
463
|
+
putRequest.onerror = () => {
|
|
464
|
+
hasError = true;
|
|
465
|
+
this.logger.error("Failed to mark event as sent:", putRequest.error);
|
|
466
|
+
reject(putRequest.error);
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
else {
|
|
470
|
+
completed++;
|
|
471
|
+
if (completed === eventIds.length && !hasError) {
|
|
472
|
+
resolve();
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
};
|
|
476
|
+
getRequest.onerror = () => {
|
|
477
|
+
hasError = true;
|
|
478
|
+
this.logger.error("Failed to get event for marking as sent:", getRequest.error);
|
|
479
|
+
reject(getRequest.error);
|
|
480
|
+
};
|
|
481
|
+
});
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
async cleanupOldEvents(maxAge = 7 * 24 * 60 * 60 * 1000) {
|
|
485
|
+
if (!this.db) {
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
const cutoffTime = Date.now() - maxAge;
|
|
489
|
+
return new Promise((resolve, reject) => {
|
|
490
|
+
const transaction = this.db.transaction([this.storeName], "readwrite");
|
|
491
|
+
const store = transaction.objectStore(this.storeName);
|
|
492
|
+
const index = store.index("queued_at");
|
|
493
|
+
const request = index.openCursor(IDBKeyRange.upperBound(cutoffTime));
|
|
494
|
+
request.onsuccess = () => {
|
|
495
|
+
const cursor = request.result;
|
|
496
|
+
if (cursor) {
|
|
497
|
+
cursor.delete();
|
|
498
|
+
cursor.continue();
|
|
499
|
+
}
|
|
500
|
+
else {
|
|
501
|
+
this.logger.info("Old events cleanup completed");
|
|
502
|
+
resolve();
|
|
503
|
+
}
|
|
504
|
+
};
|
|
505
|
+
request.onerror = () => {
|
|
506
|
+
this.logger.error("Failed to cleanup old events:", request.error);
|
|
507
|
+
reject(request.error);
|
|
508
|
+
};
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
async getQueueSize() {
|
|
512
|
+
if (!this.db) {
|
|
513
|
+
return 0;
|
|
514
|
+
}
|
|
515
|
+
return new Promise((resolve, reject) => {
|
|
516
|
+
const transaction = this.db.transaction([this.storeName], "readonly");
|
|
517
|
+
const store = transaction.objectStore(this.storeName);
|
|
518
|
+
const request = store.count();
|
|
519
|
+
request.onsuccess = () => {
|
|
520
|
+
resolve(request.result);
|
|
521
|
+
};
|
|
522
|
+
request.onerror = () => {
|
|
523
|
+
this.logger.error("Failed to get queue size:", request.error);
|
|
524
|
+
reject(request.error);
|
|
525
|
+
};
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
async clear() {
|
|
529
|
+
if (!this.db) {
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
return new Promise((resolve, reject) => {
|
|
533
|
+
const transaction = this.db.transaction([this.storeName], "readwrite");
|
|
534
|
+
const store = transaction.objectStore(this.storeName);
|
|
535
|
+
const request = store.clear();
|
|
536
|
+
request.onsuccess = () => {
|
|
537
|
+
this.logger.info("IndexedDB queue cleared");
|
|
538
|
+
resolve();
|
|
539
|
+
};
|
|
540
|
+
request.onerror = () => {
|
|
541
|
+
this.logger.error("Failed to clear IndexedDB queue:", request.error);
|
|
542
|
+
reject(request.error);
|
|
543
|
+
};
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
// Attribution storage manager
|
|
548
|
+
class AttributionStorageManager {
|
|
549
|
+
constructor(logger) {
|
|
550
|
+
this.UTM_STORAGE_KEY = "attribution_utm_data";
|
|
551
|
+
this.SESSION_STORAGE_KEY = "attribution_session";
|
|
552
|
+
this.logger = logger;
|
|
553
|
+
this.localStorage = new LocalStorageManager(logger);
|
|
554
|
+
this.indexedDB = new IndexedDBManager(logger);
|
|
555
|
+
}
|
|
556
|
+
async init() {
|
|
557
|
+
await this.indexedDB.init();
|
|
558
|
+
}
|
|
559
|
+
// UTM data management
|
|
560
|
+
storeUTMData(utmData) {
|
|
561
|
+
try {
|
|
562
|
+
const existingData = this.getUTMData();
|
|
563
|
+
// Create default UTM data if none exists
|
|
564
|
+
const defaultUTMData = {
|
|
565
|
+
utm_source: "",
|
|
566
|
+
utm_medium: "",
|
|
567
|
+
utm_campaign: "",
|
|
568
|
+
utm_term: "",
|
|
569
|
+
utm_content: "",
|
|
570
|
+
timestamp: Date.now(),
|
|
571
|
+
};
|
|
572
|
+
const newData = {
|
|
573
|
+
firstTouch: existingData?.firstTouch || defaultUTMData,
|
|
574
|
+
lastTouch: existingData?.lastTouch || defaultUTMData,
|
|
575
|
+
touchpoints: existingData?.touchpoints || [],
|
|
576
|
+
...utmData,
|
|
577
|
+
expiresAt: Date.now() + 30 * 24 * 60 * 60 * 1000, // 30 days
|
|
578
|
+
};
|
|
579
|
+
this.localStorage.set(this.UTM_STORAGE_KEY, newData);
|
|
580
|
+
this.logger.debug("UTM data stored successfully:", {
|
|
581
|
+
firstTouch: newData.firstTouch,
|
|
582
|
+
lastTouch: newData.lastTouch,
|
|
583
|
+
touchpointsCount: newData.touchpoints.length,
|
|
584
|
+
expiresAt: new Date(newData.expiresAt).toISOString(),
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
catch (error) {
|
|
588
|
+
this.logger.error("Failed to store UTM data:", error);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
getUTMData() {
|
|
592
|
+
const data = this.localStorage.get(this.UTM_STORAGE_KEY);
|
|
593
|
+
if (data && data.expiresAt && data.expiresAt > Date.now()) {
|
|
594
|
+
return data;
|
|
595
|
+
}
|
|
596
|
+
// Clean up expired data
|
|
597
|
+
if (data) {
|
|
598
|
+
this.localStorage.remove(this.UTM_STORAGE_KEY);
|
|
599
|
+
}
|
|
600
|
+
return null;
|
|
601
|
+
}
|
|
602
|
+
// Session management
|
|
603
|
+
storeSession(session) {
|
|
604
|
+
this.localStorage.set(this.SESSION_STORAGE_KEY, session);
|
|
605
|
+
}
|
|
606
|
+
getSession() {
|
|
607
|
+
return this.localStorage.get(this.SESSION_STORAGE_KEY);
|
|
608
|
+
}
|
|
609
|
+
// Event queue management
|
|
610
|
+
async queueEvent(event) {
|
|
611
|
+
try {
|
|
612
|
+
await this.indexedDB.addEvent(event);
|
|
613
|
+
}
|
|
614
|
+
catch (error) {
|
|
615
|
+
this.logger.error("Failed to queue event:", error);
|
|
616
|
+
throw error;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
async getUnsentEvents(limit = 100) {
|
|
620
|
+
return await this.indexedDB.getUnsentEvents(limit);
|
|
621
|
+
}
|
|
622
|
+
async markEventsAsSent(eventIds) {
|
|
623
|
+
await this.indexedDB.markEventsAsSent(eventIds);
|
|
624
|
+
}
|
|
625
|
+
async getQueueSize() {
|
|
626
|
+
return await this.indexedDB.getQueueSize();
|
|
627
|
+
}
|
|
628
|
+
async cleanupOldEvents() {
|
|
629
|
+
await this.indexedDB.cleanupOldEvents();
|
|
630
|
+
}
|
|
631
|
+
async clearQueue() {
|
|
632
|
+
await this.indexedDB.clear();
|
|
633
|
+
}
|
|
634
|
+
// Cleanup expired data
|
|
635
|
+
cleanupExpiredData() {
|
|
636
|
+
this.getUTMData(); // This will automatically clean up expired UTM data
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
class EventQueueManager {
|
|
641
|
+
constructor(logger, apiKey, apiEndpoint, batchSize = 100, batchInterval = 5000, maxRetries = 3, retryDelay = 1000, sendEvents) {
|
|
642
|
+
this.queue = [];
|
|
643
|
+
this.processing = false;
|
|
644
|
+
this.batchTimer = null;
|
|
645
|
+
this.logger = logger;
|
|
646
|
+
this.apiKey = apiKey;
|
|
647
|
+
this.apiEndpoint = apiEndpoint;
|
|
648
|
+
this.batchSize = batchSize;
|
|
649
|
+
this.batchInterval = batchInterval;
|
|
650
|
+
this.maxRetries = maxRetries;
|
|
651
|
+
this.retryDelay = retryDelay;
|
|
652
|
+
this.sendEvents = sendEvents;
|
|
653
|
+
// Debounced process function to avoid excessive calls
|
|
654
|
+
this.debouncedProcess = debounce(this.process.bind(this), 100);
|
|
655
|
+
}
|
|
656
|
+
add(event) {
|
|
657
|
+
this.queue.push(event);
|
|
658
|
+
this.logger.debug(`Event added to queue: ${event.event_type}`);
|
|
659
|
+
// Check if this is an immediate event
|
|
660
|
+
if (IMMEDIATE_EVENTS.includes(event.event_type)) {
|
|
661
|
+
this.logger.debug(`Immediate event detected: ${event.event_type}, processing immediately`);
|
|
662
|
+
this.processImmediate(event);
|
|
663
|
+
}
|
|
664
|
+
else {
|
|
665
|
+
// Schedule batch processing
|
|
666
|
+
this.scheduleBatchProcessing();
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
async process() {
|
|
670
|
+
if (this.processing || this.queue.length === 0) {
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
this.processing = true;
|
|
674
|
+
try {
|
|
675
|
+
const eventsToProcess = this.queue.splice(0, this.batchSize);
|
|
676
|
+
this.logger.debug(`Processing ${eventsToProcess.length} events from queue`);
|
|
677
|
+
const result = await this.sendEvents(eventsToProcess);
|
|
678
|
+
this.logger.info(`Successfully processed ${eventsToProcess.length} events`);
|
|
679
|
+
// Events are automatically removed from queue when successfully sent
|
|
680
|
+
// No need to manually clean up as they were already spliced from the queue
|
|
681
|
+
}
|
|
682
|
+
catch (error) {
|
|
683
|
+
this.logger.error("Failed to process events:", error);
|
|
684
|
+
// Put events back in queue for retry
|
|
685
|
+
const failedEvents = this.queue.splice(0, this.batchSize);
|
|
686
|
+
this.queue.unshift(...failedEvents);
|
|
687
|
+
// Schedule retry
|
|
688
|
+
setTimeout(() => {
|
|
689
|
+
this.processing = false;
|
|
690
|
+
this.debouncedProcess();
|
|
691
|
+
}, this.retryDelay);
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
this.processing = false;
|
|
695
|
+
// Process remaining events if any
|
|
696
|
+
if (this.queue.length > 0) {
|
|
697
|
+
this.debouncedProcess();
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
async processImmediate(event) {
|
|
701
|
+
try {
|
|
702
|
+
this.logger.debug(`Processing immediate event: ${event.event_type}`);
|
|
703
|
+
await this.sendEvents([event]);
|
|
704
|
+
this.logger.info(`Immediate event processed successfully: ${event.event_type}`);
|
|
705
|
+
// Event is automatically removed from queue when successfully sent
|
|
706
|
+
}
|
|
707
|
+
catch (error) {
|
|
708
|
+
this.logger.error(`Failed to process immediate event: ${event.event_type}`, error);
|
|
709
|
+
// Add to queue for retry
|
|
710
|
+
this.queue.unshift(event);
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
scheduleBatchProcessing() {
|
|
714
|
+
if (this.batchTimer) {
|
|
715
|
+
clearTimeout(this.batchTimer);
|
|
716
|
+
}
|
|
717
|
+
this.batchTimer = setTimeout(() => {
|
|
718
|
+
this.debouncedProcess();
|
|
719
|
+
}, this.batchInterval);
|
|
720
|
+
}
|
|
721
|
+
clear() {
|
|
722
|
+
this.queue = [];
|
|
723
|
+
if (this.batchTimer) {
|
|
724
|
+
clearTimeout(this.batchTimer);
|
|
725
|
+
this.batchTimer = null;
|
|
726
|
+
}
|
|
727
|
+
this.processing = false;
|
|
728
|
+
this.logger.info("Event queue cleared");
|
|
729
|
+
}
|
|
730
|
+
size() {
|
|
731
|
+
return this.queue.length;
|
|
732
|
+
}
|
|
733
|
+
// Get queue statistics
|
|
734
|
+
getStats() {
|
|
735
|
+
return {
|
|
736
|
+
size: this.queue.length,
|
|
737
|
+
processing: this.processing,
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
// Force process all events in queue
|
|
741
|
+
async flush() {
|
|
742
|
+
this.logger.info("Flushing event queue");
|
|
743
|
+
while (this.queue.length > 0) {
|
|
744
|
+
await this.process();
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
// HTTP client for sending events
|
|
749
|
+
class EventHttpClient {
|
|
750
|
+
constructor(logger, apiKey, apiEndpoint, maxRetries = 3, retryDelay = 1000) {
|
|
751
|
+
this.logger = logger;
|
|
752
|
+
this.apiKey = apiKey;
|
|
753
|
+
this.apiEndpoint = apiEndpoint;
|
|
754
|
+
this.maxRetries = maxRetries;
|
|
755
|
+
this.retryDelay = retryDelay;
|
|
756
|
+
}
|
|
757
|
+
async sendEvents(events) {
|
|
758
|
+
if (events.length === 0) {
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
// Transform events to match server format
|
|
762
|
+
const transformedEvents = events.map((event) => ({
|
|
763
|
+
event_id: event.event_id,
|
|
764
|
+
event_type: event.event_type,
|
|
765
|
+
tracking_user_id: event.tracking_user_id,
|
|
766
|
+
utm_source: event.utm_source || null,
|
|
767
|
+
utm_medium: event.utm_medium || null,
|
|
768
|
+
utm_campaign: event.utm_campaign || null,
|
|
769
|
+
utm_term: event.utm_term || null,
|
|
770
|
+
utm_content: event.utm_content || null,
|
|
771
|
+
revenue: event.revenue || null,
|
|
772
|
+
currency: event.currency || null,
|
|
773
|
+
event_data: event.event_data || null,
|
|
774
|
+
context: event.context || null,
|
|
775
|
+
timestamp: event.timestamp || getTimestamp(), // Convert to seconds
|
|
776
|
+
}));
|
|
777
|
+
const batchRequest = { events: transformedEvents };
|
|
778
|
+
await retry(async () => {
|
|
779
|
+
const response = await fetch(`${this.apiEndpoint}/attribution/events`, {
|
|
780
|
+
method: "POST",
|
|
781
|
+
headers: {
|
|
782
|
+
"Content-Type": "application/json",
|
|
783
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
784
|
+
},
|
|
785
|
+
body: JSON.stringify(batchRequest),
|
|
786
|
+
});
|
|
787
|
+
if (!response.ok) {
|
|
788
|
+
const errorText = await response.text();
|
|
789
|
+
throw new Error(`HTTP ${response.status}: ${errorText}`);
|
|
790
|
+
}
|
|
791
|
+
const result = await response.json();
|
|
792
|
+
this.logger.debug("Events sent successfully:", result);
|
|
793
|
+
// Return the sent events for cleanup
|
|
794
|
+
return { result, sentEvents: events };
|
|
795
|
+
}, this.maxRetries, this.retryDelay);
|
|
796
|
+
}
|
|
797
|
+
async sendSingleEvent(event) {
|
|
798
|
+
await this.sendEvents([event]);
|
|
799
|
+
}
|
|
800
|
+
// Test connection
|
|
801
|
+
async testConnection() {
|
|
802
|
+
try {
|
|
803
|
+
const response = await fetch(`${this.apiEndpoint}/health`, {
|
|
804
|
+
method: "GET",
|
|
805
|
+
headers: {
|
|
806
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
807
|
+
},
|
|
808
|
+
});
|
|
809
|
+
return response.ok;
|
|
810
|
+
}
|
|
811
|
+
catch (error) {
|
|
812
|
+
this.logger.error("Connection test failed:", error);
|
|
813
|
+
return false;
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
class AttributionSDK {
|
|
819
|
+
constructor(config) {
|
|
820
|
+
this.session = null;
|
|
821
|
+
this.initialized = false;
|
|
822
|
+
this.autoTrackEnabled = false;
|
|
823
|
+
this.pageViewTrackTimes = new Map();
|
|
824
|
+
// SPA tracking internals
|
|
825
|
+
this.spaTrackingEnabled = false;
|
|
826
|
+
this.lastTrackedPath = "";
|
|
827
|
+
this.originalPushState = null;
|
|
828
|
+
this.originalReplaceState = null;
|
|
829
|
+
this.popstateHandler = null;
|
|
830
|
+
this.config = {
|
|
831
|
+
apiEndpoint: defaultEndpoint,
|
|
832
|
+
batchSize: 100,
|
|
833
|
+
batchInterval: 5000,
|
|
834
|
+
maxRetries: 3,
|
|
835
|
+
retryDelay: 1000,
|
|
836
|
+
enableDebug: false,
|
|
837
|
+
autoTrack: false,
|
|
838
|
+
autoTrackPageView: false,
|
|
839
|
+
sessionTimeout: 30 * 60 * 1000, // 30 minutes
|
|
840
|
+
enableCrossDomainUTM: true,
|
|
841
|
+
crossDomainUTMParams: [
|
|
842
|
+
"utm_source",
|
|
843
|
+
"utm_medium",
|
|
844
|
+
"utm_campaign",
|
|
845
|
+
"utm_term",
|
|
846
|
+
"utm_content",
|
|
847
|
+
],
|
|
848
|
+
excludeDomains: [],
|
|
849
|
+
autoCleanUTM: true,
|
|
850
|
+
pageViewDebounceInterval: 5000, // 5 seconds default
|
|
851
|
+
...config,
|
|
852
|
+
};
|
|
853
|
+
this.logger = new ConsoleLogger(this.config.enableDebug);
|
|
854
|
+
this.storage = new AttributionStorageManager(this.logger);
|
|
855
|
+
this.httpClient = new EventHttpClient(this.logger, this.config.apiKey, this.config.apiEndpoint || defaultEndpoint, this.config.maxRetries, this.config.retryDelay);
|
|
856
|
+
this.queue = new EventQueueManager(this.logger, this.config.apiKey, this.config.apiEndpoint || defaultEndpoint, this.config.batchSize, this.config.batchInterval, this.config.maxRetries, this.config.retryDelay, (events) => this.httpClient.sendEvents(events));
|
|
857
|
+
}
|
|
858
|
+
async init() {
|
|
859
|
+
if (this.initialized) {
|
|
860
|
+
this.logger.warn("SDK already initialized");
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
try {
|
|
864
|
+
this.logger.info("Initializing GetuAI Attribution SDK");
|
|
865
|
+
// Initialize storage
|
|
866
|
+
await this.storage.init();
|
|
867
|
+
// Initialize session
|
|
868
|
+
this.initializeSession();
|
|
869
|
+
// Extract and store UTM data from current URL
|
|
870
|
+
this.extractAndStoreUTMData();
|
|
871
|
+
// Setup auto-tracking if enabled
|
|
872
|
+
if (this.config.autoTrack) {
|
|
873
|
+
this.setupAutoTracking();
|
|
874
|
+
}
|
|
875
|
+
// Setup online/offline handlers
|
|
876
|
+
this.setupNetworkHandlers();
|
|
877
|
+
// Setup page visibility handlers
|
|
878
|
+
this.setupVisibilityHandlers();
|
|
879
|
+
// Setup beforeunload handler
|
|
880
|
+
this.setupBeforeUnloadHandler();
|
|
881
|
+
this.initialized = true;
|
|
882
|
+
this.logger.info("🚀 GetuAI Attribution SDK initialized successfully");
|
|
883
|
+
this.logger.info("📄 Auto track page view = " + this.config.autoTrackPageView);
|
|
884
|
+
// Auto track page view if enabled (independent from autoTrack)
|
|
885
|
+
// Also enables SPA route tracking automatically
|
|
886
|
+
if (this.config.autoTrackPageView) {
|
|
887
|
+
this.logger.info("📄 Auto track page view enabled (including SPA route tracking)");
|
|
888
|
+
// Record the initial path for SPA tracking
|
|
889
|
+
this.lastTrackedPath = this.getCurrentPath();
|
|
890
|
+
// Setup SPA tracking for route changes
|
|
891
|
+
this.setupSPATracking();
|
|
892
|
+
setTimeout(() => {
|
|
893
|
+
// Fire asynchronously; do not block initialization
|
|
894
|
+
this.trackPageView()
|
|
895
|
+
.then(() => {
|
|
896
|
+
this.logger.info("✅ Auto track page view completed");
|
|
897
|
+
})
|
|
898
|
+
.catch((error) => this.logger.error("❌ Auto track page view failed:", error));
|
|
899
|
+
}, 100);
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
catch (error) {
|
|
903
|
+
this.logger.error("Failed to initialize SDK:", error);
|
|
904
|
+
throw error;
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
// Track custom event
|
|
908
|
+
async trackEvent(eventType, eventData, tracking_user_id, revenue, currency = Currency.USD) {
|
|
909
|
+
if (!this.initialized) {
|
|
910
|
+
this.logger.warn("SDK not initialized, event not tracked");
|
|
911
|
+
return;
|
|
912
|
+
}
|
|
913
|
+
try {
|
|
914
|
+
const currentUrl = getCurrentUrl();
|
|
915
|
+
// Try to fetch public IP (best-effort)
|
|
916
|
+
let ip_address = null;
|
|
917
|
+
try {
|
|
918
|
+
ip_address = await this.fetchPublicIP();
|
|
919
|
+
}
|
|
920
|
+
catch (e) {
|
|
921
|
+
// ignore
|
|
922
|
+
}
|
|
923
|
+
const pageContext = {
|
|
924
|
+
domain: typeof window !== "undefined" ? window.location.hostname : null,
|
|
925
|
+
path: typeof window !== "undefined" ? window.location.pathname : null,
|
|
926
|
+
title: getPageTitle(),
|
|
927
|
+
referrer: getReferrer(),
|
|
928
|
+
url: currentUrl.split("?")[0],
|
|
929
|
+
querystring: getQueryString(currentUrl),
|
|
930
|
+
query_params: getQueryParams(currentUrl),
|
|
931
|
+
ip_address: ip_address,
|
|
932
|
+
};
|
|
933
|
+
const sessionContext = {
|
|
934
|
+
session_id: this.session?.sessionId,
|
|
935
|
+
start_time: this.session?.startTime,
|
|
936
|
+
last_activity: this.session?.lastActivity,
|
|
937
|
+
page_views: this.session?.pageViews,
|
|
938
|
+
};
|
|
939
|
+
const event = {
|
|
940
|
+
event_id: generateId(),
|
|
941
|
+
event_type: eventType,
|
|
942
|
+
tracking_user_id: tracking_user_id,
|
|
943
|
+
timestamp: getTimestamp(),
|
|
944
|
+
event_data: eventData,
|
|
945
|
+
context: { page: pageContext, session: sessionContext },
|
|
946
|
+
revenue: revenue,
|
|
947
|
+
currency: currency,
|
|
948
|
+
...this.getUTMParams(),
|
|
949
|
+
};
|
|
950
|
+
this.logger.debug(`Tracking event: ${eventType}`, event);
|
|
951
|
+
// Add to queue
|
|
952
|
+
this.queue.add(event);
|
|
953
|
+
}
|
|
954
|
+
catch (error) {
|
|
955
|
+
this.logger.error(`Failed to track event ${eventType}:`, error);
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
// Track page view
|
|
959
|
+
async trackPageView(pageData, tracking_user_id) {
|
|
960
|
+
const currentUrl = getCurrentUrl();
|
|
961
|
+
// Use URL without query string as key for debouncing
|
|
962
|
+
const pageKey = currentUrl.split("?")[0];
|
|
963
|
+
const now = Date.now();
|
|
964
|
+
const debounceInterval = this.config.pageViewDebounceInterval || 5000;
|
|
965
|
+
// Check if this page was tracked recently
|
|
966
|
+
const lastTrackTime = this.pageViewTrackTimes.get(pageKey);
|
|
967
|
+
if (lastTrackTime && now - lastTrackTime < debounceInterval) {
|
|
968
|
+
this.logger.debug(`Page view debounced: ${pageKey} (last tracked ${now - lastTrackTime}ms ago)`);
|
|
969
|
+
return;
|
|
970
|
+
}
|
|
971
|
+
const pageEventData = {
|
|
972
|
+
url: currentUrl,
|
|
973
|
+
title: getPageTitle(),
|
|
974
|
+
referrer: getReferrer(),
|
|
975
|
+
user_agent: getUserAgent(),
|
|
976
|
+
...pageData,
|
|
977
|
+
};
|
|
978
|
+
await this.trackEvent(EventType.PAGE_VIEW, pageEventData, tracking_user_id);
|
|
979
|
+
// Update last track time for this page
|
|
980
|
+
this.pageViewTrackTimes.set(pageKey, now);
|
|
981
|
+
// Clean up old entries to prevent memory leak (keep only entries from last hour)
|
|
982
|
+
this.cleanupPageViewTrackTimes();
|
|
983
|
+
}
|
|
984
|
+
// Clean up old page view track times to prevent memory leak
|
|
985
|
+
cleanupPageViewTrackTimes() {
|
|
986
|
+
const oneHourAgo = Date.now() - 60 * 60 * 1000;
|
|
987
|
+
for (const [pageKey, trackTime] of this.pageViewTrackTimes.entries()) {
|
|
988
|
+
if (trackTime < oneHourAgo) {
|
|
989
|
+
this.pageViewTrackTimes.delete(pageKey);
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
// Best-effort fetch of public IP address
|
|
994
|
+
async fetchPublicIP() {
|
|
995
|
+
try {
|
|
996
|
+
const controller = new AbortController();
|
|
997
|
+
const timeout = setTimeout(() => controller.abort(), 2000);
|
|
998
|
+
const response = await fetch("https://api.ipify.org?format=json", {
|
|
999
|
+
signal: controller.signal,
|
|
1000
|
+
headers: { Accept: "application/json" },
|
|
1001
|
+
});
|
|
1002
|
+
clearTimeout(timeout);
|
|
1003
|
+
if (!response.ok) {
|
|
1004
|
+
return null;
|
|
1005
|
+
}
|
|
1006
|
+
const data = await response.json();
|
|
1007
|
+
const ip = typeof data?.ip === "string" ? data.ip : null;
|
|
1008
|
+
return ip;
|
|
1009
|
+
}
|
|
1010
|
+
catch (error) {
|
|
1011
|
+
this.logger.debug("Public IP fetch failed", error);
|
|
1012
|
+
return null;
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
// Track purchase
|
|
1016
|
+
async trackPurchase(tracking_user_id, revenue, currency = Currency.USD, purchaseData) {
|
|
1017
|
+
await this.trackEvent(EventType.PURCHASE, purchaseData, tracking_user_id, revenue, currency);
|
|
1018
|
+
}
|
|
1019
|
+
// Track login
|
|
1020
|
+
async trackLogin(tracking_user_id, loginData) {
|
|
1021
|
+
await this.trackEvent(EventType.LOGIN, loginData, tracking_user_id);
|
|
1022
|
+
}
|
|
1023
|
+
// Track signup
|
|
1024
|
+
async trackSignup(tracking_user_id, signupData) {
|
|
1025
|
+
await this.trackEvent(EventType.SIGNUP, signupData, tracking_user_id);
|
|
1026
|
+
}
|
|
1027
|
+
// Track form submission
|
|
1028
|
+
async trackFormSubmit(tracking_user_id, formData) {
|
|
1029
|
+
await this.trackEvent(EventType.FORM_SUBMIT, formData, tracking_user_id);
|
|
1030
|
+
}
|
|
1031
|
+
// Get attribution data
|
|
1032
|
+
getAttributionData() {
|
|
1033
|
+
return this.storage.getUTMData();
|
|
1034
|
+
}
|
|
1035
|
+
// Manually add UTM parameters to a URL
|
|
1036
|
+
addUTMToURL(url) {
|
|
1037
|
+
if (!this.config.enableCrossDomainUTM) {
|
|
1038
|
+
return url;
|
|
1039
|
+
}
|
|
1040
|
+
const attributionData = this.getAttributionData();
|
|
1041
|
+
if (!attributionData) {
|
|
1042
|
+
return url;
|
|
1043
|
+
}
|
|
1044
|
+
const utmParams = filterUTMParams(attributionData.lastTouch, this.config.crossDomainUTMParams);
|
|
1045
|
+
return addUTMToURL$1(url, utmParams);
|
|
1046
|
+
}
|
|
1047
|
+
// Get current UTM parameters as object
|
|
1048
|
+
getCurrentUTMParams() {
|
|
1049
|
+
const attributionData = this.getAttributionData();
|
|
1050
|
+
if (!attributionData) {
|
|
1051
|
+
return {};
|
|
1052
|
+
}
|
|
1053
|
+
return filterUTMParams(attributionData.lastTouch, this.config.crossDomainUTMParams);
|
|
1054
|
+
}
|
|
1055
|
+
// Get UTM parameters for events
|
|
1056
|
+
getUTMParams() {
|
|
1057
|
+
const attributionData = this.getAttributionData();
|
|
1058
|
+
if (!attributionData) {
|
|
1059
|
+
this.logger.debug("No attribution data available for UTM params");
|
|
1060
|
+
return {};
|
|
1061
|
+
}
|
|
1062
|
+
const utmParams = {
|
|
1063
|
+
utm_source: attributionData.lastTouch.utm_source || null,
|
|
1064
|
+
utm_medium: attributionData.lastTouch.utm_medium || null,
|
|
1065
|
+
utm_campaign: attributionData.lastTouch.utm_campaign || null,
|
|
1066
|
+
utm_term: attributionData.lastTouch.utm_term || null,
|
|
1067
|
+
utm_content: attributionData.lastTouch.utm_content || null,
|
|
1068
|
+
};
|
|
1069
|
+
// Filter out empty values and return null for empty strings
|
|
1070
|
+
const filteredParams = {};
|
|
1071
|
+
if (utmParams.utm_source && utmParams.utm_source.trim() !== "") {
|
|
1072
|
+
filteredParams.utm_source = utmParams.utm_source;
|
|
1073
|
+
}
|
|
1074
|
+
else {
|
|
1075
|
+
filteredParams.utm_source = null;
|
|
1076
|
+
}
|
|
1077
|
+
if (utmParams.utm_medium && utmParams.utm_medium.trim() !== "") {
|
|
1078
|
+
filteredParams.utm_medium = utmParams.utm_medium;
|
|
1079
|
+
}
|
|
1080
|
+
else {
|
|
1081
|
+
filteredParams.utm_medium = null;
|
|
1082
|
+
}
|
|
1083
|
+
if (utmParams.utm_campaign && utmParams.utm_campaign.trim() !== "") {
|
|
1084
|
+
filteredParams.utm_campaign = utmParams.utm_campaign;
|
|
1085
|
+
}
|
|
1086
|
+
else {
|
|
1087
|
+
filteredParams.utm_campaign = null;
|
|
1088
|
+
}
|
|
1089
|
+
if (utmParams.utm_term && utmParams.utm_term.trim() !== "") {
|
|
1090
|
+
filteredParams.utm_term = utmParams.utm_term;
|
|
1091
|
+
}
|
|
1092
|
+
else {
|
|
1093
|
+
filteredParams.utm_term = null;
|
|
1094
|
+
}
|
|
1095
|
+
if (utmParams.utm_content && utmParams.utm_content.trim() !== "") {
|
|
1096
|
+
filteredParams.utm_content = utmParams.utm_content;
|
|
1097
|
+
}
|
|
1098
|
+
else {
|
|
1099
|
+
filteredParams.utm_content = null;
|
|
1100
|
+
}
|
|
1101
|
+
this.logger.debug("UTM params for event:", filteredParams);
|
|
1102
|
+
return filteredParams;
|
|
1103
|
+
}
|
|
1104
|
+
// Initialize user session
|
|
1105
|
+
initializeSession() {
|
|
1106
|
+
const existingSession = this.storage.getSession();
|
|
1107
|
+
const now = Date.now();
|
|
1108
|
+
if (existingSession &&
|
|
1109
|
+
now - existingSession.lastActivity < this.config.sessionTimeout) {
|
|
1110
|
+
// Extend existing session
|
|
1111
|
+
this.session = {
|
|
1112
|
+
...existingSession,
|
|
1113
|
+
lastActivity: now,
|
|
1114
|
+
};
|
|
1115
|
+
}
|
|
1116
|
+
else {
|
|
1117
|
+
// Create new session
|
|
1118
|
+
this.session = {
|
|
1119
|
+
sessionId: generateSessionId(),
|
|
1120
|
+
startTime: now,
|
|
1121
|
+
lastActivity: now,
|
|
1122
|
+
pageViews: 0,
|
|
1123
|
+
};
|
|
1124
|
+
}
|
|
1125
|
+
this.storage.storeSession(this.session);
|
|
1126
|
+
this.logger.debug("Session initialized:", this.session);
|
|
1127
|
+
}
|
|
1128
|
+
// Extract and store UTM data from current URL
|
|
1129
|
+
extractAndStoreUTMData() {
|
|
1130
|
+
const currentUrl = getCurrentUrl();
|
|
1131
|
+
const utmParams = extractUTMParams(currentUrl);
|
|
1132
|
+
this.logger.debug("Extracting UTM params from URL:", currentUrl);
|
|
1133
|
+
this.logger.debug("Found UTM params:", utmParams);
|
|
1134
|
+
if (Object.keys(utmParams).length === 0) {
|
|
1135
|
+
this.logger.debug("No UTM parameters found in URL");
|
|
1136
|
+
return;
|
|
1137
|
+
}
|
|
1138
|
+
const utmData = {
|
|
1139
|
+
utm_source: utmParams.utm_source || "",
|
|
1140
|
+
utm_medium: utmParams.utm_medium || "",
|
|
1141
|
+
utm_campaign: utmParams.utm_campaign || "",
|
|
1142
|
+
utm_term: utmParams.utm_term || "",
|
|
1143
|
+
utm_content: utmParams.utm_content || "",
|
|
1144
|
+
timestamp: Date.now(),
|
|
1145
|
+
};
|
|
1146
|
+
const existingData = this.getAttributionData();
|
|
1147
|
+
const newData = {
|
|
1148
|
+
firstTouch: existingData?.firstTouch || utmData,
|
|
1149
|
+
lastTouch: utmData,
|
|
1150
|
+
touchpoints: existingData?.touchpoints || [],
|
|
1151
|
+
expiresAt: Date.now() + 30 * 24 * 60 * 60 * 1000, // 30 days
|
|
1152
|
+
};
|
|
1153
|
+
// Add to touchpoints if different from last touch
|
|
1154
|
+
if (!existingData ||
|
|
1155
|
+
existingData.lastTouch.utm_source !== utmData.utm_source ||
|
|
1156
|
+
existingData.lastTouch.utm_campaign !== utmData.utm_campaign) {
|
|
1157
|
+
newData.touchpoints.push(utmData);
|
|
1158
|
+
}
|
|
1159
|
+
this.storage.storeUTMData(newData);
|
|
1160
|
+
this.logger.info("UTM data extracted and stored successfully:", utmData);
|
|
1161
|
+
// Clean URL after storing UTM data to avoid displaying parameters in plain text
|
|
1162
|
+
if (this.config.autoCleanUTM) {
|
|
1163
|
+
cleanURL();
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
// Setup auto-tracking
|
|
1167
|
+
setupAutoTracking() {
|
|
1168
|
+
this.autoTrackEnabled = true;
|
|
1169
|
+
// Track form interactions
|
|
1170
|
+
this.setupFormTracking();
|
|
1171
|
+
// Track link clicks
|
|
1172
|
+
this.setupLinkTracking();
|
|
1173
|
+
this.logger.info("Auto-tracking enabled");
|
|
1174
|
+
}
|
|
1175
|
+
// Setup form tracking
|
|
1176
|
+
setupFormTracking() {
|
|
1177
|
+
document.addEventListener("submit", (event) => {
|
|
1178
|
+
try {
|
|
1179
|
+
const form = event.target;
|
|
1180
|
+
if (!form) {
|
|
1181
|
+
return;
|
|
1182
|
+
}
|
|
1183
|
+
const fieldValues = this.serializeFormFields(form);
|
|
1184
|
+
this.trackEvent(EventType.FORM_SUBMIT, {
|
|
1185
|
+
...fieldValues,
|
|
1186
|
+
form_id: form.id || form.name,
|
|
1187
|
+
form_action: form.action,
|
|
1188
|
+
form_method: form.method,
|
|
1189
|
+
});
|
|
1190
|
+
}
|
|
1191
|
+
catch (error) {
|
|
1192
|
+
this.logger.error("Failed to auto-track form submit:", error);
|
|
1193
|
+
}
|
|
1194
|
+
});
|
|
1195
|
+
}
|
|
1196
|
+
// Serialize all fields within a form to a flat key-value map
|
|
1197
|
+
serializeFormFields(form) {
|
|
1198
|
+
const result = {};
|
|
1199
|
+
try {
|
|
1200
|
+
const formData = new FormData(form);
|
|
1201
|
+
// Accumulate multiple values for the same name as arrays
|
|
1202
|
+
for (const [name, value] of formData.entries()) {
|
|
1203
|
+
const serializedValue = this.serializeFormValue(value);
|
|
1204
|
+
if (Object.prototype.hasOwnProperty.call(result, name)) {
|
|
1205
|
+
const existing = result[name];
|
|
1206
|
+
if (Array.isArray(existing)) {
|
|
1207
|
+
existing.push(serializedValue);
|
|
1208
|
+
result[name] = existing;
|
|
1209
|
+
}
|
|
1210
|
+
else {
|
|
1211
|
+
result[name] = [existing, serializedValue];
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
else {
|
|
1215
|
+
result[name] = serializedValue;
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
// Ensure all named fields are represented, including unchecked radios/checkboxes
|
|
1219
|
+
const elements = Array.from(form.elements);
|
|
1220
|
+
// Group elements by name
|
|
1221
|
+
const nameToElements = new Map();
|
|
1222
|
+
for (const el of elements) {
|
|
1223
|
+
const name = el.name;
|
|
1224
|
+
if (!name || el.disabled)
|
|
1225
|
+
continue;
|
|
1226
|
+
if (!nameToElements.has(name))
|
|
1227
|
+
nameToElements.set(name, []);
|
|
1228
|
+
nameToElements.get(name).push(el);
|
|
1229
|
+
}
|
|
1230
|
+
nameToElements.forEach((els, name) => {
|
|
1231
|
+
// Determine the dominant type for a group (radio/checkbox take precedence)
|
|
1232
|
+
const hasRadio = els.some((e) => e.type === "radio");
|
|
1233
|
+
const hasCheckbox = els.some((e) => e.type === "checkbox");
|
|
1234
|
+
const hasFile = els.some((e) => e.type === "file");
|
|
1235
|
+
const hasSelectMultiple = els.some((e) => e.tagName === "SELECT" && e.multiple);
|
|
1236
|
+
const hasPassword = els.some((e) => e.type === "password");
|
|
1237
|
+
if (hasCheckbox) {
|
|
1238
|
+
const checkedValues = els
|
|
1239
|
+
.filter((e) => e.type === "checkbox")
|
|
1240
|
+
.filter((e) => e.checked)
|
|
1241
|
+
.map((e) => e.value || "on");
|
|
1242
|
+
if (!Object.prototype.hasOwnProperty.call(result, name)) {
|
|
1243
|
+
result[name] = checkedValues; // [] when none checked
|
|
1244
|
+
}
|
|
1245
|
+
else if (!Array.isArray(result[name])) {
|
|
1246
|
+
result[name] = [result[name], ...checkedValues];
|
|
1247
|
+
}
|
|
1248
|
+
return;
|
|
1249
|
+
}
|
|
1250
|
+
if (hasRadio) {
|
|
1251
|
+
const checked = els
|
|
1252
|
+
.filter((e) => e.type === "radio")
|
|
1253
|
+
.find((e) => e.checked);
|
|
1254
|
+
if (!Object.prototype.hasOwnProperty.call(result, name)) {
|
|
1255
|
+
result[name] = checked ? checked.value : null;
|
|
1256
|
+
}
|
|
1257
|
+
return;
|
|
1258
|
+
}
|
|
1259
|
+
if (hasSelectMultiple) {
|
|
1260
|
+
const select = els.find((e) => e.tagName === "SELECT" && e.multiple);
|
|
1261
|
+
if (select) {
|
|
1262
|
+
const selectedValues = Array.from(select.selectedOptions).map((opt) => opt.value);
|
|
1263
|
+
if (!Object.prototype.hasOwnProperty.call(result, name)) {
|
|
1264
|
+
result[name] = selectedValues; // [] when none selected
|
|
1265
|
+
}
|
|
1266
|
+
return;
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
if (hasFile) {
|
|
1270
|
+
const input = els.find((e) => e.type === "file");
|
|
1271
|
+
if (input) {
|
|
1272
|
+
const files = input.files ? Array.from(input.files) : [];
|
|
1273
|
+
if (!Object.prototype.hasOwnProperty.call(result, name)) {
|
|
1274
|
+
result[name] = files.map((f) => this.serializeFormValue(f));
|
|
1275
|
+
}
|
|
1276
|
+
return;
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
if (hasPassword) {
|
|
1280
|
+
// Mask password fields
|
|
1281
|
+
if (Object.prototype.hasOwnProperty.call(result, name)) {
|
|
1282
|
+
result[name] =
|
|
1283
|
+
typeof result[name] === "string" ? "*****" : result[name];
|
|
1284
|
+
}
|
|
1285
|
+
else {
|
|
1286
|
+
result[name] = "*****";
|
|
1287
|
+
}
|
|
1288
|
+
return;
|
|
1289
|
+
}
|
|
1290
|
+
// For other inputs/selects/textarea, ensure at least an entry exists
|
|
1291
|
+
if (!Object.prototype.hasOwnProperty.call(result, name)) {
|
|
1292
|
+
const first = els[0];
|
|
1293
|
+
if (first.tagName === "SELECT") {
|
|
1294
|
+
const select = first;
|
|
1295
|
+
result[name] = select.multiple
|
|
1296
|
+
? Array.from(select.selectedOptions).map((o) => o.value)
|
|
1297
|
+
: select.value;
|
|
1298
|
+
}
|
|
1299
|
+
else if (first.type) {
|
|
1300
|
+
result[name] = first.value ?? "";
|
|
1301
|
+
}
|
|
1302
|
+
else {
|
|
1303
|
+
result[name] = first.value ?? "";
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
});
|
|
1307
|
+
}
|
|
1308
|
+
catch (error) {
|
|
1309
|
+
this.logger.error("Failed to serialize form fields:", error);
|
|
1310
|
+
}
|
|
1311
|
+
return result;
|
|
1312
|
+
}
|
|
1313
|
+
// Convert FormData value to a serializable value
|
|
1314
|
+
serializeFormValue(value) {
|
|
1315
|
+
if (value instanceof File) {
|
|
1316
|
+
return {
|
|
1317
|
+
file_name: value.name,
|
|
1318
|
+
file_size: value.size,
|
|
1319
|
+
file_type: value.type,
|
|
1320
|
+
};
|
|
1321
|
+
}
|
|
1322
|
+
return value;
|
|
1323
|
+
}
|
|
1324
|
+
// Setup link tracking
|
|
1325
|
+
setupLinkTracking() {
|
|
1326
|
+
document.addEventListener("click", (event) => {
|
|
1327
|
+
const target = event.target;
|
|
1328
|
+
const link = target.closest("a");
|
|
1329
|
+
if (link) {
|
|
1330
|
+
const href = link.href;
|
|
1331
|
+
const isExternal = isExternalURL(href);
|
|
1332
|
+
if (isExternal) {
|
|
1333
|
+
// Handle cross-domain UTM passing
|
|
1334
|
+
this.handleCrossDomainUTM(link, event);
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
});
|
|
1338
|
+
}
|
|
1339
|
+
// Handle cross-domain UTM parameter passing
|
|
1340
|
+
handleCrossDomainUTM(link, event) {
|
|
1341
|
+
if (!this.config.enableCrossDomainUTM) {
|
|
1342
|
+
return;
|
|
1343
|
+
}
|
|
1344
|
+
const href = link.href;
|
|
1345
|
+
// Check if domain should be excluded
|
|
1346
|
+
if (shouldExcludeDomain(href, this.config.excludeDomains)) {
|
|
1347
|
+
this.logger.debug(`Domain excluded from UTM passing: ${href}`);
|
|
1348
|
+
return;
|
|
1349
|
+
}
|
|
1350
|
+
// Get current UTM data
|
|
1351
|
+
const attributionData = this.getAttributionData();
|
|
1352
|
+
if (!attributionData) {
|
|
1353
|
+
this.logger.debug("No UTM data available for cross-domain passing");
|
|
1354
|
+
return;
|
|
1355
|
+
}
|
|
1356
|
+
// Filter UTM parameters based on configuration
|
|
1357
|
+
const utmParams = filterUTMParams(attributionData.lastTouch, this.config.crossDomainUTMParams);
|
|
1358
|
+
if (Object.keys(utmParams).length === 0) {
|
|
1359
|
+
this.logger.debug("No UTM parameters to pass");
|
|
1360
|
+
return;
|
|
1361
|
+
}
|
|
1362
|
+
// Add UTM parameters to URL
|
|
1363
|
+
const enhancedURL = addUTMToURL$1(href, utmParams);
|
|
1364
|
+
if (enhancedURL !== href) {
|
|
1365
|
+
// Update the link href
|
|
1366
|
+
link.href = enhancedURL;
|
|
1367
|
+
this.logger.debug("UTM parameters added to external link:", {
|
|
1368
|
+
original: href,
|
|
1369
|
+
enhanced: enhancedURL,
|
|
1370
|
+
utmParams,
|
|
1371
|
+
});
|
|
1372
|
+
this.logger.debug("Cross-domain UTM passed:", {
|
|
1373
|
+
link_url: enhancedURL,
|
|
1374
|
+
original_url: href,
|
|
1375
|
+
utm_params_passed: utmParams,
|
|
1376
|
+
});
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
// Setup network handlers
|
|
1380
|
+
setupNetworkHandlers() {
|
|
1381
|
+
window.addEventListener("online", () => {
|
|
1382
|
+
this.logger.info("Network connection restored");
|
|
1383
|
+
this.queue.flush();
|
|
1384
|
+
});
|
|
1385
|
+
window.addEventListener("offline", () => {
|
|
1386
|
+
this.logger.warn("Network connection lost");
|
|
1387
|
+
});
|
|
1388
|
+
}
|
|
1389
|
+
// Setup visibility handlers
|
|
1390
|
+
setupVisibilityHandlers() {
|
|
1391
|
+
document.addEventListener("visibilitychange", () => {
|
|
1392
|
+
if (document.visibilityState === "visible") {
|
|
1393
|
+
this.updateSessionActivity();
|
|
1394
|
+
}
|
|
1395
|
+
});
|
|
1396
|
+
}
|
|
1397
|
+
// Setup beforeunload handler
|
|
1398
|
+
setupBeforeUnloadHandler() {
|
|
1399
|
+
window.addEventListener("beforeunload", () => {
|
|
1400
|
+
this.updateSessionActivity();
|
|
1401
|
+
this.queue.flush();
|
|
1402
|
+
});
|
|
1403
|
+
}
|
|
1404
|
+
// Get current path (pathname + search) for comparison
|
|
1405
|
+
getCurrentPath() {
|
|
1406
|
+
if (typeof window === "undefined")
|
|
1407
|
+
return "";
|
|
1408
|
+
return window.location.pathname + window.location.search;
|
|
1409
|
+
}
|
|
1410
|
+
// Setup SPA (Single Page Application) route tracking
|
|
1411
|
+
setupSPATracking() {
|
|
1412
|
+
if (typeof window === "undefined" || typeof history === "undefined") {
|
|
1413
|
+
this.logger.warn("⚠️ SPA tracking not available in this environment");
|
|
1414
|
+
return;
|
|
1415
|
+
}
|
|
1416
|
+
if (this.spaTrackingEnabled) {
|
|
1417
|
+
this.logger.warn("⚠️ SPA tracking already enabled");
|
|
1418
|
+
return;
|
|
1419
|
+
}
|
|
1420
|
+
this.spaTrackingEnabled = true;
|
|
1421
|
+
this.lastTrackedPath = this.getCurrentPath();
|
|
1422
|
+
// Store original methods
|
|
1423
|
+
this.originalPushState = history.pushState.bind(history);
|
|
1424
|
+
this.originalReplaceState = history.replaceState.bind(history);
|
|
1425
|
+
// Create a handler for route changes
|
|
1426
|
+
const handleRouteChange = (changeType) => {
|
|
1427
|
+
const newPath = this.getCurrentPath();
|
|
1428
|
+
// Only track if path actually changed
|
|
1429
|
+
if (newPath === this.lastTrackedPath) {
|
|
1430
|
+
this.logger.debug(`🔄 [SPA] Route change detected (${changeType}) but path unchanged: ${newPath}`);
|
|
1431
|
+
return;
|
|
1432
|
+
}
|
|
1433
|
+
this.logger.debug(`🔄 [SPA] Route change detected (${changeType}): ${this.lastTrackedPath} -> ${newPath}`);
|
|
1434
|
+
this.lastTrackedPath = newPath;
|
|
1435
|
+
// Delay tracking to allow page title and content to update (100ms is sufficient for most frameworks)
|
|
1436
|
+
setTimeout(() => {
|
|
1437
|
+
this.trackPageView()
|
|
1438
|
+
.then(() => {
|
|
1439
|
+
this.logger.debug(`✅ [SPA] Page view tracked for: ${newPath}`);
|
|
1440
|
+
})
|
|
1441
|
+
.catch((error) => {
|
|
1442
|
+
this.logger.error(`❌ [SPA] Failed to track page view:`, error);
|
|
1443
|
+
});
|
|
1444
|
+
}, 100);
|
|
1445
|
+
};
|
|
1446
|
+
// Override history.pushState
|
|
1447
|
+
history.pushState = (data, unused, url) => {
|
|
1448
|
+
const result = this.originalPushState(data, unused, url);
|
|
1449
|
+
handleRouteChange("pushState");
|
|
1450
|
+
return result;
|
|
1451
|
+
};
|
|
1452
|
+
// Override history.replaceState
|
|
1453
|
+
history.replaceState = (data, unused, url) => {
|
|
1454
|
+
const result = this.originalReplaceState(data, unused, url);
|
|
1455
|
+
handleRouteChange("replaceState");
|
|
1456
|
+
return result;
|
|
1457
|
+
};
|
|
1458
|
+
// Listen for popstate (browser back/forward)
|
|
1459
|
+
this.popstateHandler = () => {
|
|
1460
|
+
handleRouteChange("popstate");
|
|
1461
|
+
};
|
|
1462
|
+
window.addEventListener("popstate", this.popstateHandler);
|
|
1463
|
+
this.logger.info("🔄 SPA tracking setup completed");
|
|
1464
|
+
}
|
|
1465
|
+
// Cleanup SPA tracking (restore original methods)
|
|
1466
|
+
cleanupSPATracking() {
|
|
1467
|
+
if (!this.spaTrackingEnabled)
|
|
1468
|
+
return;
|
|
1469
|
+
// Restore original history methods
|
|
1470
|
+
if (this.originalPushState) {
|
|
1471
|
+
history.pushState = this.originalPushState;
|
|
1472
|
+
this.originalPushState = null;
|
|
1473
|
+
}
|
|
1474
|
+
if (this.originalReplaceState) {
|
|
1475
|
+
history.replaceState = this.originalReplaceState;
|
|
1476
|
+
this.originalReplaceState = null;
|
|
1477
|
+
}
|
|
1478
|
+
// Remove popstate listener
|
|
1479
|
+
if (this.popstateHandler) {
|
|
1480
|
+
window.removeEventListener("popstate", this.popstateHandler);
|
|
1481
|
+
this.popstateHandler = null;
|
|
1482
|
+
}
|
|
1483
|
+
this.spaTrackingEnabled = false;
|
|
1484
|
+
this.logger.info("🔄 SPA tracking cleaned up");
|
|
1485
|
+
}
|
|
1486
|
+
// Update session activity
|
|
1487
|
+
updateSessionActivity() {
|
|
1488
|
+
if (this.session) {
|
|
1489
|
+
this.session.lastActivity = Date.now();
|
|
1490
|
+
this.storage.storeSession(this.session);
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
// Flush all pending events
|
|
1494
|
+
async flush() {
|
|
1495
|
+
await this.queue.flush();
|
|
1496
|
+
}
|
|
1497
|
+
// Get SDK status
|
|
1498
|
+
getStatus() {
|
|
1499
|
+
return {
|
|
1500
|
+
initialized: this.initialized,
|
|
1501
|
+
session: this.session,
|
|
1502
|
+
queueSize: this.queue.size(),
|
|
1503
|
+
online: isOnline(),
|
|
1504
|
+
crossDomainUTM: {
|
|
1505
|
+
enabled: this.config.enableCrossDomainUTM || true,
|
|
1506
|
+
currentParams: this.getCurrentUTMParams(),
|
|
1507
|
+
},
|
|
1508
|
+
spaTracking: {
|
|
1509
|
+
enabled: this.spaTrackingEnabled,
|
|
1510
|
+
currentPath: this.lastTrackedPath,
|
|
1511
|
+
},
|
|
1512
|
+
};
|
|
1513
|
+
}
|
|
1514
|
+
// Destroy SDK instance
|
|
1515
|
+
destroy() {
|
|
1516
|
+
this.queue.clear();
|
|
1517
|
+
this.autoTrackEnabled = false;
|
|
1518
|
+
this.cleanupSPATracking();
|
|
1519
|
+
this.initialized = false;
|
|
1520
|
+
this.logger.info("🗑️ SDK destroyed");
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
// Global SDK instance
|
|
1525
|
+
let globalSDK = null;
|
|
1526
|
+
// Auto-initialization function
|
|
1527
|
+
function autoInit() {
|
|
1528
|
+
// Check if SDK is already initialized
|
|
1529
|
+
if (globalSDK) {
|
|
1530
|
+
return;
|
|
1531
|
+
}
|
|
1532
|
+
// Look for configuration in script tag
|
|
1533
|
+
const script = document.currentScript;
|
|
1534
|
+
if (!script) {
|
|
1535
|
+
console.warn("GetuAI SDK: Could not find script tag for auto-initialization");
|
|
1536
|
+
return;
|
|
1537
|
+
}
|
|
1538
|
+
const apiKey = script.getAttribute("data-api-key");
|
|
1539
|
+
if (!apiKey) {
|
|
1540
|
+
console.warn("GetuAI SDK: No API key provided. Please add data-api-key attribute to script tag.");
|
|
1541
|
+
return;
|
|
1542
|
+
}
|
|
1543
|
+
const apiEndpoint = script.getAttribute("data-api-endpoint") || defaultEndpoint;
|
|
1544
|
+
const config = {
|
|
1545
|
+
apiKey,
|
|
1546
|
+
apiEndpoint,
|
|
1547
|
+
enableDebug: script.getAttribute("data-debug") === "true",
|
|
1548
|
+
autoTrack: script.getAttribute("data-auto-track") === "true",
|
|
1549
|
+
autoTrackPageView: script.getAttribute("data-auto-track-page-view") === "true",
|
|
1550
|
+
autoCleanUTM: script.getAttribute("data-auto-clean-utm") !== "false",
|
|
1551
|
+
batchSize: parseInt(script.getAttribute("data-batch-size") || "100"),
|
|
1552
|
+
batchInterval: parseInt(script.getAttribute("data-batch-interval") || "5000"),
|
|
1553
|
+
};
|
|
1554
|
+
// Initialize SDK
|
|
1555
|
+
init(config);
|
|
1556
|
+
}
|
|
1557
|
+
// Initialize SDK with configuration
|
|
1558
|
+
async function init(config) {
|
|
1559
|
+
if (globalSDK) {
|
|
1560
|
+
console.warn("GetuAI SDK: Already initialized");
|
|
1561
|
+
return globalSDK;
|
|
1562
|
+
}
|
|
1563
|
+
try {
|
|
1564
|
+
globalSDK = new AttributionSDK(config);
|
|
1565
|
+
await globalSDK.init();
|
|
1566
|
+
// Expose SDK to global scope for debugging
|
|
1567
|
+
if (config.enableDebug) {
|
|
1568
|
+
window.GetuAISDK = globalSDK;
|
|
1569
|
+
}
|
|
1570
|
+
console.log("GetuAI Attribution SDK initialized successfully");
|
|
1571
|
+
// Dispatch initialization complete event
|
|
1572
|
+
if (typeof window !== "undefined") {
|
|
1573
|
+
const event = new CustomEvent("getuaiSDKReady", {
|
|
1574
|
+
detail: { sdk: globalSDK },
|
|
1575
|
+
});
|
|
1576
|
+
window.dispatchEvent(event);
|
|
1577
|
+
}
|
|
1578
|
+
return globalSDK;
|
|
1579
|
+
}
|
|
1580
|
+
catch (error) {
|
|
1581
|
+
console.error("GetuAI SDK: Failed to initialize:", error);
|
|
1582
|
+
// Dispatch initialization error event
|
|
1583
|
+
if (typeof window !== "undefined") {
|
|
1584
|
+
const event = new CustomEvent("getuaiSDKError", {
|
|
1585
|
+
detail: { error },
|
|
1586
|
+
});
|
|
1587
|
+
window.dispatchEvent(event);
|
|
1588
|
+
}
|
|
1589
|
+
throw error;
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
// Get current SDK instance
|
|
1593
|
+
function getSDK() {
|
|
1594
|
+
return globalSDK;
|
|
1595
|
+
}
|
|
1596
|
+
// Wait for SDK to be ready
|
|
1597
|
+
function waitForSDK() {
|
|
1598
|
+
return new Promise((resolve, reject) => {
|
|
1599
|
+
if (globalSDK) {
|
|
1600
|
+
resolve(globalSDK);
|
|
1601
|
+
return;
|
|
1602
|
+
}
|
|
1603
|
+
// Listen for SDK ready event
|
|
1604
|
+
const handleReady = (event) => {
|
|
1605
|
+
window.removeEventListener("getuaiSDKReady", handleReady);
|
|
1606
|
+
window.removeEventListener("getuaiSDKError", handleError);
|
|
1607
|
+
resolve(event.detail.sdk);
|
|
1608
|
+
};
|
|
1609
|
+
const handleError = (event) => {
|
|
1610
|
+
window.removeEventListener("getuaiSDKReady", handleReady);
|
|
1611
|
+
window.removeEventListener("getuaiSDKError", handleError);
|
|
1612
|
+
reject(event.detail.error);
|
|
1613
|
+
};
|
|
1614
|
+
window.addEventListener("getuaiSDKReady", handleReady);
|
|
1615
|
+
window.addEventListener("getuaiSDKError", handleError);
|
|
1616
|
+
// Timeout after 10 seconds
|
|
1617
|
+
setTimeout(() => {
|
|
1618
|
+
window.removeEventListener("getuaiSDKReady", handleReady);
|
|
1619
|
+
window.removeEventListener("getuaiSDKError", handleError);
|
|
1620
|
+
reject(new Error("SDK initialization timeout"));
|
|
1621
|
+
}, 10000);
|
|
1622
|
+
});
|
|
1623
|
+
}
|
|
1624
|
+
// Track event
|
|
1625
|
+
async function trackEvent(eventType, eventData, tracking_user_id, revenue, currency = Currency.USD) {
|
|
1626
|
+
const sdk = getSDK();
|
|
1627
|
+
if (!sdk) {
|
|
1628
|
+
console.warn("GetuAI SDK: Not initialized. Call init() first.");
|
|
1629
|
+
return;
|
|
1630
|
+
}
|
|
1631
|
+
await sdk.trackEvent(eventType, eventData, tracking_user_id, revenue, currency);
|
|
1632
|
+
}
|
|
1633
|
+
// Track page view
|
|
1634
|
+
async function trackPageView(pageData, tracking_user_id) {
|
|
1635
|
+
const sdk = getSDK();
|
|
1636
|
+
if (!sdk) {
|
|
1637
|
+
console.warn("GetuAI SDK: Not initialized. Call init() first.");
|
|
1638
|
+
return;
|
|
1639
|
+
}
|
|
1640
|
+
await sdk.trackPageView(pageData, tracking_user_id);
|
|
1641
|
+
}
|
|
1642
|
+
// Track purchase
|
|
1643
|
+
async function trackPurchase(tracking_user_id, revenue, currency = Currency.USD, purchaseData) {
|
|
1644
|
+
const sdk = getSDK();
|
|
1645
|
+
if (!sdk) {
|
|
1646
|
+
console.warn("GetuAI SDK: Not initialized. Call init() first.");
|
|
1647
|
+
return;
|
|
1648
|
+
}
|
|
1649
|
+
await sdk.trackPurchase(tracking_user_id, revenue, currency, purchaseData);
|
|
1650
|
+
}
|
|
1651
|
+
// Track login
|
|
1652
|
+
async function trackLogin(tracking_user_id, loginData) {
|
|
1653
|
+
const sdk = getSDK();
|
|
1654
|
+
if (!sdk) {
|
|
1655
|
+
console.warn("GetuAI SDK: Not initialized. Call init() first.");
|
|
1656
|
+
return;
|
|
1657
|
+
}
|
|
1658
|
+
await sdk.trackLogin(tracking_user_id, loginData);
|
|
1659
|
+
}
|
|
1660
|
+
// Track signup
|
|
1661
|
+
async function trackSignup(tracking_user_id, signupData) {
|
|
1662
|
+
const sdk = getSDK();
|
|
1663
|
+
if (!sdk) {
|
|
1664
|
+
console.warn("GetuAI SDK: Not initialized. Call init() first.");
|
|
1665
|
+
return;
|
|
1666
|
+
}
|
|
1667
|
+
await sdk.trackSignup(tracking_user_id, signupData);
|
|
1668
|
+
}
|
|
1669
|
+
// Track form submission
|
|
1670
|
+
async function trackFormSubmit(tracking_user_id, formData) {
|
|
1671
|
+
const sdk = getSDK();
|
|
1672
|
+
if (!sdk) {
|
|
1673
|
+
console.warn("GetuAI SDK: Not initialized. Call init() first.");
|
|
1674
|
+
return;
|
|
1675
|
+
}
|
|
1676
|
+
await sdk.trackFormSubmit(tracking_user_id, formData);
|
|
1677
|
+
}
|
|
1678
|
+
// Get attribution data
|
|
1679
|
+
function getAttributionData() {
|
|
1680
|
+
const sdk = getSDK();
|
|
1681
|
+
if (!sdk) {
|
|
1682
|
+
return null;
|
|
1683
|
+
}
|
|
1684
|
+
return sdk.getAttributionData();
|
|
1685
|
+
}
|
|
1686
|
+
// Flush pending events
|
|
1687
|
+
async function flush() {
|
|
1688
|
+
const sdk = getSDK();
|
|
1689
|
+
if (!sdk) {
|
|
1690
|
+
console.warn("GetuAI SDK: Not initialized. Call init() first.");
|
|
1691
|
+
return;
|
|
1692
|
+
}
|
|
1693
|
+
await sdk.flush();
|
|
1694
|
+
}
|
|
1695
|
+
// Get SDK status
|
|
1696
|
+
function getStatus() {
|
|
1697
|
+
const sdk = getSDK();
|
|
1698
|
+
if (!sdk) {
|
|
1699
|
+
return null;
|
|
1700
|
+
}
|
|
1701
|
+
return sdk.getStatus();
|
|
1702
|
+
}
|
|
1703
|
+
// Add UTM to URL
|
|
1704
|
+
function addUTMToURL(url) {
|
|
1705
|
+
const sdk = getSDK();
|
|
1706
|
+
if (!sdk) {
|
|
1707
|
+
console.warn("GetuAI SDK: Not initialized. Call init() first.");
|
|
1708
|
+
return url;
|
|
1709
|
+
}
|
|
1710
|
+
return sdk.addUTMToURL(url);
|
|
1711
|
+
}
|
|
1712
|
+
// Get current UTM parameters
|
|
1713
|
+
function getCurrentUTMParams() {
|
|
1714
|
+
const sdk = getSDK();
|
|
1715
|
+
if (!sdk) {
|
|
1716
|
+
return {};
|
|
1717
|
+
}
|
|
1718
|
+
return sdk.getCurrentUTMParams();
|
|
1719
|
+
}
|
|
1720
|
+
// Destroy SDK
|
|
1721
|
+
function destroy() {
|
|
1722
|
+
if (globalSDK) {
|
|
1723
|
+
globalSDK.destroy();
|
|
1724
|
+
globalSDK = null;
|
|
1725
|
+
console.log("GetuAI SDK destroyed");
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
// Extend AttributionSDK class with static methods
|
|
1729
|
+
class AttributionSDKStatic extends AttributionSDK {
|
|
1730
|
+
// Static method to initialize SDK
|
|
1731
|
+
static async init(config) {
|
|
1732
|
+
return await init(config);
|
|
1733
|
+
}
|
|
1734
|
+
// Static method to track event
|
|
1735
|
+
static async trackEvent(eventType, eventData, tracking_user_id, revenue, currency = Currency.USD) {
|
|
1736
|
+
return await trackEvent(eventType, eventData, tracking_user_id, revenue, currency);
|
|
1737
|
+
}
|
|
1738
|
+
// Static method to track page view
|
|
1739
|
+
static async trackPageView(pageData, tracking_user_id) {
|
|
1740
|
+
return await trackPageView(pageData, tracking_user_id);
|
|
1741
|
+
}
|
|
1742
|
+
// Static method to track purchase
|
|
1743
|
+
static async trackPurchase(tracking_user_id, revenue, currency = Currency.USD, purchaseData) {
|
|
1744
|
+
return await trackPurchase(tracking_user_id, revenue, currency, purchaseData);
|
|
1745
|
+
}
|
|
1746
|
+
// Static method to track login
|
|
1747
|
+
static async trackLogin(tracking_user_id, loginData) {
|
|
1748
|
+
return await trackLogin(tracking_user_id, loginData);
|
|
1749
|
+
}
|
|
1750
|
+
// Static method to track signup
|
|
1751
|
+
static async trackSignup(tracking_user_id, signupData) {
|
|
1752
|
+
return await trackSignup(tracking_user_id, signupData);
|
|
1753
|
+
}
|
|
1754
|
+
// Static method to track form submission
|
|
1755
|
+
static async trackFormSubmit(tracking_user_id, formData) {
|
|
1756
|
+
return await trackFormSubmit(tracking_user_id, formData);
|
|
1757
|
+
}
|
|
1758
|
+
// Static method to get attribution data
|
|
1759
|
+
static getAttributionData() {
|
|
1760
|
+
return getAttributionData();
|
|
1761
|
+
}
|
|
1762
|
+
// Static method to flush events
|
|
1763
|
+
static async flush() {
|
|
1764
|
+
return await flush();
|
|
1765
|
+
}
|
|
1766
|
+
// Static method to get status
|
|
1767
|
+
static getStatus() {
|
|
1768
|
+
return getStatus();
|
|
1769
|
+
}
|
|
1770
|
+
// Static method to add UTM to URL
|
|
1771
|
+
static addUTMToURL(url) {
|
|
1772
|
+
return addUTMToURL(url);
|
|
1773
|
+
}
|
|
1774
|
+
// Static method to get current UTM parameters
|
|
1775
|
+
static getCurrentUTMParams() {
|
|
1776
|
+
return getCurrentUTMParams();
|
|
1777
|
+
}
|
|
1778
|
+
// Static method to destroy SDK
|
|
1779
|
+
static destroy() {
|
|
1780
|
+
destroy();
|
|
1781
|
+
}
|
|
1782
|
+
// Static method to get SDK instance
|
|
1783
|
+
static getSDK() {
|
|
1784
|
+
return getSDK();
|
|
1785
|
+
}
|
|
1786
|
+
// Static method to wait for SDK
|
|
1787
|
+
static waitForSDK() {
|
|
1788
|
+
return waitForSDK();
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
// Auto-initialize if script is loaded with data-api-key
|
|
1792
|
+
if (typeof document !== "undefined") {
|
|
1793
|
+
// Initialize immediately when script loads, don't wait for DOMContentLoaded
|
|
1794
|
+
// This ensures SDK is ready before any body scripts execute
|
|
1795
|
+
autoInit();
|
|
1796
|
+
}
|
|
1797
|
+
// Expose functions to global scope for browser usage
|
|
1798
|
+
if (typeof window !== "undefined") {
|
|
1799
|
+
window.getuaiSDK = {
|
|
1800
|
+
init,
|
|
1801
|
+
getSDK,
|
|
1802
|
+
waitForSDK,
|
|
1803
|
+
trackEvent,
|
|
1804
|
+
trackPageView,
|
|
1805
|
+
trackPurchase,
|
|
1806
|
+
trackLogin,
|
|
1807
|
+
trackSignup,
|
|
1808
|
+
trackFormSubmit,
|
|
1809
|
+
getAttributionData,
|
|
1810
|
+
flush,
|
|
1811
|
+
getStatus,
|
|
1812
|
+
addUTMToURL,
|
|
1813
|
+
getCurrentUTMParams,
|
|
1814
|
+
destroy,
|
|
1815
|
+
EventType,
|
|
1816
|
+
Currency,
|
|
1817
|
+
AttributionSDK: AttributionSDKStatic, // 暴露带静态方法的AttributionSDK类
|
|
1818
|
+
};
|
|
1819
|
+
// Also expose individual functions for backward compatibility
|
|
1820
|
+
window.init = init;
|
|
1821
|
+
window.waitForSDK = waitForSDK;
|
|
1822
|
+
window.trackEvent = trackEvent;
|
|
1823
|
+
window.trackPageView = trackPageView;
|
|
1824
|
+
window.trackPurchase = trackPurchase;
|
|
1825
|
+
window.trackLogin = trackLogin;
|
|
1826
|
+
window.trackSignup = trackSignup;
|
|
1827
|
+
window.trackFormSubmit = trackFormSubmit;
|
|
1828
|
+
window.getAttributionData = getAttributionData;
|
|
1829
|
+
window.flush = flush;
|
|
1830
|
+
window.getStatus = getStatus;
|
|
1831
|
+
window.addUTMToURL = addUTMToURL;
|
|
1832
|
+
window.getCurrentUTMParams = getCurrentUTMParams;
|
|
1833
|
+
window.destroy = destroy;
|
|
1834
|
+
window.AttributionSDK = AttributionSDKStatic; // 直接暴露带静态方法的AttributionSDK类
|
|
1835
|
+
}
|
|
1836
|
+
// Default export
|
|
1837
|
+
var index = {
|
|
1838
|
+
init,
|
|
1839
|
+
getSDK,
|
|
1840
|
+
waitForSDK,
|
|
1841
|
+
trackEvent,
|
|
1842
|
+
trackPageView,
|
|
1843
|
+
trackPurchase,
|
|
1844
|
+
trackLogin,
|
|
1845
|
+
trackSignup,
|
|
1846
|
+
trackFormSubmit,
|
|
1847
|
+
getAttributionData,
|
|
1848
|
+
flush,
|
|
1849
|
+
getStatus,
|
|
1850
|
+
addUTMToURL,
|
|
1851
|
+
getCurrentUTMParams,
|
|
1852
|
+
destroy,
|
|
1853
|
+
EventType,
|
|
1854
|
+
Currency,
|
|
1855
|
+
AttributionSDK: AttributionSDKStatic, // 包含带静态方法的AttributionSDK类
|
|
1856
|
+
};
|
|
1857
|
+
|
|
1858
|
+
export { AttributionSDKStatic as AttributionSDK, Currency, EventType, addUTMToURL, index as default, destroy, flush, getAttributionData, getCurrentUTMParams, getSDK, getStatus, init, trackEvent, trackFormSubmit, trackLogin, trackPageView, trackPurchase, trackSignup, waitForSDK };
|
|
1859
|
+
//# sourceMappingURL=index.esm.js.map
|