Skip to content

Commit a714f9b

Browse files
feat(nimbus): add user_disabled_ai targeting for Fenix and iOS (#15162)
Because * The risk_ai flag on experiments currently only generates targeting for Desktop using the `browser.ai.control.default` preference * Fenix ([Bug 2028993](https://bugzilla.mozilla.org/show_bug.cgi?id=2028993) / [D291861](https://phabricator.services.mozilla.com/D291861)) and iOS (mozilla-mobile/firefox-ios#32929) have added `user_disabled_ai` to their RecordedNimbusContext, shipping in v151 * Experiments using AI features need to exclude mobile users who have opted out of AI functionality This commit * Adds `user_disabled_ai == false` targeting expression when `risk_ai` is true and the application is Fenix or iOS * Adds version validation requiring >= v151 for mobile apps with `risk_ai` enabled * Adds model and serializer tests for both Fenix and iOS Fixes #15160
1 parent 19b1f56 commit a714f9b

File tree

5 files changed

+101
-4
lines changed

5 files changed

+101
-4
lines changed

experimenter/experimenter/experiments/api/v5/serializers.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1357,6 +1357,21 @@ def _validate_risk_ai_version(self, data):
13571357
],
13581358
}
13591359
)
1360+
1361+
if (
1362+
risk_ai
1363+
and NimbusExperiment.Application.is_mobile(application)
1364+
and firefox_min_version
1365+
and NimbusExperiment.Version.parse(firefox_min_version)
1366+
< NimbusExperiment.Version.parse(NimbusExperiment.AI_RISK_MIN_VERSION_MOBILE)
1367+
):
1368+
raise serializers.ValidationError(
1369+
{
1370+
"firefox_min_version": [
1371+
NimbusConstants.ERROR_FIREFOX_VERSION_MIN_151_FOR_AI_RISK_MOBILE
1372+
],
1373+
}
1374+
)
13601375
return data
13611376

13621377
def _validate_enrollment_targeting(self, data):

experimenter/experimenter/experiments/constants.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -972,6 +972,9 @@ class FirefoxLabsGroups(models.TextChoices):
972972
ERROR_FIREFOX_VERSION_MIN_148_FOR_AI_RISK = (
973973
"Experiments using AI features require Firefox version 148 or higher"
974974
)
975+
ERROR_FIREFOX_VERSION_MIN_151_FOR_AI_RISK_MOBILE = (
976+
"Mobile experiments using AI features require version 151 or higher"
977+
)
975978
ERROR_FIREFOX_VERSION_MAX = (
976979
"Ensure this value is greater than or equal to the minimum version"
977980
)
@@ -1049,6 +1052,8 @@ class FirefoxLabsGroups(models.TextChoices):
10491052

10501053
AI_RISK_MIN_VERSION = Version.FIREFOX_148
10511054

1055+
AI_RISK_MIN_VERSION_MOBILE = Version.FIREFOX_151
1056+
10521057
MULTIFEATURE_MAX_FEATURES = 20
10531058
ERROR_MULTIFEATURE_TOO_MANY_FEATURES = (
10541059
"Multi-feature experiments can only support up to 20 different features."

experimenter/experimenter/experiments/models.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -780,6 +780,9 @@ def targeting(self):
780780
"'browser.ai.control.default'|preferenceValue == 'available'"
781781
)
782782

783+
if self.risk_ai and self.is_mobile:
784+
expressions.append("user_disabled_ai == false")
785+
783786
# If there is no targeting defined all clients should match, so we return "true"
784787
return (
785788
" && ".join(f"({expression})" for expression in expressions)

experimenter/experimenter/experiments/tests/api/v5/test_serializers/test_nimbus_ready_for_review_serializer.py

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5379,11 +5379,53 @@ def test_risk_ai_false_allows_any_version(self):
53795379
)
53805380
self.assertTrue(serializer.is_valid(), serializer.errors)
53815381

5382-
def test_risk_ai_mobile_allows_any_version(self):
5382+
def test_risk_ai_requires_firefox_151_mobile(self):
53835383
experiment = NimbusExperimentFactory.create_with_lifecycle(
53845384
NimbusExperimentFactory.Lifecycles.CREATED,
53855385
application=NimbusExperiment.Application.FENIX,
5386-
firefox_min_version=NimbusExperiment.Version.FIREFOX_120,
5386+
firefox_min_version=NimbusExperiment.Version.FIREFOX_150,
5387+
risk_ai=True,
5388+
)
5389+
serializer = NimbusReviewSerializer(
5390+
experiment,
5391+
data=NimbusReviewSerializer(
5392+
experiment,
5393+
context={"user": self.user},
5394+
).data,
5395+
context={"user": self.user},
5396+
)
5397+
self.assertFalse(serializer.is_valid())
5398+
self.assertEqual(
5399+
serializer.errors,
5400+
{
5401+
"firefox_min_version": [
5402+
NimbusConstants.ERROR_FIREFOX_VERSION_MIN_151_FOR_AI_RISK_MOBILE
5403+
],
5404+
},
5405+
)
5406+
5407+
def test_risk_ai_accepts_firefox_151_mobile(self):
5408+
experiment = NimbusExperimentFactory.create_with_lifecycle(
5409+
NimbusExperimentFactory.Lifecycles.CREATED,
5410+
application=NimbusExperiment.Application.FENIX,
5411+
firefox_min_version=NimbusExperiment.Version.FIREFOX_151,
5412+
risk_ai=True,
5413+
)
5414+
serializer = NimbusReviewSerializer(
5415+
experiment,
5416+
data=NimbusReviewSerializer(
5417+
experiment,
5418+
context={"user": self.user},
5419+
).data,
5420+
context={"user": self.user},
5421+
)
5422+
self.assertTrue(serializer.is_valid(), serializer.errors)
5423+
5424+
def test_risk_ai_accepts_firefox_151_ios(self):
5425+
experiment = NimbusExperimentFactory.create_with_lifecycle(
5426+
NimbusExperimentFactory.Lifecycles.CREATED,
5427+
application=NimbusExperiment.Application.IOS,
5428+
firefox_min_version=NimbusExperiment.Version.FIREFOX_151,
53875429
risk_ai=True,
53885430
)
53895431
serializer = NimbusReviewSerializer(

experimenter/experimenter/experiments/tests/test_models.py

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1405,10 +1405,11 @@ def test_targeting_config_returns_None_with_invalid_slug(self):
14051405
(True, NimbusExperiment.Application.DESKTOP, True),
14061406
(False, NimbusExperiment.Application.DESKTOP, False),
14071407
(None, NimbusExperiment.Application.DESKTOP, False),
1408-
(True, NimbusExperiment.Application.FENIX, False),
14091408
]
14101409
)
1411-
def test_targeting_with_risk_ai(self, risk_ai, application, should_include_targeting):
1410+
def test_targeting_with_risk_ai_desktop(
1411+
self, risk_ai, application, should_include_targeting
1412+
):
14121413
experiment = NimbusExperimentFactory.create_with_lifecycle(
14131414
NimbusExperimentFactory.Lifecycles.LAUNCH_APPROVE_APPROVE,
14141415
application=application,
@@ -1425,6 +1426,37 @@ def test_targeting_with_risk_ai(self, risk_ai, application, should_include_targe
14251426
self.assertNotIn(ai_targeting_expr, experiment.targeting)
14261427
validate_jexl_expr(experiment.targeting, experiment.application)
14271428

1429+
@parameterized.expand(
1430+
[
1431+
(True, NimbusExperiment.Application.FENIX, True),
1432+
(True, NimbusExperiment.Application.IOS, True),
1433+
(False, NimbusExperiment.Application.FENIX, False),
1434+
(None, NimbusExperiment.Application.IOS, False),
1435+
]
1436+
)
1437+
def test_targeting_with_risk_ai_mobile(
1438+
self, risk_ai, application, should_include_targeting
1439+
):
1440+
experiment = NimbusExperimentFactory.create_with_lifecycle(
1441+
NimbusExperimentFactory.Lifecycles.LAUNCH_APPROVE_APPROVE,
1442+
application=application,
1443+
firefox_min_version=NimbusExperiment.Version.FIREFOX_151,
1444+
firefox_max_version=NimbusExperiment.Version.NO_VERSION,
1445+
channel=NimbusExperiment.Channel.NO_CHANNEL,
1446+
channels=[],
1447+
risk_ai=risk_ai,
1448+
)
1449+
mobile_ai_targeting_expr = "user_disabled_ai == false"
1450+
desktop_ai_targeting_expr = (
1451+
"'browser.ai.control.default'|preferenceValue == 'available'"
1452+
)
1453+
if should_include_targeting:
1454+
self.assertIn(mobile_ai_targeting_expr, experiment.targeting)
1455+
else:
1456+
self.assertNotIn(mobile_ai_targeting_expr, experiment.targeting)
1457+
self.assertNotIn(desktop_ai_targeting_expr, experiment.targeting)
1458+
validate_jexl_expr(experiment.targeting, experiment.application)
1459+
14281460
def test_start_date_returns_None_for_not_started_experiment(self):
14291461
experiment = NimbusExperimentFactory.create_with_lifecycle(
14301462
NimbusExperimentFactory.Lifecycles.CREATED,

0 commit comments

Comments
 (0)