test_sessions.py 48 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392
  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.sessions import _make_stats
  11. from sentry.testutils import SnubaTestCase, TestCase
  12. from sentry.testutils.cases import SessionMetricsTestCase
  13. from sentry.utils.dates import to_timestamp
  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(SessionMetricsTestCase, 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_exclude_sessions_gt_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 that any project
  117. with session data older than 90 days should be exluded
  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. date_100_days_ago = to_timestamp(
  127. (datetime.utcnow() - timedelta(days=100)).replace(tzinfo=pytz.utc)
  128. )
  129. self.store_session(
  130. self.build_session(
  131. **{
  132. "started": date_100_days_ago // 60 * 60,
  133. "received": date_100_days_ago,
  134. "project_id": project2.id,
  135. "org_id": project2.organization_id,
  136. "status": "exited",
  137. }
  138. )
  139. )
  140. data = self.backend.check_has_health_data([self.project.id, project2.id])
  141. assert data == {self.project.id}
  142. def test_check_has_health_data_without_releases_should_include_sessions_lte_90_days(self):
  143. """
  144. Test that ensures that `check_has_health_data` returns a set of projects that has health
  145. data within the last 90d if only a list of project ids is provided and any project with
  146. session data earlier than 90 days should be included
  147. """
  148. project2 = self.create_project(
  149. name="Bar2",
  150. slug="bar2",
  151. teams=[self.team],
  152. fire_project_created=True,
  153. organization=self.organization,
  154. )
  155. self.store_session(
  156. self.build_session(
  157. **{
  158. "project_id": project2.id,
  159. "org_id": project2.organization_id,
  160. "status": "exited",
  161. }
  162. )
  163. )
  164. data = self.backend.check_has_health_data([self.project.id, project2.id])
  165. assert data == {self.project.id, project2.id}
  166. def test_check_has_health_data_does_not_crash_when_sending_projects_list_as_set(self):
  167. data = self.backend.check_has_health_data({self.project.id})
  168. assert data == {self.project.id}
  169. def test_get_project_releases_by_stability(self):
  170. # Add an extra session with a different `distinct_id` so that sorting by users
  171. # is stable
  172. self.store_session(
  173. self.build_session(
  174. release=self.session_release,
  175. environment="prod",
  176. started=self.session_started,
  177. received=self.received,
  178. )
  179. )
  180. for scope in "sessions", "users":
  181. data = self.backend.get_project_releases_by_stability(
  182. [self.project.id], offset=0, limit=100, scope=scope, stats_period="24h"
  183. )
  184. assert data == [
  185. (self.project.id, self.session_release),
  186. (self.project.id, self.session_crashed_release),
  187. ]
  188. def test_get_project_releases_by_stability_for_crash_free_sort(self):
  189. """
  190. Test that ensures that using crash free rate sort options, returns a list of ASC releases
  191. according to the chosen crash_free sort option
  192. """
  193. # add another user to session_release to make sure that they are sorted correctly
  194. self.store_session(
  195. self.build_session(
  196. status="exited",
  197. release=self.session_release,
  198. environment="prod",
  199. started=self.session_started,
  200. received=self.received,
  201. )
  202. )
  203. for scope in "crash_free_sessions", "crash_free_users":
  204. data = self.backend.get_project_releases_by_stability(
  205. [self.project.id], offset=0, limit=100, scope=scope, stats_period="24h"
  206. )
  207. assert data == [
  208. (self.project.id, self.session_crashed_release),
  209. (self.project.id, self.session_release),
  210. ]
  211. def test_get_project_releases_by_stability_for_releases_with_users_data(self):
  212. """
  213. Test that ensures if releases contain no users data, then those releases should not be
  214. returned on `users` and `crash_free_users` sorts
  215. """
  216. self.store_session(
  217. self.build_session(
  218. distinct_id=None,
  219. release="release-with-no-users",
  220. environment="prod",
  221. started=self.session_started,
  222. received=self.received,
  223. )
  224. )
  225. data = self.backend.get_project_releases_by_stability(
  226. [self.project.id], offset=0, limit=100, scope="users", stats_period="24h"
  227. )
  228. assert set(data) == {
  229. (self.project.id, self.session_release),
  230. (self.project.id, self.session_crashed_release),
  231. }
  232. data = self.backend.get_project_releases_by_stability(
  233. [self.project.id], offset=0, limit=100, scope="crash_free_users", stats_period="24h"
  234. )
  235. assert set(data) == {
  236. (self.project.id, self.session_crashed_release),
  237. (self.project.id, self.session_release),
  238. }
  239. def test_get_release_adoption(self):
  240. data = self.backend.get_release_adoption(
  241. [
  242. (self.project.id, self.session_release),
  243. (self.project.id, self.session_crashed_release),
  244. (self.project.id, "dummy-release"),
  245. ]
  246. )
  247. assert data == {
  248. (self.project.id, self.session_release): {
  249. "sessions_24h": 2,
  250. "users_24h": 1,
  251. "adoption": 100.0,
  252. "sessions_adoption": 66.66666666666666,
  253. "project_sessions_24h": 3,
  254. "project_users_24h": 1,
  255. },
  256. (self.project.id, self.session_crashed_release): {
  257. "sessions_24h": 1,
  258. "users_24h": 1,
  259. "adoption": 100.0,
  260. "sessions_adoption": 33.33333333333333,
  261. "project_sessions_24h": 3,
  262. "project_users_24h": 1,
  263. },
  264. }
  265. def test_get_release_adoption_lowered(self):
  266. self.store_session(
  267. self.build_session(
  268. release=self.session_crashed_release,
  269. environment="prod",
  270. status="crashed",
  271. started=self.session_started,
  272. received=self.received,
  273. )
  274. )
  275. data = self.backend.get_release_adoption(
  276. [
  277. (self.project.id, self.session_release),
  278. (self.project.id, self.session_crashed_release),
  279. (self.project.id, "dummy-release"),
  280. ]
  281. )
  282. assert data == {
  283. (self.project.id, self.session_release): {
  284. "sessions_24h": 2,
  285. "users_24h": 1,
  286. "adoption": 50.0,
  287. "sessions_adoption": 50.0,
  288. "project_sessions_24h": 4,
  289. "project_users_24h": 2,
  290. },
  291. (self.project.id, self.session_crashed_release): {
  292. "sessions_24h": 2,
  293. "users_24h": 2,
  294. "adoption": 100.0,
  295. "sessions_adoption": 50.0,
  296. "project_sessions_24h": 4,
  297. "project_users_24h": 2,
  298. },
  299. }
  300. def test_get_release_health_data_overview_users(self):
  301. data = self.backend.get_release_health_data_overview(
  302. [
  303. (self.project.id, self.session_release),
  304. (self.project.id, self.session_crashed_release),
  305. ],
  306. summary_stats_period="24h",
  307. health_stats_period="24h",
  308. stat="users",
  309. )
  310. stats = make_24h_stats(self.received - (24 * 3600))
  311. stats[-1] = [stats[-1][0], 1]
  312. stats_ok = stats_crash = stats
  313. assert data == {
  314. (self.project.id, self.session_crashed_release): {
  315. "total_sessions": 1,
  316. "sessions_errored": 0,
  317. "total_sessions_24h": 1,
  318. "total_users": 1,
  319. "duration_p90": None,
  320. "sessions_crashed": 1,
  321. "total_users_24h": 1,
  322. "stats": {"24h": stats_crash},
  323. "crash_free_users": 0.0,
  324. "adoption": 100.0,
  325. "sessions_adoption": 33.33333333333333,
  326. "has_health_data": True,
  327. "crash_free_sessions": 0.0,
  328. "duration_p50": None,
  329. "total_project_sessions_24h": 3,
  330. "total_project_users_24h": 1,
  331. },
  332. (self.project.id, self.session_release): {
  333. "total_sessions": 2,
  334. "sessions_errored": 0,
  335. "total_sessions_24h": 2,
  336. "total_users": 1,
  337. "duration_p90": 57.0,
  338. "sessions_crashed": 0,
  339. "total_users_24h": 1,
  340. "stats": {"24h": stats_ok},
  341. "crash_free_users": 100.0,
  342. "adoption": 100.0,
  343. "sessions_adoption": 66.66666666666666,
  344. "has_health_data": True,
  345. "crash_free_sessions": 100.0,
  346. "duration_p50": 45.0,
  347. "total_project_sessions_24h": 3,
  348. "total_project_users_24h": 1,
  349. },
  350. }
  351. def test_get_release_health_data_overview_sessions(self):
  352. data = self.backend.get_release_health_data_overview(
  353. [
  354. (self.project.id, self.session_release),
  355. (self.project.id, self.session_crashed_release),
  356. ],
  357. summary_stats_period="24h",
  358. health_stats_period="24h",
  359. stat="sessions",
  360. )
  361. stats = make_24h_stats(self.received - (24 * 3600))
  362. stats_ok = stats[:-1] + [[stats[-1][0], 2]]
  363. stats_crash = stats[:-1] + [[stats[-1][0], 1]]
  364. assert data == {
  365. (self.project.id, self.session_crashed_release): {
  366. "total_sessions": 1,
  367. "sessions_errored": 0,
  368. "total_sessions_24h": 1,
  369. "total_users": 1,
  370. "duration_p90": None,
  371. "sessions_crashed": 1,
  372. "total_users_24h": 1,
  373. "stats": {"24h": stats_crash},
  374. "crash_free_users": 0.0,
  375. "adoption": 100.0,
  376. "sessions_adoption": 33.33333333333333,
  377. "has_health_data": True,
  378. "crash_free_sessions": 0.0,
  379. "duration_p50": None,
  380. "total_project_sessions_24h": 3,
  381. "total_project_users_24h": 1,
  382. },
  383. (self.project.id, self.session_release): {
  384. "total_sessions": 2,
  385. "sessions_errored": 0,
  386. "total_sessions_24h": 2,
  387. "total_users": 1,
  388. "duration_p90": 57.0,
  389. "sessions_crashed": 0,
  390. "total_users_24h": 1,
  391. "stats": {"24h": stats_ok},
  392. "crash_free_users": 100.0,
  393. "sessions_adoption": 66.66666666666666,
  394. "adoption": 100.0,
  395. "has_health_data": True,
  396. "crash_free_sessions": 100.0,
  397. "duration_p50": 45.0,
  398. "total_project_sessions_24h": 3,
  399. "total_project_users_24h": 1,
  400. },
  401. }
  402. def test_fetching_release_sessions_time_bounds_for_different_release(self):
  403. """
  404. Test that ensures only session bounds for releases are calculated according
  405. to their respective release
  406. """
  407. # Same release session
  408. self.store_session(
  409. self.build_session(
  410. release=self.session_release,
  411. environment="prod",
  412. status="exited",
  413. started=self.session_started - 3600 * 2,
  414. received=self.received - 3600 * 2,
  415. )
  416. )
  417. # Different release session
  418. self.store_session(
  419. self.build_session(
  420. release=self.session_crashed_release,
  421. environment="prod",
  422. status="crashed",
  423. started=self.session_started - 3600 * 2,
  424. received=self.received - 3600 * 2,
  425. )
  426. )
  427. expected_formatted_lower_bound = (
  428. datetime.utcfromtimestamp(self.session_started - 3600 * 2)
  429. .replace(minute=0)
  430. .isoformat()[:19]
  431. + "Z"
  432. )
  433. expected_formatted_upper_bound = (
  434. datetime.utcfromtimestamp(self.session_started).replace(minute=0).isoformat()[:19] + "Z"
  435. )
  436. # Test for self.session_release
  437. data = self.backend.get_release_sessions_time_bounds(
  438. project_id=self.project.id,
  439. release=self.session_release,
  440. org_id=self.organization.id,
  441. environments=["prod"],
  442. )
  443. assert data == {
  444. "sessions_lower_bound": expected_formatted_lower_bound,
  445. "sessions_upper_bound": expected_formatted_upper_bound,
  446. }
  447. # Test for self.session_crashed_release
  448. data = self.backend.get_release_sessions_time_bounds(
  449. project_id=self.project.id,
  450. release=self.session_crashed_release,
  451. org_id=self.organization.id,
  452. environments=["prod"],
  453. )
  454. assert data == {
  455. "sessions_lower_bound": expected_formatted_lower_bound,
  456. "sessions_upper_bound": expected_formatted_upper_bound,
  457. }
  458. def test_fetching_release_sessions_time_bounds_for_different_release_with_no_sessions(self):
  459. """
  460. Test that ensures if no sessions are available for a specific release then the bounds
  461. should be returned as None
  462. """
  463. data = self.backend.get_release_sessions_time_bounds(
  464. project_id=self.project.id,
  465. release="different_release",
  466. org_id=self.organization.id,
  467. environments=["prod"],
  468. )
  469. assert data == {
  470. "sessions_lower_bound": None,
  471. "sessions_upper_bound": None,
  472. }
  473. def test_get_crash_free_breakdown(self):
  474. start = timezone.now() - timedelta(days=4)
  475. data = self.backend.get_crash_free_breakdown(
  476. project_id=self.project.id,
  477. release=self.session_release,
  478. start=start,
  479. environments=["prod"],
  480. )
  481. # Last returned date is generated within function, should be close to now:
  482. last_date = data[-1].pop("date")
  483. assert timezone.now() - last_date < timedelta(seconds=1)
  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": 100.0,
  501. "crash_free_users": 100.0,
  502. "total_sessions": 2,
  503. "total_users": 1,
  504. },
  505. ]
  506. data = self.backend.get_crash_free_breakdown(
  507. project_id=self.project.id,
  508. release=self.session_crashed_release,
  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": 0.0,
  530. "crash_free_users": 0.0,
  531. "total_sessions": 1,
  532. "total_users": 1,
  533. },
  534. ]
  535. data = self.backend.get_crash_free_breakdown(
  536. project_id=self.project.id,
  537. release="non-existing",
  538. start=start,
  539. environments=["prod"],
  540. )
  541. data[-1].pop("date")
  542. assert data == [
  543. {
  544. "crash_free_sessions": None,
  545. "crash_free_users": None,
  546. "date": start + timedelta(days=1),
  547. "total_sessions": 0,
  548. "total_users": 0,
  549. },
  550. {
  551. "crash_free_sessions": None,
  552. "crash_free_users": None,
  553. "date": start + timedelta(days=2),
  554. "total_sessions": 0,
  555. "total_users": 0,
  556. },
  557. {
  558. "crash_free_sessions": None,
  559. "crash_free_users": None,
  560. "total_sessions": 0,
  561. "total_users": 0,
  562. },
  563. ]
  564. def test_basic_release_model_adoptions(self):
  565. """
  566. Test that the basic (project,release) data is returned
  567. """
  568. proj_id = self.project.id
  569. data = self.backend.get_changed_project_release_model_adoptions([proj_id])
  570. assert set(data) == {(proj_id, "foo@1.0.0"), (proj_id, "foo@2.0.0")}
  571. def test_old_release_model_adoptions(self):
  572. """
  573. Test that old entries (older that 72 h) are not returned
  574. """
  575. _100h = 100 * 60 * 60 # 100 hours in seconds
  576. proj_id = self.project.id
  577. self.store_session(
  578. self.build_session(
  579. release="foo@3.0.0",
  580. environment="prod",
  581. status="crashed",
  582. started=self.session_started - _100h,
  583. received=self.received - 3600 * 2,
  584. )
  585. )
  586. data = self.backend.get_changed_project_release_model_adoptions([proj_id])
  587. assert set(data) == {(proj_id, "foo@1.0.0"), (proj_id, "foo@2.0.0")}
  588. def test_multi_proj_release_model_adoptions(self):
  589. """Test that the api works with multiple projects"""
  590. proj_id = self.project.id
  591. new_proj_id = proj_id + 1
  592. self.store_session(
  593. self.build_session(
  594. project_id=new_proj_id,
  595. release="foo@3.0.0",
  596. environment="prod",
  597. status="crashed",
  598. started=self.session_started,
  599. received=self.received - 3600 * 2,
  600. )
  601. )
  602. data = self.backend.get_changed_project_release_model_adoptions([proj_id, new_proj_id])
  603. assert set(data) == {
  604. (proj_id, "foo@1.0.0"),
  605. (proj_id, "foo@2.0.0"),
  606. (new_proj_id, "foo@3.0.0"),
  607. }
  608. @staticmethod
  609. def _add_timestamps_to_series(series, start: datetime):
  610. one_day = 24 * 60 * 60
  611. day0 = one_day * int(start.timestamp() / one_day)
  612. def ts(days: int) -> int:
  613. return day0 + days * one_day
  614. return [[ts(i + 1), data] for i, data in enumerate(series)]
  615. def _test_get_project_release_stats(
  616. self, stat: OverviewStat, release: str, expected_series, expected_totals
  617. ):
  618. end = timezone.now()
  619. start = end - timedelta(days=4)
  620. stats, totals = self.backend.get_project_release_stats(
  621. self.project.id,
  622. release=release,
  623. stat=stat,
  624. rollup=86400,
  625. start=start,
  626. end=end,
  627. )
  628. # Let's not care about lists vs. tuples:
  629. stats = [[ts, data] for ts, data in stats]
  630. assert stats == self._add_timestamps_to_series(expected_series, start)
  631. assert totals == expected_totals
  632. def test_get_project_release_stats_users(self):
  633. self._test_get_project_release_stats(
  634. "users",
  635. self.session_release,
  636. [
  637. {
  638. "duration_p50": None,
  639. "duration_p90": None,
  640. "users": 0,
  641. "users_abnormal": 0,
  642. "users_crashed": 0,
  643. "users_errored": 0,
  644. "users_healthy": 0,
  645. },
  646. {
  647. "duration_p50": None,
  648. "duration_p90": None,
  649. "users": 0,
  650. "users_abnormal": 0,
  651. "users_crashed": 0,
  652. "users_errored": 0,
  653. "users_healthy": 0,
  654. },
  655. {
  656. "duration_p50": None,
  657. "duration_p90": None,
  658. "users": 0,
  659. "users_abnormal": 0,
  660. "users_crashed": 0,
  661. "users_errored": 0,
  662. "users_healthy": 0,
  663. },
  664. {
  665. "duration_p50": 45.0,
  666. "duration_p90": 57.0,
  667. "users": 1,
  668. "users_abnormal": 0,
  669. "users_crashed": 0,
  670. "users_errored": 0,
  671. "users_healthy": 1,
  672. },
  673. ],
  674. {
  675. "users": 1,
  676. "users_abnormal": 0,
  677. "users_crashed": 0,
  678. "users_errored": 0,
  679. "users_healthy": 1,
  680. },
  681. )
  682. def test_get_project_release_stats_users_crashed(self):
  683. self._test_get_project_release_stats(
  684. "users",
  685. self.session_crashed_release,
  686. [
  687. {
  688. "duration_p50": None,
  689. "duration_p90": None,
  690. "users": 0,
  691. "users_abnormal": 0,
  692. "users_crashed": 0,
  693. "users_errored": 0,
  694. "users_healthy": 0,
  695. },
  696. {
  697. "duration_p50": None,
  698. "duration_p90": None,
  699. "users": 0,
  700. "users_abnormal": 0,
  701. "users_crashed": 0,
  702. "users_errored": 0,
  703. "users_healthy": 0,
  704. },
  705. {
  706. "duration_p50": None,
  707. "duration_p90": None,
  708. "users": 0,
  709. "users_abnormal": 0,
  710. "users_crashed": 0,
  711. "users_errored": 0,
  712. "users_healthy": 0,
  713. },
  714. {
  715. "duration_p50": None,
  716. "duration_p90": None,
  717. "users": 1,
  718. "users_abnormal": 0,
  719. "users_crashed": 1,
  720. "users_errored": 0,
  721. "users_healthy": 0,
  722. },
  723. ],
  724. {
  725. "users": 1,
  726. "users_abnormal": 0,
  727. "users_crashed": 1,
  728. "users_errored": 0,
  729. "users_healthy": 0,
  730. },
  731. )
  732. def test_get_project_release_stats_sessions(self):
  733. self._test_get_project_release_stats(
  734. "sessions",
  735. self.session_release,
  736. [
  737. {
  738. "duration_p50": None,
  739. "duration_p90": None,
  740. "sessions": 0,
  741. "sessions_abnormal": 0,
  742. "sessions_crashed": 0,
  743. "sessions_errored": 0,
  744. "sessions_healthy": 0,
  745. },
  746. {
  747. "duration_p50": None,
  748. "duration_p90": None,
  749. "sessions": 0,
  750. "sessions_abnormal": 0,
  751. "sessions_crashed": 0,
  752. "sessions_errored": 0,
  753. "sessions_healthy": 0,
  754. },
  755. {
  756. "duration_p50": None,
  757. "duration_p90": None,
  758. "sessions": 0,
  759. "sessions_abnormal": 0,
  760. "sessions_crashed": 0,
  761. "sessions_errored": 0,
  762. "sessions_healthy": 0,
  763. },
  764. {
  765. "duration_p50": 45.0,
  766. "duration_p90": 57.0,
  767. "sessions": 2,
  768. "sessions_abnormal": 0,
  769. "sessions_crashed": 0,
  770. "sessions_errored": 0,
  771. "sessions_healthy": 2,
  772. },
  773. ],
  774. {
  775. "sessions": 2,
  776. "sessions_abnormal": 0,
  777. "sessions_crashed": 0,
  778. "sessions_errored": 0,
  779. "sessions_healthy": 2,
  780. },
  781. )
  782. def test_get_project_release_stats_sessions_crashed(self):
  783. self._test_get_project_release_stats(
  784. "sessions",
  785. self.session_crashed_release,
  786. [
  787. {
  788. "duration_p50": None,
  789. "duration_p90": None,
  790. "sessions": 0,
  791. "sessions_abnormal": 0,
  792. "sessions_crashed": 0,
  793. "sessions_errored": 0,
  794. "sessions_healthy": 0,
  795. },
  796. {
  797. "duration_p50": None,
  798. "duration_p90": None,
  799. "sessions": 0,
  800. "sessions_abnormal": 0,
  801. "sessions_crashed": 0,
  802. "sessions_errored": 0,
  803. "sessions_healthy": 0,
  804. },
  805. {
  806. "duration_p50": None,
  807. "duration_p90": None,
  808. "sessions": 0,
  809. "sessions_abnormal": 0,
  810. "sessions_crashed": 0,
  811. "sessions_errored": 0,
  812. "sessions_healthy": 0,
  813. },
  814. {
  815. "duration_p50": None,
  816. "duration_p90": None,
  817. "sessions": 1,
  818. "sessions_abnormal": 0,
  819. "sessions_crashed": 1,
  820. "sessions_errored": 0,
  821. "sessions_healthy": 0,
  822. },
  823. ],
  824. {
  825. "sessions": 1,
  826. "sessions_abnormal": 0,
  827. "sessions_crashed": 1,
  828. "sessions_errored": 0,
  829. "sessions_healthy": 0,
  830. },
  831. )
  832. @parametrize_backend
  833. class GetCrashFreeRateTestCase(TestCase, SnubaTestCase):
  834. """
  835. TestClass that tests that `get_current_and_previous_crash_free_rates` returns the correct
  836. `currentCrashFreeRate` and `previousCrashFreeRate` for each project
  837. TestData:
  838. Project 1:
  839. In the last 24h -> 2 Exited Sessions / 2 Total Sessions -> 100% Crash free rate
  840. In the previous 24h (>24h & <48h) -> 2 Exited + 1 Crashed Sessions / 3 Sessions -> 66.7%
  841. Project 2:
  842. In the last 24h -> 1 Exited + 1 Crashed / 2 Total Sessions -> 50% Crash free rate
  843. In the previous 24h (>24h & <48h) -> 0 Sessions -> None
  844. Project 3:
  845. In the last 24h -> 0 Sessions -> None
  846. In the previous 24h (>24h & <48h) -> 4 Exited + 1 Crashed / 5 Total Sessions -> 80%
  847. """
  848. def setUp(self):
  849. super().setUp()
  850. self.session_started = time.time() // 60 * 60
  851. self.session_started_gt_24_lt_48 = self.session_started - 30 * 60 * 60
  852. self.project2 = self.create_project(
  853. name="Bar2",
  854. slug="bar2",
  855. teams=[self.team],
  856. fire_project_created=True,
  857. organization=self.organization,
  858. )
  859. self.project3 = self.create_project(
  860. name="Bar3",
  861. slug="bar3",
  862. teams=[self.team],
  863. fire_project_created=True,
  864. organization=self.organization,
  865. )
  866. # Project 1
  867. for _ in range(0, 2):
  868. self.store_session(
  869. self.build_session(
  870. **{
  871. "project_id": self.project.id,
  872. "org_id": self.project.organization_id,
  873. "status": "exited",
  874. }
  875. )
  876. )
  877. for idx in range(0, 3):
  878. status = "exited"
  879. if idx == 2:
  880. status = "crashed"
  881. self.store_session(
  882. self.build_session(
  883. **{
  884. "project_id": self.project.id,
  885. "org_id": self.project.organization_id,
  886. "status": status,
  887. "started": self.session_started_gt_24_lt_48,
  888. }
  889. )
  890. )
  891. # Project 2
  892. for i in range(0, 2):
  893. status = "exited"
  894. if i == 1:
  895. status = "crashed"
  896. self.store_session(
  897. self.build_session(
  898. **{
  899. "project_id": self.project2.id,
  900. "org_id": self.project2.organization_id,
  901. "status": status,
  902. }
  903. )
  904. )
  905. # Project 3
  906. for i in range(0, 5):
  907. status = "exited"
  908. if i == 4:
  909. status = "crashed"
  910. self.store_session(
  911. self.build_session(
  912. **{
  913. "project_id": self.project3.id,
  914. "org_id": self.project3.organization_id,
  915. "status": status,
  916. "started": self.session_started_gt_24_lt_48,
  917. }
  918. )
  919. )
  920. def test_get_current_and_previous_crash_free_rates(self):
  921. now = timezone.now().replace(minute=15, second=23)
  922. last_24h_start = now - 24 * timedelta(hours=1)
  923. last_48h_start = now - 2 * 24 * timedelta(hours=1)
  924. data = self.backend.get_current_and_previous_crash_free_rates(
  925. org_id=self.organization.id,
  926. project_ids=[self.project.id, self.project2.id, self.project3.id],
  927. current_start=last_24h_start,
  928. current_end=now,
  929. previous_start=last_48h_start,
  930. previous_end=last_24h_start,
  931. rollup=3600,
  932. )
  933. assert data == {
  934. self.project.id: {
  935. "currentCrashFreeRate": 100,
  936. "previousCrashFreeRate": 66.66666666666667,
  937. },
  938. self.project2.id: {"currentCrashFreeRate": 50.0, "previousCrashFreeRate": None},
  939. self.project3.id: {"currentCrashFreeRate": None, "previousCrashFreeRate": 80.0},
  940. }
  941. def test_get_current_and_previous_crash_free_rates_with_zero_sessions(self):
  942. now = timezone.now().replace(minute=15, second=23)
  943. last_48h_start = now - 2 * 24 * timedelta(hours=1)
  944. last_72h_start = now - 3 * 24 * timedelta(hours=1)
  945. last_96h_start = now - 4 * 24 * timedelta(hours=1)
  946. data = self.backend.get_current_and_previous_crash_free_rates(
  947. org_id=self.organization.id,
  948. project_ids=[self.project.id],
  949. current_start=last_72h_start,
  950. current_end=last_48h_start,
  951. previous_start=last_96h_start,
  952. previous_end=last_72h_start,
  953. rollup=3600,
  954. )
  955. assert data == {
  956. self.project.id: {
  957. "currentCrashFreeRate": None,
  958. "previousCrashFreeRate": None,
  959. },
  960. }
  961. @parametrize_backend
  962. class GetProjectReleasesCountTest(TestCase, SnubaTestCase):
  963. def test_empty(self):
  964. # Test no errors when no session data
  965. org = self.create_organization()
  966. proj = self.create_project(organization=org)
  967. assert (
  968. self.backend.get_project_releases_count(
  969. org.id, [proj.id], "crash_free_users", stats_period="14d"
  970. )
  971. == 0
  972. )
  973. def test(self):
  974. project_release_1 = self.create_release(self.project)
  975. other_project = self.create_project()
  976. other_project_release_1 = self.create_release(other_project)
  977. self.bulk_store_sessions(
  978. [
  979. self.build_session(
  980. environment=self.environment.name, release=project_release_1.version
  981. ),
  982. self.build_session(
  983. environment="staging",
  984. project_id=other_project.id,
  985. release=other_project_release_1.version,
  986. ),
  987. ]
  988. )
  989. assert (
  990. self.backend.get_project_releases_count(
  991. self.organization.id, [self.project.id], "sessions"
  992. )
  993. == 1
  994. )
  995. assert (
  996. self.backend.get_project_releases_count(
  997. self.organization.id, [self.project.id], "users"
  998. )
  999. == 1
  1000. )
  1001. assert (
  1002. self.backend.get_project_releases_count(
  1003. self.organization.id, [self.project.id, other_project.id], "sessions"
  1004. )
  1005. == 2
  1006. )
  1007. assert (
  1008. self.backend.get_project_releases_count(
  1009. self.organization.id,
  1010. [self.project.id, other_project.id],
  1011. "users",
  1012. )
  1013. == 2
  1014. )
  1015. assert (
  1016. self.backend.get_project_releases_count(
  1017. self.organization.id,
  1018. [self.project.id, other_project.id],
  1019. "sessions",
  1020. environments=[self.environment.name],
  1021. )
  1022. == 1
  1023. )
  1024. @parametrize_backend
  1025. class CheckReleasesHaveHealthDataTest(TestCase, SnubaTestCase):
  1026. def run_test(self, expected, projects, releases, start=None, end=None):
  1027. if not start:
  1028. start = datetime.now() - timedelta(days=1)
  1029. if not end:
  1030. end = datetime.now()
  1031. assert (
  1032. self.backend.check_releases_have_health_data(
  1033. self.organization.id,
  1034. [p.id for p in projects],
  1035. [r.version for r in releases],
  1036. start,
  1037. end,
  1038. )
  1039. == {v.version for v in expected}
  1040. )
  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_sessions_in_all_environments(self):
  1129. """
  1130. When the environment is not specified sessions from all environments are counted
  1131. """
  1132. dev = self.dev_env.name
  1133. prod = self.prod_env.name
  1134. self.bulk_store_sessions(
  1135. [
  1136. self.build_session(
  1137. environment=dev, received=self._5_min_ago, started=self._5_min_ago
  1138. ),
  1139. self.build_session(
  1140. environment=prod, received=self._5_min_ago, started=self._5_min_ago
  1141. ),
  1142. self.build_session(
  1143. environment=prod, received=self._5_min_ago, started=self._5_min_ago
  1144. ),
  1145. self.build_session(environment=prod, received=self._2_h_ago, started=self._2_h_ago),
  1146. self.build_session(environment=dev, received=self._2_h_ago, started=self._2_h_ago),
  1147. ]
  1148. )
  1149. actual = self.backend.get_project_sessions_count(
  1150. project_id=self.project.id,
  1151. environment_id=None,
  1152. rollup=60,
  1153. start=self._1_h_ago_dt,
  1154. end=self.now_dt,
  1155. )
  1156. assert actual == 3
  1157. def test_sessions_from_multiple_projects(self):
  1158. """
  1159. Only sessions from the specified project are considered
  1160. """
  1161. dev = self.dev_env.name
  1162. prod = self.prod_env.name
  1163. self.bulk_store_sessions(
  1164. [
  1165. self.build_session(
  1166. environment=dev, received=self._5_min_ago, started=self._5_min_ago
  1167. ),
  1168. self.build_session(
  1169. environment=prod, received=self._5_min_ago, started=self._5_min_ago
  1170. ),
  1171. self.build_session(
  1172. environment=prod,
  1173. received=self._5_min_ago,
  1174. project_id=self.another_project.id,
  1175. started=self._5_min_ago,
  1176. ),
  1177. ]
  1178. )
  1179. actual = self.backend.get_project_sessions_count(
  1180. project_id=self.project.id,
  1181. environment_id=None,
  1182. rollup=60,
  1183. start=self._1_h_ago_dt,
  1184. end=self.now_dt,
  1185. )
  1186. assert actual == 2
  1187. def test_sessions_per_project_no_sessions(self):
  1188. """
  1189. Tests that no sessions are returned
  1190. """
  1191. actual = self.backend.get_num_sessions_per_project(
  1192. project_ids=[self.project.id, self.another_project.id],
  1193. environment_ids=None,
  1194. rollup=60,
  1195. start=self._30_min_ago_dt,
  1196. end=self.now_dt,
  1197. )
  1198. assert [] == actual
  1199. def test_sesions_per_project_multiple_projects(self):
  1200. dev = self.dev_env.name
  1201. prod = self.prod_env.name
  1202. test = self.test_env.name
  1203. p1 = self.project
  1204. p2 = self.another_project
  1205. p3 = self.third_project
  1206. self.bulk_store_sessions(
  1207. [
  1208. # counted in p1
  1209. self.build_session(
  1210. environment=dev, received=self._5_min_ago, started=self._5_min_ago
  1211. ),
  1212. self.build_session(
  1213. environment=prod, received=self._5_min_ago, started=self._5_min_ago
  1214. ),
  1215. self.build_session(
  1216. environment=dev, received=self._30_min_ago, started=self._30_min_ago
  1217. ),
  1218. # ignored in p1
  1219. # ignored env
  1220. self.build_session(
  1221. environment=test, received=self._30_min_ago, started=self._30_min_ago
  1222. ),
  1223. # too old
  1224. self.build_session(environment=prod, received=self._3_h_ago, started=self._3_h_ago),
  1225. # counted in p2
  1226. self.build_session(
  1227. environment=dev,
  1228. received=self._5_min_ago,
  1229. project_id=p2.id,
  1230. started=self._5_min_ago,
  1231. ),
  1232. # ignored in p2
  1233. # ignored env
  1234. self.build_session(
  1235. environment=test,
  1236. received=self._5_min_ago,
  1237. project_id=p2.id,
  1238. started=self._5_min_ago,
  1239. ),
  1240. # too old
  1241. self.build_session(
  1242. environment=prod,
  1243. received=self._3_h_ago,
  1244. project_id=p2.id,
  1245. started=self._3_h_ago,
  1246. ),
  1247. # ignored p3
  1248. self.build_session(
  1249. environment=dev,
  1250. received=self._5_min_ago,
  1251. project_id=p3.id,
  1252. started=self._5_min_ago,
  1253. ),
  1254. ]
  1255. )
  1256. actual = self.backend.get_num_sessions_per_project(
  1257. project_ids=[self.project.id, self.another_project.id],
  1258. environment_ids=[self.dev_env.id, self.prod_env.id],
  1259. rollup=60,
  1260. start=self._2_h_ago_dt,
  1261. end=self.now_dt,
  1262. )
  1263. assert set(actual) == {(p1.id, 3), (p2.id, 1)}
  1264. for eids in ([], None):
  1265. actual = self.backend.get_num_sessions_per_project(
  1266. project_ids=[self.project.id, self.another_project.id],
  1267. environment_ids=eids,
  1268. rollup=60,
  1269. start=self._2_h_ago_dt,
  1270. end=self.now_dt,
  1271. )
  1272. assert set(actual) == {(p1.id, 4), (p2.id, 2)}