test_sessions.py 54 KB

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