test_organization_events_facets.py 33 KB

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