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.