4.9 KiB
results.ts — Experiment Results Computation
(no submodules)
Purpose
Provides the computeResults function, which aggregates raw assignment and
conversion data into structured per-variant statistics. This module is a pure
function with no side effects, making it straightforward to test exhaustively
and to call from any context — the manager uses it on demand, but it can also
be called directly from analytics pipelines or reporting jobs.
The function produces assignment counts, conversion counts, conversion rates, and per-metric aggregates (count, sum, average) for every variant that received at least one in-experiment assignment.
Features
- Groups assignments by
variantId, excluding out-of-experiment users - Counts unique conversions per user (one conversion per user, regardless of how many metric events they fire)
- Aggregates named metric events into
count,sum, andavgper metric per variant - Computes
conversionRate = conversions / assignments(0 when no assignments) - Returns
totalAssignmentsandtotalConversionsrolled up across all variants for dashboard summary views - Fully deterministic — given the same inputs, produces byte-identical output
Usage: Basic Example
import { computeResults } from '@suaveplan/ab-testing/results';
import type { Assignment, MetricEvent } from '@suaveplan/ab-testing';
const assignments: Assignment[] = [
{ experimentId: 'exp-1', userId: 'u1', variantId: 'ctrl', isControl: true, timestamp: 1, inExperiment: true },
{ experimentId: 'exp-1', userId: 'u2', variantId: 'treat', isControl: false, timestamp: 2, inExperiment: true },
];
const conversions = new Map<string, MetricEvent[]>([
['u1', [{ name: 'checkout_started', value: 1 }]],
]);
const results = computeResults('exp-1', assignments, conversions);
console.log(results.totalAssignments); // 2
console.log(results.totalConversions); // 1
console.log(results.variants[0]?.conversionRate); // 1.0 (ctrl: 1/1)
console.log(results.variants[1]?.conversionRate); // 0.0 (treat: 0/1)
Usage: Advanced Example
import { computeResults } from '@suaveplan/ab-testing/results';
import type { Assignment, MetricEvent } from '@suaveplan/ab-testing';
// Multiple metric events per user (only first triggers conversion flag)
const assignments: Assignment[] = [
{ experimentId: 'exp-2', userId: 'u1', variantId: 'v1', isControl: true, timestamp: 1, inExperiment: true },
{ experimentId: 'exp-2', userId: 'u2', variantId: 'v1', isControl: true, timestamp: 2, inExperiment: true },
{ experimentId: 'exp-2', userId: 'u3', variantId: 'v2', isControl: false, timestamp: 3, inExperiment: true },
// excluded user — not counted
{ experimentId: 'exp-2', userId: 'u4', variantId: '', isControl: false, timestamp: 4, inExperiment: false },
];
const conversions = new Map<string, MetricEvent[]>([
['u1', [
{ name: 'revenue', value: 49.99 },
{ name: 'revenue', value: 19.99 },
]],
['u3', [{ name: 'revenue', value: 99.00 }]],
]);
const results = computeResults('exp-2', assignments, conversions);
// v1: 2 assignments, 1 conversion (u1), revenue avg = (49.99 + 19.99) / 2
// v2: 1 assignment, 1 conversion (u3), revenue avg = 99.00
API Reference
computeResults(experimentId, assignments, conversions): ExperimentResults
experimentId—string— stable experiment identifier copied to resultassignments—Assignment[]— all assignments for the experimentconversions—Map<string, MetricEvent[]>— keyed byuserId- Returns —
ExperimentResultswithvariants,totalAssignments,totalConversions
ExperimentResults
| Field | Type | Description |
|---|---|---|
experimentId |
string |
Echoed from input |
variants |
VariantResults[] |
Per-variant aggregates |
totalAssignments |
number |
Sum across all variants |
totalConversions |
number |
Sum across all variants |
VariantResults
| Field | Type | Description |
|---|---|---|
variantId |
string |
Variant identifier |
assignments |
number |
In-experiment assignment count |
conversions |
number |
Unique-user conversion count |
conversionRate |
number |
conversions / assignments |
metrics |
Array<{name, count, sum, avg}> |
Per-metric aggregates |
Implementation Notes
Conversion counting follows the convention that a user can convert at most once per experiment — the first metric event for each user triggers the conversion flag, but all subsequent events for that user are still accumulated into the metric aggregates. This avoids double-counting in funnel analyses while preserving the full metric history for revenue and value calculations.
Out-of-experiment assignments (inExperiment: false) are excluded entirely
from all counts. They are recorded so that the manager can return a stable
assignment for a user across multiple calls, but they should not pollute the
statistical results.