From 70c1ef0f273721b9f7601e230b472fc04a479286 Mon Sep 17 00:00:00 2001 From: Satya Patel Date: Tue, 23 Dec 2025 13:00:23 -0500 Subject: [PATCH] fix(analytics): use rolling 7-day window for WAU calculation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously WAU was calculated using non-overlapping weekly buckets. Now it uses a proper rolling 7-day window for each day, showing how WAU changes day-over-day. Definition: Users with workspace_created events on 3+ distinct days within the rolling 7-day window ending on each report date. Also updates WAUTrendChart to use 'date' field instead of 'week'. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../WAUTrendChart/WAUTrendChart.tsx | 4 +- .../trpc/src/router/analytics/analytics.ts | 74 +++++++++++-------- 2 files changed, 47 insertions(+), 31 deletions(-) diff --git a/apps/admin/src/app/(dashboard)/components/WAUTrendChart/WAUTrendChart.tsx b/apps/admin/src/app/(dashboard)/components/WAUTrendChart/WAUTrendChart.tsx index 40557cbc0a5..1dc7f334542 100644 --- a/apps/admin/src/app/(dashboard)/components/WAUTrendChart/WAUTrendChart.tsx +++ b/apps/admin/src/app/(dashboard)/components/WAUTrendChart/WAUTrendChart.tsx @@ -18,7 +18,7 @@ import type { ReactNode } from "react"; import { Area, AreaChart, XAxis, YAxis } from "recharts"; interface WAUData { - week: string; + date: string; count: number; } @@ -74,7 +74,7 @@ export function WAUTrendChart({ diff --git a/packages/trpc/src/router/analytics/analytics.ts b/packages/trpc/src/router/analytics/analytics.ts index a633bdc73cd..3eea1445442 100644 --- a/packages/trpc/src/router/analytics/analytics.ts +++ b/packages/trpc/src/router/analytics/analytics.ts @@ -105,41 +105,57 @@ export const analyticsRouter = { ) .query(async ({ input }) => { const days = input?.days ?? 30; - const numWeeks = Math.ceil(days / 7); + const lookbackDays = days + 7; // Extra 7 days to cover rolling windows - // Calculate WAU for each week - const weeklyData: { week: string; count: number }[] = []; - - for (let i = numWeeks - 1; i >= 0; i--) { - const weekEnd = i * 7; - const weekStart = weekEnd + 7; - - // Only count workspace_created as meaningful product usage - const { results } = await executeHogQLQuery<[[number]]>(` - SELECT count(DISTINCT person_id) as wau_users + // Rolling 7-day WAU: for each day, count users with 3+ active days + // of workspace_created events in the preceding 7-day window + const { results } = await executeHogQLQuery<[string, number][]>(` + SELECT + report_date as date, + count(DISTINCT person_id) as wau + FROM ( + SELECT + report_date, + person_id, + count(DISTINCT activity_date) as active_days FROM ( - SELECT person_id, count(DISTINCT toDate(timestamp)) as active_days + SELECT toDate(now()) - number as report_date + FROM numbers(${days}) + ) dates + CROSS JOIN ( + SELECT + person_id, + toDate(timestamp) as activity_date FROM events - WHERE timestamp >= now() - INTERVAL ${weekStart} DAY - AND timestamp < now() - INTERVAL ${weekEnd} DAY - AND event = 'workspace_created' - GROUP BY person_id - HAVING active_days >= 3 - ) - `); - - // Calculate the week's start date for the label - const weekDate = new Date(); - weekDate.setDate(weekDate.getDate() - weekStart); - const weekLabel = weekDate.toISOString().split("T")[0] as string; - - weeklyData.push({ - week: weekLabel, - count: results[0]?.[0] ?? 0, + WHERE event = 'workspace_created' + AND timestamp >= now() - INTERVAL ${lookbackDays} DAY + ) activities + WHERE activity_date > report_date - 7 + AND activity_date <= report_date + GROUP BY report_date, person_id + HAVING active_days >= 3 + ) + GROUP BY report_date + ORDER BY report_date ASC + `); + + // Create a map of existing data + const dataMap = new Map(results.map(([date, count]) => [date, count])); + + // Fill in all dates in the range (in case some days have 0 WAU) + const filledData: { date: string; count: number }[] = []; + const now = new Date(); + for (let i = days - 1; i >= 0; i--) { + const date = new Date(now); + date.setDate(date.getDate() - i); + const dateStr = date.toISOString().split("T")[0] as string; + filledData.push({ + date: dateStr, + count: dataMap.get(dateStr) ?? 0, }); } - return weeklyData; + return filledData; }), getRetention: adminProcedure.query(async () => {