Skip to content

Commit a940b34

Browse files
authored
feat: Add support for client-side prerequisite events (#210)
1 parent 5b25095 commit a940b34

File tree

8 files changed

+265
-27
lines changed

8 files changed

+265
-27
lines changed

Diff for: src/LaunchDarkly/FeatureFlagsState.php

+5-1
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@ public function addFlag(
4343
EvaluationDetail $detail,
4444
bool $forceReasonTracking = false,
4545
bool $withReason = false,
46-
bool $detailsOnlyIfTracked = false
46+
bool $detailsOnlyIfTracked = false,
47+
?array $prerequisites = null,
4748
): void {
4849
$this->_flagValues[$flag->getKey()] = $detail->getValue();
4950
$meta = [];
@@ -60,6 +61,9 @@ public function addFlag(
6061

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

64+
if ($prerequisites) {
65+
$meta['prerequisites'] = $prerequisites;
66+
}
6367
if ($reason && !$omitDetails) {
6468
$meta['reason'] = $reason;
6569
}

Diff for: src/LaunchDarkly/Impl/Evaluation/EvalResult.php

+13-1
Original file line numberDiff line numberDiff line change
@@ -15,23 +15,35 @@
1515
class EvalResult
1616
{
1717
private EvaluationDetail $_detail;
18+
private ?EvaluatorState $_state;
1819
private bool $_forceReasonTracking;
1920

2021
/**
2122
* @param EvaluationDetail $detail
2223
* @param bool $forceReasonTracking
2324
*/
24-
public function __construct(EvaluationDetail $detail, bool $forceReasonTracking = false)
25+
public function __construct(EvaluationDetail $detail, bool $forceReasonTracking = false, EvaluatorState $state = null)
2526
{
2627
$this->_detail = $detail;
28+
$this->_state = $state;
2729
$this->_forceReasonTracking = $forceReasonTracking;
2830
}
2931

32+
public function withState(EvaluatorState $state): EvalResult
33+
{
34+
return new EvalResult($this->_detail, $this->_forceReasonTracking, $state);
35+
}
36+
3037
public function getDetail(): EvaluationDetail
3138
{
3239
return $this->_detail;
3340
}
3441

42+
public function getState(): ?EvaluatorState
43+
{
44+
return $this->_state;
45+
}
46+
3547
public function isForceReasonTracking(): bool
3648
{
3749
return $this->_forceReasonTracking;

Diff for: src/LaunchDarkly/Impl/Evaluation/Evaluator.php

+15-18
Original file line numberDiff line numberDiff line change
@@ -16,20 +16,6 @@
1616
use LaunchDarkly\Subsystems\FeatureRequester;
1717
use Psr\Log\LoggerInterface;
1818

19-
/**
20-
* @ignore
21-
* @internal
22-
*/
23-
class EvaluatorState
24-
{
25-
public ?array $prerequisiteStack = null;
26-
public ?array $segmentStack = null;
27-
28-
public function __construct(public FeatureFlag $originalFlag)
29-
{
30-
}
31-
}
32-
3319
/**
3420
* Encapsulates the feature flag evaluation logic. The Evaluator has no direct access to the
3521
* rest of the SDK environment; if it needs to retrieve flags or segments that are referenced
@@ -62,15 +48,15 @@ public function __construct(FeatureRequester $featureRequester, ?LoggerInterface
6248
*/
6349
public function evaluate(FeatureFlag $flag, LDContext $context, ?callable $prereqEvalSink): EvalResult
6450
{
65-
$stateStack = null;
6651
$state = new EvaluatorState($flag);
6752
try {
68-
return $this->evaluateInternal($flag, $context, $prereqEvalSink, $state);
53+
return $this->evaluateInternal($flag, $context, $prereqEvalSink, $state)
54+
->withState($state);
6955
} catch (EvaluationException $e) {
70-
return new EvalResult(new EvaluationDetail(null, null, EvaluationReason::error($e->getErrorKind())));
56+
return new EvalResult(new EvaluationDetail(null, null, EvaluationReason::error($e->getErrorKind())), false, $state);
7157
} catch (\Throwable $e) {
7258
Util::logExceptionAtErrorLevel($this->_logger, $e, 'Unexpected error when evaluating flag ' . $flag->getKey());
73-
return new EvalResult(new EvaluationDetail(null, null, EvaluationReason::error(EvaluationReason::EXCEPTION_ERROR)));
59+
return new EvalResult(new EvaluationDetail(null, null, EvaluationReason::error(EvaluationReason::EXCEPTION_ERROR)), false, $state);
7460
}
7561
}
7662

@@ -144,14 +130,25 @@ private function checkPrerequisites(
144130
EvaluationReason::MALFORMED_FLAG_ERROR
145131
);
146132
}
133+
134+
if ($state->depth == 0) {
135+
if ($state->prerequisites === null) {
136+
$state->prerequisites = [];
137+
}
138+
$state->prerequisites[] = $prereqKey;
139+
}
140+
141+
147142
$prereqOk = true;
148143
$prereqFeatureFlag = $this->_featureRequester->getFeature($prereqKey);
149144
if ($prereqFeatureFlag === null) {
150145
$prereqOk = false;
151146
} else {
152147
// Note that if the prerequisite flag is off, we don't consider it a match no matter what its
153148
// off variation was. But we still need to evaluate it in order to generate an event.
149+
$state->depth++;
154150
$prereqEvalResult = $this->evaluateInternal($prereqFeatureFlag, $context, $prereqEvalSink, $state);
151+
$state->depth--;
155152
$variation = $prereq->getVariation();
156153
if (!$prereqFeatureFlag->isOn() || $prereqEvalResult->getDetail()->getVariationIndex() !== $variation) {
157154
$prereqOk = false;

Diff for: src/LaunchDarkly/Impl/Evaluation/EvaluatorState.php

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace LaunchDarkly\Impl\Evaluation;
6+
7+
use LaunchDarkly\Impl\Model\FeatureFlag;
8+
9+
/**
10+
* @ignore
11+
* @internal
12+
*/
13+
class EvaluatorState
14+
{
15+
public ?array $prerequisiteStack = null;
16+
public ?array $segmentStack = null;
17+
public ?array $prerequisites = null;
18+
public int $depth = 0;
19+
20+
public function __construct(public FeatureFlag $originalFlag)
21+
{
22+
}
23+
}

Diff for: src/LaunchDarkly/LDClient.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -501,7 +501,7 @@ public function allFlagsState(LDContext $context, array $options = []): FeatureF
501501
continue;
502502
}
503503
$result = $tempEvaluator->evaluate($flag, $context, null);
504-
$state->addFlag($flag, $result->getDetail(), $result->isForceReasonTracking(), $withReasons, $detailsOnlyIfTracked);
504+
$state->addFlag($flag, $result->getDetail(), $result->isForceReasonTracking(), $withReasons, $detailsOnlyIfTracked, $result->getState()?->prerequisites);
505505
}
506506
return $state;
507507
}

Diff for: test-service/TestService.php

+2-1
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,8 @@ public function getStatus(): array
7777
'migrations',
7878
'event-sampling',
7979
'inline-context',
80-
'anonymous-redaction'
80+
'anonymous-redaction',
81+
'client-prereq-events'
8182
],
8283
'clientVersion' => \LaunchDarkly\LDClient::VERSION
8384
];

Diff for: tests/Impl/Evaluation/RolloutRandomizationConsistencyTest.php

+8-5
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ public function buildFlag()
6363

6464
return $decodedFlag;
6565
}
66-
66+
6767
public function testVariationIndexForContext()
6868
{
6969
$flag = $this->buildFlag();
@@ -87,18 +87,21 @@ public function testVariationIndexForContext()
8787
);
8888

8989
$evaluator = new Evaluator(static::$requester);
90-
90+
9191
$context1 = LDContext::create('userKeyA');
9292
$result1 = $evaluator->evaluate($flag, $context1, EvaluatorTestUtil::expectNoPrerequisiteEvals());
93-
$this->assertEquals($expectedEvalResult1, $result1);
93+
$this->assertEquals($expectedEvalResult1->getDetail(), $result1->getDetail());
94+
$this->assertEquals($expectedEvalResult1->isForceReasonTracking(), $result1->isForceReasonTracking());
9495

9596
$context2 = LDContext::create('userKeyB');
9697
$result2 = $evaluator->evaluate($flag, $context2, EvaluatorTestUtil::expectNoPrerequisiteEvals());
97-
$this->assertEquals($expectedEvalResult2, $result2);
98+
$this->assertEquals($expectedEvalResult2->getDetail(), $result2->getDetail());
99+
$this->assertEquals($expectedEvalResult2->isForceReasonTracking(), $result2->isForceReasonTracking());
98100

99101
$context3 = LDContext::create('userKeyC');
100102
$result3 = $evaluator->evaluate($flag, $context3, EvaluatorTestUtil::expectNoPrerequisiteEvals());
101-
$this->assertEquals($expectedEvalResult3, $result3);
103+
$this->assertEquals($expectedEvalResult3->getDetail(), $result3->getDetail());
104+
$this->assertEquals($expectedEvalResult3->isForceReasonTracking(), $result3->isForceReasonTracking());
102105
}
103106

104107
public function testBucketContextByKey()

0 commit comments

Comments
 (0)