test_sessions_v2.py 26 KB

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