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

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:

  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:

# 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)

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 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:

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

  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:

{
  "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