test_sessions_v2.py 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798
  1. import math
  2. from datetime import datetime
  3. import pytest
  4. import pytz
  5. from django.http import QueryDict
  6. from freezegun import freeze_time
  7. from sentry.release_health.base import AllowedResolution, SessionsQueryConfig
  8. from sentry.snuba.sessions_v2 import (
  9. InvalidParams,
  10. QueryDefinition,
  11. get_constrained_date_range,
  12. get_timestamps,
  13. massage_sessions_result,
  14. )
  15. from sentry.utils.pytest.fixtures import django_db_all
  16. def _make_query(qs, allow_minute_resolution=True, params=None):
  17. query_config = SessionsQueryConfig(
  18. (AllowedResolution.one_minute if allow_minute_resolution else AllowedResolution.one_hour),
  19. allow_session_status_query=False,
  20. restrict_date_range=True,
  21. )
  22. return QueryDefinition(QueryDict(qs), params or {}, query_config)
  23. def result_sorted(result):
  24. """sort the groups of the results array by the `by` object, ensuring a stable order"""
  25. def stable_dict(d):
  26. return tuple(sorted(d.items(), key=lambda t: t[0]))
  27. result["groups"].sort(key=lambda group: stable_dict(group["by"]))
  28. return result
  29. @freeze_time("2018-12-11 03:21:00")
  30. def test_round_range():
  31. start, end, interval = get_constrained_date_range({"statsPeriod": "2d"})
  32. assert start == datetime(2018, 12, 9, 4, tzinfo=pytz.utc)
  33. assert end == datetime(2018, 12, 11, 3, 22, tzinfo=pytz.utc)
  34. start, end, interval = get_constrained_date_range({"statsPeriod": "2d", "interval": "1d"})
  35. assert start == datetime(2018, 12, 10, tzinfo=pytz.utc)
  36. assert end == datetime(2018, 12, 11, 3, 22, tzinfo=pytz.utc)
  37. def test_invalid_interval():
  38. with pytest.raises(InvalidParams):
  39. start, end, interval = get_constrained_date_range({"interval": "0d"})
  40. def test_round_exact():
  41. start, end, interval = get_constrained_date_range(
  42. {"start": "2021-01-12T04:06:16", "end": "2021-01-17T08:26:13", "interval": "1d"},
  43. )
  44. assert start == datetime(2021, 1, 12, tzinfo=pytz.utc)
  45. assert end == datetime(2021, 1, 18, tzinfo=pytz.utc)
  46. def test_inclusive_end():
  47. start, end, interval = get_constrained_date_range(
  48. {"start": "2021-02-24T00:00:00", "end": "2021-02-25T00:00:00", "interval": "1h"},
  49. )
  50. assert start == datetime(2021, 2, 24, tzinfo=pytz.utc)
  51. assert end == datetime(2021, 2, 25, 1, tzinfo=pytz.utc)
  52. @freeze_time("2021-03-05T11:14:17.105Z")
  53. def test_interval_restrictions():
  54. # making sure intervals are cleanly divisible
  55. with pytest.raises(InvalidParams, match="The interval has to be less than one day."):
  56. _make_query("statsPeriod=4d&interval=2d&field=sum(session)")
  57. with pytest.raises(
  58. InvalidParams, match="The interval should divide one day without a remainder."
  59. ):
  60. _make_query("statsPeriod=6h&interval=59m&field=sum(session)")
  61. with pytest.raises(
  62. InvalidParams, match="The interval should divide one day without a remainder."
  63. ):
  64. _make_query("statsPeriod=4d&interval=5h&field=sum(session)")
  65. _make_query("statsPeriod=6h&interval=90m&field=sum(session)")
  66. with pytest.raises(
  67. InvalidParams,
  68. match="The interval has to be a multiple of the minimum interval of one hour.",
  69. ):
  70. _make_query("statsPeriod=6h&interval=90m&field=sum(session)", False)
  71. with pytest.raises(
  72. InvalidParams,
  73. match="The interval has to be a multiple of the minimum interval of one minute.",
  74. ):
  75. _make_query("statsPeriod=1h&interval=90s&field=sum(session)")
  76. # restrictions for minute resolution time range
  77. with pytest.raises(
  78. InvalidParams,
  79. match="The time-range when using one-minute resolution intervals is restricted to 6 hours.",
  80. ):
  81. _make_query("statsPeriod=7h&interval=15m&field=sum(session)")
  82. with pytest.raises(
  83. InvalidParams,
  84. match="The time-range when using one-minute resolution intervals is restricted to the last 30 days.",
  85. ):
  86. _make_query(
  87. "start=2021-01-05T11:14:17&end=2021-01-05T12:14:17&interval=15m&field=sum(session)"
  88. )
  89. with pytest.raises(
  90. InvalidParams, match="Your interval and date range would create too many results."
  91. ):
  92. _make_query("statsPeriod=90d&interval=1h&field=sum(session)")
  93. @freeze_time("2020-12-18T11:14:17.105Z")
  94. def test_timestamps():
  95. query = _make_query("statsPeriod=1d&interval=12h&field=sum(session)")
  96. expected_timestamps = ["2020-12-17T12:00:00Z", "2020-12-18T00:00:00Z"]
  97. actual_timestamps = get_timestamps(query)
  98. assert actual_timestamps == expected_timestamps
  99. @freeze_time("2021-03-08T09:34:00.000Z")
  100. def test_hourly_rounded_start():
  101. query = _make_query("statsPeriod=30m&interval=1m&field=sum(session)")
  102. actual_timestamps = get_timestamps(query)
  103. assert actual_timestamps[0] == "2021-03-08T09:00:00Z"
  104. assert actual_timestamps[-1] == "2021-03-08T09:34:00Z"
  105. assert len(actual_timestamps) == 35
  106. # in this case "45m" means from 08:49:00-09:34:00, but since we round start/end
  107. # to hours, we extend the start time to 08:00:00.
  108. query = _make_query("statsPeriod=45m&interval=1m&field=sum(session)")
  109. actual_timestamps = get_timestamps(query)
  110. assert actual_timestamps[0] == "2021-03-08T08:00:00Z"
  111. assert actual_timestamps[-1] == "2021-03-08T09:34:00Z"
  112. assert len(actual_timestamps) == 95
  113. def test_rounded_end():
  114. query = _make_query(
  115. "field=sum(session)&interval=1h&start=2021-02-24T00:00:00Z&end=2021-02-25T00:00:00Z"
  116. )
  117. expected_timestamps = [
  118. "2021-02-24T00:00:00Z",
  119. "2021-02-24T01:00:00Z",
  120. "2021-02-24T02:00:00Z",
  121. "2021-02-24T03:00:00Z",
  122. "2021-02-24T04:00:00Z",
  123. "2021-02-24T05:00:00Z",
  124. "2021-02-24T06:00:00Z",
  125. "2021-02-24T07:00:00Z",
  126. "2021-02-24T08:00:00Z",
  127. "2021-02-24T09:00:00Z",
  128. "2021-02-24T10:00:00Z",
  129. "2021-02-24T11:00:00Z",
  130. "2021-02-24T12:00:00Z",
  131. "2021-02-24T13:00:00Z",
  132. "2021-02-24T14:00:00Z",
  133. "2021-02-24T15:00:00Z",
  134. "2021-02-24T16:00:00Z",
  135. "2021-02-24T17:00:00Z",
  136. "2021-02-24T18:00:00Z",
  137. "2021-02-24T19:00:00Z",
  138. "2021-02-24T20:00:00Z",
  139. "2021-02-24T21:00:00Z",
  140. "2021-02-24T22:00:00Z",
  141. "2021-02-24T23:00:00Z",
  142. "2021-02-25T00:00:00Z",
  143. ]
  144. actual_timestamps = get_timestamps(query)
  145. assert len(actual_timestamps) == 25
  146. assert actual_timestamps == expected_timestamps
  147. def test_simple_query():
  148. query = _make_query("statsPeriod=1d&interval=12h&field=sum(session)")
  149. assert query.query_columns == ["sessions"]
  150. def test_groupby_query():
  151. query = _make_query("statsPeriod=1d&interval=12h&field=sum(session)&groupBy=release")
  152. assert sorted(query.query_columns) == ["release", "sessions"]
  153. assert query.query_groupby == ["release"]
  154. def test_virtual_groupby_query():
  155. query = _make_query("statsPeriod=1d&interval=12h&field=sum(session)&groupBy=session.status")
  156. assert sorted(query.query_columns) == [
  157. "sessions",
  158. "sessions_abnormal",
  159. "sessions_crashed",
  160. "sessions_errored",
  161. ]
  162. assert query.query_groupby == []
  163. query = _make_query(
  164. "statsPeriod=1d&interval=12h&field=count_unique(user)&groupBy=session.status"
  165. )
  166. assert sorted(query.query_columns) == [
  167. "users",
  168. "users_abnormal",
  169. "users_crashed",
  170. "users_errored",
  171. ]
  172. assert query.query_groupby == []
  173. @freeze_time("2022-05-04T09:00:00.000Z")
  174. def _get_query_maker_params(project):
  175. # These parameters are computed in the API endpoint, before the
  176. # QueryDefinition is built. Since we're only testing the query
  177. # definition here, we can safely mock these.
  178. return {
  179. "start": datetime.now(),
  180. "end": datetime.now(),
  181. "organization_id": project.organization_id,
  182. }
  183. @django_db_all
  184. def test_filter_proj_slug_in_query(default_project):
  185. params = _get_query_maker_params(default_project)
  186. params["project_id"] = [default_project.id]
  187. query_def = _make_query(
  188. f"field=sum(session)&interval=2h&statsPeriod=2h&query=project%3A{default_project.slug}",
  189. params=params,
  190. )
  191. assert query_def.query == f"project:{default_project.slug}"
  192. assert query_def.params["project_id"] == [default_project.id]
  193. @django_db_all
  194. def test_filter_proj_slug_in_top_filter(default_project):
  195. params = _get_query_maker_params(default_project)
  196. params["project_id"] = [default_project.id]
  197. query_def = _make_query(
  198. f"field=sum(session)&interval=2h&statsPeriod=2h&project={default_project.id}",
  199. params=params,
  200. )
  201. assert query_def.query == ""
  202. assert query_def.params["project_id"] == [default_project.id]
  203. @django_db_all
  204. def test_filter_proj_slug_in_top_filter_and_query(default_project):
  205. params = _get_query_maker_params(default_project)
  206. params["project_id"] = [default_project.id]
  207. query_def = _make_query(
  208. f"field=sum(session)&interval=2h&statsPeriod=2h&project={default_project.id}&query=project%3A{default_project.slug}",
  209. params=params,
  210. )
  211. assert query_def.query == f"project:{default_project.slug}"
  212. assert query_def.params["project_id"] == [default_project.id]
  213. @django_db_all
  214. def test_proj_neither_in_top_filter_nor_query(default_project):
  215. params = _get_query_maker_params(default_project)
  216. query_def = _make_query(
  217. "field=sum(session)&interval=2h&statsPeriod=2h",
  218. params=params,
  219. )
  220. assert query_def.query == ""
  221. assert "project_id" not in query_def.params
  222. @django_db_all
  223. def test_filter_env_in_query(default_project):
  224. env = "prod"
  225. params = _get_query_maker_params(default_project)
  226. query_def = _make_query(
  227. f"field=sum(session)&interval=2h&statsPeriod=2h&query=environment%3A{env}",
  228. params=params,
  229. )
  230. assert query_def.query == f"environment:{env}"
  231. @django_db_all
  232. def test_filter_env_in_top_filter(default_project):
  233. env = "prod"
  234. params = _get_query_maker_params(default_project)
  235. params["environment"] = "prod"
  236. query_def = _make_query(
  237. f"field=sum(session)&interval=2h&statsPeriod=2h&environment={env}",
  238. params=params,
  239. )
  240. assert query_def.query == ""
  241. @django_db_all
  242. def test_filter_env_in_top_filter_and_query(default_project):
  243. env = "prod"
  244. params = _get_query_maker_params(default_project)
  245. params["environment"] = "prod"
  246. query_def = _make_query(
  247. f"field=sum(session)&interval=2h&statsPeriod=2h&environment={env}&query=environment%3A{env}",
  248. params=params,
  249. )
  250. assert query_def.query == f"environment:{env}"
  251. @django_db_all
  252. def test_env_neither_in_top_filter_nor_query(default_project):
  253. params = _get_query_maker_params(default_project)
  254. query_def = _make_query(
  255. "field=sum(session)&interval=2h&statsPeriod=2h",
  256. params=params,
  257. )
  258. assert query_def.query == ""
  259. @freeze_time("2020-12-18T11:14:17.105Z")
  260. def test_massage_empty():
  261. query = _make_query("statsPeriod=1d&interval=1d&field=sum(session)")
  262. expected_result = {
  263. "start": "2020-12-18T00:00:00Z",
  264. "end": "2020-12-18T11:15:00Z",
  265. "query": "",
  266. "intervals": ["2020-12-18T00:00:00Z"],
  267. "groups": [],
  268. }
  269. actual_result = result_sorted(massage_sessions_result(query, [], []))
  270. assert actual_result == expected_result
  271. @freeze_time("2020-12-18T11:14:17.105Z")
  272. def test_massage_unbalanced_results():
  273. query = _make_query("statsPeriod=1d&interval=1d&field=sum(session)&groupBy=release")
  274. result_totals = [
  275. {"release": "test-example-release", "sessions": 1},
  276. ]
  277. expected_result = {
  278. "start": "2020-12-18T00:00:00Z",
  279. "end": "2020-12-18T11:15:00Z",
  280. "query": "",
  281. "intervals": ["2020-12-18T00:00:00Z"],
  282. "groups": [
  283. {
  284. "by": {"release": "test-example-release"},
  285. "series": {"sum(session)": [0]},
  286. "totals": {"sum(session)": 1},
  287. }
  288. ],
  289. }
  290. actual_result = result_sorted(massage_sessions_result(query, result_totals, []))
  291. assert actual_result == expected_result
  292. result_totals = []
  293. result_timeseries = [
  294. {
  295. "release": "test-example-release",
  296. "sessions": 1,
  297. "bucketed_started": "2020-12-18T00:00:00+00:00",
  298. },
  299. ]
  300. expected_result = {
  301. "start": "2020-12-18T00:00:00Z",
  302. "end": "2020-12-18T11:15:00Z",
  303. "query": "",
  304. "intervals": ["2020-12-18T00:00:00Z"],
  305. "groups": [
  306. {
  307. "by": {"release": "test-example-release"},
  308. "series": {"sum(session)": [1]},
  309. "totals": {"sum(session)": 0},
  310. }
  311. ],
  312. }
  313. actual_result = result_sorted(massage_sessions_result(query, result_totals, result_timeseries))
  314. assert actual_result == expected_result
  315. @freeze_time("2020-12-18T11:14:17.105Z")
  316. def test_massage_simple_timeseries():
  317. """A timeseries is filled up when it only receives partial data"""
  318. query = _make_query("statsPeriod=1d&interval=6h&field=sum(session)")
  319. result_totals = [{"sessions": 4}]
  320. # snuba returns the datetimes as strings for now
  321. result_timeseries = [
  322. {"sessions": 2, "bucketed_started": "2020-12-18T06:00:00+00:00"},
  323. {"sessions": 2, "bucketed_started": "2020-12-17T12:00:00+00:00"},
  324. ]
  325. expected_result = {
  326. "start": "2020-12-17T12:00:00Z",
  327. "end": "2020-12-18T11:15:00Z",
  328. "query": "",
  329. "intervals": [
  330. "2020-12-17T12:00:00Z",
  331. "2020-12-17T18:00:00Z",
  332. "2020-12-18T00:00:00Z",
  333. "2020-12-18T06:00:00Z",
  334. ],
  335. "groups": [
  336. {"by": {}, "series": {"sum(session)": [2, 0, 0, 2]}, "totals": {"sum(session)": 4}}
  337. ],
  338. }
  339. actual_result = result_sorted(massage_sessions_result(query, result_totals, result_timeseries))
  340. assert actual_result == expected_result
  341. @freeze_time("2020-12-18T11:14:17.105Z")
  342. def test_massage_unordered_timeseries():
  343. query = _make_query("statsPeriod=1d&interval=6h&field=sum(session)")
  344. result_totals = [{"sessions": 10}]
  345. # snuba returns the datetimes as strings for now
  346. result_timeseries = [
  347. {"sessions": 3, "bucketed_started": "2020-12-18T00:00:00+00:00"},
  348. {"sessions": 2, "bucketed_started": "2020-12-17T18:00:00+00:00"},
  349. {"sessions": 4, "bucketed_started": "2020-12-18T06:00:00+00:00"},
  350. {"sessions": 1, "bucketed_started": "2020-12-17T12:00:00+00:00"},
  351. ]
  352. expected_result = {
  353. "start": "2020-12-17T12:00:00Z",
  354. "end": "2020-12-18T11:15:00Z",
  355. "query": "",
  356. "intervals": [
  357. "2020-12-17T12:00:00Z",
  358. "2020-12-17T18:00:00Z",
  359. "2020-12-18T00:00:00Z",
  360. "2020-12-18T06:00:00Z",
  361. ],
  362. "groups": [
  363. {"by": {}, "series": {"sum(session)": [1, 2, 3, 4]}, "totals": {"sum(session)": 10}}
  364. ],
  365. }
  366. actual_result = result_sorted(massage_sessions_result(query, result_totals, result_timeseries))
  367. assert actual_result == expected_result
  368. @freeze_time("2020-12-18T11:14:17.105Z")
  369. def test_massage_no_timeseries():
  370. query = _make_query("statsPeriod=1d&interval=6h&field=sum(session)&groupby=projects")
  371. result_totals = [{"sessions": 4}]
  372. # snuba returns the datetimes as strings for now
  373. result_timeseries = None
  374. expected_result = {
  375. "start": "2020-12-17T12:00:00Z",
  376. "end": "2020-12-18T11:15:00Z",
  377. "query": "",
  378. "intervals": [
  379. "2020-12-17T12:00:00Z",
  380. "2020-12-17T18:00:00Z",
  381. "2020-12-18T00:00:00Z",
  382. "2020-12-18T06:00:00Z",
  383. ],
  384. "groups": [{"by": {}, "totals": {"sum(session)": 4}}],
  385. }
  386. actual_result = result_sorted(massage_sessions_result(query, result_totals, result_timeseries))
  387. assert actual_result == expected_result
  388. def test_massage_exact_timeseries():
  389. query = _make_query(
  390. "start=2020-12-17T15:12:34Z&end=2020-12-18T11:14:17Z&interval=6h&field=sum(session)"
  391. )
  392. result_totals = [{"sessions": 4}]
  393. result_timeseries = [
  394. {"sessions": 2, "bucketed_started": "2020-12-18T06:00:00+00:00"},
  395. {"sessions": 2, "bucketed_started": "2020-12-17T12:00:00+00:00"},
  396. ]
  397. expected_result = {
  398. "start": "2020-12-17T12:00:00Z",
  399. "end": "2020-12-18T12:00:00Z",
  400. "query": "",
  401. "intervals": [
  402. "2020-12-17T12:00:00Z",
  403. "2020-12-17T18:00:00Z",
  404. "2020-12-18T00:00:00Z",
  405. "2020-12-18T06:00:00Z",
  406. ],
  407. "groups": [
  408. {"by": {}, "series": {"sum(session)": [2, 0, 0, 2]}, "totals": {"sum(session)": 4}}
  409. ],
  410. }
  411. actual_result = result_sorted(massage_sessions_result(query, result_totals, result_timeseries))
  412. assert actual_result == expected_result
  413. @freeze_time("2020-12-18T11:14:17.105Z")
  414. def test_massage_groupby_timeseries():
  415. query = _make_query("statsPeriod=1d&interval=6h&field=sum(session)&groupBy=release")
  416. result_totals = [
  417. {"release": "test-example-release", "sessions": 4},
  418. {"release": "test-example-release-2", "sessions": 1},
  419. ]
  420. # snuba returns the datetimes as strings for now
  421. result_timeseries = [
  422. {
  423. "release": "test-example-release",
  424. "sessions": 2,
  425. "bucketed_started": "2020-12-18T06:00:00+00:00",
  426. },
  427. {
  428. "release": "test-example-release-2",
  429. "sessions": 1,
  430. "bucketed_started": "2020-12-18T06:00:00+00:00",
  431. },
  432. {
  433. "release": "test-example-release",
  434. "sessions": 2,
  435. "bucketed_started": "2020-12-17T12:00:00+00:00",
  436. },
  437. ]
  438. expected_result = {
  439. "start": "2020-12-17T12:00:00Z",
  440. "end": "2020-12-18T11:15:00Z",
  441. "query": "",
  442. "intervals": [
  443. "2020-12-17T12:00:00Z",
  444. "2020-12-17T18:00:00Z",
  445. "2020-12-18T00:00:00Z",
  446. "2020-12-18T06:00:00Z",
  447. ],
  448. "groups": [
  449. {
  450. "by": {"release": "test-example-release"},
  451. "series": {"sum(session)": [2, 0, 0, 2]},
  452. "totals": {"sum(session)": 4},
  453. },
  454. {
  455. "by": {"release": "test-example-release-2"},
  456. "series": {"sum(session)": [0, 0, 0, 1]},
  457. "totals": {"sum(session)": 1},
  458. },
  459. ],
  460. }
  461. actual_result = result_sorted(massage_sessions_result(query, result_totals, result_timeseries))
  462. assert actual_result == expected_result
  463. @freeze_time("2020-12-18T13:25:15.769Z")
  464. def test_massage_virtual_groupby_timeseries():
  465. query = _make_query(
  466. "statsPeriod=1d&interval=6h&field=sum(session)&field=count_unique(user)&groupBy=session.status"
  467. )
  468. result_totals = [
  469. {
  470. "users": 1,
  471. "users_crashed": 1,
  472. "sessions": 31,
  473. "sessions_errored": 15,
  474. "users_errored": 1,
  475. "sessions_abnormal": 6,
  476. "sessions_crashed": 8,
  477. "users_abnormal": 0,
  478. }
  479. ]
  480. # snuba returns the datetimes as strings for now
  481. result_timeseries = [
  482. {
  483. "sessions_errored": 1,
  484. "users": 1,
  485. "users_crashed": 1,
  486. "sessions_abnormal": 0,
  487. "sessions": 3,
  488. "users_errored": 1,
  489. "users_abnormal": 0,
  490. "sessions_crashed": 1,
  491. "bucketed_started": "2020-12-18T12:00:00+00:00",
  492. },
  493. {
  494. "sessions_errored": 0,
  495. "users": 1,
  496. "users_crashed": 0,
  497. "sessions_abnormal": 0,
  498. "sessions": 3,
  499. "users_errored": 0,
  500. "users_abnormal": 0,
  501. "sessions_crashed": 0,
  502. "bucketed_started": "2020-12-18T06:00:00+00:00",
  503. },
  504. {
  505. "sessions_errored": 10,
  506. "users": 1,
  507. "users_crashed": 0,
  508. "sessions_abnormal": 2,
  509. "sessions": 15,
  510. "users_errored": 0,
  511. "users_abnormal": 0,
  512. "sessions_crashed": 4,
  513. "bucketed_started": "2020-12-18T00:00:00+00:00",
  514. },
  515. {
  516. "sessions_errored": 4,
  517. "users": 1,
  518. "users_crashed": 0,
  519. "sessions_abnormal": 4,
  520. "sessions": 10,
  521. "users_errored": 0,
  522. "users_abnormal": 0,
  523. "sessions_crashed": 3,
  524. "bucketed_started": "2020-12-17T18:00:00+00:00",
  525. },
  526. ]
  527. expected_result = {
  528. "start": "2020-12-17T18:00:00Z",
  529. "end": "2020-12-18T13:26:00Z",
  530. "query": "",
  531. "intervals": [
  532. "2020-12-17T18:00:00Z",
  533. "2020-12-18T00:00:00Z",
  534. "2020-12-18T06:00:00Z",
  535. "2020-12-18T12:00:00Z",
  536. ],
  537. "groups": [
  538. {
  539. "by": {"session.status": "abnormal"},
  540. "series": {"count_unique(user)": [0, 0, 0, 0], "sum(session)": [4, 2, 0, 0]},
  541. "totals": {"count_unique(user)": 0, "sum(session)": 6},
  542. },
  543. {
  544. "by": {"session.status": "crashed"},
  545. "series": {"count_unique(user)": [0, 0, 0, 1], "sum(session)": [3, 4, 0, 1]},
  546. "totals": {"count_unique(user)": 1, "sum(session)": 8},
  547. },
  548. {
  549. "by": {"session.status": "errored"},
  550. "series": {"count_unique(user)": [0, 0, 0, 0], "sum(session)": [0, 4, 0, 0]},
  551. "totals": {"count_unique(user)": 0, "sum(session)": 1},
  552. },
  553. {
  554. "by": {"session.status": "healthy"},
  555. "series": {"count_unique(user)": [1, 1, 1, 0], "sum(session)": [6, 5, 3, 2]},
  556. # while in one of the time slots, we have a healthy user, it is
  557. # the *same* user as the one experiencing a crash later on,
  558. # so in the *whole* time window, that one user is not counted as healthy,
  559. # so the `0` here is expected, as that's an example of the `count_unique` behavior.
  560. "totals": {"count_unique(user)": 0, "sum(session)": 16},
  561. },
  562. ],
  563. }
  564. actual_result = result_sorted(massage_sessions_result(query, result_totals, result_timeseries))
  565. assert actual_result == expected_result
  566. @freeze_time("2020-12-18T13:25:15.769Z")
  567. def test_clamping_in_massage_sessions_results_with_groupby_timeseries():
  568. query = _make_query(
  569. "statsPeriod=12h&interval=6h&field=sum(session)&field=count_unique(user)&groupBy=session.status"
  570. )
  571. # snuba returns the datetimes as strings for now
  572. result_timeseries = [
  573. {
  574. "sessions": 7,
  575. "sessions_errored": 3,
  576. "sessions_crashed": 2,
  577. "sessions_abnormal": 2,
  578. "users": 7,
  579. "users_errored": 3,
  580. "users_crashed": 2,
  581. "users_abnormal": 2,
  582. "bucketed_started": "2020-12-18T12:00:00+00:00",
  583. },
  584. {
  585. "sessions": 5,
  586. "sessions_errored": 10,
  587. "sessions_crashed": 0,
  588. "sessions_abnormal": 0,
  589. "users": 5,
  590. "users_errored": 10,
  591. "users_crashed": 0,
  592. "users_abnormal": 0,
  593. "bucketed_started": "2020-12-18T06:00:00+00:00",
  594. },
  595. ]
  596. expected_result = {
  597. "start": "2020-12-18T06:00:00Z",
  598. "end": "2020-12-18T13:26:00Z",
  599. "query": "",
  600. "intervals": [
  601. "2020-12-18T06:00:00Z",
  602. "2020-12-18T12:00:00Z",
  603. ],
  604. "groups": [
  605. {
  606. "by": {"session.status": "abnormal"},
  607. "series": {"count_unique(user)": [0, 2], "sum(session)": [0, 2]},
  608. "totals": {"count_unique(user)": 0, "sum(session)": 0},
  609. },
  610. {
  611. "by": {"session.status": "crashed"},
  612. "series": {"count_unique(user)": [0, 2], "sum(session)": [0, 2]},
  613. "totals": {"count_unique(user)": 0, "sum(session)": 0},
  614. },
  615. {
  616. "by": {"session.status": "errored"},
  617. "series": {"count_unique(user)": [10, 0], "sum(session)": [10, 0]},
  618. "totals": {"count_unique(user)": 0, "sum(session)": 0},
  619. },
  620. {
  621. "by": {"session.status": "healthy"},
  622. "series": {"count_unique(user)": [0, 4], "sum(session)": [0, 4]},
  623. "totals": {"count_unique(user)": 0, "sum(session)": 0},
  624. },
  625. ],
  626. }
  627. actual_result = result_sorted(massage_sessions_result(query, [], result_timeseries))
  628. assert actual_result == expected_result
  629. @freeze_time("2020-12-18T11:14:17.105Z")
  630. def test_nan_duration():
  631. query = _make_query(
  632. "statsPeriod=1d&interval=6h&field=avg(session.duration)&field=p50(session.duration)"
  633. )
  634. result_totals = [
  635. {
  636. "duration_avg": math.nan,
  637. "duration_quantiles": [math.inf, math.inf, math.inf, math.inf, math.inf, math.inf],
  638. },
  639. ]
  640. result_timeseries = [
  641. {
  642. "duration_avg": math.inf,
  643. "duration_quantiles": [math.inf, math.inf, math.inf, math.inf, math.inf, math.inf],
  644. "bucketed_started": "2020-12-18T06:00:00+00:00",
  645. },
  646. {
  647. "duration_avg": math.nan,
  648. "duration_quantiles": [math.nan, math.nan, math.nan, math.nan, math.nan, math.nan],
  649. "bucketed_started": "2020-12-17T12:00:00+00:00",
  650. },
  651. ]
  652. expected_result = {
  653. "start": "2020-12-17T12:00:00Z",
  654. "end": "2020-12-18T11:15:00Z",
  655. "query": "",
  656. "intervals": [
  657. "2020-12-17T12:00:00Z",
  658. "2020-12-17T18:00:00Z",
  659. "2020-12-18T00:00:00Z",
  660. "2020-12-18T06:00:00Z",
  661. ],
  662. "groups": [
  663. {
  664. "by": {},
  665. "series": {
  666. "avg(session.duration)": [None, None, None, None],
  667. "p50(session.duration)": [None, None, None, None],
  668. },
  669. "totals": {"avg(session.duration)": None, "p50(session.duration)": None},
  670. },
  671. ],
  672. }
  673. actual_result = result_sorted(massage_sessions_result(query, result_totals, result_timeseries))
  674. assert actual_result == expected_result