Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add: Send user-provided data with User-ID using the Measurement Protocol #91

Merged
merged 15 commits into from
Jul 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 = "test@gmail.com"));
$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("test@gmail.com");
$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());
}
}