Parse observeFullEvaluationData and hash targeting_key in flagevaluations events#12042
Conversation
…ions events Adds the top-level observeFullEvaluationData boolean to the UFC model, plumbs it through to the EVP flagevaluation event serializer, and gates PII handling on it: when the flag is absent/false the targeting key is SHA-256 hashed (sha256_<hex>) and the raw evaluation context is omitted from the wire; when true the raw targeting key and context are emitted. Environment: Datadog workspace Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This comment has been minimized.
This comment has been minimized.
Replace the inline "sha256_" literal with a documented HASHED_TARGETING_KEY_PREFIX constant describing the cross-SDK wire contract for privacy-preserving hashed targeting keys. Environment: Datadog workspace Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Parameterize the true/false config-parsing assertions with @valuesource and add a test locking in the fail-closed behaviour for an explicit JSON null: malformed config is rejected so full evaluation data is never observed off the back of it. Environment: Datadog workspace Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The flush-time read of FeatureFlaggingGateway.isObserveFullEvaluationDataEnabled() was a TOCTOU bug: CURRENT_CONFIG could be overwritten by a later RC update between when an evaluation happened and when the batch flushed, so events could be emitted under the wrong environment's consent (the system test observed a targeting key hashed even though the active UFC said observeFullEvaluationData=true). Capture consent when the evaluation is folded into its EvalBucket instead. On merge the value is folded with AND, so any no-consent evaluation in a bucket's lifetime sinks the whole bucket to hashed/omitted (fail-closed). buildEventList now reads bucket.observeFullEvaluationData rather than the gateway. The gateway accessor is retained; it is read at aggregation time. Adds a writer-level regression guard (a bucket aggregated under consent-off stays hashed even if the gateway later reports consent-on) plus aggregator fold tests, and an end-to-end parse->dispatch->flush test. Environment: Datadog workspace Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
| Test | Result |
|---|---|
Test_FFE_EVP_Flagevaluation_ObserveFullData_Absent_Hashed |
✅ PASS |
Test_FFE_EVP_Flagevaluation_ObserveFullData_False_Hashed |
✅ PASS |
Test_FFE_EVP_Flagevaluation_ObserveFullData_True_Unhashed |
✅ PASS |
36 passed, 8 skipped, 2704 deselected, 1 xfailed, 3 xpassed in 301.86s
Weblog: spring-boot | Scenario: FEATURE_FLAGGING_AND_EXPERIMENTATION
Note: an earlier build (d65c79f266) of this PR failed True_Unhashed — the SDK was always hashing regardless of the flag value. Root cause: FlagEvaluationWriterImpl.buildEventList() read observeFullEvaluationData from CURRENT_CONFIG at flush time, which could be overwritten by a subsequent RC update before the 10s flush fired. The fix in this PR (capturing the value per-bucket at enqueue time with a privacy-preserving fold) resolves it.
|
What & why
Feature-flag evaluations are reported to Datadog so users can see how their flags behaved in production. Today, every evaluation carries the raw targeting key (the identifier of the subject being evaluated; often a user email or user ID) and the full evaluation context. Those fields can contain personally identifiable information (PII).
This PR lets the server decide, per environment, whether that raw data may be observed via a new boolean on the UFC the SDK already downloads. It is the Java piece of the cross-SDK "Protecting PII in flagevaluations" initiative.
dd-trace-javais the pilot SDK; the same pattern will be copied to the other SDKs afterward.This directly resolves the privacy concern raised on the parent PR #11639, where @AlexeyKuznetsov-DD helpfully flagged that flag-evaluation reporting was default-on and shipped the raw
targeting_key(user IDs / emails) plus the full evaluation context in clear text, and asked for hashing / opt-in / an explicit privacy sign-off. The no-PII path is now the default.Behavior, applied only when reporting flag-evaluations is already enabled (the existing kill switch is untouched):
observeFullEvaluationDatatruefalseor absent (privacy-preserving default)sha256_<hex>"Absent" is treated exactly like
false, so an older/cached config can never accidentally leak raw values.How it works
observeFullEvaluationDataon the downloaded flag-configuration (UFC) model (top-level boolean, defaults tofalsewhen missing).CURRENT_CONFIGcan be overwritten by a later Remote Config update between when an evaluation happened and when the batch flushes. Reading the gateway at flush would retroactively apply a different environment's consent to already-collected evaluations. (This was a real bug caught by system-tests: a targeting key came back hashed even though the active UFC saidtrue.)sha256_; the context field is dropped so it's absent from the JSON (notnull, not{}).Unsalted hash
The hash is unsalted SHA-256 over the raw UTF-8 bytes (no trimming/case/Unicode normalization), emitted as lowercase hex. Unsalted is a deliberate cross-SDK contract requirement: every SDK must produce the identical digest for a given targeting key so hashed values line up across languages and against the backend. It matches the canonical test vector shared across all SDKs:
Performance
Consent is read once per aggregated evaluation (a single
AtomicReference.getplus a boolean AND on merge) on the aggregation path — not per event on the wire and not in any per-span hot path. Hashing runs at flush cadence, bounded by the number of distinct evaluation buckets, and uses aThreadLocal<MessageDigest>(no per-callMessageDigest.getInstance).Malformed config is fail-closed
An absent field defaults to
false. An explicitnullfor the field is malformed input that Moshi rejects; the whole config update is then dropped by the callers (AgentlessConfigurationSource/ the remote-config poller both swallow the failure and keep the last-known-good config), and the gateway defaults to the privacy-preserving behavior when no valid config has been dispatched. In other words, malformed config can never flip the gate on. This is covered by a test.Testing
true,false(as a@ValueSourcematrix), absent (defaults tofalse), and explicitnull(rejected / fail-closed).🤖 Generated with Claude Code