Skip to content

Commit a93dcb3

Browse files
authored
Add more info to Feature Flag error logging (#1579)
* More FF evaluation logging * Include more detailed message on exceptions returned via Result.failure, too * Add case to log connection errors with a descriptive message * Bubble up status codes from DeferredResolver
1 parent 9f65c0b commit a93dcb3

File tree

6 files changed

+97
-21
lines changed

6 files changed

+97
-21
lines changed

urbanairship-core/src/main/java/com/urbanairship/deferred/DeferredResolver.kt

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -73,33 +73,40 @@ public class DeferredResolver internal constructor(
7373
return DeferredResult.TimedOut()
7474
}
7575

76-
when (response.status) {
76+
when (val statusCode = response.status) {
7777
200 -> {
78-
val value = response.value ?: return DeferredResult.RetriableError()
79-
if (value.isNull) {
80-
return DeferredResult.RetriableError()
78+
val value = response.value
79+
// Check for null value or null JsonValue
80+
if (value == null || value.isNull) {
81+
return DeferredResult.RetriableError(statusCode = statusCode)
8182
}
8283
return try {
8384
DeferredResult.Success(resultParser(value))
8485
} catch (ex: Exception) {
85-
UALog.e(ex) { "Failed ot parse deferred" }
86-
DeferredResult.RetriableError()
86+
UALog.e(ex) { "Failed to parse deferred!" }
87+
DeferredResult.RetriableError(statusCode = statusCode)
8788
}
8889
}
8990
404 -> return DeferredResult.NotFound()
9091
409 -> return DeferredResult.OutOfDate()
9192
429 -> {
9293
response.locationHeader?.let { locationMap.put(uri, it) }
93-
return DeferredResult.RetriableError<T>(response.getRetryAfterHeader(TimeUnit.MILLISECONDS, 0))
94+
return DeferredResult.RetriableError(
95+
retryAfter = response.getRetryAfterHeader(TimeUnit.MILLISECONDS, 0),
96+
statusCode = statusCode
97+
)
9498
}
9599
307 -> {
96100
val redirect = response.locationHeader
97-
?: return DeferredResult.RetriableError<T>(response.getRetryAfterHeader(TimeUnit.MILLISECONDS, 0))
101+
?: return DeferredResult.RetriableError(
102+
retryAfter = response.getRetryAfterHeader(TimeUnit.MILLISECONDS, 0),
103+
statusCode = statusCode
104+
)
98105
locationMap[uri] = redirect
99106

100107
val retryDelay = response.getRetryAfterHeader(TimeUnit.MILLISECONDS, -1)
101108
if (retryDelay > 0) {
102-
return DeferredResult.RetriableError<T>(retryDelay)
109+
return DeferredResult.RetriableError(retryDelay, statusCode = statusCode)
103110
}
104111

105112
if (allowRetry) {
@@ -115,9 +122,9 @@ public class DeferredResolver internal constructor(
115122
)
116123
}
117124

118-
return DeferredResult.RetriableError<T>()
125+
return DeferredResult.RetriableError(statusCode = statusCode)
119126
}
120-
else -> return DeferredResult.RetriableError<T>()
127+
else -> return DeferredResult.RetriableError(statusCode = statusCode)
121128
}
122129
}
123130
}

urbanairship-core/src/main/java/com/urbanairship/deferred/DeferredResult.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,11 @@ import kotlinx.coroutines.launch
2323
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
2424
public sealed class DeferredResult<T> {
2525
public class Success<T>(public val result: T) : DeferredResult<T>()
26-
public class RetriableError<T>(public val retryAfter: Long? = null) : DeferredResult<T>()
26+
public class RetriableError<T>(
27+
public val retryAfter: Long? = null,
28+
public val statusCode: Int? = null,
29+
public val errorDescription: String? = null
30+
) : DeferredResult<T>()
2731
public class TimedOut<T>() : DeferredResult<T>()
2832
public class OutOfDate<T>() : DeferredResult<T>()
2933
public class NotFound<T>() : DeferredResult<T>()

urbanairship-core/src/test/java/com/urbanairship/deferred/DeferredResolverTest.kt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,15 @@ import com.urbanairship.json.JsonValue
1010
import io.mockk.every
1111
import io.mockk.mockk
1212
import java.util.Locale
13+
import kotlinx.coroutines.Dispatchers
14+
import kotlinx.coroutines.ExperimentalCoroutinesApi
15+
import kotlinx.coroutines.test.StandardTestDispatcher
16+
import kotlinx.coroutines.test.TestDispatcher
1317
import kotlinx.coroutines.test.TestResult
18+
import kotlinx.coroutines.test.resetMain
1419
import kotlinx.coroutines.test.runTest
20+
import kotlinx.coroutines.test.setMain
21+
import org.junit.After
1522
import org.junit.Assert.assertEquals
1623
import org.junit.Assert.assertNotNull
1724
import org.junit.Assert.assertNull
@@ -20,20 +27,30 @@ import org.junit.Before
2027
import org.junit.Test
2128
import org.junit.runner.RunWith
2229

30+
@OptIn(ExperimentalCoroutinesApi::class)
2331
@RunWith(AndroidJUnit4::class)
2432
public class DeferredResolverTest {
33+
public val testDispatcher: TestDispatcher = StandardTestDispatcher()
34+
2535
public lateinit var resolver: DeferredResolver
2636
internal val requestSession = TestRequestSession()
2737

2838
@Before
2939
public fun setup() {
40+
Dispatchers.setMain(testDispatcher)
41+
3042
val config: AirshipRuntimeConfig = mockk()
3143
every { config.requestSession } returns requestSession
3244
every { config.platform } returns UAirship.ANDROID_PLATFORM
3345

3446
resolver = DeferredResolver(config, AudienceOverridesProvider())
3547
}
3648

49+
@After
50+
public fun tearDown() {
51+
Dispatchers.resetMain()
52+
}
53+
3754
@Test
3855
public fun testSuccess(): TestResult = runTest {
3956
val expected = JsonValue.wrap("body")

urbanairship-feature-flag/src/main/java/com/urbanairship/featureflag/FeatureFlagException.kt

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,34 @@ package com.urbanairship.featureflag
66
* Thrown when a feature flag data is stale or outdated.
77
*/
88
public sealed class FeatureFlagException(override val message: String) : Exception(message) {
9-
public class FailedToFetch() : FeatureFlagException("failed to fetch data")
9+
public class FailedToFetch(message: String) : FeatureFlagException(message)
1010
}
1111

1212
internal sealed class FeatureFlagEvaluationException(override val message: String) : Exception(message) {
13-
class ConnectionError() : FeatureFlagEvaluationException("Unable to fetch data")
13+
class ConnectionError(
14+
val statusCode: Int? = null,
15+
val errorDescription: String? = null,
16+
) : FeatureFlagEvaluationException(makeMessage(statusCode, errorDescription)) {
17+
18+
private companion object {
19+
@JvmStatic
20+
private fun makeMessage(statusCode: Int? = null, errorDescription: String? = null): String {
21+
var msg = "Unable to fetch data"
22+
23+
msg += if (statusCode != null) {
24+
" ($statusCode)."
25+
} else {
26+
"."
27+
}
28+
29+
if (errorDescription != null) {
30+
msg += " $errorDescription"
31+
}
32+
33+
return msg
34+
}
35+
}
36+
}
1437
class OutOfDate() : FeatureFlagEvaluationException("Remote data is outdated")
1538
class StaleNotAllowed() : FeatureFlagEvaluationException("Stale data is not allowed")
1639
}

urbanairship-feature-flag/src/main/java/com/urbanairship/featureflag/FeatureFlagManager.kt

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,9 @@ public class FeatureFlagManager internal constructor(
9191

9292
private suspend fun flag(name: String, allowRefresh: Boolean): Result<FeatureFlag> {
9393
if (!privacyManager.isEnabled(PrivacyManager.Feature.FEATURE_FLAGS)) {
94-
return Result.failure(IllegalStateException("Feature flags are disabled"))
94+
val msg = "Failed to fetch feature flag: '$name'! Feature flags are disabled."
95+
UALog.e(msg)
96+
return Result.failure(IllegalStateException(msg))
9597
}
9698

9799
val remoteDataInfo = remoteData.fetchFlagRemoteInfo(name)
@@ -100,15 +102,17 @@ public class FeatureFlagManager internal constructor(
100102
return result
101103
}
102104

103-
return when (result.exceptionOrNull()) {
105+
return when (val e = result.exceptionOrNull()) {
104106
is FeatureFlagEvaluationException.OutOfDate -> {
105107
remoteData.notifyOutOfDate(remoteDataInfo.remoteDataInfo)
106108

107109
if (allowRefresh) {
108110
remoteData.waitForRemoteDataRefresh()
109111
flag(name = name, allowRefresh = false)
110112
} else {
111-
Result.failure(FeatureFlagException.FailedToFetch())
113+
val msg = "Failed to fetch feature flag: '$name'! Remote data is outdated."
114+
UALog.e(e, msg)
115+
Result.failure(FeatureFlagException.FailedToFetch(msg).apply { initCause(e) })
112116
}
113117
}
114118

@@ -117,11 +121,25 @@ public class FeatureFlagManager internal constructor(
117121
remoteData.waitForRemoteDataRefresh()
118122
flag(name = name, allowRefresh = false)
119123
} else {
120-
Result.failure(FeatureFlagException.FailedToFetch())
124+
val msg = "Failed to fetch feature flag: '$name'! Stale data is not allowed."
125+
UALog.e(e, msg)
126+
Result.failure(FeatureFlagException.FailedToFetch(msg).apply { initCause(e) })
121127
}
122128
}
123129

124-
else -> Result.failure(FeatureFlagException.FailedToFetch())
130+
is FeatureFlagEvaluationException.ConnectionError -> {
131+
val msg = "Failed to fetch feature flag: '$name'! Network error" +
132+
"${e.statusCode?.let { " ($it)" }}." +
133+
"${e.errorDescription?.let { " $it" }}"
134+
UALog.e(e, msg)
135+
Result.failure(FeatureFlagException.FailedToFetch(msg).apply { initCause(e) })
136+
}
137+
138+
else -> {
139+
val msg = "Failed to fetch feature flag: '$name'!"
140+
UALog.e(msg)
141+
Result.failure(FeatureFlagException.FailedToFetch(msg))
142+
}
125143
}
126144
}
127145

@@ -199,7 +217,7 @@ public class FeatureFlagManager internal constructor(
199217
}
200218
}
201219

202-
/// If we have an error, flag is eligible, or the last flag return
220+
// If we have an error, flag is eligible, or the last flag return
203221
if (result.isFailure || result.getOrNull()?.isEligible == true || isLast) {
204222
return evaluatedControl(result, info, deviceInfoProvider)
205223
}

urbanairship-feature-flag/src/main/java/com/urbanairship/featureflag/FlagDeferredResolver.kt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,14 @@ internal class FlagDeferredResolver(
107107
val backOff = result.retryAfter ?: DEFAULT_BACKOFF_MS
108108
if (!allowRetry || backOff > IMMEDIATE_BACKOFF_RETRY_MS) {
109109
backOffIntervals[requestId] = clock.currentTimeMillis() + backOff
110-
return Result.failure(FeatureFlagEvaluationException.ConnectionError())
110+
return Result.failure(FeatureFlagEvaluationException.ConnectionError(
111+
statusCode = result.statusCode,
112+
errorDescription = if (!allowRetry) {
113+
"Retries are not allowed"
114+
} else {
115+
"Unable to immediately retry. Try again in $backOff ms."
116+
}
117+
))
111118
}
112119

113120
if (backOff > 0) {

0 commit comments

Comments
 (0)