Files
2026-05-27 23:29:53 +01:00

242 lines
6.9 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# OpenSpec Change: enterprise-ab-testing
- **Change ID**: enterprise-ab-testing
- **Package**: `@suaveplan/ab-testing`
- **Category**: universal/workflow
- **Target path**: `universal/workflow/ab-testing`
- **Status**: PROPOSED
- **Date**: 2026-04-24
- **Reference spec**: `/mnt/s/web/suaveplan/enterprise/ab-testing.md`
---
## Summary
Deliver `@suaveplan/ab-testing` on the genesis branch from scratch at enterprise quality. The package provides an A/B testing engine with deterministic assignment, audience targeting, experiment lifecycle management, metric tracking, and results analysis. Genesis adds structured errors via `@suaveplan/error`, Zod schemas for all experiment types, OTel instrumentation on assignment and conversion events, injectable `Clock` and `Rng` for deterministic bucketing, integration with `@suaveplan/feature-flags` as an optional provider, event-bus emission for assignment/conversion events, and property-based tests for bucketing distribution invariants.
---
## Current-state audit (genesis)
The package does not exist on genesis yet — this is a fresh build.
---
## Available workspace deps on genesis
| Package | Role |
|---|---|
| `@suaveplan/error` (`workspace:^`) | Base class for `AbTestingError`; stable error codes |
| `@suaveplan/types` (`workspace:^`) | `LoggerLike`, `Clock`, `Rng`, `EventBusLike` |
| `@suaveplan/events` (`workspace:^`) | Event bus for assignment and conversion events |
| `@suaveplan/logger` (`workspace:^`) | Structured logging with OTel trace correlation |
| `@suaveplan/feature-flags` (`workspace:^`) | Optional feature-flag provider for experiment gating |
| `zod` (`catalog:forms`) | Zod schemas for all public types |
| `@opentelemetry/api` (`catalog:observability`) | Spans + counters on assignment/conversion |
| `tslib` (`catalog:build`) | Runtime helpers |
Dev deps: `vitest`, `@vitest/coverage-istanbul`, `fast-check`, `mitata` (all `catalog:testing`).
---
## Public API surface
### Core
```ts
function createExperimentManager(options?: ExperimentManagerOptions): ExperimentManager
interface ExperimentManager {
defineExperiment(experiment: ExperimentDefinition): void
getExperiment(id: string): ExperimentDefinition | null
listExperiments(): ReadonlyArray<ExperimentDefinition>
assign(experimentId: string, userId: string, attributes?: UserAttributes): Assignment
track(experimentId: string, userId: string, metric: MetricEvent): void
getResults(experimentId: string): ExperimentResults
use(provider: ExperimentProvider): this
}
```
### Experiment definition
```ts
interface ExperimentDefinition {
id: string
name: string
status: 'draft' | 'running' | 'paused' | 'concluded'
variants: ReadonlyArray<Variant>
targeting?: TargetingRule[]
startDate?: string
endDate?: string
sampleRate?: number // 0–1, default 1.0
primaryMetric?: string
secondaryMetrics?: string[]
}
interface Variant {
id: string
name: string
weight: number // relative weight, will be normalised
isControl?: boolean
}
interface TargetingRule {
attribute: string
operator: 'eq' | 'neq' | 'in' | 'notIn' | 'gt' | 'lt' | 'gte' | 'lte'
value: unknown
}
```
### Assignment
```ts
interface Assignment {
readonly experimentId: string
readonly userId: string
readonly variantId: string
readonly isControl: boolean
readonly timestamp: number
readonly inExperiment: boolean // false if excluded by targeting or sample rate
}
```
### Metrics
```ts
interface MetricEvent {
name: string
value?: number
timestamp?: number
properties?: Record<string, unknown>
}
interface ExperimentResults {
readonly experimentId: string
readonly variants: ReadonlyArray<VariantResults>
readonly totalAssignments: number
readonly totalConversions: number
}
interface VariantResults {
readonly variantId: string
readonly assignments: number
readonly conversions: number
readonly conversionRate: number
readonly metrics: ReadonlyArray<{ name: string; count: number; sum: number; avg: number }>
}
```
### Provider interface
```ts
interface ExperimentProvider {
readonly name: string
getAssignment?(experimentId: string, userId: string): string | null
trackAssignment?(assignment: Assignment): void
trackConversion?(experimentId: string, userId: string, metric: MetricEvent): void
}
type UserAttributes = Record<string, string | number | boolean>
interface ExperimentManagerOptions {
logger?: LoggerLike
clock?: Clock
rng?: Rng
eventBus?: EventBusLike
hashSeed?: number
}
```
### Bucketing utilities
```ts
function bucketUser(userId: string, experimentId: string, seed?: number): number // 0–1
function normaliseWeights(variants: Variant[]): Array<Variant & { normalizedWeight: number }>
function selectVariant(variants: Variant[], bucket: number): Variant
```
### Error codes
```ts
type AbTestingErrorCode =
| 'AB_EXPERIMENT_NOT_FOUND'
| 'AB_EXPERIMENT_INACTIVE'
| 'AB_INVALID_DEFINITION'
| 'AB_VARIANT_NOT_FOUND'
| 'AB_METRIC_INVALID'
function registerAbTestingErrorCodes(): void
```
---
## Genesis additions
1. **Structured `AbTestingError`** extending `SuaveplanError` with stable `AB.*` codes
2. **Zod schemas** for `ExperimentDefinition`, `Variant`, `TargetingRule`, `MetricEvent`
3. **Deterministic FNV-1a bucketing** — injectable `Rng` for testing; FNV-1a hash by default for consistency with `@suaveplan/feature-flags`
4. **Targeting rule evaluation** — 8 operators, evaluated before assignment
5. **Provider plugin interface** — `use()` for external integrations
6. **`AbortSignal`** available on async provider calls
7. **OTel instrumentation** — span `ab.assign`, counter `ab.assignments_total`, counter `ab.conversions_total`, attributes include `experiment_id`, `variant_id`
8. **Property-based tests** — bucketing distribution, targeting rule evaluation, weight normalisation
---
## File layout
```text
universal/workflow/ab-testing/
package.json
tsconfig.json
vitest.config.ts
src/
index.ts
error-codes.ts
constants/
index.ts
types/
common.ts
common.md
core/
experiment-manager/
experiment-manager.ts
experiment-manager.md
experiment-manager.types.ts
experiment-manager.test.ts
index.ts
bucketing/
bucketing.ts
bucketing.md
bucketing.test.ts
index.ts
targeting/
targeting.ts
targeting.md
targeting.test.ts
index.ts
results/
results.ts
results.md
results.test.ts
index.ts
providers/
provider.ts
provider.md
provider.test.ts
index.ts
__benchmarks__/
ab-testing.bench.ts
tests/
integration.test.ts
property.test.ts
```
---
## Impact
**Downstream packages**: Any application layer that needs experiment assignment. No existing genesis package currently depends on it.
**Workspace dep prerequisites**: `@suaveplan/error`, `@suaveplan/types`, `@suaveplan/events`, `@suaveplan/logger`, `@suaveplan/feature-flags` must be published.