test_organization_stats_v2.py 31 KB

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