test_organization_spans_aggregation.py 22 KB

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