feat(openfeature): parse observeFullEvaluationData and hash targeting_key in flagevaluations events - #5151
Conversation
…_key in flagevaluations events
Make the no-PII path the default for server-side EVP flagevaluation events,
driven by the new top-level UFC boolean observeFullEvaluationData.
Without consent, targeting_key is emitted as an unsalted SHA-256 fingerprint
prefixed with "sha256_" (71 chars) and context.evaluation is omitted entirely
(absent key, not null, not {}). With consent, both are emitted verbatim. Absent
or null consent fails closed. The digest is taken over the raw UTF-8 bytes with
no trim, case folding, or Unicode normalization so digests join across SDKs.
Consent is snapshotted in the evaluator from the exact configuration the
evaluation ran against and stamped onto evaluation metadata under the
cross-SDK key observe_full_evaluation_data. Nothing downstream of the
evaluator reads live configuration, so a Remote Config update landing between
evaluation and flush cannot retroactively apply another environment's policy.
Consent is part of the full-tier bucket key so mixed-consent evaluations never
merge, with an AND-fold on the bucket's consent field as defense in depth. When
consent is off the context is dropped from the key and never captured, because
the key must carry exactly the dimensions that survive serialization; keying on
a discarded context would burn the per-flag bucket cap on precisely the
privacy-protected traffic. Consent is deliberately not a degraded-tier
dimension: that tier emits neither targeting_key nor context, so it would add
cardinality with no privacy gain.
DoLog does not gate this behavior, and the
DD_FLAGGING_EVALUATION_COUNTS_ENABLED kill switch still disables the track
entirely.
Environment: Datadog workspace
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
🎉 All green!🧪 All tests passed 🎯 Code Coverage (details) 🔗 Commit SHA: 1346e7e | Docs | Datadog PR Page | Give us feedback! |
System-tests results against this PRRan the To exercise the new behavior end-to-end, I locally removed the following markers from
Results: 26 passed, 2 skipped, 0 failed
Follow-upA companion system-tests PR should remove the four manifest lines above to enable these tests in CI for Go. |
BenchmarksBenchmark execution time: 2026-08-05 22:03:49 Comparing candidate commit 1346e7e in PR branch Found 0 performance improvements and 0 performance regressions! Performance is the same for 326 metrics, 0 unstable metrics, 1 flaky benchmarks without significant changes.
|
L2 dogfooding — both consent paths verified end-to-end ✅Ran this branch against ffe-dogfooding with the staging org (
The 5 "full" entries under the consent-off flip are the last flush cycle that occurred before RC delivered the new UFC — everything after the RC refresh is protected. Restored the environment to Behaviors observed exactly matching the PR's contract table:
ReproduceDD_TRACE_GO_PATH=/path/to/dd-trace-go dd-auth --domain dd.datad0g.com -- env DD_SITE=datad0g.com \
docker compose -f docker-compose.yml -f local/docker-compose.go.yml up -d --build
# wait ~60s for first flush
curl -s "http://localhost:8090/logs?service=ffe-dogfooding-go&limit=1" | jq .
# open http://localhost:8080 for the dashboardTo flip consent (requires app+api keys with # fetch current, then PUT with observe_full_evaluation_data flipped
curl -s -H "DD-API-KEY: $DD_API_KEY" -H "DD-APPLICATION-KEY: $DD_APP_KEY" \
"https://api.datad0g.com/api/v2/feature-flags/environments/<env-id>"Note: this touches the shared "Staging" environment on L3 system-tests still owed per the PR's Validation status section. L1 already covers the hashed vector exhaustively ( |
Condense the added rationale comments to <=3 lines while preserving the load-bearing "why": consent snapshot must travel with the evaluation, AND-fold defense against key drift, empty-targeting-key must not hash, shadow struct is sole populator, fail-closed default. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Motivation
A server SDK upgrade must not silently ship subject PII (emails, account IDs, evaluation context) to the
flagevaluationstrack. The product metric — unique-subject counts per(flag, allocation)— needs a stable per-subject identifier, not the raw subject. This PR sends a one-way SHA-256 fingerprint by default and makes full-fidelity collection an explicit per-environment opt-in.Tracked by FFL-2962, under the server-SDK fan-out FFL-2784. The Java pilot is dd-trace-java#12042. The base track for Go was #4886. Cross-SDK contract tests were merged in system-tests#7316.
What does this PR do?
This PR makes the no-PII path the default for server-side
flagevaluationevents. A new UFC booleanobserveFullEvaluationDataselects the path.observeFullEvaluationDatatargeting_keyon the wirecontext.evaluationon the wiretruefalseor absent (default)sha256_+ 64-char lowercase hex (71 chars total)User impact:
flagevaluationstrack unless the environment opts in.(flag, allocation)stay accurate. Every SDK produces a byte-identical digest for the same subject, so hashed values join across languages.targeting_keyas the raw subject must accept thesha256_prefix on the default path.DD_FLAGGING_EVALUATION_COUNTS_ENABLEDkill switch is unchanged. It still disables the track entirely.Key design points:
observeFullEvaluationDatasits at the root of the UFC, a sibling ofenvironment. The customUnmarshalJSONcopies only the fields its shadow struct lists, so the field is added to both the struct and the shadow struct. Absent and explicitnullboth fail closed tofalse.observe_full_evaluation_data. Nothing downstream of the evaluator reads live configuration, so a Remote Config update between evaluation and flush cannot retroactively apply another environment's policy. The Java pilot hit this bug. A regression test guards both directions.No new configuration keys, no dependency changes, no generated files affected.
QA
L1 — Unit tests: done.
L2 — Dogfooding: done. Ran this branch against ffe-dogfooding with the staging org (
dd.datad0g.com). Tracer built asv2.11.0-dev.1.observeFullEvaluationDatatargeting_key(wire)context.evaluationpii_classtrue(default staging state)user-xxx)fullfalse(flipped for ~2 min)sha256_<64 hex>(71 chars)protectedThe 5 "full" entries under the consent-off flip are the last flush cycle before Remote Config delivered the new UFC. Every event after the RC refresh is protected. The staging environment was restored to
trueafter verification.L3 — System-tests: done locally, still owed in CI. Ran the
FEATURE_FLAGGING_AND_EXPERIMENTATIONscenario from DataDog/system-tests locally against this branch (golang@2.11.0-dev.1, weblog variantnet-http).Results: 26 passed, 2 skipped, 0 failed.
tests/ffe/test_flag_eval_evp.py: 11/11 passed, including the three tests unlocked by this PR andTest_FFE_EVP_Flagevaluation_Degradation(previously flaky, now clean).tests/ffe/test_flag_eval_metrics.py: 15 passed, 2 skipped (pre-existingirrelevantmarkers, unrelated to this PR).A companion
system-testsPR must remove the four manifest lines above to enable these tests in CI for Go.How to reproduce the L2 verification yourself
Run the tracer against ffe-dogfooding with the staging org:
Flip consent (requires app and API keys with
feature_flag_environment_config_write):The flip touches the shared
Stagingenvironment ondd.datad0g.com. Anything else running underDD_ENV=stagingpicks up the change too. Flip back promptly.Reviewer's Checklist
va.b.c-devversion tag. Tests already exist from system-tests#7316. The manifest activation is a companion PR. See "QA" above.BenchmarkFlagEvaluationOTelPlusEVPParallelstill covers the aggregation path.make lintlocally.make testlocally.make generatelocally. No generated files or configuration keys touched.go.modchanges: none.crypto/sha256andencoding/hexare stdlib.🤖 Generated with Claude Code