Skip to content

Commit

Permalink
feat: Add support for client-side prerequisite events (#210)
Browse files Browse the repository at this point in the history
  • Loading branch information
keelerm84 authored Oct 22, 2024
1 parent 5b25095 commit a940b34
Show file tree
Hide file tree
Showing 8 changed files with 265 additions and 27 deletions.
6 changes: 5 additions & 1 deletion src/LaunchDarkly/FeatureFlagsState.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ public function addFlag(
EvaluationDetail $detail,
bool $forceReasonTracking = false,
bool $withReason = false,
bool $detailsOnlyIfTracked = false
bool $detailsOnlyIfTracked = false,
?array $prerequisites = null,
): void {
$this->_flagValues[$flag->getKey()] = $detail->getValue();
$meta = [];
Expand All @@ -60,6 +61,9 @@ public function addFlag(

$reason = (!$withReason && !$trackReason) ? null : $detail->getReason();

if ($prerequisites) {
$meta['prerequisites'] = $prerequisites;
}
if ($reason && !$omitDetails) {
$meta['reason'] = $reason;
}
Expand Down
14 changes: 13 additions & 1 deletion src/LaunchDarkly/Impl/Evaluation/EvalResult.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,23 +15,35 @@
class EvalResult
{
private EvaluationDetail $_detail;
private ?EvaluatorState $_state;
private bool $_forceReasonTracking;

/**
* @param EvaluationDetail $detail
* @param bool $forceReasonTracking
*/
public function __construct(EvaluationDetail $detail, bool $forceReasonTracking = false)
public function __construct(EvaluationDetail $detail, bool $forceReasonTracking = false, EvaluatorState $state = null)
{
$this->_detail = $detail;
$this->_state = $state;
$this->_forceReasonTracking = $forceReasonTracking;
}

public function withState(EvaluatorState $state): EvalResult
{
return new EvalResult($this->_detail, $this->_forceReasonTracking, $state);
}

public function getDetail(): EvaluationDetail
{
return $this->_detail;
}

public function getState(): ?EvaluatorState
{
return $this->_state;
}

public function isForceReasonTracking(): bool
{
return $this->_forceReasonTracking;
Expand Down
33 changes: 15 additions & 18 deletions src/LaunchDarkly/Impl/Evaluation/Evaluator.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,6 @@
use LaunchDarkly\Subsystems\FeatureRequester;
use Psr\Log\LoggerInterface;

/**
* @ignore
* @internal
*/
class EvaluatorState
{
public ?array $prerequisiteStack = null;
public ?array $segmentStack = null;

public function __construct(public FeatureFlag $originalFlag)
{
}
}

/**
* Encapsulates the feature flag evaluation logic. The Evaluator has no direct access to the
* rest of the SDK environment; if it needs to retrieve flags or segments that are referenced
Expand Down Expand Up @@ -62,15 +48,15 @@ public function __construct(FeatureRequester $featureRequester, ?LoggerInterface
*/
public function evaluate(FeatureFlag $flag, LDContext $context, ?callable $prereqEvalSink): EvalResult
{
$stateStack = null;
$state = new EvaluatorState($flag);
try {
return $this->evaluateInternal($flag, $context, $prereqEvalSink, $state);
return $this->evaluateInternal($flag, $context, $prereqEvalSink, $state)
->withState($state);
} catch (EvaluationException $e) {
return new EvalResult(new EvaluationDetail(null, null, EvaluationReason::error($e->getErrorKind())));
return new EvalResult(new EvaluationDetail(null, null, EvaluationReason::error($e->getErrorKind())), false, $state);
} catch (\Throwable $e) {
Util::logExceptionAtErrorLevel($this->_logger, $e, 'Unexpected error when evaluating flag ' . $flag->getKey());
return new EvalResult(new EvaluationDetail(null, null, EvaluationReason::error(EvaluationReason::EXCEPTION_ERROR)));
return new EvalResult(new EvaluationDetail(null, null, EvaluationReason::error(EvaluationReason::EXCEPTION_ERROR)), false, $state);
}
}

Expand Down Expand Up @@ -144,14 +130,25 @@ private function checkPrerequisites(
EvaluationReason::MALFORMED_FLAG_ERROR
);
}

if ($state->depth == 0) {
if ($state->prerequisites === null) {
$state->prerequisites = [];
}
$state->prerequisites[] = $prereqKey;
}


$prereqOk = true;
$prereqFeatureFlag = $this->_featureRequester->getFeature($prereqKey);
if ($prereqFeatureFlag === null) {
$prereqOk = false;
} else {
// Note that if the prerequisite flag is off, we don't consider it a match no matter what its
// off variation was. But we still need to evaluate it in order to generate an event.
$state->depth++;
$prereqEvalResult = $this->evaluateInternal($prereqFeatureFlag, $context, $prereqEvalSink, $state);
$state->depth--;
$variation = $prereq->getVariation();
if (!$prereqFeatureFlag->isOn() || $prereqEvalResult->getDetail()->getVariationIndex() !== $variation) {
$prereqOk = false;
Expand Down
23 changes: 23 additions & 0 deletions src/LaunchDarkly/Impl/Evaluation/EvaluatorState.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

declare(strict_types=1);

namespace LaunchDarkly\Impl\Evaluation;

use LaunchDarkly\Impl\Model\FeatureFlag;

/**
* @ignore
* @internal
*/
class EvaluatorState
{
public ?array $prerequisiteStack = null;
public ?array $segmentStack = null;
public ?array $prerequisites = null;
public int $depth = 0;

public function __construct(public FeatureFlag $originalFlag)
{
}
}
2 changes: 1 addition & 1 deletion src/LaunchDarkly/LDClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -501,7 +501,7 @@ public function allFlagsState(LDContext $context, array $options = []): FeatureF
continue;
}
$result = $tempEvaluator->evaluate($flag, $context, null);
$state->addFlag($flag, $result->getDetail(), $result->isForceReasonTracking(), $withReasons, $detailsOnlyIfTracked);
$state->addFlag($flag, $result->getDetail(), $result->isForceReasonTracking(), $withReasons, $detailsOnlyIfTracked, $result->getState()?->prerequisites);
}
return $state;
}
Expand Down
3 changes: 2 additions & 1 deletion test-service/TestService.php
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,8 @@ public function getStatus(): array
'migrations',
'event-sampling',
'inline-context',
'anonymous-redaction'
'anonymous-redaction',
'client-prereq-events'
],
'clientVersion' => \LaunchDarkly\LDClient::VERSION
];
Expand Down
13 changes: 8 additions & 5 deletions tests/Impl/Evaluation/RolloutRandomizationConsistencyTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ public function buildFlag()

return $decodedFlag;
}

public function testVariationIndexForContext()
{
$flag = $this->buildFlag();
Expand All @@ -87,18 +87,21 @@ public function testVariationIndexForContext()
);

$evaluator = new Evaluator(static::$requester);

$context1 = LDContext::create('userKeyA');
$result1 = $evaluator->evaluate($flag, $context1, EvaluatorTestUtil::expectNoPrerequisiteEvals());
$this->assertEquals($expectedEvalResult1, $result1);
$this->assertEquals($expectedEvalResult1->getDetail(), $result1->getDetail());
$this->assertEquals($expectedEvalResult1->isForceReasonTracking(), $result1->isForceReasonTracking());

$context2 = LDContext::create('userKeyB');
$result2 = $evaluator->evaluate($flag, $context2, EvaluatorTestUtil::expectNoPrerequisiteEvals());
$this->assertEquals($expectedEvalResult2, $result2);
$this->assertEquals($expectedEvalResult2->getDetail(), $result2->getDetail());
$this->assertEquals($expectedEvalResult2->isForceReasonTracking(), $result2->isForceReasonTracking());

$context3 = LDContext::create('userKeyC');
$result3 = $evaluator->evaluate($flag, $context3, EvaluatorTestUtil::expectNoPrerequisiteEvals());
$this->assertEquals($expectedEvalResult3, $result3);
$this->assertEquals($expectedEvalResult3->getDetail(), $result3->getDetail());
$this->assertEquals($expectedEvalResult3->isForceReasonTracking(), $result3->isForceReasonTracking());
}

public function testBucketContextByKey()
Expand Down
Loading

0 comments on commit a940b34

Please sign in to comment.