123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389 |
- from datetime import timedelta
- from django.urls import reverse
- from rest_framework.exceptions import ErrorDetail
- from sentry.testutils.cases import APITestCase, SnubaTestCase
- from sentry.testutils.helpers.datetime import before_now, iso_format
- from sentry.utils.samples import load_data
- class OrganizationEventsSpansHistogramEndpointTest(APITestCase, SnubaTestCase):
- FEATURES = ["organizations:performance-span-histogram-view"]
- URL = "sentry-api-0-organization-events-spans-histogram"
- def setUp(self):
- super().setUp()
- self.features = {}
- self.login_as(user=self.user)
- self.org = self.create_organization(owner=self.user)
- self.project = self.create_project(organization=self.org)
- self.url = reverse(
- self.URL,
- kwargs={"organization_slug": self.org.slug},
- )
- self.min_ago = before_now(minutes=1).replace(microsecond=0)
- def create_event(self, **kwargs):
- if "spans" not in kwargs:
- kwargs["spans"] = [
- {
- "same_process_as_parent": True,
- "parent_span_id": "a" * 16,
- "span_id": x * 16,
- "start_timestamp": iso_format(self.min_ago + timedelta(seconds=1)),
- "timestamp": iso_format(self.min_ago + timedelta(seconds=4)),
- "op": "django.middleware",
- "description": "middleware span",
- "hash": "cd" * 8,
- "exclusive_time": 3.0,
- }
- for x in ["b", "c"]
- ] + [
- {
- "same_process_as_parent": True,
- "parent_span_id": "a" * 16,
- "span_id": x * 16,
- "start_timestamp": iso_format(self.min_ago + timedelta(seconds=4)),
- "timestamp": iso_format(self.min_ago + timedelta(seconds=5)),
- "op": "django.middleware",
- "description": "middleware span",
- "hash": "cd" * 8,
- "exclusive_time": 10.0,
- }
- for x in ["d", "e", "f"]
- ]
- data = load_data("transaction", **kwargs)
- data["transaction"] = "root transaction"
- return self.store_event(data, project_id=self.project.id)
- def format_span(self, op, group):
- return f"{op}:{group}"
- def do_request(self, query, with_feature=True):
- features = self.FEATURES if with_feature else []
- with self.feature(features):
- return self.client.get(self.url, query, format="json")
- def test_no_feature(self):
- query = {
- "projects": [-1],
- "span": self.format_span("django.middleware", "cd" * 8),
- "numBuckets": 50,
- }
- response = self.do_request(query, False)
- assert response.status_code == 404
- def test_no_projects(self):
- query = {
- "projects": [-1],
- "span": self.format_span("django.middleware", "cd" * 8),
- "numBuckets": 50,
- }
- response = self.do_request(query)
- assert response.status_code == 200
- assert response.data == {}
- def test_bad_params_missing_span(self):
- query = {
- "project": [self.project.id],
- "numBuckets": 50,
- }
- response = self.do_request(query)
- assert response.status_code == 400
- assert response.data == {"span": [ErrorDetail("This field is required.", code="required")]}
- def test_bad_params_missing_num_buckets(self):
- query = {
- "project": [self.project.id],
- "span": self.format_span("django.middleware", "cd" * 8),
- }
- response = self.do_request(query)
- assert response.status_code == 400
- assert response.data == {
- "numBuckets": [ErrorDetail("This field is required.", code="required")]
- }
- def test_bad_params_invalid_num_buckets(self):
- query = {
- "project": [self.project.id],
- "span": self.format_span("django.middleware", "cd" * 8),
- "numBuckets": "foo",
- }
- response = self.do_request(query)
- assert response.status_code == 400, "failing for numBuckets"
- assert response.data == {
- "numBuckets": ["A valid integer is required."]
- }, "failing for numBuckets"
- def test_bad_params_outside_range_num_buckets(self):
- query = {
- "project": [self.project.id],
- "span": self.format_span("django.middleware", "cd" * 8),
- "numBuckets": -1,
- }
- response = self.do_request(query)
- assert response.status_code == 400, "failing for numBuckets"
- assert response.data == {
- "numBuckets": ["Ensure this value is greater than or equal to 1."]
- }, "failing for numBuckets"
- def test_bad_params_num_buckets_too_large(self):
- query = {
- "project": [self.project.id],
- "span": self.format_span("django.middleware", "cd" * 8),
- "numBuckets": 101,
- }
- response = self.do_request(query)
- assert response.status_code == 400, "failing for numBuckets"
- assert response.data == {
- "numBuckets": ["Ensure this value is less than or equal to 100."]
- }, "failing for numBuckets"
- def test_bad_params_invalid_precision_too_small(self):
- query = {
- "project": [self.project.id],
- "span": self.format_span("django.middleware", "cd" * 8),
- "numBuckets": 50,
- "precision": -1,
- }
- response = self.do_request(query)
- assert response.status_code == 400, "failing for precision"
- assert response.data == {
- "precision": ["Ensure this value is greater than or equal to 0."],
- }, "failing for precision"
- def test_bad_params_invalid_precision_too_big(self):
- query = {
- "project": [self.project.id],
- "span": self.format_span("django.middleware", "cd" * 8),
- "numBuckets": 50,
- "precision": 100,
- }
- response = self.do_request(query)
- assert response.status_code == 400, "failing for precision"
- assert response.data == {
- "precision": ["Ensure this value is less than or equal to 4."],
- }, "failing for precision"
- def test_bad_params_reverse_min_max(self):
- query = {
- "project": [self.project.id],
- "span": self.format_span("django.middleware", "cd" * 8),
- "numBuckets": 50,
- "min": 10,
- "max": 5,
- }
- response = self.do_request(query)
- assert response.data == {"non_field_errors": ["min cannot be greater than max."]}
- def test_bad_params_invalid_min(self):
- query = {
- "project": [self.project.id],
- "span": self.format_span("django.middleware", "cd" * 8),
- "numBuckets": 50,
- "min": "foo",
- }
- response = self.do_request(query)
- assert response.status_code == 400, "failing for min"
- assert response.data == {"min": ["A valid number is required."]}, "failing for min"
- def test_bad_params_invalid_max(self):
- query = {
- "project": [self.project.id],
- "span": self.format_span("django.middleware", "cd" * 8),
- "numBuckets": 50,
- "max": "bar",
- }
- response = self.do_request(query)
- assert response.status_code == 400, "failing for max"
- assert response.data == {"max": ["A valid number is required."]}, "failing for max"
- def test_bad_params_invalid_data_filter(self):
- query = {
- "project": [self.project.id],
- "span": self.format_span("django.middleware", "cd" * 8),
- "numBuckets": 50,
- "dataFilter": "invalid",
- }
- response = self.do_request(query)
- assert response.status_code == 400, "failing for dataFilter"
- assert response.data == {
- "dataFilter": ['"invalid" is not a valid choice.']
- }, "failing for dataFilter"
- def test_histogram_empty(self):
- num_buckets = 5
- query = {
- "project": [self.project.id],
- "span": self.format_span("django.view", "cd" * 8),
- "numBuckets": num_buckets,
- }
- expected_empty_response = [{"bin": i, "count": 0} for i in range(num_buckets)]
- response = self.do_request(query)
- assert response.status_code == 200, response.content
- assert response.data == expected_empty_response
- def test_histogram(self):
- self.create_event()
- num_buckets = 50
- query = {
- "project": [self.project.id],
- "span": self.format_span("django.middleware", "cd" * 8),
- "numBuckets": num_buckets,
- }
- response = self.do_request(query)
- assert response.status_code == 200, response.content
- for bucket in response.data:
- if bucket["bin"] == 3:
- assert bucket["count"] == 2
- elif bucket["bin"] == 10:
- assert bucket["count"] == 3
- else:
- assert bucket["count"] == 0
- def test_histogram_using_min_max(self):
- self.create_event()
- num_buckets = 10
- min = 5
- max = 11
- query = {
- "project": [self.project.id],
- "span": self.format_span("django.middleware", "cd" * 8),
- "numBuckets": num_buckets,
- "min": min,
- "max": max,
- }
- response = self.do_request(query)
- assert response.status_code == 200, response.content
- for bucket in response.data:
- if bucket["bin"] == 10:
- assert bucket["count"] == 3
- else:
- assert bucket["count"] == 0
- assert response.data[0]["bin"] == min
- assert response.data[-1]["bin"] == max - 1
- def test_histogram_using_given_min_above_queried_max(self):
- self.create_event()
- num_buckets = 10
- min = 12
- query = {
- "project": [self.project.id],
- "span": self.format_span("django.middleware", "cd" * 8),
- "numBuckets": num_buckets,
- "min": min,
- }
- response = self.do_request(query)
- assert response.status_code == 200
- for bucket in response.data:
- assert bucket["count"] == 0
- assert response.data[0] == {"bin": min, "count": 0}
- assert len(response.data) == 1
- def test_histogram_using_given_max_below_queried_min(self):
- self.create_event()
- num_buckets = 10
- max = 2
- query = {
- "project": [self.project.id],
- "span": self.format_span("django.middleware", "cd" * 8),
- "numBuckets": num_buckets,
- "max": max,
- }
- response = self.do_request(query)
- assert response.status_code == 200
- for bucket in response.data:
- assert bucket["count"] == 0
- assert response.data[-1] == {"bin": max - 1, "count": 0}
- def test_histogram_all_data_filter(self):
- # populate with default spans
- self.create_event()
- spans = [
- {
- "same_process_as_parent": True,
- "parent_span_id": "a" * 16,
- "span_id": "e" * 16,
- "start_timestamp": iso_format(self.min_ago + timedelta(seconds=1)),
- "timestamp": iso_format(self.min_ago + timedelta(seconds=4)),
- "op": "django.middleware",
- "description": "middleware span",
- "hash": "cd" * 8,
- "exclusive_time": 60.0,
- }
- ]
- # populate with an outlier span
- self.create_event(spans=spans)
- query = {
- "project": [self.project.id],
- "span": self.format_span("django.middleware", "cd" * 8),
- "numBuckets": 10,
- "dataFilter": "all",
- }
- response = self.do_request(query)
- assert response.status_code == 200
- assert response.data[-1] == {"bin": 60, "count": 1}
- def test_histogram_exclude_outliers_data_filter(self):
- # populate with default spans
- self.create_event()
- spans = [
- {
- "same_process_as_parent": True,
- "parent_span_id": "a" * 16,
- "span_id": "e" * 16,
- "start_timestamp": iso_format(self.min_ago + timedelta(seconds=1)),
- "timestamp": iso_format(self.min_ago + timedelta(seconds=4)),
- "op": "django.middleware",
- "description": "middleware span",
- "hash": "cd" * 8,
- "exclusive_time": 60.0,
- }
- ]
- # populate with an outlier span
- self.create_event(spans=spans)
- query = {
- "project": [self.project.id],
- "span": self.format_span("django.middleware", "cd" * 8),
- "numBuckets": 10,
- "dataFilter": "exclude_outliers",
- }
- response = self.do_request(query)
- assert response.status_code == 200
- assert response.data[-1]["bin"] != 60
|