Skip to content

FFLSDK-254: Add a first-flags callback to the Android Flags client - #3945

Open
typotter wants to merge 24 commits into
developfrom
typo/android-flags-event-shape
Open

typotter wants to merge 24 commits into
developfrom
typo/android-flags-event-shape

Conversation

@typotter

@typotter typotter commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

What changed

Adds FlagsClient.onFirstFlags(listener): FlagsSubscription so applications can register on an existing client and learn when its first flags are installed from disk or the network, including before the first call to setEvaluationContext.

Each registration receives one retained FlagsClientEvent with type = CONFIGURATION_CHANGED and flagsChanged, all keys from that first accepted configuration, not all flags defined on the server. A valid empty configuration delivers an empty key list. Cache misses, invalid cache and failed fetches do not complete the signal. Late registrations receive the same retained event immediately from memory, without SDK I/O. The event retains the first keys, not an assignment snapshot: evaluations always read the client's current flags.

The Kotlin example app registers on its constructed client before creating its OpenFeature provider:

import com.datadog.android.flags.FlagsClient
import com.datadog.android.flags.openfeature.asOpenFeatureProvider
import timber.log.Timber

val flagsClient = FlagsClient.Builder().build()
// Application-lifetime callback; lifecycle-scoped callers retain the subscription and call unsubscribe().
flagsClient.onFirstFlags { event ->
    logAndEvaluateFirstFlags(flagsClient, event) { message -> Timber.i(message) }
}
val provider = flagsClient.asOpenFeatureProvider()

The sample helper logs the supplied keys and evaluates my-flag-key directly with a false default:

log("Installed flag keys: ${event.flagsChanged ?: "<absent>"}")
val details = client.resolve("my-flag-key", false)
log("my-flag-key = ${details.value} (reason=${details.reason})")

For shorter-lived owners, retain the returned FlagsSubscription and call unsubscribe() during cleanup. Cancellation is thread-safe, idempotent and local to that registration. It removes pending captures; an already-claimed callback may still run. It does not interrupt callbacks or undo synchronous replay.

Pending callbacks run on a dedicated background worker. Available results replay synchronously on the registering thread before onFirstFlags returns, so a later registration can run before an earlier queued one. Callbacks run outside internal locks. Callback Exceptions are logged and isolated; Errors are not caught. Dispatch UI work to the main thread.

Registration is directly on the public FlagsClient interface. Custom implementations implement this method, and Kotlin interface delegation forwards it. The event listener and subscription remain named functional interfaces; no general event bus is introduced. Events are constructed internally by the SDK; no public event constructor or builder is exposed.

Malformed network responses also change behavior for callers that do not subscribe. Previously, parse failure installed an empty configuration and completed the context update successfully. It now follows the failed-fetch path: installed flags and their context are retained, and the update reports failure. Valid empty responses still install successfully. This prevents malformed input from consuming the first-flags signal; it does not introduce a new policy for retaining flags after failed context updates.

Structure and signal flow

Before — existing installation flow

flowchart TB
    C["DatadogFlagsClient"]
    E["EvaluationsManager"]
    N["PrecomputedAssignmentsDownloader<br/>via PrecomputedAssignmentsReader"]
    R["DefaultFlagsRepository<br/>implements FlagsRepository<br/>Atomic flags and context"]
    P["FlagsPersistenceManager"]
    D["DataStoreHandler"]
    C -->|"setEvaluationContext"| E
    C -->|"Read current flags and context"| R
    E -->|"Fetch"| N
    N -->|"Response parsed by PrecomputeMapper"| E
    E -->|"setFlagsAndContext"| R
    R -->|"Construct and save network flags"| P
    P -->|"Load callback: install only if empty"| R
    P -->|"Read and write"| D
    D -->|"Read and write completion"| P
Loading

After — direct registration and first-flags delivery

flowchart TB
    A["Application"]
    C["DatadogFlagsClient<br/>implements FlagsClient.onFirstFlags<br/>Retains first FlagsClientEvent"]
    E["EvaluationsManager"]
    N["PrecomputedAssignmentsDownloader<br/>via PrecomputedAssignmentsReader"]
    R["DefaultFlagsRepository<br/>implements FlagsRepository<br/>Current flags and context"]
    P["FlagsPersistenceManager"]
    D["DataStoreHandler"]
    L["FirstFlagsLatch<br/>Retains first installed keys<br/>Owns pending registrations"]
    W["One-shot ExecutorService<br/>flags-first-flags"]
    A -->|"onFirstFlags(listener)"| C
    C -->|"Return FlagsSubscription"| A
    C -->|"firstFlags.whenComplete"| L
    A -.->|"unsubscribe: clear this pending registration"| L
    C -->|"setEvaluationContext"| E
    C -->|"Evaluate current flags"| R
    E -->|"Fetch"| N
    N -->|"Response parsed by PrecomputeMapper"| E
    E -->|"setFlagsAndContext(context, flags, onInstalled)"| R
    R -->|"Submit save after network installation"| P
    P -->|"Cache load: compare-and-set only if empty"| R
    P -->|"Read and write"| D
    D -->|"Read and write completion"| P
    R -.->|"Network: onInstalled settles bookkeeping and context callback"| E
    R -.->|"Complete after first accepted install and network hook"| L
    L -.->|"Pending batch"| W
    W -.->|"Claim listener and deliver keys outside lock"| C
    L -.->|"Available result: synchronous replay on caller"| C
    C -.->|"Build or reuse first event; invoke listener outside lock"| A
Loading

The repository continues to own persistence and its separate persistence-load barrier. A cached configuration completes the first-flags latch only if its compare-and-set installation succeeds. For a first network installation, the repository submits persistence, runs onInstalled to settle initialization bookkeeping and context completion, then completes the latch in finally. Disk write completion is not awaited, and ordinary storage-submission exceptions are logged without aborting an accepted installation.

The latch keeps the first installed keys even when the repository later replaces its current flags. The client constructs and retains one event from those keys. Pending delivery uses a one-shot worker; late replay uses the caller thread. Cancellation can suppress a queued listener until it is claimed for delivery.

Why

Cached flags can be available before network initialization finishes. Applications need a reliable signal that flags have been installed so they can evaluate them directly. Registering on an existing client avoids constructor callback timing problems, and retaining the first result prevents fast disk or network completion from being missed.

This notification is independent of readiness and does not change OpenFeature evaluation gates or evaluation reasons. Tracks FFLSDK-254.

The manager-level regression for successful installation after initialization timeout remains included. Client integration coverage is deferred to reliability/single-fit or RUM FIT/FLEX, including Flags module support as needed.

@linear-code

linear-code Bot commented Oct 1, 2026

Copy link
Copy Markdown

FFLSDK-254

@datadog-datadog-prod-us1

datadog-datadog-prod-us1 Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

Tests

✅ All CI checks and tests passed. Datadog automation helped this PR pass.

🎉 All green!

🧪 All tests passed
❄️ No new flaky tests detected

🔄 Datadog retried 1 test - 1 passed on retry View in Datadog

🎯 Code Coverage (details)
• Patch Coverage: 70.43%
• Overall Coverage: 71.70% (-0.05%)

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: 0da02ff | Docs | View more details | Give us feedback!

@typotter
typotter force-pushed the typo/android-flags-event-shape branch from a08f580 to 44e4e3e Compare October 2, 2026 15:13
@typotter
typotter requested a balanced review from Copilot October 2, 2026 16:37

Copilot AI 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.

Copilot review overview

🟡 Changes recommended

Cache misses and failures now cause repeated synchronous 100 ms read delays until flags are successfully installed.

Review effort: Balanced
Findings: 1 Medium severity

Open (1)
What changed in this PR

Adds a one-shot Android Flags callback for the first installed cache or network configuration, independent of client readiness.

Changes:

  • Adds immutable events and FlagsClient.onFirstFlags().
  • Retains first-installation keys for late registrations.
  • Adds tests, documentation, sample usage, and Detekt configuration.
File Description
sample/​kotlin/​src/​test/​kotlin/​com/​datadog/​android/​sample/​flags/​FirstFlagsSampleTest.kt Tests sample logging and evaluation.
sample/​kotlin/​src/​main/​kotlin/​com/​datadog/​android/​sample/​SampleApplication.kt Registers the sample callback.
sample/​kotlin/​src/​main/​kotlin/​com/​datadog/​android/​sample/​flags/​OpenFeatureFragment.kt Saves a Boolean flag selection.
sample/​kotlin/​src/​main/​kotlin/​com/​datadog/​android/​sample/​flags/​FirstFlagsSample.kt Logs keys and evaluates the saved flag.
sample/​kotlin/​build.gradle.kts Adds sample test dependencies.
features/​dd-sdk-android-flags/​src/​test/​kotlin/​com/​datadog/​android/​flags/​model/​FlagsClientEventTest.kt Tests event immutability and shape.
features/​dd-sdk-android-flags/​src/​test/​kotlin/​com/​datadog/​android/​flags/​internal/​repository/​FirstFlagsLatchTest.kt Tests latch delivery and concurrency.
features/​dd-sdk-android-flags/​src/​test/​kotlin/​com/​datadog/​android/​flags/​internal/​repository/​FirstFlagsInstallationTest.kt Tests cache/network installation ordering.
features/​dd-sdk-android-flags/​src/​test/​kotlin/​com/​datadog/​android/​flags/​FirstFlagsIntegrationTest.kt Tests real-client callback behavior.
features/​dd-sdk-android-flags/​src/​main/​kotlin/​com/​datadog/​android/​flags/​model/​FlagsClientEventType.kt Defines the configuration event type.
features/​dd-sdk-android-flags/​src/​main/​kotlin/​com/​datadog/​android/​flags/​model/​FlagsClientEvent.kt Adds immutable event values.
features/​dd-sdk-android-flags/​src/​main/​kotlin/​com/​datadog/​android/​flags/​internal/​repository/​FlagsRepository.kt Exposes the installation latch internally.
features/​dd-sdk-android-flags/​src/​main/​kotlin/​com/​datadog/​android/​flags/​internal/​repository/​FirstFlagsLatch.kt Retains first keys and delivers listeners.
features/​dd-sdk-android-flags/​src/​main/​kotlin/​com/​datadog/​android/​flags/​internal/​repository/​DefaultFlagsRepository.kt Signals first installation and changes read waiting.
features/​dd-sdk-android-flags/​src/​main/​kotlin/​com/​datadog/​android/​flags/​internal/​NoOpFlagsClient.kt Handles callback registration without installations.
features/​dd-sdk-android-flags/​src/​main/​kotlin/​com/​datadog/​android/​flags/​internal/​DatadogFlagsClient.kt Delivers retained events and isolates exceptions.
features/​dd-sdk-android-flags/​src/​main/​kotlin/​com/​datadog/​android/​flags/​FlagsClient.kt Declares and documents the callback API.
features/​dd-sdk-android-flags/​README.md Documents events and sample usage.
features/​dd-sdk-android-flags/​api/​dd-sdk-android-flags.api Records binary API additions.
features/​dd-sdk-android-flags/​api/​apiSurface Records public API additions.
detekt_custom_safe_calls_third_party.yml Allows the atomic installation operation.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Copilot AI 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.

Copilot review overview

🟡 Changes recommended

A blocked first-flags handler can prevent initialization completion after its timeout has been canceled.

Review effort: Balanced
Findings: 1 High severity

Open (1)
Resolved since last review (1)

Copilot AI 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.

Copilot review overview

🟡 Changes recommended

Storage-submission exceptions bypass initialization bookkeeping, allowing an older timeout to overwrite a newer context’s state.

Review effort: Balanced
Findings: 2 High severity

Open (2)

Copilot AI 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.

Copilot review overview

🟢 Approval recommended

No blocking issues were identified; only a non-blocking documentation clarification remains.

Review effort: Balanced
Findings: 1 Low severity

Open (1)
Resolved since last review (2)

@typotter
typotter marked this pull request as ready for review October 2, 2026 18:28
@typotter
typotter requested review from a team as code owners October 2, 2026 18:28
@typotter
typotter requested review from btthomas and vjfridge and removed request for a team October 2, 2026 18:28
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-06T17:49:28.486302Z 0da02ff New commits
🔒 Security Review ✅ Completed 2026-10-06T17:49:36.095913Z 0da02ff New commits
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@typotter
typotter marked this pull request as draft October 2, 2026 18:29
@typotter
typotter removed request for a team, btthomas and vjfridge October 2, 2026 18:29
@typotter
typotter marked this pull request as ready for review October 2, 2026 18:30
@typotter

typotter commented Oct 5, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the review, Sameeran. will follow up with additional tests.

@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: 0771ff9298

ℹ️ 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".

@typotter
typotter requested review from aarsilv and sameerank October 6, 2026 15:52
sameerank
sameerank previously approved these changes Oct 6, 2026

@sameerank sameerank left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

After this gets merged: #3961 still uses client.events.onFirstFlags. It needs updating to client.onFirstFlags

Comment thread features/dd-sdk-android-flags/README.md
Comment thread features/dd-sdk-android-flags/README.md
Comment thread features/dd-sdk-android-flags/README.md Outdated
Comment thread features/dd-sdk-android-flags/README.md Outdated
Comment thread features/dd-sdk-android-flags/api/apiSurface Outdated
Comment thread sample/kotlin/src/main/kotlin/com/datadog/android/sample/SampleApplication.kt Outdated
@typotter
typotter requested a review from 0xnm October 6, 2026 18:13

@aarsilv aarsilv 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.

Thanks for iterating! Deleting FirstFlagsIntegrationTest leaves the real client's onFirstFlags() untested, but this looks by design as you want to move it elsewhere 👌

@leoromanovsky leoromanovsky left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Looks to spec

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants