Skip to content

Integration matrix ​

Every package in the family has a good isolated example. What this document adds is the answer to "how to assemble your combination" — the question that sits between understanding the idea and running it in production.

Read it once, pick one option per axis, and the rest follows.


Axis 1 — where experiment definitions live ​

OptionInstallWhen
PHP configcore onlydefinitions change with a deploy anyway
Databaseyii3-ab-testing-dbyou need a kill switch and reweights without a deploy

Exactly one source may bind ExperimentProvider. Installing the db package and binding ConfigExperimentProvider in your app is a yiisoft/configDuplicate key error, by design — two sources of experiment truth is the bug, not the error message.

php
// config-only: the application binds it
ExperimentProvider::class => static fn (): ExperimentProvider
    => new ConfigExperimentProvider($params['experiments']),

With the db package, bind nothing: the package owns the key.


Axis 2 — how events reach analytics ​

OptionInstallTrade-off
Durableyii3-ab-testing-outbox + yii3-outbox-db + a workersurvives an analytics outage; costs a table and a running worker
Log shippingcore's Logger* sinks + Vector/Fluent Bitno worker, no request-time network call; delivery is the collector's problem

Both produce the same rows in the same schema v2 tables, owned by yii3-ab-testing-clickhouse.

Do not write to analytics storage from the request path. Under PHP-FPM a per-request insert means many tiny writes plus a network call inside the user's latency; that is why the direct ClickHouse writer was removed in 2.0.

Pick durable when losing events is unacceptable and you already run workers. Pick log shipping when you already run a log pipeline and would rather not add a queue.


Axis 3 — who the subject is ​

OptionHowWhen
Anonymous cookieSubjectIdMiddleware (yii3-ab-testing-web)public traffic, no login required
Authenticated idyour auth middleware writes the request attributelogged-in users; the cookie branch is never taken
Hybridboth, plus AnonymousToAuthenticatedStrategyvisitors convert from anonymous to logged-in mid-experiment

The hybrid case has a trap worth naming: without a migration strategy a visitor assigned anonymously gets a different variant after logging in, because the subject id changed. MigrateAssignments carries the assignment across.


Axis 4 — keeping the variant stable ​

OptionInstallSurvives
Nothing—nothing: changing weights re-buckets everyone
Signed cookieyii3-ab-testing-webreweights; not a cookie clear or a new device
Databaseyii3-ab-testing-db (DbAssignmentStore)reweights, devices, cookie clears
Your ownimplement ConfigurationAwareAssignmentStore—

Assignment is deterministic in (salt, subjectId), so a subject keeps their variant as long as the weights hold. Change the weights and bucket boundaries move. If your experiment will ever be reweighted mid-flight — and it will — a store is not optional.


Axis 5 — several sinks at once ​

One source must own each tracker key. To fan out, compose in your own config:

php
ExposureTracker::class => static fn (): ExposureTracker => new CompositeExposureTracker(
    new OutboxExposureTracker(/* … */),
    new LoggerExposureTracker(/* … */),
),

Installing two packages that each bind ExposureTracker is a Duplicate key error. Since 2.0 yii3-ab-testing-clickhouse binds neither, so that particular collision is gone.


Axis 6 — SSR or SPA ​

In SSR the server knows it rendered the variant. In a SPA only the client does, and that changes one thing fundamentally.

SSRSPA / headless
Identitycookierequest attribute from the token, or a client id in a header
Cross-originn/aSameSite=None + Secure + CORS credentials
Stickinesscookie storeDbAssignmentStore if cookies are out
Exposureserver-side, on rendera call the client makes

Never track exposure on the endpoint that returns assignments: that counts prefetches, routes the visitor never reached, and every repeat navigation.

Three endpoints, and they belong in your application — routing, auth and rate limiting are not this library's business:

GET  /ab/assignments   → variants + a signed receipt, no tracking
POST /ab/exposure      → the client rendered it
POST /ab/conversion    → the goal happened

The receipt must be signed (SignedReceiptCodec). Re-resolving server-side is not a substitute: after a reweight it returns the variant the visitor would get now, not the one they saw.

Runnable: examples/spa-endpoints.php in yii3-ab-testing-web.


Axis 7 — operations ​

Migrations. Register the -db package's namespace through setSourceNamespaces(), as its README shows. This needs yiisoft/db-migration 2.1 or later, which the package requires: before 2.1 the registration silently found nothing while the core was installed — migrate:up printed "up-to-date", exited 0 and created no tables.

The ClickHouse tables are a separate, explicit step: (new SchemaMigrations($client))->apply() from yii3-ab-testing-clickhouse.

The worker (durable path only) drains the outbox: outbox:clickhouse:export. Without it, events accumulate and nothing reaches analytics — and every HTTP response is still 200, so assert delivered row counts, not status codes.

Retention is opt-in SQL in yii3-ab-testing-clickhouse/retention/, never applied automatically.

Erasure. subject_id is personal data when it is a user id. DbAssignmentStore::forget($subjectId) erases a subject across experiments.


Axis 8 — reading the results ​

sql
SELECT variant, uniqExact(subject_id) AS subjects
FROM ab_exposures_v2 FINAL
WHERE experiment = 'checkout_button' AND decision_reason = 'assigned'
GROUP BY variant

Two non-negotiables:

  • FINAL. Deduplication happens on merge, not on insert, and delivery is at-least-once. Without it a retried delivery is counted twice.
  • decision_reason = 'assigned'. Forced (QA) traffic and both fallback kinds are not experiment participation.

Attribution rules — the window, repeated conversions, exclusions — are fixed in the core (AttributionWindow, RepeatedConversionPolicy) so every reporting system answers the same way.


Version compatibility ​

corewebdboutboxclickhouse
2.x2.x3.x2.x – 3.x2.x
1.61.22.11.31.2

Mixing a 2.x core with 1.x adapters is prevented by Composer constraints. Each package's UPGRADE.md covers its own migration.