Feature Flags - rajparad/firefox-ios GitHub Wiki
What is a feature flag?
Is the Firefox iOS codebase, we define a feature flag as a variable, inside a feature, that controls the status of the feature in the application.
Feature flags should logically be part of their own features, even if that's the only variable in that feature. - ie. Should not be part of a generalAppFeature, or a featureFlagFeature.
How feature flags work in Firefox iOS
Feature flags are all controlled by the FeatureFlagManager
singleton. To access the singleton, you must make a class conform to the FeatureFlaggable
protocol, which will give access to the featureFlags
variable.
class BibimbapViewModel: FeatureFlaggable {
var isNewMenuAvailable: Bool {
return featureFlags.isFeatureEnabled(.newBibimbapMenu, checking: .buildOnly)
}
}
Types of features flags
Name | Description | User Togglable |
---|---|---|
Core | Core features are features that are used for developer purposes and are not directly user impacting. | No |
Nimbus | A nimbus feature is a feature whose configuration comes from Nimbus. | Possibly |
The vast majority of feature flags should be Nimbus flags, rather than Core flags.
FeatureFlagManager
interface
The Interface | Purpose |
---|---|
isCoreFeatureEnabled(...) |
Checking where a Core feature is enabled. |
isFeatureEnabled(...) |
Checking whether a boolean based Nimbus feature is enabled. |
getCustomState<T>(...) |
Checking the status of a non-boolean based Nimbus feature. |
set(...) |
Saving a boolean based Nimbus feature user preference to UserDefaults. |
set<T: FlaggableFeatureOptions>(...) |
Saving a non-boolean based Nimbus feature user preference to UserDefaults. |
checking
parameter & how feature status is checked.
The One of the complexities of feature flags is that while Nimbus may have a default, a user may turn something off. Regardless of whether or not the user is in an experiment, their preferences should be respected. To accomplish this, the previously listed interfaces that check a feature status include a specific checking
parameter. This has three options which should cover 100% of use cases for needing to check the status of a feature.
buildOnly
- this will only check Nimbus configuration for statususerOnly
- this will check UserDefaults to see if the user has a preference. If they do, that is what will be returned. If they do not, then the Nimbus configuration is queried for statusbuildAndUser
- this will a mix of both.
How to set up a Feature Flag
Adding a feature flag to Nimbus
To add a feature to Nimbus, please read Nimbus Feature. Once this is done, add a variable to that feature named something indicative of a status. Here is an example of what that might look like
...
isEnabled:
description: >
Whether or not the feature is enabled.
type: Boolean
default: false
Adding a simple boolean feature flag in the app
Say you wanted to add a flag that controlled whether a user saw an old menu or a new menu. To add the flag in the app (for example, for the newBibimbapMenu
flag), follow these three simple steps:
- Add
case newBibimbapMenu
to theNimbusFeatureFlagID
enum. - Add this new case to the
NimbusFlaggableFeature
struct. a. If the user will have a setting to interact with for the feature, you should add this such that it returns aPrefsKeys.FeatureFlags
key, which you will have to also add. b. If the user doesn't have a setting for the feature, you should add it to thereturn nil
part of the switch statement. - In the
NimbusFeatureFlagLayer
class, you should add a case for your new feature, as well as the function it will call
...
switch featureID {
case .newBibimbapMenu:
return checkBibimbapFeature(for: featureID, from: nimbus)
...
private func checkBibimbapFeature(
for featureID: NimbusFeatureFlagID,
from nimbus: FxNimbus
) -> Bool {
let config = nimbus.features.bibimbapFeature.value()
switch featureID {
case .newBibimbapMenu: return config.newBibimbapMenu
default: return false
}
}
At this point, your work is done and you now have a feature flag that can be checked.
Adding a complex feature flag in the app
Say you wanted a flag that had more than two options. In our example, there is a morning, afternoon, and evening version of the menu. The complexity in this case is that Nimbus features must be mapped. Improvements to this will be coming in the future, but as of now, here's how to accomplish this.
- Add
case bibimbapMenuVersion
to theNimbusFeatureFlagID
enum. - Add
case bibimbapMenuVersion
to theNimbusFeatureFlagWithCustomOptionsID
enum. - Add this new case to the
NimbusFlaggableFeature
struct. a. If the user will have a setting to interact with for the feature, you should add this such that it returns aPrefsKeys.FeatureFlags
key, which you will have to also add. b. If the user doesn't have a setting for the feature, you should add it to thereturn nil
part of the switch statement. - In the
FlaggableFeatureOptions
file, create an enum for your feature flag, inheriting from String andFlaggableFeatureOptions
.
enum BibimbapMenuVersion: String, FlaggableFeatureOptions {
case morning
case afternoon
case evening
}
- In the
NimbusFeatureFlagLayer
class, you should add a case for your new feature, as well as the function it will call
...
switch featureID {
case .newBibimbapMenu:
return checkBibimbapFeature(for: featureID, from: nimbus)
...
private func checkBibimbapFeature(from nimbus: FxNimbus) -> BibimbapMenuVersion {
let config = nimbus.features.bibimbapFeature.value()
let nimbusSetting = config.bibimbapMenuVersion
switch nimbusSetting {
case .morning: return .morning
case .afternoon: return .afternoon
case .evening: return .evening
}
}
- In the
NimbusFlaggableFeature
class, undergetUserPreference
, you should add your case in the switch:
case .bibimbapMenuVersion:
return nimbusLayer.checkBibimbapFeature().rawValue
- In
NimbusFeatureFlagManager
'sgetCustomState<T>
, add your case to the switch statement.
case .bibimbapMenuVersion: return BibimbapMenuVersion(rawValue: userSetting) as? T
At this point, your work is done and you now have a feature flag that can be checked:
lazy var bibimbapMenuVersion: BibimbapMenuVersion? = featureFlags.getCustomState(for: .bibimbapMenuVersion)