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