UNPKG

@tinytapanalytics/sdk

Version:

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

471 lines (412 loc) 11.6 kB
/** * Advanced Analytics features for detailed user behavior analysis * Dynamically imported to reduce core bundle size */ import { TinyTapAnalyticsConfig } from '../types/index'; interface SessionData { id: string; startTime: number; lastActivity: number; pageViews: number; events: number; duration: number; } interface FunnelStep { name: string; selector?: string; event?: string; url?: string; } interface FunnelAnalysis { steps: FunnelStep[]; conversions: number[]; dropoffs: number[]; conversionRate: number; } export class AdvancedAnalytics { private config: TinyTapAnalyticsConfig; private sdk: any; private sessionData: SessionData; private userJourney: Array<{ timestamp: number; event: string; data: any; }> = []; private heatmapData: Map<string, { x: number; y: number; count: number }> = new Map(); constructor(config: TinyTapAnalyticsConfig, sdk: any) { this.config = config; this.sdk = sdk; this.sessionData = this.initializeSession(); this.startSessionTracking(); } /** * Initialize session tracking */ private initializeSession(): SessionData { const now = Date.now(); return { id: this.generateSessionId(), startTime: now, lastActivity: now, pageViews: 0, events: 0, duration: 0 }; } /** * Start session tracking */ private startSessionTracking(): void { // Update session on page visibility changes document.addEventListener('visibilitychange', () => { if (!document.hidden) { this.updateSessionActivity(); } }); // Update session on user interactions ['click', 'scroll', 'keydown', 'mousemove'].forEach(event => { document.addEventListener(event, () => { this.updateSessionActivity(); }, { passive: true, once: false }); }); // Send session data before page unload window.addEventListener('beforeunload', () => { this.endSession(); }); } /** * Update session activity */ private updateSessionActivity(): void { const now = Date.now(); this.sessionData.lastActivity = now; this.sessionData.duration = now - this.sessionData.startTime; } /** * Track page view with enhanced data */ public trackPageView(url?: string): void { this.sessionData.pageViews++; this.updateSessionActivity(); const pageData = { url: url || window.location.href, title: document.title, referrer: document.referrer, timestamp: Date.now(), viewport: { width: window.innerWidth, height: window.innerHeight }, screen: { width: screen.width, height: screen.height }, sessionId: this.sessionData.id, pageNumber: this.sessionData.pageViews }; this.userJourney.push({ timestamp: Date.now(), event: 'page_view', data: pageData }); this.sdk.track('enhanced_page_view', pageData); } /** * Track user engagement metrics */ public trackEngagement(): void { const engagement = { timeOnPage: Date.now() - this.sessionData.lastActivity, scrollDepth: this.calculateScrollDepth(), clickCount: this.getSessionClickCount(), sessionDuration: this.sessionData.duration, pageViews: this.sessionData.pageViews, sessionId: this.sessionData.id }; this.sdk.track('user_engagement', engagement); } /** * Setup funnel analysis */ public setupFunnelAnalysis(steps: FunnelStep[]): void { const funnelData: FunnelAnalysis = { steps, conversions: new Array(steps.length).fill(0), dropoffs: new Array(steps.length - 1).fill(0), conversionRate: 0 }; // Track funnel step completions steps.forEach((step, index) => { if (step.selector) { this.observeFunnelStep(step, index, funnelData); } else if (step.url) { this.trackURLFunnelStep(step, index, funnelData); } }); } /** * Observe funnel step by element selector */ private observeFunnelStep(step: FunnelStep, index: number, funnelData: FunnelAnalysis): void { if (!step.selector) { return; } const observer = new MutationObserver(() => { const element = document.querySelector(step.selector!); if (element) { element.addEventListener('click', () => { this.recordFunnelStep(step, index, funnelData); }); observer.disconnect(); } }); observer.observe(document.body, { childList: true, subtree: true }); // Check if element already exists const existingElement = document.querySelector(step.selector); if (existingElement) { existingElement.addEventListener('click', () => { this.recordFunnelStep(step, index, funnelData); }); observer.disconnect(); } } /** * Track funnel step by URL */ private trackURLFunnelStep(step: FunnelStep, index: number, funnelData: FunnelAnalysis): void { if (step.url && window.location.href.includes(step.url)) { this.recordFunnelStep(step, index, funnelData); } } /** * Record funnel step completion */ private recordFunnelStep(step: FunnelStep, index: number, funnelData: FunnelAnalysis): void { funnelData.conversions[index]++; // Calculate dropoff for previous steps if (index > 0) { const previousConversions = funnelData.conversions[index - 1]; const currentConversions = funnelData.conversions[index]; funnelData.dropoffs[index - 1] = previousConversions - currentConversions; } // Calculate overall conversion rate const firstStepConversions = funnelData.conversions[0]; const lastStepConversions = funnelData.conversions[funnelData.conversions.length - 1]; funnelData.conversionRate = firstStepConversions > 0 ? (lastStepConversions / firstStepConversions) * 100 : 0; this.sdk.track('funnel_step', { stepName: step.name, stepIndex: index, funnelData: funnelData, sessionId: this.sessionData.id }); } /** * Track user cohort analysis */ public trackCohort(cohortName: string, userAttributes: Record<string, any> = {}): void { const cohortData = { cohortName, userAttributes, sessionId: this.sessionData.id, firstVisit: this.isFirstVisit(), timestamp: Date.now() }; this.sdk.track('cohort_analysis', cohortData); } /** * Track attribution data */ public trackAttribution(): void { const attribution = { source: this.getTrafficSource(), medium: this.getTrafficMedium(), campaign: this.getCampaign(), referrer: document.referrer, landingPage: window.location.href, sessionId: this.sessionData.id, timestamp: Date.now() }; this.sdk.track('attribution', attribution); } /** * Track conversion funnel */ public trackConversionFunnel(funnelName: string, stepName: string, value?: number): void { const funnelData = { funnelName, stepName, value: value || 0, sessionId: this.sessionData.id, userJourney: this.getUserJourneySnapshot(), timestamp: Date.now() }; this.sdk.track('conversion_funnel', funnelData); } /** * Track user segment */ public trackUserSegment(segment: string, properties: Record<string, any> = {}): void { const segmentData = { segment, properties, sessionId: this.sessionData.id, sessionData: this.sessionData, timestamp: Date.now() }; this.sdk.track('user_segment', segmentData); } /** * Get user journey snapshot */ private getUserJourneySnapshot(): Array<{ timestamp: number; event: string; data: any; }> { // Return last 10 events to avoid excessive data return this.userJourney.slice(-10); } /** * Calculate scroll depth */ private calculateScrollDepth(): number { const windowHeight = window.innerHeight; const documentHeight = document.documentElement.scrollHeight; const scrollTop = window.pageYOffset || document.documentElement.scrollTop; return Math.round((scrollTop / (documentHeight - windowHeight)) * 100); } /** * Get session click count */ private getSessionClickCount(): number { return this.userJourney.filter(event => event.event === 'click').length; } /** * Check if this is the user's first visit */ private isFirstVisit(): boolean { try { const visited = localStorage.getItem('tinytapanalytics_visited'); if (!visited) { localStorage.setItem('tinytapanalytics_visited', 'true'); return true; } return false; } catch { return false; } } /** * Get traffic source */ private getTrafficSource(): string { const referrer = document.referrer; if (!referrer) { return 'direct'; } const url = new URL(referrer); const hostname = url.hostname.toLowerCase(); if (hostname.includes('google')) { return 'google'; } if (hostname.includes('bing')) { return 'bing'; } if (hostname.includes('yahoo')) { return 'yahoo'; } if (hostname.includes('facebook')) { return 'facebook'; } if (hostname.includes('twitter')) { return 'twitter'; } if (hostname.includes('linkedin')) { return 'linkedin'; } if (hostname.includes('instagram')) { return 'instagram'; } return 'referral'; } /** * Get traffic medium */ private getTrafficMedium(): string { const urlParams = new URLSearchParams(window.location.search); const utmMedium = urlParams.get('utm_medium'); if (utmMedium) { return utmMedium; } const source = this.getTrafficSource(); if (['google', 'bing', 'yahoo'].includes(source)) { return 'organic'; } if (['facebook', 'twitter', 'linkedin', 'instagram'].includes(source)) { return 'social'; } if (source === 'referral') { return 'referral'; } return 'direct'; } /** * Get campaign information */ private getCampaign(): string | null { const urlParams = new URLSearchParams(window.location.search); return urlParams.get('utm_campaign') || urlParams.get('campaign') || null; } /** * End session and send final data */ private endSession(): void { this.sessionData.duration = Date.now() - this.sessionData.startTime; const sessionSummary = { ...this.sessionData, userJourney: this.getUserJourneySnapshot(), finalUrl: window.location.href, endTime: Date.now() }; this.sdk.track('session_end', sessionSummary); } /** * Generate session ID */ private generateSessionId(): string { return `ciq_session_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`; } /** * Get current session data */ public getSessionData(): SessionData { return { ...this.sessionData }; } /** * Get user journey */ public getUserJourney(): Array<{ timestamp: number; event: string; data: any; }> { return [...this.userJourney]; } /** * Add custom event to user journey */ public addToUserJourney(event: string, data: any): void { this.userJourney.push({ timestamp: Date.now(), event, data }); // Keep journey manageable (last 50 events) if (this.userJourney.length > 50) { this.userJourney = this.userJourney.slice(-50); } this.sessionData.events++; } }