feat(web): add web and server deploy chart in analytics page

This commit is contained in:
Aman Varshney
2025-08-23 23:30:48 +05:30
parent d45fb7af7e
commit d28a2daea4
6 changed files with 192 additions and 12 deletions

View File

@@ -35,6 +35,8 @@ interface AggregatedAnalyticsData {
addonsDistribution: Array<{ name: string; value: number }>; addonsDistribution: Array<{ name: string; value: number }>;
runtimeDistribution: Array<{ name: string; value: number }>; runtimeDistribution: Array<{ name: string; value: number }>;
projectTypeDistribution: Array<{ name: string; value: number }>; projectTypeDistribution: Array<{ name: string; value: number }>;
webDeployDistribution: Array<{ name: string; value: number }>;
serverDeployDistribution: Array<{ name: string; value: number }>;
popularStackCombinations: Array<{ name: string; value: number }>; popularStackCombinations: Array<{ name: string; value: number }>;
databaseORMCombinations: Array<{ name: string; value: number }>; databaseORMCombinations: Array<{ name: string; value: number }>;
hourlyDistribution: Array<{ hourlyDistribution: Array<{
@@ -51,6 +53,8 @@ interface AggregatedAnalyticsData {
mostPopularORM: string; mostPopularORM: string;
mostPopularAPI: string; mostPopularAPI: string;
mostPopularPackageManager: string; mostPopularPackageManager: string;
mostPopularWebDeploy: string;
mostPopularServerDeploy: string;
}; };
} }
@@ -127,6 +131,8 @@ async function generateAnalyticsData() {
const addonsCounts: Record<string, number> = {}; const addonsCounts: Record<string, number> = {};
const runtimeCounts: Record<string, number> = {}; const runtimeCounts: Record<string, number> = {};
const projectTypeCounts: Record<string, number> = {}; const projectTypeCounts: Record<string, number> = {};
const webDeployCounts: Record<string, number> = {};
const serverDeployCounts: Record<string, number> = {};
const stackComboCounts: Record<string, number> = {}; const stackComboCounts: Record<string, number> = {};
const dbORMComboCounts: Record<string, number> = {}; const dbORMComboCounts: Record<string, number> = {};
const hourlyCounts: Record<number, number> = {}; const hourlyCounts: Record<number, number> = {};
@@ -286,6 +292,18 @@ async function generateAnalyticsData() {
const runtime = row["*.properties.runtime"] || "unknown"; const runtime = row["*.properties.runtime"] || "unknown";
runtimeCounts[runtime] = (runtimeCounts[runtime] || 0) + 1; runtimeCounts[runtime] = (runtimeCounts[runtime] || 0) + 1;
// Web Deploy (migrate "workers" to "wrangler")
const webDeploy = row["*.properties.webDeploy"] || "none";
const normalizedWebDeploy =
webDeploy === "workers" ? "wrangler" : webDeploy;
webDeployCounts[normalizedWebDeploy] =
(webDeployCounts[normalizedWebDeploy] || 0) + 1;
// Server Deploy
const serverDeploy = row["*.properties.serverDeploy"] || "none";
serverDeployCounts[serverDeploy] =
(serverDeployCounts[serverDeploy] || 0) + 1;
// Project type // Project type
const hasFrontend = const hasFrontend =
(frontend0 && frontend0 !== "none") || (frontend0 && frontend0 !== "none") ||
@@ -492,6 +510,16 @@ async function generateAnalyticsData() {
runtimeDistribution: Object.entries(runtimeCounts) runtimeDistribution: Object.entries(runtimeCounts)
.map(([name, value]) => ({ name, value })) .map(([name, value]) => ({ name, value }))
.sort((a, b) => b.value - a.value), .sort((a, b) => b.value - a.value),
// Compare only actual deployment platforms (exclude "none")
webDeployDistribution: [
{ name: "wrangler", value: webDeployCounts.wrangler || 0 },
{ name: "alchemy", value: webDeployCounts.alchemy || 0 },
].sort((a, b) => b.value - a.value),
serverDeployDistribution: [
{ name: "wrangler", value: serverDeployCounts.wrangler || 0 },
{ name: "alchemy", value: serverDeployCounts.alchemy || 0 },
].sort((a, b) => b.value - a.value),
projectTypeDistribution: Object.entries(projectTypeCounts).map( projectTypeDistribution: Object.entries(projectTypeCounts).map(
([name, value]) => ({ name, value }), ([name, value]) => ({ name, value }),
), ),
@@ -517,6 +545,8 @@ async function generateAnalyticsData() {
mostPopularORM: getMostPopular(ormCounts), mostPopularORM: getMostPopular(ormCounts),
mostPopularAPI: getMostPopular(apiCounts), mostPopularAPI: getMostPopular(apiCounts),
mostPopularPackageManager: getMostPopular(packageManagerCounts), mostPopularPackageManager: getMostPopular(packageManagerCounts),
mostPopularWebDeploy: getMostPopular(webDeployCounts),
mostPopularServerDeploy: getMostPopular(serverDeployCounts),
}, },
}; };

View File

@@ -31,10 +31,8 @@ function TechIcon({
}) { }) {
const { theme } = useTheme(); const { theme } = useTheme();
// If no icon, return empty
if (!icon) return null; if (!icon) return null;
// If it's an emoji or text icon, render as span
if (!icon.startsWith("https://")) { if (!icon.startsWith("https://")) {
return ( return (
<span <span
@@ -48,7 +46,6 @@ function TechIcon({
); );
} }
// Handle light theme variants
let iconSrc = icon; let iconSrc = icon;
if ( if (
theme === "light" && theme === "light" &&
@@ -59,7 +56,6 @@ function TechIcon({
iconSrc = icon.replace(".svg", "-light.svg"); iconSrc = icon.replace(".svg", "-light.svg");
} }
// Render as image
return ( return (
<Image <Image
src={iconSrc} src={iconSrc}

View File

@@ -637,7 +637,6 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
}); });
} }
// Workers runtime requires a server deployment (wrangler or alchemy)
if (nextStack.serverDeploy === "none") { if (nextStack.serverDeploy === "none") {
notes.serverDeploy.notes.push( notes.serverDeploy.notes.push(
"Cloudflare Workers runtime requires a server deployment. Wrangler will be selected.", "Cloudflare Workers runtime requires a server deployment. Wrangler will be selected.",
@@ -898,7 +897,6 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
}); });
} }
// Server deployment requires a backend (and not Convex)
if ( if (
nextStack.serverDeploy !== "none" && nextStack.serverDeploy !== "none" &&
(nextStack.backend === "none" || nextStack.backend === "convex") (nextStack.backend === "none" || nextStack.backend === "convex")
@@ -919,7 +917,6 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
}); });
} }
// Cloudflare server deployments (wrangler/alchemy) require Workers runtime
if (nextStack.serverDeploy !== "none" && nextStack.runtime !== "workers") { if (nextStack.serverDeploy !== "none" && nextStack.runtime !== "workers") {
notes.serverDeploy.notes.push( notes.serverDeploy.notes.push(
"Selected server deployment targets Cloudflare Workers. Runtime will be set to 'Cloudflare Workers'.", "Selected server deployment targets Cloudflare Workers. Runtime will be set to 'Cloudflare Workers'.",
@@ -937,7 +934,6 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
"Runtime set to 'Cloudflare Workers' (required by server deployment)", "Runtime set to 'Cloudflare Workers' (required by server deployment)",
}); });
// Apply Workers runtime compatibility adjustments
if (nextStack.backend !== "hono") { if (nextStack.backend !== "hono") {
notes.runtime.notes.push( notes.runtime.notes.push(
"Cloudflare Workers runtime requires Hono backend. Hono will be selected.", "Cloudflare Workers runtime requires Hono backend. Hono will be selected.",
@@ -1004,7 +1000,6 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
} }
} }
// Alchemy deployment validation - temporarily not compatible with Next.js and React Router
const isAlchemyWebDeploy = nextStack.webDeploy === "alchemy"; const isAlchemyWebDeploy = nextStack.webDeploy === "alchemy";
const isAlchemyServerDeploy = nextStack.serverDeploy === "alchemy"; const isAlchemyServerDeploy = nextStack.serverDeploy === "alchemy";
@@ -1034,12 +1029,10 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
notes.webDeploy.hasIssue = true; notes.webDeploy.hasIssue = true;
notes.serverDeploy.hasIssue = true; notes.serverDeploy.hasIssue = true;
// Remove incompatible frontends
nextStack.webFrontend = nextStack.webFrontend.filter( nextStack.webFrontend = nextStack.webFrontend.filter(
(f) => f !== "next" && f !== "react-router", (f) => f !== "next" && f !== "react-router",
); );
// If no web frontends remain, set to default
if (nextStack.webFrontend.length === 0) { if (nextStack.webFrontend.length === 0) {
nextStack.webFrontend = ["tanstack-router"]; nextStack.webFrontend = ["tanstack-router"];
} }
@@ -1639,7 +1632,6 @@ const StackBuilder = () => {
const { adjustedStack } = analyzeStackCompatibility(simulatedStack); const { adjustedStack } = analyzeStackCompatibility(simulatedStack);
const finalStack = adjustedStack ?? simulatedStack; const finalStack = adjustedStack ?? simulatedStack;
// Additional check for Alchemy compatibility with Next.js and React Router
if ( if (
category === "webFrontend" && category === "webFrontend" &&
(optionId === "next" || optionId === "react-router") (optionId === "next" || optionId === "react-router")

View File

@@ -90,6 +90,34 @@ export const getProjectTypeData = (data: AggregatedAnalyticsData | null) => {
return data.projectTypeDistribution || []; return data.projectTypeDistribution || [];
}; };
export const getWebDeployData = (data: AggregatedAnalyticsData | null) => {
if (!data) return [];
const filteredData = (data.webDeployDistribution || []).filter(
(item) => item.name !== "none",
);
const wranglerCount = filteredData
.filter((item) => item.name === "wrangler" || item.name === "workers")
.reduce((sum, item) => sum + item.value, 0);
const alchemyCount = filteredData
.filter((item) => item.name === "alchemy")
.reduce((sum, item) => sum + item.value, 0);
return [
{ name: "wrangler", value: wranglerCount },
{ name: "alchemy", value: alchemyCount },
].filter((item) => item.value > 0);
};
export const getServerDeployData = (data: AggregatedAnalyticsData | null) => {
if (!data) return [];
return (data.serverDeployDistribution || []).filter(
(item) => item.name !== "none",
);
};
export const getMonthlyTimeSeriesData = ( export const getMonthlyTimeSeriesData = (
data: AggregatedAnalyticsData | null, data: AggregatedAnalyticsData | null,
) => { ) => {

View File

@@ -27,6 +27,8 @@ import {
getPopularStackCombinations, getPopularStackCombinations,
getProjectTypeData, getProjectTypeData,
getRuntimeData, getRuntimeData,
getServerDeployData,
getWebDeployData,
} from "./data-utils"; } from "./data-utils";
import type { AggregatedAnalyticsData } from "./types"; import type { AggregatedAnalyticsData } from "./types";
import { import {
@@ -39,6 +41,8 @@ import {
ormConfig, ormConfig,
projectTypeConfig, projectTypeConfig,
runtimeConfig, runtimeConfig,
serverDeployConfig,
webDeployConfig,
} from "./types"; } from "./types";
interface StackConfigurationChartsProps { interface StackConfigurationChartsProps {
@@ -57,6 +61,8 @@ export function StackConfigurationCharts({
const authData = getAuthData(data); const authData = getAuthData(data);
const runtimeData = getRuntimeData(data); const runtimeData = getRuntimeData(data);
const projectTypeData = getProjectTypeData(data); const projectTypeData = getProjectTypeData(data);
const webDeployData = getWebDeployData(data);
const serverDeployData = getServerDeployData(data);
const popularStackCombinations = getPopularStackCombinations(data); const popularStackCombinations = getPopularStackCombinations(data);
const databaseORMCombinations = getDatabaseORMCombinations(data); const databaseORMCombinations = getDatabaseORMCombinations(data);
@@ -546,6 +552,108 @@ export function StackConfigurationCharts({
</ChartContainer> </ChartContainer>
</div> </div>
</div> </div>
<div className="rounded border border-border">
<div className="border-border border-b px-4 py-3">
<div className="flex items-center gap-2">
<span className="text-primary text-xs"></span>
<span className="font-semibold text-sm">
WEB_DEPLOYMENT_COMPARISON.PIE
</span>
</div>
<p className="mt-1 text-muted-foreground text-xs">
Web deployment platform comparison (Wrangler vs Alchemy)
</p>
</div>
<div className="p-4">
<ChartContainer
config={webDeployConfig}
className="h-[300px] w-full"
>
<PieChart>
<ChartTooltip
content={<ChartTooltipContent nameKey="name" />}
/>
<Pie
data={webDeployData}
dataKey="value"
nameKey="name"
cx="50%"
cy="50%"
outerRadius={80}
label={({ name, percent }) =>
`${name} ${(percent * 100).toFixed(0)}%`
}
>
{webDeployData.map((entry) => (
<Cell
key={`web-deploy-${entry.name}`}
fill={
entry.name === "wrangler"
? "hsl(var(--chart-1))"
: entry.name === "alchemy"
? "hsl(var(--chart-2))"
: "hsl(var(--chart-3))"
}
/>
))}
</Pie>
<ChartLegend content={<ChartLegendContent />} />
</PieChart>
</ChartContainer>
</div>
</div>
<div className="rounded border border-border">
<div className="border-border border-b px-4 py-3">
<div className="flex items-center gap-2">
<span className="text-primary text-xs"></span>
<span className="font-semibold text-sm">
SERVER_DEPLOYMENT_COMPARISON.PIE
</span>
</div>
<p className="mt-1 text-muted-foreground text-xs">
Server deployment platform comparison (Wrangler vs Alchemy)
</p>
</div>
<div className="p-4">
<ChartContainer
config={serverDeployConfig}
className="h-[300px] w-full"
>
<PieChart>
<ChartTooltip
content={<ChartTooltipContent nameKey="name" />}
/>
<Pie
data={serverDeployData}
dataKey="value"
nameKey="name"
cx="50%"
cy="50%"
outerRadius={80}
label={({ name, percent }) =>
`${name} ${(percent * 100).toFixed(0)}%`
}
>
{serverDeployData.map((entry) => (
<Cell
key={`server-deploy-${entry.name}`}
fill={
entry.name === "wrangler"
? "hsl(var(--chart-1))"
: entry.name === "alchemy"
? "hsl(var(--chart-2))"
: "hsl(var(--chart-3))"
}
/>
))}
</Pie>
<ChartLegend content={<ChartLegendContent />} />
</PieChart>
</ChartContainer>
</div>
</div>
</div> </div>
<div className="rounded border border-border"> <div className="rounded border border-border">

View File

@@ -27,6 +27,8 @@ export interface AggregatedAnalyticsData {
addonsDistribution: Array<{ name: string; value: number }>; addonsDistribution: Array<{ name: string; value: number }>;
runtimeDistribution: Array<{ name: string; value: number }>; runtimeDistribution: Array<{ name: string; value: number }>;
projectTypeDistribution: Array<{ name: string; value: number }>; projectTypeDistribution: Array<{ name: string; value: number }>;
webDeployDistribution: Array<{ name: string; value: number }>;
serverDeployDistribution: Array<{ name: string; value: number }>;
popularStackCombinations: Array<{ name: string; value: number }>; popularStackCombinations: Array<{ name: string; value: number }>;
databaseORMCombinations: Array<{ name: string; value: number }>; databaseORMCombinations: Array<{ name: string; value: number }>;
hourlyDistribution: Array<{ hourlyDistribution: Array<{
@@ -43,6 +45,8 @@ export interface AggregatedAnalyticsData {
mostPopularORM: string; mostPopularORM: string;
mostPopularAPI: string; mostPopularAPI: string;
mostPopularPackageManager: string; mostPopularPackageManager: string;
mostPopularWebDeploy: string;
mostPopularServerDeploy: string;
}; };
} }
@@ -396,6 +400,28 @@ export const projectTypeConfig = {
}, },
} satisfies ChartConfig; } satisfies ChartConfig;
export const webDeployConfig = {
wrangler: {
label: "Cloudflare Wrangler",
color: "hsl(var(--chart-1))",
},
alchemy: {
label: "Alchemy",
color: "hsl(var(--chart-2))",
},
} satisfies ChartConfig;
export const serverDeployConfig = {
wrangler: {
label: "Cloudflare Wrangler",
color: "hsl(var(--chart-1))",
},
alchemy: {
label: "Alchemy",
color: "hsl(var(--chart-2))",
},
} satisfies ChartConfig;
export const hourlyDistributionConfig = { export const hourlyDistributionConfig = {
count: { count: {
label: "Projects Created", label: "Projects Created",