fix(experiments): prevent NaN propagation and ACOS zero-value bug in experiment analysis

- Change ?? to || for salesAds/units/revenue in weekly aggregation to guard against NaN
- Add || 0 fallback to ads-only records in mergeSalesAndAdsData
- Fix parseCurrency to check isNaN on all EU-format return paths
- Handle ACOS edge case: cost > 0 with adRevenue = 0 now returns 100% instead of 0%
- Add diagnostic console logging to computeDiD for data flow tracing

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Christian Vidal Wolf
2026-02-25 16:34:55 +01:00
co-authored by Claude Opus 4.6
parent de1af7e0db
commit 8b774ae7ba
3 changed files with 36 additions and 15 deletions
+24 -5
View File
@@ -83,9 +83,9 @@ function aggregateWeeklyMetrics(
}
const w = weeklyMap.get(key)!;
w.units += r.unitsTotal ?? (r as any).units ?? 0;
w.revenue += r.salesTotal ?? (r as any).sellOut ?? 0;
w.adRevenue += r.salesAds ?? 0;
w.units += r.unitsTotal || (r as any).units || 0;
w.revenue += r.salesTotal || (r as any).sellOut || 0;
w.adRevenue += r.salesAds || 0;
w.sessions += r.glanceViews || 0;
w.cost += r.cost || 0;
w.clicks += r.clicks || 0;
@@ -110,7 +110,7 @@ function getMetricValue(w: ComputedWeeklyMetrics, metric: string): number {
case 'ctr': return w.ctr;
case 'roas': return w.roas;
case 'revenue': return w.revenue;
case 'acos': return w.cost > 0 && w.adRevenue > 0 ? (w.cost / w.adRevenue) * 100 : 0;
case 'acos': return w.cost > 0 ? (w.adRevenue > 0 ? (w.cost / w.adRevenue) * 100 : 100) : 0;
default: return w.units;
}
}
@@ -173,7 +173,8 @@ function avgMetric(data: ComputedWeeklyMetrics[], metric: string, durationWeeks:
if (metric === 'acos') {
const totalAdRevenue = data.reduce((s, w) => s + w.adRevenue, 0);
const totalCost = data.reduce((s, w) => s + w.cost, 0);
return totalAdRevenue > 0 ? (totalCost / totalAdRevenue) * 100 : 0;
if (totalCost === 0) return 0; // No ad spend → ACOS is 0
return totalAdRevenue > 0 ? (totalCost / totalAdRevenue) * 100 : 100; // Cost but no ad revenue → 100% ACOS
}
// For absolute quantities (units, revenue, sessions), we sum them and divide by the duration
@@ -293,6 +294,24 @@ export function computeDiD(
const treatmentDurationWeeks = Math.max(1, Math.round((endTs - startTs) / (7 * 86400000)));
const baselineDurationWeeks = Math.max(1, Math.round((beforeEndTs - beforeStartTs) / (7 * 86400000)));
// Debug: log data flow for ACOS diagnosis
const afterTotalCost = tSplit.after.reduce((s, w) => s + w.cost, 0);
const afterTotalAdRev = tSplit.after.reduce((s, w) => s + w.adRevenue, 0);
const beforeTotalCost = tSplit.before.reduce((s, w) => s + w.cost, 0);
const beforeTotalAdRev = tSplit.before.reduce((s, w) => s + w.adRevenue, 0);
console.log(`[computeDiD DEBUG] Experiment: ${experiment.name} | Mkt: ${experiment.marketplace}`);
console.log(` Treatment ASINs: ${treatmentAsinSet.size} | Weekly buckets: ${treatmentWeekly.length}`);
console.log(` Period: ${new Date(startTs).toISOString().split('T')[0]}${new Date(endTs).toISOString().split('T')[0]}`);
console.log(` Baseline: ${new Date(beforeStartTs).toISOString().split('T')[0]}${new Date(beforeEndTs).toISOString().split('T')[0]}`);
console.log(` After split: ${tSplit.after.length} weeks [cost=${afterTotalCost.toFixed(2)}, adRev=${afterTotalAdRev.toFixed(2)}]`);
console.log(` Before split: ${tSplit.before.length} weeks [cost=${beforeTotalCost.toFixed(2)}, adRev=${beforeTotalAdRev.toFixed(2)}]`);
if (tSplit.after.length > 0) {
console.log(` After weeks: ${tSplit.after.map(w => w.week).join(', ')}`);
}
if (tSplit.before.length > 0) {
console.log(` Before weeks: ${tSplit.before.map(w => w.week).join(', ')}`);
}
const metrics: Record<string, DiDMetricResult> = {};
for (const metric of METRICS) {
metrics[metric] = computeMetricDiD(