test_sessions.py 58 KB

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