763 lines
20 KiB
Markdown
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
|