Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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