Skip to content

Commit a5148fa

Browse files
authored
Add Firebase handler to support internally stored/managed events from applications (#100)
2 parents b7c256c + 0c14639 commit a5148fa

17 files changed

+618
-29
lines changed

src/Exception/Ga4Exception.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,16 @@ public static function throwMissingMeasurementId()
3737
return new static("Missing Measurement ID", static::REQUEST_MISSING_MEASUREMENT_ID);
3838
}
3939

40+
public static function throwMissingFirebaseAppId()
41+
{
42+
return new static("Missing Firebase APP ID", static::REQUEST_MISSING_FIREBASE_APP_ID);
43+
}
44+
45+
public static function throwMissingAppInstanceId()
46+
{
47+
return new static("Missing Application Instance ID", static::REQUEST_MISSING_FIREBASE_APP_INSTANCE_ID);
48+
}
49+
4050
public static function throwMissingApiSecret()
4151
{
4252
return new static("Missing API Secret", static::REQUEST_MISSING_API_SECRET);

src/Facade/Type/DefaultEventParamsType.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,5 +44,25 @@ public function setPageTitle(string $title);
4444
*/
4545
public function setScreenResolution(string $wxh);
4646

47+
/**
48+
* In order for user activity to display in reports like Realtime, \
49+
* "session_id" must be supplied as part of the params for an event. \
50+
* The "session_id" is supplied from the reserved "session_start" event.
51+
*
52+
* @var session_id
53+
* @param string $id eg. "123"
54+
*/
55+
public function setSessionId(string $id);
56+
57+
/**
58+
* In order for user activity to display in reports like Realtime, \
59+
* "engagement_time_msec" must be supplied as part of the params for an event. \
60+
* The "engagement_time_msec" parameter should reflect the event's engagement time in milliseconds.
61+
*
62+
* @var engagement_time_msec
63+
* @param string $msec eg. '150' for 150 milliseconds
64+
*/
65+
public function setEngagementTimeMSec(int $msec);
66+
4767
public function toArray(): array;
4868
}

src/Facade/Type/FirebaseType.php

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<?php
2+
3+
namespace AlexWestergaard\PhpGa4\Facade\Type;
4+
5+
interface FirebaseType extends IOType
6+
{
7+
const URL_LIVE = 'https://www.google-analytics.com/mp/collect';
8+
const URL_DEBUG = 'https://www.google-analytics.com/debug/mp/collect';
9+
10+
const ACCEPT_RESPONSE_HEADERS = [200, 204];
11+
12+
/**
13+
* Uniquely identifies a installation instance of an application.
14+
*
15+
* @var client_id
16+
* @param string $id eg. Cookie._ga or Cookie._gid
17+
*/
18+
public function setAppInstanceId(string $id);
19+
20+
/**
21+
* A unique identifier for a user. See User-ID for cross-platform analysis for more information on this identifier.
22+
*
23+
* @var user_id
24+
* @param string $id eg. Unique User Id
25+
*/
26+
public function setUserId(string $id);
27+
28+
/**
29+
* A Unix timestamp (in microseconds) for the time to associate with the event. This should only be set to record events that happened in the past. \
30+
* This value can be overridden via user_property or event timestamps. Events can be backdated up to 3 calendar days based on the property's timezone.
31+
*
32+
* @var timestamp_micros
33+
* @param integer|float $microOrUnix microtime(true) or time()
34+
*/
35+
public function setTimestampMicros(int|float $microOrUnix);
36+
37+
/**
38+
* Indicate if these events should be used for personalized ads.
39+
*
40+
* @var non_personalized_ads
41+
* @param boolean $allow
42+
*/
43+
public function setNonPersonalizedAds(bool $allow);
44+
45+
/**
46+
* The user properties for the measurement (Up to 25 custom per project, see link)
47+
*
48+
* @var user_properties
49+
* @param AlexWestergaard\PhpGa4\Facade\Type\UserProperty $prop
50+
* @link https://support.google.com/analytics/answer/14240153
51+
*/
52+
public function addUserProperty(UserPropertyType ...$props);
53+
54+
/**
55+
* An array of event items. Up to 25 events can be sent per request
56+
*
57+
* @var events
58+
* @param AlexWestergaard\PhpGa4\Facade\Type\Event $event
59+
*/
60+
public function addEvent(EventType ...$events);
61+
62+
/**
63+
* Validate params and send it to Google Analytics
64+
*
65+
* @return void
66+
* @throws AlexWestergaard\PhpGa4\Exception\Ga4Exception
67+
*/
68+
public function post();
69+
}

src/Facade/Type/Ga4ExceptionType.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,7 @@ interface Ga4ExceptionType
2525
const REQUEST_MISSING_MEASUREMENT_ID = 104006;
2626
const REQUEST_MISSING_API_SECRET = 104007;
2727
const REQUEST_EMPTY_EVENTLIST = 104008;
28+
29+
const REQUEST_MISSING_FIREBASE_APP_ID = 105001;
30+
const REQUEST_MISSING_FIREBASE_APP_INSTANCE_ID = 105002;
2831
}

src/Firebase.php

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
<?php
2+
3+
namespace AlexWestergaard\PhpGa4;
4+
5+
use GuzzleHttp\Client as Guzzle;
6+
use AlexWestergaard\PhpGa4\Helper;
7+
use AlexWestergaard\PhpGa4\Facade;
8+
use AlexWestergaard\PhpGa4\Exception\Ga4Exception;
9+
10+
class Firebase extends Helper\IOHelper implements Facade\Type\FirebaseType
11+
{
12+
private Guzzle $guzzle;
13+
14+
private Helper\ConsentHelper $consent;
15+
private Helper\UserDataHelper $userdata;
16+
17+
protected null|bool $non_personalized_ads = false;
18+
protected null|int $timestamp_micros;
19+
protected null|string $app_instance_id;
20+
protected null|string $user_id;
21+
protected array $user_properties = [];
22+
protected array $events = [];
23+
24+
public function __construct(
25+
private string $firebase_app_id,
26+
private string $api_secret,
27+
private bool $debug = false
28+
) {
29+
parent::__construct();
30+
$this->guzzle = new Guzzle();
31+
$this->consent = new Helper\ConsentHelper();
32+
$this->userdata = new Helper\UserDataHelper();
33+
}
34+
35+
public function getParams(): array
36+
{
37+
return [
38+
'non_personalized_ads',
39+
'timestamp_micros',
40+
'app_instance_id',
41+
'user_id',
42+
'user_properties',
43+
'events',
44+
];
45+
}
46+
47+
public function getRequiredParams(): array
48+
{
49+
return ['app_instance_id'];
50+
}
51+
52+
public function setAppInstanceId(string $id)
53+
{
54+
$this->app_instance_id = $id;
55+
return $this;
56+
}
57+
58+
public function setUserId(string $id)
59+
{
60+
$this->user_id = $id;
61+
return $this;
62+
}
63+
64+
public function setTimestampMicros(int|float $microOrUnix)
65+
{
66+
$min = Helper\ConvertHelper::timeAsMicro(strtotime('-3 days') + 10);
67+
$max = Helper\ConvertHelper::timeAsMicro(time() + 3);
68+
69+
$time = Helper\ConvertHelper::timeAsMicro($microOrUnix);
70+
71+
if ($time < $min || $time > $max) {
72+
throw Ga4Exception::throwMicrotimeExpired();
73+
}
74+
75+
$this->timestamp_micros = $time;
76+
return $this;
77+
}
78+
79+
public function addUserProperty(Facade\Type\UserPropertyType ...$props)
80+
{
81+
foreach ($props as $prop) {
82+
$this->user_properties = array_replace($this->user_properties, $prop->toArray());
83+
}
84+
85+
return $this;
86+
}
87+
88+
public function addEvent(Facade\Type\EventType ...$events)
89+
{
90+
foreach ($events as $event) {
91+
$this->events[] = $event->toArray();
92+
}
93+
94+
return $this;
95+
}
96+
97+
public function consent(): Helper\ConsentHelper
98+
{
99+
return $this->consent;
100+
}
101+
102+
public function userdata(): Helper\UserDataHelper
103+
{
104+
return $this->userdata;
105+
}
106+
107+
public function post(): void
108+
{
109+
if (empty($this->firebase_app_id)) {
110+
throw Ga4Exception::throwMissingFirebaseAppId();
111+
}
112+
113+
if (empty($this->api_secret)) {
114+
throw Ga4Exception::throwMissingApiSecret();
115+
}
116+
117+
if (empty($this->app_instance_id)) {
118+
throw Ga4Exception::throwMissingAppInstanceId();
119+
}
120+
121+
$url = $this->debug ? Facade\Type\AnalyticsType::URL_DEBUG : Facade\Type\AnalyticsType::URL_LIVE;
122+
$url .= '?' . http_build_query(['firebase_app_id' => $this->firebase_app_id, 'api_secret' => $this->api_secret]);
123+
124+
$body = array_replace_recursive(
125+
$this->toArray(),
126+
["app_instance_id" => $this->app_instance_id],
127+
["user_data" => !empty($this->user_id) ? $this->userdata->toArray() : []], // Only accepted if user_id is passed too
128+
["user_properties" => $this->user_properties],
129+
["consent" => $this->consent->toArray()],
130+
);
131+
132+
if (count($body["user_data"]) < 1) unset($body["user_data"]);
133+
if (count($body["user_properties"]) < 1) unset($body["user_properties"]);
134+
135+
$chunkEvents = array_chunk($this->events, 25);
136+
137+
if (count($chunkEvents) < 1) {
138+
throw Ga4Exception::throwMissingEvents();
139+
}
140+
141+
$this->userdata->reset();
142+
$this->user_properties = [];
143+
$this->events = [];
144+
145+
foreach ($chunkEvents as $events) {
146+
$body['events'] = $events;
147+
148+
$kB = 1024;
149+
if (($size = mb_strlen(json_encode($body))) > ($kB * 130)) {
150+
Ga4Exception::throwRequestTooLarge(intval($size / $kB));
151+
continue;
152+
}
153+
154+
$jsonBody = json_encode($body);
155+
$jsonBody = strtr($jsonBody, [':[]' => ':{}']);
156+
157+
$res = $this->guzzle->request('POST', $url, [
158+
'headers' => [
159+
'content-type' => 'application/json;charset=utf-8'
160+
],
161+
'body' => $jsonBody,
162+
]);
163+
164+
if (!in_array(($code = $res?->getStatusCode() ?? 0), Facade\Type\AnalyticsType::ACCEPT_RESPONSE_HEADERS)) {
165+
Ga4Exception::throwRequestWrongResponceCode($code);
166+
}
167+
168+
if ($code !== 204) {
169+
$callback = @json_decode($res->getBody()->getContents(), true);
170+
171+
if (json_last_error() != JSON_ERROR_NONE) {
172+
Ga4Exception::throwRequestInvalidResponse();
173+
} elseif (empty($callback)) {
174+
Ga4Exception::throwRequestEmptyResponse();
175+
} elseif (!empty($callback['validationMessages'])) {
176+
foreach ($callback['validationMessages'] as $msg) {
177+
Ga4Exception::throwRequestInvalidBody($msg);
178+
}
179+
}
180+
}
181+
}
182+
183+
if (Ga4Exception::hasThrowStack()) {
184+
throw Ga4Exception::getThrowStack();
185+
}
186+
}
187+
188+
public static function new(string $firebase_app_id, string $api_secret, bool $debug = false): static
189+
{
190+
return new static($firebase_app_id, $api_secret, $debug);
191+
}
192+
193+
/**
194+
* Deprecated references
195+
*/
196+
197+
/** @deprecated 1.1.9 Please use `Analytics->consent->setAdPersonalizationPermission()` instead */
198+
public function setNonPersonalizedAds(bool $exclude)
199+
{
200+
$this->consent->setAdPersonalizationPermission(!$exclude);
201+
return $this;
202+
}
203+
204+
/** @deprecated 1.1.1 Please use `Analytics->consent->setAdPersonalizationPermission()` instead */
205+
public function allowPersonalisedAds(bool $allow)
206+
{
207+
$this->consent->setAdPersonalizationPermission($allow);
208+
}
209+
210+
/** @deprecated 1.1.1 Please use `Analytics->setTimestampMicros()` instead */
211+
public function setTimestamp(int|float $microOrUnix)
212+
{
213+
$this->setTimestampMicros($microOrUnix);
214+
}
215+
}

src/Helper/EventHelper.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ abstract class EventHelper extends IOHelper implements EventType
1515
protected null|string $page_title;
1616
protected null|string $screen_resolution;
1717

18+
protected null|string $session_id;
19+
protected null|int $engagement_time_msec;
20+
1821
protected array $campaign = [];
1922

2023
public function setLanguage(string $lang)
@@ -60,6 +63,18 @@ public function setEventPage(DefaultEventParamsType $page)
6063
return $this;
6164
}
6265

66+
public function setSessionId(string $id)
67+
{
68+
$this->session_id = $id;
69+
return $this;
70+
}
71+
72+
public function setEngagementTimeMSec(int $msec)
73+
{
74+
$this->engagement_time_msec = $msec;
75+
return $this;
76+
}
77+
6378
public function toArray(): array
6479
{
6580
$return = [];

0 commit comments

Comments
 (0)