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

google/analytics-admin

Official Google PHP client for the Analytics Admin API, enabling developers to programmatically manage Google Analytics 4 accounts, properties, data streams, audiences, and related configuration from PHP or Laravel applications.

View on GitHub
Deep Wiki
Context7

Getting Started

google/analytics-admin is the GAPIC-generated PHP client for the Google Analytics Admin API — the control plane for GA4. It manages accounts, properties, data streams, custom dimensions, conversion events, audiences, links (Ads/Firebase/BigQuery), measurement-protocol secrets, access bindings, and data-retention settings. It does not report on traffic; use google/analytics-data for that (runAccessReport is the one reporting exception that lives here).

Minimal setup

  1. composer require google/analytics-admin (requires PHP 8.1+ on current releases).
  2. In GCP: enable analyticsadmin.googleapis.com, create a service account, download the JSON key.
  3. In the GA4 UI (Admin → Account/Property Access Management), add the service-account email as a user with at least Editor. Without this you get PERMISSION_DENIED even with a valid token — this is the single most common first-run failure.
  4. Configure the client in Laravel:
// config/services.php
'analytics' => [
    'credentials' => env('GOOGLE_APPLICATION_CREDENTIALS'), // path or JSON array
    'account_id'  => env('GA_ACCOUNT_ID'),
],

// AppServiceProvider::register()
use Google\Analytics\Admin\V1beta\Client\AnalyticsAdminServiceClient;

$this->app->singleton(AnalyticsAdminServiceClient::class, fn () => new AnalyticsAdminServiceClient([
    'credentials' => config('services.analytics.credentials'),
    'transport'   => 'rest', // avoids needing ext-grpc; fine for admin traffic
    'credentialsConfig' => [
        'scopes' => ['https://www.googleapis.com/auth/analytics.edit'],
    ],
]));

First use case — list what you can reach, then create a web stream and grab its measurement ID:

$client = app(AnalyticsAdminServiceClient::class);

foreach ($client->listAccountSummaries() as $summary) {
    dump($summary->getDisplayName(), $summary->getPropertySummaries());
}

$stream = (new DataStream())
    ->setType(DataStream\DataStreamType::WEB)
    ->setDisplayName('Website')
    ->setWebStreamData(
        (new DataStream\WebStreamData())->setDefaultUri('https://acme.test')
    );

$created = $client->createDataStream('properties/123456789', $stream);
$measurementId = $created->getWebStreamData()->getMeasurementId(); // G-XXXXXXX

Implementation Patterns

Wrap the client behind your own interface. The GAPIC client is huge and auto-generated; don't inject it into controllers. Define a narrow contract (AnalyticsProvisioner with createProperty, ensureConversionEvent, measurementIdFor, …) and bind the real implementation to the GAPIC client. This is also what makes the code testable.

final class GoogleAnalyticsProvisioner implements AnalyticsProvisioner
{
    public function __construct(private AnalyticsAdminServiceClient $client) {}
}

Tenant provisioning as a queued job. Creating a property + data stream + measurement ID is a multi-step, quota-limited, partially-consistent operation — never do it inline in a web request. Push a ProvisionAnalyticsProperty job with $tries = 5 and backoff(), persist the properties/… resource name as soon as it exists so retries are idempotent (check for an existing property before creating).

Declarative sync for dimensions and events. There is no upsert. The reliable pattern is list → diff → create/update/archive, driven by a PHP config array that mirrors your product's analytics contract:

$existing = collect($client->listCustomDimensions($propertyName))
    ->keyBy(fn ($d) => $d->getParameterName());

foreach (config('analytics.dimensions') as $dim) {
    if ($existing->has($dim['parameter_name'])) {
        continue;
    }
    $client->createCustomDimension($propertyName, (new CustomDimension())
        ->setParameterName($dim['parameter_name'])
        ->setDisplayName($dim['display_name'])
        ->setScope($dim['scope']));
}

Run this from a scheduled command so drift is corrected automatically.

Updates always need a FieldMask. Send only the fields you mutate:

$property = (new Property())
    ->setName('properties/123456789')
    ->setDisplayName('Acme — Prod');

$client->updateProperty($property, new FieldMask(['paths' => ['display_name']]));

Cache the read side aggressively. Account/property/stream listings change rarely. Cache::remember("ga:properties:{$accountId}", 3600, fn () => ...) and bust the key after writes. listAccountSummaries() is a single cheap call that returns accounts and properties — prefer it over nested listAccounts + listProperties loops.

Pagination. Every list* returns a PagedListResponse; iterate directly or tune the page:

$paged = $client->listProperties(['pageSize' => 200, 'filter' => 'parent:accounts/123']);
foreach ($paged->iterateAllElements() as $property) { /* ... */ }

Getting the measurement ID server-side. createMeasurementProtocolSecret() on a web stream gives you a secret for sending Measurement Protocol events from Laravel (server-side purchases, refunds) without exposing anything to the browser. Pair the stream's measurementId with the secret and store both per tenant.

Data retention & consent. updateDataRetentionSettings() (14 vs 2 months) and acknowledgeUserDataCollection() are one-liners you should call at provisioning time rather than leaving to manual UI work.

Account creation is a redirect flow. You cannot createAccount(). Use provisionAccountTicket() to get a redirectUri, send the customer there to accept the GA4 terms, and reconcile the resulting account via a webhook/callback or polling.

Testing. Either mock your own interface, or stub the wire layer — GAPIC ships Google\ApiCore\Testing\MockTransport, which you pass as 'transport' => $mockTransport to exercise real serialization without network calls.

Gotchas and Tips

  • Admin API ≠ Data API. Trying to run traffic reports here will not work. If you need sessions/conversions/revenue, install google/analytics-data alongside this.
  • 403 is almost always missing GA4 user access. Enabling the API and having a valid service account is not enough; the service-account email must be added inside GA4 Access Management. Check that before debugging scopes.
  • Writes are quota-constrained. Admin API reads are generous (~1,200/min per project); writes are far tighter and property creation is slower still. Batch, queue, and cache rather than looping in a request.
  • Eventual consistency. A freshly created property or stream may not appear in subsequent list* calls or accept dimension creation for a short window. Retry with backoff in provisioning jobs; don't assume read-after-write.
  • Resource names are the API. accounts/123, properties/456, properties/456/dataStreams/789. Methods take the string name, not a numeric ID. Extract with Str::after($name, 'properties/') or basename($name).
  • Immutable / undeletable fields. A property's parent account is fixed. Custom dimensions cannot be hard-deleted by the API. Conversion events are "deleted" by archiving (deletable is false for GA4-native events). Design sync logic around create+update, not delete, wherever possible.
  • An empty FieldMask is not "update everything" in a useful way — it can be rejected or silently no-op. Always enumerate the paths you intend to change.
  • Protobuf objects, not arrays. Responses are generated message classes with getters (getDisplayName(), getName()). For logging, use $msg->serializeToJsonString() rather than json_encode() on nested structures.
  • Choose one API version and stick to it. V1beta is where most production features live; V1alpha has newer resources but less stability. Mixing the two clients produces confusing resource-name and enum mismatches — the enums are not interchangeable across namespaces.
  • Error handling: catch Google\ApiCore\ApiException, branch on $e->getStatus() (NOT_FOUND, PERMISSION_DENIED, INVALID_ARGUMENT, RESOURCE_EXHAUSTED for quota), and report() the rest. Quota errors should back off, not fail the job.
  • Retries are configurable via retrySettings on the client or per-call $optionalArgs — useful for createProperty. Don't wrap non-idempotent creates in blind retries without an existence check.
  • Transport choice matters operationally. rest keeps your Docker image free of ext-grpc and works fine for admin call volumes; switch to grpc only if you have a measured latency problem.
  • Scope creep: request analytics.readonly for any code path that only reads. A single client with analytics.edit everywhere will eventually do something destructive in a staging script.
  • Community is thin (0 dependents, ~22 stars). Treat the Google API reference and proto definitions as the real docs — the README mostly covers auth. When in doubt, read the *Client.php method signatures, since they are the source of truth for argument order.
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.
teamzac/geocoder
ideatica/config-writer
keaton-php/pipe-dream
jacksonwebservices/laravel-user-suite
maksimru/conditional-laravel-debugbar
gismatul/nova-support-chat
gismatul/laravel-support-chat
networkrailbusinesssystems/oracle-api
alphabalex/messaging-made-easy
rawphp/warp
shootkiran/merobug
narakode/fineauth
numencode/wn-syncops-plugin
ibnnajjaar/mib-global-pay
ably/pubsub-server
mstfkhazaal/defender
mfonte/base62x
karnoweb/metapilot
tobya/bcsapiwrapper
swalker2/cpanel