From fa4a7e30a18e5288121bed4a137a3f7cabb6110a Mon Sep 17 00:00:00 2001 From: Beth Rennie Date: Mon, 3 Feb 2025 15:00:52 -0500 Subject: [PATCH] feat(schemas): Add a firefoxLabsDescriptionLinks field to DesktopNimbusExperiment (#12137) Because: - some Firefox Labs descriptions include placeholder links; and - we need to provide the URLs for those links outside the Fluent strings This comit: - adds a new field to the DesktopNimbusExperiment model; and - bumps the version of mozilla-nimbus-schemas to 2025.1.1. Fixes #12132 --- schemas/VERSION | 2 +- schemas/index.d.ts | 12 +++ .../experiments/experiments.py | 10 ++- .../desktop-131-fxLabsOptIn-isNotRollout.json | 1 + .../desktop-131-fxLabsOptIn-isRollout.json | 1 + ...ktop-137-fxLabsOptIn-descriptionLinks.json | 90 +++++++++++++++++++ .../tests/experiments/test_experiments.py | 45 ++++++++-- schemas/package.json | 2 +- schemas/poetry.lock | 15 +++- schemas/pyproject.toml | 7 +- ...topAllVersionsNimbusExperiment.schema.json | 18 ++++ .../DesktopNimbusExperiment.schema.json | 18 ++++ 12 files changed, 207 insertions(+), 14 deletions(-) create mode 100644 schemas/mozilla_nimbus_schemas/tests/experiments/fixtures/experiments/desktop/desktop-137-fxLabsOptIn-descriptionLinks.json diff --git a/schemas/VERSION b/schemas/VERSION index fb94802d83..7343555253 100644 --- a/schemas/VERSION +++ b/schemas/VERSION @@ -1 +1 @@ -2024.12.2 +2025.1.1 diff --git a/schemas/index.d.ts b/schemas/index.d.ts index f89ba82f6f..3fc96c460e 100644 --- a/schemas/index.d.ts +++ b/schemas/index.d.ts @@ -171,6 +171,12 @@ export interface DesktopAllVersionsNimbusExperiment { * The description shown in Firefox Labs (Fluent ID) */ firefoxLabsDescription?: string | null; + /** + * Links that will be used with the firefoxLabsDescription Fluent ID. May be null for Firefox Labs Opt-In recipes that do not use links. + */ + firefoxLabsDescriptionLinks?: { + [k: string]: string; + } | null; /** * Opt out of feature schema validation. */ @@ -512,6 +518,12 @@ export interface DesktopNimbusExperiment { * The description shown in Firefox Labs (Fluent ID) */ firefoxLabsDescription?: string | null; + /** + * Links that will be used with the firefoxLabsDescription Fluent ID. May be null for Firefox Labs Opt-In recipes that do not use links. + */ + firefoxLabsDescriptionLinks?: { + [k: string]: string; + } | null; /** * Opt out of feature schema validation. */ diff --git a/schemas/mozilla_nimbus_schemas/experiments/experiments.py b/schemas/mozilla_nimbus_schemas/experiments/experiments.py index 08c3333f17..325ec688da 100644 --- a/schemas/mozilla_nimbus_schemas/experiments/experiments.py +++ b/schemas/mozilla_nimbus_schemas/experiments/experiments.py @@ -2,7 +2,7 @@ from enum import Enum from typing import Any, Literal -from pydantic import BaseModel, ConfigDict, Field, RootModel, model_validator +from pydantic import BaseModel, ConfigDict, Field, HttpUrl, RootModel, model_validator from pydantic.json_schema import SkipJsonSchema from typing_extensions import Self @@ -302,6 +302,13 @@ class DesktopNimbusExperiment(BaseExperiment): description="The description shown in Firefox Labs (Fluent ID)", default=None, ) + firefoxLabsDescriptionLinks: dict[str, HttpUrl] | None = Field( + description=( + "Links that will be used with the firefoxLabsDescription Fluent ID. May be " + "null for Firefox Labs Opt-In recipes that do not use links." + ), + default=None, + ) featureValidationOptOut: bool | SkipJsonSchema[None] = Field( description="Opt out of feature schema validation.", default=None, @@ -358,6 +365,7 @@ def validate_firefox_labs(cls, data: Self) -> Self: "then": { "required": [ "firefoxLabsDescription", + "firefoxLabsDescriptionLinks", "firefoxLabsGroup", "firefoxLabsTitle", ], diff --git a/schemas/mozilla_nimbus_schemas/tests/experiments/fixtures/experiments/desktop/desktop-131-fxLabsOptIn-isNotRollout.json b/schemas/mozilla_nimbus_schemas/tests/experiments/fixtures/experiments/desktop/desktop-131-fxLabsOptIn-isNotRollout.json index 2dd0a9558d..1e669ab82a 100644 --- a/schemas/mozilla_nimbus_schemas/tests/experiments/fixtures/experiments/desktop/desktop-131-fxLabsOptIn-isNotRollout.json +++ b/schemas/mozilla_nimbus_schemas/tests/experiments/fixtures/experiments/desktop/desktop-131-fxLabsOptIn-isNotRollout.json @@ -53,6 +53,7 @@ "isFirefoxLabsOptIn": true, "firefoxLabsTitle": "test-title", "firefoxLabsDescription": "test-desc", + "firefoxLabsDescriptionLinks": null, "firefoxLabsGroup": "test", "isRollout": false, "requiresRestart": false diff --git a/schemas/mozilla_nimbus_schemas/tests/experiments/fixtures/experiments/desktop/desktop-131-fxLabsOptIn-isRollout.json b/schemas/mozilla_nimbus_schemas/tests/experiments/fixtures/experiments/desktop/desktop-131-fxLabsOptIn-isRollout.json index 491658fe4f..d8b9acbbec 100644 --- a/schemas/mozilla_nimbus_schemas/tests/experiments/fixtures/experiments/desktop/desktop-131-fxLabsOptIn-isRollout.json +++ b/schemas/mozilla_nimbus_schemas/tests/experiments/fixtures/experiments/desktop/desktop-131-fxLabsOptIn-isRollout.json @@ -79,6 +79,7 @@ "isFirefoxLabsOptIn": true, "firefoxLabsTitle": "test-title", "firefoxLabsDescription": "test-desc", + "firefoxLabsDescriptionLinks": null, "firefoxLabsGroup": "test", "isRollout": true, "requiresRestart": false diff --git a/schemas/mozilla_nimbus_schemas/tests/experiments/fixtures/experiments/desktop/desktop-137-fxLabsOptIn-descriptionLinks.json b/schemas/mozilla_nimbus_schemas/tests/experiments/fixtures/experiments/desktop/desktop-137-fxLabsOptIn-descriptionLinks.json new file mode 100644 index 0000000000..d5dd40e6c3 --- /dev/null +++ b/schemas/mozilla_nimbus_schemas/tests/experiments/fixtures/experiments/desktop/desktop-137-fxLabsOptIn-descriptionLinks.json @@ -0,0 +1,90 @@ + +{ + "appId": "firefox-desktop", + "appName": "firefox_desktop", + "application": "firefox-desktop", + "arguments": {}, + "branches": [ + { + "feature": { + "featureId": "this-is-included-for-desktop-pre-95-support", + "enabled": false, + "value": {} + }, + "features": [ + { + "featureId": "pocketNewtab", + "value": { "enabled": "true" } + }, + { + "featureId": "upgradeDialog", + "value": { + "enabled": false + } + } + ], + "ratio": 1, + "slug": "control" + }, + { + "feature": { + "featureId": "this-is-included-for-desktop-pre-95-support", + "enabled": false, + "value": {} + }, + "features": [ + { + "featureId": "pocketNewtab", + "value": { + "enabled": true, + "compactLayout": true, + "lastCardMessageEnabled": true, + "loadMore": true, + "newFooterSection": true + } + }, + { + "featureId": "upgradeDialog", + "value": { + "enabled": true + } + } + ], + "ratio": 1, + "slug": "treatment" + } + ], + "bucketConfig": { + "count": 10000, + "namespace": "firefox-desktop-multifeature-test", + "randomizationUnit": "normandy_id", + "start": 0, + "total": 10000 + }, + "channel": "nightly", + "endDate": null, + "featureIds": ["upgradeDialog", "pocketNewtab"], + "id": "mr2-upgrade-spotlight-holdback", + "isEnrollmentPaused": false, + "outcomes": [], + "probeSets": [], + "proposedDuration": 63, + "proposedEnrollment": 7, + "referenceBranch": "control", + "schemaVersion": "1.7.1", + "slug": "firefox-desktop-multifeature-test", + "startDate": "2021-10-26", + "targeting": "true", + "userFacingDescription": "Experimenting on onboarding content when you upgrade Firefox.", + "userFacingName": "MR2 Upgrade Spotlight Holdback", + "isFirefoxLabsOptIn": true, + "firefoxLabsTitle": "test-title", + "firefoxLabsDescription": "test-desc", + "firefoxLabsDescriptionLinks": { + "foo": "https://example.com/foo", + "bar": "https://example.com/bar" + }, + "firefoxLabsGroup": "test", + "isRollout": true, + "requiresRestart": false +} diff --git a/schemas/mozilla_nimbus_schemas/tests/experiments/test_experiments.py b/schemas/mozilla_nimbus_schemas/tests/experiments/test_experiments.py index 3aa57d1fde..762c04e8b7 100644 --- a/schemas/mozilla_nimbus_schemas/tests/experiments/test_experiments.py +++ b/schemas/mozilla_nimbus_schemas/tests/experiments/test_experiments.py @@ -96,7 +96,7 @@ def load_schema(name: str) -> Validator: validator = validator_for(schema) validator.check_schema(schema) - return validator(schema) + return validator(schema, format_checker=validator.FORMAT_CHECKER) @pytest.mark.parametrize("experiment_file", FIXTURE_DIR.joinpath("desktop").iterdir()) @@ -137,6 +137,7 @@ def test_desktop_nimbus_expirement_with_fxlabs_opt_in_is_not_rollout( "isFirefoxLabsOptIn": True, "firefoxLabsTitle": "test-title", "firefoxLabsDescription": "test-desc", + "firefoxLabsDescriptionLinks": None, "firefoxLabsGroup": "test-group", } ) @@ -154,6 +155,7 @@ def test_desktop_nimbus_experiment_with_fxlabs_opt_in_is_rollout( "isFirefoxLabsOptIn": True, "firefoxLabsTitle": "test-title", "firefoxLabsDescription": "test-desc", + "firefoxLabsDescriptionLinks": None, "firefoxLabsGroup": "test-group", } ) @@ -174,19 +176,50 @@ def test_desktop_nimbus_experiment_with_fxlabs_opt_in_but_missing_required_field experiment_json["isFirefoxLabsOptIn"] = True validate_desktop_experiment(experiment_json, valid=False, valid_all_versions=False) - errors = list( - desktop_all_versions_nimbus_experiment_schema_validator.iter_errors( + error_messages = [ + e.message + for e in desktop_all_versions_nimbus_experiment_schema_validator.iter_errors( experiment_json ) - ) - error_messages = [e.message for e in errors] + ] - assert len(error_messages) == 5 + assert len(error_messages) == 6 assert error_messages.count("'firefoxLabsTitle' is a required property") == 3 assert error_messages.count("'firefoxLabsDescription' is a required property") == 1 + assert ( + error_messages.count("'firefoxLabsDescriptionLinks' is a required property") == 1 + ) assert error_messages.count("'firefoxLabsGroup' is a required property") == 1 +def test_desktop_nimbus_experiment_with_fxlabs_opt_in_invalid_description_links( + validate_desktop_experiment, + desktop_all_versions_nimbus_experiment_schema_validator, +): + experiment_json = _desktop_nimbus_experiment(isRollout=True) + experiment_json.update( + { + "isFirefoxLabsOptIn": True, + "firefoxLabsTitle": "placeholder-title", + "firefoxLabsDescription": "placeholder-desc", + "firefoxLabsDescriptionLinks": {"foo": "bar"}, + "firefoxLabsGroup": "placeholder-group", + } + ) + + validate_desktop_experiment(experiment_json, valid=False, valid_all_versions=False) + + error_messages = [ + e.message + for e in desktop_all_versions_nimbus_experiment_schema_validator.iter_errors( + experiment_json + ) + ] + + assert len(error_messages) == 1 + assert "{'foo': 'bar'} is not valid under any of the given schemas" in error_messages + + def _desktop_nimbus_experiment(isRollout: bool) -> dict[str, Any]: return { "appId": "firefox-desktop", diff --git a/schemas/package.json b/schemas/package.json index 49b0ee39f7..e27c207272 100644 --- a/schemas/package.json +++ b/schemas/package.json @@ -1,6 +1,6 @@ { "name": "@mozilla/nimbus-schemas", - "version": "2024.12.2", + "version": "2025.1.1", "description": "Schemas used by Mozilla Nimbus and related projects.", "main": "index.d.ts", "repository": { diff --git a/schemas/poetry.lock b/schemas/poetry.lock index 90e4645047..8afe24f729 100644 --- a/schemas/poetry.lock +++ b/schemas/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.5 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand. [[package]] name = "annotated-types" @@ -1085,6 +1085,17 @@ files = [ [package.extras] idna2008 = ["idna"] +[[package]] +name = "rfc3987" +version = "1.3.8" +description = "Parsing and validation of URIs (RFC 3986) and IRIs (RFC 3987)" +optional = false +python-versions = "*" +files = [ + {file = "rfc3987-1.3.8-py2.py3-none-any.whl", hash = "sha256:10702b1e51e5658843460b189b185c0366d2cf4cff716f13111b0ea9fd2dce53"}, + {file = "rfc3987-1.3.8.tar.gz", hash = "sha256:d3c4d257a560d544e9826b38bc81db676890c79ab9d7ac92b39c7a253d5ca733"}, +] + [[package]] name = "rich" version = "13.8.0" @@ -1351,4 +1362,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "e80305fe0d55e08a9c5d2ae03413e3515188ea9281dcdc405058dbc9775a9473" +content-hash = "a96e4e4e2895fbe044b13f8be461faeb73ea27d67fb71d874ad922ad13208dc1" diff --git a/schemas/pyproject.toml b/schemas/pyproject.toml index 35b808b23b..c23fb4d72d 100644 --- a/schemas/pyproject.toml +++ b/schemas/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "mozilla-nimbus-schemas" -version = "2024.12.2" +version = "2025.1.1" description = "Schemas used by Mozilla Nimbus and related projects." authors = ["mikewilli"] license = "MPL 2.0" @@ -21,11 +21,12 @@ typing-extensions = ">=4.0.1" # Required until Python 3.11 jsonschema = "^4.23.0" [tool.poetry.group.dev.dependencies] -ruff = ">=0.5.0,<0.9.2" +PyYAML = "^6.0" black = ">=23.3,<25.0" pytest = "^7.3.1" +rfc3987 = "^1.3.8" +ruff = ">=0.5.0,<0.9.2" twine = "^5.1.1" -PyYAML = "^6.0" [build-system] diff --git a/schemas/schemas/DesktopAllVersionsNimbusExperiment.schema.json b/schemas/schemas/DesktopAllVersionsNimbusExperiment.schema.json index 2a2b9276ea..75323ccf2e 100644 --- a/schemas/schemas/DesktopAllVersionsNimbusExperiment.schema.json +++ b/schemas/schemas/DesktopAllVersionsNimbusExperiment.schema.json @@ -198,6 +198,23 @@ ], "description": "The description shown in Firefox Labs (Fluent ID)" }, + "firefoxLabsDescriptionLinks": { + "anyOf": [ + { + "additionalProperties": { + "format": "uri", + "maxLength": 2083, + "minLength": 1, + "type": "string" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "description": "Links that will be used with the firefoxLabsDescription Fluent ID. May be null for Firefox Labs Opt-In recipes that do not use links." + }, "featureValidationOptOut": { "description": "Opt out of feature schema validation.", "type": "boolean" @@ -256,6 +273,7 @@ }, "required": [ "firefoxLabsDescription", + "firefoxLabsDescriptionLinks", "firefoxLabsGroup", "firefoxLabsTitle" ], diff --git a/schemas/schemas/DesktopNimbusExperiment.schema.json b/schemas/schemas/DesktopNimbusExperiment.schema.json index 27277a9445..23ac87e19e 100644 --- a/schemas/schemas/DesktopNimbusExperiment.schema.json +++ b/schemas/schemas/DesktopNimbusExperiment.schema.json @@ -198,6 +198,23 @@ ], "description": "The description shown in Firefox Labs (Fluent ID)" }, + "firefoxLabsDescriptionLinks": { + "anyOf": [ + { + "additionalProperties": { + "format": "uri", + "maxLength": 2083, + "minLength": 1, + "type": "string" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "description": "Links that will be used with the firefoxLabsDescription Fluent ID. May be null for Firefox Labs Opt-In recipes that do not use links." + }, "featureValidationOptOut": { "description": "Opt out of feature schema validation.", "type": "boolean" @@ -256,6 +273,7 @@ }, "required": [ "firefoxLabsDescription", + "firefoxLabsDescriptionLinks", "firefoxLabsGroup", "firefoxLabsTitle" ],