test_organization_events_trace.py 61 KB

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