setono/google-analytics-bundle
A Symfony bundle for Google Analytics integration that lets you configure tracking, send page views and events, and manage analytics settings from services and templates with minimal setup.
Read this first: this is a Symfony bundle, not a Laravel package. It ships a Bundle class, a DI extension, a config tree, and a Twig extension. You cannot composer require it into a Laravel app and expect anything to boot — there is no service provider, no Blade directive, no facade. If you are on Laravel, treat this assessment as "what the bundle does and how to port/replicate it"; if you are on Symfony, use it directly.
Minimal steps (Symfony):
composer require setono/google-analytics-bundleSetono\GoogleAnalyticsBundle\SetonoGoogleAnalyticsBundle in config/bundles.phpG-XXXXXXXXXX one — not a UA- property):# config/packages/setono_google_analytics.yaml
setono_google_analytics:
tracking_id: '%env(GOOGLE_ANALYTICS_ID)%'
<head>, in your base layout via the bundle's Twig extension (check src/Twig/ for the exact function name — the API surface here is small and autocompletes in your IDE):<head>
{{ google_analytics() }}
</head>
Where to look first, in this order:
src/DependencyInjection/Configuration.php — the only authoritative list of config keys. Docs are thin for a 1-star package; the config tree is the docs.src/Twig/ — the render entry point and whether it emits gtag.js inline or via a template partial.src/Renderer/ (or equivalent) — the service that builds the JS. This is your decoration/override point.First real use case: page-view tracking falls out of step 4 for free. The moment you get value is the first conversion event — a purchase fired from your success page or, better, a server-rendered dataLayer push immediately after checkout. That's the pattern you'll reuse for every other event.
Laravel reality check: the cheapest port is a Blade component (<x-ga4-snippet :id="config('services.ga4.id')" />) plus a small Ga4Events service that serializes events to a window.dataLayer.push(...) array in a @push('scripts') stack. You do not need this bundle to do that — but reading its renderer is a good reference for what GA4 actually requires (consent defaults, gtag('config', ...), send_page_view).
One snippet, many events. Rendering the snippet is the bundle's whole job; everything else is your event plumbing. Keep a single server-side service that accumulates events and renders them once per request — do not call the renderer per component, or you'll emit nested gtag('config') calls and duplicate page views.
Server-triggered, client-executed. The pattern that survives ad blockers and SPA navigations reasonably well: compute the payload in PHP after the business action (order placed, signup confirmed), stash it in the session/flash or a view variable, render it as a dataLayer.push on the next page. In Symfony you'd do this via an event subscriber on kernel.response or by decorating the renderer to append queued events. In Laravel: a Ga4Queue singleton resolved in a middleware that flushes into the Blade stack.
// the shape you want, regardless of framework
$events = [
['name' => 'purchase', 'params' => [
'transaction_id' => $order->number,
'value' => (float) $order->total,
'currency' => 'DKK',
'items' => $order->lines->map(fn ($l) => [
'item_id' => $l->sku,
'item_name' => $l->name,
'price' => (float) $l->price,
'quantity' => $l->qty,
])->all(),
]],
];
Consent gating is a first-class concern, not an afterthought. GA4 in the EU means gtag('consent', 'default', {...}) before the config call, then an update on banner accept. If the bundle renders the config unconditionally you'll either (a) violate consent requirements or (b) need to decorate the renderer to emit the consent block first. Budget for this on day one — retrofitting it after a legal review is painful because it changes snippet ordering.
Multi-domain / subdomain setups. Set the cookie domain explicitly if you track across www. and app subdomains; cross-domain measurement additionally requires linker config. This is the class of thing the bundle can't decide for you.
Don't double up with GTM. If you already load Google Tag Manager, do not also use this bundle to render gtag.js. Pick one loader. Running both produces duplicated page_view hits and confusing DebugView output — and it's the single most common misconfiguration in this space.
Testing. Assert on rendered output rather than on Google: decode the dataLayer JSON from the response body in a feature test and assert event names/params. Never make a real hit to google-analytics.com in CI.
Reporting is a different package. This bundle sends tracking data (client-side beacon). It does not read reports. If what you actually wanted was "show me pageviews in my dashboard", you want the GA4 Data API (google/analytics-data + a thin wrapper, or spatie/laravel-analytics on Laravel). Mixing up these two is a very common evaluation mistake.
UA- ID will silently no-op. Also confirm you're not pasting the property ID (a bare integer) — that's for the Data API, not for gtag.api_secret.script-src you need a nonce or hash. Check whether the renderer exposes a nonce option; if not, decorate it or move the bootstrap to an external file.debug_mode: true (or _dbg), open GA4 → Admin → DebugView, and watch the browser Network tab for POST /g/collect. If the request never appears, it's a JS/CSP/consent problem, not a bundle problem. If it appears but DebugView is empty, it's an ID/property mismatch.composer.json, read Configuration.php before trusting any blog post, and assume you may need to fork. The code is MIT and the surface is small — a fork is hours, not weeks, which is the main thing that makes this tolerable.decorates: setono_google_analytics.renderer) rather than editing vendor code — that's where consent blocks, nonces, and custom gtag config belong.gtag('js') → gtag('config') → events. Any deviation and GA4 drops or misattributes hits, and it will not warn you.How can I help you explore Laravel packages today?