Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
167 changes: 167 additions & 0 deletions frontend/app/api/analytics/cohorts/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import { NextResponse } from 'next/server';
import { connectToDatabase } from '@/lib/mongodb';

interface CohortData {
cohort: string;
size: number;
retention: number[];
periods: string[];
}

export async function GET(request: Request) {
try {
const { searchParams } = new URL(request.url);
const startDate = new Date(searchParams.get('startDate') || '');
const endDate = new Date(searchParams.get('endDate') || '');
const period = searchParams.get('period') || 'week';
const cohortSize = searchParams.get('cohortSize') || 'week';

if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) {
return NextResponse.json(
{ error: 'Invalid date range' },
{ status: 400 }
);
}

const { db } = await connectToDatabase();

// Function to format date based on period
const formatDate = (date: Date, size: 'day' | 'week' | 'month' = 'week'): string => {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');

switch (size) {
case 'day':
return `${year}-${month}-${day}`;
case 'week': {
// Get week number
const firstDayOfYear = new Date(date.getFullYear(), 0, 1);
const pastDaysOfYear = (date.getTime() - firstDayOfYear.getTime()) / 86400000;
const weekNumber = Math.ceil((pastDaysOfYear + firstDayOfYear.getDay() + 1) / 7);
return `${year}-W${String(weekNumber).padStart(2, '0')}`;
}
case 'month':
default:
return `${year}-${month}`;
}
};

// Get all users grouped by cohort
const usersByCohort = await db.collection('analytics_events').aggregate([
{
$match: {
timestamp: {
$gte: startDate.toISOString(),
$lte: endDate.toISOString(),
},
},
},
{
$group: {
_id: '$userId',
firstSeen: { $min: '$timestamp' },
events: {
$push: {
name: '$name',
timestamp: '$timestamp',
},
},
},
},
{
$project: {
cohort: { $dateToString: { format: '%Y-%m-%d', date: { $toDate: '$firstSeen' } } },
firstSeen: 1,
events: 1,
},
},
{
$group: {
_id: '$cohort',
users: { $push: '$$ROOT' },
size: { $sum: 1 },
},
},
{
$sort: { _id: 1 },
},
]).toArray();

// Calculate retention for each cohort
const cohorts: CohortData[] = [];
const allPeriods: Set<string> = new Set();

for (const cohort of usersByCohort) {
const cohortDate = new Date(cohort._id);
const cohortData: CohortData = {
cohort: formatDate(cohortDate, cohortSize as 'day' | 'week' | 'month'),
size: cohort.size,
retention: [],
periods: [],
};

// Calculate retention for each period
let currentDate = new Date(cohortDate);
let periodIndex = 0;

while (currentDate <= endDate) {
const periodEnd = new Date(currentDate);

// Set period end based on period type
if (period === 'day') {
periodEnd.setDate(periodEnd.getDate() + 1);
} else if (period === 'week') {
periodEnd.setDate(periodEnd.getDate() + 7);
} else {
// month
periodEnd.setMonth(periodEnd.getMonth() + 1);
}

// Count users active in this period
const activeUsers = cohort.users.filter((user: any) => {
return user.events.some((event: any) => {
const eventDate = new Date(event.timestamp);
return eventDate >= currentDate && eventDate < periodEnd;
});
}).length;

const retention = cohort.size > 0 ? (activeUsers / cohort.size) * 100 : 0;
cohortData.retention.push(parseFloat(retention.toFixed(2)));

// Track the period
const periodLabel = formatDate(currentDate, period as 'day' | 'week' | 'month');
cohortData.periods.push(periodLabel);
allPeriods.add(periodLabel);

// Move to next period
currentDate = periodEnd;
periodIndex++;
}

cohorts.push(cohortData);
}

// Ensure all cohorts have the same number of periods
const maxPeriods = Math.max(...cohorts.map(c => c.retention.length));
for (const cohort of cohorts) {
while (cohort.retention.length < maxPeriods) {
cohort.retention.push(0);
// Add a placeholder period if needed
if (cohort.periods.length < maxPeriods) {
const lastPeriod = cohort.periods[cohort.periods.length - 1] || '';
// Try to increment the period (simplified)
cohort.periods.push(`${lastPeriod}+`);
}
}
}

return NextResponse.json(cohorts);
} catch (error) {
console.error('Error analyzing cohorts:', error);
return NextResponse.json(
{ error: 'Failed to analyze cohorts' },
{ status: 500 }
);
}
}
115 changes: 115 additions & 0 deletions frontend/app/api/analytics/funnel/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { NextResponse } from 'next/server';
import { connectToDatabase } from '@/lib/mongodb';

export async function GET(request: Request) {
try {
const { searchParams } = new URL(request.url);
const steps = searchParams.get('steps')?.split(',') || [];
const startDate = new Date(searchParams.get('startDate') || '');
const endDate = new Date(searchParams.get('endDate') || '');

if (steps.length === 0 || isNaN(startDate.getTime()) || isNaN(endDate.getTime())) {
return NextResponse.json(
{ error: 'Missing or invalid parameters' },
{ status: 400 }
);
}

const { db } = await connectToDatabase();
const funnelData = [];

// Get count for each step in the funnel
for (let i = 0; i < steps.length; i++) {
const step = steps[i];

// For the first step, just count the unique users
if (i === 0) {
const result = await db.collection('analytics_events').aggregate([
{
$match: {
name: step,
timestamp: {
$gte: startDate.toISOString(),
$lte: endDate.toISOString(),
},
},
},
{
$group: {
_id: '$userId',
count: { $sum: 1 },
firstSeen: { $min: '$timestamp' },
},
},
{
$count: 'total',
},
]).toArray();

funnelData.push({
name: step,
count: result[0]?.total || 0,
percentage: 100, // First step is 100%
});
} else {
// For subsequent steps, count users who completed this step after completing all previous steps
const previousSteps = steps.slice(0, i);

const result = await db.collection('analytics_events').aggregate([
{
$match: {
$or: [
{ name: step },
...previousSteps.map(prevStep => ({ name: prevStep })),
],
timestamp: {
$gte: startDate.toISOString(),
$lte: endDate.toISOString(),
},
},
},
{
$group: {
_id: '$userId',
steps: { $addToSet: '$name' },
},
},
{
$match: {
// User must have all previous steps and the current step
$expr: {
$and: [
{ $in: [step, '$steps'] },
...previousSteps.map(prevStep => ({
$in: [prevStep, '$steps'],
})),
],
},
},
},
{
$count: 'total',
},
]).toArray();

const percentage = funnelData[0]?.count > 0
? Math.round(((result[0]?.total || 0) / funnelData[0].count) * 100 * 100) / 100
: 0;

funnelData.push({
name: step,
count: result[0]?.total || 0,
percentage,
});
}
}

return NextResponse.json(funnelData);
} catch (error) {
console.error('Error analyzing funnel:', error);
return NextResponse.json(
{ error: 'Failed to analyze funnel' },
{ status: 500 }
);
}
}
78 changes: 78 additions & 0 deletions frontend/app/api/analytics/retention/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { NextResponse } from 'next/server';
import { connectToDatabase } from '@/lib/mongodb';

export async function GET(request: Request) {
try {
const { searchParams } = new URL(request.url);
const startDate = new Date(searchParams.get('startDate') || '');
const endDate = new Date(searchParams.get('endDate') || '');
const cohortSize = searchParams.get('cohortSize') || 'day';

if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) {
return NextResponse.json(
{ error: 'Invalid date range' },
{ status: 400 }
);
}

const { db } = await connectToDatabase();

// Get all users who performed any action in the date range
const usersPipeline = [
{
$match: {
timestamp: {
$gte: startDate.toISOString(),
$lte: endDate.toISOString(),
},
},
},
{
$group: {
_id: '$userId',
firstSeen: { $min: '$timestamp' },
lastSeen: { $max: '$timestamp' },
},
},
];

const users = await db
.collection('analytics_events')
.aggregate(usersPipeline)
.toArray();

// Calculate retention metrics
const retentionData = [];
const days = Math.ceil((endDate.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24));

for (let day = 0; day <= days; day++) {
const currentDate = new Date(startDate);
currentDate.setDate(startDate.getDate() + day);

const retainedUsers = users.filter(user => {
const userFirstSeen = new Date(user.firstSeen);
const userLastSeen = new Date(user.lastSeen);

// Check if user was active on this day after their first seen date
return (
userFirstSeen <= currentDate &&
userLastSeen >= currentDate
);
}).length;

retentionData.push({
day,
retained: retainedUsers,
percentage: users.length > 0 ? (retainedUsers / users.length) * 100 : 0,
});
}

return NextResponse.json(retentionData);
} catch (error) {
console.error('Error calculating retention:', error);
return NextResponse.json(
{ error: 'Failed to calculate retention' },
{ status: 500 }
);
}
}
Loading