diff --git a/experimenter/experimenter/nimbus_ui/constants.py b/experimenter/experimenter/nimbus_ui/constants.py
index 1f23e1a99..db5678a9a 100644
--- a/experimenter/experimenter/nimbus_ui/constants.py
+++ b/experimenter/experimenter/nimbus_ui/constants.py
@@ -275,6 +275,15 @@ class NimbusUIConstants:
"All statistically significant changes that have occurred in the experiment"
)
NOTABLE_CHANGES_ABSENT_TEXT = "There are no notable changes in this experiment"
+ METRIC_ERRORS_TOOLTIP = "No data available"
+ METRIC_ERRORS_TITLE = "Metric unavailable"
+ METRIC_ERRORS_TEXT = "Other metrics may not be affected."
+ METRIC_NO_DATA_TOOLTIP = "Not available yet"
+ METRIC_NO_DATA_TITLE = "Not available yet"
+ METRIC_NO_DATA_TEXT = (
+ "The analysis window for this metric has not been computed yet. "
+ "Results will appear once it has."
+ )
FEATURE_MONITORING_CARD_TITLE = "Feature Monitoring"
FEATURE_MONITORING_OPEN_DASHBOARD_TEXT = "Open in Grafana"
FEATURE_MONITORING_DASHBOARD_INFO = (
diff --git a/experimenter/experimenter/nimbus_ui/templates/nimbus_experiments/results-inner.html b/experimenter/experimenter/nimbus_ui/templates/nimbus_experiments/results-inner.html
index a5bd292b3..7ed798e13 100644
--- a/experimenter/experimenter/nimbus_ui/templates/nimbus_experiments/results-inner.html
+++ b/experimenter/experimenter/nimbus_ui/templates/nimbus_experiments/results-inner.html
@@ -272,9 +272,9 @@
{{ metric.friendly_name }}
-
+ title="{% if metric.has_errors %}{{ NimbusUIConstants.METRIC_ERRORS_TOOLTIP }}{% elif not metric.has_data %}{{ NimbusUIConstants.METRIC_NO_DATA_TOOLTIP }}{% elif metric.overall_change == 'positive' %}Positive changes{% elif metric.overall_change == 'negative' %}Negative changes{% elif metric.overall_change == 'mixed' %}Mixed changes{% elif metric.overall_change == 'neutral' %}No notable change{% endif %}">
{% if not forloop.last %}
·{% endif %}
@@ -389,14 +389,24 @@
{{ area }}
{% if metric.has_errors or not metric.has_data %}
{% if forloop.parentloop.parentloop.first %}
-
-
-
Metric unavailable
-
Other metrics may not be affected.
-
Contact Experimenter Support
-
+ {% if metric.has_errors %}
+
+
+
{{ NimbusUIConstants.METRIC_ERRORS_TITLE }}
+
{{ NimbusUIConstants.METRIC_ERRORS_TEXT }}
+
Contact Experimenter Support
+
+ {% else %}
+
+
+
{{ NimbusUIConstants.METRIC_NO_DATA_TITLE }}
+
{{ NimbusUIConstants.METRIC_NO_DATA_TEXT }}
+
+ {% endif %}
{% else %}
diff --git a/experimenter/experimenter/nimbus_ui/tests/test_views.py b/experimenter/experimenter/nimbus_ui/tests/test_views.py
index 376dd2132..930fc35c8 100644
--- a/experimenter/experimenter/nimbus_ui/tests/test_views.py
+++ b/experimenter/experimenter/nimbus_ui/tests/test_views.py
@@ -3864,6 +3864,94 @@ def test_results_template_fragment_used_with_htmx(self):
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, "nimbus_experiments/results-fragment.html")
+ def create_experiment_with_kpi_results(self, errors):
+ experiment = NimbusExperimentFactory.create_with_lifecycle(
+ NimbusExperimentFactory.Lifecycles.ENDING_APPROVE_APPROVE,
+ application=NimbusExperiment.Application.DESKTOP,
+ primary_outcomes=[],
+ secondary_outcomes=[],
+ is_rollout=False,
+ )
+ experiment.delete_branches()
+ experiment.reference_branch = NimbusBranchFactory.create(
+ experiment=experiment, name="Control", slug="control"
+ )
+ NimbusBranchFactory.create(
+ experiment=experiment, name="Treatment A", slug="treatment-a"
+ )
+ empty_metric = {
+ "absolute": {"all": [], "first": {}},
+ "difference": {"control": {"all": [], "first": {}}},
+ "relative_uplift": {"control": {"all": [], "first": {}}},
+ }
+ branch_data = {
+ "branch_data": {
+ "other_metrics": {NimbusConstants.RETENTION: empty_metric},
+ }
+ }
+ experiment.results_data = {
+ "v3": {
+ "metadata": {
+ "metrics": {
+ NimbusConstants.RETENTION: {"friendly_name": "Retention"},
+ }
+ },
+ "errors": errors,
+ "overall": {
+ "enrollments": {
+ "all": {
+ "control": branch_data,
+ "treatment-a": branch_data,
+ }
+ }
+ },
+ }
+ }
+ experiment.save()
+ return experiment
+
+ def test_results_view_renders_error_state_for_metric_with_errors(self):
+ experiment = self.create_experiment_with_kpi_results(
+ {
+ NimbusConstants.RETENTION: [
+ {"analysis_basis": "enrollments", "segment": "all"}
+ ]
+ }
+ )
+
+ response = self.client.get(
+ reverse(
+ "nimbus-ui-results",
+ kwargs={"slug": experiment.slug},
+ query={"reference_branch": "control"},
+ ),
+ )
+
+ self.assertEqual(response.status_code, 200)
+ self.assertContains(response, "fa-triangle-exclamation text-warning")
+ self.assertContains(response, NimbusUIConstants.METRIC_ERRORS_TOOLTIP)
+ self.assertContains(response, NimbusUIConstants.METRIC_ERRORS_TITLE)
+ self.assertContains(response, escape(NimbusUIConstants.METRIC_ERRORS_TEXT))
+ self.assertContains(response, "Contact Experimenter Support")
+
+ def test_results_view_renders_no_data_state_for_metric_without_data(self):
+ experiment = self.create_experiment_with_kpi_results({})
+
+ response = self.client.get(
+ reverse(
+ "nimbus-ui-results",
+ kwargs={"slug": experiment.slug},
+ query={"reference_branch": "control"},
+ ),
+ )
+
+ self.assertEqual(response.status_code, 200)
+ self.assertContains(response, "fa-circle-info text-info")
+ self.assertContains(response, NimbusUIConstants.METRIC_NO_DATA_TOOLTIP)
+ self.assertContains(response, NimbusUIConstants.METRIC_NO_DATA_TEXT)
+ self.assertNotContains(response, NimbusUIConstants.METRIC_ERRORS_TITLE)
+ self.assertNotContains(response, "Contact Experimenter Support")
+
@parameterized.expand(
[
(