test_organization_events_trace.py 65 KB

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