test_organization_events_trace.py 44 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263
  1. from datetime import timedelta
  2. from uuid import uuid4
  3. import pytest
  4. from django.urls import NoReverseMatch, reverse
  5. from sentry import options
  6. from sentry.testutils import APITestCase, SnubaTestCase
  7. from sentry.testutils.helpers.datetime import before_now, iso_format
  8. from sentry.testutils.silo import region_silo_test
  9. from sentry.utils.samples import load_data
  10. class OrganizationEventsTraceEndpointBase(APITestCase, SnubaTestCase):
  11. FEATURES = [
  12. "organizations:performance-view",
  13. "organizations:performance-file-io-main-thread-detector",
  14. ]
  15. def get_start_end(self, duration):
  16. return self.day_ago, self.day_ago + timedelta(milliseconds=duration)
  17. def create_event(
  18. self,
  19. trace,
  20. transaction,
  21. spans,
  22. parent_span_id,
  23. project_id,
  24. tags=None,
  25. duration=4000,
  26. span_id=None,
  27. measurements=None,
  28. file_io_performance_issue=False,
  29. **kwargs,
  30. ):
  31. start, end = self.get_start_end(duration)
  32. data = load_data(
  33. "transaction",
  34. trace=trace,
  35. spans=spans,
  36. timestamp=end,
  37. start_timestamp=start,
  38. )
  39. data["transaction"] = transaction
  40. data["contexts"]["trace"]["parent_span_id"] = parent_span_id
  41. if span_id:
  42. data["contexts"]["trace"]["span_id"] = span_id
  43. if measurements:
  44. for key, value in measurements.items():
  45. data["measurements"][key]["value"] = value
  46. if tags is not None:
  47. data["tags"] = tags
  48. if file_io_performance_issue:
  49. span = data["spans"][0]
  50. if "data" not in span:
  51. span["data"] = {}
  52. span["data"].update({"duration": 1, "blocked_main_thread": True})
  53. with self.feature(self.FEATURES):
  54. return self.store_event(data, project_id=project_id, **kwargs)
  55. def setUp(self):
  56. """
  57. Span structure:
  58. root
  59. gen1-0
  60. gen2-0
  61. gen3-0
  62. gen1-1
  63. gen2-1
  64. gen1-2
  65. gen2-2
  66. """
  67. super().setUp()
  68. options.set("performance.issues.all.problem-detection", 1.0)
  69. options.set("performance.issues.file_io_main_thread.problem-creation", 1.0)
  70. self.login_as(user=self.user)
  71. self.day_ago = before_now(days=1).replace(hour=10, minute=0, second=0, microsecond=0)
  72. self.root_span_ids = [uuid4().hex[:16] for _ in range(3)]
  73. self.trace_id = uuid4().hex
  74. self.url = reverse(
  75. self.url_name,
  76. kwargs={"organization_slug": self.project.organization.slug, "trace_id": self.trace_id},
  77. )
  78. def load_trace(self):
  79. self.root_event = self.create_event(
  80. trace=self.trace_id,
  81. transaction="root",
  82. spans=[
  83. {
  84. "same_process_as_parent": True,
  85. "op": "http",
  86. "description": f"GET gen1-{i}",
  87. "span_id": root_span_id,
  88. "trace_id": self.trace_id,
  89. }
  90. for i, root_span_id in enumerate(self.root_span_ids)
  91. ],
  92. measurements={
  93. "lcp": 1000,
  94. "fcp": 750,
  95. },
  96. parent_span_id=None,
  97. file_io_performance_issue=True,
  98. project_id=self.project.id,
  99. duration=3000,
  100. )
  101. # First Generation
  102. self.gen1_span_ids = [uuid4().hex[:16] for _ in range(3)]
  103. self.gen1_project = self.create_project(organization=self.organization)
  104. self.gen1_events = [
  105. self.create_event(
  106. trace=self.trace_id,
  107. transaction=f"/transaction/gen1-{i}",
  108. spans=[
  109. {
  110. "same_process_as_parent": True,
  111. "op": "http",
  112. "description": f"GET gen2-{i}",
  113. "span_id": gen1_span_id,
  114. "trace_id": self.trace_id,
  115. }
  116. ],
  117. parent_span_id=root_span_id,
  118. project_id=self.gen1_project.id,
  119. duration=2000,
  120. )
  121. for i, (root_span_id, gen1_span_id) in enumerate(
  122. zip(self.root_span_ids, self.gen1_span_ids)
  123. )
  124. ]
  125. # Second Generation
  126. self.gen2_span_ids = [uuid4().hex[:16] for _ in range(3)]
  127. self.gen2_project = self.create_project(organization=self.organization)
  128. # Intentially pick a span id that starts with 0s
  129. self.gen2_span_id = "0011" * 4
  130. self.gen2_events = [
  131. self.create_event(
  132. trace=self.trace_id,
  133. transaction=f"/transaction/gen2-{i}",
  134. spans=[
  135. {
  136. "same_process_as_parent": True,
  137. "op": "http",
  138. "description": f"GET gen3-{i}" if i == 0 else f"SPAN gen3-{i}",
  139. "span_id": gen2_span_id,
  140. "trace_id": self.trace_id,
  141. }
  142. ],
  143. parent_span_id=gen1_span_id,
  144. span_id=self.gen2_span_id if i == 0 else None,
  145. project_id=self.gen2_project.id,
  146. duration=1000,
  147. )
  148. for i, (gen1_span_id, gen2_span_id) in enumerate(
  149. zip(self.gen1_span_ids, self.gen2_span_ids)
  150. )
  151. ]
  152. # Third generation
  153. self.gen3_project = self.create_project(organization=self.organization)
  154. self.gen3_event = self.create_event(
  155. trace=self.trace_id,
  156. transaction="/transaction/gen3-0",
  157. spans=[],
  158. project_id=self.gen3_project.id,
  159. parent_span_id=self.gen2_span_id,
  160. duration=500,
  161. )
  162. def load_errors(self):
  163. start, _ = self.get_start_end(1000)
  164. error_data = load_data(
  165. "javascript",
  166. timestamp=start,
  167. )
  168. error_data["contexts"]["trace"] = {
  169. "type": "trace",
  170. "trace_id": self.trace_id,
  171. "span_id": self.gen1_span_ids[0],
  172. }
  173. error_data["level"] = "fatal"
  174. error = self.store_event(error_data, project_id=self.gen1_project.id)
  175. error_data["level"] = "warning"
  176. error1 = self.store_event(error_data, project_id=self.gen1_project.id)
  177. return error, error1
  178. def load_default(self):
  179. start, _ = self.get_start_end(1000)
  180. return self.store_event(
  181. {
  182. "timestamp": iso_format(start),
  183. "contexts": {
  184. "trace": {
  185. "type": "trace",
  186. "trace_id": self.trace_id,
  187. "span_id": self.root_span_ids[0],
  188. },
  189. },
  190. "level": "debug",
  191. "message": "this is a log message",
  192. },
  193. project_id=self.gen1_project.id,
  194. )
  195. @region_silo_test
  196. class OrganizationEventsTraceLightEndpointTest(OrganizationEventsTraceEndpointBase):
  197. url_name = "sentry-api-0-organization-events-trace-light"
  198. def test_no_projects(self):
  199. user = self.create_user()
  200. org = self.create_organization(owner=user)
  201. self.login_as(user=user)
  202. url = reverse(
  203. self.url_name,
  204. kwargs={"organization_slug": org.slug, "trace_id": uuid4().hex},
  205. )
  206. with self.feature(self.FEATURES):
  207. response = self.client.get(
  208. url,
  209. format="json",
  210. )
  211. assert response.status_code == 404, response.content
  212. def test_bad_ids(self):
  213. # Fake event id
  214. with self.feature(self.FEATURES):
  215. response = self.client.get(
  216. self.url,
  217. data={"event_id": uuid4().hex},
  218. format="json",
  219. )
  220. assert response.status_code == 404, response.content
  221. # Invalid event id
  222. with self.feature(self.FEATURES):
  223. response = self.client.get(
  224. self.url,
  225. data={"event_id": "not-a-event"},
  226. format="json",
  227. )
  228. assert response.status_code == 400, response.content
  229. # Fake trace id
  230. self.url = reverse(
  231. "sentry-api-0-organization-events-trace-light",
  232. kwargs={"organization_slug": self.project.organization.slug, "trace_id": uuid4().hex},
  233. )
  234. with self.feature(self.FEATURES):
  235. response = self.client.get(
  236. self.url,
  237. data={"event_id": "a" * 32},
  238. format="json",
  239. )
  240. assert response.status_code == 404, response.content
  241. # Invalid trace id
  242. with pytest.raises(NoReverseMatch):
  243. self.url = reverse(
  244. "sentry-api-0-organization-events-trace-light",
  245. kwargs={
  246. "organization_slug": self.project.organization.slug,
  247. "trace_id": "not-a-trace",
  248. },
  249. )
  250. def test_no_roots(self):
  251. """Even when there's no root, we return the current event"""
  252. self.load_trace()
  253. no_root_trace = uuid4().hex
  254. parent_span_id = uuid4().hex[:16]
  255. no_root_event = self.create_event(
  256. trace=no_root_trace,
  257. transaction="/not_root/but_only_transaction",
  258. spans=[],
  259. parent_span_id=parent_span_id,
  260. project_id=self.project.id,
  261. )
  262. url = reverse(
  263. "sentry-api-0-organization-events-trace-light",
  264. kwargs={"organization_slug": self.project.organization.slug, "trace_id": no_root_trace},
  265. )
  266. with self.feature(self.FEATURES):
  267. response = self.client.get(
  268. url,
  269. data={"event_id": no_root_event.event_id},
  270. format="json",
  271. )
  272. assert response.status_code == 200, response.content
  273. assert len(response.data) == 1
  274. event = response.data[0]
  275. # Basically know nothing about this event
  276. assert event["generation"] is None
  277. assert event["parent_event_id"] is None
  278. assert event["parent_span_id"] == parent_span_id
  279. assert event["event_id"] == no_root_event.event_id
  280. def test_multiple_roots(self):
  281. self.load_trace()
  282. second_root = self.create_event(
  283. trace=self.trace_id,
  284. transaction="/second_root",
  285. spans=[],
  286. parent_span_id=None,
  287. project_id=self.project.id,
  288. )
  289. with self.feature(self.FEATURES):
  290. response = self.client.get(
  291. self.url,
  292. data={"event_id": second_root.event_id, "project": -1},
  293. format="json",
  294. )
  295. assert response.status_code == 200, response.content
  296. assert len(response.data) == 1
  297. event = response.data[0]
  298. assert event["generation"] == 0
  299. assert event["parent_event_id"] is None
  300. assert event["parent_span_id"] is None
  301. def test_root_event(self):
  302. self.load_trace()
  303. root_event_id = self.root_event.event_id
  304. with self.feature(self.FEATURES):
  305. response = self.client.get(
  306. self.url,
  307. data={"event_id": root_event_id, "project": -1},
  308. format="json",
  309. )
  310. assert response.status_code == 200, response.content
  311. assert len(response.data) == 4
  312. events = {item["event_id"]: item for item in response.data}
  313. assert root_event_id in events
  314. event = events[root_event_id]
  315. assert event["generation"] == 0
  316. assert event["parent_event_id"] is None
  317. assert event["parent_span_id"] is None
  318. for i, child_event in enumerate(self.gen1_events):
  319. child_event_id = child_event.event_id
  320. assert child_event_id in events
  321. event = events[child_event_id]
  322. assert event["generation"] == 1
  323. assert event["parent_event_id"] == root_event_id
  324. assert event["parent_span_id"] == self.root_span_ids[i]
  325. def test_root_with_multiple_roots(self):
  326. self.load_trace()
  327. root_event_id = self.root_event.event_id
  328. self.create_event(
  329. trace=self.trace_id,
  330. transaction="/second_root",
  331. spans=[],
  332. parent_span_id=None,
  333. project_id=self.project.id,
  334. )
  335. with self.feature(self.FEATURES):
  336. response = self.client.get(
  337. self.url,
  338. data={"event_id": self.root_event.event_id},
  339. format="json",
  340. )
  341. assert response.status_code == 200, response.content
  342. assert len(response.data) == 4
  343. events = {item["event_id"]: item for item in response.data}
  344. assert root_event_id in events
  345. event = events[root_event_id]
  346. assert event["generation"] == 0
  347. assert event["parent_event_id"] is None
  348. assert event["parent_span_id"] is None
  349. for i, child_event in enumerate(self.gen1_events):
  350. child_event_id = child_event.event_id
  351. assert child_event_id in events
  352. event = events[child_event_id]
  353. assert event["generation"] == 1
  354. assert event["parent_event_id"] == root_event_id
  355. assert event["parent_span_id"] == self.root_span_ids[i]
  356. def test_direct_parent_with_children(self):
  357. self.load_trace()
  358. root_event_id = self.root_event.event_id
  359. current_event = self.gen1_events[0].event_id
  360. child_event_id = self.gen2_events[0].event_id
  361. with self.feature(self.FEATURES):
  362. response = self.client.get(
  363. self.url,
  364. data={"event_id": current_event, "project": -1},
  365. format="json",
  366. )
  367. assert response.status_code == 200, response.content
  368. assert len(response.data) == 3
  369. events = {item["event_id"]: item for item in response.data}
  370. assert root_event_id in events
  371. event = events[root_event_id]
  372. assert event["generation"] == 0
  373. assert event["parent_event_id"] is None
  374. assert event["parent_span_id"] is None
  375. assert current_event in events
  376. event = events[current_event]
  377. assert event["generation"] == 1
  378. assert event["parent_event_id"] == root_event_id
  379. assert event["parent_span_id"] == self.root_span_ids[0]
  380. assert child_event_id in events
  381. event = events[child_event_id]
  382. assert event["generation"] == 2
  383. assert event["parent_event_id"] == current_event
  384. assert event["parent_span_id"] == self.gen1_span_ids[0]
  385. def test_direct_parent_with_children_and_multiple_root(self):
  386. self.load_trace()
  387. root_event_id = self.root_event.event_id
  388. current_event = self.gen1_events[0].event_id
  389. child_event_id = self.gen2_events[0].event_id
  390. self.create_event(
  391. trace=self.trace_id,
  392. transaction="/second_root",
  393. spans=[],
  394. parent_span_id=None,
  395. project_id=self.project.id,
  396. )
  397. with self.feature(self.FEATURES):
  398. response = self.client.get(
  399. self.url,
  400. data={"event_id": current_event, "project": -1},
  401. format="json",
  402. )
  403. assert response.status_code == 200, response.content
  404. assert len(response.data) == 3
  405. events = {item["event_id"]: item for item in response.data}
  406. assert root_event_id in events
  407. event = events[root_event_id]
  408. assert event["generation"] == 0
  409. assert event["parent_event_id"] is None
  410. assert event["parent_span_id"] is None
  411. assert current_event in events
  412. event = events[current_event]
  413. assert event["generation"] == 1
  414. assert event["parent_event_id"] == root_event_id
  415. assert event["parent_span_id"] == self.root_span_ids[0]
  416. assert child_event_id in events
  417. event = events[child_event_id]
  418. assert event["generation"] == 2
  419. assert event["parent_event_id"] == current_event
  420. assert event["parent_span_id"] == self.gen1_span_ids[0]
  421. def test_second_generation_with_children(self):
  422. self.load_trace()
  423. current_event = self.gen2_events[0].event_id
  424. child_event_id = self.gen3_event.event_id
  425. with self.feature(self.FEATURES):
  426. response = self.client.get(
  427. self.url,
  428. data={"event_id": current_event, "project": -1},
  429. format="json",
  430. )
  431. assert response.status_code == 200, response.content
  432. assert len(response.data) == 2
  433. events = {item["event_id"]: item for item in response.data}
  434. assert current_event in events
  435. event = events[current_event]
  436. # Parent/generation is unknown in this case
  437. assert event["generation"] is None
  438. assert event["parent_event_id"] is None
  439. # But we still know the parent_span
  440. assert event["parent_span_id"] == self.gen1_span_ids[0]
  441. assert child_event_id in events
  442. event = events[child_event_id]
  443. assert event["generation"] is None
  444. assert event["parent_event_id"] == current_event
  445. assert event["parent_span_id"] == self.gen2_span_id
  446. def test_third_generation_no_children(self):
  447. self.load_trace()
  448. current_event = self.gen3_event.event_id
  449. with self.feature(self.FEATURES):
  450. response = self.client.get(
  451. self.url,
  452. data={"event_id": current_event, "project": -1},
  453. format="json",
  454. )
  455. assert response.status_code == 200, response.content
  456. assert len(response.data) == 1
  457. event = response.data[0]
  458. assert event["generation"] is None
  459. # Parent is unknown in this case
  460. assert event["parent_event_id"] is None
  461. # But we still know the parent_span
  462. assert event["parent_span_id"] == self.gen2_span_id
  463. def test_sibling_transactions(self):
  464. """More than one transaction can share a parent_span_id"""
  465. self.load_trace()
  466. gen3_event_siblings = [
  467. self.create_event(
  468. trace=self.trace_id,
  469. transaction="/transaction/gen3-1",
  470. spans=[],
  471. project_id=self.create_project(organization=self.organization).id,
  472. parent_span_id=self.gen2_span_ids[1],
  473. duration=500,
  474. ).event_id,
  475. self.create_event(
  476. trace=self.trace_id,
  477. transaction="/transaction/gen3-2",
  478. spans=[],
  479. project_id=self.create_project(organization=self.organization).id,
  480. parent_span_id=self.gen2_span_ids[1],
  481. duration=525,
  482. ).event_id,
  483. ]
  484. current_event = self.gen2_events[1].event_id
  485. with self.feature(self.FEATURES):
  486. response = self.client.get(
  487. self.url,
  488. data={"event_id": current_event, "project": -1},
  489. format="json",
  490. )
  491. assert len(response.data) == 3
  492. events = {item["event_id"]: item for item in response.data}
  493. for child_event_id in gen3_event_siblings:
  494. assert child_event_id in events
  495. event = events[child_event_id]
  496. assert event["generation"] is None
  497. assert event["parent_event_id"] == current_event
  498. assert event["parent_span_id"] == self.gen2_span_ids[1]
  499. def test_with_error_event(self):
  500. self.load_trace()
  501. root_event_id = self.root_event.event_id
  502. current_transaction_event = self.gen1_events[0].event_id
  503. start, _ = self.get_start_end(1000)
  504. error_data = load_data(
  505. "javascript",
  506. timestamp=start,
  507. )
  508. error_data["contexts"]["trace"] = {
  509. "type": "trace",
  510. "trace_id": self.trace_id,
  511. "span_id": self.gen1_span_ids[0],
  512. }
  513. error_data["tags"] = [["transaction", "/transaction/gen1-0"]]
  514. error = self.store_event(error_data, project_id=self.gen1_project.id)
  515. def assertions(response):
  516. assert response.status_code == 200, response.content
  517. assert len(response.data) == 3
  518. events = {item["event_id"]: item for item in response.data}
  519. assert root_event_id in events
  520. event = events[root_event_id]
  521. assert event["generation"] == 0
  522. assert event["parent_event_id"] is None
  523. assert event["parent_span_id"] is None
  524. assert len(event["errors"]) == 0
  525. assert current_transaction_event in events
  526. event = events[current_transaction_event]
  527. assert event["generation"] == 1
  528. assert event["parent_event_id"] == root_event_id
  529. assert event["parent_span_id"] == self.root_span_ids[0]
  530. assert len(event["errors"]) == 1
  531. assert event["errors"][0]["event_id"] == error.event_id
  532. assert event["errors"][0]["issue_id"] == error.group_id
  533. with self.feature(self.FEATURES):
  534. response = self.client.get(
  535. self.url,
  536. data={"event_id": error.event_id, "project": -1},
  537. format="json",
  538. )
  539. assertions(response)
  540. with self.feature(self.FEATURES):
  541. response = self.client.get(
  542. self.url,
  543. data={"event_id": current_transaction_event, "project": -1},
  544. format="json",
  545. )
  546. assertions(response)
  547. @region_silo_test
  548. class OrganizationEventsTraceEndpointTest(OrganizationEventsTraceEndpointBase):
  549. url_name = "sentry-api-0-organization-events-trace"
  550. def assert_event(self, result, event_data, message):
  551. assert result["event_id"] == event_data.event_id, message
  552. assert result["timestamp"] == event_data.data["timestamp"], message
  553. assert result["start_timestamp"] == event_data.data["start_timestamp"], message
  554. def assert_trace_data(self, root, gen2_no_children=True):
  555. """see the setUp docstring for an idea of what the response structure looks like"""
  556. self.assert_event(root, self.root_event, "root")
  557. assert root["parent_event_id"] is None
  558. assert root["parent_span_id"] is None
  559. assert root["generation"] == 0
  560. assert root["transaction.duration"] == 3000
  561. assert len(root["children"]) == 3
  562. assert len(root["performance_issues"]) == 1
  563. assert root["performance_issues"][0]["suspect_spans"][0] == self.root_span_ids[0]
  564. for i, gen1 in enumerate(root["children"]):
  565. self.assert_event(gen1, self.gen1_events[i], f"gen1_{i}")
  566. assert gen1["parent_event_id"] == self.root_event.event_id
  567. assert gen1["parent_span_id"] == self.root_span_ids[i]
  568. assert gen1["generation"] == 1
  569. assert gen1["transaction.duration"] == 2000
  570. assert len(gen1["children"]) == 1
  571. gen2 = gen1["children"][0]
  572. self.assert_event(gen2, self.gen2_events[i], f"gen2_{i}")
  573. assert gen2["parent_event_id"] == self.gen1_events[i].event_id
  574. assert gen2["parent_span_id"] == self.gen1_span_ids[i]
  575. assert gen2["generation"] == 2
  576. assert gen2["transaction.duration"] == 1000
  577. # Only the first gen2 descendent has a child
  578. if i == 0:
  579. assert len(gen2["children"]) == 1
  580. gen3 = gen2["children"][0]
  581. self.assert_event(gen3, self.gen3_event, f"gen3_{i}")
  582. assert gen3["parent_event_id"] == self.gen2_events[i].event_id
  583. assert gen3["parent_span_id"] == self.gen2_span_id
  584. assert gen3["generation"] == 3
  585. assert gen3["transaction.duration"] == 500
  586. assert len(gen3["children"]) == 0
  587. elif gen2_no_children:
  588. assert len(gen2["children"]) == 0
  589. def test_no_projects(self):
  590. user = self.create_user()
  591. org = self.create_organization(owner=user)
  592. self.login_as(user=user)
  593. url = reverse(
  594. self.url_name,
  595. kwargs={"organization_slug": org.slug, "trace_id": uuid4().hex},
  596. )
  597. with self.feature(self.FEATURES):
  598. response = self.client.get(
  599. url,
  600. format="json",
  601. )
  602. assert response.status_code == 404, response.content
  603. def test_simple(self):
  604. self.load_trace()
  605. with self.feature(self.FEATURES):
  606. response = self.client.get(
  607. self.url,
  608. data={"project": -1},
  609. format="json",
  610. )
  611. assert response.status_code == 200, response.content
  612. self.assert_trace_data(response.data[0])
  613. # We shouldn't have detailed fields here
  614. assert "transaction.status" not in response.data[0]
  615. assert "tags" not in response.data[0]
  616. assert "measurements" not in response.data[0]
  617. def test_detailed_trace(self):
  618. self.load_trace()
  619. with self.feature(self.FEATURES):
  620. response = self.client.get(
  621. self.url,
  622. data={"project": -1, "detailed": 1},
  623. format="json",
  624. )
  625. assert response.status_code == 200, response.content
  626. self.assert_trace_data(response.data[0])
  627. root = response.data[0]
  628. assert root["transaction.status"] == "ok"
  629. root_tags = {tag["key"]: tag["value"] for tag in root["tags"]}
  630. for [key, value] in self.root_event.tags:
  631. if not key.startswith("sentry:"):
  632. assert root_tags[key] == value, f"tags - {key}"
  633. else:
  634. assert root_tags[key[7:]] == value, f"tags - {key}"
  635. assert root["measurements"]["lcp"]["value"] == 1000
  636. assert root["measurements"]["fcp"]["value"] == 750
  637. def test_detailed_trace_with_bad_tags(self):
  638. """Basically test that we're actually using the event serializer's method for tags"""
  639. trace = uuid4().hex
  640. self.create_event(
  641. trace=trace,
  642. transaction="bad-tags",
  643. parent_span_id=None,
  644. spans=[],
  645. project_id=self.project.id,
  646. tags=[["somethinglong" * 250, "somethinglong" * 250]],
  647. duration=3000,
  648. assert_no_errors=False,
  649. )
  650. url = reverse(
  651. self.url_name,
  652. kwargs={"organization_slug": self.project.organization.slug, "trace_id": trace},
  653. )
  654. with self.feature(self.FEATURES):
  655. response = self.client.get(
  656. url,
  657. data={"project": -1, "detailed": 1},
  658. format="json",
  659. )
  660. assert response.status_code == 200, response.content
  661. root = response.data[0]
  662. assert root["transaction.status"] == "ok"
  663. assert {"key": None, "value": None} in root["tags"]
  664. def test_bad_span_loop(self):
  665. """Maliciously create a loop in the span structure
  666. Structure then becomes something like this:
  667. root
  668. gen1-0...
  669. gen1-1
  670. gen2-1
  671. gen3-1
  672. gen_2-1
  673. gen3-1...
  674. """
  675. self.load_trace()
  676. gen3_loop_event = self.create_event(
  677. trace=self.trace_id,
  678. transaction="/transaction/gen3-1/loop",
  679. spans=[
  680. {
  681. "same_process_as_parent": True,
  682. "op": "http",
  683. "description": "GET gen2-1",
  684. "span_id": self.gen1_span_ids[1],
  685. "trace_id": self.trace_id,
  686. }
  687. ],
  688. parent_span_id=self.gen2_span_ids[1],
  689. project_id=self.project.id,
  690. )
  691. with self.feature(self.FEATURES):
  692. response = self.client.get(
  693. self.url,
  694. data={"project": -1},
  695. format="json",
  696. )
  697. assert response.status_code == 200, response.content
  698. # Should be the same as the simple testcase
  699. self.assert_trace_data(response.data[0], gen2_no_children=False)
  700. # The difference is that gen3-1 should exist with no children
  701. gen2_1 = response.data[0]["children"][1]["children"][0]
  702. assert len(gen2_1["children"]) == 1
  703. gen3_1 = gen2_1["children"][0]
  704. assert gen3_1["event_id"] == gen3_loop_event.event_id
  705. # We didn't even try to start the loop of spans
  706. assert len(gen3_1["children"]) == 0
  707. def test_bad_orphan_span_loop(self):
  708. """Maliciously create a loop in the span structure but for an orphan event"""
  709. root_span_id = uuid4().hex[:16]
  710. root_parent_span = uuid4().hex[:16]
  711. root_event = self.create_event(
  712. trace=self.trace_id,
  713. transaction="/orphan/root/",
  714. spans=[
  715. {
  716. "same_process_as_parent": True,
  717. "op": "http",
  718. "description": "GET orphan_child",
  719. "span_id": root_span_id,
  720. "trace_id": self.trace_id,
  721. }
  722. ],
  723. parent_span_id=root_parent_span,
  724. project_id=self.project.id,
  725. duration=3000,
  726. )
  727. orphan_child = self.create_event(
  728. trace=self.trace_id,
  729. transaction="/orphan/child/",
  730. spans=[
  731. {
  732. "same_process_as_parent": True,
  733. "op": "http",
  734. "description": "GET orphan_root",
  735. "span_id": root_parent_span,
  736. "trace_id": self.trace_id,
  737. }
  738. ],
  739. parent_span_id=root_span_id,
  740. project_id=self.project.id,
  741. duration=300,
  742. )
  743. with self.feature(self.FEATURES):
  744. response = self.client.get(
  745. self.url,
  746. data={"project": -1},
  747. format="json",
  748. )
  749. assert response.status_code == 200, response.content
  750. assert len(response.data) == 1
  751. # There really isn't a right answer to which orphan is the "root" since this loops, but the current
  752. # implementation will make the older event the root
  753. root = response.data[0]
  754. self.assert_event(root, root_event, "root")
  755. assert len(root["children"]) == 1
  756. child = root["children"][0]
  757. self.assert_event(child, orphan_child, "child")
  758. def test_multiple_roots(self):
  759. trace_id = uuid4().hex
  760. first_root = self.create_event(
  761. trace=trace_id,
  762. transaction="/first_root",
  763. spans=[],
  764. parent_span_id=None,
  765. project_id=self.project.id,
  766. duration=500,
  767. )
  768. second_root = self.create_event(
  769. trace=trace_id,
  770. transaction="/second_root",
  771. spans=[],
  772. parent_span_id=None,
  773. project_id=self.project.id,
  774. duration=1000,
  775. )
  776. self.url = reverse(
  777. self.url_name,
  778. kwargs={"organization_slug": self.project.organization.slug, "trace_id": trace_id},
  779. )
  780. with self.feature(self.FEATURES):
  781. response = self.client.get(
  782. self.url,
  783. data={"project": -1},
  784. format="json",
  785. )
  786. assert response.status_code == 200, response.content
  787. assert len(response.data) == 2
  788. self.assert_event(response.data[0], first_root, "first_root")
  789. self.assert_event(response.data[1], second_root, "second_root")
  790. def test_sibling_transactions(self):
  791. """More than one transaction can share a parent_span_id"""
  792. self.load_trace()
  793. gen3_event_siblings = [
  794. self.create_event(
  795. trace=self.trace_id,
  796. transaction="/transaction/gen3-1",
  797. spans=[],
  798. project_id=self.create_project(organization=self.organization).id,
  799. parent_span_id=self.gen2_span_ids[1],
  800. duration=500,
  801. ).event_id,
  802. self.create_event(
  803. trace=self.trace_id,
  804. transaction="/transaction/gen3-2",
  805. spans=[],
  806. project_id=self.create_project(organization=self.organization).id,
  807. parent_span_id=self.gen2_span_ids[1],
  808. duration=525,
  809. ).event_id,
  810. ]
  811. with self.feature(self.FEATURES):
  812. response = self.client.get(
  813. self.url,
  814. data={"project": -1},
  815. format="json",
  816. )
  817. assert response.status_code == 200, response.content
  818. # Should be the same as the simple testcase, but skip checking gen2 children
  819. self.assert_trace_data(response.data[0], gen2_no_children=False)
  820. gen2_parent = response.data[0]["children"][1]["children"][0]
  821. assert len(gen2_parent["children"]) == 2
  822. assert [child["event_id"] for child in gen2_parent["children"]] == gen3_event_siblings
  823. def test_with_orphan_siblings(self):
  824. self.load_trace()
  825. parent_span_id = uuid4().hex[:16]
  826. root_event = self.create_event(
  827. trace=self.trace_id,
  828. transaction="/orphan/root",
  829. spans=[],
  830. # Some random id so its separated from the rest of the trace
  831. parent_span_id=parent_span_id,
  832. project_id=self.project.id,
  833. # Shorter duration means that this event happened first, and should be ordered first
  834. duration=1000,
  835. )
  836. root_sibling_event = self.create_event(
  837. trace=self.trace_id,
  838. transaction="/orphan/root-sibling",
  839. spans=[],
  840. parent_span_id=parent_span_id,
  841. project_id=self.project.id,
  842. duration=1250,
  843. )
  844. with self.feature(self.FEATURES):
  845. response = self.client.get(
  846. self.url,
  847. data={"project": -1},
  848. format="json",
  849. )
  850. assert response.status_code == 200, response.content
  851. assert len(response.data) == 3
  852. # The first item of the response should be the main trace
  853. main, *orphans = response.data
  854. self.assert_trace_data(main)
  855. assert [root_event.event_id, root_sibling_event.event_id] == [
  856. orphan["event_id"] for orphan in orphans
  857. ]
  858. def test_with_orphan_trace(self):
  859. self.load_trace()
  860. orphan_span_ids = {
  861. key: uuid4().hex[:16]
  862. for key in ["root", "root_span", "child", "child_span", "grandchild", "grandchild_span"]
  863. }
  864. # Create the orphan transactions
  865. root_event = self.create_event(
  866. trace=self.trace_id,
  867. transaction="/orphan/root",
  868. spans=[
  869. {
  870. "same_process_as_parent": True,
  871. "op": "http",
  872. "description": "GET gen1 orphan",
  873. "span_id": orphan_span_ids["root_span"],
  874. "trace_id": self.trace_id,
  875. }
  876. ],
  877. # Some random id so its separated from the rest of the trace
  878. parent_span_id=uuid4().hex[:16],
  879. span_id=orphan_span_ids["root"],
  880. project_id=self.project.id,
  881. duration=1000,
  882. )
  883. child_event = self.create_event(
  884. trace=self.trace_id,
  885. transaction="/orphan/child1-0",
  886. spans=[
  887. {
  888. "same_process_as_parent": True,
  889. "op": "http",
  890. "description": "GET gen1 orphan",
  891. "span_id": orphan_span_ids["child_span"],
  892. "trace_id": self.trace_id,
  893. }
  894. ],
  895. parent_span_id=orphan_span_ids["root_span"],
  896. span_id=orphan_span_ids["child"],
  897. project_id=self.gen1_project.id,
  898. # Because the snuba query orders based is_root then timestamp, this causes grandchild1-0 to be added to
  899. # results first before child1-0
  900. duration=2500,
  901. )
  902. grandchild_event = self.create_event(
  903. trace=self.trace_id,
  904. transaction="/orphan/grandchild1-0",
  905. spans=[
  906. {
  907. "same_process_as_parent": True,
  908. "op": "http",
  909. "description": "GET gen1 orphan",
  910. "span_id": orphan_span_ids["grandchild_span"],
  911. "trace_id": self.trace_id,
  912. }
  913. ],
  914. parent_span_id=orphan_span_ids["child_span"],
  915. span_id=orphan_span_ids["grandchild"],
  916. project_id=self.gen1_project.id,
  917. duration=1500,
  918. )
  919. with self.feature(self.FEATURES):
  920. response = self.client.get(
  921. self.url,
  922. data={"project": -1},
  923. format="json",
  924. )
  925. assert response.status_code == 200, response.content
  926. assert len(response.data) == 2
  927. # The first item of the response should be the main trace
  928. main, orphans = response.data
  929. self.assert_trace_data(main)
  930. self.assert_event(orphans, root_event, "orphan-root")
  931. assert len(orphans["children"]) == 1
  932. assert orphans["generation"] == 0
  933. assert orphans["parent_event_id"] is None
  934. child = orphans["children"][0]
  935. self.assert_event(child, child_event, "orphan-child")
  936. assert len(child["children"]) == 1
  937. assert child["generation"] == 1
  938. assert child["parent_event_id"] == root_event.event_id
  939. grandchild = child["children"][0]
  940. self.assert_event(grandchild, grandchild_event, "orphan-grandchild")
  941. assert grandchild["generation"] == 2
  942. assert grandchild["parent_event_id"] == child_event.event_id
  943. def test_with_errors(self):
  944. self.load_trace()
  945. error, error1 = self.load_errors()
  946. with self.feature(self.FEATURES):
  947. response = self.client.get(
  948. self.url,
  949. data={"project": -1},
  950. format="json",
  951. )
  952. assert response.status_code == 200, response.content
  953. self.assert_trace_data(response.data[0])
  954. gen1_event = response.data[0]["children"][0]
  955. assert len(gen1_event["errors"]) == 2
  956. assert {
  957. "event_id": error.event_id,
  958. "issue_id": error.group_id,
  959. "span": self.gen1_span_ids[0],
  960. "project_id": self.gen1_project.id,
  961. "project_slug": self.gen1_project.slug,
  962. "level": "fatal",
  963. "title": error.title,
  964. } in gen1_event["errors"]
  965. assert {
  966. "event_id": error1.event_id,
  967. "issue_id": error1.group_id,
  968. "span": self.gen1_span_ids[0],
  969. "project_id": self.gen1_project.id,
  970. "project_slug": self.gen1_project.slug,
  971. "level": "warning",
  972. "title": error1.title,
  973. } in gen1_event["errors"]
  974. def test_with_default(self):
  975. self.load_trace()
  976. start, _ = self.get_start_end(1000)
  977. default_event = self.load_default()
  978. with self.feature(self.FEATURES):
  979. response = self.client.get(
  980. self.url,
  981. data={"project": -1},
  982. format="json",
  983. )
  984. assert response.status_code == 200, response.content
  985. self.assert_trace_data(response.data[0])
  986. root_event = response.data[0]
  987. assert len(root_event["errors"]) == 1
  988. assert {
  989. "event_id": default_event.event_id,
  990. "issue_id": default_event.group_id,
  991. "span": self.root_span_ids[0],
  992. "project_id": self.gen1_project.id,
  993. "project_slug": self.gen1_project.slug,
  994. "level": "debug",
  995. "title": "this is a log message",
  996. } in root_event["errors"]
  997. def test_pruning_root(self):
  998. self.load_trace()
  999. # Pruning shouldn't happen for the root event
  1000. with self.feature(self.FEATURES):
  1001. response = self.client.get(
  1002. self.url,
  1003. data={"project": -1, "event_id": self.root_event.event_id},
  1004. format="json",
  1005. )
  1006. assert response.status_code == 200, response.content
  1007. self.assert_trace_data(response.data[0])
  1008. def test_pruning_event(self):
  1009. self.load_trace()
  1010. with self.feature(self.FEATURES):
  1011. response = self.client.get(
  1012. self.url,
  1013. data={"project": -1, "event_id": self.gen2_events[0].event_id},
  1014. format="json",
  1015. )
  1016. assert response.status_code == 200, response.content
  1017. root = response.data[0]
  1018. self.assert_event(root, self.root_event, "root")
  1019. # Because of snuba query orders by timestamp we should still have all of the root's children
  1020. assert len(root["children"]) == 3
  1021. for i, gen1 in enumerate(root["children"]):
  1022. self.assert_event(gen1, self.gen1_events[i], f"gen1_{i}")
  1023. if i == 0:
  1024. assert len(gen1["children"]) == 1
  1025. gen2 = gen1["children"][0]
  1026. self.assert_event(gen2, self.gen2_events[0], "gen2_0")
  1027. assert len(gen2["children"]) == 1
  1028. gen3 = gen2["children"][0]
  1029. self.assert_event(gen3, self.gen3_event, "gen3_0")
  1030. else:
  1031. assert len(gen1["children"]) == 0
  1032. @region_silo_test
  1033. class OrganizationEventsTraceMetaEndpointTest(OrganizationEventsTraceEndpointBase):
  1034. url_name = "sentry-api-0-organization-events-trace-meta"
  1035. def test_no_projects(self):
  1036. user = self.create_user()
  1037. org = self.create_organization(owner=user)
  1038. self.login_as(user=user)
  1039. url = reverse(
  1040. self.url_name,
  1041. kwargs={"organization_slug": org.slug, "trace_id": uuid4().hex},
  1042. )
  1043. with self.feature(self.FEATURES):
  1044. response = self.client.get(
  1045. url,
  1046. format="json",
  1047. )
  1048. assert response.status_code == 404, response.content
  1049. def test_bad_ids(self):
  1050. # Fake trace id
  1051. self.url = reverse(
  1052. self.url_name,
  1053. kwargs={"organization_slug": self.project.organization.slug, "trace_id": uuid4().hex},
  1054. )
  1055. with self.feature(self.FEATURES):
  1056. response = self.client.get(
  1057. self.url,
  1058. format="json",
  1059. )
  1060. assert response.status_code == 200, response.content
  1061. data = response.data
  1062. assert data["projects"] == 0
  1063. assert data["transactions"] == 0
  1064. assert data["errors"] == 0
  1065. assert data["performance_issues"] == 0
  1066. # Invalid trace id
  1067. with pytest.raises(NoReverseMatch):
  1068. self.url = reverse(
  1069. self.url_name,
  1070. kwargs={
  1071. "organization_slug": self.project.organization.slug,
  1072. "trace_id": "not-a-trace",
  1073. },
  1074. )
  1075. def test_simple(self):
  1076. self.load_trace()
  1077. with self.feature(self.FEATURES):
  1078. response = self.client.get(
  1079. self.url,
  1080. data={"project": -1},
  1081. format="json",
  1082. )
  1083. assert response.status_code == 200, response.content
  1084. data = response.data
  1085. assert data["projects"] == 4
  1086. assert data["transactions"] == 8
  1087. assert data["errors"] == 0
  1088. assert data["performance_issues"] == 1
  1089. def test_with_errors(self):
  1090. self.load_trace()
  1091. self.load_errors()
  1092. with self.feature(self.FEATURES):
  1093. response = self.client.get(
  1094. self.url,
  1095. data={"project": -1},
  1096. format="json",
  1097. )
  1098. assert response.status_code == 200, response.content
  1099. data = response.data
  1100. assert data["projects"] == 4
  1101. assert data["transactions"] == 8
  1102. assert data["errors"] == 2
  1103. assert data["performance_issues"] == 1
  1104. def test_with_default(self):
  1105. self.load_trace()
  1106. self.load_default()
  1107. with self.feature(self.FEATURES):
  1108. response = self.client.get(
  1109. self.url,
  1110. data={"project": -1},
  1111. format="json",
  1112. )
  1113. assert response.status_code == 200, response.content
  1114. data = response.data
  1115. assert data["projects"] == 4
  1116. assert data["transactions"] == 8
  1117. assert data["errors"] == 1
  1118. assert data["performance_issues"] == 1