Skip to content
Merged
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
41 changes: 41 additions & 0 deletions docs/experimenter/openapi-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -1237,6 +1237,35 @@
"takeaways_summary": {
"type": "string",
"nullable": true
},
"conclusion_recommendations": {
"type": "string",
"readOnly": true
},
"project_impact": {
"enum": [
"HIGH",
"MODERATE",
"TARGETED"
],
"type": "string",
"nullable": true
},
"next_steps": {
"type": "string",
"nullable": true
},
"reviewer_emails": {
"type": "string",
"readOnly": true
},
"editor_emails": {
"type": "string",
"readOnly": true
},
"analysis_errors_count": {
"type": "string",
"readOnly": true
}
},
"required": [
Expand Down Expand Up @@ -1282,6 +1311,14 @@
"type": "string",
"readOnly": true
},
"reviewer_emails": {
"type": "string",
"readOnly": true
},
"editor_emails": {
"type": "string",
"readOnly": true
},
"application_display": {
"type": "string",
"readOnly": true
Expand Down Expand Up @@ -1473,6 +1510,10 @@
"results_data": {
"type": "string",
"readOnly": true
},
"analysis_errors_by_key": {
"type": "string",
"readOnly": true
}
},
"required": [
Expand Down
41 changes: 41 additions & 0 deletions docs/experimenter/swagger-ui.html
Original file line number Diff line number Diff line change
Expand Up @@ -1249,6 +1249,35 @@
"takeaways_summary": {
"type": "string",
"nullable": true
},
"conclusion_recommendations": {
"type": "string",
"readOnly": true
},
"project_impact": {
"enum": [
"HIGH",
"MODERATE",
"TARGETED"
],
"type": "string",
"nullable": true
},
"next_steps": {
"type": "string",
"nullable": true
},
"reviewer_emails": {
"type": "string",
"readOnly": true
},
"editor_emails": {
"type": "string",
"readOnly": true
},
"analysis_errors_count": {
"type": "string",
"readOnly": true
}
},
"required": [
Expand Down Expand Up @@ -1294,6 +1323,14 @@
"type": "string",
"readOnly": true
},
"reviewer_emails": {
"type": "string",
"readOnly": true
},
"editor_emails": {
"type": "string",
"readOnly": true
},
"application_display": {
"type": "string",
"readOnly": true
Expand Down Expand Up @@ -1485,6 +1522,10 @@
"results_data": {
"type": "string",
"readOnly": true
},
"analysis_errors_by_key": {
"type": "string",
"readOnly": true
}
},
"required": [
Expand Down
21 changes: 21 additions & 0 deletions experimenter/experimenter/experiments/api/v5/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,9 @@ class NimbusExperimentCsvSerializer(serializers.ModelSerializer):
feature_configs = serializers.SerializerMethodField()
experiment_summary = serializers.CharField(source="experiment_url")
results_url = serializers.SerializerMethodField()
conclusion_recommendations = serializers.SerializerMethodField()
reviewer_emails = serializers.SerializerMethodField()
editor_emails = serializers.SerializerMethodField()

class Meta:
model = NimbusExperiment
Expand All @@ -249,6 +252,12 @@ class Meta:
"takeaways_gain_amount",
"takeaways_qbr_learning",
"takeaways_summary",
"conclusion_recommendations",
"project_impact",
"next_steps",
"reviewer_emails",
"editor_emails",
"analysis_errors_count",
]

def get_feature_configs(self, obj):
Expand All @@ -260,6 +269,15 @@ def get_feature_configs(self, obj):
def get_results_url(self, obj):
return f"{obj.experiment_url}results" if obj.results_ready else ""

def get_conclusion_recommendations(self, obj):
return ",".join(obj.conclusion_recommendation_labels)

def get_reviewer_emails(self, obj):
return ",".join(obj.reviewer_emails)

def get_editor_emails(self, obj):
return ",".join(obj.editor_emails)


class NimbusExperimentYamlSerializer(serializers.ModelSerializer):
owner = serializers.SlugRelatedField(read_only=True, slug_field="email")
Expand Down Expand Up @@ -294,6 +312,8 @@ class Meta:
"hypothesis",
"is_rollout",
"owner",
"reviewer_emails",
"editor_emails",
"application_display",
"channels",
"feature_configs",
Expand Down Expand Up @@ -338,6 +358,7 @@ class Meta:
"excluded_experiments",
"parent_experiment",
"results_data",
"analysis_errors_by_key",
]

def get_hypothesis(self, obj):
Expand Down
18 changes: 16 additions & 2 deletions experimenter/experimenter/experiments/api/v5/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import yaml
from dateutil.relativedelta import relativedelta
from django.db.models import F
from django.db.models import F, Prefetch
from django.http import HttpResponse
from rest_framework.generics import ListAPIView, UpdateAPIView
from rest_framework.pagination import PageNumberPagination
Expand All @@ -32,7 +32,15 @@ class NimbusExperimentCsvListView(CachedListMixin, ListAPIView):
cache_content_type = "text/csv; charset=utf-8"
queryset = (
NimbusExperiment.objects.select_related("owner")
.prefetch_related("feature_configs")
.prefetch_related(
"feature_configs",
Prefetch(
"changes",
queryset=NimbusChangeLog.objects.select_related("changed_by").defer(
"experiment_data"
),
),
)
.filter(is_archived=False)
)
serializer_class = NimbusExperimentCsvSerializer
Expand Down Expand Up @@ -195,6 +203,12 @@ class NimbusExperimentYamlListView(CachedListMixin, ListAPIView):
"tags",
"required_experiments",
"excluded_experiments",
Prefetch(
"changes",
queryset=NimbusChangeLog.objects.select_related("changed_by").defer(
"experiment_data"
),
),
)
.filter(is_archived=False, status=NimbusExperiment.Status.COMPLETE)
.order_by(F("_start_date").desc(nulls_last=True), "-id")
Expand Down
35 changes: 35 additions & 0 deletions experimenter/experimenter/experiments/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2773,6 +2773,41 @@ def has_results_errors(self):
return True
return False

@property
def analysis_errors_by_key(self):
counts = {}
if self.results_data:
errors = self.results_data.get("v3", {}).get("errors", {})
for key, key_errors in errors.items():
if key_errors:
counts[key] = len(key_errors)
return counts

@property
def analysis_errors_count(self):
return sum(self.analysis_errors_by_key.values())

@property
def reviewer_emails(self):
return sorted(
{
change.changed_by.email
for change in self.changes.all()
if change.old_publish_status == self.PublishStatus.REVIEW
and change.new_publish_status == self.PublishStatus.APPROVED
}
)

@property
def editor_emails(self):
return sorted(
{
change.changed_by.email
for change in self.changes.all()
if change.changed_by.email != settings.KINTO_DEFAULT_CHANGELOG_USER
}
)

def get_invalid_fields_errors(self, serializer_class=None):
if serializer_class is None:
serializer = self._review_serializer
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@
from experimenter.experiments.api.v5.serializers import NimbusExperimentCsvSerializer
from experimenter.experiments.models import NimbusExperiment
from experimenter.experiments.tests.factories import (
NimbusChangeLogFactory,
NimbusExperimentFactory,
NimbusFeatureConfigFactory,
)
from experimenter.openidc.tests.factories import UserFactory


class TestNimbusExperimentCsvSerializer(TestCase):
Expand Down Expand Up @@ -45,5 +47,65 @@ def test_serializer_outputs_expected_schema_with_results_link(self):
"takeaways_gain_amount": experiment.takeaways_gain_amount,
"takeaways_qbr_learning": experiment.takeaways_qbr_learning,
"takeaways_summary": experiment.takeaways_summary,
"conclusion_recommendations": "",
"project_impact": None,
"next_steps": None,
"reviewer_emails": experiment.owner.email,
"editor_emails": experiment.owner.email,
"analysis_errors_count": 0,
},
)

def test_serializer_outputs_reviewers_editors_errors_and_takeaways(self):
application = NimbusExperiment.Application.DESKTOP
feature_config = NimbusFeatureConfigFactory.create(application=application)

experiment = NimbusExperimentFactory.create_with_lifecycle(
NimbusExperimentFactory.Lifecycles.ENDING_APPROVE_APPROVE,
application=application,
feature_configs=[feature_config],
owner=UserFactory.create(email="owner@example.com"),
conclusion_recommendations=[
NimbusExperiment.ConclusionRecommendation.RERUN,
NimbusExperiment.ConclusionRecommendation.GRADUATE,
],
project_impact=NimbusExperiment.ProjectImpact.HIGH,
next_steps="Ship it.",
results_data={
"v3": {
"errors": {
"ad_clicks": [
{"analysis_basis": "enrollments", "segment": "all"},
{"analysis_basis": "exposures", "segment": "all"},
],
"experiment": [{"analysis_basis": "enrollments"}],
},
}
},
)
NimbusChangeLogFactory.create(
experiment=experiment,
changed_by=UserFactory.create(email="reviewer@example.com"),
old_publish_status=NimbusExperiment.PublishStatus.REVIEW,
new_publish_status=NimbusExperiment.PublishStatus.APPROVED,
)
NimbusChangeLogFactory.create(
experiment=experiment,
changed_by=UserFactory.create(email="editor@example.com"),
old_publish_status=NimbusExperiment.PublishStatus.IDLE,
new_publish_status=NimbusExperiment.PublishStatus.IDLE,
)

data = NimbusExperimentCsvSerializer(experiment).data

self.assertEqual(
data["reviewer_emails"], "owner@example.com,reviewer@example.com"
)
self.assertEqual(
data["editor_emails"],
"editor@example.com,owner@example.com,reviewer@example.com",
)
self.assertEqual(data["analysis_errors_count"], 3)
self.assertEqual(data["conclusion_recommendations"], "Rerun,Graduate")
self.assertEqual(data["project_impact"], NimbusExperiment.ProjectImpact.HIGH)
self.assertEqual(data["next_steps"], "Ship it.")
Loading
Loading