-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Automatic assignment tracking (#8)
- Loading branch information
Showing
23 changed files
with
1,102 additions
and
38 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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,]); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
); | ||
} | ||
} |
Oops, something went wrong.