test_organization_stats_v2.py 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752
  1. import functools
  2. from datetime import datetime, timedelta, timezone
  3. from django.urls import reverse
  4. from sentry.constants import DataCategory
  5. from sentry.testutils.cases import APITestCase, OutcomesSnubaTest
  6. from sentry.testutils.helpers.datetime import freeze_time
  7. from sentry.testutils.silo import region_silo_test
  8. from sentry.utils.outcomes import Outcome
  9. @region_silo_test
  10. class OrganizationStatsTestV2(APITestCase, OutcomesSnubaTest):
  11. def setUp(self):
  12. super().setUp()
  13. self.now = datetime(2021, 3, 14, 12, 27, 28, tzinfo=timezone.utc)
  14. self.login_as(user=self.user)
  15. self.org = self.organization
  16. self.org.flags.allow_joinleave = False
  17. self.org.save()
  18. self.org2 = self.create_organization()
  19. self.org3 = self.create_organization()
  20. self.project = self.create_project(
  21. name="bar", teams=[self.create_team(organization=self.org, members=[self.user])]
  22. )
  23. self.project2 = self.create_project(
  24. name="foo", teams=[self.create_team(organization=self.org, members=[self.user])]
  25. )
  26. self.project3 = self.create_project(organization=self.org2)
  27. self.user2 = self.create_user(is_superuser=False)
  28. self.create_member(user=self.user2, organization=self.organization, role="member", teams=[])
  29. self.create_member(user=self.user2, organization=self.org3, role="member", teams=[])
  30. self.project4 = self.create_project(
  31. name="users2sproj",
  32. teams=[self.create_team(organization=self.org, members=[self.user2])],
  33. )
  34. self.store_outcomes(
  35. {
  36. "org_id": self.org.id,
  37. "timestamp": self.now - timedelta(hours=1),
  38. "project_id": self.project.id,
  39. "outcome": Outcome.ACCEPTED,
  40. "reason": "none",
  41. "category": DataCategory.ERROR,
  42. "quantity": 1,
  43. },
  44. 5,
  45. )
  46. self.store_outcomes(
  47. {
  48. "org_id": self.org.id,
  49. "timestamp": self.now - timedelta(hours=1),
  50. "project_id": self.project.id,
  51. "outcome": Outcome.ACCEPTED,
  52. "reason": "none",
  53. "category": DataCategory.DEFAULT, # test that this shows up under error
  54. "quantity": 1,
  55. }
  56. )
  57. self.store_outcomes(
  58. {
  59. "org_id": self.org.id,
  60. "timestamp": self.now - timedelta(hours=1),
  61. "project_id": self.project.id,
  62. "outcome": Outcome.RATE_LIMITED,
  63. "reason": "smart_rate_limit",
  64. "category": DataCategory.ATTACHMENT,
  65. "quantity": 1024,
  66. }
  67. )
  68. self.store_outcomes(
  69. {
  70. "org_id": self.org.id,
  71. "timestamp": self.now - timedelta(hours=1),
  72. "project_id": self.project2.id,
  73. "outcome": Outcome.RATE_LIMITED,
  74. "reason": "smart_rate_limit",
  75. "category": DataCategory.TRANSACTION,
  76. "quantity": 1,
  77. }
  78. )
  79. def do_request(self, query, user=None, org=None):
  80. self.login_as(user=user or self.user)
  81. url = reverse(
  82. "sentry-api-0-organization-stats-v2",
  83. kwargs={"organization_slug": (org or self.organization).slug},
  84. )
  85. return self.client.get(url, query, format="json")
  86. def test_empty_request(self):
  87. response = self.do_request({})
  88. assert response.status_code == 400, response.content
  89. assert result_sorted(response.data) == {"detail": 'At least one "field" is required.'}
  90. def test_inaccessible_project(self):
  91. response = self.do_request({"project": [self.project3.id]})
  92. assert response.status_code == 403, response.content
  93. assert result_sorted(response.data) == {
  94. "detail": "You do not have permission to perform this action."
  95. }
  96. def test_no_projects_available(self):
  97. response = self.do_request(
  98. {
  99. "groupBy": ["project"],
  100. "statsPeriod": "1d",
  101. "interval": "1d",
  102. "field": ["sum(quantity)"],
  103. "category": ["error", "transaction"],
  104. },
  105. user=self.user2,
  106. org=self.org3,
  107. )
  108. assert response.status_code == 400, response.content
  109. assert result_sorted(response.data) == {
  110. "detail": "No projects available",
  111. }
  112. def test_unknown_field(self):
  113. response = self.do_request(
  114. {
  115. "field": ["summ(qarntenty)"],
  116. "statsPeriod": "1d",
  117. "interval": "1d",
  118. }
  119. )
  120. assert response.status_code == 400, response.content
  121. assert result_sorted(response.data) == {
  122. "detail": 'Invalid field: "summ(qarntenty)"',
  123. }
  124. def test_no_end_param(self):
  125. response = self.do_request(
  126. {"field": ["sum(quantity)"], "interval": "1d", "start": "2021-03-14T00:00:00Z"}
  127. )
  128. assert response.status_code == 400, response.content
  129. assert result_sorted(response.data) == {"detail": "start and end are both required"}
  130. @freeze_time(datetime(2021, 3, 14, 12, 27, 28, tzinfo=timezone.utc))
  131. def test_future_request(self):
  132. response = self.do_request(
  133. {
  134. "field": ["sum(quantity)"],
  135. "interval": "1h",
  136. "category": ["error"],
  137. "start": "2021-03-14T15:30:00",
  138. "end": "2021-03-14T16:30:00",
  139. }
  140. )
  141. assert response.status_code == 200, response.content
  142. assert result_sorted(response.data) == {
  143. "intervals": [
  144. "2021-03-14T12:00:00Z",
  145. "2021-03-14T13:00:00Z",
  146. "2021-03-14T14:00:00Z",
  147. "2021-03-14T15:00:00Z",
  148. "2021-03-14T16:00:00Z",
  149. ],
  150. "groups": [
  151. {
  152. "by": {},
  153. "series": {"sum(quantity)": [0, 0, 0, 0, 0]},
  154. "totals": {"sum(quantity)": 0},
  155. }
  156. ],
  157. "start": "2021-03-14T12:00:00Z",
  158. "end": "2021-03-14T17:00:00Z",
  159. }
  160. def test_unknown_category(self):
  161. response = self.do_request(
  162. {
  163. "field": ["sum(quantity)"],
  164. "statsPeriod": "1d",
  165. "interval": "1d",
  166. "category": "scoobydoo",
  167. }
  168. )
  169. assert response.status_code == 400, response.content
  170. assert result_sorted(response.data) == {
  171. "detail": 'Invalid category: "scoobydoo"',
  172. }
  173. def test_unknown_outcome(self):
  174. response = self.do_request(
  175. {
  176. "field": ["sum(quantity)"],
  177. "statsPeriod": "1d",
  178. "interval": "1d",
  179. "category": "error",
  180. "outcome": "scoobydoo",
  181. }
  182. )
  183. assert response.status_code == 400, response.content
  184. assert result_sorted(response.data) == {
  185. "detail": 'Invalid outcome: "scoobydoo"',
  186. }
  187. def test_unknown_groupby(self):
  188. response = self.do_request(
  189. {
  190. "field": ["sum(quantity)"],
  191. "groupBy": ["category_"],
  192. "statsPeriod": "1d",
  193. "interval": "1d",
  194. }
  195. )
  196. assert response.status_code == 400, response.content
  197. assert result_sorted(response.data) == {"detail": 'Invalid groupBy: "category_"'}
  198. def test_resolution_invalid(self):
  199. self.login_as(user=self.user)
  200. make_request = functools.partial(
  201. self.client.get,
  202. reverse("sentry-api-0-organization-stats-v2", args=[self.org.slug]),
  203. )
  204. response = make_request(
  205. {
  206. "statsPeriod": "1d",
  207. "interval": "bad_interval",
  208. }
  209. )
  210. assert response.status_code == 400, response.content
  211. @freeze_time("2021-03-14T12:27:28.303Z")
  212. def test_attachment_filter_only(self):
  213. response = self.do_request(
  214. {
  215. "project": [-1],
  216. "statsPeriod": "1d",
  217. "interval": "1d",
  218. "field": ["sum(quantity)"],
  219. "category": ["error", "attachment"],
  220. }
  221. )
  222. assert response.status_code == 400, response.content
  223. assert result_sorted(response.data) == {
  224. "detail": "if filtering by attachment no other category may be present"
  225. }
  226. @freeze_time("2021-03-14T12:27:28.303Z")
  227. def test_timeseries_interval(self):
  228. response = self.do_request(
  229. {
  230. "project": [-1],
  231. "category": ["error"],
  232. "statsPeriod": "1d",
  233. "interval": "1d",
  234. "field": ["sum(quantity)"],
  235. }
  236. )
  237. assert response.status_code == 200, response.content
  238. assert result_sorted(response.data) == {
  239. "intervals": ["2021-03-13T00:00:00Z", "2021-03-14T00:00:00Z"],
  240. "groups": [
  241. {"by": {}, "series": {"sum(quantity)": [0, 6]}, "totals": {"sum(quantity)": 6}}
  242. ],
  243. "start": "2021-03-13T00:00:00Z",
  244. "end": "2021-03-15T00:00:00Z",
  245. }
  246. response = self.do_request(
  247. {
  248. "project": [-1],
  249. "statsPeriod": "1d",
  250. "interval": "6h",
  251. "field": ["sum(quantity)"],
  252. "category": ["error"],
  253. }
  254. )
  255. assert response.status_code == 200, response.content
  256. assert result_sorted(response.data) == {
  257. "intervals": [
  258. "2021-03-13T12:00:00Z",
  259. "2021-03-13T18:00:00Z",
  260. "2021-03-14T00:00:00Z",
  261. "2021-03-14T06:00:00Z",
  262. "2021-03-14T12:00:00Z",
  263. ],
  264. "groups": [
  265. {
  266. "by": {},
  267. "series": {"sum(quantity)": [0, 0, 0, 6, 0]},
  268. "totals": {"sum(quantity)": 6},
  269. }
  270. ],
  271. "start": "2021-03-13T12:00:00Z",
  272. "end": "2021-03-14T18:00:00Z",
  273. }
  274. @freeze_time("2021-03-14T12:27:28.303Z")
  275. def test_user_org_total_all_accessible(self):
  276. response = self.do_request(
  277. {
  278. "project": [-1],
  279. "statsPeriod": "1d",
  280. "interval": "1d",
  281. "field": ["sum(quantity)"],
  282. "category": ["error", "transaction"],
  283. },
  284. user=self.user2,
  285. )
  286. assert response.status_code == 200, response.content
  287. assert result_sorted(response.data) == {
  288. "start": "2021-03-13T00:00:00Z",
  289. "end": "2021-03-15T00:00:00Z",
  290. "intervals": ["2021-03-13T00:00:00Z", "2021-03-14T00:00:00Z"],
  291. "groups": [
  292. {"by": {}, "series": {"sum(quantity)": [0, 7]}, "totals": {"sum(quantity)": 7}}
  293. ],
  294. }
  295. @freeze_time("2021-03-14T12:27:28.303Z")
  296. def test_user_no_proj_specific_access(self):
  297. response = self.do_request(
  298. {
  299. "project": self.project.id,
  300. "statsPeriod": "1d",
  301. "interval": "1d",
  302. "field": ["sum(quantity)"],
  303. "category": ["error", "transaction"],
  304. },
  305. user=self.user2,
  306. )
  307. assert response.status_code == 403
  308. response = self.do_request(
  309. {
  310. "project": [-1],
  311. "statsPeriod": "1d",
  312. "interval": "1d",
  313. "field": ["sum(quantity)"],
  314. "category": ["error", "transaction"],
  315. "groupBy": ["project"],
  316. },
  317. user=self.user2,
  318. )
  319. assert response.status_code == 200
  320. assert result_sorted(response.data) == {
  321. "start": "2021-03-13T00:00:00Z",
  322. "end": "2021-03-15T00:00:00Z",
  323. "groups": [],
  324. }
  325. @freeze_time("2021-03-14T12:27:28.303Z")
  326. def test_no_project_access(self):
  327. user = self.create_user(is_superuser=False)
  328. self.create_member(user=user, organization=self.organization, role="member", teams=[])
  329. response = self.do_request(
  330. {
  331. "project": [self.project.id],
  332. "statsPeriod": "1d",
  333. "interval": "1d",
  334. "category": ["error", "transaction"],
  335. "field": ["sum(quantity)"],
  336. },
  337. org=self.organization,
  338. user=user,
  339. )
  340. assert response.status_code == 403, response.content
  341. assert result_sorted(response.data) == {
  342. "detail": "You do not have permission to perform this action."
  343. }
  344. response = self.do_request(
  345. {
  346. "project": [self.project.id],
  347. "groupBy": ["project"],
  348. "statsPeriod": "1d",
  349. "interval": "1d",
  350. "category": ["error", "transaction"],
  351. "field": ["sum(quantity)"],
  352. },
  353. org=self.organization,
  354. user=user,
  355. )
  356. assert response.status_code == 403, response.content
  357. assert result_sorted(response.data) == {
  358. "detail": "You do not have permission to perform this action."
  359. }
  360. @freeze_time("2021-03-14T12:27:28.303Z")
  361. def test_open_membership_semantics(self):
  362. self.org.flags.allow_joinleave = True
  363. self.org.save()
  364. response = self.do_request(
  365. {
  366. "project": [-1],
  367. "statsPeriod": "1d",
  368. "interval": "1d",
  369. "field": ["sum(quantity)"],
  370. "category": ["error", "transaction"],
  371. "groupBy": ["project"],
  372. },
  373. user=self.user2,
  374. )
  375. assert response.status_code == 200
  376. assert result_sorted(response.data) == {
  377. "start": "2021-03-13T00:00:00Z",
  378. "end": "2021-03-15T00:00:00Z",
  379. "groups": [
  380. {
  381. "by": {"project": self.project.id},
  382. "totals": {"sum(quantity)": 6},
  383. },
  384. {
  385. "by": {"project": self.project2.id},
  386. "totals": {"sum(quantity)": 1},
  387. },
  388. ],
  389. }
  390. @freeze_time("2021-03-14T12:27:28.303Z")
  391. def test_org_simple(self):
  392. make_request = functools.partial(
  393. self.client.get, reverse("sentry-api-0-organization-stats-v2", args=[self.org.slug])
  394. )
  395. response = make_request(
  396. {
  397. "statsPeriod": "2d",
  398. "interval": "1d",
  399. "field": ["sum(quantity)"],
  400. "groupBy": ["category", "outcome", "reason"],
  401. }
  402. )
  403. assert response.status_code == 200, response.content
  404. assert result_sorted(response.data) == {
  405. "start": "2021-03-12T00:00:00Z",
  406. "end": "2021-03-15T00:00:00Z",
  407. "intervals": ["2021-03-12T00:00:00Z", "2021-03-13T00:00:00Z", "2021-03-14T00:00:00Z"],
  408. "groups": [
  409. {
  410. "by": {
  411. "outcome": "rate_limited",
  412. "reason": "spike_protection",
  413. "category": "attachment",
  414. },
  415. "totals": {"sum(quantity)": 1024},
  416. "series": {"sum(quantity)": [0, 0, 1024]},
  417. },
  418. {
  419. "by": {"outcome": "accepted", "reason": "none", "category": "error"},
  420. "totals": {"sum(quantity)": 6},
  421. "series": {"sum(quantity)": [0, 0, 6]},
  422. },
  423. {
  424. "by": {
  425. "category": "transaction",
  426. "reason": "spike_protection",
  427. "outcome": "rate_limited",
  428. },
  429. "totals": {"sum(quantity)": 1},
  430. "series": {"sum(quantity)": [0, 0, 1]},
  431. },
  432. ],
  433. }
  434. @freeze_time("2021-03-14T12:27:28.303Z")
  435. def test_org_multiple_fields(self):
  436. make_request = functools.partial(
  437. self.client.get, reverse("sentry-api-0-organization-stats-v2", args=[self.org.slug])
  438. )
  439. response = make_request(
  440. {
  441. "statsPeriod": "2d",
  442. "interval": "1d",
  443. "field": ["sum(quantity)", "sum(times_seen)"],
  444. "groupBy": ["category", "outcome", "reason"],
  445. }
  446. )
  447. assert response.status_code == 200, response.content
  448. assert result_sorted(response.data) == {
  449. "start": "2021-03-12T00:00:00Z",
  450. "end": "2021-03-15T00:00:00Z",
  451. "intervals": ["2021-03-12T00:00:00Z", "2021-03-13T00:00:00Z", "2021-03-14T00:00:00Z"],
  452. "groups": [
  453. {
  454. "by": {
  455. "outcome": "rate_limited",
  456. "category": "attachment",
  457. "reason": "spike_protection",
  458. },
  459. "totals": {"sum(quantity)": 1024, "sum(times_seen)": 1},
  460. "series": {"sum(quantity)": [0, 0, 1024], "sum(times_seen)": [0, 0, 1]},
  461. },
  462. {
  463. "by": {"outcome": "accepted", "reason": "none", "category": "error"},
  464. "totals": {"sum(quantity)": 6, "sum(times_seen)": 6},
  465. "series": {"sum(quantity)": [0, 0, 6], "sum(times_seen)": [0, 0, 6]},
  466. },
  467. {
  468. "by": {
  469. "category": "transaction",
  470. "reason": "spike_protection",
  471. "outcome": "rate_limited",
  472. },
  473. "totals": {"sum(quantity)": 1, "sum(times_seen)": 1},
  474. "series": {"sum(quantity)": [0, 0, 1], "sum(times_seen)": [0, 0, 1]},
  475. },
  476. ],
  477. }
  478. @freeze_time("2021-03-14T12:27:28.303Z")
  479. def test_org_group_by_project(self):
  480. make_request = functools.partial(
  481. self.client.get,
  482. reverse("sentry-api-0-organization-stats-v2", args=[self.org.slug]),
  483. )
  484. response = make_request(
  485. {
  486. "statsPeriod": "1d",
  487. "interval": "1d",
  488. "field": ["sum(times_seen)"],
  489. "groupBy": ["project"],
  490. "category": ["error", "transaction"],
  491. }
  492. )
  493. assert response.status_code == 200, response.content
  494. assert result_sorted(response.data) == {
  495. "start": "2021-03-13T00:00:00Z",
  496. "end": "2021-03-15T00:00:00Z",
  497. "groups": [
  498. {
  499. "by": {"project": self.project.id},
  500. "totals": {"sum(times_seen)": 6},
  501. },
  502. {
  503. "by": {"project": self.project2.id},
  504. "totals": {"sum(times_seen)": 1},
  505. },
  506. ],
  507. }
  508. @freeze_time("2021-03-14T12:27:28.303Z")
  509. def test_org_project_totals_per_project(self):
  510. make_request = functools.partial(
  511. self.client.get,
  512. reverse("sentry-api-0-organization-stats-v2", args=[self.org.slug]),
  513. )
  514. response_per_group = make_request(
  515. {
  516. "statsPeriod": "1d",
  517. "interval": "1h",
  518. "field": ["sum(times_seen)"],
  519. "groupBy": ["project"],
  520. "category": ["error", "transaction"],
  521. }
  522. )
  523. response_total = make_request(
  524. {
  525. "statsPeriod": "1d",
  526. "interval": "1h",
  527. "field": ["sum(times_seen)"],
  528. "category": ["error", "transaction"],
  529. }
  530. )
  531. per_group_total = 0
  532. for total in response_per_group.data["groups"]:
  533. per_group_total += total["totals"]["sum(times_seen)"]
  534. assert response_per_group.status_code == 200, response_per_group.content
  535. assert response_total.status_code == 200, response_total.content
  536. assert response_total.data["groups"][0]["totals"]["sum(times_seen)"] == per_group_total
  537. @freeze_time("2021-03-14T12:27:28.303Z")
  538. def test_project_filter(self):
  539. make_request = functools.partial(
  540. self.client.get,
  541. reverse("sentry-api-0-organization-stats-v2", args=[self.org.slug]),
  542. )
  543. response = make_request(
  544. {
  545. "project": self.project.id,
  546. "statsPeriod": "1d",
  547. "interval": "1d",
  548. "field": ["sum(quantity)"],
  549. "category": ["error", "transaction"],
  550. }
  551. )
  552. assert response.status_code == 200, response.content
  553. assert result_sorted(response.data) == {
  554. "start": "2021-03-13T00:00:00Z",
  555. "end": "2021-03-15T00:00:00Z",
  556. "intervals": ["2021-03-13T00:00:00Z", "2021-03-14T00:00:00Z"],
  557. "groups": [
  558. {"by": {}, "totals": {"sum(quantity)": 6}, "series": {"sum(quantity)": [0, 6]}}
  559. ],
  560. }
  561. @freeze_time("2021-03-14T12:27:28.303Z")
  562. def test_reason_filter(self):
  563. make_request = functools.partial(
  564. self.client.get,
  565. reverse("sentry-api-0-organization-stats-v2", args=[self.org.slug]),
  566. )
  567. response = make_request(
  568. {
  569. "statsPeriod": "1d",
  570. "interval": "1d",
  571. "field": ["sum(times_seen)"],
  572. "reason": ["spike_protection"],
  573. "groupBy": ["category"],
  574. }
  575. )
  576. assert response.status_code == 200, response.content
  577. assert result_sorted(response.data) == {
  578. "start": "2021-03-13T00:00:00Z",
  579. "end": "2021-03-15T00:00:00Z",
  580. "intervals": ["2021-03-13T00:00:00Z", "2021-03-14T00:00:00Z"],
  581. "groups": [
  582. {
  583. "by": {"category": "attachment"},
  584. "totals": {"sum(times_seen)": 1},
  585. "series": {"sum(times_seen)": [0, 1]},
  586. },
  587. {
  588. "by": {"category": "transaction"},
  589. "totals": {"sum(times_seen)": 1},
  590. "series": {"sum(times_seen)": [0, 1]},
  591. },
  592. ],
  593. }
  594. @freeze_time("2021-03-14T12:27:28.303Z")
  595. def test_outcome_filter(self):
  596. make_request = functools.partial(
  597. self.client.get,
  598. reverse("sentry-api-0-organization-stats-v2", args=[self.org.slug]),
  599. )
  600. response = make_request(
  601. {
  602. "statsPeriod": "1d",
  603. "interval": "1d",
  604. "field": ["sum(quantity)"],
  605. "outcome": "accepted",
  606. "category": ["error", "transaction"],
  607. }
  608. )
  609. assert response.status_code == 200, response.content
  610. assert result_sorted(response.data) == {
  611. "start": "2021-03-13T00:00:00Z",
  612. "end": "2021-03-15T00:00:00Z",
  613. "intervals": ["2021-03-13T00:00:00Z", "2021-03-14T00:00:00Z"],
  614. "groups": [
  615. {"by": {}, "totals": {"sum(quantity)": 6}, "series": {"sum(quantity)": [0, 6]}}
  616. ],
  617. }
  618. @freeze_time("2021-03-14T12:27:28.303Z")
  619. def test_category_filter(self):
  620. make_request = functools.partial(
  621. self.client.get,
  622. reverse("sentry-api-0-organization-stats-v2", args=[self.org.slug]),
  623. )
  624. response = make_request(
  625. {
  626. "statsPeriod": "1d",
  627. "interval": "1d",
  628. "field": ["sum(quantity)"],
  629. "category": "error",
  630. }
  631. )
  632. assert response.status_code == 200, response.content
  633. assert result_sorted(response.data) == {
  634. "start": "2021-03-13T00:00:00Z",
  635. "end": "2021-03-15T00:00:00Z",
  636. "intervals": ["2021-03-13T00:00:00Z", "2021-03-14T00:00:00Z"],
  637. "groups": [
  638. {"by": {}, "totals": {"sum(quantity)": 6}, "series": {"sum(quantity)": [0, 6]}}
  639. ],
  640. }
  641. @freeze_time("2021-03-14T12:27:28.303Z")
  642. def test_minute_interval(self):
  643. make_request = functools.partial(
  644. self.client.get,
  645. reverse("sentry-api-0-organization-stats-v2", args=[self.org.slug]),
  646. )
  647. response = make_request(
  648. {
  649. "statsPeriod": "1h",
  650. "interval": "15m",
  651. "field": ["sum(quantity)"],
  652. "category": "error",
  653. }
  654. )
  655. assert response.status_code == 200, response.content
  656. assert result_sorted(response.data) == {
  657. "start": "2021-03-14T11:15:00Z",
  658. "end": "2021-03-14T12:30:00Z",
  659. "intervals": [
  660. "2021-03-14T11:15:00Z",
  661. "2021-03-14T11:30:00Z",
  662. "2021-03-14T11:45:00Z",
  663. "2021-03-14T12:00:00Z",
  664. "2021-03-14T12:15:00Z",
  665. ],
  666. "groups": [
  667. {
  668. "by": {},
  669. "totals": {"sum(quantity)": 6},
  670. "series": {"sum(quantity)": [6, 0, 0, 0, 0]},
  671. }
  672. ],
  673. }
  674. def result_sorted(result):
  675. """sort the groups of the results array by the `by` object, ensuring a stable order"""
  676. def stable_dict(d):
  677. return tuple(sorted(d.items(), key=lambda t: t[0]))
  678. if "groups" in result:
  679. result["groups"].sort(key=lambda group: stable_dict(group["by"]))
  680. return result
  681. # TEST invalid parameter