UNPKG

@tinytapanalytics/sdk

Version:

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

422 lines (362 loc) 10.2 kB
/** * Heatmap features for visual user behavior analysis * Dynamically imported to reduce core bundle size */ import { TinyTapAnalyticsConfig } from '../types/index'; interface HeatmapPoint { x: number; y: number; intensity: number; timestamp: number; type: 'click' | 'move' | 'scroll'; } interface HeatmapData { url: string; viewport: { width: number; height: number }; points: HeatmapPoint[]; sessionId: string; userId?: string; } export class Heatmap { private config: TinyTapAnalyticsConfig; private sdk: any; private clickPoints: HeatmapPoint[] = []; private movePoints: HeatmapPoint[] = []; private scrollPoints: HeatmapPoint[] = []; private isActive = false; private samplingRate = 0.1; // Sample 10% of users private maxPoints = 1000; private debounceTimer?: number; constructor(config: TinyTapAnalyticsConfig, sdk: any) { this.config = config; this.sdk = sdk; // Use custom sampling rate from config if provided if (config.heatmapSamplingRate !== undefined) { this.samplingRate = config.heatmapSamplingRate; } } /** * Start heatmap tracking */ public start(): void { if (this.isActive || !this.shouldTrackHeatmap()) { return; } this.isActive = true; this.setupClickTracking(); this.setupMoveTracking(); this.setupScrollTracking(); this.setupPageUnloadTracking(); if (this.config.debug) { console.log('TinyTapAnalytics: Heatmap tracking started'); } } /** * Stop heatmap tracking */ public stop(): void { if (!this.isActive) { return; } this.isActive = false; this.sendHeatmapData(); if (this.config.debug) { console.log('TinyTapAnalytics: Heatmap tracking stopped'); } } /** * Setup click tracking */ private setupClickTracking(): void { document.addEventListener('click', (event) => { if (!this.isActive) { return; } const point: HeatmapPoint = { x: event.clientX + window.scrollX, y: event.clientY + window.scrollY, intensity: 1, timestamp: Date.now(), type: 'click' }; this.addClickPoint(point); }, true); } /** * Setup mouse movement tracking */ private setupMoveTracking(): void { let lastMoveTime = 0; const moveThrottle = 100; // Track every 100ms document.addEventListener('mousemove', (event) => { if (!this.isActive) { return; } const now = Date.now(); if (now - lastMoveTime < moveThrottle) { return; } lastMoveTime = now; const point: HeatmapPoint = { x: event.clientX + window.scrollX, y: event.clientY + window.scrollY, intensity: 0.1, timestamp: now, type: 'move' }; this.addMovePoint(point); }, { passive: true }); } /** * Setup scroll tracking */ private setupScrollTracking(): void { let lastScrollTime = 0; const scrollThrottle = 250; // Track every 250ms window.addEventListener('scroll', () => { if (!this.isActive) { return; } const now = Date.now(); if (now - lastScrollTime < scrollThrottle) { return; } lastScrollTime = now; const scrollDepth = this.getScrollDepth(); const point: HeatmapPoint = { x: window.innerWidth / 2, // Center of viewport y: window.scrollY + (window.innerHeight / 2), intensity: scrollDepth / 100, timestamp: now, type: 'scroll' }; this.addScrollPoint(point); }, { passive: true }); } /** * Setup page unload tracking */ private setupPageUnloadTracking(): void { window.addEventListener('beforeunload', () => { this.sendHeatmapData(); }); document.addEventListener('visibilitychange', () => { if (document.hidden) { this.sendHeatmapData(); } }); } /** * Add click point with clustering */ private addClickPoint(point: HeatmapPoint): void { // Check for nearby clicks and cluster them const nearbyPoint = this.findNearbyPoint(this.clickPoints, point, 20); if (nearbyPoint) { nearbyPoint.intensity += 1; nearbyPoint.timestamp = point.timestamp; } else { this.clickPoints.push(point); } this.trimPointsArray(this.clickPoints); this.debouncedSend(); } /** * Add move point with sampling */ private addMovePoint(point: HeatmapPoint): void { // Sample mouse moves to reduce data volume if (Math.random() > 0.1) { return; } // Only track 10% of moves this.movePoints.push(point); this.trimPointsArray(this.movePoints); } /** * Add scroll point */ private addScrollPoint(point: HeatmapPoint): void { this.scrollPoints.push(point); this.trimPointsArray(this.scrollPoints); } /** * Find nearby point for clustering */ private findNearbyPoint(points: HeatmapPoint[], newPoint: HeatmapPoint, radius: number): HeatmapPoint | null { return points.find(point => { const distance = Math.sqrt( Math.pow(point.x - newPoint.x, 2) + Math.pow(point.y - newPoint.y, 2) ); return distance <= radius; }) || null; } /** * Trim points array to prevent memory issues */ private trimPointsArray(points: HeatmapPoint[]): void { if (points.length > this.maxPoints) { points.splice(0, points.length - this.maxPoints); } } /** * Debounced send to reduce API calls */ private debouncedSend(): void { if (this.debounceTimer) { clearTimeout(this.debounceTimer); } this.debounceTimer = window.setTimeout(() => { this.sendHeatmapData(); }, 5000); // Send every 5 seconds of activity } /** * Send heatmap data to API */ private sendHeatmapData(): void { if (this.clickPoints.length === 0 && this.movePoints.length === 0 && this.scrollPoints.length === 0) { return; } const heatmapData: HeatmapData = { url: window.location.href, viewport: { width: window.innerWidth, height: window.innerHeight }, points: [ ...this.clickPoints, ...this.movePoints, ...this.scrollPoints ], sessionId: this.sdk.sessionId, userId: this.sdk.userId }; this.sdk.track('heatmap_data', heatmapData); // Clear points after sending this.clickPoints = []; this.movePoints = []; this.scrollPoints = []; if (this.config.debug) { console.log('TinyTapAnalytics: Heatmap data sent', { pointCount: heatmapData.points.length, url: heatmapData.url }); } } /** * Generate heatmap visualization */ public generateHeatmapVisualization(containerId: string): void { if (!this.isActive) { console.warn('TinyTapAnalytics: Heatmap tracking is not active'); return; } const container = document.getElementById(containerId); if (!container) { console.error('TinyTapAnalytics: Heatmap container not found'); return; } // Create canvas for heatmap const canvas = document.createElement('canvas'); canvas.width = window.innerWidth; canvas.height = document.documentElement.scrollHeight; canvas.style.position = 'absolute'; canvas.style.top = '0'; canvas.style.left = '0'; canvas.style.pointerEvents = 'none'; canvas.style.zIndex = '999999'; const ctx = canvas.getContext('2d'); if (!ctx) { return; } // Draw heatmap points this.drawHeatmapPoints(ctx, this.clickPoints, 'rgba(255, 0, 0, 0.6)'); this.drawHeatmapPoints(ctx, this.movePoints, 'rgba(0, 255, 0, 0.3)'); this.drawHeatmapPoints(ctx, this.scrollPoints, 'rgba(0, 0, 255, 0.4)'); container.appendChild(canvas); } /** * Draw heatmap points on canvas */ private drawHeatmapPoints(ctx: CanvasRenderingContext2D, points: HeatmapPoint[], color: string): void { points.forEach(point => { const radius = Math.max(5, point.intensity * 20); ctx.beginPath(); ctx.arc(point.x, point.y, radius, 0, 2 * Math.PI); ctx.fillStyle = color; ctx.fill(); }); } /** * Get scroll depth percentage */ 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); } /** * Check if should track heatmap for this user */ private shouldTrackHeatmap(): boolean { // Use sampling to reduce data volume and performance impact return Math.random() < this.samplingRate; } /** * Get current heatmap statistics */ public getStats(): { isActive: boolean; clickPoints: number; movePoints: number; scrollPoints: number; totalPoints: number; } { return { isActive: this.isActive, clickPoints: this.clickPoints.length, movePoints: this.movePoints.length, scrollPoints: this.scrollPoints.length, totalPoints: this.clickPoints.length + this.movePoints.length + this.scrollPoints.length }; } /** * Clear all heatmap data */ public clearData(): void { this.clickPoints = []; this.movePoints = []; this.scrollPoints = []; if (this.config.debug) { console.log('TinyTapAnalytics: Heatmap data cleared'); } } /** * Set sampling rate */ public setSamplingRate(rate: number): void { this.samplingRate = Math.max(0, Math.min(1, rate)); if (this.config.debug) { console.log('TinyTapAnalytics: Heatmap sampling rate set to', this.samplingRate); } } /** * Export heatmap data */ public exportData(): HeatmapData { return { url: window.location.href, viewport: { width: window.innerWidth, height: window.innerHeight }, points: [ ...this.clickPoints, ...this.movePoints, ...this.scrollPoints ], sessionId: this.sdk.sessionId, userId: this.sdk.userId }; } }