test_organization_sessions.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408
  1. from __future__ import absolute_import
  2. import datetime
  3. import pytz
  4. from uuid import uuid4
  5. from freezegun import freeze_time
  6. from django.core.urlresolvers import reverse
  7. from sentry.testutils import APITestCase, SnubaTestCase
  8. from sentry.utils.dates import to_timestamp
  9. def result_sorted(result):
  10. """sort the groups of the results array by the `by` object, ensuring a stable order"""
  11. def stable_dict(d):
  12. return tuple(sorted(d.items(), key=lambda t: t[0]))
  13. result["groups"].sort(key=lambda group: stable_dict(group["by"]))
  14. return result
  15. class OrganizationSessionsEndpointTest(APITestCase, SnubaTestCase):
  16. def setUp(self):
  17. super(OrganizationSessionsEndpointTest, self).setUp()
  18. self.setup_fixture()
  19. def setup_fixture(self):
  20. self.timestamp = to_timestamp(datetime.datetime(2021, 1, 14, 12, 27, 28, tzinfo=pytz.utc))
  21. self.received = self.timestamp
  22. self.session_started = self.timestamp // 60 * 60
  23. self.organization1 = self.organization
  24. self.organization2 = self.create_organization()
  25. self.project1 = self.project
  26. self.project2 = self.create_project()
  27. self.project3 = self.create_project()
  28. self.project4 = self.create_project(organization=self.organization2)
  29. template = {
  30. "distinct_id": "00000000-0000-0000-0000-000000000000",
  31. "status": "exited",
  32. "seq": 0,
  33. "release": "foo@1.0.0",
  34. "environment": "production",
  35. "retention_days": 90,
  36. "duration": None,
  37. "errors": 0,
  38. "started": self.session_started,
  39. "received": self.received,
  40. }
  41. def make_session(project, **kwargs):
  42. return dict(
  43. template,
  44. session_id=uuid4().hex,
  45. org_id=project.organization_id,
  46. project_id=project.id,
  47. **kwargs
  48. )
  49. self.store_session(make_session(self.project1))
  50. self.store_session(make_session(self.project1, release="foo@1.1.0"))
  51. self.store_session(make_session(self.project1, started=self.session_started - 60 * 60))
  52. self.store_session(make_session(self.project1, started=self.session_started - 12 * 60 * 60))
  53. self.store_session(make_session(self.project2, status="crashed"))
  54. self.store_session(make_session(self.project2, environment="development"))
  55. self.store_session(make_session(self.project3, errors=1))
  56. self.store_session(
  57. make_session(
  58. self.project3,
  59. distinct_id="39887d89-13b2-4c84-8c23-5d13d2102664",
  60. started=self.session_started - 60 * 60,
  61. )
  62. )
  63. self.store_session(
  64. make_session(
  65. self.project3, distinct_id="39887d89-13b2-4c84-8c23-5d13d2102664", errors=1
  66. )
  67. )
  68. self.store_session(make_session(self.project4))
  69. def do_request(self, query):
  70. self.login_as(user=self.user)
  71. url = reverse(
  72. "sentry-api-0-organization-sessions",
  73. kwargs={"organization_slug": self.organization.slug},
  74. )
  75. return self.client.get(url, query, format="json")
  76. def test_empty_request(self):
  77. response = self.do_request({})
  78. assert response.status_code == 400, response.content
  79. assert response.data == {"detail": 'Request is missing a "field"'}
  80. def test_inaccessible_project(self):
  81. response = self.do_request({"project": [self.project4.id]})
  82. assert response.status_code == 403, response.content
  83. assert response.data == {"detail": "You do not have permission to perform this action."}
  84. def test_unknown_field(self):
  85. response = self.do_request({"field": ["summ(sessin)"]})
  86. assert response.status_code == 400, response.content
  87. assert response.data == {"detail": 'Invalid field: "summ(sessin)"'}
  88. def test_unknown_groupby(self):
  89. response = self.do_request({"field": ["sum(session)"], "groupBy": ["envriomnent"]})
  90. assert response.status_code == 400, response.content
  91. assert response.data == {"detail": 'Invalid groupBy: "envriomnent"'}
  92. def test_too_many_points(self):
  93. # TODO: looks like this is well within the range of valid points
  94. return
  95. # default statsPeriod is 90d
  96. response = self.do_request({"field": ["sum(session)"], "interval": "1h"})
  97. assert response.status_code == 400, response.content
  98. assert response.data == {}
  99. @freeze_time("2021-01-14T12:27:28.303Z")
  100. def test_timeseries_interval(self):
  101. response = self.do_request(
  102. {"statsPeriod": "1d", "interval": "1d", "field": ["sum(session)"]}
  103. )
  104. assert response.status_code == 200, response.content
  105. assert result_sorted(response.data) == {
  106. "query": "",
  107. "intervals": ["2021-01-14T00:00:00Z"],
  108. "groups": [{"by": {}, "series": {"sum(session)": [9]}, "totals": {"sum(session)": 9}}],
  109. }
  110. response = self.do_request(
  111. {"statsPeriod": "1d", "interval": "6h", "field": ["sum(session)"]}
  112. )
  113. assert response.status_code == 200, response.content
  114. assert result_sorted(response.data) == {
  115. "query": "",
  116. "intervals": [
  117. "2021-01-13T18:00:00Z",
  118. "2021-01-14T00:00:00Z",
  119. "2021-01-14T06:00:00Z",
  120. "2021-01-14T12:00:00Z",
  121. ],
  122. "groups": [
  123. {"by": {}, "series": {"sum(session)": [0, 1, 2, 6]}, "totals": {"sum(session)": 9}}
  124. ],
  125. }
  126. @freeze_time("2021-01-14T12:27:28.303Z")
  127. def test_minimum_interval(self):
  128. # smallest interval is 1h
  129. response = self.do_request(
  130. {"statsPeriod": "2h", "interval": "5m", "field": ["sum(session)"]}
  131. )
  132. assert response.status_code == 200, response.content
  133. assert result_sorted(response.data) == {
  134. "query": "",
  135. "intervals": ["2021-01-14T11:00:00Z", "2021-01-14T12:00:00Z"],
  136. "groups": [
  137. {"by": {}, "series": {"sum(session)": [2, 6]}, "totals": {"sum(session)": 8}}
  138. ],
  139. }
  140. @freeze_time("2021-01-14T12:27:28.303Z")
  141. def test_filter_projects(self):
  142. response = self.do_request(
  143. {
  144. "statsPeriod": "1d",
  145. "interval": "1d",
  146. "field": ["sum(session)"],
  147. "project": [self.project2.id, self.project3.id],
  148. }
  149. )
  150. assert response.status_code == 200, response.content
  151. assert result_sorted(response.data)["groups"] == [
  152. {"by": {}, "series": {"sum(session)": [5]}, "totals": {"sum(session)": 5}}
  153. ]
  154. @freeze_time("2021-01-14T12:27:28.303Z")
  155. def test_filter_environment(self):
  156. response = self.do_request(
  157. {
  158. "statsPeriod": "1d",
  159. "interval": "1d",
  160. "field": ["sum(session)"],
  161. "query": "environment:development",
  162. }
  163. )
  164. assert response.status_code == 200, response.content
  165. assert result_sorted(response.data)["groups"] == [
  166. {"by": {}, "series": {"sum(session)": [1]}, "totals": {"sum(session)": 1}}
  167. ]
  168. @freeze_time("2021-01-14T12:27:28.303Z")
  169. def test_filter_release(self):
  170. response = self.do_request(
  171. {
  172. "statsPeriod": "1d",
  173. "interval": "1d",
  174. "field": ["sum(session)"],
  175. "query": "release:foo@1.1.0",
  176. }
  177. )
  178. assert response.status_code == 200, response.content
  179. assert result_sorted(response.data)["groups"] == [
  180. {"by": {}, "series": {"sum(session)": [1]}, "totals": {"sum(session)": 1}}
  181. ]
  182. @freeze_time("2021-01-14T12:27:28.303Z")
  183. def test_groupby_project(self):
  184. response = self.do_request(
  185. {
  186. "statsPeriod": "1d",
  187. "interval": "1d",
  188. "field": ["sum(session)"],
  189. "groupBy": ["project"],
  190. }
  191. )
  192. assert response.status_code == 200, response.content
  193. assert result_sorted(response.data)["groups"] == [
  194. {
  195. "by": {"project": self.project1.id},
  196. "series": {"sum(session)": [4]},
  197. "totals": {"sum(session)": 4},
  198. },
  199. {
  200. "by": {"project": self.project2.id},
  201. "series": {"sum(session)": [2]},
  202. "totals": {"sum(session)": 2},
  203. },
  204. {
  205. "by": {"project": self.project3.id},
  206. "series": {"sum(session)": [3]},
  207. "totals": {"sum(session)": 3},
  208. },
  209. ]
  210. @freeze_time("2021-01-14T12:27:28.303Z")
  211. def test_groupby_environment(self):
  212. response = self.do_request(
  213. {
  214. "statsPeriod": "1d",
  215. "interval": "1d",
  216. "field": ["sum(session)"],
  217. "groupBy": ["environment"],
  218. }
  219. )
  220. assert response.status_code == 200, response.content
  221. assert result_sorted(response.data)["groups"] == [
  222. {
  223. "by": {"environment": "development"},
  224. "series": {"sum(session)": [1]},
  225. "totals": {"sum(session)": 1},
  226. },
  227. {
  228. "by": {"environment": "production"},
  229. "series": {"sum(session)": [8]},
  230. "totals": {"sum(session)": 8},
  231. },
  232. ]
  233. @freeze_time("2021-01-14T12:27:28.303Z")
  234. def test_groupby_release(self):
  235. response = self.do_request(
  236. {
  237. "statsPeriod": "1d",
  238. "interval": "1d",
  239. "field": ["sum(session)"],
  240. "groupBy": ["release"],
  241. }
  242. )
  243. assert response.status_code == 200, response.content
  244. assert result_sorted(response.data)["groups"] == [
  245. {
  246. "by": {"release": "foo@1.0.0"},
  247. "series": {"sum(session)": [8]},
  248. "totals": {"sum(session)": 8},
  249. },
  250. {
  251. "by": {"release": "foo@1.1.0"},
  252. "series": {"sum(session)": [1]},
  253. "totals": {"sum(session)": 1},
  254. },
  255. ]
  256. @freeze_time("2021-01-14T12:27:28.303Z")
  257. def test_groupby_status(self):
  258. response = self.do_request(
  259. {
  260. "statsPeriod": "1d",
  261. "interval": "1d",
  262. "field": ["sum(session)"],
  263. "groupBy": ["session.status"],
  264. }
  265. )
  266. assert response.status_code == 200, response.content
  267. assert result_sorted(response.data)["groups"] == [
  268. {
  269. "by": {"session.status": "abnormal"},
  270. "series": {"sum(session)": [0]},
  271. "totals": {"sum(session)": 0},
  272. },
  273. {
  274. "by": {"session.status": "crashed"},
  275. "series": {"sum(session)": [1]},
  276. "totals": {"sum(session)": 1},
  277. },
  278. {
  279. "by": {"session.status": "errored"},
  280. "series": {"sum(session)": [3]},
  281. "totals": {"sum(session)": 3},
  282. },
  283. {
  284. "by": {"session.status": "healthy"},
  285. "series": {"sum(session)": [6]},
  286. "totals": {"sum(session)": 6},
  287. },
  288. ]
  289. @freeze_time("2021-01-14T12:27:28.303Z")
  290. def test_groupby_cross(self):
  291. response = self.do_request(
  292. {
  293. "statsPeriod": "1d",
  294. "interval": "1d",
  295. "field": ["sum(session)"],
  296. "groupBy": ["release", "environment"],
  297. }
  298. )
  299. assert response.status_code == 200, response.content
  300. assert result_sorted(response.data)["groups"] == [
  301. {
  302. "by": {"environment": "development", "release": "foo@1.0.0"},
  303. "series": {"sum(session)": [1]},
  304. "totals": {"sum(session)": 1},
  305. },
  306. {
  307. "by": {"environment": "production", "release": "foo@1.0.0"},
  308. "series": {"sum(session)": [7]},
  309. "totals": {"sum(session)": 7},
  310. },
  311. {
  312. "by": {"environment": "production", "release": "foo@1.1.0"},
  313. "series": {"sum(session)": [1]},
  314. "totals": {"sum(session)": 1},
  315. },
  316. ]
  317. @freeze_time("2021-01-14T12:27:28.303Z")
  318. def test_users_groupby(self):
  319. response = self.do_request(
  320. {"statsPeriod": "1d", "interval": "1d", "field": ["count_unique(user)"]}
  321. )
  322. assert response.status_code == 200, response.content
  323. assert result_sorted(response.data)["groups"] == [
  324. {"by": {}, "series": {"count_unique(user)": [1]}, "totals": {"count_unique(user)": 1}}
  325. ]
  326. response = self.do_request(
  327. {
  328. "statsPeriod": "1d",
  329. "interval": "1d",
  330. "field": ["count_unique(user)"],
  331. "groupBy": ["session.status"],
  332. }
  333. )
  334. assert response.status_code == 200, response.content
  335. assert result_sorted(response.data)["groups"] == [
  336. {
  337. "by": {"session.status": "abnormal"},
  338. "series": {"count_unique(user)": [0]},
  339. "totals": {"count_unique(user)": 0},
  340. },
  341. {
  342. "by": {"session.status": "crashed"},
  343. "series": {"count_unique(user)": [0]},
  344. "totals": {"count_unique(user)": 0},
  345. },
  346. {
  347. "by": {"session.status": "errored"},
  348. "series": {"count_unique(user)": [1]},
  349. "totals": {"count_unique(user)": 1},
  350. },
  351. {
  352. "by": {"session.status": "healthy"},
  353. "series": {"count_unique(user)": [0]},
  354. "totals": {"count_unique(user)": 0},
  355. },
  356. ]