From 1bec912d853322687cad7193f70a08f943157019 Mon Sep 17 00:00:00 2001 From: Aman Varshney Date: Sat, 31 May 2025 21:29:37 +0530 Subject: [PATCH] Add more detailed analytics charts --- apps/web/src/app/(home)/analytics/page.tsx | 451 ++++++++++++++++++++- bun.lock | 2 +- 2 files changed, 440 insertions(+), 13 deletions(-) diff --git a/apps/web/src/app/(home)/analytics/page.tsx b/apps/web/src/app/(home)/analytics/page.tsx index 9fc3db9..85c8279 100644 --- a/apps/web/src/app/(home)/analytics/page.tsx +++ b/apps/web/src/app/(home)/analytics/page.tsx @@ -30,6 +30,7 @@ import Navbar from "../_components/navbar"; interface AnalyticsData { date: string; + hour: number; cli_version: string; node_version: string; platform: string; @@ -47,6 +48,7 @@ interface AnalyticsData { addons: string[]; git: string; install: string; + runtime: string; } const timeSeriesConfig = { @@ -365,6 +367,43 @@ const addonsConfig = { }, } satisfies ChartConfig; +const runtimeConfig = { + node: { + label: "Node.js", + color: "hsl(var(--chart-1))", + }, + bun: { + label: "Bun", + color: "hsl(var(--chart-2))", + }, + none: { + label: "None", + color: "hsl(var(--chart-6))", + }, +} satisfies ChartConfig; + +const projectTypeConfig = { + fullstack: { + label: "Full-stack", + color: "hsl(var(--chart-1))", + }, + "frontend-only": { + label: "Frontend-only", + color: "hsl(var(--chart-2))", + }, + "backend-only": { + label: "Backend-only", + color: "hsl(var(--chart-3))", + }, +} satisfies ChartConfig; + +const hourlyDistributionConfig = { + count: { + label: "Projects Created", + color: "hsl(var(--chart-1))", + }, +} satisfies ChartConfig; + export default function AnalyticsPage() { const [data, setData] = useState([]); const [lastUpdated, setLastUpdated] = useState(null); @@ -387,6 +426,16 @@ export default function AnalyticsPage() { ? timestamp.split("T")[0] : timestamp.split(" ")[0]; + let hour = 0; + try { + const timestampDate = new Date(timestamp); + if (!Number.isNaN(timestampDate.getTime())) { + hour = timestampDate.getUTCHours(); + } + } catch { + hour = 0; + } + const addons = [ row["*.properties.addons.0"], row["*.properties.addons.1"], @@ -398,6 +447,7 @@ export default function AnalyticsPage() { return { date, + hour, cli_version: row["*.properties.cli_version"] || "unknown", node_version: row["*.properties.node_version"] || "unknown", platform: row["*.properties.platform"] || "unknown", @@ -423,6 +473,7 @@ export default function AnalyticsPage() { row["*.properties.install"] === "True" ? "enabled" : "disabled", + runtime: row["*.properties.runtime"] || "unknown", }; }) .filter((item): item is AnalyticsData => @@ -825,6 +876,140 @@ export default function AnalyticsPage() { .sort((a, b) => b.value - a.value); }; + const getRuntimeData = () => { + const runtimeCounts = data.reduce( + (acc, item) => { + const runtime = item.runtime || "none"; + acc[runtime] = (acc[runtime] || 0) + 1; + return acc; + }, + {} as Record, + ); + + return Object.entries(runtimeCounts) + .map(([name, value]) => ({ + name, + value, + })) + .sort((a, b) => b.value - a.value); + }; + + const getProjectTypeData = () => { + const typeCounts = data.reduce( + (acc, item) => { + const hasFrontend = + (item.frontend0 && item.frontend0 !== "none") || + (item.frontend1 && item.frontend1 !== "none"); + const hasBackend = item.backend && item.backend !== "none"; + + let type: string; + if (hasFrontend && hasBackend) { + type = "fullstack"; + } else if (hasFrontend && !hasBackend) { + type = "frontend-only"; + } else if (!hasFrontend && hasBackend) { + type = "backend-only"; + } else { + type = "frontend-only"; + } + + acc[type] = (acc[type] || 0) + 1; + return acc; + }, + {} as Record, + ); + + return Object.entries(typeCounts).map(([name, value]) => ({ + name, + value, + })); + }; + + const getMonthlyTimeSeriesData = () => { + if (data.length === 0) return []; + + const monthlyCounts = data.reduce( + (acc, item) => { + const date = new Date(item.date); + const monthKey = format(date, "yyyy-MM"); + acc[monthKey] = (acc[monthKey] || 0) + 1; + return acc; + }, + {} as Record, + ); + + return Object.entries(monthlyCounts) + .map(([month, count]) => ({ + month, + displayMonth: format(parseISO(`${month}-01`), "MMM yyyy"), + count, + })) + .sort((a, b) => a.month.localeCompare(b.month)); + }; + + const getPopularStackCombinations = () => { + const comboCounts = data.reduce( + (acc, item) => { + const frontend = item.frontend0 || item.frontend1 || "none"; + const backend = item.backend || "none"; + const combo = `${frontend} + ${backend}`; + acc[combo] = (acc[combo] || 0) + 1; + return acc; + }, + {} as Record, + ); + + return Object.entries(comboCounts) + .map(([name, value]) => ({ + name, + value, + })) + .sort((a, b) => b.value - a.value) + .slice(0, 8); + }; + + const getDatabaseORMCombinations = () => { + const comboCounts = data.reduce( + (acc, item) => { + const database = item.database || "none"; + const orm = item.orm || "none"; + if (database !== "none" && orm !== "none") { + const combo = `${database} + ${orm}`; + acc[combo] = (acc[combo] || 0) + 1; + } + return acc; + }, + {} as Record, + ); + + return Object.entries(comboCounts) + .map(([name, value]) => ({ + name, + value, + })) + .sort((a, b) => b.value - a.value) + .slice(0, 6); + }; + + const getHourlyDistributionData = () => { + if (data.length === 0) return []; + + const hourlyCounts = data.reduce( + (acc, item) => { + const hour = item.hour; + acc[hour] = (acc[hour] || 0) + 1; + return acc; + }, + {} as Record, + ); + + return Array.from({ length: 24 }, (_, hour) => ({ + hour: hour.toString().padStart(2, "0"), + displayHour: `${hour.toString().padStart(2, "0")}:00`, + count: hourlyCounts[hour] || 0, + })); + }; + const totalProjects = data.length; const getAvgProjectsPerDay = () => { if (data.length === 0) return 0; @@ -847,11 +1032,18 @@ export default function AnalyticsPage() { const frontendData = getFrontendData(); const backendData = getBackendData(); + const runtimeData = getRuntimeData(); const mostPopularFrontend = frontendData.length > 0 ? frontendData[0].name : "None"; const mostPopularBackend = backendData.length > 0 ? backendData[0].name : "None"; + const projectTypeData = getProjectTypeData(); + const monthlyTimeSeriesData = getMonthlyTimeSeriesData(); + const popularStackCombinations = getPopularStackCombinations(); + const databaseORMCombinations = getDatabaseORMCombinations(); + const hourlyDistributionData = getHourlyDistributionData(); + return (
@@ -1164,6 +1356,44 @@ export default function AnalyticsPage() {
+
+
+
+ + + MONTHLY_TRENDS.CHART + +
+

+ # Monthly project creation trends +

+
+
+ + + + + + } /> + + + +
+
+
@@ -1209,6 +1439,46 @@ export default function AnalyticsPage() {
+ +
+
+
+ + + HOURLY_DISTRIBUTION.BAR + +
+

+ # Projects created by hour of day (UTC) +

+
+
+ + + + + + } + labelFormatter={(value, payload) => { + const hour = payload?.[0]?.payload?.displayHour; + return hour ? `${hour} UTC` : value; + }} + /> + + + +
+
@@ -1225,6 +1495,44 @@ export default function AnalyticsPage() { +
+
+
+ + + POPULAR_STACK_COMBINATIONS.BAR + +
+

+ # Most popular frontend + backend combinations +

+
+
+ + + + + + } /> + + + +
+
+
@@ -1240,7 +1548,7 @@ export default function AnalyticsPage() {
@@ -1507,6 +1815,130 @@ export default function AnalyticsPage() {
+ +
+
+
+ + + RUNTIME_DISTRIBUTION.PIE + +
+

+ # JavaScript runtime preference distribution +

+
+
+ + + } + /> + + `${name} ${(percent * 100).toFixed(0)}%` + } + > + {runtimeData.map((entry) => ( + + ))} + + } /> + + +
+
+ +
+
+
+ + + PROJECT_TYPES.PIE + +
+

+ # Full-stack vs Frontend-only vs Backend-only projects +

+
+
+ + + } + /> + + `${name} ${(percent * 100).toFixed(0)}%` + } + > + {projectTypeData.map((entry) => ( + + ))} + + } /> + + +
+
+
+ +
+
+
+ + + DATABASE_ORM_COMBINATIONS.BAR + +
+

+ # Popular database + ORM combinations +

+
+
+ + + + + + } /> + + + +
@@ -1524,7 +1956,7 @@ export default function AnalyticsPage() {
@@ -1593,16 +2025,11 @@ export default function AnalyticsPage() {
-
-
- - DEV_ENVIRONMENT.CONFIG - -
-
- - [TOOLING_PREFERENCES] +
+ + DEV_ENVIRONMENT.CONFIG +
@@ -1783,7 +2210,7 @@ export default function AnalyticsPage() {
diff --git a/bun.lock b/bun.lock index 1686c25..6df6b06 100644 --- a/bun.lock +++ b/bun.lock @@ -14,7 +14,7 @@ }, "apps/cli": { "name": "create-better-t-stack", - "version": "2.14.1", + "version": "2.14.4", "bin": { "create-better-t-stack": "dist/index.js", },