test_organization_spans_aggregation.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492
  1. import hashlib
  2. from datetime import timedelta
  3. from unittest import mock
  4. from uuid import uuid4
  5. from django.urls import reverse
  6. from sentry.api.endpoints.organization_spans_aggregation import NULL_GROUP
  7. from sentry.testutils.cases import APITestCase, SnubaTestCase
  8. from sentry.testutils.helpers.datetime import before_now
  9. from sentry.utils.samples import load_data
  10. MOCK_SNUBA_RESPONSE = {
  11. "data": [
  12. {
  13. "transaction_id": "80fe542aea4945ffbe612646987ee449",
  14. "count": 71,
  15. "spans": [
  16. [
  17. "root_1",
  18. 1,
  19. "parent_1",
  20. "e238e6c2e2466b07",
  21. "e238e6c2e2466b07",
  22. "api/0/foo",
  23. "other",
  24. "2023-09-13 17:12:19",
  25. 100,
  26. 1000,
  27. 1000,
  28. ],
  29. [
  30. "B1",
  31. 0,
  32. "root_1",
  33. "B",
  34. "B",
  35. "connect",
  36. "db",
  37. "2023-09-13 17:12:19",
  38. 150,
  39. 50,
  40. 50.0,
  41. ],
  42. [
  43. "C1",
  44. 0,
  45. "root_1",
  46. "C",
  47. "C",
  48. "resolve_conditions",
  49. "discover.endpoint",
  50. "2023-09-13 17:12:19",
  51. 155,
  52. 0,
  53. 10.0,
  54. ],
  55. [
  56. "D1",
  57. 0,
  58. "C1",
  59. "D",
  60. "D",
  61. "resolve_orderby",
  62. "discover.snql",
  63. "2023-09-13 17:12:19",
  64. 157,
  65. 0,
  66. 20.0,
  67. ],
  68. [
  69. "E1",
  70. 0,
  71. "C1",
  72. NULL_GROUP,
  73. "E",
  74. "resolve_columns",
  75. "discover.snql",
  76. "2023-09-13 17:12:19",
  77. 157,
  78. 0,
  79. 20.0,
  80. ],
  81. ],
  82. },
  83. {
  84. "transaction_id": "86b21833d1854d9b811000b91e7fccfa",
  85. "count": 71,
  86. "spans": [
  87. [
  88. "root_2",
  89. 1,
  90. "parent_2",
  91. "e238e6c2e2466b07",
  92. "e238e6c2e2466b07",
  93. "bind_organization_context",
  94. "other",
  95. "2023-09-13 17:12:39",
  96. 100,
  97. 700,
  98. 0.0,
  99. ],
  100. [
  101. "B2",
  102. 0,
  103. "root_2",
  104. "B",
  105. "B",
  106. "connect",
  107. "db",
  108. "2023-09-13 17:12:39",
  109. 110,
  110. 10,
  111. 30.0,
  112. ],
  113. [
  114. "C2",
  115. 0,
  116. "root_2",
  117. "C",
  118. "C",
  119. "resolve_conditions",
  120. "discover.endpoint",
  121. "2023-09-13 17:12:39",
  122. 115,
  123. 0,
  124. 40.0,
  125. ],
  126. [
  127. "D2",
  128. 0,
  129. "C2",
  130. "D",
  131. "D",
  132. "resolve_orderby",
  133. "discover.snql",
  134. "2023-09-13 17:12:39",
  135. 150,
  136. 0,
  137. 10.0,
  138. ],
  139. [
  140. "D2-duplicate",
  141. 0,
  142. "C2",
  143. "D",
  144. "D",
  145. "resolve_orderby",
  146. "discover.snql",
  147. "2023-09-13 17:12:40",
  148. 155,
  149. 0,
  150. 20.0,
  151. ],
  152. [
  153. "E2",
  154. 0,
  155. "C2",
  156. NULL_GROUP,
  157. "E",
  158. "resolve_columns",
  159. "discover.snql",
  160. "2023-09-13 17:12:39",
  161. 157,
  162. 0,
  163. 20.0,
  164. ],
  165. ],
  166. },
  167. ]
  168. }
  169. class OrganizationSpansAggregationTest(APITestCase, SnubaTestCase):
  170. url_name = "sentry-api-0-organization-spans-aggregation"
  171. FEATURES = [
  172. "organizations:starfish-aggregate-span-waterfall",
  173. "organizations:performance-view",
  174. ]
  175. def get_start_end(self, duration):
  176. return self.day_ago, self.day_ago + timedelta(milliseconds=duration)
  177. def create_event(
  178. self,
  179. trace,
  180. transaction,
  181. spans,
  182. parent_span_id,
  183. project_id,
  184. tags=None,
  185. duration=4000,
  186. span_id=None,
  187. measurements=None,
  188. trace_context=None,
  189. **kwargs,
  190. ):
  191. start, end = self.get_start_end(duration)
  192. data = load_data(
  193. "transaction",
  194. trace=trace,
  195. spans=spans,
  196. timestamp=end,
  197. start_timestamp=start,
  198. trace_context=trace_context,
  199. )
  200. data["transaction"] = transaction
  201. data["contexts"]["trace"]["parent_span_id"] = parent_span_id
  202. if span_id:
  203. data["contexts"]["trace"]["span_id"] = span_id
  204. if measurements:
  205. for key, value in measurements.items():
  206. data["measurements"][key]["value"] = value
  207. if tags is not None:
  208. data["tags"] = tags
  209. with self.feature(self.FEATURES):
  210. return self.store_event(data, project_id=project_id, **kwargs)
  211. def setUp(self):
  212. super().setUp()
  213. self.login_as(user=self.user)
  214. self.day_ago = before_now(days=1).replace(hour=10, minute=0, second=0, microsecond=0)
  215. self.span_ids_event_1 = dict(
  216. zip(["A", "B", "C", "D", "E"], [uuid4().hex[:16] for _ in range(5)])
  217. )
  218. self.trace_id_1 = uuid4().hex
  219. self.root_event_1 = self.create_event(
  220. trace=self.trace_id_1,
  221. trace_context={
  222. "trace_id": self.trace_id_1,
  223. "span_id": self.span_ids_event_1["A"],
  224. "exclusive_time": 100,
  225. },
  226. transaction="api/0/foo",
  227. spans=[
  228. {
  229. "same_process_as_parent": True,
  230. "op": "db",
  231. "description": "connect",
  232. "span_id": self.span_ids_event_1["B"],
  233. "trace_id": self.trace_id_1,
  234. "parent_span_id": self.span_ids_event_1["A"],
  235. "exclusive_time": 50.0,
  236. "data": {
  237. "duration": 0.050,
  238. "offset": 0.050,
  239. "span.group": "B",
  240. "span.description": "connect",
  241. },
  242. },
  243. {
  244. "same_process_as_parent": True,
  245. "op": "discover.endpoint",
  246. "description": "resolve_conditions",
  247. "span_id": self.span_ids_event_1["C"],
  248. "trace_id": self.trace_id_1,
  249. "parent_span_id": self.span_ids_event_1["A"],
  250. "exclusive_time": 10,
  251. "data": {
  252. "duration": 0.00,
  253. "offset": 0.055,
  254. "span.group": "C",
  255. "span.description": "connect",
  256. },
  257. },
  258. {
  259. "same_process_as_parent": True,
  260. "op": "discover.snql",
  261. "description": "resolve_orderby",
  262. "span_id": self.span_ids_event_1["D"],
  263. "trace_id": self.trace_id_1,
  264. "parent_span_id": self.span_ids_event_1["C"],
  265. "exclusive_time": 20,
  266. "data": {
  267. "duration": 0.00,
  268. "offset": 0.057,
  269. "span.group": "D",
  270. "span.description": "resolve_orderby",
  271. },
  272. },
  273. {
  274. "same_process_as_parent": True,
  275. "op": "discover.snql",
  276. "description": "resolve_columns",
  277. "span_id": self.span_ids_event_1["E"],
  278. "trace_id": self.trace_id_1,
  279. "parent_span_id": self.span_ids_event_1["C"],
  280. "exclusive_time": 20,
  281. "data": {
  282. "duration": 0.00,
  283. "offset": 0.057,
  284. "span.description": "resolve_columns",
  285. },
  286. },
  287. ],
  288. parent_span_id=None,
  289. project_id=self.project.id,
  290. duration=1000,
  291. )
  292. self.span_ids_event_2 = dict(
  293. zip(["A", "B", "C", "D", "D2", "E"], [uuid4().hex[:16] for _ in range(6)])
  294. )
  295. self.trace_id_2 = uuid4().hex
  296. self.root_event_2 = self.create_event(
  297. trace=self.trace_id_2,
  298. trace_context={
  299. "trace_id": self.trace_id_2,
  300. "span_id": self.span_ids_event_2["A"],
  301. "exclusive_time": 100,
  302. },
  303. transaction="api/0/foo",
  304. spans=[
  305. {
  306. "same_process_as_parent": True,
  307. "op": "db",
  308. "description": "connect",
  309. "span_id": self.span_ids_event_2["B"],
  310. "trace_id": self.trace_id_2,
  311. "parent_span_id": self.span_ids_event_2["A"],
  312. "exclusive_time": 50.0,
  313. "data": {
  314. "duration": 0.010,
  315. "offset": 0.010,
  316. "span.group": "B",
  317. "span.description": "connect",
  318. },
  319. },
  320. {
  321. "same_process_as_parent": True,
  322. "op": "discover.endpoint",
  323. "description": "resolve_conditions",
  324. "span_id": self.span_ids_event_2["C"],
  325. "trace_id": self.trace_id_2,
  326. "parent_span_id": self.span_ids_event_2["A"],
  327. "exclusive_time": 10,
  328. "data": {
  329. "duration": 0.00,
  330. "offset": 0.015,
  331. "span.group": "C",
  332. "span.description": "connect",
  333. },
  334. },
  335. {
  336. "same_process_as_parent": True,
  337. "op": "discover.snql",
  338. "description": "resolve_orderby",
  339. "span_id": self.span_ids_event_2["D"],
  340. "trace_id": self.trace_id_2,
  341. "parent_span_id": self.span_ids_event_2["C"],
  342. "exclusive_time": 10,
  343. "data": {
  344. "duration": 0.00,
  345. "offset": 0.050,
  346. "span.group": "D",
  347. "span.description": "resolve_orderby",
  348. },
  349. },
  350. {
  351. "same_process_as_parent": True,
  352. "op": "discover.snql",
  353. "description": "resolve_orderby",
  354. "span_id": self.span_ids_event_2["D2"],
  355. "trace_id": self.trace_id_2,
  356. "parent_span_id": self.span_ids_event_2["C"],
  357. "exclusive_time": 20,
  358. "data": {
  359. "duration": 0.00,
  360. "offset": 1.055,
  361. "span.group": "D",
  362. "span.description": "resolve_orderby",
  363. },
  364. },
  365. {
  366. "same_process_as_parent": True,
  367. "op": "discover.snql",
  368. "description": "resolve_columns",
  369. "span_id": self.span_ids_event_2["E"],
  370. "trace_id": self.trace_id_2,
  371. "parent_span_id": self.span_ids_event_2["C"],
  372. "exclusive_time": 20,
  373. "data": {
  374. "duration": 0.00,
  375. "offset": 0.057,
  376. "span.description": "resolve_columns",
  377. },
  378. },
  379. ],
  380. parent_span_id=None,
  381. project_id=self.project.id,
  382. duration=700,
  383. )
  384. self.url = reverse(
  385. self.url_name,
  386. kwargs={"organization_slug": self.project.organization.slug},
  387. )
  388. @mock.patch("sentry.api.endpoints.organization_spans_aggregation.raw_snql_query")
  389. def test_simple(self, mock_query):
  390. mock_query.side_effect = [MOCK_SNUBA_RESPONSE]
  391. for backend in ["indexedSpans", "nodestore"]:
  392. with self.feature(self.FEATURES):
  393. response = self.client.get(
  394. self.url,
  395. data={"transaction": "api/0/foo", "backend": backend},
  396. format="json",
  397. )
  398. assert response.data
  399. data = response.data
  400. root_fingerprint = hashlib.md5(b"e238e6c2e2466b07").hexdigest()[:16]
  401. assert root_fingerprint in data
  402. assert data[root_fingerprint]["count()"] == 2
  403. assert data[root_fingerprint]["description"] == "api/0/foo"
  404. assert round(data[root_fingerprint]["avg(duration)"]) == 850
  405. if backend == "indexedSpans":
  406. assert data[root_fingerprint]["samples"] == {
  407. ("80fe542aea4945ffbe612646987ee449", "root_1"),
  408. ("86b21833d1854d9b811000b91e7fccfa", "root_2"),
  409. }
  410. else:
  411. assert data[root_fingerprint]["samples"] == {
  412. (self.root_event_1.event_id, self.span_ids_event_1["A"]),
  413. (self.root_event_2.event_id, self.span_ids_event_2["A"]),
  414. }
  415. fingerprint = hashlib.md5(b"e238e6c2e2466b07-B").hexdigest()[:16]
  416. assert data[fingerprint]["description"] == "connect"
  417. assert round(data[fingerprint]["avg(duration)"]) == 30
  418. fingerprint = hashlib.md5(b"e238e6c2e2466b07-C-D").hexdigest()[:16]
  419. assert data[fingerprint]["description"] == "resolve_orderby"
  420. assert data[fingerprint]["avg(exclusive_time)"] == 15.0
  421. assert data[fingerprint]["count()"] == 2
  422. fingerprint = hashlib.md5(b"e238e6c2e2466b07-C-D2").hexdigest()[:16]
  423. assert data[fingerprint]["description"] == "resolve_orderby"
  424. assert data[fingerprint]["avg(exclusive_time)"] == 20.0
  425. assert data[fingerprint]["count()"] == 1
  426. @mock.patch("sentry.api.endpoints.organization_spans_aggregation.raw_snql_query")
  427. def test_offset_logic(self, mock_query):
  428. mock_query.side_effect = [MOCK_SNUBA_RESPONSE]
  429. for backend in ["indexedSpans", "nodestore"]:
  430. with self.feature(self.FEATURES):
  431. response = self.client.get(
  432. self.url,
  433. data={"transaction": "api/0/foo", "backend": backend},
  434. format="json",
  435. )
  436. assert response.data
  437. data = response.data
  438. root_fingerprint = hashlib.md5(b"e238e6c2e2466b07").hexdigest()[:16]
  439. assert root_fingerprint in data
  440. assert data[root_fingerprint]["avg(absolute_offset)"] == 0.0
  441. fingerprint = hashlib.md5(b"e238e6c2e2466b07-B").hexdigest()[:16]
  442. assert data[fingerprint]["avg(absolute_offset)"] == 30.0
  443. fingerprint = hashlib.md5(b"e238e6c2e2466b07-C").hexdigest()[:16]
  444. assert data[fingerprint]["avg(absolute_offset)"] == 35.0
  445. fingerprint = hashlib.md5(b"e238e6c2e2466b07-C-D").hexdigest()[:16]
  446. assert data[fingerprint]["avg(absolute_offset)"] == 53.5
  447. fingerprint = hashlib.md5(b"e238e6c2e2466b07-C-D2").hexdigest()[:16]
  448. assert data[fingerprint]["avg(absolute_offset)"] == 1075.0
  449. @mock.patch("sentry.api.endpoints.organization_spans_aggregation.raw_snql_query")
  450. def test_null_group_fallback(self, mock_query):
  451. mock_query.side_effect = [MOCK_SNUBA_RESPONSE]
  452. for backend in ["indexedSpans", "nodestore"]:
  453. with self.feature(self.FEATURES):
  454. response = self.client.get(
  455. self.url,
  456. data={"transaction": "api/0/foo", "backend": backend},
  457. format="json",
  458. )
  459. assert response.data
  460. data = response.data
  461. root_fingerprint = hashlib.md5(b"e238e6c2e2466b07-C-discover.snql").hexdigest()[:16]
  462. assert root_fingerprint in data
  463. assert data[root_fingerprint]["description"] == "resolve_columns"
  464. assert data[root_fingerprint]["count()"] == 2