Skip to content

Commit 4520bfc

Browse files
authored
feat: Automatic assignment tracking (#8)
1 parent c50c9d6 commit 4520bfc

23 files changed

+1102
-38
lines changed

src/Amplitude/Amplitude.php

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
<?php
2+
3+
namespace AmplitudeExperiment\Amplitude;
4+
5+
use AmplitudeExperiment\Backoff;
6+
use GuzzleHttp\Client;
7+
use GuzzleHttp\Promise\PromiseInterface;
8+
use Monolog\Logger;
9+
use function AmplitudeExperiment\initializeLogger;
10+
11+
require_once __DIR__ . '/../Util.php';
12+
13+
/**
14+
* Amplitude client for sending events to Amplitude.
15+
*/
16+
class Amplitude
17+
{
18+
private string $apiKey;
19+
protected array $queue = [];
20+
protected Client $httpClient;
21+
private Logger $logger;
22+
private ?AmplitudeConfig $config;
23+
24+
public function __construct(string $apiKey, bool $debug, AmplitudeConfig $config = null)
25+
{
26+
$this->apiKey = $apiKey;
27+
$this->httpClient = new Client();
28+
$this->logger = initializeLogger($debug);
29+
$this->config = $config ?? AmplitudeConfig::builder()->build();
30+
}
31+
32+
public function flush(): PromiseInterface
33+
{
34+
$payload = ["api_key" => $this->apiKey, "events" => $this->queue, "options" => ["min_id_length" => $this->config->minIdLength]];
35+
36+
// Fetch initial flag configs and await the result.
37+
return Backoff::doWithBackoff(
38+
function () use ($payload) {
39+
return $this->post($this->config->serverUrl, $payload)->then(
40+
function () {
41+
$this->queue = [];
42+
}
43+
);
44+
},
45+
new Backoff($this->config->flushMaxRetries, 1, 1, 1)
46+
);
47+
}
48+
49+
public function logEvent(Event $event)
50+
{
51+
$this->queue[] = $event->toArray();
52+
if (count($this->queue) >= $this->config->flushQueueSize) {
53+
$this->flush()->wait();
54+
}
55+
}
56+
57+
/**
58+
* Flush the queue when the client is destructed.
59+
*/
60+
public function __destruct()
61+
{
62+
if (count($this->queue) > 0) {
63+
$this->flush()->wait();
64+
}
65+
}
66+
67+
private function post(string $url, array $payload): PromiseInterface
68+
{
69+
// Using sendAsync to make an asynchronous request
70+
$promise = $this->httpClient->postAsync($url, [
71+
'json' => $payload,
72+
]);
73+
74+
return $promise->then(
75+
function ($response) use ($payload) {
76+
// Process the successful response if needed
77+
$this->logger->debug("[Amplitude] Event sent successfully: " . json_encode($payload));
78+
},
79+
function (\Exception $exception) use ($payload) {
80+
// Handle the exception for async request
81+
$this->logger->error('[Amplitude] Failed to send event: ' . json_encode($payload) . ', ' . $exception->getMessage());
82+
throw $exception;
83+
}
84+
);
85+
}
86+
}

src/Amplitude/AmplitudeConfig.php

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
<?php
2+
3+
namespace AmplitudeExperiment\Amplitude;
4+
5+
/**
6+
* Configuration options for Amplitude. This is an object that can be created using
7+
* a {@link AmplitudeConfigBuilder}. Example usage:
8+
*
9+
* AmplitudeConfigBuilder::builder()->serverZone("EU")->build();
10+
*/
11+
class AmplitudeConfig
12+
{
13+
/**
14+
* The events buffered in memory will flush when exceed flushQueueSize
15+
* Must be positive.
16+
*/
17+
public int $flushQueueSize;
18+
/**
19+
* The maximum retry attempts for an event when receiving error response.
20+
*/
21+
public int $flushMaxRetries;
22+
/**
23+
* The minimum length of user_id and device_id for events. Default to 5.
24+
*/
25+
public int $minIdLength;
26+
/**
27+
* The server zone of project. Default to 'US'. Support 'EU'.
28+
*/
29+
public string $serverZone;
30+
/**
31+
* API endpoint url. Default to None. Auto selected by configured server_zone
32+
*/
33+
public string $serverUrl;
34+
/**
35+
* True to use batch API endpoint, False to use HTTP V2 API endpoint.
36+
*/
37+
public string $useBatch;
38+
39+
const DEFAULTS = [
40+
'serverZone' => 'US',
41+
'serverUrl' => [
42+
'EU' => [
43+
'batch' => 'https://api.eu.amplitude.com/batch',
44+
'v2' => 'https://api.eu.amplitude.com/2/httpapi'
45+
],
46+
'US' => [
47+
'batch' => 'https://api2.amplitude.com/batch',
48+
'v2' => 'https://api2.amplitude.com/2/httpapi'
49+
]
50+
],
51+
'useBatch' => false,
52+
'minIdLength' => 5,
53+
'flushQueueSize' => 200,
54+
'flushMaxRetries' => 12,
55+
];
56+
57+
public function __construct(
58+
int $flushQueueSize,
59+
int $flushMaxRetries,
60+
int $minIdLength,
61+
string $serverZone,
62+
string $serverUrl,
63+
bool $useBatch
64+
)
65+
{
66+
$this->flushQueueSize = $flushQueueSize;
67+
$this->flushMaxRetries = $flushMaxRetries;
68+
$this->minIdLength = $minIdLength;
69+
$this->serverZone = $serverZone;
70+
$this->serverUrl = $serverUrl;
71+
$this->useBatch = $useBatch;
72+
}
73+
74+
public static function builder(): AmplitudeConfigBuilder
75+
{
76+
return new AmplitudeConfigBuilder();
77+
}
78+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
<?php
2+
3+
namespace AmplitudeExperiment\Amplitude;
4+
5+
class AmplitudeConfigBuilder
6+
{
7+
protected int $flushQueueSize = AmplitudeConfig::DEFAULTS['flushQueueSize'];
8+
protected int $flushMaxRetries = AmplitudeConfig::DEFAULTS['flushMaxRetries'];
9+
protected int $minIdLength = AmplitudeConfig::DEFAULTS['minIdLength'];
10+
protected string $serverZone = AmplitudeConfig::DEFAULTS['serverZone'];
11+
protected ?string $serverUrl = null;
12+
protected bool $useBatch = AmplitudeConfig::DEFAULTS['useBatch'];
13+
14+
public function __construct()
15+
{
16+
}
17+
18+
public function flushQueueSize(int $flushQueueSize): AmplitudeConfigBuilder
19+
{
20+
$this->flushQueueSize = $flushQueueSize;
21+
return $this;
22+
}
23+
24+
public function flushMaxRetries(int $flushMaxRetries): AmplitudeConfigBuilder
25+
{
26+
$this->flushMaxRetries = $flushMaxRetries;
27+
return $this;
28+
}
29+
30+
public function minIdLength(int $minIdLength): AmplitudeConfigBuilder
31+
{
32+
$this->minIdLength = $minIdLength;
33+
return $this;
34+
}
35+
36+
public function serverZone(string $serverZone): AmplitudeConfigBuilder
37+
{
38+
$this->serverZone = $serverZone;
39+
return $this;
40+
}
41+
42+
public function serverUrl(string $serverUrl): AmplitudeConfigBuilder
43+
{
44+
$this->serverUrl = $serverUrl;
45+
return $this;
46+
}
47+
48+
public function useBatch(bool $useBatch): AmplitudeConfigBuilder
49+
{
50+
$this->useBatch = $useBatch;
51+
return $this;
52+
}
53+
54+
public function build()
55+
{
56+
if (!$this->serverUrl) {
57+
if ($this->useBatch) {
58+
$this->serverUrl = AmplitudeConfig::DEFAULTS['serverUrl'][$this->serverZone]['batch'];
59+
} else {
60+
$this->serverUrl = AmplitudeConfig::DEFAULTS['serverUrl'][$this->serverZone]['v2'];
61+
}
62+
}
63+
return new AmplitudeConfig(
64+
$this->flushQueueSize,
65+
$this->flushMaxRetries,
66+
$this->minIdLength,
67+
$this->serverZone,
68+
$this->serverUrl,
69+
$this->useBatch
70+
);
71+
}
72+
}

src/Amplitude/Event.php

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
namespace AmplitudeExperiment\Amplitude;
4+
5+
class Event
6+
{
7+
public ?string $eventType = null;
8+
public ?array $eventProperties = null;
9+
public ?array $userProperties = null;
10+
public ?string $userId = null;
11+
public ?string $deviceId = null;
12+
public ?string $insertId = null;
13+
14+
public function __construct(string $eventType)
15+
{
16+
$this->eventType = $eventType;
17+
}
18+
19+
public function toArray(): array
20+
{
21+
return array_filter([
22+
'event_type' => $this->eventType,
23+
'event_properties' => $this->eventProperties,
24+
'user_properties' => $this->userProperties,
25+
'user_id' => $this->userId,
26+
'device_id' => $this->deviceId,
27+
'insert_id' => $this->insertId,]);
28+
}
29+
}

src/Assignment/Assignment.php

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
namespace AmplitudeExperiment\Assignment;
4+
5+
use AmplitudeExperiment\User;
6+
7+
class Assignment
8+
{
9+
public User $user;
10+
public array $variants;
11+
public int $timestamp;
12+
13+
public function __construct(User $user, array $variants)
14+
{
15+
$this->user = $user;
16+
$this->variants = $variants;
17+
$this->timestamp = floor(microtime(true) * 1000);
18+
}
19+
20+
public function canonicalize(): string
21+
{
22+
$canonical = trim("{$this->user->userId} {$this->user->deviceId}") . ' ';
23+
$sortedKeys = array_keys($this->variants);
24+
sort($sortedKeys);
25+
foreach ($sortedKeys as $key) {
26+
$variant = $this->variants[$key];
27+
if (!$variant->key) {
28+
continue;
29+
}
30+
$canonical .= trim($key) . ' ' . trim($variant->key) . ' ';
31+
}
32+
return $canonical;
33+
}
34+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
3+
namespace AmplitudeExperiment\Assignment;
4+
5+
use AmplitudeExperiment\Amplitude\AmplitudeConfig;
6+
7+
/**
8+
* Configuration options for assignment tracking. This is an object that can be created using
9+
* a {@link AssignmentConfigBuilder}. Example usage:
10+
*
11+
* AssignmentConfigBuilder::builder('api-key')->build()
12+
*/
13+
14+
class AssignmentConfig
15+
{
16+
public string $apiKey;
17+
public int $cacheCapacity;
18+
public AmplitudeConfig $amplitudeConfig;
19+
20+
const DEFAULTS = [
21+
'cacheCapacity' => 65536,
22+
];
23+
24+
public function __construct(string $apiKey, int $cacheCapacity, AmplitudeConfig $amplitudeConfig)
25+
{
26+
$this->apiKey = $apiKey;
27+
$this->cacheCapacity = $cacheCapacity;
28+
$this->amplitudeConfig = $amplitudeConfig;
29+
}
30+
31+
public static function builder(string $apiKey): AssignmentConfigBuilder
32+
{
33+
return new AssignmentConfigBuilder($apiKey);
34+
}
35+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
3+
namespace AmplitudeExperiment\Assignment;
4+
5+
use AmplitudeExperiment\Amplitude\AmplitudeConfigBuilder;
6+
7+
/**
8+
* Extends AmplitudeConfigBuilder to allow configuration {@link AmplitudeConfig} of underlying {@link Amplitude} client.
9+
*/
10+
11+
class AssignmentConfigBuilder extends AmplitudeConfigBuilder
12+
{
13+
protected string $apiKey;
14+
protected int $cacheCapacity = AssignmentConfig::DEFAULTS['cacheCapacity'];
15+
public function __construct(string $apiKey)
16+
{
17+
parent::__construct();
18+
$this->apiKey = $apiKey;
19+
}
20+
21+
public function cacheCapacity(int $cacheCapacity): AssignmentConfigBuilder
22+
{
23+
$this->cacheCapacity = $cacheCapacity;
24+
return $this;
25+
}
26+
27+
public function build(): AssignmentConfig
28+
{
29+
return new AssignmentConfig(
30+
$this->apiKey,
31+
$this->cacheCapacity,
32+
parent::build()
33+
);
34+
}
35+
}

0 commit comments

Comments
 (0)