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-laravelRequires 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-configSet 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:
MIRAFIVE_SECRET_KEY=mf_…
MIRAFIVE_WEBSITE_KEY=mf_…Add the tracker to your layout's <head>:
<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:
<?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
-
Run the check command. It sends an install check, which is never stored or billed, and prints the receipt:
php artisan mirafive:checkIt ends with
The key and host work.A wrong key prints the error code instead, such asunauthorized. -
Load a page on its real domain and look for
POST https://events.mirafive.io/v1/batch/mf_…answering202in the browser's network tab. The tracker skipslocalhostunless you adddata-track-localhost(see Tracker tag). -
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'])| Setting | What it prints |
|---|---|
MIRAFIVE_WEBSITE_KEY | data-key. Without it, the directive prints nothing. |
MIRAFIVE_SCRIPT_MODE=full | data-mode="full". |
MIRAFIVE_HOST other than the default | data-host. |
MIRAFIVE_SCRIPT_URL | src, e.g. a self-hosted copy. Default https://cdn.mirafive.io/mira.js. |
'nonce' => '…' in the array | A CSP nonce on both script tags, instead of Vite's. |
The attributes the tracker reads are listed on the script tag page.
Consent
Server events and the tracker have separate collection modes:
| Setting | Default | |
|---|---|---|
| Server events | MIRAFIVE_MODE | full |
| Tracker tag | MIRAFIVE_SCRIPT_MODE | consentless |
- Full server events carry the
userId,anonymousIdandsessionIdyou 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
InvalidArgumentExceptionon 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 valueMira::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:
MIRAFIVE_QUEUE=redis:mirafiveThe 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_KEYlogs an error and fails the job instead of dropping it. A worker withMIRAFIVE_ENABLED=falsedrops 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:
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:
<env name="MIRAFIVE_ENABLED" value="false"/>API reference
Configuration
config/mirafive.php, publish tag mirafive-config.
| Key | Env | Default | Description |
|---|---|---|---|
enabled | MIRAFIVE_ENABLED | true | Off, or without a secret key: nothing is sent and no key is needed, flags answer their fallbacks. Off also omits the tracker tag. |
secret_key | MIRAFIVE_SECRET_KEY | Secret key of a server source. | |
website_key | MIRAFIVE_WEBSITE_KEY | Website key, printed by @mirafiveScript. | |
host | MIRAFIVE_HOST | https://events.mirafive.io | With scheme. |
mode | MIRAFIVE_MODE | full | Server events: full or consentless. |
script_mode | MIRAFIVE_SCRIPT_MODE | consentless | The tracker tag: consentless or full. |
queue | MIRAFIVE_QUEUE | null | null sends after the response; a queue name or connection:queue hands each batch to a job. |
script_url | MIRAFIVE_SCRIPT_URL | https://cdn.mirafive.io/mira.js | The tracker script. |
flags.refresh_seconds | MIRAFIVE_FLAGS_REFRESH | 30 | How old the flag document may get. At least 10. |
flags.cache_store | MIRAFIVE_FLAGS_CACHE | default store | The 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
| Method | Description |
|---|---|
track(string $name, ?string $userId = null, ?string $anonymousId = null, ?string $sessionId = null, array $properties = [], DateTimeInterface|int|null $time = null, ?array $page = null): void | Buffers one event. |
identify(string $userId, array $traits = [], ?string $anonymousId = null, DateTimeInterface|int|null $time = null): void | Buffers $identify. |
send(array $events, ?string $idempotencyKey = null): Receipt | Sends now. Throws MiraFive\MiraError. |
flush(): void | Sends the buffer. The package calls it for you. |
flags(): MiraFlags | The flags of the server source. |
forUser(Authenticatable $user): UserContext | The calls below with getAuthIdentifier() as the user id. |
optedOut(Request $request): bool | Whether the request carries Sec-GPC: 1 or DNT: 1. |
fake(): MiraFake | Swaps the facade for a recorder. |
UserContext (from forUser()):
| Method | Description |
|---|---|
track(string $name, array $properties = [], ?string $anonymousId = null, ?string $sessionId = null, DateTimeInterface|int|null $time = null, ?array $page = null): void | Buffers one event for the user. |
identify(array $traits = [], ?string $anonymousId = null): void | Buffers $identify for the user. |
flags(array $properties = [], array $consent = [], ?string $anonymousId = null, ?bool $optedOut = null): UserFlags | The user's flags. null reads the opt-out from the current request. |
Blade
| Directive | Description |
|---|---|
@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
| Method | Description |
|---|---|
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
| Symptom | Cause and fix |
|---|---|
| Nothing arrives | Run php artisan mirafive:check, then look for [mirafive] warnings in your log. |
unauthorized | MIRAFIVE_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_bearer | The website key ended up in MIRAFIVE_SECRET_KEY. Server code needs the secret key. |
InvalidArgumentException: A consentless client may not send userId | MIRAFIVE_MODE is consentless. Drop the identifiers, or switch to full where you hold consent. |
| No tracker tag | MIRAFIVE_WEBSITE_KEY is empty or MIRAFIVE_ENABLED is false. |
| Queued batches never arrive | No worker runs the configured queue, e.g. php artisan queue:work redis --queue=mirafive. |
| A flag always answers its fallback | Check 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.