test_discover_saved_queries.py 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670
  1. from django.urls import reverse
  2. from sentry.discover.models import DiscoverSavedQuery
  3. from sentry.testutils import APITestCase, SnubaTestCase
  4. from sentry.testutils.helpers.datetime import before_now, iso_format
  5. class DiscoverSavedQueryBase(APITestCase, SnubaTestCase):
  6. def setUp(self):
  7. super().setUp()
  8. self.login_as(user=self.user)
  9. self.org = self.create_organization(owner=self.user)
  10. self.projects = [
  11. self.create_project(organization=self.org),
  12. self.create_project(organization=self.org),
  13. ]
  14. self.project_ids = [project.id for project in self.projects]
  15. self.project_ids_without_access = [self.create_project().id]
  16. query = {"fields": ["test"], "conditions": [], "limit": 10}
  17. model = DiscoverSavedQuery.objects.create(
  18. organization=self.org, created_by=self.user, name="Test query", query=query, version=1
  19. )
  20. model.set_projects(self.project_ids)
  21. class DiscoverSavedQueriesTest(DiscoverSavedQueryBase):
  22. feature_name = "organizations:discover"
  23. def setUp(self):
  24. super().setUp()
  25. self.url = reverse("sentry-api-0-discover-saved-queries", args=[self.org.slug])
  26. def test_get(self):
  27. with self.feature(self.feature_name):
  28. response = self.client.get(self.url)
  29. assert response.status_code == 200, response.content
  30. assert len(response.data) == 1
  31. assert response.data[0]["name"] == "Test query"
  32. assert response.data[0]["projects"] == self.project_ids
  33. assert response.data[0]["fields"] == ["test"]
  34. assert response.data[0]["conditions"] == []
  35. assert response.data[0]["limit"] == 10
  36. assert response.data[0]["version"] == 1
  37. assert "createdBy" in response.data[0]
  38. assert response.data[0]["createdBy"]["username"] == self.user.username
  39. assert not response.data[0]["expired"]
  40. def test_get_version_filter(self):
  41. with self.feature(self.feature_name):
  42. response = self.client.get(self.url, format="json", data={"query": "version:1"})
  43. assert response.status_code == 200, response.content
  44. assert len(response.data) == 1
  45. assert response.data[0]["name"] == "Test query"
  46. with self.feature(self.feature_name):
  47. response = self.client.get(self.url, format="json", data={"query": "version:2"})
  48. assert response.status_code == 200, response.content
  49. assert len(response.data) == 0
  50. def test_get_name_filter(self):
  51. with self.feature(self.feature_name):
  52. response = self.client.get(self.url, format="json", data={"query": "Test"})
  53. assert response.status_code == 200, response.content
  54. assert len(response.data) == 1
  55. assert response.data[0]["name"] == "Test query"
  56. with self.feature(self.feature_name):
  57. # Also available as the name: filter.
  58. response = self.client.get(self.url, format="json", data={"query": "name:Test"})
  59. assert response.status_code == 200, response.content
  60. assert len(response.data) == 1
  61. assert response.data[0]["name"] == "Test query"
  62. with self.feature(self.feature_name):
  63. response = self.client.get(self.url, format="json", data={"query": "name:Nope"})
  64. assert response.status_code == 200, response.content
  65. assert len(response.data) == 0
  66. def test_get_all_paginated(self):
  67. for i in range(0, 10):
  68. query = {"fields": ["test"], "conditions": [], "limit": 10}
  69. model = DiscoverSavedQuery.objects.create(
  70. organization=self.org,
  71. created_by=self.user,
  72. name=f"My query {i}",
  73. query=query,
  74. version=1,
  75. )
  76. model.set_projects(self.project_ids)
  77. with self.feature(self.feature_name):
  78. response = self.client.get(self.url, data={"per_page": 1})
  79. assert response.status_code == 200, response.content
  80. assert len(response.data) == 1
  81. with self.feature(self.feature_name):
  82. # The all parameter ignores pagination and returns all values.
  83. response = self.client.get(self.url, data={"per_page": 1, "all": 1})
  84. assert response.status_code == 200, response.content
  85. assert len(response.data) == 11
  86. def test_get_sortby(self):
  87. query = {"fields": ["message"], "query": "", "limit": 10}
  88. model = DiscoverSavedQuery.objects.create(
  89. organization=self.org,
  90. created_by=self.user,
  91. name="My query",
  92. query=query,
  93. version=2,
  94. date_created=before_now(minutes=10),
  95. date_updated=before_now(minutes=10),
  96. )
  97. model.set_projects(self.project_ids)
  98. sort_options = {
  99. "dateCreated": True,
  100. "-dateCreated": False,
  101. "dateUpdated": True,
  102. "-dateUpdated": False,
  103. "name": True,
  104. "-name": False,
  105. }
  106. for sorting, forward_sort in sort_options.items():
  107. with self.feature(self.feature_name):
  108. response = self.client.get(self.url, data={"sortBy": sorting})
  109. assert response.status_code == 200
  110. values = [row[sorting.strip("-")] for row in response.data]
  111. if not forward_sort:
  112. values = list(reversed(values))
  113. assert list(sorted(values)) == values
  114. def test_get_sortby_most_popular(self):
  115. query = {"fields": ["message"], "query": "", "limit": 10}
  116. model = DiscoverSavedQuery.objects.create(
  117. organization=self.org,
  118. created_by=self.user,
  119. name="My query",
  120. query=query,
  121. version=2,
  122. visits=3,
  123. date_created=before_now(minutes=10),
  124. date_updated=before_now(minutes=10),
  125. last_visited=before_now(minutes=5),
  126. )
  127. model.set_projects(self.project_ids)
  128. for forward_sort in [True, False]:
  129. sorting = "mostPopular" if forward_sort else "-mostPopular"
  130. with self.feature(self.feature_name):
  131. response = self.client.get(self.url, data={"sortBy": sorting})
  132. assert response.status_code == 200
  133. values = [row["name"] for row in response.data]
  134. expected = ["My query", "Test query"]
  135. if not forward_sort:
  136. expected = list(reversed(expected))
  137. assert values == expected
  138. def test_get_sortby_recently_viewed(self):
  139. query = {"fields": ["message"], "query": "", "limit": 10}
  140. model = DiscoverSavedQuery.objects.create(
  141. organization=self.org,
  142. created_by=self.user,
  143. name="My query",
  144. query=query,
  145. version=2,
  146. visits=3,
  147. date_created=before_now(minutes=10),
  148. date_updated=before_now(minutes=10),
  149. last_visited=before_now(minutes=5),
  150. )
  151. model.set_projects(self.project_ids)
  152. for forward_sort in [True, False]:
  153. sorting = "recentlyViewed" if forward_sort else "-recentlyViewed"
  154. with self.feature(self.feature_name):
  155. response = self.client.get(self.url, data={"sortBy": sorting})
  156. assert response.status_code == 200
  157. values = [row["name"] for row in response.data]
  158. expected = ["Test query", "My query"]
  159. if not forward_sort:
  160. expected = list(reversed(expected))
  161. assert values == expected
  162. def test_get_sortby_myqueries(self):
  163. uhoh_user = self.create_user(username="uhoh")
  164. self.create_member(organization=self.org, user=uhoh_user)
  165. whoops_user = self.create_user(username="whoops")
  166. self.create_member(organization=self.org, user=whoops_user)
  167. query = {"fields": ["message"], "query": "", "limit": 10}
  168. model = DiscoverSavedQuery.objects.create(
  169. organization=self.org,
  170. created_by=uhoh_user,
  171. name="a query for uhoh",
  172. query=query,
  173. version=2,
  174. date_created=before_now(minutes=10),
  175. date_updated=before_now(minutes=10),
  176. )
  177. model.set_projects(self.project_ids)
  178. model = DiscoverSavedQuery.objects.create(
  179. organization=self.org,
  180. created_by=whoops_user,
  181. name="a query for whoops",
  182. query=query,
  183. version=2,
  184. date_created=before_now(minutes=10),
  185. date_updated=before_now(minutes=10),
  186. )
  187. model.set_projects(self.project_ids)
  188. with self.feature(self.feature_name):
  189. response = self.client.get(self.url, data={"sortBy": "myqueries"})
  190. assert response.status_code == 200, response.content
  191. values = [int(item["createdBy"]["id"]) for item in response.data]
  192. assert values == [self.user.id, uhoh_user.id, whoops_user.id]
  193. def test_get_expired_query(self):
  194. query = {
  195. "start": iso_format(before_now(days=90)),
  196. "end": iso_format(before_now(days=61)),
  197. }
  198. DiscoverSavedQuery.objects.create(
  199. organization=self.org,
  200. created_by=self.user,
  201. name="My expired query",
  202. query=query,
  203. version=2,
  204. date_created=before_now(days=90),
  205. date_updated=before_now(minutes=10),
  206. )
  207. with self.options({"system.event-retention-days": 60}), self.feature(self.feature_name):
  208. response = self.client.get(self.url, {"query": "name:My expired query"})
  209. assert response.status_code == 200, response.content
  210. assert response.data[0]["expired"]
  211. def test_post(self):
  212. with self.feature(self.feature_name):
  213. response = self.client.post(
  214. self.url,
  215. {
  216. "name": "New query",
  217. "projects": self.project_ids,
  218. "fields": [],
  219. "range": "24h",
  220. "limit": 20,
  221. "conditions": [],
  222. "aggregations": [],
  223. "orderby": "-time",
  224. },
  225. )
  226. assert response.status_code == 201, response.content
  227. assert response.data["name"] == "New query"
  228. assert response.data["projects"] == self.project_ids
  229. assert response.data["range"] == "24h"
  230. assert not hasattr(response.data, "start")
  231. assert not hasattr(response.data, "end")
  232. def test_post_invalid_projects(self):
  233. with self.feature(self.feature_name):
  234. response = self.client.post(
  235. self.url,
  236. {
  237. "name": "New query",
  238. "projects": self.project_ids_without_access,
  239. "fields": [],
  240. "range": "24h",
  241. "limit": 20,
  242. "conditions": [],
  243. "aggregations": [],
  244. "orderby": "-time",
  245. },
  246. )
  247. assert response.status_code == 403, response.content
  248. def test_post_all_projects(self):
  249. with self.feature(self.feature_name):
  250. response = self.client.post(
  251. self.url,
  252. {
  253. "name": "All projects",
  254. "projects": [-1],
  255. "conditions": [],
  256. "fields": ["title", "count()"],
  257. "range": "24h",
  258. "orderby": "time",
  259. },
  260. )
  261. assert response.status_code == 201, response.content
  262. assert response.data["projects"] == [-1]
  263. assert response.data["name"] == "All projects"
  264. def test_post_cannot_use_version_two_fields(self):
  265. with self.feature(self.feature_name):
  266. response = self.client.post(
  267. self.url,
  268. {
  269. "name": "New query",
  270. "projects": self.project_ids,
  271. "fields": ["id"],
  272. "range": "24h",
  273. "limit": 20,
  274. "environment": ["dev"],
  275. "yAxis": ["count(id)"],
  276. "aggregations": [],
  277. "orderby": "-time",
  278. },
  279. )
  280. assert response.status_code == 400, response.content
  281. assert (
  282. "You cannot use the environment, yAxis attribute(s) with the selected version"
  283. == response.data["non_field_errors"][0]
  284. )
  285. class DiscoverSavedQueriesVersion2Test(DiscoverSavedQueryBase):
  286. feature_name = "organizations:discover-query"
  287. def setUp(self):
  288. super().setUp()
  289. self.url = reverse("sentry-api-0-discover-saved-queries", args=[self.org.slug])
  290. def test_post_invalid_conditions(self):
  291. with self.feature(self.feature_name):
  292. response = self.client.post(
  293. self.url,
  294. {
  295. "name": "New query",
  296. "projects": self.project_ids,
  297. "fields": ["title", "count()"],
  298. "range": "24h",
  299. "version": 2,
  300. "conditions": [["field", "=", "value"]],
  301. },
  302. )
  303. assert response.status_code == 400, response.content
  304. assert (
  305. "You cannot use the conditions attribute(s) with the selected version"
  306. == response.data["non_field_errors"][0]
  307. )
  308. def test_post_require_selected_fields(self):
  309. with self.feature(self.feature_name):
  310. response = self.client.post(
  311. self.url,
  312. {
  313. "name": "New query",
  314. "projects": self.project_ids,
  315. "fields": [],
  316. "range": "24h",
  317. "version": 2,
  318. },
  319. )
  320. assert response.status_code == 400, response.content
  321. assert "You must include at least one field." == response.data["non_field_errors"][0]
  322. def test_post_success(self):
  323. with self.feature(self.feature_name):
  324. response = self.client.post(
  325. self.url,
  326. {
  327. "name": "new query",
  328. "projects": self.project_ids,
  329. "fields": ["title", "count()", "project"],
  330. "environment": ["dev"],
  331. "query": "event.type:error browser.name:Firefox",
  332. "range": "24h",
  333. "yAxis": ["count(id)"],
  334. "display": "releases",
  335. "version": 2,
  336. },
  337. )
  338. assert response.status_code == 201, response.content
  339. data = response.data
  340. assert data["fields"] == ["title", "count()", "project"]
  341. assert data["range"] == "24h"
  342. assert data["environment"] == ["dev"]
  343. assert data["query"] == "event.type:error browser.name:Firefox"
  344. assert data["yAxis"] == ["count(id)"]
  345. assert data["display"] == "releases"
  346. assert data["version"] == 2
  347. def test_post_all_projects(self):
  348. with self.feature(self.feature_name):
  349. response = self.client.post(
  350. self.url,
  351. {
  352. "name": "New query",
  353. "projects": [-1],
  354. "fields": ["title", "count()"],
  355. "range": "24h",
  356. "version": 2,
  357. },
  358. )
  359. assert response.status_code == 201, response.content
  360. assert response.data["projects"] == [-1]
  361. def test_save_with_project(self):
  362. with self.feature(self.feature_name):
  363. url = reverse("sentry-api-0-discover-saved-queries", args=[self.org.slug])
  364. response = self.client.post(
  365. url,
  366. {
  367. "name": "project query",
  368. "projects": self.project_ids,
  369. "fields": ["title", "count()"],
  370. "range": "24h",
  371. "query": f"project:{self.projects[0].slug}",
  372. "version": 2,
  373. },
  374. )
  375. assert response.status_code == 201, response.content
  376. assert DiscoverSavedQuery.objects.filter(name="project query").exists()
  377. def test_save_with_project_and_my_projects(self):
  378. team = self.create_team(organization=self.org, members=[self.user])
  379. project = self.create_project(organization=self.org, teams=[team])
  380. with self.feature(self.feature_name):
  381. url = reverse("sentry-api-0-discover-saved-queries", args=[self.org.slug])
  382. response = self.client.post(
  383. url,
  384. {
  385. "name": "project query",
  386. "projects": [],
  387. "fields": ["title", "count()"],
  388. "range": "24h",
  389. "query": f"project:{project.slug}",
  390. "version": 2,
  391. },
  392. )
  393. assert response.status_code == 201, response.content
  394. assert DiscoverSavedQuery.objects.filter(name="project query").exists()
  395. def test_save_with_org_projects(self):
  396. project = self.create_project(organization=self.org)
  397. with self.feature(self.feature_name):
  398. url = reverse("sentry-api-0-discover-saved-queries", args=[self.org.slug])
  399. response = self.client.post(
  400. url,
  401. {
  402. "name": "project query",
  403. "projects": [project.id],
  404. "fields": ["title", "count()"],
  405. "range": "24h",
  406. "version": 2,
  407. },
  408. )
  409. assert response.status_code == 201, response.content
  410. assert DiscoverSavedQuery.objects.filter(name="project query").exists()
  411. def test_save_with_team_project(self):
  412. team = self.create_team(organization=self.org, members=[self.user])
  413. project = self.create_project(organization=self.org, teams=[team])
  414. self.create_project(organization=self.org, teams=[team])
  415. with self.feature(self.feature_name):
  416. url = reverse("sentry-api-0-discover-saved-queries", args=[self.org.slug])
  417. response = self.client.post(
  418. url,
  419. {
  420. "name": "project query",
  421. "projects": [project.id],
  422. "fields": ["title", "count()"],
  423. "range": "24h",
  424. "version": 2,
  425. },
  426. )
  427. assert response.status_code == 201, response.content
  428. assert DiscoverSavedQuery.objects.filter(name="project query").exists()
  429. def test_save_without_team(self):
  430. team = self.create_team(organization=self.org, members=[])
  431. self.create_project(organization=self.org, teams=[team])
  432. with self.feature(self.feature_name):
  433. url = reverse("sentry-api-0-discover-saved-queries", args=[self.org.slug])
  434. response = self.client.post(
  435. url,
  436. {
  437. "name": "without team query",
  438. "projects": [],
  439. "fields": ["title", "count()"],
  440. "range": "24h",
  441. "version": 2,
  442. },
  443. )
  444. assert response.status_code == 400
  445. assert "No Projects found, join a Team" == response.data["detail"]
  446. def test_save_with_team_and_without_project(self):
  447. team = self.create_team(organization=self.org, members=[self.user])
  448. self.create_project(organization=self.org, teams=[team])
  449. with self.feature(self.feature_name):
  450. url = reverse("sentry-api-0-discover-saved-queries", args=[self.org.slug])
  451. response = self.client.post(
  452. url,
  453. {
  454. "name": "with team query",
  455. "projects": [],
  456. "fields": ["title", "count()"],
  457. "range": "24h",
  458. "version": 2,
  459. },
  460. )
  461. assert response.status_code == 201, response.content
  462. assert DiscoverSavedQuery.objects.filter(name="with team query").exists()
  463. def test_save_with_wrong_projects(self):
  464. other_org = self.create_organization(owner=self.user)
  465. project = self.create_project(organization=other_org)
  466. project2 = self.create_project(organization=self.org)
  467. with self.feature(self.feature_name):
  468. url = reverse("sentry-api-0-discover-saved-queries", args=[self.org.slug])
  469. response = self.client.post(
  470. url,
  471. {
  472. "name": "project query",
  473. "projects": [project.id],
  474. "fields": ["title", "count()"],
  475. "range": "24h",
  476. "query": f"project:{project.slug}",
  477. "version": 2,
  478. },
  479. )
  480. assert response.status_code == 403, response.content
  481. assert not DiscoverSavedQuery.objects.filter(name="project query").exists()
  482. with self.feature(self.feature_name):
  483. url = reverse("sentry-api-0-discover-saved-queries", args=[self.org.slug])
  484. response = self.client.post(
  485. url,
  486. {
  487. "name": "project query",
  488. "projects": [project.id, project2.id],
  489. "fields": ["title", "count()"],
  490. "range": "24h",
  491. "query": f"project:{project.slug} project:{project2.slug}",
  492. "version": 2,
  493. },
  494. )
  495. assert response.status_code == 403, response.content
  496. assert not DiscoverSavedQuery.objects.filter(name="project query").exists()
  497. # Mix of wrong + valid
  498. with self.feature(self.feature_name):
  499. url = reverse("sentry-api-0-discover-saved-queries", args=[self.org.slug])
  500. response = self.client.post(
  501. url,
  502. {
  503. "name": "project query",
  504. "projects": [-1],
  505. "fields": ["title", "count()"],
  506. "range": "24h",
  507. "query": f"project:{project.slug} project:{project2.slug}",
  508. "version": 2,
  509. },
  510. )
  511. assert response.status_code == 400, response.content
  512. assert not DiscoverSavedQuery.objects.filter(name="project query").exists()
  513. def test_save_with_equation(self):
  514. with self.feature(self.feature_name):
  515. response = self.client.post(
  516. self.url,
  517. {
  518. "name": "Equation query",
  519. "projects": [-1],
  520. "fields": [
  521. "title",
  522. "equation|count_if(measurements.lcp,greater,4000) / count()",
  523. "count()",
  524. "count_if(measurements.lcp,greater,4000)",
  525. ],
  526. "orderby": "equation[0]",
  527. "range": "24h",
  528. "query": "title:1",
  529. "version": 2,
  530. },
  531. )
  532. assert response.status_code == 201, response.content
  533. assert DiscoverSavedQuery.objects.filter(name="Equation query").exists()
  534. def test_save_with_invalid_equation(self):
  535. with self.feature(self.feature_name):
  536. response = self.client.post(
  537. self.url,
  538. {
  539. "name": "Equation query",
  540. "projects": [-1],
  541. "fields": [
  542. "title",
  543. "equation|count_if(measurements.lcp,greater,4000) / 0",
  544. "count()",
  545. "count_if(measurements.lcp,greater,4000)",
  546. ],
  547. "orderby": "equation[0]",
  548. "range": "24h",
  549. "query": "title:1",
  550. "version": 2,
  551. },
  552. )
  553. assert response.status_code == 400, response.content
  554. assert not DiscoverSavedQuery.objects.filter(name="Equation query").exists()
  555. def test_save_invalid_query(self):
  556. with self.feature(self.feature_name):
  557. response = self.client.post(
  558. self.url,
  559. {
  560. "name": "Bad query",
  561. "projects": [-1],
  562. "fields": ["title", "count()"],
  563. "range": "24h",
  564. "query": "spaceAfterColon: 1",
  565. "version": 2,
  566. },
  567. )
  568. assert response.status_code == 400, response.content
  569. assert not DiscoverSavedQuery.objects.filter(name="Bad query").exists()
  570. def test_save_invalid_query_orderby(self):
  571. with self.feature(self.feature_name):
  572. response = self.client.post(
  573. self.url,
  574. {
  575. "name": "Bad query",
  576. "projects": [-1],
  577. "fields": ["title", "count()"],
  578. "orderby": "fake()",
  579. "range": "24h",
  580. "query": "title:1",
  581. "version": 2,
  582. },
  583. )
  584. assert response.status_code == 400, response.content
  585. assert not DiscoverSavedQuery.objects.filter(name="Bad query").exists()
  586. def test_save_query_long_name(self):
  587. with self.feature(self.feature_name):
  588. response = self.client.post(
  589. self.url,
  590. {
  591. "name": "Bad query" * 200,
  592. "projects": [-1],
  593. "fields": ["title", "count()"],
  594. "range": "24h",
  595. "query": "spaceAfterColon:1",
  596. "version": 2,
  597. },
  598. )
  599. assert response.status_code == 400, response.content
  600. assert not DiscoverSavedQuery.objects.filter(name="Bad query" * 200).exists()