test_organization_event_details.py 16 KB

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