← back to Watches
analytics/funnel-tracker.js
302 lines
/**
* User Funnel Tracking System
* Track user journeys through predefined conversion funnels
*/
class FunnelTracker {
constructor(analytics) {
this.analytics = analytics;
this.funnels = new Map();
this.userFunnelState = this.loadFunnelState();
this.setupFunnels();
}
/**
* Define conversion funnels
*/
setupFunnels() {
// Watch Discovery Funnel
this.registerFunnel('watch_discovery', [
{ step: 'landing', name: 'Land on Site' },
{ step: 'browse', name: 'Browse Watches' },
{ step: 'view_details', name: 'View Watch Details' },
{ step: 'compare', name: 'Compare Watches' },
{ step: 'export', name: 'Export Data' }
]);
// Price Research Funnel
this.registerFunnel('price_research', [
{ step: 'search', name: 'Search Watch' },
{ step: 'view_price_history', name: 'View Price History' },
{ step: 'view_predictions', name: 'View Price Predictions' },
{ step: 'export_report', name: 'Export Report' }
]);
// Collection Analysis Funnel
this.registerFunnel('collection_analysis', [
{ step: 'filter_series', name: 'Filter by Series' },
{ step: 'view_collection_stats', name: 'View Collection Stats' },
{ step: 'compare_models', name: 'Compare Models' },
{ step: 'deep_analysis', name: 'Deep Analysis' }
]);
// Investment Research Funnel
this.registerFunnel('investment_research', [
{ step: 'view_trending', name: 'View Trending Watches' },
{ step: 'check_appreciation', name: 'Check Appreciation' },
{ step: 'view_predictions', name: 'View Predictions' },
{ step: 'export_investment_data', name: 'Export Investment Data' }
]);
}
/**
* Register a new funnel
*/
registerFunnel(funnelId, steps) {
this.funnels.set(funnelId, {
id: funnelId,
steps: steps,
conversions: new Map()
});
}
/**
* Track funnel step
*/
trackStep(funnelId, stepId, metadata = {}) {
const funnel = this.funnels.get(funnelId);
if (!funnel) {
console.warn(`Funnel ${funnelId} not found`);
return;
}
const step = funnel.steps.find(s => s.step === stepId);
if (!step) {
console.warn(`Step ${stepId} not found in funnel ${funnelId}`);
return;
}
// Update user's funnel state
if (!this.userFunnelState[funnelId]) {
this.userFunnelState[funnelId] = {
steps: [],
startTime: Date.now(),
metadata: {}
};
}
const funnelState = this.userFunnelState[funnelId];
const stepIndex = funnel.steps.findIndex(s => s.step === stepId);
// Check if this is a new step or revisit
const existingStepIndex = funnelState.steps.findIndex(s => s.step === stepId);
if (existingStepIndex === -1) {
funnelState.steps.push({
step: stepId,
timestamp: Date.now(),
stepIndex: stepIndex,
metadata: metadata
});
} else {
// Update existing step
funnelState.steps[existingStepIndex].timestamp = Date.now();
funnelState.steps[existingStepIndex].metadata = {
...funnelState.steps[existingStepIndex].metadata,
...metadata
};
}
// Update global metadata
funnelState.metadata = {
...funnelState.metadata,
...metadata
};
this.saveFunnelState();
// Track in analytics
this.analytics.trackEvent('funnel_step', {
funnel_id: funnelId,
funnel_name: this.getFunnelName(funnelId),
step_id: stepId,
step_name: step.name,
step_index: stepIndex,
total_steps: funnel.steps.length,
time_in_funnel: Date.now() - funnelState.startTime,
completion_percentage: ((stepIndex + 1) / funnel.steps.length) * 100,
...metadata
});
// Check if funnel is completed
if (stepIndex === funnel.steps.length - 1) {
this.trackFunnelCompletion(funnelId);
}
// Send to backend
this.sendFunnelDataToBackend(funnelId, stepId, metadata);
}
/**
* Track funnel completion
*/
trackFunnelCompletion(funnelId) {
const funnelState = this.userFunnelState[funnelId];
if (!funnelState) return;
const completionTime = Date.now() - funnelState.startTime;
const funnel = this.funnels.get(funnelId);
this.analytics.trackEvent('funnel_completion', {
funnel_id: funnelId,
funnel_name: this.getFunnelName(funnelId),
completion_time: completionTime,
steps_completed: funnelState.steps.length,
total_steps: funnel.steps.length,
metadata: funnelState.metadata
});
// Clear funnel state after completion
delete this.userFunnelState[funnelId];
this.saveFunnelState();
}
/**
* Track funnel abandonment
*/
trackAbandonment(funnelId, reason = '') {
const funnelState = this.userFunnelState[funnelId];
if (!funnelState) return;
const funnel = this.funnels.get(funnelId);
const lastStep = funnelState.steps[funnelState.steps.length - 1];
this.analytics.trackEvent('funnel_abandonment', {
funnel_id: funnelId,
funnel_name: this.getFunnelName(funnelId),
abandoned_at_step: lastStep?.step,
abandoned_at_step_index: lastStep?.stepIndex,
steps_completed: funnelState.steps.length,
total_steps: funnel.steps.length,
time_before_abandonment: Date.now() - funnelState.startTime,
abandonment_reason: reason
});
}
/**
* Get funnel analytics
*/
getFunnelAnalytics(funnelId) {
const funnelState = this.userFunnelState[funnelId];
if (!funnelState) return null;
const funnel = this.funnels.get(funnelId);
const currentStepIndex = Math.max(...funnelState.steps.map(s => s.stepIndex));
return {
funnelId,
currentStep: currentStepIndex,
totalSteps: funnel.steps.length,
completionPercentage: ((currentStepIndex + 1) / funnel.steps.length) * 100,
timeInFunnel: Date.now() - funnelState.startTime,
steps: funnelState.steps,
metadata: funnelState.metadata
};
}
/**
* Get all active funnels for user
*/
getActiveFunnels() {
return Object.keys(this.userFunnelState).map(funnelId =>
this.getFunnelAnalytics(funnelId)
).filter(Boolean);
}
/**
* Load funnel state from storage
*/
loadFunnelState() {
try {
const state = localStorage.getItem('funnel_state');
return state ? JSON.parse(state) : {};
} catch (error) {
console.error('Failed to load funnel state:', error);
return {};
}
}
/**
* Save funnel state to storage
*/
saveFunnelState() {
try {
localStorage.setItem('funnel_state', JSON.stringify(this.userFunnelState));
} catch (error) {
console.error('Failed to save funnel state:', error);
}
}
/**
* Get funnel name
*/
getFunnelName(funnelId) {
return funnelId.split('_').map(word =>
word.charAt(0).toUpperCase() + word.slice(1)
).join(' ');
}
/**
* Send funnel data to backend
*/
async sendFunnelDataToBackend(funnelId, stepId, metadata) {
try {
await fetch('/api/analytics/funnels', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
funnelId,
stepId,
metadata,
timestamp: Date.now(),
userId: this.analytics.userId,
sessionId: this.analytics.sessionId
})
});
} catch (error) {
console.error('Failed to send funnel data:', error);
}
}
/**
* Get funnel conversion rates (from backend)
*/
async getFunnelConversionRates(funnelId, dateRange = 30) {
try {
const response = await fetch(`/api/analytics/funnels/${funnelId}/conversions?days=${dateRange}`);
return await response.json();
} catch (error) {
console.error('Failed to get funnel conversion rates:', error);
return null;
}
}
/**
* Export funnel data
*/
exportFunnelData() {
return {
activeFunnels: this.getActiveFunnels(),
funnelState: this.userFunnelState,
timestamp: Date.now()
};
}
}
// Export
if (typeof module !== 'undefined' && module.exports) {
module.exports = FunnelTracker;
} else {
window.FunnelTracker = FunnelTracker;
}