feat: complete experiments tab analytics optimization with modular kpis and sparklines

This commit is contained in:
Christian Vidal Wolf
2026-02-21 15:31:02 +01:00
parent 67e93f6653
commit d78a88afe1
5 changed files with 362 additions and 53 deletions
+49 -19
View File
@@ -6,7 +6,7 @@ import {
ExperimentType,
ExperimentStatus,
} from '../types';
import { SalesRecord } from '../types';
import { CombinedKPIs } from '../types';
const API_BASE = '/api/experiments';
@@ -109,7 +109,7 @@ export const listExperiments = async (
const today = new Date();
const startDate = new Date(exp.start_date);
const endDate = exp.end_date ? new Date(exp.end_date) : null;
let progressPercent = 0;
if (endDate) {
const totalDays = endDate.getTime() - startDate.getTime();
@@ -123,10 +123,12 @@ export const listExperiments = async (
type: exp.type,
status: exp.status,
asin_count: exp.asins?.length || 0,
asins: exp.asins || [],
marketplace: exp.marketplace,
start_date: exp.start_date,
end_date: exp.end_date,
progress_percent: Math.round(progressPercent),
primary_metric: exp.primary_metric,
actual_lift_percent: exp.actual_lift_percent,
owner: exp.owner,
};
@@ -138,7 +140,7 @@ export const getActiveExperimentsForASINs = async (
): Promise<Map<string, ActiveExperiment[]>> => {
const params = new URLSearchParams();
asins.forEach(asin => params.append('asin', asin));
const response = await fetch(`${API_BASE}?${params.toString()}&status=active`);
if (!response.ok) {
@@ -148,10 +150,10 @@ export const getActiveExperimentsForASINs = async (
const data = await response.json();
const today = new Date().toISOString().split('T')[0];
// Filter experiments that are currently active
const activeExperiments = data.filter((exp: any) =>
exp.status === 'active' &&
const activeExperiments = data.filter((exp: any) =>
exp.status === 'active' &&
exp.start_date <= today &&
(!exp.end_date || exp.end_date >= today)
);
@@ -191,17 +193,23 @@ export const getActiveExperimentsForASINs = async (
export const calculateExperimentPerformance = async (
experiment: Experiment,
salesData: SalesRecord[]
salesData: CombinedKPIs[]
): Promise<{
baseline_units: number;
baseline_revenue: number;
baseline_gv: number;
baseline_cvr: number;
baseline_acos: number;
experiment_units: number;
experiment_revenue: number;
experiment_gv: number;
experiment_cvr: number;
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();
// Calculate baseline period (same duration before experiment)
const durationMs = endDate.getTime() - startDate.getTime();
const baselineStart = new Date(startDate.getTime() - durationMs);
@@ -209,40 +217,62 @@ export const calculateExperimentPerformance = async (
// Filter sales data for experiment ASINs
const asinSet = new Set(experiment.asins.map(a => a.toUpperCase()));
const baselineData = salesData.filter(r => {
const recordDate = new Date(`${r.year}-${getMonthNumber(r.month)}-01`);
const recordDate = new Date(r.year, 0, 1 + (r.week - 1) * 7);
return asinSet.has(r.asin.toUpperCase()) &&
recordDate >= baselineStart && recordDate < baselineEnd;
});
const experimentData = salesData.filter(r => {
const recordDate = new Date(`${r.year}-${getMonthNumber(r.month)}-01`);
const recordDate = new Date(r.year, 0, 1 + (r.week - 1) * 7);
return asinSet.has(r.asin.toUpperCase()) &&
recordDate >= startDate && recordDate <= endDate;
});
const baseline_units = baselineData.reduce((sum, r) => sum + r.units, 0);
const baseline_revenue = baselineData.reduce((sum, r) => sum + r.sellOut, 0);
const experiment_units = experimentData.reduce((sum, r) => sum + r.units, 0);
const experiment_revenue = experimentData.reduce((sum, r) => sum + r.sellOut, 0);
const baseline_units = baselineData.reduce((sum, r) => sum + r.unitsTotal, 0);
const baseline_revenue = baselineData.reduce((sum, r) => sum + r.salesTotal, 0);
const baseline_gv = baselineData.reduce((sum, r) => sum + (r.glanceViews || 0), 0);
const baseline_cvr = baseline_gv > 0 ? (baseline_units / baseline_gv) * 100 : 0;
const baseline_spend = baselineData.reduce((sum, r) => sum + (r.cost || 0), 0);
const baseline_acos = baseline_revenue > 0 ? (baseline_spend / baseline_revenue) * 100 : 0;
const actual_lift_percent = baseline_units > 0
? ((experiment_units - baseline_units) / baseline_units) * 100
: 0;
const experiment_units = experimentData.reduce((sum, r) => sum + r.unitsTotal, 0);
const experiment_revenue = experimentData.reduce((sum, r) => sum + r.salesTotal, 0);
const experiment_gv = experimentData.reduce((sum, r) => sum + (r.glanceViews || 0), 0);
const experiment_cvr = experiment_gv > 0 ? (experiment_units / experiment_gv) * 100 : 0;
const experiment_spend = experimentData.reduce((sum, r) => sum + (r.cost || 0), 0);
const experiment_acos = experiment_revenue > 0 ? (experiment_spend / experiment_revenue) * 100 : 0;
let actual_lift_percent = 0;
if (experiment.primary_metric === 'cvr' && baseline_cvr > 0) {
actual_lift_percent = ((experiment_cvr - baseline_cvr) / baseline_cvr) * 100;
} else if (experiment.primary_metric === 'revenue' && baseline_revenue > 0) {
actual_lift_percent = ((experiment_revenue - baseline_revenue) / baseline_revenue) * 100;
} else if (experiment.primary_metric === 'acos' && baseline_acos > 0) {
actual_lift_percent = ((baseline_acos - experiment_acos) / Math.max(0.1, baseline_acos)) * 100; // inverted, lower is better
} else if (baseline_units > 0) {
actual_lift_percent = ((experiment_units - baseline_units) / baseline_units) * 100;
}
return {
baseline_units,
baseline_revenue,
baseline_gv,
baseline_cvr: Math.round(baseline_cvr * 10) / 10,
baseline_acos: Math.round(baseline_acos * 10) / 10,
experiment_units,
experiment_revenue,
experiment_gv,
experiment_cvr: Math.round(experiment_cvr * 10) / 10,
experiment_acos: Math.round(experiment_acos * 10) / 10,
actual_lift_percent: Math.round(actual_lift_percent * 10) / 10,
};
};
export const updateExperimentResults = async (
experimentId: string,
salesData: SalesRecord[]
salesData: CombinedKPIs[]
): Promise<Experiment> => {
const experiment = await getExperiment(experimentId);
if (!experiment) throw new Error('Experiment not found');