Skip to content

Commit b4a5f68

Browse files
authored
add recorded targeting context doc (#668)
1 parent b517831 commit b4a5f68

File tree

3 files changed

+372
-0
lines changed

3 files changed

+372
-0
lines changed
Lines changed: 362 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,362 @@
1+
---
2+
id: recording-targeting-context-values-to-glean
3+
title: Recording Targeting Context Values to Glean
4+
slug: /recording-targeting-context-values-to-glean
5+
---
6+
7+
import Tabs from "@theme/Tabs";
8+
import TabItem from "@theme/TabItem";
9+
10+
In order to support automated population sizing efforts, the Nimbus SDK has been updated to include an interface by
11+
which values that are a part of the Nimbus targeting context can be recorded to Glean. This page documents how to
12+
implement the aforementioned interface, and provides guidance on updating the Nimbus targeting context moving forward.
13+
14+
There are a number of implementation details that are worth noting. The first to be covered is how the code is
15+
structured in Rust, as it uses some of the more complex features of UniFFI, followed by how the code is structured in the Firefox
16+
Android and iOS repositories.
17+
18+
## Rust Nimbus SDK code
19+
20+
To start, a new Rust trait was defined, with methods to be implemented to perform four key functions.
21+
22+
1. `to_json`: to return a JSON representation of the `RecordedContext`'s values
23+
2. `get_event_queries`: to return a map of an event query name to an event query
24+
3. `set_event_query_values`: to set the internal calculated values for the event queries
25+
4. `record`: to record the internal values of the object to Glean
26+
27+
```rust
28+
pub trait RecordedContext: Send + Sync {
29+
/// Returns a JSON representation of the context object
30+
///
31+
/// This method will be implemented in foreign code, i.e: Kotlin, Swift, Python, etc...
32+
fn to_json(&self) -> JsonObject;
33+
34+
/// Returns a HashMap representation of the event queries that will be used in the targeting
35+
/// context
36+
///
37+
/// This method will be implemented in foreign code, i.e: Kotlin, Swift, Python, etc...
38+
fn get_event_queries(&self) -> HashMap<String, String>;
39+
40+
/// Sets the object's internal value for the event query values
41+
///
42+
/// This method will be implemented in foreign code, i.e: Kotlin, Swift, Python, etc...
43+
fn set_event_query_values(&self, event_query_values: HashMap<String, f64>);
44+
45+
/// Records the context object to Glean
46+
///
47+
/// This method will be implemented in foreign code, i.e: Kotlin, Swift, Python, etc...
48+
fn record(&self);
49+
}
50+
```
51+
52+
We then use the UDL to define this trait such that it uses foreign implementations. As such, we will end up with kotlin/
53+
swift classes that implement the methods as described above.
54+
55+
```
56+
[Trait, WithForeign]
57+
interface RecordedContext {
58+
JsonObject to_json();
59+
60+
record<string, string> get_event_queries();
61+
62+
void set_event_query_values(record<string, f64> event_query_values);
63+
64+
void record();
65+
};
66+
```
67+
68+
We also have some internal Rust methods extending off the trait for validating/executing the event queries, but they are
69+
not really of much consequence to implementing developers.
70+
71+
## `to_json`
72+
73+
The JSON object value returned from `to_json` will ultimately be flattened on top of the existing fields present in the
74+
targeting attributes. **Values present in the targeting attributes by default will be overridden by values in the
75+
recorded context.**
76+
77+
```rust
78+
// components/nimbus/src/stateful/evaluator.rs
79+
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
80+
pub struct TargetingAttributes {
81+
#[serde(flatten)]
82+
pub app_context: AppContext,
83+
pub language: Option<String>,
84+
pub region: Option<String>,
85+
#[serde(flatten)]
86+
pub recorded_context: Option<JsonObject>,
87+
```
88+
89+
Below is an example implementation of `to_json` in both Kotlin and Swift.
90+
91+
<Tabs
92+
defaultValue="kotlin"
93+
values={[
94+
{ label: "Kotlin", value: "kotlin" },
95+
{ label: "Swift", value: "swift" },
96+
]}>
97+
<TabItem value="kotlin">
98+
99+
```kotlin
100+
// RecordedNimbusContext.kt
101+
override fun toJson(): JsonObject {
102+
val obj = JSONObject(
103+
mapOf(
104+
"is_first_run" to isFirstRun,
105+
// more fields here
106+
),
107+
)
108+
return obj
109+
}
110+
```
111+
112+
</TabItem>
113+
<TabItem value="swift">
114+
115+
```swift
116+
// RecordedNimbusContext.swift
117+
func toJson() -> JsonObject {
118+
guard let data = try? JSONSerialization.data(withJSONObject: [
119+
"is_first_run": isFirstRun,
120+
]),
121+
let jsonString = NSString(data: data, encoding: String.Encoding.utf8.rawValue) as? String
122+
else {
123+
return "{}"
124+
}
125+
return jsonString
126+
}
127+
```
128+
129+
</TabItem>
130+
</Tabs>
131+
132+
## `get_event_queries`
133+
134+
In both Kotlin and Swift, as long as the member variable for `eventQueries` conforms to the type `Map<String, String>`
135+
it can be simply returned from this function.
136+
137+
<Tabs
138+
defaultValue="kotlin"
139+
values={[
140+
{ label: "Kotlin", value: "kotlin" },
141+
{ label: "Swift", value: "swift" },
142+
]}>
143+
<TabItem value="kotlin">
144+
145+
```kotlin
146+
// RecordedNimbusContext.kt
147+
override fun getEventQueries(): Map<String, String> {
148+
return eventQueries
149+
}
150+
```
151+
152+
</TabItem>
153+
<TabItem value="swift">
154+
155+
```swift
156+
// RecordedNimbusContext.swift
157+
func getEventQueries() -> [String: String] {
158+
return eventQueries
159+
}
160+
```
161+
162+
</TabItem>
163+
</Tabs>
164+
165+
## `set_event_query_values`
166+
167+
In both Kotlin and Swift, as long as the member variable for `eventQueryValues` conforms to the type
168+
`Map<String, Double>` it can be simply returned from this function.
169+
170+
<Tabs
171+
defaultValue="kotlin"
172+
values={[
173+
{ label: "Kotlin", value: "kotlin" },
174+
{ label: "Swift", value: "swift" },
175+
]}>
176+
<TabItem value="kotlin">
177+
178+
```kotlin
179+
// RecordedNimbusContext.kt
180+
override fun setEventQueryValues(eventQueryValues: Map<String, Double>) {
181+
this.eventQueryValues = eventQueryValues
182+
}
183+
```
184+
185+
</TabItem>
186+
<TabItem value="swift">
187+
188+
```swift
189+
// RecordedNimbusContext.swift
190+
func setEventQueryValues(eventQueryValues: [String: Double]) {
191+
self.eventQueryValues = eventQueryValues
192+
}
193+
```
194+
195+
</TabItem>
196+
</Tabs>
197+
198+
## `record`
199+
200+
The `record` method should actually record the context's value to Glean. The Glean metric's definition can be found in
201+
the `metrics.yaml` file.
202+
- [Android `metrics.yaml`](https://searchfox.org/mozilla-central/source/mobile/android/fenix/app/metrics.yaml)
203+
- [iOS `metrics.yaml`](https://github.com/mozilla-mobile/firefox-ios/blob/main/firefox-ios/Client/metrics.yaml)
204+
205+
```yaml
206+
# metrics.yaml
207+
nimbus_system:
208+
recorded_nimbus_context:
209+
type: object
210+
structure:
211+
type: object
212+
properties:
213+
is_first_run:
214+
type: boolean
215+
event_query_values:
216+
type: object
217+
properties:
218+
days_opened_in_last_28:
219+
type: number
220+
```
221+
222+
The metric definition determines what properties exist for the Glean types, so we must make sure to use those types when
223+
setting the value for the metric.
224+
225+
<Tabs
226+
defaultValue="kotlin"
227+
values={[
228+
{ label: "Kotlin", value: "kotlin" },
229+
{ label: "Swift", value: "swift" },
230+
]}>
231+
<TabItem value="kotlin">
232+
233+
```kotlin
234+
// RecordedNimbusContext.kt
235+
override fun record() {
236+
val eventQueryValuesObject = NimbusSystem.RecordedNimbusContextObjectItemEventQueryValuesObject(
237+
daysOpenedInLast28 = eventQueryValues[DAYS_OPENED_IN_LAST_28]?.toInt(),
238+
)
239+
NimbusSystem.recordedNimbusContext.set(
240+
NimbusSystem.RecordedNimbusContextObject(
241+
isFirstRun = isFirstRun,
242+
eventQueryValues = eventQueryValuesObject,
243+
),
244+
)
245+
}
246+
```
247+
248+
</TabItem>
249+
<TabItem value="swift">
250+
251+
```swift
252+
// RecordedNimbusContext.swift
253+
func record() {
254+
let eventQueryValuesObject = GleanMetrics.NimbusSystem.RecordedNimbusContextObjectItemEventQueryValuesObject(
255+
daysOpenedInLast28: eventQueryValues[RecordedNimbusContext.DAYS_OPENED_IN_LAST_28].toInt64()
256+
)
257+
258+
GleanMetrics.NimbusSystem.recordedNimbusContext.set(
259+
GleanMetrics.NimbusSystem.RecordedNimbusContextObject(
260+
isFirstRun: isFirstRun,
261+
eventQueryValues: eventQueryValuesObject,
262+
)
263+
)
264+
}
265+
```
266+
267+
</TabItem>
268+
</Tabs>
269+
270+
### Additional methods
271+
272+
Two additional methods have also been exposed to assist developers with a) validating their event queries and b)
273+
calculating targeting attributes that have historically been provided by the Rust code. `validateEventQueries` is used
274+
in testing, to ensure the event queries being run are in fact valid event queries.
275+
276+
`getCalculatedAttributes` accepts the app installation date, the path to the Nimbus database, and the locale string,
277+
executes some commands in Rust to read from the database and calculate additional fields based on the installation date,
278+
and returns the resulting values to the caller. It should be used during the construction of any foreign implementation
279+
of the `RecordedContext` trait.
280+
281+
## Adding new fields
282+
283+
The new field should be added to the `RecordedNimbusContext` class in each of the following locations:
284+
285+
- the constructor
286+
- as a member variable **(Swift only)**
287+
- the `record` method
288+
- the `toJson` method
289+
- the `create` method **(Kotlin only)**
290+
- the `createForTest` method **(Kotlin only)**
291+
292+
The field also needs to be added to the appropriate `metrics.yaml` file for the application, under the
293+
`nimbus_system.recorded_nimbus_context` metric.
294+
295+
- [Android `RecordedNimbusContext` class](https://searchfox.org/mozilla-central/source/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/experiments/RecordedNimbusContext.kt)
296+
- [Android `metrics.yaml`](https://searchfox.org/mozilla-central/source/mobile/android/fenix/app/metrics.yaml)
297+
- [iOS `RecordedNimbusContext` class](https://github.com/mozilla-mobile/firefox-ios/blob/main/firefox-ios/Client/Experiments/RecordedNimbusContext.swift)
298+
- [iOS `metrics.yaml`](https://github.com/mozilla-mobile/firefox-ios/blob/main/firefox-ios/Client/metrics.yaml)
299+
300+
:::info
301+
302+
In the future, the goal is for this file and its tests to be statically assessed to ensure all the fields are present where they should be.
303+
304+
:::
305+
306+
## Adding new event queries
307+
308+
Event queries are marginally simpler to add than new fields. Adding a new one requires the following changes:
309+
310+
- a new `const`/`static` value for the event query's name
311+
- a new record in the `EVENT_QUERIES` map
312+
- a new entry in the `event_query_values` property in the `nimbus_system.recorded_nimbus_context` metric
313+
314+
<Tabs
315+
defaultValue="kotlin"
316+
values={[
317+
{ label: "Kotlin", value: "kotlin" },
318+
{ label: "Swift", value: "swift" },
319+
]}>
320+
<TabItem value="kotlin">
321+
322+
[`metrics.yaml`](https://searchfox.org/mozilla-central/source/mobile/android/fenix/app/metrics.yaml)
323+
[`RecordedNimbusContext` file](https://searchfox.org/mozilla-central/source/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/experiments/RecordedNimbusContext.kt)
324+
325+
```kotlin
326+
/**
327+
* The following constants are string constants of the keys that appear in the [EVENT_QUERIES] map.
328+
*/
329+
const val DAYS_OPENED_IN_LAST_28 = "days_opened_in_last_28"
330+
331+
/**
332+
* [EVENT_QUERIES] is a map of keys to Nimbus SDK EventStore queries.
333+
*/
334+
private val EVENT_QUERIES = mapOf(
335+
DAYS_OPENED_IN_LAST_28 to "'events.app_opened'|eventCountNonZero('Days', 28, 0)",
336+
)
337+
```
338+
339+
</TabItem>
340+
<TabItem value="swift">
341+
342+
[`metrics.yaml`](https://github.com/mozilla-mobile/firefox-ios/blob/main/firefox-ios/Client/metrics.yaml)
343+
[`RecordedNimbusContext` class](https://github.com/mozilla-mobile/firefox-ios/blob/main/firefox-ios/Client/Experiments/RecordedNimbusContext.swift)
344+
345+
```swift
346+
class RecordedNimbusContext: RecordedContext {
347+
/**
348+
* The following constants are string constants of the keys that appear in the [EVENT_QUERIES] map.
349+
*/
350+
static let DAYS_OPENED_IN_LAST_28: String = "days_opened_in_last_28"
351+
352+
/**
353+
* [EVENT_QUERIES] is a map of keys to Nimbus SDK EventStore queries.
354+
*/
355+
static let EVENT_QUERIES = [
356+
DAYS_OPENED_IN_LAST_28: "'events.app_opened'|eventCountNonZero('Days', 28, 0)",
357+
]
358+
```
359+
360+
</TabItem>
361+
</Tabs>
362+

docs/workflow/implementing/mobile-targeting/android-custom-targeting.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@ id: android-custom-targeting
33
title: Adding new targeting attributes to Android
44
slug: /android-custom-targeting
55
---
6+
7+
:::warning DEPRECATED
8+
**This method of adding new targeting attributes is deprecated. Please use the method described in the [Recorded Targeting Context doc](/recording-targeting-context-values-to-glean#adding-new-fields).**
9+
:::
10+
611
# Adding new targeting attributes to Android
712
This page demonstrates how to add new targeting attributes to Android, enabling experiment creators more specific targeting.
813
For more general documentation on targeting custom audiences, check out [the custom audiences docs](/workflow/implementing/custom-audiences)

docs/workflow/implementing/mobile-targeting/ios-custom-targeting.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ title: Adding new targeting attributes to iOS
44
slug: /ios-custom-targeting
55
---
66
# Adding new targeting attributes to iOS
7+
8+
:::warning DEPRECATED
9+
**This method of adding new targeting attributes is deprecated. Please use the method described in the [Recorded Targeting Context doc](/recording-targeting-context-values-to-glean#adding-new-fields).**
10+
:::
11+
712
This page demonstrates how to add new targeting attributes to iOS, enabling experiment creators more specific targeting.
813
For more general documentation on targeting custom audiences, check out [the custom audiences docs](/workflow/implementing/custom-audiences)
914

0 commit comments

Comments
 (0)