Skip to content

Commit 61ded66

Browse files
authored
Add: Send user-provided data with User-ID using the Measurement Protocol (#91)
2 parents 6d82bff + 4dcb1d2 commit 61ded66

File tree

5 files changed

+321
-4
lines changed

5 files changed

+321
-4
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -372,3 +372,4 @@ Two important points:
372372
- [Measurement Protocol: Events](https://developers.google.com/analytics/devguides/collection/protocol/ga4/reference/events)
373373
- [Reserved Event Names](https://developers.google.com/analytics/devguides/collection/protocol/ga4/reference?client_type=gtag#reserved_event_names)
374374
- [Measurement Protocol: Validation](https://developers.google.com/analytics/devguides/collection/protocol/ga4/validating-events?client_type=gtag)
375+
- [Measurement Protocol: User Data](https://developers.google.com/analytics/devguides/collection/ga4/uid-data)

src/Analytics.php

+15-4
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
use AlexWestergaard\PhpGa4\Helper;
77
use AlexWestergaard\PhpGa4\Facade;
88
use AlexWestergaard\PhpGa4\Exception\Ga4Exception;
9-
use AlexWestergaard\PhpGa4\Helper\ConsentHelper;
109

1110
/**
1211
* Analytics wrapper to contain UserProperties and Events to post on Google Analytics
@@ -15,7 +14,8 @@ class Analytics extends Helper\IOHelper implements Facade\Type\AnalyticsType
1514
{
1615
private Guzzle $guzzle;
1716

18-
private ConsentHelper $consent;
17+
private Helper\ConsentHelper $consent;
18+
private Helper\UserDataHelper $userdata;
1919

2020
protected null|bool $non_personalized_ads = false;
2121
protected null|int $timestamp_micros;
@@ -31,7 +31,8 @@ public function __construct(
3131
) {
3232
parent::__construct();
3333
$this->guzzle = new Guzzle();
34-
$this->consent = new ConsentHelper();
34+
$this->consent = new Helper\ConsentHelper();
35+
$this->userdata = new Helper\UserDataHelper();
3536
}
3637

3738
public function getParams(): array
@@ -106,11 +107,16 @@ public function addEvent(Facade\Type\EventType ...$events)
106107
return $this;
107108
}
108109

109-
public function consent(): ConsentHelper
110+
public function consent(): Helper\ConsentHelper
110111
{
111112
return $this->consent;
112113
}
113114

115+
public function userdata(): Helper\UserDataHelper
116+
{
117+
return $this->userdata;
118+
}
119+
114120
public function post(): void
115121
{
116122
if (empty($this->measurement_id)) {
@@ -126,16 +132,21 @@ public function post(): void
126132

127133
$body = array_replace_recursive(
128134
$this->toArray(),
135+
["user_data" => $this->user_id != null ? $this->userdata->toArray() : []], // Only accepted if user_id is passed too
129136
["user_properties" => $this->user_properties],
130137
["consent" => $this->consent->toArray()],
131138
);
132139

140+
if (count($body["user_data"]) < 1) unset($body["user_data"]);
141+
if (count($body["user_properties"]) < 1) unset($body["user_properties"]);
142+
133143
$chunkEvents = array_chunk($this->events, 25);
134144

135145
if (count($chunkEvents) < 1) {
136146
throw Ga4Exception::throwMissingEvents();
137147
}
138148

149+
$this->userdata->reset();
139150
$this->user_properties = [];
140151
$this->events = [];
141152

src/Helper/CountryIsoHelper.php

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
namespace AlexWestergaard\PhpGa4\Helper;
4+
5+
class CountryIsoHelper
6+
{
7+
static public function valid(string $iso): bool
8+
{
9+
return array_search(mb_strtoupper(trim($iso)), [
10+
"AF", "AX", "AL", "DZ", "AS", "AD", "AO", "AI", "AQ", "AG", "AR", "AM", "AW",
11+
"AU", "AT", "AZ", "BS", "BH", "BD", "BB", "BY", "BE", "BZ", "BJ", "BM", "BT",
12+
"BO", "BA", "BW", "BV", "BR", "IO", "BN", "BG", "BF", "BI", "KH", "CM", "CA",
13+
"CV", "KY", "CF", "TD", "CL", "CN", "CX", "CC", "CO", "KM", "CG", "CD", "CK",
14+
"CR", "CI", "HR", "CU", "CY", "CZ", "DK", "DJ", "DM", "DO", "EC", "EG", "SV",
15+
"GQ", "ER", "EE", "ET", "FK", "FO", "FJ", "FI", "FR", "GF", "PF", "TF", "GA",
16+
"GM", "GE", "DE", "GH", "GI", "GR", "GL", "GD", "GP", "GU", "GT", "GG", "GN",
17+
"GW", "GY", "HT", "HM", "VA", "HN", "HK", "HU", "IS", "IN", "ID", "IR", "IQ",
18+
"IE", "IM", "IL", "IT", "JM", "JP", "JE", "JO", "KZ", "KE", "KI", "KR", "KP",
19+
"KW", "KG", "LA", "LV", "LB", "LS", "LR", "LY", "LI", "LT", "LU", "MO", "MK",
20+
"MG", "MW", "MY", "MV", "ML", "MT", "MH", "MQ", "MR", "MU", "YT", "MX", "FM",
21+
"MD", "MC", "MN", "ME", "MS", "MA", "MZ", "MM", "NA", "NR", "NP", "NL", "AN",
22+
"NC", "NZ", "NI", "NE", "NG", "NU", "NF", "MP", "NO", "OM", "PK", "PW", "PS",
23+
"PA", "PG", "PY", "PE", "PH", "PN", "PL", "PT", "PR", "QA", "RE", "RO", "RU",
24+
"RW", "BL", "SH", "KN", "LC", "MF", "PM", "VC", "WS", "SM", "ST", "SA", "SN",
25+
"RS", "SC", "SL", "SG", "SK", "SI", "SB", "SO", "ZA", "GS", "ES", "LK", "SD",
26+
"SR", "SJ", "SZ", "SE", "CH", "SY", "TW", "TJ", "TZ", "TH", "TL", "TG", "TK",
27+
"TO", "TT", "TN", "TR", "TM", "TC", "TV", "UG", "UA", "AE", "GB", "US", "UM",
28+
"UY", "UZ", "VU", "VE", "VN", "VG", "VI", "WF", "EH", "YE", "ZM", "ZW",
29+
], true) !== false;
30+
}
31+
}

src/Helper/UserDataHelper.php

+219
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
<?php
2+
3+
namespace AlexWestergaard\PhpGa4\Helper;
4+
5+
class UserDataHelper
6+
{
7+
private ?string $sha256_email_address = null;
8+
private ?string $sha256_phone_number = null;
9+
10+
private ?string $sha256_first_name = null;
11+
private ?string $sha256_last_name = null;
12+
private ?string $sha256_street = null;
13+
private ?string $city = null;
14+
private ?string $region = null;
15+
private ?string $postal_code = null;
16+
private ?string $country = null;
17+
18+
public function reset(): void
19+
{
20+
$this->sha256_email_address = null;
21+
$this->sha256_phone_number = null;
22+
$this->sha256_first_name = null;
23+
$this->sha256_last_name = null;
24+
$this->sha256_street = null;
25+
$this->city = null;
26+
$this->region = null;
27+
$this->postal_code = null;
28+
$this->country = null;
29+
}
30+
31+
/**
32+
* @param string $email
33+
* @return bool
34+
*/
35+
public function setEmail(string $email): bool
36+
{
37+
$email = str_replace(" ", "", mb_strtolower($email));
38+
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) return false;
39+
40+
// https://support.google.com/mail/answer/7436150
41+
if (
42+
substr($email, -mb_strlen("@gmail.com")) == "@gmail.com" ||
43+
substr($email, -mb_strlen("@googlemail.com")) == "@googlemail.com"
44+
) {
45+
[$addr, $host] = explode("@", $email, 2);
46+
// https://support.google.com/mail/thread/125577450/gmail-and-googlemail
47+
if ($host == "googlemail.com") {
48+
$host = "gmail.com";
49+
}
50+
// https://gmail.googleblog.com/2008/03/2-hidden-ways-to-get-more-from-your.html
51+
$addr = explode("+", $addr, 2)[0];
52+
$addr = str_replace(".", "", $addr);
53+
$email = implode("@", [trim($addr), trim($host)]);
54+
}
55+
56+
$this->sha256_email_address = hash("sha256", $email);
57+
return true;
58+
}
59+
60+
/**
61+
* @param int $number International number (without prefix "+" and dashes) eg. \
62+
* "+1-123-4567890" for USA or\
63+
* "+44-1234-5678900" for UK or\
64+
* "+45-12345678" for DK
65+
* @return bool
66+
*/
67+
public function setPhone(int $number): bool
68+
{
69+
$sNumber = strval($number);
70+
if (strlen($sNumber) < 3 || strlen($sNumber) > 15) {
71+
return false;
72+
}
73+
74+
$this->sha256_phone_number = hash("sha256", "+{$sNumber}");
75+
return true;
76+
}
77+
78+
/**
79+
* @param string $firstName Users first name
80+
* @return bool
81+
*/
82+
public function setFirstName(string $firstName): bool
83+
{
84+
if (empty($firstName)) return false;
85+
$this->sha256_first_name = hash("sha256", $this->strip($firstName, true));
86+
return true;
87+
}
88+
89+
/**
90+
* @param string $lastName Users last name
91+
* @return bool
92+
*/
93+
public function setLastName(string $lastName): bool
94+
{
95+
if (empty($lastName)) return false;
96+
$this->sha256_last_name = hash("sha256", $this->strip($lastName, true));
97+
return true;
98+
}
99+
100+
/**
101+
* @param string $street Users street name
102+
* @return bool
103+
*/
104+
public function setStreet(string $street): bool
105+
{
106+
if (empty($street)) return false;
107+
$this->sha256_street = hash("sha256", $this->strip($street));
108+
return true;
109+
}
110+
111+
/**
112+
* @param string $city Users city name
113+
* @return bool
114+
*/
115+
public function setCity(string $city): bool
116+
{
117+
if (empty($city)) return false;
118+
$this->city = $this->strip($city, true);
119+
return true;
120+
}
121+
122+
/**
123+
* @param string $region Users region name
124+
* @return bool
125+
*/
126+
public function setRegion(string $region): bool
127+
{
128+
if (empty($region)) return false;
129+
$this->region = $this->strip($region, true);
130+
return true;
131+
}
132+
133+
/**
134+
* @param string $postalCode Users postal code
135+
* @return bool
136+
*/
137+
public function setPostalCode(string $postalCode): bool
138+
{
139+
if (empty($postalCode)) return false;
140+
$this->postal_code = $this->strip($postalCode);
141+
return true;
142+
}
143+
144+
/**
145+
* @param string $iso Users country (ISO)
146+
* @return bool
147+
*/
148+
public function setCountry(string $iso): bool
149+
{
150+
if (!CountryIsoHelper::valid($iso)) {
151+
return false;
152+
}
153+
154+
$this->country = mb_strtoupper(trim($iso));
155+
return true;
156+
}
157+
158+
public function toArray(): array
159+
{
160+
$res = [];
161+
162+
if (!empty($this->sha256_email_address)) {
163+
$res["sha256_email_address"] = $this->sha256_email_address;
164+
}
165+
166+
if (!empty($this->sha256_phone_number)) {
167+
$res["sha256_phone_number"] = $this->sha256_phone_number;
168+
}
169+
170+
$addr = [];
171+
172+
if (!empty($this->sha256_first_name)) {
173+
$addr["sha256_first_name"] = $this->sha256_first_name;
174+
}
175+
176+
if (!empty($this->sha256_last_name)) {
177+
$addr["sha256_last_name"] = $this->sha256_last_name;
178+
}
179+
180+
if (!empty($this->sha256_street)) {
181+
$addr["sha256_street"] = $this->sha256_street;
182+
}
183+
184+
if (!empty($this->city)) {
185+
$addr["city"] = $this->city;
186+
}
187+
188+
if (!empty($this->region)) {
189+
$addr["region"] = $this->region;
190+
}
191+
192+
if (!empty($this->postal_code)) {
193+
$addr["postal_code"] = $this->postal_code;
194+
}
195+
196+
if (!empty($this->country)) {
197+
$addr["country"] = $this->country;
198+
}
199+
200+
if (!empty($this->sha256_phone_number)) {
201+
$res["sha256_phone_number"] = $this->sha256_phone_number;
202+
}
203+
204+
if (count($addr) > 0) {
205+
$res["address"] = $addr;
206+
}
207+
208+
return $res;
209+
}
210+
211+
private function strip(string $s, bool $removeDigits = false): string
212+
{
213+
$d = $removeDigits ? '0-9' : '';
214+
215+
$s = preg_replace("[^a-zA-Z{$d}\-\_\.\,\s]", "", $s);
216+
$s = mb_strtolower($s);
217+
return trim($s);
218+
}
219+
}

test/Unit/UserDataTest.php

+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<?php
2+
3+
namespace AlexWestergaard\PhpGa4Test\Unit;
4+
5+
use AlexWestergaard\PhpGa4\Event\Login;
6+
use AlexWestergaard\PhpGa4\Helper\UserDataHelper;
7+
use AlexWestergaard\PhpGa4Test\TestCase;
8+
9+
final class UserDataTest extends TestCase
10+
{
11+
public function test_user_data_is_fillable()
12+
{
13+
$uda = new UserDataHelper();
14+
$this->assertTrue($uda->setEmail($setEmail = "[email protected]"));
15+
$this->assertTrue($uda->setPhone($setPhone = 4500000000));
16+
$this->assertTrue($uda->setFirstName($setFirstName = "test"));
17+
$this->assertTrue($uda->setLastName($setLastName = "person"));
18+
$this->assertTrue($uda->setStreet($setStreet = "some street 11"));
19+
$this->assertTrue($uda->setCity($setCity = "somewhere"));
20+
$this->assertTrue($uda->setRegion($setRegion = "inthere"));
21+
$this->assertTrue($uda->setPostalCode($setPostalCode = "1234"));
22+
$this->assertTrue($uda->setCountry($setCountry = "DK"));
23+
24+
$export = $uda->toArray();
25+
$this->assertIsArray($export);
26+
$this->assertEquals(hash("sha256", $setEmail), $export["sha256_email_address"], $setEmail);
27+
$this->assertEquals(hash("sha256", '+' . $setPhone), $export["sha256_phone_number"], $setPhone);
28+
29+
$this->assertArrayHasKey("address", $export);
30+
$this->assertIsArray($export["address"]);
31+
$this->assertEquals(hash("sha256", $setFirstName), $export["address"]["sha256_first_name"], $setFirstName);
32+
$this->assertEquals(hash("sha256", $setLastName), $export["address"]["sha256_last_name"], $setLastName);
33+
$this->assertEquals(hash("sha256", $setStreet), $export["address"]["sha256_street"], $setStreet);
34+
$this->assertEquals($setCity, $export["address"]["city"], $setCity);
35+
$this->assertEquals($setRegion, $export["address"]["region"], $setRegion);
36+
$this->assertEquals($setPostalCode, $export["address"]["postal_code"], $setPostalCode);
37+
$this->assertEquals($setCountry, $export["address"]["country"], $setCountry);
38+
}
39+
public function test_user_data_is_sendable()
40+
{
41+
$uad = $this->analytics->userdata();
42+
$uad->setEmail("[email protected]");
43+
$uad->setPhone(4500000000);
44+
$uad->setFirstName("test");
45+
$uad->setLastName("person");
46+
$uad->setStreet("some street 11");
47+
$uad->setCity("somewhere");
48+
$uad->setRegion("inthere");
49+
$uad->setPostalCode("1234");
50+
$uad->setCountry("DK");
51+
52+
$this->analytics->addEvent(Login::new());
53+
$this->assertNull($this->analytics->post());
54+
}
55+
}

0 commit comments

Comments
 (0)