Skip to content

llms.txt — rasuvaeff/yii3-ab-testing-outbox ​

The compact reference the package ships for AI coding assistants. Raw file: llms.txt.

markdown
# rasuvaeff/yii3-ab-testing-outbox

Outbox producer for Yii3 A/B testing events. Records exposures/conversions as
durable `yii3-outbox` messages; a worker exports them later.

## Install

```bash
composer require rasuvaeff/yii3-ab-testing-outbox
```

## Development

```bash
make build
make test
make test-coverage
make mutation
```

## Rules

- Payload is core's canonical schema v2 row, produced by `CanonicalEventSerializer`
  — never assembled here. Fields: v(=2), event_id, occurred_at, experiment,
  variant, subject_id, decision_reason, assignment_source, experiment_revision,
  environment, dimensions; conversions add goal + exposure_event_id.
- `dimensions` is a JSON STRING, not a nested object: the exporter rejects
  non-scalar payload fields.
- The context allow-list and the clock live in the CORE now, configured on the
  AbTesting facade so every delivery path filters identically.
- Aggregate ids are pseudonymous `exposure:<digest>` / `conversion:<digest>`.
  `PseudonymousAggregateIdStrategy` requires a non-empty secret and throws
  `InvalidArgumentException` for blank input; replace `AggregateIdStrategyInterface`
  for a custom grouping policy.
- Trackers take core events (ExposureEvent/ConversionEvent), not an Assignment.
  There is no `eventId` argument: the core minted it, and the tracker writes it
  into the payload AND passes it to `Outbox::record(id: …)`. They must stay
  equal — the exporter may fill the event-id column from either, and a mismatch
  splits one event into two rows that never deduplicate.
- Consumers branch on payload `v`. A v1 message has no decision_reason and no
  dimensions, so routing it into the v2 tables fails on a missing field — keep
  `AbTestingClickHouseRoutes::legacyV1Map()` wired until the queue drains.
- One-source rule: binds `ExposureTracker` + `ConversionTracker`; do not install
  next to another tracker backend without an app-side `Composite*Tracker`.
- v2 routes target `ab_exposures_v2`/`ab_conversions_v2`, owned by
  `yii3-ab-testing-clickhouse` (`AnalyticsSchemaV2`). Never restate table names
  or column order here — an INSERT is positional.
- `migrations/` contains the outbox ClickHouse v1 schema, kept only for draining. Defaults
  are `ab_outbox_exposures` / `ab_outbox_conversions`, not the incompatible
  direct-sink tables; DDL placeholders are `outbox_exposures_table` /
  `outbox_conversions_table`.
- The sink tables use `ReplacingMergeTree ORDER BY event_id` for retry dedup and
  store payload `event_at` separately from server-defaulted `ingested_at`.

## API

```php
use Rasuvaeff\Yii3AbTestingOutbox\OutboxExposureTracker;
use Rasuvaeff\Yii3AbTestingOutbox\OutboxConversionTracker;
use Rasuvaeff\Yii3AbTestingOutbox\AbTestingClickHouseRoutes;
use Rasuvaeff\Yii3AbTestingOutbox\DefaultAbTestingOutboxMessageFactory;
use Rasuvaeff\Yii3AbTestingOutbox\PseudonymousAggregateIdStrategy;
use Rasuvaeff\Yii3Outbox\Outbox;

$outbox = new Outbox(storage: $storage, clock: $clock);
$factory = new DefaultAbTestingOutboxMessageFactory(
    aggregateIdStrategy: new PseudonymousAggregateIdStrategy(secret: $appSecret),
);
$exposure = new OutboxExposureTracker($outbox, $factory);
$conversion = new OutboxConversionTracker($outbox, $factory);

// The facade mints the event and calls the tracker.
$event = $ab->trackExposure($assignment);
$ab->trackConversion($assignment, goal: 'purchase', exposure: $event);

// route map for yii3-outbox-clickhouse — schema v2 tables of yii3-ab-testing-clickhouse:
$routes = AbTestingClickHouseRoutes::map();
// while v1 messages queued before the 2.0 upgrade still drain:
$legacy = AbTestingClickHouseRoutes::legacyV1Map();  // ab_outbox_exposures / ab_outbox_conversions
```

Create the v2 tables with `(new SchemaMigrations($client))->apply()` from
`yii3-ab-testing-clickhouse` (a `suggest` of this package). `migrations/` here
holds only the v1 drain tables. The clean-install pipeline is:
`tracker -> yii3-outbox-db -> yii3-outbox-clickhouse (outbox:clickhouse:export) -> ab_exposures_v2 / ab_conversions_v2`.

Custom serialization: implement `AbTestingOutboxMessageFactoryInterface`
(default `DefaultAbTestingOutboxMessageFactory`) and pass it to the trackers.
The factory returns an `AbTestingOutboxPayload` (type, JSON payload, aggregate
id) that the tracker hands to `Outbox::record()`. Message types are fixed by
`AbTestingOutboxEventType`: `ab.exposure`, `ab.conversion`.

## Exposure payload (v2)

```json
{"v":2,"event_id":"0198f2c1-4d3a-7c9e-8b21-6f4a2d9e0c17","occurred_at":"2026-08-01 10:00:00.123","experiment":"checkout","variant":"green","subject_id":"user-1","decision_reason":"assigned","assignment_source":"computed","experiment_revision":"db:7","environment":"production","dimensions":"{\"country\":\"RU\"}"}
```

Conversion adds `goal` and `exposure_event_id`. `v` is transport-meta only — not exported to ClickHouse.

The config-plugin requires the application param
`rasuvaeff/yii3-ab-testing-outbox.aggregateIdSecret` to be a non-empty string;
missing or blank values throw `InvalidArgumentException` while building DI.