UNPKG

@tinytapanalytics/sdk

Version:

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

381 lines (328 loc) 10.2 kB
/** * Error Handler for TinyTapAnalytics SDK * Provides graceful error handling that never breaks host websites */ import { TinyTapAnalyticsConfig, SDKError } from '../types/index'; import packageJson from '../../package.json'; export class ErrorHandler { private config: TinyTapAnalyticsConfig; private errorCount = 0; private maxErrors = 10; private errorBuffer: SDKError[] = []; private reportingInterval: number | null = null; private isShutdown = false; private reportingIntervalMs: number; constructor(config: TinyTapAnalyticsConfig, reportingIntervalMs = 30000) { this.config = config; this.reportingIntervalMs = reportingIntervalMs; this.setupGlobalErrorHandling(); this.startErrorReporting(); } /** * Handle an error gracefully */ public handle(error: Error, context?: string, data?: any): void { if (this.isShutdown) { return; } try { const sdkError = this.createSDKError(error, context, data); // Always log to console in debug mode if (this.config.debug) { console.error('TinyTapAnalytics SDK Error:', sdkError); } // Track error count to prevent infinite loops this.errorCount++; if (this.errorCount > this.maxErrors) { this.shutdown(); return; } // Buffer error for reporting this.bufferError(sdkError); // Handle specific error types this.handleSpecificError(sdkError); } catch (handlingError) { // If error handling itself fails, shutdown gracefully if (this.config.debug) { console.error('TinyTapAnalytics: Error handler failed:', handlingError); } this.shutdown(); } } /** * Report a warning (non-fatal error) */ public warn(message: string, context?: string, data?: any): void { if (this.config.debug) { console.warn('TinyTapAnalytics Warning:', message, { context, data }); } const warning: SDKError = { code: 'WARNING', message, context: context || 'general', timestamp: Date.now(), originalError: undefined, stack: new Error().stack }; this.bufferError(warning); } /** * Get current error statistics */ public getStats(): { errorCount: number; isShutdown: boolean; bufferSize: number } { return { errorCount: this.errorCount, isShutdown: this.isShutdown, bufferSize: this.errorBuffer.length }; } /** * Manually shutdown error handling */ public shutdown(): void { if (this.isShutdown) { return; } this.isShutdown = true; if (this.reportingInterval) { clearInterval(this.reportingInterval); this.reportingInterval = null; } // Final error report this.reportErrors(); if (this.config.debug) { console.log('TinyTapAnalytics: Error handler shutdown due to excessive errors'); } } /** * Create standardized SDK error */ private createSDKError(error: Error, context?: string, data?: any): SDKError { return { code: this.getErrorCode(error), message: error.message || 'Unknown error', context: context || 'unknown', originalError: error, timestamp: Date.now(), stack: error.stack, data }; } /** * Get appropriate error code based on error type */ private getErrorCode(error: Error): string { if (error.name === 'TypeError') { return 'TYPE_ERROR'; } if (error.name === 'ReferenceError') { return 'REFERENCE_ERROR'; } if (error.name === 'NetworkError') { return 'NETWORK_ERROR'; } if (error.message.includes('quota')) { return 'STORAGE_QUOTA_EXCEEDED'; } if (error.message.includes('timeout')) { return 'TIMEOUT_ERROR'; } if (error.message.includes('abort')) { return 'REQUEST_ABORTED'; } if (error.message.includes('CORS')) { return 'CORS_ERROR'; } if (error.message.includes('CSP')) { return 'CSP_VIOLATION'; } return 'GENERAL_ERROR'; } /** * Handle specific error types with appropriate actions */ private handleSpecificError(sdkError: SDKError): void { switch (sdkError.code) { case 'STORAGE_QUOTA_EXCEEDED': this.handleStorageQuotaError(); break; case 'NETWORK_ERROR': case 'TIMEOUT_ERROR': this.handleNetworkError(sdkError); break; case 'CSP_VIOLATION': this.handleCSPViolation(sdkError); break; case 'CORS_ERROR': this.handleCORSError(sdkError); break; } } /** * Handle storage quota exceeded errors */ private handleStorageQuotaError(): void { try { // Clear old data to free up space const keys = Object.keys(localStorage); keys.forEach(key => { if (key.startsWith('tinytapanalytics_') && key.includes('_old_')) { localStorage.removeItem(key); } }); } catch (error) { // If we can't clear storage, we'll have to operate without persistence } } /** * Handle network-related errors */ private handleNetworkError(sdkError: SDKError): void { // Network errors are expected and shouldn't cause shutdown // They're handled by the NetworkManager's retry logic } /** * Handle Content Security Policy violations */ private handleCSPViolation(sdkError: SDKError): void { if (this.config.debug) { console.warn('TinyTapAnalytics: CSP violation detected. Some features may be limited.'); } // Could trigger fallback mode for stricter CSP environments } /** * Handle CORS errors */ private handleCORSError(sdkError: SDKError): void { if (this.config.debug) { console.warn('TinyTapAnalytics: CORS error detected. Check domain configuration.'); } } /** * Buffer error for later reporting */ private bufferError(error: SDKError): void { this.errorBuffer.push(error); // Keep buffer size manageable if (this.errorBuffer.length > 50) { this.errorBuffer = this.errorBuffer.slice(-25); // Keep last 25 errors } } /** * Set up global error handling for SDK-related errors */ private setupGlobalErrorHandling(): void { // Only catch errors that are clearly from our SDK const originalErrorHandler = window.onerror; window.onerror = (message, source, lineno, colno, error) => { // Check if error is from our SDK if (this.isSDKError(error, source, message)) { this.handle(error || new Error(String(message)), 'global_error'); } // Call original error handler if (originalErrorHandler) { return originalErrorHandler.call(window, message, source, lineno, colno, error); } return false; // Don't prevent default browser error handling }; // Handle unhandled promise rejections from SDK const originalRejectionHandler = window.onunhandledrejection; window.onunhandledrejection = (event) => { if (this.isSDKError(event.reason)) { this.handle( event.reason instanceof Error ? event.reason : new Error(String(event.reason)), 'unhandled_promise_rejection' ); } // Call original rejection handler if (originalRejectionHandler) { return originalRejectionHandler.call(window, event); } return false; }; } /** * Check if an error is from our SDK */ private isSDKError(error: any, source?: string, message?: string | Event): boolean { // Check stack trace for SDK references if (error && error.stack) { if (error.stack.includes('TinyTapAnalytics') || error.stack.includes('tinytapanalytics') || error.stack.includes('tracker.js')) { return true; } } // Check source URL if (source && (source.includes('tinytapanalytics') || source.includes('tracker.js'))) { return true; } // Check error message if (message && typeof message === 'string' && message.includes('TinyTapAnalytics')) { return true; } return false; } /** * Start periodic error reporting */ private startErrorReporting(): void { // Report errors periodically this.reportingInterval = window.setInterval(() => { this.reportErrors(); }, this.reportingIntervalMs); } /** * Report buffered errors to the API */ private reportErrors(): void { if (this.errorBuffer.length === 0 || this.isShutdown) { return; } try { const errors = [...this.errorBuffer]; this.errorBuffer = []; // Send errors to API (don't use NetworkManager to avoid circular dependencies) const payload = { sdk_version: packageJson.version, website_id: this.config.websiteId, timestamp: new Date().toISOString(), errors: errors.map(error => ({ code: error.code, message: error.message, context: error.context, timestamp: error.timestamp, stack: error.stack ? error.stack.substring(0, 1000) : undefined, // Limit stack trace size data: error.data })) }; // Use sendBeacon if available for reliability if (navigator.sendBeacon) { const url = `${this.config.endpoint}/api/v1/errors`; navigator.sendBeacon(url, JSON.stringify(payload)); } else { // Fallback to fetch with no await to avoid blocking fetch(`${this.config.endpoint}/api/v1/errors`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }).catch(() => { // Silently fail error reporting }); } } catch (reportingError) { // If error reporting fails, we can't do much except log if (this.config.debug) { console.error('TinyTapAnalytics: Failed to report errors:', reportingError); } } } /** * Clean up resources */ public destroy(): void { this.shutdown(); // Clean up global error handlers if needed // Note: We don't restore original handlers as other scripts might depend on them } }