test_sessions_v2.py 26 KB

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