@clianta/sdk 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/CHANGELOG.md +96 -0
- package/LICENSE +21 -0
- package/README.md +303 -0
- package/dist/clianta.cjs.js +1809 -0
- package/dist/clianta.cjs.js.map +1 -0
- package/dist/clianta.esm.js +1801 -0
- package/dist/clianta.esm.js.map +1 -0
- package/dist/clianta.umd.js +1815 -0
- package/dist/clianta.umd.js.map +1 -0
- package/dist/clianta.umd.min.js +7 -0
- package/dist/clianta.umd.min.js.map +1 -0
- package/dist/index.d.ts +353 -0
- package/package.json +69 -0
|
@@ -0,0 +1,1815 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Clianta SDK v1.0.0
|
|
3
|
+
* (c) 2026 Clianta
|
|
4
|
+
* Released under the MIT License.
|
|
5
|
+
*/
|
|
6
|
+
(function (global, factory) {
|
|
7
|
+
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
|
|
8
|
+
typeof define === 'function' && define.amd ? define(['exports'], factory) :
|
|
9
|
+
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.Clianta = {}));
|
|
10
|
+
})(this, (function (exports) { 'use strict';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Clianta SDK - Configuration
|
|
14
|
+
* @version 1.0.0
|
|
15
|
+
*/
|
|
16
|
+
/** SDK Version */
|
|
17
|
+
const SDK_VERSION = '1.0.0';
|
|
18
|
+
/** Default API endpoint based on environment */
|
|
19
|
+
const getDefaultApiEndpoint = () => {
|
|
20
|
+
if (typeof window === 'undefined')
|
|
21
|
+
return 'https://api.clianta.online';
|
|
22
|
+
const hostname = window.location.hostname;
|
|
23
|
+
if (hostname.includes('localhost') || hostname.includes('127.0.0.1')) {
|
|
24
|
+
return 'http://localhost:5000';
|
|
25
|
+
}
|
|
26
|
+
return 'https://api.clianta.online';
|
|
27
|
+
};
|
|
28
|
+
/** Core plugins enabled by default */
|
|
29
|
+
const DEFAULT_PLUGINS = [
|
|
30
|
+
'pageView',
|
|
31
|
+
'forms',
|
|
32
|
+
'scroll',
|
|
33
|
+
'clicks',
|
|
34
|
+
'engagement',
|
|
35
|
+
'downloads',
|
|
36
|
+
'exitIntent',
|
|
37
|
+
];
|
|
38
|
+
/** Default configuration values */
|
|
39
|
+
const DEFAULT_CONFIG = {
|
|
40
|
+
apiEndpoint: getDefaultApiEndpoint(),
|
|
41
|
+
debug: false,
|
|
42
|
+
autoPageView: true,
|
|
43
|
+
plugins: DEFAULT_PLUGINS,
|
|
44
|
+
sessionTimeout: 30 * 60 * 1000, // 30 minutes
|
|
45
|
+
batchSize: 10,
|
|
46
|
+
flushInterval: 5000, // 5 seconds
|
|
47
|
+
consent: {
|
|
48
|
+
defaultConsent: { analytics: true, marketing: false, personalization: false },
|
|
49
|
+
waitForConsent: false,
|
|
50
|
+
storageKey: 'mb_consent',
|
|
51
|
+
},
|
|
52
|
+
cookieDomain: '',
|
|
53
|
+
useCookies: false,
|
|
54
|
+
};
|
|
55
|
+
/** Storage keys */
|
|
56
|
+
const STORAGE_KEYS = {
|
|
57
|
+
VISITOR_ID: 'mb_vid',
|
|
58
|
+
SESSION_ID: 'mb_sid',
|
|
59
|
+
SESSION_TIMESTAMP: 'mb_st',
|
|
60
|
+
CONSENT: 'mb_consent',
|
|
61
|
+
EVENT_QUEUE: 'mb_queue',
|
|
62
|
+
};
|
|
63
|
+
/** Scroll depth milestones to track */
|
|
64
|
+
const SCROLL_MILESTONES = [25, 50, 75, 100];
|
|
65
|
+
/** File extensions to track as downloads */
|
|
66
|
+
const DOWNLOAD_EXTENSIONS = [
|
|
67
|
+
'.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx',
|
|
68
|
+
'.zip', '.rar', '.tar', '.gz', '.7z',
|
|
69
|
+
'.csv', '.txt', '.json', '.xml',
|
|
70
|
+
'.mp3', '.mp4', '.wav', '.avi', '.mov',
|
|
71
|
+
];
|
|
72
|
+
/**
|
|
73
|
+
* Merge user config with defaults
|
|
74
|
+
*/
|
|
75
|
+
function mergeConfig(userConfig = {}) {
|
|
76
|
+
return {
|
|
77
|
+
...DEFAULT_CONFIG,
|
|
78
|
+
...userConfig,
|
|
79
|
+
consent: {
|
|
80
|
+
...DEFAULT_CONFIG.consent,
|
|
81
|
+
...userConfig.consent,
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* MorrisB Tracking SDK - Debug Logger
|
|
88
|
+
* @version 3.0.0
|
|
89
|
+
*/
|
|
90
|
+
const LOG_PREFIX = '[Clianta]';
|
|
91
|
+
const LOG_STYLES = {
|
|
92
|
+
debug: 'color: #6b7280; font-weight: normal;',
|
|
93
|
+
info: 'color: #3b82f6; font-weight: normal;',
|
|
94
|
+
warn: 'color: #f59e0b; font-weight: bold;',
|
|
95
|
+
error: 'color: #ef4444; font-weight: bold;',
|
|
96
|
+
};
|
|
97
|
+
const LOG_LEVELS = {
|
|
98
|
+
debug: 0,
|
|
99
|
+
info: 1,
|
|
100
|
+
warn: 2,
|
|
101
|
+
error: 3,
|
|
102
|
+
};
|
|
103
|
+
/**
|
|
104
|
+
* Create a logger instance
|
|
105
|
+
*/
|
|
106
|
+
function createLogger(enabled = false) {
|
|
107
|
+
let currentLevel = 'debug';
|
|
108
|
+
let isEnabled = enabled;
|
|
109
|
+
const shouldLog = (level) => {
|
|
110
|
+
if (!isEnabled)
|
|
111
|
+
return false;
|
|
112
|
+
return LOG_LEVELS[level] >= LOG_LEVELS[currentLevel];
|
|
113
|
+
};
|
|
114
|
+
const formatArgs = (level, args) => {
|
|
115
|
+
if (typeof console !== 'undefined' && typeof window !== 'undefined') {
|
|
116
|
+
// Browser with styled console
|
|
117
|
+
return [`%c${LOG_PREFIX}`, LOG_STYLES[level], ...args];
|
|
118
|
+
}
|
|
119
|
+
// Node.js or basic console
|
|
120
|
+
return [`${LOG_PREFIX} [${level.toUpperCase()}]`, ...args];
|
|
121
|
+
};
|
|
122
|
+
return {
|
|
123
|
+
get enabled() {
|
|
124
|
+
return isEnabled;
|
|
125
|
+
},
|
|
126
|
+
set enabled(value) {
|
|
127
|
+
isEnabled = value;
|
|
128
|
+
},
|
|
129
|
+
debug(...args) {
|
|
130
|
+
if (shouldLog('debug') && typeof console !== 'undefined') {
|
|
131
|
+
console.log(...formatArgs('debug', args));
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
info(...args) {
|
|
135
|
+
if (shouldLog('info') && typeof console !== 'undefined') {
|
|
136
|
+
console.info(...formatArgs('info', args));
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
warn(...args) {
|
|
140
|
+
if (shouldLog('warn') && typeof console !== 'undefined') {
|
|
141
|
+
console.warn(...formatArgs('warn', args));
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
error(...args) {
|
|
145
|
+
if (shouldLog('error') && typeof console !== 'undefined') {
|
|
146
|
+
console.error(...formatArgs('error', args));
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
setLevel(level) {
|
|
150
|
+
currentLevel = level;
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
/** Global logger instance */
|
|
155
|
+
const logger = createLogger(false);
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* MorrisB Tracking SDK - Transport Layer
|
|
159
|
+
* Handles sending events to the backend with retry logic
|
|
160
|
+
* @version 3.0.0
|
|
161
|
+
*/
|
|
162
|
+
const DEFAULT_TIMEOUT = 10000; // 10 seconds
|
|
163
|
+
const DEFAULT_MAX_RETRIES = 3;
|
|
164
|
+
const DEFAULT_RETRY_DELAY = 1000; // 1 second
|
|
165
|
+
/**
|
|
166
|
+
* Transport class for sending data to the backend
|
|
167
|
+
*/
|
|
168
|
+
class Transport {
|
|
169
|
+
constructor(config) {
|
|
170
|
+
this.config = {
|
|
171
|
+
apiEndpoint: config.apiEndpoint,
|
|
172
|
+
maxRetries: config.maxRetries ?? DEFAULT_MAX_RETRIES,
|
|
173
|
+
retryDelay: config.retryDelay ?? DEFAULT_RETRY_DELAY,
|
|
174
|
+
timeout: config.timeout ?? DEFAULT_TIMEOUT,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Send events to the tracking endpoint
|
|
179
|
+
*/
|
|
180
|
+
async sendEvents(events) {
|
|
181
|
+
const url = `${this.config.apiEndpoint}/api/public/track/event`;
|
|
182
|
+
const payload = JSON.stringify({ events });
|
|
183
|
+
return this.send(url, payload);
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Send identify request
|
|
187
|
+
*/
|
|
188
|
+
async sendIdentify(data) {
|
|
189
|
+
const url = `${this.config.apiEndpoint}/api/public/track/identify`;
|
|
190
|
+
const payload = JSON.stringify(data);
|
|
191
|
+
return this.send(url, payload);
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Send events synchronously (for page unload)
|
|
195
|
+
* Uses navigator.sendBeacon for reliability
|
|
196
|
+
*/
|
|
197
|
+
sendBeacon(events) {
|
|
198
|
+
if (typeof navigator === 'undefined' || !navigator.sendBeacon) {
|
|
199
|
+
logger.warn('sendBeacon not available, events may be lost');
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
const url = `${this.config.apiEndpoint}/api/public/track/event`;
|
|
203
|
+
const payload = JSON.stringify({ events });
|
|
204
|
+
const blob = new Blob([payload], { type: 'application/json' });
|
|
205
|
+
try {
|
|
206
|
+
const success = navigator.sendBeacon(url, blob);
|
|
207
|
+
if (success) {
|
|
208
|
+
logger.debug(`Beacon sent ${events.length} events`);
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
logger.warn('sendBeacon returned false');
|
|
212
|
+
}
|
|
213
|
+
return success;
|
|
214
|
+
}
|
|
215
|
+
catch (error) {
|
|
216
|
+
logger.error('sendBeacon error:', error);
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Internal send with retry logic
|
|
222
|
+
*/
|
|
223
|
+
async send(url, payload, attempt = 1) {
|
|
224
|
+
try {
|
|
225
|
+
const response = await this.fetchWithTimeout(url, {
|
|
226
|
+
method: 'POST',
|
|
227
|
+
headers: {
|
|
228
|
+
'Content-Type': 'application/json',
|
|
229
|
+
},
|
|
230
|
+
body: payload,
|
|
231
|
+
keepalive: true,
|
|
232
|
+
});
|
|
233
|
+
if (response.ok) {
|
|
234
|
+
logger.debug('Request successful:', url);
|
|
235
|
+
return { success: true, status: response.status };
|
|
236
|
+
}
|
|
237
|
+
// Server error - may retry
|
|
238
|
+
if (response.status >= 500 && attempt < this.config.maxRetries) {
|
|
239
|
+
logger.warn(`Server error (${response.status}), retrying...`);
|
|
240
|
+
await this.delay(this.config.retryDelay * attempt);
|
|
241
|
+
return this.send(url, payload, attempt + 1);
|
|
242
|
+
}
|
|
243
|
+
// Client error - don't retry
|
|
244
|
+
logger.error(`Request failed with status ${response.status}`);
|
|
245
|
+
return { success: false, status: response.status };
|
|
246
|
+
}
|
|
247
|
+
catch (error) {
|
|
248
|
+
// Network error - retry if possible
|
|
249
|
+
if (attempt < this.config.maxRetries) {
|
|
250
|
+
logger.warn(`Network error, retrying (${attempt}/${this.config.maxRetries})...`);
|
|
251
|
+
await this.delay(this.config.retryDelay * attempt);
|
|
252
|
+
return this.send(url, payload, attempt + 1);
|
|
253
|
+
}
|
|
254
|
+
logger.error('Request failed after retries:', error);
|
|
255
|
+
return { success: false, error: error };
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Fetch with timeout
|
|
260
|
+
*/
|
|
261
|
+
async fetchWithTimeout(url, options) {
|
|
262
|
+
const controller = new AbortController();
|
|
263
|
+
const timeout = setTimeout(() => controller.abort(), this.config.timeout);
|
|
264
|
+
try {
|
|
265
|
+
const response = await fetch(url, {
|
|
266
|
+
...options,
|
|
267
|
+
signal: controller.signal,
|
|
268
|
+
});
|
|
269
|
+
return response;
|
|
270
|
+
}
|
|
271
|
+
finally {
|
|
272
|
+
clearTimeout(timeout);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Delay helper
|
|
277
|
+
*/
|
|
278
|
+
delay(ms) {
|
|
279
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* MorrisB Tracking SDK - Utility Functions
|
|
285
|
+
* @version 3.0.0
|
|
286
|
+
*/
|
|
287
|
+
// ============================================
|
|
288
|
+
// UUID GENERATION
|
|
289
|
+
// ============================================
|
|
290
|
+
/**
|
|
291
|
+
* Generate a UUID v4
|
|
292
|
+
*/
|
|
293
|
+
function generateUUID() {
|
|
294
|
+
// Use crypto.randomUUID if available (modern browsers)
|
|
295
|
+
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
|
|
296
|
+
return crypto.randomUUID();
|
|
297
|
+
}
|
|
298
|
+
// Fallback to manual generation
|
|
299
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
|
300
|
+
const r = (Math.random() * 16) | 0;
|
|
301
|
+
const v = c === 'x' ? r : (r & 0x3) | 0x8;
|
|
302
|
+
return v.toString(16);
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
// ============================================
|
|
306
|
+
// STORAGE UTILITIES
|
|
307
|
+
// ============================================
|
|
308
|
+
/**
|
|
309
|
+
* Safely get from localStorage
|
|
310
|
+
*/
|
|
311
|
+
function getLocalStorage(key) {
|
|
312
|
+
try {
|
|
313
|
+
if (typeof localStorage !== 'undefined') {
|
|
314
|
+
return localStorage.getItem(key);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
catch {
|
|
318
|
+
// localStorage not available or blocked
|
|
319
|
+
}
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Safely set to localStorage
|
|
324
|
+
*/
|
|
325
|
+
function setLocalStorage(key, value) {
|
|
326
|
+
try {
|
|
327
|
+
if (typeof localStorage !== 'undefined') {
|
|
328
|
+
localStorage.setItem(key, value);
|
|
329
|
+
return true;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
catch {
|
|
333
|
+
// localStorage not available or blocked
|
|
334
|
+
}
|
|
335
|
+
return false;
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Safely get from sessionStorage
|
|
339
|
+
*/
|
|
340
|
+
function getSessionStorage(key) {
|
|
341
|
+
try {
|
|
342
|
+
if (typeof sessionStorage !== 'undefined') {
|
|
343
|
+
return sessionStorage.getItem(key);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
catch {
|
|
347
|
+
// sessionStorage not available or blocked
|
|
348
|
+
}
|
|
349
|
+
return null;
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Safely set to sessionStorage
|
|
353
|
+
*/
|
|
354
|
+
function setSessionStorage(key, value) {
|
|
355
|
+
try {
|
|
356
|
+
if (typeof sessionStorage !== 'undefined') {
|
|
357
|
+
sessionStorage.setItem(key, value);
|
|
358
|
+
return true;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
catch {
|
|
362
|
+
// sessionStorage not available or blocked
|
|
363
|
+
}
|
|
364
|
+
return false;
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Get or set a cookie
|
|
368
|
+
*/
|
|
369
|
+
function cookie(name, value, days) {
|
|
370
|
+
if (typeof document === 'undefined')
|
|
371
|
+
return null;
|
|
372
|
+
// Get cookie
|
|
373
|
+
if (value === undefined) {
|
|
374
|
+
const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'));
|
|
375
|
+
return match ? match[2] : null;
|
|
376
|
+
}
|
|
377
|
+
// Set cookie
|
|
378
|
+
let expires = '';
|
|
379
|
+
if (days) {
|
|
380
|
+
const date = new Date();
|
|
381
|
+
date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000);
|
|
382
|
+
expires = '; expires=' + date.toUTCString();
|
|
383
|
+
}
|
|
384
|
+
document.cookie = name + '=' + value + expires + '; path=/; SameSite=Lax';
|
|
385
|
+
return value;
|
|
386
|
+
}
|
|
387
|
+
// ============================================
|
|
388
|
+
// VISITOR & SESSION MANAGEMENT
|
|
389
|
+
// ============================================
|
|
390
|
+
/**
|
|
391
|
+
* Get or create a persistent visitor ID
|
|
392
|
+
*/
|
|
393
|
+
function getOrCreateVisitorId(useCookies = false) {
|
|
394
|
+
const key = STORAGE_KEYS.VISITOR_ID;
|
|
395
|
+
// Try to get existing ID
|
|
396
|
+
let visitorId = null;
|
|
397
|
+
if (useCookies) {
|
|
398
|
+
visitorId = cookie(key);
|
|
399
|
+
}
|
|
400
|
+
else {
|
|
401
|
+
visitorId = getLocalStorage(key);
|
|
402
|
+
}
|
|
403
|
+
// Create new ID if not found
|
|
404
|
+
if (!visitorId) {
|
|
405
|
+
visitorId = generateUUID();
|
|
406
|
+
if (useCookies) {
|
|
407
|
+
cookie(key, visitorId, 365); // 1 year
|
|
408
|
+
}
|
|
409
|
+
else {
|
|
410
|
+
setLocalStorage(key, visitorId);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
return visitorId;
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* Get or create a session ID (expires after timeout)
|
|
417
|
+
*/
|
|
418
|
+
function getOrCreateSessionId(timeout) {
|
|
419
|
+
const sidKey = STORAGE_KEYS.SESSION_ID;
|
|
420
|
+
const tsKey = STORAGE_KEYS.SESSION_TIMESTAMP;
|
|
421
|
+
let sessionId = getSessionStorage(sidKey);
|
|
422
|
+
const lastActivity = parseInt(getSessionStorage(tsKey) || '0', 10);
|
|
423
|
+
const now = Date.now();
|
|
424
|
+
// Check if session expired
|
|
425
|
+
if (!sessionId || now - lastActivity > timeout) {
|
|
426
|
+
sessionId = generateUUID();
|
|
427
|
+
setSessionStorage(sidKey, sessionId);
|
|
428
|
+
}
|
|
429
|
+
// Update last activity
|
|
430
|
+
setSessionStorage(tsKey, now.toString());
|
|
431
|
+
return sessionId;
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* Reset visitor and session IDs
|
|
435
|
+
*/
|
|
436
|
+
function resetIds(useCookies = false) {
|
|
437
|
+
const visitorKey = STORAGE_KEYS.VISITOR_ID;
|
|
438
|
+
if (useCookies) {
|
|
439
|
+
cookie(visitorKey, '', -1); // Delete cookie
|
|
440
|
+
}
|
|
441
|
+
else {
|
|
442
|
+
try {
|
|
443
|
+
localStorage.removeItem(visitorKey);
|
|
444
|
+
}
|
|
445
|
+
catch {
|
|
446
|
+
// Ignore
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
try {
|
|
450
|
+
sessionStorage.removeItem(STORAGE_KEYS.SESSION_ID);
|
|
451
|
+
sessionStorage.removeItem(STORAGE_KEYS.SESSION_TIMESTAMP);
|
|
452
|
+
}
|
|
453
|
+
catch {
|
|
454
|
+
// Ignore
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
// ============================================
|
|
458
|
+
// URL UTILITIES
|
|
459
|
+
// ============================================
|
|
460
|
+
/**
|
|
461
|
+
* Extract UTM parameters from URL
|
|
462
|
+
*/
|
|
463
|
+
function getUTMParams() {
|
|
464
|
+
if (typeof window === 'undefined')
|
|
465
|
+
return {};
|
|
466
|
+
try {
|
|
467
|
+
const params = new URLSearchParams(window.location.search);
|
|
468
|
+
return {
|
|
469
|
+
utmSource: params.get('utm_source') || undefined,
|
|
470
|
+
utmMedium: params.get('utm_medium') || undefined,
|
|
471
|
+
utmCampaign: params.get('utm_campaign') || undefined,
|
|
472
|
+
utmTerm: params.get('utm_term') || undefined,
|
|
473
|
+
utmContent: params.get('utm_content') || undefined,
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
catch {
|
|
477
|
+
return {};
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
/**
|
|
481
|
+
* Check if URL is a download link
|
|
482
|
+
*/
|
|
483
|
+
function isDownloadUrl(url) {
|
|
484
|
+
const lowerUrl = url.toLowerCase();
|
|
485
|
+
return DOWNLOAD_EXTENSIONS.some((ext) => lowerUrl.includes(ext));
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* Extract filename from URL
|
|
489
|
+
*/
|
|
490
|
+
function getFilenameFromUrl(url) {
|
|
491
|
+
try {
|
|
492
|
+
return url.split('/').pop()?.split('?')[0] || 'unknown';
|
|
493
|
+
}
|
|
494
|
+
catch {
|
|
495
|
+
return 'unknown';
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* Extract file extension from URL
|
|
500
|
+
*/
|
|
501
|
+
function getFileExtension(url) {
|
|
502
|
+
const filename = getFilenameFromUrl(url);
|
|
503
|
+
const parts = filename.split('.');
|
|
504
|
+
return parts.length > 1 ? parts.pop() || 'unknown' : 'unknown';
|
|
505
|
+
}
|
|
506
|
+
// ============================================
|
|
507
|
+
// DOM UTILITIES
|
|
508
|
+
// ============================================
|
|
509
|
+
/**
|
|
510
|
+
* Get text content from element (truncated)
|
|
511
|
+
*/
|
|
512
|
+
function getElementText(element, maxLength = 100) {
|
|
513
|
+
const text = element.innerText ||
|
|
514
|
+
element.textContent ||
|
|
515
|
+
element.value ||
|
|
516
|
+
'';
|
|
517
|
+
return text.trim().substring(0, maxLength);
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* Get element identification info
|
|
521
|
+
*/
|
|
522
|
+
function getElementInfo(element) {
|
|
523
|
+
return {
|
|
524
|
+
tag: element.tagName?.toLowerCase() || 'unknown',
|
|
525
|
+
id: element.id || '',
|
|
526
|
+
className: element.className || '',
|
|
527
|
+
text: getElementText(element, 50),
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
/**
|
|
531
|
+
* Check if element is a trackable click target
|
|
532
|
+
*/
|
|
533
|
+
function isTrackableClickElement(element) {
|
|
534
|
+
const trackableTags = ['BUTTON', 'A', 'INPUT'];
|
|
535
|
+
return (trackableTags.includes(element.tagName) ||
|
|
536
|
+
element.hasAttribute('data-track-click') ||
|
|
537
|
+
element.classList.contains('track-click'));
|
|
538
|
+
}
|
|
539
|
+
/**
|
|
540
|
+
* Check if device is mobile
|
|
541
|
+
*/
|
|
542
|
+
function isMobile() {
|
|
543
|
+
if (typeof navigator === 'undefined')
|
|
544
|
+
return false;
|
|
545
|
+
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
|
546
|
+
}
|
|
547
|
+
// ============================================
|
|
548
|
+
// DEVICE INFO
|
|
549
|
+
// ============================================
|
|
550
|
+
/**
|
|
551
|
+
* Get current device information
|
|
552
|
+
*/
|
|
553
|
+
function getDeviceInfo() {
|
|
554
|
+
if (typeof window === 'undefined' || typeof navigator === 'undefined') {
|
|
555
|
+
return {
|
|
556
|
+
userAgent: 'unknown',
|
|
557
|
+
screen: 'unknown',
|
|
558
|
+
language: 'unknown',
|
|
559
|
+
timezone: 'unknown',
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
return {
|
|
563
|
+
userAgent: navigator.userAgent,
|
|
564
|
+
screen: `${screen.width}x${screen.height}`,
|
|
565
|
+
language: navigator.language,
|
|
566
|
+
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone || 'unknown',
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* MorrisB Tracking SDK - Event Queue
|
|
572
|
+
* Handles batching and flushing of events
|
|
573
|
+
* @version 3.0.0
|
|
574
|
+
*/
|
|
575
|
+
const MAX_QUEUE_SIZE = 1000;
|
|
576
|
+
/**
|
|
577
|
+
* Event queue with batching, persistence, and auto-flush
|
|
578
|
+
*/
|
|
579
|
+
class EventQueue {
|
|
580
|
+
constructor(transport, config = {}) {
|
|
581
|
+
this.queue = [];
|
|
582
|
+
this.flushTimer = null;
|
|
583
|
+
this.isFlushing = false;
|
|
584
|
+
this.transport = transport;
|
|
585
|
+
this.config = {
|
|
586
|
+
batchSize: config.batchSize ?? 10,
|
|
587
|
+
flushInterval: config.flushInterval ?? 5000,
|
|
588
|
+
maxQueueSize: config.maxQueueSize ?? MAX_QUEUE_SIZE,
|
|
589
|
+
storageKey: config.storageKey ?? STORAGE_KEYS.EVENT_QUEUE,
|
|
590
|
+
};
|
|
591
|
+
// Restore persisted queue
|
|
592
|
+
this.restoreQueue();
|
|
593
|
+
// Start auto-flush timer
|
|
594
|
+
this.startFlushTimer();
|
|
595
|
+
// Setup unload handlers
|
|
596
|
+
this.setupUnloadHandlers();
|
|
597
|
+
}
|
|
598
|
+
/**
|
|
599
|
+
* Add an event to the queue
|
|
600
|
+
*/
|
|
601
|
+
push(event) {
|
|
602
|
+
// Don't exceed max queue size
|
|
603
|
+
if (this.queue.length >= this.config.maxQueueSize) {
|
|
604
|
+
logger.warn('Queue full, dropping oldest event');
|
|
605
|
+
this.queue.shift();
|
|
606
|
+
}
|
|
607
|
+
this.queue.push(event);
|
|
608
|
+
logger.debug('Event queued:', event.eventName, `(${this.queue.length} in queue)`);
|
|
609
|
+
// Flush if batch size reached
|
|
610
|
+
if (this.queue.length >= this.config.batchSize) {
|
|
611
|
+
this.flush();
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
/**
|
|
615
|
+
* Flush the queue (send all events)
|
|
616
|
+
*/
|
|
617
|
+
async flush() {
|
|
618
|
+
if (this.isFlushing || this.queue.length === 0) {
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
this.isFlushing = true;
|
|
622
|
+
try {
|
|
623
|
+
// Take all events from queue
|
|
624
|
+
const events = this.queue.splice(0, this.queue.length);
|
|
625
|
+
logger.debug(`Flushing ${events.length} events`);
|
|
626
|
+
// Clear persisted queue
|
|
627
|
+
this.persistQueue([]);
|
|
628
|
+
// Send to backend
|
|
629
|
+
const result = await this.transport.sendEvents(events);
|
|
630
|
+
if (!result.success) {
|
|
631
|
+
// Re-queue events on failure (at the front)
|
|
632
|
+
logger.warn('Flush failed, re-queuing events');
|
|
633
|
+
this.queue.unshift(...events);
|
|
634
|
+
this.persistQueue(this.queue);
|
|
635
|
+
}
|
|
636
|
+
else {
|
|
637
|
+
logger.debug('Flush successful');
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
catch (error) {
|
|
641
|
+
logger.error('Flush error:', error);
|
|
642
|
+
}
|
|
643
|
+
finally {
|
|
644
|
+
this.isFlushing = false;
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
/**
|
|
648
|
+
* Flush synchronously using sendBeacon (for page unload)
|
|
649
|
+
*/
|
|
650
|
+
flushSync() {
|
|
651
|
+
if (this.queue.length === 0)
|
|
652
|
+
return;
|
|
653
|
+
const events = this.queue.splice(0, this.queue.length);
|
|
654
|
+
logger.debug(`Sync flushing ${events.length} events via beacon`);
|
|
655
|
+
const success = this.transport.sendBeacon(events);
|
|
656
|
+
if (!success) {
|
|
657
|
+
// Re-queue and persist for next page load
|
|
658
|
+
this.queue.unshift(...events);
|
|
659
|
+
this.persistQueue(this.queue);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
/**
|
|
663
|
+
* Get current queue length
|
|
664
|
+
*/
|
|
665
|
+
get length() {
|
|
666
|
+
return this.queue.length;
|
|
667
|
+
}
|
|
668
|
+
/**
|
|
669
|
+
* Clear the queue
|
|
670
|
+
*/
|
|
671
|
+
clear() {
|
|
672
|
+
this.queue = [];
|
|
673
|
+
this.persistQueue([]);
|
|
674
|
+
}
|
|
675
|
+
/**
|
|
676
|
+
* Stop the flush timer
|
|
677
|
+
*/
|
|
678
|
+
destroy() {
|
|
679
|
+
if (this.flushTimer) {
|
|
680
|
+
clearInterval(this.flushTimer);
|
|
681
|
+
this.flushTimer = null;
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
/**
|
|
685
|
+
* Start auto-flush timer
|
|
686
|
+
*/
|
|
687
|
+
startFlushTimer() {
|
|
688
|
+
if (this.flushTimer) {
|
|
689
|
+
clearInterval(this.flushTimer);
|
|
690
|
+
}
|
|
691
|
+
this.flushTimer = setInterval(() => {
|
|
692
|
+
this.flush();
|
|
693
|
+
}, this.config.flushInterval);
|
|
694
|
+
}
|
|
695
|
+
/**
|
|
696
|
+
* Setup page unload handlers
|
|
697
|
+
*/
|
|
698
|
+
setupUnloadHandlers() {
|
|
699
|
+
if (typeof window === 'undefined')
|
|
700
|
+
return;
|
|
701
|
+
// Flush on page unload
|
|
702
|
+
window.addEventListener('beforeunload', () => {
|
|
703
|
+
this.flushSync();
|
|
704
|
+
});
|
|
705
|
+
// Flush when page becomes hidden
|
|
706
|
+
window.addEventListener('visibilitychange', () => {
|
|
707
|
+
if (document.visibilityState === 'hidden') {
|
|
708
|
+
this.flushSync();
|
|
709
|
+
}
|
|
710
|
+
});
|
|
711
|
+
// Flush on page hide (iOS Safari)
|
|
712
|
+
window.addEventListener('pagehide', () => {
|
|
713
|
+
this.flushSync();
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
/**
|
|
717
|
+
* Persist queue to localStorage
|
|
718
|
+
*/
|
|
719
|
+
persistQueue(events) {
|
|
720
|
+
try {
|
|
721
|
+
setLocalStorage(this.config.storageKey, JSON.stringify(events));
|
|
722
|
+
}
|
|
723
|
+
catch {
|
|
724
|
+
// Ignore storage errors
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
/**
|
|
728
|
+
* Restore queue from localStorage
|
|
729
|
+
*/
|
|
730
|
+
restoreQueue() {
|
|
731
|
+
try {
|
|
732
|
+
const stored = getLocalStorage(this.config.storageKey);
|
|
733
|
+
if (stored) {
|
|
734
|
+
const events = JSON.parse(stored);
|
|
735
|
+
if (Array.isArray(events) && events.length > 0) {
|
|
736
|
+
this.queue = events;
|
|
737
|
+
logger.debug(`Restored ${events.length} events from storage`);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
catch {
|
|
742
|
+
// Ignore parse errors
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
/**
|
|
748
|
+
* MorrisB Tracking SDK - Plugin Base
|
|
749
|
+
* @version 3.0.0
|
|
750
|
+
*/
|
|
751
|
+
/**
|
|
752
|
+
* Base class for plugins
|
|
753
|
+
*/
|
|
754
|
+
class BasePlugin {
|
|
755
|
+
constructor() {
|
|
756
|
+
this.tracker = null;
|
|
757
|
+
}
|
|
758
|
+
init(tracker) {
|
|
759
|
+
this.tracker = tracker;
|
|
760
|
+
}
|
|
761
|
+
destroy() {
|
|
762
|
+
this.tracker = null;
|
|
763
|
+
}
|
|
764
|
+
track(eventType, eventName, properties) {
|
|
765
|
+
if (this.tracker) {
|
|
766
|
+
this.tracker.track(eventType, eventName, properties);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
/**
|
|
772
|
+
* MorrisB Tracking SDK - Page View Plugin
|
|
773
|
+
* @version 3.0.0
|
|
774
|
+
*/
|
|
775
|
+
/**
|
|
776
|
+
* Page View Plugin - Tracks page views
|
|
777
|
+
*/
|
|
778
|
+
class PageViewPlugin extends BasePlugin {
|
|
779
|
+
constructor() {
|
|
780
|
+
super(...arguments);
|
|
781
|
+
this.name = 'pageView';
|
|
782
|
+
}
|
|
783
|
+
init(tracker) {
|
|
784
|
+
super.init(tracker);
|
|
785
|
+
// Track initial page view
|
|
786
|
+
this.trackPageView();
|
|
787
|
+
// Track SPA navigation (History API)
|
|
788
|
+
if (typeof window !== 'undefined') {
|
|
789
|
+
// Intercept pushState and replaceState
|
|
790
|
+
const originalPushState = history.pushState;
|
|
791
|
+
const originalReplaceState = history.replaceState;
|
|
792
|
+
history.pushState = (...args) => {
|
|
793
|
+
originalPushState.apply(history, args);
|
|
794
|
+
this.trackPageView();
|
|
795
|
+
};
|
|
796
|
+
history.replaceState = (...args) => {
|
|
797
|
+
originalReplaceState.apply(history, args);
|
|
798
|
+
this.trackPageView();
|
|
799
|
+
};
|
|
800
|
+
// Handle back/forward navigation
|
|
801
|
+
window.addEventListener('popstate', () => {
|
|
802
|
+
this.trackPageView();
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
trackPageView() {
|
|
807
|
+
if (typeof window === 'undefined' || typeof document === 'undefined')
|
|
808
|
+
return;
|
|
809
|
+
this.track('page_view', 'Page Viewed', {
|
|
810
|
+
title: document.title,
|
|
811
|
+
path: window.location.pathname,
|
|
812
|
+
search: window.location.search,
|
|
813
|
+
hash: window.location.hash,
|
|
814
|
+
referrer: document.referrer || 'direct',
|
|
815
|
+
viewport: `${window.innerWidth}x${window.innerHeight}`,
|
|
816
|
+
});
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
/**
|
|
821
|
+
* MorrisB Tracking SDK - Scroll Depth Plugin
|
|
822
|
+
* @version 3.0.0
|
|
823
|
+
*/
|
|
824
|
+
/**
|
|
825
|
+
* Scroll Depth Plugin - Tracks scroll milestones
|
|
826
|
+
*/
|
|
827
|
+
class ScrollPlugin extends BasePlugin {
|
|
828
|
+
constructor() {
|
|
829
|
+
super(...arguments);
|
|
830
|
+
this.name = 'scroll';
|
|
831
|
+
this.milestonesReached = new Set();
|
|
832
|
+
this.maxScrollDepth = 0;
|
|
833
|
+
this.pageLoadTime = 0;
|
|
834
|
+
this.scrollTimeout = null;
|
|
835
|
+
this.boundHandler = null;
|
|
836
|
+
}
|
|
837
|
+
init(tracker) {
|
|
838
|
+
super.init(tracker);
|
|
839
|
+
this.pageLoadTime = Date.now();
|
|
840
|
+
if (typeof window !== 'undefined') {
|
|
841
|
+
this.boundHandler = this.handleScroll.bind(this);
|
|
842
|
+
window.addEventListener('scroll', this.boundHandler, { passive: true });
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
destroy() {
|
|
846
|
+
if (this.boundHandler && typeof window !== 'undefined') {
|
|
847
|
+
window.removeEventListener('scroll', this.boundHandler);
|
|
848
|
+
}
|
|
849
|
+
if (this.scrollTimeout) {
|
|
850
|
+
clearTimeout(this.scrollTimeout);
|
|
851
|
+
}
|
|
852
|
+
super.destroy();
|
|
853
|
+
}
|
|
854
|
+
handleScroll() {
|
|
855
|
+
// Debounce scroll tracking
|
|
856
|
+
if (this.scrollTimeout) {
|
|
857
|
+
clearTimeout(this.scrollTimeout);
|
|
858
|
+
}
|
|
859
|
+
this.scrollTimeout = setTimeout(() => this.trackScrollDepth(), 150);
|
|
860
|
+
}
|
|
861
|
+
trackScrollDepth() {
|
|
862
|
+
if (typeof window === 'undefined' || typeof document === 'undefined')
|
|
863
|
+
return;
|
|
864
|
+
const windowHeight = window.innerHeight;
|
|
865
|
+
const documentHeight = document.documentElement.scrollHeight;
|
|
866
|
+
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
|
|
867
|
+
const scrollPercent = Math.floor((scrollTop / (documentHeight - windowHeight)) * 100);
|
|
868
|
+
// Clamp to valid range
|
|
869
|
+
const clampedPercent = Math.max(0, Math.min(100, scrollPercent));
|
|
870
|
+
// Update max scroll depth
|
|
871
|
+
if (clampedPercent > this.maxScrollDepth) {
|
|
872
|
+
this.maxScrollDepth = clampedPercent;
|
|
873
|
+
}
|
|
874
|
+
// Track milestones
|
|
875
|
+
for (const milestone of SCROLL_MILESTONES) {
|
|
876
|
+
if (clampedPercent >= milestone && !this.milestonesReached.has(milestone)) {
|
|
877
|
+
this.milestonesReached.add(milestone);
|
|
878
|
+
this.track('scroll_depth', `Scrolled ${milestone}%`, {
|
|
879
|
+
depth: milestone,
|
|
880
|
+
maxDepth: this.maxScrollDepth,
|
|
881
|
+
timeToReach: Date.now() - this.pageLoadTime,
|
|
882
|
+
});
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
/**
|
|
889
|
+
* MorrisB Tracking SDK - Form Tracking Plugin
|
|
890
|
+
* @version 3.0.0
|
|
891
|
+
*/
|
|
892
|
+
/**
|
|
893
|
+
* Form Tracking Plugin - Auto-tracks form views, interactions, and submissions
|
|
894
|
+
*/
|
|
895
|
+
class FormsPlugin extends BasePlugin {
|
|
896
|
+
constructor() {
|
|
897
|
+
super(...arguments);
|
|
898
|
+
this.name = 'forms';
|
|
899
|
+
this.trackedForms = new WeakSet();
|
|
900
|
+
this.formInteractions = new Set();
|
|
901
|
+
this.observer = null;
|
|
902
|
+
}
|
|
903
|
+
init(tracker) {
|
|
904
|
+
super.init(tracker);
|
|
905
|
+
if (typeof document === 'undefined')
|
|
906
|
+
return;
|
|
907
|
+
// Track existing forms
|
|
908
|
+
this.trackAllForms();
|
|
909
|
+
// Watch for dynamically added forms
|
|
910
|
+
if (typeof MutationObserver !== 'undefined') {
|
|
911
|
+
this.observer = new MutationObserver(() => this.trackAllForms());
|
|
912
|
+
this.observer.observe(document.body, { childList: true, subtree: true });
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
destroy() {
|
|
916
|
+
if (this.observer) {
|
|
917
|
+
this.observer.disconnect();
|
|
918
|
+
this.observer = null;
|
|
919
|
+
}
|
|
920
|
+
super.destroy();
|
|
921
|
+
}
|
|
922
|
+
trackAllForms() {
|
|
923
|
+
document.querySelectorAll('form').forEach((form) => {
|
|
924
|
+
this.setupFormTracking(form);
|
|
925
|
+
});
|
|
926
|
+
}
|
|
927
|
+
setupFormTracking(form) {
|
|
928
|
+
if (this.trackedForms.has(form))
|
|
929
|
+
return;
|
|
930
|
+
this.trackedForms.add(form);
|
|
931
|
+
const formId = form.id || form.name || `form-${Math.random().toString(36).substr(2, 9)}`;
|
|
932
|
+
// Track form view
|
|
933
|
+
this.track('form_view', 'Form Viewed', {
|
|
934
|
+
formId,
|
|
935
|
+
action: form.action,
|
|
936
|
+
method: form.method,
|
|
937
|
+
fieldCount: form.elements.length,
|
|
938
|
+
});
|
|
939
|
+
// Track field interactions
|
|
940
|
+
Array.from(form.elements).forEach((field) => {
|
|
941
|
+
if (field instanceof HTMLInputElement ||
|
|
942
|
+
field instanceof HTMLSelectElement ||
|
|
943
|
+
field instanceof HTMLTextAreaElement) {
|
|
944
|
+
if (!field.name || field.type === 'submit' || field.type === 'button')
|
|
945
|
+
return;
|
|
946
|
+
['focus', 'blur', 'change'].forEach((eventType) => {
|
|
947
|
+
field.addEventListener(eventType, () => {
|
|
948
|
+
const key = `${formId}-${field.name}-${eventType}`;
|
|
949
|
+
if (!this.formInteractions.has(key)) {
|
|
950
|
+
this.formInteractions.add(key);
|
|
951
|
+
this.track('form_interaction', 'Form Field Interaction', {
|
|
952
|
+
formId,
|
|
953
|
+
fieldName: field.name,
|
|
954
|
+
fieldType: field.type,
|
|
955
|
+
interactionType: eventType,
|
|
956
|
+
});
|
|
957
|
+
}
|
|
958
|
+
});
|
|
959
|
+
});
|
|
960
|
+
}
|
|
961
|
+
});
|
|
962
|
+
// Track form submission
|
|
963
|
+
form.addEventListener('submit', () => {
|
|
964
|
+
this.track('form_submit', 'Form Submitted', {
|
|
965
|
+
formId,
|
|
966
|
+
action: form.action,
|
|
967
|
+
method: form.method,
|
|
968
|
+
});
|
|
969
|
+
// Auto-identify if email field found
|
|
970
|
+
this.autoIdentify(form);
|
|
971
|
+
});
|
|
972
|
+
}
|
|
973
|
+
autoIdentify(form) {
|
|
974
|
+
const emailField = form.querySelector('input[type="email"], input[name*="email"]');
|
|
975
|
+
if (!emailField?.value || !this.tracker)
|
|
976
|
+
return;
|
|
977
|
+
const email = emailField.value;
|
|
978
|
+
const traits = {};
|
|
979
|
+
// Capture common fields
|
|
980
|
+
const firstNameField = form.querySelector('[name*="first"], [name*="fname"]');
|
|
981
|
+
const lastNameField = form.querySelector('[name*="last"], [name*="lname"]');
|
|
982
|
+
const companyField = form.querySelector('[name*="company"], [name*="organization"]');
|
|
983
|
+
const phoneField = form.querySelector('[type="tel"], [name*="phone"]');
|
|
984
|
+
if (firstNameField?.value)
|
|
985
|
+
traits.firstName = firstNameField.value;
|
|
986
|
+
if (lastNameField?.value)
|
|
987
|
+
traits.lastName = lastNameField.value;
|
|
988
|
+
if (companyField?.value)
|
|
989
|
+
traits.company = companyField.value;
|
|
990
|
+
if (phoneField?.value)
|
|
991
|
+
traits.phone = phoneField.value;
|
|
992
|
+
this.tracker.identify(email, traits);
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
/**
|
|
997
|
+
* MorrisB Tracking SDK - Click Tracking Plugin
|
|
998
|
+
* @version 3.0.0
|
|
999
|
+
*/
|
|
1000
|
+
/**
|
|
1001
|
+
* Click Tracking Plugin - Tracks button and CTA clicks
|
|
1002
|
+
*/
|
|
1003
|
+
class ClicksPlugin extends BasePlugin {
|
|
1004
|
+
constructor() {
|
|
1005
|
+
super(...arguments);
|
|
1006
|
+
this.name = 'clicks';
|
|
1007
|
+
this.boundHandler = null;
|
|
1008
|
+
}
|
|
1009
|
+
init(tracker) {
|
|
1010
|
+
super.init(tracker);
|
|
1011
|
+
if (typeof document !== 'undefined') {
|
|
1012
|
+
this.boundHandler = this.handleClick.bind(this);
|
|
1013
|
+
document.addEventListener('click', this.boundHandler, true);
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
destroy() {
|
|
1017
|
+
if (this.boundHandler && typeof document !== 'undefined') {
|
|
1018
|
+
document.removeEventListener('click', this.boundHandler, true);
|
|
1019
|
+
}
|
|
1020
|
+
super.destroy();
|
|
1021
|
+
}
|
|
1022
|
+
handleClick(e) {
|
|
1023
|
+
const target = e.target;
|
|
1024
|
+
if (!target || !isTrackableClickElement(target))
|
|
1025
|
+
return;
|
|
1026
|
+
const buttonText = getElementText(target, 100);
|
|
1027
|
+
const elementInfo = getElementInfo(target);
|
|
1028
|
+
this.track('button_click', 'Button Clicked', {
|
|
1029
|
+
buttonText,
|
|
1030
|
+
elementType: target.tagName.toLowerCase(),
|
|
1031
|
+
elementId: elementInfo.id,
|
|
1032
|
+
elementClass: elementInfo.className,
|
|
1033
|
+
href: target.href || undefined,
|
|
1034
|
+
});
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
/**
|
|
1039
|
+
* MorrisB Tracking SDK - Engagement Plugin
|
|
1040
|
+
* @version 3.0.0
|
|
1041
|
+
*/
|
|
1042
|
+
/**
|
|
1043
|
+
* Engagement Plugin - Tracks user engagement and time on page
|
|
1044
|
+
*/
|
|
1045
|
+
class EngagementPlugin extends BasePlugin {
|
|
1046
|
+
constructor() {
|
|
1047
|
+
super(...arguments);
|
|
1048
|
+
this.name = 'engagement';
|
|
1049
|
+
this.pageLoadTime = 0;
|
|
1050
|
+
this.engagementStartTime = 0;
|
|
1051
|
+
this.isEngaged = false;
|
|
1052
|
+
this.engagementTimeout = null;
|
|
1053
|
+
this.boundMarkEngaged = null;
|
|
1054
|
+
this.boundTrackTimeOnPage = null;
|
|
1055
|
+
}
|
|
1056
|
+
init(tracker) {
|
|
1057
|
+
super.init(tracker);
|
|
1058
|
+
this.pageLoadTime = Date.now();
|
|
1059
|
+
this.engagementStartTime = Date.now();
|
|
1060
|
+
if (typeof document === 'undefined' || typeof window === 'undefined')
|
|
1061
|
+
return;
|
|
1062
|
+
// Setup engagement detection
|
|
1063
|
+
this.boundMarkEngaged = this.markEngaged.bind(this);
|
|
1064
|
+
this.boundTrackTimeOnPage = this.trackTimeOnPage.bind(this);
|
|
1065
|
+
['mousemove', 'keydown', 'touchstart', 'scroll'].forEach((event) => {
|
|
1066
|
+
document.addEventListener(event, this.boundMarkEngaged, { passive: true });
|
|
1067
|
+
});
|
|
1068
|
+
// Track time on page before unload
|
|
1069
|
+
window.addEventListener('beforeunload', this.boundTrackTimeOnPage);
|
|
1070
|
+
window.addEventListener('visibilitychange', () => {
|
|
1071
|
+
if (document.visibilityState === 'hidden') {
|
|
1072
|
+
this.trackTimeOnPage();
|
|
1073
|
+
}
|
|
1074
|
+
else {
|
|
1075
|
+
// Reset engagement timer when page becomes visible again
|
|
1076
|
+
this.engagementStartTime = Date.now();
|
|
1077
|
+
}
|
|
1078
|
+
});
|
|
1079
|
+
}
|
|
1080
|
+
destroy() {
|
|
1081
|
+
if (this.boundMarkEngaged && typeof document !== 'undefined') {
|
|
1082
|
+
['mousemove', 'keydown', 'touchstart', 'scroll'].forEach((event) => {
|
|
1083
|
+
document.removeEventListener(event, this.boundMarkEngaged);
|
|
1084
|
+
});
|
|
1085
|
+
}
|
|
1086
|
+
if (this.boundTrackTimeOnPage && typeof window !== 'undefined') {
|
|
1087
|
+
window.removeEventListener('beforeunload', this.boundTrackTimeOnPage);
|
|
1088
|
+
}
|
|
1089
|
+
if (this.engagementTimeout) {
|
|
1090
|
+
clearTimeout(this.engagementTimeout);
|
|
1091
|
+
}
|
|
1092
|
+
super.destroy();
|
|
1093
|
+
}
|
|
1094
|
+
markEngaged() {
|
|
1095
|
+
if (!this.isEngaged) {
|
|
1096
|
+
this.isEngaged = true;
|
|
1097
|
+
this.track('engagement', 'User Engaged', {
|
|
1098
|
+
timeToEngage: Date.now() - this.pageLoadTime,
|
|
1099
|
+
});
|
|
1100
|
+
}
|
|
1101
|
+
// Reset engagement timeout
|
|
1102
|
+
if (this.engagementTimeout) {
|
|
1103
|
+
clearTimeout(this.engagementTimeout);
|
|
1104
|
+
}
|
|
1105
|
+
this.engagementTimeout = setTimeout(() => {
|
|
1106
|
+
this.isEngaged = false;
|
|
1107
|
+
}, 30000); // 30 seconds of inactivity
|
|
1108
|
+
}
|
|
1109
|
+
trackTimeOnPage() {
|
|
1110
|
+
const timeSpent = Math.floor((Date.now() - this.engagementStartTime) / 1000);
|
|
1111
|
+
if (timeSpent > 0) {
|
|
1112
|
+
this.track('time_on_page', 'Time Spent', {
|
|
1113
|
+
seconds: timeSpent,
|
|
1114
|
+
engaged: this.isEngaged,
|
|
1115
|
+
});
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
/**
|
|
1121
|
+
* MorrisB Tracking SDK - Downloads Plugin
|
|
1122
|
+
* @version 3.0.0
|
|
1123
|
+
*/
|
|
1124
|
+
/**
|
|
1125
|
+
* Downloads Plugin - Tracks file downloads
|
|
1126
|
+
*/
|
|
1127
|
+
class DownloadsPlugin extends BasePlugin {
|
|
1128
|
+
constructor() {
|
|
1129
|
+
super(...arguments);
|
|
1130
|
+
this.name = 'downloads';
|
|
1131
|
+
this.trackedDownloads = new Set();
|
|
1132
|
+
this.boundHandler = null;
|
|
1133
|
+
}
|
|
1134
|
+
init(tracker) {
|
|
1135
|
+
super.init(tracker);
|
|
1136
|
+
if (typeof document !== 'undefined') {
|
|
1137
|
+
this.boundHandler = this.handleClick.bind(this);
|
|
1138
|
+
document.addEventListener('click', this.boundHandler, true);
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
destroy() {
|
|
1142
|
+
if (this.boundHandler && typeof document !== 'undefined') {
|
|
1143
|
+
document.removeEventListener('click', this.boundHandler, true);
|
|
1144
|
+
}
|
|
1145
|
+
super.destroy();
|
|
1146
|
+
}
|
|
1147
|
+
handleClick(e) {
|
|
1148
|
+
const link = e.target.closest('a');
|
|
1149
|
+
if (!link || !link.href)
|
|
1150
|
+
return;
|
|
1151
|
+
const url = link.href;
|
|
1152
|
+
// Check if it's a download link
|
|
1153
|
+
if (!isDownloadUrl(url))
|
|
1154
|
+
return;
|
|
1155
|
+
// Avoid tracking the same download multiple times
|
|
1156
|
+
if (this.trackedDownloads.has(url))
|
|
1157
|
+
return;
|
|
1158
|
+
this.trackedDownloads.add(url);
|
|
1159
|
+
this.track('download', 'File Download', {
|
|
1160
|
+
url,
|
|
1161
|
+
filename: getFilenameFromUrl(url),
|
|
1162
|
+
fileType: getFileExtension(url),
|
|
1163
|
+
linkText: getElementText(link, 100),
|
|
1164
|
+
});
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
/**
|
|
1169
|
+
* MorrisB Tracking SDK - Exit Intent Plugin
|
|
1170
|
+
* @version 3.0.0
|
|
1171
|
+
*/
|
|
1172
|
+
/**
|
|
1173
|
+
* Exit Intent Plugin - Detects when user intends to leave the page
|
|
1174
|
+
*/
|
|
1175
|
+
class ExitIntentPlugin extends BasePlugin {
|
|
1176
|
+
constructor() {
|
|
1177
|
+
super(...arguments);
|
|
1178
|
+
this.name = 'exitIntent';
|
|
1179
|
+
this.exitIntentShown = false;
|
|
1180
|
+
this.pageLoadTime = 0;
|
|
1181
|
+
this.boundHandler = null;
|
|
1182
|
+
}
|
|
1183
|
+
init(tracker) {
|
|
1184
|
+
super.init(tracker);
|
|
1185
|
+
this.pageLoadTime = Date.now();
|
|
1186
|
+
// Skip on mobile (no mouse events)
|
|
1187
|
+
if (isMobile())
|
|
1188
|
+
return;
|
|
1189
|
+
if (typeof document !== 'undefined') {
|
|
1190
|
+
this.boundHandler = this.handleMouseLeave.bind(this);
|
|
1191
|
+
document.addEventListener('mouseleave', this.boundHandler);
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
destroy() {
|
|
1195
|
+
if (this.boundHandler && typeof document !== 'undefined') {
|
|
1196
|
+
document.removeEventListener('mouseleave', this.boundHandler);
|
|
1197
|
+
}
|
|
1198
|
+
super.destroy();
|
|
1199
|
+
}
|
|
1200
|
+
handleMouseLeave(e) {
|
|
1201
|
+
// Only trigger when mouse leaves from the top of the page
|
|
1202
|
+
if (e.clientY > 0 || this.exitIntentShown)
|
|
1203
|
+
return;
|
|
1204
|
+
this.exitIntentShown = true;
|
|
1205
|
+
this.track('exit_intent', 'Exit Intent Detected', {
|
|
1206
|
+
timeOnPage: Date.now() - this.pageLoadTime,
|
|
1207
|
+
});
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
/**
|
|
1212
|
+
* MorrisB Tracking SDK - Error Tracking Plugin
|
|
1213
|
+
* @version 3.0.0
|
|
1214
|
+
*/
|
|
1215
|
+
/**
|
|
1216
|
+
* Error Tracking Plugin - Tracks JavaScript errors
|
|
1217
|
+
*/
|
|
1218
|
+
class ErrorsPlugin extends BasePlugin {
|
|
1219
|
+
constructor() {
|
|
1220
|
+
super(...arguments);
|
|
1221
|
+
this.name = 'errors';
|
|
1222
|
+
this.boundErrorHandler = null;
|
|
1223
|
+
this.boundRejectionHandler = null;
|
|
1224
|
+
}
|
|
1225
|
+
init(tracker) {
|
|
1226
|
+
super.init(tracker);
|
|
1227
|
+
if (typeof window !== 'undefined') {
|
|
1228
|
+
this.boundErrorHandler = this.handleError.bind(this);
|
|
1229
|
+
this.boundRejectionHandler = this.handleRejection.bind(this);
|
|
1230
|
+
window.addEventListener('error', this.boundErrorHandler);
|
|
1231
|
+
window.addEventListener('unhandledrejection', this.boundRejectionHandler);
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
destroy() {
|
|
1235
|
+
if (typeof window !== 'undefined') {
|
|
1236
|
+
if (this.boundErrorHandler) {
|
|
1237
|
+
window.removeEventListener('error', this.boundErrorHandler);
|
|
1238
|
+
}
|
|
1239
|
+
if (this.boundRejectionHandler) {
|
|
1240
|
+
window.removeEventListener('unhandledrejection', this.boundRejectionHandler);
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
super.destroy();
|
|
1244
|
+
}
|
|
1245
|
+
handleError(e) {
|
|
1246
|
+
this.track('error', 'JavaScript Error', {
|
|
1247
|
+
message: e.message,
|
|
1248
|
+
filename: e.filename,
|
|
1249
|
+
line: e.lineno,
|
|
1250
|
+
column: e.colno,
|
|
1251
|
+
stack: e.error?.stack?.substring(0, 500),
|
|
1252
|
+
});
|
|
1253
|
+
}
|
|
1254
|
+
handleRejection(e) {
|
|
1255
|
+
this.track('error', 'Unhandled Promise Rejection', {
|
|
1256
|
+
reason: String(e.reason).substring(0, 200),
|
|
1257
|
+
});
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
/**
|
|
1262
|
+
* MorrisB Tracking SDK - Performance Plugin
|
|
1263
|
+
* @version 3.0.0
|
|
1264
|
+
*/
|
|
1265
|
+
/**
|
|
1266
|
+
* Performance Plugin - Tracks page performance and Web Vitals
|
|
1267
|
+
*/
|
|
1268
|
+
class PerformancePlugin extends BasePlugin {
|
|
1269
|
+
constructor() {
|
|
1270
|
+
super(...arguments);
|
|
1271
|
+
this.name = 'performance';
|
|
1272
|
+
}
|
|
1273
|
+
init(tracker) {
|
|
1274
|
+
super.init(tracker);
|
|
1275
|
+
if (typeof window !== 'undefined') {
|
|
1276
|
+
// Track performance after page load
|
|
1277
|
+
window.addEventListener('load', () => {
|
|
1278
|
+
// Delay to ensure all metrics are available
|
|
1279
|
+
setTimeout(() => this.trackPerformance(), 100);
|
|
1280
|
+
});
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
trackPerformance() {
|
|
1284
|
+
if (typeof performance === 'undefined')
|
|
1285
|
+
return;
|
|
1286
|
+
// Use Navigation Timing API
|
|
1287
|
+
const timing = performance.timing;
|
|
1288
|
+
if (!timing)
|
|
1289
|
+
return;
|
|
1290
|
+
const loadTime = timing.loadEventEnd - timing.navigationStart;
|
|
1291
|
+
const domReady = timing.domContentLoadedEventEnd - timing.navigationStart;
|
|
1292
|
+
const ttfb = timing.responseStart - timing.navigationStart;
|
|
1293
|
+
const domInteractive = timing.domInteractive - timing.navigationStart;
|
|
1294
|
+
this.track('performance', 'Page Performance', {
|
|
1295
|
+
loadTime,
|
|
1296
|
+
domReady,
|
|
1297
|
+
ttfb, // Time to First Byte
|
|
1298
|
+
domInteractive,
|
|
1299
|
+
});
|
|
1300
|
+
// Track Web Vitals if available
|
|
1301
|
+
this.trackWebVitals();
|
|
1302
|
+
}
|
|
1303
|
+
trackWebVitals() {
|
|
1304
|
+
// LCP (Largest Contentful Paint)
|
|
1305
|
+
if ('PerformanceObserver' in window) {
|
|
1306
|
+
try {
|
|
1307
|
+
const lcpObserver = new PerformanceObserver((entryList) => {
|
|
1308
|
+
const entries = entryList.getEntries();
|
|
1309
|
+
const lastEntry = entries[entries.length - 1];
|
|
1310
|
+
if (lastEntry) {
|
|
1311
|
+
this.track('performance', 'Web Vital - LCP', {
|
|
1312
|
+
metric: 'LCP',
|
|
1313
|
+
value: Math.round(lastEntry.startTime),
|
|
1314
|
+
});
|
|
1315
|
+
}
|
|
1316
|
+
});
|
|
1317
|
+
lcpObserver.observe({ type: 'largest-contentful-paint', buffered: true });
|
|
1318
|
+
}
|
|
1319
|
+
catch {
|
|
1320
|
+
// LCP not supported
|
|
1321
|
+
}
|
|
1322
|
+
// FID (First Input Delay)
|
|
1323
|
+
try {
|
|
1324
|
+
const fidObserver = new PerformanceObserver((entryList) => {
|
|
1325
|
+
const entries = entryList.getEntries();
|
|
1326
|
+
const firstEntry = entries[0];
|
|
1327
|
+
if (firstEntry) {
|
|
1328
|
+
this.track('performance', 'Web Vital - FID', {
|
|
1329
|
+
metric: 'FID',
|
|
1330
|
+
value: Math.round(firstEntry.processingStart - firstEntry.startTime),
|
|
1331
|
+
});
|
|
1332
|
+
}
|
|
1333
|
+
});
|
|
1334
|
+
fidObserver.observe({ type: 'first-input', buffered: true });
|
|
1335
|
+
}
|
|
1336
|
+
catch {
|
|
1337
|
+
// FID not supported
|
|
1338
|
+
}
|
|
1339
|
+
// CLS (Cumulative Layout Shift)
|
|
1340
|
+
try {
|
|
1341
|
+
let clsValue = 0;
|
|
1342
|
+
const clsObserver = new PerformanceObserver((entryList) => {
|
|
1343
|
+
const entries = entryList.getEntries();
|
|
1344
|
+
entries.forEach((entry) => {
|
|
1345
|
+
if (!entry.hadRecentInput) {
|
|
1346
|
+
clsValue += entry.value || 0;
|
|
1347
|
+
}
|
|
1348
|
+
});
|
|
1349
|
+
});
|
|
1350
|
+
clsObserver.observe({ type: 'layout-shift', buffered: true });
|
|
1351
|
+
// Report CLS after page is hidden
|
|
1352
|
+
window.addEventListener('visibilitychange', () => {
|
|
1353
|
+
if (document.visibilityState === 'hidden' && clsValue > 0) {
|
|
1354
|
+
this.track('performance', 'Web Vital - CLS', {
|
|
1355
|
+
metric: 'CLS',
|
|
1356
|
+
value: Math.round(clsValue * 1000) / 1000,
|
|
1357
|
+
});
|
|
1358
|
+
}
|
|
1359
|
+
}, { once: true });
|
|
1360
|
+
}
|
|
1361
|
+
catch {
|
|
1362
|
+
// CLS not supported
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
/**
|
|
1369
|
+
* MorrisB Tracking SDK - Plugins Index
|
|
1370
|
+
* @version 3.0.0
|
|
1371
|
+
*/
|
|
1372
|
+
/**
|
|
1373
|
+
* Get plugin instance by name
|
|
1374
|
+
*/
|
|
1375
|
+
function getPlugin(name) {
|
|
1376
|
+
switch (name) {
|
|
1377
|
+
case 'pageView':
|
|
1378
|
+
return new PageViewPlugin();
|
|
1379
|
+
case 'scroll':
|
|
1380
|
+
return new ScrollPlugin();
|
|
1381
|
+
case 'forms':
|
|
1382
|
+
return new FormsPlugin();
|
|
1383
|
+
case 'clicks':
|
|
1384
|
+
return new ClicksPlugin();
|
|
1385
|
+
case 'engagement':
|
|
1386
|
+
return new EngagementPlugin();
|
|
1387
|
+
case 'downloads':
|
|
1388
|
+
return new DownloadsPlugin();
|
|
1389
|
+
case 'exitIntent':
|
|
1390
|
+
return new ExitIntentPlugin();
|
|
1391
|
+
case 'errors':
|
|
1392
|
+
return new ErrorsPlugin();
|
|
1393
|
+
case 'performance':
|
|
1394
|
+
return new PerformancePlugin();
|
|
1395
|
+
default:
|
|
1396
|
+
throw new Error(`Unknown plugin: ${name}`);
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
/**
|
|
1401
|
+
* MorrisB Tracking SDK - Main Tracker Class
|
|
1402
|
+
* @version 3.0.0
|
|
1403
|
+
*/
|
|
1404
|
+
/**
|
|
1405
|
+
* Main MorrisB Tracker Class
|
|
1406
|
+
*/
|
|
1407
|
+
class Tracker {
|
|
1408
|
+
constructor(workspaceId, userConfig = {}) {
|
|
1409
|
+
this.plugins = [];
|
|
1410
|
+
this.isInitialized = false;
|
|
1411
|
+
if (!workspaceId) {
|
|
1412
|
+
throw new Error('[Clianta] Workspace ID is required');
|
|
1413
|
+
}
|
|
1414
|
+
this.workspaceId = workspaceId;
|
|
1415
|
+
this.config = mergeConfig(userConfig);
|
|
1416
|
+
// Setup debug mode
|
|
1417
|
+
logger.enabled = this.config.debug;
|
|
1418
|
+
logger.info(`Initializing SDK v${SDK_VERSION}`, { workspaceId });
|
|
1419
|
+
// Initialize transport and queue
|
|
1420
|
+
this.transport = new Transport({ apiEndpoint: this.config.apiEndpoint });
|
|
1421
|
+
this.queue = new EventQueue(this.transport, {
|
|
1422
|
+
batchSize: this.config.batchSize,
|
|
1423
|
+
flushInterval: this.config.flushInterval,
|
|
1424
|
+
});
|
|
1425
|
+
// Get or create visitor and session IDs
|
|
1426
|
+
this.visitorId = getOrCreateVisitorId(this.config.useCookies);
|
|
1427
|
+
this.sessionId = getOrCreateSessionId(this.config.sessionTimeout);
|
|
1428
|
+
logger.debug('IDs created', { visitorId: this.visitorId, sessionId: this.sessionId });
|
|
1429
|
+
// Initialize plugins
|
|
1430
|
+
this.initPlugins();
|
|
1431
|
+
this.isInitialized = true;
|
|
1432
|
+
logger.info('SDK initialized successfully');
|
|
1433
|
+
}
|
|
1434
|
+
/**
|
|
1435
|
+
* Initialize enabled plugins
|
|
1436
|
+
*/
|
|
1437
|
+
initPlugins() {
|
|
1438
|
+
const pluginsToLoad = this.config.plugins;
|
|
1439
|
+
// Skip pageView plugin if autoPageView is disabled
|
|
1440
|
+
const filteredPlugins = this.config.autoPageView
|
|
1441
|
+
? pluginsToLoad
|
|
1442
|
+
: pluginsToLoad.filter((p) => p !== 'pageView');
|
|
1443
|
+
for (const pluginName of filteredPlugins) {
|
|
1444
|
+
try {
|
|
1445
|
+
const plugin = getPlugin(pluginName);
|
|
1446
|
+
plugin.init(this);
|
|
1447
|
+
this.plugins.push(plugin);
|
|
1448
|
+
logger.debug(`Plugin loaded: ${pluginName}`);
|
|
1449
|
+
}
|
|
1450
|
+
catch (error) {
|
|
1451
|
+
logger.error(`Failed to load plugin: ${pluginName}`, error);
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
/**
|
|
1456
|
+
* Track a custom event
|
|
1457
|
+
*/
|
|
1458
|
+
track(eventType, eventName, properties = {}) {
|
|
1459
|
+
if (!this.isInitialized) {
|
|
1460
|
+
logger.warn('SDK not initialized, event dropped');
|
|
1461
|
+
return;
|
|
1462
|
+
}
|
|
1463
|
+
const event = {
|
|
1464
|
+
workspaceId: this.workspaceId,
|
|
1465
|
+
visitorId: this.visitorId,
|
|
1466
|
+
sessionId: this.sessionId,
|
|
1467
|
+
eventType: eventType,
|
|
1468
|
+
eventName,
|
|
1469
|
+
url: typeof window !== 'undefined' ? window.location.href : '',
|
|
1470
|
+
referrer: typeof document !== 'undefined' ? document.referrer || undefined : undefined,
|
|
1471
|
+
properties,
|
|
1472
|
+
device: getDeviceInfo(),
|
|
1473
|
+
utm: getUTMParams(),
|
|
1474
|
+
timestamp: new Date().toISOString(),
|
|
1475
|
+
sdkVersion: SDK_VERSION,
|
|
1476
|
+
};
|
|
1477
|
+
this.queue.push(event);
|
|
1478
|
+
logger.debug('Event tracked:', eventName, properties);
|
|
1479
|
+
}
|
|
1480
|
+
/**
|
|
1481
|
+
* Track a page view
|
|
1482
|
+
*/
|
|
1483
|
+
page(name, properties = {}) {
|
|
1484
|
+
const pageName = name || (typeof document !== 'undefined' ? document.title : 'Page View');
|
|
1485
|
+
this.track('page_view', pageName, {
|
|
1486
|
+
...properties,
|
|
1487
|
+
path: typeof window !== 'undefined' ? window.location.pathname : '',
|
|
1488
|
+
});
|
|
1489
|
+
}
|
|
1490
|
+
/**
|
|
1491
|
+
* Identify a visitor
|
|
1492
|
+
*/
|
|
1493
|
+
async identify(email, traits = {}) {
|
|
1494
|
+
if (!email) {
|
|
1495
|
+
logger.warn('Email is required for identification');
|
|
1496
|
+
return;
|
|
1497
|
+
}
|
|
1498
|
+
logger.info('Identifying visitor:', email);
|
|
1499
|
+
const result = await this.transport.sendIdentify({
|
|
1500
|
+
workspaceId: this.workspaceId,
|
|
1501
|
+
visitorId: this.visitorId,
|
|
1502
|
+
email,
|
|
1503
|
+
properties: traits,
|
|
1504
|
+
});
|
|
1505
|
+
if (result.success) {
|
|
1506
|
+
logger.info('Visitor identified successfully');
|
|
1507
|
+
}
|
|
1508
|
+
else {
|
|
1509
|
+
logger.error('Failed to identify visitor:', result.error);
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
/**
|
|
1513
|
+
* Update consent state
|
|
1514
|
+
*/
|
|
1515
|
+
consent(state) {
|
|
1516
|
+
logger.info('Consent updated:', state);
|
|
1517
|
+
// TODO: Implement consent management
|
|
1518
|
+
// - Store consent state
|
|
1519
|
+
// - Enable/disable tracking based on consent
|
|
1520
|
+
// - Notify plugins
|
|
1521
|
+
}
|
|
1522
|
+
/**
|
|
1523
|
+
* Toggle debug mode
|
|
1524
|
+
*/
|
|
1525
|
+
debug(enabled) {
|
|
1526
|
+
logger.enabled = enabled;
|
|
1527
|
+
logger.info(`Debug mode ${enabled ? 'enabled' : 'disabled'}`);
|
|
1528
|
+
}
|
|
1529
|
+
/**
|
|
1530
|
+
* Get visitor ID
|
|
1531
|
+
*/
|
|
1532
|
+
getVisitorId() {
|
|
1533
|
+
return this.visitorId;
|
|
1534
|
+
}
|
|
1535
|
+
/**
|
|
1536
|
+
* Get session ID
|
|
1537
|
+
*/
|
|
1538
|
+
getSessionId() {
|
|
1539
|
+
return this.sessionId;
|
|
1540
|
+
}
|
|
1541
|
+
/**
|
|
1542
|
+
* Get workspace ID
|
|
1543
|
+
*/
|
|
1544
|
+
getWorkspaceId() {
|
|
1545
|
+
return this.workspaceId;
|
|
1546
|
+
}
|
|
1547
|
+
/**
|
|
1548
|
+
* Get current configuration
|
|
1549
|
+
*/
|
|
1550
|
+
getConfig() {
|
|
1551
|
+
return { ...this.config };
|
|
1552
|
+
}
|
|
1553
|
+
/**
|
|
1554
|
+
* Force flush event queue
|
|
1555
|
+
*/
|
|
1556
|
+
async flush() {
|
|
1557
|
+
await this.queue.flush();
|
|
1558
|
+
}
|
|
1559
|
+
/**
|
|
1560
|
+
* Reset visitor and session (for logout)
|
|
1561
|
+
*/
|
|
1562
|
+
reset() {
|
|
1563
|
+
logger.info('Resetting visitor data');
|
|
1564
|
+
resetIds(this.config.useCookies);
|
|
1565
|
+
this.visitorId = getOrCreateVisitorId(this.config.useCookies);
|
|
1566
|
+
this.sessionId = getOrCreateSessionId(this.config.sessionTimeout);
|
|
1567
|
+
this.queue.clear();
|
|
1568
|
+
}
|
|
1569
|
+
/**
|
|
1570
|
+
* Destroy tracker and cleanup
|
|
1571
|
+
*/
|
|
1572
|
+
destroy() {
|
|
1573
|
+
logger.info('Destroying tracker');
|
|
1574
|
+
// Flush any remaining events
|
|
1575
|
+
this.queue.flush();
|
|
1576
|
+
// Destroy plugins
|
|
1577
|
+
for (const plugin of this.plugins) {
|
|
1578
|
+
if (plugin.destroy) {
|
|
1579
|
+
plugin.destroy();
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
this.plugins = [];
|
|
1583
|
+
// Destroy queue
|
|
1584
|
+
this.queue.destroy();
|
|
1585
|
+
this.isInitialized = false;
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
/**
|
|
1590
|
+
* Clianta SDK - CRM API Client
|
|
1591
|
+
* @version 1.0.0
|
|
1592
|
+
*/
|
|
1593
|
+
/**
|
|
1594
|
+
* CRM API Client for managing contacts and opportunities
|
|
1595
|
+
*/
|
|
1596
|
+
class CRMClient {
|
|
1597
|
+
constructor(apiEndpoint, workspaceId, authToken) {
|
|
1598
|
+
this.apiEndpoint = apiEndpoint;
|
|
1599
|
+
this.workspaceId = workspaceId;
|
|
1600
|
+
this.authToken = authToken;
|
|
1601
|
+
}
|
|
1602
|
+
/**
|
|
1603
|
+
* Set authentication token for API requests
|
|
1604
|
+
*/
|
|
1605
|
+
setAuthToken(token) {
|
|
1606
|
+
this.authToken = token;
|
|
1607
|
+
}
|
|
1608
|
+
/**
|
|
1609
|
+
* Make authenticated API request
|
|
1610
|
+
*/
|
|
1611
|
+
async request(endpoint, options = {}) {
|
|
1612
|
+
const url = `${this.apiEndpoint}${endpoint}`;
|
|
1613
|
+
const headers = {
|
|
1614
|
+
'Content-Type': 'application/json',
|
|
1615
|
+
...(options.headers || {}),
|
|
1616
|
+
};
|
|
1617
|
+
if (this.authToken) {
|
|
1618
|
+
headers['Authorization'] = `Bearer ${this.authToken}`;
|
|
1619
|
+
}
|
|
1620
|
+
try {
|
|
1621
|
+
const response = await fetch(url, {
|
|
1622
|
+
...options,
|
|
1623
|
+
headers,
|
|
1624
|
+
});
|
|
1625
|
+
const data = await response.json();
|
|
1626
|
+
if (!response.ok) {
|
|
1627
|
+
return {
|
|
1628
|
+
success: false,
|
|
1629
|
+
error: data.message || 'Request failed',
|
|
1630
|
+
status: response.status,
|
|
1631
|
+
};
|
|
1632
|
+
}
|
|
1633
|
+
return {
|
|
1634
|
+
success: true,
|
|
1635
|
+
data: data.data || data,
|
|
1636
|
+
status: response.status,
|
|
1637
|
+
};
|
|
1638
|
+
}
|
|
1639
|
+
catch (error) {
|
|
1640
|
+
return {
|
|
1641
|
+
success: false,
|
|
1642
|
+
error: error instanceof Error ? error.message : 'Network error',
|
|
1643
|
+
status: 0,
|
|
1644
|
+
};
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
// ============================================
|
|
1648
|
+
// CONTACTS API
|
|
1649
|
+
// ============================================
|
|
1650
|
+
/**
|
|
1651
|
+
* Get all contacts with pagination
|
|
1652
|
+
*/
|
|
1653
|
+
async getContacts(params) {
|
|
1654
|
+
const queryParams = new URLSearchParams();
|
|
1655
|
+
if (params?.page)
|
|
1656
|
+
queryParams.set('page', params.page.toString());
|
|
1657
|
+
if (params?.limit)
|
|
1658
|
+
queryParams.set('limit', params.limit.toString());
|
|
1659
|
+
if (params?.search)
|
|
1660
|
+
queryParams.set('search', params.search);
|
|
1661
|
+
if (params?.status)
|
|
1662
|
+
queryParams.set('status', params.status);
|
|
1663
|
+
const query = queryParams.toString();
|
|
1664
|
+
const endpoint = `/api/workspaces/${this.workspaceId}/contacts${query ? `?${query}` : ''}`;
|
|
1665
|
+
return this.request(endpoint);
|
|
1666
|
+
}
|
|
1667
|
+
/**
|
|
1668
|
+
* Get a single contact by ID
|
|
1669
|
+
*/
|
|
1670
|
+
async getContact(contactId) {
|
|
1671
|
+
return this.request(`/api/workspaces/${this.workspaceId}/contacts/${contactId}`);
|
|
1672
|
+
}
|
|
1673
|
+
/**
|
|
1674
|
+
* Create a new contact
|
|
1675
|
+
*/
|
|
1676
|
+
async createContact(contact) {
|
|
1677
|
+
return this.request(`/api/workspaces/${this.workspaceId}/contacts`, {
|
|
1678
|
+
method: 'POST',
|
|
1679
|
+
body: JSON.stringify(contact),
|
|
1680
|
+
});
|
|
1681
|
+
}
|
|
1682
|
+
/**
|
|
1683
|
+
* Update an existing contact
|
|
1684
|
+
*/
|
|
1685
|
+
async updateContact(contactId, updates) {
|
|
1686
|
+
return this.request(`/api/workspaces/${this.workspaceId}/contacts/${contactId}`, {
|
|
1687
|
+
method: 'PUT',
|
|
1688
|
+
body: JSON.stringify(updates),
|
|
1689
|
+
});
|
|
1690
|
+
}
|
|
1691
|
+
/**
|
|
1692
|
+
* Delete a contact
|
|
1693
|
+
*/
|
|
1694
|
+
async deleteContact(contactId) {
|
|
1695
|
+
return this.request(`/api/workspaces/${this.workspaceId}/contacts/${contactId}`, {
|
|
1696
|
+
method: 'DELETE',
|
|
1697
|
+
});
|
|
1698
|
+
}
|
|
1699
|
+
// ============================================
|
|
1700
|
+
// OPPORTUNITIES API
|
|
1701
|
+
// ============================================
|
|
1702
|
+
/**
|
|
1703
|
+
* Get all opportunities with pagination
|
|
1704
|
+
*/
|
|
1705
|
+
async getOpportunities(params) {
|
|
1706
|
+
const queryParams = new URLSearchParams();
|
|
1707
|
+
if (params?.page)
|
|
1708
|
+
queryParams.set('page', params.page.toString());
|
|
1709
|
+
if (params?.limit)
|
|
1710
|
+
queryParams.set('limit', params.limit.toString());
|
|
1711
|
+
if (params?.pipelineId)
|
|
1712
|
+
queryParams.set('pipelineId', params.pipelineId);
|
|
1713
|
+
if (params?.stageId)
|
|
1714
|
+
queryParams.set('stageId', params.stageId);
|
|
1715
|
+
const query = queryParams.toString();
|
|
1716
|
+
const endpoint = `/api/workspaces/${this.workspaceId}/opportunities${query ? `?${query}` : ''}`;
|
|
1717
|
+
return this.request(endpoint);
|
|
1718
|
+
}
|
|
1719
|
+
/**
|
|
1720
|
+
* Get a single opportunity by ID
|
|
1721
|
+
*/
|
|
1722
|
+
async getOpportunity(opportunityId) {
|
|
1723
|
+
return this.request(`/api/workspaces/${this.workspaceId}/opportunities/${opportunityId}`);
|
|
1724
|
+
}
|
|
1725
|
+
/**
|
|
1726
|
+
* Create a new opportunity
|
|
1727
|
+
*/
|
|
1728
|
+
async createOpportunity(opportunity) {
|
|
1729
|
+
return this.request(`/api/workspaces/${this.workspaceId}/opportunities`, {
|
|
1730
|
+
method: 'POST',
|
|
1731
|
+
body: JSON.stringify(opportunity),
|
|
1732
|
+
});
|
|
1733
|
+
}
|
|
1734
|
+
/**
|
|
1735
|
+
* Update an existing opportunity
|
|
1736
|
+
*/
|
|
1737
|
+
async updateOpportunity(opportunityId, updates) {
|
|
1738
|
+
return this.request(`/api/workspaces/${this.workspaceId}/opportunities/${opportunityId}`, {
|
|
1739
|
+
method: 'PUT',
|
|
1740
|
+
body: JSON.stringify(updates),
|
|
1741
|
+
});
|
|
1742
|
+
}
|
|
1743
|
+
/**
|
|
1744
|
+
* Delete an opportunity
|
|
1745
|
+
*/
|
|
1746
|
+
async deleteOpportunity(opportunityId) {
|
|
1747
|
+
return this.request(`/api/workspaces/${this.workspaceId}/opportunities/${opportunityId}`, {
|
|
1748
|
+
method: 'DELETE',
|
|
1749
|
+
});
|
|
1750
|
+
}
|
|
1751
|
+
/**
|
|
1752
|
+
* Move opportunity to a different stage
|
|
1753
|
+
*/
|
|
1754
|
+
async moveOpportunity(opportunityId, stageId) {
|
|
1755
|
+
return this.request(`/api/workspaces/${this.workspaceId}/opportunities/${opportunityId}/move`, {
|
|
1756
|
+
method: 'POST',
|
|
1757
|
+
body: JSON.stringify({ stageId }),
|
|
1758
|
+
});
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
/**
|
|
1763
|
+
* Clianta SDK
|
|
1764
|
+
* Professional CRM and tracking SDK for lead generation
|
|
1765
|
+
* @version 1.0.0
|
|
1766
|
+
*/
|
|
1767
|
+
// Global instance cache
|
|
1768
|
+
let globalInstance = null;
|
|
1769
|
+
/**
|
|
1770
|
+
* Initialize or get the Clianta tracker instance
|
|
1771
|
+
*
|
|
1772
|
+
* @example
|
|
1773
|
+
* // Simple initialization
|
|
1774
|
+
* const tracker = clianta('your-workspace-id');
|
|
1775
|
+
*
|
|
1776
|
+
* @example
|
|
1777
|
+
* // With configuration
|
|
1778
|
+
* const tracker = clianta('your-workspace-id', {
|
|
1779
|
+
* debug: true,
|
|
1780
|
+
* plugins: ['pageView', 'forms', 'scroll'],
|
|
1781
|
+
* });
|
|
1782
|
+
*/
|
|
1783
|
+
function clianta(workspaceId, config) {
|
|
1784
|
+
// Return existing instance if same workspace
|
|
1785
|
+
if (globalInstance && globalInstance.getWorkspaceId() === workspaceId) {
|
|
1786
|
+
return globalInstance;
|
|
1787
|
+
}
|
|
1788
|
+
// Destroy existing instance if workspace changed
|
|
1789
|
+
if (globalInstance) {
|
|
1790
|
+
globalInstance.destroy();
|
|
1791
|
+
}
|
|
1792
|
+
// Create new instance
|
|
1793
|
+
globalInstance = new Tracker(workspaceId, config);
|
|
1794
|
+
return globalInstance;
|
|
1795
|
+
}
|
|
1796
|
+
// Attach to window for <script> usage
|
|
1797
|
+
if (typeof window !== 'undefined') {
|
|
1798
|
+
window.clianta = clianta;
|
|
1799
|
+
window.Clianta = {
|
|
1800
|
+
clianta,
|
|
1801
|
+
Tracker,
|
|
1802
|
+
CRMClient,
|
|
1803
|
+
};
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
exports.CRMClient = CRMClient;
|
|
1807
|
+
exports.SDK_VERSION = SDK_VERSION;
|
|
1808
|
+
exports.Tracker = Tracker;
|
|
1809
|
+
exports.clianta = clianta;
|
|
1810
|
+
exports.default = clianta;
|
|
1811
|
+
|
|
1812
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
1813
|
+
|
|
1814
|
+
}));
|
|
1815
|
+
//# sourceMappingURL=clianta.umd.js.map
|