test_organization_events_trace.py 59 KB

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