test_organization_events_trace.py 43 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243
  1. from datetime import timedelta
  2. from uuid import uuid4
  3. import pytest
  4. from django.urls import NoReverseMatch, reverse
  5. from sentry.testutils import APITestCase, SnubaTestCase
  6. from sentry.testutils.helpers.datetime import before_now, iso_format
  7. from sentry.testutils.silo import region_silo_test
  8. from sentry.utils.samples import load_data
  9. class OrganizationEventsTraceEndpointBase(APITestCase, SnubaTestCase):
  10. FEATURES = ["organizations:performance-view"]
  11. def get_start_end(self, duration):
  12. return self.day_ago, self.day_ago + timedelta(milliseconds=duration)
  13. def create_event(
  14. self,
  15. trace,
  16. transaction,
  17. spans,
  18. parent_span_id,
  19. project_id,
  20. tags=None,
  21. duration=4000,
  22. span_id=None,
  23. measurements=None,
  24. **kwargs,
  25. ):
  26. start, end = self.get_start_end(duration)
  27. data = load_data(
  28. "transaction",
  29. trace=trace,
  30. spans=spans,
  31. timestamp=end,
  32. start_timestamp=start,
  33. )
  34. data["transaction"] = transaction
  35. data["contexts"]["trace"]["parent_span_id"] = parent_span_id
  36. if span_id:
  37. data["contexts"]["trace"]["span_id"] = span_id
  38. if measurements:
  39. for key, value in measurements.items():
  40. data["measurements"][key]["value"] = value
  41. if tags is not None:
  42. data["tags"] = tags
  43. return self.store_event(data, project_id=project_id, **kwargs)
  44. def setUp(self):
  45. """
  46. Span structure:
  47. root
  48. gen1-0
  49. gen2-0
  50. gen3-0
  51. gen1-1
  52. gen2-1
  53. gen1-2
  54. gen2-2
  55. """
  56. super().setUp()
  57. self.login_as(user=self.user)
  58. self.day_ago = before_now(days=1).replace(hour=10, minute=0, second=0, microsecond=0)
  59. self.root_span_ids = [uuid4().hex[:16] for _ in range(3)]
  60. self.trace_id = uuid4().hex
  61. self.url = reverse(
  62. self.url_name,
  63. kwargs={"organization_slug": self.project.organization.slug, "trace_id": self.trace_id},
  64. )
  65. def load_trace(self):
  66. self.root_event = self.create_event(
  67. trace=self.trace_id,
  68. transaction="root",
  69. spans=[
  70. {
  71. "same_process_as_parent": True,
  72. "op": "http",
  73. "description": f"GET gen1-{i}",
  74. "span_id": root_span_id,
  75. "trace_id": self.trace_id,
  76. }
  77. for i, root_span_id in enumerate(self.root_span_ids)
  78. ],
  79. measurements={
  80. "lcp": 1000,
  81. "fcp": 750,
  82. },
  83. parent_span_id=None,
  84. project_id=self.project.id,
  85. duration=3000,
  86. )
  87. # First Generation
  88. self.gen1_span_ids = [uuid4().hex[:16] for _ in range(3)]
  89. self.gen1_project = self.create_project(organization=self.organization)
  90. self.gen1_events = [
  91. self.create_event(
  92. trace=self.trace_id,
  93. transaction=f"/transaction/gen1-{i}",
  94. spans=[
  95. {
  96. "same_process_as_parent": True,
  97. "op": "http",
  98. "description": f"GET gen2-{i}",
  99. "span_id": gen1_span_id,
  100. "trace_id": self.trace_id,
  101. }
  102. ],
  103. parent_span_id=root_span_id,
  104. project_id=self.gen1_project.id,
  105. duration=2000,
  106. )
  107. for i, (root_span_id, gen1_span_id) in enumerate(
  108. zip(self.root_span_ids, self.gen1_span_ids)
  109. )
  110. ]
  111. # Second Generation
  112. self.gen2_span_ids = [uuid4().hex[:16] for _ in range(3)]
  113. self.gen2_project = self.create_project(organization=self.organization)
  114. # Intentially pick a span id that starts with 0s
  115. self.gen2_span_id = "0011" * 4
  116. self.gen2_events = [
  117. self.create_event(
  118. trace=self.trace_id,
  119. transaction=f"/transaction/gen2-{i}",
  120. spans=[
  121. {
  122. "same_process_as_parent": True,
  123. "op": "http",
  124. "description": f"GET gen3-{i}" if i == 0 else f"SPAN gen3-{i}",
  125. "span_id": gen2_span_id,
  126. "trace_id": self.trace_id,
  127. }
  128. ],
  129. parent_span_id=gen1_span_id,
  130. span_id=self.gen2_span_id if i == 0 else None,
  131. project_id=self.gen2_project.id,
  132. duration=1000,
  133. )
  134. for i, (gen1_span_id, gen2_span_id) in enumerate(
  135. zip(self.gen1_span_ids, self.gen2_span_ids)
  136. )
  137. ]
  138. # Third generation
  139. self.gen3_project = self.create_project(organization=self.organization)
  140. self.gen3_event = self.create_event(
  141. trace=self.trace_id,
  142. transaction="/transaction/gen3-0",
  143. spans=[],
  144. project_id=self.gen3_project.id,
  145. parent_span_id=self.gen2_span_id,
  146. duration=500,
  147. )
  148. def load_errors(self):
  149. start, _ = self.get_start_end(1000)
  150. error_data = load_data(
  151. "javascript",
  152. timestamp=start,
  153. )
  154. error_data["contexts"]["trace"] = {
  155. "type": "trace",
  156. "trace_id": self.trace_id,
  157. "span_id": self.gen1_span_ids[0],
  158. }
  159. error_data["level"] = "fatal"
  160. error = self.store_event(error_data, project_id=self.gen1_project.id)
  161. error_data["level"] = "warning"
  162. error1 = self.store_event(error_data, project_id=self.gen1_project.id)
  163. return error, error1
  164. def load_default(self):
  165. start, _ = self.get_start_end(1000)
  166. return self.store_event(
  167. {
  168. "timestamp": iso_format(start),
  169. "contexts": {
  170. "trace": {
  171. "type": "trace",
  172. "trace_id": self.trace_id,
  173. "span_id": self.root_span_ids[0],
  174. },
  175. },
  176. "level": "debug",
  177. "message": "this is a log message",
  178. },
  179. project_id=self.gen1_project.id,
  180. )
  181. @region_silo_test
  182. class OrganizationEventsTraceLightEndpointTest(OrganizationEventsTraceEndpointBase):
  183. url_name = "sentry-api-0-organization-events-trace-light"
  184. def test_no_projects(self):
  185. user = self.create_user()
  186. org = self.create_organization(owner=user)
  187. self.login_as(user=user)
  188. url = reverse(
  189. self.url_name,
  190. kwargs={"organization_slug": org.slug, "trace_id": uuid4().hex},
  191. )
  192. with self.feature(self.FEATURES):
  193. response = self.client.get(
  194. url,
  195. format="json",
  196. )
  197. assert response.status_code == 404, response.content
  198. def test_bad_ids(self):
  199. # Fake event id
  200. with self.feature(self.FEATURES):
  201. response = self.client.get(
  202. self.url,
  203. data={"event_id": uuid4().hex},
  204. format="json",
  205. )
  206. assert response.status_code == 404, response.content
  207. # Invalid event id
  208. with self.feature(self.FEATURES):
  209. response = self.client.get(
  210. self.url,
  211. data={"event_id": "not-a-event"},
  212. format="json",
  213. )
  214. assert response.status_code == 400, response.content
  215. # Fake trace id
  216. self.url = reverse(
  217. "sentry-api-0-organization-events-trace-light",
  218. kwargs={"organization_slug": self.project.organization.slug, "trace_id": uuid4().hex},
  219. )
  220. with self.feature(self.FEATURES):
  221. response = self.client.get(
  222. self.url,
  223. data={"event_id": "a" * 32},
  224. format="json",
  225. )
  226. assert response.status_code == 404, response.content
  227. # Invalid trace id
  228. with pytest.raises(NoReverseMatch):
  229. self.url = reverse(
  230. "sentry-api-0-organization-events-trace-light",
  231. kwargs={
  232. "organization_slug": self.project.organization.slug,
  233. "trace_id": "not-a-trace",
  234. },
  235. )
  236. def test_no_roots(self):
  237. """Even when there's no root, we return the current event"""
  238. self.load_trace()
  239. no_root_trace = uuid4().hex
  240. parent_span_id = uuid4().hex[:16]
  241. no_root_event = self.create_event(
  242. trace=no_root_trace,
  243. transaction="/not_root/but_only_transaction",
  244. spans=[],
  245. parent_span_id=parent_span_id,
  246. project_id=self.project.id,
  247. )
  248. url = reverse(
  249. "sentry-api-0-organization-events-trace-light",
  250. kwargs={"organization_slug": self.project.organization.slug, "trace_id": no_root_trace},
  251. )
  252. with self.feature(self.FEATURES):
  253. response = self.client.get(
  254. url,
  255. data={"event_id": no_root_event.event_id},
  256. format="json",
  257. )
  258. assert response.status_code == 200, response.content
  259. assert len(response.data) == 1
  260. event = response.data[0]
  261. # Basically know nothing about this event
  262. assert event["generation"] is None
  263. assert event["parent_event_id"] is None
  264. assert event["parent_span_id"] == parent_span_id
  265. assert event["event_id"] == no_root_event.event_id
  266. def test_multiple_roots(self):
  267. self.load_trace()
  268. second_root = self.create_event(
  269. trace=self.trace_id,
  270. transaction="/second_root",
  271. spans=[],
  272. parent_span_id=None,
  273. project_id=self.project.id,
  274. )
  275. with self.feature(self.FEATURES):
  276. response = self.client.get(
  277. self.url,
  278. data={"event_id": second_root.event_id, "project": -1},
  279. format="json",
  280. )
  281. assert response.status_code == 200, response.content
  282. assert len(response.data) == 1
  283. event = response.data[0]
  284. assert event["generation"] == 0
  285. assert event["parent_event_id"] is None
  286. assert event["parent_span_id"] is None
  287. def test_root_event(self):
  288. self.load_trace()
  289. root_event_id = self.root_event.event_id
  290. with self.feature(self.FEATURES):
  291. response = self.client.get(
  292. self.url,
  293. data={"event_id": root_event_id, "project": -1},
  294. format="json",
  295. )
  296. assert response.status_code == 200, response.content
  297. assert len(response.data) == 4
  298. events = {item["event_id"]: item for item in response.data}
  299. assert root_event_id in events
  300. event = events[root_event_id]
  301. assert event["generation"] == 0
  302. assert event["parent_event_id"] is None
  303. assert event["parent_span_id"] is None
  304. for i, child_event in enumerate(self.gen1_events):
  305. child_event_id = child_event.event_id
  306. assert child_event_id in events
  307. event = events[child_event_id]
  308. assert event["generation"] == 1
  309. assert event["parent_event_id"] == root_event_id
  310. assert event["parent_span_id"] == self.root_span_ids[i]
  311. def test_root_with_multiple_roots(self):
  312. self.load_trace()
  313. root_event_id = self.root_event.event_id
  314. self.create_event(
  315. trace=self.trace_id,
  316. transaction="/second_root",
  317. spans=[],
  318. parent_span_id=None,
  319. project_id=self.project.id,
  320. )
  321. with self.feature(self.FEATURES):
  322. response = self.client.get(
  323. self.url,
  324. data={"event_id": self.root_event.event_id},
  325. format="json",
  326. )
  327. assert response.status_code == 200, response.content
  328. assert len(response.data) == 4
  329. events = {item["event_id"]: item for item in response.data}
  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. for i, child_event in enumerate(self.gen1_events):
  336. child_event_id = child_event.event_id
  337. assert child_event_id in events
  338. event = events[child_event_id]
  339. assert event["generation"] == 1
  340. assert event["parent_event_id"] == root_event_id
  341. assert event["parent_span_id"] == self.root_span_ids[i]
  342. def test_direct_parent_with_children(self):
  343. self.load_trace()
  344. root_event_id = self.root_event.event_id
  345. current_event = self.gen1_events[0].event_id
  346. child_event_id = self.gen2_events[0].event_id
  347. with self.feature(self.FEATURES):
  348. response = self.client.get(
  349. self.url,
  350. data={"event_id": current_event, "project": -1},
  351. format="json",
  352. )
  353. assert response.status_code == 200, response.content
  354. assert len(response.data) == 3
  355. events = {item["event_id"]: item for item in response.data}
  356. assert root_event_id in events
  357. event = events[root_event_id]
  358. assert event["generation"] == 0
  359. assert event["parent_event_id"] is None
  360. assert event["parent_span_id"] is None
  361. assert current_event in events
  362. event = events[current_event]
  363. assert event["generation"] == 1
  364. assert event["parent_event_id"] == root_event_id
  365. assert event["parent_span_id"] == self.root_span_ids[0]
  366. assert child_event_id in events
  367. event = events[child_event_id]
  368. assert event["generation"] == 2
  369. assert event["parent_event_id"] == current_event
  370. assert event["parent_span_id"] == self.gen1_span_ids[0]
  371. def test_direct_parent_with_children_and_multiple_root(self):
  372. self.load_trace()
  373. root_event_id = self.root_event.event_id
  374. current_event = self.gen1_events[0].event_id
  375. child_event_id = self.gen2_events[0].event_id
  376. self.create_event(
  377. trace=self.trace_id,
  378. transaction="/second_root",
  379. spans=[],
  380. parent_span_id=None,
  381. project_id=self.project.id,
  382. )
  383. with self.feature(self.FEATURES):
  384. response = self.client.get(
  385. self.url,
  386. data={"event_id": current_event, "project": -1},
  387. format="json",
  388. )
  389. assert response.status_code == 200, response.content
  390. assert len(response.data) == 3
  391. events = {item["event_id"]: item for item in response.data}
  392. assert root_event_id in events
  393. event = events[root_event_id]
  394. assert event["generation"] == 0
  395. assert event["parent_event_id"] is None
  396. assert event["parent_span_id"] is None
  397. assert current_event in events
  398. event = events[current_event]
  399. assert event["generation"] == 1
  400. assert event["parent_event_id"] == root_event_id
  401. assert event["parent_span_id"] == self.root_span_ids[0]
  402. assert child_event_id in events
  403. event = events[child_event_id]
  404. assert event["generation"] == 2
  405. assert event["parent_event_id"] == current_event
  406. assert event["parent_span_id"] == self.gen1_span_ids[0]
  407. def test_second_generation_with_children(self):
  408. self.load_trace()
  409. current_event = self.gen2_events[0].event_id
  410. child_event_id = self.gen3_event.event_id
  411. with self.feature(self.FEATURES):
  412. response = self.client.get(
  413. self.url,
  414. data={"event_id": current_event, "project": -1},
  415. format="json",
  416. )
  417. assert response.status_code == 200, response.content
  418. assert len(response.data) == 2
  419. events = {item["event_id"]: item for item in response.data}
  420. assert current_event in events
  421. event = events[current_event]
  422. # Parent/generation is unknown in this case
  423. assert event["generation"] is None
  424. assert event["parent_event_id"] is None
  425. # But we still know the parent_span
  426. assert event["parent_span_id"] == self.gen1_span_ids[0]
  427. assert child_event_id in events
  428. event = events[child_event_id]
  429. assert event["generation"] is None
  430. assert event["parent_event_id"] == current_event
  431. assert event["parent_span_id"] == self.gen2_span_id
  432. def test_third_generation_no_children(self):
  433. self.load_trace()
  434. current_event = self.gen3_event.event_id
  435. with self.feature(self.FEATURES):
  436. response = self.client.get(
  437. self.url,
  438. data={"event_id": current_event, "project": -1},
  439. format="json",
  440. )
  441. assert response.status_code == 200, response.content
  442. assert len(response.data) == 1
  443. event = response.data[0]
  444. assert event["generation"] is None
  445. # Parent is unknown in this case
  446. assert event["parent_event_id"] is None
  447. # But we still know the parent_span
  448. assert event["parent_span_id"] == self.gen2_span_id
  449. def test_sibling_transactions(self):
  450. """More than one transaction can share a parent_span_id"""
  451. self.load_trace()
  452. gen3_event_siblings = [
  453. self.create_event(
  454. trace=self.trace_id,
  455. transaction="/transaction/gen3-1",
  456. spans=[],
  457. project_id=self.create_project(organization=self.organization).id,
  458. parent_span_id=self.gen2_span_ids[1],
  459. duration=500,
  460. ).event_id,
  461. self.create_event(
  462. trace=self.trace_id,
  463. transaction="/transaction/gen3-2",
  464. spans=[],
  465. project_id=self.create_project(organization=self.organization).id,
  466. parent_span_id=self.gen2_span_ids[1],
  467. duration=525,
  468. ).event_id,
  469. ]
  470. current_event = self.gen2_events[1].event_id
  471. with self.feature(self.FEATURES):
  472. response = self.client.get(
  473. self.url,
  474. data={"event_id": current_event, "project": -1},
  475. format="json",
  476. )
  477. assert len(response.data) == 3
  478. events = {item["event_id"]: item for item in response.data}
  479. for child_event_id in gen3_event_siblings:
  480. assert child_event_id in events
  481. event = events[child_event_id]
  482. assert event["generation"] is None
  483. assert event["parent_event_id"] == current_event
  484. assert event["parent_span_id"] == self.gen2_span_ids[1]
  485. def test_with_error_event(self):
  486. self.load_trace()
  487. root_event_id = self.root_event.event_id
  488. current_transaction_event = self.gen1_events[0].event_id
  489. start, _ = self.get_start_end(1000)
  490. error_data = load_data(
  491. "javascript",
  492. timestamp=start,
  493. )
  494. error_data["contexts"]["trace"] = {
  495. "type": "trace",
  496. "trace_id": self.trace_id,
  497. "span_id": self.gen1_span_ids[0],
  498. }
  499. error_data["tags"] = [["transaction", "/transaction/gen1-0"]]
  500. error = self.store_event(error_data, project_id=self.gen1_project.id)
  501. def assertions(response):
  502. assert response.status_code == 200, response.content
  503. assert len(response.data) == 3
  504. events = {item["event_id"]: item for item in response.data}
  505. assert root_event_id in events
  506. event = events[root_event_id]
  507. assert event["generation"] == 0
  508. assert event["parent_event_id"] is None
  509. assert event["parent_span_id"] is None
  510. assert len(event["errors"]) == 0
  511. assert current_transaction_event in events
  512. event = events[current_transaction_event]
  513. assert event["generation"] == 1
  514. assert event["parent_event_id"] == root_event_id
  515. assert event["parent_span_id"] == self.root_span_ids[0]
  516. assert len(event["errors"]) == 1
  517. assert event["errors"][0]["event_id"] == error.event_id
  518. assert event["errors"][0]["issue_id"] == error.group_id
  519. with self.feature(self.FEATURES):
  520. response = self.client.get(
  521. self.url,
  522. data={"event_id": error.event_id, "project": -1},
  523. format="json",
  524. )
  525. assertions(response)
  526. with self.feature(self.FEATURES):
  527. response = self.client.get(
  528. self.url,
  529. data={"event_id": current_transaction_event, "project": -1},
  530. format="json",
  531. )
  532. assertions(response)
  533. @region_silo_test
  534. class OrganizationEventsTraceEndpointTest(OrganizationEventsTraceEndpointBase):
  535. url_name = "sentry-api-0-organization-events-trace"
  536. def assert_event(self, result, event_data, message):
  537. assert result["event_id"] == event_data.event_id, message
  538. assert result["timestamp"] == event_data.data["timestamp"], message
  539. assert result["start_timestamp"] == event_data.data["start_timestamp"], message
  540. def assert_trace_data(self, root, gen2_no_children=True):
  541. """see the setUp docstring for an idea of what the response structure looks like"""
  542. self.assert_event(root, self.root_event, "root")
  543. assert root["parent_event_id"] is None
  544. assert root["parent_span_id"] is None
  545. assert root["generation"] == 0
  546. assert root["transaction.duration"] == 3000
  547. assert len(root["children"]) == 3
  548. for i, gen1 in enumerate(root["children"]):
  549. self.assert_event(gen1, self.gen1_events[i], f"gen1_{i}")
  550. assert gen1["parent_event_id"] == self.root_event.event_id
  551. assert gen1["parent_span_id"] == self.root_span_ids[i]
  552. assert gen1["generation"] == 1
  553. assert gen1["transaction.duration"] == 2000
  554. assert len(gen1["children"]) == 1
  555. gen2 = gen1["children"][0]
  556. self.assert_event(gen2, self.gen2_events[i], f"gen2_{i}")
  557. assert gen2["parent_event_id"] == self.gen1_events[i].event_id
  558. assert gen2["parent_span_id"] == self.gen1_span_ids[i]
  559. assert gen2["generation"] == 2
  560. assert gen2["transaction.duration"] == 1000
  561. # Only the first gen2 descendent has a child
  562. if i == 0:
  563. assert len(gen2["children"]) == 1
  564. gen3 = gen2["children"][0]
  565. self.assert_event(gen3, self.gen3_event, f"gen3_{i}")
  566. assert gen3["parent_event_id"] == self.gen2_events[i].event_id
  567. assert gen3["parent_span_id"] == self.gen2_span_id
  568. assert gen3["generation"] == 3
  569. assert gen3["transaction.duration"] == 500
  570. assert len(gen3["children"]) == 0
  571. elif gen2_no_children:
  572. assert len(gen2["children"]) == 0
  573. def test_no_projects(self):
  574. user = self.create_user()
  575. org = self.create_organization(owner=user)
  576. self.login_as(user=user)
  577. url = reverse(
  578. self.url_name,
  579. kwargs={"organization_slug": org.slug, "trace_id": uuid4().hex},
  580. )
  581. with self.feature(self.FEATURES):
  582. response = self.client.get(
  583. url,
  584. format="json",
  585. )
  586. assert response.status_code == 404, response.content
  587. def test_simple(self):
  588. self.load_trace()
  589. with self.feature(self.FEATURES):
  590. response = self.client.get(
  591. self.url,
  592. data={"project": -1},
  593. format="json",
  594. )
  595. assert response.status_code == 200, response.content
  596. self.assert_trace_data(response.data[0])
  597. # We shouldn't have detailed fields here
  598. assert "transaction.status" not in response.data[0]
  599. assert "tags" not in response.data[0]
  600. assert "measurements" not in response.data[0]
  601. def test_detailed_trace(self):
  602. self.load_trace()
  603. with self.feature(self.FEATURES):
  604. response = self.client.get(
  605. self.url,
  606. data={"project": -1, "detailed": 1},
  607. format="json",
  608. )
  609. assert response.status_code == 200, response.content
  610. self.assert_trace_data(response.data[0])
  611. root = response.data[0]
  612. assert root["transaction.status"] == "ok"
  613. root_tags = {tag["key"]: tag["value"] for tag in root["tags"]}
  614. for [key, value] in self.root_event.tags:
  615. if not key.startswith("sentry:"):
  616. assert root_tags[key] == value, f"tags - {key}"
  617. else:
  618. assert root_tags[key[7:]] == value, f"tags - {key}"
  619. assert root["measurements"]["lcp"]["value"] == 1000
  620. assert root["measurements"]["fcp"]["value"] == 750
  621. def test_detailed_trace_with_bad_tags(self):
  622. """Basically test that we're actually using the event serializer's method for tags"""
  623. trace = uuid4().hex
  624. self.create_event(
  625. trace=trace,
  626. transaction="bad-tags",
  627. parent_span_id=None,
  628. spans=[],
  629. project_id=self.project.id,
  630. tags=[["somethinglong" * 250, "somethinglong" * 250]],
  631. duration=3000,
  632. assert_no_errors=False,
  633. )
  634. url = reverse(
  635. self.url_name,
  636. kwargs={"organization_slug": self.project.organization.slug, "trace_id": trace},
  637. )
  638. with self.feature(self.FEATURES):
  639. response = self.client.get(
  640. url,
  641. data={"project": -1, "detailed": 1},
  642. format="json",
  643. )
  644. assert response.status_code == 200, response.content
  645. root = response.data[0]
  646. assert root["transaction.status"] == "ok"
  647. assert {"key": None, "value": None} in root["tags"]
  648. def test_bad_span_loop(self):
  649. """Maliciously create a loop in the span structure
  650. Structure then becomes something like this:
  651. root
  652. gen1-0...
  653. gen1-1
  654. gen2-1
  655. gen3-1
  656. gen_2-1
  657. gen3-1...
  658. """
  659. self.load_trace()
  660. gen3_loop_event = self.create_event(
  661. trace=self.trace_id,
  662. transaction="/transaction/gen3-1/loop",
  663. spans=[
  664. {
  665. "same_process_as_parent": True,
  666. "op": "http",
  667. "description": "GET gen2-1",
  668. "span_id": self.gen1_span_ids[1],
  669. "trace_id": self.trace_id,
  670. }
  671. ],
  672. parent_span_id=self.gen2_span_ids[1],
  673. project_id=self.project.id,
  674. )
  675. with self.feature(self.FEATURES):
  676. response = self.client.get(
  677. self.url,
  678. data={"project": -1},
  679. format="json",
  680. )
  681. assert response.status_code == 200, response.content
  682. # Should be the same as the simple testcase
  683. self.assert_trace_data(response.data[0], gen2_no_children=False)
  684. # The difference is that gen3-1 should exist with no children
  685. gen2_1 = response.data[0]["children"][1]["children"][0]
  686. assert len(gen2_1["children"]) == 1
  687. gen3_1 = gen2_1["children"][0]
  688. assert gen3_1["event_id"] == gen3_loop_event.event_id
  689. # We didn't even try to start the loop of spans
  690. assert len(gen3_1["children"]) == 0
  691. def test_bad_orphan_span_loop(self):
  692. """Maliciously create a loop in the span structure but for an orphan event"""
  693. root_span_id = uuid4().hex[:16]
  694. root_parent_span = uuid4().hex[:16]
  695. root_event = self.create_event(
  696. trace=self.trace_id,
  697. transaction="/orphan/root/",
  698. spans=[
  699. {
  700. "same_process_as_parent": True,
  701. "op": "http",
  702. "description": "GET orphan_child",
  703. "span_id": root_span_id,
  704. "trace_id": self.trace_id,
  705. }
  706. ],
  707. parent_span_id=root_parent_span,
  708. project_id=self.project.id,
  709. duration=3000,
  710. )
  711. orphan_child = self.create_event(
  712. trace=self.trace_id,
  713. transaction="/orphan/child/",
  714. spans=[
  715. {
  716. "same_process_as_parent": True,
  717. "op": "http",
  718. "description": "GET orphan_root",
  719. "span_id": root_parent_span,
  720. "trace_id": self.trace_id,
  721. }
  722. ],
  723. parent_span_id=root_span_id,
  724. project_id=self.project.id,
  725. duration=300,
  726. )
  727. with self.feature(self.FEATURES):
  728. response = self.client.get(
  729. self.url,
  730. data={"project": -1},
  731. format="json",
  732. )
  733. assert response.status_code == 200, response.content
  734. assert len(response.data) == 1
  735. # There really isn't a right answer to which orphan is the "root" since this loops, but the current
  736. # implementation will make the older event the root
  737. root = response.data[0]
  738. self.assert_event(root, root_event, "root")
  739. assert len(root["children"]) == 1
  740. child = root["children"][0]
  741. self.assert_event(child, orphan_child, "child")
  742. def test_multiple_roots(self):
  743. trace_id = uuid4().hex
  744. first_root = self.create_event(
  745. trace=trace_id,
  746. transaction="/first_root",
  747. spans=[],
  748. parent_span_id=None,
  749. project_id=self.project.id,
  750. duration=500,
  751. )
  752. second_root = self.create_event(
  753. trace=trace_id,
  754. transaction="/second_root",
  755. spans=[],
  756. parent_span_id=None,
  757. project_id=self.project.id,
  758. duration=1000,
  759. )
  760. self.url = reverse(
  761. self.url_name,
  762. kwargs={"organization_slug": self.project.organization.slug, "trace_id": trace_id},
  763. )
  764. with self.feature(self.FEATURES):
  765. response = self.client.get(
  766. self.url,
  767. data={"project": -1},
  768. format="json",
  769. )
  770. assert response.status_code == 200, response.content
  771. assert len(response.data) == 2
  772. self.assert_event(response.data[0], first_root, "first_root")
  773. self.assert_event(response.data[1], second_root, "second_root")
  774. def test_sibling_transactions(self):
  775. """More than one transaction can share a parent_span_id"""
  776. self.load_trace()
  777. gen3_event_siblings = [
  778. self.create_event(
  779. trace=self.trace_id,
  780. transaction="/transaction/gen3-1",
  781. spans=[],
  782. project_id=self.create_project(organization=self.organization).id,
  783. parent_span_id=self.gen2_span_ids[1],
  784. duration=500,
  785. ).event_id,
  786. self.create_event(
  787. trace=self.trace_id,
  788. transaction="/transaction/gen3-2",
  789. spans=[],
  790. project_id=self.create_project(organization=self.organization).id,
  791. parent_span_id=self.gen2_span_ids[1],
  792. duration=525,
  793. ).event_id,
  794. ]
  795. with self.feature(self.FEATURES):
  796. response = self.client.get(
  797. self.url,
  798. data={"project": -1},
  799. format="json",
  800. )
  801. assert response.status_code == 200, response.content
  802. # Should be the same as the simple testcase, but skip checking gen2 children
  803. self.assert_trace_data(response.data[0], gen2_no_children=False)
  804. gen2_parent = response.data[0]["children"][1]["children"][0]
  805. assert len(gen2_parent["children"]) == 2
  806. assert [child["event_id"] for child in gen2_parent["children"]] == gen3_event_siblings
  807. def test_with_orphan_siblings(self):
  808. self.load_trace()
  809. parent_span_id = uuid4().hex[:16]
  810. root_event = self.create_event(
  811. trace=self.trace_id,
  812. transaction="/orphan/root",
  813. spans=[],
  814. # Some random id so its separated from the rest of the trace
  815. parent_span_id=parent_span_id,
  816. project_id=self.project.id,
  817. # Shorter duration means that this event happened first, and should be ordered first
  818. duration=1000,
  819. )
  820. root_sibling_event = self.create_event(
  821. trace=self.trace_id,
  822. transaction="/orphan/root-sibling",
  823. spans=[],
  824. parent_span_id=parent_span_id,
  825. project_id=self.project.id,
  826. duration=1250,
  827. )
  828. with self.feature(self.FEATURES):
  829. response = self.client.get(
  830. self.url,
  831. data={"project": -1},
  832. format="json",
  833. )
  834. assert response.status_code == 200, response.content
  835. assert len(response.data) == 3
  836. # The first item of the response should be the main trace
  837. main, *orphans = response.data
  838. self.assert_trace_data(main)
  839. assert [root_event.event_id, root_sibling_event.event_id] == [
  840. orphan["event_id"] for orphan in orphans
  841. ]
  842. def test_with_orphan_trace(self):
  843. self.load_trace()
  844. orphan_span_ids = {
  845. key: uuid4().hex[:16]
  846. for key in ["root", "root_span", "child", "child_span", "grandchild", "grandchild_span"]
  847. }
  848. # Create the orphan transactions
  849. root_event = self.create_event(
  850. trace=self.trace_id,
  851. transaction="/orphan/root",
  852. spans=[
  853. {
  854. "same_process_as_parent": True,
  855. "op": "http",
  856. "description": "GET gen1 orphan",
  857. "span_id": orphan_span_ids["root_span"],
  858. "trace_id": self.trace_id,
  859. }
  860. ],
  861. # Some random id so its separated from the rest of the trace
  862. parent_span_id=uuid4().hex[:16],
  863. span_id=orphan_span_ids["root"],
  864. project_id=self.project.id,
  865. duration=1000,
  866. )
  867. child_event = self.create_event(
  868. trace=self.trace_id,
  869. transaction="/orphan/child1-0",
  870. spans=[
  871. {
  872. "same_process_as_parent": True,
  873. "op": "http",
  874. "description": "GET gen1 orphan",
  875. "span_id": orphan_span_ids["child_span"],
  876. "trace_id": self.trace_id,
  877. }
  878. ],
  879. parent_span_id=orphan_span_ids["root_span"],
  880. span_id=orphan_span_ids["child"],
  881. project_id=self.gen1_project.id,
  882. # Because the snuba query orders based is_root then timestamp, this causes grandchild1-0 to be added to
  883. # results first before child1-0
  884. duration=2500,
  885. )
  886. grandchild_event = self.create_event(
  887. trace=self.trace_id,
  888. transaction="/orphan/grandchild1-0",
  889. spans=[
  890. {
  891. "same_process_as_parent": True,
  892. "op": "http",
  893. "description": "GET gen1 orphan",
  894. "span_id": orphan_span_ids["grandchild_span"],
  895. "trace_id": self.trace_id,
  896. }
  897. ],
  898. parent_span_id=orphan_span_ids["child_span"],
  899. span_id=orphan_span_ids["grandchild"],
  900. project_id=self.gen1_project.id,
  901. duration=1500,
  902. )
  903. with self.feature(self.FEATURES):
  904. response = self.client.get(
  905. self.url,
  906. data={"project": -1},
  907. format="json",
  908. )
  909. assert response.status_code == 200, response.content
  910. assert len(response.data) == 2
  911. # The first item of the response should be the main trace
  912. main, orphans = response.data
  913. self.assert_trace_data(main)
  914. self.assert_event(orphans, root_event, "orphan-root")
  915. assert len(orphans["children"]) == 1
  916. assert orphans["generation"] == 0
  917. assert orphans["parent_event_id"] is None
  918. child = orphans["children"][0]
  919. self.assert_event(child, child_event, "orphan-child")
  920. assert len(child["children"]) == 1
  921. assert child["generation"] == 1
  922. assert child["parent_event_id"] == root_event.event_id
  923. grandchild = child["children"][0]
  924. self.assert_event(grandchild, grandchild_event, "orphan-grandchild")
  925. assert grandchild["generation"] == 2
  926. assert grandchild["parent_event_id"] == child_event.event_id
  927. def test_with_errors(self):
  928. self.load_trace()
  929. error, error1 = self.load_errors()
  930. with self.feature(self.FEATURES):
  931. response = self.client.get(
  932. self.url,
  933. data={"project": -1},
  934. format="json",
  935. )
  936. assert response.status_code == 200, response.content
  937. self.assert_trace_data(response.data[0])
  938. gen1_event = response.data[0]["children"][0]
  939. assert len(gen1_event["errors"]) == 2
  940. assert {
  941. "event_id": error.event_id,
  942. "issue_id": error.group_id,
  943. "span": self.gen1_span_ids[0],
  944. "project_id": self.gen1_project.id,
  945. "project_slug": self.gen1_project.slug,
  946. "level": "fatal",
  947. "title": error.title,
  948. } in gen1_event["errors"]
  949. assert {
  950. "event_id": error1.event_id,
  951. "issue_id": error1.group_id,
  952. "span": self.gen1_span_ids[0],
  953. "project_id": self.gen1_project.id,
  954. "project_slug": self.gen1_project.slug,
  955. "level": "warning",
  956. "title": error1.title,
  957. } in gen1_event["errors"]
  958. def test_with_default(self):
  959. self.load_trace()
  960. start, _ = self.get_start_end(1000)
  961. default_event = self.load_default()
  962. with self.feature(self.FEATURES):
  963. response = self.client.get(
  964. self.url,
  965. data={"project": -1},
  966. format="json",
  967. )
  968. assert response.status_code == 200, response.content
  969. self.assert_trace_data(response.data[0])
  970. root_event = response.data[0]
  971. assert len(root_event["errors"]) == 1
  972. assert {
  973. "event_id": default_event.event_id,
  974. "issue_id": default_event.group_id,
  975. "span": self.root_span_ids[0],
  976. "project_id": self.gen1_project.id,
  977. "project_slug": self.gen1_project.slug,
  978. "level": "debug",
  979. "title": "this is a log message",
  980. } in root_event["errors"]
  981. def test_pruning_root(self):
  982. self.load_trace()
  983. # Pruning shouldn't happen for the root event
  984. with self.feature(self.FEATURES):
  985. response = self.client.get(
  986. self.url,
  987. data={"project": -1, "event_id": self.root_event.event_id},
  988. format="json",
  989. )
  990. assert response.status_code == 200, response.content
  991. self.assert_trace_data(response.data[0])
  992. def test_pruning_event(self):
  993. self.load_trace()
  994. with self.feature(self.FEATURES):
  995. response = self.client.get(
  996. self.url,
  997. data={"project": -1, "event_id": self.gen2_events[0].event_id},
  998. format="json",
  999. )
  1000. assert response.status_code == 200, response.content
  1001. root = response.data[0]
  1002. self.assert_event(root, self.root_event, "root")
  1003. # Because of snuba query orders by timestamp we should still have all of the root's children
  1004. assert len(root["children"]) == 3
  1005. for i, gen1 in enumerate(root["children"]):
  1006. self.assert_event(gen1, self.gen1_events[i], f"gen1_{i}")
  1007. if i == 0:
  1008. assert len(gen1["children"]) == 1
  1009. gen2 = gen1["children"][0]
  1010. self.assert_event(gen2, self.gen2_events[0], "gen2_0")
  1011. assert len(gen2["children"]) == 1
  1012. gen3 = gen2["children"][0]
  1013. self.assert_event(gen3, self.gen3_event, "gen3_0")
  1014. else:
  1015. assert len(gen1["children"]) == 0
  1016. @region_silo_test
  1017. class OrganizationEventsTraceMetaEndpointTest(OrganizationEventsTraceEndpointBase):
  1018. url_name = "sentry-api-0-organization-events-trace-meta"
  1019. def test_no_projects(self):
  1020. user = self.create_user()
  1021. org = self.create_organization(owner=user)
  1022. self.login_as(user=user)
  1023. url = reverse(
  1024. self.url_name,
  1025. kwargs={"organization_slug": org.slug, "trace_id": uuid4().hex},
  1026. )
  1027. with self.feature(self.FEATURES):
  1028. response = self.client.get(
  1029. url,
  1030. format="json",
  1031. )
  1032. assert response.status_code == 404, response.content
  1033. def test_bad_ids(self):
  1034. # Fake trace id
  1035. self.url = reverse(
  1036. self.url_name,
  1037. kwargs={"organization_slug": self.project.organization.slug, "trace_id": uuid4().hex},
  1038. )
  1039. with self.feature(self.FEATURES):
  1040. response = self.client.get(
  1041. self.url,
  1042. format="json",
  1043. )
  1044. assert response.status_code == 200, response.content
  1045. data = response.data
  1046. assert data["projects"] == 0
  1047. assert data["transactions"] == 0
  1048. assert data["errors"] == 0
  1049. # Invalid trace id
  1050. with pytest.raises(NoReverseMatch):
  1051. self.url = reverse(
  1052. self.url_name,
  1053. kwargs={
  1054. "organization_slug": self.project.organization.slug,
  1055. "trace_id": "not-a-trace",
  1056. },
  1057. )
  1058. def test_simple(self):
  1059. self.load_trace()
  1060. with self.feature(self.FEATURES):
  1061. response = self.client.get(
  1062. self.url,
  1063. data={"project": -1},
  1064. format="json",
  1065. )
  1066. assert response.status_code == 200, response.content
  1067. data = response.data
  1068. assert data["projects"] == 4
  1069. assert data["transactions"] == 8
  1070. assert data["errors"] == 0
  1071. def test_with_errors(self):
  1072. self.load_trace()
  1073. self.load_errors()
  1074. with self.feature(self.FEATURES):
  1075. response = self.client.get(
  1076. self.url,
  1077. data={"project": -1},
  1078. format="json",
  1079. )
  1080. assert response.status_code == 200, response.content
  1081. data = response.data
  1082. assert data["projects"] == 4
  1083. assert data["transactions"] == 8
  1084. assert data["errors"] == 2
  1085. def test_with_default(self):
  1086. self.load_trace()
  1087. self.load_default()
  1088. with self.feature(self.FEATURES):
  1089. response = self.client.get(
  1090. self.url,
  1091. data={"project": -1},
  1092. format="json",
  1093. )
  1094. assert response.status_code == 200, response.content
  1095. data = response.data
  1096. assert data["projects"] == 4
  1097. assert data["transactions"] == 8
  1098. assert data["errors"] == 1