test_organization_events_trace.py 62 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663
  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. with self.feature(self.FEATURES):
  449. response = self.client.get(
  450. self.url,
  451. data={"event_id": error.event_id, "project": -1},
  452. format="json",
  453. )
  454. assertions(response)
  455. with self.feature(self.FEATURES):
  456. data: dict[str, str | int] = {"event_id": current_transaction_event, "project": -1}
  457. response = self.client.get(self.url, data=data, format="json")
  458. assertions(response)
  459. def assert_orphan_error_response(self, response, error, span_id):
  460. assert response.status_code == 200, response.content
  461. assert response.data["transactions"] == []
  462. assert len(response.data["orphan_errors"]) == 1
  463. assert {
  464. "event_id": error.event_id,
  465. "issue_id": error.group_id,
  466. "span": span_id,
  467. "project_id": self.project.id,
  468. "project_slug": self.project.slug,
  469. "level": "fatal",
  470. "title": error.title,
  471. "timestamp": datetime.fromisoformat(error.timestamp).timestamp(),
  472. "generation": 0,
  473. "event_type": "error",
  474. } == response.data["orphan_errors"][0]
  475. def test_with_one_orphan_error(self):
  476. self.load_trace()
  477. span_id = uuid4().hex[:16]
  478. start, _ = self.get_start_end_from_day_ago(1000)
  479. error_data = load_data(
  480. "javascript",
  481. timestamp=start,
  482. )
  483. error_data["contexts"]["trace"] = {
  484. "type": "trace",
  485. "trace_id": self.trace_id,
  486. "span_id": span_id,
  487. }
  488. error_data["level"] = "fatal"
  489. error = self.store_event(error_data, project_id=self.project.id)
  490. with self.feature(
  491. [*self.FEATURES, "organizations:performance-tracing-without-performance"]
  492. ):
  493. response = self.client.get(
  494. self.url,
  495. data={"event_id": error.event_id, "project": -1},
  496. format="json",
  497. )
  498. self.assert_orphan_error_response(response, error, span_id)
  499. def test_with_multiple_orphan_errors(self):
  500. self.load_trace()
  501. span_id = uuid4().hex[:16]
  502. start, end = self.get_start_end_from_day_ago(1000)
  503. error_data = load_data(
  504. "javascript",
  505. timestamp=start,
  506. )
  507. error_data["contexts"]["trace"] = {
  508. "type": "trace",
  509. "trace_id": self.trace_id,
  510. "span_id": span_id,
  511. }
  512. error_data["level"] = "fatal"
  513. error = self.store_event(error_data, project_id=self.project.id)
  514. error_data1 = load_data(
  515. "javascript",
  516. timestamp=end,
  517. )
  518. error_data1["contexts"]["trace"] = {
  519. "type": "trace",
  520. "trace_id": self.trace_id,
  521. "span_id": span_id,
  522. }
  523. error_data1["level"] = "warning"
  524. self.store_event(error_data1, project_id=self.project.id)
  525. with self.feature(
  526. [*self.FEATURES, "organizations:performance-tracing-without-performance"]
  527. ):
  528. response = self.client.get(
  529. self.url,
  530. data={"event_id": error.event_id, "project": -1},
  531. format="json",
  532. )
  533. self.assert_orphan_error_response(response, error, span_id)
  534. def test_with_unknown_event(self):
  535. with self.feature(
  536. [*self.FEATURES, "organizations:performance-tracing-without-performance"]
  537. ):
  538. response = self.client.get(
  539. self.url,
  540. data={"event_id": "766758c00ff54d8ab865369ecab53ae6", "project": "-1"},
  541. format="json",
  542. )
  543. assert response.status_code == 404
  544. class OrganizationEventsTraceEndpointTest(OrganizationEventsTraceEndpointBase):
  545. url_name = "sentry-api-0-organization-events-trace"
  546. check_generation = True
  547. def assert_event(self, result, event_data, message):
  548. assert result["transaction"] == event_data.transaction, message
  549. assert result["event_id"] == event_data.event_id
  550. assert result["start_timestamp"] == event_data.data["start_timestamp"]
  551. assert result["profile_id"] == event_data.data["contexts"]["profile"]["profile_id"]
  552. def assert_trace_data(self, root, gen2_no_children=True):
  553. """see the setUp docstring for an idea of what the response structure looks like"""
  554. self.assert_event(root, self.root_event, "root")
  555. assert root["parent_event_id"] is None
  556. assert root["parent_span_id"] is None
  557. if self.check_generation:
  558. assert root["generation"] == 0
  559. assert root["transaction.duration"] == 3000
  560. assert len(root["children"]) == 3
  561. self.assert_performance_issues(root)
  562. for i, gen1 in enumerate(root["children"]):
  563. self.assert_event(gen1, self.gen1_events[i], f"gen1_{i}")
  564. assert gen1["parent_event_id"] == self.root_event.event_id
  565. assert gen1["parent_span_id"] == self.root_span_ids[i]
  566. if self.check_generation:
  567. assert gen1["generation"] == 1
  568. assert gen1["transaction.duration"] == 2000
  569. assert len(gen1["children"]) == 1
  570. gen2 = gen1["children"][0]
  571. self.assert_event(gen2, self.gen2_events[i], f"gen2_{i}")
  572. assert gen2["parent_event_id"] == self.gen1_events[i].event_id
  573. assert gen2["parent_span_id"] == self.gen1_span_ids[i]
  574. if self.check_generation:
  575. assert gen2["generation"] == 2
  576. assert gen2["transaction.duration"] == 1000
  577. # Only the first gen2 descendent has a child
  578. if i == 0:
  579. assert len(gen2["children"]) == 1
  580. gen3 = gen2["children"][0]
  581. self.assert_event(gen3, self.gen3_event, f"gen3_{i}")
  582. assert gen3["parent_event_id"] == self.gen2_events[i].event_id
  583. assert gen3["parent_span_id"] == self.gen2_span_id
  584. if self.check_generation:
  585. assert gen3["generation"] == 3
  586. assert gen3["transaction.duration"] == 500
  587. assert len(gen3["children"]) == 0
  588. elif gen2_no_children:
  589. assert len(gen2["children"]) == 0
  590. def assert_performance_issues(self, root):
  591. """Broken in the non-spans endpoint, but we're not maintaining that anymore"""
  592. pass
  593. def client_get(self, data, url=None):
  594. if url is None:
  595. url = self.url
  596. return self.client.get(
  597. url,
  598. data,
  599. format="json",
  600. )
  601. def test_no_projects(self):
  602. user = self.create_user()
  603. org = self.create_organization(owner=user)
  604. self.login_as(user=user)
  605. url = reverse(
  606. self.url_name,
  607. kwargs={"organization_slug": org.slug, "trace_id": uuid4().hex},
  608. )
  609. with self.feature(self.FEATURES):
  610. response = self.client.get(
  611. url,
  612. format="json",
  613. )
  614. assert response.status_code == 404, response.content
  615. def test_simple(self):
  616. self.load_trace()
  617. with self.feature(self.FEATURES):
  618. response = self.client_get(
  619. data={"project": -1},
  620. )
  621. assert response.status_code == 200, response.content
  622. trace_transaction = response.data["transactions"][0]
  623. self.assert_trace_data(trace_transaction)
  624. # We shouldn't have detailed fields here
  625. assert "transaction.status" not in trace_transaction
  626. assert "tags" not in trace_transaction
  627. assert "measurements" not in trace_transaction
  628. def test_simple_with_limit(self):
  629. self.load_trace()
  630. with self.feature(self.FEATURES):
  631. response = self.client_get(
  632. data={"project": -1, "limit": 200},
  633. )
  634. assert response.status_code == 200, response.content
  635. trace_transaction = response.data["transactions"][0]
  636. self.assert_trace_data(trace_transaction)
  637. # We shouldn't have detailed fields here
  638. assert "transaction.status" not in trace_transaction
  639. assert "tags" not in trace_transaction
  640. assert "measurements" not in trace_transaction
  641. def test_detailed_trace(self):
  642. self.load_trace()
  643. with self.feature(self.FEATURES):
  644. response = self.client_get(
  645. data={"project": -1, "detailed": 1},
  646. )
  647. assert response.status_code == 200, response.content
  648. trace_transaction = response.data["transactions"][0]
  649. self.assert_trace_data(trace_transaction)
  650. root = trace_transaction
  651. assert root["transaction.status"] == "ok"
  652. root_tags = {tag["key"]: tag["value"] for tag in root["tags"]}
  653. for [key, value] in self.root_event.tags:
  654. if not key.startswith("sentry:"):
  655. assert root_tags[key] == value, f"tags - {key}"
  656. else:
  657. assert root_tags[key[7:]] == value, f"tags - {key}"
  658. assert root["measurements"]["lcp"]["value"] == 1000
  659. assert root["measurements"]["fcp"]["value"] == 750
  660. def test_detailed_trace_with_bad_tags(self):
  661. """Basically test that we're actually using the event serializer's method for tags"""
  662. trace = uuid4().hex
  663. self.create_event(
  664. trace_id=trace,
  665. transaction="bad-tags",
  666. parent_span_id=None,
  667. spans=[],
  668. project_id=self.project.id,
  669. tags=[["somethinglong" * 250, "somethinglong" * 250]],
  670. milliseconds=3000,
  671. store_event_kwargs={"assert_no_errors": False},
  672. )
  673. url = reverse(
  674. self.url_name,
  675. kwargs={"organization_slug": self.project.organization.slug, "trace_id": trace},
  676. )
  677. with self.feature(self.FEATURES):
  678. response = self.client_get(
  679. data={"project": -1, "detailed": 1},
  680. url=url,
  681. )
  682. assert response.status_code == 200, response.content
  683. root = response.data["transactions"][0]
  684. assert root["transaction.status"] == "ok"
  685. assert {"key": None, "value": None} in root["tags"]
  686. def test_bad_span_loop(self):
  687. """Maliciously create a loop in the span structure
  688. Structure then becomes something like this:
  689. root
  690. gen1-0...
  691. gen1-1
  692. gen2-1
  693. gen3-1
  694. gen_2-1
  695. gen3-1...
  696. """
  697. self.load_trace()
  698. gen3_loop_event = self.create_event(
  699. trace_id=self.trace_id,
  700. transaction="/transaction/gen3-1/loop",
  701. spans=[
  702. {
  703. "same_process_as_parent": True,
  704. "op": "http",
  705. "description": "GET gen2-1",
  706. "span_id": self.gen1_span_ids[1],
  707. "trace_id": self.trace_id,
  708. }
  709. ],
  710. parent_span_id=self.gen2_span_ids[1],
  711. project_id=self.project.id,
  712. )
  713. with self.feature(self.FEATURES):
  714. response = self.client_get(
  715. data={"project": -1},
  716. )
  717. assert response.status_code == 200, response.content
  718. # Should be the same as the simple testcase
  719. trace_transaction = response.data["transactions"][0]
  720. self.assert_trace_data(trace_transaction, gen2_no_children=False)
  721. # The difference is that gen3-1 should exist with no children
  722. gen2_1 = trace_transaction["children"][1]["children"][0]
  723. assert len(gen2_1["children"]) == 1
  724. gen3_1 = gen2_1["children"][0]
  725. assert gen3_1["event_id"] == gen3_loop_event.event_id
  726. # We didn't even try to start the loop of spans
  727. assert len(gen3_1["children"]) == 0
  728. def test_bad_orphan_span_loop(self):
  729. """Maliciously create a loop in the span structure but for an orphan event"""
  730. root_span_id = uuid4().hex[:16]
  731. root_parent_span = uuid4().hex[:16]
  732. root_event = self.create_event(
  733. trace_id=self.trace_id,
  734. transaction="/orphan/root/",
  735. spans=[
  736. {
  737. "same_process_as_parent": True,
  738. "op": "http",
  739. "description": "GET orphan_child",
  740. "span_id": root_span_id,
  741. "trace_id": self.trace_id,
  742. }
  743. ],
  744. parent_span_id=root_parent_span,
  745. project_id=self.project.id,
  746. milliseconds=3000,
  747. start_timestamp=self.day_ago - timedelta(minutes=1),
  748. )
  749. orphan_child = self.create_event(
  750. trace_id=self.trace_id,
  751. transaction="/orphan/child/",
  752. spans=[
  753. {
  754. "same_process_as_parent": True,
  755. "op": "http",
  756. "description": "GET orphan_root",
  757. "span_id": root_parent_span,
  758. "trace_id": self.trace_id,
  759. }
  760. ],
  761. parent_span_id=root_span_id,
  762. project_id=self.project.id,
  763. milliseconds=300,
  764. )
  765. with self.feature(self.FEATURES):
  766. response = self.client_get(
  767. data={"project": -1},
  768. )
  769. assert response.status_code == 200, response.content
  770. assert len(response.data["transactions"]) == 1
  771. # There really isn't a right answer to which orphan is the "root" since this loops, but the current
  772. # implementation will make the older event the root
  773. root = response.data["transactions"][0]
  774. self.assert_event(root, root_event, "root")
  775. assert len(root["children"]) == 1
  776. child = root["children"][0]
  777. self.assert_event(child, orphan_child, "child")
  778. def test_multiple_roots(self):
  779. trace_id = uuid4().hex
  780. first_root = self.create_event(
  781. trace_id=trace_id,
  782. transaction="/first_root",
  783. spans=[],
  784. parent_span_id=None,
  785. project_id=self.project.id,
  786. milliseconds=500,
  787. )
  788. second_root = self.create_event(
  789. trace_id=trace_id,
  790. transaction="/second_root",
  791. spans=[],
  792. parent_span_id=None,
  793. project_id=self.project.id,
  794. milliseconds=1000,
  795. )
  796. self.url = reverse(
  797. self.url_name,
  798. kwargs={"organization_slug": self.project.organization.slug, "trace_id": trace_id},
  799. )
  800. with self.feature(self.FEATURES):
  801. response = self.client_get(
  802. data={"project": -1},
  803. )
  804. assert response.status_code == 200, response.content
  805. assert len(response.data["transactions"]) == 2
  806. self.assert_event(response.data["transactions"][0], first_root, "first_root")
  807. self.assert_event(response.data["transactions"][1], second_root, "second_root")
  808. def test_sibling_transactions(self):
  809. """More than one transaction can share a parent_span_id"""
  810. self.load_trace()
  811. gen3_event_siblings = [
  812. self.create_event(
  813. trace_id=self.trace_id,
  814. transaction="/transaction/gen3-1",
  815. spans=[],
  816. project_id=self.create_project(organization=self.organization).id,
  817. parent_span_id=self.gen2_span_ids[1],
  818. milliseconds=1000,
  819. ).event_id,
  820. self.create_event(
  821. trace_id=self.trace_id,
  822. transaction="/transaction/gen3-2",
  823. spans=[],
  824. project_id=self.create_project(organization=self.organization).id,
  825. parent_span_id=self.gen2_span_ids[1],
  826. milliseconds=2000,
  827. ).event_id,
  828. ]
  829. with self.feature(self.FEATURES):
  830. response = self.client_get(
  831. data={"project": -1},
  832. )
  833. assert response.status_code == 200, response.content
  834. # Should be the same as the simple testcase, but skip checking gen2 children
  835. self.assert_trace_data(response.data["transactions"][0], gen2_no_children=False)
  836. gen2_parent = response.data["transactions"][0]["children"][1]["children"][0]
  837. assert len(gen2_parent["children"]) == 2
  838. assert [child["event_id"] for child in gen2_parent["children"]] == gen3_event_siblings
  839. def test_with_orphan_siblings(self):
  840. self.load_trace()
  841. parent_span_id = uuid4().hex[:16]
  842. root_event = self.create_event(
  843. trace_id=self.trace_id,
  844. transaction="/orphan/root",
  845. spans=[],
  846. # Some random id so its separated from the rest of the trace
  847. parent_span_id=parent_span_id,
  848. project_id=self.project.id,
  849. # Shorter duration means that this event happened first, and should be ordered first
  850. milliseconds=1000,
  851. )
  852. root_sibling_event = self.create_event(
  853. trace_id=self.trace_id,
  854. transaction="/orphan/root-sibling",
  855. spans=[],
  856. parent_span_id=parent_span_id,
  857. project_id=self.project.id,
  858. milliseconds=2000,
  859. )
  860. with self.feature(self.FEATURES):
  861. response = self.client_get(
  862. data={"project": -1},
  863. )
  864. assert response.status_code == 200, response.content
  865. assert len(response.data["transactions"]) == 3
  866. # The first item of the response should be the main trace
  867. main, *orphans = response.data["transactions"]
  868. self.assert_trace_data(main)
  869. assert [root_event.event_id, root_sibling_event.event_id] == [
  870. orphan["event_id"] for orphan in orphans
  871. ]
  872. def test_with_orphan_trace(self):
  873. self.load_trace()
  874. orphan_span_ids = {
  875. key: uuid4().hex[:16]
  876. for key in ["root", "root_span", "child", "child_span", "grandchild", "grandchild_span"]
  877. }
  878. # Create the orphan transactions
  879. root_event = self.create_event(
  880. trace_id=self.trace_id,
  881. transaction="/orphan/root",
  882. spans=[
  883. {
  884. "same_process_as_parent": True,
  885. "op": "http",
  886. "description": "GET gen1 orphan",
  887. "span_id": orphan_span_ids["root_span"],
  888. "trace_id": self.trace_id,
  889. }
  890. ],
  891. # Some random id so its separated from the rest of the trace
  892. parent_span_id=uuid4().hex[:16],
  893. span_id=orphan_span_ids["root"],
  894. project_id=self.project.id,
  895. milliseconds=3000,
  896. start_timestamp=self.day_ago - timedelta(minutes=1),
  897. )
  898. child_event = self.create_event(
  899. trace_id=self.trace_id,
  900. transaction="/orphan/child1-0",
  901. spans=[
  902. {
  903. "same_process_as_parent": True,
  904. "op": "http",
  905. "description": "GET gen1 orphan",
  906. "span_id": orphan_span_ids["child_span"],
  907. "trace_id": self.trace_id,
  908. }
  909. ],
  910. parent_span_id=orphan_span_ids["root_span"],
  911. span_id=orphan_span_ids["child"],
  912. project_id=self.gen1_project.id,
  913. # Because the snuba query orders based is_root then timestamp, this causes grandchild1-0 to be added to
  914. # results first before child1-0
  915. milliseconds=2000,
  916. )
  917. grandchild_event = self.create_event(
  918. trace_id=self.trace_id,
  919. transaction="/orphan/grandchild1-0",
  920. spans=[
  921. {
  922. "same_process_as_parent": True,
  923. "op": "http",
  924. "description": "GET gen1 orphan",
  925. "span_id": orphan_span_ids["grandchild_span"],
  926. "trace_id": self.trace_id,
  927. }
  928. ],
  929. parent_span_id=orphan_span_ids["child_span"],
  930. span_id=orphan_span_ids["grandchild"],
  931. project_id=self.gen1_project.id,
  932. milliseconds=1000,
  933. )
  934. with self.feature(self.FEATURES):
  935. response = self.client_get(
  936. data={"project": -1},
  937. )
  938. assert response.status_code == 200, response.content
  939. assert len(response.data["transactions"]) == 2
  940. # The first item of the response should be the main trace
  941. main, orphans = response.data["transactions"]
  942. self.assert_trace_data(main)
  943. self.assert_event(orphans, root_event, "orphan-root")
  944. assert len(orphans["children"]) == 1
  945. if self.check_generation:
  946. assert orphans["generation"] == 0
  947. assert orphans["parent_event_id"] is None
  948. child = orphans["children"][0]
  949. self.assert_event(child, child_event, "orphan-child")
  950. assert len(child["children"]) == 1
  951. if self.check_generation:
  952. assert child["generation"] == 1
  953. assert child["parent_event_id"] == root_event.event_id
  954. grandchild = child["children"][0]
  955. self.assert_event(grandchild, grandchild_event, "orphan-grandchild")
  956. if self.check_generation:
  957. assert grandchild["generation"] == 2
  958. assert grandchild["parent_event_id"] == child_event.event_id
  959. def test_with_errors(self):
  960. self.load_trace()
  961. error, error1, _ = self.load_errors(self.gen1_project, self.gen1_span_ids[0])
  962. with self.feature(self.FEATURES):
  963. response = self.client_get(
  964. data={"project": -1},
  965. )
  966. assert response.status_code == 200, response.content
  967. self.assert_trace_data(response.data["transactions"][0])
  968. gen1_event = response.data["transactions"][0]["children"][0]
  969. assert len(gen1_event["errors"]) == 3
  970. data = {
  971. "event_id": error.event_id,
  972. "issue_id": error.group_id,
  973. "span": self.gen1_span_ids[0],
  974. "project_id": self.gen1_project.id,
  975. "project_slug": self.gen1_project.slug,
  976. "level": "fatal",
  977. "title": error.title,
  978. "timestamp": datetime.fromisoformat(error.timestamp).timestamp(),
  979. "generation": 0,
  980. "event_type": "error",
  981. }
  982. data1 = {
  983. "event_id": error1.event_id,
  984. "issue_id": error1.group_id,
  985. "span": self.gen1_span_ids[0],
  986. "project_id": self.gen1_project.id,
  987. "project_slug": self.gen1_project.slug,
  988. "level": "warning",
  989. "title": error1.title,
  990. "timestamp": datetime.fromisoformat(error1.timestamp).timestamp(),
  991. "generation": 0,
  992. "event_type": "error",
  993. }
  994. assert data in gen1_event["errors"]
  995. assert data1 in gen1_event["errors"]
  996. def test_with_only_orphan_errors_with_same_span_ids(self):
  997. span_id = uuid4().hex[:16]
  998. start, end = self.get_start_end_from_day_ago(10000)
  999. # Error 1
  1000. error_data = load_data(
  1001. "javascript",
  1002. timestamp=end,
  1003. )
  1004. error_data["contexts"]["trace"] = {
  1005. "type": "trace",
  1006. "trace_id": self.trace_id,
  1007. "span_id": span_id,
  1008. }
  1009. error_data["level"] = "fatal"
  1010. error = self.store_event(error_data, project_id=self.project.id)
  1011. # Error 2 before after Error 1
  1012. error_data1 = load_data(
  1013. "javascript",
  1014. timestamp=start,
  1015. )
  1016. error_data1["level"] = "warning"
  1017. error_data1["contexts"]["trace"] = {
  1018. "type": "trace",
  1019. "trace_id": self.trace_id,
  1020. "span_id": span_id,
  1021. }
  1022. error1 = self.store_event(error_data1, project_id=self.project.id)
  1023. with self.feature(
  1024. [*self.FEATURES, "organizations:performance-tracing-without-performance"]
  1025. ):
  1026. response = self.client_get(
  1027. data={"project": -1},
  1028. )
  1029. assert response.status_code == 200, response.content
  1030. assert len(response.data) == 2
  1031. # Sorting by timestamp puts Error1 after Error2 in the response
  1032. assert {
  1033. "event_id": error.event_id,
  1034. "issue_id": error.group_id,
  1035. "span": span_id,
  1036. "project_id": self.project.id,
  1037. "project_slug": self.project.slug,
  1038. "level": "fatal",
  1039. "title": error.title,
  1040. "timestamp": datetime.fromisoformat(error.timestamp).timestamp(),
  1041. "generation": 0,
  1042. "event_type": "error",
  1043. } == response.data["orphan_errors"][1]
  1044. assert {
  1045. "event_id": error1.event_id,
  1046. "issue_id": error1.group_id,
  1047. "span": span_id,
  1048. "project_id": self.project.id,
  1049. "project_slug": self.project.slug,
  1050. "level": "warning",
  1051. "title": error1.title,
  1052. "timestamp": datetime.fromisoformat(error1.timestamp).timestamp(),
  1053. "generation": 0,
  1054. "event_type": "error",
  1055. } == response.data["orphan_errors"][0]
  1056. def test_with_only_orphan_errors_with_different_span_ids(self):
  1057. start, _ = self.get_start_end_from_day_ago(1000)
  1058. span_id = uuid4().hex[:16]
  1059. error_data = load_data(
  1060. "javascript",
  1061. timestamp=start,
  1062. )
  1063. error_data["contexts"]["trace"] = {
  1064. "type": "trace",
  1065. "trace_id": self.trace_id,
  1066. "span_id": span_id,
  1067. }
  1068. error_data["level"] = "fatal"
  1069. error = self.store_event(error_data, project_id=self.project.id)
  1070. error_data["level"] = "warning"
  1071. span_id1 = uuid4().hex[:16]
  1072. error_data["contexts"]["trace"] = {
  1073. "type": "trace",
  1074. "trace_id": self.trace_id,
  1075. "span_id": span_id1,
  1076. }
  1077. error1 = self.store_event(error_data, project_id=self.project.id)
  1078. with self.feature(
  1079. [*self.FEATURES, "organizations:performance-tracing-without-performance"]
  1080. ):
  1081. response = self.client_get(
  1082. data={"project": -1},
  1083. )
  1084. assert response.status_code == 200, response.content
  1085. assert len(response.data["orphan_errors"]) == 2
  1086. assert {
  1087. "event_id": error.event_id,
  1088. "issue_id": error.group_id,
  1089. "span": span_id,
  1090. "project_id": self.project.id,
  1091. "project_slug": self.project.slug,
  1092. "level": "fatal",
  1093. "title": error.title,
  1094. "timestamp": datetime.fromisoformat(error.timestamp).timestamp(),
  1095. "generation": 0,
  1096. "event_type": "error",
  1097. } in response.data["orphan_errors"]
  1098. assert {
  1099. "event_id": error1.event_id,
  1100. "issue_id": error1.group_id,
  1101. "span": span_id1,
  1102. "project_id": self.project.id,
  1103. "project_slug": self.project.slug,
  1104. "level": "warning",
  1105. "title": error1.title,
  1106. "timestamp": datetime.fromisoformat(error1.timestamp).timestamp(),
  1107. "generation": 0,
  1108. "event_type": "error",
  1109. } in response.data["orphan_errors"]
  1110. def test_with_mixup_of_orphan_errors_with_simple_trace_data(self):
  1111. self.load_trace()
  1112. start, _ = self.get_start_end_from_day_ago(1000)
  1113. span_id = uuid4().hex[:16]
  1114. error_data = load_data(
  1115. "javascript",
  1116. timestamp=start,
  1117. )
  1118. error_data["contexts"]["trace"] = {
  1119. "type": "trace",
  1120. "trace_id": self.trace_id,
  1121. "span_id": span_id,
  1122. }
  1123. error_data["level"] = "fatal"
  1124. error = self.store_event(error_data, project_id=self.project.id)
  1125. error_data["level"] = "warning"
  1126. span_id1 = uuid4().hex[:16]
  1127. error_data["contexts"]["trace"] = {
  1128. "type": "trace",
  1129. "trace_id": self.trace_id,
  1130. "span_id": span_id1,
  1131. }
  1132. with self.feature(
  1133. [*self.FEATURES, "organizations:performance-tracing-without-performance"]
  1134. ):
  1135. response = self.client_get(
  1136. data={"project": -1},
  1137. )
  1138. assert response.status_code == 200, response.content
  1139. assert len(response.data["transactions"]) == 1
  1140. assert len(response.data["orphan_errors"]) == 1
  1141. self.assert_trace_data(response.data["transactions"][0])
  1142. assert {
  1143. "event_id": error.event_id,
  1144. "issue_id": error.group_id,
  1145. "span": span_id,
  1146. "project_id": self.project.id,
  1147. "project_slug": self.project.slug,
  1148. "level": "fatal",
  1149. "title": error.title,
  1150. "timestamp": datetime.fromisoformat(error.timestamp).timestamp(),
  1151. "generation": 0,
  1152. "event_type": "error",
  1153. } in response.data["orphan_errors"]
  1154. def test_with_default(self):
  1155. self.load_trace()
  1156. start, _ = self.get_start_end_from_day_ago(1000)
  1157. default_event = self.load_default()
  1158. with self.feature(self.FEATURES):
  1159. response = self.client_get(
  1160. data={"project": -1},
  1161. )
  1162. assert response.status_code == 200, response.content
  1163. self.assert_trace_data(response.data["transactions"][0])
  1164. root_event = response.data["transactions"][0]
  1165. assert len(root_event["errors"]) == 1
  1166. assert {
  1167. "event_id": default_event.event_id,
  1168. "issue_id": default_event.group_id,
  1169. "span": self.root_span_ids[0],
  1170. "project_id": self.gen1_project.id,
  1171. "project_slug": self.gen1_project.slug,
  1172. "level": "debug",
  1173. "title": "this is a log message",
  1174. "timestamp": datetime.fromisoformat(default_event.timestamp).timestamp(),
  1175. "generation": 0,
  1176. "event_type": "error",
  1177. } in root_event["errors"]
  1178. def test_pruning_root(self):
  1179. self.load_trace()
  1180. # Pruning shouldn't happen for the root event
  1181. with self.feature(self.FEATURES):
  1182. response = self.client_get(
  1183. data={"project": -1, "event_id": self.root_event.event_id},
  1184. )
  1185. assert response.status_code == 200, response.content
  1186. self.assert_trace_data(response.data["transactions"][0])
  1187. def test_pruning_event(self):
  1188. self.load_trace()
  1189. with self.feature(self.FEATURES):
  1190. response = self.client_get(
  1191. data={"project": -1, "event_id": self.gen2_events[0].event_id},
  1192. )
  1193. assert response.status_code == 200, response.content
  1194. root = response.data["transactions"][0]
  1195. self.assert_event(root, self.root_event, "root")
  1196. # Because of snuba query orders by timestamp we should still have all of the root's children
  1197. assert len(root["children"]) == 3
  1198. for i, gen1 in enumerate(root["children"]):
  1199. self.assert_event(gen1, self.gen1_events[i], f"gen1_{i}")
  1200. if i == 0:
  1201. assert len(gen1["children"]) == 1
  1202. gen2 = gen1["children"][0]
  1203. self.assert_event(gen2, self.gen2_events[0], "gen2_0")
  1204. assert len(gen2["children"]) == 1
  1205. gen3 = gen2["children"][0]
  1206. self.assert_event(gen3, self.gen3_event, "gen3_0")
  1207. else:
  1208. assert len(gen1["children"]) == 0
  1209. @mock.patch("sentry.api.endpoints.organization_events_trace.query_trace_data")
  1210. def test_timestamp_optimization(self, mock_query):
  1211. """When timestamp is passed we'll ignore the statsPeriod and make a query with a smaller start & end"""
  1212. self.load_trace()
  1213. with self.feature(self.FEATURES):
  1214. self.client_get(
  1215. data={
  1216. "project": -1,
  1217. "timestamp": self.root_event.timestamp,
  1218. "statsPeriod": "90d",
  1219. },
  1220. )
  1221. mock_query.assert_called_once()
  1222. params = mock_query.call_args.args[1]
  1223. assert abs((params["end"] - params["start"]).days) <= 7
  1224. def test_timestamp_optimization_without_mock(self):
  1225. """Make sure that even if the params are smaller the query still works"""
  1226. self.load_trace()
  1227. with self.feature(self.FEATURES):
  1228. response = self.client_get(
  1229. data={
  1230. "project": -1,
  1231. "timestamp": self.root_event.timestamp,
  1232. "statsPeriod": "90d",
  1233. },
  1234. )
  1235. assert response.status_code == 200, response.content
  1236. trace_transaction = response.data["transactions"][0]
  1237. self.assert_trace_data(trace_transaction)
  1238. # We shouldn't have detailed fields here
  1239. assert "transaction.status" not in trace_transaction
  1240. assert "tags" not in trace_transaction
  1241. assert "measurements" not in trace_transaction
  1242. class OrganizationEventsTraceEndpointTestUsingSpans(OrganizationEventsTraceEndpointTest):
  1243. check_generation = False
  1244. def client_get(self, data, url=None):
  1245. data["useSpans"] = 1
  1246. return super().client_get(data, url)
  1247. def assert_performance_issues(self, root):
  1248. assert len(root["performance_issues"]) == 2
  1249. # The perf issues are the last 2 spans
  1250. perf_issue_spans = {span["span_id"]: span for span in self.root_event.data["spans"][-2:]}
  1251. for perf_issue in root["performance_issues"]:
  1252. assert len(perf_issue["suspect_spans"]) == 1
  1253. expected = perf_issue_spans[perf_issue["suspect_spans"][0]]
  1254. assert perf_issue["start"] == expected["start_timestamp"]
  1255. assert perf_issue["end"] == expected["timestamp"]
  1256. def test_simple(self):
  1257. self.load_trace()
  1258. with self.feature(self.FEATURES):
  1259. response = self.client_get(
  1260. data={},
  1261. )
  1262. assert response.status_code == 200, response.content
  1263. trace_transaction = response.data["transactions"][0]
  1264. self.assert_trace_data(trace_transaction)
  1265. # We shouldn't have detailed fields here
  1266. assert "transaction.status" not in trace_transaction
  1267. assert "tags" not in trace_transaction
  1268. def test_simple_with_limit(self):
  1269. self.load_trace()
  1270. with self.feature(self.FEATURES):
  1271. response = self.client_get(
  1272. data={"limit": 200},
  1273. )
  1274. assert response.status_code == 200, response.content
  1275. trace_transaction = response.data["transactions"][0]
  1276. self.assert_trace_data(trace_transaction)
  1277. # We shouldn't have detailed fields here
  1278. assert "transaction.status" not in trace_transaction
  1279. assert "tags" not in trace_transaction
  1280. @pytest.mark.skip(
  1281. "Loops can only be orphans cause the most recent parent to be saved will overwrite the previous"
  1282. )
  1283. def test_bad_span_loop(self):
  1284. super().test_bad_span_loop()
  1285. @pytest.mark.skip("Can't use the detailed response with useSpans on")
  1286. def test_detailed_trace_with_bad_tags(self):
  1287. super().test_detailed_trace_with_bad_tags()
  1288. @pytest.mark.skip("We shouldn't need to prune with events anymore since spans should be faster")
  1289. def test_pruning_event(self):
  1290. super().test_pruning_event()
  1291. def test_detailed_trace(self):
  1292. """Can't use detailed with useSpans, so this should actually just 400"""
  1293. with self.feature(self.FEATURES):
  1294. response = self.client_get(
  1295. data={"detailed": 1},
  1296. )
  1297. assert response.status_code == 400, response.content
  1298. @mock.patch("sentry.api.endpoints.organization_events_trace.SpansIndexedQueryBuilder")
  1299. def test_indexed_spans_only_query_required_projects(self, mock_query_builder):
  1300. mock_builder = mock.Mock()
  1301. mock_builder.resolve_column_name.return_value = "span_id"
  1302. mock_builder.run_query.return_value = {}
  1303. mock_query_builder.return_value = mock_builder
  1304. # Add a few more projects to the org
  1305. self.create_project(organization=self.organization)
  1306. self.create_project(organization=self.organization)
  1307. self.load_trace()
  1308. with self.feature(self.FEATURES):
  1309. response = self.client_get(
  1310. data={},
  1311. )
  1312. assert sorted(
  1313. [self.project.id, self.gen1_project.id, self.gen2_project.id, self.gen3_project.id]
  1314. ) == sorted(mock_query_builder.mock_calls[0].args[1]["project_id"])
  1315. assert sorted(
  1316. [self.project.id, self.gen1_project.id, self.gen2_project.id, self.gen3_project.id]
  1317. ) == sorted([p.id for p in mock_query_builder.mock_calls[0].args[1]["project_objects"]])
  1318. assert response.status_code == 200, response.content
  1319. def test_event_id(self):
  1320. self.load_trace()
  1321. # When given an event_id even if its not the root transaction we should prioritize loading that specific event
  1322. # over loading roots
  1323. with self.feature(self.FEATURES):
  1324. response = self.client_get(
  1325. data={
  1326. "timestamp": self.root_event.timestamp,
  1327. # Limit of one means the only result is the target event
  1328. "limit": 1,
  1329. "eventId": self.gen1_events[0].event_id,
  1330. },
  1331. )
  1332. assert response.status_code == 200, response.content
  1333. trace_transaction = response.data["transactions"][0]
  1334. self.assert_event(trace_transaction, self.gen1_events[0], "root")
  1335. def test_timestamp_optimization_without_mock(self):
  1336. """Make sure that even if the params are smaller the query still works"""
  1337. self.load_trace()
  1338. with self.feature(self.FEATURES):
  1339. response = self.client_get(
  1340. data={
  1341. "timestamp": self.root_event.timestamp,
  1342. "statsPeriod": "90d",
  1343. },
  1344. )
  1345. assert response.status_code == 200, response.content
  1346. trace_transaction = response.data["transactions"][0]
  1347. self.assert_trace_data(trace_transaction)
  1348. # We shouldn't have detailed fields here
  1349. assert "transaction.status" not in trace_transaction
  1350. assert "tags" not in trace_transaction
  1351. def test_measurements(self):
  1352. self.load_trace()
  1353. with self.feature(self.FEATURES):
  1354. response = self.client_get(
  1355. data={},
  1356. )
  1357. assert response.status_code == 200, response.content
  1358. trace_transaction = response.data["transactions"][0]
  1359. self.assert_trace_data(trace_transaction)
  1360. root = trace_transaction
  1361. assert root["measurements"]["lcp"]["value"] == 1000
  1362. assert root["measurements"]["lcp"]["type"] == "duration"
  1363. assert root["measurements"]["fid"]["value"] == 3.5
  1364. assert root["measurements"]["fid"]["type"] == "duration"
  1365. def test_project_param(self):
  1366. self.load_trace()
  1367. with self.feature(self.FEATURES):
  1368. # If project is included we should still return the entire trace
  1369. response = self.client_get(
  1370. data={"project": self.project.id},
  1371. )
  1372. assert response.status_code == 200, response.content
  1373. trace_transaction = response.data["transactions"][0]
  1374. self.assert_trace_data(trace_transaction)
  1375. # We shouldn't have detailed fields here
  1376. assert "transaction.status" not in trace_transaction
  1377. assert "tags" not in trace_transaction
  1378. def test_split_by_char_optimization(self):
  1379. self.load_trace()
  1380. # This changes the span_id condition so its a split on a string instead of an array
  1381. options.set("performance.traces.span_query_minimum_spans", 1)
  1382. with self.feature(self.FEATURES):
  1383. response = self.client_get(
  1384. data={},
  1385. )
  1386. assert response.status_code == 200, response.content
  1387. trace_transaction = response.data["transactions"][0]
  1388. self.assert_trace_data(trace_transaction)
  1389. # We shouldn't have detailed fields here
  1390. assert "transaction.status" not in trace_transaction
  1391. assert "tags" not in trace_transaction
  1392. class OrganizationEventsTraceMetaEndpointTest(OrganizationEventsTraceEndpointBase):
  1393. url_name = "sentry-api-0-organization-events-trace-meta"
  1394. def test_no_projects(self):
  1395. user = self.create_user()
  1396. org = self.create_organization(owner=user)
  1397. self.login_as(user=user)
  1398. url = reverse(
  1399. self.url_name,
  1400. kwargs={"organization_slug": org.slug, "trace_id": uuid4().hex},
  1401. )
  1402. with self.feature(self.FEATURES):
  1403. response = self.client.get(
  1404. url,
  1405. format="json",
  1406. )
  1407. assert response.status_code == 404, response.content
  1408. def test_bad_ids(self):
  1409. # Fake trace id
  1410. self.url = reverse(
  1411. self.url_name,
  1412. kwargs={"organization_slug": self.project.organization.slug, "trace_id": uuid4().hex},
  1413. )
  1414. with self.feature(self.FEATURES):
  1415. response = self.client.get(
  1416. self.url,
  1417. format="json",
  1418. )
  1419. assert response.status_code == 200, response.content
  1420. data = response.data
  1421. assert data["projects"] == 0
  1422. assert data["transactions"] == 0
  1423. assert data["errors"] == 0
  1424. assert data["performance_issues"] == 0
  1425. # Invalid trace id
  1426. with pytest.raises(NoReverseMatch):
  1427. self.url = reverse(
  1428. self.url_name,
  1429. kwargs={
  1430. "organization_slug": self.project.organization.slug,
  1431. "trace_id": "not-a-trace",
  1432. },
  1433. )
  1434. def test_simple(self):
  1435. self.load_trace()
  1436. with self.feature(self.FEATURES):
  1437. response = self.client.get(
  1438. self.url,
  1439. data={"project": -1},
  1440. format="json",
  1441. )
  1442. assert response.status_code == 200, response.content
  1443. data = response.data
  1444. assert data["projects"] == 4
  1445. assert data["transactions"] == 8
  1446. assert data["errors"] == 0
  1447. assert data["performance_issues"] == 2
  1448. def test_with_errors(self):
  1449. self.load_trace()
  1450. self.load_errors(self.gen1_project, self.gen1_span_ids[0])
  1451. with self.feature(self.FEATURES):
  1452. response = self.client.get(
  1453. self.url,
  1454. data={"project": -1},
  1455. format="json",
  1456. )
  1457. assert response.status_code == 200, response.content
  1458. data = response.data
  1459. assert data["projects"] == 5
  1460. assert data["transactions"] == 8
  1461. assert data["errors"] == 3
  1462. assert data["performance_issues"] == 2
  1463. def test_with_default(self):
  1464. self.load_trace()
  1465. self.load_default()
  1466. with self.feature(self.FEATURES):
  1467. response = self.client.get(
  1468. self.url,
  1469. data={"project": -1},
  1470. format="json",
  1471. )
  1472. assert response.status_code == 200, response.content
  1473. data = response.data
  1474. assert data["projects"] == 4
  1475. assert data["transactions"] == 8
  1476. assert data["errors"] == 1
  1477. assert data["performance_issues"] == 2