rasuvaeff/yii3-ab-testing-clickhouse

ClickHouse exposure and conversion trackers for Yii3 A/B testing

Maintainers

Package info

github.com/rasuvaeff/yii3-ab-testing-clickhouse

pkg:composer/rasuvaeff/yii3-ab-testing-clickhouse

Transparency log

Statistics

Installs: 2

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.1.1 2026-07-25 15:09 UTC

README

Stable Version Total Downloads Build Static Analysis PHP License Русская версия

ClickHouse exposure and conversion trackers for Yii3 A/B testing. Implements the ExposureTracker and ConversionTracker interfaces from rasuvaeff/yii3-ab-testing, buffering events in memory and writing them to ClickHouse in batches.

Using an AI coding assistant? llms.txt contains a compact API reference you can ingest in your prompt context.

Requirements

  • PHP 8.3+
  • rasuvaeff/yii3-ab-testing ^1.2
  • rasuvaeff/clickhouse-toolkit ^1.1
  • a PSR-18 HTTP client (for example guzzlehttp/guzzle) for the ClickHouse connection

Installation

composer require rasuvaeff/yii3-ab-testing-clickhouse

With Yii3 config-plugin this package binds ExposureTracker, ConversionTracker and ClickHouseTrackingFlushMiddleware automatically. Do not bind the tracker interfaces from another adapter at the same time or yiisoft/config reports a Duplicate key error. To send events to several sinks, compose them with the core CompositeExposureTracker / CompositeConversionTracker.

The DI factory pulls a Rasuvaeff\ClickHouseToolkit\ClickHouseClientFactory from the container and uses it to build the batch writers. Bind the factory in your application:

use Rasuvaeff\ClickHouseToolkit\ClickHouseClientFactory;
use Rasuvaeff\ClickHouseToolkit\ClickHouseConfig;

return [
    ClickHouseClientFactory::class => static fn (): ClickHouseClientFactory => new ClickHouseClientFactory(
        new ClickHouseConfig(host: getenv('CLICKHOUSE_HOST') ?: '127.0.0.1', /* ... */),
    ),
];

Database schema

DDL for the two event tables ships under migrations/ as ClickHouse *.sql files, applied by the toolkit's ClickHouseMigrationRunner. The table names are {{exposures_table}} / {{conversions_table}} placeholders, resolved by the runner before the file is hashed and executed:

use Rasuvaeff\ClickHouseToolkit\ClickHouseMigrationRunner;

(new ClickHouseMigrationRunner(
    client: $clickHouseClient,
    migrationsPath: __DIR__ . '/vendor/rasuvaeff/yii3-ab-testing-clickhouse/migrations',
    placeholders: [
        'exposures_table' => 'ab_exposures',
        'conversions_table' => 'ab_conversions',
    ],
))->run();

Wired through rasuvaeff/yii3-clickhouse-toolkit (v1.1+), the same values come from params. The name is used twice — by the writer and by the migration — so define it once:

// config/common/params.php
$exposures = 'ab_exposures';
$conversions = 'ab_conversions';

return [
    'rasuvaeff/yii3-ab-testing-clickhouse' => [
        'exposuresTable' => $exposures,
        'conversionsTable' => $conversions,
    ],
    'rasuvaeff/yii3-clickhouse-toolkit' => [
        'migrationPlaceholders' => [
            'exposures_table' => $exposures,
            'conversions_table' => $conversions,
        ],
    ],
];

Two params blocks rather than one because yiisoft/config allows exactly one vendor package to define a given params key: this package cannot contribute to the toolkit's migrationPlaceholders without a Duplicate key error.

Before v1.1 the params renamed only the writer while the shipped DDL always created ab_exposures / ab_conversions — configuring them produced a writer inserting into a table nothing had created.

Renaming after the first apply. The runner's checksum covers the resolved SQL, so changing a name once the migration has been applied is reported as a divergence instead of silently creating a second table. Create the new table yourself (the DDL is in migrations/), or drop the file's row from the _migrations table, then re-run.

Table Columns
ab_exposures experiment, variant, subject_id, is_forced, is_fallback, is_sticky, environment, ts
ab_conversions experiment, variant, subject_id, goal, is_forced, is_fallback, is_sticky, environment, ts

Both are MergeTree partitioned by toYYYYMM(ts); ts defaults to now().

Usage

use Rasuvaeff\ClickHouseToolkit\ClickHouseBatchWriter;
use Rasuvaeff\Yii3AbTesting\AbTesting;
use Rasuvaeff\Yii3AbTestingClickHouse\ClickHouseConversionTracker;
use Rasuvaeff\Yii3AbTestingClickHouse\ClickHouseExposureTracker;

$exposure = new ClickHouseExposureTracker(
    writer: new ClickHouseBatchWriter($client, 'ab_exposures', ClickHouseExposureTracker::COLUMNS),
);
$conversion = new ClickHouseConversionTracker(
    writer: new ClickHouseBatchWriter($client, 'ab_conversions', ClickHouseConversionTracker::COLUMNS),
);

$ab = new AbTesting(
    provider: $provider,
    strategy: $strategy,
    exposureTracker: $exposure,
    conversionTracker: $conversion,
);

$assignment = $ab->assign(experiment: 'checkout-button', subjectId: (string) $userId);
$ab->trackExposure($assignment);            // buffered, not sent yet
$ab->trackConversion($assignment, goal: 'purchase');

Request-end flushing

Tracking never makes a network call on trackExposure() or trackConversion(). Rows are appended to an in-memory buffer and written on flush(). The package ships ClickHouseTrackingFlushMiddleware for the recommended request-end flush:

use Rasuvaeff\Yii3AbTestingClickHouse\ClickHouseTrackingFlushMiddleware;

return [
    ClickHouseTrackingFlushMiddleware::class,
    // place it late in the PSR-15 pipeline
];

The middleware wraps the downstream handler in try/finally, flushes both trackers after the request, and swallows/logs flush failures so analytics never breaks the user response.

If you do not use a PSR-15 pipeline, call flush() yourself once at request end or from register_shutdown_function().

API reference

Class Description
ClickHouseExposureTracker Buffers exposures; flush() batch-writes to ab_exposures
ClickHouseConversionTracker Buffers conversions (with goal); flush() batch-writes to ab_conversions
ClickHouseTrackingFlushMiddleware PSR-15 middleware that flushes both trackers safely at request end

Security

  • Connection credentials travel via the toolkit's ClickHouseClientFactory (headers / config from env), never in URLs. The toolkit validates table and column identifiers and uses parameterized inserts.
  • subject_id is stored verbatim and may be personally identifiable. Apply TTL / partition retention per your privacy policy.
  • Middleware swallows flush failures by design, so add logging/monitoring for the warning message if analytics delivery matters operationally.

Examples

See examples/ for a runnable script (no server required — uses an in-memory writer).

Development

composer build          # full gate: validate + normalize + cs + psalm + test
composer cs:fix         # auto-fix code style
composer psalm          # static analysis
composer test           # run unit tests (integration tests skipped without CLICKHOUSE_HOST)

License

BSD-3-Clause. See LICENSE.md.