From e71b8792530c32c2987cf8bfbaa53b5ddb000105 Mon Sep 17 00:00:00 2001 From: tyiuhc Date: Mon, 13 Nov 2023 16:26:36 -0800 Subject: [PATCH 01/15] initial commit --- src/Analytics/BaseEvent.php | 54 ++++++++++ src/Analytics/Client.php | 8 ++ src/Assignment/Assignment.php | 28 +++++ src/Assignment/AssignmentConfig.php | 8 ++ src/Assignment/AssignmentFilter.php | 28 +++++ src/Assignment/AssignmentService.php | 68 ++++++++++++ src/Assignment/LRUCache.php | 120 +++++++++++++++++++++ src/Util.php | 14 +++ tests/Assignment/AssignmentServiceTest.php | 60 +++++++++++ 9 files changed, 388 insertions(+) create mode 100644 src/Analytics/BaseEvent.php create mode 100644 src/Analytics/Client.php create mode 100644 src/Assignment/Assignment.php create mode 100644 src/Assignment/AssignmentConfig.php create mode 100644 src/Assignment/AssignmentFilter.php create mode 100644 src/Assignment/AssignmentService.php create mode 100644 src/Assignment/LRUCache.php create mode 100644 tests/Assignment/AssignmentServiceTest.php diff --git a/src/Analytics/BaseEvent.php b/src/Analytics/BaseEvent.php new file mode 100644 index 0000000..839c266 --- /dev/null +++ b/src/Analytics/BaseEvent.php @@ -0,0 +1,54 @@ +eventType = $eventType; + } +} diff --git a/src/Analytics/Client.php b/src/Analytics/Client.php new file mode 100644 index 0000000..6434179 --- /dev/null +++ b/src/Analytics/Client.php @@ -0,0 +1,8 @@ +user = $user; + $this->results = $results; + $this->timestamp = time(); + } + + public function canonicalize(): string { + $canonical = trim("{$this->user->userId} {$this->user->deviceId}"); + + foreach (array_keys($this->results) as $key) { + $value = $this->results[$key]; + $canonical .= " " . trim($key) . " " . trim($value['value']); + } + + return $canonical; + } +} diff --git a/src/Assignment/AssignmentConfig.php b/src/Assignment/AssignmentConfig.php new file mode 100644 index 0000000..5827755 --- /dev/null +++ b/src/Assignment/AssignmentConfig.php @@ -0,0 +1,8 @@ +cache = new LRUCache($size, $ttlMillis); + } + + public function shouldTrack(Assignment $assignment): bool { + if (count($assignment->results) === 0) { + return false; + } + + $canonicalAssignment = $assignment->canonicalize(); + $track = $this->cache->get($canonicalAssignment) === null; + + if ($track) { + $this->cache->put($canonicalAssignment, 0); + } + + return $track; + } +} diff --git a/src/Assignment/AssignmentService.php b/src/Assignment/AssignmentService.php new file mode 100644 index 0000000..f1285f7 --- /dev/null +++ b/src/Assignment/AssignmentService.php @@ -0,0 +1,68 @@ +amplitude = $amplitude; + $this->assignmentFilter = $assignmentFilter; + } + + public function track(Assignment $assignment): void + { + if ($this->assignmentFilter->shouldTrack($assignment)) { + $this->amplitude->logEvent($this->toEvent($assignment)); + } + } + + public static function toEvent(Assignment $assignment): BaseEvent + { + $event = new BaseEvent('[Experiment] Assignment'); + $event->userId = $assignment->user->userId; + $event->deviceId = $assignment->user->deviceId; + $event->eventProperties = []; + $event->userProperties = []; + + foreach ($assignment->results as $resultsKey => $result) { + $event->eventProperties["{$resultsKey}.variant"] = $result['value']; + } + + $set = []; + $unset = []; + foreach ($assignment->results as $resultsKey => $result) { + $flagType = $result['metadata']['flagType'] ?? null; + $default = $result['metadata']['default'] ?? false; + if ($flagType == FLAG_TYPE_MUTUAL_EXCLUSION_GROUP) { + continue; + } elseif ($default) { + $unset["[Experiment] {$resultsKey}"] = '-'; + } else { + $set["[Experiment] {$resultsKey}"] = $result['value']; + } + } + + $event->userProperties['$set'] = $set; + $event->userProperties['$unset'] = $unset; + + $hash = hashCode($assignment->canonicalize()); + + $event->insertId = "{$event->userId} {$event->deviceId} {$hash} " . + floor($assignment->timestamp / DAY_SECS); + + return $event; + } +} diff --git a/src/Assignment/LRUCache.php b/src/Assignment/LRUCache.php new file mode 100644 index 0000000..68f995b --- /dev/null +++ b/src/Assignment/LRUCache.php @@ -0,0 +1,120 @@ +prev = null; + $this->next = null; + $this->data = $data; + } +} + +class CacheItem { + public $key; + public $value; + public $createdAt; + + public function __construct($key, $value) { + $this->key = $key; + $this->value = $value; + $this->createdAt = time(); + } +} + +class LRUCache { + private $capacity; + private $ttlMillis; + private $cache; + private $head; + private $tail; + + public function __construct($capacity, $ttlMillis) { + $this->capacity = $capacity; + $this->ttlMillis = $ttlMillis; + $this->cache = []; + $this->head = null; + $this->tail = null; + } + + public function put($key, $value): void { + if (isset($this->cache[$key])) { + $this->removeFromList($key); + } elseif (count($this->cache) >= $this->capacity) { + $this->evictLRU(); + } + + $cacheItem = new CacheItem($key, $value); + $node = new ListNode($cacheItem); + $this->cache[$key] = $node; + $this->insertToList($node); + } + + public function get($key) { + if (isset($this->cache[$key])) { + $node = $this->cache[$key]; + $timeElapsed = time() - $node->data->createdAt; + + if ($timeElapsed > $this->ttlMillis) { + $this->remove($key); + return null; + } + + $this->removeFromList($key); + $this->insertToList($node); + return $node->data->value; + } + + return null; + } + + public function remove($key): void { + $this->removeFromList($key); + unset($this->cache[$key]); + } + + public function clear(): void { + $this->cache = []; + $this->head = null; + $this->tail = null; + } + + private function evictLRU(): void { + if ($this->head) { + $this->remove($this->head->data->key); + } + } + + private function removeFromList($key): void { + $node = $this->cache[$key]; + + if ($node->prev) { + $node->prev->next = $node->next; + } else { + $this->head = $node->next; + } + + if ($node->next) { + $node->next->prev = $node->prev; + } else { + $this->tail = $node->prev; + } + } + + private function insertToList($node): void { + if ($this->tail) { + $this->tail->next = $node; + $node->prev = $this->tail; + $node->next = null; + $this->tail = $node; + } else { + $this->head = $node; + $this->tail = $node; + } + } +} + diff --git a/src/Util.php b/src/Util.php index 28ac9d4..1553e54 100644 --- a/src/Util.php +++ b/src/Util.php @@ -15,3 +15,17 @@ function initializeLogger(?bool $debug): Logger $logger->pushHandler($handler); return $logger; } + +function hashCode(string $s): int +{ + $hash = 0; + if (strlen($s) === 0) { + return $hash; + } + for ($i = 0; $i < strlen($s); $i++) { + $chr = ord($s[$i]); + $hash = ($hash << 5) - $hash + $chr; + $hash |= 0; + } + return $hash; +} diff --git a/tests/Assignment/AssignmentServiceTest.php b/tests/Assignment/AssignmentServiceTest.php new file mode 100644 index 0000000..67a18fa --- /dev/null +++ b/tests/Assignment/AssignmentServiceTest.php @@ -0,0 +1,60 @@ +userId('user')->deviceId('device')->build(); + $results = [ + 'flag-key-1' => [ + 'value' => 'on', + ], + 'flag-key-2' => [ + 'value' => 'control', + 'metadata' => [ + 'default' => true, + ], + ], + ]; + + // Create Assignment object + $assignment = new Assignment($user, $results); + + // Convert Assignment to Event + $event = AssignmentService::toEvent($assignment); + + // Assertions + $this->assertEquals($user->userId, $event->userId); + $this->assertEquals($user->deviceId, $event->deviceId); + $this->assertEquals('[Experiment] Assignment', $event->eventType); + + $eventProperties = $event->eventProperties; + $this->assertCount(2, $eventProperties); + $this->assertEquals('on', $eventProperties['flag-key-1.variant']); + $this->assertEquals('control', $eventProperties['flag-key-2.variant']); + + $userProperties = $event->userProperties; + $this->assertCount(2, $userProperties); + $this->assertCount(1, $userProperties['$set']); + $this->assertCount(1, $userProperties['$unset']); + + $canonicalization = 'user device flag-key-1 on flag-key-2 control'; + $expected = 'user device ' . hashCode($canonicalization) . ' ' . floor($assignment->timestamp / DAY_SECS); + $this->assertEquals($expected, $event->insertId); + } +} + + From d66ae2c5334fa8e0824c71986c7a48f7df79b02a Mon Sep 17 00:00:00 2001 From: tyiuhc Date: Wed, 15 Nov 2023 15:26:34 -0800 Subject: [PATCH 02/15] add analytics package --- src/Amplitude/Amplitude.php | 74 ++++++++ src/Amplitude/Event.php | 29 +++ src/Analytics/BaseEvent.php | 54 ------ src/Analytics/Client.php | 8 - src/Assignment/Assignment.php | 20 +- src/Assignment/AssignmentConfig.php | 7 + src/Assignment/AssignmentFilter.php | 6 +- src/Assignment/AssignmentService.php | 21 ++- src/Assignment/LRUCache.php | 4 +- src/Local/LocalEvaluationClient.php | 19 ++ src/Local/LocalEvaluationConfig.php | 9 +- src/Local/LocalEvaluationConfigBuilder.php | 12 +- tests/Amplitude/AmplitudeTest.php | 32 ++++ tests/Assignment/AssignmentFilterTest.php | 206 +++++++++++++++++++++ tests/Assignment/AssignmentServiceTest.php | 4 +- tests/Local/LocalEvaluationClientTest.php | 11 ++ 16 files changed, 427 insertions(+), 89 deletions(-) create mode 100644 src/Amplitude/Amplitude.php create mode 100644 src/Amplitude/Event.php delete mode 100644 src/Analytics/BaseEvent.php delete mode 100644 src/Analytics/Client.php create mode 100644 tests/Amplitude/AmplitudeTest.php create mode 100644 tests/Assignment/AssignmentFilterTest.php diff --git a/src/Amplitude/Amplitude.php b/src/Amplitude/Amplitude.php new file mode 100644 index 0000000..38b6552 --- /dev/null +++ b/src/Amplitude/Amplitude.php @@ -0,0 +1,74 @@ +apiKey = $apiKey; + $this->httpClient = new Client(); + $this->logger = initializeLogger($debug); + } + + /** + * @throws GuzzleException + */ + public function flush() + { + $payload = ["api_key" => $this->apiKey, "events" => $this->queue]; + $this->post('https://api2.amplitude.com/batch', $payload); + echo print_r($payload, true); + } + + public function logEvent(Event $event) + { + $this->queue[] = $event->toArray(); + } + + /** + * @throws GuzzleException + */ + public function __destruct() + { + $this->flush(); + } + + /** + * @throws GuzzleException + */ + private function post(string $url, array $payload) + { + // Using sendAsync to make an asynchronous request + $promise = $this->httpClient->postAsync($url, [ + 'json' => $payload, + ]); + + return $promise->then( + function ($response) { + // Process the successful response if needed + $statusCode = $response->getStatusCode(); + $responseData = json_decode($response->getBody(), true); + // ... process the response data + + return $responseData; + }, + function (\Exception $exception) { + // Handle the exception for async request + throw new \Exception("Async request error: " . $exception->getMessage()); + } + ); + } +} diff --git a/src/Amplitude/Event.php b/src/Amplitude/Event.php new file mode 100644 index 0000000..d2615a6 --- /dev/null +++ b/src/Amplitude/Event.php @@ -0,0 +1,29 @@ +eventType = $eventType; + } + + public function toArray(): array + { + return array_filter([ + 'event_type' => $this->eventType, + 'event_properties' => $this->eventProperties, + 'user_properties' => $this->userProperties, + 'user_id' => $this->userId, + 'device_id' => $this->deviceId, + 'insert_id' => $this->insertId,]); + } +} diff --git a/src/Analytics/BaseEvent.php b/src/Analytics/BaseEvent.php deleted file mode 100644 index 839c266..0000000 --- a/src/Analytics/BaseEvent.php +++ /dev/null @@ -1,54 +0,0 @@ -eventType = $eventType; - } -} diff --git a/src/Analytics/Client.php b/src/Analytics/Client.php deleted file mode 100644 index 6434179..0000000 --- a/src/Analytics/Client.php +++ /dev/null @@ -1,8 +0,0 @@ -user = $user; $this->results = $results; - $this->timestamp = time(); + $this->timestamp = floor(microtime(true) * 1000); } - public function canonicalize(): string { + public function canonicalize(): string + { $canonical = trim("{$this->user->userId} {$this->user->deviceId}"); - - foreach (array_keys($this->results) as $key) { + $sortedKeys = array_keys($this->results); + sort($sortedKeys); + foreach ($sortedKeys as $key) { $value = $this->results[$key]; - $canonical .= " " . trim($key) . " " . trim($value['value']); + $canonical .= " " . trim($key) . " " . trim($value['key']); } - + echo $canonical . "\n"; return $canonical; } } diff --git a/src/Assignment/AssignmentConfig.php b/src/Assignment/AssignmentConfig.php index 5827755..9b60c4c 100644 --- a/src/Assignment/AssignmentConfig.php +++ b/src/Assignment/AssignmentConfig.php @@ -4,5 +4,12 @@ class AssignmentConfig { + public string $apiKey; + public int $cacheCapacity; + public function __construct(string $apiKey, int $cacheCapacity = 65536) + { + $this->apiKey = $apiKey; + $this->cacheCapacity = $cacheCapacity; + } } diff --git a/src/Assignment/AssignmentFilter.php b/src/Assignment/AssignmentFilter.php index 5ebc7e2..433e10f 100644 --- a/src/Assignment/AssignmentFilter.php +++ b/src/Assignment/AssignmentFilter.php @@ -1,17 +1,19 @@ cache = new LRUCache($size, $ttlMillis); } - public function shouldTrack(Assignment $assignment): bool { + public function shouldTrack(Assignment $assignment): bool + { if (count($assignment->results) === 0) { return false; } diff --git a/src/Assignment/AssignmentService.php b/src/Assignment/AssignmentService.php index f1285f7..c01ee42 100644 --- a/src/Assignment/AssignmentService.php +++ b/src/Assignment/AssignmentService.php @@ -2,21 +2,22 @@ namespace AmplitudeExperiment\Assignment; -use AmplitudeExperiment\Analytics\Client; -use AmplitudeExperiment\Analytics\BaseEvent; +use AmplitudeExperiment\Amplitude\Amplitude; +use AmplitudeExperiment\Amplitude\Event; use function AmplitudeExperiment\hashCode; require_once __DIR__ . '/../Util.php'; -const FLAG_TYPE_MUTUAL_EXCLUSION_GROUP = "mutual-exclusion-group"; -const DAY_SECS = 24 * 60 * 60; +const FLAG_TYPE_MUTUAL_EXCLUSION_GROUP = 'mutual-exclusion-group'; +const FLAG_TYPE_HOLDOUT_GROUP = 'holdout-group';; +const DAY_MILLIS = 24 * 60 * 60 * 1000; class AssignmentService { - private Client $amplitude; + private Amplitude $amplitude; private AssignmentFilter $assignmentFilter; - public function __construct(Client $amplitude, AssignmentFilter $assignmentFilter) + public function __construct(Amplitude $amplitude, AssignmentFilter $assignmentFilter) { $this->amplitude = $amplitude; $this->assignmentFilter = $assignmentFilter; @@ -29,16 +30,16 @@ public function track(Assignment $assignment): void } } - public static function toEvent(Assignment $assignment): BaseEvent + public static function toEvent(Assignment $assignment): Event { - $event = new BaseEvent('[Experiment] Assignment'); + $event = new Event('[Experiment] Assignment'); $event->userId = $assignment->user->userId; $event->deviceId = $assignment->user->deviceId; $event->eventProperties = []; $event->userProperties = []; foreach ($assignment->results as $resultsKey => $result) { - $event->eventProperties["{$resultsKey}.variant"] = $result['value']; + $event->eventProperties["{$resultsKey}.variant"] = $result['key']; } $set = []; @@ -61,7 +62,7 @@ public static function toEvent(Assignment $assignment): BaseEvent $hash = hashCode($assignment->canonicalize()); $event->insertId = "{$event->userId} {$event->deviceId} {$hash} " . - floor($assignment->timestamp / DAY_SECS); + floor($assignment->timestamp / DAY_MILLIS); return $event; } diff --git a/src/Assignment/LRUCache.php b/src/Assignment/LRUCache.php index 68f995b..87e7e3a 100644 --- a/src/Assignment/LRUCache.php +++ b/src/Assignment/LRUCache.php @@ -22,7 +22,7 @@ class CacheItem { public function __construct($key, $value) { $this->key = $key; $this->value = $value; - $this->createdAt = time(); + $this->createdAt = floor(microtime(true) * 1000); } } @@ -57,7 +57,7 @@ public function put($key, $value): void { public function get($key) { if (isset($this->cache[$key])) { $node = $this->cache[$key]; - $timeElapsed = time() - $node->data->createdAt; + $timeElapsed = floor(microtime(true) * 1000) - $node->data->createdAt; if ($timeElapsed > $this->ttlMillis) { $this->remove($key); diff --git a/src/Local/LocalEvaluationClient.php b/src/Local/LocalEvaluationClient.php index f27f8a0..cc8093b 100644 --- a/src/Local/LocalEvaluationClient.php +++ b/src/Local/LocalEvaluationClient.php @@ -2,6 +2,10 @@ namespace AmplitudeExperiment\Local; +use AmplitudeExperiment\Amplitude\Amplitude; +use AmplitudeExperiment\Assignment\Assignment; +use AmplitudeExperiment\Assignment\AssignmentFilter; +use AmplitudeExperiment\Assignment\AssignmentService; use AmplitudeExperiment\EvaluationCore\EvaluationEngine; use AmplitudeExperiment\Flag\FlagConfigFetcher; use AmplitudeExperiment\Flag\FlagConfigService; @@ -12,9 +16,12 @@ use Monolog\Logger; use function AmplitudeExperiment\EvaluationCore\topologicalSort; use function AmplitudeExperiment\initializeLogger; +use const AmplitudeExperiment\Assignment\FLAG_TYPE_HOLDOUT_GROUP; +use const AmplitudeExperiment\Assignment\FLAG_TYPE_MUTUAL_EXCLUSION_GROUP; require_once __DIR__ . '/../EvaluationCore/Util.php'; require_once __DIR__ . '/../Util.php'; +require_once __DIR__ . '/../Assignment/AssignmentService.php'; /** * Experiment client for evaluating variants for a user locally. @@ -27,6 +34,7 @@ class LocalEvaluationClient private FlagConfigService $flagConfigService; private EvaluationEngine $evaluation; private Logger $logger; + private ?AssignmentService $assignmentService = null; public function __construct(string $apiKey, ?LocalEvaluationConfig $config = null) { @@ -36,6 +44,9 @@ public function __construct(string $apiKey, ?LocalEvaluationConfig $config = nul $this->flagConfigService = new FlagConfigService($fetcher, $this->config->debug, $this->config->bootstrap); $this->logger = initializeLogger($this->config->debug ? Logger::DEBUG : Logger::INFO); $this->evaluation = new EvaluationEngine(); + if ($config->assignmentConfig) { + $this->assignmentService = new AssignmentService(new Amplitude($config->assignmentConfig->apiKey), new AssignmentFilter($config->assignmentConfig->cacheCapacity)); + } } /** @@ -73,6 +84,7 @@ public function evaluate(User $user, array $flagKeys = []): array $this->logger->debug('[Experiment] Evaluate - user: ' . json_encode($user->toArray()) . ' with flags: ' . json_encode($flags)); $results = $this->evaluation->evaluate($user->toEvaluationContext(), $flags); $variants = []; + $assignmentResults = []; $filter = !empty($flagKeys); foreach ($results as $flagKey => $flagResult) { @@ -80,9 +92,16 @@ public function evaluate(User $user, array $flagKeys = []): array if ($included) { $variants[$flagKey] = Variant::convertEvaluationVariantToVariant($flagResult); } + if ($included || $flagResult['metadata']['flagType'] == FLAG_TYPE_HOLDOUT_GROUP || $flagResult['metadata']['flagType'] == FLAG_TYPE_MUTUAL_EXCLUSION_GROUP) { + $assignmentResults[$flagKey] = $flagResult; + } } $this->logger->debug('[Experiment] Evaluate - variants:', $variants); + if ($this->assignmentService) { + echo "Tracking assignment\n"; + $this->assignmentService->track(new Assignment($user, $assignmentResults)); + } return $variants; } } diff --git a/src/Local/LocalEvaluationConfig.php b/src/Local/LocalEvaluationConfig.php index 08df08d..367cbfe 100644 --- a/src/Local/LocalEvaluationConfig.php +++ b/src/Local/LocalEvaluationConfig.php @@ -2,6 +2,8 @@ namespace AmplitudeExperiment\Local; +use AmplitudeExperiment\Assignment\AssignmentConfig; + class LocalEvaluationConfig { /** @@ -18,18 +20,21 @@ class LocalEvaluationConfig * Useful if you are managing the flag configurations separately. */ public array $bootstrap; + public ?AssignmentConfig $assignmentConfig; const DEFAULTS = [ 'debug' => false, 'serverUrl' => 'https://api.lab.amplitude.com', - 'bootstrap' => [] + 'bootstrap' => [], + 'assignmentConfig' => null ]; - public function __construct(bool $debug, string $serverUrl, array $bootstrap) + public function __construct(bool $debug, string $serverUrl, array $bootstrap, ?AssignmentConfig $assignmentConfig) { $this->debug = $debug; $this->serverUrl = $serverUrl; $this->bootstrap = $bootstrap; + $this->assignmentConfig = $assignmentConfig; } public static function builder(): LocalEvaluationConfigBuilder diff --git a/src/Local/LocalEvaluationConfigBuilder.php b/src/Local/LocalEvaluationConfigBuilder.php index a474544..496eb46 100644 --- a/src/Local/LocalEvaluationConfigBuilder.php +++ b/src/Local/LocalEvaluationConfigBuilder.php @@ -2,11 +2,14 @@ namespace AmplitudeExperiment\Local; +use AmplitudeExperiment\Assignment\AssignmentConfig; + class LocalEvaluationConfigBuilder { protected bool $debug = LocalEvaluationConfig::DEFAULTS['debug']; protected string $serverUrl = LocalEvaluationConfig::DEFAULTS['serverUrl']; protected array $bootstrap = LocalEvaluationConfig::DEFAULTS['bootstrap']; + protected ?AssignmentConfig $assignmentConfig = LocalEvaluationConfig::DEFAULTS['assignmentConfig']; public function __construct() { @@ -30,12 +33,19 @@ public function bootstrap(array $bootstrap): LocalEvaluationConfigBuilder return $this; } + public function assignmentConfig(AssignmentConfig $assignmentConfig): LocalEvaluationConfigBuilder + { + $this->assignmentConfig = $assignmentConfig; + return $this; + } + public function build(): LocalEvaluationConfig { return new LocalEvaluationConfig( $this->debug, $this->serverUrl, - $this->bootstrap + $this->bootstrap, + $this->assignmentConfig ); } } diff --git a/tests/Amplitude/AmplitudeTest.php b/tests/Amplitude/AmplitudeTest.php new file mode 100644 index 0000000..4dae25c --- /dev/null +++ b/tests/Amplitude/AmplitudeTest.php @@ -0,0 +1,32 @@ +userId = 'tim.yiu@amplitude.com'; + $event2->userId = 'tim.yiu@amplitude.com'; + $event3->userId = 'tim.yiu@amplitude.com'; + $client->logEvent($event1); + $client->logEvent($event2); + $client->logEvent($event3); + $client->flush(); + $this->assertTrue(true); + } +} diff --git a/tests/Assignment/AssignmentFilterTest.php b/tests/Assignment/AssignmentFilterTest.php new file mode 100644 index 0000000..bc92321 --- /dev/null +++ b/tests/Assignment/AssignmentFilterTest.php @@ -0,0 +1,206 @@ +userId('user')->build(); + $results = [ + 'flag-key-1' => [ + 'value' => 'on', + ], + 'flag-key-2' => [ + 'value' => 'control', + 'metadata' => [ + 'default' => true, + ], + ], + ]; + + $filter = new AssignmentFilter(100); + $assignment = new Assignment($user, $results); + $this->assertTrue($filter->shouldTrack($assignment)); + } + + public function testDuplicateAssignment() + { + $user = User::builder()->userId('user')->build(); + $results = [ + 'flag-key-1' => [ + 'value' => 'on', + ], + 'flag-key-2' => [ + 'value' => 'control', + 'metadata' => [ + 'default' => true, + ], + ], + ]; + + $filter = new AssignmentFilter(100); + $assignment1 = new Assignment($user, $results); + $assignment2 = new Assignment($user, $results); + $filter->shouldTrack($assignment1); + $this->assertFalse($filter->shouldTrack($assignment2)); + } + + public function testSameUserDifferentResults() + { + $user = User::builder()->userId('user')->build(); + $results1 = [ + 'flag-key-1' => [ + 'value' => 'on', + ], + 'flag-key-2' => [ + 'value' => 'control', + 'metadata' => [ + 'default' => true, + ], + ], + ]; + + $results2 = [ + 'flag-key-1' => [ + 'value' => 'control', + ], + 'flag-key-2' => [ + 'value' => 'on', + ], + ]; + + $filter = new AssignmentFilter(100); + $assignment1 = new Assignment($user, $results1); + $assignment2 = new Assignment($user, $results2); + $this->assertTrue($filter->shouldTrack($assignment1)); + $this->assertTrue($filter->shouldTrack($assignment2)); + } + + public function testSameResultDifferentUser() + { + $user1 = User::builder()->userId('user')->build(); + $user2 = User::builder()->userId('different-user')->build(); + $results = [ + 'flag-key-1' => [ + 'value' => 'on', + ], + 'flag-key-2' => [ + 'value' => 'control', + 'metadata' => [ + 'default' => true, + ], + ], + ]; + + $filter = new AssignmentFilter(100); + $assignment1 = new Assignment($user1, $results); + $assignment2 = new Assignment($user2, $results); + $this->assertTrue($filter->shouldTrack($assignment1)); + $this->assertTrue($filter->shouldTrack($assignment2)); + } + + public function testEmptyResult() + { + $user1 = User::builder()->userId('user')->build(); + $user2 = User::builder()->userId('different-user')->build(); + + $filter = new AssignmentFilter(100); + $assignment1 = new Assignment($user1, []); + $assignment2 = new Assignment($user1, []); + $assignment3 = new Assignment($user2, []); + $this->assertFalse($filter->shouldTrack($assignment1)); + $this->assertFalse($filter->shouldTrack($assignment2)); + $this->assertFalse($filter->shouldTrack($assignment3)); + } + + public function testDuplicateAssignmentsDifferentResultOrdering() + { + $user = User::builder()->userId('user')->build(); + $result1 = [ + 'value' => 'on', + ]; + + $result2 = [ + 'value' => 'control', + 'metadata' => [ + 'default' => true, + ] + ]; + + $results1 = [ + 'flag-key-1' => $result1, + 'flag-key-2' => $result2, + ]; + + $results2 = [ + 'flag-key-2' => $result2, + 'flag-key-1' => $result1, + ]; + + $filter = new AssignmentFilter(100); + $assignment1 = new Assignment($user, $results1); + $assignment2 = new Assignment($user, $results2); + $this->assertTrue($filter->shouldTrack($assignment1)); + $this->assertFalse($filter->shouldTrack($assignment2)); + } + + public function testLruReplacement() + { + $user1 = User::builder()->userId('user1')->build(); + $user2 = User::builder()->userId('user2')->build(); + $user3 = User::builder()->userId('user3')->build(); + $results = [ + 'flag-key-1' => [ + 'value' => 'on', + ], + 'flag-key-2' => [ + 'value' => 'control', + 'metadata' => [ + 'default' => true, + ] + ], + ]; + + $filter = new AssignmentFilter(2); + $assignment1 = new Assignment($user1, $results); + $assignment2 = new Assignment($user2, $results); + $assignment3 = new Assignment($user3, $results); + $this->assertTrue($filter->shouldTrack($assignment1)); + $this->assertTrue($filter->shouldTrack($assignment2)); + $this->assertTrue($filter->shouldTrack($assignment3)); + $this->assertTrue($filter->shouldTrack($assignment1)); + } + + public function testTtlBasedEviction() + { + $user1 = User::builder()->userId('user')->build(); + $user2 = User::builder()->userId('different-user')->build(); + $results = [ + 'flag-key-1' => [ + 'value' => 'on', + ], + 'flag-key-2' => [ + 'value' => 'control', + 'metadata' => [ + 'default' => true, + ] + ], + ]; + + $filter = new AssignmentFilter(100, 1000); + $assignment1 = new Assignment($user1, $results); + $assignment2 = new Assignment($user2, $results); + $this->assertTrue($filter->shouldTrack($assignment1)); + \sleep(1.05); + $this->assertTrue($filter->shouldTrack($assignment2)); + } +} diff --git a/tests/Assignment/AssignmentServiceTest.php b/tests/Assignment/AssignmentServiceTest.php index 67a18fa..f38bc4c 100644 --- a/tests/Assignment/AssignmentServiceTest.php +++ b/tests/Assignment/AssignmentServiceTest.php @@ -7,7 +7,7 @@ use AmplitudeExperiment\Assignment\AssignmentService; use AmplitudeExperiment\User; use PHPUnit\Framework\TestCase; -use const AmplitudeExperiment\Assignment\DAY_SECS; +use const AmplitudeExperiment\Assignment\DAY_MILLIS; use function AmplitudeExperiment\hashCode; require_once __DIR__ . '/../../src/Util.php'; @@ -52,7 +52,7 @@ public function testAssignmentToEventAsExpected() $this->assertCount(1, $userProperties['$unset']); $canonicalization = 'user device flag-key-1 on flag-key-2 control'; - $expected = 'user device ' . hashCode($canonicalization) . ' ' . floor($assignment->timestamp / DAY_SECS); + $expected = 'user device ' . hashCode($canonicalization) . ' ' . floor($assignment->timestamp / DAY_MILLIS); $this->assertEquals($expected, $event->insertId); } } diff --git a/tests/Local/LocalEvaluationClientTest.php b/tests/Local/LocalEvaluationClientTest.php index 88f16f6..e83cdab 100644 --- a/tests/Local/LocalEvaluationClientTest.php +++ b/tests/Local/LocalEvaluationClientTest.php @@ -2,6 +2,7 @@ namespace AmplitudeExperiment\Test\Local; +use AmplitudeExperiment\Assignment\AssignmentConfig; use AmplitudeExperiment\Experiment; use AmplitudeExperiment\Local\LocalEvaluationClient; use AmplitudeExperiment\Local\LocalEvaluationConfig; @@ -69,4 +70,14 @@ public function testEvaluateWithDependenciesVariantHeldOut() self::assertEquals(null, $variant->payload); self::assertTrue($variant->metadata["default"]); } + + public function testAssignment() + { + $aConfig = new AssignmentConfig('a6dd847b9d2f03c816d4f3f8458cdc1d'); + $config = LocalEvaluationConfig::builder()->debug(true)->assignmentConfig($aConfig)->build(); + $client = new LocalEvaluationClient($this->apiKey, $config); + $client->start()->wait(); + $user = User::builder()->userId('tim.yiu@amplitude.com')->build(); + $client->evaluate($user); + } } From f20efe1137a0251c710b512ebb25675ccbd01a22 Mon Sep 17 00:00:00 2001 From: tyiuhc Date: Wed, 15 Nov 2023 16:15:30 -0800 Subject: [PATCH 03/15] add simple backoff logic to Amplitude --- src/Amplitude/Amplitude.php | 53 +++++++++++++++++++++++++------ tests/Amplitude/AmplitudeTest.php | 10 ++---- 2 files changed, 46 insertions(+), 17 deletions(-) diff --git a/src/Amplitude/Amplitude.php b/src/Amplitude/Amplitude.php index 38b6552..5a6f85e 100644 --- a/src/Amplitude/Amplitude.php +++ b/src/Amplitude/Amplitude.php @@ -2,8 +2,10 @@ namespace AmplitudeExperiment\Amplitude; +use AmplitudeExperiment\Backoff; use GuzzleHttp\Client; use GuzzleHttp\Exception\GuzzleException; +use GuzzleHttp\Promise\PromiseInterface; use Monolog\Logger; use function AmplitudeExperiment\initializeLogger; @@ -26,11 +28,21 @@ public function __construct(string $apiKey, bool $debug = false) /** * @throws GuzzleException */ - public function flush() + public function flush(): PromiseInterface { $payload = ["api_key" => $this->apiKey, "events" => $this->queue]; - $this->post('https://api2.amplitude.com/batch', $payload); - echo print_r($payload, true); + + // Fetch initial flag configs and await the result. + return Backoff::doWithBackoff( + function () use ($payload) { + return $this->post('https://api2.amplitude.com/batch', $payload)->then( + function () { + $this->queue = []; + } + ); + }, + new Backoff(5, 1, 1, 1) + ); } public function logEvent(Event $event) @@ -43,13 +55,36 @@ public function logEvent(Event $event) */ public function __destruct() { - $this->flush(); + if (count($this->queue) > 0) { + $this->flush()->wait(); + } } +// private function handleResponse(int $code): bool +// { +// if ($code >= 200 && $code < 300) { +// $this->logger->debug("[Experiment] Event sent successfully"); +// return true; +// } elseif ($code == 429) { +// $this->logger->error("[Experiment] Event could not be sent - Exceeded daily quota"); +// } elseif ($code == 413) { +// $this->logger->error("[Experiment] Event could not be sent - Payload too large"); +// } elseif ($code == 408) { +// $this->logger->error("[Experiment] Event could not be sent - Timed out"); +// } elseif ($code >= 400 && $code < 500) { +// $this->logger->error("[Experiment] Event could not be sent - Invalid request"); +// } elseif ($code >= 500) { +// $this->logger->error("[Experiment] Event could not be sent - Http request failed"); +// } else { +// $this->logger->error("[Experiment] Event could not be sent - Http request status unknown"); +// } +// return false; +// } + /** * @throws GuzzleException */ - private function post(string $url, array $payload) + private function post(string $url, array $payload): PromiseInterface { // Using sendAsync to make an asynchronous request $promise = $this->httpClient->postAsync($url, [ @@ -59,15 +94,15 @@ private function post(string $url, array $payload) return $promise->then( function ($response) { // Process the successful response if needed - $statusCode = $response->getStatusCode(); + $this->logger->debug("[Amplitude] Event sent successfully"); $responseData = json_decode($response->getBody(), true); - // ... process the response data return $responseData; }, - function (\Exception $exception) { + function (\Exception $exception) use ($payload) { // Handle the exception for async request - throw new \Exception("Async request error: " . $exception->getMessage()); + $this->logger->error('[Amplitude] Failed to send event: ' . json_encode($payload) . ', ' . $exception->getMessage()); + throw $exception; } ); } diff --git a/tests/Amplitude/AmplitudeTest.php b/tests/Amplitude/AmplitudeTest.php index 4dae25c..f8dc2cb 100644 --- a/tests/Amplitude/AmplitudeTest.php +++ b/tests/Amplitude/AmplitudeTest.php @@ -16,17 +16,11 @@ class AmplitudeTest extends TestCase */ public function testAmplitude() { - $client = new Amplitude(self::API_KEY); + $client = new Amplitude(self::API_KEY, true); $event1 = new Event('test1'); - $event2 = new Event('test2'); - $event3 = new Event('test3'); $event1->userId = 'tim.yiu@amplitude.com'; - $event2->userId = 'tim.yiu@amplitude.com'; - $event3->userId = 'tim.yiu@amplitude.com'; $client->logEvent($event1); - $client->logEvent($event2); - $client->logEvent($event3); - $client->flush(); + $client->flush()->wait(); $this->assertTrue(true); } } From dc6aab48da4896f3678c22eda1f05fe8c5327e0a Mon Sep 17 00:00:00 2001 From: tyiuhc Date: Thu, 16 Nov 2023 14:44:12 -0800 Subject: [PATCH 04/15] add tests --- src/Amplitude/Amplitude.php | 45 ++++------------ src/Amplitude/AmplitudeConfig.php | 49 +++++++++++++++++ src/Amplitude/AmplitudeConfigBuilder.php | 64 +++++++++++++++++++++++ src/Assignment/Assignment.php | 1 - src/Assignment/AssignmentConfig.php | 6 ++- src/Flag/FlagConfigFetcher.php | 2 +- src/Local/LocalEvaluationClient.php | 21 +++++--- src/Remote/RemoteEvaluationClient.php | 2 +- src/Util.php | 3 +- tests/Amplitude/AmplitudeTest.php | 20 +++++++ tests/Local/LocalEvaluationClientTest.php | 4 +- 11 files changed, 169 insertions(+), 48 deletions(-) create mode 100644 src/Amplitude/AmplitudeConfig.php create mode 100644 src/Amplitude/AmplitudeConfigBuilder.php diff --git a/src/Amplitude/Amplitude.php b/src/Amplitude/Amplitude.php index 5a6f85e..a292035 100644 --- a/src/Amplitude/Amplitude.php +++ b/src/Amplitude/Amplitude.php @@ -17,17 +17,16 @@ class Amplitude private array $queue = []; private Client $httpClient; private Logger $logger; + private ?AmplitudeConfig $config; - public function __construct(string $apiKey, bool $debug = false) + public function __construct(string $apiKey, bool $debug, AmplitudeConfig $config = null) { $this->apiKey = $apiKey; $this->httpClient = new Client(); $this->logger = initializeLogger($debug); + $this->config = $config ?? AmplitudeConfig::builder()->build(); } - /** - * @throws GuzzleException - */ public function flush(): PromiseInterface { $payload = ["api_key" => $this->apiKey, "events" => $this->queue]; @@ -35,24 +34,24 @@ public function flush(): PromiseInterface // Fetch initial flag configs and await the result. return Backoff::doWithBackoff( function () use ($payload) { - return $this->post('https://api2.amplitude.com/batch', $payload)->then( + return $this->post($this->config->serverUrl, $payload)->then( function () { $this->queue = []; } ); }, - new Backoff(5, 1, 1, 1) + new Backoff($this->config->flushMaxRetries, 1, 1, 1) ); } public function logEvent(Event $event) { $this->queue[] = $event->toArray(); + if (count($this->queue) >= $this->config->flushQueueSize) { + $this->flush()->wait(); + } } - /** - * @throws GuzzleException - */ public function __destruct() { if (count($this->queue) > 0) { @@ -60,27 +59,6 @@ public function __destruct() } } -// private function handleResponse(int $code): bool -// { -// if ($code >= 200 && $code < 300) { -// $this->logger->debug("[Experiment] Event sent successfully"); -// return true; -// } elseif ($code == 429) { -// $this->logger->error("[Experiment] Event could not be sent - Exceeded daily quota"); -// } elseif ($code == 413) { -// $this->logger->error("[Experiment] Event could not be sent - Payload too large"); -// } elseif ($code == 408) { -// $this->logger->error("[Experiment] Event could not be sent - Timed out"); -// } elseif ($code >= 400 && $code < 500) { -// $this->logger->error("[Experiment] Event could not be sent - Invalid request"); -// } elseif ($code >= 500) { -// $this->logger->error("[Experiment] Event could not be sent - Http request failed"); -// } else { -// $this->logger->error("[Experiment] Event could not be sent - Http request status unknown"); -// } -// return false; -// } - /** * @throws GuzzleException */ @@ -92,12 +70,9 @@ private function post(string $url, array $payload): PromiseInterface ]); return $promise->then( - function ($response) { + function ($response) use ($payload) { // Process the successful response if needed - $this->logger->debug("[Amplitude] Event sent successfully"); - $responseData = json_decode($response->getBody(), true); - - return $responseData; + $this->logger->debug("[Amplitude] Event sent successfully: " . json_encode($payload)); }, function (\Exception $exception) use ($payload) { // Handle the exception for async request diff --git a/src/Amplitude/AmplitudeConfig.php b/src/Amplitude/AmplitudeConfig.php new file mode 100644 index 0000000..3755119 --- /dev/null +++ b/src/Amplitude/AmplitudeConfig.php @@ -0,0 +1,49 @@ + 'US', + 'serverUrl' => [ + 'EU' => [ + 'batch' => 'https://api.eu.amplitude.com/batch', + 'v2' => 'https://api.eu.amplitude.com/2/httpapi' + ], + 'US' => [ + 'batch' => 'https://api2.amplitude.com/batch', + 'v2' => 'https://api2.amplitude.com/2/httpapi' + ] + ], + 'useBatch' => false, + 'flushQueueSize' => 200, + 'flushMaxRetries' => 12, + ]; + + public function __construct( + int $flushQueueSize, + int $flushMaxRetries, + string $serverZone, + string $serverUrl, + bool $useBatch + ) + { + $this->flushQueueSize = $flushQueueSize; + $this->flushMaxRetries = $flushMaxRetries; + $this->serverZone = $serverZone; + $this->serverUrl = $serverUrl; + $this->useBatch = $useBatch; + } + + public static function builder(): AmplitudeConfigBuilder + { + return new AmplitudeConfigBuilder(); + } +} diff --git a/src/Amplitude/AmplitudeConfigBuilder.php b/src/Amplitude/AmplitudeConfigBuilder.php new file mode 100644 index 0000000..470c309 --- /dev/null +++ b/src/Amplitude/AmplitudeConfigBuilder.php @@ -0,0 +1,64 @@ +flushQueueSize = $flushQueueSize; + return $this; + } + + public function flushMaxRetries(int $flushMaxRetries): AmplitudeConfigBuilder + { + $this->flushMaxRetries = $flushMaxRetries; + return $this; + } + + public function serverZone(string $serverZone): AmplitudeConfigBuilder + { + $this->serverZone = $serverZone; + return $this; + } + + public function serverUrl(string $serverUrl): AmplitudeConfigBuilder + { + $this->serverUrl = $serverUrl; + return $this; + } + + public function useBatch(bool $useBatch): AmplitudeConfigBuilder + { + $this->useBatch = $useBatch; + return $this; + } + + public function build(): AmplitudeConfig + { + if (!$this->serverUrl) { + if ($this->useBatch) { + $this->serverUrl = AmplitudeConfig::DEFAULTS['serverUrl'][$this->serverZone]['batch']; + } else { + $this->serverUrl = AmplitudeConfig::DEFAULTS['serverUrl'][$this->serverZone]['v2']; + } + } + return new AmplitudeConfig( + $this->flushQueueSize, + $this->flushMaxRetries, + $this->serverZone, + $this->serverUrl, + $this->useBatch + ); + } +} diff --git a/src/Assignment/Assignment.php b/src/Assignment/Assignment.php index 7cd7b3a..01c3551 100644 --- a/src/Assignment/Assignment.php +++ b/src/Assignment/Assignment.php @@ -26,7 +26,6 @@ public function canonicalize(): string $value = $this->results[$key]; $canonical .= " " . trim($key) . " " . trim($value['key']); } - echo $canonical . "\n"; return $canonical; } } diff --git a/src/Assignment/AssignmentConfig.php b/src/Assignment/AssignmentConfig.php index 9b60c4c..39f6af1 100644 --- a/src/Assignment/AssignmentConfig.php +++ b/src/Assignment/AssignmentConfig.php @@ -2,14 +2,18 @@ namespace AmplitudeExperiment\Assignment; +use AmplitudeExperiment\Amplitude\AmplitudeConfig; + class AssignmentConfig { public string $apiKey; public int $cacheCapacity; + public ?AmplitudeConfig $amplitudeConfig; - public function __construct(string $apiKey, int $cacheCapacity = 65536) + public function __construct(string $apiKey, int $cacheCapacity = 65536, ?AmplitudeConfig $amplitudeConfig = null) { $this->apiKey = $apiKey; $this->cacheCapacity = $cacheCapacity; + $this->amplitudeConfig = $amplitudeConfig ?? AmplitudeConfig::builder()->build(); } } diff --git a/src/Flag/FlagConfigFetcher.php b/src/Flag/FlagConfigFetcher.php index dcde016..1d848ae 100644 --- a/src/Flag/FlagConfigFetcher.php +++ b/src/Flag/FlagConfigFetcher.php @@ -24,7 +24,7 @@ class FlagConfigFetcher private string $serverUrl; private Client $httpClient; - public function __construct(string $apiKey, string $serverUrl = LocalEvaluationConfig::DEFAULTS["serverUrl"], bool $debug = false) + public function __construct(string $apiKey, bool $debug, string $serverUrl = LocalEvaluationConfig::DEFAULTS["serverUrl"]) { $this->apiKey = $apiKey; $this->serverUrl = $serverUrl; diff --git a/src/Local/LocalEvaluationClient.php b/src/Local/LocalEvaluationClient.php index cc8093b..e4976ff 100644 --- a/src/Local/LocalEvaluationClient.php +++ b/src/Local/LocalEvaluationClient.php @@ -4,6 +4,7 @@ use AmplitudeExperiment\Amplitude\Amplitude; use AmplitudeExperiment\Assignment\Assignment; +use AmplitudeExperiment\Assignment\AssignmentConfig; use AmplitudeExperiment\Assignment\AssignmentFilter; use AmplitudeExperiment\Assignment\AssignmentService; use AmplitudeExperiment\EvaluationCore\EvaluationEngine; @@ -40,13 +41,11 @@ public function __construct(string $apiKey, ?LocalEvaluationConfig $config = nul { $this->apiKey = $apiKey; $this->config = $config ?? LocalEvaluationConfig::builder()->build(); - $fetcher = new FlagConfigFetcher($apiKey, $this->config->serverUrl, $this->config->debug); + $fetcher = new FlagConfigFetcher($apiKey, $this->config->debug, $this->config->serverUrl); $this->flagConfigService = new FlagConfigService($fetcher, $this->config->debug, $this->config->bootstrap); - $this->logger = initializeLogger($this->config->debug ? Logger::DEBUG : Logger::INFO); + $this->logger = initializeLogger($this->config->debug); + $this->initializeAssignmentService($config->assignmentConfig); $this->evaluation = new EvaluationEngine(); - if ($config->assignmentConfig) { - $this->assignmentService = new AssignmentService(new Amplitude($config->assignmentConfig->apiKey), new AssignmentFilter($config->assignmentConfig->cacheCapacity)); - } } /** @@ -99,9 +98,19 @@ public function evaluate(User $user, array $flagKeys = []): array $this->logger->debug('[Experiment] Evaluate - variants:', $variants); if ($this->assignmentService) { - echo "Tracking assignment\n"; $this->assignmentService->track(new Assignment($user, $assignmentResults)); } return $variants; } + + private function initializeAssignmentService(?AssignmentConfig $config): void + { + if ($config) { + $this->assignmentService = new AssignmentService( + new Amplitude($config->apiKey, + $this->config->debug, + $config->amplitudeConfig), + new AssignmentFilter($config->cacheCapacity)); + } + } } diff --git a/src/Remote/RemoteEvaluationClient.php b/src/Remote/RemoteEvaluationClient.php index f774620..a7106a2 100644 --- a/src/Remote/RemoteEvaluationClient.php +++ b/src/Remote/RemoteEvaluationClient.php @@ -39,7 +39,7 @@ public function __construct(string $apiKey, ?RemoteEvaluationConfig $config = nu $this->apiKey = $apiKey; $this->config = $config ?? RemoteEvaluationConfig::builder()->build(); $this->httpClient = new Client(); - $this->logger = initializeLogger($this->config->debug ? Logger::DEBUG : Logger::INFO); + $this->logger = initializeLogger($this->config->debug); } /** diff --git a/src/Util.php b/src/Util.php index 1553e54..46e3802 100644 --- a/src/Util.php +++ b/src/Util.php @@ -6,8 +6,9 @@ use Monolog\Handler\StreamHandler; use Monolog\Logger; -function initializeLogger(?bool $debug): Logger +function initializeLogger(bool $debug): Logger { + echo "Initializing logger " . $debug . " \n"; $logger = new Logger('AmplitudeExperiment'); $handler = new StreamHandler('php://stdout', $debug ? Logger::DEBUG : Logger::INFO); $formatter = new LineFormatter(null, null, false, true); diff --git a/tests/Amplitude/AmplitudeTest.php b/tests/Amplitude/AmplitudeTest.php index f8dc2cb..b99405a 100644 --- a/tests/Amplitude/AmplitudeTest.php +++ b/tests/Amplitude/AmplitudeTest.php @@ -3,6 +3,7 @@ namespace Amplitude; use AmplitudeExperiment\Amplitude\Amplitude; +use AmplitudeExperiment\Amplitude\AmplitudeConfig; use AmplitudeExperiment\Amplitude\Event; use GuzzleHttp\Exception\GuzzleException; use Monolog\Test\TestCase; @@ -11,6 +12,25 @@ class AmplitudeTest extends TestCase { const API_KEY = 'a6dd847b9d2f03c816d4f3f8458cdc1d'; + public function testAmplitudeConfigServerUrl() { + $config = AmplitudeConfig::builder() + ->build(); + $this->assertEquals('https://api2.amplitude.com/2/httpapi', $config->serverUrl); + $config = AmplitudeConfig::builder() + ->useBatch(true) + ->build(); + $this->assertEquals('https://api2.amplitude.com/batch', $config->serverUrl); + $config = AmplitudeConfig::builder() + ->serverZone('EU') + ->build(); + $this->assertEquals('https://api.eu.amplitude.com/2/httpapi', $config->serverUrl); + $config = AmplitudeConfig::builder() + ->serverZone('EU') + ->useBatch(true) + ->build(); + $this->assertEquals('https://api.eu.amplitude.com/batch', $config->serverUrl); + } + /** * @throws GuzzleException */ diff --git a/tests/Local/LocalEvaluationClientTest.php b/tests/Local/LocalEvaluationClientTest.php index e83cdab..6013ca7 100644 --- a/tests/Local/LocalEvaluationClientTest.php +++ b/tests/Local/LocalEvaluationClientTest.php @@ -23,7 +23,7 @@ public function __construct() ->deviceId('test_device') ->build(); $experiment = new Experiment(); - $config = LocalEvaluationConfig::builder()->debug(true)->build(); + $config = LocalEvaluationConfig::builder()->debug(false)->build(); $this->client = $experiment->initializeLocal($this->apiKey, $config); } @@ -74,7 +74,7 @@ public function testEvaluateWithDependenciesVariantHeldOut() public function testAssignment() { $aConfig = new AssignmentConfig('a6dd847b9d2f03c816d4f3f8458cdc1d'); - $config = LocalEvaluationConfig::builder()->debug(true)->assignmentConfig($aConfig)->build(); + $config = LocalEvaluationConfig::builder()->debug(false)->assignmentConfig($aConfig)->build(); $client = new LocalEvaluationClient($this->apiKey, $config); $client->start()->wait(); $user = User::builder()->userId('tim.yiu@amplitude.com')->build(); From aff09edd4bbfcb02fb6f2fd060726081f0bd9873 Mon Sep 17 00:00:00 2001 From: tyiuhc Date: Thu, 16 Nov 2023 16:28:51 -0800 Subject: [PATCH 05/15] add Amplitude backoff tests --- src/Amplitude/Amplitude.php | 9 +-- src/{ => Remote}/FetchOptions.php | 2 +- src/Remote/RemoteEvaluationClient.php | 1 - src/Util.php | 1 - tests/Amplitude/AmplitudeTest.php | 84 +++++++++++++++++++-- tests/Amplitude/MockAmplitude.php | 23 ++++++ tests/Local/LocalEvaluationClientTest.php | 2 +- tests/Remote/RemoteEvaluationClientTest.php | 2 +- 8 files changed, 106 insertions(+), 18 deletions(-) rename src/{ => Remote}/FetchOptions.php (92%) create mode 100644 tests/Amplitude/MockAmplitude.php diff --git a/src/Amplitude/Amplitude.php b/src/Amplitude/Amplitude.php index a292035..5e92a9c 100644 --- a/src/Amplitude/Amplitude.php +++ b/src/Amplitude/Amplitude.php @@ -14,8 +14,8 @@ class Amplitude { private string $apiKey; - private array $queue = []; - private Client $httpClient; + protected array $queue = []; + protected Client $httpClient; private Logger $logger; private ?AmplitudeConfig $config; @@ -46,6 +46,7 @@ function () { public function logEvent(Event $event) { + print_r($event->toArray()); $this->queue[] = $event->toArray(); if (count($this->queue) >= $this->config->flushQueueSize) { $this->flush()->wait(); @@ -59,9 +60,6 @@ public function __destruct() } } - /** - * @throws GuzzleException - */ private function post(string $url, array $payload): PromiseInterface { // Using sendAsync to make an asynchronous request @@ -72,6 +70,7 @@ private function post(string $url, array $payload): PromiseInterface return $promise->then( function ($response) use ($payload) { // Process the successful response if needed + echo $response->getBody(); $this->logger->debug("[Amplitude] Event sent successfully: " . json_encode($payload)); }, function (\Exception $exception) use ($payload) { diff --git a/src/FetchOptions.php b/src/Remote/FetchOptions.php similarity index 92% rename from src/FetchOptions.php rename to src/Remote/FetchOptions.php index f0043a0..970ca27 100644 --- a/src/FetchOptions.php +++ b/src/Remote/FetchOptions.php @@ -1,6 +1,6 @@ historyContainer = []; + } + + public function testAmplitudeConfigServerUrl() + { $config = AmplitudeConfig::builder() ->build(); $this->assertEquals('https://api2.amplitude.com/2/httpapi', $config->serverUrl); @@ -31,16 +43,72 @@ public function testAmplitudeConfigServerUrl() { $this->assertEquals('https://api.eu.amplitude.com/batch', $config->serverUrl); } - /** - * @throws GuzzleException - */ public function testAmplitude() { - $client = new Amplitude(self::API_KEY, true); + $client = new MockAmplitude(self::API_KEY, true); $event1 = new Event('test1'); $event1->userId = 'tim.yiu@amplitude.com'; $client->logEvent($event1); $client->flush()->wait(); $this->assertTrue(true); } + + public function testBackoffRetriesToFailure() + { + $config = AmplitudeConfig::builder() + ->flushMaxRetries(5) + ->build(); + $client = new MockAmplitude(self::API_KEY, true, $config); + $mock = new MockHandler([ + new RequestException('Error Communicating with Server', new Request('POST', 'test')), + new RequestException('Error Communicating with Server', new Request('POST', 'test')), + new RequestException('Error Communicating with Server', new Request('POST', 'test')), + new RequestException('Error Communicating with Server', new Request('POST', 'test')), + new RequestException('Error Communicating with Server', new Request('POST', 'test')), + ]); + + $handlerStack = HandlerStack::create($mock); + $httpClient = new Client(['handler' => $handlerStack, + 'on_stats' => function (TransferStats $stats) { + $this->historyContainer[] = $stats; + },]); + $client->setHttpClient($httpClient); + $event1 = new Event('test'); + $event1->userId = 'user_id'; + $client->logEvent($event1); + $client->flush()->wait(); + $this->assertEquals(5, $this->countPostRequests()); + } + + public function testBackoffRetriesThenSuccess() + { + $config = AmplitudeConfig::builder() + ->flushMaxRetries(5) + ->build(); + $client = new MockAmplitude(self::API_KEY, true, $config); + $mock = new MockHandler([ + new RequestException('Error Communicating with Server', new Request('POST', 'test')), + new RequestException('Error Communicating with Server', new Request('POST', 'test')), + new Response(200, ['X-Foo' => 'Bar']), + ]); + + $handlerStack = HandlerStack::create($mock); + $httpClient = new Client(['handler' => $handlerStack, + 'on_stats' => function (TransferStats $stats) { + $this->historyContainer[] = $stats; + },]); + $client->setHttpClient($httpClient); + $event1 = new Event('test'); + $event1->userId = 'user_id'; + $client->logEvent($event1); + $client->flush()->wait(); + $this->assertEquals(3, $this->countPostRequests()); + } + + private function countPostRequests(): int + { + return count(array_filter($this->historyContainer, function (TransferStats $stats) { + return $stats->getRequest()->getMethod() === 'POST'; + })); + } } diff --git a/tests/Amplitude/MockAmplitude.php b/tests/Amplitude/MockAmplitude.php new file mode 100644 index 0000000..04cd9cb --- /dev/null +++ b/tests/Amplitude/MockAmplitude.php @@ -0,0 +1,23 @@ +httpClient = $httpClient; + } + public function getQueueSize() : int { + return count($this->queue); + } +} diff --git a/tests/Local/LocalEvaluationClientTest.php b/tests/Local/LocalEvaluationClientTest.php index 6013ca7..c6f1eae 100644 --- a/tests/Local/LocalEvaluationClientTest.php +++ b/tests/Local/LocalEvaluationClientTest.php @@ -74,7 +74,7 @@ public function testEvaluateWithDependenciesVariantHeldOut() public function testAssignment() { $aConfig = new AssignmentConfig('a6dd847b9d2f03c816d4f3f8458cdc1d'); - $config = LocalEvaluationConfig::builder()->debug(false)->assignmentConfig($aConfig)->build(); + $config = LocalEvaluationConfig::builder()->debug(true)->assignmentConfig($aConfig)->build(); $client = new LocalEvaluationClient($this->apiKey, $config); $client->start()->wait(); $user = User::builder()->userId('tim.yiu@amplitude.com')->build(); diff --git a/tests/Remote/RemoteEvaluationClientTest.php b/tests/Remote/RemoteEvaluationClientTest.php index dd76b47..f90737b 100644 --- a/tests/Remote/RemoteEvaluationClientTest.php +++ b/tests/Remote/RemoteEvaluationClientTest.php @@ -3,7 +3,7 @@ namespace AmplitudeExperiment\Test\Remote; use AmplitudeExperiment\Experiment; -use AmplitudeExperiment\FetchOptions; +use AmplitudeExperiment\Remote\FetchOptions; use AmplitudeExperiment\Remote\RemoteEvaluationClient; use AmplitudeExperiment\Remote\RemoteEvaluationConfig; use AmplitudeExperiment\User; From 9490bd7835dcca5e1a53d92bcacc8be586c222cb Mon Sep 17 00:00:00 2001 From: tyiuhc Date: Fri, 17 Nov 2023 15:39:33 -0800 Subject: [PATCH 06/15] update assignment to take in variant array; update toEvent and tests --- src/Assignment/Assignment.php | 17 +++--- src/Assignment/AssignmentFilter.php | 2 +- src/Assignment/AssignmentService.php | 24 +++++--- src/Flag/FlagConfigFetcher.php | 2 +- src/Local/LocalEvaluationClient.php | 2 +- tests/Amplitude/AmplitudeTest.php | 67 +++++++++++++++++---- tests/Amplitude/MockAmplitude.php | 2 - tests/Assignment/AssignmentServiceTest.php | 69 +++++++++++++++------- 8 files changed, 133 insertions(+), 52 deletions(-) diff --git a/src/Assignment/Assignment.php b/src/Assignment/Assignment.php index 01c3551..42b4844 100644 --- a/src/Assignment/Assignment.php +++ b/src/Assignment/Assignment.php @@ -7,24 +7,27 @@ class Assignment { public User $user; - public array $results; + public array $variants; public int $timestamp; - public function __construct(User $user, array $results) + public function __construct(User $user, array $variants) { $this->user = $user; - $this->results = $results; + $this->variants = $variants; $this->timestamp = floor(microtime(true) * 1000); } public function canonicalize(): string { - $canonical = trim("{$this->user->userId} {$this->user->deviceId}"); - $sortedKeys = array_keys($this->results); + $canonical = trim("{$this->user->userId} {$this->user->deviceId}") . ' '; + $sortedKeys = array_keys($this->variants); sort($sortedKeys); foreach ($sortedKeys as $key) { - $value = $this->results[$key]; - $canonical .= " " . trim($key) . " " . trim($value['key']); + $variant = $this->variants[$key]; + if (!$variant->key) { + continue; + } + $canonical .= trim($key) . ' ' . trim($variant->key) . ' '; } return $canonical; } diff --git a/src/Assignment/AssignmentFilter.php b/src/Assignment/AssignmentFilter.php index 433e10f..7f3e6ef 100644 --- a/src/Assignment/AssignmentFilter.php +++ b/src/Assignment/AssignmentFilter.php @@ -14,7 +14,7 @@ public function __construct(int $size, int $ttlMillis = DAY_MILLIS) public function shouldTrack(Assignment $assignment): bool { - if (count($assignment->results) === 0) { + if (count($assignment->variants) === 0) { return false; } diff --git a/src/Assignment/AssignmentService.php b/src/Assignment/AssignmentService.php index c01ee42..7c4e69d 100644 --- a/src/Assignment/AssignmentService.php +++ b/src/Assignment/AssignmentService.php @@ -38,21 +38,27 @@ public static function toEvent(Assignment $assignment): Event $event->eventProperties = []; $event->userProperties = []; - foreach ($assignment->results as $resultsKey => $result) { - $event->eventProperties["{$resultsKey}.variant"] = $result['key']; - } - $set = []; $unset = []; - foreach ($assignment->results as $resultsKey => $result) { - $flagType = $result['metadata']['flagType'] ?? null; - $default = $result['metadata']['default'] ?? false; + foreach ($assignment->variants as $flagKey => $variant) { + if (!$variant->key) { + echo $flagKey . "\n"; + continue; + } + $event->eventProperties["{$flagKey}.variant"] = $variant->key; + $version = $variant->metadata['flagVersion'] ?? null; + $segmentName = $variant->metadata['segmentName'] ?? null; + if ($version && $segmentName) { + $event->eventProperties["{$flagKey}.details"] = "v{$version} rule:{$segmentName}"; + } + $flagType = $variant->metadata['flagType'] ?? null; + $default = $variant->metadata['default'] ?? false; if ($flagType == FLAG_TYPE_MUTUAL_EXCLUSION_GROUP) { continue; } elseif ($default) { - $unset["[Experiment] {$resultsKey}"] = '-'; + $unset["[Experiment] {$flagKey}"] = '-'; } else { - $set["[Experiment] {$resultsKey}"] = $result['value']; + $set["[Experiment] {$flagKey}"] = $variant->key; } } diff --git a/src/Flag/FlagConfigFetcher.php b/src/Flag/FlagConfigFetcher.php index 1d848ae..2085680 100644 --- a/src/Flag/FlagConfigFetcher.php +++ b/src/Flag/FlagConfigFetcher.php @@ -40,7 +40,7 @@ public function __construct(string $apiKey, bool $debug, string $serverUrl = Loc */ public function fetch(): PromiseInterface { - $endpoint = $this->serverUrl . '/sdk/v2/flags'; + $endpoint = $this->serverUrl . '/sdk/v2/flags?v=0'; $headers = [ 'Authorization' => 'Api-Key ' . $this->apiKey, 'Accept' => 'application/json', diff --git a/src/Local/LocalEvaluationClient.php b/src/Local/LocalEvaluationClient.php index e4976ff..94828c2 100644 --- a/src/Local/LocalEvaluationClient.php +++ b/src/Local/LocalEvaluationClient.php @@ -92,7 +92,7 @@ public function evaluate(User $user, array $flagKeys = []): array $variants[$flagKey] = Variant::convertEvaluationVariantToVariant($flagResult); } if ($included || $flagResult['metadata']['flagType'] == FLAG_TYPE_HOLDOUT_GROUP || $flagResult['metadata']['flagType'] == FLAG_TYPE_MUTUAL_EXCLUSION_GROUP) { - $assignmentResults[$flagKey] = $flagResult; + $assignmentResults[$flagKey] = Variant::convertEvaluationVariantToVariant($flagResult); } } diff --git a/tests/Amplitude/AmplitudeTest.php b/tests/Amplitude/AmplitudeTest.php index b8d35b6..e582f32 100644 --- a/tests/Amplitude/AmplitudeTest.php +++ b/tests/Amplitude/AmplitudeTest.php @@ -15,14 +15,24 @@ class AmplitudeTest extends TestCase { - private array $historyContainer; + private array $postContainer; const API_KEY = 'a6dd847b9d2f03c816d4f3f8458cdc1d'; public function setUp(): void { - $this->historyContainer = []; + $this->postContainer = []; } +// public function testAmplitude() +// { +// $client = new MockAmplitude(self::API_KEY, true); +// $event1 = new Event('test1'); +// $event1->userId = 'tim.yiu@amplitude.com'; +// $client->logEvent($event1); +// $client->flush()->wait(); +// $this->assertTrue(true); +// } + public function testAmplitudeConfigServerUrl() { $config = AmplitudeConfig::builder() @@ -43,14 +53,51 @@ public function testAmplitudeConfigServerUrl() $this->assertEquals('https://api.eu.amplitude.com/batch', $config->serverUrl); } - public function testAmplitude() + public function testEmptyQueueAfterFlushSuccess() { $client = new MockAmplitude(self::API_KEY, true); + $mock = new MockHandler([ + new Response(200, ['X-Foo' => 'Bar']), + ]); + + $handlerStack = HandlerStack::create($mock); + $httpClient = new Client(['handler' => $handlerStack]); + $client->setHttpClient($httpClient); $event1 = new Event('test1'); - $event1->userId = 'tim.yiu@amplitude.com'; + $event2 = new Event('test2'); + $event3 = new Event('test3'); $client->logEvent($event1); + $client->logEvent($event2); + $client->logEvent($event3); + $this->assertEquals(3, $client->getQueueSize()); $client->flush()->wait(); - $this->assertTrue(true); + $this->assertEquals(0, $client->getQueueSize()); + } + + public function testFlushAfterMaxQueue() + { + $config = AmplitudeConfig::builder() + ->flushQueueSize(3) + ->build(); + $client = new MockAmplitude(self::API_KEY, true, $config); + $mock = new MockHandler([ + new Response(200, ['X-Foo' => 'Bar']), + ]); + $handlerStack = HandlerStack::create($mock); + $httpClient = new Client(['handler' => $handlerStack, + 'on_stats' => function (TransferStats $stats) { + $this->postContainer[] = $stats; + }]); + $client->setHttpClient($httpClient); + $event1 = new Event('test1'); + $event2 = new Event('test2'); + $event3 = new Event('test3'); + $client->logEvent($event1); + $client->logEvent($event2); + $this->assertEquals(2, $client->getQueueSize()); + $client->logEvent($event3); + $this->assertEquals(1, $this->countPostRequests()); + $this->assertEquals(0, $client->getQueueSize()); } public function testBackoffRetriesToFailure() @@ -70,8 +117,8 @@ public function testBackoffRetriesToFailure() $handlerStack = HandlerStack::create($mock); $httpClient = new Client(['handler' => $handlerStack, 'on_stats' => function (TransferStats $stats) { - $this->historyContainer[] = $stats; - },]); + $this->postContainer[] = $stats; + }]); $client->setHttpClient($httpClient); $event1 = new Event('test'); $event1->userId = 'user_id'; @@ -95,8 +142,8 @@ public function testBackoffRetriesThenSuccess() $handlerStack = HandlerStack::create($mock); $httpClient = new Client(['handler' => $handlerStack, 'on_stats' => function (TransferStats $stats) { - $this->historyContainer[] = $stats; - },]); + $this->postContainer[] = $stats; + }]); $client->setHttpClient($httpClient); $event1 = new Event('test'); $event1->userId = 'user_id'; @@ -107,7 +154,7 @@ public function testBackoffRetriesThenSuccess() private function countPostRequests(): int { - return count(array_filter($this->historyContainer, function (TransferStats $stats) { + return count(array_filter($this->postContainer, function (TransferStats $stats) { return $stats->getRequest()->getMethod() === 'POST'; })); } diff --git a/tests/Amplitude/MockAmplitude.php b/tests/Amplitude/MockAmplitude.php index 04cd9cb..cc21a08 100644 --- a/tests/Amplitude/MockAmplitude.php +++ b/tests/Amplitude/MockAmplitude.php @@ -8,8 +8,6 @@ class MockAmplitude extends Amplitude { - private int $retries = 0; - public function __construct(string $apiKey, bool $debug, AmplitudeConfig $config = null) { parent::__construct($apiKey, $debug, $config); diff --git a/tests/Assignment/AssignmentServiceTest.php b/tests/Assignment/AssignmentServiceTest.php index f38bc4c..47b7eba 100644 --- a/tests/Assignment/AssignmentServiceTest.php +++ b/tests/Assignment/AssignmentServiceTest.php @@ -6,6 +6,7 @@ use AmplitudeExperiment\Assignment\Assignment; use AmplitudeExperiment\Assignment\AssignmentService; use AmplitudeExperiment\User; +use AmplitudeExperiment\Variant; use PHPUnit\Framework\TestCase; use const AmplitudeExperiment\Assignment\DAY_MILLIS; use function AmplitudeExperiment\hashCode; @@ -16,24 +17,35 @@ class AssignmentServiceTest extends TestCase { public function testAssignmentToEventAsExpected() { - // Mock ExperimentUser and results $user = User::builder()->userId('user')->deviceId('device')->build(); $results = [ - 'flag-key-1' => [ - 'value' => 'on', - ], - 'flag-key-2' => [ - 'value' => 'control', - 'metadata' => [ - 'default' => true, - ], - ], + 'basic' => new Variant( + 'control', 'control', null, null, + ['segmentName' => 'All Other Users', 'flagType' => 'experiment', 'flagVersion' => 10, 'default' => false] + ), + 'different_value' => new Variant( + 'on', 'control', null, null, + ['segmentName' => 'All Other Users', 'flagType' => 'experiment', 'flagVersion' => 10, 'default' => false] + ), + 'default' => new Variant( + 'off', null, null, null, + ['segmentName' => 'All Other Users', 'flagType' => 'experiment', 'flagVersion' => 10, 'default' => true] + ), + 'mutex' => new Variant( + 'slot-1', 'slot-1', null, null, + ['segmentName' => 'All Other Users', 'flagType' => 'mutual-exclusion-group', 'flagVersion' => 10, 'default' => false] + ), + 'holdout' => new Variant('holdout', 'holdout', null, null, + ['segmentName' => 'All Other Users', 'flagType' => 'holdout-group', 'flagVersion' => 10, 'default' => false] + ), + 'partial_metadata' => new Variant('on', 'on', null, null, + ['segmentName' => 'All Other Users', 'flagType' => 'release'] + ), + 'empty_metadata' => new Variant('on', 'on'), + 'empty_variant' => new Variant() ]; - // Create Assignment object $assignment = new Assignment($user, $results); - - // Convert Assignment to Event $event = AssignmentService::toEvent($assignment); // Assertions @@ -42,17 +54,32 @@ public function testAssignmentToEventAsExpected() $this->assertEquals('[Experiment] Assignment', $event->eventType); $eventProperties = $event->eventProperties; - $this->assertCount(2, $eventProperties); - $this->assertEquals('on', $eventProperties['flag-key-1.variant']); - $this->assertEquals('control', $eventProperties['flag-key-2.variant']); + $this->assertEquals('control', $eventProperties['basic.variant']); + $this->assertEquals('v10 rule:All Other Users', $eventProperties['basic.details']); + $this->assertEquals('on', $eventProperties['different_value.variant']); + $this->assertEquals('v10 rule:All Other Users', $eventProperties['different_value.details']); + $this->assertEquals('off', $eventProperties['default.variant']); + $this->assertEquals('v10 rule:All Other Users', $eventProperties['default.details']); + $this->assertEquals('slot-1', $eventProperties['mutex.variant']); + $this->assertEquals('v10 rule:All Other Users', $eventProperties['mutex.details']); + $this->assertEquals('holdout', $eventProperties['holdout.variant']); + $this->assertEquals('v10 rule:All Other Users', $eventProperties['holdout.details']); + $this->assertEquals('on', $eventProperties['partial_metadata.variant']); + $this->assertEquals('on', $eventProperties['empty_metadata.variant']); $userProperties = $event->userProperties; - $this->assertCount(2, $userProperties); - $this->assertCount(1, $userProperties['$set']); - $this->assertCount(1, $userProperties['$unset']); + $setProperties = $userProperties['$set']; + $this->assertEquals('control', $setProperties['[Experiment] basic']); + $this->assertEquals('on', $setProperties['[Experiment] different_value']); + $this->assertEquals('holdout', $setProperties['[Experiment] holdout']); + $this->assertEquals('on', $setProperties['[Experiment] partial_metadata']); + $this->assertEquals('on', $setProperties['[Experiment] empty_metadata']); + $unsetProperties = $userProperties['$unset']; + $this->assertEquals('-', $unsetProperties['[Experiment] default']); - $canonicalization = 'user device flag-key-1 on flag-key-2 control'; - $expected = 'user device ' . hashCode($canonicalization) . ' ' . floor($assignment->timestamp / DAY_MILLIS); + $canonicalization = 'user device basic control default off different_value on empty_metadata on holdout ' . + 'holdout mutex slot-1 partial_metadata on '; + $expected = "user device " . hashCode($canonicalization) . ' ' . floor($assignment->timestamp / DAY_MILLIS); $this->assertEquals($expected, $event->insertId); } } From 957a1f61e3496c1e7e4583553271f43521a434f2 Mon Sep 17 00:00:00 2001 From: tyiuhc Date: Fri, 17 Nov 2023 15:54:22 -0800 Subject: [PATCH 07/15] fix: assignment filter test and code style --- tests/Assignment/AssignmentFilterTest.php | 87 +++++---------------- tests/Local/LocalEvaluationClientTest.php | 21 ++--- tests/Remote/RemoteEvaluationClientTest.php | 16 ++-- 3 files changed, 37 insertions(+), 87 deletions(-) diff --git a/tests/Assignment/AssignmentFilterTest.php b/tests/Assignment/AssignmentFilterTest.php index bc92321..140edbf 100644 --- a/tests/Assignment/AssignmentFilterTest.php +++ b/tests/Assignment/AssignmentFilterTest.php @@ -5,6 +5,7 @@ use AmplitudeExperiment\Assignment\Assignment; use AmplitudeExperiment\Assignment\AssignmentFilter; use AmplitudeExperiment\User; +use AmplitudeExperiment\Variant; use PHPUnit\Framework\TestCase; use function AmplitudeExperiment\sleep; @@ -16,15 +17,8 @@ public function testSingleAssignment() { $user = User::builder()->userId('user')->build(); $results = [ - 'flag-key-1' => [ - 'value' => 'on', - ], - 'flag-key-2' => [ - 'value' => 'control', - 'metadata' => [ - 'default' => true, - ], - ], + 'flag-key-1' => new Variant('on'), + 'flag-key-2' => new Variant('control', null, null, null, ['default' => true]) ]; $filter = new AssignmentFilter(100); @@ -36,15 +30,8 @@ public function testDuplicateAssignment() { $user = User::builder()->userId('user')->build(); $results = [ - 'flag-key-1' => [ - 'value' => 'on', - ], - 'flag-key-2' => [ - 'value' => 'control', - 'metadata' => [ - 'default' => true, - ], - ], + 'flag-key-1' => new Variant('on'), + 'flag-key-2' => new Variant('control', null, null, null, ['default' => true]) ]; $filter = new AssignmentFilter(100); @@ -58,24 +45,13 @@ public function testSameUserDifferentResults() { $user = User::builder()->userId('user')->build(); $results1 = [ - 'flag-key-1' => [ - 'value' => 'on', - ], - 'flag-key-2' => [ - 'value' => 'control', - 'metadata' => [ - 'default' => true, - ], - ], + 'flag-key-1' => new Variant('on'), + 'flag-key-2' => new Variant('control', null, null, null, ['default' => true]) ]; $results2 = [ - 'flag-key-1' => [ - 'value' => 'control', - ], - 'flag-key-2' => [ - 'value' => 'on', - ], + 'flag-key-1' => new Variant('control'), + 'flag-key-2' => new Variant('on') ]; $filter = new AssignmentFilter(100); @@ -90,15 +66,8 @@ public function testSameResultDifferentUser() $user1 = User::builder()->userId('user')->build(); $user2 = User::builder()->userId('different-user')->build(); $results = [ - 'flag-key-1' => [ - 'value' => 'on', - ], - 'flag-key-2' => [ - 'value' => 'control', - 'metadata' => [ - 'default' => true, - ], - ], + 'flag-key-1' => new Variant('on'), + 'flag-key-2' => new Variant('control', null, null, null, ['default' => true]) ]; $filter = new AssignmentFilter(100); @@ -125,16 +94,10 @@ public function testEmptyResult() public function testDuplicateAssignmentsDifferentResultOrdering() { $user = User::builder()->userId('user')->build(); - $result1 = [ - 'value' => 'on', - ]; + $result1 = new Variant('on'); - $result2 = [ - 'value' => 'control', - 'metadata' => [ - 'default' => true, - ] - ]; + $result2 = + new Variant('control', null, null, null, ['default' => true]); $results1 = [ 'flag-key-1' => $result1, @@ -159,15 +122,8 @@ public function testLruReplacement() $user2 = User::builder()->userId('user2')->build(); $user3 = User::builder()->userId('user3')->build(); $results = [ - 'flag-key-1' => [ - 'value' => 'on', - ], - 'flag-key-2' => [ - 'value' => 'control', - 'metadata' => [ - 'default' => true, - ] - ], + 'flag-key-1' => new Variant('on'), + 'flag-key-2' => new Variant('control', null, null, null, ['default' => true]) ]; $filter = new AssignmentFilter(2); @@ -185,15 +141,8 @@ public function testTtlBasedEviction() $user1 = User::builder()->userId('user')->build(); $user2 = User::builder()->userId('different-user')->build(); $results = [ - 'flag-key-1' => [ - 'value' => 'on', - ], - 'flag-key-2' => [ - 'value' => 'control', - 'metadata' => [ - 'default' => true, - ] - ], + 'flag-key-1' => new Variant('on'), + 'flag-key-2' => new Variant('control', null, null, null, ['default' => true]) ]; $filter = new AssignmentFilter(100, 1000); diff --git a/tests/Local/LocalEvaluationClientTest.php b/tests/Local/LocalEvaluationClientTest.php index c6f1eae..3367734 100644 --- a/tests/Local/LocalEvaluationClientTest.php +++ b/tests/Local/LocalEvaluationClientTest.php @@ -36,39 +36,39 @@ public function testEvaluateAllFlags() { $variants = $this->client->evaluate($this->testUser); $variant = $variants['sdk-local-evaluation-ci-test']; - self::assertEquals("on", $variant->key); - self::assertEquals("payload", $variant->payload); + $this->assertEquals("on", $variant->key); + $this->assertEquals("payload", $variant->payload); } public function testEvaluateOneFlagSuccess() { $variants = $this->client->evaluate($this->testUser, ["sdk-local-evaluation-ci-test"]); $variant = $variants['sdk-local-evaluation-ci-test']; - self::assertEquals("on", $variant->key); - self::assertEquals("payload", $variant->payload); + $this->assertEquals("on", $variant->key); + $this->assertEquals("payload", $variant->payload); } public function testEvaluateWithDependenciesWithFlagKeysSuccess() { $variants = $this->client->evaluate($this->testUser, ['sdk-ci-local-dependencies-test']); $variant = $variants['sdk-ci-local-dependencies-test']; - self::assertEquals("control", $variant->key); - self::assertEquals(null, $variant->payload); + $this->assertEquals("control", $variant->key); + $this->assertEquals(null, $variant->payload); } public function testEvaluateWithDependenciesWithUnknownFlagKeysNoVariant() { $variants = $this->client->evaluate($this->testUser, ['does-not-exist']); - self::assertFalse(isset($variants['sdk-ci-local-dependencies-test'])); + $this->assertFalse(isset($variants['sdk-ci-local-dependencies-test'])); } public function testEvaluateWithDependenciesVariantHeldOut() { $variants = $this->client->evaluate($this->testUser); $variant = $variants['sdk-ci-local-dependencies-test-holdout']; - self::assertEquals("off", $variant->key); - self::assertEquals(null, $variant->payload); - self::assertTrue($variant->metadata["default"]); + $this->assertEquals("off", $variant->key); + $this->assertEquals(null, $variant->payload); + $this->assertTrue($variant->metadata["default"]); } public function testAssignment() @@ -79,5 +79,6 @@ public function testAssignment() $client->start()->wait(); $user = User::builder()->userId('tim.yiu@amplitude.com')->build(); $client->evaluate($user); + $this->assertTrue(true); } } diff --git a/tests/Remote/RemoteEvaluationClientTest.php b/tests/Remote/RemoteEvaluationClientTest.php index f90737b..19b3798 100644 --- a/tests/Remote/RemoteEvaluationClientTest.php +++ b/tests/Remote/RemoteEvaluationClientTest.php @@ -31,8 +31,8 @@ public function testFetchSuccess() $client = new RemoteEvaluationClient($this->apiKey); $variants = $client->fetch($this->testUser)->wait(); $variant = $variants['sdk-ci-test']; - self::assertEquals("on", $variant->key); - self::assertEquals("payload", $variant->payload); + $this->assertEquals("on", $variant->key); + $this->assertEquals("payload", $variant->payload); } /** @@ -61,8 +61,8 @@ public function testFetchNoRetriesTimeoutFailureRetrySuccess() $client = new RemoteEvaluationClient($this->apiKey, $config); $variants = $client->fetch($this->testUser)->wait(); $variant = $variants['sdk-ci-test']; - self::assertEquals("on", $variant->key); - self::assertEquals("payload", $variant->payload); + $this->assertEquals("on", $variant->key); + $this->assertEquals("payload", $variant->payload); } /** @@ -79,8 +79,8 @@ public function testFetchRetryOnceTimeoutFirstThenSucceedWithZeroBackoff() $client = new RemoteEvaluationClient($this->apiKey, $config); $variants = $client->fetch($this->testUser)->wait(); $variant = $variants['sdk-ci-test']; - self::assertEquals("on", $variant->key); - self::assertEquals("payload", $variant->payload); + $this->assertEquals("on", $variant->key); + $this->assertEquals("payload", $variant->payload); } /** @@ -92,8 +92,8 @@ public function testFetchWithFlagKeysOptionsSuccess() $variants = $client->fetch($this->testUser, new FetchOptions(['sdk-ci-test']))->wait(); $variant = $variants['sdk-ci-test']; $this->assertEquals(1, sizeof($variants)); - self::assertEquals("on", $variant->key); - self::assertEquals("payload", $variant->payload); + $this->assertEquals("on", $variant->key); + $this->assertEquals("payload", $variant->payload); } public function testExperimentInitializeRemote() From 84d8b6f895039081a94180869a61c000f15a112a Mon Sep 17 00:00:00 2001 From: tyiuhc Date: Fri, 17 Nov 2023 16:17:38 -0800 Subject: [PATCH 08/15] add test-logevent to assignment service tests --- tests/Assignment/AssignmentServiceTest.php | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/tests/Assignment/AssignmentServiceTest.php b/tests/Assignment/AssignmentServiceTest.php index 47b7eba..7595446 100644 --- a/tests/Assignment/AssignmentServiceTest.php +++ b/tests/Assignment/AssignmentServiceTest.php @@ -2,8 +2,9 @@ namespace AmplitudeExperiment\Test\Assignment; - +use AmplitudeExperiment\Amplitude\Amplitude; use AmplitudeExperiment\Assignment\Assignment; +use AmplitudeExperiment\Assignment\AssignmentFilter; use AmplitudeExperiment\Assignment\AssignmentService; use AmplitudeExperiment\User; use AmplitudeExperiment\Variant; @@ -48,7 +49,6 @@ public function testAssignmentToEventAsExpected() $assignment = new Assignment($user, $results); $event = AssignmentService::toEvent($assignment); - // Assertions $this->assertEquals($user->userId, $event->userId); $this->assertEquals($user->deviceId, $event->deviceId); $this->assertEquals('[Experiment] Assignment', $event->eventType); @@ -77,11 +77,24 @@ public function testAssignmentToEventAsExpected() $unsetProperties = $userProperties['$unset']; $this->assertEquals('-', $unsetProperties['[Experiment] default']); - $canonicalization = 'user device basic control default off different_value on empty_metadata on holdout ' . - 'holdout mutex slot-1 partial_metadata on '; + $canonicalization = 'user device basic control default off different_value on empty_metadata on holdout holdout mutex slot-1 partial_metadata on '; $expected = "user device " . hashCode($canonicalization) . ' ' . floor($assignment->timestamp / DAY_MILLIS); $this->assertEquals($expected, $event->insertId); } + + public function testlogEventCalledInAmplitude() { + $assignmentFilter = new AssignmentFilter(1); + $mockAmp = $this->getMockBuilder(Amplitude::class) + ->setConstructorArgs(['', false]) + ->onlyMethods(['logEvent']) + ->getMock(); + $results = [ + 'flag-key-1' => new Variant('on') + ]; + $service = new AssignmentService($mockAmp, $assignmentFilter); + $mockAmp->expects($this->once())->method('logEvent'); + $service->track(new Assignment(User::builder()->userId('user')->build(), $results)); + } } From 95c260eb1447fa1c79f49387714da61285b37e44 Mon Sep 17 00:00:00 2001 From: tyiuhc Date: Mon, 20 Nov 2023 14:41:52 -0800 Subject: [PATCH 09/15] simplify AssignmentConfigBuilder --- src/Amplitude/AmplitudeConfigBuilder.php | 2 +- src/Assignment/AssignmentConfig.php | 15 ++++++++--- src/Assignment/AssignmentConfigBuilder.php | 31 ++++++++++++++++++++++ src/Local/LocalEvaluationClient.php | 3 +++ src/Remote/RemoteEvaluationClient.php | 2 +- tests/Local/LocalEvaluationClientTest.php | 2 +- 6 files changed, 49 insertions(+), 6 deletions(-) create mode 100644 src/Assignment/AssignmentConfigBuilder.php diff --git a/src/Amplitude/AmplitudeConfigBuilder.php b/src/Amplitude/AmplitudeConfigBuilder.php index 470c309..7bd5925 100644 --- a/src/Amplitude/AmplitudeConfigBuilder.php +++ b/src/Amplitude/AmplitudeConfigBuilder.php @@ -44,7 +44,7 @@ public function useBatch(bool $useBatch): AmplitudeConfigBuilder return $this; } - public function build(): AmplitudeConfig + public function build() { if (!$this->serverUrl) { if ($this->useBatch) { diff --git a/src/Assignment/AssignmentConfig.php b/src/Assignment/AssignmentConfig.php index 39f6af1..13e3403 100644 --- a/src/Assignment/AssignmentConfig.php +++ b/src/Assignment/AssignmentConfig.php @@ -8,12 +8,21 @@ class AssignmentConfig { public string $apiKey; public int $cacheCapacity; - public ?AmplitudeConfig $amplitudeConfig; + public AmplitudeConfig $amplitudeConfig; - public function __construct(string $apiKey, int $cacheCapacity = 65536, ?AmplitudeConfig $amplitudeConfig = null) + const DEFAULTS = [ + 'cacheCapacity' => 65536, + ]; + + public function __construct(string $apiKey, int $cacheCapacity, AmplitudeConfig $amplitudeConfig) { $this->apiKey = $apiKey; $this->cacheCapacity = $cacheCapacity; - $this->amplitudeConfig = $amplitudeConfig ?? AmplitudeConfig::builder()->build(); + $this->amplitudeConfig = $amplitudeConfig; + } + + public static function builder(string $apiKey): AssignmentConfigBuilder + { + return new AssignmentConfigBuilder($apiKey); } } diff --git a/src/Assignment/AssignmentConfigBuilder.php b/src/Assignment/AssignmentConfigBuilder.php new file mode 100644 index 0000000..6022bb5 --- /dev/null +++ b/src/Assignment/AssignmentConfigBuilder.php @@ -0,0 +1,31 @@ +apiKey = $apiKey; + } + + public function cacheCapacity(int $cacheCapacity): AssignmentConfigBuilder + { + $this->cacheCapacity = $cacheCapacity; + return $this; + } + + public function build(): AssignmentConfig + { + return new AssignmentConfig( + $this->apiKey, + $this->cacheCapacity, + parent::build() + ); + } +} diff --git a/src/Local/LocalEvaluationClient.php b/src/Local/LocalEvaluationClient.php index 94828c2..9626adc 100644 --- a/src/Local/LocalEvaluationClient.php +++ b/src/Local/LocalEvaluationClient.php @@ -82,6 +82,8 @@ public function evaluate(User $user, array $flagKeys = []): array } $this->logger->debug('[Experiment] Evaluate - user: ' . json_encode($user->toArray()) . ' with flags: ' . json_encode($flags)); $results = $this->evaluation->evaluate($user->toEvaluationContext(), $flags); + print(json_encode($results) . "\n"); + $variants = []; $assignmentResults = []; $filter = !empty($flagKeys); @@ -100,6 +102,7 @@ public function evaluate(User $user, array $flagKeys = []): array if ($this->assignmentService) { $this->assignmentService->track(new Assignment($user, $assignmentResults)); } + print_r(count($variants) . "\n"); return $variants; } diff --git a/src/Remote/RemoteEvaluationClient.php b/src/Remote/RemoteEvaluationClient.php index 945db9c..73b8e15 100644 --- a/src/Remote/RemoteEvaluationClient.php +++ b/src/Remote/RemoteEvaluationClient.php @@ -86,7 +86,7 @@ public function doFetch(User $user, int $timeoutMillis, ?FetchOptions $options = $serializedUser = base64_encode(json_encode($libraryUser->toArray())); // Define the request URL - $endpoint = $this->config->serverUrl . '/sdk/v2/vardata'; + $endpoint = $this->config->serverUrl . '/sdk/v2/vardata?v=0'; // Define the request headers $headers = [ diff --git a/tests/Local/LocalEvaluationClientTest.php b/tests/Local/LocalEvaluationClientTest.php index 3367734..cb05dbb 100644 --- a/tests/Local/LocalEvaluationClientTest.php +++ b/tests/Local/LocalEvaluationClientTest.php @@ -73,7 +73,7 @@ public function testEvaluateWithDependenciesVariantHeldOut() public function testAssignment() { - $aConfig = new AssignmentConfig('a6dd847b9d2f03c816d4f3f8458cdc1d'); + $aConfig = AssignmentConfig::builder('a6dd847b9d2f03c816d4f3f8458cdc1d')->flushMaxRetries(4)->build(); $config = LocalEvaluationConfig::builder()->debug(true)->assignmentConfig($aConfig)->build(); $client = new LocalEvaluationClient($this->apiKey, $config); $client->start()->wait(); From a66d8c2a83642aadef546eb80ecc7de4dcecf697 Mon Sep 17 00:00:00 2001 From: tyiuhc Date: Mon, 20 Nov 2023 15:17:50 -0800 Subject: [PATCH 10/15] clean up --- src/Local/LocalEvaluationClient.php | 2 -- tests/Amplitude/AmplitudeTest.php | 19 ++++++++----------- 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/src/Local/LocalEvaluationClient.php b/src/Local/LocalEvaluationClient.php index 9626adc..cdc3986 100644 --- a/src/Local/LocalEvaluationClient.php +++ b/src/Local/LocalEvaluationClient.php @@ -82,7 +82,6 @@ public function evaluate(User $user, array $flagKeys = []): array } $this->logger->debug('[Experiment] Evaluate - user: ' . json_encode($user->toArray()) . ' with flags: ' . json_encode($flags)); $results = $this->evaluation->evaluate($user->toEvaluationContext(), $flags); - print(json_encode($results) . "\n"); $variants = []; $assignmentResults = []; @@ -102,7 +101,6 @@ public function evaluate(User $user, array $flagKeys = []): array if ($this->assignmentService) { $this->assignmentService->track(new Assignment($user, $assignmentResults)); } - print_r(count($variants) . "\n"); return $variants; } diff --git a/tests/Amplitude/AmplitudeTest.php b/tests/Amplitude/AmplitudeTest.php index e582f32..50ea91c 100644 --- a/tests/Amplitude/AmplitudeTest.php +++ b/tests/Amplitude/AmplitudeTest.php @@ -16,23 +16,13 @@ class AmplitudeTest extends TestCase { private array $postContainer; - const API_KEY = 'a6dd847b9d2f03c816d4f3f8458cdc1d'; + const API_KEY = 'test'; public function setUp(): void { $this->postContainer = []; } -// public function testAmplitude() -// { -// $client = new MockAmplitude(self::API_KEY, true); -// $event1 = new Event('test1'); -// $event1->userId = 'tim.yiu@amplitude.com'; -// $client->logEvent($event1); -// $client->flush()->wait(); -// $this->assertTrue(true); -// } - public function testAmplitudeConfigServerUrl() { $config = AmplitudeConfig::builder() @@ -51,6 +41,11 @@ public function testAmplitudeConfigServerUrl() ->useBatch(true) ->build(); $this->assertEquals('https://api.eu.amplitude.com/batch', $config->serverUrl); + $config = AmplitudeConfig::builder() + ->serverUrl('test') + ->useBatch(true) + ->build(); + $this->assertEquals('test', $config->serverUrl); } public function testEmptyQueueAfterFlushSuccess() @@ -125,6 +120,7 @@ public function testBackoffRetriesToFailure() $client->logEvent($event1); $client->flush()->wait(); $this->assertEquals(5, $this->countPostRequests()); + $this->assertEquals(1, $client->getQueueSize()); } public function testBackoffRetriesThenSuccess() @@ -150,6 +146,7 @@ public function testBackoffRetriesThenSuccess() $client->logEvent($event1); $client->flush()->wait(); $this->assertEquals(3, $this->countPostRequests()); + $this->assertEquals(0, $client->getQueueSize()); } private function countPostRequests(): int From aa96e2ca2a3233eb2a3b6bb722d38e06f74fb4e0 Mon Sep 17 00:00:00 2001 From: tyiuhc Date: Mon, 20 Nov 2023 15:56:21 -0800 Subject: [PATCH 11/15] clean up --- src/Amplitude/Amplitude.php | 2 -- src/Assignment/AssignmentService.php | 1 - 2 files changed, 3 deletions(-) diff --git a/src/Amplitude/Amplitude.php b/src/Amplitude/Amplitude.php index 5e92a9c..c7f3da7 100644 --- a/src/Amplitude/Amplitude.php +++ b/src/Amplitude/Amplitude.php @@ -46,7 +46,6 @@ function () { public function logEvent(Event $event) { - print_r($event->toArray()); $this->queue[] = $event->toArray(); if (count($this->queue) >= $this->config->flushQueueSize) { $this->flush()->wait(); @@ -70,7 +69,6 @@ private function post(string $url, array $payload): PromiseInterface return $promise->then( function ($response) use ($payload) { // Process the successful response if needed - echo $response->getBody(); $this->logger->debug("[Amplitude] Event sent successfully: " . json_encode($payload)); }, function (\Exception $exception) use ($payload) { diff --git a/src/Assignment/AssignmentService.php b/src/Assignment/AssignmentService.php index 7c4e69d..17d685f 100644 --- a/src/Assignment/AssignmentService.php +++ b/src/Assignment/AssignmentService.php @@ -42,7 +42,6 @@ public static function toEvent(Assignment $assignment): Event $unset = []; foreach ($assignment->variants as $flagKey => $variant) { if (!$variant->key) { - echo $flagKey . "\n"; continue; } $event->eventProperties["{$flagKey}.variant"] = $variant->key; From 21b420f4a1fc0f5f459cdb912c067ad6de3ec6bd Mon Sep 17 00:00:00 2001 From: tyiuhc Date: Tue, 21 Nov 2023 11:18:37 -0800 Subject: [PATCH 12/15] add minIdLength amplitude option --- src/Amplitude/Amplitude.php | 2 +- src/Amplitude/AmplitudeConfig.php | 4 ++++ src/Amplitude/AmplitudeConfigBuilder.php | 8 ++++++++ tests/Local/LocalEvaluationClientTest.php | 4 ++-- 4 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/Amplitude/Amplitude.php b/src/Amplitude/Amplitude.php index c7f3da7..1f0a04b 100644 --- a/src/Amplitude/Amplitude.php +++ b/src/Amplitude/Amplitude.php @@ -29,7 +29,7 @@ public function __construct(string $apiKey, bool $debug, AmplitudeConfig $config public function flush(): PromiseInterface { - $payload = ["api_key" => $this->apiKey, "events" => $this->queue]; + $payload = ["api_key" => $this->apiKey, "events" => $this->queue, "options" => ["min_id_length" => $this->config->minIdLength]]; // Fetch initial flag configs and await the result. return Backoff::doWithBackoff( diff --git a/src/Amplitude/AmplitudeConfig.php b/src/Amplitude/AmplitudeConfig.php index 3755119..bb9024c 100644 --- a/src/Amplitude/AmplitudeConfig.php +++ b/src/Amplitude/AmplitudeConfig.php @@ -6,6 +6,7 @@ class AmplitudeConfig { public int $flushQueueSize; public int $flushMaxRetries; + public int $minIdLength; public string $serverZone; public string $serverUrl; public string $useBatch; @@ -23,6 +24,7 @@ class AmplitudeConfig ] ], 'useBatch' => false, + 'minIdLength' => 5, 'flushQueueSize' => 200, 'flushMaxRetries' => 12, ]; @@ -30,6 +32,7 @@ class AmplitudeConfig public function __construct( int $flushQueueSize, int $flushMaxRetries, + int $minIdLength, string $serverZone, string $serverUrl, bool $useBatch @@ -37,6 +40,7 @@ public function __construct( { $this->flushQueueSize = $flushQueueSize; $this->flushMaxRetries = $flushMaxRetries; + $this->minIdLength = $minIdLength; $this->serverZone = $serverZone; $this->serverUrl = $serverUrl; $this->useBatch = $useBatch; diff --git a/src/Amplitude/AmplitudeConfigBuilder.php b/src/Amplitude/AmplitudeConfigBuilder.php index 7bd5925..59b7659 100644 --- a/src/Amplitude/AmplitudeConfigBuilder.php +++ b/src/Amplitude/AmplitudeConfigBuilder.php @@ -6,6 +6,7 @@ class AmplitudeConfigBuilder { protected int $flushQueueSize = AmplitudeConfig::DEFAULTS['flushQueueSize']; protected int $flushMaxRetries = AmplitudeConfig::DEFAULTS['flushMaxRetries']; + protected int $minIdLength = AmplitudeConfig::DEFAULTS['minIdLength']; protected string $serverZone = AmplitudeConfig::DEFAULTS['serverZone']; protected ?string $serverUrl = null; protected bool $useBatch = AmplitudeConfig::DEFAULTS['useBatch']; @@ -26,6 +27,12 @@ public function flushMaxRetries(int $flushMaxRetries): AmplitudeConfigBuilder return $this; } + public function minIdLength(int $minIdLength): AmplitudeConfigBuilder + { + $this->minIdLength = $minIdLength; + return $this; + } + public function serverZone(string $serverZone): AmplitudeConfigBuilder { $this->serverZone = $serverZone; @@ -56,6 +63,7 @@ public function build() return new AmplitudeConfig( $this->flushQueueSize, $this->flushMaxRetries, + $this->minIdLength, $this->serverZone, $this->serverUrl, $this->useBatch diff --git a/tests/Local/LocalEvaluationClientTest.php b/tests/Local/LocalEvaluationClientTest.php index cb05dbb..2d89a0e 100644 --- a/tests/Local/LocalEvaluationClientTest.php +++ b/tests/Local/LocalEvaluationClientTest.php @@ -73,11 +73,11 @@ public function testEvaluateWithDependenciesVariantHeldOut() public function testAssignment() { - $aConfig = AssignmentConfig::builder('a6dd847b9d2f03c816d4f3f8458cdc1d')->flushMaxRetries(4)->build(); + $aConfig = AssignmentConfig::builder('a6dd847b9d2f03c816d4f3f8458cdc1d')->minIdLength(2)->build(); $config = LocalEvaluationConfig::builder()->debug(true)->assignmentConfig($aConfig)->build(); $client = new LocalEvaluationClient($this->apiKey, $config); $client->start()->wait(); - $user = User::builder()->userId('tim.yiu@amplitude.com')->build(); + $user = User::builder()->userId('tim.yiu@amplitude.com')->deviceId('d0')->build(); $client->evaluate($user); $this->assertTrue(true); } From 2dfb5f0e139ec298499c9a725ba72ceb4bfbbd86 Mon Sep 17 00:00:00 2001 From: tyiuhc Date: Wed, 22 Nov 2023 10:36:36 -0800 Subject: [PATCH 13/15] update evaluate logic --- src/Local/LocalEvaluationClient.php | 27 ++++----------------------- 1 file changed, 4 insertions(+), 23 deletions(-) diff --git a/src/Local/LocalEvaluationClient.php b/src/Local/LocalEvaluationClient.php index cdc3986..5593f6a 100644 --- a/src/Local/LocalEvaluationClient.php +++ b/src/Local/LocalEvaluationClient.php @@ -12,17 +12,13 @@ use AmplitudeExperiment\Flag\FlagConfigService; use AmplitudeExperiment\User; use AmplitudeExperiment\Util; -use AmplitudeExperiment\Variant; use GuzzleHttp\Promise\PromiseInterface; use Monolog\Logger; use function AmplitudeExperiment\EvaluationCore\topologicalSort; use function AmplitudeExperiment\initializeLogger; -use const AmplitudeExperiment\Assignment\FLAG_TYPE_HOLDOUT_GROUP; -use const AmplitudeExperiment\Assignment\FLAG_TYPE_MUTUAL_EXCLUSION_GROUP; require_once __DIR__ . '/../EvaluationCore/Util.php'; require_once __DIR__ . '/../Util.php'; -require_once __DIR__ . '/../Assignment/AssignmentService.php'; /** * Experiment client for evaluating variants for a user locally. @@ -81,27 +77,12 @@ public function evaluate(User $user, array $flagKeys = []): array $this->logger->error('[Experiment] Evaluate - error sorting flags: ' . $e->getMessage()); } $this->logger->debug('[Experiment] Evaluate - user: ' . json_encode($user->toArray()) . ' with flags: ' . json_encode($flags)); - $results = $this->evaluation->evaluate($user->toEvaluationContext(), $flags); - - $variants = []; - $assignmentResults = []; - $filter = !empty($flagKeys); - - foreach ($results as $flagKey => $flagResult) { - $included = !$filter || in_array($flagKey, $flagKeys); - if ($included) { - $variants[$flagKey] = Variant::convertEvaluationVariantToVariant($flagResult); - } - if ($included || $flagResult['metadata']['flagType'] == FLAG_TYPE_HOLDOUT_GROUP || $flagResult['metadata']['flagType'] == FLAG_TYPE_MUTUAL_EXCLUSION_GROUP) { - $assignmentResults[$flagKey] = Variant::convertEvaluationVariantToVariant($flagResult); - } - } - - $this->logger->debug('[Experiment] Evaluate - variants:', $variants); + $results = array_map('AmplitudeExperiment\Variant::convertEvaluationVariantToVariant',$this->evaluation->evaluate($user->toEvaluationContext(), $flags)); + $this->logger->debug('[Experiment] Evaluate - variants:', $results); if ($this->assignmentService) { - $this->assignmentService->track(new Assignment($user, $assignmentResults)); + $this->assignmentService->track(new Assignment($user, $results)); } - return $variants; + return $results; } private function initializeAssignmentService(?AssignmentConfig $config): void From 8037a7de9b0ce0e8a6ad9743414fcee84d877856 Mon Sep 17 00:00:00 2001 From: tyiuhc Date: Wed, 22 Nov 2023 11:09:23 -0800 Subject: [PATCH 14/15] cleanup and add descriptions --- src/Amplitude/Amplitude.php | 7 +++++- src/Amplitude/AmplitudeConfig.php | 25 ++++++++++++++++++++++ src/Assignment/AssignmentConfig.php | 7 ++++++ src/Assignment/AssignmentConfigBuilder.php | 4 ++++ src/Assignment/AssignmentService.php | 1 - tests/Local/LocalEvaluationClientTest.php | 12 ----------- 6 files changed, 42 insertions(+), 14 deletions(-) diff --git a/src/Amplitude/Amplitude.php b/src/Amplitude/Amplitude.php index 1f0a04b..faa0035 100644 --- a/src/Amplitude/Amplitude.php +++ b/src/Amplitude/Amplitude.php @@ -4,13 +4,15 @@ use AmplitudeExperiment\Backoff; use GuzzleHttp\Client; -use GuzzleHttp\Exception\GuzzleException; use GuzzleHttp\Promise\PromiseInterface; use Monolog\Logger; use function AmplitudeExperiment\initializeLogger; require_once __DIR__ . '/../Util.php'; +/** + * Amplitude client for sending events to Amplitude. + */ class Amplitude { private string $apiKey; @@ -52,6 +54,9 @@ public function logEvent(Event $event) } } + /** + * Flush the queue when the client is destructed. + */ public function __destruct() { if (count($this->queue) > 0) { diff --git a/src/Amplitude/AmplitudeConfig.php b/src/Amplitude/AmplitudeConfig.php index bb9024c..045b935 100644 --- a/src/Amplitude/AmplitudeConfig.php +++ b/src/Amplitude/AmplitudeConfig.php @@ -2,13 +2,38 @@ namespace AmplitudeExperiment\Amplitude; +/** + * Configuration options for Amplitude. This is an object that can be created using + * a {@link AmplitudeConfigBuilder}. Example usage: + * + * AmplitudeConfigBuilder::builder()->serverZone("EU")->build(); + */ class AmplitudeConfig { + /** + * The events buffered in memory will flush when exceed flushQueueSize + * Must be positive. + */ public int $flushQueueSize; + /** + * The maximum retry attempts for an event when receiving error response. + */ public int $flushMaxRetries; + /** + * The minimum length of user_id and device_id for events. Default to 5. + */ public int $minIdLength; + /** + * The server zone of project. Default to 'US'. Support 'EU'. + */ public string $serverZone; + /** + * API endpoint url. Default to None. Auto selected by configured server_zone + */ public string $serverUrl; + /** + * True to use batch API endpoint, False to use HTTP V2 API endpoint. + */ public string $useBatch; const DEFAULTS = [ diff --git a/src/Assignment/AssignmentConfig.php b/src/Assignment/AssignmentConfig.php index 13e3403..b02ff60 100644 --- a/src/Assignment/AssignmentConfig.php +++ b/src/Assignment/AssignmentConfig.php @@ -4,6 +4,13 @@ use AmplitudeExperiment\Amplitude\AmplitudeConfig; +/** + * Configuration options for assignment tracking. This is an object that can be created using + * a {@link AssignmentConfigBuilder}. Example usage: + * + * AssignmentConfigBuilder::builder('api-key')->build() + */ + class AssignmentConfig { public string $apiKey; diff --git a/src/Assignment/AssignmentConfigBuilder.php b/src/Assignment/AssignmentConfigBuilder.php index 6022bb5..fe353c6 100644 --- a/src/Assignment/AssignmentConfigBuilder.php +++ b/src/Assignment/AssignmentConfigBuilder.php @@ -4,6 +4,10 @@ use AmplitudeExperiment\Amplitude\AmplitudeConfigBuilder; +/** + * Extends AmplitudeConfigBuilder to allow configuration {@link AmplitudeConfig} of underlying {@link Amplitude} client. + */ + class AssignmentConfigBuilder extends AmplitudeConfigBuilder { protected string $apiKey; diff --git a/src/Assignment/AssignmentService.php b/src/Assignment/AssignmentService.php index 17d685f..1a6b257 100644 --- a/src/Assignment/AssignmentService.php +++ b/src/Assignment/AssignmentService.php @@ -9,7 +9,6 @@ require_once __DIR__ . '/../Util.php'; const FLAG_TYPE_MUTUAL_EXCLUSION_GROUP = 'mutual-exclusion-group'; -const FLAG_TYPE_HOLDOUT_GROUP = 'holdout-group';; const DAY_MILLIS = 24 * 60 * 60 * 1000; class AssignmentService diff --git a/tests/Local/LocalEvaluationClientTest.php b/tests/Local/LocalEvaluationClientTest.php index 2d89a0e..7226e9f 100644 --- a/tests/Local/LocalEvaluationClientTest.php +++ b/tests/Local/LocalEvaluationClientTest.php @@ -2,7 +2,6 @@ namespace AmplitudeExperiment\Test\Local; -use AmplitudeExperiment\Assignment\AssignmentConfig; use AmplitudeExperiment\Experiment; use AmplitudeExperiment\Local\LocalEvaluationClient; use AmplitudeExperiment\Local\LocalEvaluationConfig; @@ -70,15 +69,4 @@ public function testEvaluateWithDependenciesVariantHeldOut() $this->assertEquals(null, $variant->payload); $this->assertTrue($variant->metadata["default"]); } - - public function testAssignment() - { - $aConfig = AssignmentConfig::builder('a6dd847b9d2f03c816d4f3f8458cdc1d')->minIdLength(2)->build(); - $config = LocalEvaluationConfig::builder()->debug(true)->assignmentConfig($aConfig)->build(); - $client = new LocalEvaluationClient($this->apiKey, $config); - $client->start()->wait(); - $user = User::builder()->userId('tim.yiu@amplitude.com')->deviceId('d0')->build(); - $client->evaluate($user); - $this->assertTrue(true); - } } From 23f180be7e1670d0cf0c1123f066637cb27cf727 Mon Sep 17 00:00:00 2001 From: tyiuhc Date: Wed, 22 Nov 2023 11:56:40 -0800 Subject: [PATCH 15/15] nit: debug mode in local eval client test --- tests/Local/LocalEvaluationClientTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Local/LocalEvaluationClientTest.php b/tests/Local/LocalEvaluationClientTest.php index 7226e9f..5837e2d 100644 --- a/tests/Local/LocalEvaluationClientTest.php +++ b/tests/Local/LocalEvaluationClientTest.php @@ -22,7 +22,7 @@ public function __construct() ->deviceId('test_device') ->build(); $experiment = new Experiment(); - $config = LocalEvaluationConfig::builder()->debug(false)->build(); + $config = LocalEvaluationConfig::builder()->debug(true)->build(); $this->client = $experiment->initializeLocal($this->apiKey, $config); }