test_sessions.py 58 KB

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