test_organization_events_facets.py 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952
  1. from datetime import timedelta
  2. from unittest import mock
  3. from uuid import uuid4
  4. import requests
  5. from django.urls import reverse
  6. from django.utils import timezone
  7. from rest_framework.exceptions import ParseError
  8. from sentry.testutils.cases import APITestCase, SnubaTestCase
  9. from sentry.testutils.helpers.datetime import before_now, iso_format
  10. class OrganizationEventsFacetsEndpointTest(SnubaTestCase, APITestCase):
  11. def setUp(self):
  12. super().setUp()
  13. self.min_ago = before_now(minutes=1).replace(microsecond=0)
  14. self.day_ago = before_now(days=1).replace(microsecond=0)
  15. self.login_as(user=self.user)
  16. self.project = self.create_project()
  17. self.project2 = self.create_project()
  18. self.url = reverse(
  19. "sentry-api-0-organization-events-facets",
  20. kwargs={"organization_id_or_slug": self.project.organization.slug},
  21. )
  22. self.min_ago_iso = iso_format(self.min_ago)
  23. self.features = {"organizations:discover-basic": True, "organizations:global-views": True}
  24. def assert_facet(self, response, key, expected):
  25. actual = None
  26. for facet in response.data:
  27. if facet["key"] == key:
  28. actual = facet
  29. break
  30. assert actual is not None, f"Could not find {key} facet in {response.data}"
  31. assert "topValues" in actual
  32. key = lambda row: row["name"] if row["name"] is not None else ""
  33. assert sorted(expected, key=key) == sorted(actual["topValues"], key=key)
  34. def test_performance_view_feature(self):
  35. self.features.update(
  36. {
  37. "organizations:discover-basic": False,
  38. "organizations:performance-view": True,
  39. }
  40. )
  41. with self.feature(self.features):
  42. response = self.client.get(self.url, data={"project": self.project.id}, format="json")
  43. assert response.status_code == 200, response.content
  44. def test_simple(self):
  45. self.store_event(
  46. data={
  47. "event_id": uuid4().hex,
  48. "timestamp": self.min_ago_iso,
  49. "tags": {"number": "one"},
  50. },
  51. project_id=self.project2.id,
  52. )
  53. self.store_event(
  54. data={
  55. "event_id": uuid4().hex,
  56. "timestamp": self.min_ago_iso,
  57. "tags": {"number": "one"},
  58. },
  59. project_id=self.project.id,
  60. )
  61. self.store_event(
  62. data={
  63. "event_id": uuid4().hex,
  64. "timestamp": self.min_ago_iso,
  65. "tags": {"number": "two"},
  66. },
  67. project_id=self.project.id,
  68. )
  69. with self.feature(self.features):
  70. response = self.client.get(self.url, format="json")
  71. assert response.status_code == 200, response.content
  72. expected = [
  73. {"count": 2, "name": "one", "value": "one"},
  74. {"count": 1, "name": "two", "value": "two"},
  75. ]
  76. self.assert_facet(response, "number", expected)
  77. def test_order_by(self):
  78. self.store_event(
  79. data={
  80. "event_id": uuid4().hex,
  81. "timestamp": self.min_ago_iso,
  82. "tags": {"alpha": "one"},
  83. "environment": "aaaa",
  84. },
  85. project_id=self.project2.id,
  86. )
  87. self.store_event(
  88. data={
  89. "event_id": uuid4().hex,
  90. "timestamp": self.min_ago_iso,
  91. "tags": {"beta": "one"},
  92. "environment": "bbbb",
  93. },
  94. project_id=self.project.id,
  95. )
  96. self.store_event(
  97. data={
  98. "event_id": uuid4().hex,
  99. "timestamp": self.min_ago_iso,
  100. "tags": {"charlie": "two"},
  101. "environment": "cccc",
  102. },
  103. project_id=self.project.id,
  104. )
  105. with self.feature(self.features):
  106. response = self.client.get(self.url, format="json")
  107. assert response.status_code == 200, response.content
  108. keys = [facet["key"] for facet in response.data]
  109. assert ["alpha", "beta", "charlie", "environment", "level", "project"] == keys
  110. def test_with_message_query(self):
  111. self.store_event(
  112. data={
  113. "event_id": uuid4().hex,
  114. "timestamp": self.min_ago_iso,
  115. "message": "how to make fast",
  116. "tags": {"color": "green"},
  117. },
  118. project_id=self.project.id,
  119. )
  120. self.store_event(
  121. data={
  122. "event_id": uuid4().hex,
  123. "timestamp": self.min_ago_iso,
  124. "message": "Delet the Data",
  125. "tags": {"color": "red"},
  126. },
  127. project_id=self.project.id,
  128. )
  129. self.store_event(
  130. data={
  131. "event_id": uuid4().hex,
  132. "timestamp": self.min_ago_iso,
  133. "message": "Data the Delet ",
  134. "tags": {"color": "yellow"},
  135. },
  136. project_id=self.project2.id,
  137. )
  138. with self.feature(self.features):
  139. response = self.client.get(self.url, {"query": "delet"}, format="json")
  140. assert response.status_code == 200, response.content
  141. expected = [
  142. {"count": 1, "name": "yellow", "value": "yellow"},
  143. {"count": 1, "name": "red", "value": "red"},
  144. ]
  145. self.assert_facet(response, "color", expected)
  146. def test_with_condition(self):
  147. self.store_event(
  148. data={
  149. "event_id": uuid4().hex,
  150. "timestamp": self.min_ago_iso,
  151. "message": "how to make fast",
  152. "tags": {"color": "green"},
  153. },
  154. project_id=self.project.id,
  155. )
  156. self.store_event(
  157. data={
  158. "event_id": uuid4().hex,
  159. "timestamp": self.min_ago_iso,
  160. "message": "Delet the Data",
  161. "tags": {"color": "red"},
  162. },
  163. project_id=self.project.id,
  164. )
  165. self.store_event(
  166. data={
  167. "event_id": uuid4().hex,
  168. "timestamp": self.min_ago_iso,
  169. "message": "Data the Delet ",
  170. "tags": {"color": "yellow"},
  171. },
  172. project_id=self.project2.id,
  173. )
  174. with self.feature(self.features):
  175. response = self.client.get(self.url, {"query": "color:yellow"}, format="json")
  176. assert response.status_code == 200, response.content
  177. expected = [{"count": 1, "name": "yellow", "value": "yellow"}]
  178. self.assert_facet(response, "color", expected)
  179. def test_with_conditional_filter(self):
  180. self.store_event(
  181. data={
  182. "event_id": uuid4().hex,
  183. "timestamp": self.min_ago_iso,
  184. "message": "how to make fast",
  185. "tags": {"color": "green"},
  186. },
  187. project_id=self.project.id,
  188. )
  189. self.store_event(
  190. data={
  191. "event_id": uuid4().hex,
  192. "timestamp": self.min_ago_iso,
  193. "message": "Delet the Data",
  194. "tags": {"color": "red"},
  195. },
  196. project_id=self.project.id,
  197. )
  198. self.store_event(
  199. data={
  200. "event_id": uuid4().hex,
  201. "timestamp": self.min_ago_iso,
  202. "message": "Data the Delet ",
  203. "tags": {"color": "yellow"},
  204. },
  205. project_id=self.project2.id,
  206. )
  207. with self.feature(self.features):
  208. response = self.client.get(
  209. self.url, {"query": "color:yellow OR color:red"}, format="json"
  210. )
  211. assert response.status_code == 200, response.content
  212. expected = [
  213. {"count": 1, "name": "yellow", "value": "yellow"},
  214. {"count": 1, "name": "red", "value": "red"},
  215. ]
  216. self.assert_facet(response, "color", expected)
  217. def test_start_end(self):
  218. two_days_ago = self.day_ago - timedelta(days=1)
  219. hour_ago = self.min_ago - timedelta(hours=1)
  220. two_hours_ago = hour_ago - timedelta(hours=1)
  221. self.store_event(
  222. data={
  223. "event_id": uuid4().hex,
  224. "timestamp": iso_format(two_days_ago),
  225. "tags": {"color": "red"},
  226. },
  227. project_id=self.project.id,
  228. )
  229. self.store_event(
  230. data={
  231. "event_id": uuid4().hex,
  232. "timestamp": iso_format(hour_ago),
  233. "tags": {"color": "red"},
  234. },
  235. project_id=self.project.id,
  236. )
  237. self.store_event(
  238. data={
  239. "event_id": uuid4().hex,
  240. "timestamp": iso_format(two_hours_ago),
  241. "tags": {"color": "red"},
  242. },
  243. project_id=self.project.id,
  244. )
  245. self.store_event(
  246. data={
  247. "event_id": uuid4().hex,
  248. "timestamp": iso_format(timezone.now()),
  249. "tags": {"color": "red"},
  250. },
  251. project_id=self.project2.id,
  252. )
  253. with self.feature(self.features):
  254. response = self.client.get(
  255. self.url,
  256. {"start": iso_format(self.day_ago), "end": iso_format(self.min_ago)},
  257. format="json",
  258. )
  259. assert response.status_code == 200, response.content
  260. expected = [{"count": 2, "name": "red", "value": "red"}]
  261. self.assert_facet(response, "color", expected)
  262. def test_excluded_tag(self):
  263. self.user = self.create_user()
  264. self.user2 = self.create_user()
  265. self.store_event(
  266. data={
  267. "event_id": uuid4().hex,
  268. "timestamp": iso_format(self.day_ago),
  269. "message": "very bad",
  270. "tags": {"sentry:user": self.user.email},
  271. },
  272. project_id=self.project.id,
  273. )
  274. self.store_event(
  275. data={
  276. "event_id": uuid4().hex,
  277. "timestamp": iso_format(self.day_ago),
  278. "message": "very bad",
  279. "tags": {"sentry:user": self.user2.email},
  280. },
  281. project_id=self.project.id,
  282. )
  283. self.store_event(
  284. data={
  285. "event_id": uuid4().hex,
  286. "timestamp": iso_format(self.day_ago),
  287. "message": "very bad",
  288. "tags": {"sentry:user": self.user2.email},
  289. },
  290. project_id=self.project.id,
  291. )
  292. with self.feature(self.features):
  293. response = self.client.get(self.url, format="json", data={"project": [self.project.id]})
  294. assert response.status_code == 200, response.content
  295. expected = [
  296. {"count": 2, "name": self.user2.email, "value": self.user2.email},
  297. {"count": 1, "name": self.user.email, "value": self.user.email},
  298. ]
  299. self.assert_facet(response, "user", expected)
  300. def test_no_projects(self):
  301. org = self.create_organization(owner=self.user)
  302. url = reverse(
  303. "sentry-api-0-organization-events-facets", kwargs={"organization_id_or_slug": org.slug}
  304. )
  305. with self.feature("organizations:discover-basic"):
  306. response = self.client.get(url, format="json")
  307. assert response.status_code == 200, response.content
  308. assert response.data == []
  309. def test_multiple_projects_without_global_view(self):
  310. self.store_event(data={"event_id": uuid4().hex}, project_id=self.project.id)
  311. self.store_event(data={"event_id": uuid4().hex}, project_id=self.project2.id)
  312. with self.feature("organizations:discover-basic"):
  313. response = self.client.get(self.url, format="json")
  314. assert response.status_code == 400, response.content
  315. assert response.data == {"detail": "You cannot view events from multiple projects."}
  316. def test_project_selected(self):
  317. self.store_event(
  318. data={
  319. "event_id": uuid4().hex,
  320. "timestamp": self.min_ago_iso,
  321. "tags": {"number": "two"},
  322. },
  323. project_id=self.project.id,
  324. )
  325. self.store_event(
  326. data={
  327. "event_id": uuid4().hex,
  328. "timestamp": self.min_ago_iso,
  329. "tags": {"number": "one"},
  330. },
  331. project_id=self.project2.id,
  332. )
  333. with self.feature(self.features):
  334. response = self.client.get(self.url, {"project": [self.project.id]}, format="json")
  335. assert response.status_code == 200, response.content
  336. expected = [{"name": "two", "value": "two", "count": 1}]
  337. self.assert_facet(response, "number", expected)
  338. def test_project_filtered(self):
  339. self.store_event(
  340. data={
  341. "event_id": uuid4().hex,
  342. "timestamp": self.min_ago_iso,
  343. "tags": {"number": "two"},
  344. },
  345. project_id=self.project.id,
  346. )
  347. self.store_event(
  348. data={
  349. "event_id": uuid4().hex,
  350. "timestamp": self.min_ago_iso,
  351. "tags": {"number": "one"},
  352. },
  353. project_id=self.project2.id,
  354. )
  355. with self.feature(self.features):
  356. response = self.client.get(
  357. self.url, {"query": f"project:{self.project.slug}"}, format="json"
  358. )
  359. assert response.status_code == 200, response.content
  360. expected = [{"name": "two", "value": "two", "count": 1}]
  361. self.assert_facet(response, "number", expected)
  362. def test_project_key(self):
  363. self.store_event(
  364. data={
  365. "event_id": uuid4().hex,
  366. "timestamp": self.min_ago_iso,
  367. "tags": {"color": "green"},
  368. },
  369. project_id=self.project.id,
  370. )
  371. self.store_event(
  372. data={
  373. "event_id": uuid4().hex,
  374. "timestamp": self.min_ago_iso,
  375. "tags": {"number": "one"},
  376. },
  377. project_id=self.project2.id,
  378. )
  379. self.store_event(
  380. data={
  381. "event_id": uuid4().hex,
  382. "timestamp": self.min_ago_iso,
  383. "tags": {"color": "green"},
  384. },
  385. project_id=self.project.id,
  386. )
  387. self.store_event(
  388. data={"event_id": uuid4().hex, "timestamp": self.min_ago_iso, "tags": {"color": "red"}},
  389. project_id=self.project.id,
  390. )
  391. with self.feature(self.features):
  392. response = self.client.get(self.url, format="json")
  393. assert response.status_code == 200, response.content
  394. expected = [
  395. {"count": 3, "name": self.project.slug, "value": self.project.id},
  396. {"count": 1, "name": self.project2.slug, "value": self.project2.id},
  397. ]
  398. self.assert_facet(response, "project", expected)
  399. def test_project_key_with_project_tag(self):
  400. self.organization.flags.allow_joinleave = False
  401. self.organization.save()
  402. member_user = self.create_user()
  403. team = self.create_team(members=[member_user])
  404. private_project1 = self.create_project(organization=self.organization, teams=[team])
  405. private_project2 = self.create_project(organization=self.organization, teams=[team])
  406. self.login_as(member_user)
  407. self.store_event(
  408. data={
  409. "event_id": uuid4().hex,
  410. "timestamp": self.min_ago_iso,
  411. "tags": {"color": "green", "project": "%d" % private_project1.id},
  412. },
  413. project_id=self.project.id,
  414. )
  415. self.store_event(
  416. data={
  417. "event_id": uuid4().hex,
  418. "timestamp": self.min_ago_iso,
  419. "tags": {"number": "one", "project": "%d" % private_project1.id},
  420. },
  421. project_id=private_project1.id,
  422. )
  423. self.store_event(
  424. data={
  425. "event_id": uuid4().hex,
  426. "timestamp": self.min_ago_iso,
  427. "tags": {"color": "green"},
  428. },
  429. project_id=private_project1.id,
  430. )
  431. self.store_event(
  432. data={"event_id": uuid4().hex, "timestamp": self.min_ago_iso, "tags": {"color": "red"}},
  433. project_id=private_project2.id,
  434. )
  435. self.store_event(
  436. data={"event_id": uuid4().hex, "timestamp": self.min_ago_iso, "tags": {"color": "red"}},
  437. project_id=private_project2.id,
  438. )
  439. with self.feature(self.features):
  440. response = self.client.get(self.url, format="json")
  441. assert response.status_code == 200, response.content
  442. expected = [
  443. {"count": 2, "name": private_project1.slug, "value": private_project1.id},
  444. {"count": 2, "name": private_project2.slug, "value": private_project2.id},
  445. ]
  446. self.assert_facet(response, "project", expected)
  447. def test_malformed_query(self):
  448. self.store_event(data={"event_id": uuid4().hex}, project_id=self.project.id)
  449. self.store_event(data={"event_id": uuid4().hex}, project_id=self.project2.id)
  450. with self.feature(self.features):
  451. response = self.client.get(self.url, format="json", data={"query": "\n\n\n\n"})
  452. assert response.status_code == 400, response.content
  453. assert response.data["detail"].endswith(
  454. "(column 1). This is commonly caused by unmatched parentheses. Enclose any text in double quotes."
  455. )
  456. @mock.patch("sentry.search.events.builder.discover.raw_snql_query")
  457. def test_handling_snuba_errors(self, mock_query):
  458. mock_query.side_effect = ParseError("test")
  459. with self.feature(self.features):
  460. response = self.client.get(self.url, format="json")
  461. assert response.status_code == 400, response.content
  462. def test_environment(self):
  463. self.store_event(
  464. data={
  465. "event_id": uuid4().hex,
  466. "timestamp": self.min_ago_iso,
  467. "tags": {"number": "one"},
  468. "environment": "staging",
  469. },
  470. project_id=self.project2.id,
  471. )
  472. self.store_event(
  473. data={
  474. "event_id": uuid4().hex,
  475. "timestamp": self.min_ago_iso,
  476. "tags": {"number": "one"},
  477. "environment": "production",
  478. },
  479. project_id=self.project.id,
  480. )
  481. self.store_event(
  482. data={
  483. "event_id": uuid4().hex,
  484. "timestamp": self.min_ago_iso,
  485. "tags": {"number": "two"},
  486. },
  487. project_id=self.project.id,
  488. )
  489. with self.feature(self.features):
  490. response = self.client.get(self.url, format="json")
  491. assert response.status_code == 200, response.content
  492. expected = [
  493. {"count": 1, "name": "production", "value": "production"},
  494. {"count": 1, "name": "staging", "value": "staging"},
  495. {"count": 1, "name": None, "value": None},
  496. ]
  497. self.assert_facet(response, "environment", expected)
  498. with self.feature(self.features):
  499. # query by an environment
  500. response = self.client.get(self.url, {"environment": "staging"}, format="json")
  501. assert response.status_code == 200, response.content
  502. expected = [{"count": 1, "name": "staging", "value": "staging"}]
  503. self.assert_facet(response, "environment", expected)
  504. with self.feature(self.features):
  505. # query by multiple environments
  506. response = self.client.get(
  507. self.url, {"environment": ["staging", "production"]}, format="json"
  508. )
  509. assert response.status_code == 200, response.content
  510. expected = [
  511. {"count": 1, "name": "production", "value": "production"},
  512. {"count": 1, "name": "staging", "value": "staging"},
  513. ]
  514. self.assert_facet(response, "environment", expected)
  515. with self.feature(self.features):
  516. # query by multiple environments, including the "no environment" environment
  517. response = self.client.get(
  518. self.url, {"environment": ["staging", "production", ""]}, format="json"
  519. )
  520. assert response.status_code == 200, response.content
  521. expected = [
  522. {"count": 1, "name": "production", "value": "production"},
  523. {"count": 1, "name": "staging", "value": "staging"},
  524. {"count": 1, "name": None, "value": None},
  525. ]
  526. self.assert_facet(response, "environment", expected)
  527. def test_out_of_retention(self):
  528. with self.options({"system.event-retention-days": 10}):
  529. with self.feature(self.features):
  530. response = self.client.get(
  531. self.url,
  532. format="json",
  533. data={
  534. "start": iso_format(before_now(days=20)),
  535. "end": iso_format(before_now(days=15)),
  536. },
  537. )
  538. assert response.status_code == 400
  539. @mock.patch("sentry.utils.snuba.quantize_time")
  540. def test_quantize_dates(self, mock_quantize):
  541. mock_quantize.return_value = before_now(days=1)
  542. with self.feature("organizations:discover-basic"):
  543. # Don't quantize short time periods
  544. self.client.get(
  545. self.url,
  546. format="json",
  547. data={"statsPeriod": "1h", "query": "", "field": ["id", "timestamp"]},
  548. )
  549. # Don't quantize absolute date periods
  550. self.client.get(
  551. self.url,
  552. format="json",
  553. data={
  554. "start": iso_format(before_now(days=20)),
  555. "end": iso_format(before_now(days=15)),
  556. "query": "",
  557. "field": ["id", "timestamp"],
  558. },
  559. )
  560. assert len(mock_quantize.mock_calls) == 0
  561. # Quantize long date periods
  562. self.client.get(
  563. self.url,
  564. format="json",
  565. data={"field": ["id", "timestamp"], "statsPeriod": "90d", "query": ""},
  566. )
  567. assert len(mock_quantize.mock_calls) == 2
  568. def test_device_class(self):
  569. self.store_event(
  570. data={
  571. "event_id": uuid4().hex,
  572. "timestamp": self.min_ago_iso,
  573. "tags": {"device.class": "1"},
  574. },
  575. project_id=self.project2.id,
  576. )
  577. self.store_event(
  578. data={
  579. "event_id": uuid4().hex,
  580. "timestamp": self.min_ago_iso,
  581. "tags": {"device.class": "2"},
  582. },
  583. project_id=self.project.id,
  584. )
  585. self.store_event(
  586. data={
  587. "event_id": uuid4().hex,
  588. "timestamp": self.min_ago_iso,
  589. "tags": {"device.class": "3"},
  590. },
  591. project_id=self.project.id,
  592. )
  593. with self.feature(self.features):
  594. response = self.client.get(self.url, format="json")
  595. assert response.status_code == 200, response.content
  596. expected = [
  597. {"count": 1, "name": "high", "value": "high"},
  598. {"count": 1, "name": "medium", "value": "medium"},
  599. {"count": 1, "name": "low", "value": "low"},
  600. ]
  601. self.assert_facet(response, "device.class", expected)
  602. def test_with_cursor_parameter(self):
  603. test_project = self.create_project()
  604. test_tags = {
  605. "a": "one",
  606. "b": "two",
  607. "c": "three",
  608. "d": "four",
  609. "e": "five",
  610. "f": "six",
  611. "g": "seven",
  612. "h": "eight",
  613. "i": "nine",
  614. "j": "ten",
  615. "k": "eleven",
  616. }
  617. self.store_event(
  618. data={"event_id": uuid4().hex, "timestamp": self.min_ago_iso, "tags": test_tags},
  619. project_id=test_project.id,
  620. )
  621. # Test the default query fetches the first 10 results
  622. with self.feature(self.features):
  623. response = self.client.get(self.url, format="json", data={"project": test_project.id})
  624. links = requests.utils.parse_header_links(
  625. response.get("link", "").rstrip(">").replace(">,<", ",<")
  626. )
  627. assert response.status_code == 200, response.content
  628. assert links[1]["results"] == "true" # There are more results to be fetched
  629. assert links[1]["cursor"] == "0:10:0"
  630. assert len(response.data) == 10
  631. # Loop over the first 10 tags to ensure they're in the results
  632. for tag_key in list(test_tags.keys())[:10]:
  633. expected = [
  634. {"count": 1, "name": test_tags[tag_key], "value": test_tags[tag_key]},
  635. ]
  636. self.assert_facet(response, tag_key, expected)
  637. # Get the next page
  638. with self.feature(self.features):
  639. response = self.client.get(
  640. self.url, format="json", data={"project": test_project.id, "cursor": "0:10:0"}
  641. )
  642. links = requests.utils.parse_header_links(
  643. response.get("link", "").rstrip(">").replace(">,<", ",<")
  644. )
  645. assert response.status_code == 200, response.content
  646. assert links[1]["results"] == "false" # There should be no more tags to fetch
  647. assert len(response.data) == 2
  648. expected = [
  649. {"count": 1, "name": "eleven", "value": "eleven"},
  650. ]
  651. self.assert_facet(response, "k", expected)
  652. expected = [
  653. {"count": 1, "name": "error", "value": "error"},
  654. ]
  655. self.assert_facet(response, "level", expected)
  656. def test_projects_data_are_injected_on_first_page_with_multiple_projects_selected(self):
  657. test_project = self.create_project()
  658. test_project2 = self.create_project()
  659. test_tags = {
  660. "a": "one",
  661. "b": "two",
  662. "c": "three",
  663. "d": "four",
  664. "e": "five",
  665. "f": "six",
  666. "g": "seven",
  667. "h": "eight",
  668. "i": "nine",
  669. "j": "ten",
  670. "k": "eleven",
  671. }
  672. self.store_event(
  673. data={"event_id": uuid4().hex, "timestamp": self.min_ago_iso, "tags": test_tags},
  674. project_id=test_project.id,
  675. )
  676. # Test the default query fetches the first 10 results
  677. with self.feature(self.features):
  678. response = self.client.get(
  679. self.url, format="json", data={"project": [test_project.id, test_project2.id]}
  680. )
  681. links = requests.utils.parse_header_links(
  682. response.get("link", "").rstrip(">").replace(">,<", ",<")
  683. )
  684. assert response.status_code == 200, response.content
  685. assert links[1]["results"] == "true" # There are more results to be fetched
  686. assert links[1]["cursor"] == "0:10:0"
  687. assert len(response.data) == 10
  688. # Project is injected into the first page
  689. expected = [
  690. {"count": 1, "name": test_project.slug, "value": test_project.id},
  691. ]
  692. self.assert_facet(response, "project", expected)
  693. # Loop over the first 9 tags to ensure they're in the results
  694. # in this case, the 10th key is "projects" since it was injected
  695. for tag_key in list(test_tags.keys())[:9]:
  696. expected = [
  697. {"count": 1, "name": test_tags[tag_key], "value": test_tags[tag_key]},
  698. ]
  699. self.assert_facet(response, tag_key, expected)
  700. # Get the next page
  701. with self.feature(self.features):
  702. response = self.client.get(
  703. self.url,
  704. format="json",
  705. data={"project": [test_project.id, test_project2.id], "cursor": "0:10:0"},
  706. )
  707. links = requests.utils.parse_header_links(
  708. response.get("link", "").rstrip(">").replace(">,<", ",<")
  709. )
  710. assert response.status_code == 200, response.content
  711. assert links[1]["results"] == "false" # There should be no more tags to fetch
  712. assert len(response.data) == 3
  713. expected = [
  714. {"count": 1, "name": "ten", "value": "ten"},
  715. ]
  716. self.assert_facet(response, "j", expected)
  717. expected = [
  718. {"count": 1, "name": "eleven", "value": "eleven"},
  719. ]
  720. self.assert_facet(response, "k", expected)
  721. expected = [
  722. {"count": 1, "name": "error", "value": "error"},
  723. ]
  724. self.assert_facet(response, "level", expected)
  725. def test_multiple_pages_with_single_project(self):
  726. test_project = self.create_project()
  727. test_tags = {str(i): str(i) for i in range(22)}
  728. self.store_event(
  729. data={"event_id": uuid4().hex, "timestamp": self.min_ago_iso, "tags": test_tags},
  730. project_id=test_project.id,
  731. )
  732. # Test the default query fetches the first 10 results
  733. with self.feature(self.features):
  734. response = self.client.get(self.url, format="json", data={"project": test_project.id})
  735. links = requests.utils.parse_header_links(
  736. response.get("link", "").rstrip(">").replace(">,<", ",<")
  737. )
  738. assert response.status_code == 200, response.content
  739. assert links[1]["results"] == "true" # There are more results to be fetched
  740. assert links[1]["cursor"] == "0:10:0"
  741. assert len(response.data) == 10
  742. # Get the next page
  743. with self.feature(self.features):
  744. response = self.client.get(
  745. self.url,
  746. format="json",
  747. data={"project": test_project.id, "cursor": links[1]["cursor"]},
  748. )
  749. links = requests.utils.parse_header_links(
  750. response.get("link", "").rstrip(">").replace(">,<", ",<")
  751. )
  752. assert response.status_code == 200, response.content
  753. assert links[1]["results"] == "true" # There are more tags to fetch
  754. assert len(response.data) == 10
  755. # Get the next page
  756. with self.feature(self.features):
  757. response = self.client.get(
  758. self.url,
  759. format="json",
  760. data={"project": test_project.id, "cursor": links[1]["cursor"]},
  761. )
  762. links = requests.utils.parse_header_links(
  763. response.get("link", "").rstrip(">").replace(">,<", ",<")
  764. )
  765. assert response.status_code == 200, response.content
  766. assert links[1]["results"] == "false" # There should be no more tags to fetch
  767. assert len(response.data) == 3
  768. def test_multiple_pages_with_multiple_projects(self):
  769. test_project = self.create_project()
  770. test_project2 = self.create_project()
  771. test_tags = {str(i): str(i) for i in range(22)} # At least 3 pages worth of information
  772. self.store_event(
  773. data={"event_id": uuid4().hex, "timestamp": self.min_ago_iso, "tags": test_tags},
  774. project_id=test_project.id,
  775. )
  776. # Test the default query fetches the first 10 results
  777. with self.feature(self.features):
  778. response = self.client.get(
  779. self.url, format="json", data={"project": [test_project.id, test_project2.id]}
  780. )
  781. links = requests.utils.parse_header_links(
  782. response.get("link", "").rstrip(">").replace(">,<", ",<")
  783. )
  784. assert response.status_code == 200, response.content
  785. assert links[1]["results"] == "true" # There are more results to be fetched
  786. assert links[1]["cursor"] == "0:10:0"
  787. assert len(response.data) == 10
  788. # Get the next page
  789. with self.feature(self.features):
  790. response = self.client.get(
  791. self.url,
  792. format="json",
  793. data={"project": [test_project.id, test_project2.id], "cursor": links[1]["cursor"]},
  794. )
  795. links = requests.utils.parse_header_links(
  796. response.get("link", "").rstrip(">").replace(">,<", ",<")
  797. )
  798. assert response.status_code == 200, response.content
  799. assert links[1]["results"] == "true" # There are more tags to fetch
  800. assert len(response.data) == 10
  801. # Get the next page
  802. with self.feature(self.features):
  803. response = self.client.get(
  804. self.url,
  805. format="json",
  806. data={"project": [test_project.id, test_project2.id], "cursor": links[1]["cursor"]},
  807. )
  808. links = requests.utils.parse_header_links(
  809. response.get("link", "").rstrip(">").replace(">,<", ",<")
  810. )
  811. assert response.status_code == 200, response.content
  812. assert links[1]["results"] == "false" # There should be no more tags to fetch
  813. assert len(response.data) == 4 # 4 because projects and levels were added to the base 22
  814. def test_get_all_tags(self):
  815. test_project = self.create_project()
  816. test_tags = {str(i): str(i) for i in range(22)}
  817. self.store_event(
  818. data={"event_id": uuid4().hex, "timestamp": self.min_ago_iso, "tags": test_tags},
  819. project_id=test_project.id,
  820. )
  821. # Test the default query fetches the first 10 results
  822. with self.feature(self.features):
  823. response = self.client.get(
  824. self.url, format="json", data={"project": test_project.id, "includeAll": True}
  825. )
  826. assert response.status_code == 200, response.content
  827. assert len(response.data) == 23
  828. @mock.patch("sentry.search.events.builder.discover.raw_snql_query")
  829. def test_dont_turbo_trace_queries(self, mock_run):
  830. # Need to create more projects so we'll even want to turbo in the first place
  831. for _ in range(3):
  832. self.create_project()
  833. with self.feature(self.features):
  834. self.client.get(self.url, {"query": f"trace:{'a' * 32}"}, format="json")
  835. mock_run.assert_called_once
  836. assert not mock_run.mock_calls[0].args[0].flags.turbo
  837. @mock.patch("sentry.search.events.builder.discover.raw_snql_query")
  838. def test_use_turbo_without_trace(self, mock_run):
  839. # Need to create more projects so we'll even want to turbo in the first place
  840. for _ in range(3):
  841. self.create_project()
  842. with self.feature(self.features):
  843. self.client.get(self.url, format="json")
  844. mock_run.assert_called_once
  845. assert mock_run.mock_calls[0].args[0].flags.turbo