@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.
@@ -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