Skip to content

Commit b2d669e

Browse files
authored
Merge pull request #3996 from DataDog/leo.romanovsky/ffe-apm-span-enrichment
feat(FeatureFlags): FFE APM feature-flag span enrichment (experimental, gated)
2 parents 39036dc + bf321e6 commit b2d669e

23 files changed

Lines changed: 1191 additions & 13 deletions

components-rs/common.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -486,6 +486,8 @@ typedef struct ddog_FfeResult {
486486
_zend_string * allocation_key;
487487
int32_t reason;
488488
int32_t error_code;
489+
int32_t serial_id;
490+
bool has_serial_id;
489491
bool do_log;
490492
bool valid;
491493
} ddog_FfeResult;

components-rs/ffe.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,14 @@ pub struct FfeResult {
9797
pub allocation_key: MaybeOwnedZendString,
9898
pub reason: i32,
9999
pub error_code: i32,
100+
// serial_id is the selected split's serial id, carried for APM span
101+
// enrichment (ffe_flags_enc). The source field is Option<i32>; since the
102+
// C ABI cannot represent Option<i32> as a plain field, we surface the
103+
// presence separately via has_serial_id. Consumers MUST gate on
104+
// has_serial_id (the Pattern B "missing variant => default" semantic) and
105+
// never treat serial_id == 0 as "absent".
106+
pub serial_id: i32,
107+
pub has_serial_id: bool,
100108
pub do_log: bool,
101109
pub valid: bool,
102110
}
@@ -220,6 +228,8 @@ fn result_from_assignment(assignment: Result<ffe::Assignment, EvaluationError>)
220228
AssignmentReason::Default => REASON_DEFAULT,
221229
},
222230
error_code: ERROR_NONE,
231+
serial_id: assignment.serial_id.unwrap_or(0),
232+
has_serial_id: assignment.serial_id.is_some(),
223233
do_log: assignment.do_log,
224234
valid: true,
225235
}
@@ -244,6 +254,8 @@ fn result_from_assignment(assignment: Result<ffe::Assignment, EvaluationError>)
244254
allocation_key: None,
245255
reason,
246256
error_code,
257+
serial_id: 0,
258+
has_serial_id: false,
247259
do_log: false,
248260
valid: true,
249261
}
@@ -258,6 +270,8 @@ fn invalid_result() -> FfeResult {
258270
allocation_key: None,
259271
reason: REASON_ERROR,
260272
error_code: ERROR_GENERAL,
273+
serial_id: 0,
274+
has_serial_id: false,
261275
do_log: false,
262276
valid: false,
263277
}

ext/configuration.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,8 @@ enum datadog_sidecar_connection_mode {
109109
CONFIG(DOUBLE, DD_REMOTE_CONFIG_POLL_INTERVAL_SECONDS, "5.0", .ini_change = zai_config_system_ini_change) \
110110
CONFIG(BOOL, DD_REMOTE_CONFIG_ENABLED, "true", .ini_change = zai_config_system_ini_change) \
111111
CONFIG(INT, DD_TRACE_RETRY_INTERVAL, "100", .ini_change = zai_config_system_ini_change) \
112-
CONFIG(BOOL, DD_EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED, "true")
112+
CONFIG(BOOL, DD_EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED, "true") \
113+
CONFIG(BOOL, DD_EXPERIMENTAL_FLAGGING_PROVIDER_SPAN_ENRICHMENT_ENABLED, "false")
113114

114115
#define DD_CONFIGURATIONS_ONLY
115116
#ifdef DDTRACE

metadata/supported-configurations.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -410,6 +410,13 @@
410410
"default": "false"
411411
}
412412
],
413+
"DD_EXPERIMENTAL_FLAGGING_PROVIDER_SPAN_ENRICHMENT_ENABLED": [
414+
{
415+
"implementation": "A",
416+
"type": "boolean",
417+
"default": "false"
418+
}
419+
],
413420
"DD_EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED": [
414421
{
415422
"implementation": "B",

src/DDTrace/OpenFeature/DataDogProvider.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,12 +111,15 @@ private function resolve(
111111
mixed $defaultValue,
112112
?EvaluationContext $context
113113
): ResolutionDetailsInterface {
114-
$details = $this->evaluate($flagKey, $expectedType, $defaultValue, $this->normalizeContext($context));
114+
$normalizedContext = $this->normalizeContext($context);
115+
$details = $this->evaluate($flagKey, $expectedType, $defaultValue, $normalizedContext);
115116
$this->warnIfNonProductionRuntime($details);
116117
// The PHP OpenFeature SDK does not pass ResolutionDetails to finally
117118
// hooks, so PHP records metrics here after native evaluation has the
118119
// final provider result.
119120
$this->recordEvaluationMetric($flagKey, $details);
121+
// APM span enrichment is recorded inside NativeEvaluator::evaluate()
122+
// (the shared choke point), so there is nothing to do here.
120123

121124
$builder = (new ResolutionDetailsBuilder())
122125
->withValue($details->getValue())

src/api/FeatureFlags/Internal/NativeEvaluator.php

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace DDTrace\FeatureFlags\Internal;
44

55
use DDTrace\FeatureFlags\EvaluationType;
6+
use DDTrace\FeatureFlags\SpanEnrichmentRegistry;
67

78
final class NativeEvaluator implements Evaluator
89
{
@@ -50,7 +51,17 @@ public function evaluate(
5051
$rawResult = $this->withProviderState($rawResult);
5152
}
5253

53-
return $this->mapper->map($rawResult, $expectedType, $defaultValue);
54+
$details = $this->mapper->map($rawResult, $expectedType, $defaultValue);
55+
56+
// APM feature-flag span enrichment. This is the single choke point both
57+
// the native Client and the OpenFeature DataDogProvider evaluate through,
58+
// and it is only reachable with the extension loaded (UnavailableEvaluator
59+
// is returned otherwise), so enrichment is recorded here once rather than
60+
// duplicated in each caller. No-op when the gate is off or there is no
61+
// active root span; never throws into evaluation.
62+
SpanEnrichmentRegistry::record($flagKey, $details, $targetingKey);
63+
64+
return $details;
5465
}
5566

5667
private function typeId($expectedType)

src/api/FeatureFlags/Internal/ResultMapper.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,19 @@ private function exposureData($rawResult)
274274
$exposureData['doLog'] = (bool) $this->read($rawResult, array('do_log', 'doLog'), false);
275275
}
276276

277+
// serialId is the selected split's serial id, surfaced from the native
278+
// bridge for APM span enrichment. It is only present when the native
279+
// result actually carried one; a null/absent value must be left out
280+
// entirely rather than defaulted to 0/false. Absence alone does NOT
281+
// mean a runtime default was used -- a waterfall-assigned variant can
282+
// also have no serial id. SpanEnrichmentRegistry::record() only
283+
// treats an evaluation as a runtime default when BOTH serialId AND the
284+
// variant are absent.
285+
$serialId = $this->read($rawResult, array('serial_id', 'serialId'), null);
286+
if ($serialId !== null) {
287+
$exposureData['serialId'] = (int) $serialId;
288+
}
289+
277290
return $exposureData;
278291
}
279292

0 commit comments

Comments
 (0)