test_organization_events_trace.py 45 KB

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