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

devpro/laravel-ga4-event-tracking

Server-side Google Analytics 4 event tracking for Laravel. Send GA4 events directly from your backend via the Measurement Protocol with a clean, fluent API, queue support, and automatic user and request context. No client-side JavaScript required.

View on GitHub
Deep Wiki
Context7

Getting Started

Install

composer require devpro/laravel-ga4-event-tracking

The service provider and GA4 facade are auto-discovered. Publish the config:

php artisan vendor:publish --provider="Devpro\GA4EventTracking\GA4EventTrackingServiceProvider"

Credentials. You need two things from GA4, not the gtag.js snippet:

  1. GA4_MEASUREMENT_ID — Admin → Data Streams → your web stream → "Measurement ID" (G-XXXXXXX).
  2. GA4_API_SECRET — same screen → "Measurement Protocol API secrets" → Create. This secret is the thing people forget; without it every request 204s and nothing shows up.
GA4_MEASUREMENT_ID=G-XXXXXXXXXX
GA4_API_SECRET=abc123...
GA4_ENABLED=true

First use case. The reason to reach for this package at all is the event the browser can't be trusted to send: a completed purchase. Ad blockers, iOS ITP, and closed tabs eat client-side purchase events; a server-side call from your order-paid listener does not.

use Devpro\GA4EventTracking\Facades\GA4;

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

Where to look first. The package is a thin wrapper around POST https://www.google-analytics.com/mp/collect — read the source (it's small: config, service provider, one client class, one facade) before you build on it. Then read Google's Measurement Protocol event reference for parameter names; the package will happily forward whatever array you give it, valid or not.

Verify it works. Send a test event, then open GA4 → Reports → Realtime. Standard reports lag 24–48h.

Implementation Patterns

Always set a client_id or user_id. GA4 will accept the POST without one and then silently discard the event. The cleanest approach is to capture the _ga cookie (format GA1.1.1234567890.1699999999 — take the last two dot-separated parts) when the user is on-site, stash it on the order/user record, and replay it server-side. If you have no cookie, generate a UUIDv4 and persist it per-user so the server-side event stitches to the same pseudo-identity over time.

GA4::setClientId($order->ga_client_id)
    ->event('purchase', [...]);

Wrap every call in a queued job. The HTTP call is synchronous; you don't want a slow Google endpoint in the request cycle of a checkout.

class TrackGA4Event implements ShouldQueue
{
    public int $tries = 3;
    public array $backoff = [10, 60];

    public function __construct(private string $name, private array $params) {}

    public function handle(): void
    {
        GA4::event($this->name, $this->params);
    }
}

TrackGA4Event::dispatch('purchase', $payload)->afterCommit();

afterCommit() matters — dispatching inside a DB transaction otherwise fires the job before the row exists.

Map your domain events, don't invent names. GA4's built-in reports (ecommerce funnel, retention, acquisition) only populate for recommended event names: purchase, sign_up, login, add_to_cart, begin_checkout, refund, generate_lead, trial_start. Use custom names only for things GA4 has no shape for; those events land in "Events" but only get parameter breakdowns if you register custom dimensions (Admin → Custom definitions), which is a separate manual step per parameter.

Centralise the payload shape. Create one GA4Payload builder or AnalyticsEvent class rather than sprinkling arrays through controllers — you'll want one place to add currency, user_properties, and a debug_mode flag.

GA4::event('sign_up', ['method' => 'google']);
GA4::setUserProperties(['plan' => 'pro', 'company_size' => 'smb']);

Test with Http::fake(). No SDK mocking needed — assert the outbound request instead, which is the only contract that actually matters:

Http::fake();
GA4::event('purchase', ['transaction_id' => 'T-1', 'value' => 42.00, 'currency' => 'USD']);

Http::assertSent(fn ($req) => str_contains($req->url(), '/mp/collect')
    && $req['events'][0]['name'] === 'purchase'
    && $req['events'][0]['params']['value'] === 42.00);

Use the validation endpoint while developing. GA4's /debug/mp/collect returns a JSON body describing schema errors; the normal endpoint returns 204 regardless. If the package exposes a debug/validation switch, enable it in local and keep it off elsewhere. Otherwise, temporarily point the base URL at the debug host in a service provider.

Feature-flag it. GA4_ENABLED=false in testing and local by default. You do not want CI runs or a developer's laptop polluting production analytics — and once a fake event is in a property it cannot be deleted.

Gotchas and Tips

204 means nothing. Success, bad payload, wrong measurement ID, revoked secret — all return 204 No Content on the live endpoint. Silent failure is the default behaviour. If you need to know it worked, either use the debug endpoint or watch Realtime, and never assume "no exception" = "tracked".

client_id is not optional. This is the single most common cause of "my events never appear." Also note that a user_id alone does not substitute — GA4 still wants a client_id to attach the hit to a session.

No session stitching for free. Server-side events don't carry the browser's session_id or engagement_time_msec, so they can inflate session counts or land in a separate session. Pass session_id if you captured it, and pass engagement_time_msec for events you want counted in session-based metrics.

Backdating is capped at 72 hours. timestamp_micros lets you replay historical events, but anything older than 72h is dropped. Backfills for old orders won't work — plan for a clean cutover date.

PII will be dropped. Names, emails, phone numbers, and street addresses in params are ignored (and risk account issues). Hash or omit. This trips people up on purchase payloads that include email for "enhanced conversions" — that's a different pipeline.

One property per config. The measurement ID and secret are global config values. Multi-tenant or multi-brand setups need to swap config at runtime (config(['ga4.measurement_id' => ...])) before each call, or bind a fresh instance of the underlying client per tenant. Verify which mechanism the package actually supports before designing around it.

No built-in queue, retry, or batching. Everything is one synchronous HTTP request per event. High-volume apps should queue (see above), and if you're sending many events for one user action, note the Measurement Protocol accepts up to 25 events in a single payload — the package likely doesn't batch for you.

Config cache. php artisan config:cache after changing env vars, or your new API secret won't load. Classic deploy gotcha.

Pin the version and vendor-review it. 21 stars, zero dependents, single maintainer — that's fine for a ~200-line wrapper, but don't treat it as infrastructure. composer require "devpro/laravel-ga4-event-tracking:^x.y" with a caret on the minor, keep a test that asserts the outbound request shape, and be prepared for the fallback: Http::post('https://www.google-analytics.com/mp/collect', ...) is four lines if the package ever stalls. The value it adds is config handling and a facade, not protocol logic.

Consent is still yours to solve. Server-side tracking sidesteps ad blockers, not GDPR. Gate the calls on the same consent flag your front-end uses, and document that order records retain the _ga client ID — that's personal data in most jurisdictions.

Debugging checklist when events don't show: (1) Is GA4_ENABLED true and config cached? (2) Is there a client_id in the request body? Use Http::fake() or a Log tap to dump the payload. (3) Is the API secret from the same stream as the measurement ID? (4) Realtime only shows a rolling 30-minute window — check within minutes, not tomorrow. (5) Custom parameter not appearing in reports → you skipped the custom dimension registration.

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.
adachsoft/changelog-linter
adachsoft/collection
payerurl/telegrambot-crypto-payment-instant-payout
dskripchenko/laravel-hybrid-search
riorizkyrainey/lara-ced
mehedimi/static-vel
zharp/zharp
nonsapiens/bigquery-model-sync
lgu-bayambang-ict-office/lgu-laravel-development-standards
andydefer/laravel-support
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel
draftsmaninc/draftsman
square-bit/laravel-volition
himito/laravel-mailman
htinlynn/genius-helper
maco-studios/hephaestus
adachsoft/php-code-style