From 0423cacde808246e96a960cc7dc0a98e3ba12f92 Mon Sep 17 00:00:00 2001 From: hhan2024 Date: Tue, 29 Sep 2026 13:57:52 -0400 Subject: [PATCH] Expose FFE serial IDs in flag metadata with span enrichment Preserve zero and omit absent serial IDs while retaining existing flag metadata. Environment: Datadog workspace Co-Authored-By: Codex GPT-5 --- .../FeatureFlags/Internal/NativeEvaluator.php | 7 ++++- .../FeatureFlags/Internal/ResultMapper.php | 23 +++++++++++--- .../Unit/FeatureFlags/ResultMapperTest.php | 31 +++++++++++++++++++ 3 files changed, 55 insertions(+), 6 deletions(-) diff --git a/src/api/FeatureFlags/Internal/NativeEvaluator.php b/src/api/FeatureFlags/Internal/NativeEvaluator.php index b1984034af6..2691ea9f2ff 100644 --- a/src/api/FeatureFlags/Internal/NativeEvaluator.php +++ b/src/api/FeatureFlags/Internal/NativeEvaluator.php @@ -57,7 +57,12 @@ public function evaluate( $rawResult = $this->withProviderState($rawResult); } - $details = $this->mapper->map($rawResult, $expectedType, $defaultValue); + $details = $this->mapper->map( + $rawResult, + $expectedType, + $defaultValue, + SpanEnrichmentRegistry::gateEnabled() + ); // Native clients submit now. OpenFeature can retain this snapshot until // its hooks finish, independently of native/OTLP metric recording. diff --git a/src/api/FeatureFlags/Internal/ResultMapper.php b/src/api/FeatureFlags/Internal/ResultMapper.php index d263d64e085..f82608a745d 100644 --- a/src/api/FeatureFlags/Internal/ResultMapper.php +++ b/src/api/FeatureFlags/Internal/ResultMapper.php @@ -9,6 +9,7 @@ final class ResultMapper { + const SERIAL_ID_METADATA_KEY = '__dd_split_serial_id'; const BRIDGE_REASON_STATIC = 0; const BRIDGE_REASON_DEFAULT = 1; const BRIDGE_REASON_TARGETING_MATCH = 2; @@ -27,9 +28,10 @@ final class ResultMapper * @param array|object|null $rawResult * @param string $expectedType One of EvaluationType::*. * @param mixed $defaultValue + * @param bool $spanEnrichmentEnabled * @return EvaluationDetails */ - public function map($rawResult, $expectedType, $defaultValue) + public function map($rawResult, $expectedType, $defaultValue, $spanEnrichmentEnabled = false) { if (!EvaluationType::isValid($expectedType)) { throw new \InvalidArgumentException('Unknown feature flag value type: ' . (string) $expectedType); @@ -69,7 +71,7 @@ public function map($rawResult, $expectedType, $defaultValue) $reason = $this->mapReason($this->read($rawResult, array('reason'), self::BRIDGE_REASON_DEFAULT)); if ($this->isDefaultReturn($rawResult, $reason)) { - return $this->defaultDetails($defaultValue, $expectedType, $reason, $rawResult); + return $this->defaultDetails($defaultValue, $expectedType, $reason, $rawResult, $spanEnrichmentEnabled); } $decoded = null; @@ -93,13 +95,13 @@ public function map($rawResult, $expectedType, $defaultValue) $this->read($rawResult, array('variant'), null), null, null, - $this->readArray($rawResult, array('flag_metadata', 'flagMetadata', 'metadata')), + $this->flagMetadata($rawResult, $spanEnrichmentEnabled), $this->exposureData($rawResult), $this->providerState($rawResult) ); } - private function defaultDetails($defaultValue, $expectedType, $reason, $rawResult) + private function defaultDetails($defaultValue, $expectedType, $reason, $rawResult, $spanEnrichmentEnabled) { return new EvaluationDetails( $defaultValue, @@ -108,7 +110,7 @@ private function defaultDetails($defaultValue, $expectedType, $reason, $rawResul null, null, null, - $this->readArray($rawResult, array('flag_metadata', 'flagMetadata', 'metadata')), + $this->flagMetadata($rawResult, $spanEnrichmentEnabled), array(), $this->providerState($rawResult) ); @@ -290,6 +292,17 @@ private function exposureData($rawResult) return $exposureData; } + private function flagMetadata($rawResult, $spanEnrichmentEnabled) + { + $metadata = $this->readArray($rawResult, array('flag_metadata', 'flagMetadata', 'metadata')); + $serialId = $this->read($rawResult, array('serial_id', 'serialId'), null); + if ($spanEnrichmentEnabled && $serialId !== null) { + $metadata[self::SERIAL_ID_METADATA_KEY] = (int) $serialId; + } + + return $metadata; + } + private function providerState($rawResult) { $providerState = $this->readArray($rawResult, array('provider_state', 'providerState')); diff --git a/tests/api/Unit/FeatureFlags/ResultMapperTest.php b/tests/api/Unit/FeatureFlags/ResultMapperTest.php index bc2881f81a4..05056d1de64 100644 --- a/tests/api/Unit/FeatureFlags/ResultMapperTest.php +++ b/tests/api/Unit/FeatureFlags/ResultMapperTest.php @@ -10,6 +10,37 @@ final class ResultMapperTest extends TestCase { + /** + * @dataProvider serialIdMetadataProvider + */ + public function testSerialIdFlagMetadata($serialFields, $enabled, $expectedMetadata) + { + $rawResult = array_merge(array( + 'value_json' => '"blue"', + 'variant' => 'variant-a', + 'reason' => ResultMapper::BRIDGE_REASON_SPLIT, + 'error_code' => ResultMapper::BRIDGE_ERROR_NONE, + 'flag_metadata' => array('owner' => 'ffe'), + ), $serialFields); + + foreach (array($rawResult, (object) $rawResult) as $result) { + $details = (new ResultMapper())->map($result, EvaluationType::STRING, 'red', $enabled); + $this->assertSame($expectedMetadata, $details->getFlagMetadata()); + } + } + + public function serialIdMetadataProvider() + { + return array( + 'positive' => array(array('serial_id' => 4242), true, array('owner' => 'ffe', '__dd_split_serial_id' => 4242)), + 'zero' => array(array('serial_id' => 0), true, array('owner' => 'ffe', '__dd_split_serial_id' => 0)), + 'camel case' => array(array('serialId' => 7), true, array('owner' => 'ffe', '__dd_split_serial_id' => 7)), + 'missing' => array(array(), true, array('owner' => 'ffe')), + 'null' => array(array('serial_id' => null), true, array('owner' => 'ffe')), + 'gate disabled' => array(array('serial_id' => 4242), false, array('owner' => 'ffe')), + ); + } + public function testMapsSuccessfulBridgeResultToEvaluationDetails() { $details = (new ResultMapper())->map(array(