test_organization_events_span_indexed.py 41 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191
  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. use_rpc = False
  7. """Test the indexed spans dataset.
  8. To run this locally you may need to set the ENABLE_SPANS_CONSUMER flag to True in Snuba.
  9. A way to do this is
  10. 1. run: `sentry devservices down snuba`
  11. 2. clone snuba locally
  12. 3. run: `export ENABLE_SPANS_CONSUMER=True`
  13. 4. run snuba
  14. At this point tests should work locally
  15. Once span ingestion is on by default this will no longer need to be done
  16. """
  17. @property
  18. def dataset(self):
  19. if self.is_eap:
  20. return "spans"
  21. else:
  22. return "spansIndexed"
  23. def do_request(self, query, features=None, **kwargs):
  24. query["useRpc"] = "1" if self.use_rpc else "0"
  25. return super().do_request(query, features, **kwargs)
  26. def setUp(self):
  27. super().setUp()
  28. self.features = {
  29. "organizations:starfish-view": True,
  30. }
  31. @pytest.mark.querybuilder
  32. def test_simple(self):
  33. self.store_spans(
  34. [
  35. self.create_span(
  36. {"description": "foo", "sentry_tags": {"status": "success"}},
  37. start_ts=self.ten_mins_ago,
  38. ),
  39. self.create_span(
  40. {"description": "bar", "sentry_tags": {"status": "invalid_argument"}},
  41. start_ts=self.ten_mins_ago,
  42. ),
  43. ],
  44. is_eap=self.is_eap,
  45. )
  46. response = self.do_request(
  47. {
  48. "field": ["span.status", "description", "count()"],
  49. "query": "",
  50. "orderby": "description",
  51. "project": self.project.id,
  52. "dataset": self.dataset,
  53. }
  54. )
  55. assert response.status_code == 200, response.content
  56. data = response.data["data"]
  57. meta = response.data["meta"]
  58. assert len(data) == 2
  59. assert data == [
  60. {
  61. "span.status": "invalid_argument",
  62. "description": "bar",
  63. "count()": 1,
  64. },
  65. {
  66. "span.status": "ok",
  67. "description": "foo",
  68. "count()": 1,
  69. },
  70. ]
  71. assert meta["dataset"] == self.dataset
  72. def test_id_fields(self):
  73. self.store_spans(
  74. [
  75. self.create_span(
  76. {"description": "foo", "sentry_tags": {"status": "success"}},
  77. start_ts=self.ten_mins_ago,
  78. ),
  79. self.create_span(
  80. {"description": "bar", "sentry_tags": {"status": "invalid_argument"}},
  81. start_ts=self.ten_mins_ago,
  82. ),
  83. ],
  84. is_eap=self.is_eap,
  85. )
  86. response = self.do_request(
  87. {
  88. "field": ["id", "span_id"],
  89. "query": "",
  90. "orderby": "id",
  91. "project": self.project.id,
  92. "dataset": self.dataset,
  93. }
  94. )
  95. assert response.status_code == 200, response.content
  96. data = response.data["data"]
  97. meta = response.data["meta"]
  98. assert len(data) == 2
  99. for obj in data:
  100. assert obj["id"] == obj["span_id"]
  101. assert meta["dataset"] == self.dataset
  102. def test_sentry_tags_vs_tags(self):
  103. self.store_spans(
  104. [
  105. self.create_span(
  106. {"sentry_tags": {"transaction.method": "foo"}}, start_ts=self.ten_mins_ago
  107. ),
  108. ],
  109. is_eap=self.is_eap,
  110. )
  111. response = self.do_request(
  112. {
  113. "field": ["transaction.method", "count()"],
  114. "query": "",
  115. "orderby": "count()",
  116. "project": self.project.id,
  117. "dataset": self.dataset,
  118. }
  119. )
  120. assert response.status_code == 200, response.content
  121. data = response.data["data"]
  122. meta = response.data["meta"]
  123. assert len(data) == 1
  124. assert data[0]["transaction.method"] == "foo"
  125. assert meta["dataset"] == self.dataset
  126. def test_sentry_tags_syntax(self):
  127. self.store_spans(
  128. [
  129. self.create_span(
  130. {"sentry_tags": {"transaction.method": "foo"}}, start_ts=self.ten_mins_ago
  131. ),
  132. ],
  133. is_eap=self.is_eap,
  134. )
  135. response = self.do_request(
  136. {
  137. "field": ["sentry_tags[transaction.method]", "count()"],
  138. "query": "",
  139. "orderby": "count()",
  140. "project": self.project.id,
  141. "dataset": self.dataset,
  142. }
  143. )
  144. assert response.status_code == 200, response.content
  145. data = response.data["data"]
  146. meta = response.data["meta"]
  147. assert len(data) == 1
  148. assert data[0]["sentry_tags[transaction.method]"] == "foo"
  149. assert meta["dataset"] == self.dataset
  150. def test_module_alias(self):
  151. # Delegates `span.module` to `sentry_tags[category]`. Maps `"db.redis"` spans to the `"cache"` module
  152. self.store_spans(
  153. [
  154. self.create_span(
  155. {
  156. "op": "db.redis",
  157. "description": "EXEC *",
  158. "sentry_tags": {
  159. "description": "EXEC *",
  160. "category": "db",
  161. "op": "db.redis",
  162. "transaction": "/app/index",
  163. },
  164. },
  165. start_ts=self.ten_mins_ago,
  166. ),
  167. ],
  168. is_eap=self.is_eap,
  169. )
  170. response = self.do_request(
  171. {
  172. "field": ["span.module", "span.description"],
  173. "query": "span.module:cache",
  174. "project": self.project.id,
  175. "dataset": self.dataset,
  176. }
  177. )
  178. assert response.status_code == 200, response.content
  179. data = response.data["data"]
  180. meta = response.data["meta"]
  181. assert len(data) == 1
  182. assert data[0]["span.module"] == "cache"
  183. assert data[0]["span.description"] == "EXEC *"
  184. assert meta["dataset"] == self.dataset
  185. def test_device_class_filter_unknown(self):
  186. self.store_spans(
  187. [
  188. self.create_span({"sentry_tags": {"device.class": ""}}, start_ts=self.ten_mins_ago),
  189. ],
  190. is_eap=self.is_eap,
  191. )
  192. response = self.do_request(
  193. {
  194. "field": ["device.class", "count()"],
  195. "query": "device.class:Unknown",
  196. "orderby": "count()",
  197. "project": self.project.id,
  198. "dataset": self.dataset,
  199. }
  200. )
  201. assert response.status_code == 200, response.content
  202. data = response.data["data"]
  203. meta = response.data["meta"]
  204. assert len(data) == 1
  205. assert data[0]["device.class"] == "Unknown"
  206. assert meta["dataset"] == self.dataset
  207. def test_network_span(self):
  208. self.store_spans(
  209. [
  210. self.create_span(
  211. {
  212. "sentry_tags": {
  213. "action": "GET",
  214. "category": "http",
  215. "description": "GET https://*.resource.com",
  216. "domain": "*.resource.com",
  217. "op": "http.client",
  218. "status_code": "200",
  219. "transaction": "/api/0/data/",
  220. "transaction.method": "GET",
  221. "transaction.op": "http.server",
  222. }
  223. },
  224. start_ts=self.ten_mins_ago,
  225. ),
  226. ],
  227. is_eap=self.is_eap,
  228. )
  229. response = self.do_request(
  230. {
  231. "field": ["span.op", "span.status_code"],
  232. "query": "span.module:http span.status_code:200",
  233. "project": self.project.id,
  234. "dataset": self.dataset,
  235. }
  236. )
  237. assert response.status_code == 200, response.content
  238. data = response.data["data"]
  239. meta = response.data["meta"]
  240. assert len(data) == 1
  241. assert data[0]["span.op"] == "http.client"
  242. assert data[0]["span.status_code"] == "200"
  243. assert meta["dataset"] == self.dataset
  244. def test_other_category_span(self):
  245. self.store_spans(
  246. [
  247. self.create_span(
  248. {
  249. "sentry_tags": {
  250. "action": "GET",
  251. "category": "alternative",
  252. "description": "GET https://*.resource.com",
  253. "domain": "*.resource.com",
  254. "op": "alternative",
  255. "status_code": "200",
  256. "transaction": "/api/0/data/",
  257. "transaction.method": "GET",
  258. "transaction.op": "http.server",
  259. }
  260. },
  261. start_ts=self.ten_mins_ago,
  262. ),
  263. ],
  264. is_eap=self.is_eap,
  265. )
  266. response = self.do_request(
  267. {
  268. "field": ["span.op", "span.status_code"],
  269. "query": "span.module:other span.status_code:200",
  270. "project": self.project.id,
  271. "dataset": self.dataset,
  272. }
  273. )
  274. assert response.status_code == 200, response.content
  275. data = response.data["data"]
  276. meta = response.data["meta"]
  277. assert len(data) == 1
  278. assert data[0]["span.op"] == "alternative"
  279. assert data[0]["span.status_code"] == "200"
  280. assert meta["dataset"] == self.dataset
  281. def test_inp_span(self):
  282. replay_id = uuid.uuid4().hex
  283. self.store_spans(
  284. [
  285. self.create_span(
  286. {
  287. "sentry_tags": {
  288. "replay_id": replay_id,
  289. "browser.name": "Chrome",
  290. "transaction": "/pageloads/",
  291. }
  292. },
  293. start_ts=self.ten_mins_ago,
  294. ),
  295. ],
  296. is_eap=self.is_eap,
  297. )
  298. response = self.do_request(
  299. {
  300. "field": ["replay.id", "browser.name", "origin.transaction", "count()"],
  301. "query": f"replay.id:{replay_id} AND browser.name:Chrome AND origin.transaction:/pageloads/",
  302. "orderby": "count()",
  303. "project": self.project.id,
  304. "dataset": self.dataset,
  305. }
  306. )
  307. assert response.status_code == 200, response.content
  308. data = response.data["data"]
  309. meta = response.data["meta"]
  310. assert len(data) == 1
  311. assert data[0]["replay.id"] == replay_id
  312. assert data[0]["browser.name"] == "Chrome"
  313. assert data[0]["origin.transaction"] == "/pageloads/"
  314. assert meta["dataset"] == self.dataset
  315. def test_id_filtering(self):
  316. span = self.create_span({"description": "foo"}, start_ts=self.ten_mins_ago)
  317. self.store_span(span, is_eap=self.is_eap)
  318. response = self.do_request(
  319. {
  320. "field": ["description", "count()"],
  321. "query": f"id:{span['span_id']}",
  322. "orderby": "description",
  323. "project": self.project.id,
  324. "dataset": self.dataset,
  325. }
  326. )
  327. assert response.status_code == 200, response.content
  328. data = response.data["data"]
  329. meta = response.data["meta"]
  330. assert len(data) == 1
  331. assert data[0]["description"] == "foo"
  332. assert meta["dataset"] == self.dataset
  333. response = self.do_request(
  334. {
  335. "field": ["description", "count()"],
  336. "query": f"transaction.id:{span['event_id']}",
  337. "orderby": "description",
  338. "project": self.project.id,
  339. "dataset": self.dataset,
  340. }
  341. )
  342. assert response.status_code == 200, response.content
  343. data = response.data["data"]
  344. meta = response.data["meta"]
  345. assert len(data) == 1
  346. assert data[0]["description"] == "foo"
  347. assert meta["dataset"] == self.dataset
  348. def test_span_op_casing(self):
  349. self.store_spans(
  350. [
  351. self.create_span(
  352. {
  353. "sentry_tags": {
  354. "replay_id": "abc123",
  355. "browser.name": "Chrome",
  356. "transaction": "/pageloads/",
  357. "op": "this is a transaction",
  358. }
  359. },
  360. start_ts=self.ten_mins_ago,
  361. ),
  362. ],
  363. is_eap=self.is_eap,
  364. )
  365. response = self.do_request(
  366. {
  367. "field": ["span.op", "count()"],
  368. "query": 'span.op:"ThIs Is a TraNSActiON"',
  369. "orderby": "count()",
  370. "project": self.project.id,
  371. "dataset": self.dataset,
  372. }
  373. )
  374. assert response.status_code == 200, response.content
  375. data = response.data["data"]
  376. meta = response.data["meta"]
  377. assert len(data) == 1
  378. assert data[0]["span.op"] == "this is a transaction"
  379. assert meta["dataset"] == self.dataset
  380. def test_queue_span(self):
  381. self.store_spans(
  382. [
  383. self.create_span(
  384. {
  385. "measurements": {
  386. "messaging.message.body.size": {"value": 1024, "unit": "byte"},
  387. "messaging.message.receive.latency": {
  388. "value": 1000,
  389. "unit": "millisecond",
  390. },
  391. "messaging.message.retry.count": {"value": 2, "unit": "none"},
  392. },
  393. "sentry_tags": {
  394. "transaction": "queue-processor",
  395. "messaging.destination.name": "events",
  396. "messaging.message.id": "abc123",
  397. "trace.status": "ok",
  398. },
  399. },
  400. start_ts=self.ten_mins_ago,
  401. ),
  402. ],
  403. is_eap=self.is_eap,
  404. )
  405. response = self.do_request(
  406. {
  407. "field": [
  408. "transaction",
  409. "messaging.destination.name",
  410. "messaging.message.id",
  411. "measurements.messaging.message.receive.latency",
  412. "measurements.messaging.message.body.size",
  413. "measurements.messaging.message.retry.count",
  414. "trace.status",
  415. "count()",
  416. ],
  417. "query": 'messaging.destination.name:"events"',
  418. "orderby": "count()",
  419. "project": self.project.id,
  420. "dataset": self.dataset,
  421. }
  422. )
  423. assert response.status_code == 200, response.content
  424. data = response.data["data"]
  425. meta = response.data["meta"]
  426. assert len(data) == 1
  427. assert data[0]["transaction"] == "queue-processor"
  428. assert data[0]["messaging.destination.name"] == "events"
  429. assert data[0]["messaging.message.id"] == "abc123"
  430. assert data[0]["trace.status"] == "ok"
  431. assert data[0]["measurements.messaging.message.receive.latency"] == 1000
  432. assert data[0]["measurements.messaging.message.body.size"] == 1024
  433. assert data[0]["measurements.messaging.message.retry.count"] == 2
  434. assert meta["dataset"] == self.dataset
  435. def test_tag_wildcards(self):
  436. self.store_spans(
  437. [
  438. self.create_span(
  439. {"description": "foo", "tags": {"foo": "BaR"}},
  440. start_ts=self.ten_mins_ago,
  441. ),
  442. self.create_span(
  443. {"description": "qux", "tags": {"foo": "QuX"}},
  444. start_ts=self.ten_mins_ago,
  445. ),
  446. ],
  447. is_eap=self.is_eap,
  448. )
  449. for query in [
  450. "foo:b*",
  451. "foo:*r",
  452. "foo:*a*",
  453. "foo:b*r",
  454. ]:
  455. response = self.do_request(
  456. {
  457. "field": ["foo", "count()"],
  458. "query": query,
  459. "project": self.project.id,
  460. "dataset": self.dataset,
  461. }
  462. )
  463. assert response.status_code == 200, response.content
  464. assert response.data["data"] == [{"foo": "BaR", "count()": 1}]
  465. def test_query_for_missing_tag(self):
  466. self.store_spans(
  467. [
  468. self.create_span(
  469. {"description": "foo"},
  470. start_ts=self.ten_mins_ago,
  471. ),
  472. self.create_span(
  473. {"description": "qux", "tags": {"foo": "bar"}},
  474. start_ts=self.ten_mins_ago,
  475. ),
  476. ],
  477. is_eap=self.is_eap,
  478. )
  479. response = self.do_request(
  480. {
  481. "field": ["foo", "count()"],
  482. "query": 'foo:""',
  483. "project": self.project.id,
  484. "dataset": self.dataset,
  485. }
  486. )
  487. assert response.status_code == 200, response.content
  488. assert response.data["data"] == [{"foo": "", "count()": 1}]
  489. class OrganizationEventsEAPSpanEndpointTest(OrganizationEventsSpanIndexedEndpointTest):
  490. is_eap = True
  491. use_rpc = False
  492. def test_simple(self):
  493. self.store_spans(
  494. [
  495. self.create_span(
  496. {"description": "foo", "sentry_tags": {"status": "success"}},
  497. start_ts=self.ten_mins_ago,
  498. ),
  499. self.create_span(
  500. {"description": "bar", "sentry_tags": {"status": "invalid_argument"}},
  501. start_ts=self.ten_mins_ago,
  502. ),
  503. ],
  504. is_eap=self.is_eap,
  505. )
  506. response = self.do_request(
  507. {
  508. "field": ["span.status", "description", "count()"],
  509. "query": "",
  510. "orderby": "description",
  511. "project": self.project.id,
  512. "dataset": self.dataset,
  513. }
  514. )
  515. assert response.status_code == 200, response.content
  516. data = response.data["data"]
  517. meta = response.data["meta"]
  518. assert len(data) == 2
  519. assert data == [
  520. {
  521. "span.status": "invalid_argument",
  522. "description": "bar",
  523. "count()": 1,
  524. },
  525. {
  526. "span.status": "success",
  527. "description": "foo",
  528. "count()": 1,
  529. },
  530. ]
  531. assert meta["dataset"] == self.dataset
  532. @pytest.mark.xfail(reason="event_id isn't being written to the new table")
  533. def test_id_filtering(self):
  534. super().test_id_filtering()
  535. def test_span_duration(self):
  536. spans = [
  537. self.create_span(
  538. {"description": "bar", "sentry_tags": {"status": "invalid_argument"}},
  539. start_ts=self.ten_mins_ago,
  540. ),
  541. self.create_span(
  542. {"description": "foo", "sentry_tags": {"status": "success"}},
  543. start_ts=self.ten_mins_ago,
  544. ),
  545. ]
  546. self.store_spans(spans, is_eap=self.is_eap)
  547. response = self.do_request(
  548. {
  549. "field": ["span.duration", "description"],
  550. "query": "",
  551. "orderby": "description",
  552. "project": self.project.id,
  553. "dataset": self.dataset,
  554. }
  555. )
  556. assert response.status_code == 200, response.content
  557. data = response.data["data"]
  558. meta = response.data["meta"]
  559. assert len(data) == 2
  560. assert data == [
  561. {
  562. "span.duration": 1000.0,
  563. "description": "bar",
  564. "project.name": self.project.slug,
  565. "id": spans[0]["span_id"],
  566. },
  567. {
  568. "span.duration": 1000.0,
  569. "description": "foo",
  570. "project.name": self.project.slug,
  571. "id": spans[1]["span_id"],
  572. },
  573. ]
  574. assert meta["dataset"] == self.dataset
  575. def test_aggregate_numeric_attr_weighted(self):
  576. self.store_spans(
  577. [
  578. self.create_span(
  579. {
  580. "description": "foo",
  581. "sentry_tags": {"status": "success"},
  582. "tags": {"bar": "bar1"},
  583. },
  584. start_ts=self.ten_mins_ago,
  585. ),
  586. self.create_span(
  587. {
  588. "description": "foo",
  589. "sentry_tags": {"status": "success"},
  590. "tags": {"bar": "bar2"},
  591. },
  592. measurements={"foo": {"value": 5}},
  593. start_ts=self.ten_mins_ago,
  594. ),
  595. self.create_span(
  596. {
  597. "description": "foo",
  598. "sentry_tags": {"status": "success"},
  599. "tags": {"bar": "bar3"},
  600. },
  601. start_ts=self.ten_mins_ago,
  602. ),
  603. ],
  604. is_eap=self.is_eap,
  605. )
  606. response = self.do_request(
  607. {
  608. "field": [
  609. "description",
  610. "count_unique_weighted(bar)",
  611. "count_unique_weighted(tags[bar])",
  612. "count_unique_weighted(tags[bar,string])",
  613. "count_weighted()",
  614. "count_weighted(span.duration)",
  615. "count_weighted(tags[foo, number])",
  616. "sum_weighted(tags[foo,number])",
  617. "avg_weighted(tags[foo,number])",
  618. "p50_weighted(tags[foo,number])",
  619. "p75_weighted(tags[foo,number])",
  620. "p95_weighted(tags[foo,number])",
  621. "p99_weighted(tags[foo,number])",
  622. "p100_weighted(tags[foo,number])",
  623. "min_weighted(tags[foo,number])",
  624. "max_weighted(tags[foo,number])",
  625. ],
  626. "query": "",
  627. "orderby": "description",
  628. "project": self.project.id,
  629. "dataset": self.dataset,
  630. }
  631. )
  632. assert response.status_code == 200, response.content
  633. assert len(response.data["data"]) == 1
  634. data = response.data["data"]
  635. assert data[0] == {
  636. "description": "foo",
  637. "count_unique_weighted(bar)": 3,
  638. "count_unique_weighted(tags[bar])": 3,
  639. "count_unique_weighted(tags[bar,string])": 3,
  640. "count_weighted()": 3,
  641. "count_weighted(span.duration)": 3,
  642. "count_weighted(tags[foo, number])": 1,
  643. "sum_weighted(tags[foo,number])": 5.0,
  644. "avg_weighted(tags[foo,number])": 5.0,
  645. "p50_weighted(tags[foo,number])": 5.0,
  646. "p75_weighted(tags[foo,number])": 5.0,
  647. "p95_weighted(tags[foo,number])": 5.0,
  648. "p99_weighted(tags[foo,number])": 5.0,
  649. "p100_weighted(tags[foo,number])": 5.0,
  650. "min_weighted(tags[foo,number])": 5.0,
  651. "max_weighted(tags[foo,number])": 5.0,
  652. }
  653. def test_numeric_attr_without_space(self):
  654. self.store_spans(
  655. [
  656. self.create_span(
  657. {
  658. "description": "foo",
  659. "sentry_tags": {"status": "success"},
  660. "tags": {"foo": "five"},
  661. },
  662. measurements={"foo": {"value": 5}},
  663. start_ts=self.ten_mins_ago,
  664. ),
  665. ],
  666. is_eap=self.is_eap,
  667. )
  668. response = self.do_request(
  669. {
  670. "field": ["description", "tags[foo,number]", "tags[foo,string]", "tags[foo]"],
  671. "query": "",
  672. "orderby": "description",
  673. "project": self.project.id,
  674. "dataset": self.dataset,
  675. }
  676. )
  677. assert response.status_code == 200, response.content
  678. assert len(response.data["data"]) == 1
  679. data = response.data["data"]
  680. assert data[0]["tags[foo,number]"] == 5
  681. assert data[0]["tags[foo,string]"] == "five"
  682. assert data[0]["tags[foo]"] == "five"
  683. def test_numeric_attr_with_spaces(self):
  684. self.store_spans(
  685. [
  686. self.create_span(
  687. {
  688. "description": "foo",
  689. "sentry_tags": {"status": "success"},
  690. "tags": {"foo": "five"},
  691. },
  692. measurements={"foo": {"value": 5}},
  693. start_ts=self.ten_mins_ago,
  694. ),
  695. ],
  696. is_eap=self.is_eap,
  697. )
  698. response = self.do_request(
  699. {
  700. "field": ["description", "tags[foo, number]", "tags[foo, string]", "tags[foo]"],
  701. "query": "",
  702. "orderby": "description",
  703. "project": self.project.id,
  704. "dataset": self.dataset,
  705. }
  706. )
  707. assert response.status_code == 200, response.content
  708. assert len(response.data["data"]) == 1
  709. data = response.data["data"]
  710. assert data[0]["tags[foo, number]"] == 5
  711. assert data[0]["tags[foo, string]"] == "five"
  712. assert data[0]["tags[foo]"] == "five"
  713. def test_numeric_attr_filtering(self):
  714. self.store_spans(
  715. [
  716. self.create_span(
  717. {
  718. "description": "foo",
  719. "sentry_tags": {"status": "success"},
  720. "tags": {"foo": "five"},
  721. },
  722. measurements={"foo": {"value": 5}},
  723. start_ts=self.ten_mins_ago,
  724. ),
  725. self.create_span(
  726. {"description": "bar", "sentry_tags": {"status": "success", "foo": "five"}},
  727. measurements={"foo": {"value": 8}},
  728. start_ts=self.ten_mins_ago,
  729. ),
  730. ],
  731. is_eap=self.is_eap,
  732. )
  733. response = self.do_request(
  734. {
  735. "field": ["description", "tags[foo,number]"],
  736. "query": "tags[foo,number]:5",
  737. "orderby": "description",
  738. "project": self.project.id,
  739. "dataset": self.dataset,
  740. }
  741. )
  742. assert response.status_code == 200, response.content
  743. assert len(response.data["data"]) == 1
  744. data = response.data["data"]
  745. assert data[0]["tags[foo,number]"] == 5
  746. assert data[0]["description"] == "foo"
  747. def test_long_attr_name(self):
  748. response = self.do_request(
  749. {
  750. "field": ["description", "z" * 201],
  751. "query": "",
  752. "orderby": "description",
  753. "project": self.project.id,
  754. "dataset": self.dataset,
  755. }
  756. )
  757. assert response.status_code == 400, response.content
  758. assert "Is Too Long" in response.data["detail"].title()
  759. def test_numeric_attr_orderby(self):
  760. self.store_spans(
  761. [
  762. self.create_span(
  763. {
  764. "description": "baz",
  765. "sentry_tags": {"status": "success"},
  766. "tags": {"foo": "five"},
  767. },
  768. measurements={"foo": {"value": 71}},
  769. start_ts=self.ten_mins_ago,
  770. ),
  771. self.create_span(
  772. {
  773. "description": "foo",
  774. "sentry_tags": {"status": "success"},
  775. "tags": {"foo": "five"},
  776. },
  777. measurements={"foo": {"value": 5}},
  778. start_ts=self.ten_mins_ago,
  779. ),
  780. self.create_span(
  781. {
  782. "description": "bar",
  783. "sentry_tags": {"status": "success"},
  784. "tags": {"foo": "five"},
  785. },
  786. measurements={"foo": {"value": 8}},
  787. start_ts=self.ten_mins_ago,
  788. ),
  789. ],
  790. is_eap=self.is_eap,
  791. )
  792. response = self.do_request(
  793. {
  794. "field": ["description", "tags[foo,number]"],
  795. "query": "",
  796. "orderby": ["tags[foo,number]"],
  797. "project": self.project.id,
  798. "dataset": self.dataset,
  799. }
  800. )
  801. assert response.status_code == 200, response.content
  802. assert len(response.data["data"]) == 3
  803. data = response.data["data"]
  804. assert data[0]["tags[foo,number]"] == 5
  805. assert data[0]["description"] == "foo"
  806. assert data[1]["tags[foo,number]"] == 8
  807. assert data[1]["description"] == "bar"
  808. assert data[2]["tags[foo,number]"] == 71
  809. assert data[2]["description"] == "baz"
  810. def test_aggregate_numeric_attr(self):
  811. self.store_spans(
  812. [
  813. self.create_span(
  814. {
  815. "description": "foo",
  816. "sentry_tags": {"status": "success"},
  817. "tags": {"bar": "bar1"},
  818. },
  819. start_ts=self.ten_mins_ago,
  820. ),
  821. self.create_span(
  822. {
  823. "description": "foo",
  824. "sentry_tags": {"status": "success"},
  825. "tags": {"bar": "bar2"},
  826. },
  827. measurements={"foo": {"value": 5}},
  828. start_ts=self.ten_mins_ago,
  829. ),
  830. ],
  831. is_eap=self.is_eap,
  832. )
  833. response = self.do_request(
  834. {
  835. "field": [
  836. "description",
  837. "count_unique(bar)",
  838. "count_unique(tags[bar])",
  839. "count_unique(tags[bar,string])",
  840. "count()",
  841. "count(span.duration)",
  842. "count(tags[foo, number])",
  843. "sum(tags[foo,number])",
  844. "avg(tags[foo,number])",
  845. "p50(tags[foo,number])",
  846. "p75(tags[foo,number])",
  847. "p95(tags[foo,number])",
  848. "p99(tags[foo,number])",
  849. "p100(tags[foo,number])",
  850. "min(tags[foo,number])",
  851. "max(tags[foo,number])",
  852. ],
  853. "query": "",
  854. "orderby": "description",
  855. "project": self.project.id,
  856. "dataset": self.dataset,
  857. }
  858. )
  859. assert response.status_code == 200, response.content
  860. assert len(response.data["data"]) == 1
  861. data = response.data["data"]
  862. assert data[0] == {
  863. "description": "foo",
  864. "count_unique(bar)": 2,
  865. "count_unique(tags[bar])": 2,
  866. "count_unique(tags[bar,string])": 2,
  867. "count()": 2,
  868. "count(span.duration)": 2,
  869. "count(tags[foo, number])": 1,
  870. "sum(tags[foo,number])": 5.0,
  871. "avg(tags[foo,number])": 5.0,
  872. "p50(tags[foo,number])": 5.0,
  873. "p75(tags[foo,number])": 5.0,
  874. "p95(tags[foo,number])": 5.0,
  875. "p99(tags[foo,number])": 5.0,
  876. "p100(tags[foo,number])": 5.0,
  877. "min(tags[foo,number])": 5.0,
  878. "max(tags[foo,number])": 5.0,
  879. }
  880. def test_margin_of_error(self):
  881. total_samples = 10
  882. in_group = 5
  883. spans = []
  884. for _ in range(in_group):
  885. spans.append(
  886. self.create_span(
  887. {
  888. "description": "foo",
  889. "sentry_tags": {"status": "success"},
  890. "measurements": {"client_sample_rate": {"value": 0.00001}},
  891. },
  892. start_ts=self.ten_mins_ago,
  893. )
  894. )
  895. for _ in range(total_samples - in_group):
  896. spans.append(
  897. self.create_span(
  898. {
  899. "description": "bar",
  900. "sentry_tags": {"status": "success"},
  901. "measurements": {"client_sample_rate": {"value": 0.00001}},
  902. },
  903. )
  904. )
  905. self.store_spans(
  906. spans,
  907. is_eap=self.is_eap,
  908. )
  909. response = self.do_request(
  910. {
  911. "field": [
  912. "margin_of_error()",
  913. "lower_count_limit()",
  914. "upper_count_limit()",
  915. "count_weighted()",
  916. ],
  917. "query": "description:foo",
  918. "project": self.project.id,
  919. "dataset": self.dataset,
  920. }
  921. )
  922. assert response.status_code == 200, response.content
  923. assert len(response.data["data"]) == 1
  924. data = response.data["data"][0]
  925. margin_of_error = data["margin_of_error()"]
  926. lower_limit = data["lower_count_limit()"]
  927. upper_limit = data["upper_count_limit()"]
  928. extrapolated = data["count_weighted()"]
  929. assert margin_of_error == pytest.approx(0.306, rel=1e-1)
  930. # How to read this; these results mean that the extrapolated count is
  931. # 500k, with a lower estimated bound of ~200k, and an upper bound of 800k
  932. assert lower_limit == pytest.approx(190_000, abs=5000)
  933. assert extrapolated == pytest.approx(500_000, abs=5000)
  934. assert upper_limit == pytest.approx(810_000, abs=5000)
  935. def test_skip_aggregate_conditions_option(self):
  936. span_1 = self.create_span(
  937. {"description": "foo", "sentry_tags": {"status": "success"}},
  938. start_ts=self.ten_mins_ago,
  939. )
  940. span_2 = self.create_span(
  941. {"description": "bar", "sentry_tags": {"status": "invalid_argument"}},
  942. start_ts=self.ten_mins_ago,
  943. )
  944. self.store_spans(
  945. [span_1, span_2],
  946. is_eap=self.is_eap,
  947. )
  948. response = self.do_request(
  949. {
  950. "field": ["description"],
  951. "query": "description:foo count():>1",
  952. "orderby": "description",
  953. "project": self.project.id,
  954. "dataset": self.dataset,
  955. "allowAggregateConditions": "0",
  956. }
  957. )
  958. assert response.status_code == 200, response.content
  959. data = response.data["data"]
  960. meta = response.data["meta"]
  961. assert len(data) == 1
  962. assert data == [
  963. {
  964. "description": "foo",
  965. "project.name": self.project.slug,
  966. "id": span_1["span_id"],
  967. },
  968. ]
  969. assert meta["dataset"] == self.dataset
  970. class OrganizationEventsEAPRPCSpanEndpointTest(OrganizationEventsEAPSpanEndpointTest):
  971. """These tests aren't fully passing yet, currently inheriting xfail from the eap tests"""
  972. is_eap = True
  973. use_rpc = True
  974. def test_span_duration(self):
  975. spans = [
  976. self.create_span(
  977. {"description": "bar", "sentry_tags": {"status": "invalid_argument"}},
  978. start_ts=self.ten_mins_ago,
  979. ),
  980. self.create_span(
  981. {"description": "foo", "sentry_tags": {"status": "success"}},
  982. start_ts=self.ten_mins_ago,
  983. ),
  984. ]
  985. self.store_spans(spans, is_eap=self.is_eap)
  986. response = self.do_request(
  987. {
  988. "field": ["span.duration", "description"],
  989. "query": "",
  990. "orderby": "description",
  991. "project": self.project.id,
  992. "dataset": self.dataset,
  993. }
  994. )
  995. assert response.status_code == 200, response.content
  996. data = response.data["data"]
  997. meta = response.data["meta"]
  998. assert len(data) == 2
  999. assert data == [
  1000. {
  1001. "span.duration": 1000.0,
  1002. "description": "bar",
  1003. "project.name": self.project.slug,
  1004. "id": spans[0]["span_id"],
  1005. },
  1006. {
  1007. "span.duration": 1000.0,
  1008. "description": "foo",
  1009. "project.name": self.project.slug,
  1010. "id": spans[1]["span_id"],
  1011. },
  1012. ]
  1013. assert meta["dataset"] == self.dataset
  1014. @pytest.mark.xfail(reason="extrapolation not implemented yet")
  1015. def test_aggregate_numeric_attr_weighted(self):
  1016. super().test_aggregate_numeric_attr_weighted()
  1017. @pytest.mark.xfail(reason="RPC failing because of aliasing")
  1018. def test_numeric_attr_without_space(self):
  1019. super().test_numeric_attr_without_space()
  1020. @pytest.mark.xfail(reason="RPC failing because of aliasing")
  1021. def test_numeric_attr_with_spaces(self):
  1022. super().test_numeric_attr_with_spaces()
  1023. @pytest.mark.xfail(reason="RPC failing because of aliasing")
  1024. def test_numeric_attr_filtering(self):
  1025. super().test_numeric_attr_filtering()
  1026. @pytest.mark.xfail(reason="RPC failing because of aliasing")
  1027. def test_numeric_attr_orderby(self):
  1028. super().test_numeric_attr_orderby()
  1029. def test_aggregate_numeric_attr(self):
  1030. self.store_spans(
  1031. [
  1032. self.create_span(
  1033. {
  1034. "description": "foo",
  1035. "sentry_tags": {"status": "success"},
  1036. "tags": {"bar": "bar1"},
  1037. },
  1038. start_ts=self.ten_mins_ago,
  1039. ),
  1040. self.create_span(
  1041. {
  1042. "description": "foo",
  1043. "sentry_tags": {"status": "success"},
  1044. "tags": {"bar": "bar2"},
  1045. },
  1046. measurements={"foo": {"value": 5}},
  1047. start_ts=self.ten_mins_ago,
  1048. ),
  1049. ],
  1050. is_eap=self.is_eap,
  1051. )
  1052. response = self.do_request(
  1053. {
  1054. "field": [
  1055. "description",
  1056. "count_unique(bar)",
  1057. "count_unique(tags[bar])",
  1058. "count_unique(tags[bar,string])",
  1059. "count()",
  1060. "count(span.duration)",
  1061. "count(tags[foo, number])",
  1062. "sum(tags[foo,number])",
  1063. "avg(tags[foo,number])",
  1064. "p50(tags[foo,number])",
  1065. # TODO: bring p75 back once its added to the rpc
  1066. # "p75(tags[foo,number])",
  1067. "p95(tags[foo,number])",
  1068. "p99(tags[foo,number])",
  1069. "p100(tags[foo,number])",
  1070. "min(tags[foo,number])",
  1071. "max(tags[foo,number])",
  1072. ],
  1073. "query": "",
  1074. "orderby": "description",
  1075. "project": self.project.id,
  1076. "dataset": self.dataset,
  1077. }
  1078. )
  1079. assert response.status_code == 200, response.content
  1080. assert len(response.data["data"]) == 1
  1081. data = response.data["data"]
  1082. assert data[0] == {
  1083. "description": "foo",
  1084. "count_unique(bar)": 2,
  1085. "count_unique(tags[bar])": 2,
  1086. "count_unique(tags[bar,string])": 2,
  1087. "count()": 2,
  1088. "count(span.duration)": 2,
  1089. "count(tags[foo, number])": 1,
  1090. "sum(tags[foo,number])": 5.0,
  1091. "avg(tags[foo,number])": 5.0,
  1092. "p50(tags[foo,number])": 5.0,
  1093. # TODO: bring p75 back once its added to the rpc
  1094. # "p75(tags[foo,number])": 5.0,
  1095. "p95(tags[foo,number])": 5.0,
  1096. "p99(tags[foo,number])": 5.0,
  1097. "p100(tags[foo,number])": 5.0,
  1098. "min(tags[foo,number])": 5.0,
  1099. "max(tags[foo,number])": 5.0,
  1100. }
  1101. @pytest.mark.xfail(reason="extrapolation not implemented yet")
  1102. def test_margin_of_error(self):
  1103. super().test_margin_of_error()