Files
obsidian-vault/work/wiki/apple-browsers/pixels.md
T

269 lines
9.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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