abrouter/laravel-abtest
Laravel A/B testing package from Abrouter for creating experiments, splitting users into variants, and tracking conversions with a simple facade. Integrates with Abrouter for feature flags, targeting, and analytics.
Install the package
composer require abrouter/laravel-abtest
Laravel auto-discovers the service provider; no manual registration needed.
Publish the config (so you can tune timeouts, cache TTL, and the API endpoint without touching vendor):
php artisan vendor:publish --provider="Abrouter\LaravelClient\Providers\AbrouterServiceProvider"
This creates config/abrouter.php.
Grab a token from the ABRouter dashboard (free tier is enough to prototype) and add it to .env:
ABROUTER_TOKEN=your-token-here
The default host points at https://abrouter.com/api/v1; swap it if you self-host.
Create your first experiment in the ABRouter UI (name + branches, e.g. A / B), then use it in a controller:
use Abrouter\LaravelClient\Facades\Abrouter;
$branch = Abrouter::getABTest('checkout-cta')->run();
return view('checkout', ['cta' => $branch === 'B' ? 'Buy now' : 'Add to cart']);
run() resolves the current user (via auth()->id() by default) and returns the assigned branch name.
For a feature flag, skip branches entirely:
if (Abrouter::getFeatureFlag('new-pricing-page')->isEnabled()) { ... }
Your first meaningful use case is usually a single boolean flag guarding a risky code path — it exposes the whole plumbing (token, user ID, cache) with the least surface area, then you graduate to multi-branch tests once the token and identity flow are proven.
Resolve the user explicitly when auth()->id() isn't right. For guests, API calls, or jobs, pass the identifier in:
Abrouter::getABTest('onboarding-flow')->runForUser($user->uuid);
Store an anonymous cookie/session UUID for pre-login visitors so the same guest doesn't flip branches on refresh.
Wrap experiments in a thin service. Don't scatter Abrouter:: calls across controllers and Blade views. A small ExperimentService with named methods (isNewPricingEnabled(), checkoutCtaVariant()) gives you one place to add fallbacks, logging, and conversion tracking, and makes it trivial to swap the mock in tests.
Use it cleanly in Blade by pre-computing the branch in the controller (or a view composer) rather than calling the facade inside templates — you want one API decision per request, not one per partial.
Pair the variant with a conversion call. ABRouter expects you to report the outcome so the dashboard can compute lift; call the track endpoint (or your wrapper's equivalent) at the success point — checkout complete, signup, etc. — with the same user ID you used to assign the branch, otherwise the assignment and the conversion are treated as different users.
Guard rollout with the flag's own targeting rules (percentage, user list) rather than hardcoding if ($branch === 'B'). That way you can tune exposure from the dashboard without a deploy.
Cache deliberately. The package hits the ABRouter API per decision; the config exposes a cache TTL. Short TTLs (60–300s) are fine for experiments, but for feature flags guarding hot paths you may extend it — just remember the trade-off: longer cache = slower to disable a flag during an incident.
Test by faking the decision, not the HTTP call. Bind a stub of your ExperimentService in the container (or use Laravel's Http::fake() if you call the client directly) so unit tests never touch ABRouter.
GPL-3.0 license. This is a strong copyleft license. Evaluate with your legal team before shipping in a proprietary product — it's the biggest non-technical risk in the package and a legitimate reason to reach for an alternative if your project is closed-source.
Guests without a stable ID get re-bucketed. If you call run() anonymously with no user ID, ABRouter can't deterministically hash and the "same" user may see different branches. Always supply a persistent identifier.
API failures are silent by default. If the token is wrong, or the endpoint is unreachable, the assignment logic won't be correct. Decide what your fallback should be (usually: serve the control branch / flag off) and implement it in your wrapper — don't rely on the package to fail loudly.
Set a sensible timeout. Network round-trips on every request add latency. Check the timeout key in config/abrouter.php, and pair it with the cache TTL so a slow ABRouter doesn't drag down your p95.
config:cache after publishing. If you change ABROUTER_TOKEN in .env and see no effect in production, it's almost always the cached config — php artisan config:clear.
Local/testing should bypass ABRouter. Either set a fake token and always return the control branch from your wrapper, or short-circuit in a if (app()->environment('local')) check. Otherwise your test suite depends on an external service and CI becomes flaky.
Don't mutate an experiment's branch names mid-test. Renaming A → control breaks historical assignment data and any code branching on the string. Treat branch identifiers as immutable once your first user is bucketed.
Extension points worth knowing: the config file lets you override the HTTP host and cache store, and the package resolves the user via the standard Laravel auth guard — if you use a custom guard or a non-id primary key, verify the default resolver picks it up or supply the ID yourself.
How can I help you explore Laravel packages today?