test_sessions.py 58 KB

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