← back to Watches
analytics/cohort-analysis.js
288 lines
/**
* Cohort Analysis System
* Track user cohorts based on first visit date and behavior patterns
*/
class CohortAnalyzer {
constructor(analytics) {
this.analytics = analytics;
this.cohortData = this.loadCohortData();
this.initializeCohort();
}
/**
* Initialize user's cohort
*/
initializeCohort() {
if (!this.cohortData.cohortMonth) {
const now = new Date();
this.cohortData.cohortMonth = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`;
this.cohortData.cohortWeek = this.getWeekId(now);
this.cohortData.firstVisit = now.toISOString();
this.cohortData.visits = [];
this.cohortData.actions = [];
this.saveCohortData();
// Track cohort creation
this.analytics.trackEvent('cohort_created', {
cohort_month: this.cohortData.cohortMonth,
cohort_week: this.cohortData.cohortWeek,
first_visit: this.cohortData.firstVisit
});
}
// Record this visit
this.recordVisit();
}
/**
* Record a visit
*/
recordVisit() {
const visit = {
timestamp: Date.now(),
date: new Date().toISOString(),
daysSinceFirstVisit: this.getDaysSinceFirstVisit(),
weeksSinceFirstVisit: this.getWeeksSinceFirstVisit(),
monthsSinceFirstVisit: this.getMonthsSinceFirstVisit()
};
this.cohortData.visits.push(visit);
this.cohortData.lastVisit = visit.date;
this.cohortData.totalVisits = this.cohortData.visits.length;
this.saveCohortData();
// Track retention
this.trackRetention(visit);
}
/**
* Track user retention
*/
trackRetention(visit) {
this.analytics.trackEvent('user_retention', {
cohort_month: this.cohortData.cohortMonth,
cohort_week: this.cohortData.cohortWeek,
days_since_first_visit: visit.daysSinceFirstVisit,
weeks_since_first_visit: visit.weeksSinceFirstVisit,
months_since_first_visit: visit.monthsSinceFirstVisit,
total_visits: this.cohortData.totalVisits,
is_returning_user: this.cohortData.totalVisits > 1
});
}
/**
* Record user action for cohort analysis
*/
recordAction(actionType, actionDetails = {}) {
const action = {
type: actionType,
timestamp: Date.now(),
daysSinceFirstVisit: this.getDaysSinceFirstVisit(),
details: actionDetails
};
this.cohortData.actions.push(action);
this.saveCohortData();
// Track cohort action
this.analytics.trackEvent('cohort_action', {
cohort_month: this.cohortData.cohortMonth,
action_type: actionType,
days_since_first_visit: action.daysSinceFirstVisit,
total_actions: this.cohortData.actions.length,
...actionDetails
});
}
/**
* Get days since first visit
*/
getDaysSinceFirstVisit() {
if (!this.cohortData.firstVisit) return 0;
const firstVisit = new Date(this.cohortData.firstVisit);
const now = new Date();
return Math.floor((now - firstVisit) / (1000 * 60 * 60 * 24));
}
/**
* Get weeks since first visit
*/
getWeeksSinceFirstVisit() {
return Math.floor(this.getDaysSinceFirstVisit() / 7);
}
/**
* Get months since first visit
*/
getMonthsSinceFirstVisit() {
if (!this.cohortData.firstVisit) return 0;
const firstVisit = new Date(this.cohortData.firstVisit);
const now = new Date();
return (now.getFullYear() - firstVisit.getFullYear()) * 12 +
(now.getMonth() - firstVisit.getMonth());
}
/**
* Get week ID for a date
*/
getWeekId(date) {
const yearStart = new Date(date.getFullYear(), 0, 1);
const weekNumber = Math.ceil(((date - yearStart) / 86400000 + yearStart.getDay() + 1) / 7);
return `${date.getFullYear()}-W${String(weekNumber).padStart(2, '0')}`;
}
/**
* Get user engagement score
*/
getEngagementScore() {
const daysSinceFirst = this.getDaysSinceFirstVisit();
if (daysSinceFirst === 0) return 0;
const visitFrequency = this.cohortData.totalVisits / (daysSinceFirst + 1);
const actionFrequency = this.cohortData.actions.length / (daysSinceFirst + 1);
const recency = this.getDaysSinceLastVisit();
// Calculate weighted engagement score
const score = (
(visitFrequency * 40) +
(actionFrequency * 40) +
(Math.max(0, 30 - recency) / 30 * 20)
);
return Math.min(100, Math.round(score));
}
/**
* Get days since last visit
*/
getDaysSinceLastVisit() {
if (!this.cohortData.lastVisit) return 0;
const lastVisit = new Date(this.cohortData.lastVisit);
const now = new Date();
return Math.floor((now - lastVisit) / (1000 * 60 * 60 * 24));
}
/**
* Get cohort segment
*/
getCohortSegment() {
const engagementScore = this.getEngagementScore();
const daysSinceFirst = this.getDaysSinceFirstVisit();
if (daysSinceFirst === 0) {
return 'new_user';
} else if (daysSinceFirst < 7) {
return engagementScore > 50 ? 'active_new' : 'passive_new';
} else if (daysSinceFirst < 30) {
if (engagementScore > 60) return 'power_user';
if (engagementScore > 30) return 'regular_user';
return 'occasional_user';
} else {
if (engagementScore > 60) return 'loyal_user';
if (engagementScore > 30) return 'returning_user';
if (this.getDaysSinceLastVisit() > 30) return 'at_risk';
return 'dormant';
}
}
/**
* Get user lifecycle stage
*/
getLifecycleStage() {
const totalVisits = this.cohortData.totalVisits;
const daysSinceFirst = this.getDaysSinceFirstVisit();
const daysSinceLast = this.getDaysSinceLastVisit();
if (totalVisits === 1) return 'acquisition';
if (daysSinceFirst < 7 && totalVisits < 5) return 'activation';
if (totalVisits >= 5 && daysSinceLast < 30) return 'active';
if (daysSinceLast >= 30 && daysSinceLast < 60) return 'at_risk';
if (daysSinceLast >= 60) return 'churned';
return 'retention';
}
/**
* Get cohort metrics summary
*/
getCohortMetrics() {
return {
cohortMonth: this.cohortData.cohortMonth,
cohortWeek: this.cohortData.cohortWeek,
firstVisit: this.cohortData.firstVisit,
lastVisit: this.cohortData.lastVisit,
totalVisits: this.cohortData.totalVisits,
totalActions: this.cohortData.actions.length,
daysSinceFirstVisit: this.getDaysSinceFirstVisit(),
daysSinceLastVisit: this.getDaysSinceLastVisit(),
engagementScore: this.getEngagementScore(),
cohortSegment: this.getCohortSegment(),
lifecycleStage: this.getLifecycleStage()
};
}
/**
* Load cohort data from storage
*/
loadCohortData() {
try {
const data = localStorage.getItem('cohort_data');
return data ? JSON.parse(data) : {
visits: [],
actions: []
};
} catch (error) {
console.error('Failed to load cohort data:', error);
return { visits: [], actions: [] };
}
}
/**
* Save cohort data to storage
*/
saveCohortData() {
try {
localStorage.setItem('cohort_data', JSON.stringify(this.cohortData));
} catch (error) {
console.error('Failed to save cohort data:', error);
}
}
/**
* Export cohort data
*/
exportCohortData() {
return {
...this.cohortData,
metrics: this.getCohortMetrics()
};
}
/**
* Send cohort data to backend
*/
async syncWithBackend() {
try {
await fetch('/api/analytics/cohorts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
userId: this.analytics.userId,
sessionId: this.analytics.sessionId,
cohortData: this.exportCohortData()
})
});
} catch (error) {
console.error('Failed to sync cohort data:', error);
}
}
}
// Export
if (typeof module !== 'undefined' && module.exports) {
module.exports = CohortAnalyzer;
} else {
window.CohortAnalyzer = CohortAnalyzer;
}