Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Ab Laravel Package

jenssegers/ab

Jenssegers/AB is a lightweight A/B testing package for Laravel that lets you define experiments, split traffic between variants, and track which version performs best. Simple to configure and easy to integrate into existing applications for data-driven decisions.

View on GitHub
Deep Wiki
Context7

Getting Started

1. Confirm the framework baseline before you type a command.

The only capability claim in the release history is v0.2.0: "Laravel 5 support." Open composer.json and read the real constraint:

composer show jenssegers/ab --all | head -20

Check the laravel/framework and php constraints against your runtime. If you are on Laravel 6+, treat the package as unsupported until you have proven otherwise in a throwaway branch — do not assume forward compatibility because nothing has broken yet.

2. Pin the exact version. Pre-1.0 means a minor bump may legally break you. Install with an exact constraint, not a caret:

composer require "jenssegers/ab:0.2.0"

3. Wire it up.

  • Laravel ≤5.4: add the service provider to config/app.php providers and the facade to aliases.
  • Laravel 5.5+: package auto-discovery should handle it; verify the provider appears in php artisan package:discover output.
php artisan vendor:publish --provider="Jenssegers\AB\ServiceProvider"
php artisan migrate   # if the assignment/goal store is database-backed

The publish step is the moment to answer the question the README probably does not: where does sticky assignment live? Read the published config and the migration together. Cookie, session, cache, and database have very different operational stories.

4. First use case: a single low-blast-radius copy test.

Pick something where a wrong answer costs a week, not a quarter. A marketing headline, a CTA label, an onboarding illustration. Never start on pricing, checkout, auth, or entitlements.

// routes/web.php or a controller
public function index(\Jenssegers\AB\Facades\Ab $ab)
{
    $variant = $ab->experiment('homepage-headline'); // true/false (or 'a'/'b' — check the source)
    return view('home', compact('variant'));
}
{{-- resources/views/home.blade.php --}}
<h1>
    @if ($variant)
        Ship faster with Acme
    @else
        Acme — build, deploy, relax
    @endif
</h1>

<a href="{{ route('signup') }}" data-experiment="homepage-headline">Start free</a>
// the goal handler, NOT the page render
public function store(Request $request)
{
    // ... create the user ...
    Ab::track('homepage-headline');   // fires the conversion
    return redirect()->route('dashboard');
}

5. Where to look first, in order: composer.json (real constraints) → the published config (storage driver, TTL, exposure settings) → the Tester/Experiment class that implements experiment() and track() (bucketing algorithm, dedupe rules) → the migration (schema, indexes, retention). The last two are the documentation. Budget half a day for it and write down what you find; nobody has published this before you.


Implementation Patterns

Never call the facade from a controller or view directly. The single highest-value thing you do on day one is put an internal contract in front of it:

// app/Contracts/ExperimentService.php
interface ExperimentService
{
    public function variant(string $key, ?string $visitorId = null): string;
    public function track(string $key, array $meta = []): void;
    public function active(string $key): bool;
}
// app/Services/PackageExperimentService.php
final class PackageExperimentService implements ExperimentService
{
    public function variant(string $key, ?string $visitorId = null): string
    {
        if (! $this->active($key)) {
            return 'control';
        }

        if ($this->isBot()) {                 // policy enforced centrally
            return 'control';
        }

        return \Jenssegers\AB\Facades\Ab::experiment($key) ? 'b' : 'a';
    }

    public function active(string $key): bool
    {
        return (bool) config("experiments.enabled.{$key}", false);
    }

    public function track(string $key, array $meta = []): void
    {
        if (! $this->active($key)) {
            return;
        }

        \Jenssegers\AB\Facades\Ab::track($key);
        event(new ExperimentConverted($key, $this->visitorId(), $meta)); // your own analytics
    }
}

This buys you three things a raw facade call does not: a central kill switch (config/experiments.php), bot filtering in one place, and a swap to Laravel Pennant or Unleash that touches one file.

Resolve once per request, not per call site. If bucketing reads from a session or cookie, calling experiment() repeatedly is fine — but if it can be inconsistent, resolve in a middleware and share via View::share or a request attribute so the header, the CTA, and the tracking pixel never disagree.

// app/Http/Middleware/ResolveExperiments.php
public function handle($request, Closure $next)
{
    $request->attributes->set('variant.homepage-headline',
        app(ExperimentService::class)->variant('homepage-headline'));

    return $next($request);
}

Namespace experiment keys as constants. Bucketing usually hashes on the key string; a typo is not a crash, it is a brand-new experiment with silently split traffic.

final class Experiments
{
    public const HOMEPAGE_HEADLINE = 'homepage-headline-2024-q3';
    public const ONBOARDING_STEP   = 'onboarding-step-v2';
}

Date-stamp or version the key. Renaming an experiment is how you get two half-populated datasets that look like one.

Track goals at the goal, never at the exposure. Exposure = "the variant was rendered." Conversion = "the user did the thing." Firing track() in the Blade template will produce a 100% conversion rate and an embarrassing Slack message.

Log the variant alongside the event. Even if the package has its own store, ship the variant into your normal analytics payload. Reconciliation between the package's numbers and your warehouse is how you discover that the cookie layer dropped a week of assignments.

analytics()->track('signup_completed', [
    'variant_homepage_headline' => app(ExperimentService::class)
        ->variant(Experiments::HOMEPAGE_HEADLINE),
]);

Contract-test the invariants you actually care about. The package's own test coverage is unknown; yours is not optional.

public function test_same_visitor_gets_same_variant_across_requests()
{
    $service = app(ExperimentService::class);
    config(['experiments.enabled' => [Experiments::HOMEPAGE_HEADLINE => true]]);

    $first = $service->variant(Experiments::HOMEPAGE_HEADLINE);
    $this->get('/')->assertOk();
    $second = $service->variant(Experiments::HOMEPAGE_HEADLINE);

    $this->assertSame($first, $second);
}

For multi-variant tests, if the package is boolean-only, compose it: wrap a weighted random_int selection inside a single experiment flag, so the package owns "in or out" and your code owns "which of the three." Persist your sub-choice the same way the package persists its own — otherwise you have two incompatible stickiness mechanisms.


Gotchas and Tips

Sticky assignment is the whole product, and it is the least documented part. Before you write a line of application code, run these four experiments in a staging environment and record the answers:

  1. Set the cookie, clear it, reload — does the variant change?
  2. Clear the cache / restart Redis — does the variant change?
  3. Hit the page from a fresh incognito session (new device) — expected? Usually yes, but decide whether that contaminates your dataset.
  4. Queue a job on a CLI worker with no session — what does experiment() return? If it throws or returns null, every background job touching a variant will behave differently from the web path.

Bots will be in your data unless you exclude them. No package at this maturity ships reliable crawler filtering. Filter at the adapter on user agent, or at the analytics layer on the event. Decide before the experiment starts, not after the numbers look strange.

Goal dedupe semantics are the integration cost, not the code. Find out whether track() records once per visitor per experiment, once per session, or once per call. Then find out the attribution window — if the conversion fires two weeks later, does it still count? If you do not know, your conversion rate is undefined, not approximate.

Renaming or deleting an experiment mid-flight re-randomizes everyone. If the bucket is derived from the experiment key, every visitor is a new visitor. Freeze keys for the duration of a test and archive rather than delete.

php artisan config:cache will bite you. Do not read env() inside your adapter — under config caching it returns null. Put defaults in config/experiments.php and read through config() only.

Session driver choice changes your data. file vs. redis vs. cookie have different eviction behaviour and different multi-server behaviour. If you run more than one app server and the package leans on the session, verify the session driver is shared. A load-balanced pair with local file sessions silently serves both variants.

Facade alias collisions. Ab is a short alias. If your project already defines a global Ab helper or another package claims the alias, resolution order in config/app.php picks a winner and you will debug it for an hour. Import the fully-qualified facade class in your adapter and move on.

Treat a dependency-review gate as mandatory. A bare composer update can move a 0.x package to a breaking version. Pin exact, and add the package to whatever review step you have for third-party changes.

Do not let it justify staying on an old framework. If adopting this means pinning the app to an EOL Laravel, the package has become a migration blocker disguised as a testing tool. Put the removal date in the ticket when you add the dependency.

Extension points worth knowing: the published config (storage driver, TTL, cookie name) and the service container binding for the tester class. If the package resolves its tester from the container, you can bind a subclass that adds bot filtering, per-tenant prefixes, or a deterministic bucketing override for tests — that is the cleanest place to inject behaviour you cannot get from the config.

When it stops being the right tool, swap it in an afternoon. Because everything goes through ExperimentService, moving to Laravel Pennant is variant()/track() reimplementation plus a data-import decision about in-flight assignments. That boundary is the entire reason to accept this dependency at 0.x — keep it intact and the exit is cheap.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console