Skip to content
This repository was archived by the owner on Jul 9, 2025. It is now read-only.

Commit f4213fb

Browse files
committed
Bug 1670293 - RSExperimentLoader should expose evaluateJexl for testing r=andreio
Differential Revision: https://phabricator.services.mozilla.com/D104401
1 parent 0bdf21e commit f4213fb

File tree

3 files changed

+131
-18
lines changed

3 files changed

+131
-18
lines changed

toolkit/components/messaging-system/lib/RemoteSettingsExperimentLoader.jsm

Lines changed: 32 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -117,32 +117,48 @@ class _RemoteSettingsExperimentLoader {
117117
this._initialized = false;
118118
}
119119

120-
/**
121-
* Checks targeting of a recipe if it is defined
122-
* @param {Recipe} recipe
123-
* @param {{[key: string]: any}} customContext A custom filter context
124-
* @returns {Promise<boolean>} Should we process the recipe?
125-
*/
126-
async checkTargeting(recipe, customContext = {}) {
120+
async evaluateJexl(jexlString, customContext) {
121+
if (customContext && !customContext.experiment) {
122+
throw new Error(
123+
"Expected an .experiment property in second param of this function"
124+
);
125+
}
126+
127127
const context = TargetingContext.combineContexts(
128-
{ experiment: recipe },
129128
customContext,
129+
this.manager.createTargetingContext(),
130130
ASRouterTargeting.Environment
131131
);
132-
const { targeting } = recipe;
133-
if (!targeting) {
134-
log.debug("No targeting for recipe, so it matches automatically");
135-
return true;
136-
}
137-
log.debug("Testing targeting expression:", targeting);
132+
133+
log.debug("Testing targeting expression:", jexlString);
138134
const targetingContext = new TargetingContext(context);
135+
139136
let result = false;
140137
try {
141-
result = await targetingContext.evalWithDefault(targeting);
138+
result = await targetingContext.evalWithDefault(jexlString);
142139
} catch (e) {
143140
log.debug("Targeting failed because of an error");
144141
Cu.reportError(e);
145142
}
143+
return result;
144+
}
145+
146+
/**
147+
* Checks targeting of a recipe if it is defined
148+
* @param {Recipe} recipe
149+
* @param {{[key: string]: any}} customContext A custom filter context
150+
* @returns {Promise<boolean>} Should we process the recipe?
151+
*/
152+
async checkTargeting(recipe) {
153+
if (!recipe.targeting) {
154+
log.debug("No targeting for recipe, so it matches automatically");
155+
return true;
156+
}
157+
158+
const result = await this.evaluateJexl(recipe.targeting, {
159+
experiment: recipe,
160+
});
161+
146162
return Boolean(result);
147163
}
148164

@@ -172,10 +188,8 @@ class _RemoteSettingsExperimentLoader {
172188

173189
let matches = 0;
174190
if (recipes && !loadingError) {
175-
const context = this.manager.createTargetingContext();
176-
177191
for (const r of recipes) {
178-
if (await this.checkTargeting(r, context)) {
192+
if (await this.checkTargeting(r)) {
179193
matches++;
180194
log.debug(`${r.id} matched`);
181195
await this.manager.onRecipe(r, "rs-loader");
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
[DEFAULT]
22

33
[browser_remotesettings_experiment_enroll.js]
4+
[browser_experiment_evaluate_jexl.js]
45
tags = remote-settings
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
"use strict";
2+
3+
const { RemoteSettingsExperimentLoader } = ChromeUtils.import(
4+
"resource://messaging-system/lib/RemoteSettingsExperimentLoader.jsm"
5+
);
6+
const { ExperimentManager } = ChromeUtils.import(
7+
"resource://messaging-system/experiments/ExperimentManager.jsm"
8+
);
9+
const { ExperimentFakes } = ChromeUtils.import(
10+
"resource://testing-common/MSTestUtils.jsm"
11+
);
12+
13+
add_task(async function setup() {
14+
await SpecialPowers.pushPrefEnv({
15+
set: [
16+
["messaging-system.log", "all"],
17+
["app.shield.optoutstudies.enabled", true],
18+
],
19+
});
20+
21+
registerCleanupFunction(async () => {
22+
await SpecialPowers.popPrefEnv();
23+
});
24+
});
25+
26+
const FAKE_CONTEXT = {
27+
experiment: ExperimentFakes.recipe("fake-test-experiment"),
28+
};
29+
30+
add_task(async function test_throws_if_no_experiment_in_context() {
31+
await Assert.rejects(
32+
RemoteSettingsExperimentLoader.evaluateJexl("true", { customThing: 1 }),
33+
/Expected an .experiment property/,
34+
"should throw if experiment is not passed to the custom context"
35+
);
36+
});
37+
38+
add_task(async function test_evaluate_jexl() {
39+
Assert.deepEqual(
40+
await RemoteSettingsExperimentLoader.evaluateJexl(
41+
`["hello"]`,
42+
FAKE_CONTEXT
43+
),
44+
["hello"],
45+
"should return the evaluated result of a jexl expression"
46+
);
47+
});
48+
49+
add_task(async function test_evaluate_custom_context() {
50+
const result = await RemoteSettingsExperimentLoader.evaluateJexl(
51+
"experiment.slug",
52+
FAKE_CONTEXT
53+
);
54+
Assert.equal(
55+
result,
56+
"fake-test-experiment",
57+
"should have the custom .experiment context"
58+
);
59+
});
60+
61+
add_task(async function test_evaluate_active_experiments() {
62+
const result = await RemoteSettingsExperimentLoader.evaluateJexl(
63+
"isFirstStartup",
64+
FAKE_CONTEXT
65+
);
66+
Assert.equal(
67+
typeof result,
68+
"boolean",
69+
"should have a .isFirstStartup property from ExperimentManager "
70+
);
71+
});
72+
73+
add_task(async function test_evaluate_active_experiments() {
74+
// Add an experiment to active experiments
75+
const slug = "foo" + Date.now();
76+
ExperimentManager.store.addExperiment(ExperimentFakes.experiment(slug));
77+
registerCleanupFunction(() => {
78+
ExperimentManager.store._deleteForTests(slug);
79+
});
80+
81+
Assert.equal(
82+
await RemoteSettingsExperimentLoader.evaluateJexl(
83+
`"${slug}" in activeExperiments`,
84+
FAKE_CONTEXT
85+
),
86+
true,
87+
"should find an active experiment"
88+
);
89+
90+
Assert.equal(
91+
await RemoteSettingsExperimentLoader.evaluateJexl(
92+
`"does-not-exist-fake" in activeExperiments`,
93+
FAKE_CONTEXT
94+
),
95+
false,
96+
"should not find an experiment that doesn't exist"
97+
);
98+
});

0 commit comments

Comments
 (0)