Skip to content

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.