Skip to content

Commit

Permalink
feat: AssignmentTrackingProvider used to track local evaluation assig…
Browse files Browse the repository at this point in the history
…nment events (#14)
  • Loading branch information
tyiuhc authored Feb 15, 2024
1 parent e6c0b30 commit 62a6696
Show file tree
Hide file tree
Showing 15 changed files with 351 additions and 181 deletions.
9 changes: 7 additions & 2 deletions src/Amplitude/Amplitude.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -12,6 +14,9 @@
*/
class Amplitude
{
/**
* The Amplitude Project API key.
*/
private string $apiKey;
/**
* @var array<array<string,mixed>>
Expand All @@ -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);
}

Expand Down
24 changes: 19 additions & 5 deletions src/Amplitude/AmplitudeConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -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}.
Expand All @@ -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;
/**
Expand All @@ -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',
Expand All @@ -58,9 +68,10 @@ class AmplitudeConfig
'useBatch' => false,
'minIdLength' => 5,
'flushQueueSize' => 200,
'flushMaxRetries' => 12,
'httpClient' => null,
'guzzleClientConfig' => []
'guzzleClientConfig' => [],
'logger' => null,
'logLevel' => LogLevel::ERROR,
];

/**
Expand All @@ -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;
Expand All @@ -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
Expand Down
25 changes: 20 additions & 5 deletions src/Amplitude/AmplitudeConfigBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace AmplitudeExperiment\Amplitude;

use AmplitudeExperiment\Http\HttpClientInterface;
use Psr\Log\LoggerInterface;

class AmplitudeConfigBuilder
{
Expand All @@ -16,6 +17,8 @@ class AmplitudeConfigBuilder
* @var array<string, mixed>
*/
protected array $guzzleClientConfig = AmplitudeConfig::DEFAULTS['guzzleClientConfig'];
protected ?LoggerInterface $logger = AmplitudeConfig::DEFAULTS['logger'];
protected int $logLevel = AmplitudeConfig::DEFAULTS['logLevel'];

public function __construct()
{
Expand Down Expand Up @@ -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) {
Expand All @@ -85,7 +98,9 @@ public function build()
$this->serverUrl,
$this->useBatch,
$this->httpClient,
$this->guzzleClientConfig
$this->guzzleClientConfig,
$this->logger,
$this->logLevel
);
}
}
14 changes: 14 additions & 0 deletions src/Amplitude/Event.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

namespace AmplitudeExperiment\Amplitude;

use RuntimeException;

class Event
{
public ?string $eventType = null;
Expand Down Expand Up @@ -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;
}
}
99 changes: 97 additions & 2 deletions src/Assignment/Assignment.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -13,15 +22,19 @@ class Assignment
*/
public array $variants;
public int $timestamp;
public string $apiKey;
public int $minIdLength;

/**
* @param array<string, Variant> $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
Expand All @@ -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<string, mixed>
*/
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;
}
}
28 changes: 16 additions & 12 deletions src/Assignment/AssignmentConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,46 +2,50 @@

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;
/**
* The maximum number of assignments stored in the assignment cache
*/
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);
}
}
Loading

0 comments on commit 62a6696

Please sign in to comment.