test_sessions.py 53 KB

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