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
9 changes: 9 additions & 0 deletions experimenter/experimenter/nimbus_ui/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -272,9 +272,9 @@ <h4 class="mb-0">{{ area }}</h4>
<div>
<small class="text-muted">{{ metric.friendly_name }}</small>
<small>
<i class="fa-solid ms-1 me-2 {% if metric.has_errors or not metric.has_data %}fa-triangle-exclamation text-warning{% elif metric.overall_change == 'positive' %}fa-up-long text-success{% elif metric.overall_change == 'negative' %}fa-down-long text-danger{% elif metric.overall_change == 'mixed' %}fa-up-down text-warning{% elif metric.overall_change == 'neutral' %}fa-left-long text-muted{% endif %}"
<i class="fa-solid ms-1 me-2 {% if metric.has_errors %}fa-triangle-exclamation text-warning{% elif not metric.has_data %}fa-circle-info text-info{% elif metric.overall_change == 'positive' %}fa-up-long text-success{% elif metric.overall_change == 'negative' %}fa-down-long text-danger{% elif metric.overall_change == 'mixed' %}fa-up-down text-warning{% elif metric.overall_change == 'neutral' %}fa-left-long text-muted{% endif %}"
data-bs-toggle="tooltip"
title="{% if metric.has_errors or not metric.has_data %}No data available{% 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 %}">
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 %}">
</i>
</small>
{% if not forloop.last %}<span class="text-muted">&middot;</span>{% endif %}
Expand Down Expand Up @@ -389,14 +389,24 @@ <h4 class="mb-0">{{ area }}</h4>
{% if metric.has_errors or not metric.has_data %}
{% if forloop.parentloop.parentloop.first %}
<div class="d-flex align-items-center" style="height: 100px;">
<div class="position-absolute alert alert-warning d-flex align-items-center justify-content-center gap-2 end-0 start-0 {% if branch_data|length > 4 %}mx-5{% else %}mx-3{% endif %} mb-0">
<i class="fa-solid fa-triangle-exclamation fs-5"></i>
<p class="mb-0 fw-semibold">Metric unavailable</p>
<p class="mb-0">Other metrics may not be affected.</p>
<a class="btn btn-secondary bg-secondary-subtle border-0 text-body fw-semibold"
target="_blank"
href="{{ ask_experimenter_slack_link }}">Contact Experimenter Support</a>
</div>
{% if metric.has_errors %}
<div class="position-absolute alert alert-warning d-flex align-items-center justify-content-center gap-2 end-0 start-0 {% if branch_data|length > 4 %}mx-5{% else %}mx-3{% endif %} mb-0"
style="min-height: 100px">
<i class="fa-solid fa-triangle-exclamation fs-5"></i>
<p class="mb-0 fw-semibold">{{ NimbusUIConstants.METRIC_ERRORS_TITLE }}</p>
<p class="mb-0">{{ NimbusUIConstants.METRIC_ERRORS_TEXT }}</p>
<a class="btn btn-secondary bg-secondary-subtle border-0 text-body fw-semibold"
target="_blank"
href="{{ ask_experimenter_slack_link }}">Contact Experimenter Support</a>
</div>
{% else %}
<div class="position-absolute alert alert-info d-flex align-items-center justify-content-center gap-2 end-0 start-0 {% if branch_data|length > 4 %}mx-5{% else %}mx-3{% endif %} mb-0"
style="min-height: 100px">
<i class="fa-solid fa-circle-info fs-5"></i>
<p class="mb-0 fw-semibold">{{ NimbusUIConstants.METRIC_NO_DATA_TITLE }}</p>
<p class="mb-0">{{ NimbusUIConstants.METRIC_NO_DATA_TEXT }}</p>
</div>
{% endif %}
</div>
{% else %}
<div class="invisible" style="height: 100px;" aria-hidden="true"></div>
Expand Down
88 changes: 88 additions & 0 deletions experimenter/experimenter/nimbus_ui/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
[
(
Expand Down
Loading