MIRA FIVE

Laravel

Track MIRA FIVE events from Laravel, evaluate feature flags in-process and add the tracker tag with one Blade directive.

mirafive/sdk-laravel adds MIRA FIVE to a Laravel app: a Mira facade for server events and feature flags, Blade directives for the tracker tag and the flag bootstrap, and Mira::fake() for tests. Events are sent after the response, so pages never wait for analytics. It wraps the PHP SDK.

Install

composer require mirafive/sdk-laravel

Requires PHP 8.3 or newer and Laravel 11, 12 or 13. The service provider and the Mira facade are discovered automatically. No configuration file is needed; to change the defaults, publish it:

php artisan vendor:publish --tag=mirafive-config

Set up

You need two keys, see Keys for where to find them: the secret key of a server source for server events and flags, and the website key of a website source for the tracker tag. Put both in .env:

.env
MIRAFIVE_SECRET_KEY=mf_…
MIRAFIVE_WEBSITE_KEY=mf_…

Add the tracker to your layout's <head>:

resources/views/layouts/app.blade.php
<head>
    @mirafiveScript
</head>

It prints the hosted script with your website key, and a Vite CSP nonce when one is set. The secret key never reaches the page. The tracker starts in consentless mode: no cookies, no storage, no consent banner.

Track a server event where it happens:

app/Http/Controllers/SignupController.php
<?php

namespace App\Http\Controllers;

use App\Models\User;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use MiraFive\Laravel\Facades\Mira;

class SignupController extends Controller
{
    public function store(Request $request): RedirectResponse
    {
        $user = User::create($request->validate([
            'name' => ['required', 'string'],
            'email' => ['required', 'email'],
            'password' => ['required', 'min:12'],
        ]));

        Mira::track('signup', userId: (string) $user->id, properties: ['plan' => 'free']);

        return redirect()->route('dashboard');
    }
}

That is the whole install. track() buffers, and the package sends the buffer after the response. You never call flush().

Server events are sent in full mode by default. See Consent.

Verify

  1. Run the check command. It sends an install check, which is never stored or billed, and prints the receipt:

    php artisan mirafive:check

    It ends with The key and host work. A wrong key prints the error code instead, such as unauthorized.

  2. Load a page on its real domain and look for POST https://events.mirafive.io/v1/batch/mf_… answering 202 in the browser's network tab. The tracker skips localhost unless you add data-track-localhost (see Tracker tag).

  3. Trigger a server event, then open the source's live view in MIRA FIVE and find both.

Nothing arriving? See Troubleshooting.

Track events

The facade takes the same named arguments as the PHP SDK:

use MiraFive\Laravel\Facades\Mira;

Mira::track('order completed', userId: (string) $order->user_id, properties: [
    'revenue' => $order->total,
    'currency' => 'EUR',
]);

Mira::forUser($user) fills in getAuthIdentifier() as the user id, which is the primary key for Eloquent users:

Mira::forUser($request->user())->track('report_exported', ['format' => 'csv']);

For events that must be recorded exactly once, such as a payment webhook, Mira::send() sends at once with an idempotency key and returns the MiraFive\Receipt. It never goes through the queue:

Mira::send([
    ['name' => 'order completed', 'userId' => (string) $order->user_id, 'properties' => ['revenue' => 129, 'currency' => 'EUR']],
], idempotencyKey: "order-{$order->id}");

MiraFive\Mira and MiraFive\Flags\MiraFlags are container singletons; you can inject them instead of using the facade. Input MIRA FIVE would refuse throws an InvalidArgumentException at the call. Event names, properties and revenue are covered in Track events.

Identify users

Identify the user after login, with the internal id and a few traits. In app/Providers/AppServiceProvider.php:

use Illuminate\Auth\Events\Login;
use Illuminate\Support\Facades\Event;
use MiraFive\Laravel\Facades\Mira;

public function boot(): void
{
    Event::listen(Login::class, function (Login $event): void {
        Mira::forUser($event->user)->identify(['plan' => $event->user->plan]);
    });
}

Never send an email address as the user id. See Identify users.

Tracker tag

@mirafiveScript takes more tracker attributes as an array. true prints a bare attribute:

@mirafiveScript(['data-autocapture' => true, 'data-site-search' => 'q'])
SettingWhat it prints
MIRAFIVE_WEBSITE_KEYdata-key. Without it, the directive prints nothing.
MIRAFIVE_SCRIPT_MODE=fulldata-mode="full".
MIRAFIVE_HOST other than the defaultdata-host.
MIRAFIVE_SCRIPT_URLsrc, e.g. a self-hosted copy. Default https://cdn.mirafive.io/mira.js.
'nonce' => '…' in the arrayA CSP nonce on both script tags, instead of Vite's.

The attributes the tracker reads are listed on the script tag page.

Server events and the tracker have separate collection modes:

SettingDefault
Server eventsMIRAFIVE_MODEfull
Tracker tagMIRAFIVE_SCRIPT_MODEconsentless
  • Full server events carry the userId, anonymousId and sessionId you pass. Your site holds the consent or other lawful basis for them; the package cannot ask.
  • Consentless carries no identifiers. On the server, passing one throws an InvalidArgumentException on the first call.
  • A tracker in full mode stores nothing until your consent banner grants it with mirafive('consent', true). See Script tag.
  • No personal data in event names or properties: no names, email addresses, phone numbers or free text a person typed.

See Consent.

Feature flags

Mira::flags() is the MiraFive\Flags\MiraFlags of your server source. Its answers come from a flag document it fetches on first use and shares between requests through your cache store:

use MiraFive\Laravel\Facades\Mira;

$flags = Mira::flags()->for(
    userId: (string) $request->user()->id,
    properties: ['plan' => $request->user()->plan],
    consent: ['experiments' => true, 'targeting' => false],
    optedOut: Mira::optedOut($request),
);

$flags->enabled('new-checkout');                     // bool
$flags->variant('pricing-test', 'control');          // string
$flags->config('checkout-limits', ['maxItems' => 10]); // the variant's value

Mira::optedOut($request) is true when the request carries Sec-GPC: 1 or DNT: 1. Mira::forUser($user)->flags(properties: [...]) does the same with the user id filled in, and reads the opt-out from the current request unless you pass optedOut.

Reads never throw; without a document every flag answers its fallback. The unit, consent, segments and experiments work as in the PHP SDK. See Feature flags.

Bootstrap in Blade

Hand the server's answers to the browser so the first paint shows the right variant:

<head>
    @mirafiveFlags(['userId' => auth()->id(), 'properties' => ['plan' => auth()->user()?->plan]])
    @mirafiveScript(['data-flags' => true])
</head>

The array takes userId, anonymousId, properties, consent and optedOut; optedOut is read from Sec-GPC and DNT when you leave it out. You can also pass flags you already read: @mirafiveFlags($flags). The block carries only flags your website reads, escaped so no value can end the script.

A page with a bootstrap belongs to one visitor. The package adds Cache-Control: private, no-store to every response whose view printed @mirafiveFlags. If you print $flags->bootstrap() yourself, send the header too:

use MiraFive\Flags\MiraFlags;

return response($html)->withHeaders(MiraFlags::BOOTSTRAP_HEADERS);

Queues and Octane

When events are sent. The buffer is sent once when Laravel terminates (after the response under PHP-FPM), after each Octane request, task and tick, and after each queued job. The PHP SDK's own shutdown flush is off. Every 100 events it is sent early. send() is always immediate.

Queued delivery. Set MIRAFIVE_QUEUE and each buffered batch is encoded once and handed to a MiraFive\Laravel\Queue\SendBatch job, so the request does no delivery work:

.env
MIRAFIVE_QUEUE=redis:mirafive

The value is a queue name (mirafive), connection:queue (redis:mirafive), or true for the default queue. Run a worker for it:

php artisan queue:work redis --queue=mirafive
  • The job carries the body, never the key: the worker sends it with its own configuration.
  • The body keeps its batch id, so a job that runs twice is stored once.
  • Retryable failures are retried by the queue: 5 tries, backoff of 10 s, 1 min, 5 min and 15 min. Refused batches are logged and dropped.
  • A worker without MIRAFIVE_SECRET_KEY logs an error and fails the job instead of dropping it. A worker with MIRAFIVE_ENABLED=false drops it.
  • Batches are up to 1 MiB of JSON. Check your queue's message limit (SQS allows 256 KB).

Octane. No request state is kept in singletons, and the buffer is flushed at the end of every request. The package adds MiraFive\Mira, MiraFlags, the transport and the facade's client to octane.warm, so a worker keeps one HTTP connection and one flag document.

Testing

Mira::fake() records instead of sending:

tests/Feature/SignupTest.php
use MiraFive\Laravel\Facades\Mira;

it('tracks the signup', function () {
    $mira = Mira::fake();

    $this->post('/signup', [
        'name' => 'Ada',
        'email' => 'ada@example.com',
        'password' => 'correct-horse-battery',
    ])->assertRedirect();

    $mira->assertTracked('signup');
    $mira->assertTracked('signup', fn (array $event) => $event['properties']['plan'] === 'free');
    $mira->assertTracked('signup', 1); // exactly once
    $mira->assertNotTracked('refund');
});

it('stays quiet', function () {
    Mira::fake()->assertNothingTracked();
});

The fake records track(), identify() and send() through the facade and through an injected MiraFive\Laravel\Client. Input is still checked, so an event MIRA FIVE would refuse fails the test. Flags answer their fallbacks. Code that injects MiraFive\Mira directly is not faked: set MIRAFIVE_ENABLED=false in phpunit.xml so it sends nothing:

phpunit.xml
<env name="MIRAFIVE_ENABLED" value="false"/>

API reference

Configuration

config/mirafive.php, publish tag mirafive-config.

KeyEnvDefaultDescription
enabledMIRAFIVE_ENABLEDtrueOff, or without a secret key: nothing is sent and no key is needed, flags answer their fallbacks. Off also omits the tracker tag.
secret_keyMIRAFIVE_SECRET_KEYSecret key of a server source.
website_keyMIRAFIVE_WEBSITE_KEYWebsite key, printed by @mirafiveScript.
hostMIRAFIVE_HOSThttps://events.mirafive.ioWith scheme.
modeMIRAFIVE_MODEfullServer events: full or consentless.
script_modeMIRAFIVE_SCRIPT_MODEconsentlessThe tracker tag: consentless or full.
queueMIRAFIVE_QUEUEnullnull sends after the response; a queue name or connection:queue hands each batch to a job.
script_urlMIRAFIVE_SCRIPT_URLhttps://cdn.mirafive.io/mira.jsThe tracker script.
flags.refresh_secondsMIRAFIVE_FLAGS_REFRESH30How old the flag document may get. At least 10.
flags.cache_storeMIRAFIVE_FLAGS_CACHEdefault storeThe cache store that shares the flag document and the delivery pause between requests.

Delivery failures go to your default log channel as warnings, prefixed [mirafive]. To send through your own HTTP client, bind MiraFive\Http\Transport (for example to MiraFive\Http\Psr18Transport) before the client is resolved.

MiraFive\Laravel\Facades\Mira

MethodDescription
track(string $name, ?string $userId = null, ?string $anonymousId = null, ?string $sessionId = null, array $properties = [], DateTimeInterface|int|null $time = null, ?array $page = null): voidBuffers one event.
identify(string $userId, array $traits = [], ?string $anonymousId = null, DateTimeInterface|int|null $time = null): voidBuffers $identify.
send(array $events, ?string $idempotencyKey = null): ReceiptSends now. Throws MiraFive\MiraError.
flush(): voidSends the buffer. The package calls it for you.
flags(): MiraFlagsThe flags of the server source.
forUser(Authenticatable $user): UserContextThe calls below with getAuthIdentifier() as the user id.
optedOut(Request $request): boolWhether the request carries Sec-GPC: 1 or DNT: 1.
fake(): MiraFakeSwaps the facade for a recorder.

UserContext (from forUser()):

MethodDescription
track(string $name, array $properties = [], ?string $anonymousId = null, ?string $sessionId = null, DateTimeInterface|int|null $time = null, ?array $page = null): voidBuffers one event for the user.
identify(array $traits = [], ?string $anonymousId = null): voidBuffers $identify for the user.
flags(array $properties = [], array $consent = [], ?string $anonymousId = null, ?bool $optedOut = null): UserFlagsThe user's flags. null reads the opt-out from the current request.

Blade

DirectiveDescription
@mirafiveScript or @mirafiveScript([...])The tracker tag with the website key and extra attributes.
@mirafiveFlags([...]) or @mirafiveFlags($flags)The flag bootstrap block. Adds Cache-Control: private, no-store to the response.

MiraFive\Laravel\Testing\MiraFake

MethodDescription
assertTracked(string $name, callable|int|null $callback = null)Tracked at least once, matching the callback, or exactly this many times. The callback receives name, userId, anonymousId, sessionId, properties, time, page.
assertNotTracked(string $name, ?callable $callback = null)Not tracked (matching the callback).
assertIdentified(?string $userId = null, ?callable $callback = null)A user (this user) was identified. The callback receives userId, traits, anonymousId.
assertNothingTracked()Nothing tracked, sent or identified.
tracked(?string $name = null), identified()What was recorded.

Artisan

php artisan mirafive:check sends $install_check with the configured key and host and prints the batch, the accepted and dropped counts and the reason. It fails unless the reason is install_check.

Troubleshooting

SymptomCause and fix
Nothing arrivesRun php artisan mirafive:check, then look for [mirafive] warnings in your log.
unauthorizedMIRAFIVE_SECRET_KEY is missing, wrong or revoked; it must be the secret key of a server source. After changing .env in production, run php artisan config:cache again.
website_key_as_bearerThe website key ended up in MIRAFIVE_SECRET_KEY. Server code needs the secret key.
InvalidArgumentException: A consentless client may not send userIdMIRAFIVE_MODE is consentless. Drop the identifiers, or switch to full where you hold consent.
No tracker tagMIRAFIVE_WEBSITE_KEY is empty or MIRAFIVE_ENABLED is false.
Queued batches never arriveNo worker runs the configured queue, e.g. php artisan queue:work redis --queue=mirafive.
A flag always answers its fallbackCheck Mira::flags()->status() and $flags->evaluate($key)->errorCode: NOT_READY means no document (see the log), FLAG_NOT_FOUND that the flag is not a flag of this source.

Set up with an AI agent

Paste this into your coding agent:

Add MIRA FIVE analytics to this Laravel application with the Composer package mirafive/sdk-laravel.
Docs: https://docs.mirafive.io/sdks/laravel.md

1. Run `composer require mirafive/sdk-laravel` (PHP 8.3+, Laravel 11–13). The provider and the Mira facade are
   auto-discovered; do not register them by hand.
2. Add to .env.example (empty values) and ask me for the real values for .env:
     MIRAFIVE_SECRET_KEY=   (secret key of a server source; never print it into HTML or JavaScript)
     MIRAFIVE_WEBSITE_KEY=  (website key of a website source)
   Only add MIRAFIVE_HOST, MIRAFIVE_MODE or MIRAFIVE_SCRIPT_MODE if I ask for them. The tracker is consentless by
   default; set MIRAFIVE_SCRIPT_MODE=full only if the site has a consent banner that calls mirafive('consent', …).
3. Put @mirafiveScript inside <head> of the main Blade layout(s), once per page.
4. Track the signup where the user is created:
     \MiraFive\Laravel\Facades\Mira::track('signup', userId: (string) $user->getAuthIdentifier(), properties: ['plan' => $plan]);
   Use the internal user id, never an email address. No personal data in event names or properties.
   Identify on login: listen to Illuminate\Auth\Events\Login and call
     Mira::forUser($event->user)->identify(['plan' => ...]);
5. Do not call flush(); the package sends after the response, after Octane requests and after queued jobs.
6. In tests, call $mira = Mira::fake(); and assert with $mira->assertTracked('signup').
   Add <env name="MIRAFIVE_ENABLED" value="false"/> to phpunit.xml.
7. Verify with `php artisan mirafive:check`: it must end with "The key and host work". Report what you changed.
Do not add other analytics libraries, cookies or consent banners.

On this page