llms.txt — rasuvaeff/yii3-ab-testing-clickhouse
The compact reference the package ships for AI coding assistants. Raw file: llms.txt.
markdown
# yii3-ab-testing-clickhouse
ClickHouse analytics schema for Yii3 A/B testing. Owns the tables and (from
2.1) the reporting queries. Namespace `Rasuvaeff\Yii3AbTestingClickHouse\`.
## Install
```bash
composer require rasuvaeff/yii3-ab-testing-clickhouse
```
## Rules
- **This package does NOT write events.** The direct writers were removed in
2.0. Events arrive through `yii3-ab-testing-outbox` + a worker, or through a
log-shipping collector reading core's `Logger*` sinks.
- Never insert from the request path. Under PHP-FPM that is one synchronous
insert of a few rows per request, inside the visitor's latency.
- `config/di.php` binds nothing, so this package cannot collide with a tracker
backend on the `ExposureTracker`/`ConversionTracker` keys.
- Column ORDER is part of the contract: an INSERT lists columns positionally, so
a producer that reorders them writes a variant into the subject column with no
error.
- `ingested_at` must NOT be supplied by a producer — the table fills it with
`DEFAULT now()`. It records arrival; partitioning and dedup depend on
occurrence.
- Read with `FINAL` (or `GROUP BY event_id` + `argMax`). ReplacingMergeTree
collapses on merge only, and delivery is at-least-once, so a plain count
over-counts after any retry.
- Analysis population is `decision_reason = 'assigned'`; forced and both
fallback kinds are excluded.
- v1 tables (`ab_exposures`, `ab_conversions`) are still created and are NOT
migrated into v2: their rows have no event identity and their `ts` is
ingestion time.
- `retention/` is opt-in deployment SQL, never applied automatically.
## API reference
### AnalyticsSchemaV2
```php
AnalyticsSchemaV2::EXPOSURES_TABLE; // 'ab_exposures_v2'
AnalyticsSchemaV2::CONVERSIONS_TABLE; // 'ab_conversions_v2'
AnalyticsSchemaV2::EXPOSURE_COLUMNS; // ordered insert columns
// event_id, occurred_at, experiment, variant, subject_id, decision_reason,
// assignment_source, experiment_revision, environment, dimensions
AnalyticsSchemaV2::CONVERSION_COLUMNS; // the same plus goal, exposure_event_id
AnalyticsSchemaV2::DEDUPLICATION_NOTE; // why every read needs FINAL
```
### SchemaMigrations
```php
use Rasuvaeff\Yii3AbTestingClickHouse\SchemaMigrations;
$migrations = new SchemaMigrations($clickHouseClient, $logger);
$migrations->apply(); // default table names
$migrations->apply('exp_v2', 'conv_v2'); // custom names
$migrations->runner(); // the underlying ClickHouseMigrationRunner
SchemaMigrations::path(); // directory of the shipped .sql
```
Applying is explicit, never automatic: creating analytics tables is a
deployment step.
### Schema v2
```sql
CREATE TABLE ab_exposures_v2
(
event_id String,
occurred_at DateTime64(3, 'UTC'),
ingested_at DateTime('UTC') DEFAULT now(),
experiment LowCardinality(String),
variant LowCardinality(String),
subject_id String,
decision_reason LowCardinality(String),
assignment_source LowCardinality(String),
experiment_revision LowCardinality(String) DEFAULT '',
environment LowCardinality(String) DEFAULT '',
dimensions String DEFAULT '{}'
)
ENGINE = ReplacingMergeTree(ingested_at)
PARTITION BY toYYYYMM(occurred_at)
ORDER BY (experiment, event_id)
```
`ab_conversions_v2` adds `goal` and `exposure_event_id`.
`dimensions` is a JSON **string**, not a `Map`: the outbox exporter rejects
nested payload fields, and both delivery paths must produce identical rows.
Read it with `JSONExtract(dimensions, 'Map(String, String)')`.
`occurred_at` drives the partition key, so a retried delivery must carry the
original event time — otherwise it lands in a different partition and
ReplacingMergeTree cannot collapse it.