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

Analytics Data Laravel Package

google/analytics-data

Idiomatic PHP client for Google Analytics Data, supporting REST and gRPC. Install via Composer, authenticate with Google Cloud, and query analytics reports with ready-made client classes.

View on GitHub
Deep Wiki
Context7

Getting Started

Install the generated client and authenticate with a service account that has at least Viewer access on the GA4 property:

composer require google/analytics-data
// config/services.php
'google_analytics' => [
    'property_id' => env('GA4_PROPERTY_ID'), // e.g. "properties/123456789"
    'credentials' => env('GOOGLE_APPLICATION_CREDENTIALS'),
],
// app/Providers/AnalyticsServiceProvider.php
use Google\Analytics\Data\V1beta\Client\BetaAnalyticsDataClient;

$this->app->singleton(BetaAnalyticsDataClient::class, fn () => new BetaAnalyticsDataClient([
    'credentials' => config('services.google_analytics.credentials'),
]));

Three things must be true before your first request succeeds, and each fails with a different error:

  1. The Google Analytics Data API is enabled in the Google Cloud project behind the service account (PERMISSION_DENIED / "API has not been used" otherwise).
  2. The service account email ([email protected]) is added as a user on the GA4 property in Admin → Property Access Management (PERMISSION_DENIED with a valid token).
  3. The property identifier is properties/<numeric id>, not the bare number and not the measurement ID (G-XXXXXXX).

First use case: a headline dashboard — sessions and active users for the last 28 days, grouped by channel:

use Google\Analytics\Data\V1beta\{DateRange, Dimension, Metric, RunReportRequest};

$request = (new RunReportRequest())
    ->setProperty(config('services.google_analytics.property_id'))
    ->setDimensions([new Dimension(['name' => 'sessionDefaultChannelGroup'])])
    ->setMetrics([new Metric(['name' => 'sessions']), new Metric(['name' => 'activeUsers'])])
    ->setDateRanges([new DateRange(['start_date' => '28daysAgo', 'end_date' => 'yesterday'])])
    ->setLimit(20);

$response = app(BetaAnalyticsDataClient::class)->runReport($request);

foreach ($response->getRows() as $row) {
    $channel = $row->getDimensionValues()[0]->getValue();
    $sessions = (int) $row->getMetricValues()[0]->getValue();
    $users    = (int) $row->getMetricValues()[1]->getValue();
}

Look first at the official Data API v1 docs for dimension/metric names, then at the generated PHP API reference for exact setters. Don't guess names — call getMetadata() against the property and cache the result; it enumerates every dimension and metric actually available (including custom ones like customEvent:signup_completed).

Implementation Patterns

Wrap the client, don't sprinkle it. A thin repository (AnalyticsRepository::report(ReportQuery $q): ReportResult) that owns request building and row normalization gives you one place to cache, retry, log, and fake in tests. The generated client is awkward to mock directly; an interface is worth the ten lines.

Build a request DTO rather than chaining setters in controllers. Requests are immutable-ish protobuf messages; assembling them in a value object lets you reuse a "base report" (property, date range, filters) and vary dimensions per screen.

Pagination is limit/offset, not cursors. Default is 10,000 rows; the API caps a single request at 250,000. For exports, loop:

$offset = 0;
do {
    $response = $client->runReport($base->setLimit(10000)->setOffset($offset));
    foreach ($response->getRows() as $row) { /* accumulate */ }
    $offset += 10000;
} while ($offset < $response->getRowCount());

For scheduled exports, prefer a queued job writing to a table over a synchronous request — GA4 responses for 100k+ rows take seconds to tens of seconds and will blow through PHP max_execution_time.

Use batchRunReports for dashboards with several cards. One HTTP round trip carries up to five RunReportRequests, which cuts latency and, more importantly, concurrent-request pressure on quota. Same for batchRunPivotReports.

Filters. FilterExpression is a tree: Filter, FilterExpressionList (AND/OR group), notExpression. In practice you get one level of nesting — an andGroup of orGroups or the reverse. Dimension filters support EXACT, BEGINS_WITH, ENDS_WITH, CONTAINS, FULL_REGEXP, PARTIAL_REGEXP, and inListFilter for small bounded sets; metric filters take numeric comparisons.

use Google\Analytics\Data\V1beta\{Filter, FilterExpression, FilterExpressionList};

$byChannel = (new FilterExpression())->setFilter(new Filter([
    'field_name' => 'sessionDefaultChannelGroup',
    'string_filter' => new Filter\StringFilter([
        'match_type' => Filter\StringFilter\MatchType::EXACT,
        'value' => 'Organic Search',
    ]),
]));

$request->setDimensionFilter(
    (new FilterExpression())->setAndGroup(new FilterExpressionList(['expressions' => [$byChannel, $byDevice]]))
);

Ordering. orderBys is required for "top N" semantics — otherwise you get arbitrary rows truncated at limit. For dimension sorts choose OrderType::ALPHANUMERIC for things like date or pagePath if lexicographic order is wrong.

Real-time. runRealtimeReport() covers roughly the last 30 minutes and has a reduced dimension/metric surface plus no date ranges. It's for a live counter widget, not for reporting — don't try to feed it into the same aggregation pipeline as runReport.

Caching. GA4 recomputes reports server-side on every request; there's no response cache. A 5–15 minute cache keyed on md5(serialize($query)) is the single highest-leverage change for a dashboard. Cache getMetadata() for hours, not minutes.

Quota awareness. Set ->setReturnPropertyQuota(true) on your heaviest request and log getPropertyQuota() (tokens per day/hour, concurrent requests, consumed/remaining). Wire a circuit-breaker or a degraded "data delayed" state in the UI when remaining is near zero.

Gotchas and Tips

  • Client namespace moved. Older releases expose Google\Analytics\Data\V1beta\BetaAnalyticsDataClient; newer ones use Google\Analytics\Data\V1beta\Client\BetaAnalyticsDataClient. Sample code and blog posts are split between the two. If an import breaks after composer update, check for the Client\ subnamespace first — this is the most common upgrade failure. The old class still works but is deprecated.

  • Everything is a string. getValue() returns string for integers, floats, percentages, and currency alike. Cast deliberately per metric ((int) for sessions, (float) for engagementRate, (float) for totalRevenue) and beware that empty/absent metrics are often "" or "0" rather than null.

  • keepEmptyRows defaults to false. Rows whose metrics are all zero are dropped. This silently changes "% of total" math and makes date series have holes. Set ->setKeepEmptyRows(true) for time series, and leave it off for top-N lists.

  • Metric compatibility. Metrics have scopes (event-based vs session-based). Combining certain metrics in one request fails with INVALID_ARGUMENT about incompatible metrics. When it happens, split into two requests and join in PHP — don't hunt for a magic combination.

  • Sampling and thresholding. Check getMetadata()->getSamplingMetadatas() before publishing numbers; large cardinality dimensions over long ranges get sampled. Also check getMetadata()->getSubjectToThresholding() — when demographics or signals are involved, rows may be withheld for privacy and totals won't add up.

  • Row count is not row count. getRowCount() is the total matching rows across pagination; count(getRows()) is what came back on this page. Paginating past the end returns zero rows with a nonzero rowCount — guard your do/while.

  • Date semantics are property-local. start_date/end_date accept YYYY-MM-DD, today, yesterday, NdaysAgo; interpretation uses the property's time zone (getMetadata()->getTimeZone()), not your server's or the user's. "Yesterday" in a UTC app can differ from the property's yesterday.

  • Currency is not formatted. totalRevenue comes back as a bare number; the ISO code lives in getMetadata()->getCurrencyCode(). Formatting is entirely yours, and for multi-property setups the code differs per property.

  • PERMISSION_DENIED has three causes and the message rarely distinguishes them: API not enabled, service account not added to the property, or wrong property ID format. Validate properties/\d+ with a regex before the API call — it saves a lot of support time.

  • Transport. Defaults to gRPC if ext-grpc is installed, otherwise REST. On shared hosting, containers without the extension, or behind proxies that mangle HTTP/2, force REST explicitly: new BetaAnalyticsDataClient(['transport' => 'rest']). REST is fine for this API; streaming isn't a factor.

  • Retries. The client has retry settings for UNAVAILABLE/DEADLINE_EXCEEDED, but 429 RESOURCE_EXHAUSTED needs backoff on your side. Add a small exponential backoff in your repository wrapper, and never retry inside a request loop over 200 rows.

  • Error type. All failures surface as Google\ApiCore\ApiException; use $e->getStatus() (a ApiStatus constant) or $e->getBasicMessage() rather than parsing getMessage(). Map ApiStatus::RESOURCE_EXHAUSTED and ApiStatus::PERMISSION_DENIED to distinct user-facing states.

  • Testing. Use Google\ApiCore\Testing\MockTransport (or MockHandler) injected via the client's transport option to return canned protobuf responses. Better: code against your own interface and bind a fake in the container so the vast majority of tests never touch google/analytics-data at all.

  • Extension points worth knowing: client options credentials, transport, apiEndpoint, universeDomain, and clientConfig for custom retry/timeout settings. The message classes are plain protobuf, so serializeToJsonString() and mergeFromJsonString() are handy for logging requests/responses to your Laravel log without leaking credentials.

  • Cost of cardinality. Dimensions like fullPageUrl, pageTitle, or customEvent:* explode row counts and token consumption. Add limit + orderBys and a dimensionFilter narrowing to real traffic (sessionDefaultChannelGroup not equal to "Direct", bots excluded) before you reach for pagination.

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.
adachsoft/changelog-linter
adachsoft/collection
payerurl/telegrambot-crypto-payment-instant-payout
dskripchenko/laravel-hybrid-search
riorizkyrainey/lara-ced
mehedimi/static-vel
zharp/zharp
nonsapiens/bigquery-model-sync
lgu-bayambang-ict-office/lgu-laravel-development-standards
andydefer/laravel-support
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel
draftsmaninc/draftsman
square-bit/laravel-volition
himito/laravel-mailman
htinlynn/genius-helper
maco-studios/hephaestus
adachsoft/php-code-style