This directory contains system tests for the Feature Flags & Experimentation (FFE) functionality.
| File | Description |
|---|---|
test_dynamic_evaluation.py |
Dynamic flag evaluation via Remote Config |
test_exposure_egress.py |
Shared exposure cache and payload contract across Agent, sidecar, and direct egress |
test_exposures_datadog_agent.py |
Flag exposure tracking and reporting through the Datadog Agent |
test_flag_eval_metrics.py |
Evaluation metrics (OTel counter) |
test_flag_eval_evp.py |
Server-side EVP flagevaluation payloads, aggregation, and bounds |
./run.sh FEATURE_FLAGGING_AND_EXPERIMENTATION --library <language>This section captures lessons learned from implementing flag evaluation metrics in dd-trace-py to help accelerate implementations in other SDKs.
CI takes 1+ hour. Local testing takes ~2 minutes.
Run FFE tests locally:
./run.sh FEATURE_FLAGGING_AND_EXPERIMENTATION --library <language> -k "test_ffe_eval"Example for Python:
./run.sh FEATURE_FLAGGING_AND_EXPERIMENTATION --library python -k "test_ffe_eval"For Python, create binaries/python-load-from-s3 with the commit SHA:
echo "<commit-sha>" > binaries/python-load-from-s3Check if the wheel is on S3 by verifying the "Upload wheels to S3" job passed in the SDK's CI.
-
Implement
feature_flag.evaluationsOTel counter metric- Emitted on every flag evaluation via OpenFeature hooks
- Use a Finally hook (not in evaluate()) to capture type conversion errors
-
Required tags:
feature_flag.key— the flag keyfeature_flag.result.variant— the resolved variantfeature_flag.result.reason— lowercase (e.g.,targeting_match,error,default,disabled,static,split)feature_flag.result.allocation_key— only when present and non-emptyerror.type— only on error, lowercase (e.g.,flag_not_found,type_mismatch,parse_error,provider_not_ready)
-
Cross-tracer consistency requirements:
- Return
PROVIDER_NOT_READY(notGENERAL) when no config is loaded - Use
"unknown"(not empty string) as fallback for missing reason - Use raw lowercase error codes directly (no conversion functions)
- Return
-
Add OTel OTLP metrics exporter to weblog Dockerfiles
For Python, add to each weblog Dockerfile:
RUN pip install opentelemetry-exporter-otlp-proto-http -
Enable tests in manifest
In
manifests/<language>.yml, change:tests/ffe/test_flag_eval_metrics.py: missing_feature
to:
tests/ffe/test_flag_eval_metrics.py: <version>
The system tests cover:
| Reason | Test | Scenario |
|---|---|---|
static |
Test_FFE_Eval_Metric_Basic |
No rules, no shards (catch-all) |
targeting_match |
Test_FFE_Eval_Reason_Targeting |
Targeting rules match context |
split |
Test_FFE_Eval_Reason_Split |
50/50 shard-based rollout |
default |
Test_FFE_Eval_Reason_Default |
Rules don't match |
disabled |
Test_FFE_Eval_Reason_Disabled |
Flag is disabled |
| Error Code | Test | Trigger |
|---|---|---|
flag_not_found |
Test_FFE_Eval_Config_Exists_Flag_Missing |
Config exists, flag missing |
type_mismatch |
Test_FFE_Eval_Metric_Type_Mismatch |
Request boolean from string flag |
parse_error |
Test_FFE_Eval_Metric_Parse_Error_Invalid_Regex |
Invalid regex pattern in condition |
parse_error |
Test_FFE_Eval_Metric_Parse_Error_Variant_Type_Mismatch |
Variant value doesn't match declared type |
provider_not_ready |
Test_FFE_Eval_No_Config_Loaded |
No config loaded |
Test_FFE_Eval_Metric_Count— Multiple evaluations produce correct countTest_FFE_Eval_Metric_Different_Flags— Different flags get separate seriesTest_FFE_Eval_Metric_Numeric_To_Integer— Numeric to integer conversionTest_FFE_Eval_Targeting_Key_Optional— Targeting key is optionalTest_FFE_Eval_Nested_Attributes_Ignored— Nested attributes silently ignored (OF.3)Test_FFE_Eval_Lowercase_Consistency— All tag values are lowercase
The totalShards field must be inside each shard object, not at the allocation level:
# CORRECT format for split/shard-based allocation
"splits": [
{
"variationKey": "on",
"shards": [
{
"salt": "test-salt",
"totalShards": 10000, # Inside shard object
"ranges": [{"start": 0, "end": 5000}],
}
],
},
{
"variationKey": "off",
"shards": [
{
"salt": "test-salt",
"totalShards": 10000,
"ranges": [{"start": 5000, "end": 10000}],
}
],
},
]Note: A single variation covering 100% (0-10000) returns STATIC, not SPLIT. You need multiple variations with different shard ranges for a true SPLIT reason.
cat logs_feature_flagging_and_experimentation/docker/weblog/stdout.log
cat logs_feature_flagging_and_experimentation/docker/weblog/stderr.logThe tests use interfaces.agent.get_metrics() to retrieve metrics from the agent.
./run.sh FEATURE_FLAGGING_AND_EXPERIMENTATION --library python -k "Test_FFE_Eval_Reason_Split"- Check UFC fixture format (especially
totalShardsplacement) - Verify the fixture matches
flags-v1.jsonformat in dd-trace-py
- Need multiple variations with different shard ranges
- Single variation covering 100% returns
static
- Verify OTel exporter is installed in weblog
- Check
DD_METRICS_OTEL_ENABLED=trueis set in scenario - Verify the metric export interval (10s default)
- Run locally first:
./run.sh FEATURE_FLAGGING_AND_EXPERIMENTATION --library <lang> -k "test_ffe_eval" - Only push to CI when local tests pass
- Implement metrics in SDK following the Go reference implementation
- Create system-tests branch off
main - Add OTel exporter to weblog Dockerfiles
- Enable tests in manifest file
- Run locally with
./run.shto iterate quickly - Push to CI only when local tests pass