379 lines
10 KiB
Markdown
379 lines
10 KiB
Markdown
---
|
|
source: ~/DuckDuckGo/apple-browsers.git/main/.cursor/rules/feature-flags.mdc
|
|
confidence: 0.9
|
|
namespace: work
|
|
last_synced: 2026-04-28
|
|
alwaysApply: false
|
|
---
|
|
|
|
# Feature Flag Patterns
|
|
|
|
## Type-Safe Feature Flags
|
|
Use enum-based feature flags with protocols for type safety:
|
|
|
|
```swift
|
|
// ✅ CORRECT - Type-safe feature flags
|
|
protocol FeatureFlag: RawRepresentable where RawValue == String {
|
|
var defaultValue: Bool { get }
|
|
var description: String { get }
|
|
}
|
|
|
|
enum UIFeatureFlag: String, FeatureFlag {
|
|
case newTabPageRedesign = "new_tab_page_redesign"
|
|
case advancedPrivacySettings = "advanced_privacy_settings"
|
|
case voiceSearch = "voice_search"
|
|
case experimentalUI = "experimental_ui"
|
|
|
|
var defaultValue: Bool {
|
|
switch self {
|
|
case .newTabPageRedesign: return false
|
|
case .advancedPrivacySettings: return true
|
|
case .voiceSearch: return false
|
|
case .experimentalUI: return false
|
|
}
|
|
}
|
|
|
|
var description: String {
|
|
switch self {
|
|
case .newTabPageRedesign:
|
|
return "Enable redesigned new tab page"
|
|
case .advancedPrivacySettings:
|
|
return "Show advanced privacy settings"
|
|
case .voiceSearch:
|
|
return "Enable voice search functionality"
|
|
case .experimentalUI:
|
|
return "Enable experimental UI components"
|
|
}
|
|
}
|
|
}
|
|
|
|
enum NetworkFeatureFlag: String, FeatureFlag {
|
|
case networkProtectionV2 = "network_protection_v2"
|
|
case enhancedBlocking = "enhanced_blocking"
|
|
|
|
var defaultValue: Bool {
|
|
switch self {
|
|
case .networkProtectionV2: return false
|
|
case .enhancedBlocking: return true
|
|
}
|
|
}
|
|
|
|
var description: String {
|
|
switch self {
|
|
case .networkProtectionV2:
|
|
return "Enable Network Protection V2"
|
|
case .enhancedBlocking:
|
|
return "Enhanced content blocking"
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
## FeatureFlagger Protocol Implementation
|
|
Extend the existing FeatureFlagger with type-safe methods:
|
|
|
|
```swift
|
|
// ✅ CORRECT - Type-safe FeatureFlagger extension
|
|
extension FeatureFlagger {
|
|
func isEnabled<Flag: FeatureFlag>(_ flag: Flag) -> Bool {
|
|
return isFeatureOn(flag.rawValue) ?? flag.defaultValue
|
|
}
|
|
|
|
func setEnabled<Flag: FeatureFlag>(_ enabled: Bool, for flag: Flag) {
|
|
setFeatureOn(flag.rawValue, enabled: enabled)
|
|
}
|
|
}
|
|
|
|
// Usage in code
|
|
final class FeatureViewModel: ObservableObject {
|
|
private let featureFlagger: FeatureFlagger
|
|
|
|
init(featureFlagger: FeatureFlagger) {
|
|
self.featureFlagger = featureFlagger
|
|
}
|
|
|
|
func loadContent() {
|
|
if featureFlagger.isEnabled(UIFeatureFlag.newTabPageRedesign) {
|
|
loadNewDesign()
|
|
} else {
|
|
loadLegacyDesign()
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
## Feature Flag ViewModifier
|
|
Create SwiftUI modifiers for conditional UI:
|
|
|
|
```swift
|
|
// ✅ ADVANCED - SwiftUI feature flag modifier
|
|
struct FeatureFlagModifier<Flag: FeatureFlag>: ViewModifier {
|
|
let flag: Flag
|
|
let featureFlagger: FeatureFlagger
|
|
let fallback: () -> AnyView
|
|
|
|
func body(content: Content) -> some View {
|
|
if featureFlagger.isEnabled(flag) {
|
|
content
|
|
} else {
|
|
fallback()
|
|
}
|
|
}
|
|
}
|
|
|
|
extension View {
|
|
func featureFlag<Flag: FeatureFlag>(
|
|
_ flag: Flag,
|
|
featureFlagger: FeatureFlagger,
|
|
@ViewBuilder fallback: @escaping () -> some View = { EmptyView() }
|
|
) -> some View {
|
|
modifier(FeatureFlagModifier(
|
|
flag: flag,
|
|
featureFlagger: featureFlagger,
|
|
fallback: { AnyView(fallback()) }
|
|
))
|
|
}
|
|
}
|
|
|
|
// Usage
|
|
struct ContentView: View {
|
|
@Environment(\.dependencies) var dependencies
|
|
|
|
var body: some View {
|
|
VStack {
|
|
NewFeatureView()
|
|
.featureFlag(UIFeatureFlag.experimentalUI,
|
|
featureFlagger: dependencies.featureFlagger) {
|
|
LegacyFeatureView()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
## Feature Flag Property Wrapper
|
|
Create a property wrapper for reactive feature flags:
|
|
|
|
```swift
|
|
// ✅ ADVANCED - Reactive feature flag property wrapper
|
|
@propertyWrapper
|
|
struct FeatureFlagState<Flag: FeatureFlag>: DynamicProperty {
|
|
@ObservedObject private var flagger: ObservableFeatureFlagger
|
|
private let flag: Flag
|
|
|
|
var wrappedValue: Bool {
|
|
get { flagger.isEnabled(flag) }
|
|
nonmutating set { flagger.setEnabled(newValue, for: flag) }
|
|
}
|
|
|
|
var projectedValue: Binding<Bool> {
|
|
Binding(
|
|
get: { wrappedValue },
|
|
set: { wrappedValue = $0 }
|
|
)
|
|
}
|
|
|
|
init(_ flag: Flag) {
|
|
self.flag = flag
|
|
self._flagger = ObservedObject(wrappedValue: ObservableFeatureFlagger.shared)
|
|
}
|
|
}
|
|
|
|
// Usage in SwiftUI views
|
|
struct SettingsView: View {
|
|
@FeatureFlagState(UIFeatureFlag.voiceSearch) var voiceSearchEnabled
|
|
@FeatureFlagState(UIFeatureFlag.experimentalUI) var experimentalUIEnabled
|
|
|
|
var body: some View {
|
|
Form {
|
|
Toggle("Voice Search", isOn: $voiceSearchEnabled)
|
|
Toggle("Experimental UI", isOn: $experimentalUIEnabled)
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
## A/B Testing Integration
|
|
Integrate feature flags with A/B testing:
|
|
|
|
```swift
|
|
// ✅ ADVANCED - A/B testing with feature flags
|
|
enum ABTestVariant: String, CaseIterable {
|
|
case control = "control"
|
|
case variantA = "variant_a"
|
|
case variantB = "variant_b"
|
|
|
|
var displayName: String {
|
|
switch self {
|
|
case .control: return "Control Group"
|
|
case .variantA: return "Variant A"
|
|
case .variantB: return "Variant B"
|
|
}
|
|
}
|
|
}
|
|
|
|
protocol ABTestFeatureFlag: FeatureFlag {
|
|
var variants: [ABTestVariant] { get }
|
|
var currentVariant: ABTestVariant { get }
|
|
}
|
|
|
|
enum ExperimentalFeatureFlag: String, ABTestFeatureFlag {
|
|
case newOnboardingFlow = "new_onboarding_flow"
|
|
case redesignedSearch = "redesigned_search"
|
|
|
|
var defaultValue: Bool { true }
|
|
|
|
var description: String {
|
|
switch self {
|
|
case .newOnboardingFlow: return "New onboarding flow experiment"
|
|
case .redesignedSearch: return "Redesigned search interface experiment"
|
|
}
|
|
}
|
|
|
|
var variants: [ABTestVariant] {
|
|
[.control, .variantA, .variantB]
|
|
}
|
|
|
|
var currentVariant: ABTestVariant {
|
|
// Get variant from A/B testing service
|
|
return ABTestingService.shared.getVariant(for: self.rawValue)
|
|
}
|
|
}
|
|
|
|
// Usage with variants
|
|
func configureOnboarding() {
|
|
let experiment = ExperimentalFeatureFlag.newOnboardingFlow
|
|
|
|
switch experiment.currentVariant {
|
|
case .control:
|
|
showLegacyOnboarding()
|
|
case .variantA:
|
|
showNewOnboardingVariantA()
|
|
case .variantB:
|
|
showNewOnboardingVariantB()
|
|
}
|
|
|
|
// Track experiment exposure
|
|
PixelFiring.fire(.experimentExposure(experiment.rawValue, experiment.currentVariant.rawValue))
|
|
}
|
|
```
|
|
|
|
## Remote Feature Flags
|
|
Integrate with remote configuration:
|
|
|
|
```swift
|
|
// ✅ ADVANCED - Remote feature flag management
|
|
protocol RemoteFeatureFlag: FeatureFlag {
|
|
var remoteKey: String { get }
|
|
var localOverrideKey: String? { get }
|
|
}
|
|
|
|
extension RemoteFeatureFlag {
|
|
var remoteKey: String { rawValue }
|
|
var localOverrideKey: String? { "local_override_\(rawValue)" }
|
|
}
|
|
|
|
enum RemoteUIFeatureFlag: String, RemoteFeatureFlag {
|
|
case serverDrivenUI = "server_driven_ui"
|
|
case dynamicThemes = "dynamic_themes"
|
|
|
|
var defaultValue: Bool { false }
|
|
|
|
var description: String {
|
|
switch self {
|
|
case .serverDrivenUI: return "Server-driven UI configuration"
|
|
case .dynamicThemes: return "Dynamic theme system"
|
|
}
|
|
}
|
|
}
|
|
|
|
final class RemoteFeatureFlagger: FeatureFlagger {
|
|
private let remoteConfig: RemoteConfigProtocol
|
|
private let localDefaults: UserDefaults
|
|
|
|
init(remoteConfig: RemoteConfigProtocol, localDefaults: UserDefaults) {
|
|
self.remoteConfig = remoteConfig
|
|
self.localDefaults = localDefaults
|
|
}
|
|
|
|
func isEnabled<Flag: RemoteFeatureFlag>(_ flag: Flag) -> Bool {
|
|
// Check local override first
|
|
if let overrideKey = flag.localOverrideKey,
|
|
let localOverride = localDefaults.object(forKey: overrideKey) as? Bool {
|
|
return localOverride
|
|
}
|
|
|
|
// Check remote config
|
|
if let remoteValue = remoteConfig.boolValue(for: flag.remoteKey) {
|
|
return remoteValue
|
|
}
|
|
|
|
// Fall back to default
|
|
return flag.defaultValue
|
|
}
|
|
|
|
func setLocalOverride<Flag: RemoteFeatureFlag>(_ enabled: Bool?, for flag: Flag) {
|
|
guard let overrideKey = flag.localOverrideKey else { return }
|
|
|
|
if let enabled = enabled {
|
|
localDefaults.set(enabled, forKey: overrideKey)
|
|
} else {
|
|
localDefaults.removeObject(forKey: overrideKey)
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
## Debug Feature Flag Interface
|
|
Create debug interface for testing:
|
|
|
|
```swift
|
|
// ✅ DEBUG - Feature flag debug interface
|
|
#if DEBUG
|
|
struct FeatureFlagDebugView: View {
|
|
@StateObject private var debugFlags = DebugFeatureFlags()
|
|
|
|
var body: some View {
|
|
NavigationView {
|
|
List {
|
|
Section("UI Features") {
|
|
ForEach(UIFeatureFlag.allCases, id: \.rawValue) { flag in
|
|
FeatureFlagRow(flag: flag, debugFlags: debugFlags)
|
|
}
|
|
}
|
|
|
|
Section("Network Features") {
|
|
ForEach(NetworkFeatureFlag.allCases, id: \.rawValue) { flag in
|
|
FeatureFlagRow(flag: flag, debugFlags: debugFlags)
|
|
}
|
|
}
|
|
}
|
|
.navigationTitle("Feature Flags")
|
|
}
|
|
}
|
|
}
|
|
|
|
struct FeatureFlagRow<Flag: FeatureFlag>: View where Flag: CaseIterable {
|
|
let flag: Flag
|
|
@ObservedObject var debugFlags: DebugFeatureFlags
|
|
|
|
var body: some View {
|
|
Toggle(isOn: Binding(
|
|
get: { debugFlags.isEnabled(flag) },
|
|
set: { debugFlags.setEnabled($0, for: flag) }
|
|
)) {
|
|
VStack(alignment: .leading) {
|
|
Text(flag.rawValue)
|
|
.font(.headline)
|
|
Text(flag.description)
|
|
.font(.caption)
|
|
.foregroundColor(.secondary)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
```
|
|
|
|
See `analytics-patterns.md` for pixel firing patterns and `configuration-management.md` for advanced configuration management.
|