Skip to content

Commit 95042fc

Browse files
Conditional experiment matching (#1661)
* PoC conditional experiment matching * Add test case * Fix broken test case
1 parent 26fad2e commit 95042fc

File tree

6 files changed

+203
-1
lines changed

6 files changed

+203
-1
lines changed

injected/entry-points/integration.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,18 @@ function generateConfig() {
2222
platform: {
2323
name: 'extension',
2424
},
25+
currentCohorts: [
26+
{
27+
feature: 'ContentScopeExperiments',
28+
subfeature: 'bloops',
29+
cohort: 'control',
30+
},
31+
{
32+
feature: 'ContentScopeExperiments',
33+
subfeature: 'test',
34+
cohort: 'treatment',
35+
},
36+
],
2537
site: {
2638
domain: topLevelUrl.hostname,
2739
url: topLevelUrl.href,
@@ -97,6 +109,7 @@ async function initCode() {
97109
site: processedConfig.site,
98110
bundledConfig: processedConfig.bundledConfig,
99111
messagingConfig: processedConfig.messagingConfig,
112+
currentCohorts: processedConfig.currentCohorts,
100113
});
101114

102115
// mark this phase as loaded

injected/integration-test/pages.spec.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,15 @@ test.describe('Test integration pages', () => {
3737
);
3838
});
3939

40+
test('Test infra with experiments', async ({ page }, testInfo) => {
41+
await testPage(
42+
page,
43+
testInfo,
44+
'/infra/pages/conditional-matching-experiments.html',
45+
'./integration-test/test-pages/infra/config/conditional-matching-experiments.json',
46+
);
47+
});
48+
4049
test('Test infra fallback', async ({ page }, testInfo) => {
4150
await page.addInitScript(() => {
4251
// This ensures that our fallback code applies and so we simulate other platforms than Chromium.
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
{
2+
"features": {
3+
"contentScopeExperiments": {
4+
"exceptions": [],
5+
"state": "enabled",
6+
"features": {
7+
"bloops": {
8+
"state": "enabled",
9+
"rollout": {},
10+
"cohorts": [
11+
{
12+
"name": "control",
13+
"weight": 1
14+
},
15+
{
16+
"name": "treatment",
17+
"weight": 1
18+
}
19+
]
20+
},
21+
"test": {
22+
"state": "enabled",
23+
"rollout": {},
24+
"cohorts": [
25+
{
26+
"name": "control",
27+
"weight": 1
28+
},
29+
{
30+
"name": "treatment",
31+
"weight": 1
32+
}
33+
]
34+
}
35+
}
36+
},
37+
"apiManipulation": {
38+
"state": "enabled",
39+
"settings": {
40+
"apiChanges": {
41+
"Navigator.prototype.hardwareConcurrency": {
42+
"type": "descriptor",
43+
"getterValue": {
44+
"type": "number",
45+
"value": 100
46+
}
47+
}
48+
},
49+
"conditionalChanges": [
50+
{
51+
"condition": {
52+
"experiment": {
53+
"experimentName": "bloops",
54+
"cohort": "treatment"
55+
}
56+
},
57+
"patchSettings": [
58+
{
59+
"op": "replace",
60+
"path": "/apiChanges/Navigator.prototype.hardwareConcurrency/getterValue/value",
61+
"value": 200
62+
}
63+
]
64+
},
65+
{
66+
"condition": {
67+
"experiment": {
68+
"experimentName": "bloops",
69+
"cohort": "control"
70+
}
71+
},
72+
"patchSettings": [
73+
{
74+
"op": "replace",
75+
"path": "/apiChanges/Navigator.prototype.hardwareConcurrency/getterValue/value",
76+
"value": 300
77+
}
78+
]
79+
}
80+
]
81+
}
82+
},
83+
"exceptions": []
84+
}
85+
}

injected/integration-test/test-pages/infra/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
<p><a href="../../index.html">[Home]</a></p>
1111
<ul>
1212
<li><a href="./pages/conditional-matching.html">Conditional matching</a> - <a href="./config/conditional-matching.json">Config</a></li>
13+
<li><a href="./pages/conditional-matching-experiments.html">Conditional matching experiments</a> - <a href="./config/conditional-matching-experiments.json">Config</a></li>
1314
</ul>
1415
</body>
1516
</html>
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8">
5+
<meta name="viewport" content="width=device-width">
6+
<title>Conditional Matching experiments</title>
7+
<link rel="stylesheet" href="../../shared/style.css">
8+
</head>
9+
<body>
10+
<script src="../../shared/utils.js"></script>
11+
<p><a href="../index.html">[Infra]</a></p>
12+
13+
<p>This page verifies that APIs get modified when in an experiment. Ensure you're sending the following cohorts:
14+
<code>
15+
currentCohorts: [
16+
{
17+
"feature": "ContentScopeExperiments",
18+
"subfeature": "bloops",
19+
"cohort": "control",
20+
},
21+
{
22+
"feature": "ContentScopeExperiments",
23+
"subfeature": "test",
24+
"cohort": "treatment",
25+
},
26+
],
27+
</code>
28+
</p>
29+
30+
<script>
31+
test('Conditional matching experiments', async () => {
32+
const res = navigator.hardwareConcurrency;
33+
// Either is valid here, but 100 is the default which would mean the experiment is not running
34+
const expected = res === 200 ? 200 : 300;
35+
const results = [
36+
{
37+
name: "APIs changing, expecting to always match",
38+
result: res,
39+
expected: expected,
40+
}
41+
];
42+
return results;
43+
});
44+
45+
// eslint-disable-next-line no-undef
46+
renderResults();
47+
</script>
48+
</body>
49+
</html>

injected/src/config-feature.js

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,18 @@ export default class ConfigFeature {
1616
/** @type {string} */
1717
name;
1818

19-
/** @type {{ debug?: boolean, desktopModeEnabled?: boolean, forcedZoomEnabled?: boolean, featureSettings?: Record<string, unknown>, assets?: import('./content-feature.js').AssetConfig | undefined, site: import('./content-feature.js').Site, messagingConfig?: import('@duckduckgo/messaging').MessagingConfig } | null} */
19+
/**
20+
* @type {{
21+
* debug?: boolean,
22+
* desktopModeEnabled?: boolean,
23+
* forcedZoomEnabled?: boolean,
24+
* featureSettings?: Record<string, unknown>,
25+
* assets?: import('./content-feature.js').AssetConfig | undefined,
26+
* site: import('./content-feature.js').Site,
27+
* messagingConfig?: import('@duckduckgo/messaging').MessagingConfig,
28+
* currentCohorts?: [{feature: string, cohort: string, subfeature: string}],
29+
* } | null}
30+
*/
2031
#args;
2132

2233
/**
@@ -95,6 +106,9 @@ export default class ConfigFeature {
95106
* @typedef {object} ConditionBlock
96107
* @property {string[] | string} [domain]
97108
* @property {object} [urlPattern]
109+
* @property {object} [experiment]
110+
* @property {string} [experiment.experimentName]
111+
* @property {string} [experiment.cohort]
98112
*/
99113

100114
/**
@@ -121,6 +135,7 @@ export default class ConfigFeature {
121135
const conditionChecks = {
122136
domain: this._matchDomainConditional,
123137
urlPattern: this._matchUrlPatternConditional,
138+
experiment: this._matchExperimentConditional,
124139
};
125140

126141
for (const key in conditionBlock) {
@@ -152,6 +167,36 @@ export default class ConfigFeature {
152167
return true;
153168
}
154169

170+
/**
171+
* Takes a condition block and returns true if the current experiment matches the experimentName and cohort.
172+
* Expects:
173+
* ```json
174+
* {
175+
* "experiment": {
176+
* "experimentName": "experimentName",
177+
* "cohort": "cohort-name"
178+
* }
179+
* }
180+
* ```
181+
* Where featureName "ContentScopeExperiments" has a subfeature "experimentName" and cohort "cohort-name"
182+
* @param {ConditionBlock} conditionBlock
183+
* @returns {boolean}
184+
*/
185+
_matchExperimentConditional(conditionBlock) {
186+
if (!conditionBlock.experiment) return false;
187+
const experiment = conditionBlock.experiment;
188+
if (!experiment.experimentName || !experiment.cohort) return false;
189+
const currentCohorts = this.args?.currentCohorts;
190+
if (!currentCohorts) return false;
191+
return currentCohorts.some((cohort) => {
192+
return (
193+
cohort.feature === 'ContentScopeExperiments' &&
194+
cohort.subfeature === experiment.experimentName &&
195+
cohort.cohort === experiment.cohort
196+
);
197+
});
198+
}
199+
155200
/**
156201
* Takes a condtion block and returns true if the current url matches the urlPattern.
157202
* @param {ConditionBlock} conditionBlock

0 commit comments

Comments
 (0)