Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: AssignmentTrackingProvider used to track local evaluation assignment events #14

Merged
merged 8 commits into from
Feb 15, 2024
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) {
tyiuhc marked this conversation as resolved.
Show resolved Hide resolved
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
Loading