test_organization_stats_v2.py 37 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111
  1. from datetime import datetime, timedelta, timezone
  2. import pytest
  3. from sentry.constants import DataCategory
  4. from sentry.sentry_metrics.use_case_id_registry import UseCaseID
  5. from sentry.testutils.cases import APITestCase, BaseMetricsLayerTestCase, OutcomesSnubaTest
  6. from sentry.testutils.helpers import with_feature
  7. from sentry.testutils.helpers.datetime import freeze_time
  8. from sentry.utils.outcomes import Outcome
  9. pytestmark = pytest.mark.sentry_metrics
  10. class OrganizationStatsTestV2(APITestCase, OutcomesSnubaTest):
  11. endpoint = "sentry-api-0-organization-stats-v2"
  12. def setUp(self):
  13. super().setUp()
  14. self.now = datetime(2021, 3, 14, 12, 27, 28, tzinfo=timezone.utc)
  15. self.login_as(user=self.user)
  16. self.org = self.organization
  17. self.org.flags.allow_joinleave = False
  18. self.org.save()
  19. self.org2 = self.create_organization()
  20. self.org3 = self.create_organization()
  21. self.project = self.create_project(
  22. name="bar", teams=[self.create_team(organization=self.org, members=[self.user])]
  23. )
  24. self.project2 = self.create_project(
  25. name="foo", teams=[self.create_team(organization=self.org, members=[self.user])]
  26. )
  27. self.project3 = self.create_project(organization=self.org2)
  28. self.user2 = self.create_user(is_superuser=False)
  29. self.create_member(user=self.user2, organization=self.organization, role="member", teams=[])
  30. self.create_member(user=self.user2, organization=self.org3, role="member", teams=[])
  31. self.project4 = self.create_project(
  32. name="users2sproj",
  33. teams=[self.create_team(organization=self.org, members=[self.user2])],
  34. )
  35. self.store_outcomes(
  36. {
  37. "org_id": self.org.id,
  38. "timestamp": self.now - timedelta(hours=1),
  39. "project_id": self.project.id,
  40. "outcome": Outcome.ACCEPTED,
  41. "reason": "none",
  42. "category": DataCategory.ERROR,
  43. "quantity": 1,
  44. },
  45. 5,
  46. )
  47. self.store_outcomes(
  48. {
  49. "org_id": self.org.id,
  50. "timestamp": self.now - timedelta(hours=1),
  51. "project_id": self.project.id,
  52. "outcome": Outcome.ACCEPTED,
  53. "reason": "none",
  54. "category": DataCategory.DEFAULT, # test that this shows up under error
  55. "quantity": 1,
  56. }
  57. )
  58. self.store_outcomes(
  59. {
  60. "org_id": self.org.id,
  61. "timestamp": self.now - timedelta(hours=1),
  62. "project_id": self.project.id,
  63. "outcome": Outcome.RATE_LIMITED,
  64. "reason": "smart_rate_limit",
  65. "category": DataCategory.ATTACHMENT,
  66. "quantity": 1024,
  67. }
  68. )
  69. self.store_outcomes(
  70. {
  71. "org_id": self.org.id,
  72. "timestamp": self.now - timedelta(hours=1),
  73. "project_id": self.project2.id,
  74. "outcome": Outcome.RATE_LIMITED,
  75. "reason": "smart_rate_limit",
  76. "category": DataCategory.TRANSACTION,
  77. "quantity": 1,
  78. }
  79. )
  80. def do_request(self, query, user=None, org=None, status_code=200):
  81. self.login_as(user=user or self.user)
  82. org_slug = (org or self.organization).slug
  83. if status_code >= 400:
  84. return self.get_error_response(org_slug, **query, status_code=status_code)
  85. return self.get_success_response(org_slug, **query, status_code=status_code)
  86. def test_empty_request(self):
  87. response = self.do_request({}, status_code=400)
  88. assert result_sorted(response.data) == {"detail": 'At least one "field" is required.'}
  89. def test_inaccessible_project(self):
  90. response = self.do_request({"project": [self.project3.id]}, status_code=403)
  91. assert result_sorted(response.data) == {
  92. "detail": "You do not have permission to perform this action."
  93. }
  94. def test_no_projects_available(self):
  95. response = self.do_request(
  96. {
  97. "groupBy": ["project"],
  98. "statsPeriod": "1d",
  99. "interval": "1d",
  100. "field": ["sum(quantity)"],
  101. "category": ["error", "transaction"],
  102. },
  103. user=self.user2,
  104. org=self.org3,
  105. status_code=400,
  106. )
  107. assert result_sorted(response.data) == {
  108. "detail": "No projects available",
  109. }
  110. def test_unknown_field(self):
  111. response = self.do_request(
  112. {
  113. "field": ["summ(qarntenty)"],
  114. "statsPeriod": "1d",
  115. "interval": "1d",
  116. },
  117. status_code=400,
  118. )
  119. assert result_sorted(response.data) == {
  120. "detail": 'Invalid field: "summ(qarntenty)"',
  121. }
  122. def test_no_end_param(self):
  123. response = self.do_request(
  124. {"field": ["sum(quantity)"], "interval": "1d", "start": "2021-03-14T00:00:00Z"},
  125. status_code=400,
  126. )
  127. assert result_sorted(response.data) == {"detail": "start and end are both required"}
  128. @freeze_time(datetime(2021, 3, 14, 12, 27, 28, tzinfo=timezone.utc))
  129. def test_future_request(self):
  130. response = self.do_request(
  131. {
  132. "field": ["sum(quantity)"],
  133. "interval": "1h",
  134. "category": ["error"],
  135. "start": "2021-03-14T15:30:00",
  136. "end": "2021-03-14T16:30:00",
  137. },
  138. status_code=200,
  139. )
  140. assert result_sorted(response.data) == {
  141. "intervals": [
  142. "2021-03-14T12:00:00Z",
  143. "2021-03-14T13:00:00Z",
  144. "2021-03-14T14:00:00Z",
  145. "2021-03-14T15:00:00Z",
  146. "2021-03-14T16:00:00Z",
  147. ],
  148. "groups": [
  149. {
  150. "by": {},
  151. "series": {"sum(quantity)": [0, 0, 0, 0, 0]},
  152. "totals": {"sum(quantity)": 0},
  153. }
  154. ],
  155. "start": "2021-03-14T12:00:00Z",
  156. "end": "2021-03-14T17:00:00Z",
  157. }
  158. def test_unknown_category(self):
  159. response = self.do_request(
  160. {
  161. "field": ["sum(quantity)"],
  162. "statsPeriod": "1d",
  163. "interval": "1d",
  164. "category": "scoobydoo",
  165. },
  166. status_code=400,
  167. )
  168. assert result_sorted(response.data) == {
  169. "detail": 'Invalid category: "scoobydoo"',
  170. }
  171. def test_unknown_outcome(self):
  172. response = self.do_request(
  173. {
  174. "field": ["sum(quantity)"],
  175. "statsPeriod": "1d",
  176. "interval": "1d",
  177. "category": "error",
  178. "outcome": "scoobydoo",
  179. },
  180. status_code=400,
  181. )
  182. assert result_sorted(response.data) == {
  183. "detail": 'Invalid outcome: "scoobydoo"',
  184. }
  185. def test_unknown_groupby(self):
  186. response = self.do_request(
  187. {
  188. "field": ["sum(quantity)"],
  189. "groupBy": ["category_"],
  190. "statsPeriod": "1d",
  191. "interval": "1d",
  192. },
  193. status_code=400,
  194. )
  195. assert result_sorted(response.data) == {"detail": 'Invalid groupBy: "category_"'}
  196. def test_resolution_invalid(self):
  197. self.do_request(
  198. {
  199. "statsPeriod": "1d",
  200. "interval": "bad_interval",
  201. },
  202. org=self.org,
  203. status_code=400,
  204. )
  205. @freeze_time("2021-03-14T12:27:28.303Z")
  206. def test_attachment_filter_only(self):
  207. response = self.do_request(
  208. {
  209. "project": [-1],
  210. "statsPeriod": "1d",
  211. "interval": "1d",
  212. "field": ["sum(quantity)"],
  213. "category": ["error", "attachment"],
  214. },
  215. status_code=400,
  216. )
  217. assert result_sorted(response.data) == {
  218. "detail": "if filtering by attachment no other category may be present"
  219. }
  220. @freeze_time("2021-03-14T12:27:28.303Z")
  221. def test_timeseries_interval(self):
  222. response = self.do_request(
  223. {
  224. "project": [-1],
  225. "category": ["error"],
  226. "statsPeriod": "1d",
  227. "interval": "1d",
  228. "field": ["sum(quantity)"],
  229. },
  230. status_code=200,
  231. )
  232. assert result_sorted(response.data) == {
  233. "intervals": ["2021-03-13T00:00:00Z", "2021-03-14T00:00:00Z"],
  234. "groups": [
  235. {"by": {}, "series": {"sum(quantity)": [0, 6]}, "totals": {"sum(quantity)": 6}}
  236. ],
  237. "start": "2021-03-13T00:00:00Z",
  238. "end": "2021-03-15T00:00:00Z",
  239. }
  240. response = self.do_request(
  241. {
  242. "project": [-1],
  243. "statsPeriod": "1d",
  244. "interval": "6h",
  245. "field": ["sum(quantity)"],
  246. "category": ["error"],
  247. },
  248. status_code=200,
  249. )
  250. assert result_sorted(response.data) == {
  251. "intervals": [
  252. "2021-03-13T12:00:00Z",
  253. "2021-03-13T18:00:00Z",
  254. "2021-03-14T00:00:00Z",
  255. "2021-03-14T06:00:00Z",
  256. "2021-03-14T12:00:00Z",
  257. ],
  258. "groups": [
  259. {
  260. "by": {},
  261. "series": {"sum(quantity)": [0, 0, 0, 6, 0]},
  262. "totals": {"sum(quantity)": 6},
  263. }
  264. ],
  265. "start": "2021-03-13T12:00:00Z",
  266. "end": "2021-03-14T18:00:00Z",
  267. }
  268. @freeze_time("2021-03-14T12:27:28.303Z")
  269. def test_user_org_total_all_accessible(self):
  270. response = self.do_request(
  271. {
  272. "project": [-1],
  273. "statsPeriod": "1d",
  274. "interval": "1d",
  275. "field": ["sum(quantity)"],
  276. "category": ["error", "transaction"],
  277. },
  278. user=self.user2,
  279. status_code=200,
  280. )
  281. assert result_sorted(response.data) == {
  282. "start": "2021-03-13T00:00:00Z",
  283. "end": "2021-03-15T00:00:00Z",
  284. "intervals": ["2021-03-13T00:00:00Z", "2021-03-14T00:00:00Z"],
  285. "groups": [
  286. {"by": {}, "series": {"sum(quantity)": [0, 7]}, "totals": {"sum(quantity)": 7}}
  287. ],
  288. }
  289. @freeze_time("2021-03-14T12:27:28.303Z")
  290. def test_user_no_proj_specific_access(self):
  291. response = self.do_request(
  292. {
  293. "project": self.project.id,
  294. "statsPeriod": "1d",
  295. "interval": "1d",
  296. "field": ["sum(quantity)"],
  297. "category": ["error", "transaction"],
  298. },
  299. user=self.user2,
  300. status_code=403,
  301. )
  302. response = self.do_request(
  303. {
  304. "project": [-1],
  305. "statsPeriod": "1d",
  306. "interval": "1d",
  307. "field": ["sum(quantity)"],
  308. "category": ["error", "transaction"],
  309. "groupBy": ["project"],
  310. },
  311. user=self.user2,
  312. status_code=200,
  313. )
  314. assert result_sorted(response.data) == {
  315. "start": "2021-03-13T00:00:00Z",
  316. "end": "2021-03-15T00:00:00Z",
  317. "groups": [],
  318. }
  319. @freeze_time("2021-03-14T12:27:28.303Z")
  320. def test_no_project_access(self):
  321. user = self.create_user(is_superuser=False)
  322. self.create_member(user=user, organization=self.organization, role="member", teams=[])
  323. response = self.do_request(
  324. {
  325. "project": [self.project.id],
  326. "statsPeriod": "1d",
  327. "interval": "1d",
  328. "category": ["error", "transaction"],
  329. "field": ["sum(quantity)"],
  330. },
  331. org=self.organization,
  332. user=user,
  333. status_code=403,
  334. )
  335. assert result_sorted(response.data) == {
  336. "detail": "You do not have permission to perform this action."
  337. }
  338. response = self.do_request(
  339. {
  340. "project": [self.project.id],
  341. "groupBy": ["project"],
  342. "statsPeriod": "1d",
  343. "interval": "1d",
  344. "category": ["error", "transaction"],
  345. "field": ["sum(quantity)"],
  346. },
  347. org=self.organization,
  348. user=user,
  349. status_code=403,
  350. )
  351. assert result_sorted(response.data) == {
  352. "detail": "You do not have permission to perform this action."
  353. }
  354. @freeze_time("2021-03-14T12:27:28.303Z")
  355. def test_open_membership_semantics(self):
  356. self.org.flags.allow_joinleave = True
  357. self.org.save()
  358. response = self.do_request(
  359. {
  360. "project": [-1],
  361. "statsPeriod": "1d",
  362. "interval": "1d",
  363. "field": ["sum(quantity)"],
  364. "category": ["error", "transaction"],
  365. "groupBy": ["project"],
  366. },
  367. user=self.user2,
  368. status_code=200,
  369. )
  370. assert result_sorted(response.data) == {
  371. "start": "2021-03-13T00:00:00Z",
  372. "end": "2021-03-15T00:00:00Z",
  373. "groups": [
  374. {
  375. "by": {"project": self.project.id},
  376. "totals": {"sum(quantity)": 6},
  377. },
  378. {
  379. "by": {"project": self.project2.id},
  380. "totals": {"sum(quantity)": 1},
  381. },
  382. ],
  383. }
  384. @freeze_time("2021-03-14T12:27:28.303Z")
  385. def test_org_simple(self):
  386. response = self.do_request(
  387. {
  388. "statsPeriod": "2d",
  389. "interval": "1d",
  390. "field": ["sum(quantity)"],
  391. "groupBy": ["category", "outcome", "reason"],
  392. },
  393. org=self.org,
  394. status_code=200,
  395. )
  396. assert result_sorted(response.data) == {
  397. "start": "2021-03-12T00:00:00Z",
  398. "end": "2021-03-15T00:00:00Z",
  399. "intervals": ["2021-03-12T00:00:00Z", "2021-03-13T00:00:00Z", "2021-03-14T00:00:00Z"],
  400. "groups": [
  401. {
  402. "by": {
  403. "outcome": "rate_limited",
  404. "reason": "spike_protection",
  405. "category": "attachment",
  406. },
  407. "totals": {"sum(quantity)": 1024},
  408. "series": {"sum(quantity)": [0, 0, 1024]},
  409. },
  410. {
  411. "by": {"outcome": "accepted", "reason": "none", "category": "error"},
  412. "totals": {"sum(quantity)": 6},
  413. "series": {"sum(quantity)": [0, 0, 6]},
  414. },
  415. {
  416. "by": {
  417. "category": "transaction",
  418. "reason": "spike_protection",
  419. "outcome": "rate_limited",
  420. },
  421. "totals": {"sum(quantity)": 1},
  422. "series": {"sum(quantity)": [0, 0, 1]},
  423. },
  424. ],
  425. }
  426. @freeze_time("2021-03-14T12:27:28.303Z")
  427. def test_staff_org_individual_category(self):
  428. staff_user = self.create_user(is_staff=True, is_superuser=True)
  429. self.login_as(user=staff_user, superuser=True)
  430. category_group_mapping = {
  431. "attachment": {
  432. "by": {
  433. "outcome": "rate_limited",
  434. "reason": "spike_protection",
  435. },
  436. "totals": {"sum(quantity)": 1024},
  437. "series": {"sum(quantity)": [0, 0, 1024]},
  438. },
  439. "error": {
  440. "by": {"outcome": "accepted", "reason": "none"},
  441. "totals": {"sum(quantity)": 6},
  442. "series": {"sum(quantity)": [0, 0, 6]},
  443. },
  444. "transaction": {
  445. "by": {
  446. "reason": "spike_protection",
  447. "outcome": "rate_limited",
  448. },
  449. "totals": {"sum(quantity)": 1},
  450. "series": {"sum(quantity)": [0, 0, 1]},
  451. },
  452. }
  453. # Test each category individually
  454. for category in ["attachment", "error", "transaction"]:
  455. response = self.do_request(
  456. {
  457. "category": category,
  458. "statsPeriod": "2d",
  459. "interval": "1d",
  460. "field": ["sum(quantity)"],
  461. "groupBy": ["outcome", "reason"],
  462. },
  463. org=self.org,
  464. status_code=200,
  465. )
  466. assert result_sorted(response.data) == {
  467. "start": "2021-03-12T00:00:00Z",
  468. "end": "2021-03-15T00:00:00Z",
  469. "intervals": [
  470. "2021-03-12T00:00:00Z",
  471. "2021-03-13T00:00:00Z",
  472. "2021-03-14T00:00:00Z",
  473. ],
  474. "groups": [category_group_mapping[category]],
  475. }
  476. @freeze_time("2021-03-14T12:27:28.303Z")
  477. def test_org_multiple_fields(self):
  478. response = self.do_request(
  479. {
  480. "statsPeriod": "2d",
  481. "interval": "1d",
  482. "field": ["sum(quantity)", "sum(times_seen)"],
  483. "groupBy": ["category", "outcome", "reason"],
  484. },
  485. org=self.org,
  486. status_code=200,
  487. )
  488. assert result_sorted(response.data) == {
  489. "start": "2021-03-12T00:00:00Z",
  490. "end": "2021-03-15T00:00:00Z",
  491. "intervals": ["2021-03-12T00:00:00Z", "2021-03-13T00:00:00Z", "2021-03-14T00:00:00Z"],
  492. "groups": [
  493. {
  494. "by": {
  495. "outcome": "rate_limited",
  496. "category": "attachment",
  497. "reason": "spike_protection",
  498. },
  499. "totals": {"sum(quantity)": 1024, "sum(times_seen)": 1},
  500. "series": {"sum(quantity)": [0, 0, 1024], "sum(times_seen)": [0, 0, 1]},
  501. },
  502. {
  503. "by": {"outcome": "accepted", "reason": "none", "category": "error"},
  504. "totals": {"sum(quantity)": 6, "sum(times_seen)": 6},
  505. "series": {"sum(quantity)": [0, 0, 6], "sum(times_seen)": [0, 0, 6]},
  506. },
  507. {
  508. "by": {
  509. "category": "transaction",
  510. "reason": "spike_protection",
  511. "outcome": "rate_limited",
  512. },
  513. "totals": {"sum(quantity)": 1, "sum(times_seen)": 1},
  514. "series": {"sum(quantity)": [0, 0, 1], "sum(times_seen)": [0, 0, 1]},
  515. },
  516. ],
  517. }
  518. @freeze_time("2021-03-14T12:27:28.303Z")
  519. def test_org_group_by_project(self):
  520. response = self.do_request(
  521. {
  522. "statsPeriod": "1d",
  523. "interval": "1d",
  524. "field": ["sum(times_seen)"],
  525. "groupBy": ["project"],
  526. "category": ["error", "transaction"],
  527. },
  528. org=self.org,
  529. status_code=200,
  530. )
  531. assert result_sorted(response.data) == {
  532. "start": "2021-03-13T00:00:00Z",
  533. "end": "2021-03-15T00:00:00Z",
  534. "groups": [
  535. {
  536. "by": {"project": self.project.id},
  537. "totals": {"sum(times_seen)": 6},
  538. },
  539. {
  540. "by": {"project": self.project2.id},
  541. "totals": {"sum(times_seen)": 1},
  542. },
  543. ],
  544. }
  545. @freeze_time("2021-03-14T12:27:28.303Z")
  546. def test_org_project_totals_per_project(self):
  547. response_per_group = self.do_request(
  548. {
  549. "statsPeriod": "1d",
  550. "interval": "1h",
  551. "field": ["sum(times_seen)"],
  552. "groupBy": ["project"],
  553. "category": ["error", "transaction"],
  554. },
  555. org=self.org,
  556. status_code=200,
  557. )
  558. response_total = self.do_request(
  559. {
  560. "statsPeriod": "1d",
  561. "interval": "1h",
  562. "field": ["sum(times_seen)"],
  563. "category": ["error", "transaction"],
  564. },
  565. org=self.org,
  566. status_code=200,
  567. )
  568. per_group_total = 0
  569. for total in response_per_group.data["groups"]:
  570. per_group_total += total["totals"]["sum(times_seen)"]
  571. assert response_per_group.status_code == 200, response_per_group.content
  572. assert response_total.status_code == 200, response_total.content
  573. assert response_total.data["groups"][0]["totals"]["sum(times_seen)"] == per_group_total
  574. @freeze_time("2021-03-14T12:27:28.303Z")
  575. def test_project_filter(self):
  576. response = self.do_request(
  577. {
  578. "project": self.project.id,
  579. "statsPeriod": "1d",
  580. "interval": "1d",
  581. "field": ["sum(quantity)"],
  582. "category": ["error", "transaction"],
  583. },
  584. org=self.org,
  585. status_code=200,
  586. )
  587. assert result_sorted(response.data) == {
  588. "start": "2021-03-13T00:00:00Z",
  589. "end": "2021-03-15T00:00:00Z",
  590. "intervals": ["2021-03-13T00:00:00Z", "2021-03-14T00:00:00Z"],
  591. "groups": [
  592. {"by": {}, "totals": {"sum(quantity)": 6}, "series": {"sum(quantity)": [0, 6]}}
  593. ],
  594. }
  595. @freeze_time("2021-03-14T12:27:28.303Z")
  596. def test_staff_project_filter(self):
  597. staff_user = self.create_user(is_staff=True, is_superuser=True)
  598. self.login_as(user=staff_user, superuser=True)
  599. shared_query_params = {
  600. "field": "sum(quantity)",
  601. "groupBy": ["outcome", "reason"],
  602. "interval": "1d",
  603. "statsPeriod": "1d",
  604. }
  605. shared_data = {
  606. "start": "2021-03-13T00:00:00Z",
  607. "end": "2021-03-15T00:00:00Z",
  608. "intervals": ["2021-03-13T00:00:00Z", "2021-03-14T00:00:00Z"],
  609. }
  610. # Test error category
  611. response = self.do_request(
  612. {
  613. **shared_query_params,
  614. "category": "error",
  615. "project": self.project.id,
  616. },
  617. org=self.org,
  618. status_code=200,
  619. )
  620. assert result_sorted(response.data) == {
  621. **shared_data,
  622. "groups": [
  623. {
  624. "by": {"outcome": "accepted", "reason": "none"},
  625. "totals": {"sum(quantity)": 6},
  626. "series": {"sum(quantity)": [0, 6]},
  627. },
  628. ],
  629. }
  630. # Test transaction category
  631. response = self.do_request(
  632. {
  633. **shared_query_params,
  634. "category": "transaction",
  635. "project": self.project2.id,
  636. },
  637. org=self.org,
  638. status_code=200,
  639. )
  640. assert result_sorted(response.data) == {
  641. **shared_data,
  642. "groups": [
  643. {
  644. "by": {"outcome": "rate_limited", "reason": "spike_protection"},
  645. "totals": {"sum(quantity)": 1},
  646. "series": {"sum(quantity)": [0, 1]},
  647. }
  648. ],
  649. }
  650. @freeze_time("2021-03-14T12:27:28.303Z")
  651. def test_reason_filter(self):
  652. response = self.do_request(
  653. {
  654. "statsPeriod": "1d",
  655. "interval": "1d",
  656. "field": ["sum(times_seen)"],
  657. "reason": ["spike_protection"],
  658. "groupBy": ["category"],
  659. },
  660. org=self.org,
  661. status_code=200,
  662. )
  663. assert result_sorted(response.data) == {
  664. "start": "2021-03-13T00:00:00Z",
  665. "end": "2021-03-15T00:00:00Z",
  666. "intervals": ["2021-03-13T00:00:00Z", "2021-03-14T00:00:00Z"],
  667. "groups": [
  668. {
  669. "by": {"category": "attachment"},
  670. "totals": {"sum(times_seen)": 1},
  671. "series": {"sum(times_seen)": [0, 1]},
  672. },
  673. {
  674. "by": {"category": "transaction"},
  675. "totals": {"sum(times_seen)": 1},
  676. "series": {"sum(times_seen)": [0, 1]},
  677. },
  678. ],
  679. }
  680. @freeze_time("2021-03-14T12:27:28.303Z")
  681. def test_outcome_filter(self):
  682. response = self.do_request(
  683. {
  684. "statsPeriod": "1d",
  685. "interval": "1d",
  686. "field": ["sum(quantity)"],
  687. "outcome": "accepted",
  688. "category": ["error", "transaction"],
  689. },
  690. org=self.org,
  691. status_code=200,
  692. )
  693. assert result_sorted(response.data) == {
  694. "start": "2021-03-13T00:00:00Z",
  695. "end": "2021-03-15T00:00:00Z",
  696. "intervals": ["2021-03-13T00:00:00Z", "2021-03-14T00:00:00Z"],
  697. "groups": [
  698. {"by": {}, "totals": {"sum(quantity)": 6}, "series": {"sum(quantity)": [0, 6]}}
  699. ],
  700. }
  701. @freeze_time("2021-03-14T12:27:28.303Z")
  702. def test_category_filter(self):
  703. response = self.do_request(
  704. {
  705. "statsPeriod": "1d",
  706. "interval": "1d",
  707. "field": ["sum(quantity)"],
  708. "category": "error",
  709. },
  710. org=self.org,
  711. status_code=200,
  712. )
  713. assert result_sorted(response.data) == {
  714. "start": "2021-03-13T00:00:00Z",
  715. "end": "2021-03-15T00:00:00Z",
  716. "intervals": ["2021-03-13T00:00:00Z", "2021-03-14T00:00:00Z"],
  717. "groups": [
  718. {"by": {}, "totals": {"sum(quantity)": 6}, "series": {"sum(quantity)": [0, 6]}}
  719. ],
  720. }
  721. @freeze_time("2021-03-14T12:27:28.303Z")
  722. def test_minute_interval_sum_quantity(self):
  723. response = self.do_request(
  724. {
  725. "statsPeriod": "1h",
  726. "interval": "15m",
  727. "field": ["sum(quantity)"],
  728. "category": "error",
  729. },
  730. org=self.org,
  731. status_code=200,
  732. )
  733. assert result_sorted(response.data) == {
  734. "start": "2021-03-14T11:15:00Z",
  735. "end": "2021-03-14T12:30:00Z",
  736. "intervals": [
  737. "2021-03-14T11:15:00Z",
  738. "2021-03-14T11:30:00Z",
  739. "2021-03-14T11:45:00Z",
  740. "2021-03-14T12:00:00Z",
  741. "2021-03-14T12:15:00Z",
  742. ],
  743. "groups": [
  744. {
  745. "by": {},
  746. "totals": {"sum(quantity)": 6},
  747. "series": {"sum(quantity)": [6, 0, 0, 0, 0]},
  748. }
  749. ],
  750. }
  751. @freeze_time("2021-03-14T12:27:28.303Z")
  752. def test_minute_interval_sum_times_seen(self):
  753. response = self.do_request(
  754. {
  755. "statsPeriod": "1h",
  756. "interval": "15m",
  757. "field": ["sum(times_seen)"],
  758. "category": "error",
  759. }
  760. )
  761. assert response.status_code == 200, response.content
  762. assert result_sorted(response.data) == {
  763. "start": "2021-03-14T11:15:00Z",
  764. "end": "2021-03-14T12:30:00Z",
  765. "intervals": [
  766. "2021-03-14T11:15:00Z",
  767. "2021-03-14T11:30:00Z",
  768. "2021-03-14T11:45:00Z",
  769. "2021-03-14T12:00:00Z",
  770. "2021-03-14T12:15:00Z",
  771. ],
  772. "groups": [
  773. {
  774. "by": {},
  775. "totals": {"sum(times_seen)": 6},
  776. "series": {"sum(times_seen)": [6, 0, 0, 0, 0]},
  777. }
  778. ],
  779. }
  780. def result_sorted(result):
  781. """sort the groups of the results array by the `by` object, ensuring a stable order"""
  782. def stable_dict(d):
  783. return tuple(sorted(d.items(), key=lambda t: t[0]))
  784. if "groups" in result:
  785. result["groups"].sort(key=lambda group: stable_dict(group["by"]))
  786. return result
  787. # TEST invalid parameter
  788. class OrganizationStatsMetricsTestV2(APITestCase, BaseMetricsLayerTestCase):
  789. endpoint = "sentry-api-0-organization-stats-v2"
  790. @property
  791. def now(self):
  792. return datetime(2021, 3, 14, 12, 27, 28, tzinfo=timezone.utc)
  793. def ts(self, dt: datetime) -> int:
  794. return int(dt.timestamp())
  795. def do_request(self, query, user=None, org=None, status_code=200):
  796. self.login_as(user=user or self.user)
  797. org_slug = (org or self.organization).slug
  798. if status_code >= 400:
  799. return self.get_error_response(org_slug, **query, status_code=status_code)
  800. return self.get_success_response(org_slug, **query, status_code=status_code)
  801. def setUp(self):
  802. super().setUp()
  803. self.login_as(user=self.user)
  804. self.org = self.organization
  805. self.org.flags.allow_joinleave = False
  806. self.org.save()
  807. self.org2 = self.create_organization()
  808. self.org3 = self.create_organization()
  809. self.project = self.create_project(
  810. name="bar", teams=[self.create_team(organization=self.org, members=[self.user])]
  811. )
  812. self.project2 = self.create_project(
  813. name="foo", teams=[self.create_team(organization=self.org, members=[self.user])]
  814. )
  815. self.project3 = self.create_project(organization=self.org2)
  816. self.user2 = self.create_user(is_superuser=False)
  817. self.create_member(user=self.user2, organization=self.organization, role="member", teams=[])
  818. self.create_member(user=self.user2, organization=self.org3, role="member", teams=[])
  819. self.project4 = self.create_project(
  820. name="users2sproj",
  821. teams=[self.create_team(organization=self.org, members=[self.user2])],
  822. )
  823. self.store_metric(
  824. org_id=self.org.id,
  825. project_id=self.project.id,
  826. type="counter",
  827. name="c:metric_stats/volume@none",
  828. timestamp=self.ts(self.now - timedelta(hours=1)),
  829. use_case_id=UseCaseID.METRIC_STATS,
  830. tags={"mri": "mri.foo", "outcome.id": str(Outcome.ACCEPTED)},
  831. value=1,
  832. )
  833. self.store_metric(
  834. org_id=self.org.id,
  835. project_id=self.project2.id,
  836. type="counter",
  837. name="c:metric_stats/volume@none",
  838. timestamp=self.ts(self.now - timedelta(hours=1)),
  839. use_case_id=UseCaseID.METRIC_STATS,
  840. tags={"mri": "mri.foo", "outcome.id": str(Outcome.ACCEPTED)},
  841. value=1,
  842. )
  843. self.store_metric(
  844. org_id=self.org.id,
  845. project_id=self.project2.id,
  846. type="counter",
  847. name="c:metric_stats/volume@none",
  848. timestamp=self.ts(self.now - timedelta(hours=1)),
  849. use_case_id=UseCaseID.METRIC_STATS,
  850. tags={"mri": "mri.bar", "outcome.id": str(Outcome.FILTERED)},
  851. value=1,
  852. )
  853. self.store_metric(
  854. org_id=self.org.id,
  855. project_id=self.project.id,
  856. type="gauge",
  857. name="g:metric_stats/cardinality@none",
  858. timestamp=self.ts(self.now - timedelta(hours=1)),
  859. use_case_id=UseCaseID.METRIC_STATS,
  860. tags={"mri": "", "cardinality.window": "60"},
  861. value=1,
  862. )
  863. self.store_metric(
  864. org_id=self.org.id,
  865. project_id=self.project2.id,
  866. type="gauge",
  867. name="g:metric_stats/cardinality@none",
  868. timestamp=self.ts(self.now - timedelta(hours=1)),
  869. use_case_id=UseCaseID.METRIC_STATS,
  870. tags={"mri": "", "cardinality.window": "60"},
  871. value=2,
  872. )
  873. self.store_metric(
  874. org_id=self.org.id,
  875. project_id=self.project2.id,
  876. type="gauge",
  877. name="g:metric_stats/cardinality@none",
  878. timestamp=self.ts(self.now - timedelta(hours=1)),
  879. use_case_id=UseCaseID.METRIC_STATS,
  880. tags={"mri": "", "cardinality.window": "60"},
  881. value=3,
  882. )
  883. @freeze_time("2021-03-14T12:27:28.303Z")
  884. @with_feature("organizations:custom-metrics")
  885. def test_metrics_category(self):
  886. response = self.do_request(
  887. {
  888. "project": [-1],
  889. "category": ["metricOutcomes"],
  890. "statsPeriod": "1d",
  891. "interval": "1d",
  892. "field": ["sum(quantity)"],
  893. },
  894. status_code=200,
  895. )
  896. assert result_sorted(response.data) == {
  897. "intervals": ["2021-03-13T00:00:00Z", "2021-03-14T00:00:00Z"],
  898. "groups": [
  899. {"by": {}, "series": {"sum(quantity)": [0, 3]}, "totals": {"sum(quantity)": 3}}
  900. ],
  901. "start": "2021-03-13T00:00:00Z",
  902. "end": "2021-03-15T00:00:00Z",
  903. }
  904. @freeze_time("2021-03-14T12:27:28.303Z")
  905. @with_feature("organizations:custom-metrics")
  906. def test_metrics_group_by_project(self):
  907. response = self.do_request(
  908. {
  909. "project": [-1],
  910. "category": ["metricOutcomes"],
  911. "groupBy": ["project"],
  912. "statsPeriod": "1d",
  913. "interval": "1d",
  914. "field": ["sum(quantity)"],
  915. },
  916. status_code=200,
  917. )
  918. assert result_sorted(response.data) == {
  919. "intervals": ["2021-03-13T00:00:00Z", "2021-03-14T00:00:00Z"],
  920. "groups": [
  921. {
  922. "by": {"project": self.project.id},
  923. "series": {"sum(quantity)": [0, 1]},
  924. "totals": {"sum(quantity)": 1},
  925. },
  926. {
  927. "by": {"project": self.project2.id},
  928. "series": {"sum(quantity)": [0, 2]},
  929. "totals": {"sum(quantity)": 2},
  930. },
  931. ],
  932. "start": "2021-03-13T00:00:00Z",
  933. "end": "2021-03-15T00:00:00Z",
  934. }
  935. @freeze_time("2021-03-14T12:27:28.303Z")
  936. @with_feature("organizations:custom-metrics")
  937. def test_metrics_multiple_group_by(self):
  938. response = self.do_request(
  939. {
  940. "project": [-1],
  941. "category": ["metricOutcomes"],
  942. "groupBy": ["project", "outcome"],
  943. "statsPeriod": "1d",
  944. "interval": "1d",
  945. "field": ["sum(quantity)"],
  946. },
  947. status_code=200,
  948. )
  949. assert result_sorted(response.data) == {
  950. "end": "2021-03-15T00:00:00Z",
  951. "groups": [
  952. {
  953. "by": {"outcome": "accepted", "project": self.project.id},
  954. "series": {"sum(quantity)": [0, 1]},
  955. "totals": {"sum(quantity)": 1},
  956. },
  957. {
  958. "by": {"outcome": "accepted", "project": self.project2.id},
  959. "series": {"sum(quantity)": [0, 1]},
  960. "totals": {"sum(quantity)": 1},
  961. },
  962. {
  963. "by": {"outcome": "filtered", "project": self.project2.id},
  964. "series": {"sum(quantity)": [0, 1]},
  965. "totals": {"sum(quantity)": 1},
  966. },
  967. ],
  968. "intervals": ["2021-03-13T00:00:00Z", "2021-03-14T00:00:00Z"],
  969. "start": "2021-03-13T00:00:00Z",
  970. }
  971. @freeze_time("2021-03-14T12:27:28.303Z")
  972. @with_feature("organizations:custom-metrics")
  973. def test_metric_hour(self):
  974. response = self.do_request(
  975. {
  976. "project": [-1],
  977. "category": ["metricHour"],
  978. "groupBy": ["project"],
  979. "statsPeriod": "1h",
  980. "interval": "1h",
  981. "field": ["sum(quantity)"],
  982. },
  983. status_code=200,
  984. )
  985. assert result_sorted(response.data) == {
  986. "end": "2021-03-14T13:00:00Z",
  987. "groups": [
  988. {
  989. "by": {"project": self.project.id},
  990. "series": {"sum(quantity)": [1, 0]},
  991. "totals": {"sum(quantity)": 1},
  992. },
  993. {
  994. "by": {"project": self.project2.id},
  995. "series": {"sum(quantity)": [3, 0]},
  996. "totals": {"sum(quantity)": 3},
  997. },
  998. ],
  999. "intervals": ["2021-03-14T11:00:00Z", "2021-03-14T12:00:00Z"],
  1000. "start": "2021-03-14T11:00:00Z",
  1001. }