test_organization_events_trace.py 66 KB

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