Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions .github/workflows/experimenter-mozcloud-pr-preview.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
name: Experimenter -- PR Preview

on:
pull_request:
types: [labeled, unlabeled, synchronize]

env:
PROJECT_ID: moz-fx-experimenter-prod-6cd5
IMAGE_BASE: us-docker.pkg.dev/moz-fx-experimenter-prod-6cd5/experimenter-prod/experimenter

jobs:
build_and_push:
if: >
(
github.event_name == 'pull_request' &&
contains(github.event.pull_request.labels.*.name, 'pr-preview') &&
github.event.pull_request.head.repo.full_name == github.repository
)
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
id-token: write
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.event.pull_request.head.sha }}
persist-credentials: false
fetch-tags: true
fetch-depth: 0

- uses: ./.github/actions/check-changed-paths
id: check-paths
with:
paths: "experimenter/"

- uses: ./.github/actions/retry
if: steps.check-paths.outputs.should-run == 'true'
with:
label: Build experimenter prod image
run: |
./scripts/store_git_info.sh
make build_prod

- name: Tag image(s) for GAR
if: steps.check-paths.outputs.should-run == 'true'
id: meta
shell: bash
run: |
IMAGE_TAG="$(git rev-parse --short=10 HEAD)"

docker tag experimenter:deploy "${IMAGE_BASE}:${IMAGE_TAG}"
echo "image_tag=${IMAGE_TAG}" >> "$GITHUB_OUTPUT"

- name: Push to Google Artifact Registry
if: steps.check-paths.outputs.should-run == 'true'
uses: mozilla-it/deploy-actions/docker-push@ef0f037316873ff408a598f1cd98876dd7851e53 # v6.7.0
with:
image_tags: |-
${{ env.IMAGE_BASE }}:${{ steps.meta.outputs.image_tag }}
workload_identity_pool_project_number: ${{ vars.GCPV2_WORKLOAD_IDENTITY_POOL_PROJECT_NUMBER }}
project_id: ${{ env.PROJECT_ID }}
4 changes: 1 addition & 3 deletions .github/workflows/experimenter-mozcloud-publish.yaml
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
name: Experimenter -- Build, Tag and Push Container Images to GAR Repository

on:
pull_request:
types: [labeled, unlabeled, synchronize]
push:
branches:
- main
Expand All @@ -25,7 +23,7 @@ jobs:
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ (github.event_name == 'pull_request' && github.event.pull_request.head.sha) || github.ref }}
ref: ${{ github.ref }}
persist-credentials: false
fetch-tags: true
fetch-depth: 0
Expand Down
7 changes: 0 additions & 7 deletions experimenter/experimenter/experiments/api/v5/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -452,8 +452,6 @@ def get_risk_flags(self, obj):
flags.append("Revenue")
if obj.risk_brand:
flags.append("Brand")
if obj.risk_message:
flags.append("Message")
if obj.risk_ai:
flags.append("AI")
return flags
Expand Down Expand Up @@ -634,11 +632,6 @@ class NimbusReviewSerializer(serializers.ModelSerializer):
allow_null=False,
error_messages={"null": NimbusConstants.ERROR_REQUIRED_QUESTION},
)
risk_message = serializers.BooleanField(
required=True,
allow_null=False,
error_messages={"null": NimbusConstants.ERROR_REQUIRED_QUESTION},
)
risk_ai = serializers.BooleanField(
required=True,
allow_null=False,
Expand Down
12 changes: 12 additions & 0 deletions experimenter/experimenter/experiments/changelog_utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import json
import uuid

from django.contrib.auth.models import User
from django.contrib.postgres.fields import ArrayField
from django.core.exceptions import FieldDoesNotExist
from django.db import models
Expand Down Expand Up @@ -59,6 +60,7 @@ class NimbusExperimentChangeLogSerializer(serializers.ModelSerializer):
many=True, read_only=True, slug_field="email"
)
tags = serializers.SlugRelatedField(many=True, read_only=True, slug_field="name")
message_reviewer = serializers.SlugRelatedField(read_only=True, slug_field="email")

class Meta:
model = NimbusExperiment
Expand Down Expand Up @@ -148,6 +150,16 @@ def get_formatted_change_object(field_name, field_diff, changelog, timestamp):
old_value = json.dumps(old_value, indent=2)
new_value = json.dumps(new_value, indent=2)

# elif (
# isinstance(field_instance, models.ForeignKey)
# and field_instance.related_model is User
# ):
# values = {
# User.objects.
# }
# old_value = "dingus"
# new_value = "dongus"

elif isinstance(field_instance, (models.JSONField, ArrayField)):
event_name = ChangeEventType.DETAILED.name
if old_value is not None:
Expand Down
38 changes: 38 additions & 0 deletions experimenter/experimenter/experiments/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,8 @@ class ApplicationConfig:
#: If None, this application will not support Firefox Labs.
firefox_labs: FirefoxLabs | None = field(default=None)

messaging_features: list[str] = field(default=None)

def get_kinto_collection_for_experiment(self, experiment: NimbusExperiment) -> str:
if self.kinto_collections_by_feature_id is not None:
return self.get_kinto_collection_for_feature_ids(
Expand Down Expand Up @@ -295,6 +297,42 @@ def kinto_collections(self) -> set[str]:
FirefoxLabs.Groups.NEWTAB_WIDGETS: Version.FIREFOX_151,
},
),
# https://searchfox.org/firefox-main/source/browser/components/asrouter/modules/MessagingExperimentConstants.sys.mjs
messaging_features=[
"cfr",
"infobar",
"moments-page",
"pbNewtab",
"spotlight",
"featureCallout",
"fxms_bmb_button",
"fxms-message",
"fxms-message-1",
"fxms-message-2",
"fxms-message-3",
"fxms-message-4",
"fxms-message-5",
"fxms-message-6",
"fxms-message-7",
"fxms-message-8",
"fxms-message-9",
"fxms-message-10",
"fxms-message-11",
"fxms-message-12",
"fxms-message-13",
"fxms-message-14",
"fxms-message-15",
"fxms-message-16",
"fxms-message-17",
"fxms-message-18",
"fxms-message-19",
"fxms-message-20",
"fxms-message-21",
"fxms-message-22",
"fxms-message-23",
"fxms-message-24",
"fxms-message-25",
]
)

APPLICATION_CONFIG_FENIX = ApplicationConfig(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models


def create_permissions(apps, schema_editor):
ContentType = apps.get_model("contenttypes", "ContentType")
NimbusExperiment = apps.get_model("experiments", "NimbusExperiment")
Group = apps.get_model("auth", "Group")
Permission = apps.get_model("auth", "Permission")

content_type = ContentType.objects.get_for_model(NimbusExperiment)

omc = Group.objects.create(name="OMC")
can_perform_message_review = Permission.objects.create(
codename="can_perform_message_review",
name="Can perform message review",
content_type=content_type,
)
omc.permissions.add(can_perform_message_review)


def delete_permissions(apps, schema_editor):
ContentType = apps.get_model("contenttypes", "ContentType")
NimbusExperiment = apps.get_model("experiments", "NimbusExperiment")
Group = apps.get_model("auth", "Group")
Permission = apps.get_model("auth", "Permission")

content_type = ContentType.objects.get_for_model(NimbusExperiment)

Group.objects.filter(name="OMC").delete()
Permission.objects.filter(
content_type=content_type, codename="can_perform_message_review"
).delete()


class Migration(migrations.Migration):
dependencies = [
("experiments", "0337_nimbusexperiment_sizing_data"),
("contenttypes", "0002_remove_content_type_name"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]

operations = [
migrations.AddField(
model_name="nimbusexperiment",
name="message_reviewer",
field=models.ForeignKey(
blank=True,
default=None,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to=settings.AUTH_USER_MODEL,
),
),
migrations.RunPython(create_permissions, delete_permissions),
]
11 changes: 11 additions & 0 deletions experimenter/experimenter/experiments/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,7 @@ def default_firefox_version_parsed():
risk_message = models.BooleanField(
"Is a Message Risk Flag", default=None, blank=True, null=True
)
message_reviewer = models.ForeignKey(User, default=None, blank=True, null=True, on_delete=models.SET_NULL)
risk_ai = models.BooleanField(
"Is an AI Risk Flag", default=None, blank=True, null=True
)
Expand Down Expand Up @@ -2922,6 +2923,16 @@ def notification_emails(self):
)
return list({email for email in emails if email})

@property
def is_messaging_experiment(self):
application_config = self.application_config
return (
application_config.messaging_features
and self.feature_configs.filter(
slug__in=application_config.messaging_features
).exists()
)


class NimbusBranch(models.Model):
experiment = models.ForeignKey(
Expand Down
7 changes: 7 additions & 0 deletions experimenter/experimenter/features/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import json
import logging
import re
from collections.abc import Iterable
from dataclasses import dataclass
Expand Down Expand Up @@ -30,6 +31,7 @@
FeatureVariableType.BOOLEAN: bool,
}

logger = logging.getLogger()

@dataclass
class Feature:
Expand Down Expand Up @@ -122,6 +124,7 @@ def _read_manifest(
def _load_features(cls):
features = []
version_re = re.compile(r"^v(?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)")
i = 0

for application in NimbusConstants.APPLICATION_CONFIGS.values():
application_dir: Path = settings.FEATURE_MANIFESTS_PATH / application.slug
Expand All @@ -139,11 +142,15 @@ def _load_features(cls):

application_yaml_path = child / "experimenter.yaml"
if application_yaml_path.exists():
i+=1
logging.info(f"Loading manifest {i} {application_yaml_path}")
features.extend(
cls._read_manifest(
application, application_yaml_path, version
)
)
logging.info(f"Loaded manifest {i} {application_yaml_path}")


return features

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,14 @@ def handle(self, *args, **options):
# When we are ingesting versioned Features, we want to update the
# NimbusFeatureConfig objects with the most up-to-date description.
updated: set[tuple[str, str]] = set()
for feature in itertools.chain(
ALL_FEATURES = list(itertools.chain(
Features.unversioned(),
sorted(Features.versioned(), key=lambda f: f.version, reverse=True),
):
))
N = len(ALL_FEATURES)
for (i, feature) in enumerate(ALL_FEATURES):
logger.info(f"processing feature {i}/{N}")

key = (feature.application_slug, feature.slug)
if key in updated:
# We have already processes the unversioned feature OR a
Expand Down Expand Up @@ -210,10 +214,11 @@ def handle(self, *args, **options):
schema.save(update_fields=dirty_fields)

logger.info(
f"Feature Loaded: {feature.application_slug}/{feature.slug} "
f"Feature {i}/{N} Loaded: {feature.application_slug}/{feature.slug} "
f"(version {feature.version})"
)

logger.info("attempting bulk create of {len(schemas_to_create)} schemas")
NimbusVersionedSchema.objects.bulk_create(schemas_to_create)

logger.info("Features Updated")
Expand Down
4 changes: 2 additions & 2 deletions experimenter/experimenter/nimbus_ui/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ class NimbusUIConstants:
ERROR_ROLLOUT_REENABLE_REQUIRES_CURRENT_PHASE = (
"Cannot duplicate the final phase because this rollout has no current phase."
)
MESSAGE_CONSULT_URL = "https://mozilla-hub.atlassian.net/wiki/spaces/FPS/pages/2542436381/OMC+Team+2026+-+Onboarding+Messaging+Communication#OMC-Intake---Requesting-work%2FCode-Reviews%2FConsultation"

RISK_MESSAGE_URL = "https://mozilla-hub.atlassian.net/wiki/spaces/FIREFOX/pages/208308555/Message+Consult+Creation"
REVIEW_URL = "https://experimenter.info/getting-started/for-reviewers"
VALIDATING_EXPERIMENTS_URL = (
"https://experimenter.info/data-analysis/validating-experiments/"
Expand Down Expand Up @@ -193,7 +193,7 @@ class NimbusUIConstants:
}
OVERVIEW_PAGE_LINKS = {
"risk_link": "https://mana.mozilla.org/wiki/display/FIREFOX/Pref-Flip+and+Add-On+Experiments#PrefFlipandAddOnExperiments-Doesthishavehighrisktothebrand?",
"message_consult_link": "https://mozilla-hub.atlassian.net/wiki/spaces/FIREFOX/pages/208308555/Message+Consult+Creation",
"message_consult_link": "https://mozilla-hub.atlassian.net/wiki/spaces/FPS/pages/2542436381/OMC+Team+2026+-+Onboarding+Messaging+Communication#OMC-Intake---Requesting-work%2FCode-Reviews%2FConsultation",
"revenue_risk_link": "https://experimenter.info/workflow/risk-mitigation#vp-sign-off",
"partner_related_risk_link": "https://experimenter.info/workflow/risk-mitigation#legal-sign-off",
}
Expand Down
Loading
Loading