test_organization_spans_aggregation.py 20 KB

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