/** * ConversionIQ JavaScript SDK * Lightweight tracking SDK for conversion optimization insights * * @version 1.0.0 * @author ConversionIQ Team */ import { EventQueue } from './core/EventQueue'; import { NetworkManager } from './core/NetworkManager'; import { PrivacyManager } from './core/PrivacyManager'; import { EnvironmentDetector } from './core/EnvironmentDetector'; import { ErrorHandler } from './core/ErrorHandler'; import { MicroInteractionTracking } from './features/MicroInteractionTracking'; import { ConversionIQConfig, UserContext, EventData, TrackingOptions, ConversionData, PrivacySettings } from './types/index'; declare global { interface Window { ConversionIQ: ConversionIQSDK | any[]; __conversioniq_config?: ConversionIQConfig; __conversioniq_queue?: any[]; } } /** * Main ConversionIQ SDK class */ class ConversionIQSDK { private config: ConversionIQConfig; private eventQueue: EventQueue; private networkManager: NetworkManager; private privacyManager: PrivacyManager; private environmentDetector: EnvironmentDetector; private errorHandler: ErrorHandler; private microInteractionTracking?: any; // Lazy loaded private isInitialized = false; private sessionId: string; private userId?: string; private eventListeners: Array<{ target: EventTarget; type: string; handler: EventListener; options?: boolean | AddEventListenerOptions; }> = []; constructor(config: ConversionIQConfig) { // Support batchInterval as an alias for flushInterval const flushInterval = config.batchInterval || config.flushInterval || 5000; this.config = { endpoint: 'https://conversioniq-api-dev.azurewebsites.net', batchSize: 10, timeout: 5000, enableAutoTracking: true, enablePrivacyMode: true, debug: false, ...config, flushInterval }; this.sessionId = this.generateSessionId(); this.errorHandler = new ErrorHandler(this.config); this.privacyManager = new PrivacyManager(this.config); this.environmentDetector = new EnvironmentDetector(); this.networkManager = new NetworkManager(this.config, this.errorHandler); this.eventQueue = new EventQueue(this.config, this.networkManager); } /** * Initialize the SDK */ public async init(): Promise { try { if (this.isInitialized) { console.warn('ConversionIQ: Already initialized'); return; } // Wait for DOM to be ready await this.waitForDOM(); // Initialize privacy manager await this.privacyManager.init(); // Check if tracking is allowed if (!this.privacyManager.canTrack('essential')) { if (this.config.debug) { console.log('ConversionIQ: Tracking disabled due to privacy settings'); } return; } // Start auto-tracking if enabled if (this.config.enableAutoTracking) { this.startAutoTracking(); } // Start micro-interaction tracking if enabled (supports both property names) if ((this.config as any).enableMicroInteractionTracking || (this.config as any).enableMicroInteractions) { await this.startMicroInteractionTracking(); } // Set initialized flag before tracking events this.isInitialized = true; // Track initial page view await this.trackPageView(); // Process any queued events from before initialization this.processQueuedEvents(); if (this.config.debug) { console.log('ConversionIQ: Initialized successfully', { sessionId: this.sessionId, endpoint: this.config.endpoint, website: this.config.websiteId }); } } catch (error) { this.errorHandler.handle(error as Error, 'initialization'); } } /** * Set user identification */ public identify(userId: string, context?: Partial): void { try { this.userId = userId; if (context) { // Store user context for future events this.config.userContext = { ...this.config.userContext, ...context }; } // Track identify event this.track('identify', { user_id: userId, context: context || {} }); if (this.config.debug) { console.log('ConversionIQ: User identified', { userId, context }); } } catch (error) { this.errorHandler.handle(error as Error, 'identify'); } } /** * Track a custom event */ public async track(eventType: string, data?: EventData, options?: TrackingOptions): Promise { try { if (!this.isInitialized && eventType !== 'identify') { // Queue event for processing after initialization this.queueEvent(eventType, data, options); return; } // Check privacy permissions if (!this.privacyManager.canTrack(this.getDataTypeForEvent(eventType))) { if (this.config.debug) { console.log('ConversionIQ: Event blocked by privacy settings', eventType); } return; } const eventPayload = this.buildEventPayload(eventType, data, options); await this.eventQueue.enqueue(eventPayload); if (this.config.debug) { console.log('ConversionIQ: Event tracked', eventPayload); } } catch (error) { this.errorHandler.handle(error as Error, 'track'); } } /** * Track a conversion event */ public async trackConversion(data: ConversionData): Promise { await this.track('conversion', { value: data.value, currency: data.currency || 'USD', transaction_id: data.transactionId, items: data.items || [], metadata: data.metadata || {} }); } /** * Track a page view */ public async trackPageView(url?: string): Promise { const pageUrl = url || window.location.href; await this.track('page_view', { url: pageUrl, title: document.title, referrer: document.referrer, path: window.location.pathname, search: window.location.search, hash: window.location.hash }); } /** * Track element interaction */ public async trackClick(element: Element | string, metadata?: Record): Promise { const targetElement = typeof element === 'string' ? document.querySelector(element) : element; if (!targetElement) { if (this.config.debug) { console.warn('ConversionIQ: Element not found for click tracking', element); } return; } const selector = this.getElementSelector(targetElement); const elementData = this.getElementData(targetElement); await this.track('click', { element: selector, element_type: targetElement.tagName.toLowerCase(), element_text: elementData.text, element_attributes: elementData.attributes, page_url: window.location.href, timestamp: Date.now(), metadata: metadata || {} }); } /** * Flush all pending events immediately */ public async flush(): Promise { try { await this.eventQueue.flush(); if (this.config.debug) { console.log('ConversionIQ: Events flushed'); } } catch (error) { this.errorHandler.handle(error as Error, 'flush'); } } /** * Update privacy consent */ public updatePrivacyConsent(consents: Record): void { this.privacyManager.updateConsent(consents); if (this.config.debug) { console.log('ConversionIQ: Privacy consent updated', consents); } } /** * Get current privacy status */ public getPrivacyStatus(): PrivacySettings { return this.privacyManager.getConsentStatus(); } /** * Get session ID */ public getSessionId(): string { return this.sessionId; } /** * Get micro-interaction tracking statistics */ public getMicroInteractionStats(): any { if (!this.microInteractionTracking) { return null; } return this.microInteractionTracking.getStats(); } /** * Update micro-interaction tracking profile * @param profile - The profile to use: 'minimal', 'balanced', 'detailed', or 'performance' */ public setMicroInteractionProfile(profile: 'minimal' | 'balanced' | 'detailed' | 'performance'): void { if (!this.microInteractionTracking) { if (this.config.debug) { console.warn('ConversionIQ: Micro-interaction tracking not enabled. Enable it with enableMicroInteractions: true'); } return; } this.microInteractionTracking.setProfile(profile); } /** * Get current micro-interaction tracking profile */ public getMicroInteractionProfile(): string | null { if (!this.microInteractionTracking) { return null; } return this.microInteractionTracking.getProfile(); } // Private methods private async startMicroInteractionTracking(): Promise { try { this.microInteractionTracking = new MicroInteractionTracking(this.config, this); this.microInteractionTracking.start(); if (this.config.debug) { console.log('ConversionIQ: Micro-interaction tracking started'); } } catch (error) { this.errorHandler.handle(error as Error, 'micro_interaction_tracking'); } } private async waitForDOM(): Promise { return new Promise((resolve) => { if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => resolve()); } else { resolve(); } }); } private startAutoTracking(): void { // Track clicks on important elements const clickHandler = (event: Event) => { const target = event.target as Element; if (this.shouldAutoTrack(target)) { this.trackClick(target); } }; document.addEventListener('click', clickHandler, true); this.eventListeners.push({ target: document, type: 'click', handler: clickHandler, options: true }); // Track form submissions const submitHandler = (event: Event) => { const form = event.target as HTMLFormElement; this.track('form_submit', { form_id: form.id || null, form_action: form.action || null, form_method: form.method || 'get' }); }; document.addEventListener('submit', submitHandler, true); this.eventListeners.push({ target: document, type: 'submit', handler: submitHandler, options: true }); // Track scroll depth let maxScrollDepth = 0; let scrollTimeout: number; const scrollHandler = () => { clearTimeout(scrollTimeout); scrollTimeout = window.setTimeout(() => { const scrollDepth = this.getScrollDepth(); if (scrollDepth > maxScrollDepth) { maxScrollDepth = scrollDepth; // Track scroll milestones if (scrollDepth >= 25 && maxScrollDepth < 25) { this.track('scroll', { depth: 25 }); } else if (scrollDepth >= 50 && maxScrollDepth < 50) { this.track('scroll', { depth: 50 }); } else if (scrollDepth >= 75 && maxScrollDepth < 75) { this.track('scroll', { depth: 75 }); } else if (scrollDepth >= 90 && maxScrollDepth < 90) { this.track('scroll', { depth: 90 }); } } }, 250); }; window.addEventListener('scroll', scrollHandler); this.eventListeners.push({ target: window, type: 'scroll', handler: scrollHandler }); // Track page unload const unloadHandler = () => { this.flush(); }; window.addEventListener('beforeunload', unloadHandler); this.eventListeners.push({ target: window, type: 'beforeunload', handler: unloadHandler }); // Track SPA navigation if (this.environmentDetector.isSPA()) { this.setupSPATracking(); } } private shouldAutoTrack(element: Element): boolean { const tagName = element.tagName.toLowerCase(); // Track buttons and links if (tagName === 'button' || tagName === 'a') { return true; } // Track elements with data-track attribute if (element.hasAttribute('data-track')) { return true; } // Track elements with common CTA classes const classList = Array.from(element.classList); const ctaKeywords = ['btn', 'button', 'cta', 'submit', 'checkout', 'buy', 'purchase']; return ctaKeywords.some(keyword => classList.some(className => className.toLowerCase().includes(keyword)) ); } private setupSPATracking(): void { // Hook into History API const originalPushState = history.pushState; const originalReplaceState = history.replaceState; history.pushState = (...args) => { originalPushState.apply(history, args); setTimeout(() => this.trackPageView(), 100); }; history.replaceState = (...args) => { originalReplaceState.apply(history, args); setTimeout(() => this.trackPageView(), 100); }; const popstateHandler = () => { setTimeout(() => this.trackPageView(), 100); }; window.addEventListener('popstate', popstateHandler); this.eventListeners.push({ target: window, type: 'popstate', handler: popstateHandler }); } private buildEventPayload(eventType: string, data?: EventData, options?: TrackingOptions): any { const timestamp = new Date().toISOString(); return { website_id: this.config.websiteId, event_type: eventType, user_id: this.userId, session_id: this.sessionId, timestamp: timestamp, user_agent: navigator.userAgent, page_url: window.location.href, referrer: document.referrer, metadata: { ...data, device_type: this.getDeviceType(), viewport_width: window.innerWidth, viewport_height: window.innerHeight, screen_width: screen.width, screen_height: screen.height, timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, language: navigator.language, user_context: this.config.userContext || {}, sdk_version: '2.0.12', ...options?.metadata } }; } private getElementSelector(element: Element): string { // Generate a unique selector for the element if (element.id) { return `#${element.id}`; } if (element.className) { const classes = Array.from(element.classList).join('.'); return `.${classes}`; } // Fallback to tag name with nth-child const parent = element.parentElement; if (parent) { const siblings = Array.from(parent.children); const index = siblings.indexOf(element) + 1; return `${element.tagName.toLowerCase()}:nth-child(${index})`; } return element.tagName.toLowerCase(); } private getElementData(element: Element): { text: string; attributes: Record } { const text = element.textContent?.trim() || ''; const attributes: Record = {}; // Collect important attributes const importantAttrs = ['class', 'id', 'type', 'href', 'data-track', 'title', 'aria-label']; importantAttrs.forEach(attr => { const value = element.getAttribute(attr); if (value) { attributes[attr] = value; } }); return { text, attributes }; } private getScrollDepth(): number { const windowHeight = window.innerHeight; const documentHeight = document.documentElement.scrollHeight; const scrollTop = window.pageYOffset || document.documentElement.scrollTop; return Math.round((scrollTop / (documentHeight - windowHeight)) * 100); } private getDeviceType(): string { const userAgent = navigator.userAgent.toLowerCase(); if (/tablet|ipad|playbook|silk/.test(userAgent)) { return 'tablet'; } if (/mobile|iphone|ipod|android|blackberry|opera|mini|windows\sce|palm|smartphone|iemobile/.test(userAgent)) { return 'mobile'; } return 'desktop'; } private getDataTypeForEvent(eventType: string): 'essential' | 'functional' | 'analytics' | 'marketing' { const dataTypeMap: Record = { 'page_view': 'essential', 'error': 'essential', 'click': 'functional', 'scroll': 'functional', 'form_submit': 'functional', 'conversion': 'analytics', 'identify': 'analytics' }; return (dataTypeMap[eventType] || 'analytics') as any; } private queueEvent(eventType: string, data?: EventData, options?: TrackingOptions): void { // Store in temporary queue for processing after init if (!window.__conversioniq_queue) { window.__conversioniq_queue = []; } window.__conversioniq_queue.push(['track', eventType, data, options]); } private processQueuedEvents(): void { const queue = (window as any).__conversioniq_queue || []; queue.forEach((args: any[]) => { const [method, ...params] = args; if (method === 'track') { this.track(params[0], params[1], params[2]); } }); // Clear the queue (window as any).__conversioniq_queue = []; } private generateSessionId(): string { return 'ciq_' + Date.now().toString(36) + Math.random().toString(36).substring(2); } /** * Clean up and destroy the SDK */ public destroy(): void { try { // Remove all event listeners this.eventListeners.forEach(({ target, type, handler, options }) => { target.removeEventListener(type, handler, options); }); this.eventListeners = []; // Stop micro-interaction tracking if (this.microInteractionTracking) { this.microInteractionTracking.stop(); this.microInteractionTracking = undefined; } // Destroy sub-components if (this.eventQueue) { this.eventQueue.destroy(); } if (this.privacyManager) { this.privacyManager.destroy(); } if (this.errorHandler) { this.errorHandler.destroy(); } this.isInitialized = false; if (this.config.debug) { console.log('ConversionIQ: SDK destroyed'); } } catch (error) { console.error('ConversionIQ: Error during destroy', error); } } } // Initialize SDK when script loads (only if not using ES modules) (function() { // Skip auto-initialization if SDK is being imported as a module // This allows frameworks like React/Vue to manually instantiate the SDK if (typeof module !== 'undefined' && module.exports) { return; } // Get configuration from script tag or global variable const config = window.__conversioniq_config || {}; // Get API key from script tag const scriptTag = document.currentScript as HTMLScriptElement; if (scriptTag && scriptTag.dataset.apiKey) { config.apiKey = scriptTag.dataset.apiKey; config.websiteId = scriptTag.dataset.websiteId || scriptTag.dataset.apiKey; } // Only auto-initialize if configuration is provided via script tag or window.__conversioniq_config // This prevents creating a demo instance when used as an npm package if (!config.apiKey && !config.websiteId) { // Don't auto-initialize - wait for manual initialization window.ConversionIQ = window.ConversionIQ || []; return; } // Set defaults if not provided config.apiKey = config.apiKey || 'demo'; config.websiteId = config.websiteId || 'demo'; // Warn if using demo credentials if (config.apiKey === 'demo' || config.websiteId === 'demo') { console.warn( 'ConversionIQ: Using demo credentials. Please configure your actual apiKey and websiteId from your dashboard. ' + 'Visit https://dashboard.conversioniq.com to get your credentials.' ); } // Replace the queue array with the actual SDK const existingQueue = window.ConversionIQ || []; const sdk = new ConversionIQSDK(config); // Initialize the SDK sdk.init().catch(error => { console.error('ConversionIQ: Failed to initialize', error); }); // Process any queued calls if (Array.isArray(existingQueue)) { existingQueue.forEach((args: any[]) => { const [method, ...params] = args; if (typeof (sdk as any)[method] === 'function') { (sdk as any)[method](...params); } }); } // Expose SDK globally window.ConversionIQ = sdk; })(); export default ConversionIQSDK;