test_sessions.py 58 KB

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