test_organization_stats_v2.py 28 KB

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