test_organization_events_span_indexed.py 48 KB

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