test_organization_event_details.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378
  1. from datetime import timedelta
  2. import pytest
  3. from django.urls import NoReverseMatch, reverse
  4. from sentry.models.group import Group
  5. from sentry.search.events import constants
  6. from sentry.testutils.cases import APITestCase, MetricsEnhancedPerformanceTestCase, SnubaTestCase
  7. from sentry.testutils.helpers.datetime import before_now, iso_format
  8. from sentry.utils.samples import load_data
  9. from tests.sentry.issues.test_utils import OccurrenceTestMixin
  10. pytestmark = pytest.mark.sentry_metrics
  11. def format_project_event(project_slug, event_id):
  12. return f"{project_slug}:{event_id}"
  13. class OrganizationEventDetailsEndpointTest(APITestCase, SnubaTestCase, OccurrenceTestMixin):
  14. def setUp(self):
  15. super().setUp()
  16. min_ago = iso_format(before_now(minutes=1))
  17. two_min_ago = iso_format(before_now(minutes=2))
  18. three_min_ago = iso_format(before_now(minutes=3))
  19. self.login_as(user=self.user)
  20. self.project = self.create_project()
  21. self.project_2 = self.create_project()
  22. self.store_event(
  23. data={
  24. "event_id": "a" * 32,
  25. "message": "oh no",
  26. "timestamp": three_min_ago,
  27. "fingerprint": ["group-1"],
  28. },
  29. project_id=self.project.id,
  30. )
  31. self.store_event(
  32. data={
  33. "event_id": "b" * 32,
  34. "message": "very bad",
  35. "timestamp": two_min_ago,
  36. "fingerprint": ["group-1"],
  37. },
  38. project_id=self.project.id,
  39. )
  40. self.store_event(
  41. data={
  42. "event_id": "c" * 32,
  43. "message": "very bad",
  44. "timestamp": min_ago,
  45. "fingerprint": ["group-2"],
  46. },
  47. project_id=self.project.id,
  48. )
  49. self.groups = list(Group.objects.all().order_by("id"))
  50. def test_performance_flag(self):
  51. url = reverse(
  52. "sentry-api-0-organization-event-details",
  53. kwargs={
  54. "organization_slug": self.project.organization.slug,
  55. "project_slug": self.project.slug,
  56. "event_id": "a" * 32,
  57. },
  58. )
  59. with self.feature(
  60. {"organizations:discover-basic": False, "organizations:performance-view": True}
  61. ):
  62. response = self.client.get(url, format="json")
  63. assert response.status_code == 200, response.content
  64. assert response.data["id"] == "a" * 32
  65. assert response.data["projectSlug"] == self.project.slug
  66. def test_simple(self):
  67. url = reverse(
  68. "sentry-api-0-organization-event-details",
  69. kwargs={
  70. "organization_slug": self.project.organization.slug,
  71. "project_slug": self.project.slug,
  72. "event_id": "a" * 32,
  73. },
  74. )
  75. with self.feature("organizations:discover-basic"):
  76. response = self.client.get(url, format="json")
  77. assert response.status_code == 200, response.content
  78. assert response.data["id"] == "a" * 32
  79. assert response.data["projectSlug"] == self.project.slug
  80. def test_simple_transaction(self):
  81. min_ago = iso_format(before_now(minutes=1))
  82. event = self.store_event(
  83. data={
  84. "event_id": "d" * 32,
  85. "type": "transaction",
  86. "transaction": "api.issue.delete",
  87. "spans": [],
  88. "contexts": {"trace": {"op": "foobar", "trace_id": "a" * 32, "span_id": "a" * 16}},
  89. "start_timestamp": iso_format(before_now(minutes=1, seconds=5)),
  90. "timestamp": min_ago,
  91. },
  92. project_id=self.project.id,
  93. )
  94. url = reverse(
  95. "sentry-api-0-organization-event-details",
  96. kwargs={
  97. "organization_slug": self.project.organization.slug,
  98. "project_slug": self.project.slug,
  99. "event_id": event.event_id,
  100. },
  101. )
  102. with self.feature("organizations:discover-basic"):
  103. response = self.client.get(url, format="json")
  104. assert response.status_code == 200
  105. assert response.data["id"] == "d" * 32
  106. assert response.data["type"] == "transaction"
  107. def test_no_access_missing_feature(self):
  108. with self.feature({"organizations:discover-basic": False}):
  109. url = reverse(
  110. "sentry-api-0-organization-event-details",
  111. kwargs={
  112. "organization_slug": self.project.organization.slug,
  113. "project_slug": self.project.slug,
  114. "event_id": "a" * 32,
  115. },
  116. )
  117. response = self.client.get(url, format="json")
  118. assert response.status_code == 404, response.content
  119. def test_access_non_member_project(self):
  120. # Add a new user to a project and then access events on project they are not part of.
  121. member_user = self.create_user()
  122. team = self.create_team(members=[member_user])
  123. self.create_project(organization=self.organization, teams=[team])
  124. # Enable open membership
  125. self.organization.flags.allow_joinleave = True
  126. self.organization.save()
  127. self.login_as(member_user)
  128. url = reverse(
  129. "sentry-api-0-organization-event-details",
  130. kwargs={
  131. "organization_slug": self.organization.slug,
  132. "project_slug": self.project.slug,
  133. "event_id": "a" * 32,
  134. },
  135. )
  136. with self.feature("organizations:discover-basic"):
  137. response = self.client.get(url, format="json")
  138. assert response.status_code == 200, response.content
  139. # When open membership is off, access should be denied to non owner users
  140. self.organization.flags.allow_joinleave = False
  141. self.organization.save()
  142. with self.feature("organizations:discover-basic"):
  143. response = self.client.get(url, format="json")
  144. assert response.status_code == 404, response.content
  145. def test_no_event(self):
  146. url = reverse(
  147. "sentry-api-0-organization-event-details",
  148. kwargs={
  149. "organization_slug": self.project.organization.slug,
  150. "project_slug": self.project.slug,
  151. "event_id": "d" * 32,
  152. },
  153. )
  154. with self.feature("organizations:discover-basic"):
  155. response = self.client.get(url, format="json")
  156. assert response.status_code == 404, response.content
  157. def test_invalid_event_id(self):
  158. with pytest.raises(NoReverseMatch):
  159. reverse(
  160. "sentry-api-0-organization-event-details",
  161. kwargs={
  162. "organization_slug": self.project.organization.slug,
  163. "project_slug": self.project.slug,
  164. "event_id": "not-an-event",
  165. },
  166. )
  167. def test_long_trace_description(self):
  168. data = load_data("transaction")
  169. data["event_id"] = "d" * 32
  170. data["timestamp"] = iso_format(before_now(minutes=1))
  171. data["start_timestamp"] = iso_format(before_now(minutes=1) - timedelta(seconds=5))
  172. data["contexts"]["trace"]["description"] = "b" * 512
  173. self.store_event(data=data, project_id=self.project.id)
  174. url = reverse(
  175. "sentry-api-0-organization-event-details",
  176. kwargs={
  177. "organization_slug": self.project.organization.slug,
  178. "project_slug": self.project.slug,
  179. "event_id": "d" * 32,
  180. },
  181. )
  182. with self.feature("organizations:discover-basic"):
  183. response = self.client.get(url, format="json")
  184. assert response.status_code == 200, response.content
  185. trace = response.data["contexts"]["trace"]
  186. original_trace = data["contexts"]["trace"]
  187. assert trace["trace_id"] == original_trace["trace_id"]
  188. assert trace["span_id"] == original_trace["span_id"]
  189. assert trace["parent_span_id"] == original_trace["parent_span_id"]
  190. assert trace["description"][:-3] in original_trace["description"]
  191. def test_blank_fields(self):
  192. url = reverse(
  193. "sentry-api-0-organization-event-details",
  194. kwargs={
  195. "organization_slug": self.project.organization.slug,
  196. "project_slug": self.project.slug,
  197. "event_id": "a" * 32,
  198. },
  199. )
  200. with self.feature("organizations:discover-basic"):
  201. response = self.client.get(
  202. url,
  203. data={"field": ["", " "], "statsPeriod": "24h"},
  204. format="json",
  205. )
  206. assert response.status_code == 200, response.content
  207. assert response.data["id"] == "a" * 32
  208. assert response.data["projectSlug"] == self.project.slug
  209. def test_out_of_retention(self):
  210. self.store_event(
  211. data={
  212. "event_id": "d" * 32,
  213. "message": "oh no",
  214. "timestamp": iso_format(before_now(days=2)),
  215. "fingerprint": ["group-1"],
  216. },
  217. project_id=self.project.id,
  218. )
  219. url = reverse(
  220. "sentry-api-0-organization-event-details",
  221. kwargs={
  222. "organization_slug": self.project.organization.slug,
  223. "project_slug": self.project.slug,
  224. "event_id": "d" * 32,
  225. },
  226. )
  227. with self.options({"system.event-retention-days": 1}):
  228. response = self.client.get(
  229. url,
  230. format="json",
  231. )
  232. assert response.status_code == 404, response.content
  233. def test_generic_event(self):
  234. occurrence, _ = self.process_occurrence(
  235. project_id=self.project.id,
  236. event_data={
  237. "level": "info",
  238. },
  239. )
  240. url = reverse(
  241. "sentry-api-0-organization-event-details",
  242. kwargs={
  243. "organization_slug": self.project.organization.slug,
  244. "project_slug": self.project.slug,
  245. "event_id": occurrence.event_id,
  246. },
  247. )
  248. with self.feature("organizations:discover-basic"):
  249. response = self.client.get(url, format="json")
  250. assert response.status_code == 200, response.content
  251. assert response.data["id"] == occurrence.event_id
  252. assert response.data["projectSlug"] == self.project.slug
  253. assert response.data["occurrence"] is not None
  254. assert response.data["occurrence"]["id"] == occurrence.id
  255. class EventComparisonTest(MetricsEnhancedPerformanceTestCase):
  256. endpoint = "sentry-api-0-organization-event-details"
  257. def setUp(self):
  258. self.init_snuba()
  259. self.ten_mins_ago = before_now(minutes=10)
  260. self.transaction_data = load_data("transaction", timestamp=self.ten_mins_ago)
  261. self.RESULT_COLUMN = "span.averageResults"
  262. event = self.store_event(self.transaction_data, self.project)
  263. self.url = reverse(
  264. self.endpoint,
  265. kwargs={
  266. "organization_slug": self.project.organization.slug,
  267. "project_slug": self.project.slug,
  268. "event_id": event.event_id,
  269. },
  270. )
  271. self.login_as(user=self.user)
  272. self.store_span_metric(
  273. 1,
  274. internal_metric=constants.SELF_TIME_LIGHT,
  275. timestamp=self.ten_mins_ago,
  276. tags={"span.group": "26b881987e4bad99"},
  277. )
  278. def test_get(self):
  279. response = self.client.get(self.url, {"averageColumn": "span.self_time"})
  280. assert response.status_code == 200, response.content
  281. entries = response.data["entries"] # type: ignore[attr-defined]
  282. for entry in entries:
  283. if entry["type"] == "spans":
  284. for span in entry["data"]:
  285. if span["op"] == "db":
  286. assert span[self.RESULT_COLUMN] == {"avg(span.self_time)": 1.0}
  287. if span["op"] == "django.middleware":
  288. assert self.RESULT_COLUMN not in span
  289. def test_get_multiple_columns(self):
  290. self.store_span_metric(
  291. 2,
  292. internal_metric=constants.SPAN_METRICS_MAP["span.duration"],
  293. timestamp=self.ten_mins_ago,
  294. tags={"span.group": "26b881987e4bad99"},
  295. )
  296. response = self.client.get(self.url, {"averageColumn": ["span.self_time", "span.duration"]})
  297. assert response.status_code == 200, response.content
  298. entries = response.data["entries"] # type: ignore[attr-defined]
  299. for entry in entries:
  300. if entry["type"] == "spans":
  301. for span in entry["data"]:
  302. if span["op"] == "db":
  303. assert span[self.RESULT_COLUMN] == {
  304. "avg(span.self_time)": 1.0,
  305. "avg(span.duration)": 2.0,
  306. }
  307. if span["op"] == "django.middlewares":
  308. assert self.RESULT_COLUMN not in span
  309. def test_nan_column(self):
  310. # If there's nothing stored for a metric, span.duration in this case the query returns nan
  311. response = self.client.get(self.url, {"averageColumn": ["span.self_time", "span.duration"]})
  312. assert response.status_code == 200, response.content
  313. entries = response.data["entries"] # type: ignore[attr-defined]
  314. for entry in entries:
  315. if entry["type"] == "spans":
  316. for span in entry["data"]:
  317. if span["op"] == "db":
  318. assert span[self.RESULT_COLUMN] == {"avg(span.self_time)": 1.0}
  319. if span["op"] == "django.middlewares":
  320. assert self.RESULT_COLUMN not in span
  321. def test_invalid_column(self):
  322. # If any columns are invalid, ignore average field in results completely
  323. response = self.client.get(
  324. self.url, {"averageColumn": ["span.self_time", "span.everything"]}
  325. )
  326. assert response.status_code == 200, response.content
  327. entries = response.data["entries"] # type: ignore[attr-defined]
  328. for entry in entries:
  329. if entry["type"] == "spans":
  330. for span in entry["data"]:
  331. assert self.RESULT_COLUMN not in span