diff --git a/README.md b/README.md index b55741f..93b6466 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/src/Analytics.php b/src/Analytics.php index 9a0082b..20daca2 100644 --- a/src/Analytics.php +++ b/src/Analytics.php @@ -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 @@ -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; @@ -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 @@ -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)) { @@ -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 = []; diff --git a/src/Helper/CountryIsoHelper.php b/src/Helper/CountryIsoHelper.php new file mode 100644 index 0000000..f2e6d0d --- /dev/null +++ b/src/Helper/CountryIsoHelper.php @@ -0,0 +1,31 @@ +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); + } +} diff --git a/test/Unit/UserDataTest.php b/test/Unit/UserDataTest.php new file mode 100644 index 0000000..2055d81 --- /dev/null +++ b/test/Unit/UserDataTest.php @@ -0,0 +1,55 @@ +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()); + } +}