Skip to content

Enable EVP flagevaluation system tests for Python - #7183

Draft
leoromanovsky wants to merge 4 commits into
mainfrom
leo.romanovsky/ffe-evp-flagevaluation-enable-python-clean
Draft

leoromanovsky wants to merge 4 commits into
mainfrom
leo.romanovsky/ffe-evp-flagevaluation-enable-python-clean

Conversation

@leoromanovsky

@leoromanovsky leoromanovsky commented Jun 20, 2026 •

Copy link
Copy Markdown
Contributor

Motivation

Python needs the shared server-side EVP flagevaluation system-test contract enabled so APM reviewers can validate that Python evaluations produce the same backend-visible signal as the other SDKs. This PR provides the public system-tests companion for the Python SDK fanout.

Changes

  • Changes tests/ffe/test_flag_eval_evp.py in manifests/python.yml from missing_feature (FFL-2446) to v4.12.0rc1.
  • Keeps the test implementation inherited from current system-tests main.

Decisions

  • Keep this PR to a single Python manifest entry so the language enablement diff stays isolated.
  • The matching SDK implementation is DataDog/dd-trace-py#18613.
  • Existing OTel metric coverage stays in tests/ffe/test_flag_eval_metrics.py; this PR is only for EVP flagevaluation.

Validation Evidence

System Tests

@github-actions

Copy link
Copy Markdown
Contributor

CODEOWNERS have been resolved as:

manifests/python.yml                                                    @DataDog/apm-python @DataDog/asm-python

@datadog-official

datadog-official Bot commented Jun 20, 2026 •

Copy link
Copy Markdown

Pipelines  Tests

✨ Fix all issues with BitsAI

⚠️ Warnings

🚦 9 Pipeline jobs failed

Testing the test | System Tests (python, dev) / End-to-end #1 / django-poc 1   View in Datadog   GitHub Actions

🧪 3 Tests failed

tests.ffe.test_flag_eval_evp.Test_FFE_EVP_Flagevaluation_Basic.test_ffe_evp_flagevaluation_basic[django-poc] from system_tests_suite   View in Datadog
AssertionError: Timed out waiting for EVP flagevaluation event for flag evp-basic-flag
assert False
 +  where False = <bound method ProxyBasedInterfaceValidator.wait_for of AgentInterfaceValidator('agent')>(<function wait_for_evp_flagevaluation_event.<locals>.<lambda> at 0x7f115a145760>, timeout=30)
 +    where <bound method ProxyBasedInterfaceValidator.wait_for of AgentInterfaceValidator('agent')> = AgentInterfaceValidator('agent').wait_for
 +      where AgentInterfaceValidator('agent') = interfaces.agent

self = <tests.ffe.test_flag_eval_evp.Test_FFE_EVP_Flagevaluation_Basic object at 0x7f1182d00830>

    def test_ffe_evp_flagevaluation_basic(self) -> None:
        assert self.r.status_code == 200, f"Flag evaluation failed: {self.r.text}"
...
tests.ffe.test_flag_eval_evp.Test_FFE_EVP_Flagevaluation_Burst_Aggregation.test_ffe_evp_flagevaluation_burst_aggregation[django-poc] from system_tests_suite   View in Datadog
AssertionError: Timed out waiting for EVP flagevaluation event for flag evp-burst-aggregation-flag
assert False
 +  where False = <bound method ProxyBasedInterfaceValidator.wait_for of AgentInterfaceValidator('agent')>(<function wait_for_evp_flagevaluation_event.<locals>.<lambda> at 0x7f115adc4b80>, timeout=30)
 +    where <bound method ProxyBasedInterfaceValidator.wait_for of AgentInterfaceValidator('agent')> = AgentInterfaceValidator('agent').wait_for
 +      where AgentInterfaceValidator('agent') = interfaces.agent

self = <tests.ffe.test_flag_eval_evp.Test_FFE_EVP_Flagevaluation_Burst_Aggregation object at 0x7f1182ed4d70>

    def test_ffe_evp_flagevaluation_burst_aggregation(self) -> None:
        for index, response in enumerate(self.responses):
...
View all 3 test failures

Testing the test | System Tests (python, dev) / End-to-end #1 / django-py3.13 1   View in Datadog   GitHub Actions

🧪 3 Tests failed

tests.ffe.test_flag_eval_evp.Test_FFE_EVP_Flagevaluation_Basic.test_ffe_evp_flagevaluation_basic[django-py3.13] from system_tests_suite   View in Datadog
AssertionError: Timed out waiting for EVP flagevaluation event for flag evp-basic-flag
assert False
 +  where False = <bound method ProxyBasedInterfaceValidator.wait_for of AgentInterfaceValidator('agent')>(<function wait_for_evp_flagevaluation_event.<locals>.<lambda> at 0x7f3c64a5d440>, timeout=30)
 +    where <bound method ProxyBasedInterfaceValidator.wait_for of AgentInterfaceValidator('agent')> = AgentInterfaceValidator('agent').wait_for
 +      where AgentInterfaceValidator('agent') = interfaces.agent

self = <tests.ffe.test_flag_eval_evp.Test_FFE_EVP_Flagevaluation_Basic object at 0x7f3c91b4a390>

    def test_ffe_evp_flagevaluation_basic(self) -> None:
        assert self.r.status_code == 200, f"Flag evaluation failed: {self.r.text}"
...
tests.ffe.test_flag_eval_evp.Test_FFE_EVP_Flagevaluation_Burst_Aggregation.test_ffe_evp_flagevaluation_burst_aggregation[django-py3.13] from system_tests_suite   View in Datadog
AssertionError: Timed out waiting for EVP flagevaluation event for flag evp-burst-aggregation-flag
assert False
 +  where False = <bound method ProxyBasedInterfaceValidator.wait_for of AgentInterfaceValidator('agent')>(<function wait_for_evp_flagevaluation_event.<locals>.<lambda> at 0x7f3c653342c0>, timeout=30)
 +    where <bound method ProxyBasedInterfaceValidator.wait_for of AgentInterfaceValidator('agent')> = AgentInterfaceValidator('agent').wait_for
 +      where AgentInterfaceValidator('agent') = interfaces.agent

self = <tests.ffe.test_flag_eval_evp.Test_FFE_EVP_Flagevaluation_Burst_Aggregation object at 0x7f3c91650c50>

    def test_ffe_evp_flagevaluation_burst_aggregation(self) -> None:
        for index, response in enumerate(self.responses):
...
View all 3 test failures

Testing the test | System Tests (python, dev) / End-to-end #1 / fastapi 1   View in Datadog   GitHub Actions

🧪 3 Tests failed

tests.ffe.test_flag_eval_evp.Test_FFE_EVP_Flagevaluation_Basic.test_ffe_evp_flagevaluation_basic[fastapi] from system_tests_suite   View in Datadog
AssertionError: Timed out waiting for EVP flagevaluation event for flag evp-basic-flag
assert False
 +  where False = <bound method ProxyBasedInterfaceValidator.wait_for of AgentInterfaceValidator('agent')>(<function wait_for_evp_flagevaluation_event.<locals>.<lambda> at 0x7f9e2ae45f80>, timeout=30)
 +    where <bound method ProxyBasedInterfaceValidator.wait_for of AgentInterfaceValidator('agent')> = AgentInterfaceValidator('agent').wait_for
 +      where AgentInterfaceValidator('agent') = interfaces.agent

self = <tests.ffe.test_flag_eval_evp.Test_FFE_EVP_Flagevaluation_Basic object at 0x7f9e548a4c20>

    def test_ffe_evp_flagevaluation_basic(self) -> None:
        assert self.r.status_code == 200, f"Flag evaluation failed: {self.r.text}"
...
tests.ffe.test_flag_eval_evp.Test_FFE_EVP_Flagevaluation_Burst_Aggregation.test_ffe_evp_flagevaluation_burst_aggregation[fastapi] from system_tests_suite   View in Datadog
AssertionError: Timed out waiting for EVP flagevaluation event for flag evp-burst-aggregation-flag
assert False
 +  where False = <bound method ProxyBasedInterfaceValidator.wait_for of AgentInterfaceValidator('agent')>(<function wait_for_evp_flagevaluation_event.<locals>.<lambda> at 0x7f9e2b57cf40>, timeout=30)
 +    where <bound method ProxyBasedInterfaceValidator.wait_for of AgentInterfaceValidator('agent')> = AgentInterfaceValidator('agent').wait_for
 +      where AgentInterfaceValidator('agent') = interfaces.agent

self = <tests.ffe.test_flag_eval_evp.Test_FFE_EVP_Flagevaluation_Burst_Aggregation object at 0x7f9e548a4500>

    def test_ffe_evp_flagevaluation_burst_aggregation(self) -> None:
        for index, response in enumerate(self.responses):
...
View all 3 test failures

View all 9 failed jobs.

ℹ️ Info

No other issues found (see more)

❄️ No new flaky tests detected

Useful? React with 👍 / 👎

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: 29a2ec3 | Docs | Datadog PR Page | Give us feedback!

@datadog-system-tests-org

datadog-system-tests-org Bot commented Jun 20, 2026 •

Copy link
Copy Markdown

Pipelines

⚠️ Warnings

🚦 9 Pipeline jobs failed

Testing the test | System Tests (python, dev) / End-to-end #1 / django-poc 1   View in Datadog   GitHub Actions

Testing the test | System Tests (python, dev) / End-to-end #1 / django-py3.13 1   View in Datadog   GitHub Actions

Testing the test | System Tests (python, dev) / End-to-end #1 / fastapi 1   View in Datadog   GitHub Actions

View all 9 failed jobs.

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: a50be98 | Docs | Give us feedback!

Base automatically changed from leo.romanovsky/ffe-evp-flagevaluation-contract-fix to main June 22, 2026 19:40
…p-flagevaluation-enable-python-clean

# Conflicts:
#	tests/ffe/test_flag_eval_evp.py
gh-worker-dd-mergequeue-cf854d Bot pushed a commit to DataDog/dd-trace-py that referenced this pull request Jul 14, 2026
## Motivation

Server-side feature-flag evaluations need to be observable in the same backend track as the rest of the cross-SDK rollout so APM can judge rollout smoothness and time-to-land from real evaluation counts, not only app-local behavior or metric-side approximations. This contribution gives Python services backend-visible `flagevaluation` telemetry with bounded cardinality and batching, so customers and internal dogfooding can verify which flags evaluated, for which targeting keys, and how often, while preserving the existing `feature_flag.evaluations` metric path.

## Changes

- Adds a Python EVP `flagevaluation` writer behind `DD_FLAGGING_EVALUATION_COUNTS_ENABLED`; existing OTel `feature_flag.evaluations` metrics remain unchanged.
- Registers an OpenFeature `finally_after` logging hook that captures a cheap evaluation snapshot and hands it to a background writer.
- Renames the capture hook to `FlagEvalEVPHook` / `_flag_eval_evp_hook.py` so it is distinct from `FlagEvalMetricsHook`.
- Posts batched payloads to the Agent EVP proxy route `/evp_proxy/v2/api/v2/flagevaluation` with `X-Datadog-EVP-Subdomain: event-platform-intake`.
- Aggregates evaluations by schema-visible dimensions: flag key, variant key, allocation key, runtime-default state, error message, targeting key, and pruned evaluation context.
- Flattens and prunes evaluation context before enqueue with deterministic 256-field / 256-character bounds.
- Splits encoded `flagEvaluations` request bodies under the shared EVP payload limit before POSTing.
- Degrades a single oversized full-tier row by dropping `targeting_key` and `context`; if the degraded row still cannot fit, the writer drops and logs it.
- Uses evaluation-time metadata to populate `first_evaluation` and `last_evaluation` rather than flush time.
- Keeps accepting the legacy `DataDogProvider(initialization_timeout=...)` keyword for compatibility, while async initialization no longer uses it.

### Evaluation Capture And Flush

```mermaid
flowchart TD
  Eval["OpenFeature evaluation"] --> Metrics["FlagEvalMetricsHook\nexisting OTel metric"]
  Eval --> Logging["FlagEvalEVPHook\nfinally_after"]
  Logging --> Snapshot["Cheap scalar snapshot\nflag, variant, allocation,\ntargeting_key, context, eval time"]
  Snapshot --> Queue["Bounded non-blocking queue\nQueue size 4096"]
  Queue --> Drain["Background drain worker\n0.1s queue drain"]
  Drain --> Aggregate["FlagEvaluationWriter aggregation\nfull tier then degraded tier"]
  Aggregate --> Flush["Periodic flush\n10s interval"]
  Flush --> EVP["Agent EVP proxy\n/evp_proxy/v2/api/v2/flagevaluation"]
```

### Payload Limit Handling

```mermaid
flowchart TD
  Events["Aggregated flagEvaluations"] --> Encode["Encode each event with compact JSON"]
  Encode --> FitsEvent{"Single event fits\ninside shared EVP limit?"}
  FitsEvent -- "yes" --> Batch["Add to current payload"]
  FitsEvent -- "no" --> Degrade["Drop targeting_key and context\nfor that event only"]
  Degrade --> FitsDegraded{"Degraded event fits?"}
  FitsDegraded -- "yes" --> Batch
  FitsDegraded -- "no" --> Drop["Drop and count\npayload_limit"]
  Batch --> FitsPayload{"Payload still <=\nDEFAULT_EVP_PAYLOAD_SIZE_LIMIT?"}
  FitsPayload -- "yes" --> Continue["Continue batching"]
  FitsPayload -- "no" --> Split["Close current payload\nstart next payload"]
  Split --> Continue
  Continue --> Post["POST each payload sequentially"]
```

## Decisions

- Keep this as a dual-path implementation: the EVP logging writer is additive and separately gated, while the existing OTel metric hook remains registered when the provider is enabled.
- Use the existing Agent EVP proxy transport and the canonical `flagevaluation` backend route instead of adding a Python-specific transport or schema.
- Keep the evaluation logging hook non-blocking on application evaluation threads: queueing is best-effort, bounded, and drop-counted under backpressure.
- Do aggregation, JSON serialization, payload splitting, and POSTs only in the background writer path.
- Split `flagEvaluations` payloads by encoded byte size using `DEFAULT_EVP_PAYLOAD_SIZE_LIMIT`; send split payloads sequentially from the writer thread.
- Use a two-tier aggregation model: full buckets retain `targeting_key` and pruned context; degraded buckets retain only lower-cardinality schema dimensions.
- Store `targeting_key` in the dedicated EVP field and remove `targetingKey` / `targeting_key` from context before building context keys or payloads.
- Do not emit OpenFeature `reason` because it is not accepted by the canonical worker schema, and do not populate `targeting_rule.key` until Python exposes real targeting-rule metadata.
- Keep `openfeature-sdk` as an application/test/lint dependency rather than adding it as a runtime dependency of `ddtrace`; the dogfooding app dependency floor is being tightened separately in https://github.com/DataDog/ffe-dogfooding/pull/88.

## Validation Evidence

### Dogfooding App

- `ffe-dogfooding` `app-python` was run with local `dd-trace-py` artifacts and reached `PROVIDER_READY`.
- The running dogfooding app reported `openfeature-sdk=0.9.0`.
- Evaluated `ffe-dogfooding-string-flag` through the Python dogfooding app with 5 evaluations per targeting key over one persistent connection:
  - `python-batch-keepalive-20260623T004034Z-alpha`
  - `python-batch-keepalive-20260623T004034Z-bravo`
  - `python-batch-keepalive-20260623T004034Z-charlie`
- App-side result: all evaluations returned `variant_2` with allocation `allocation-override-392dd7c149f8`.

### System Tests

- Companion draft PR: DataDog/system-tests#7183

### Staging End-To-End

- Dogfooding ran without the local mock-intake EVP tee/proxy, so the Agent sent EVP traffic through the normal backend path.
- Retriever staging query returned 3 backend `flagevaluation` rows for the exact targeting keys above.
- Each targeting key returned one row with `evaluation_count=5`:
  - `python-batch-keepalive-20260623T004034Z-alpha`
  - `python-batch-keepalive-20260623T004034Z-bravo`
  - `python-batch-keepalive-20260623T004034Z-charlie`
- All rows had `flag.key=ffe-dogfooding-string-flag`, `variant.key=variant_2`, and `allocation.key=allocation-override-392dd7c149f8`.



Co-authored-by: leo.romanovsky <leo.romanovsky@datadoghq.com>
brettlangdon pushed a commit to DataDog/dd-trace-py that referenced this pull request Jul 15, 2026
## Motivation

Server-side feature-flag evaluations need to be observable in the same backend track as the rest of the cross-SDK rollout so APM can judge rollout smoothness and time-to-land from real evaluation counts, not only app-local behavior or metric-side approximations. This contribution gives Python services backend-visible `flagevaluation` telemetry with bounded cardinality and batching, so customers and internal dogfooding can verify which flags evaluated, for which targeting keys, and how often, while preserving the existing `feature_flag.evaluations` metric path.

## Changes

- Adds a Python EVP `flagevaluation` writer behind `DD_FLAGGING_EVALUATION_COUNTS_ENABLED`; existing OTel `feature_flag.evaluations` metrics remain unchanged.
- Registers an OpenFeature `finally_after` logging hook that captures a cheap evaluation snapshot and hands it to a background writer.
- Renames the capture hook to `FlagEvalEVPHook` / `_flag_eval_evp_hook.py` so it is distinct from `FlagEvalMetricsHook`.
- Posts batched payloads to the Agent EVP proxy route `/evp_proxy/v2/api/v2/flagevaluation` with `X-Datadog-EVP-Subdomain: event-platform-intake`.
- Aggregates evaluations by schema-visible dimensions: flag key, variant key, allocation key, runtime-default state, error message, targeting key, and pruned evaluation context.
- Flattens and prunes evaluation context before enqueue with deterministic 256-field / 256-character bounds.
- Splits encoded `flagEvaluations` request bodies under the shared EVP payload limit before POSTing.
- Degrades a single oversized full-tier row by dropping `targeting_key` and `context`; if the degraded row still cannot fit, the writer drops and logs it.
- Uses evaluation-time metadata to populate `first_evaluation` and `last_evaluation` rather than flush time.
- Keeps accepting the legacy `DataDogProvider(initialization_timeout=...)` keyword for compatibility, while async initialization no longer uses it.

### Evaluation Capture And Flush

```mermaid
flowchart TD
  Eval["OpenFeature evaluation"] --> Metrics["FlagEvalMetricsHook\nexisting OTel metric"]
  Eval --> Logging["FlagEvalEVPHook\nfinally_after"]
  Logging --> Snapshot["Cheap scalar snapshot\nflag, variant, allocation,\ntargeting_key, context, eval time"]
  Snapshot --> Queue["Bounded non-blocking queue\nQueue size 4096"]
  Queue --> Drain["Background drain worker\n0.1s queue drain"]
  Drain --> Aggregate["FlagEvaluationWriter aggregation\nfull tier then degraded tier"]
  Aggregate --> Flush["Periodic flush\n10s interval"]
  Flush --> EVP["Agent EVP proxy\n/evp_proxy/v2/api/v2/flagevaluation"]
```

### Payload Limit Handling

```mermaid
flowchart TD
  Events["Aggregated flagEvaluations"] --> Encode["Encode each event with compact JSON"]
  Encode --> FitsEvent{"Single event fits\ninside shared EVP limit?"}
  FitsEvent -- "yes" --> Batch["Add to current payload"]
  FitsEvent -- "no" --> Degrade["Drop targeting_key and context\nfor that event only"]
  Degrade --> FitsDegraded{"Degraded event fits?"}
  FitsDegraded -- "yes" --> Batch
  FitsDegraded -- "no" --> Drop["Drop and count\npayload_limit"]
  Batch --> FitsPayload{"Payload still <=\nDEFAULT_EVP_PAYLOAD_SIZE_LIMIT?"}
  FitsPayload -- "yes" --> Continue["Continue batching"]
  FitsPayload -- "no" --> Split["Close current payload\nstart next payload"]
  Split --> Continue
  Continue --> Post["POST each payload sequentially"]
```

## Decisions

- Keep this as a dual-path implementation: the EVP logging writer is additive and separately gated, while the existing OTel metric hook remains registered when the provider is enabled.
- Use the existing Agent EVP proxy transport and the canonical `flagevaluation` backend route instead of adding a Python-specific transport or schema.
- Keep the evaluation logging hook non-blocking on application evaluation threads: queueing is best-effort, bounded, and drop-counted under backpressure.
- Do aggregation, JSON serialization, payload splitting, and POSTs only in the background writer path.
- Split `flagEvaluations` payloads by encoded byte size using `DEFAULT_EVP_PAYLOAD_SIZE_LIMIT`; send split payloads sequentially from the writer thread.
- Use a two-tier aggregation model: full buckets retain `targeting_key` and pruned context; degraded buckets retain only lower-cardinality schema dimensions.
- Store `targeting_key` in the dedicated EVP field and remove `targetingKey` / `targeting_key` from context before building context keys or payloads.
- Do not emit OpenFeature `reason` because it is not accepted by the canonical worker schema, and do not populate `targeting_rule.key` until Python exposes real targeting-rule metadata.
- Keep `openfeature-sdk` as an application/test/lint dependency rather than adding it as a runtime dependency of `ddtrace`; the dogfooding app dependency floor is being tightened separately in https://github.com/DataDog/ffe-dogfooding/pull/88.

## Validation Evidence

### Dogfooding App

- `ffe-dogfooding` `app-python` was run with local `dd-trace-py` artifacts and reached `PROVIDER_READY`.
- The running dogfooding app reported `openfeature-sdk=0.9.0`.
- Evaluated `ffe-dogfooding-string-flag` through the Python dogfooding app with 5 evaluations per targeting key over one persistent connection:
  - `python-batch-keepalive-20260623T004034Z-alpha`
  - `python-batch-keepalive-20260623T004034Z-bravo`
  - `python-batch-keepalive-20260623T004034Z-charlie`
- App-side result: all evaluations returned `variant_2` with allocation `allocation-override-392dd7c149f8`.

### System Tests

- Companion draft PR: DataDog/system-tests#7183

### Staging End-To-End

- Dogfooding ran without the local mock-intake EVP tee/proxy, so the Agent sent EVP traffic through the normal backend path.
- Retriever staging query returned 3 backend `flagevaluation` rows for the exact targeting keys above.
- Each targeting key returned one row with `evaluation_count=5`:
  - `python-batch-keepalive-20260623T004034Z-alpha`
  - `python-batch-keepalive-20260623T004034Z-bravo`
  - `python-batch-keepalive-20260623T004034Z-charlie`
- All rows had `flag.key=ffe-dogfooding-string-flag`, `variant.key=variant_2`, and `allocation.key=allocation-override-392dd7c149f8`.



Co-authored-by: leo.romanovsky <leo.romanovsky@datadoghq.com>
vlad-scherbich pushed a commit to DataDog/dd-trace-py that referenced this pull request Jul 15, 2026
## Motivation

Server-side feature-flag evaluations need to be observable in the same backend track as the rest of the cross-SDK rollout so APM can judge rollout smoothness and time-to-land from real evaluation counts, not only app-local behavior or metric-side approximations. This contribution gives Python services backend-visible `flagevaluation` telemetry with bounded cardinality and batching, so customers and internal dogfooding can verify which flags evaluated, for which targeting keys, and how often, while preserving the existing `feature_flag.evaluations` metric path.

## Changes

- Adds a Python EVP `flagevaluation` writer behind `DD_FLAGGING_EVALUATION_COUNTS_ENABLED`; existing OTel `feature_flag.evaluations` metrics remain unchanged.
- Registers an OpenFeature `finally_after` logging hook that captures a cheap evaluation snapshot and hands it to a background writer.
- Renames the capture hook to `FlagEvalEVPHook` / `_flag_eval_evp_hook.py` so it is distinct from `FlagEvalMetricsHook`.
- Posts batched payloads to the Agent EVP proxy route `/evp_proxy/v2/api/v2/flagevaluation` with `X-Datadog-EVP-Subdomain: event-platform-intake`.
- Aggregates evaluations by schema-visible dimensions: flag key, variant key, allocation key, runtime-default state, error message, targeting key, and pruned evaluation context.
- Flattens and prunes evaluation context before enqueue with deterministic 256-field / 256-character bounds.
- Splits encoded `flagEvaluations` request bodies under the shared EVP payload limit before POSTing.
- Degrades a single oversized full-tier row by dropping `targeting_key` and `context`; if the degraded row still cannot fit, the writer drops and logs it.
- Uses evaluation-time metadata to populate `first_evaluation` and `last_evaluation` rather than flush time.
- Keeps accepting the legacy `DataDogProvider(initialization_timeout=...)` keyword for compatibility, while async initialization no longer uses it.

### Evaluation Capture And Flush

```mermaid
flowchart TD
  Eval["OpenFeature evaluation"] --> Metrics["FlagEvalMetricsHook\nexisting OTel metric"]
  Eval --> Logging["FlagEvalEVPHook\nfinally_after"]
  Logging --> Snapshot["Cheap scalar snapshot\nflag, variant, allocation,\ntargeting_key, context, eval time"]
  Snapshot --> Queue["Bounded non-blocking queue\nQueue size 4096"]
  Queue --> Drain["Background drain worker\n0.1s queue drain"]
  Drain --> Aggregate["FlagEvaluationWriter aggregation\nfull tier then degraded tier"]
  Aggregate --> Flush["Periodic flush\n10s interval"]
  Flush --> EVP["Agent EVP proxy\n/evp_proxy/v2/api/v2/flagevaluation"]
```

### Payload Limit Handling

```mermaid
flowchart TD
  Events["Aggregated flagEvaluations"] --> Encode["Encode each event with compact JSON"]
  Encode --> FitsEvent{"Single event fits\ninside shared EVP limit?"}
  FitsEvent -- "yes" --> Batch["Add to current payload"]
  FitsEvent -- "no" --> Degrade["Drop targeting_key and context\nfor that event only"]
  Degrade --> FitsDegraded{"Degraded event fits?"}
  FitsDegraded -- "yes" --> Batch
  FitsDegraded -- "no" --> Drop["Drop and count\npayload_limit"]
  Batch --> FitsPayload{"Payload still <=\nDEFAULT_EVP_PAYLOAD_SIZE_LIMIT?"}
  FitsPayload -- "yes" --> Continue["Continue batching"]
  FitsPayload -- "no" --> Split["Close current payload\nstart next payload"]
  Split --> Continue
  Continue --> Post["POST each payload sequentially"]
```

## Decisions

- Keep this as a dual-path implementation: the EVP logging writer is additive and separately gated, while the existing OTel metric hook remains registered when the provider is enabled.
- Use the existing Agent EVP proxy transport and the canonical `flagevaluation` backend route instead of adding a Python-specific transport or schema.
- Keep the evaluation logging hook non-blocking on application evaluation threads: queueing is best-effort, bounded, and drop-counted under backpressure.
- Do aggregation, JSON serialization, payload splitting, and POSTs only in the background writer path.
- Split `flagEvaluations` payloads by encoded byte size using `DEFAULT_EVP_PAYLOAD_SIZE_LIMIT`; send split payloads sequentially from the writer thread.
- Use a two-tier aggregation model: full buckets retain `targeting_key` and pruned context; degraded buckets retain only lower-cardinality schema dimensions.
- Store `targeting_key` in the dedicated EVP field and remove `targetingKey` / `targeting_key` from context before building context keys or payloads.
- Do not emit OpenFeature `reason` because it is not accepted by the canonical worker schema, and do not populate `targeting_rule.key` until Python exposes real targeting-rule metadata.
- Keep `openfeature-sdk` as an application/test/lint dependency rather than adding it as a runtime dependency of `ddtrace`; the dogfooding app dependency floor is being tightened separately in https://github.com/DataDog/ffe-dogfooding/pull/88.

## Validation Evidence

### Dogfooding App

- `ffe-dogfooding` `app-python` was run with local `dd-trace-py` artifacts and reached `PROVIDER_READY`.
- The running dogfooding app reported `openfeature-sdk=0.9.0`.
- Evaluated `ffe-dogfooding-string-flag` through the Python dogfooding app with 5 evaluations per targeting key over one persistent connection:
  - `python-batch-keepalive-20260623T004034Z-alpha`
  - `python-batch-keepalive-20260623T004034Z-bravo`
  - `python-batch-keepalive-20260623T004034Z-charlie`
- App-side result: all evaluations returned `variant_2` with allocation `allocation-override-392dd7c149f8`.

### System Tests

- Companion draft PR: DataDog/system-tests#7183

### Staging End-To-End

- Dogfooding ran without the local mock-intake EVP tee/proxy, so the Agent sent EVP traffic through the normal backend path.
- Retriever staging query returned 3 backend `flagevaluation` rows for the exact targeting keys above.
- Each targeting key returned one row with `evaluation_count=5`:
  - `python-batch-keepalive-20260623T004034Z-alpha`
  - `python-batch-keepalive-20260623T004034Z-bravo`
  - `python-batch-keepalive-20260623T004034Z-charlie`
- All rows had `flag.key=ffe-dogfooding-string-flag`, `variant.key=variant_2`, and `allocation.key=allocation-override-392dd7c149f8`.



Co-authored-by: leo.romanovsky <leo.romanovsky@datadoghq.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant