test_sessions.py 48 KB

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