test_sessions.py 54 KB

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