mirror of
https://github.com/hanzoai/sentinel.git
synced 2026-09-29 22:34:33 +00:00
2385 lines
87 KiB
Python
2385 lines
87 KiB
Python
import datetime
|
|
from unittest.mock import patch
|
|
from uuid import uuid4
|
|
|
|
import pytest
|
|
from django.urls import reverse
|
|
from django.utils import timezone
|
|
|
|
from sentry import release_health
|
|
from sentry.models.releaseprojectenvironment import ReleaseProjectEnvironment
|
|
from sentry.release_health.metrics import MetricsReleaseHealthBackend
|
|
from sentry.snuba.metrics import to_intervals
|
|
from sentry.testutils.cases import APITestCase, BaseMetricsTestCase, SnubaTestCase
|
|
from sentry.testutils.helpers.datetime import freeze_time
|
|
from sentry.testutils.helpers.link_header import parse_link_header
|
|
from sentry.testutils.silo import region_silo_test
|
|
from sentry.utils.cursors import Cursor
|
|
from sentry.utils.dates import to_timestamp
|
|
|
|
pytestmark = pytest.mark.sentry_metrics
|
|
|
|
|
|
def result_sorted(result):
|
|
"""sort the groups of the results array by the `by` object, ensuring a stable order"""
|
|
|
|
def stable_dict(d):
|
|
return tuple(sorted(d.items(), key=lambda t: t[0]))
|
|
|
|
result["groups"].sort(key=lambda group: stable_dict(group["by"]))
|
|
return result
|
|
|
|
|
|
ONE_DAY_AGO = datetime.datetime.now(tz=datetime.UTC) - datetime.timedelta(days=1)
|
|
TWO_DAYS_AGO = datetime.datetime.now(tz=datetime.UTC) - datetime.timedelta(days=2)
|
|
MOCK_DATETIME = ONE_DAY_AGO.replace(hour=12, minute=27, second=28, microsecond=303000)
|
|
MOCK_DATETIME_PLUS_TEN_MINUTES = MOCK_DATETIME + datetime.timedelta(minutes=10)
|
|
MOCK_DATETIME_PLUS_ONE_HOUR = MOCK_DATETIME + datetime.timedelta(hours=1)
|
|
SNUBA_TIME_FORMAT = "%Y-%m-%dT%H:%M:%SZ"
|
|
MOCK_DATETIME_START_OF_DAY = MOCK_DATETIME.replace(hour=0, minute=0, second=0)
|
|
|
|
TIMESTAMP = to_timestamp(MOCK_DATETIME)
|
|
RECEIVED = TIMESTAMP
|
|
SESSION_STARTED = TIMESTAMP // 3600 * 3600 # round to the hour
|
|
|
|
TEMPLATE = {
|
|
"distinct_id": "00000000-0000-0000-0000-000000000000",
|
|
"status": "exited",
|
|
"seq": 0,
|
|
"release": "[email protected]",
|
|
"environment": "production",
|
|
"retention_days": 90,
|
|
"duration": 123.4,
|
|
"errors": 0,
|
|
"started": SESSION_STARTED,
|
|
"received": RECEIVED,
|
|
}
|
|
|
|
|
|
def make_duration(kwargs):
|
|
"""Randomish but deterministic duration"""
|
|
return float(len(str(kwargs)))
|
|
|
|
|
|
def make_session(project, **kwargs):
|
|
return dict(
|
|
dict(
|
|
TEMPLATE,
|
|
session_id=uuid4().hex,
|
|
org_id=project.organization_id,
|
|
project_id=project.id,
|
|
duration=make_duration(kwargs),
|
|
),
|
|
**kwargs,
|
|
)
|
|
|
|
|
|
def adjust_start(start: datetime.datetime, interval: int) -> datetime.datetime:
|
|
# align start and end to the beginning of the intervals
|
|
start, _end, _num_intervals = to_intervals(
|
|
start, start + datetime.timedelta(minutes=1), interval
|
|
)
|
|
return start
|
|
|
|
|
|
def adjust_end(end: datetime.datetime, interval: int) -> datetime.datetime:
|
|
# align start and end to the beginning of the intervals
|
|
_start, end, _num_intervals = to_intervals(end - datetime.timedelta(minutes=1), end, interval)
|
|
return end
|
|
|
|
|
|
class OrganizationSessionsEndpointTest(APITestCase, SnubaTestCase):
|
|
def setUp(self):
|
|
super().setUp()
|
|
self.setup_fixture()
|
|
|
|
def setup_fixture(self):
|
|
self.organization1 = self.organization
|
|
self.organization2 = self.create_organization()
|
|
self.organization3 = self.create_organization()
|
|
self.project1 = self.project
|
|
self.project2 = self.create_project()
|
|
self.project3 = self.create_project()
|
|
self.project4 = self.create_project(organization=self.organization2)
|
|
|
|
self.user2 = self.create_user(is_superuser=False)
|
|
self.create_member(
|
|
user=self.user2, organization=self.organization1, role="member", teams=[]
|
|
)
|
|
self.create_member(user=self.user, organization=self.organization3, role="admin", teams=[])
|
|
|
|
self.create_environment(self.project2, name="development")
|
|
|
|
self.bulk_store_sessions(
|
|
[
|
|
make_session(self.project1, started=SESSION_STARTED + 12 * 60),
|
|
make_session(self.project1, started=SESSION_STARTED + 24 * 60, release="[email protected]"),
|
|
make_session(self.project1, started=SESSION_STARTED - 60 * 60),
|
|
make_session(self.project1, started=SESSION_STARTED - 12 * 60 * 60),
|
|
make_session(self.project2, status="crashed"),
|
|
make_session(self.project2, environment="development"),
|
|
make_session(self.project3, errors=1, release="[email protected]"),
|
|
make_session(
|
|
self.project3,
|
|
distinct_id="39887d89-13b2-4c84-8c23-5d13d2102664",
|
|
started=SESSION_STARTED - 60 * 60,
|
|
),
|
|
make_session(
|
|
self.project3, distinct_id="39887d89-13b2-4c84-8c23-5d13d2102664", errors=1
|
|
),
|
|
make_session(self.project4),
|
|
]
|
|
)
|
|
|
|
def do_request(self, query, user=None, org=None):
|
|
self.login_as(user=user or self.user)
|
|
url = reverse(
|
|
"sentry-api-0-organization-sessions",
|
|
kwargs={"organization_slug": (org or self.organization).slug},
|
|
)
|
|
return self.client.get(url, query, format="json")
|
|
|
|
def test_empty_request(self):
|
|
response = self.do_request({})
|
|
|
|
assert response.status_code == 400, response.content
|
|
assert response.data == {"detail": 'Request is missing a "field"'}
|
|
|
|
def test_inaccessible_project(self):
|
|
response = self.do_request({"project": [self.project4.id]})
|
|
|
|
assert response.status_code == 403, response.content
|
|
assert response.data == {"detail": "You do not have permission to perform this action."}
|
|
|
|
def test_unknown_field(self):
|
|
response = self.do_request({"field": ["summ(session)"]})
|
|
|
|
assert response.status_code == 400, response.content
|
|
assert response.data == {"detail": 'Invalid field: "summ(session)"'}
|
|
|
|
def test_unknown_groupby(self):
|
|
response = self.do_request({"field": ["sum(session)"], "groupBy": ["environment_"]})
|
|
|
|
assert response.status_code == 400, response.content
|
|
assert response.data == {"detail": 'Invalid groupBy: "environment_"'}
|
|
|
|
def test_illegal_groupby(self):
|
|
response = self.do_request({"field": ["sum(session)"], "groupBy": ["issue.id"]})
|
|
|
|
assert response.status_code == 400, response.content
|
|
assert response.data == {"detail": 'Invalid groupBy: "issue.id"'}
|
|
|
|
def test_invalid_query(self):
|
|
response = self.do_request(
|
|
{"statsPeriod": "1d", "field": ["sum(session)"], "query": ["foo:bar"]}
|
|
)
|
|
|
|
assert response.status_code == 400, response.content
|
|
assert response.data["detail"] == "Invalid search filter: foo"
|
|
|
|
response = self.do_request(
|
|
{
|
|
"statsPeriod": "1d",
|
|
"field": ["sum(session)"],
|
|
"query": ["release:[email protected] (123)"],
|
|
}
|
|
)
|
|
|
|
assert response.status_code == 400, response.content
|
|
# TODO: it would be good to provide a better error here,
|
|
# since its not obvious where `message` comes from.
|
|
assert response.data["detail"] == "Invalid search filter: message"
|
|
|
|
def test_illegal_query(self):
|
|
response = self.do_request(
|
|
{"statsPeriod": "1d", "field": ["sum(session)"], "query": ["issue.id:123"]}
|
|
)
|
|
assert response.status_code == 400, response.content
|
|
assert response.data["detail"] == "Invalid search filter: issue.id"
|
|
|
|
def test_too_many_points(self):
|
|
# default statsPeriod is 90d
|
|
response = self.do_request({"field": ["sum(session)"], "interval": "1h"})
|
|
|
|
assert response.status_code == 400, response.content
|
|
assert response.data == {
|
|
"detail": "Your interval and date range would create too many results. "
|
|
"Use a larger interval, or a smaller date range."
|
|
}
|
|
|
|
@freeze_time(MOCK_DATETIME)
|
|
def test_future_request(self):
|
|
start = MOCK_DATETIME + datetime.timedelta(days=1)
|
|
end = MOCK_DATETIME + datetime.timedelta(days=2)
|
|
response = self.do_request(
|
|
{
|
|
"project": [-1],
|
|
"interval": "1h",
|
|
"field": ["sum(session)"],
|
|
"start": start.strftime(SNUBA_TIME_FORMAT),
|
|
"end": end.strftime(SNUBA_TIME_FORMAT),
|
|
}
|
|
)
|
|
assert response.status_code == 200, response.content
|
|
|
|
@freeze_time(MOCK_DATETIME)
|
|
def test_timeseries_interval(self):
|
|
response = self.do_request(
|
|
{"project": [-1], "statsPeriod": "1d", "interval": "1d", "field": ["sum(session)"]}
|
|
)
|
|
|
|
start_of_day_snuba_format = MOCK_DATETIME_START_OF_DAY.strftime(SNUBA_TIME_FORMAT)
|
|
previous_start_of_day_snuba_format = (
|
|
MOCK_DATETIME_START_OF_DAY - datetime.timedelta(days=1)
|
|
).strftime(SNUBA_TIME_FORMAT)
|
|
|
|
expected_start = MOCK_DATETIME_START_OF_DAY - datetime.timedelta(days=1)
|
|
expected_end = MOCK_DATETIME_START_OF_DAY + datetime.timedelta(days=1)
|
|
assert response.status_code == 200, response.content
|
|
assert result_sorted(response.data) == {
|
|
"start": expected_start.strftime(SNUBA_TIME_FORMAT),
|
|
"end": expected_end.strftime(SNUBA_TIME_FORMAT),
|
|
"query": "",
|
|
"intervals": [previous_start_of_day_snuba_format, start_of_day_snuba_format],
|
|
"groups": [
|
|
{"by": {}, "series": {"sum(session)": [0, 9]}, "totals": {"sum(session)": 9}}
|
|
],
|
|
}
|
|
|
|
response = self.do_request(
|
|
{"project": [-1], "statsPeriod": "1d", "interval": "6h", "field": ["sum(session)"]}
|
|
)
|
|
|
|
six_hours = 6 * 60 * 60
|
|
expected_start = adjust_start(TWO_DAYS_AGO.replace(hour=12, minute=0, second=0), six_hours)
|
|
expected_end = adjust_end(MOCK_DATETIME.replace(minute=28, second=0), six_hours)
|
|
assert response.status_code == 200, response.content
|
|
assert result_sorted(response.data) == {
|
|
"start": expected_start.strftime(SNUBA_TIME_FORMAT),
|
|
"end": expected_end.strftime(SNUBA_TIME_FORMAT),
|
|
"query": "",
|
|
"intervals": [
|
|
TWO_DAYS_AGO.replace(hour=12, minute=0, second=0).strftime(SNUBA_TIME_FORMAT),
|
|
TWO_DAYS_AGO.replace(hour=18, minute=0, second=0).strftime(SNUBA_TIME_FORMAT),
|
|
MOCK_DATETIME.replace(hour=0, minute=0, second=0).strftime(SNUBA_TIME_FORMAT),
|
|
MOCK_DATETIME.replace(hour=6, minute=0, second=0).strftime(SNUBA_TIME_FORMAT),
|
|
MOCK_DATETIME.replace(hour=12, minute=0, second=0).strftime(SNUBA_TIME_FORMAT),
|
|
],
|
|
"groups": [
|
|
{
|
|
"by": {},
|
|
"series": {"sum(session)": [0, 0, 1, 2, 6]},
|
|
"totals": {"sum(session)": 9},
|
|
}
|
|
],
|
|
}
|
|
|
|
@freeze_time(MOCK_DATETIME)
|
|
def test_user_all_accessible(self):
|
|
response = self.do_request(
|
|
{"project": [-1], "statsPeriod": "1d", "interval": "1d", "field": ["sum(session)"]},
|
|
user=self.user2,
|
|
)
|
|
|
|
start_of_previous_day = MOCK_DATETIME_START_OF_DAY - datetime.timedelta(days=1)
|
|
start_of_day_snuba_format = MOCK_DATETIME_START_OF_DAY.strftime(SNUBA_TIME_FORMAT)
|
|
start_of_previous_day_snuba_format = start_of_previous_day.strftime(SNUBA_TIME_FORMAT)
|
|
|
|
expected_start = MOCK_DATETIME_START_OF_DAY - datetime.timedelta(days=1)
|
|
expected_end = MOCK_DATETIME_START_OF_DAY + datetime.timedelta(days=1)
|
|
|
|
assert response.status_code == 200, response.content
|
|
assert result_sorted(response.data) == {
|
|
"start": expected_start.strftime(SNUBA_TIME_FORMAT),
|
|
"end": expected_end.strftime(SNUBA_TIME_FORMAT),
|
|
"query": "",
|
|
"intervals": [start_of_previous_day_snuba_format, start_of_day_snuba_format],
|
|
"groups": [
|
|
{"by": {}, "series": {"sum(session)": [0, 9]}, "totals": {"sum(session)": 9}}
|
|
],
|
|
}
|
|
|
|
def test_no_projects(self):
|
|
response = self.do_request(
|
|
{"project": [-1], "statsPeriod": "1d", "interval": "1d", "field": ["sum(session)"]},
|
|
org=self.organization3,
|
|
)
|
|
|
|
assert response.status_code == 400, response.content
|
|
assert response.data == {"detail": "No projects available"}
|
|
|
|
@freeze_time(MOCK_DATETIME_PLUS_TEN_MINUTES)
|
|
def test_minute_resolution(self):
|
|
with self.feature("organizations:minute-resolution-sessions"):
|
|
response = self.do_request(
|
|
{
|
|
"project": [self.project1.id, self.project2.id],
|
|
"statsPeriod": "30m",
|
|
"interval": "10m",
|
|
"field": ["sum(session)"],
|
|
}
|
|
)
|
|
assert response.status_code == 200, response.content
|
|
|
|
ten_min = 10 * 60
|
|
expected_start = adjust_start(
|
|
MOCK_DATETIME.replace(hour=12, minute=0, second=0), ten_min
|
|
)
|
|
expected_end = adjust_end(MOCK_DATETIME.replace(hour=12, minute=38, second=0), ten_min)
|
|
|
|
assert result_sorted(response.data) == {
|
|
"start": expected_start.strftime(SNUBA_TIME_FORMAT),
|
|
"end": expected_end.strftime(SNUBA_TIME_FORMAT),
|
|
"query": "",
|
|
"intervals": [
|
|
*[
|
|
MOCK_DATETIME.replace(hour=12, minute=min, second=0).strftime(
|
|
SNUBA_TIME_FORMAT
|
|
)
|
|
for min in [0, 10, 20, 30]
|
|
],
|
|
],
|
|
"groups": [
|
|
{
|
|
"by": {},
|
|
"series": {"sum(session)": [2, 1, 1, 0]},
|
|
"totals": {"sum(session)": 4},
|
|
}
|
|
],
|
|
}
|
|
|
|
@freeze_time(MOCK_DATETIME_PLUS_TEN_MINUTES)
|
|
def test_10s_resolution(self):
|
|
with self.feature("organizations:minute-resolution-sessions"):
|
|
response = self.do_request(
|
|
{
|
|
"project": [self.project1.id],
|
|
"statsPeriod": "1m",
|
|
"interval": "10s",
|
|
"field": ["sum(session)"],
|
|
}
|
|
)
|
|
assert response.status_code == 200, response.content
|
|
|
|
if release_health.backend.is_metrics_based():
|
|
# With the metrics backend, we should get exactly what we asked for,
|
|
# 6 intervals with 10 second length. However, since we add both the
|
|
# starting and ending interval we get 7 intervals.
|
|
assert len(response.data["intervals"]) == 7
|
|
else:
|
|
# With the sessions backend, the resolution will be 1m and will include starting and ending
|
|
# minute so 2 intervals
|
|
assert len(response.data["intervals"]) == 2
|
|
|
|
@freeze_time(MOCK_DATETIME)
|
|
def test_filter_projects(self):
|
|
response = self.do_request(
|
|
{
|
|
"statsPeriod": "1d",
|
|
"interval": "1d",
|
|
"field": ["sum(session)"],
|
|
"project": [self.project2.id, self.project3.id],
|
|
}
|
|
)
|
|
|
|
assert response.status_code == 200, response.content
|
|
assert result_sorted(response.data)["groups"] == [
|
|
{"by": {}, "series": {"sum(session)": [0, 5]}, "totals": {"sum(session)": 5}}
|
|
]
|
|
|
|
@freeze_time(MOCK_DATETIME)
|
|
def test_anr_invalid_aggregates(self):
|
|
default_request = {
|
|
"project": [-1],
|
|
"statsPeriod": "1d",
|
|
"interval": "1d",
|
|
"field": ["anr_rate()", "crash_free_rate(user)"],
|
|
}
|
|
|
|
def req(**kwargs):
|
|
return self.do_request(dict(default_request, **kwargs))
|
|
|
|
response = req()
|
|
assert response.status_code == 200
|
|
|
|
if release_health.backend.is_metrics_based():
|
|
# Both these fields are supported by the metrics backend
|
|
assert response.data["groups"] == [
|
|
{
|
|
"by": {},
|
|
"totals": {"anr_rate()": 0.0, "crash_free_rate(user)": 1.0},
|
|
"series": {"anr_rate()": [None, 0.0], "crash_free_rate(user)": [None, 1.0]},
|
|
}
|
|
]
|
|
else:
|
|
# Both these fields are not supported by the sessions backend
|
|
assert response.data["groups"] == []
|
|
|
|
response = req(field=["anr_rate()", "sum(session)"])
|
|
assert response.status_code == 200
|
|
|
|
if release_health.backend.is_metrics_based():
|
|
# Both these fields are supported by the metrics backend
|
|
assert response.data["groups"] == [
|
|
{
|
|
"by": {},
|
|
"totals": {"anr_rate()": 0.0, "sum(session)": 9},
|
|
"series": {"anr_rate()": [None, 0.0], "sum(session)": [0, 9]},
|
|
}
|
|
]
|
|
else:
|
|
# Only sum(session) is supported by the sessions backend
|
|
assert response.data["groups"] == [
|
|
{"by": {}, "totals": {"sum(session)": 9}, "series": {"sum(session)": [0, 9]}}
|
|
]
|
|
|
|
@freeze_time(MOCK_DATETIME)
|
|
def test_filter_environment(self):
|
|
response = self.do_request(
|
|
{
|
|
"project": [-1],
|
|
"statsPeriod": "1d",
|
|
"interval": "1d",
|
|
"field": ["sum(session)"],
|
|
"query": "environment:development",
|
|
}
|
|
)
|
|
|
|
assert response.status_code == 200, response.content
|
|
assert result_sorted(response.data)["groups"] == [
|
|
{"by": {}, "series": {"sum(session)": [0, 1]}, "totals": {"sum(session)": 1}}
|
|
]
|
|
|
|
response = self.do_request(
|
|
{
|
|
"project": [-1],
|
|
"statsPeriod": "1d",
|
|
"interval": "1d",
|
|
"field": ["sum(session)"],
|
|
"environment": ["development"],
|
|
}
|
|
)
|
|
|
|
assert response.status_code == 200, response.content
|
|
assert result_sorted(response.data)["groups"] == [
|
|
{"by": {}, "series": {"sum(session)": [0, 1]}, "totals": {"sum(session)": 1}}
|
|
]
|
|
|
|
@freeze_time(MOCK_DATETIME)
|
|
def test_filter_release(self):
|
|
response = self.do_request(
|
|
{
|
|
"project": [-1],
|
|
"statsPeriod": "1d",
|
|
"interval": "1d",
|
|
"field": ["sum(session)"],
|
|
"query": "release:[email protected]",
|
|
}
|
|
)
|
|
|
|
assert response.status_code == 200, response.content
|
|
assert result_sorted(response.data)["groups"] == [
|
|
{"by": {}, "series": {"sum(session)": [0, 1]}, "totals": {"sum(session)": 1}}
|
|
]
|
|
|
|
response = self.do_request(
|
|
{
|
|
"project": [-1],
|
|
"statsPeriod": "1d",
|
|
"interval": "1d",
|
|
"field": ["sum(session)"],
|
|
"query": 'release:"[email protected]" or release:"[email protected]"',
|
|
}
|
|
)
|
|
|
|
assert response.status_code == 200, response.content
|
|
assert result_sorted(response.data)["groups"] == [
|
|
{"by": {}, "series": {"sum(session)": [0, 2]}, "totals": {"sum(session)": 2}}
|
|
]
|
|
|
|
response = self.do_request(
|
|
{
|
|
"project": [-1],
|
|
"statsPeriod": "1d",
|
|
"interval": "1d",
|
|
"field": ["sum(session)"],
|
|
"query": 'release:"[email protected]" or release:["[email protected]", release:"[email protected]"]',
|
|
"groupBy": ["release"],
|
|
}
|
|
)
|
|
|
|
assert response.status_code == 200, response.content
|
|
assert result_sorted(response.data)["groups"] == [
|
|
{
|
|
"by": {"release": "[email protected]"},
|
|
"series": {"sum(session)": [0, 1]},
|
|
"totals": {"sum(session)": 1},
|
|
},
|
|
{
|
|
"by": {"release": "[email protected]"},
|
|
"series": {"sum(session)": [0, 1]},
|
|
"totals": {"sum(session)": 1},
|
|
},
|
|
]
|
|
|
|
@freeze_time(MOCK_DATETIME)
|
|
def test_filter_unknown_release(self):
|
|
response = self.do_request(
|
|
{
|
|
"project": [-1],
|
|
"statsPeriod": "1d",
|
|
"interval": "1h",
|
|
"field": ["sum(session)"],
|
|
"query": "release:[email protected]",
|
|
"groupBy": "session.status",
|
|
}
|
|
)
|
|
|
|
assert response.status_code == 200, response.content
|
|
|
|
@freeze_time(MOCK_DATETIME)
|
|
def test_filter_unknown_release_in(self):
|
|
response = self.do_request(
|
|
{
|
|
"project": [-1],
|
|
"statsPeriod": "1d",
|
|
"interval": "1d",
|
|
"field": ["sum(session)"],
|
|
"query": "release:[[email protected]]",
|
|
"groupBy": "session.status",
|
|
}
|
|
)
|
|
|
|
assert response.status_code == 200, response.content
|
|
assert result_sorted(response.data)["groups"] == [
|
|
{
|
|
"by": {"session.status": status},
|
|
"series": {"sum(session)": [0, 0]},
|
|
"totals": {"sum(session)": 0},
|
|
}
|
|
for status in ("abnormal", "crashed", "errored", "healthy")
|
|
]
|
|
|
|
@freeze_time(MOCK_DATETIME)
|
|
def test_groupby_project(self):
|
|
response = self.do_request(
|
|
{
|
|
"project": [-1],
|
|
"statsPeriod": "1d",
|
|
"interval": "1d",
|
|
"field": ["sum(session)"],
|
|
"groupBy": ["project"],
|
|
}
|
|
)
|
|
|
|
assert response.status_code == 200, response.content
|
|
assert result_sorted(response.data)["groups"] == [
|
|
{
|
|
"by": {"project": self.project1.id},
|
|
"series": {"sum(session)": [0, 4]},
|
|
"totals": {"sum(session)": 4},
|
|
},
|
|
{
|
|
"by": {"project": self.project2.id},
|
|
"series": {"sum(session)": [0, 2]},
|
|
"totals": {"sum(session)": 2},
|
|
},
|
|
{
|
|
"by": {"project": self.project3.id},
|
|
"series": {"sum(session)": [0, 3]},
|
|
"totals": {"sum(session)": 3},
|
|
},
|
|
]
|
|
|
|
@freeze_time(MOCK_DATETIME)
|
|
def test_groupby_environment(self):
|
|
response = self.do_request(
|
|
{
|
|
"project": [-1],
|
|
"statsPeriod": "1d",
|
|
"interval": "1d",
|
|
"field": ["sum(session)"],
|
|
"groupBy": ["environment"],
|
|
}
|
|
)
|
|
|
|
assert response.status_code == 200, response.content
|
|
assert result_sorted(response.data)["groups"] == [
|
|
{
|
|
"by": {"environment": "development"},
|
|
"series": {"sum(session)": [0, 1]},
|
|
"totals": {"sum(session)": 1},
|
|
},
|
|
{
|
|
"by": {"environment": "production"},
|
|
"series": {"sum(session)": [0, 8]},
|
|
"totals": {"sum(session)": 8},
|
|
},
|
|
]
|
|
|
|
@freeze_time(MOCK_DATETIME)
|
|
def test_groupby_release(self):
|
|
response = self.do_request(
|
|
{
|
|
"project": [-1],
|
|
"statsPeriod": "1d",
|
|
"interval": "1d",
|
|
"field": ["sum(session)"],
|
|
"groupBy": ["release"],
|
|
}
|
|
)
|
|
|
|
assert response.status_code == 200, response.content
|
|
assert result_sorted(response.data)["groups"] == [
|
|
{
|
|
"by": {"release": "[email protected]"},
|
|
"series": {"sum(session)": [0, 7]},
|
|
"totals": {"sum(session)": 7},
|
|
},
|
|
{
|
|
"by": {"release": "[email protected]"},
|
|
"series": {"sum(session)": [0, 1]},
|
|
"totals": {"sum(session)": 1},
|
|
},
|
|
{
|
|
"by": {"release": "[email protected]"},
|
|
"series": {"sum(session)": [0, 1]},
|
|
"totals": {"sum(session)": 1},
|
|
},
|
|
]
|
|
|
|
@freeze_time(MOCK_DATETIME)
|
|
def test_groupby_status(self):
|
|
response = self.do_request(
|
|
{
|
|
"project": [-1],
|
|
"statsPeriod": "1d",
|
|
"interval": "1d",
|
|
"field": ["sum(session)"],
|
|
"groupBy": ["session.status"],
|
|
}
|
|
)
|
|
|
|
assert response.status_code == 200, response.content
|
|
assert result_sorted(response.data)["groups"] == [
|
|
{
|
|
"by": {"session.status": "abnormal"},
|
|
"series": {"sum(session)": [0, 0]},
|
|
"totals": {"sum(session)": 0},
|
|
},
|
|
{
|
|
"by": {"session.status": "crashed"},
|
|
"series": {"sum(session)": [0, 1]},
|
|
"totals": {"sum(session)": 1},
|
|
},
|
|
{
|
|
"by": {"session.status": "errored"},
|
|
"series": {"sum(session)": [0, 2]},
|
|
"totals": {"sum(session)": 2},
|
|
},
|
|
{
|
|
"by": {"session.status": "healthy"},
|
|
"series": {"sum(session)": [0, 6]},
|
|
"totals": {"sum(session)": 6},
|
|
},
|
|
]
|
|
|
|
@freeze_time(MOCK_DATETIME)
|
|
def test_groupby_cross(self):
|
|
response = self.do_request(
|
|
{
|
|
"project": [-1],
|
|
"statsPeriod": "1d",
|
|
"interval": "1d",
|
|
"field": ["sum(session)"],
|
|
"groupBy": ["release", "environment"],
|
|
}
|
|
)
|
|
|
|
assert response.status_code == 200, response.content
|
|
assert result_sorted(response.data)["groups"] == [
|
|
{
|
|
"by": {"environment": "development", "release": "[email protected]"},
|
|
"series": {"sum(session)": [0, 1]},
|
|
"totals": {"sum(session)": 1},
|
|
},
|
|
{
|
|
"by": {"environment": "production", "release": "[email protected]"},
|
|
"series": {"sum(session)": [0, 6]},
|
|
"totals": {"sum(session)": 6},
|
|
},
|
|
{
|
|
"by": {"environment": "production", "release": "[email protected]"},
|
|
"series": {"sum(session)": [0, 1]},
|
|
"totals": {"sum(session)": 1},
|
|
},
|
|
{
|
|
"by": {"environment": "production", "release": "[email protected]"},
|
|
"series": {"sum(session)": [0, 1]},
|
|
"totals": {"sum(session)": 1},
|
|
},
|
|
]
|
|
|
|
@freeze_time(MOCK_DATETIME)
|
|
def test_users_groupby(self):
|
|
response = self.do_request(
|
|
{
|
|
"project": [-1],
|
|
"statsPeriod": "1d",
|
|
"interval": "1d",
|
|
"field": ["count_unique(user)"],
|
|
}
|
|
)
|
|
|
|
assert response.status_code == 200, response.content
|
|
assert result_sorted(response.data)["groups"] == [
|
|
{
|
|
"by": {},
|
|
"series": {"count_unique(user)": [0, 1]},
|
|
"totals": {"count_unique(user)": 1},
|
|
}
|
|
]
|
|
|
|
response = self.do_request(
|
|
{
|
|
"project": [-1],
|
|
"statsPeriod": "1d",
|
|
"interval": "1d",
|
|
"field": ["count_unique(user)"],
|
|
"groupBy": ["session.status"],
|
|
}
|
|
)
|
|
|
|
assert response.status_code == 200, response.content
|
|
assert result_sorted(response.data)["groups"] == [
|
|
{
|
|
"by": {"session.status": "abnormal"},
|
|
"series": {"count_unique(user)": [0, 0]},
|
|
"totals": {"count_unique(user)": 0},
|
|
},
|
|
{
|
|
"by": {"session.status": "crashed"},
|
|
"series": {"count_unique(user)": [0, 0]},
|
|
"totals": {"count_unique(user)": 0},
|
|
},
|
|
{
|
|
"by": {"session.status": "errored"},
|
|
"series": {"count_unique(user)": [0, 1]},
|
|
"totals": {"count_unique(user)": 1},
|
|
},
|
|
{
|
|
"by": {"session.status": "healthy"},
|
|
"series": {"count_unique(user)": [0, 0]},
|
|
"totals": {"count_unique(user)": 0},
|
|
},
|
|
]
|
|
|
|
expected_duration_values = {
|
|
"avg(session.duration)": 42375.0,
|
|
"max(session.duration)": 80000.0,
|
|
"p50(session.duration)": 33500.0,
|
|
"p75(session.duration)": 53750.0,
|
|
"p90(session.duration)": 71600.0,
|
|
"p95(session.duration)": 75800.0,
|
|
"p99(session.duration)": 79159.99999999999,
|
|
}
|
|
|
|
@freeze_time(MOCK_DATETIME)
|
|
def test_users_groupby_status_advanced(self):
|
|
project = self.create_project()
|
|
|
|
user1 = uuid4().hex
|
|
session1 = uuid4().hex
|
|
|
|
user2 = uuid4().hex
|
|
session2a = uuid4().hex
|
|
session2b = uuid4().hex
|
|
|
|
user3 = uuid4().hex
|
|
session3 = uuid4().hex
|
|
|
|
self.store_session(
|
|
make_session(project, session_id=session1, distinct_id=user1, status="ok")
|
|
)
|
|
self.store_session(
|
|
make_session(
|
|
project, session_id=session1, distinct_id=user1, seq=1, errors=1, status="errored"
|
|
)
|
|
)
|
|
self.store_session(
|
|
make_session(project, session_id=session1, distinct_id=user1, seq=2, status="crashed")
|
|
)
|
|
|
|
self.store_session(
|
|
make_session(project, session_id=session2a, distinct_id=user2, status="ok")
|
|
)
|
|
self.store_session(
|
|
make_session(project, session_id=session2b, distinct_id=user2, status="ok")
|
|
)
|
|
self.store_session(
|
|
make_session(project, session_id=session2b, distinct_id=user2, status="abnormal")
|
|
)
|
|
|
|
self.store_session(
|
|
make_session(
|
|
project, session_id=session3, distinct_id=user3, errors=123, status="errored"
|
|
)
|
|
)
|
|
|
|
# Add some extra healthy users:
|
|
for _ in range(3):
|
|
user = uuid4().hex
|
|
self.store_session(make_session(project, distinct_id=user))
|
|
|
|
# First, check if totals make sense:
|
|
response = self.do_request(
|
|
{
|
|
"project": [project.id],
|
|
"statsPeriod": "1d",
|
|
"interval": "1d",
|
|
"field": ["count_unique(user)"],
|
|
}
|
|
)
|
|
assert response.status_code == 200, response.content
|
|
assert result_sorted(response.data)["groups"] == [
|
|
{
|
|
"by": {},
|
|
"series": {"count_unique(user)": [0, 6]},
|
|
"totals": {"count_unique(user)": 6},
|
|
},
|
|
]
|
|
|
|
# Then check if grouping makes sense:
|
|
response = self.do_request(
|
|
{
|
|
"project": [project.id],
|
|
"statsPeriod": "1d",
|
|
"interval": "1d",
|
|
"field": ["count_unique(user)"],
|
|
"groupBy": ["session.status"],
|
|
}
|
|
)
|
|
assert response.status_code == 200, response.content
|
|
assert result_sorted(response.data)["groups"] == [
|
|
{
|
|
"by": {"session.status": "abnormal"},
|
|
"series": {"count_unique(user)": [0, 1]},
|
|
"totals": {"count_unique(user)": 1},
|
|
},
|
|
{
|
|
"by": {"session.status": "crashed"},
|
|
"series": {"count_unique(user)": [0, 1]},
|
|
"totals": {"count_unique(user)": 1},
|
|
},
|
|
{
|
|
"by": {"session.status": "errored"},
|
|
"series": {"count_unique(user)": [0, 1]},
|
|
"totals": {"count_unique(user)": 1},
|
|
},
|
|
{
|
|
# user
|
|
"by": {"session.status": "healthy"},
|
|
"series": {"count_unique(user)": [0, 3]},
|
|
"totals": {"count_unique(user)": 3},
|
|
},
|
|
]
|
|
|
|
@freeze_time(MOCK_DATETIME)
|
|
def test_duration_percentiles(self):
|
|
response = self.do_request(
|
|
{
|
|
"project": [-1],
|
|
"statsPeriod": "1d",
|
|
"interval": "1d",
|
|
"field": [
|
|
"avg(session.duration)",
|
|
"p50(session.duration)",
|
|
"p75(session.duration)",
|
|
"p90(session.duration)",
|
|
"p95(session.duration)",
|
|
"p99(session.duration)",
|
|
"max(session.duration)",
|
|
],
|
|
}
|
|
)
|
|
|
|
assert response.status_code == 200, response.content
|
|
|
|
expected = self.expected_duration_values
|
|
|
|
groups = result_sorted(response.data)["groups"]
|
|
assert len(groups) == 1, groups
|
|
group = groups[0]
|
|
assert group["by"] == {}
|
|
|
|
assert group["totals"] == pytest.approx(expected)
|
|
for key, series in group["series"].items():
|
|
assert series == pytest.approx([None, expected[key]])
|
|
|
|
@freeze_time(MOCK_DATETIME)
|
|
def test_duration_percentiles_groupby(self):
|
|
response = self.do_request(
|
|
{
|
|
"project": [-1],
|
|
"statsPeriod": "1d",
|
|
"interval": "1d",
|
|
"field": [
|
|
"avg(session.duration)",
|
|
"p50(session.duration)",
|
|
"p75(session.duration)",
|
|
"p90(session.duration)",
|
|
"p95(session.duration)",
|
|
"p99(session.duration)",
|
|
"max(session.duration)",
|
|
],
|
|
"groupBy": "session.status",
|
|
}
|
|
)
|
|
|
|
assert response.status_code == 200, response.content
|
|
|
|
expected = self.expected_duration_values
|
|
|
|
seen = set() # Make sure all session statuses are listed
|
|
for group in result_sorted(response.data)["groups"]:
|
|
seen.add(group["by"].get("session.status"))
|
|
|
|
if group["by"] == {"session.status": "healthy"}:
|
|
assert group["totals"] == pytest.approx(expected)
|
|
for key, series in group["series"].items():
|
|
assert series == pytest.approx([None, expected[key]])
|
|
else:
|
|
# Everything's none:
|
|
assert group["totals"] == {key: None for key in expected}, group["by"]
|
|
assert group["series"] == {key: [None, None] for key in expected}
|
|
|
|
assert seen == {"abnormal", "crashed", "errored", "healthy"}
|
|
|
|
@freeze_time(MOCK_DATETIME)
|
|
def test_snuba_limit_exceeded(self):
|
|
# 2 * 4 => only show two groups
|
|
with patch("sentry.snuba.sessions_v2.SNUBA_LIMIT", 8), patch(
|
|
"sentry.snuba.metrics.query.MAX_POINTS", 8
|
|
):
|
|
response = self.do_request(
|
|
{
|
|
"project": [-1],
|
|
"statsPeriod": "3d",
|
|
"interval": "1d",
|
|
# "user" is the first field, but "session" always wins:
|
|
"field": ["count_unique(user)", "sum(session)"],
|
|
"groupBy": ["project", "release", "environment"],
|
|
}
|
|
)
|
|
|
|
assert response.status_code == 200, response.content
|
|
assert result_sorted(response.data)["groups"] == [
|
|
{
|
|
"by": {
|
|
"release": "[email protected]",
|
|
"environment": "production",
|
|
"project": self.project1.id,
|
|
},
|
|
"totals": {"sum(session)": 3, "count_unique(user)": 0},
|
|
"series": {"sum(session)": [0, 0, 0, 3], "count_unique(user)": [0, 0, 0, 0]},
|
|
},
|
|
{
|
|
"by": {
|
|
"release": "[email protected]",
|
|
"environment": "production",
|
|
"project": self.project3.id,
|
|
},
|
|
"totals": {"sum(session)": 2, "count_unique(user)": 1},
|
|
"series": {"sum(session)": [0, 0, 0, 2], "count_unique(user)": [0, 0, 0, 1]},
|
|
},
|
|
]
|
|
|
|
@freeze_time(MOCK_DATETIME)
|
|
def test_snuba_limit_exceeded_groupby_status(self):
|
|
"""Get consistent result when grouping by status"""
|
|
# 2 * 4 => only show two groups
|
|
with patch("sentry.snuba.sessions_v2.SNUBA_LIMIT", 8), patch(
|
|
"sentry.snuba.metrics.query.MAX_POINTS", 8
|
|
):
|
|
response = self.do_request(
|
|
{
|
|
"project": [-1],
|
|
"statsPeriod": "3d",
|
|
"interval": "1d",
|
|
"field": ["sum(session)", "count_unique(user)"],
|
|
"groupBy": ["project", "release", "environment", "session.status"],
|
|
}
|
|
)
|
|
|
|
assert response.status_code == 200, response.content
|
|
assert result_sorted(response.data)["groups"] == [
|
|
{
|
|
"by": {
|
|
"project": self.project1.id,
|
|
"release": "[email protected]",
|
|
"session.status": "abnormal",
|
|
"environment": "production",
|
|
},
|
|
"totals": {"sum(session)": 0, "count_unique(user)": 0},
|
|
"series": {"sum(session)": [0, 0, 0, 0], "count_unique(user)": [0, 0, 0, 0]},
|
|
},
|
|
{
|
|
"by": {
|
|
"project": self.project1.id,
|
|
"release": "[email protected]",
|
|
"session.status": "crashed",
|
|
"environment": "production",
|
|
},
|
|
"totals": {"sum(session)": 0, "count_unique(user)": 0},
|
|
"series": {"sum(session)": [0, 0, 0, 0], "count_unique(user)": [0, 0, 0, 0]},
|
|
},
|
|
{
|
|
"by": {
|
|
"project": self.project1.id,
|
|
"release": "[email protected]",
|
|
"environment": "production",
|
|
"session.status": "errored",
|
|
},
|
|
"totals": {"sum(session)": 0, "count_unique(user)": 0},
|
|
"series": {"sum(session)": [0, 0, 0, 0], "count_unique(user)": [0, 0, 0, 0]},
|
|
},
|
|
{
|
|
"by": {
|
|
"project": self.project1.id,
|
|
"session.status": "healthy",
|
|
"release": "[email protected]",
|
|
"environment": "production",
|
|
},
|
|
"totals": {"sum(session)": 3, "count_unique(user)": 0},
|
|
"series": {"sum(session)": [0, 0, 0, 3], "count_unique(user)": [0, 0, 0, 0]},
|
|
},
|
|
{
|
|
"by": {
|
|
"session.status": "abnormal",
|
|
"release": "[email protected]",
|
|
"project": self.project3.id,
|
|
"environment": "production",
|
|
},
|
|
"totals": {"sum(session)": 0, "count_unique(user)": 0},
|
|
"series": {"sum(session)": [0, 0, 0, 0], "count_unique(user)": [0, 0, 0, 0]},
|
|
},
|
|
{
|
|
"by": {
|
|
"release": "[email protected]",
|
|
"project": self.project3.id,
|
|
"session.status": "crashed",
|
|
"environment": "production",
|
|
},
|
|
"totals": {"sum(session)": 0, "count_unique(user)": 0},
|
|
"series": {"sum(session)": [0, 0, 0, 0], "count_unique(user)": [0, 0, 0, 0]},
|
|
},
|
|
{
|
|
"by": {
|
|
"release": "[email protected]",
|
|
"project": self.project3.id,
|
|
"environment": "production",
|
|
"session.status": "errored",
|
|
},
|
|
"totals": {"sum(session)": 1, "count_unique(user)": 1},
|
|
"series": {"sum(session)": [0, 0, 0, 1], "count_unique(user)": [0, 0, 0, 1]},
|
|
},
|
|
{
|
|
"by": {
|
|
"session.status": "healthy",
|
|
"release": "[email protected]",
|
|
"project": self.project3.id,
|
|
"environment": "production",
|
|
},
|
|
"totals": {"sum(session)": 1, "count_unique(user)": 0},
|
|
"series": {"sum(session)": [0, 0, 0, 1], "count_unique(user)": [0, 0, 0, 0]},
|
|
},
|
|
]
|
|
|
|
@freeze_time(MOCK_DATETIME)
|
|
def test_environment_filter_not_present_in_query(self):
|
|
self.create_environment(name="abc")
|
|
response = self.do_request(
|
|
{
|
|
"project": [-1],
|
|
"statsPeriod": "1d",
|
|
"interval": "1d",
|
|
"field": ["sum(session)"],
|
|
"environment": ["development", "abc"],
|
|
}
|
|
)
|
|
|
|
assert response.status_code == 200, response.content
|
|
assert result_sorted(response.data)["groups"] == [
|
|
{"by": {}, "series": {"sum(session)": [0, 1]}, "totals": {"sum(session)": 1}}
|
|
]
|
|
|
|
@freeze_time(MOCK_DATETIME)
|
|
def test_sessions_without_users(self):
|
|
# The first field defines by which groups additional queries are filtered
|
|
# But if the first field is the user count, the series should still
|
|
# contain the session counts even if the project does not track users
|
|
response = self.do_request(
|
|
{
|
|
"project": self.project.id, # project without users
|
|
"statsPeriod": "1d",
|
|
"interval": "1d",
|
|
"field": ["count_unique(user)", "sum(session)"],
|
|
"groupBy": "release",
|
|
}
|
|
)
|
|
|
|
assert response.status_code == 200, response.content
|
|
assert result_sorted(response.data)["groups"] == [
|
|
{
|
|
"by": {"release": "[email protected]"},
|
|
"series": {"count_unique(user)": [0, 0], "sum(session)": [0, 3]},
|
|
"totals": {"count_unique(user)": 0, "sum(session)": 3},
|
|
},
|
|
{
|
|
"by": {"release": "[email protected]"},
|
|
"series": {"count_unique(user)": [0, 0], "sum(session)": [0, 1]},
|
|
"totals": {"count_unique(user)": 0, "sum(session)": 1},
|
|
},
|
|
]
|
|
|
|
@freeze_time(MOCK_DATETIME + datetime.timedelta(days=2))
|
|
def test_groupby_no_data(self):
|
|
# Empty results for everything
|
|
response = self.do_request(
|
|
{
|
|
"project": self.project.id, # project without users
|
|
"statsPeriod": "1d",
|
|
"interval": "1d",
|
|
"field": ["count_unique(user)", "sum(session)"],
|
|
"groupBy": "release",
|
|
}
|
|
)
|
|
|
|
assert response.status_code == 200, response.content
|
|
assert result_sorted(response.data)["groups"] == []
|
|
|
|
@freeze_time(MOCK_DATETIME)
|
|
def test_mix_known_and_unknown_strings(self):
|
|
response = self.do_request(
|
|
{
|
|
"project": self.project.id, # project without users
|
|
"statsPeriod": "1d",
|
|
"interval": "1d",
|
|
"field": ["count_unique(user)", "sum(session)"],
|
|
"query": "environment:[production,foo]",
|
|
}
|
|
)
|
|
assert response.status_code == 200, response.data
|
|
|
|
@freeze_time(MOCK_DATETIME)
|
|
def test_release_semver_filter(self):
|
|
r1 = self.create_release(version="[email protected]")
|
|
r2 = self.create_release(version="[email protected]")
|
|
r3 = self.create_release(version="[email protected]")
|
|
|
|
for r in (r1, r2, r3):
|
|
self.store_session(make_session(self.project, release=r.version))
|
|
|
|
response = self.do_request(
|
|
{
|
|
"project": self.project.id,
|
|
"statsPeriod": "1d",
|
|
"interval": "1d",
|
|
"field": ["sum(session)"],
|
|
"groupBy": ["release"],
|
|
"query": "release.version:1.*",
|
|
}
|
|
)
|
|
assert response.status_code == 200
|
|
assert sorted(response.data["groups"], key=lambda x: x["by"]["release"]) == [
|
|
{
|
|
"by": {"release": "[email protected]"},
|
|
"totals": {"sum(session)": 1},
|
|
"series": {"sum(session)": [0, 1]},
|
|
},
|
|
{
|
|
"by": {"release": "[email protected]"},
|
|
"totals": {"sum(session)": 1},
|
|
"series": {"sum(session)": [0, 1]},
|
|
},
|
|
]
|
|
|
|
@freeze_time(MOCK_DATETIME)
|
|
def test_release_package_filter(self):
|
|
r1 = self.create_release(version="[email protected]+124")
|
|
r2 = self.create_release(version="[email protected]+125")
|
|
r3 = self.create_release(version="[email protected]+126")
|
|
|
|
for r in (r1, r2, r3):
|
|
self.store_session(make_session(self.project, release=r.version))
|
|
|
|
response = self.do_request(
|
|
{
|
|
"project": self.project.id,
|
|
"statsPeriod": "1d",
|
|
"interval": "1d",
|
|
"field": ["sum(session)"],
|
|
"groupBy": ["release"],
|
|
"query": "release.package:ahmed2",
|
|
}
|
|
)
|
|
assert response.status_code == 200
|
|
assert sorted(response.data["groups"], key=lambda x: x["by"]["release"]) == [
|
|
{
|
|
"by": {"release": "[email protected]+125"},
|
|
"totals": {"sum(session)": 1},
|
|
"series": {"sum(session)": [0, 1]},
|
|
},
|
|
{
|
|
"by": {"release": "[email protected]+126"},
|
|
"totals": {"sum(session)": 1},
|
|
"series": {"sum(session)": [0, 1]},
|
|
},
|
|
]
|
|
|
|
@freeze_time(MOCK_DATETIME)
|
|
def test_release_build_filter(self):
|
|
r1 = self.create_release(version="[email protected]+124")
|
|
r2 = self.create_release(version="[email protected]+123")
|
|
r3 = self.create_release(version="[email protected]+125")
|
|
|
|
for r in (r1, r2, r3):
|
|
self.store_session(make_session(self.project, release=r.version))
|
|
|
|
response = self.do_request(
|
|
{
|
|
"project": self.project.id,
|
|
"statsPeriod": "1d",
|
|
"interval": "1d",
|
|
"field": ["sum(session)"],
|
|
"groupBy": ["release"],
|
|
"query": "release.build:<125",
|
|
}
|
|
)
|
|
assert response.status_code == 200
|
|
assert sorted(response.data["groups"], key=lambda x: x["by"]["release"]) == [
|
|
{
|
|
"by": {"release": "[email protected]+123"},
|
|
"totals": {"sum(session)": 1},
|
|
"series": {"sum(session)": [0, 1]},
|
|
},
|
|
{
|
|
"by": {"release": "[email protected]+124"},
|
|
"totals": {"sum(session)": 1},
|
|
"series": {"sum(session)": [0, 1]},
|
|
},
|
|
]
|
|
|
|
@freeze_time(MOCK_DATETIME)
|
|
def test_release_stage_filter(self):
|
|
new_env = self.create_environment(name="new_env")
|
|
adopted_release = self.create_release(version="adopted_release")
|
|
not_adopted_release = self.create_release(version="not_adopted_release")
|
|
|
|
ReleaseProjectEnvironment.objects.create(
|
|
project_id=self.project.id,
|
|
release_id=adopted_release.id,
|
|
environment_id=new_env.id,
|
|
adopted=timezone.now(),
|
|
)
|
|
ReleaseProjectEnvironment.objects.create(
|
|
project_id=self.project.id,
|
|
release_id=not_adopted_release.id,
|
|
environment_id=new_env.id,
|
|
)
|
|
for r in (adopted_release, not_adopted_release):
|
|
self.store_session(
|
|
make_session(self.project, release=r.version, environment=new_env.name)
|
|
)
|
|
|
|
response = self.do_request(
|
|
{
|
|
"project": self.project.id,
|
|
"statsPeriod": "1d",
|
|
"interval": "1d",
|
|
"field": ["sum(session)"],
|
|
"groupBy": ["release"],
|
|
"query": "release.stage:adopted",
|
|
"environment": new_env.name,
|
|
}
|
|
)
|
|
assert response.status_code == 200
|
|
assert response.data["groups"] == [
|
|
{
|
|
"by": {"release": "adopted_release"},
|
|
"totals": {"sum(session)": 1},
|
|
"series": {"sum(session)": [0, 1]},
|
|
},
|
|
]
|
|
|
|
|
|
@region_silo_test
|
|
@patch("sentry.release_health.backend", MetricsReleaseHealthBackend())
|
|
class OrganizationSessionsEndpointMetricsTest(
|
|
BaseMetricsTestCase, OrganizationSessionsEndpointTest
|
|
):
|
|
"""Repeat all tests with metrics backend"""
|
|
|
|
@freeze_time(MOCK_DATETIME)
|
|
def test_orderby(self):
|
|
response = self.do_request(
|
|
{
|
|
"project": [-1],
|
|
"statsPeriod": "2d",
|
|
"interval": "1d",
|
|
"field": ["sum(session)"],
|
|
"orderBy": "foobar",
|
|
}
|
|
)
|
|
assert response.status_code == 400
|
|
assert response.data == {"detail": "'orderBy' must be one of the provided 'fields'"}
|
|
|
|
response = self.do_request(
|
|
{
|
|
"project": [-1],
|
|
"statsPeriod": "2d",
|
|
"interval": "1d",
|
|
"field": ["sum(session)"],
|
|
"orderBy": "count_unique(user)", # wrong field
|
|
}
|
|
)
|
|
assert response.status_code == 400
|
|
assert response.data == {"detail": "'orderBy' must be one of the provided 'fields'"}
|
|
|
|
# Cannot sort by more than one field
|
|
response = self.do_request(
|
|
{
|
|
"project": [-1],
|
|
"statsPeriod": "2d",
|
|
"interval": "1d",
|
|
"field": ["sum(session)", "count_unique(user)"],
|
|
"orderBy": ["sum(session)", "count_unique(user)"],
|
|
}
|
|
)
|
|
assert response.status_code == 400
|
|
assert response.data == {"detail": "Cannot order by multiple fields"}
|
|
|
|
response = self.do_request(
|
|
{
|
|
"project": [-1],
|
|
"statsPeriod": "2d",
|
|
"interval": "1d",
|
|
"field": ["sum(session)"],
|
|
"orderBy": "sum(session)", # misses group by, but why not
|
|
}
|
|
)
|
|
assert response.status_code == 200
|
|
|
|
response = self.do_request(
|
|
{
|
|
"project": [-1],
|
|
"statsPeriod": "2d",
|
|
"interval": "1d",
|
|
"field": ["sum(session)"],
|
|
"orderBy": "sum(session)",
|
|
"groupBy": ["session.status"],
|
|
}
|
|
)
|
|
assert response.status_code == 400
|
|
assert response.data == {"detail": "Cannot use 'orderBy' when grouping by sessions.status"}
|
|
|
|
response = self.do_request(
|
|
{
|
|
"project": [self.project.id, self.project3.id],
|
|
"statsPeriod": "2d",
|
|
"interval": "1d",
|
|
"field": ["sum(session)", "p95(session.duration)"],
|
|
"orderBy": "p95(session.duration)",
|
|
"groupBy": ["project", "release", "environment"],
|
|
}
|
|
)
|
|
|
|
expected_groups = [
|
|
{
|
|
"by": {
|
|
"project": self.project.id,
|
|
"release": "[email protected]",
|
|
"environment": "production",
|
|
},
|
|
"totals": {"sum(session)": 3, "p95(session.duration)": 25000.0},
|
|
"series": {
|
|
"sum(session)": [0, 0, 3],
|
|
"p95(session.duration)": [None, None, 25000.0],
|
|
},
|
|
},
|
|
{
|
|
"by": {
|
|
"project": self.project3.id,
|
|
"release": "[email protected]",
|
|
"environment": "production",
|
|
},
|
|
"totals": {"sum(session)": 1, "p95(session.duration)": 37000.0},
|
|
"series": {
|
|
"sum(session)": [0, 0, 1],
|
|
"p95(session.duration)": [None, None, 37000.0],
|
|
},
|
|
},
|
|
{
|
|
"by": {
|
|
"project": self.project.id,
|
|
"release": "[email protected]",
|
|
"environment": "production",
|
|
},
|
|
"totals": {"sum(session)": 1, "p95(session.duration)": 49000.0},
|
|
"series": {
|
|
"sum(session)": [0, 0, 1],
|
|
"p95(session.duration)": [None, None, 49000.0],
|
|
},
|
|
},
|
|
{
|
|
"by": {
|
|
"project": self.project3.id,
|
|
"release": "[email protected]",
|
|
"environment": "production",
|
|
},
|
|
"totals": {"sum(session)": 2, "p95(session.duration)": 79400.0},
|
|
"series": {
|
|
"sum(session)": [0, 0, 2],
|
|
"p95(session.duration)": [None, None, 79400.0],
|
|
},
|
|
},
|
|
]
|
|
|
|
# Not using `result_sorted` here, because we want to verify the order
|
|
assert response.status_code == 200, response.data
|
|
assert response.data["groups"] == expected_groups
|
|
|
|
# Sort descending
|
|
response = self.do_request(
|
|
{
|
|
"project": [self.project.id, self.project3.id],
|
|
"statsPeriod": "2d",
|
|
"interval": "1d",
|
|
"field": ["sum(session)", "p95(session.duration)"],
|
|
"orderBy": "-p95(session.duration)",
|
|
"groupBy": ["project", "release", "environment"],
|
|
}
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
assert response.data["groups"] == list(reversed(expected_groups))
|
|
|
|
# Add some more code coverage
|
|
all_fields = [
|
|
"sum(session)",
|
|
"count_unique(user)",
|
|
"avg(session.duration)",
|
|
]
|
|
for field in all_fields:
|
|
assert (
|
|
self.do_request(
|
|
{
|
|
"project": [self.project.id, self.project3.id],
|
|
"statsPeriod": "2d",
|
|
"interval": "1d",
|
|
"field": all_fields,
|
|
"orderBy": field,
|
|
"groupBy": ["project", "release", "environment"],
|
|
}
|
|
).status_code
|
|
== 200
|
|
)
|
|
|
|
@freeze_time(MOCK_DATETIME)
|
|
def test_wildcard_search(self):
|
|
default_request = {
|
|
"project": [-1],
|
|
"statsPeriod": "2d",
|
|
"interval": "1d",
|
|
}
|
|
|
|
def req(**kwargs):
|
|
return self.do_request(dict(default_request, **kwargs))
|
|
|
|
response = req(field=["sum(session)"], query="release:foo@*")
|
|
assert response.status_code == 400
|
|
assert response.data == {"detail": "Invalid condition: wildcard search is not supported"}
|
|
|
|
response = req(field=["sum(session)"], query="release:foo@* AND release:bar@*")
|
|
assert response.status_code == 400
|
|
assert response.data == {"detail": "Invalid condition: wildcard search is not supported"}
|
|
|
|
response = req(field=["sum(session)"], query="release:foo@* OR release:bar@*")
|
|
assert response.status_code == 400
|
|
assert response.data == {"detail": "Invalid condition: wildcard search is not supported"}
|
|
|
|
response = req(field=["sum(session)"], query="(release:foo@* OR release:bar) OR project:1")
|
|
assert response.status_code == 400
|
|
assert response.data == {"detail": "Invalid condition: wildcard search is not supported"}
|
|
|
|
@freeze_time(MOCK_DATETIME)
|
|
def test_filter_by_session_status(self):
|
|
default_request = {
|
|
"project": [-1],
|
|
"statsPeriod": "1d",
|
|
"interval": "1d",
|
|
}
|
|
|
|
def req(**kwargs):
|
|
return self.do_request(dict(default_request, **kwargs))
|
|
|
|
response = req(field=["sum(session)"], query="session.status:bogus")
|
|
assert response.status_code == 200, response.content
|
|
assert result_sorted(response.data)["groups"] == []
|
|
|
|
response = req(field=["sum(session)"], query="!session.status:healthy")
|
|
assert response.status_code == 200, response.content
|
|
assert result_sorted(response.data)["groups"] == [
|
|
{"by": {}, "series": {"sum(session)": [0, 3]}, "totals": {"sum(session)": 3}}
|
|
]
|
|
|
|
# sum(session) filtered by multiple statuses adds them
|
|
response = req(field=["sum(session)"], query="session.status:[healthy, errored]")
|
|
assert response.status_code == 200, response.content
|
|
assert result_sorted(response.data)["groups"] == [
|
|
{"by": {}, "series": {"sum(session)": [0, 8]}, "totals": {"sum(session)": 8}}
|
|
]
|
|
|
|
response = req(
|
|
field=["sum(session)"],
|
|
query="session.status:[healthy, errored]",
|
|
groupBy="session.status",
|
|
)
|
|
assert response.status_code == 200, response.content
|
|
assert result_sorted(response.data)["groups"] == [
|
|
{
|
|
"by": {"session.status": "errored"},
|
|
"totals": {"sum(session)": 2},
|
|
"series": {"sum(session)": [0, 2]},
|
|
},
|
|
{
|
|
"by": {"session.status": "healthy"},
|
|
"totals": {"sum(session)": 6},
|
|
"series": {"sum(session)": [0, 6]},
|
|
},
|
|
]
|
|
|
|
response = req(field=["sum(session)"], query="session.status:healthy release:[email protected]")
|
|
assert response.status_code == 200, response.content
|
|
assert result_sorted(response.data)["groups"] == [
|
|
{"by": {}, "series": {"sum(session)": [0, 1]}, "totals": {"sum(session)": 1}}
|
|
]
|
|
|
|
response = req(field=["sum(session)"], query="session.status:healthy OR release:[email protected]")
|
|
assert response.status_code == 400, response.data
|
|
assert response.data == {"detail": "Unable to parse condition with session.status"}
|
|
|
|
# count_unique(user) does not work with multiple session statuses selected
|
|
response = req(field=["count_unique(user)"], query="session.status:[healthy, errored]")
|
|
assert response.status_code == 400, response.data
|
|
assert response.data == {
|
|
"detail": "Cannot filter count_unique by multiple session.status unless it is in groupBy"
|
|
}
|
|
|
|
response = req(field=["p95(session.duration)"], query="session.status:abnormal")
|
|
assert response.status_code == 200, response.content
|
|
assert result_sorted(response.data)["groups"] == []
|
|
|
|
@freeze_time(MOCK_DATETIME)
|
|
def test_filter_by_session_status_with_groupby(self):
|
|
default_request = {
|
|
"project": [-1],
|
|
"statsPeriod": "1d",
|
|
"interval": "1d",
|
|
"groupBy": "release",
|
|
}
|
|
|
|
def req(**kwargs):
|
|
return self.do_request(dict(default_request, **kwargs))
|
|
|
|
response = req(field=["sum(session)"], query="session.status:healthy")
|
|
assert response.status_code == 200, response.content
|
|
assert result_sorted(response.data)["groups"] == [
|
|
{
|
|
"by": {"release": "[email protected]"},
|
|
"series": {"sum(session)": [0, 5]},
|
|
"totals": {"sum(session)": 5},
|
|
},
|
|
{
|
|
"by": {"release": "[email protected]"},
|
|
"series": {"sum(session)": [0, 1]},
|
|
"totals": {"sum(session)": 1},
|
|
},
|
|
{
|
|
"by": {"release": "[email protected]"},
|
|
"series": {"sum(session)": [0, 0]},
|
|
"totals": {"sum(session)": 0},
|
|
},
|
|
]
|
|
|
|
@freeze_time(MOCK_DATETIME)
|
|
def test_filter_by_session_status_with_orderby(self):
|
|
default_request = {
|
|
"project": [-1],
|
|
"statsPeriod": "1d",
|
|
"interval": "1d",
|
|
}
|
|
|
|
def req(**kwargs):
|
|
return self.do_request(dict(default_request, **kwargs))
|
|
|
|
response = req(
|
|
field=["sum(session)"],
|
|
query="session.status:[abnormal,crashed]",
|
|
groupBy="release",
|
|
orderBy="sum(session)",
|
|
)
|
|
assert response.status_code == 400, response.content
|
|
assert response.data == {"detail": "Cannot order by sum(session) with the current filters"}
|
|
|
|
response = req(
|
|
field=["sum(session)"],
|
|
query="session.status:healthy",
|
|
groupBy="release",
|
|
orderBy="sum(session)",
|
|
)
|
|
assert response.status_code == 400, response.content
|
|
assert response.data == {"detail": "Cannot order by sum(session) with the current filters"}
|
|
|
|
@freeze_time(MOCK_DATETIME)
|
|
def test_anr_rate(self):
|
|
def store_anr_session(user_id, mechanism):
|
|
self.store_session(
|
|
make_session(
|
|
self.project2,
|
|
distinct_id=user_id,
|
|
errors=1,
|
|
status="abnormal",
|
|
abnormal_mechanism=mechanism,
|
|
)
|
|
)
|
|
|
|
self.store_session(
|
|
make_session(
|
|
self.project2,
|
|
distinct_id="610c480b-3c47-4871-8c03-05ea04595eb0",
|
|
started=SESSION_STARTED - 60 * 60,
|
|
)
|
|
)
|
|
store_anr_session("610c480b-3c47-4871-8c03-05ea04595eb0", "anr_foreground")
|
|
|
|
self.store_session(
|
|
make_session(
|
|
self.project2,
|
|
distinct_id="ac0b74a2-8ace-415a-82d2-0fdb0d81dec4",
|
|
started=SESSION_STARTED - 60 * 60,
|
|
)
|
|
)
|
|
store_anr_session("ac0b74a2-8ace-415a-82d2-0fdb0d81dec4", "anr_background")
|
|
|
|
self.store_session(
|
|
make_session(
|
|
self.project2,
|
|
distinct_id="5344c005-653b-48b7-bbaf-d362c2f268dd",
|
|
started=SESSION_STARTED - 60 * 60,
|
|
)
|
|
)
|
|
|
|
default_request = {
|
|
"project": [-1],
|
|
"statsPeriod": "1d",
|
|
"interval": "1d",
|
|
"field": ["anr_rate()"],
|
|
}
|
|
|
|
def req(**kwargs):
|
|
return self.do_request(dict(default_request, **kwargs))
|
|
|
|
# basic test case
|
|
response = req()
|
|
assert response.status_code == 200
|
|
assert response.data["groups"] == [
|
|
{"by": {}, "totals": {"anr_rate()": 0.5}, "series": {"anr_rate()": [None, 0.5]}}
|
|
]
|
|
|
|
# group by session.status
|
|
response = req(
|
|
groupBy="session.status",
|
|
)
|
|
assert response.status_code == 400, response.content
|
|
assert response.data == {"detail": "Cannot group field anr_rate() by session.status"}
|
|
|
|
# valid group by
|
|
response = req(
|
|
field=["anr_rate()", "foreground_anr_rate()"],
|
|
groupBy=["release", "environment"],
|
|
orderBy=["anr_rate()"],
|
|
query="release:[email protected]",
|
|
)
|
|
|
|
assert response.status_code == 200, response.content
|
|
assert response.data["groups"] == [
|
|
{
|
|
"by": {"environment": "production", "release": "[email protected]"},
|
|
"series": {
|
|
"anr_rate()": [None, 0.5],
|
|
"foreground_anr_rate()": [None, 0.25],
|
|
},
|
|
"totals": {
|
|
"anr_rate()": 0.5,
|
|
"foreground_anr_rate()": 0.25,
|
|
},
|
|
},
|
|
]
|
|
|
|
@freeze_time(MOCK_DATETIME)
|
|
def test_crash_rate(self):
|
|
default_request = {
|
|
"project": [-1],
|
|
"statsPeriod": "1d",
|
|
"interval": "1d",
|
|
"field": ["crash_rate(session)"],
|
|
}
|
|
|
|
def req(**kwargs):
|
|
return self.do_request(dict(default_request, **kwargs))
|
|
|
|
# 1 - filter session.status
|
|
response = req(
|
|
query="session.status:[abnormal,crashed]",
|
|
)
|
|
assert response.status_code == 400, response.content
|
|
assert response.data == {
|
|
"detail": "Cannot filter field crash_rate(session) by session.status"
|
|
}
|
|
|
|
# 2 - group by session.status
|
|
response = req(
|
|
groupBy="session.status",
|
|
)
|
|
assert response.status_code == 400, response.content
|
|
assert response.data == {
|
|
"detail": "Cannot group field crash_rate(session) by session.status"
|
|
}
|
|
|
|
# 4 - fetch all
|
|
response = req(
|
|
field=[
|
|
"crash_rate(session)",
|
|
"crash_rate(user)",
|
|
"crash_free_rate(session)",
|
|
"crash_free_rate(user)",
|
|
],
|
|
groupBy=["release", "environment"],
|
|
orderBy=["crash_free_rate(session)"],
|
|
query="release:[email protected]",
|
|
)
|
|
assert response.status_code == 200, response.content
|
|
assert response.data["groups"] == [
|
|
{
|
|
"by": {"environment": "production", "release": "[email protected]"},
|
|
"series": {
|
|
"crash_free_rate(session)": [None, 0.8333333333333334],
|
|
"crash_free_rate(user)": [None, 1.0],
|
|
"crash_rate(session)": [None, 0.16666666666666666],
|
|
"crash_rate(user)": [None, 0.0],
|
|
},
|
|
"totals": {
|
|
"crash_free_rate(session)": 0.8333333333333334,
|
|
"crash_free_rate(user)": 1.0,
|
|
"crash_rate(session)": 0.16666666666666666,
|
|
"crash_rate(user)": 0.0,
|
|
},
|
|
},
|
|
{
|
|
"by": {"environment": "development", "release": "[email protected]"},
|
|
"series": {
|
|
"crash_free_rate(session)": [None, 1.0],
|
|
"crash_free_rate(user)": [None, None],
|
|
"crash_rate(session)": [None, 0.0],
|
|
"crash_rate(user)": [None, None],
|
|
},
|
|
"totals": {
|
|
"crash_free_rate(session)": 1.0,
|
|
"crash_free_rate(user)": None,
|
|
"crash_rate(session)": 0.0,
|
|
"crash_rate(user)": None,
|
|
},
|
|
},
|
|
]
|
|
|
|
@freeze_time(MOCK_DATETIME)
|
|
def test_pagination(self):
|
|
def do_request(cursor):
|
|
return self.do_request(
|
|
{
|
|
"project": self.project.id, # project without users
|
|
"statsPeriod": "1d",
|
|
"interval": "1d",
|
|
"field": ["count_unique(user)", "sum(session)"],
|
|
"query": "",
|
|
"groupBy": "release",
|
|
"orderBy": "sum(session)",
|
|
"per_page": 1,
|
|
**({"cursor": cursor} if cursor else {}),
|
|
}
|
|
)
|
|
|
|
response = do_request(None)
|
|
|
|
assert response.status_code == 200, response.data
|
|
assert len(response.data["groups"]) == 1
|
|
assert response.data["groups"] == [
|
|
{
|
|
"by": {"release": "[email protected]"},
|
|
"series": {"count_unique(user)": [0, 0], "sum(session)": [0, 1]},
|
|
"totals": {"count_unique(user)": 0, "sum(session)": 1},
|
|
}
|
|
]
|
|
links = {link["rel"]: link for url, link in parse_link_header(response["Link"]).items()}
|
|
assert links["previous"]["results"] == "false"
|
|
assert links["next"]["results"] == "true"
|
|
|
|
response = do_request(links["next"]["cursor"])
|
|
assert response.status_code == 200, response.data
|
|
assert len(response.data["groups"]) == 1
|
|
assert response.data["groups"] == [
|
|
{
|
|
"by": {"release": "[email protected]"},
|
|
"series": {"count_unique(user)": [0, 0], "sum(session)": [0, 3]},
|
|
"totals": {"count_unique(user)": 0, "sum(session)": 3},
|
|
}
|
|
]
|
|
links = {link["rel"]: link for url, link in parse_link_header(response["Link"]).items()}
|
|
assert links["previous"]["results"] == "true"
|
|
assert links["next"]["results"] == "false"
|
|
|
|
def test_unrestricted_date_range(self):
|
|
response = self.do_request(
|
|
{
|
|
"project": [-1],
|
|
"statsPeriod": "7h",
|
|
"interval": "5m",
|
|
"field": ["sum(session)"],
|
|
}
|
|
)
|
|
assert response.status_code == 200
|
|
|
|
@freeze_time(MOCK_DATETIME)
|
|
def test_release_is_empty(self):
|
|
self.store_session(
|
|
make_session(
|
|
self.project1, started=SESSION_STARTED + 12 * 60, release="", environment=""
|
|
)
|
|
)
|
|
for query in ('release:"" environment:""', 'release:"" OR environment:""'):
|
|
# Empty strings are invalid values for releases and environments, but we should still handle those cases
|
|
# correctly at the query layer
|
|
response = self.do_request(
|
|
{
|
|
"project": self.project.id, # project without users
|
|
"statsPeriod": "1d",
|
|
"interval": "1d",
|
|
"field": ["sum(session)"],
|
|
"query": query,
|
|
"groupBy": ["release", "environment"],
|
|
}
|
|
)
|
|
|
|
assert response.status_code == 200, response.content
|
|
assert result_sorted(response.data)["groups"] == [
|
|
{
|
|
"by": {"environment": "", "release": ""},
|
|
"series": {"sum(session)": [0, 1]},
|
|
"totals": {"sum(session)": 1},
|
|
}
|
|
]
|
|
|
|
|
|
@region_silo_test
|
|
@patch("sentry.release_health.backend", MetricsReleaseHealthBackend())
|
|
class SessionsMetricsSortReleaseTimestampTest(BaseMetricsTestCase, APITestCase):
|
|
def do_request(self, query, user=None, org=None):
|
|
self.login_as(user=user or self.user)
|
|
url = reverse(
|
|
"sentry-api-0-organization-sessions",
|
|
kwargs={"organization_slug": (org or self.organization).slug},
|
|
)
|
|
return self.client.get(url, query, format="json")
|
|
|
|
@freeze_time(MOCK_DATETIME)
|
|
def test_order_by_with_no_releases(self):
|
|
"""
|
|
Test that ensures if we have no releases in the preflight query when trying to order by
|
|
`release.timestamp`, we get no groups.
|
|
Essentially testing the empty preflight query filters branch.
|
|
"""
|
|
project_random = self.create_project()
|
|
for _ in range(0, 2):
|
|
self.store_session(make_session(project_random))
|
|
self.store_session(make_session(project_random, status="crashed"))
|
|
|
|
response = self.do_request(
|
|
{
|
|
"project": project_random.id,
|
|
"statsPeriod": "1d",
|
|
"interval": "1d",
|
|
"field": ["crash_free_rate(session)"],
|
|
"groupBy": ["release"],
|
|
"orderBy": "-release.timestamp",
|
|
"per_page": 3,
|
|
}
|
|
)
|
|
assert response.data["groups"] == []
|
|
|
|
def test_order_by_max_limit(self):
|
|
response = self.do_request(
|
|
{
|
|
"project": self.project.id,
|
|
"statsPeriod": "1d",
|
|
"interval": "1d",
|
|
"field": ["crash_free_rate(session)"],
|
|
"groupBy": ["release"],
|
|
"orderBy": "-release.timestamp",
|
|
"per_page": 103,
|
|
}
|
|
)
|
|
assert response.data["detail"] == (
|
|
"This limit is too high for queries that requests a preflight query. "
|
|
"Please choose a limit below 100"
|
|
)
|
|
|
|
@freeze_time(MOCK_DATETIME)
|
|
def test_order_by(self):
|
|
"""
|
|
Test that ensures that we are able to get the crash_free_rate for the most 2 recent
|
|
releases when grouping by release
|
|
"""
|
|
# Step 1: Create 3 releases
|
|
release1b = self.create_release(version="1B")
|
|
release1c = self.create_release(version="1C")
|
|
release1d = self.create_release(version="1D")
|
|
|
|
# Step 2: Create crash free rate for each of those releases
|
|
# Release 1c -> 66.7% Crash free rate
|
|
for _ in range(0, 2):
|
|
self.store_session(make_session(self.project, release=release1c.version))
|
|
self.store_session(make_session(self.project, release=release1c.version, status="crashed"))
|
|
|
|
# Release 1b -> 33.3% Crash free rate
|
|
for _ in range(0, 2):
|
|
self.store_session(
|
|
make_session(self.project, release=release1b.version, status="crashed")
|
|
)
|
|
self.store_session(make_session(self.project, release=release1b.version))
|
|
|
|
# Create Sessions in each of these releases
|
|
# Release 1d -> 80% Crash free rate
|
|
for _ in range(0, 4):
|
|
self.store_session(make_session(self.project, release=release1d.version))
|
|
self.store_session(make_session(self.project, release=release1d.version, status="crashed"))
|
|
|
|
# Step 3: Make request
|
|
response = self.do_request(
|
|
{
|
|
"project": self.project.id, # project without users
|
|
"statsPeriod": "1d",
|
|
"interval": "1d",
|
|
"field": ["crash_free_rate(session)"],
|
|
"groupBy": ["release"],
|
|
"orderBy": "-release.timestamp",
|
|
"per_page": 3,
|
|
}
|
|
)
|
|
# Step 4: Validate Results
|
|
assert response.data["groups"] == [
|
|
{
|
|
"by": {"release": "1D"},
|
|
"totals": {"crash_free_rate(session)": 0.8},
|
|
"series": {"crash_free_rate(session)": [None, 0.8]},
|
|
},
|
|
{
|
|
"by": {"release": "1C"},
|
|
"totals": {"crash_free_rate(session)": 0.6666666666666667},
|
|
"series": {"crash_free_rate(session)": [None, 0.6666666666666667]},
|
|
},
|
|
{
|
|
"by": {"release": "1B"},
|
|
"totals": {"crash_free_rate(session)": 0.33333333333333337},
|
|
"series": {"crash_free_rate(session)": [None, 0.33333333333333337]},
|
|
},
|
|
]
|
|
|
|
@freeze_time(MOCK_DATETIME)
|
|
def test_order_by_with_session_status_groupby(self):
|
|
"""
|
|
Test that ensures we are able to group by session.status and order by `release.timestamp`
|
|
since `release.timestamp` is generated from a preflight query
|
|
"""
|
|
rando_project = self.create_project()
|
|
|
|
release_1a = self.create_release(project=rando_project, version="1A")
|
|
release_1b = self.create_release(project=rando_project, version="1B")
|
|
|
|
# Release 1B sessions
|
|
for _ in range(4):
|
|
self.store_session(
|
|
make_session(rando_project, release=release_1b.version, status="crashed")
|
|
)
|
|
for _ in range(10):
|
|
self.store_session(make_session(rando_project, release=release_1b.version))
|
|
for _ in range(3):
|
|
self.store_session(make_session(rando_project, errors=1, release=release_1b.version))
|
|
|
|
# Release 1A sessions
|
|
for _ in range(0, 2):
|
|
self.store_session(
|
|
make_session(rando_project, release=release_1a.version, status="crashed")
|
|
)
|
|
self.store_session(make_session(rando_project, release=release_1a.version))
|
|
for _ in range(3):
|
|
self.store_session(make_session(rando_project, errors=1, release=release_1a.version))
|
|
|
|
response = self.do_request(
|
|
{
|
|
"project": rando_project.id,
|
|
"statsPeriod": "1d",
|
|
"interval": "1d",
|
|
"field": ["sum(session)"],
|
|
"groupBy": ["release", "session.status"],
|
|
"orderBy": "-release.timestamp",
|
|
}
|
|
)
|
|
assert response.data["groups"] == [
|
|
{
|
|
"by": {"release": "1B", "session.status": "abnormal"},
|
|
"totals": {"sum(session)": 0},
|
|
"series": {"sum(session)": [0, 0]},
|
|
},
|
|
{
|
|
"by": {"release": "1B", "session.status": "crashed"},
|
|
"totals": {"sum(session)": 4},
|
|
"series": {"sum(session)": [0, 4]},
|
|
},
|
|
{
|
|
"by": {"release": "1B", "session.status": "errored"},
|
|
"totals": {"sum(session)": 3},
|
|
"series": {"sum(session)": [0, 3]},
|
|
},
|
|
{
|
|
"by": {"release": "1B", "session.status": "healthy"},
|
|
"totals": {"sum(session)": 10},
|
|
"series": {"sum(session)": [0, 10]},
|
|
},
|
|
{
|
|
"by": {"release": "1A", "session.status": "abnormal"},
|
|
"totals": {"sum(session)": 0},
|
|
"series": {"sum(session)": [0, 0]},
|
|
},
|
|
{
|
|
"by": {"release": "1A", "session.status": "crashed"},
|
|
"totals": {"sum(session)": 2},
|
|
"series": {"sum(session)": [0, 2]},
|
|
},
|
|
{
|
|
"by": {"release": "1A", "session.status": "errored"},
|
|
"totals": {"sum(session)": 3},
|
|
"series": {"sum(session)": [0, 3]},
|
|
},
|
|
{
|
|
"by": {"release": "1A", "session.status": "healthy"},
|
|
"totals": {"sum(session)": 1},
|
|
"series": {"sum(session)": [0, 1]},
|
|
},
|
|
]
|
|
|
|
@freeze_time(MOCK_DATETIME)
|
|
def test_order_by_with_limit(self):
|
|
rando_project = self.create_project()
|
|
|
|
# Create two releases with no metrics data and then two releases with metric data
|
|
release_1a = self.create_release(project=rando_project, version="1A")
|
|
release_1b = self.create_release(project=rando_project, version="1B")
|
|
self.create_release(project=rando_project, version="1C")
|
|
self.create_release(project=rando_project, version="1D")
|
|
|
|
self.store_session(make_session(rando_project, release=release_1a.version))
|
|
self.store_session(make_session(rando_project, release=release_1b.version))
|
|
self.store_session(
|
|
make_session(rando_project, release=release_1b.version, status="crashed")
|
|
)
|
|
|
|
response = self.do_request(
|
|
{
|
|
"project": rando_project.id,
|
|
"statsPeriod": "1d",
|
|
"interval": "1d",
|
|
"field": ["sum(session)"],
|
|
"groupBy": ["release"],
|
|
"orderBy": "-release.timestamp",
|
|
"per_page": 3,
|
|
}
|
|
)
|
|
|
|
assert response.data["groups"] == [
|
|
{
|
|
"by": {"release": "1D"},
|
|
"totals": {"sum(session)": 0},
|
|
"series": {"sum(session)": [0, 0]},
|
|
},
|
|
{
|
|
"by": {"release": "1C"},
|
|
"totals": {"sum(session)": 0},
|
|
"series": {"sum(session)": [0, 0]},
|
|
},
|
|
{
|
|
"by": {"release": "1B"},
|
|
"totals": {"sum(session)": 2},
|
|
"series": {"sum(session)": [0, 2]},
|
|
},
|
|
]
|
|
|
|
response = self.do_request(
|
|
{
|
|
"project": rando_project.id,
|
|
"statsPeriod": "1d",
|
|
"interval": "1d",
|
|
"field": ["sum(session)"],
|
|
"groupBy": ["release", "session.status"],
|
|
"orderBy": "-release.timestamp",
|
|
"per_page": 4,
|
|
}
|
|
)
|
|
assert response.data["groups"] == [
|
|
{
|
|
"by": {"release": "1D", "session.status": None},
|
|
"totals": {"sum(session)": 0},
|
|
"series": {"sum(session)": [0, 0]},
|
|
},
|
|
{
|
|
"by": {"release": "1C", "session.status": None},
|
|
"totals": {"sum(session)": 0},
|
|
"series": {"sum(session)": [0, 0]},
|
|
},
|
|
{
|
|
"by": {"release": "1B", "session.status": "abnormal"},
|
|
"totals": {"sum(session)": 0},
|
|
"series": {"sum(session)": [0, 0]},
|
|
},
|
|
{
|
|
"by": {"release": "1B", "session.status": "crashed"},
|
|
"totals": {"sum(session)": 1},
|
|
"series": {"sum(session)": [0, 1]},
|
|
},
|
|
]
|
|
|
|
response = self.do_request(
|
|
{
|
|
"project": rando_project.id,
|
|
"statsPeriod": "1d",
|
|
"interval": "1d",
|
|
"field": ["sum(session)"],
|
|
"groupBy": ["release", "session.status", "project"],
|
|
"orderBy": "-release.timestamp",
|
|
"per_page": 2,
|
|
}
|
|
)
|
|
assert response.data["groups"] == [
|
|
{
|
|
"by": {"release": "1D", "session.status": None, "project": None},
|
|
"totals": {"sum(session)": 0},
|
|
"series": {"sum(session)": [0, 0]},
|
|
},
|
|
{
|
|
"by": {"release": "1C", "session.status": None, "project": None},
|
|
"totals": {"sum(session)": 0},
|
|
"series": {"sum(session)": [0, 0]},
|
|
},
|
|
]
|
|
|
|
@freeze_time(MOCK_DATETIME)
|
|
def test_order_by_with_limit_and_offset(self):
|
|
rando_project = self.create_project()
|
|
|
|
# Create two releases with no metrics data and then two releases with metric data
|
|
release_1a = self.create_release(project=rando_project, version="1A")
|
|
release_1b = self.create_release(project=rando_project, version="1B")
|
|
self.create_release(project=rando_project, version="1C")
|
|
self.create_release(project=rando_project, version="1D")
|
|
|
|
self.store_session(make_session(rando_project, release=release_1a.version))
|
|
self.store_session(make_session(rando_project, release=release_1b.version))
|
|
|
|
response = self.do_request(
|
|
{
|
|
"project": rando_project.id,
|
|
"statsPeriod": "1d",
|
|
"interval": "1d",
|
|
"field": ["sum(session)"],
|
|
"groupBy": ["release"],
|
|
"orderBy": "-release.timestamp",
|
|
"per_page": 3,
|
|
"cursor": Cursor(0, 1),
|
|
}
|
|
)
|
|
|
|
assert response.data["detail"] == (
|
|
"Passing an offset value greater than 0 when ordering by release.timestamp "
|
|
"is not permitted"
|
|
)
|
|
|
|
@freeze_time(MOCK_DATETIME)
|
|
def test_order_by_with_environment_filter_on_preflight(self):
|
|
rando_project = self.create_project()
|
|
rando_env = self.create_environment(name="rando_env", project=self.project)
|
|
|
|
# Create two releases with no metrics data and then two releases with metric data
|
|
release_1a = self.create_release(
|
|
project=rando_project, version="1A", environments=[rando_env]
|
|
)
|
|
release_1b = self.create_release(
|
|
project=rando_project, version="1B", environments=[rando_env]
|
|
)
|
|
release_1c = self.create_release(project=rando_project, version="1C")
|
|
release_1d = self.create_release(project=rando_project, version="1D")
|
|
|
|
self.store_session(
|
|
make_session(rando_project, release=release_1a.version, environment="rando_env")
|
|
)
|
|
self.store_session(
|
|
make_session(rando_project, release=release_1b.version, environment="rando_env")
|
|
)
|
|
self.store_session(make_session(rando_project, release=release_1c.version))
|
|
self.store_session(make_session(rando_project, release=release_1d.version))
|
|
|
|
# Test env condition with IN
|
|
response = self.do_request(
|
|
{
|
|
"project": rando_project.id,
|
|
"statsPeriod": "1d",
|
|
"interval": "1d",
|
|
"field": ["sum(session)"],
|
|
"query": "environment:[rando_env,rando_enc2]",
|
|
"groupBy": ["release", "environment"],
|
|
"orderBy": "-release.timestamp",
|
|
"per_page": 4,
|
|
}
|
|
)
|
|
assert response.data["groups"] == [
|
|
{
|
|
"by": {"release": "1B", "environment": "rando_env"},
|
|
"totals": {"sum(session)": 1},
|
|
"series": {"sum(session)": [0, 1]},
|
|
},
|
|
{
|
|
"by": {"release": "1A", "environment": "rando_env"},
|
|
"totals": {"sum(session)": 1},
|
|
"series": {"sum(session)": [0, 1]},
|
|
},
|
|
]
|
|
|
|
# Test env condition with NOT IN
|
|
response = self.do_request(
|
|
{
|
|
"project": rando_project.id,
|
|
"statsPeriod": "1d",
|
|
"interval": "1d",
|
|
"field": ["sum(session)"],
|
|
"query": "!environment:[rando_env,rando_enc2]",
|
|
"groupBy": ["release", "environment"],
|
|
"orderBy": "-release.timestamp",
|
|
"per_page": 4,
|
|
}
|
|
)
|
|
assert response.data["groups"] == [
|
|
{
|
|
"by": {"release": "1D", "environment": "production"},
|
|
"totals": {"sum(session)": 1},
|
|
"series": {"sum(session)": [0, 1]},
|
|
},
|
|
{
|
|
"by": {"release": "1C", "environment": "production"},
|
|
"totals": {"sum(session)": 1},
|
|
"series": {"sum(session)": [0, 1]},
|
|
},
|
|
]
|
|
|
|
# Test env condition with invalid OR operation
|
|
response = self.do_request(
|
|
{
|
|
"project": rando_project.id,
|
|
"statsPeriod": "1d",
|
|
"interval": "1d",
|
|
"field": ["sum(session)"],
|
|
"query": "environment:rando_env OR environment:rando_enc2",
|
|
"groupBy": ["release", "environment"],
|
|
"orderBy": "-release.timestamp",
|
|
"per_page": 4,
|
|
}
|
|
)
|
|
assert response.json()["detail"] == "Unable to parse condition with environment"
|
|
|
|
@freeze_time(MOCK_DATETIME)
|
|
def test_order_by_without_release_groupby(self):
|
|
rando_project = self.create_project()
|
|
response = self.do_request(
|
|
{
|
|
"project": rando_project.id,
|
|
"statsPeriod": "1d",
|
|
"interval": "1d",
|
|
"query": "session.status:[crashed,errored]",
|
|
"field": ["sum(session)"],
|
|
"orderBy": "-release.timestamp",
|
|
"per_page": 2,
|
|
}
|
|
)
|
|
assert response.data["detail"] == (
|
|
"To sort by release.timestamp, tag release must be in the groupBy"
|
|
)
|
|
|
|
@freeze_time(MOCK_DATETIME)
|
|
def test_order_by_release_with_session_status_current_filter(self):
|
|
rando_project = self.create_project()
|
|
|
|
release_1a = self.create_release(project=rando_project, version="1A")
|
|
release_1b = self.create_release(project=rando_project, version="1B")
|
|
|
|
# Release 1B sessions
|
|
for _ in range(4):
|
|
self.store_session(
|
|
make_session(rando_project, release=release_1b.version, status="crashed")
|
|
)
|
|
for _ in range(10):
|
|
self.store_session(make_session(rando_project, release=release_1b.version))
|
|
for _ in range(3):
|
|
self.store_session(make_session(rando_project, errors=1, release=release_1b.version))
|
|
|
|
# Release 1A sessions
|
|
for _ in range(0, 2):
|
|
self.store_session(
|
|
make_session(rando_project, release=release_1a.version, status="crashed")
|
|
)
|
|
self.store_session(make_session(rando_project, release=release_1a.version))
|
|
for _ in range(3):
|
|
self.store_session(make_session(rando_project, errors=1, release=release_1a.version))
|
|
|
|
response = self.do_request(
|
|
{
|
|
"project": rando_project.id,
|
|
"statsPeriod": "1d",
|
|
"interval": "1d",
|
|
"query": "session.status:[crashed,errored]",
|
|
"field": ["sum(session)"],
|
|
"groupBy": ["release"],
|
|
"orderBy": "-release.timestamp",
|
|
}
|
|
)
|
|
|
|
assert response.data["groups"] == [
|
|
{
|
|
"by": {"release": "1B"},
|
|
"totals": {"sum(session)": 7},
|
|
"series": {"sum(session)": [0, 7]},
|
|
},
|
|
{
|
|
"by": {"release": "1A"},
|
|
"totals": {"sum(session)": 5},
|
|
"series": {"sum(session)": [0, 5]},
|
|
},
|
|
]
|