Skip to content

Commit 65dedd6

Browse files
feat: improve error handling and state management in identifyUser and resetUser (#252)
* feat: improve error handling and state management in identifyUser and resetUser * feat: PR comment updates * chore: address PR comments * fix: move flushEvents()
1 parent ad6275b commit 65dedd6

File tree

3 files changed

+488
-84
lines changed

3 files changed

+488
-84
lines changed

android-client-sdk/src/main/java/com/devcycle/sdk/android/api/DevCycleClient.kt

Lines changed: 94 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -203,41 +203,48 @@ class DevCycleClient private constructor(
203203
@JvmOverloads
204204
@Synchronized
205205
fun identifyUser(user: DevCycleUser, callback: DevCycleCallback<Map<String, BaseConfigVariable>>? = null) {
206+
// Validate user before modifying any client state
207+
val updatedUser: PopulatedUser = try {
208+
if (this@DevCycleClient.user.userId == user.userId) {
209+
this@DevCycleClient.user.copyUserAndUpdateFromDevCycleUser(user)
210+
} else {
211+
// Apply the same validation logic as in DevCycleClientBuilder
212+
DevCycleClient.validateDevCycleUser(user, dvcSharedPrefs)
213+
PopulatedUser.fromUserParam(user, context)
214+
}
215+
} catch (t: Throwable) {
216+
// On invalid user, return error and keep existing user/config
217+
callback?.onError(t)
218+
return
219+
}
220+
206221
flushEvents()
207222

208-
val updatedUser: PopulatedUser = if (this@DevCycleClient.user.userId == user.userId) {
209-
this@DevCycleClient.user.copyUserAndUpdateFromDevCycleUser(user)
210-
} else {
211-
// Apply the same validation logic as in DevCycleClientBuilder
212-
DevCycleClient.validateDevCycleUser(user, dvcSharedPrefs)
213-
PopulatedUser.fromUserParam(user, context)
214-
}
223+
// Store previous state for recovery
224+
val previousUser = latestIdentifiedUser
215225
latestIdentifiedUser = updatedUser
216226

217-
if (isExecuting.get()) {
218-
configRequestQueue.add(UserAndCallback(updatedUser, callback))
219-
DevCycleLogger.d("Queued identifyUser request for user_id %s", updatedUser.userId)
220-
return
221-
}
227+
fetchConfigForUser(updatedUser, object : DevCycleCallback<Map<String, BaseConfigVariable>> {
228+
override fun onSuccess(variables: Map<String, BaseConfigVariable>) {
229+
callback?.onSuccess(variables)
230+
}
222231

223-
isExecuting.set(true)
224-
coroutineScope.launch {
225-
withContext(coroutineContext) {
226-
try {
227-
fetchConfig(updatedUser)
232+
override fun onError(error: Throwable) {
233+
DevCycleLogger.d("Error fetching config for user_id %s: %s", updatedUser.userId, error.message)
234+
235+
// In the event that the config request fails (i.e. the device is offline)
236+
// Fallback to using a Cached Configuration for the User if available
237+
val hasCachedConfig = tryLoadCachedConfigForUser(updatedUser)
238+
if (hasCachedConfig) {
239+
// Successfully used cached config, return success
228240
config?.variables?.let { callback?.onSuccess(it) }
229-
} catch (t: Throwable) {
230-
DevCycleLogger.d("Error fetching config for user_id %s", updatedUser.userId)
231-
// In the event that the config request fails (i.e. the device is offline)
232-
// Fallback to using a Cached Configuration for the User if available
233-
useCachedConfigForUser(updatedUser)
234-
callback?.onError(t)
235-
} finally {
236-
handleQueuedConfigRequests()
237-
isExecuting.set(false)
241+
} else {
242+
// No cached config available, restore previous state and return error
243+
latestIdentifiedUser = previousUser
244+
callback?.onError(error)
238245
}
239246
}
240-
}
247+
})
241248
}
242249

243250
/**
@@ -249,31 +256,32 @@ class DevCycleClient private constructor(
249256
@JvmOverloads
250257
@Synchronized
251258
fun resetUser(callback: DevCycleCallback<Map<String, BaseConfigVariable>>? = null) {
252-
val newUser: PopulatedUser = PopulatedUser.buildAnonymous(this.context)
253-
latestIdentifiedUser = newUser
254-
255-
if (isExecuting.get()) {
256-
configRequestQueue.add(UserAndCallback(newUser, callback))
257-
DevCycleLogger.d("Queued resetUser request for new anonymous user")
258-
return
259+
val cachedAnonUserId = dvcSharedPrefs.getAnonUserId()
260+
if (cachedAnonUserId != null) {
261+
dvcSharedPrefs.clearAnonUserId()
259262
}
260263

264+
// Store previous state for recovery
265+
val previousUser = latestIdentifiedUser
266+
val newUser = PopulatedUser.buildAnonymous(this.context, dvcSharedPrefs)
267+
latestIdentifiedUser = newUser
268+
261269
flushEvents()
262270

263-
coroutineScope.launch {
264-
withContext(coroutineContext) {
265-
isExecuting.set(true)
266-
try {
267-
fetchConfig(newUser)
268-
config?.variables?.let { callback?.onSuccess(it) }
269-
} catch (t: Throwable) {
270-
callback?.onError(t)
271-
} finally {
272-
handleQueuedConfigRequests()
273-
isExecuting.set(false)
271+
fetchConfigForUser(newUser, object : DevCycleCallback<Map<String, BaseConfigVariable>> {
272+
override fun onSuccess(variables: Map<String, BaseConfigVariable>) {
273+
callback?.onSuccess(variables)
274+
}
275+
276+
override fun onError(error: Throwable) {
277+
// Restore the original anonymous user id and previous user on error
278+
if (cachedAnonUserId != null) {
279+
dvcSharedPrefs.setAnonUserId(cachedAnonUserId)
274280
}
281+
latestIdentifiedUser = previousUser
282+
callback?.onError(error)
275283
}
276-
}
284+
})
277285
}
278286

279287
fun close(callback: DevCycleCallback<String>? = null) {
@@ -510,6 +518,32 @@ class DevCycleClient private constructor(
510518
}
511519
}
512520

521+
private fun fetchConfigForUser(
522+
user: PopulatedUser,
523+
callback: DevCycleCallback<Map<String, BaseConfigVariable>>
524+
) {
525+
if (isExecuting.get()) {
526+
configRequestQueue.add(UserAndCallback(user, callback))
527+
return
528+
}
529+
530+
isExecuting.set(true)
531+
coroutineScope.launch {
532+
withContext(coroutineContext) {
533+
try {
534+
fetchConfig(user)
535+
config?.variables?.let { callback?.onSuccess(it) }
536+
} catch (t: Throwable) {
537+
callback?.onError(t)
538+
} finally {
539+
handleQueuedConfigRequests()
540+
isExecuting.set(false)
541+
}
542+
}
543+
}
544+
}
545+
546+
513547
private fun refetchConfig(
514548
sse: Boolean = false,
515549
lastModified: Long? = null,
@@ -557,6 +591,20 @@ class DevCycleClient private constructor(
557591
}
558592
}
559593

594+
private fun tryLoadCachedConfigForUser(user: PopulatedUser): Boolean {
595+
val cachedConfig = if (disableConfigCache) null else dvcSharedPrefs.getConfig(user)
596+
597+
if (cachedConfig != null) {
598+
config = cachedConfig
599+
isConfigCached.set(true)
600+
DevCycleLogger.d("Loaded config from cache for user_id %s", user.userId)
601+
observable.configUpdated(config)
602+
return true
603+
} else {
604+
return false
605+
}
606+
}
607+
560608
class DevCycleClientBuilder {
561609
private var context: Context? = null
562610
private var customLifecycleHandler: Handler? = null

android-client-sdk/src/main/java/com/devcycle/sdk/android/model/PopulatedUser.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import java.util.*
1010
import android.content.pm.PackageInfo
1111
import com.devcycle.BuildConfig
1212
import com.devcycle.sdk.android.util.DevCycleLogger
13+
import com.devcycle.sdk.android.util.DVCSharedPrefs
1314
import kotlin.IllegalArgumentException
1415

1516
@JsonInclude(JsonInclude.Include.NON_NULL)
@@ -67,8 +68,8 @@ internal data class PopulatedUser constructor(
6768
}
6869

6970
internal companion object {
70-
@JvmSynthetic internal fun buildAnonymous(context: Context): PopulatedUser {
71-
val userId = UUID.randomUUID().toString()
71+
@JvmSynthetic internal fun buildAnonymous(context: Context, dvcSharedPrefs: DVCSharedPrefs): PopulatedUser {
72+
val userId = dvcSharedPrefs.getOrCreateAnonUserId()
7273
val isAnonymous = true
7374
val locale: Locale = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
7475
context.resources.configuration.locales[0]

0 commit comments

Comments
 (0)