fourlabs/gamp-bundle
Symfony bundle for Google Analytics Measurement Protocol, enabling server-side tracking of pageviews, events, and ecommerce hits without client-side JavaScript. Provides simple service integration and configurable tracking IDs for backend analytics.
First, the mismatch: this is a Symfony bundle, not a Laravel package. There's no service provider, no facade, no config/gamp.php. In a Laravel codebase you have two realistic paths:
fourlabs/gamp HTTP client. Composer-require that library directly, bind it as a singleton in a service provider, and skip the bundle entirely. This is the sensible option for Laravel.Http::post() wrapper of your own — ~30 lines, no dead dependency, and you control the GA4 migration.Minimal steps for path 1:
composer require fourlabs/gamp # the client, not the bundle
// AppServiceProvider::register()
$this->app->singleton(\FourLabs\Gamp\Gamp::class, fn () =>
new \FourLabs\Gamp\Gamp(config('services.ga.tracking_id'))
);
Where to look first: DependencyInjection/Configuration.php in the bundle tells you every tunable option and its default — that's the fastest way to understand what the package actually does. Then the Gamp class in the underlying library: its public methods are the API surface. Everything else is Symfony plumbing you'll be reimplementing.
First use case: server-side conversion tracking. The browser can't be trusted to fire a hit for order.completed (ad-blockers, redirects, API-only clients, webhook-driven subscription renewals). Send a transaction or event hit from the controller/job that actually changes state.
$gamp->event('purchase', ['category' => 'checkout', 'label' => $order->sku, 'value' => $order->total]);
Wrap it, don't scatter it. Injecting a concrete third-party class across 40 controllers guarantees you can't migrate off it later. Define an AnalyticsTracker interface with pageview() / event() / transaction(), and make the Gamp implementation one binding. When you move to GA4 — or to a different vendor — you change one class.
Always supply a client ID, and get it from the _ga cookie. This is the single biggest quality lever. Parse GA1.2.1234567890.1600000000 → 1234567890.1600000000 and pass it as cid. Without it you generate a UUID per request, every hit becomes a brand-new user, and your reports show inflated users, 100% bounce, and zero session continuity. If the cookie is absent (API client, ad-blocker), fall back to a UUID derived from a stable identifier you do have — but persist it, don't regenerate.
Queue everything. The client makes a blocking HTTP call to www.google-analytics.com. Inline in a request cycle, a slow/unreachable Google adds hundreds of milliseconds to your response. Push hits onto a Laravel job:
dispatch(new SendAnalyticsHit($payload))->onQueue('analytics');
Wrap the job body in try/catch and log-and-drop. Analytics must never fail a business transaction, and retrying a tracking hit forever is worse than losing it.
Use a decorator for the auto-tracking case. If you want fire-and-forget pageviews, a middleware (Laravel) or kernel.terminate listener (Symfony) is the pattern the bundle itself uses — but scope it hard: exclude /admin, /horizon, /telescope, health checks, and anything with Authorization headers. Server-side auto-pageviews silently duplicate your client-side gtag and produce double-counted sessions.
Batch when volume is high. The Measurement Protocol has a /batch endpoint accepting up to 20 hits per POST. If you're tracking per-item events on a large catalogue, one batched job beats 20 sequential HTTP calls.
Test against the payload, not the network. Because the client injects an HTTP transport, swap it in tests and assert the array you hand over — cid present, t correct, dp / ec / ev where you expect. That's a far more valuable test than asserting a 200 from Google.
Gate it by environment. A tracking_id of null in local and testing should make the tracker a no-op. Otherwise your local test runs pollute real property data and will show up in someone's weekly report.
2.5.0 is a constraint bump, not a fix. The only change is Symfony 5 support (#27). No code touched the endpoint, the payload, or the authentication — which is the part that matters. The package still posts to /collect with a UA-XXXX-Y property ID, and Universal Analytics stopped processing standard-property hits in July 2023. If you deploy this as-is, nothing will appear in Google Analytics and no error will be raised — /collect returns a 204 No Content regardless. You will spend a day debugging "missing data" before realising the endpoint is a black hole. GA4 needs a different host (/mp/collect), measurement_id (G-XXXX), and an api_secret query parameter. Treat this package as a pattern reference, not a dependency.
Symfony 5 support is still below modern Laravel. Symfony 5 maps to the Laravel 6–8 era; Laravel 9+ requires Symfony 6, and 11/12 are on Symfony 7. So the 2.5.0 bump gets you past the install error on an older app but still won't composer require cleanly in a current one. Composer tells you in seconds — but read the error instead of reaching for --ignore-platform-reqs, which trades an install error for a runtime one. And note the release only moved the bundle's constraint; the underlying fourlabs/gamp client is where the behavioural code lives, so confirm its status independently before assuming anything improved.
No response body means no confirmation. Even on the live endpoint, a successful hit returns an empty 204. To actually validate a payload, POST it to https://www.google-analytics.com/debug/mp/collect — it returns a JSON validationMessages array. Do this once per hit shape in staging, and log the debug output on the first failure in production. Never ship a new event schema without running it through debug first.
Custom dimensions are positional and configurable in the UI. You send cd1, cd2 — but GA only interprets those if dimension 1 and 2 are actually defined in the property settings, with matching scope (hit/session/user). A mismatch doesn't error; the data just lands nowhere. Keep a documented map of index → name in the repo, because the GA console is where people forget.
dp (document path) and dt (document title) are required for pageviews. Omit them and the hit is accepted but bucketed oddly — you'll see (not set) rows. Same story with ua (user agent): without it, Google classifies everything as a bot and drops it from reporting.
Consent and PII. Server-side hits are not exempt from GDPR. If the user declined analytics cookies, you must not send a hit — capture consent server-side and check it before dispatching. And never put emails, names, or address fragments in cd fields or event labels; the Measurement Protocol terms forbid PII and Google does periodically audit.
Extension points, if you stay on the bundle: the HTTP client is the seam. Replace it with a client that adds retry/backoff, a circuit breaker, or a logging decorator — that's cleaner than forking. The tracker service itself can be decorated with Symfony's decorates: to add no-op behaviour per environment without touching call sites. Both still work on the 2.5.0 release.
Set a sane timeout. If Google is unreachable and your HTTP client has no timeout, a queued analytics job can hang a worker indefinitely. Explicitly set connect/read timeouts of a couple of seconds — a dropped hit is cheap, a wedged queue is not.
How can I help you explore Laravel packages today?