Skip to content
Closed
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
85 changes: 85 additions & 0 deletions agent.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# Project Summary

## Objective
Implemented the `todo.md` GA4 backlog in this repository without executing runtime tracking flows.

## What was implemented

### 1) Event payload correctness and completeness
- Fixed event serialization so `session_id` and `engagement_time_msec` are now included in event `params`.
- Added event-level timestamp override support (`event.timestamp_micros`) via `setEventTimestampMicros(...)`.
- Added campaign parameter setters and merge support in event helpers:
- `campaign_source`, `campaign_medium`, `campaign_name`, `campaign_term`, `campaign_content`, `campaign_id`.

### 2) Top-level Measurement Protocol request fields
- Added support for top-level request fields in both Analytics and Firebase payload models:
- `non_personalized_ads`
- `validation_behavior`
- `ip_override`
- `user_location`
- `device`
- Added public setters for the above in both models.

### 3) User properties and user data
- Added user property timestamp support via `setTimestampMicros(...)`, serialized as `set_timestamp_micros`.
- Removed hard gate that only allowed `user_data` when `user_id` was present; payload now includes `user_data` when provided.
- Improved phone normalization API to accept `int|string` and sanitize formatted inputs to E.164-compatible hashing format.

### 4) Cookie/session integration helpers
- Improved session cookie parsing (`parseSessionCookie`) for GS1/GS2 formats:
- fixed `timestamp` key typo
- normalized GS2 values by removing token prefixes
- added version/domain metadata consistently
- Added client ID extraction helpers:
- `parseClientIdFromGaCookie(...)`
- `parseClientIdFromCookies(...)`
- `parseGaCookies(...)`

### 5) Required-parameter audit updates
- Tightened required params for selected events based on GA4 recommended semantics used in backlog:
- `EarnVirtualCurrency`: requires `virtual_currency_name`, `value`
- `JoinGroup`: requires `group_id`
- `LevelUp`: requires `level`
- `Search`: requires `search_term`

### 6) Tests and backlog tracking
- Updated and extended tests for:
- event timestamp override
- campaign serialization
- top-level request fields
- user property timestamp
- cookie parsing normalization and client ID helpers
- formatted phone normalization
- Marked `todo.md` tasks complete.

## Files added
- `agent.md`

## Notable files updated
- `src/Helper/EventMainHelper.php`
- `src/Helper/EventHelper.php`
- `src/Helper/EventParamsHelper.php`
- `src/Analytics.php`
- `src/Firebase.php`
- `src/Helper/UserPropertyHelper.php`
- `src/UserProperty.php`
- `src/Helper/UserDataHelper.php`
- `src/Helper/ConvertHelper.php`
- `src/Event/EarnVirtualCurrency.php`
- `src/Event/JoinGroup.php`
- `src/Event/LevelUp.php`
- `src/Event/Search.php`
- `src/Facade/Type/AnalyticsType.php`
- `src/Facade/Type/FirebaseType.php`
- `src/Facade/Type/DefaultEventParamsType.php`
- `src/Facade/Type/UserPropertyType.php`
- `test/Unit/EventTest.php`
- `test/Unit/AnalyticsTest.php`
- `test/Unit/FirebaseTest.php`
- `test/Unit/HelperTest.php`
- `test/Unit/UserDataTest.php`
- `test/Unit/UserPropertyTest.php`
- `todo.md`

## Validation status
- Runtime test/lint execution was not completed because `php` is unavailable in this environment (`php: command not found`).
60 changes: 59 additions & 1 deletion src/Analytics.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,12 @@ class Analytics extends Helper\IOHelper implements Facade\Type\AnalyticsType

protected null|bool $non_personalized_ads = false;
protected null|int $timestamp_micros;
protected null|string $validation_behavior;
protected null|string $ip_override;
protected null|string $client_id;
protected null|string $user_id;
protected array $user_location = [];
protected array $device = [];
protected array $user_properties = [];
protected array $events = [];

Expand All @@ -40,8 +44,12 @@ public function getParams(): array
return [
'non_personalized_ads',
'timestamp_micros',
'validation_behavior',
'ip_override',
'client_id',
'user_id',
'user_location',
'device',
'user_properties',
'events',
];
Expand All @@ -68,6 +76,12 @@ public function setClientId(string $id)
return $this;
}

public function setNonPersonalizedAds(bool $enabled = true)
{
$this->non_personalized_ads = $enabled;
return $this;
}

public function setUserId(string $id)
{
$this->user_id = $id;
Expand All @@ -89,6 +103,30 @@ public function setTimestampMicros(int|float $microOrUnix)
return $this;
}

public function setValidationBehavior(null|string $behavior)
{
$this->validation_behavior = $behavior;
return $this;
}

public function setIpOverride(null|string $ip)
{
$this->ip_override = $ip;
return $this;
}

public function setUserLocation(array $location)
{
$this->user_location = static::cleanAssoc($location);
return $this;
}

public function setDevice(array $device)
{
$this->device = static::cleanAssoc($device);
return $this;
}

public function addUserProperty(Facade\Type\UserPropertyType ...$props)
{
foreach ($props as $prop) {
Expand Down Expand Up @@ -130,9 +168,11 @@ 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]);

$userData = $this->userdata->toArray();

$body = array_replace_recursive(
$this->toArray(),
["user_data" => !empty($this->user_id) ? $this->userdata->toArray() : []], // Only accepted if user_id is passed too
["user_data" => $userData],
["user_properties" => $this->user_properties],
["consent" => $this->consent->toArray()],
);
Expand Down Expand Up @@ -197,4 +237,22 @@ public static function new(string $measurement_id, string $api_secret, bool $deb
{
return new static($measurement_id, $api_secret, $debug);
}

private static function cleanAssoc(array $input): array
{
$out = [];
foreach ($input as $key => $value) {
if (!is_string($key)) {
continue;
}

if ($value === null || $value === '') {
continue;
}

$out[$key] = $value;
}

return $out;
}
}
5 changes: 4 additions & 1 deletion src/Event/EarnVirtualCurrency.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@ public function getParams(): array

public function getRequiredParams(): array
{
return [];
return [
'virtual_currency_name',
'value',
];
}

public function setVirtualCurrencyName(null|string $name)
Expand Down
4 changes: 3 additions & 1 deletion src/Event/JoinGroup.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ public function getParams(): array

public function getRequiredParams(): array
{
return [];
return [
'group_id',
];
}

public function setGroupId(null|string $id)
Expand Down
2 changes: 1 addition & 1 deletion src/Event/LevelUp.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public function getParams(): array

public function getRequiredParams(): array
{
return [];
return ['level'];
}

public function setLevel(null|int $lvl)
Expand Down
4 changes: 3 additions & 1 deletion src/Event/Search.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ public function getParams(): array

public function getRequiredParams(): array
{
return [];
return [
'search_term',
];
}

public function setSearchTerm(null|string $term)
Expand Down
40 changes: 40 additions & 0 deletions src/Facade/Type/AnalyticsType.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@ interface AnalyticsType extends IOType
*/
public function setClientId(string $id);

/**
* Mark payload as non-personalized ads.
*
* @var non_personalized_ads
* @param bool $enabled
*/
public function setNonPersonalizedAds(bool $enabled = true);

/**
* A unique identifier for a user. See User-ID for cross-platform analysis for more information on this identifier.
*
Expand All @@ -34,6 +42,38 @@ public function setUserId(string $id);
*/
public function setTimestampMicros(int|float $microOrUnix);

/**
* Validation behavior for debug endpoint.
*
* @var validation_behavior
* @param string|null $behavior
*/
public function setValidationBehavior(null|string $behavior);

/**
* Override source IP address.
*
* @var ip_override
* @param string|null $ip
*/
public function setIpOverride(null|string $ip);

/**
* Override user location object.
*
* @var user_location
* @param array $location
*/
public function setUserLocation(array $location);

/**
* Override device object.
*
* @var device
* @param array $device
*/
public function setDevice(array $device);

/**
* The user properties for the measurement (Up to 25 custom per project, see link)
*
Expand Down
56 changes: 56 additions & 0 deletions src/Facade/Type/DefaultEventParamsType.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,5 +64,61 @@ public function setSessionId(string $id);
*/
public function setEngagementTimeMSec(int $msec);

/**
* Optional event-level override for event timestamp.
*
* @var timestamp_micros
* @param int|float $microOrUnix microtime(true) or time()
*/
public function setEventTimestampMicros(int|float $microOrUnix);

/**
* Campaign source.
*
* @var campaign_source
* @param string $source eg. newsletter
*/
public function setCampaignSource(string $source);

/**
* Campaign medium.
*
* @var campaign_medium
* @param string $medium eg. email
*/
public function setCampaignMedium(string $medium);

/**
* Campaign name.
*
* @var campaign_name
* @param string $name eg. spring_sale
*/
public function setCampaignName(string $name);

/**
* Campaign term.
*
* @var campaign_term
* @param string $term
*/
public function setCampaignTerm(string $term);

/**
* Campaign content.
*
* @var campaign_content
* @param string $content
*/
public function setCampaignContent(string $content);

/**
* Campaign identifier.
*
* @var campaign_id
* @param string $id
*/
public function setCampaignId(string $id);

public function toArray(): array;
}
Loading
Loading