├── .github └── workflows │ └── run-tests.yml ├── .gitignore ├── LICENSE ├── README.md ├── composer.json ├── config └── config.php ├── phpunit.xml ├── resources └── views │ └── sendClientID.blade.php ├── src ├── Events │ ├── BroadcastEvent.php │ └── EventBroadcaster.php ├── Exceptions │ ├── MissingClientIdException.php │ ├── ReservedEventNameException.php │ └── ReservedParameterNameException.php ├── Facades │ └── GA4.php ├── GA4.php ├── Http │ ├── ClientIdRepository.php │ ├── ClientIdSession.php │ ├── SessionIdRepository.php │ ├── SessionIdSession.php │ └── StoreClientIdInSession.php ├── Jobs │ └── SendEventToAnalytics.php ├── Listeners │ └── DispatchAnalyticsJob.php ├── ServiceProvider.php ├── ShouldBroadcastToAnalytics.php └── aliases.php └── tests ├── TestCase.php └── Unit └── SendGA4EventTest.php /.github/workflows/run-tests.yml: -------------------------------------------------------------------------------- 1 | name: run-tests 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | fail-fast: true 10 | matrix: 11 | php: [8.2, 8.1] 12 | laravel: [10.*, 9.*, 8.*] 13 | dependency-version: [prefer-lowest, prefer-stable] 14 | include: 15 | - laravel: 10.* 16 | testbench: 8.* 17 | - laravel: 9.* 18 | testbench: 7.* 19 | - laravel: 8.* 20 | testbench: 6.5.* 21 | 22 | name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.dependency-version }} 23 | 24 | steps: 25 | - name: Checkout code 26 | uses: actions/checkout@v4 27 | 28 | - name: Cache dependencies 29 | uses: actions/cache@v4 30 | with: 31 | path: ~/.composer/cache/files 32 | key: dependencies-laravel-${{ matrix.laravel }}-php-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }} 33 | 34 | - name: Setup PHP 35 | uses: shivammathur/setup-php@v2 36 | with: 37 | php-version: ${{ matrix.php }} 38 | extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick, mysql, mysqli, pdo_mysql 39 | coverage: none 40 | 41 | - name: Install dependencies 42 | run: | 43 | composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" --no-interaction --no-update 44 | composer update --${{ matrix.dependency-version }} --prefer-dist --no-interaction --no-suggest 45 | 46 | - name: Execute tests 47 | run: vendor/bin/phpunit 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .phpunit.result.cache 3 | .composer.lock 4 | vendor 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Luke Towers 4 | Copyright (c) Mike Wall 5 | Copyright (c) 2020 Protone Media 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel Google Analytics 4 Measurement Protocol Event Tracking 2 | 3 | [![Version](https://img.shields.io/github/v/release/luketowers/laravel-ga4-event-tracking?sort=semver&style=flat-square)](https://github.com/luketowers/laravel-ga4-event-tracking/releases) 4 | [![Tests](https://img.shields.io/github/actions/workflow/status/luketowers/laravel-ga4-event-tracking/run-tests.yml?&label=tests&style=flat-square)](https://github.com/luketowers/laravel-ga4-event-tracking/actions) 5 | [![License](https://img.shields.io/github/license/luketowers/laravel-ga4-event-tracking?label=open%20source&style=flat-square)](https://packagist.org/packages/luketowers/laravel-ga4-event-tracking) 6 | 7 | 8 | Simplifies using the [Measurement Protocol for Google Analytics 4](https://developers.google.com/analytics/devguides/collection/protocol/ga4) to track events in Laravel applications. 9 | 10 | ## Installation 11 | 12 | 1) Install package via Composer 13 | 14 | ``` bash 15 | composer require luketowers/laravel-ga4-event-tracking 16 | ``` 17 | 18 | 2) Set `GA4_MEASUREMENT_ID` and `GA4_MEASUREMENT_PROTOCOL_API_SECRET` in your .env file. 19 | 20 | > Copy from `Google Analytics > Admin > Data Streams > [Select Site] > Measurement ID` & `Google Analytics > Admin > Data Streams > [Select Site] > Measurement Protocol API secrets` respectively. 21 | 22 | 3) Optional: Publish the config / view files by running this command in your terminal: 23 | 24 | ``` bash 25 | php artisan vendor:publish --tag=ga4-event-tracking.config --tag=ga4-event-tracking.views 26 | ``` 27 | 28 | 4) Include the `sendGA4ClientID` directive in your layout file after the Google Analytics Code tracking code. 29 | 30 | ```blade 31 | 32 | @sendGA4ClientID 33 | 34 | ``` 35 | 36 | The [`client_id`](https://developers.google.com/analytics/devguides/collection/protocol/ga4/reference?client_type=gtag#payload_post_body) is required to send an event to Google Analytics. This package provides a Blade directive which you can put in your layout file after the Google Analytics Code tracking code. It grabs the current user's GA `client_id` from either the `ga()` or `gtag()` helper functions injected by Google Analytics and makes a POST request to your application to store the `client_id` in the session, which is later used by the `DispatchAnalyticsJob` when sending events to GA4. 37 | 38 | If you do not use this blade directive, you will have to handle retrieving, storing, and sending the `client_id` yourself. You can use `GA$::setClientId($clientId)` to set the `client_id` manually. 39 | 40 | ## Usage 41 | 42 | This package provides two ways to send events to Google Analytics 4: 43 | 44 | ### Directly via the `GA4` facade: 45 | 46 | Sending event directly is as simple as calling the `sendEvent($eventData)` method on the `GA4` facade from anywhere in your backend to post event to Google Analytics 4. `$eventData` contains the name and params of the event as per this [reference page](https://developers.google.com/analytics/devguides/collection/protocol/ga4/reference/events#login). For example: 47 | 48 | ```php 49 | GA4::sendEvent([ 50 | 'name' => 'login', 51 | 'params' => [ 52 | 'method' => 'Google', 53 | ], 54 | ]); 55 | ``` 56 | 57 | The `sendEvent()` method will return an array with the status of the request. 58 | 59 | 60 | ### Broadcast events to GA4 via the Laravel Event System 61 | 62 | Just add the `ShouldBroadcastToAnalytics` interface to your event, and you're ready! You don't have to manually bind any listeners. 63 | 64 | ```php 65 | order = $order; 83 | } 84 | } 85 | ``` 86 | 87 | There are additional methods that let you customize the call sent to GA4. 88 | 89 | #### `broadcastGA4EventAs` 90 | 91 | With this method you can customize the name of the [Event Action](https://developers.google.com/analytics/devguides/collection/analyticsjs/field-reference#eventAction). By default, we use the class name with the class's namespace removed. This method gives you access to the underlying `GA4` class instance as well. 92 | 93 | #### `eventOccurredAt` 94 | 95 | With this method you can customize the time that the event occurred at. This will be used in the `timestamp_micros` parameter sent to the Measurement Protocol. By default, we use the current time. You must return an instance of `Carbon\Carbon` in order for it to be used. 96 | 97 | ```php 98 | use Carbon\Carbon; 99 | use LukeTowers\GA4EventTracking\GA4; 100 | use LukeTowers\GA4EventTracking\ShouldBroadcastToAnalytics; 101 | use Illuminate\Queue\SerializesModels; 102 | 103 | class OrderSubmitted extends Event implements ShouldBroadcastToAnalytics 104 | { 105 | use SerializesModels; 106 | 107 | protected Carbon $submittedAt; 108 | 109 | public function __construct( 110 | public Order $order 111 | ) { 112 | $this->submittedAt = now(); 113 | } 114 | 115 | public function eventOccurredAt(): Carbon 116 | { 117 | return $this->submittedAt; 118 | } 119 | } 120 | ``` 121 | 122 | #### `withGA4Parameters` 123 | 124 | With this method you can set the parameters of the event being sent. 125 | 126 | ```php 127 | order = $order; 146 | } 147 | 148 | public function withGA4Parameters(GA4 $ga4): array 149 | { 150 | $eventData = [ 151 | 'transaction_id' => $order->id, 152 | 'value' => $order->amount_total, 153 | 'currency' => 'USD', 154 | 'tax' => $order->amount_tax, 155 | 'shipping' => $order->amount_shipping, 156 | 'items' => [], 157 | 'event_category' => config('app.name'), 158 | 'event_label' => 'Order Created', 159 | ]; 160 | 161 | foreach ($order->items as $item) { 162 | $eventData['items'][] = [ 163 | 'id' => $item->sku ?: 'p-' . $item->product->id, 164 | 'name' => $item->title, 165 | 'brand' => $item->product->brand->name ?? '', 166 | 'category' => $item->product->category->title ?? '', 167 | 'quantity' => $item->quantity, 168 | 'price' => $item->price, 169 | 'variant' => $item->variant->title ?? '', 170 | ]; 171 | } 172 | 173 | return $eventData; 174 | } 175 | 176 | public function broadcastGA4EventAs(GA4 $ga4): string 177 | { 178 | return 'purchase'; 179 | } 180 | } 181 | ``` 182 | 183 | 184 | ### Handle framework and 3rd-party events 185 | 186 | If you want to handle events where you can't add the `ShouldBroadcastToAnalytics` interface, you can manually register them in your `EventServiceProvider` using the `DispatchAnalyticsJob` listener. 187 | 188 | ```php 189 | [ 207 | SendEmailVerificationNotification::class, 208 | DispatchAnalyticsJob::class, 209 | ], 210 | ]; 211 | } 212 | ``` 213 | 214 | ### Debugging Mode 215 | 216 | You can also enable [debugging mode](https://developers.google.com/analytics/devguides/collection/protocol/ga4/validating-events) by calling `enableDebugging()` method before calling the `sendEvent()` method. Like so - `GA4::enableDebugging()->sendEvent($eventData)`. The `sendEvent()` method will return the response (array) from Google Analytics request in that case. 217 | 218 | 219 | ## Testing 220 | 221 | ``` bash 222 | composer test 223 | ``` 224 | or 225 | 226 | ``` bash 227 | ./vendor/bin/phpunit 228 | ``` 229 | 230 | ## Security 231 | 232 | If you discover any security related issues, please use the [Report a vulnerability](https://github.com/luketowers/laravel-ga4-event-tracking/security/advisories/new) button instead of using the issue tracker. 233 | 234 | ## Credits: 235 | 236 | This package is a fork of the following projects: 237 | 238 | - [protonemedia/laravel-analytics-event-tracking](https://github.com/protonemedia/laravel-analytics-event-tracking): Original package, but only supports Universal Analytics. 239 | - [daikazu/laravel-ga4-event-tracking](https://github.com/daikazu/laravel-ga4-event-tracking): Forked from the original package to support Google Analytics 4 but the package was not maintained and it was not compatible with Laravel 10. 240 | - [accexs/laravel-ga4-event-tracking](https://github.com/accexs/laravel-ga4-event-tracking): Forked from `daikazu`'s package but was missing some features and was not cleanly forked. 241 | 242 | ## License 243 | 244 | This package is open-sourced software licensed under the [MIT License](LICENSE). 245 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "luketowers/laravel-ga4-event-tracking", 3 | "description": "Simplifies using the Measurement Protocol for Google Analytics 4 to track events in Laravel applications.", 4 | "license": "MIT", 5 | "authors": [ 6 | { 7 | "name": "Luke Towers", 8 | "role": "Current Maintainer" 9 | }, 10 | { 11 | "name": "Ronny Arvelo", 12 | "role": "Previous Maintainer" 13 | }, 14 | { 15 | "name": "Mike Wall", 16 | "email": "daikazu@gmail.com", 17 | "homepage": "http://mikewall.dev" 18 | }, 19 | { 20 | "name": "Pascal Baljet", 21 | "email": "pascal@protone.media", 22 | "role": "Original Author" 23 | } 24 | ], 25 | "homepage": "https://github.com/LukeTowers/laravel-ga4-event-tracking", 26 | "keywords": [ 27 | "laravel", 28 | "google-analytics-4", 29 | "GA4", 30 | "laravel-analytics", 31 | "analytics", 32 | "analytics-event-tracking" 33 | ], 34 | "require": { 35 | "php": "^8.1", 36 | "illuminate/bus": "^8.0 || ^9.0 || ^10.0 || ^11.0 || ^12.0", 37 | "illuminate/queue": "^8.0 || ^9.0 || ^10.0 || ^11.0 || ^12.0", 38 | "illuminate/http": "^8.74 || ^9.0 || ^10.0 || ^11.0 || ^12.0", 39 | "illuminate/validation": "^8.0 || ^9.0 || ^10.0 || ^11.0 || ^12.0", 40 | "guzzlehttp/guzzle": "^7.5" 41 | }, 42 | "require-dev": { 43 | "mockery/mockery": "^1.4.4", 44 | "nesbot/carbon": "^2.66 || ^3.8.4", 45 | "orchestra/testbench": "^6.20 || ^7.0 || ^8.0", 46 | "phpunit/phpunit": "^9.5" 47 | }, 48 | "autoload": { 49 | "files": [ 50 | "src/aliases.php" 51 | ], 52 | "psr-4": { 53 | "LukeTowers\\GA4EventTracking\\": "src" 54 | } 55 | }, 56 | "autoload-dev": { 57 | "psr-4": { 58 | "LukeTowers\\GA4EventTracking\\Tests\\": "tests" 59 | } 60 | }, 61 | "scripts": { 62 | "test": "vendor/bin/phpunit", 63 | "test-coverage": "vendor/bin/phpunit --coverage-html coverage" 64 | }, 65 | "extra": { 66 | "laravel": { 67 | "providers": [ 68 | "LukeTowers\\GA4EventTracking\\ServiceProvider" 69 | ] 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /config/config.php: -------------------------------------------------------------------------------- 1 | true, 9 | 10 | /** 11 | * Your GA4 Measurement ID, looks like "G-XXXXXXXXXX", copy from: 12 | * Google Analytics > Admin > Data Streams > [Select Site] > Measurement ID 13 | * @see https://support.google.com/analytics/answer/9304153?hl=en for setup instructions. 14 | */ 15 | 'measurement_id' => env('GA4_MEASUREMENT_ID'), 16 | 17 | /** 18 | * Your GA4 Measurement Protocol API Secret, copy from: 19 | * Google Analytics > Admin > Data Streams > [Select Site] > Measurement Protocol API secrets 20 | * @see https://developers.google.com/analytics/devguides/collection/protocol/ga4/reference?client_type=gtag#payload_post_body 21 | */ 22 | 'api_secret' => env('GA4_MEASUREMENT_PROTOCOL_API_SECRET', null), 23 | 24 | /** 25 | * The session key to store the GA4 Client ID in. 26 | */ 27 | 'client_id_session_key' => 'ga4-event-tracking-client-id', 28 | 29 | /** 30 | * The session key to store the GA4 Session ID in. 31 | */ 32 | 'session_id_session_key' => 'ga4-event-tracking-session-id', 33 | 34 | /** 35 | * HTTP URI to post the Client ID to (from the Blade Directive). 36 | */ 37 | 'http_uri' => '/gaid', 38 | 39 | /* 40 | * This queue will be used to perform the API calls to GA. 41 | * Leave empty to use the default queue. 42 | */ 43 | 'queue_name' => '', 44 | 45 | /** 46 | * Send the ID of the authenticated user to GA. 47 | */ 48 | 'send_user_id' => false, 49 | ]; 50 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | src/ 15 | 16 | 17 | 18 | 19 | ./tests/Unit 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /resources/views/sendClientID.blade.php: -------------------------------------------------------------------------------- 1 | 47 | -------------------------------------------------------------------------------- /src/Events/BroadcastEvent.php: -------------------------------------------------------------------------------- 1 | GA4 = $GA4; 15 | } 16 | 17 | public function withParameters(callable $callback): self 18 | { 19 | $callback($this->GA4); 20 | 21 | return $this; 22 | } 23 | 24 | public function handle($event): void 25 | { 26 | $eventAction = method_exists($event, 'broadcastGA4EventAs') 27 | ? $event->broadcastGA4EventAs($this->GA4) 28 | : str(class_basename($event))->snake()->toString(); 29 | 30 | $this->GA4->setEventAction($eventAction); 31 | 32 | if (method_exists($event, 'withGA4Parameters')) { 33 | $this->GA4->setEventParams($event->withGA4Parameters($this->GA4)); 34 | } 35 | 36 | $occurredAt = Carbon::now(); 37 | if (method_exists($event, 'eventOccurredAt')) { 38 | $occurredAt = $event->eventOccurredAt(); 39 | } 40 | if ($occurredAt instanceof Carbon) { 41 | $this->GA4->setTimestampMicros($occurredAt->timestamp . $occurredAt->micro); 42 | } 43 | 44 | $this->GA4->sendAsSystemEvent(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Events/EventBroadcaster.php: -------------------------------------------------------------------------------- 1 | isConfigured() && config('ga4-event-tracking.is_enabled', true); 106 | } 107 | 108 | public function setClientId(string $clientId): static 109 | { 110 | $this->clientId = $clientId; 111 | return $this; 112 | } 113 | 114 | public function setUserId(string $userId): static 115 | { 116 | $this->userId = $userId; 117 | return $this; 118 | } 119 | 120 | public function setTimestampMicros(string $timestampMicros): static 121 | { 122 | // @TODO: Perform validation on this to ensure it's a valid timestamp 123 | $this->timestampMicros = $timestampMicros; 124 | return $this; 125 | } 126 | 127 | public function setSessionId(string $sessionId): static 128 | { 129 | $this->sessionId = $sessionId; 130 | return $this; 131 | } 132 | 133 | public function getSessionId(): ?string 134 | { 135 | return $this->sessionId; 136 | } 137 | 138 | public function setUserProperties(array $userProperties): static 139 | { 140 | $this->userProperties = $userProperties; 141 | return $this; 142 | } 143 | 144 | public function getUserProperties(): array 145 | { 146 | return $this->userProperties; 147 | } 148 | 149 | public function setEventAction(string $eventAction): void 150 | { 151 | $this->eventAction = $eventAction; 152 | } 153 | 154 | public function setEventParams(array $eventParams): void 155 | { 156 | if (!isset($eventParams['session_id']) && !is_null($this->sessionId)) { 157 | // Required to have events show up in session based reporting 158 | // @see https://developers.google.com/analytics/devguides/collection/protocol/ga4/sending-events?client_type=gtag#format_the_request 159 | $eventParams['session_id'] = $this->sessionId; 160 | } 161 | 162 | $this->eventParams = $eventParams; 163 | } 164 | 165 | public function enableDebugging(bool $toggle = true): static 166 | { 167 | $this->debugging = $toggle; 168 | return $this; 169 | } 170 | 171 | /** 172 | * @throws MissingClientIdException 173 | * @throws ReservedEventNameException 174 | * @throws ReservedParamNameException 175 | */ 176 | public function sendEvent(array $eventData): array 177 | { 178 | return $this->sendEvents([$eventData]); 179 | } 180 | 181 | /** 182 | * @throws MissingClientIdException 183 | * @throws ReservedEventNameException 184 | * @throws ReservedParamNameException 185 | */ 186 | public function sendEvents(array $events): array 187 | { 188 | if (!$this->isEnabled()) { 189 | return [ 190 | 'status' => false, 191 | 'message' => 'GA4 Event Tracking is not configured.', 192 | ]; 193 | } 194 | 195 | if ( 196 | !$this->clientId 197 | && !$this->clientId = session(config('ga4-event-tracking.client_id_session_key')) 198 | ) { 199 | throw new MissingClientIdException; 200 | } 201 | 202 | $this->validateEvents($events); 203 | 204 | if (!empty($this->userProperties)) { 205 | $this->validateUserProperties($this->userProperties); 206 | } 207 | 208 | $requestData = [ 209 | 'client_id' => $this->clientId, 210 | 'events' => $events, 211 | ]; 212 | 213 | if (!empty($this->userId)) { 214 | $requestData['user_id'] = $this->userId; 215 | } 216 | 217 | if (!empty($this->timestampMicros)) { 218 | // @TODO: Perform validation on this to ensure that it's within the past 72 hours 219 | // @see https://developers.google.com/analytics/devguides/collection/protocol/ga4/reference?client_type=gtag#payload_post_body 220 | $requestData['timestamp_micros'] = $this->timestampMicros; 221 | } 222 | 223 | if (!empty($this->userProperties)) { 224 | $requestData['user_properties'] = $this->userProperties; 225 | } 226 | 227 | $response = Http::withOptions([ 228 | 'query' => [ 229 | 'measurement_id' => config('ga4-event-tracking.measurement_id'), 230 | 'api_secret' => config('ga4-event-tracking.api_secret'), 231 | ], 232 | ])->post($this->getRequestUrl(), $requestData); 233 | 234 | if ($this->debugging) { 235 | return $response->json(); 236 | } 237 | 238 | return [ 239 | 'status' => $response->successful(), 240 | ]; 241 | } 242 | 243 | protected function getRequestUrl(): string 244 | { 245 | $url = 'https://www.google-analytics.com'; 246 | $url .= $this->debugging ? '/debug' : ''; 247 | 248 | return $url . '/mp/collect'; 249 | } 250 | 251 | public function sendAsSystemEvent(): void 252 | { 253 | $this->sendEvent([ 254 | 'name' => $this->eventAction, 255 | 'params' => $this->eventParams, 256 | ]); 257 | } 258 | 259 | /** 260 | * @throws ReservedEventNameException if a reserved event name is used 261 | * @throws ReservedParameterNameException if a reserved parameter name is used 262 | * @throws Exception if more than 25 events are sent at once 263 | */ 264 | public function validateEvents(array $events): array 265 | { 266 | if (count($events) > 25) { 267 | throw new \Exception('You can only send 25 events at a time to GA4.'); 268 | } 269 | 270 | foreach ($events as $event) { 271 | $this->validateEvent($event); 272 | } 273 | 274 | return $events; 275 | } 276 | 277 | /** 278 | * @throws ReservedEventNameException if a reserved event name is used 279 | * @throws ReservedParameterNameException if a reserved parameter name is used 280 | */ 281 | public function validateEvent(array $event): void 282 | { 283 | if (!isset($event['name'])) { 284 | throw new \Exception('Event name is required.'); 285 | } 286 | 287 | if (in_array($event['name'], static::RESERVED_EVENT_NAMES)) { 288 | throw new ReservedEventNameException("The event name {$event['name']} is reserved for Google Analytics 4. Please use a different name."); 289 | } 290 | 291 | if (!empty($event['params'])) { 292 | $this->validateParams($event['params']); 293 | } 294 | } 295 | 296 | /** 297 | * @throws ReservedParameterNameException if a reserved parameter name is used 298 | */ 299 | public function validateParams(array $params): void 300 | { 301 | foreach ($params as $key => $value) { 302 | if (Str::startsWith($key, static::RESERVED_PARAM_PREFIXES)) { 303 | throw new ReservedParameterNameException("The parameter name {$key} is reserved for Google Analytics 4. Please use a different name."); 304 | } 305 | } 306 | } 307 | 308 | /** 309 | * @throws Exception if a reserved user property name is used 310 | */ 311 | public function validateUserProperties(array $params): void 312 | { 313 | foreach ($params as $key => $value) { 314 | if (in_array($key, static::RESERVED_USER_PROPERTY_NAMES)) { 315 | throw new \Exception("The user property name {$key} is reserved for Google Analytics 4. Please use a different name."); 316 | } 317 | 318 | if (Str::startsWith($key, static::RESERVED_USER_PROPERTY_PREFIXES)) { 319 | throw new \Exception("The user property name {$key} is reserved for Google Analytics 4. Please use a different name."); 320 | } 321 | } 322 | } 323 | } 324 | -------------------------------------------------------------------------------- /src/Http/ClientIdRepository.php: -------------------------------------------------------------------------------- 1 | session = $session; 17 | $this->key = $key; 18 | } 19 | 20 | /** 21 | * Stores the Client ID in the session. 22 | */ 23 | public function update(string $clientId): void 24 | { 25 | $this->session->put($this->key, $clientId); 26 | } 27 | 28 | /** 29 | * Gets the Client ID from the session or generates one. 30 | */ 31 | public function get(): ?string 32 | { 33 | return $this->session->get($this->key, fn () => $this->generateId()); 34 | } 35 | 36 | /** 37 | * Generates a UUID and stores it in the session. 38 | */ 39 | private function generateId(): string 40 | { 41 | return tap(Str::uuid(), fn ($id) => $this->update($id)); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Http/SessionIdRepository.php: -------------------------------------------------------------------------------- 1 | session = $session; 16 | $this->key = $key; 17 | } 18 | 19 | /** 20 | * Stores the GA4 Session ID in the session. 21 | */ 22 | public function update(string $sessionId): void 23 | { 24 | $this->session->put($this->key, $sessionId); 25 | } 26 | 27 | /** 28 | * Gets the GA4 Session ID from the session or generates one. 29 | */ 30 | public function get(): ?string 31 | { 32 | return $this->session->get($this->key, fn () => $this->generateId()); 33 | } 34 | 35 | /** 36 | * Generates a GA4 Session ID and stores it in the session. 37 | */ 38 | private function generateId(): string 39 | { 40 | return tap(now()->timestamp, fn ($id) => $this->update($id)); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Http/StoreClientIdInSession.php: -------------------------------------------------------------------------------- 1 | has('client_id')) { 16 | $data = $request->validate(['client_id' => 'required|string|max:255']); 17 | $clientIdSession->update($data['client_id']); 18 | } 19 | 20 | if ($request->has('session_id')) { 21 | $data = $request->validate(['session_id' => 'required|string|max:255']); 22 | $sessionIdSession->update($data['session_id']); 23 | } 24 | 25 | return response()->json(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Jobs/SendEventToAnalytics.php: -------------------------------------------------------------------------------- 1 | event = $event; 28 | $this->clientId = $clientId; 29 | $this->userId = $userId; 30 | $this->sessionId = $sessionId; 31 | } 32 | 33 | public function handle(EventBroadcaster $broadcaster) 34 | { 35 | if ($this->clientId) { 36 | $broadcaster->withParameters(fn (GA4 $GA4) => $GA4->setClientId($this->clientId)); 37 | } 38 | 39 | if ($this->userId) { 40 | $broadcaster->withParameters(fn (GA4 $GA4) => $GA4->setUserId($this->userId)); 41 | } 42 | 43 | if ($this->sessionId) { 44 | $broadcaster->withParameters(fn (GA4 $GA4) => $GA4->setSessionId($this->sessionId)); 45 | } 46 | 47 | $broadcaster->handle($this->event); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Listeners/DispatchAnalyticsJob.php: -------------------------------------------------------------------------------- 1 | clientIdRepository = $clientIdRepository; 21 | $this->sessionIdRepository = $sessionIdRepository; 22 | } 23 | 24 | public function handle($event): void 25 | { 26 | $job = new SendEventToAnalytics($event, $this->clientIdRepository->get(), $this->userId(), $this->sessionIdRepository->get()); 27 | if ($queueName = config('ga4-event-tracking.tracking.queue_name')) { 28 | $job->onQueue($queueName); 29 | } 30 | 31 | dispatch($job); 32 | } 33 | 34 | private function userId(): ?string 35 | { 36 | if (!config('ga4-event-tracking.tracking.send_user_id', false)) { 37 | return null; 38 | } 39 | 40 | return Auth::id(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/ServiceProvider.php: -------------------------------------------------------------------------------- 1 | loadViewsFrom(__DIR__.'/../resources/views', 'ga4-event-tracking'); 28 | 29 | // Publishing is only necessary when using the CLI. 30 | if ($this->app->runningInConsole()) { 31 | $this->bootForConsole(); 32 | } 33 | 34 | $this->app->booted(function () { 35 | // Only register the listener if the measurement_id and api_secret are set 36 | // to avoid unnecessary overhead. 37 | if (config('ga4-event-tracking.measurement_id') !== null 38 | && !config('ga4-event-tracking.api_secret') !== null 39 | ) { 40 | Event::listen(ShouldBroadcastToAnalytics::class, DispatchAnalyticsJob::class); 41 | } 42 | }); 43 | 44 | Blade::directive('sendGA4ClientID', function () { 45 | return ""; 46 | }); 47 | } 48 | 49 | /** 50 | * Register any package services. 51 | */ 52 | public function register(): void 53 | { 54 | $this->mergeConfigFrom(__DIR__.'/../config/config.php', 'ga4-event-tracking'); 55 | 56 | $this->app->singleton(EventBroadcaster::class, BroadcastEvent::class); 57 | $this->registerClientId(); 58 | $this->registerAnalytics(); 59 | $this->registerRoute(); 60 | } 61 | 62 | /** 63 | * Get the services provided by the provider. 64 | */ 65 | public function provides() 66 | { 67 | return ['ga4-event-tracking']; 68 | } 69 | 70 | /** 71 | * Console-specific booting. 72 | */ 73 | protected function bootForConsole(): void 74 | { 75 | // Publishing the configuration file. 76 | $this->publishes([ 77 | __DIR__.'/../config/config.php' => config_path('ga4-event-tracking.php'), 78 | ], 'ga4-event-tracking.config'); 79 | 80 | // Publishing the views. 81 | $this->publishes([ 82 | __DIR__.'/../resources/views' => base_path('resources/views/vendor/ga4-event-tracking'), 83 | ], 'ga4-event-tracking.views'); 84 | } 85 | 86 | protected function registerAnalytics() 87 | { 88 | $this->app->bind('ga4', function () { 89 | return new GA4(); 90 | }); 91 | } 92 | 93 | protected function registerClientId() 94 | { 95 | $this->app->singleton(ClientIdRepository::class, ClientIdSession::class); 96 | $this->app->singleton(SessionIdRepository::class, SessionIdSession::class); 97 | 98 | $this->app->bind('ga4-event-tracking.client-id', function () { 99 | return $this->app->make(ClientIdSession::class)->get(); 100 | }); 101 | $this->app->bind('ga4-event-tracking.session-id', function () { 102 | return $this->app->make(SessionIdSession::class)->get(); 103 | }); 104 | 105 | $this->app->singleton(ClientIdSession::class, function () { 106 | return new ClientIdSession( 107 | $this->app->make('session.store'), 108 | config('ga4-event-tracking.client_id_session_key', 'ga4-event-tracking-client-id') 109 | ); 110 | }); 111 | $this->app->singleton(SessionIdSession::class, function () { 112 | return new SessionIdSession( 113 | $this->app->make('session.store'), 114 | config('ga4-event-tracking.session_id_session_key', 'ga4-event-tracking-session-id') 115 | ); 116 | }); 117 | } 118 | 119 | protected function registerRoute() 120 | { 121 | if ($httpUri = config('ga4-event-tracking.http_uri')) { 122 | Route::post($httpUri, StoreClientIdInSession::class)->middleware('web'); 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/ShouldBroadcastToAnalytics.php: -------------------------------------------------------------------------------- 1 | sendEvent([ 15 | 'name' => 'test_event', 16 | 'params' => [ 17 | 'method' => 'none', 18 | 'some_id' => '123', 19 | 'some_other_id' => '456', 20 | 21 | ], 22 | ]); 23 | 24 | $this->assertTrue($test['status']); 25 | } 26 | 27 | public function test_if_event_name_is_reserved() 28 | { 29 | $this->expectException(ReservedEventNameException::class); 30 | 31 | GA4::setClientId('123456789') 32 | ->sendEvent([ 33 | 'name' => 'ad_click', 34 | ]); 35 | } 36 | } 37 | --------------------------------------------------------------------------------