From 62a66960c744b0c7b91793146da4979e8cb57bf0 Mon Sep 17 00:00:00 2001 From: Tim Yiu <137842098+tyiuhc@users.noreply.github.com> Date: Thu, 15 Feb 2024 15:13:03 -0800 Subject: [PATCH] feat: AssignmentTrackingProvider used to track local evaluation assignment events (#14) --- src/Amplitude/Amplitude.php | 9 +- src/Amplitude/AmplitudeConfig.php | 24 ++++- src/Amplitude/AmplitudeConfigBuilder.php | 25 ++++- src/Amplitude/Event.php | 14 +++ src/Assignment/Assignment.php | 99 ++++++++++++++++++- src/Assignment/AssignmentConfig.php | 28 +++--- src/Assignment/AssignmentConfigBuilder.php | 30 +++--- src/Assignment/AssignmentService.php | 61 +++--------- src/Assignment/AssignmentTrackingProvider.php | 29 ++++++ .../DefaultAssignmentTrackingProvider.php | 22 +++++ src/Local/LocalEvaluationClient.php | 12 +-- tests/Amplitude/AmplitudeTest.php | 14 +-- tests/Amplitude/MockAmplitude.php | 4 +- tests/Assignment/AssignmentServiceTest.php | 79 +-------------- tests/Assignment/AssignmentTest.php | 82 +++++++++++++++ 15 files changed, 351 insertions(+), 181 deletions(-) create mode 100644 src/Assignment/AssignmentTrackingProvider.php create mode 100644 src/Assignment/DefaultAssignmentTrackingProvider.php create mode 100644 tests/Assignment/AssignmentTest.php diff --git a/src/Amplitude/Amplitude.php b/src/Amplitude/Amplitude.php index a9a2674..fa0ca2c 100644 --- a/src/Amplitude/Amplitude.php +++ b/src/Amplitude/Amplitude.php @@ -4,6 +4,8 @@ use AmplitudeExperiment\Http\HttpClientInterface; use AmplitudeExperiment\Http\GuzzleHttpClient; +use AmplitudeExperiment\Logger\DefaultLogger; +use AmplitudeExperiment\Logger\InternalLogger; use Psr\Http\Client\ClientExceptionInterface; use Psr\Log\LoggerInterface; @@ -12,6 +14,9 @@ */ class Amplitude { + /** + * The Amplitude Project API key. + */ private string $apiKey; /** * @var array> @@ -21,11 +26,11 @@ class Amplitude private LoggerInterface $logger; private AmplitudeConfig $config; - public function __construct(string $apiKey, LoggerInterface $logger, AmplitudeConfig $config = null) + public function __construct(string $apiKey, AmplitudeConfig $config = null) { $this->apiKey = $apiKey; - $this->logger = $logger; $this->config = $config ?? AmplitudeConfig::builder()->build(); + $this->logger = new InternalLogger($this->config->logger ?? new DefaultLogger(), $this->config->logLevel); $this->httpClient = $this->config->httpClient ?? $this->config->httpClient ?? new GuzzleHttpClient($this->config->guzzleClientConfig); } diff --git a/src/Amplitude/AmplitudeConfig.php b/src/Amplitude/AmplitudeConfig.php index 4e9c9ba..6a730c2 100644 --- a/src/Amplitude/AmplitudeConfig.php +++ b/src/Amplitude/AmplitudeConfig.php @@ -5,6 +5,8 @@ use AmplitudeExperiment\Assignment\AssignmentConfig; use AmplitudeExperiment\Assignment\AssignmentConfigBuilder; use AmplitudeExperiment\Http\HttpClientInterface; +use AmplitudeExperiment\Logger\LogLevel; +use Psr\Log\LoggerInterface; /** * Configuration options for Amplitude. The Amplitude object is created when you create an {@link AssignmentConfig}. @@ -18,7 +20,7 @@ class AmplitudeConfig */ public int $flushQueueSize; /** - * The maximum retry attempts for an event when receiving error response. + * The minimum length of the id field in events. Default to 5. */ public int $minIdLength; /** @@ -42,6 +44,14 @@ class AmplitudeConfig * The configuration for the underlying default {@link GuzzleHttpClient} client (if used). See {@link GUZZLE_DEFAULTS} for defaults. */ public array $guzzleClientConfig; + /** + * Set to use custom logger. If not set, a {@link DefaultLogger} is used. + */ + public ?LoggerInterface $logger; + /** + * The {@link LogLevel} to use for the logger. + */ + public int $logLevel; const DEFAULTS = [ 'serverZone' => 'US', @@ -58,9 +68,10 @@ class AmplitudeConfig 'useBatch' => false, 'minIdLength' => 5, 'flushQueueSize' => 200, - 'flushMaxRetries' => 12, 'httpClient' => null, - 'guzzleClientConfig' => [] + 'guzzleClientConfig' => [], + 'logger' => null, + 'logLevel' => LogLevel::ERROR, ]; /** @@ -73,8 +84,9 @@ public function __construct( string $serverUrl, bool $useBatch, ?HttpClientInterface $httpClient, - array $guzzleClientConfig - ) + array $guzzleClientConfig, + ?LoggerInterface $logger, + int $logLevel) { $this->flushQueueSize = $flushQueueSize; $this->minIdLength = $minIdLength; @@ -83,6 +95,8 @@ public function __construct( $this->useBatch = $useBatch; $this->httpClient = $httpClient; $this->guzzleClientConfig = $guzzleClientConfig; + $this->logger = $logger; + $this->logLevel = $logLevel; } public static function builder(): AmplitudeConfigBuilder diff --git a/src/Amplitude/AmplitudeConfigBuilder.php b/src/Amplitude/AmplitudeConfigBuilder.php index 27222d4..948ea64 100644 --- a/src/Amplitude/AmplitudeConfigBuilder.php +++ b/src/Amplitude/AmplitudeConfigBuilder.php @@ -3,6 +3,7 @@ namespace AmplitudeExperiment\Amplitude; use AmplitudeExperiment\Http\HttpClientInterface; +use Psr\Log\LoggerInterface; class AmplitudeConfigBuilder { @@ -16,6 +17,8 @@ class AmplitudeConfigBuilder * @var array */ protected array $guzzleClientConfig = AmplitudeConfig::DEFAULTS['guzzleClientConfig']; + protected ?LoggerInterface $logger = AmplitudeConfig::DEFAULTS['logger']; + protected int $logLevel = AmplitudeConfig::DEFAULTS['logLevel']; public function __construct() { @@ -66,10 +69,20 @@ public function guzzleClientConfig(array $guzzleClientConfig): AmplitudeConfigBu return $this; } - /** - * @phpstan-ignore-next-line - */ - public function build() + public function logger(LoggerInterface $logger): AmplitudeConfigBuilder + { + $this->logger = $logger; + return $this; + } + + public function logLevel(int $logLevel): AmplitudeConfigBuilder + { + $this->logLevel = $logLevel; + return $this; + } + + + public function build(): AmplitudeConfig { if (!$this->serverUrl) { if ($this->useBatch) { @@ -85,7 +98,9 @@ public function build() $this->serverUrl, $this->useBatch, $this->httpClient, - $this->guzzleClientConfig + $this->guzzleClientConfig, + $this->logger, + $this->logLevel ); } } diff --git a/src/Amplitude/Event.php b/src/Amplitude/Event.php index a2865ac..56c18a4 100644 --- a/src/Amplitude/Event.php +++ b/src/Amplitude/Event.php @@ -2,6 +2,8 @@ namespace AmplitudeExperiment\Amplitude; +use RuntimeException; + class Event { public ?string $eventType = null; @@ -35,4 +37,16 @@ public function toArray(): array 'device_id' => $this->deviceId, 'insert_id' => $this->insertId,]); } + + /** + * @throws RuntimeException + */ + public function toJSONString(): string + { + $jsonString = json_encode($this->toArray()); + if (!$jsonString) { + throw new RuntimeException('Failed to encode Event to JSON string'); + } + return $jsonString; + } } diff --git a/src/Assignment/Assignment.php b/src/Assignment/Assignment.php index 69baed3..8b89fec 100644 --- a/src/Assignment/Assignment.php +++ b/src/Assignment/Assignment.php @@ -2,9 +2,18 @@ namespace AmplitudeExperiment\Assignment; +use AmplitudeExperiment\Amplitude\Event; use AmplitudeExperiment\User; use AmplitudeExperiment\Variant; +use RuntimeException; +use function AmplitudeExperiment\hashCode; +require_once __DIR__ . '/../Util.php'; +require_once __DIR__ . '/AssignmentService.php'; + +/** + * Event class for tracking assignments to Amplitude Experiment. + */ class Assignment { public User $user; @@ -13,15 +22,19 @@ class Assignment */ public array $variants; public int $timestamp; + public string $apiKey; + public int $minIdLength; /** * @param array $variants */ - public function __construct(User $user, array $variants) + public function __construct(User $user, array $variants, string $apiKey = '', int $minIdLength = AssignmentConfig::DEFAULTS['minIdLength']) { $this->user = $user; $this->variants = $variants; - $this->timestamp = (int) floor(microtime(true) * 1000); + $this->timestamp = (int)floor(microtime(true) * 1000); + $this->apiKey = $apiKey; + $this->minIdLength = $minIdLength; } public function canonicalize(): string @@ -38,4 +51,86 @@ public function canonicalize(): string } return $canonical; } + + /** + * Convert an Assignment to an Amplitude event + */ + public function toEvent(): Event + { + $event = new Event('[Experiment] Assignment'); + $event->userId = $this->user->userId; + $event->deviceId = $this->user->deviceId; + $event->eventProperties = []; + $event->userProperties = []; + + $set = []; + $unset = []; + foreach ($this->variants as $flagKey => $variant) { + if (!$variant->key) { + 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] {$flagKey}"] = '-'; + } else { + $set["[Experiment] {$flagKey}"] = $variant->key; + } + } + + $event->userProperties['$set'] = $set; + $event->userProperties['$unset'] = $unset; + + $hash = hashCode($this->canonicalize()); + + $event->insertId = "{$event->userId} {$event->deviceId} {$hash} " . + floor($this->timestamp / DAY_MILLIS); + + return $event; + } + + + /** + * Convert an Assignment to an array representation of an Amplitude event + * @return array + */ + public function toArray(): array + { + return $this->toEvent()->toArray(); + } + + /** + * Convert an Assignment to an Amplitude event JSON string + * @throws RuntimeException + */ + public function toJSONString(): string + { + $jsonString = json_encode($this->toArray()); + if (!$jsonString) { + throw new RuntimeException('Failed to encode Assignment to JSON string'); + } + return $jsonString; + } + + /** + * Convert an Assignment to a JSON string that can be used as a payload to the Amplitude event upload API + * @throws RuntimeException + */ + public function toJSONPayload(): string + { + $payload = ["api_key" => $this->apiKey, "events" => [$this->toEvent()], "options" => ["min_id_length" => $this->minIdLength]]; + $jsonString = json_encode($payload); + if (!$jsonString) { + throw new RuntimeException('Failed to encode Assignment to JSON payload'); + } + return $jsonString; + } } diff --git a/src/Assignment/AssignmentConfig.php b/src/Assignment/AssignmentConfig.php index fc8931e..5242355 100644 --- a/src/Assignment/AssignmentConfig.php +++ b/src/Assignment/AssignmentConfig.php @@ -2,21 +2,19 @@ namespace AmplitudeExperiment\Assignment; -use AmplitudeExperiment\Amplitude\AmplitudeConfig; - /** * Configuration options for assignment tracking. This is an object that can be created using - * a {@link AssignmentConfigBuilder}, which also sets options for {@link AmplitudeConfig}. Example usage: + * a {@link AssignmentConfigBuilder}. Example usage: * * ``` - * AssignmentConfigBuilder::builder('api-key')->minIdLength(10)->build(); + * AssignmentConfigBuilder::builder('api-key')->cacheCapacity(1000)->build(); * ``` */ class AssignmentConfig { /** - * The Amplitude Analytics API key. + * The Amplitude Project API key. */ public string $apiKey; /** @@ -24,24 +22,30 @@ class AssignmentConfig */ public int $cacheCapacity; /** - * Configuration options for the underlying {@link Amplitude} client. This is created when - * calling {@link AssignmentConfigBuilder::build()} and does not need to be explicitly set. + * The provider for tracking assignment events to Amplitude + */ + public AssignmentTrackingProvider $assignmentTrackingProvider; + /** + * The minimum length of the id field in events. Default to 5. This is set in {@link AmplitudeConfig} if the + * {@link DefaultAssignmentTrackingProvider} is used, and does not need to be set here. */ - public AmplitudeConfig $amplitudeConfig; + public int $minIdLength; const DEFAULTS = [ 'cacheCapacity' => 65536, + 'minIdLength' => 5, ]; - public function __construct(string $apiKey, int $cacheCapacity, AmplitudeConfig $amplitudeConfig) + public function __construct(string $apiKey, int $cacheCapacity, AssignmentTrackingProvider $assignmentTrackingProvider, int $minIdLength) { $this->apiKey = $apiKey; $this->cacheCapacity = $cacheCapacity; - $this->amplitudeConfig = $amplitudeConfig; + $this->assignmentTrackingProvider = $assignmentTrackingProvider; + $this->minIdLength = $minIdLength; } - public static function builder(string $apiKey): AssignmentConfigBuilder + public static function builder(string $apiKey, AssignmentTrackingProvider $assignmentTrackingProvider): AssignmentConfigBuilder { - return new AssignmentConfigBuilder($apiKey); + return new AssignmentConfigBuilder($apiKey, $assignmentTrackingProvider); } } diff --git a/src/Assignment/AssignmentConfigBuilder.php b/src/Assignment/AssignmentConfigBuilder.php index 1882639..24d8bd9 100644 --- a/src/Assignment/AssignmentConfigBuilder.php +++ b/src/Assignment/AssignmentConfigBuilder.php @@ -2,21 +2,17 @@ namespace AmplitudeExperiment\Assignment; -use AmplitudeExperiment\Amplitude\AmplitudeConfig; -use AmplitudeExperiment\Amplitude\AmplitudeConfigBuilder; - -/** - * Extends AmplitudeConfigBuilder to allow configuration {@link AmplitudeConfig} of underlying {@link Amplitude} client. - */ -class AssignmentConfigBuilder extends AmplitudeConfigBuilder +class AssignmentConfigBuilder { - protected string $apiKey; protected int $cacheCapacity = AssignmentConfig::DEFAULTS['cacheCapacity']; + protected AssignmentTrackingProvider $assignmentTrackingProvider; + protected string $apiKey; + protected int $minIdLength = AssignmentConfig::DEFAULTS['minIdLength']; - public function __construct(string $apiKey) + public function __construct(string $apiKey, AssignmentTrackingProvider $assignmentTrackingProvider) { - parent::__construct(); $this->apiKey = $apiKey; + $this->assignmentTrackingProvider = $assignmentTrackingProvider; } public function cacheCapacity(int $cacheCapacity): AssignmentConfigBuilder @@ -25,15 +21,19 @@ public function cacheCapacity(int $cacheCapacity): AssignmentConfigBuilder return $this; } - /** - * @phpstan-ignore-next-line - */ - public function build() + public function minIdLength(int $minIdLength): AssignmentConfigBuilder + { + $this->minIdLength = $minIdLength; + return $this; + } + + public function build(): AssignmentConfig { return new AssignmentConfig( $this->apiKey, $this->cacheCapacity, - parent::build() + $this->assignmentTrackingProvider, + $this->minIdLength ); } } diff --git a/src/Assignment/AssignmentService.php b/src/Assignment/AssignmentService.php index 1a6b257..d73a927 100644 --- a/src/Assignment/AssignmentService.php +++ b/src/Assignment/AssignmentService.php @@ -2,9 +2,8 @@ namespace AmplitudeExperiment\Assignment; -use AmplitudeExperiment\Amplitude\Amplitude; -use AmplitudeExperiment\Amplitude\Event; -use function AmplitudeExperiment\hashCode; +use AmplitudeExperiment\User; +use AmplitudeExperiment\Variant; require_once __DIR__ . '/../Util.php'; @@ -13,61 +12,31 @@ class AssignmentService { - private Amplitude $amplitude; + private AssignmentTrackingProvider $assignmentTrackingProvider; private AssignmentFilter $assignmentFilter; + private string $apiKey; + private int $minIdLength; - public function __construct(Amplitude $amplitude, AssignmentFilter $assignmentFilter) + public function __construct(AssignmentTrackingProvider $assignmentTrackingProvider, AssignmentFilter $assignmentFilter, string $apiKey = '', int $minIdLength = AssignmentConfig::DEFAULTS['minIdLength']) { - $this->amplitude = $amplitude; + $this->assignmentTrackingProvider = $assignmentTrackingProvider; $this->assignmentFilter = $assignmentFilter; + $this->apiKey = $apiKey; + $this->minIdLength = $minIdLength; } public function track(Assignment $assignment): void { if ($this->assignmentFilter->shouldTrack($assignment)) { - $this->amplitude->logEvent($this->toEvent($assignment)); + $this->assignmentTrackingProvider->track($assignment); } } - public static function toEvent(Assignment $assignment): Event + /** + * @param array $variants + */ + public function createAssignment(User $user, array $variants): Assignment { - $event = new Event('[Experiment] Assignment'); - $event->userId = $assignment->user->userId; - $event->deviceId = $assignment->user->deviceId; - $event->eventProperties = []; - $event->userProperties = []; - - $set = []; - $unset = []; - foreach ($assignment->variants as $flagKey => $variant) { - if (!$variant->key) { - 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] {$flagKey}"] = '-'; - } else { - $set["[Experiment] {$flagKey}"] = $variant->key; - } - } - - $event->userProperties['$set'] = $set; - $event->userProperties['$unset'] = $unset; - - $hash = hashCode($assignment->canonicalize()); - - $event->insertId = "{$event->userId} {$event->deviceId} {$hash} " . - floor($assignment->timestamp / DAY_MILLIS); - - return $event; + return new Assignment($user, $variants, $this->apiKey, $this->minIdLength); } } diff --git a/src/Assignment/AssignmentTrackingProvider.php b/src/Assignment/AssignmentTrackingProvider.php new file mode 100644 index 0000000..2a011b2 --- /dev/null +++ b/src/Assignment/AssignmentTrackingProvider.php @@ -0,0 +1,29 @@ +amplitude = $amplitude; + } + + public function track(Assignment $assignment): void + { + $this->amplitude->logEvent($assignment->toEvent()); + } +} diff --git a/src/Local/LocalEvaluationClient.php b/src/Local/LocalEvaluationClient.php index 8065f36..ebcd0f8 100644 --- a/src/Local/LocalEvaluationClient.php +++ b/src/Local/LocalEvaluationClient.php @@ -2,8 +2,6 @@ namespace AmplitudeExperiment\Local; -use AmplitudeExperiment\Amplitude\Amplitude; -use AmplitudeExperiment\Assignment\Assignment; use AmplitudeExperiment\Assignment\AssignmentConfig; use AmplitudeExperiment\Assignment\AssignmentFilter; use AmplitudeExperiment\Assignment\AssignmentService; @@ -75,7 +73,7 @@ public function evaluate(User $user, array $flagKeys = []): array $results = array_map('AmplitudeExperiment\Variant::convertEvaluationVariantToVariant', $this->evaluation->evaluate($user->toEvaluationContext(), $flags)); $this->logger->debug('[Experiment] Evaluate - variants:' . json_encode($results)); if ($this->assignmentService) { - $this->assignmentService->track(new Assignment($user, $results)); + $this->assignmentService->track($this->assignmentService->createAssignment($user, $results)); } return $results; } @@ -93,10 +91,10 @@ private function initializeAssignmentService(?AssignmentConfig $config): void { if ($config) { $this->assignmentService = new AssignmentService( - new Amplitude($config->apiKey, - $this->logger, - $config->amplitudeConfig), - new AssignmentFilter($config->cacheCapacity)); + $config->assignmentTrackingProvider, + new AssignmentFilter($config->cacheCapacity), + $config->apiKey, + $config->minIdLength); } } } diff --git a/tests/Amplitude/AmplitudeTest.php b/tests/Amplitude/AmplitudeTest.php index 5152d4a..27f0b84 100644 --- a/tests/Amplitude/AmplitudeTest.php +++ b/tests/Amplitude/AmplitudeTest.php @@ -18,14 +18,8 @@ class AmplitudeTest extends TestCase { - private LoggerInterface $logger; const API_KEY = 'test'; - public function setUp(): void - { - $this->logger = new InternalLogger(new DefaultLogger(), LogLevel::DEBUG); - } - public function testAmplitudeConfigServerUrl() { $config = AmplitudeConfig::builder() @@ -53,7 +47,7 @@ public function testAmplitudeConfigServerUrl() public function testEmptyQueueAfterFlushSuccess() { - $client = new MockAmplitude(self::API_KEY, $this->logger); + $client = new MockAmplitude(self::API_KEY); $mock = new MockHandler([ new Response(200, ['X-Foo' => 'Bar']), ]); @@ -79,7 +73,7 @@ public function testFlushAfterMaxQueue() $config = AmplitudeConfig::builder() ->flushQueueSize(3) ->build(); - $client = new MockAmplitude(self::API_KEY, $this->logger, $config); + $client = new MockAmplitude(self::API_KEY, $config); $mockHandler = new MockHandler([ function (RequestInterface $request, array $options) use (&$requestCounter) { $requestCounter++; @@ -110,7 +104,7 @@ public function testBackoffRetriesToFailure() // Initialize the request counter $requestCounter = 0; $config = AmplitudeConfig::builder()->build(); - $client = new MockAmplitude(self::API_KEY, $this->logger, $config); + $client = new MockAmplitude(self::API_KEY, $config); // Set up the mock handler with request counter incrementation logic $mockHandler = new MockHandler(array_fill(1, 5, function (RequestInterface $request, array $options) use (&$requestCounter) { @@ -138,7 +132,7 @@ public function testBackoffRetriesThenSuccess() // Initialize the request counter $requestCounter = 0; $config = AmplitudeConfig::builder()->build(); - $client = new MockAmplitude(self::API_KEY, $this->logger, $config); + $client = new MockAmplitude(self::API_KEY, $config); // Set up the mock handler with request counter incrementation logic $mockHandler = new MockHandler(array_fill(1, 2, function (RequestInterface $request, array $options) use (&$requestCounter) { diff --git a/tests/Amplitude/MockAmplitude.php b/tests/Amplitude/MockAmplitude.php index dfa8e07..6a0d6e3 100644 --- a/tests/Amplitude/MockAmplitude.php +++ b/tests/Amplitude/MockAmplitude.php @@ -9,9 +9,9 @@ class MockAmplitude extends Amplitude { - public function __construct(string $apiKey, LoggerInterface $logger, AmplitudeConfig $config = null) + public function __construct(string $apiKey, AmplitudeConfig $config = null) { - parent::__construct($apiKey, $logger, $config); + parent::__construct($apiKey, $config); } public function setHttpClient(HttpClientInterface $httpClient) { $this->httpClient = $httpClient; diff --git a/tests/Assignment/AssignmentServiceTest.php b/tests/Assignment/AssignmentServiceTest.php index 7a33c4b..d513b80 100644 --- a/tests/Assignment/AssignmentServiceTest.php +++ b/tests/Assignment/AssignmentServiceTest.php @@ -6,96 +6,25 @@ use AmplitudeExperiment\Assignment\Assignment; use AmplitudeExperiment\Assignment\AssignmentFilter; use AmplitudeExperiment\Assignment\AssignmentService; -use AmplitudeExperiment\Logger\DefaultLogger; -use AmplitudeExperiment\Logger\InternalLogger; -use AmplitudeExperiment\Logger\LogLevel; +use AmplitudeExperiment\Assignment\DefaultAssignmentTrackingProvider; use AmplitudeExperiment\User; use AmplitudeExperiment\Variant; use PHPUnit\Framework\TestCase; -use const AmplitudeExperiment\Assignment\DAY_MILLIS; -use function AmplitudeExperiment\hashCode; - -require_once __DIR__ . '/../../src/Util.php'; class AssignmentServiceTest extends TestCase { - public function testAssignmentToEventAsExpected() - { - $user = User::builder()->userId('user')->deviceId('device')->build(); - $results = [ - '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() - ]; - - $assignment = new Assignment($user, $results); - $event = AssignmentService::toEvent($assignment); - - $this->assertEquals($user->userId, $event->userId); - $this->assertEquals($user->deviceId, $event->deviceId); - $this->assertEquals('[Experiment] Assignment', $event->eventType); - - $eventProperties = $event->eventProperties; - $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; - $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 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(['', new InternalLogger(new DefaultLogger(), LogLevel::INFO)]) + ->setConstructorArgs(['']) ->onlyMethods(['logEvent']) ->getMock(); $results = [ 'flag-key-1' => new Variant('on') ]; - $service = new AssignmentService($mockAmp, $assignmentFilter); + $assigmentTrackingProvider = new DefaultAssignmentTrackingProvider($mockAmp); + $service = new AssignmentService($assigmentTrackingProvider, $assignmentFilter); $mockAmp->expects($this->once())->method('logEvent'); $service->track(new Assignment(User::builder()->userId('user')->build(), $results)); } diff --git a/tests/Assignment/AssignmentTest.php b/tests/Assignment/AssignmentTest.php new file mode 100644 index 0000000..92ae678 --- /dev/null +++ b/tests/Assignment/AssignmentTest.php @@ -0,0 +1,82 @@ +userId('user')->deviceId('device')->build(); + $results = [ + '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() + ]; + + $assignment = new Assignment($user, $results, 'apiKey', 10); + $event = $assignment->toEvent(); + + $this->assertEquals($user->userId, $event->userId); + $this->assertEquals($user->deviceId, $event->deviceId); + $this->assertEquals('[Experiment] Assignment', $event->eventType); + + $eventProperties = $event->eventProperties; + $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; + $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 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); + + $expectedPayload = json_encode(["api_key" => 'apiKey', "events" => [$event], "options" => ["min_id_length" => 10]]); + $this->assertEquals($expectedPayload, $assignment->toJSONPayload()); + } +}