@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
text/typescript
/**
* 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';