From 224c241208532a6b16b1ea907e4638516201a50d Mon Sep 17 00:00:00 2001 From: Leo Romanovsky Date: Wed, 27 May 2026 23:26:15 -0400 Subject: [PATCH 1/8] test(ffe): enable PHP evaluation system tests --- docs/understand/weblogs/end-to-end_weblog.md | 14 ++ manifests/php.yml | 6 +- .../test_ffe/test_dynamic_evaluation.py | 15 +- utils/build/docker/php/common/ffe.php | 209 ++++++++++++++++++ .../docker/php/common/rewrite-rules.conf | 1 + utils/build/docker/php/parametric/server.php | 154 +++++++++++++ .../_test_clients/_test_client_parametric.py | 12 +- 7 files changed, 401 insertions(+), 10 deletions(-) create mode 100644 utils/build/docker/php/common/ffe.php diff --git a/docs/understand/weblogs/end-to-end_weblog.md b/docs/understand/weblogs/end-to-end_weblog.md index 08dabf6b078..ff88e4c623e 100644 --- a/docs/understand/weblogs/end-to-end_weblog.md +++ b/docs/understand/weblogs/end-to-end_weblog.md @@ -1026,6 +1026,20 @@ The endpoint must accept a query string parameter `code`, which should be an int This endpoint is used for client-stats tests to provide a separate "resource" via the endpoint path `stats-unique` to disambiguate those tests from other stats generating tests. +### POST /ffe + +This endpoint is used by the Feature Flags & Experimentation scenario. It must +accept a JSON body with these fields: + +- `flag`: the feature flag key to evaluate. +- `variationType`: the expected variation type. +- `defaultValue`: the value to return when evaluation cannot resolve the flag. +- `targetingKey`: the evaluation subject key. +- `attributes`: flat scalar targeting attributes. + +The response must be JSON and include at least `value` and `reason`. Error +responses should also include `errorCode` and `errorMessage`. + ### GET /healthcheck Returns a JSON dict, with those values : diff --git a/manifests/php.yml b/manifests/php.yml index d0c49bb846b..df06f030515 100644 --- a/manifests/php.yml +++ b/manifests/php.yml @@ -605,7 +605,7 @@ manifest: component_version: <1.12.0 tests/docker_ssi/test_docker_ssi_appsec.py::TestDockerSSIAppsecFeatures::test_telemetry_source_ssi: v1.8.3 tests/docker_ssi/test_docker_ssi_crash.py::TestDockerSSICrash::test_crash: missing_feature (No implemented the endpoint /crashme) - tests/ffe/test_dynamic_evaluation.py: missing_feature + tests/ffe/test_dynamic_evaluation.py: v1.20.0-dev tests/ffe/test_exposures.py: missing_feature tests/ffe/test_flag_eval_metrics.py: missing_feature tests/integration_frameworks/llm/anthropic/test_anthropic_llmobs.py::TestAnthropicLlmObsMessages::test_create_error: bug (MLOB-1234) @@ -730,7 +730,7 @@ manifest: tests/parametric/test_dynamic_configuration.py::TestDynamicConfigV1_ServiceTargets::test_not_match_service_target: missing_feature tests/parametric/test_dynamic_configuration.py::TestDynamicConfigV2: '>=1.16.0' tests/parametric/test_dynamic_configuration.py::TestDynamicConfigV2::test_tracing_client_tracing_tags: missing_feature - tests/parametric/test_ffe/test_dynamic_evaluation.py::Test_Feature_Flag_Dynamic_Evaluation: missing_feature + tests/parametric/test_ffe/test_dynamic_evaluation.py::Test_Feature_Flag_Dynamic_Evaluation: v1.20.0-dev tests/parametric/test_headers_b3.py::Test_Headers_B3::test_headers_b3_migrated_extract_invalid: - declaration: missing_feature (Need to remove b3=b3multi alias) component_version: <1.16.0 @@ -868,7 +868,7 @@ manifest: tests/parametric/test_parametric_endpoints.py::Test_Parametric_DDSpan_Start: v1.13.0+4663b2fa7c20c6920f347d059b57dc2a419cb7f7 tests/parametric/test_parametric_endpoints.py::Test_Parametric_DDTrace_Baggage: missing_feature (baggage is not supported) tests/parametric/test_parametric_endpoints.py::Test_Parametric_DDTrace_Current_Span: bug (APMAPI-778) # current span endpoint should return span and trace id of zero if no span is "active" - tests/parametric/test_parametric_endpoints.py::Test_Parametric_FFE_Start: missing_feature + tests/parametric/test_parametric_endpoints.py::Test_Parametric_FFE_Start: v1.20.0-dev tests/parametric/test_parametric_endpoints.py::Test_Parametric_Otel_Baggage: missing_feature (otel baggage is not supported) tests/parametric/test_parametric_endpoints.py::Test_Parametric_Otel_Current_Span: bug (APMAPI-778) # otel current span endpoint should return a span and trace id of zero if no span is "active" tests/parametric/test_parametric_endpoints.py::Test_Parametric_Write_Log: missing_feature diff --git a/tests/parametric/test_ffe/test_dynamic_evaluation.py b/tests/parametric/test_ffe/test_dynamic_evaluation.py index fe57f6075a5..99831a0ccc9 100644 --- a/tests/parametric/test_ffe/test_dynamic_evaluation.py +++ b/tests/parametric/test_ffe/test_dynamic_evaluation.py @@ -76,7 +76,16 @@ def _set_and_wait_ffe_rc( test_agent.set_remote_config(path=f"{RC_PATH}/{config_id}/config", payload=rc_config) # Wait for RC acknowledgment - return test_agent.wait_for_rc_apply_state(RC_PRODUCT, state=RemoteConfigApplyState.ACKNOWLEDGED, clear=True) + return test_agent.wait_for_rc_apply_state( + RC_PRODUCT, state=RemoteConfigApplyState.ACKNOWLEDGED, clear=True, wait_loops=500 + ) + + +def _start_ffe_provider(test_library: APMLibrary) -> bool: + # The PHP parametric server is one long-lived CLI request, so load the same + # canonical UFC fixture directly there while this test still requires RC ACK. + configuration = UFC_FIXTURE_DATA if test_library.lang == "php" else None + return test_library.ffe_start(configuration) @scenarios.parametric @@ -129,7 +138,7 @@ def test_ffe_flag_evaluation(self, test_case_file: str, test_agent: TestAgentAPI _set_and_wait_ffe_rc(test_agent, UFC_FIXTURE_DATA) # Initialize FFE provider - success = test_library.ffe_start() + success = _start_ffe_provider(test_library) assert success, "Failed to start FFE provider" # Run each test case @@ -171,7 +180,7 @@ def test_ffe_of7_empty_targeting_key(self, test_agent: TestAgentAPI, test_librar _set_and_wait_ffe_rc(test_agent, UFC_FIXTURE_DATA) # Initialize FFE provider - success = test_library.ffe_start() + success = _start_ffe_provider(test_library) assert success, "Failed to start FFE provider" # Evaluate flag with empty targeting key diff --git a/utils/build/docker/php/common/ffe.php b/utils/build/docker/php/common/ffe.php new file mode 100644 index 00000000000..e4230d21691 --- /dev/null +++ b/utils/build/docker/php/common/ffe.php @@ -0,0 +1,209 @@ + null, + 'reason' => 'ERROR', + 'variant' => null, + 'errorCode' => $errorCode, + 'errorMessage' => $errorMessage, + 'providerState' => array('ready' => false), + )); +} + +function dd_ffe_read_payload() +{ + $rawBody = file_get_contents('php://input'); + if ($rawBody === false || $rawBody === '') { + return array(); + } + + $payload = json_decode($rawBody, true); + if (json_last_error() !== JSON_ERROR_NONE || !is_array($payload)) { + dd_ffe_error_response(400, 'INVALID_REQUEST', 'Expected a JSON object request body.'); + exit; + } + + return $payload; +} + +function dd_ffe_normalized_variation_type($variationType) +{ + return strtoupper(str_replace('-', '_', (string) $variationType)); +} + +function dd_ffe_normalize_default_value($defaultValue, $variationType) +{ + switch (dd_ffe_normalized_variation_type($variationType)) { + case 'BOOLEAN': + return is_bool($defaultValue) ? $defaultValue : (bool) $defaultValue; + case 'STRING': + return is_string($defaultValue) ? $defaultValue : (string) $defaultValue; + case 'INTEGER': + return is_int($defaultValue) ? $defaultValue : (int) $defaultValue; + case 'NUMERIC': + case 'FLOAT': + case 'DOUBLE': + return is_int($defaultValue) || is_float($defaultValue) ? $defaultValue : (float) $defaultValue; + case 'JSON': + case 'OBJECT': + return is_array($defaultValue) ? $defaultValue : array(); + default: + return $defaultValue; + } +} + +function dd_ffe_scalar_attributes(array $attributes) +{ + $normalized = array(); + foreach ($attributes as $key => $value) { + if (is_bool($value) || is_int($value) || is_float($value) || is_string($value)) { + $normalized[(string) $key] = $value; + } + } + + return $normalized; +} + +function dd_ffe_evaluate_with_client($flagKey, $variationType, $defaultValue, $targetingKey, array $attributes) +{ + if (!class_exists('\\DDTrace\\FeatureFlags\\Client')) { + return null; + } + + if (method_exists('\\DDTrace\\FeatureFlags\\Client', 'create')) { + $client = \DDTrace\FeatureFlags\Client::create(); + } else { + $client = new \DDTrace\FeatureFlags\Client(); + } + + $context = array( + 'targetingKey' => $targetingKey, + 'attributes' => $attributes, + ); + + switch (dd_ffe_normalized_variation_type($variationType)) { + case 'BOOLEAN': + return $client->getBooleanDetails($flagKey, $defaultValue, $context); + case 'STRING': + return $client->getStringDetails($flagKey, $defaultValue, $context); + case 'INTEGER': + return $client->getIntegerDetails($flagKey, $defaultValue, $context); + case 'NUMERIC': + case 'FLOAT': + case 'DOUBLE': + return $client->getFloatDetails($flagKey, $defaultValue, $context); + case 'JSON': + case 'OBJECT': + return $client->getObjectDetails($flagKey, is_array($defaultValue) ? $defaultValue : array(), $context); + default: + throw new InvalidArgumentException('Unsupported variationType: ' . (string) $variationType); + } +} + +function dd_ffe_warning_handler($severity, $message) +{ + if ($severity === E_USER_WARNING && strpos($message, 'Datadog-backed PHP feature flag evaluation') !== false) { + return true; + } + + return false; +} + +function dd_ffe_evaluate($flagKey, $variationType, $defaultValue, $targetingKey, array $attributes) +{ + set_error_handler('dd_ffe_warning_handler'); + try { + return dd_ffe_evaluate_with_client($flagKey, $variationType, $defaultValue, $targetingKey, $attributes); + } finally { + restore_error_handler(); + } +} + +function dd_ffe_details_payload($details) +{ + $payload = array( + 'value' => $details->getValue(), + 'reason' => $details->getReason(), + 'variant' => $details->getVariant(), + 'errorCode' => $details->getErrorCode(), + 'errorMessage' => $details->getErrorMessage(), + 'flagMetadata' => $details->getFlagMetadata(), + 'exposureData' => $details->getExposureData(), + 'providerState' => $details->getProviderState(), + ); + + if (method_exists($details, 'getValueType')) { + $payload['valueType'] = $details->getValueType(); + } + + return $payload; +} + +$payload = dd_ffe_read_payload(); + +if (!isset($payload['flag']) || !is_string($payload['flag']) || $payload['flag'] === '') { + dd_ffe_error_response(400, 'INVALID_REQUEST', 'Expected non-empty string field: flag.'); + exit; +} + +if (!array_key_exists('variationType', $payload) || !is_string($payload['variationType'])) { + dd_ffe_error_response(400, 'INVALID_REQUEST', 'Expected string field: variationType.'); + exit; +} + +if (!array_key_exists('defaultValue', $payload)) { + dd_ffe_error_response(400, 'INVALID_REQUEST', 'Expected field: defaultValue.'); + exit; +} + +$flagKey = $payload['flag']; +$variationType = $payload['variationType']; +$defaultValue = dd_ffe_normalize_default_value($payload['defaultValue'], $variationType); +$targetingKey = isset($payload['targetingKey']) && $payload['targetingKey'] !== null + ? (string) $payload['targetingKey'] + : null; +$attributes = isset($payload['attributes']) && is_array($payload['attributes']) + ? dd_ffe_scalar_attributes($payload['attributes']) + : array(); + +try { + $details = dd_ffe_evaluate($flagKey, $variationType, $defaultValue, $targetingKey, $attributes); + if ($details !== null) { + dd_ffe_json_response(200, dd_ffe_details_payload($details)); + return; + } +} catch (Throwable $exception) { + dd_ffe_json_response(200, array( + 'value' => $defaultValue, + 'reason' => 'ERROR', + 'variant' => null, + 'errorCode' => 'PROVIDER_NOT_READY', + 'errorMessage' => $exception->getMessage(), + 'providerState' => array( + 'ready' => false, + 'productionRuntime' => false, + ), + )); + return; +} + +dd_ffe_json_response(200, array( + 'value' => $defaultValue, + 'reason' => 'ERROR', + 'variant' => null, + 'errorCode' => 'PROVIDER_NOT_READY', + 'errorMessage' => 'Datadog-backed PHP feature flag evaluation is not wired in this weblog yet.', + 'providerState' => array( + 'ready' => false, + 'productionRuntime' => false, + ), +)); diff --git a/utils/build/docker/php/common/rewrite-rules.conf b/utils/build/docker/php/common/rewrite-rules.conf index 1191ca1fc4c..e55c8981dde 100644 --- a/utils/build/docker/php/common/rewrite-rules.conf +++ b/utils/build/docker/php/common/rewrite-rules.conf @@ -34,6 +34,7 @@ RewriteRule "^/trace/mongo$" "/trace_mongo/" RewriteRule "^/e2e_otel_span$" "/e2e_otel_span/" RewriteRule "^/e2e_single_span$" "/e2e_single_span/" RewriteRule "^/crashme$" "/crashme/" +RewriteRule "^/ffe$" "/ffe/" RewriteRule "^/exceptionreplay/(.*)$" "/debugger/exceptionreplay/$1" [QSA] RewriteRule "^/llm$" "/llm/" RewriteRule "^/stats-unique$" "/stats-unique/" diff --git a/utils/build/docker/php/parametric/server.php b/utils/build/docker/php/parametric/server.php index 60240250a42..1548de54de5 100644 --- a/utils/build/docker/php/parametric/server.php +++ b/utils/build/docker/php/parametric/server.php @@ -61,6 +61,91 @@ function arg($req, $arg) { return ($buffer[$req] ??= json_decode($req->getBody()->buffer(), true))[$arg] ?? null; } +function ffeNormalizeDefaultValue($defaultValue, $variationType) { + switch (strtoupper((string)$variationType)) { + case 'BOOLEAN': + return is_bool($defaultValue) ? $defaultValue : (bool)$defaultValue; + case 'STRING': + return is_string($defaultValue) ? $defaultValue : (string)$defaultValue; + case 'INTEGER': + return is_int($defaultValue) ? $defaultValue : (int)$defaultValue; + case 'NUMERIC': + case 'FLOAT': + case 'DOUBLE': + return is_int($defaultValue) || is_float($defaultValue) ? $defaultValue : (float)$defaultValue; + case 'JSON': + case 'OBJECT': + return is_array($defaultValue) ? $defaultValue : []; + default: + return $defaultValue; + } +} + +function ffeEvaluate($client, $flagKey, $variationType, $defaultValue, $targetingKey, array $attributes) { + $context = [ + 'targetingKey' => $targetingKey, + 'attributes' => $attributes, + ]; + + switch (strtoupper((string)$variationType)) { + case 'BOOLEAN': + return $client->getBooleanDetails($flagKey, $defaultValue, $context); + case 'STRING': + return $client->getStringDetails($flagKey, $defaultValue, $context); + case 'INTEGER': + return $client->getIntegerDetails($flagKey, $defaultValue, $context); + case 'NUMERIC': + case 'FLOAT': + case 'DOUBLE': + return $client->getFloatDetails($flagKey, $defaultValue, $context); + case 'JSON': + case 'OBJECT': + return $client->getObjectDetails($flagKey, $defaultValue, $context); + default: + throw new InvalidArgumentException('Unsupported variationType: ' . (string)$variationType); + } +} + +function ffeDetailsPayload($details) { + $value = $details->getValue(); + if (method_exists($details, 'getValueType') && $details->getValueType() === 'object') { + $value = ffeJsonResponseValue($value); + } + + $payload = [ + 'value' => $value, + 'reason' => $details->getReason(), + 'variant' => $details->getVariant(), + 'errorCode' => $details->getErrorCode(), + 'errorMessage' => $details->getErrorMessage(), + 'flagMetadata' => $details->getFlagMetadata(), + 'exposureData' => $details->getExposureData(), + 'providerState' => $details->getProviderState(), + ]; + + if (method_exists($details, 'getValueType')) { + $payload['valueType'] = $details->getValueType(); + } + + return $payload; +} + +function ffeJsonResponseValue($value) { + if (!is_array($value)) { + return $value; + } + + if ($value === []) { + return new stdClass(); + } + + foreach ($value as $key => $nestedValue) { + $value[$key] = ffeJsonResponseValue($nestedValue); + } + + return $value; +} + // Source: https://magp.ie/2015/09/30/convert-large-integer-to-hexadecimal-without-php-math-extension/ function convertBase16ToBase10($numString) { @@ -123,6 +208,8 @@ function remappedSpanKind($spanKind) { $spansDistributedTracingHeaders = []; /** @var Logger[] $loggerDict */ $loggerDict = []; +/** @var ?\DDTrace\FeatureFlags\Client $ffeClient */ +$ffeClient = null; // Construct the OTel LoggerProvider directly when DD_LOGS_OTEL_ENABLED=true. // Mirrors how a user would wire up OTLP logs export themselves. dd-trace-php's @@ -151,6 +238,73 @@ function remappedSpanKind($spanKind) { } $router = new Router($server, $logger, $errorHandler); +$router->addRoute('POST', '/ffe/start', new ClosureRequestHandler(function (Request $req) use (&$ffeClient) { + if (!class_exists('\\DDTrace\\FeatureFlags\\Client')) { + return new Response(status: 500, headers: ['content-type' => 'application/json'], body: json_encode([ + 'success' => false, + 'message' => 'DDTrace\\FeatureFlags\\Client is not available', + ])); + } + + $configuration = arg($req, 'configuration'); + if ($configuration !== null) { + if (!function_exists('\\DDTrace\\Testing\\ffe_load_config')) { + return new Response(status: 500, headers: ['content-type' => 'application/json'], body: json_encode([ + 'success' => false, + 'message' => 'DDTrace\\Testing\\ffe_load_config is not available', + ])); + } + + $configurationJson = json_encode($configuration); + if (!is_string($configurationJson) || !\DDTrace\Testing\ffe_load_config($configurationJson)) { + return new Response(status: 500, headers: ['content-type' => 'application/json'], body: json_encode([ + 'success' => false, + 'message' => 'Failed to load FFE test configuration', + ])); + } + } + + $ffeClient = new \DDTrace\FeatureFlags\Client(); + + return jsonResponse(['success' => true]); +})); +$router->addRoute('POST', '/ffe/evaluate', new ClosureRequestHandler(function (Request $req) use (&$ffeClient) { + if ($ffeClient === null) { + $ffeClient = new \DDTrace\FeatureFlags\Client(); + } + + $variationType = arg($req, 'variationType'); + $defaultValue = ffeNormalizeDefaultValue(arg($req, 'defaultValue'), $variationType); + $targetingKey = arg($req, 'targetingKey'); + if ($targetingKey !== null) { + $targetingKey = (string)$targetingKey; + } + $attributes = arg($req, 'attributes') ?? []; + if (!is_array($attributes)) { + $attributes = []; + } + + try { + $details = ffeEvaluate( + $ffeClient, + arg($req, 'flag'), + $variationType, + $defaultValue, + $targetingKey, + $attributes + ); + + return jsonResponse(ffeDetailsPayload($details)); + } catch (Throwable $e) { + return jsonResponse([ + 'value' => $defaultValue, + 'reason' => 'ERROR', + 'variant' => null, + 'errorCode' => 'GENERAL', + 'errorMessage' => $e->getMessage(), + ]); + } +})); $router->addRoute('POST', '/trace/span/start', new ClosureRequestHandler(function (Request $req) use (&$spans, &$activeSpan, &$spansDistributedTracingHeaders) { if ($parent = arg($req, 'parent_id')) { if (isset($spans[$parent])) { diff --git a/utils/docker_fixtures/_test_clients/_test_client_parametric.py b/utils/docker_fixtures/_test_clients/_test_client_parametric.py index 34de1039feb..9758f25014d 100644 --- a/utils/docker_fixtures/_test_clients/_test_client_parametric.py +++ b/utils/docker_fixtures/_test_clients/_test_client_parametric.py @@ -737,14 +737,18 @@ def otel_current_span(self) -> _TestOtelSpan | None: return _TestOtelSpan(self, span_response["span_id"], span_response["trace_id"]) - def ffe_start(self) -> bool: + def ffe_start(self, configuration: dict | None = None) -> bool: """Initialize the FFE (Feature Flagging & Experimentation) provider. Returns: bool: True if the provider was initialized successfully, False otherwise """ - resp = self._session.post(self._url("/ffe/start"), json={}) + payload = {} + if configuration is not None: + payload["configuration"] = configuration + + resp = self._session.post(self._url("/ffe/start"), json=payload) return HTTPStatus(resp.status_code).is_success def ffe_evaluate( @@ -1163,9 +1167,9 @@ def write_log( ) -> bool: return self._client.write_log(logger_name, level, message, span_id=span_id) - def ffe_start(self) -> bool: + def ffe_start(self, configuration: dict | None = None) -> bool: """Initialize the FFE (Feature Flagging & Experimentation) provider.""" - return self._client.ffe_start() + return self._client.ffe_start(configuration) def ffe_evaluate( self, From 9bcf43d464bdc1876c7b484c8bdd1e3402c591be Mon Sep 17 00:00:00 2001 From: Leo Romanovsky Date: Thu, 28 May 2026 11:38:12 -0400 Subject: [PATCH 2/8] Fix PHP FFE test activation versions --- manifests/php.yml | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/manifests/php.yml b/manifests/php.yml index 23544f7cb31..d480453a9b7 100644 --- a/manifests/php.yml +++ b/manifests/php.yml @@ -605,7 +605,11 @@ manifest: component_version: <1.12.0 tests/docker_ssi/test_docker_ssi_appsec.py::TestDockerSSIAppsecFeatures::test_telemetry_source_ssi: v1.8.3 tests/docker_ssi/test_docker_ssi_crash.py::TestDockerSSICrash::test_crash: missing_feature (No implemented the endpoint /crashme) - tests/ffe/test_dynamic_evaluation.py: v1.20.0-dev + tests/ffe/test_dynamic_evaluation.py: + - declaration: missing_feature (declared version for php is v1.21.0-dev) + component_version: "<1.21.0-dev" + - declaration: missing_feature (DDTrace FeatureFlags runtime is not in this dev build) + component_version: 1.21.0+c197795aa7d7d4e52819b5ff35c2e02e89841bfd tests/ffe/test_exposures.py: missing_feature tests/ffe/test_flag_eval_metrics.py: missing_feature tests/integration_frameworks/llm/anthropic/test_anthropic_llmobs.py::TestAnthropicLlmObsMessages::test_create_error: bug (MLOB-1234) @@ -731,7 +735,11 @@ manifest: tests/parametric/test_dynamic_configuration.py::TestDynamicConfigV1_ServiceTargets::test_not_match_service_target: missing_feature tests/parametric/test_dynamic_configuration.py::TestDynamicConfigV2: '>=1.16.0' tests/parametric/test_dynamic_configuration.py::TestDynamicConfigV2::test_tracing_client_tracing_tags: missing_feature - tests/parametric/test_ffe/test_dynamic_evaluation.py::Test_Feature_Flag_Dynamic_Evaluation: v1.20.0-dev + tests/parametric/test_ffe/test_dynamic_evaluation.py::Test_Feature_Flag_Dynamic_Evaluation: + - declaration: missing_feature (declared version for php is v1.21.0-dev) + component_version: "<1.21.0-dev" + - declaration: missing_feature (DDTrace FeatureFlags runtime is not in this dev build) + component_version: 1.21.0+c197795aa7d7d4e52819b5ff35c2e02e89841bfd tests/parametric/test_headers_b3.py::Test_Headers_B3::test_headers_b3_migrated_extract_invalid: - declaration: missing_feature (Need to remove b3=b3multi alias) component_version: <1.16.0 @@ -869,7 +877,11 @@ manifest: tests/parametric/test_parametric_endpoints.py::Test_Parametric_DDSpan_Start: v1.13.0+4663b2fa7c20c6920f347d059b57dc2a419cb7f7 tests/parametric/test_parametric_endpoints.py::Test_Parametric_DDTrace_Baggage: missing_feature (baggage is not supported) tests/parametric/test_parametric_endpoints.py::Test_Parametric_DDTrace_Current_Span: bug (APMAPI-778) # current span endpoint should return span and trace id of zero if no span is "active" - tests/parametric/test_parametric_endpoints.py::Test_Parametric_FFE_Start: v1.20.0-dev + tests/parametric/test_parametric_endpoints.py::Test_Parametric_FFE_Start: + - declaration: missing_feature (declared version for php is v1.21.0-dev) + component_version: "<1.21.0-dev" + - declaration: missing_feature (DDTrace FeatureFlags runtime is not in this dev build) + component_version: 1.21.0+c197795aa7d7d4e52819b5ff35c2e02e89841bfd tests/parametric/test_parametric_endpoints.py::Test_Parametric_Otel_Baggage: missing_feature (otel baggage is not supported) tests/parametric/test_parametric_endpoints.py::Test_Parametric_Otel_Current_Span: bug (APMAPI-778) # otel current span endpoint should return a span and trace id of zero if no span is "active" tests/parametric/test_parametric_endpoints.py::Test_Parametric_Write_Log: missing_feature From e2d43701d6427cef362f9bf79f0f7c51f9fead51 Mon Sep 17 00:00:00 2001 From: Leo Romanovsky Date: Thu, 28 May 2026 11:44:32 -0400 Subject: [PATCH 3/8] Skip stale PHP FFE dev build --- manifests/php.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/manifests/php.yml b/manifests/php.yml index d480453a9b7..bead4bdb0b0 100644 --- a/manifests/php.yml +++ b/manifests/php.yml @@ -610,6 +610,8 @@ manifest: component_version: "<1.21.0-dev" - declaration: missing_feature (DDTrace FeatureFlags runtime is not in this dev build) component_version: 1.21.0+c197795aa7d7d4e52819b5ff35c2e02e89841bfd + - declaration: missing_feature (DDTrace FeatureFlags runtime is not in this dev build) + component_version: 1.21.0+e402ea5f89cfb3c20033c65138df860ba680dec2 tests/ffe/test_exposures.py: missing_feature tests/ffe/test_flag_eval_metrics.py: missing_feature tests/integration_frameworks/llm/anthropic/test_anthropic_llmobs.py::TestAnthropicLlmObsMessages::test_create_error: bug (MLOB-1234) @@ -740,6 +742,8 @@ manifest: component_version: "<1.21.0-dev" - declaration: missing_feature (DDTrace FeatureFlags runtime is not in this dev build) component_version: 1.21.0+c197795aa7d7d4e52819b5ff35c2e02e89841bfd + - declaration: missing_feature (DDTrace FeatureFlags runtime is not in this dev build) + component_version: 1.21.0+e402ea5f89cfb3c20033c65138df860ba680dec2 tests/parametric/test_headers_b3.py::Test_Headers_B3::test_headers_b3_migrated_extract_invalid: - declaration: missing_feature (Need to remove b3=b3multi alias) component_version: <1.16.0 @@ -882,6 +886,8 @@ manifest: component_version: "<1.21.0-dev" - declaration: missing_feature (DDTrace FeatureFlags runtime is not in this dev build) component_version: 1.21.0+c197795aa7d7d4e52819b5ff35c2e02e89841bfd + - declaration: missing_feature (DDTrace FeatureFlags runtime is not in this dev build) + component_version: 1.21.0+e402ea5f89cfb3c20033c65138df860ba680dec2 tests/parametric/test_parametric_endpoints.py::Test_Parametric_Otel_Baggage: missing_feature (otel baggage is not supported) tests/parametric/test_parametric_endpoints.py::Test_Parametric_Otel_Current_Span: bug (APMAPI-778) # otel current span endpoint should return a span and trace id of zero if no span is "active" tests/parametric/test_parametric_endpoints.py::Test_Parametric_Write_Log: missing_feature From 19b9c41c2f18ed1b3c73ca0e219067ab2cba3b5d Mon Sep 17 00:00:00 2001 From: Leo Romanovsky Date: Thu, 28 May 2026 13:25:52 -0400 Subject: [PATCH 4/8] Bump PHP FFE tests to 1.21 dev --- manifests/php.yml | 24 +++--------------------- 1 file changed, 3 insertions(+), 21 deletions(-) diff --git a/manifests/php.yml b/manifests/php.yml index bead4bdb0b0..04c5c7720fc 100644 --- a/manifests/php.yml +++ b/manifests/php.yml @@ -605,13 +605,7 @@ manifest: component_version: <1.12.0 tests/docker_ssi/test_docker_ssi_appsec.py::TestDockerSSIAppsecFeatures::test_telemetry_source_ssi: v1.8.3 tests/docker_ssi/test_docker_ssi_crash.py::TestDockerSSICrash::test_crash: missing_feature (No implemented the endpoint /crashme) - tests/ffe/test_dynamic_evaluation.py: - - declaration: missing_feature (declared version for php is v1.21.0-dev) - component_version: "<1.21.0-dev" - - declaration: missing_feature (DDTrace FeatureFlags runtime is not in this dev build) - component_version: 1.21.0+c197795aa7d7d4e52819b5ff35c2e02e89841bfd - - declaration: missing_feature (DDTrace FeatureFlags runtime is not in this dev build) - component_version: 1.21.0+e402ea5f89cfb3c20033c65138df860ba680dec2 + tests/ffe/test_dynamic_evaluation.py: v1.21.0-dev tests/ffe/test_exposures.py: missing_feature tests/ffe/test_flag_eval_metrics.py: missing_feature tests/integration_frameworks/llm/anthropic/test_anthropic_llmobs.py::TestAnthropicLlmObsMessages::test_create_error: bug (MLOB-1234) @@ -737,13 +731,7 @@ manifest: tests/parametric/test_dynamic_configuration.py::TestDynamicConfigV1_ServiceTargets::test_not_match_service_target: missing_feature tests/parametric/test_dynamic_configuration.py::TestDynamicConfigV2: '>=1.16.0' tests/parametric/test_dynamic_configuration.py::TestDynamicConfigV2::test_tracing_client_tracing_tags: missing_feature - tests/parametric/test_ffe/test_dynamic_evaluation.py::Test_Feature_Flag_Dynamic_Evaluation: - - declaration: missing_feature (declared version for php is v1.21.0-dev) - component_version: "<1.21.0-dev" - - declaration: missing_feature (DDTrace FeatureFlags runtime is not in this dev build) - component_version: 1.21.0+c197795aa7d7d4e52819b5ff35c2e02e89841bfd - - declaration: missing_feature (DDTrace FeatureFlags runtime is not in this dev build) - component_version: 1.21.0+e402ea5f89cfb3c20033c65138df860ba680dec2 + tests/parametric/test_ffe/test_dynamic_evaluation.py::Test_Feature_Flag_Dynamic_Evaluation: v1.21.0-dev tests/parametric/test_headers_b3.py::Test_Headers_B3::test_headers_b3_migrated_extract_invalid: - declaration: missing_feature (Need to remove b3=b3multi alias) component_version: <1.16.0 @@ -881,13 +869,7 @@ manifest: tests/parametric/test_parametric_endpoints.py::Test_Parametric_DDSpan_Start: v1.13.0+4663b2fa7c20c6920f347d059b57dc2a419cb7f7 tests/parametric/test_parametric_endpoints.py::Test_Parametric_DDTrace_Baggage: missing_feature (baggage is not supported) tests/parametric/test_parametric_endpoints.py::Test_Parametric_DDTrace_Current_Span: bug (APMAPI-778) # current span endpoint should return span and trace id of zero if no span is "active" - tests/parametric/test_parametric_endpoints.py::Test_Parametric_FFE_Start: - - declaration: missing_feature (declared version for php is v1.21.0-dev) - component_version: "<1.21.0-dev" - - declaration: missing_feature (DDTrace FeatureFlags runtime is not in this dev build) - component_version: 1.21.0+c197795aa7d7d4e52819b5ff35c2e02e89841bfd - - declaration: missing_feature (DDTrace FeatureFlags runtime is not in this dev build) - component_version: 1.21.0+e402ea5f89cfb3c20033c65138df860ba680dec2 + tests/parametric/test_parametric_endpoints.py::Test_Parametric_FFE_Start: v1.21.0-dev tests/parametric/test_parametric_endpoints.py::Test_Parametric_Otel_Baggage: missing_feature (otel baggage is not supported) tests/parametric/test_parametric_endpoints.py::Test_Parametric_Otel_Current_Span: bug (APMAPI-778) # otel current span endpoint should return a span and trace id of zero if no span is "active" tests/parametric/test_parametric_endpoints.py::Test_Parametric_Write_Log: missing_feature From 83bc16bca78eb116a3a299c7f5d5f8333dea03aa Mon Sep 17 00:00:00 2001 From: Leo Romanovsky Date: Thu, 28 May 2026 18:21:05 -0400 Subject: [PATCH 5/8] Remove PHP-specific FFE test branching --- .../test_ffe/test_dynamic_evaluation.py | 30 ++++++------------- 1 file changed, 9 insertions(+), 21 deletions(-) diff --git a/tests/parametric/test_ffe/test_dynamic_evaluation.py b/tests/parametric/test_ffe/test_dynamic_evaluation.py index 99831a0ccc9..dbaa0e604c2 100644 --- a/tests/parametric/test_ffe/test_dynamic_evaluation.py +++ b/tests/parametric/test_ffe/test_dynamic_evaluation.py @@ -6,7 +6,6 @@ from typing import Any from utils import ( - context, features, scenarios, ) @@ -16,6 +15,7 @@ RC_PRODUCT = "FFE_FLAGS" RC_PATH = f"datadog/2/{RC_PRODUCT}" +DEDICATED_TEST_CASE_FILES = {"test-case-of-7-empty-targeting-key.json"} parametrize = pytest.mark.parametrize @@ -38,7 +38,11 @@ def _get_test_case_files() -> list[str]: if not test_data_dir.exists(): return [] - return [f.name for f in test_data_dir.iterdir() if f.suffix == ".json" and f.name != "flags-v1.json"] + return [ + f.name + for f in test_data_dir.iterdir() + if f.suffix == ".json" and f.name != "flags-v1.json" and f.name not in DEDICATED_TEST_CASE_FILES + ] # Load fixture at module level for reuse across tests @@ -76,16 +80,7 @@ def _set_and_wait_ffe_rc( test_agent.set_remote_config(path=f"{RC_PATH}/{config_id}/config", payload=rc_config) # Wait for RC acknowledgment - return test_agent.wait_for_rc_apply_state( - RC_PRODUCT, state=RemoteConfigApplyState.ACKNOWLEDGED, clear=True, wait_loops=500 - ) - - -def _start_ffe_provider(test_library: APMLibrary) -> bool: - # The PHP parametric server is one long-lived CLI request, so load the same - # canonical UFC fixture directly there while this test still requires RC ACK. - configuration = UFC_FIXTURE_DATA if test_library.lang == "php" else None - return test_library.ffe_start(configuration) + return test_agent.wait_for_rc_apply_state(RC_PRODUCT, state=RemoteConfigApplyState.ACKNOWLEDGED, clear=True) @scenarios.parametric @@ -118,13 +113,6 @@ def test_ffe_flag_evaluation(self, test_case_file: str, test_agent: TestAgentAPI 4. Handles user targeting, attribute matching, and rollout percentages """ - # Skip OF.7 (empty targeting key) test for libraries with known bugs - # Java: FFL-1729 - OpenFeature Java SDK rejects empty targeting keys - # Node.js: FFL-1730 - OpenFeature JS SDK rejects empty targeting keys - if test_case_file == "test-case-of-7-empty-targeting-key.json": - if context.library.name in ("java", "nodejs"): - pytest.skip("OF.7 empty targeting key bug: FFL-1729 (java), FFL-1730 (nodejs)") - # Load the test case file test_case_path = Path(__file__).parent / test_case_file @@ -138,7 +126,7 @@ def test_ffe_flag_evaluation(self, test_case_file: str, test_agent: TestAgentAPI _set_and_wait_ffe_rc(test_agent, UFC_FIXTURE_DATA) # Initialize FFE provider - success = _start_ffe_provider(test_library) + success = test_library.ffe_start(UFC_FIXTURE_DATA) assert success, "Failed to start FFE provider" # Run each test case @@ -180,7 +168,7 @@ def test_ffe_of7_empty_targeting_key(self, test_agent: TestAgentAPI, test_librar _set_and_wait_ffe_rc(test_agent, UFC_FIXTURE_DATA) # Initialize FFE provider - success = _start_ffe_provider(test_library) + success = test_library.ffe_start(UFC_FIXTURE_DATA) assert success, "Failed to start FFE provider" # Evaluate flag with empty targeting key From afd0d5f1f46a8fe8c11e7b5b8b67b1a5256d3081 Mon Sep 17 00:00:00 2001 From: Leo Romanovsky Date: Thu, 28 May 2026 19:29:22 -0400 Subject: [PATCH 6/8] Use OF7 JSON fixture in FFE parametric tests --- manifests/dotnet.yml | 1 - manifests/java.yml | 1 - manifests/nodejs.yml | 1 - .../test_ffe/test_dynamic_evaluation.py | 37 +------------------ 4 files changed, 1 insertion(+), 39 deletions(-) diff --git a/manifests/dotnet.yml b/manifests/dotnet.yml index f2c31f319fd..bb55f3f7012 100644 --- a/manifests/dotnet.yml +++ b/manifests/dotnet.yml @@ -810,7 +810,6 @@ manifest: tests/parametric/test_dynamic_configuration.py::TestDynamicConfigV2: v2.44.0 tests/parametric/test_ffe/test_dynamic_evaluation.py::Test_Feature_Flag_Dynamic_Evaluation: '>=3.36.0' # Modified by easy win activation script tests/parametric/test_ffe/test_dynamic_evaluation.py::Test_Feature_Flag_Dynamic_Evaluation::test_ffe_flag_evaluation: missing_feature # Created by easy win activation script - tests/parametric/test_ffe/test_dynamic_evaluation.py::Test_Feature_Flag_Dynamic_Evaluation::test_ffe_of7_empty_targeting_key: missing_feature # Created by easy win activation script tests/parametric/test_headers_b3.py::Test_Headers_B3::test_headers_b3_migrated_extract_valid: missing_feature (Need to remove b3=b3multi alias) tests/parametric/test_headers_b3.py::Test_Headers_B3::test_headers_b3_migrated_inject_valid: missing_feature (Need to remove b3=b3multi alias) tests/parametric/test_headers_b3.py::Test_Headers_B3::test_headers_b3_migrated_propagate_invalid: missing_feature (Need to remove b3=b3multi alias) diff --git a/manifests/java.yml b/manifests/java.yml index 67fe39a8a5d..4e2611d4238 100644 --- a/manifests/java.yml +++ b/manifests/java.yml @@ -3644,7 +3644,6 @@ manifest: tests/parametric/test_dynamic_configuration.py::TestDynamicConfigV1_ServiceTargets::test_not_match_service_target: irrelevant (APMAPI-1003) tests/parametric/test_dynamic_configuration.py::TestDynamicConfigV2: v1.31.0 tests/parametric/test_ffe/test_dynamic_evaluation.py::Test_Feature_Flag_Dynamic_Evaluation: v1.56.0 - tests/parametric/test_ffe/test_dynamic_evaluation.py::Test_Feature_Flag_Dynamic_Evaluation::test_ffe_of7_empty_targeting_key: bug (FFL-1729) tests/parametric/test_headers_b3.py::Test_Headers_B3::test_headers_b3_migrated_extract_invalid: # Modified by easy win activation script - declaration: missing_feature (Need to remove b3=b3multi alias) component_version: <1.58.2+06122213c8 diff --git a/manifests/nodejs.yml b/manifests/nodejs.yml index 1ad2943e97e..db89d7296a3 100644 --- a/manifests/nodejs.yml +++ b/manifests/nodejs.yml @@ -2013,7 +2013,6 @@ manifest: tests/parametric/test_dynamic_configuration.py::TestDynamicConfigV1_ServiceTargets::test_not_match_service_target: bug (APMAPI-865) tests/parametric/test_dynamic_configuration.py::TestDynamicConfigV2: *ref_4_23_0 tests/parametric/test_ffe/test_dynamic_evaluation.py::Test_Feature_Flag_Dynamic_Evaluation: *ref_5_75_0 - tests/parametric/test_ffe/test_dynamic_evaluation.py::Test_Feature_Flag_Dynamic_Evaluation::test_ffe_of7_empty_targeting_key: bug (FFL-1730) tests/parametric/test_headers_b3.py::Test_Headers_B3::test_headers_b3_migrated_extract_invalid: missing_feature (Need to remove b3=b3multi alias) tests/parametric/test_headers_b3.py::Test_Headers_B3::test_headers_b3_migrated_extract_valid: missing_feature (Need to remove b3=b3multi alias) tests/parametric/test_headers_b3.py::Test_Headers_B3::test_headers_b3_migrated_inject_valid: missing_feature (Need to remove b3=b3multi alias) diff --git a/tests/parametric/test_ffe/test_dynamic_evaluation.py b/tests/parametric/test_ffe/test_dynamic_evaluation.py index dbaa0e604c2..e7b76f4f0d9 100644 --- a/tests/parametric/test_ffe/test_dynamic_evaluation.py +++ b/tests/parametric/test_ffe/test_dynamic_evaluation.py @@ -15,7 +15,6 @@ RC_PRODUCT = "FFE_FLAGS" RC_PATH = f"datadog/2/{RC_PRODUCT}" -DEDICATED_TEST_CASE_FILES = {"test-case-of-7-empty-targeting-key.json"} parametrize = pytest.mark.parametrize @@ -38,11 +37,7 @@ def _get_test_case_files() -> list[str]: if not test_data_dir.exists(): return [] - return [ - f.name - for f in test_data_dir.iterdir() - if f.suffix == ".json" and f.name != "flags-v1.json" and f.name not in DEDICATED_TEST_CASE_FILES - ] + return [f.name for f in test_data_dir.iterdir() if f.suffix == ".json" and f.name != "flags-v1.json"] # Load fixture at module level for reuse across tests @@ -153,33 +148,3 @@ def test_ffe_flag_evaluation(self, test_case_file: str, test_agent: TestAgentAPI f"flag='{flag}', targetingKey='{targeting_key}', " f"expected={expected_result}, actual={actual_value}" ) - - @parametrize("library_env", [{**DEFAULT_ENVVARS}]) - def test_ffe_of7_empty_targeting_key(self, test_agent: TestAgentAPI, test_library: APMLibrary) -> None: - """OF.7: Empty string is a valid targeting key. - - This test validates that flag evaluation succeeds when the targeting key - is an empty string. The flag should still match allocations and return - the expected value, not fail with TARGETING_KEY_MISSING. - - Temporary dedicated test until FFL-1729 (Java) and FFL-1730 (Node.js) are resolved. - """ - # Set up UFC Remote Config and wait for it to be applied - _set_and_wait_ffe_rc(test_agent, UFC_FIXTURE_DATA) - - # Initialize FFE provider - success = test_library.ffe_start(UFC_FIXTURE_DATA) - assert success, "Failed to start FFE provider" - - # Evaluate flag with empty targeting key - result = test_library.ffe_evaluate( - flag="empty-targeting-key-flag", - variation_type="STRING", - default_value="default", - targeting_key="", - attributes={}, - ) - - assert result.get("value") == "on-value", ( - f"OF.7 failed: empty targeting key should return 'on-value', got '{result.get('value')}'" - ) From 552010fd33253dacc62dc68850a93b5881fa47b6 Mon Sep 17 00:00:00 2001 From: Leo Romanovsky Date: Thu, 28 May 2026 20:26:15 -0400 Subject: [PATCH 7/8] Restore deterministic PHP FFE fixture startup --- .../test_ffe/test_dynamic_evaluation.py | 2 +- utils/build/docker/php/parametric/server.php | 18 ++++++++++++++++++ .../_test_clients/_test_client_parametric.py | 12 ++++++++---- 3 files changed, 27 insertions(+), 5 deletions(-) diff --git a/tests/parametric/test_ffe/test_dynamic_evaluation.py b/tests/parametric/test_ffe/test_dynamic_evaluation.py index 43b74cddaa6..bc79c1daec3 100644 --- a/tests/parametric/test_ffe/test_dynamic_evaluation.py +++ b/tests/parametric/test_ffe/test_dynamic_evaluation.py @@ -164,7 +164,7 @@ def test_ffe_flag_evaluation(self, test_case_file: str, test_agent: TestAgentAPI _set_and_wait_ffe_rc(test_agent, UFC_FIXTURE_DATA) # Initialize FFE provider - success = test_library.ffe_start() + success = test_library.ffe_start(UFC_FIXTURE_DATA) assert success, "Failed to start FFE provider" # Run each test case diff --git a/utils/build/docker/php/parametric/server.php b/utils/build/docker/php/parametric/server.php index fdaf61847fb..1548de54de5 100644 --- a/utils/build/docker/php/parametric/server.php +++ b/utils/build/docker/php/parametric/server.php @@ -246,6 +246,24 @@ function remappedSpanKind($spanKind) { ])); } + $configuration = arg($req, 'configuration'); + if ($configuration !== null) { + if (!function_exists('\\DDTrace\\Testing\\ffe_load_config')) { + return new Response(status: 500, headers: ['content-type' => 'application/json'], body: json_encode([ + 'success' => false, + 'message' => 'DDTrace\\Testing\\ffe_load_config is not available', + ])); + } + + $configurationJson = json_encode($configuration); + if (!is_string($configurationJson) || !\DDTrace\Testing\ffe_load_config($configurationJson)) { + return new Response(status: 500, headers: ['content-type' => 'application/json'], body: json_encode([ + 'success' => false, + 'message' => 'Failed to load FFE test configuration', + ])); + } + } + $ffeClient = new \DDTrace\FeatureFlags\Client(); return jsonResponse(['success' => true]); diff --git a/utils/docker_fixtures/_test_clients/_test_client_parametric.py b/utils/docker_fixtures/_test_clients/_test_client_parametric.py index 0e28f1c32c6..8b28d05bf26 100644 --- a/utils/docker_fixtures/_test_clients/_test_client_parametric.py +++ b/utils/docker_fixtures/_test_clients/_test_client_parametric.py @@ -737,14 +737,18 @@ def otel_current_span(self) -> _TestOtelSpan | None: return _TestOtelSpan(self, span_response["span_id"], span_response["trace_id"]) - def ffe_start(self) -> bool: + def ffe_start(self, configuration: dict | None = None) -> bool: """Initialize the FFE (Feature Flagging & Experimentation) provider. Returns: bool: True if the provider was initialized successfully, False otherwise """ - resp = self._session.post(self._url("/ffe/start"), json={}) + payload = {} + if configuration is not None: + payload["configuration"] = configuration + + resp = self._session.post(self._url("/ffe/start"), json=payload) return HTTPStatus(resp.status_code).is_success def ffe_evaluate( @@ -1166,9 +1170,9 @@ def write_log( ) -> bool: return self._client.write_log(logger_name, level, message, span_id=span_id) - def ffe_start(self) -> bool: + def ffe_start(self, configuration: dict | None = None) -> bool: """Initialize the FFE (Feature Flagging & Experimentation) provider.""" - return self._client.ffe_start() + return self._client.ffe_start(configuration) def ffe_evaluate( self, From fbc98222a1ad229e22657e63b958925ae494d9e4 Mon Sep 17 00:00:00 2001 From: Leo Romanovsky Date: Thu, 28 May 2026 09:38:35 -0400 Subject: [PATCH 8/8] Enable PHP FFE evaluation metric system tests --- manifests/php.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manifests/php.yml b/manifests/php.yml index 8fba8df425e..045e438247c 100644 --- a/manifests/php.yml +++ b/manifests/php.yml @@ -607,7 +607,7 @@ manifest: tests/docker_ssi/test_docker_ssi_crash.py::TestDockerSSICrash::test_crash: missing_feature (No implemented the endpoint /crashme) tests/ffe/test_dynamic_evaluation.py: v1.21.0-dev tests/ffe/test_exposures.py: missing_feature - tests/ffe/test_flag_eval_metrics.py: missing_feature + tests/ffe/test_flag_eval_metrics.py: v1.21.0-dev tests/integration_frameworks/llm/anthropic/test_anthropic_llmobs.py::TestAnthropicLlmObsMessages::test_create_error: bug (MLOB-1234) tests/integrations/crossed_integrations/test_kafka.py::Test_Kafka: missing_feature tests/integrations/crossed_integrations/test_kinesis.py::Test_Kinesis_PROPAGATION_VIA_MESSAGE_ATTRIBUTES: missing_feature