test_organization_events_vitals.py 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264
  1. from datetime import timedelta
  2. from django.urls import reverse
  3. from sentry.models.transaction_threshold import ProjectTransactionThreshold, TransactionMetric
  4. from sentry.testutils import APITestCase, SnubaTestCase
  5. from sentry.testutils.helpers.datetime import before_now, iso_format
  6. from sentry.utils.samples import load_data
  7. class OrganizationEventsVitalsEndpointTest(APITestCase, SnubaTestCase):
  8. def setUp(self):
  9. super().setUp()
  10. self.start = before_now(days=1).replace(hour=10, minute=0, second=0, microsecond=0)
  11. self.end = self.start + timedelta(hours=6)
  12. self.transaction_data = load_data("transaction", timestamp=self.start)
  13. self.query = {
  14. "start": iso_format(self.start),
  15. "end": iso_format(self.end),
  16. }
  17. self.features = {}
  18. def store_event(self, data, measurements=None, **kwargs):
  19. if measurements:
  20. for vital, value in measurements.items():
  21. data["measurements"][vital]["value"] = value
  22. return super().store_event(
  23. data.copy(),
  24. project_id=self.project.id,
  25. )
  26. def do_request(self, query=None, features=None):
  27. if features is None:
  28. features = {"organizations:discover-basic": True}
  29. features.update(self.features)
  30. if query is None:
  31. query = self.query
  32. self.login_as(user=self.user)
  33. url = reverse(
  34. "sentry-api-0-organization-events-vitals",
  35. kwargs={"organization_slug": self.organization.slug},
  36. )
  37. with self.feature(features):
  38. return self.client.get(url, query, format="json")
  39. def test_no_projects(self):
  40. response = self.do_request()
  41. assert response.status_code == 200, response.content
  42. assert len(response.data) == 0
  43. def test_no_vitals(self):
  44. self.store_event(
  45. self.transaction_data,
  46. project_id=self.project.id,
  47. )
  48. self.query.update({"vital": []})
  49. response = self.do_request()
  50. assert response.status_code == 400, response.content
  51. assert "Need to pass at least one vital" == response.data["detail"]
  52. def test_bad_vital(self):
  53. self.store_event(
  54. self.transaction_data,
  55. project_id=self.project.id,
  56. )
  57. self.query.update({"vital": ["foobar"]})
  58. response = self.do_request()
  59. assert response.status_code == 400, response.content
  60. assert "foobar is not a valid vital" == response.data["detail"]
  61. def test_simple(self):
  62. data = self.transaction_data.copy()
  63. for lcp in [2000, 3000, 5000]:
  64. self.store_event(
  65. data,
  66. {"lcp": lcp},
  67. project_id=self.project.id,
  68. )
  69. self.query.update({"vital": ["measurements.lcp"]})
  70. response = self.do_request()
  71. assert response.status_code == 200, response.content
  72. assert response.data["measurements.lcp"] == {
  73. "good": 1,
  74. "meh": 1,
  75. "poor": 1,
  76. "total": 3,
  77. "p75": 4000,
  78. }
  79. def test_simple_with_refining_user_misery_filter(self):
  80. project1 = self.create_project(organization=self.organization)
  81. project2 = self.create_project(organization=self.organization)
  82. ProjectTransactionThreshold.objects.create(
  83. project=project1,
  84. organization=project1.organization,
  85. threshold=100,
  86. metric=TransactionMetric.LCP.value,
  87. )
  88. ProjectTransactionThreshold.objects.create(
  89. project=project2,
  90. organization=project2.organization,
  91. threshold=1000,
  92. metric=TransactionMetric.LCP.value,
  93. )
  94. data = self.transaction_data.copy()
  95. for project in [project1, project2]:
  96. for lcp in [2000, 3000, 5000]:
  97. self.store_event(
  98. data,
  99. {"lcp": lcp},
  100. project_id=project.id,
  101. )
  102. self.query.update({"vital": ["measurements.lcp"]})
  103. response = self.do_request(
  104. features={"organizations:global-views": True, "organizations:discover-basic": True}
  105. )
  106. assert response.status_code == 200, response.content
  107. assert response.data["measurements.lcp"] == {
  108. "good": 0,
  109. "meh": 1,
  110. "poor": 1,
  111. "total": 2,
  112. "p75": 4500,
  113. }
  114. self.query.update({"query": "user_misery():<0.04"})
  115. response = self.do_request(
  116. features={"organizations:global-views": True, "organizations:discover-basic": True}
  117. )
  118. assert response.status_code == 200, response.content
  119. assert len(response.data) == 1
  120. assert response.data["measurements.lcp"] == {
  121. "good": 0,
  122. "meh": 1,
  123. "poor": 1,
  124. "total": 2,
  125. "p75": 4500,
  126. }
  127. def test_grouping(self):
  128. counts = [
  129. (100, 2),
  130. (3000, 3),
  131. (4500, 1),
  132. ]
  133. for duration, count in counts:
  134. for _ in range(count):
  135. self.store_event(
  136. load_data("transaction", timestamp=self.start),
  137. {"lcp": duration},
  138. project_id=self.project.id,
  139. )
  140. self.query.update({"vital": ["measurements.lcp"]})
  141. response = self.do_request()
  142. assert response.status_code == 200
  143. assert response.data["measurements.lcp"] == {
  144. "good": 2,
  145. "meh": 3,
  146. "poor": 1,
  147. "total": 6,
  148. "p75": 3000,
  149. }
  150. def test_multiple_vitals(self):
  151. vitals = {"lcp": 3000, "fid": 50, "cls": 0.15, "fcp": 5000, "fp": 4000}
  152. self.store_event(
  153. load_data("transaction", timestamp=self.start),
  154. vitals,
  155. project_id=self.project.id,
  156. )
  157. self.query.update(
  158. {
  159. "vital": [
  160. "measurements.lcp",
  161. "measurements.fid",
  162. "measurements.cls",
  163. "measurements.fcp",
  164. "measurements.fp",
  165. ]
  166. }
  167. )
  168. response = self.do_request()
  169. assert response.status_code == 200
  170. assert response.data["measurements.lcp"] == {
  171. "good": 0,
  172. "meh": 1,
  173. "poor": 0,
  174. "total": 1,
  175. "p75": 3000,
  176. }
  177. assert response.data["measurements.fid"] == {
  178. "good": 1,
  179. "meh": 0,
  180. "poor": 0,
  181. "total": 1,
  182. "p75": 50,
  183. }
  184. assert response.data["measurements.cls"] == {
  185. "good": 0,
  186. "meh": 1,
  187. "poor": 0,
  188. "total": 1,
  189. "p75": 0.15,
  190. }
  191. assert response.data["measurements.fcp"] == {
  192. "good": 0,
  193. "meh": 0,
  194. "poor": 1,
  195. "total": 1,
  196. "p75": 5000,
  197. }
  198. assert response.data["measurements.fp"] == {
  199. "good": 0,
  200. "meh": 0,
  201. "poor": 1,
  202. "total": 1,
  203. "p75": 4000,
  204. }
  205. def test_transactions_without_vitals(self):
  206. del self.transaction_data["measurements"]
  207. self.store_event(
  208. self.transaction_data,
  209. project_id=self.project.id,
  210. )
  211. self.query.update({"vital": ["measurements.lcp", "measurements.fcp"]})
  212. response = self.do_request()
  213. assert response.status_code == 200
  214. assert response.data["measurements.lcp"] == {
  215. "good": 0,
  216. "meh": 0,
  217. "poor": 0,
  218. "total": 0,
  219. "p75": None,
  220. }
  221. assert response.data["measurements.fcp"] == {
  222. "good": 0,
  223. "meh": 0,
  224. "poor": 0,
  225. "total": 0,
  226. "p75": None,
  227. }
  228. class OrganizationEventsVitalsEndpointTestWithSnql(OrganizationEventsVitalsEndpointTest):
  229. def setUp(self):
  230. super().setUp()
  231. self.features = {"organizations:performance-use-snql": True}