test_organization_sessions.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545
  1. import datetime
  2. import pytz
  3. from uuid import uuid4
  4. from freezegun import freeze_time
  5. from django.core.urlresolvers import reverse
  6. from sentry.testutils import APITestCase, SnubaTestCase
  7. from sentry.utils.dates import to_timestamp
  8. def result_sorted(result):
  9. """sort the groups of the results array by the `by` object, ensuring a stable order"""
  10. def stable_dict(d):
  11. return tuple(sorted(d.items(), key=lambda t: t[0]))
  12. result["groups"].sort(key=lambda group: stable_dict(group["by"]))
  13. return result
  14. class OrganizationSessionsEndpointTest(APITestCase, SnubaTestCase):
  15. def setUp(self):
  16. super().setUp()
  17. self.setup_fixture()
  18. def setup_fixture(self):
  19. self.timestamp = to_timestamp(datetime.datetime(2021, 1, 14, 12, 27, 28, tzinfo=pytz.utc))
  20. self.received = self.timestamp
  21. self.session_started = self.timestamp // 60 * 60
  22. self.organization1 = self.organization
  23. self.organization2 = self.create_organization()
  24. self.organization3 = 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. self.user2 = self.create_user(is_superuser=False)
  30. self.create_member(
  31. user=self.user2, organization=self.organization1, role="member", teams=[]
  32. )
  33. self.create_member(user=self.user, organization=self.organization3, role="admin", teams=[])
  34. self.create_environment(self.project2, name="development")
  35. template = {
  36. "distinct_id": "00000000-0000-0000-0000-000000000000",
  37. "status": "exited",
  38. "seq": 0,
  39. "release": "foo@1.0.0",
  40. "environment": "production",
  41. "retention_days": 90,
  42. "duration": None,
  43. "errors": 0,
  44. "started": self.session_started,
  45. "received": self.received,
  46. }
  47. def make_session(project, **kwargs):
  48. return dict(
  49. template,
  50. session_id=uuid4().hex,
  51. org_id=project.organization_id,
  52. project_id=project.id,
  53. **kwargs,
  54. )
  55. self.store_session(make_session(self.project1))
  56. self.store_session(make_session(self.project1, release="foo@1.1.0"))
  57. self.store_session(make_session(self.project1, started=self.session_started - 60 * 60))
  58. self.store_session(make_session(self.project1, started=self.session_started - 12 * 60 * 60))
  59. self.store_session(make_session(self.project2, status="crashed"))
  60. self.store_session(make_session(self.project2, environment="development"))
  61. self.store_session(make_session(self.project3, errors=1, release="foo@1.2.0"))
  62. self.store_session(
  63. make_session(
  64. self.project3,
  65. distinct_id="39887d89-13b2-4c84-8c23-5d13d2102664",
  66. started=self.session_started - 60 * 60,
  67. )
  68. )
  69. self.store_session(
  70. make_session(
  71. self.project3, distinct_id="39887d89-13b2-4c84-8c23-5d13d2102664", errors=1
  72. )
  73. )
  74. self.store_session(make_session(self.project4))
  75. def do_request(self, query, user=None, org=None):
  76. self.login_as(user=user or self.user)
  77. url = reverse(
  78. "sentry-api-0-organization-sessions",
  79. kwargs={"organization_slug": (org or self.organization).slug},
  80. )
  81. return self.client.get(url, query, format="json")
  82. def test_empty_request(self):
  83. response = self.do_request({})
  84. assert response.status_code == 400, response.content
  85. assert response.data == {"detail": 'Request is missing a "field"'}
  86. def test_inaccessible_project(self):
  87. response = self.do_request({"project": [self.project4.id]})
  88. assert response.status_code == 403, response.content
  89. assert response.data == {"detail": "You do not have permission to perform this action."}
  90. def test_unknown_field(self):
  91. response = self.do_request({"field": ["summ(sessin)"]})
  92. assert response.status_code == 400, response.content
  93. assert response.data == {"detail": 'Invalid field: "summ(sessin)"'}
  94. def test_unknown_groupby(self):
  95. response = self.do_request({"field": ["sum(session)"], "groupBy": ["envriomnent"]})
  96. assert response.status_code == 400, response.content
  97. assert response.data == {"detail": 'Invalid groupBy: "envriomnent"'}
  98. def test_invalid_query(self):
  99. response = self.do_request(
  100. {"statsPeriod": "1d", "field": ["sum(session)"], "query": ["foo:bar"]}
  101. )
  102. assert response.status_code == 400, response.content
  103. assert response.data == {"detail": 'Invalid query field: "foo"'}
  104. response = self.do_request(
  105. {
  106. "statsPeriod": "1d",
  107. "field": ["sum(session)"],
  108. "query": ["release:foo-bar@1.2.3 (123)"],
  109. }
  110. )
  111. assert response.status_code == 400, response.content
  112. # TODO: it would be good to provide a better error here,
  113. # since its not obvious where `message` comes from.
  114. assert response.data == {"detail": 'Invalid query field: "message"'}
  115. def test_too_many_points(self):
  116. # default statsPeriod is 90d
  117. response = self.do_request({"field": ["sum(session)"], "interval": "1h"})
  118. assert response.status_code == 400, response.content
  119. assert response.data == {
  120. "detail": "Your interval and date range would create too many results. "
  121. "Use a larger interval, or a smaller date range."
  122. }
  123. @freeze_time("2021-01-14T12:27:28.303Z")
  124. def test_timeseries_interval(self):
  125. response = self.do_request(
  126. {"project": [-1], "statsPeriod": "1d", "interval": "1d", "field": ["sum(session)"]}
  127. )
  128. assert response.status_code == 200, response.content
  129. assert result_sorted(response.data) == {
  130. "query": "",
  131. "intervals": ["2021-01-14T00:00:00Z"],
  132. "groups": [{"by": {}, "series": {"sum(session)": [9]}, "totals": {"sum(session)": 9}}],
  133. }
  134. response = self.do_request(
  135. {"project": [-1], "statsPeriod": "1d", "interval": "6h", "field": ["sum(session)"]}
  136. )
  137. assert response.status_code == 200, response.content
  138. assert result_sorted(response.data) == {
  139. "query": "",
  140. "intervals": [
  141. "2021-01-13T18:00:00Z",
  142. "2021-01-14T00:00:00Z",
  143. "2021-01-14T06:00:00Z",
  144. "2021-01-14T12:00:00Z",
  145. ],
  146. "groups": [
  147. {"by": {}, "series": {"sum(session)": [0, 1, 2, 6]}, "totals": {"sum(session)": 9}}
  148. ],
  149. }
  150. @freeze_time("2021-01-14T12:27:28.303Z")
  151. def test_user_all_accessible(self):
  152. response = self.do_request(
  153. {"project": [-1], "statsPeriod": "1d", "interval": "1d", "field": ["sum(session)"]},
  154. user=self.user2,
  155. )
  156. assert response.status_code == 200, response.content
  157. assert result_sorted(response.data) == {
  158. "query": "",
  159. "intervals": ["2021-01-14T00:00:00Z"],
  160. "groups": [{"by": {}, "series": {"sum(session)": [9]}, "totals": {"sum(session)": 9}}],
  161. }
  162. def test_no_projects(self):
  163. response = self.do_request(
  164. {"project": [-1], "statsPeriod": "1d", "interval": "1d", "field": ["sum(session)"]},
  165. org=self.organization3,
  166. )
  167. assert response.status_code == 400, response.content
  168. assert response.data == {"detail": "No projects available"}
  169. @freeze_time("2021-01-14T12:27:28.303Z")
  170. def test_minimum_interval(self):
  171. # smallest interval is 1h
  172. response = self.do_request(
  173. {"project": [-1], "statsPeriod": "2h", "interval": "5m", "field": ["sum(session)"]}
  174. )
  175. assert response.status_code == 400, response.content
  176. assert response.data == {
  177. "detail": "The interval has to be a multiple of the minimum interval of one hour."
  178. }
  179. response = self.do_request(
  180. {"project": [-1], "statsPeriod": "2h", "interval": "1h", "field": ["sum(session)"]}
  181. )
  182. assert response.status_code == 200, response.content
  183. assert result_sorted(response.data) == {
  184. "query": "",
  185. "intervals": ["2021-01-14T11:00:00Z", "2021-01-14T12:00:00Z"],
  186. "groups": [
  187. {"by": {}, "series": {"sum(session)": [2, 6]}, "totals": {"sum(session)": 8}}
  188. ],
  189. }
  190. @freeze_time("2021-01-14T12:27:28.303Z")
  191. def test_filter_projects(self):
  192. response = self.do_request(
  193. {
  194. "statsPeriod": "1d",
  195. "interval": "1d",
  196. "field": ["sum(session)"],
  197. "project": [self.project2.id, self.project3.id],
  198. }
  199. )
  200. assert response.status_code == 200, response.content
  201. assert result_sorted(response.data)["groups"] == [
  202. {"by": {}, "series": {"sum(session)": [5]}, "totals": {"sum(session)": 5}}
  203. ]
  204. @freeze_time("2021-01-14T12:27:28.303Z")
  205. def test_filter_environment(self):
  206. response = self.do_request(
  207. {
  208. "project": [-1],
  209. "statsPeriod": "1d",
  210. "interval": "1d",
  211. "field": ["sum(session)"],
  212. "query": "environment:development",
  213. }
  214. )
  215. assert response.status_code == 200, response.content
  216. assert result_sorted(response.data)["groups"] == [
  217. {"by": {}, "series": {"sum(session)": [1]}, "totals": {"sum(session)": 1}}
  218. ]
  219. response = self.do_request(
  220. {
  221. "project": [-1],
  222. "statsPeriod": "1d",
  223. "interval": "1d",
  224. "field": ["sum(session)"],
  225. "environment": ["development"],
  226. }
  227. )
  228. assert response.status_code == 200, response.content
  229. assert result_sorted(response.data)["groups"] == [
  230. {"by": {}, "series": {"sum(session)": [1]}, "totals": {"sum(session)": 1}}
  231. ]
  232. @freeze_time("2021-01-14T12:27:28.303Z")
  233. def test_filter_release(self):
  234. response = self.do_request(
  235. {
  236. "project": [-1],
  237. "statsPeriod": "1d",
  238. "interval": "1d",
  239. "field": ["sum(session)"],
  240. "query": "release:foo@1.1.0",
  241. }
  242. )
  243. assert response.status_code == 200, response.content
  244. assert result_sorted(response.data)["groups"] == [
  245. {"by": {}, "series": {"sum(session)": [1]}, "totals": {"sum(session)": 1}}
  246. ]
  247. response = self.do_request(
  248. {
  249. "project": [-1],
  250. "statsPeriod": "1d",
  251. "interval": "1d",
  252. "field": ["sum(session)"],
  253. "query": 'release:"foo@1.1.0" or release:"foo@1.2.0"',
  254. }
  255. )
  256. assert response.status_code == 200, response.content
  257. assert result_sorted(response.data)["groups"] == [
  258. {"by": {}, "series": {"sum(session)": [2]}, "totals": {"sum(session)": 2}}
  259. ]
  260. response = self.do_request(
  261. {
  262. "project": [-1],
  263. "statsPeriod": "1d",
  264. "interval": "1d",
  265. "field": ["sum(session)"],
  266. "query": 'release:"foo@1.1.0" or release:"foo@1.2.0" or release:"foo@1.3.0"',
  267. "groupBy": ["release"],
  268. }
  269. )
  270. assert response.status_code == 200, response.content
  271. assert result_sorted(response.data)["groups"] == [
  272. {
  273. "by": {"release": "foo@1.1.0"},
  274. "series": {"sum(session)": [1]},
  275. "totals": {"sum(session)": 1},
  276. },
  277. {
  278. "by": {"release": "foo@1.2.0"},
  279. "series": {"sum(session)": [1]},
  280. "totals": {"sum(session)": 1},
  281. },
  282. ]
  283. @freeze_time("2021-01-14T12:27:28.303Z")
  284. def test_groupby_project(self):
  285. response = self.do_request(
  286. {
  287. "project": [-1],
  288. "statsPeriod": "1d",
  289. "interval": "1d",
  290. "field": ["sum(session)"],
  291. "groupBy": ["project"],
  292. }
  293. )
  294. assert response.status_code == 200, response.content
  295. assert result_sorted(response.data)["groups"] == [
  296. {
  297. "by": {"project": self.project1.id},
  298. "series": {"sum(session)": [4]},
  299. "totals": {"sum(session)": 4},
  300. },
  301. {
  302. "by": {"project": self.project2.id},
  303. "series": {"sum(session)": [2]},
  304. "totals": {"sum(session)": 2},
  305. },
  306. {
  307. "by": {"project": self.project3.id},
  308. "series": {"sum(session)": [3]},
  309. "totals": {"sum(session)": 3},
  310. },
  311. ]
  312. @freeze_time("2021-01-14T12:27:28.303Z")
  313. def test_groupby_environment(self):
  314. response = self.do_request(
  315. {
  316. "project": [-1],
  317. "statsPeriod": "1d",
  318. "interval": "1d",
  319. "field": ["sum(session)"],
  320. "groupBy": ["environment"],
  321. }
  322. )
  323. assert response.status_code == 200, response.content
  324. assert result_sorted(response.data)["groups"] == [
  325. {
  326. "by": {"environment": "development"},
  327. "series": {"sum(session)": [1]},
  328. "totals": {"sum(session)": 1},
  329. },
  330. {
  331. "by": {"environment": "production"},
  332. "series": {"sum(session)": [8]},
  333. "totals": {"sum(session)": 8},
  334. },
  335. ]
  336. @freeze_time("2021-01-14T12:27:28.303Z")
  337. def test_groupby_release(self):
  338. response = self.do_request(
  339. {
  340. "project": [-1],
  341. "statsPeriod": "1d",
  342. "interval": "1d",
  343. "field": ["sum(session)"],
  344. "groupBy": ["release"],
  345. }
  346. )
  347. assert response.status_code == 200, response.content
  348. assert result_sorted(response.data)["groups"] == [
  349. {
  350. "by": {"release": "foo@1.0.0"},
  351. "series": {"sum(session)": [7]},
  352. "totals": {"sum(session)": 7},
  353. },
  354. {
  355. "by": {"release": "foo@1.1.0"},
  356. "series": {"sum(session)": [1]},
  357. "totals": {"sum(session)": 1},
  358. },
  359. {
  360. "by": {"release": "foo@1.2.0"},
  361. "series": {"sum(session)": [1]},
  362. "totals": {"sum(session)": 1},
  363. },
  364. ]
  365. @freeze_time("2021-01-14T12:27:28.303Z")
  366. def test_groupby_status(self):
  367. response = self.do_request(
  368. {
  369. "project": [-1],
  370. "statsPeriod": "1d",
  371. "interval": "1d",
  372. "field": ["sum(session)"],
  373. "groupBy": ["session.status"],
  374. }
  375. )
  376. assert response.status_code == 200, response.content
  377. assert result_sorted(response.data)["groups"] == [
  378. {
  379. "by": {"session.status": "abnormal"},
  380. "series": {"sum(session)": [0]},
  381. "totals": {"sum(session)": 0},
  382. },
  383. {
  384. "by": {"session.status": "crashed"},
  385. "series": {"sum(session)": [1]},
  386. "totals": {"sum(session)": 1},
  387. },
  388. {
  389. "by": {"session.status": "errored"},
  390. "series": {"sum(session)": [3]},
  391. "totals": {"sum(session)": 3},
  392. },
  393. {
  394. "by": {"session.status": "healthy"},
  395. "series": {"sum(session)": [6]},
  396. "totals": {"sum(session)": 6},
  397. },
  398. ]
  399. @freeze_time("2021-01-14T12:27:28.303Z")
  400. def test_groupby_cross(self):
  401. response = self.do_request(
  402. {
  403. "project": [-1],
  404. "statsPeriod": "1d",
  405. "interval": "1d",
  406. "field": ["sum(session)"],
  407. "groupBy": ["release", "environment"],
  408. }
  409. )
  410. assert response.status_code == 200, response.content
  411. assert result_sorted(response.data)["groups"] == [
  412. {
  413. "by": {"environment": "development", "release": "foo@1.0.0"},
  414. "series": {"sum(session)": [1]},
  415. "totals": {"sum(session)": 1},
  416. },
  417. {
  418. "by": {"environment": "production", "release": "foo@1.0.0"},
  419. "series": {"sum(session)": [6]},
  420. "totals": {"sum(session)": 6},
  421. },
  422. {
  423. "by": {"environment": "production", "release": "foo@1.1.0"},
  424. "series": {"sum(session)": [1]},
  425. "totals": {"sum(session)": 1},
  426. },
  427. {
  428. "by": {"environment": "production", "release": "foo@1.2.0"},
  429. "series": {"sum(session)": [1]},
  430. "totals": {"sum(session)": 1},
  431. },
  432. ]
  433. @freeze_time("2021-01-14T12:27:28.303Z")
  434. def test_users_groupby(self):
  435. response = self.do_request(
  436. {
  437. "project": [-1],
  438. "statsPeriod": "1d",
  439. "interval": "1d",
  440. "field": ["count_unique(user)"],
  441. }
  442. )
  443. assert response.status_code == 200, response.content
  444. assert result_sorted(response.data)["groups"] == [
  445. {"by": {}, "series": {"count_unique(user)": [1]}, "totals": {"count_unique(user)": 1}}
  446. ]
  447. response = self.do_request(
  448. {
  449. "project": [-1],
  450. "statsPeriod": "1d",
  451. "interval": "1d",
  452. "field": ["count_unique(user)"],
  453. "groupBy": ["session.status"],
  454. }
  455. )
  456. assert response.status_code == 200, response.content
  457. assert result_sorted(response.data)["groups"] == [
  458. {
  459. "by": {"session.status": "abnormal"},
  460. "series": {"count_unique(user)": [0]},
  461. "totals": {"count_unique(user)": 0},
  462. },
  463. {
  464. "by": {"session.status": "crashed"},
  465. "series": {"count_unique(user)": [0]},
  466. "totals": {"count_unique(user)": 0},
  467. },
  468. {
  469. "by": {"session.status": "errored"},
  470. "series": {"count_unique(user)": [1]},
  471. "totals": {"count_unique(user)": 1},
  472. },
  473. {
  474. "by": {"session.status": "healthy"},
  475. "series": {"count_unique(user)": [0]},
  476. "totals": {"count_unique(user)": 0},
  477. },
  478. ]