fix: resolve UTC date matching and implement per-product ASIN breakdown table for Line experiments

This commit is contained in:
Christian Vidal Wolf
2026-02-21 19:37:07 +01:00
parent 8bd78c2e7f
commit aa9a7845b7
3 changed files with 181 additions and 13 deletions
+99 -5
View File
@@ -64,10 +64,57 @@ const ExperimentDetail: React.FC<ExperimentDetailProps> = ({ experimentId, onClo
}
};
const targetAsins = useMemo(() => {
if (!experiment) return [];
return Array.from(getExperimentAsins(experiment.asins, salesData));
}, [experiment, salesData]);
const productBreakdown = useMemo(() => {
if (!experiment || targetAsins.length <= 1 || !salesData.length) return null;
const parseDate = (d: string) => {
const [y, m, da] = d.split('T')[0].split('-');
return new Date(Number(y), Number(m) - 1, Number(da));
};
const startDate = parseDate(experiment.start_date);
const endDate = experiment.end_date ? parseDate(experiment.end_date) : new Date();
const asinMap = new Map<string, { units: number; revenue: number; gv: number; spend: number }>();
targetAsins.forEach(a => asinMap.set(a, { units: 0, revenue: 0, gv: 0, spend: 0 }));
salesData.forEach(r => {
const asin = (r.asin || '').toUpperCase();
if (!asinMap.has(asin)) return;
const recordDate = new Date(r.year, 0, 1 + ((r.week || 1) - 1) * 7);
if (recordDate >= startDate && recordDate <= endDate) {
const d = asinMap.get(asin)!;
d.units += r.unitsTotal || 0;
d.revenue += r.salesTotal || 0;
d.gv += r.glanceViews || 0;
d.spend += r.cost || 0;
}
});
return Array.from(asinMap.entries())
.map(([asin, metrics]) => ({
asin,
...metrics,
cvr: metrics.gv > 0 ? (metrics.units / metrics.gv) * 100 : 0,
acos: metrics.revenue > 0 ? (metrics.spend / metrics.revenue) * 100 : 0
}))
.filter(m => m.units > 0 || m.gv > 0 || m.spend > 0)
.sort((a, b) => b[experiment.primary_metric === 'bsr' ? 'units' : experiment.primary_metric as 'units' | 'revenue' | 'gv' | 'cvr' | 'acos' || 'units'] - a[experiment.primary_metric === 'bsr' ? 'units' : experiment.primary_metric as 'units' | 'revenue' | 'gv' | 'cvr' | 'acos' || 'units']);
}, [experiment, salesData, targetAsins]);
const targetAsinsOriginal = experiment?.asins || [];
const chartData = useMemo(() => {
if (!experiment || !salesData.length) return [];
const asinSet = getExperimentAsins(experiment.asins, salesData);
const asinSet = new Set(targetAsins);
const weeklyMap = new Map<string, any>();
salesData.forEach(r => {
@@ -488,19 +535,66 @@ const ExperimentDetail: React.FC<ExperimentDetailProps> = ({ experimentId, onClo
</div>
{/* ASINs */}
<DetailSection title="📦 Target ASINs">
<DetailSection title={`📦 Target ASINs (${targetAsins.length})`}>
<div className="flex flex-col gap-2">
<div className="flex flex-wrap gap-2">
{(experiment.asins || []).map((asin, idx) => (
{targetAsinsOriginal.map((asin, idx) => (
<span
key={idx}
className="px-2.5 py-1 bg-slate-800 border border-slate-600 rounded-lg text-sm text-slate-300 font-mono"
key={`orig-${idx}`}
className="px-2.5 py-1 bg-indigo-900/40 border border-indigo-500/30 rounded-lg text-sm text-indigo-300 font-bold"
>
{asin}
</span>
))}
{targetAsins.map((asin, idx) => {
// Don't show again if it's the exact same as an explicit ASIN,
// but show if it was resolved from a LINE: prefix
if (targetAsinsOriginal.includes(asin)) return null;
return (
<span
key={`resolved-${idx}`}
className="px-2.5 py-1 bg-slate-800 border border-slate-600 rounded-lg text-sm text-slate-300 font-mono"
>
{asin}
</span>
);
})}
</div>
</div>
</DetailSection>
{/* Product Breakdown (Only shown if multiple ASINs, i.e. product line) */}
{productBreakdown && productBreakdown.length > 0 && (
<DetailSection title="📊 Product Performance Breakdown (During Experiment)">
<div className="overflow-x-auto rounded-xl border border-slate-700">
<table className="w-full text-left text-sm whitespace-nowrap">
<thead className="bg-slate-800/50 text-slate-400">
<tr>
<th className="px-4 py-3 font-medium">ASIN</th>
<th className="px-4 py-3 font-medium text-right">Units</th>
<th className="px-4 py-3 font-medium text-right">Revenue</th>
<th className="px-4 py-3 font-medium text-right">Glance Views</th>
<th className="px-4 py-3 font-medium text-right">CVR</th>
<th className="px-4 py-3 font-medium text-right">ACOS</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-700/50">
{productBreakdown.map(p => (
<tr key={p.asin} className="hover:bg-slate-800/30 transition-colors">
<td className="px-4 py-3 font-mono text-slate-300">{p.asin}</td>
<td className="px-4 py-3 text-right text-white font-medium">{p.units.toLocaleString()}</td>
<td className="px-4 py-3 text-right text-emerald-400">{p.revenue.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</td>
<td className="px-4 py-3 text-right text-slate-300">{p.gv.toLocaleString()}</td>
<td className="px-4 py-3 text-right text-amber-400">{p.cvr.toFixed(1)}%</td>
<td className="px-4 py-3 text-right text-rose-400">{p.acos.toFixed(1)}%</td>
</tr>
))}
</tbody>
</table>
</div>
</DetailSection>
)}
{/* Learnings */}
<DetailSection title="💡 Learnings & Conclusions">
{editing ? (
+7 -2
View File
@@ -232,8 +232,13 @@ export const calculateExperimentPerformance = async (
experiment_acos: number;
actual_lift_percent: number;
}> => {
const startDate = new Date(experiment.start_date);
const endDate = experiment.end_date ? new Date(experiment.end_date) : new Date();
const parseLocalDate = (dateStr: string) => {
const [y, m, d] = dateStr.split('T')[0].split('-');
return new Date(Number(y), Number(m) - 1, Number(d));
};
const startDate = parseLocalDate(experiment.start_date);
const endDate = experiment.end_date ? parseLocalDate(experiment.end_date) : new Date();
// Calculate baseline period (same duration before experiment)
const durationMs = endDate.getTime() - startDate.getTime();
+69
View File
@@ -0,0 +1,69 @@
import fs from 'fs';
import path from 'path';
// read dummy data or mock
const mockSalesData = [
{
asin: 'B08F2J8S1Y',
line: 'LEGENDS',
year: 2026,
week: 1,
unitsTotal: 10,
salesTotal: 100,
},
{
asin: 'B08F2J8S1Y',
line: 'LEGENDS',
year: 2026,
week: 2,
unitsTotal: 15,
salesTotal: 150,
}
];
const experimentAsins = ['LINE:LEGENDS'];
const lines = new Set();
const explicitAsins = new Set();
experimentAsins.forEach(a => {
const val = (a || '').trim().toUpperCase();
if (val.startsWith('LINE:')) {
lines.add(val.substring(5).trim());
} else if (val) {
explicitAsins.add(val);
}
});
const asinSet = new Set(explicitAsins);
if (lines.size > 0 && mockSalesData) {
mockSalesData.forEach(r => {
const line = (r.line || '').trim().toUpperCase();
if (line && lines.has(line)) {
if (r.asin) asinSet.add(r.asin.toUpperCase());
}
});
}
console.log('Resolved ASINs:', Array.from(asinSet));
// Date test
const startDateStr = '2026-01-01';
const endDateStr = '2026-01-14';
const startDate = new Date(startDateStr);
const endDate = new Date(endDateStr);
console.log('Start Date UTC:', startDate.toISOString(), 'Local:', startDate.toString());
console.log('End Date UTC:', endDate.toISOString(), 'Local:', endDate.toString());
const recordDate1 = new Date(2026, 0, 1); // week 1
const recordDate2 = new Date(2026, 0, 8); // week 2
const recordDate3 = new Date(2026, 0, 15); // week 3
console.log('Week 1 Record Date Local:', recordDate1.toString());
console.log('Week 1 included in experiment?', recordDate1 >= startDate && recordDate1 <= endDate);
console.log('Week 2 Record Date Local:', recordDate2.toString());
console.log('Week 2 included in experiment?', recordDate2 >= startDate && recordDate2 <= endDate);
console.log('Week 3 Record Date Local:', recordDate3.toString());
console.log('Week 3 included in experiment?', recordDate3 >= startDate && recordDate3 <= endDate);