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

Php Ga4 Laravel Package

alexwestergaard/php-ga4

A lightweight PHP client for Google Analytics 4 that sends server-side events through the GA4 Measurement Protocol. Build and dispatch events with a simple, fluent API—great for tracking purchases, signups, and conversions from Laravel or any PHP backend, no client-side JavaScript required.

Deep Wiki
Context7

Getting Started

Install and wire up credentials first. Pull the package in with Composer and publish/set up its config (or instantiate it directly). You need two things from GA4 admin before anything works:

  1. Measurement ID (G-XXXXXXXXXX) — Admin → Data Streams → your web stream.
  2. API secret — Admin → Data Streams → your stream → Measurement Protocol API secrets → Create.

Store the secret in .env (GA4_MEASUREMENT_ID, GA4_API_SECRET) and expose it through config/services.php. Never ship the secret to the browser.

Where to look first: the README's quick-start, then src/ for the event/parameter classes and the transport class. Most of your day-to-day work will be constructing events and passing them to the client, so skim the list of pre-built event classes before hand-rolling arrays — the package usually ships typed builders for purchase, add_to_cart, page_view, sign_up, etc.

First use case that pays for itself: server-side conversion tracking. Fire a purchase event from your order-paid webhook or checkout success handler. The browser gtag call can be blocked by ad blockers; a server-side send from Laravel bypasses that and gives you a reliable revenue signal:

$ga4->event('purchase', [
    'transaction_id' => $order->number,
    'value'          => (float) $order->total,
    'currency'       => $order->currency,
    'items'          => $order->lines->map(fn ($l) => [
        'item_id'   => $l->sku,
        'item_name' => $l->name,
        'price'     => (float) $l->unit_price,
        'quantity'  => $l->qty,
    ])->all(),
], clientId: $order->ga_client_id, userId: (string) $order->user_id);

If you see the event in GA4 Realtime, you're done. If you don't, switch to the debug endpoint first (see Gotchas) rather than guessing at parameters.

Implementation Patterns

Bind it as a singleton in AppServiceProvider. The client is stateless per request, so a singleton built from config is fine:

$this->app->singleton(Ga4::class, fn () => new Ga4(
    config('services.ga4.measurement_id'),
    config('services.ga4.secret'),
));

Inject it via constructor wherever you need it; don't resolve it ad hoc from the container in ten different places.

Wrap sends in queued jobs. A network call inside the request cycle adds latency and couples checkout to Google's uptime. Dispatch a SendGa4Event job (with $tries, a short retryUntil, and backoff) from a domain event listener. This also gives you a clean retry path when GA4 returns a transient error, and keeps the HTTP call off the hot path.

Persist the client_id and session_id. GA4's Measurement Protocol needs a client_id to attribute events to a user. The most common pattern is to read the _ga cookie (GA1.1.<client_id> — strip the first two segments) on the incoming request and store it on your order/user/lead record. For session attribution, also capture session_id from the _ga_<container> cookie (it's the numeric portion after GS1.1.) and send it plus engagement_time_msec on every event — otherwise events can land in "Unassigned" traffic reports.

Model domain events explicitly. Create a small mapping layer rather than sprinkling raw event names through controllers:

// app/Analytics/Ga4Event.php
final class OrderPaid implements ShouldQueue
{
    public function toGa4Payload(): array { /* purchase */ }
}

That gives you one place to update when GA4 renames a parameter or your marketing team changes what counts as a begin_checkout.

Use server-side tagging as a switch, not a rewrite. If you route to a GTM server container instead of GA4 directly, you typically point the client at your container endpoint (https://tags.yourdomain.com/...) and let the container forward to GA4, Meta, etc. Keep this behind config (GA4_ENDPOINT) so staging can hit a test container and production the live one. The package's server-side-tagging support is the reason to prefer it over hand-rolling Http::post() — endpoint selection, batching, and payload shape are already handled.

Batch where you can. Multiple events fired from the same request (e.g. view_item_list + select_item on a listing page) should go in one request where the library allows it. It halves the round trips and reduces the chance of per-IP rate limiting.

Testing. Fake the outbound call — Http::fake() if the package is built on the Laravel HTTP client, otherwise bind a fake transport via the extension point. Assert on the JSON body's events[0].name and params.transaction_id, not on the whole payload (GA4 adds fields).

Gotchas and Tips

The debug endpoint is the only sane way to validate. Hit .../debug/mp/collect instead of /mp/collect and you get a validationMessages array back listing every malformed parameter. Wire a GA4_DEBUG env flag that swaps the endpoint or adds "debug_mode": 1 to params — the latter also makes events show up in GA4 DebugView. Use it on staging for every new event; the production endpoint silently accepts and silently drops bad payloads.

Measurement Protocol cannot start a session. It can only attach events to a session that already exists. Firing a purchase from a webhook days after the fact, with no client_id/session_id, will often land unattributed or be discarded. Capture identifiers at the moment the user is on-site and carry them forward; don't try to reconstruct them server-side.

client_id or user_id is mandatory. A payload with neither returns a 200 and records nothing. This is the single most common cause of "it says success but no data."

Parameter limits are strict and silent. 25 params per event, 25 per item, names ≤ 40 chars, string values ≤ 100 chars, alphanumeric + underscores only. Oversized payloads are rejected, not truncated. Validate before sending.

Event names are restricted. Must start with a letter, alphanumeric + underscores, ≤ 40 chars. Names beginning with ga_, firebase_, google_, or gtag_ are reserved and rejected. Don't namespace events with your app name if it collides with those prefixes.

Backdating is capped at 72 hours. timestamp_micros older than that is dropped. If you're replaying historical orders from a migration, don't expect them to land — and absolutely don't use timestamp_micros to fake order dates for reporting purposes; report off your own DB instead.

Currency is required for any monetary event, and the value must be a number, not a string. "value" => "19.99" frequently fails or is coerced inconsistently. Cast with (float).

User properties use last-write-wins semantics. Sending user_properties on every event is fine, but the most recent value on every event overwrites prior ones — so don't send a "plan" property only on signup and expect it to persist across events that omit it. Either always send it or accept the drift.

Consent signals matter for EU traffic. If you've wired Google Consent Mode, forward the gcs and gcd parameters through the Measurement Protocol payload so server-side hits respect the same consent state as the browser. Otherwise server-side events will look like they escaped your cookie banner.

Rate limiting exists even if undocumented. Large backfills or high-traffic endpoints can trigger 429s. Respect them with exponential backoff in the job, and consider batching (up to 25 events per request) rather than parallelizing single-event calls.

Server-side tagging strips user context. If you route through a GTM server container, remember Google no longer sees the browser's IP or User-Agent automatically unless you forward them. The container may geo-resolve to your server's location and misattribute traffic geography. Forward X-Forwarded-For / User-Agent explicitly if geo matters.

Extension points worth knowing: a custom transport/HTTP client (to add logging, auth headers, or a Guzzle mock in tests), custom event classes if the built-in list doesn't cover your vertical (e.g. SaaS-specific trial_started), and a payload middleware/interceptor if you need to inject user_properties, consent flags, or debug_mode uniformly without touching every call site.

Pin the version and watch the release cadence. With a 63-star library and an Unlicense license, there is no vendor SLA. Lock the minor version in composer.json and wrap the client behind your own thin interface — if the project goes dormant, you can swap the transport for a hand-rolled Http::post() to https://www.google-analytics.com/mp/collect in an afternoon without rewriting every call site.

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.
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package