clicktrail / laravel
Laravel adapter for ClickTrail attribution: service provider + facade, capture middleware, queued delivery jobs, Blade components/directives, artisan diagnostics, first-party proxy stub, webhook signature verification, consent resolver interface.
Requires
- php: >=8.1
- clicktrail/php-sdk: dev-main
- illuminate/config: ^10.0|^11.0
- illuminate/http: ^10.0|^11.0
- illuminate/support: ^10.0|^11.0
README
English | Português | Deutsch | 中文
clicktrail/laravel
See which campaign, keyword, click ID and landing page created each form submission and conversion in any Laravel 10/11 app.
Index
- Why
- Installation
- Quick start
- Blade output
- Queued delivery
- Failed events and replay
- Consent
- First-party proxy
- Diagnostics
- Webhook signatures
- How it differs
- Testing
- License
Why
Most tracking packages store what a page showed. ClickTrail proves which campaign created the lead or sale. This package is a thin adapter over clicktrail/php-sdk: the SDK owns parse/classify/merge/serialize; Laravel owns capture middleware, consent-gated session persistence, queued delivery, Blade output, and artisan diagnostics.
Installation
composer require clicktrail/laravel
Package auto-discovery registers the service provider and the ClickTrail facade. Publish the config and fill CLICKTRAIL_SITE_ID and CLICKTRAIL_ENDPOINT in .env:
php artisan vendor:publish --tag=clicktrail-config
Quick start
clicktrail() is the main entry point (a ClickTrail facade also exists).
// 1. Register capture on the route groups whose traffic should build // first/last-touch state (routes/web.php): Route::middleware(['web', 'clicktrail.capture'])->group(function () { Route::get('/', fn () => view('welcome')); }); // 2. A visitor arrives from Google Ads; the middleware merges the touch. // After the request: session('clicktrail.attribution'); // JSON with first->source === 'google', first->clickIds['gclid'] set. // Persisted ONLY when consent permits analytics storage; unknown = denied. // 3. Inspect or merge from your own code via the helper: $state = clicktrail()->capture($request); // StoredState for this request clicktrail()->pendingPayloads(); // [] means nothing queued yet // 4. On conversion (form submit, order), queue delivery: \ClickTrail\Laravel\Jobs\DeliverEventsJob::dispatch(); // batched POST to CLICKTRAIL_ENDPOINT with idempotency keys; retries after // 200ms/1s/5s; nothing is sent during the request itself.
A direct visit afterwards changes nothing: first touch stays, stored last touch persists. That is the SDK's merge law, tested, not promised.
Blade output
{{-- renders the first-party loader <script> tag from config --}} @clicktrailHead {{-- hidden attribution inputs inside a <form>, so the server-side submit carries source / click IDs verbatim --}} @clicktrailAttribution {{-- normalized consent snapshot as data-ct-consent-* attributes --}} <div @clicktrailConsent>...</div> {{-- or as explicit components --}} <x-clicktrail::head /> <x-clicktrail::attribution-inputs />
Queued delivery
Events are never sent during the request. Dispatch the delivery job from your own triggers:
\ClickTrail\Laravel\Jobs\DeliverEventsJob::dispatch(); // flush() throws RetryableException on 429/5xx/network. Laravel re-runs the // job via backoff([200ms, 1000ms, 5000ms]). A PermanentException fails the // job and routes payloads to the failed-events table.
The job uses the BatchClient, so bind a PSR-18 client as Psr\Http\Client\ClientInterface plus PSR-17 request/stream factories before dispatching. Queue connection and queue name come from clicktrail.queue_connection / clicktrail.queue.
Failed events and replay
After all retries fail, payloads are stored verbatim in the clicktrail_failed_events table (clicktrail.persist_failed_events, default true). Diagnose, then replay through the same helper:
foreach (\ClickTrail\Laravel\Models\ClickTrailFailedEvent::get() as $row) { clicktrail()->restorePayloads(json_decode($row->payload, true)); } // payloads are back in the BatchClient queue; the next DeliverEventsJob run // sends them unchanged, same idempotency keys, no duplicates.
Consent
ClickTrail is a consent consumer, not a CMP. Implement ClickTrail\Laravel\Consent\ConsentResolverInterface with your CMP adapter and bind it, or set clicktrail.consent_resolver to its class name. Until then the shipped NullConsentResolver returns an unknown snapshot, which is treated as denied everywhere: no identifiers are persisted and no events are delivered. Setting clicktrail.consent_required to false declares this use needs no CMP gating.
First-party proxy (optional)
With CLICKTRAIL_FIRST_PARTY_PROXY=true the provider registers POST /clicktrail/collect. It minimally validates the batch payload shape and re-queues delivery through your own infrastructure.
Diagnostics
php artisan clicktrail:diagnose
Checks config presence, endpoint reachability (TCP-level flag), and consent resolver resolution.
Webhook signatures
Verify ClickTrail webhook callbacks with HMAC-SHA256 constant-time comparison:
\ClickTrail\Laravel\Support\WebhookSignature::verify($payload, $request->header('X-ClickTrail-Signature'), $secret); // === true only when the signature matches; constant-time, no timing leak
How it differs
| Package | What it does | Boundary |
|---|---|---|
| combindma/laravel-trail | Stores UTMs/referrers in cookies | ClickTrail proves which campaign created the lead or sale: deterministic first/last-touch merge laws validated by golden fixtures shared with our WordPress and GTM engines, consent-gated persistence, batched delivery with idempotency keys |
| DirectoryTree/Metrics | Counts anonymous events | Complementary: ClickTrail connects campaigns to identities and revenue |
See ../docs/COMPETITOR-NOTES.md for the full analysis.
Testing
php tests/_runner.php # full suite, standalone (no kernel boot) vendor/bin/phpunit --testdox # PHPUnit pass (CI, PHP 8.3)
CI lints all files and runs both stages on PHP 8.1–8.3 (.github/workflows/ci.yml).
License
MIT. Copyright (c) 2026 Vizuh OÜ