jeffersongoncalves/filament-gtag
A Filament plugin that adds Google Analytics gtag.js tracking to your admin panel, with configurable measurement IDs and simple setup for monitoring panel usage and visitor activity.
Install all layers. The plugin is a Filament UI on top of jeffersongoncalves/laravel-gtag, spatie/laravel-settings, and (as of 3.4.0) filament-analytics-core ^3.1:
composer require jeffersongoncalves/filament-gtag
Pulling the plugin should drag in the rest; verify with composer show jeffersongoncalves/filament-gtag and check that filament-analytics-core resolves to >=3.1 — the new navigationGroup() API depends on it.
Publish and migrate. spatie/laravel-settings stores values in the database, so a migration is required before the UI can save anything:
php artisan vendor:publish --tag=filament-gtag-config
php artisan vendor:publish --tag=laravel-gtag-config # if you need script-level overrides
php artisan migrate
If the plugin ships its own settings migration tag (commonly filament-gtag-settings or settings-migrations), publish that instead — check vendor/jeffersongoncalves/filament-gtag/database/settings first.
Register the plugin on the panel you want the settings page to appear in:
use JeffersonGoncalves\FilamentGtag\FilamentGtagPlugin;
public function panel(Panel $panel): Panel
{
return $panel
->plugins([
FilamentGtagPlugin::make()
->navigationGroup('Integrations'), // new in 3.4.0
]);
}
Omit navigationGroup() and the page keeps its translated default group, exactly as before.
Where to look first.
config/filament-gtag.php — navigation group, page slug, access gate.src/Settings/GtagSettings.php (or src/GtagSettings.php) — the actual settings class. This is the source of truth for property names (enabled, tracking_id, measurement_id, etc.).src/Filament/Pages/… — the form schema; it mirrors the settings properties.src/FilamentGtagPlugin.php — the plugin's public fluent API (navigationGroup(), and whatever else has accreted since 1.x). Read this before subclassing anything.resources/lang/ — 17 locales as of 1.3.0.First use case: open the new "Google Analytics" page in the panel, toggle it on, paste a GA4 measurement ID (G-XXXXXXXXXX), save, then load a front-end page and search the HTML source for googletagmanager.com/gtag/js. If the script is absent, the problem is almost always in step 2 (migration) or the front-end layout not using the hook that laravel-gtag injects into.
Read settings in application code, not the DB directly. Always resolve the typed settings object — it is cached and respects spatie's serialization:
use JeffersonGoncalves\FilamentGtag\Settings\GtagSettings;
$settings = app(GtagSettings::class);
if ($settings->enabled && $settings->tracking_id) {
// e.g. pass to a Blade view, a consent banner, or a queued event
}
Gate the plugin per panel. In a multi-panel app you usually only want the settings page on an admin panel, and often only in production:
->plugins([
...(app()->isProduction() ? [FilamentGtagPlugin::make()] : []),
])
File the page into your own nav group (3.4.0). Instead of subclassing the page just to override getNavigationGroup(), pass the group at registration. The argument is string|Closure, so the group can be computed per-request — useful for role-dependent menus or a group name that comes from a translation key:
FilamentGtagPlugin::make()
->navigationGroup(fn () => __('admin.groups.marketing'))
Keep the string identical to the group you already use for your other marketing pages; Filament matches groups by label, and a typo silently creates a second group rather than erroring. If your panel groups are declared as enums or constants, use those — don't hand-roll the string in two places.
Seed defaults for reproducibility. Because settings live in the DB, an empty settings table row on a fresh deploy means the plugin's page renders with nulls. Add defaults in a seeder rather than relying on the form:
app(GtagSettings::class)->enabled = app()->isProduction();
app(GtagSettings::class)->tracking_id = config('services.gtag.id');
app(GtagSettings::class)->save();
This is also the cleanest way to keep secrets out of the panel — keep the DB value blank and inject the ID at deploy time.
Programmatic writes for support workflows. A common pattern: an artisan command or an admin action that flips analytics off during an incident without a deploy:
$settings = app(GtagSettings::class);
$settings->enabled = false;
$settings->save(); // clears the settings cache for you
Pair with the underlying package's config. laravel-gtag typically owns where the script is injected (a Blade directive or a view hook in your layout). The Filament plugin owns what the ID is. Keep the script tag in resources/views/layouts/app.blade.php and let the plugin supply the values — don't duplicate the gtag.js snippet by hand.
Testing. Use RefreshDatabase and run the settings migrations in TestCase. In tests, set settings explicitly instead of hitting the Filament page:
app(GtagSettings::class)->fill(['enabled' => true, 'tracking_id' => 'G-TEST123'])->save();
$this->get('/')->assertSee('G-TEST123', false);
If you assert on navigation placement, assert the rendered group label rather than introspecting the plugin object — that covers the Closure form too.
Local/staging hygiene. Give non-production panels a separate settings scope or simply leave the plugin unregistered and the enabled flag false. Nothing is worse than staging traffic polluting a production GA4 property.
save() invalidates it, but direct DB edits (or a restore from a dump) do not. Run php artisan settings:clear-cache — and remember to run it in your deploy script if settings are ever written outside the app.navigationGroup() requires filament-analytics-core >= 3.1. If you've pinned the core package lower (or an old composer.lock kept it back), the method either doesn't exist or is ignored and the page silently stays in the translated default group. If the group doesn't change after upgrading the plugin, check composer show filament-analytics-core before touching your panel code.anonymize_ip) you must touch the settings class, its migration (spatie settings migrations are additive per property group), and the Filament form schema. Missing the migration yields a silent null or a MissingSettingsMigration-style error.G-…; legacy Universal Analytics UA-… IDs are dead. If the plugin's form doesn't enforce a pattern, add ->regex('/^G-[A-Z0-9]+$/') to the field or you will ship a broken tag that fails silently in the browser (gtag logs a console warning but nothing else).Content-Security-Policy, googletagmanager.com must be allowlisted and any inline gtag bootstrap needs a nonce. The plugin does not solve this; expect a blank analytics dashboard until you add the directives.gtag('consent', 'default', {...})) before the gtag script loads, and only set analytics_storage: 'granted' after your banner accepts. Do not treat enabled = true as consent.googletagmanager.com; if you're rendering via SSR/Inertia the script may load before the DOM is ready. Check the network tab, not just the page source.gtag.js includes. Verify by counting occurrences of gtag/js?id= in the rendered HTML..env — the settings table is readable by anyone with DB access and visible in the Filament form.filament-analytics-core ^3.1; earlier 1.x lines required laravel-gtag ^3.0. If you're upgrading across majors, expect config key renames in the underlying packages and possibly a moved class name for the settings object — diff config/laravel-gtag.php and grep src/ for the settings namespace after every major bump rather than assuming your published copy still matches.navigationGroup() is the only way to get consistent casing with the rest of your menu. Any custom fields you add to the form need their own ->label(__('...')) entries.enabled true? (4) does the layout actually render the laravel-gtag hook? (5) does the browser console show a CSP or 404 error? Nine times out of ten it's step 2 or 4.navigationGroup() removes the single most common reason to subclass the page. Subclass only when you need to change the slug, the access gate, or the form schema — then point the plugin at your class and keep the settings object untouched.How can I help you explore Laravel packages today?