Skip to content

Commit

Permalink
feat: Automatic assignment tracking (#8)
Browse files Browse the repository at this point in the history
  • Loading branch information
tyiuhc authored Nov 22, 2023
1 parent c50c9d6 commit 4520bfc
Show file tree
Hide file tree
Showing 23 changed files with 1,102 additions and 38 deletions.
86 changes: 86 additions & 0 deletions src/Amplitude/Amplitude.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<?php

namespace AmplitudeExperiment\Amplitude;

use AmplitudeExperiment\Backoff;
use GuzzleHttp\Client;
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;
protected array $queue = [];
protected Client $httpClient;
private Logger $logger;
private ?AmplitudeConfig $config;

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();
}

public function flush(): PromiseInterface
{
$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(
function () use ($payload) {
return $this->post($this->config->serverUrl, $payload)->then(
function () {
$this->queue = [];
}
);
},
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();
}
}

/**
* Flush the queue when the client is destructed.
*/
public function __destruct()
{
if (count($this->queue) > 0) {
$this->flush()->wait();
}
}

private function post(string $url, array $payload): PromiseInterface
{
// Using sendAsync to make an asynchronous request
$promise = $this->httpClient->postAsync($url, [
'json' => $payload,
]);

return $promise->then(
function ($response) use ($payload) {
// Process the successful response if needed
$this->logger->debug("[Amplitude] Event sent successfully: " . json_encode($payload));
},
function (\Exception $exception) use ($payload) {
// Handle the exception for async request
$this->logger->error('[Amplitude] Failed to send event: ' . json_encode($payload) . ', ' . $exception->getMessage());
throw $exception;
}
);
}
}
78 changes: 78 additions & 0 deletions src/Amplitude/AmplitudeConfig.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<?php

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 = [
'serverZone' => '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,
'minIdLength' => 5,
'flushQueueSize' => 200,
'flushMaxRetries' => 12,
];

public function __construct(
int $flushQueueSize,
int $flushMaxRetries,
int $minIdLength,
string $serverZone,
string $serverUrl,
bool $useBatch
)
{
$this->flushQueueSize = $flushQueueSize;
$this->flushMaxRetries = $flushMaxRetries;
$this->minIdLength = $minIdLength;
$this->serverZone = $serverZone;
$this->serverUrl = $serverUrl;
$this->useBatch = $useBatch;
}

public static function builder(): AmplitudeConfigBuilder
{
return new AmplitudeConfigBuilder();
}
}
72 changes: 72 additions & 0 deletions src/Amplitude/AmplitudeConfigBuilder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<?php

namespace AmplitudeExperiment\Amplitude;

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'];

public function __construct()
{
}

public function flushQueueSize(int $flushQueueSize): AmplitudeConfigBuilder
{
$this->flushQueueSize = $flushQueueSize;
return $this;
}

public function flushMaxRetries(int $flushMaxRetries): AmplitudeConfigBuilder
{
$this->flushMaxRetries = $flushMaxRetries;
return $this;
}

public function minIdLength(int $minIdLength): AmplitudeConfigBuilder
{
$this->minIdLength = $minIdLength;
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()
{
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->minIdLength,
$this->serverZone,
$this->serverUrl,
$this->useBatch
);
}
}
29 changes: 29 additions & 0 deletions src/Amplitude/Event.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

namespace AmplitudeExperiment\Amplitude;

class Event
{
public ?string $eventType = null;
public ?array $eventProperties = null;
public ?array $userProperties = null;
public ?string $userId = null;
public ?string $deviceId = null;
public ?string $insertId = null;

public function __construct(string $eventType)
{
$this->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,]);
}
}
34 changes: 34 additions & 0 deletions src/Assignment/Assignment.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

namespace AmplitudeExperiment\Assignment;

use AmplitudeExperiment\User;

class Assignment
{
public User $user;
public array $variants;
public int $timestamp;

public function __construct(User $user, array $variants)
{
$this->user = $user;
$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->variants);
sort($sortedKeys);
foreach ($sortedKeys as $key) {
$variant = $this->variants[$key];
if (!$variant->key) {
continue;
}
$canonical .= trim($key) . ' ' . trim($variant->key) . ' ';
}
return $canonical;
}
}
35 changes: 35 additions & 0 deletions src/Assignment/AssignmentConfig.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

namespace AmplitudeExperiment\Assignment;

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;
public int $cacheCapacity;
public AmplitudeConfig $amplitudeConfig;

const DEFAULTS = [
'cacheCapacity' => 65536,
];

public function __construct(string $apiKey, int $cacheCapacity, AmplitudeConfig $amplitudeConfig)
{
$this->apiKey = $apiKey;
$this->cacheCapacity = $cacheCapacity;
$this->amplitudeConfig = $amplitudeConfig;
}

public static function builder(string $apiKey): AssignmentConfigBuilder
{
return new AssignmentConfigBuilder($apiKey);
}
}
35 changes: 35 additions & 0 deletions src/Assignment/AssignmentConfigBuilder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

namespace AmplitudeExperiment\Assignment;

use AmplitudeExperiment\Amplitude\AmplitudeConfigBuilder;

/**
* Extends AmplitudeConfigBuilder to allow configuration {@link AmplitudeConfig} of underlying {@link Amplitude} client.
*/

class AssignmentConfigBuilder extends AmplitudeConfigBuilder
{
protected string $apiKey;
protected int $cacheCapacity = AssignmentConfig::DEFAULTS['cacheCapacity'];
public function __construct(string $apiKey)
{
parent::__construct();
$this->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()
);
}
}
Loading

0 comments on commit 4520bfc

Please sign in to comment.