From 12ccfae419def0e7e7d6c150d02c0e860baf8548 Mon Sep 17 00:00:00 2001 From: mantrakp04 Date: Tue, 5 May 2026 10:20:20 -0700 Subject: [PATCH 1/3] feat(dashboard): add weekly users metrics for projects - Introduced a new API endpoint to fetch weekly and daily user metrics for managed projects. - Updated the dashboard to utilize this new endpoint, replacing the previous daily active users data. - Created a new component to visualize weekly users metrics in the project cards. - Refactored existing components to accommodate the new data structure and ensure proper rendering of user activity charts. This change enhances the analytics capabilities of the dashboard, providing better insights into user engagement over time. --- .../route.tsx | 66 ++++++++++++++----- .../projects/page-client.tsx | 46 +++++++++---- .../dashboard/src/components/project-card.tsx | 7 +- ...ne.tsx => project-weekly-users-metric.tsx} | 17 ++--- 4 files changed, 96 insertions(+), 40 deletions(-) rename apps/backend/src/app/api/latest/internal/{projects-dau => projects-weekly-users}/route.tsx (64%) rename apps/dashboard/src/components/{project-dau-sparkline.tsx => project-weekly-users-metric.tsx} (85%) diff --git a/apps/backend/src/app/api/latest/internal/projects-dau/route.tsx b/apps/backend/src/app/api/latest/internal/projects-weekly-users/route.tsx similarity index 64% rename from apps/backend/src/app/api/latest/internal/projects-dau/route.tsx rename to apps/backend/src/app/api/latest/internal/projects-weekly-users/route.tsx index 6700632049..9685940ac6 100644 --- a/apps/backend/src/app/api/latest/internal/projects-dau/route.tsx +++ b/apps/backend/src/app/api/latest/internal/projects-weekly-users/route.tsx @@ -24,7 +24,10 @@ export const GET = createSmartRouteHandler({ statusCode: yupNumber().oneOf([200]).defined(), bodyType: yupString().oneOf(["json"]).defined(), body: yupObject({ - projects: yupRecord(yupString().defined(), MetricsDataPointsSchema).defined(), + projects: yupRecord(yupString().defined(), yupObject({ + weekly_users: yupNumber().integer().defined(), + daily_users: MetricsDataPointsSchema, + }).defined()).defined(), }).defined(), }), handler: async (req) => { @@ -52,9 +55,12 @@ export const GET = createSmartRouteHandler({ return out; }; - const byProject: Record = {}; + const byProject: Record = {}; for (const id of projectIds) { - byProject[id] = emptySeries(); + byProject[id] = { + weekly_users: 0, + daily_users: emptySeries(), + }; } if (projectIds.length === 0) { @@ -65,15 +71,41 @@ export const GET = createSmartRouteHandler({ }; } - let rows: { projectId: string, day: string, dau: number }[] = []; + let rows: { projectId: string, weeklyUsers: number }[] = []; + let dailyRows: { projectId: string, day: string, dailyUsers: number }[] = []; try { const clickhouseClient = getClickhouseAdminClient(); const result = await clickhouseClient.query({ + query: ` + SELECT + project_id AS projectId, + uniqExact(assumeNotNull(user_id)) AS weeklyUsers + FROM analytics_internal.events + WHERE event_type = '$token-refresh' + AND project_id IN {projectIds:Array(String)} + AND branch_id = {branchId:String} + AND user_id IS NOT NULL + AND event_at >= {since:DateTime} + AND event_at < {untilExclusive:DateTime} + AND coalesce(CAST(data.is_anonymous, 'Nullable(UInt8)'), 0) = 0 + GROUP BY projectId + `, + query_params: { + projectIds, + branchId: DEFAULT_BRANCH_ID, + since: since.toISOString().slice(0, 19), + untilExclusive: untilExclusive.toISOString().slice(0, 19), + }, + format: "JSONEachRow", + }); + rows = await result.json(); + + const dailyResult = await clickhouseClient.query({ query: ` SELECT project_id AS projectId, toDate(event_at) AS day, - uniqExact(assumeNotNull(user_id)) AS dau + uniqExact(assumeNotNull(user_id)) AS dailyUsers FROM analytics_internal.events WHERE event_type = '$token-refresh' AND project_id IN {projectIds:Array(String)} @@ -92,13 +124,13 @@ export const GET = createSmartRouteHandler({ }, format: "JSONEachRow", }); - rows = await result.json(); + dailyRows = await dailyResult.json(); } catch (error) { const captureId = error instanceof ClickHouseError - ? "internal-projects-dau-clickhouse-error" - : "internal-projects-dau-unexpected-error"; + ? "internal-projects-weekly-users-clickhouse-error" + : "internal-projects-weekly-users-unexpected-error"; captureError(captureId, new StackAssertionError( - "Failed to load projects DAU.", + "Failed to load projects weekly users.", { cause: error, projectCount: projectIds.length }, )); return { @@ -107,21 +139,25 @@ export const GET = createSmartRouteHandler({ body: { projects: byProject }, }; } - const index = new Map>(); for (const row of rows) { + byProject[row.projectId].weekly_users = Number(row.weeklyUsers); + } + + const dailyIndex = new Map>(); + for (const row of dailyRows) { const dayKey = row.day.split("T")[0]; - let m = index.get(row.projectId); + let m = dailyIndex.get(row.projectId); if (!m) { m = new Map(); - index.set(row.projectId, m); + dailyIndex.set(row.projectId, m); } - m.set(dayKey, Number(row.dau)); + m.set(dayKey, Number(row.dailyUsers)); } for (const id of projectIds) { - const m = index.get(id); + const m = dailyIndex.get(id); if (!m) continue; - byProject[id] = byProject[id].map((point) => ({ + byProject[id].daily_users = byProject[id].daily_users.map((point) => ({ date: point.date, activity: m.get(point.date) ?? 0, })); diff --git a/apps/dashboard/src/app/(main)/(protected)/(outside-dashboard)/projects/page-client.tsx b/apps/dashboard/src/app/(main)/(protected)/(outside-dashboard)/projects/page-client.tsx index b8e3cdbfa1..438eb84a54 100644 --- a/apps/dashboard/src/app/(main)/(protected)/(outside-dashboard)/projects/page-client.tsx +++ b/apps/dashboard/src/app/(main)/(protected)/(outside-dashboard)/projects/page-client.tsx @@ -68,7 +68,8 @@ export default function PageClient() { const [openingConfigFile, setOpeningConfigFile] = useState(false); const [projectStatuses, setProjectStatuses] = useState>(new Map()); const [loadingProjectStatuses, setLoadingProjectStatuses] = useState(true); - const [projectDau, setProjectDau] = useState>(new Map()); + const [projectWeeklyUsers, setProjectWeeklyUsers] = useState>(new Map()); + const [projectWeeklyUsersChart, setProjectWeeklyUsersChart] = useState>(new Map()); const router = useRouter(); useEffect(() => { @@ -124,32 +125,48 @@ export default function PageClient() { let cancelled = false; runAsynchronously(async () => { try { - const response = await appInternals.sendRequest("/internal/projects-dau", {}, "client"); + const response = await appInternals.sendRequest("/internal/projects-weekly-users", {}, "client"); if (!response.ok) { - console.warn("[projects-dau] request failed", response.status, await response.text()); + console.warn("[projects-weekly-users] request failed", response.status, await response.text()); return; } const body = await response.json(); if (body == null || typeof body !== "object" || !("projects" in body) || body.projects == null || typeof body.projects !== "object") { - console.warn("[projects-dau] unexpected body", body); + console.warn("[projects-weekly-users] unexpected body", body); return; } - const map = new Map(); - for (const [projectId, series] of Object.entries(body.projects as Record)) { - if (!Array.isArray(series)) continue; + const weeklyUsersMap = new Map(); + const weeklyUsersChartMap = new Map(); + for (const [projectId, value] of Object.entries(body.projects)) { + if (value == null || typeof value !== "object") { + continue; + } + const weeklyUsers = "weekly_users" in value ? value.weekly_users : undefined; + if (typeof weeklyUsers === "number") { + weeklyUsersMap.set(projectId, weeklyUsers); + } + const dailyUsers = "daily_users" in value ? value.daily_users : undefined; + if (!Array.isArray(dailyUsers)) { + continue; + } const points: { date: string, activity: number }[] = []; - for (const point of series) { - if (point != null && typeof point === "object" && "date" in point && "activity" in point && typeof (point as any).date === "string" && typeof (point as any).activity === "number") { - points.push({ date: (point as any).date, activity: (point as any).activity }); + for (const point of dailyUsers) { + if (point != null && typeof point === "object" && "date" in point && "activity" in point) { + const date = point.date; + const activity = point.activity; + if (typeof date === "string" && typeof activity === "number") { + points.push({ date, activity }); + } } } - map.set(projectId, points); + weeklyUsersChartMap.set(projectId, points); } if (!cancelled) { - setProjectDau(map); + setProjectWeeklyUsers(weeklyUsersMap); + setProjectWeeklyUsersChart(weeklyUsersChartMap); } } catch (e) { - console.warn("[projects-dau] fetch error", e); + console.warn("[projects-weekly-users] fetch error", e); } }); return () => { @@ -355,7 +372,8 @@ export default function PageClient() { project={project} href={projectHref} showIncompleteBadge={!loadingProjectStatuses && onboardingStatus !== "completed"} - dau={projectDau.get(project.id)} + weeklyUsers={projectWeeklyUsers.get(project.id)} + weeklyUsersChart={projectWeeklyUsersChart.get(project.id)} /> ); })} diff --git a/apps/dashboard/src/components/project-card.tsx b/apps/dashboard/src/components/project-card.tsx index 8d9918cdca..c4b4132da9 100644 --- a/apps/dashboard/src/components/project-card.tsx +++ b/apps/dashboard/src/components/project-card.tsx @@ -2,7 +2,7 @@ import { DesignBadge } from "@/components/design-components/badge"; import { DesignCard } from "@/components/design-components/card"; import { Link } from "@/components/link"; -import { ProjectDauSparkline } from "@/components/project-dau-sparkline"; +import { ProjectWeeklyUsersMetric } from "@/components/project-weekly-users-metric"; import { useFromNow } from '@/hooks/use-from-now'; import { FolderOpenIcon } from "@phosphor-icons/react"; import { AdminProject } from '@stackframe/stack'; @@ -12,7 +12,8 @@ export function ProjectCard(props: { project: AdminProject, href?: string, showIncompleteBadge?: boolean, - dau?: { date: string, activity: number }[], + weeklyUsers?: number, + weeklyUsersChart?: { date: string, activity: number }[], }) { const createdAt = useFromNow(props.project.createdAt); const href = props.href ?? urlString`/projects/${props.project.id}`; @@ -49,7 +50,7 @@ export function ProjectCard(props: {
- +
diff --git a/apps/dashboard/src/components/project-dau-sparkline.tsx b/apps/dashboard/src/components/project-weekly-users-metric.tsx similarity index 85% rename from apps/dashboard/src/components/project-dau-sparkline.tsx rename to apps/dashboard/src/components/project-weekly-users-metric.tsx index 7b50270d9d..9241ad423e 100644 --- a/apps/dashboard/src/components/project-dau-sparkline.tsx +++ b/apps/dashboard/src/components/project-weekly-users-metric.tsx @@ -33,10 +33,11 @@ function EmptyBaseline({ count }: { count: number }) { ); } -export function ProjectDauSparkline(props: { data: DataPoint[] | undefined }) { +export function ProjectWeeklyUsersMetric(props: { weeklyUsers: number | undefined, data: DataPoint[] | undefined }) { + const weeklyUsers = props.weeklyUsers ?? 0; const data = props.data; - const total = data?.reduce((sum, d) => sum + d.activity, 0) ?? 0; - const hasActivity = total > 0; + const dailyTotal = data?.reduce((sum, d) => sum + d.activity, 0) ?? 0; + const hasActivity = weeklyUsers > 0 || dailyTotal > 0; const gradId = useId().replace(/:/g, ''); return ( @@ -48,10 +49,10 @@ export function ProjectDauSparkline(props: { data: DataPoint[] | undefined }) { (hasActivity ? 'text-foreground' : 'text-muted-foreground/50') } > - {total.toLocaleString()} + {weeklyUsers.toLocaleString()} - /wk + users/wk @@ -61,7 +62,7 @@ export function ProjectDauSparkline(props: { data: DataPoint[] | undefined }) { - + @@ -79,14 +80,14 @@ export function ProjectDauSparkline(props: { data: DataPoint[] | undefined }) { labelStyle={{ color: 'hsl(var(--muted-foreground))', marginBottom: 1, fontSize: 10 }} itemStyle={{ color: 'hsl(var(--foreground))', padding: 0 }} labelFormatter={(label: string) => formatDay(label)} - formatter={(value: number) => [value.toLocaleString(), 'active']} + formatter={(value: number) => [value.toLocaleString(), 'daily active users']} /> From 76e67122a1ea0c3b6f5bb9f43b2e1cb5764dfeb6 Mon Sep 17 00:00:00 2001 From: mantrakp04 Date: Tue, 5 May 2026 17:26:33 -0700 Subject: [PATCH 2/3] feat(projects): implement applyProjectWeeklyUsersRows function and add tests - Introduced the `applyProjectWeeklyUsersRows` function to process weekly and daily user metrics for projects. - Created a new test file to validate the functionality of the new helper, ensuring it correctly applies user data and handles unknown projects. - Updated the existing route to utilize the new function for better code organization and clarity. This change enhances the backend's ability to manage and analyze user engagement metrics for projects. --- .../projects-weekly-users/route.test.tsx | 68 +++++++ .../internal/projects-weekly-users/route.tsx | 180 ++++++++++-------- .../projects/page-client.tsx | 70 ++++--- 3 files changed, 201 insertions(+), 117 deletions(-) create mode 100644 apps/backend/src/app/api/latest/internal/projects-weekly-users/route.test.tsx diff --git a/apps/backend/src/app/api/latest/internal/projects-weekly-users/route.test.tsx b/apps/backend/src/app/api/latest/internal/projects-weekly-users/route.test.tsx new file mode 100644 index 0000000000..6eb0075b13 --- /dev/null +++ b/apps/backend/src/app/api/latest/internal/projects-weekly-users/route.test.tsx @@ -0,0 +1,68 @@ +import { describe, expect, it } from "vitest"; +import { applyProjectWeeklyUsersRows } from "./route"; + +describe("internal projects weekly users helpers", () => { + it("applies ClickHouse rows through a Map and skips unknown projects", () => { + const byProject = new Map([ + ["project-a", { + weekly_users: 0, + daily_users: [ + { date: "2026-05-01", activity: 0 }, + { date: "2026-05-02", activity: 0 }, + ], + }], + ["__proto__", { + weekly_users: 0, + daily_users: [ + { date: "2026-05-01", activity: 0 }, + { date: "2026-05-02", activity: 0 }, + ], + }], + ]); + + applyProjectWeeklyUsersRows( + byProject, + [ + { projectId: "project-a", weeklyUsers: 4 }, + { projectId: "__proto__", weeklyUsers: 7 }, + { projectId: "missing-project", weeklyUsers: 99 }, + ], + [ + { projectId: "project-a", day: "2026-05-01", dailyUsers: 2 }, + { projectId: "__proto__", day: "2026-05-02", dailyUsers: 5 }, + { projectId: "missing-project", day: "2026-05-01", dailyUsers: 99 }, + ], + ); + + expect(Object.fromEntries(byProject)).toMatchInlineSnapshot(` + { + "__proto__": { + "daily_users": [ + { + "activity": 0, + "date": "2026-05-01", + }, + { + "activity": 5, + "date": "2026-05-02", + }, + ], + "weekly_users": 7, + }, + "project-a": { + "daily_users": [ + { + "activity": 2, + "date": "2026-05-01", + }, + { + "activity": 0, + "date": "2026-05-02", + }, + ], + "weekly_users": 4, + }, + } + `); + }); +}); diff --git a/apps/backend/src/app/api/latest/internal/projects-weekly-users/route.tsx b/apps/backend/src/app/api/latest/internal/projects-weekly-users/route.tsx index 9685940ac6..a147189772 100644 --- a/apps/backend/src/app/api/latest/internal/projects-weekly-users/route.tsx +++ b/apps/backend/src/app/api/latest/internal/projects-weekly-users/route.tsx @@ -10,6 +10,48 @@ import { StackAssertionError, captureError } from "@stackframe/stack-shared/dist const WINDOW_DAYS = 7; const ONE_DAY_MS = 24 * 60 * 60 * 1000; +type ProjectWeeklyUsers = { + weekly_users: number, + daily_users: { date: string, activity: number }[], +}; + +export function applyProjectWeeklyUsersRows( + byProject: Map, + rows: { projectId: string, weeklyUsers: number }[], + dailyRows: { projectId: string, day: string, dailyUsers: number }[], +) { + for (const row of rows) { + const project = byProject.get(row.projectId); + if (project == null) { + continue; + } + project.weekly_users = Number(row.weeklyUsers); + } + + const dailyIndex = new Map>(); + for (const row of dailyRows) { + if (!byProject.has(row.projectId)) { + continue; + } + const dayKey = row.day.split("T")[0]; + let m = dailyIndex.get(row.projectId); + if (!m) { + m = new Map(); + dailyIndex.set(row.projectId, m); + } + m.set(dayKey, Number(row.dailyUsers)); + } + + for (const [id, project] of byProject) { + const m = dailyIndex.get(id); + if (!m) continue; + project.daily_users = project.daily_users.map((point) => ({ + date: point.date, + activity: m.get(point.date) ?? 0, + })); + } +} + export const GET = createSmartRouteHandler({ metadata: { hidden: true }, request: yupObject({ @@ -55,76 +97,77 @@ export const GET = createSmartRouteHandler({ return out; }; - const byProject: Record = {}; + const byProject = new Map(); for (const id of projectIds) { - byProject[id] = { + byProject.set(id, { weekly_users: 0, daily_users: emptySeries(), - }; + }); } + const projectsResponse = () => Object.fromEntries(byProject); if (projectIds.length === 0) { return { statusCode: 200, bodyType: "json", - body: { projects: byProject }, + body: { projects: projectsResponse() }, }; } + const clickhouseClient = getClickhouseAdminClient(); + const queryParams = { + projectIds, + branchId: DEFAULT_BRANCH_ID, + since: since.toISOString().slice(0, 19), + untilExclusive: untilExclusive.toISOString().slice(0, 19), + }; + let rows: { projectId: string, weeklyUsers: number }[] = []; let dailyRows: { projectId: string, day: string, dailyUsers: number }[] = []; try { - const clickhouseClient = getClickhouseAdminClient(); - const result = await clickhouseClient.query({ - query: ` - SELECT - project_id AS projectId, - uniqExact(assumeNotNull(user_id)) AS weeklyUsers - FROM analytics_internal.events - WHERE event_type = '$token-refresh' - AND project_id IN {projectIds:Array(String)} - AND branch_id = {branchId:String} - AND user_id IS NOT NULL - AND event_at >= {since:DateTime} - AND event_at < {untilExclusive:DateTime} - AND coalesce(CAST(data.is_anonymous, 'Nullable(UInt8)'), 0) = 0 - GROUP BY projectId - `, - query_params: { - projectIds, - branchId: DEFAULT_BRANCH_ID, - since: since.toISOString().slice(0, 19), - untilExclusive: untilExclusive.toISOString().slice(0, 19), - }, - format: "JSONEachRow", - }); - rows = await result.json(); - - const dailyResult = await clickhouseClient.query({ - query: ` - SELECT - project_id AS projectId, - toDate(event_at) AS day, - uniqExact(assumeNotNull(user_id)) AS dailyUsers - FROM analytics_internal.events - WHERE event_type = '$token-refresh' - AND project_id IN {projectIds:Array(String)} - AND branch_id = {branchId:String} - AND user_id IS NOT NULL - AND event_at >= {since:DateTime} - AND event_at < {untilExclusive:DateTime} - AND coalesce(CAST(data.is_anonymous, 'Nullable(UInt8)'), 0) = 0 - GROUP BY projectId, day - `, - query_params: { - projectIds, - branchId: DEFAULT_BRANCH_ID, - since: since.toISOString().slice(0, 19), - untilExclusive: untilExclusive.toISOString().slice(0, 19), - }, - format: "JSONEachRow", - }); - dailyRows = await dailyResult.json(); + const [weeklyResult, dailyResult] = await Promise.all([ + clickhouseClient.query({ + query: ` + SELECT + project_id AS projectId, + uniqExact(assumeNotNull(user_id)) AS weeklyUsers + FROM analytics_internal.events + WHERE event_type = '$token-refresh' + AND project_id IN {projectIds:Array(String)} + AND branch_id = {branchId:String} + AND user_id IS NOT NULL + AND event_at >= {since:DateTime} + AND event_at < {untilExclusive:DateTime} + AND coalesce(CAST(data.is_anonymous, 'Nullable(UInt8)'), 0) = 0 + GROUP BY projectId + `, + query_params: queryParams, + format: "JSONEachRow", + }), + clickhouseClient.query({ + query: ` + SELECT + project_id AS projectId, + toDate(event_at) AS day, + uniqExact(assumeNotNull(user_id)) AS dailyUsers + FROM analytics_internal.events + WHERE event_type = '$token-refresh' + AND project_id IN {projectIds:Array(String)} + AND branch_id = {branchId:String} + AND user_id IS NOT NULL + AND event_at >= {since:DateTime} + AND event_at < {untilExclusive:DateTime} + AND coalesce(CAST(data.is_anonymous, 'Nullable(UInt8)'), 0) = 0 + GROUP BY projectId, day + `, + query_params: queryParams, + format: "JSONEachRow", + }), + ]); + [rows, dailyRows] = await Promise.all([ + weeklyResult.json<{ projectId: string, weeklyUsers: number }>(), + dailyResult.json<{ projectId: string, day: string, dailyUsers: number }>(), + ]); } catch (error) { const captureId = error instanceof ClickHouseError ? "internal-projects-weekly-users-clickhouse-error" @@ -136,37 +179,16 @@ export const GET = createSmartRouteHandler({ return { statusCode: 200, bodyType: "json", - body: { projects: byProject }, + body: { projects: projectsResponse() }, }; } - for (const row of rows) { - byProject[row.projectId].weekly_users = Number(row.weeklyUsers); - } - const dailyIndex = new Map>(); - for (const row of dailyRows) { - const dayKey = row.day.split("T")[0]; - let m = dailyIndex.get(row.projectId); - if (!m) { - m = new Map(); - dailyIndex.set(row.projectId, m); - } - m.set(dayKey, Number(row.dailyUsers)); - } - - for (const id of projectIds) { - const m = dailyIndex.get(id); - if (!m) continue; - byProject[id].daily_users = byProject[id].daily_users.map((point) => ({ - date: point.date, - activity: m.get(point.date) ?? 0, - })); - } + applyProjectWeeklyUsersRows(byProject, rows, dailyRows); return { statusCode: 200, bodyType: "json", - body: { projects: byProject }, + body: { projects: projectsResponse() }, }; }, }); diff --git a/apps/dashboard/src/app/(main)/(protected)/(outside-dashboard)/projects/page-client.tsx b/apps/dashboard/src/app/(main)/(protected)/(outside-dashboard)/projects/page-client.tsx index 438eb84a54..07f19d661c 100644 --- a/apps/dashboard/src/app/(main)/(protected)/(outside-dashboard)/projects/page-client.tsx +++ b/apps/dashboard/src/app/(main)/(protected)/(outside-dashboard)/projects/page-client.tsx @@ -124,49 +124,43 @@ export default function PageClient() { useEffect(() => { let cancelled = false; runAsynchronously(async () => { - try { - const response = await appInternals.sendRequest("/internal/projects-weekly-users", {}, "client"); - if (!response.ok) { - console.warn("[projects-weekly-users] request failed", response.status, await response.text()); - return; + const response = await appInternals.sendRequest("/internal/projects-weekly-users", {}, "client"); + if (!response.ok) { + throw new Error(`Failed to load project weekly users: ${response.status} ${await response.text()}`); + } + const body = await response.json(); + if (body == null || typeof body !== "object" || !("projects" in body) || body.projects == null || typeof body.projects !== "object") { + throw new Error("Failed to load project weekly users: response body did not include a projects object."); + } + const weeklyUsersMap = new Map(); + const weeklyUsersChartMap = new Map(); + for (const [projectId, value] of Object.entries(body.projects)) { + if (value == null || typeof value !== "object") { + continue; } - const body = await response.json(); - if (body == null || typeof body !== "object" || !("projects" in body) || body.projects == null || typeof body.projects !== "object") { - console.warn("[projects-weekly-users] unexpected body", body); - return; + const weeklyUsers = "weekly_users" in value ? value.weekly_users : undefined; + if (typeof weeklyUsers === "number") { + weeklyUsersMap.set(projectId, weeklyUsers); } - const weeklyUsersMap = new Map(); - const weeklyUsersChartMap = new Map(); - for (const [projectId, value] of Object.entries(body.projects)) { - if (value == null || typeof value !== "object") { - continue; - } - const weeklyUsers = "weekly_users" in value ? value.weekly_users : undefined; - if (typeof weeklyUsers === "number") { - weeklyUsersMap.set(projectId, weeklyUsers); - } - const dailyUsers = "daily_users" in value ? value.daily_users : undefined; - if (!Array.isArray(dailyUsers)) { - continue; - } - const points: { date: string, activity: number }[] = []; - for (const point of dailyUsers) { - if (point != null && typeof point === "object" && "date" in point && "activity" in point) { - const date = point.date; - const activity = point.activity; - if (typeof date === "string" && typeof activity === "number") { - points.push({ date, activity }); - } + const dailyUsers = "daily_users" in value ? value.daily_users : undefined; + if (!Array.isArray(dailyUsers)) { + continue; + } + const points: { date: string, activity: number }[] = []; + for (const point of dailyUsers) { + if (point != null && typeof point === "object" && "date" in point && "activity" in point) { + const date = point.date; + const activity = point.activity; + if (typeof date === "string" && typeof activity === "number") { + points.push({ date, activity }); } } - weeklyUsersChartMap.set(projectId, points); - } - if (!cancelled) { - setProjectWeeklyUsers(weeklyUsersMap); - setProjectWeeklyUsersChart(weeklyUsersChartMap); } - } catch (e) { - console.warn("[projects-weekly-users] fetch error", e); + weeklyUsersChartMap.set(projectId, points); + } + if (!cancelled) { + setProjectWeeklyUsers(weeklyUsersMap); + setProjectWeeklyUsersChart(weeklyUsersChartMap); } }); return () => { From 7ef33a11c3f0f566ef9152d16f100d9c8115de79 Mon Sep 17 00:00:00 2001 From: mantrakp04 Date: Tue, 5 May 2026 17:33:37 -0700 Subject: [PATCH 3/3] fix(api): adjust date conversion to UTC in projects weekly users query - Updated the SQL query in the projects weekly users API to convert event dates to UTC using `toDate(event_at, 'UTC')`. - This change ensures consistent date handling across different time zones, improving the accuracy of user metrics. This fix enhances the reliability of the analytics data retrieved from the backend. --- .../src/app/api/latest/internal/projects-weekly-users/route.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/backend/src/app/api/latest/internal/projects-weekly-users/route.tsx b/apps/backend/src/app/api/latest/internal/projects-weekly-users/route.tsx index a147189772..35886f24ec 100644 --- a/apps/backend/src/app/api/latest/internal/projects-weekly-users/route.tsx +++ b/apps/backend/src/app/api/latest/internal/projects-weekly-users/route.tsx @@ -148,7 +148,7 @@ export const GET = createSmartRouteHandler({ query: ` SELECT project_id AS projectId, - toDate(event_at) AS day, + toDate(event_at, 'UTC') AS day, uniqExact(assumeNotNull(user_id)) AS dailyUsers FROM analytics_internal.events WHERE event_type = '$token-refresh'