269 lines
9.5 KiB
Markdown
269 lines
9.5 KiB
Markdown
---
|
||
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 pixel’s 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
|