test_organization_events_trace.py 64 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702
  1. from datetime import datetime, timedelta
  2. from unittest import mock
  3. from uuid import uuid4
  4. import pytest
  5. from django.urls import NoReverseMatch, reverse
  6. from sentry import options
  7. from sentry.testutils.cases import TraceTestCase
  8. from sentry.utils.samples import load_data
  9. from tests.snuba.api.endpoints.test_organization_events import OrganizationEventsEndpointTestBase
  10. class OrganizationEventsTraceEndpointBase(OrganizationEventsEndpointTestBase, TraceTestCase):
  11. url_name: str
  12. FEATURES = [
  13. "organizations:performance-view",
  14. "organizations:performance-file-io-main-thread-detector",
  15. "organizations:trace-view-load-more",
  16. "organizations:performance-slow-db-issue",
  17. ]
  18. def setUp(self):
  19. """
  20. Span structure:
  21. root
  22. gen1-0
  23. gen2-0
  24. gen3-0
  25. gen1-1
  26. gen2-1
  27. gen1-2
  28. gen2-2
  29. """
  30. super().setUp()
  31. options.set("performance.issues.all.problem-detection", 1.0)
  32. options.set("performance.issues.file_io_main_thread.problem-creation", 1.0)
  33. options.set("performance.issues.slow_db_query.problem-creation", 1.0)
  34. self.login_as(user=self.user)
  35. self.url = reverse(
  36. self.url_name,
  37. kwargs={
  38. "organization_slug": self.project.organization.slug,
  39. "trace_id": self.trace_id,
  40. },
  41. )
  42. def load_trace(self):
  43. self.root_event = self.create_event(
  44. trace_id=self.trace_id,
  45. transaction="root",
  46. spans=[
  47. {
  48. "same_process_as_parent": True,
  49. "op": "http",
  50. "description": f"GET gen1-{i}",
  51. "span_id": root_span_id,
  52. "trace_id": self.trace_id,
  53. }
  54. for i, root_span_id in enumerate(self.root_span_ids)
  55. ],
  56. measurements={
  57. "lcp": 1000,
  58. "fcp": 750,
  59. "fid": 3.5,
  60. },
  61. parent_span_id=None,
  62. file_io_performance_issue=True,
  63. slow_db_performance_issue=True,
  64. project_id=self.project.id,
  65. milliseconds=3000,
  66. )
  67. # First Generation
  68. # TODO: temporary, this is until we deprecate using this endpoint without useSpans
  69. if isinstance(self, OrganizationEventsTraceEndpointTestUsingSpans):
  70. self.gen1_span_ids = ["0014" * 4, *(uuid4().hex[:16] for _ in range(2))]
  71. else:
  72. self.gen1_span_ids = [uuid4().hex[:16] for _ in range(3)]
  73. self.gen1_project = self.create_project(organization=self.organization)
  74. self.gen1_events = [
  75. self.create_event(
  76. trace_id=self.trace_id,
  77. transaction=f"/transaction/gen1-{i}",
  78. spans=[
  79. {
  80. "same_process_as_parent": True,
  81. "op": "http",
  82. "description": f"GET gen2-{i}",
  83. "span_id": gen1_span_id,
  84. "trace_id": self.trace_id,
  85. }
  86. ],
  87. parent_span_id=root_span_id,
  88. project_id=self.gen1_project.id,
  89. milliseconds=2000,
  90. )
  91. for i, (root_span_id, gen1_span_id) in enumerate(
  92. zip(self.root_span_ids, self.gen1_span_ids)
  93. )
  94. ]
  95. # Second Generation
  96. self.gen2_span_ids = [uuid4().hex[:16] for _ in range(3)]
  97. self.gen2_project = self.create_project(organization=self.organization)
  98. # Intentially pick a span id that starts with 0s
  99. self.gen2_span_id = "0011" * 4
  100. self.gen2_events = [
  101. self.create_event(
  102. trace_id=self.trace_id,
  103. transaction=f"/transaction/gen2-{i}",
  104. spans=[
  105. {
  106. "same_process_as_parent": True,
  107. "op": "http",
  108. "description": f"GET gen3-{i}" if i == 0 else f"SPAN gen3-{i}",
  109. "span_id": gen2_span_id,
  110. "trace_id": self.trace_id,
  111. }
  112. ],
  113. parent_span_id=gen1_span_id,
  114. span_id=self.gen2_span_id if i == 0 else None,
  115. project_id=self.gen2_project.id,
  116. milliseconds=1000,
  117. )
  118. for i, (gen1_span_id, gen2_span_id) in enumerate(
  119. zip(self.gen1_span_ids, self.gen2_span_ids)
  120. )
  121. ]
  122. # Third generation
  123. self.gen3_project = self.create_project(organization=self.organization)
  124. self.gen3_event = self.create_event(
  125. trace_id=self.trace_id,
  126. transaction="/transaction/gen3-0",
  127. spans=[],
  128. project_id=self.gen3_project.id,
  129. parent_span_id=self.gen2_span_id,
  130. milliseconds=500,
  131. )
  132. class OrganizationEventsTraceLightEndpointTest(OrganizationEventsTraceEndpointBase):
  133. url_name = "sentry-api-0-organization-events-trace-light"
  134. def test_no_projects(self):
  135. user = self.create_user()
  136. org = self.create_organization(owner=user)
  137. self.login_as(user=user)
  138. url = reverse(
  139. self.url_name,
  140. kwargs={"organization_slug": org.slug, "trace_id": uuid4().hex},
  141. )
  142. with self.feature(self.FEATURES):
  143. response = self.client.get(
  144. url,
  145. format="json",
  146. )
  147. assert response.status_code == 404, response.content
  148. def test_bad_ids(self):
  149. # Fake event id
  150. with self.feature(self.FEATURES):
  151. response = self.client.get(
  152. self.url,
  153. data={"event_id": uuid4().hex},
  154. format="json",
  155. )
  156. assert response.status_code == 404, response.content
  157. # Invalid event id
  158. with self.feature(self.FEATURES):
  159. response = self.client.get(
  160. self.url,
  161. data={"event_id": "not-a-event"},
  162. format="json",
  163. )
  164. assert response.status_code == 400, response.content
  165. # Fake trace id
  166. self.url = reverse(
  167. "sentry-api-0-organization-events-trace-light",
  168. kwargs={"organization_slug": self.project.organization.slug, "trace_id": uuid4().hex},
  169. )
  170. with self.feature(self.FEATURES):
  171. response = self.client.get(
  172. self.url,
  173. data={"event_id": "a" * 32},
  174. format="json",
  175. )
  176. assert response.status_code == 404, response.content
  177. # Invalid trace id
  178. with pytest.raises(NoReverseMatch):
  179. self.url = reverse(
  180. "sentry-api-0-organization-events-trace-light",
  181. kwargs={
  182. "organization_slug": self.project.organization.slug,
  183. "trace_id": "not-a-trace",
  184. },
  185. )
  186. def test_no_roots(self):
  187. """Even when there's no root, we return the current event"""
  188. self.load_trace()
  189. no_root_trace = uuid4().hex
  190. parent_span_id = uuid4().hex[:16]
  191. no_root_event = self.create_event(
  192. trace_id=no_root_trace,
  193. transaction="/not_root/but_only_transaction",
  194. spans=[],
  195. parent_span_id=parent_span_id,
  196. project_id=self.project.id,
  197. )
  198. url = reverse(
  199. "sentry-api-0-organization-events-trace-light",
  200. kwargs={"organization_slug": self.project.organization.slug, "trace_id": no_root_trace},
  201. )
  202. with self.feature(self.FEATURES):
  203. response = self.client.get(
  204. url,
  205. data={"event_id": no_root_event.event_id},
  206. format="json",
  207. )
  208. assert response.status_code == 200, response.content
  209. assert len(response.data["transactions"]) == 1
  210. event = response.data["transactions"][0]
  211. # Basically know nothing about this event
  212. assert event["generation"] is None
  213. assert event["parent_event_id"] is None
  214. assert event["parent_span_id"] == parent_span_id
  215. assert event["event_id"] == no_root_event.event_id
  216. def test_multiple_roots(self):
  217. self.load_trace()
  218. second_root = self.create_event(
  219. trace_id=self.trace_id,
  220. transaction="/second_root",
  221. spans=[],
  222. parent_span_id=None,
  223. project_id=self.project.id,
  224. )
  225. with self.feature(self.FEATURES):
  226. data: dict[str, str | int] = {"event_id": second_root.event_id, "project": -1}
  227. response = self.client.get(self.url, data=data, format="json")
  228. assert response.status_code == 200, response.content
  229. assert len(response.data["transactions"]) == 1
  230. event = response.data["transactions"][0]
  231. assert event["generation"] == 0
  232. assert event["parent_event_id"] is None
  233. assert event["parent_span_id"] is None
  234. def test_root_event(self):
  235. self.load_trace()
  236. root_event_id = self.root_event.event_id
  237. with self.feature(self.FEATURES):
  238. data: dict[str, str | int] = {"event_id": root_event_id, "project": -1}
  239. response = self.client.get(self.url, data=data, format="json")
  240. assert response.status_code == 200, response.content
  241. assert len(response.data["transactions"]) == 4
  242. events = {item["event_id"]: item for item in response.data["transactions"]}
  243. assert root_event_id in events
  244. event = events[root_event_id]
  245. assert event["generation"] == 0
  246. assert event["parent_event_id"] is None
  247. assert event["parent_span_id"] is None
  248. for i, child_event in enumerate(self.gen1_events):
  249. child_event_id = child_event.event_id
  250. assert child_event_id in events
  251. event = events[child_event_id]
  252. assert event["generation"] == 1
  253. assert event["parent_event_id"] == root_event_id
  254. assert event["parent_span_id"] == self.root_span_ids[i]
  255. def test_root_with_multiple_roots(self):
  256. self.load_trace()
  257. root_event_id = self.root_event.event_id
  258. self.create_event(
  259. trace_id=self.trace_id,
  260. transaction="/second_root",
  261. spans=[],
  262. parent_span_id=None,
  263. project_id=self.project.id,
  264. )
  265. with self.feature(self.FEATURES):
  266. response = self.client.get(
  267. self.url,
  268. data={"event_id": self.root_event.event_id},
  269. format="json",
  270. )
  271. assert response.status_code == 200, response.content
  272. assert len(response.data["transactions"]) == 4
  273. events = {item["event_id"]: item for item in response.data["transactions"]}
  274. assert root_event_id in events
  275. event = events[root_event_id]
  276. assert event["generation"] == 0
  277. assert event["parent_event_id"] is None
  278. assert event["parent_span_id"] is None
  279. for i, child_event in enumerate(self.gen1_events):
  280. child_event_id = child_event.event_id
  281. assert child_event_id in events
  282. event = events[child_event_id]
  283. assert event["generation"] == 1
  284. assert event["parent_event_id"] == root_event_id
  285. assert event["parent_span_id"] == self.root_span_ids[i]
  286. def test_direct_parent_with_children(self):
  287. self.load_trace()
  288. root_event_id = self.root_event.event_id
  289. current_event = self.gen1_events[0].event_id
  290. child_event_id = self.gen2_events[0].event_id
  291. with self.feature(self.FEATURES):
  292. data: dict[str, str | int] = {"event_id": current_event, "project": -1}
  293. response = self.client.get(self.url, data=data, format="json")
  294. assert response.status_code == 200, response.content
  295. assert len(response.data["transactions"]) == 3
  296. events = {item["event_id"]: item for item in response.data["transactions"]}
  297. assert root_event_id in events
  298. event = events[root_event_id]
  299. assert event["generation"] == 0
  300. assert event["parent_event_id"] is None
  301. assert event["parent_span_id"] is None
  302. assert current_event in events
  303. event = events[current_event]
  304. assert event["generation"] == 1
  305. assert event["parent_event_id"] == root_event_id
  306. assert event["parent_span_id"] == self.root_span_ids[0]
  307. assert child_event_id in events
  308. event = events[child_event_id]
  309. assert event["generation"] == 2
  310. assert event["parent_event_id"] == current_event
  311. assert event["parent_span_id"] == self.gen1_span_ids[0]
  312. def test_direct_parent_with_children_and_multiple_root(self):
  313. self.load_trace()
  314. root_event_id = self.root_event.event_id
  315. current_event = self.gen1_events[0].event_id
  316. child_event_id = self.gen2_events[0].event_id
  317. self.create_event(
  318. trace_id=self.trace_id,
  319. transaction="/second_root",
  320. spans=[],
  321. parent_span_id=None,
  322. project_id=self.project.id,
  323. )
  324. with self.feature(self.FEATURES):
  325. data: dict[str, str | int] = {"event_id": current_event, "project": -1}
  326. response = self.client.get(self.url, data=data, format="json")
  327. assert response.status_code == 200, response.content
  328. assert len(response.data["transactions"]) == 3
  329. events = {item["event_id"]: item for item in response.data["transactions"]}
  330. assert root_event_id in events
  331. event = events[root_event_id]
  332. assert event["generation"] == 0
  333. assert event["parent_event_id"] is None
  334. assert event["parent_span_id"] is None
  335. assert current_event in events
  336. event = events[current_event]
  337. assert event["generation"] == 1
  338. assert event["parent_event_id"] == root_event_id
  339. assert event["parent_span_id"] == self.root_span_ids[0]
  340. assert child_event_id in events
  341. event = events[child_event_id]
  342. assert event["generation"] == 2
  343. assert event["parent_event_id"] == current_event
  344. assert event["parent_span_id"] == self.gen1_span_ids[0]
  345. def test_second_generation_with_children(self):
  346. self.load_trace()
  347. current_event = self.gen2_events[0].event_id
  348. child_event_id = self.gen3_event.event_id
  349. with self.feature(self.FEATURES):
  350. data: dict[str, str | int] = {"event_id": current_event, "project": -1}
  351. response = self.client.get(self.url, data=data, format="json")
  352. assert response.status_code == 200, response.content
  353. assert len(response.data["transactions"]) == 2
  354. events = {item["event_id"]: item for item in response.data["transactions"]}
  355. assert current_event in events
  356. event = events[current_event]
  357. # Parent/generation is unknown in this case
  358. assert event["generation"] is None
  359. assert event["parent_event_id"] is None
  360. # But we still know the parent_span
  361. assert event["parent_span_id"] == self.gen1_span_ids[0]
  362. assert child_event_id in events
  363. event = events[child_event_id]
  364. assert event["generation"] is None
  365. assert event["parent_event_id"] == current_event
  366. assert event["parent_span_id"] == self.gen2_span_id
  367. def test_third_generation_no_children(self):
  368. self.load_trace()
  369. current_event = self.gen3_event.event_id
  370. with self.feature(self.FEATURES):
  371. data: dict[str, str | int] = {"event_id": current_event, "project": -1}
  372. response = self.client.get(self.url, data=data, format="json")
  373. assert response.status_code == 200, response.content
  374. assert len(response.data["transactions"]) == 1
  375. event = response.data["transactions"][0]
  376. assert event["generation"] is None
  377. # Parent is unknown in this case
  378. assert event["parent_event_id"] is None
  379. # But we still know the parent_span
  380. assert event["parent_span_id"] == self.gen2_span_id
  381. def test_sibling_transactions(self):
  382. """More than one transaction can share a parent_span_id"""
  383. self.load_trace()
  384. gen3_event_siblings = [
  385. self.create_event(
  386. trace_id=self.trace_id,
  387. transaction="/transaction/gen3-1",
  388. spans=[],
  389. project_id=self.create_project(organization=self.organization).id,
  390. parent_span_id=self.gen2_span_ids[1],
  391. milliseconds=500,
  392. ).event_id,
  393. self.create_event(
  394. trace_id=self.trace_id,
  395. transaction="/transaction/gen3-2",
  396. spans=[],
  397. project_id=self.create_project(organization=self.organization).id,
  398. parent_span_id=self.gen2_span_ids[1],
  399. milliseconds=1500,
  400. ).event_id,
  401. ]
  402. current_event = self.gen2_events[1].event_id
  403. with self.feature(self.FEATURES):
  404. data: dict[str, str | int] = {"event_id": current_event, "project": -1}
  405. response = self.client.get(self.url, data=data, format="json")
  406. assert len(response.data["transactions"]) == 3
  407. events = {item["event_id"]: item for item in response.data["transactions"]}
  408. for child_event_id in gen3_event_siblings:
  409. assert child_event_id in events
  410. event = events[child_event_id]
  411. assert event["generation"] is None
  412. assert event["parent_event_id"] == current_event
  413. assert event["parent_span_id"] == self.gen2_span_ids[1]
  414. def test_with_error_event(self):
  415. self.load_trace()
  416. root_event_id = self.root_event.event_id
  417. current_transaction_event = self.gen1_events[0].event_id
  418. start, _ = self.get_start_end_from_day_ago(1000)
  419. error_data = load_data(
  420. "javascript",
  421. timestamp=start,
  422. )
  423. error_data["contexts"]["trace"] = {
  424. "type": "trace",
  425. "trace_id": self.trace_id,
  426. "span_id": self.gen1_span_ids[0],
  427. }
  428. error_data["tags"] = [["transaction", "/transaction/gen1-0"]]
  429. error = self.store_event(error_data, project_id=self.gen1_project.id)
  430. def assertions(response):
  431. assert response.status_code == 200, response.content
  432. assert len(response.data["transactions"]) == 3
  433. events = {item["event_id"]: item for item in response.data["transactions"]}
  434. assert root_event_id in events
  435. event = events[root_event_id]
  436. assert event["generation"] == 0
  437. assert event["parent_event_id"] is None
  438. assert event["parent_span_id"] is None
  439. assert len(event["errors"]) == 0
  440. assert current_transaction_event in events
  441. event = events[current_transaction_event]
  442. assert event["generation"] == 1
  443. assert event["parent_event_id"] == root_event_id
  444. assert event["parent_span_id"] == self.root_span_ids[0]
  445. assert len(event["errors"]) == 1
  446. assert event["errors"][0]["event_id"] == error.event_id
  447. assert event["errors"][0]["issue_id"] == error.group_id
  448. assert event["errors"][0]["message"] == error.search_message
  449. with self.feature(self.FEATURES):
  450. response = self.client.get(
  451. self.url,
  452. data={"event_id": error.event_id, "project": -1},
  453. format="json",
  454. )
  455. assertions(response)
  456. with self.feature(self.FEATURES):
  457. data: dict[str, str | int] = {"event_id": current_transaction_event, "project": -1}
  458. response = self.client.get(self.url, data=data, format="json")
  459. assertions(response)
  460. def assert_orphan_error_response(self, response, error, span_id):
  461. assert response.status_code == 200, response.content
  462. assert response.data["transactions"] == []
  463. assert len(response.data["orphan_errors"]) == 1
  464. assert {
  465. "event_id": error.event_id,
  466. "issue_id": error.group_id,
  467. "span": span_id,
  468. "project_id": self.project.id,
  469. "project_slug": self.project.slug,
  470. "level": "fatal",
  471. "title": error.title,
  472. "timestamp": datetime.fromisoformat(error.timestamp).timestamp(),
  473. "generation": 0,
  474. "event_type": "error",
  475. "message": error.search_message,
  476. } == response.data["orphan_errors"][0]
  477. def test_with_one_orphan_error(self):
  478. self.load_trace()
  479. span_id = uuid4().hex[:16]
  480. start, _ = self.get_start_end_from_day_ago(1000)
  481. error_data = load_data(
  482. "javascript",
  483. timestamp=start,
  484. )
  485. error_data["contexts"]["trace"] = {
  486. "type": "trace",
  487. "trace_id": self.trace_id,
  488. "span_id": span_id,
  489. }
  490. error_data["level"] = "fatal"
  491. error = self.store_event(error_data, project_id=self.project.id)
  492. with self.feature(
  493. [*self.FEATURES, "organizations:performance-tracing-without-performance"]
  494. ):
  495. response = self.client.get(
  496. self.url,
  497. data={"event_id": error.event_id, "project": -1},
  498. format="json",
  499. )
  500. self.assert_orphan_error_response(response, error, span_id)
  501. def test_with_multiple_orphan_errors(self):
  502. self.load_trace()
  503. span_id = uuid4().hex[:16]
  504. start, end = self.get_start_end_from_day_ago(1000)
  505. error_data = load_data(
  506. "javascript",
  507. timestamp=start,
  508. )
  509. error_data["contexts"]["trace"] = {
  510. "type": "trace",
  511. "trace_id": self.trace_id,
  512. "span_id": span_id,
  513. }
  514. error_data["level"] = "fatal"
  515. error = self.store_event(error_data, project_id=self.project.id)
  516. error_data1 = load_data(
  517. "javascript",
  518. timestamp=end,
  519. )
  520. error_data1["contexts"]["trace"] = {
  521. "type": "trace",
  522. "trace_id": self.trace_id,
  523. "span_id": span_id,
  524. }
  525. error_data1["level"] = "warning"
  526. self.store_event(error_data1, project_id=self.project.id)
  527. with self.feature(
  528. [*self.FEATURES, "organizations:performance-tracing-without-performance"]
  529. ):
  530. response = self.client.get(
  531. self.url,
  532. data={"event_id": error.event_id, "project": -1},
  533. format="json",
  534. )
  535. self.assert_orphan_error_response(response, error, span_id)
  536. def test_with_unknown_event(self):
  537. with self.feature(
  538. [*self.FEATURES, "organizations:performance-tracing-without-performance"]
  539. ):
  540. response = self.client.get(
  541. self.url,
  542. data={"event_id": "766758c00ff54d8ab865369ecab53ae6", "project": "-1"},
  543. format="json",
  544. )
  545. assert response.status_code == 404
  546. class OrganizationEventsTraceEndpointTest(OrganizationEventsTraceEndpointBase):
  547. url_name = "sentry-api-0-organization-events-trace"
  548. check_generation = True
  549. def assert_event(self, result, event_data, message):
  550. assert result["transaction"] == event_data.transaction, message
  551. assert result["event_id"] == event_data.event_id
  552. assert result["start_timestamp"] == event_data.data["start_timestamp"]
  553. assert result["profile_id"] == event_data.data["contexts"]["profile"]["profile_id"]
  554. def assert_trace_data(self, root, gen2_no_children=True):
  555. """see the setUp docstring for an idea of what the response structure looks like"""
  556. self.assert_event(root, self.root_event, "root")
  557. assert root["parent_event_id"] is None
  558. assert root["parent_span_id"] is None
  559. if self.check_generation:
  560. assert root["generation"] == 0
  561. assert root["transaction.duration"] == 3000
  562. assert len(root["children"]) == 3
  563. self.assert_performance_issues(root)
  564. for i, gen1 in enumerate(root["children"]):
  565. self.assert_event(gen1, self.gen1_events[i], f"gen1_{i}")
  566. assert gen1["parent_event_id"] == self.root_event.event_id
  567. assert gen1["parent_span_id"] == self.root_span_ids[i]
  568. if self.check_generation:
  569. assert gen1["generation"] == 1
  570. assert gen1["transaction.duration"] == 2000
  571. assert len(gen1["children"]) == 1
  572. gen2 = gen1["children"][0]
  573. self.assert_event(gen2, self.gen2_events[i], f"gen2_{i}")
  574. assert gen2["parent_event_id"] == self.gen1_events[i].event_id
  575. assert gen2["parent_span_id"] == self.gen1_span_ids[i]
  576. if self.check_generation:
  577. assert gen2["generation"] == 2
  578. assert gen2["transaction.duration"] == 1000
  579. # Only the first gen2 descendent has a child
  580. if i == 0:
  581. assert len(gen2["children"]) == 1
  582. gen3 = gen2["children"][0]
  583. self.assert_event(gen3, self.gen3_event, f"gen3_{i}")
  584. assert gen3["parent_event_id"] == self.gen2_events[i].event_id
  585. assert gen3["parent_span_id"] == self.gen2_span_id
  586. if self.check_generation:
  587. assert gen3["generation"] == 3
  588. assert gen3["transaction.duration"] == 500
  589. assert len(gen3["children"]) == 0
  590. elif gen2_no_children:
  591. assert len(gen2["children"]) == 0
  592. def assert_performance_issues(self, root):
  593. """Broken in the non-spans endpoint, but we're not maintaining that anymore"""
  594. pass
  595. def client_get(self, data, url=None):
  596. if url is None:
  597. url = self.url
  598. return self.client.get(
  599. url,
  600. data,
  601. format="json",
  602. )
  603. def test_no_projects(self):
  604. user = self.create_user()
  605. org = self.create_organization(owner=user)
  606. self.login_as(user=user)
  607. url = reverse(
  608. self.url_name,
  609. kwargs={"organization_slug": org.slug, "trace_id": uuid4().hex},
  610. )
  611. with self.feature(self.FEATURES):
  612. response = self.client.get(
  613. url,
  614. format="json",
  615. )
  616. assert response.status_code == 404, response.content
  617. def test_simple(self):
  618. self.load_trace()
  619. with self.feature(self.FEATURES):
  620. response = self.client_get(
  621. data={"project": -1},
  622. )
  623. assert response.status_code == 200, response.content
  624. trace_transaction = response.data["transactions"][0]
  625. self.assert_trace_data(trace_transaction)
  626. # We shouldn't have detailed fields here
  627. assert "transaction.status" not in trace_transaction
  628. assert "tags" not in trace_transaction
  629. assert "measurements" not in trace_transaction
  630. def test_simple_with_limit(self):
  631. self.load_trace()
  632. with self.feature(self.FEATURES):
  633. response = self.client_get(
  634. data={"project": -1, "limit": 200},
  635. )
  636. assert response.status_code == 200, response.content
  637. trace_transaction = response.data["transactions"][0]
  638. self.assert_trace_data(trace_transaction)
  639. # We shouldn't have detailed fields here
  640. assert "transaction.status" not in trace_transaction
  641. assert "tags" not in trace_transaction
  642. assert "measurements" not in trace_transaction
  643. def test_detailed_trace(self):
  644. self.load_trace()
  645. with self.feature(self.FEATURES):
  646. response = self.client_get(
  647. data={"project": -1, "detailed": 1},
  648. )
  649. assert response.status_code == 200, response.content
  650. trace_transaction = response.data["transactions"][0]
  651. self.assert_trace_data(trace_transaction)
  652. root = trace_transaction
  653. assert root["transaction.status"] == "ok"
  654. root_tags = {tag["key"]: tag["value"] for tag in root["tags"]}
  655. for [key, value] in self.root_event.tags:
  656. if not key.startswith("sentry:"):
  657. assert root_tags[key] == value, f"tags - {key}"
  658. else:
  659. assert root_tags[key[7:]] == value, f"tags - {key}"
  660. assert root["measurements"]["lcp"]["value"] == 1000
  661. assert root["measurements"]["fcp"]["value"] == 750
  662. def test_detailed_trace_with_bad_tags(self):
  663. """Basically test that we're actually using the event serializer's method for tags"""
  664. trace = uuid4().hex
  665. self.create_event(
  666. trace_id=trace,
  667. transaction="bad-tags",
  668. parent_span_id=None,
  669. spans=[],
  670. project_id=self.project.id,
  671. tags=[["somethinglong" * 250, "somethinglong" * 250]],
  672. milliseconds=3000,
  673. store_event_kwargs={"assert_no_errors": False},
  674. )
  675. url = reverse(
  676. self.url_name,
  677. kwargs={"organization_slug": self.project.organization.slug, "trace_id": trace},
  678. )
  679. with self.feature(self.FEATURES):
  680. response = self.client_get(
  681. data={"project": -1, "detailed": 1},
  682. url=url,
  683. )
  684. assert response.status_code == 200, response.content
  685. root = response.data["transactions"][0]
  686. assert root["transaction.status"] == "ok"
  687. assert {"key": None, "value": None} in root["tags"]
  688. def test_bad_span_loop(self):
  689. """Maliciously create a loop in the span structure
  690. Structure then becomes something like this:
  691. root
  692. gen1-0...
  693. gen1-1
  694. gen2-1
  695. gen3-1
  696. gen_2-1
  697. gen3-1...
  698. """
  699. self.load_trace()
  700. gen3_loop_event = self.create_event(
  701. trace_id=self.trace_id,
  702. transaction="/transaction/gen3-1/loop",
  703. spans=[
  704. {
  705. "same_process_as_parent": True,
  706. "op": "http",
  707. "description": "GET gen2-1",
  708. "span_id": self.gen1_span_ids[1],
  709. "trace_id": self.trace_id,
  710. }
  711. ],
  712. parent_span_id=self.gen2_span_ids[1],
  713. project_id=self.project.id,
  714. )
  715. with self.feature(self.FEATURES):
  716. response = self.client_get(
  717. data={"project": -1},
  718. )
  719. assert response.status_code == 200, response.content
  720. # Should be the same as the simple testcase
  721. trace_transaction = response.data["transactions"][0]
  722. self.assert_trace_data(trace_transaction, gen2_no_children=False)
  723. # The difference is that gen3-1 should exist with no children
  724. gen2_1 = trace_transaction["children"][1]["children"][0]
  725. assert len(gen2_1["children"]) == 1
  726. gen3_1 = gen2_1["children"][0]
  727. assert gen3_1["event_id"] == gen3_loop_event.event_id
  728. # We didn't even try to start the loop of spans
  729. assert len(gen3_1["children"]) == 0
  730. def test_bad_orphan_span_loop(self):
  731. """Maliciously create a loop in the span structure but for an orphan event"""
  732. root_span_id = uuid4().hex[:16]
  733. root_parent_span = uuid4().hex[:16]
  734. root_event = self.create_event(
  735. trace_id=self.trace_id,
  736. transaction="/orphan/root/",
  737. spans=[
  738. {
  739. "same_process_as_parent": True,
  740. "op": "http",
  741. "description": "GET orphan_child",
  742. "span_id": root_span_id,
  743. "trace_id": self.trace_id,
  744. }
  745. ],
  746. parent_span_id=root_parent_span,
  747. project_id=self.project.id,
  748. milliseconds=3000,
  749. start_timestamp=self.day_ago - timedelta(minutes=1),
  750. )
  751. orphan_child = self.create_event(
  752. trace_id=self.trace_id,
  753. transaction="/orphan/child/",
  754. spans=[
  755. {
  756. "same_process_as_parent": True,
  757. "op": "http",
  758. "description": "GET orphan_root",
  759. "span_id": root_parent_span,
  760. "trace_id": self.trace_id,
  761. }
  762. ],
  763. parent_span_id=root_span_id,
  764. project_id=self.project.id,
  765. milliseconds=300,
  766. )
  767. with self.feature(self.FEATURES):
  768. response = self.client_get(
  769. data={"project": -1},
  770. )
  771. assert response.status_code == 200, response.content
  772. assert len(response.data["transactions"]) == 1
  773. # There really isn't a right answer to which orphan is the "root" since this loops, but the current
  774. # implementation will make the older event the root
  775. root = response.data["transactions"][0]
  776. self.assert_event(root, root_event, "root")
  777. assert len(root["children"]) == 1
  778. child = root["children"][0]
  779. self.assert_event(child, orphan_child, "child")
  780. def test_multiple_roots(self):
  781. trace_id = uuid4().hex
  782. first_root = self.create_event(
  783. trace_id=trace_id,
  784. transaction="/first_root",
  785. spans=[],
  786. parent_span_id=None,
  787. project_id=self.project.id,
  788. milliseconds=500,
  789. )
  790. second_root = self.create_event(
  791. trace_id=trace_id,
  792. transaction="/second_root",
  793. spans=[],
  794. parent_span_id=None,
  795. project_id=self.project.id,
  796. milliseconds=1000,
  797. )
  798. self.url = reverse(
  799. self.url_name,
  800. kwargs={"organization_slug": self.project.organization.slug, "trace_id": trace_id},
  801. )
  802. with self.feature(self.FEATURES):
  803. response = self.client_get(
  804. data={"project": -1},
  805. )
  806. assert response.status_code == 200, response.content
  807. assert len(response.data["transactions"]) == 2
  808. self.assert_event(response.data["transactions"][0], first_root, "first_root")
  809. self.assert_event(response.data["transactions"][1], second_root, "second_root")
  810. def test_sibling_transactions(self):
  811. """More than one transaction can share a parent_span_id"""
  812. self.load_trace()
  813. gen3_event_siblings = [
  814. self.create_event(
  815. trace_id=self.trace_id,
  816. transaction="/transaction/gen3-1",
  817. spans=[],
  818. project_id=self.create_project(organization=self.organization).id,
  819. parent_span_id=self.gen2_span_ids[1],
  820. milliseconds=1000,
  821. ).event_id,
  822. self.create_event(
  823. trace_id=self.trace_id,
  824. transaction="/transaction/gen3-2",
  825. spans=[],
  826. project_id=self.create_project(organization=self.organization).id,
  827. parent_span_id=self.gen2_span_ids[1],
  828. milliseconds=2000,
  829. ).event_id,
  830. ]
  831. with self.feature(self.FEATURES):
  832. response = self.client_get(
  833. data={"project": -1},
  834. )
  835. assert response.status_code == 200, response.content
  836. # Should be the same as the simple testcase, but skip checking gen2 children
  837. self.assert_trace_data(response.data["transactions"][0], gen2_no_children=False)
  838. gen2_parent = response.data["transactions"][0]["children"][1]["children"][0]
  839. assert len(gen2_parent["children"]) == 2
  840. assert [child["event_id"] for child in gen2_parent["children"]] == gen3_event_siblings
  841. def test_with_orphan_siblings(self):
  842. self.load_trace()
  843. parent_span_id = uuid4().hex[:16]
  844. root_event = self.create_event(
  845. trace_id=self.trace_id,
  846. transaction="/orphan/root",
  847. spans=[],
  848. # Some random id so its separated from the rest of the trace
  849. parent_span_id=parent_span_id,
  850. project_id=self.project.id,
  851. # Shorter duration means that this event happened first, and should be ordered first
  852. milliseconds=1000,
  853. )
  854. root_sibling_event = self.create_event(
  855. trace_id=self.trace_id,
  856. transaction="/orphan/root-sibling",
  857. spans=[],
  858. parent_span_id=parent_span_id,
  859. project_id=self.project.id,
  860. milliseconds=2000,
  861. )
  862. with self.feature(self.FEATURES):
  863. response = self.client_get(
  864. data={"project": -1},
  865. )
  866. assert response.status_code == 200, response.content
  867. assert len(response.data["transactions"]) == 3
  868. # The first item of the response should be the main trace
  869. main, *orphans = response.data["transactions"]
  870. self.assert_trace_data(main)
  871. assert [root_event.event_id, root_sibling_event.event_id] == [
  872. orphan["event_id"] for orphan in orphans
  873. ]
  874. def test_with_orphan_trace(self):
  875. self.load_trace()
  876. orphan_span_ids = {
  877. key: uuid4().hex[:16]
  878. for key in ["root", "root_span", "child", "child_span", "grandchild", "grandchild_span"]
  879. }
  880. # Create the orphan transactions
  881. root_event = self.create_event(
  882. trace_id=self.trace_id,
  883. transaction="/orphan/root",
  884. spans=[
  885. {
  886. "same_process_as_parent": True,
  887. "op": "http",
  888. "description": "GET gen1 orphan",
  889. "span_id": orphan_span_ids["root_span"],
  890. "trace_id": self.trace_id,
  891. }
  892. ],
  893. # Some random id so its separated from the rest of the trace
  894. parent_span_id=uuid4().hex[:16],
  895. span_id=orphan_span_ids["root"],
  896. project_id=self.project.id,
  897. milliseconds=3000,
  898. start_timestamp=self.day_ago - timedelta(minutes=1),
  899. )
  900. child_event = self.create_event(
  901. trace_id=self.trace_id,
  902. transaction="/orphan/child1-0",
  903. spans=[
  904. {
  905. "same_process_as_parent": True,
  906. "op": "http",
  907. "description": "GET gen1 orphan",
  908. "span_id": orphan_span_ids["child_span"],
  909. "trace_id": self.trace_id,
  910. }
  911. ],
  912. parent_span_id=orphan_span_ids["root_span"],
  913. span_id=orphan_span_ids["child"],
  914. project_id=self.gen1_project.id,
  915. # Because the snuba query orders based is_root then timestamp, this causes grandchild1-0 to be added to
  916. # results first before child1-0
  917. milliseconds=2000,
  918. )
  919. grandchild_event = self.create_event(
  920. trace_id=self.trace_id,
  921. transaction="/orphan/grandchild1-0",
  922. spans=[
  923. {
  924. "same_process_as_parent": True,
  925. "op": "http",
  926. "description": "GET gen1 orphan",
  927. "span_id": orphan_span_ids["grandchild_span"],
  928. "trace_id": self.trace_id,
  929. }
  930. ],
  931. parent_span_id=orphan_span_ids["child_span"],
  932. span_id=orphan_span_ids["grandchild"],
  933. project_id=self.gen1_project.id,
  934. milliseconds=1000,
  935. )
  936. with self.feature(self.FEATURES):
  937. response = self.client_get(
  938. data={"project": -1},
  939. )
  940. assert response.status_code == 200, response.content
  941. assert len(response.data["transactions"]) == 2
  942. # The first item of the response should be the main trace
  943. main, orphans = response.data["transactions"]
  944. self.assert_trace_data(main)
  945. self.assert_event(orphans, root_event, "orphan-root")
  946. assert len(orphans["children"]) == 1
  947. if self.check_generation:
  948. assert orphans["generation"] == 0
  949. assert orphans["parent_event_id"] is None
  950. child = orphans["children"][0]
  951. self.assert_event(child, child_event, "orphan-child")
  952. assert len(child["children"]) == 1
  953. if self.check_generation:
  954. assert child["generation"] == 1
  955. assert child["parent_event_id"] == root_event.event_id
  956. grandchild = child["children"][0]
  957. self.assert_event(grandchild, grandchild_event, "orphan-grandchild")
  958. if self.check_generation:
  959. assert grandchild["generation"] == 2
  960. assert grandchild["parent_event_id"] == child_event.event_id
  961. def test_with_errors(self):
  962. self.load_trace()
  963. error, error1, _ = self.load_errors(self.gen1_project, self.gen1_span_ids[0])
  964. with self.feature(self.FEATURES):
  965. response = self.client_get(
  966. data={"project": -1},
  967. )
  968. assert response.status_code == 200, response.content
  969. self.assert_trace_data(response.data["transactions"][0])
  970. gen1_event = response.data["transactions"][0]["children"][0]
  971. assert len(gen1_event["errors"]) == 3
  972. data = {
  973. "event_id": error.event_id,
  974. "issue_id": error.group_id,
  975. "span": self.gen1_span_ids[0],
  976. "project_id": self.gen1_project.id,
  977. "project_slug": self.gen1_project.slug,
  978. "level": "fatal",
  979. "title": error.title,
  980. "timestamp": datetime.fromisoformat(error.timestamp).timestamp(),
  981. "generation": 0,
  982. "event_type": "error",
  983. "message": error.search_message,
  984. }
  985. data1 = {
  986. "event_id": error1.event_id,
  987. "issue_id": error1.group_id,
  988. "span": self.gen1_span_ids[0],
  989. "project_id": self.gen1_project.id,
  990. "project_slug": self.gen1_project.slug,
  991. "level": "warning",
  992. "title": error1.title,
  993. "timestamp": datetime.fromisoformat(error1.timestamp).timestamp(),
  994. "generation": 0,
  995. "event_type": "error",
  996. "message": error1.search_message,
  997. }
  998. assert data in gen1_event["errors"]
  999. assert data1 in gen1_event["errors"]
  1000. def test_with_only_orphan_errors_with_same_span_ids(self):
  1001. span_id = uuid4().hex[:16]
  1002. start, end = self.get_start_end_from_day_ago(10000)
  1003. # Error 1
  1004. error_data = load_data(
  1005. "javascript",
  1006. timestamp=end,
  1007. )
  1008. error_data["contexts"]["trace"] = {
  1009. "type": "trace",
  1010. "trace_id": self.trace_id,
  1011. "span_id": span_id,
  1012. }
  1013. error_data["level"] = "fatal"
  1014. error = self.store_event(error_data, project_id=self.project.id)
  1015. # Error 2 before after Error 1
  1016. error_data1 = load_data(
  1017. "javascript",
  1018. timestamp=start,
  1019. )
  1020. error_data1["level"] = "warning"
  1021. error_data1["contexts"]["trace"] = {
  1022. "type": "trace",
  1023. "trace_id": self.trace_id,
  1024. "span_id": span_id,
  1025. }
  1026. error1 = self.store_event(error_data1, project_id=self.project.id)
  1027. with self.feature(
  1028. [*self.FEATURES, "organizations:performance-tracing-without-performance"]
  1029. ):
  1030. response = self.client_get(
  1031. data={"project": -1},
  1032. )
  1033. assert response.status_code == 200, response.content
  1034. assert len(response.data) == 2
  1035. # Sorting by timestamp puts Error1 after Error2 in the response
  1036. assert {
  1037. "event_id": error.event_id,
  1038. "issue_id": error.group_id,
  1039. "span": span_id,
  1040. "project_id": self.project.id,
  1041. "project_slug": self.project.slug,
  1042. "level": "fatal",
  1043. "title": error.title,
  1044. "timestamp": datetime.fromisoformat(error.timestamp).timestamp(),
  1045. "generation": 0,
  1046. "event_type": "error",
  1047. "message": error.search_message,
  1048. } == response.data["orphan_errors"][1]
  1049. assert {
  1050. "event_id": error1.event_id,
  1051. "issue_id": error1.group_id,
  1052. "span": span_id,
  1053. "project_id": self.project.id,
  1054. "project_slug": self.project.slug,
  1055. "level": "warning",
  1056. "title": error1.title,
  1057. "timestamp": datetime.fromisoformat(error1.timestamp).timestamp(),
  1058. "generation": 0,
  1059. "event_type": "error",
  1060. "message": error1.search_message,
  1061. } == response.data["orphan_errors"][0]
  1062. def test_with_only_orphan_errors_with_different_span_ids(self):
  1063. start, _ = self.get_start_end_from_day_ago(1000)
  1064. span_id = uuid4().hex[:16]
  1065. error_data = load_data(
  1066. "javascript",
  1067. timestamp=start,
  1068. )
  1069. error_data["contexts"]["trace"] = {
  1070. "type": "trace",
  1071. "trace_id": self.trace_id,
  1072. "span_id": span_id,
  1073. }
  1074. error_data["level"] = "fatal"
  1075. error = self.store_event(error_data, project_id=self.project.id)
  1076. error_data["level"] = "warning"
  1077. span_id1 = uuid4().hex[:16]
  1078. error_data["contexts"]["trace"] = {
  1079. "type": "trace",
  1080. "trace_id": self.trace_id,
  1081. "span_id": span_id1,
  1082. }
  1083. error1 = self.store_event(error_data, project_id=self.project.id)
  1084. with self.feature(
  1085. [*self.FEATURES, "organizations:performance-tracing-without-performance"]
  1086. ):
  1087. response = self.client_get(
  1088. data={"project": -1},
  1089. )
  1090. assert response.status_code == 200, response.content
  1091. assert len(response.data["orphan_errors"]) == 2
  1092. assert {
  1093. "event_id": error.event_id,
  1094. "issue_id": error.group_id,
  1095. "span": span_id,
  1096. "project_id": self.project.id,
  1097. "project_slug": self.project.slug,
  1098. "level": "fatal",
  1099. "title": error.title,
  1100. "timestamp": datetime.fromisoformat(error.timestamp).timestamp(),
  1101. "generation": 0,
  1102. "event_type": "error",
  1103. "message": error.search_message,
  1104. } in response.data["orphan_errors"]
  1105. assert {
  1106. "event_id": error1.event_id,
  1107. "issue_id": error1.group_id,
  1108. "span": span_id1,
  1109. "project_id": self.project.id,
  1110. "project_slug": self.project.slug,
  1111. "level": "warning",
  1112. "title": error1.title,
  1113. "timestamp": datetime.fromisoformat(error1.timestamp).timestamp(),
  1114. "generation": 0,
  1115. "event_type": "error",
  1116. "message": error1.search_message,
  1117. } in response.data["orphan_errors"]
  1118. def test_with_mixup_of_orphan_errors_with_simple_trace_data(self):
  1119. self.load_trace()
  1120. start, _ = self.get_start_end_from_day_ago(1000)
  1121. span_id = uuid4().hex[:16]
  1122. error_data = load_data(
  1123. "javascript",
  1124. timestamp=start,
  1125. )
  1126. error_data["contexts"]["trace"] = {
  1127. "type": "trace",
  1128. "trace_id": self.trace_id,
  1129. "span_id": span_id,
  1130. }
  1131. error_data["level"] = "fatal"
  1132. error = self.store_event(error_data, project_id=self.project.id)
  1133. error_data["level"] = "warning"
  1134. span_id1 = uuid4().hex[:16]
  1135. error_data["contexts"]["trace"] = {
  1136. "type": "trace",
  1137. "trace_id": self.trace_id,
  1138. "span_id": span_id1,
  1139. }
  1140. with self.feature(
  1141. [*self.FEATURES, "organizations:performance-tracing-without-performance"]
  1142. ):
  1143. response = self.client_get(
  1144. data={"project": -1},
  1145. )
  1146. assert response.status_code == 200, response.content
  1147. assert len(response.data["transactions"]) == 1
  1148. assert len(response.data["orphan_errors"]) == 1
  1149. self.assert_trace_data(response.data["transactions"][0])
  1150. assert {
  1151. "event_id": error.event_id,
  1152. "issue_id": error.group_id,
  1153. "span": span_id,
  1154. "project_id": self.project.id,
  1155. "project_slug": self.project.slug,
  1156. "level": "fatal",
  1157. "title": error.title,
  1158. "timestamp": datetime.fromisoformat(error.timestamp).timestamp(),
  1159. "generation": 0,
  1160. "event_type": "error",
  1161. "message": error.search_message,
  1162. } in response.data["orphan_errors"]
  1163. def test_with_default(self):
  1164. self.load_trace()
  1165. start, _ = self.get_start_end_from_day_ago(1000)
  1166. default_event = self.load_default()
  1167. with self.feature(self.FEATURES):
  1168. response = self.client_get(
  1169. data={"project": -1},
  1170. )
  1171. assert response.status_code == 200, response.content
  1172. self.assert_trace_data(response.data["transactions"][0])
  1173. root_event = response.data["transactions"][0]
  1174. assert len(root_event["errors"]) == 1
  1175. assert {
  1176. "event_id": default_event.event_id,
  1177. "issue_id": default_event.group_id,
  1178. "span": self.root_span_ids[0],
  1179. "project_id": self.gen1_project.id,
  1180. "project_slug": self.gen1_project.slug,
  1181. "level": "debug",
  1182. "title": "this is a log message",
  1183. "timestamp": datetime.fromisoformat(default_event.timestamp).timestamp(),
  1184. "generation": 0,
  1185. "event_type": "error",
  1186. "message": default_event.search_message,
  1187. } in root_event["errors"]
  1188. def test_pruning_root(self):
  1189. self.load_trace()
  1190. # Pruning shouldn't happen for the root event
  1191. with self.feature(self.FEATURES):
  1192. response = self.client_get(
  1193. data={"project": -1, "event_id": self.root_event.event_id},
  1194. )
  1195. assert response.status_code == 200, response.content
  1196. self.assert_trace_data(response.data["transactions"][0])
  1197. def test_pruning_event(self):
  1198. self.load_trace()
  1199. with self.feature(self.FEATURES):
  1200. response = self.client_get(
  1201. data={"project": -1, "event_id": self.gen2_events[0].event_id},
  1202. )
  1203. assert response.status_code == 200, response.content
  1204. root = response.data["transactions"][0]
  1205. self.assert_event(root, self.root_event, "root")
  1206. # Because of snuba query orders by timestamp we should still have all of the root's children
  1207. assert len(root["children"]) == 3
  1208. for i, gen1 in enumerate(root["children"]):
  1209. self.assert_event(gen1, self.gen1_events[i], f"gen1_{i}")
  1210. if i == 0:
  1211. assert len(gen1["children"]) == 1
  1212. gen2 = gen1["children"][0]
  1213. self.assert_event(gen2, self.gen2_events[0], "gen2_0")
  1214. assert len(gen2["children"]) == 1
  1215. gen3 = gen2["children"][0]
  1216. self.assert_event(gen3, self.gen3_event, "gen3_0")
  1217. else:
  1218. assert len(gen1["children"]) == 0
  1219. @mock.patch("sentry.api.endpoints.organization_events_trace.query_trace_data")
  1220. def test_timestamp_optimization(self, mock_query):
  1221. """When timestamp is passed we'll ignore the statsPeriod and make a query with a smaller start & end"""
  1222. self.load_trace()
  1223. with self.feature(self.FEATURES):
  1224. self.client_get(
  1225. data={
  1226. "project": -1,
  1227. "timestamp": self.root_event.timestamp,
  1228. "statsPeriod": "90d",
  1229. },
  1230. )
  1231. mock_query.assert_called_once()
  1232. params = mock_query.call_args.args[1]
  1233. assert abs((params["end"] - params["start"]).days) <= 7
  1234. def test_timestamp_optimization_without_mock(self):
  1235. """Make sure that even if the params are smaller the query still works"""
  1236. self.load_trace()
  1237. with self.feature(self.FEATURES):
  1238. response = self.client_get(
  1239. data={
  1240. "project": -1,
  1241. "timestamp": self.root_event.timestamp,
  1242. "statsPeriod": "90d",
  1243. },
  1244. )
  1245. assert response.status_code == 200, response.content
  1246. trace_transaction = response.data["transactions"][0]
  1247. self.assert_trace_data(trace_transaction)
  1248. # We shouldn't have detailed fields here
  1249. assert "transaction.status" not in trace_transaction
  1250. assert "tags" not in trace_transaction
  1251. assert "measurements" not in trace_transaction
  1252. class OrganizationEventsTraceEndpointTestUsingSpans(OrganizationEventsTraceEndpointTest):
  1253. check_generation = False
  1254. def client_get(self, data, url=None):
  1255. data["useSpans"] = 1
  1256. return super().client_get(data, url)
  1257. def assert_performance_issues(self, root):
  1258. assert len(root["performance_issues"]) == 2
  1259. # The perf issues are the last 2 spans
  1260. perf_issue_spans = {span["span_id"]: span for span in self.root_event.data["spans"][-2:]}
  1261. for perf_issue in root["performance_issues"]:
  1262. assert len(perf_issue["suspect_spans"]) == 1
  1263. expected = perf_issue_spans[perf_issue["suspect_spans"][0]]
  1264. assert perf_issue["start"] == expected["start_timestamp"]
  1265. assert perf_issue["end"] == expected["timestamp"]
  1266. def test_simple(self):
  1267. self.load_trace()
  1268. with self.feature(self.FEATURES):
  1269. response = self.client_get(
  1270. data={},
  1271. )
  1272. assert response.status_code == 200, response.content
  1273. trace_transaction = response.data["transactions"][0]
  1274. self.assert_trace_data(trace_transaction)
  1275. # We shouldn't have detailed fields here
  1276. assert "transaction.status" not in trace_transaction
  1277. assert "tags" not in trace_transaction
  1278. def test_simple_with_limit(self):
  1279. self.load_trace()
  1280. with self.feature(self.FEATURES):
  1281. response = self.client_get(
  1282. data={"limit": 200},
  1283. )
  1284. assert response.status_code == 200, response.content
  1285. trace_transaction = response.data["transactions"][0]
  1286. self.assert_trace_data(trace_transaction)
  1287. # We shouldn't have detailed fields here
  1288. assert "transaction.status" not in trace_transaction
  1289. assert "tags" not in trace_transaction
  1290. def test_with_error_event(self):
  1291. self.load_trace()
  1292. start, _ = self.get_start_end_from_day_ago(1000)
  1293. error_data = load_data(
  1294. "javascript",
  1295. timestamp=start,
  1296. )
  1297. error_data["contexts"]["trace"] = {
  1298. "type": "trace",
  1299. "trace_id": self.trace_id,
  1300. "span_id": self.gen1_span_ids[0],
  1301. }
  1302. error_data["tags"] = [["transaction", "/transaction/gen1-0"]]
  1303. error = self.store_event(error_data, project_id=self.gen1_project.id)
  1304. with self.feature(self.FEATURES):
  1305. response = self.client_get(
  1306. data={},
  1307. )
  1308. assert response.status_code == 200, response.content
  1309. trace_transaction = response.data["transactions"][0]
  1310. self.assert_trace_data(trace_transaction)
  1311. errors = trace_transaction["children"][0]["errors"]
  1312. assert len(errors) == 1
  1313. error_result = errors[0]
  1314. assert error_result["event_id"] == error.event_id
  1315. assert error_result["span"] == self.gen1_span_ids[0]
  1316. assert error_result["title"] == error.title
  1317. assert error_result["message"] == error.search_message
  1318. @pytest.mark.skip(
  1319. "Loops can only be orphans cause the most recent parent to be saved will overwrite the previous"
  1320. )
  1321. def test_bad_span_loop(self):
  1322. super().test_bad_span_loop()
  1323. @pytest.mark.skip("Can't use the detailed response with useSpans on")
  1324. def test_detailed_trace_with_bad_tags(self):
  1325. super().test_detailed_trace_with_bad_tags()
  1326. @pytest.mark.skip("We shouldn't need to prune with events anymore since spans should be faster")
  1327. def test_pruning_event(self):
  1328. super().test_pruning_event()
  1329. def test_detailed_trace(self):
  1330. """Can't use detailed with useSpans, so this should actually just 400"""
  1331. with self.feature(self.FEATURES):
  1332. response = self.client_get(
  1333. data={"detailed": 1},
  1334. )
  1335. assert response.status_code == 400, response.content
  1336. @mock.patch("sentry.api.endpoints.organization_events_trace.SpansIndexedQueryBuilder")
  1337. def test_indexed_spans_only_query_required_projects(self, mock_query_builder):
  1338. mock_builder = mock.Mock()
  1339. mock_builder.resolve_column_name.return_value = "span_id"
  1340. mock_builder.run_query.return_value = {}
  1341. mock_query_builder.return_value = mock_builder
  1342. # Add a few more projects to the org
  1343. self.create_project(organization=self.organization)
  1344. self.create_project(organization=self.organization)
  1345. self.load_trace()
  1346. with self.feature(self.FEATURES):
  1347. response = self.client_get(
  1348. data={},
  1349. )
  1350. assert sorted(
  1351. [self.project.id, self.gen1_project.id, self.gen2_project.id, self.gen3_project.id]
  1352. ) == sorted(mock_query_builder.mock_calls[0].args[1]["project_id"])
  1353. assert sorted(
  1354. [self.project.id, self.gen1_project.id, self.gen2_project.id, self.gen3_project.id]
  1355. ) == sorted([p.id for p in mock_query_builder.mock_calls[0].args[1]["project_objects"]])
  1356. assert response.status_code == 200, response.content
  1357. def test_event_id(self):
  1358. self.load_trace()
  1359. # When given an event_id even if its not the root transaction we should prioritize loading that specific event
  1360. # over loading roots
  1361. with self.feature(self.FEATURES):
  1362. response = self.client_get(
  1363. data={
  1364. "timestamp": self.root_event.timestamp,
  1365. # Limit of one means the only result is the target event
  1366. "limit": 1,
  1367. "eventId": self.gen1_events[0].event_id,
  1368. },
  1369. )
  1370. assert response.status_code == 200, response.content
  1371. trace_transaction = response.data["transactions"][0]
  1372. self.assert_event(trace_transaction, self.gen1_events[0], "root")
  1373. def test_timestamp_optimization_without_mock(self):
  1374. """Make sure that even if the params are smaller the query still works"""
  1375. self.load_trace()
  1376. with self.feature(self.FEATURES):
  1377. response = self.client_get(
  1378. data={
  1379. "timestamp": self.root_event.timestamp,
  1380. "statsPeriod": "90d",
  1381. },
  1382. )
  1383. assert response.status_code == 200, response.content
  1384. trace_transaction = response.data["transactions"][0]
  1385. self.assert_trace_data(trace_transaction)
  1386. # We shouldn't have detailed fields here
  1387. assert "transaction.status" not in trace_transaction
  1388. assert "tags" not in trace_transaction
  1389. def test_measurements(self):
  1390. self.load_trace()
  1391. with self.feature(self.FEATURES):
  1392. response = self.client_get(
  1393. data={},
  1394. )
  1395. assert response.status_code == 200, response.content
  1396. trace_transaction = response.data["transactions"][0]
  1397. self.assert_trace_data(trace_transaction)
  1398. root = trace_transaction
  1399. assert root["measurements"]["lcp"]["value"] == 1000
  1400. assert root["measurements"]["lcp"]["type"] == "duration"
  1401. assert root["measurements"]["fid"]["value"] == 3.5
  1402. assert root["measurements"]["fid"]["type"] == "duration"
  1403. def test_project_param(self):
  1404. self.load_trace()
  1405. with self.feature(self.FEATURES):
  1406. # If project is included we should still return the entire trace
  1407. response = self.client_get(
  1408. data={"project": self.project.id},
  1409. )
  1410. assert response.status_code == 200, response.content
  1411. trace_transaction = response.data["transactions"][0]
  1412. self.assert_trace_data(trace_transaction)
  1413. # We shouldn't have detailed fields here
  1414. assert "transaction.status" not in trace_transaction
  1415. assert "tags" not in trace_transaction
  1416. def test_split_by_char_optimization(self):
  1417. self.load_trace()
  1418. # This changes the span_id condition so its a split on a string instead of an array
  1419. options.set("performance.traces.span_query_minimum_spans", 1)
  1420. with self.feature(self.FEATURES):
  1421. response = self.client_get(
  1422. data={},
  1423. )
  1424. assert response.status_code == 200, response.content
  1425. trace_transaction = response.data["transactions"][0]
  1426. self.assert_trace_data(trace_transaction)
  1427. # We shouldn't have detailed fields here
  1428. assert "transaction.status" not in trace_transaction
  1429. assert "tags" not in trace_transaction
  1430. class OrganizationEventsTraceMetaEndpointTest(OrganizationEventsTraceEndpointBase):
  1431. url_name = "sentry-api-0-organization-events-trace-meta"
  1432. def test_no_projects(self):
  1433. user = self.create_user()
  1434. org = self.create_organization(owner=user)
  1435. self.login_as(user=user)
  1436. url = reverse(
  1437. self.url_name,
  1438. kwargs={"organization_slug": org.slug, "trace_id": uuid4().hex},
  1439. )
  1440. with self.feature(self.FEATURES):
  1441. response = self.client.get(
  1442. url,
  1443. format="json",
  1444. )
  1445. assert response.status_code == 404, response.content
  1446. def test_bad_ids(self):
  1447. # Fake trace id
  1448. self.url = reverse(
  1449. self.url_name,
  1450. kwargs={"organization_slug": self.project.organization.slug, "trace_id": uuid4().hex},
  1451. )
  1452. with self.feature(self.FEATURES):
  1453. response = self.client.get(
  1454. self.url,
  1455. format="json",
  1456. )
  1457. assert response.status_code == 200, response.content
  1458. data = response.data
  1459. assert data["projects"] == 0
  1460. assert data["transactions"] == 0
  1461. assert data["errors"] == 0
  1462. assert data["performance_issues"] == 0
  1463. # Invalid trace id
  1464. with pytest.raises(NoReverseMatch):
  1465. self.url = reverse(
  1466. self.url_name,
  1467. kwargs={
  1468. "organization_slug": self.project.organization.slug,
  1469. "trace_id": "not-a-trace",
  1470. },
  1471. )
  1472. def test_simple(self):
  1473. self.load_trace()
  1474. with self.feature(self.FEATURES):
  1475. response = self.client.get(
  1476. self.url,
  1477. data={"project": -1},
  1478. format="json",
  1479. )
  1480. assert response.status_code == 200, response.content
  1481. data = response.data
  1482. assert data["projects"] == 4
  1483. assert data["transactions"] == 8
  1484. assert data["errors"] == 0
  1485. assert data["performance_issues"] == 2
  1486. def test_with_errors(self):
  1487. self.load_trace()
  1488. self.load_errors(self.gen1_project, self.gen1_span_ids[0])
  1489. with self.feature(self.FEATURES):
  1490. response = self.client.get(
  1491. self.url,
  1492. data={"project": -1},
  1493. format="json",
  1494. )
  1495. assert response.status_code == 200, response.content
  1496. data = response.data
  1497. assert data["projects"] == 5
  1498. assert data["transactions"] == 8
  1499. assert data["errors"] == 3
  1500. assert data["performance_issues"] == 2
  1501. def test_with_default(self):
  1502. self.load_trace()
  1503. self.load_default()
  1504. with self.feature(self.FEATURES):
  1505. response = self.client.get(
  1506. self.url,
  1507. data={"project": -1},
  1508. format="json",
  1509. )
  1510. assert response.status_code == 200, response.content
  1511. data = response.data
  1512. assert data["projects"] == 4
  1513. assert data["transactions"] == 8
  1514. assert data["errors"] == 1
  1515. assert data["performance_issues"] == 2