Drop-in offline conversion tracking for Laravel apps using Google Ads.
- Captures
gclid(andgbraid/wbraid) from incoming traffic and persists it across the visitor's session - Records conversion events with a one-line API
- Buffers in cache, syncs to your database, and uploads to Google Ads in batched, queued jobs
- Honors Google Ads' minimum reporting delay (default 6 hours)
- Bring-your-own model — implement a small contract or use the included
Leadmodel out of the box - Supports both call-site values (
record('Event', 100)) and config-mapped defaults
Status: pre-release. Not yet on Packagist.
composer require electrictomcat/laravel-google-ads-conversionsPublish the config and the migration:
php artisan vendor:publish --tag="laravel-google-ads-conversions-config"
php artisan vendor:publish --tag="laravel-google-ads-conversions-migrations"
php artisan migrateAdd these to your .env (see Google's OAuth setup for how to mint the refresh token):
GOOGLE_ADS_DEVELOPER_TOKEN=
GOOGLE_ADS_CLIENT_ID=
GOOGLE_ADS_CLIENT_SECRET=
GOOGLE_ADS_REFRESH_TOKEN=
GOOGLE_ADS_CUSTOMER_ID=123-456-7890Register the middleware in bootstrap/app.php:
->withMiddleware(function (Middleware $middleware) {
$middleware->web(append: [
\ElectricTomCat\GoogleAdsConversions\Http\Middleware\CaptureGclid::class,
]);
})Schedule the upload job in routes/console.php:
use ElectricTomCat\GoogleAdsConversions\Jobs\UploadPendingConversions;
use Illuminate\Support\Facades\Schedule;
Schedule::job(new UploadPendingConversions)->hourly();From anywhere in your app — controllers, jobs, Livewire components, observers:
use ElectricTomCat\GoogleAdsConversions\Facades\GoogleAdsConversions;
GoogleAdsConversions::record('Quote Form', 100);The first argument is your internal event name. The second is an optional value. The full signature is:
GoogleAdsConversions::record(
eventName: 'Quote Form',
value: 100.0, // optional — falls back to per-event config
currency: 'USD', // optional — falls back to per-event then package default
gclid: null, // optional — manually override the GCLID lookup
);Edit config/google-ads-conversions.php. Each event entry is either a string (just the action name) or an array with optional value/currency defaults:
'events' => [
// Simple: event name → Google Ads action name (or full resource path)
'Quote Form' => 'Quote Submission',
'Phone Call' => 'customers/1234567890/conversionActions/111111',
// With per-event default value/currency that the call site can still override
'Demo Booked' => [
'action' => 'Demo Booked',
'value' => 250.00,
'currency' => 'USD',
],
// Catches any event named "Page Navigation: /anything" by prefix
'Page Navigation' => 'Page Navigation',
],The call site always wins — record('Demo Booked', 999) overrides the config's 250.00. Omit the value at the call site to use the config default.
The package ships a Lead model and matching migration that work out of the box. If you'd rather use your own model — say, you already have a Visitor table and want to track conversions there — implement the HasConversions contract.
The fastest path: drop the HasConversionsTrait onto your existing model:
use ElectricTomCat\GoogleAdsConversions\Contracts\HasConversions;
use ElectricTomCat\GoogleAdsConversions\Models\Concerns\HasConversionsTrait;
use Illuminate\Database\Eloquent\Casts\AsCollection;
class Visitor extends Model implements HasConversions
{
use HasConversionsTrait;
protected $fillable = ['gclid', 'visitor_id', 'conversions', /* ... */];
protected $casts = [
'conversions' => AsCollection::class,
];
}Make sure your table has at minimum these columns:
gclid(string, unique, indexed)visitor_id(uuid, nullable)conversions(json, nullable)
Then point the package at it:
// config/google-ads-conversions.php
'model' => \App\Models\Visitor::class,For full control, implement HasConversions from scratch — see src/Contracts/HasConversions.php for the contract.
- Middleware (
CaptureGclid) — runs on the landing request, extractsgclidfrom the URL, sets cookies + session, buffers a stub lead record in cache. - Recording (
GoogleAdsConversions::record()) — pushes a conversion entry into a per-gclid cache bucket. Cheap. Fire from anywhere, including HTTP requests where the user has nogclidon the URL but does have one in their session/cookie. - Sync (
syncToDatabase()) — flushes the cache buffer into the configured model's table. Runs as the first half of the queued job. - Upload (
uploadPendingConversions()) — finds every pending conversion older than the delay window and ships eligible batches to Google Ads viaUploadClickConversions. Marks each shipped conversion as'uploaded'with a timestamp.
composer testThe suite uses Pest 4 + Orchestra Testbench against an in-memory SQLite database.
See CHANGELOG.md.
Issues and pull requests welcome.
- Tom Michael
- Built on top of the Spatie package skeleton and
googleads/google-ads-php
The MIT License (MIT). See LICENSE.md.