diff --git a/.github/workflows/cron.yml b/.github/workflows/cron.yml new file mode 100644 index 0000000..59dc99e --- /dev/null +++ b/.github/workflows/cron.yml @@ -0,0 +1,11 @@ +name: Cron | Monthly Health Check + +on: + workflow_dispatch: + schedule: + - cron: 0 2 1 * * + +jobs: + validate-master: + if: github.ref == 'refs/heads/master' + uses: ./.github/workflows/ci.yml diff --git a/README.md b/README.md index a136b07..b55741f 100644 --- a/README.md +++ b/README.md @@ -65,11 +65,26 @@ $analytics = Analytics::new( api_secret: 'xYzzX_xYzzXzxyZxX', debug: true|false ); + +// You can set CONSENT here if not done through the gtat.js +// Read full docs here: https://support.google.com/tagmanager/answer/13802165 +$consent = $analytics->consent(); // returns a consent handler + +// Sets consent for sending user data from the request's events +// and user properties to Google for advertising purposes. +$consent->setAdUserDataPermission(); +$consent->getAdUserDataPermission(); +$consent->clearAdUserDataPermission(); + +// Sets consent for personalized advertising for the user. +$consent->setAdPersonalizationPermission(); +$consent->getAdPersonalizationPermission(); +$consent->clearAdPersonalizationPermission(); ``` ### Data flow -`session_id` > Google Analytics does not specify a required type of **session or user id**. You are free to use any kind of **unique identifier** you want; the catch, however, is that Google Analytics populates some internal data with `gtag.js`, that is then referenced to their `_ga` cookie session id. Just be aware that `gtag.js` is using *client-side Javascript* and can therefore have some **GDPR complications** as requests back to Google Analytics contains client information; such as their IP Address. +`session_id` > Google Analytics does not specify a required type of **session or user id**. You are free to use any kind of **unique identifier** you want; the catch, however, is that Google Analytics populates some internal data with `gtag.js`, that is then referenced to their `_ga` cookie session id. Just be aware that `gtag.js` is using _client-side Javascript_ and can therefore have some **GDPR complications** as requests back to Google Analytics contains client information; such as their IP Address. 1. Acquire proper GDPR Consent 2. Client/GTAG.js sends session_start and first_visit to GA4 @@ -143,7 +158,7 @@ $event->setEventPage($eventPage); ![badge](https://shields.io/badge/AddShippingInfo-informational) ![badge](https://shields.io/badge/Purchase-informational) ![badge](https://shields.io/badge/Refund-informational) - + ### Engagement / Gaming ![badge](https://shields.io/badge/EarnVirtualCurrency-informational) @@ -175,12 +190,12 @@ foreach ($visitors as $collection) { // Group of events, perhaps need logic to change from json or array to event objects // Maybe its formatted well for the > ConvertHelper::parseEvents([...]) < helper $groups = $collection['events']; - + // If gtag.js, this can be the _ga or _gid cookie // This can be any kind of session identifier // Usually derives from $_COOKIE['_ga'] or $_COOKIE['_gid'] set by GTAG.js $visitor = $collection['session_id']; - + // load logged in user/visitor // This can be any kind of unique identifier, readable is easier for you // Just be wary not to use GDPR sensitive information @@ -192,14 +207,14 @@ foreach ($visitors as $collection) { $analytics = Analytics::new($measurementId, $apiSecret) ->setClientId($visitor) ->setTimestampMicros($time); - + if ($user !== null) { $analytics->setUserId($user); } - + $analytics->addUserParameter(...$data['userParameters']); // pseudo logic for adding user parameters $analytics->addEvent(...$data['events']); // pseudo logic for adding events - + $analytics->post(); // send events to Google Analytics } catch (Exception\Ga4Exception $exception) { // Handle exception @@ -216,27 +231,24 @@ foreach ($visitors as $collection) { ```js // array< array< eventName, array > > -axios.post( - '/your-api-endpoint/ga4-event-receiver', - [ - // Note each event is its own object inside an array as - // this allows to pass the same event type multiple times +axios.post("/your-api-endpoint/ga4-event-receiver", [ + // Note each event is its own object inside an array as + // this allows to pass the same event type multiple times + { + addToCart: { + currency: "EUR", + value: 13.37, + items: [ { - addToCart: { - currency: 'EUR', - value: 13.37, - items: [ - { - 'item_id': 1, - 'item_name': 'Cup', - 'price': 13.37, - 'quantity': 1 - } - ] - } - } - ] -) + item_id: 1, + item_name: "Cup", + price: 13.37, + quantity: 1, + }, + ], + }, + }, +]); ``` #### Backend @@ -274,7 +286,7 @@ class ExampleEvent extends AlexWestergaard\PhpGa4\Helper\EventHelper // variables should be nullable as unset() will set variable as null protected null|mixed $my_variable; protected null|mixed $my_required_variable; - + // Arrays should always be instanciated empty protected array $my_array = []; diff --git a/src/Analytics.php b/src/Analytics.php index e5c405d..9a0082b 100644 --- a/src/Analytics.php +++ b/src/Analytics.php @@ -61,12 +61,6 @@ public function getRequiredParams(): array return $return; } - public function setNonPersonalizedAds(bool $exclude) - { - $this->non_personalized_ads = $exclude; - return $this; - } - public function setClientId(string $id) { $this->client_id = $id; @@ -112,7 +106,7 @@ public function addEvent(Facade\Type\EventType ...$events) return $this; } - public function consent() + public function consent(): ConsentHelper { return $this->consent; } @@ -130,30 +124,23 @@ public function post(): void $url = $this->debug ? Facade\Type\AnalyticsType::URL_DEBUG : Facade\Type\AnalyticsType::URL_LIVE; $url .= '?' . http_build_query(['measurement_id' => $this->measurement_id, 'api_secret' => $this->api_secret]); - $body = $this->toArray(); - array_merge_recursive( + $body = array_replace_recursive( $this->toArray(), + ["user_properties" => $this->user_properties], ["consent" => $this->consent->toArray()], ); - $chunkUserProperties = array_chunk($this->user_properties, 25, true); - $this->user_properties = []; - $chunkEvents = array_chunk($this->events, 25); - $this->events = []; - $chunkMax = count($chunkEvents) > count($chunkUserProperties) ? count($chunkEvents) : count($chunkUserProperties); + if (count($chunkEvents) < 1) { + throw Ga4Exception::throwMissingEvents(); + } - for ($chunk = 0; $chunk < $chunkMax; $chunk++) { - $body['user_properties'] = $chunkUserProperties[$chunk] ?? []; - if (empty($body['user_properties'])) { - unset($body['user_properties']); - } + $this->user_properties = []; + $this->events = []; - $body['events'] = $chunkEvents[$chunk] ?? []; - if (empty($body['events'])) { - unset($body['events']); - } + foreach ($chunkEvents as $events) { + $body['events'] = $events; $kB = 1024; if (($size = mb_strlen(json_encode($body))) > ($kB * 130)) { @@ -204,13 +191,20 @@ public static function new(string $measurementId, string $apiSecret, bool $debug * Deprecated references */ - /** @deprecated 1.1.1 */ + /** @deprecated 1.1.9 Please use `Analytics->consent->setAdPersonalizationPermission()` instead */ + public function setNonPersonalizedAds(bool $exclude) + { + $this->consent->setAdPersonalizationPermission(!$exclude); + return $this; + } + + /** @deprecated 1.1.1 Please use `Analytics->consent->setAdPersonalizationPermission()` instead */ public function allowPersonalisedAds(bool $allow) { - $this->setNonPersonalizedAds(!$allow); + $this->consent->setAdPersonalizationPermission($allow); } - /** @deprecated 1.1.1 */ + /** @deprecated 1.1.1 Please use `Analytics->setTimestampMicros()` instead */ public function setTimestamp(int|float $microOrUnix) { $this->setTimestampMicros($microOrUnix); diff --git a/src/Exception/Ga4Exception.php b/src/Exception/Ga4Exception.php index c56b77a..a8eadd7 100644 --- a/src/Exception/Ga4Exception.php +++ b/src/Exception/Ga4Exception.php @@ -81,4 +81,9 @@ public static function throwRequestInvalidBody(array $msg) static::REQUEST_INVALID_BODY ); } + + public static function throwMissingEvents() + { + return new static("Request must include at least 1 event with a name", static::REQUEST_EMPTY_EVENTLIST); + } } diff --git a/src/Facade/Type/AnalyticsType.php b/src/Facade/Type/AnalyticsType.php index 2f04138..b12ccc2 100644 --- a/src/Facade/Type/AnalyticsType.php +++ b/src/Facade/Type/AnalyticsType.php @@ -43,10 +43,11 @@ public function setTimestampMicros(int|float $microOrUnix); public function setNonPersonalizedAds(bool $allow); /** - * The user properties for the measurement + * The user properties for the measurement (Up to 25 custom per project, see link) * * @var user_properties * @param AlexWestergaard\PhpGa4\Facade\Type\UserProperty $prop + * @link https://support.google.com/analytics/answer/14240153 */ public function addUserProperty(UserPropertyType ...$props); diff --git a/src/Facade/Type/Ga4ExceptionType.php b/src/Facade/Type/Ga4ExceptionType.php index fc1ba4f..1fd91a3 100644 --- a/src/Facade/Type/Ga4ExceptionType.php +++ b/src/Facade/Type/Ga4ExceptionType.php @@ -24,4 +24,5 @@ interface Ga4ExceptionType const REQUEST_INVALID_BODY = 104005; const REQUEST_MISSING_MEASUREMENT_ID = 104006; const REQUEST_MISSING_API_SECRET = 104007; + const REQUEST_EMPTY_EVENTLIST = 104008; } diff --git a/src/Helper/ConsentHelper.php b/src/Helper/ConsentHelper.php index be034f1..eb5e57a 100644 --- a/src/Helper/ConsentHelper.php +++ b/src/Helper/ConsentHelper.php @@ -4,8 +4,8 @@ class ConsentHelper { - const GRANTED = "granted"; - const DENIED = "denied"; + const GRANTED = "GRANTED"; + const DENIED = "DENIED"; private ?string $ad_user_data = null; private ?string $ad_personalization = null; diff --git a/test/Unit/AnalyticsTest.php b/test/Unit/AnalyticsTest.php index b654a47..5d43be2 100644 --- a/test/Unit/AnalyticsTest.php +++ b/test/Unit/AnalyticsTest.php @@ -6,6 +6,7 @@ use AlexWestergaard\PhpGa4\Facade; use AlexWestergaard\PhpGa4\Event; use AlexWestergaard\PhpGa4\Analytics; +use AlexWestergaard\PhpGa4\Event\Login; use AlexWestergaard\PhpGa4Test\TestCase; final class AnalyticsTest extends TestCase @@ -17,7 +18,6 @@ public function test_can_configure_and_export() $this->prefill['api_secret'], $debug = true ) - ->setNonPersonalizedAds($nonPersonalisedAds = true) ->setClientId($this->prefill['client_id']) ->setUserId($this->prefill['user_id']) ->setTimestampMicros($time = time()) @@ -27,7 +27,6 @@ public function test_can_configure_and_export() $asArray = $analytics->toArray(); $this->assertIsArray($asArray); - $this->assertArrayHasKey('non_personalized_ads', $asArray); $this->assertArrayHasKey('timestamp_micros', $asArray); $this->assertArrayHasKey('client_id', $asArray); $this->assertArrayHasKey('user_id', $asArray); @@ -36,7 +35,6 @@ public function test_can_configure_and_export() $timeAsMicro = $time * 1_000_000; - $this->assertEquals($nonPersonalisedAds, $asArray['non_personalized_ads']); $this->assertEquals($timeAsMicro, $asArray['timestamp_micros']); $this->assertEquals($this->prefill['client_id'], $asArray['client_id']); $this->assertEquals($this->prefill['user_id'], $asArray['user_id']); @@ -46,7 +44,7 @@ public function test_can_configure_and_export() public function test_can_post_to_google() { - $this->assertNull($this->analytics->post()); + $this->assertNull($this->analytics->addEvent(Login::new())->post()); } public function test_converts_to_full_microtime_stamp() @@ -68,6 +66,8 @@ public function test_throws_if_microtime_older_than_three_days() public function test_exports_userproperty_to_array() { + $this->analytics->addEvent(Login::new()); + $userProperty = UserProperty::new() ->setName('customer_tier') ->setValue('premium'); @@ -138,7 +138,7 @@ public function test_throws_on_too_large_request_package() $userProperty->setValue($overflowValue); } - $this->analytics->addUserProperty($userProperty)->post(); + $this->analytics->addEvent(Login::new())->addUserProperty($userProperty)->post(); } public function test_timeasmicro_throws_exceeding_max() diff --git a/test/Unit/ConsentTest.php b/test/Unit/ConsentTest.php index 77ffd00..bdc2856 100644 --- a/test/Unit/ConsentTest.php +++ b/test/Unit/ConsentTest.php @@ -2,6 +2,7 @@ namespace AlexWestergaard\PhpGa4Test\Unit; +use AlexWestergaard\PhpGa4\Event\Login; use AlexWestergaard\PhpGa4\Helper\ConsentHelper; use AlexWestergaard\PhpGa4Test\TestCase; @@ -9,6 +10,8 @@ final class ConsentTest extends TestCase { public function test_no_consent_is_empty() { + $this->analytics->addEvent(Login::new()); + $export = $this->analytics->consent()->toArray(); $this->assertIsArray($export); $this->assertCount(0, $export); @@ -16,6 +19,8 @@ public function test_no_consent_is_empty() public function test_consent_ad_user_data_granted() { + $this->analytics->addEvent(Login::new()); + $this->analytics->consent()->setAdUserDataPermission(true); $export = $this->analytics->consent()->toArray(); @@ -28,6 +33,8 @@ public function test_consent_ad_user_data_granted() public function test_consent_ad_personalization_granted() { + $this->analytics->addEvent(Login::new()); + $this->analytics->consent()->setAdPersonalizationPermission(true); $export = $this->analytics->consent()->toArray(); @@ -40,6 +47,8 @@ public function test_consent_ad_personalization_granted() public function test_consent_granted() { + $this->analytics->addEvent(Login::new()); + $this->analytics->consent()->setAdUserDataPermission(true); $this->analytics->consent()->setAdPersonalizationPermission(true); @@ -54,6 +63,8 @@ public function test_consent_granted() public function test_consent_granted_posted() { + $this->analytics->addEvent(Login::new()); + $this->analytics->consent()->setAdUserDataPermission(true); $this->analytics->consent()->setAdPersonalizationPermission(true); @@ -69,6 +80,8 @@ public function test_consent_granted_posted() public function test_consent_ad_user_data_denied() { + $this->analytics->addEvent(Login::new()); + $this->analytics->consent()->setAdUserDataPermission(false); $export = $this->analytics->consent()->toArray(); @@ -81,6 +94,8 @@ public function test_consent_ad_user_data_denied() public function test_consent_ad_personalization_denied() { + $this->analytics->addEvent(Login::new()); + $this->analytics->consent()->setAdPersonalizationPermission(false); $export = $this->analytics->consent()->toArray(); @@ -93,6 +108,8 @@ public function test_consent_ad_personalization_denied() public function test_consent_denied() { + $this->analytics->addEvent(Login::new()); + $this->analytics->consent()->setAdUserDataPermission(false); $this->analytics->consent()->setAdPersonalizationPermission(false); @@ -107,6 +124,8 @@ public function test_consent_denied() public function test_consent_denied_posted() { + $this->analytics->addEvent(Login::new()); + $this->analytics->consent()->setAdUserDataPermission(false); $this->analytics->consent()->setAdPersonalizationPermission(false);