From 1d1278dbb38c9967b15f7ead0502e3a514665c7d Mon Sep 17 00:00:00 2001 From: Jared Lockhart <119884+jaredlockhart@users.noreply.github.com> Date: Thu, 19 Mar 2026 12:19:23 -0400 Subject: [PATCH 01/14] docs(nimbus): add comprehensive Firefox for Android targeting guide MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Because * The existing Android custom targeting page is deprecated and only covers how to add new custom attributes — there is no guide covering the available targeting attributes, behavioral targeting, or common patterns * Desktop now has a comprehensive targeting guide but Android does not This commit * Adds a new Fenix Targeting Guide covering all available targeting attributes (from RecordedNimbusContext.kt and CustomAttributeProvider.kt), behavioral targeting via event queries, how targeting differs from desktop, common patterns from targeting/constants.py, install attribution, Terms of Use targeting, add-on detection, and how to add new attributes Fixes #801 Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/platform-guides/android/targeting.md | 430 ++++++++++++++++++++++ 1 file changed, 430 insertions(+) create mode 100644 docs/platform-guides/android/targeting.md diff --git a/docs/platform-guides/android/targeting.md b/docs/platform-guides/android/targeting.md new file mode 100644 index 000000000..7dd846989 --- /dev/null +++ b/docs/platform-guides/android/targeting.md @@ -0,0 +1,430 @@ +--- +id: fenix-targeting +title: Firefox for Android (Fenix) Targeting Guide +slug: /platform-guides/android/targeting +--- + +This guide covers how targeting works for Firefox for Android (Fenix) experiments and rollouts in Nimbus. It explains the available targeting attributes, how to write JEXL expressions, and how to test and debug your targeting. + +## How Targeting Works + +When you create an experiment in Experimenter, you configure **who** should be enrolled. Targeting happens at two levels: + +1. **Basic targeting** (UI fields) — application, channel, Firefox version range, locale, country, language +2. **Advanced targeting** — a [JEXL](https://github.com/mozilla/mozjexl) expression evaluated against the client's targeting context to filter users by install age, device properties, UTM attribution, and more + +Both levels are combined into a single JEXL expression that the Nimbus SDK evaluates on every Firefox for Android installation. If the expression evaluates to `true`, the client is eligible for enrollment. + +### Evaluation Flow + +1. Firefox for Android starts and initializes the Nimbus SDK +2. The SDK fetches experiment recipes from Remote Settings +3. For each experiment, the SDK evaluates the `targeting` JEXL expression against the current targeting context +4. Clients that match targeting and fall into an eligible bucket are enrolled. Existing enrollments that no longer match targeting are unenrolled (unless protected by a [sticky clause](#sticky-targeting)). + +### How Targeting Differs from Desktop + +Firefox for Android uses the cross-platform **Nimbus SDK** (written in Rust), not the desktop-specific Nimbus client. Key differences: + +- **Channel** is determined by the app ID (e.g., `org.mozilla.firefox` for release, `org.mozilla.firefox_beta` for beta), not a `browserSettings.update.channel` attribute +- **Version comparisons** use `app_version|versionCompare(...)` instead of `version|versionCompare(...)` +- **Sticky targeting** uses `is_already_enrolled` instead of `experiment.slug in activeExperiments` +- **Behavioral targeting** via event queries (e.g., `|eventCountNonZero`) is available — this is not available on desktop +- **Language** filtering uses `language in [...]` (two-letter code extracted from locale) + +## Basic Targeting (UI Fields) + +These are configured directly in the Experimenter audience form: + +| Field | Description | +|-------|-------------| +| **Channel** | A single channel: `release`, `beta`, `nightly`, or `developer`. On mobile, channel is determined by the app ID, so each channel is a separate application. | +| **Min/Max Version** | Firefox version range (e.g., 134 to 140). Uses `app_version|versionCompare(...)` internally. | +| **Locales** | Browser locale codes (e.g., `en-US`, `de`). Can include or exclude. | +| **Languages** | Two-letter language codes (e.g., `en`, `de`). Can include or exclude. This is extracted from the locale. | +| **Countries** | Country codes from the locale region (e.g., `US`, `DE`). Can include or exclude. | +| **Population %** | Percentage of eligible users to enroll (bucketing). | + +These fields are translated into JEXL conditions that are combined with any advanced targeting you specify. + +## Advanced Targeting + +Advanced targeting uses pre-defined configurations or custom JEXL expressions. In the Experimenter UI, you select from a dropdown of pre-defined targeting configs, each backed by a JEXL expression. + +These configs are defined in [`targeting/constants.py`](https://github.com/mozilla/experimenter/blob/main/experimenter/experimenter/targeting/constants.py) in the Experimenter repo. To add a new one, see [Adding New Targeting Options](#adding-new-targeting-options) below. + +## Targeting Attributes Reference + +The targeting context for Firefox for Android is assembled from multiple sources: + +1. **App context** — provided by the application at startup (app name, version, channel, device info) +2. **Computed attributes** — calculated by the Nimbus SDK (days since install, language, region) +3. **Recorded context** — app-specific attributes defined in [`RecordedNimbusContext.kt`](https://searchfox.org/mozilla-mobile/source/fenix/app/src/main/java/org/mozilla/fenix/experiments/RecordedNimbusContext.kt) that are recorded to Glean for population sizing +4. **Custom attributes** — additional attributes from [`CustomAttributeProvider.kt`](https://searchfox.org/mozilla-mobile/source/fenix/app/src/main/java/org/mozilla/fenix/messaging/CustomAttributeProvider.kt), available at startup for experiment targeting + +### App & Version + +| Attribute | Type | Description | Example | +|-----------|------|-------------|---------| +| `app_name` | `string` | Application name (always `"fenix"`) | `app_name == 'fenix'` | +| `app_id` | `string` | Application package ID | `app_id == 'org.mozilla.firefox'` | +| `app_version` | `string` | App version string (e.g., `"147.0"`) | `app_version\|versionCompare('134.!') >= 0` | +| `channel` | `string` | Build channel (`release`, `beta`, `nightly`, `developer`) | Usually set via UI, not JEXL | + +:::note +Version targeting is typically set via the Min/Max Version UI fields (which generate `app_version|versionCompare('X.!') >= 0` for min and `app_version|versionCompare('X.*') <= 0` for max automatically). +::: + +### Install & Update + +| Attribute | Type | Description | Example | +|-----------|------|-------------|---------| +| `days_since_install` | `number` | Days since app was first installed | `days_since_install < 7` | +| `days_since_update` | `number` | Days since last app update | `days_since_update < 7 && days_since_install >= 7` | +| `is_first_run` | `boolean` | True during the app's first run | `is_first_run` | +| `number_of_app_launches` | `number` | Total number of app launches | `number_of_app_launches <= 20` | + +:::note +`isFirstRun` (camelCase, string `"true"`/`"false"`) exists for backwards compatibility. Prefer `is_first_run` (snake_case, boolean) for new targeting. +::: + +### Locale & Region + +| Attribute | Type | Description | Example | +|-----------|------|-------------|---------| +| `locale` | `string` | Full locale tag (e.g., `en-US`) | `locale in ['en-US', 'en-GB']` | +| `language` | `string` | Two-letter language code extracted from locale (e.g., `en`) | `language in ['en', 'fr']` | +| `region` | `string` | Country code extracted from locale (e.g., `US`) | `region in ['US', 'CA']` | + +:::note +Locale, language, and region targeting is typically set via the Experimenter UI fields, but can also be used directly in advanced targeting expressions. +::: + +### Device & OS + +| Attribute | Type | Description | Example | +|-----------|------|-------------|---------| +| `android_sdk_version` | `string` | Android API level as a string (e.g., `"33"` for Android 13) | `android_sdk_version\|versionCompare('33') >= 0` | +| `device_manufacturer` | `string` | Device manufacturer (from `Build.MANUFACTURER`) | `device_manufacturer == 'Google'` | +| `device_model` | `string` | Device model (from `Build.MODEL`) | `device_model == 'Pixel 8'` | +| `is_large_device` | `boolean` | Whether the device has a large screen | `is_large_device` | +| `architecture` | `string` | CPU architecture (e.g., `arm`, `x86`) | | + +### Install Attribution (UTM) + +| Attribute | Type | Description | Example | +|-----------|------|-------------|---------| +| `install_referrer_response_utm_source` | `string` | UTM source from install referrer | `install_referrer_response_utm_source == 'eea-browser-choice'` | +| `install_referrer_response_utm_medium` | `string` | UTM medium | | +| `install_referrer_response_utm_campaign` | `string` | UTM campaign | | +| `install_referrer_response_utm_term` | `string` | UTM term | | +| `install_referrer_response_utm_content` | `string` | UTM content | | + +### Terms of Use & Privacy + +| Attribute | Type | Description | Example | +|-----------|------|-------------|---------| +| `user_accepted_tou` | `boolean` | Whether the user has accepted Terms of Use | `user_accepted_tou == false && days_since_install >= 28` | +| `no_shortcuts_or_stories_opt_outs` | `boolean` | Whether the user has not opted out of sponsored shortcuts/stories | `no_shortcuts_or_stories_opt_outs == true` | +| `tou_points` | `number` | Terms of Use experience points (scoring for ToU targeting tiers) | `tou_points == 0` | + +### Add-ons + +| Attribute | Type | Description | Example | +|-----------|------|-------------|---------| +| `addon_ids` | `string[]` | List of installed add-on IDs | `'uBlock0@raymondhill.net' in addon_ids` | + +**Detecting ad blockers:** + +``` +// Has uBlock Origin installed +'uBlock0@raymondhill.net' in addon_ids + +// Does NOT have any common ad blocker +('uBlock0@raymondhill.net' in addon_ids) == false +&& ('{d10d0bf8-f5b5-c8b4-a8b2-2b9879e08c5d}' in addon_ids) == false +&& ('adguardadblocker@adguard.com' in addon_ids) == false +&& ('firefox@ghostery.com' in addon_ids) == false +``` + +### Experiment & Rollout Enrollment + +| Attribute | Type | Description | Example | +|-----------|------|-------------|---------| +| `is_already_enrolled` | `boolean` | Whether the client is already enrolled in this experiment | Used in sticky clauses | +| `active_experiments` | `string[]` | Currently enrolled experiment slugs | `'my-experiment' in active_experiments` | +| `enrollments` | `string[]` | All experiment enrollments (including past) | `('other-slug' in enrollments) == false` | +| `enrollments_map` | `object` | Experiment slug → branch slug mapping | Used for branch-level exclusion | + +### Additional Attributes (Messaging / Display Triggers) + +These attributes are available from [`CustomAttributeProvider.kt`](https://searchfox.org/mozilla-mobile/source/fenix/app/src/main/java/org/mozilla/fenix/messaging/CustomAttributeProvider.kt) and are primarily used for messaging display triggers but can also be used in experiment targeting: + +| Attribute | Type | Description | Example | +|-----------|------|-------------|---------| +| `is_default_browser` | `boolean` | Whether Firefox is the default browser | `is_default_browser` | +| `are_notifications_enabled` | `boolean` | Whether notification permissions are granted | `are_notifications_enabled` | +| `search_widget_is_installed` | `boolean` | Whether the search widget is on the home screen | `search_widget_is_installed` | +| `is_fxa_signed_in` | `boolean` | Whether the user is signed into Firefox Account | `is_fxa_signed_in` | +| `fxa_connected_devices` | `number` | Number of connected FxA devices | `fxa_connected_devices >= 2` | +| `date_string` | `string` | Current date as `yyyy-MM-dd` | | +| `adjust_campaign` | `string` | Adjust campaign ID | | +| `adjust_network` | `string` | Adjust network | | +| `adjust_ad_group` | `string` | Adjust ad group | | +| `adjust_creative` | `string` | Adjust creative | | + +:::warning +Attributes from `CustomAttributeProvider` are evaluated at startup. Attributes that require initialization after startup (like `are_notifications_enabled`) **cannot** reliably target first-run experiments — they will only be accurate from the second startup onward. +::: + +## Behavioral Targeting (Event Queries) + +Firefox for Android supports **behavioral targeting** via event queries — this is a capability unique to the cross-platform Nimbus SDK and is **not available on desktop**. + +Event queries let you target users based on their past behavior by querying the Nimbus event store. Events are bucketed by time interval. + +### Available Events + +| Event | Description | +|-------|-------------| +| `events.app_opened` | Application opened | +| `sync_auth.sign_in` | User signed into Sync | + +### Event Query Transforms + +| Transform | Returns | Description | +|-----------|---------|-------------| +| `\|eventSum(interval, bucket_count, starting_bucket)` | `number` | Sum of event counts over the interval | +| `\|eventCountNonZero(interval, bucket_count, starting_bucket)` | `number` | Number of buckets with at least one event | +| `\|eventAverage(interval, bucket_count, starting_bucket)` | `number` | Average events per bucket | +| `\|eventAveragePerNonZeroInterval(interval, bucket_count, starting_bucket)` | `number` | Average events per non-zero bucket | +| `\|eventLastSeen(interval, starting_bucket)` | `number` | Buckets since the event last occurred | + +**Interval values:** `Minutes`, `Hours`, `Days`, `Weeks`, `Months`, `Years` + +**Examples from targeting configs:** + +``` +// Core active users: opened app at least 21 of the last 28 days +'events.app_opened'|eventCountNonZero('Days', 28, 0) >= 21 + +// Recently logged in: signed into Sync within the last 12 weeks +'sync_auth.sign_in'|eventCountNonZero('Weeks', 12, 0) >= 1 +``` + +### Pre-Computed Event Queries + +The [`RecordedNimbusContext`](https://searchfox.org/mozilla-mobile/source/fenix/app/src/main/java/org/mozilla/fenix/experiments/RecordedNimbusContext.kt) pre-computes one event query and makes it available as a simple numeric attribute: + +| Attribute | Type | Description | Equivalent Event Query | +|-----------|------|-------------|----------------------| +| `events.days_opened_in_last_28` | `number` | Days the app was opened in the last 28 days | `'events.app_opened'\|eventCountNonZero('Days', 28, 0)` | + +## JEXL Expression Syntax + +Nimbus uses [mozjexl](https://github.com/mozilla/mozjexl). The same operators and syntax are available on all platforms — see the [Desktop Targeting Guide](https://github.com/mozilla/mozjexl) for the full JEXL reference. + +### Key Filters for Android + +In addition to the standard filters, the Nimbus SDK provides event query transforms for behavioral targeting: + +| Filter | Description | Example | +|--------|-------------|---------| +| `\|versionCompare` | Compare version strings | `android_sdk_version\|versionCompare('33') >= 0` | +| `\|eventCountNonZero` | Count non-zero event buckets | `'events.app_opened'\|eventCountNonZero('Days', 28, 0) >= 21` | +| `\|eventSum` | Sum event counts over interval | `'events.app_opened'\|eventSum('Days', 7, 0)` | +| `\|eventLastSeen` | Buckets since event last occurred | `'events.app_opened'\|eventLastSeen('Days', 0)` | + +## Sticky Targeting + +Targeting is re-evaluated periodically. If a targeting expression references attributes that can change, a client could be unenrolled. To prevent this, mark the experiment as using **sticky enrollment**. + +On Android, the sticky clause uses `is_already_enrolled`: + +``` +(is_already_enrolled) || () +``` + +The same sticky/non-sticky split applies as on desktop — the same sticky/non-sticky split described in the Sticky Targeting section of the Desktop Targeting Guide applies here. + +## First-Run Targeting + +First-run experiments target users during their very first app session. These use `is_first_run` (or the legacy `isFirstRun == 'true'`). + +``` +// First-run targeting +is_first_run + +// Legacy form (backwards compatibility) +isFirstRun == 'true' + +// First-run on Android 13+ (API 33) +(android_sdk_version|versionCompare('33') >= 0) && is_first_run + +// Combined first-run check (both forms for compatibility) +(isFirstRun == 'true' || is_first_run == true) && days_since_install < 7 +``` + +:::warning +Custom attributes from `CustomAttributeProvider` that require initialization after startup (like `are_notifications_enabled`, `is_default_browser`) are **not available** for first-run targeting. Only attributes set before the Nimbus SDK initializes can be used. +::: + +## Common Targeting Patterns + +### New users (installed less than 7 days ago) + +``` +days_since_install < 7 +``` + +### Existing users (7+ days since install) + +``` +days_since_install >= 7 +``` + +### Recently updated users (not new) + +``` +days_since_update < 7 && days_since_install >= 7 +``` + +### Users in the first 2 weeks + +``` +days_since_install < 15 +``` + +### Core active users (21+ days active in last 28) + +``` +'events.app_opened'|eventCountNonZero('Days', 28, 0) >= 21 +``` + +### Recently logged into Sync + +``` +'sync_auth.sign_in'|eventCountNonZero('Weeks', 12, 0) >= 1 +``` + +### Android version requirements + +``` +// Android 8.0+ (API 26) +android_sdk_version|versionCompare('26') >= 0 + +// Android 10+ (API 29) +android_sdk_version|versionCompare('29') >= 0 + +// Android 13+ (API 33) +android_sdk_version|versionCompare('33') >= 0 +``` + +### Early vs. later app launches + +``` +// First 20 launches +number_of_app_launches <= 20 + +// After 20 launches +number_of_app_launches > 20 +``` + +### Large screen devices + +``` +is_large_device +``` + +### EU DMA browser choice users + +``` +install_referrer_response_utm_source == 'eea-browser-choice' +``` + +### Terms of Use targeting + +``` +// Existing users who haven't accepted ToU +user_accepted_tou == false && days_since_install >= 28 + +// Users who accepted ToU +user_accepted_tou == true + +// ToU experience point tiers +tou_points == 0 +tou_points == 1 +tou_points > 1 +``` + +### Ad blocker detection + +``` +// Has any common ad blocker +'uBlock0@raymondhill.net' in addon_ids +|| '{d10d0bf8-f5b5-c8b4-a8b2-2b9879e08c5d}' in addon_ids +|| 'adguardadblocker@adguard.com' in addon_ids +|| 'firefox@ghostery.com' in addon_ids +``` + +### Mutual exclusion with other experiments + +``` +('other-experiment-slug' in enrollments) == false +``` + +## Recorded Targeting Context (Telemetry) + +Firefox for Android records a snapshot of targeting attribute values via the `nimbus_system.recorded_nimbus_context` Glean metric, submitted in the `nimbus` ping. This is used for: + +- **Population sizing** — estimating how many clients match a targeting expression before launch +- **Debugging** — verifying what attribute values a client had when targeting was evaluated + +The recording logic is in [`RecordedNimbusContext.kt`](https://searchfox.org/mozilla-mobile/source/fenix/app/src/main/java/org/mozilla/fenix/experiments/RecordedNimbusContext.kt). The recorded attributes include: `is_first_run`, `event_query_values.days_opened_in_last_28`, UTM parameters, `android_sdk_version`, `app_version`, `locale`, `days_since_install`, `days_since_update`, `language`, `region`, `device_manufacturer`, `device_model`, `user_accepted_tou`, `no_shortcuts_or_stories_opt_outs`, `addon_ids`, and `tou_points`. + +## Testing & Debugging + +### Nimbus DevTools + +The [Nimbus Developer Tools](https://github.com/mozilla-extensions/nimbus-devtools) can be used for testing targeting on Android via the Nimbus CLI or by connecting to Firefox for Android. See the [Nimbus Developer Tools Guide](/resources/nimbus-devtools-guide) for details. + +### Preview Mode + +You can test experiments using Preview mode: + +1. Set the experiment to Preview in Experimenter +2. In Firefox for Android, navigate to `about:config` and enable the Nimbus preview collection +3. The app will fetch and evaluate the preview recipe + +### Common Mistakes + +- **Using `version` instead of `app_version`** — on Android, the version attribute is `app_version`, not `version` +- **Using `isFirstRun` (string) instead of `is_first_run` (boolean)** — the camelCase form is legacy and compares as a string (`== 'true'`); prefer the snake_case boolean form +- **First-run targeting with late-init attributes** — attributes like `are_notifications_enabled` or `is_default_browser` are not available at first startup +- **Forgetting sticky enrollment** — if your targeting checks a changeable attribute (like `days_since_install`), mark the experiment as sticky +- **Using desktop-style attribute names** — Android uses snake_case (`days_since_install`, `is_first_run`), not camelCase + +## Adding New Targeting Options + +To add a new pre-defined targeting option to the Experimenter dropdown: + +1. **Add to `targeting/constants.py`** — create a new `NimbusTargetingConfig` instance with the JEXL expression, description, and `Application.FENIX.name` in `application_choice_names` +2. **Test locally** — verify the JEXL expression evaluates correctly +3. **Submit a PR** to `mozilla/experimenter` with the new config + +If your targeting requires a **new attribute** that doesn't exist yet: + +1. **Add the attribute** to [`RecordedNimbusContext.kt`](https://searchfox.org/mozilla-mobile/source/fenix/app/src/main/java/org/mozilla/fenix/experiments/RecordedNimbusContext.kt) — add the field, include it in `toJson()` (which makes it available in the targeting context), and include it in `record()` (which records it to Glean for population sizing) +2. **Add a corresponding Glean metric** in the Fenix `metrics.yaml` for the recorded value +3. **If the attribute is only needed at startup** (not for population sizing), add it to [`CustomAttributeProvider.kt`](https://searchfox.org/mozilla-mobile/source/fenix/app/src/main/java/org/mozilla/fenix/messaging/CustomAttributeProvider.kt) instead +4. **Wait for the release train** — the attribute will be available starting in the Firefox for Android version that ships the change +5. **Add the targeting config** to Experimenter's `constants.py` as above + +See [Recording Targeting Context](/advanced/recording-targeting-context) for more details on the process. + +## Further Reading + + +- [Behavioral Targeting](/advanced/behavioral-targeting) — event query transforms and available events +- [Recording Targeting Context](/advanced/recording-targeting-context) — how to add new recorded attributes +- [Nimbus Developer Tools Guide](/resources/nimbus-devtools-guide) — testing and debugging tools From e18e30d8028bd3cbf78e18f9bfa657becafa80b0 Mon Sep 17 00:00:00 2001 From: Jared Lockhart <119884+jaredlockhart@users.noreply.github.com> Date: Thu, 19 Mar 2026 12:21:15 -0400 Subject: [PATCH 02/14] docs(nimbus): replace Android custom targeting doc with comprehensive guide Because * The existing Android custom targeting page was deprecated and only covered how to add new custom attributes * Better to replace it in-place than add a separate document This commit * Replaces the deprecated custom-targeting.md with the comprehensive Fenix Targeting Guide, preserving the existing slug and id so existing links continue to work Co-Authored-By: Claude Opus 4.6 (1M context) --- .../android/custom-targeting.md | 438 ++++++++++++++++-- docs/platform-guides/android/targeting.md | 430 ----------------- 2 files changed, 412 insertions(+), 456 deletions(-) delete mode 100644 docs/platform-guides/android/targeting.md diff --git a/docs/platform-guides/android/custom-targeting.md b/docs/platform-guides/android/custom-targeting.md index 3c3b94b81..ef1056817 100644 --- a/docs/platform-guides/android/custom-targeting.md +++ b/docs/platform-guides/android/custom-targeting.md @@ -1,44 +1,430 @@ --- id: android-custom-targeting -title: Custom Targeting +title: Firefox for Android (Fenix) Targeting Guide slug: /platform-guides/android/custom-targeting --- -:::warning DEPRECATED -**This method of adding new targeting attributes is deprecated. Please use the method described in the [Recorded Targeting Context doc](/advanced/recording-targeting-context#adding-new-fields).** +This guide covers how targeting works for Firefox for Android (Fenix) experiments and rollouts in Nimbus. It explains the available targeting attributes, how to write JEXL expressions, and how to test and debug your targeting. + +## How Targeting Works + +When you create an experiment in Experimenter, you configure **who** should be enrolled. Targeting happens at two levels: + +1. **Basic targeting** (UI fields) — application, channel, Firefox version range, locale, country, language +2. **Advanced targeting** — a [JEXL](https://github.com/mozilla/mozjexl) expression evaluated against the client's targeting context to filter users by install age, device properties, UTM attribution, and more + +Both levels are combined into a single JEXL expression that the Nimbus SDK evaluates on every Firefox for Android installation. If the expression evaluates to `true`, the client is eligible for enrollment. + +### Evaluation Flow + +1. Firefox for Android starts and initializes the Nimbus SDK +2. The SDK fetches experiment recipes from Remote Settings +3. For each experiment, the SDK evaluates the `targeting` JEXL expression against the current targeting context +4. Clients that match targeting and fall into an eligible bucket are enrolled. Existing enrollments that no longer match targeting are unenrolled (unless protected by a [sticky clause](#sticky-targeting)). + +### How Targeting Differs from Desktop + +Firefox for Android uses the cross-platform **Nimbus SDK** (written in Rust), not the desktop-specific Nimbus client. Key differences: + +- **Channel** is determined by the app ID (e.g., `org.mozilla.firefox` for release, `org.mozilla.firefox_beta` for beta), not a `browserSettings.update.channel` attribute +- **Version comparisons** use `app_version|versionCompare(...)` instead of `version|versionCompare(...)` +- **Sticky targeting** uses `is_already_enrolled` instead of `experiment.slug in activeExperiments` +- **Behavioral targeting** via event queries (e.g., `|eventCountNonZero`) is available — this is not available on desktop +- **Language** filtering uses `language in [...]` (two-letter code extracted from locale) + +## Basic Targeting (UI Fields) + +These are configured directly in the Experimenter audience form: + +| Field | Description | +|-------|-------------| +| **Channel** | A single channel: `release`, `beta`, `nightly`, or `developer`. On mobile, channel is determined by the app ID, so each channel is a separate application. | +| **Min/Max Version** | Firefox version range (e.g., 134 to 140). Uses `app_version|versionCompare(...)` internally. | +| **Locales** | Browser locale codes (e.g., `en-US`, `de`). Can include or exclude. | +| **Languages** | Two-letter language codes (e.g., `en`, `de`). Can include or exclude. This is extracted from the locale. | +| **Countries** | Country codes from the locale region (e.g., `US`, `DE`). Can include or exclude. | +| **Population %** | Percentage of eligible users to enroll (bucketing). | + +These fields are translated into JEXL conditions that are combined with any advanced targeting you specify. + +## Advanced Targeting + +Advanced targeting uses pre-defined configurations or custom JEXL expressions. In the Experimenter UI, you select from a dropdown of pre-defined targeting configs, each backed by a JEXL expression. + +These configs are defined in [`targeting/constants.py`](https://github.com/mozilla/experimenter/blob/main/experimenter/experimenter/targeting/constants.py) in the Experimenter repo. To add a new one, see [Adding New Targeting Options](#adding-new-targeting-options) below. + +## Targeting Attributes Reference + +The targeting context for Firefox for Android is assembled from multiple sources: + +1. **App context** — provided by the application at startup (app name, version, channel, device info) +2. **Computed attributes** — calculated by the Nimbus SDK (days since install, language, region) +3. **Recorded context** — app-specific attributes defined in [`RecordedNimbusContext.kt`](https://searchfox.org/mozilla-mobile/source/fenix/app/src/main/java/org/mozilla/fenix/experiments/RecordedNimbusContext.kt) that are recorded to Glean for population sizing +4. **Custom attributes** — additional attributes from [`CustomAttributeProvider.kt`](https://searchfox.org/mozilla-mobile/source/fenix/app/src/main/java/org/mozilla/fenix/messaging/CustomAttributeProvider.kt), available at startup for experiment targeting + +### App & Version + +| Attribute | Type | Description | Example | +|-----------|------|-------------|---------| +| `app_name` | `string` | Application name (always `"fenix"`) | `app_name == 'fenix'` | +| `app_id` | `string` | Application package ID | `app_id == 'org.mozilla.firefox'` | +| `app_version` | `string` | App version string (e.g., `"147.0"`) | `app_version\|versionCompare('134.!') >= 0` | +| `channel` | `string` | Build channel (`release`, `beta`, `nightly`, `developer`) | Usually set via UI, not JEXL | + +:::note +Version targeting is typically set via the Min/Max Version UI fields (which generate `app_version|versionCompare('X.!') >= 0` for min and `app_version|versionCompare('X.*') <= 0` for max automatically). +::: + +### Install & Update + +| Attribute | Type | Description | Example | +|-----------|------|-------------|---------| +| `days_since_install` | `number` | Days since app was first installed | `days_since_install < 7` | +| `days_since_update` | `number` | Days since last app update | `days_since_update < 7 && days_since_install >= 7` | +| `is_first_run` | `boolean` | True during the app's first run | `is_first_run` | +| `number_of_app_launches` | `number` | Total number of app launches | `number_of_app_launches <= 20` | + +:::note +`isFirstRun` (camelCase, string `"true"`/`"false"`) exists for backwards compatibility. Prefer `is_first_run` (snake_case, boolean) for new targeting. +::: + +### Locale & Region + +| Attribute | Type | Description | Example | +|-----------|------|-------------|---------| +| `locale` | `string` | Full locale tag (e.g., `en-US`) | `locale in ['en-US', 'en-GB']` | +| `language` | `string` | Two-letter language code extracted from locale (e.g., `en`) | `language in ['en', 'fr']` | +| `region` | `string` | Country code extracted from locale (e.g., `US`) | `region in ['US', 'CA']` | + +:::note +Locale, language, and region targeting is typically set via the Experimenter UI fields, but can also be used directly in advanced targeting expressions. ::: -## Adding New Targeting Attributes to Android -This page demonstrates how to add new targeting attributes to Android, enabling experiment creators more specific targeting. -For more general documentation on targeting custom audiences, check out [the custom audiences docs](/advanced/custom-audiences) +### Device & OS + +| Attribute | Type | Description | Example | +|-----------|------|-------------|---------| +| `android_sdk_version` | `string` | Android API level as a string (e.g., `"33"` for Android 13) | `android_sdk_version\|versionCompare('33') >= 0` | +| `device_manufacturer` | `string` | Device manufacturer (from `Build.MANUFACTURER`) | `device_manufacturer == 'Google'` | +| `device_model` | `string` | Device model (from `Build.MODEL`) | `device_model == 'Pixel 8'` | +| `is_large_device` | `boolean` | Whether the device has a large screen | `is_large_device` | +| `architecture` | `string` | CPU architecture (e.g., `arm`, `x86`) | | + +### Install Attribution (UTM) + +| Attribute | Type | Description | Example | +|-----------|------|-------------|---------| +| `install_referrer_response_utm_source` | `string` | UTM source from install referrer | `install_referrer_response_utm_source == 'eea-browser-choice'` | +| `install_referrer_response_utm_medium` | `string` | UTM medium | | +| `install_referrer_response_utm_campaign` | `string` | UTM campaign | | +| `install_referrer_response_utm_term` | `string` | UTM term | | +| `install_referrer_response_utm_content` | `string` | UTM content | | + +### Terms of Use & Privacy + +| Attribute | Type | Description | Example | +|-----------|------|-------------|---------| +| `user_accepted_tou` | `boolean` | Whether the user has accepted Terms of Use | `user_accepted_tou == false && days_since_install >= 28` | +| `no_shortcuts_or_stories_opt_outs` | `boolean` | Whether the user has not opted out of sponsored shortcuts/stories | `no_shortcuts_or_stories_opt_outs == true` | +| `tou_points` | `number` | Terms of Use experience points (scoring for ToU targeting tiers) | `tou_points == 0` | + +### Add-ons + +| Attribute | Type | Description | Example | +|-----------|------|-------------|---------| +| `addon_ids` | `string[]` | List of installed add-on IDs | `'uBlock0@raymondhill.net' in addon_ids` | + +**Detecting ad blockers:** + +``` +// Has uBlock Origin installed +'uBlock0@raymondhill.net' in addon_ids + +// Does NOT have any common ad blocker +('uBlock0@raymondhill.net' in addon_ids) == false +&& ('{d10d0bf8-f5b5-c8b4-a8b2-2b9879e08c5d}' in addon_ids) == false +&& ('adguardadblocker@adguard.com' in addon_ids) == false +&& ('firefox@ghostery.com' in addon_ids) == false +``` + +### Experiment & Rollout Enrollment + +| Attribute | Type | Description | Example | +|-----------|------|-------------|---------| +| `is_already_enrolled` | `boolean` | Whether the client is already enrolled in this experiment | Used in sticky clauses | +| `active_experiments` | `string[]` | Currently enrolled experiment slugs | `'my-experiment' in active_experiments` | +| `enrollments` | `string[]` | All experiment enrollments (including past) | `('other-slug' in enrollments) == false` | +| `enrollments_map` | `object` | Experiment slug → branch slug mapping | Used for branch-level exclusion | + +### Additional Attributes (Messaging / Display Triggers) + +These attributes are available from [`CustomAttributeProvider.kt`](https://searchfox.org/mozilla-mobile/source/fenix/app/src/main/java/org/mozilla/fenix/messaging/CustomAttributeProvider.kt) and are primarily used for messaging display triggers but can also be used in experiment targeting: + +| Attribute | Type | Description | Example | +|-----------|------|-------------|---------| +| `is_default_browser` | `boolean` | Whether Firefox is the default browser | `is_default_browser` | +| `are_notifications_enabled` | `boolean` | Whether notification permissions are granted | `are_notifications_enabled` | +| `search_widget_is_installed` | `boolean` | Whether the search widget is on the home screen | `search_widget_is_installed` | +| `is_fxa_signed_in` | `boolean` | Whether the user is signed into Firefox Account | `is_fxa_signed_in` | +| `fxa_connected_devices` | `number` | Number of connected FxA devices | `fxa_connected_devices >= 2` | +| `date_string` | `string` | Current date as `yyyy-MM-dd` | | +| `adjust_campaign` | `string` | Adjust campaign ID | | +| `adjust_network` | `string` | Adjust network | | +| `adjust_ad_group` | `string` | Adjust ad group | | +| `adjust_creative` | `string` | Adjust creative | | -## Adding the Attribute to the Application -The Nimbus SDK exposes a new `customTargetingAttributes` parameter in its initializer that is a `Map` map. We can take advantage of this parameter to pass in new targeting attributes without modifying the Nimbus SDK at all. :::warning -A current limitation is that both the key and the value of the targeting attribute are **strings**. Please reach out to the Nimbus SDK team for any targeting attributes that require integer comparison, or any other richer `JEXL` expressions that cannot be done with strings. +Attributes from `CustomAttributeProvider` are evaluated at startup. Attributes that require initialization after startup (like `are_notifications_enabled`) **cannot** reliably target first-run experiments — they will only be accurate from the second startup onward. ::: +## Behavioral Targeting (Event Queries) + +Firefox for Android supports **behavioral targeting** via event queries — this is a capability unique to the cross-platform Nimbus SDK and is **not available on desktop**. + +Event queries let you target users based on their past behavior by querying the Nimbus event store. Events are bucketed by time interval. + +### Available Events + +| Event | Description | +|-------|-------------| +| `events.app_opened` | Application opened | +| `sync_auth.sign_in` | User signed into Sync | + +### Event Query Transforms + +| Transform | Returns | Description | +|-----------|---------|-------------| +| `\|eventSum(interval, bucket_count, starting_bucket)` | `number` | Sum of event counts over the interval | +| `\|eventCountNonZero(interval, bucket_count, starting_bucket)` | `number` | Number of buckets with at least one event | +| `\|eventAverage(interval, bucket_count, starting_bucket)` | `number` | Average events per bucket | +| `\|eventAveragePerNonZeroInterval(interval, bucket_count, starting_bucket)` | `number` | Average events per non-zero bucket | +| `\|eventLastSeen(interval, starting_bucket)` | `number` | Buckets since the event last occurred | + +**Interval values:** `Minutes`, `Hours`, `Days`, `Weeks`, `Months`, `Years` + +**Examples from targeting configs:** -### How to Add a New Attribute -In [NimbusSetup.kt](https://github.com/mozilla-mobile/fenix/blob/main/app/src/main/java/org/mozilla/fenix/experiments/NimbusSetup.kt#L61) `NimbusAppInfo` now optionally takes in a map `customTargetingAttributes` that will be used to add custom targeting. Simply add a new key-value pair to the map and it will be available for targeting. For example: -```kotlin -val appInfo = NimbusAppInfo( - appName = "fenix", - channel = BuildConfig.BUILD_TYPE, - customTargetingAttributes = mapOf( - "newTargetingAttributeName" to "targetingAttributeValue", - ) -) ``` +// Core active users: opened app at least 21 of the last 28 days +'events.app_opened'|eventCountNonZero('Days', 28, 0) >= 21 + +// Recently logged in: signed into Sync within the last 12 weeks +'sync_auth.sign_in'|eventCountNonZero('Weeks', 12, 0) >= 1 +``` + +### Pre-Computed Event Queries + +The [`RecordedNimbusContext`](https://searchfox.org/mozilla-mobile/source/fenix/app/src/main/java/org/mozilla/fenix/experiments/RecordedNimbusContext.kt) pre-computes one event query and makes it available as a simple numeric attribute: + +| Attribute | Type | Description | Equivalent Event Query | +|-----------|------|-------------|----------------------| +| `events.days_opened_in_last_28` | `number` | Days the app was opened in the last 28 days | `'events.app_opened'\|eventCountNonZero('Days', 28, 0)` | -Note that since we need to add the targeting attributes on the client code, the attribute changes will have to ride the trains before they are available for targeting. +## JEXL Expression Syntax + +Nimbus uses [mozjexl](https://github.com/mozilla/mozjexl). The same operators and syntax are available on all platforms — see the [Desktop Targeting Guide](https://github.com/mozilla/mozjexl) for the full JEXL reference. + +### Key Filters for Android + +In addition to the standard filters, the Nimbus SDK provides event query transforms for behavioral targeting: + +| Filter | Description | Example | +|--------|-------------|---------| +| `\|versionCompare` | Compare version strings | `android_sdk_version\|versionCompare('33') >= 0` | +| `\|eventCountNonZero` | Count non-zero event buckets | `'events.app_opened'\|eventCountNonZero('Days', 28, 0) >= 21` | +| `\|eventSum` | Sum event counts over interval | `'events.app_opened'\|eventSum('Days', 7, 0)` | +| `\|eventLastSeen` | Buckets since event last occurred | `'events.app_opened'\|eventLastSeen('Days', 0)` | + +## Sticky Targeting + +Targeting is re-evaluated periodically. If a targeting expression references attributes that can change, a client could be unenrolled. To prevent this, mark the experiment as using **sticky enrollment**. + +On Android, the sticky clause uses `is_already_enrolled`: + +``` +(is_already_enrolled) || () +``` + +The same sticky/non-sticky split applies as on desktop — the same sticky/non-sticky split described in the Sticky Targeting section of the Desktop Targeting Guide applies here. + +## First-Run Targeting + +First-run experiments target users during their very first app session. These use `is_first_run` (or the legacy `isFirstRun == 'true'`). + +``` +// First-run targeting +is_first_run + +// Legacy form (backwards compatibility) +isFirstRun == 'true' + +// First-run on Android 13+ (API 33) +(android_sdk_version|versionCompare('33') >= 0) && is_first_run + +// Combined first-run check (both forms for compatibility) +(isFirstRun == 'true' || is_first_run == true) && days_since_install < 7 +``` -## Adding the Attribute on Experimenter -After the targeting attribute is ready on the app, you will need to modify experimenter to allow creating experiments that target the attribute you created. Follow the instructions on [the custom audiences page](/advanced/custom-audiences#how-to-add-a-new-custom-audience) to add the new targeting on experimenter. :::warning -The targeting `JEXL` expression on experimenter **must** use the same name as the key given to the SDK. For example, if the app defines a key-value pair, with key `isFirstRun`. experimenter expression must use the same name (i.e `isFirstRun`). +Custom attributes from `CustomAttributeProvider` that require initialization after startup (like `are_notifications_enabled`, `is_default_browser`) are **not available** for first-run targeting. Only attributes set before the Nimbus SDK initializes can be used. ::: -## Example -- Check out this PR for an example on how to add new targeting attributes for Android: https://github.com/mozilla-mobile/fenix/pull/20642 -- Check out this PR for an example on how to add new targeting attributes to experimenter: https://github.com/mozilla/experimenter/pull/6257 +## Common Targeting Patterns + +### New users (installed less than 7 days ago) + +``` +days_since_install < 7 +``` + +### Existing users (7+ days since install) + +``` +days_since_install >= 7 +``` + +### Recently updated users (not new) + +``` +days_since_update < 7 && days_since_install >= 7 +``` + +### Users in the first 2 weeks + +``` +days_since_install < 15 +``` + +### Core active users (21+ days active in last 28) + +``` +'events.app_opened'|eventCountNonZero('Days', 28, 0) >= 21 +``` + +### Recently logged into Sync + +``` +'sync_auth.sign_in'|eventCountNonZero('Weeks', 12, 0) >= 1 +``` + +### Android version requirements + +``` +// Android 8.0+ (API 26) +android_sdk_version|versionCompare('26') >= 0 + +// Android 10+ (API 29) +android_sdk_version|versionCompare('29') >= 0 + +// Android 13+ (API 33) +android_sdk_version|versionCompare('33') >= 0 +``` + +### Early vs. later app launches + +``` +// First 20 launches +number_of_app_launches <= 20 + +// After 20 launches +number_of_app_launches > 20 +``` + +### Large screen devices + +``` +is_large_device +``` + +### EU DMA browser choice users + +``` +install_referrer_response_utm_source == 'eea-browser-choice' +``` + +### Terms of Use targeting + +``` +// Existing users who haven't accepted ToU +user_accepted_tou == false && days_since_install >= 28 + +// Users who accepted ToU +user_accepted_tou == true + +// ToU experience point tiers +tou_points == 0 +tou_points == 1 +tou_points > 1 +``` + +### Ad blocker detection + +``` +// Has any common ad blocker +'uBlock0@raymondhill.net' in addon_ids +|| '{d10d0bf8-f5b5-c8b4-a8b2-2b9879e08c5d}' in addon_ids +|| 'adguardadblocker@adguard.com' in addon_ids +|| 'firefox@ghostery.com' in addon_ids +``` + +### Mutual exclusion with other experiments + +``` +('other-experiment-slug' in enrollments) == false +``` + +## Recorded Targeting Context (Telemetry) + +Firefox for Android records a snapshot of targeting attribute values via the `nimbus_system.recorded_nimbus_context` Glean metric, submitted in the `nimbus` ping. This is used for: + +- **Population sizing** — estimating how many clients match a targeting expression before launch +- **Debugging** — verifying what attribute values a client had when targeting was evaluated + +The recording logic is in [`RecordedNimbusContext.kt`](https://searchfox.org/mozilla-mobile/source/fenix/app/src/main/java/org/mozilla/fenix/experiments/RecordedNimbusContext.kt). The recorded attributes include: `is_first_run`, `event_query_values.days_opened_in_last_28`, UTM parameters, `android_sdk_version`, `app_version`, `locale`, `days_since_install`, `days_since_update`, `language`, `region`, `device_manufacturer`, `device_model`, `user_accepted_tou`, `no_shortcuts_or_stories_opt_outs`, `addon_ids`, and `tou_points`. + +## Testing & Debugging + +### Nimbus DevTools + +The [Nimbus Developer Tools](https://github.com/mozilla-extensions/nimbus-devtools) can be used for testing targeting on Android via the Nimbus CLI or by connecting to Firefox for Android. See the [Nimbus Developer Tools Guide](/resources/nimbus-devtools-guide) for details. + +### Preview Mode + +You can test experiments using Preview mode: + +1. Set the experiment to Preview in Experimenter +2. In Firefox for Android, navigate to `about:config` and enable the Nimbus preview collection +3. The app will fetch and evaluate the preview recipe + +### Common Mistakes + +- **Using `version` instead of `app_version`** — on Android, the version attribute is `app_version`, not `version` +- **Using `isFirstRun` (string) instead of `is_first_run` (boolean)** — the camelCase form is legacy and compares as a string (`== 'true'`); prefer the snake_case boolean form +- **First-run targeting with late-init attributes** — attributes like `are_notifications_enabled` or `is_default_browser` are not available at first startup +- **Forgetting sticky enrollment** — if your targeting checks a changeable attribute (like `days_since_install`), mark the experiment as sticky +- **Using desktop-style attribute names** — Android uses snake_case (`days_since_install`, `is_first_run`), not camelCase + +## Adding New Targeting Options + +To add a new pre-defined targeting option to the Experimenter dropdown: + +1. **Add to `targeting/constants.py`** — create a new `NimbusTargetingConfig` instance with the JEXL expression, description, and `Application.FENIX.name` in `application_choice_names` +2. **Test locally** — verify the JEXL expression evaluates correctly +3. **Submit a PR** to `mozilla/experimenter` with the new config + +If your targeting requires a **new attribute** that doesn't exist yet: + +1. **Add the attribute** to [`RecordedNimbusContext.kt`](https://searchfox.org/mozilla-mobile/source/fenix/app/src/main/java/org/mozilla/fenix/experiments/RecordedNimbusContext.kt) — add the field, include it in `toJson()` (which makes it available in the targeting context), and include it in `record()` (which records it to Glean for population sizing) +2. **Add a corresponding Glean metric** in the Fenix `metrics.yaml` for the recorded value +3. **If the attribute is only needed at startup** (not for population sizing), add it to [`CustomAttributeProvider.kt`](https://searchfox.org/mozilla-mobile/source/fenix/app/src/main/java/org/mozilla/fenix/messaging/CustomAttributeProvider.kt) instead +4. **Wait for the release train** — the attribute will be available starting in the Firefox for Android version that ships the change +5. **Add the targeting config** to Experimenter's `constants.py` as above + +See [Recording Targeting Context](/advanced/recording-targeting-context) for more details on the process. + +## Further Reading + + +- [Behavioral Targeting](/advanced/behavioral-targeting) — event query transforms and available events +- [Recording Targeting Context](/advanced/recording-targeting-context) — how to add new recorded attributes +- [Nimbus Developer Tools Guide](/resources/nimbus-devtools-guide) — testing and debugging tools diff --git a/docs/platform-guides/android/targeting.md b/docs/platform-guides/android/targeting.md deleted file mode 100644 index 7dd846989..000000000 --- a/docs/platform-guides/android/targeting.md +++ /dev/null @@ -1,430 +0,0 @@ ---- -id: fenix-targeting -title: Firefox for Android (Fenix) Targeting Guide -slug: /platform-guides/android/targeting ---- - -This guide covers how targeting works for Firefox for Android (Fenix) experiments and rollouts in Nimbus. It explains the available targeting attributes, how to write JEXL expressions, and how to test and debug your targeting. - -## How Targeting Works - -When you create an experiment in Experimenter, you configure **who** should be enrolled. Targeting happens at two levels: - -1. **Basic targeting** (UI fields) — application, channel, Firefox version range, locale, country, language -2. **Advanced targeting** — a [JEXL](https://github.com/mozilla/mozjexl) expression evaluated against the client's targeting context to filter users by install age, device properties, UTM attribution, and more - -Both levels are combined into a single JEXL expression that the Nimbus SDK evaluates on every Firefox for Android installation. If the expression evaluates to `true`, the client is eligible for enrollment. - -### Evaluation Flow - -1. Firefox for Android starts and initializes the Nimbus SDK -2. The SDK fetches experiment recipes from Remote Settings -3. For each experiment, the SDK evaluates the `targeting` JEXL expression against the current targeting context -4. Clients that match targeting and fall into an eligible bucket are enrolled. Existing enrollments that no longer match targeting are unenrolled (unless protected by a [sticky clause](#sticky-targeting)). - -### How Targeting Differs from Desktop - -Firefox for Android uses the cross-platform **Nimbus SDK** (written in Rust), not the desktop-specific Nimbus client. Key differences: - -- **Channel** is determined by the app ID (e.g., `org.mozilla.firefox` for release, `org.mozilla.firefox_beta` for beta), not a `browserSettings.update.channel` attribute -- **Version comparisons** use `app_version|versionCompare(...)` instead of `version|versionCompare(...)` -- **Sticky targeting** uses `is_already_enrolled` instead of `experiment.slug in activeExperiments` -- **Behavioral targeting** via event queries (e.g., `|eventCountNonZero`) is available — this is not available on desktop -- **Language** filtering uses `language in [...]` (two-letter code extracted from locale) - -## Basic Targeting (UI Fields) - -These are configured directly in the Experimenter audience form: - -| Field | Description | -|-------|-------------| -| **Channel** | A single channel: `release`, `beta`, `nightly`, or `developer`. On mobile, channel is determined by the app ID, so each channel is a separate application. | -| **Min/Max Version** | Firefox version range (e.g., 134 to 140). Uses `app_version|versionCompare(...)` internally. | -| **Locales** | Browser locale codes (e.g., `en-US`, `de`). Can include or exclude. | -| **Languages** | Two-letter language codes (e.g., `en`, `de`). Can include or exclude. This is extracted from the locale. | -| **Countries** | Country codes from the locale region (e.g., `US`, `DE`). Can include or exclude. | -| **Population %** | Percentage of eligible users to enroll (bucketing). | - -These fields are translated into JEXL conditions that are combined with any advanced targeting you specify. - -## Advanced Targeting - -Advanced targeting uses pre-defined configurations or custom JEXL expressions. In the Experimenter UI, you select from a dropdown of pre-defined targeting configs, each backed by a JEXL expression. - -These configs are defined in [`targeting/constants.py`](https://github.com/mozilla/experimenter/blob/main/experimenter/experimenter/targeting/constants.py) in the Experimenter repo. To add a new one, see [Adding New Targeting Options](#adding-new-targeting-options) below. - -## Targeting Attributes Reference - -The targeting context for Firefox for Android is assembled from multiple sources: - -1. **App context** — provided by the application at startup (app name, version, channel, device info) -2. **Computed attributes** — calculated by the Nimbus SDK (days since install, language, region) -3. **Recorded context** — app-specific attributes defined in [`RecordedNimbusContext.kt`](https://searchfox.org/mozilla-mobile/source/fenix/app/src/main/java/org/mozilla/fenix/experiments/RecordedNimbusContext.kt) that are recorded to Glean for population sizing -4. **Custom attributes** — additional attributes from [`CustomAttributeProvider.kt`](https://searchfox.org/mozilla-mobile/source/fenix/app/src/main/java/org/mozilla/fenix/messaging/CustomAttributeProvider.kt), available at startup for experiment targeting - -### App & Version - -| Attribute | Type | Description | Example | -|-----------|------|-------------|---------| -| `app_name` | `string` | Application name (always `"fenix"`) | `app_name == 'fenix'` | -| `app_id` | `string` | Application package ID | `app_id == 'org.mozilla.firefox'` | -| `app_version` | `string` | App version string (e.g., `"147.0"`) | `app_version\|versionCompare('134.!') >= 0` | -| `channel` | `string` | Build channel (`release`, `beta`, `nightly`, `developer`) | Usually set via UI, not JEXL | - -:::note -Version targeting is typically set via the Min/Max Version UI fields (which generate `app_version|versionCompare('X.!') >= 0` for min and `app_version|versionCompare('X.*') <= 0` for max automatically). -::: - -### Install & Update - -| Attribute | Type | Description | Example | -|-----------|------|-------------|---------| -| `days_since_install` | `number` | Days since app was first installed | `days_since_install < 7` | -| `days_since_update` | `number` | Days since last app update | `days_since_update < 7 && days_since_install >= 7` | -| `is_first_run` | `boolean` | True during the app's first run | `is_first_run` | -| `number_of_app_launches` | `number` | Total number of app launches | `number_of_app_launches <= 20` | - -:::note -`isFirstRun` (camelCase, string `"true"`/`"false"`) exists for backwards compatibility. Prefer `is_first_run` (snake_case, boolean) for new targeting. -::: - -### Locale & Region - -| Attribute | Type | Description | Example | -|-----------|------|-------------|---------| -| `locale` | `string` | Full locale tag (e.g., `en-US`) | `locale in ['en-US', 'en-GB']` | -| `language` | `string` | Two-letter language code extracted from locale (e.g., `en`) | `language in ['en', 'fr']` | -| `region` | `string` | Country code extracted from locale (e.g., `US`) | `region in ['US', 'CA']` | - -:::note -Locale, language, and region targeting is typically set via the Experimenter UI fields, but can also be used directly in advanced targeting expressions. -::: - -### Device & OS - -| Attribute | Type | Description | Example | -|-----------|------|-------------|---------| -| `android_sdk_version` | `string` | Android API level as a string (e.g., `"33"` for Android 13) | `android_sdk_version\|versionCompare('33') >= 0` | -| `device_manufacturer` | `string` | Device manufacturer (from `Build.MANUFACTURER`) | `device_manufacturer == 'Google'` | -| `device_model` | `string` | Device model (from `Build.MODEL`) | `device_model == 'Pixel 8'` | -| `is_large_device` | `boolean` | Whether the device has a large screen | `is_large_device` | -| `architecture` | `string` | CPU architecture (e.g., `arm`, `x86`) | | - -### Install Attribution (UTM) - -| Attribute | Type | Description | Example | -|-----------|------|-------------|---------| -| `install_referrer_response_utm_source` | `string` | UTM source from install referrer | `install_referrer_response_utm_source == 'eea-browser-choice'` | -| `install_referrer_response_utm_medium` | `string` | UTM medium | | -| `install_referrer_response_utm_campaign` | `string` | UTM campaign | | -| `install_referrer_response_utm_term` | `string` | UTM term | | -| `install_referrer_response_utm_content` | `string` | UTM content | | - -### Terms of Use & Privacy - -| Attribute | Type | Description | Example | -|-----------|------|-------------|---------| -| `user_accepted_tou` | `boolean` | Whether the user has accepted Terms of Use | `user_accepted_tou == false && days_since_install >= 28` | -| `no_shortcuts_or_stories_opt_outs` | `boolean` | Whether the user has not opted out of sponsored shortcuts/stories | `no_shortcuts_or_stories_opt_outs == true` | -| `tou_points` | `number` | Terms of Use experience points (scoring for ToU targeting tiers) | `tou_points == 0` | - -### Add-ons - -| Attribute | Type | Description | Example | -|-----------|------|-------------|---------| -| `addon_ids` | `string[]` | List of installed add-on IDs | `'uBlock0@raymondhill.net' in addon_ids` | - -**Detecting ad blockers:** - -``` -// Has uBlock Origin installed -'uBlock0@raymondhill.net' in addon_ids - -// Does NOT have any common ad blocker -('uBlock0@raymondhill.net' in addon_ids) == false -&& ('{d10d0bf8-f5b5-c8b4-a8b2-2b9879e08c5d}' in addon_ids) == false -&& ('adguardadblocker@adguard.com' in addon_ids) == false -&& ('firefox@ghostery.com' in addon_ids) == false -``` - -### Experiment & Rollout Enrollment - -| Attribute | Type | Description | Example | -|-----------|------|-------------|---------| -| `is_already_enrolled` | `boolean` | Whether the client is already enrolled in this experiment | Used in sticky clauses | -| `active_experiments` | `string[]` | Currently enrolled experiment slugs | `'my-experiment' in active_experiments` | -| `enrollments` | `string[]` | All experiment enrollments (including past) | `('other-slug' in enrollments) == false` | -| `enrollments_map` | `object` | Experiment slug → branch slug mapping | Used for branch-level exclusion | - -### Additional Attributes (Messaging / Display Triggers) - -These attributes are available from [`CustomAttributeProvider.kt`](https://searchfox.org/mozilla-mobile/source/fenix/app/src/main/java/org/mozilla/fenix/messaging/CustomAttributeProvider.kt) and are primarily used for messaging display triggers but can also be used in experiment targeting: - -| Attribute | Type | Description | Example | -|-----------|------|-------------|---------| -| `is_default_browser` | `boolean` | Whether Firefox is the default browser | `is_default_browser` | -| `are_notifications_enabled` | `boolean` | Whether notification permissions are granted | `are_notifications_enabled` | -| `search_widget_is_installed` | `boolean` | Whether the search widget is on the home screen | `search_widget_is_installed` | -| `is_fxa_signed_in` | `boolean` | Whether the user is signed into Firefox Account | `is_fxa_signed_in` | -| `fxa_connected_devices` | `number` | Number of connected FxA devices | `fxa_connected_devices >= 2` | -| `date_string` | `string` | Current date as `yyyy-MM-dd` | | -| `adjust_campaign` | `string` | Adjust campaign ID | | -| `adjust_network` | `string` | Adjust network | | -| `adjust_ad_group` | `string` | Adjust ad group | | -| `adjust_creative` | `string` | Adjust creative | | - -:::warning -Attributes from `CustomAttributeProvider` are evaluated at startup. Attributes that require initialization after startup (like `are_notifications_enabled`) **cannot** reliably target first-run experiments — they will only be accurate from the second startup onward. -::: - -## Behavioral Targeting (Event Queries) - -Firefox for Android supports **behavioral targeting** via event queries — this is a capability unique to the cross-platform Nimbus SDK and is **not available on desktop**. - -Event queries let you target users based on their past behavior by querying the Nimbus event store. Events are bucketed by time interval. - -### Available Events - -| Event | Description | -|-------|-------------| -| `events.app_opened` | Application opened | -| `sync_auth.sign_in` | User signed into Sync | - -### Event Query Transforms - -| Transform | Returns | Description | -|-----------|---------|-------------| -| `\|eventSum(interval, bucket_count, starting_bucket)` | `number` | Sum of event counts over the interval | -| `\|eventCountNonZero(interval, bucket_count, starting_bucket)` | `number` | Number of buckets with at least one event | -| `\|eventAverage(interval, bucket_count, starting_bucket)` | `number` | Average events per bucket | -| `\|eventAveragePerNonZeroInterval(interval, bucket_count, starting_bucket)` | `number` | Average events per non-zero bucket | -| `\|eventLastSeen(interval, starting_bucket)` | `number` | Buckets since the event last occurred | - -**Interval values:** `Minutes`, `Hours`, `Days`, `Weeks`, `Months`, `Years` - -**Examples from targeting configs:** - -``` -// Core active users: opened app at least 21 of the last 28 days -'events.app_opened'|eventCountNonZero('Days', 28, 0) >= 21 - -// Recently logged in: signed into Sync within the last 12 weeks -'sync_auth.sign_in'|eventCountNonZero('Weeks', 12, 0) >= 1 -``` - -### Pre-Computed Event Queries - -The [`RecordedNimbusContext`](https://searchfox.org/mozilla-mobile/source/fenix/app/src/main/java/org/mozilla/fenix/experiments/RecordedNimbusContext.kt) pre-computes one event query and makes it available as a simple numeric attribute: - -| Attribute | Type | Description | Equivalent Event Query | -|-----------|------|-------------|----------------------| -| `events.days_opened_in_last_28` | `number` | Days the app was opened in the last 28 days | `'events.app_opened'\|eventCountNonZero('Days', 28, 0)` | - -## JEXL Expression Syntax - -Nimbus uses [mozjexl](https://github.com/mozilla/mozjexl). The same operators and syntax are available on all platforms — see the [Desktop Targeting Guide](https://github.com/mozilla/mozjexl) for the full JEXL reference. - -### Key Filters for Android - -In addition to the standard filters, the Nimbus SDK provides event query transforms for behavioral targeting: - -| Filter | Description | Example | -|--------|-------------|---------| -| `\|versionCompare` | Compare version strings | `android_sdk_version\|versionCompare('33') >= 0` | -| `\|eventCountNonZero` | Count non-zero event buckets | `'events.app_opened'\|eventCountNonZero('Days', 28, 0) >= 21` | -| `\|eventSum` | Sum event counts over interval | `'events.app_opened'\|eventSum('Days', 7, 0)` | -| `\|eventLastSeen` | Buckets since event last occurred | `'events.app_opened'\|eventLastSeen('Days', 0)` | - -## Sticky Targeting - -Targeting is re-evaluated periodically. If a targeting expression references attributes that can change, a client could be unenrolled. To prevent this, mark the experiment as using **sticky enrollment**. - -On Android, the sticky clause uses `is_already_enrolled`: - -``` -(is_already_enrolled) || () -``` - -The same sticky/non-sticky split applies as on desktop — the same sticky/non-sticky split described in the Sticky Targeting section of the Desktop Targeting Guide applies here. - -## First-Run Targeting - -First-run experiments target users during their very first app session. These use `is_first_run` (or the legacy `isFirstRun == 'true'`). - -``` -// First-run targeting -is_first_run - -// Legacy form (backwards compatibility) -isFirstRun == 'true' - -// First-run on Android 13+ (API 33) -(android_sdk_version|versionCompare('33') >= 0) && is_first_run - -// Combined first-run check (both forms for compatibility) -(isFirstRun == 'true' || is_first_run == true) && days_since_install < 7 -``` - -:::warning -Custom attributes from `CustomAttributeProvider` that require initialization after startup (like `are_notifications_enabled`, `is_default_browser`) are **not available** for first-run targeting. Only attributes set before the Nimbus SDK initializes can be used. -::: - -## Common Targeting Patterns - -### New users (installed less than 7 days ago) - -``` -days_since_install < 7 -``` - -### Existing users (7+ days since install) - -``` -days_since_install >= 7 -``` - -### Recently updated users (not new) - -``` -days_since_update < 7 && days_since_install >= 7 -``` - -### Users in the first 2 weeks - -``` -days_since_install < 15 -``` - -### Core active users (21+ days active in last 28) - -``` -'events.app_opened'|eventCountNonZero('Days', 28, 0) >= 21 -``` - -### Recently logged into Sync - -``` -'sync_auth.sign_in'|eventCountNonZero('Weeks', 12, 0) >= 1 -``` - -### Android version requirements - -``` -// Android 8.0+ (API 26) -android_sdk_version|versionCompare('26') >= 0 - -// Android 10+ (API 29) -android_sdk_version|versionCompare('29') >= 0 - -// Android 13+ (API 33) -android_sdk_version|versionCompare('33') >= 0 -``` - -### Early vs. later app launches - -``` -// First 20 launches -number_of_app_launches <= 20 - -// After 20 launches -number_of_app_launches > 20 -``` - -### Large screen devices - -``` -is_large_device -``` - -### EU DMA browser choice users - -``` -install_referrer_response_utm_source == 'eea-browser-choice' -``` - -### Terms of Use targeting - -``` -// Existing users who haven't accepted ToU -user_accepted_tou == false && days_since_install >= 28 - -// Users who accepted ToU -user_accepted_tou == true - -// ToU experience point tiers -tou_points == 0 -tou_points == 1 -tou_points > 1 -``` - -### Ad blocker detection - -``` -// Has any common ad blocker -'uBlock0@raymondhill.net' in addon_ids -|| '{d10d0bf8-f5b5-c8b4-a8b2-2b9879e08c5d}' in addon_ids -|| 'adguardadblocker@adguard.com' in addon_ids -|| 'firefox@ghostery.com' in addon_ids -``` - -### Mutual exclusion with other experiments - -``` -('other-experiment-slug' in enrollments) == false -``` - -## Recorded Targeting Context (Telemetry) - -Firefox for Android records a snapshot of targeting attribute values via the `nimbus_system.recorded_nimbus_context` Glean metric, submitted in the `nimbus` ping. This is used for: - -- **Population sizing** — estimating how many clients match a targeting expression before launch -- **Debugging** — verifying what attribute values a client had when targeting was evaluated - -The recording logic is in [`RecordedNimbusContext.kt`](https://searchfox.org/mozilla-mobile/source/fenix/app/src/main/java/org/mozilla/fenix/experiments/RecordedNimbusContext.kt). The recorded attributes include: `is_first_run`, `event_query_values.days_opened_in_last_28`, UTM parameters, `android_sdk_version`, `app_version`, `locale`, `days_since_install`, `days_since_update`, `language`, `region`, `device_manufacturer`, `device_model`, `user_accepted_tou`, `no_shortcuts_or_stories_opt_outs`, `addon_ids`, and `tou_points`. - -## Testing & Debugging - -### Nimbus DevTools - -The [Nimbus Developer Tools](https://github.com/mozilla-extensions/nimbus-devtools) can be used for testing targeting on Android via the Nimbus CLI or by connecting to Firefox for Android. See the [Nimbus Developer Tools Guide](/resources/nimbus-devtools-guide) for details. - -### Preview Mode - -You can test experiments using Preview mode: - -1. Set the experiment to Preview in Experimenter -2. In Firefox for Android, navigate to `about:config` and enable the Nimbus preview collection -3. The app will fetch and evaluate the preview recipe - -### Common Mistakes - -- **Using `version` instead of `app_version`** — on Android, the version attribute is `app_version`, not `version` -- **Using `isFirstRun` (string) instead of `is_first_run` (boolean)** — the camelCase form is legacy and compares as a string (`== 'true'`); prefer the snake_case boolean form -- **First-run targeting with late-init attributes** — attributes like `are_notifications_enabled` or `is_default_browser` are not available at first startup -- **Forgetting sticky enrollment** — if your targeting checks a changeable attribute (like `days_since_install`), mark the experiment as sticky -- **Using desktop-style attribute names** — Android uses snake_case (`days_since_install`, `is_first_run`), not camelCase - -## Adding New Targeting Options - -To add a new pre-defined targeting option to the Experimenter dropdown: - -1. **Add to `targeting/constants.py`** — create a new `NimbusTargetingConfig` instance with the JEXL expression, description, and `Application.FENIX.name` in `application_choice_names` -2. **Test locally** — verify the JEXL expression evaluates correctly -3. **Submit a PR** to `mozilla/experimenter` with the new config - -If your targeting requires a **new attribute** that doesn't exist yet: - -1. **Add the attribute** to [`RecordedNimbusContext.kt`](https://searchfox.org/mozilla-mobile/source/fenix/app/src/main/java/org/mozilla/fenix/experiments/RecordedNimbusContext.kt) — add the field, include it in `toJson()` (which makes it available in the targeting context), and include it in `record()` (which records it to Glean for population sizing) -2. **Add a corresponding Glean metric** in the Fenix `metrics.yaml` for the recorded value -3. **If the attribute is only needed at startup** (not for population sizing), add it to [`CustomAttributeProvider.kt`](https://searchfox.org/mozilla-mobile/source/fenix/app/src/main/java/org/mozilla/fenix/messaging/CustomAttributeProvider.kt) instead -4. **Wait for the release train** — the attribute will be available starting in the Firefox for Android version that ships the change -5. **Add the targeting config** to Experimenter's `constants.py` as above - -See [Recording Targeting Context](/advanced/recording-targeting-context) for more details on the process. - -## Further Reading - - -- [Behavioral Targeting](/advanced/behavioral-targeting) — event query transforms and available events -- [Recording Targeting Context](/advanced/recording-targeting-context) — how to add new recorded attributes -- [Nimbus Developer Tools Guide](/resources/nimbus-devtools-guide) — testing and debugging tools From 28c3312374cfaafad128678b3291adb9833b6e93 Mon Sep 17 00:00:00 2001 From: Jared Lockhart <119884+jaredlockhart@users.noreply.github.com> Date: Thu, 19 Mar 2026 12:21:55 -0400 Subject: [PATCH 03/14] docs(nimbus): include legacy customTargetingAttributes content from original doc Because * The original custom-targeting.md had content about the deprecated customTargetingAttributes approach that should be preserved This commit * Adds a deprecation warning block with the legacy customTargetingAttributes code example and a pointer to the modern RecordedNimbusContext approach * Adds link to Custom Audiences doc in Further Reading Co-Authored-By: Claude Opus 4.6 (1M context) --- .../android/custom-targeting.md | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/docs/platform-guides/android/custom-targeting.md b/docs/platform-guides/android/custom-targeting.md index ef1056817..da0efce82 100644 --- a/docs/platform-guides/android/custom-targeting.md +++ b/docs/platform-guides/android/custom-targeting.md @@ -420,11 +420,27 @@ If your targeting requires a **new attribute** that doesn't exist yet: 4. **Wait for the release train** — the attribute will be available starting in the Firefox for Android version that ships the change 5. **Add the targeting config** to Experimenter's `constants.py` as above -See [Recording Targeting Context](/advanced/recording-targeting-context) for more details on the process. +See [Recording Targeting Context](/advanced/recording-targeting-context) and [Custom Audiences](/advanced/custom-audiences) for more details on the process. -## Further Reading +:::warning Legacy: customTargetingAttributes +An older method of adding targeting attributes used the `customTargetingAttributes` parameter on `NimbusAppInfo`: + +```kotlin +val appInfo = NimbusAppInfo( + appName = "fenix", + channel = BuildConfig.BUILD_TYPE, + customTargetingAttributes = mapOf( + "newTargetingAttributeName" to "targetingAttributeValue", + ) +) +``` +**This approach is deprecated.** It only supports string key/value pairs and does not record values to Glean for population sizing. Use `RecordedNimbusContext` instead, as described above. See the [Recorded Targeting Context doc](/advanced/recording-targeting-context#adding-new-fields) for the current method. +::: + +## Further Reading - [Behavioral Targeting](/advanced/behavioral-targeting) — event query transforms and available events - [Recording Targeting Context](/advanced/recording-targeting-context) — how to add new recorded attributes +- [Custom Audiences](/advanced/custom-audiences) — adding new targeting options to the Experimenter dropdown - [Nimbus Developer Tools Guide](/resources/nimbus-devtools-guide) — testing and debugging tools From c4210f5c932729d0451767ba5b491f4af26d0cfa Mon Sep 17 00:00:00 2001 From: Jared Lockhart <119884+jaredlockhart@users.noreply.github.com> Date: Thu, 19 Mar 2026 12:26:10 -0400 Subject: [PATCH 04/14] docs(nimbus): remove 'how it differs from desktop' section Because * Each platform guide should be self-contained and focused on its own application, not defined relative to another This commit * Removes the 'How Targeting Differs from Desktop' section Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/platform-guides/android/custom-targeting.md | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/docs/platform-guides/android/custom-targeting.md b/docs/platform-guides/android/custom-targeting.md index da0efce82..a40c3e9af 100644 --- a/docs/platform-guides/android/custom-targeting.md +++ b/docs/platform-guides/android/custom-targeting.md @@ -22,16 +22,6 @@ Both levels are combined into a single JEXL expression that the Nimbus SDK evalu 3. For each experiment, the SDK evaluates the `targeting` JEXL expression against the current targeting context 4. Clients that match targeting and fall into an eligible bucket are enrolled. Existing enrollments that no longer match targeting are unenrolled (unless protected by a [sticky clause](#sticky-targeting)). -### How Targeting Differs from Desktop - -Firefox for Android uses the cross-platform **Nimbus SDK** (written in Rust), not the desktop-specific Nimbus client. Key differences: - -- **Channel** is determined by the app ID (e.g., `org.mozilla.firefox` for release, `org.mozilla.firefox_beta` for beta), not a `browserSettings.update.channel` attribute -- **Version comparisons** use `app_version|versionCompare(...)` instead of `version|versionCompare(...)` -- **Sticky targeting** uses `is_already_enrolled` instead of `experiment.slug in activeExperiments` -- **Behavioral targeting** via event queries (e.g., `|eventCountNonZero`) is available — this is not available on desktop -- **Language** filtering uses `language in [...]` (two-letter code extracted from locale) - ## Basic Targeting (UI Fields) These are configured directly in the Experimenter audience form: From 93d9a9185bcc0cd6738fbd9ab94510e120fe9579 Mon Sep 17 00:00:00 2001 From: Jared Lockhart <119884+jaredlockhart@users.noreply.github.com> Date: Thu, 19 Mar 2026 12:48:18 -0400 Subject: [PATCH 05/14] docs(nimbus): fix locale vs language for mobile targeting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Because * The Experimenter UI shows Languages (two-letter codes) for mobile and Locales (full locale codes) for desktop — they are mutually exclusive fields in the same UI slot * The Fenix guide incorrectly listed Locales as a basic targeting field This commit * Removes Locales from the Basic Targeting table, keeping only Languages * Renames the "Locale & Region" section to "Language & Region" and reorders to put language first * Notes that locale is available in the targeting context but not used by the Experimenter UI for mobile Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/platform-guides/android/custom-targeting.md | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/docs/platform-guides/android/custom-targeting.md b/docs/platform-guides/android/custom-targeting.md index a40c3e9af..2af6f5fa4 100644 --- a/docs/platform-guides/android/custom-targeting.md +++ b/docs/platform-guides/android/custom-targeting.md @@ -28,11 +28,10 @@ These are configured directly in the Experimenter audience form: | Field | Description | |-------|-------------| -| **Channel** | A single channel: `release`, `beta`, `nightly`, or `developer`. On mobile, channel is determined by the app ID, so each channel is a separate application. | +| **Channel** | A single channel: `release`, `beta`, `nightly`, or `developer`. On mobile, channel is determined by the app ID (e.g., `org.mozilla.firefox` for release), so each channel is a separate application. | | **Min/Max Version** | Firefox version range (e.g., 134 to 140). Uses `app_version|versionCompare(...)` internally. | -| **Locales** | Browser locale codes (e.g., `en-US`, `de`). Can include or exclude. | -| **Languages** | Two-letter language codes (e.g., `en`, `de`). Can include or exclude. This is extracted from the locale. | -| **Countries** | Country codes from the locale region (e.g., `US`, `DE`). Can include or exclude. | +| **Languages** | Two-letter language codes (e.g., `en`, `de`). Can include or exclude. Extracted from the device locale. (Desktop uses full locale codes instead; mobile uses languages.) | +| **Countries** | Country codes extracted from the locale region (e.g., `US`, `DE`). Can include or exclude. | | **Population %** | Percentage of eligible users to enroll (bucketing). | These fields are translated into JEXL conditions that are combined with any advanced targeting you specify. @@ -78,16 +77,16 @@ Version targeting is typically set via the Min/Max Version UI fields (which gene `isFirstRun` (camelCase, string `"true"`/`"false"`) exists for backwards compatibility. Prefer `is_first_run` (snake_case, boolean) for new targeting. ::: -### Locale & Region +### Language & Region | Attribute | Type | Description | Example | |-----------|------|-------------|---------| -| `locale` | `string` | Full locale tag (e.g., `en-US`) | `locale in ['en-US', 'en-GB']` | | `language` | `string` | Two-letter language code extracted from locale (e.g., `en`) | `language in ['en', 'fr']` | | `region` | `string` | Country code extracted from locale (e.g., `US`) | `region in ['US', 'CA']` | +| `locale` | `string` | Full locale tag (e.g., `en-US`) | Available in the targeting context but not used by the Experimenter UI for mobile — use `language` instead | :::note -Locale, language, and region targeting is typically set via the Experimenter UI fields, but can also be used directly in advanced targeting expressions. +Language and region targeting is typically set via the Experimenter UI fields (which generate `language in [...]` / `region in [...]` expressions automatically), but can also be used directly in advanced targeting expressions. ::: ### Device & OS From 8031b5ff0176f4bde0fb4bd0879f06d06e8a5217 Mon Sep 17 00:00:00 2001 From: Jared Lockhart <119884+jaredlockhart@users.noreply.github.com> Date: Thu, 19 Mar 2026 12:51:09 -0400 Subject: [PATCH 06/14] docs(nimbus): verify Fenix targeting against model tests Because * Needed to ensure all targeting details match what the experimenter model actually generates for Fenix, as confirmed by the targeting tests This commit * Expands the sticky targeting section with the concrete mobile sticky/non-sticky table and a real example from the test suite * Adds note that version targeting expressions are only included for Firefox for Android 98+ Co-Authored-By: Claude Opus 4.6 (1M context) --- .../android/custom-targeting.md | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/docs/platform-guides/android/custom-targeting.md b/docs/platform-guides/android/custom-targeting.md index 2af6f5fa4..00097cb3a 100644 --- a/docs/platform-guides/android/custom-targeting.md +++ b/docs/platform-guides/android/custom-targeting.md @@ -61,7 +61,7 @@ The targeting context for Firefox for Android is assembled from multiple sources | `channel` | `string` | Build channel (`release`, `beta`, `nightly`, `developer`) | Usually set via UI, not JEXL | :::note -Version targeting is typically set via the Min/Max Version UI fields (which generate `app_version|versionCompare('X.!') >= 0` for min and `app_version|versionCompare('X.*') <= 0` for max automatically). +Version targeting is typically set via the Min/Max Version UI fields (which generate `app_version|versionCompare('X.!') >= 0` for min and `app_version|versionCompare('X.*') <= 0` for max automatically). Version targeting expressions are only included for Firefox for Android version 98 and above — earlier versions did not support this feature in the Nimbus SDK. ::: ### Install & Update @@ -234,7 +234,27 @@ On Android, the sticky clause uses `is_already_enrolled`: (is_already_enrolled) || () ``` -The same sticky/non-sticky split applies as on desktop — the same sticky/non-sticky split described in the Sticky Targeting section of the Desktop Targeting Guide applies here. +Not all parts of the targeting are wrapped in the sticky clause. Experimenter splits the expression into **sticky** and **non-sticky** parts: + +| Sticky (skipped for enrolled clients) | Non-sticky (always evaluated) | +|---|---| +| Advanced targeting config expression | Max version | +| Min version | | +| Languages / Countries | | +| Excluded / Required experiments | | + +For example, a sticky Fenix experiment targeting new users on version 100+, English, in Canada would produce: + +``` +(app_version|versionCompare('101.*') <= 0) +&& ((is_already_enrolled) + || ((days_since_install < 7) + && (app_version|versionCompare('100.!') >= 0) + && (language in ['en']) + && (region in ['CA']))) +``` + +An enrolled client will still be unenrolled if it updates past the max version, but won't be unenrolled if `days_since_install` exceeds 7. ## First-Run Targeting From 907871a2c136a65aae431035a997305a1a0e21be Mon Sep 17 00:00:00 2001 From: Jared Lockhart <119884+jaredlockhart@users.noreply.github.com> Date: Thu, 19 Mar 2026 12:52:31 -0400 Subject: [PATCH 07/14] docs(nimbus): make Fenix guide self-contained, remove desktop comparisons Because * Each platform guide should be self-contained and not defined relative to another platform This commit * Removes desktop comparison language from Languages field, locale attribute, JEXL section, common mistakes, and behavioral targeting * Removes outdated minimum version note (version 98) * Makes the JEXL section self-contained with its own operator list Co-Authored-By: Claude Opus 4.6 (1M context) --- .../android/custom-targeting.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/platform-guides/android/custom-targeting.md b/docs/platform-guides/android/custom-targeting.md index 00097cb3a..2f130d240 100644 --- a/docs/platform-guides/android/custom-targeting.md +++ b/docs/platform-guides/android/custom-targeting.md @@ -30,7 +30,7 @@ These are configured directly in the Experimenter audience form: |-------|-------------| | **Channel** | A single channel: `release`, `beta`, `nightly`, or `developer`. On mobile, channel is determined by the app ID (e.g., `org.mozilla.firefox` for release), so each channel is a separate application. | | **Min/Max Version** | Firefox version range (e.g., 134 to 140). Uses `app_version|versionCompare(...)` internally. | -| **Languages** | Two-letter language codes (e.g., `en`, `de`). Can include or exclude. Extracted from the device locale. (Desktop uses full locale codes instead; mobile uses languages.) | +| **Languages** | Two-letter language codes (e.g., `en`, `de`). Can include or exclude. Extracted from the device locale. | | **Countries** | Country codes extracted from the locale region (e.g., `US`, `DE`). Can include or exclude. | | **Population %** | Percentage of eligible users to enroll (bucketing). | @@ -61,7 +61,7 @@ The targeting context for Firefox for Android is assembled from multiple sources | `channel` | `string` | Build channel (`release`, `beta`, `nightly`, `developer`) | Usually set via UI, not JEXL | :::note -Version targeting is typically set via the Min/Max Version UI fields (which generate `app_version|versionCompare('X.!') >= 0` for min and `app_version|versionCompare('X.*') <= 0` for max automatically). Version targeting expressions are only included for Firefox for Android version 98 and above — earlier versions did not support this feature in the Nimbus SDK. +Version targeting is typically set via the Min/Max Version UI fields (which generate `app_version|versionCompare('X.!') >= 0` for min and `app_version|versionCompare('X.*') <= 0` for max automatically). ::: ### Install & Update @@ -83,7 +83,7 @@ Version targeting is typically set via the Min/Max Version UI fields (which gene |-----------|------|-------------|---------| | `language` | `string` | Two-letter language code extracted from locale (e.g., `en`) | `language in ['en', 'fr']` | | `region` | `string` | Country code extracted from locale (e.g., `US`) | `region in ['US', 'CA']` | -| `locale` | `string` | Full locale tag (e.g., `en-US`) | Available in the targeting context but not used by the Experimenter UI for mobile — use `language` instead | +| `locale` | `string` | Full locale tag (e.g., `en-US`) | Available in the targeting context but not used by the Experimenter UI — use `language` instead | :::note Language and region targeting is typically set via the Experimenter UI fields (which generate `language in [...]` / `region in [...]` expressions automatically), but can also be used directly in advanced targeting expressions. @@ -168,7 +168,7 @@ Attributes from `CustomAttributeProvider` are evaluated at startup. Attributes t ## Behavioral Targeting (Event Queries) -Firefox for Android supports **behavioral targeting** via event queries — this is a capability unique to the cross-platform Nimbus SDK and is **not available on desktop**. +Firefox for Android supports **behavioral targeting** via event queries. Event queries let you target users based on their past behavior by querying the Nimbus event store. Events are bucketed by time interval. @@ -211,11 +211,11 @@ The [`RecordedNimbusContext`](https://searchfox.org/mozilla-mobile/source/fenix/ ## JEXL Expression Syntax -Nimbus uses [mozjexl](https://github.com/mozilla/mozjexl). The same operators and syntax are available on all platforms — see the [Desktop Targeting Guide](https://github.com/mozilla/mozjexl) for the full JEXL reference. +Nimbus uses [mozjexl](https://github.com/mozilla/mozjexl), a Mozilla-extended version of JEXL. The standard operators (`&&`, `||`, `!`, `==`, `!=`, `<`, `>`, `<=`, `>=`, `in`) and arithmetic operators (`+`, `-`, `*`, `/`, `%`) are all available. -### Key Filters for Android +### Key Filters -In addition to the standard filters, the Nimbus SDK provides event query transforms for behavioral targeting: +Filters transform values using the pipe (`|`) syntax: | Filter | Description | Example | |--------|-------------|---------| @@ -407,11 +407,11 @@ You can test experiments using Preview mode: ### Common Mistakes -- **Using `version` instead of `app_version`** — on Android, the version attribute is `app_version`, not `version` +- **Using `version` instead of `app_version`** — the version attribute is `app_version` - **Using `isFirstRun` (string) instead of `is_first_run` (boolean)** — the camelCase form is legacy and compares as a string (`== 'true'`); prefer the snake_case boolean form - **First-run targeting with late-init attributes** — attributes like `are_notifications_enabled` or `is_default_browser` are not available at first startup - **Forgetting sticky enrollment** — if your targeting checks a changeable attribute (like `days_since_install`), mark the experiment as sticky -- **Using desktop-style attribute names** — Android uses snake_case (`days_since_install`, `is_first_run`), not camelCase +- **Using camelCase attribute names** — targeting attributes use snake_case (`days_since_install`, `is_first_run`) ## Adding New Targeting Options From 75a9073c1ec450aed71f25318bc9fb667e09210f Mon Sep 17 00:00:00 2001 From: Jared Lockhart <119884+jaredlockhart@users.noreply.github.com> Date: Thu, 19 Mar 2026 12:56:37 -0400 Subject: [PATCH 08/14] docs(nimbus): use jexl-rs instead of mozjexl for Fenix targeting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Because * Fenix uses jexl-rs (Rust), not mozjexl (JavaScript) — the two have different capabilities * jexl-rs has no ! (NOT) operator, no built-in transforms, and different registered transforms than mozjexl This commit * Replaces all mozjexl references with jexl-rs * Documents the exact operators from jexl-rs grammar (no ! operator) * Adds warning about using == false for negation instead of ! * Lists only the transforms registered in Nimbus SDK targeting.rs: versionCompare, eventSum, eventCountNonZero, eventAveragePerInterval, eventAveragePerNonZeroInterval, eventLastSeen, preferenceIsUserSet, bucketSample * Fixes eventAverage → eventAveragePerInterval (correct name) * Notes that mozjexl transforms (date, length, keys, etc.) are not available in jexl-rs Co-Authored-By: Claude Opus 4.6 (1M context) --- .../android/custom-targeting.md | 51 ++++++++++++++++--- 1 file changed, 45 insertions(+), 6 deletions(-) diff --git a/docs/platform-guides/android/custom-targeting.md b/docs/platform-guides/android/custom-targeting.md index 2f130d240..165373b58 100644 --- a/docs/platform-guides/android/custom-targeting.md +++ b/docs/platform-guides/android/custom-targeting.md @@ -11,7 +11,7 @@ This guide covers how targeting works for Firefox for Android (Fenix) experiment When you create an experiment in Experimenter, you configure **who** should be enrolled. Targeting happens at two levels: 1. **Basic targeting** (UI fields) — application, channel, Firefox version range, locale, country, language -2. **Advanced targeting** — a [JEXL](https://github.com/mozilla/mozjexl) expression evaluated against the client's targeting context to filter users by install age, device properties, UTM attribution, and more +2. **Advanced targeting** — a [JEXL](https://github.com/mozilla/jexl-rs) expression evaluated against the client's targeting context to filter users by install age, device properties, UTM attribution, and more Both levels are combined into a single JEXL expression that the Nimbus SDK evaluates on every Firefox for Android installation. If the expression evaluates to `true`, the client is eligible for enrollment. @@ -185,7 +185,7 @@ Event queries let you target users based on their past behavior by querying the |-----------|---------|-------------| | `\|eventSum(interval, bucket_count, starting_bucket)` | `number` | Sum of event counts over the interval | | `\|eventCountNonZero(interval, bucket_count, starting_bucket)` | `number` | Number of buckets with at least one event | -| `\|eventAverage(interval, bucket_count, starting_bucket)` | `number` | Average events per bucket | +| `\|eventAveragePerInterval(interval, bucket_count, starting_bucket)` | `number` | Average events per bucket | | `\|eventAveragePerNonZeroInterval(interval, bucket_count, starting_bucket)` | `number` | Average events per non-zero bucket | | `\|eventLastSeen(interval, starting_bucket)` | `number` | Buckets since the event last occurred | @@ -211,18 +211,57 @@ The [`RecordedNimbusContext`](https://searchfox.org/mozilla-mobile/source/fenix/ ## JEXL Expression Syntax -Nimbus uses [mozjexl](https://github.com/mozilla/mozjexl), a Mozilla-extended version of JEXL. The standard operators (`&&`, `||`, `!`, `==`, `!=`, `<`, `>`, `<=`, `>=`, `in`) and arithmetic operators (`+`, `-`, `*`, `/`, `%`) are all available. +Firefox for Android uses [jexl-rs](https://github.com/mozilla/jexl-rs), a Rust implementation of JEXL. -### Key Filters +### Operators -Filters transform values using the pipe (`|`) syntax: +| Operator | Description | Example | +|----------|-------------|---------| +| `&&` | Logical AND | `days_since_install >= 7 && days_since_update < 7` | +| `\|\|` | Logical OR | `'uBlock0@raymondhill.net' in addon_ids \|\| 'firefox@ghostery.com' in addon_ids` | +| `==` | Equality | `user_accepted_tou == false` | +| `!=` | Inequality | `install_referrer_response_utm_source != ''` | +| `<`, `>`, `<=`, `>=` | Comparison | `days_since_install < 7` | +| `in` | Element in array, substring in string, or key in object | `'uBlock0@raymondhill.net' in addon_ids` | +| `+` | Add / concatenate strings | | +| `-` | Subtract | | +| `*` | Multiply | | +| `/` | Divide | | +| `%` | Modulus | | +| `? :` | Ternary (conditional) | `is_first_run ? 'new' : 'existing'` | + +:::warning No negation operator +jexl-rs does **not** have a `!` (NOT) operator. To negate a condition, use `== false`: + +``` +// Correct +user_accepted_tou == false +('uBlock0@raymondhill.net' in addon_ids) == false + +// WRONG — will not parse +!user_accepted_tou +!('uBlock0@raymondhill.net' in addon_ids) +``` +::: + +### Filters (Pipe Transforms) + +Filters transform values using the pipe (`|`) syntax. jexl-rs has no built-in transforms — all available transforms are registered by the Nimbus SDK: | Filter | Description | Example | |--------|-------------|---------| -| `\|versionCompare` | Compare version strings | `android_sdk_version\|versionCompare('33') >= 0` | +| `\|versionCompare` | Compare version strings (returns negative, 0, or positive) | `android_sdk_version\|versionCompare('33') >= 0` | | `\|eventCountNonZero` | Count non-zero event buckets | `'events.app_opened'\|eventCountNonZero('Days', 28, 0) >= 21` | | `\|eventSum` | Sum event counts over interval | `'events.app_opened'\|eventSum('Days', 7, 0)` | +| `\|eventAveragePerInterval` | Average events per bucket | `'events.app_opened'\|eventAveragePerInterval('Days', 28, 0)` | +| `\|eventAveragePerNonZeroInterval` | Average events per non-zero bucket | `'events.app_opened'\|eventAveragePerNonZeroInterval('Days', 28, 0)` | | `\|eventLastSeen` | Buckets since event last occurred | `'events.app_opened'\|eventLastSeen('Days', 0)` | +| `\|preferenceIsUserSet` | True if a preference has been explicitly set by the user | Used for pref conflict checks | +| `\|bucketSample` | Bucket-based sampling | Used internally for bucketing | + +:::note +Transforms available on desktop (like `|date`, `|length`, `|keys`, `|preferenceValue`, `|regExpMatch`, `|mapToProperty`) are **not available** in jexl-rs. Only the transforms listed above can be used in Fenix targeting expressions. +::: ## Sticky Targeting From 747ecb4f15343732c6b0c7490eafb46e6b5ed6c2 Mon Sep 17 00:00:00 2001 From: Jared Lockhart <119884+jaredlockhart@users.noreply.github.com> Date: Thu, 19 Mar 2026 12:58:14 -0400 Subject: [PATCH 09/14] docs(nimbus): remove preferenceIsUserSet from Fenix filters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Because * preferenceIsUserSet depends on GeckoPrefStore which is a desktop-only concept — on Fenix the store is None so it always returns false This commit * Removes preferenceIsUserSet from the available filters table Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/platform-guides/android/custom-targeting.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/platform-guides/android/custom-targeting.md b/docs/platform-guides/android/custom-targeting.md index 165373b58..63e45ab91 100644 --- a/docs/platform-guides/android/custom-targeting.md +++ b/docs/platform-guides/android/custom-targeting.md @@ -256,7 +256,6 @@ Filters transform values using the pipe (`|`) syntax. jexl-rs has no built-in tr | `\|eventAveragePerInterval` | Average events per bucket | `'events.app_opened'\|eventAveragePerInterval('Days', 28, 0)` | | `\|eventAveragePerNonZeroInterval` | Average events per non-zero bucket | `'events.app_opened'\|eventAveragePerNonZeroInterval('Days', 28, 0)` | | `\|eventLastSeen` | Buckets since event last occurred | `'events.app_opened'\|eventLastSeen('Days', 0)` | -| `\|preferenceIsUserSet` | True if a preference has been explicitly set by the user | Used for pref conflict checks | | `\|bucketSample` | Bucket-based sampling | Used internally for bucketing | :::note From f6bb33c35b87a144c89d09dde8e9da9085ca5079 Mon Sep 17 00:00:00 2001 From: Jared Lockhart <119884+jaredlockhart@users.noreply.github.com> Date: Thu, 19 Mar 2026 12:58:39 -0400 Subject: [PATCH 10/14] docs(nimbus): remove preference references from Fenix guide MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Because * Gecko preference support for Fenix is still in progress — omit all preference references until that work is complete This commit * Removes mention of preferenceValue from the unavailable transforms note * Simplifies the note to just state jexl-rs has no built-in transforms Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/platform-guides/android/custom-targeting.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/platform-guides/android/custom-targeting.md b/docs/platform-guides/android/custom-targeting.md index 63e45ab91..7cf7394db 100644 --- a/docs/platform-guides/android/custom-targeting.md +++ b/docs/platform-guides/android/custom-targeting.md @@ -259,7 +259,7 @@ Filters transform values using the pipe (`|`) syntax. jexl-rs has no built-in tr | `\|bucketSample` | Bucket-based sampling | Used internally for bucketing | :::note -Transforms available on desktop (like `|date`, `|length`, `|keys`, `|preferenceValue`, `|regExpMatch`, `|mapToProperty`) are **not available** in jexl-rs. Only the transforms listed above can be used in Fenix targeting expressions. +jexl-rs has no built-in transforms. Only the transforms listed above (registered by the Nimbus SDK) can be used in Fenix targeting expressions. ::: ## Sticky Targeting From a31f8ea1b2364fe51d378f7fa327e766dd14d8dc Mon Sep 17 00:00:00 2001 From: Jared Lockhart <119884+jaredlockhart@users.noreply.github.com> Date: Thu, 19 Mar 2026 13:02:07 -0400 Subject: [PATCH 11/14] docs(nimbus): use real examples from targeting configs for all attributes Because * Several attribute examples were invented rather than sourced from real targeting configs * Missing attributes that are used in real configs: is_phone, searchEngines.current, homePageSettings This commit * Adds missing attributes: is_phone, searchEngines.current, homePageSettings.isDefault, homePageSettings.isCustomUrl * Replaces invented examples with real ones from targeting/constants.py (is_default_browser, enrollments_map) or marks as not typically used in targeting (app_name, app_id, channel) * Removes invented examples for attributes with no real targeting usage (device_manufacturer, device_model, are_notifications_enabled, etc.) * Adds phone users pattern to common patterns section * Reorders enrollment attributes to put commonly-used ones first Co-Authored-By: Claude Opus 4.6 (1M context) --- .../android/custom-targeting.md | 59 +++++++++++++++---- 1 file changed, 47 insertions(+), 12 deletions(-) diff --git a/docs/platform-guides/android/custom-targeting.md b/docs/platform-guides/android/custom-targeting.md index 7cf7394db..88840f37a 100644 --- a/docs/platform-guides/android/custom-targeting.md +++ b/docs/platform-guides/android/custom-targeting.md @@ -55,10 +55,10 @@ The targeting context for Firefox for Android is assembled from multiple sources | Attribute | Type | Description | Example | |-----------|------|-------------|---------| -| `app_name` | `string` | Application name (always `"fenix"`) | `app_name == 'fenix'` | -| `app_id` | `string` | Application package ID | `app_id == 'org.mozilla.firefox'` | +| `app_name` | `string` | Application name (always `"fenix"`) | Set automatically, not typically used in targeting | +| `app_id` | `string` | Application package ID (e.g., `org.mozilla.firefox`) | Set automatically, not typically used in targeting | | `app_version` | `string` | App version string (e.g., `"147.0"`) | `app_version\|versionCompare('134.!') >= 0` | -| `channel` | `string` | Build channel (`release`, `beta`, `nightly`, `developer`) | Usually set via UI, not JEXL | +| `channel` | `string` | Build channel (`release`, `beta`, `nightly`, `developer`) | Set via the Channel UI field | :::note Version targeting is typically set via the Min/Max Version UI fields (which generate `app_version|versionCompare('X.!') >= 0` for min and `app_version|versionCompare('X.*') <= 0` for max automatically). @@ -94,9 +94,10 @@ Language and region targeting is typically set via the Experimenter UI fields (w | Attribute | Type | Description | Example | |-----------|------|-------------|---------| | `android_sdk_version` | `string` | Android API level as a string (e.g., `"33"` for Android 13) | `android_sdk_version\|versionCompare('33') >= 0` | -| `device_manufacturer` | `string` | Device manufacturer (from `Build.MANUFACTURER`) | `device_manufacturer == 'Google'` | -| `device_model` | `string` | Device model (from `Build.MODEL`) | `device_model == 'Pixel 8'` | +| `is_phone` | `boolean` | Whether the device is a phone (not a tablet) | `is_phone` | | `is_large_device` | `boolean` | Whether the device has a large screen | `is_large_device` | +| `device_manufacturer` | `string` | Device manufacturer (from `Build.MANUFACTURER`) | | +| `device_model` | `string` | Device model (from `Build.MODEL`) | | | `architecture` | `string` | CPU architecture (e.g., `arm`, `x86`) | | ### Install Attribution (UTM) @@ -136,14 +137,39 @@ Language and region targeting is typically set via the Experimenter UI fields (w && ('firefox@ghostery.com' in addon_ids) == false ``` +### Search Engine + +| Attribute | Type | Description | Example | +|-----------|------|-------------|---------| +| `searchEngines` | `object` | Search engine information | | +| `searchEngines.current` | `string` | Current default search engine identifier | `'google' in searchEngines.current` | + +**Common patterns:** + +``` +// Users with Google as default search engine +'google' in searchEngines.current + +// Users with Bing as default +searchEngines.current == 'bing' +``` + +### Home Page + +| Attribute | Type | Description | Example | +|-----------|------|-------------|---------| +| `homePageSettings` | `object` | Home page configuration | | +| `homePageSettings.isDefault` | `boolean` | Using the default home page | `homePageSettings.isDefault` | +| `homePageSettings.isCustomUrl` | `boolean` | Using a custom URL as home page | `homePageSettings.isCustomUrl` | + ### Experiment & Rollout Enrollment | Attribute | Type | Description | Example | |-----------|------|-------------|---------| | `is_already_enrolled` | `boolean` | Whether the client is already enrolled in this experiment | Used in sticky clauses | -| `active_experiments` | `string[]` | Currently enrolled experiment slugs | `'my-experiment' in active_experiments` | | `enrollments` | `string[]` | All experiment enrollments (including past) | `('other-slug' in enrollments) == false` | -| `enrollments_map` | `object` | Experiment slug → branch slug mapping | Used for branch-level exclusion | +| `enrollments_map` | `object` | Experiment slug → branch slug mapping | `enrollments_map['other-slug'] == 'control'` | +| `active_experiments` | `string[]` | Currently enrolled experiment slugs | | ### Additional Attributes (Messaging / Display Triggers) @@ -151,11 +177,11 @@ These attributes are available from [`CustomAttributeProvider.kt`](https://searc | Attribute | Type | Description | Example | |-----------|------|-------------|---------| -| `is_default_browser` | `boolean` | Whether Firefox is the default browser | `is_default_browser` | -| `are_notifications_enabled` | `boolean` | Whether notification permissions are granted | `are_notifications_enabled` | -| `search_widget_is_installed` | `boolean` | Whether the search widget is on the home screen | `search_widget_is_installed` | -| `is_fxa_signed_in` | `boolean` | Whether the user is signed into Firefox Account | `is_fxa_signed_in` | -| `fxa_connected_devices` | `number` | Number of connected FxA devices | `fxa_connected_devices >= 2` | +| `is_default_browser` | `boolean` | Whether Firefox is the default browser | `is_default_browser == true && is_first_run` | +| `are_notifications_enabled` | `boolean` | Whether notification permissions are granted | | +| `search_widget_is_installed` | `boolean` | Whether the search widget is on the home screen | | +| `is_fxa_signed_in` | `boolean` | Whether the user is signed into Firefox Account | | +| `fxa_connected_devices` | `number` | Number of connected FxA devices | | | `date_string` | `string` | Current date as `yyyy-MM-dd` | | | `adjust_campaign` | `string` | Adjust campaign ID | | | `adjust_network` | `string` | Adjust network | | @@ -377,6 +403,15 @@ number_of_app_launches <= 20 number_of_app_launches > 20 ``` +### Phone users + +``` +is_phone + +// Phone users who are existing (28+ days) +is_phone && days_since_install >= 28 +``` + ### Large screen devices ``` From 304b82a75f23f841cb8f99075c77ce1b62b0f923 Mon Sep 17 00:00:00 2001 From: Jared Lockhart <119884+jaredlockhart@users.noreply.github.com> Date: Thu, 19 Mar 2026 13:05:11 -0400 Subject: [PATCH 12/14] docs(nimbus): keep UI field notes but remove 'use directly' encouragement Because * The notes explaining how UI fields generate JEXL are useful context * But we shouldn't encourage users to replicate built-in functionality in advanced targeting expressions This commit * Restores version and language/region notes explaining the generated JEXL * Removes "but can also be used directly in advanced targeting" wording Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/platform-guides/android/custom-targeting.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/platform-guides/android/custom-targeting.md b/docs/platform-guides/android/custom-targeting.md index 88840f37a..4b8c95b30 100644 --- a/docs/platform-guides/android/custom-targeting.md +++ b/docs/platform-guides/android/custom-targeting.md @@ -61,7 +61,7 @@ The targeting context for Firefox for Android is assembled from multiple sources | `channel` | `string` | Build channel (`release`, `beta`, `nightly`, `developer`) | Set via the Channel UI field | :::note -Version targeting is typically set via the Min/Max Version UI fields (which generate `app_version|versionCompare('X.!') >= 0` for min and `app_version|versionCompare('X.*') <= 0` for max automatically). +Version targeting is set via the Min/Max Version UI fields, which generate `app_version|versionCompare('X.!') >= 0` for min and `app_version|versionCompare('X.*') <= 0` for max. ::: ### Install & Update @@ -83,10 +83,10 @@ Version targeting is typically set via the Min/Max Version UI fields (which gene |-----------|------|-------------|---------| | `language` | `string` | Two-letter language code extracted from locale (e.g., `en`) | `language in ['en', 'fr']` | | `region` | `string` | Country code extracted from locale (e.g., `US`) | `region in ['US', 'CA']` | -| `locale` | `string` | Full locale tag (e.g., `en-US`) | Available in the targeting context but not used by the Experimenter UI — use `language` instead | +| `locale` | `string` | Full locale tag (e.g., `en-US`) | Available in the targeting context but not used by the Experimenter UI | :::note -Language and region targeting is typically set via the Experimenter UI fields (which generate `language in [...]` / `region in [...]` expressions automatically), but can also be used directly in advanced targeting expressions. +Language and region targeting is set via the Experimenter UI fields, which generate `language in [...]` / `region in [...]` expressions. ::: ### Device & OS From 1aca49c290c5baf654fcd2e5b2ac08a576fd5539 Mon Sep 17 00:00:00 2001 From: Jared Lockhart <119884+jaredlockhart@users.noreply.github.com> Date: Thu, 19 Mar 2026 13:08:29 -0400 Subject: [PATCH 13/14] docs(nimbus): remove locale from Fenix language & region table Because * The Experimenter UI uses language (not locale) for mobile and locale is not used in any mobile targeting config Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/platform-guides/android/custom-targeting.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/platform-guides/android/custom-targeting.md b/docs/platform-guides/android/custom-targeting.md index 4b8c95b30..09ccf7176 100644 --- a/docs/platform-guides/android/custom-targeting.md +++ b/docs/platform-guides/android/custom-targeting.md @@ -83,7 +83,6 @@ Version targeting is set via the Min/Max Version UI fields, which generate `app_ |-----------|------|-------------|---------| | `language` | `string` | Two-letter language code extracted from locale (e.g., `en`) | `language in ['en', 'fr']` | | `region` | `string` | Country code extracted from locale (e.g., `US`) | `region in ['US', 'CA']` | -| `locale` | `string` | Full locale tag (e.g., `en-US`) | Available in the targeting context but not used by the Experimenter UI | :::note Language and region targeting is set via the Experimenter UI fields, which generate `language in [...]` / `region in [...]` expressions. From 285a4f09f49a227520bcd14027211070ae8f6b57 Mon Sep 17 00:00:00 2001 From: Jared Lockhart <119884+jaredlockhart@users.noreply.github.com> Date: Thu, 19 Mar 2026 13:14:06 -0400 Subject: [PATCH 14/14] docs(nimbus): add real telemetry-sourced examples for device and UTM fields Because * Several attribute rows had empty example columns * Queried real Fenix recorded_nimbus_context telemetry to find actual values This commit * Adds real examples for device_manufacturer (samsung, Xiaomi, Google, etc.) and device_model (SM-S928B) sourced from telemetry * Adds utm_medium example with common real value (organic) Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/platform-guides/android/custom-targeting.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/platform-guides/android/custom-targeting.md b/docs/platform-guides/android/custom-targeting.md index 09ccf7176..f67513e26 100644 --- a/docs/platform-guides/android/custom-targeting.md +++ b/docs/platform-guides/android/custom-targeting.md @@ -95,8 +95,8 @@ Language and region targeting is set via the Experimenter UI fields, which gener | `android_sdk_version` | `string` | Android API level as a string (e.g., `"33"` for Android 13) | `android_sdk_version\|versionCompare('33') >= 0` | | `is_phone` | `boolean` | Whether the device is a phone (not a tablet) | `is_phone` | | `is_large_device` | `boolean` | Whether the device has a large screen | `is_large_device` | -| `device_manufacturer` | `string` | Device manufacturer (from `Build.MANUFACTURER`) | | -| `device_model` | `string` | Device model (from `Build.MODEL`) | | +| `device_manufacturer` | `string` | Device manufacturer (from `Build.MANUFACTURER`). Common values: `samsung`, `Xiaomi`, `Google`, `motorola`, `OPPO`, `OnePlus` | `device_manufacturer == 'samsung'` | +| `device_model` | `string` | Device model (from `Build.MODEL`). e.g., `SM-S928B`, `Pixel 8` | `device_model == 'SM-S928B'` | | `architecture` | `string` | CPU architecture (e.g., `arm`, `x86`) | | ### Install Attribution (UTM) @@ -104,7 +104,7 @@ Language and region targeting is set via the Experimenter UI fields, which gener | Attribute | Type | Description | Example | |-----------|------|-------------|---------| | `install_referrer_response_utm_source` | `string` | UTM source from install referrer | `install_referrer_response_utm_source == 'eea-browser-choice'` | -| `install_referrer_response_utm_medium` | `string` | UTM medium | | +| `install_referrer_response_utm_medium` | `string` | UTM medium. Common values: `organic`, `cpc` | `install_referrer_response_utm_medium == 'organic'` | | `install_referrer_response_utm_campaign` | `string` | UTM campaign | | | `install_referrer_response_utm_term` | `string` | UTM term | | | `install_referrer_response_utm_content` | `string` | UTM content | |