test_organization_stats_v2.py 28 KB

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