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