test_sessions.py 54 KB

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