test_organization_events_spans_histogram.py 13 KB

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