UNPKG

@tinytapanalytics/sdk

Version:

Behavioral psychology platform that detects visitor frustration, predicts abandonment, and helps you save at-risk conversions in real-time

1,012 lines (874 loc) 32.2 kB
/** * TinyTapAnalytics JavaScript SDK * Lightweight tracking SDK for conversion optimization insights * * @version 1.0.0 * @author TinyTapAnalytics 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 { Heatmap } from './features/Heatmap'; import { PerformanceMonitoring } from './features/PerformanceMonitoring'; import { TinyTapAnalyticsConfig, UserContext, EventData, TrackingOptions, ConversionData, PrivacySettings } from './types/index'; import packageJson from '../package.json'; declare global { interface Window { TinyTapAnalytics: TinyTapAnalyticsSDK | unknown[]; __tinytapanalytics_config?: TinyTapAnalyticsConfig; } } /** * Main TinyTapAnalytics SDK class */ class TinyTapAnalyticsSDK { private config: TinyTapAnalyticsConfig; private eventQueue: EventQueue; private networkManager: NetworkManager; private privacyManager: PrivacyManager; private environmentDetector: EnvironmentDetector; private errorHandler: ErrorHandler; private microInteractionTracking?: any; // Lazy loaded private heatmap?: any; // Lazy loaded private performanceMonitoring?: PerformanceMonitoring; private isInitialized = false; private sessionId: string; private userId?: string; private eventListeners: Array<{ target: EventTarget; type: string; handler: EventListener; options?: boolean | AddEventListenerOptions; }> = []; constructor(config: TinyTapAnalyticsConfig) { // Support batchInterval as an alias for flushInterval const flushInterval = config.batchInterval || config.flushInterval || 5000; // Use build-time injected API URL or config override const defaultEndpoint = process.env.API_URL || 'https://api.tinytapanalytics.com'; this.config = { endpoint: defaultEndpoint, 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<void> { try { if (this.isInitialized) { console.warn('TinyTapAnalytics: 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('TinyTapAnalytics: Essential tracking blocked by privacy settings, clearing consent data'); } // Clear old consent data that's blocking essential tracking // This shouldn't happen normally, but handles corrupted localStorage try { localStorage.removeItem('tinytapanalytics_consent'); // Reinitialize privacy manager await this.privacyManager.init(); } catch (e) { // If still blocked, log and return if (!this.privacyManager.canTrack('essential')) { if (this.config.debug) { console.log('TinyTapAnalytics: Essential tracking still blocked after clearing'); } return; } } } // Auto-grant analytics consent for integrations (like Shopify) where store owner explicitly installed // If enableAutoTracking is true, assume the site owner wants full analytics tracking if (this.config.enableAutoTracking && !this.privacyManager.canTrack('analytics')) { // Grant analytics consent automatically for auto-tracking scenarios this.updatePrivacyConsent({ necessary: true, analytics: true, marketing: false // Keep marketing opt-in by default }); } // 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(); } // Start heatmap tracking if enabled if (this.config.enableHeatmap) { await this.startHeatmapTracking(); } // Start performance monitoring if enabled if (this.config.enablePerformanceMonitoring) { this.startPerformanceMonitoring(); } // 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('TinyTapAnalytics: 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<UserContext>): 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('TinyTapAnalytics: 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<void> { 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('TinyTapAnalytics: 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('TinyTapAnalytics: Event tracked', eventPayload); } } catch (error) { this.errorHandler.handle(error as Error, 'track'); } } /** * Track a conversion event */ public async trackConversion(data: ConversionData): Promise<void> { 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<void> { 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<string, any>): Promise<void> { const targetElement = typeof element === 'string' ? document.querySelector(element) : element; if (!targetElement) { if (this.config.debug) { console.warn('TinyTapAnalytics: 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<void> { try { await this.eventQueue.flush(); if (this.config.debug) { console.log('TinyTapAnalytics: Events flushed'); } } catch (error) { this.errorHandler.handle(error as Error, 'flush'); } } /** * Update privacy consent */ public updatePrivacyConsent(consents: Record<string, boolean>): void { this.privacyManager.updateConsent(consents); if (this.config.debug) { console.log('TinyTapAnalytics: 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('TinyTapAnalytics: 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<void> { try { this.microInteractionTracking = new MicroInteractionTracking(this.config, this); this.microInteractionTracking.start(); if (this.config.debug) { console.log('TinyTapAnalytics: Micro-interaction tracking started'); } } catch (error) { this.errorHandler.handle(error as Error, 'micro_interaction_tracking'); } } private async startHeatmapTracking(): Promise<void> { try { this.heatmap = new Heatmap(this.config, this); this.heatmap.start(); if (this.config.debug) { const samplingRate = this.config.heatmapSamplingRate ?? 0.1; // Default 10% console.log('TinyTapAnalytics: Heatmap tracking started with sampling rate:', samplingRate); } } catch (error) { this.errorHandler.handle(error as Error, 'heatmap_tracking'); } } private startPerformanceMonitoring(): void { try { this.performanceMonitoring = new PerformanceMonitoring(this.config, this); this.performanceMonitoring.start(); if (this.config.debug) { console.log('TinyTapAnalytics: Performance monitoring started'); } } catch (error) { this.errorHandler.handle(error as Error, 'performance_monitoring'); } } private async waitForDOM(): Promise<void> { 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 }); // Shopify-specific: Track Add to Cart actions this.setupShopifyCartTracking(); // 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: packageJson.version, ...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<string, string> } { const text = element.textContent?.trim() || ''; const attributes: Record<string, string> = {}; // 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<string, 'essential' | 'functional' | 'analytics' | 'marketing'> = { 'page_view': 'essential', 'error': 'essential', 'click': 'functional', 'scroll': 'functional', 'form_submit': 'functional', 'conversion': 'analytics', 'identify': 'analytics' }; return (dataTypeMap[eventType] || 'analytics') as 'essential' | 'functional' | 'analytics' | 'marketing'; } private queueEvent(eventType: string, data?: EventData, options?: TrackingOptions): void { // Store in temporary queue for processing after init if (!window.__tinytapanalytics_queue) { window.__tinytapanalytics_queue = []; } window.__tinytapanalytics_queue.push(['track', eventType, data, options]); } private processQueuedEvents(): void { const queue = window.__tinytapanalytics_queue || []; queue.forEach((args) => { const [method, ...params] = args; if (method === 'track') { this.track(params[0] as string, params[1] as EventData, params[2] as TrackingOptions); } }); // Clear the queue window.__tinytapanalytics_queue = []; } private generateSessionId(): string { return 'ciq_' + Date.now().toString(36) + Math.random().toString(36).substring(2); } /** * Set up Shopify-specific cart tracking */ private setupShopifyCartTracking(): void { // Intercept Shopify's fetch/XMLHttpRequest for cart actions const originalFetch = window.fetch; window.fetch = async (...args) => { const [resource, config] = args; const url = typeof resource === 'string' ? resource : (resource as Request).url; // Track cart add/update/change if (url.includes('/cart/add') || url.includes('/cart/update') || url.includes('/cart/change')) { try { const body = config?.body ? JSON.parse(config.body as string) : {}; this.track('add_to_cart', { product_id: body.id || body.items?.[0]?.id, quantity: body.quantity || body.items?.[0]?.quantity || 1, variant_id: body.variant_id || body.id, cart_action: url.includes('/cart/add') ? 'add' : url.includes('/cart/update') ? 'update' : 'change' }); } catch (error) { this.track('add_to_cart', { cart_action: 'unknown' }); } } // Track remove from cart if (url.includes('/cart/change') && config?.method?.toUpperCase() === 'POST') { try { const body = config?.body ? JSON.parse(config.body as string) : {}; if (body.quantity === 0) { this.track('remove_from_cart', { product_id: body.id, variant_id: body.id }); } } catch (error) { // Silent fail } } // Track cart.js requests (cart view) if (url.includes('/cart.js') && config?.method?.toUpperCase() === 'GET') { this.track('cart_view', { source: 'api' }); } // Track search if (url.includes('/search') || url.includes('/search/suggest')) { try { const urlObj = new URL(url, window.location.origin); const query = urlObj.searchParams.get('q') || urlObj.searchParams.get('query'); if (query) { this.track('search', { query: query, results_shown: true }); } } catch (error) { // Silent fail } } return originalFetch.apply(window, args); }; // Track form-based cart additions document.addEventListener('submit', (event) => { const form = event.target as HTMLFormElement; if (form.action?.includes('/cart/add')) { const formData = new FormData(form); this.track('add_to_cart', { product_id: formData.get('id'), quantity: formData.get('quantity') || 1, variant_id: formData.get('id'), cart_action: 'form_submit' }); } }, true); // Track checkout button clicks document.addEventListener('click', (event) => { const target = event.target as HTMLElement; const button = target.closest('button, a, [role="button"]'); if (button) { const text = button.textContent?.toLowerCase() || ''; const href = (button as HTMLAnchorElement).href || ''; // Checkout started if (text.includes('checkout') || text.includes('check out') || href.includes('/checkout')) { this.track('checkout_started', { button_text: button.textContent?.trim(), button_type: button.tagName.toLowerCase() }); } // Add to wishlist if (text.includes('wishlist') || text.includes('save for later') || button.classList.contains('wishlist')) { this.track('add_to_wishlist', { button_text: button.textContent?.trim() }); } // Quick view if (text.includes('quick view') || text.includes('quick shop') || button.classList.contains('quick-view')) { this.track('product_quick_view', { button_text: button.textContent?.trim() }); } } }, true); // Track product views (on product pages) if (window.location.pathname.includes('/products/')) { // Extract product handle from URL const productHandle = window.location.pathname.split('/products/')[1]?.split('/')[0]; if (productHandle) { this.track('product_view', { product_handle: productHandle, url: window.location.href }); } } // Track cart page views if (window.location.pathname.includes('/cart')) { this.track('cart_view', { source: 'page' }); } // Track search page if (window.location.pathname.includes('/search') && window.location.search.includes('q=')) { try { const params = new URLSearchParams(window.location.search); const query = params.get('q'); if (query) { this.track('search', { query: query, source: 'page' }); } } catch (error) { // Silent fail } } // Track collection/category views if (window.location.pathname.includes('/collections/')) { const collectionHandle = window.location.pathname.split('/collections/')[1]?.split('/')[0]; if (collectionHandle && collectionHandle !== 'all') { this.track('collection_view', { collection_handle: collectionHandle, url: window.location.href }); } } // Track order confirmation page (purchase complete) if (window.location.pathname.includes('/orders/') || window.location.pathname.includes('/thank')) { // Try to extract order info from Shopify's checkout object if (typeof window !== 'undefined' && window.Shopify?.checkout) { const checkout = window.Shopify.checkout; this.track('purchase_complete', { order_id: checkout.order_id, total_price: checkout.total_price, currency: checkout.currency, order_number: checkout.order_number }); } else { // Fallback - just track that user reached thank you page this.track('purchase_complete', { source: 'thank_you_page' }); } } if (this.config.debug) { console.log('TinyTapAnalytics: Enhanced Shopify e-commerce tracking initialized'); } } /** * 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; } // Stop heatmap tracking if (this.heatmap) { this.heatmap.stop(); this.heatmap = undefined; } // Stop performance monitoring if (this.performanceMonitoring) { this.performanceMonitoring.stop(); this.performanceMonitoring = 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('TinyTapAnalytics: SDK destroyed'); } } catch (error) { console.error('TinyTapAnalytics: 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 // Check if we're in a browser environment with a document (not Node.js/SSR) if (typeof window === 'undefined' || typeof document === 'undefined') { return; } // Get configuration from script tag or global variable const config = window.__tinytapanalytics_config || {}; // Get API key from script tag data attributes // Note: document.currentScript may be null if script is dynamically inserted (like Shopify script tags) let scriptTag = document.currentScript as HTMLScriptElement; // Fallback: Find our script tag by looking for tinytap in the src if (!scriptTag) { const scripts = Array.from(document.getElementsByTagName('script')); scriptTag = scripts.find(s => s.src && s.src.includes('tinytap')) as HTMLScriptElement; } if (scriptTag && scriptTag.dataset.apiKey) { config.apiKey = scriptTag.dataset.apiKey; config.websiteId = scriptTag.dataset.websiteId || scriptTag.dataset.apiKey; } // Also check for configuration in script src URL query parameters (for Shopify integration) if (scriptTag && scriptTag.src) { try { const url = new URL(scriptTag.src); const urlWebsiteId = url.searchParams.get('websiteId'); const urlApiKey = url.searchParams.get('apiKey'); const urlEnableAutoTracking = url.searchParams.get('enableAutoTracking'); const urlEnableMicroInteractionTracking = url.searchParams.get('enableMicroInteractionTracking'); const urlEnableHeatmap = url.searchParams.get('enableHeatmap'); const urlDebug = url.searchParams.get('debug'); if (urlWebsiteId) { config.websiteId = urlWebsiteId; } if (urlApiKey) { config.apiKey = urlApiKey; } if (urlEnableAutoTracking === 'true') { config.enableAutoTracking = true; } if (urlEnableMicroInteractionTracking === 'true') { config.enableMicroInteractionTracking = true; } if (urlEnableHeatmap === 'true') { config.enableHeatmap = true; } if (urlDebug === 'true') { config.debug = true; } } catch (error) { // Invalid URL, skip query parameter extraction } } // Only auto-initialize if configuration is provided via script tag or window.__tinytapanalytics_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.TinyTapAnalytics = window.TinyTapAnalytics || []; 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( 'TinyTapAnalytics: Using demo credentials. Please configure your actual apiKey and websiteId from your dashboard. ' + 'Visit https://dashboard.tinytapanalytics.com to get your credentials.' ); } // Replace the queue array with the actual SDK const existingQueue = window.TinyTapAnalytics || []; const sdk = new TinyTapAnalyticsSDK(config); // Expose SDK globally IMMEDIATELY (before init completes) window.TinyTapAnalytics = sdk; // Initialize the SDK sdk.init() .then(() => { // Process any queued calls after successful init if (Array.isArray(existingQueue)) { (existingQueue as Array<[string, ...unknown[]]>).forEach((args) => { const [method, ...params] = args; if (typeof method === 'string' && method in sdk && typeof sdk[method as keyof typeof sdk] === 'function') { (sdk[method as keyof typeof sdk] as (...args: unknown[]) => unknown)(...params); } }); } }) .catch(error => { console.error('TinyTapAnalytics: Failed to initialize', error); // Clear problematic localStorage and note the issue if (config.debug) { console.log('TinyTapAnalytics: Clearing localStorage and retrying is recommended'); } }); })(); export default TinyTapAnalyticsSDK; // Export types for TypeScript consumers export type { TinyTapAnalyticsConfig, UserContext, EventData, TrackingOptions, ConversionData, PrivacySettings } from './types/index';