test_sessions_v2.py 27 KB

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