test_organization_events_trace.py 62 KB

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