Files
obsidian-vault/work/wiki/apple-browsers/feature-flags-addition.md
T

763 lines
20 KiB
Markdown

---
source: ~/DuckDuckGo/apple-browsers.git/main/.cursor/rules/feature-flags-addition.mdc
confidence: 0.9
namespace: work
last_synced: 2026-04-28
description: "Interactive pattern for adding feature flags to iOS and/or macOS with proper configuration"
alwaysApply: false
---
# Feature Flag Addition Pattern
## When This Pattern Applies
This pattern is activated when the user explicitly requests to add a feature flag, such as:
- "Add a feature flag for [feature name]"
- "Create a feature flag for [feature name] on [platform]"
- "I need a feature flag to control [feature name]"
## Overview
Adding a feature flag requires careful consideration of several factors:
1. **Platform** (iOS, macOS, or both)
2. **Source type** (how the flag is controlled)
3. **Default value** (fallback behavior)
4. **Local overriding** (debug menu access)
5. **Remote configuration** (if applicable)
## Step 1: Validate and Check for Duplicates
Before adding a new feature flag, check if a similar flag already exists:
```bash
# Search for similar flags
grep -i "case.*[searchTerm]" iOS/Core/FeatureFlag.swift
grep -i "case.*[searchTerm]" macOS/LocalPackages/FeatureFlags/Sources/FeatureFlags/FeatureFlag.swift
```
## Step 1.5: Create Asana Task (REQUIRED)
**STOP:** Before proceeding with implementation, the user must create an Asana task.
Instruct the user:
```
Please create an Asana task in the Apple Feature Flags Registry:
1. Open Asana
2. Navigate to the "Apple Feature Flags Registry" project
3. Create a new task default feature flag task
4. Copy the task URL
Paste the Asana task URL when ready to continue.
```
**This is mandatory** - all feature flags must be tracked in the Apple Feature Flags Registry.
## Step 2: Ask Clarifying Questions
### Question 1: Platform Selection
**Ask the user:**
```
Which platform(s) should this feature flag target?
a) iOS only
b) macOS only
c) Both iOS and macOS
```
**Default:** Infer from user's request. If ambiguous, ask.
### Question 2: Feature Flag Source Type
**Ask the user:**
```
What source type should this feature flag use?
a) .remoteReleasable - Can be controlled remotely in production (RECOMMENDED for most features)
• Allows gradual rollout
• Can be toggled without app updates
• Requires Privacy Config setup
b) .remoteDevelopment - Remote control in development environments only
• For testing remote config before production
• Not visible in production builds
c) .internalOnly() - Only enabled for internal users
• Always on for internal users
• Always off for external users
• No remote control
d) .disabled - Always off for everyone
• Placeholder for future features
• Code is present but inactive
Which option? (a is recommended for new features)
```
**Important:** If user selects `a` or `b`, proceed to Question 2b.
### Question 2b: Parent Feature Selection (for remote flags)
**Ask the user:**
```
For remote feature flags, we need to add a subfeature to PrivacyFeature.swift.
Which parent feature should this belong to?
Platform-specific generic:
a) macOSBrowserConfig - Generic macOS browser features
b) iOSBrowserConfig - Generic iOS browser features
Domain-specific (if applicable):
c) aiChat - AI Chat related features
d) sync - Sync related features
e) privacyPro - Privacy Pro subscription features
f) autofill - Autofill related features
g) networkProtection - VPN related features
h) duckPlayer - Duck Player features
i) dbp - Data Broker Protection features
j) htmlNewTabPage - New Tab Page features
k) maliciousSiteProtection - Malicious site protection
l) Other existing parent feature (specify name)
m) Create NEW parent feature (requires additional setup)
Which option?
```
**Guidance for selection:**
- Use platform-specific generic (a/b) when feature doesn't fit existing domains
- Use domain-specific when feature clearly belongs to an existing area
- Creating a new parent feature (m) requires:
1. Adding case to `PrivacyFeature` enum
2. Creating new `[FeatureName]Subfeature` enum
3. Coordinating with backend team for remote config
### Question 3: Default Value
**Ask the user:**
```
What should the default value be?
a) false - Feature OFF when remote config unavailable (RECOMMENDED)
• Safer option
• Opt-in behavior
• Better for new/experimental features
b) true - Feature ON when remote config unavailable
• Used when feature should be on by default
• Useful for rollback safety (can disable remotely)
• Better for stable features being gradually enabled
Which option? (a is recommended for new features)
```
**Explanation:** The default value is used when:
- Remote config is unavailable
- Flag source is local-only (`.internalOnly`, `.disabled`)
- Network is down or config fetch fails
### Question 4: Local Overriding
**Ask the user:**
```
Should this feature flag support local overriding?
a) true - Allow internal users to toggle in debug menu (RECOMMENDED)
• Enables testing both states
• Useful during development
• No effect on external users
b) false - No local override available
• Use for production pixels/metrics
• Use for security-critical flags
• Use when override would break functionality
Which option? (a is recommended unless there's a specific reason)
```
### Question 5: Asana Task Link
**REQUIRED:** Before proceeding, the user must create an Asana task.
**Instruct the user:**
```
Please create an Asana task for this feature flag:
1. Go to Asana
2. Navigate to: Apple Feature Flags Registry
3. Create a new task with:
- Title: [Feature name] feature flag
- Add any relevant context or description in the task
4. Copy the task URL
Once created, paste the Asana task URL here:
```
**Note:** All feature flags MUST have an associated Asana task in the Apple Feature Flags Registry for tracking and documentation purposes.
## Step 3: Implementation
### File Locations
- **iOS:** `iOS/Core/FeatureFlag.swift`
- **macOS:** `macOS/LocalPackages/FeatureFlags/Sources/FeatureFlags/FeatureFlag.swift`
- **Shared (remote flags):** `SharedPackages/BrowserServicesKit/Sources/BrowserServicesKit/PrivacyConfig/Features/PrivacyFeature.swift`
### 3.1: Add Feature Flag Enum Case
#### For iOS (`iOS/Core/FeatureFlag.swift`)
```swift
public enum FeatureFlag: String {
// ... existing cases ...
/// https://app.asana.com/[task-url]
case yourFeatureName
```
#### For macOS (`macOS/LocalPackages/FeatureFlags/Sources/FeatureFlags/FeatureFlag.swift`)
```swift
public enum FeatureFlag: String, CaseIterable {
// ... existing cases ...
/// https://app.asana.com/[task-url]
case yourFeatureName
```
**Naming conventions:**
- Use camelCase
- Be descriptive but concise
- Follow existing patterns in the file
### 3.2: Add to `defaultValue` Switch
Find the `defaultValue` computed property and add your case:
```swift
public var defaultValue: Bool {
switch self {
// If default is TRUE, add to this group:
case .existingTrueCase1,
.existingTrueCase2,
.yourFeatureName: // Add here if default is true
true
default:
false // All other cases default to false
}
}
```
**OR** if default is false, no change needed (handled by `default` case).
### 3.3: Add to `source` Switch
```swift
public var source: FeatureFlagSource {
switch self {
// ... other cases ...
case .yourFeatureName:
return .remoteReleasable(.subfeature(MacOSBrowserConfigSubfeature.yourFeatureName))
// OR
return .internalOnly()
// OR
return .disabled
}
}
```
**Examples by source type:**
```swift
// Remote releasable with macOS-specific subfeature
case .macOSFeature:
return .remoteReleasable(.subfeature(MacOSBrowserConfigSubfeature.macOSFeature))
// Remote releasable with iOS-specific subfeature
case .iOSFeature:
return .remoteReleasable(.subfeature(iOSBrowserConfigSubfeature.iOSFeature))
// Remote releasable with domain-specific subfeature
case .aiFeature:
return .remoteReleasable(.subfeature(AIChatSubfeature.aiFeature))
// Remote releasable with parent feature (no subfeature)
case .newParentFeature:
return .remoteReleasable(.feature(.newParentFeature))
// Remote development (testing)
case .experimentalFeature:
return .remoteDevelopment(.subfeature(MacOSBrowserConfigSubfeature.experimentalFeature))
// Internal only
case .debugFeature:
return .internalOnly()
// Always disabled (placeholder)
case .futureFeature:
return .disabled
```
### 3.4: Add to `supportsLocalOverriding` Switch
```swift
public var supportsLocalOverriding: Bool {
switch self {
case .existingOverridableFlag1,
.existingOverridableFlag2,
.yourFeatureName: // Add here if supports local override
return true
case .existingNonOverridableFlag1,
.existingNonOverridableFlag2:
return false
}
}
```
**Note:** Most flags should support local overriding for testing purposes.
### 3.5: Add Subfeature to PrivacyFeature.swift (Remote Flags Only)
**File:** `SharedPackages/BrowserServicesKit/Sources/BrowserServicesKit/PrivacyConfig/Features/PrivacyFeature.swift`
**Important Documentation Note:**
- **MacOSBrowserConfigSubfeature** and **iOSBrowserConfigSubfeature**: Include documentation comments with Asana task URLs
- **All other domain-specific subfeatures** (PrivacyPro, AIChat, Sync, DBP, etc.): NO documentation comments - just the case name
#### For macOS-specific features:
```swift
public enum MacOSBrowserConfigSubfeature: String, PrivacySubfeature {
public var parent: PrivacyFeature {
.macOSBrowserConfig
}
// ... existing cases ...
/// https://app.asana.com/[task-url]
case yourFeatureName
}
```
#### For iOS-specific features:
```swift
public enum iOSBrowserConfigSubfeature: String, PrivacySubfeature {
public var parent: PrivacyFeature {
.iOSBrowserConfig
}
// ... existing cases ...
/// https://app.asana.com/[task-url]
case yourFeatureName
}
```
#### For domain-specific features (e.g., PrivacyPro, AIChat, Sync, etc.):
**Important:** Domain-specific subfeatures should NOT include documentation comments in PrivacyFeature.swift. Keep them clean and simple with just the case name.
```swift
public enum [DomainName]Subfeature: String, PrivacySubfeature {
public var parent: PrivacyFeature { .[domainName] }
// ... existing cases ...
case yourFeatureName
}
```
**Example for PrivacyPro features:**
```swift
public enum PrivacyProSubfeature: String, Equatable, PrivacySubfeature {
public var parent: PrivacyFeature { .privacyPro }
// ... existing cases ...
case yourNewFeature
}
```
### 3.6: Creating a New Parent Feature (Advanced)
If you need to create a NEW parent feature:
**Step 1:** Add to `PrivacyFeature` enum:
```swift
public enum PrivacyFeature: String {
// ... existing cases ...
case yourNewFeature
}
```
**Step 2:** Create subfeature enum:
```swift
public enum YourNewFeatureSubfeature: String, PrivacySubfeature {
public var parent: PrivacyFeature {
.yourNewFeature
}
case firstSubfeature
case secondSubfeature
}
```
**Step 3:** Coordinate with backend team to add feature to remote Privacy Config.
### 3.7: Add Feature Flag Category (macOS Only)
**File:** `macOS/LocalPackages/FeatureFlags/Sources/FeatureFlags/FeatureFlagCategory.swift`
On macOS, feature flags can be organized into categories for better organization in the debug menu. Consider if your feature flag should be categorized.
**Available Categories:**
- `duckAI` - Duck.ai related features
- `dbp` - Personal Information Removal
- `subscription` - Subscription/Privacy Pro features
- `sync` - Sync related features
- `updates` - Update related features
- `vpn` - VPN related features
- `osSupportWarnings` - OS Support Warnings
- `other` - Default for uncategorized flags
**When to categorize:**
- If the feature belongs to a clear domain (Subscription, VPN, Sync, Duck.ai, etc.), add it to the appropriate category
- If unsure or the feature is general browser functionality, it can remain in `.other` (default)
**How to categorize:**
**Step 1:** If needed, add a new category to the enum:
```swift
public enum FeatureFlagCategory: String, CaseIterable, Comparable {
case duckAI = "Duck.ai"
// ... existing cases ...
case yourNewCategory = "Your Category Name"
// ... other cases ...
}
```
**Step 2:** Add your feature flag to the appropriate category in the `category` computed property:
```swift
extension FeatureFlag: FeatureFlagCategorization {
public var category: FeatureFlagCategory {
switch self {
// ... existing cases ...
case .yourFeatureFlag1,
.yourFeatureFlag2:
return .yourCategory
default:
return .other
}
}
}
```
**Example for Subscription features:**
```swift
case .privacyProAuthV2,
.privacyProFreeTrial,
.paidAIChat,
.tierMessagingEnabled,
.allowProTierPurchase:
return .subscription
```
**Note:** iOS does not have feature flag categories - this is macOS-specific functionality.
## Step 4: Usage in Code
### Basic Usage
```swift
// Check if feature is enabled
if featureFlagger.isFeatureOn(.yourFeatureName) {
// Feature-specific code
}
```
### With Dependency Injection
```swift
final class MyViewController {
private let featureFlagger: FeatureFlagger
init(featureFlagger: FeatureFlagger) {
self.featureFlagger = featureFlagger
}
func setupUI() {
if featureFlagger.isFeatureOn(.yourFeatureName) {
setupNewUI()
} else {
setupLegacyUI()
}
}
}
```
### iOS-specific (via AppDependencies)
```swift
if AppDependencies.shared.featureFlagger.isFeatureOn(.yourFeatureName) {
// iOS-specific feature code
}
```
### macOS-specific (via Application)
```swift
if Application.appDelegate.featureFlagger.isFeatureOn(.yourFeatureName) {
// macOS-specific feature code
}
```
## Complete Example
### Example: Add "Enhanced Bookmarks UI" feature flag for macOS
**Step 1: User Request**
```
User: "Add a feature flag for enhanced bookmarks UI on macOS"
```
**Step 2: Questions**
```
1. Platform: macOS ✓
2. Source: a) .remoteReleasable
3. Parent: a) macOSBrowserConfig
4. Default: a) false
5. Local override: a) true
6. Asana: https://app.asana.com/0/123456789/987654321
```
**Step 3: Implementation**
**File 1:** `macOS/LocalPackages/FeatureFlags/Sources/FeatureFlags/FeatureFlag.swift`
```swift
public enum FeatureFlag: String, CaseIterable {
// ... existing cases ...
/// https://app.asana.com/0/123456789/987654321
case enhancedBookmarksUI
}
extension FeatureFlag: FeatureFlagDescribing {
public var defaultValue: Bool {
switch self {
// ... existing true cases ...
default:
false // enhancedBookmarksUI uses default false
}
}
public var supportsLocalOverriding: Bool {
switch self {
case .existingFlag1,
.existingFlag2,
.enhancedBookmarksUI: // ← Added here
return true
// ... rest of cases
}
}
public var source: FeatureFlagSource {
switch self {
// ... other cases ...
case .enhancedBookmarksUI:
return .remoteReleasable(.subfeature(MacOSBrowserConfigSubfeature.enhancedBookmarksUI))
}
}
}
```
**File 2:** `SharedPackages/BrowserServicesKit/Sources/BrowserServicesKit/PrivacyConfig/Features/PrivacyFeature.swift`
```swift
public enum MacOSBrowserConfigSubfeature: String, PrivacySubfeature {
public var parent: PrivacyFeature {
.macOSBrowserConfig
}
// ... existing cases ...
/// https://app.asana.com/0/123456789/987654321
case enhancedBookmarksUI
}
```
## Anti-Patterns to Avoid
### ❌ DON'T: Add feature flag without Asana task
```swift
// ❌ BAD: No tracking or documentation
case mysteriousFeature
```
```swift
// ✅ GOOD: Clear documentation with Asana task from Apple Feature Flags Registry
/// https://app.asana.com/0/123456789/987654321
case tabGrouping
```
**CRITICAL:** Every feature flag MUST have an Asana task in the Apple Feature Flags Registry. This is not optional.
### ❌ DON'T: Use generic names
```swift
// ❌ BAD: Too vague
case newFeature
case experiment1
case testFlag
```
```swift
// ✅ GOOD: Descriptive names
case improvedTabSwitcher
case aiChatSidebar
case passwordAutofillV2
```
### ❌ DON'T: Forget to add to all required switches
```swift
// ❌ BAD: Missing from supportsLocalOverriding
case newFeature // Added to enum
// source: return .remoteReleasable(...)
// defaultValue: false (via default)
// supportsLocalOverriding: ❌ MISSING!
```
### ❌ DON'T: Use wrong parent for domain-specific features
```swift
// ❌ BAD: AI feature in generic config
case aiNewFeature:
return .remoteReleasable(.subfeature(MacOSBrowserConfigSubfeature.aiNewFeature))
// ✅ GOOD: AI feature in AI domain
case aiNewFeature:
return .remoteReleasable(.subfeature(AIChatSubfeature.aiNewFeature))
```
### ❌ DON'T: Add to iOS when feature is macOS-only (or vice versa)
```swift
// ❌ BAD: Adding macOS-specific flag to iOS
// In iOS/Core/FeatureFlag.swift:
case macOSOnlyFeature // This doesn't make sense!
```
### ❌ DON'T: Add documentation comments to domain-specific subfeatures in PrivacyFeature.swift
```swift
// ❌ BAD: Adding comments to domain-specific subfeatures (e.g., PrivacyPro, AIChat, Sync)
public enum PrivacyProSubfeature: String, Equatable, PrivacySubfeature {
public var parent: PrivacyFeature { .privacyPro }
/// https://app.asana.com/...
case tierMessagingEnabled // ❌ Don't add ANY comments here!
}
```
```swift
// ✅ GOOD: Domain-specific subfeatures without comments
public enum PrivacyProSubfeature: String, Equatable, PrivacySubfeature {
public var parent: PrivacyFeature { .privacyPro }
case tierMessagingEnabled // ✅ Clean and simple
case allowProTierPurchase
}
```
**Note:** Only `MacOSBrowserConfigSubfeature` and `iOSBrowserConfigSubfeature` should have documentation comments. All other domain-specific subfeatures (PrivacyPro, AIChat, Sync, DBP, etc.) should be kept clean without comments.
## Testing Your Feature Flag
### Manual Testing
1. **Internal user testing:**
- Enable internal user mode
- Access debug menu to toggle flag
- Test both on/off states
2. **Production simulation:**
- Disable internal user mode
- Verify default value behavior
- Test without remote config
### Debug Menu Access
**macOS:**
- Develop menu → Feature Flags
- Toggle individual flags
- Changes persist across sessions
**iOS:**
- Settings → Debug → Feature Flags
- Toggle individual flags
- Changes persist across sessions
## Remote Configuration (Next Steps)
After adding the feature flag code, coordinate with backend team to:
1. Add feature to Privacy Configuration JSON
2. Set initial state (enabled/disabled/internal)
3. Configure rollout percentage (if gradual rollout)
4. Set up A/B test cohorts (if applicable)
Example Privacy Config structure:
```json
{
"macOSBrowserConfig": {
"state": "enabled",
"features": {
"enhancedBookmarksUI": {
"state": "internal",
"rollout": {
"steps": [
{ "percent": 10 }
]
}
}
}
}
}
```
## Summary Checklist
When adding a feature flag, ensure you:
- [ ] Checked for existing similar flags
- [ ] **Created Asana task in Apple Feature Flags Registry (REQUIRED)**
- [ ] Asked all required questions
- [ ] Added enum case with Asana task link
- [ ] Updated `defaultValue` switch (if non-default)
- [ ] Updated `source` switch
- [ ] Updated `supportsLocalOverriding` switch
- [ ] Added subfeature to PrivacyFeature.swift (if remote)
- [ ] Added to appropriate category in FeatureFlagCategory.swift (macOS only, if applicable)
- [ ] Used descriptive naming
- [ ] Tested in debug menu
- [ ] Coordinated with backend (if remote)
## Reference Documentation
For more information, see:
- `feature-flags.md` - Type-safe feature flag patterns
- `abn-experiment-framework.md` - A/B testing with feature flags
- `SharedPackages/BrowserServicesKit/Sources/BrowserServicesKit/FeatureFlagger/FeatureFlagger.swift` - Core implementation