test_organization_events_trace.py 45 KB

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