test_organization_events_facets.py 33 KB

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