Skip to content

Commit

Permalink
Add: Send user-provided data with User-ID using the Measurement Proto…
Browse files Browse the repository at this point in the history
…col (#91)
  • Loading branch information
aawnu authored Jul 2, 2024
2 parents 6d82bff + 4dcb1d2 commit 61ded66
Show file tree
Hide file tree
Showing 5 changed files with 321 additions and 4 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -372,3 +372,4 @@ Two important points:
- [Measurement Protocol: Events](https://developers.google.com/analytics/devguides/collection/protocol/ga4/reference/events)
- [Reserved Event Names](https://developers.google.com/analytics/devguides/collection/protocol/ga4/reference?client_type=gtag#reserved_event_names)
- [Measurement Protocol: Validation](https://developers.google.com/analytics/devguides/collection/protocol/ga4/validating-events?client_type=gtag)
- [Measurement Protocol: User Data](https://developers.google.com/analytics/devguides/collection/ga4/uid-data)
19 changes: 15 additions & 4 deletions src/Analytics.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
use AlexWestergaard\PhpGa4\Helper;
use AlexWestergaard\PhpGa4\Facade;
use AlexWestergaard\PhpGa4\Exception\Ga4Exception;
use AlexWestergaard\PhpGa4\Helper\ConsentHelper;

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

private ConsentHelper $consent;
private Helper\ConsentHelper $consent;
private Helper\UserDataHelper $userdata;

protected null|bool $non_personalized_ads = false;
protected null|int $timestamp_micros;
Expand All @@ -31,7 +31,8 @@ public function __construct(
) {
parent::__construct();
$this->guzzle = new Guzzle();
$this->consent = new ConsentHelper();
$this->consent = new Helper\ConsentHelper();
$this->userdata = new Helper\UserDataHelper();
}

public function getParams(): array
Expand Down Expand Up @@ -106,11 +107,16 @@ public function addEvent(Facade\Type\EventType ...$events)
return $this;
}

public function consent(): ConsentHelper
public function consent(): Helper\ConsentHelper
{
return $this->consent;
}

public function userdata(): Helper\UserDataHelper
{
return $this->userdata;
}

public function post(): void
{
if (empty($this->measurement_id)) {
Expand All @@ -126,16 +132,21 @@ public function post(): void

$body = array_replace_recursive(
$this->toArray(),
["user_data" => $this->user_id != null ? $this->userdata->toArray() : []], // Only accepted if user_id is passed too
["user_properties" => $this->user_properties],
["consent" => $this->consent->toArray()],
);

if (count($body["user_data"]) < 1) unset($body["user_data"]);
if (count($body["user_properties"]) < 1) unset($body["user_properties"]);

$chunkEvents = array_chunk($this->events, 25);

if (count($chunkEvents) < 1) {
throw Ga4Exception::throwMissingEvents();
}

$this->userdata->reset();
$this->user_properties = [];
$this->events = [];

Expand Down
31 changes: 31 additions & 0 deletions src/Helper/CountryIsoHelper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

namespace AlexWestergaard\PhpGa4\Helper;

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

namespace AlexWestergaard\PhpGa4\Helper;

class UserDataHelper
{
private ?string $sha256_email_address = null;
private ?string $sha256_phone_number = null;

private ?string $sha256_first_name = null;
private ?string $sha256_last_name = null;
private ?string $sha256_street = null;
private ?string $city = null;
private ?string $region = null;
private ?string $postal_code = null;
private ?string $country = null;

public function reset(): void
{
$this->sha256_email_address = null;
$this->sha256_phone_number = null;
$this->sha256_first_name = null;
$this->sha256_last_name = null;
$this->sha256_street = null;
$this->city = null;
$this->region = null;
$this->postal_code = null;
$this->country = null;
}

/**
* @param string $email
* @return bool
*/
public function setEmail(string $email): bool
{
$email = str_replace(" ", "", mb_strtolower($email));
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) return false;

// https://support.google.com/mail/answer/7436150
if (
substr($email, -mb_strlen("@gmail.com")) == "@gmail.com" ||
substr($email, -mb_strlen("@googlemail.com")) == "@googlemail.com"
) {
[$addr, $host] = explode("@", $email, 2);
// https://support.google.com/mail/thread/125577450/gmail-and-googlemail
if ($host == "googlemail.com") {
$host = "gmail.com";
}
// https://gmail.googleblog.com/2008/03/2-hidden-ways-to-get-more-from-your.html
$addr = explode("+", $addr, 2)[0];
$addr = str_replace(".", "", $addr);
$email = implode("@", [trim($addr), trim($host)]);
}

$this->sha256_email_address = hash("sha256", $email);
return true;
}

/**
* @param int $number International number (without prefix "+" and dashes) eg. \
* "+1-123-4567890" for USA or\
* "+44-1234-5678900" for UK or\
* "+45-12345678" for DK
* @return bool
*/
public function setPhone(int $number): bool
{
$sNumber = strval($number);
if (strlen($sNumber) < 3 || strlen($sNumber) > 15) {
return false;
}

$this->sha256_phone_number = hash("sha256", "+{$sNumber}");
return true;
}

/**
* @param string $firstName Users first name
* @return bool
*/
public function setFirstName(string $firstName): bool
{
if (empty($firstName)) return false;
$this->sha256_first_name = hash("sha256", $this->strip($firstName, true));
return true;
}

/**
* @param string $lastName Users last name
* @return bool
*/
public function setLastName(string $lastName): bool
{
if (empty($lastName)) return false;
$this->sha256_last_name = hash("sha256", $this->strip($lastName, true));
return true;
}

/**
* @param string $street Users street name
* @return bool
*/
public function setStreet(string $street): bool
{
if (empty($street)) return false;
$this->sha256_street = hash("sha256", $this->strip($street));
return true;
}

/**
* @param string $city Users city name
* @return bool
*/
public function setCity(string $city): bool
{
if (empty($city)) return false;
$this->city = $this->strip($city, true);
return true;
}

/**
* @param string $region Users region name
* @return bool
*/
public function setRegion(string $region): bool
{
if (empty($region)) return false;
$this->region = $this->strip($region, true);
return true;
}

/**
* @param string $postalCode Users postal code
* @return bool
*/
public function setPostalCode(string $postalCode): bool
{
if (empty($postalCode)) return false;
$this->postal_code = $this->strip($postalCode);
return true;
}

/**
* @param string $iso Users country (ISO)
* @return bool
*/
public function setCountry(string $iso): bool
{
if (!CountryIsoHelper::valid($iso)) {
return false;
}

$this->country = mb_strtoupper(trim($iso));
return true;
}

public function toArray(): array
{
$res = [];

if (!empty($this->sha256_email_address)) {
$res["sha256_email_address"] = $this->sha256_email_address;
}

if (!empty($this->sha256_phone_number)) {
$res["sha256_phone_number"] = $this->sha256_phone_number;
}

$addr = [];

if (!empty($this->sha256_first_name)) {
$addr["sha256_first_name"] = $this->sha256_first_name;
}

if (!empty($this->sha256_last_name)) {
$addr["sha256_last_name"] = $this->sha256_last_name;
}

if (!empty($this->sha256_street)) {
$addr["sha256_street"] = $this->sha256_street;
}

if (!empty($this->city)) {
$addr["city"] = $this->city;
}

if (!empty($this->region)) {
$addr["region"] = $this->region;
}

if (!empty($this->postal_code)) {
$addr["postal_code"] = $this->postal_code;
}

if (!empty($this->country)) {
$addr["country"] = $this->country;
}

if (!empty($this->sha256_phone_number)) {
$res["sha256_phone_number"] = $this->sha256_phone_number;
}

if (count($addr) > 0) {
$res["address"] = $addr;
}

return $res;
}

private function strip(string $s, bool $removeDigits = false): string
{
$d = $removeDigits ? '0-9' : '';

$s = preg_replace("[^a-zA-Z{$d}\-\_\.\,\s]", "", $s);
$s = mb_strtolower($s);
return trim($s);
}
}
55 changes: 55 additions & 0 deletions test/Unit/UserDataTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php

namespace AlexWestergaard\PhpGa4Test\Unit;

use AlexWestergaard\PhpGa4\Event\Login;
use AlexWestergaard\PhpGa4\Helper\UserDataHelper;
use AlexWestergaard\PhpGa4Test\TestCase;

final class UserDataTest extends TestCase
{
public function test_user_data_is_fillable()
{
$uda = new UserDataHelper();
$this->assertTrue($uda->setEmail($setEmail = "[email protected]"));
$this->assertTrue($uda->setPhone($setPhone = 4500000000));
$this->assertTrue($uda->setFirstName($setFirstName = "test"));
$this->assertTrue($uda->setLastName($setLastName = "person"));
$this->assertTrue($uda->setStreet($setStreet = "some street 11"));
$this->assertTrue($uda->setCity($setCity = "somewhere"));
$this->assertTrue($uda->setRegion($setRegion = "inthere"));
$this->assertTrue($uda->setPostalCode($setPostalCode = "1234"));
$this->assertTrue($uda->setCountry($setCountry = "DK"));

$export = $uda->toArray();
$this->assertIsArray($export);
$this->assertEquals(hash("sha256", $setEmail), $export["sha256_email_address"], $setEmail);
$this->assertEquals(hash("sha256", '+' . $setPhone), $export["sha256_phone_number"], $setPhone);

$this->assertArrayHasKey("address", $export);
$this->assertIsArray($export["address"]);
$this->assertEquals(hash("sha256", $setFirstName), $export["address"]["sha256_first_name"], $setFirstName);
$this->assertEquals(hash("sha256", $setLastName), $export["address"]["sha256_last_name"], $setLastName);
$this->assertEquals(hash("sha256", $setStreet), $export["address"]["sha256_street"], $setStreet);
$this->assertEquals($setCity, $export["address"]["city"], $setCity);
$this->assertEquals($setRegion, $export["address"]["region"], $setRegion);
$this->assertEquals($setPostalCode, $export["address"]["postal_code"], $setPostalCode);
$this->assertEquals($setCountry, $export["address"]["country"], $setCountry);
}
public function test_user_data_is_sendable()
{
$uad = $this->analytics->userdata();
$uad->setEmail("[email protected]");
$uad->setPhone(4500000000);
$uad->setFirstName("test");
$uad->setLastName("person");
$uad->setStreet("some street 11");
$uad->setCity("somewhere");
$uad->setRegion("inthere");
$uad->setPostalCode("1234");
$uad->setCountry("DK");

$this->analytics->addEvent(Login::new());
$this->assertNull($this->analytics->post());
}
}

0 comments on commit 61ded66

Please sign in to comment.