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

Laravel Ga4 Event Tracking Laravel Package

luketowers/laravel-ga4-event-tracking

Server-side Google Analytics 4 event tracking for Laravel. Send events to GA4's Measurement Protocol from your backend using a simple, fluent API, keeping credentials out of the browser and giving you full control over the payloads you send.

View on GitHub
Deep Wiki
Context7

Getting Started

The package wraps the GA4 Measurement Protocol — it sends events to Google Analytics from your server. It is not a reporting/read package (that's spatie/laravel-analytics territory), and it is not a replacement for gtag.js; it's the thing you reach for when the browser can't or won't report (ad blockers, iOS ITP, post-purchase webhooks, refunds, backend-only flows, offline conversions).

  1. Install — composer require luketowers/laravel-ga4-event-tracking. On Laravel 13 you need v2.2.1 or later (that's the entire content of this release — the illuminate/* constraint was widened to ^13.0). On Laravel 11/12, ^2.2 resolves fine.
  2. Publish the config — php artisan vendor:publish and select the package's config tag, which lands config/ga4-event-tracking.php.
  3. Get credentials — GA4 Admin → Data Streams → your web stream → Measurement Protocol API secrets → Create. You need the Measurement ID (G-XXXXXXX) and that API secret. Put them in .env as GA4_MEASUREMENT_ID and GA4_API_SECRET. Never commit the secret; it grants write access to your property.
  4. First event — send a server-side purchase from an order-paid listener:
use LukeTowers\GA4EventTracking\Facades\GA4;

GA4::event('purchase', [
    'transaction_id' => $order->number,
    'value'          => $order->total,
    'currency'       => 'USD',
    'items'          => $order->lines->map(fn ($l) => [
        'item_id'   => $l->sku,
        'item_name' => $l->name,
        'price'     => $l->unit_price,
        'quantity'  => $l->qty,
    ])->all(),
]);

The first use case worth building is almost always purchase reconciliation: your browser-side purchase is lost for a meaningful slice of users, and a server-side duplicate is deduplicated by GA4 on transaction_id, so sending it is safe.

Look first at: the published config file (it exposes the client ID, session ID, user ID, debug_mode, and engagement-time knobs you will need to set per-event), then the facade's method list in the README — the surface is small, roughly event(), setUserId(), setClientId(), setSessionId(), plus config-driven defaults.

Implementation Patterns

Don't call it inline in a controller. The package issues a synchronous HTTP request inside the request lifecycle. Put it behind a queued job, or dispatch your own event and let a queued listener do the work. Wrap in DB::afterCommit() if the event is tied to a write, so you never report a purchase that gets rolled back.

Stitch the server event to the browser session. This is the pattern that separates a working integration from a pile of "direct / (none)" traffic. On the front end, read the _ga cookie (GA1.1.<client_id>.<timestamp> — the client ID is the last two dot-separated segments) and the _ga_<STREAM_ID> cookie, POST them to your backend at checkout, and persist them on the order. Then at event time:

GA4::setClientId($order->ga_client_id);
GA4::setSessionId($order->ga_session_id);

GA4::event('purchase', [...]);

Without a client_id, Measurement Protocol still accepts the hit but attributes it to a synthetic user — no source, no session, no attribution. Setting user_id as well as client_id is what gets you cross-device stitching; setting only user_id is not a substitute for client_id on web streams.

Model it as a domain event, not a call site. Define App\Events\OrderPaid, and one queued listener that owns all GA4 mapping. When marketing asks for a new parameter six months from now you change one file, and you can replay historical events from the queue.

Centralise the payload shape. GA4 is strict about items arrays and silently drops malformed params. Build a small Ga4Payload/mapper class (or per-event data objects) that converts your models into GA4's expected keys — item_id, item_name, price, quantity, item_category. Treat the GA4 parameter reference as your schema; you'll be reading it often.

Use it for the things the browser can't do: refunds (refund with the original transaction_id), subscription renewals, sign_up from an API, lead creation from a webhook, and backfilling a historical import. These are the highest-value uses and the ones no client-side tag can cover.

Testing: Http::fake() on *.google-analytics.com/** (and the debug/mp/collect endpoint) and assert the JSON body. Assert both URL query params (measurement_id, api_secret) and the client_id, events[0].name, and events[0].params shape. Without fakes your test suite fires real hits into your production property.

Gotchas and Tips

  • The endpoint always returns 204. A green HTTP status means nothing — Google accepts garbage payloads and drops them server-side. During development, point the client at https://www.google-analytics.com/debug/mp/collect (the package's debug_mode flag helps too); the response there contains a validationMessages array telling you exactly which parameter is wrong. Do this before you trust a single event.
  • A client_id with no engagement_time_msec shows up in Realtime but vanishes from standard reports. If events "disappear" overnight, this is almost always why. Set a non-zero engagement time in config or per event.
  • Custom parameters require registration. A params key that isn't a GA4 default won't appear in any report until you register it as a custom dimension in the GA4 admin, and it won't be usable in audiences. Decide your custom parameter names up front, in snake_case, and register them the day you ship.
  • config:cache will bite you. All credentials live in config/ga4-event-tracking.php reading from env(). If you change GA4_API_SECRET and don't php artisan config:cache, you'll spend an hour debugging hits that are silently dropped for an invalid secret.
  • No idempotency for non-purchase events. GA4 only deduplicates on transaction_id for purchase. Retried jobs will double-count sign_up and refund. Guard with a ga4_reported_at column or a unique queued job, and make your listener idempotent — queued listeners will retry.
  • Validate the API secret's blast radius. It's write-only, but anyone with it can pollute your property. Keep it in env/vault, not in a DB settings table.
  • Latency is real. Realtime shows events in minutes; standard reports take 24–48 hours. Don't ship on Friday and expect Monday-morning numbers to be complete.
  • Extension point: if you need custom transport (proxy, retry policy, batching, logging), swap the underlying client binding in a service provider rather than sprinkling GA4::event() calls through the app — the package's client is a thin wrapper over Laravel's HTTP client, so binding your own implementation is the clean seam.
  • This release is a compatibility bump only. Nothing is deprecated and nothing breaks if you're already on 2.2.x; the sole change is the widened illuminate/support constraint for Laravel 13. If you're upgrading the app to Laravel 13, this is your floor — and it's a good moment to check whether the package's HTTP call is still blocking your checkout path and to confirm your client_id stitching survived the framework upgrade's cookie handling changes.
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.
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
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource