test_organization_events_span_indexed.py 62 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823
  1. import uuid
  2. from datetime import datetime, timezone
  3. from unittest import mock
  4. import pytest
  5. from tests.snuba.api.endpoints.test_organization_events import OrganizationEventsEndpointTestBase
  6. class OrganizationEventsSpanIndexedEndpointTest(OrganizationEventsEndpointTestBase):
  7. is_eap = False
  8. use_rpc = False
  9. """Test the indexed spans dataset.
  10. To run this locally you may need to set the ENABLE_SPANS_CONSUMER flag to True in Snuba.
  11. A way to do this is
  12. 1. run: `sentry devservices down snuba`
  13. 2. clone snuba locally
  14. 3. run: `export ENABLE_SPANS_CONSUMER=True`
  15. 4. run snuba
  16. At this point tests should work locally
  17. Once span ingestion is on by default this will no longer need to be done
  18. """
  19. @property
  20. def dataset(self):
  21. if self.is_eap:
  22. return "spans"
  23. else:
  24. return "spansIndexed"
  25. def do_request(self, query, features=None, **kwargs):
  26. query["useRpc"] = "1" if self.use_rpc else "0"
  27. return super().do_request(query, features, **kwargs)
  28. def setUp(self):
  29. super().setUp()
  30. self.features = {
  31. "organizations:starfish-view": True,
  32. }
  33. @pytest.mark.querybuilder
  34. def test_simple(self):
  35. self.store_spans(
  36. [
  37. self.create_span(
  38. {"description": "foo", "sentry_tags": {"status": "success"}},
  39. start_ts=self.ten_mins_ago,
  40. ),
  41. self.create_span(
  42. {"description": "bar", "sentry_tags": {"status": "invalid_argument"}},
  43. start_ts=self.ten_mins_ago,
  44. ),
  45. ],
  46. is_eap=self.is_eap,
  47. )
  48. response = self.do_request(
  49. {
  50. "field": ["span.status", "description", "count()"],
  51. "query": "",
  52. "orderby": "description",
  53. "project": self.project.id,
  54. "dataset": self.dataset,
  55. }
  56. )
  57. assert response.status_code == 200, response.content
  58. data = response.data["data"]
  59. meta = response.data["meta"]
  60. assert len(data) == 2
  61. assert data == [
  62. {
  63. "span.status": "invalid_argument",
  64. "description": "bar",
  65. "count()": 1,
  66. },
  67. {
  68. "span.status": "ok",
  69. "description": "foo",
  70. "count()": 1,
  71. },
  72. ]
  73. assert meta["dataset"] == self.dataset
  74. def test_spm(self):
  75. self.store_spans(
  76. [
  77. self.create_span(
  78. {"description": "foo", "sentry_tags": {"status": "success"}},
  79. start_ts=self.ten_mins_ago,
  80. ),
  81. ],
  82. is_eap=self.is_eap,
  83. )
  84. response = self.do_request(
  85. {
  86. "field": ["description", "spm()"],
  87. "query": "",
  88. "orderby": "description",
  89. "project": self.project.id,
  90. "dataset": self.dataset,
  91. }
  92. )
  93. assert response.status_code == 200, response.content
  94. data = response.data["data"]
  95. meta = response.data["meta"]
  96. assert len(data) == 1
  97. assert data == [
  98. {
  99. "description": "foo",
  100. "spm()": 1 / (90 * 24 * 60),
  101. },
  102. ]
  103. assert meta["dataset"] == self.dataset
  104. def test_id_fields(self):
  105. self.store_spans(
  106. [
  107. self.create_span(
  108. {"description": "foo", "sentry_tags": {"status": "success"}},
  109. start_ts=self.ten_mins_ago,
  110. ),
  111. self.create_span(
  112. {"description": "bar", "sentry_tags": {"status": "invalid_argument"}},
  113. start_ts=self.ten_mins_ago,
  114. ),
  115. ],
  116. is_eap=self.is_eap,
  117. )
  118. response = self.do_request(
  119. {
  120. "field": ["id", "span_id"],
  121. "query": "",
  122. "orderby": "id",
  123. "project": self.project.id,
  124. "dataset": self.dataset,
  125. }
  126. )
  127. assert response.status_code == 200, response.content
  128. data = response.data["data"]
  129. meta = response.data["meta"]
  130. assert len(data) == 2
  131. for obj in data:
  132. assert obj["id"] == obj["span_id"]
  133. assert meta["dataset"] == self.dataset
  134. def test_sentry_tags_vs_tags(self):
  135. self.store_spans(
  136. [
  137. self.create_span(
  138. {"sentry_tags": {"transaction.method": "foo"}}, start_ts=self.ten_mins_ago
  139. ),
  140. ],
  141. is_eap=self.is_eap,
  142. )
  143. response = self.do_request(
  144. {
  145. "field": ["transaction.method", "count()"],
  146. "query": "",
  147. "orderby": "count()",
  148. "project": self.project.id,
  149. "dataset": self.dataset,
  150. }
  151. )
  152. assert response.status_code == 200, response.content
  153. data = response.data["data"]
  154. meta = response.data["meta"]
  155. assert len(data) == 1
  156. assert data[0]["transaction.method"] == "foo"
  157. assert meta["dataset"] == self.dataset
  158. def test_sentry_tags_syntax(self):
  159. self.store_spans(
  160. [
  161. self.create_span(
  162. {"sentry_tags": {"transaction.method": "foo"}}, start_ts=self.ten_mins_ago
  163. ),
  164. ],
  165. is_eap=self.is_eap,
  166. )
  167. response = self.do_request(
  168. {
  169. "field": ["sentry_tags[transaction.method]", "count()"],
  170. "query": "",
  171. "orderby": "count()",
  172. "project": self.project.id,
  173. "dataset": self.dataset,
  174. }
  175. )
  176. assert response.status_code == 200, response.content
  177. data = response.data["data"]
  178. meta = response.data["meta"]
  179. assert len(data) == 1
  180. assert data[0]["sentry_tags[transaction.method]"] == "foo"
  181. assert meta["dataset"] == self.dataset
  182. def test_module_alias(self):
  183. # Delegates `span.module` to `sentry_tags[category]`. Maps `"db.redis"` spans to the `"cache"` module
  184. self.store_spans(
  185. [
  186. self.create_span(
  187. {
  188. "op": "db.redis",
  189. "description": "EXEC *",
  190. "sentry_tags": {
  191. "description": "EXEC *",
  192. "category": "db",
  193. "op": "db.redis",
  194. "transaction": "/app/index",
  195. },
  196. },
  197. start_ts=self.ten_mins_ago,
  198. ),
  199. ],
  200. is_eap=self.is_eap,
  201. )
  202. response = self.do_request(
  203. {
  204. "field": ["span.module", "span.description"],
  205. "query": "span.module:cache",
  206. "project": self.project.id,
  207. "dataset": self.dataset,
  208. }
  209. )
  210. assert response.status_code == 200, response.content
  211. data = response.data["data"]
  212. meta = response.data["meta"]
  213. assert len(data) == 1
  214. assert data[0]["span.module"] == "cache"
  215. assert data[0]["span.description"] == "EXEC *"
  216. assert meta["dataset"] == self.dataset
  217. def test_device_class_filter_unknown(self):
  218. self.store_spans(
  219. [
  220. self.create_span({"sentry_tags": {"device.class": ""}}, start_ts=self.ten_mins_ago),
  221. ],
  222. is_eap=self.is_eap,
  223. )
  224. response = self.do_request(
  225. {
  226. "field": ["device.class", "count()"],
  227. "query": "device.class:Unknown",
  228. "orderby": "count()",
  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]["device.class"] == "Unknown"
  238. assert meta["dataset"] == self.dataset
  239. def test_span_module(self):
  240. self.store_spans(
  241. [
  242. self.create_span(
  243. {
  244. "sentry_tags": {
  245. "op": "http",
  246. "category": "http",
  247. }
  248. },
  249. start_ts=self.ten_mins_ago,
  250. ),
  251. self.create_span(
  252. {
  253. "sentry_tags": {
  254. "op": "alternative",
  255. "category": "other",
  256. }
  257. },
  258. start_ts=self.ten_mins_ago,
  259. ),
  260. self.create_span(
  261. {
  262. "sentry_tags": {
  263. "op": "alternative",
  264. "category": "other",
  265. }
  266. },
  267. start_ts=self.ten_mins_ago,
  268. ),
  269. ],
  270. is_eap=self.is_eap,
  271. )
  272. response = self.do_request(
  273. {
  274. "field": ["span.module", "count()"],
  275. "query": "",
  276. "orderby": "-count()",
  277. "project": self.project.id,
  278. "dataset": self.dataset,
  279. }
  280. )
  281. assert response.status_code == 200, response.content
  282. data = response.data["data"]
  283. meta = response.data["meta"]
  284. assert len(data) == 2
  285. assert data[0]["span.module"] == "other"
  286. assert data[1]["span.module"] == "http"
  287. assert meta["dataset"] == self.dataset
  288. def test_network_span(self):
  289. self.store_spans(
  290. [
  291. self.create_span(
  292. {
  293. "sentry_tags": {
  294. "action": "GET",
  295. "category": "http",
  296. "description": "GET https://*.resource.com",
  297. "domain": "*.resource.com",
  298. "op": "http.client",
  299. "status_code": "200",
  300. "transaction": "/api/0/data/",
  301. "transaction.method": "GET",
  302. "transaction.op": "http.server",
  303. }
  304. },
  305. start_ts=self.ten_mins_ago,
  306. ),
  307. ],
  308. is_eap=self.is_eap,
  309. )
  310. response = self.do_request(
  311. {
  312. "field": ["span.op", "span.status_code"],
  313. "query": "span.status_code:200",
  314. "project": self.project.id,
  315. "dataset": self.dataset,
  316. }
  317. )
  318. assert response.status_code == 200, response.content
  319. data = response.data["data"]
  320. meta = response.data["meta"]
  321. assert len(data) == 1
  322. assert data[0]["span.op"] == "http.client"
  323. assert data[0]["span.status_code"] == "200"
  324. assert meta["dataset"] == self.dataset
  325. def test_other_category_span(self):
  326. self.store_spans(
  327. [
  328. self.create_span(
  329. {
  330. "sentry_tags": {
  331. "action": "GET",
  332. "category": "alternative",
  333. "description": "GET https://*.resource.com",
  334. "domain": "*.resource.com",
  335. "op": "alternative",
  336. "status_code": "200",
  337. "transaction": "/api/0/data/",
  338. "transaction.method": "GET",
  339. "transaction.op": "http.server",
  340. }
  341. },
  342. start_ts=self.ten_mins_ago,
  343. ),
  344. ],
  345. is_eap=self.is_eap,
  346. )
  347. response = self.do_request(
  348. {
  349. "field": ["span.op", "span.status_code"],
  350. "query": "span.module:other span.status_code:200",
  351. "project": self.project.id,
  352. "dataset": self.dataset,
  353. }
  354. )
  355. assert response.status_code == 200, response.content
  356. data = response.data["data"]
  357. meta = response.data["meta"]
  358. assert len(data) == 1
  359. assert data[0]["span.op"] == "alternative"
  360. assert data[0]["span.status_code"] == "200"
  361. assert meta["dataset"] == self.dataset
  362. def test_inp_span(self):
  363. replay_id = uuid.uuid4().hex
  364. self.store_spans(
  365. [
  366. self.create_span(
  367. {
  368. "sentry_tags": {
  369. "replay_id": replay_id,
  370. "browser.name": "Chrome",
  371. "transaction": "/pageloads/",
  372. }
  373. },
  374. start_ts=self.ten_mins_ago,
  375. ),
  376. ],
  377. is_eap=self.is_eap,
  378. )
  379. response = self.do_request(
  380. {
  381. "field": ["replay.id", "browser.name", "origin.transaction", "count()"],
  382. "query": f"replay.id:{replay_id} AND browser.name:Chrome AND origin.transaction:/pageloads/",
  383. "orderby": "count()",
  384. "project": self.project.id,
  385. "dataset": self.dataset,
  386. }
  387. )
  388. assert response.status_code == 200, response.content
  389. data = response.data["data"]
  390. meta = response.data["meta"]
  391. assert len(data) == 1
  392. assert data[0]["replay.id"] == replay_id
  393. assert data[0]["browser.name"] == "Chrome"
  394. assert data[0]["origin.transaction"] == "/pageloads/"
  395. assert meta["dataset"] == self.dataset
  396. def test_id_filtering(self):
  397. span = self.create_span({"description": "foo"}, start_ts=self.ten_mins_ago)
  398. self.store_span(span, is_eap=self.is_eap)
  399. response = self.do_request(
  400. {
  401. "field": ["description", "count()"],
  402. "query": f"id:{span['span_id']}",
  403. "orderby": "description",
  404. "project": self.project.id,
  405. "dataset": self.dataset,
  406. }
  407. )
  408. assert response.status_code == 200, response.content
  409. data = response.data["data"]
  410. meta = response.data["meta"]
  411. assert len(data) == 1
  412. assert data[0]["description"] == "foo"
  413. assert meta["dataset"] == self.dataset
  414. response = self.do_request(
  415. {
  416. "field": ["description", "count()"],
  417. "query": f"transaction.id:{span['event_id']}",
  418. "orderby": "description",
  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]["description"] == "foo"
  428. assert meta["dataset"] == self.dataset
  429. def test_span_op_casing(self):
  430. self.store_spans(
  431. [
  432. self.create_span(
  433. {
  434. "sentry_tags": {
  435. "replay_id": "abc123",
  436. "browser.name": "Chrome",
  437. "transaction": "/pageloads/",
  438. "op": "this is a transaction",
  439. }
  440. },
  441. start_ts=self.ten_mins_ago,
  442. ),
  443. ],
  444. is_eap=self.is_eap,
  445. )
  446. response = self.do_request(
  447. {
  448. "field": ["span.op", "count()"],
  449. "query": 'span.op:"ThIs Is a TraNSActiON"',
  450. "orderby": "count()",
  451. "project": self.project.id,
  452. "dataset": self.dataset,
  453. }
  454. )
  455. assert response.status_code == 200, response.content
  456. data = response.data["data"]
  457. meta = response.data["meta"]
  458. assert len(data) == 1
  459. assert data[0]["span.op"] == "this is a transaction"
  460. assert meta["dataset"] == self.dataset
  461. def test_queue_span(self):
  462. self.store_spans(
  463. [
  464. self.create_span(
  465. {
  466. "measurements": {
  467. "messaging.message.body.size": {"value": 1024, "unit": "byte"},
  468. "messaging.message.receive.latency": {
  469. "value": 1000,
  470. "unit": "millisecond",
  471. },
  472. "messaging.message.retry.count": {"value": 2, "unit": "none"},
  473. },
  474. "sentry_tags": {
  475. "transaction": "queue-processor",
  476. "messaging.destination.name": "events",
  477. "messaging.message.id": "abc123",
  478. "trace.status": "ok",
  479. },
  480. },
  481. start_ts=self.ten_mins_ago,
  482. ),
  483. ],
  484. is_eap=self.is_eap,
  485. )
  486. response = self.do_request(
  487. {
  488. "field": [
  489. "transaction",
  490. "messaging.destination.name",
  491. "messaging.message.id",
  492. "measurements.messaging.message.receive.latency",
  493. "measurements.messaging.message.body.size",
  494. "measurements.messaging.message.retry.count",
  495. "trace.status",
  496. "count()",
  497. ],
  498. "query": 'messaging.destination.name:"events"',
  499. "orderby": "count()",
  500. "project": self.project.id,
  501. "dataset": self.dataset,
  502. }
  503. )
  504. assert response.status_code == 200, response.content
  505. data = response.data["data"]
  506. meta = response.data["meta"]
  507. assert len(data) == 1
  508. assert data[0]["transaction"] == "queue-processor"
  509. assert data[0]["messaging.destination.name"] == "events"
  510. assert data[0]["messaging.message.id"] == "abc123"
  511. assert data[0]["trace.status"] == "ok"
  512. assert data[0]["measurements.messaging.message.receive.latency"] == 1000
  513. assert data[0]["measurements.messaging.message.body.size"] == 1024
  514. assert data[0]["measurements.messaging.message.retry.count"] == 2
  515. assert meta["dataset"] == self.dataset
  516. def test_tag_wildcards(self):
  517. self.store_spans(
  518. [
  519. self.create_span(
  520. {"description": "foo", "tags": {"foo": "BaR"}},
  521. start_ts=self.ten_mins_ago,
  522. ),
  523. self.create_span(
  524. {"description": "qux", "tags": {"foo": "QuX"}},
  525. start_ts=self.ten_mins_ago,
  526. ),
  527. ],
  528. is_eap=self.is_eap,
  529. )
  530. for query in [
  531. "foo:b*",
  532. "foo:*r",
  533. "foo:*a*",
  534. "foo:b*r",
  535. ]:
  536. response = self.do_request(
  537. {
  538. "field": ["foo", "count()"],
  539. "query": query,
  540. "project": self.project.id,
  541. "dataset": self.dataset,
  542. }
  543. )
  544. assert response.status_code == 200, response.content
  545. assert response.data["data"] == [{"foo": "BaR", "count()": 1}]
  546. def test_query_for_missing_tag(self):
  547. self.store_spans(
  548. [
  549. self.create_span(
  550. {"description": "foo"},
  551. start_ts=self.ten_mins_ago,
  552. ),
  553. self.create_span(
  554. {"description": "qux", "tags": {"foo": "bar"}},
  555. start_ts=self.ten_mins_ago,
  556. ),
  557. ],
  558. is_eap=self.is_eap,
  559. )
  560. response = self.do_request(
  561. {
  562. "field": ["foo", "count()"],
  563. "query": 'foo:""',
  564. "project": self.project.id,
  565. "dataset": self.dataset,
  566. }
  567. )
  568. assert response.status_code == 200, response.content
  569. assert response.data["data"] == [{"foo": "", "count()": 1}]
  570. @pytest.mark.xfail
  571. def test_count_field_type(self):
  572. response = self.do_request(
  573. {
  574. "field": ["count()"],
  575. "project": self.project.id,
  576. "dataset": self.dataset,
  577. }
  578. )
  579. assert response.status_code == 200, response.content
  580. assert response.data["meta"]["fields"] == {"count()": "integer"}
  581. assert response.data["meta"]["units"] == {"count()": None}
  582. assert response.data["data"] == [{"count()": 0}]
  583. def test_simple_measurements(self):
  584. keys = [
  585. ("app_start_cold", "duration", "millisecond"),
  586. ("app_start_warm", "duration", "millisecond"),
  587. ("frames_frozen", "number", None), # should be integer but keeping it consistent
  588. ("frames_frozen_rate", "percentage", None),
  589. ("frames_slow", "number", None), # should be integer but keeping it consistent
  590. ("frames_slow_rate", "percentage", None),
  591. ("frames_total", "number", None), # should be integer but keeping it consistent
  592. ("time_to_initial_display", "duration", "millisecond"),
  593. ("time_to_full_display", "duration", "millisecond"),
  594. ("stall_count", "number", None), # should be integer but keeping it consistent
  595. ("stall_percentage", "percentage", None),
  596. ("stall_stall_longest_time", "number", None),
  597. ("stall_stall_total_time", "number", None),
  598. ("cls", "number", None),
  599. ("fcp", "duration", "millisecond"),
  600. ("fid", "duration", "millisecond"),
  601. ("fp", "duration", "millisecond"),
  602. ("inp", "duration", "millisecond"),
  603. ("lcp", "duration", "millisecond"),
  604. ("ttfb", "duration", "millisecond"),
  605. ("ttfb.requesttime", "duration", "millisecond"),
  606. ("score.cls", "number", None),
  607. ("score.fcp", "number", None),
  608. ("score.fid", "number", None),
  609. ("score.inp", "number", None),
  610. ("score.lcp", "number", None),
  611. ("score.ttfb", "number", None),
  612. ("score.total", "number", None),
  613. ("score.weight.cls", "number", None),
  614. ("score.weight.fcp", "number", None),
  615. ("score.weight.fid", "number", None),
  616. ("score.weight.inp", "number", None),
  617. ("score.weight.lcp", "number", None),
  618. ("score.weight.ttfb", "number", None),
  619. ("cache.item_size", "number", None),
  620. ("messaging.message.body.size", "number", None),
  621. ("messaging.message.receive.latency", "number", None),
  622. ("messaging.message.retry.count", "number", None),
  623. ]
  624. self.store_spans(
  625. [
  626. self.create_span(
  627. {
  628. "description": "foo",
  629. "sentry_tags": {"status": "success"},
  630. "tags": {"bar": "bar2"},
  631. },
  632. measurements={k: {"value": i + 1} for i, (k, _, _) in enumerate(keys)},
  633. start_ts=self.ten_mins_ago,
  634. ),
  635. ],
  636. is_eap=self.is_eap,
  637. )
  638. for i, (k, type, unit) in enumerate(keys):
  639. key = f"measurements.{k}"
  640. response = self.do_request(
  641. {
  642. "field": [key],
  643. "query": "description:foo",
  644. "project": self.project.id,
  645. "dataset": self.dataset,
  646. }
  647. )
  648. assert response.status_code == 200, response.content
  649. assert response.data["meta"] == {
  650. "dataset": mock.ANY,
  651. "datasetReason": "unchanged",
  652. "fields": {
  653. key: type,
  654. "id": "string",
  655. "project.name": "string",
  656. },
  657. "isMetricsData": False,
  658. "isMetricsExtractedData": False,
  659. "tips": {},
  660. "units": {
  661. key: unit,
  662. "id": None,
  663. "project.name": None,
  664. },
  665. }
  666. assert response.data["data"] == [
  667. {
  668. key: i + 1,
  669. "id": mock.ANY,
  670. "project.name": self.project.slug,
  671. }
  672. ]
  673. def test_environment(self):
  674. self.create_environment(self.project, name="prod")
  675. self.create_environment(self.project, name="test")
  676. self.store_spans(
  677. [
  678. self.create_span(
  679. {"description": "foo", "sentry_tags": {"environment": "prod"}},
  680. start_ts=self.ten_mins_ago,
  681. ),
  682. self.create_span(
  683. {"description": "foo", "sentry_tags": {"environment": "test"}},
  684. start_ts=self.ten_mins_ago,
  685. ),
  686. ],
  687. is_eap=self.is_eap,
  688. )
  689. response = self.do_request(
  690. {
  691. "field": ["environment", "count()"],
  692. "project": self.project.id,
  693. "environment": "prod",
  694. "dataset": self.dataset,
  695. }
  696. )
  697. assert response.status_code == 200, response.content
  698. assert response.data["data"] == [
  699. {"environment": "prod", "count()": 1},
  700. ]
  701. def test_transaction(self):
  702. self.store_spans(
  703. [
  704. self.create_span(
  705. {"description": "foo", "sentry_tags": {"transaction": "bar"}},
  706. start_ts=self.ten_mins_ago,
  707. ),
  708. ],
  709. is_eap=self.is_eap,
  710. )
  711. response = self.do_request(
  712. {
  713. "field": ["description", "count()"],
  714. "query": "transaction:bar",
  715. "orderby": "description",
  716. "project": self.project.id,
  717. "dataset": self.dataset,
  718. }
  719. )
  720. assert response.status_code == 200, response.content
  721. data = response.data["data"]
  722. meta = response.data["meta"]
  723. assert len(data) == 1
  724. assert data == [
  725. {
  726. "description": "foo",
  727. "count()": 1,
  728. },
  729. ]
  730. assert meta["dataset"] == self.dataset
  731. def test_orderby_alias(self):
  732. self.store_spans(
  733. [
  734. self.create_span(
  735. {"description": "foo", "sentry_tags": {"status": "success"}},
  736. start_ts=self.ten_mins_ago,
  737. ),
  738. self.create_span(
  739. {"description": "bar", "sentry_tags": {"status": "invalid_argument"}},
  740. duration=2000,
  741. start_ts=self.ten_mins_ago,
  742. ),
  743. ],
  744. is_eap=self.is_eap,
  745. )
  746. response = self.do_request(
  747. {
  748. "field": ["span.description", "sum(span.self_time)"],
  749. "query": "",
  750. "orderby": "sum_span_self_time",
  751. "project": self.project.id,
  752. "dataset": self.dataset,
  753. }
  754. )
  755. assert response.status_code == 200, response.content
  756. data = response.data["data"]
  757. meta = response.data["meta"]
  758. assert len(data) == 2
  759. assert data == [
  760. {
  761. "span.description": "foo",
  762. "sum(span.self_time)": 1000,
  763. },
  764. {
  765. "span.description": "bar",
  766. "sum(span.self_time)": 2000,
  767. },
  768. ]
  769. assert meta["dataset"] == self.dataset
  770. @pytest.mark.querybuilder
  771. def test_explore_sample_query(self):
  772. spans = [
  773. self.create_span(
  774. {"description": "foo", "sentry_tags": {"status": "success"}},
  775. start_ts=self.ten_mins_ago,
  776. ),
  777. self.create_span(
  778. {"description": "bar", "sentry_tags": {"status": "invalid_argument"}},
  779. start_ts=self.nine_mins_ago,
  780. ),
  781. ]
  782. self.store_spans(
  783. spans,
  784. is_eap=self.is_eap,
  785. )
  786. response = self.do_request(
  787. {
  788. "field": [
  789. "id",
  790. "project",
  791. "span.op",
  792. "span.description",
  793. "span.duration",
  794. "timestamp",
  795. "trace",
  796. "transaction.span_id",
  797. ],
  798. # This is to skip INP spans
  799. "query": "!transaction.span_id:00",
  800. "orderby": "timestamp",
  801. "statsPeriod": "1h",
  802. "project": self.project.id,
  803. "dataset": self.dataset,
  804. }
  805. )
  806. assert response.status_code == 200, response.content
  807. data = response.data["data"]
  808. meta = response.data["meta"]
  809. assert len(data) == 2
  810. for source, result in zip(spans, data):
  811. assert result["id"] == source["span_id"], "id"
  812. assert result["span.duration"] == 1000.0, "duration"
  813. assert result["span.op"] == "", "op"
  814. assert result["span.description"] == source["description"], "description"
  815. ts = datetime.fromisoformat(result["timestamp"])
  816. assert ts.tzinfo == timezone.utc
  817. assert ts.timestamp() == pytest.approx(
  818. source["end_timestamp_precise"], abs=5
  819. ), "timestamp"
  820. assert result["transaction.span_id"] == source["segment_id"], "transaction.span_id"
  821. assert result["project"] == result["project.name"] == self.project.slug, "project"
  822. assert meta["dataset"] == self.dataset
  823. def test_span_status(self):
  824. self.store_spans(
  825. [
  826. self.create_span(
  827. {"description": "foo", "sentry_tags": {"status": "internal_error"}},
  828. start_ts=self.ten_mins_ago,
  829. ),
  830. ],
  831. is_eap=self.is_eap,
  832. )
  833. response = self.do_request(
  834. {
  835. "field": ["description", "count()"],
  836. "query": "span.status:internal_error",
  837. "orderby": "description",
  838. "project": self.project.id,
  839. "dataset": self.dataset,
  840. }
  841. )
  842. assert response.status_code == 200, response.content
  843. data = response.data["data"]
  844. meta = response.data["meta"]
  845. assert len(data) == 1
  846. assert data == [
  847. {
  848. "description": "foo",
  849. "count()": 1,
  850. },
  851. ]
  852. assert meta["dataset"] == self.dataset
  853. def test_handle_nans_from_snuba(self):
  854. self.store_spans(
  855. [self.create_span({"description": "foo"}, start_ts=self.ten_mins_ago)],
  856. is_eap=self.is_eap,
  857. )
  858. response = self.do_request(
  859. {
  860. "field": ["description", "count()"],
  861. "query": "span.status:internal_error",
  862. "orderby": "description",
  863. "project": self.project.id,
  864. "dataset": self.dataset,
  865. }
  866. )
  867. assert response.status_code == 200, response.content
  868. def test_in_filter(self):
  869. self.store_spans(
  870. [
  871. self.create_span(
  872. {"description": "foo", "sentry_tags": {"transaction": "bar"}},
  873. start_ts=self.ten_mins_ago,
  874. ),
  875. self.create_span(
  876. {"description": "foo", "sentry_tags": {"transaction": "baz"}},
  877. start_ts=self.ten_mins_ago,
  878. ),
  879. self.create_span(
  880. {"description": "foo", "sentry_tags": {"transaction": "bat"}},
  881. start_ts=self.ten_mins_ago,
  882. ),
  883. ],
  884. is_eap=self.is_eap,
  885. )
  886. response = self.do_request(
  887. {
  888. "field": ["transaction", "count()"],
  889. "query": "transaction:[bar, baz]",
  890. "orderby": "transaction",
  891. "project": self.project.id,
  892. "dataset": self.dataset,
  893. }
  894. )
  895. assert response.status_code == 200, response.content
  896. data = response.data["data"]
  897. meta = response.data["meta"]
  898. assert len(data) == 2
  899. assert data == [
  900. {
  901. "transaction": "bar",
  902. "count()": 1,
  903. },
  904. {
  905. "transaction": "baz",
  906. "count()": 1,
  907. },
  908. ]
  909. assert meta["dataset"] == self.dataset
  910. class OrganizationEventsEAPSpanEndpointTest(OrganizationEventsSpanIndexedEndpointTest):
  911. is_eap = True
  912. use_rpc = False
  913. def test_simple(self):
  914. self.store_spans(
  915. [
  916. self.create_span(
  917. {"description": "foo", "sentry_tags": {"status": "success"}},
  918. start_ts=self.ten_mins_ago,
  919. ),
  920. self.create_span(
  921. {"description": "bar", "sentry_tags": {"status": "invalid_argument"}},
  922. start_ts=self.ten_mins_ago,
  923. ),
  924. ],
  925. is_eap=self.is_eap,
  926. )
  927. response = self.do_request(
  928. {
  929. "field": ["span.status", "description", "count()"],
  930. "query": "",
  931. "orderby": "description",
  932. "project": self.project.id,
  933. "dataset": self.dataset,
  934. }
  935. )
  936. assert response.status_code == 200, response.content
  937. data = response.data["data"]
  938. meta = response.data["meta"]
  939. assert len(data) == 2
  940. assert data == [
  941. {
  942. "span.status": "invalid_argument",
  943. "description": "bar",
  944. "count()": 1,
  945. },
  946. {
  947. "span.status": "success",
  948. "description": "foo",
  949. "count()": 1,
  950. },
  951. ]
  952. assert meta["dataset"] == self.dataset
  953. @pytest.mark.xfail(reason="event_id isn't being written to the new table")
  954. def test_id_filtering(self):
  955. super().test_id_filtering()
  956. def test_span_duration(self):
  957. spans = [
  958. self.create_span(
  959. {"description": "bar", "sentry_tags": {"status": "invalid_argument"}},
  960. start_ts=self.ten_mins_ago,
  961. ),
  962. self.create_span(
  963. {"description": "foo", "sentry_tags": {"status": "success"}},
  964. start_ts=self.ten_mins_ago,
  965. ),
  966. ]
  967. self.store_spans(spans, is_eap=self.is_eap)
  968. response = self.do_request(
  969. {
  970. "field": ["span.duration", "description"],
  971. "query": "",
  972. "orderby": "description",
  973. "project": self.project.id,
  974. "dataset": self.dataset,
  975. }
  976. )
  977. assert response.status_code == 200, response.content
  978. data = response.data["data"]
  979. meta = response.data["meta"]
  980. assert len(data) == 2
  981. assert data == [
  982. {
  983. "span.duration": 1000.0,
  984. "description": "bar",
  985. "project.name": self.project.slug,
  986. "id": spans[0]["span_id"],
  987. },
  988. {
  989. "span.duration": 1000.0,
  990. "description": "foo",
  991. "project.name": self.project.slug,
  992. "id": spans[1]["span_id"],
  993. },
  994. ]
  995. assert meta["dataset"] == self.dataset
  996. @pytest.mark.xfail
  997. def test_aggregate_numeric_attr(self):
  998. self.store_spans(
  999. [
  1000. self.create_span(
  1001. {
  1002. "description": "foo",
  1003. "sentry_tags": {"status": "success"},
  1004. "tags": {"bar": "bar1"},
  1005. },
  1006. start_ts=self.ten_mins_ago,
  1007. ),
  1008. self.create_span(
  1009. {
  1010. "description": "foo",
  1011. "sentry_tags": {"status": "success"},
  1012. "tags": {"bar": "bar2"},
  1013. },
  1014. measurements={"foo": {"value": 5}},
  1015. start_ts=self.ten_mins_ago,
  1016. ),
  1017. self.create_span(
  1018. {
  1019. "description": "foo",
  1020. "sentry_tags": {"status": "success"},
  1021. "tags": {"bar": "bar3"},
  1022. },
  1023. start_ts=self.ten_mins_ago,
  1024. ),
  1025. ],
  1026. is_eap=self.is_eap,
  1027. )
  1028. response = self.do_request(
  1029. {
  1030. "field": [
  1031. "description",
  1032. "count_unique(bar)",
  1033. "count_unique(tags[bar])",
  1034. "count_unique(tags[bar,string])",
  1035. "count()",
  1036. "count(span.duration)",
  1037. "count(tags[foo, number])",
  1038. "sum(tags[foo,number])",
  1039. "avg(tags[foo,number])",
  1040. "p50(tags[foo,number])",
  1041. "p75(tags[foo,number])",
  1042. "p95(tags[foo,number])",
  1043. "p99(tags[foo,number])",
  1044. "p100(tags[foo,number])",
  1045. "min(tags[foo,number])",
  1046. "max(tags[foo,number])",
  1047. ],
  1048. "query": "",
  1049. "orderby": "description",
  1050. "project": self.project.id,
  1051. "dataset": self.dataset,
  1052. }
  1053. )
  1054. assert response.status_code == 200, response.content
  1055. assert len(response.data["data"]) == 1
  1056. data = response.data["data"]
  1057. assert data[0] == {
  1058. "description": "foo",
  1059. "count_unique(bar)": 3,
  1060. "count_unique(tags[bar])": 3,
  1061. "count_unique(tags[bar,string])": 3,
  1062. "count()": 3,
  1063. "count(span.duration)": 3,
  1064. "count(tags[foo, number])": 1,
  1065. "sum(tags[foo,number])": 5.0,
  1066. "avg(tags[foo,number])": 5.0,
  1067. "p50(tags[foo,number])": 5.0,
  1068. "p75(tags[foo,number])": 5.0,
  1069. "p95(tags[foo,number])": 5.0,
  1070. "p99(tags[foo,number])": 5.0,
  1071. "p100(tags[foo,number])": 5.0,
  1072. "min(tags[foo,number])": 5.0,
  1073. "max(tags[foo,number])": 5.0,
  1074. }
  1075. def test_numeric_attr_without_space(self):
  1076. self.store_spans(
  1077. [
  1078. self.create_span(
  1079. {
  1080. "description": "foo",
  1081. "sentry_tags": {"status": "success"},
  1082. "tags": {"foo": "five"},
  1083. },
  1084. measurements={"foo": {"value": 5}},
  1085. start_ts=self.ten_mins_ago,
  1086. ),
  1087. ],
  1088. is_eap=self.is_eap,
  1089. )
  1090. response = self.do_request(
  1091. {
  1092. "field": ["description", "tags[foo,number]", "tags[foo,string]", "tags[foo]"],
  1093. "query": "",
  1094. "orderby": "description",
  1095. "project": self.project.id,
  1096. "dataset": self.dataset,
  1097. }
  1098. )
  1099. assert response.status_code == 200, response.content
  1100. assert len(response.data["data"]) == 1
  1101. data = response.data["data"]
  1102. assert data[0]["tags[foo,number]"] == 5
  1103. assert data[0]["tags[foo,string]"] == "five"
  1104. assert data[0]["tags[foo]"] == "five"
  1105. def test_numeric_attr_with_spaces(self):
  1106. self.store_spans(
  1107. [
  1108. self.create_span(
  1109. {
  1110. "description": "foo",
  1111. "sentry_tags": {"status": "success"},
  1112. "tags": {"foo": "five"},
  1113. },
  1114. measurements={"foo": {"value": 5}},
  1115. start_ts=self.ten_mins_ago,
  1116. ),
  1117. ],
  1118. is_eap=self.is_eap,
  1119. )
  1120. response = self.do_request(
  1121. {
  1122. "field": ["description", "tags[foo, number]", "tags[foo, string]", "tags[foo]"],
  1123. "query": "",
  1124. "orderby": "description",
  1125. "project": self.project.id,
  1126. "dataset": self.dataset,
  1127. }
  1128. )
  1129. assert response.status_code == 200, response.content
  1130. assert len(response.data["data"]) == 1
  1131. data = response.data["data"]
  1132. assert data[0]["tags[foo, number]"] == 5
  1133. assert data[0]["tags[foo, string]"] == "five"
  1134. assert data[0]["tags[foo]"] == "five"
  1135. def test_numeric_attr_filtering(self):
  1136. self.store_spans(
  1137. [
  1138. self.create_span(
  1139. {
  1140. "description": "foo",
  1141. "sentry_tags": {"status": "success"},
  1142. "tags": {"foo": "five"},
  1143. },
  1144. measurements={"foo": {"value": 5}},
  1145. start_ts=self.ten_mins_ago,
  1146. ),
  1147. self.create_span(
  1148. {"description": "bar", "sentry_tags": {"status": "success", "foo": "five"}},
  1149. measurements={"foo": {"value": 8}},
  1150. start_ts=self.ten_mins_ago,
  1151. ),
  1152. ],
  1153. is_eap=self.is_eap,
  1154. )
  1155. response = self.do_request(
  1156. {
  1157. "field": ["description", "tags[foo,number]"],
  1158. "query": "tags[foo,number]:5",
  1159. "orderby": "description",
  1160. "project": self.project.id,
  1161. "dataset": self.dataset,
  1162. }
  1163. )
  1164. assert response.status_code == 200, response.content
  1165. assert len(response.data["data"]) == 1
  1166. data = response.data["data"]
  1167. assert data[0]["tags[foo,number]"] == 5
  1168. assert data[0]["description"] == "foo"
  1169. def test_long_attr_name(self):
  1170. response = self.do_request(
  1171. {
  1172. "field": ["description", "z" * 201],
  1173. "query": "",
  1174. "orderby": "description",
  1175. "project": self.project.id,
  1176. "dataset": self.dataset,
  1177. }
  1178. )
  1179. assert response.status_code == 400, response.content
  1180. assert "Is Too Long" in response.data["detail"].title()
  1181. def test_numeric_attr_orderby(self):
  1182. self.store_spans(
  1183. [
  1184. self.create_span(
  1185. {
  1186. "description": "baz",
  1187. "sentry_tags": {"status": "success"},
  1188. "tags": {"foo": "five"},
  1189. },
  1190. measurements={"foo": {"value": 71}},
  1191. start_ts=self.ten_mins_ago,
  1192. ),
  1193. self.create_span(
  1194. {
  1195. "description": "foo",
  1196. "sentry_tags": {"status": "success"},
  1197. "tags": {"foo": "five"},
  1198. },
  1199. measurements={"foo": {"value": 5}},
  1200. start_ts=self.ten_mins_ago,
  1201. ),
  1202. self.create_span(
  1203. {
  1204. "description": "bar",
  1205. "sentry_tags": {"status": "success"},
  1206. "tags": {"foo": "five"},
  1207. },
  1208. measurements={"foo": {"value": 8}},
  1209. start_ts=self.ten_mins_ago,
  1210. ),
  1211. ],
  1212. is_eap=self.is_eap,
  1213. )
  1214. response = self.do_request(
  1215. {
  1216. "field": ["description", "tags[foo,number]"],
  1217. "query": "",
  1218. "orderby": ["tags[foo,number]"],
  1219. "project": self.project.id,
  1220. "dataset": self.dataset,
  1221. }
  1222. )
  1223. assert response.status_code == 200, response.content
  1224. assert len(response.data["data"]) == 3
  1225. data = response.data["data"]
  1226. assert data[0]["tags[foo,number]"] == 5
  1227. assert data[0]["description"] == "foo"
  1228. assert data[1]["tags[foo,number]"] == 8
  1229. assert data[1]["description"] == "bar"
  1230. assert data[2]["tags[foo,number]"] == 71
  1231. assert data[2]["description"] == "baz"
  1232. def test_margin_of_error(self):
  1233. total_samples = 10
  1234. in_group = 5
  1235. spans = []
  1236. for _ in range(in_group):
  1237. spans.append(
  1238. self.create_span(
  1239. {
  1240. "description": "foo",
  1241. "sentry_tags": {"status": "success"},
  1242. "measurements": {"client_sample_rate": {"value": 0.00001}},
  1243. },
  1244. start_ts=self.ten_mins_ago,
  1245. )
  1246. )
  1247. for _ in range(total_samples - in_group):
  1248. spans.append(
  1249. self.create_span(
  1250. {
  1251. "description": "bar",
  1252. "sentry_tags": {"status": "success"},
  1253. "measurements": {"client_sample_rate": {"value": 0.00001}},
  1254. },
  1255. )
  1256. )
  1257. self.store_spans(
  1258. spans,
  1259. is_eap=self.is_eap,
  1260. )
  1261. response = self.do_request(
  1262. {
  1263. "field": [
  1264. "margin_of_error()",
  1265. "lower_count_limit()",
  1266. "upper_count_limit()",
  1267. "count()",
  1268. ],
  1269. "query": "description:foo",
  1270. "project": self.project.id,
  1271. "dataset": self.dataset,
  1272. }
  1273. )
  1274. assert response.status_code == 200, response.content
  1275. assert len(response.data["data"]) == 1
  1276. data = response.data["data"][0]
  1277. margin_of_error = data["margin_of_error()"]
  1278. lower_limit = data["lower_count_limit()"]
  1279. upper_limit = data["upper_count_limit()"]
  1280. extrapolated = data["count()"]
  1281. assert margin_of_error == pytest.approx(0.306, rel=1e-1)
  1282. # How to read this; these results mean that the extrapolated count is
  1283. # 500k, with a lower estimated bound of ~200k, and an upper bound of 800k
  1284. assert lower_limit == pytest.approx(190_000, abs=5000)
  1285. assert extrapolated == pytest.approx(500_000, abs=5000)
  1286. assert upper_limit == pytest.approx(810_000, abs=5000)
  1287. def test_skip_aggregate_conditions_option(self):
  1288. span_1 = self.create_span(
  1289. {"description": "foo", "sentry_tags": {"status": "success"}},
  1290. start_ts=self.ten_mins_ago,
  1291. )
  1292. span_2 = self.create_span(
  1293. {"description": "bar", "sentry_tags": {"status": "invalid_argument"}},
  1294. start_ts=self.ten_mins_ago,
  1295. )
  1296. self.store_spans(
  1297. [span_1, span_2],
  1298. is_eap=self.is_eap,
  1299. )
  1300. response = self.do_request(
  1301. {
  1302. "field": ["description"],
  1303. "query": "description:foo count():>1",
  1304. "orderby": "description",
  1305. "project": self.project.id,
  1306. "dataset": self.dataset,
  1307. "allowAggregateConditions": "0",
  1308. }
  1309. )
  1310. assert response.status_code == 200, response.content
  1311. data = response.data["data"]
  1312. meta = response.data["meta"]
  1313. assert len(data) == 1
  1314. assert data == [
  1315. {
  1316. "description": "foo",
  1317. "project.name": self.project.slug,
  1318. "id": span_1["span_id"],
  1319. },
  1320. ]
  1321. assert meta["dataset"] == self.dataset
  1322. def test_span_data_fields_http_resource(self):
  1323. self.store_spans(
  1324. [
  1325. self.create_span(
  1326. {
  1327. "op": "resource.img",
  1328. "description": "/image/",
  1329. "data": {
  1330. "http.decoded_response_content_length": 1,
  1331. "http.response_content_length": 2,
  1332. "http.response_transfer_size": 3,
  1333. },
  1334. },
  1335. start_ts=self.ten_mins_ago,
  1336. ),
  1337. ],
  1338. is_eap=self.is_eap,
  1339. )
  1340. response = self.do_request(
  1341. {
  1342. "field": [
  1343. "http.decoded_response_content_length",
  1344. "http.response_content_length",
  1345. "http.response_transfer_size",
  1346. ],
  1347. "project": self.project.id,
  1348. "dataset": self.dataset,
  1349. "allowAggregateConditions": "0",
  1350. }
  1351. )
  1352. assert response.status_code == 200, response.content
  1353. assert response.data["data"] == [
  1354. {
  1355. "http.decoded_response_content_length": 1,
  1356. "http.response_content_length": 2,
  1357. "http.response_transfer_size": 3,
  1358. "project.name": self.project.slug,
  1359. "id": mock.ANY,
  1360. },
  1361. ]
  1362. assert response.data["meta"] == {
  1363. "dataset": mock.ANY,
  1364. "datasetReason": "unchanged",
  1365. "fields": {
  1366. "http.decoded_response_content_length": "size",
  1367. "http.response_content_length": "size",
  1368. "http.response_transfer_size": "size",
  1369. "id": "string",
  1370. "project.name": "string",
  1371. },
  1372. "isMetricsData": False,
  1373. "isMetricsExtractedData": False,
  1374. "tips": {},
  1375. "units": {
  1376. "http.decoded_response_content_length": "byte",
  1377. "http.response_content_length": "byte",
  1378. "http.response_transfer_size": "byte",
  1379. "id": None,
  1380. "project.name": None,
  1381. },
  1382. }
  1383. def test_filtering_numeric_attr(self):
  1384. span_1 = self.create_span(
  1385. {"description": "foo"},
  1386. measurements={"foo": {"value": 30}},
  1387. start_ts=self.ten_mins_ago,
  1388. )
  1389. span_2 = self.create_span(
  1390. {"description": "foo"},
  1391. measurements={"foo": {"value": 10}},
  1392. start_ts=self.ten_mins_ago,
  1393. )
  1394. self.store_spans([span_1, span_2], is_eap=self.is_eap)
  1395. response = self.do_request(
  1396. {
  1397. "field": ["tags[foo,number]"],
  1398. "query": "span.duration:>=0 tags[foo,number]:>20",
  1399. "project": self.project.id,
  1400. "dataset": self.dataset,
  1401. }
  1402. )
  1403. assert response.status_code == 200, response.content
  1404. assert response.data["data"] == [
  1405. {
  1406. "id": span_1["span_id"],
  1407. "project.name": self.project.slug,
  1408. "tags[foo,number]": 30,
  1409. },
  1410. ]
  1411. class OrganizationEventsEAPRPCSpanEndpointTest(OrganizationEventsEAPSpanEndpointTest):
  1412. """These tests aren't fully passing yet, currently inheriting xfail from the eap tests"""
  1413. is_eap = True
  1414. use_rpc = True
  1415. def test_extrapolation(self):
  1416. """Extrapolation only changes the number when there's a sample rate"""
  1417. spans = []
  1418. spans.append(
  1419. self.create_span(
  1420. {
  1421. "description": "foo",
  1422. "sentry_tags": {"status": "success"},
  1423. "measurements": {"client_sample_rate": {"value": 0.1}},
  1424. },
  1425. start_ts=self.ten_mins_ago,
  1426. )
  1427. )
  1428. spans.append(
  1429. self.create_span(
  1430. {
  1431. "description": "bar",
  1432. "sentry_tags": {"status": "success"},
  1433. },
  1434. start_ts=self.ten_mins_ago,
  1435. )
  1436. )
  1437. self.store_spans(spans, is_eap=self.is_eap)
  1438. response = self.do_request(
  1439. {
  1440. "field": ["description", "count()"],
  1441. "orderby": "-count()",
  1442. "query": "",
  1443. "project": self.project.id,
  1444. "dataset": self.dataset,
  1445. }
  1446. )
  1447. assert response.status_code == 200, response.content
  1448. data = response.data["data"]
  1449. confidence = response.data["confidence"]
  1450. assert len(data) == 2
  1451. assert len(confidence) == 2
  1452. assert data[0]["count()"] == 10
  1453. assert confidence[0]["count()"] == "low"
  1454. assert data[1]["count()"] == 1
  1455. # While logically the confidence for 1 event at 100% sample rate should be high, we're going with low until we
  1456. # get customer feedback
  1457. assert confidence[1]["count()"] == "low"
  1458. def test_span_duration(self):
  1459. spans = [
  1460. self.create_span(
  1461. {"description": "bar", "sentry_tags": {"status": "invalid_argument"}},
  1462. start_ts=self.ten_mins_ago,
  1463. ),
  1464. self.create_span(
  1465. {"description": "foo", "sentry_tags": {"status": "success"}},
  1466. start_ts=self.ten_mins_ago,
  1467. ),
  1468. ]
  1469. self.store_spans(spans, is_eap=self.is_eap)
  1470. response = self.do_request(
  1471. {
  1472. "field": ["span.duration", "description"],
  1473. "query": "",
  1474. "orderby": "description",
  1475. "project": self.project.id,
  1476. "dataset": self.dataset,
  1477. }
  1478. )
  1479. assert response.status_code == 200, response.content
  1480. data = response.data["data"]
  1481. meta = response.data["meta"]
  1482. assert len(data) == 2
  1483. assert data == [
  1484. {
  1485. "span.duration": 1000.0,
  1486. "description": "bar",
  1487. "project.name": self.project.slug,
  1488. "id": spans[0]["span_id"],
  1489. },
  1490. {
  1491. "span.duration": 1000.0,
  1492. "description": "foo",
  1493. "project.name": self.project.slug,
  1494. "id": spans[1]["span_id"],
  1495. },
  1496. ]
  1497. assert meta["dataset"] == self.dataset
  1498. def test_average_sampling_rate(self):
  1499. spans = []
  1500. spans.append(
  1501. self.create_span(
  1502. {
  1503. "description": "foo",
  1504. "sentry_tags": {"status": "success"},
  1505. "measurements": {"client_sample_rate": {"value": 0.1}},
  1506. },
  1507. start_ts=self.ten_mins_ago,
  1508. )
  1509. )
  1510. spans.append(
  1511. self.create_span(
  1512. {
  1513. "description": "bar",
  1514. "sentry_tags": {"status": "success"},
  1515. "measurements": {"client_sample_rate": {"value": 0.85}},
  1516. },
  1517. start_ts=self.ten_mins_ago,
  1518. )
  1519. )
  1520. self.store_spans(spans, is_eap=self.is_eap)
  1521. response = self.do_request(
  1522. {
  1523. "field": [
  1524. "avg_sample(sampling_rate)",
  1525. "count()",
  1526. "min(sampling_rate)",
  1527. "count_sample()",
  1528. ],
  1529. "query": "",
  1530. "project": self.project.id,
  1531. "dataset": self.dataset,
  1532. }
  1533. )
  1534. assert response.status_code == 200, response.content
  1535. data = response.data["data"]
  1536. confidence = response.data["confidence"]
  1537. assert len(data) == 1
  1538. assert data[0]["avg_sample(sampling_rate)"] == pytest.approx(0.475)
  1539. assert data[0]["min(sampling_rate)"] == pytest.approx(0.1)
  1540. assert data[0]["count_sample()"] == 2
  1541. assert data[0]["count()"] == 11
  1542. assert confidence[0]["count()"] == "low"
  1543. @pytest.mark.xfail
  1544. def test_aggregate_numeric_attr(self):
  1545. self.store_spans(
  1546. [
  1547. self.create_span(
  1548. {
  1549. "description": "foo",
  1550. "sentry_tags": {"status": "success"},
  1551. "tags": {"bar": "bar1"},
  1552. },
  1553. start_ts=self.ten_mins_ago,
  1554. ),
  1555. self.create_span(
  1556. {
  1557. "description": "foo",
  1558. "sentry_tags": {"status": "success"},
  1559. "tags": {"bar": "bar2"},
  1560. },
  1561. measurements={"foo": {"value": 5}},
  1562. start_ts=self.ten_mins_ago,
  1563. ),
  1564. ],
  1565. is_eap=self.is_eap,
  1566. )
  1567. response = self.do_request(
  1568. {
  1569. "field": [
  1570. "description",
  1571. "count_unique(bar)",
  1572. "count_unique(tags[bar])",
  1573. "count_unique(tags[bar,string])",
  1574. "count()",
  1575. "count(span.duration)",
  1576. "count(tags[foo, number])",
  1577. "sum(tags[foo,number])",
  1578. "avg(tags[foo,number])",
  1579. "p50(tags[foo,number])",
  1580. "p75(tags[foo,number])",
  1581. "p95(tags[foo,number])",
  1582. "p99(tags[foo,number])",
  1583. "p100(tags[foo,number])",
  1584. "min(tags[foo,number])",
  1585. "max(tags[foo,number])",
  1586. ],
  1587. "query": "",
  1588. "orderby": "description",
  1589. "project": self.project.id,
  1590. "dataset": self.dataset,
  1591. }
  1592. )
  1593. assert response.status_code == 200, response.content
  1594. assert len(response.data["data"]) == 1
  1595. data = response.data["data"]
  1596. assert data[0] == {
  1597. "description": "foo",
  1598. "count_unique(bar)": 2,
  1599. "count_unique(tags[bar])": 2,
  1600. "count_unique(tags[bar,string])": 2,
  1601. "count()": 2,
  1602. "count(span.duration)": 2,
  1603. "count(tags[foo, number])": 1,
  1604. "sum(tags[foo,number])": 5.0,
  1605. "avg(tags[foo,number])": 5.0,
  1606. "p50(tags[foo,number])": 5.0,
  1607. "p75(tags[foo,number])": 5.0,
  1608. "p95(tags[foo,number])": 5.0,
  1609. "p99(tags[foo,number])": 5.0,
  1610. "p100(tags[foo,number])": 5.0,
  1611. "min(tags[foo,number])": 5.0,
  1612. "max(tags[foo,number])": 5.0,
  1613. }
  1614. @pytest.mark.skip(reason="margin will not be moved to the RPC")
  1615. def test_margin_of_error(self):
  1616. super().test_margin_of_error()
  1617. @pytest.mark.skip(reason="module not migrated over")
  1618. def test_module_alias(self):
  1619. super().test_module_alias()
  1620. @pytest.mark.xfail(
  1621. reason="wip: depends on rpc having a way to set a different default in virtual contexts"
  1622. )
  1623. def test_span_module(self):
  1624. super().test_span_module()
  1625. def test_inp_span(self):
  1626. replay_id = uuid.uuid4().hex
  1627. self.store_spans(
  1628. [
  1629. self.create_span(
  1630. {
  1631. "sentry_tags": {
  1632. "replay_id": replay_id,
  1633. "browser.name": "Chrome",
  1634. "transaction": "/pageloads/",
  1635. }
  1636. },
  1637. start_ts=self.ten_mins_ago,
  1638. ),
  1639. ],
  1640. is_eap=self.is_eap,
  1641. )
  1642. response = self.do_request(
  1643. {
  1644. # Not moving origin.transaction to RPC, its equivalent to transaction and just represents the
  1645. # transaction that's related to the span
  1646. "field": ["replay.id", "browser.name", "transaction", "count()"],
  1647. "query": f"replay.id:{replay_id} AND browser.name:Chrome AND transaction:/pageloads/",
  1648. "orderby": "count()",
  1649. "project": self.project.id,
  1650. "dataset": self.dataset,
  1651. }
  1652. )
  1653. assert response.status_code == 200, response.content
  1654. data = response.data["data"]
  1655. meta = response.data["meta"]
  1656. assert len(data) == 1
  1657. assert data[0]["replay.id"] == replay_id
  1658. assert data[0]["browser.name"] == "Chrome"
  1659. assert data[0]["transaction"] == "/pageloads/"
  1660. assert meta["dataset"] == self.dataset
  1661. @pytest.mark.xfail(
  1662. reason="wip: depends on rpc having a way to set a different default in virtual contexts"
  1663. )
  1664. # https://github.com/getsentry/projects/issues/215?issue=getsentry%7Cprojects%7C488
  1665. def test_other_category_span(self):
  1666. super().test_other_category_span()
  1667. @pytest.mark.xfail(
  1668. reason="wip: not implemented yet, depends on rpc having a way to filter based on casing"
  1669. )
  1670. # https://github.com/getsentry/projects/issues/215?issue=getsentry%7Cprojects%7C489
  1671. def test_span_op_casing(self):
  1672. super().test_span_op_casing()
  1673. def test_tag_wildcards(self):
  1674. self.store_spans(
  1675. [
  1676. self.create_span(
  1677. {"description": "foo", "tags": {"foo": "bar"}},
  1678. start_ts=self.ten_mins_ago,
  1679. ),
  1680. self.create_span(
  1681. {"description": "qux", "tags": {"foo": "qux"}},
  1682. start_ts=self.ten_mins_ago,
  1683. ),
  1684. ],
  1685. is_eap=self.is_eap,
  1686. )
  1687. for query in [
  1688. "foo:b*",
  1689. "foo:*r",
  1690. "foo:*a*",
  1691. "foo:b*r",
  1692. ]:
  1693. response = self.do_request(
  1694. {
  1695. "field": ["foo", "count()"],
  1696. "query": query,
  1697. "project": self.project.id,
  1698. "dataset": self.dataset,
  1699. }
  1700. )
  1701. assert response.status_code == 200, response.content
  1702. assert response.data["data"] == [{"foo": "bar", "count()": 1}]
  1703. @pytest.mark.xfail(reason="wip: rate not implemented yet")
  1704. def test_spm(self):
  1705. super().test_spm()