Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Ga4 Laravel Package

jeffersongoncalves/laravel-ga4

Laravel GA4 offers a simple integration for Google Analytics 4, letting you send events and page views to GA4 directly from your Laravel application. Configure your measurement ID and start tracking server-side events quickly.

View on GitHub
Deep Wiki
Context7

Getting Started

This is the first release (v1.0.0), so there is no upgrade path to worry about — but there is also no battle-testing yet. Pin the exact version in composer.json ("vendor/package": "1.0.0") until the API surface settles across a couple of point releases, and skim the CHANGELOG before every composer update.

Minimal setup:

  1. composer require vendor/package
  2. Publish the config: php artisan vendor:publish --tag=ga4-config (check the README for the exact tag — packages vary).
  3. Create a Google Cloud service account, enable the Google Analytics Data API and Google Analytics Admin API, download the JSON key, and store it outside the web root (e.g. storage/app/ga4/service-account.json).
  4. In the GA4 Admin UI, add the service account email as a property user. Viewer is enough for reports; conversion-event provisioning needs Editor.
  5. Set GA4_PROPERTY_ID and the credentials path (or inline JSON) in .env.

Where to look first: the README's quick-start, then the config file (it tells you which env vars actually matter and whether credentials can be passed inline), then the client/service classes in src/ — that is where you will find whether the package wraps the raw Google SDK or hand-rolls HTTP calls, which determines how you test it.

First use case: a realtime sanity check. Before building anything, confirm auth works:

$active = GA4::runRealtimeReport([
    'metrics' => [['name' => 'activeUsers']],
]);

If that returns rows, your credentials and property ID are correct. If it returns an empty set, you are probably authenticated but pointing at the wrong property — check the numeric property ID (not the G-XXXXXXX measurement ID, which is a different thing entirely).

Implementation Patterns

Reporting — cache aggressively. runReport() is the workhorse and the fastest way to burn quota. Wrap it in a cached service rather than calling it from controllers:

$report = Cache::remember("ga4:pageviews:{$days}", now()->addMinutes(15), fn () => GA4::runReport([
    'dateRanges' => [['startDate' => "{$days}daysAgo", 'endDate' => 'today']],
    'dimensions' => [['name' => 'pagePath']],
    'metrics'    => [['name' => 'screenPageViews'], ['name' => 'averageSessionDuration']],
    'orderBys'   => [['desc' => true, 'metric' => ['metricName' => 'screenPageViews']]],
    'limit'      => 25,
]));

Realtime is for dashboards only. runRealtimeReport() covers roughly the last 30 minutes and supports a much narrower set of dimensions/metrics than standard reports. Use it for "users online now" widgets and nothing else — don't build reporting features on it.

Admin API is provisioning, not runtime. listConversionEvents() / createConversionEvent() belong in an artisan command or a seeder, not in a request cycle:

// app/Console/Commands/SyncGa4Conversions.php
foreach (config('analytics.conversion_events') as $eventName) {
    if (! in_array($eventName, $existing, true)) {
        GA4::createConversionEvent(['eventName' => $eventName]);
    }
}

Keeping the desired list in config makes the command idempotent and lets you run it per environment (staging property vs production property).

Measurement Protocol — send from the server, always queued. This is where most day-to-day value lives: purchases, signups, server-side conversions that the browser tag misses. Never call it inline in a checkout request.

GA4::sendEvent([
    'client_id' => $this->clientIdFromGaCookie($request->cookie('_ga')),
    'user_id'   => (string) $order->user_id,
    'events'    => [[
        'name'   => 'purchase',
        'params' => [
            'transaction_id' => $order->reference,
            'value'          => (float) $order->total,
            'currency'       => $order->currency,
            'items'          => $order->items->map->toGa4Item()->all(),
        ],
    ]],
]);

Wrap it in a queued job or a queued event listener with $tries = 3 and a backoff. The Measurement Protocol accepts batches, so if you emit several events for one order, collect them into a single sendEvent() call rather than three HTTP round-trips.

Extract the GA client ID properly. The _ga cookie looks like GA1.1.123456789.1699999999; the client ID is the last two segments (123456789.1699999999). If the cookie is absent (consent-blocked, server-to-server traffic, mobile app), generate a UUID and persist it against the user so future events stitch together. Pass user_id whenever you have it — it makes cross-device reporting actually work.

Testing. Bind a fake or use Http::fake() in your test suite; assert on the outgoing payload rather than on the package's return object, since the response shape for Measurement Protocol is famously uninformative (204 No Content on success).

Gotchas and Tips

  • Service account not added to the property is the #1 cause of 403 PERMISSION_DENIED. The API call is fine; the account simply isn't a user on that property.
  • Property ID vs measurement ID. The Data and Admin APIs need the numeric property ID. Passing G-XXXXXXX yields confusing 404s.
  • Dimension/metric compatibility. GA4 rejects invalid pairings with a 400 and a message naming the offending field. This is not a bug in the package — build reports iteratively in the GA4 Query Explorer first, then port the payload.
  • Quota. Standard properties get a limited number of tokens per property per day plus concurrent-request limits. One uncached dashboard with 12 widgets, refreshed on every page load, will exhaust it by mid-afternoon. Cache, or batch multiple reports into a single batchRunReports if the package exposes it.
  • Row limits. Default responses cap at 10k rows and the API caps around 100k per request; anything larger needs limit/offset pagination. Silent truncation is easy to miss because the response still looks successful.
  • createConversionEvent() is not idempotent. Calling it twice for the same event name errors out. Always check listConversionEvents() first — hence the guard in the command above.
  • Measurement Protocol events are not instant. They can take hours to appear in standard reports. Validate payloads against the debug endpoint (validationBehavior / debug mode) rather than waiting for the UI. Events timestamped more than 72 hours in the past are rejected.
  • Consent and PII. Server-side events bypass client-side consent banners entirely. If you operate under GDPR/ePrivacy, gate the job behind your consent record — the package will happily send whatever you give it.
  • Config caching. php artisan config:cache will bake the credentials path in; make sure it's an absolute path or resolved via storage_path() in the config file, otherwise deploys break in ways that only show up in production.
  • Credentials in .env. Inline JSON in an env var is fragile (escaping, newlines). A file path is easier to rotate and won't blow up your .env parser.
  • Timezone. Date ranges are evaluated in the GA4 property's timezone, not your app's. "Today" means two different things, and month-boundary reports will look off by a day.
  • Initial-release risk. With no prior versions there's no track record on error handling, retry behaviour, or how the package surfaces Google's error payloads. Read the client class before you depend on it in a critical path, and consider wrapping calls in your own thin repository so a future breaking change is a one-file fix rather than a find-and-replace.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
htinlynn/genius-helper
maco-studios/hephaestus
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator