UNPKG

@tinytapanalytics/sdk

Version:

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

352 lines (304 loc) 9.85 kB
/** * Performance Monitor for TinyTapAnalytics SDK * Tracks Core Web Vitals and SDK performance metrics */ import { TinyTapAnalyticsConfig } from '../types/index'; interface PerformanceMetric { name: string; value: number; timestamp: number; rating: 'good' | 'needs-improvement' | 'poor'; } interface WebVitalsData { CLS?: PerformanceMetric; FID?: PerformanceMetric; FCP?: PerformanceMetric; LCP?: PerformanceMetric; TTFB?: PerformanceMetric; } export class PerformanceMonitor { private config: TinyTapAnalyticsConfig; private observers: PerformanceObserver[] = []; private vitals: WebVitalsData = {}; private sdkMetrics: Map<string, number> = new Map(); private isSupported: boolean; constructor(config: TinyTapAnalyticsConfig) { this.config = config; this.isSupported = 'PerformanceObserver' in window; } /** * Initialize performance monitoring */ public init(): void { if (!this.isSupported) { if (this.config.debug) { console.warn('TinyTapAnalytics: PerformanceObserver not supported'); } return; } this.measureWebVitals(); this.measureSDKPerformance(); this.setupNavigationTiming(); } /** * Measure Core Web Vitals */ private measureWebVitals(): void { // Largest Contentful Paint (LCP) this.observeMetric('largest-contentful-paint', (entries) => { const lastEntry = entries[entries.length - 1] as PerformanceEntry & { renderTime?: number; loadTime?: number }; const value = lastEntry.renderTime || lastEntry.loadTime || 0; this.vitals.LCP = { name: 'LCP', value, timestamp: Date.now(), rating: this.getRating('LCP', value) }; }); // First Input Delay (FID) this.observeMetric('first-input', (entries) => { const firstInput = entries[0] as PerformanceEntry & { processingStart?: number }; const value = firstInput.processingStart ? firstInput.processingStart - firstInput.startTime : 0; this.vitals.FID = { name: 'FID', value, timestamp: Date.now(), rating: this.getRating('FID', value) }; }); // First Contentful Paint (FCP) this.observeMetric('paint', (entries) => { for (const entry of entries) { if (entry.name === 'first-contentful-paint') { this.vitals.FCP = { name: 'FCP', value: entry.startTime, timestamp: Date.now(), rating: this.getRating('FCP', entry.startTime) }; } } }); // Cumulative Layout Shift (CLS) this.observeMetric('layout-shift', (entries) => { let clsValue = 0; for (const entry of entries) { const layoutShift = entry as PerformanceEntry & { value?: number; hadRecentInput?: boolean }; if (!layoutShift.hadRecentInput) { clsValue += layoutShift.value || 0; } } this.vitals.CLS = { name: 'CLS', value: clsValue, timestamp: Date.now(), rating: this.getRating('CLS', clsValue) }; }, { buffered: true }); } /** * Measure SDK-specific performance metrics */ private measureSDKPerformance(): void { // Track bundle parsing time const bundleStartTime = performance.now(); this.sdkMetrics.set('bundle_parse_start', bundleStartTime); // Track initialization time this.measureAsync('sdk_init', async () => { // This will be called from the main SDK initialization }); // Track event processing times this.measureSync('event_processing', () => { // This will be called during event processing }); // Monitor memory usage if available if ('memory' in performance) { const memoryInfo = (performance as any).memory; this.sdkMetrics.set('memory_used', memoryInfo.usedJSHeapSize); this.sdkMetrics.set('memory_total', memoryInfo.totalJSHeapSize); this.sdkMetrics.set('memory_limit', memoryInfo.jsHeapSizeLimit); } } /** * Setup navigation timing measurements */ private setupNavigationTiming(): void { this.observeMetric('navigation', (entries) => { const navEntry = entries[0] as PerformanceNavigationTiming; // Time to First Byte (TTFB) const ttfb = navEntry.responseStart - navEntry.requestStart; this.vitals.TTFB = { name: 'TTFB', value: ttfb, timestamp: Date.now(), rating: this.getRating('TTFB', ttfb) }; // Track other navigation metrics this.sdkMetrics.set('dns_lookup', navEntry.domainLookupEnd - navEntry.domainLookupStart); this.sdkMetrics.set('tcp_connect', navEntry.connectEnd - navEntry.connectStart); this.sdkMetrics.set('request_response', navEntry.responseEnd - navEntry.requestStart); this.sdkMetrics.set('dom_processing', navEntry.domContentLoadedEventEnd - navEntry.responseEnd); }); } /** * Observe performance metrics */ private observeMetric( entryType: string, callback: (entries: PerformanceEntry[]) => void, options: PerformanceObserverInit = {} ): void { try { const observer = new PerformanceObserver((list) => { callback(list.getEntries()); }); observer.observe({ entryTypes: [entryType], ...options }); this.observers.push(observer); } catch (error) { if (this.config.debug) { console.warn(`TinyTapAnalytics: Failed to observe ${entryType}:`, error); } } } /** * Measure synchronous operation performance */ public measureSync<T>(name: string, fn: () => T): T { const start = performance.now(); const result = fn(); const duration = performance.now() - start; this.sdkMetrics.set(name, duration); if (this.config.debug && duration > 5) { console.warn(`TinyTapAnalytics: Slow operation ${name}: ${duration.toFixed(2)}ms`); } return result; } /** * Measure asynchronous operation performance */ public async measureAsync<T>(name: string, fn: () => Promise<T>): Promise<T> { const start = performance.now(); const result = await fn(); const duration = performance.now() - start; this.sdkMetrics.set(name, duration); if (this.config.debug && duration > 10) { console.warn(`TinyTapAnalytics: Slow async operation ${name}: ${duration.toFixed(2)}ms`); } return result; } /** * Get performance rating based on metric thresholds */ private getRating(metric: string, value: number): 'good' | 'needs-improvement' | 'poor' { const thresholds = { LCP: [2500, 4000], // Good: ≤2.5s, Poor: >4s FID: [100, 300], // Good: ≤100ms, Poor: >300ms FCP: [1800, 3000], // Good: ≤1.8s, Poor: >3s CLS: [0.1, 0.25], // Good: ≤0.1, Poor: >0.25 TTFB: [800, 1800] // Good: ≤800ms, Poor: >1.8s }; const [good, poor] = thresholds[metric as keyof typeof thresholds] || [0, 0]; if (value <= good) { return 'good'; } if (value <= poor) { return 'needs-improvement'; } return 'poor'; } /** * Get all Web Vitals data */ public getWebVitals(): WebVitalsData { return { ...this.vitals }; } /** * Get SDK performance metrics */ public getSDKMetrics(): Record<string, number> { return Object.fromEntries(this.sdkMetrics); } /** * Get performance summary */ public getPerformanceSummary(): { webVitals: WebVitalsData; sdkMetrics: Record<string, number>; overallRating: 'good' | 'needs-improvement' | 'poor'; } { const webVitals = this.getWebVitals(); const sdkMetrics = this.getSDKMetrics(); // Calculate overall rating based on Core Web Vitals const ratings = Object.values(webVitals).map(metric => metric.rating); const poorCount = ratings.filter(r => r === 'poor').length; const improvementCount = ratings.filter(r => r === 'needs-improvement').length; let overallRating: 'good' | 'needs-improvement' | 'poor'; if (poorCount > 0) { overallRating = 'poor'; } else if (improvementCount > 0) { overallRating = 'needs-improvement'; } else { overallRating = 'good'; } return { webVitals, sdkMetrics, overallRating }; } /** * Report performance data to analytics endpoint */ public async reportPerformance(): Promise<void> { if (!this.config.enablePerformanceMonitoring) { return; } try { const performanceData = this.getPerformanceSummary(); const payload = { timestamp: new Date().toISOString(), websiteId: this.config.websiteId, userAgent: navigator.userAgent, url: window.location.href, ...performanceData }; // Use sendBeacon for reliability if (navigator.sendBeacon) { const blob = new Blob([JSON.stringify(payload)], { type: 'application/json' }); navigator.sendBeacon(`${this.config.endpoint}/api/v1/performance`, blob); } else { // Fallback to fetch fetch(`${this.config.endpoint}/api/v1/performance`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), keepalive: true }).catch(() => { // Silently fail performance reporting }); } } catch (error) { if (this.config.debug) { console.warn('TinyTapAnalytics: Failed to report performance data:', error); } } } /** * Clean up performance observers */ public destroy(): void { this.observers.forEach(observer => { try { observer.disconnect(); } catch (error) { // Ignore cleanup errors } }); this.observers = []; this.vitals = {}; this.sdkMetrics.clear(); } }