From 5dfcdd71c0d1e98dbbdbb48cb08f23af56391553 Mon Sep 17 00:00:00 2001 From: Christian Vidal Wolf Date: Wed, 25 Feb 2026 16:54:56 +0100 Subject: [PATCH] fix(parser): prevent regex collision where /sale/i matched ACOS column instead of attributed sales The generic regex /sale/i in ads column detection was matching "Advertising Cost of Sales" (the ACOS % column) before reaching "7 day total sales" (the actual attributed sales amount). This caused attributedSales30d to receive the ACOS percentage instead of the real sales value, making experiment ACOS = 0 or 100%. - Replace /sale/i with specific regexes: /\d+\s*day.*sale/i, /total\s+sale/i, etc. - Replace /cost/i with regexes that exclude "cost of sales" columns - Replace /unit/i with specific regexes for unit columns - Add diagnostic logging for ads Excel column headers and first-row values - Revert ACOS 100% fallback (root cause now fixed) Co-Authored-By: Claude Opus 4.6 --- services/dataProcessor.ts | 22 ++++++++++++++++------ services/experimentAnalysis.ts | 5 ++--- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/services/dataProcessor.ts b/services/dataProcessor.ts index f315af1..575f108 100644 --- a/services/dataProcessor.ts +++ b/services/dataProcessor.ts @@ -448,15 +448,15 @@ export const processAdsCSV = (file: File): Promise => { const countryRaw = getColumnValue(row, ['country', 'marketplace', 'portfolio', 'customer', 'kunde']); const weekRaw = getColumnValue(row, ['week', 'woche', 'semana']); const asin = getColumnValue(row, ['asin']); - const costRaw = getColumnValue(row, ['cost', 'spend', 'ausgaben', 'gasto', 'coste', /ad\s*spend/i, /cost/i]); + const costRaw = getColumnValue(row, ['cost', 'spend', 'ausgaben', 'gasto', 'coste', /ad\s*spend/i, /^cost$/i, /^spend$/i, /(? 0) { + console.log(`[Ads Excel] Sheet "${sheetName}" columns: ${Object.keys(jsonData[0]).join(' | ')}`); + } + // No need to skip index 0 since headers are mapped as keys automatically for (let i = 0; i < jsonData.length; i++) { const row = jsonData[i]; @@ -522,15 +527,15 @@ export const processAdsExcel = async (fileOrBuffer: File | ArrayBuffer): Promise const countryRaw = getColumnValue(row, ['country', 'marketplace', 'portfolio', 'customer', 'kunde']); const weekRaw = getColumnValue(row, ['week', 'woche', 'semana']); const asin = getColumnValue(row, ['asin']); - const costRaw = getColumnValue(row, ['cost', 'spend', 'ausgaben', 'gasto', 'coste', /ad\s*spend/i, /cost/i]); + const costRaw = getColumnValue(row, ['cost', 'spend', 'ausgaben', 'gasto', 'coste', /ad\s*spend/i, /^cost$/i, /^spend$/i, /(? 53) continue; + // Log first parsed row per sheet for column match diagnosis + if (allData.length === 0 || (allData.length > 0 && allData[allData.length - 1]?.year !== year)) { + console.log(`[Ads Excel] First row sheet ${year}: costRaw="${costRaw}" salesRaw="${salesRaw}" unitsRaw="${unitsRaw}" acosRaw="${acosRaw}"`); + } + allData.push({ country: mapCountryToMarketplace(String(countryRaw)), year, diff --git a/services/experimentAnalysis.ts b/services/experimentAnalysis.ts index 4c1d673..86a53a9 100644 --- a/services/experimentAnalysis.ts +++ b/services/experimentAnalysis.ts @@ -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 : 100) : 0; + case 'acos': return w.cost > 0 && w.adRevenue > 0 ? (w.cost / w.adRevenue) * 100 : 0; default: return w.units; } } @@ -173,8 +173,7 @@ 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); - 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 + return totalAdRevenue > 0 && totalCost > 0 ? (totalCost / totalAdRevenue) * 100 : 0; } // For absolute quantities (units, revenue, sessions), we sum them and divide by the duration