freema/ga4-measurement-protocol-bundle
Symfony bundle for sending server-side events to Google Analytics 4 via the Measurement Protocol, with straightforward configuration for tracking conversions and user activity from your PHP application.
Feature and roadmap decisions around server-side measurement. This package is a client for GA4's Measurement Protocol — the HTTP endpoint that lets your backend send events to GA4 without a browser. That unlocks a specific, valuable slice of the roadmap:
client_id + session_id + event name lets you reconcile server events against client-side gtag events, or intentionally route a given event to one path only.Build vs. buy. The underlying work is a signed POST to /mp/collect with api_secret, client_id, and an events array. That is genuinely a one-to-three-day build. The package's value is typed payload construction, DI configuration, and a testable seam. Treat it as a thin adapter, not a platform.
What this is not. It does not read data. It does not replace the GA4 Data API, BigQuery export, or a CDP. It is one-way, fire-and-forget event ingestion.
Caveat on fit: despite being filed under a Laravel/PHP search, this is a Symfony bundle. In Laravel you'd either use the underlying client library standalone or bind it into the container yourself. Budget a half-day for that adapter work — and note that the bundle's default user-ID handler is built on Symfony's Request::getSession(), which has no Laravel equivalent. You will be writing your own handler regardless (see below).
Consider it when:
^1.2.1 or later. See the release note below — earlier versions silently dropped every event originating outside a session.Look elsewhere (or write it yourself) when:
204 and silently drops malformed events. Release 1.2.1 removes one internal cause of silent drops, but the protocol-level one remains: you need your own validation pass against /debug/mp/collect, plus queueing, retries, and a dead-letter log. If those are must-haves, the wrapper saves you little.Release 1.2.1 in context — what changed and why it matters:
DefaultCustomUserIdHandler called Request::getSession(), which throws on stateless routes and API requests; the resulting error meant send() failed and the event was never transmitted. Server-side conversion events overwhelmingly come from queues, webhooks, and API endpoints — the exact routes that had no session. Anyone who piloted this package on a queued purchase event before 1.2.1 may have been measuring zero, not a conversion rate. Validate any prior pilot data before drawing conclusions from it.user_id that is an array or a non-stringable object was previously sent as the literal string "Array" — polluting user-level reporting. It is now ignored. Good hygiene, but it also means the package will quietly decline to attach a user ID rather than complain; instrument that.For executives — one paragraph: A low-cost way to stop losing conversion signal to ad blockers and to get backend-only revenue events (renewals, refunds, offline closes) into GA4, so the attribution numbers we report match the numbers finance sees. Effort is days, not weeks. The latest patch fixed a defect that had been silently suppressing events from backend and API routes — the very events we care about — and the project is now actively tested across current PHP and Symfony versions. The residual risk is that it's a tiny open-source dependency with a single maintainer, so we mitigate by wrapping it behind an internal interface and vendoring the code — we can replace it without touching product code.
For engineering — the actual ask: Adopt this as a thin adapter, not a framework, and pin ^1.2.1 as the floor. Concretely:
AnalyticsClient interface (track(event, userId, properties)); implement it with this package.measurement_id and api_secret via env, never in code.DefaultCustomUserIdHandler. It is session-bound and unusable from Laravel API routes, queue workers, and jobs — the exact contexts we need. Implement your own user-ID handler (or pass user_id explicitly per event) and register it. This is also the class the 1.2.1 fix touched, so keep our usage on the explicit side of it.client_id/session_id from the existing _ga cookie where present; generate and persist a first-party ID where it isn't. Treat absence explicitly rather than letting the client omit the field./debug/mp/collect endpoint before enabling in production.Sequencing recommendation: Ship one high-value event end-to-end first — purchase, deduplicated against the existing client-side gtag event — verify in GA4 DebugView and Realtime from a queued/job context, not a controller, confirm no double-counting for one full billing cycle, then expand to renewals and refunds. That single slice is what proves or kills the business case; everything after it is repetition. If a prior pilot was run on 1.2.0, re-baseline it before quoting results.
How can I help you explore Laravel packages today?