test_organization_events_facets.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611
  1. from datetime import timedelta
  2. from unittest import mock
  3. from uuid import uuid4
  4. from django.urls import reverse
  5. from django.utils import timezone
  6. from pytz import utc
  7. from rest_framework.exceptions import ParseError
  8. from sentry.testutils 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_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_with_message_query(self):
  78. self.store_event(
  79. data={
  80. "event_id": uuid4().hex,
  81. "timestamp": self.min_ago_iso,
  82. "message": "how to make fast",
  83. "tags": {"color": "green"},
  84. },
  85. project_id=self.project.id,
  86. )
  87. self.store_event(
  88. data={
  89. "event_id": uuid4().hex,
  90. "timestamp": self.min_ago_iso,
  91. "message": "Delet the Data",
  92. "tags": {"color": "red"},
  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. "message": "Data the Delet ",
  101. "tags": {"color": "yellow"},
  102. },
  103. project_id=self.project2.id,
  104. )
  105. with self.feature(self.features):
  106. response = self.client.get(self.url, {"query": "delet"}, format="json")
  107. assert response.status_code == 200, response.content
  108. expected = [
  109. {"count": 1, "name": "yellow", "value": "yellow"},
  110. {"count": 1, "name": "red", "value": "red"},
  111. ]
  112. self.assert_facet(response, "color", expected)
  113. def test_with_condition(self):
  114. self.store_event(
  115. data={
  116. "event_id": uuid4().hex,
  117. "timestamp": self.min_ago_iso,
  118. "message": "how to make fast",
  119. "tags": {"color": "green"},
  120. },
  121. project_id=self.project.id,
  122. )
  123. self.store_event(
  124. data={
  125. "event_id": uuid4().hex,
  126. "timestamp": self.min_ago_iso,
  127. "message": "Delet the Data",
  128. "tags": {"color": "red"},
  129. },
  130. project_id=self.project.id,
  131. )
  132. self.store_event(
  133. data={
  134. "event_id": uuid4().hex,
  135. "timestamp": self.min_ago_iso,
  136. "message": "Data the Delet ",
  137. "tags": {"color": "yellow"},
  138. },
  139. project_id=self.project2.id,
  140. )
  141. with self.feature(self.features):
  142. response = self.client.get(self.url, {"query": "color:yellow"}, format="json")
  143. assert response.status_code == 200, response.content
  144. expected = [{"count": 1, "name": "yellow", "value": "yellow"}]
  145. self.assert_facet(response, "color", expected)
  146. def test_with_conditional_filter(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(
  176. self.url, {"query": "color:yellow OR color:red"}, format="json"
  177. )
  178. assert response.status_code == 200, response.content
  179. expected = [
  180. {"count": 1, "name": "yellow", "value": "yellow"},
  181. {"count": 1, "name": "red", "value": "red"},
  182. ]
  183. self.assert_facet(response, "color", expected)
  184. def test_start_end(self):
  185. two_days_ago = self.day_ago - timedelta(days=1)
  186. hour_ago = self.min_ago - timedelta(hours=1)
  187. two_hours_ago = hour_ago - timedelta(hours=1)
  188. self.store_event(
  189. data={
  190. "event_id": uuid4().hex,
  191. "timestamp": iso_format(two_days_ago),
  192. "tags": {"color": "red"},
  193. },
  194. project_id=self.project.id,
  195. )
  196. self.store_event(
  197. data={
  198. "event_id": uuid4().hex,
  199. "timestamp": iso_format(hour_ago),
  200. "tags": {"color": "red"},
  201. },
  202. project_id=self.project.id,
  203. )
  204. self.store_event(
  205. data={
  206. "event_id": uuid4().hex,
  207. "timestamp": iso_format(two_hours_ago),
  208. "tags": {"color": "red"},
  209. },
  210. project_id=self.project.id,
  211. )
  212. self.store_event(
  213. data={
  214. "event_id": uuid4().hex,
  215. "timestamp": iso_format(timezone.now()),
  216. "tags": {"color": "red"},
  217. },
  218. project_id=self.project2.id,
  219. )
  220. with self.feature(self.features):
  221. response = self.client.get(
  222. self.url,
  223. {"start": iso_format(self.day_ago), "end": iso_format(self.min_ago)},
  224. format="json",
  225. )
  226. assert response.status_code == 200, response.content
  227. expected = [{"count": 2, "name": "red", "value": "red"}]
  228. self.assert_facet(response, "color", expected)
  229. def test_excluded_tag(self):
  230. self.user = self.create_user()
  231. self.user2 = self.create_user()
  232. self.store_event(
  233. data={
  234. "event_id": uuid4().hex,
  235. "timestamp": iso_format(self.day_ago),
  236. "message": "very bad",
  237. "tags": {"sentry:user": self.user.email},
  238. },
  239. project_id=self.project.id,
  240. )
  241. self.store_event(
  242. data={
  243. "event_id": uuid4().hex,
  244. "timestamp": iso_format(self.day_ago),
  245. "message": "very bad",
  246. "tags": {"sentry:user": self.user2.email},
  247. },
  248. project_id=self.project.id,
  249. )
  250. self.store_event(
  251. data={
  252. "event_id": uuid4().hex,
  253. "timestamp": iso_format(self.day_ago),
  254. "message": "very bad",
  255. "tags": {"sentry:user": self.user2.email},
  256. },
  257. project_id=self.project.id,
  258. )
  259. with self.feature(self.features):
  260. response = self.client.get(self.url, format="json", data={"project": [self.project.id]})
  261. assert response.status_code == 200, response.content
  262. expected = [
  263. {"count": 2, "name": self.user2.email, "value": self.user2.email},
  264. {"count": 1, "name": self.user.email, "value": self.user.email},
  265. ]
  266. self.assert_facet(response, "user", expected)
  267. def test_no_projects(self):
  268. org = self.create_organization(owner=self.user)
  269. url = reverse(
  270. "sentry-api-0-organization-events-facets", kwargs={"organization_slug": org.slug}
  271. )
  272. with self.feature("organizations:discover-basic"):
  273. response = self.client.get(url, format="json")
  274. assert response.status_code == 200, response.content
  275. assert response.data == []
  276. def test_multiple_projects_without_global_view(self):
  277. self.store_event(data={"event_id": uuid4().hex}, project_id=self.project.id)
  278. self.store_event(data={"event_id": uuid4().hex}, project_id=self.project2.id)
  279. with self.feature("organizations:discover-basic"):
  280. response = self.client.get(self.url, format="json")
  281. assert response.status_code == 400, response.content
  282. assert response.data == {"detail": "You cannot view events from multiple projects."}
  283. def test_project_selected(self):
  284. self.store_event(
  285. data={
  286. "event_id": uuid4().hex,
  287. "timestamp": self.min_ago_iso,
  288. "tags": {"number": "two"},
  289. },
  290. project_id=self.project.id,
  291. )
  292. self.store_event(
  293. data={
  294. "event_id": uuid4().hex,
  295. "timestamp": self.min_ago_iso,
  296. "tags": {"number": "one"},
  297. },
  298. project_id=self.project2.id,
  299. )
  300. with self.feature(self.features):
  301. response = self.client.get(self.url, {"project": [self.project.id]}, format="json")
  302. assert response.status_code == 200, response.content
  303. expected = [{"name": "two", "value": "two", "count": 1}]
  304. self.assert_facet(response, "number", expected)
  305. def test_project_filtered(self):
  306. self.store_event(
  307. data={
  308. "event_id": uuid4().hex,
  309. "timestamp": self.min_ago_iso,
  310. "tags": {"number": "two"},
  311. },
  312. project_id=self.project.id,
  313. )
  314. self.store_event(
  315. data={
  316. "event_id": uuid4().hex,
  317. "timestamp": self.min_ago_iso,
  318. "tags": {"number": "one"},
  319. },
  320. project_id=self.project2.id,
  321. )
  322. with self.feature(self.features):
  323. response = self.client.get(
  324. self.url, {"query": f"project:{self.project.slug}"}, format="json"
  325. )
  326. assert response.status_code == 200, response.content
  327. expected = [{"name": "two", "value": "two", "count": 1}]
  328. self.assert_facet(response, "number", expected)
  329. def test_project_key(self):
  330. self.store_event(
  331. data={
  332. "event_id": uuid4().hex,
  333. "timestamp": self.min_ago_iso,
  334. "tags": {"color": "green"},
  335. },
  336. project_id=self.project.id,
  337. )
  338. self.store_event(
  339. data={
  340. "event_id": uuid4().hex,
  341. "timestamp": self.min_ago_iso,
  342. "tags": {"number": "one"},
  343. },
  344. project_id=self.project2.id,
  345. )
  346. self.store_event(
  347. data={
  348. "event_id": uuid4().hex,
  349. "timestamp": self.min_ago_iso,
  350. "tags": {"color": "green"},
  351. },
  352. project_id=self.project.id,
  353. )
  354. self.store_event(
  355. data={"event_id": uuid4().hex, "timestamp": self.min_ago_iso, "tags": {"color": "red"}},
  356. project_id=self.project.id,
  357. )
  358. with self.feature(self.features):
  359. response = self.client.get(self.url, format="json")
  360. assert response.status_code == 200, response.content
  361. expected = [
  362. {"count": 3, "name": self.project.slug, "value": self.project.id},
  363. {"count": 1, "name": self.project2.slug, "value": self.project2.id},
  364. ]
  365. self.assert_facet(response, "project", expected)
  366. def test_project_key_with_project_tag(self):
  367. self.organization.flags.allow_joinleave = False
  368. self.organization.save()
  369. member_user = self.create_user()
  370. team = self.create_team(members=[member_user])
  371. private_project1 = self.create_project(organization=self.organization, teams=[team])
  372. private_project2 = self.create_project(organization=self.organization, teams=[team])
  373. self.login_as(member_user)
  374. self.store_event(
  375. data={
  376. "event_id": uuid4().hex,
  377. "timestamp": self.min_ago_iso,
  378. "tags": {"color": "green", "project": "%d" % private_project1.id},
  379. },
  380. project_id=self.project.id,
  381. )
  382. self.store_event(
  383. data={
  384. "event_id": uuid4().hex,
  385. "timestamp": self.min_ago_iso,
  386. "tags": {"number": "one", "project": "%d" % private_project1.id},
  387. },
  388. project_id=private_project1.id,
  389. )
  390. self.store_event(
  391. data={
  392. "event_id": uuid4().hex,
  393. "timestamp": self.min_ago_iso,
  394. "tags": {"color": "green"},
  395. },
  396. project_id=private_project1.id,
  397. )
  398. self.store_event(
  399. data={"event_id": uuid4().hex, "timestamp": self.min_ago_iso, "tags": {"color": "red"}},
  400. project_id=private_project2.id,
  401. )
  402. self.store_event(
  403. data={"event_id": uuid4().hex, "timestamp": self.min_ago_iso, "tags": {"color": "red"}},
  404. project_id=private_project2.id,
  405. )
  406. with self.feature(self.features):
  407. response = self.client.get(self.url, format="json")
  408. assert response.status_code == 200, response.content
  409. expected = [
  410. {"count": 2, "name": private_project1.slug, "value": private_project1.id},
  411. {"count": 2, "name": private_project2.slug, "value": private_project2.id},
  412. ]
  413. self.assert_facet(response, "project", expected)
  414. def test_malformed_query(self):
  415. self.store_event(data={"event_id": uuid4().hex}, project_id=self.project.id)
  416. self.store_event(data={"event_id": uuid4().hex}, project_id=self.project2.id)
  417. with self.feature(self.features):
  418. response = self.client.get(self.url, format="json", data={"query": "\n\n\n\n"})
  419. assert response.status_code == 400, response.content
  420. assert response.data["detail"].endswith(
  421. "(column 1). This is commonly caused by unmatched parentheses. Enclose any text in double quotes."
  422. )
  423. @mock.patch("sentry.snuba.discover.raw_query")
  424. def test_handling_snuba_errors(self, mock_query):
  425. mock_query.side_effect = ParseError("test")
  426. with self.feature(self.features):
  427. response = self.client.get(self.url, format="json")
  428. assert response.status_code == 400, response.content
  429. def test_environment(self):
  430. self.store_event(
  431. data={
  432. "event_id": uuid4().hex,
  433. "timestamp": self.min_ago_iso,
  434. "tags": {"number": "one"},
  435. "environment": "staging",
  436. },
  437. project_id=self.project2.id,
  438. )
  439. self.store_event(
  440. data={
  441. "event_id": uuid4().hex,
  442. "timestamp": self.min_ago_iso,
  443. "tags": {"number": "one"},
  444. "environment": "production",
  445. },
  446. project_id=self.project.id,
  447. )
  448. self.store_event(
  449. data={
  450. "event_id": uuid4().hex,
  451. "timestamp": self.min_ago_iso,
  452. "tags": {"number": "two"},
  453. },
  454. project_id=self.project.id,
  455. )
  456. with self.feature(self.features):
  457. response = self.client.get(self.url, format="json")
  458. assert response.status_code == 200, response.content
  459. expected = [
  460. {"count": 1, "name": "production", "value": "production"},
  461. {"count": 1, "name": "staging", "value": "staging"},
  462. {"count": 1, "name": None, "value": None},
  463. ]
  464. self.assert_facet(response, "environment", expected)
  465. with self.feature(self.features):
  466. # query by an environment
  467. response = self.client.get(self.url, {"environment": "staging"}, format="json")
  468. assert response.status_code == 200, response.content
  469. expected = [{"count": 1, "name": "staging", "value": "staging"}]
  470. self.assert_facet(response, "environment", expected)
  471. with self.feature(self.features):
  472. # query by multiple environments
  473. response = self.client.get(
  474. self.url, {"environment": ["staging", "production"]}, format="json"
  475. )
  476. assert response.status_code == 200, response.content
  477. expected = [
  478. {"count": 1, "name": "production", "value": "production"},
  479. {"count": 1, "name": "staging", "value": "staging"},
  480. ]
  481. self.assert_facet(response, "environment", expected)
  482. with self.feature(self.features):
  483. # query by multiple environments, including the "no environment" environment
  484. response = self.client.get(
  485. self.url, {"environment": ["staging", "production", ""]}, format="json"
  486. )
  487. assert response.status_code == 200, response.content
  488. expected = [
  489. {"count": 1, "name": "production", "value": "production"},
  490. {"count": 1, "name": "staging", "value": "staging"},
  491. {"count": 1, "name": None, "value": None},
  492. ]
  493. self.assert_facet(response, "environment", expected)
  494. def test_out_of_retention(self):
  495. with self.options({"system.event-retention-days": 10}):
  496. with self.feature(self.features):
  497. response = self.client.get(
  498. self.url,
  499. format="json",
  500. data={
  501. "start": iso_format(before_now(days=20)),
  502. "end": iso_format(before_now(days=15)),
  503. },
  504. )
  505. assert response.status_code == 400
  506. @mock.patch("sentry.utils.snuba.quantize_time")
  507. def test_quantize_dates(self, mock_quantize):
  508. mock_quantize.return_value = before_now(days=1).replace(tzinfo=utc)
  509. with self.feature("organizations:discover-basic"):
  510. # Don't quantize short time periods
  511. self.client.get(
  512. self.url,
  513. format="json",
  514. data={"statsPeriod": "1h", "query": "", "field": ["id", "timestamp"]},
  515. )
  516. # Don't quantize absolute date periods
  517. self.client.get(
  518. self.url,
  519. format="json",
  520. data={
  521. "start": iso_format(before_now(days=20)),
  522. "end": iso_format(before_now(days=15)),
  523. "query": "",
  524. "field": ["id", "timestamp"],
  525. },
  526. )
  527. assert len(mock_quantize.mock_calls) == 0
  528. # Quantize long date periods
  529. self.client.get(
  530. self.url,
  531. format="json",
  532. data={"field": ["id", "timestamp"], "statsPeriod": "90d", "query": ""},
  533. )
  534. assert len(mock_quantize.mock_calls) == 2
  535. class OrganizationEventsFacetsEndpointTestWithSnql(OrganizationEventsFacetsEndpointTest):
  536. def setUp(self):
  537. super().setUp()
  538. self.features["organizations:discover-use-snql"] = True
  539. # Separate test for now to keep the patching simpler
  540. @mock.patch("sentry.search.events.builder.raw_snql_query")
  541. def test_handling_snuba_errors(self, mock_query):
  542. mock_query.side_effect = ParseError("test")
  543. with self.feature(self.features):
  544. response = self.client.get(self.url, format="json")
  545. assert response.status_code == 400, response.content