feat: use cached config if avail on init with openfeature provider#287
feat: use cached config if avail on init with openfeature provider#287
Conversation
66525c1 to
93c4cb3
Compare
There was a problem hiding this comment.
Pull request overview
Implements cache-first initialization for static-context providers, adds OpenFeature provider event support, and refines HTTP status classification to better distinguish auth/config errors from retryable server errors.
Changes:
- Add persisted-cache management utilities (per-user cache clearing) and expand HTTP response code handling (403/429 + better default mapping).
- Enable cache-first DevCycleClient initialization with background refresh and config-updated callbacks.
- Add/extend tests around cache-first init behavior, cached evaluation reason mapping, and shared-prefs cache clearing.
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| android-client-sdk/src/main/java/com/devcycle/sdk/android/api/DevCycleClient.kt | Cache-first init, background refresh, config-updated callbacks, identity-switch behavior changes |
| android-client-sdk/src/main/java/com/devcycle/sdk/android/openfeature/DevCycleProvider.kt | Adds OpenFeature provider event observation + cached-eval reason mapping |
| android-client-sdk/src/main/java/com/devcycle/sdk/android/util/DVCSharedPrefs.kt | Adds clearConfigForUser for persisted cache management |
| android-client-sdk/src/main/java/com/devcycle/sdk/android/model/HttpResponseCode.kt | Adds 403/429 and adjusts unmapped-code classification |
| android-client-sdk/src/main/java/com/devcycle/sdk/android/model/EvalReason.kt | Adds source and helper for marking cached evaluations |
| android-client-sdk/src/main/java/com/devcycle/sdk/android/exception/DVCRequestException.kt | Carries raw status code + adds auth error classification |
| android-client-sdk/src/main/java/com/devcycle/sdk/android/api/Request.kt | Propagates raw HTTP status into DVCRequestException |
| android-client-sdk/src/test/java/com/devcycle/sdk/android/api/DevCycleClientTests.kt | Adds tests for cache-first init + user-change/cache-clear scenarios (needs fixes) |
| android-client-sdk/src/test/java/com/devcycle/sdk/android/openfeature/DevCycleProviderTest.kt | Adds tests for cache-hit init behavior + cached reason mapping |
| android-client-sdk/src/test/java/com/devcycle/sdk/android/util/DVCSharedPrefsTests.kt | Adds tests for clearing config/expiry per user |
| android-client-sdk/src/test/java/com/devcycle/sdk/android/model/EvalReasonTests.kt | Adds unit tests for EvalReason.withCachedSource |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
android-client-sdk/src/main/java/com/devcycle/sdk/android/openfeature/DevCycleProvider.kt
Show resolved
Hide resolved
android-client-sdk/src/main/java/com/devcycle/sdk/android/api/DevCycleClient.kt
Outdated
Show resolved
Hide resolved
android-client-sdk/src/main/java/com/devcycle/sdk/android/api/DevCycleClient.kt
Outdated
Show resolved
Hide resolved
android-client-sdk/src/main/java/com/devcycle/sdk/android/api/DevCycleClient.kt
Outdated
Show resolved
Hide resolved
android-client-sdk/src/test/java/com/devcycle/sdk/android/api/DevCycleClientTests.kt
Show resolved
Hide resolved
android-client-sdk/src/test/java/com/devcycle/sdk/android/api/DevCycleClientTests.kt
Outdated
Show resolved
Hide resolved
android-client-sdk/src/test/java/com/devcycle/sdk/android/api/DevCycleClientTests.kt
Show resolved
Hide resolved
ca38781 to
705caac
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 11 out of 11 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
android-client-sdk/src/main/java/com/devcycle/sdk/android/api/DevCycleClient.kt
Show resolved
Hide resolved
android-client-sdk/src/test/java/com/devcycle/sdk/android/api/DevCycleClientTests.kt
Outdated
Show resolved
Hide resolved
android-client-sdk/src/test/java/com/devcycle/sdk/android/api/DevCycleClientTests.kt
Show resolved
Hide resolved
705caac to
4fe81d7
Compare
4fe81d7 to
2c9f270
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 11 out of 11 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
android-client-sdk/src/main/java/com/devcycle/sdk/android/api/DevCycleClient.kt
Outdated
Show resolved
Hide resolved
android-client-sdk/src/test/java/com/devcycle/sdk/android/api/DevCycleClientTests.kt
Outdated
Show resolved
Hide resolved
2c9f270 to
2cd29c6
Compare
2cd29c6 to
938aedb
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 11 out of 11 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| val isRetryable get() = httpResponseCode.code >= 500 | ||
| fun getErrorResponse(): ErrorResponse = errorResponse | ||
|
|
||
| val isRetryable get() = statusCode >= 500 |
There was a problem hiding this comment.
isRetryable is now statusCode >= 500, which makes HTTP 429 (Too Many Requests) non-retryable. Previously 429 would have been treated as retryable because unmapped codes fell back to 500; with the new TOO_MANY_REQUESTS(429) enum this becomes a behavior regression (config fetch retries + event flushing retries will stop on 429). Consider treating 429 as retryable (e.g., include it in isRetryable or base retryability on HttpResponseCode rather than raw status range).
| val isRetryable get() = statusCode >= 500 | |
| val isRetryable get() = httpResponseCode == HttpResponseCode.TOO_MANY_REQUESTS || statusCode >= 500 |
There was a problem hiding this comment.
429 should be retryable right?
android-client-sdk/src/main/java/com/devcycle/sdk/android/api/DevCycleClient.kt
Outdated
Show resolved
Hide resolved
| override fun onError(error: Throwable) { | ||
| DevCycleLogger.d("Error fetching config for user_id %s: %s", updatedUser.userId, error.message) | ||
|
|
||
| if (error is DVCRequestException && (error.isAuthError || error.statusCode == 400)) { | ||
| dvcSharedPrefs.clearConfigForUser(updatedUser) | ||
| DevCycleLogger.w("Config error during identifyUser (${error.statusCode}). Persisted cache cleared.") | ||
| latestIdentifiedUser = previousUser |
There was a problem hiding this comment.
PR description says a user switch "clears in-memory config immediately to prevent serving stale values for the wrong identity", but in identifyUser the existing in-memory config (and user used for evaluations/events) is kept until the async fetch completes. That means evaluations right after identifyUser can still be served from the previous user's config. If the intent is to stop serving stale values immediately on user switch, consider clearing/invalidating the in-memory config state (or gating evaluations) at the start of the identify flow.
There was a problem hiding this comment.
this should be fine, as we await for the config for the new identity to succeed before clearing cache
| } | ||
|
|
||
| override fun onError(t: Throwable) { | ||
| DevCycleLogger.e("Background refresh auth/config error: ${t.message}") |
There was a problem hiding this comment.
will this error always be an auth / config error or can it be transient?
There was a problem hiding this comment.
Good catch, I believe it can be a transient error.
938aedb to
06742b9
Compare
06742b9 to
cdabaf0
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 11 out of 11 changed files in this pull request and generated 1 comment.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| override fun onSuccess(result: Map<String, BaseConfigVariable>) { | ||
| DevCycleLogger.d("Background refresh succeeded") | ||
| } | ||
|
|
||
| override fun onError(error: Throwable) { |
There was a problem hiding this comment.
In performBackgroundRefresh(), you capture userAtRefreshStart, but refetchConfig() always fetches using latestIdentifiedUser (and will queue using whatever latestIdentifiedUser is at queue-time). If the user changes concurrently (e.g., identifyUser runs while the refresh is being scheduled/queued), the refresh request may be executed for a different user than userAtRefreshStart, yet on error you clear the persisted cache for userAtRefreshStart. This can clear the wrong user's cache and misattribute PROVIDER_ERROR/config errors. Consider refactoring so background refresh fetches for a specific user (e.g., pass userAtRefreshStart into refetchConfig/fetchConfig) and uses that same user for any cache-clearing decisions.
| override fun onSuccess(result: Map<String, BaseConfigVariable>) { | |
| DevCycleLogger.d("Background refresh succeeded") | |
| } | |
| override fun onError(error: Throwable) { | |
| override fun onSuccess(result: Map<String, BaseConfigVariable>) { | |
| if (latestIdentifiedUser != userAtRefreshStart) { | |
| DevCycleLogger.d("Background refresh completed after user changed; ignoring stale result") | |
| return | |
| } | |
| DevCycleLogger.d("Background refresh succeeded") | |
| } | |
| override fun onError(error: Throwable) { | |
| if (latestIdentifiedUser != userAtRefreshStart) { | |
| DevCycleLogger.w("Background refresh failed after user changed; skipping stale cache/error handling.") | |
| return | |
| } |
There was a problem hiding this comment.
I believe this is out of the scope of this PR. It's a pre-existing concurrency edge case in refetchConfig that exists independently of our changes
Implements open-feature/protocol#64 — cache-first initialization for static-context
providers.
feat: cache-first initialization
feat: OpenFeature provider events
feat: cache management
fix: HTTP status code classification
correctly