From aca7cffb9765fadaf16dea7d091093c554a11e89 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 4 Nov 2026 14:43:46 +0000 Subject: [PATCH 1/2] feat: Populate OpenFeature flag metadata from the LaunchDarkly evaluation reason Co-Authored-By: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> --- README.md | 20 ++++- composer.json | 2 +- src/ResolutionDetailsConverter.php | 40 +++++++++ tests/ResolutionDetailsConverterTest.php | 110 +++++++++++++++++++++++ 4 files changed, 170 insertions(+), 2 deletions(-) create mode 100644 tests/ResolutionDetailsConverterTest.php diff --git a/README.md b/README.md index 9550bab..fe772fa 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ This matrix mirrors the [feature matrix of the OpenFeature SDK for PHP](https:// | ❌ | Shutdown | Not supported — not available in the OpenFeature PHP SDK ([php-sdk#93](https://github.com/open-feature/php-sdk/issues/93)). | | ❌ | Tracking | Not supported — the OpenFeature PHP SDK provider interface has no `track` method. LaunchDarkly custom events can be sent through an `LDClient` directly. | | ❌ | Transaction Context Propagation | Not supported — not available in the OpenFeature PHP SDK, which has no transaction context. | -| ❌ | Flag metadata | Not supported — not available in the OpenFeature PHP SDK, whose `EvaluationDetails` and `ResolutionDetails` carry no flag metadata, so LaunchDarkly evaluation reason details cannot be surfaced. | +| ✅ | Flag metadata | LaunchDarkly evaluation reason details are returned as OpenFeature flag metadata. Requires OpenFeature PHP SDK 2.4.0 or later. See [Flag Metadata](#flag-metadata). | Supported: ✅ | Partially supported: ⚠️ | Not supported: ❌ @@ -162,6 +162,24 @@ $attributes = [ $context = new EvaluationContext(null, new Attributes($attributes)); ``` +### Flag Metadata + +Evaluations include flag metadata for the LaunchDarkly specific parts of the evaluation result which have no OpenFeature equivalent. Each entry is absent when it does not apply to the evaluation. + +| Key | Type | Description | +|---------------------|---------|--------------------------------------------------------------------| +| `variationIndex` | integer | The index of the returned variation. Absent for default values. | +| `inExperiment` | boolean | Present, and `true`, when the evaluation was part of an experiment. | +| `ruleIndex` | integer | The index of the rule that matched. | +| `ruleId` | string | The identifier of the rule that matched. | +| `prerequisiteKey` | string | The key of the prerequisite flag that failed. | +| `bigSegmentsStatus` | string | The status of the Big Segments query made during the evaluation. | + +```php +$details = $client->getBooleanDetails("my-flag", false, $context); +$inExperiment = $details->getMetadata()["inExperiment"] ?? false; +``` + ## Learn more Check out our [documentation](http://docs.launchdarkly.com) for in-depth instructions on configuring and using LaunchDarkly. You can also head straight to the [complete reference guide for this SDK](https://docs.launchdarkly.com/sdk/server-side/php). diff --git a/composer.json b/composer.json index c035244..dfcb428 100644 --- a/composer.json +++ b/composer.json @@ -24,7 +24,7 @@ "guzzlehttp/guzzle": "^7", "launchdarkly/server-sdk": "^6.2", "myclabs/php-enum": ">1.8", - "open-feature/sdk": "^2.0" + "open-feature/sdk": "^2.4" }, "require-dev": { "friendsofphp/php-cs-fixer": "^3.16", diff --git a/src/ResolutionDetailsConverter.php b/src/ResolutionDetailsConverter.php index 935185e..a11f8b7 100644 --- a/src/ResolutionDetailsConverter.php +++ b/src/ResolutionDetailsConverter.php @@ -36,9 +36,49 @@ public function toResolutionDetails(EvaluationDetail $result): ResolutionDetails $builder->withVariant(strval($variationIndex)); } + $builder->withMetadata(self::toFlagMetadata($variationIndex, $reason)); + return $builder->build(); } + /** + * The LaunchDarkly specific parts of an evaluation result which have no + * OpenFeature equivalent. Each entry is absent when it does not apply. + * + * @return array + */ + private static function toFlagMetadata(?int $variationIndex, EvaluationReason $reason): array + { + $metadata = []; + + if ($variationIndex !== null) { + $metadata['variationIndex'] = $variationIndex; + } + + if ($reason->isInExperiment()) { + $metadata['inExperiment'] = true; + } + + if ($reason->getRuleIndex() !== null) { + $metadata['ruleIndex'] = $reason->getRuleIndex(); + } + + if ($reason->getRuleId() !== null) { + $metadata['ruleId'] = $reason->getRuleId(); + } + + if ($reason->getPrerequisiteKey() !== null) { + $metadata['prerequisiteKey'] = $reason->getPrerequisiteKey(); + } + + $bigSegmentsStatus = $reason->bigSegmentsEvaluationStatus(); + if ($bigSegmentsStatus !== null) { + $metadata['bigSegmentsStatus'] = $bigSegmentsStatus->value; + } + + return $metadata; + } + private static function kindToString(string $kind): string { switch ($kind) { diff --git a/tests/ResolutionDetailsConverterTest.php b/tests/ResolutionDetailsConverterTest.php new file mode 100644 index 0000000..245fa01 --- /dev/null +++ b/tests/ResolutionDetailsConverterTest.php @@ -0,0 +1,110 @@ +converter = new ResolutionDetailsConverter(); + } + + public function testFallthroughHasOnlyVariationIndex(): void + { + $details = $this->converter->toResolutionDetails( + new EvaluationDetail(true, 1, EvaluationReason::fallthrough()) + ); + + $this->assertSame(['variationIndex' => 1], $details->getMetadata()); + } + + public function testExperimentFallthroughIncludesInExperiment(): void + { + $details = $this->converter->toResolutionDetails( + new EvaluationDetail(true, 0, EvaluationReason::fallthrough(true)) + ); + + $this->assertSame(['variationIndex' => 0, 'inExperiment' => true], $details->getMetadata()); + } + + public function testRuleMatchIncludesRuleIndexAndId(): void + { + $details = $this->converter->toResolutionDetails( + new EvaluationDetail('value', 2, EvaluationReason::ruleMatch(3, 'rule-id', true)) + ); + + $this->assertSame( + ['variationIndex' => 2, 'inExperiment' => true, 'ruleIndex' => 3, 'ruleId' => 'rule-id'], + $details->getMetadata() + ); + } + + public function testPrerequisiteFailedIncludesPrerequisiteKey(): void + { + $details = $this->converter->toResolutionDetails( + new EvaluationDetail(false, 0, EvaluationReason::prerequisiteFailed('prereq-key')) + ); + + $this->assertSame(['variationIndex' => 0, 'prerequisiteKey' => 'prereq-key'], $details->getMetadata()); + } + + public function testBigSegmentsStatusIsIncluded(): void + { + $reason = EvaluationReason::fallthrough() + ->withBigSegmentsEvaluationStatus(BigSegmentsEvaluationStatus::STALE); + $details = $this->converter->toResolutionDetails(new EvaluationDetail(true, 0, $reason)); + + $this->assertSame(['variationIndex' => 0, 'bigSegmentsStatus' => 'STALE'], $details->getMetadata()); + } + + public function testDefaultValueHasNoMetadata(): void + { + $details = $this->converter->toResolutionDetails( + new EvaluationDetail(true, null, EvaluationReason::error(EvaluationReason::FLAG_NOT_FOUND_ERROR)) + ); + + $this->assertSame([], $details->getMetadata()); + } + + public function testOffWithoutOffVariationHasNoMetadata(): void + { + $details = $this->converter->toResolutionDetails( + new EvaluationDetail(null, null, EvaluationReason::off()) + ); + + $this->assertSame([], $details->getMetadata()); + } + + public function testProviderEvaluationIncludesRuleMetadata(): void + { + $td = new Integrations\TestData(); + // @phpstan-ignore-next-line argument.type (the SDK documents the variadic values as an array) + $rule = $td->flag('flag-key')->ifMatch('key', 'user-key'); + $td->update($rule->thenReturn(true)); + + $provider = new Provider('sdk-key', ['feature_requester' => $td, 'send_events' => false]); + $details = $provider->resolveBooleanValue('flag-key', false, new EvaluationContext('user-key')); + + $metadata = $details->getMetadata(); + $this->assertNotNull($metadata); + $this->assertSame(0, $metadata['variationIndex']); + $this->assertSame(0, $metadata['ruleIndex']); + $this->assertArrayHasKey('ruleId', $metadata); + $this->assertArrayNotHasKey('inExperiment', $metadata); + $this->assertArrayNotHasKey('prerequisiteKey', $metadata); + $this->assertArrayNotHasKey('bigSegmentsStatus', $metadata); + } +} From 3e825b7fa23e54c2f24f7f32e6b511f17bd360f7 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 4 Nov 2026 20:58:02 +0000 Subject: [PATCH 2/2] fix: Require LaunchDarkly PHP SDK 6.4 for Big Segments evaluation status Co-Authored-By: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index dfcb428..449ecc0 100644 --- a/composer.json +++ b/composer.json @@ -22,7 +22,7 @@ "require": { "php": ">=8.2", "guzzlehttp/guzzle": "^7", - "launchdarkly/server-sdk": "^6.2", + "launchdarkly/server-sdk": "^6.4", "myclabs/php-enum": ">1.8", "open-feature/sdk": "^2.4" },