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
57 changes: 57 additions & 0 deletions experimenter/experimenter/experiments/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1258,6 +1258,63 @@ def timeline(self):

return timeline_entries

@property
def default_metrics(self):
analysis_data = self.results_data.get("v3", {}) if self.results_data else {}
other_metrics = analysis_data.get("other_metrics", {})
metrics_metadata = analysis_data.get("metadata", {}).get("metrics", {})
default_metrics = {}

for value in other_metrics.values():
for metricKey, metricValue in value.items():
default_metrics[metricKey] = metrics_metadata.get(metricKey, {}).get(
"friendlyName", metricValue
)

return default_metrics

def get_branch_data(self, analysis_basis, selected_segment):
overall_results = (
(
self.results_data.get("v3", {})
.get("overall", {})
.get(analysis_basis, {})
.get(selected_segment, {})
)
if self.results_data
else {}
)

branch_data = []

for branch in self.branches.all().prefetch_related("screenshots"):
slug = branch.slug
participant_metrics = (
overall_results.get(slug, {})
.get("branch_data", {})
.get("other_metrics", {})
.get("identity", {})
)
num_participants = (
participant_metrics.get("absolute", {}).get("first", {}).get("point", 0)
)

branch_data.insert(
0
if self.reference_branch and slug == self.reference_branch.slug
else len(branch_data),
{
"slug": slug,
"name": branch.name,
"screenshots": branch.screenshots.all,
"description": branch.description,
"percentage": participant_metrics.get("percent"),
"num_participants": num_participants,
},
)

return branch_data

@property
def experiment_active_status(self):
timeline = self.timeline()
Expand Down
82 changes: 82 additions & 0 deletions experimenter/experimenter/experiments/tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2224,6 +2224,88 @@ def test_sidebar_links_sets_active_flag_correctly(self):
f"{link['title']} should not be active for path {path}",
)

def test_default_metrics_set_on_creation(self):
experiment = NimbusExperimentFactory.create()

experiment.results_data = {
"v3": {
"other_metrics": {"group": {"metricA": "Metric A"}},
"metadata": {
"metrics": {"metricA": {"friendlyName": "Friendly Metric A"}},
},
}
}

experiment.save()

self.assertEqual(
experiment.default_metrics,
{"metricA": "Friendly Metric A"},
)

def test_get_branch_data_returns_correct_data(self):
experiment = NimbusExperimentFactory.create()
branch_a = NimbusBranchFactory.create(
experiment=experiment, name="Branch A", slug="branch-a"
)
branch_b = NimbusBranchFactory.create(
experiment=experiment, name="Branch B", slug="branch-b"
)

experiment.results_data = {
"v3": {
"overall": {
"enrollments": {
"all": {
"branch-a": {
"branch_data": {
"other_metrics": {
"identity": {
"absolute": {"first": {"point": 150}},
"percent": 12,
}
}
}
},
"branch-b": {
"branch_data": {
"other_metrics": {
"identity": {
"absolute": {"first": {"point": 75}},
"percent": 88,
}
}
}
},
}
}
}
}
}
experiment.save()

result = experiment.get_branch_data("enrollments", "all")

self.assertEqual(len(result), 4)

index_map = {item.get("slug"): i for i, item in enumerate(result)}
first = result[index_map.get("branch-a")]
second = result[index_map.get("branch-b")]

# Validate first branch
self.assertEqual(first["slug"], "branch-a")
self.assertEqual(first["name"], "Branch A")
self.assertEqual(first["percentage"], 12)
self.assertEqual(first["num_participants"], 150)
self.assertEqual(first["description"], branch_a.description)

# Validate second branch
self.assertEqual(second["slug"], "branch-b")
self.assertEqual(second["name"], "Branch B")
self.assertEqual(second["percentage"], 88)
self.assertEqual(second["num_participants"], 75)
self.assertEqual(second["description"], branch_b.description)

def test_conclusion_recommendation_labels(self):
recommendations = list(NimbusConstants.ConclusionRecommendation)
experiment = NimbusExperimentFactory.create_with_lifecycle(
Expand Down
7 changes: 7 additions & 0 deletions experimenter/experimenter/nimbus_ui/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,13 @@ class NimbusUIConstants:
LIVE_MONITOR_TOOLTIP = """Live Monitoring shows enrollment/unenrollment for this
delivery"""

OVERVIEW_SECTIONS = [
"Hypothesis",
"Branch overview",
"Key takeaways",
"Next steps",
"Project impact",
]
FEATURE_PAGE_LINKS = {
"feature_learn_more_url": "https://experimenter.info/for-product#track-your-feature-health",
"deliveries_table_tooltip": """This shows all Nimbus experiments, rollouts, Labs
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{% load nimbus_extras %}

<div class="d-flex gap-4">
{% with shot=branch.screenshots.first %}
{% if shot and shot.image %}
<img src="{{ shot.image.url }}"
alt="{{ shot.description|default:branch.name }}"
class="border border-secondary-subtle rounded w-50"
style="height: 120px;
object-fit: cover" />
{% else %}
<div class="border border-secondary-subtle rounded text-center d-flex align-items-center justify-content-center w-50"
style="height: 120px">
<small class="text-muted">No screenshot</small>
</div>
{% endif %}
{% endwith %}
<div class="w-50">
<span class="badge rounded-pill text-body-secondary bg-body-secondary border border-secondary-subtle d-flex-inline d-inline-flex flex-wrap gap-2">
{{ branch.name }}
<div class="d-inline-flex gap-1 fw-normal">
{{ branch.percentage|floatformat:"0g" }}%
<span>&middot;</span>
{{ branch.num_participants|short_number }} users
</div>
</span>
<h6 class="mt-2">{{ branch.name }}</h6>
<p class="text-muted">{{ branch.description|truncatechars:100 }}</p>
</div>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
{% extends "nimbus_experiments/experiment_base.html" %}

{% load static %}
{% load nimbus_extras %}

{% block title %}{{ experiment.name }}{% endblock %}

{% block main_content %}
<div class="card px-5 py-4 shadow-sm">
<h3>Overview</h3>
<div class="d-inline-flex gap-4">
{% for section in overview_sections %}
<p class="text-muted">{{ section }}</p>
{% if not forloop.last %}<span class="text-muted">&middot;</span>{% endif %}
{% endfor %}
</div>
<div class="d-flex flex-column gap-4">
<div class="card p-5 mt-4">
<div class="row">
<div class="col-xxl">
<h5>Hypothesis</h5>
{% with hyp=experiment.hypothesis|default:"No hypothesis set." %}
{% if hyp|length > 300 %}
<p>
{{ hyp|truncatechars:300 }}
<a href="{{ experiment.get_detail_url }}" class="ms-1">More</a>
</p>
{% else %}
<p>{{ hyp }}</p>
{% endif %}
{% endwith %}
</div>
<div class="col-xxl">
<div class="row">
<div class="col">
<p class="text-muted mb-0">Product Owner</p>
<p>{{ experiment.owner|default:"No product owner set." }}</p>
</div>
<div class="col">
<p class="text-muted mb-0">Advanced Targeting</p>
<p>{{ experiment.targeting_config.name }}</p>
</div>
<div class="col">
<p class="text-muted mb-0">Application</p>
<p>{{ experiment.application|default:"Unknown" }}</p>
</div>
</div>
<div class="row">
<div class="col">
<p class="text-muted mb-0">Feature tags</p>
{% for tag in experiment.tags.all %}
<span class="badge bg-light text-dark">{{ tag.name }}</span>
{% empty %}
<p>None</p>
{% endfor %}
</div>
<div class="col">
<p class="text-muted mb-0">Localization</p>
{% for locale in experiment.locales.all %}
<p class="mb-0">{{ locale.name }}</p>
{% empty %}
<p class="mb-0">None</p>
{% endfor %}
</div>
<div class="col">
<p class="text-muted mb-0">Version</p>
<p>
{{ experiment.firefox_min_version }}
{% if experiment.firefox_max_version %}
- {{ experiment.firefox_max_version }}
{% else %}
+
{% endif %}
</p>
</div>
</div>
</div>
</div>
</div>
<div class="card p-5">
<div class="row row-cols-2 row-cols-xxl-3 g-4">
{% for branch in branch_data %}
<div class="col">
{% include "common/branch_card.html" with branch=branch %}

</div>
{% endfor %}
</div>
</div>
<div class="card p-5">
<div class="row">
<div class="col">
<h5>Key takeaways</h5>
<p class="text-muted">
{{ experiment.key_takeaways|default:"Highlight the most important learnings or patterns from this experiment." }}
</p>
</div>
<div class="col">
<h5>Next steps</h5>
<p class="text-muted">
{{ experiment.key_takeaways|default:"Outline what should happen next based on these results — fixes, follow-ups, or future tests." }}
</p>
</div>
<div class="col">
<div class="d-flex align-items-start gap-2">
{% comment %} TODO: {% endcomment %}
<h5>Project Impact</h5>
<span class="badge rounded-pill text-bg-warning bg-opacity-25 border border-secondary-subtle">Incomplete</span>
</div>
<p class="text-muted">
{{ experiment.key_takeaways|default:"Set an impact rating so others can understand the scale of this experiment’s effect." }}
</p>
</div>
</div>
</div>
</div>
</div>
{% endblock main_content %}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import json
from datetime import date

import humanize
from django import template
from django.utils.safestring import mark_safe

Expand Down Expand Up @@ -303,3 +304,20 @@ def experiment_date_progress(experiment):
result["days_text"] = "N/A"

return result


@register.filter
def short_number(value, precision=1):
formatted_number = str(value)
formatted_number_components = humanize.intword(value, format=f"%.{precision}f").split(
" "
)
number = formatted_number_components[0]

if len(formatted_number_components) > 1:
magnitude = formatted_number_components[1]
if magnitude == "thousand":
magnitude = "K"
formatted_number = f"{number}{magnitude[0].capitalize()}"

return formatted_number
14 changes: 14 additions & 0 deletions experimenter/experimenter/nimbus_ui/tests/test_filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
qa_icon_info,
remove_underscores,
render_channel_icons,
short_number,
status_icon_info,
)
from experimenter.nimbus_ui.templatetags.nimbus_extras import (
Expand Down Expand Up @@ -120,6 +121,19 @@ def test_channel_icon_info(self, channel):
self.assertEqual(result["icon"], expected["icon"])
self.assertEqual(result["color"], expected["color"])

def test_short_number(self):
self.assertEqual(short_number(100), "100")
self.assertEqual(short_number(1000), "1.0K")
self.assertEqual(short_number(1500), "1.5K")
self.assertEqual(short_number(123456), "123.5K")
self.assertEqual(short_number(510951), "511.0K")
self.assertEqual(short_number(1000000), "1.0M")
self.assertEqual(short_number(13500000), "13.5M")

def test_invalid_short_number(self):
self.assertEqual(short_number("invalid"), "invalid")
self.assertEqual(short_number(None), "None")


class TestHomeFilters(AuthTestCase):
def _make_all_qa_statuses(self):
Expand Down
4 changes: 2 additions & 2 deletions experimenter/experimenter/nimbus_ui/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -3036,7 +3036,7 @@ def test_results_view_context_and_defaults(
experiment.save()

response = self.client.get(
reverse("nimbus-ui-results", kwargs={"slug": experiment.slug}),
reverse("nimbus-ui-new-results", kwargs={"slug": experiment.slug}),
)

self.assertEqual(response.status_code, 200)
Expand Down Expand Up @@ -3069,7 +3069,7 @@ def test_results_view_query_param_overrides(self):

response = self.client.get(
reverse(
"nimbus-ui-results",
"nimbus-ui-new-results",
kwargs={"slug": experiment.slug},
query={
"reference_branch": "treatment-a",
Expand Down
Loading