test_organization_events_trace.py 63 KB

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