12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685 |
- import time
- from datetime import datetime, timedelta, timezone
- import pytest
- from django.utils import timezone as django_timezone
- from sentry.release_health.base import OverviewStat
- from sentry.release_health.metrics import MetricsReleaseHealthBackend
- from sentry.release_health.sessions import SessionsReleaseHealthBackend
- from sentry.sentry_metrics.use_case_id_registry import UseCaseID
- from sentry.snuba.sessions import _make_stats
- from sentry.testutils.cases import BaseMetricsTestCase, SnubaTestCase, TestCase
- from sentry.testutils.silo import region_silo_test
- pytestmark = pytest.mark.sentry_metrics
- def parametrize_backend(cls):
- """
- hack to parametrize test-classes by backend. Ideally we'd move
- over to pytest-style tests so we can use `pytest.mark.parametrize`, but
- hopefully we won't have more than one backend in the future.
- """
- assert not hasattr(cls, "backend")
- cls.backend = SessionsReleaseHealthBackend()
- class MetricsLayerTest(BaseMetricsTestCase, cls): # type: ignore[valid-type]
- __doc__ = f"Repeat tests from {cls} with metrics layer"
- backend = MetricsReleaseHealthBackend()
- adjust_interval = True # HACK interval adjustment for new MetricsLayer implementation
- MetricsLayerTest.__name__ = f"{cls.__name__}MetricsLayer"
- globals()[MetricsLayerTest.__name__] = MetricsLayerTest
- return cls
- def format_timestamp(dt):
- if not isinstance(dt, datetime):
- dt = datetime.utcfromtimestamp(dt)
- return dt.strftime("%Y-%m-%dT%H:%M:%S+00:00")
- def make_24h_stats(ts, adjust_start=False):
- ret_val = _make_stats(datetime.utcfromtimestamp(ts).replace(tzinfo=timezone.utc), 3600, 24)
- if adjust_start:
- # HACK this adds another interval at the beginning in accordance with the new way of calculating intervals
- # https://www.notion.so/sentry/Metrics-Layer-get_intervals-bug-dce140607d054201a5e6629b070cb969
- ret_val.insert(0, [ret_val[0][0] - 3600, 0])
- return ret_val
- @parametrize_backend
- class SnubaSessionsTest(TestCase, SnubaTestCase):
- adjust_interval = False # HACK interval adjustment for new MetricsLayer implementation
- def setUp(self):
- super().setUp()
- self.received = time.time()
- self.session_started = time.time() // 60 * 60
- self.session_release = "foo@1.0.0"
- self.session_crashed_release = "foo@2.0.0"
- session_1 = "5d52fd05-fcc9-4bf3-9dc9-267783670341"
- session_2 = "5e910c1a-6941-460e-9843-24103fb6a63c"
- session_3 = "a148c0c5-06a2-423b-8901-6b43b812cf82"
- user_1 = "39887d89-13b2-4c84-8c23-5d13d2102666"
- self.store_session(
- self.build_session(
- distinct_id=user_1,
- session_id=session_1,
- status="exited",
- release=self.session_release,
- environment="prod",
- started=self.session_started,
- received=self.received,
- )
- )
- self.store_session(
- self.build_session(
- distinct_id=user_1,
- session_id=session_2,
- release=self.session_release,
- environment="prod",
- duration=None,
- started=self.session_started,
- received=self.received,
- )
- )
- self.store_session(
- self.build_session(
- distinct_id=user_1,
- session_id=session_2,
- seq=1,
- duration=30,
- status="exited",
- release=self.session_release,
- environment="prod",
- started=self.session_started,
- received=self.received,
- )
- )
- self.store_session(
- self.build_session(
- distinct_id=user_1,
- session_id=session_3,
- status="crashed",
- release=self.session_crashed_release,
- environment="prod",
- started=self.session_started,
- received=self.received,
- )
- )
- def test_get_oldest_health_data_for_releases(self):
- data = self.backend.get_oldest_health_data_for_releases(
- [(self.project.id, self.session_release)]
- )
- assert data == {
- (self.project.id, self.session_release): format_timestamp(
- self.session_started // 3600 * 3600
- )
- }
- def test_check_has_health_data(self):
- data = self.backend.check_has_health_data(
- [(self.project.id, self.session_release), (self.project.id, "dummy-release")]
- )
- assert data == {(self.project.id, self.session_release)}
- def test_check_has_health_data_without_releases_should_include_sessions_lte_90_days(self):
- """
- Test that ensures that `check_has_health_data` returns a set of projects that has health
- data within the last 90d if only a list of project ids is provided and any project with
- session data earlier than 90 days should be included
- """
- project2 = self.create_project(
- name="Bar2",
- slug="bar2",
- teams=[self.team],
- fire_project_created=True,
- organization=self.organization,
- )
- self.store_session(
- self.build_session(
- **{
- "project_id": project2.id,
- "org_id": project2.organization_id,
- "status": "exited",
- }
- )
- )
- data = self.backend.check_has_health_data([self.project.id, project2.id])
- assert data == {self.project.id, project2.id}
- def test_check_has_health_data_does_not_crash_when_sending_projects_list_as_set(self):
- data = self.backend.check_has_health_data({self.project.id})
- assert data == {self.project.id}
- def test_get_project_releases_by_stability(self):
- # Add an extra session with a different `distinct_id` so that sorting by users
- # is stable
- self.store_session(
- self.build_session(
- release=self.session_release,
- environment="prod",
- started=self.session_started,
- received=self.received,
- )
- )
- for scope in "sessions", "users":
- data = self.backend.get_project_releases_by_stability(
- [self.project.id], offset=0, limit=100, scope=scope, stats_period="24h"
- )
- assert data == [
- (self.project.id, self.session_release),
- (self.project.id, self.session_crashed_release),
- ]
- def test_get_project_releases_by_stability_for_crash_free_sort(self):
- """
- Test that ensures that using crash free rate sort options, returns a list of ASC releases
- according to the chosen crash_free sort option
- """
- # add another user to session_release to make sure that they are sorted correctly
- self.store_session(
- self.build_session(
- status="exited",
- release=self.session_release,
- environment="prod",
- started=self.session_started,
- received=self.received,
- )
- )
- for scope in "crash_free_sessions", "crash_free_users":
- data = self.backend.get_project_releases_by_stability(
- [self.project.id], offset=0, limit=100, scope=scope, stats_period="24h"
- )
- assert data == [
- (self.project.id, self.session_crashed_release),
- (self.project.id, self.session_release),
- ]
- def test_get_project_releases_by_stability_for_releases_with_users_data(self):
- """
- Test that ensures if releases contain no users data, then those releases should not be
- returned on `users` and `crash_free_users` sorts
- """
- self.store_session(
- self.build_session(
- distinct_id=None,
- release="release-with-no-users",
- environment="prod",
- started=self.session_started,
- received=self.received,
- )
- )
- data = self.backend.get_project_releases_by_stability(
- [self.project.id], offset=0, limit=100, scope="users", stats_period="24h"
- )
- assert set(data) == {
- (self.project.id, self.session_release),
- (self.project.id, self.session_crashed_release),
- }
- data = self.backend.get_project_releases_by_stability(
- [self.project.id], offset=0, limit=100, scope="crash_free_users", stats_period="24h"
- )
- assert set(data) == {
- (self.project.id, self.session_crashed_release),
- (self.project.id, self.session_release),
- }
- def test_get_release_adoption(self):
- data = self.backend.get_release_adoption(
- [
- (self.project.id, self.session_release),
- (self.project.id, self.session_crashed_release),
- (self.project.id, "dummy-release"),
- ]
- )
- assert data == {
- (self.project.id, self.session_release): {
- "sessions_24h": 2,
- "users_24h": 1,
- "adoption": 100.0,
- "sessions_adoption": 66.66666666666666,
- "project_sessions_24h": 3,
- "project_users_24h": 1,
- },
- (self.project.id, self.session_crashed_release): {
- "sessions_24h": 1,
- "users_24h": 1,
- "adoption": 100.0,
- "sessions_adoption": 33.33333333333333,
- "project_sessions_24h": 3,
- "project_users_24h": 1,
- },
- }
- def test_get_release_adoption_lowered(self):
- self.store_session(
- self.build_session(
- release=self.session_crashed_release,
- environment="prod",
- status="crashed",
- started=self.session_started,
- received=self.received,
- )
- )
- data = self.backend.get_release_adoption(
- [
- (self.project.id, self.session_release),
- (self.project.id, self.session_crashed_release),
- (self.project.id, "dummy-release"),
- ]
- )
- assert data == {
- (self.project.id, self.session_release): {
- "sessions_24h": 2,
- "users_24h": 1,
- "adoption": 50.0,
- "sessions_adoption": 50.0,
- "project_sessions_24h": 4,
- "project_users_24h": 2,
- },
- (self.project.id, self.session_crashed_release): {
- "sessions_24h": 2,
- "users_24h": 2,
- "adoption": 100.0,
- "sessions_adoption": 50.0,
- "project_sessions_24h": 4,
- "project_users_24h": 2,
- },
- }
- def test_get_release_health_data_overview_users(self):
- data = self.backend.get_release_health_data_overview(
- [
- (self.project.id, self.session_release),
- (self.project.id, self.session_crashed_release),
- ],
- summary_stats_period="24h",
- health_stats_period="24h",
- stat="users",
- )
- stats = make_24h_stats(self.received - (24 * 3600), adjust_start=self.adjust_interval)
- stats[-1] = [stats[-1][0], 1]
- stats_ok = stats_crash = stats
- assert data == {
- (self.project.id, self.session_crashed_release): {
- "total_sessions": 1,
- "sessions_errored": 0,
- "total_sessions_24h": 1,
- "total_users": 1,
- "duration_p90": None,
- "sessions_crashed": 1,
- "total_users_24h": 1,
- "stats": {"24h": stats_crash},
- "crash_free_users": 0.0,
- "adoption": 100.0,
- "sessions_adoption": 33.33333333333333,
- "has_health_data": True,
- "crash_free_sessions": 0.0,
- "duration_p50": None,
- "total_project_sessions_24h": 3,
- "total_project_users_24h": 1,
- },
- (self.project.id, self.session_release): {
- "total_sessions": 2,
- "sessions_errored": 0,
- "total_sessions_24h": 2,
- "total_users": 1,
- "duration_p90": 57.0,
- "sessions_crashed": 0,
- "total_users_24h": 1,
- "stats": {"24h": stats_ok},
- "crash_free_users": 100.0,
- "adoption": 100.0,
- "sessions_adoption": 66.66666666666666,
- "has_health_data": True,
- "crash_free_sessions": 100.0,
- "duration_p50": 45.0,
- "total_project_sessions_24h": 3,
- "total_project_users_24h": 1,
- },
- }
- def test_get_release_health_data_overview_sessions(self):
- data = self.backend.get_release_health_data_overview(
- [
- (self.project.id, self.session_release),
- (self.project.id, self.session_crashed_release),
- ],
- summary_stats_period="24h",
- health_stats_period="24h",
- stat="sessions",
- )
- stats = make_24h_stats(self.received - (24 * 3600), adjust_start=self.adjust_interval)
- stats_ok = stats[:-1] + [[stats[-1][0], 2]]
- stats_crash = stats[:-1] + [[stats[-1][0], 1]]
- assert data == {
- (self.project.id, self.session_crashed_release): {
- "total_sessions": 1,
- "sessions_errored": 0,
- "total_sessions_24h": 1,
- "total_users": 1,
- "duration_p90": None,
- "sessions_crashed": 1,
- "total_users_24h": 1,
- "stats": {"24h": stats_crash},
- "crash_free_users": 0.0,
- "adoption": 100.0,
- "sessions_adoption": 33.33333333333333,
- "has_health_data": True,
- "crash_free_sessions": 0.0,
- "duration_p50": None,
- "total_project_sessions_24h": 3,
- "total_project_users_24h": 1,
- },
- (self.project.id, self.session_release): {
- "total_sessions": 2,
- "sessions_errored": 0,
- "total_sessions_24h": 2,
- "total_users": 1,
- "duration_p90": 57.0,
- "sessions_crashed": 0,
- "total_users_24h": 1,
- "stats": {"24h": stats_ok},
- "crash_free_users": 100.0,
- "sessions_adoption": 66.66666666666666,
- "adoption": 100.0,
- "has_health_data": True,
- "crash_free_sessions": 100.0,
- "duration_p50": 45.0,
- "total_project_sessions_24h": 3,
- "total_project_users_24h": 1,
- },
- }
- def test_fetching_release_sessions_time_bounds_for_different_release(self):
- """
- Test that ensures only session bounds for releases are calculated according
- to their respective release
- """
- # Same release session
- self.store_session(
- self.build_session(
- release=self.session_release,
- environment="prod",
- status="exited",
- started=self.session_started - 3600 * 2,
- received=self.received - 3600 * 2,
- )
- )
- # Different release session
- self.store_session(
- self.build_session(
- release=self.session_crashed_release,
- environment="prod",
- status="crashed",
- started=self.session_started - 3600 * 2,
- received=self.received - 3600 * 2,
- )
- )
- expected_formatted_lower_bound = (
- datetime.utcfromtimestamp(self.session_started - 3600 * 2)
- .replace(minute=0)
- .isoformat()[:19]
- + "Z"
- )
- expected_formatted_upper_bound = (
- datetime.utcfromtimestamp(self.session_started).replace(minute=0).isoformat()[:19] + "Z"
- )
- # Test for self.session_release
- data = self.backend.get_release_sessions_time_bounds(
- project_id=self.project.id,
- release=self.session_release,
- org_id=self.organization.id,
- environments=["prod"],
- )
- assert data == {
- "sessions_lower_bound": expected_formatted_lower_bound,
- "sessions_upper_bound": expected_formatted_upper_bound,
- }
- # Test for self.session_crashed_release
- data = self.backend.get_release_sessions_time_bounds(
- project_id=self.project.id,
- release=self.session_crashed_release,
- org_id=self.organization.id,
- environments=["prod"],
- )
- assert data == {
- "sessions_lower_bound": expected_formatted_lower_bound,
- "sessions_upper_bound": expected_formatted_upper_bound,
- }
- def test_fetching_release_sessions_time_bounds_for_different_release_with_no_sessions(self):
- """
- Test that ensures if no sessions are available for a specific release then the bounds
- should be returned as None
- """
- data = self.backend.get_release_sessions_time_bounds(
- project_id=self.project.id,
- release="different_release",
- org_id=self.organization.id,
- environments=["prod"],
- )
- assert data == {
- "sessions_lower_bound": None,
- "sessions_upper_bound": None,
- }
- def test_get_crash_free_breakdown(self):
- start = django_timezone.now() - timedelta(days=4)
- # it should work with and without environments
- for environments in [None, ["prod"]]:
- data = self.backend.get_crash_free_breakdown(
- project_id=self.project.id,
- release=self.session_release,
- start=start,
- environments=environments,
- )
- # Last returned date is generated within function, should be close to now:
- last_date = data[-1].pop("date")
- assert django_timezone.now() - last_date < timedelta(seconds=1)
- assert data == [
- {
- "crash_free_sessions": None,
- "crash_free_users": None,
- "date": start + timedelta(days=1),
- "total_sessions": 0,
- "total_users": 0,
- },
- {
- "crash_free_sessions": None,
- "crash_free_users": None,
- "date": start + timedelta(days=2),
- "total_sessions": 0,
- "total_users": 0,
- },
- {
- "crash_free_sessions": 100.0,
- "crash_free_users": 100.0,
- "total_sessions": 2,
- "total_users": 1,
- },
- ]
- data = self.backend.get_crash_free_breakdown(
- project_id=self.project.id,
- release=self.session_crashed_release,
- start=start,
- environments=["prod"],
- )
- data[-1].pop("date")
- assert data == [
- {
- "crash_free_sessions": None,
- "crash_free_users": None,
- "date": start + timedelta(days=1),
- "total_sessions": 0,
- "total_users": 0,
- },
- {
- "crash_free_sessions": None,
- "crash_free_users": None,
- "date": start + timedelta(days=2),
- "total_sessions": 0,
- "total_users": 0,
- },
- {
- "crash_free_sessions": 0.0,
- "crash_free_users": 0.0,
- "total_sessions": 1,
- "total_users": 1,
- },
- ]
- data = self.backend.get_crash_free_breakdown(
- project_id=self.project.id,
- release="non-existing",
- start=start,
- environments=["prod"],
- )
- data[-1].pop("date")
- assert data == [
- {
- "crash_free_sessions": None,
- "crash_free_users": None,
- "date": start + timedelta(days=1),
- "total_sessions": 0,
- "total_users": 0,
- },
- {
- "crash_free_sessions": None,
- "crash_free_users": None,
- "date": start + timedelta(days=2),
- "total_sessions": 0,
- "total_users": 0,
- },
- {
- "crash_free_sessions": None,
- "crash_free_users": None,
- "total_sessions": 0,
- "total_users": 0,
- },
- ]
- def test_basic_release_model_adoptions(self):
- """
- Test that the basic (project,release) data is returned
- """
- proj_id = self.project.id
- data = self.backend.get_changed_project_release_model_adoptions([proj_id])
- assert set(data) == {(proj_id, "foo@1.0.0"), (proj_id, "foo@2.0.0")}
- def test_old_release_model_adoptions(self):
- """
- Test that old entries (older that 72 h) are not returned
- """
- _100h = 100 * 60 * 60 # 100 hours in seconds
- proj_id = self.project.id
- self.store_session(
- self.build_session(
- release="foo@3.0.0",
- environment="prod",
- status="crashed",
- started=self.session_started - _100h,
- received=self.received - 3600 * 2,
- )
- )
- data = self.backend.get_changed_project_release_model_adoptions([proj_id])
- assert set(data) == {(proj_id, "foo@1.0.0"), (proj_id, "foo@2.0.0")}
- def test_multi_proj_release_model_adoptions(self):
- """Test that the api works with multiple projects"""
- proj_id = self.project.id
- new_proj_id = proj_id + 1
- self.store_session(
- self.build_session(
- project_id=new_proj_id,
- release="foo@3.0.0",
- environment="prod",
- status="crashed",
- started=self.session_started,
- received=self.received - 3600 * 2,
- )
- )
- data = self.backend.get_changed_project_release_model_adoptions([proj_id, new_proj_id])
- assert set(data) == {
- (proj_id, "foo@1.0.0"),
- (proj_id, "foo@2.0.0"),
- (new_proj_id, "foo@3.0.0"),
- }
- @staticmethod
- def _add_timestamps_to_series(series, start: datetime):
- one_day = 24 * 60 * 60
- day0 = one_day * int(start.timestamp() / one_day)
- def ts(days: int) -> int:
- return day0 + days * one_day
- return [[ts(i + 1), data] for i, data in enumerate(series)]
- def _test_get_project_release_stats(
- self, stat: OverviewStat, release: str, expected_series, expected_totals
- ):
- end = django_timezone.now()
- start = end - timedelta(days=4)
- stats, totals = self.backend.get_project_release_stats(
- self.project.id,
- release=release,
- stat=stat,
- rollup=86400,
- start=start,
- end=end,
- )
- # Let's not care about lists vs. tuples:
- stats = [[ts, data] for ts, data in stats]
- assert stats == self._add_timestamps_to_series(expected_series, start)
- assert totals == expected_totals
- def test_get_project_release_stats_users(self):
- self._test_get_project_release_stats(
- "users",
- self.session_release,
- [
- {
- "duration_p50": None,
- "duration_p90": None,
- "users": 0,
- "users_abnormal": 0,
- "users_crashed": 0,
- "users_errored": 0,
- "users_healthy": 0,
- },
- {
- "duration_p50": None,
- "duration_p90": None,
- "users": 0,
- "users_abnormal": 0,
- "users_crashed": 0,
- "users_errored": 0,
- "users_healthy": 0,
- },
- {
- "duration_p50": None,
- "duration_p90": None,
- "users": 0,
- "users_abnormal": 0,
- "users_crashed": 0,
- "users_errored": 0,
- "users_healthy": 0,
- },
- {
- "duration_p50": 45.0,
- "duration_p90": 57.0,
- "users": 1,
- "users_abnormal": 0,
- "users_crashed": 0,
- "users_errored": 0,
- "users_healthy": 1,
- },
- ],
- {
- "users": 1,
- "users_abnormal": 0,
- "users_crashed": 0,
- "users_errored": 0,
- "users_healthy": 1,
- },
- )
- def test_get_project_release_stats_users_crashed(self):
- self._test_get_project_release_stats(
- "users",
- self.session_crashed_release,
- [
- {
- "duration_p50": None,
- "duration_p90": None,
- "users": 0,
- "users_abnormal": 0,
- "users_crashed": 0,
- "users_errored": 0,
- "users_healthy": 0,
- },
- {
- "duration_p50": None,
- "duration_p90": None,
- "users": 0,
- "users_abnormal": 0,
- "users_crashed": 0,
- "users_errored": 0,
- "users_healthy": 0,
- },
- {
- "duration_p50": None,
- "duration_p90": None,
- "users": 0,
- "users_abnormal": 0,
- "users_crashed": 0,
- "users_errored": 0,
- "users_healthy": 0,
- },
- {
- "duration_p50": None,
- "duration_p90": None,
- "users": 1,
- "users_abnormal": 0,
- "users_crashed": 1,
- "users_errored": 0,
- "users_healthy": 0,
- },
- ],
- {
- "users": 1,
- "users_abnormal": 0,
- "users_crashed": 1,
- "users_errored": 0,
- "users_healthy": 0,
- },
- )
- def test_get_project_release_stats_sessions(self):
- self._test_get_project_release_stats(
- "sessions",
- self.session_release,
- [
- {
- "duration_p50": None,
- "duration_p90": None,
- "sessions": 0,
- "sessions_abnormal": 0,
- "sessions_crashed": 0,
- "sessions_errored": 0,
- "sessions_healthy": 0,
- },
- {
- "duration_p50": None,
- "duration_p90": None,
- "sessions": 0,
- "sessions_abnormal": 0,
- "sessions_crashed": 0,
- "sessions_errored": 0,
- "sessions_healthy": 0,
- },
- {
- "duration_p50": None,
- "duration_p90": None,
- "sessions": 0,
- "sessions_abnormal": 0,
- "sessions_crashed": 0,
- "sessions_errored": 0,
- "sessions_healthy": 0,
- },
- {
- "duration_p50": 45.0,
- "duration_p90": 57.0,
- "sessions": 2,
- "sessions_abnormal": 0,
- "sessions_crashed": 0,
- "sessions_errored": 0,
- "sessions_healthy": 2,
- },
- ],
- {
- "sessions": 2,
- "sessions_abnormal": 0,
- "sessions_crashed": 0,
- "sessions_errored": 0,
- "sessions_healthy": 2,
- },
- )
- def test_get_project_release_stats_sessions_crashed(self):
- self._test_get_project_release_stats(
- "sessions",
- self.session_crashed_release,
- [
- {
- "duration_p50": None,
- "duration_p90": None,
- "sessions": 0,
- "sessions_abnormal": 0,
- "sessions_crashed": 0,
- "sessions_errored": 0,
- "sessions_healthy": 0,
- },
- {
- "duration_p50": None,
- "duration_p90": None,
- "sessions": 0,
- "sessions_abnormal": 0,
- "sessions_crashed": 0,
- "sessions_errored": 0,
- "sessions_healthy": 0,
- },
- {
- "duration_p50": None,
- "duration_p90": None,
- "sessions": 0,
- "sessions_abnormal": 0,
- "sessions_crashed": 0,
- "sessions_errored": 0,
- "sessions_healthy": 0,
- },
- {
- "duration_p50": None,
- "duration_p90": None,
- "sessions": 1,
- "sessions_abnormal": 0,
- "sessions_crashed": 1,
- "sessions_errored": 0,
- "sessions_healthy": 0,
- },
- ],
- {
- "sessions": 1,
- "sessions_abnormal": 0,
- "sessions_crashed": 1,
- "sessions_errored": 0,
- "sessions_healthy": 0,
- },
- )
- def test_get_project_release_stats_no_sessions(self):
- """
- Test still returning correct data when no sessions are available
- :return:
- """
- self._test_get_project_release_stats(
- "sessions",
- "INEXISTENT-RELEASE",
- [
- {
- "duration_p50": None,
- "duration_p90": None,
- "sessions": 0,
- "sessions_abnormal": 0,
- "sessions_crashed": 0,
- "sessions_errored": 0,
- "sessions_healthy": 0,
- },
- {
- "duration_p50": None,
- "duration_p90": None,
- "sessions": 0,
- "sessions_abnormal": 0,
- "sessions_crashed": 0,
- "sessions_errored": 0,
- "sessions_healthy": 0,
- },
- {
- "duration_p50": None,
- "duration_p90": None,
- "sessions": 0,
- "sessions_abnormal": 0,
- "sessions_crashed": 0,
- "sessions_errored": 0,
- "sessions_healthy": 0,
- },
- {
- "duration_p50": None,
- "duration_p90": None,
- "sessions": 0,
- "sessions_abnormal": 0,
- "sessions_crashed": 0,
- "sessions_errored": 0,
- "sessions_healthy": 0,
- },
- ],
- {
- "sessions": 0,
- "sessions_abnormal": 0,
- "sessions_crashed": 0,
- "sessions_errored": 0,
- "sessions_healthy": 0,
- },
- )
- def test_get_project_release_stats_no_users(self):
- self._test_get_project_release_stats(
- "users",
- "INEXISTENT-RELEASE",
- [
- {
- "duration_p50": None,
- "duration_p90": None,
- "users": 0,
- "users_abnormal": 0,
- "users_crashed": 0,
- "users_errored": 0,
- "users_healthy": 0,
- },
- {
- "duration_p50": None,
- "duration_p90": None,
- "users": 0,
- "users_abnormal": 0,
- "users_crashed": 0,
- "users_errored": 0,
- "users_healthy": 0,
- },
- {
- "duration_p50": None,
- "duration_p90": None,
- "users": 0,
- "users_abnormal": 0,
- "users_crashed": 0,
- "users_errored": 0,
- "users_healthy": 0,
- },
- {
- "duration_p50": None,
- "duration_p90": None,
- "users": 0,
- "users_abnormal": 0,
- "users_crashed": 0,
- "users_errored": 0,
- "users_healthy": 0,
- },
- ],
- {
- "users": 0,
- "users_abnormal": 0,
- "users_crashed": 0,
- "users_errored": 0,
- "users_healthy": 0,
- },
- )
- @parametrize_backend
- class GetCrashFreeRateTestCase(TestCase, SnubaTestCase):
- """
- TestClass that tests that `get_current_and_previous_crash_free_rates` returns the correct
- `currentCrashFreeRate` and `previousCrashFreeRate` for each project
- TestData:
- Project 1:
- In the last 24h -> 2 Exited Sessions / 2 Total Sessions -> 100% Crash free rate
- In the previous 24h (>24h & <48h) -> 2 Exited + 1 Crashed Sessions / 3 Sessions -> 66.7%
- Project 2:
- In the last 24h -> 1 Exited + 1 Crashed / 2 Total Sessions -> 50% Crash free rate
- In the previous 24h (>24h & <48h) -> 0 Sessions -> None
- Project 3:
- In the last 24h -> 0 Sessions -> None
- In the previous 24h (>24h & <48h) -> 4 Exited + 1 Crashed / 5 Total Sessions -> 80%
- """
- def setUp(self):
- super().setUp()
- self.session_started = time.time() // 60 * 60
- self.session_started_gt_24_lt_48 = self.session_started - 30 * 60 * 60
- self.project2 = self.create_project(
- name="Bar2",
- slug="bar2",
- teams=[self.team],
- fire_project_created=True,
- organization=self.organization,
- )
- self.project3 = self.create_project(
- name="Bar3",
- slug="bar3",
- teams=[self.team],
- fire_project_created=True,
- organization=self.organization,
- )
- # Project 1
- for _ in range(0, 2):
- self.store_session(
- self.build_session(
- **{
- "project_id": self.project.id,
- "org_id": self.project.organization_id,
- "status": "exited",
- }
- )
- )
- for idx in range(0, 3):
- status = "exited"
- if idx == 2:
- status = "crashed"
- self.store_session(
- self.build_session(
- **{
- "project_id": self.project.id,
- "org_id": self.project.organization_id,
- "status": status,
- "started": self.session_started_gt_24_lt_48,
- }
- )
- )
- # Project 2
- for i in range(0, 2):
- status = "exited"
- if i == 1:
- status = "crashed"
- self.store_session(
- self.build_session(
- **{
- "project_id": self.project2.id,
- "org_id": self.project2.organization_id,
- "status": status,
- }
- )
- )
- # Project 3
- for i in range(0, 5):
- status = "exited"
- if i == 4:
- status = "crashed"
- self.store_session(
- self.build_session(
- **{
- "project_id": self.project3.id,
- "org_id": self.project3.organization_id,
- "status": status,
- "started": self.session_started_gt_24_lt_48,
- }
- )
- )
- def test_get_current_and_previous_crash_free_rates(self):
- now = django_timezone.now().replace(minute=15, second=23)
- last_24h_start = now - 24 * timedelta(hours=1)
- last_48h_start = now - 2 * 24 * timedelta(hours=1)
- data = self.backend.get_current_and_previous_crash_free_rates(
- org_id=self.organization.id,
- project_ids=[self.project.id, self.project2.id, self.project3.id],
- current_start=last_24h_start,
- current_end=now,
- previous_start=last_48h_start,
- previous_end=last_24h_start,
- rollup=3600,
- )
- assert data == {
- self.project.id: {
- "currentCrashFreeRate": 100,
- "previousCrashFreeRate": 66.66666666666667,
- },
- self.project2.id: {"currentCrashFreeRate": 50.0, "previousCrashFreeRate": None},
- self.project3.id: {"currentCrashFreeRate": None, "previousCrashFreeRate": 80.0},
- }
- def test_get_current_and_previous_crash_free_rates_with_zero_sessions(self):
- now = django_timezone.now().replace(minute=15, second=23)
- last_48h_start = now - 2 * 24 * timedelta(hours=1)
- last_72h_start = now - 3 * 24 * timedelta(hours=1)
- last_96h_start = now - 4 * 24 * timedelta(hours=1)
- data = self.backend.get_current_and_previous_crash_free_rates(
- org_id=self.organization.id,
- project_ids=[self.project.id],
- current_start=last_72h_start,
- current_end=last_48h_start,
- previous_start=last_96h_start,
- previous_end=last_72h_start,
- rollup=3600,
- )
- assert data == {
- self.project.id: {
- "currentCrashFreeRate": None,
- "previousCrashFreeRate": None,
- },
- }
- @region_silo_test
- @parametrize_backend
- class GetProjectReleasesCountTest(TestCase, SnubaTestCase):
- def test_empty(self):
- # Test no errors when no session data
- org = self.create_organization()
- proj = self.create_project(organization=org)
- assert (
- self.backend.get_project_releases_count(
- org.id, [proj.id], "crash_free_users", stats_period="14d"
- )
- == 0
- )
- def test_with_other_metrics(self):
- if not self.backend.is_metrics_based():
- return
- # Test no errors when no session data
- org = self.create_organization()
- proj = self.create_project(organization=org)
- # Insert a different set metric:
- for value in 1, 2, 3:
- self.store_metric(
- org_id=org.id,
- project_id=proj.id,
- name="foobarbaz", # any other metric ID
- timestamp=int(time.time()),
- tags={},
- type="set",
- value=value,
- use_case_id=UseCaseID.SESSIONS,
- )
- assert (
- self.backend.get_project_releases_count(
- org.id, [proj.id], "crash_free_users", stats_period="14d"
- )
- == 0
- )
- def test(self):
- project_release_1 = self.create_release(self.project)
- other_project = self.create_project()
- other_project_release_1 = self.create_release(other_project)
- self.bulk_store_sessions(
- [
- self.build_session(
- environment=self.environment.name, release=project_release_1.version
- ),
- self.build_session(
- environment="staging",
- project_id=other_project.id,
- release=other_project_release_1.version,
- ),
- ]
- )
- assert (
- self.backend.get_project_releases_count(
- self.organization.id, [self.project.id], "sessions"
- )
- == 1
- )
- assert (
- self.backend.get_project_releases_count(
- self.organization.id, [self.project.id], "users"
- )
- == 1
- )
- assert (
- self.backend.get_project_releases_count(
- self.organization.id, [self.project.id, other_project.id], "sessions"
- )
- == 2
- )
- assert (
- self.backend.get_project_releases_count(
- self.organization.id,
- [self.project.id, other_project.id],
- "users",
- )
- == 2
- )
- assert (
- self.backend.get_project_releases_count(
- self.organization.id,
- [self.project.id, other_project.id],
- "sessions",
- environments=[self.environment.name],
- )
- == 1
- )
- @parametrize_backend
- class CheckReleasesHaveHealthDataTest(TestCase, SnubaTestCase):
- def run_test(self, expected, projects, releases, start=None, end=None):
- if not start:
- start = datetime.now() - timedelta(days=1)
- if not end:
- end = datetime.now()
- assert self.backend.check_releases_have_health_data(
- self.organization.id,
- [p.id for p in projects],
- [r.version for r in releases],
- start,
- end,
- ) == {v.version for v in expected}
- def test_empty(self):
- # Test no errors when no session data
- project_release_1 = self.create_release(self.project)
- self.run_test([], [self.project], [project_release_1])
- def test(self):
- other_project = self.create_project()
- release_1 = self.create_release(
- self.project, version="1", additional_projects=[other_project]
- )
- release_2 = self.create_release(other_project, version="2")
- self.bulk_store_sessions(
- [
- self.build_session(release=release_1),
- self.build_session(project_id=other_project, release=release_1),
- self.build_session(project_id=other_project, release=release_2),
- ]
- )
- self.run_test([release_1], [self.project], [release_1])
- self.run_test([release_1], [self.project], [release_1, release_2])
- self.run_test([release_1], [other_project], [release_1])
- self.run_test([release_1, release_2], [other_project], [release_1, release_2])
- self.run_test([release_1, release_2], [self.project, other_project], [release_1, release_2])
- @parametrize_backend
- class CheckNumberOfSessions(TestCase, SnubaTestCase):
- def setUp(self):
- super().setUp()
- self.dev_env = self.create_environment(name="development", project=self.project)
- self.prod_env = self.create_environment(name="production", project=self.project)
- self.test_env = self.create_environment(name="test", project=self.project)
- self.another_project = self.create_project()
- self.third_project = self.create_project()
- # now_dt should be set to 17:40 of some day not in the future and (system time - now_dt)
- # must be less than 90 days for the metrics DB TTL
- ONE_DAY_AGO = datetime.now(tz=timezone.utc) - timedelta(days=1)
- self.now_dt = ONE_DAY_AGO.replace(hour=17, minute=40, second=0)
- self._5_min_ago_dt = self.now_dt - timedelta(minutes=5)
- self._30_min_ago_dt = self.now_dt - timedelta(minutes=30)
- self._1_h_ago_dt = self.now_dt - timedelta(hours=1)
- self._2_h_ago_dt = self.now_dt - timedelta(hours=2)
- self._3_h_ago_dt = self.now_dt - timedelta(hours=3)
- self.now = self.now_dt.timestamp()
- self._5_min_ago = self._5_min_ago_dt.timestamp()
- self._30_min_ago = self._30_min_ago_dt.timestamp()
- self._1_h_ago = self._1_h_ago_dt.timestamp()
- self._2_h_ago = self._2_h_ago_dt.timestamp()
- self._3_h_ago = self._3_h_ago_dt.timestamp()
- def test_no_sessions(self):
- """
- Tests that when there are no sessions the function behaves and returns 0
- """
- actual = self.backend.get_project_sessions_count(
- project_id=self.project.id,
- environment_id=None,
- rollup=60,
- start=self._30_min_ago_dt,
- end=self.now_dt,
- )
- assert 0 == actual
- def test_sessions_in_environment(self):
- """
- Tests that it correctly picks up the sessions for the selected environment
- in the selected time, not counting other environments and other times
- """
- dev = self.dev_env.name
- prod = self.prod_env.name
- self.bulk_store_sessions(
- [
- self.build_session(
- environment=dev, received=self._5_min_ago, started=self._5_min_ago
- ),
- self.build_session(
- environment=prod, received=self._5_min_ago, started=self._5_min_ago
- ),
- self.build_session(
- environment=prod, received=self._5_min_ago, started=self._5_min_ago
- ),
- self.build_session(environment=prod, received=self._2_h_ago, started=self._2_h_ago),
- ]
- )
- actual = self.backend.get_project_sessions_count(
- project_id=self.project.id,
- environment_id=self.prod_env.id,
- rollup=60,
- start=self._1_h_ago_dt,
- end=self.now_dt,
- )
- assert actual == 2
- def test_environment_without_sessions(self):
- """
- We should get zero sessions, even if the environment name has not been indexed
- by the metrics indexer.
- """
- env_without_sessions = self.create_environment(
- name="this_has_no_sessions", project=self.project
- )
- self.bulk_store_sessions(
- [
- self.build_session(
- environment=self.prod_env.name,
- received=self._5_min_ago,
- started=self._5_min_ago,
- ),
- self.build_session(
- environment=None, received=self._5_min_ago, started=self._5_min_ago
- ),
- ]
- )
- count_env_all = self.backend.get_project_sessions_count(
- project_id=self.project.id,
- environment_id=None,
- rollup=60,
- start=self._1_h_ago_dt,
- end=self.now_dt,
- )
- assert count_env_all == 2
- count_env_new = self.backend.get_project_sessions_count(
- project_id=self.project.id,
- environment_id=env_without_sessions.id,
- rollup=60,
- start=self._1_h_ago_dt,
- end=self.now_dt,
- )
- assert count_env_new == 0
- def test_sessions_in_all_environments(self):
- """
- When the environment is not specified sessions from all environments are counted
- """
- dev = self.dev_env.name
- prod = self.prod_env.name
- self.bulk_store_sessions(
- [
- self.build_session(
- environment=dev, received=self._5_min_ago, started=self._5_min_ago
- ),
- self.build_session(
- environment=prod, received=self._5_min_ago, started=self._5_min_ago
- ),
- self.build_session(
- environment=prod, received=self._5_min_ago, started=self._5_min_ago
- ),
- self.build_session(environment=prod, received=self._2_h_ago, started=self._2_h_ago),
- self.build_session(environment=dev, received=self._2_h_ago, started=self._2_h_ago),
- ]
- )
- actual = self.backend.get_project_sessions_count(
- project_id=self.project.id,
- environment_id=None,
- rollup=60,
- start=self._1_h_ago_dt,
- end=self.now_dt,
- )
- assert actual == 3
- def test_sessions_from_multiple_projects(self):
- """
- Only sessions from the specified project are considered
- """
- dev = self.dev_env.name
- prod = self.prod_env.name
- self.bulk_store_sessions(
- [
- self.build_session(
- environment=dev, received=self._5_min_ago, started=self._5_min_ago
- ),
- self.build_session(
- environment=prod, received=self._5_min_ago, started=self._5_min_ago
- ),
- self.build_session(
- environment=prod,
- received=self._5_min_ago,
- project_id=self.another_project.id,
- started=self._5_min_ago,
- ),
- ]
- )
- actual = self.backend.get_project_sessions_count(
- project_id=self.project.id,
- environment_id=None,
- rollup=60,
- start=self._1_h_ago_dt,
- end=self.now_dt,
- )
- assert actual == 2
- def test_sessions_per_project_no_sessions(self):
- """
- Tests that no sessions are returned
- """
- actual = self.backend.get_num_sessions_per_project(
- project_ids=[self.project.id, self.another_project.id],
- environment_ids=None,
- rollup=60,
- start=self._30_min_ago_dt,
- end=self.now_dt,
- )
- assert [] == actual
- def test_sesions_per_project_multiple_projects(self):
- dev = self.dev_env.name
- prod = self.prod_env.name
- test = self.test_env.name
- p1 = self.project
- p2 = self.another_project
- p3 = self.third_project
- self.bulk_store_sessions(
- [
- # counted in p1
- self.build_session(
- environment=dev, received=self._5_min_ago, started=self._5_min_ago
- ),
- self.build_session(
- environment=prod, received=self._5_min_ago, started=self._5_min_ago
- ),
- self.build_session(
- environment=dev, received=self._30_min_ago, started=self._30_min_ago
- ),
- # ignored in p1
- # ignored env
- self.build_session(
- environment=test, received=self._30_min_ago, started=self._30_min_ago
- ),
- # too old
- self.build_session(environment=prod, received=self._3_h_ago, started=self._3_h_ago),
- # counted in p2
- self.build_session(
- environment=dev,
- received=self._5_min_ago,
- project_id=p2.id,
- started=self._5_min_ago,
- ),
- # ignored in p2
- # ignored env
- self.build_session(
- environment=test,
- received=self._5_min_ago,
- project_id=p2.id,
- started=self._5_min_ago,
- ),
- # too old
- self.build_session(
- environment=prod,
- received=self._3_h_ago,
- project_id=p2.id,
- started=self._3_h_ago,
- ),
- # ignored p3
- self.build_session(
- environment=dev,
- received=self._5_min_ago,
- project_id=p3.id,
- started=self._5_min_ago,
- ),
- ]
- )
- actual = self.backend.get_num_sessions_per_project(
- project_ids=[self.project.id, self.another_project.id],
- environment_ids=[self.dev_env.id, self.prod_env.id],
- rollup=60,
- start=self._2_h_ago_dt,
- end=self.now_dt,
- )
- assert set(actual) == {(p1.id, 3), (p2.id, 1)}
- for eids in ([], None):
- actual = self.backend.get_num_sessions_per_project(
- project_ids=[self.project.id, self.another_project.id],
- environment_ids=eids,
- rollup=60,
- start=self._2_h_ago_dt,
- end=self.now_dt,
- )
- assert set(actual) == {(p1.id, 4), (p2.id, 2)}
- @region_silo_test(stable=True)
- @parametrize_backend
- class InitWithoutUserTestCase(TestCase, SnubaTestCase):
- def setUp(self):
- super().setUp()
- self.received = time.time()
- self.session_started = time.time() // 60 * 60
- self.session_release = "foo@1.0.0"
- session_1 = "5d52fd05-fcc9-4bf3-9dc9-267783670341"
- session_2 = "5e910c1a-6941-460e-9843-24103fb6a63c"
- session_3 = "a148c0c5-06a2-423b-8901-6b43b812cf82"
- user_1 = "39887d89-13b2-4c84-8c23-5d13d2102666"
- user_2 = "39887d89-13b2-4c84-8c23-5d13d2102667"
- user_3 = "39887d89-13b2-4c84-8c23-5d13d2102668"
- self.bulk_store_sessions(
- [
- self.build_session(
- distinct_id=user_1,
- session_id=session_1,
- status="exited",
- release=self.session_release,
- environment="prod",
- started=self.session_started,
- received=self.received,
- ),
- self.build_session(
- distinct_id=user_2,
- session_id=session_2,
- status="crashed",
- release=self.session_release,
- environment="prod",
- started=self.session_started,
- received=self.received,
- ),
- # session_3 initial update: no user ID
- self.build_session(
- distinct_id=None,
- session_id=session_3,
- status="ok",
- seq=0,
- release=self.session_release,
- environment="prod",
- started=self.session_started,
- received=self.received,
- ),
- # session_3 subsequent update: user ID is here!
- self.build_session(
- distinct_id=user_3,
- session_id=session_3,
- status="ok",
- seq=123,
- release=self.session_release,
- environment="prod",
- started=self.session_started,
- received=self.received,
- ),
- ]
- )
- def test_get_release_adoption(self):
- data = self.backend.get_release_adoption(
- [
- (self.project.id, self.session_release),
- ]
- )
- inner = data[(self.project.id, self.session_release)]
- assert inner["users_24h"] == 3
- def test_get_release_health_data_overview_users(self):
- data = self.backend.get_release_health_data_overview(
- [
- (self.project.id, self.session_release),
- ],
- summary_stats_period="24h",
- health_stats_period="24h",
- stat="users",
- )
- inner = data[(self.project.id, self.session_release)]
- assert inner["total_users"] == 3
- assert inner["total_users_24h"] == 3
- assert inner["crash_free_users"] == 66.66666666666667
- assert inner["total_project_users_24h"] == 3
- def test_get_crash_free_breakdown(self):
- start = django_timezone.now() - timedelta(days=4)
- data = self.backend.get_crash_free_breakdown(
- project_id=self.project.id,
- release=self.session_release,
- start=start,
- environments=["prod"],
- )
- # Last returned date is generated within function, should be close to now:
- last_date = data[-1].pop("date")
- assert django_timezone.now() - last_date < timedelta(seconds=1)
- assert data == [
- {
- "crash_free_sessions": None,
- "crash_free_users": None,
- "date": start + timedelta(days=1),
- "total_sessions": 0,
- "total_users": 0,
- },
- {
- "crash_free_sessions": None,
- "crash_free_users": None,
- "date": start + timedelta(days=2),
- "total_sessions": 0,
- "total_users": 0,
- },
- {
- "crash_free_sessions": 66.66666666666667,
- "crash_free_users": 66.66666666666667,
- "total_sessions": 3,
- "total_users": 3,
- },
- ]
- def test_get_project_release_stats_users(self):
- end = django_timezone.now()
- start = end - timedelta(days=4)
- stats, totals = self.backend.get_project_release_stats(
- self.project.id,
- release=self.session_release,
- stat="users",
- rollup=86400,
- start=start,
- end=end,
- )
- assert stats[3][1] == {
- "duration_p50": 60.0,
- "duration_p90": 60.0,
- "users": 3,
- "users_abnormal": 0,
- "users_crashed": 1,
- "users_errored": 0,
- "users_healthy": 2,
- }
|