test_organization_events_trace.py 66 KB

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