Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[PM-9401] Server feature flags manager #3656

Merged
merged 20 commits into from
Aug 6, 2024
Merged

Conversation

andrebispo5
Copy link
Contributor

🎟️ Tracking

Android - Implement Feature Flags Support

📔 Objective

Add support for hardcoded feature flags.
Add support for server-related feature flags. Be able to use them as boolean, int or string types.

⏰ Reminders before review

  • Contributor guidelines followed
  • All formatters and local linters executed and passed
  • Written new unit and / or integration tests where applicable
  • Used internationalization (i18n) for all UI strings
  • CI builds passed
  • Communicated to DevOps any deployment requirements
  • Updated any necessary documentation or informed the documentation team

🦮 Reviewer guidelines

  • 👍 (:+1:) or similar for great changes
  • 📝 (:memo:) or ℹ️ (:information_source:) for notes or general info
  • ❓ (:question:) for questions
  • 🤔 (:thinking:) or 💭 (:thought_balloon:) for more open inquiry that's not quite a confirmed
    issue and could potentially benefit from discussion
  • 🎨 (:art:) for suggestions / improvements
  • ❌ (:x:) or ⚠️ (:warning:) for more significant problems or concerns needing attention
  • 🌱 (:seedling:) or ♻️ (:recycle:) for future improvements or indications of technical debt
  • ⛏ (:pick:) for minor or nitpick changes

@andrebispo5 andrebispo5 requested review from david-livefront and SaintPatrck and removed request for david-livefront July 31, 2024 14:18
Copy link
Contributor

github-actions bot commented Jul 31, 2024

Logo
Checkmarx One – Scan Summary & Detailscd343ff3-3915-4bfb-8beb-8d31dea2d543

No New Or Fixed Issues Found

override val featureFlagsLocal: Map<String, Boolean>
get() = mapOf(CIPHER_KEY_ENCRYPTION_KEY to true)

override val featureFlagsServerStateFlow: StateFlow<Map<String, String>?>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we want to expose feature flags as strings, we want them to be type safe, right?


override val featureFlagsServerStateFlow: StateFlow<Map<String, String>?>
get() = configDiskSource
.serverConfigFlow
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we just subscribe to the serverConfigRepository.serverConfigStateFlow?

/**
* Manages the available feature flags for the Bitwarden application.
*/
interface BitwardenFeatureFlagManager {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason why you renamed this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the manager can have server-side flags, which may come from a self-hosted environments, I think removing the Bitwarden word from the class makes sense. What do you think?

@@ -136,10 +139,23 @@ object PlatformManagerModule {
dispatcherManager = dispatcherManager,
)

@Provides
@Singleton
fun providesBitwardenFeatureFlagManager(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's rename this function since the class was renamed.

fun providesFeatureFlagManager(

@@ -106,6 +106,6 @@ private val SERVER_CONFIG = ServerConfig(
notificationsUrl = "http://localhost:61840",
ssoUrl = "http://localhost:51822",
),
featureStates = mapOf("duo-redirect" to true, "flexible-collections-v-1" to false),
featureStates = mapOf("duo-redirect" to "true", "flexible-collections-v-1" to "false"),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like the sample JSON above returns true, not "true"? Does the API return a string or boolean?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ConfigDiskSource will the values as strings in order for them to be casted into the 3 possible types.


/**
* Enum to hold feature flag keys.
* [stringValue] corresponds to the string value of a give key
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we prefix this with @property:

* @property stringValue corresponds to the string value of a give key.

import java.time.Instant

class FakeServerConfigRepository(
private val configDiskSource: ConfigDiskSource,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should not need this dependency in a fake

@Test
fun `getFeatureFlag Boolean should return value if exists`() = runTest {
val flagValue = manager.getFeatureFlag(
FlagKey.EmailVerification,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add the name for this param.

/**
* Returns a map of constant feature flags that are only used locally.
*/
val featureFlagsLocal: Map<String, Boolean>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm curious why the interfaces exposes both this map and the flow of a map from the server. When the rest of the app interacts with the feature flags, it shouldn't care whether the flag is local or from the server, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe it's a naming confusion. Local is not meant as cached, the local flags are just constants that live on the device and are not affected by server/region changes.
Server flags are provided by the server, are dynamic and can change over time.
For now locals are only used to initialise the SDK.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These flags are currently just used for the SDK. So we could call it sdkFeatureFlags or something like that?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the time being I think we can. If, down the line, we need constant flags locally we can add them as such.

Comment on lines 64 to 68
private suspend fun getFlagStringValueOrNull(key: FlagKey, forceRefresh: Boolean): String? {
val configuration = serverConfigRepository.getServerConfig(forceRefresh)
return configuration?.serverData?.featureStates?.get(key.stringValue)
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this can be condensed a bit:

Suggested change
private suspend fun getFlagStringValueOrNull(key: FlagKey, forceRefresh: Boolean): String? {
val configuration = serverConfigRepository.getServerConfig(forceRefresh)
return configuration?.serverData?.featureStates?.get(key.stringValue)
}
}
private suspend fun getFlagStringValueOrNull(key: FlagKey, forceRefresh: Boolean): String? =
serverConfigRepository
.getServerConfig(forceRefresh)
?.serverData
?.featureStates
?.get(key.stringValue)

return getFlagStringValueOrNull(key, forceRefresh) ?: defaultValue
}

private suspend fun getFlagStringValueOrNull(key: FlagKey, forceRefresh: Boolean): String? {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a little confused - this means that all three getFeatureFlag methods only check for remote feature flags? Is there a reason we can't check both remote and local sources, so that callers of the methods don't have to know where the flag is coming from? If not, I think it would be better to document in the comments for these methods that they only check remote flags.

Copy link

codecov bot commented Aug 2, 2024

Codecov Report

Attention: Patch coverage is 76.00000% with 6 lines in your changes missing coverage. Please review.

Project coverage is 87.79%. Comparing base (f110687) to head (7d928cd).
Report is 1 commits behind head on main.

Files Patch % Lines
...en/data/platform/manager/FeatureFlagManagerImpl.kt 78.26% 1 Missing and 4 partials ⚠️
...rden/data/platform/manager/SdkClientManagerImpl.kt 50.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3656      +/-   ##
==========================================
- Coverage   87.80%   87.79%   -0.01%     
==========================================
  Files         368      368              
  Lines       30482    30503      +21     
  Branches     4557     4564       +7     
==========================================
+ Hits        26765    26781      +16     
- Misses       2128     2129       +1     
- Partials     1589     1593       +4     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@@ -102,5 +102,8 @@ object PlatformNetworkModule {

// Respect model default property values.
coerceInputValues = true

// Allow unquoted json keys and values to be parsed
isLenient = true
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a global flag, I have some concerns about making everything lenient.

@@ -31,7 +31,7 @@ data class ConfigResponseJson(
val environment: EnvironmentJson?,

@SerialName("featureStates")
val featureStates: Map<String, Boolean>?,
val featureStates: Map<String, String>?,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it might make more sense to parse this as a JsonElement or JsonPrimitive, I don't think we would need to make the Json instance isLenient and it would imply the unsafe nature of this property.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let me try that out.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It worked out.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome!

* @property [stringValue] corresponds to the string value of a give key
* @property [defaultValue] corresponds to default value of the flag of type [T]
*/
sealed class FlagKey<out T : Any> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we move this into the model package


@Test
fun `ConfigDiskSource flow with null should trigger default flag value value`() = runTest {
fakeServerConfigRepository.mutableServerConfigFlow.tryEmit(null)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this just be

fakeServerConfigRepository.serverConfigValue = null

mutableServerConfigFlow.value = value
}

val mutableServerConfigFlow = MutableStateFlow<ServerConfig?>(SERVER_CONFIG)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we make this private


/**
* Class to hold feature flag keys.
* @property [stringValue] corresponds to the string value of a give key
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* @property [stringValue] corresponds to the string value of a give key
* @property [stringValue] corresponds to the string value of a given key

* @property [defaultValue] corresponds to default value of the flag of type [T]
*/
sealed class FlagKey<out T : Any> {
abstract val stringValue: String
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💭 would it maybe be more clear to name this property keyName or just name

* @property [keyName] corresponds to the string value of a given key
* @property [defaultValue] corresponds to default value of the flag of type [T]
*/
sealed class FlagKey<out T : Any> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

* Data object holding the key for an Int flag to be used in tests
*/
data object DummyInt : FlagKey<Int>() {
override val keyName: String = "email-verification"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will duplicating the keyName cause any issues?

when (defaultValue::class) {
Boolean::class -> it.content.toBoolean() as? T
String::class -> it.content as? T
Int::class -> it.content.toInt() as? T
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think you need the ? on the as's

mutableServerConfigFlow.value = value
}

private val mutableServerConfigFlow = MutableStateFlow<ServerConfig?>(SERVER_CONFIG)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@andrebispo5 andrebispo5 merged commit 994a577 into main Aug 6, 2024
9 checks passed
@andrebispo5 andrebispo5 deleted the pm-9401/server-feature-flags branch August 6, 2024 15:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants