Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
4c36f02
test(ffe): add agentless serverless exposure contract
leoromanovsky Aug 7, 2026
f387372
test(ffe): separate serverless contract from SDK enablement
leoromanovsky Aug 7, 2026
972b475
fix(ffe): use integer agentless poll interval
leoromanovsky Aug 8, 2026
ab7e4e5
test(ffe): use Datadog exposure route names
leoromanovsky Aug 8, 2026
009373c
test(ffe): cover direct agentless exposure egress
leoromanovsky Aug 8, 2026
6a43bea
test(ffe): account for capture key scrubbing
leoromanovsky Aug 8, 2026
4c2aca8
test: register direct exposure scenario group exception
leoromanovsky Aug 8, 2026
be461a4
ci: run direct agentless exposure scenario
leoromanovsky Aug 8, 2026
690a85b
test(ffe): identify Agent exposure tests
leoromanovsky Aug 9, 2026
fd59899
test: share exposure egress contract
leoromanovsky Aug 9, 2026
e230b5d
test: accept scrubbed sidecar API key
leoromanovsky Aug 9, 2026
0ad24cf
Merge origin/main into agent/nodejs-agentless-serverless-exposures
leoromanovsky Aug 13, 2026
4722018
test(ffe): address exposure review feedback
leoromanovsky Aug 15, 2026
d11223f
Merge remote-tracking branch 'origin/main' into agent/nodejs-agentles…
leoromanovsky Aug 15, 2026
a5674ac
test(ffe): harden sidecar topology checks
leoromanovsky Aug 17, 2026
64740ce
test(php): mark OTel interoperability regressions as bugs
leoromanovsky Aug 17, 2026
9e37350
test(php): re-enable OTel interoperability tests
leoromanovsky Aug 17, 2026
437603f
Merge remote-tracking branch 'origin/main' into agent/nodejs-agentles…
leoromanovsky Aug 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/run-end-to-end.yml
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,9 @@ jobs:
- name: Run FEATURE_FLAGGING_AND_EXPERIMENTATION_AGENTLESS scenario
if: steps.build.outcome == 'success' && !cancelled() && contains(inputs.scenarios, '"FEATURE_FLAGGING_AND_EXPERIMENTATION_AGENTLESS"')
run: ./run.sh FEATURE_FLAGGING_AND_EXPERIMENTATION_AGENTLESS
- name: Run FEATURE_FLAGGING_AND_EXPERIMENTATION_AGENTLESS_SERVERLESS scenario
if: steps.build.outcome == 'success' && !cancelled() && contains(inputs.scenarios, '"FEATURE_FLAGGING_AND_EXPERIMENTATION_AGENTLESS_SERVERLESS"')
run: ./run.sh FEATURE_FLAGGING_AND_EXPERIMENTATION_AGENTLESS_SERVERLESS
- name: Run AGENT_SUPPORTING_SPAN_EVENTS scenario
if: steps.build.outcome == 'success' && !cancelled() && contains(inputs.scenarios, '"AGENT_SUPPORTING_SPAN_EVENTS"')
run: ./run.sh AGENT_SUPPORTING_SPAN_EVENTS
Expand Down
1 change: 1 addition & 0 deletions manifests/cpp_httpd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ manifest:
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_agentless_configuration.py: missing_feature (C++ is not in scope for v1 agentless server SDK coverage; FFL-2731 tracks the system-tests contract)
tests/ffe/test_agentless_serverless_exposures.py: 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)
Expand Down
1 change: 1 addition & 0 deletions manifests/cpp_nginx.yml
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,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/docker_ssi/test_docker_ssi_appsec.py::TestDockerSSIAppsecFeatures::test_telemetry_source_ssi: missing_feature
tests/ffe/test_agentless_configuration.py: missing_feature (C++ is not in scope for v1 agentless server SDK coverage; FFL-2731 tracks the system-tests contract)
tests/ffe/test_agentless_serverless_exposures.py: 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)
Expand Down
1 change: 1 addition & 0 deletions manifests/dotnet.yml
Original file line number Diff line number Diff line change
Expand Up @@ -725,6 +725,7 @@ manifest:
component_version: <3.22.0
tests/docker_ssi/test_docker_ssi_appsec.py::TestDockerSSIAppsecFeatures::test_telemetry_source_ssi: v3.36.0
tests/ffe/test_agentless_configuration.py: missing_feature (FFL-2703 tracks .NET agentless configuration-source implementation; FFL-2731 tracks the system-tests contract)
tests/ffe/test_agentless_serverless_exposures.py: missing_feature (Not yet implemented)
tests/ffe/test_dynamic_evaluation.py: v3.36.0
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)
Expand Down
1 change: 1 addition & 0 deletions manifests/golang.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1152,6 +1152,7 @@ manifest:
tests/debugger/test_debugger_telemetry.py::Test_Debugger_Telemetry: missing_feature
tests/docker_ssi/test_docker_ssi_appsec.py::TestDockerSSIAppsecFeatures::test_telemetry_source_ssi: v2.0.0
tests/ffe/test_agentless_configuration.py: missing_feature (FFL-2695 tracks Go agentless configuration-source implementation; FFL-2731 tracks the system-tests contract)
tests/ffe/test_agentless_serverless_exposures.py: missing_feature (Not yet implemented)
tests/ffe/test_dynamic_evaluation.py::Test_FFE_Flag_Parse_Error_Isolation::test_valid_flag_unaffected: bug (FFL-2184)
tests/ffe/test_dynamic_evaluation.py::Test_FFE_RC_Down_From_Start: v2.4.0
tests/ffe/test_dynamic_evaluation.py::Test_FFE_RC_Unavailable: v2.4.0
Expand Down
1 change: 1 addition & 0 deletions manifests/java.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3278,6 +3278,7 @@ manifest:
- weblog_declaration:
"*": irrelevant
spring-boot: v1.65.0
tests/ffe/test_agentless_serverless_exposures.py: missing_feature (Not yet implemented)
tests/ffe/test_dynamic_evaluation.py:
- weblog_declaration:
"*": irrelevant
Expand Down
1 change: 1 addition & 0 deletions manifests/nodejs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1744,6 +1744,7 @@ manifest:
component_version: <5.66.0
tests/docker_ssi/test_docker_ssi_appsec.py::TestDockerSSIAppsecFeatures::test_telemetry_source_ssi: *ref_5_83_0
tests/ffe/test_agentless_configuration.py: *ref_6_8_0
tests/ffe/test_agentless_serverless_exposures.py: missing_feature (dd-trace-js#9527)
tests/ffe/test_dynamic_evaluation.py:
- weblog_declaration:
"*": incomplete_test_app
Expand Down
1 change: 1 addition & 0 deletions manifests/php.yml
Original file line number Diff line number Diff line change
Expand Up @@ -795,6 +795,7 @@ manifest:
tests/docker_ssi/test_docker_ssi_appsec.py::TestDockerSSIAppsecFeatures::test_telemetry_source_ssi: v1.8.3
tests/docker_ssi/test_docker_ssi_crash.py::TestDockerSSICrash::test_crash: missing_feature (No implemented the endpoint /crashme)
tests/ffe/test_agentless_configuration.py: missing_feature (FFL-2705 tracks PHP agentless configuration-source implementation; FFL-2731 tracks the system-tests contract)
tests/ffe/test_agentless_serverless_exposures.py: missing_feature (Not yet implemented)
tests/ffe/test_dynamic_evaluation.py:
- weblog_declaration:
"*": v1.21.0-dev
Expand Down
1 change: 1 addition & 0 deletions manifests/python.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1380,6 +1380,7 @@ manifest:
- declaration: bug (INPLAT-603)
component_version: '>=3.0.0-dev'
tests/ffe/test_agentless_configuration.py: missing_feature (FFL-2699 tracks Python agentless configuration-source implementation; FFL-2731 tracks the system-tests contract)
tests/ffe/test_agentless_serverless_exposures.py: missing_feature (Not yet implemented)
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
Expand Down
1 change: 1 addition & 0 deletions manifests/ruby.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1998,6 +1998,7 @@ manifest:
component_version: <2.7.0
tests/docker_ssi/test_docker_ssi_servicenaming.py::TestDockerServiceNaming::test_service_name: missing_feature (No implemented)
tests/ffe/test_agentless_configuration.py: missing_feature (FFL-2701 tracks Ruby agentless configuration-source implementation; FFL-2731 tracks the system-tests contract)
tests/ffe/test_agentless_serverless_exposures.py: missing_feature (Not yet implemented)
tests/ffe/test_dynamic_evaluation.py:
- weblog_declaration:
"*": irrelevant
Expand Down
1 change: 1 addition & 0 deletions manifests/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,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/docker_ssi/test_docker_ssi_appsec.py::TestDockerSSIAppsecFeatures::test_telemetry_source_ssi: missing_feature
tests/ffe/test_agentless_configuration.py: missing_feature (Rust is not in scope for v1 agentless server SDK coverage; FFL-2731 tracks the system-tests contract)
tests/ffe/test_agentless_serverless_exposures.py: 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)
Expand Down
4 changes: 4 additions & 0 deletions mirror_images.lock.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ images:
digest: sha256:0199e251729b25276f97049cbcd39dd1997e98413a07b10a3c7e9e755d505a42
target: registry.ddbuild.io/system-tests/mirror/datadog/dd-appsec-php-ci:php-8.2-release
tag: php-8.2-release
datadog/serverless-init:1.9.13:
digest: sha256:d187a970ce66d6f1ec4ba26e7c38783ad2e1c6e423fc1b1d39e81ba6332317cf
target: registry.ddbuild.io/system-tests/mirror/datadog/serverless-init:1.9.13
tag: 1.9.13
datadog/system-tests:apache-mod-7.0-zts.base-v1:
digest: sha256:bafc072734341cafb86f45673df2de4851a04b951d0717c88f7958cac1815b20
target: registry.ddbuild.io/system-tests/mirror/datadog/system-tests:apache-mod-7.0-zts.base-v1
Expand Down
1 change: 1 addition & 0 deletions mirror_images.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
- "datadog/agent:latest"
- "datadog/dd-appsec-php-ci:php-8.0-release"
- "datadog/dd-appsec-php-ci:php-8.2-release"
- "datadog/serverless-init:1.9.13"
Comment thread
leoromanovsky marked this conversation as resolved.
Outdated
- "datadog/system-tests:apache-mod-7.0-zts.base-v1"
- "datadog/system-tests:apache-mod-7.0.base-v1"
- "datadog/system-tests:apache-mod-7.1-zts.base-v1"
Expand Down
53 changes: 53 additions & 0 deletions tests/ffe/test_agentless_serverless_exposures.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
"""Test exposure delivery with agentless UFC and serverless-init."""

from tests.ffe.utils.exposures import assert_exposure_side_effects_contract, exposure_events_from_data
from utils import features, interfaces, scenarios, weblog
from utils._context.component_version import Version
from utils.mocked_backend.ffe import EXPECTED_API_KEY


@scenarios.feature_flagging_and_experimentation_agentless_serverless
@features.feature_flags_exposures
class Test_FFE_Agentless_Serverless_Exposures:
flag_key = "empty-targeting-key-flag"
targeting_key = "agentless-serverless-exposure-user"

def setup_agentless_serverless_exposure(self) -> None:
self.responses = [
weblog.post(
"/ffe",
json={
"flag": self.flag_key,
"variationType": "STRING",
"defaultValue": "default",
"targetingKey": self.targeting_key,
"attributes": {},
},
)
for _ in range(5)
]

def test_agentless_serverless_exposure(self) -> None:
scenario = scenarios.feature_flagging_and_experimentation_agentless_serverless
assert scenario.components["serverless-init"] == Version("1.9.13")

matching_requests = assert_exposure_side_effects_contract(
interfaces.ffe_direct,
self.responses,
flag_key=self.flag_key,
targeting_key=self.targeting_key,
expected_value="on-value",
expected_variant="on",
)
assert len(matching_requests) == 1
request = matching_requests[0]
assert request["host"] == "event-platform-intake.datad0g.com"
assert request["response"]["status_code"] == 202

headers = {name.lower(): value for name, value in request["request"]["headers"]}
assert headers["dd-api-key"] == EXPECTED_API_KEY

assert not any(
exposure_events_from_data(data, {self.flag_key}, self.targeting_key)
for data in interfaces.ffe_sidecar.get_data()
)
64 changes: 14 additions & 50 deletions tests/ffe/test_exposures.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

import json

from tests.ffe.utils.exposures import (
EXPOSURES_PATH,
EXPOSURE_WAIT_TIMEOUT_SECONDS,
assert_exposure_side_effects_contract,
exposure_events_from_data,
)
from tests.ffe.utils.fixtures import make_exposure_ufc_fixture as make_ufc_fixture
from utils import (
weblog,
Expand All @@ -14,41 +20,6 @@

RC_PRODUCT = "FFE_FLAGS"
RC_PATH = f"datadog/2/{RC_PRODUCT}"
EXPOSURES_PATH = "/api/v2/exposures"
EXPOSURE_WAIT_TIMEOUT_SECONDS = 30


def exposure_events_from_data(
data: dict, flag_keys: set[str] | None = None, subject_id: str | None = None
) -> list[dict]:
"""Return exposure events from one agent payload matching the optional flag/subject filters."""
if data.get("path") != EXPOSURES_PATH:
return []

exposure_data = data.get("request", {}).get("content")
if not isinstance(exposure_data, dict):
return []

exposures = exposure_data.get("exposures")
if not isinstance(exposures, list):
return []

events = []
for event in exposures:
if not isinstance(event, dict):
continue

flag = event.get("flag")
subject = event.get("subject")
event_flag_key = flag.get("key") if isinstance(flag, dict) else None
event_subject_id = subject.get("id") if isinstance(subject, dict) else None

if flag_keys is not None and event_flag_key not in flag_keys:
continue
if subject_id is not None and event_subject_id != subject_id:
continue
events.append(event)
return events


def find_exposure_events(flag_key: str, subject_id: str | None = None) -> list[dict]:
Expand Down Expand Up @@ -536,21 +507,14 @@ def setup_ffe_exposure_caching_same_subject(self):
self.responses.append(r)

def test_ffe_exposure_caching_same_subject(self):
"""Test that multiple evaluations for the same subject generate at most one exposure event."""
# Verify all requests succeeded
for i, r in enumerate(self.responses):
assert r.status_code == 200, f"Request {i + 1} failed: {r.text}"
result = json.loads(r.text)
assert result["value"] == "value-a", f"Request {i + 1}: expected 'value-a', got '{result['value']}'"

# Count exposure events for this specific subject
exposure_count = wait_for_min_exposure_count(self.flag_key, 1, self.targeting_key)

# The exposure cache should deduplicate events - we expect exactly 1 exposure
# for the same (subject, allocation, variant) tuple
assert exposure_count == 1, (
f"Expected exactly 1 exposure event for subject '{self.targeting_key}' due to caching, "
f"but found {exposure_count} events"
"""Test the shared exposure contract through Remote Configuration and an Agent."""
assert_exposure_side_effects_contract(
interfaces.agent,
self.responses,
flag_key=self.flag_key,
targeting_key=self.targeting_key,
expected_value="value-a",
expected_variant="variant-a",
)


Expand Down
93 changes: 93 additions & 0 deletions tests/ffe/utils/exposures.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
"""Route-neutral Feature Flags exposure expectations."""

from collections.abc import Iterable
import json

from utils._weblog import HttpResponse
from utils.interfaces._core import ProxyBasedInterfaceValidator

EXPOSURES_PATH = "/api/v2/exposures"
EXPOSURE_WAIT_TIMEOUT_SECONDS = 30
EXPOSURE_CACHE_SETTLE_SECONDS = 3


def exposure_events_from_data(
data: dict, flag_keys: set[str] | None = None, subject_id: str | None = None
) -> list[dict]:
"""Return matching exposure events from one captured request."""
if data.get("path") != EXPOSURES_PATH:
return []

exposure_data = data.get("request", {}).get("content")
if not isinstance(exposure_data, dict):
return []

exposures = exposure_data.get("exposures")
if not isinstance(exposures, list):
return []

events = []
for event in exposures:
if not isinstance(event, dict):
continue

flag = event.get("flag")
subject = event.get("subject")
event_flag_key = flag.get("key") if isinstance(flag, dict) else None
event_subject_id = subject.get("id") if isinstance(subject, dict) else None

if flag_keys is not None and event_flag_key not in flag_keys:
continue
if subject_id is not None and event_subject_id != subject_id:
continue
events.append(event)
return events


def assert_exposure_side_effects_contract(
interface: ProxyBasedInterfaceValidator,
responses: Iterable[HttpResponse],
*,
flag_key: str,
targeting_key: str,
expected_value: str,
expected_variant: str,
expected_allocation: str = "default-allocation",
) -> list[dict]:
"""Assert one exposure contract through Agent, sidecar, or direct intake capture."""
for index, response in enumerate(responses, start=1):
assert response.status_code == 200, f"Evaluation {index} failed: {response.text}"
value = json.loads(response.text)["value"]
assert value == expected_value, f"Evaluation {index} returned {value!r}"

def matches_exposure(data: dict) -> bool:
return bool(exposure_events_from_data(data, {flag_key}, targeting_key))

assert interface.wait_for(
matches_exposure,
timeout=EXPOSURE_WAIT_TIMEOUT_SECONDS,
), f"Timed out waiting for exposure event for {flag_key!r} and {targeting_key!r}"

if not interface.replay:
interface.wait(EXPOSURE_CACHE_SETTLE_SECONDS)

matching_requests = [data for data in interface.get_data() if matches_exposure(data)]
events = [
event
for request in matching_requests
for event in exposure_events_from_data(request, {flag_key}, targeting_key)
]
assert len(events) == 1, f"The exposure cache produced {len(events)} matching events instead of one"

for request in matching_requests:
context = request["request"]["content"]["context"]
assert context["service"] == "weblog"
assert context["version"] == "1.0.0"
assert context["env"] == "system-tests"

event = events[0]
assert event["flag"]["key"] == flag_key
assert event["variant"]["key"] == expected_variant
assert event["allocation"]["key"] == expected_allocation
assert event["subject"]["id"] == targeting_key
return matching_requests
6 changes: 4 additions & 2 deletions tests/test_the_test/test_compute_libraries_and_scenarios.py
Original file line number Diff line number Diff line change
Expand Up @@ -503,7 +503,8 @@ def test_agentless_ffe_mocked_backend_file(self):
default_libs_with_dev,
3600,
"false",
"DEFAULT,FEATURE_FLAGGING_AND_EXPERIMENTATION_AGENTLESS,PARAMETRIC",
"DEFAULT,FEATURE_FLAGGING_AND_EXPERIMENTATION_AGENTLESS,"
"FEATURE_FLAGGING_AND_EXPERIMENTATION_AGENTLESS_SERVERLESS,PARAMETRIC",
"",
)

Expand All @@ -515,7 +516,8 @@ def test_end_to_end_scenario_framework_file(self):
default_libs_with_dev,
3600,
"false",
"DEFAULT,FEATURE_FLAGGING_AND_EXPERIMENTATION_AGENTLESS",
"DEFAULT,FEATURE_FLAGGING_AND_EXPERIMENTATION_AGENTLESS,"
"FEATURE_FLAGGING_AND_EXPERIMENTATION_AGENTLESS_SERVERLESS",
"end_to_end",
)

Expand Down
9 changes: 9 additions & 0 deletions tests/test_the_test/test_group_rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,19 @@ def test_tracer_release():
assert scenario_groups.end_to_end not in dormant_agentless_scenario.scenario_groups
assert scenario_groups.tracer_release not in dormant_agentless_scenario.scenario_groups

serverless_exposure_scenario = scenarios.feature_flagging_and_experimentation_agentless_serverless
assert serverless_exposure_scenario.include_agent is False
assert serverless_exposure_scenario.use_proxy is True
assert scenario_groups.ffe in serverless_exposure_scenario.scenario_groups
assert scenario_groups.all not in serverless_exposure_scenario.scenario_groups
assert scenario_groups.end_to_end not in serverless_exposure_scenario.scenario_groups
assert scenario_groups.tracer_release not in serverless_exposure_scenario.scenario_groups

not_in_tracer_release_group = [
# list of scenario that will never be part of tracer release
scenarios.fuzzer,
dormant_agentless_scenario,
serverless_exposure_scenario,
scenarios.mock_the_test,
scenarios.mock_the_test_2,
scenarios.test_the_test,
Expand Down
Loading
Loading