test_organization_events_span_indexed.py 86 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526
  1. import uuid
  2. from datetime import datetime, timezone
  3. from unittest import mock
  4. import pytest
  5. import urllib3
  6. from sentry.testutils.helpers import parse_link_header
  7. from tests.snuba.api.endpoints.test_organization_events import OrganizationEventsEndpointTestBase
  8. class OrganizationEventsSpanIndexedEndpointTest(OrganizationEventsEndpointTestBase):
  9. is_eap = False
  10. use_rpc = False
  11. """Test the indexed spans dataset.
  12. To run this locally you may need to set the ENABLE_SPANS_CONSUMER flag to True in Snuba.
  13. A way to do this is
  14. 1. run: `sentry devservices down snuba`
  15. 2. clone snuba locally
  16. 3. run: `export ENABLE_SPANS_CONSUMER=True`
  17. 4. run snuba
  18. At this point tests should work locally
  19. Once span ingestion is on by default this will no longer need to be done
  20. """
  21. @property
  22. def dataset(self):
  23. if self.is_eap:
  24. return "spans"
  25. else:
  26. return "spansIndexed"
  27. def do_request(self, query, features=None, **kwargs):
  28. query["useRpc"] = "1" if self.use_rpc else "0"
  29. return super().do_request(query, features, **kwargs)
  30. def setUp(self):
  31. super().setUp()
  32. self.features = {
  33. "organizations:starfish-view": True,
  34. }
  35. @pytest.mark.querybuilder
  36. def test_simple(self):
  37. self.store_spans(
  38. [
  39. self.create_span(
  40. {"description": "foo", "sentry_tags": {"status": "success"}},
  41. start_ts=self.ten_mins_ago,
  42. ),
  43. self.create_span(
  44. {"description": "bar", "sentry_tags": {"status": "invalid_argument"}},
  45. start_ts=self.ten_mins_ago,
  46. ),
  47. ],
  48. is_eap=self.is_eap,
  49. )
  50. response = self.do_request(
  51. {
  52. "field": ["span.status", "description", "count()"],
  53. "query": "",
  54. "orderby": "description",
  55. "project": self.project.id,
  56. "dataset": self.dataset,
  57. }
  58. )
  59. assert response.status_code == 200, response.content
  60. data = response.data["data"]
  61. meta = response.data["meta"]
  62. assert len(data) == 2
  63. assert data == [
  64. {
  65. "span.status": "invalid_argument",
  66. "description": "bar",
  67. "count()": 1,
  68. },
  69. {
  70. "span.status": "ok",
  71. "description": "foo",
  72. "count()": 1,
  73. },
  74. ]
  75. assert meta["dataset"] == self.dataset
  76. def test_spm(self):
  77. self.store_spans(
  78. [
  79. self.create_span(
  80. {"description": "foo", "sentry_tags": {"status": "success"}},
  81. start_ts=self.ten_mins_ago,
  82. ),
  83. ],
  84. is_eap=self.is_eap,
  85. )
  86. response = self.do_request(
  87. {
  88. "field": ["description", "spm()"],
  89. "query": "",
  90. "orderby": "description",
  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) == 1
  99. assert data == [
  100. {
  101. "description": "foo",
  102. "spm()": 1 / (90 * 24 * 60),
  103. },
  104. ]
  105. assert meta["dataset"] == self.dataset
  106. def test_id_fields(self):
  107. self.store_spans(
  108. [
  109. self.create_span(
  110. {"description": "foo", "sentry_tags": {"status": "success"}},
  111. start_ts=self.ten_mins_ago,
  112. ),
  113. self.create_span(
  114. {"description": "bar", "sentry_tags": {"status": "invalid_argument"}},
  115. start_ts=self.ten_mins_ago,
  116. ),
  117. ],
  118. is_eap=self.is_eap,
  119. )
  120. response = self.do_request(
  121. {
  122. "field": ["id", "span_id"],
  123. "query": "",
  124. "orderby": "id",
  125. "project": self.project.id,
  126. "dataset": self.dataset,
  127. }
  128. )
  129. assert response.status_code == 200, response.content
  130. data = response.data["data"]
  131. meta = response.data["meta"]
  132. assert len(data) == 2
  133. for obj in data:
  134. assert obj["id"] == obj["span_id"]
  135. assert meta["dataset"] == self.dataset
  136. def test_sentry_tags_vs_tags(self):
  137. self.store_spans(
  138. [
  139. self.create_span(
  140. {"sentry_tags": {"transaction.method": "foo"}}, start_ts=self.ten_mins_ago
  141. ),
  142. ],
  143. is_eap=self.is_eap,
  144. )
  145. response = self.do_request(
  146. {
  147. "field": ["transaction.method", "count()"],
  148. "query": "",
  149. "orderby": "count()",
  150. "project": self.project.id,
  151. "dataset": self.dataset,
  152. }
  153. )
  154. assert response.status_code == 200, response.content
  155. data = response.data["data"]
  156. meta = response.data["meta"]
  157. assert len(data) == 1
  158. assert data[0]["transaction.method"] == "foo"
  159. assert meta["dataset"] == self.dataset
  160. def test_sentry_tags_syntax(self):
  161. self.store_spans(
  162. [
  163. self.create_span(
  164. {"sentry_tags": {"transaction.method": "foo"}}, start_ts=self.ten_mins_ago
  165. ),
  166. ],
  167. is_eap=self.is_eap,
  168. )
  169. response = self.do_request(
  170. {
  171. "field": ["sentry_tags[transaction.method]", "count()"],
  172. "query": "",
  173. "orderby": "count()",
  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]["sentry_tags[transaction.method]"] == "foo"
  183. assert meta["dataset"] == self.dataset
  184. def test_module_alias(self):
  185. # Delegates `span.module` to `sentry_tags[category]`. Maps `"db.redis"` spans to the `"cache"` module
  186. self.store_spans(
  187. [
  188. self.create_span(
  189. {
  190. "op": "db.redis",
  191. "description": "EXEC *",
  192. "sentry_tags": {
  193. "description": "EXEC *",
  194. "category": "db",
  195. "op": "db.redis",
  196. "transaction": "/app/index",
  197. },
  198. },
  199. start_ts=self.ten_mins_ago,
  200. ),
  201. ],
  202. is_eap=self.is_eap,
  203. )
  204. response = self.do_request(
  205. {
  206. "field": ["span.module", "span.description"],
  207. "query": "span.module:cache",
  208. "project": self.project.id,
  209. "dataset": self.dataset,
  210. }
  211. )
  212. assert response.status_code == 200, response.content
  213. data = response.data["data"]
  214. meta = response.data["meta"]
  215. assert len(data) == 1
  216. assert data[0]["span.module"] == "cache"
  217. assert data[0]["span.description"] == "EXEC *"
  218. assert meta["dataset"] == self.dataset
  219. def test_device_class_filter_unknown(self):
  220. self.store_spans(
  221. [
  222. self.create_span({"sentry_tags": {"device.class": ""}}, start_ts=self.ten_mins_ago),
  223. ],
  224. is_eap=self.is_eap,
  225. )
  226. response = self.do_request(
  227. {
  228. "field": ["device.class", "count()"],
  229. "query": "device.class:Unknown",
  230. "orderby": "count()",
  231. "project": self.project.id,
  232. "dataset": self.dataset,
  233. }
  234. )
  235. assert response.status_code == 200, response.content
  236. data = response.data["data"]
  237. meta = response.data["meta"]
  238. assert len(data) == 1
  239. assert data[0]["device.class"] == "Unknown"
  240. assert meta["dataset"] == self.dataset
  241. def test_span_module(self):
  242. self.store_spans(
  243. [
  244. self.create_span(
  245. {
  246. "sentry_tags": {
  247. "op": "http",
  248. "category": "http",
  249. }
  250. },
  251. start_ts=self.ten_mins_ago,
  252. ),
  253. self.create_span(
  254. {
  255. "sentry_tags": {
  256. "op": "alternative",
  257. "category": "other",
  258. }
  259. },
  260. start_ts=self.ten_mins_ago,
  261. ),
  262. self.create_span(
  263. {
  264. "sentry_tags": {
  265. "op": "alternative",
  266. "category": "other",
  267. }
  268. },
  269. start_ts=self.ten_mins_ago,
  270. ),
  271. ],
  272. is_eap=self.is_eap,
  273. )
  274. response = self.do_request(
  275. {
  276. "field": ["span.module", "count()"],
  277. "query": "",
  278. "orderby": "-count()",
  279. "project": self.project.id,
  280. "dataset": self.dataset,
  281. }
  282. )
  283. assert response.status_code == 200, response.content
  284. data = response.data["data"]
  285. meta = response.data["meta"]
  286. assert len(data) == 2
  287. assert data[0]["span.module"] == "other"
  288. assert data[1]["span.module"] == "http"
  289. assert meta["dataset"] == self.dataset
  290. def test_network_span(self):
  291. self.store_spans(
  292. [
  293. self.create_span(
  294. {
  295. "sentry_tags": {
  296. "action": "GET",
  297. "category": "http",
  298. "description": "GET https://*.resource.com",
  299. "domain": "*.resource.com",
  300. "op": "http.client",
  301. "status_code": "200",
  302. "transaction": "/api/0/data/",
  303. "transaction.method": "GET",
  304. "transaction.op": "http.server",
  305. }
  306. },
  307. start_ts=self.ten_mins_ago,
  308. ),
  309. ],
  310. is_eap=self.is_eap,
  311. )
  312. response = self.do_request(
  313. {
  314. "field": ["span.op", "span.status_code"],
  315. "query": "span.status_code:200",
  316. "project": self.project.id,
  317. "dataset": self.dataset,
  318. }
  319. )
  320. assert response.status_code == 200, response.content
  321. data = response.data["data"]
  322. meta = response.data["meta"]
  323. assert len(data) == 1
  324. assert data[0]["span.op"] == "http.client"
  325. assert data[0]["span.status_code"] == "200"
  326. assert meta["dataset"] == self.dataset
  327. def test_other_category_span(self):
  328. self.store_spans(
  329. [
  330. self.create_span(
  331. {
  332. "sentry_tags": {
  333. "action": "GET",
  334. "category": "alternative",
  335. "description": "GET https://*.resource.com",
  336. "domain": "*.resource.com",
  337. "op": "alternative",
  338. "status_code": "200",
  339. "transaction": "/api/0/data/",
  340. "transaction.method": "GET",
  341. "transaction.op": "http.server",
  342. }
  343. },
  344. start_ts=self.ten_mins_ago,
  345. ),
  346. ],
  347. is_eap=self.is_eap,
  348. )
  349. response = self.do_request(
  350. {
  351. "field": ["span.op", "span.status_code"],
  352. "query": "span.module:other span.status_code:200",
  353. "project": self.project.id,
  354. "dataset": self.dataset,
  355. }
  356. )
  357. assert response.status_code == 200, response.content
  358. data = response.data["data"]
  359. meta = response.data["meta"]
  360. assert len(data) == 1
  361. assert data[0]["span.op"] == "alternative"
  362. assert data[0]["span.status_code"] == "200"
  363. assert meta["dataset"] == self.dataset
  364. def test_inp_span(self):
  365. replay_id = uuid.uuid4().hex
  366. self.store_spans(
  367. [
  368. self.create_span(
  369. {
  370. "sentry_tags": {
  371. "replay_id": replay_id,
  372. "browser.name": "Chrome",
  373. "transaction": "/pageloads/",
  374. }
  375. },
  376. start_ts=self.ten_mins_ago,
  377. ),
  378. ],
  379. is_eap=self.is_eap,
  380. )
  381. response = self.do_request(
  382. {
  383. "field": ["replay.id", "browser.name", "origin.transaction", "count()"],
  384. "query": f"replay.id:{replay_id} AND browser.name:Chrome AND origin.transaction:/pageloads/",
  385. "orderby": "count()",
  386. "project": self.project.id,
  387. "dataset": self.dataset,
  388. }
  389. )
  390. assert response.status_code == 200, response.content
  391. data = response.data["data"]
  392. meta = response.data["meta"]
  393. assert len(data) == 1
  394. assert data[0]["replay.id"] == replay_id
  395. assert data[0]["browser.name"] == "Chrome"
  396. assert data[0]["origin.transaction"] == "/pageloads/"
  397. assert meta["dataset"] == self.dataset
  398. def test_id_filtering(self):
  399. span = self.create_span({"description": "foo"}, start_ts=self.ten_mins_ago)
  400. self.store_span(span, is_eap=self.is_eap)
  401. response = self.do_request(
  402. {
  403. "field": ["description", "count()"],
  404. "query": f"id:{span['span_id']}",
  405. "orderby": "description",
  406. "project": self.project.id,
  407. "dataset": self.dataset,
  408. }
  409. )
  410. assert response.status_code == 200, response.content
  411. data = response.data["data"]
  412. meta = response.data["meta"]
  413. assert len(data) == 1
  414. assert data[0]["description"] == "foo"
  415. assert meta["dataset"] == self.dataset
  416. response = self.do_request(
  417. {
  418. "field": ["description", "count()"],
  419. "query": f"transaction.id:{span['event_id']}",
  420. "orderby": "description",
  421. "project": self.project.id,
  422. "dataset": self.dataset,
  423. }
  424. )
  425. assert response.status_code == 200, response.content
  426. data = response.data["data"]
  427. meta = response.data["meta"]
  428. assert len(data) == 1
  429. assert data[0]["description"] == "foo"
  430. assert meta["dataset"] == self.dataset
  431. def test_span_op_casing(self):
  432. self.store_spans(
  433. [
  434. self.create_span(
  435. {
  436. "sentry_tags": {
  437. "replay_id": "abc123",
  438. "browser.name": "Chrome",
  439. "transaction": "/pageloads/",
  440. "op": "this is a transaction",
  441. }
  442. },
  443. start_ts=self.ten_mins_ago,
  444. ),
  445. ],
  446. is_eap=self.is_eap,
  447. )
  448. response = self.do_request(
  449. {
  450. "field": ["span.op", "count()"],
  451. "query": 'span.op:"ThIs Is a TraNSActiON"',
  452. "orderby": "count()",
  453. "project": self.project.id,
  454. "dataset": self.dataset,
  455. }
  456. )
  457. assert response.status_code == 200, response.content
  458. data = response.data["data"]
  459. meta = response.data["meta"]
  460. assert len(data) == 1
  461. assert data[0]["span.op"] == "this is a transaction"
  462. assert meta["dataset"] == self.dataset
  463. def test_queue_span(self):
  464. self.store_spans(
  465. [
  466. self.create_span(
  467. {
  468. "measurements": {
  469. "messaging.message.body.size": {"value": 1024, "unit": "byte"},
  470. "messaging.message.receive.latency": {
  471. "value": 1000,
  472. "unit": "millisecond",
  473. },
  474. "messaging.message.retry.count": {"value": 2, "unit": "none"},
  475. },
  476. "sentry_tags": {
  477. "transaction": "queue-processor",
  478. "messaging.destination.name": "events",
  479. "messaging.message.id": "abc123",
  480. "trace.status": "ok",
  481. },
  482. },
  483. start_ts=self.ten_mins_ago,
  484. ),
  485. ],
  486. is_eap=self.is_eap,
  487. )
  488. response = self.do_request(
  489. {
  490. "field": [
  491. "transaction",
  492. "messaging.destination.name",
  493. "messaging.message.id",
  494. "measurements.messaging.message.receive.latency",
  495. "measurements.messaging.message.body.size",
  496. "measurements.messaging.message.retry.count",
  497. "trace.status",
  498. "count()",
  499. ],
  500. "query": 'messaging.destination.name:"events"',
  501. "orderby": "count()",
  502. "project": self.project.id,
  503. "dataset": self.dataset,
  504. }
  505. )
  506. assert response.status_code == 200, response.content
  507. data = response.data["data"]
  508. meta = response.data["meta"]
  509. assert len(data) == 1
  510. assert data[0]["transaction"] == "queue-processor"
  511. assert data[0]["messaging.destination.name"] == "events"
  512. assert data[0]["messaging.message.id"] == "abc123"
  513. assert data[0]["trace.status"] == "ok"
  514. assert data[0]["measurements.messaging.message.receive.latency"] == 1000
  515. assert data[0]["measurements.messaging.message.body.size"] == 1024
  516. assert data[0]["measurements.messaging.message.retry.count"] == 2
  517. assert meta["dataset"] == self.dataset
  518. def test_tag_wildcards(self):
  519. self.store_spans(
  520. [
  521. self.create_span(
  522. {"description": "foo", "tags": {"foo": "BaR"}},
  523. start_ts=self.ten_mins_ago,
  524. ),
  525. self.create_span(
  526. {"description": "qux", "tags": {"foo": "QuX"}},
  527. start_ts=self.ten_mins_ago,
  528. ),
  529. ],
  530. is_eap=self.is_eap,
  531. )
  532. for query in [
  533. "foo:b*",
  534. "foo:*r",
  535. "foo:*a*",
  536. "foo:b*r",
  537. ]:
  538. response = self.do_request(
  539. {
  540. "field": ["foo", "count()"],
  541. "query": query,
  542. "project": self.project.id,
  543. "dataset": self.dataset,
  544. }
  545. )
  546. assert response.status_code == 200, response.content
  547. assert response.data["data"] == [{"foo": "BaR", "count()": 1}]
  548. def test_query_for_missing_tag(self):
  549. self.store_spans(
  550. [
  551. self.create_span(
  552. {"description": "foo"},
  553. start_ts=self.ten_mins_ago,
  554. ),
  555. self.create_span(
  556. {"description": "qux", "tags": {"foo": "bar"}},
  557. start_ts=self.ten_mins_ago,
  558. ),
  559. ],
  560. is_eap=self.is_eap,
  561. )
  562. response = self.do_request(
  563. {
  564. "field": ["foo", "count()"],
  565. "query": 'foo:""',
  566. "project": self.project.id,
  567. "dataset": self.dataset,
  568. }
  569. )
  570. assert response.status_code == 200, response.content
  571. assert response.data["data"] == [{"foo": "", "count()": 1}]
  572. def test_count_field_type(self):
  573. response = self.do_request(
  574. {
  575. "field": ["count()"],
  576. "project": self.project.id,
  577. "dataset": self.dataset,
  578. }
  579. )
  580. assert response.status_code == 200, response.content
  581. assert response.data["meta"]["fields"] == {"count()": "integer"}
  582. assert response.data["meta"]["units"] == {"count()": None}
  583. assert response.data["data"] == [{"count()": 0}]
  584. def _test_simple_measurements(self, keys):
  585. self.store_spans(
  586. [
  587. self.create_span(
  588. {
  589. "description": "foo",
  590. "sentry_tags": {"status": "success"},
  591. "tags": {"bar": "bar2"},
  592. },
  593. measurements={k: {"value": (i + 1) / 10} for i, (k, _, _) in enumerate(keys)},
  594. start_ts=self.ten_mins_ago,
  595. ),
  596. ],
  597. is_eap=self.is_eap,
  598. )
  599. for i, (k, type, unit) in enumerate(keys):
  600. key = f"measurements.{k}"
  601. response = self.do_request(
  602. {
  603. "field": [key],
  604. "query": "description:foo",
  605. "project": self.project.id,
  606. "dataset": self.dataset,
  607. }
  608. )
  609. assert response.status_code == 200, response.content
  610. expected = {
  611. "dataset": mock.ANY,
  612. "datasetReason": "unchanged",
  613. "fields": {
  614. key: type,
  615. "id": "string",
  616. "project.name": "string",
  617. },
  618. "isMetricsData": False,
  619. "isMetricsExtractedData": False,
  620. "tips": {},
  621. "units": {
  622. key: unit,
  623. "id": None,
  624. "project.name": None,
  625. },
  626. }
  627. if self.use_rpc:
  628. expected["accuracy"] = {
  629. "confidence": [{}],
  630. }
  631. assert response.data["meta"] == expected
  632. assert response.data["data"] == [
  633. {
  634. key: pytest.approx((i + 1) / 10),
  635. "id": mock.ANY,
  636. "project.name": self.project.slug,
  637. }
  638. ]
  639. def test_simple_measurements(self):
  640. keys = [
  641. ("app_start_cold", "duration", "millisecond"),
  642. ("app_start_warm", "duration", "millisecond"),
  643. ("frames_frozen", "number", None), # should be integer but keeping it consistent
  644. ("frames_frozen_rate", "percentage", None),
  645. ("frames_slow", "number", None), # should be integer but keeping it consistent
  646. ("frames_slow_rate", "percentage", None),
  647. ("frames_total", "number", None), # should be integer but keeping it consistent
  648. ("time_to_initial_display", "duration", "millisecond"),
  649. ("time_to_full_display", "duration", "millisecond"),
  650. ("stall_count", "number", None), # should be integer but keeping it consistent
  651. ("stall_percentage", "percentage", None),
  652. ("stall_stall_longest_time", "number", None),
  653. ("stall_stall_total_time", "number", None),
  654. ("cls", "number", None),
  655. ("fcp", "duration", "millisecond"),
  656. ("fid", "duration", "millisecond"),
  657. ("fp", "duration", "millisecond"),
  658. ("inp", "duration", "millisecond"),
  659. ("lcp", "duration", "millisecond"),
  660. ("ttfb", "duration", "millisecond"),
  661. ("ttfb.requesttime", "duration", "millisecond"),
  662. ("score.cls", "number", None),
  663. ("score.fcp", "number", None),
  664. ("score.fid", "number", None),
  665. ("score.inp", "number", None),
  666. ("score.lcp", "number", None),
  667. ("score.ttfb", "number", None),
  668. ("score.total", "number", None),
  669. ("score.weight.cls", "number", None),
  670. ("score.weight.fcp", "number", None),
  671. ("score.weight.fid", "number", None),
  672. ("score.weight.inp", "number", None),
  673. ("score.weight.lcp", "number", None),
  674. ("score.weight.ttfb", "number", None),
  675. ("messaging.message.receive.latency", "duration", "millisecond"),
  676. ("messaging.message.retry.count", "number", None),
  677. # size fields aren't property support pre-RPC
  678. ("cache.item_size", "number", None),
  679. ("messaging.message.body.size", "number", None),
  680. ]
  681. self._test_simple_measurements(keys)
  682. def test_environment(self):
  683. self.create_environment(self.project, name="prod")
  684. self.create_environment(self.project, name="test")
  685. self.store_spans(
  686. [
  687. self.create_span(
  688. {"description": "foo", "sentry_tags": {"environment": "prod"}},
  689. start_ts=self.ten_mins_ago,
  690. ),
  691. self.create_span(
  692. {"description": "foo", "sentry_tags": {"environment": "test"}},
  693. start_ts=self.ten_mins_ago,
  694. ),
  695. ],
  696. is_eap=self.is_eap,
  697. )
  698. response = self.do_request(
  699. {
  700. "field": ["environment", "count()"],
  701. "project": self.project.id,
  702. "environment": "prod",
  703. "orderby": "environment",
  704. "dataset": self.dataset,
  705. }
  706. )
  707. assert response.status_code == 200, response.content
  708. assert response.data["data"] == [
  709. {"environment": "prod", "count()": 1},
  710. ]
  711. response = self.do_request(
  712. {
  713. "field": ["environment", "count()"],
  714. "project": self.project.id,
  715. "environment": ["prod", "test"],
  716. "orderby": "environment",
  717. "dataset": self.dataset,
  718. }
  719. )
  720. assert response.status_code == 200, response.content
  721. assert response.data["data"] == [
  722. {"environment": "prod", "count()": 1},
  723. {"environment": "test", "count()": 1},
  724. ]
  725. def test_transaction(self):
  726. self.store_spans(
  727. [
  728. self.create_span(
  729. {"description": "foo", "sentry_tags": {"transaction": "bar"}},
  730. start_ts=self.ten_mins_ago,
  731. ),
  732. ],
  733. is_eap=self.is_eap,
  734. )
  735. response = self.do_request(
  736. {
  737. "field": ["description", "count()"],
  738. "query": "transaction:bar",
  739. "orderby": "description",
  740. "project": self.project.id,
  741. "dataset": self.dataset,
  742. }
  743. )
  744. assert response.status_code == 200, response.content
  745. data = response.data["data"]
  746. meta = response.data["meta"]
  747. assert len(data) == 1
  748. assert data == [
  749. {
  750. "description": "foo",
  751. "count()": 1,
  752. },
  753. ]
  754. assert meta["dataset"] == self.dataset
  755. def test_orderby_alias(self):
  756. self.store_spans(
  757. [
  758. self.create_span(
  759. {"description": "foo", "sentry_tags": {"status": "success"}},
  760. start_ts=self.ten_mins_ago,
  761. ),
  762. self.create_span(
  763. {"description": "bar", "sentry_tags": {"status": "invalid_argument"}},
  764. duration=2000,
  765. start_ts=self.ten_mins_ago,
  766. ),
  767. ],
  768. is_eap=self.is_eap,
  769. )
  770. response = self.do_request(
  771. {
  772. "field": ["span.description", "sum(span.self_time)"],
  773. "query": "",
  774. "orderby": "sum_span_self_time",
  775. "project": self.project.id,
  776. "dataset": self.dataset,
  777. }
  778. )
  779. assert response.status_code == 200, response.content
  780. data = response.data["data"]
  781. meta = response.data["meta"]
  782. assert len(data) == 2
  783. assert data == [
  784. {
  785. "span.description": "foo",
  786. "sum(span.self_time)": 1000,
  787. },
  788. {
  789. "span.description": "bar",
  790. "sum(span.self_time)": 2000,
  791. },
  792. ]
  793. assert meta["dataset"] == self.dataset
  794. @pytest.mark.querybuilder
  795. def test_explore_sample_query(self):
  796. spans = [
  797. self.create_span(
  798. {"description": "foo", "sentry_tags": {"status": "success"}},
  799. start_ts=self.ten_mins_ago,
  800. ),
  801. self.create_span(
  802. {"description": "bar", "sentry_tags": {"status": "invalid_argument"}},
  803. start_ts=self.nine_mins_ago,
  804. ),
  805. ]
  806. self.store_spans(
  807. spans,
  808. is_eap=self.is_eap,
  809. )
  810. response = self.do_request(
  811. {
  812. "field": [
  813. "id",
  814. "project",
  815. "span.op",
  816. "span.description",
  817. "span.duration",
  818. "timestamp",
  819. "trace",
  820. "transaction.span_id",
  821. ],
  822. # This is to skip INP spans
  823. "query": "!transaction.span_id:00",
  824. "orderby": "timestamp",
  825. "statsPeriod": "1h",
  826. "project": self.project.id,
  827. "dataset": self.dataset,
  828. }
  829. )
  830. assert response.status_code == 200, response.content
  831. data = response.data["data"]
  832. meta = response.data["meta"]
  833. assert len(data) == 2
  834. for source, result in zip(spans, data):
  835. assert result["id"] == source["span_id"], "id"
  836. assert result["span.duration"] == 1000.0, "duration"
  837. assert result["span.op"] == "", "op"
  838. assert result["span.description"] == source["description"], "description"
  839. ts = datetime.fromisoformat(result["timestamp"])
  840. assert ts.tzinfo == timezone.utc
  841. assert ts.timestamp() == pytest.approx(
  842. source["end_timestamp_precise"], abs=5
  843. ), "timestamp"
  844. assert result["transaction.span_id"] == source["segment_id"], "transaction.span_id"
  845. assert result["project"] == result["project.name"] == self.project.slug, "project"
  846. assert meta["dataset"] == self.dataset
  847. def test_span_status(self):
  848. self.store_spans(
  849. [
  850. self.create_span(
  851. {"description": "foo", "sentry_tags": {"status": "internal_error"}},
  852. start_ts=self.ten_mins_ago,
  853. ),
  854. ],
  855. is_eap=self.is_eap,
  856. )
  857. response = self.do_request(
  858. {
  859. "field": ["description", "count()"],
  860. "query": "span.status:internal_error",
  861. "orderby": "description",
  862. "project": self.project.id,
  863. "dataset": self.dataset,
  864. }
  865. )
  866. assert response.status_code == 200, response.content
  867. data = response.data["data"]
  868. meta = response.data["meta"]
  869. assert len(data) == 1
  870. assert data == [
  871. {
  872. "description": "foo",
  873. "count()": 1,
  874. },
  875. ]
  876. assert meta["dataset"] == self.dataset
  877. def test_handle_nans_from_snuba(self):
  878. self.store_spans(
  879. [self.create_span({"description": "foo"}, start_ts=self.ten_mins_ago)],
  880. is_eap=self.is_eap,
  881. )
  882. response = self.do_request(
  883. {
  884. "field": ["description", "count()"],
  885. "query": "span.status:internal_error",
  886. "orderby": "description",
  887. "project": self.project.id,
  888. "dataset": self.dataset,
  889. }
  890. )
  891. assert response.status_code == 200, response.content
  892. def test_in_filter(self):
  893. self.store_spans(
  894. [
  895. self.create_span(
  896. {"description": "foo", "sentry_tags": {"transaction": "bar"}},
  897. start_ts=self.ten_mins_ago,
  898. ),
  899. self.create_span(
  900. {"description": "foo", "sentry_tags": {"transaction": "baz"}},
  901. start_ts=self.ten_mins_ago,
  902. ),
  903. self.create_span(
  904. {"description": "foo", "sentry_tags": {"transaction": "bat"}},
  905. start_ts=self.ten_mins_ago,
  906. ),
  907. ],
  908. is_eap=self.is_eap,
  909. )
  910. response = self.do_request(
  911. {
  912. "field": ["transaction", "count()"],
  913. "query": "transaction:[bar, baz]",
  914. "orderby": "transaction",
  915. "project": self.project.id,
  916. "dataset": self.dataset,
  917. }
  918. )
  919. assert response.status_code == 200, response.content
  920. data = response.data["data"]
  921. meta = response.data["meta"]
  922. assert len(data) == 2
  923. assert data == [
  924. {
  925. "transaction": "bar",
  926. "count()": 1,
  927. },
  928. {
  929. "transaction": "baz",
  930. "count()": 1,
  931. },
  932. ]
  933. assert meta["dataset"] == self.dataset
  934. def _test_aggregate_filter(self, queries):
  935. self.store_spans(
  936. [
  937. self.create_span(
  938. {"sentry_tags": {"transaction": "foo"}},
  939. measurements={
  940. "lcp": {"value": 5000},
  941. "http.response_content_length": {"value": 5000},
  942. },
  943. start_ts=self.ten_mins_ago,
  944. ),
  945. self.create_span(
  946. {"sentry_tags": {"transaction": "foo"}},
  947. measurements={
  948. "lcp": {"value": 5000},
  949. "http.response_content_length": {"value": 5000},
  950. },
  951. start_ts=self.ten_mins_ago,
  952. ),
  953. self.create_span(
  954. {"sentry_tags": {"transaction": "bar"}},
  955. measurements={
  956. "lcp": {"value": 1000},
  957. "http.response_content_length": {"value": 1000},
  958. },
  959. start_ts=self.ten_mins_ago,
  960. ),
  961. ],
  962. is_eap=self.is_eap,
  963. )
  964. for query in queries:
  965. response = self.do_request(
  966. {
  967. "field": ["transaction", "count()"],
  968. "query": query,
  969. "orderby": "transaction",
  970. "project": self.project.id,
  971. "dataset": self.dataset,
  972. }
  973. )
  974. assert response.status_code == 200, response.content
  975. data = response.data["data"]
  976. meta = response.data["meta"]
  977. assert len(data) == 1
  978. assert data[0]["transaction"] == "foo"
  979. assert data[0]["count()"] == 2
  980. assert meta["dataset"] == self.dataset
  981. def test_aggregate_filter(self):
  982. self._test_aggregate_filter(
  983. [
  984. "count():2",
  985. "count():>1",
  986. "avg(measurements.lcp):>3000",
  987. "avg(measurements.lcp):>3s",
  988. "count():>1 avg(measurements.lcp):>3000",
  989. "count():>1 AND avg(measurements.lcp):>3000",
  990. "count():>1 OR avg(measurements.lcp):>3000",
  991. ]
  992. )
  993. def test_pagination_samples(self):
  994. self.store_spans(
  995. [
  996. self.create_span(
  997. {"description": "a"},
  998. start_ts=self.ten_mins_ago,
  999. ),
  1000. self.create_span(
  1001. {"description": "b"},
  1002. start_ts=self.ten_mins_ago,
  1003. ),
  1004. ],
  1005. is_eap=self.is_eap,
  1006. )
  1007. response = self.do_request(
  1008. {
  1009. "field": ["description"],
  1010. "query": "",
  1011. "orderby": "description",
  1012. "project": self.project.id,
  1013. "dataset": self.dataset,
  1014. "per_page": 1,
  1015. }
  1016. )
  1017. assert response.status_code == 200, response.content
  1018. assert response.data["data"] == [
  1019. {
  1020. "id": mock.ANY,
  1021. "project.name": self.project.slug,
  1022. "description": "a",
  1023. },
  1024. ]
  1025. links = {}
  1026. for url, attrs in parse_link_header(response["Link"]).items():
  1027. links[attrs["rel"]] = attrs
  1028. attrs["href"] = url
  1029. assert links["previous"]["results"] == "false"
  1030. assert links["next"]["results"] == "true"
  1031. assert links["next"]["href"] is not None
  1032. response = self.client.get(links["next"]["href"], format="json")
  1033. assert response.status_code == 200, response.content
  1034. assert response.data["data"] == [
  1035. {
  1036. "id": mock.ANY,
  1037. "project.name": self.project.slug,
  1038. "description": "b",
  1039. },
  1040. ]
  1041. links = {}
  1042. for url, attrs in parse_link_header(response["Link"]).items():
  1043. links[attrs["rel"]] = attrs
  1044. attrs["href"] = url
  1045. assert links["previous"]["results"] == "true"
  1046. assert links["next"]["results"] == "false"
  1047. assert links["previous"]["href"] is not None
  1048. response = self.client.get(links["previous"]["href"], format="json")
  1049. assert response.status_code == 200, response.content
  1050. assert response.data["data"] == [
  1051. {
  1052. "id": mock.ANY,
  1053. "project.name": self.project.slug,
  1054. "description": "a",
  1055. },
  1056. ]
  1057. links = {}
  1058. for url, attrs in parse_link_header(response["Link"]).items():
  1059. links[attrs["rel"]] = attrs
  1060. attrs["href"] = url
  1061. assert links["previous"]["results"] == "false"
  1062. assert links["next"]["results"] == "true"
  1063. def test_precise_timestamps(self):
  1064. self.store_spans(
  1065. [
  1066. self.create_span(
  1067. {"description": "foo", "sentry_tags": {"status": "success"}},
  1068. start_ts=self.ten_mins_ago,
  1069. ),
  1070. ],
  1071. is_eap=self.is_eap,
  1072. )
  1073. response = self.do_request(
  1074. {
  1075. "field": ["precise.start_ts", "precise.finish_ts"],
  1076. "project": self.project.id,
  1077. "dataset": self.dataset,
  1078. }
  1079. )
  1080. start = self.ten_mins_ago.timestamp()
  1081. finish = start + 1
  1082. assert response.status_code == 200, response.content
  1083. assert response.data["data"] == [
  1084. {
  1085. "id": mock.ANY,
  1086. "project.name": self.project.slug,
  1087. "precise.start_ts": start,
  1088. "precise.finish_ts": finish,
  1089. },
  1090. ]
  1091. def test_replay_id(self):
  1092. self.store_spans(
  1093. [
  1094. self.create_span(
  1095. {"description": "foo", "sentry_tags": {"replay_id": "123"}},
  1096. start_ts=self.ten_mins_ago,
  1097. ),
  1098. self.create_span(
  1099. {"description": "foo", "tags": {"replayId": "321"}},
  1100. start_ts=self.ten_mins_ago,
  1101. ),
  1102. ],
  1103. is_eap=self.is_eap,
  1104. )
  1105. response = self.do_request(
  1106. {
  1107. "field": ["replay"],
  1108. "project": self.project.id,
  1109. "dataset": self.dataset,
  1110. "orderby": "replay",
  1111. }
  1112. )
  1113. assert response.status_code == 200, response.content
  1114. assert response.data["data"] == [
  1115. {
  1116. "id": mock.ANY,
  1117. "project.name": self.project.slug,
  1118. "replay": "123",
  1119. },
  1120. {
  1121. "id": mock.ANY,
  1122. "project.name": self.project.slug,
  1123. "replay": "321",
  1124. },
  1125. ]
  1126. def test_user_display(self):
  1127. self.store_spans(
  1128. [
  1129. self.create_span(
  1130. {"description": "foo", "sentry_tags": {"user.email": "test@test.com"}},
  1131. start_ts=self.ten_mins_ago,
  1132. ),
  1133. self.create_span(
  1134. {"description": "foo", "sentry_tags": {"user.username": "test"}},
  1135. start_ts=self.ten_mins_ago,
  1136. ),
  1137. ],
  1138. is_eap=self.is_eap,
  1139. )
  1140. response = self.do_request(
  1141. {
  1142. "field": ["user.display"],
  1143. "project": self.project.id,
  1144. "dataset": self.dataset,
  1145. "orderby": "user.display",
  1146. }
  1147. )
  1148. assert response.status_code == 200, response.content
  1149. assert response.data["data"] == [
  1150. {
  1151. "id": mock.ANY,
  1152. "project.name": self.project.slug,
  1153. "user.display": "test",
  1154. },
  1155. {
  1156. "id": mock.ANY,
  1157. "project.name": self.project.slug,
  1158. "user.display": "test@test.com",
  1159. },
  1160. ]
  1161. def test_query_with_asterisk(self):
  1162. self.store_spans(
  1163. [
  1164. self.create_span(
  1165. {"description": "select * from database"},
  1166. start_ts=self.ten_mins_ago,
  1167. ),
  1168. ],
  1169. is_eap=self.is_eap,
  1170. )
  1171. response = self.do_request(
  1172. {
  1173. "field": ["span.description"],
  1174. "query": 'span.description:"select \\* from database"',
  1175. "project": self.project.id,
  1176. "dataset": self.dataset,
  1177. }
  1178. )
  1179. assert response.status_code == 200, response.content
  1180. assert len(response.data["data"]) == 1
  1181. assert response.data["data"][0]["span.description"] == "select * from database"
  1182. class OrganizationEventsEAPSpanEndpointTest(OrganizationEventsSpanIndexedEndpointTest):
  1183. is_eap = True
  1184. use_rpc = False
  1185. def test_simple(self):
  1186. self.store_spans(
  1187. [
  1188. self.create_span(
  1189. {"description": "foo", "sentry_tags": {"status": "success"}},
  1190. start_ts=self.ten_mins_ago,
  1191. ),
  1192. self.create_span(
  1193. {"description": "bar", "sentry_tags": {"status": "invalid_argument"}},
  1194. start_ts=self.ten_mins_ago,
  1195. ),
  1196. ],
  1197. is_eap=self.is_eap,
  1198. )
  1199. response = self.do_request(
  1200. {
  1201. "field": ["span.status", "description", "count()"],
  1202. "query": "",
  1203. "orderby": "description",
  1204. "project": self.project.id,
  1205. "dataset": self.dataset,
  1206. }
  1207. )
  1208. assert response.status_code == 200, response.content
  1209. data = response.data["data"]
  1210. meta = response.data["meta"]
  1211. assert len(data) == 2
  1212. assert data == [
  1213. {
  1214. "span.status": "invalid_argument",
  1215. "description": "bar",
  1216. "count()": 1,
  1217. },
  1218. {
  1219. "span.status": "success",
  1220. "description": "foo",
  1221. "count()": 1,
  1222. },
  1223. ]
  1224. assert meta["dataset"] == self.dataset
  1225. @pytest.mark.xfail(reason="event_id isn't being written to the new table")
  1226. def test_id_filtering(self):
  1227. super().test_id_filtering()
  1228. def test_span_duration(self):
  1229. spans = [
  1230. self.create_span(
  1231. {"description": "bar", "sentry_tags": {"status": "invalid_argument"}},
  1232. start_ts=self.ten_mins_ago,
  1233. ),
  1234. self.create_span(
  1235. {"description": "foo", "sentry_tags": {"status": "success"}},
  1236. start_ts=self.ten_mins_ago,
  1237. ),
  1238. ]
  1239. self.store_spans(spans, is_eap=self.is_eap)
  1240. response = self.do_request(
  1241. {
  1242. "field": ["span.duration", "description"],
  1243. "query": "",
  1244. "orderby": "description",
  1245. "project": self.project.id,
  1246. "dataset": self.dataset,
  1247. }
  1248. )
  1249. assert response.status_code == 200, response.content
  1250. data = response.data["data"]
  1251. meta = response.data["meta"]
  1252. assert len(data) == 2
  1253. assert data == [
  1254. {
  1255. "span.duration": 1000.0,
  1256. "description": "bar",
  1257. "project.name": self.project.slug,
  1258. "id": spans[0]["span_id"],
  1259. },
  1260. {
  1261. "span.duration": 1000.0,
  1262. "description": "foo",
  1263. "project.name": self.project.slug,
  1264. "id": spans[1]["span_id"],
  1265. },
  1266. ]
  1267. assert meta["dataset"] == self.dataset
  1268. @pytest.mark.xfail
  1269. def test_aggregate_numeric_attr(self):
  1270. self.store_spans(
  1271. [
  1272. self.create_span(
  1273. {
  1274. "description": "foo",
  1275. "sentry_tags": {"status": "success"},
  1276. "tags": {"bar": "bar1"},
  1277. },
  1278. start_ts=self.ten_mins_ago,
  1279. ),
  1280. self.create_span(
  1281. {
  1282. "description": "foo",
  1283. "sentry_tags": {"status": "success"},
  1284. "tags": {"bar": "bar2"},
  1285. },
  1286. measurements={"foo": {"value": 5}},
  1287. start_ts=self.ten_mins_ago,
  1288. ),
  1289. self.create_span(
  1290. {
  1291. "description": "foo",
  1292. "sentry_tags": {"status": "success"},
  1293. "tags": {"bar": "bar3"},
  1294. },
  1295. start_ts=self.ten_mins_ago,
  1296. ),
  1297. ],
  1298. is_eap=self.is_eap,
  1299. )
  1300. response = self.do_request(
  1301. {
  1302. "field": [
  1303. "description",
  1304. "count_unique(bar)",
  1305. "count_unique(tags[bar])",
  1306. "count_unique(tags[bar,string])",
  1307. "count()",
  1308. "count(span.duration)",
  1309. "count(tags[foo, number])",
  1310. "sum(tags[foo,number])",
  1311. "avg(tags[foo,number])",
  1312. "p50(tags[foo,number])",
  1313. "p75(tags[foo,number])",
  1314. "p95(tags[foo,number])",
  1315. "p99(tags[foo,number])",
  1316. "p100(tags[foo,number])",
  1317. "min(tags[foo,number])",
  1318. "max(tags[foo,number])",
  1319. ],
  1320. "query": "",
  1321. "orderby": "description",
  1322. "project": self.project.id,
  1323. "dataset": self.dataset,
  1324. }
  1325. )
  1326. assert response.status_code == 200, response.content
  1327. assert len(response.data["data"]) == 1
  1328. data = response.data["data"]
  1329. assert data[0] == {
  1330. "description": "foo",
  1331. "count_unique(bar)": 3,
  1332. "count_unique(tags[bar])": 3,
  1333. "count_unique(tags[bar,string])": 3,
  1334. "count()": 3,
  1335. "count(span.duration)": 3,
  1336. "count(tags[foo, number])": 1,
  1337. "sum(tags[foo,number])": 5.0,
  1338. "avg(tags[foo,number])": 5.0,
  1339. "p50(tags[foo,number])": 5.0,
  1340. "p75(tags[foo,number])": 5.0,
  1341. "p95(tags[foo,number])": 5.0,
  1342. "p99(tags[foo,number])": 5.0,
  1343. "p100(tags[foo,number])": 5.0,
  1344. "min(tags[foo,number])": 5.0,
  1345. "max(tags[foo,number])": 5.0,
  1346. }
  1347. def test_numeric_attr_without_space(self):
  1348. self.store_spans(
  1349. [
  1350. self.create_span(
  1351. {
  1352. "description": "foo",
  1353. "sentry_tags": {"status": "success"},
  1354. "tags": {"foo": "five"},
  1355. },
  1356. measurements={"foo": {"value": 5}},
  1357. start_ts=self.ten_mins_ago,
  1358. ),
  1359. ],
  1360. is_eap=self.is_eap,
  1361. )
  1362. response = self.do_request(
  1363. {
  1364. "field": ["description", "tags[foo,number]", "tags[foo,string]", "tags[foo]"],
  1365. "query": "",
  1366. "orderby": "description",
  1367. "project": self.project.id,
  1368. "dataset": self.dataset,
  1369. }
  1370. )
  1371. assert response.status_code == 200, response.content
  1372. assert len(response.data["data"]) == 1
  1373. data = response.data["data"]
  1374. assert data[0]["tags[foo,number]"] == 5
  1375. assert data[0]["tags[foo,string]"] == "five"
  1376. assert data[0]["tags[foo]"] == "five"
  1377. def test_numeric_attr_with_spaces(self):
  1378. self.store_spans(
  1379. [
  1380. self.create_span(
  1381. {
  1382. "description": "foo",
  1383. "sentry_tags": {"status": "success"},
  1384. "tags": {"foo": "five"},
  1385. },
  1386. measurements={"foo": {"value": 5}},
  1387. start_ts=self.ten_mins_ago,
  1388. ),
  1389. ],
  1390. is_eap=self.is_eap,
  1391. )
  1392. response = self.do_request(
  1393. {
  1394. "field": ["description", "tags[foo, number]", "tags[foo, string]", "tags[foo]"],
  1395. "query": "",
  1396. "orderby": "description",
  1397. "project": self.project.id,
  1398. "dataset": self.dataset,
  1399. }
  1400. )
  1401. assert response.status_code == 200, response.content
  1402. assert len(response.data["data"]) == 1
  1403. data = response.data["data"]
  1404. assert data[0]["tags[foo, number]"] == 5
  1405. assert data[0]["tags[foo, string]"] == "five"
  1406. assert data[0]["tags[foo]"] == "five"
  1407. def test_numeric_attr_filtering(self):
  1408. self.store_spans(
  1409. [
  1410. self.create_span(
  1411. {
  1412. "description": "foo",
  1413. "sentry_tags": {"status": "success"},
  1414. "tags": {"foo": "five"},
  1415. },
  1416. measurements={"foo": {"value": 5}},
  1417. start_ts=self.ten_mins_ago,
  1418. ),
  1419. self.create_span(
  1420. {"description": "bar", "sentry_tags": {"status": "success", "foo": "five"}},
  1421. measurements={"foo": {"value": 8}},
  1422. start_ts=self.ten_mins_ago,
  1423. ),
  1424. ],
  1425. is_eap=self.is_eap,
  1426. )
  1427. response = self.do_request(
  1428. {
  1429. "field": ["description", "tags[foo,number]"],
  1430. "query": "tags[foo,number]:5",
  1431. "orderby": "description",
  1432. "project": self.project.id,
  1433. "dataset": self.dataset,
  1434. }
  1435. )
  1436. assert response.status_code == 200, response.content
  1437. assert len(response.data["data"]) == 1
  1438. data = response.data["data"]
  1439. assert data[0]["tags[foo,number]"] == 5
  1440. assert data[0]["description"] == "foo"
  1441. def test_long_attr_name(self):
  1442. response = self.do_request(
  1443. {
  1444. "field": ["description", "z" * 201],
  1445. "query": "",
  1446. "orderby": "description",
  1447. "project": self.project.id,
  1448. "dataset": self.dataset,
  1449. }
  1450. )
  1451. assert response.status_code == 400, response.content
  1452. assert "Is Too Long" in response.data["detail"].title()
  1453. def test_numeric_attr_orderby(self):
  1454. self.store_spans(
  1455. [
  1456. self.create_span(
  1457. {
  1458. "description": "baz",
  1459. "sentry_tags": {"status": "success"},
  1460. "tags": {"foo": "five"},
  1461. },
  1462. measurements={"foo": {"value": 71}},
  1463. start_ts=self.ten_mins_ago,
  1464. ),
  1465. self.create_span(
  1466. {
  1467. "description": "foo",
  1468. "sentry_tags": {"status": "success"},
  1469. "tags": {"foo": "five"},
  1470. },
  1471. measurements={"foo": {"value": 5}},
  1472. start_ts=self.ten_mins_ago,
  1473. ),
  1474. self.create_span(
  1475. {
  1476. "description": "bar",
  1477. "sentry_tags": {"status": "success"},
  1478. "tags": {"foo": "five"},
  1479. },
  1480. measurements={"foo": {"value": 8}},
  1481. start_ts=self.ten_mins_ago,
  1482. ),
  1483. ],
  1484. is_eap=self.is_eap,
  1485. )
  1486. response = self.do_request(
  1487. {
  1488. "field": ["description", "tags[foo,number]"],
  1489. "query": "",
  1490. "orderby": ["tags[foo,number]"],
  1491. "project": self.project.id,
  1492. "dataset": self.dataset,
  1493. }
  1494. )
  1495. assert response.status_code == 200, response.content
  1496. assert len(response.data["data"]) == 3
  1497. data = response.data["data"]
  1498. assert data[0]["tags[foo,number]"] == 5
  1499. assert data[0]["description"] == "foo"
  1500. assert data[1]["tags[foo,number]"] == 8
  1501. assert data[1]["description"] == "bar"
  1502. assert data[2]["tags[foo,number]"] == 71
  1503. assert data[2]["description"] == "baz"
  1504. def test_margin_of_error(self):
  1505. total_samples = 10
  1506. in_group = 5
  1507. spans = []
  1508. for _ in range(in_group):
  1509. spans.append(
  1510. self.create_span(
  1511. {
  1512. "description": "foo",
  1513. "sentry_tags": {"status": "success"},
  1514. "measurements": {"client_sample_rate": {"value": 0.00001}},
  1515. },
  1516. start_ts=self.ten_mins_ago,
  1517. )
  1518. )
  1519. for _ in range(total_samples - in_group):
  1520. spans.append(
  1521. self.create_span(
  1522. {
  1523. "description": "bar",
  1524. "sentry_tags": {"status": "success"},
  1525. "measurements": {"client_sample_rate": {"value": 0.00001}},
  1526. },
  1527. )
  1528. )
  1529. self.store_spans(
  1530. spans,
  1531. is_eap=self.is_eap,
  1532. )
  1533. response = self.do_request(
  1534. {
  1535. "field": [
  1536. "margin_of_error()",
  1537. "lower_count_limit()",
  1538. "upper_count_limit()",
  1539. "count()",
  1540. ],
  1541. "query": "description:foo",
  1542. "project": self.project.id,
  1543. "dataset": self.dataset,
  1544. }
  1545. )
  1546. assert response.status_code == 200, response.content
  1547. assert len(response.data["data"]) == 1
  1548. data = response.data["data"][0]
  1549. margin_of_error = data["margin_of_error()"]
  1550. lower_limit = data["lower_count_limit()"]
  1551. upper_limit = data["upper_count_limit()"]
  1552. extrapolated = data["count()"]
  1553. assert margin_of_error == pytest.approx(0.306, rel=1e-1)
  1554. # How to read this; these results mean that the extrapolated count is
  1555. # 500k, with a lower estimated bound of ~200k, and an upper bound of 800k
  1556. assert lower_limit == pytest.approx(190_000, abs=5000)
  1557. assert extrapolated == pytest.approx(500_000, abs=5000)
  1558. assert upper_limit == pytest.approx(810_000, abs=5000)
  1559. def test_skip_aggregate_conditions_option(self):
  1560. span_1 = self.create_span(
  1561. {"description": "foo", "sentry_tags": {"status": "success"}},
  1562. start_ts=self.ten_mins_ago,
  1563. )
  1564. span_2 = self.create_span(
  1565. {"description": "bar", "sentry_tags": {"status": "invalid_argument"}},
  1566. start_ts=self.ten_mins_ago,
  1567. )
  1568. self.store_spans(
  1569. [span_1, span_2],
  1570. is_eap=self.is_eap,
  1571. )
  1572. response = self.do_request(
  1573. {
  1574. "field": ["description"],
  1575. "query": "description:foo count():>1",
  1576. "orderby": "description",
  1577. "project": self.project.id,
  1578. "dataset": self.dataset,
  1579. "allowAggregateConditions": "0",
  1580. }
  1581. )
  1582. assert response.status_code == 200, response.content
  1583. data = response.data["data"]
  1584. meta = response.data["meta"]
  1585. assert len(data) == 1
  1586. assert data == [
  1587. {
  1588. "description": "foo",
  1589. "project.name": self.project.slug,
  1590. "id": span_1["span_id"],
  1591. },
  1592. ]
  1593. assert meta["dataset"] == self.dataset
  1594. def test_span_data_fields_http_resource(self):
  1595. self.store_spans(
  1596. [
  1597. self.create_span(
  1598. {
  1599. "op": "resource.img",
  1600. "description": "/image/",
  1601. "data": {
  1602. "http.decoded_response_content_length": 1,
  1603. "http.response_content_length": 2,
  1604. "http.response_transfer_size": 3,
  1605. },
  1606. },
  1607. start_ts=self.ten_mins_ago,
  1608. ),
  1609. ],
  1610. is_eap=self.is_eap,
  1611. )
  1612. response = self.do_request(
  1613. {
  1614. "field": [
  1615. "http.decoded_response_content_length",
  1616. "http.response_content_length",
  1617. "http.response_transfer_size",
  1618. ],
  1619. "query": "http.decoded_response_content_length:>0 http.response_content_length:>0 http.response_transfer_size:>0",
  1620. "project": self.project.id,
  1621. "dataset": self.dataset,
  1622. "allowAggregateConditions": "0",
  1623. }
  1624. )
  1625. assert response.status_code == 200, response.content
  1626. assert response.data["data"] == [
  1627. {
  1628. "http.decoded_response_content_length": 1,
  1629. "http.response_content_length": 2,
  1630. "http.response_transfer_size": 3,
  1631. "project.name": self.project.slug,
  1632. "id": mock.ANY,
  1633. },
  1634. ]
  1635. expected = {
  1636. "dataset": mock.ANY,
  1637. "datasetReason": "unchanged",
  1638. "fields": {
  1639. "http.decoded_response_content_length": "size",
  1640. "http.response_content_length": "size",
  1641. "http.response_transfer_size": "size",
  1642. "id": "string",
  1643. "project.name": "string",
  1644. },
  1645. "isMetricsData": False,
  1646. "isMetricsExtractedData": False,
  1647. "tips": {},
  1648. "units": {
  1649. "http.decoded_response_content_length": "byte",
  1650. "http.response_content_length": "byte",
  1651. "http.response_transfer_size": "byte",
  1652. "id": None,
  1653. "project.name": None,
  1654. },
  1655. }
  1656. if self.use_rpc:
  1657. expected["accuracy"] = {
  1658. "confidence": [{}],
  1659. }
  1660. assert response.data["meta"] == expected
  1661. def test_filtering_numeric_attr(self):
  1662. span_1 = self.create_span(
  1663. {"description": "foo"},
  1664. measurements={"foo": {"value": 30}},
  1665. start_ts=self.ten_mins_ago,
  1666. )
  1667. span_2 = self.create_span(
  1668. {"description": "foo"},
  1669. measurements={"foo": {"value": 10}},
  1670. start_ts=self.ten_mins_ago,
  1671. )
  1672. self.store_spans([span_1, span_2], is_eap=self.is_eap)
  1673. response = self.do_request(
  1674. {
  1675. "field": ["tags[foo,number]"],
  1676. "query": "span.duration:>=0 tags[foo,number]:>20",
  1677. "project": self.project.id,
  1678. "dataset": self.dataset,
  1679. }
  1680. )
  1681. assert response.status_code == 200, response.content
  1682. assert response.data["data"] == [
  1683. {
  1684. "id": span_1["span_id"],
  1685. "project.name": self.project.slug,
  1686. "tags[foo,number]": 30,
  1687. },
  1688. ]
  1689. def test_byte_fields(self):
  1690. self.store_spans(
  1691. [
  1692. self.create_span(
  1693. {
  1694. "description": "foo",
  1695. "data": {
  1696. "cache.item_size": 1,
  1697. "messaging.message.body.size": 2,
  1698. },
  1699. },
  1700. start_ts=self.ten_mins_ago,
  1701. ),
  1702. ],
  1703. is_eap=self.is_eap,
  1704. )
  1705. response = self.do_request(
  1706. {
  1707. "field": [
  1708. "cache.item_size",
  1709. "measurements.cache.item_size",
  1710. "messaging.message.body.size",
  1711. "measurements.messaging.message.body.size",
  1712. ],
  1713. "project": self.project.id,
  1714. "dataset": self.dataset,
  1715. }
  1716. )
  1717. assert response.data["data"] == [
  1718. {
  1719. "id": mock.ANY,
  1720. "project.name": self.project.slug,
  1721. "cache.item_size": 1.0,
  1722. "measurements.cache.item_size": 1.0,
  1723. "measurements.messaging.message.body.size": 2.0,
  1724. "messaging.message.body.size": 2.0,
  1725. },
  1726. ]
  1727. def test_aggregate_filter(self):
  1728. self._test_aggregate_filter(
  1729. [
  1730. "count():2",
  1731. "count():>1",
  1732. "avg(measurements.lcp):>3000",
  1733. "avg(measurements.lcp):>3s",
  1734. "count():>1 avg(measurements.lcp):>3000",
  1735. "count():>1 AND avg(measurements.lcp):>3000",
  1736. "count():>1 OR avg(measurements.lcp):>3000",
  1737. "(count():>1 AND avg(http.response_content_length):>3000) OR (count():>1 AND avg(measurements.lcp):>3000)",
  1738. ]
  1739. )
  1740. class OrganizationEventsEAPRPCSpanEndpointTest(OrganizationEventsEAPSpanEndpointTest):
  1741. is_eap = True
  1742. use_rpc = True
  1743. @mock.patch(
  1744. "sentry.utils.snuba_rpc._snuba_pool.urlopen", side_effect=urllib3.exceptions.TimeoutError
  1745. )
  1746. def test_timeout(self, mock_rpc):
  1747. response = self.do_request(
  1748. {
  1749. "field": ["span.status", "description", "count()"],
  1750. "query": "",
  1751. "orderby": "description",
  1752. "project": self.project.id,
  1753. "dataset": self.dataset,
  1754. }
  1755. )
  1756. assert response.status_code == 400, response.content
  1757. assert "Query timeout" in response.data["detail"]
  1758. def test_extrapolation(self):
  1759. """Extrapolation only changes the number when there's a sample rate"""
  1760. spans = []
  1761. spans.append(
  1762. self.create_span(
  1763. {
  1764. "description": "foo",
  1765. "sentry_tags": {"status": "success"},
  1766. "measurements": {"client_sample_rate": {"value": 0.1}},
  1767. },
  1768. start_ts=self.ten_mins_ago,
  1769. )
  1770. )
  1771. spans.append(
  1772. self.create_span(
  1773. {
  1774. "description": "bar",
  1775. "sentry_tags": {"status": "success"},
  1776. },
  1777. start_ts=self.ten_mins_ago,
  1778. )
  1779. )
  1780. self.store_spans(spans, is_eap=self.is_eap)
  1781. response = self.do_request(
  1782. {
  1783. "field": ["description", "count()"],
  1784. "orderby": "-count()",
  1785. "query": "",
  1786. "project": self.project.id,
  1787. "dataset": self.dataset,
  1788. }
  1789. )
  1790. assert response.status_code == 200, response.content
  1791. data = response.data["data"]
  1792. meta = response.data["meta"]
  1793. confidence = meta["accuracy"]["confidence"]
  1794. assert len(data) == 2
  1795. assert len(confidence) == 2
  1796. assert data[0]["count()"] == 10
  1797. assert confidence[0]["count()"] == "low"
  1798. assert data[1]["count()"] == 1
  1799. # While logically the confidence for 1 event at 100% sample rate should be high, we're going with low until we
  1800. # get customer feedback
  1801. assert confidence[1]["count()"] == "low"
  1802. def test_span_duration(self):
  1803. spans = [
  1804. self.create_span(
  1805. {"description": "bar", "sentry_tags": {"status": "invalid_argument"}},
  1806. start_ts=self.ten_mins_ago,
  1807. ),
  1808. self.create_span(
  1809. {"description": "foo", "sentry_tags": {"status": "success"}},
  1810. start_ts=self.ten_mins_ago,
  1811. ),
  1812. ]
  1813. self.store_spans(spans, is_eap=self.is_eap)
  1814. response = self.do_request(
  1815. {
  1816. "field": ["span.duration", "description"],
  1817. "query": "",
  1818. "orderby": "description",
  1819. "project": self.project.id,
  1820. "dataset": self.dataset,
  1821. }
  1822. )
  1823. assert response.status_code == 200, response.content
  1824. data = response.data["data"]
  1825. meta = response.data["meta"]
  1826. assert len(data) == 2
  1827. assert data == [
  1828. {
  1829. "span.duration": 1000.0,
  1830. "description": "bar",
  1831. "project.name": self.project.slug,
  1832. "id": spans[0]["span_id"],
  1833. },
  1834. {
  1835. "span.duration": 1000.0,
  1836. "description": "foo",
  1837. "project.name": self.project.slug,
  1838. "id": spans[1]["span_id"],
  1839. },
  1840. ]
  1841. assert meta["dataset"] == self.dataset
  1842. def test_average_sampling_rate(self):
  1843. spans = []
  1844. spans.append(
  1845. self.create_span(
  1846. {
  1847. "description": "foo",
  1848. "sentry_tags": {"status": "success"},
  1849. "measurements": {"client_sample_rate": {"value": 0.1}},
  1850. },
  1851. start_ts=self.ten_mins_ago,
  1852. )
  1853. )
  1854. spans.append(
  1855. self.create_span(
  1856. {
  1857. "description": "bar",
  1858. "sentry_tags": {"status": "success"},
  1859. "measurements": {"client_sample_rate": {"value": 0.85}},
  1860. },
  1861. start_ts=self.ten_mins_ago,
  1862. )
  1863. )
  1864. self.store_spans(spans, is_eap=self.is_eap)
  1865. response = self.do_request(
  1866. {
  1867. "field": [
  1868. "avg_sample(sampling_rate)",
  1869. "count()",
  1870. "min(sampling_rate)",
  1871. "count_sample()",
  1872. ],
  1873. "query": "",
  1874. "project": self.project.id,
  1875. "dataset": self.dataset,
  1876. }
  1877. )
  1878. assert response.status_code == 200, response.content
  1879. data = response.data["data"]
  1880. meta = response.data["meta"]
  1881. confidence = meta["accuracy"]["confidence"]
  1882. assert len(data) == 1
  1883. assert data[0]["avg_sample(sampling_rate)"] == pytest.approx(0.475)
  1884. assert data[0]["min(sampling_rate)"] == pytest.approx(0.1)
  1885. assert data[0]["count_sample()"] == 2
  1886. assert data[0]["count()"] == 11
  1887. assert confidence[0]["count()"] == "low"
  1888. def test_aggregate_numeric_attr(self):
  1889. self.store_spans(
  1890. [
  1891. self.create_span(
  1892. {
  1893. "description": "foo",
  1894. "sentry_tags": {"status": "success"},
  1895. "tags": {"bar": "bar1"},
  1896. },
  1897. start_ts=self.ten_mins_ago,
  1898. ),
  1899. self.create_span(
  1900. {
  1901. "description": "foo",
  1902. "sentry_tags": {"status": "success"},
  1903. "tags": {"bar": "bar2"},
  1904. },
  1905. measurements={"foo": {"value": 5}},
  1906. start_ts=self.ten_mins_ago,
  1907. ),
  1908. ],
  1909. is_eap=self.is_eap,
  1910. )
  1911. response = self.do_request(
  1912. {
  1913. "field": [
  1914. "description",
  1915. "count_unique(bar)",
  1916. "count_unique(tags[bar])",
  1917. "count_unique(tags[bar,string])",
  1918. "count()",
  1919. "count(span.duration)",
  1920. "count(tags[foo, number])",
  1921. "sum(tags[foo,number])",
  1922. "avg(tags[foo,number])",
  1923. "p50(tags[foo,number])",
  1924. "p75(tags[foo,number])",
  1925. "p95(tags[foo,number])",
  1926. "p99(tags[foo,number])",
  1927. "p100(tags[foo,number])",
  1928. "min(tags[foo,number])",
  1929. "max(tags[foo,number])",
  1930. ],
  1931. "query": "",
  1932. "orderby": "description",
  1933. "project": self.project.id,
  1934. "dataset": self.dataset,
  1935. }
  1936. )
  1937. assert response.status_code == 200, response.content
  1938. assert len(response.data["data"]) == 1
  1939. data = response.data["data"]
  1940. assert data[0] == {
  1941. "description": "foo",
  1942. "count_unique(bar)": 2,
  1943. "count_unique(tags[bar])": 2,
  1944. "count_unique(tags[bar,string])": 2,
  1945. "count()": 2,
  1946. "count(span.duration)": 2,
  1947. "count(tags[foo, number])": 1,
  1948. "sum(tags[foo,number])": 5.0,
  1949. "avg(tags[foo,number])": 5.0,
  1950. "p50(tags[foo,number])": 5.0,
  1951. "p75(tags[foo,number])": 5.0,
  1952. "p95(tags[foo,number])": 5.0,
  1953. "p99(tags[foo,number])": 5.0,
  1954. "p100(tags[foo,number])": 5.0,
  1955. "min(tags[foo,number])": 5.0,
  1956. "max(tags[foo,number])": 5.0,
  1957. }
  1958. @pytest.mark.skip(reason="margin will not be moved to the RPC")
  1959. def test_margin_of_error(self):
  1960. super().test_margin_of_error()
  1961. @pytest.mark.skip(reason="module not migrated over")
  1962. def test_module_alias(self):
  1963. super().test_module_alias()
  1964. @pytest.mark.xfail(
  1965. reason="wip: depends on rpc having a way to set a different default in virtual contexts"
  1966. )
  1967. def test_span_module(self):
  1968. super().test_span_module()
  1969. def test_inp_span(self):
  1970. replay_id = uuid.uuid4().hex
  1971. self.store_spans(
  1972. [
  1973. self.create_span(
  1974. {
  1975. "sentry_tags": {
  1976. "replay_id": replay_id,
  1977. "browser.name": "Chrome",
  1978. "transaction": "/pageloads/",
  1979. }
  1980. },
  1981. start_ts=self.ten_mins_ago,
  1982. ),
  1983. ],
  1984. is_eap=self.is_eap,
  1985. )
  1986. response = self.do_request(
  1987. {
  1988. # Not moving origin.transaction to RPC, its equivalent to transaction and just represents the
  1989. # transaction that's related to the span
  1990. "field": ["replay.id", "browser.name", "transaction", "count()"],
  1991. "query": f"replay.id:{replay_id} AND browser.name:Chrome AND transaction:/pageloads/",
  1992. "orderby": "count()",
  1993. "project": self.project.id,
  1994. "dataset": self.dataset,
  1995. }
  1996. )
  1997. assert response.status_code == 200, response.content
  1998. data = response.data["data"]
  1999. meta = response.data["meta"]
  2000. assert len(data) == 1
  2001. assert data[0]["replay.id"] == replay_id
  2002. assert data[0]["browser.name"] == "Chrome"
  2003. assert data[0]["transaction"] == "/pageloads/"
  2004. assert meta["dataset"] == self.dataset
  2005. @pytest.mark.xfail(
  2006. reason="wip: depends on rpc having a way to set a different default in virtual contexts"
  2007. )
  2008. # https://github.com/getsentry/projects/issues/215?issue=getsentry%7Cprojects%7C488
  2009. def test_other_category_span(self):
  2010. super().test_other_category_span()
  2011. @pytest.mark.xfail(
  2012. reason="wip: not implemented yet, depends on rpc having a way to filter based on casing"
  2013. )
  2014. # https://github.com/getsentry/projects/issues/215?issue=getsentry%7Cprojects%7C489
  2015. def test_span_op_casing(self):
  2016. super().test_span_op_casing()
  2017. def test_tag_wildcards(self):
  2018. self.store_spans(
  2019. [
  2020. self.create_span(
  2021. {"description": "foo", "tags": {"foo": "bar"}},
  2022. start_ts=self.ten_mins_ago,
  2023. ),
  2024. self.create_span(
  2025. {"description": "qux", "tags": {"foo": "qux"}},
  2026. start_ts=self.ten_mins_ago,
  2027. ),
  2028. ],
  2029. is_eap=self.is_eap,
  2030. )
  2031. for query in [
  2032. "foo:b*",
  2033. "foo:*r",
  2034. "foo:*a*",
  2035. "foo:b*r",
  2036. ]:
  2037. response = self.do_request(
  2038. {
  2039. "field": ["foo", "count()"],
  2040. "query": query,
  2041. "project": self.project.id,
  2042. "dataset": self.dataset,
  2043. }
  2044. )
  2045. assert response.status_code == 200, response.content
  2046. assert response.data["data"] == [{"foo": "bar", "count()": 1}]
  2047. @pytest.mark.xfail(reason="wip: rate not implemented yet")
  2048. def test_spm(self):
  2049. super().test_spm()
  2050. def test_is_transaction(self):
  2051. self.store_spans(
  2052. [
  2053. self.create_span(
  2054. {
  2055. "description": "foo",
  2056. "sentry_tags": {"status": "success"},
  2057. "is_segment": True,
  2058. },
  2059. start_ts=self.ten_mins_ago,
  2060. ),
  2061. self.create_span(
  2062. {
  2063. "description": "bar",
  2064. "sentry_tags": {"status": "success"},
  2065. "is_segment": False,
  2066. },
  2067. start_ts=self.ten_mins_ago,
  2068. ),
  2069. ],
  2070. is_eap=self.is_eap,
  2071. )
  2072. response = self.do_request(
  2073. {
  2074. "field": ["span.status", "description", "count()", "is_transaction"],
  2075. "query": "is_transaction:true",
  2076. "orderby": "description",
  2077. "project": self.project.id,
  2078. "dataset": self.dataset,
  2079. }
  2080. )
  2081. assert response.status_code == 200, response.content
  2082. data = response.data["data"]
  2083. meta = response.data["meta"]
  2084. assert len(data) == 1
  2085. assert data == [
  2086. {
  2087. "is_transaction": True,
  2088. "span.status": "success",
  2089. "description": "foo",
  2090. "count()": 1,
  2091. },
  2092. ]
  2093. assert meta["dataset"] == self.dataset
  2094. def test_is_not_transaction(self):
  2095. self.store_spans(
  2096. [
  2097. self.create_span(
  2098. {
  2099. "description": "foo",
  2100. "sentry_tags": {"status": "success"},
  2101. "is_segment": True,
  2102. },
  2103. start_ts=self.ten_mins_ago,
  2104. ),
  2105. self.create_span(
  2106. {
  2107. "description": "bar",
  2108. "sentry_tags": {"status": "success"},
  2109. "is_segment": False,
  2110. },
  2111. start_ts=self.ten_mins_ago,
  2112. ),
  2113. ],
  2114. is_eap=self.is_eap,
  2115. )
  2116. response = self.do_request(
  2117. {
  2118. "field": ["span.status", "description", "count()", "is_transaction"],
  2119. "query": "is_transaction:0",
  2120. "orderby": "description",
  2121. "project": self.project.id,
  2122. "dataset": self.dataset,
  2123. }
  2124. )
  2125. assert response.status_code == 200, response.content
  2126. data = response.data["data"]
  2127. meta = response.data["meta"]
  2128. assert len(data) == 1
  2129. assert data == [
  2130. {
  2131. "is_transaction": False,
  2132. "span.status": "success",
  2133. "description": "bar",
  2134. "count()": 1,
  2135. },
  2136. ]
  2137. assert meta["dataset"] == self.dataset
  2138. def test_byte_fields(self):
  2139. self.store_spans(
  2140. [
  2141. self.create_span(
  2142. {
  2143. "description": "foo",
  2144. "data": {
  2145. "cache.item_size": 1,
  2146. "messaging.message.body.size": 2,
  2147. },
  2148. },
  2149. start_ts=self.ten_mins_ago,
  2150. ),
  2151. ],
  2152. is_eap=self.is_eap,
  2153. )
  2154. response = self.do_request(
  2155. {
  2156. "field": [
  2157. "cache.item_size",
  2158. "measurements.cache.item_size",
  2159. "messaging.message.body.size",
  2160. "measurements.messaging.message.body.size",
  2161. ],
  2162. "project": self.project.id,
  2163. "dataset": self.dataset,
  2164. }
  2165. )
  2166. assert response.data["data"] == [
  2167. {
  2168. "id": mock.ANY,
  2169. "project.name": self.project.slug,
  2170. "cache.item_size": 1.0,
  2171. "measurements.cache.item_size": 1.0,
  2172. "measurements.messaging.message.body.size": 2.0,
  2173. "messaging.message.body.size": 2.0,
  2174. },
  2175. ]
  2176. assert response.data["meta"]["fields"] == {
  2177. "id": "string",
  2178. "project.name": "string",
  2179. "cache.item_size": "size",
  2180. "measurements.cache.item_size": "size",
  2181. "measurements.messaging.message.body.size": "size",
  2182. "messaging.message.body.size": "size",
  2183. }
  2184. assert response.data["meta"]["units"] == {
  2185. "id": None,
  2186. "project.name": None,
  2187. "cache.item_size": "byte",
  2188. "measurements.cache.item_size": "byte",
  2189. "measurements.messaging.message.body.size": "byte",
  2190. "messaging.message.body.size": "byte",
  2191. }
  2192. def test_simple_measurements(self):
  2193. keys = [
  2194. ("app_start_cold", "duration", "millisecond"),
  2195. ("app_start_warm", "duration", "millisecond"),
  2196. ("frames_frozen", "number", None), # should be integer but keeping it consistent
  2197. ("frames_frozen_rate", "percentage", None),
  2198. ("frames_slow", "number", None), # should be integer but keeping it consistent
  2199. ("frames_slow_rate", "percentage", None),
  2200. ("frames_total", "number", None), # should be integer but keeping it consistent
  2201. ("time_to_initial_display", "duration", "millisecond"),
  2202. ("time_to_full_display", "duration", "millisecond"),
  2203. ("stall_count", "number", None), # should be integer but keeping it consistent
  2204. ("stall_percentage", "percentage", None),
  2205. ("stall_stall_longest_time", "number", None),
  2206. ("stall_stall_total_time", "number", None),
  2207. ("cls", "number", None),
  2208. ("fcp", "duration", "millisecond"),
  2209. ("fid", "duration", "millisecond"),
  2210. ("fp", "duration", "millisecond"),
  2211. ("inp", "duration", "millisecond"),
  2212. ("lcp", "duration", "millisecond"),
  2213. ("ttfb", "duration", "millisecond"),
  2214. ("ttfb.requesttime", "duration", "millisecond"),
  2215. ("score.cls", "number", None),
  2216. ("score.fcp", "number", None),
  2217. ("score.fid", "number", None),
  2218. ("score.inp", "number", None),
  2219. ("score.lcp", "number", None),
  2220. ("score.ttfb", "number", None),
  2221. ("score.total", "number", None),
  2222. ("score.weight.cls", "number", None),
  2223. ("score.weight.fcp", "number", None),
  2224. ("score.weight.fid", "number", None),
  2225. ("score.weight.inp", "number", None),
  2226. ("score.weight.lcp", "number", None),
  2227. ("score.weight.ttfb", "number", None),
  2228. ("cache.item_size", "size", "byte"),
  2229. ("messaging.message.body.size", "size", "byte"),
  2230. ("messaging.message.receive.latency", "duration", "millisecond"),
  2231. ("messaging.message.retry.count", "number", None),
  2232. ]
  2233. self._test_simple_measurements(keys)
  2234. def test_explore_sample_query(self):
  2235. spans = [
  2236. self.create_span(
  2237. {"description": "foo", "sentry_tags": {"status": "success"}},
  2238. start_ts=self.ten_mins_ago,
  2239. ),
  2240. self.create_span(
  2241. {"description": "bar", "sentry_tags": {"status": "invalid_argument"}},
  2242. start_ts=self.nine_mins_ago,
  2243. ),
  2244. ]
  2245. self.store_spans(
  2246. spans,
  2247. is_eap=self.is_eap,
  2248. )
  2249. response = self.do_request(
  2250. {
  2251. "field": [
  2252. "id",
  2253. "project",
  2254. "span.op",
  2255. "span.description",
  2256. "span.duration",
  2257. "timestamp",
  2258. "trace",
  2259. "transaction.span_id",
  2260. ],
  2261. # This is to skip INP spans
  2262. "query": "!transaction.span_id:00",
  2263. "orderby": "timestamp",
  2264. "statsPeriod": "1h",
  2265. "project": self.project.id,
  2266. "dataset": self.dataset,
  2267. }
  2268. )
  2269. assert response.status_code == 200, response.content
  2270. data = response.data["data"]
  2271. meta = response.data["meta"]
  2272. assert len(data) == 2
  2273. for source, result in zip(spans, data):
  2274. assert result["id"] == source["span_id"], "id"
  2275. assert result["span.duration"] == 1000.0, "duration"
  2276. # TODO: once the snuba change to return Nones has merged remove the or
  2277. assert result["span.op"] is None or result["span.op"] == "", "op"
  2278. assert result["span.description"] == source["description"], "description"
  2279. ts = datetime.fromisoformat(result["timestamp"])
  2280. assert ts.tzinfo == timezone.utc
  2281. assert ts.timestamp() == pytest.approx(
  2282. source["end_timestamp_precise"], abs=5
  2283. ), "timestamp"
  2284. assert result["transaction.span_id"] == source["segment_id"], "transaction.span_id"
  2285. assert result["project"] == result["project.name"] == self.project.slug, "project"
  2286. assert meta["dataset"] == self.dataset
  2287. def test_query_for_missing_tag(self):
  2288. self.store_spans(
  2289. [
  2290. self.create_span(
  2291. {"description": "foo"},
  2292. start_ts=self.ten_mins_ago,
  2293. ),
  2294. self.create_span(
  2295. {"description": "qux", "tags": {"foo": "bar"}},
  2296. start_ts=self.ten_mins_ago,
  2297. ),
  2298. ],
  2299. is_eap=self.is_eap,
  2300. )
  2301. response = self.do_request(
  2302. {
  2303. "field": ["foo", "count()"],
  2304. "query": "has:foo",
  2305. "project": self.project.id,
  2306. "dataset": self.dataset,
  2307. }
  2308. )
  2309. assert response.status_code == 200, response.content
  2310. assert response.data["data"] == [{"foo": "bar", "count()": 1}]
  2311. def test_query_for_missing_tag_negated(self):
  2312. self.store_spans(
  2313. [
  2314. self.create_span(
  2315. {"description": "foo"},
  2316. start_ts=self.ten_mins_ago,
  2317. ),
  2318. self.create_span(
  2319. {"description": "qux", "tags": {"foo": "bar"}},
  2320. start_ts=self.ten_mins_ago,
  2321. ),
  2322. ],
  2323. is_eap=self.is_eap,
  2324. )
  2325. response = self.do_request(
  2326. {
  2327. "field": ["foo", "count()"],
  2328. "query": "!has:foo",
  2329. "project": self.project.id,
  2330. "dataset": self.dataset,
  2331. }
  2332. )
  2333. assert response.status_code == 200, response.content
  2334. data = response.data["data"]
  2335. assert len(data) == 1
  2336. assert data[0]["foo"] == "" or data[0]["foo"] is None
  2337. assert data[0]["count()"] == 1
  2338. def test_device_class_filter_unknown(self):
  2339. self.store_spans(
  2340. [
  2341. self.create_span({"sentry_tags": {"device.class": ""}}, start_ts=self.ten_mins_ago),
  2342. ],
  2343. is_eap=self.is_eap,
  2344. )
  2345. response = self.do_request(
  2346. {
  2347. "field": ["device.class", "count()"],
  2348. "query": "device.class:Unknown",
  2349. "orderby": "count()",
  2350. "project": self.project.id,
  2351. "dataset": self.dataset,
  2352. }
  2353. )
  2354. assert response.status_code == 200, response.content
  2355. data = response.data["data"]
  2356. meta = response.data["meta"]
  2357. assert len(data) == 1
  2358. assert data[0]["device.class"] == "Unknown"
  2359. assert meta["dataset"] == self.dataset
  2360. @pytest.mark.skip(reason="replay id alias not migrated over")
  2361. def test_replay_id(self):
  2362. super().test_replay_id()
  2363. @pytest.mark.skip(reason="user display alias not migrated over")
  2364. def test_user_display(self):
  2365. super().test_user_display()