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

Ga4 Measurement Protocol Bundle Laravel Package

freema/ga4-measurement-protocol-bundle

Symfony bundle for sending server-side events to Google Analytics 4 via the Measurement Protocol, with straightforward configuration for tracking conversions and user activity from your PHP application.

View on GitHub
Deep Wiki
Context7

Getting Started

This is a Symfony bundle wrapping a GA4 Measurement Protocol client. As a Laravel developer, the first decision is whether you actually want the bundle or just the underlying PHP client. In Laravel you will not get Symfony bundle auto-discovery, so the practical path is usually:

  1. Install the underlying client package (or the bundle if you are bridging Symfony services).
  2. Register the client as a singleton in AppServiceProvider.
  3. Call it from a small application-level gateway, never directly from controllers.

Minimal Laravel wiring:

// app/Providers/AppServiceProvider.php
use Freema\Ga4MeasurementProtocol\Client;

public function register(): void
{
    $this->app->singleton(Client::class, fn () => new Client(
        config('services.ga4.measurement_id'),
        config('services.ga4.api_secret'),
    ));
}
// config/services.php
'ga4' => [
    'measurement_id' => env('GA4_MEASUREMENT_ID'),
    'api_secret'     => env('GA4_API_SECRET'),
],

For a Symfony application, install the bundle and configure it under freema_ga4_measurement_protocol with measurement_id and api_secret; the client is then autowirable as a service. Pin the bundle to ^1.2.1 if you go this route — 1.2.1 fixes a bug where events were silently dropped on any request without a session, which is exactly the situation you hit when you call the tracker from a queue worker, an artisan command, a webhook, or a stateless API route.

Where to look first:

  • The README for the exact configuration keys and service ID.
  • src/DependencyInjection/Configuration.php for the supported config tree.
  • The bundle's service definition file (services.xml / services.yaml) for the client service ID and whether it is autowirable.
  • The bundle's user-ID handler (DefaultCustomUserIdHandler) — see the gotchas, this is where the 1.2.1 fix landed and it changes how user_id is populated.
  • The underlying freema/ga4-measurement-protocol-php client for request/event classes. Note that as of 1.2.1 the Composer download no longer ships tests, so read the suite on GitHub rather than in vendor/.

First use case: send a server-side purchase event after an order is paid, using the browser's _ga cookie value as client_id. This is the highest-value use of the Measurement Protocol because ad blockers, iOS tracking prevention, and checkout redirects regularly lose client-side purchase events.

Implementation Patterns

Wrap the client in a domain gateway. Do not let GA4 payload details leak into controllers or jobs. Create App\Services\Analytics\Ga4Tracker with methods like purchase(Order $order, string $clientId), signUp(User $user, string $clientId), and lead(Lead $lead, string $clientId). This makes event naming, parameter mapping, consent checks, and testing consistent.

Resolve client_id and session_id deliberately. GA4 requires a client_id; without one, events may be accepted but not attributed. Read it from the _ga cookie (GA1.1.123456789.1234567890 → 123456789.1234567890). Read session_id from the _ga_<CONTAINER> cookie if you need session-level attribution. If the cookie is missing, fall back to a persisted UUID for logged-in users, or pass user_id as well. Do not generate a fresh random ID on every event, or you will fragment the user's history.

Don't rely on the bundle's automatic user_id. DefaultCustomUserIdHandler reads the user ID out of the request session. In Laravel there is no Symfony session on most code paths — queued jobs, console commands, webhooks, stateless API routes — so you will get no user_id at all. Pass user_id explicitly through your gateway. Before 1.2.1 the failure was worse than "no user ID": Request::getSession() threw on sessionless routes, the client swallowed it, send() returned an error, and the event was never sent. If you are on 1.2.0 or earlier and calling the tracker from a job, assume your events may be missing entirely.

Check the return value of send(). This client signals failure by returning an error object, not by throwing. The 1.2.1 session bug is a textbook example: the send returned an error and nobody noticed for two minor versions. Log non-success returns and alert on them.

Fire after the database transaction commits. Use Laravel events/listeners or DB::afterCommit() so you never send a purchase event for an order that later rolls back.

Queue the send. Measurement Protocol calls are external HTTP requests. Dispatch a job (SendGa4Event) with retries and logging. Keep the job payload small: event name, client ID, user_id, and the minimal parameters. Reconstruct or pass the relevant order data explicitly — remember the job has no session, so nothing is inherited from the originating request.

Use the debug endpoint in non-production. The GA4 debug endpoint (/debug/mp/collect) returns validationMessages. Assert that this array is empty in staging before trusting a payload shape. Never enable debug mode in production unless you are actively troubleshooting.

Batch where it makes sense. The protocol accepts multiple events per request. Batch low-value events (page views, minor interactions) rather than making one HTTP call per event. Keep high-value commerce events isolated so a bad payload does not invalidate a batch.

Map ecommerce events to GA4's expected names. Use purchase, begin_checkout, add_to_cart, sign_up, generate_lead, and login. For purchase, include transaction_id, value, currency, and items[] with item_id, item_name, price, and quantity. GA4's built-in reports expect these names and parameters; custom names will only appear in explorations.

Gate on consent. If you run in the EU/UK or have consent-mode requirements, do not send events until analytics consent is recorded. Store the consent decision with the order/user and check it inside the gateway before calling the client.

Keep a local audit trail. Log the event name, client ID, user_id, timestamp, and the client's return status. Because the Measurement Protocol is fire-and-forget and this client reports failure by return value rather than exception, this log is often the only way to answer "did we send the purchase?" — and it is what would have surfaced the 1.2.1 bug on day one.

Gotchas and Tips

  • This is a Symfony bundle, not a Laravel package. In Laravel, expect to wire the client manually or use the underlying PHP library. Do not install the bundle expecting service auto-registration.
  • Silent drops on sessionless requests (fixed in 1.2.1). DefaultCustomUserIdHandler called Request::getSession(), which throws on stateless routes and API requests; send() then returned an error and the event never went out. On 1.2.0 or below, treat any tracker call from a job, command, or webhook as unreliable. Upgrade and re-send anything historical you care about.
  • Automatic user_id is best-effort, not a feature you should depend on. Post-1.2.1 the handler simply returns no user ID when there is no session, and it now discards session values that cannot be cast to a string. Before, an array user ID was happily sent as the literal string "Array", polluting your user attribution. Set user_id yourself.
  • A 2xx response does not mean GA4 accepted the event. The normal endpoint returns success for malformed payloads too. Use the debug endpoint to validate during development.
  • client_id is mandatory in practice. user_id alone is not a substitute for attribution. If you cannot obtain a client ID, decide explicitly whether to skip the event rather than sending an unusable one.
  • Realtime and engagement reports need more than an event name. Include session_id and engagement_time_msec where appropriate; otherwise events can appear in standard reports but behave oddly in realtime.
  • Timestamps must be in the past and within the allowed backfill window (up to 72 hours). Future timestamps are rejected or ignored. Do not use timestamp_micros for "scheduled" events.
  • Respect payload limits. GA4 caps events per request, parameters per event, and string lengths, and the overall request body is small. Truncate long product names and avoid dumping entire serialized models into params.
  • Never send PII. No email addresses, names, phone numbers, or raw addresses in event parameters. If you need identity stitching, use user_id with a stable internal identifier.
  • Event and parameter names are case-sensitive and should be snake_case. Reserved names and GA4's automatic event names can collide with custom ones. Prefer the documented ecommerce schema.
  • The API secret is a bearer-style secret. Keep it in environment variables, never in frontend code or logs, and rotate it if exposed. It only authorizes sending data to your property; it does not grant read access.
  • Low adoption means read the source — and now read it on GitHub. This package has one star and zero dependents, and from 1.2.1 the Composer dist no longer includes tests, CI config, or dev files. You cannot learn expected behavior from vendor/; read the suite and release notes upstream. Pin the version in composer.json, and inspect the client's HTTP layer for timeout and error-handling behavior before relying on it in a revenue-critical path.
  • Compatibility floor: 1.2.1 CI covers PHP 8.1–8.5 against Symfony 5.4, 6.4, 7.4 and 8.x. If you are on PHP 8.0 or Symfony 5.3, you are outside the tested matrix.
  • Extension point: if the bundle's client is too thin, decorate your own gateway around it. That is also the cleanest place to add retries, circuit breaking, sampling, explicit user_id injection, and multi-property routing.
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.
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core
nodisrupt/laravel-agent
sierratecnologia/media-manager