Skip to main content

Behavioral Targeting

Behavioral targeting is a term used to describe a set of jexl transforms which can be used to target specific user behaviors. User behaviors might be the user opened the app, the user logged in, the user navigated to a specific view, or any user-triggered event that has Glean metrics associated with it.

In order to maintain user privacy, this entire system lives in the Nimbus client launched by our mobile applications. Additionally, events are recorded and stored as counts in time interval-based buckets, allowing for a predictably small amount of disk spaceto be used for this event store.

Event Bucketing#

Stored events are bucketed into time intervals. The time intervals are Minutes, Hours, Days, Weeks, Months, and Years. No additional setup is required for this bucketing process, it is handled entirely by the SDK. Buckets for each of the time intervals are created and stored whenever a new event is recorded.

Bucket Advancement & Retention#

When buckets are created, they have a starting date. This date is set to Jan 1 00:00:00 UTC of the current year. As time passes, the current time is incremented by the time difference in whole intervals and the buckets are advanced that many positions.

Bucket advancement occurs when an event is recorded, or when a query is performed. Buckets always advance based off whole increments of their time interval; Minutes will advance by full minutes, Hours by full hours, and so on. One exception to this rule is the Months time interval – it advances in increments of 28 days.

Based on the current datetime, the buckets may not advance at all, or may advance so much that all the buckets are cleared. As an example, if the current date for the Hours bucket is set to May 1 10:00:00 UTC, and an event is recorded at May 1 12:45:00 UTC, the buckets will be advanced 2 positions, the current date will be updated to May 1 12:00:00 UTC, and the event will be recorded in the bucket for the 12pm hour.


Each time interval has a maximum number of buckets it retains. As the time intervals move forward, buckets are rotated off of the deque and new buckets are added. If a query is performed that would go beyond the bucket count, it instead is cut off at the bucket count.

The following is a list of the time intervals and their bucket counts:

Minutes 60Hours   24Days    56Weeks   52Months  12Years   4

Querying for User Behavior#

User behaviors are recorded in the same way as Glean events, and there are a number of ways in which they can be queried.

The following is a list of jexl transforms that exist within the Nimbus targeting helper, and thus are usable on all projects that use the Nimbus Rust library.

eventSumCalculates the sum of all bucket values within the rangeinterval, bucket_count, starting_bucketint
eventCountNonZeroCalculates the total number of buckets with a non-zero value within the rangeinterval, bucket_count, starting_bucketint
eventAverageCalculates the average of all event bucket values within the rangeinterval, bucket_count, starting_bucketfloat
eventAveragePerNonZeroIntervalCalculates the average of all buckets with a non-zero value within the rangeinterval, bucket_count, starting_bucketfloat
eventLastSeenReturns the number of whole time intervals between the starting bucket and the first bucket with a non-zero valueinterval, starting_bucketint

Designing Experiments & Behavior Triggers#

The following are the existing options for behavioral targeting as defined in Experimenter (found under Advanced Targeting in the audience editor). In order to use these targeting options, the application must be Firefox or Focus for Android or iOS.

NameDescriptionTargeting String
Core Active UsersA user who has used the application 21 out of the last 28 days.
'app-opened-event'|eventCountNonZero('Days', 28, 0) >= 21
Recently Logged In UsersA user who has logged into Sync within the last 12 weeks.
'sync-signin-event'|eventCountNonZero('Weeks', 12, 0) > 0

There are many ways these queries could be used to our advantage when writing behavior-oriented code. One example could be to show a certain message to users after they have launched the app n times, and after 12hrs has passed from when they first opened the application.

'app-opened-event'|eventSum('Years', 4, 0) >= 3 &&      // The sum of app opened events within the last four years must be 3 or more(  'app-opened-event'|eventSum('Hours', 12, 12) >= 1 ||  // The sum of app opened events within 12hrs, starting 12hrs ago  'app-opened-event'|eventSum('Days', 7, 1) >= 1 ||     // The sum of app opened events within 7 days, starting 1 day ago  'app-opened-event'|eventSum('Weeks', 52, 1) >= 1      // The sum of app opened events within 52 weeks, starting 1 week ago)                                                            // Any one of these results must have been 1 or more

Instrumented Events#

The following are the events that are currently instrumented in Firefox for iOS and Android, respectively:

Application openedapp_opened
User logged into Syncsync_auth.sign_in


In order to instrument a new behavior/event, an equivalent call to the Nimbus event recording method must be made alongside the call to record a Glean event.


While this process is currently required, long-term we hope to have a hook in Glean that will record certain events automatically.

On Firefox for Android, a call should be made to immediately following the Glean event being recorded. The argument should be the event name.

import org.mozilla.fenix.ext.components
// class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {    // ...    final override fun onCreate(savedInstanceState: Bundle?) {        // ...        if (settings().isTelemetryEnabled) {            // ...            safeIntent                ?.let(::getIntentSource)                ?.also {                    Events.appOpened.record(Events.AppOpenedExtra(it))                    // This will record an event in Nimbus' internal event store. Used for behavioral targeting          "app_opened")                }        }        // ...    }}

Example in PR