diff --git a/src/components/ConversionDashboard.tsx b/src/components/ConversionDashboard.tsx index ecf4752..7837a5d 100644 --- a/src/components/ConversionDashboard.tsx +++ b/src/components/ConversionDashboard.tsx @@ -24,17 +24,17 @@ interface DailyCount { count: number; } -interface PropBreakdown { +interface ConversionDetail { goal: string; - prop: string; - value: string; + amount: string; + variant: string; count: number; } interface StatsResponse { plausible: DailyCount[]; dropped: DailyCount[]; - propBreakdowns: PropBreakdown[]; + details: ConversionDetail[]; dateFrom: string; dateTo: string; error?: string; @@ -335,28 +335,27 @@ export default function ConversionDashboard({ - {/* Amount breakdowns per goal */} - {data.propBreakdowns.length > 0 && ( + {/* Conversion details per goal */} + {data.details.length > 0 && (
{goalKeys.map((goal) => { - const allAmounts = data.propBreakdowns.filter( - (p) => p.goal === goal && p.prop === "amount" - ); - if (allAmounts.length === 0) return null; - const tracked = allAmounts - .filter((p) => p.value && !isNaN(parseFloat(p.value))) - .sort((a, b) => parseFloat(b.value) - parseFloat(a.value)); - const untrackedCount = allAmounts - .filter((p) => !p.value || isNaN(parseFloat(p.value))) - .reduce((s, p) => s + p.count, 0); + const rows = data.details.filter((d) => d.goal === goal); + if (rows.length === 0) return null; + const hasVariant = goal === "Membership-complete"; + const tracked = rows + .filter((r) => r.amount && !isNaN(parseFloat(r.amount))) + .sort((a, b) => parseFloat(b.amount) - parseFloat(a.amount)); + const untrackedCount = rows + .filter((r) => !r.amount || isNaN(parseFloat(r.amount))) + .reduce((s, r) => s + r.count, 0); const total = tracked.reduce( - (sum, p) => sum + parseFloat(p.value) * p.count, + (sum, r) => sum + parseFloat(r.amount) * r.count, 0 ); const colors = GOAL_COLORS[goal]; return (

@@ -368,24 +367,35 @@ export default function ConversionDashboard({ > ${total.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}

-
+
+ {hasVariant && ( + + )} - {tracked.map((a) => ( - - - + {tracked.map((r, i) => ( + + + {hasVariant && ( + + )} + ))} {untrackedCount > 0 && ( + {hasVariant && )} @@ -397,38 +407,6 @@ export default function ConversionDashboard({ })} )} - - {/* Membership variant breakdown */} - {(() => { - const variants = data.propBreakdowns.filter( - (p) => - p.goal === "Membership-complete" && - p.prop === "membership_variant" - ); - if (variants.length === 0) return null; - return ( -
-

- Membership — Calculator Variant -

-
- {variants.map((v) => ( -
- - {v.value || "(not set)"} - - - {v.count} - -
- ))} -
-
- ); - })()} )} diff --git a/src/pages/api/admin/conversion-stats.ts b/src/pages/api/admin/conversion-stats.ts index f674a6d..e5c8d4e 100644 --- a/src/pages/api/admin/conversion-stats.ts +++ b/src/pages/api/admin/conversion-stats.ts @@ -77,20 +77,20 @@ async function fetchPlausibleStats( return results.flat(); } -interface PropBreakdown { +interface ConversionDetail { goal: string; - prop: string; - value: string; + amount: string; + variant: string; count: number; } -async function fetchPlausiblePropBreakdown( +async function fetchPlausibleConversionDetails( apiKey: string, goal: string, - prop: string, + props: string[], dateFrom: string, dateTo: string -): Promise { +): Promise { const response = await fetch(PLAUSIBLE_API, { method: "POST", headers: { @@ -102,7 +102,7 @@ async function fetchPlausiblePropBreakdown( metrics: ["events"], date_range: [dateFrom, dateTo], filters: [["is", "event:goal", [goal]]], - dimensions: [`event:props:${prop}`], + dimensions: props.map((p) => `event:props:${p}`), }), }); @@ -112,20 +112,21 @@ async function fetchPlausiblePropBreakdown( return data.results.map((r) => ({ goal, - prop, - value: r.dimensions[0], + amount: r.dimensions[props.indexOf("amount")] ?? "", + variant: r.dimensions[props.indexOf("membership_variant")] ?? "", count: r.metrics[0], })); } -async function fetchPropertyBreakdowns( +async function fetchConversionDetails( apiKey: string, dateFrom: string, dateTo: string -): Promise { +): Promise { const queries = [ - ...GOALS.map((goal) => fetchPlausiblePropBreakdown(apiKey, goal, "amount", dateFrom, dateTo)), - fetchPlausiblePropBreakdown(apiKey, "Membership-complete", "membership_variant", dateFrom, dateTo), + fetchPlausibleConversionDetails(apiKey, "One-time-donate", ["amount"], dateFrom, dateTo), + fetchPlausibleConversionDetails(apiKey, "Monthly-donate", ["amount"], dateFrom, dateTo), + fetchPlausibleConversionDetails(apiKey, "Membership-complete", ["amount", "membership_variant"], dateFrom, dateTo), ]; const results = await Promise.all(queries); return results.flat(); @@ -135,7 +136,7 @@ async function fetchDroppedEvents( kv: KVNamespace, dateFrom: string, dateTo: string -): Promise<{ daily: DailyCount[]; propBreakdowns: PropBreakdown[] }> { +): Promise<{ daily: DailyCount[]; details: ConversionDetail[] }> { const allKeys: string[] = []; let cursor: string | undefined; do { @@ -182,21 +183,21 @@ async function fetchDroppedEvents( daily.push({ date, goal: goalParts.join(":"), count }); } - const propCounts = new Map(); + const detailCounts = new Map(); for (const e of filtered) { - for (const [prop, value] of Object.entries(e.props)) { - const key = `${e.eventName}:${prop}:${value}`; - propCounts.set(key, (propCounts.get(key) || 0) + 1); - } + const amount = String(e.props.amount ?? ""); + const variant = String(e.props.membership_variant ?? ""); + const key = `${e.eventName}:${amount}:${variant}`; + detailCounts.set(key, (detailCounts.get(key) || 0) + 1); } - const propBreakdowns: PropBreakdown[] = []; - for (const [key, count] of propCounts) { - const [goal, prop, ...valueParts] = key.split(":"); - propBreakdowns.push({ goal, prop, value: valueParts.join(":"), count }); + const details: ConversionDetail[] = []; + for (const [key, count] of detailCounts) { + const [goal, amount, variant] = key.split(":"); + details.push({ goal, amount, variant, count }); } - return { daily, propBreakdowns }; + return { daily, details }; } export const GET: APIRoute = async ({ request, locals }) => { @@ -217,15 +218,15 @@ export const GET: APIRoute = async ({ request, locals }) => { const ctx = locals.runtime?.ctx; try { - const [plausible, dropped, propBreakdowns] = await Promise.all([ + const [plausible, dropped, details] = await Promise.all([ apiKey ? fetchPlausibleStats(apiKey, dateFrom, dateTo) : Promise.resolve([]), kv ? fetchDroppedEvents(kv, dateFrom, dateTo) - : Promise.resolve({ daily: [], propBreakdowns: [] }), + : Promise.resolve({ daily: [], details: [] }), apiKey - ? fetchPropertyBreakdowns(apiKey, dateFrom, dateTo) + ? fetchConversionDetails(apiKey, dateFrom, dateTo) : Promise.resolve([]), ]); @@ -233,7 +234,7 @@ export const GET: APIRoute = async ({ request, locals }) => { JSON.stringify({ plausible, dropped: dropped.daily, - propBreakdowns: [...propBreakdowns, ...dropped.propBreakdowns], + details: [...details, ...dropped.details], dateFrom, dateTo, }),
AmountVariantCount
${parseFloat(a.value).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}{a.count}
+ ${parseFloat(r.amount).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })} + + {r.variant || "—"} + {r.count}
Untracked} {untrackedCount}