Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions docs/understand/weblogs/end-to-end_weblog.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 :
Expand Down
1 change: 0 additions & 1 deletion manifests/dotnet.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was fixed; removing

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)
Expand Down
1 change: 0 additions & 1 deletion manifests/java.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was fixed; removing

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
Expand Down
1 change: 0 additions & 1 deletion manifests/nodejs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was fixed; removing

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)
Expand Down
6 changes: 3 additions & 3 deletions manifests/php.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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.21.0-dev
Comment thread
leoromanovsky marked this conversation as resolved.
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)
Expand Down Expand Up @@ -731,7 +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: missing_feature
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
Expand Down Expand Up @@ -869,7 +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: missing_feature
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
Expand Down
40 changes: 1 addition & 39 deletions tests/parametric/test_ffe/test_dynamic_evaluation.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
from typing import Any

from utils import (
context,
features,
scenarios,
)
Expand Down Expand Up @@ -109,13 +108,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)")
Comment on lines -114 to -119
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was fixed; removing - works for php as well


# Load the test case file
test_case_path = Path(__file__).parent / test_case_file

Expand All @@ -129,7 +121,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
Expand All @@ -156,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.
Comment on lines -162 to -164
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

special test not needed; using the centralized fixtures again for all languages


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()
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')}'"
)
209 changes: 209 additions & 0 deletions utils/build/docker/php/common/ffe.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
<?php

function dd_ffe_json_response($statusCode, array $payload)
{
header('Content-Type: application/json');
http_response_code($statusCode);
echo json_encode($payload, JSON_UNESCAPED_SLASHES);
}

function dd_ffe_error_response($statusCode, $errorCode, $errorMessage)
{
dd_ffe_json_response($statusCode, array(
'value' => 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,
),
));
1 change: 1 addition & 0 deletions utils/build/docker/php/common/rewrite-rules.conf
Original file line number Diff line number Diff line change
Expand Up @@ -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/"
Expand Down
Loading