Files
sentinel/tests/snuba/api/endpoints/test_organization_events_trends.py
2024-02-15 17:01:50 +00:00

920 lines
33 KiB
Python

from datetime import timedelta
from django.urls import reverse
from sentry.testutils.cases import APITestCase, SnubaTestCase
from sentry.testutils.helpers import parse_link_header
from sentry.testutils.helpers.datetime import before_now, iso_format
from sentry.testutils.silo import region_silo_test
from sentry.utils.samples import load_data
class OrganizationEventsTrendsBase(APITestCase, SnubaTestCase):
def setUp(self):
super().setUp()
self.login_as(user=self.user)
self.day_ago = before_now(days=1).replace(hour=10, minute=0, second=0, microsecond=0)
self.prototype = load_data("transaction")
data = self.prototype.copy()
data["start_timestamp"] = iso_format(self.day_ago + timedelta(minutes=30))
data["user"] = {"email": "[email protected]"}
data["timestamp"] = iso_format(self.day_ago + timedelta(minutes=30, seconds=2))
data["measurements"]["lcp"]["value"] = 2000
self.store_event(data, project_id=self.project.id)
second = [0, 2, 10]
for i in range(3):
data = self.prototype.copy()
data["start_timestamp"] = iso_format(self.day_ago + timedelta(hours=1, minutes=30 + i))
data["timestamp"] = iso_format(
self.day_ago + timedelta(hours=1, minutes=30 + i, seconds=second[i])
)
data["measurements"]["lcp"]["value"] = second[i] * 1000
data["user"] = {"email": f"foo{i}@example.com"}
self.store_event(data, project_id=self.project.id)
self.expected_data = {
"count_range_1": 1,
"count_range_2": 3,
"transaction": self.prototype["transaction"],
"project": self.project.slug,
}
def assert_event(self, data):
for key, value in self.expected_data.items():
assert data[key] == value, key
@region_silo_test
class OrganizationEventsTrendsEndpointTest(OrganizationEventsTrendsBase):
def setUp(self):
super().setUp()
self.url = reverse(
"sentry-api-0-organization-events-trends",
kwargs={"organization_slug": self.project.organization.slug},
)
self.features = {"organizations:performance-view": True}
def test_simple(self):
with self.feature(self.features):
response = self.client.get(
self.url,
format="json",
data={
"end": iso_format(self.day_ago + timedelta(hours=2)),
"start": iso_format(self.day_ago),
"field": ["project", "transaction"],
"query": "event.type:transaction",
"trendType": "regression",
},
)
assert response.status_code == 200, response.content
events = response.data
assert len(events["data"]) == 1
self.expected_data.update(
{
"aggregate_range_1": 2000,
"aggregate_range_2": 2000,
"count_percentage": 3.0,
"trend_difference": 0.0,
"trend_percentage": 1.0,
}
)
self.assert_event(events["data"][0])
def test_web_vital(self):
with self.feature(self.features):
response = self.client.get(
self.url,
format="json",
data={
"end": iso_format(self.day_ago + timedelta(hours=2)),
"start": iso_format(self.day_ago),
"field": ["project", "transaction"],
"query": "event.type:transaction",
"trendType": "regression",
"trendFunction": "p50(measurements.lcp)",
},
)
assert response.status_code == 200, response.content
events = response.data
assert len(events["data"]) == 1
# LCP values are identical to duration
self.expected_data.update(
{
"aggregate_range_1": 2000,
"aggregate_range_2": 2000,
"count_percentage": 3.0,
"trend_difference": 0.0,
"trend_percentage": 1.0,
}
)
self.assert_event(events["data"][0])
def test_p75(self):
with self.feature(self.features):
response = self.client.get(
self.url,
format="json",
data={
"end": iso_format(self.day_ago + timedelta(hours=2)),
"start": iso_format(self.day_ago),
"field": ["project", "transaction"],
"query": "event.type:transaction",
"trendFunction": "p75()",
},
)
assert response.status_code == 200, response.content
events = response.data
assert len(events["data"]) == 1
self.expected_data.update(
{
"aggregate_range_1": 2000,
"aggregate_range_2": 6000,
"count_percentage": 3.0,
"trend_difference": 4000.0,
"trend_percentage": 3.0,
}
)
self.assert_event(events["data"][0])
def test_p95(self):
with self.feature(self.features):
response = self.client.get(
self.url,
format="json",
data={
"end": iso_format(self.day_ago + timedelta(hours=2)),
"start": iso_format(self.day_ago),
"field": ["project", "transaction"],
"query": "event.type:transaction",
"trendFunction": "p95()",
},
)
assert response.status_code == 200, response.content
events = response.data
assert len(events["data"]) == 1
self.expected_data.update(
{
"aggregate_range_1": 2000,
"aggregate_range_2": 9200,
"count_percentage": 3.0,
"trend_difference": 7200.0,
"trend_percentage": 4.6,
}
)
self.assert_event(events["data"][0])
def test_p99(self):
with self.feature(self.features):
response = self.client.get(
self.url,
format="json",
data={
"end": iso_format(self.day_ago + timedelta(hours=2)),
"start": iso_format(self.day_ago),
"field": ["project", "transaction"],
"query": "event.type:transaction",
"trendFunction": "p99()",
},
)
assert response.status_code == 200, response.content
events = response.data
assert len(events["data"]) == 1
self.expected_data.update(
{
"aggregate_range_1": 2000,
"aggregate_range_2": 9840,
"count_percentage": 3.0,
"trend_difference": 7840.0,
"trend_percentage": 4.92,
}
)
self.assert_event(events["data"][0])
def test_trend_percentage_query_alias(self):
queries = [
("trend_percentage():>0%", "regression", 1),
("trend_percentage():392%", "regression", 1),
("trend_percentage():>0%", "improved", 0),
("trend_percentage():392%", "improved", 0),
]
for query_data in queries:
with self.feature(self.features):
response = self.client.get(
self.url,
format="json",
data={
"end": iso_format(self.day_ago + timedelta(hours=2)),
"start": iso_format(self.day_ago),
"field": ["project", "transaction"],
"query": f"event.type:transaction {query_data[0]}",
"trendType": query_data[1],
# Use p99 since it has the most significant change
"trendFunction": "p99()",
},
)
assert response.status_code == 200, response.content
events = response.data
assert len(events["data"]) == query_data[2], query_data
def test_trend_percentage_query_alias_as_sort(self):
with self.feature(self.features):
response = self.client.get(
self.url,
format="json",
data={
"end": iso_format(self.day_ago + timedelta(hours=2)),
"start": iso_format(self.day_ago),
"field": ["project", "transaction"],
"query": "event.type:transaction",
"trendType": "improved",
"trendFunction": "p50()",
"sort": "trend_percentage()",
},
)
assert response.status_code == 200, response.content
events = response.data
assert len(events["data"]) == 1
def test_trend_difference_query_alias(self):
queries = [
("trend_difference():>7s", "regression", 1),
("trend_difference():7.84s", "regression", 1),
("trend_difference():>7s", "improved", 0),
("trend_difference():7.84s", "improved", 0),
]
for query_data in queries:
with self.feature(self.features):
response = self.client.get(
self.url,
format="json",
data={
"end": iso_format(self.day_ago + timedelta(hours=2)),
"start": iso_format(self.day_ago),
"field": ["project", "transaction"],
"query": f"event.type:transaction {query_data[0]}",
"trendType": query_data[1],
# Use p99 since it has the most significant change
"trendFunction": "p99()",
},
)
assert response.status_code == 200, response.content
events = response.data
assert len(events["data"]) == query_data[2], query_data
def test_avg_trend_function(self):
with self.feature(self.features):
response = self.client.get(
self.url,
format="json",
data={
"end": iso_format(self.day_ago + timedelta(hours=2)),
"start": iso_format(self.day_ago),
"field": ["project", "transaction"],
"query": "event.type:transaction",
"trendFunction": "avg(transaction.duration)",
"project": [self.project.id],
},
)
assert response.status_code == 200, response.content
events = response.data
assert len(events["data"]) == 1
self.expected_data.update(
{
"aggregate_range_1": 2000,
"aggregate_range_2": 4000,
"count_percentage": 3.0,
"trend_difference": 2000.0,
"trend_percentage": 2.0,
}
)
self.assert_event(events["data"][0])
def test_invalid_trend_function(self):
with self.feature(self.features):
response = self.client.get(
self.url,
format="json",
data={
"end": iso_format(self.day_ago + timedelta(hours=2)),
"start": iso_format(self.day_ago),
"field": ["project", "transaction"],
"query": "event.type:transaction",
"trendFunction": "apdex(450)",
"project": [self.project.id],
},
)
assert response.status_code == 400
def test_divide_by_zero(self):
with self.feature(self.features):
response = self.client.get(
self.url,
format="json",
data={
# Set the timeframe to where the second range has no transactions so all the counts/percentile are 0
"end": iso_format(self.day_ago + timedelta(hours=2)),
"start": iso_format(self.day_ago - timedelta(hours=2)),
"field": ["project", "transaction"],
"query": "event.type:transaction",
"project": [self.project.id],
},
)
assert response.status_code == 200, response.content
events = response.data
assert len(events["data"]) == 1
self.expected_data.update(
{
"count_range_2": 4,
"count_range_1": 0,
"aggregate_range_1": 0,
"aggregate_range_2": 2000.0,
"count_percentage": None,
"trend_difference": 0,
"trend_percentage": None,
}
)
self.assert_event(events["data"][0])
def test_auto_aggregation(self):
# absolute_correlation is automatically added, and not a part of data otherwise
with self.feature(self.features):
response = self.client.get(
self.url,
format="json",
data={
# Set the timeframe to where the second range has no transactions so all the counts/percentile are 0
"end": iso_format(self.day_ago + timedelta(hours=2)),
"start": iso_format(self.day_ago - timedelta(hours=2)),
"field": ["project", "transaction"],
"query": "event.type:transaction absolute_correlation():>0.2",
"project": [self.project.id],
},
)
assert response.status_code == 200, response.content
events = response.data
assert len(events["data"]) == 1
self.expected_data.update(
{
"count_range_2": 4,
"count_range_1": 0,
"aggregate_range_1": 0,
"aggregate_range_2": 2000.0,
"count_percentage": None,
"trend_difference": 0,
"trend_percentage": None,
}
)
self.assert_event(events["data"][0])
@region_silo_test
class OrganizationEventsTrendsStatsEndpointTest(OrganizationEventsTrendsBase):
def setUp(self):
super().setUp()
self.url = reverse(
"sentry-api-0-organization-events-trends-stats",
kwargs={"organization_slug": self.project.organization.slug},
)
self.features = {"organizations:performance-view": True}
def test_simple(self):
with self.feature(self.features):
response = self.client.get(
self.url,
format="json",
data={
"end": iso_format(self.day_ago + timedelta(hours=2)),
"start": iso_format(self.day_ago),
"interval": "1h",
"field": ["project", "transaction"],
"query": "event.type:transaction",
},
)
assert response.status_code == 200, response.content
events = response.data["events"]
result_stats = response.data["stats"]
assert len(events["data"]) == 1
self.expected_data.update(
{
"aggregate_range_1": 2000,
"aggregate_range_2": 2000,
"count_percentage": 3.0,
"trend_difference": 0.0,
"trend_percentage": 1.0,
}
)
self.assert_event(events["data"][0])
stats = result_stats[f"{self.project.slug},{self.prototype['transaction']}"]
assert [attrs for time, attrs in stats["data"]] == [
[{"count": 2000}],
[{"count": 2000}],
]
def test_web_vital(self):
with self.feature(self.features):
response = self.client.get(
self.url,
format="json",
data={
"end": iso_format(self.day_ago + timedelta(hours=2)),
"start": iso_format(self.day_ago),
"interval": "1h",
"field": ["project", "transaction"],
"query": "event.type:transaction",
"trendFunction": "p50(measurements.lcp)",
},
)
assert response.status_code == 200, response.content
events = response.data["events"]
result_stats = response.data["stats"]
assert len(events["data"]) == 1
self.expected_data.update(
{
"aggregate_range_1": 2000,
"aggregate_range_2": 2000,
"count_percentage": 3.0,
"trend_difference": 0.0,
"trend_percentage": 1.0,
}
)
self.assert_event(events["data"][0])
stats = result_stats[f"{self.project.slug},{self.prototype['transaction']}"]
assert [attrs for time, attrs in stats["data"]] == [
[{"count": 2000}],
[{"count": 2000}],
]
def test_p75(self):
with self.feature(self.features):
response = self.client.get(
self.url,
format="json",
data={
"end": iso_format(self.day_ago + timedelta(hours=2)),
"start": iso_format(self.day_ago),
"interval": "1h",
"field": ["project", "transaction"],
"query": "event.type:transaction",
"trendFunction": "p75()",
},
)
assert response.status_code == 200, response.content
events = response.data["events"]
result_stats = response.data["stats"]
assert len(events["data"]) == 1
self.expected_data.update(
{
"aggregate_range_1": 2000,
"aggregate_range_2": 6000,
"count_percentage": 3.0,
"trend_difference": 4000.0,
"trend_percentage": 3.0,
}
)
self.assert_event(events["data"][0])
stats = result_stats[f"{self.project.slug},{self.prototype['transaction']}"]
assert [attrs for time, attrs in stats["data"]] == [
[{"count": 2000}],
[{"count": 6000}],
]
def test_p95(self):
with self.feature(self.features):
response = self.client.get(
self.url,
format="json",
data={
"end": iso_format(self.day_ago + timedelta(hours=2)),
"start": iso_format(self.day_ago),
"interval": "1h",
"field": ["project", "transaction"],
"query": "event.type:transaction",
"trendFunction": "p95()",
},
)
assert response.status_code == 200, response.content
events = response.data["events"]
result_stats = response.data["stats"]
assert len(events["data"]) == 1
self.expected_data.update(
{
"aggregate_range_1": 2000,
"aggregate_range_2": 9200,
"count_percentage": 3.0,
"trend_difference": 7200.0,
"trend_percentage": 4.6,
}
)
self.assert_event(events["data"][0])
stats = result_stats[f"{self.project.slug},{self.prototype['transaction']}"]
assert [attrs for time, attrs in stats["data"]] == [
[{"count": 2000}],
[{"count": 9200}],
]
def test_p99(self):
with self.feature(self.features):
response = self.client.get(
self.url,
format="json",
data={
"end": iso_format(self.day_ago + timedelta(hours=2)),
"start": iso_format(self.day_ago),
"interval": "1h",
"field": ["project", "transaction"],
"query": "event.type:transaction",
"trendFunction": "p99()",
},
)
assert response.status_code == 200, response.content
events = response.data["events"]
result_stats = response.data["stats"]
assert len(events["data"]) == 1
self.expected_data.update(
{
"aggregate_range_1": 2000,
"aggregate_range_2": 9840,
"count_percentage": 3.0,
"trend_difference": 7840.0,
"trend_percentage": 4.92,
}
)
self.assert_event(events["data"][0])
stats = result_stats[f"{self.project.slug},{self.prototype['transaction']}"]
assert [attrs for time, attrs in stats["data"]] == [
[{"count": 2000}],
[{"count": 9840}],
]
def test_avg_trend_function(self):
with self.feature(self.features):
response = self.client.get(
self.url,
format="json",
data={
"end": iso_format(self.day_ago + timedelta(hours=2)),
"interval": "1h",
"start": iso_format(self.day_ago),
"field": ["project", "transaction"],
"query": "event.type:transaction",
"trendFunction": "avg(transaction.duration)",
"project": [self.project.id],
},
)
assert response.status_code == 200, response.content
events = response.data["events"]
result_stats = response.data["stats"]
assert len(events["data"]) == 1
self.expected_data.update(
{
"aggregate_range_1": 2000,
"aggregate_range_2": 4000,
"count_percentage": 3.0,
"trend_difference": 2000.0,
"trend_percentage": 2.0,
}
)
self.assert_event(events["data"][0])
stats = result_stats[f"{self.project.slug},{self.prototype['transaction']}"]
assert [attrs for time, attrs in stats["data"]] == [
[{"count": 2000}],
[{"count": 4000}],
]
def test_alias_in_conditions(self):
query_parts = [
"event.type:transaction",
"count_percentage():>0.25",
"count_percentage():<4",
"trend_percentage():>0%",
]
queries = [" ".join(query_parts), " AND ".join(query_parts)]
for query in queries:
with self.feature(self.features):
response = self.client.get(
self.url,
format="json",
data={
"end": iso_format(self.day_ago + timedelta(hours=2)),
"interval": "1h",
"start": iso_format(self.day_ago),
"field": ["project", "transaction"],
"query": query,
"trendFunction": "avg(transaction.duration)",
"project": [self.project.id],
},
)
assert response.status_code == 200, response.content
events = response.data["events"]
result_stats = response.data["stats"]
assert len(events["data"]) == 1
self.expected_data.update(
{
"aggregate_range_1": 2000,
"aggregate_range_2": 4000,
"count_percentage": 3.0,
"trend_difference": 2000.0,
"trend_percentage": 2.0,
}
)
self.assert_event(events["data"][0])
stats = result_stats[f"{self.project.slug},{self.prototype['transaction']}"]
assert [attrs for time, attrs in stats["data"]] == [
[{"count": 2000}],
[{"count": 4000}],
]
def test_trend_with_middle(self):
with self.feature(self.features):
response = self.client.get(
self.url,
format="json",
data={
"end": iso_format(self.day_ago + timedelta(hours=2)),
"middle": iso_format(self.day_ago + timedelta(hours=1, minutes=31)),
"start": iso_format(self.day_ago),
"interval": "1h",
"field": ["project", "transaction"],
"query": "event.type:transaction",
"trendFunction": "avg(transaction.duration)",
"project": [self.project.id],
},
)
assert response.status_code == 200, response.content
events = response.data["events"]
result_stats = response.data["stats"]
assert len(events["data"]) == 1
self.expected_data.update(
{
"count_range_2": 2,
"count_range_1": 2,
"aggregate_range_1": 1000,
"aggregate_range_2": 6000,
"count_percentage": 1.0,
"trend_difference": 5000.0,
"trend_percentage": 6.0,
}
)
self.assert_event(events["data"][0])
stats = result_stats[f"{self.project.slug},{self.prototype['transaction']}"]
assert [attrs for time, attrs in stats["data"]] == [
[{"count": 2000}],
[{"count": 4000}],
]
def test_invalid_middle_date(self):
with self.feature(self.features):
response = self.client.get(
self.url,
format="json",
data={
"start": iso_format(self.day_ago),
"middle": "blah",
"end": iso_format(self.day_ago + timedelta(hours=2)),
"field": ["project", "transaction"],
"query": "event.type:transaction",
"trendFunction": "p50()",
"project": [self.project.id],
},
)
assert response.status_code == 400
response = self.client.get(
self.url,
format="json",
data={
"start": iso_format(self.day_ago),
"middle": iso_format(self.day_ago - timedelta(hours=2)),
"end": iso_format(self.day_ago + timedelta(hours=2)),
"field": ["project", "transaction"],
"query": "event.type:transaction",
"trendFunction": "apdex(450)",
"project": [self.project.id],
},
)
assert response.status_code == 400
response = self.client.get(
self.url,
format="json",
data={
"start": iso_format(self.day_ago),
"middle": iso_format(self.day_ago + timedelta(hours=4)),
"end": iso_format(self.day_ago + timedelta(hours=2)),
"field": ["project", "transaction"],
"query": "event.type:transaction",
"trendFunction": "apdex(450)",
"project": [self.project.id],
},
)
assert response.status_code == 400
def test_invalid_trend_function(self):
with self.feature(self.features):
response = self.client.get(
self.url,
format="json",
data={
"end": iso_format(self.day_ago + timedelta(hours=2)),
"start": iso_format(self.day_ago),
"field": ["project", "transaction"],
"query": "event.type:transaction",
"trendFunction": "apdex(450)",
"project": [self.project.id],
},
)
assert response.status_code == 400
def test_divide_by_zero(self):
with self.feature(self.features):
response = self.client.get(
self.url,
format="json",
data={
# Set the timeframe to where the second range has no transactions so all the counts/percentile are 0
"end": iso_format(self.day_ago + timedelta(hours=2)),
"start": iso_format(self.day_ago - timedelta(hours=2)),
"interval": "1h",
"field": ["project", "transaction"],
"query": "event.type:transaction",
"project": [self.project.id],
},
)
assert response.status_code == 200, response.content
events = response.data["events"]
result_stats = response.data["stats"]
assert len(events["data"]) == 1
self.expected_data.update(
{
"count_range_2": 4,
"count_range_1": 0,
"aggregate_range_1": 0,
"aggregate_range_2": 2000.0,
"count_percentage": None,
"trend_difference": 0,
"trend_percentage": None,
}
)
self.assert_event(events["data"][0])
stats = result_stats[f"{self.project.slug},{self.prototype['transaction']}"]
assert [attrs for time, attrs in stats["data"]] == [
[{"count": 0}],
[{"count": 0}],
[{"count": 2000}],
[{"count": 2000}],
]
@region_silo_test
class OrganizationEventsTrendsPagingTest(APITestCase, SnubaTestCase):
def setUp(self):
super().setUp()
self.login_as(user=self.user)
self.url = reverse(
"sentry-api-0-organization-events-trends-stats",
kwargs={"organization_slug": self.project.organization.slug},
)
self.day_ago = before_now(days=1).replace(hour=10, minute=0, second=0, microsecond=0)
self.prototype = load_data("transaction")
self.features = {"organizations:performance-view": True}
# Make 10 transactions for paging
for i in range(10):
for j in range(2):
data = self.prototype.copy()
data["user"] = {"email": "[email protected]"}
data["start_timestamp"] = iso_format(self.day_ago + timedelta(minutes=30))
data["timestamp"] = iso_format(
self.day_ago + timedelta(hours=j, minutes=30, seconds=2)
)
if i < 5:
data["transaction"] = f"transaction_1{i}"
else:
data["transaction"] = f"transaction_2{i}"
self.store_event(data, project_id=self.project.id)
def _parse_links(self, header):
# links come in {url: {...attrs}}, but we need {rel: {...attrs}}
links = {}
for url, attrs in parse_link_header(header).items():
links[attrs["rel"]] = attrs
attrs["href"] = url
return links
def test_pagination(self):
with self.feature(self.features):
response = self.client.get(
self.url,
format="json",
data={
# Set the timeframe to where the second range has no transactions so all the counts/percentile are 0
"end": iso_format(self.day_ago + timedelta(hours=2)),
"start": iso_format(self.day_ago - timedelta(hours=2)),
"field": ["project", "transaction"],
"query": "event.type:transaction",
"project": [self.project.id],
},
)
assert response.status_code == 200, response.content
links = self._parse_links(response["Link"])
assert links["previous"]["results"] == "false"
assert links["next"]["results"] == "true"
assert len(response.data["events"]["data"]) == 5
response = self.client.get(links["next"]["href"], format="json")
assert response.status_code == 200, response.content
links = self._parse_links(response["Link"])
assert links["previous"]["results"] == "true"
assert links["next"]["results"] == "false"
assert len(response.data["events"]["data"]) == 5
def test_pagination_with_query(self):
with self.feature(self.features):
response = self.client.get(
self.url,
format="json",
data={
# Set the timeframe to where the second range has no transactions so all the counts/percentile are 0
"end": iso_format(self.day_ago + timedelta(hours=2)),
"start": iso_format(self.day_ago - timedelta(hours=2)),
"field": ["project", "transaction"],
"query": "event.type:transaction transaction:transaction_1*",
"project": [self.project.id],
},
)
assert response.status_code == 200, response.content
links = self._parse_links(response["Link"])
assert links["previous"]["results"] == "false"
assert links["next"]["results"] == "false"
assert len(response.data["events"]["data"]) == 5