test_organization_events_spans_histogram.py 13 KB

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