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 Analytics Laravel Package

spatie/laravel-analytics

Retrieve data from Google Analytics in your Laravel app with spatie/laravel-analytics. Simple methods like fetchMostVisitedPages and fetchVisitorsAndPageViews return Laravel collections, with easy Composer installation and publishable config.

View on GitHub
Deep Wiki
Context7

Getting Started

Three steps and you have data on screen:

composer require spatie/laravel-analytics
php artisan vendor:publish --tag="analytics-config"
  1. Credentials — create a GCP project, enable the Google Analytics Data API, create a service account key (JSON), and drop it at storage_path('app/analytics/service-account-credentials.json'). Create the directory; the package won't do it for you.
  2. Property access — in GA4 → Admin → Property Access Management, add the client_email from the JSON as a Viewer/Analyst. This is the step people skip and then debug a 403 for an hour.
  3. Property ID — ANALYTICS_PROPERTY_ID is the numeric property ID (Admin → Property Settings), not the G-XXXXXXX measurement ID.

First use case — a dashboard widget:

use Spatie\Analytics\Facades\Analytics;
use Spatie\Analytics\Period;

$topPages = Analytics::fetchMostVisitedPages(Period::days(7), 10);
// each item: ['fullPageUrl' => ..., 'pageTitle' => ..., 'screenPageViews' => ...]

Where to look first: config/analytics.php (property id, cache TTL, cache store), the facade's method list in the README, and — importantly — the GA4 Data API schema for valid metric/dimension names. The package is a thin wrapper; your real reference is Google's schema, not the package README.

Implementation Patterns

Wrap the facade, don't sprinkle it. The facade binds a single configured property, so every consumer is coupled to global config. Put analytics behind your own class:

class AnalyticsDashboard
{
    public function summary(Period $period): array
    {
        return [
            'pages' => Analytics::fetchMostVisitedPages($period, 10),
            'referrers' => Analytics::fetchTopReferrers($period, 10),
            'browsers' => Analytics::fetchTopBrowsers($period, 5),
        ];
    }
}

Then inject/type-hint it in controllers, Livewire components, or a view composer. This gives you one place to add caching, error handling, and test doubles.

Use get() as the escape hatch. The named methods cover maybe 20% of real reporting needs. Everything else is get() with GA4 metric/dimension names:

Analytics::get(
    period: Period::create(now()->subDays(30), now()),
    metrics: ['activeUsers', 'sessions', 'conversions'],
    dimensions: ['sessionDefaultChannelGroup'],
    maxResults: 50,
    orderBy: [OrderBy::metric('sessions', true)],
);

Write small named helpers (conversionsByChannel(), signupsByLandingPage()) around get() so call sites stay readable and the metric strings live in one file.

Layer caching deliberately. The package caches whole API responses for cache_lifetime_in_minutes (default 1440). For dashboard tiles rendered on every request, that's usually enough. For anything heavier — cohort tables, 12-month trends, per-tenant reports — cache your derived result on top with a longer TTL, or persist to an analytics_snapshots table.

Snapshot to the database on a schedule. This is the single highest-leverage pattern: a nightly command writes yesterday's numbers into your own tables. You then get fast dashboards, chart history beyond GA4's retention window, and no per-request API dependency.

// app/Console/Commands/SnapshotAnalytics.php
$row = Analytics::get(Period::create($date, $date), ['activeUsers'], ['date'])->first();
AnalyticsSnapshot::updateOrCreate(['date' => $date], $row);

Fake in tests. Analytics::fake() short-circuits every method to return an empty Collection; pass an array or Collection to control the payload. This makes dashboard feature tests fast and network-free:

Analytics::fake([['fullPageUrl' => '/pricing', 'pageTitle' => 'Pricing', 'screenPageViews' => 42]]);
$this->get('/dashboard')->assertOk()->assertSee('Pricing');

Environment separation. Give staging its own ANALYTICS_PROPERTY_ID (or a dedicated GA4 property) so QA doesn't pollute production reporting, and prefer passing credentials as an array via env (base64-decoded JSON) on ephemeral/serverless hosts where storage_path() isn't persistent.

Gotchas and Tips

  • The README contradicts itself on return keys. It documents fetchVisitorsAndPageViews as returning date/visitors/pageViews in one place and activeUsers/screenPageViews/pageTitle in another (v4 vs v5 leftovers). Always dd() a result once before writing Blade templates against it — the GA4 camelCase names are what you'll actually get.
  • v4 → v5 is a breaking rename. Universal Analytics ga:* metrics are gone. pageviews → screenPageViews, users → activeUsers, sessions survives. Budget migration time if you're porting an old dashboard.
  • property_id mistakes are the most common setup failure: measurement ID (G-…) instead of numeric property ID, or service account never added to the property. Both surface as PERMISSION_DENIED.
  • "Today" is not the same as "today." Period::days(7) builds Carbon dates in your app timezone; GA4 reports in the property timezone. Plus GA4 has processing latency (hours, longer for some dimensions), so today's numbers always look low. For daily reports, always query yesterday-and-before using explicit Period::create() dates.
  • Cache keys include the period, so Period::days(7) computed at 09:00 and 17:00 produce different keys — good for correctness, but it means "cache for a day" rarely behaves as a full day of hits. If you need deterministic caching, build the period from date strings (now()->startOfDay()).
  • Stale data during development is guaranteed. Set ANALYTICS_CACHE_LIFETIME=0 locally or point the package cache.store at array — otherwise you'll chase a bug that's actually a cached response.
  • The package cache and the Google client cache are separate. cache_lifetime_in_minutes and the cache.store block configure different layers. Touching one won't clear the other; to hard-refresh, flush the store.
  • maxResults defaults are low — get() defaults to 10 while the named methods default to 20. Silent truncation is a classic "why is this chart wrong" bug; pass an explicit limit.
  • INVALID_ARGUMENT from Google almost always means an incompatible metric/dimension combination or a typo in a field name. Check the GA4 schema; there's no client-side validation.
  • Multi-tenant is not supported out of the box. One property per app config. For agency/SaaS scenarios either swap config(['analytics.property_id' => ...]) immediately before each call inside a dedicated resolver, or bypass the facade and construct the underlying Analytics Data client yourself.
  • Wrap calls in try/catch on user-facing pages. Google outages and quota exhaustion (RESOURCE_EXHAUSTED) happen; a dashboard should degrade to the last snapshot, not a 500.
  • Treat the package as a convenience layer, not a data platform. Its value is the credentials/caching/period plumbing and the ~10 canned queries. Everything beyond that is the Google SDK — learn OrderBy, FilterExpression, NumericFilter, and keepEmptyRows directly, because that's where your real reporting work lives.
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.
abuseio/abuseio
seophp/robots-laravel
seophp/head-laravel
bladeknigt89/yii2-dd
gesflow/sender-laravel
apxcde/laravel-telltale
smswire/smswire-php
ulsyairil/transactional-outbox
wyox/laravel-gitlab-reporter
amprest/laravel-datatables
itashia/laravel-jalali
carlosocarvalho/elasticsearch
rwsite/wp-field
voorhof/stickler-light
e-nikitin/laravel-bigquery
codewithathis/paperless-ngx
wika-group/sessionservice-for-laravel
clikgate/clikgate-laravel
waqas-badar/laravel-saga
aporat/laravel-cloudwatch-logger