Skip to content

fix(flags): dedupe exposures by latest assignment#3526

Open
leoromanovsky wants to merge 4 commits into
developfrom
leo.romanovsky/android-flags-exposure-cache-cycle
Open

fix(flags): dedupe exposures by latest assignment#3526
leoromanovsky wants to merge 4 commits into
developfrom
leo.romanovsky/android-flags-exposure-cache-cycle

Conversation

@leoromanovsky

@leoromanovsky leoromanovsky commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Motivation

Android flags exposure deduplication currently treats every previously seen targetingKey + flagName + allocationKey + variationKey tuple as permanently sent. That suppresses valid exposure events when the same subject and flag cycles assignments, for example A -> B -> A, because the final A was seen earlier.

Related iOS PR: DataDog/dd-sdk-ios#2987 applies the same mobile exposure-cache contract.

Changes

  • Cache exposures by targetingKey + flagName.
  • Store the latest allocationKey + variationKey as the cache value.
  • Emit an exposure when the subject/flag has no cached value or when the latest assignment differs from the cached value.
  • Keep a bounded Android LruCache, but make it entry-count based with a 5,000-entry limit instead of using approximate object-size accounting.
  • Add a comment documenting the expected high-water mark of two subjects and 2,500 flags each; normal flag keys are typically tens of characters, so count-based sizing is easier to reason about.
  • Add a regression test proving A -> B -> A writes three exposure events.
  • Update the existing mixed scenario to expect a cycle-back exposure.

Decisions

  • Kept the change internal to the processor with no public API changes.
  • Preserved an LRU bound for apps with many flags moving through a client process, while sizing it directly around the expected two-subject, 2,500-flag high-water mark.
  • Avoided approximate byte accounting in sizeOf; AndroidX LruCache defaults to one unit per entry when sizeOf is not overridden.
  • Matched the iOS PR's count-based mobile policy while using Android's platform cache primitive.
  • Kept synchronization around cache check/update so duplicate suppression remains atomic.
  • Validation: focused regression test failed before the implementation, then passed after the fix. JAVA_HOME=/opt/homebrew/Cellar/openjdk@17/17.0.19/libexec/openjdk.jdk/Contents/Home ANDROID_HOME=/opt/homebrew/share/android-commandlinetools ./gradlew :features:dd-sdk-android-flags:testDebugUnitTest passed.

@leoromanovsky leoromanovsky force-pushed the leo.romanovsky/android-flags-exposure-cache-cycle branch from 9daf3ee to 695e6ef Compare June 10, 2026 18:36
@codecov-commenter

codecov-commenter commented Jun 10, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 72.19%. Comparing base (8b031cb) to head (416ee2d).

Additional details and impacted files
@@             Coverage Diff             @@
##           develop    #3526      +/-   ##
===========================================
- Coverage    72.22%   72.19%   -0.03%     
===========================================
  Files          965      965              
  Lines        35635    35630       -5     
  Branches      5947     5947              
===========================================
- Hits         25736    25723      -13     
- Misses        8282     8289       +7     
- Partials      1617     1618       +1     
Files with missing lines Coverage Δ
.../android/flags/internal/ExposureEventsProcessor.kt 100.00% <100.00%> (ø)

... and 36 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@datadog-datadog-prod-us1

datadog-datadog-prod-us1 Bot commented Jun 10, 2026

Copy link
Copy Markdown

Pipelines

Fix all issues with BitsAI

⚠️ Warnings

🚦 1 Pipeline job failed

DataDog/dd-sdk-android | analysis:ktlint   View in Datadog   GitLab

Useful? React with 👍 / 👎

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

@leoromanovsky leoromanovsky marked this pull request as ready for review June 10, 2026 20:31
@leoromanovsky leoromanovsky requested review from a team as code owners June 10, 2026 20:31
@dd-octo-sts-6cbbf8

dd-octo-sts-6cbbf8 Bot commented Jun 10, 2026

Copy link
Copy Markdown

🐑 PR Shepherd is maintaining this PR

I watch your PR and automatically fix CI failures, rebase your branch, handle flaky tests, and push it to the merge queue when it's ready.

More about what I do → Guide

To pause me on this PR, add the flow-skip label.

@leoromanovsky leoromanovsky requested a review from typotter June 10, 2026 20:31

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bed9e6ad17

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

val lastSentValue = exposuresSentCache[cacheKey]
if (lastSentValue != cacheValue) {
@Suppress("UnsafeThirdPartyFunctionCall") // safe - non-null key and value
exposuresSentCache.put(cacheKey, cacheValue)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep cache updates ordered with exposure writes

When the same subject/flag is evaluated concurrently with different assignments, this cache update can happen before the previous exposure is actually written. For example, thread A caches A and pauses, thread B caches and writes B, then thread A writes A; the cache still says B, so the next B exposure is suppressed even though the latest emitted assignment was A. The new latest-assignment contract only holds if updating the cache and emitting the exposure remain ordered for a given cache key.

Useful? React with 👍 / 👎.

@sbarrio sbarrio requested a review from hamorillo June 19, 2026 12:58
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.

3 participants