test_organization_events_span_indexed.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619
  1. import uuid
  2. import pytest
  3. from tests.snuba.api.endpoints.test_organization_events import OrganizationEventsEndpointTestBase
  4. class OrganizationEventsSpanIndexedEndpointTest(OrganizationEventsEndpointTestBase):
  5. is_eap = False
  6. """Test the indexed spans dataset.
  7. To run this locally you may need to set the ENABLE_SPANS_CONSUMER flag to True in Snuba.
  8. A way to do this is
  9. 1. run: `sentry devservices down snuba`
  10. 2. clone snuba locally
  11. 3. run: `export ENABLE_SPANS_CONSUMER=True`
  12. 4. run snuba
  13. At this point tests should work locally
  14. Once span ingestion is on by default this will no longer need to be done
  15. """
  16. @property
  17. def dataset(self):
  18. if self.is_eap:
  19. return "spans"
  20. else:
  21. return "spansIndexed"
  22. def setUp(self):
  23. super().setUp()
  24. self.features = {
  25. "organizations:starfish-view": True,
  26. }
  27. @pytest.mark.querybuilder
  28. def test_simple(self):
  29. self.store_spans(
  30. [
  31. self.create_span(
  32. {"description": "foo", "sentry_tags": {"status": "success"}},
  33. start_ts=self.ten_mins_ago,
  34. ),
  35. self.create_span(
  36. {"description": "bar", "sentry_tags": {"status": "invalid_argument"}},
  37. start_ts=self.ten_mins_ago,
  38. ),
  39. ],
  40. is_eap=self.is_eap,
  41. )
  42. response = self.do_request(
  43. {
  44. "field": ["span.status", "description", "count()"],
  45. "query": "",
  46. "orderby": "description",
  47. "project": self.project.id,
  48. "dataset": self.dataset,
  49. }
  50. )
  51. assert response.status_code == 200, response.content
  52. data = response.data["data"]
  53. meta = response.data["meta"]
  54. assert len(data) == 2
  55. assert data == [
  56. {
  57. "span.status": "invalid_argument",
  58. "description": "bar",
  59. "count()": 1,
  60. },
  61. {
  62. "span.status": "ok",
  63. "description": "foo",
  64. "count()": 1,
  65. },
  66. ]
  67. assert meta["dataset"] == self.dataset
  68. def test_id_fields(self):
  69. self.store_spans(
  70. [
  71. self.create_span(
  72. {"description": "foo", "sentry_tags": {"status": "success"}},
  73. start_ts=self.ten_mins_ago,
  74. ),
  75. self.create_span(
  76. {"description": "bar", "sentry_tags": {"status": "invalid_argument"}},
  77. start_ts=self.ten_mins_ago,
  78. ),
  79. ],
  80. is_eap=self.is_eap,
  81. )
  82. response = self.do_request(
  83. {
  84. "field": ["id", "span_id"],
  85. "query": "",
  86. "orderby": "id",
  87. "project": self.project.id,
  88. "dataset": self.dataset,
  89. }
  90. )
  91. assert response.status_code == 200, response.content
  92. data = response.data["data"]
  93. meta = response.data["meta"]
  94. assert len(data) == 2
  95. for obj in data:
  96. assert obj["id"] == obj["span_id"]
  97. assert meta["dataset"] == self.dataset
  98. def test_sentry_tags_vs_tags(self):
  99. self.store_spans(
  100. [
  101. self.create_span(
  102. {"sentry_tags": {"transaction.method": "foo"}}, start_ts=self.ten_mins_ago
  103. ),
  104. ],
  105. is_eap=self.is_eap,
  106. )
  107. response = self.do_request(
  108. {
  109. "field": ["transaction.method", "count()"],
  110. "query": "",
  111. "orderby": "count()",
  112. "project": self.project.id,
  113. "dataset": self.dataset,
  114. }
  115. )
  116. assert response.status_code == 200, response.content
  117. data = response.data["data"]
  118. meta = response.data["meta"]
  119. assert len(data) == 1
  120. assert data[0]["transaction.method"] == "foo"
  121. assert meta["dataset"] == self.dataset
  122. def test_sentry_tags_syntax(self):
  123. self.store_spans(
  124. [
  125. self.create_span(
  126. {"sentry_tags": {"transaction.method": "foo"}}, start_ts=self.ten_mins_ago
  127. ),
  128. ],
  129. is_eap=self.is_eap,
  130. )
  131. response = self.do_request(
  132. {
  133. "field": ["sentry_tags[transaction.method]", "count()"],
  134. "query": "",
  135. "orderby": "count()",
  136. "project": self.project.id,
  137. "dataset": self.dataset,
  138. }
  139. )
  140. assert response.status_code == 200, response.content
  141. data = response.data["data"]
  142. meta = response.data["meta"]
  143. assert len(data) == 1
  144. assert data[0]["sentry_tags[transaction.method]"] == "foo"
  145. assert meta["dataset"] == self.dataset
  146. def test_module_alias(self):
  147. # Delegates `span.module` to `sentry_tags[category]`. Maps `"db.redis"` spans to the `"cache"` module
  148. self.store_spans(
  149. [
  150. self.create_span(
  151. {
  152. "op": "db.redis",
  153. "description": "EXEC *",
  154. "sentry_tags": {
  155. "description": "EXEC *",
  156. "category": "db",
  157. "op": "db.redis",
  158. "transaction": "/app/index",
  159. },
  160. },
  161. start_ts=self.ten_mins_ago,
  162. ),
  163. ],
  164. is_eap=self.is_eap,
  165. )
  166. response = self.do_request(
  167. {
  168. "field": ["span.module", "span.description"],
  169. "query": "span.module:cache",
  170. "project": self.project.id,
  171. "dataset": self.dataset,
  172. }
  173. )
  174. assert response.status_code == 200, response.content
  175. data = response.data["data"]
  176. meta = response.data["meta"]
  177. assert len(data) == 1
  178. assert data[0]["span.module"] == "cache"
  179. assert data[0]["span.description"] == "EXEC *"
  180. assert meta["dataset"] == self.dataset
  181. def test_device_class_filter_unknown(self):
  182. self.store_spans(
  183. [
  184. self.create_span({"sentry_tags": {"device.class": ""}}, start_ts=self.ten_mins_ago),
  185. ],
  186. is_eap=self.is_eap,
  187. )
  188. response = self.do_request(
  189. {
  190. "field": ["device.class", "count()"],
  191. "query": "device.class:Unknown",
  192. "orderby": "count()",
  193. "project": self.project.id,
  194. "dataset": self.dataset,
  195. }
  196. )
  197. assert response.status_code == 200, response.content
  198. data = response.data["data"]
  199. meta = response.data["meta"]
  200. assert len(data) == 1
  201. assert data[0]["device.class"] == "Unknown"
  202. assert meta["dataset"] == self.dataset
  203. def test_network_span(self):
  204. self.store_spans(
  205. [
  206. self.create_span(
  207. {
  208. "sentry_tags": {
  209. "action": "GET",
  210. "category": "http",
  211. "description": "GET https://*.resource.com",
  212. "domain": "*.resource.com",
  213. "op": "http.client",
  214. "status_code": "200",
  215. "transaction": "/api/0/data/",
  216. "transaction.method": "GET",
  217. "transaction.op": "http.server",
  218. }
  219. },
  220. start_ts=self.ten_mins_ago,
  221. ),
  222. ],
  223. is_eap=self.is_eap,
  224. )
  225. response = self.do_request(
  226. {
  227. "field": ["span.op", "span.status_code"],
  228. "query": "span.module:http span.status_code:200",
  229. "project": self.project.id,
  230. "dataset": self.dataset,
  231. }
  232. )
  233. assert response.status_code == 200, response.content
  234. data = response.data["data"]
  235. meta = response.data["meta"]
  236. assert len(data) == 1
  237. assert data[0]["span.op"] == "http.client"
  238. assert data[0]["span.status_code"] == "200"
  239. assert meta["dataset"] == self.dataset
  240. def test_other_category_span(self):
  241. self.store_spans(
  242. [
  243. self.create_span(
  244. {
  245. "sentry_tags": {
  246. "action": "GET",
  247. "category": "alternative",
  248. "description": "GET https://*.resource.com",
  249. "domain": "*.resource.com",
  250. "op": "alternative",
  251. "status_code": "200",
  252. "transaction": "/api/0/data/",
  253. "transaction.method": "GET",
  254. "transaction.op": "http.server",
  255. }
  256. },
  257. start_ts=self.ten_mins_ago,
  258. ),
  259. ],
  260. is_eap=self.is_eap,
  261. )
  262. response = self.do_request(
  263. {
  264. "field": ["span.op", "span.status_code"],
  265. "query": "span.module:other span.status_code:200",
  266. "project": self.project.id,
  267. "dataset": self.dataset,
  268. }
  269. )
  270. assert response.status_code == 200, response.content
  271. data = response.data["data"]
  272. meta = response.data["meta"]
  273. assert len(data) == 1
  274. assert data[0]["span.op"] == "alternative"
  275. assert data[0]["span.status_code"] == "200"
  276. assert meta["dataset"] == self.dataset
  277. def test_inp_span(self):
  278. replay_id = uuid.uuid4().hex
  279. self.store_spans(
  280. [
  281. self.create_span(
  282. {
  283. "sentry_tags": {
  284. "replay_id": replay_id,
  285. "browser.name": "Chrome",
  286. "transaction": "/pageloads/",
  287. }
  288. },
  289. start_ts=self.ten_mins_ago,
  290. ),
  291. ],
  292. is_eap=self.is_eap,
  293. )
  294. response = self.do_request(
  295. {
  296. "field": ["replay.id", "browser.name", "origin.transaction", "count()"],
  297. "query": f"replay.id:{replay_id} AND browser.name:Chrome AND origin.transaction:/pageloads/",
  298. "orderby": "count()",
  299. "project": self.project.id,
  300. "dataset": self.dataset,
  301. }
  302. )
  303. assert response.status_code == 200, response.content
  304. data = response.data["data"]
  305. meta = response.data["meta"]
  306. assert len(data) == 1
  307. assert data[0]["replay.id"] == replay_id
  308. assert data[0]["browser.name"] == "Chrome"
  309. assert data[0]["origin.transaction"] == "/pageloads/"
  310. assert meta["dataset"] == self.dataset
  311. def test_id_filtering(self):
  312. span = self.create_span({"description": "foo"}, start_ts=self.ten_mins_ago)
  313. self.store_span(span, is_eap=self.is_eap)
  314. response = self.do_request(
  315. {
  316. "field": ["description", "count()"],
  317. "query": f"id:{span['span_id']}",
  318. "orderby": "description",
  319. "project": self.project.id,
  320. "dataset": self.dataset,
  321. }
  322. )
  323. assert response.status_code == 200, response.content
  324. data = response.data["data"]
  325. meta = response.data["meta"]
  326. assert len(data) == 1
  327. assert data[0]["description"] == "foo"
  328. assert meta["dataset"] == self.dataset
  329. response = self.do_request(
  330. {
  331. "field": ["description", "count()"],
  332. "query": f"transaction.id:{span['event_id']}",
  333. "orderby": "description",
  334. "project": self.project.id,
  335. "dataset": self.dataset,
  336. }
  337. )
  338. assert response.status_code == 200, response.content
  339. data = response.data["data"]
  340. meta = response.data["meta"]
  341. assert len(data) == 1
  342. assert data[0]["description"] == "foo"
  343. assert meta["dataset"] == self.dataset
  344. def test_span_op_casing(self):
  345. self.store_spans(
  346. [
  347. self.create_span(
  348. {
  349. "sentry_tags": {
  350. "replay_id": "abc123",
  351. "browser.name": "Chrome",
  352. "transaction": "/pageloads/",
  353. "op": "this is a transaction",
  354. }
  355. },
  356. start_ts=self.ten_mins_ago,
  357. ),
  358. ],
  359. is_eap=self.is_eap,
  360. )
  361. response = self.do_request(
  362. {
  363. "field": ["span.op", "count()"],
  364. "query": 'span.op:"ThIs Is a TraNSActiON"',
  365. "orderby": "count()",
  366. "project": self.project.id,
  367. "dataset": self.dataset,
  368. }
  369. )
  370. assert response.status_code == 200, response.content
  371. data = response.data["data"]
  372. meta = response.data["meta"]
  373. assert len(data) == 1
  374. assert data[0]["span.op"] == "this is a transaction"
  375. assert meta["dataset"] == self.dataset
  376. def test_queue_span(self):
  377. self.store_spans(
  378. [
  379. self.create_span(
  380. {
  381. "measurements": {
  382. "messaging.message.body.size": {"value": 1024, "unit": "byte"},
  383. "messaging.message.receive.latency": {
  384. "value": 1000,
  385. "unit": "millisecond",
  386. },
  387. "messaging.message.retry.count": {"value": 2, "unit": "none"},
  388. },
  389. "sentry_tags": {
  390. "transaction": "queue-processor",
  391. "messaging.destination.name": "events",
  392. "messaging.message.id": "abc123",
  393. "trace.status": "ok",
  394. },
  395. },
  396. start_ts=self.ten_mins_ago,
  397. ),
  398. ],
  399. is_eap=self.is_eap,
  400. )
  401. response = self.do_request(
  402. {
  403. "field": [
  404. "transaction",
  405. "messaging.destination.name",
  406. "messaging.message.id",
  407. "measurements.messaging.message.receive.latency",
  408. "measurements.messaging.message.body.size",
  409. "measurements.messaging.message.retry.count",
  410. "trace.status",
  411. "count()",
  412. ],
  413. "query": 'messaging.destination.name:"events"',
  414. "orderby": "count()",
  415. "project": self.project.id,
  416. "dataset": self.dataset,
  417. }
  418. )
  419. assert response.status_code == 200, response.content
  420. data = response.data["data"]
  421. meta = response.data["meta"]
  422. assert len(data) == 1
  423. assert data[0]["transaction"] == "queue-processor"
  424. assert data[0]["messaging.destination.name"] == "events"
  425. assert data[0]["messaging.message.id"] == "abc123"
  426. assert data[0]["trace.status"] == "ok"
  427. assert data[0]["measurements.messaging.message.receive.latency"] == 1000
  428. assert data[0]["measurements.messaging.message.body.size"] == 1024
  429. assert data[0]["measurements.messaging.message.retry.count"] == 2
  430. assert meta["dataset"] == self.dataset
  431. def test_tag_wildcards(self):
  432. self.store_spans(
  433. [
  434. self.create_span(
  435. {"description": "foo", "tags": {"foo": "BaR"}},
  436. start_ts=self.ten_mins_ago,
  437. ),
  438. self.create_span(
  439. {"description": "qux", "tags": {"foo": "QuX"}},
  440. start_ts=self.ten_mins_ago,
  441. ),
  442. ],
  443. is_eap=self.is_eap,
  444. )
  445. for query in [
  446. "foo:b*",
  447. "foo:*r",
  448. "foo:*a*",
  449. "foo:b*r",
  450. ]:
  451. response = self.do_request(
  452. {
  453. "field": ["foo", "count()"],
  454. "query": query,
  455. "project": self.project.id,
  456. "dataset": self.dataset,
  457. }
  458. )
  459. assert response.status_code == 200, response.content
  460. assert response.data["data"] == [{"foo": "BaR", "count()": 1}]
  461. class OrganizationEventsEAPSpanEndpointTest(OrganizationEventsSpanIndexedEndpointTest):
  462. is_eap = True
  463. def test_simple(self):
  464. self.store_spans(
  465. [
  466. self.create_span(
  467. {"description": "foo", "sentry_tags": {"status": "success"}},
  468. start_ts=self.ten_mins_ago,
  469. ),
  470. self.create_span(
  471. {"description": "bar", "sentry_tags": {"status": "invalid_argument"}},
  472. start_ts=self.ten_mins_ago,
  473. ),
  474. ],
  475. is_eap=self.is_eap,
  476. )
  477. response = self.do_request(
  478. {
  479. "field": ["span.status", "description", "count()"],
  480. "query": "",
  481. "orderby": "description",
  482. "project": self.project.id,
  483. "dataset": self.dataset,
  484. }
  485. )
  486. assert response.status_code == 200, response.content
  487. data = response.data["data"]
  488. meta = response.data["meta"]
  489. assert len(data) == 2
  490. assert data == [
  491. {
  492. "span.status": "invalid_argument",
  493. "description": "bar",
  494. "count()": 1,
  495. },
  496. {
  497. "span.status": "success",
  498. "description": "foo",
  499. "count()": 1,
  500. },
  501. ]
  502. assert meta["dataset"] == self.dataset
  503. @pytest.mark.xfail(reason="event_id isn't being written to the new table")
  504. def test_id_filtering(self):
  505. super().test_id_filtering()
  506. def test_span_duration(self):
  507. self.store_spans(
  508. [
  509. self.create_span(
  510. {"description": "foo", "sentry_tags": {"status": "success"}},
  511. start_ts=self.ten_mins_ago,
  512. ),
  513. self.create_span(
  514. {"description": "bar", "sentry_tags": {"status": "invalid_argument"}},
  515. start_ts=self.ten_mins_ago,
  516. ),
  517. ],
  518. is_eap=self.is_eap,
  519. )
  520. response = self.do_request(
  521. {
  522. "field": ["span.duration", "description", "count()"],
  523. "query": "",
  524. "orderby": "description",
  525. "project": self.project.id,
  526. "dataset": self.dataset,
  527. }
  528. )
  529. assert response.status_code == 200, response.content
  530. data = response.data["data"]
  531. meta = response.data["meta"]
  532. assert len(data) == 2
  533. assert data == [
  534. {
  535. "span.duration": 1000,
  536. "description": "bar",
  537. "count()": 1,
  538. },
  539. {
  540. "span.duration": 1000,
  541. "description": "foo",
  542. "count()": 1,
  543. },
  544. ]
  545. assert meta["dataset"] == self.dataset
  546. def test_extrapolation_smoke(self):
  547. """This is a hack, we just want to make sure nothing errors from using the weighted functions"""
  548. for function in [
  549. "count_weighted()",
  550. "sum_weighted(span.duration)",
  551. "avg_weighted(span.duration)",
  552. "percentile_weighted(span.duration, 0.23)",
  553. "p50_weighted()",
  554. "p75_weighted()",
  555. "p90_weighted()",
  556. "p95_weighted()",
  557. "p99_weighted()",
  558. "p100_weighted()",
  559. "min_weighted(span.duration)",
  560. "max_weighted(span.duration)",
  561. ]:
  562. response = self.do_request(
  563. {
  564. "field": ["description", function],
  565. "query": "",
  566. "orderby": "description",
  567. "project": self.project.id,
  568. "dataset": self.dataset,
  569. }
  570. )
  571. assert response.status_code == 200, f"error: {response.content}\naggregate: {function}"