From 41d2a721fd923cb8aa6211471c4ffd486613c230 Mon Sep 17 00:00:00 2001 From: Leo Romanovsky Date: Thu, 8 Oct 2026 04:29:08 +0000 Subject: [PATCH 1/4] Enable shared Agentless EVP routing for PHP feature flags --- components-rs/datadog.h | 5 ++ components-rs/ffe/lifecycle.rs | 10 +++ components-rs/ffe/settings.rs | 73 +++++++++++++++++ ext/sidecar.c | 26 +++++- ...agentless_evp_direct_openfeature_wire.phpt | 49 ++++++++++++ tests/ext/ffe/agentless_evp_direct_wire.phpt | 48 +++++++++++ tests/ext/ffe/agentless_evp_v2_wire.phpt | 45 +++++++++++ tests/ext/ffe/agentless_evp_v4_wire.phpt | 45 +++++++++++ tests/ext/ffe/stubs/evp_tls_fixture.inc | 28 +++++++ tests/ext/ffe/stubs/flag_evaluation_wire.inc | 79 ++++++++++++++++++- 10 files changed, 401 insertions(+), 7 deletions(-) create mode 100644 tests/ext/ffe/agentless_evp_direct_openfeature_wire.phpt create mode 100644 tests/ext/ffe/agentless_evp_direct_wire.phpt create mode 100644 tests/ext/ffe/agentless_evp_v2_wire.phpt create mode 100644 tests/ext/ffe/agentless_evp_v4_wire.phpt create mode 100644 tests/ext/ffe/stubs/evp_tls_fixture.inc diff --git a/components-rs/datadog.h b/components-rs/datadog.h index 8416523ae99..0dafa383a9f 100644 --- a/components-rs/datadog.h +++ b/components-rs/datadog.h @@ -162,6 +162,11 @@ struct ddog_FfeRuntimeConfig ddog_ffe_configure(const struct ddog_FfeSettingsInp struct ddog_FfeRuntimeConfig ddog_ffe_runtime_config(void); +/** + * Caller owns the returned endpoint and must release it with endpoint_drop. + */ +struct ddog_Endpoint *ddog_ffe_direct_evp_endpoint(void); + /** * First-use compatibility path for PHP providers without an initialize API. * Concurrent first callers share one deadline; later calls never renew it. diff --git a/components-rs/ffe/lifecycle.rs b/components-rs/ffe/lifecycle.rs index 5383de97506..a4f1dc018bf 100644 --- a/components-rs/ffe/lifecycle.rs +++ b/components-rs/ffe/lifecycle.rs @@ -135,6 +135,16 @@ pub extern "C" fn ddog_ffe_runtime_config() -> FfeRuntimeConfig { lifecycle().config } +/// Caller owns the returned endpoint and must release it with endpoint_drop. +#[no_mangle] +pub extern "C" fn ddog_ffe_direct_evp_endpoint() -> Option> { + lifecycle() + .settings + .as_ref() + .and_then(FeatureFlagsSettings::direct_evp_endpoint) + .map(Box::new) +} + /// First-use compatibility path for PHP providers without an initialize API. /// Concurrent first callers share one deadline; later calls never renew it. #[no_mangle] diff --git a/components-rs/ffe/settings.rs b/components-rs/ffe/settings.rs index 07a300acbb9..47c0e296032 100644 --- a/components-rs/ffe/settings.rs +++ b/components-rs/ffe/settings.rs @@ -195,6 +195,34 @@ impl FeatureFlagsSettings { &self.api_key, ) } + + /// Event delivery has a separate canonical intake. A custom configuration + /// URL must never become the destination for the Datadog API key. + pub fn direct_evp_endpoint(&self) -> Option { + if !self.resolution.enabled + || self.resolution.source != ConfigurationSource::Agentless + || self.api_key.is_empty() + { + return None; + } + let site = normalize_site(&self.site).ok()?; + let endpoint = libdd_common::Endpoint { + url: format!("https://event-platform-intake.{site}/") + .parse() + .ok()?, + api_key: Some(self.api_key.clone().into()), + ..Default::default() + }; + // Reuse the shared transport's full DNS/header/HTTPS validation. Invalid + // direct credentials leave local discovery available without them. + let config = datadog_sidecar::service::EvpTransportConfig::prefer_local_then_direct( + libdd_common::Endpoint::default(), + Some(endpoint), + "event-platform-intake", + ); + config.validate().ok()?; + config.direct_endpoint + } } fn validated_numeric( @@ -638,4 +666,49 @@ mod tests { assert!(!debug.contains("password")); assert!(debug.contains("[REDACTED]")); } + + #[test] + fn direct_evp_uses_only_valid_agentless_credentials_and_canonical_site() { + let settings = |source, enabled, site, api_key| { + FeatureFlagsSettings::resolve(SettingsInput { + source: SourceInput { + enabled, + enabled_set: true, + source, + source_set: true, + legacy_enabled: false, + legacy_enabled_set: false, + }, + agentless_base_url: "https://custom.example/config?tenant=one", + poll_interval_seconds: 30, + request_timeout_seconds: 5, + initialization_timeout_ms: 1000, + site, + api_key, + environment: "test", + }) + }; + let configured = settings("agentless", true, "US3.Datadoghq.com", "test-key"); + assert!(configured.agentless_endpoint().unwrap().api_key().is_none()); + let endpoint = configured.direct_evp_endpoint().unwrap(); + assert_eq!( + endpoint.url, + "https://event-platform-intake.us3.datadoghq.com/" + ); + assert_eq!(endpoint.api_key.as_deref(), Some("test-key")); + for (source, enabled, site, key) in [ + ("remote_config", true, "datadoghq.com", "test-key"), + ("agentless", false, "datadoghq.com", "test-key"), + ("agentless", true, "datadoghq.com", ""), + ("agentless", true, "datadoghq.com", "bad\r\nkey"), + ("agentless", true, "datadoghq.com:443", "test-key"), + ("agentless", true, "datadoghq.com/path", "test-key"), + ("agentless", true, "datadoghq.com@other.example", "test-key"), + ("agentless", true, "bad..example", "test-key"), + ] { + assert!(settings(source, enabled, site, key) + .direct_evp_endpoint() + .is_none()); + } + } } diff --git a/ext/sidecar.c b/ext/sidecar.c index 9557e8586cd..1c4656c2638 100644 --- a/ext/sidecar.c +++ b/ext/sidecar.c @@ -186,9 +186,29 @@ static void dd_sidecar_post_connect(ddog_SidecarTransport **transport, bool is_f .origin = DDOG_CHARSLICE_C("dd-trace-php"), .version = DDOG_CHARSLICE_C(PHP_DDTRACE_VERSION), }; - datadog_ffi_try("Failed configuring PHP FFE EVP transport", - ddog_sidecar_session_set_evp_transport(transport, DDOG_EVP_TRANSPORT_MODE_AGENT_ONLY, - datadog_endpoint, NULL, DDOG_CHARSLICE_C("event-platform-intake"), &ffe_producer)); + ddog_FfeRuntimeConfig ffe_config = ddog_ffe_runtime_config(); + ddog_EvpTransportMode ffe_mode = ffe_config.enabled + && ffe_config.source == DDOG_FFE_CONFIGURATION_SOURCE_AGENTLESS + ? DDOG_EVP_TRANSPORT_MODE_PREFER_LOCAL_THEN_DIRECT + : DDOG_EVP_TRANSPORT_MODE_AGENT_ONLY; + ddog_Endpoint *ffe_direct = ddog_ffe_direct_evp_endpoint(); + // FFE's local candidate is the Agent URL even when APM trace delivery is + // agentless. Never give the local route an API-key-bearing trace endpoint. + char *ffe_agent_url = datadog_agent_url(); + ddog_Endpoint *ffe_agent = datadog_parse_agent_url((ddog_CharSlice) { + .ptr = ffe_agent_url, .len = strlen(ffe_agent_url)}); + free(ffe_agent_url); + if (ffe_agent) { + dd_set_endpoint_test_token(ffe_agent); + ddog_endpoint_set_timeout(ffe_agent, get_global_DD_TRACE_AGENT_TIMEOUT()); + datadog_ffi_try("Failed configuring PHP FFE EVP transport", + ddog_sidecar_session_set_evp_transport(transport, ffe_mode, + ffe_agent, ffe_direct, DDOG_CHARSLICE_C("event-platform-intake"), &ffe_producer)); + ddog_endpoint_drop(ffe_agent); + } + if (ffe_direct) { + ddog_endpoint_drop(ffe_direct); + } #endif if (get_global_DD_INSTRUMENTATION_TELEMETRY_ENABLED()) { diff --git a/tests/ext/ffe/agentless_evp_direct_openfeature_wire.phpt b/tests/ext/ffe/agentless_evp_direct_openfeature_wire.phpt new file mode 100644 index 00000000000..8387cb89640 --- /dev/null +++ b/tests/ext/ffe/agentless_evp_direct_openfeature_wire.phpt @@ -0,0 +1,49 @@ +--TEST-- +Agentless canonical direct HTTPS: FFE OpenFeature evaluation counts and deduplicated exposures reach canonical direct intake with privacy +--SKIPIF-- + +--ENV-- +_DD_DEBUG_SIDECAR_IPC_MODE=instance_per_process +DD_SITE=example.invalid +NO_PROXY=127.0.0.1,localhost +DD_FEATURE_FLAGS_ENABLED=1 +DD_FEATURE_FLAGS_CONFIGURATION_SOURCE=agentless +DD_EXPERIMENTAL_FLAGGING_PROVIDER_INITIALIZATION_TIMEOUT_MS=200 +DD_INSTRUMENTATION_TELEMETRY_ENABLED=0 +DD_REMOTE_CONFIG_ENABLED=0 +DD_METRICS_OTEL_ENABLED=0 +DD_TRACE_GENERATE_ROOT_SPAN=0 +DD_TRACE_CLI_ENABLED=1 +DD_EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED=true +DD_SERVICE=php-ffe-wire +DD_ENV=test +DD_VERSION=wire-version +DD_API_KEY=local-test-key-must-not-reach-agent +--FILE-- + +--EXPECT-- +service=php-ffe-wire +identity=bool(true) +exposures=int(2) +consented_count=int(2) +consented_subject=string(7) "subject" +consented_context=array(1) { + ["plan"]=> + string(3) "pro" +} +protected_count=int(1) +protected_subject=string(71) "sha256_a9491f4c1bf7b0cffbadcba2db8f028e4b3f2867cb59e1f3a0bc1968f3c51242" +protected_context_absent=bool(true) diff --git a/tests/ext/ffe/agentless_evp_direct_wire.phpt b/tests/ext/ffe/agentless_evp_direct_wire.phpt new file mode 100644 index 00000000000..9811e680697 --- /dev/null +++ b/tests/ext/ffe/agentless_evp_direct_wire.phpt @@ -0,0 +1,48 @@ +--TEST-- +Agentless canonical direct HTTPS: FFE native evaluation counts and deduplicated exposures reach canonical direct intake with privacy +--SKIPIF-- + +--ENV-- +_DD_DEBUG_SIDECAR_IPC_MODE=instance_per_process +DD_SITE=example.invalid +NO_PROXY=127.0.0.1,localhost +DD_FEATURE_FLAGS_ENABLED=1 +DD_FEATURE_FLAGS_CONFIGURATION_SOURCE=agentless +DD_EXPERIMENTAL_FLAGGING_PROVIDER_INITIALIZATION_TIMEOUT_MS=200 +DD_INSTRUMENTATION_TELEMETRY_ENABLED=0 +DD_REMOTE_CONFIG_ENABLED=0 +DD_METRICS_OTEL_ENABLED=0 +DD_TRACE_GENERATE_ROOT_SPAN=0 +DD_TRACE_CLI_ENABLED=1 +DD_EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED=true +DD_SERVICE=php-ffe-wire +DD_ENV=test +DD_VERSION=wire-version +DD_API_KEY=local-test-key-must-not-reach-agent +--FILE-- + +--EXPECT-- +service=php-ffe-wire +identity=bool(true) +exposures=int(2) +consented_count=int(2) +consented_subject=string(7) "subject" +consented_context=array(1) { + ["plan"]=> + string(3) "pro" +} +protected_count=int(1) +protected_subject=string(71) "sha256_a9491f4c1bf7b0cffbadcba2db8f028e4b3f2867cb59e1f3a0bc1968f3c51242" +protected_context_absent=bool(true) diff --git a/tests/ext/ffe/agentless_evp_v2_wire.phpt b/tests/ext/ffe/agentless_evp_v2_wire.phpt new file mode 100644 index 00000000000..827ce278178 --- /dev/null +++ b/tests/ext/ffe/agentless_evp_v2_wire.phpt @@ -0,0 +1,45 @@ +--TEST-- +Agentless v2 prefixed relay: FFE OpenFeature evaluation counts and deduplicated exposures reach Agent EVP with privacy +--SKIPIF-- + +--ENV-- +DD_SITE=example.invalid +NO_PROXY=127.0.0.1,localhost +DD_FEATURE_FLAGS_ENABLED=1 +DD_FEATURE_FLAGS_CONFIGURATION_SOURCE=agentless +DD_EXPERIMENTAL_FLAGGING_PROVIDER_INITIALIZATION_TIMEOUT_MS=200 +DD_INSTRUMENTATION_TELEMETRY_ENABLED=0 +DD_REMOTE_CONFIG_ENABLED=0 +DD_METRICS_OTEL_ENABLED=0 +DD_TRACE_GENERATE_ROOT_SPAN=0 +DD_TRACE_CLI_ENABLED=1 +DD_EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED=true +DD_SERVICE=php-ffe-wire +DD_ENV=test +DD_VERSION=wire-version +DD_API_KEY=local-test-key-must-not-reach-agent +--FILE-- + +--EXPECT-- +service=php-ffe-wire +identity=bool(true) +exposures=int(2) +consented_count=int(2) +consented_subject=string(7) "subject" +consented_context=array(1) { + ["plan"]=> + string(3) "pro" +} +protected_count=int(1) +protected_subject=string(71) "sha256_a9491f4c1bf7b0cffbadcba2db8f028e4b3f2867cb59e1f3a0bc1968f3c51242" +protected_context_absent=bool(true) diff --git a/tests/ext/ffe/agentless_evp_v4_wire.phpt b/tests/ext/ffe/agentless_evp_v4_wire.phpt new file mode 100644 index 00000000000..58cf351afb6 --- /dev/null +++ b/tests/ext/ffe/agentless_evp_v4_wire.phpt @@ -0,0 +1,45 @@ +--TEST-- +Agentless v4 prefixed relay: FFE native evaluation counts and deduplicated exposures reach Agent EVP with privacy +--SKIPIF-- + +--ENV-- +DD_TRACE_AGENTLESS=1 +DD_SITE=example.invalid +NO_PROXY=127.0.0.1,localhost +DD_FEATURE_FLAGS_ENABLED=1 +DD_FEATURE_FLAGS_CONFIGURATION_SOURCE=agentless +DD_EXPERIMENTAL_FLAGGING_PROVIDER_INITIALIZATION_TIMEOUT_MS=200 +DD_INSTRUMENTATION_TELEMETRY_ENABLED=0 +DD_REMOTE_CONFIG_ENABLED=0 +DD_METRICS_OTEL_ENABLED=0 +DD_TRACE_GENERATE_ROOT_SPAN=0 +DD_TRACE_CLI_ENABLED=1 +DD_EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED=true +DD_SERVICE=php-ffe-wire +DD_ENV=test +DD_VERSION=wire-version +DD_API_KEY=local-test-key-must-not-reach-agent +--FILE-- + +--EXPECT-- +service=php-ffe-wire +identity=bool(true) +exposures=int(2) +consented_count=int(2) +consented_subject=string(7) "subject" +consented_context=array(1) { + ["plan"]=> + string(3) "pro" +} +protected_count=int(1) +protected_subject=string(71) "sha256_a9491f4c1bf7b0cffbadcba2db8f028e4b3f2867cb59e1f3a0bc1968f3c51242" +protected_context_absent=bool(true) diff --git a/tests/ext/ffe/stubs/evp_tls_fixture.inc b/tests/ext/ffe/stubs/evp_tls_fixture.inc new file mode 100644 index 00000000000..2302068db85 --- /dev/null +++ b/tests/ext/ffe/stubs/evp_tls_fixture.inc @@ -0,0 +1,28 @@ + $config, 'private_key_bits' => 2048, 'digest_alg' => 'sha256'); + $caKey = openssl_pkey_new($options); + $caCsr = openssl_csr_new(array('commonName' => 'FFE fixture CA'), $caKey, $options); + $ca = openssl_csr_sign($caCsr, null, $caKey, 1, $options + array('x509_extensions' => 'ca_ext'), 1); + $key = openssl_pkey_new($options); + $csr = openssl_csr_new(array('commonName' => 'event-platform-intake.example.invalid'), $key, $options); + $cert = openssl_csr_sign($csr, $ca, $caKey, 1, $options + array('x509_extensions' => 'leaf_ext'), 2); + if (!$ca || !$cert) throw new RuntimeException('cannot generate fixture certificates'); + openssl_x509_export($ca, $caPem); + openssl_x509_export($cert, $certPem); + openssl_pkey_export($key, $keyPem, null, $options); + file_put_contents($dir . '/ca.pem', $caPem); + file_put_contents($dir . '/server.pem', $certPem . $keyPem); + return $dir; +} diff --git a/tests/ext/ffe/stubs/flag_evaluation_wire.inc b/tests/ext/ffe/stubs/flag_evaluation_wire.inc index 20f8d40ec59..6114993fcd6 100644 --- a/tests/ext/ffe/stubs/flag_evaluation_wire.inc +++ b/tests/ext/ffe/stubs/flag_evaluation_wire.inc @@ -1,10 +1,38 @@ $direct ? array() : ($evpVersion === 'v4' ? array('/evp_proxy/v2', '/evp_proxy/v4') : array('/evp_proxy/v2')), + 'evp_proxy_allowed_headers' => array('DD-EVP-ORIGIN', 'DD-EVP-ORIGIN-VERSION'), + )); + } + $responseStatus = $isEvp ? '202 Accepted' : '200 OK'; + fwrite($connection, "HTTP/1.1 $responseStatus\r\nContent-Type: application/json\r\nContent-Length: " + . strlen($response) . "\r\nConnection: close\r\n\r\n" . $response); fclose($connection); } while (microtime(true) < $deadline); $stdout .= stream_get_contents($pipes[1]); @@ -78,7 +139,13 @@ try { $identity = true; foreach ($captures as $capture) { list($request, $headers, $payload) = $capture; - $identity = $identity && !isset($headers['dd-api-key']) + $routeHeaders = $direct + ? isset($headers['dd-api-key']) && $headers['dd-api-key'] === 'local-test-key-must-not-reach-agent' + && !isset($headers['x-datadog-evp-subdomain']) + && $headers['host'] === 'event-platform-intake.example.invalid' + : !isset($headers['dd-api-key']) && isset($headers['x-datadog-evp-subdomain']) + && $headers['x-datadog-evp-subdomain'] === 'event-platform-intake'; + $identity = $identity && $routeHeaders && isset($headers['dd-evp-origin']) && $headers['dd-evp-origin'] === 'dd-trace-php' && isset($headers['dd-evp-origin-version']) && $headers['dd-evp-origin-version'] === phpversion('ddtrace'); if (!$identity) { @@ -118,5 +185,9 @@ try { fclose($pipes[2]); proc_close($process); fclose($listener); + if ($tlsDir !== null) { + foreach (glob($tlsDir . '/*') as $file) unlink($file); + rmdir($tlsDir); + } } ?> From 265d8438c54f5d29a6035f42d6b64ce68b140908 Mon Sep 17 00:00:00 2001 From: Leo Romanovsky Date: Thu, 8 Oct 2026 07:09:01 +0000 Subject: [PATCH 2/4] Run enabled PHP agentless contracts against packaged CI artifacts --- .gitlab/generate-package.php | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/.gitlab/generate-package.php b/.gitlab/generate-package.php index 21e121a8739..93743a0499e 100644 --- a/.gitlab/generate-package.php +++ b/.gitlab/generate-package.php @@ -1418,6 +1418,7 @@ function package_extension_jobs(array $platform, string $kind = "all"): array KUBERNETES_MEMORY_LIMIT: 4Gi RUST_BACKTRACE: 1 BUILD_SH_ARGS: php + SYSTEM_TESTS_REF: main USE_IMAGE_MIRROR: "1" PIP_CACHE_DIR: $CI_PROJECT_DIR/.cache/pip APT_CACHE: $CI_PROJECT_DIR/.cache/apt @@ -1452,6 +1453,7 @@ function package_extension_jobs(array $platform, string $kind = "all"): array - /tmp/vault kv get --format=json "kv/k8s/gitlab-runner/dd-trace-php/datadoghq-api-key" 2>/dev/null | python3 -c "import sys,json;print(json.load(sys.stdin)['data']['data']['key'])" > /tmp/.dd-api-key 2>/dev/null || true - git clone https://github.com/DataDog/system-tests.git + - git -C system-tests checkout "$SYSTEM_TESTS_REF" - mv packages/{datadog-setup.php,dd-library-php-*x86_64-linux-gnu.tar.gz} system-tests/binaries - cd system-tests - ./build.sh $BUILD_SH_ARGS @@ -1474,6 +1476,27 @@ function package_extension_jobs(array $platform, string $kind = "all"): array script: - ./run.sh +"System Tests: [php-fpm-8.2, feature-flags-agentless]": + extends: .system_tests + variables: + BUILD_SH_ARGS: -w php-fpm-8.2 php + # Remove this temporary pin after DataDog/system-tests#7988 merges. + SYSTEM_TESTS_REF: da9642da998a964f6d08fedd6fe116d5034bc15d + script: + - ./run.sh FEATURE_FLAGGING_AND_EXPERIMENTATION_AGENTLESS tests/ffe/test_agentless_configuration.py::Test_FFE_Agentless_Configuration + - ./run.sh FEATURE_FLAGGING_AND_EXPERIMENTATION_AGENTLESS_DIRECT tests/ffe/test_exposure_egress.py::Test_FFE_Exposure_Egress_Agentless_Direct tests/ffe/test_exposure_egress.py::Test_FFE_Exposure_Egress_Agentless_Direct_Shutdown tests/ffe/test_flag_eval_evp.py::Test_FFE_EVP_Flagevaluation_Egress_Agentless_Direct + - ./run.sh FEATURE_FLAGGING_AND_EXPERIMENTATION_AGENTLESS_SERVERLESS tests/ffe/test_exposure_egress.py::Test_FFE_Exposure_Egress_Agentless_Sidecar tests/ffe/test_flag_eval_evp.py::Test_FFE_EVP_Flagevaluation_Egress_Agentless_Sidecar + - | + python3 - <<'PY' + from pathlib import Path + import xml.etree.ElementTree as ET + for suffix, expected in [("", 1), ("_direct", 3), ("_serverless", 2)]: + report = Path(f"logs_feature_flagging_and_experimentation_agentless{suffix}/reportJunit.xml") + cases = ET.parse(report).findall(".//testcase") + assert len(cases) == expected, (str(report), len(cases), expected) + assert all(not any(case.find(tag) is not None for tag in ("failure", "error", "skipped")) for case in cases), str(report) + PY + "System Tests": extends: .system_tests parallel: From d949ef5892ec98cef68696564c28ddb5b0caa601 Mon Sep 17 00:00:00 2001 From: Leo Romanovsky Date: Thu, 8 Oct 2026 07:57:57 +0000 Subject: [PATCH 3/4] Use system-tests revision with portable PHP process evidence --- .gitlab/generate-package.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab/generate-package.php b/.gitlab/generate-package.php index 93743a0499e..ee0d777c3cd 100644 --- a/.gitlab/generate-package.php +++ b/.gitlab/generate-package.php @@ -1481,7 +1481,7 @@ function package_extension_jobs(array $platform, string $kind = "all"): array variables: BUILD_SH_ARGS: -w php-fpm-8.2 php # Remove this temporary pin after DataDog/system-tests#7988 merges. - SYSTEM_TESTS_REF: da9642da998a964f6d08fedd6fe116d5034bc15d + SYSTEM_TESTS_REF: b1394b5961ef408f0a9e067c88ae144cb7debb4d script: - ./run.sh FEATURE_FLAGGING_AND_EXPERIMENTATION_AGENTLESS tests/ffe/test_agentless_configuration.py::Test_FFE_Agentless_Configuration - ./run.sh FEATURE_FLAGGING_AND_EXPERIMENTATION_AGENTLESS_DIRECT tests/ffe/test_exposure_egress.py::Test_FFE_Exposure_Egress_Agentless_Direct tests/ffe/test_exposure_egress.py::Test_FFE_Exposure_Egress_Agentless_Direct_Shutdown tests/ffe/test_flag_eval_evp.py::Test_FFE_EVP_Flagevaluation_Egress_Agentless_Direct From 39f8e1736a65b903938ac9a85d4e8ac131fc4d9d Mon Sep 17 00:00:00 2001 From: Leo Romanovsky Date: Thu, 8 Oct 2026 09:40:35 +0000 Subject: [PATCH 4/4] test(ffe): keep wire producer alive until both writers are captured --- tests/ext/ffe/stubs/flag_evaluation_wire.inc | 24 ++++++++++++++++--- .../ffe/stubs/flag_evaluation_wire_app.php | 10 +++++--- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/tests/ext/ffe/stubs/flag_evaluation_wire.inc b/tests/ext/ffe/stubs/flag_evaluation_wire.inc index cf50a7c5331..4c5d7de7c03 100644 --- a/tests/ext/ffe/stubs/flag_evaluation_wire.inc +++ b/tests/ext/ffe/stubs/flag_evaluation_wire.inc @@ -38,7 +38,7 @@ $process = ffeTestProcess(__DIR__ . '/flag_evaluation_wire_app.php', $pipes); if (!is_resource($process)) { throw new RuntimeException('failed to launch evaluation process'); } -fclose($pipes[0]); +$acknowledged = false; $captures = array(); $requests = array(); $stdout = $stderr = ''; @@ -46,10 +46,12 @@ $exitedAt = null; $exitCode = null; $lastCaptureAt = 0; try { - $deadline = microtime(true) + (getenv('USE_ZEND_ALLOC') === '0' ? 30 : 10); + $startedAt = microtime(true); + $readyAt = null; do { $stdout .= ffeTestProcessRead($pipes[1]); $stderr .= ffeTestProcessRead($pipes[2]); + if ($readyAt === null && strpos($stdout, "fixture_wire_ready\n") !== false) $readyAt = microtime(true); if ($exitedAt === null) { $status = proc_get_status($process); if (!$status['running']) { @@ -121,7 +123,22 @@ try { fwrite($connection, "HTTP/1.1 $responseStatus\r\nContent-Type: application/json\r\nContent-Length: " . strlen($response) . "\r\nConnection: close\r\n\r\n" . $response); fclose($connection); - } while (microtime(true) < $deadline); + if (!$acknowledged) { + $counted = $exposed = 0; + foreach ($captures as $captured) { + if (isset($captured[2]['flagEvaluations'])) { + foreach ($captured[2]['flagEvaluations'] as $row) $counted += $row['evaluation_count']; + } + if (isset($captured[2]['exposures'])) $exposed += count($captured[2]['exposures']); + } + if ($counted >= 3 && $exposed >= 2) { + fwrite($pipes[0], "captures_received\n"); + fclose($pipes[0]); + $acknowledged = true; + } + } + } while (microtime(true) - ($readyAt === null ? $startedAt : $readyAt) + < ($readyAt === null ? 60 : (getenv('USE_ZEND_ALLOC') === '0' ? 30 : 10))); $stdout .= ffeTestProcessRead($pipes[1]); $stderr .= ffeTestProcessRead($pipes[2]); if (!$captures) { @@ -177,6 +194,7 @@ try { echo 'protected_context_absent='; var_dump(!isset($rows['protected']['context']['evaluation'])); } finally { if ($exitedAt === null) proc_terminate($process); + if (is_resource($pipes[0])) fclose($pipes[0]); fclose($pipes[1]); fclose($pipes[2]); proc_close($process); diff --git a/tests/ext/ffe/stubs/flag_evaluation_wire_app.php b/tests/ext/ffe/stubs/flag_evaluation_wire_app.php index 3627ceb4c13..8247fef66d9 100644 --- a/tests/ext/ffe/stubs/flag_evaluation_wire_app.php +++ b/tests/ext/ffe/stubs/flag_evaluation_wire_app.php @@ -1,6 +1,7 @@ returned), "\n"; }); @@ -44,6 +45,9 @@ } echo 'exposure_flush=', json_encode(\DDTrace\Testing\flush_ffe_exposures()), "\n"; echo "evaluated\n"; -// Give the ordinary sidecar coalescer time to send while the parent captures. -// Shutdown delivery has its own lifecycle tests; this tests the live writer. -usleep(1000000); +// Keep the live producer present until both writers reach the local capture. +// Fixed sleeps race instrumented sender startup; teardown has separate tests. +stream_set_timeout(STDIN, getenv('USE_ZEND_ALLOC') === '0' ? 30 : 10); +if (fgets(STDIN) !== "captures_received\n") { + throw new RuntimeException("fixture did not acknowledge both EVP writers"); +}