Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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
22 changes: 20 additions & 2 deletions src/Remote/FetchOptions.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,33 @@ class FetchOptions
*
* @var string[]
*/
public array $flagKeys;
public array $flagKeys = [];

/**
* Whether to track the assignment event.
*
* @var ?bool
*/
public ?bool $tracksAssignment = null;

/**
* Whether to track the exposure event.
*
* @var ?bool
*/
public ?bool $tracksExposure = null;

/**
* FetchOptions constructor.
*
* @param string[] $flagKeys Specific flag keys to evaluate and set variants for.
* @param ?bool $tracksAssignment Whether to track the assignment event.
* @param ?bool $tracksExposure Whether to track the exposure event.
*/
public function __construct(array $flagKeys)
public function __construct(array $flagKeys = [], ?bool $tracksAssignment = null, ?bool $tracksExposure = null)
{
$this->flagKeys = $flagKeys;
$this->tracksAssignment = $tracksAssignment;
$this->tracksExposure = $tracksExposure;
}
}
46 changes: 39 additions & 7 deletions src/Remote/RemoteEvaluationClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,16 +41,18 @@ public function __construct(string $apiKey, ?RemoteEvaluationConfig $config = nu
}

/**
* Fetch all variants for a user.
*
* This method will automatically retry if configured (default).
* Fetch variants for a user with specific options.
*
* @param User $user The {@link User} context
* @param array<string> $flagKeys The flags to evaluate for this specific fetch request.
* @return array<Variant> A {@link Variant} array for the user on success, empty array on error.
* @param FetchOptions $options The {@link FetchOptions} object
* @return Variant[] A {@link Variant} array for the user on success, empty array on error.
*/
public function fetch(User $user, array $flagKeys = []): array
private function fetchWithOptions(User $user, FetchOptions $options): array
{
$flagKeys = $options->flagKeys;
$tracksAssignment = $options->tracksAssignment;
$tracksExposure = $options->tracksExposure;

if ($user->userId == null && $user->deviceId == null) {
$this->logger->warning('[Experiment] user id and device id are null; Amplitude may not resolve identity');
}
Expand All @@ -72,7 +74,7 @@ public function fetch(User $user, array $flagKeys = []): array
->withHeader('Content-Type', 'application/json')
->withHeader('X-Amp-Exp-User', $serializedUser);

if (!empty($flagKeys)) {
if ($flagKeys !== null && !empty($flagKeys)) {
$flagKeysJson = json_encode($flagKeys);
if ($flagKeysJson === false) {
$this->logger->error('[Experiment] Failed to fetch variants: ' . json_last_error_msg());
Expand All @@ -81,6 +83,13 @@ public function fetch(User $user, array $flagKeys = []): array
$request = $request->withHeader('X-Amp-Exp-Flag-Keys', base64_encode($flagKeysJson));
}

if ($tracksAssignment !== null) {
$request = $request->withHeader('X-Amp-Exp-Track', $tracksAssignment ? 'track' : 'no-track');
}
if ($tracksExposure !== null) {
$request = $request->withHeader('X-Amp-Exp-Exposure-Track', $tracksExposure ? 'track' : 'no-track');
}

$httpClient = $this->httpClient->getClient();

try {
Expand All @@ -101,4 +110,27 @@ public function fetch(User $user, array $flagKeys = []): array
return [];
}
}

/**
* Fetch all variants for a user.
*
* This method will automatically retry if configured (default).
*
* @param User $user The {@link User} context
* @param array<string>|FetchOptions|null $arg Either flags to evaluate for this specific fetch request or a {@link FetchOptions} object.
* If an array is provided, it is treated as the flag keys and will be converted to a {@link FetchOptions} object.
* If a {@link FetchOptions} object is provided, it will be used as is.
* If no arguments are provided, a default {@link FetchOptions} object without any options will be used.
* @return Variant[] A {@link Variant} array for the user on success, empty array on error.
*/
public function fetch(User $user, $arg = null): array
{
if ($arg !== null && is_array($arg)) {
return $this->fetchWithOptions($user, new FetchOptions($arg, null, null));
}
if ($arg !== null && is_object($arg) && $arg instanceof FetchOptions) {
return $this->fetchWithOptions($user, $arg);
}
return $this->fetchWithOptions($user, new FetchOptions());
}
}
70 changes: 70 additions & 0 deletions tests/Remote/RemoteEvaluationClientTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use AmplitudeExperiment\Experiment;
use AmplitudeExperiment\Remote\RemoteEvaluationClient;
use AmplitudeExperiment\Remote\RemoteEvaluationConfig;
use AmplitudeExperiment\Remote\FetchOptions;
use AmplitudeExperiment\Test\Util\MockGuzzleHttpClient;
use AmplitudeExperiment\User;
use GuzzleHttp\Exception\RequestException;
Expand Down Expand Up @@ -153,4 +154,73 @@ public function testExperimentInitializeRemote()
$client = $experiment->initializeRemote($this->apiKey);
$this->assertEquals($client, $experiment->initializeRemote($this->apiKey));
}

public function testFetchWithFetchOptionsSuccess()
{
// Create an instance of GuzzleFetchClient with the custom handler stack
$mockHandler = new MockHandler([
function (RequestInterface $request, array $options) {
$headers = $request->getHeaders();
$this->assertEquals(base64_encode('["sdk-ci-test"]'), $headers['X-Amp-Exp-Flag-Keys'][0]);
$this->assertEquals('track', $headers['X-Amp-Exp-Track'][0]);
$this->assertEquals('track', $headers['X-Amp-Exp-Exposure-Track'][0]);
return new Response(200, [], '{"sdk-ci-test":{"key":"on","payload":"payload"}}');
},
function (RequestInterface $request, array $options) {
$headers = $request->getHeaders();
$this->assertEquals(base64_encode('["sdk-ci-test"]'), $headers['X-Amp-Exp-Flag-Keys'][0]);
$this->assertEquals('no-track', $headers['X-Amp-Exp-Track'][0]);
$this->assertEquals('no-track', $headers['X-Amp-Exp-Exposure-Track'][0]);
return new Response(200, [], '{"sdk-ci-test":{"key":"on","payload":"payload"}}');
},
function (RequestInterface $request, array $options) {
$headers = $request->getHeaders();
$this->assertEquals(base64_encode('["sdk-ci-test"]'), $headers['X-Amp-Exp-Flag-Keys'][0]);
$this->assertArrayNotHasKey('X-Amp-Exp-Track', $headers);
$this->assertArrayNotHasKey('X-Amp-Exp-Exposure-Track', $headers);
return new Response(200, [], '{"sdk-ci-test":{"key":"on","payload":"payload"}}');
},
function (RequestInterface $request, array $options) {
$headers = $request->getHeaders();
$this->assertEquals(base64_encode('["sdk-ci-test"]'), $headers['X-Amp-Exp-Flag-Keys'][0]);
$this->assertArrayNotHasKey('X-Amp-Exp-Track', $headers);
$this->assertArrayNotHasKey('X-Amp-Exp-Exposure-Track', $headers);
return new Response(200, [], '{"sdk-ci-test":{"key":"on","payload":"payload"}}');
},
function (RequestInterface $request, array $options) {
$headers = $request->getHeaders();
$this->assertArrayNotHasKey('X-Amp-Exp-Flag-Keys', $headers);
$this->assertArrayNotHasKey('X-Amp-Exp-Track', $headers);
$this->assertArrayNotHasKey('X-Amp-Exp-Exposure-Track', $headers);
return new Response(200, [], '{"sdk-ci-test":{"key":"on","payload":"payload"}}');
},
]);
$handlerStack = HandlerStack::create($mockHandler);
$httpClient = new MockGuzzleHttpClient([
'retries' => 1,
'timeoutMillis' => 1000,
], $handlerStack);

$client = new RemoteEvaluationClient($this->apiKey, RemoteEvaluationConfig::builder()->httpClient($httpClient)->build());

$variants = $client->fetch($this->testUser, new FetchOptions(['sdk-ci-test'], true, true));
$this->assertEquals(1, sizeof($variants));
$this->assertEquals("on", $variants['sdk-ci-test']->key);

$variants = $client->fetch($this->testUser, new FetchOptions(['sdk-ci-test'], false, false));
$this->assertEquals(1, sizeof($variants));
$this->assertEquals("on", $variants['sdk-ci-test']->key);

$variants = $client->fetch($this->testUser, new FetchOptions(['sdk-ci-test']));
$this->assertEquals(1, sizeof($variants));
$this->assertEquals("on", $variants['sdk-ci-test']->key);

$variants = $client->fetch($this->testUser, ['sdk-ci-test']);
$this->assertEquals(1, sizeof($variants));
$this->assertEquals("on", $variants['sdk-ci-test']->key);

$variants = $client->fetch($this->testUser);
$this->assertEquals(1, sizeof($variants));
$this->assertEquals("on", $variants['sdk-ci-test']->key);
}
}