From 74547e25b4b683dfe07785e0e5087362dd9ff5b4 Mon Sep 17 00:00:00 2001 From: Leo Romanovsky Date: Tue, 16 Jun 2026 14:36:39 -0400 Subject: [PATCH 01/11] Add EVP flagevaluation system tests --- manifests/cpp.yml | 1 + manifests/cpp_httpd.yml | 1 + manifests/cpp_kong.yml | 1 + manifests/cpp_nginx.yml | 1 + manifests/dotnet.yml | 1 + manifests/envoy.yml | 1 + manifests/golang.yml | 2 + manifests/haproxy.yml | 1 + manifests/java.yml | 1 + manifests/java_lambda.yml | 1 + manifests/java_otel.yml | 1 + manifests/nodejs.yml | 1 + manifests/nodejs_lambda.yml | 1 + manifests/nodejs_otel.yml | 1 + manifests/php.yml | 1 + manifests/python.yml | 1 + manifests/python_lambda.yml | 1 + manifests/python_otel.yml | 1 + manifests/ruby.yml | 1 + manifests/rust.yml | 1 + tests/ffe/README.md | 1 + tests/ffe/test_flag_eval_evp.py | 417 ++++++++++++++++++++++++++++++++ utils/_features.py | 8 + 23 files changed, 447 insertions(+) create mode 100644 tests/ffe/test_flag_eval_evp.py diff --git a/manifests/cpp.yml b/manifests/cpp.yml index e0eb9f60b05..483d9b2ef56 100644 --- a/manifests/cpp.yml +++ b/manifests/cpp.yml @@ -20,6 +20,7 @@ manifest: tests/debugger/test_debugger_exception_replay.py::Test_Debugger_Exception_Replay::test_exception_replay_stackoverflow: missing_feature (Implemented only for dotnet) tests/debugger/test_debugger_probe_snapshot.py::Test_Debugger_Line_Probe_Snaphots::test_process_tags_snapshot: missing_feature (Not yet implemented) tests/debugger/test_debugger_probe_snapshot.py::Test_Debugger_Line_Probe_Snaphots::test_process_tags_snapshot_svc: missing_feature (Not yet implemented) + tests/ffe/test_flag_eval_evp.py: irrelevant (only parametric tests are run for cpp) tests/integration_frameworks/llm/anthropic/test_anthropic_llmobs.py::TestAnthropicLlmObsMessages::test_create_error: bug (MLOB-1234) tests/integrations/crossed_integrations/test_sqs.py::Test_SQS_PROPAGATION_VIA_AWS_XRAY_HEADERS: irrelevant (Localstack SQS does not support AWS Xray Header parsing) tests/integrations/test_base_service.py::Test_BaseService_SqlSpan: irrelevant (/rasp/sqli endpoint is not available) diff --git a/manifests/cpp_httpd.yml b/manifests/cpp_httpd.yml index 9a153512a35..0d80cd6dd99 100644 --- a/manifests/cpp_httpd.yml +++ b/manifests/cpp_httpd.yml @@ -40,6 +40,7 @@ manifest: tests/debugger/test_debugger_probe_snapshot.py::Test_Debugger_Line_Probe_Snaphots::test_process_tags_snapshot_svc: missing_feature (Not yet implemented) tests/ffe/test_dynamic_evaluation.py: missing_feature tests/ffe/test_exposures.py: missing_feature + tests/ffe/test_flag_eval_evp.py: missing_feature (FFL-2446) 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) tests/integrations/crossed_integrations/: missing_feature (Endpoint not implemented) diff --git a/manifests/cpp_kong.yml b/manifests/cpp_kong.yml index 07c1d7bdfef..32d2f4ddc52 100644 --- a/manifests/cpp_kong.yml +++ b/manifests/cpp_kong.yml @@ -8,6 +8,7 @@ manifest: tests/appsec/: irrelevant (ASM is not implemented in Kong plugin) tests/debugger/: irrelevant tests/ffe/: missing_feature + tests/ffe/test_flag_eval_evp.py: missing_feature (FFL-2446) tests/ffe/test_flag_eval_metrics.py: missing_feature tests/integrations/: missing_feature (Endpoints not implemented) tests/otel/: irrelevant (library does not implement OpenTelemetry) diff --git a/manifests/cpp_nginx.yml b/manifests/cpp_nginx.yml index b002372fda7..d936f34d2f7 100644 --- a/manifests/cpp_nginx.yml +++ b/manifests/cpp_nginx.yml @@ -267,6 +267,7 @@ manifest: 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/ffe/test_flag_eval_evp.py: missing_feature (FFL-2446) 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) tests/integrations/crossed_integrations/test_kafka.py::Test_Kafka: missing_feature diff --git a/manifests/dotnet.yml b/manifests/dotnet.yml index de45b66fecd..f14002b6524 100644 --- a/manifests/dotnet.yml +++ b/manifests/dotnet.yml @@ -710,6 +710,7 @@ manifest: tests/ffe/test_dynamic_evaluation.py::Test_FFE_Flag_Parse_Error_Isolation: bug (FFL-2184) tests/ffe/test_dynamic_evaluation.py::Test_FFE_Unknown_Operator_Tolerance: bug (FFL-2184) tests/ffe/test_exposures.py: v3.36.0 + tests/ffe/test_flag_eval_evp.py: missing_feature (FFL-2446) tests/ffe/test_flag_eval_metrics.py: v3.44.0 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: v2.0.0-prerelease diff --git a/manifests/envoy.yml b/manifests/envoy.yml index e035dc1f099..3a3aa8a9d66 100644 --- a/manifests/envoy.yml +++ b/manifests/envoy.yml @@ -35,6 +35,7 @@ manifest: tests/appsec/test_traces.py::Test_AppSecEventSpanTags::test_header_collection: irrelevant (test) tests/appsec/test_traces.py::Test_CollectRespondHeaders::test_header_collection: missing_feature (The endpoint /headers is not implemented in the weblog) tests/appsec/test_versions.py::Test_Events: v1.72.0 + tests/ffe/test_flag_eval_evp.py: irrelevant (proxy weblog does not implement server-side FFE EVP) tests/parametric/test_otel_span_methods.py::Test_Otel_Span_Methods::test_otel_record_exception_sets_handling_stack_in_go: irrelevant tests/test_config_consistency.py::Test_Config_UnifiedServiceTagging_CustomService: v1.72.0 tests/test_scrubbing.py: v1.72.0 diff --git a/manifests/golang.yml b/manifests/golang.yml index 40220a3eaaf..bd25f0db411 100644 --- a/manifests/golang.yml +++ b/manifests/golang.yml @@ -885,6 +885,8 @@ manifest: tests/ffe/test_dynamic_evaluation.py::Test_FFE_RC_Unavailable: v2.4.0 tests/ffe/test_dynamic_evaluation.py::Test_FFE_Unknown_Operator_Tolerance::test_unknown_operator_errors: bug (FFL-2182) tests/ffe/test_exposures.py: v2.6.0-dev # Easy win for chi, echo, gin, net-http, net-http-orchestrion, uds-echo and version 2.5.0 + tests/ffe/test_flag_eval_evp.py: v2.9.0-dev + tests/ffe/test_flag_eval_evp.py::Test_FFE_EVP_Flagevaluation_Degradation: missing_feature (FFL-2446 - no system-test cap override to force degradation) tests/ffe/test_flag_eval_metrics.py: v2.8.0 tests/ffe/test_flag_eval_metrics.py::Test_FFE_Eval_Metric_Parse_Error_Invalid_Regex: irrelevant (Go validates regex at config load time) tests/ffe/test_flag_eval_metrics.py::Test_FFE_Eval_Nested_Attributes_Ignored: irrelevant (FFL-1980) diff --git a/manifests/haproxy.yml b/manifests/haproxy.yml index b8d0062f712..457debd360e 100644 --- a/manifests/haproxy.yml +++ b/manifests/haproxy.yml @@ -33,6 +33,7 @@ manifest: tests/appsec/test_traces.py::Test_AppSecEventSpanTags::test_header_collection: irrelevant (test) tests/appsec/test_traces.py::Test_CollectRespondHeaders::test_header_collection: missing_feature (The endpoint /headers is not implemented in the weblog) tests/appsec/test_versions.py::Test_Events: v2.4.0 + tests/ffe/test_flag_eval_evp.py: irrelevant (proxy weblog does not implement server-side FFE EVP) tests/parametric/test_otel_span_methods.py::Test_Otel_Span_Methods::test_otel_record_exception_sets_handling_stack_in_go: irrelevant tests/test_config_consistency.py::Test_Config_UnifiedServiceTagging_CustomService: v2.4.0 tests/test_scrubbing.py: v2.4.0 diff --git a/manifests/java.yml b/manifests/java.yml index be6f5bf1446..425c691cb88 100644 --- a/manifests/java.yml +++ b/manifests/java.yml @@ -3235,6 +3235,7 @@ manifest: "*": irrelevant spring-boot: v1.56.0 tests/ffe/test_exposures.py::Test_FFE_EXP_5_Missing_Targeting_Key: bug (FFL-1729) + tests/ffe/test_flag_eval_evp.py: missing_feature (FFL-2446) tests/ffe/test_flag_eval_metrics.py: - weblog_declaration: "*": irrelevant diff --git a/manifests/java_lambda.yml b/manifests/java_lambda.yml index e5e59b60a33..83478d4eba8 100644 --- a/manifests/java_lambda.yml +++ b/manifests/java_lambda.yml @@ -61,6 +61,7 @@ manifest: tests/appsec/waf/test_blocking.py::Test_Blocking: missing_feature tests/appsec/waf/test_blocking.py::Test_Blocking_strip_response_headers: missing_feature tests/appsec/waf/test_blocking.py::Test_CustomBlockingResponse: missing_feature + tests/ffe/test_flag_eval_evp.py: irrelevant (server-side FFE EVP weblog scenario is not applicable to lambda) tests/test_resource_renaming.py: missing_feature tests/test_rum_injection.py: irrelevant (RUM injection only supported for Java) tests/test_v1_payloads.py: missing_feature diff --git a/manifests/java_otel.yml b/manifests/java_otel.yml index d060e27ace3..199c68682e8 100644 --- a/manifests/java_otel.yml +++ b/manifests/java_otel.yml @@ -12,6 +12,7 @@ manifest: tests/debugger/test_debugger_exception_replay.py::Test_Debugger_Exception_Replay::test_exception_replay_stackoverflow: missing_feature (Implemented only for dotnet) tests/debugger/test_debugger_probe_snapshot.py::Test_Debugger_Line_Probe_Snaphots::test_process_tags_snapshot: missing_feature (Not yet implemented) tests/debugger/test_debugger_probe_snapshot.py::Test_Debugger_Line_Probe_Snaphots::test_process_tags_snapshot_svc: missing_feature (Not yet implemented) + tests/ffe/test_flag_eval_evp.py: irrelevant (OpenTelemetry test library does not implement server-side FFE EVP) tests/integration_frameworks/llm/anthropic/test_anthropic_llmobs.py::TestAnthropicLlmObsMessages::test_create_error: bug (MLOB-1234) tests/integrations/crossed_integrations/test_sqs.py::Test_SQS_PROPAGATION_VIA_AWS_XRAY_HEADERS: irrelevant (Localstack SQS does not support AWS Xray Header parsing) tests/integrations/test_cassandra.py::Test_Cassandra: missing_feature (Endpoint is not implemented on weblog) diff --git a/manifests/nodejs.yml b/manifests/nodejs.yml index 8b685f67e50..b84ccadc54b 100644 --- a/manifests/nodejs.yml +++ b/manifests/nodejs.yml @@ -1695,6 +1695,7 @@ manifest: "*": incomplete_test_app express4: *ref_5_77_0 tests/ffe/test_exposures.py::Test_FFE_EXP_5_Missing_Targeting_Key: bug (FFL-1730) + tests/ffe/test_flag_eval_evp.py: missing_feature (FFL-2446) tests/ffe/test_flag_eval_metrics.py: - weblog_declaration: "*": incomplete_test_app diff --git a/manifests/nodejs_lambda.yml b/manifests/nodejs_lambda.yml index 901cb99e60b..c8a1f0e526f 100644 --- a/manifests/nodejs_lambda.yml +++ b/manifests/nodejs_lambda.yml @@ -63,6 +63,7 @@ manifest: tests/appsec/waf/test_blocking.py::Test_Blocking: missing_feature tests/appsec/waf/test_blocking.py::Test_Blocking_strip_response_headers: missing_feature tests/appsec/waf/test_blocking.py::Test_CustomBlockingResponse: missing_feature + tests/ffe/test_flag_eval_evp.py: irrelevant (server-side FFE EVP weblog scenario is not applicable to lambda) tests/test_resource_renaming.py: missing_feature tests/test_rum_injection.py: irrelevant tests/test_v1_payloads.py: missing_feature diff --git a/manifests/nodejs_otel.yml b/manifests/nodejs_otel.yml index e0b22eb08e8..3a018abfb43 100644 --- a/manifests/nodejs_otel.yml +++ b/manifests/nodejs_otel.yml @@ -11,6 +11,7 @@ manifest: tests/debugger/test_debugger_exception_replay.py::Test_Debugger_Exception_Replay::test_exception_replay_stackoverflow: missing_feature (Implemented only for dotnet) tests/debugger/test_debugger_probe_snapshot.py::Test_Debugger_Line_Probe_Snaphots::test_process_tags_snapshot: missing_feature (Not yet implemented) tests/debugger/test_debugger_probe_snapshot.py::Test_Debugger_Line_Probe_Snaphots::test_process_tags_snapshot_svc: missing_feature (Not yet implemented) + tests/ffe/test_flag_eval_evp.py: irrelevant (OpenTelemetry test library does not implement server-side FFE EVP) tests/integration_frameworks/llm/anthropic/test_anthropic_llmobs.py::TestAnthropicLlmObsMessages::test_create_error: bug (MLOB-1234) tests/integrations/crossed_integrations/test_sqs.py::Test_SQS_PROPAGATION_VIA_AWS_XRAY_HEADERS: irrelevant (Localstack SQS does not support AWS Xray Header parsing) tests/integrations/test_cassandra.py::Test_Cassandra: missing_feature (Endpoint is not implemented on weblog) diff --git a/manifests/php.yml b/manifests/php.yml index 07a348c59e8..2b9f8448ac2 100644 --- a/manifests/php.yml +++ b/manifests/php.yml @@ -621,6 +621,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: v1.21.0-dev + tests/ffe/test_flag_eval_evp.py: missing_feature (FFL-2446) 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 diff --git a/manifests/python.yml b/manifests/python.yml index 2a46e41b952..b5471df30f8 100644 --- a/manifests/python.yml +++ b/manifests/python.yml @@ -1368,6 +1368,7 @@ manifest: tests/ffe/test_dynamic_evaluation.py::Test_FFE_RC_Down_From_Start: v4.0.0 tests/ffe/test_dynamic_evaluation.py::Test_FFE_RC_Unavailable: flaky (FFL-1622) tests/ffe/test_exposures.py: v4.2.0-dev + tests/ffe/test_flag_eval_evp.py: missing_feature (FFL-2446) tests/ffe/test_flag_eval_metrics.py: v4.7.0 tests/ffe/test_flag_eval_metrics.py::Test_FFE_Eval_Nested_Attributes_Ignored: irrelevant (FFL-1980) tests/integration_frameworks/llm/anthropic/test_anthropic_apm.py::TestAnthropicApmMessages: v3.16.0 diff --git a/manifests/python_lambda.yml b/manifests/python_lambda.yml index ee4c9622137..7d78673d0b6 100644 --- a/manifests/python_lambda.yml +++ b/manifests/python_lambda.yml @@ -281,6 +281,7 @@ manifest: tests/debugger/test_debugger_exception_replay.py::Test_Debugger_Exception_Replay::test_exception_replay_stackoverflow: missing_feature (Implemented only for dotnet) tests/debugger/test_debugger_probe_snapshot.py::Test_Debugger_Line_Probe_Snaphots::test_process_tags_snapshot: missing_feature (Not yet implemented) tests/debugger/test_debugger_probe_snapshot.py::Test_Debugger_Line_Probe_Snaphots::test_process_tags_snapshot_svc: missing_feature (Not yet implemented) + tests/ffe/test_flag_eval_evp.py: irrelevant (server-side FFE EVP weblog scenario is not applicable to lambda) tests/integration_frameworks/llm/anthropic/test_anthropic_llmobs.py::TestAnthropicLlmObsMessages::test_create_error: bug (MLOB-1234) tests/integrations/crossed_integrations/test_sqs.py::Test_SQS_PROPAGATION_VIA_AWS_XRAY_HEADERS: irrelevant (Localstack SQS does not support AWS Xray Header parsing) tests/integrations/test_cassandra.py::Test_Cassandra: missing_feature (Endpoint is not implemented on weblog) diff --git a/manifests/python_otel.yml b/manifests/python_otel.yml index 9a430ae11bf..ba5d575ea14 100644 --- a/manifests/python_otel.yml +++ b/manifests/python_otel.yml @@ -11,6 +11,7 @@ manifest: tests/debugger/test_debugger_exception_replay.py::Test_Debugger_Exception_Replay::test_exception_replay_stackoverflow: missing_feature (Implemented only for dotnet) tests/debugger/test_debugger_probe_snapshot.py::Test_Debugger_Line_Probe_Snaphots::test_process_tags_snapshot: missing_feature (Not yet implemented) tests/debugger/test_debugger_probe_snapshot.py::Test_Debugger_Line_Probe_Snaphots::test_process_tags_snapshot_svc: missing_feature (Not yet implemented) + tests/ffe/test_flag_eval_evp.py: irrelevant (OpenTelemetry test library does not implement server-side FFE EVP) tests/integration_frameworks/llm/anthropic/test_anthropic_llmobs.py::TestAnthropicLlmObsMessages::test_create_error: bug (MLOB-1234) tests/integrations/crossed_integrations/test_sqs.py::Test_SQS_PROPAGATION_VIA_AWS_XRAY_HEADERS: irrelevant (Localstack SQS does not support AWS Xray Header parsing) tests/integrations/test_cassandra.py::Test_Cassandra: missing_feature (Endpoint is not implemented on weblog) diff --git a/manifests/ruby.yml b/manifests/ruby.yml index b1fbaffbeab..34f6c3bb6a3 100644 --- a/manifests/ruby.yml +++ b/manifests/ruby.yml @@ -1886,6 +1886,7 @@ manifest: - weblog_declaration: "*": irrelevant rails72: v2.23.0-dev + tests/ffe/test_flag_eval_evp.py: missing_feature (FFL-2446) tests/ffe/test_flag_eval_metrics.py: - weblog_declaration: "*": irrelevant diff --git a/manifests/rust.yml b/manifests/rust.yml index d4d8330577a..6052e92a428 100644 --- a/manifests/rust.yml +++ b/manifests/rust.yml @@ -25,6 +25,7 @@ manifest: 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/ffe/test_flag_eval_evp.py: missing_feature (FFL-2446) 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) tests/integrations/crossed_integrations/test_sqs.py::Test_SQS_PROPAGATION_VIA_AWS_XRAY_HEADERS: irrelevant (Localstack SQS does not support AWS Xray Header parsing) diff --git a/tests/ffe/README.md b/tests/ffe/README.md index c67f4488c5f..9aca2fb8dba 100644 --- a/tests/ffe/README.md +++ b/tests/ffe/README.md @@ -9,6 +9,7 @@ This directory contains system tests for the Feature Flags & Experimentation (FF | `test_dynamic_evaluation.py` | Dynamic flag evaluation via Remote Config | | `test_exposures.py` | Flag exposure tracking and reporting | | `test_flag_eval_metrics.py` | Evaluation metrics (OTel counter) | +| `test_flag_eval_evp.py` | Server-side EVP flagevaluation payloads, aggregation, and bounds | ## Running FFE Tests diff --git a/tests/ffe/test_flag_eval_evp.py b/tests/ffe/test_flag_eval_evp.py new file mode 100644 index 00000000000..2ee5c7e0d7c --- /dev/null +++ b/tests/ffe/test_flag_eval_evp.py @@ -0,0 +1,417 @@ +"""Test server-side feature flag evaluation counts via EVP flagevaluation.""" + +import json +import time +from typing import Any +from typing import cast + +import pytest + +from utils import HttpResponse +from utils import features +from utils import interfaces +from utils import remote_config as rc +from utils import scenarios +from utils import weblog + + +RC_PRODUCT = "FFE_FLAGS" +RC_PATH = f"datadog/2/{RC_PRODUCT}" +EVP_FLAGEVALUATIONS_PATH = "/api/v2/flagevaluations" +EVP_FLUSH_WAIT_SECONDS = 12 + +JSON = dict[str, Any] + + +def make_ufc_fixture( + flag_key: str, + variant_key: str = "on", + variation_type: str = "STRING", + *, + enabled: bool = True, +) -> JSON: + values: dict[str, dict[str, str | bool | float | int]] = { + "STRING": {"on": "on-value", "off": "off-value"}, + "BOOLEAN": {"on": True, "off": False}, + "NUMERIC": {"on": 1.5, "off": 0.0}, + "INTEGER": {"on": 42, "off": 0}, + } + var_values = values[variation_type] + + return { + "createdAt": "2024-04-17T19:40:53.716Z", + "format": "SERVER", + "environment": {"name": "Test"}, + "flags": { + flag_key: { + "key": flag_key, + "enabled": enabled, + "variationType": variation_type, + "variations": { + "on": {"key": "on", "value": var_values["on"]}, + "off": {"key": "off", "value": var_values["off"]}, + }, + "allocations": [ + { + "key": "default-allocation", + "rules": [], + "splits": [{"variationKey": variant_key, "shards": []}], + "doLog": True, + } + ], + } + }, + } + + +def make_multi_flag_fixture(flag_keys: list[str]) -> JSON: + fixture = make_ufc_fixture(flag_keys[0]) + flags = cast("JSON", fixture["flags"]) + for flag_key in flag_keys[1:]: + flags[flag_key] = cast("JSON", make_ufc_fixture(flag_key)["flags"])[flag_key] + return fixture + + +def evaluate_flag( + flag_key: str, + *, + targeting_key: str = "user-1", + attributes: JSON | None = None, + variation_type: str = "STRING", + default_value: object = "default", +) -> HttpResponse: + return weblog.post( + "/ffe", + json={ + "flag": flag_key, + "variationType": variation_type, + "defaultValue": default_value, + "targetingKey": targeting_key, + "attributes": attributes or {}, + }, + ) + + +def wait_for_evp_flush() -> None: + time.sleep(EVP_FLUSH_WAIT_SECONDS) + + +def find_evp_flagevaluation_events(flag_key: str) -> list[tuple[JSON, JSON]]: + results: list[tuple[JSON, JSON]] = [] + + for data in interfaces.agent.get_data(path_filters=EVP_FLAGEVALUATIONS_PATH): + content = data["request"]["content"] + if not isinstance(content, dict): + continue + + events = content.get("flagEvaluations") + if not isinstance(events, list): + continue + + for event in events: + if not isinstance(event, dict): + continue + + flag = event.get("flag") + if isinstance(flag, dict) and flag.get("key") == flag_key: + results.append((cast("JSON", content), cast("JSON", event))) + + return results + + +def sum_evaluation_count(events: list[tuple[JSON, JSON]]) -> int: + total = 0 + for _, event in events: + count = event.get("evaluation_count") + if isinstance(count, int): + total += count + return total + + +def assert_no_reason_field(value: object, path: str = "$") -> None: + if isinstance(value, dict): + assert "reason" not in value, f"OpenFeature reason must not be emitted at {path}" + for key, child in value.items(): + assert_no_reason_field(child, f"{path}.{key}") + elif isinstance(value, list): + for index, child in enumerate(value): + assert_no_reason_field(child, f"{path}[{index}]") + + +def object_key(value: object, field_name: str) -> str | None: + if value is None: + return None + assert isinstance(value, dict), f"{field_name} must be an object when present" + key = value.get("key") + assert isinstance(key, str), f"{field_name}.key must be a string" + assert key, f"{field_name}.key must be non-empty" + return key + + +def assert_batch_context(batch: JSON) -> None: + context = batch.get("context") + assert isinstance(context, dict), "batch context must be an object" + assert context.get("service") == "weblog", f"expected service weblog, got {context}" + if "env" in context: + assert context["env"] == "system-tests", f"expected env system-tests, got {context}" + + +def assert_event_contract(event: JSON, flag_key: str) -> None: + assert_no_reason_field(event) + assert "targeting_rule" not in event, "targeting_rule must be omitted without real rule metadata" + + flag = event.get("flag") + assert isinstance(flag, dict), "flag must be an object" + assert flag.get("key") == flag_key, f"expected flag {flag_key}, got {flag}" + + timestamp = event.get("timestamp") + first_evaluation = event.get("first_evaluation") + last_evaluation = event.get("last_evaluation") + evaluation_count = event.get("evaluation_count") + + assert isinstance(timestamp, int), "timestamp must be an integer" + assert isinstance(first_evaluation, int), "first_evaluation must be an integer" + assert isinstance(last_evaluation, int), "last_evaluation must be an integer" + assert isinstance(evaluation_count, int), "evaluation_count must be an integer" + assert evaluation_count >= 1, f"evaluation_count must be positive: {event}" + assert first_evaluation <= last_evaluation, f"first_evaluation must be <= last_evaluation: {event}" + + object_key(event.get("variant"), "variant") + object_key(event.get("allocation"), "allocation") + + +def event_identity(event: JSON) -> str: + visible_identity = { + "flag": event.get("flag"), + "variant": event.get("variant"), + "allocation": event.get("allocation"), + "runtime_default_used": event.get("runtime_default_used"), + "targeting_key": event.get("targeting_key"), + "targeting_rule": event.get("targeting_rule"), + "error": event.get("error"), + "context": event.get("context"), + } + return json.dumps(visible_identity, sort_keys=True, default=str) + + +def assert_no_duplicate_visible_events(events: list[tuple[JSON, JSON]]) -> None: + seen: set[str] = set() + duplicates: set[str] = set() + for _, event in events: + identity = event_identity(event) + if identity in seen: + duplicates.add(identity) + seen.add(identity) + + assert not duplicates, f"found duplicate serialized-visible EVP buckets: {sorted(duplicates)}" + + +@scenarios.feature_flagging_and_experimentation +@features.feature_flags_evp_flagevaluation +class Test_FFE_EVP_Flagevaluation_Basic: + """Test that flag evaluation produces an EVP flagevaluation payload.""" + + def setup_ffe_evp_flagevaluation_basic(self) -> None: + config_id = "ffe-evp-basic" + self.flag_key = "evp-basic-flag" + rc.tracer_rc_state.reset().set_config(f"{RC_PATH}/{config_id}/config", make_ufc_fixture(self.flag_key)).apply() + + self.r = evaluate_flag(self.flag_key, targeting_key="evp-basic-user", attributes={}) + wait_for_evp_flush() + + def test_ffe_evp_flagevaluation_basic(self) -> None: + assert self.r.status_code == 200, f"Flag evaluation failed: {self.r.text}" + + events = find_evp_flagevaluation_events(self.flag_key) + assert events, f"Expected EVP flagevaluation event for flag {self.flag_key}" + + batch, event = events[0] + assert_batch_context(batch) + assert_event_contract(event, self.flag_key) + assert object_key(event.get("variant"), "variant") == "on" + assert object_key(event.get("allocation"), "allocation") == "default-allocation" + + +@scenarios.feature_flagging_and_experimentation +@features.feature_flags_evp_flagevaluation +class Test_FFE_EVP_Flagevaluation_Count: + """Test that repeated evaluations are counted in EVP flagevaluation payloads.""" + + def setup_ffe_evp_flagevaluation_count(self) -> None: + config_id = "ffe-evp-count" + self.flag_key = "evp-count-flag" + self.eval_count = 5 + rc.tracer_rc_state.reset().set_config(f"{RC_PATH}/{config_id}/config", make_ufc_fixture(self.flag_key)).apply() + + self.responses = [ + evaluate_flag(self.flag_key, targeting_key="evp-count-user", attributes={}) for _ in range(self.eval_count) + ] + wait_for_evp_flush() + + def test_ffe_evp_flagevaluation_count(self) -> None: + for index, response in enumerate(self.responses): + assert response.status_code == 200, f"Request {index + 1} failed: {response.text}" + + events = find_evp_flagevaluation_events(self.flag_key) + assert events, f"Expected EVP flagevaluation event for flag {self.flag_key}" + + for _, event in events: + assert_event_contract(event, self.flag_key) + + total_count = sum_evaluation_count(events) + assert total_count >= self.eval_count, f"Expected count >= {self.eval_count}, got {total_count}" + + +@scenarios.feature_flagging_and_experimentation +@features.feature_flags_evp_flagevaluation +class Test_FFE_EVP_Flagevaluation_Context_Bounds: + """Test that EVP evaluation context is bounded before it reaches payloads.""" + + def setup_ffe_evp_flagevaluation_context_bounds(self) -> None: + config_id = "ffe-evp-context-bounds" + self.flag_key = "evp-context-bounds-flag" + self.oversized_field = "field_010_oversized" + rc.tracer_rc_state.reset().set_config(f"{RC_PATH}/{config_id}/config", make_ufc_fixture(self.flag_key)).apply() + + attributes: JSON = {f"field_{index:03d}": f"value-{index}" for index in range(300)} + attributes[self.oversized_field] = "x" * 300 + + self.r = evaluate_flag(self.flag_key, targeting_key="evp-context-user", attributes=attributes) + wait_for_evp_flush() + + def test_ffe_evp_flagevaluation_context_bounds(self) -> None: + assert self.r.status_code == 200, f"Flag evaluation failed: {self.r.text}" + + events = find_evp_flagevaluation_events(self.flag_key) + assert events, f"Expected EVP flagevaluation event for flag {self.flag_key}" + + full_context_events = 0 + for _, event in events: + assert_event_contract(event, self.flag_key) + context = event.get("context") + if context is None: + continue + + assert isinstance(context, dict), "context must be an object when present" + evaluation_context = context.get("evaluation") + if evaluation_context is None: + continue + + full_context_events += 1 + assert isinstance(evaluation_context, dict), "context.evaluation must be an object" + assert len(evaluation_context) <= 256, f"context.evaluation has too many fields: {len(evaluation_context)}" + assert self.oversized_field not in evaluation_context, "oversized string context field must be omitted" + + if full_context_events == 0: + for _, event in events: + assert "context" not in event, f"degraded event should omit context: {event}" + + +@scenarios.feature_flagging_and_experimentation +@features.feature_flags_evp_flagevaluation +class Test_FFE_EVP_Flagevaluation_Runtime_Default: + """Test that runtime defaults are surfaced without OpenFeature reason.""" + + def setup_ffe_evp_flagevaluation_runtime_default(self) -> None: + rc.tracer_rc_state.reset().apply() + + self.flag_key = "evp-runtime-default-flag" + self.r = evaluate_flag(self.flag_key, targeting_key="evp-default-user", attributes={}) + wait_for_evp_flush() + + def test_ffe_evp_flagevaluation_runtime_default(self) -> None: + assert self.r.status_code == 200, f"Flag evaluation request failed: {self.r.text}" + + events = find_evp_flagevaluation_events(self.flag_key) + assert events, f"Expected EVP flagevaluation event for flag {self.flag_key}" + + for _, event in events: + assert_event_contract(event, self.flag_key) + + assert any(event.get("runtime_default_used") is True for _, event in events), ( + f"Expected runtime_default_used=true for flag {self.flag_key}, got {events}" + ) + + +@scenarios.feature_flagging_and_experimentation +@features.feature_flags_evp_flagevaluation +class Test_FFE_EVP_Flagevaluation_Load_Aggregation: + """Test CI-safe load aggregation without treating system-tests as a perf test.""" + + def setup_ffe_evp_flagevaluation_load_aggregation(self) -> None: + config_id = "ffe-evp-load-aggregation" + self.flag_keys = ["evp-load-flag-a", "evp-load-flag-b"] + self.evals_per_flag = 30 + rc.tracer_rc_state.reset().set_config( + f"{RC_PATH}/{config_id}/config", make_multi_flag_fixture(self.flag_keys) + ).apply() + + self.responses = [] + for flag_key in self.flag_keys: + for index in range(self.evals_per_flag): + self.responses.append( + evaluate_flag( + flag_key, + targeting_key=f"evp-load-user-{index % 10}", + attributes={"bucket": index % 10, "cohort": f"cohort-{index % 3}"}, + ) + ) + + wait_for_evp_flush() + + def test_ffe_evp_flagevaluation_load_aggregation(self) -> None: + for index, response in enumerate(self.responses): + assert response.status_code == 200, f"Request {index + 1} failed: {response.text}" + + for flag_key in self.flag_keys: + events = find_evp_flagevaluation_events(flag_key) + assert events, f"Expected EVP flagevaluation events for flag {flag_key}" + assert_no_duplicate_visible_events(events) + + for _, event in events: + assert_event_contract(event, flag_key) + + total_count = sum_evaluation_count(events) + assert total_count >= self.evals_per_flag, ( + f"Expected count >= {self.evals_per_flag} for {flag_key}, got {total_count}" + ) + + +@scenarios.feature_flagging_and_experimentation +@features.feature_flags_evp_flagevaluation +class Test_FFE_EVP_Flagevaluation_Degradation: + """Test degraded EVP shape when an SDK exposes a test cap override.""" + + def setup_ffe_evp_flagevaluation_degradation(self) -> None: + config_id = "ffe-evp-degradation" + self.flag_key = "evp-degradation-flag" + self.eval_count = 150 + rc.tracer_rc_state.reset().set_config(f"{RC_PATH}/{config_id}/config", make_ufc_fixture(self.flag_key)).apply() + + self.responses = [ + evaluate_flag( + self.flag_key, + targeting_key=f"evp-degradation-user-{index}", + attributes={"distinct": index}, + ) + for index in range(self.eval_count) + ] + wait_for_evp_flush() + + def test_ffe_evp_flagevaluation_degradation(self) -> None: + for index, response in enumerate(self.responses): + assert response.status_code == 200, f"Request {index + 1} failed: {response.text}" + + events = find_evp_flagevaluation_events(self.flag_key) + assert events, f"Expected EVP flagevaluation events for flag {self.flag_key}" + + degraded_events = [event for _, event in events if "context" not in event and "targeting_key" not in event] + if not degraded_events: + pytest.skip("No SDK/test cap override is available to force degraded EVP flagevaluation buckets") + + for event in degraded_events: + assert_event_contract(event, self.flag_key) + assert "context" not in event, f"degraded event must omit context: {event}" + assert "targeting_key" not in event, f"degraded event must omit targeting_key: {event}" + assert object_key(event.get("variant"), "variant") == "on" + assert object_key(event.get("allocation"), "allocation") == "default-allocation" diff --git a/utils/_features.py b/utils/_features.py index 01b424a2213..b091233ac6d 100644 --- a/utils/_features.py +++ b/utils/_features.py @@ -2705,6 +2705,14 @@ def feature_flags_eval_metrics(test_object): """ return _mark_test_object(test_object, feature_id=548, owner=_Owner.ffe) + @staticmethod + def feature_flags_evp_flagevaluation(test_object): + """Feature Flags EVP Flagevaluation + + https://feature-parity.us1.prod.dog/#/?feature=548 + """ + return _mark_test_object(test_object, feature_id=548, owner=_Owner.ffe) + @staticmethod def feature_flags_event_enrichment(test_object): """Feature Flags Event Enrichment (APM span tags) From 29743d2271485191beb57b1a7a5b4fe93fd2fb90 Mon Sep 17 00:00:00 2001 From: Leo Romanovsky Date: Tue, 16 Jun 2026 14:59:41 -0400 Subject: [PATCH 02/11] Strengthen EVP flagevaluation stress coverage --- manifests/golang.yml | 3 +- tests/ffe/test_flag_eval_evp.py | 92 +++++++++++++++++++++++++++++++-- 2 files changed, 88 insertions(+), 7 deletions(-) diff --git a/manifests/golang.yml b/manifests/golang.yml index bd25f0db411..33159533609 100644 --- a/manifests/golang.yml +++ b/manifests/golang.yml @@ -885,8 +885,7 @@ manifest: tests/ffe/test_dynamic_evaluation.py::Test_FFE_RC_Unavailable: v2.4.0 tests/ffe/test_dynamic_evaluation.py::Test_FFE_Unknown_Operator_Tolerance::test_unknown_operator_errors: bug (FFL-2182) tests/ffe/test_exposures.py: v2.6.0-dev # Easy win for chi, echo, gin, net-http, net-http-orchestrion, uds-echo and version 2.5.0 - tests/ffe/test_flag_eval_evp.py: v2.9.0-dev - tests/ffe/test_flag_eval_evp.py::Test_FFE_EVP_Flagevaluation_Degradation: missing_feature (FFL-2446 - no system-test cap override to force degradation) + tests/ffe/test_flag_eval_evp.py: missing_feature (FFL-2446) tests/ffe/test_flag_eval_metrics.py: v2.8.0 tests/ffe/test_flag_eval_metrics.py::Test_FFE_Eval_Metric_Parse_Error_Invalid_Regex: irrelevant (Go validates regex at config load time) tests/ffe/test_flag_eval_metrics.py::Test_FFE_Eval_Nested_Attributes_Ignored: irrelevant (FFL-1980) diff --git a/tests/ffe/test_flag_eval_evp.py b/tests/ffe/test_flag_eval_evp.py index 2ee5c7e0d7c..d8163c66a02 100644 --- a/tests/ffe/test_flag_eval_evp.py +++ b/tests/ffe/test_flag_eval_evp.py @@ -2,6 +2,7 @@ import json import time +from concurrent.futures import ThreadPoolExecutor from typing import Any from typing import cast @@ -128,6 +129,11 @@ def sum_evaluation_count(events: list[tuple[JSON, JSON]]) -> int: return total +def assert_total_evaluation_count(events: list[tuple[JSON, JSON]], expected: int, flag_key: str) -> None: + total_count = sum_evaluation_count(events) + assert total_count == expected, f"Expected count == {expected} for {flag_key}, got {total_count}" + + def assert_no_reason_field(value: object, path: str = "$") -> None: if isinstance(value, dict): assert "reason" not in value, f"OpenFeature reason must not be emitted at {path}" @@ -258,8 +264,8 @@ def test_ffe_evp_flagevaluation_count(self) -> None: for _, event in events: assert_event_contract(event, self.flag_key) - total_count = sum_evaluation_count(events) - assert total_count >= self.eval_count, f"Expected count >= {self.eval_count}, got {total_count}" + assert_no_duplicate_visible_events(events) + assert_total_evaluation_count(events, self.eval_count, self.flag_key) @scenarios.feature_flagging_and_experimentation @@ -371,10 +377,86 @@ def test_ffe_evp_flagevaluation_load_aggregation(self) -> None: for _, event in events: assert_event_contract(event, flag_key) - total_count = sum_evaluation_count(events) - assert total_count >= self.evals_per_flag, ( - f"Expected count >= {self.evals_per_flag} for {flag_key}, got {total_count}" + assert_total_evaluation_count(events, self.evals_per_flag, flag_key) + + +@scenarios.feature_flagging_and_experimentation +@features.feature_flags_evp_flagevaluation +class Test_FFE_EVP_Flagevaluation_Burst_Aggregation: + """Test a bounded request burst through the async EVP aggregation path.""" + + def setup_ffe_evp_flagevaluation_burst_aggregation(self) -> None: + config_id = "ffe-evp-burst-aggregation" + self.flag_key = "evp-burst-aggregation-flag" + self.eval_count = 512 + rc.tracer_rc_state.reset().set_config(f"{RC_PATH}/{config_id}/config", make_ufc_fixture(self.flag_key)).apply() + + with ThreadPoolExecutor(max_workers=32) as executor: + self.responses = list( + executor.map( + lambda _: evaluate_flag( + self.flag_key, + targeting_key="evp-burst-user", + attributes={"bucket": "burst", "cohort": "stress"}, + ), + range(self.eval_count), + ) + ) + + wait_for_evp_flush() + + def test_ffe_evp_flagevaluation_burst_aggregation(self) -> None: + for index, response in enumerate(self.responses): + assert response.status_code == 200, f"Request {index + 1} failed: {response.text}" + + events = find_evp_flagevaluation_events(self.flag_key) + assert events, f"Expected EVP flagevaluation events for flag {self.flag_key}" + + for _, event in events: + assert_event_contract(event, self.flag_key) + + assert_no_duplicate_visible_events(events) + assert_total_evaluation_count(events, self.eval_count, self.flag_key) + + +@scenarios.feature_flagging_and_experimentation +@features.feature_flags_evp_flagevaluation +class Test_FFE_EVP_Flagevaluation_High_Cardinality_Aggregation: + """Test many full-tier aggregation buckets stay distinct and counted.""" + + def setup_ffe_evp_flagevaluation_high_cardinality_aggregation(self) -> None: + config_id = "ffe-evp-high-cardinality-aggregation" + self.flag_key = "evp-high-cardinality-aggregation-flag" + self.eval_count = 128 + rc.tracer_rc_state.reset().set_config(f"{RC_PATH}/{config_id}/config", make_ufc_fixture(self.flag_key)).apply() + + self.responses = [ + evaluate_flag( + self.flag_key, + targeting_key=f"evp-cardinality-user-{index}", + attributes={ + "bucket": index, + "cohort": f"cohort-{index % 8}", + "typed": index % 2 == 0, + }, ) + for index in range(self.eval_count) + ] + + wait_for_evp_flush() + + def test_ffe_evp_flagevaluation_high_cardinality_aggregation(self) -> None: + for index, response in enumerate(self.responses): + assert response.status_code == 200, f"Request {index + 1} failed: {response.text}" + + events = find_evp_flagevaluation_events(self.flag_key) + assert events, f"Expected EVP flagevaluation events for flag {self.flag_key}" + + for _, event in events: + assert_event_contract(event, self.flag_key) + + assert_no_duplicate_visible_events(events) + assert_total_evaluation_count(events, self.eval_count, self.flag_key) @scenarios.feature_flagging_and_experimentation From d4aebe5dab7045292ca8ee57bd2fc7117f1c358b Mon Sep 17 00:00:00 2001 From: Leo Romanovsky Date: Tue, 16 Jun 2026 17:47:18 -0400 Subject: [PATCH 03/11] Remove fixed EVP flagevaluation setup sleeps --- tests/ffe/test_flag_eval_evp.py | 78 +++++++++++++++++++-------------- 1 file changed, 46 insertions(+), 32 deletions(-) diff --git a/tests/ffe/test_flag_eval_evp.py b/tests/ffe/test_flag_eval_evp.py index d8163c66a02..2df37203d31 100644 --- a/tests/ffe/test_flag_eval_evp.py +++ b/tests/ffe/test_flag_eval_evp.py @@ -1,7 +1,6 @@ """Test server-side feature flag evaluation counts via EVP flagevaluation.""" import json -import time from concurrent.futures import ThreadPoolExecutor from typing import Any from typing import cast @@ -19,7 +18,7 @@ RC_PRODUCT = "FFE_FLAGS" RC_PATH = f"datadog/2/{RC_PRODUCT}" EVP_FLAGEVALUATIONS_PATH = "/api/v2/flagevaluations" -EVP_FLUSH_WAIT_SECONDS = 12 +EVP_WAIT_TIMEOUT_SECONDS = 30 JSON = dict[str, Any] @@ -93,29 +92,46 @@ def evaluate_flag( ) -def wait_for_evp_flush() -> None: - time.sleep(EVP_FLUSH_WAIT_SECONDS) +def evp_flagevaluation_events_from_data(data: JSON, flag_key: str) -> list[tuple[JSON, JSON]]: + if data.get("path") != EVP_FLAGEVALUATIONS_PATH: + return [] + request = data.get("request") + if not isinstance(request, dict): + return [] -def find_evp_flagevaluation_events(flag_key: str) -> list[tuple[JSON, JSON]]: - results: list[tuple[JSON, JSON]] = [] + content = request.get("content") + if not isinstance(content, dict): + return [] - for data in interfaces.agent.get_data(path_filters=EVP_FLAGEVALUATIONS_PATH): - content = data["request"]["content"] - if not isinstance(content, dict): - continue + events = content.get("flagEvaluations") + if not isinstance(events, list): + return [] - events = content.get("flagEvaluations") - if not isinstance(events, list): + results: list[tuple[JSON, JSON]] = [] + for event in events: + if not isinstance(event, dict): continue - for event in events: - if not isinstance(event, dict): - continue + flag = event.get("flag") + if isinstance(flag, dict) and flag.get("key") == flag_key: + results.append((cast("JSON", content), cast("JSON", event))) + + return results + + +def wait_for_evp_flagevaluation_event(flag_key: str) -> None: + assert interfaces.agent.wait_for( + lambda data: bool(evp_flagevaluation_events_from_data(cast("JSON", data), flag_key)), + timeout=EVP_WAIT_TIMEOUT_SECONDS, + ), f"Timed out waiting for EVP flagevaluation event for flag {flag_key}" - flag = event.get("flag") - if isinstance(flag, dict) and flag.get("key") == flag_key: - results.append((cast("JSON", content), cast("JSON", event))) + +def find_evp_flagevaluation_events(flag_key: str) -> list[tuple[JSON, JSON]]: + results: list[tuple[JSON, JSON]] = [] + + for data in interfaces.agent.get_data(path_filters=EVP_FLAGEVALUATIONS_PATH): + results.extend(evp_flagevaluation_events_from_data(cast("JSON", data), flag_key)) return results @@ -201,15 +217,16 @@ def event_identity(event: JSON) -> str: def assert_no_duplicate_visible_events(events: list[tuple[JSON, JSON]]) -> None: - seen: set[str] = set() + seen_by_batch: dict[int, set[str]] = {} duplicates: set[str] = set() - for _, event in events: + for batch, event in events: + seen = seen_by_batch.setdefault(id(batch), set()) identity = event_identity(event) if identity in seen: duplicates.add(identity) seen.add(identity) - assert not duplicates, f"found duplicate serialized-visible EVP buckets: {sorted(duplicates)}" + assert not duplicates, f"found duplicate serialized-visible EVP buckets in one payload: {sorted(duplicates)}" @scenarios.feature_flagging_and_experimentation @@ -223,11 +240,11 @@ def setup_ffe_evp_flagevaluation_basic(self) -> None: rc.tracer_rc_state.reset().set_config(f"{RC_PATH}/{config_id}/config", make_ufc_fixture(self.flag_key)).apply() self.r = evaluate_flag(self.flag_key, targeting_key="evp-basic-user", attributes={}) - wait_for_evp_flush() def test_ffe_evp_flagevaluation_basic(self) -> None: assert self.r.status_code == 200, f"Flag evaluation failed: {self.r.text}" + wait_for_evp_flagevaluation_event(self.flag_key) events = find_evp_flagevaluation_events(self.flag_key) assert events, f"Expected EVP flagevaluation event for flag {self.flag_key}" @@ -252,12 +269,12 @@ def setup_ffe_evp_flagevaluation_count(self) -> None: self.responses = [ evaluate_flag(self.flag_key, targeting_key="evp-count-user", attributes={}) for _ in range(self.eval_count) ] - wait_for_evp_flush() def test_ffe_evp_flagevaluation_count(self) -> None: for index, response in enumerate(self.responses): assert response.status_code == 200, f"Request {index + 1} failed: {response.text}" + wait_for_evp_flagevaluation_event(self.flag_key) events = find_evp_flagevaluation_events(self.flag_key) assert events, f"Expected EVP flagevaluation event for flag {self.flag_key}" @@ -283,11 +300,11 @@ def setup_ffe_evp_flagevaluation_context_bounds(self) -> None: attributes[self.oversized_field] = "x" * 300 self.r = evaluate_flag(self.flag_key, targeting_key="evp-context-user", attributes=attributes) - wait_for_evp_flush() def test_ffe_evp_flagevaluation_context_bounds(self) -> None: assert self.r.status_code == 200, f"Flag evaluation failed: {self.r.text}" + wait_for_evp_flagevaluation_event(self.flag_key) events = find_evp_flagevaluation_events(self.flag_key) assert events, f"Expected EVP flagevaluation event for flag {self.flag_key}" @@ -323,11 +340,11 @@ def setup_ffe_evp_flagevaluation_runtime_default(self) -> None: self.flag_key = "evp-runtime-default-flag" self.r = evaluate_flag(self.flag_key, targeting_key="evp-default-user", attributes={}) - wait_for_evp_flush() def test_ffe_evp_flagevaluation_runtime_default(self) -> None: assert self.r.status_code == 200, f"Flag evaluation request failed: {self.r.text}" + wait_for_evp_flagevaluation_event(self.flag_key) events = find_evp_flagevaluation_events(self.flag_key) assert events, f"Expected EVP flagevaluation event for flag {self.flag_key}" @@ -363,13 +380,12 @@ def setup_ffe_evp_flagevaluation_load_aggregation(self) -> None: ) ) - wait_for_evp_flush() - def test_ffe_evp_flagevaluation_load_aggregation(self) -> None: for index, response in enumerate(self.responses): assert response.status_code == 200, f"Request {index + 1} failed: {response.text}" for flag_key in self.flag_keys: + wait_for_evp_flagevaluation_event(flag_key) events = find_evp_flagevaluation_events(flag_key) assert events, f"Expected EVP flagevaluation events for flag {flag_key}" assert_no_duplicate_visible_events(events) @@ -403,12 +419,11 @@ def setup_ffe_evp_flagevaluation_burst_aggregation(self) -> None: ) ) - wait_for_evp_flush() - def test_ffe_evp_flagevaluation_burst_aggregation(self) -> None: for index, response in enumerate(self.responses): assert response.status_code == 200, f"Request {index + 1} failed: {response.text}" + wait_for_evp_flagevaluation_event(self.flag_key) events = find_evp_flagevaluation_events(self.flag_key) assert events, f"Expected EVP flagevaluation events for flag {self.flag_key}" @@ -443,12 +458,11 @@ def setup_ffe_evp_flagevaluation_high_cardinality_aggregation(self) -> None: for index in range(self.eval_count) ] - wait_for_evp_flush() - def test_ffe_evp_flagevaluation_high_cardinality_aggregation(self) -> None: for index, response in enumerate(self.responses): assert response.status_code == 200, f"Request {index + 1} failed: {response.text}" + wait_for_evp_flagevaluation_event(self.flag_key) events = find_evp_flagevaluation_events(self.flag_key) assert events, f"Expected EVP flagevaluation events for flag {self.flag_key}" @@ -478,12 +492,12 @@ def setup_ffe_evp_flagevaluation_degradation(self) -> None: ) for index in range(self.eval_count) ] - wait_for_evp_flush() def test_ffe_evp_flagevaluation_degradation(self) -> None: for index, response in enumerate(self.responses): assert response.status_code == 200, f"Request {index + 1} failed: {response.text}" + wait_for_evp_flagevaluation_event(self.flag_key) events = find_evp_flagevaluation_events(self.flag_key) assert events, f"Expected EVP flagevaluation events for flag {self.flag_key}" From bfc5f34192614bd3aa9c2ee0987625c058288d44 Mon Sep 17 00:00:00 2001 From: Leo Romanovsky Date: Tue, 16 Jun 2026 18:38:10 -0400 Subject: [PATCH 04/11] Skip disabled EVP flagevaluation tests before setup --- tests/ffe/test_flag_eval_evp.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/ffe/test_flag_eval_evp.py b/tests/ffe/test_flag_eval_evp.py index 2df37203d31..0ff64c4ec82 100644 --- a/tests/ffe/test_flag_eval_evp.py +++ b/tests/ffe/test_flag_eval_evp.py @@ -231,6 +231,7 @@ def assert_no_duplicate_visible_events(events: list[tuple[JSON, JSON]]) -> None: @scenarios.feature_flagging_and_experimentation @features.feature_flags_evp_flagevaluation +@pytest.mark.skip_if_xfail class Test_FFE_EVP_Flagevaluation_Basic: """Test that flag evaluation produces an EVP flagevaluation payload.""" @@ -257,6 +258,7 @@ def test_ffe_evp_flagevaluation_basic(self) -> None: @scenarios.feature_flagging_and_experimentation @features.feature_flags_evp_flagevaluation +@pytest.mark.skip_if_xfail class Test_FFE_EVP_Flagevaluation_Count: """Test that repeated evaluations are counted in EVP flagevaluation payloads.""" @@ -287,6 +289,7 @@ def test_ffe_evp_flagevaluation_count(self) -> None: @scenarios.feature_flagging_and_experimentation @features.feature_flags_evp_flagevaluation +@pytest.mark.skip_if_xfail class Test_FFE_EVP_Flagevaluation_Context_Bounds: """Test that EVP evaluation context is bounded before it reaches payloads.""" @@ -332,6 +335,7 @@ def test_ffe_evp_flagevaluation_context_bounds(self) -> None: @scenarios.feature_flagging_and_experimentation @features.feature_flags_evp_flagevaluation +@pytest.mark.skip_if_xfail class Test_FFE_EVP_Flagevaluation_Runtime_Default: """Test that runtime defaults are surfaced without OpenFeature reason.""" @@ -358,6 +362,7 @@ def test_ffe_evp_flagevaluation_runtime_default(self) -> None: @scenarios.feature_flagging_and_experimentation @features.feature_flags_evp_flagevaluation +@pytest.mark.skip_if_xfail class Test_FFE_EVP_Flagevaluation_Load_Aggregation: """Test CI-safe load aggregation without treating system-tests as a perf test.""" @@ -398,6 +403,7 @@ def test_ffe_evp_flagevaluation_load_aggregation(self) -> None: @scenarios.feature_flagging_and_experimentation @features.feature_flags_evp_flagevaluation +@pytest.mark.skip_if_xfail class Test_FFE_EVP_Flagevaluation_Burst_Aggregation: """Test a bounded request burst through the async EVP aggregation path.""" @@ -436,6 +442,7 @@ def test_ffe_evp_flagevaluation_burst_aggregation(self) -> None: @scenarios.feature_flagging_and_experimentation @features.feature_flags_evp_flagevaluation +@pytest.mark.skip_if_xfail class Test_FFE_EVP_Flagevaluation_High_Cardinality_Aggregation: """Test many full-tier aggregation buckets stay distinct and counted.""" @@ -475,6 +482,7 @@ def test_ffe_evp_flagevaluation_high_cardinality_aggregation(self) -> None: @scenarios.feature_flagging_and_experimentation @features.feature_flags_evp_flagevaluation +@pytest.mark.skip_if_xfail class Test_FFE_EVP_Flagevaluation_Degradation: """Test degraded EVP shape when an SDK exposes a test cap override.""" From 431453adbe3e31e3b14af67d2eb9da09856709cc Mon Sep 17 00:00:00 2001 From: Leo Romanovsky Date: Tue, 16 Jun 2026 20:42:46 -0400 Subject: [PATCH 05/11] Enable EVP flagevaluation tests for Go --- manifests/golang.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manifests/golang.yml b/manifests/golang.yml index 33159533609..3050c10014f 100644 --- a/manifests/golang.yml +++ b/manifests/golang.yml @@ -885,7 +885,7 @@ manifest: tests/ffe/test_dynamic_evaluation.py::Test_FFE_RC_Unavailable: v2.4.0 tests/ffe/test_dynamic_evaluation.py::Test_FFE_Unknown_Operator_Tolerance::test_unknown_operator_errors: bug (FFL-2182) tests/ffe/test_exposures.py: v2.6.0-dev # Easy win for chi, echo, gin, net-http, net-http-orchestrion, uds-echo and version 2.5.0 - tests/ffe/test_flag_eval_evp.py: missing_feature (FFL-2446) + tests/ffe/test_flag_eval_evp.py: v2.10.0-dev tests/ffe/test_flag_eval_metrics.py: v2.8.0 tests/ffe/test_flag_eval_metrics.py::Test_FFE_Eval_Metric_Parse_Error_Invalid_Regex: irrelevant (Go validates regex at config load time) tests/ffe/test_flag_eval_metrics.py::Test_FFE_Eval_Nested_Attributes_Ignored: irrelevant (FFL-1980) From b8e86f6228660ae83a7fe02b53190567d9a048ab Mon Sep 17 00:00:00 2001 From: Leo Romanovsky Date: Tue, 16 Jun 2026 21:08:22 -0400 Subject: [PATCH 06/11] Address EVP flagevaluation review feedback --- manifests/ruby_lambda.yml | 1 + tests/ffe/_fixtures.py | 62 +++++++++++++++++++++++++++++ tests/ffe/test_exposures.py | 32 +-------------- tests/ffe/test_flag_eval_evp.py | 50 +++-------------------- tests/ffe/test_flag_eval_metrics.py | 37 +---------------- utils/_features.py | 4 +- 6 files changed, 72 insertions(+), 114 deletions(-) create mode 100644 tests/ffe/_fixtures.py diff --git a/manifests/ruby_lambda.yml b/manifests/ruby_lambda.yml index 478a405f7f7..bb35a24816b 100644 --- a/manifests/ruby_lambda.yml +++ b/manifests/ruby_lambda.yml @@ -77,3 +77,4 @@ manifest: tests/appsec/waf/test_blocking.py::Test_Blocking::test_html_template_v2: missing_feature tests/appsec/waf/test_blocking.py::Test_Blocking_strip_response_headers: missing_feature tests/appsec/waf/test_blocking.py::Test_CustomBlockingResponse: v3.29.0 + tests/ffe/test_flag_eval_evp.py: irrelevant (server-side FFE EVP weblog scenario is not applicable to lambda) diff --git a/tests/ffe/_fixtures.py b/tests/ffe/_fixtures.py new file mode 100644 index 00000000000..6342544a990 --- /dev/null +++ b/tests/ffe/_fixtures.py @@ -0,0 +1,62 @@ +"""Shared FFE Remote Config fixtures for system tests.""" + +from typing import Any + + +JSON = dict[str, Any] +VariationValue = str | bool | float | int + + +DEFAULT_VARIATION_VALUES: dict[str, dict[str, VariationValue]] = { + "STRING": {"on": "on-value", "off": "off-value"}, + "BOOLEAN": {"on": True, "off": False}, + "NUMERIC": {"on": 1.5, "off": 0.0}, + "INTEGER": {"on": 42, "off": 0}, +} + + +def make_ufc_fixture( + flag_key: str, + variant_key: str = "on", + variation_type: str = "STRING", + *, + enabled: bool = True, + allocation_key: str = "default-allocation", + variation_values: dict[str, VariationValue] | None = None, +) -> JSON: + values = variation_values or DEFAULT_VARIATION_VALUES[variation_type] + + return { + "createdAt": "2024-04-17T19:40:53.716Z", + "format": "SERVER", + "environment": {"name": "Test"}, + "flags": { + flag_key: { + "key": flag_key, + "enabled": enabled, + "variationType": variation_type, + "variations": {key: {"key": key, "value": value} for key, value in values.items()}, + "allocations": [ + { + "key": allocation_key, + "rules": [], + "splits": [{"variationKey": variant_key, "shards": []}], + "doLog": True, + } + ], + } + }, + } + + +def make_exposure_ufc_fixture( + flag_key: str, + variant_key: str = "variant-a", + allocation_key: str = "default-allocation", +) -> JSON: + return make_ufc_fixture( + flag_key, + variant_key, + allocation_key=allocation_key, + variation_values={"variant-a": "value-a", "variant-b": "value-b"}, + ) diff --git a/tests/ffe/test_exposures.py b/tests/ffe/test_exposures.py index faf3cb64730..1cbad04b08e 100644 --- a/tests/ffe/test_exposures.py +++ b/tests/ffe/test_exposures.py @@ -2,6 +2,7 @@ import json +from tests.ffe._fixtures import make_exposure_ufc_fixture as make_ufc_fixture from utils import ( weblog, interfaces, @@ -446,37 +447,6 @@ def count_exposure_events(flag_key: str, subject_id: str | None = None) -> int: return count -def make_ufc_fixture(flag_key: str, variant_key: str = "variant-a", allocation_key: str = "default-allocation"): - """Create a UFC fixture with the given flag key and variant. - - Each test should use a unique flag_key to avoid counting exposures from other tests. - """ - return { - "createdAt": "2024-04-17T19:40:53.716Z", - "format": "SERVER", - "environment": {"name": "Test"}, - "flags": { - flag_key: { - "key": flag_key, - "enabled": True, - "variationType": "STRING", - "variations": { - "variant-a": {"key": "variant-a", "value": "value-a"}, - "variant-b": {"key": "variant-b", "value": "value-b"}, - }, - "allocations": [ - { - "key": allocation_key, - "rules": [], - "splits": [{"variationKey": variant_key, "shards": []}], - "doLog": True, - } - ], - } - }, - } - - @scenarios.feature_flagging_and_experimentation @features.feature_flags_exposures class Test_FFE_Exposure_Caching_Same_Subject: diff --git a/tests/ffe/test_flag_eval_evp.py b/tests/ffe/test_flag_eval_evp.py index 0ff64c4ec82..69e26e4240d 100644 --- a/tests/ffe/test_flag_eval_evp.py +++ b/tests/ffe/test_flag_eval_evp.py @@ -2,11 +2,11 @@ import json from concurrent.futures import ThreadPoolExecutor -from typing import Any from typing import cast import pytest +from tests.ffe._fixtures import JSON, make_ufc_fixture from utils import HttpResponse from utils import features from utils import interfaces @@ -20,49 +20,6 @@ EVP_FLAGEVALUATIONS_PATH = "/api/v2/flagevaluations" EVP_WAIT_TIMEOUT_SECONDS = 30 -JSON = dict[str, Any] - - -def make_ufc_fixture( - flag_key: str, - variant_key: str = "on", - variation_type: str = "STRING", - *, - enabled: bool = True, -) -> JSON: - values: dict[str, dict[str, str | bool | float | int]] = { - "STRING": {"on": "on-value", "off": "off-value"}, - "BOOLEAN": {"on": True, "off": False}, - "NUMERIC": {"on": 1.5, "off": 0.0}, - "INTEGER": {"on": 42, "off": 0}, - } - var_values = values[variation_type] - - return { - "createdAt": "2024-04-17T19:40:53.716Z", - "format": "SERVER", - "environment": {"name": "Test"}, - "flags": { - flag_key: { - "key": flag_key, - "enabled": enabled, - "variationType": variation_type, - "variations": { - "on": {"key": "on", "value": var_values["on"]}, - "off": {"key": "off", "value": var_values["off"]}, - }, - "allocations": [ - { - "key": "default-allocation", - "rules": [], - "splits": [{"variationKey": variant_key, "shards": []}], - "doLog": True, - } - ], - } - }, - } - def make_multi_flag_fixture(flag_keys: list[str]) -> JSON: fixture = make_ufc_fixture(flag_keys[0]) @@ -511,7 +468,10 @@ def test_ffe_evp_flagevaluation_degradation(self) -> None: degraded_events = [event for _, event in events if "context" not in event and "targeting_key" not in event] if not degraded_events: - pytest.skip("No SDK/test cap override is available to force degraded EVP flagevaluation buckets") + pytest.skip( + "No SDK/test cap override is available; production EVP flagevaluation caps are too high for " + "a CI-safe degradation system test" + ) for event in degraded_events: assert_event_contract(event, self.flag_key) diff --git a/tests/ffe/test_flag_eval_metrics.py b/tests/ffe/test_flag_eval_metrics.py index e7759610f7a..cefba4d0ce6 100644 --- a/tests/ffe/test_flag_eval_metrics.py +++ b/tests/ffe/test_flag_eval_metrics.py @@ -1,5 +1,6 @@ """Test feature flag evaluation metrics via OTel Metrics API.""" +from tests.ffe._fixtures import make_ufc_fixture from utils import ( weblog, interfaces, @@ -13,42 +14,6 @@ RC_PATH = f"datadog/2/{RC_PRODUCT}" -def make_ufc_fixture(flag_key: str, variant_key: str = "on", variation_type: str = "STRING", *, enabled: bool = True): - """Create a UFC fixture with the given flag configuration.""" - values: dict[str, dict[str, str | bool | float | int]] = { - "STRING": {"on": "on-value", "off": "off-value"}, - "BOOLEAN": {"on": True, "off": False}, - "NUMERIC": {"on": 1.5, "off": 0.0}, # Decimal value for type_mismatch testing (NUMERIC→INTEGER) - "INTEGER": {"on": 42, "off": 0}, - } - var_values = values[variation_type] - - return { - "createdAt": "2024-04-17T19:40:53.716Z", - "format": "SERVER", - "environment": {"name": "Test"}, - "flags": { - flag_key: { - "key": flag_key, - "enabled": enabled, - "variationType": variation_type, - "variations": { - "on": {"key": "on", "value": var_values["on"]}, - "off": {"key": "off", "value": var_values["off"]}, - }, - "allocations": [ - { - "key": "default-allocation", - "rules": [], - "splits": [{"variationKey": variant_key, "shards": []}], - "doLog": True, - } - ], - } - }, - } - - def find_eval_metrics(flag_key: str | None = None): """Find feature_flag.evaluations metrics in agent data. diff --git a/utils/_features.py b/utils/_features.py index b091233ac6d..00c73e3b026 100644 --- a/utils/_features.py +++ b/utils/_features.py @@ -2709,9 +2709,9 @@ def feature_flags_eval_metrics(test_object): def feature_flags_evp_flagevaluation(test_object): """Feature Flags EVP Flagevaluation - https://feature-parity.us1.prod.dog/#/?feature=548 + https://feature-parity.us1.prod.dog/#/?feature=540 """ - return _mark_test_object(test_object, feature_id=548, owner=_Owner.ffe) + return _mark_test_object(test_object, feature_id=540, owner=_Owner.ffe) @staticmethod def feature_flags_event_enrichment(test_object): From 411be120d2fe806d36fa821719fc12813f83560d Mon Sep 17 00:00:00 2001 From: Leo Romanovsky Date: Tue, 16 Jun 2026 21:19:45 -0400 Subject: [PATCH 07/11] Make EVP degradation test hit production cap naturally --- tests/ffe/test_flag_eval_evp.py | 41 +++++++++++++++++++++------------ 1 file changed, 26 insertions(+), 15 deletions(-) diff --git a/tests/ffe/test_flag_eval_evp.py b/tests/ffe/test_flag_eval_evp.py index 69e26e4240d..c67e75df44b 100644 --- a/tests/ffe/test_flag_eval_evp.py +++ b/tests/ffe/test_flag_eval_evp.py @@ -19,6 +19,9 @@ RC_PATH = f"datadog/2/{RC_PRODUCT}" EVP_FLAGEVALUATIONS_PATH = "/api/v2/flagevaluations" EVP_WAIT_TIMEOUT_SECONDS = 30 +EVP_LOAD_WAIT_TIMEOUT_SECONDS = 60 +EVP_FULL_TIER_PER_FLAG_CAP = 10_000 +EVP_DEGRADATION_OVERFLOW_EVALS = 50 def make_multi_flag_fixture(flag_keys: list[str]) -> JSON: @@ -102,6 +105,13 @@ def sum_evaluation_count(events: list[tuple[JSON, JSON]]) -> int: return total +def wait_for_evp_flagevaluation_count(flag_key: str, expected: int) -> None: + assert interfaces.agent.wait_for( + lambda _: sum_evaluation_count(find_evp_flagevaluation_events(flag_key)) >= expected, + timeout=EVP_LOAD_WAIT_TIMEOUT_SECONDS, + ), f"Timed out waiting for EVP flagevaluation count >= {expected} for flag {flag_key}" + + def assert_total_evaluation_count(events: list[tuple[JSON, JSON]], expected: int, flag_key: str) -> None: total_count = sum_evaluation_count(events) assert total_count == expected, f"Expected count == {expected} for {flag_key}, got {total_count}" @@ -441,37 +451,38 @@ def test_ffe_evp_flagevaluation_high_cardinality_aggregation(self) -> None: @features.feature_flags_evp_flagevaluation @pytest.mark.skip_if_xfail class Test_FFE_EVP_Flagevaluation_Degradation: - """Test degraded EVP shape when an SDK exposes a test cap override.""" + """Test degraded EVP shape after the production per-flag full-tier cap is exceeded.""" def setup_ffe_evp_flagevaluation_degradation(self) -> None: config_id = "ffe-evp-degradation" self.flag_key = "evp-degradation-flag" - self.eval_count = 150 + self.eval_count = EVP_FULL_TIER_PER_FLAG_CAP + EVP_DEGRADATION_OVERFLOW_EVALS rc.tracer_rc_state.reset().set_config(f"{RC_PATH}/{config_id}/config", make_ufc_fixture(self.flag_key)).apply() - self.responses = [ - evaluate_flag( - self.flag_key, - targeting_key=f"evp-degradation-user-{index}", - attributes={"distinct": index}, + with ThreadPoolExecutor(max_workers=64) as executor: + self.responses = list( + executor.map( + lambda index: evaluate_flag( + self.flag_key, + targeting_key=f"evp-degradation-user-{index}", + attributes={}, + ), + range(self.eval_count), + ) ) - for index in range(self.eval_count) - ] def test_ffe_evp_flagevaluation_degradation(self) -> None: for index, response in enumerate(self.responses): assert response.status_code == 200, f"Request {index + 1} failed: {response.text}" - wait_for_evp_flagevaluation_event(self.flag_key) + wait_for_evp_flagevaluation_count(self.flag_key, self.eval_count) events = find_evp_flagevaluation_events(self.flag_key) assert events, f"Expected EVP flagevaluation events for flag {self.flag_key}" degraded_events = [event for _, event in events if "context" not in event and "targeting_key" not in event] - if not degraded_events: - pytest.skip( - "No SDK/test cap override is available; production EVP flagevaluation caps are too high for " - "a CI-safe degradation system test" - ) + assert degraded_events, f"Expected degraded EVP flagevaluation buckets for flag {self.flag_key}" + assert_no_duplicate_visible_events(events) + assert_total_evaluation_count(events, self.eval_count, self.flag_key) for event in degraded_events: assert_event_contract(event, self.flag_key) From 8d9591ca5412b27db7fe4e85d132e2d23b07e221 Mon Sep 17 00:00:00 2001 From: Leo Romanovsky Date: Tue, 16 Jun 2026 21:25:08 -0400 Subject: [PATCH 08/11] Move FFE fixtures under allowed utils package --- tests/ffe/test_exposures.py | 2 +- tests/ffe/test_flag_eval_evp.py | 2 +- tests/ffe/test_flag_eval_metrics.py | 2 +- tests/ffe/utils/__init__.py | 1 + tests/ffe/{_fixtures.py => utils/fixtures.py} | 0 5 files changed, 4 insertions(+), 3 deletions(-) create mode 100644 tests/ffe/utils/__init__.py rename tests/ffe/{_fixtures.py => utils/fixtures.py} (100%) diff --git a/tests/ffe/test_exposures.py b/tests/ffe/test_exposures.py index 1cbad04b08e..d2b66abe8bb 100644 --- a/tests/ffe/test_exposures.py +++ b/tests/ffe/test_exposures.py @@ -2,7 +2,7 @@ import json -from tests.ffe._fixtures import make_exposure_ufc_fixture as make_ufc_fixture +from tests.ffe.utils.fixtures import make_exposure_ufc_fixture as make_ufc_fixture from utils import ( weblog, interfaces, diff --git a/tests/ffe/test_flag_eval_evp.py b/tests/ffe/test_flag_eval_evp.py index c67e75df44b..b376269a41a 100644 --- a/tests/ffe/test_flag_eval_evp.py +++ b/tests/ffe/test_flag_eval_evp.py @@ -6,7 +6,7 @@ import pytest -from tests.ffe._fixtures import JSON, make_ufc_fixture +from tests.ffe.utils.fixtures import JSON, make_ufc_fixture from utils import HttpResponse from utils import features from utils import interfaces diff --git a/tests/ffe/test_flag_eval_metrics.py b/tests/ffe/test_flag_eval_metrics.py index cefba4d0ce6..245bfb169a5 100644 --- a/tests/ffe/test_flag_eval_metrics.py +++ b/tests/ffe/test_flag_eval_metrics.py @@ -1,6 +1,6 @@ """Test feature flag evaluation metrics via OTel Metrics API.""" -from tests.ffe._fixtures import make_ufc_fixture +from tests.ffe.utils.fixtures import make_ufc_fixture from utils import ( weblog, interfaces, diff --git a/tests/ffe/utils/__init__.py b/tests/ffe/utils/__init__.py new file mode 100644 index 00000000000..71f935c48db --- /dev/null +++ b/tests/ffe/utils/__init__.py @@ -0,0 +1 @@ +"""FFE test utilities.""" diff --git a/tests/ffe/_fixtures.py b/tests/ffe/utils/fixtures.py similarity index 100% rename from tests/ffe/_fixtures.py rename to tests/ffe/utils/fixtures.py From 15cfff8cd4d00d967451cfef160a971dc9146bed Mon Sep 17 00:00:00 2001 From: Leo Romanovsky Date: Tue, 16 Jun 2026 22:48:22 -0400 Subject: [PATCH 09/11] Make EVP degradation test use batched evaluations --- tests/ffe/test_flag_eval_evp.py | 36 ++++++---- .../FeatureFlagEvaluatorController.cs | 34 +++++---- .../docker/golang/app/_shared/common/ffe.go | 42 ++++++----- .../FeatureFlagEvaluatorController.java | 70 ++++++++++++------- utils/build/docker/nodejs/express/app.js | 44 ++++++------ utils/build/docker/php/common/ffe.php | 12 +++- utils/build/docker/python/flask/app.py | 32 +++++---- .../controllers/open_feature_controller.rb | 43 +++++++----- 8 files changed, 191 insertions(+), 122 deletions(-) diff --git a/tests/ffe/test_flag_eval_evp.py b/tests/ffe/test_flag_eval_evp.py index b376269a41a..b9013c9da46 100644 --- a/tests/ffe/test_flag_eval_evp.py +++ b/tests/ffe/test_flag_eval_evp.py @@ -36,20 +36,21 @@ def evaluate_flag( flag_key: str, *, targeting_key: str = "user-1", + targeting_keys: list[str] | None = None, attributes: JSON | None = None, variation_type: str = "STRING", default_value: object = "default", ) -> HttpResponse: - return weblog.post( - "/ffe", - json={ - "flag": flag_key, - "variationType": variation_type, - "defaultValue": default_value, - "targetingKey": targeting_key, - "attributes": attributes or {}, - }, - ) + payload = { + "flag": flag_key, + "variationType": variation_type, + "defaultValue": default_value, + "targetingKey": targeting_key, + "attributes": attributes or {}, + } + if targeting_keys is not None: + payload["targetingKeys"] = targeting_keys + return weblog.post("/ffe", json=payload) def evp_flagevaluation_events_from_data(data: JSON, flag_key: str) -> list[tuple[JSON, JSON]]: @@ -459,15 +460,22 @@ def setup_ffe_evp_flagevaluation_degradation(self) -> None: self.eval_count = EVP_FULL_TIER_PER_FLAG_CAP + EVP_DEGRADATION_OVERFLOW_EVALS rc.tracer_rc_state.reset().set_config(f"{RC_PATH}/{config_id}/config", make_ufc_fixture(self.flag_key)).apply() - with ThreadPoolExecutor(max_workers=64) as executor: + batch_size = 400 + batches = [ + [f"evp-degradation-user-{index}" for index in range(start, min(start + batch_size, self.eval_count))] + for start in range(0, self.eval_count, batch_size) + ] + + with ThreadPoolExecutor(max_workers=8) as executor: self.responses = list( executor.map( - lambda index: evaluate_flag( + lambda targeting_keys: evaluate_flag( self.flag_key, - targeting_key=f"evp-degradation-user-{index}", + targeting_key=targeting_keys[0], + targeting_keys=targeting_keys, attributes={}, ), - range(self.eval_count), + batches, ) ) diff --git a/utils/build/docker/dotnet/weblog/Controllers/FeatureFlagEvaluatorController.cs b/utils/build/docker/dotnet/weblog/Controllers/FeatureFlagEvaluatorController.cs index 3d0ad54290b..4825b9b999e 100644 --- a/utils/build/docker/dotnet/weblog/Controllers/FeatureFlagEvaluatorController.cs +++ b/utils/build/docker/dotnet/weblog/Controllers/FeatureFlagEvaluatorController.cs @@ -47,18 +47,25 @@ public async Task Evaluate([FromBody] EvaluateRequest request) { object value; string reason = "DEFAULT"; - var context = CreateContext(request); + var targetingKeys = request.TargetingKeys != null && request.TargetingKeys.Count > 0 + ? request.TargetingKeys + : new List { request.TargetingKey }; try { - value = request.VariationType?.ToUpper() switch + value = request.DefaultValue; + foreach (var targetingKey in targetingKeys) { - "BOOLEAN" => await _client.GetBooleanValueAsync(request.Flag, GetDefaultValueAsBool(request.DefaultValue), context), - "STRING" => await _client.GetStringValueAsync(request.Flag, GetDefaultValueAsString(request.DefaultValue), context), - "INTEGER" => await _client.GetIntegerValueAsync(request.Flag, GetDefaultValueAsInt(request.DefaultValue), context), - "NUMERIC" => await _client.GetDoubleValueAsync(request.Flag, GetDefaultValueAsDouble(request.DefaultValue), context), - _ => request.DefaultValue - }; + var context = CreateContext(request, targetingKey); + value = request.VariationType?.ToUpper() switch + { + "BOOLEAN" => await _client.GetBooleanValueAsync(request.Flag, GetDefaultValueAsBool(request.DefaultValue), context), + "STRING" => await _client.GetStringValueAsync(request.Flag, GetDefaultValueAsString(request.DefaultValue), context), + "INTEGER" => await _client.GetIntegerValueAsync(request.Flag, GetDefaultValueAsInt(request.DefaultValue), context), + "NUMERIC" => await _client.GetDoubleValueAsync(request.Flag, GetDefaultValueAsDouble(request.DefaultValue), context), + _ => request.DefaultValue + }; + } } catch (Exception) { @@ -66,7 +73,7 @@ public async Task Evaluate([FromBody] EvaluateRequest request) reason = "ERROR"; } - return Ok(new { reason, value }); + return Ok(new { reason, value, count = targetingKeys.Count }); } private static bool GetDefaultValueAsBool(object defaultValue) @@ -123,10 +130,10 @@ private static double GetDefaultValueAsDouble(object defaultValue) return Convert.ToDouble(defaultValue); } - private static EvaluationContext CreateContext(EvaluateRequest request) + private static EvaluationContext CreateContext(EvaluateRequest request, string targetingKey) { var builder = EvaluationContext.Builder(); - builder.SetTargetingKey(request.TargetingKey); + builder.SetTargetingKey(targetingKey); if (request.Attributes != null) { @@ -161,8 +168,11 @@ public class EvaluateRequest [JsonPropertyName("targetingKey")] public string TargetingKey { get; set; } + [JsonPropertyName("targetingKeys")] + public List TargetingKeys { get; set; } + [JsonPropertyName("attributes")] public Dictionary Attributes { get; set; } } } -} \ No newline at end of file +} diff --git a/utils/build/docker/golang/app/_shared/common/ffe.go b/utils/build/docker/golang/app/_shared/common/ffe.go index 929d0bd7eaa..38a6d45561e 100644 --- a/utils/build/docker/golang/app/_shared/common/ffe.go +++ b/utils/build/docker/golang/app/_shared/common/ffe.go @@ -26,6 +26,7 @@ func FFeEval() func(writer http.ResponseWriter, request *http.Request) { VariationType string `json:"variationType"` DefaultValue any `json:"defaultValue"` TargetingKey string `json:"targetingKey"` + TargetingKeys []string `json:"targetingKeys"` Attributes map[string]any `json:"attributes"` } if err := json.NewDecoder(request.Body).Decode(&body); err != nil { @@ -34,31 +35,38 @@ func FFeEval() func(writer http.ResponseWriter, request *http.Request) { } ctx := request.Context() - evalCtx := of.NewEvaluationContext(body.TargetingKey, body.Attributes) + targetingKeys := body.TargetingKeys + if len(targetingKeys) == 0 { + targetingKeys = []string{body.TargetingKey} + } var val any - switch body.VariationType { - case "BOOLEAN": - defBool, _ := body.DefaultValue.(bool) - val, _ = ofClient.BooleanValue(ctx, body.Flag, defBool, evalCtx) - case "STRING": - defStr, _ := body.DefaultValue.(string) - val, _ = ofClient.StringValue(ctx, body.Flag, defStr, evalCtx) - case "INTEGER": - defFloat, _ := body.DefaultValue.(float64) - val, _ = ofClient.IntValue(ctx, body.Flag, int64(defFloat), evalCtx) - case "NUMERIC": - defFloat, _ := body.DefaultValue.(float64) - val, _ = ofClient.FloatValue(ctx, body.Flag, defFloat, evalCtx) - default: - val = ofClient.Object(ctx, body.Flag, body.DefaultValue, evalCtx) + for _, targetingKey := range targetingKeys { + evalCtx := of.NewEvaluationContext(targetingKey, body.Attributes) + switch body.VariationType { + case "BOOLEAN": + defBool, _ := body.DefaultValue.(bool) + val, _ = ofClient.BooleanValue(ctx, body.Flag, defBool, evalCtx) + case "STRING": + defStr, _ := body.DefaultValue.(string) + val, _ = ofClient.StringValue(ctx, body.Flag, defStr, evalCtx) + case "INTEGER": + defFloat, _ := body.DefaultValue.(float64) + val, _ = ofClient.IntValue(ctx, body.Flag, int64(defFloat), evalCtx) + case "NUMERIC": + defFloat, _ := body.DefaultValue.(float64) + val, _ = ofClient.FloatValue(ctx, body.Flag, defFloat, evalCtx) + default: + val = ofClient.Object(ctx, body.Flag, body.DefaultValue, evalCtx) + } } writer.WriteHeader(http.StatusOK) response := struct { Value any `json:"value"` - }{val} + Count int `json:"count"` + }{val, len(targetingKeys)} if err := json.NewEncoder(writer).Encode(response); err != nil { http.Error(writer, "failed to encode response", http.StatusInternalServerError) diff --git a/utils/build/docker/java/spring-boot/src/main/java/com/datadoghq/system_tests/springboot/featureflag/FeatureFlagEvaluatorController.java b/utils/build/docker/java/spring-boot/src/main/java/com/datadoghq/system_tests/springboot/featureflag/FeatureFlagEvaluatorController.java index 25098eee4a4..fdbd7b11daf 100644 --- a/utils/build/docker/java/spring-boot/src/main/java/com/datadoghq/system_tests/springboot/featureflag/FeatureFlagEvaluatorController.java +++ b/utils/build/docker/java/spring-boot/src/main/java/com/datadoghq/system_tests/springboot/featureflag/FeatureFlagEvaluatorController.java @@ -64,32 +64,38 @@ public ProviderState getState() { public ResponseEntity> evaluate(@RequestBody final EvaluateRequest request) { Object value; String reason; - final EvaluationContext context = context(request); + final List targetingKeys = request.getTargetingKeys() == null || request.getTargetingKeys().isEmpty() + ? List.of(request.getTargetingKey()) + : request.getTargetingKeys(); try { - switch (request.getVariationType()) { - case "BOOLEAN": - value = client.getBooleanValue(request.getFlag(), (Boolean) request.getDefaultValue(), context); - break; - case "STRING": - value = client.getStringValue(request.getFlag(), (String) request.getDefaultValue(), context); - break; - case "INTEGER": - value = client.getIntegerValue(request.getFlag(), (Integer) request.getDefaultValue(), context); - break; - case "NUMERIC": - final Number number = (Number) request.getDefaultValue(); - if (number instanceof Double) { - value = client.getDoubleValue(request.getFlag(), number.doubleValue(), context); - } else { - value = client.getIntegerValue(request.getFlag(), number.intValue(), context); - } - break; - case "JSON": - final Value objectValue = client.getObjectValue(request.getFlag(), Value.objectToValue(request.getDefaultValue()), context); - value = context.convertValue(objectValue); - break; - default: - value = request.getDefaultValue(); + value = request.getDefaultValue(); + for (final String targetingKey : targetingKeys) { + final EvaluationContext context = context(request, targetingKey); + switch (request.getVariationType()) { + case "BOOLEAN": + value = client.getBooleanValue(request.getFlag(), (Boolean) request.getDefaultValue(), context); + break; + case "STRING": + value = client.getStringValue(request.getFlag(), (String) request.getDefaultValue(), context); + break; + case "INTEGER": + value = client.getIntegerValue(request.getFlag(), (Integer) request.getDefaultValue(), context); + break; + case "NUMERIC": + final Number number = (Number) request.getDefaultValue(); + if (number instanceof Double) { + value = client.getDoubleValue(request.getFlag(), number.doubleValue(), context); + } else { + value = client.getIntegerValue(request.getFlag(), number.intValue(), context); + } + break; + case "JSON": + final Value objectValue = client.getObjectValue(request.getFlag(), Value.objectToValue(request.getDefaultValue()), context); + value = context.convertValue(objectValue); + break; + default: + value = request.getDefaultValue(); + } } reason = "DEFAULT"; @@ -101,12 +107,13 @@ public ResponseEntity> evaluate(@RequestBody final EvaluateR final Map result = new HashMap<>(); result.put("reason", reason); result.put("value", value); + result.put("count", targetingKeys.size()); return ResponseEntity.ok(result); } - private static EvaluationContext context(final EvaluateRequest request) { + private static EvaluationContext context(final EvaluateRequest request, final String targetingKey) { final MutableContext context = new MutableContext(); - context.setTargetingKey(request.getTargetingKey()); + context.setTargetingKey(targetingKey); request.attributes.forEach((key, value) -> { if (value instanceof Boolean) { context.add(key, (Boolean) value); @@ -132,6 +139,7 @@ public static class EvaluateRequest { private String variationType; private Object defaultValue; private String targetingKey; + private List targetingKeys; private Map attributes; public Map getAttributes() { @@ -166,6 +174,14 @@ public void setTargetingKey(String targetingKey) { this.targetingKey = targetingKey; } + public List getTargetingKeys() { + return targetingKeys; + } + + public void setTargetingKeys(List targetingKeys) { + this.targetingKeys = targetingKeys; + } + public String getVariationType() { return variationType; } diff --git a/utils/build/docker/nodejs/express/app.js b/utils/build/docker/nodejs/express/app.js index e21e24ddeae..93cc8e56368 100644 --- a/utils/build/docker/nodejs/express/app.js +++ b/utils/build/docker/nodejs/express/app.js @@ -785,34 +785,38 @@ if (process.env.DD_EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED === 'true') { // Single FFE endpoint that evaluates feature flags app.post('/ffe', async (req, res) => { try { - const { flag, variationType, defaultValue, targetingKey, attributes } = req.body + const { flag, variationType, defaultValue, targetingKey, targetingKeys, attributes } = req.body if (!openFeatureClient) { return res.status(500).json({ error: 'FFE provider not initialized' }) } let value - const context = { targetingKey, ...attributes } - - switch (variationType) { - case 'BOOLEAN': - value = await openFeatureClient.getBooleanValue(flag, defaultValue, context) - break - case 'STRING': - value = await openFeatureClient.getStringValue(flag, defaultValue, context) - break - case 'INTEGER': - case 'NUMERIC': - value = await openFeatureClient.getNumberValue(flag, defaultValue, context) - break - case 'JSON': - value = await openFeatureClient.getObjectValue(flag, defaultValue, context) - break - default: - return res.status(400).json({ error: `Unknown variation type: ${variationType}` }) + const keys = Array.isArray(targetingKeys) && targetingKeys.length > 0 ? targetingKeys : [targetingKey] + + for (const key of keys) { + const context = { targetingKey: key, ...attributes } + + switch (variationType) { + case 'BOOLEAN': + value = await openFeatureClient.getBooleanValue(flag, defaultValue, context) + break + case 'STRING': + value = await openFeatureClient.getStringValue(flag, defaultValue, context) + break + case 'INTEGER': + case 'NUMERIC': + value = await openFeatureClient.getNumberValue(flag, defaultValue, context) + break + case 'JSON': + value = await openFeatureClient.getObjectValue(flag, defaultValue, context) + break + default: + return res.status(400).json({ error: `Unknown variation type: ${variationType}` }) + } } - res.status(200).json({ value }) + res.status(200).json({ value, count: keys.length }) } catch (error) { console.error('[FFE] Error:', error) res.status(500).json({ error: error.message }) diff --git a/utils/build/docker/php/common/ffe.php b/utils/build/docker/php/common/ffe.php index e4230d21691..1fae050cbec 100644 --- a/utils/build/docker/php/common/ffe.php +++ b/utils/build/docker/php/common/ffe.php @@ -171,14 +171,22 @@ function dd_ffe_details_payload($details) $targetingKey = isset($payload['targetingKey']) && $payload['targetingKey'] !== null ? (string) $payload['targetingKey'] : null; +$targetingKeys = isset($payload['targetingKeys']) && is_array($payload['targetingKeys']) && count($payload['targetingKeys']) > 0 + ? array_values(array_map('strval', $payload['targetingKeys'])) + : array($targetingKey); $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); + $details = null; + foreach ($targetingKeys as $key) { + $details = dd_ffe_evaluate($flagKey, $variationType, $defaultValue, $key, $attributes); + } if ($details !== null) { - dd_ffe_json_response(200, dd_ffe_details_payload($details)); + $response = dd_ffe_details_payload($details); + $response['count'] = count($targetingKeys); + dd_ffe_json_response(200, $response); return; } } catch (Throwable $exception) { diff --git a/utils/build/docker/python/flask/app.py b/utils/build/docker/python/flask/app.py index 58764bf78d3..6cacc62026f 100644 --- a/utils/build/docker/python/flask/app.py +++ b/utils/build/docker/python/flask/app.py @@ -2117,23 +2117,27 @@ def ffe(): variation_type = body.get("variationType") default_value = body.get("defaultValue") targeting_key = body.get("targetingKey") + targeting_keys = body.get("targetingKeys") attributes = body.get("attributes", {}) - # Build context - context = EvaluationContext(targeting_key=targeting_key, attributes=attributes) - # Evaluate based on variation type - if variation_type == "BOOLEAN": - value = openfeature_client.get_boolean_value(flag, default_value, context) - elif variation_type == "STRING": - value = openfeature_client.get_string_value(flag, default_value, context) - elif variation_type in ["INTEGER", "NUMERIC"]: - value = openfeature_client.get_integer_value(flag, default_value, context) - elif variation_type == "JSON": - value = openfeature_client.get_object_value(flag, default_value, context) - else: - return JSONResponse({"error": f"Unknown variation type: {variation_type}"}, status_code=400) + if not isinstance(targeting_keys, list) or not targeting_keys: + targeting_keys = [targeting_key] + + value = None + for key in targeting_keys: + context = EvaluationContext(targeting_key=key, attributes=attributes) + if variation_type == "BOOLEAN": + value = openfeature_client.get_boolean_value(flag, default_value, context) + elif variation_type == "STRING": + value = openfeature_client.get_string_value(flag, default_value, context) + elif variation_type in ["INTEGER", "NUMERIC"]: + value = openfeature_client.get_integer_value(flag, default_value, context) + elif variation_type == "JSON": + value = openfeature_client.get_object_value(flag, default_value, context) + else: + return JSONResponse({"error": f"Unknown variation type: {variation_type}"}, status_code=400) - return jsonify({"value": value}), 200 + return jsonify({"value": value, "count": len(targeting_keys)}), 200 @app.route("/external_request", methods=["GET", "TRACE", "POST", "PUT"]) diff --git a/utils/build/docker/ruby/rails72/app/controllers/open_feature_controller.rb b/utils/build/docker/ruby/rails72/app/controllers/open_feature_controller.rb index 8f950cbe9e4..db75aecc130 100644 --- a/utils/build/docker/ruby/rails72/app/controllers/open_feature_controller.rb +++ b/utils/build/docker/ruby/rails72/app/controllers/open_feature_controller.rb @@ -24,28 +24,39 @@ def evaluate variation_type: payload['variationType'], default_value: payload['defaultValue'], targeting_key: payload['targetingKey'], + targeting_keys: payload['targetingKeys'], attributes: payload['attributes'] } begin - context = OpenFeature::SDK::EvaluationContext.new( - targeting_key: args[:targeting_key], **args[:attributes] - ) - options = { - flag_key: args[:flag], default_value: args[:default_value], evaluation_context: context - } - - value = - case args[:variation_type] - when 'BOOLEAN'then client.fetch_boolean_value(**options) - when 'STRING' then client.fetch_string_value(**options) - when 'INTEGER' then client.fetch_integer_value(**options) - when 'NUMERIC' then client.fetch_float_value(**options) - when 'JSON' then client.fetch_object_value(**options) - else 'FATAL_UNEXPECTED_VARIATION_TYPE' + targeting_keys = + if args[:targeting_keys].is_a?(Array) && !args[:targeting_keys].empty? + args[:targeting_keys] + else + [args[:targeting_key]] end + value = nil - render json: {value: value, reason: 'DEFAULT'} + targeting_keys.each do |targeting_key| + context = OpenFeature::SDK::EvaluationContext.new( + targeting_key: targeting_key, **args[:attributes] + ) + options = { + flag_key: args[:flag], default_value: args[:default_value], evaluation_context: context + } + + value = + case args[:variation_type] + when 'BOOLEAN'then client.fetch_boolean_value(**options) + when 'STRING' then client.fetch_string_value(**options) + when 'INTEGER' then client.fetch_integer_value(**options) + when 'NUMERIC' then client.fetch_float_value(**options) + when 'JSON' then client.fetch_object_value(**options) + else 'FATAL_UNEXPECTED_VARIATION_TYPE' + end + end + + render json: {value: value, reason: 'DEFAULT', count: targeting_keys.length} rescue render json: {value: args[:default_value], reason: 'ERROR'} end From e082ba49ad3548651aa265ea9a24bdbd209a5e43 Mon Sep 17 00:00:00 2001 From: Leo Romanovsky Date: Tue, 16 Jun 2026 22:59:58 -0400 Subject: [PATCH 10/11] Copy Node Express app into final weblog images --- utils/build/docker/nodejs/express4.Dockerfile | 1 + utils/build/docker/nodejs/express5.Dockerfile | 1 + utils/build/docker/nodejs/uds-express4.Dockerfile | 1 + 3 files changed, 3 insertions(+) diff --git a/utils/build/docker/nodejs/express4.Dockerfile b/utils/build/docker/nodejs/express4.Dockerfile index 3082b8e2207..dc27bcb0854 100644 --- a/utils/build/docker/nodejs/express4.Dockerfile +++ b/utils/build/docker/nodejs/express4.Dockerfile @@ -11,6 +11,7 @@ ENV PGPORT=5433 ENV DD_DATA_STREAMS_ENABLED=true # docker startup +COPY utils/build/docker/nodejs/express/app.js app.js COPY utils/build/docker/nodejs/app.sh app.sh RUN chmod +x app.sh RUN printf 'node app.js' >> app.sh diff --git a/utils/build/docker/nodejs/express5.Dockerfile b/utils/build/docker/nodejs/express5.Dockerfile index 833b13b1bb4..c9be75159e3 100644 --- a/utils/build/docker/nodejs/express5.Dockerfile +++ b/utils/build/docker/nodejs/express5.Dockerfile @@ -11,6 +11,7 @@ ENV PGPORT=5433 ENV DD_DATA_STREAMS_ENABLED=true # docker startup +COPY utils/build/docker/nodejs/express/app.js app.js COPY utils/build/docker/nodejs/app.sh app.sh RUN chmod +x app.sh RUN printf 'node app.js' >> app.sh diff --git a/utils/build/docker/nodejs/uds-express4.Dockerfile b/utils/build/docker/nodejs/uds-express4.Dockerfile index 62a425cb097..b6933bdb22e 100644 --- a/utils/build/docker/nodejs/uds-express4.Dockerfile +++ b/utils/build/docker/nodejs/uds-express4.Dockerfile @@ -14,6 +14,7 @@ ENV UDS_WEBLOG=1 ENV DD_DATA_STREAMS_ENABLED=true # docker startup +COPY utils/build/docker/nodejs/express/app.js app.js COPY utils/build/docker/nodejs/app.sh app.sh RUN printf 'node app.js' >> app.sh COPY utils/build/docker/set-uds-transport.sh set-uds-transport.sh From 8c569a9d49a5c1d6b03688df3f4d8ad26528cd60 Mon Sep 17 00:00:00 2001 From: Leo Romanovsky Date: Tue, 16 Jun 2026 23:00:51 -0400 Subject: [PATCH 11/11] Revert "Copy Node Express app into final weblog images" This reverts commit e082ba49ad3548651aa265ea9a24bdbd209a5e43. --- utils/build/docker/nodejs/express4.Dockerfile | 1 - utils/build/docker/nodejs/express5.Dockerfile | 1 - utils/build/docker/nodejs/uds-express4.Dockerfile | 1 - 3 files changed, 3 deletions(-) diff --git a/utils/build/docker/nodejs/express4.Dockerfile b/utils/build/docker/nodejs/express4.Dockerfile index dc27bcb0854..3082b8e2207 100644 --- a/utils/build/docker/nodejs/express4.Dockerfile +++ b/utils/build/docker/nodejs/express4.Dockerfile @@ -11,7 +11,6 @@ ENV PGPORT=5433 ENV DD_DATA_STREAMS_ENABLED=true # docker startup -COPY utils/build/docker/nodejs/express/app.js app.js COPY utils/build/docker/nodejs/app.sh app.sh RUN chmod +x app.sh RUN printf 'node app.js' >> app.sh diff --git a/utils/build/docker/nodejs/express5.Dockerfile b/utils/build/docker/nodejs/express5.Dockerfile index c9be75159e3..833b13b1bb4 100644 --- a/utils/build/docker/nodejs/express5.Dockerfile +++ b/utils/build/docker/nodejs/express5.Dockerfile @@ -11,7 +11,6 @@ ENV PGPORT=5433 ENV DD_DATA_STREAMS_ENABLED=true # docker startup -COPY utils/build/docker/nodejs/express/app.js app.js COPY utils/build/docker/nodejs/app.sh app.sh RUN chmod +x app.sh RUN printf 'node app.js' >> app.sh diff --git a/utils/build/docker/nodejs/uds-express4.Dockerfile b/utils/build/docker/nodejs/uds-express4.Dockerfile index b6933bdb22e..62a425cb097 100644 --- a/utils/build/docker/nodejs/uds-express4.Dockerfile +++ b/utils/build/docker/nodejs/uds-express4.Dockerfile @@ -14,7 +14,6 @@ ENV UDS_WEBLOG=1 ENV DD_DATA_STREAMS_ENABLED=true # docker startup -COPY utils/build/docker/nodejs/express/app.js app.js COPY utils/build/docker/nodejs/app.sh app.sh RUN printf 'node app.js' >> app.sh COPY utils/build/docker/set-uds-transport.sh set-uds-transport.sh