test_sessions.py 54 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590
  1. from __future__ import annotations
  2. import time
  3. from datetime import UTC, datetime, timedelta
  4. from unittest import mock
  5. import pytest
  6. from django.utils import timezone
  7. from sentry.release_health.base import OverviewStat
  8. from sentry.release_health.metrics import MetricsReleaseHealthBackend
  9. from sentry.release_health.sessions import SessionsReleaseHealthBackend
  10. from sentry.sentry_metrics.use_case_id_registry import UseCaseID
  11. from sentry.snuba.sessions import _make_stats
  12. from sentry.testutils.cases import BaseMetricsTestCase, SnubaTestCase, TestCase
  13. pytestmark = pytest.mark.sentry_metrics
  14. def parametrize_backend(cls):
  15. """
  16. hack to parametrize test-classes by backend. Ideally we'd move
  17. over to pytest-style tests so we can use `pytest.mark.parametrize`, but
  18. hopefully we won't have more than one backend in the future.
  19. """
  20. assert isinstance(cls.backend, SessionsReleaseHealthBackend)
  21. newcls = type(
  22. f"{cls.__name__}MetricsLayer",
  23. (BaseMetricsTestCase, cls),
  24. {
  25. "__doc__": f"Repeat tests from {cls} with metrics layer",
  26. "backend": MetricsReleaseHealthBackend(),
  27. "adjust_interval": True, # HACK interval adjustment for new MetricsLayer implementation
  28. },
  29. )
  30. globals()[newcls.__name__] = newcls
  31. return cls
  32. def format_timestamp(dt):
  33. if not isinstance(dt, datetime):
  34. dt = datetime.fromtimestamp(dt)
  35. return dt.strftime("%Y-%m-%dT%H:%M:%S+00:00")
  36. def make_24h_stats(ts, adjust_start=False):
  37. ret_val = _make_stats(datetime.fromtimestamp(ts, UTC), 3600, 24)
  38. if adjust_start:
  39. # HACK this adds another interval at the beginning in accordance with the new way of calculating intervals
  40. # https://www.notion.so/sentry/Metrics-Layer-get_intervals-bug-dce140607d054201a5e6629b070cb969
  41. ret_val.insert(0, [ret_val[0][0] - 3600, 0])
  42. return ret_val
  43. @parametrize_backend
  44. class SnubaSessionsTest(TestCase, SnubaTestCase):
  45. backend = SessionsReleaseHealthBackend()
  46. adjust_interval = False # HACK interval adjustment for new MetricsLayer implementation
  47. def setUp(self):
  48. super().setUp()
  49. self.received = time.time()
  50. self.session_started = time.time() // 60 * 60
  51. self.session_release = "foo@1.0.0"
  52. self.session_crashed_release = "foo@2.0.0"
  53. session_1 = "5d52fd05-fcc9-4bf3-9dc9-267783670341"
  54. session_2 = "5e910c1a-6941-460e-9843-24103fb6a63c"
  55. session_3 = "a148c0c5-06a2-423b-8901-6b43b812cf82"
  56. user_1 = "39887d89-13b2-4c84-8c23-5d13d2102666"
  57. self.store_session(
  58. self.build_session(
  59. distinct_id=user_1,
  60. session_id=session_1,
  61. status="exited",
  62. release=self.session_release,
  63. environment="prod",
  64. started=self.session_started,
  65. received=self.received,
  66. )
  67. )
  68. self.store_session(
  69. self.build_session(
  70. distinct_id=user_1,
  71. session_id=session_2,
  72. release=self.session_release,
  73. environment="prod",
  74. duration=None,
  75. started=self.session_started,
  76. received=self.received,
  77. )
  78. )
  79. self.store_session(
  80. self.build_session(
  81. distinct_id=user_1,
  82. session_id=session_2,
  83. seq=1,
  84. duration=30,
  85. status="exited",
  86. release=self.session_release,
  87. environment="prod",
  88. started=self.session_started,
  89. received=self.received,
  90. )
  91. )
  92. self.store_session(
  93. self.build_session(
  94. distinct_id=user_1,
  95. session_id=session_3,
  96. status="crashed",
  97. release=self.session_crashed_release,
  98. environment="prod",
  99. started=self.session_started,
  100. received=self.received,
  101. )
  102. )
  103. def test_get_oldest_health_data_for_releases(self):
  104. data = self.backend.get_oldest_health_data_for_releases(
  105. [(self.project.id, self.session_release)]
  106. )
  107. assert data == {
  108. (self.project.id, self.session_release): format_timestamp(
  109. self.session_started // 3600 * 3600
  110. )
  111. }
  112. def test_check_has_health_data(self):
  113. data = self.backend.check_has_health_data(
  114. [(self.project.id, self.session_release), (self.project.id, "dummy-release")]
  115. )
  116. assert data == {(self.project.id, self.session_release)}
  117. def test_check_has_health_data_without_releases_should_include_sessions_lte_90_days(self):
  118. """
  119. Test that ensures that `check_has_health_data` returns a set of projects that has health
  120. data within the last 90d if only a list of project ids is provided and any project with
  121. session data earlier than 90 days should be included
  122. """
  123. project2 = self.create_project(
  124. name="Bar2",
  125. slug="bar2",
  126. teams=[self.team],
  127. fire_project_created=True,
  128. organization=self.organization,
  129. )
  130. self.store_session(
  131. self.build_session(
  132. **{
  133. "project_id": project2.id,
  134. "org_id": project2.organization_id,
  135. "status": "exited",
  136. }
  137. )
  138. )
  139. data = self.backend.check_has_health_data([self.project.id, project2.id])
  140. assert data == {self.project.id, project2.id}
  141. def test_check_has_health_data_does_not_crash_when_sending_projects_list_as_set(self):
  142. data = self.backend.check_has_health_data({self.project.id})
  143. assert data == {self.project.id}
  144. def test_get_project_releases_by_stability(self):
  145. # Add an extra session with a different `distinct_id` so that sorting by users
  146. # is stable
  147. self.store_session(
  148. self.build_session(
  149. release=self.session_release,
  150. environment="prod",
  151. started=self.session_started,
  152. received=self.received,
  153. )
  154. )
  155. for scope in "sessions", "users":
  156. data = self.backend.get_project_releases_by_stability(
  157. [self.project.id], offset=0, limit=100, scope=scope, stats_period="24h"
  158. )
  159. assert data == [
  160. (self.project.id, self.session_release),
  161. (self.project.id, self.session_crashed_release),
  162. ]
  163. def test_get_project_releases_by_stability_for_crash_free_sort(self):
  164. """
  165. Test that ensures that using crash free rate sort options, returns a list of ASC releases
  166. according to the chosen crash_free sort option
  167. """
  168. # add another user to session_release to make sure that they are sorted correctly
  169. self.store_session(
  170. self.build_session(
  171. status="exited",
  172. release=self.session_release,
  173. environment="prod",
  174. started=self.session_started,
  175. received=self.received,
  176. )
  177. )
  178. for scope in "crash_free_sessions", "crash_free_users":
  179. data = self.backend.get_project_releases_by_stability(
  180. [self.project.id], offset=0, limit=100, scope=scope, stats_period="24h"
  181. )
  182. assert data == [
  183. (self.project.id, self.session_crashed_release),
  184. (self.project.id, self.session_release),
  185. ]
  186. def test_get_project_releases_by_stability_for_releases_with_users_data(self):
  187. """
  188. Test that ensures if releases contain no users data, then those releases should not be
  189. returned on `users` and `crash_free_users` sorts
  190. """
  191. self.store_session(
  192. self.build_session(
  193. distinct_id=None,
  194. release="release-with-no-users",
  195. environment="prod",
  196. started=self.session_started,
  197. received=self.received,
  198. )
  199. )
  200. data = self.backend.get_project_releases_by_stability(
  201. [self.project.id], offset=0, limit=100, scope="users", stats_period="24h"
  202. )
  203. assert set(data) == {
  204. (self.project.id, self.session_release),
  205. (self.project.id, self.session_crashed_release),
  206. }
  207. data = self.backend.get_project_releases_by_stability(
  208. [self.project.id], offset=0, limit=100, scope="crash_free_users", stats_period="24h"
  209. )
  210. assert set(data) == {
  211. (self.project.id, self.session_crashed_release),
  212. (self.project.id, self.session_release),
  213. }
  214. def test_get_release_adoption(self):
  215. data = self.backend.get_release_adoption(
  216. [
  217. (self.project.id, self.session_release),
  218. (self.project.id, self.session_crashed_release),
  219. (self.project.id, "dummy-release"),
  220. ]
  221. )
  222. assert data == {
  223. (self.project.id, self.session_release): {
  224. "sessions_24h": 2,
  225. "users_24h": 1,
  226. "adoption": 100.0,
  227. "sessions_adoption": 66.66666666666666,
  228. "project_sessions_24h": 3,
  229. "project_users_24h": 1,
  230. },
  231. (self.project.id, self.session_crashed_release): {
  232. "sessions_24h": 1,
  233. "users_24h": 1,
  234. "adoption": 100.0,
  235. "sessions_adoption": 33.33333333333333,
  236. "project_sessions_24h": 3,
  237. "project_users_24h": 1,
  238. },
  239. }
  240. def test_get_release_adoption_lowered(self):
  241. self.store_session(
  242. self.build_session(
  243. release=self.session_crashed_release,
  244. environment="prod",
  245. status="crashed",
  246. started=self.session_started,
  247. received=self.received,
  248. )
  249. )
  250. data = self.backend.get_release_adoption(
  251. [
  252. (self.project.id, self.session_release),
  253. (self.project.id, self.session_crashed_release),
  254. (self.project.id, "dummy-release"),
  255. ]
  256. )
  257. assert data == {
  258. (self.project.id, self.session_release): {
  259. "sessions_24h": 2,
  260. "users_24h": 1,
  261. "adoption": 50.0,
  262. "sessions_adoption": 50.0,
  263. "project_sessions_24h": 4,
  264. "project_users_24h": 2,
  265. },
  266. (self.project.id, self.session_crashed_release): {
  267. "sessions_24h": 2,
  268. "users_24h": 2,
  269. "adoption": 100.0,
  270. "sessions_adoption": 50.0,
  271. "project_sessions_24h": 4,
  272. "project_users_24h": 2,
  273. },
  274. }
  275. def test_fetching_release_sessions_time_bounds_for_different_release(self):
  276. """
  277. Test that ensures only session bounds for releases are calculated according
  278. to their respective release
  279. """
  280. # Same release session
  281. self.store_session(
  282. self.build_session(
  283. release=self.session_release,
  284. environment="prod",
  285. status="exited",
  286. started=self.session_started - 3600 * 2,
  287. received=self.received - 3600 * 2,
  288. )
  289. )
  290. # Different release session
  291. self.store_session(
  292. self.build_session(
  293. release=self.session_crashed_release,
  294. environment="prod",
  295. status="crashed",
  296. started=self.session_started - 3600 * 2,
  297. received=self.received - 3600 * 2,
  298. )
  299. )
  300. expected_formatted_lower_bound = (
  301. datetime.fromtimestamp(self.session_started - 3600 * 2)
  302. .replace(minute=0)
  303. .isoformat()[:19]
  304. + "Z"
  305. )
  306. expected_formatted_upper_bound = (
  307. datetime.fromtimestamp(self.session_started).replace(minute=0).isoformat()[:19] + "Z"
  308. )
  309. # Test for self.session_release
  310. data = self.backend.get_release_sessions_time_bounds(
  311. project_id=self.project.id,
  312. release=self.session_release,
  313. org_id=self.organization.id,
  314. environments=["prod"],
  315. )
  316. assert data == {
  317. "sessions_lower_bound": expected_formatted_lower_bound,
  318. "sessions_upper_bound": expected_formatted_upper_bound,
  319. }
  320. # Test for self.session_crashed_release
  321. data = self.backend.get_release_sessions_time_bounds(
  322. project_id=self.project.id,
  323. release=self.session_crashed_release,
  324. org_id=self.organization.id,
  325. environments=["prod"],
  326. )
  327. assert data == {
  328. "sessions_lower_bound": expected_formatted_lower_bound,
  329. "sessions_upper_bound": expected_formatted_upper_bound,
  330. }
  331. def test_fetching_release_sessions_time_bounds_for_different_release_with_no_sessions(self):
  332. """
  333. Test that ensures if no sessions are available for a specific release then the bounds
  334. should be returned as None
  335. """
  336. data = self.backend.get_release_sessions_time_bounds(
  337. project_id=self.project.id,
  338. release="different_release",
  339. org_id=self.organization.id,
  340. environments=["prod"],
  341. )
  342. assert data == {
  343. "sessions_lower_bound": None,
  344. "sessions_upper_bound": None,
  345. }
  346. def test_get_crash_free_breakdown(self):
  347. start = timezone.now() - timedelta(days=4)
  348. # it should work with and without environments
  349. for environments in [None, ["prod"]]:
  350. data = self.backend.get_crash_free_breakdown(
  351. project_id=self.project.id,
  352. release=self.session_release,
  353. start=start,
  354. environments=environments,
  355. )
  356. # Last returned date is generated within function, should be close to now:
  357. last_date = data[-1]["date"]
  358. assert timezone.now() - last_date < timedelta(seconds=1)
  359. assert data == [
  360. {
  361. "crash_free_sessions": None,
  362. "crash_free_users": None,
  363. "date": start + timedelta(days=1),
  364. "total_sessions": 0,
  365. "total_users": 0,
  366. },
  367. {
  368. "crash_free_sessions": None,
  369. "crash_free_users": None,
  370. "date": start + timedelta(days=2),
  371. "total_sessions": 0,
  372. "total_users": 0,
  373. },
  374. {
  375. "crash_free_sessions": 100.0,
  376. "crash_free_users": 100.0,
  377. "total_sessions": 2,
  378. "total_users": 1,
  379. "date": mock.ANY, # tested above
  380. },
  381. ]
  382. data = self.backend.get_crash_free_breakdown(
  383. project_id=self.project.id,
  384. release=self.session_crashed_release,
  385. start=start,
  386. environments=["prod"],
  387. )
  388. assert data == [
  389. {
  390. "crash_free_sessions": None,
  391. "crash_free_users": None,
  392. "date": start + timedelta(days=1),
  393. "total_sessions": 0,
  394. "total_users": 0,
  395. },
  396. {
  397. "crash_free_sessions": None,
  398. "crash_free_users": None,
  399. "date": start + timedelta(days=2),
  400. "total_sessions": 0,
  401. "total_users": 0,
  402. },
  403. {
  404. "crash_free_sessions": 0.0,
  405. "crash_free_users": 0.0,
  406. "total_sessions": 1,
  407. "total_users": 1,
  408. "date": mock.ANY,
  409. },
  410. ]
  411. data = self.backend.get_crash_free_breakdown(
  412. project_id=self.project.id,
  413. release="non-existing",
  414. start=start,
  415. environments=["prod"],
  416. )
  417. assert data == [
  418. {
  419. "crash_free_sessions": None,
  420. "crash_free_users": None,
  421. "date": start + timedelta(days=1),
  422. "total_sessions": 0,
  423. "total_users": 0,
  424. },
  425. {
  426. "crash_free_sessions": None,
  427. "crash_free_users": None,
  428. "date": start + timedelta(days=2),
  429. "total_sessions": 0,
  430. "total_users": 0,
  431. },
  432. {
  433. "crash_free_sessions": None,
  434. "crash_free_users": None,
  435. "total_sessions": 0,
  436. "total_users": 0,
  437. "date": mock.ANY,
  438. },
  439. ]
  440. def test_basic_release_model_adoptions(self):
  441. """
  442. Test that the basic (project,release) data is returned
  443. """
  444. proj_id = self.project.id
  445. data = self.backend.get_changed_project_release_model_adoptions([proj_id])
  446. assert set(data) == {(proj_id, "foo@1.0.0"), (proj_id, "foo@2.0.0")}
  447. def test_old_release_model_adoptions(self):
  448. """
  449. Test that old entries (older that 72 h) are not returned
  450. """
  451. _100h = 100 * 60 * 60 # 100 hours in seconds
  452. proj_id = self.project.id
  453. self.store_session(
  454. self.build_session(
  455. release="foo@3.0.0",
  456. environment="prod",
  457. status="crashed",
  458. started=self.session_started - _100h,
  459. received=self.received - 3600 * 2,
  460. )
  461. )
  462. data = self.backend.get_changed_project_release_model_adoptions([proj_id])
  463. assert set(data) == {(proj_id, "foo@1.0.0"), (proj_id, "foo@2.0.0")}
  464. def test_multi_proj_release_model_adoptions(self):
  465. """Test that the api works with multiple projects"""
  466. proj_id = self.project.id
  467. new_proj_id = proj_id + 1
  468. self.store_session(
  469. self.build_session(
  470. project_id=new_proj_id,
  471. release="foo@3.0.0",
  472. environment="prod",
  473. status="crashed",
  474. started=self.session_started,
  475. received=self.received - 3600 * 2,
  476. )
  477. )
  478. data = self.backend.get_changed_project_release_model_adoptions([proj_id, new_proj_id])
  479. assert set(data) == {
  480. (proj_id, "foo@1.0.0"),
  481. (proj_id, "foo@2.0.0"),
  482. (new_proj_id, "foo@3.0.0"),
  483. }
  484. @staticmethod
  485. def _add_timestamps_to_series(series, start: datetime):
  486. one_day = 24 * 60 * 60
  487. day0 = one_day * int(start.timestamp() / one_day)
  488. def ts(days: int) -> int:
  489. return day0 + days * one_day
  490. return [(ts(i + 1), data) for i, data in enumerate(series)]
  491. def _test_get_project_release_stats(
  492. self, stat: OverviewStat, release: str, expected_series, expected_totals
  493. ):
  494. end = timezone.now()
  495. start = end - timedelta(days=4)
  496. stats, totals = self.backend.get_project_release_stats(
  497. self.project.id,
  498. release=release,
  499. stat=stat,
  500. rollup=86400,
  501. start=start,
  502. end=end,
  503. )
  504. # one system returns lists instead of tuples
  505. normed = [(ts, data) for ts, data in stats]
  506. assert normed == self._add_timestamps_to_series(expected_series, start)
  507. assert totals == expected_totals
  508. def test_get_project_release_stats_users(self):
  509. self._test_get_project_release_stats(
  510. "users",
  511. self.session_release,
  512. [
  513. {
  514. "duration_p50": None,
  515. "duration_p90": None,
  516. "users": 0,
  517. "users_abnormal": 0,
  518. "users_crashed": 0,
  519. "users_errored": 0,
  520. "users_healthy": 0,
  521. },
  522. {
  523. "duration_p50": None,
  524. "duration_p90": None,
  525. "users": 0,
  526. "users_abnormal": 0,
  527. "users_crashed": 0,
  528. "users_errored": 0,
  529. "users_healthy": 0,
  530. },
  531. {
  532. "duration_p50": None,
  533. "duration_p90": None,
  534. "users": 0,
  535. "users_abnormal": 0,
  536. "users_crashed": 0,
  537. "users_errored": 0,
  538. "users_healthy": 0,
  539. },
  540. {
  541. "duration_p50": 45.0,
  542. "duration_p90": 57.0,
  543. "users": 1,
  544. "users_abnormal": 0,
  545. "users_crashed": 0,
  546. "users_errored": 0,
  547. "users_healthy": 1,
  548. },
  549. ],
  550. {
  551. "users": 1,
  552. "users_abnormal": 0,
  553. "users_crashed": 0,
  554. "users_errored": 0,
  555. "users_healthy": 1,
  556. },
  557. )
  558. def test_get_project_release_stats_users_crashed(self):
  559. self._test_get_project_release_stats(
  560. "users",
  561. self.session_crashed_release,
  562. [
  563. {
  564. "duration_p50": None,
  565. "duration_p90": None,
  566. "users": 0,
  567. "users_abnormal": 0,
  568. "users_crashed": 0,
  569. "users_errored": 0,
  570. "users_healthy": 0,
  571. },
  572. {
  573. "duration_p50": None,
  574. "duration_p90": None,
  575. "users": 0,
  576. "users_abnormal": 0,
  577. "users_crashed": 0,
  578. "users_errored": 0,
  579. "users_healthy": 0,
  580. },
  581. {
  582. "duration_p50": None,
  583. "duration_p90": None,
  584. "users": 0,
  585. "users_abnormal": 0,
  586. "users_crashed": 0,
  587. "users_errored": 0,
  588. "users_healthy": 0,
  589. },
  590. {
  591. "duration_p50": None,
  592. "duration_p90": None,
  593. "users": 1,
  594. "users_abnormal": 0,
  595. "users_crashed": 1,
  596. "users_errored": 0,
  597. "users_healthy": 0,
  598. },
  599. ],
  600. {
  601. "users": 1,
  602. "users_abnormal": 0,
  603. "users_crashed": 1,
  604. "users_errored": 0,
  605. "users_healthy": 0,
  606. },
  607. )
  608. def test_get_project_release_stats_sessions(self):
  609. self._test_get_project_release_stats(
  610. "sessions",
  611. self.session_release,
  612. [
  613. {
  614. "duration_p50": None,
  615. "duration_p90": None,
  616. "sessions": 0,
  617. "sessions_abnormal": 0,
  618. "sessions_crashed": 0,
  619. "sessions_errored": 0,
  620. "sessions_healthy": 0,
  621. },
  622. {
  623. "duration_p50": None,
  624. "duration_p90": None,
  625. "sessions": 0,
  626. "sessions_abnormal": 0,
  627. "sessions_crashed": 0,
  628. "sessions_errored": 0,
  629. "sessions_healthy": 0,
  630. },
  631. {
  632. "duration_p50": None,
  633. "duration_p90": None,
  634. "sessions": 0,
  635. "sessions_abnormal": 0,
  636. "sessions_crashed": 0,
  637. "sessions_errored": 0,
  638. "sessions_healthy": 0,
  639. },
  640. {
  641. "duration_p50": 45.0,
  642. "duration_p90": 57.0,
  643. "sessions": 2,
  644. "sessions_abnormal": 0,
  645. "sessions_crashed": 0,
  646. "sessions_errored": 0,
  647. "sessions_healthy": 2,
  648. },
  649. ],
  650. {
  651. "sessions": 2,
  652. "sessions_abnormal": 0,
  653. "sessions_crashed": 0,
  654. "sessions_errored": 0,
  655. "sessions_healthy": 2,
  656. },
  657. )
  658. def test_get_project_release_stats_sessions_crashed(self):
  659. self._test_get_project_release_stats(
  660. "sessions",
  661. self.session_crashed_release,
  662. [
  663. {
  664. "duration_p50": None,
  665. "duration_p90": None,
  666. "sessions": 0,
  667. "sessions_abnormal": 0,
  668. "sessions_crashed": 0,
  669. "sessions_errored": 0,
  670. "sessions_healthy": 0,
  671. },
  672. {
  673. "duration_p50": None,
  674. "duration_p90": None,
  675. "sessions": 0,
  676. "sessions_abnormal": 0,
  677. "sessions_crashed": 0,
  678. "sessions_errored": 0,
  679. "sessions_healthy": 0,
  680. },
  681. {
  682. "duration_p50": None,
  683. "duration_p90": None,
  684. "sessions": 0,
  685. "sessions_abnormal": 0,
  686. "sessions_crashed": 0,
  687. "sessions_errored": 0,
  688. "sessions_healthy": 0,
  689. },
  690. {
  691. "duration_p50": None,
  692. "duration_p90": None,
  693. "sessions": 1,
  694. "sessions_abnormal": 0,
  695. "sessions_crashed": 1,
  696. "sessions_errored": 0,
  697. "sessions_healthy": 0,
  698. },
  699. ],
  700. {
  701. "sessions": 1,
  702. "sessions_abnormal": 0,
  703. "sessions_crashed": 1,
  704. "sessions_errored": 0,
  705. "sessions_healthy": 0,
  706. },
  707. )
  708. def test_get_project_release_stats_no_sessions(self):
  709. """
  710. Test still returning correct data when no sessions are available
  711. :return:
  712. """
  713. self._test_get_project_release_stats(
  714. "sessions",
  715. "INEXISTENT-RELEASE",
  716. [
  717. {
  718. "duration_p50": None,
  719. "duration_p90": None,
  720. "sessions": 0,
  721. "sessions_abnormal": 0,
  722. "sessions_crashed": 0,
  723. "sessions_errored": 0,
  724. "sessions_healthy": 0,
  725. },
  726. {
  727. "duration_p50": None,
  728. "duration_p90": None,
  729. "sessions": 0,
  730. "sessions_abnormal": 0,
  731. "sessions_crashed": 0,
  732. "sessions_errored": 0,
  733. "sessions_healthy": 0,
  734. },
  735. {
  736. "duration_p50": None,
  737. "duration_p90": None,
  738. "sessions": 0,
  739. "sessions_abnormal": 0,
  740. "sessions_crashed": 0,
  741. "sessions_errored": 0,
  742. "sessions_healthy": 0,
  743. },
  744. {
  745. "duration_p50": None,
  746. "duration_p90": None,
  747. "sessions": 0,
  748. "sessions_abnormal": 0,
  749. "sessions_crashed": 0,
  750. "sessions_errored": 0,
  751. "sessions_healthy": 0,
  752. },
  753. ],
  754. {
  755. "sessions": 0,
  756. "sessions_abnormal": 0,
  757. "sessions_crashed": 0,
  758. "sessions_errored": 0,
  759. "sessions_healthy": 0,
  760. },
  761. )
  762. def test_get_project_release_stats_no_users(self):
  763. self._test_get_project_release_stats(
  764. "users",
  765. "INEXISTENT-RELEASE",
  766. [
  767. {
  768. "duration_p50": None,
  769. "duration_p90": None,
  770. "users": 0,
  771. "users_abnormal": 0,
  772. "users_crashed": 0,
  773. "users_errored": 0,
  774. "users_healthy": 0,
  775. },
  776. {
  777. "duration_p50": None,
  778. "duration_p90": None,
  779. "users": 0,
  780. "users_abnormal": 0,
  781. "users_crashed": 0,
  782. "users_errored": 0,
  783. "users_healthy": 0,
  784. },
  785. {
  786. "duration_p50": None,
  787. "duration_p90": None,
  788. "users": 0,
  789. "users_abnormal": 0,
  790. "users_crashed": 0,
  791. "users_errored": 0,
  792. "users_healthy": 0,
  793. },
  794. {
  795. "duration_p50": None,
  796. "duration_p90": None,
  797. "users": 0,
  798. "users_abnormal": 0,
  799. "users_crashed": 0,
  800. "users_errored": 0,
  801. "users_healthy": 0,
  802. },
  803. ],
  804. {
  805. "users": 0,
  806. "users_abnormal": 0,
  807. "users_crashed": 0,
  808. "users_errored": 0,
  809. "users_healthy": 0,
  810. },
  811. )
  812. @parametrize_backend
  813. class GetCrashFreeRateTestCase(TestCase, SnubaTestCase):
  814. """
  815. TestClass that tests that `get_current_and_previous_crash_free_rates` returns the correct
  816. `currentCrashFreeRate` and `previousCrashFreeRate` for each project
  817. TestData:
  818. Project 1:
  819. In the last 24h -> 2 Exited Sessions / 2 Total Sessions -> 100% Crash free rate
  820. In the previous 24h (>24h & <48h) -> 2 Exited + 1 Crashed Sessions / 3 Sessions -> 66.7%
  821. Project 2:
  822. In the last 24h -> 1 Exited + 1 Crashed / 2 Total Sessions -> 50% Crash free rate
  823. In the previous 24h (>24h & <48h) -> 0 Sessions -> None
  824. Project 3:
  825. In the last 24h -> 0 Sessions -> None
  826. In the previous 24h (>24h & <48h) -> 4 Exited + 1 Crashed / 5 Total Sessions -> 80%
  827. """
  828. backend = SessionsReleaseHealthBackend()
  829. def setUp(self):
  830. super().setUp()
  831. self.session_started = time.time() // 60 * 60
  832. self.session_started_gt_24_lt_48 = self.session_started - 30 * 60 * 60
  833. self.project2 = self.create_project(
  834. name="Bar2",
  835. slug="bar2",
  836. teams=[self.team],
  837. fire_project_created=True,
  838. organization=self.organization,
  839. )
  840. self.project3 = self.create_project(
  841. name="Bar3",
  842. slug="bar3",
  843. teams=[self.team],
  844. fire_project_created=True,
  845. organization=self.organization,
  846. )
  847. # Project 1
  848. for _ in range(0, 2):
  849. self.store_session(
  850. self.build_session(
  851. **{
  852. "project_id": self.project.id,
  853. "org_id": self.project.organization_id,
  854. "status": "exited",
  855. }
  856. )
  857. )
  858. for idx in range(0, 3):
  859. status = "exited"
  860. if idx == 2:
  861. status = "crashed"
  862. self.store_session(
  863. self.build_session(
  864. **{
  865. "project_id": self.project.id,
  866. "org_id": self.project.organization_id,
  867. "status": status,
  868. "started": self.session_started_gt_24_lt_48,
  869. }
  870. )
  871. )
  872. # Project 2
  873. for i in range(0, 2):
  874. status = "exited"
  875. if i == 1:
  876. status = "crashed"
  877. self.store_session(
  878. self.build_session(
  879. **{
  880. "project_id": self.project2.id,
  881. "org_id": self.project2.organization_id,
  882. "status": status,
  883. }
  884. )
  885. )
  886. # Project 3
  887. for i in range(0, 5):
  888. status = "exited"
  889. if i == 4:
  890. status = "crashed"
  891. self.store_session(
  892. self.build_session(
  893. **{
  894. "project_id": self.project3.id,
  895. "org_id": self.project3.organization_id,
  896. "status": status,
  897. "started": self.session_started_gt_24_lt_48,
  898. }
  899. )
  900. )
  901. def test_get_current_and_previous_crash_free_rates(self):
  902. now = timezone.now().replace(minute=15, second=23)
  903. last_24h_start = now - 24 * timedelta(hours=1)
  904. last_48h_start = now - 2 * 24 * timedelta(hours=1)
  905. data = self.backend.get_current_and_previous_crash_free_rates(
  906. org_id=self.organization.id,
  907. project_ids=[self.project.id, self.project2.id, self.project3.id],
  908. current_start=last_24h_start,
  909. current_end=now,
  910. previous_start=last_48h_start,
  911. previous_end=last_24h_start,
  912. rollup=3600,
  913. )
  914. assert data == {
  915. self.project.id: {
  916. "currentCrashFreeRate": 100,
  917. "previousCrashFreeRate": 66.66666666666667,
  918. },
  919. self.project2.id: {"currentCrashFreeRate": 50.0, "previousCrashFreeRate": None},
  920. self.project3.id: {"currentCrashFreeRate": None, "previousCrashFreeRate": 80.0},
  921. }
  922. def test_get_current_and_previous_crash_free_rates_with_zero_sessions(self):
  923. now = timezone.now().replace(minute=15, second=23)
  924. last_48h_start = now - 2 * 24 * timedelta(hours=1)
  925. last_72h_start = now - 3 * 24 * timedelta(hours=1)
  926. last_96h_start = now - 4 * 24 * timedelta(hours=1)
  927. data = self.backend.get_current_and_previous_crash_free_rates(
  928. org_id=self.organization.id,
  929. project_ids=[self.project.id],
  930. current_start=last_72h_start,
  931. current_end=last_48h_start,
  932. previous_start=last_96h_start,
  933. previous_end=last_72h_start,
  934. rollup=3600,
  935. )
  936. assert data == {
  937. self.project.id: {
  938. "currentCrashFreeRate": None,
  939. "previousCrashFreeRate": None,
  940. },
  941. }
  942. @parametrize_backend
  943. class GetProjectReleasesCountTest(TestCase, SnubaTestCase):
  944. backend = SessionsReleaseHealthBackend()
  945. def test_empty(self):
  946. # Test no errors when no session data
  947. org = self.create_organization()
  948. proj = self.create_project(organization=org)
  949. assert (
  950. self.backend.get_project_releases_count(
  951. org.id, [proj.id], "crash_free_users", stats_period="14d"
  952. )
  953. == 0
  954. )
  955. def test_with_other_metrics(self):
  956. if not self.backend.is_metrics_based():
  957. return
  958. assert isinstance(self, BaseMetricsTestCase)
  959. # Test no errors when no session data
  960. org = self.create_organization()
  961. proj = self.create_project(organization=org)
  962. # Insert a different set metric:
  963. for value in 1, 2, 3:
  964. self.store_metric(
  965. org_id=org.id,
  966. project_id=proj.id,
  967. name="foobarbaz", # any other metric ID
  968. timestamp=int(time.time()),
  969. tags={},
  970. type="set",
  971. value=value,
  972. use_case_id=UseCaseID.SESSIONS,
  973. )
  974. assert (
  975. self.backend.get_project_releases_count(
  976. org.id, [proj.id], "crash_free_users", stats_period="14d"
  977. )
  978. == 0
  979. )
  980. def test(self):
  981. project_release_1 = self.create_release(self.project)
  982. other_project = self.create_project()
  983. other_project_release_1 = self.create_release(other_project)
  984. self.bulk_store_sessions(
  985. [
  986. self.build_session(
  987. environment=self.environment.name, release=project_release_1.version
  988. ),
  989. self.build_session(
  990. environment="staging",
  991. project_id=other_project.id,
  992. release=other_project_release_1.version,
  993. ),
  994. ]
  995. )
  996. assert (
  997. self.backend.get_project_releases_count(
  998. self.organization.id, [self.project.id], "sessions"
  999. )
  1000. == 1
  1001. )
  1002. assert (
  1003. self.backend.get_project_releases_count(
  1004. self.organization.id, [self.project.id], "users"
  1005. )
  1006. == 1
  1007. )
  1008. assert (
  1009. self.backend.get_project_releases_count(
  1010. self.organization.id, [self.project.id, other_project.id], "sessions"
  1011. )
  1012. == 2
  1013. )
  1014. assert (
  1015. self.backend.get_project_releases_count(
  1016. self.organization.id,
  1017. [self.project.id, other_project.id],
  1018. "users",
  1019. )
  1020. == 2
  1021. )
  1022. assert (
  1023. self.backend.get_project_releases_count(
  1024. self.organization.id,
  1025. [self.project.id, other_project.id],
  1026. "sessions",
  1027. environments=[self.environment.name],
  1028. )
  1029. == 1
  1030. )
  1031. @parametrize_backend
  1032. class CheckReleasesHaveHealthDataTest(TestCase, SnubaTestCase):
  1033. backend = SessionsReleaseHealthBackend()
  1034. def run_test(self, expected, projects, releases, start=None, end=None):
  1035. if not start:
  1036. start = datetime.now() - timedelta(days=1)
  1037. if not end:
  1038. end = datetime.now()
  1039. assert self.backend.check_releases_have_health_data(
  1040. self.organization.id,
  1041. [p.id for p in projects],
  1042. [r.version for r in releases],
  1043. start,
  1044. end,
  1045. ) == {v.version for v in expected}
  1046. def test_empty(self):
  1047. # Test no errors when no session data
  1048. project_release_1 = self.create_release(self.project)
  1049. self.run_test([], [self.project], [project_release_1])
  1050. def test(self):
  1051. other_project = self.create_project()
  1052. release_1 = self.create_release(
  1053. self.project, version="1", additional_projects=[other_project]
  1054. )
  1055. release_2 = self.create_release(other_project, version="2")
  1056. self.bulk_store_sessions(
  1057. [
  1058. self.build_session(release=release_1),
  1059. self.build_session(project_id=other_project, release=release_1),
  1060. self.build_session(project_id=other_project, release=release_2),
  1061. ]
  1062. )
  1063. self.run_test([release_1], [self.project], [release_1])
  1064. self.run_test([release_1], [self.project], [release_1, release_2])
  1065. self.run_test([release_1], [other_project], [release_1])
  1066. self.run_test([release_1, release_2], [other_project], [release_1, release_2])
  1067. self.run_test([release_1, release_2], [self.project, other_project], [release_1, release_2])
  1068. @parametrize_backend
  1069. class CheckNumberOfSessions(TestCase, SnubaTestCase):
  1070. backend = SessionsReleaseHealthBackend()
  1071. def setUp(self):
  1072. super().setUp()
  1073. self.dev_env = self.create_environment(name="development", project=self.project)
  1074. self.prod_env = self.create_environment(name="production", project=self.project)
  1075. self.test_env = self.create_environment(name="test", project=self.project)
  1076. self.another_project = self.create_project()
  1077. self.third_project = self.create_project()
  1078. # now_dt should be set to 17:40 of some day not in the future and (system time - now_dt)
  1079. # must be less than 90 days for the metrics DB TTL
  1080. ONE_DAY_AGO = timezone.now() - timedelta(days=1)
  1081. self.now_dt = ONE_DAY_AGO.replace(hour=17, minute=40, second=0)
  1082. self._5_min_ago_dt = self.now_dt - timedelta(minutes=5)
  1083. self._30_min_ago_dt = self.now_dt - timedelta(minutes=30)
  1084. self._1_h_ago_dt = self.now_dt - timedelta(hours=1)
  1085. self._2_h_ago_dt = self.now_dt - timedelta(hours=2)
  1086. self._3_h_ago_dt = self.now_dt - timedelta(hours=3)
  1087. self.now = self.now_dt.timestamp()
  1088. self._5_min_ago = self._5_min_ago_dt.timestamp()
  1089. self._30_min_ago = self._30_min_ago_dt.timestamp()
  1090. self._1_h_ago = self._1_h_ago_dt.timestamp()
  1091. self._2_h_ago = self._2_h_ago_dt.timestamp()
  1092. self._3_h_ago = self._3_h_ago_dt.timestamp()
  1093. def test_no_sessions(self):
  1094. """
  1095. Tests that when there are no sessions the function behaves and returns 0
  1096. """
  1097. actual = self.backend.get_project_sessions_count(
  1098. project_id=self.project.id,
  1099. environment_id=None,
  1100. rollup=60,
  1101. start=self._30_min_ago_dt,
  1102. end=self.now_dt,
  1103. )
  1104. assert 0 == actual
  1105. def test_sessions_in_environment(self):
  1106. """
  1107. Tests that it correctly picks up the sessions for the selected environment
  1108. in the selected time, not counting other environments and other times
  1109. """
  1110. dev = self.dev_env.name
  1111. prod = self.prod_env.name
  1112. self.bulk_store_sessions(
  1113. [
  1114. self.build_session(
  1115. environment=dev, received=self._5_min_ago, started=self._5_min_ago
  1116. ),
  1117. self.build_session(
  1118. environment=prod, received=self._5_min_ago, started=self._5_min_ago
  1119. ),
  1120. self.build_session(
  1121. environment=prod, received=self._5_min_ago, started=self._5_min_ago
  1122. ),
  1123. self.build_session(environment=prod, received=self._2_h_ago, started=self._2_h_ago),
  1124. ]
  1125. )
  1126. actual = self.backend.get_project_sessions_count(
  1127. project_id=self.project.id,
  1128. environment_id=self.prod_env.id,
  1129. rollup=60,
  1130. start=self._1_h_ago_dt,
  1131. end=self.now_dt,
  1132. )
  1133. assert actual == 2
  1134. def test_environment_without_sessions(self):
  1135. """
  1136. We should get zero sessions, even if the environment name has not been indexed
  1137. by the metrics indexer.
  1138. """
  1139. env_without_sessions = self.create_environment(
  1140. name="this_has_no_sessions", project=self.project
  1141. )
  1142. self.bulk_store_sessions(
  1143. [
  1144. self.build_session(
  1145. environment=self.prod_env.name,
  1146. received=self._5_min_ago,
  1147. started=self._5_min_ago,
  1148. ),
  1149. self.build_session(
  1150. environment=None, received=self._5_min_ago, started=self._5_min_ago
  1151. ),
  1152. ]
  1153. )
  1154. count_env_all = self.backend.get_project_sessions_count(
  1155. project_id=self.project.id,
  1156. environment_id=None,
  1157. rollup=60,
  1158. start=self._1_h_ago_dt,
  1159. end=self.now_dt,
  1160. )
  1161. assert count_env_all == 2
  1162. count_env_new = self.backend.get_project_sessions_count(
  1163. project_id=self.project.id,
  1164. environment_id=env_without_sessions.id,
  1165. rollup=60,
  1166. start=self._1_h_ago_dt,
  1167. end=self.now_dt,
  1168. )
  1169. assert count_env_new == 0
  1170. def test_sessions_in_all_environments(self):
  1171. """
  1172. When the environment is not specified sessions from all environments are counted
  1173. """
  1174. dev = self.dev_env.name
  1175. prod = self.prod_env.name
  1176. self.bulk_store_sessions(
  1177. [
  1178. self.build_session(
  1179. environment=dev, received=self._5_min_ago, started=self._5_min_ago
  1180. ),
  1181. self.build_session(
  1182. environment=prod, received=self._5_min_ago, started=self._5_min_ago
  1183. ),
  1184. self.build_session(
  1185. environment=prod, received=self._5_min_ago, started=self._5_min_ago
  1186. ),
  1187. self.build_session(environment=prod, received=self._2_h_ago, started=self._2_h_ago),
  1188. self.build_session(environment=dev, received=self._2_h_ago, started=self._2_h_ago),
  1189. ]
  1190. )
  1191. actual = self.backend.get_project_sessions_count(
  1192. project_id=self.project.id,
  1193. environment_id=None,
  1194. rollup=60,
  1195. start=self._1_h_ago_dt,
  1196. end=self.now_dt,
  1197. )
  1198. assert actual == 3
  1199. def test_sessions_from_multiple_projects(self):
  1200. """
  1201. Only sessions from the specified project are considered
  1202. """
  1203. dev = self.dev_env.name
  1204. prod = self.prod_env.name
  1205. self.bulk_store_sessions(
  1206. [
  1207. self.build_session(
  1208. environment=dev, received=self._5_min_ago, started=self._5_min_ago
  1209. ),
  1210. self.build_session(
  1211. environment=prod, received=self._5_min_ago, started=self._5_min_ago
  1212. ),
  1213. self.build_session(
  1214. environment=prod,
  1215. received=self._5_min_ago,
  1216. project_id=self.another_project.id,
  1217. started=self._5_min_ago,
  1218. ),
  1219. ]
  1220. )
  1221. actual = self.backend.get_project_sessions_count(
  1222. project_id=self.project.id,
  1223. environment_id=None,
  1224. rollup=60,
  1225. start=self._1_h_ago_dt,
  1226. end=self.now_dt,
  1227. )
  1228. assert actual == 2
  1229. def test_sessions_per_project_no_sessions(self):
  1230. """
  1231. Tests that no sessions are returned
  1232. """
  1233. actual = self.backend.get_num_sessions_per_project(
  1234. project_ids=[self.project.id, self.another_project.id],
  1235. environment_ids=None,
  1236. rollup=60,
  1237. start=self._30_min_ago_dt,
  1238. end=self.now_dt,
  1239. )
  1240. assert [] == actual
  1241. def test_sesions_per_project_multiple_projects(self):
  1242. dev = self.dev_env.name
  1243. prod = self.prod_env.name
  1244. test = self.test_env.name
  1245. p1 = self.project
  1246. p2 = self.another_project
  1247. p3 = self.third_project
  1248. self.bulk_store_sessions(
  1249. [
  1250. # counted in p1
  1251. self.build_session(
  1252. environment=dev, received=self._5_min_ago, started=self._5_min_ago
  1253. ),
  1254. self.build_session(
  1255. environment=prod, received=self._5_min_ago, started=self._5_min_ago
  1256. ),
  1257. self.build_session(
  1258. environment=dev, received=self._30_min_ago, started=self._30_min_ago
  1259. ),
  1260. # ignored in p1
  1261. # ignored env
  1262. self.build_session(
  1263. environment=test, received=self._30_min_ago, started=self._30_min_ago
  1264. ),
  1265. # too old
  1266. self.build_session(environment=prod, received=self._3_h_ago, started=self._3_h_ago),
  1267. # counted in p2
  1268. self.build_session(
  1269. environment=dev,
  1270. received=self._5_min_ago,
  1271. project_id=p2.id,
  1272. started=self._5_min_ago,
  1273. ),
  1274. # ignored in p2
  1275. # ignored env
  1276. self.build_session(
  1277. environment=test,
  1278. received=self._5_min_ago,
  1279. project_id=p2.id,
  1280. started=self._5_min_ago,
  1281. ),
  1282. # too old
  1283. self.build_session(
  1284. environment=prod,
  1285. received=self._3_h_ago,
  1286. project_id=p2.id,
  1287. started=self._3_h_ago,
  1288. ),
  1289. # ignored p3
  1290. self.build_session(
  1291. environment=dev,
  1292. received=self._5_min_ago,
  1293. project_id=p3.id,
  1294. started=self._5_min_ago,
  1295. ),
  1296. ]
  1297. )
  1298. actual = self.backend.get_num_sessions_per_project(
  1299. project_ids=[self.project.id, self.another_project.id],
  1300. environment_ids=[self.dev_env.id, self.prod_env.id],
  1301. rollup=60,
  1302. start=self._2_h_ago_dt,
  1303. end=self.now_dt,
  1304. )
  1305. assert set(actual) == {(p1.id, 3), (p2.id, 1)}
  1306. eids_tests: tuple[list[int] | None, ...] = ([], None)
  1307. for eids in eids_tests:
  1308. actual = self.backend.get_num_sessions_per_project(
  1309. project_ids=[self.project.id, self.another_project.id],
  1310. environment_ids=eids,
  1311. rollup=60,
  1312. start=self._2_h_ago_dt,
  1313. end=self.now_dt,
  1314. )
  1315. assert set(actual) == {(p1.id, 4), (p2.id, 2)}
  1316. @parametrize_backend
  1317. class InitWithoutUserTestCase(TestCase, SnubaTestCase):
  1318. backend = SessionsReleaseHealthBackend()
  1319. def setUp(self):
  1320. super().setUp()
  1321. self.received = time.time()
  1322. self.session_started = time.time() // 60 * 60
  1323. self.session_release = "foo@1.0.0"
  1324. session_1 = "5d52fd05-fcc9-4bf3-9dc9-267783670341"
  1325. session_2 = "5e910c1a-6941-460e-9843-24103fb6a63c"
  1326. session_3 = "a148c0c5-06a2-423b-8901-6b43b812cf82"
  1327. user_1 = "39887d89-13b2-4c84-8c23-5d13d2102666"
  1328. user_2 = "39887d89-13b2-4c84-8c23-5d13d2102667"
  1329. user_3 = "39887d89-13b2-4c84-8c23-5d13d2102668"
  1330. self.bulk_store_sessions(
  1331. [
  1332. self.build_session(
  1333. distinct_id=user_1,
  1334. session_id=session_1,
  1335. status="exited",
  1336. release=self.session_release,
  1337. environment="prod",
  1338. started=self.session_started,
  1339. received=self.received,
  1340. ),
  1341. self.build_session(
  1342. distinct_id=user_2,
  1343. session_id=session_2,
  1344. status="crashed",
  1345. release=self.session_release,
  1346. environment="prod",
  1347. started=self.session_started,
  1348. received=self.received,
  1349. ),
  1350. # session_3 initial update: no user ID
  1351. self.build_session(
  1352. distinct_id=None,
  1353. session_id=session_3,
  1354. status="ok",
  1355. seq=0,
  1356. release=self.session_release,
  1357. environment="prod",
  1358. started=self.session_started,
  1359. received=self.received,
  1360. ),
  1361. # session_3 subsequent update: user ID is here!
  1362. self.build_session(
  1363. distinct_id=user_3,
  1364. session_id=session_3,
  1365. status="ok",
  1366. seq=123,
  1367. release=self.session_release,
  1368. environment="prod",
  1369. started=self.session_started,
  1370. received=self.received,
  1371. ),
  1372. ]
  1373. )
  1374. def test_get_release_adoption(self):
  1375. data = self.backend.get_release_adoption(
  1376. [
  1377. (self.project.id, self.session_release),
  1378. ]
  1379. )
  1380. inner = data[(self.project.id, self.session_release)]
  1381. assert inner["users_24h"] == 3
  1382. def test_get_release_health_data_overview_users(self):
  1383. data = self.backend.get_release_health_data_overview(
  1384. [
  1385. (self.project.id, self.session_release),
  1386. ],
  1387. summary_stats_period="24h",
  1388. health_stats_period="24h",
  1389. stat="users",
  1390. )
  1391. inner = data[(self.project.id, self.session_release)]
  1392. assert inner["total_users"] == 3
  1393. assert inner["crash_free_users"] == 66.66666666666667
  1394. def test_get_crash_free_breakdown(self):
  1395. start = timezone.now() - timedelta(days=4)
  1396. data = self.backend.get_crash_free_breakdown(
  1397. project_id=self.project.id,
  1398. release=self.session_release,
  1399. start=start,
  1400. environments=["prod"],
  1401. )
  1402. # Last returned date is generated within function, should be close to now:
  1403. last_date = data[-1]["date"]
  1404. assert timezone.now() - last_date < timedelta(seconds=1)
  1405. assert data == [
  1406. {
  1407. "crash_free_sessions": None,
  1408. "crash_free_users": None,
  1409. "date": start + timedelta(days=1),
  1410. "total_sessions": 0,
  1411. "total_users": 0,
  1412. },
  1413. {
  1414. "crash_free_sessions": None,
  1415. "crash_free_users": None,
  1416. "date": start + timedelta(days=2),
  1417. "total_sessions": 0,
  1418. "total_users": 0,
  1419. },
  1420. {
  1421. "crash_free_sessions": 66.66666666666667,
  1422. "crash_free_users": 66.66666666666667,
  1423. "total_sessions": 3,
  1424. "total_users": 3,
  1425. "date": mock.ANY, # tested above
  1426. },
  1427. ]
  1428. def test_get_project_release_stats_users(self):
  1429. end = timezone.now()
  1430. start = end - timedelta(days=4)
  1431. stats, totals = self.backend.get_project_release_stats(
  1432. self.project.id,
  1433. release=self.session_release,
  1434. stat="users",
  1435. rollup=86400,
  1436. start=start,
  1437. end=end,
  1438. )
  1439. assert stats[3][1] == {
  1440. "duration_p50": 60.0,
  1441. "duration_p90": 60.0,
  1442. "users": 3,
  1443. "users_abnormal": 0,
  1444. "users_crashed": 1,
  1445. "users_errored": 0,
  1446. "users_healthy": 2,
  1447. }