Skip to content

Commit aef580d

Browse files
committed
Add support for session_id and timestamp_micros
See https://www.analyticsmania.com/post/unassigned-in-google-analytics-4/#measurement-protocol > If your developers send data via MP to the currently active session on your site, each event must contain client_id and session_id parameters. If session_id is not included (or the session_id does not match the ID of the currently active session), the traffic source of that session will be not set. > If your developers send data via MP to the session that has already timed out (but is not older than 72 hours), they also need to send a timestamp_micros parameter. If that is not done, then you guessed it – the source/medium will be not set.
1 parent 608e6a9 commit aef580d

File tree

9 files changed

+179
-11
lines changed

9 files changed

+179
-11
lines changed

README.md

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -84,11 +84,44 @@ class OrderWasCreated implements ShouldBroadcastToAnalytics
8484
}
8585
```
8686

87-
There are two additional methods that lets you customize the call sent to GA4.
87+
There are additional methods that let you customize the call sent to GA4.
8888

89-
With the `broadcastGA4EventAs` 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.
89+
#### `broadcastGA4EventAs`
9090

91-
With the `withGA4Parameters` method you can set the parameters of the event being sent.
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 DevPro\GA4EventTracking\GA4;
100+
use DevPro\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.
92125

93126
```php
94127
<?php

config/config.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,15 @@
2222
'api_secret' => env('GA4_MEASUREMENT_PROTOCOL_API_SECRET', null),
2323

2424
/**
25-
* The session key to store the Client ID in.
25+
* The session key to store the GA4 Client ID in.
2626
*/
2727
'client_id_session_key' => 'ga4-event-tracking-client-id',
2828

29+
/**
30+
* The session key to store the GA4 Session ID in.
31+
*/
32+
'session_id_session_key' => 'ga4-event-tracking-session-id',
33+
2934
/**
3035
* HTTP URI to post the Client ID to (from the Blade Directive).
3136
*/

resources/views/sendClientID.blade.php

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,44 @@
11
<script>
2-
function postClientId(clientId) {
2+
function ga4PostId(id, value) {
33
let data = new FormData();
4-
data.append('client_id', clientId);
4+
data.append(id, value);
55
let xhr = new XMLHttpRequest();
66
xhr.open('POST', "{{ url(config('ga4-event-tracking.http_uri'))}}", true);
77
xhr.setRequestHeader('X-CSRF-TOKEN', '{{ csrf_token() }}');
88
xhr.send(data);
99
}
1010
11+
function postClientId(clientId) {
12+
ga4PostId('client_id', clientId);
13+
}
14+
15+
function postSessionId(sessionId) {
16+
ga4PostId('session_id', sessionId);
17+
}
18+
1119
function collectClientId() {
1220
if (typeof ga !== 'undefined') {
1321
ga(function () {
1422
let clientId = ga.getAll()[0].get('clientId');
1523
if (clientId !== @json(app('ga4-event-tracking.client-id'))) {
1624
postClientId(clientId);
1725
}
26+
let sessionId = ga.getAll()[0].get('sessionId');
27+
if (sessionId !== @json(app('ga4-event-tracking.session-id'))) {
28+
postSessionId(sessionId);
29+
}
1830
});
1931
} else if (typeof gtag !== 'undefined') {
2032
gtag('get', @json(config('ga4-event-tracking.measurement_id')), 'client_id', function (clientId) {
2133
if (clientId !== @json(app('ga4-event-tracking.client-id'))) {
2234
postClientId(clientId);
2335
}
2436
});
37+
gtag('get', @json(config('ga4-event-tracking.measurement_id')), 'session_id', function (sessionId) {
38+
if (sessionId !== @json(app('ga4-event-tracking.session-id'))) {
39+
postSessionId(sessionId);
40+
}
41+
});
2542
}
2643
}
2744

src/Events/BroadcastEvent.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace DevPro\GA4EventTracking\Events;
44

5+
use Carbon\Carbon;
56
use DevPro\GA4EventTracking\GA4;
67

78
class BroadcastEvent implements EventBroadcaster
@@ -32,6 +33,14 @@ public function handle($event): void
3233
$this->GA4->setEventParams($event->withGA4Parameters($this->GA4));
3334
}
3435

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+
3544
$this->GA4->sendAsSystemEvent();
3645
}
3746
}

src/GA4.php

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ class GA4
1414

1515
protected string $userId = '';
1616

17+
protected string $timestampMicros = '';
18+
19+
protected ?string $sessionId = null;
20+
1721
protected array $userProperties = [];
1822

1923
protected bool $debugging = false;
@@ -113,6 +117,24 @@ public function setUserId(string $userId): static
113117
return $this;
114118
}
115119

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+
116138
public function setUserProperties(array $userProperties): static
117139
{
118140
$this->userProperties = $userProperties;
@@ -131,6 +153,12 @@ public function setEventAction(string $eventAction): void
131153

132154
public function setEventParams(array $eventParams): void
133155
{
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+
134162
$this->eventParams = $eventParams;
135163
}
136164

@@ -186,6 +214,12 @@ public function sendEvents(array $events): array
186214
$requestData['user_id'] = $this->userId;
187215
}
188216

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+
189223
if (!empty($this->userProperties)) {
190224
$requestData['user_properties'] = $this->userProperties;
191225
}

src/Http/SessionIdRepository.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
namespace DevPro\GA4EventTracking\Http;
4+
5+
interface SessionIdRepository
6+
{
7+
public function update(string $sessionId): void;
8+
9+
public function get(): ?string;
10+
}

src/Http/SessionIdSession.php

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?php
2+
3+
namespace DevPro\GA4EventTracking\Http;
4+
5+
use Illuminate\Session\Store;
6+
7+
class SessionIdSession implements SessionIdRepository
8+
{
9+
private Store $session;
10+
11+
private string $key;
12+
13+
public function __construct(Store $session, string $key)
14+
{
15+
$this->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+
}

src/Http/StoreClientIdInSession.php

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,19 @@
88
class StoreClientIdInSession
99
{
1010
/**
11-
* Stores the posted Client ID in the session.
11+
* Stores the posted GA4 Client ID or Session ID in the session.
1212
*/
13-
public function __invoke(Request $request, ClientIdSession $clientIdSession): JsonResponse
13+
public function __invoke(Request $request, ClientIdSession $clientIdSession, SessionIdSession $sessionIdSession): JsonResponse
1414
{
15-
$data = $request->validate(['client_id' => 'required|string|max:255']);
15+
if ($request->has('client_id')) {
16+
$data = $request->validate(['client_id' => 'required|string|max:255']);
17+
$clientIdSession->update($data['client_id']);
18+
}
1619

17-
$clientIdSession->update($data['client_id']);
20+
if ($request->has('session_id')) {
21+
$data = $request->validate(['session_id' => 'required|string|max:255']);
22+
$sessionIdSession->update($data['session_id']);
23+
}
1824

1925
return response()->json();
2026
}

src/ServiceProvider.php

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
use DevPro\GA4EventTracking\Events\EventBroadcaster;
77
use DevPro\GA4EventTracking\Http\ClientIdRepository;
88
use DevPro\GA4EventTracking\Http\ClientIdSession;
9+
use DevPro\GA4EventTracking\Http\SessionIdRepository;
10+
use DevPro\GA4EventTracking\Http\SessionIdSession;
911
use DevPro\GA4EventTracking\Http\StoreClientIdInSession;
1012
use DevPro\GA4EventTracking\Listeners\DispatchAnalyticsJob;
1113
use Illuminate\Support\Facades\Blade;
@@ -89,15 +91,25 @@ protected function registerAnalytics()
8991
protected function registerClientId()
9092
{
9193
$this->app->singleton(ClientIdRepository::class, ClientIdSession::class);
94+
$this->app->singleton(SessionIdRepository::class, SessionIdSession::class);
9295

9396
$this->app->bind('ga4-event-tracking.client-id', function () {
9497
return $this->app->make(ClientIdSession::class)->get();
9598
});
99+
$this->app->bind('ga4-event-tracking.session-id', function () {
100+
return $this->app->make(SessionIdSession::class)->get();
101+
});
96102

97103
$this->app->singleton(ClientIdSession::class, function () {
98104
return new ClientIdSession(
99105
$this->app->make('session.store'),
100-
config('ga4-event-tracking.client_id_session_key')
106+
config('ga4-event-tracking.client_id_session_key', 'ga4-event-tracking-client-id')
107+
);
108+
});
109+
$this->app->singleton(SessionIdSession::class, function () {
110+
return new SessionIdSession(
111+
$this->app->make('session.store'),
112+
config('ga4-event-tracking.session_id_session_key', 'ga4-event-tracking-session-id')
101113
);
102114
});
103115
}

0 commit comments

Comments
 (0)