test_organization_sessions.py 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765
  1. import datetime
  2. from unittest.mock import patch
  3. from uuid import uuid4
  4. import pytest
  5. import pytz
  6. from django.urls import reverse
  7. from freezegun import freeze_time
  8. from sentry.release_health.metrics import MetricsReleaseHealthBackend
  9. from sentry.testutils import APITestCase, SnubaTestCase
  10. from sentry.testutils.cases import SessionMetricsTestCase
  11. from sentry.utils.dates import to_timestamp
  12. def result_sorted(result):
  13. """sort the groups of the results array by the `by` object, ensuring a stable order"""
  14. def stable_dict(d):
  15. return tuple(sorted(d.items(), key=lambda t: t[0]))
  16. result["groups"].sort(key=lambda group: stable_dict(group["by"]))
  17. return result
  18. class OrganizationSessionsEndpointTest(APITestCase, SnubaTestCase):
  19. def setUp(self):
  20. super().setUp()
  21. self.setup_fixture()
  22. def setup_fixture(self):
  23. self.timestamp = to_timestamp(datetime.datetime(2021, 1, 14, 12, 27, 28, tzinfo=pytz.utc))
  24. self.received = self.timestamp
  25. self.session_started = self.timestamp // 3600 * 3600 # round to the hour
  26. self.organization1 = self.organization
  27. self.organization2 = self.create_organization()
  28. self.organization3 = self.create_organization()
  29. self.project1 = self.project
  30. self.project2 = self.create_project()
  31. self.project3 = self.create_project()
  32. self.project4 = self.create_project(organization=self.organization2)
  33. self.user2 = self.create_user(is_superuser=False)
  34. self.create_member(
  35. user=self.user2, organization=self.organization1, role="member", teams=[]
  36. )
  37. self.create_member(user=self.user, organization=self.organization3, role="admin", teams=[])
  38. self.create_environment(self.project2, name="development")
  39. template = {
  40. "distinct_id": "00000000-0000-0000-0000-000000000000",
  41. "status": "exited",
  42. "seq": 0,
  43. "release": "foo@1.0.0",
  44. "environment": "production",
  45. "retention_days": 90,
  46. "duration": 123.4,
  47. "errors": 0,
  48. "started": self.session_started,
  49. "received": self.received,
  50. }
  51. def make_duration(kwargs):
  52. """Randomish but deterministic duration"""
  53. return float(len(str(kwargs)))
  54. def make_session(project, **kwargs):
  55. return dict(
  56. template,
  57. session_id=uuid4().hex,
  58. org_id=project.organization_id,
  59. project_id=project.id,
  60. duration=make_duration(kwargs),
  61. **kwargs,
  62. )
  63. self.store_session(make_session(self.project1, started=self.session_started + 12 * 60))
  64. self.store_session(
  65. make_session(self.project1, started=self.session_started + 24 * 60, release="foo@1.1.0")
  66. )
  67. self.store_session(make_session(self.project1, started=self.session_started - 60 * 60))
  68. self.store_session(make_session(self.project1, started=self.session_started - 12 * 60 * 60))
  69. self.store_session(make_session(self.project2, status="crashed"))
  70. self.store_session(make_session(self.project2, environment="development"))
  71. self.store_session(make_session(self.project3, errors=1, release="foo@1.2.0"))
  72. self.store_session(
  73. make_session(
  74. self.project3,
  75. distinct_id="39887d89-13b2-4c84-8c23-5d13d2102664",
  76. started=self.session_started - 60 * 60,
  77. )
  78. )
  79. self.store_session(
  80. make_session(
  81. self.project3, distinct_id="39887d89-13b2-4c84-8c23-5d13d2102664", errors=1
  82. )
  83. )
  84. self.store_session(make_session(self.project4))
  85. def do_request(self, query, user=None, org=None):
  86. self.login_as(user=user or self.user)
  87. url = reverse(
  88. "sentry-api-0-organization-sessions",
  89. kwargs={"organization_slug": (org or self.organization).slug},
  90. )
  91. return self.client.get(url, query, format="json")
  92. def test_empty_request(self):
  93. response = self.do_request({})
  94. assert response.status_code == 400, response.content
  95. assert response.data == {"detail": 'Request is missing a "field"'}
  96. def test_inaccessible_project(self):
  97. response = self.do_request({"project": [self.project4.id]})
  98. assert response.status_code == 403, response.content
  99. assert response.data == {"detail": "You do not have permission to perform this action."}
  100. def test_unknown_field(self):
  101. response = self.do_request({"field": ["summ(sessin)"]})
  102. assert response.status_code == 400, response.content
  103. assert response.data == {"detail": 'Invalid field: "summ(sessin)"'}
  104. def test_unknown_groupby(self):
  105. response = self.do_request({"field": ["sum(session)"], "groupBy": ["envriomnent"]})
  106. assert response.status_code == 400, response.content
  107. assert response.data == {"detail": 'Invalid groupBy: "envriomnent"'}
  108. def test_illegal_groupby(self):
  109. response = self.do_request({"field": ["sum(session)"], "groupBy": ["issue.id"]})
  110. assert response.status_code == 400, response.content
  111. assert response.data == {"detail": 'Invalid groupBy: "issue.id"'}
  112. def test_invalid_query(self):
  113. response = self.do_request(
  114. {"statsPeriod": "1d", "field": ["sum(session)"], "query": ["foo:bar"]}
  115. )
  116. assert response.status_code == 400, response.content
  117. assert response.data == {"detail": 'Invalid query field: "foo"'}
  118. response = self.do_request(
  119. {
  120. "statsPeriod": "1d",
  121. "field": ["sum(session)"],
  122. "query": ["release:foo-bar@1.2.3 (123)"],
  123. }
  124. )
  125. assert response.status_code == 400, response.content
  126. # TODO: it would be good to provide a better error here,
  127. # since its not obvious where `message` comes from.
  128. assert response.data == {"detail": 'Invalid query field: "message"'}
  129. def test_illegal_query(self):
  130. response = self.do_request(
  131. {"statsPeriod": "1d", "field": ["sum(session)"], "query": ["issue.id:123"]}
  132. )
  133. assert response.status_code == 400, response.content
  134. assert response.data == {"detail": 'Invalid query field: "group_id"'}
  135. def test_too_many_points(self):
  136. # default statsPeriod is 90d
  137. response = self.do_request({"field": ["sum(session)"], "interval": "1h"})
  138. assert response.status_code == 400, response.content
  139. assert response.data == {
  140. "detail": "Your interval and date range would create too many results. "
  141. "Use a larger interval, or a smaller date range."
  142. }
  143. @freeze_time("2021-01-14T12:27:28.303Z")
  144. def test_timeseries_interval(self):
  145. response = self.do_request(
  146. {"project": [-1], "statsPeriod": "1d", "interval": "1d", "field": ["sum(session)"]}
  147. )
  148. assert response.status_code == 200, response.content
  149. assert result_sorted(response.data) == {
  150. "start": "2021-01-14T00:00:00Z",
  151. "end": "2021-01-14T12:28:00Z",
  152. "query": "",
  153. "intervals": ["2021-01-14T00:00:00Z"],
  154. "groups": [{"by": {}, "series": {"sum(session)": [9]}, "totals": {"sum(session)": 9}}],
  155. }
  156. response = self.do_request(
  157. {"project": [-1], "statsPeriod": "1d", "interval": "6h", "field": ["sum(session)"]}
  158. )
  159. assert response.status_code == 200, response.content
  160. assert result_sorted(response.data) == {
  161. "start": "2021-01-13T18:00:00Z",
  162. "end": "2021-01-14T12:28:00Z",
  163. "query": "",
  164. "intervals": [
  165. "2021-01-13T18:00:00Z",
  166. "2021-01-14T00:00:00Z",
  167. "2021-01-14T06:00:00Z",
  168. "2021-01-14T12:00:00Z",
  169. ],
  170. "groups": [
  171. {"by": {}, "series": {"sum(session)": [0, 1, 2, 6]}, "totals": {"sum(session)": 9}}
  172. ],
  173. }
  174. @freeze_time("2021-01-14T12:27:28.303Z")
  175. def test_user_all_accessible(self):
  176. response = self.do_request(
  177. {"project": [-1], "statsPeriod": "1d", "interval": "1d", "field": ["sum(session)"]},
  178. user=self.user2,
  179. )
  180. assert response.status_code == 200, response.content
  181. assert result_sorted(response.data) == {
  182. "start": "2021-01-14T00:00:00Z",
  183. "end": "2021-01-14T12:28:00Z",
  184. "query": "",
  185. "intervals": ["2021-01-14T00:00:00Z"],
  186. "groups": [{"by": {}, "series": {"sum(session)": [9]}, "totals": {"sum(session)": 9}}],
  187. }
  188. def test_no_projects(self):
  189. response = self.do_request(
  190. {"project": [-1], "statsPeriod": "1d", "interval": "1d", "field": ["sum(session)"]},
  191. org=self.organization3,
  192. )
  193. assert response.status_code == 400, response.content
  194. assert response.data == {"detail": "No projects available"}
  195. @freeze_time("2021-01-14T12:37:28.303Z")
  196. def test_minute_resolution(self):
  197. with self.feature("organizations:minute-resolution-sessions"):
  198. response = self.do_request(
  199. {
  200. "project": [self.project1.id, self.project2.id],
  201. "statsPeriod": "30m",
  202. "interval": "10m",
  203. "field": ["sum(session)"],
  204. }
  205. )
  206. assert response.status_code == 200, response.content
  207. assert result_sorted(response.data) == {
  208. "start": "2021-01-14T12:00:00Z",
  209. "end": "2021-01-14T12:38:00Z",
  210. "query": "",
  211. "intervals": [
  212. "2021-01-14T12:00:00Z",
  213. "2021-01-14T12:10:00Z",
  214. "2021-01-14T12:20:00Z",
  215. "2021-01-14T12:30:00Z",
  216. ],
  217. "groups": [
  218. {
  219. "by": {},
  220. "series": {"sum(session)": [2, 1, 1, 0]},
  221. "totals": {"sum(session)": 4},
  222. }
  223. ],
  224. }
  225. @freeze_time("2021-01-14T12:37:28.303Z")
  226. def test_10s_resolution(self):
  227. with self.feature("organizations:minute-resolution-sessions"):
  228. response = self.do_request(
  229. {
  230. "project": [self.project1.id],
  231. "statsPeriod": "1m",
  232. "interval": "10s",
  233. "field": ["sum(session)"],
  234. }
  235. )
  236. assert response.status_code == 200, response.content
  237. from sentry.api.endpoints.organization_sessions import release_health
  238. if release_health.is_metrics_based():
  239. # With the metrics backend, we should get exactly what we asked for,
  240. # 6 intervals with 10 second length. However, because of rounding,
  241. # we get it rounded to the next minute (see https://github.com/getsentry/sentry/blob/d6c59c32307eee7162301c76b74af419055b9b39/src/sentry/snuba/sessions_v2.py#L388-L392)
  242. assert len(response.data["intervals"]) == 9
  243. else:
  244. # With the sessions backend, the entire period will be aligned
  245. # to one hour, and the resolution will still be one minute:
  246. assert len(response.data["intervals"]) == 38
  247. @freeze_time("2021-01-14T12:27:28.303Z")
  248. def test_filter_projects(self):
  249. response = self.do_request(
  250. {
  251. "statsPeriod": "1d",
  252. "interval": "1d",
  253. "field": ["sum(session)"],
  254. "project": [self.project2.id, self.project3.id],
  255. }
  256. )
  257. assert response.status_code == 200, response.content
  258. assert result_sorted(response.data)["groups"] == [
  259. {"by": {}, "series": {"sum(session)": [5]}, "totals": {"sum(session)": 5}}
  260. ]
  261. @freeze_time("2021-01-14T12:27:28.303Z")
  262. def test_filter_environment(self):
  263. response = self.do_request(
  264. {
  265. "project": [-1],
  266. "statsPeriod": "1d",
  267. "interval": "1d",
  268. "field": ["sum(session)"],
  269. "query": "environment:development",
  270. }
  271. )
  272. assert response.status_code == 200, response.content
  273. assert result_sorted(response.data)["groups"] == [
  274. {"by": {}, "series": {"sum(session)": [1]}, "totals": {"sum(session)": 1}}
  275. ]
  276. response = self.do_request(
  277. {
  278. "project": [-1],
  279. "statsPeriod": "1d",
  280. "interval": "1d",
  281. "field": ["sum(session)"],
  282. "environment": ["development"],
  283. }
  284. )
  285. assert response.status_code == 200, response.content
  286. assert result_sorted(response.data)["groups"] == [
  287. {"by": {}, "series": {"sum(session)": [1]}, "totals": {"sum(session)": 1}}
  288. ]
  289. @freeze_time("2021-01-14T12:27:28.303Z")
  290. def test_filter_release(self):
  291. response = self.do_request(
  292. {
  293. "project": [-1],
  294. "statsPeriod": "1d",
  295. "interval": "1d",
  296. "field": ["sum(session)"],
  297. "query": "release:foo@1.1.0",
  298. }
  299. )
  300. assert response.status_code == 200, response.content
  301. assert result_sorted(response.data)["groups"] == [
  302. {"by": {}, "series": {"sum(session)": [1]}, "totals": {"sum(session)": 1}}
  303. ]
  304. response = self.do_request(
  305. {
  306. "project": [-1],
  307. "statsPeriod": "1d",
  308. "interval": "1d",
  309. "field": ["sum(session)"],
  310. "query": 'release:"foo@1.1.0" or release:"foo@1.2.0"',
  311. }
  312. )
  313. assert response.status_code == 200, response.content
  314. assert result_sorted(response.data)["groups"] == [
  315. {"by": {}, "series": {"sum(session)": [2]}, "totals": {"sum(session)": 2}}
  316. ]
  317. response = self.do_request(
  318. {
  319. "project": [-1],
  320. "statsPeriod": "1d",
  321. "interval": "1d",
  322. "field": ["sum(session)"],
  323. "query": 'release:"foo@1.1.0" or release:"foo@1.2.0" or release:"foo@1.3.0"',
  324. "groupBy": ["release"],
  325. }
  326. )
  327. assert response.status_code == 200, response.content
  328. assert result_sorted(response.data)["groups"] == [
  329. {
  330. "by": {"release": "foo@1.1.0"},
  331. "series": {"sum(session)": [1]},
  332. "totals": {"sum(session)": 1},
  333. },
  334. {
  335. "by": {"release": "foo@1.2.0"},
  336. "series": {"sum(session)": [1]},
  337. "totals": {"sum(session)": 1},
  338. },
  339. ]
  340. @freeze_time("2021-01-14T12:27:28.303Z")
  341. def test_filter_unknown_release(self):
  342. response = self.do_request(
  343. {
  344. "project": [-1],
  345. "statsPeriod": "1d",
  346. "interval": "1h",
  347. "field": ["sum(session)"],
  348. "query": "release:foo@6.6.6",
  349. "groupBy": "session.status",
  350. }
  351. )
  352. assert response.status_code == 200, response.content
  353. @freeze_time("2021-01-14T12:27:28.303Z")
  354. def test_groupby_project(self):
  355. response = self.do_request(
  356. {
  357. "project": [-1],
  358. "statsPeriod": "1d",
  359. "interval": "1d",
  360. "field": ["sum(session)"],
  361. "groupBy": ["project"],
  362. }
  363. )
  364. assert response.status_code == 200, response.content
  365. assert result_sorted(response.data)["groups"] == [
  366. {
  367. "by": {"project": self.project1.id},
  368. "series": {"sum(session)": [4]},
  369. "totals": {"sum(session)": 4},
  370. },
  371. {
  372. "by": {"project": self.project2.id},
  373. "series": {"sum(session)": [2]},
  374. "totals": {"sum(session)": 2},
  375. },
  376. {
  377. "by": {"project": self.project3.id},
  378. "series": {"sum(session)": [3]},
  379. "totals": {"sum(session)": 3},
  380. },
  381. ]
  382. @freeze_time("2021-01-14T12:27:28.303Z")
  383. def test_groupby_environment(self):
  384. response = self.do_request(
  385. {
  386. "project": [-1],
  387. "statsPeriod": "1d",
  388. "interval": "1d",
  389. "field": ["sum(session)"],
  390. "groupBy": ["environment"],
  391. }
  392. )
  393. assert response.status_code == 200, response.content
  394. assert result_sorted(response.data)["groups"] == [
  395. {
  396. "by": {"environment": "development"},
  397. "series": {"sum(session)": [1]},
  398. "totals": {"sum(session)": 1},
  399. },
  400. {
  401. "by": {"environment": "production"},
  402. "series": {"sum(session)": [8]},
  403. "totals": {"sum(session)": 8},
  404. },
  405. ]
  406. @freeze_time("2021-01-14T12:27:28.303Z")
  407. def test_groupby_release(self):
  408. response = self.do_request(
  409. {
  410. "project": [-1],
  411. "statsPeriod": "1d",
  412. "interval": "1d",
  413. "field": ["sum(session)"],
  414. "groupBy": ["release"],
  415. }
  416. )
  417. assert response.status_code == 200, response.content
  418. assert result_sorted(response.data)["groups"] == [
  419. {
  420. "by": {"release": "foo@1.0.0"},
  421. "series": {"sum(session)": [7]},
  422. "totals": {"sum(session)": 7},
  423. },
  424. {
  425. "by": {"release": "foo@1.1.0"},
  426. "series": {"sum(session)": [1]},
  427. "totals": {"sum(session)": 1},
  428. },
  429. {
  430. "by": {"release": "foo@1.2.0"},
  431. "series": {"sum(session)": [1]},
  432. "totals": {"sum(session)": 1},
  433. },
  434. ]
  435. @freeze_time("2021-01-14T12:27:28.303Z")
  436. def test_groupby_status(self):
  437. response = self.do_request(
  438. {
  439. "project": [-1],
  440. "statsPeriod": "1d",
  441. "interval": "1d",
  442. "field": ["sum(session)"],
  443. "groupBy": ["session.status"],
  444. }
  445. )
  446. assert response.status_code == 200, response.content
  447. assert result_sorted(response.data)["groups"] == [
  448. {
  449. "by": {"session.status": "abnormal"},
  450. "series": {"sum(session)": [0]},
  451. "totals": {"sum(session)": 0},
  452. },
  453. {
  454. "by": {"session.status": "crashed"},
  455. "series": {"sum(session)": [1]},
  456. "totals": {"sum(session)": 1},
  457. },
  458. {
  459. "by": {"session.status": "errored"},
  460. "series": {"sum(session)": [2]},
  461. "totals": {"sum(session)": 2},
  462. },
  463. {
  464. "by": {"session.status": "healthy"},
  465. "series": {"sum(session)": [6]},
  466. "totals": {"sum(session)": 6},
  467. },
  468. ]
  469. @freeze_time("2021-01-14T12:27:28.303Z")
  470. def test_groupby_cross(self):
  471. response = self.do_request(
  472. {
  473. "project": [-1],
  474. "statsPeriod": "1d",
  475. "interval": "1d",
  476. "field": ["sum(session)"],
  477. "groupBy": ["release", "environment"],
  478. }
  479. )
  480. assert response.status_code == 200, response.content
  481. assert result_sorted(response.data)["groups"] == [
  482. {
  483. "by": {"environment": "development", "release": "foo@1.0.0"},
  484. "series": {"sum(session)": [1]},
  485. "totals": {"sum(session)": 1},
  486. },
  487. {
  488. "by": {"environment": "production", "release": "foo@1.0.0"},
  489. "series": {"sum(session)": [6]},
  490. "totals": {"sum(session)": 6},
  491. },
  492. {
  493. "by": {"environment": "production", "release": "foo@1.1.0"},
  494. "series": {"sum(session)": [1]},
  495. "totals": {"sum(session)": 1},
  496. },
  497. {
  498. "by": {"environment": "production", "release": "foo@1.2.0"},
  499. "series": {"sum(session)": [1]},
  500. "totals": {"sum(session)": 1},
  501. },
  502. ]
  503. @freeze_time("2021-01-14T12:27:28.303Z")
  504. def test_users_groupby(self):
  505. response = self.do_request(
  506. {
  507. "project": [-1],
  508. "statsPeriod": "1d",
  509. "interval": "1d",
  510. "field": ["count_unique(user)"],
  511. }
  512. )
  513. assert response.status_code == 200, response.content
  514. assert result_sorted(response.data)["groups"] == [
  515. {"by": {}, "series": {"count_unique(user)": [1]}, "totals": {"count_unique(user)": 1}}
  516. ]
  517. response = self.do_request(
  518. {
  519. "project": [-1],
  520. "statsPeriod": "1d",
  521. "interval": "1d",
  522. "field": ["count_unique(user)"],
  523. "groupBy": ["session.status"],
  524. }
  525. )
  526. assert response.status_code == 200, response.content
  527. assert result_sorted(response.data)["groups"] == [
  528. {
  529. "by": {"session.status": "abnormal"},
  530. "series": {"count_unique(user)": [0]},
  531. "totals": {"count_unique(user)": 0},
  532. },
  533. {
  534. "by": {"session.status": "crashed"},
  535. "series": {"count_unique(user)": [0]},
  536. "totals": {"count_unique(user)": 0},
  537. },
  538. {
  539. "by": {"session.status": "errored"},
  540. "series": {"count_unique(user)": [1]},
  541. "totals": {"count_unique(user)": 1},
  542. },
  543. {
  544. "by": {"session.status": "healthy"},
  545. "series": {"count_unique(user)": [0]},
  546. "totals": {"count_unique(user)": 0},
  547. },
  548. ]
  549. expected_duration_values = {
  550. "avg(session.duration)": 42375.0,
  551. "max(session.duration)": 80000.0,
  552. "p50(session.duration)": 33500.0,
  553. "p75(session.duration)": 53750.0,
  554. "p90(session.duration)": 71600.0,
  555. "p95(session.duration)": 75800.0,
  556. "p99(session.duration)": 79159.99999999999,
  557. }
  558. @freeze_time("2021-01-14T12:27:28.303Z")
  559. def test_duration_percentiles(self):
  560. response = self.do_request(
  561. {
  562. "project": [-1],
  563. "statsPeriod": "1d",
  564. "interval": "1d",
  565. "field": [
  566. "avg(session.duration)",
  567. "p50(session.duration)",
  568. "p75(session.duration)",
  569. "p90(session.duration)",
  570. "p95(session.duration)",
  571. "p99(session.duration)",
  572. "max(session.duration)",
  573. ],
  574. }
  575. )
  576. assert response.status_code == 200, response.content
  577. expected = self.expected_duration_values
  578. groups = result_sorted(response.data)["groups"]
  579. assert len(groups) == 1, groups
  580. group = groups[0]
  581. assert group["totals"] == pytest.approx(expected)
  582. for key, series in group["series"].items():
  583. assert series == pytest.approx([expected[key]])
  584. @freeze_time("2021-01-14T12:27:28.303Z")
  585. def test_duration_percentiles_groupby(self):
  586. response = self.do_request(
  587. {
  588. "project": [-1],
  589. "statsPeriod": "1d",
  590. "interval": "1d",
  591. "field": [
  592. "avg(session.duration)",
  593. "p50(session.duration)",
  594. "p75(session.duration)",
  595. "p90(session.duration)",
  596. "p95(session.duration)",
  597. "p99(session.duration)",
  598. "max(session.duration)",
  599. ],
  600. "groupBy": "session.status",
  601. }
  602. )
  603. assert response.status_code == 200, response.content
  604. expected = self.expected_duration_values
  605. seen = set() # Make sure all session statuses are listed
  606. for group in result_sorted(response.data)["groups"]:
  607. seen.add(group["by"].get("session.status"))
  608. if group["by"] == {"session.status": "healthy"}:
  609. assert group["totals"] == pytest.approx(expected)
  610. for key, series in group["series"].items():
  611. assert series == pytest.approx([expected[key]])
  612. else:
  613. # Everything's none:
  614. assert group["totals"] == {key: None for key in expected}, group["by"]
  615. assert group["series"] == {key: [None] for key in expected}
  616. assert seen == {"abnormal", "crashed", "errored", "healthy"}
  617. @freeze_time("2021-01-14T12:37:28.303Z")
  618. def test_snuba_limit_exceeded(self):
  619. # 2 * 3 => only show two groups
  620. with patch("sentry.snuba.sessions_v2.SNUBA_LIMIT", 6), patch(
  621. "sentry.release_health.metrics_sessions_v2.SNUBA_LIMIT", 6
  622. ):
  623. response = self.do_request(
  624. {
  625. "project": [-1],
  626. "statsPeriod": "3d",
  627. "interval": "1d",
  628. "field": ["sum(session)", "count_unique(user)"],
  629. "groupBy": ["project", "release", "environment"],
  630. }
  631. )
  632. assert response.status_code == 200, response.content
  633. assert result_sorted(response.data)["groups"] == [
  634. {
  635. "by": {
  636. "release": "foo@1.0.0",
  637. "environment": "production",
  638. "project": self.project1.id,
  639. },
  640. "totals": {"sum(session)": 3, "count_unique(user)": 0},
  641. "series": {"sum(session)": [0, 0, 3], "count_unique(user)": [0, 0, 0]},
  642. },
  643. {
  644. "by": {
  645. "release": "foo@1.0.0",
  646. "environment": "production",
  647. "project": self.project3.id,
  648. },
  649. "totals": {"sum(session)": 2, "count_unique(user)": 1},
  650. "series": {"sum(session)": [0, 0, 2], "count_unique(user)": [0, 0, 1]},
  651. },
  652. ]
  653. @freeze_time("2021-01-14T12:27:28.303Z")
  654. def test_environment_filter_not_present_in_query(self):
  655. self.create_environment(name="abc")
  656. response = self.do_request(
  657. {
  658. "project": [-1],
  659. "statsPeriod": "1d",
  660. "interval": "1d",
  661. "field": ["sum(session)"],
  662. "environment": ["development", "abc"],
  663. }
  664. )
  665. assert response.status_code == 200, response.content
  666. assert result_sorted(response.data)["groups"] == [
  667. {"by": {}, "series": {"sum(session)": [1]}, "totals": {"sum(session)": 1}}
  668. ]
  669. @patch("sentry.api.endpoints.organization_sessions.release_health", MetricsReleaseHealthBackend())
  670. class OrganizationSessionsEndpointMetricsTest(
  671. SessionMetricsTestCase, OrganizationSessionsEndpointTest
  672. ):
  673. """Repeat with metrics backend"""