Skip to content

feat: Graceful mode and error logging policies#2963

Merged
dd-mergequeue[bot] merged 6 commits into
feature/feature-flaggingfrom
typo/FFL-1187-graceful-mode
Oct 29, 2025
Merged

feat: Graceful mode and error logging policies#2963
dd-mergequeue[bot] merged 6 commits into
feature/feature-flaggingfrom
typo/FFL-1187-graceful-mode

Conversation

@typotter

Copy link
Copy Markdown
Contributor

What does this PR do?

Implements graceful mode error handling for the Flags SDK, providing three-tier policy-based error handling (Graceful/Error/Strict) that adapts behavior based on build type and configuration.

Key changes:

  • Added gracefulModeEnabled configuration parameter (defaults to true)
  • Implemented debug build detection using ApplicationInfo.FLAG_DEBUGGABLE
  • Added policy-aware error logging in FlagsFeature.logErrorWithPolicy()
  • Refactored NoOpFlagsClient to use policy-based logging functions
  • Updated client creation and retrieval to crash in strict mode for misuse scenarios

Policy behavior:

  • Graceful Policy (Release builds): Conditional logging through SDK logger
  • Error Policy (Debug + graceful enabled): Always log to System.err, non-crashing
  • Strict Policy (Debug + graceful disabled): Crash immediately on misuse with
    IllegalStateException

Motivation

The Flags SDK needs configurable error handling to balance developer experience across
different environments:

  • Development/QA: Fail fast with crashes to catch configuration errors early
  • Staging/Dogfooding: Loud warnings without crashes for visibility without disruption
  • Production: Silent fallbacks for maximum stability

This aligns with OpenFeature error handling patterns and provides flexibility for teams
with different development workflows (small teams vs. large modular apps, monoliths vs.
DI-based architectures).

Related: FFL-1187

Additional Notes

Implementation highlights:

  1. Debug Detection: Uses ApplicationInfo.FLAG_DEBUGGABLE (same pattern as
    DatadogCore.isAppDebuggable()) with performance caching via @Volatile field primed
    during onInitialize()

  2. Thread Safety:

    • @Volatile fields for appContext, debugBuildCache, and initialized flag
    • Double-check locking pattern in getOrRegisterNewClient()
    • Proper lifecycle management with cleanup in onStop()
  3. Crash Points (Strict Mode Only):

    • Duplicate client creation: FlagsFeature.getOrRegisterNewClient()
    • Client not found: FlagsClient.get()
    • Uses Kotlin's error() function to throw IllegalStateException
  4. Safe Fallback: When FlagsFeature not enabled, always uses graceful behavior (can't determine policy without config)

Documentation:

  • Comprehensive KDoc with environment-specific guidance
  • Added @throws IllegalStateException documentation to build() and get() methods

Intentional deviations from spec:

  • Kept NoOpFlagsClient name instead of FallbackClient

Review checklist (to be filled by reviewers)

  • Feature or bugfix MUST have appropriate tests (unit, integration, e2e)
  • Make sure you discussed the feature or bugfix with the maintaining team in an Issue
  • Make sure each commit and the PR mention the Issue number (cf the CONTRIBUTING doc)

@typotter
typotter force-pushed the typo/FFL-1187-graceful-mode branch from 25de984 to b672f0f Compare October 22, 2025 23:10
@typotter
typotter marked this pull request as ready for review October 22, 2025 23:11
@typotter
typotter requested review from a team as code owners October 22, 2025 23:11
@codecov-commenter

codecov-commenter commented Oct 22, 2025

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 72.16495% with 27 lines in your changes missing coverage. Please review.
✅ Project coverage is 70.88%. Comparing base (412d3f3) to head (826ac08).

Files with missing lines Patch % Lines
...in/kotlin/com/datadog/android/flags/FlagsClient.kt 53.49% 19 Missing and 1 partial ⚠️
...com/datadog/android/flags/internal/FlagsFeature.kt 79.41% 5 Missing and 2 partials ⚠️
Additional details and impacted files
@@                     Coverage Diff                      @@
##           feature/feature-flagging    #2963      +/-   ##
============================================================
+ Coverage                     70.84%   70.88%   +0.04%     
============================================================
  Files                           840      840              
  Lines                         30559    30603      +44     
  Branches                       5169     5176       +7     
============================================================
+ Hits                          21649    21691      +42     
- Misses                         7446     7453       +7     
+ Partials                       1464     1459       -5     
Files with missing lines Coverage Δ
...in/com/datadog/android/flags/FlagsConfiguration.kt 92.31% <100.00%> (+1.83%) ⬆️
.../datadog/android/flags/internal/NoOpFlagsClient.kt 100.00% <100.00%> (+4.17%) ⬆️
...com/datadog/android/flags/internal/FlagsFeature.kt 83.33% <79.41%> (-5.04%) ⬇️
...in/kotlin/com/datadog/android/flags/FlagsClient.kt 35.29% <53.49%> (-0.28%) ⬇️

... and 40 files with indirect coverage changes

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

internal fun logErrorWithPolicy(
message: String,
level: InternalLogger.Level = InternalLogger.Level.ERROR,
shouldCrashInStrict: Boolean = false

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is a controversial approach, because customers may choose strict mode by mistake in the release builds and get a crash in production.

Are we doing the same in iOS SDK?

@typotter typotter Oct 23, 2025

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

strict mode cannot be enabled in release builds - the graceful mode option is overridden by the build type below. When !isDebugBuild, we log with the internal logger only

@typotter
typotter force-pushed the typo/FFL-1187-graceful-mode branch from e0b4add to 304213a Compare October 23, 2025 14:20

@typotter typotter left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks for the review. Confirming with @ncreated on the matter of System.err

ptal

{ "Missing required context parameters: $missingParams" }
)

val logWithPolicy: (String, InternalLogger.Level) -> Unit = { message, level ->

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

maybe it is worth to create typealias for (String, InternalLogger.Level) -> Unit

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

yes, agreed. helps to define the interaction.

@Test
fun `M throw IllegalArgumentException W Builder() {blank name}`() {
// When/Then
org.junit.jupiter.api.assertThrows<IllegalArgumentException> {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

this needs to go to import

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

done

verify(mockLogger).log(
eq(InternalLogger.Level.ERROR),
eq(InternalLogger.Target.USER),
any(),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It is better to verify the message as well, otherwise it can be an error for something else. Same for the test below.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

done

whenever(mockSdkCore.createSingleThreadExecutorService(any())) doReturn mockExecutorService

// Setup mockContext with default release build (flags = 0)
val applicationInfo = android.content.pm.ApplicationInfo().apply {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

also needs to go to import

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

done

)

// Then - uses android.util.Log.e which can't be easily verified in unit tests
// Just verify it doesn't crash and doesn't use internalLogger

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

you can also wrap logErrorWithPolicy above in assertNotThrows to explicitly highlight the intention.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

👍

* @param level The log level for conditional logging (graceful policy)
* @param shouldCrashInStrict If true, crashes in strict policy
*/
internal fun logErrorWithPolicy(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

this method should be moved below, after the override methods

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

done

Update release build tests to include '[Datadog Flags]' prefix in log message assertions.

The logErrorWithPolicy() method adds LOG_TAG as a prefix when logging through
internalLogger, so tests need to check for the full formatted message.

Fixed tests:
- M log through internalLogger W logErrorWithPolicy() { release build }
- M log through internalLogger W logErrorWithPolicy() { release build, gracefulModeEnabled false }

All 217 tests passing.
@typotter
typotter force-pushed the typo/FFL-1187-graceful-mode branch from cfe311d to 2e986bb Compare October 27, 2025 18:40

@typotter typotter left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks for the review.
Since the feature branch now has the package renaming (pr #2966), I ended up rebuilding the change on a fresh tree.
ptal

{ "Missing required context parameters: $missingParams" }
)

val logWithPolicy: (String, InternalLogger.Level) -> Unit = { message, level ->

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

yes, agreed. helps to define the interaction.

* @param level The log level for conditional logging (graceful policy)
* @param shouldCrashInStrict If true, crashes in strict policy
*/
internal fun logErrorWithPolicy(

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

done

@Test
fun `M throw IllegalArgumentException W Builder() {blank name}`() {
// When/Then
org.junit.jupiter.api.assertThrows<IllegalArgumentException> {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

done

verify(mockLogger).log(
eq(InternalLogger.Level.ERROR),
eq(InternalLogger.Target.USER),
any(),

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

done

whenever(mockSdkCore.createSingleThreadExecutorService(any())) doReturn mockExecutorService

// Setup mockContext with default release build (flags = 0)
val applicationInfo = android.content.pm.ApplicationInfo().apply {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

done

verify(mockInternalLogger).log(
eq(InternalLogger.Level.ERROR),
eq(InternalLogger.Target.USER),
any(),

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

done

@Test
fun `M log through internalLogger W logErrorWithPolicy() { release build, gracefulModeEnabled false }`() {
// Given - release build should ignore gracefulModeEnabled setting
val releaseAppInfo = android.content.pm.ApplicationInfo().apply { flags = 0 }

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

thanks!

)

// Then - uses android.util.Log.e which can't be easily verified in unit tests
// Just verify it doesn't crash and doesn't use internalLogger

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

👍

@typotter
typotter requested a review from 0xnm October 27, 2025 21:33
0xnm
0xnm previously approved these changes Oct 28, 2025
featureSdkCore: FeatureSdkCore,
flagsFeature: FlagsFeature
flagsFeature: FlagsFeature,
name: String = DEFAULT_CLIENT_NAME

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

is default argument needed here, given it is already there for public API?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

negative

)
} else {
// Create FlagsContext combining core SDK context with feature configuration
// Build a [DatadogFlagsClient] instance from its dependencies.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

this comment is wrong

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Comment is meant to apply to the whole block here.


@Test
fun `M log critical error W resolveBooleanValue()`(forge: Forge) {
fun `M log error W resolveBooleanValue()`(forge: Forge) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

although here and for other tests in this file it is not error, but warning

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

fixed.

Comment on lines -6 to -8

package com.datadog.android.flags.internal

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

seems like redundant edits

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

fixed

@typotter

Copy link
Copy Markdown
Contributor Author

/merge

@dd-devflow-routing-codex

dd-devflow-routing-codex Bot commented Oct 29, 2025

Copy link
Copy Markdown

View all feedbacks in Devflow UI.

2025-10-29 14:42:14 UTC ℹ️ Start processing command /merge


2025-10-29 14:42:19 UTC ℹ️ MergeQueue: pull request added to the queue

The expected merge time in feature/feature-flagging is approximately 1h (p90).


2025-10-29 15:51:10 UTCMergeQueue: The checks failed on this merge request

Tests failed on this commit 7320516:

What to do next?

  • Investigate the failures and when ready, re-add your pull request to the queue!
  • If your PR checks are green, try to rebase/merge. It might be because the CI run is a bit old.
  • Any question, go check the FAQ.

@typotter

Copy link
Copy Markdown
Contributor Author

/merge

@dd-devflow-routing-codex

dd-devflow-routing-codex Bot commented Oct 29, 2025

Copy link
Copy Markdown

View all feedbacks in Devflow UI.

2025-10-29 16:27:05 UTC ℹ️ Start processing command /merge


2025-10-29 16:27:11 UTC ℹ️ MergeQueue: pull request added to the queue

The expected merge time in feature/feature-flagging is approximately 1h (p90).


2025-10-29 17:42:20 UTCMergeQueue: The checks failed on this merge request

Tests failed on this commit a0a6f64:

What to do next?

  • Investigate the failures and when ready, re-add your pull request to the queue!
  • If your PR checks are green, try to rebase/merge. It might be because the CI run is a bit old.
  • Any question, go check the FAQ.

@typotter

Copy link
Copy Markdown
Contributor Author

/merge

@dd-devflow-routing-codex

dd-devflow-routing-codex Bot commented Oct 29, 2025

Copy link
Copy Markdown

View all feedbacks in Devflow UI.

2025-10-29 17:55:10 UTC ℹ️ Start processing command /merge


2025-10-29 17:55:15 UTC ℹ️ MergeQueue: pull request added to the queue

The expected merge time in feature/feature-flagging is approximately 1h (p90).


2025-10-29 18:53:55 UTC ℹ️ MergeQueue: This merge request was merged

@dd-mergequeue
dd-mergequeue Bot merged commit 6868ce4 into feature/feature-flagging Oct 29, 2025
27 checks passed
@dd-mergequeue
dd-mergequeue Bot deleted the typo/FFL-1187-graceful-mode branch October 29, 2025 18:53
This was referenced Nov 4, 2025
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.

5 participants