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

Php Ga Measurement Protocol Laravel Package

theiconic/php-ga-measurement-protocol

A PHP client for the Google Analytics Measurement Protocol, letting you send server-side hits like pageviews, events, and ecommerce transactions directly to Google Analytics from your Laravel or PHP app without client-side JavaScript.

View on GitHub
Deep Wiki
Context7

Getting Started

Install with Composer (pin to the release that has batch support):

composer require theiconic/php-ga-measurement-protocol:^2.9

The whole API is a fluent builder on TheIconic\Tracking\GoogleAnalytics\Analytics. You set parameters one at a time, then call a send*() method for the hit type:

use TheIconic\Tracking\GoogleAnalytics\Analytics;

$analytics = new Analytics(true); // true = force HTTPS

$analytics
    ->setProtocolVersion('1')
    ->setTrackingId('UA-123456-1')
    ->setClientId('1234567890.1600000000')
    ->setDocumentPath('/checkout/success')
    ->setDocumentTitle('Order complete')
    ->setAnonymizeIp(true);

$analytics->sendPageview();

$response = $analytics->getResponse();
if (! $response->isSuccessful()) {
    // getHttpStatusCode() / log for later inspection
}

Where to look first

  • src/Analytics.php — the fluent setters, the send*() entry points (sendPageview, sendEvent, sendScreenview, sendTransaction, sendItem, sendException, sendTiming, sendSocialInteraction) and the response accessor. As of 2.9.0 this is also where the new batch-hit entry point lives — read the method and its tests before using it, since the release note is a one-liner and there is no prose documentation for it yet.
  • src/Parameters/ — the catalogue of what you can actually set, grouped by scope (Hit/, Session/, TrafficSources/, ECommerce/, System/). This is your reference when a setter name isn't obvious.
  • src/Network/HttpClient.php — SSL, timeouts, async mode, proxy, and how the query string is assembled. Batch support changes what this sends, so re-read it if you adopt batching.
  • src/Exception/ — InvalidParameterException (unknown setter name) and InvalidPayloadException (missing required parameters for a hit type).

First use case

The natural first adoption is the thing JavaScript can't do: a hit that fires from a webhook, queue worker, cron, or CLI command. A payment provider confirms an order → you send a server-side pageview or event with the same client_id the browser used, so the conversion is attributed to the real session rather than appearing as a brand-new anonymous user. If you're already doing that at any volume, 2.9.0's batch support is the first upgrade worth taking.

Implementation Patterns

Wrap it in a configured factory, not a singleton

Analytics accumulates state across calls. Bind a small factory that produces a fresh instance per hit, pre-loaded with your defaults:

final class AnalyticsFactory
{
    public function __construct(
        private string $trackingId,
        private ?string $endpoint = null,
    ) {}

    public function forClient(string $clientId): Analytics
    {
        $analytics = new Analytics(true);

        $analytics
            ->setProtocolVersion('1')
            ->setTrackingId($this->trackingId)
            ->setClientId($clientId)
            ->setDataSource('server')
            ->setAnonymizeIp(true)
            ->setRequestTimeout(2);

        if ($this->endpoint) {
            $analytics->setEndpoint($this->endpoint);
        }

        return $analytics;
    }
}

Resolve it from the container, keep the tracking ID in config/env, and never reuse the returned object for a second hit.

Stitch the client ID to the browser session

For attribution to work, the server-side hit must carry the same cid the browser tracker uses. Parse it out of the _ga cookie rather than inventing one:

$clientId = null;

if (! empty($_COOKIE['_ga'])) {
    // _ga looks like: GA1.2.1234567890.1600000000
    if (preg_match('/^GA\d+\.\d+\.(\d+\.\d+)$/', $_COOKIE['_ga'], $m)) {
        $clientId = $m[1];
    }
}

$clientId ??= $user->analytics_client_id ?? Str::uuid()->toString();

Persist a generated ID on the user record if you also send hits from unauthenticated contexts (emails, webhooks) so repeated server hits group into one user. setUserId() is useful as a cross-device join key, but it does not replace client_id.

Decouple the request from the hit

Analytics calls are network I/O. On latency-sensitive paths (checkout, login, API responses), push the hit description into a queue and build the Analytics object in the worker:

final class SendAnalyticsHit implements ShouldQueue
{
    public function __construct(
        public string $type,        // 'pageview', 'event', ...
        public array $params,
        public string $clientId,
    ) {}

    public function handle(AnalyticsFactory $factory): void
    {
        $analytics = $factory->forClient($this->clientId);

        foreach ($this->params as $setter => $value) {
            $analytics->{$setter}($value);
        }

        $analytics->{'send' . ucfirst($this->type)}();

        if (! $analytics->getResponse()->isSuccessful()) {
            // log / retry policy
        }
    }
}

This keeps the request fast, makes retries explicit, and lets you add sampling or rate limiting in one place. Store the parameters as plain scalars — don't try to serialize the Analytics or Payload object.

Batch hits (new in 2.9.0)

2.9.0 adds a batch hit feature, which means you no longer have to hand-roll the /batch/collect body. The Measurement Protocol rules still apply: at most 20 hits per request and a hard payload size ceiling (16 KB), and every hit in the batch must carry its own tid/cid/t — hits do not inherit parameters from each other.

Treat it as a queued flush rather than a per-request call:

// Shape only — confirm the exact method names against
// src/Analytics.php and the package tests before relying on this.
$analytics = $factory->forClient($clientId);
$analytics->setBatchHits($bufferedHits); // array of ParameterBag-style hit definitions
$analytics->sendBatch();

$response = $analytics->getResponse();

Practical guidance regardless of the exact API:

  • Buffer, then flush. Accumulate hit descriptions in your own array (or a analytics_hits queue table) and send one batch per N hits or per time window — e.g. from a scheduled command running every minute. Don't build a batch inside a web request.
  • Cap at 20 and measure the body size. A batch of 20 e-commerce item hits with long product names and labels can approach the size limit. Truncate labels, and split into chunks of 20 with a size check.
  • Chunk, don't fail. When a flush exceeds the cap, split and send sequentially rather than dropping the overflow.
  • Keep the per-hit path as the default. Batch is an optimisation for volume; single hits are easier to debug and to attribute to a specific code path. Only move a hit type to batch once you know the payload is valid.
  • Watch the response. Batch responses are per-hit, not a single 1×1 GIF, so isSuccessful() alone won't tell you which hits failed. Parse the response in your flush job and log the failing indices.

Async mode for genuinely fire-and-forget hits

$analytics->setAsyncRequest(true);
$analytics->sendEvent();

This opens a non-blocking socket and returns immediately. You get no usable response, so errors vanish. It's appropriate for high-volume, best-effort instrumentation (e.g. backend job metrics) and inappropriate for conversions you may need to debug or reconcile. Async and batch solve different problems — batching cuts request count, async cuts latency — and combining them gives you essentially no delivery signal.

E-commerce: transaction then one hit per item

$analytics->setTransactionId($order->id)
    ->setTransactionAffiliation('Web')
    ->setTransactionRevenue($order->total)
    ->setTransactionShipping($order->shipping)
    ->setTransactionTax($order->tax)
    ->setTransactionCurrency('EUR')
    ->sendTransaction();

foreach ($order->items as $item) {
    $analytics->setTransactionId($order->id)
        ->setItemName($item->name)
        ->setItemCode($item->sku)
        ->setItemCategory($item->category)
        ->setItemPrice($item->price)
        ->setItemQuantity($item->quantity)
        ->sendItem();
}

Set every item parameter on every iteration — the builder retains previous values, so an omitted setItemCategory() silently reuses the last item's category. Item hits require the transaction ID; without it they're dropped or orphaned. This is the highest-value candidate for batching, since a 10-item order is 11 requests today.

Instrument backend events as GA events, not fake pageviews

$analytics
    ->setEventCategory('account')
    ->setEventAction('signup_confirmed')
    ->setEventLabel($plan->slug)
    ->setEventValue(1)
    ->sendEvent();

Same for sendTiming() (job duration) and sendException() (background worker failures) if you want operational data alongside product analytics. Keep these out of user-facing properties unless you're deliberately aggregating them.

Validate with the debug endpoint during development

$analytics->setEndpoint('https://www.google-analytics.com/debug/collect');

The response body is JSON with per-hit validation results, which is the only real way to know whether Google accepted the payload. Point a config flag at this endpoint in local/staging and at the normal endpoint in production. If the batch feature doesn't expose a debug-endpoint equivalent, validate your hit definitions individually against /debug/collect before you start batching them.

Testing

Inject a spy HTTP client (setHttpClient()) or point setEndpoint() at a local recording endpoint; then assert on the outgoing query string (or batch body). Combine that with a small set of integration tests against /debug/collect to catch invalid parameter combinations. Keep tests asserting your payload construction, not Google's behaviour.

Gotchas and Tips

  • Universal Analytics is sunset. This library implements the v1 Measurement Protocol for UA properties (UA-XXXX-Y). UA stopped processing standard hits in July 2023. If your property is GA4, the payload is fundamentally different: measurement_id + api_secret, a JSON body POSTed to /mp/collect, and events nested under events[]. You can override the endpoint, but the client builds a query string, not a JSON body, so you'd be fighting the transport — and the new batch feature batches UA hits, not GA4 events. For new work, evaluate a GA4-native client or a thin Http-based implementation instead of forking this one.
  • HTTP 200 does not mean "accepted." The /collect endpoint returns a 1×1 GIF for almost anything, including malformed hits. isSuccessful() only tells you the request reached Google. Use /debug/collect to distinguish "sent" from "valid."
  • Client ID is not optional in practice. Generate a random cid per request and every hit is a new user: sessions fragment, conversions never attribute, and reports are noise. Read the _ga cookie or persist an ID per user.
  • Instance state leaks between hits. setDocumentPath() from a pageview carries into a subsequent event on the same object, and e-commerce item parameters persist across items. Always build a fresh Analytics per hit; treat it as a value object, never a singleton. This bites hardest in batch construction: if you reuse one builder to assemble 20 hits, every hit inherits the last one's parameters.
  • Typo'd setters fail loudly; raw parameters don't. setDocumentPathh() throws InvalidParameterException at call time. The generic setParameter('cd1', $value) escape hatch bypasses that safety net — verify parameter names against the Measurement Protocol docs, and prefer the named setters where they exist.
  • Required parameters throw InvalidPayloadException before the request. Useful, but the library only validates what it models — semantic mistakes (wrong currency, revenue as a formatted string, missing ti on item hits) still go through silently. Batch mode makes this worse: one bad hit can invalidate a chunk, so validate first.
  • Batch limits are hard limits. 20 hits per request, and the endpoint enforces a body size ceiling. The library batching for you doesn't mean it will split oversized or over-count batches — check what it does at the boundary and chunk at your layer if unsure.
  • Async mode hides failures. No response, no exception you can act on, and it still opens a socket. Use a queue instead if you care about delivery.
  • Set a short request timeout. A slow or blocked egress path can stall workers; 1–2 seconds is plenty for a hit you don't block on. Raise it modestly for batch flushes — a 20-hit batch is one request but a bigger payload, and a timeout on a flush loses up to 20 hits at once. If egress is restricted, configure the proxy support rather than opening the firewall.
  • Don't send PII. No emails, names, phone numbers, or full URLs containing them in documentPath, event labels, or custom dimensions. GA's terms forbid it and it turns an analytics call into a GDPR/CCPA liability. Use internal IDs, and enable setAnonymizeIp(true) for EU traffic.
  • setEndpoint() is your main extension point — local capture servers, proxies, regional endpoints, and debug validation all go through it. setHttpClient() is the second for testing or custom transport. Beyond that, adding first-class setters means adding parameter classes under src/Parameters and letting the setter resolution pick them up; check Analytics/ParameterSetter before assuming a name will resolve.
  • Batch support is brand new and undocumented. The 2.9.0 release note is a single line. There is no README section, no upgrade note, and no guarantee the batch method signature is stable across 2.9.x. Pin the exact version, read the source and tests for the batch entry point, and wrap it behind your own service method so a later rename is a one-file change. Report or PR doc gaps upstream rather than relying on blog posts.
  • Maintenance risk, slightly improved. The previous gap since the last tag was 2020; 2.9.0 shows the package is still receiving community PRs. That's encouraging but doesn't change the underlying picture — small contributor pool, no vendor LTS commitment, and the protocol it targets is deprecated. Pin the version, run your test suite on your PHP version (especially PHP 8.x) to catch deprecations, and budget for a fork or replacement if analytics is a product requirement rather than a nice-to-have.
  • Filter your own traffic. Health checks, bots, and internal load tests will pollute the property unless you gate on environment, user agent, or a dedicated internal flag — cheap to add at the factory layer, expensive to clean up in reports later. Apply the same gate before hits enter a batch buffer.
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.
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
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk