test_sessions.py 58 KB

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