Repository navigation
[PHP] Expose FFE serial IDs in flag metadata with span enrichment #4247
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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() | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🤖 Comment from Claude working fully autonomously overnight: [P2] Please add an end-to-end test for this gate argument. At the moment, no in-repo test checks it.
I changed this argument to Suggested test: tests/api/Unit/FeatureFlags/flag_metadata_serial_id.phpt--TEST--
FFE flag metadata carries the selected split serial ID only when span enrichment is enabled
--ENV--
DD_METRICS_OTEL_ENABLED=false
DD_TRACE_GENERATE_ROOT_SPAN=0
DD_EXPERIMENTAL_FLAGGING_PROVIDER_SPAN_ENRICHMENT_ENABLED=true
--FILE--
<?php
require __DIR__ . '/../../../Common/FeatureFlags/ffe_api_bootstrap.inc';
function ffe_serial_split($serialId)
{
$split = array('variationKey' => 'blue', 'shards' => array());
if ($serialId !== null) {
$split['serialId'] = $serialId;
}
return $split;
}
function ffe_serial_flag($key, array $allocations)
{
return array(
'key' => $key, 'enabled' => true, 'variationType' => 'STRING',
'variations' => array('blue' => array('key' => 'blue', 'value' => 'blue')),
'allocations' => $allocations,
);
}
function ffe_serial_allocation($key, $serialId, array $rules = array())
{
return array('key' => $key, 'rules' => $rules, 'doLog' => false, 'splits' => array(ffe_serial_split($serialId)));
}
function show($label, $details)
{
echo $label, ' metadata=', json_encode($details->getFlagMetadata()), "\n";
}
$vipRule = array('conditions' => array(array('attribute' => 'plan', 'operator' => 'ONE_OF', 'value' => array('vip'))));
$config = array(
'createdAt' => '2026-05-22T00:00:00Z',
'environment' => array('name' => 'test'),
'flags' => array(
'positive' => ffe_serial_flag('positive', array(ffe_serial_allocation('a-positive', 4242))),
'zero' => ffe_serial_flag('zero', array(ffe_serial_allocation('a-zero', 0))),
'missing' => ffe_serial_flag('missing', array(ffe_serial_allocation('a-missing', null))),
'targeted' => ffe_serial_flag('targeted', array(
ffe_serial_allocation('a-vip', 201, array($vipRule)),
ffe_serial_allocation('a-standard', 200),
)),
),
);
var_dump(\DDTrace\Testing\ffe_load_config(json_encode($config)));
$client = new \DDTrace\FeatureFlags\Client();
$context = array('targetingKey' => 'subject');
show('on positive', $client->getStringDetails('positive', 'fallback', $context));
show('on zero', $client->getStringDetails('zero', 'fallback', $context));
show('on missing', $client->getStringDetails('missing', 'fallback', $context));
show('on unknown', $client->getStringDetails('unknown', 'fallback', $context));
show('on vip', $client->getStringDetails('targeted', 'fallback', array('targetingKey' => 'subject', 'attributes' => array('plan' => 'vip'))));
show('on standard', $client->getStringDetails('targeted', 'fallback', array('targetingKey' => 'subject', 'attributes' => array('plan' => 'standard'))));
// Gate off: the serial ID stays in exposure data but is not added to flag metadata.
var_dump(ini_set('datadog.experimental_flagging_provider_span_enrichment_enabled', '0') !== false);
$off = $client->getStringDetails('positive', 'fallback', $context);
show('off positive', $off);
$exposure = $off->getExposureData();
echo 'off exposure serialId=', json_encode($exposure['serialId']), "\n";
?>
--EXPECT--
bool(true)
on positive metadata={"__dd_split_serial_id":4242}
on zero metadata={"__dd_split_serial_id":0}
on missing metadata=[]
on unknown metadata=[]
on vip metadata={"__dd_split_serial_id":201}
on standard metadata={"__dd_split_serial_id":200}
bool(true)
off positive metadata=[]
off exposure serialId=4242It follows How I ran it
|
||
| ); | ||
|
|
||
| // Native clients submit now. OpenFeature can retain this snapshot until | ||
| // its hooks finish, independently of native/OTLP metric recording. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,6 +9,7 @@ | |
|
|
||
| final class ResultMapper | ||
| { | ||
| const SERIAL_ID_METADATA_KEY = '__dd_split_serial_id'; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🤖 Comment from Claude working fully autonomously overnight: [P3] Optional nit: give this constant a name that is not already used. Two constants with one name and two values in the same feature are easy to confuse when you read the code. For example, Suggested change and check const SPLIT_SERIAL_ID_FLAG_METADATA_KEY = '__dd_split_serial_id'; $metadata[self::SPLIT_SERIAL_ID_FLAG_METADATA_KEY] = (int) $serialId;No other file uses I also checked the mix-up case. When |
||
| 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<string, mixed>|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), | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🤖 Comment from Claude working fully autonomously overnight: Heads up: When native selects a split but PHP then rejects the value, the serial ID is not added to flag metadata. The system-test contract covers evaluations that return a split's value, so I think the current behavior is fine. I mention it in case other SDKs keep the ID on these errors. No action is needed now. What I observedA JSON flag with a |
||
| $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), | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🤖 Comment from Claude working fully autonomously overnight: [P3] On this default-return path, flag metadata can report a split serial ID while the same evaluation returns the caller's default. I suggest keeping the base behavior here. Native can select a split whose JSON value is The case is narrow: a JSON flag with a Reproduction with the release extensionUFC flag (other fields as in the existing FFE tests): 'jsonnull' => array(
'key' => 'jsonnull', 'enabled' => true, 'variationType' => 'JSON',
'variations' => array('nullv' => array('key' => 'nullv', 'value' => null)),
'allocations' => array(array(
'key' => 'a-tb', 'rules' => array(), 'doLog' => true,
'startAt' => '2020-01-01T00:00:00Z', 'endAt' => '2099-01-01T00:00:00Z',
'splits' => array(array('variationKey' => 'nullv', 'shards' => array(), 'serialId' => 9)),
)),
),With ddtrace 1.26.2 and this PR's sources, gate on: With the patch below, Suggested patch and testPass only four arguments at line 74: return $this->defaultDetails($defaultValue, $expectedType, $reason, $rawResult);Restore private function defaultDetails($defaultValue, $expectedType, $reason, $rawResult)
{
return new EvaluationDetails(
$defaultValue,
$expectedType,
$reason,
null,
null,
null,
$this->readArray($rawResult, array('flag_metadata', 'flagMetadata', 'metadata')),
array(),
$this->providerState($rawResult)
);
}Add to public function testDefaultReturnOmitsSerialIdMetadata()
{
// Native can select a split whose JSON value is null. The mapper then
// returns the caller's default, so no split serial ID is reported.
$details = (new ResultMapper())->map(array(
'value_json' => 'null',
'variant' => 'null-variation',
'reason' => ResultMapper::BRIDGE_REASON_DEFAULT,
'error_code' => ResultMapper::BRIDGE_ERROR_NONE,
'serial_id' => 9,
), EvaluationType::OBJECT, array('fallback' => true), true);
$this->assertSame(array('fallback' => true), $details->getValue());
$this->assertNull($details->getVariant());
$this->assertSame(array(), $details->getExposureData());
$this->assertSame(array(), $details->getFlagMetadata());
}This test fails on this head and passes with the patch. With the patch, |
||
| array(), | ||
| $this->providerState($rawResult) | ||
| ); | ||
|
|
@@ -290,6 +292,17 @@ private function exposureData($rawResult) | |
| return $exposureData; | ||
| } | ||
|
|
||
| private function flagMetadata($rawResult, $spanEnrichmentEnabled) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🤖 Comment from Claude working fully autonomously overnight: Heads up: OpenFeature users do not get this metadata key. The PR description says "Both the native client and OpenFeature provider use this evaluator", which is true of the evaluator. But No action is needed for the system-test contract, because the PHP test adapter uses the native Possible follow-up, after open-feature/sdk releases withMetadata() (sketch, not run) if (method_exists($builder, 'withMetadata') && $details->getFlagMetadata() !== array()) {
$builder->withMetadata($details->getFlagMetadata());
} |
||
| { | ||
| $metadata = $this->readArray($rawResult, array('flag_metadata', 'flagMetadata', 'metadata')); | ||
| $serialId = $this->read($rawResult, array('serial_id', 'serialId'), null); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🤖 Comment from Claude working fully autonomously overnight: [P3] Optional nit: check the gate before you read the serial ID. Today Suggested change private function flagMetadata($rawResult, $spanEnrichmentEnabled)
{
$metadata = $this->readArray($rawResult, array('flag_metadata', 'flagMetadata', 'metadata'));
if (!$spanEnrichmentEnabled) {
return $metadata;
}
$serialId = $this->read($rawResult, array('serial_id', 'serialId'), null);
if ($serialId !== null) {
$metadata[self::SERIAL_ID_METADATA_KEY] = (int) $serialId;
}
return $metadata;
}The behavior does not change: zero is kept, and null or missing IDs are left out. I applied only this method on this head. The three FeatureFlags PHPUnit test classes pass, including the new |
||
| 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')); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,6 +10,37 @@ | |
|
|
||
| final class ResultMapperTest extends TestCase | ||
| { | ||
| /** | ||
| * @dataProvider serialIdMetadataProvider | ||
| */ | ||
| public function testSerialIdFlagMetadata($serialFields, $enabled, $expectedMetadata) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🤖 Comment from Claude working fully autonomously overnight: [P2] Please re-run the 5 serial-ID system tests at this head, and add the receipt to this PR's Validation section. The system-test result in this PR is for an earlier commit. The companion system-tests PR, DataDog/system-tests#7857, names SDK commit The run also used the 1.25.1 release extension with patched PHP sources. That is a valid approach, but the receipt should say so, with the commit and the system-tests revision. I do not expect a different result. I ran a direct probe at this head (1.26.2 release extension plus this PR's sources), and it shows the expected metadata for a positive ID, zero, a missing ID, and the gate off. Suggested receipt for the Validation sectionThe command is the one from DataDog/system-tests#7857. Fill in the angle-bracket fields from the new run. |
||
| { | ||
| $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( | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🤖 Comment from Claude working fully autonomously overnight:
Reminder: When the system-tests PHP manifest enables
test_serial_id_metadata.py(today the entry ismissing_feature), use the first dd-trace-php release that contains this commit. The latest release (1.26.2) and current master do not add__dd_split_serial_id.The adapter change in DataDog/system-tests#7857 (
(object) $details->getFlagMetadata()) has to merge before that. On system-testsmain, the PHP adapter sends$details->getFlagMetadata()without the cast. An empty PHP array encodes as[], and the test requires a map or null, sotest_serial_id_absent_when_split_has_no_serial_idwould fail. The cast does not depend on this PR, becausegetFlagMetadata()returns an array before and after it. So #7857 and this PR can merge in either order. No code change is needed here.