android-client-sdk icon indicating copy to clipboard operation
android-client-sdk copied to clipboard

Incorrect flag value returned after updating context

Open hoda0013 opened this issue 6 months ago • 5 comments

Describe the bug

TL;DR I start my app and am initializing LDClient with a default, anonymous context. I then update that context and set the context's key to the user's unique ID and then request a feature flag and I get the expected flag value returned. Then I then kill my app, put the device in airplane mode and start the app again, I go through the exact same process where I initialize the LDClient with a default, anonymous context, then update the context with the same user id and then request a feature flag, but this time the flag value is not correct and seems to be returning the values I'd expect if I was still using the the default, anonymous context.

A more in depth explanation of how the app is setup:

Whenever our app starts up, in Application.onCreate() we immediately call LDClient.init(...) with a default, anonymous LDContext. This is because we want to always initialize the LDClient as soon as the app's process is started and we don't yet know if the app has a logged in user (we'd have to make an async db call to determine this).

Whenever we attempt to get a LD flag value, we check if the user is logged in. If they are and we haven't updated the LD context, we call LDClient.identify(newContext) and block until it returns. Then we use that context to get the flag value. This seems to generally work fine, but I'm running into an issue and either I'm misunderstanding how this should work or I've found a bug.

Here's the scenario:

  • User opens app
  • LDClient.init called with anonymous user
  • App requests flag and user is logged in already
  • LDClient.identify() is called with an updated context. Then boolVariation is called on LDClient and the correct value is returned.

My understanding at this point is that has cached that context and pulled down the correct flag values for that context and persisted them.

Now, we continue the scenario:

  • Kill app
  • Put device in airplane mode
  • Open app
  • LDClient.init called with anonymous user
  • App requests flag and user is logged in already
  • LDClient.identify() is called with an updated context. When I build this new context, I'm setting the key to the same user id that I did the first time. But this time the flag returns an incorrect value. My expectation is that because I called LDClient.identify() with my updated context, that LD should return the cached flag values for that context even though I'm offline b/c it should exist in a cache. What seems to be happening is that it's returning flag values for my initial, anonymous context.

Initializing the LDClient in the application class:

    fun getDefaultLDContext(versionName: String): LDContext = LDContext.builder(
        ContextKind.DEFAULT,
        "anonymous"
    )
        .anonymous(true)
        .set(
            "version",
            versionName
        ) // Setting this because if you don't set any custom attributes and then try to update the context with custom attributes you get an NPE
        .build()  

    fun getLDConfig(): LDConfig = LDConfig.Builder()
        .mobileKey(PRODUCTION_MOBILE_KEY)
        .secondaryMobileKeys(
            mapOf(
                DEVELOPMENT_MOBILE_KEY_NAME to DEVELOPMENT_MOBILE_KEY,
                STAGING_MOBILE_KEY_NAME to STAGING_MOBILE_KEY
            )
        )
        .events(
            Components.sendEvents()
                .allAttributesPrivate(true)
        )
        .logLevel(LDLogLevel.DEBUG)
        .disableBackgroundUpdating(false)
        .build()

    override fun onCreate() {
        /**
         *  Initialize LD immediately before any dependency injection occurs and
         *  without blocking the thread, the client connects in the background and periodically
         *  checks for flag value updates
         */
        println("application onCreate()")

        if (BuildConfig.DEBUG) {
            Timber.plant(Timber.DebugTree())
        }
        val defaultLDContext = LaunchDarklyHelper.getDefaultLDContext(BuildConfig.VERSION_NAME)
        LDClient.init(
            this,
            LaunchDarklyHelper.getLDConfig(),
            defaultLDContext,
            0
        )
        println("LDClient init finished")
        super.onCreate()
}

Updating the context before requesting a flag. Using suspendCoroutine so that I can call .get() on the Future and not block the calling thread. Once the context has been updated, then we resume the coroutine.

    private suspend fun build(session: Session): ClientEnvironment {
        return mutex.withLock {
            val appEnv = appEnvironmentService.getAppEnvironment()
            println("FF appEnv: $appEnv")
            val ldClient =
                launchDarklyHelper.getClientForEnvironment(appEnv)
            println("FF ldClient: $ldClient")
            if (this.session != session) {
                println("FF session ${this.session }!= session $session")
                this.session = session
                suspendCoroutine {
                    println("FF updating ld context")
                    launchDarklyHelper.updateLDContext(session, ldClient)
                    println("FF finished updating LD Context")
                    it.resume(ClientEnvironment(ldClient, appEnv))
                }
            } else {
                ClientEnvironment(ldClient, appEnv)
            }
        }
    }

    fun updateLDContext(
        session: Session,
        ldClient: LDClient
    ) {
        val updatedContext =
            LDContext.builderFromContext(getDefaultLDContext(BuildConfig.VERSION_NAME))
                .anonymous(false)
                .key(session.userId)
                .set(
                    "role",
                    session.role.name
                )
                .set(
                    "email",
                    session.emailAddress
                )
                .set(
                    "company_id",
                    session.company.id
                )
                .set(
                    "reseller_id",
                    session.reseller.id
                )
                .build()

        ldClient.identify(updatedContext)
            .get()
    }

To reproduce

  • Set Initialize LDClient and set context to anonymous context
  • Update the context
  • request feature flags, values should be correct for the updated context (and I'm assuming cached with the user's id as the key)
  • kill app
  • put device in airplane mode
  • Open app
  • Set Initialize LDClient and set context to anonymous context
  • Update the context
  • request feature flag. Now feature flag values are for the anonymous context, not the updated one.

Expected behavior If I have accessed flags with my updated context, those flags should be cached and returned, even if the device is in airplane mode. If I do this exact same test and just don't put the device in airplane mode, everything works as expected.

Logs If applicable, add any log output related to your problem.

SDK version 4.3.1

Language version, developer tools Android Studio, Kotlin 1.8.10

OS/platform Android

Additional context Not sure if this matter, but, I'm seeing this happen when running an Android WorkManager job after the app has been killed. When the job triggers, first the application class starts, LDClient has init() called with the default, anonymous context, then the WorkManager job starts and updates the LDContext and requests a flag value.

hoda0013 avatar Aug 01 '24 19:08 hoda0013