[2026-04-28] Add apple-browsers .cursor rules (43 files) + executor v2 context

This commit is contained in:
Alexey Martemyanov
2026-04-28 00:30:19 +06:00
parent dac59f4cff
commit 639338eda3
44 changed files with 18230 additions and 0 deletions
+29
View File
@@ -0,0 +1,29 @@
---
confidence: 1.0
namespace: work
last_updated: 2026-04-28
---
# Apple Browsers — Cursor Rules Index
Copies of `.cursor/rules/*.mdc` from `~/DuckDuckGo/apple-browsers.git/main/.cursor/rules/`.
Originals stay in place (Cursor reads them from there). These copies are for agent search via obsidian-mcp.
**Do not edit here** — edit originals in the repo and re-sync.
## Files
| Rule | Purpose |
|------|---------|
| architecture.mdc | Overall app architecture patterns |
| code-style.mdc | Swift code style guidelines |
| development-commands.mdc | Build commands, Xcode setup |
| testing.mdc | Unit test patterns |
| ui-testing.mdc | UI test patterns, UITestCase |
| pull-request.mdc | PR workflow (Executor overrides confirmation gates) |
| feature-flags.mdc | Feature flag usage |
| anti-patterns.mdc | What NOT to do |
| general.mdc | General guidelines |
| branch-naming-conventions.mdc | Branch naming (executor/* prefix for Executor) |
See full list: 43 files total.
@@ -0,0 +1,606 @@
---
source: ~/DuckDuckGo/apple-browsers.git/main/.cursor/rules/abn-experiment-framework.mdc
confidence: 0.9
namespace: work
last_synced: 2026-04-28
alwaysApply: false
---
# A/B/N Experiment Framework
## Overview
The DuckDuckGo browser includes a comprehensive A/B/N experiment framework that enables data-driven feature testing across iOS and macOS platforms. This framework allows you to safely experiment with new ideas while maintaining control groups and measuring impact.
**Reference**: Video of knowledge sharing session: ✓ A/B/N Experiment Framework
## What is A/B/N Testing?
An **A/B/N test** is a method of experimenting with multiple variants of a feature (A, B, ... N) to determine which performs better. It's like traditional A/B testing but scaled up to support more than two groups.
### Use Cases
You can use this framework to explore:
- **UI/UX variations**: Whether blue buttons outperform green buttons
- **Content experiments**: If showing pictures of cats 🐱 or dogs 🐶 boosts user retention
- **Feature comparisons**: Different implementations of the same functionality
- **Performance optimization**: Testing various algorithms or approaches
### When to Use A/B/N Testing
✅ **Use for**:
- Comparing user behavior between two or more feature variants
- Validating hypotheses before rolling out changes to all users
- Safely experimenting with new ideas while maintaining control groups
- Measurable, impactful decisions with clear success metrics
❌ **Don't use for**:
- Simple bug fixes or obvious improvements
- Changes without measurable impact
- Features that can't be easily reversed
⚠️ **Note**: Not every change needs a test—reserve it for measurable, impactful decisions. This is typically decided in collaboration with ODRIs and Data Science.
## Framework Architecture
### Remote Configuration System
A/B/N experiments are supported via **remote configuration** on both macOS and iOS:
- **Sub-features** are used for experiments
- **Parent features** group related experiments
- **Cohorts** define the different variants
- **Weights** control user distribution
- **Targets** allow locale-based segmentation
## Configuration Setup
### 1. Privacy Config Structure
Experiments are defined in the Privacy Configuration with this structure:
```json
"amazingMacroFeature": {
"state": "enabled",
"features": {
"petsPictures": {
"state": "enabled",
"description": "This feature shows users pictures of cute pets",
"targets": [
{ "localeLanguage": "en", "localeCountry": "US" },
{ "localeLanguage": "fr", "localeCountry": "CA" }
],
"cohorts": [
{ "name": "cats", "weight": 1 },
{ "name": "dogs", "weight": 1 }
]
}
}
}
```
### Configuration Elements
#### **State Options**
- `enabled`: Visible to all users
- `internal`: Visible only to internal users
- `disabled`: Hidden from all users
#### **Description**
Explains the experiment's purpose for team reference.
#### **Targets** (Optional)
Specify user segments based on locale:
```json
"targets": [
{ "localeLanguage": "en", "localeCountry": "US" },
{ "localeLanguage": "fr", "localeCountry": "CA" }
]
```
#### **Cohorts**
Define experiment variants:
- `name`: Cohort identifier (e.g., "cats", "dogs")
- `weight`: Probability of assignment (normally 1 or 0)
## Client Implementation
### Step 1: Add Feature to PrivacyFeature (BSK)
#### Check for Existing Features
```swift
// In PrivacyFeature enum, check if parent feature exists
public enum PrivacyFeature: String, CaseIterable {
case amazingMacroFeature
// ... other features
}
// Add sub-feature to existing enum or create new one
public enum AmazingMacroFeatureSubfeatures: String, CaseIterable {
case petsPictures
// ... other sub-features
}
```
#### Add New Features
If the parent feature doesn't exist:
1. Add it to the `PrivacyFeature` enum
2. Create a new sub-features enum
3. Add your sub-feature to the enum
### Step 2: Define Feature Flag
Add your experiment to the local `FeatureFlag` enum:
```swift
public enum FeatureFlag: String, CaseIterable {
case debugMenu
case sslCertificatesBypass
case maliciousSiteProtection
// ... existing flags
case petsPictures
public var cohortType: (any FeatureFlagCohortDescribing.Type)? {
switch self {
case .petsPictures:
return PetsPicturesCohort.self
default:
return nil
}
}
public enum PetsPicturesCohort: String, FeatureFlagCohortDescribing {
case cats
case dogs
}
public var source: FeatureFlagSource {
switch self {
// ... other cases
case .petsPictures:
return .remoteReleasable(.subfeature(AmazingMacroFeatureSubfeatures.petsPictures))
}
}
public var supportsLocalOverriding: Bool {
switch self {
// ... other cases
case .petsPictures:
return true
}
}
}
```
#### Key Properties
**`cohortType`**: Links to experiment cohorts enum
- Must conform to `String, FeatureFlagCohortDescribing`
- Defines available variants (cats, dogs)
**`source`**: Defines feature flag toggle location
- `.disabled`: Feature is off
- `.internalOnly`: Internal users only
- `.remoteDevelopment`: Development testing
- `.remoteReleasable`: Production experiments
**`supportsLocalOverriding`**: Enables debug menu overrides
- `true`: Internal users can override cohort assignment
- `false`: No local overrides allowed
### Step 3: Implement Cohort Decision Logic
Request cohort assignment when needed:
```swift
// ✅ CORRECT: Request cohort only when decision is needed
guard let petsCohort = Application.appDelegate.featureFlagger.resolveCohort(for: .petsPictures) as? FeatureFlag.PetsPicturesCohort else {
return
}
switch petsCohort {
case .cats:
showCatPics()
case .dogs:
showDogPics()
}
```
⚠️ **Important**: Only request the cohort at the moment it's needed! This ensures accurate assignment and avoids data dilution.
### Step 4: Handle Dynamic Cohort Changes
For features that need to respond to runtime cohort changes:
```swift
private func subscribeToPetsExperimentFeatureFlagChanges() {
guard let overridesHandler = Application.appDelegate.featureFlagger.localOverrides?.actionHandler as? FeatureFlagOverridesPublishingHandler<FeatureFlag> else {
return
}
overridesHandler.experimentFlagDidChangePublisher
.filter { $0.0 == .petsPictures }
.sink { (_, cohort) in
guard let newCohort = FeatureFlag.PetsPicturesCohort.cohort(for: cohort) else { return }
switch newCohort {
case .cats:
// IMMEDIATELY SHOW A CUTE CAT
case .dogs:
// IMMEDIATELY SHOW A CUTE DOG
}
}
.store(in: &cancellables)
}
```
## Metrics and Analytics
### Default Retention Metrics
The framework automatically tracks core engagement metrics without additional configuration:
#### 1. Enrollment Pixel
Tracks when users join experiments:
```
Pixel Name: experiment_enroll_{experimentName}_{cohortName}
Parameters:
- enrollmentDate: Date in ET (YYYY-MM-DD format)
```
#### 2. Search Activity Pixels
Monitors search behavior post-enrollment:
```
Pixel Name: experiment_metrics_{experimentName}_{cohortName}
Parameters:
- metric: "search"
- conversionWindowDays: Time frame (e.g., "1", "5-7")
- value: Number of searches performed
- enrollmentDate: Enrollment date (YYYY-MM-DD)
```
**Predefined Tracking Windows**:
- **Value 1**: Conversion windows [1, 2, 3, 4, 5, 6, 7, 5-7]
- **Values 4, 6, 11, 21, 30**: Conversion windows [5-7, 8-15]
#### 3. App Usage Pixels
Tracks app engagement (launches, foregrounds, etc.):
```
Pixel Name: experiment_metrics_{experimentName}_{cohortName}
Parameters:
- metric: "app_use"
- conversionWindowDays: Time frame (e.g., "0", "1", "5-7")
- value: Number of app usage events
- enrollmentDate: Enrollment date (YYYY-MM-DD)
```
**Predefined Tracking Windows**:
- **Value 1**: Conversion windows [0, 1, 2, 3, 4, 5, 6, 7, 5-7]
- **Values 4, 6, 11, 21, 30**: Conversion windows [5-7, 8-15]
### Custom Metrics
Track experiment-specific behaviors using `PixelExperimentKit`:
#### Import Required Framework
```swift
import PixelExperimentKit
```
#### Fire Custom Metric Pixels
```swift
// Method 1: Direct pixel firing
func fireExperimentPixel(
for subfeatureID: SubfeatureID,
metric: String,
conversionWindowDays: ConversionWindow,
value: String
)
// Method 2: Threshold-based pixel firing
func fireExperimentPixelIfThresholdReached(
for subfeatureID: SubfeatureID,
metric: String,
conversionWindowDays: ConversionWindow,
threshold: NumberOfCalls
)
```
#### Example: Button Click Tracking
```swift
// Track immediate button clicks
PixelKit.fireExperimentPixel(
for: "petsPictures",
metric: "adopt_button_clicks",
conversionWindowDays: 1...1,
value: "1"
)
// Track threshold-based clicks (fires after 5 clicks)
PixelKit.fireExperimentPixelIfThresholdReached(
for: "petsPictures",
metric: "button_clicks",
conversionWindowDays: 1...7,
threshold: 5
)
```
#### Custom Metric Examples
```swift
// Form completion tracking
PixelKit.fireExperimentPixel(
for: "petsPictures",
metric: "form_completed",
conversionWindowDays: 1...1,
value: "true"
)
// Feature adoption tracking
PixelKit.fireExperimentPixel(
for: "petsPictures",
metric: "set_as_default",
conversionWindowDays: 1...7,
value: "true"
)
// Error tracking
PixelKit.fireExperimentPixel(
for: "petsPictures",
metric: "error_occurred",
conversionWindowDays: 1...1,
value: "network_timeout"
)
```
## Experiment Management
### Stop Accepting New Users to a Cohort
To prevent new enrollments while maintaining existing users:
```json
"cohorts": [
{ "name": "cats", "weight": 0 }, // No new users
{ "name": "dogs", "weight": 1 } // All new users go here
]
```
**Behavior**:
- **Existing users**: Remain in their assigned cohorts
- **New users**: Only assigned to cohorts with weight > 0
- **No enrollment**: Set all weights to 0
### Remove Users from a Cohort
To completely remove a cohort and reassign users:
```json
// Before: Two cohorts
"cohorts": [
{ "name": "cats", "weight": 1 },
{ "name": "dogs", "weight": 1 }
]
// After: Cats cohort removed
"cohorts": [
{ "name": "dogs", "weight": 1 }
]
```
**Behavior**:
- **Existing users**: Automatically reassigned to remaining cohorts
- **New users**: Assigned to available cohorts
- **No cohorts**: No users enrolled if all cohorts removed
### Stop an Experiment
Complete cleanup requires both code and configuration changes:
#### 1. Clean Up Code
```swift
// ❌ Remove experiment-specific logic
// switch petsCohort {
// case .cats:
// showCatPics()
// case .dogs:
// showDogPics()
// }
// ✅ Implement final chosen behavior
showDogPics() // Or whatever was determined to be the winner
```
#### 2. Update Configuration
```json
// Remove cohorts or entire sub-feature
"amazingMacroFeature": {
"state": "enabled",
"features": {
// "petsPictures": { ... } // Remove entire sub-feature
}
}
```
⚠️ **Critical**: Always remove code before removing configuration to prevent runtime errors.
## Development and Testing
### Feature Flag Sources for Development
Control experiment access during development:
#### Internal-Only Testing
```swift
public var source: FeatureFlagSource {
switch self {
case .petsPictures:
// Only internal users see this experiment
return .internalOnly(.subfeature(AmazingMacroFeatureSubfeatures.petsPictures))
}
}
```
#### Development Random Assignment
```swift
public var source: FeatureFlagSource {
switch self {
case .petsPictures:
// Internal users get random assignment based on remote config
return .remoteDevelopment(.subfeature(AmazingMacroFeatureSubfeatures.petsPictures))
}
}
```
#### Production Release
```swift
public var source: FeatureFlagSource {
switch self {
case .petsPictures:
// All users participate based on remote config
return .remoteReleasable(.subfeature(AmazingMacroFeatureSubfeatures.petsPictures))
}
}
```
### Local Overrides for Testing
Enable debug menu overrides for internal testing:
```swift
public var supportsLocalOverriding: Bool {
switch self {
case .petsPictures:
return true // Enables debug menu cohort selection
}
}
```
**Usage**:
1. Open debug menu in internal builds
2. Navigate to "Feature Flag Overrides"
3. Select specific cohort for testing
4. App immediately reflects cohort change
## Best Practices
### ✅ DO
```swift
// Request cohort only when needed
guard let cohort = featureFlagger.resolveCohort(for: .petsPictures) as? FeatureFlag.PetsPicturesCohort else { return }
// Use meaningful cohort names
public enum PetsPicturesCohort: String, FeatureFlagCohortDescribing {
case cats // Clear, descriptive names
case dogs
}
// Track relevant custom metrics
PixelKit.fireExperimentPixel(
for: "petsPictures",
metric: "adoption_success",
conversionWindowDays: 1...7,
value: "true"
)
// Clean up after experiments
// Remove cohort logic and implement winning variant
```
### ❌ DON'T
```swift
// Don't request cohorts unnecessarily
let cohort = featureFlagger.resolveCohort(for: .petsPictures) // ❌ Called too early
// Don't use unclear cohort names
public enum TestCohort: String {
case a // ❌ Unclear what this represents
case b
}
// Don't forget to clean up
// Leaving experiment code after completion ❌
// Don't remove config before code
// Can cause runtime crashes ❌
```
### 🔒 Security and Privacy
```swift
// Don't log sensitive cohort information
Logger.debug("User assigned to cohort: \(cohort)") // ❌ Potential privacy issue
// Use privacy-safe logging
Logger.debug("Experiment cohort assigned") // ✅ Safe
// Don't store cohort assignments locally
UserDefaults.standard.set(cohort.rawValue, forKey: "cohort") // ❌ Privacy risk
```
## Troubleshooting
### Common Issues
#### Cohort Assignment Not Working
```swift
// Check feature flag setup
guard let cohort = featureFlagger.resolveCohort(for: .petsPictures) else {
Logger.error("Failed to resolve cohort for petsPictures")
return
}
```
#### Metrics Not Appearing
```swift
// Verify PixelExperimentKit import
import PixelExperimentKit
// Check subfeature ID matches config
PixelKit.fireExperimentPixel(
for: "petsPictures", // Must match config exactly
metric: "test_metric",
conversionWindowDays: 1...1,
value: "1"
)
```
#### Debug Menu Not Showing Experiment
```swift
public var supportsLocalOverriding: Bool {
switch self {
case .petsPictures:
return true // Must be true for debug menu
}
}
```
### Validation Checklist
- [ ] Privacy config includes correct cohort names and weights
- [ ] FeatureFlag enum properly configured with cohort type
- [ ] Feature flag source matches intended audience
- [ ] Custom metrics fire at appropriate times
- [ ] Local overrides work in debug builds
- [ ] Experiment cleanup plan documented
---
This A/B/N experiment framework provides a robust, scalable solution for data-driven feature development in the DuckDuckGo browser, enabling safe experimentation while maintaining user privacy and providing comprehensive analytics.
@@ -0,0 +1,160 @@
---
source: ~/DuckDuckGo/apple-browsers.git/main/.cursor/rules/analytics-patterns.mdc
confidence: 0.9
namespace: work
last_synced: 2026-04-28
alwaysApply: false
---
# Analytics and Pixel Patterns
## Structured Pixel Events
Use the existing Pixel.fire pattern with structured event definitions:
```swift
// ✅ CORRECT - Use existing Pixel.fire with proper parameters
extension PixelEvent {
static let featureUsed = "feature_used"
static let performanceMetric = "performance_metric"
static let errorOccurred = "error_occurred"
static let userAction = "user_action"
}
// Usage examples from codebase
Pixel.fire(pixel: .webKitTerminationDidReloadCurrentTab)
Pixel.fire(pixel: .cachedTabPreviewsExceedsTabCount, withAdditionalParameters: [
PixelParameters.tabPreviewCountDelta: "\(storedPreviews - totalTabs)"
])
Pixel.fire(pixel: .autofillLoginsSavePromptDisplayed, withAdditionalParameters: [
PixelParameters.autofillPromptTrigger: "manual"
])
```
## Pixel Parameters
Use the established PixelParameters constants:
```swift
// ✅ CORRECT - Use existing PixelParameters
extension PixelParameters {
static let featureName = "fn"
static let errorType = "et"
static let performanceValue = "pv"
static let userActionSource = "uas"
}
// Usage
Pixel.fire(pixel: .newFeatureUsed, withAdditionalParameters: [
PixelParameters.featureName: "voice_search",
PixelParameters.userActionSource: "keyboard_shortcut"
])
```
## Performance Metrics
Track performance metrics consistently:
```swift
// ✅ CORRECT - Performance tracking pattern
final class PerformanceTracker {
static func trackPageLoad(duration: TimeInterval, url: URL) {
let parameters = [
PixelParameters.duration: String(format: "%.3f", duration),
PixelParameters.domain: url.host ?? "unknown"
]
Pixel.fire(pixel: .pageLoadTime, withAdditionalParameters: parameters)
}
static func trackMemoryUsage(bytes: Int, context: String) {
let parameters = [
PixelParameters.memoryUsage: "\(bytes)",
PixelParameters.context: context
]
Pixel.fire(pixel: .memoryUsage, withAdditionalParameters: parameters)
}
}
```
## Error Tracking
Track errors with context:
```swift
// ✅ CORRECT - Error tracking
extension Pixel {
static func fireError(_ error: Error, context: String = "") {
let parameters = [
PixelParameters.errorType: String(describing: type(of: error)),
PixelParameters.context: context
]
Pixel.fire(pixel: .errorOccurred, withAdditionalParameters: parameters)
}
}
// Usage
do {
try await networkService.fetchData()
} catch {
Pixel.fireError(error, context: "data_fetch")
throw error
}
```
## Feature Usage Tracking
Track feature adoption and usage:
```swift
// ✅ CORRECT - Feature usage tracking
final class FeatureTracker {
static func trackFeatureUsage(_ feature: String, source: String = "") {
let parameters = [
PixelParameters.featureName: feature,
PixelParameters.userActionSource: source
]
Pixel.fire(pixel: .featureUsed, withAdditionalParameters: parameters)
}
static func trackFeatureEnabled(_ feature: String, enabled: Bool) {
let parameters = [
PixelParameters.featureName: feature,
PixelParameters.enabled: enabled ? "true" : "false"
]
Pixel.fire(pixel: .featureToggled, withAdditionalParameters: parameters)
}
}
```
## Privacy-Safe Analytics
Ensure all analytics respect privacy:
```swift
// ✅ CORRECT - Privacy-safe analytics
final class PrivacyAnalytics {
static func trackWithPrivacy(event: String, value: String) {
// Hash sensitive values
let hashedValue = value.sha256Hash
let parameters = [
PixelParameters.hashedValue: hashedValue
]
Pixel.fire(pixel: event, withAdditionalParameters: parameters)
}
static func trackAggregateMetric(metric: String, count: Int) {
// Only send aggregate data, never individual events
let parameters = [
PixelParameters.metric: metric,
PixelParameters.count: "\(count)"
]
Pixel.fire(pixel: .aggregateMetric, withAdditionalParameters: parameters)
}
}
```
See `feature-flags.md` for A/B test analytics and `privacy-security.md` for privacy requirements.
+392
View File
@@ -0,0 +1,392 @@
---
source: ~/DuckDuckGo/apple-browsers.git/main/.cursor/rules/anti-patterns.mdc
confidence: 0.9
namespace: work
last_synced: 2026-04-28
alwaysApply: true
---
# Anti-patterns and Common Mistakes to Avoid
## Singleton Anti-patterns
### ❌ NEVER: Static Shared Instances Without Dependency Injection (.shared instance pattern)
**Example:** See [singleton-antipattern.swift](anti-patterns/singleton-antipattern.swift)
### ❌ NEVER: Global State Access
```swift
// ❌ AVOID - Global state access
var globalSettings: [String: Any] = [:]
func someFunction() {
globalSettings["key"] = "value" // Global state is hard to test and debug
}
// ✅ CORRECT - Injected dependencies
final class SomeService {
private let settings: AppSettings
init(settings: AppSettings) {
self.settings = settings
}
func someFunction() {
settings.setValue("value", for: "key")
}
}
```
## Async/Await Anti-patterns
### ❌ NEVER: UI Updates Without @MainActor
**Example:** See [async-ui-updates.swift](anti-patterns/async-ui-updates.swift)
### ❌ NEVER: Unhandled Async Errors
```swift
// ❌ AVOID - Swallowing async errors
func fetchData() async {
let data = try? await networkService.getData() // Silently ignoring errors
// Process data...
}
// ✅ CORRECT - Proper error handling
func fetchData() async throws {
let data = try await networkService.getData()
// Process data...
}
// Or handle errors appropriately:
func fetchData() async {
do {
let data = try await networkService.getData()
// Process data...
} catch {
// Log error and show user-friendly message
logger.error("Failed to fetch data: \(error)")
await showError(error)
}
}
```
### ❌ NEVER: Blocking Main Thread with Sync Operations
```swift
// ❌ AVOID - Blocking main thread
@MainActor
func loadData() {
let data = NetworkService.fetchDataSynchronously() // Blocks UI
updateUI(with: data)
}
// ✅ CORRECT - Use async operations
@MainActor
func loadData() async {
let data = try await NetworkService.fetchData() // Non-blocking
updateUI(with: data)
}
```
## Memory Management Anti-patterns
### ❌ NEVER: Strong Reference Cycles in Closures
**Example:** See [memory-leak-closure.swift](anti-patterns/memory-leak-closure.swift)
### ❌ NEVER: Retaining View Controllers in Cache
```swift
// ❌ AVOID - Caching view controllers without cleanup
class NavigationManager {
private var cachedViewControllers: [String: UIViewController] = [:]
func getViewController(for identifier: String) -> UIViewController {
if let cached = cachedViewControllers[identifier] {
return cached // May contain stale data and strong references
}
let vc = createViewController(for: identifier)
cachedViewControllers[identifier] = vc
return vc
}
}
// ✅ CORRECT - Cache view models, not view controllers
class NavigationManager {
private var cachedViewModels: [String: ViewModel] = [:]
func getViewController(for identifier: String) -> UIViewController {
let viewModel = getOrCreateViewModel(for: identifier)
return createViewController(with: viewModel)
}
private func getOrCreateViewModel(for identifier: String) -> ViewModel {
if let cached = cachedViewModels[identifier] {
return cached
}
let viewModel = createViewModel(for: identifier)
cachedViewModels[identifier] = viewModel
return viewModel
}
}
```
## Error Handling Anti-patterns
### ❌ NEVER: Force Unwrapping Without Justification
**Example:** See [force-unwrapping.swift](anti-patterns/force-unwrapping.swift)
### ❌ NEVER: Generic Error Messages
```swift
// ❌ AVOID - Generic error handling
func handleError(_ error: Error) {
print("Something went wrong") // Not helpful for debugging
showAlert("Error occurred") // Not helpful for users
}
// ✅ CORRECT - Specific error handling
enum NetworkError: LocalizedError {
case noConnection
case timeout
case unauthorized
case serverError(Int)
var errorDescription: String? {
switch self {
case .noConnection:
return "No internet connection. Please check your network settings."
case .timeout:
return "Request timed out. Please try again."
case .unauthorized:
return "You are not authorized to access this resource."
case .serverError(let code):
return "Server error (\(code)). Please try again later."
}
}
}
func handleNetworkError(_ error: NetworkError) {
logger.error("Network error: \(error)")
showAlert(error.localizedDescription)
}
```
## SwiftUI Anti-patterns
### ❌ NEVER: Heavy Computation in View Body
```swift
// ❌ AVOID - Expensive operations in body
struct ContentView: View {
let items: [Item]
var body: some View {
List {
ForEach(items) { item in
Text(expensiveProcessing(item)) // Computed every view update
}
}
}
private func expensiveProcessing(_ item: Item) -> String {
// Heavy computation
return item.data.complexProcessing()
}
}
// ✅ CORRECT - Pre-compute or use lazy loading
struct ContentView: View {
@StateObject private var viewModel: ContentViewModel
var body: some View {
List {
ForEach(viewModel.processedItems) { item in
Text(item.displayText)
}
}
.onAppear {
viewModel.processItems()
}
}
}
```
### ❌ NEVER: Direct State Mutation from View
```swift
// ❌ AVOID - Direct state mutation in view
struct ContentView: View {
@State private var items: [Item] = []
var body: some View {
List {
ForEach(items) { item in
ItemRow(item: item) { updatedItem in
// Don't mutate state directly in view
if let index = items.firstIndex(where: { $0.id == updatedItem.id }) {
items[index] = updatedItem
}
}
}
}
}
}
// ✅ CORRECT - Use ViewModel for state management
struct ContentView: View {
@StateObject private var viewModel: ContentViewModel
var body: some View {
List {
ForEach(viewModel.items) { item in
ItemRow(item: item) { updatedItem in
viewModel.updateItem(updatedItem)
}
}
}
}
}
```
## Design System Anti-patterns
### ❌ NEVER: Hardcoded Colors or Icons
**Example:** See [design-system-violation.swift](anti-patterns/design-system-violation.swift)
## Network and API Anti-patterns
### ❌ NEVER: Hardcoded URLs or API Keys
```swift
// ❌ AVOID - Hardcoded values
func fetchData() async throws -> Data {
let url = URL(string: "https://api.example.com/data")! // Hardcoded URL
let apiKey = "abc123xyz" // Hardcoded API key
var request = URLRequest(url: url)
request.addValue(apiKey, forHTTPHeaderField: "Authorization")
let (data, _) = try await URLSession.shared.data(for: request)
return data
}
// ✅ CORRECT - Configuration-based approach
struct APIConfiguration {
let baseURL: URL
let apiKey: String
static let production = APIConfiguration(
baseURL: URL(string: "https://api.duckduckgo.com")!,
apiKey: Bundle.main.object(forInfoDictionaryKey: "API_KEY") as! String
)
}
func fetchData() async throws -> Data {
let config = APIConfiguration.production
let url = config.baseURL.appendingPathComponent("data")
var request = URLRequest(url: url)
request.addValue(config.apiKey, forHTTPHeaderField: "Authorization")
let (data, _) = try await URLSession.shared.data(for: request)
return data
}
```
## Testing Anti-patterns
### ❌ NEVER: Testing Implementation Details
```swift
// ❌ AVOID - Testing private implementation
class ViewModelTests: XCTestCase {
func testPrivateMethod() {
let viewModel = ViewModel()
// Don't test private methods directly
let result = viewModel.privateHelperMethod()
XCTAssertEqual(result, expected)
}
}
// ✅ CORRECT - Test public behavior
class ViewModelTests: XCTestCase {
func testLoadDataUpdatesState() async {
let mockService = MockDataService()
let viewModel = ViewModel(service: mockService)
await viewModel.loadData()
// Test the observable behavior, not implementation
XCTAssertFalse(viewModel.isLoading)
XCTAssertNotNil(viewModel.data)
XCTAssertNil(viewModel.error)
}
}
```
### ❌ NEVER: Tests That Don't Test Anything
```swift
// ❌ AVOID - Tests without assertions
func testInitialization() {
let viewModel = ViewModel()
// Test does nothing
}
// ❌ AVOID - Tests that can't fail
func testAlwaysTrue() {
XCTAssertTrue(true) // This test is meaningless
}
// ✅ CORRECT - Meaningful tests with specific assertions
func testInitializationSetsDefaultState() {
let viewModel = ViewModel()
XCTAssertEqual(viewModel.state, .idle)
XCTAssertTrue(viewModel.items.isEmpty)
XCTAssertFalse(viewModel.isLoading)
}
```
## Performance Anti-patterns
### ❌ NEVER: Synchronous Operations on Main Thread
```swift
// ❌ AVOID - Blocking main thread
@MainActor
func processLargeDataSet() {
let result = heavyComputation() // Blocks UI
updateUI(with: result)
}
// ✅ CORRECT - Background processing
@MainActor
func processLargeDataSet() async {
let result = await Task.detached(priority: .userInitiated) {
return heavyComputation()
}.value
updateUI(with: result)
}
```
## Communication Anti-patterns
### ❌ NEVER: Celebrate Partial Results or Progress
```
// ❌ AVOID - Celebrating when work is incomplete
"✅ MISSION ACCOMPLISHED!" (when tests still failing)
"🎯 Outstanding Achievement:" (when task isn't finished)
"📊 FINAL RESULTS:" (when results aren't final)
"✅ Successfully achieved X" (when Y tests still failing)
// ✅ CORRECT - Focus on what's left to do
"7 tests still failing. Continuing to fix remaining issues."
"Progress made but task incomplete. Working on remaining failures."
"X tests now passing, Y still need work."
```
**Never celebrate or summarize achievements when:**
- Tests are still failing
- Tasks are incomplete
- User's request hasn't been fully satisfied
- Work is in progress
**Only summarize results when:**
- ALL tests pass (100% success rate)
- Task is completely finished
- User's request is fully satisfied
- No work remaining
These anti-patterns should be actively avoided to maintain code quality, testability, and performance in the DuckDuckGo browser codebase.
@@ -0,0 +1,576 @@
---
source: ~/DuckDuckGo/apple-browsers.git/main/.cursor/rules/app-lifecycle-state-machine.mdc
confidence: 0.9
namespace: work
last_synced: 2026-04-28
alwaysApply: false
---
# App Lifecycle State Machine Architecture
## Overview
The DuckDuckGo browser has moved away from traditional AppDelegate-based lifecycle handling to a **state machine architecture**. While AppDelegate still exists, it has been significantly thinned out and now delegates responsibility to a structured state machine.
This approach ensures that lifecycle handling is **predictable, organized, and easy to maintain**.
## Architecture Components
### Three Core States
The architecture revolves around a state machine with three major states:
#### 1. **Launching** (Transient State)
- **Associated with**: `application(_:didFinishLaunchingWithOptions:)`
- **File**: `Launching.swift`
- **Purpose**: App's initial setup and dependency configuration
- **Responsibilities**:
- Initialize all services and objects
- Configure dependencies
- Prepare UI components
- Create `MainViewController` and set as `rootViewController`
#### 2. **Foreground** (Permanent State)
- **Associated with**: `applicationDidBecomeActive(_:)`
- **File**: `Foreground.swift`
- **Purpose**: App is fully interactive and user can engage with UI
- **Responsibilities**:
- Resume suspended work
- Handle user interactions
- Manage active UI state
#### 3. **Background** (Permanent State)
- **Associated with**: `applicationDidEnterBackground(_:)`
- **File**: `Background.swift`
- **Purpose**: App is not active and UI is not visible
- **Responsibilities**:
- Suspend ongoing work that doesn't need background execution
- Prepare for potential termination
- Handle background tasks
## State Machine Methods
### Core Transition Methods
All states implement specific methods for handling transitions:
#### `onTransition()`
- **When**: Called whenever the app enters that state from another state
- **Purpose**: Setup or cleanup during state transitions
- **Available in**: Foreground, Background
#### `willLeave()`
- **When**: Called before transitioning away from current state
- **Purpose**: Prepare for potential state change
- **Note**: Transition may be cancelled, in which case `didReturn()` is called
- **Available in**: Foreground, Background
#### `didReturn()`
- **When**: Called after successful transition to destination state OR when transition is cancelled
- **Purpose**: Finalize state entry or handle cancelled transition
- **Available in**: Foreground, Background
## Common Lifecycle Scenarios
### Cold App Start
```swift
// Flow: Launching → Foreground
1. Launching.init() // Initial setup
2. Foreground.onTransition() // Enter foreground
3. Foreground.didReturn() // Finalize foreground entry
```
### App Backgrounding
```swift
// Flow: Foreground → Background
1. Foreground.willLeave() // Prepare to leave foreground
2. Background.onTransition() // Enter background
3. Background.didReturn() // Finalize background entry
```
### App Foregrounding
```swift
// Flow: Background → Foreground
1. Background.willLeave() // Prepare to leave background
2. Foreground.onTransition() // Enter foreground
3. Foreground.didReturn() // Finalize foreground entry
```
### Interrupted Foreground (Alert/App Switcher)
```swift
// User receives alert but dismisses it
1. Foreground.willLeave() // Attempt to leave
2. Foreground.didReturn() // Cancelled - stay in foreground
// User opens App Switcher
1. Foreground.willLeave() // Attempt to leave
// Two possible outcomes:
// A. User returns directly:
2. Foreground.didReturn() // Return to foreground
// B. User switches to another app:
2. Background.onTransition() // Actually transition to background
3. Background.didReturn() // Finalize background entry
```
## Special iOS 18+ Scenarios
### Face ID Authentication on Cold Start
#### Successful Authentication
```swift
1. Launching.init()
2. Foreground.onTransition()
3. Foreground.didReturn()
```
#### Failed Authentication
```swift
1. Launching.init()
2. Background.onTransition() // Goes to background on auth failure
3. Background.didReturn()
```
### DuckDuckGo Face ID Lock
#### Cold Start with DDG Face ID
```swift
1. Launching.init()
2. Foreground.onTransition()
3. Foreground.didReturn()
4. Foreground.willLeave() // DDG auth triggers
5. Foreground.didReturn() // User passes auth
```
### Critical Setup Failure
```swift
1. Launching.init() throws // Setup fails (e.g., disk space)
2. Terminating.init() // App terminates
```
## Code Placement Patterns
### ⚙️ One-time Setup → `AppConfiguration`
**Location**: Inside `Launching.swift`
For setup that happens once and doesn't need ongoing lifecycle management:
```swift
class AppConfiguration {
func start() {
// Basic setup that doesn't require dependencies
setupGlobalUserAgent()
configureLogging()
}
func finalize() {
// Setup that requires access to services or MainCoordinator
configureWithDependencies()
}
}
```
**Use Cases**:
- Setting global user agents
- Initial configuration
- One-time system setup
### 🔄 Lifecycle-Reactive Logic → `Service`
For code that needs to react to app lifecycle events:
```swift
class MyLifecycleService {
func resumeWork() {
// Called from Foreground.onTransition() or didReturn()
}
func suspendWork() {
// Called from Background.onTransition() or Foreground.willLeave()
}
}
// In Launching.swift
let myService = MyLifecycleService()
services.myService = myService // Store in services for lifecycle access
```
**Service Patterns**:
- **Initialize**: In `Launching.init()`
- **Resume work**: In `Foreground` methods
- **Suspend work**: In `Background` methods
- **Assign to services**: Make available to other states
**Use Cases**:
- Network managers
- Timer services
- Data synchronization
- Background task management
### 🖼️ UI-Related Logic → `MainCoordinator`
**Location**: MainCoordinator initialization and management
For logic that involves creating or modifying the main view:
```swift
class MainCoordinator {
func setupMainViewController() {
// UI setup and configuration
}
func handleDeepLink(_ url: URL) {
// Navigation and UI state changes
}
}
```
**Use Cases**:
- View controller creation
- Navigation management
- UI state configuration
- Deep link handling
## Practical Examples
### 📊 Example 1: Pixel Analytics Service
**Requirement**: Send "Hello" pixel on foreground, "Goodbye" pixel on background
```swift
// 1. Create Service
class PixelService {
func sendHelloPixel() {
// Send hello pixel
}
func sendGoodbyePixel() {
// Send goodbye pixel
}
}
// 2. Initialize in Launching
class Launching {
func init() {
let pixelService = PixelService()
services.pixelService = pixelService
}
}
// 3. Use in Foreground
class Foreground {
func onTransition() {
services.pixelService.sendHelloPixel()
}
}
// 4. Use in Background
class Background {
func onTransition() {
services.pixelService.sendGoodbyePixel()
}
}
```
### ⏱️ Example 2: Session Timer Service
**Requirement**: Track session time, pause on interruptions, resume on return
```swift
class SessionTimeService {
private var timer: Timer?
func startTimer() {
// Start session timing
}
func pauseTimer() {
// Pause session timing
}
func resumeTimer() {
// Resume session timing
}
}
// Launching
class Launching {
func init() {
let sessionService = SessionTimeService()
services.sessionService = sessionService
}
}
// Foreground - Handle interruptions
class Foreground {
func didReturn() {
// Start/resume timer when entering or returning to foreground
services.sessionService.resumeTimer()
}
func willLeave() {
// Pause timer when potentially leaving foreground
services.sessionService.pauseTimer()
}
}
// Background
class Background {
func onTransition() {
// Timer already paused by Foreground.willLeave()
}
}
```
### 🧹 Example 3: Auto-Clear Data Service
**Requirement**: Clear data immediately on app wake to avoid UI glitches
```swift
class AutoClearService {
func startDataClearing() async {
// Clear user data
}
func waitForCompletion() async {
// Wait for clearing to complete
}
}
// Launching - Start clearing immediately
class Launching {
func init() {
let autoClearService = AutoClearService()
services.autoClearService = autoClearService
// Start clearing immediately on cold start
Task {
await autoClearService.startDataClearing()
}
}
}
// Foreground - Wait for completion before proceeding
class Foreground {
func onTransition() async {
// Wait for data clearing before loading URLs or handling deep links
await services.autoClearService.waitForCompletion()
handlePendingDeepLinks()
}
}
// Background - Start clearing before transitioning to foreground
class Background {
func willLeave() {
// Start clearing early to be ready for foreground transition
Task {
await services.autoClearService.startDataClearing()
}
}
func didReturn() {
// If transition was cancelled, clearing is still beneficial
// No action needed as clearing is irreversible
}
}
```
## State Context and Services
### Service Management
```swift
// Services are stored in StateContext for cross-state access
class StateContext {
var pixelService: PixelService!
var sessionService: SessionTimeService!
var autoClearService: AutoClearService!
// ... other services
}
// Access pattern in states
class Foreground {
func onTransition() {
services.pixelService.sendHelloPixel()
services.sessionService.resumeTimer()
}
}
```
### Service Lifecycle Best Practices
```swift
// ✅ CORRECT: Service with proper lifecycle management
class MyService {
private var isActive = false
func activate() {
guard !isActive else { return }
isActive = true
startWork()
}
func deactivate() {
guard isActive else { return }
isActive = false
stopWork()
}
private func startWork() {
// Begin service operations
}
private func stopWork() {
// Clean up service operations
}
}
// Usage in states
class Foreground {
func didReturn() {
services.myService.activate()
}
func willLeave() {
services.myService.deactivate()
}
}
```
## Decision Tree: Where Should My Code Go?
```
📋 What type of code are you adding?
├── 🔧 One-time setup that doesn't need lifecycle management?
│ └── ➡️ AppConfiguration (in Launching.swift)
│ ├── start() for basic setup
│ └── finalize() for dependency-requiring setup
├── 🔄 Logic that reacts to app state changes?
│ └── ➡️ Create a Service
│ ├── Initialize in Launching.init()
│ ├── Store in services for cross-state access
│ ├── Resume work in Foreground methods
│ └── Suspend work in Background methods
├── 🖼️ UI setup or view management?
│ └── ➡️ MainCoordinator
│ ├── View controller creation
│ ├── Navigation setup
│ └── Deep link handling
└── 🤔 Something else?
└── ➡️ Let's discuss through tech design
```
## Best Practices
### ✅ DO
```swift
// Store services for cross-state access
services.myService = MyService()
// Use proper lifecycle methods
func didReturn() {
resumeWork()
}
func willLeave() {
pauseWork()
}
// Handle state transitions gracefully
func onTransition() {
await waitForCriticalWork()
proceedWithStateLogic()
}
```
### ❌ DON'T
```swift
// Don't bypass the state machine
AppDelegate.shared.doSomething() // ❌
// Don't create services without storing them
let service = MyService() // ❌ Will be deallocated
// Don't ignore willLeave/didReturn patterns
func onTransition() {
// Only using onTransition misses important interrupt scenarios
}
// Don't block UI with long operations
func onTransition() {
performLongRunningTask() // ❌ Should be async
}
```
### 🔒 Memory Management
```swift
// Services are retained by StateContext
class StateContext {
var services: [String: AnyObject] = [:]
func addService<T: AnyObject>(_ service: T, for key: String) {
services[key] = service
}
}
// Clean up resources in state transitions
class MyService {
func cleanup() {
// Release resources, cancel operations
}
}
```
## Debugging and Monitoring
### State Transition Logging
```swift
class Foreground {
func onTransition() {
Logger.lifecycle.info("Entering Foreground state")
// State logic
}
func willLeave() {
Logger.lifecycle.info("Will leave Foreground state")
// Cleanup logic
}
func didReturn() {
Logger.lifecycle.info("Returned to Foreground state")
// Resume logic
}
}
```
### Performance Monitoring
```swift
class Launching {
func init() {
let startTime = CFAbsoluteTimeGetCurrent()
// Initialization logic
let duration = CFAbsoluteTimeGetCurrent() - startTime
Logger.performance.info("Launching completed in \(duration)s")
}
}
```
---
This state machine architecture provides a robust, maintainable approach to app lifecycle management that scales with the complexity of the DuckDuckGo browser while maintaining clear separation of concerns.
+81
View File
@@ -0,0 +1,81 @@
---
source: ~/DuckDuckGo/apple-browsers.git/main/.cursor/rules/architecture.mdc
confidence: 0.9
namespace: work
last_synced: 2026-04-28
alwaysApply: false
---
# DuckDuckGo Browser Architecture Guidelines
## Overall Architecture
- This is a multi-platform monorepo supporting iOS and macOS browsers
- Shared code lives in `SharedPackages/` directory
- Platform-specific code in `iOS/` and `macOS/` directories
- Follow modular architecture with clear separation of concerns
## Architecture Patterns
### MVVM for SwiftUI Views
- Use MVVM pattern for all SwiftUI views
- ViewModels should conform to `ObservableObject`
- Use `@Published` properties for reactive updates
- Keep ViewModels testable and free from UI concerns
Example:
```swift
class FeatureViewModel: ObservableObject {
@Published var state: FeatureState = .idle
private let service: FeatureServiceProtocol
init(service: FeatureServiceProtocol) {
self.service = service
}
}
```
### Coordinator Pattern
- Use coordinators for navigation and flow control
- Main app flow managed by `MainCoordinator`
- Create feature-specific coordinators as needed
- Coordinators handle navigation logic, not views
### Dependency Injection
- Use constructor injection for dependencies
- Define protocols for all dependencies
- Use `AppDependencyProvider` for shared dependencies
- Keep dependencies explicit and testable
## Code Organization
### Feature-Based Structure
- Organize code by features, not layers
- Each feature should have its own folder containing:
- Views (SwiftUI/UIKit)
- ViewModels
- Services
- Models
- Tests
### File Naming Conventions
- ViewModels: `FeatureNameViewModel.swift`
- Views: `FeatureNameView.swift` (SwiftUI) or `FeatureNameViewController.swift` (UIKit)
- Services: `FeatureNameService.swift`
- Protocols: `FeatureNameProtocol.swift` or embed in main file
### Extension Organization
- Split large classes into focused extensions
- Name extensions descriptively: `MainViewController+Email.swift`
- Group related functionality in extensions
## Privacy-First Design
- All features must consider privacy implications
- Use secure storage for sensitive data
- Implement proper data clearing mechanisms
- Follow fireproofing patterns where applicable
## Testing Requirements
- Write unit tests for all ViewModels and Services
- Test files should mirror source structure
- Use mock objects for dependencies
- Test async code with Combine publishers
@@ -0,0 +1,319 @@
---
source: ~/DuckDuckGo/apple-browsers.git/main/.cursor/rules/branch-naming-conventions.mdc
confidence: 0.9
namespace: work
last_synced: 2026-04-28
alwaysApply: false
---
# Branch Naming Conventions & GitHub Flow
## Overview
DuckDuckGo browser development follows **GitHub Flow**, a streamlined branching strategy that maintains a single main branch with feature branches for development work.
**Reference**: [GitHub Flow Documentation](https://docs.github.com/en/get-started/using-github/github-flow)
## Core Principles
### Main Branch Strategy
- **Single source of truth**: All development branches from `main`
- **Always deployable**: `main` branch should always be in a deployable state
- **Merge via PR**: All changes merged back through Pull Requests (except releases)
### Branch Lifecycle
1. Create branch from `main`
2. Develop feature/fix on branch
3. Open Pull Request to `main`
4. Code review and testing
5. Merge to `main`
6. **Delete branch** immediately after merge
## Branch Naming Conventions
### Single Developer Features & Bugfixes
For work by a single developer:
```
Format: <developer-name>/<feature-or-fix-name>
```
**Examples:**
- `alice/bookmark-sync`
- `alice/fix-bookmark-sync`
- `bob/credit-card-autofill`
- `charlie/fix-crash-on-startup`
**Guidelines:**
- Use kebab-case (lowercase with hyphens)
- Be descriptive but concise
- Include "fix-" prefix for bugfixes when helpful
### Multi-Developer Features
For collaborative features, use a two-stage approach:
#### 1. Base Feature Branch
```
Format: <feature-name>
Example: autofill
```
#### 2. Individual Developer Branches
```
Format: <feature-name>/<developer-name>/<sub-feature-name>
Example: autofill/alice/settings-list-changes
```
**Workflow:**
1. Create base feature branch from `main`
2. Developers create individual branches from base feature branch
3. Individual branches merge into base feature branch
4. Base feature branch merges into `main`
### Release Branches
Release branches follow semantic versioning:
```
Format: release/<version>
Example: release/0.18.5
```
**Special Notes:**
- No developer name prefix
- Use semantic versioning format
- **Exception**: Release branches merge to `main` via local merge (not PR)
- Delete immediately after merge
### Hotfix Branches
Critical fixes for production issues:
```
Format: hotfix/<version>
Example: hotfix/5.50.1
```
**Critical Requirements:**
- Use hotfix version number
- **MUST delete immediately after merge**
- Some tooling blocks subsequent hotfixes if previous hotfix branch exists
- Higher priority than regular releases
## Branch Management Best Practices
### ✅ DO
```bash
# Create feature branch from main
git checkout main
git pull origin main
git checkout -b alice/new-feature
# Meaningful commit messages
git commit -m "Add user authentication for secure vault"
# Keep branches up to date
git rebase main # or git merge main
# Delete branch after merge
git branch -d alice/new-feature
git push origin --delete alice/new-feature
```
### ❌ DON'T
```bash
# Don't use unclear names
git checkout -b temp
git checkout -b fix
git checkout -b test-branch
# Don't leave merged branches
# (Clutters repository and can cause tooling issues)
# Don't work directly on main
git checkout main
# Edit files directly... ❌
# Don't use inconsistent naming
git checkout -b Alice/NewFeature # Mixed case
git checkout -b alice_new_feature # Underscore instead of hyphen
```
## Example Workflows
### Single Developer Feature
```bash
# Start new feature
git checkout main
git pull origin main
git checkout -b alice/password-manager
# Work on feature
# ... make changes ...
git add .
git commit -m "Implement password storage encryption"
# Push and create PR
git push origin alice/password-manager
# Create PR via GitHub UI
# After PR is merged, cleanup
git checkout main
git pull origin main
git branch -d alice/password-manager
git push origin --delete alice/password-manager
```
### Multi-Developer Feature
```bash
# Team lead creates base branch
git checkout main
git pull origin main
git checkout -b autofill
git push origin autofill
# Developer creates individual branch
git checkout autofill
git pull origin autofill
git checkout -b autofill/alice/credential-storage
# Work and merge to base feature branch
# ... development work ...
git push origin autofill/alice/credential-storage
# Create PR to merge into 'autofill' branch
# Eventually merge base feature to main
# Create PR from 'autofill' to 'main'
```
### Hotfix Workflow
```bash
# Create hotfix from main
git checkout main
git pull origin main
git checkout -b hotfix/5.50.1
# Fix critical issue
# ... emergency fixes ...
git commit -m "Fix critical security vulnerability"
# Merge and IMMEDIATELY delete
git push origin hotfix/5.50.1
# Create PR and merge immediately
git branch -d hotfix/5.50.1
git push origin --delete hotfix/5.50.1
```
## Common Patterns
### Feature Names
| Type | Good Examples | Bad Examples |
|------|---------------|--------------|
| **New Features** | `user-authentication`<br>`bookmark-sync`<br>`credit-card-autofill` | `feature`<br>`new-stuff`<br>`implementation` |
| **Bug Fixes** | `fix-memory-leak`<br>`fix-crash-on-startup`<br>`fix-bookmark-deletion` | `bug`<br>`fix`<br>`temp-fix` |
| **Improvements** | `improve-performance`<br>`optimize-database`<br>`refactor-networking` | `better`<br>`update`<br>`changes` |
### Developer Names
| Format | Example |
|--------|---------|
| **First name** | `alice/new-feature` |
| **GitHub username** | `alice-dev/new-feature` |
| **Consistent choice** | Pick one format and stick to it |
## Branch Protection & CI
### Main Branch Protection
- **Required status checks**: All CI must pass
- **Required reviews**: At least one approval required
- **No force pushes**: Maintain history integrity
- **Delete head branches**: Automatic cleanup after merge
### Feature Branch CI
- Shellcheck validation for script changes
- Unit tests must pass
- Build verification for iOS and macOS
- Code style validation
## Git Configuration Tips
### Helpful Git Settings
```bash
# Auto-delete tracking branches for deleted remotes
git config --global fetch.prune true
# Auto-setup upstream when pushing new branches
git config --global push.autoSetupRemote true
# Use more descriptive default branch names
git config --global init.defaultBranch main
```
### Useful Aliases
```bash
# Quick branch switching
git config --global alias.co checkout
git config --global alias.br branch
# Clean up merged branches
git config --global alias.cleanup "!git branch --merged | grep -v '\\*\\|main\\|develop' | xargs -n 1 git branch -d"
# Show branch with tracking info
git config --global alias.branches "branch -vv"
```
## Troubleshooting
### Branch Already Exists
```bash
# If remote branch exists but you don't have it locally
git fetch origin
git checkout -b alice/feature-name origin/alice/feature-name
```
### Hotfix Branch Blocked
```bash
# If hotfix creation is blocked, check for existing hotfix branches
git branch -r | grep hotfix
# Delete any remaining hotfix branches
git push origin --delete hotfix/previous-version
```
### Sync with Main
```bash
# Keep feature branch updated with main
git checkout alice/feature-name
git rebase main # or: git merge main
git push origin alice/feature-name --force-with-lease # if rebased
```
## Integration with Development Tools
### Xcode Integration
- Branch names appear in Xcode source control
- Use descriptive names for better identification
- Avoid special characters that might cause Xcode issues
### CI/CD Pipeline
- Branch names used in build artifacts
- Feature branches trigger full test suites
- Release branches trigger deployment pipelines
### Issue Tracking
- Reference GitHub issues in branch names when helpful:
- `alice/fix-issue-1234-memory-leak`
- `bob/feature-567-dark-mode`
---
Following these conventions ensures consistent, organized development workflow across the DuckDuckGo browser codebase and facilitates collaboration between team members.
@@ -0,0 +1,448 @@
---
source: ~/DuckDuckGo/apple-browsers.git/main/.cursor/rules/browserserviceskit-integration.mdc
confidence: 0.9
namespace: work
last_synced: 2026-04-28
alwaysApply: false
---
# BrowserServicesKit Integration Guide
## Overview
BrowserServicesKit is the core shared library providing essential browser functionality to both iOS and macOS DuckDuckGo applications. It ensures consistent behavior and code reuse across platforms while maintaining privacy as the primary focus.
## Core Modules and Usage
### Privacy & Protection
Always use BrowserServicesKit for privacy-related functionality:
```swift
// ✅ CORRECT - Content blocking integration
import BrowserServicesKit
import ContentBlocking
final class PrivacyManager {
private let contentBlockingManager = ContentBlockingManager.shared
func enableContentBlocking(for webView: WKWebView) {
contentBlockingManager.enable(for: webView)
}
func updateBlockingRules() async {
await contentBlockingManager.updateRules()
}
}
// ✅ CORRECT - Privacy configuration
import PrivacyConfig
final class PrivacyFeatureManager {
private let privacyConfig = PrivacyConfiguration.shared
func isFeatureEnabled(_ feature: PrivacyFeature) -> Bool {
return privacyConfig.isEnabled(feature)
}
}
```
### Data Management
Use BrowserServicesKit for all data persistence:
```swift
// ✅ CORRECT - Bookmarks management
import Bookmarks
final class BookmarkService {
private let bookmarkManager = BookmarkManager.shared
func saveBookmark(_ bookmark: Bookmark) async {
await bookmarkManager.save(bookmark)
}
func fetchBookmarks() async -> [Bookmark] {
return await bookmarkManager.fetchAll()
}
}
// ✅ CORRECT - Secure credential storage
import SecureVault
final class CredentialManager {
private let secureVault = SecureVault.shared
func storeCredential(_ credential: WebsiteCredential) async throws {
try await secureVault.store(credential)
}
func retrieveCredentials(for domain: String) async throws -> [WebsiteCredential] {
return try await secureVault.credentials(for: domain)
}
}
```
### Navigation and URL Handling
Use BrowserServicesKit for navigation logic:
```swift
// ✅ CORRECT - Navigation handling
import Navigation
final class NavigationManager {
private let navigationController = NavigationController()
func navigate(to url: URL) {
let request = NavigationRequest(url: url)
navigationController.navigate(request)
}
func canGoBack() -> Bool {
return navigationController.canGoBack
}
}
```
### User Scripts and Content Injection
Use BrowserServicesKit for JavaScript injection:
```swift
// ✅ CORRECT - User script management
import UserScript
final class ContentScriptManager {
private let userScriptManager = UserScriptManager()
func injectPrivacyScripts(into webView: WKWebView) {
let scripts = userScriptManager.privacyScripts
scripts.forEach { script in
webView.configuration.userContentController.addUserScript(script)
}
}
}
```
## Platform-Specific Integration
### iOS Integration Pattern
```swift
// ✅ CORRECT - iOS-specific BrowserServicesKit usage
import BrowserServicesKit
import UIKit
final class iOSBrowserViewController: UIViewController {
private let contentBlockingManager = ContentBlockingManager.shared
private let privacyDashboard = PrivacyDashboard()
override func viewDidLoad() {
super.viewDidLoad()
setupBrowserServices()
}
private func setupBrowserServices() {
// Configure content blocking for iOS
contentBlockingManager.configure(for: .iOS)
// Setup privacy dashboard
privacyDashboard.delegate = self
}
}
```
### macOS Integration Pattern
```swift
// ✅ CORRECT - macOS-specific BrowserServicesKit usage
import BrowserServicesKit
import AppKit
final class macOSBrowserViewController: NSViewController {
private let contentBlockingManager = ContentBlockingManager.shared
private let downloadManager = DownloadManager.shared
override func viewDidLoad() {
super.viewDidLoad()
setupBrowserServices()
}
private func setupBrowserServices() {
// Configure content blocking for macOS
contentBlockingManager.configure(for: .macOS)
// Setup download handling
downloadManager.delegate = self
}
}
```
## Feature-Specific Integration
### Autofill Integration
```swift
// ✅ CORRECT - Autofill implementation
import Autofill
import BrowserServicesKit
final class AutofillCoordinator {
private let autofillManager = AutofillManager.shared
func setupAutofill(for webView: WKWebView) {
// Configure autofill user scripts
let autofillScripts = autofillManager.userScripts
autofillScripts.forEach { script in
webView.configuration.userContentController.addUserScript(script)
}
// Setup message handlers
autofillManager.setupMessageHandlers(for: webView)
}
func handleAutofillRequest(_ request: AutofillRequest) async {
await autofillManager.handleRequest(request)
}
}
```
### Sync Integration
```swift
// ✅ CORRECT - Sync functionality
import DDGSync
import BrowserServicesKit
final class SyncManager {
private let syncService = SyncService.shared
func enableSync() async {
await syncService.enable()
}
func syncBookmarks() async {
await syncService.sync(.bookmarks)
}
func syncCredentials() async {
await syncService.sync(.credentials)
}
}
```
## Testing with BrowserServicesKit
### Mock BrowserServicesKit Components
```swift
// ✅ CORRECT - Testing with mocks
import BrowserServicesKit
import XCTest
final class MockContentBlockingManager: ContentBlockingManagerProtocol {
var enabledRules: [ContentBlockingRule] = []
var isBlocked: Bool = false
func enable(rules: [ContentBlockingRule]) {
enabledRules = rules
}
func isBlocked(url: URL) -> Bool {
return isBlocked
}
}
final class BrowserFeatureTests: XCTestCase {
private var mockContentBlocking: MockContentBlockingManager!
private var browserManager: BrowserManager!
override func setUp() {
super.setUp()
mockContentBlocking = MockContentBlockingManager()
browserManager = BrowserManager(contentBlocking: mockContentBlocking)
}
func testContentBlockingEnabled() {
// Given
let rules = [ContentBlockingRule.trackerRule]
// When
browserManager.enableContentBlocking(rules: rules)
// Then
XCTAssertEqual(mockContentBlocking.enabledRules, rules)
}
}
```
## Configuration and Environment
### Environment-Based Configuration
```swift
// ✅ CORRECT - Environment configuration
import Configuration
import BrowserServicesKit
final class AppConfiguration {
static func configure() {
// Configure BrowserServicesKit for current environment
let config = Configuration.current
BrowserServicesKit.configure(
environment: config.environment,
privacyConfig: config.privacyConfig,
contentBlockingConfig: config.contentBlockingConfig
)
}
}
```
### Feature Flag Integration
```swift
// ✅ CORRECT - Feature flag integration
import FeatureFlags
import BrowserServicesKit
extension FeatureFlags {
var isAdvancedPrivacyEnabled: Bool {
return isEnabled(.advancedPrivacy)
}
var isEnhancedAutofillEnabled: Bool {
return isEnabled(.enhancedAutofill)
}
}
final class FeatureFlagBrowserManager {
func configureFeatures() {
if FeatureFlags.shared.isAdvancedPrivacyEnabled {
PrivacyConfiguration.shared.enableAdvancedFeatures()
}
if FeatureFlags.shared.isEnhancedAutofillEnabled {
AutofillManager.shared.enableEnhancedFeatures()
}
}
}
```
## Performance Optimization
### Efficient BrowserServicesKit Usage
```swift
// ✅ CORRECT - Performance-optimized usage
import BrowserServicesKit
final class OptimizedBrowserManager {
private let contentBlockingManager = ContentBlockingManager.shared
private var cachedRules: [ContentBlockingRule] = []
func loadContentBlockingRules() async {
// Cache rules to avoid repeated API calls
if cachedRules.isEmpty {
cachedRules = await contentBlockingManager.loadRules()
}
// Apply cached rules
await contentBlockingManager.apply(cachedRules)
}
func updateRulesIfNeeded() async {
let lastUpdate = await contentBlockingManager.lastUpdateTime
let shouldUpdate = Date().timeIntervalSince(lastUpdate) > 3600 // 1 hour
if shouldUpdate {
await loadContentBlockingRules()
}
}
}
```
## Error Handling
### BrowserServicesKit Error Handling
```swift
// ✅ CORRECT - Error handling patterns
import BrowserServicesKit
enum BrowserServiceError: LocalizedError {
case contentBlockingFailed
case bookmarkSaveFailed
case credentialStoreFailed
var errorDescription: String? {
switch self {
case .contentBlockingFailed:
return "Failed to enable content blocking"
case .bookmarkSaveFailed:
return "Failed to save bookmark"
case .credentialStoreFailed:
return "Failed to store credential"
}
}
}
final class ErrorHandlingBrowserManager {
func enableContentBlocking() async {
do {
try await ContentBlockingManager.shared.enable()
} catch {
// Log error and provide fallback
Logger.error("Content blocking failed: \(error)")
await showErrorToUser(BrowserServiceError.contentBlockingFailed)
}
}
}
```
## API Usage Guidelines
### Consistent API Patterns
```swift
// ✅ CORRECT - Follow BrowserServicesKit API patterns
import BrowserServicesKit
final class BrowserAPIManager {
// Use async/await for async operations
func loadData() async throws -> BrowserData {
return try await BrowserDataManager.shared.load()
}
// Use Combine for reactive streams
func observePrivacyEvents() -> AnyPublisher<PrivacyEvent, Never> {
return PrivacyManager.shared.privacyEventPublisher
}
// Use completion handlers only when required by platform APIs
func legacyOperation(completion: @escaping (Result<Data, Error>) -> Void) {
Task {
do {
let data = try await modernAsyncOperation()
completion(.success(data))
} catch {
completion(.failure(error))
}
}
}
}
```
## Common Integration Patterns
### Dependency Injection with BrowserServicesKit
```swift
// ✅ CORRECT - Dependency injection pattern
protocol BrowserServiceProvider {
var contentBlockingManager: ContentBlockingManagerProtocol { get }
var bookmarkManager: BookmarkManagerProtocol { get }
var autofillManager: AutofillManagerProtocol { get }
}
final class DefaultBrowserServiceProvider: BrowserServiceProvider {
let contentBlockingManager: ContentBlockingManagerProtocol = ContentBlockingManager.shared
let bookmarkManager: BookmarkManagerProtocol = BookmarkManager.shared
let autofillManager: AutofillManagerProtocol = AutofillManager.shared
}
final class BrowserViewModel: ObservableObject {
private let serviceProvider: BrowserServiceProvider
init(serviceProvider: BrowserServiceProvider = DefaultBrowserServiceProvider()) {
self.serviceProvider = serviceProvider
}
}
```
This guide ensures proper integration with BrowserServicesKit while maintaining privacy-first principles and cross-platform compatibility.
+975
View File
@@ -0,0 +1,975 @@
---
source: ~/DuckDuckGo/apple-browsers.git/main/.cursor/rules/code-style.mdc
confidence: 0.9
namespace: work
last_synced: 2026-04-28
alwaysApply: true
---
# Swift Code Style Guide
*This style guide is based on the [official iOS style guide](iOS/styleguide/STYLEGUIDE.md) and incorporates DuckDuckGo-specific patterns and requirements.*
## Correctness
**Strive to make your code compile without warnings.** This rule informs many style decisions such as using `#selector` types instead of string literals.
## SwiftLint
We use [SwiftLint](https://github.com/realm/SwiftLint) for enforcing Swift style and conventions. See the [SwiftLint configuration](.swiftlint.yml) for specific rules.
**Key SwiftLint settings**:
- Line length: 150 characters (not the default 100)
- Force cast/try: warnings (not errors for pragmatic development)
- Identifier naming: flexible for single-letter variables in closures
## Naming Conventions
Follow the [Swift API Design Guidelines](https://swift.org/documentation/api-design-guidelines/) with these key principles:
### Core Principles
- **Clarity at the call site** over brevity
- **Use camelCase** (not snake_case)
- **UpperCamelCase** for types and protocols
- **lowerCamelCase** for everything else
- **Include all needed words** while omitting needless words
- **Use names based on roles**, not types
### Type Names
```swift
// ✅ CORRECT: Descriptive, UpperCamelCase
class UserAuthenticationManager { }
struct BookmarkItem { }
enum NavigationState { }
protocol DataSourceProtocol { }
// ❌ INCORRECT: Too generic
class Manager { }
struct Data { }
```
### Variable and Function Names
```swift
// ✅ CORRECT: Descriptive lowerCamelCase
let maximumRetryCount = 3
var isLoading = false
func fetchUserData() { }
// Boolean properties should read like assertions
var isEnabled: Bool
var hasCompleted: Bool
var canDelete: Bool
// ❌ INCORRECT: Abbreviations and unclear names
let usrMgr = UserManager()
func calcTotal() { }
```
### Protocol Naming
```swift
// ✅ CORRECT: Capability protocols end in -able, -ible, -ing
protocol Loadable { }
protocol Refreshable { }
protocol UserAuthenticating { }
// ✅ CORRECT: Type protocols are nouns
protocol DataSource { }
protocol Delegate { }
```
### Method Naming Patterns
```swift
// ✅ CORRECT: Method naming patterns
// Factory methods begin with "make"
func makeLocationManager() -> CLLocationManager
// Verb methods follow -ed, -ing rule for non-mutating
func sorted() -> [Element] // non-mutating
func sort() // mutating
// Boolean methods read like assertions
func canDelete() -> Bool
func hasCompleted() -> Bool
```
### Delegate Methods
When creating custom delegate methods, the **unnamed first parameter should be the delegate source**:
```swift
// ✅ CORRECT: Delegate pattern
func namePickerView(_ namePickerView: NamePickerView, didSelectName name: String)
func namePickerViewShouldReload(_ namePickerView: NamePickerView) -> Bool
// ❌ INCORRECT: Missing source parameter
func didSelectName(namePicker: NamePickerViewController, name: String)
func namePickerShouldReload() -> Bool
```
### Use Type Inferred Context
Use compiler inferred context to write shorter, clear code:
```swift
// ✅ CORRECT: Type inferred context
let selector = #selector(viewDidLoad)
view.backgroundColor = .red
let toView = context.view(forKey: .to)
let view = UIView(frame: .zero)
// ❌ INCORRECT: Redundant type information
let selector = #selector(ViewController.viewDidLoad)
view.backgroundColor = UIColor.red
let toView = context.view(forKey: UITransitionContextViewKey.to)
let view = UIView(frame: CGRect.zero)
```
### Generics
Generic type parameters should be **descriptive, UpperCamelCase names**:
```swift
// ✅ CORRECT: Descriptive generic names
struct Stack<Element> { ... }
func write<Target: OutputStream>(to target: inout Target)
func swap<T>(_ a: inout T, _ b: inout T) // T is acceptable when no meaningful relationship
// ❌ INCORRECT: Non-descriptive or wrong case
struct Stack<T> { ... }
func write<target: OutputStream>(to target: inout target)
```
### Language
Use **US English spelling** to match Apple's API:
```swift
// ✅ CORRECT: US English
let color = "red"
// ❌ INCORRECT: British English
let colour = "red"
```
## Code Organization
### File Structure
```swift
// 1. Import statements (minimal - only what's needed)
import UIKit
import Combine
// 2. Protocol definitions
protocol FeatureDelegate: AnyObject {
func featureDidUpdate()
}
// 3. Main type declaration
class FeatureViewController: UIViewController {
// Properties first
private let viewModel: FeatureViewModel
// Lifecycle methods
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
}
// Private methods
private func setupUI() { }
}
// 4. Extensions for protocol conformance
// MARK: - UITableViewDataSource
extension FeatureViewController: UITableViewDataSource {
// Protocol methods
}
```
### Protocol Conformance
**Prefer separate extensions** for protocol conformance to keep related methods grouped:
```swift
// ✅ CORRECT: Separate extensions
class MyViewController: UIViewController {
// class implementation
}
// MARK: - UITableViewDataSource
extension MyViewController: UITableViewDataSource {
// table view data source methods
}
// MARK: - UIScrollViewDelegate
extension MyViewController: UIScrollViewDelegate {
// scroll view delegate methods
}
// ❌ INCORRECT: All in main class declaration
class MyViewController: UIViewController, UITableViewDataSource, UIScrollViewDelegate {
// all methods mixed together
}
```
### Minimal Imports
**Import only the modules a source file requires**:
```swift
// ✅ CORRECT: Minimal imports
import UIKit
var view: UIView
var deviceModels: [String]
// ✅ CORRECT: Foundation when UIKit not needed
import Foundation
var deviceModels: [String]
// ❌ INCORRECT: Unnecessary imports
import UIKit
import Foundation // UIKit already includes Foundation
var view: UIView
var deviceModels: [String]
```
### Remove Unused Code
**Remove unused (dead) code**, including Xcode template code:
```swift
// ✅ CORRECT: Keep only implemented methods
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return Database.contacts.count
}
// ❌ INCORRECT: Template code and unused methods
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
override func numberOfSections(in tableView: UITableView) -> Int {
// #warning Incomplete implementation, return the number of sections
return 1
}
```
## Formatting and Style
### Line Breaks and Length
- **Line margin: 150 characters** (not the default 100)
- **Long lines should be wrapped** at around 150 characters
- **Avoid trailing whitespace** at ends of lines
- **Add single newline** at end of each file
### Spacing
- **Indent using 4 spaces** rather than tabs
- **Method braces open on same line**, close on new line
- **One blank line between methods**
- **No blank lines after opening brace or before closing brace**
```swift
// ✅ CORRECT: Spacing and braces
if user.isHappy {
// Do something
} else {
// Do something else
}
// ❌ INCORRECT: Wrong brace placement
if user.isHappy
{
// Do something
}
else {
// Do something else
}
```
### Colons
**Colons have no space on left, one space on right**. Exceptions: ternary operator `? :`, empty dictionary `[:]`, `#selector` syntax:
```swift
// ✅ CORRECT: Colon spacing
class TestDatabase: Database {
var data: [String: CGFloat] = ["A": 1.2, "B": 3.2]
}
// ❌ INCORRECT: Wrong colon spacing
class TestDatabase : Database {
var data :[String:CGFloat] = ["A" : 1.2, "B":3.2]
}
```
### Function Parameters
**Closing parentheses should not appear on line by themselves**:
```swift
// ✅ CORRECT: Closing parenthesis placement
let user = try await getUser(
for: userID,
on: connection)
// ❌ INCORRECT: Closing parenthesis on own line
let user = try await getUser(
for: userID,
on: connection
)
```
## Function Declarations
### Short Functions
**Keep short function declarations on one line**:
```swift
// ✅ CORRECT: Short function on one line
func reticulateSplines(spline: [Double]) -> Bool {
// implementation
}
```
### Long Function Signatures
**For functions with long signatures, put each parameter on new line**:
```swift
// ✅ CORRECT: Long signature formatting
func reticulateSplines(spline: [Double],
adjustmentFactor: Double,
translateConstant: Int,
comment: String) -> Bool {
// implementation
}
```
### Return Types
**Use `Void` for closure/function outputs, `()` for inputs**:
```swift
// ✅ CORRECT: Return type formatting
func updateConstraints() -> Void {
// implementation
}
typealias CompletionHandler = (result) -> Void
// ❌ INCORRECT: Wrong return type syntax
func updateConstraints() -> () {
// implementation
}
typealias CompletionHandler = (result) -> ()
```
## Function Calls
**Mirror function declaration style at call sites**:
```swift
// ✅ CORRECT: Single line when it fits
let success = reticulateSplines(splines)
// ✅ CORRECT: Multi-line when wrapped
let success = reticulateSplines(
spline: splines,
adjustmentFactor: 1.3,
translateConstant: 2,
comment: "normalize the display")
```
## Closure Expressions
### Trailing Closure Syntax
**Use trailing closure syntax only for single closure at end**:
```swift
// ✅ CORRECT: Trailing closure usage
UIView.animate(withDuration: 1.0) {
self.myView.alpha = 0
}
UIView.animate(withDuration: 1.0, animations: {
self.myView.alpha = 0
}, completion: { finished in
self.myView.removeFromSuperview()
})
// ❌ INCORRECT: Trailing closure with multiple closures
UIView.animate(withDuration: 1.0, animations: {
self.myView.alpha = 0
}) { f in
self.myView.removeFromSuperview()
}
```
### Single-Expression Closures
**Use implicit returns for single-expression closures**:
```swift
// ✅ CORRECT: Implicit return
attendeeList.sort { a, b in
a > b
}
```
### Chained Methods
**Format chained methods for clarity**:
```swift
// ✅ CORRECT: Chained methods - compact when clear
let value = numbers.map { $0 * 2 }.filter { $0 % 3 == 0 }.index(of: 90)
// ✅ CORRECT: Chained methods - multi-line when complex
let value = numbers
.map { $0 * 2 }
.filter { $0 > 50 }
.map { $0 + 10 }
```
## Types and Constants
### Native Types
**Always use Swift's native types** when available:
```swift
// ✅ CORRECT: Native Swift types
let width = 120.0 // Double
let widthString = "\(width)" // String
// ❌ INCORRECT: Objective-C types
let width: NSNumber = 120.0 // NSNumber
let widthString: NSString = width.stringValue // NSString
```
### Constants vs Variables
**Use `let` by default, change to `var` only when compiler complains**:
```swift
// ✅ CORRECT: Type properties for constants
enum Math {
static let e = 2.718281828459045235360287
static let root2 = 1.41421356237309504880168872
}
let hypotenuse = side * Math.root2
// ❌ INCORRECT: Global constants
let e = 2.718281828459045235360287 // pollutes global namespace
let root2 = 1.41421356237309504880168872
```
### Type Inference
**Prefer compact code and let compiler infer types**:
```swift
// ✅ CORRECT: Type inference
let message = "Click the button"
let currentBounds = computeViewBounds()
var names = ["Mic", "Sam", "Christine"]
let maximumWidth: CGFloat = 106.5 // Specify when needed
// ❌ INCORRECT: Unnecessary type annotations
let message: String = "Click the button"
let currentBounds: CGRect = computeViewBounds()
```
### Empty Collections
**Use type annotation for empty arrays and dictionaries**:
```swift
// ✅ CORRECT: Type annotation for empty collections
var names: [String] = []
var lookup: [String: Int] = [:]
// ❌ INCORRECT: Constructor syntax
var names = [String]()
var lookup = [String: Int]()
```
### Syntactic Sugar
**Prefer shortcut type declarations**:
```swift
// ✅ CORRECT: Syntactic sugar
var deviceModels: [String]
var employees: [Int: String]
var faxNumber: Int?
// ❌ INCORRECT: Full generics syntax
var deviceModels: Array<String>
var employees: Dictionary<Int, String>
var faxNumber: Optional<Int>
```
## Optionals
### Optional Declarations
**Use `?` for optional types, `!` only when you know initialization timing**:
```swift
// ✅ CORRECT: Optional usage
var subview: UIView?
var volume: Double?
// Use ! only for outlets that initialize in viewDidLoad
@IBOutlet weak var tableView: UITableView!
```
### Optional Binding
**Shadow original names in optional binding**:
```swift
// ✅ CORRECT: Shadow original name
if let subview = subview, let volume = volume {
// do something with unwrapped subview and volume
}
// ❌ INCORRECT: Different names for unwrapped values
if let unwrappedSubview = optionalSubview {
if let realVolume = volume {
// do something with unwrappedSubview and realVolume
}
}
```
### Optional Chaining vs Binding
**Use optional chaining for single access, binding for multiple operations**:
```swift
// ✅ CORRECT: Optional chaining for single access
textContainer?.textLabel?.setNeedsDisplay()
// ✅ CORRECT: Optional binding for multiple operations
if let textContainer = textContainer {
// do many things with textContainer
}
```
## Memory Management
### Reference Cycles
**Prevent reference cycles with `weak` and `unowned` references.**
**Example:** See [memory-management.swift](code-style/memory-management.swift)
### Lazy Initialization
**Use lazy initialization for fine-grained control**:
```swift
// ✅ CORRECT: Lazy initialization
lazy var locationManager = makeLocationManager()
private func makeLocationManager() -> CLLocationManager {
let manager = CLLocationManager()
manager.desiredAccuracy = kCLLocationAccuracyBest
manager.delegate = self
manager.requestAlwaysAuthorization()
return manager
}
```
## Access Control
### Access Control Order
**Access control comes first, except for `static` and attributes**:
```swift
// ✅ CORRECT: Access control ordering
private let message = "Great Scott!"
class TimeMachine {
private dynamic lazy var fluxCapacitor = FluxCapacitor()
@IBAction private func activate() { }
static private let timeConstant = 88.0
}
// ❌ INCORRECT: Wrong ordering
fileprivate let message = "Great Scott!"
class TimeMachine {
lazy dynamic private var fluxCapacitor = FluxCapacitor()
}
```
### Private vs Fileprivate
**Prefer `private` to `fileprivate`**; use `fileprivate` only when compiler requires it.
## Control Flow
### Loop Style
**Prefer `for-in` style over `while-condition-increment`**:
```swift
// ✅ CORRECT: for-in style
for _ in 0..<3 {
print("Hello three times")
}
for (index, person) in attendeeList.enumerated() {
print("\(person) is at position #\(index)")
}
// ❌ INCORRECT: while style
var i = 0
while i < 3 {
print("Hello three times")
i += 1
}
```
### Ternary Operator
**Use ternary operator only when it increases clarity**:
```swift
// ✅ CORRECT: Simple ternary usage
let value = 5
result = value != 0 ? x : y
let isHorizontal = true
result = isHorizontal ? x : y
// ❌ INCORRECT: Complex nested ternary
result = a > b ? x = c > d ? c : d : y
```
### Golden Path
**Use the "golden path" pattern - don't nest `if` statements**:
```swift
// ✅ CORRECT: Golden path with guard
func computeFFT(context: Context?, inputData: InputData?) throws -> Frequencies {
guard let context = context else {
throw FFTError.noContext
}
guard let inputData = inputData else {
throw FFTError.noInputData
}
// use context and input to compute the frequencies
return frequencies
}
// ❌ INCORRECT: Nested if statements
func computeFFT(context: Context?, inputData: InputData?) throws -> Frequencies {
if let context = context {
if let inputData = inputData {
// use context and input to compute the frequencies
return frequencies
} else {
throw FFTError.noInputData
}
} else {
throw FFTError.noContext
}
}
```
### Compound Guard Statements
**Use compound guard for multiple optionals**:
```swift
// ✅ CORRECT: Compound guard
guard
let number1 = number1,
let number2 = number2,
let number3 = number3
else {
fatalError("impossible")
}
// ❌ INCORRECT: Nested optional binding
if let number1 = number1 {
if let number2 = number2 {
if let number3 = number3 {
// do something with numbers
}
}
}
```
## Class and Struct Definitions
### Example Well-Styled Class
```swift
final class Circle: Shape {
var x: Int, y: Int
var radius: Double
var diameter: Double {
get {
radius * 2
}
set {
radius = newValue / 2
}
}
init(x: Int, y: Int, radius: Double) {
self.x = x
self.y = y
self.radius = radius
}
convenience init(x: Int, y: Int, diameter: Double) {
self.init(x: x, y: y, radius: diameter / 2)
}
override func area() -> Double {
Double.pi * radius * radius
}
}
extension Circle: CustomStringConvertible {
var description: String {
"center = \(centerString) area = \(area())"
}
private var centerString: String {
"(\(x),\(y))"
}
}
```
### Use of Self
**Avoid using `self` unless required by compiler**:
```swift
// ✅ CORRECT: Self only when required
class PhotoViewController: UIViewController {
var image: UIImage
init(image: UIImage) {
self.image = image // Required to disambiguate
super.init(nibName: nil, bundle: nil)
}
func setupImageView() {
imageView.image = image // self not needed
}
}
```
### Computed Properties
**Omit get clause for read-only computed properties**:
```swift
// ✅ CORRECT: Implicit get for read-only
var diameter: Double {
radius * 2
}
// ❌ INCORRECT: Unnecessary get clause
var diameter: Double {
get {
return radius * 2
}
}
```
### Final
**Use `final` when inheritance is not intended**:
```swift
// ✅ CORRECT: Final for utility classes
final class Box<T> {
let value: T
init(_ value: T) {
self.value = value
}
}
```
## DuckDuckGo-Specific Patterns
### Design System Integration (MANDATORY)
**ALWAYS use DesignResourcesKit** for colors, typography, and icons.
**Example:** See [design-system-integration.swift](code-style/design-system-integration.swift)
### Dependency Injection Pattern
**Use AppDependencyProvider for dependency injection.**
**Example:** See [dependency-injection.swift](code-style/dependency-injection.swift)
### Async/Await Patterns
**Example:** See [async-await-pattern.swift](code-style/async-await-pattern.swift)
### Property Wrappers
**Example:** See [property-wrappers.swift](code-style/property-wrappers.swift)
## Comments and Documentation
### When to Comment
**Use comments to explain WHY, not WHAT**:
```swift
// ✅ CORRECT: Explains why
// We delay the animation to avoid conflicting with the previous transition
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.animateTransition()
}
// ❌ INCORRECT: Explains what (obvious from code)
// Set the background color to red
view.backgroundColor = .red
```
### Comment Style
**Prefer double/triple-slash over C-style comments**:
```swift
// ✅ CORRECT: Swift-style comments
// This is a comment
/// This is a documentation comment
// ❌ INCORRECT: C-style comments
/* This is a comment */
```
## String Literals
### Multi-line Strings
**Use multi-line string syntax for long strings**:
```swift
// ✅ CORRECT: Multi-line string formatting
let message = """
You cannot charge the flux \
capacitor with a 9V battery.
You must use a super-charger \
which costs 10 credits. You currently \
have \(credits) credits available.
"""
// ❌ INCORRECT: Concatenation or inline text
let message = """You cannot charge the flux \
capacitor with a 9V battery.
You must use a super-charger \
which costs 10 credits. You currently \
have \(credits) credits available.
"""
```
## Prohibited Patterns
### No Emoji
**Do not use emoji in code** - it creates unnecessary friction:
```swift
// ❌ FORBIDDEN: Emoji in code
let isHappy = true 😀
func celebrate() 🎉 { }
// ✅ CORRECT: Clear, text-based names
let isHappy = true
func celebrate() { }
```
### No Color/Image Literals
**Do not use `#colorLiteral` or `#imageLiteral`** - they're hard to read and maintain:
```swift
// ❌ FORBIDDEN: Literals
let color = #colorLiteral(red: 1, green: 0, blue: 0, alpha: 1)
let image = #imageLiteral(resourceName: "icon")
// ✅ CORRECT: Explicit constructors (but prefer DesignResourcesKit)
let color = UIColor(red: 1, green: 0, blue: 0, alpha: 1)
let image = UIImage(named: "icon")
// ✅ BEST: DesignResourcesKit
let color = UIColor(designSystemColor: .accent)
let image = DesignSystemImages.Color.Size24.bookmark
```
### No Parentheses Around Conditionals
**Don't use unnecessary parentheses**:
```swift
// ✅ CORRECT: No parentheses needed
if name == "Hello" {
print("World")
}
// ❌ INCORRECT: Unnecessary parentheses
if (name == "Hello") {
print("World")
}
```
### No Semicolons
**Swift doesn't require semicolons** - don't use them:
```swift
// ✅ CORRECT: No semicolons
let swift = "not a scripting language"
// ❌ INCORRECT: Unnecessary semicolons
let swift = "not a scripting language";
```
## Error Handling and Assertions
### Fatal Errors
**Use `fatalError()` when app reaches unrecoverable state**:
```swift
// ✅ CORRECT: Fatal error for impossible states
guard let viewController = storyboard.instantiateViewController(withIdentifier: "Main") as? MainViewController else {
fatalError("Failed to instantiate MainViewController from storyboard")
}
```
### Assertions
**Use `assert()` and `assertionFailure()` for recoverable but unexpected states**:
```swift
// ✅ CORRECT: Assert for development debugging
func processItems(_ items: [Item]) {
assert(!items.isEmpty, "Items array should not be empty")
// Handle empty array gracefully in release builds
guard !items.isEmpty else { return }
// Process items...
}
```
## Logging
**Use unified logging system** for all logging.
**Example:** See [logging-pattern.swift](code-style/logging-pattern.swift)
**See [Logging Guidelines](logging-guidelines.md) for comprehensive logging patterns.**
## Unit Test Naming
**Use "when/then" convention for test names**:
```swift
// ✅ CORRECT: When/then test naming
func testWhenUrlIsNotATrackerThenMatchesIsFalse() { }
func testWhenUserTapsBookmarkButtonThenBookmarkIsAdded() { }
func testWhenNetworkFailsThenErrorIsDisplayed() { }
// ❌ INCORRECT: Unclear test names
func testBookmarks() { }
func testNetworking() { }
```
## Functions vs Methods
**Prefer methods over free functions** for discoverability:
```swift
// ✅ CORRECT: Methods are easily discoverable
let sorted = items.mergeSorted()
rocket.launch()
// ❌ INCORRECT: Free functions are hard to discover
let sorted = mergeSort(items)
launch(&rocket)
// ✅ ACCEPTABLE: Free functions that feel natural
let tuples = zip(a, b)
let value = max(x, y, z)
```
---
**Remember**: This style guide ensures consistency across the DuckDuckGo browser codebase. When in doubt, prioritize clarity and follow the patterns established in existing code.
@@ -0,0 +1,359 @@
---
source: ~/DuckDuckGo/apple-browsers.git/main/.cursor/rules/design-system-designresourceskit.mdc
confidence: 0.9
namespace: work
last_synced: 2026-04-28
alwaysApply: true
---
# DuckDuckGo iOS Design System & DesignResourcesKit (DRK)
## Overview
The DuckDuckGo iOS design system is implemented through **DesignResourcesKit (DRK)**, a shared Swift package that contains our design tokens, type styles, colors, and design system elements.
**Repository**: [https://github.com/duckduckgo/DesignResourcesKit](https://github.com/duckduckgo/DesignResourcesKit)
**Figma Designs**: [🖱️ iOS & iPadOS Components](https://www.figma.com/file/GzGKD6gR24AHoUqVykX1ah/%F0%9F%93%B1-iOS-%26-iPadOS-Components?type=design&node-id=3938%3A23329&mode=design&t=0fuiNF84nnV5zExC-1)
### What DRK Contains
✅ **Currently Included**:
- **Type styles and typography** (based on system styles)
- **Semantic color system** (with light/dark mode support)
- **Design tokens and foundations**
🔄 **Future Expansion**:
- **Reusable components** (when patterns emerge)
- **Advanced interaction patterns**
❌ **Not Included**:
- **Icons** (remain in iOS app directly for now)
## ⚠️ Critical Rule: Don't Break the Design System
> **If you take only one thing away from this documentation**:
> **Don't add new colors or type styles outside of the design system without reading the guidelines below.**
Breaking the design system:
- **Undermines consistency** across the app
- **Creates maintenance debt** with scattered styles
- **Breaks accessibility** features like dynamic type
- **Fragments the user experience**
## Typography System
### Philosophy
Our typography system is **based on system styles** rather than hardcoded sizes. This ensures:
- **Automatic dynamic type support** for accessibility
- **Consistent scaling** across different user preferences
- **Platform-appropriate styling** that feels native
### UIKit Usage
DRK defines **static functions on UIFont** for all typography.
**Example:** See [uikit-typography-usage.swift](design-system-designresourceskit/uikit-typography-usage.swift)
#### Available Typography Styles
**Example:** See [uikit-typography-styles.swift](design-system-designresourceskit/uikit-typography-styles.swift)
#### Best Practices for UIKit
**Example:** See [uikit-typography-best-practices.swift](design-system-designresourceskit/uikit-typography-best-practices.swift)
### SwiftUI Usage
DRK provides **view modifiers and extensions** for SwiftUI that should be used instead of direct font access.
**Example:** See [swiftui-typography-usage.swift](design-system-designresourceskit/swiftui-typography-usage.swift)
#### Available SwiftUI Typography Modifiers
**Example:** See [swiftui-typography-modifiers.swift](design-system-designresourceskit/swiftui-typography-modifiers.swift)
#### SwiftUI Code Review Guidelines
**When reviewing PRs**: Look for `.font()` usage as a red flag.
**Example:** See [swiftui-code-review-red-flags.swift](design-system-designresourceskit/swiftui-code-review-red-flags.swift)
### Emergency Escape Hatch (Avoid!)
**For legacy layout fixes only**: If you absolutely must disable dynamic type, there's a deliberately obtusely named function:
```swift
// ❌ LAST RESORT: Only for fixing legacy layouts
let fixedFont = UIFont.daxFontOutsideOfTheDesignSystemToFixLegacyLayoutBreakage()
```
**Important Notes**:
- This function **may not exist** in current DRK versions
- If you need it, you must **revert the commit** that removed it: [Commit 971979d](https://github.com/duckduckgo/DesignResourcesKit/pull/1/commits/971979d3dcd95567b9812b800eb22ab1611ce3a5)
- This is **deliberately annoying** to discourage usage
- **Always prefer** fixing the layout to support dynamic type instead
## Color System
### Semantic Color Approach
Our color system uses **semantic naming** rather than literal colors (e.g., "primary text" instead of "black"). This enables:
- **Automatic dark mode support**
- **Future theme flexibility**
- **Accessibility compliance**
- **Consistent visual hierarchy**
### Color Categories
#### Text Colors
**UIKit Example:** See [colors-text-uikit.swift](design-system-designresourceskit/colors-text-uikit.swift)
**SwiftUI Example:** See [colors-text-swiftui.swift](design-system-designresourceskit/colors-text-swiftui.swift)
#### Background Colors
**Example:** See [colors-background.swift](design-system-designresourceskit/colors-background.swift)
#### Control Colors
```swift
// UIKit
button.backgroundColor = UIColor(designSystemColor: .controlsFillPrimary)
button.backgroundColor = UIColor(designSystemColor: .controlsFillSecondary)
// SwiftUI
Button("Action") { }
.foregroundColor(Color(designSystemColor: .controlsFillPrimary))
.background(Color(designSystemColor: .controlsFillSecondary))
```
#### Button-Specific Colors
```swift
// UIKit
primaryButton.backgroundColor = UIColor(designSystemColor: .buttonPrimaryBackground)
primaryButton.setTitleColor(UIColor(designSystemColor: .buttonPrimaryText), for: .normal)
secondaryButton.backgroundColor = UIColor(designSystemColor: .buttonSecondaryBackground)
secondaryButton.setTitleColor(UIColor(designSystemColor: .buttonSecondaryText), for: .normal)
// SwiftUI
Button("Primary Action") { }
.foregroundColor(Color(designSystemColor: .buttonPrimaryText))
.background(Color(designSystemColor: .buttonPrimaryBackground))
Button("Secondary Action") { }
.foregroundColor(Color(designSystemColor: .buttonSecondaryText))
.background(Color(designSystemColor: .buttonSecondaryBackground))
```
#### Accent Colors
```swift
// UIKit
view.tintColor = UIColor(designSystemColor: .accent)
// SwiftUI
Image(systemName: "heart.fill")
.foregroundColor(Color(designSystemColor: .accent))
```
### Anti-patterns: What NOT to Do
**Example:** See [colors-anti-patterns.swift](design-system-designresourceskit/colors-anti-patterns.swift)
## Enforcement and Code Review
### Automated Enforcement
#### Danger Integration
**Asset catalog enforcement**: We use [Danger](https://danger.systems/) to prevent new colors being added directly to iOS app asset catalogs.
**Example:** See [danger-integration.rb](design-system-designresourceskit/danger-integration.rb)
### Manual Code Review Checklist
#### ✅ Look for in PRs:
- **DRK typography usage**: `UIFont.daxTitle1()`, `.daxBody()` modifiers
- **DRK color usage**: `UIColor(designSystemColor: .textPrimary)`
- **No hardcoded colors**: No hex values, RGB tuples, or named colors
- **No `.font()` modifiers** in SwiftUI (red flag for design system violations)
- **Semantic naming**: Colors described by purpose, not appearance
#### Code Review Examples
**Example:** See [code-review-checklist.swift](design-system-designresourceskit/code-review-checklist.swift)
### Opportunistic Improvements
**Most of the iOS app currently does not use the design system**, so you're encouraged to:
1. **Opportunistically refactor** old code to use DRK when you encounter it
2. **Update hardcoded colors** to semantic colors when working in an area
3. **Replace system fonts** with DRK typography when touching text styling
4. **File follow-up tickets** for systematic cleanup when you notice patterns
#### Example: Opportunistic Refactoring
**Example:** See [opportunistic-refactoring.swift](design-system-designresourceskit/opportunistic-refactoring.swift)
## Components
### Current State: Minimal Component Library
We primarily use **system components** rather than custom ones, following iOS design guidelines. This is different from our Android app which has more custom components.
**Philosophy**:
- **System components first** - leverages platform conventions
- **Custom components only when needed** - avoid overengineering
- **Reusable when patterns emerge** - extract when used in multiple places
### Existing Custom Components
#### Blue Button (Reusable)
Our primary custom component used across multiple screens.
**Example:** See [blue-button-component.swift](design-system-designresourceskit/blue-button-component.swift)
**Candidate for DRK**: This button is used in multiple places and should be extracted into DesignResourcesKit as a reusable component.
### Future Component Strategy
#### When to Create Components
**✅ Create a component when**:
- Pattern is used in **3+ different contexts**
- Styling is **complex or specialized**
- Behavior needs to be **consistent across usage**
- Component **encapsulates design system tokens**
**❌ Don't create a component when**:
- Used in only **one place** (keep it local)
- **System component exists** that meets needs
- Component would be **overly generic** or complex
#### Emerging Patterns to Watch
Look for these patterns that might become components:
```swift
// Bottom sheets - if format becomes consistent
struct BottomSheetView: View {
// Consistent styling, behavior, animation
// Could become reusable component
}
// Info cards/panels - if layout patterns emerge
struct InfoCardView: View {
// Standard card styling with DRK colors
// Could be extracted if reused
}
// Form elements - if custom styling is needed
struct FormFieldView: View {
// Consistent form field styling
// Could become component library
}
```
#### Component Creation Process
1. **Identify the pattern** in your current work
2. **Check if existing implementations** could be generalized
3. **Design the API** to be flexible but opinionated
4. **Implement using DRK tokens** for colors, typography, spacing
5. **Add to DesignResourcesKit** package
6. **Update existing usages** to use the new component
7. **Document the component** with usage examples
**Example:** See [component-creation-example.swift](design-system-designresourceskit/component-creation-example.swift)
## Modularization Strategy
### Why DRK is a Separate Package
**High friction is a feature**: Making DRK a separate module provides beneficial constraints:
1. **Immutability encouragement**: Changes require more thought and process
2. **API stability**: Forces consideration of breaking changes
3. **Reusability**: Can be shared across iOS/macOS if needed
4. **Clear boundaries**: Separates design tokens from app logic
5. **Version control**: Can be tagged and versioned independently
### Design System Evolution
**Original Discussion**: [Tech Design: How to modularise iOS/macOS design system elements](✓ Tech Design: How to modularise iOS/macOS design system elements)
**Guiding Principles**:
- **Start minimal**: Don't over-engineer early
- **Evolve based on usage**: Add components when patterns emerge
- **Maintain consistency**: All additions should follow established patterns
- **Document decisions**: Keep rationale for future developers
## Working with DesignResourcesKit
### Adding New Design Tokens
**Process for adding colors/typography**:
1. **Design system first**: Ensure token is defined in Figma
2. **Semantic naming**: Use purpose-based names (`textPrimary` not `black`)
3. **Light/dark variants**: Define both light and dark mode values
4. **PR to DRK**: Add to DesignResourcesKit repository
5. **Update app**: Use new tokens in consuming apps
6. **Documentation**: Update usage examples and guidelines
### Updating DRK Version
**In consuming app (iOS/macOS):**
**Example:** See [updating-drk-version.swift](design-system-designresourceskit/updating-drk-version.swift)
**Testing DRK changes**:
- Test in both **light and dark modes**
- Verify **dynamic type scaling** works correctly
- Check **accessibility** with larger text sizes
- Test on **different device sizes**
### Local Development
**For iterating on DRK:**
**Example:** See [local-development.sh](design-system-designresourceskit/local-development.sh)
## Resources and References
### Official Resources
- **GitHub Repository**: [duckduckgo/DesignResourcesKit](https://github.com/duckduckgo/DesignResourcesKit)
- **Figma Designs**: [iOS & iPadOS Components](https://www.figma.com/file/GzGKD6gR24AHoUqVykX1ah/%F0%9F%93%B1-iOS-%26-iPadOS-Components?type=design&node-id=3938%3A23329&mode=design&t=0fuiNF84nnV5zExC-1)
### Related Documentation
- **Colors**: [Tech Design: How to organise colors and icons in iOS and macOS wrt the design system](✓ Tech Design: How to organise colors and icons in iOS and macOS wrt the design system)
- **Colors Update**: [Tech Design: Redefine design system colors in DesignResourcesKit](✓ Tech Design: Redefine design system colors in DesignResourcesKit)
- **Typography**: [Tech Design: How to organise typography/label styles in iOS and macOS wrt the design system](✓ Tech Design: How to organise typography/label styles in iOS and macOS wrt the design system)
- **Enforcement**: [Use danger to stop new colors being added to the iOS app](✓ Use danger to stop new colors being added to the iOS app)
### Quick Reference
#### UIKit Checklist
- [ ] Use `UIFont.daxTitle1()`, `UIFont.daxBody()`, etc.
- [ ] Use `UIColor(designSystemColor: .textPrimary)` etc.
- [ ] No hardcoded colors or fonts
- [ ] No system colors for app content
#### SwiftUI Checklist
- [ ] Use `.daxTitle1()`, `.daxBody()` modifiers
- [ ] Use `Color(designSystemColor: .textPrimary)` etc.
- [ ] Avoid `.font()` modifier (red flag in reviews)
- [ ] No hardcoded colors
#### Code Review Checklist
- [ ] No new colors in asset catalogs
- [ ] DRK typography used consistently
- [ ] Semantic color naming
- [ ] No hardcoded styling
- [ ] Opportunistic improvements to legacy code
---
**Remember**: The design system is only as strong as our commitment to using it. Every PR is an opportunity to improve consistency and user experience.
@@ -0,0 +1,178 @@
---
source: ~/DuckDuckGo/apple-browsers.git/main/.cursor/rules/development-commands.mdc
confidence: 0.9
namespace: work
last_synced: 2026-04-28
alwaysApply: true
---
# Development Commands & Build Instructions
## 📋 When to Use This Document
Use these instructions when you need to:
- Build the iOS Browser app for testing or development
- Build the macOS Browser app for testing or development
- Verify that code changes compile successfully
- Prepare the app for testing or debugging
- Understand build failures and how to fix them
## 🚦 Golden Rules for Building
### ✅ ALWAYS DO THESE
1. **Use the full shell wrapper**: `/bin/sh -c 'set -e -o pipefail && xcodebuild ... | xcbeautify'`
2. **Detect environment first**: Never hardcode paths or simulator IDs
3. **Check exit codes**: Ensure the build succeeded before proceeding
4. **Use absolute paths**: Always use full paths for workspace files
5. **Include xcbeautify**: Output is unreadable without it
### ❌ NEVER DO THESE
1. **Never use `-jobs` flag**: It's been removed from all commands
2. **Never skip xcbeautify**: Raw xcodebuild output is nearly impossible to parse
3. **Never use .xcodeproj files**: Always use .xcworkspace
4. **Never hardcode simulator IDs**: They change between systems
5. **Never ignore build failures**: Always check and handle errors
## 🔍 Phase 1: Environment Detection
### Pre-Flight Checks
Before building, validate your environment.
**Example:** See [pre-flight-checks.sh](development-commands/pre-flight-checks.sh)
### Required Variables to Detect
| Variable | Purpose | Detection Command | Expected Format |
|----------|---------|-------------------|-----------------|
| `WORKSPACE_PATH` | Full path to .xcworkspace | `pwd` + `find . -name "DuckDuckGo.xcworkspace"` | `/Users/.../DuckDuckGo.xcworkspace` |
| `SIMULATOR_ID` | iOS Simulator UUID | `xcrun simctl list devices \| grep iPhone` | `XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX` |
| `ARCHITECTURE` | Mac CPU type | `uname -m` | `arm64` or `x86_64` |
### Detection Commands
**Example:** See [environment-detection.sh](development-commands/environment-detection.sh)
## 🏗️ Phase 2: Build Execution
### iOS Build Command Template
Replace the placeholders with your detected values.
**Example:** See [ios-build-template.sh](development-commands/ios-build-template.sh)
### macOS Build Command Template
Replace the placeholders with your detected values.
**Example:** See [macos-build-template.sh](development-commands/macos-build-template.sh)
### Complete Working Examples
#### iOS Build (Real Values)
**Example:** See [ios-build-example.sh](development-commands/ios-build-example.sh)
#### macOS Build (Real Values)
**Example:** See [macos-build-example.sh](development-commands/macos-build-example.sh)
## ✅ Phase 3: Build Verification
### Signs of Success
- Command exits with code 0
- Last line contains "BUILD SUCCEEDED"
- No error messages in red
- Build time is within expected range (see performance table below)
### Signs of Failure
- Command exits with non-zero code
- Output contains "BUILD FAILED"
- Red error messages appear
- Build hangs for more than 15 minutes
### Performance Expectations
| Build Type | Expected Duration | Action if Exceeded |
|------------|------------------|-------------------|
| First build | 5-10 minutes | Normal - downloading dependencies |
| Subsequent build | 1-3 minutes | Check for errors in output |
| Clean build | 3-5 minutes | Normal - rebuilding everything |
| Incremental | 10-30 seconds | Normal for small changes |
| Hanging >15 min | Abnormal | Cancel and check for issues |
## 🔧 Error Recovery
### If Build Fails - Immediate Actions
1. **Check the error message** - Last few red lines usually indicate the issue
2. **Clean and retry:** See [error-recovery-clean.sh](development-commands/error-recovery-clean.sh)
3. **If "No such module" errors:** See [error-recovery-derived-data.sh](development-commands/error-recovery-derived-data.sh)
4. **If simulator issues:** See [error-recovery-simulator.sh](development-commands/error-recovery-simulator.sh)
### Common Problems and Solutions
| Problem | Diagnosis Command | Solution |
|---------|------------------|----------|
| No workspace found | `ls *.xcworkspace` | Ensure you're in project root directory |
| Simulator not found | `xcrun simctl list devices` | Pick a different simulator ID from the list |
| "Command not found: xcbeautify" | `which xcbeautify` | Install: `brew install xcbeautify` |
| Build hangs | Check Activity Monitor | Kill xcodebuild process and retry |
| "No such module" | Check package resolution | Clean DerivedData and rebuild |
| Provisioning errors | Check Xcode account | May need manual Xcode intervention |
## 🤖 Complete Automation Script
Use this script for reliable, automated builds.
**Example:** See [complete-automation-script.sh](development-commands/complete-automation-script.sh)
## 📊 Build Flag Reference
Understanding what each flag does:
| Flag | Purpose | Impact |
|------|---------|--------|
| `ONLY_ACTIVE_ARCH=YES` | Build only for current architecture | 50% faster builds |
| `DEBUG_INFORMATION_FORMAT=dwarf` | Use DWARF debug symbols | Smaller build size |
| `COMPILER_INDEX_STORE_ENABLE=NO` | Skip code indexing | Faster builds |
| `-allowProvisioningUpdates` | Auto-update certificates | Prevents signing failures |
| `-disableAutomaticPackageResolution` | Skip package updates | Faster, more stable |
| `-parallelizeTargets` | Build targets in parallel | Uses all CPU cores |
| `-scheme` | Which app to build | Selects iOS or macOS |
| `-configuration` | Debug or Release | Debug = faster, Release = optimized |
| `-destination` | Where to run | Simulator/device/Mac |
## 📚 Additional Resources
### Available Schemes
- `iOS Browser` - Main iOS app
- `macOS Browser` - Main macOS app (sometimes called "DuckDuckGo")
### Useful Commands
**Example:** See [useful-commands.sh](development-commands/useful-commands.sh)
## ✅ Task Completion Checklist
Before considering the build task complete, verify:
- [ ] Build command executed without errors
- [ ] "BUILD SUCCEEDED" message appeared
- [ ] Exit code was 0
- [ ] Build time was within expected range
- [ ] No unresolved errors in output
- [ ] If requested, both iOS and macOS builds completed
## 🚨 Critical Warnings
### For Release Builds
If building for release/production, change `-configuration Debug` to `-configuration Release`
### For Device Builds
If building for a physical iOS device (not simulator), you'll need:
- Device UUID instead of simulator ID
- Valid provisioning profiles
- Device connected and trusted
### For CI/Automation
- Always check exit codes
- Implement timeouts (15 minutes max)
- Log full output for debugging
- Clean build environment between runs
@@ -0,0 +1,447 @@
---
source: ~/DuckDuckGo/apple-browsers.git/main/.cursor/rules/duckplayer-userscript-integration.mdc
confidence: 0.9
namespace: work
last_synced: 2026-04-28
alwaysApply: false
---
# DuckPlayer UserScript Integration Guide
## Overview
DuckPlayer uses two primary UserScript components to bridge native iOS functionality with web content:
- `DuckPlayerUserScriptYouTube`: Manages communication with YouTube.com pages
- `DuckPlayerUserScriptPlayer`: Handles communication within the DuckPlayer web view
## Architecture Overview
### UserScript Communication Flow
```swift
// Communication flow:
// Web Content -> UserScript -> Native Handler -> ViewModel/Presenter
// Native UI -> Publisher -> UserScript -> Web Content
// ✅ CORRECT - Bidirectional communication pattern
final class DuckPlayerUserScriptYouTube: NSObject, Subfeature {
// Incoming: Web -> Native
func handler(forMethodNamed methodName: String) -> Subfeature.Handler? {
switch methodName {
case "onCurrentTimeStamp": return onCurrentTimeStamp
case "onYoutubeError": return onYoutubeError
default: return nil
}
}
// Outgoing: Native -> Web
private func pushToWebView(method: String, params: [String: String]) {
broker?.push(method: method, params: params, for: self, into: webView)
}
}
```
## DuckPlayerUserScriptYouTube Integration
### Component Responsibilities
**Primary Role**: Bridge between YouTube.com pages and native DuckPlayer controls
**Key Responsibilities**:
- Manages media control events (play/pause)
- Handles audio muting state
- Tracks video timestamp updates
- Responds to URL changes
- Manages script readiness state with event queuing
- Provides initial setup configuration
### Event Queuing System
The UserScript implements an event queuing system to handle events before scripts are ready:
```swift
// ✅ CORRECT - Event queuing implementation
private enum QueuedEvent {
case mediaControl(pause: Bool)
case muteAudio(mute: Bool)
case urlChanged(pageType: String)
}
private var otherEventsQueue: [QueuedEvent] = []
private var areScriptsReady = false
private func handleEvent(_ event: QueuedEvent) {
switch event {
case .urlChanged:
// URL changes are always processed immediately
processEvent(event)
default:
if areScriptsReady {
processEvent(event)
} else {
// Queue events until scripts are ready
otherEventsQueue.append(event)
}
}
}
// Process queued events when scripts become ready
func onDuckPlayerScriptsReady(params: Any, original: WKScriptMessage) -> Encodable? {
areScriptsReady = true
while !otherEventsQueue.isEmpty {
let event = otherEventsQueue.removeFirst()
processEvent(event)
}
return nil
}
```
### Publisher Integration Pattern
```swift
// ✅ CORRECT - Reactive publisher pattern
private func setupSubscriptions() {
duckPlayer?.mediaControlPublisher
.sink { [weak self] pause in
self?.handleMediaControl(pause: pause)
}
.store(in: &cancellables)
duckPlayer?.muteAudioPublisher
.sink { [weak self] mute in
self?.handleMuteAudio(mute: mute)
}
.store(in: &cancellables)
duckPlayer?.urlChangedPublisher
.sink { [weak self] url in
self?.onUrlChanged(url: url)
}
.store(in: &cancellables)
}
```
### Message Origin Security
```swift
// ✅ CORRECT - Strict origin validation
let messageOriginPolicy: MessageOriginPolicy = .only(rules: [
.exact(hostname: DuckPlayerSettingsDefault.OriginDomains.duckduckgo),
.exact(hostname: DuckPlayerSettingsDefault.OriginDomains.youtube),
.exact(hostname: DuckPlayerSettingsDefault.OriginDomains.youtubeMobile),
.exact(hostname: DuckPlayerSettingsDefault.OriginDomains.youtubeWWW),
.exact(hostname: DuckPlayerSettingsDefault.OriginDomains.youtubeNoCookie),
.exact(hostname: DuckPlayerSettingsDefault.OriginDomains.youtubeNoCookieWWW)
])
```
### Page Type Detection
```swift
// ✅ CORRECT - URL-based page type detection
func onUrlChanged(url: URL) {
areScriptsReady = false
// Determine page type for proper script behavior
let pageType = DuckPlayerUserScript.getPageType(url: url)
let shouldClearEvents = pageType != DuckPlayerUserScript.PageType.YOUTUBE
if shouldClearEvents {
// Clear queued events when navigating away from YouTube
otherEventsQueue.removeAll()
}
// Always store the latest URL change event
handleEvent(.urlChanged(pageType: pageType))
}
```
## DuckPlayerUserScriptPlayer Integration
### Component Responsibilities
**Primary Role**: Handle communication within the DuckPlayer web view
**Key Responsibilities**:
- Provides initial setup configuration
- Updates video timestamps to the view model
- Handles YouTube error states
- Manages locale and page type information
### ViewModel Communication
```swift
// ✅ CORRECT - Direct view model updates
@MainActor
private func onCurrentTimeStamp(params: Any, original: WKScriptMessage) -> Encodable? {
guard let dict = params as? [String: Any],
let timeString = dict["timestamp"] as? String,
let timeInterval = Double(timeString) else {
return [:] as [String: String]
}
// Update view model directly
viewModel?.updateTimeStamp(timeStamp: timeInterval)
return [:] as [String: String]
}
```
### Initial Setup Pattern
Both UserScripts implement initial setup handlers to configure the web environment:
```swift
// ✅ CORRECT - Initial setup with environment data
@MainActor
private func initialSetup(params: Any, original: WKScriptMessage) -> Encodable? {
struct InitialSetupResult: Encodable {
let locale: String
let playbackPaused: Bool
let pageType: String
}
let result = InitialSetupResult(
locale: Locale.current.languageCode ?? "en",
playbackPaused: false,
pageType: DuckPlayerUserScript.getPageType(url: webView?.url)
)
return result
}
```
## Common Integration Patterns
### Memory Management
```swift
// ✅ CORRECT - Proper cleanup and weak references
final class DuckPlayerUserScriptYouTube: NSObject, Subfeature {
private weak var duckPlayer: DuckPlayerControlling?
private weak var webView: WKWebView?
private var cancellables = Set<AnyCancellable>()
deinit {
// Clean up subscriptions
cancellables.forEach { $0.cancel() }
cancellables.removeAll()
}
}
```
### Error Handling
```swift
// ✅ CORRECT - Graceful error handling
@MainActor
private func onYoutubeError(params: Any, original: WKScriptMessage) -> Encodable? {
// Log error for debugging
if let errorData = params as? [String: Any] {
os_log(.error, "YouTube error: %{public}@", errorData.description)
}
// Return empty response to acknowledge receipt
return [:] as [String: String]
}
```
### Constants and Type Safety
```swift
// ✅ CORRECT - Centralized constants
struct DuckPlayerUserScript {
enum Handlers {
static let onCurrentTimeStamp = "onCurrentTimeStamp"
static let onYoutubeError = "onYoutubeError"
static let initialSetup = "initialSetup"
static let onDuckPlayerScriptsReady = "onDuckPlayerScriptsReady"
}
enum FEEvents {
static let onMediaControl = "onMediaControl"
static let onMuteAudio = "onMuteAudio"
static let onUrlChanged = "onUrlChanged"
}
enum Constants {
static let featureName = "duckPlayer"
static let timestamp = "timestamp"
static let pause = "pause"
static let mute = "mute"
static let pageType = "pageType"
static let locale = "locale"
static let localeDefault = "en"
}
}
```
## Testing UserScript Components
### Mock Testing Pattern
```swift
// ✅ CORRECT - Testing with mocks
final class DuckPlayerUserScriptYouTubeTests: XCTestCase {
private var sut: DuckPlayerUserScriptYouTube!
private var mockDuckPlayer: MockDuckPlayerControlling!
private var mockBroker: MockUserScriptMessageBroker!
override func setUp() {
super.setUp()
mockDuckPlayer = MockDuckPlayerControlling()
mockBroker = MockUserScriptMessageBroker()
sut = DuckPlayerUserScriptYouTube(duckPlayer: mockDuckPlayer)
sut.with(broker: mockBroker)
}
func testMediaControlEvent() {
// Given
let expectation = expectation(description: "Media control sent")
mockBroker.pushExpectation = expectation
// When
mockDuckPlayer.mediaControlPublisher.send(true)
// Then
wait(for: [expectation], timeout: 1.0)
XCTAssertEqual(mockBroker.lastMethod, "onMediaControl")
XCTAssertEqual(mockBroker.lastParams["pause"], "true")
}
}
```
### Event Queue Testing
```swift
// ✅ CORRECT - Testing event queuing
func testEventsQueuedBeforeScriptsReady() {
// Given scripts are not ready
// When events are sent
mockDuckPlayer.mediaControlPublisher.send(true)
mockDuckPlayer.muteAudioPublisher.send(true)
// Then no events are pushed to web view
XCTAssertNil(mockBroker.lastMethod)
// When scripts become ready
_ = sut.onDuckPlayerScriptsReady(params: [:], original: mockScriptMessage)
// Then queued events are processed
XCTAssertEqual(mockBroker.pushedMethods, ["onMediaControl", "onMuteAudio"])
}
```
## Integration Best Practices
### 1. Always Use Weak References
```swift
// ✅ CORRECT
private weak var duckPlayer: DuckPlayerControlling?
private weak var webView: WKWebView?
// ❌ INCORRECT - Avoid retain cycles
private var duckPlayer: DuckPlayerControlling?
private var webView: WKWebView?
```
### 2. Handle Script Readiness
```swift
// ✅ CORRECT - Check script readiness before sending events
if areScriptsReady {
processEvent(event)
} else {
otherEventsQueue.append(event)
}
// ❌ INCORRECT - Don't send events before scripts are ready
pushToWebView(method: "onMediaControl", params: params)
```
### 3. Use Type-Safe Parameters
```swift
// ✅ CORRECT - Type-safe parameter handling
struct TimestampUpdate: Codable {
let timestamp: TimeInterval
}
func handleTimestamp(_ data: TimestampUpdate) {
presenter.updateTimestamp(data.timestamp)
}
// ❌ INCORRECT - Avoid untyped dictionaries
func handleMessage(_ data: [String: Any]) {
if let timestamp = data["timestamp"] as? Double {
// Error-prone string-based access
}
}
```
### 4. Implement Proper Cleanup
```swift
// ✅ CORRECT - Clean up resources
deinit {
cancellables.forEach { $0.cancel() }
cancellables.removeAll()
otherEventsQueue.removeAll()
}
```
### 5. Follow Message Origin Policy
```swift
// ✅ CORRECT - Validate message origins
let messageOriginPolicy: MessageOriginPolicy = .only(rules: [
.exact(hostname: "youtube.com"),
.exact(hostname: "www.youtube.com")
])
// ❌ INCORRECT - Don't use overly permissive policies
let messageOriginPolicy: MessageOriginPolicy = .all
```
## Common Integration Issues
### Issue: Events Lost During Navigation
```swift
// ✅ SOLUTION - Clear state on navigation
func onUrlChanged(url: URL) {
areScriptsReady = false
if !isYouTubeURL(url) {
// Clear events when leaving YouTube
otherEventsQueue.removeAll()
}
}
```
### Issue: Memory Leaks from Strong References
```swift
// ✅ SOLUTION - Use weak self in closures
duckPlayer?.mediaControlPublisher
.sink { [weak self] pause in
self?.handleMediaControl(pause: pause)
}
.store(in: &cancellables)
```
### Issue: Race Conditions with Script Loading
```swift
// ✅ SOLUTION - Queue events until ready
private func handleEvent(_ event: QueuedEvent) {
guard areScriptsReady else {
otherEventsQueue.append(event)
return
}
processEvent(event)
}
```
This comprehensive guide ensures proper implementation of DuckPlayer UserScript components following established patterns for security, performance, and maintainability.
+767
View File
@@ -0,0 +1,767 @@
---
source: ~/DuckDuckGo/apple-browsers.git/main/.cursor/rules/duckplayer.mdc
confidence: 0.9
namespace: work
last_synced: 2026-04-28
alwaysApply: false
---
# DuckPlayer Implementation Guide
## Overview
DuckPlayer provides video playback within the app, separate from the web view-based player. The architecture separates concerns into distinct components using a presenter pattern, native UI views, and JavaScript integration for seamless video playback experiences.
## Architecture Components
### Core Architecture Pattern
DuckPlayer follows a presenter-driven architecture with clear separation of concerns:
```swift
// ✅ CORRECT - Presenter coordinates between components
final class DuckPlayerNativeUIPresenter {
private let navigationHandler: NativeDuckPlayerNavigationHandler
private let state: DuckPlayerState
private let pixelFiring: DuckPlayerPixelFiring
func presentPlayer(for videoID: String) {
// Coordinates pill presentation, player setup, and analytics
updateState(videoID: videoID)
configurePillType()
firePixels()
}
}
// ❌ INCORRECT - Don't manage all responsibilities in one view
struct DuckPlayerView: View {
@State private var videoID: String = ""
@State private var isPresented = false
// Don't handle navigation, state, and analytics directly in views
}
```
### State Management Pattern
Use `DuckPlayerState` for centralized video state management:
```swift
// ✅ CORRECT - Centralized state management
final class DuckPlayerState {
var videoID: String?
var hasBeenShown: Bool = false
var timestamp: TimeInterval?
func reset() {
videoID = nil
hasBeenShown = false
timestamp = nil
}
}
// ❌ INCORRECT - Scattered state across components
struct DuckPlayerView: View {
@State private var videoID: String = ""
@State private var timestamp: TimeInterval = 0
// Don't duplicate state management
}
```
## Component Responsibilities
### DuckPlayerNativeUIPresenter
**Role**: Primary coordinator and state manager for the Native UI
**Key Responsibilities**:
- Manages presentation lifecycle of player UI components
- Coordinates between pill types (welcome, entry, re-entry)
- Handles user interactions and navigation events
- Manages constraint updates and visibility state
- Integrates with app navigation and browser features
- Handles orientation changes and UI adaptations
- Manages pixel firing for analytics tracking
- Controls toast notifications and dismiss count tracking
```swift
// ✅ CORRECT - Presenter pattern implementation
final class DuckPlayerNativeUIPresenter {
private weak var containerView: DuckPlayerContainer?
private let navigationHandler: NativeDuckPlayerNavigationHandler
private let state: DuckPlayerState
private let pixelFiring: DuckPlayerPixelFiring
func presentWelcomePill() {
// Configure welcome pill for first-time users
configureContainerForPill(.welcome)
fireWelcomePillPixel()
}
func presentEntryPill(for videoID: String) {
// Configure entry pill for returning users
state.videoID = videoID
configureContainerForPill(.entry)
fireEntryPillPixel()
}
func presentReEntryPill(for videoID: String) {
// Configure re-entry pill for previously watched videos
state.videoID = videoID
configureContainerForPill(.reEntry)
fireReEntryPillPixel()
}
}
```
### NativeDuckPlayerNavigationHandler
**Role**: Manages video playback navigation and browser integration
```swift
// ✅ CORRECT - Navigation handler pattern
final class NativeDuckPlayerNavigationHandler {
private let webView: WKWebView
private let presenter: DuckPlayerNativeUIPresenter
func handleYouTubeURL(_ url: URL) -> Bool {
guard shouldHandleNatively(url) else { return false }
let videoID = extractVideoID(from: url)
presenter.presentPlayer(for: videoID)
return true
}
private func shouldHandleNatively(_ url: URL) -> Bool {
// Check if URL should be handled by native player
return isYouTubeURL(url) && isNativeUIEnabled()
}
}
// ❌ INCORRECT - Don't handle navigation directly in views
struct DuckPlayerView: View {
func handleURL(_ url: URL) {
// Don't put navigation logic in views
}
}
```
## View Architecture
### Pill Management System
DuckPlayer uses a three-tier pill system based on user interaction history:
```swift
// ✅ CORRECT - Pill type management
enum DuckPlayerPillType {
case welcome // First-time users (priming modal not yet presented)
case entry // Returning users viewing new videos
case reEntry // Users returning to previously watched videos
}
final class DuckPlayerContainer: UIView {
private var currentPillType: DuckPlayerPillType?
func configurePill(_ type: DuckPlayerPillType, for videoID: String) {
switch type {
case .welcome:
presentWelcomePill()
case .entry:
presentEntryPill(videoID: videoID)
case .reEntry:
presentReEntryPill(videoID: videoID)
}
}
}
```
### SwiftUI View Components
Follow reactive patterns for view models:
```swift
// ✅ CORRECT - Reactive view model pattern
final class DuckPlayerWelcomePillViewModel: ObservableObject {
@Published var isAnimating = false
@Published var isPresented = false
private let pixelFiring: DuckPlayerPixelFiring
private let onDismiss: () -> Void
init(pixelFiring: DuckPlayerPixelFiring, onDismiss: @escaping () -> Void) {
self.pixelFiring = pixelFiring
self.onDismiss = onDismiss
}
func startAnimation() {
isAnimating = true
pixelFiring.fireWelcomePillShownPixel()
}
func handleUserTap() {
pixelFiring.fireWelcomePillTappedPixel()
onDismiss()
}
}
// ❌ INCORRECT - Don't handle business logic directly in views
struct DuckPlayerWelcomePillView: View {
@State private var isAnimating = false
var body: some View {
// Don't put pixel firing and business logic here
Button("Watch in DuckPlayer") {
// Business logic should be in view model
Analytics.shared.firePixel(.welcomePillTapped)
}
}
}
```
## UserScript Integration
### JavaScript Bridge Pattern
Use UserScript components for native-web communication:
```swift
// ✅ CORRECT - UserScript integration pattern
final class DuckPlayerUserScriptYouTube: NSObject, UserScript {
private let name = "DuckPlayerUserScriptYouTube"
private let source = DuckPlayerUserScriptSource.youtube
func messageReceived(_ message: Any) {
guard let dict = message as? [String: Any],
let messageType = dict["type"] as? String else { return }
switch messageType {
case "timestampUpdate":
handleTimestampUpdate(dict)
case "playerStateChange":
handlePlayerStateChange(dict)
case "error":
handleError(dict)
default:
break
}
}
private func handleTimestampUpdate(_ data: [String: Any]) {
guard let timestamp = data["timestamp"] as? TimeInterval else { return }
presenter.updateVideoTimestamp(timestamp)
}
}
// ❌ INCORRECT - Don't handle JavaScript communication directly in views
struct DuckPlayerWebView: UIViewRepresentable {
func makeUIView(context: Context) -> WKWebView {
let webView = WKWebView()
// Don't add message handlers directly here
return webView
}
}
```
### Event Queuing System
Implement event queuing for script readiness:
```swift
// ✅ CORRECT - Event queuing pattern
final class DuckPlayerUserScript {
private var eventQueue: [UserScriptEvent] = []
private var isScriptReady = false
func queueEvent(_ event: UserScriptEvent) {
if isScriptReady {
processEvent(event)
} else {
eventQueue.append(event)
}
}
func onScriptReady() {
isScriptReady = true
eventQueue.forEach { processEvent($0) }
eventQueue.removeAll()
}
}
```
## Analytics Integration
### Pixel Firing Protocol
Use protocol-based pixel firing with debouncing:
```swift
// ✅ CORRECT - Protocol-based pixel firing
protocol DuckPlayerPixelFiring {
func fireWelcomePillShownPixel()
func fireWelcomePillTappedPixel()
func fireEntryPillShownPixel()
func fireVideoPlaybackStartedPixel()
func fireDailyPixel(_ pixel: DuckPlayerDailyPixel)
}
final class DuckPlayerPixelHandler: DuckPlayerPixelFiring {
private let pixelKit: PixelKit
private let debouncer: PixelDebouncer
func fireWelcomePillShownPixel() {
debouncer.debounce {
pixelKit.fire(.duckPlayerWelcomePillShown)
}
}
}
// ❌ INCORRECT - Don't fire pixels directly from views
struct DuckPlayerView: View {
var body: some View {
Button("Play") {
// Don't fire pixels directly
PixelKit.shared.fire(.duckPlayerPlayTapped)
}
}
}
```
### DuckPlayer Native Pixels
DuckPlayer Native fires various pixels to track user interactions and system events:
#### Pill Interaction Pixels
```swift
// Welcome Pill (first-time users)
.duckPlayerNativeWelcomePillShown // When welcome pill is displayed
.duckPlayerNativeWelcomePillTapped // When user taps welcome pill
.duckPlayerNativeWelcomePillDismissed // When welcome pill is dismissed
// Entry Pill (returning users, new videos)
.duckPlayerNativeEntryPillShown // When entry pill is displayed
.duckPlayerNativeEntryPillTapped // When user taps entry pill
.duckPlayerNativeEntryPillDismissed // When entry pill is dismissed
// Re-entry Pill (previously watched videos)
.duckPlayerNativeReEntryPillShown // When re-entry pill is displayed
.duckPlayerNativeReEntryPillTapped // When user taps re-entry pill
.duckPlayerNativeReEntryPillDismissed // When re-entry pill is dismissed
```
#### Video Playback Pixels
```swift
// Playback events
.duckPlayerNativeVideoPlaybackStarted // When video starts playing
.duckPlayerNativeVideoPlaybackPaused // When video is paused
.duckPlayerNativeVideoPlaybackResumed // When video is resumed
.duckPlayerNativeVideoPlaybackCompleted // When video finishes
// Daily unique playback tracking
.duckPlayerNativeDailyVideoPlayed // Fired once per day when any video is played
```
#### YouTube Error Pixels
DuckPlayer Native tracks YouTube-specific errors with both volume (impression) and daily-unique pixels:
```swift
// Sign-in Required Errors
.duckPlayerNativeYouTubeSignInErrorImpression // Every occurrence
.duckPlayerNativeYouTubeSignInErrorDaily // Once per day
// Age-Restricted Content Errors
.duckPlayerNativeYouTubeAgeRestrictedErrorImpression // Every occurrence
.duckPlayerNativeYouTubeAgeRestrictedErrorDaily // Once per day
// No-Embed Errors (embedding disabled)
.duckPlayerNativeYouTubeNoEmbedErrorImpression // Every occurrence
.duckPlayerNativeYouTubeNoEmbedErrorDaily // Once per day
// Unknown/Generic Errors
.duckPlayerNativeYouTubeUnknownErrorImpression // Every occurrence
.duckPlayerNativeYouTubeUnknownErrorDaily // Once per day
```
#### Error Handling Implementation
YouTube errors are handled in the UserScript layer:
```swift
// In DuckPlayerUserScriptPlayer.swift
@MainActor
private func onYoutubeError(params: Any, original: WKScriptMessage) -> Encodable? {
let (volumePixel, dailyPixel) = getPixelsForNativeYouTubeErrorParams(params)
DailyPixel.fire(pixel: dailyPixel)
Pixel.fire(pixel: volumePixel)
return [:] as [String: String]
}
private func getPixelsForNativeYouTubeErrorParams(_ params: Any) -> (Pixel.Event, Pixel.Event) {
if let paramsDict = params as? [String: Any],
let errorParam = paramsDict["error"] as? String {
switch errorParam {
case "sign-in-required":
return (.duckPlayerNativeYouTubeSignInErrorImpression, .duckPlayerNativeYouTubeSignInErrorDaily)
case "age-restricted":
return (.duckPlayerNativeYouTubeAgeRestrictedErrorImpression, .duckPlayerNativeYouTubeAgeRestrictedErrorDaily)
case "no-embed":
return (.duckPlayerNativeYouTubeNoEmbedErrorImpression, .duckPlayerNativeYouTubeNoEmbedErrorDaily)
default:
return (.duckPlayerNativeYouTubeUnknownErrorImpression, .duckPlayerNativeYouTubeUnknownErrorDaily)
}
}
return (.duckPlayerNativeYouTubeUnknownErrorImpression, .duckPlayerNativeYouTubeUnknownErrorDaily)
}
```
### Analytics Best Practices
```swift
// ✅ CORRECT - Centralized analytics tracking
final class DuckPlayerAnalytics {
private let pixelFiring: DuckPlayerPixelFiring
func trackPillPresentation(_ type: DuckPlayerPillType) {
switch type {
case .welcome:
pixelFiring.fireWelcomePillShownPixel()
case .entry:
pixelFiring.fireEntryPillShownPixel()
case .reEntry:
pixelFiring.fireReEntryPillShownPixel()
}
}
func trackVideoPlayback(duration: TimeInterval) {
let parameters = ["duration": String(duration)]
pixelFiring.fireVideoPlaybackPixel(parameters: parameters)
}
func trackYouTubeError(_ error: DuckPlayerError) {
// Errors are tracked in UserScript layer
// This method exists for future expansion
}
}
```
### Pixel Naming Convention
All DuckPlayer Native pixels follow this naming pattern:
- **Volume pixels**: `duckplayer_native_{event}_impression_ios_{formfactor}`
- **Daily pixels**: `duckplayer_native_{event}_daily-unique_ios_{formfactor}`
The formfactor (phone/tablet) is automatically appended by the pixel infrastructure.
## Toast Notification System
### Toast Implementation Pattern
```swift
// ✅ CORRECT - Toast notification system
final class DuckPlayerToastManager {
private weak var containerView: UIView?
func showToast(_ message: String, position: ToastPosition = .top) {
let toastView = DuckPlayerToastView(message: message)
containerView?.addSubview(toastView)
toastView.show(at: position) { [weak self] in
self?.hideToast(toastView)
}
}
private func hideToast(_ toastView: DuckPlayerToastView) {
toastView.hide {
toastView.removeFromSuperview()
}
}
}
struct DuckPlayerToastView: View {
let message: String
@State private var isVisible = false
var body: some View {
Text(message)
.padding()
.background(Color(designSystemColor: .surface))
.cornerRadius(8)
.scaleEffect(isVisible ? 1.0 : 0.8)
.opacity(isVisible ? 1.0 : 0.0)
.animation(.spring(response: 0.3), value: isVisible)
.onAppear {
isVisible = true
}
}
}
```
## Variant Management
### Variant Configuration Pattern
```swift
// ✅ CORRECT - Variant configuration system
enum DuckPlayerVariant: String, CaseIterable {
case classic = "Web"
case nativeOptIn = "Opt-in"
case nativeOptOut = "Opt-out"
var configuration: DuckPlayerConfiguration {
switch self {
case .classic:
return DuckPlayerConfiguration(
nativeUIEnabled: false,
playerMode: .alwaysAsk,
newTabBehavior: true,
serpIntegration: false
)
case .nativeOptIn:
return DuckPlayerConfiguration(
nativeUIEnabled: true,
playerMode: .askUserPreference,
autoplayEnabled: true,
serpIntegration: true,
primingModalEnabled: true
)
case .nativeOptOut:
return DuckPlayerConfiguration(
nativeUIEnabled: true,
playerMode: .automatic,
autoplayEnabled: true,
serpIntegration: true,
primingModalEnabled: false
)
}
}
}
// ❌ INCORRECT - Don't hardcode variant configurations
struct DuckPlayerSettings {
var isNativeEnabled: Bool {
// Don't hardcode variant logic
return UserDefaults.standard.bool(forKey: "native_enabled")
}
}
```
### Runtime Variant Switching
```swift
// ✅ CORRECT - Runtime variant management
final class DuckPlayerVariantManager {
private let appSettings: AppSettings
var currentVariant: DuckPlayerVariant {
get {
let rawValue = appSettings.duckPlayerVariant
return DuckPlayerVariant(rawValue: rawValue) ?? .classic
}
set {
appSettings.duckPlayerVariant = newValue.rawValue
applyVariantConfiguration(newValue.configuration)
}
}
private func applyVariantConfiguration(_ config: DuckPlayerConfiguration) {
appSettings.duckPlayerNativeUIEnabled = config.nativeUIEnabled
appSettings.duckPlayerSerpIntegration = config.serpIntegration
appSettings.duckPlayerAutoplayEnabled = config.autoplayEnabled
// Notify components of configuration change
NotificationCenter.default.post(name: .duckPlayerVariantChanged, object: config)
}
}
```
## Performance Optimization
### Lazy Loading Pattern
```swift
// ✅ CORRECT - Lazy loading for performance
final class DuckPlayerNativeUIPresenter {
private lazy var welcomePillViewModel = DuckPlayerWelcomePillViewModel(
pixelFiring: pixelFiring,
onDismiss: { [weak self] in self?.dismissWelcomePill() }
)
private lazy var entryPillViewModel = DuckPlayerEntryPillViewModel(
pixelFiring: pixelFiring,
onPlay: { [weak self] in self?.startVideoPlayback() }
)
func presentWelcomePill() {
// Only create view model when needed
containerView?.configurePill(.welcome, viewModel: welcomePillViewModel)
}
}
// ❌ INCORRECT - Don't create all view models upfront
final class DuckPlayerNativeUIPresenter {
private let welcomePillViewModel: DuckPlayerWelcomePillViewModel
private let entryPillViewModel: DuckPlayerEntryPillViewModel
private let reEntryPillViewModel: DuckPlayerMiniPillViewModel
init() {
// Don't create all view models immediately
welcomePillViewModel = DuckPlayerWelcomePillViewModel(...)
entryPillViewModel = DuckPlayerEntryPillViewModel(...)
reEntryPillViewModel = DuckPlayerMiniPillViewModel(...)
}
}
```
## Testing Patterns
### Presenter Testing
```swift
// ✅ CORRECT - Testing presenter components
final class DuckPlayerNativeUIPresenterTests: XCTestCase {
private var sut: DuckPlayerNativeUIPresenter!
private var mockNavigationHandler: MockNativeDuckPlayerNavigationHandler!
private var mockPixelFiring: MockDuckPlayerPixelFiring!
private var mockState: DuckPlayerState!
override func setUp() {
super.setUp()
mockNavigationHandler = MockNativeDuckPlayerNavigationHandler()
mockPixelFiring = MockDuckPlayerPixelFiring()
mockState = DuckPlayerState()
sut = DuckPlayerNativeUIPresenter(
navigationHandler: mockNavigationHandler,
pixelFiring: mockPixelFiring,
state: mockState
)
}
func testPresentWelcomePill() {
// When
sut.presentWelcomePill()
// Then
XCTAssertTrue(mockPixelFiring.fireWelcomePillShownPixelCalled)
XCTAssertEqual(sut.currentPillType, .welcome)
}
}
```
### UserScript Testing
```swift
// ✅ CORRECT - Testing UserScript components
final class DuckPlayerUserScriptTests: XCTestCase {
private var sut: DuckPlayerUserScriptYouTube!
private var mockPresenter: MockDuckPlayerPresenter!
func testTimestampUpdateMessage() {
// Given
let message = ["type": "timestampUpdate", "timestamp": 120.5]
// When
sut.messageReceived(message)
// Then
XCTAssertEqual(mockPresenter.lastTimestampUpdate, 120.5)
}
}
```
## Common Patterns
### Error Handling
```swift
// ✅ CORRECT - Comprehensive error handling
enum DuckPlayerError: Error {
case videoNotFound
case networkError
case playbackError(underlying: Error)
case invalidConfiguration
}
final class DuckPlayerErrorHandler {
private let pixelFiring: DuckPlayerPixelFiring
func handleError(_ error: DuckPlayerError) {
switch error {
case .videoNotFound:
pixelFiring.fireErrorPixel(.videoNotFound)
showErrorToast("Video not available")
case .networkError:
pixelFiring.fireErrorPixel(.networkError)
showErrorToast("Network connection required")
case .playbackError(let underlying):
pixelFiring.fireErrorPixel(.playbackError, parameters: ["error": underlying.localizedDescription])
showErrorToast("Playback error occurred")
case .invalidConfiguration:
pixelFiring.fireErrorPixel(.invalidConfiguration)
// Handle configuration errors silently
}
}
}
```
### Memory Management
```swift
// ✅ CORRECT - Proper memory management
final class DuckPlayerNativeUIPresenter {
private weak var containerView: DuckPlayerContainer?
private var cancellables = Set<AnyCancellable>()
deinit {
cancellables.removeAll()
cleanupResources()
}
private func cleanupResources() {
// Clean up any retained resources
containerView?.removeFromSuperview()
state.reset()
}
}
```
## Migration Guidelines
### Integrating DuckPlayer
When adding DuckPlayer to new areas:
1. **Use the presenter pattern** - Don't put business logic in views
2. **Follow the pill system** - Implement appropriate pill types for user journey
3. **Integrate analytics** - Use the pixel firing protocol for tracking
4. **Handle variants** - Support all three DuckPlayer variants
5. **Test thoroughly** - Write tests for presenter, UserScript, and view components
### Common Integration Mistakes
```swift
// ❌ INCORRECT - Don't bypass the presenter
struct MyFeatureView: View {
@State private var showDuckPlayer = false
var body: some View {
Button("Play Video") {
// Don't create DuckPlayer components directly
showDuckPlayer = true
}
}
}
// ✅ CORRECT - Use the presenter pattern
struct MyFeatureView: View {
private let duckPlayerPresenter: DuckPlayerNativeUIPresenter
var body: some View {
Button("Play Video") {
duckPlayerPresenter.presentPlayer(for: videoID)
}
}
}
```
This guide provides the foundation for implementing and maintaining DuckPlayer components following established patterns and best practices in the DuckDuckGo browser codebase.
@@ -0,0 +1,762 @@
---
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
+378
View File
@@ -0,0 +1,378 @@
---
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.
+177
View File
@@ -0,0 +1,177 @@
---
source: ~/DuckDuckGo/apple-browsers.git/main/.cursor/rules/general.mdc
confidence: 0.9
namespace: work
last_synced: 2026-04-28
alwaysApply: true
---
# DuckDuckGo Browser Development Rules Overview
## Project Context
This is the DuckDuckGo browser for iOS and macOS, built with privacy-first principles, modern Swift patterns, and cross-platform architecture.
**Key Directories:**
- `iOS/` - iOS browser app (UIKit + SwiftUI hybrid)
- `macOS/` - macOS browser app (AppKit + SwiftUI hybrid)
- `SharedPackages/` - Cross-platform Swift packages
## Architecture Summary
- **Pattern**: MVVM + Coordinators + Dependency Injection
- **UI**: SwiftUI preferred, UIKit/AppKit for legacy
- **Storage**: Core Data + GRDB + Keychain for sensitive data
- **Design**: DesignResourcesKit for colors/icons (MANDATORY)
- **Testing**: >80% coverage required
## Available Rules (`.cursor/rules/`)
Development rules are stored in `.cursor/rules/`.
You MUST list all the available rules and you MUST consult the appropriate rule file before starting any work!
### Core (Always Apply)
- `anti-patterns.mdc` - What NOT to do; use with ViewModels, testing, WebView work
- `code-style.mdc` - Swift style guide
- `privacy-security.mdc` - Privacy requirements; use with network calls, analytics, credentials
- `import-hygiene.mdc` - Import management and SwiftUI preview scoping
- `logging-guidelines.mdc` - Logger usage (never print())
### Architecture & Patterns
- `architecture.mdc` - MVVM, DI patterns; use for new ViewModels
- `project-structure.mdc` - Directory layout
- `browserserviceskit-integration.mdc` - BSK integration; use for cross-platform code
- `shared-packages.mdc` - Cross-platform packages; use for cross-platform code
- `subscription-architecture.mdc` - Privacy Pro subscription
### Feature Development
- `feature-flags.mdc` + `feature-flags-addition.mdc` - Feature flags
- `abn-experiment-framework.mdc` - A/B testing
- `user-defaults-storage.mdc` - UserDefaults, @UserDefaultsWrapper; use for settings/preferences
### UI Development
- `swiftui-style.mdc` - SwiftUI + DesignResourcesKit; use for new ViewModels, UI work
- `swiftui-advanced.mdc` - Advanced SwiftUI patterns
- `design-system-designresourceskit.mdc` - Colors, typography, icons (MANDATORY)
- `webkit-browser.mdc` - WebView patterns
### Platform-Specific
- `ios-architecture.mdc` - iOS AppDependencyProvider, MainCoordinator, UIKit
- `ios-tracker-blocking-implementation.mdc` - iOS content blocking
- `macos-window-management.mdc` - macOS windows
- `macos-system-integration.mdc` - macOS system services
- `macos-singletons-removal.mdc` - Removing singletons from macOS
### Feature-Specific
- `duckplayer.mdc` + `duckplayer-userscript-integration.mdc` - DuckPlayer
- `securevault-guidelines.mdc` - Credentials/vault storage
- `app-lifecycle-state-machine.mdc` - App state management
- `network-quality-*.mdc` (4 files) - Network quality assessment
### Testing & Quality
- `testing.mdc` - Testing patterns, xcodebuild commands
- `ui-testing.mdc` - UI testing for macOS browser
- `maestro-device-selection.mdc` - Maestro test device config
- `performance-optimization.mdc` - Performance; use with network calls
### Workflow & Process
- `development-commands.mdc` - Build commands
- `pull-request.mdc` + `branch-naming-conventions.mdc` - PRs and git workflow
- `analytics-patterns.mdc` - Pixel analytics
## Quick Start Checklist
### Before Writing Any Code:
1. ✅ Read `privacy-security.mdc` - Privacy is non-negotiable
2. ✅ Check platform rules (`ios-architecture.mdc` or `macos-system-integration.mdc`)
3. ✅ Review `anti-patterns.mdc` - Avoid common mistakes
4. ✅ REMEMBER: NEVER commit, push, or run tests without explicit user permission or unless explicitly asked to
### For UI Development:
1. ✅ Use `swiftui-style.mdc` for SwiftUI components
2. ✅ MUST use DesignResourcesKit colors: `Color(designSystemColor: .textPrimary)`
3. ✅ MUST use DesignResourcesKit icons: `DesignSystemImages.Glyphs.Size16.add`
### For New Features:
1. ✅ Follow `architecture.mdc` for MVVM + DI patterns
2. ✅ Use AppDependencyProvider (iOS) or equivalent (macOS)
3. ✅ Write tests per `testing.mdc` requirements
## Critical Don'ts (from anti-patterns.mdc)
- ❌ NEVER commit, push changes, create or delete branches on git or trigger github actions without EXPLICIT user permission
- ❌ NEVER run tests without EXPLICIT user permission or if user explicitly asked to in their prompt
- ❌ NEVER use `.shared` singletons - use dependency injection instead
- ❌ NEVER hardcode colors/icons (use DesignResourcesKit)
- ❌ NEVER update UI without @MainActor
- ❌ NEVER ignore privacy implications
- ❌ NEVER force unwrap without justification
- ❌ NEVER use `print()` statements - use appropriate Logger extensions instead
## Logging Guidelines
**NEVER use `print()` in production code. ALWAYS use appropriate Logger extensions:**
**Example:** See [logging-guidelines.swift](general/logging-guidelines.swift)
**Available Logger categories:**
- `Logger.general` - General app functionality
- `Logger.network` - Network requests and responses
- `Logger.ui` - UI updates and user interactions
- `Logger.tests` - Test-specific logging (import `os.log` in tests)
**Benefits of Logger extensions:**
- Structured logging with categories and levels
- Better performance than print() statements
- Automatic log collection and filtering
- Integration with system logging infrastructure
## Dependency Injection Pattern (iOS)
**Example:** See [dependency-injection-pattern.swift](general/dependency-injection-pattern.swift)
## Design System Usage (MANDATORY)
**Example:** See [design-system-usage.swift](general/design-system-usage.swift)
## Code Review Checklist
1. Privacy implications assessed (`privacy-security.mdc`)
2. Design system properly used (`design-system-designresourceskit.mdc`)
3. Architecture patterns followed (platform-specific rules)
4. Anti-patterns avoided (`anti-patterns.mdc`)
5. Tests written and passing (`testing.mdc`)
6. Performance considered (`performance-optimization.mdc`)
7. PR template followed (`pull-request.mdc`)
## Git & Testing Workflow Rules
### 🚨 MANDATORY: Never Auto-Execute Commands
**NEVER commit, push, or run tests without EXPLICIT user permission.**
#### Git Workflow:
1. Make file changes as requested
2. **STOP** before any `git add`, `git commit`, or `git push` commands
3. **ASK** the user: "Should I commit/push these changes?"
4. **WAIT** for explicit permission (e.g., "yes", "commit it", "push it", "go ahead")
5. Only then execute git commands
#### Testing Workflow:
1. Write or modify code as requested
2. **STOP** before running any tests (`swift test`, `npm test`, `xcodebuild test`, etc.)
3. **ASK** the user: "Should I run the tests?"
4. **WAIT** for explicit permission (e.g., "yes", "run tests", "test it")
5. Only then execute test commands
#### What NOT to Do:
**Example:** See [git-workflow-wrong.sh](general/git-workflow-wrong.sh)
#### What TO Do:
**Example:** See [git-workflow-correct.sh](general/git-workflow-correct.sh)
**These rules have NO exceptions. Always ask before executing git or test commands.**
## Communication Style
- Keep responses concise and focused on the task
- Avoid enthusiastic language like "Perfect!", "You are absolutely right!", "Excellent!"
- Keep work summaries brief - focus on what was changed, not how great it is
- Let the code quality speak for itself rather than using excessive praise
---
This overview ensures you understand the project context and know which specific rules to consult for your development task.
@@ -0,0 +1,54 @@
---
source: ~/DuckDuckGo/apple-browsers.git/main/.cursor/rules/import-hygiene.mdc
confidence: 0.9
namespace: work
last_synced: 2026-04-28
alwaysApply: true
---
# Import Hygiene & Preview-Only Imports
## Purpose
Prevent accidental or unrequested import churn that causes build/lint issues and diffs unrelated to the task. Ensure SwiftUI is only imported where required (e.g., #Preview blocks) and avoid touching existing imports unless strictly necessary.
## Rules (Always Apply)
1. Do not change imports unless:
- A new symbol is introduced that the compiler cannot resolve without the import
- An existing import is provably unused and removal is part of the explicit task scope
- The change resolves a red compiler error you introduced in this edit
2. Keep platform/framework imports minimal and local:
- Prefer `import AppKit` for macOS UI code
- Prefer `import UIKit` for iOS UI code
- Do not add `import SwiftUI` to AppKit/UIKit view controllers unless they embed SwiftUI.
3. Scope SwiftUI to previews:
- Only import `SwiftUI` inside `#if DEBUG` blocks for `#Preview` declarations
- **Example:** See [swiftui-preview-import.swift](import-hygiene/swiftui-preview-import.swift)
4. Keep Shared Modules stable:
- Do not remove `import Common` or other project modules unless a dedicated cleanup task
- If a module is required elsewhere in the file, do not move or duplicate it
5. Lint & Build first, then adjust:
- If a file shows missing-types errors after your edits (e.g., `Cannot find type 'FireproofDomains'`), prefer adding the specific missing import required for those existing symbols
- Avoid speculative imports
6. No import reordering for style-only reasons unless the repository enforces it via formatter
## Rationale
- Unnecessary import edits generate churn and can break platform- or target-specific build settings
- Scoping SwiftUI to previews avoids accidental framework inclusion and linking in non-preview code paths
## Examples
- **CORRECT (AppKit-only controller):** See [appkit-only-controller.swift](import-hygiene/appkit-only-controller.swift)
- **CORRECT (preview-only SwiftUI):** See [preview-only-swiftui.swift](import-hygiene/preview-only-swiftui.swift)
- **AVOID:** See [import-to-avoid.swift](import-hygiene/import-to-avoid.swift)
## Enforcement Guidance
- During PR review, reject changes that add/remove imports without a clear necessity
- Prefer comments in code review over automated reordering unless enforced by tooling
@@ -0,0 +1,207 @@
---
source: ~/DuckDuckGo/apple-browsers.git/main/.cursor/rules/instrumentation-facades.mdc
confidence: 0.9
namespace: work
last_synced: 2026-04-28
description: Pattern for abstracting pixel and wide event instrumentation behind domain-specific protocols.
alwaysApply: false
---
# Instrumentation Facades
Feature code often becomes verbose when sending many pixels, or mixing pixel calls and wide event lifecycle management.
Consider a subscription purchase flow that needs to:
- Fire a daily pixel when purchase starts
- Start a wide event flow
- Update the wide event with timing data
- Fire unique pixels on success
- Complete the wide event with success/failure/cancelled status
- Handle multiple error cases with different failing steps
This leads to instrumentation code scattered throughout the feature, making it hard to:
- Understand the feature's core logic
- Test the feature in isolation
- Modify instrumentation without touching feature code
- Ensure all instrumentation points are covered
We can improve this using the facade pattern, abstracting our instrumentation behind a protocol. This is done by defining a protocol with domain-specific hooks that the feature calls, then implementing the protocol in a dedicated object that handles all instrumentation.
### Benefits
1. **Feature code emits domain events only** - Cleaner, more readable feature logic
2. **Instrumentation logic is centralized** - Easy to audit and modify
3. **Easy to inject mocks for unit testing** - Test feature behavior without pixel dependencies
## File Organization
Instrumentation facades should be placed in the **same module as the feature** they instrument:
| Component | Location |
|-----------|----------|
| Protocol | Feature module (e.g., `Subscription/SubscriptionPurchaseInstrumentation.swift`) |
| Default Implementation | Same module as protocol |
| Mock for Testing | Test target or same module |
For features that span iOS and macOS, place the protocol and implementation in a shared package (e.g., `BrowserServicesKit`).
## Pattern Structure
### Step 1: Define the Protocol
Create a protocol with methods for each instrumentation hook your feature needs. Name methods after domain events, not pixels:
```swift
public protocol SubscriptionPurchaseInstrumentation: AnyObject {
func purchaseAttemptStarted(selectionID: String, freeTrialEligible: Bool, ...)
func purchaseCancelled()
func purchaseFailed(step: FailingStep, error: Error)
func activationSucceeded()
// ... other domain events
}
```
### Step 2: Implement the Default Class
Create an implementation that translates domain events to pixels and wide events. The implementation:
- Fires appropriate pixels (standard, daily, or unique)
- Manages wide event lifecycle (start, update, complete)
- Tracks internal state like the current wide event data
```swift
public final class DefaultSubscriptionPurchaseInstrumentation: SubscriptionPurchaseInstrumentation {
private let wideEvent: WideEventManaging
private var purchaseWideEventData: SubscriptionPurchaseWideEventData?
public func purchaseAttemptStarted(...) {
DailyPixel.fireDailyAndCount(pixel: .subscriptionPurchaseAttempt, ...)
purchaseWideEventData = SubscriptionPurchaseWideEventData(...)
wideEvent.startFlow(purchaseWideEventData!)
}
public func activationSucceeded() {
UniquePixel.fire(pixel: .subscriptionActivated)
wideEvent.completeFlow(purchaseWideEventData!, status: .success, ...)
}
}
```
### Step 3: Use in Feature Code
Inject the instrumentation protocol and call it at appropriate points in your feature logic:
```swift
final class SubscriptionPurchaseFeature {
private let instrumentation: SubscriptionPurchaseInstrumentation
func subscriptionSelected(...) async {
instrumentation.purchaseAttemptStarted(...)
switch await performPurchase() {
case .success:
instrumentation.activationSucceeded()
case .failure(let error):
instrumentation.purchaseFailed(step: .accountPayment, error: error)
}
}
}
```
## Dependency Injection
### Constructor Injection (Preferred)
Pass the instrumentation as an init parameter:
```swift
final class SubscriptionPurchaseFeature {
private let instrumentation: SubscriptionPurchaseInstrumentation
init(instrumentation: SubscriptionPurchaseInstrumentation = DefaultSubscriptionPurchaseInstrumentation()) {
self.instrumentation = instrumentation
}
}
```
### Property Injection
For cases where the instrumentation is set after initialization (e.g., UserScripts):
```swift
final class DebugUserScript {
weak var instrumentation: TabInstrumentationProtocol?
}
// In the parent object:
private let instrumentation = TabInstrumentation()
func configureUserScripts() {
userScripts.debugScript.instrumentation = instrumentation
}
```
## Testing with Mocks
Create a mock that records method calls for verification:
```swift
final class MockSubscriptionPurchaseInstrumentation: SubscriptionPurchaseInstrumentation {
private(set) var purchaseAttemptStartedCalls: [...] = []
private(set) var activationSucceededCallCount = 0
func purchaseAttemptStarted(...) {
purchaseAttemptStartedCalls.append(...)
}
}
```
Then verify behavior in tests:
```swift
func testPurchaseSuccess() async {
let mock = MockSubscriptionPurchaseInstrumentation()
let feature = SubscriptionPurchaseFeature(instrumentation: mock)
await feature.subscriptionSelected(...)
XCTAssertEqual(mock.activationSucceededCallCount, 1)
}
```
## Alternative: EventMapping for Shared Packages
For features in shared Swift packages that can't directly import `Pixel` or `PixelKit`, use `EventMapping` instead of a full instrumentation facade. See `SharedPackages/BrowserServicesKit/Sources/Common/EventMapping.swift` for the base class.
The pattern:
1. **Define events in the shared package** as an enum (e.g., `MyFeatureEvent`)
2. **Create an EventMapper in the app target** that switches on events and fires the appropriate pixels
3. **Inject the EventMapping** into your shared package class
See `MaliciousSiteProtectionEventMapper` in `iOS/DuckDuckGo/MaliciousSiteProtection/Events/` for a production example.
**When to use EventMapping vs Instrumentation Facades:**
- Use **EventMapping** when: Feature is in a shared package, events are simple fire-and-forget
- Use **Instrumentation Facades** when: Feature needs wide event lifecycle management, complex state tracking, or many related instrumentation calls
## When to Use Instrumentation Facades
Use this pattern when any of the following are true:
- A feature has 3+ distinct instrumentation calls
- Pixels and wide events are mixed in the same flow
- You need to test feature logic without pixel side effects
- Instrumentation logic is complex (conditional firing, parameter assembly)
Skip this pattern for:
- Simple features with 1-2 pixels
## Design Guidelines
1. **Name methods after domain events, not pixels**: Use `purchaseAttemptStarted`, not `fireSubscriptionPurchaseAttemptPixel`.
2. **Keep the protocol focused**: One protocol per feature/flow. Don't create a mega-protocol for all app instrumentation.
3. **Hide implementation details**: The protocol shouldn't expose whether something is a daily pixel, unique pixel, or wide event.
4. **Document expected call order**: If methods must be called in sequence (e.g., `startPurchase` before `completePurchase`), document this in the protocol.
## Related Documentation
- `pixels.mdc` - One-off instrumentation events
@@ -0,0 +1,447 @@
---
source: ~/DuckDuckGo/apple-browsers.git/main/.cursor/rules/ios-architecture.mdc
confidence: 0.9
namespace: work
last_synced: 2026-04-28
alwaysApply: false
---
# iOS DuckDuckGo Browser Architecture Rules
## Dependency Injection and AppDependencies
### Use AppDependencyProvider Pattern
ALWAYS use the centralized dependency provider for all service dependencies:
```swift
// ✅ CORRECT - Use the shared dependency provider
final class FeatureViewModel: ObservableObject {
private let networkService: NetworkServiceProtocol
init(dependencies: DependencyProvider = AppDependencyProvider.shared) {
self.networkService = dependencies.networkService
}
}
// ❌ INCORRECT - Direct singleton access
final class FeatureViewModel: ObservableObject {
private let networkService = NetworkService.shared // Avoid singletons
}
```
### Protocol-Based Dependencies
ALWAYS define protocols for dependencies to enable testing:
```swift
// ✅ CORRECT - Protocol abstraction
protocol FeatureServiceProtocol {
func fetchData() async throws -> [Item]
}
final class FeatureService: FeatureServiceProtocol {
// Implementation
}
// ❌ INCORRECT - Concrete dependency
final class ViewModel {
private let service: FeatureService // Hard to test
}
```
## AppSettings and Configuration
### Use AppSettings Protocol
ALWAYS access settings through the AppSettings protocol:
```swift
// ✅ CORRECT - Protocol-based settings access
final class SettingsViewModel: ObservableObject {
private let appSettings: AppSettings
init(appSettings: AppSettings) {
self.appSettings = appSettings
}
var isFeatureEnabled: Bool {
get { appSettings.featureEnabled }
set { appSettings.featureEnabled = newValue }
}
}
// ❌ INCORRECT - Direct UserDefaults access
final class SettingsViewModel: ObservableObject {
var isFeatureEnabled: Bool {
get { UserDefaults.standard.bool(forKey: "feature_enabled") }
set { UserDefaults.standard.set(newValue, forKey: "feature_enabled") }
}
}
```
### UserDefaults Property Wrapper
Use the established @UserDefaultsWrapper pattern for new settings:
```swift
// ✅ CORRECT - Property wrapper usage
extension AppUserDefaults {
@UserDefaultsWrapper(key: .newFeatureEnabled, defaultValue: false)
var newFeatureEnabled: Bool
}
// ❌ INCORRECT - Manual UserDefaults handling
extension AppUserDefaults {
var newFeatureEnabled: Bool {
get { userDefaults.bool(forKey: "new_feature_enabled") }
set { userDefaults.set(newValue, forKey: "new_feature_enabled") }
}
}
```
## Navigation and Coordinators
### Use MainCoordinator for App-Level Navigation
ALWAYS use MainCoordinator for deep links, URL handling, and app-level navigation:
```swift
// ✅ CORRECT - MainCoordinator usage
@MainActor
final class FeatureCoordinator {
private weak var mainCoordinator: MainCoordinator?
func handleFeatureAction() {
mainCoordinator?.handleURL(featureURL)
}
}
// ❌ INCORRECT - Direct navigation from ViewModels
final class FeatureViewModel: ObservableObject {
func handleAction() {
// Don't navigate directly from ViewModels
navigationController?.pushViewController(detailVC, animated: true)
}
}
```
### URL Handling Pattern
Implement URLHandling protocol for custom URL schemes:
```swift
// ✅ CORRECT - URLHandling protocol implementation
extension FeatureCoordinator: URLHandling {
func handleURL(_ url: URL) {
guard url.scheme == "duckduckgo",
url.host == "feature" else { return }
presentFeature(with: url.queryParameters)
}
func shouldProcessDeepLink(_ url: URL) -> Bool {
return url.scheme == "duckduckgo" && url.host == "feature"
}
}
```
## SwiftUI and Design System Integration
### Use DesignResourcesKit Colors
ALWAYS use semantic colors from DesignResourcesKit:
```swift
// ✅ CORRECT - Semantic color usage
struct FeatureView: View {
var body: some View {
VStack {
Text("Title")
.foregroundColor(Color(designSystemColor: .textPrimary))
Rectangle()
.fill(Color(designSystemColor: .surface))
}
.background(Color(designSystemColor: .background))
}
}
// ❌ INCORRECT - Hardcoded colors
struct FeatureView: View {
var body: some View {
VStack {
Text("Title")
.foregroundColor(.black) // Don't hardcode colors
Rectangle()
.fill(.gray) // Use semantic colors instead
}
}
}
```
### Use DesignResourcesKit Icons
ALWAYS use icons from DesignResourcesKitIcons:
```swift
// ✅ CORRECT - Design system icons
struct IconButton: View {
var body: some View {
Button(action: action) {
Image(uiImage: DesignSystemImages.Glyphs.Size16.add)
.foregroundColor(Color(designSystemColor: .accent))
}
}
}
// ❌ INCORRECT - System icons or custom images
struct IconButton: View {
var body: some View {
Button(action: action) {
Image(systemName: "plus") // Use design system icons
}
}
}
```
### Theme Integration
Use Theme protocol for complex color requirements:
```swift
// ✅ CORRECT - Theme integration
struct ThemedView: View {
@EnvironmentObject var themeManager: ThemeManager
var body: some View {
Rectangle()
.fill(Color(themeManager.currentTheme.backgroundColor))
}
}
```
## MVVM Pattern Implementation
### ObservableObject ViewModels
ALWAYS use ObservableObject for SwiftUI ViewModels:
```swift
// ✅ CORRECT - ObservableObject ViewModel
@MainActor
final class FeatureViewModel: ObservableObject {
@Published private(set) var items: [Item] = []
@Published private(set) var isLoading = false
@Published private(set) var error: Error?
private let service: FeatureServiceProtocol
private var cancellables = Set<AnyCancellable>()
init(service: FeatureServiceProtocol) {
self.service = service
setupBindings()
}
func loadData() async {
isLoading = true
defer { isLoading = false }
do {
items = try await service.fetchItems()
error = nil
} catch {
self.error = error
}
}
private func setupBindings() {
// Setup Combine bindings
}
}
```
### Published Property Guidelines
- Use `@Published private(set)` for read-only state
- Use `@Published` for two-way bindings
- Always mark ViewModels with `@MainActor`
## Async/Await Patterns
### MainActor Usage
ALWAYS use @MainActor for UI-related async operations:
```swift
// ✅ CORRECT - MainActor for UI updates
@MainActor
final class CoordinatorClass {
func presentModal() async {
let viewModel = try await createViewModel()
let hostingController = UIHostingController(rootView: FeatureView(viewModel: viewModel))
navigationController.present(hostingController, animated: true)
}
}
// ❌ INCORRECT - UI updates without MainActor
final class CoordinatorClass {
func presentModal() async {
let viewModel = try await createViewModel()
// This will crash - UI updates must be on main thread
navigationController.present(hostingController, animated: true)
}
}
```
### Task Management
Use Task for async operations in ViewModels:
```swift
// ✅ CORRECT - Task usage in ViewModels
final class ViewModel: ObservableObject {
func performAction() {
Task {
await loadData()
}
}
}
```
## Singleton Pattern Guidelines
### Avoid Singletons - Use Dependency Injection
```swift
// ❌ INCORRECT - Singleton pattern
final class FeatureManager {
static let shared = FeatureManager()
private init() {}
}
// ✅ CORRECT - Dependency injection
protocol FeatureManagerProtocol {
func performAction()
}
final class FeatureManager: FeatureManagerProtocol {
func performAction() {
// Implementation
}
}
// Register in AppDependencyProvider
extension AppDependencyProvider {
var featureManager: FeatureManagerProtocol {
return FeatureManager()
}
}
```
### Acceptable Singleton Usage
Only use singletons for truly global state that must be shared across the entire app:
```swift
// ✅ ACCEPTABLE - Global theme management
final class ThemeManager {
static let shared = ThemeManager()
private init() {}
}
// ✅ ACCEPTABLE - Content blocking state
final class ContentBlocking {
static let shared = ContentBlocking()
private init() {}
}
```
## Error Handling
### Use Result Types for Service Layer
```swift
// ✅ CORRECT - Result types in services
protocol NetworkServiceProtocol {
func fetchData() async -> Result<Data, NetworkError>
}
// ✅ CORRECT - Async throws in ViewModels
final class ViewModel: ObservableObject {
func loadData() async {
do {
let result = try await service.fetchData()
// Handle success
} catch {
// Handle error
}
}
}
```
### Custom Error Types
Define domain-specific error types:
```swift
// ✅ CORRECT - Domain-specific errors
enum FeatureError: LocalizedError {
case networkUnavailable
case invalidData
case unauthorized
var errorDescription: String? {
switch self {
case .networkUnavailable:
return "Network connection unavailable"
case .invalidData:
return "Invalid data received"
case .unauthorized:
return "User not authorized"
}
}
}
```
## Performance Optimization
### Lazy Loading for Expensive Operations
```swift
// ✅ CORRECT - Lazy property initialization
final class DataManager {
private lazy var expensiveResource: ExpensiveResource = {
return ExpensiveResource()
}()
}
```
### Background Processing
```swift
// ✅ CORRECT - Background processing with main thread UI updates
final class ViewModel: ObservableObject {
@Published var result: ProcessedData?
func processData() {
Task.detached(priority: .userInitiated) {
let processed = await heavyProcessing()
await MainActor.run {
self.result = processed
}
}
}
}
```
## Testing Requirements
### Testable Architecture
ALWAYS write testable code using dependency injection:
```swift
// ✅ CORRECT - Testable implementation
final class FeatureViewModel: ObservableObject {
private let service: FeatureServiceProtocol
init(service: FeatureServiceProtocol) {
self.service = service
}
}
// Test setup
final class FeatureViewModelTests: XCTestCase {
func testLoadData() async {
let mockService = MockFeatureService()
let viewModel = FeatureViewModel(service: mockService)
await viewModel.loadData()
XCTAssertTrue(mockService.fetchDataCalled)
}
}
```
@@ -0,0 +1,923 @@
---
source: ~/DuckDuckGo/apple-browsers.git/main/.cursor/rules/ios-tracker-blocking-implementation.mdc
confidence: 0.9
namespace: work
last_synced: 2026-04-28
alwaysApply: false
---
# iOS Tracker Blocking Implementation Guide
## Overview
This document provides a comprehensive overview of how content blocking has been implemented on iOS to protect user privacy and block tracking attempts. Our implementation uses a dual-approach strategy that combines the efficiency of WebKit's native content blocking with the flexibility of JavaScript-based protection.
**Implementation Strategy:**
- **Primary Layer**: Content Blocker Rules (WebKit native, compiled to bytecode)
- **Secondary Layer**: JavaScript injection with Tracker Radar data (gap coverage + surrogates)
## Content Blocker Rules
### Architecture Overview
Content Blocker Rules form the primary layer of our tracker blocking implementation. These rules are converted from our Tracker Radar dataset into Apple's Content Blocker Rules format and compiled by WebKit into efficient bytecode for optimal performance.
**Key Characteristics:**
- ✅ **High Performance**: Rules are compiled to bytecode by WebKit
- ✅ **Low Memory Footprint**: Optimized for mobile devices
- ✅ **Battery Efficient**: Minimal CPU overhead
- ❌ **Limited Flexibility**: Cannot support complex logic or surrogates
- ❌ **No Runtime Modification**: Rules are static once compiled
### Implementation Details
#### ContentBlockerRulesManager
The `ContentBlockerRulesManager` is responsible for converting Tracker Radar data into Apple's Content Blocker format and managing the rule compilation process.
```swift
import WebKit
import BrowserServicesKit
final class ContentBlockerRulesManager {
private let trackerDataManager: TrackerDataManager
private let compilationQueue = DispatchQueue(label: "content-blocker-compilation", qos: .utility)
init(trackerDataManager: TrackerDataManager) {
self.trackerDataManager = trackerDataManager
}
/// Converts Tracker Radar data to Content Blocker Rules format
func generateContentBlockerRules() async throws -> [WKContentRuleList] {
return try await withCheckedThrowingContinuation { continuation in
compilationQueue.async {
do {
let trackerData = self.trackerDataManager.embeddedTrackerData
let rules = self.convertToContentBlockerFormat(trackerData)
let ruleList = try self.compileRules(rules)
continuation.resume(returning: [ruleList])
} catch {
continuation.resume(throwing: error)
}
}
}
}
/// Converts TrackerData to Apple's Content Blocker Rules JSON format
private func convertToContentBlockerFormat(_ trackerData: TrackerData) -> [[String: Any]] {
var rules: [[String: Any]] = []
for (domain, tracker) in trackerData.trackers {
for rule in tracker.rules ?? [] {
let contentBlockerRule: [String: Any] = [
"trigger": [
"url-filter": rule.rule,
"resource-type": rule.resourceTypes,
"if-domain": rule.whitelist?.compactMap { "*\($0)" }
].compactMapValues { $0 },
"action": [
"type": "block"
]
]
rules.append(contentBlockerRule)
}
}
return rules
}
/// Compiles rules using WebKit's WKContentRuleListStore
private func compileRules(_ rules: [[String: Any]]) throws -> WKContentRuleList {
let jsonData = try JSONSerialization.data(withJSONObject: rules)
let jsonString = String(data: jsonData, encoding: .utf8)!
return try await withCheckedThrowingContinuation { continuation in
WKContentRuleListStore.default().compileContentRuleList(
forIdentifier: "DuckDuckGoContentBlocker",
encodedContentRuleList: jsonString
) { ruleList, error in
if let error = error {
continuation.resume(throwing: error)
} else if let ruleList = ruleList {
continuation.resume(returning: ruleList)
} else {
continuation.resume(throwing: ContentBlockerError.compilationFailed)
}
}
}
}
}
enum ContentBlockerError: Error {
case compilationFailed
case invalidRuleFormat
case trackerDataUnavailable
}
```
#### Integration with WKWebView
Content Blocker Rules are applied to WKWebView through the `WKUserContentController`:
```swift
extension BrowserWebView {
func applyContentBlockerRules() async {
do {
let ruleLists = try await contentBlockerManager.generateContentBlockerRules()
await MainActor.run {
for ruleList in ruleLists {
webView.configuration.userContentController.add(ruleList)
}
Logger.privacy.info("Applied \(ruleLists.count) content blocker rule lists")
}
} catch {
Logger.privacy.error("Failed to apply content blocker rules: \(error)")
// Fallback to JavaScript-only blocking
await applyJavaScriptBlockingOnly()
}
}
}
```
### ContentBlockerRulesUserScript
To understand which resources were blocked by Content Blocker Rules, we inject JavaScript that monitors network requests and infers blocking behavior:
```swift
final class ContentBlockerRulesUserScript {
static let source = """
(function() {
'use strict';
const blockedResources = new Set();
const allowedResources = new Set();
// Monitor XMLHttpRequest
const originalXHROpen = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function(method, url, async, user, password) {
const startTime = Date.now();
this.addEventListener('loadend', function() {
const duration = Date.now() - startTime;
if (this.status === 0 && duration < 10) {
// Likely blocked by content blocker
blockedResources.add(url);
reportBlockedResource(url, 'xhr');
} else {
allowedResources.add(url);
}
});
return originalXHROpen.call(this, method, url, async, user, password);
};
// Monitor Fetch API
const originalFetch = window.fetch;
window.fetch = function(input, init) {
const url = typeof input === 'string' ? input : input.url;
const startTime = Date.now();
return originalFetch.call(this, input, init)
.then(response => {
allowedResources.add(url);
return response;
})
.catch(error => {
const duration = Date.now() - startTime;
if (duration < 10) {
blockedResources.add(url);
reportBlockedResource(url, 'fetch');
}
throw error;
});
};
// Monitor image loading
const originalImageSrc = Object.getOwnPropertyDescriptor(Image.prototype, 'src');
Object.defineProperty(Image.prototype, 'src', {
set: function(value) {
const img = this;
img.addEventListener('error', function() {
if (img.naturalWidth === 0 && img.naturalHeight === 0) {
blockedResources.add(value);
reportBlockedResource(value, 'image');
}
});
img.addEventListener('load', function() {
allowedResources.add(value);
});
return originalImageSrc.set.call(this, value);
},
get: originalImageSrc.get
});
function reportBlockedResource(url, type) {
if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.contentBlocker) {
window.webkit.messageHandlers.contentBlocker.postMessage({
type: 'blocked',
url: url,
resourceType: type,
timestamp: Date.now()
});
}
}
// Report statistics periodically
setInterval(function() {
if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.contentBlocker) {
window.webkit.messageHandlers.contentBlocker.postMessage({
type: 'statistics',
blocked: blockedResources.size,
allowed: allowedResources.size,
timestamp: Date.now()
});
}
}, 5000);
})();
"""
}
```
## JavaScript Injection with Tracker Radar
### Purpose and Advantages
While Content Blocker Rules provide excellent performance, they have limitations that JavaScript injection can address:
**JavaScript Injection Advantages:**
- ✅ **Surrogate Support**: Can inject replacement scripts to prevent page breakage
- ✅ **Runtime Logic**: Complex blocking decisions based on page context
- ✅ **Gap Coverage**: Handles edge cases missed by static rules
- ✅ **Real-time Updates**: Can adapt to new tracking techniques
- ❌ **Performance Overhead**: Higher CPU and memory usage
- ❌ **Battery Impact**: More intensive than native blocking
### ContentBlockerUserScript Implementation
The `ContentBlockerUserScript` implements the full Tracker Radar blocking algorithm in JavaScript:
```swift
final class ContentBlockerUserScript {
private let trackerDataManager: TrackerDataManager
private let surrogateManager: SurrogateManager
init(trackerDataManager: TrackerDataManager, surrogateManager: SurrogateManager) {
self.trackerDataManager = trackerDataManager
self.surrogateManager = surrogateManager
}
var source: String {
return generateTrackerBlockingScript()
}
private func generateTrackerBlockingScript() -> String {
let trackerData = trackerDataManager.embeddedTrackerData
let surrogates = surrogateManager.allSurrogates
return """
(function() {
'use strict';
// Embedded tracker data
const TRACKER_DATA = \(trackerData.jsonString);
const SURROGATES = \(surrogates.jsonString);
class TrackerBlocker {
constructor() {
this.blockedCount = 0;
this.allowedCount = 0;
this.setupInterception();
}
setupInterception() {
this.interceptXMLHttpRequest();
this.interceptFetch();
this.interceptScriptLoading();
this.interceptImageLoading();
this.setupMutationObserver();
}
shouldBlockResource(url, type, initiator) {
try {
const urlObj = new URL(url, window.location.href);
const domain = urlObj.hostname;
// Check if domain is in tracker list
const tracker = TRACKER_DATA.trackers[domain];
if (!tracker) return false;
// Apply tracker rules
if (tracker.rules) {
for (const rule of tracker.rules) {
if (this.matchesRule(url, rule, type)) {
// Check for whitelist exceptions
if (rule.whitelist && this.matchesWhitelist(window.location.hostname, rule.whitelist)) {
return false;
}
return true;
}
}
}
return false;
} catch (error) {
console.warn('Error checking tracker status:', error);
return false;
}
}
matchesRule(url, rule, resourceType) {
// Simple regex matching for rule.rule
try {
const regex = new RegExp(rule.rule, 'i');
if (!regex.test(url)) return false;
// Check resource type restrictions
if (rule.resourceTypes && rule.resourceTypes.length > 0) {
return rule.resourceTypes.includes(resourceType);
}
return true;
} catch (error) {
return false;
}
}
matchesWhitelist(hostname, whitelist) {
return whitelist.some(domain => {
if (domain.startsWith('*.')) {
const suffix = domain.substring(2);
return hostname === suffix || hostname.endsWith('.' + suffix);
}
return hostname === domain;
});
}
getSurrogate(url) {
for (const surrogate of SURROGATES) {
if (surrogate.matches.some(pattern => {
try {
const regex = new RegExp(pattern, 'i');
return regex.test(url);
} catch {
return false;
}
})) {
return surrogate.replacement;
}
}
return null;
}
interceptXMLHttpRequest() {
const originalOpen = XMLHttpRequest.prototype.open;
const self = this;
XMLHttpRequest.prototype.open = function(method, url, async, user, password) {
if (self.shouldBlockResource(url, 'xmlhttprequest', 'script')) {
self.blockedCount++;
self.reportBlocked(url, 'xhr');
// Simulate blocked request
setTimeout(() => {
const event = new Event('error');
this.dispatchEvent(event);
}, 0);
return;
}
self.allowedCount++;
return originalOpen.call(this, method, url, async, user, password);
};
}
interceptFetch() {
const originalFetch = window.fetch;
const self = this;
window.fetch = function(input, init) {
const url = typeof input === 'string' ? input : input.url;
if (self.shouldBlockResource(url, 'xmlhttprequest', 'script')) {
self.blockedCount++;
self.reportBlocked(url, 'fetch');
return Promise.reject(new TypeError('Failed to fetch'));
}
self.allowedCount++;
return originalFetch.call(this, input, init);
};
}
interceptScriptLoading() {
const self = this;
const originalCreateElement = document.createElement;
document.createElement = function(tagName) {
const element = originalCreateElement.call(this, tagName);
if (tagName.toLowerCase() === 'script') {
const originalSrcSetter = Object.getOwnPropertyDescriptor(HTMLScriptElement.prototype, 'src').set;
Object.defineProperty(element, 'src', {
set: function(value) {
if (self.shouldBlockResource(value, 'script', 'document')) {
self.blockedCount++;
self.reportBlocked(value, 'script');
// Check for surrogate
const surrogate = self.getSurrogate(value);
if (surrogate) {
self.injectSurrogate(surrogate);
}
return;
}
self.allowedCount++;
return originalSrcSetter.call(this, value);
},
get: function() {
return this.getAttribute('src');
}
});
}
return element;
};
}
interceptImageLoading() {
const self = this;
const originalImageSrc = Object.getOwnPropertyDescriptor(Image.prototype, 'src');
Object.defineProperty(Image.prototype, 'src', {
set: function(value) {
if (self.shouldBlockResource(value, 'image', 'document')) {
self.blockedCount++;
self.reportBlocked(value, 'image');
return;
}
self.allowedCount++;
return originalImageSrc.set.call(this, value);
},
get: originalImageSrc.get
});
}
setupMutationObserver() {
const self = this;
const observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
mutation.addedNodes.forEach(function(node) {
if (node.nodeType === Node.ELEMENT_NODE) {
self.processNewElement(node);
}
});
});
});
observer.observe(document.body || document.documentElement, {
childList: true,
subtree: true
});
}
processNewElement(element) {
// Check scripts
if (element.tagName === 'SCRIPT' && element.src) {
if (this.shouldBlockResource(element.src, 'script', 'document')) {
this.blockedCount++;
this.reportBlocked(element.src, 'script');
element.remove();
// Inject surrogate if available
const surrogate = this.getSurrogate(element.src);
if (surrogate) {
this.injectSurrogate(surrogate);
}
}
}
// Check images (tracking pixels)
if (element.tagName === 'IMG' && element.src) {
if (this.shouldBlockResource(element.src, 'image', 'document')) {
this.blockedCount++;
this.reportBlocked(element.src, 'image');
element.remove();
}
}
// Check iframes
if (element.tagName === 'IFRAME' && element.src) {
if (this.shouldBlockResource(element.src, 'subdocument', 'document')) {
this.blockedCount++;
this.reportBlocked(element.src, 'iframe');
element.remove();
}
}
}
injectSurrogate(surrogateCode) {
try {
const script = document.createElement('script');
script.textContent = surrogateCode;
script.setAttribute('data-surrogate', 'true');
(document.head || document.documentElement).appendChild(script);
} catch (error) {
console.warn('Failed to inject surrogate:', error);
}
}
reportBlocked(url, type) {
if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.trackerBlocked) {
window.webkit.messageHandlers.trackerBlocked.postMessage({
url: url,
type: type,
timestamp: Date.now()
});
}
}
getStatistics() {
return {
blocked: this.blockedCount,
allowed: this.allowedCount
};
}
}
// Initialize tracker blocker
window.duckduckgoTracker = new TrackerBlocker();
// Report statistics periodically
setInterval(function() {
const stats = window.duckduckgoTracker.getStatistics();
if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.trackerStats) {
window.webkit.messageHandlers.trackerStats.postMessage(stats);
}
}, 10000);
})();
"""
}
}
```
### Surrogate Support
One of the key advantages of JavaScript injection is the ability to provide surrogate scripts that replace blocked trackers to prevent page breakage:
```swift
final class SurrogateManager {
private let surrogates: [Surrogate]
struct Surrogate: Codable {
let name: String
let matches: [String] // Regex patterns
let replacement: String // JavaScript code
}
var allSurrogates: [Surrogate] {
return surrogates
}
init() {
// Load surrogates from embedded data
self.surrogates = loadEmbeddedSurrogates()
}
private func loadEmbeddedSurrogates() -> [Surrogate] {
// Common surrogates for popular tracking libraries
return [
Surrogate(
name: "Google Tag Manager",
matches: ["googletagmanager\\.com/gtm\\.js"],
replacement: """
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'GA_MEASUREMENT_ID', { 'send_page_view': false });
"""
),
Surrogate(
name: "Google Analytics",
matches: ["google-analytics\\.com/analytics\\.js", "googletagmanager\\.com/gtag/js"],
replacement: """
window.ga = window.ga || function() {
(ga.q = ga.q || []).push(arguments);
};
ga.l = +new Date;
ga('create', 'UA-XXXXXXXX-X', 'auto');
ga('send', 'pageview');
"""
),
Surrogate(
name: "Facebook Pixel",
matches: ["connect\\.facebook\\.net/.*?/fbevents\\.js"],
replacement: """
window.fbq = function() {};
window.fbq.push = function() {};
window.fbq.loaded = true;
window.fbq.version = '2.0';
window.fbq.queue = [];
"""
)
]
}
}
```
## Hybrid Integration Strategy
### Layered Protection Approach
The most effective tracker blocking combines both techniques:
1. **Content Blocker Rules** handle the majority of tracking requests efficiently
2. **JavaScript Injection** provides gap coverage and surrogate support
3. **Message Handlers** coordinate between both layers
```swift
extension BrowserViewController: WKScriptMessageHandler {
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
switch message.name {
case "trackerBlocked":
handleTrackerBlocked(message.body)
case "trackerStats":
handleTrackerStatistics(message.body)
case "contentBlocker":
handleContentBlockerMessage(message.body)
default:
break
}
}
private func handleTrackerBlocked(_ messageBody: Any) {
guard let data = messageBody as? [String: Any],
let url = data["url"] as? String,
let type = data["type"] as? String else { return }
// Update privacy dashboard
privacyDashboard.recordBlockedTracker(url: url, type: type)
// Update UI indicators
updateTrackerCountIndicator()
Logger.privacy.debug("Blocked tracker: \(url) (\(type))")
}
private func handleTrackerStatistics(_ messageBody: Any) {
guard let stats = messageBody as? [String: Any],
let blocked = stats["blocked"] as? Int,
let allowed = stats["allowed"] as? Int else { return }
privacyDashboard.updateStatistics(blocked: blocked, allowed: allowed)
}
}
```
### Configuration and Initialization
The complete setup combines both blocking techniques:
```swift
extension BrowserWebView {
func setupTrackerBlocking() async {
let configuration = webView.configuration
let userContentController = configuration.userContentController
// 1. Apply Content Blocker Rules
await applyContentBlockerRules()
// 2. Add JavaScript-based blocking
let contentBlockerScript = WKUserScript(
source: ContentBlockerUserScript(
trackerDataManager: trackerDataManager,
surrogateManager: surrogateManager
).source,
injectionTime: .atDocumentStart,
forMainFrameOnly: false
)
userContentController.addUserScript(contentBlockerScript)
// 3. Add inference script for Content Blocker Rules
let inferenceScript = WKUserScript(
source: ContentBlockerRulesUserScript.source,
injectionTime: .atDocumentStart,
forMainFrameOnly: false
)
userContentController.addUserScript(inferenceScript)
// 4. Register message handlers
userContentController.add(self, name: "trackerBlocked")
userContentController.add(self, name: "trackerStats")
userContentController.add(self, name: "contentBlocker")
Logger.privacy.info("Tracker blocking initialized with hybrid approach")
}
}
```
## Why Not Content Blocking Extension?
**Content Blocking Extensions** would apply to every WKWebView in iOS system-wide, but we chose our approach for specific reasons:
### UX Control
- **Manual Activation Required**: Content Blocking Extensions require users to manually enable them in Settings
- **Limited User Guidance**: Difficult to provide contextual help for activation
- **Our Approach**: Complete control over blocking state and user experience
### Surrogate Support
- **Extensions Cannot Inject**: Content Blocking Extensions can only block, not replace with surrogates
- **Page Breakage Risk**: Many sites depend on tracking scripts for functionality
- **Our Approach**: Intelligent replacement prevents site breakage
### Dynamic Configuration
- **Static Rules Only**: Extensions cannot modify behavior based on user preferences
- **No A/B Testing**: Cannot experiment with different blocking strategies
- **Our Approach**: Runtime configuration and experimentation capability
## Performance Considerations
### Memory Usage
```swift
class TrackerBlockingPerformanceMonitor {
private var lastMemoryWarning: Date?
func optimizeForMemoryPressure() {
// Reduce JavaScript blocker complexity under memory pressure
if let lastWarning = lastMemoryWarning,
Date().timeIntervalSince(lastWarning) < 60 {
// Rely more heavily on Content Blocker Rules
disableComplexJavaScriptBlocking()
}
}
func handleMemoryWarning() {
lastMemoryWarning = Date()
// Clear caches
trackerDataManager.clearCache()
surrogateManager.clearCache()
// Temporarily disable non-essential blocking
temporarilyReduceBlockingComplexity()
}
}
```
### Battery Impact Monitoring
```swift
extension BrowserViewController {
func monitorBlockingPerformance() {
// Monitor JavaScript execution time
let startTime = CFAbsoluteTimeGetCurrent()
webView.evaluateJavaScript("window.duckduckgoTracker.getStatistics()") { result, error in
let executionTime = CFAbsoluteTimeGetCurrent() - startTime
if executionTime > 0.1 { // 100ms threshold
Logger.privacy.warning("Tracker blocking JS execution time: \(executionTime)s")
// Consider optimizing or reducing complexity
}
}
}
}
```
## Testing and Debugging
### Unit Testing Content Blocker Rules
```swift
class ContentBlockerRulesTests: XCTestCase {
func testTrackerDataConversion() async throws {
let mockTrackerData = TrackerData.mock
let rulesManager = ContentBlockerRulesManager(trackerDataManager: MockTrackerDataManager(data: mockTrackerData))
let rules = try await rulesManager.generateContentBlockerRules()
XCTAssertFalse(rules.isEmpty)
XCTAssertEqual(rules.count, 1)
}
func testRuleCompilation() async throws {
let simpleRule = [
[
"trigger": [
"url-filter": ".*tracker\\.com.*"
],
"action": [
"type": "block"
]
]
]
let jsonData = try JSONSerialization.data(withJSONObject: simpleRule)
let jsonString = String(data: jsonData, encoding: .utf8)!
let ruleList = try await withCheckedThrowingContinuation { continuation in
WKContentRuleListStore.default().compileContentRuleList(
forIdentifier: "test",
encodedContentRuleList: jsonString
) { ruleList, error in
if let error = error {
continuation.resume(throwing: error)
} else if let ruleList = ruleList {
continuation.resume(returning: ruleList)
} else {
continuation.resume(throwing: ContentBlockerError.compilationFailed)
}
}
}
XCTAssertNotNil(ruleList)
}
}
```
### UI Testing with Tracker Blocking
```swift
class TrackerBlockingUITests: XCTestCase {
func testTrackerCounterUpdates() {
let app = XCUIApplication()
app.launch()
// Navigate to a page with known trackers
app.textFields["urlField"].tap()
app.textFields["urlField"].typeText("https://example.com/page-with-trackers")
app.buttons["Go"].tap()
// Wait for page load and blocking to take effect
sleep(3)
// Check that tracker counter updated
let trackerCounter = app.staticTexts["trackerCount"]
XCTAssertTrue(trackerCounter.exists)
let counterText = trackerCounter.label
XCTAssertTrue(counterText.contains("blocked"))
}
}
```
## Maintenance and Updates
### Tracker Radar Updates
```swift
class TrackerDataUpdateManager {
func checkForUpdates() async {
let latestVersion = try await fetchLatestTrackerDataVersion()
let currentVersion = trackerDataManager.currentVersion
if latestVersion > currentVersion {
await downloadAndApplyUpdate(version: latestVersion)
}
}
private func downloadAndApplyUpdate(version: String) async {
do {
let newTrackerData = try await downloadTrackerData(version: version)
// Validate data integrity
guard validateTrackerData(newTrackerData) else {
throw TrackerDataError.invalidData
}
// Apply update
trackerDataManager.updateData(newTrackerData)
// Regenerate Content Blocker Rules
await regenerateContentBlockerRules()
Logger.privacy.info("Updated tracker data to version \(version)")
} catch {
Logger.privacy.error("Failed to update tracker data: \(error)")
}
}
}
```
## Conclusion
The iOS tracker blocking implementation provides comprehensive protection through a sophisticated dual-layer approach:
- **Content Blocker Rules** deliver high-performance blocking for the majority of tracking attempts
- **JavaScript injection** ensures complete coverage and enables advanced features like surrogates
- **Hybrid coordination** maximizes both performance and protection effectiveness
This architecture gives us complete control over the user experience while maintaining the privacy protection that DuckDuckGo users expect, without the limitations of system-wide Content Blocking Extensions.
**Key Benefits:**
- ✅ **Performance**: WebKit-optimized native blocking for most requests
- ✅ **Completeness**: JavaScript layer catches edge cases and provides surrogates
- ✅ **Control**: Full UX control without requiring manual user configuration
- ✅ **Flexibility**: Runtime configuration and A/B testing capabilities
- ✅ **Maintenance**: Easy updates and improvements to blocking logic
This implementation forms the foundation of DuckDuckGo's iOS privacy protection, ensuring users browse with confidence knowing their privacy is protected by industry-leading tracker blocking technology.
@@ -0,0 +1,581 @@
---
source: ~/DuckDuckGo/apple-browsers.git/main/.cursor/rules/logging-guidelines.mdc
confidence: 0.9
namespace: work
last_synced: 2026-04-28
alwaysApply: false
---
# Logging Guidelines & Telemetry Capture
## Overview
The DuckDuckGo browser apps for iOS and macOS leverage **Apple's Unified Logging System** for capturing telemetry and debugging information. This system enables efficient tracking of app behavior, issue diagnosis, and performance monitoring in a structured and privacy-conscious manner.
**Key Benefits**:
- **Privacy-first**: Built-in privacy controls for sensitive data
- **Performance**: Optimized for minimal overhead
- **Integration**: Native Apple ecosystem support
- **Debugging**: Rich contextual information and filtering
## How to Log
### Using the Logger Class
We utilize the `Logger` class from Apple's `os` framework for all logging activities:
```swift
import os
// Basic logging examples
Logger.yourFeatureName.debug("Something to log, with info: \(infoVar)")
Logger.anotherFeatureName.error("Some error happened: \(error.localizedDescription, privacy: .public)")
Logger.networking.info("API request completed for endpoint: \(endpoint, privacy: .public)")
Logger.performance.debug("Operation took \(duration)ms to complete")
```
### Creating Custom Loggers
#### Single Feature Logger
For new features, create a dedicated logger file named `Logger+YourFeatureName.swift`:
```swift
import os
public extension Logger {
static var yourFeatureName: Logger = {
Logger(subsystem: "Your Feature Name", category: "")
}()
static var anotherFeatureName: Logger = {
Logger(subsystem: "Another feature name", category: "Subsystem in the feature")
}()
}
```
#### Multiple Feature Loggers
For related features, add to existing logger extensions (e.g., `Logger+Multiple.swift`):
```swift
import os
public extension Logger {
// Networking loggers
static var networking: Logger = {
Logger(subsystem: "Networking", category: "API")
}()
static var cache: Logger = {
Logger(subsystem: "Networking", category: "Cache")
}()
// UI loggers
static var tabManagement: Logger = {
Logger(subsystem: "UI", category: "Tab Management")
}()
static var bookmarks: Logger = {
Logger(subsystem: "UI", category: "Bookmarks")
}()
}
```
### Logger Placement Strategy
**Framework/Package Level**: For shared functionality across iOS and macOS
```swift
// In BrowserServicesKit
public extension Logger {
static var secureVault: Logger = {
Logger(subsystem: "BrowserServicesKit", category: "SecureVault")
}()
static var sync: Logger = {
Logger(subsystem: "BrowserServicesKit", category: "Sync")
}()
}
```
**App Level**: For platform-specific features
```swift
// In iOS app
public extension Logger {
static var widgets: Logger = {
Logger(subsystem: "iOS App", category: "Widgets")
}()
}
// In macOS app
public extension Logger {
static var windowManagement: Logger = {
Logger(subsystem: "macOS App", category: "Window Management")
}()
}
```
## Subsystem and Category Guidelines
### Subsystem Naming
**Purpose**: Corresponds to large functional areas of your app
**Examples**:
- `"Networking"` - All network-related functionality
- `"UI"` - User interface components
- `"Data Storage"` - Database and persistence
- `"Security"` - Authentication and encryption
- `"Performance"` - Performance monitoring and optimization
### Category Naming
**Purpose**: Specific components or features within subsystems
**Examples**:
```swift
// Networking subsystem categories
Logger(subsystem: "Networking", category: "API Calls")
Logger(subsystem: "Networking", category: "Cache Management")
Logger(subsystem: "Networking", category: "Request Retry")
// UI subsystem categories
Logger(subsystem: "UI", category: "Tab Management")
Logger(subsystem: "UI", category: "Settings")
Logger(subsystem: "UI", category: "Bookmarks")
// Data Storage subsystem categories
Logger(subsystem: "Data Storage", category: "SecureVault")
Logger(subsystem: "Data Storage", category: "Core Data")
Logger(subsystem: "Data Storage", category: "User Defaults")
```
## Log Levels and Privacy
### Choosing Log Levels
#### `debug` - Development and Troubleshooting
- **Purpose**: Verbose output for development debugging
- **Retention**: Short-lived in memory
- **Use cases**: Variable values, execution flow, temporary debugging
```swift
Logger.networking.debug("Request headers: \(headers)")
Logger.ui.debug("User tapped button at coordinates: \(point)")
Logger.performance.debug("Cache hit for key: \(key)")
```
#### `info` - Important Events
- **Purpose**: Interesting or important information
- **Retention**: Longer than debug, available for analysis
- **Use cases**: User actions, system state changes, feature usage
```swift
Logger.auth.info("User successfully authenticated")
Logger.sync.info("Sync operation completed with \(itemCount) items")
Logger.features.info("Feature flag \(flagName, privacy: .public) enabled")
```
#### `error` - Handled Errors
- **Purpose**: Something went wrong but was handled gracefully
- **Retention**: Available for longer-term analysis
- **Requirements**: Always include `error.localizedDescription`
```swift
Logger.networking.error("API request failed: \(error.localizedDescription, privacy: .public)")
Logger.database.error("Failed to save context: \(error.localizedDescription, privacy: .public)")
Logger.auth.error("Keychain access denied: \(error.localizedDescription, privacy: .public)")
```
#### `fault` - Critical Issues
- **Purpose**: Critical issues preventing normal app function
- **Retention**: Highest priority, always preserved
- **Requirements**: Include error description when available
```swift
Logger.database.fault("Database corruption detected: \(error.localizedDescription, privacy: .public)")
Logger.security.fault("Critical security violation: \(details, privacy: .public)")
Logger.system.fault("App unable to initialize required services")
```
### Privacy Settings
#### Default Privacy Behavior
**All interpolated values are `.private` by default** - only visible in debug builds:
```swift
// These values are private by default
Logger.auth.info("User \(username) logged in") // username is private
Logger.network.debug("Response time: \(responseTime)ms") // responseTime is private
```
#### Public Information
Mark non-sensitive information as `.public` for visibility in release builds:
```swift
// Error descriptions should typically be public
Logger.network.error("Connection failed: \(error.localizedDescription, privacy: .public)")
// System information can be public
Logger.performance.info("App launched in \(launchTime, privacy: .public)ms")
// Feature flags and settings (non-PII) can be public
Logger.features.info("Dark mode: \(isDarkMode, privacy: .public)")
```
#### Privacy Decision Matrix
| Data Type | Privacy Level | Example |
|-----------|---------------|---------|
| **User PII** | `.private` (default) | Email, username, personal data |
| **Error descriptions** | `.public` | `error.localizedDescription` |
| **System metrics** | `.public` | Performance timings, counts |
| **Feature states** | `.public` | Feature flags, app settings |
| **Debug values** | `.private` (default) | Variable contents, internal state |
## Best Practices
### ✅ DO
#### Direct Logging
```swift
// ✅ CORRECT: Log directly where events occur
func authenticateUser() {
Logger.auth.info("Starting user authentication")
do {
let result = try performAuthentication()
Logger.auth.info("Authentication successful")
} catch {
Logger.auth.error("Authentication failed: \(error.localizedDescription, privacy: .public)")
}
}
```
#### Meaningful Context
```swift
// ✅ CORRECT: Include relevant context
Logger.sync.info("Sync completed: \(syncedItems, privacy: .public) items, \(conflicts, privacy: .public) conflicts")
Logger.network.debug("Cache hit for URL: \(url.absoluteString, privacy: .public)")
Logger.ui.debug("View controller \(type(of: self)) appeared")
```
#### Consistent Logger Usage
```swift
// ✅ CORRECT: Use established loggers consistently
extension BookmarkManager {
func addBookmark(_ bookmark: Bookmark) {
Logger.bookmarks.info("Adding bookmark: \(bookmark.title ?? "Untitled")")
// Implementation
}
func deleteBookmark(_ bookmark: Bookmark) {
Logger.bookmarks.info("Deleting bookmark: \(bookmark.title ?? "Untitled")")
// Implementation
}
}
```
### ❌ DON'T
#### Wrapper Functions
```swift
// ❌ AVOID: Wrapper functions obscure context
func logError(_ message: String) {
Logger.general.error("\(message)") // Loses class, line number context
}
// Use direct logging instead
Logger.networking.error("Connection timeout: \(error.localizedDescription, privacy: .public)")
```
#### Logger Injection
```swift
// ❌ AVOID: Injecting loggers
class NetworkManager {
private let logger: Logger
init(logger: Logger) { // Unnecessary complexity
self.logger = logger
}
}
// ✅ CORRECT: Use global logger extensions
class NetworkManager {
func performRequest() {
Logger.networking.info("Starting network request")
}
}
```
#### Overly Verbose Debug Logging
```swift
// ❌ AVOID: Too much debug noise
func processItems(_ items: [Item]) {
Logger.processing.debug("Starting to process items")
for item in items {
Logger.processing.debug("Processing item: \(item.id)")
Logger.processing.debug("Item name: \(item.name)")
Logger.processing.debug("Item processed successfully")
}
Logger.processing.debug("Finished processing all items")
}
// ✅ CORRECT: Focused, meaningful debug logs
func processItems(_ items: [Item]) {
Logger.processing.debug("Processing \(items.count) items")
// Process items...
Logger.processing.debug("Item processing completed")
}
```
## Reading and Filtering Logs
### 1. Xcode Console
**Best for**: App-specific debugging during development
#### Setup for Optimal Readability
1. **Add columns**: Type, Library, Subsystem, Category
2. **Filter by process**: Your app name
3. **Use contextual menu**: Show/Hide specific log types
#### Console Filtering
```
// Filter by subsystem
subsystem:com.yourapp.Networking
// Filter by category
category:API
// Hide system noise
subsystem:com.apple. (!contains)
// Show only errors and faults
type:error OR type:fault
```
### 2. Console.app
**Best for**: System-wide debugging and cross-app analysis
#### Recommended Filters for DuckDuckGo
```
// Focus on DuckDuckGo process
process:duckduckgo (contains)
// Hide system noise
subsystem:com.apple. (!contains)
subsystem:PrototypeTools (!contains)
library:Security (!contains)
library:TextInput (!contains)
// Show specific subsystems
subsystem:Networking (contains)
subsystem:UI (contains)
```
#### Advanced Filtering Examples
```
// Errors in the last hour
type:error AND time:>-1h
// Specific feature debugging
subsystem:BrowserServicesKit AND category:SecureVault
// Performance monitoring
message:performance (contains) AND type:info
```
### 3. Command Line Tool
**Best for**: Scripting and automated analysis
#### Basic Usage
```bash
# Show logs for specific subsystem
log show --predicate 'subsystem == "com.duckduckgo.Networking"' --info
# Show recent errors
log show --predicate 'messageType == "Error"' --last 1h
# Export logs to file
log show --predicate 'process == "DuckDuckGo"' --start '2024-01-01 00:00:00' > app_logs.txt
# Real-time streaming
log stream --predicate 'subsystem == "com.duckduckgo.UI"'
```
#### Advanced Command Examples
```bash
# Debugging specific feature
log show --predicate 'subsystem == "BrowserServicesKit" AND category == "SecureVault"' --debug
# Performance analysis
log show --predicate 'message CONTAINS "performance"' --info --last 24h
# Error analysis with context
log show --predicate 'messageType >= "Error"' --info --start '2024-01-01'
# Multiple conditions
log show --predicate 'subsystem BEGINSWITH "com.duckduckgo" AND messageType == "Error"' --last 2h
```
### 4. Sysdiagnose
**Best for**: Remote debugging and Apple DTS submissions
#### What's Included
- Complete system snapshot
- All system and app logs
- Memory usage data
- Kernel information
- Crash reports
- Network status
- Performance data
#### Usage
```bash
# Generate sysdiagnose
sudo sysdiagnose
# The generated file can be analyzed with Console.app
# Located in /var/tmp/ or Desktop
```
## Log Export for Internal Users
### macOS Debug Menu Export
**Available to**: Internal users only
**Platform**: macOS only
#### How to Export
1. Open **Debug menu**
2. Navigate to **Logging** > **Export logs**
3. Logs are exported as a ZIP file to Desktop
4. Includes filtered logs based on app subsystems
#### Export Contents
The exported ZIP contains:
- App-specific logs filtered by subsystem
- Recent system logs relevant to the app
- Crash reports if available
- Basic system information
## Logging Patterns by Feature
### Authentication & Security
```swift
public extension Logger {
static var auth: Logger = { Logger(subsystem: "Security", category: "Authentication") }()
static var keychain: Logger = { Logger(subsystem: "Security", category: "Keychain") }()
static var encryption: Logger = { Logger(subsystem: "Security", category: "Encryption") }()
}
// Usage examples
Logger.auth.info("User authentication attempt")
Logger.keychain.error("Keychain access failed: \(error.localizedDescription, privacy: .public)")
Logger.encryption.debug("Encrypting data with algorithm: \(algorithm, privacy: .public)")
```
### Networking & API
```swift
public extension Logger {
static var networking: Logger = { Logger(subsystem: "Networking", category: "HTTP") }()
static var api: Logger = { Logger(subsystem: "Networking", category: "API") }()
static var cache: Logger = { Logger(subsystem: "Networking", category: "Cache") }()
}
// Usage examples
Logger.networking.info("HTTP request to \(endpoint, privacy: .public)")
Logger.api.error("API call failed: \(error.localizedDescription, privacy: .public)")
Logger.cache.debug("Cache hit for key: \(cacheKey)")
```
### Data & Storage
```swift
public extension Logger {
static var database: Logger = { Logger(subsystem: "Data Storage", category: "Core Data") }()
static var secureVault: Logger = { Logger(subsystem: "Data Storage", category: "SecureVault") }()
static var sync: Logger = { Logger(subsystem: "Data Storage", category: "Sync") }()
}
// Usage examples
Logger.database.info("Core Data migration completed")
Logger.secureVault.error("SecureVault operation failed: \(error.localizedDescription, privacy: .public)")
Logger.sync.info("Sync completed: \(itemCount, privacy: .public) items")
```
### Performance Monitoring
```swift
public extension Logger {
static var performance: Logger = { Logger(subsystem: "Performance", category: "Metrics") }()
static var memory: Logger = { Logger(subsystem: "Performance", category: "Memory") }()
static var startup: Logger = { Logger(subsystem: "Performance", category: "Startup") }()
}
// Usage examples
Logger.performance.info("Operation completed in \(duration, privacy: .public)ms")
Logger.memory.debug("Memory usage: \(memoryUsage, privacy: .public)MB")
Logger.startup.info("App launch completed in \(launchTime, privacy: .public)ms")
```
## Integration with App Lifecycle
### State Machine Logging
```swift
// In app lifecycle state machine
class Launching {
func init() {
Logger.lifecycle.info("App entering Launching state")
// Initialization logic
Logger.lifecycle.info("Launching state completed")
}
}
class Foreground {
func onTransition() {
Logger.lifecycle.info("App transitioning to Foreground")
}
func didReturn() {
Logger.lifecycle.info("App returned to Foreground state")
}
}
```
### Service Lifecycle Logging
```swift
class MyService {
func start() {
Logger.services.info("Starting \(type(of: self)) service")
// Service startup logic
}
func stop() {
Logger.services.info("Stopping \(type(of: self)) service")
// Service cleanup logic
}
}
```
---
Following these logging guidelines ensures consistent, privacy-conscious, and effective telemetry capture across the DuckDuckGo browser ecosystem, enabling better debugging, monitoring, and user experience optimization.
@@ -0,0 +1,220 @@
---
source: ~/DuckDuckGo/apple-browsers.git/main/.cursor/rules/macos-singletons-removal.mdc
confidence: 0.9
namespace: work
last_synced: 2026-04-28
alwaysApply: false
---
## macOS Singleton Removal Rules
### Purpose
Define a concrete pattern for removing `.shared` singletons in the macOS app by replacing them with app-owned instances and dependency injection. The `AIChatPreferences` and `AboutPreferences` refactors in `AppDelegate` are canonical examples.
### Rules
1. **Do not introduce new singletons**
- Never add new `static let shared` or similar global singletons.
- New dependencies must be passed in via initializers or factory methods, not fetched from global state.
2. **Move ownership to the composition root (AppDelegate)**
- Add a stored property on the macOS composition root (currently `AppDelegate`) for the dependency, for example:
- `let aiChatPreferences: AIChatPreferences`
- Construct the instance during app setup using real dependencies:
- Inject storage (e.g. `DefaultAIChatPreferencesStorage`)
- Inject configuration objects (e.g. `AIChatMenuVisibilityConfigurable`)
- Inject window managers via protocols (e.g. `WindowControllersManagerProtocol`)
- Inject feature flaggers and other services as needed
- Prefer protocol-typed properties in `AppDelegate` when the dependency has a clear protocol (to keep testing and substitution easy).
3. **Thread the dependency through initializers**
- For view controllers and models that need the former singleton, add initializer parameters and store them as non-optional properties. Example:
- `init(..., aiChatPreferences: AIChatPreferences = NSApp.delegateTyped.aiChatPreferences, ...)`
- Avoid using `NSApp.delegateTyped` or `Application.appDelegate` and prefer to pass the dependency down from a parent object.
- If a parent object doesn't contain the dependency, it should be updated to have it passed down from its own parent object, observing the exceptions mentioned below.
- It is explicitly allowed to use `NSApp.delegateTyped`:
- In the `Tab` initializer (following the existing pattern for other dependencies)
- In default parameter values for the `MainViewController` initializer (this is the entry point for the dependency chain)
- In default parameter values for the `TabCollectionViewModel` initializer (following the existing pattern for other dependencies)
- In default parameter values for the `TabViewModel` initializer (temporary exception, will be refactored later)
- **Exception**: For `@MainActor` initializers, use optional parameters with `nil` defaults and assign from `NSApp.delegateTyped` in the initializer body.
- **Important: Main actor isolation**: If an initializer is marked `@MainActor` and you need to default a parameter from `NSApp.delegateTyped`, use an optional parameter with `nil` default instead of accessing `NSApp.delegateTyped` in the default value. Then assign the value inside the initializer body:
- ❌ `init(savedZoomLevelsCoordinating: SavedZoomLevelsCoordinating = NSApp.delegateTyped.accessibilityPreferences)` (causes main actor isolation warning)
- ✅ `init(savedZoomLevelsCoordinating: SavedZoomLevelsCoordinating? = nil)` with `self.savedZoomLevelsCoordinating = savedZoomLevelsCoordinating ?? NSApp.delegateTyped.accessibilityPreferences` in the body
- When creating child objects from a parent that already has the dependency, pass the property down rather than re-reading from `NSApp.delegateTyped`.
- **For preferences models that need to reach SwiftUI views**, thread through the entire chain:
- `MainViewController` (with default parameter) → `BrowserTabViewController` → `PreferencesViewController` → `PreferencesSidebarModel` → `PreferencesRootView`
- Follow the existing pattern used by other preferences (e.g., `searchPreferences`, `tabsPreferences`, `aiChatPreferences`)
- When adding to `PreferencesSidebarModel`, add the property alongside existing preferences and update both the main `init` and convenience `init`
- **For dependencies that need to reach UserScripts initialization** (e.g., `DuckPlayerPreferences`), thread through the content blocking infrastructure:
- `AppDelegate` → `AppContentBlocking` → `UserContentUpdating` → `ScriptSourceProvider` (via `ScriptSourceProviding` protocol) → `UserScripts`
- Add the dependency to `ScriptSourceProviding` protocol as a property
- Add it to `ScriptSourceProvider` struct (property and initializer parameter)
- Add it to `UserContentUpdating` initializer and pass to `ScriptSourceProvider` in `makeValue` closure
- Add it to `AppContentBlocking` initializers (both convenience and main) and pass to `UserContentUpdating`
- Pass it from `AppDelegate` to `AppContentBlocking` initialization
- In `UserScripts`, access via `sourceProvider.duckPlayerPreferences` instead of using a default parameter
- This follows the same pattern as `WebTrackingProtectionPreferences` and `CookiePopupProtectionPreferences`
4. **Update utility code and extensions carefully**
- For helpers like `URL` extensions where dependency injection is impractical, read the instance from the composition root instead of a singleton:
- `NSApp.delegateTyped.aiChatPreferences` instead of `AIChatPreferences.shared`.
- Keep these usages minimal; prefer passing dependencies into call sites where it's feasible.
- **In AppDelegate extensions**: When the code is in an extension of `AppDelegate` (e.g., `extension AppDelegate`), access properties directly via `self.propertyName` rather than `NSApp.delegateTyped.propertyName`:
- ✅ `duckPlayerPreferences.reset()` (in `extension AppDelegate`)
- ❌ `NSApp.delegateTyped.duckPlayerPreferences.reset()` (unnecessary indirection)
- **For protocol-typed dependencies**: If a class conforms to a protocol (e.g., `AccessibilityPreferences` conforms to `SavedZoomLevelsCoordinating`), you can use the protocol type in initializers. The dependency can be passed as the protocol type while still being owned as the concrete type in `AppDelegate`.
- **In SwiftUI views**, once a dependency is available on a model (e.g., `PreferencesSidebarModel`), use the model's property rather than accessing via `NSApp.delegateTyped`:
- ✅ `AboutView(model: model.aboutPreferences)`
- ❌ `AboutView(model: NSApp.delegateTyped.aboutPreferences)`
- This ensures the view uses the injected instance and maintains proper dependency flow.
5. **Simplify protocol wrappers that only exist for the singleton**
- If a protocol exists solely to hide a singleton (e.g. a minimal `AIFeaturesStatusProviding` that just wraps `AIChatPreferences.shared`), prefer depending directly on the concrete type once it is injectible.
- Update initializers and stored properties to use the concrete type (`AIChatPreferences`) when it already exposes the required API and publishers.
6. **Update tests to construct their own instances**
- In tests, build the dependency explicitly instead of using global state. For example:
- `AIChatPreferences(storage: MockAIChatPreferencesStorage(), aiChatMenuConfiguration: MockAIChatConfig(), windowControllersManager: WindowControllersManagerMock(), featureFlagger: MockFeatureFlagger())`
- Pass these instances into the subject under test via its initializer (e.g. `PreferencesSidebarModel`, `BrowserTabViewController`, `PreferencesViewController`).
- Remove ad-hoc singleton-like test doubles (e.g. `MockAIChatPreferences.shared`) once real instances are injected.
- **Update all test helper methods**: When a test file has helper methods that create instances (e.g., `PreferencesSidebarModel` factory methods), update all of them to include the new dependency parameter.
- **Reuse existing mocks**: When initializing the dependency in tests, reuse existing mock objects from the test setup:
- Use `mockFeatureFlagger.internalUserDecider` if `MockFeatureFlagger` is already available
- Use existing `windowControllersManager` instances (e.g., `WindowControllersManagerMock()`)
- Example: `AboutPreferences(internalUserDecider: mockFeatureFlagger.internalUserDecider, featureFlagger: mockFeatureFlagger, windowControllersManager: windowControllersManager)`
- **Create shared test instances**: For test classes that have many tests using the dependency, create a shared instance as a property:
- `let accessibilityPreferences = AccessibilityPreferences()` at the class level
- Reuse this instance across multiple test methods to avoid creating duplicate instances
- **Update helper methods in test extensions**: Don't forget to update helper methods in extensions (e.g., `TabViewModel.aTabViewModel` static property) that create instances
- **Search comprehensively**: Use `grep` to find all test files that instantiate classes requiring the dependency, including:
- Direct instantiations in test methods
- Helper/factory methods that create instances
- Integration tests that create full object graphs
- Static helper properties/methods in test extensions
7. **Remove the singleton API last**
- After all production code and tests use the injected instance or the app-owned property, delete `static let shared` and any remaining references to it.
- Ensure you do not leave a mixed state where some call sites use the injected instance and others still use `.shared` for the same type within the modified area.
- Change `private init` to `init` to make the initializer publicly accessible once the singleton is removed.
### Example: AboutPreferences Refactoring
The `AboutPreferences.shared` singleton removal demonstrates the complete pattern:
1. **AppDelegate**: Added `let aboutPreferences: AboutPreferences` and initialized it with dependencies (`internalUserDecider`, `featureFlagger`, `windowControllersManager`)
2. **MainViewController**: Added `aboutPreferences: AboutPreferences = NSApp.delegateTyped.aboutPreferences` parameter (with default) and passed it to `BrowserTabViewController`
3. **BrowserTabViewController**: Added `aboutPreferences` property and parameter, stored it, and passed it to `PreferencesViewController`
4. **PreferencesViewController**: Added `aboutPreferences` parameter and passed it to `PreferencesSidebarModel`
5. **PreferencesSidebarModel**: Added `let aboutPreferences: AboutPreferences` property and updated both initializers to accept and store it
6. **PreferencesRootView**: Updated to use `model.aboutPreferences` instead of `NSApp.delegateTyped.aboutPreferences`
7. **Test files**: Updated all test files that create instances in the dependency chain:
- `PreferencesSidebarModelTests.swift`: Updated 3 helper methods to include `aboutPreferences` parameter
- `BrowserTabViewControllerOnboardingTests.swift`: Added `aboutPreferences` to `BrowserTabViewController` initialization
- `RootViewV2Tests.swift`: Added `aboutPreferences` to `PreferencesSidebarModel` initialization
- All tests reuse existing mocks: `AboutPreferences(internalUserDecider: mockFeatureFlagger.internalUserDecider, featureFlagger: mockFeatureFlagger, windowControllersManager: windowControllersManager)`
8. **AboutPreferences**: Removed `static let shared` and changed `private init` to `init`
This pattern ensures the dependency flows through the entire chain while maintaining testability and avoiding global state access in views.
### Example: AccessibilityPreferences Refactoring
The `AccessibilityPreferences.shared` singleton removal demonstrates additional patterns:
1. **AppDelegate**: Added `let accessibilityPreferences: AccessibilityPreferences` and initialized it with default dependencies
2. **Dependency chain**: Threaded through `MainViewController` → `BrowserTabViewController` → `PreferencesViewController` → `PreferencesSidebarModel` → `PreferencesRootView`
3. **TabViewModel**: Updated existing `accessibilityPreferences` parameter default from `.shared` to `NSApp.delegateTyped.accessibilityPreferences`
4. **Fire initializer (Main actor isolation)**: Used optional parameter pattern to avoid main actor isolation warning:
```swift
@MainActor
init(savedZoomLevelsCoordinating: SavedZoomLevelsCoordinating? = nil, ...) {
self.savedZoomLevelsCoordinating = savedZoomLevelsCoordinating ?? NSApp.delegateTyped.accessibilityPreferences
}
```
5. **Protocol conformance**: `AccessibilityPreferences` conforms to `SavedZoomLevelsCoordinating`, allowing it to be passed as a protocol type where needed
6. **Test patterns**: Created shared `accessibilityPreferences` instance in test classes:
```swift
final class TabViewModelTests: XCTestCase {
let accessibilityPreferences = AccessibilityPreferences()
// ... tests reuse this instance
}
```
7. **Test helper methods**: Updated all helper methods including static properties in extensions (e.g., `TabViewModel.aTabViewModel`)
### Example: DuckPlayerPreferences Refactoring
The `DuckPlayerPreferences.shared` singleton removal demonstrates the pattern for dependencies that need to reach `UserScripts` initialization:
1. **AppDelegate**: Added `let duckPlayerPreferences: DuckPlayerPreferences` and initialized it with dependencies (`privacyConfigurationManager`, `internalUserDecider`)
2. **Dependency chain for UserScripts**: Threaded through:
- `AppDelegate` → `AppContentBlocking` → `UserContentUpdating` → `ScriptSourceProvider` (via `ScriptSourceProviding` protocol) → `UserScripts`
- This follows the same pattern as `WebTrackingProtectionPreferences` and `CookiePopupProtectionPreferences`
3. **ScriptSourceProviding protocol**: Added `var duckPlayerPreferences: DuckPlayerPreferences { get }` property
4. **ScriptSourceProvider**: Added `duckPlayerPreferences` property and parameter to initializer
5. **UserContentUpdating**: Added `duckPlayerPreferences` parameter and passed it to `ScriptSourceProvider` in the `makeValue` closure
6. **AppContentBlocking**: Added `duckPlayerPreferences` to both convenience and main initializers, passed it to `UserContentUpdating`
7. **AppDelegate**: Passed `duckPlayerPreferences` to `AppContentBlocking` initialization (both DEBUG and release paths)
8. **UserScripts**: Removed default parameter `duckPlayerPreferences: DuckPlayerPreferences = NSApp.delegateTyped.duckPlayerPreferences` and accessed it via `sourceProvider.duckPlayerPreferences` instead
9. **Preferences view chain**: Also threaded through `MainViewController` → `BrowserTabViewController` → `PreferencesViewController` → `PreferencesSidebarModel` → `PreferencesRootView` for SwiftUI views
10. **MainMenuActions**: Updated to use `duckPlayerPreferences` directly (since it's in `extension AppDelegate`)
This pattern ensures dependencies that need to reach `UserScripts` initialization are properly injected through the content blocking infrastructure, avoiding default parameters that access `NSApp.delegateTyped` during initialization.
### Test Updates Checklist
When removing a singleton, ensure all tests are updated:
1. **Find all test files** that instantiate classes in the dependency chain:
```bash
grep -r "ClassName(" macOS/UnitTests macOS/IntegrationTests
```
2. **Update helper methods**: If test files have helper/factory methods that create instances, update all of them:
- Look for `private func` methods that return the type
- Look for `create*` or `make*` helper methods
- Example: `PreferencesSidebarModelTests.swift` had 3 helper methods that all needed `aboutPreferences`
3. **Reuse existing mocks**: When creating the dependency instance in tests:
- Check what mocks are already available in `setUp()` or test properties
- Use `mockFeatureFlagger.internalUserDecider` if available
- Reuse `WindowControllersManagerMock()` instances already created
- Avoid creating duplicate mock instances
4. **Verify compilation**: After updates, ensure:
- No linting errors
- All test files compile successfully
- Run tests to verify they pass
### Enforcement
- **Never approve PRs that add new `.shared`-style singletons in macOS code.**
- **When reviewing singleton removals, require:**
- A clearly owned instance on the composition root (`AppDelegate`).
- Dependencies threaded via initializers with sensible defaults from `NSApp.delegateTyped`.
- Tests constructing their own instances without relying on global state.
- No remaining usages of the removed `TypeName.shared` in the modified scope.
@@ -0,0 +1,424 @@
---
source: ~/DuckDuckGo/apple-browsers.git/main/.cursor/rules/macos-system-integration.mdc
confidence: 0.9
namespace: work
last_synced: 2026-04-28
alwaysApply: false
---
# macOS System Integration Patterns
## Background Agents and Services
Use proper service management for background agents:
```swift
// ✅ CORRECT - Background service management
final class BackgroundServiceManager {
private let agentIdentifier = "com.duckduckgo.agent"
private let extensionIdentifier = "com.duckduckgo.extension"
func registerBackgroundAgent() throws {
let service = SMAppService.agent(plistName: "BackgroundAgent.plist")
do {
try service.register()
print("Background agent registered successfully")
} catch {
print("Failed to register background agent: \(error)")
throw error
}
}
func unregisterBackgroundAgent() throws {
let service = SMAppService.agent(plistName: "BackgroundAgent.plist")
do {
try service.unregister()
print("Background agent unregistered successfully")
} catch {
print("Failed to unregister background agent: \(error)")
throw error
}
}
func checkServiceStatus() -> SMAppService.Status {
let service = SMAppService.agent(plistName: "BackgroundAgent.plist")
return service.status
}
}
// ❌ INCORRECT - Direct background processing in main app
final class FeatureManager {
func startBackgroundWork() {
// Don't run continuous background work in main app
DispatchQueue.global().async {
while true {
// This will drain battery and violate sandboxing
self.performWork()
Thread.sleep(forTimeInterval: 60)
}
}
}
}
```
## System Extensions
Use proper system extension lifecycle management:
```swift
// ✅ CORRECT - System extension management
import SystemExtensions
final class SystemExtensionManager: NSObject {
private let extensionIdentifier = "com.duckduckgo.network-extension"
func installExtension() {
let request = OSSystemExtensionRequest.activationRequest(
forExtensionWithIdentifier: extensionIdentifier,
queue: .main
)
request.delegate = self
OSSystemExtensionManager.shared.submitRequest(request)
}
func uninstallExtension() {
let request = OSSystemExtensionRequest.deactivationRequest(
forExtensionWithIdentifier: extensionIdentifier,
queue: .main
)
request.delegate = self
OSSystemExtensionManager.shared.submitRequest(request)
}
func checkExtensionStatus() async -> OSSystemExtensionRequest.Result? {
// Check if extension is already installed
return await withCheckedContinuation { continuation in
let request = OSSystemExtensionRequest.propertiesRequest(
forExtensionWithIdentifier: extensionIdentifier,
queue: .main
)
// Handle the properties request to determine status
// Implementation details...
continuation.resume(returning: nil)
}
}
}
// MARK: - OSSystemExtensionRequestDelegate
extension SystemExtensionManager: OSSystemExtensionRequestDelegate {
func request(
_ request: OSSystemExtensionRequest,
actionForReplacingExtension existing: OSSystemExtensionProperties,
withExtension extension: OSSystemExtensionProperties
) -> OSSystemExtensionRequest.ReplacementAction {
return .replace
}
func requestNeedsUserApproval(_ request: OSSystemExtensionRequest) {
print("System extension requires user approval")
// Show UI to guide user through approval process
showUserApprovalGuidance()
}
func request(
_ request: OSSystemExtensionRequest,
didFinishWithResult result: OSSystemExtensionRequest.Result
) {
switch result {
case .completed:
print("System extension request completed successfully")
handleExtensionActivated()
case .willCompleteAfterReboot:
print("System extension will be activated after reboot")
showRebootRequiredMessage()
@unknown default:
print("Unknown system extension result: \(result)")
}
}
func request(_ request: OSSystemExtensionRequest, didFailWithError error: Error) {
print("System extension request failed: \(error)")
handleExtensionError(error)
}
private func showUserApprovalGuidance() {
// Show UI to guide user through System Preferences
}
private func handleExtensionActivated() {
// Update UI to reflect extension is active
}
private func showRebootRequiredMessage() {
// Show UI indicating reboot is required
}
private func handleExtensionError(_ error: Error) {
// Handle extension installation errors
}
}
```
## Login Items Management
Use the modern SMAppService API for login items:
```swift
// ✅ CORRECT - Modern login items API
import ServiceManagement
final class LoginItemsManager {
func enableLoginItem() throws {
do {
try SMAppService.mainApp.register()
print("Login item enabled successfully")
} catch {
print("Failed to enable login item: \(error)")
throw LoginItemError.registrationFailed(error)
}
}
func disableLoginItem() throws {
do {
try SMAppService.mainApp.unregister()
print("Login item disabled successfully")
} catch {
print("Failed to disable login item: \(error)")
throw LoginItemError.unregistrationFailed(error)
}
}
var isLoginItemEnabled: Bool {
return SMAppService.mainApp.status == .enabled
}
var loginItemStatus: SMAppService.Status {
return SMAppService.mainApp.status
}
}
enum LoginItemError: LocalizedError {
case registrationFailed(Error)
case unregistrationFailed(Error)
var errorDescription: String? {
switch self {
case .registrationFailed(let error):
return "Failed to register login item: \(error.localizedDescription)"
case .unregistrationFailed(let error):
return "Failed to unregister login item: \(error.localizedDescription)"
}
}
}
// ❌ INCORRECT - Deprecated APIs
final class OldLoginItemsManager {
func enableLoginItem() {
// Don't use deprecated LSSharedFileList APIs
let loginItems = LSSharedFileListCreate(nil, kLSSharedFileListSessionLoginItems, nil)
// ... deprecated implementation
}
}
```
## Workspace Integration
Integrate properly with macOS workspace:
```swift
// ✅ CORRECT - Workspace integration
final class WorkspaceIntegration {
func openFileInFinder(at url: URL) {
NSWorkspace.shared.selectFile(nil, inFileViewerRootedAtPath: url.path)
}
func revealInFinder(fileAt url: URL) {
NSWorkspace.shared.selectFile(url.path, inFileViewerRootedAtPath: url.deletingLastPathComponent().path)
}
func openWithDefaultApplication(url: URL) {
NSWorkspace.shared.open(url)
}
func openWithApplication(url: URL, applicationURL: URL) {
NSWorkspace.shared.open([url], withApplicationAt: applicationURL, configuration: NSWorkspace.OpenConfiguration())
}
func getDefaultApplication(for url: URL) -> URL? {
return NSWorkspace.shared.urlForApplication(toOpen: url)
}
}
```
## Dock Integration
Handle dock interactions properly:
```swift
// ✅ CORRECT - Dock integration
final class DockIntegration {
func setBadgeCount(_ count: Int) {
NSApp.dockTile.badgeLabel = count > 0 ? "\(count)" : nil
}
func clearBadge() {
NSApp.dockTile.badgeLabel = nil
}
func setDockMenu(_ menu: NSMenu) {
NSApp.dockTile.contentView = nil
NSApp.dockTile.showsApplicationBadge = true
// Custom dock menu would be set through app delegate
}
}
// In AppDelegate
extension AppDelegate: NSApplicationDelegate {
func applicationDockMenu(_ sender: NSApplication) -> NSMenu? {
let dockMenu = NSMenu()
dockMenu.addItem(NSMenuItem(
title: "New Window",
action: #selector(newWindow),
keyEquivalent: ""
))
dockMenu.addItem(NSMenuItem(
title: "New Private Window",
action: #selector(newPrivateWindow),
keyEquivalent: ""
))
return dockMenu
}
@objc func newWindow() {
WindowsManager.openNewWindow()
}
@objc func newPrivateWindow() {
WindowsManager.openNewWindow(burnerMode: .burner)
}
}
```
## Notification Center Integration
Handle notifications properly:
```swift
// ✅ CORRECT - User notification handling
import UserNotifications
final class NotificationManager: NSObject {
func requestNotificationPermission() async -> Bool {
let center = UNUserNotificationCenter.current()
do {
let granted = try await center.requestAuthorization(options: [.alert, .sound, .badge])
return granted
} catch {
print("Failed to request notification permission: \(error)")
return false
}
}
func scheduleNotification(title: String, body: String, identifier: String) async {
let content = UNMutableNotificationContent()
content.title = title
content.body = body
content.sound = .default
let request = UNNotificationRequest(
identifier: identifier,
content: content,
trigger: nil
)
do {
try await UNUserNotificationCenter.current().add(request)
} catch {
print("Failed to schedule notification: \(error)")
}
}
}
// MARK: - UNUserNotificationCenterDelegate
extension NotificationManager: UNUserNotificationCenterDelegate {
func userNotificationCenter(
_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: @escaping () -> Void
) {
// Handle notification tap
handleNotificationResponse(response)
completionHandler()
}
func userNotificationCenter(
_ center: UNUserNotificationCenter,
willPresent notification: UNNotification,
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void
) {
// Show notification even when app is in foreground
completionHandler([.banner, .sound])
}
private func handleNotificationResponse(_ response: UNNotificationResponse) {
// Handle different notification actions
switch response.actionIdentifier {
case UNNotificationDefaultActionIdentifier:
// User tapped the notification
break
case UNNotificationDismissActionIdentifier:
// User dismissed the notification
break
default:
break
}
}
}
```
## App Group UserDefaults
Use app group UserDefaults for settings shared with system extensions:
```swift
// ✅ CORRECT - App group UserDefaults
extension AppUserDefaults {
private static let appGroupUserDefaults = UserDefaults(suiteName: "group.com.duckduckgo.app")
var networkProtectionEnabled: Bool {
get {
appGroupUserDefaults?.bool(forKey: "network_protection_enabled") ?? false
}
set {
appGroupUserDefaults?.set(newValue, forKey: "network_protection_enabled")
// Notify system extension of change
notifySystemExtension(of: .networkProtectionToggled(newValue))
}
}
var vpnServerLocation: String? {
get {
appGroupUserDefaults?.string(forKey: "vpn_server_location")
}
set {
appGroupUserDefaults?.set(newValue, forKey: "vpn_server_location")
}
}
private func notifySystemExtension(of change: SystemExtensionNotification) {
// Send notification to system extension via app group communication
let notificationName = "com.duckduckgo.settings.changed"
DistributedNotificationCenter.default().post(
name: Notification.Name(notificationName),
object: change.rawValue
)
}
}
enum SystemExtensionNotification: String {
case networkProtectionToggled = "network_protection_toggled"
case vpnServerChanged = "vpn_server_changed"
}
```
See `macos-window-management.md` for window management patterns and `macos-preferences.md` for preferences UI patterns.
@@ -0,0 +1,307 @@
---
source: ~/DuckDuckGo/apple-browsers.git/main/.cursor/rules/macos-window-management.mdc
confidence: 0.9
namespace: work
last_synced: 2026-04-28
alwaysApply: false
---
# macOS Window Management and AppKit Patterns
## WindowsManager for Window Operations
ALWAYS use WindowsManager for creating and managing browser windows:
```swift
// ✅ CORRECT - WindowsManager usage
@MainActor
final class FeatureCoordinator {
func openNewWindow() {
let tabCollection = TabCollectionViewModel()
WindowsManager.openNewWindow(
with: tabCollection,
burnerMode: .regular,
droppingPoint: nil
)
}
func openWindowWithURL(_ url: URL) {
let tabCollection = TabCollectionViewModel()
let window = WindowsManager.openNewWindow(with: tabCollection)
window?.tabCollectionViewModel.addTab(with: url)
}
}
// ❌ INCORRECT - Direct window creation
final class FeatureCoordinator {
func openNewWindow() {
let window = NSWindow() // Don't create windows directly
window.makeKeyAndOrderFront(nil)
}
}
```
## Window Controller Architecture
Use NSWindowController for complex window management:
```swift
// ✅ CORRECT - NSWindowController pattern
final class FeatureWindowController: NSWindowController {
private let viewModel: FeatureViewModel
init(viewModel: FeatureViewModel) {
self.viewModel = viewModel
super.init(window: nil)
setupWindow()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupWindow() {
let contentViewController = FeatureViewController(viewModel: viewModel)
window = NSWindow(contentViewController: contentViewController)
window?.setContentSize(NSSize(width: 800, height: 600))
window?.minSize = NSSize(width: 400, height: 300)
window?.center()
window?.title = "Feature Window"
// Configure window behavior
window?.isRestorable = true
window?.identifier = NSUserInterfaceItemIdentifier("FeatureWindow")
}
override func windowDidLoad() {
super.windowDidLoad()
// Additional window setup
window?.delegate = self
setupToolbar()
}
}
// MARK: - NSWindowDelegate
extension FeatureWindowController: NSWindowDelegate {
func windowWillClose(_ notification: Notification) {
// Clean up resources
viewModel.cleanup()
}
func windowDidBecomeMain(_ notification: Notification) {
// Handle window becoming main
viewModel.windowDidBecomeActive()
}
}
```
## Multi-Window State Management
Use TabCollectionViewModel for window-specific state:
```swift
// ✅ CORRECT - Window-specific state management
@MainActor
final class WindowCoordinator {
private let tabCollectionViewModel: TabCollectionViewModel
private weak var windowController: NSWindowController?
init(tabCollectionViewModel: TabCollectionViewModel) {
self.tabCollectionViewModel = tabCollectionViewModel
}
func currentTab() -> Tab? {
return tabCollectionViewModel.selectedTab
}
func addNewTab(with url: URL? = nil) {
tabCollectionViewModel.addTab(with: url)
}
func closeCurrentTab() {
guard let currentTab = tabCollectionViewModel.selectedTab else { return }
tabCollectionViewModel.removeTab(currentTab)
}
func closeWindow() {
windowController?.close()
}
}
```
## Window State Restoration
Implement proper state restoration:
```swift
// ✅ CORRECT - Window state restoration
extension FeatureWindowController {
override func restoreState(with coder: NSCoder) {
super.restoreState(with: coder)
// Restore window-specific state
if let savedData = coder.decodeObject(forKey: "viewModelState") as? Data {
viewModel.restoreState(from: savedData)
}
}
override func encodeRestorableState(with coder: NSCoder) {
super.encodeRestorableState(with: coder)
// Save window-specific state
if let stateData = viewModel.encodeState() {
coder.encode(stateData, forKey: "viewModelState")
}
}
}
```
## NSViewController and SwiftUI Integration
Use NSHostingView for SwiftUI integration:
```swift
// ✅ CORRECT - NSHostingView integration
final class FeatureViewController: NSViewController {
private let viewModel: FeatureViewModel
init(viewModel: FeatureViewModel) {
self.viewModel = viewModel
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func loadView() {
view = NSHostingView(rootView: FeatureView(viewModel: viewModel))
}
override func viewDidLoad() {
super.viewDidLoad()
title = "Feature"
preferredContentSize = NSSize(width: 400, height: 300)
}
override func viewWillAppear() {
super.viewWillAppear()
viewModel.viewWillAppear()
}
override func viewDidAppear() {
super.viewDidAppear()
viewModel.viewDidAppear()
}
}
```
## View Controller Lifecycle
Follow AppKit view controller patterns:
```swift
// ✅ CORRECT - AppKit lifecycle management
final class FeatureViewController: NSViewController {
private var observations: Set<NSObjectProtocol> = []
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
bindViewModel()
setupNotifications()
}
override func viewWillAppear() {
super.viewWillAppear()
viewModel.refreshData()
}
override func viewDidAppear() {
super.viewDidAppear()
// View is fully visible
viewModel.trackViewAppearance()
}
override func viewWillDisappear() {
super.viewWillDisappear()
viewModel.saveUserChanges()
}
private func setupNotifications() {
let observation = NotificationCenter.default.addObserver(
forName: .dataUpdated,
object: nil,
queue: .main
) { [weak self] _ in
self?.viewModel.refreshData()
}
observations.insert(observation)
}
deinit {
observations.forEach { NotificationCenter.default.removeObserver($0) }
observations.removeAll()
}
}
```
## Memory Management for Multiple Windows
Implement proper cleanup for window controllers:
```swift
// ✅ CORRECT - Window memory management
final class FeatureWindowController: NSWindowController {
private var observers: Set<NSObjectProtocol> = []
deinit {
// Clean up resources
observers.forEach { NotificationCenter.default.removeObserver($0) }
observers.removeAll()
viewModel.cleanup()
}
override func close() {
// Prepare for closure
viewModel.saveState()
super.close()
}
func cleanupBeforeClose() {
// Cancel any ongoing operations
viewModel.cancelOngoingOperations()
// Remove from window tracking
WindowTracker.shared.removeWindow(self)
}
}
```
## Window Cascading and Positioning
Handle window positioning properly:
```swift
// ✅ CORRECT - Window positioning
extension WindowsManager {
static func positionNewWindow(_ window: NSWindow) {
if let lastWindow = NSApp.orderedWindows.first {
let origin = lastWindow.frame.origin
let offset: CGFloat = 30
let newOrigin = NSPoint(
x: origin.x + offset,
y: origin.y - offset
)
// Ensure window stays on screen
let screenFrame = window.screen?.visibleFrame ?? NSScreen.main?.visibleFrame ?? .zero
if screenFrame.contains(NSRect(origin: newOrigin, size: window.frame.size)) {
window.setFrameOrigin(newOrigin)
} else {
window.center()
}
} else {
window.center()
}
}
}
```
See `macos-system-integration.md` for system-level integration patterns and `macos-preferences.md` for preferences management.
@@ -0,0 +1,136 @@
---
source: ~/DuckDuckGo/apple-browsers.git/main/.cursor/rules/maestro-device-selection.mdc
confidence: 0.9
namespace: work
last_synced: 2026-04-28
alwaysApply: false
---
# Maestro Test Device Selection (iPhone/iPad)
## Overview
Maestro tests can run on either iPhone or iPad simulators based on tags in the test YAML files. This allows you to test iPad-specific UI layouts and features while maintaining a single test suite.
## How It Works
- **Default**: Tests run on iPhone 16 with iOS 18.2
- **iPad Tests**: Add `ipad` tag to run on iPad 10th generation with iOS 18.2
## Usage
### For Individual iPad Tests
Add the `ipad` tag to your test file:
```yaml
appId: com.duckduckgo.mobile.ios
tags:
- ipad
- your-other-tags
name: Your iPad Test
---
# Your test steps here
```
### Running Tests
The same commands work as before:
```bash
# Run a single test (device selected based on tags)
./run_ui_tests.sh path/to/test.yaml
# Run all tests in a folder (each test runs on appropriate device)
./run_ui_tests.sh path/to/tests/
```
## Implementation Details
### Setup Phase (`setup_ui_tests.sh`)
1. Creates both iPhone and iPad simulators upfront
2. Boots and configures both simulators with English locale
3. Installs the app on both devices
4. Saves both simulator UUIDs for test execution
### Test Execution (`run_ui_tests.sh`)
1. Opens Simulator app if not already running
2. Checks each test file for the `ipad` tag using grep
3. Boots the required simulator if it's not already running
4. Uses the pre-created simulator based on the tag
5. Reinstalls the app before each test for clean state
6. Reports which device type was used in test results
## Simulator Specifications
- **iPhone**: iPhone 16, iOS 18.2
- **iPad**: iPad (10th generation), iOS 18.2
Both simulators are configured with:
- Language: English (en)
- Locale: en_US
- Name suffix: "(maestro)" for easy identification
## Example Test Structure
```yaml
appId: com.duckduckgo.mobile.ios
tags:
- ipad
- duckplayer
name: DuckPlayer iPad Layout Test
---
# This test will automatically run on iPad simulator
- runFlow:
file: ../shared/setup.yaml
# Test iPad-specific UI elements
- assertVisible: "Split View" # Example iPad-specific element
```
## Technical Implementation
### Tag Detection Function
The script uses awk to detect the 'ipad' tag:
```bash
check_for_ipad_tag() {
local test_file=$1
# Check if the test has an 'ipad' tag
if awk '/^tags:/{flag=1} flag && /^[^-]/{exit} flag && /- ipad/{found=1; exit} END{exit !found}' "$test_file" 2>/dev/null; then
echo "true"
else
echo "false"
fi
}
```
### Pre-created Simulators
Both simulators are created during setup for better performance:
```bash
# In setup_ui_tests.sh
create_or_get_simulator() {
local device_name=$1
local device_type=$2
local simulator_name="$device_name $target_os (maestro)"
# Creates simulator if it doesn't exist
# Returns simulator UUID
}
# Creates both simulators
iphone_uuid=$(create_or_get_simulator "iPhone-16" "iPhone-16")
ipad_uuid=$(create_or_get_simulator "iPad-10th-generation" "iPad-10th-generation")
```
### Simulator UUID Storage
UUIDs are saved to files for test execution:
- iPhone UUID: `DerivedData/device_uuid.txt`
- iPad UUID: `DerivedData/device_uuid_ipad.txt`
## Benefits
- **Single Test Suite**: Maintain one set of tests for both device types
- **Automatic Selection**: No need to manually switch simulators
- **Efficient Testing**: Only creates simulators when needed
- **Clear Indication**: Test output shows which device type is being used
## Future Enhancements
- Support for additional iPad models
- Different iOS versions per device type
- Landscape/portrait orientation configuration
- Device-specific timeout adjustments
@@ -0,0 +1,184 @@
---
source: ~/DuckDuckGo/apple-browsers.git/main/.cursor/rules/network-quality-scoring.mdc
confidence: 0.9
namespace: work
last_synced: 2026-04-28
alwaysApply: false
---
# NetworkQualityMonitor Scoring Algorithm
## Overview
NetworkQualityMonitor uses a browser-optimized scoring algorithm that prioritizes latency and consistency over raw bandwidth. The scoring is weighted as follows:
- **HTTP Response (40%)**: Most critical for browser experience
- **Bandwidth (35%)**: Important for media and downloads
- **DNS (10%)**: Foundation of all connections
- **Buffer Bloat (15%)**: Network congestion under load
## HTTP Response Scoring (40% Weight)
### Measurement Process
1. **Endpoints**: Tests 10+ global endpoints including DuckDuckGo, major CDNs, and popular platforms
2. **Sampling**: 15 requests per endpoint with interleaved ordering to avoid bias
3. **Statistical Analysis**:
- Calculates median response time for each site
- Uses **median of all site medians** for overall response time
- Calculates standard deviation for each site's measurements
- Uses **median of all site standard deviations** for consistency metric
### Response Time Calculation
```swift
// For each site: calculate median of its measurements
let siteMedians = sites.map { calculateMedian($0.measurements) }
// Overall response time: median of all site medians
let overallResponseTime = median(siteMedians)
```
This approach:
- Resists outliers at both per-site and cross-site levels
- Provides typical latency experience across geographic regions
- Doesn't get skewed by one particularly fast CDN or slow server
### Consistency Calculation
```swift
// For each site: calculate standard deviation of its measurements
let siteStdDevs = sites.map { calculateStdDev($0.measurements) }
// Overall consistency: median of all site standard deviations
let overallStdDev = median(siteStdDevs)
```
This avoids mixing different latency populations (e.g., 20ms CDN vs 300ms cross-ocean).
### Coefficient of Variation (CV) Scoring
The scoring uses Coefficient of Variation to fairly compare different latency ranges:
```swift
// CV = stdDev / mean - normalizes variance relative to baseline
let coefficientOfVariation = stdDev / averageResponseTime
// Apply CV-based penalty (fairer than raw standard deviation)
// Example: 10ms variance on 50ms latency (20% CV) penalized more than
// 10ms variance on 200ms latency (5% CV)
```
#### CV Penalty Thresholds
- **< 10% CV**: No penalty (excellent consistency)
- **10-20% CV**: -10 points (good consistency)
- **20-35% CV**: -25 points (acceptable consistency)
- **35-50% CV**: -40 points (poor consistency)
- **50-75% CV**: -55 points (very poor consistency)
- **> 75% CV**: -70 points (severe instability)
### Response Time Thresholds
- **Excellent (100 pts)**: < 50ms - Instantaneous
- **Very Good (85 pts)**: 50-75ms - Very responsive
- **Good (70 pts)**: 75-100ms - Target for production
- **Fair (55 pts)**: 100-150ms - Slight delay noticeable
- **Below Average (40 pts)**: 150-200ms - Definitely noticeable
- **Poor (25 pts)**: 200-300ms - Users frustrated
- **Very Poor (10 pts)**: > 300ms - Severe issues
## Bandwidth Scoring (35% Weight)
### Download (85% of bandwidth score)
Tests multiple endpoints with adaptive sizing:
- CloudFlare: 25MB
- OVH: 10MB
- Hetzner: 10MB
#### Download Speed Thresholds
- **100+ Mbps (100 pts)**: Excellent - instant page loads, 4K streaming
- **50-100 Mbps (85 pts)**: Very good - smooth HD streaming
- **25-50 Mbps (70 pts)**: Good - normal browsing experience
- **10-25 Mbps (55 pts)**: Fair - acceptable for most tasks
- **5-10 Mbps (40 pts)**: Below average - noticeable delays
- **2-5 Mbps (25 pts)**: Poor - significant limitations
- **< 2 Mbps (10 pts)**: Very poor - barely usable
### Upload (15% of bandwidth score)
Tests 5MB chunks to multiple endpoints.
#### Upload Speed Thresholds
- **50+ Mbps (100 pts)**: Excellent
- **20-50 Mbps (85 pts)**: Very good
- **10-20 Mbps (70 pts)**: Good
- **5-10 Mbps (55 pts)**: Fair
- **2-5 Mbps (40 pts)**: Below average
- **1-2 Mbps (25 pts)**: Poor
- **< 1 Mbps (10 pts)**: Very poor
## DNS Scoring (10% Weight)
### Measurement
- Tests 11 popular domains
- Uses median resolution time (resistant to outliers)
- Measures with `CFHostStartInfoResolution`
### DNS Resolution Thresholds
- **< 10ms (100 pts)**: Excellent - likely cached
- **10-30ms (85 pts)**: Good - fast resolver
- **30-50ms (70 pts)**: Fair - acceptable
- **50-100ms (50 pts)**: Poor - noticeable delay
- **> 100ms (30 pts)**: Very poor - severe issues
## Buffer Bloat Scoring (15% Weight)
### Measurement Process
1. **Baseline**: 10 latency measurements without load
2. **Loaded**: 15 measurements during concurrent download
3. **Calculation**: `loadedLatency - baselineLatency`
### Buffer Bloat Grades
- **Grade A (90 pts)**: < 5ms increase - No congestion
- **Grade B (70 pts)**: 5-30ms increase - Minimal impact
- **Grade C (50 pts)**: 30-100ms increase - Noticeable under load
- **Grade D (30 pts)**: 100-200ms increase - Significant congestion
- **Grade F (10 pts)**: > 200ms increase - Severe issues
## Overall Score Calculation
```swift
overallScore = (httpResponse * 0.40) +
(bandwidth * 0.35) +
(dns * 0.10) +
(bufferBloat * 0.15)
```
## Quality Determination
```swift
func determineQuality(from score: Double) -> NetworkQuality {
switch score {
case 80...: return .excellent // 🟢
case 60..<80: return .good // 🟡
case 40..<60: return .fair // 🟠
default: return .poor // 🔴
}
}
```
## Key Design Decisions
1. **Median-based calculations**: Resistant to outliers and network spikes
2. **Coefficient of Variation**: Fair comparison across different latency ranges
3. **Browser-optimized weights**: Prioritizes latency over bandwidth
4. **Interleaved testing**: Avoids consecutive requests to same endpoint
5. **Per-site then overall**: Avoids mixing different geographic populations
@@ -0,0 +1,104 @@
---
source: ~/DuckDuckGo/apple-browsers.git/main/.cursor/rules/network-quality-test-config.mdc
confidence: 0.9
namespace: work
last_synced: 2026-04-28
alwaysApply: false
---
# NetworkQualityMonitor Test Configuration
## Optimized Test Parameters
The NetworkQualityMonitor uses carefully tuned parameters to balance speed and accuracy for browser performance testing.
## Test Phases and Data Sizes
### HTTP Response Testing (Latency)
- **Samples**: 15 per endpoint
- **Endpoints**: ~12 globally distributed CDNs and services
- **Methodology**: Warm-up phase + interleaved sampling
- **Timeout**: 5 seconds
- **Total time**: ~3-4 minutes
### Bandwidth Testing (Download)
- **File size**: 50MB per server (reduced from 100MB)
- **Servers**: 3 test servers (CloudFlare, OVH, Hetzner)
- **Runs**: 1 per server (reduced from 2)
- **Total download**: ~150MB (was 800MB)
- **Timeout**: 20 seconds per server
- **Measurement window**:
- At 100 Mbps: ~4 seconds
- At 25 Mbps: ~16 seconds
- At 10 Mbps: ~40 seconds
### Upload Testing
- **Chunk size**: 20MB (reduced from 50MB)
- **Chunks**: 2 sequential uploads
- **Total upload**: 40MB (was 100MB)
- **Timeout**: 25 seconds total
- **Servers**: 3 endpoints
### DNS Resolution Testing
- **Domains**: 11 popular domains
- **Tests**: Resolution time and failure rate
- **Timeout**: Default system resolver timeout
## Performance Optimizations
### Why These Sizes?
**50MB for Downloads:**
- Large enough to overcome TCP slow start
- Provides stable measurement window (4-40 seconds)
- Small enough to complete quickly on slower connections
- Balances accuracy with user experience
**20MB for Uploads:**
- Sufficient to measure upload capacity
- Most users have asymmetric connections (slower upload)
- Reduces test time significantly
### Total Test Duration
**Typical completion times:**
- Fast connection (100+ Mbps): ~2-3 minutes
- Good connection (25-50 Mbps): ~3-4 minutes
- Fair connection (10-25 Mbps): ~4-5 minutes
- Poor connection (<10 Mbps): ~5-7 minutes
### Data Usage
**Total data transferred:**
- Download: ~150MB
- Upload: ~40MB
- **Total: ~190MB** (reduced from ~900MB)
## Configuration Code
```swift
TestConfiguration(
latencyTestURLs: [/* 12 CDN endpoints */],
bandwidthTestURLs: [
"https://speed.cloudflare.com/__down?bytes=52428800", // 50MB
"https://proof.ovh.net/files/50Mb.dat", // 50MB
"https://speed.hetzner.de/50MB.bin" // 50MB
],
uploadTestURLs: [/* 3 upload endpoints */],
dnsTestDomains: [/* 11 popular domains */],
latencySamplesPerEndpoint: 15,
bandwidthRunsPerServer: 1,
uploadChunkSize: 20_971_520, // 20MB
uploadChunkCount: 2,
latencyTestTimeout: 5,
bandwidthTestTimeout: 20,
uploadTestTimeout: 25
)
```
## Quick Test Mode
For rapid connectivity checks, the system also supports:
- 10MB quick downloads for server selection
- HEAD requests for basic connectivity
- Reduced sample counts for faster results
@@ -0,0 +1,166 @@
---
source: ~/DuckDuckGo/apple-browsers.git/main/.cursor/rules/network-quality-testing.mdc
confidence: 0.9
namespace: work
last_synced: 2026-04-28
alwaysApply: false
---
# NetworkQualityMonitor Testing Framework
## Overview
The NetworkQualityMonitor is a comprehensive network quality testing framework designed for the DuckDuckGo Privacy Browser. It provides pre-flight network connectivity and performance checks to ensure optimal browser performance.
## Architecture Principles
### SOLID Design
- **Single Responsibility**: Each tester handles one specific network metric
- **Open/Closed**: Protocol-based design allows extension without modification
- **Dependency Injection**: All dependencies injected for testability
- **Interface Segregation**: Focused protocols for each test type
### Component Structure
```
NetworkQualityMonitor (Orchestrator)
├── HttpResponseTester (Latency)
├── BandwidthTester (Speed)
├── DNSTester (Resolution)
├── BufferBloatTester (Congestion)
└── NetworkScoreCalculator (Scoring)
```
## Test Implementation Details
### HTTP Response Testing (Latency)
- **Multi-endpoint sampling**: Tests CDN endpoints (CloudFlare, Fastly, CloudFront)
- **Statistical analysis**: Calculates median, mean, standard deviation, CV
- **Smart aggregation**: Best site selection with weighted penalties
- **Metrics**: P50/P95 percentiles, variance, failure rate
### Bandwidth Testing
- **Server selection**: Quick 10MB test, then full test on best servers
- **Download measurement**: Multiple runs, returns maximum speed
- **Upload measurement**: Chunked uploads (50MB x 2)
- **Optimization**: Range requests, cache-busting, timeout protection
### DNS Testing
- **Domain resolution**: Popular domains (google.com, cloudflare.com)
- **Timing precision**: CFAbsoluteTime for microsecond accuracy
- **Failure tracking**: Resolution success/failure rates
- **System resolver**: Uses native DNS resolution
### Buffer Bloat Testing
- **Baseline measurement**: Unloaded network latency
- **Load testing**: Concurrent downloads during latency measurement
- **Grade assignment**: A-F based on latency increase percentage
- **Real-time impact**: Critical for video calls, gaming
## Scoring Algorithm
### Component Weights
- HTTP Response: 25%
- Bandwidth: 35%
- DNS: 15%
- Buffer Bloat: 25%
### Quality Ratings
- **Excellent (80-100)**: Optimal performance
- **Good (60-79)**: Good for most tasks
- **Fair (40-59)**: May experience issues
- **Poor (0-39)**: Significant issues likely
## Testing Best Practices
### Unit Testing
```swift
// Use protocol-based mocks
class MockHttpResponseTester: HttpResponseTesting {
func performTest(...) async throws -> HttpResponseResult {
// Return deterministic results
}
}
```
### Integration Testing
- Mock NetworkSession for controlled responses
- Test error scenarios and edge cases
- Verify progress callback behavior
### Performance Testing
- Monitor memory usage during large downloads
- Verify timeout handling
- Test concurrent execution
## Usage Patterns
### Basic Implementation
```swift
let monitor = NetworkQualityMonitor()
let results = try await monitor.runTest()
print("Quality: \(results.quality.rawValue)")
```
### With Progress Reporting
```swift
monitor.progressCallback = { progress, message in
// Update UI with progress
}
```
### Custom Configuration
```swift
let config = TestConfiguration(
latencyTestURLs: customURLs,
latencySamplesPerEndpoint: 20
)
let monitor = NetworkQualityMonitor(configuration: config)
```
## Security Considerations
- **HTTPS only**: All endpoints use secure connections
- **No user data**: Only generic test payloads
- **Certificate validation**: Standard validation enabled
- **Rate limiting**: Built-in delays between samples
## Error Handling
### Error Types
- `invalidResponse`: HTTP errors, malformed data
- `allTestsFailed`: Complete connectivity loss
- `insufficientData`: Not enough samples collected
- `timeout`: Test exceeded time limit
### Recovery Strategies
- Continue testing if individual endpoints fail
- Provide partial results when possible
- Clear error reporting with localized descriptions
## Performance Optimizations
- **HEAD requests**: Minimal data for latency tests
- **Range requests**: Efficient server selection
- **Connection reuse**: URLSession connection pooling
- **Memory streaming**: Large downloads streamed, not buffered
## Package Integration
### Adding to Project
1. Add NetworkQualityMonitor package dependency
2. Import NetworkQualityMonitor module
3. Initialize with configuration
4. Handle async test execution
### Debug Menu Integration
- Available under Debug → Network Quality
- Individual test execution
- Detailed result display
## Future Enhancements
- IPv6 testing separation
- Jitter analysis
- Packet loss detection
- Geographic server selection
- Historical trending
@@ -0,0 +1,108 @@
---
source: ~/DuckDuckGo/apple-browsers.git/main/.cursor/rules/network-quality-variance-scoring.mdc
confidence: 0.9
namespace: work
last_synced: 2026-04-28
alwaysApply: false
---
# Network Quality Variance Scoring Using Coefficient of Variation
## Overview
NetworkQualityMonitor displays variance in **milliseconds** (user-friendly) but scores using **Coefficient of Variation (CV)** internally. This provides fair comparison across different latency ranges.
## Display vs Scoring
- **UI Display**: Shows variance in milliseconds (e.g., "5.2 ms (10.4%)")
- **Internal Scoring**: Uses CV = (stdDev/mean) × 100 for penalties
- **Quality Labels**: Based on CV percentage thresholds
## Why CV for Scoring?
A 10ms variance means different things at different latencies:
- 10ms variance on 50ms latency = 20% CV (moderate issue)
- 10ms variance on 200ms latency = 5% CV (excellent consistency)
Using CV ensures fair scoring regardless of base latency.
## CV Thresholds and Test Iterations
| CV Range | Quality | Penalty | Test Iterations | Testing Impact |
|----------|---------|---------|-----------------|----------------|
| <10% | Excellent | 0 pts | ~30 iterations | Quick reliable tests |
| 10-20% | Good | -20 pts | ~100 iterations | Reasonable test time |
| 20-40% | Fair | -40 pts | ~400 iterations | Lengthy test cycles |
| >40% | Poor | -60 to -80 pts | 1000+ iterations | Practically unreliable |
## Implementation
```swift
// Display shows milliseconds
value: String(format: "%.1f ms", responseVariance)
// Quality label uses CV internally
let cv = (variance / avgResponseTime) * 100
if cv < 10 { return "Excellent" }
else if cv < 20 { return "Good" }
else if cv < 40 { return "Fair" }
else { return "Poor" }
// Scoring penalty based on CV
switch coefficientOfVariation {
case ..<10: penalty = 0
case 10..<20: penalty = 20
case 20..<40: penalty = 40
case 40..<60: penalty = 60
default: penalty = 80
}
```
## Statistical Analysis
Smart Warm-up Phase:
- Initial "cold" request to each endpoint (DNS resolution, TLS handshake)
- These measurements are discarded to eliminate first-request bias
- Ensures subsequent measurements reflect warm connection performance
Interleaved Sampling:
- Endpoints tested in randomized rounds (not consecutively)
- Prevents TCP connection reuse artifacts
- 15 samples per endpoint with 50ms delays between measurements
- More representative of real browsing patterns
Per-Site Calculations:
- Calculate median response time (robust to outliers)
- Calculate variance and standard deviation
- Track individual site consistency
Global Aggregation:
adjustedResponseTime = median(all_site_medians)
Variance Scoring:
- Display: Standard deviation in milliseconds (user-friendly)
- Scoring: Coefficient of Variation (CV = stdDev/mean × 100)
- CV determines penalties based on relative variance
Dual Penalty System:
1. CV-based: Relative variance penalties (up to 80 points)
2. P95-P50 Spread: Percentage-based spike penalties (up to 40 points)
## Key Metrics
- **averageResponseTime**: Median of all site medians (geographic reality)
- **responseVariance**: Standard deviation in ms (consistency indicator)
- **latencySpread**: P95-P50 difference (spike detection)
- **p50/p95**: Percentiles for typical and worst-case assessment
## Testing Impact
The CV directly determines test reliability:
- **<10% CV**: Standard test suite (30-50 iterations)
- **10-20% CV**: Increase to 100-150 iterations
- **20-40% CV**: Need 400+ iterations for confidence
- **>40% CV**: Results unreliable even with 1000+ iterations
## Key Principle
**Variance has HUGE impact on performance testing.** High CV connections can turn a 1-hour test into a 10-hour marathon with less reliable results than a 30-minute test on a stable connection.
@@ -0,0 +1,407 @@
---
source: ~/DuckDuckGo/apple-browsers.git/main/.cursor/rules/performance-optimization.mdc
confidence: 0.9
namespace: work
last_synced: 2026-04-28
alwaysApply: false
---
# Performance Optimization Guidelines
## Memory Management
### Avoid Retain Cycles
```swift
// Use weak/unowned references appropriately
class ViewController: UIViewController {
private var timer: Timer?
override func viewDidLoad() {
super.viewDidLoad()
// Bad - Creates retain cycle
timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
self.updateUI()
}
// Good - Weak reference prevents retain cycle
timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
self?.updateUI()
}
}
deinit {
timer?.invalidate()
}
}
```
### Lazy Loading
```swift
class DataManager {
// Load expensive resources only when needed
private lazy var database: Database = {
return Database()
}()
// Use computed properties for lightweight calculations
var itemCount: Int {
return items.count
}
// Cache expensive computations
private var _processedData: [ProcessedItem]?
var processedData: [ProcessedItem] {
if let cached = _processedData {
return cached
}
let processed = items.map { ProcessedItem($0) }
_processedData = processed
return processed
}
}
```
### Memory-Efficient Collections
```swift
// Use appropriate collection types
struct LargeDataSet {
// Bad - Loads all data into memory
var allItems: [Item] {
return database.fetchAll()
}
// Good - Use lazy sequences
var items: LazySequence<[Item]> {
return database.fetchAll().lazy
}
// Better - Use pagination
func items(page: Int, pageSize: Int = 50) -> [Item] {
return database.fetch(offset: page * pageSize, limit: pageSize)
}
}
```
## UI Performance
### Main Thread Protection
```swift
class ImageLoader {
func loadImage(from url: URL, completion: @escaping (UIImage?) -> Void) {
Task {
// Perform heavy work on background queue
let data = try? await URLSession.shared.data(from: url).0
let image = data.flatMap { UIImage(data: $0) }
// Always update UI on main thread
await MainActor.run {
completion(image)
}
}
}
}
```
### Efficient Table/Collection Views
```swift
class OptimizedTableViewController: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Register reusable cells
tableView.register(CustomCell.self, forCellReuseIdentifier: "Cell")
// Set estimated heights for better scrolling
tableView.estimatedRowHeight = 44.0
tableView.rowHeight = UITableView.automaticDimension
// Enable prefetching
tableView.prefetchDataSource = self
}
// Reuse cells efficiently
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! CustomCell
// Configure cell with minimal work
cell.configure(with: items[indexPath.row])
// Cancel any ongoing async work
cell.prepareForReuse()
return cell
}
}
extension OptimizedTableViewController: UITableViewDataSourcePrefetching {
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
// Preload data for upcoming cells
let urls = indexPaths.compactMap { items[$0.row].imageURL }
ImageCache.shared.preload(urls: urls)
}
}
```
### Image Optimization
```swift
extension UIImage {
// Resize images to appropriate size
func resized(to targetSize: CGSize) -> UIImage? {
let renderer = UIGraphicsImageRenderer(size: targetSize)
return renderer.image { _ in
self.draw(in: CGRect(origin: .zero, size: targetSize))
}
}
// Decode images on background queue
func decodedImage() -> UIImage? {
guard let cgImage = cgImage else { return nil }
let colorSpace = CGColorSpaceCreateDeviceRGB()
let context = CGContext(
data: nil,
width: cgImage.width,
height: cgImage.height,
bitsPerComponent: 8,
bytesPerRow: cgImage.width * 4,
space: colorSpace,
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue
)
context?.draw(cgImage, in: CGRect(x: 0, y: 0, width: cgImage.width, height: cgImage.height))
guard let decodedImage = context?.makeImage() else { return nil }
return UIImage(cgImage: decodedImage)
}
}
```
## Network Performance
### Efficient API Calls
```swift
class APIClient {
private let session: URLSession
private let cache = URLCache(
memoryCapacity: 10 * 1024 * 1024, // 10 MB
diskCapacity: 50 * 1024 * 1024, // 50 MB
diskPath: nil
)
init() {
let configuration = URLSessionConfiguration.default
configuration.urlCache = cache
configuration.requestCachePolicy = .returnCacheDataElseLoad
configuration.timeoutIntervalForRequest = 30
configuration.httpMaximumConnectionsPerHost = 5
self.session = URLSession(configuration: configuration)
}
// Batch requests when possible
func fetchMultipleItems(ids: [String]) async throws -> [Item] {
// Bad - Multiple individual requests
// let items = try await ids.asyncMap { try await fetchItem(id: $0) }
// Good - Single batch request
let request = BatchRequest(ids: ids)
return try await fetch(request)
}
// Use compression
func createRequest(url: URL) -> URLRequest {
var request = URLRequest(url: url)
request.addValue("gzip, deflate", forHTTPHeaderField: "Accept-Encoding")
return request
}
}
```
### Download Optimization
```swift
class DownloadManager {
// Use background sessions for large downloads
private lazy var backgroundSession: URLSession = {
let configuration = URLSessionConfiguration.background(withIdentifier: "com.duckduckgo.downloads")
configuration.isDiscretionary = true
configuration.sessionSendsLaunchEvents = true
return URLSession(configuration: configuration, delegate: self, delegateQueue: nil)
}()
// Resume interrupted downloads
func resumeDownload(from resumeData: Data) {
let task = backgroundSession.downloadTask(withResumeData: resumeData)
task.resume()
}
// Limit concurrent downloads
private let downloadQueue = OperationQueue()
init() {
downloadQueue.maxConcurrentOperationCount = 3
}
}
```
## Database Performance
### Efficient Queries
```swift
import GRDB
class DatabaseManager {
// Use indexes for frequently queried columns
func createIndexes(_ db: Database) throws {
try db.create(index: "idx_bookmarks_url", on: "bookmarks", columns: ["url"])
try db.create(index: "idx_history_date", on: "history", columns: ["visitDate"])
}
// Batch operations
func insertMultipleItems(_ items: [Item]) throws {
try dbQueue.write { db in
// Use transactions for bulk operations
try items.forEach { item in
try item.insert(db)
}
}
}
// Use appropriate fetch limits
func fetchRecentHistory(limit: Int = 100) throws -> [HistoryItem] {
try dbQueue.read { db in
try HistoryItem
.order(Column("visitDate").desc)
.limit(limit)
.fetchAll(db)
}
}
// Optimize complex queries
func searchBookmarks(query: String) throws -> [Bookmark] {
try dbQueue.read { db in
// Use FTS (Full Text Search) for text searching
let pattern = "%\(query)%"
return try Bookmark
.filter(Column("title").like(pattern) || Column("url").like(pattern))
.limit(50)
.fetchAll(db)
}
}
}
```
## Algorithm Optimization
### Use Efficient Data Structures
```swift
// Choose appropriate data structures
class URLMatcher {
// Bad - O(n) lookup
private var blockedURLs: [String] = []
func isBlocked(_ url: String) -> Bool {
return blockedURLs.contains(url)
}
// Good - O(1) lookup
private var blockedURLSet: Set<String> = []
func isBlockedOptimized(_ url: String) -> Bool {
return blockedURLSet.contains(url)
}
}
```
### Avoid Expensive Operations
```swift
extension Array {
// Bad - Creates multiple intermediate arrays
func processItems() -> [ProcessedItem] {
return self
.compactMap { $0 as? Item }
.filter { $0.isValid }
.map { ProcessedItem($0) }
.sorted { $0.priority > $1.priority }
}
// Good - Use lazy evaluation
func processItemsOptimized() -> [ProcessedItem] {
return self.lazy
.compactMap { $0 as? Item }
.filter { $0.isValid }
.map { ProcessedItem($0) }
.sorted { $0.priority > $1.priority }
}
}
```
## Monitoring and Profiling
### Performance Metrics
```swift
class PerformanceMonitor {
static func measure<T>(
_ title: String,
operation: () throws -> T
) rethrows -> T {
let startTime = CFAbsoluteTimeGetCurrent()
defer {
let timeElapsed = CFAbsoluteTimeGetCurrent() - startTime
print("⏱ \(title): \(timeElapsed)s")
// Log slow operations
if timeElapsed > 1.0 {
Pixel.fire(.performanceWarning(operation: title, duration: timeElapsed))
}
}
return try operation()
}
}
// Usage
let results = PerformanceMonitor.measure("Database Query") {
try database.fetchAllBookmarks()
}
```
### Memory Monitoring
```swift
class MemoryMonitor {
static var currentMemoryUsage: Double {
var info = mach_task_basic_info()
var count = mach_msg_type_number_t(MemoryLayout<mach_task_basic_info>.size) / 4
let result = withUnsafeMutablePointer(to: &info) {
$0.withMemoryRebound(to: integer_t.self, capacity: 1) {
task_info(mach_task_self_,
task_flavor_t(MACH_TASK_BASIC_INFO),
$0,
&count)
}
}
return result == KERN_SUCCESS ? Double(info.resident_size) / 1024.0 / 1024.0 : 0
}
static func logMemoryUsage(_ context: String) {
let usage = currentMemoryUsage
print("💾 Memory usage (\(context)): \(usage) MB")
if usage > 200 { // 200 MB threshold
Pixel.fire(.highMemoryUsage(context: context, usage: usage))
}
}
}
```
## Best Practices Summary
1. **Profile First**: Use Instruments to identify actual bottlenecks
2. **Measure Impact**: Quantify performance improvements
3. **Cache Wisely**: Cache expensive computations but watch memory usage
4. **Async Everything**: Keep UI responsive with background processing
5. **Batch Operations**: Combine multiple operations when possible
6. **Lazy Loading**: Load data only when needed
7. **Resource Management**: Release resources promptly
8. **Monitor Production**: Track performance metrics in production
@@ -0,0 +1,307 @@
---
source: ~/DuckDuckGo/apple-browsers.git/main/.cursor/rules/pixel-definitions.mdc
confidence: 0.9
namespace: work
last_synced: 2026-04-28
description: Rules for creating and maintaining pixel definition JSON5 files that document pixels fired by the iOS and macOS apps
alwaysApply: false
---
# Pixel Registry Definitions
## Overview
Pixel definitions are JSON5 files that document pixels and wide events fired by the iOS and macOS apps. They live in:
- **iOS:** `iOS/PixelDefinitions/pixels/definitions/*.json5`
- **macOS:** `macOS/PixelDefinitions/pixels/definitions/*.json5`
- **iOS wide events:** `iOS/PixelDefinitions/wide_events/definitions/*.json5`
Each platform has its own `params_dictionary.json5` and `suffixes_dictionary.json5` for reusable definitions.
**Note:** Each definitions directory contains a `TEMPLATE.json5` file. These are scaffolds for creating new definition files — they are not real pixel definitions. Ignore them when reviewing or auditing existing definitions (their placeholder `expires` dates are intentional examples).
## Pixel Definition Structure
Each `.json5` file is a JSON5 object where keys are pixel names and values describe the pixel:
```json5
{
"pixel_name_here": {
"description": "When and why this pixel fires",
"owners": ["githubUsername"],
"triggers": ["other"],
"suffixes": ["first_daily_count", "platform", "form_factor"],
"parameters": ["appVersion", "errorCode", "errorDomain"],
// Only for temporary pixels — omit for permanent ones
"expires": "2025-06-30"
}
}
```
### Required Fields
| Field | Type | Description |
|-------|------|-------------|
| `description` | string | When the pixel fires and its purpose |
| `owners` | string[] | GitHub usernames of responsible people |
| `triggers` | string[] | What causes the pixel to fire (see trigger values below) |
### Optional Fields
| Field | Type | Description |
|-------|------|-------------|
| `suffixes` | array | Dynamic parts appended to the pixel name |
| `parameters` | array | Query parameters sent with the pixel |
| `expires` | string | ISO date (`YYYY-MM-DD`) for temporary pixels |
### Trigger Values
Valid trigger values: `"other"`, `"scheduled"`, `"startup"`, `"page_load"`, `"new_tab"`, `"exception"`, `"user_submitted"`, `"search_ddg"`.
Most pixels use `"other"`. Use `"scheduled"` for daily/periodic pixels, `"startup"` for app-launch pixels, and `"page_load"` for navigation-related pixels.
## Determining Parameters from Swift Code
Pixel definitions must document **all** query parameters sent over the wire, including default ones. To determine the correct parameters:
### Always-included Parameters
**`appVersion`** is added by default to every pixel by PixelKit. Include `"appVersion"` in every definition, unless the pixel call disables it.
**`pixelSource`** is automatically added only if the pixel's `standardParameters` property returns `[.pixelSource]`. Check the pixel event's `standardParameters` computed property in Swift — if it returns `[.pixelSource]`, include `"pixelSource"` in the definition.
### Error Parameters
If the pixel event carries an `Error` (via associated value or the `error` property), PixelKit automatically extracts and sends:
- `errorCode` (key: `"e"`) and `errorDomain` (key: `"d"`)
- `underlyingErrorCode` (key: `"ue"`) and `underlyingErrorDomain` (key: `"ud"`) if present
Include these dictionary references in the definition when the pixel carries error information.
### Custom Parameters
Check the pixel event's `parameters` computed property in Swift for any additional parameters. Also inspect the call site where the pixel is fired — look for `withAdditionalParameters:` arguments and trace any helper functions that build those parameters. These are pixel-specific and must be included in the definition (either as dictionary references or inline objects).
### Where to Look in Swift
- **iOS:** `iOS/Core/PixelEvent.swift` defines pixel names. `iOS/Core/Pixel.swift` has `PixelParameters` constants. Check the `parameters` and `standardParameters` properties on the pixel event enum.
- **macOS:** `macOS/DuckDuckGo/Statistics/GeneralPixel.swift` defines many pixel names, parameters, and standard parameters. However, pixel events can also be defined in dedicated files (e.g. `UpdateFlowPixels.swift`, `CrashReportPixels.swift`) — search for types conforming to `PixelKitEvent`.
- **Shared:** `SharedPackages/BrowserServicesKit/Sources/PixelKit/` contains `PixelKit.swift` (firing logic) and `PixelKitEvent.swift` (protocol).
## Reusing Parameters from the Dictionary
`params_dictionary.json5` defines common parameters. Reference them by key name as a string:
```json5
"parameters": [
"appVersion", // Reuses definition from params_dictionary.json5
"errorCode", // key: "e", type: integer
"errorDomain", // key: "d", type: string
"underlyingErrorCode",
"underlyingErrorDomain"
]
```
To define a custom inline parameter, use an object instead:
```json5
"parameters": [
"appVersion",
{
"key": "customParam",
"type": "string",
"description": "What this parameter represents",
"enum": ["value1", "value2"]
}
]
```
### Parameter Object Fields
- `key` — the actual query parameter key sent in the pixel (use this for fixed keys)
- `keyPattern` — regex pattern for dynamic keys (e.g. `"^ue[0-9]?$"` for `ue`, `ue0`, `ue1`, etc.)
- `type` — `"string"`, `"integer"`, `"number"`, or `"boolean"`
- `description` — what the parameter represents
- `enum` — allowed values (optional)
- `pattern` — regex validation pattern (optional)
- `examples` — example values (optional)
Use `key` or `keyPattern`, not both.
## Suffixes
A suffix is a string appended to the base pixel name to create distinct variants of the same pixel. For example, a pixel with the `"daily"` suffix set will produce a variant with `_daily` appended to the name (e.g. `m_mac_default-browser_daily`). When a pixel has multiple suffix sets, PixelKit generates all combinations — so `["first_daily_count", "platform", "form_factor"]` produces variants like `m_pixel_count_ios_phone`, `m_pixel_daily_ios_tablet`, etc.
In some cases a suffix value is always present (rather than being a set of variants). For example, a pixel fired with the legacy daily frequency always gets `_d` appended to its name. When the suffix is fixed and always present, it may be baked directly into the pixel name in the definition (e.g. `m_mac_daily_active_user_d`) rather than declared in the `suffixes` field. See "Legacy Suffix Patterns" below.
### Reusing Suffixes from the Dictionary
`suffixes_dictionary.json5` defines common suffixes. Reference them by key name:
```json5
// Each string references a suffix set from the dictionary
"suffixes": ["first_daily_count", "platform", "form_factor"]
```
Common suffix keys (both platforms unless noted):
- `first_daily_count` — `["first", "daily", "count"]`
- `legacy_daily_count` — `["d", "c"]`
- `daily` — `["daily"]`
- `daily_standard` — `["daily", ""]`
- `count` — `["count"]`
- `daily_count` — `["daily", "count"]` (macOS only)
- `platform` — `["ios"]` (iOS only)
- `form_factor` — `["phone", "tablet"]` (iOS only)
- `time_bucket` — `["0", "0.1", "0.5", "1", "5", "10", "20", "40", "more"]` (iOS only)
For custom inline suffixes, use an object:
```json5
"suffixes": [
"first_daily_count",
{
"description": "The result of the operation",
"enum": ["success", "failure"]
}
]
```
Inline suffix objects support `description`, `enum`, and optionally `key`, `type`, and `pattern` (same fields as parameter objects).
### Mapping Swift Firing Methods to Suffixes
The suffix you use depends on how the pixel is fired in Swift:
**iOS** (uses `DailyPixel` / `Pixel` / `UniquePixel`):
| Swift Method | Suffix Dictionary Key |
|---|---|
| `Pixel.fire(pixel:)` | No scheduling suffix (use only `platform`/`form_factor`) |
| `DailyPixel.fire(pixel:)` | `"daily"` |
| `DailyPixel.fireDailyAndCount(pixel:)` | `"first_daily_count"` |
| `DailyPixel.fireDailyAndCount(pixel:, pixelNameSuffixes: .legacyDailyPixelSuffixes)` | `"legacy_daily_count"` |
| `UniquePixel.fire(pixel:)` | No suffix (pixel name typically ends in `_u` or `_unique`) |
On iOS, most pixels also get `platform` and `form_factor` suffixes appended automatically. Include `["platform", "form_factor"]` for iOS pixels unless you confirm otherwise.
**macOS** (uses `PixelKit.fire(event, frequency:)`):
| `frequency:` Value | Suffix Dictionary Key |
|---|---|
| `.standard` (or omitted) | No scheduling suffix |
| `.daily` | `"daily"` |
| `.dailyAndCount` | `"daily_count"` (macOS) or `"first_daily_count"` (iOS) |
| `.dailyAndStandard` | `"daily_standard"` |
| `.legacyDaily` | No suffix field — the `_d` is baked into the pixel name (see "Legacy Suffix Patterns") |
| `.legacyDailyAndCount` | `"legacy_daily_count"` |
Note: iOS `DailyPixel.fireDailyAndCount` uses `"first_daily_count"` (includes a `_first` pixel on first-ever fire). macOS `.dailyAndCount` uses `"daily_count"` (no `_first`). Check which platform you are writing for.
### Compound Suffixes
Suffixes can be nested in an inner array to form compound suffixes (combined into a single suffix segment):
```json5
"suffixes": [["platform", "form_factor"]]
```
This produces suffixes like `ios_phone`, `ios_tablet` rather than separate independent suffix positions.
### Legacy Suffix Patterns
Some older pixels use legacy firing frequencies (`.legacyDaily`, `.legacyDailyAndCount`) where the pixel library appends short suffixes like `_d` (daily) or `_c` (count) to the pixel name. In these cases, the suffix is baked directly into the pixel name in the definition rather than declared in the `suffixes` field.
For example, `m_mac_daily_active_user_d` is fired with `frequency: .legacyDaily`. The Swift code defines the base name as `"m_mac_daily_active_user"`, and PixelKit appends `_d` automatically. The definition uses the full name `m_mac_daily_active_user_d` with no `suffixes` field — this is correct.
Compare this to the modern pattern where `m_mac_default-browser` uses `frequency: .daily` and declares `"suffixes": ["daily"]`, producing `m_mac_default-browser_daily`.
When writing definitions for legacy pixels, use the full pixel name (including the baked-in suffix) and use the `"legacy_daily_count"` suffix dictionary entry only if the pixel uses `.legacyDailyAndCount` (which produces both `_d` and `_c` variants).
## Wide Events (iOS)
Wide events use a different, richer schema in `iOS/PixelDefinitions/wide_events/definitions/`. They have a hierarchical structure with `meta`, `feature`, and `feature.data` sections:
```json5
{
"event-name": {
"description": "The purpose of the wide event",
"owners": ["githubUsername"],
"meta": {
"type": "unique-event-name",
"version": "0.0"
},
"feature": {
"name": "feature-name",
"status": ["SUCCESS", "FAILURE", "UNKNOWN"],
"data": {
"ext": {
"custom_field": {
"type": "string",
"description": "A custom extension field",
"enum": ["val1", "val2"]
},
// Can reference props_dictionary.json entries by string
"application_state": "foregroundBackgroundState"
},
"error": {
"domain": { "type": "string", "description": "Error domain" },
"code": { "type": "integer", "description": "Error code" }
}
}
}
}
}
```
Wide events sent as standard pixels (in `pixels/definitions/`) use `wideEvent*` parameters from the params dictionary (e.g. `wideEventAppName`, `wideEventFeatureStatus`, `wideEventErrorDomain`).
## Naming Conventions
- **iOS pixels:** typically prefixed with `m_` (e.g. `m_netp_ev_good_latency`), though some lack this prefix (e.g. `autofill_extension_*`, `attributed_metric_*`)
- **macOS pixels:** typically prefixed with `m_mac_` (e.g. `m_mac_daily_active_user_d`)
- **iOS wide events (as pixels):** prefixed with `m_ios_wide_` (e.g. `m_ios_wide_vpn_connection`)
- Use lowercase with underscores or hyphens
- Always use the exact pixel name string from the Swift code (e.g. from `PixelEvent.swift` or `GeneralPixel.swift`)
## File Organization
Group related pixels into a single definition file named after the feature area, for example:
- `navigation.json5` — Browser navigation pixels
- `onboarding.json5` — Onboarding pixels
- `vpn_latency.json5` — VPN latency measurement pixels
See existing files in the `definitions/` directory for naming patterns.
## Validation and Linting
Run from the `iOS/` or `macOS/` directory:
```bash
# Install dependencies (from repo root)
npm clean-install --include-workspace-root
# Full validation (schema + formatting)
npm run validate-pixel-defs
# Schema validation only
npm run validate-defs-without-formatting
# Check formatting
npm run pixel-lint
# Auto-fix formatting
npm run pixel-lint.fix
```
**Always run `npm run validate-pixel-defs`** from the relevant platform directory after making changes.
## Quick Reference: Adding a New Pixel
1. Determine the pixel name and parameters from the iOS/macOS codebase
2. Find or create the appropriate `.json5` file in `{platform}/PixelDefinitions/pixels/definitions/`
3. Add your pixel entry with `description`, `owners`, `triggers`
4. Add `suffixes` and `parameters` — reuse dictionary entries wherever possible
5. Check whether the pixel should be temporary, and if so then add `"expires": "YYYY-MM-DD"`
6. Run `npm run validate-pixel-defs` from the platform directory
7. Run `npm run pixel-lint.fix` if formatting issues are reported
+268
View File
@@ -0,0 +1,268 @@
---
source: ~/DuckDuckGo/apple-browsers.git/main/.cursor/rules/pixels.mdc
confidence: 0.9
namespace: work
last_synced: 2026-04-28
description: How to define, name, and fire pixels on iOS and macOS.
alwaysApply: false
---
# Pixels
Pixels are one-off telemetry events sent via HTTP GET with a name and optional parameters. They are used for:
- Basic feature usage events (e.g., button clicks, screen impressions)
- Errors (e.g., network failures, parsing errors)
- Conversion and retention (e.g, subscription purchase and activation)
Pixels have the following requirements:
- Use clear & transparent naming, so it's obvious what the pixel and parameters are for. Pixel names should be self-documenting - avoid cryptic abbreviations or shorthand.
- Only include information that is essential for the pixel
- Do not use values that are overly precise, e.g. if using an integer value in a parameter, bucket it into ranges rather than including the value verbatim
- Never include PII, URLs, or other forms of user-identifiable information in pixel names or parameters
## Types of Pixels
### Standard Pixels
Sent every time the event occurs.
```swift
Pixel.fire(pixel: .subscriptionRestoreAfterPurchaseAttempt)
Pixel.fire(pixel: .autofillLoginsSavePromptDisplayed, withAdditionalParameters: [
PixelParameters.autofillPromptTrigger: "manual"
])
```
### Daily Pixels
Sent once per day per event. Used to determine the number of users affected by a particular error
```swift
DailyPixel.fireDailyAndCount(pixel: .subscriptionPurchaseAttempt, pixelNameSuffixes: DailyPixel.Constant.legacyDailyPixelSuffixes)
```
### Unique Pixels
Sent once per install for the lifetime of the install.
```swift
UniquePixel.fire(pixel: .subscriptionActivated)
```
## Pixel Definition Patterns
### iOS Pixels
iOS pixels are defined as cases on `Pixel.Event` in `iOS/Core/PixelEvent.swift`. Each enum case maps to an HTTP pixel name string via a computed `name` property.
#### Adding a New iOS Pixel
1. Add a new enum case to `Pixel.Event` in `iOS/Core/PixelEvent.swift`.
2. Add a corresponding case to the `name` computed property (also in `PixelEvent.swift`) that returns the pixel's string name.
3. Fire the pixel using `Pixel.fire`, `DailyPixel.fireDailyAndCount`, or `UniquePixel.fire`.
4. Add the matching pixel definition in `iOS/PixelDefinitions/pixels/definitions/*.json5` - see the `Pixel Validation` section below for more.
#### Enum Case Definition
```swift
extension Pixel {
public enum Event {
case appInstall
case appLaunch
case subscriptionPurchaseAttempt
case subscriptionPurchaseSuccess
case subscriptionActivated
// ...
}
}
```
#### Enum-to-String Mapping
The `Pixel.Event` enum has a computed `name` property that maps each case to its HTTP pixel name string:
```swift
extension Pixel.Event {
public var name: String {
switch self {
case .appInstall: return "m_install"
case .appLaunch: return "ml"
case .subscriptionPurchaseAttempt: return "m_subscribe"
// ...
}
}
}
```
#### Naming Convention
iOS pixel names follow these conventions:
- **Prefix**: Always start with `m_` (for "mobile").
- **Separators**: Use underscores (`_`) or hyphens (`-`) between words.
- **Format**: Use `m_feature_action` or `m_feature-sub-feature_action`.
- **Clarity**: Names should be clear and self-documenting. Anyone reading the pixel name should understand what it represents without needing additional context.
**Avoid legacy shorthand naming.** The codebase contains legacy pixels with cryptic names like `ml`, `mp`, `mf`, `m_r`. These are difficult to understand and should not be used as a template for new pixels. New pixels should use descriptive names.
Examples:
| Style | Enum Case | String Name | Notes |
|-------|-----------|-------------|-------|
| Good | `.pullToRefresh` | `"m_pull-to-reload"` | Clear and descriptive |
| Good | `.autofillLoginsSavePromptDisplayed` | `"m_autofill_logins_save_prompt_displayed"` | Self-documenting |
| Legacy | `.appLaunch` | `"ml"` | Avoid this style for new pixels |
| Legacy | `.privacyDashboardOpened` | `"mp"` | Avoid this style for new pixels |
#### Parameterized Pixel Cases
Enum cases can have associated values that are interpolated into the pixel name:
```swift
// Enum definition with associated value
case networkProtectionLatency(quality: String)
case syncLocalTimestampResolutionTriggered(Feature)
// In the name property
case .networkProtectionLatency(let quality):
return "m_netp_ev_\(quality)_latency"
case .syncLocalTimestampResolutionTriggered(let feature):
return "m_sync_\(feature.name)_local_timestamp_resolution_triggered"
```
### macOS Pixels (PixelKit)
macOS uses `PixelKitEvent` protocol, typically in feature-specific files. The implementation of the protocol looks like this:
```swift
// SharedPackages/BrowserServicesKit/Sources/PixelKit/PixelKitEvent.swift
public protocol PixelKitEvent {
var name: String { get }
var standardParameters: [PixelKitStandardParameter]? { get }
var parameters: [String: String]? { get }
var error: NSError? { get }
}
```
An example implementation of this protocol is:
```swift
// macOS/DuckDuckGo/Statistics/SubscriptionPixel.swift
enum SubscriptionPixel: PixelKitEvent {
case subscriptionActive(AuthVersion)
case subscriptionPurchaseAttempt
case subscriptionPurchaseSuccess
// ...
var name: String {
switch self {
case .subscriptionActive: return "m_mac_privacy-pro_app_subscription_active"
case .subscriptionPurchaseAttempt: return "m_mac_privacy-pro_terms-conditions_subscribe_click"
// ...
}
}
var parameters: [String: String]? {
switch self {
case .subscriptionActive(let authVersion):
return [AuthVersion.key: authVersion.rawValue]
default:
return nil
}
}
}
```
## Pixel Parameters
Use structured parameter keys when reusing them across multiple pixels:
```swift
extension PixelParameters {
static let source = "source"
}
Pixel.fire(pixel: .featureUsed, withAdditionalParameters: [
PixelParameters.source: "keyboard_shortcut"
])
```
## PixelFiring Protocol
For dependency injection and testing, use the `PixelFiring` protocol:
```swift
// iOS/Core/PixelFiring.swift
public protocol PixelFiring {
static func fire(_ pixel: Pixel.Event,
withAdditionalParameters params: [String: String],
includedParameters: [Pixel.QueryParameters],
onComplete: @escaping (Error?) -> Void)
static func fire(_ pixel: Pixel.Event,
withAdditionalParameters params: [String: String])
}
```
## Best Practices
1. **Choose the right pixel type**: When using a standard pixel, consider using a daily pixel as well in order to determine how many users may be impacted by an issue.
2. **Use structured parameters**: Define parameter keys as constants to avoid typos and enable refactoring.
3. **Include error context**: When firing error pixels, include the error object as part of its error parameters.
4. **Consider using EventMapping**: When sending pixels from inside a shared Swift package, define pixels as new enum and emit them using `EventMapping`, then implement the event mapper on the client side.
5. **Consider using instrumentation facades**: For features with multiple related pixels, consider defining an instrumentation protocol to centralize pixel firing logic. See `instrumentation-facade.mdc`.
## Pixel Validation
Pixel definitions are validated from the `PixelDefinitions` folders in each platform:
- `iOS/PixelDefinitions/`
- `macOS/PixelDefinitions/`
Validation runs via `npm run validate-pixel-defs` from the platform folder. JSON5 formatting is enforced with Prettier.
### Directory Layout
- `pixels/definitions/*.json5`: The actual pixel definitions. Each file contains multiple pixel entries.
- `pixels/params_dictionary.json5`: Shared parameter definitions referenced by key.
- `pixels/suffixes_dictionary.json5`: Shared suffix definitions referenced by key.
- `product.json`: Product metadata.
### Defining a Pixel in JSON5
Each JSON5 file defines a map of `pixel_name` to metadata. Use `TEMPLATE.json5` from each platform folder as a starting point. Example:
```json5
{
"m_feature_action": {
"description": "Describe when the pixel fires and its purpose",
"owners": ["github-username"],
"triggers": ["other"],
"suffixes": ["first_daily_count", "platform"],
"parameters": ["appVersion"],
"expires": "2025-01-30"
}
}
```
Key fields:
- `description`: Clear explanation of the pixels purpose and timing.
- `owners`: GitHub usernames responsible for the pixel.
- `triggers`: One or more trigger categories used by validation.
- `suffixes`: Either shared suffix keys from `suffixes_dictionary.json5` or inline suffix definitions.
- `parameters`: Either shared parameter keys from `params_dictionary.json5` or inline parameter definitions.
- `expires` (optional): Date for temporary pixels; omit for permanent pixels.
Prefer referencing shared suffix/parameter keys where possible to keep definitions consistent and validatable.
Pixels have some default values, please check the Pixel implementation in the respective platform to determine what those are.
## Related Files
- `iOS/Core/Pixel.swift` - iOS pixel firing implementation
- `iOS/Core/DailyPixel.swift` - Daily pixel implementation
- `iOS/Core/UniquePixel.swift` - Unique pixel implementation
- `iOS/Core/PixelEvent.swift` - iOS pixel event definitions
- `SharedPackages/BrowserServicesKit/Sources/PixelKit/` - Shared PixelKit implementation
@@ -0,0 +1,87 @@
---
source: ~/DuckDuckGo/apple-browsers.git/main/.cursor/rules/privacy-security.mdc
confidence: 0.9
namespace: work
last_synced: 2026-04-28
alwaysApply: true
---
# Privacy & Security Guidelines
## Core Principles
### Privacy by Design
- Never collect or transmit user data without explicit consent
- All features must have privacy implications documented
- Default to the most private option
- Implement data minimization - only collect what's absolutely necessary
### Secure Storage
**Example:** See [secure-storage.swift](privacy-security/secure-storage.swift)
## Data Handling
### User Data Classification
1. **Sensitive Data**: Passwords, credentials, personal information
- Must use Keychain or encrypted storage
- Never log or transmit in plain text
- Clear on app logout/uninstall
2. **Private Data**: Browsing history, bookmarks, settings
- Store locally only
- Implement proper data clearing
- Respect fireproofing settings
3. **Anonymous Data**: Crash reports, usage statistics
- Only collect with user consent
- Strip all identifying information
- Use differential privacy where applicable
### Network Security
**Example:** See [network-security.swift](privacy-security/network-security.swift)
## Content Blocking
### Tracker Protection
**Example:** See [tracker-protection.swift](privacy-security/tracker-protection.swift)
### Cookie Management
**Example:** See [cookie-management.swift](privacy-security/cookie-management.swift)
## Authentication & Authorization
### Biometric Authentication
**Example:** See [biometric-authentication.swift](privacy-security/biometric-authentication.swift)
### Credential Management
**Example:** See [credential-management.swift](privacy-security/credential-management.swift)
## Error Handling
### Secure Error Messages
**Example:** See [secure-error-messages.swift](privacy-security/secure-error-messages.swift)
## Code Security
### Input Validation
**Example:** See [input-validation.swift](privacy-security/input-validation.swift)
### Secure Defaults
**Example:** See [secure-defaults.swift](privacy-security/secure-defaults.swift)
## Testing Security
### Security Test Cases
**Example:** See [security-test-cases.swift](privacy-security/security-test-cases.swift)
## Review Checklist
Before committing code, ensure:
- [ ] No hardcoded secrets or API keys
- [ ] All user data is properly classified and protected
- [ ] Network requests use HTTPS
- [ ] Input validation is implemented
- [ ] Error messages don't leak sensitive information
- [ ] Logging doesn't include PII
- [ ] Data clearing mechanisms are tested
- [ ] Privacy impact has been assessed
@@ -0,0 +1,344 @@
---
source: ~/DuckDuckGo/apple-browsers.git/main/.cursor/rules/project-structure.mdc
confidence: 0.9
namespace: work
last_synced: 2026-04-28
alwaysApply: false
---
# Project Structure & Organization
## Workspace Structure
### Root Level
```
DuckDuckGo.xcworkspace/ # Main workspace (ALWAYS open this)
├── iOS/ # iOS app target
├── macOS/ # macOS app target
├── SharedPackages/ # Cross-platform Swift packages
├── fastlane/ # CI/CD automation
└── README.md # Main project documentation
```
### iOS App Structure
```
iOS/
├── DuckDuckGo/ # Main iOS app
│ ├── AppDelegate.swift # App lifecycle
│ ├── MainViewController.swift # Primary browser interface
│ ├── BrowserTab.swift # Tab state and WebKit integration
│ ├── AIChat/ # AI chat integration
│ ├── AppLifecycle/ # App lifecycle management
│ ├── Autofill/ # Form autofill features
│ ├── Bookmarks/ # Bookmark management
│ ├── BrowsingMenu/ # Browser menu UI
│ ├── Configuration/ # App configuration
│ ├── DataImport/ # Data import utilities
│ ├── HealthKitReporting/ # Health data reporting
│ ├── MainWindow/ # Main window controllers
│ ├── Subscription/ # Premium features
│ ├── SyncPrompt/ # Sync feature prompts
│ ├── TabSwitcher/ # Tab switching UI
│ └── WebView/ # Web view management
├── Core/ # iOS-specific shared utilities
├── AutofillCredentialProvider/ # Password autofill extension
├── PacketTunnelProvider/ # VPN network extension
├── OpenAction/ # Share sheet integration
├── Widgets/ # Home screen widgets
└── Configuration/ # Build configurations
```
### macOS App Structure
```
macOS/
├── DuckDuckGo/ # Main macOS app
│ ├── AppDelegate.swift # App lifecycle
│ ├── MainWindow.swift # Primary window controller
│ ├── BrowserTabViewController.swift # Web view management
│ ├── AIChat/ # AI chat integration
│ ├── Autofill/ # Form autofill features
│ ├── Bookmarks/ # Bookmark management UI
│ ├── Downloads/ # Download handling
│ ├── NavigationBar/ # URL bar and navigation
│ ├── NetworkProtection/ # VPN integration
│ ├── Preferences/ # Settings and preferences
│ ├── Subscription/ # Premium features
│ ├── SyncPrompt/ # Sync feature prompts
│ ├── TabBar/ # Tab management UI
│ └── WebView/ # Web view management
├── DuckDuckGoVPN/ # Standalone VPN app
├── NetworkProtectionSystemExtension/ # System-level VPN
├── DuckDuckGoDBPBackgroundAgent/ # Data Broker Protection
├── DuckDuckGoNotifications/ # System notifications
└── Configuration/ # Build configurations
```
## Dependencies and Packages
### Primary Dependency: BrowserServicesKit
```swift
// ✅ CORRECT - Always use BrowserServicesKit for shared functionality
import BrowserServicesKit
// Features provided by BrowserServicesKit:
// - Content blocking and privacy protection
// - Bookmarks and history management
// - Secure credential storage
// - Autofill functionality
// - Navigation handling
// - User script injection
// - Privacy configuration
// - Sync functionality
```
### Shared Packages
```
SharedPackages/
├── AIChat/ # AI chat functionality
├── BrowserServicesKit/ # Core browser services
├── DataBrokerProtectionCore/ # Data broker protection
├── DesignResourcesKitIcons/ # Shared icon resources
├── Onboarding/ # User onboarding experience
├── UIComponents/ # Reusable UI components
└── VPN/ # VPN functionality
```
### Package Dependencies
```swift
// ✅ CORRECT - Use shared packages for cross-platform features
import DesignResourcesKitIcons
import UIComponents
import BrowserServicesKit
// ❌ INCORRECT - Don't duplicate functionality across platforms
// Keep platform-specific code in iOS/ and macOS/ directories only
```
## Build Configuration
### Xcode Workspace Setup
```swift
// ✅ CORRECT - Always open workspace, not individual projects
// Open: DuckDuckGo.xcworkspace
// Don't open: iOS/DuckDuckGo-iOS.xcodeproj or macOS/DuckDuckGo-macOS.xcodeproj
```
### Build Requirements
```
iOS:
- Xcode 15.0 or later
- Swift 5.9 or later
- iOS 15.0+ deployment target
- Valid Apple Developer account
- Provisioning profiles for extensions
macOS:
- Xcode 15.0 or later
- Swift 5.9 or later
- macOS 11.4+ deployment target
- Developer ID certificate (for notarization)
- System extension entitlements
```
### Configuration Files
```
iOS/Configuration/
├── Configuration.xcconfig # Base configuration
├── Configuration-Alpha.xcconfig # Alpha build settings
├── Configuration-Debug.xcconfig # Debug build settings
└── BuildNumber.xcconfig # Build number management
macOS/Configuration/
├── Base.xcconfig # Base configuration
├── Debug.xcconfig # Debug build settings
├── Release.xcconfig # Release build settings
└── AppStore.xcconfig # App Store specific
```
## Development Setup
### Initial Setup
```bash
# ✅ CORRECT - Development setup steps
# 1. Open workspace at root level
open DuckDuckGo.xcworkspace
# 2. Install Ruby dependencies (for Fastlane)
bundle install
# 3. Ensure all certificates and provisioning profiles are installed
# 4. SwiftLint is enforced - run before committing
```
### Key Technologies
```swift
// iOS Stack
// - Language: Swift 5.9+
// - UI: UIKit with SwiftUI components
// - Architecture: MVVM with AppDependencyProvider
// - Web Engine: WebKit with privacy enhancements
// macOS Stack
// - Language: Swift 5.9+
// - UI: AppKit with SwiftUI components
// - Architecture: MVVM with Combine
// - Web Engine: WebKit with privacy enhancements
// - System Integration: Native macOS features
```
## App Extensions and System Integration
### iOS Extensions
```swift
// ✅ CORRECT - iOS extension organization
// AutofillCredentialProvider/ - Password autofill
// PacketTunnelProvider/ - VPN network extension
// OpenAction/ - Share sheet integration
// Widgets/ - Home screen widgets
```
### macOS Extensions
```swift
// ✅ CORRECT - macOS extension organization
// NetworkProtectionSystemExtension/ - System-level VPN
// DuckDuckGoDBPBackgroundAgent/ - Background data protection
// DuckDuckGoNotifications/ - System notifications
// VPNProxyExtension/ - VPN proxy functionality
```
## Testing Structure
### iOS Testing
```
iOS/
├── DuckDuckGoTests/ # Unit tests
├── IntegrationTests/ # Integration tests
├── PerformanceTests/ # Performance benchmarks
├── UITests/ # UI automation tests
├── SharedTestUtils/ # Shared test utilities
└── WebViewUnitTests/ # WebKit-specific tests
```
### macOS Testing
```
macOS/
├── UnitTests/ # Unit test suite
├── IntegrationTests/ # Integration testing
├── UITests/ # UI automation tests
└── SyncE2EUITests/ # End-to-end sync tests
```
## Important Files and Entry Points
### iOS Key Files
```swift
// Core Application Files
AppDelegate.swift # App lifecycle and initialization
MainViewController.swift # Primary browser interface
BrowserTab.swift # Tab state management
TabsBarViewController.swift # Tab bar UI (iPad)
BookmarksViewController.swift # Bookmarks management
PrivacyDashboardViewController.swift # Privacy protection UI
```
### macOS Key Files
```swift
// Core Application Files
AppDelegate.swift # App lifecycle management
MainWindow.swift # Primary window controller
BrowserTabViewController.swift # Web view management
NavigationBarViewController.swift # URL bar and controls
PreferencesViewController.swift # Settings interface
BookmarksBarViewController.swift # Bookmarks toolbar
```
## Common Development Tasks
### Running the Apps
```swift
// ✅ CORRECT - Running applications
// iOS: Select "iOS Browser" scheme and simulator/device
// macOS: Select "DuckDuckGo" scheme and click Run
```
### Adding New Features
```swift
// ✅ CORRECT - Feature development flow
// 1. Determine if feature belongs in:
// - BrowserServicesKit (shared functionality)
// - Platform-specific code (iOS/ or macOS/)
// 2. Use appropriate dependency injection patterns
// 3. Follow existing architecture patterns
// 4. Write comprehensive tests
```
### Debugging and Testing
```swift
// ✅ CORRECT - Testing and debugging
// - Run tests: Cmd+U or Test navigator
// - Use Safari Web Inspector for web content debugging
// - Enable verbose logging in Debug builds
// - Use Instruments for performance profiling
```
## Build Schemes and Configurations
### iOS Build Schemes
```
- DuckDuckGo (iOS) - Main app
- iOS Browser - Browser-specific build
- Alpha - Alpha testing build
- Debug - Development build
```
### macOS Build Schemes
```
- DuckDuckGo - Main app
- Debug - Development build
- Release - Release build
- Review - Review build
```
## Platform-Specific Considerations
### iOS-Specific Features
```swift
// iOS-specific implementations
// - iPad-specific UI adaptations
// - iPhone-specific layouts
// - iOS system integration
// - App Store requirements
```
### macOS-Specific Features
```swift
// macOS-specific implementations
// - Menu bar integration
// - Touch Bar support (MacBook Pro)
// - Dock integration
// - System extensions
// - Universal Binary (Intel + Apple Silicon)
// - Notarization requirements
```
## Development Best Practices
### Code Organization
```swift
// ✅ CORRECT - Follow established patterns
// - Use dependency injection via AppDependencyProvider
// - Keep platform-specific code in appropriate directories
// - Share common functionality through BrowserServicesKit
// - Follow MVVM architecture patterns
```
### Performance Considerations
```swift
// ✅ CORRECT - Performance optimization
// - Native Apple Silicon support
// - Efficient memory management
// - Hardware acceleration
// - Optimized content blocking
```
This structure ensures maintainable, testable code while providing comprehensive browser functionality across both iOS and macOS platforms.
+349
View File
@@ -0,0 +1,349 @@
---
source: ~/DuckDuckGo/apple-browsers.git/main/.cursor/rules/pull-request.mdc
confidence: 0.9
namespace: work
last_synced: 2026-04-28
description: "PR, pull request, commit, push, git push, gh pr create, open PR, create PR, reviewer, Asana task, merge, GitHub"
alwaysApply: false
---
# Pull Request Guidelines & Workflow
## 🚨 CRITICAL: Required Information and Approval Before Creating PR
**MANDATORY**: Before creating any PR, you MUST:
### Step 1: Gather Required Information
### 1. Task/Issue URL
**Ask**: "What is the Asana task URL for this PR?"
- **NEVER proceed** with placeholder text like `[TASK_ID]` or `[INSERT_URL]`
- **NEVER assume** you can skip this step
- **ONLY proceed** if user explicitly says to omit it or provides the URL
### 2. PR Reviewer Assignment (CRITICAL for Asana Integration)
**Ask**: "Who should review this PR?"
- Ask if they want to:
- Assign a specific reviewer (get their GitHub username for `--reviewer` flag)
- Use auto-assignment (`--reviewer Apple-dev` team)
- Handle it themselves after PR creation
- **NEVER proceed** without understanding the reviewer assignment strategy
- **ONLY proceed** if user explicitly provides the information or strategy
**WHY THIS MATTERS**:
- GitHub Action only creates Asana subtask when reviewer is **assigned via GitHub's reviewer mechanism**
- Using `--reviewer` flag triggers the `review_requested` event that runs the Asana integration
- Without reviewer assignment, no Asana subtask is created automatically
### 3. Tech Design URL (For Significant Changes)
- **Default to "N/A"** for minor changes and bug fixes
- **ASK for significant changes** (new features, architectural changes)
- **Can be omitted** if user doesn't explicitly provide one - use "N/A"
- Unlike Task/Issue URL, this is **optional** and can default to "N/A"
### 4. Exception: User Explicitly Opts Out
The **ONLY** acceptable reason to skip asking for Task URL and Reviewer is if the user explicitly states:
- "Skip Asana task" or "No Asana task"
- "I'll assign reviewer myself" or "Use auto-assignment"
**Failure to ask for Task URL and Reviewer = violation of PR workflow.**
---
### Step 2: Get User Approval Before Creating PR
**MANDATORY**: After gathering all information, you MUST:
1. **Present the complete PR body text** to the user for review and approval
2. **Include the reviewer name** that will be assigned
3. **Show the exact text** that will be used in the PR body (not the command)
4. **Wait for explicit approval** before proceeding
5. **ONLY after approval**: Execute the `gh pr create` command
**Do NOT create the PR without showing the user the exact PR body text first.**
**Format for approval request:**
```
Here's the PR I'm about to create:
**Title:** [PR title]
**Reviewer:** @username
**PR Body:**
[Show complete PR body text here]
Proceed with creating the PR?
```
After user approves, then execute the `gh pr create` command.
## 🚨 CRITICAL: Always Open PR URL After Creation
**MANDATORY**: After creating or updating a PR, **IMMEDIATELY** run:
```bash
open <PR_URL>
```
This ensures the PR is accessible and properly formatted in the browser.
## Objective
- **Maintain a clear and maintainable list** of open PRs in the Apple repositories
- **Improve PR review turnaround time** through proper assignment and notification processes
- **Establish clear rules** for internal (Apple team) and external (FrontEnd, etc.) contributions
- **Remove PR assignment** as part of the Apple Weekly process
## PR Types and Assignment Strategy
We have **two different types** of code contributions:
### **Projects**
Large features or significant changes with designated technical reviewers.
### **Tasks**
Small improvements or bug fixes that require flexible reviewer assignment.
**Key Principle**: A PR **assignee** is the PR author, a PR **reviewer** is whoever will review it.
## Assignment Workflows
### Projects Workflow
For significant features and planned work:
1. **Use Technical Reviewer**: The technical reviewer should be the default person to assign the PR review
2. **No MM Posting**: There's no need to post the PR link on MM (Mattermost)
3. **Review Assignment Process**:
- Create PR with: `gh pr create --reviewer TECHNICAL_REVIEWER_USERNAME`
- This automatically creates Asana subtask and assigns it to the reviewer
- No need to manually ping on Asana (automation handles it)
4. **Shared Responsibility**: Both the technical reviewer and developer are responsible for staying in sync
5. **Fallback**: If the technical reviewer can't review the PR, request different reviewer in GitHub UI (triggers new Asana assignment)
### Tasks Workflow
For bug fixes and small improvements:
1. **Pre-Agreement**: Think about who's the best person to review this task and **agree with them to be the reviewer even before posting the PR** (similar to choosing technical reviewer for projects)
2. **When Uncertain**: If you don't know who would be the best person, or the problem is generic and doesn't require domain knowledge, use **GitHub auto assignment** with `--reviewer Apple-dev`
3. **Assignment Process**:
- Create PR with: `gh pr create --reviewer USERNAME` (or `--reviewer Apple-dev` for auto)
- Asana subtask is automatically created and assigned
- No need to manually ping on Asana (automation notifies them)
- If reviewer is AFK, request different reviewer in GitHub UI (triggers new assignment)
4. **Availability Management**:
- Set your GitHub to "away" to prevent auto-selection if unavailable
- Use your best judgment for availability
5. **Reviewer Flexibility**: If assigned as reviewer but can't review or don't feel comfortable with the area, discuss reassignment with the PR author
## Auto Review Assignment
**Algorithm**: Load balance routing to equally distribute review work
**Process**:
1. Use `gh pr create --reviewer Apple-dev` OR manually select the **"Apple-dev" team** as reviewer in GitHub UI
2. GitHub will automatically assign an individual based on load balancing
3. Asana workflow automatically creates subtask and assigns to the selected reviewer's Asana account
### Assignment on Asana
**AUTOMATED**: When you assign a reviewer on GitHub (via `--reviewer` or UI), the workflow automatically:
- Extracts Asana task ID from PR body
- Creates a "Code Review" subtask in that Asana task
- Assigns the subtask to the GitHub reviewer's Asana account
**Manual steps (if needed):**
- If automation fails or reviewer doesn't match, manually create subtask in Asana
- **Reviewer completes** the code review subtask once review is finished
- **Communication**: Use best judgment to contact PR author via Asana, MM, or PR comments for review feedback
## Draft PRs
**Purpose**: Share in-progress work for early feedback
**Guidelines**:
- Use Draft PRs for work-in-progress sharing
- **Your responsibility**: Don't let drafts stay around for long periods
- **No rigid timeframes**: Use best judgment on when to close drafts
- **Goal**: Keep open PR list as clean as possible
## PR Labels
Use pre-defined labels to classify PR intention/state:
### Current Available Labels
- **`[Hacktoberfest]` & `[hacktoberfest-accepted]`**: For PRs related to Hacktoberfest event
- **`[Pending Product Review]`**: PR is being reviewed in Ship Reviews - **NEVER merge** if this tag is present
- **`[dependencies]`**: Automatically used by Dependabot
**Adding New Labels**: Discuss with the team before creating new labels
## Auto-Merge on Approval
**Feature**: Automatically merge PR after review approval
**Setup Process**:
1. Set PR to auto-merge using GitHub's built-in functionality
2. No specific labels required
3. **Documentation**: [GitHub Auto-merge Guide](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/incorporating-changes-from-a-pull-request/automatically-merging-a-pull-request)
**Requirement**: At least one green review is required due to branch protection
## Branch Protection
**Requirement**: **At least one green review** is required to merge any PR
## Feature Flags & PR Size Guidelines
### PR Size Best Practices
- **Keep PRs as short as possible** for efficiency and respectful time management
- **Use feature flags** (static or dynamic) so changes can be merged without affecting the final product
- **Smaller PRs = better feedback**: More likely to receive constructive review comments
### When Uncertain
- **Talk with technical reviewer** and/or project advisor about breaking down PRs
- **Use feature flags** to enable gradual rollout and safe merging
## Pull Request Template
**MANDATORY**: When creating Pull Requests, ALWAYS follow this template structure:
```markdown
Task/Issue URL: [MUST ASK USER - Do not proceed with placeholder]
Tech Design URL: [ASK USER for significant changes; can default to N/A if not provided]
CC: [ASK USER for stakeholders; can default to N/A if not provided]
### Description
[Provide a clear and concise description of the changes as a bulleted list. List changes in order of significance - most impactful/critical changes first, implementation details last. Be brief and omit small changes that are not directly related to the core issue being fixed. Format as bullet points without subsection titles]
### Testing Steps
[List detailed manual testing steps only. Do not include "run tests" or similar - CI runs automated tests. Focus on manual verification steps that require human interaction]
### Impact and Risks
**Impact Level: [Assess as High, Medium, Low, or None]**
#### What could go wrong?
[List potential risks and mitigation strategies]
### Quality Considerations
[Include relevant considerations for edge cases, performance, monitoring, documentation, and privacy/security]
### Notes to Reviewer
[Include any specific notes for the reviewer, if applicable]
```
### Template Guidelines
#### Required Information
- **Task/Issue URL**: **MUST ASK USER** - Always obtain the actual Asana task URL, never use placeholders
- **Tech Design URL**: **ASK USER** for significant changes; can be omitted and default to "N/A" if not explicitly provided
- **CC**: **ASK USER** for relevant stakeholders; can default to "N/A" if not explicitly provided
- **Description**: Clear, concise bulleted list of changes in order of significance - most critical/impactful changes first, implementation details last. Be brief and omit small changes not directly related to the core issue. No subsection titles
- **Testing Steps**: Manual testing steps only (CI runs automated tests). Focus on human verification steps
- **Impact Assessment**: Use guidelines below
- **Risk Analysis**: Potential issues and mitigation strategies
- **Quality Considerations**: Edge cases, performance, monitoring, documentation, privacy/security
#### Impact Level Assessment
- **High**: Changes affecting user privacy/security, data loss potential, core functionality breaks, billing impacts, significant performance effects
- **Medium**: Feature disruption, user flow changes, significant UI changes, analytics/tracking impacts
- **Low**: Minor bug fixes, small UI adjustments, existing feature improvements, non-critical feature additions
- **None**: Internal tooling, documentation, refactoring without behavior changes, test improvements
#### Quality Considerations Checklist
- **Edge cases** that have been considered
- **Performance impacts** and optimizations made
- **Monitoring and analytics** additions or changes
- **Documentation updates** required
- **Privacy and security** considerations, if applicable
## PR Creation Workflow
**CRITICAL**: After creating or updating a PR, **ALWAYS open the PR URL** in the browser immediately.
### Steps:
1. **Create PR with reviewer assignment**:
```bash
# For specific reviewer
gh pr create --reviewer USERNAME
# For auto-assignment (Apple-dev team)
gh pr create --reviewer Apple-dev
```
2. **Immediately run**: `open <PR_URL>` (the URL returned by gh command)
3. Verify the PR appears correctly in the browser
### ⚠️ CRITICAL: Reviewer Assignment Requirement
**The Asana integration ONLY works when reviewers are assigned via GitHub's reviewer mechanism.**
**How it works:**
- GitHub Action `.github/workflows/create_asana_pr_subtask.yml` triggers on `review_requested` event
- Extracts Asana task ID from PR body (looks for `Task/Issue URL: https://app.asana.com/...`)
- Creates subtask in Asana and assigns it to the GitHub reviewer
**This means:**
- ✅ **CORRECT**: `gh pr create --reviewer USERNAME` (triggers Asana assignment)
- ✅ **CORRECT**: Manually request reviewer in GitHub UI (triggers Asana assignment)
- ❌ **WRONG**: Only mentioning reviewer in PR description (does NOT trigger Asana assignment)
- ❌ **WRONG**: Creating PR without `--reviewer` flag (does NOT trigger Asana assignment)
**If you forget to assign reviewer during creation:**
1. Request reviewer manually in GitHub UI
2. This will trigger the workflow and create the Asana subtask
## Review Process Best Practices
### For PR Authors
1. **Pre-review checklist**: Ensure all template sections are complete
2. **Self-review**: Review your own changes before requesting review
3. **Context**: Provide sufficient context for reviewers
4. **Responsive**: Address review comments promptly
5. **Asana updates**: Keep related Asana tasks updated
### For PR Reviewers
1. **Timely reviews**: Prioritize PR reviews to maintain good turnaround time
2. **Thorough but efficient**: Balance thoroughness with review speed
3. **Constructive feedback**: Provide actionable suggestions
4. **Asana completion**: Mark code review subtasks as complete
5. **Communication**: Use appropriate channels (Asana, MM, PR comments) for feedback
## Workflow Summary
### For Projects:
1. Technical reviewer assigned by default
2. Ready for review → **Use `gh pr create --reviewer USERNAME`** → Asana subtask auto-created
3. No MM posting required (Asana handles it)
4. **Open PR URL in browser**
### For Tasks:
1. Pre-agree on reviewer OR use auto-assignment
2. **Use `gh pr create --reviewer USERNAME`** or `--reviewer Apple-dev` for auto-assignment
3. Asana subtask is automatically created and assigned to reviewer
4. Handle AFK reviewers by requesting different reviewer in GitHub (triggers new Asana assignment)
5. **Open PR URL in browser**
### For All PRs:
1. **CRITICAL**: Use `--reviewer` flag when creating PR (enables Asana integration)
2. Include valid Asana task URL in PR body (required for automation)
3. Use feature flags for safe merging
4. Keep PRs small and focused
5. Apply appropriate labels
6. Set auto-merge if desired
7. Follow template requirements
8. Maintain clean draft PR list
9. **ALWAYS open PR URL after creation/update**
---
**Goal**: Efficient, clear, and maintainable PR workflows that respect everyone's time while maintaining code quality.
@@ -0,0 +1,548 @@
---
source: ~/DuckDuckGo/apple-browsers.git/main/.cursor/rules/securevault-guidelines.mdc
confidence: 0.9
namespace: work
last_synced: 2026-04-28
alwaysApply: false
---
# SecureVault Implementation Guidelines
## Introduction
These guidelines encapsulate vital information for using SecureVault in our application. SecureVault is our secure storage system built on GRDB with SQLCipher encryption, providing a layered security approach for sensitive data.
For general GRDB knowledge, refer to [GRDB documentation](https://github.com/groue/GRDB.swift). This guide covers DuckDuckGo-specific patterns and requirements.
## Essential Knowledge
### SecureVault Architecture Overview
SecureVault is a protocol-based system that provides:
- **L0**: Not encrypted (currently unused)
- **L1**: Secret key encrypted (usernames, domains, duck addresses)
- **L2**: User password encrypted with time-based access (user passwords)
- **L3**: User password required at request time (future: credit cards, sensitive data)
```swift
// Core SecureVault protocol pattern
public protocol SecureVault {
associatedtype DatabaseProvider: SecureStorageDatabaseProvider
init(providers: SecureStorageProviders<DatabaseProvider>)
}
// Example: AutofillSecureVault implementation
public protocol AutofillSecureVault: SecureVault {
func storeWebsiteCredentials(_ credentials: SecureVaultModels.WebsiteCredentials) throws -> Int64
func websiteCredentialsFor(domain: String) throws -> [SecureVaultModels.WebsiteCredentials]
// ... other autofill-specific methods
}
```
## Database Location and Setup
### Choosing the Database Location
**Important Note**: If you need to share the Database through AppGroup, please refer to [Sharing Database through AppGroup](#sharing-database-through-appgroup) section below.
For typical single-app usage:
- Create a dedicated folder for the database file
- This helps separate it from other files and makes file coordination easier
- Makes future migration simpler if needed
```swift
// Example database location setup
let databaseDirectory = FileManager.default
.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0]
.appendingPathComponent("SecureVault")
try FileManager.default.createDirectory(at: databaseDirectory,
withIntermediateDirectories: true)
let databaseURL = databaseDirectory.appendingPathComponent("vault.sqlite")
```
### Database Configuration
The simplest and best (future-proof) way to setup database is to use the following configuration:
```swift
var config = Configuration()
config.prepareDatabase { database in
try database.usePassphrase(key)
try database.execute(sql: "PRAGMA cipher_plaintext_header_size = 32")
}
```
**Why the PRAGMA statement?**
- This PRAGMA enables sharing databases across App Groups
- It has no negative impact when used for single-app scenarios
- Setting it upfront makes future AppGroup sharing easier
- For details, see [iOS] Requirements to store Vault in AppGroup Container
### Choosing DatabaseWriter
GRDB operates with two possible writers:
#### DatabaseQueue
- **Single writer, single reader**
- Simpler configuration
- Suitable for single-threaded access
#### DatabasePool
- **Single writer, multiple readers**
- Enables WAL mode on SQLite file
- Better performance for multi-threaded access
```swift
// DatabaseQueue example
let queue = try DatabaseQueue(path: databaseURL.path, configuration: config)
// DatabasePool example
let pool = try DatabasePool(path: databaseURL.path, configuration: config)
```
**Decision criteria:**
- Single thread access (e.g., UserScripts only): Either option works
- Multi-threaded access: Use `DatabasePool` for better performance
## SecureVault Instance Management
### ✅ CORRECT: Single Instance Pattern
> GRDB documentation is extremely clear on the topic:
> "Open one single DatabaseQueue or DatabasePool per database file, for the whole duration of your use of the database. Not for the duration of each database access, but really for the duration of all database accesses to this file."
**Follow DuckDuckGo patterns**: Combine this with [✓ Approved] Avoiding static vars and singletons:
```swift
// ✅ CORRECT: Factory pattern with dependency injection
public let AutofillSecureVaultFactory: AutofillVaultFactory = SecureVaultFactory<DefaultAutofillSecureVault>(
makeCryptoProvider: {
return AutofillCryptoProvider()
},
makeKeyStoreProvider: { reporter in
return AutofillKeyStoreProvider(reporter: reporter)
},
makeDatabaseProvider: { key, _ in
return try DefaultAutofillDatabaseProvider(key: key)
}
)
// Usage in ViewModels via dependency injection
final class MyFeatureViewModel: ObservableObject {
private let vault: any AutofillSecureVault
init(dependencies: DependencyProvider = AppDependencyProvider.shared) {
self.vault = dependencies.autofillSecureVault
}
}
```
### ❌ AVOID: Multiple Instances
```swift
// ❌ DON'T DO THIS - creates multiple instances
func someMethod() {
let vault1 = try AutofillSecureVaultFactory.makeVault(reporter: nil)
// ... later in another method
let vault2 = try AutofillSecureVaultFactory.makeVault(reporter: nil)
// This can cause database corruption and performance issues
}
```
## Sharing Database through AppGroup
Since we've started sharing SecureVault (GRDB) in AppGroup with the intention of using system-wide extensions to access it, there are important considerations and requirements.
### Side Effects and Challenges
When sharing across App Groups, you must handle:
1. **File-access constraints**: Multiple processes accessing simultaneously
2. **System file lock constraints**: OS monitors SQLite access locks
3. **WAL mode performance**: Proper setup for read/write performance
4. **Persistent WAL**: Correct read-only access between processes
### Critical Setup Requirements
#### 1. Database Configuration for AppGroup
```swift
var config = Configuration()
config.prepareDatabase { database in
try database.usePassphrase(key)
// MANDATORY for AppGroup sharing
try database.execute(sql: "PRAGMA cipher_plaintext_header_size = 32")
// Additional AppGroup optimizations
try database.execute(sql: "PRAGMA wal_checkpoint = TRUNCATE")
}
// Use DatabasePool for AppGroup scenarios
let pool = try DatabasePool(path: sharedDatabaseURL.path, configuration: config)
```
#### 2. Background Task Management
**Critical**: Extend host app lifecycle to prevent 0xdead10cc crashes:
```swift
// ✅ REQUIRED: Use BackgroundTask during database operations
func performDatabaseOperation() async {
let backgroundTask = await UIApplication.shared.beginBackgroundTask(withName: "SecureVault Operation") {
// Cleanup if needed
}
defer {
if backgroundTask != .invalid {
UIApplication.shared.endBackgroundTask(backgroundTask)
}
}
// Perform your database operations here
try await vault.performOperation()
}
```
#### 3. Using ExpiringActivity (Alternative)
For more sophisticated background management:
```swift
import ActivityKit
func performDatabaseOperationWithActivity() async {
let activity = try? Activity<DatabaseActivityAttributes>.request(
attributes: DatabaseActivityAttributes(),
content: .init(state: .active),
pushToken: nil
)
defer {
Task {
await activity?.end()
}
}
// Database operations
try await vault.performOperation()
}
```
### Crash Prevention: 0xdead10cc
**Problem**: App crashes with `Termination Reason: RUNNINGBOARD 0xdead10cc` when:
- App transitions from Background to Suspended state
- Database locks are still held
- Extensions need database access
**Solution**: Always use background tasks or expiring activities during database operations in AppGroup scenarios.
## Performance Optimization
### WAL Mode Optimization
```swift
// Optimize WAL checkpoints for AppGroup usage
config.prepareDatabase { database in
try database.usePassphrase(key)
try database.execute(sql: "PRAGMA cipher_plaintext_header_size = 32")
// Performance optimizations
try database.execute(sql: "PRAGMA journal_mode = WAL")
try database.execute(sql: "PRAGMA synchronous = NORMAL")
try database.execute(sql: "PRAGMA cache_size = -16384") // 16MB cache
try database.execute(sql: "PRAGMA temp_store = MEMORY")
}
```
### Efficient Database Operations
```swift
// ✅ GOOD: Batch operations
try vault.inDatabaseTransaction { database in
for credential in credentials {
try vault.storeWebsiteCredentials(credential, in: database)
}
}
// ❌ AVOID: Individual transactions
for credential in credentials {
try vault.storeWebsiteCredentials(credential) // Creates separate transaction each time
}
```
## Encryption and Security
### Multi-Layer Encryption Implementation
```swift
public class DefaultAutofillSecureVault<T: AutofillDatabaseProvider>: AutofillSecureVault {
// L1: Secret key encryption (stored in Keychain)
private func l1Encrypt(data: Data) throws -> Data {
let l1Key = try providers.keystore.l1Key()
return try providers.crypto.encrypt(data, withKey: l1Key)
}
// L2: User password encryption (with expiring access)
private func l2Encrypt(data: Data, using l2Key: Data? = nil) throws -> Data {
let key: Data = try {
if let l2Key {
return l2Key
}
let password = try passwordInUse()
return try l2KeyFrom(password: password)
}()
return try providers.crypto.encrypt(data, withKey: key)
}
// Password-based access with expiration
public func authWith(password: Data) throws -> any AutofillSecureVault {
lock.lock()
defer { lock.unlock() }
do {
_ = try self.l2KeyFrom(password: password) // Validates password
self.expiringPassword.value = password
return self
} catch {
let error = error as? SecureStorageError ?? .authError(cause: error)
throw error
}
}
}
```
### Password Management
```swift
// Secure password reset
public func resetL2Password(oldPassword: Data?, newPassword: Data) throws {
lock.lock()
defer { lock.unlock() }
// Force re-auth on future calls
self.expiringPassword.value = nil
do {
// Use provided old password or stored generated password
let generatedPassword = try self.providers.keystore.generatedPassword()
guard let oldPassword = oldPassword ?? generatedPassword else {
throw SecureStorageError.invalidPassword
}
// Get decrypted L2 key using old password
let l2Key = try self.l2KeyFrom(password: oldPassword)
// Derive new encryption key
let newEncryptionKey = try self.providers.crypto.deriveKeyFromPassword(newPassword)
// Encrypt L2 key with new encryption key
let encryptedKey = try self.providers.crypto.encrypt(l2Key, withKey: newEncryptionKey)
// Store encrypted L2 key
try self.providers.keystore.storeEncryptedL2Key(encryptedKey)
// Clear generated password
try self.providers.keystore.clearGeneratedPassword()
} catch {
if let error = error as? SecureStorageError {
throw error
} else {
throw SecureStorageError.databaseError(cause: error)
}
}
}
```
## Testing Patterns
### Mock SecureVault Implementation
```swift
// ✅ FOLLOW: Use existing mock patterns
typealias MockVaultFactory = SecureVaultFactory<MockSecureVault<MockDatabaseProvider>>
let MockSecureVaultFactory = SecureVaultFactory<MockSecureVault>(
makeCryptoProvider: {
let provider = MockCryptoProvider()
provider._derivedKey = "derived".data(using: .utf8)
return provider
},
makeKeyStoreProvider: { _ in
let provider = MockKeyStoreProvider()
provider._l1Key = "key".data(using: .utf8)
return provider
},
makeDatabaseProvider: { key, _ in
return try MockDatabaseProvider(key: key)
}
)
// Usage in tests
final class MockSecureVault<T: AutofillDatabaseProvider>: AutofillSecureVault {
var storedAccounts: [SecureVaultModels.WebsiteAccount] = []
var storedCredentials: [Int64: SecureVaultModels.WebsiteCredentials] = [:]
// Simplified implementations for testing
func encrypt(_ data: Data, using key: Data) throws -> Data { data }
func decrypt(_ data: Data, using key: Data) throws -> Data { data }
// Test-specific storage
func storeWebsiteCredentials(_ credentials: SecureVaultModels.WebsiteCredentials) throws -> Int64 {
let id = Int64(storedCredentials.count + 1)
storedCredentials[id] = credentials
if let account = credentials.account {
storedAccounts.append(account)
}
return id
}
}
```
### Testing Database Operations
```swift
func testSecureVaultStorage() async throws {
let vault = try MockSecureVaultFactory.makeVault(reporter: nil)
let account = SecureVaultModels.WebsiteAccount(
title: "Test Site",
username: "testuser",
domain: "example.com"
)
let credentials = SecureVaultModels.WebsiteCredentials(
account: account,
password: "testpassword".data(using: .utf8)
)
// Test storage
let storedId = try vault.storeWebsiteCredentials(credentials)
XCTAssertGreaterThan(storedId, 0)
// Test retrieval
let retrievedCredentials = try vault.websiteCredentialsFor(domain: "example.com")
XCTAssertEqual(retrievedCredentials.count, 1)
XCTAssertEqual(retrievedCredentials.first?.account?.username, "testuser")
}
```
## Updating GRDB
Instead of using GRDB directly as source code, we package it into an XCFramework to speed up compilation time.
### Update Process
1. **Check the fork**: Go to our [GRDB fork](https://github.com/duckduckgo/GRDB.swift)
2. **Follow instructions**: Run the provided script to create a new release
3. **If script fails**:
- The script patches GRDB code that may change between releases
- Create a task in **Apple Developer Infrastructure (CI, Releases, DevEx)**
- Request script fixes and new release creation
### GRDB Version Considerations
```swift
// Check current GRDB version compatibility
import GRDB
// Ensure new features/APIs are available
#if compiler(>=5.9) && canImport(GRDB, _version: 6.0)
// Use newer GRDB features
#else
// Fallback to older patterns
#endif
```
## Error Handling Patterns
### SecureVault-Specific Errors
```swift
// Handle authentication errors gracefully
func handleSecureVaultOperation() async {
do {
let data = try await vault.getSensitiveData()
processData(data)
} catch SecureStorageError.authRequired {
// Prompt user for password
await requestUserAuthentication()
} catch SecureStorageError.invalidPassword {
// Show password error message
showPasswordError()
} catch SecureStorageError.databaseError(let cause) {
// Handle database-specific errors
Logger.secureStorage.error("Database error: \(cause)")
showGenericError()
} catch {
// Handle other errors
Logger.secureStorage.error("Unexpected error: \(error)")
showGenericError()
}
}
```
### Background Access Error Handling
```swift
// Handle AppGroup background access gracefully
func performBackgroundVaultOperation() async {
let backgroundTask = await UIApplication.shared.beginBackgroundTask(withName: "Vault Access")
defer {
if backgroundTask != .invalid {
UIApplication.shared.endBackgroundTask(backgroundTask)
}
}
do {
try await vault.performOperation()
} catch {
// If we're in background and get access errors, defer operation
if UIApplication.shared.applicationState == .background {
queueOperationForForeground()
} else {
throw error
}
}
}
```
## Best Practices Summary
### ✅ DO
- Use the factory pattern with dependency injection
- Set up database configuration with AppGroup PRAGMA upfront
- Use background tasks for AppGroup database operations
- Implement proper error handling for authentication states
- Follow the single instance pattern for database writers
- Use DatabasePool for multi-threaded access
- Batch database operations when possible
- Use existing mock patterns for testing
### ❌ DON'T
- Create multiple SecureVault instances for the same database
- Perform database operations without background tasks in AppGroup scenarios
- Ignore authentication required errors
- Use force unwrapping with SecureVault operations
- Mix DatabaseQueue and DatabasePool for the same database file
- Perform individual transactions for batch operations
- Skip error handling for background access scenarios
### 🔒 Security Reminders
- Never log decrypted password data
- Always validate user passwords before storing
- Use appropriate encryption layers (L1/L2/L3) for data sensitivity
- Implement proper cleanup for background tasks
- Handle device lock scenarios gracefully
- Clear sensitive data from memory when appropriate
---
This documentation ensures secure, performant, and maintainable SecureVault implementation across the DuckDuckGo browser ecosystem.
@@ -0,0 +1,362 @@
---
source: ~/DuckDuckGo/apple-browsers.git/main/.cursor/rules/shared-packages.mdc
confidence: 0.9
namespace: work
last_synced: 2026-04-28
alwaysApply: false
---
# Shared Packages Development Guidelines
## Package Structure
### Standard Package Layout
```
SharedPackages/
├── FeatureName/
│ ├── Package.swift
│ ├── README.md
│ ├── Sources/
│ │ └── FeatureName/
│ │ ├── Public/ # Public API
│ │ ├── Internal/ # Internal implementation
│ │ └── Resources/ # Assets and resources
│ └── Tests/
│ └── FeatureNameTests/
│ └── FeatureTests.swift
```
### Package.swift Configuration
```swift
// swift-tools-version: 5.7
import PackageDescription
let package = Package(
name: "FeatureName",
platforms: [
.iOS(.v15),
.macOS(.v12)
],
products: [
.library(
name: "FeatureName",
targets: ["FeatureName"]
)
],
dependencies: [
// Only include truly necessary dependencies
.package(url: "https://github.com/DuckDuckGo/BrowserServicesKit", from: "1.0.0")
],
targets: [
.target(
name: "FeatureName",
dependencies: ["BrowserServicesKit"],
resources: [
.process("Resources")
]
),
.testTarget(
name: "FeatureNameTests",
dependencies: ["FeatureName"]
)
]
)
```
## Cross-Platform Compatibility
### Platform-Specific Code
```swift
#if os(iOS)
import UIKit
public typealias PlatformView = UIView
public typealias PlatformViewController = UIViewController
public typealias PlatformColor = UIColor
#elseif os(macOS)
import AppKit
public typealias PlatformView = NSView
public typealias PlatformViewController = NSViewController
public typealias PlatformColor = NSColor
#endif
// Use platform-agnostic types
public protocol CrossPlatformViewProtocol {
var backgroundColor: PlatformColor? { get set }
}
```
### Conditional Compilation
```swift
public class FeatureManager {
public func performAction() {
#if os(iOS)
performIOSAction()
#elseif os(macOS)
performMacOSAction()
#endif
}
#if os(iOS)
private func performIOSAction() {
// iOS-specific implementation
}
#endif
#if os(macOS)
private func performMacOSAction() {
// macOS-specific implementation
}
#endif
}
```
## API Design
### Public API Guidelines
```swift
// Mark public APIs clearly
public protocol FeatureServiceProtocol {
func fetchData() async throws -> [Item]
}
public final class FeatureService: FeatureServiceProtocol {
// Use dependency injection
private let networkClient: NetworkClientProtocol
public init(networkClient: NetworkClientProtocol) {
self.networkClient = networkClient
}
public func fetchData() async throws -> [Item] {
// Implementation
}
}
```
### Internal Implementation
```swift
// Keep implementation details internal
internal final class FeatureImplementation {
// Not exposed to package consumers
}
// Use extensions for internal helpers
internal extension String {
var sanitized: String {
// Internal helper method
}
}
```
## Resource Management
### Bundled Resources
```swift
public enum FeatureResources {
private static let bundle = Bundle.module
public static var configuration: Data {
guard let url = bundle.url(forResource: "config", withExtension: "json"),
let data = try? Data(contentsOf: url) else {
fatalError("Missing required resource: config.json")
}
return data
}
public static func image(named name: String) -> PlatformImage? {
#if os(iOS)
return UIImage(named: name, in: bundle, with: nil)
#elseif os(macOS)
return bundle.image(forResource: name)
#endif
}
}
```
## Dependency Management
### Minimal Dependencies
```swift
// Avoid unnecessary dependencies
// Bad: Importing entire framework for one function
import HeavyFramework
// Good: Implement minimal version or use protocol
protocol DateFormatterProtocol {
func string(from date: Date) -> String
}
```
### Version Management
```swift
// Use semantic versioning
// Package.swift
dependencies: [
.package(url: "https://github.com/example/package",
from: "1.0.0"), // Allows 1.x.x
.package(url: "https://github.com/example/strict",
exact: "2.1.0"), // Exact version
.package(url: "https://github.com/example/range",
"1.0.0"..<"2.0.0") // Version range
]
```
## Testing Shared Packages
### Cross-Platform Tests
```swift
import XCTest
@testable import FeatureName
final class FeatureTests: XCTestCase {
func testCrossPlatformBehavior() {
let feature = Feature()
#if os(iOS)
XCTAssertNotNil(feature.iosSpecificProperty)
#elseif os(macOS)
XCTAssertNotNil(feature.macOSSpecificProperty)
#endif
// Test common behavior
XCTAssertEqual(feature.commonProperty, expectedValue)
}
}
```
### Test Utilities
```swift
// Provide test utilities in a separate target
public extension XCTestCase {
func waitForCondition(
_ condition: @autoclosure () -> Bool,
timeout: TimeInterval = 1.0,
message: String = "Condition not met"
) {
let expectation = expectation(description: message)
Task {
while !condition() {
try? await Task.sleep(nanoseconds: 100_000_000) // 0.1s
}
expectation.fulfill()
}
wait(for: [expectation], timeout: timeout)
}
}
```
## Documentation
### Package Documentation
```swift
/// A service for managing user preferences across platforms.
///
/// This service provides a unified interface for storing and retrieving
/// user preferences, with platform-specific implementations for iOS and macOS.
///
/// ## Usage Example
/// ```swift
/// let preferences = UserPreferencesService()
/// preferences.set("value", for: .theme)
/// let theme = preferences.get(.theme)
/// ```
public final class UserPreferencesService {
/// Initializes a new preferences service.
///
/// - Parameter storage: The storage backend to use. Defaults to UserDefaults.
public init(storage: PreferencesStorage = .userDefaults) {
// Implementation
}
}
```
## Migration and Versioning
### API Evolution
```swift
public protocol FeatureProtocolV2 {
// New required method
func newRequiredMethod()
// Existing method
func existingMethod()
}
// Provide default implementation for backward compatibility
public extension FeatureProtocolV2 {
func newRequiredMethod() {
// Default implementation
}
}
// Deprecation
@available(*, deprecated, renamed: "newMethod()")
public func oldMethod() {
newMethod()
}
```
### Breaking Changes
```swift
// Use versioned types when making breaking changes
public struct ConfigurationV1 {
public let setting: String
}
public struct ConfigurationV2 {
public let setting: String
public let newRequired: Bool
// Provide migration
public init(from v1: ConfigurationV1) {
self.setting = v1.setting
self.newRequired = false // Default value
}
}
```
## Performance Considerations
### Lazy Loading
```swift
public final class ResourceManager {
// Lazy load expensive resources
private lazy var heavyResource: HeavyResource = {
return HeavyResource()
}()
// Use computed properties for lightweight calculations
public var lightweightValue: String {
return "Calculated on demand"
}
}
```
### Memory Management
```swift
public final class CacheManager {
private let cache = NSCache<NSString, CacheItem>()
public init() {
// Configure cache limits
cache.countLimit = 100
cache.totalCostLimit = 50 * 1024 * 1024 // 50MB
// Respond to memory warnings
#if os(iOS)
NotificationCenter.default.addObserver(
self,
selector: #selector(clearCache),
name: UIApplication.didReceiveMemoryWarningNotification,
object: nil
)
#endif
}
@objc private func clearCache() {
cache.removeAllObjects()
}
}
```
@@ -0,0 +1,376 @@
---
source: ~/DuckDuckGo/apple-browsers.git/main/.cursor/rules/subscription-architecture.mdc
confidence: 0.9
namespace: work
last_synced: 2026-04-28
alwaysApply: false
---
# Subscription Architecture & Implementation
## Overview
DuckDuckGo's subscription system provides access to premium features including VPN (Network Protection), Personal Information Removal (PIR), Identity Theft Restoration (ITR), and AI Chat. The system supports multiple purchase platforms and cross-platform activation.
## Core Architecture
### Shared Foundation: BrowserServicesKit
All subscription logic is centralized in `BrowserServicesKit/Sources/Subscription/`:
```swift
// ✅ CORRECT - Use BrowserServicesKit for core subscription logic
import BrowserServicesKit
final class SubscriptionViewModel: ObservableObject {
private let subscriptionManager: SubscriptionManager
init(subscriptionManager: SubscriptionManager = SubscriptionManager.shared) {
self.subscriptionManager = subscriptionManager
}
}
// ❌ INCORRECT - Don't duplicate subscription logic in platform code
final class SubscriptionViewModel: ObservableObject {
func checkSubscriptionStatus() {
// Don't reimplement subscription logic
}
}
```
### Platform-Specific Purchase Methods
#### iOS
- **Purchase Method**: App Store only (StoreKit)
- **Geographic Coverage**: Global
- **Cross-Platform**: Can activate Stripe subscriptions from other platforms
#### macOS App Store Build
- **Purchase Method**: App Store only (StoreKit)
- **Geographic Coverage**: Global
- **Cross-Platform**: Can activate Stripe subscriptions
#### macOS Direct Download Build
- **US Users**: Stripe web purchases
- **Non-US Users**: Redirected to iOS App Store
- **Cross-Platform**: Primary platform for Stripe purchases
### Version Management
ALWAYS use V2 implementations for new code:
```swift
// ✅ CORRECT - Use V2 implementations
let subscriptionManager = SubscriptionManagerV2()
let purchaseManager = StorePurchaseManagerV2()
let purchaseFlow = AppStorePurchaseFlowV2()
// ❌ INCORRECT - Don't use V1 implementations
let subscriptionManager = SubscriptionManager() // Legacy
let purchaseFlow = AppStorePurchaseFlow() // Legacy
```
## Premium Features Implementation
### Feature Entitlements
```swift
// ✅ CORRECT - Check entitlements through SubscriptionManager
final class FeatureViewModel: ObservableObject {
private let subscriptionManager: SubscriptionManager
var isFeatureEnabled: Bool {
subscriptionManager.hasEntitlement(for: .networkProtection)
}
var availableFeatures: [SubscriptionFeature] {
subscriptionManager.entitlements.compactMap { entitlement in
switch entitlement {
case .networkProtection:
return .vpn
case .dataBrokerProtection:
return .personalInformationRemoval
case .identityTheftRestoration:
return .identityTheftRestoration
default:
return nil
}
}
}
}
```
### VPN Integration
```swift
// ✅ CORRECT - VPN entitlement integration
final class VPNManager: ObservableObject {
private let subscriptionManager: SubscriptionManager
func enableVPN() async {
guard subscriptionManager.hasEntitlement(for: .networkProtection) else {
await showSubscriptionPrompt()
return
}
// Enable VPN functionality
await startVPNConnection()
}
}
```
### Personal Information Removal (PIR)
```swift
// ✅ CORRECT - PIR implementation with freemium support
final class PIRManager: ObservableObject {
private let subscriptionManager: SubscriptionManager
var isFreemiumEligible: Bool {
// Check feature flag and eligibility
FeatureFlags.shared.isEnabled(.freemiumPIR) &&
!subscriptionManager.isUserSubscribed &&
isUSUser
}
func performScan() async {
if subscriptionManager.hasEntitlement(for: .dataBrokerProtection) {
await performFullScan()
} else if isFreemiumEligible {
await performLimitedScan()
} else {
await showSubscriptionPrompt()
}
}
}
```
## Purchase Flow Implementation
### Free Trial Support
```swift
// ✅ CORRECT - Free trial implementation
final class SubscriptionPurchaseViewModel: ObservableObject {
@Published var isTrialEligible = false
@Published var trialPeriod: String = ""
func checkTrialEligibility() async {
guard FeatureFlags.shared.isEnabled(.privacyProFreeTrial) else {
isTrialEligible = false
return
}
// Check server-side eligibility
let eligible = await subscriptionManager.checkFreshFreeTrialEligibility()
await MainActor.run {
isTrialEligible = eligible
if let product = subscriptionManager.currentProduct,
let offer = product.introductoryOffer {
trialPeriod = offer.localizedPeriod
}
}
}
}
```
### Platform-Specific Purchase
```swift
// ✅ CORRECT - Platform-aware purchase flow
final class PurchaseFlowCoordinator {
private let subscriptionManager: SubscriptionManager
func initiatePurchase() async {
#if os(iOS)
// iOS always uses App Store
await purchaseViaAppStore()
#elseif os(macOS)
if Bundle.main.isMacAppStore {
await purchaseViaAppStore()
} else {
// Direct download build
if isUSUser {
await purchaseViaStripe()
} else {
await redirectToiOSApp()
}
}
#endif
}
}
```
## Cross-Platform Activation
### URL Handling
```swift
// ✅ CORRECT - Subscription URL handling
final class SubscriptionURLHandler {
func handleSubscriptionURL(_ url: URL) {
guard url.scheme == "duckduckgo",
url.host == "subscription" else { return }
let components = URLComponents(url: url, resolvingAgainstBaseURL: false)
if let token = components?.queryItems?.first(where: { $0.name == "token" })?.value {
Task {
await subscriptionManager.activateSubscription(with: token)
}
}
}
}
```
### Authentication Bridge
```swift
// ✅ CORRECT - V1 to V2 authentication migration
final class AuthenticationManager {
func migrateToV2() async {
let bridge = SubscriptionAuthV1toV2Bridge()
if let v1Token = await bridge.extractV1Token() {
await subscriptionManager.migrateFromV1(token: v1Token)
}
}
}
```
## Testing Patterns
### Mock Subscription Manager
```swift
// ✅ CORRECT - Mock for testing
final class MockSubscriptionManager: SubscriptionManager {
var mockEntitlements: [SubscriptionEntitlement] = []
var mockSubscriptionStatus: Bool = false
override var isUserSubscribed: Bool {
mockSubscriptionStatus
}
override var entitlements: [SubscriptionEntitlement] {
mockEntitlements
}
override func hasEntitlement(for feature: SubscriptionFeature) -> Bool {
mockEntitlements.contains(where: { $0.feature == feature })
}
}
```
### Test Subscription States
```swift
// ✅ CORRECT - Test different subscription states
final class SubscriptionViewModelTests: XCTestCase {
private var viewModel: SubscriptionViewModel!
private var mockManager: MockSubscriptionManager!
func testSubscribedUser() {
// Given
mockManager.mockSubscriptionStatus = true
mockManager.mockEntitlements = [.networkProtection, .dataBrokerProtection]
// When
viewModel.checkSubscriptionStatus()
// Then
XCTAssertTrue(viewModel.isSubscribed)
XCTAssertTrue(viewModel.hasVPNAccess)
XCTAssertTrue(viewModel.hasPIRAccess)
}
func testFreeTrialEligibility() async {
// Given
FeatureFlags.shared.enable(.privacyProFreeTrial)
mockManager.mockTrialEligibility = true
// When
await viewModel.checkTrialEligibility()
// Then
XCTAssertTrue(viewModel.isTrialEligible)
}
}
```
## Feature Flags and Configuration
### Subscription Feature Flags
```swift
// ✅ CORRECT - Feature flag usage
enum SubscriptionFeatureFlag: String, CaseIterable {
case privacyProFreeTrial = "privacyProFreeTrial"
case iosStripeSubscriptions = "iosStripeSubscriptions"
case freemiumPIR = "DBPSubfeature.freemium"
var isEnabled: Bool {
FeatureFlags.shared.isEnabled(self)
}
}
```
### Environment Configuration
```swift
// ✅ CORRECT - Environment-based configuration
extension SubscriptionEnvironment {
static var `default`: SubscriptionEnvironment {
#if os(iOS)
// iOS always uses App Store
return .appStore
#elseif os(macOS)
if Bundle.main.isMacAppStore {
return .appStore
} else {
return FeatureFlags.shared.isEnabled(.iosStripeSubscriptions) ? .stripe : .appStore
}
#endif
}
}
```
## Analytics and Tracking
### Subscription Pixels
```swift
// ✅ CORRECT - Analytics implementation
final class SubscriptionAnalytics {
func trackPurchaseFlow(origin: SubscriptionFunnelOrigin) {
PixelKit.fire(
pixel: .subscriptionPurchaseFlowStarted,
parameters: [
"origin": origin.rawValue,
"platform": currentPlatform.rawValue
]
)
}
func trackTrialEligibility(eligible: Bool) {
PixelKit.fire(
pixel: .subscriptionTrialEligibilityCheck,
parameters: [
"eligible": eligible.description
]
)
}
}
```
## Important Implementation Notes
### Security Considerations
- Store authentication tokens in Keychain only
- Use HTTPS for all subscription API calls
- Validate receipts server-side
- Implement proper token refresh logic
### Performance Optimization
- Cache subscription status locally
- Use background queues for API calls
- Implement offline capability for cached states
- Minimize UI blocking operations
### User Experience
- Provide clear trial information
- Handle purchase failures gracefully
- Support subscription restoration
- Maintain consistent UI across platforms
### Common Pitfalls to Avoid
- Don't hardcode subscription URLs
- Don't bypass entitlement checks
- Don't duplicate subscription logic across platforms
- Don't ignore V1 to V2 migration paths
- Don't forget to handle cross-platform activation
@@ -0,0 +1,274 @@
---
source: ~/DuckDuckGo/apple-browsers.git/main/.cursor/rules/swiftui-advanced.mdc
confidence: 0.9
namespace: work
last_synced: 2026-04-28
alwaysApply: false
---
# Advanced SwiftUI Patterns
## ViewModifier Composition
Create reusable ViewModifiers for common styling:
```swift
// ✅ ADVANCED - Composable ViewModifiers
struct DuckDuckGoButtonStyle: ViewModifier {
let style: ButtonStyleType
func body(content: Content) -> some View {
content
.padding(.horizontal, 16)
.padding(.vertical, 12)
.background(backgroundColorForStyle())
.foregroundColor(textColorForStyle())
.cornerRadius(8)
.font(.body.weight(.medium))
}
private func backgroundColorForStyle() -> Color {
switch style {
case .primary:
return Color(designSystemColor: .buttonPrimaryBackground)
case .secondary:
return Color(designSystemColor: .buttonSecondaryBackground)
case .ghost:
return .clear
}
}
private func textColorForStyle() -> Color {
switch style {
case .primary:
return Color(designSystemColor: .buttonPrimaryText)
case .secondary:
return Color(designSystemColor: .buttonSecondaryText)
case .ghost:
return Color(designSystemColor: .textLink)
}
}
}
// Usage
extension View {
func duckDuckGoButtonStyle(_ style: ButtonStyleType) -> some View {
modifier(DuckDuckGoButtonStyle(style: style))
}
}
```
## PreferenceKey for Cross-View Communication
Use PreferenceKey for sophisticated view communication:
```swift
// ✅ ADVANCED - PreferenceKey for collecting data from child views
struct ViewSizePreferenceKey: PreferenceKey {
static var defaultValue: [String: CGSize] = [:]
static func reduce(value: inout [String: CGSize], nextValue: () -> [String: CGSize]) {
value.merge(nextValue()) { $1 }
}
}
struct SizeReportingView<Content: View>: View {
let id: String
let content: Content
init(id: String, @ViewBuilder content: () -> Content) {
self.id = id
self.content = content()
}
var body: some View {
content
.background(
GeometryReader { geometry in
Color.clear
.preference(key: ViewSizePreferenceKey.self,
value: [id: geometry.size])
}
)
}
}
// Usage
struct ParentView: View {
@State private var childSizes: [String: CGSize] = [:]
var body: some View {
VStack {
SizeReportingView(id: "header") {
HeaderView()
}
SizeReportingView(id: "content") {
ContentView()
}
}
.onPreferenceChange(ViewSizePreferenceKey.self) { sizes in
self.childSizes = sizes
}
}
}
```
## Environment-based Dependency Injection
Use SwiftUI Environment for dependency injection:
```swift
// ✅ ADVANCED - Environment-based DI
struct DependencyProviderKey: EnvironmentKey {
static let defaultValue: DependencyProvider = AppDependencyProvider.shared
}
extension EnvironmentValues {
var dependencies: DependencyProvider {
get { self[DependencyProviderKey.self] }
set { self[DependencyProviderKey.self] = newValue }
}
}
// Usage in views
struct FeatureView: View {
@Environment(\.dependencies) var dependencies
@StateObject private var viewModel: FeatureViewModel
init() {
// Note: This approach has limitations - see ios-architecture.md for preferred DI pattern
}
var body: some View {
// View implementation
}
}
```
## Complex Animation Patterns
Use sophisticated animations for better UX:
```swift
// ✅ ADVANCED - Coordinated animations
struct TabSwitcherView: View {
@State private var selectedTab: Int = 0
@State private var animationPhase: AnimationPhase = .idle
enum AnimationPhase {
case idle, switching, settled
}
var body: some View {
VStack {
TabPickerView(selectedTab: $selectedTab)
.animation(.easeInOut(duration: 0.3), value: selectedTab)
TabContent(selectedTab: selectedTab)
.transition(.asymmetric(
insertion: .move(edge: .trailing).combined(with: .opacity),
removal: .move(edge: .leading).combined(with: .opacity)
))
.animation(.spring(response: 0.6, dampingFraction: 0.8), value: selectedTab)
}
.onChange(of: selectedTab) { newValue in
withAnimation(.easeInOut(duration: 0.1)) {
animationPhase = .switching
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
withAnimation(.easeOut(duration: 0.1)) {
animationPhase = .settled
}
}
}
}
}
```
## Custom Layout with Layout Protocol (iOS 16+)
Create sophisticated layouts:
```swift
// ✅ ADVANCED - Custom layout for complex arrangements
@available(iOS 16.0, macOS 13.0, *)
struct FlexibleGrid: Layout {
let spacing: CGFloat
let itemSize: CGSize
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
let containerWidth = proposal.width ?? 300
let itemsPerRow = max(1, Int(containerWidth / (itemSize.width + spacing)))
let rows = (subviews.count + itemsPerRow - 1) / itemsPerRow
let totalWidth = CGFloat(itemsPerRow) * itemSize.width + CGFloat(itemsPerRow - 1) * spacing
let totalHeight = CGFloat(rows) * itemSize.height + CGFloat(rows - 1) * spacing
return CGSize(width: totalWidth, height: totalHeight)
}
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
let containerWidth = bounds.width
let itemsPerRow = max(1, Int(containerWidth / (itemSize.width + spacing)))
for (index, subview) in subviews.enumerated() {
let row = index / itemsPerRow
let column = index % itemsPerRow
let x = bounds.minX + CGFloat(column) * (itemSize.width + spacing)
let y = bounds.minY + CGFloat(row) * (itemSize.height + spacing)
subview.place(at: CGPoint(x: x, y: y), proposal: ProposedViewSize(itemSize))
}
}
}
```
## State Management with @Observable (iOS 17+)
Use the modern @Observable macro when available:
```swift
// ✅ MODERN - @Observable for iOS 17+
@available(iOS 17.0, macOS 14.0, *)
@Observable
final class ModernViewModel {
var items: [Item] = []
var isLoading = false
var selectedItem: Item?
private let service: FeatureServiceProtocol
init(service: FeatureServiceProtocol) {
self.service = service
}
@MainActor
func loadData() async {
isLoading = true
defer { isLoading = false }
do {
items = try await service.fetchItems()
} catch {
// Handle error
}
}
}
// Usage
struct ModernFeatureView: View {
@State private var viewModel: ModernViewModel
init(service: FeatureServiceProtocol) {
self._viewModel = State(initialValue: ModernViewModel(service: service))
}
var body: some View {
List(viewModel.items, selection: $viewModel.selectedItem) { item in
ItemRow(item: item)
}
.task {
await viewModel.loadData()
}
}
}
```
See `swiftui-style.md` for basic SwiftUI patterns and design system integration.
+336
View File
@@ -0,0 +1,336 @@
---
source: ~/DuckDuckGo/apple-browsers.git/main/.cursor/rules/swiftui-style.mdc
confidence: 0.9
namespace: work
last_synced: 2026-04-28
alwaysApply: false
---
# SwiftUI Style Guide with Design System Integration for DuckDuckGo Browser
## View Structure
### View Organization
```swift
struct FeatureView: View {
// MARK: - Environment and State
@Environment(\.colorScheme) var colorScheme
@EnvironmentObject var appSettings: AppSettings
// MARK: - State and Binding
@State private var localState = false
@Binding var externalState: Bool
// MARK: - View Model
@StateObject private var viewModel: FeatureViewModel
// MARK: - Body
var body: some View {
content
.onAppear { viewModel.onAppear() }
}
// MARK: - Subviews
@ViewBuilder
private var content: some View {
// Main content here
}
}
```
## Design System Integration
### REQUIRED: Use DesignResourcesKit Colors
ALWAYS use semantic colors from DesignResourcesKit instead of hardcoded or system colors:
```swift
// ✅ CORRECT - DesignResourcesKit semantic colors
Text("Title")
.foregroundColor(Color(designSystemColor: .textPrimary))
.background(Color(designSystemColor: .surface))
VStack {
Rectangle()
.fill(Color(designSystemColor: .accent))
Button("Action") { }
.foregroundColor(Color(designSystemColor: .controlsFillPrimary))
}
.background(Color(designSystemColor: .background))
// ❌ INCORRECT - Hardcoded or system colors
Text("Title")
.foregroundColor(.black) // Don't use hardcoded colors
.background(.gray) // Don't use system colors
// ❌ INCORRECT - Manual dark mode handling
@Environment(\.colorScheme) var colorScheme
let textColor = colorScheme == .dark ? Color.white : Color.black // Use semantic colors instead
```
### REQUIRED: Use DesignResourcesKit Icons
ALWAYS use icons from DesignResourcesKitIcons package:
```swift
// ✅ CORRECT - DesignResourcesKit icons
Button(action: addAction) {
Image(uiImage: DesignSystemImages.Glyphs.Size16.add)
.foregroundColor(Color(designSystemColor: .accent))
}
Image(uiImage: DesignSystemImages.Color.Size24.bookmark)
.resizable()
.frame(width: 24, height: 24)
// ❌ INCORRECT - System or custom icons
Button(action: addAction) {
Image(systemName: "plus") // Use DesignResourcesKit icons
}
Image("custom_icon") // Use DesignResourcesKit icons instead
```
### Design System Color Categories
Use appropriate semantic color categories:
```swift
// Text colors
.foregroundColor(Color(designSystemColor: .textPrimary))
.foregroundColor(Color(designSystemColor: .textSecondary))
.foregroundColor(Color(designSystemColor: .textLink))
// Background colors
.background(Color(designSystemColor: .background))
.background(Color(designSystemColor: .surface))
.background(Color(designSystemColor: .panel))
// Control colors
.foregroundColor(Color(designSystemColor: .controlsFillPrimary))
.foregroundColor(Color(designSystemColor: .controlsFillSecondary))
// Button colors (use specific button color tokens)
.foregroundColor(Color(designSystemColor: .buttonPrimaryText))
.background(Color(designSystemColor: .buttonPrimaryBackground))
```
### Typography with Design System
Use semantic typography that integrates with the design system:
```swift
// ✅ CORRECT - Design system typography
Text("Header")
.font(.title2.weight(.semibold))
.foregroundColor(Color(designSystemColor: .textPrimary))
Text("Body")
.font(.body)
.foregroundColor(Color(designSystemColor: .textSecondary))
Text("Caption")
.font(.caption)
.foregroundColor(Color(designSystemColor: .textSecondary))
// Platform-specific typography (macOS)
#if os(macOS)
Text("Preference Title")
.font(Fonts.preferencePaneTitle)
.foregroundColor(Color(designSystemColor: .textPrimary))
#endif
```
### Theme Integration
Use Theme protocol for complex scenarios:
```swift
// ✅ CORRECT - Theme integration for advanced scenarios
struct ComplexView: View {
@EnvironmentObject var themeManager: ThemeManager
var body: some View {
VStack {
Text("Content")
.foregroundColor(Color(themeManager.currentTheme.textColor))
}
.background(Color(themeManager.currentTheme.backgroundColor))
}
}
// ✅ PREFERRED - Direct semantic colors for simple cases
struct SimpleView: View {
var body: some View {
Text("Content")
.foregroundColor(Color(designSystemColor: .textPrimary))
.background(Color(designSystemColor: .background))
}
}
```
## Component Patterns
### Reusable Components
- Create small, focused components
- Use ViewModifiers for common styling
- Leverage ViewBuilder for conditional content
```swift
struct PrimaryButton: View {
let title: String
let action: () -> Void
var body: some View {
Button(action: action) {
Text(title)
.foregroundColor(.white)
.padding()
.background(Color.accentColor)
.cornerRadius(8)
}
}
}
```
### Lists and Navigation
```swift
List {
Section {
ForEach(items) { item in
NavigationLink(destination: DetailView(item: item)) {
ItemRow(item: item)
}
}
} header: {
Text("Section Title")
}
}
.listStyle(.insetGrouped)
```
## State Management
### View Model Pattern
```swift
class FeatureViewModel: ObservableObject {
@Published var items: [Item] = []
@Published var isLoading = false
@Published var error: Error?
func loadData() async {
isLoading = true
defer { isLoading = false }
do {
items = try await service.fetchItems()
} catch {
self.error = error
}
}
}
```
### Async Operations
```swift
struct ContentView: View {
@StateObject private var viewModel = ViewModel()
var body: some View {
content
.task {
await viewModel.loadData()
}
.refreshable {
await viewModel.refresh()
}
}
}
```
## Animations and Transitions
### Smooth Animations
```swift
@State private var isExpanded = false
var body: some View {
VStack {
content
.frame(height: isExpanded ? 200 : 100)
.animation(.spring(), value: isExpanded)
}
}
```
### Custom Transitions
```swift
.transition(.asymmetric(
insertion: .move(edge: .trailing).combined(with: .opacity),
removal: .move(edge: .leading).combined(with: .opacity)
))
```
## Accessibility
### Always Include Accessibility
```swift
Image(systemName: "star.fill")
.accessibilityLabel("Favorite")
.accessibilityHint("Double tap to toggle favorite status")
Button(action: action) {
Text("Submit")
}
.accessibilityIdentifier("submit_button")
```
## Performance Considerations
### Lazy Loading
```swift
ScrollView {
LazyVStack {
ForEach(items) { item in
ItemView(item: item)
}
}
}
```
### Avoid Expensive Operations in Body
```swift
// Bad
var body: some View {
let processedData = expensiveOperation(data) // Don't do this
Text(processedData)
}
// Good
@State private var processedData: String = ""
var body: some View {
Text(processedData)
.onAppear {
processedData = expensiveOperation(data)
}
}
```
## Preview Support
### Comprehensive Previews
```swift
struct FeatureView_Previews: PreviewProvider {
static var previews: some View {
Group {
FeatureView()
.previewDisplayName("Default")
FeatureView()
.preferredColorScheme(.dark)
.previewDisplayName("Dark Mode")
FeatureView()
.previewDevice("iPhone SE (3rd generation)")
.previewDisplayName("Small Device")
}
}
}
```
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,217 @@
---
source: ~/DuckDuckGo/apple-browsers.git/main/.cursor/rules/user-defaults-storage.mdc
confidence: 0.9
namespace: work
last_synced: 2026-04-28
alwaysApply: false
---
# User Defaults Settings Storage and Reading
## ✅ RECOMMENDED - KVO Pattern with KeyValueStore
Use the KVO pattern with KeyValueStore for all new persistent settings:
```swift
// ✅ CORRECT - KVO pattern with KeyValueStore
struct AppearancePreferencesUserDefaultsPersistor: AppearancePreferencesPersistor {
enum Key: String {
case newTabPageIsOmnibarVisible = "new-tab-page.omnibar.is-visible"
case newTabPageIsProtectionsReportVisible = "new-tab-page.protections-report.is-visible"
case userPreferences = "user.preferences"
case lastUpdateCheck = "last.update.check"
}
private let keyValueStore: KeyValueStoring
init(keyValueStore: KeyValueStoring) {
self.keyValueStore = keyValueStore
}
var isOmnibarVisible: Bool {
get { (try? keyValueStore.object(forKey: Key.newTabPageIsOmnibarVisible.rawValue) as? Bool) ?? true }
set { try? keyValueStore.set(newValue, forKey: Key.newTabPageIsOmnibarVisible.rawValue) }
}
var isProtectionsReportVisible: Bool {
get { (try? keyValueStore.object(forKey: Key.newTabPageIsProtectionsReportVisible.rawValue) as? Bool) ?? false }
set { try? keyValueStore.set(newValue, forKey: Key.newTabPageIsProtectionsReportVisible.rawValue) }
}
var userPreferences: [String: String] {
get { (try? keyValueStore.object(forKey: Key.userPreferences.rawValue) as? [String: String]) ?? [:] }
set { try? keyValueStore.set(newValue, forKey: Key.userPreferences.rawValue) }
}
var lastUpdateCheck: Date {
get { (try? keyValueStore.object(forKey: Key.lastUpdateCheck.rawValue) as? Date) ?? Date.distantPast }
set { try? keyValueStore.set(newValue, forKey: Key.lastUpdateCheck.rawValue) }
}
}
```
## Key Guidelines for KVO Pattern
1. **Use struct conforming to protocol** - Follow the persistor pattern
2. **Define keys as enum with String raw values** - Use kebab-case for key names
3. **Use KeyValueStoring protocol** - Not direct UserDefaults access
4. **Computed properties with get/set** - Handle storage operations in accessors
5. **Use try? for error handling** - KeyValueStore operations can throw
6. **Provide default values** - Use nil coalescing operator (??) for defaults
7. **Inject KeyValueStore in init** - Enable dependency injection and testing
## Advanced Pattern for Optional Values
```swift
// ✅ CORRECT - Optional values pattern
struct SettingsUserDefaultsPersistor: SettingsPersistor {
enum Key: String {
case optionalUserName = "user.name"
case optionalTheme = "app.theme"
}
private let keyValueStore: KeyValueStoring
init(keyValueStore: KeyValueStoring) {
self.keyValueStore = keyValueStore
}
var optionalUserName: String? {
get { try? keyValueStore.object(forKey: Key.optionalUserName.rawValue) as? String }
set {
if let value = newValue {
try? keyValueStore.set(value, forKey: Key.optionalUserName.rawValue)
} else {
try? keyValueStore.removeObject(forKey: Key.optionalUserName.rawValue)
}
}
}
var selectedTheme: Theme? {
get {
guard let rawValue = try? keyValueStore.object(forKey: Key.optionalTheme.rawValue) as? String else { return nil }
return Theme(rawValue: rawValue)
}
set {
if let value = newValue {
try? keyValueStore.set(value.rawValue, forKey: Key.optionalTheme.rawValue)
} else {
try? keyValueStore.removeObject(forKey: Key.optionalTheme.rawValue)
}
}
}
}
```
## Platform-Specific Storage
```swift
// ✅ CORRECT - Platform-specific KeyValueStore usage
struct PlatformSettingsUserDefaultsPersistor: PlatformSettingsPersistor {
enum Key: String {
case platformSpecificSetting = "platform.specific.setting"
}
private let keyValueStore: KeyValueStoring
init(keyValueStore: KeyValueStoring) {
self.keyValueStore = keyValueStore
}
var platformSpecificSetting: Bool {
get {
#if os(iOS)
return (try? keyValueStore.object(forKey: Key.platformSpecificSetting.rawValue) as? Bool) ?? false
#elseif os(macOS)
return (try? keyValueStore.object(forKey: Key.platformSpecificSetting.rawValue) as? Bool) ?? true
#endif
}
set {
try? keyValueStore.set(newValue, forKey: Key.platformSpecificSetting.rawValue)
}
}
}
```
## 🚫 DEPRECATED - @UserDefaultsWrapper Pattern
The following pattern is deprecated and should not be used for new code:
```swift
// ❌ DEPRECATED - Do not use @UserDefaultsWrapper for new code
extension AppUserDefaults {
@UserDefaultsWrapper(key: .newFeatureEnabled, defaultValue: false)
var newFeatureEnabled: Bool
@UserDefaultsWrapper(key: .lastUpdateCheck, defaultValue: Date.distantPast)
var lastUpdateCheck: Date
}
```
## Migration from Property Wrappers
When migrating from `@UserDefaultsWrapper` to the KVO pattern:
1. **Create a new persistor struct** - Following the naming convention `*UserDefaultsPersistor`
2. **Define keys enum** - Convert string keys to enum cases
3. **Convert properties** - Transform @UserDefaultsWrapper properties to computed properties
4. **Update injection** - Pass KeyValueStore through dependency injection
5. **Preserve key names** - Ensure existing UserDefaults keys remain unchanged
## Testing Pattern
```swift
// ✅ CORRECT - Testing with mock KeyValueStore
class MockKeyValueStore: KeyValueStoring {
private var storage: [String: Any] = [:]
func object(forKey key: String) throws -> Any? {
return storage[key]
}
func set(_ value: Any, forKey key: String) throws {
storage[key] = value
}
func removeObject(forKey key: String) throws {
storage.removeValue(forKey: key)
}
}
// In tests
let mockStore = MockKeyValueStore()
let persistor = AppearancePreferencesUserDefaultsPersistor(keyValueStore: mockStore)
persistor.isOmnibarVisible = true
XCTAssertTrue(persistor.isOmnibarVisible)
```
## What NOT to Do
```swift
// ❌ INCORRECT - Direct UserDefaults access
var newFeatureEnabled: Bool {
get { return UserDefaults.standard.bool(forKey: "newFeature") }
set { UserDefaults.standard.set(newValue, forKey: "newFeature") }
}
// ❌ INCORRECT - Using @UserDefaultsWrapper for new code
@UserDefaultsWrapper(key: .newFeatureEnabled, defaultValue: false)
var newFeatureEnabled: Bool
// ❌ INCORRECT - Not handling errors
var setting: Bool {
get { keyValueStore.object(forKey: "key") as? Bool ?? false } // Missing try?
set { keyValueStore.set(newValue, forKey: "key") } // Missing try?
}
// ❌ INCORRECT - Not using enum for keys
var setting: Bool {
get { (try? keyValueStore.object(forKey: "hardcoded-key") as? Bool) ?? false }
set { try? keyValueStore.set(newValue, forKey: "hardcoded-key") }
}
```
The KVO pattern with KeyValueStore provides better testability, error handling, and dependency injection while maintaining type safety and consistency across the codebase.
+387
View File
@@ -0,0 +1,387 @@
---
source: ~/DuckDuckGo/apple-browsers.git/main/.cursor/rules/webkit-browser.mdc
confidence: 0.9
namespace: work
last_synced: 2026-04-28
alwaysApply: false
---
# WebKit & Browser Development Guidelines
## WebView Configuration
### Basic WebView Setup
```swift
import WebKit
class BrowserWebView {
private lazy var webView: WKWebView = {
let configuration = WKWebViewConfiguration()
// Enable JavaScript
configuration.preferences.javaScriptEnabled = true
// Set user agent
configuration.applicationNameForUserAgent = UserAgentManager.shared.userAgent
// Configure content blockers
configuration.userContentController = makeUserContentController()
// Enable developer extras in debug
#if DEBUG
configuration.preferences.setValue(true, forKey: "developerExtrasEnabled")
#endif
let webView = WKWebView(frame: .zero, configuration: configuration)
webView.allowsBackForwardNavigationGestures = true
webView.allowsLinkPreview = true
return webView
}()
}
```
### User Scripts Management
```swift
private func makeUserContentController() -> WKUserContentController {
let controller = WKUserContentController()
// Add content blocking scripts
let contentBlockingScript = WKUserScript(
source: ContentBlockingUserScript.source,
injectionTime: .atDocumentStart,
forMainFrameOnly: false
)
controller.addUserScript(contentBlockingScript)
// Add message handlers
controller.add(self, name: "duckduckgo")
return controller
}
```
## Tab Management
### Tab Model
```swift
class Tab: NSObject {
let id = UUID()
private(set) var url: URL?
private(set) var title: String?
private(set) var favicon: UIImage?
weak var webView: WKWebView?
weak var delegate: TabDelegate?
private var observations: Set<NSKeyValueObservation> = []
init(url: URL? = nil) {
self.url = url
super.init()
setupWebView()
}
private func setupWebView() {
let webView = WKWebView(frame: .zero, configuration: TabManager.shared.configuration)
self.webView = webView
// Observe properties
observations.insert(
webView.observe(\.url) { [weak self] _, _ in
self?.urlDidChange()
}
)
observations.insert(
webView.observe(\.title) { [weak self] webView, _ in
self?.title = webView.title
self?.delegate?.tab(self!, didUpdateTitle: webView.title)
}
)
observations.insert(
webView.observe(\.estimatedProgress) { [weak self] webView, _ in
self?.delegate?.tab(self!, didUpdateProgress: webView.estimatedProgress)
}
)
}
}
```
### Tab Lifecycle
```swift
extension Tab {
func load(url: URL) {
let request = URLRequest(url: url)
webView?.load(request)
}
func reload() {
webView?.reload()
}
func stop() {
webView?.stopLoading()
}
func goBack() {
webView?.goBack()
}
func goForward() {
webView?.goForward()
}
func close() {
observations.forEach { $0.invalidate() }
observations.removeAll()
webView?.stopLoading()
webView?.removeFromSuperview()
webView = nil
}
}
```
## Navigation Handling
### Navigation Delegate
```swift
extension BrowserViewController: WKNavigationDelegate {
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction) async -> WKNavigationActionPolicy {
let url = navigationAction.request.url
// Handle special URLs
if let url = url, URLSchemeHandler.shared.canHandle(url) {
URLSchemeHandler.shared.handle(url)
return .cancel
}
// Apply content blocking
if contentBlocker.shouldBlock(url) {
return .cancel
}
// Check for downloads
if shouldDownload(navigationAction) {
startDownload(from: navigationAction.request)
return .cancel
}
return .allow
}
func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
updateProgressBar(animated: true)
updateNavigationButtons()
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
hideProgressBar()
captureHistory()
updateFavicon()
}
func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
handleNavigationError(error)
}
}
```
## JavaScript Bridge
### Message Handling
```swift
extension BrowserViewController: WKScriptMessageHandler {
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
guard let dict = message.body as? [String: Any] else { return }
switch message.name {
case "duckduckgo":
handleDuckDuckGoMessage(dict)
case "autofill":
handleAutofillMessage(dict)
case "tracker":
handleTrackerMessage(dict)
default:
break
}
}
private func handleDuckDuckGoMessage(_ message: [String: Any]) {
guard let action = message["action"] as? String else { return }
switch action {
case "openSettings":
presentSettings()
case "reportBrokenSite":
presentBrokenSiteReport()
default:
break
}
}
}
```
### JavaScript Injection
```swift
extension WKWebView {
func evaluateJavaScriptSafely(_ script: String) async throws -> Any? {
return try await withCheckedThrowingContinuation { continuation in
evaluateJavaScript(script) { result, error in
if let error = error {
continuation.resume(throwing: error)
} else {
continuation.resume(returning: result)
}
}
}
}
func injectPrivacyProtection() async {
let script = """
(function() {
// Override fingerprinting methods
const originalCanvas = HTMLCanvasElement.prototype.toDataURL;
HTMLCanvasElement.prototype.toDataURL = function() {
return "";
};
// Block tracking pixels
const observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
mutation.addedNodes.forEach(function(node) {
if (node.tagName === 'IMG' && isTrackingPixel(node.src)) {
node.remove();
}
});
});
});
observer.observe(document.body, { childList: true, subtree: true });
})();
"""
try? await evaluateJavaScriptSafely(script)
}
}
```
## Cookie Management
### Cookie Handling
```swift
extension BrowserViewController {
func clearCookies(completion: @escaping () -> Void) {
let dataStore = WKWebsiteDataStore.default()
let dataTypes = Set([WKWebsiteDataTypeCookies])
dataStore.fetchDataRecords(ofTypes: dataTypes) { records in
let fireproofedDomains = FireproofingManager.shared.fireproofedDomains
let recordsToDelete = records.filter { record in
!fireproofedDomains.contains(where: { record.displayName.contains($0) })
}
dataStore.removeData(ofTypes: dataTypes, for: recordsToDelete) {
completion()
}
}
}
}
```
## Download Management
### Download Delegate
```swift
extension BrowserViewController: WKDownloadDelegate {
func download(_ download: WKDownload, decideDestinationUsing response: URLResponse, suggestedFilename: String) async -> URL? {
let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
let destinationURL = documentsURL.appendingPathComponent(suggestedFilename)
// Check if file exists and generate unique name if needed
return FileManager.default.uniqueURL(for: destinationURL)
}
func download(_ download: WKDownload, didFailWithError error: Error, resumeData: Data?) {
// Handle download failure
if let resumeData = resumeData {
// Store resume data for later
DownloadManager.shared.storeResumeData(resumeData, for: download)
}
}
func downloadDidFinish(_ download: WKDownload) {
// Handle successful download
DownloadManager.shared.completeDownload(download)
}
}
```
## Performance Optimization
### Memory Management
```swift
class TabManager {
private let maxInMemoryTabs = 5
private var tabs: [Tab] = []
func optimizeMemory() {
let activeTabs = tabs.filter { $0.isActive }
let inactiveTabs = tabs.filter { !$0.isActive }
.sorted { $0.lastAccessDate < $1.lastAccessDate }
// Suspend inactive tabs if we have too many in memory
if activeTabs.count + inactiveTabs.count > maxInMemoryTabs {
let tabsToSuspend = inactiveTabs.prefix(inactiveTabs.count - (maxInMemoryTabs - activeTabs.count))
tabsToSuspend.forEach { $0.suspend() }
}
}
}
extension Tab {
func suspend() {
// Take snapshot
webView?.takeSnapshot(with: nil) { [weak self] image, error in
self?.snapshot = image
self?.webView?.removeFromSuperview()
self?.webView = nil
}
}
func resume() {
guard webView == nil else { return }
setupWebView()
if let url = url {
load(url: url)
}
}
}
```
## Security Considerations
### Certificate Validation
```swift
extension BrowserViewController: WKNavigationDelegate {
func webView(_ webView: WKWebView, didReceive challenge: URLAuthenticationChallenge) async -> (URLSession.AuthChallengeDisposition, URLCredential?) {
guard challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust,
let serverTrust = challenge.protectionSpace.serverTrust else {
return (.performDefaultHandling, nil)
}
// Perform certificate pinning for DuckDuckGo domains
if CertificatePinning.shared.shouldPin(host: challenge.protectionSpace.host) {
do {
try CertificatePinning.shared.validate(serverTrust, host: challenge.protectionSpace.host)
let credential = URLCredential(trust: serverTrust)
return (.useCredential, credential)
} catch {
return (.cancelAuthenticationChallenge, nil)
}
}
return (.performDefaultHandling, nil)
}
}
```