test_organization_events_spans_histogram.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387
  1. from datetime import timedelta
  2. from django.urls import reverse
  3. from rest_framework.exceptions import ErrorDetail
  4. from sentry.testutils.cases import APITestCase, SnubaTestCase
  5. from sentry.testutils.helpers.datetime import before_now, iso_format
  6. from sentry.testutils.silo import region_silo_test
  7. from sentry.utils.samples import load_data
  8. @region_silo_test
  9. class OrganizationEventsSpansHistogramEndpointTest(APITestCase, SnubaTestCase):
  10. FEATURES = ["organizations:performance-span-histogram-view"]
  11. URL = "sentry-api-0-organization-events-spans-histogram"
  12. def setUp(self):
  13. super().setUp()
  14. self.features = {}
  15. self.login_as(user=self.user)
  16. self.org = self.create_organization(owner=self.user)
  17. self.project = self.create_project(organization=self.org)
  18. self.url = reverse(
  19. self.URL,
  20. kwargs={"organization_slug": self.org.slug},
  21. )
  22. self.min_ago = before_now(minutes=1).replace(microsecond=0)
  23. def create_event(self, **kwargs):
  24. if "spans" not in kwargs:
  25. kwargs["spans"] = [
  26. {
  27. "same_process_as_parent": True,
  28. "parent_span_id": "a" * 16,
  29. "span_id": x * 16,
  30. "start_timestamp": iso_format(self.min_ago + timedelta(seconds=1)),
  31. "timestamp": iso_format(self.min_ago + timedelta(seconds=4)),
  32. "op": "django.middleware",
  33. "description": "middleware span",
  34. "exclusive_time": 3.0,
  35. }
  36. for x in ["b", "c"]
  37. ] + [
  38. {
  39. "same_process_as_parent": True,
  40. "parent_span_id": "a" * 16,
  41. "span_id": x * 16,
  42. "start_timestamp": iso_format(self.min_ago + timedelta(seconds=4)),
  43. "timestamp": iso_format(self.min_ago + timedelta(seconds=5)),
  44. "op": "django.middleware",
  45. "description": "middleware span",
  46. "exclusive_time": 10.0,
  47. }
  48. for x in ["d", "e", "f"]
  49. ]
  50. data = load_data("transaction", **kwargs)
  51. data["transaction"] = "root transaction"
  52. return self.store_event(data, project_id=self.project.id)
  53. def format_span(self, op, group):
  54. return f"{op}:{group}"
  55. def do_request(self, query, with_feature=True):
  56. features = self.FEATURES if with_feature else []
  57. with self.feature(features):
  58. return self.client.get(self.url, query, format="json")
  59. def test_no_feature(self):
  60. query = {
  61. "projects": [-1],
  62. "span": self.format_span("django.middleware", "2b9cbb96dbf59baa"),
  63. "numBuckets": 50,
  64. }
  65. response = self.do_request(query, False)
  66. assert response.status_code == 404
  67. def test_no_projects(self):
  68. query = {
  69. "projects": [-1],
  70. "span": self.format_span("django.middleware", "2b9cbb96dbf59baa"),
  71. "numBuckets": 50,
  72. }
  73. response = self.do_request(query)
  74. assert response.status_code == 200
  75. assert response.data == {}
  76. def test_bad_params_missing_span(self):
  77. query = {
  78. "project": [self.project.id],
  79. "numBuckets": 50,
  80. }
  81. response = self.do_request(query)
  82. assert response.status_code == 400
  83. assert response.data == {"span": [ErrorDetail("This field is required.", code="required")]}
  84. def test_bad_params_missing_num_buckets(self):
  85. query = {
  86. "project": [self.project.id],
  87. "span": self.format_span("django.middleware", "2b9cbb96dbf59baa"),
  88. }
  89. response = self.do_request(query)
  90. assert response.status_code == 400
  91. assert response.data == {
  92. "numBuckets": [ErrorDetail("This field is required.", code="required")]
  93. }
  94. def test_bad_params_invalid_num_buckets(self):
  95. query = {
  96. "project": [self.project.id],
  97. "span": self.format_span("django.middleware", "2b9cbb96dbf59baa"),
  98. "numBuckets": "foo",
  99. }
  100. response = self.do_request(query)
  101. assert response.status_code == 400, "failing for numBuckets"
  102. assert response.data == {
  103. "numBuckets": ["A valid integer is required."]
  104. }, "failing for numBuckets"
  105. def test_bad_params_outside_range_num_buckets(self):
  106. query = {
  107. "project": [self.project.id],
  108. "span": self.format_span("django.middleware", "2b9cbb96dbf59baa"),
  109. "numBuckets": -1,
  110. }
  111. response = self.do_request(query)
  112. assert response.status_code == 400, "failing for numBuckets"
  113. assert response.data == {
  114. "numBuckets": ["Ensure this value is greater than or equal to 1."]
  115. }, "failing for numBuckets"
  116. def test_bad_params_num_buckets_too_large(self):
  117. query = {
  118. "project": [self.project.id],
  119. "span": self.format_span("django.middleware", "2b9cbb96dbf59baa"),
  120. "numBuckets": 101,
  121. }
  122. response = self.do_request(query)
  123. assert response.status_code == 400, "failing for numBuckets"
  124. assert response.data == {
  125. "numBuckets": ["Ensure this value is less than or equal to 100."]
  126. }, "failing for numBuckets"
  127. def test_bad_params_invalid_precision_too_small(self):
  128. query = {
  129. "project": [self.project.id],
  130. "span": self.format_span("django.middleware", "2b9cbb96dbf59baa"),
  131. "numBuckets": 50,
  132. "precision": -1,
  133. }
  134. response = self.do_request(query)
  135. assert response.status_code == 400, "failing for precision"
  136. assert response.data == {
  137. "precision": ["Ensure this value is greater than or equal to 0."],
  138. }, "failing for precision"
  139. def test_bad_params_invalid_precision_too_big(self):
  140. query = {
  141. "project": [self.project.id],
  142. "span": self.format_span("django.middleware", "2b9cbb96dbf59baa"),
  143. "numBuckets": 50,
  144. "precision": 100,
  145. }
  146. response = self.do_request(query)
  147. assert response.status_code == 400, "failing for precision"
  148. assert response.data == {
  149. "precision": ["Ensure this value is less than or equal to 4."],
  150. }, "failing for precision"
  151. def test_bad_params_reverse_min_max(self):
  152. query = {
  153. "project": [self.project.id],
  154. "span": self.format_span("django.middleware", "2b9cbb96dbf59baa"),
  155. "numBuckets": 50,
  156. "min": 10,
  157. "max": 5,
  158. }
  159. response = self.do_request(query)
  160. assert response.data == {"non_field_errors": ["min cannot be greater than max."]}
  161. def test_bad_params_invalid_min(self):
  162. query = {
  163. "project": [self.project.id],
  164. "span": self.format_span("django.middleware", "2b9cbb96dbf59baa"),
  165. "numBuckets": 50,
  166. "min": "foo",
  167. }
  168. response = self.do_request(query)
  169. assert response.status_code == 400, "failing for min"
  170. assert response.data == {"min": ["A valid number is required."]}, "failing for min"
  171. def test_bad_params_invalid_max(self):
  172. query = {
  173. "project": [self.project.id],
  174. "span": self.format_span("django.middleware", "2b9cbb96dbf59baa"),
  175. "numBuckets": 50,
  176. "max": "bar",
  177. }
  178. response = self.do_request(query)
  179. assert response.status_code == 400, "failing for max"
  180. assert response.data == {"max": ["A valid number is required."]}, "failing for max"
  181. def test_bad_params_invalid_data_filter(self):
  182. query = {
  183. "project": [self.project.id],
  184. "span": self.format_span("django.middleware", "2b9cbb96dbf59baa"),
  185. "numBuckets": 50,
  186. "dataFilter": "invalid",
  187. }
  188. response = self.do_request(query)
  189. assert response.status_code == 400, "failing for dataFilter"
  190. assert response.data == {
  191. "dataFilter": ['"invalid" is not a valid choice.']
  192. }, "failing for dataFilter"
  193. def test_histogram_empty(self):
  194. num_buckets = 5
  195. query = {
  196. "project": [self.project.id],
  197. "span": self.format_span("django.view", "2b9cbb96dbf59baa"),
  198. "numBuckets": num_buckets,
  199. }
  200. expected_empty_response = [{"bin": i, "count": 0} for i in range(num_buckets)]
  201. response = self.do_request(query)
  202. assert response.status_code == 200, response.content
  203. assert response.data == expected_empty_response
  204. def test_histogram(self):
  205. self.create_event()
  206. num_buckets = 50
  207. query = {
  208. "project": [self.project.id],
  209. "span": self.format_span("django.middleware", "2b9cbb96dbf59baa"),
  210. "numBuckets": num_buckets,
  211. }
  212. response = self.do_request(query)
  213. assert response.status_code == 200, response.content
  214. for bucket in response.data:
  215. if bucket["bin"] == 3:
  216. assert bucket["count"] == 2
  217. elif bucket["bin"] == 10:
  218. assert bucket["count"] == 3
  219. else:
  220. assert bucket["count"] == 0
  221. def test_histogram_using_min_max(self):
  222. self.create_event()
  223. num_buckets = 10
  224. min = 5
  225. max = 11
  226. query = {
  227. "project": [self.project.id],
  228. "span": self.format_span("django.middleware", "2b9cbb96dbf59baa"),
  229. "numBuckets": num_buckets,
  230. "min": min,
  231. "max": max,
  232. }
  233. response = self.do_request(query)
  234. assert response.status_code == 200, response.content
  235. for bucket in response.data:
  236. if bucket["bin"] == 10:
  237. assert bucket["count"] == 3
  238. else:
  239. assert bucket["count"] == 0
  240. assert response.data[0]["bin"] == min
  241. assert response.data[-1]["bin"] == max - 1
  242. def test_histogram_using_given_min_above_queried_max(self):
  243. self.create_event()
  244. num_buckets = 10
  245. min = 12
  246. query = {
  247. "project": [self.project.id],
  248. "span": self.format_span("django.middleware", "2b9cbb96dbf59baa"),
  249. "numBuckets": num_buckets,
  250. "min": min,
  251. }
  252. response = self.do_request(query)
  253. assert response.status_code == 200
  254. for bucket in response.data:
  255. assert bucket["count"] == 0
  256. assert response.data[0] == {"bin": min, "count": 0}
  257. assert len(response.data) == 1
  258. def test_histogram_using_given_max_below_queried_min(self):
  259. self.create_event()
  260. num_buckets = 10
  261. max = 2
  262. query = {
  263. "project": [self.project.id],
  264. "span": self.format_span("django.middleware", "2b9cbb96dbf59baa"),
  265. "numBuckets": num_buckets,
  266. "max": max,
  267. }
  268. response = self.do_request(query)
  269. assert response.status_code == 200
  270. for bucket in response.data:
  271. assert bucket["count"] == 0
  272. assert response.data[-1] == {"bin": max - 1, "count": 0}
  273. def test_histogram_all_data_filter(self):
  274. # populate with default spans
  275. self.create_event()
  276. spans = [
  277. {
  278. "same_process_as_parent": True,
  279. "parent_span_id": "a" * 16,
  280. "span_id": "e" * 16,
  281. "start_timestamp": iso_format(self.min_ago + timedelta(seconds=1)),
  282. "timestamp": iso_format(self.min_ago + timedelta(seconds=4)),
  283. "op": "django.middleware",
  284. "description": "middleware span",
  285. "exclusive_time": 60.0,
  286. }
  287. ]
  288. # populate with an outlier span
  289. self.create_event(spans=spans)
  290. query = {
  291. "project": [self.project.id],
  292. "span": self.format_span("django.middleware", "2b9cbb96dbf59baa"),
  293. "numBuckets": 10,
  294. "dataFilter": "all",
  295. }
  296. response = self.do_request(query)
  297. assert response.status_code == 200
  298. assert response.data[-1] == {"bin": 60, "count": 1}
  299. def test_histogram_exclude_outliers_data_filter(self):
  300. # populate with default spans
  301. self.create_event()
  302. spans = [
  303. {
  304. "same_process_as_parent": True,
  305. "parent_span_id": "a" * 16,
  306. "span_id": "e" * 16,
  307. "start_timestamp": iso_format(self.min_ago + timedelta(seconds=1)),
  308. "timestamp": iso_format(self.min_ago + timedelta(seconds=4)),
  309. "op": "django.middleware",
  310. "description": "middleware span",
  311. "exclusive_time": 60.0,
  312. }
  313. ]
  314. # populate with an outlier span
  315. self.create_event(spans=spans)
  316. query = {
  317. "project": [self.project.id],
  318. "span": self.format_span("django.middleware", "2b9cbb96dbf59baa"),
  319. "numBuckets": 10,
  320. "dataFilter": "exclude_outliers",
  321. }
  322. response = self.do_request(query)
  323. assert response.status_code == 200
  324. assert response.data[-1]["bin"] != 60