jeffersongoncalves/laravel-gtm
Laravel GTM is a lightweight package for adding Google Tag Manager to Laravel apps. Set your container ID in config, and the package injects the GTM script and noscript fallback into your layouts, so you can manage analytics and marketing tags from the GTM dashboard without editing Blade templates.
The package injects third-party snippets (GTM and similar) into your rendered HTML from configuration rather than from Blade templates. The 3.1.0 release adds first-class CSP nonce support, which makes it viable for apps that ship a strict Content-Security-Policy.
Minimal setup:
composer require <vendor>/<package>
php artisan vendor:publish --tag=<package>-config
config/<package>.php (or the matching .env keys) instead of hardcoding them in app.blade.php.<x-...> component to add in the common case. Check the package's service provider registration if you use a custom render path (e.g. Inertia root templates, Lighthouse responses, PDF generation).First use case to validate end-to-end: a single GTM container on your public layout. Load the page, confirm the snippet appears once, and confirm the tag fires in GTM Preview before wiring anything else.
Where to look first in the source:
nonce="...".If your app sets a nonce via Vite::cspNonce() (common with laravel-security-headers 2.1+), you should see nonce="..." on the injected <script> tags with zero configuration changes. That is the headline behaviour of 3.1.0, and it is worth confirming on a staging environment on day one.
Config-driven snippets, one per environment. Keep the container ID in .env and reference it from config. A GTM_ID that is empty in local/testing and only populated in staging/production is the cleanest way to avoid polluting local page loads and test assertions.
Policy alignment in laravel-security-headers. Because the injected script now carries the nonce, your CSP directive can stay strict:
// config/security-headers.php (illustrative)
'Content-Security-Policy' => "script-src 'self' 'nonce-{nonce}' https://www.googletagmanager.com;",
Set the nonce once at the start of the request (the middleware from laravel-security-headers or your own) via Vite::cspNonce($nonce), and let the package pick it up. The GTM snippet forwarding the nonce to the gtm.js tag it creates is the important detail — without it you would need 'unsafe-inline' or a hash for the dynamically injected loader.
Feature-flag by route. Most apps want analytics everywhere except admin, health checks, and webhook endpoints. Rather than conditionals in Blade, do the filtering in config or by scoping the package's middleware to your web group and excluding route names. This keeps the injection logic in one place and testable.
Testing assertions. Write a feature test that hits your home route and asserts the container ID is present and appears exactly once:
$response = $this->get('/');
$response->assertOk();
$this->assertSame(1, substr_count($response->getContent(), config('<package>.gtm_id')));
For the nonce behaviour, a second test where you set the nonce explicitly and assert nonce="..." appears on the injected tag guards against regressions when you upgrade the package.
Multi-tenant and multi-domain. If you serve several brands from one codebase, resolve the container ID from the current tenant/domain in a config override inside a service provider's boot(), rather than publishing multiple config files.
Staged rollout. Because injection is response-level, you can enable it behind a percentage flag in a middleware without touching layout templates — useful for a low-risk trial before making analytics mandatory.
Nonce only flows if it is set before rendering. Vite::cspNonce() must be populated before the response is generated. If you set the nonce in a terminable middleware or after the response body is built, the snippet will render without it and your CSP will block the script — with no error in your application logs, only in the browser console. Symptom to recognise: page loads, no analytics, Refused to execute inline script in DevTools.
The forwarded nonce is the easy one to miss. The GTM snippet creates the gtm.js tag itself. 3.1.0 forwards the nonce to that created tag, so the inline bootstrap and its child script are both covered. If you have a customised or overridden snippet (see below), that forwarding will not be present.
Overriding the snippet kills the nonce handling. If you have published views or a custom renderer to tweak the snippet, you own the nonce attribute from now on. Prefer leaving the default renderer in place and using the config, otherwise you will silently re-open the CSP hole the release just closed.
Nothing changes for apps without a nonce. This is genuinely non-breaking: no nonce set means no attribute emitted, identical output to 3.0.x. Upgrading is low risk — but still drop the nonce attribute onto a staging page and check the browser console for CSP violations before promoting.
Nonce vs. hash vs. strict-dynamic. A nonce-based script-src is per-request. If you cache full-page HTML (CDN, response cache, Cache::remember on rendered output), a cached page will serve a stale nonce and be blocked. Either exclude nonce-bearing responses from the cache or move to a hash-based policy for those routes. This is the most common integration bug in apps that adopt nonces late.
Third-party scripts spawned by GTM still need their own allowances. The nonce covers the GTM loader; tags GTM injects at runtime are handled by strict-dynamic (if used) or need their own script-src entries. The release does not — and cannot — solve that for you.
Debugging order when analytics goes quiet: (1) view source, confirm the snippet is present; (2) confirm the nonce attribute matches the one in the response header; (3) check the browser console for CSP refusals; (4) check GTM Preview. Working in that order separates injection problems from policy problems from GTM configuration problems.
Verify the injected position. The snippet is inserted on response, so its location relative to your other scripts is controlled by the package, not your template. Anything that depends on ordering (a dataLayer push in your own inline script) should be checked after upgrading.
Extension points worth knowing: config values for enabling/disabling per environment, the renderer if you must customise markup (accepting the nonce caveat above), and the middleware registration if you need to scope injection to a subset of routes.
How can I help you explore Laravel packages today?