feat: Graceful mode and error logging policies#2963
Conversation
25de984 to
b672f0f
Compare
Codecov Report❌ Patch coverage is 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
🚀 New features to boost your workflow:
|
| internal fun logErrorWithPolicy( | ||
| message: String, | ||
| level: InternalLogger.Level = InternalLogger.Level.ERROR, | ||
| shouldCrashInStrict: Boolean = false |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
e0b4add to
304213a
Compare
| { "Missing required context parameters: $missingParams" } | ||
| ) | ||
|
|
||
| val logWithPolicy: (String, InternalLogger.Level) -> Unit = { message, level -> |
There was a problem hiding this comment.
maybe it is worth to create typealias for (String, InternalLogger.Level) -> Unit
There was a problem hiding this comment.
yes, agreed. helps to define the interaction.
| @Test | ||
| fun `M throw IllegalArgumentException W Builder() {blank name}`() { | ||
| // When/Then | ||
| org.junit.jupiter.api.assertThrows<IllegalArgumentException> { |
| verify(mockLogger).log( | ||
| eq(InternalLogger.Level.ERROR), | ||
| eq(InternalLogger.Target.USER), | ||
| any(), |
There was a problem hiding this comment.
It is better to verify the message as well, otherwise it can be an error for something else. Same for the test below.
| whenever(mockSdkCore.createSingleThreadExecutorService(any())) doReturn mockExecutorService | ||
|
|
||
| // Setup mockContext with default release build (flags = 0) | ||
| val applicationInfo = android.content.pm.ApplicationInfo().apply { |
| ) | ||
|
|
||
| // 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 |
There was a problem hiding this comment.
you can also wrap logErrorWithPolicy above in assertNotThrows to explicitly highlight the intention.
| * @param level The log level for conditional logging (graceful policy) | ||
| * @param shouldCrashInStrict If true, crashes in strict policy | ||
| */ | ||
| internal fun logErrorWithPolicy( |
There was a problem hiding this comment.
this method should be moved below, after the override methods
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.
cfe311d to
2e986bb
Compare
| { "Missing required context parameters: $missingParams" } | ||
| ) | ||
|
|
||
| val logWithPolicy: (String, InternalLogger.Level) -> Unit = { message, level -> |
There was a problem hiding this comment.
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( |
| @Test | ||
| fun `M throw IllegalArgumentException W Builder() {blank name}`() { | ||
| // When/Then | ||
| org.junit.jupiter.api.assertThrows<IllegalArgumentException> { |
| verify(mockLogger).log( | ||
| eq(InternalLogger.Level.ERROR), | ||
| eq(InternalLogger.Target.USER), | ||
| any(), |
| whenever(mockSdkCore.createSingleThreadExecutorService(any())) doReturn mockExecutorService | ||
|
|
||
| // Setup mockContext with default release build (flags = 0) | ||
| val applicationInfo = android.content.pm.ApplicationInfo().apply { |
| verify(mockInternalLogger).log( | ||
| eq(InternalLogger.Level.ERROR), | ||
| eq(InternalLogger.Target.USER), | ||
| any(), |
| @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 } |
| ) | ||
|
|
||
| // 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 |
| featureSdkCore: FeatureSdkCore, | ||
| flagsFeature: FlagsFeature | ||
| flagsFeature: FlagsFeature, | ||
| name: String = DEFAULT_CLIENT_NAME |
There was a problem hiding this comment.
is default argument needed here, given it is already there for public API?
| ) | ||
| } else { | ||
| // Create FlagsContext combining core SDK context with feature configuration | ||
| // Build a [DatadogFlagsClient] instance from its dependencies. |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
although here and for other tests in this file it is not error, but warning
|
|
||
| package com.datadog.android.flags.internal | ||
|
|
|
/merge |
|
View all feedbacks in Devflow UI.
The expected merge time in
Tests failed on this commit 7320516: What to do next?
|
|
/merge |
|
View all feedbacks in Devflow UI.
The expected merge time in
Tests failed on this commit a0a6f64: What to do next?
|
|
/merge |
|
View all feedbacks in Devflow UI.
The expected merge time in
|
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:
gracefulModeEnabledconfiguration parameter (defaults totrue)ApplicationInfo.FLAG_DEBUGGABLEFlagsFeature.logErrorWithPolicy()NoOpFlagsClientto use policy-based logging functionsPolicy behavior:
System.err, non-crashingIllegalStateExceptionMotivation
The Flags SDK needs configurable error handling to balance developer experience across
different environments:
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:
Debug Detection: Uses
ApplicationInfo.FLAG_DEBUGGABLE(same pattern asDatadogCore.isAppDebuggable()) with performance caching via@Volatilefield primedduring
onInitialize()Thread Safety:
@Volatilefields forappContext,debugBuildCache, andinitializedflaggetOrRegisterNewClient()onStop()Crash Points (Strict Mode Only):
FlagsFeature.getOrRegisterNewClient()FlagsClient.get()error()function to throwIllegalStateExceptionSafe Fallback: When
FlagsFeaturenot enabled, always uses graceful behavior (can't determine policy without config)Documentation:
@throws IllegalStateExceptiondocumentation tobuild()andget()methodsIntentional deviations from spec:
NoOpFlagsClientname instead ofFallbackClientReview checklist (to be filled by reviewers)