From 2db71e00d09603f560c7b0a675711bdc9023c8b2 Mon Sep 17 00:00:00 2001 From: Leo Romanovsky Date: Fri, 6 Feb 2026 21:03:10 -0500 Subject: [PATCH 1/2] feat(php): Enable FFE tests and add /ffe endpoints - Add /ffe/start and /ffe/evaluate to PHP parametric server - Add /ffe.php for end-to-end tests - Remove missing_feature for PHP FFE tests in manifest --- manifests/php.yml | 3 -- utils/build/docker/php/common/ffe.php | 53 +++++++++++++++++++ .../build/docker/php/parametric/composer.json | 3 +- utils/build/docker/php/parametric/server.php | 45 ++++++++++++++++ 4 files changed, 100 insertions(+), 4 deletions(-) create mode 100644 utils/build/docker/php/common/ffe.php diff --git a/manifests/php.yml b/manifests/php.yml index 12505d77ce0..4551bbe97dc 100644 --- a/manifests/php.yml +++ b/manifests/php.yml @@ -483,8 +483,6 @@ manifest: - declaration: missing_feature (Not implemented yet) component_version: <1.12.0 tests/docker_ssi/test_docker_ssi_appsec.py::TestDockerSSIAppsecFeatures::test_telemetry_source_ssi: missing_feature - tests/ffe/test_dynamic_evaluation.py: missing_feature - tests/ffe/test_exposures.py: missing_feature 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 tests/integrations/crossed_integrations/test_rabbitmq.py::Test_RabbitMQ_Trace_Context_Propagation: missing_feature @@ -543,7 +541,6 @@ manifest: tests/parametric/test_dynamic_configuration.py::TestDynamicConfigV1_EmptyServiceTargets: v1.4.0 tests/parametric/test_dynamic_configuration.py::TestDynamicConfigV1_ServiceTargets: missing_feature tests/parametric/test_dynamic_configuration.py::TestDynamicConfigV2: missing_feature - tests/parametric/test_ffe/test_dynamic_evaluation.py::Test_Feature_Flag_Dynamic_Evaluation: missing_feature 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/utils/build/docker/php/common/ffe.php b/utils/build/docker/php/common/ffe.php new file mode 100644 index 00000000000..cf65d7da9c0 --- /dev/null +++ b/utils/build/docker/php/common/ffe.php @@ -0,0 +1,53 @@ + 'Invalid JSON body']); + exit; +} + +$flag = isset($input['flag']) ? $input['flag'] : null; +$variationType = isset($input['variationType']) ? $input['variationType'] : null; +$defaultValue = isset($input['defaultValue']) ? $input['defaultValue'] : null; +$targetingKey = array_key_exists('targetingKey', $input) ? $input['targetingKey'] : ''; +$attributes = isset($input['attributes']) ? $input['attributes'] : []; + +try { + // Use OpenFeature API if available, fall back to direct provider + if (class_exists('\OpenFeature\API')) { + $provider = new \DDTrace\OpenFeature\DataDogProvider(); + \OpenFeature\API::setProvider($provider); + $client = \OpenFeature\API::getClient(); + + $context = new \OpenFeature\implementation\flags\EvaluationContext( + $targetingKey, + new \OpenFeature\implementation\flags\Attributes($attributes) + ); + + $value = match ($variationType) { + 'BOOLEAN' => $client->getBooleanValue($flag, (bool) $defaultValue, $context), + 'STRING' => $client->getStringValue($flag, (string) $defaultValue, $context), + 'INTEGER' => $client->getIntegerValue($flag, (int) $defaultValue, $context), + 'NUMERIC' => $client->getFloatValue($flag, (float) $defaultValue, $context), + 'JSON' => $client->getObjectValue($flag, is_array($defaultValue) ? $defaultValue : [], $context), + default => $defaultValue, + }; + } else { + // Fallback to direct provider (no OpenFeature SDK installed) + $provider = \DDTrace\FeatureFlags\Provider::getInstance(); + $provider->start(); + $result = $provider->evaluate($flag, $variationType, $defaultValue, $targetingKey, $attributes); + $value = $result['value']; + } + + // Flush exposure events immediately for system test observability + \DDTrace\FeatureFlags\Provider::getInstance()->flush(); + + echo json_encode(['value' => $value]); +} catch (\Throwable $e) { + echo json_encode(['value' => $defaultValue, 'error' => $e->getMessage()]); +} diff --git a/utils/build/docker/php/parametric/composer.json b/utils/build/docker/php/parametric/composer.json index 38a79500f95..6499cccb53c 100644 --- a/utils/build/docker/php/parametric/composer.json +++ b/utils/build/docker/php/parametric/composer.json @@ -7,7 +7,8 @@ "amphp/log": "2.x-dev", "open-telemetry/sdk": "^1.0.0", "symfony/http-client": "6.4.x-dev", - "nyholm/psr7": "^1.8@dev" + "nyholm/psr7": "^1.8@dev", + "open-feature/sdk": "^2.0" }, "config": { "allow-plugins": { diff --git a/utils/build/docker/php/parametric/server.php b/utils/build/docker/php/parametric/server.php index 5d87f161db5..066da41c7c6 100644 --- a/utils/build/docker/php/parametric/server.php +++ b/utils/build/docker/php/parametric/server.php @@ -546,6 +546,51 @@ function remappedSpanKind($spanKind) { return jsonResponse([]); })); +// FFE (Feature Flags & Experimentation) endpoints +$openFeatureClient = null; + +$router->addRoute('POST', '/ffe/start', new ClosureRequestHandler(function (Request $req) use (&$openFeatureClient) { + try { + $provider = new \DDTrace\OpenFeature\DataDogProvider(); + \OpenFeature\API::setProvider($provider); + $openFeatureClient = \OpenFeature\API::getClient(); + return jsonResponse([]); + } catch (\Throwable $e) { + return new Response(status: 500, body: json_encode(['error' => $e->getMessage()])); + } +})); + +$router->addRoute('POST', '/ffe/evaluate', new ClosureRequestHandler(function (Request $req) use (&$openFeatureClient) { + try { + if ($openFeatureClient === null) { + $provider = new \DDTrace\OpenFeature\DataDogProvider(); + \OpenFeature\API::setProvider($provider); + $openFeatureClient = \OpenFeature\API::getClient(); + } + + $flag = arg($req, 'flag'); + $variationType = arg($req, 'variationType'); + $defaultValue = arg($req, 'defaultValue'); + $targetingKey = arg($req, 'targetingKey'); + $attributes = arg($req, 'attributes') ?? []; + + $context = new \OpenFeature\implementation\flags\EvaluationContext($targetingKey, new \OpenFeature\implementation\flags\Attributes($attributes)); + + $value = match ($variationType) { + 'BOOLEAN' => $openFeatureClient->getBooleanValue($flag, (bool) $defaultValue, $context), + 'STRING' => $openFeatureClient->getStringValue($flag, (string) $defaultValue, $context), + 'INTEGER' => $openFeatureClient->getIntegerValue($flag, (int) $defaultValue, $context), + 'NUMERIC' => $openFeatureClient->getFloatValue($flag, (float) $defaultValue, $context), + 'JSON' => $openFeatureClient->getObjectValue($flag, is_array($defaultValue) ? $defaultValue : [], $context), + default => $defaultValue, + }; + + return jsonResponse(['value' => $value]); + } catch (\Throwable $e) { + return new Response(status: 500, body: json_encode(['error' => $e->getMessage()])); + } +})); + $middleware = new class implements Middleware { public function handleRequest(Request $request, RequestHandler $next): Response { $response = $next->handleRequest($request); From 5490b6cef67f167f84623d3e3ab7a1901b34703e Mon Sep 17 00:00:00 2001 From: Leo Romanovsky Date: Wed, 11 Feb 2026 11:14:09 -0500 Subject: [PATCH 2/2] fix(php): Add /ffe route and mark exposure cache xfail - Add Apache rewrite rule for /ffe endpoint to ffe.php - Mark Test_FFE_Exposure_Caching_Same_Subject as missing_feature because PHP shared-nothing architecture resets the in-memory ExposureCache on every HTTP request --- manifests/php.yml | 1 + utils/build/docker/php/apache-mod/php.conf | 1 + 2 files changed, 2 insertions(+) diff --git a/manifests/php.yml b/manifests/php.yml index 4551bbe97dc..7c668c60c72 100644 --- a/manifests/php.yml +++ b/manifests/php.yml @@ -483,6 +483,7 @@ manifest: - declaration: missing_feature (Not implemented yet) component_version: <1.12.0 tests/docker_ssi/test_docker_ssi_appsec.py::TestDockerSSIAppsecFeatures::test_telemetry_source_ssi: missing_feature + tests/ffe/test_exposures.py::Test_FFE_Exposure_Caching_Same_Subject::test_ffe_exposure_caching_same_subject: missing_feature (PHP shared-nothing architecture - exposure cache does not persist across HTTP requests) 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 tests/integrations/crossed_integrations/test_rabbitmq.py::Test_RabbitMQ_Trace_Context_Propagation: missing_feature diff --git a/utils/build/docker/php/apache-mod/php.conf b/utils/build/docker/php/apache-mod/php.conf index 1c56b062c20..ad29fb8c133 100644 --- a/utils/build/docker/php/apache-mod/php.conf +++ b/utils/build/docker/php/apache-mod/php.conf @@ -27,6 +27,7 @@ RewriteRule "^/load_dependency$" "/load_dependency/" RewriteRule "^/signup$" "/signup/" RewriteRule "^/shell_execution$" "/shell_execution/" + RewriteRule "^/ffe$" "/ffe.php" [L] RewriteCond /var/www/html/%{REQUEST_URI} !-f RewriteRule "^/rasp/(.*)" "/rasp/$1.php" [L] RewriteRule "^/api_security.sampling/.*" "/api_security_sampling.php$0" [L]