-
Notifications
You must be signed in to change notification settings - Fork 844
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
Conversation
…ts to be String - String
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>?> |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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"), |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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> |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
private suspend fun getFlagStringValueOrNull(key: FlagKey, forceRefresh: Boolean): String? { | ||
val configuration = serverConfigRepository.getServerConfig(forceRefresh) | ||
return configuration?.serverData?.featureStates?.get(key.stringValue) | ||
} | ||
} |
There was a problem hiding this comment.
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:
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? { |
There was a problem hiding this comment.
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.
Codecov ReportAttention: Patch coverage is
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. |
@@ -102,5 +102,8 @@ object PlatformNetworkModule { | |||
|
|||
// Respect model default property values. | |||
coerceInputValues = true | |||
|
|||
// Allow unquoted json keys and values to be parsed | |||
isLenient = true |
There was a problem hiding this comment.
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.
app/src/main/java/com/x8bit/bitwarden/data/platform/manager/FeatureFlagManagerImpl.kt
Outdated
Show resolved
Hide resolved
app/src/main/java/com/x8bit/bitwarden/data/platform/manager/FeatureFlagManagerImpl.kt
Outdated
Show resolved
Hide resolved
@@ -31,7 +31,7 @@ data class ConfigResponseJson( | |||
val environment: EnvironmentJson?, | |||
|
|||
@SerialName("featureStates") | |||
val featureStates: Map<String, Boolean>?, | |||
val featureStates: Map<String, String>?, |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It worked out.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Awesome!
app/src/main/java/com/x8bit/bitwarden/data/platform/manager/SdkClientManagerImpl.kt
Show resolved
Hide resolved
* @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> { |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
* @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 |
There was a problem hiding this comment.
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
…warden/android into pm-9401/server-feature-flags
* @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> { |
There was a problem hiding this comment.
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" |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
🎟️ 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
🦮 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 confirmedissue 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