test_sessions.py 53 KB

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