20 KiB
source, confidence, namespace, last_synced, description, alwaysApply
| source | confidence | namespace | last_synced | description | alwaysApply |
|---|---|---|---|---|---|
| ~/DuckDuckGo/apple-browsers.git/main/.cursor/rules/feature-flags-addition.mdc | 0.9 | work | 2026-04-28 | Interactive pattern for adding feature flags to iOS and/or macOS with proper configuration | 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:
- Platform (iOS, macOS, or both)
- Source type (how the flag is controlled)
- Default value (fallback behavior)
- Local overriding (debug menu access)
- Remote configuration (if applicable)
Step 1: Validate and Check for Duplicates
Before adding a new feature flag, check if a similar flag already exists:
# 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:
- Adding case to
PrivacyFeatureenum - Creating new
[FeatureName]Subfeatureenum - Coordinating with backend team for remote config
- Adding case to
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)
public enum FeatureFlag: String {
// ... existing cases ...
/// https://app.asana.com/[task-url]
case yourFeatureName
For macOS (macOS/LocalPackages/FeatureFlags/Sources/FeatureFlags/FeatureFlag.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:
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
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:
// 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
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:
public enum MacOSBrowserConfigSubfeature: String, PrivacySubfeature {
public var parent: PrivacyFeature {
.macOSBrowserConfig
}
// ... existing cases ...
/// https://app.asana.com/[task-url]
case yourFeatureName
}
For iOS-specific features:
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.
public enum [DomainName]Subfeature: String, PrivacySubfeature {
public var parent: PrivacyFeature { .[domainName] }
// ... existing cases ...
case yourFeatureName
}
Example for PrivacyPro features:
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:
public enum PrivacyFeature: String {
// ... existing cases ...
case yourNewFeature
}
Step 2: Create subfeature enum:
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 featuresdbp- Personal Information Removalsubscription- Subscription/Privacy Pro featuressync- Sync related featuresupdates- Update related featuresvpn- VPN related featuresosSupportWarnings- OS Support Warningsother- 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:
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:
extension FeatureFlag: FeatureFlagCategorization {
public var category: FeatureFlagCategory {
switch self {
// ... existing cases ...
case .yourFeatureFlag1,
.yourFeatureFlag2:
return .yourCategory
default:
return .other
}
}
}
Example for Subscription features:
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
// Check if feature is enabled
if featureFlagger.isFeatureOn(.yourFeatureName) {
// Feature-specific code
}
With Dependency Injection
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)
if AppDependencies.shared.featureFlagger.isFeatureOn(.yourFeatureName) {
// iOS-specific feature code
}
macOS-specific (via Application)
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
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
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
// ❌ BAD: No tracking or documentation
case mysteriousFeature
// ✅ 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
// ❌ BAD: Too vague
case newFeature
case experiment1
case testFlag
// ✅ GOOD: Descriptive names
case improvedTabSwitcher
case aiChatSidebar
case passwordAutofillV2
❌ DON'T: Forget to add to all required switches
// ❌ 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
// ❌ 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)
// ❌ 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
// ❌ 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!
}
// ✅ 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
-
Internal user testing:
- Enable internal user mode
- Access debug menu to toggle flag
- Test both on/off states
-
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:
- Add feature to Privacy Configuration JSON
- Set initial state (enabled/disabled/internal)
- Configure rollout percentage (if gradual rollout)
- Set up A/B test cohorts (if applicable)
Example Privacy Config structure:
{
"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
defaultValueswitch (if non-default) - Updated
sourceswitch - Updated
supportsLocalOverridingswitch - 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 patternsabn-experiment-framework.md- A/B testing with feature flagsSharedPackages/BrowserServicesKit/Sources/BrowserServicesKit/FeatureFlagger/FeatureFlagger.swift- Core implementation