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 })}
-
+
| Amount |
+ {hasVariant && (
+ Variant |
+ )}
Count |
- {tracked.map((a) => (
-
- | ${parseFloat(a.value).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })} |
- {a.count} |
+ {tracked.map((r, i) => (
+
+ |
+ ${parseFloat(r.amount).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
+ |
+ {hasVariant && (
+
+ {r.variant || "—"}
+ |
+ )}
+ {r.count} |
))}
{untrackedCount > 0 && (
| Untracked |
+ {hasVariant && | }
{untrackedCount} |
)}
@@ -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,
}),