test_organization_events_span_indexed.py 36 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040
  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. def test_query_for_missing_tag(self):
  462. self.store_spans(
  463. [
  464. self.create_span(
  465. {"description": "foo"},
  466. start_ts=self.ten_mins_ago,
  467. ),
  468. self.create_span(
  469. {"description": "qux", "tags": {"foo": "bar"}},
  470. start_ts=self.ten_mins_ago,
  471. ),
  472. ],
  473. is_eap=self.is_eap,
  474. )
  475. response = self.do_request(
  476. {
  477. "field": ["foo", "count()"],
  478. "query": 'foo:""',
  479. "project": self.project.id,
  480. "dataset": self.dataset,
  481. }
  482. )
  483. assert response.status_code == 200, response.content
  484. assert response.data["data"] == [{"foo": "", "count()": 1}]
  485. @pytest.mark.xfail(
  486. reason="Snuba is not stable for the EAP dataset, xfailing since its prone to failure"
  487. )
  488. class OrganizationEventsEAPSpanEndpointTest(OrganizationEventsSpanIndexedEndpointTest):
  489. is_eap = True
  490. def test_simple(self):
  491. self.store_spans(
  492. [
  493. self.create_span(
  494. {"description": "foo", "sentry_tags": {"status": "success"}},
  495. start_ts=self.ten_mins_ago,
  496. ),
  497. self.create_span(
  498. {"description": "bar", "sentry_tags": {"status": "invalid_argument"}},
  499. start_ts=self.ten_mins_ago,
  500. ),
  501. ],
  502. is_eap=self.is_eap,
  503. )
  504. response = self.do_request(
  505. {
  506. "field": ["span.status", "description", "count()"],
  507. "query": "",
  508. "orderby": "description",
  509. "project": self.project.id,
  510. "dataset": self.dataset,
  511. }
  512. )
  513. assert response.status_code == 200, response.content
  514. data = response.data["data"]
  515. meta = response.data["meta"]
  516. assert len(data) == 2
  517. assert data == [
  518. {
  519. "span.status": "invalid_argument",
  520. "description": "bar",
  521. "count()": 1,
  522. },
  523. {
  524. "span.status": "success",
  525. "description": "foo",
  526. "count()": 1,
  527. },
  528. ]
  529. assert meta["dataset"] == self.dataset
  530. @pytest.mark.xfail(reason="event_id isn't being written to the new table")
  531. def test_id_filtering(self):
  532. super().test_id_filtering()
  533. def test_span_duration(self):
  534. spans = [
  535. self.create_span(
  536. {"description": "bar", "sentry_tags": {"status": "invalid_argument"}},
  537. start_ts=self.ten_mins_ago,
  538. ),
  539. self.create_span(
  540. {"description": "foo", "sentry_tags": {"status": "success"}},
  541. start_ts=self.ten_mins_ago,
  542. ),
  543. ]
  544. self.store_spans(spans, is_eap=self.is_eap)
  545. response = self.do_request(
  546. {
  547. "field": ["span.duration", "description"],
  548. "query": "",
  549. "orderby": "description",
  550. "project": self.project.id,
  551. "dataset": self.dataset,
  552. }
  553. )
  554. assert response.status_code == 200, response.content
  555. data = response.data["data"]
  556. meta = response.data["meta"]
  557. assert len(data) == 2
  558. assert data == [
  559. {
  560. "span.duration": 1000.0,
  561. "description": "bar",
  562. "project.name": self.project.slug,
  563. "id": spans[0]["span_id"],
  564. },
  565. {
  566. "span.duration": 1000.0,
  567. "description": "foo",
  568. "project.name": self.project.slug,
  569. "id": spans[1]["span_id"],
  570. },
  571. ]
  572. assert meta["dataset"] == self.dataset
  573. def test_aggregate_numeric_attr_weighted(self):
  574. self.store_spans(
  575. [
  576. self.create_span(
  577. {
  578. "description": "foo",
  579. "sentry_tags": {"status": "success"},
  580. "tags": {"bar": "bar1"},
  581. },
  582. start_ts=self.ten_mins_ago,
  583. ),
  584. self.create_span(
  585. {
  586. "description": "foo",
  587. "sentry_tags": {"status": "success"},
  588. "tags": {"bar": "bar2"},
  589. },
  590. measurements={"foo": {"value": 5}},
  591. start_ts=self.ten_mins_ago,
  592. ),
  593. self.create_span(
  594. {
  595. "description": "foo",
  596. "sentry_tags": {"status": "success"},
  597. "tags": {"bar": "bar3"},
  598. },
  599. start_ts=self.ten_mins_ago,
  600. ),
  601. ],
  602. is_eap=self.is_eap,
  603. )
  604. response = self.do_request(
  605. {
  606. "field": [
  607. "description",
  608. "count_unique_weighted(bar)",
  609. "count_unique_weighted(tags[bar])",
  610. "count_unique_weighted(tags[bar,string])",
  611. "count_weighted()",
  612. "count_weighted(span.duration)",
  613. "count_weighted(tags[foo, number])",
  614. "sum_weighted(tags[foo,number])",
  615. "avg_weighted(tags[foo,number])",
  616. "p50_weighted(tags[foo,number])",
  617. "p75_weighted(tags[foo,number])",
  618. "p95_weighted(tags[foo,number])",
  619. "p99_weighted(tags[foo,number])",
  620. "p100_weighted(tags[foo,number])",
  621. "min_weighted(tags[foo,number])",
  622. "max_weighted(tags[foo,number])",
  623. ],
  624. "query": "",
  625. "orderby": "description",
  626. "project": self.project.id,
  627. "dataset": self.dataset,
  628. }
  629. )
  630. assert response.status_code == 200, response.content
  631. assert len(response.data["data"]) == 1
  632. data = response.data["data"]
  633. assert data[0] == {
  634. "description": "foo",
  635. "count_unique_weighted(bar)": 3,
  636. "count_unique_weighted(tags[bar])": 3,
  637. "count_unique_weighted(tags[bar,string])": 3,
  638. "count_weighted()": 3,
  639. "count_weighted(span.duration)": 3,
  640. "count_weighted(tags[foo, number])": 1,
  641. "sum_weighted(tags[foo,number])": 5.0,
  642. "avg_weighted(tags[foo,number])": 5.0,
  643. "p50_weighted(tags[foo,number])": 5.0,
  644. "p75_weighted(tags[foo,number])": 5.0,
  645. "p95_weighted(tags[foo,number])": 5.0,
  646. "p99_weighted(tags[foo,number])": 5.0,
  647. "p100_weighted(tags[foo,number])": 5.0,
  648. "min_weighted(tags[foo,number])": 5.0,
  649. "max_weighted(tags[foo,number])": 5.0,
  650. }
  651. def test_numeric_attr_without_space(self):
  652. self.store_spans(
  653. [
  654. self.create_span(
  655. {
  656. "description": "foo",
  657. "sentry_tags": {"status": "success"},
  658. "tags": {"foo": "five"},
  659. },
  660. measurements={"foo": {"value": 5}},
  661. start_ts=self.ten_mins_ago,
  662. ),
  663. ],
  664. is_eap=self.is_eap,
  665. )
  666. response = self.do_request(
  667. {
  668. "field": ["description", "tags[foo,number]", "tags[foo,string]", "tags[foo]"],
  669. "query": "",
  670. "orderby": "description",
  671. "project": self.project.id,
  672. "dataset": self.dataset,
  673. }
  674. )
  675. assert response.status_code == 200, response.content
  676. assert len(response.data["data"]) == 1
  677. data = response.data["data"]
  678. assert data[0]["tags[foo,number]"] == 5
  679. assert data[0]["tags[foo,string]"] == "five"
  680. assert data[0]["tags[foo]"] == "five"
  681. def test_numeric_attr_with_spaces(self):
  682. self.store_spans(
  683. [
  684. self.create_span(
  685. {
  686. "description": "foo",
  687. "sentry_tags": {"status": "success"},
  688. "tags": {"foo": "five"},
  689. },
  690. measurements={"foo": {"value": 5}},
  691. start_ts=self.ten_mins_ago,
  692. ),
  693. ],
  694. is_eap=self.is_eap,
  695. )
  696. response = self.do_request(
  697. {
  698. "field": ["description", "tags[foo, number]", "tags[foo, string]", "tags[foo]"],
  699. "query": "",
  700. "orderby": "description",
  701. "project": self.project.id,
  702. "dataset": self.dataset,
  703. }
  704. )
  705. assert response.status_code == 200, response.content
  706. assert len(response.data["data"]) == 1
  707. data = response.data["data"]
  708. assert data[0]["tags[foo, number]"] == 5
  709. assert data[0]["tags[foo, string]"] == "five"
  710. assert data[0]["tags[foo]"] == "five"
  711. def test_numeric_attr_filtering(self):
  712. self.store_spans(
  713. [
  714. self.create_span(
  715. {
  716. "description": "foo",
  717. "sentry_tags": {"status": "success"},
  718. "tags": {"foo": "five"},
  719. },
  720. measurements={"foo": {"value": 5}},
  721. start_ts=self.ten_mins_ago,
  722. ),
  723. self.create_span(
  724. {"description": "bar", "sentry_tags": {"status": "success", "foo": "five"}},
  725. measurements={"foo": {"value": 8}},
  726. start_ts=self.ten_mins_ago,
  727. ),
  728. ],
  729. is_eap=self.is_eap,
  730. )
  731. response = self.do_request(
  732. {
  733. "field": ["description", "tags[foo,number]"],
  734. "query": "tags[foo,number]:5",
  735. "orderby": "description",
  736. "project": self.project.id,
  737. "dataset": self.dataset,
  738. }
  739. )
  740. assert response.status_code == 200, response.content
  741. assert len(response.data["data"]) == 1
  742. data = response.data["data"]
  743. assert data[0]["tags[foo,number]"] == 5
  744. assert data[0]["description"] == "foo"
  745. def test_long_attr_name(self):
  746. response = self.do_request(
  747. {
  748. "field": ["description", "z" * 201],
  749. "query": "",
  750. "orderby": "description",
  751. "project": self.project.id,
  752. "dataset": self.dataset,
  753. }
  754. )
  755. assert response.status_code == 400, response.content
  756. assert "Is Too Long" in response.data["detail"].title()
  757. def test_numeric_attr_orderby(self):
  758. self.store_spans(
  759. [
  760. self.create_span(
  761. {
  762. "description": "baz",
  763. "sentry_tags": {"status": "success"},
  764. "tags": {"foo": "five"},
  765. },
  766. measurements={"foo": {"value": 71}},
  767. start_ts=self.ten_mins_ago,
  768. ),
  769. self.create_span(
  770. {
  771. "description": "foo",
  772. "sentry_tags": {"status": "success"},
  773. "tags": {"foo": "five"},
  774. },
  775. measurements={"foo": {"value": 5}},
  776. start_ts=self.ten_mins_ago,
  777. ),
  778. self.create_span(
  779. {
  780. "description": "bar",
  781. "sentry_tags": {"status": "success"},
  782. "tags": {"foo": "five"},
  783. },
  784. measurements={"foo": {"value": 8}},
  785. start_ts=self.ten_mins_ago,
  786. ),
  787. ],
  788. is_eap=self.is_eap,
  789. )
  790. response = self.do_request(
  791. {
  792. "field": ["description", "tags[foo,number]"],
  793. "query": "",
  794. "orderby": ["tags[foo,number]"],
  795. "project": self.project.id,
  796. "dataset": self.dataset,
  797. }
  798. )
  799. assert response.status_code == 200, response.content
  800. assert len(response.data["data"]) == 3
  801. data = response.data["data"]
  802. assert data[0]["tags[foo,number]"] == 5
  803. assert data[0]["description"] == "foo"
  804. assert data[1]["tags[foo,number]"] == 8
  805. assert data[1]["description"] == "bar"
  806. assert data[2]["tags[foo,number]"] == 71
  807. assert data[2]["description"] == "baz"
  808. def test_aggregate_numeric_attr(self):
  809. self.store_spans(
  810. [
  811. self.create_span(
  812. {
  813. "description": "foo",
  814. "sentry_tags": {"status": "success"},
  815. "tags": {"bar": "bar1"},
  816. },
  817. start_ts=self.ten_mins_ago,
  818. ),
  819. self.create_span(
  820. {
  821. "description": "foo",
  822. "sentry_tags": {"status": "success"},
  823. "tags": {"bar": "bar2"},
  824. },
  825. measurements={"foo": {"value": 5}},
  826. start_ts=self.ten_mins_ago,
  827. ),
  828. ],
  829. is_eap=self.is_eap,
  830. )
  831. response = self.do_request(
  832. {
  833. "field": [
  834. "description",
  835. "count_unique(bar)",
  836. "count_unique(tags[bar])",
  837. "count_unique(tags[bar,string])",
  838. "count()",
  839. "count(span.duration)",
  840. "count(tags[foo, number])",
  841. "sum(tags[foo,number])",
  842. "avg(tags[foo,number])",
  843. "p50(tags[foo,number])",
  844. "p75(tags[foo,number])",
  845. "p95(tags[foo,number])",
  846. "p99(tags[foo,number])",
  847. "p100(tags[foo,number])",
  848. "min(tags[foo,number])",
  849. "max(tags[foo,number])",
  850. ],
  851. "query": "",
  852. "orderby": "description",
  853. "project": self.project.id,
  854. "dataset": self.dataset,
  855. }
  856. )
  857. assert response.status_code == 200, response.content
  858. assert len(response.data["data"]) == 1
  859. data = response.data["data"]
  860. assert data[0] == {
  861. "description": "foo",
  862. "count_unique(bar)": 2,
  863. "count_unique(tags[bar])": 2,
  864. "count_unique(tags[bar,string])": 2,
  865. "count()": 2,
  866. "count(span.duration)": 2,
  867. "count(tags[foo, number])": 1,
  868. "sum(tags[foo,number])": 5.0,
  869. "avg(tags[foo,number])": 5.0,
  870. "p50(tags[foo,number])": 5.0,
  871. "p75(tags[foo,number])": 5.0,
  872. "p95(tags[foo,number])": 5.0,
  873. "p99(tags[foo,number])": 5.0,
  874. "p100(tags[foo,number])": 5.0,
  875. "min(tags[foo,number])": 5.0,
  876. "max(tags[foo,number])": 5.0,
  877. }
  878. def test_margin_of_error(self):
  879. total_samples = 10
  880. in_group = 5
  881. spans = []
  882. for _ in range(in_group):
  883. spans.append(
  884. self.create_span(
  885. {
  886. "description": "foo",
  887. "sentry_tags": {"status": "success"},
  888. "measurements": {"client_sample_rate": {"value": 0.00001}},
  889. },
  890. start_ts=self.ten_mins_ago,
  891. )
  892. )
  893. for _ in range(total_samples - in_group):
  894. spans.append(
  895. self.create_span(
  896. {
  897. "description": "bar",
  898. "sentry_tags": {"status": "success"},
  899. "measurements": {"client_sample_rate": {"value": 0.00001}},
  900. },
  901. )
  902. )
  903. self.store_spans(
  904. spans,
  905. is_eap=self.is_eap,
  906. )
  907. response = self.do_request(
  908. {
  909. "field": [
  910. "margin_of_error()",
  911. "lower_count_limit()",
  912. "upper_count_limit()",
  913. "count_weighted()",
  914. ],
  915. "query": "description:foo",
  916. "project": self.project.id,
  917. "dataset": self.dataset,
  918. }
  919. )
  920. assert response.status_code == 200, response.content
  921. assert len(response.data["data"]) == 1
  922. data = response.data["data"][0]
  923. margin_of_error = data["margin_of_error()"]
  924. lower_limit = data["lower_count_limit()"]
  925. upper_limit = data["upper_count_limit()"]
  926. extrapolated = data["count_weighted()"]
  927. assert margin_of_error == pytest.approx(0.306, rel=1e-1)
  928. # How to read this; these results mean that the extrapolated count is
  929. # 500k, with a lower estimated bound of ~200k, and an upper bound of 800k
  930. assert lower_limit == pytest.approx(190_000, abs=5000)
  931. assert extrapolated == pytest.approx(500_000)
  932. assert upper_limit == pytest.approx(810_000, abs=5000)
  933. def test_skip_aggregate_conditions_option(self):
  934. span_1 = self.create_span(
  935. {"description": "foo", "sentry_tags": {"status": "success"}},
  936. start_ts=self.ten_mins_ago,
  937. )
  938. span_2 = self.create_span(
  939. {"description": "bar", "sentry_tags": {"status": "invalid_argument"}},
  940. start_ts=self.ten_mins_ago,
  941. )
  942. self.store_spans(
  943. [span_1, span_2],
  944. is_eap=self.is_eap,
  945. )
  946. response = self.do_request(
  947. {
  948. "field": ["description"],
  949. "query": "description:foo count():>1",
  950. "orderby": "description",
  951. "project": self.project.id,
  952. "dataset": self.dataset,
  953. "allowAggregateConditions": "0",
  954. }
  955. )
  956. assert response.status_code == 200, response.content
  957. data = response.data["data"]
  958. meta = response.data["meta"]
  959. assert len(data) == 1
  960. assert data == [
  961. {
  962. "description": "foo",
  963. "project.name": self.project.slug,
  964. "id": span_1["span_id"],
  965. },
  966. ]
  967. assert meta["dataset"] == self.dataset