From 1e4199d75bfd37ae565a3a0ab05ac76db76e5aa8 Mon Sep 17 00:00:00 2001 From: Ryan Albrecht Date: Fri, 18 Jul 2025 13:11:11 -0700 Subject: [PATCH 1/6] wip: update queries to fetch the new metric --- src/sentry/release_health/metrics.py | 28 ++++++++++++++++++- .../release_health/metrics_sessions_v2.py | 12 ++++++-- src/sentry/snuba/metrics/naming_layer/mri.py | 14 +++++++++- 3 files changed, 50 insertions(+), 4 deletions(-) diff --git a/src/sentry/release_health/metrics.py b/src/sentry/release_health/metrics.py index 0bcd5a424a5ce7..a77d9956158441 100644 --- a/src/sentry/release_health/metrics.py +++ b/src/sentry/release_health/metrics.py @@ -780,6 +780,11 @@ def _get_session_by_status_for_overview( select = [ MetricField(metric_mri=SessionMRI.ABNORMAL.value, alias="abnormal", op=None), + MetricField( + metric_mri=SessionMRI.UNHANDLED.value, + alias="unhandled", + op=None, + ), MetricField(metric_mri=SessionMRI.CRASHED.value, alias="crashed", op=None), MetricField(metric_mri=SessionMRI.ALL.value, alias="init", op=None), MetricField( @@ -820,7 +825,13 @@ def _get_session_by_status_for_overview( release = by.get("release") totals = group.get("totals", {}) - for status in ["abnormal", "crashed", "init", "errored_preaggr"]: + for status in [ + "abnormal", + "unhandled", + "crashed", + "init", + "errored_preaggr", + ]: value = totals.get(status) if value is not None and value != 0.0: ret_val[(proj_id, release, status)] = value @@ -1037,6 +1048,7 @@ def get_release_health_data_overview( if not has_health_data and summary_stats_period != "90d": fetch_has_health_data_releases.add((project_id, release)) + sessions_unhandled = rv_sessions.get((project_id, release, "unhandled"), 0) sessions_crashed = rv_sessions.get((project_id, release, "crashed"), 0) users_crashed = rv_users.get((project_id, release, "crashed_users"), 0) @@ -1051,10 +1063,14 @@ def get_release_health_data_overview( "total_sessions": total_sessions, "total_users": total_users, "has_health_data": has_health_data, + "sessions_unhandled": sessions_unhandled, "sessions_crashed": sessions_crashed, "crash_free_users": ( 100 - users_crashed / total_users * 100 if total_users else None ), + "handled_sessions": ( + 100 - (sessions_unhandled + sessions_crashed) / total_sessions * 100 + ), "crash_free_sessions": ( 100 - sessions_crashed / float(total_sessions) * 100 if total_sessions else None ), @@ -1063,6 +1079,7 @@ def get_release_health_data_overview( rv_errored_sessions.get((project_id, release), 0) + rv_sessions.get((project_id, release, "errored_preaggr"), 0) - sessions_crashed + - sessions_unhandled - rv_sessions.get((project_id, release, "abnormal"), 0), ), "duration_p50": None, @@ -1427,6 +1444,9 @@ def get_project_release_stats( MetricField( metric_mri=SessionMRI.CRASHED_USER.value, alias="users_crashed", op=None ), + MetricField( + metric_mri=SessionMRI.UNHANDLED_USER.value, alias="users_unhandled", op=None + ), MetricField( metric_mri=SessionMRI.ERRORED_USER.value, alias="users_errored", op=None ), @@ -1441,6 +1461,11 @@ def get_project_release_stats( metric_mri=SessionMRI.ABNORMAL.value, alias="sessions_abnormal", op=None ), MetricField(metric_mri=SessionMRI.CRASHED.value, alias="sessions_crashed", op=None), + MetricField( + metric_mri=SessionMRI.UNHANDLED.value, + alias="sessions_unhandled", + op=None, + ), MetricField(metric_mri=SessionMRI.ERRORED.value, alias="sessions_errored", op=None), MetricField(metric_mri=SessionMRI.HEALTHY.value, alias="sessions_healthy", op=None), ] @@ -1500,6 +1525,7 @@ def get_project_release_stats( f"{stat}": 0, f"{stat}_abnormal": 0, f"{stat}_crashed": 0, + f"{stat}_unhandled": 0, f"{stat}_errored": 0, f"{stat}_healthy": 0, } diff --git a/src/sentry/release_health/metrics_sessions_v2.py b/src/sentry/release_health/metrics_sessions_v2.py index 5c47102fdad97c..f12d94eeb6689a 100644 --- a/src/sentry/release_health/metrics_sessions_v2.py +++ b/src/sentry/release_health/metrics_sessions_v2.py @@ -1,7 +1,7 @@ -""" This module offers the same functionality as sessions_v2, but pulls its data +"""This module offers the same functionality as sessions_v2, but pulls its data from the `metrics` dataset instead of `sessions`. -Do not call this module directly. Use the `release_health` service instead. """ +Do not call this module directly. Use the `release_health` service instead.""" import logging from abc import ABC, abstractmethod @@ -73,6 +73,7 @@ class SessionStatus(Enum): CRASHED = "crashed" ERRORED = "errored" HEALTHY = "healthy" + UNHANDLED = "unhandled" ALL_STATUSES = frozenset(iter(SessionStatus)) @@ -242,6 +243,7 @@ def _get_metric_fields( self.status_to_metric_field[SessionStatus.ABNORMAL], self.status_to_metric_field[SessionStatus.CRASHED], self.status_to_metric_field[SessionStatus.ERRORED], + self.status_to_metric_field[SessionStatus.UNHANDLED], ] return [self.get_all_field()] @@ -265,6 +267,7 @@ class SumSessionField(CountField): SessionStatus.ABNORMAL: MetricField(None, SessionMRI.ABNORMAL.value), SessionStatus.CRASHED: MetricField(None, SessionMRI.CRASHED.value), SessionStatus.ERRORED: MetricField(None, SessionMRI.ERRORED.value), + SessionStatus.UNHANDLED: MetricField(None, SessionMRI.UNHANDLED.value), None: MetricField(None, SessionMRI.ALL.value), } @@ -298,6 +301,7 @@ def __init__( SessionStatus.ABNORMAL: MetricField(None, SessionMRI.ABNORMAL_USER.value), SessionStatus.CRASHED: MetricField(None, SessionMRI.CRASHED_USER.value), SessionStatus.ERRORED: MetricField(None, SessionMRI.ERRORED_USER.value), + SessionStatus.UNHANDLED: MetricField(None, SessionMRI.UNHANDLED_USER.value), None: MetricField(None, SessionMRI.ALL_USER.value), } @@ -335,6 +339,8 @@ class SimpleForwardingField(Field): """ field_name_to_metric_name = { + "unhandled_rate(session)": SessionMRI.UNHANDLED_RATE, + "unhandled_rate(user)": SessionMRI.UNHANDLED_USER_RATE, "crash_rate(session)": SessionMRI.CRASH_RATE, "crash_rate(user)": SessionMRI.CRASH_USER_RATE, "crash_free_rate(session)": SessionMRI.CRASH_FREE_RATE, @@ -373,6 +379,8 @@ def _get_metric_fields( "p95(session.duration)": DurationField, "p99(session.duration)": DurationField, "max(session.duration)": DurationField, + "unhandled_rate(session)": SimpleForwardingField, + "unhandled_rate(user)": SimpleForwardingField, "crash_rate(session)": SimpleForwardingField, "crash_rate(user)": SimpleForwardingField, "crash_free_rate(session)": SimpleForwardingField, diff --git a/src/sentry/snuba/metrics/naming_layer/mri.py b/src/sentry/snuba/metrics/naming_layer/mri.py index 33ae019c23f1f8..b19ef312ef3f95 100644 --- a/src/sentry/snuba/metrics/naming_layer/mri.py +++ b/src/sentry/snuba/metrics/naming_layer/mri.py @@ -72,20 +72,32 @@ class SessionMRI(Enum): ERRORED_PREAGGREGATED = "e:sessions/error.preaggr@none" ERRORED_SET = "e:sessions/error.unique@none" ERRORED_ALL = "e:sessions/all_errored@none" + HANDLED = "e:sessions/handled.unique@none" # all sessions excluding handled and crashed + UNHANDLED = "e:sessions/unhandled@none" # unhandled, does not include crashed CRASHED_AND_ABNORMAL = "e:sessions/crashed_abnormal@none" CRASHED = "e:sessions/crashed@none" CRASH_FREE = "e:sessions/crash_free@none" ABNORMAL = "e:sessions/abnormal@none" + HANDLED_RATE = "e:sessions/handled_rate@ratio" # all sessions excluding handled and crashed + UNHANDLED_RATE = "e:sessions/unhandled_rate@ratio" # unhandled, does not include crashed CRASH_RATE = "e:sessions/crash_rate@ratio" - CRASH_FREE_RATE = "e:sessions/crash_free_rate@ratio" + CRASH_FREE_RATE = "e:sessions/crash_free_rate@ratio" # includes handled and unhandled ALL_USER = "e:sessions/user.all@none" HEALTHY_USER = "e:sessions/user.healthy@none" ERRORED_USER = "e:sessions/user.errored@none" ERRORED_USER_ALL = "e:sessions/user.all_errored@none" + HANDLED_USER = "e:sessions/user.handled@none" # all sessions excluding handled and crashed + UNHANDLED_USER = "e:sessions/user.unhandled@none" # unhandled, does not include crashed CRASHED_AND_ABNORMAL_USER = "e:sessions/user.crashed_abnormal@none" CRASHED_USER = "e:sessions/user.crashed@none" CRASH_FREE_USER = "e:sessions/user.crash_free@none" ABNORMAL_USER = "e:sessions/user.abnormal@none" + HANDLED_USER_RATE = ( + "e:sessions/user.handled_rate@ratio" # all sessions excluding handled and crashed + ) + UNHANDLED_USER_RATE = ( + "e:sessions/user.unhandled_rate@ratio" # unhandled, does not include crashed + ) CRASH_USER_RATE = "e:sessions/user.crash_rate@ratio" CRASH_FREE_USER_RATE = "e:sessions/user.crash_free_rate@ratio" ANR_USER = "e:sessions/user.anr@none" From f396c1ac1a3d203d8f86a7170c0592e37c959ad9 Mon Sep 17 00:00:00 2001 From: Colton Allen Date: Tue, 22 Jul 2025 14:58:53 -0500 Subject: [PATCH 2/6] Fix typing --- src/sentry/release_health/base.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/sentry/release_health/base.py b/src/sentry/release_health/base.py index 8b047830709803..27caec424f1a18 100644 --- a/src/sentry/release_health/base.py +++ b/src/sentry/release_health/base.py @@ -36,6 +36,8 @@ "crash_free_rate(user)", "anr_rate()", "foreground_anr_rate()", + "unhandled_rate(session)", + "unhandled_rate(user)", ] GroupByFieldName = Literal[ @@ -182,6 +184,8 @@ class ReleaseHealthOverview(TypedDict, total=False): duration_p50: float | None duration_p90: float | None stats: Mapping[StatsPeriod, ReleaseHealthStats] + sessions_unhandled: int + handled_sessions: float class CrashFreeBreakdown(TypedDict): From d70524166626adc906bea7bf46adb60aa5264636 Mon Sep 17 00:00:00 2001 From: Ryan Albrecht Date: Tue, 22 Jul 2025 13:19:10 -0700 Subject: [PATCH 3/6] account for nullable total_sessions --- src/sentry/release_health/metrics.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/sentry/release_health/metrics.py b/src/sentry/release_health/metrics.py index a77d9956158441..bc7558dcc451d2 100644 --- a/src/sentry/release_health/metrics.py +++ b/src/sentry/release_health/metrics.py @@ -1053,6 +1053,8 @@ def get_release_health_data_overview( users_crashed = rv_users.get((project_id, release, "crashed_users"), 0) + sum_unhandled = sessions_unhandled + sessions_crashed + rv_row = rv[project_id, release] = { "adoption": adoption_info.get("adoption"), "sessions_adoption": adoption_info.get("sessions_adoption"), @@ -1069,7 +1071,7 @@ def get_release_health_data_overview( 100 - users_crashed / total_users * 100 if total_users else None ), "handled_sessions": ( - 100 - (sessions_unhandled + sessions_crashed) / total_sessions * 100 + 100 - sum_unhandled / float(total_sessions) * 100 if total_sessions else None ), "crash_free_sessions": ( 100 - sessions_crashed / float(total_sessions) * 100 if total_sessions else None From c16aa6d6a92f5ef01a7c679431202b3980f2ab79 Mon Sep 17 00:00:00 2001 From: Ryan Albrecht Date: Tue, 22 Jul 2025 13:33:34 -0700 Subject: [PATCH 4/6] update types to allow for None --- src/sentry/release_health/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sentry/release_health/base.py b/src/sentry/release_health/base.py index 27caec424f1a18..ac1bd7e1036325 100644 --- a/src/sentry/release_health/base.py +++ b/src/sentry/release_health/base.py @@ -185,7 +185,7 @@ class ReleaseHealthOverview(TypedDict, total=False): duration_p90: float | None stats: Mapping[StatsPeriod, ReleaseHealthStats] sessions_unhandled: int - handled_sessions: float + handled_sessions: float | None class CrashFreeBreakdown(TypedDict): From 2768bd0763f3cb9278274453976d461a685cefd8 Mon Sep 17 00:00:00 2001 From: Ryan Albrecht Date: Tue, 22 Jul 2025 16:11:43 -0700 Subject: [PATCH 5/6] missed some spots --- src/sentry/release_health/base.py | 2 ++ src/sentry/release_health/metrics.py | 10 +++------- src/sentry/testutils/cases.py | 2 +- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/sentry/release_health/base.py b/src/sentry/release_health/base.py index ac1bd7e1036325..cd7d72e2aa9064 100644 --- a/src/sentry/release_health/base.py +++ b/src/sentry/release_health/base.py @@ -206,6 +206,7 @@ class UserCounts(TypedDict): users_healthy: int users_crashed: int users_abnormal: int + users_unhandled: int users_errored: int @@ -218,6 +219,7 @@ class SessionCounts(TypedDict): sessions_healthy: int sessions_crashed: int sessions_abnormal: int + sessions_unhandled: int sessions_errored: int diff --git a/src/sentry/release_health/metrics.py b/src/sentry/release_health/metrics.py index bc7558dcc451d2..f80a18e8dbbd3a 100644 --- a/src/sentry/release_health/metrics.py +++ b/src/sentry/release_health/metrics.py @@ -720,7 +720,7 @@ def _get_errored_sessions_for_overview( end: datetime, ) -> Mapping[tuple[int, str], int]: """ - Count of errored sessions, incl fatal (abnormal, crashed) sessions, + Count of errored sessions, incl fatal (abnormal, unhandled, crashed) sessions, excl errored *preaggregated* sessions """ project_ids = [p.id for p in projects] @@ -774,17 +774,13 @@ def _get_session_by_status_for_overview( end: datetime, ) -> Mapping[tuple[int, str, str], int]: """ - Counts of init, abnormal and crashed sessions, purpose-built for overview + Counts of init, abnormal, unhandled and crashed sessions, purpose-built for overview """ project_ids = [p.id for p in projects] select = [ MetricField(metric_mri=SessionMRI.ABNORMAL.value, alias="abnormal", op=None), - MetricField( - metric_mri=SessionMRI.UNHANDLED.value, - alias="unhandled", - op=None, - ), + MetricField(metric_mri=SessionMRI.UNHANDLED.value, alias="unhandled", op=None), MetricField(metric_mri=SessionMRI.CRASHED.value, alias="crashed", op=None), MetricField(metric_mri=SessionMRI.ALL.value, alias="init", op=None), MetricField( diff --git a/src/sentry/testutils/cases.py b/src/sentry/testutils/cases.py index 441c2fe9536237..a6adc538c5d2bf 100644 --- a/src/sentry/testutils/cases.py +++ b/src/sentry/testutils/cases.py @@ -1455,7 +1455,7 @@ def push(mri: str, tags, value): elif not user_is_nil: push(SessionMRI.RAW_USER.value, {}, user) - if status in ("abnormal", "crashed"): # fatal + if status in ("abnormal", "unhandled", "crashed"): # fatal push(SessionMRI.RAW_SESSION.value, {"session.status": status}, +1) if not user_is_nil: push(SessionMRI.RAW_USER.value, {"session.status": status}, user) From 39f9c510849894ea3af31df98138c441a7321ad1 Mon Sep 17 00:00:00 2001 From: Ryan Albrecht Date: Wed, 23 Jul 2025 10:18:10 -0700 Subject: [PATCH 6/6] wip more touchpoints, and failing tests :( --- src/sentry/snuba/metrics/fields/snql.py | 14 ++++++++ .../snuba/metrics/naming_layer/public.py | 2 ++ src/sentry/snuba/sessions_v2.py | 18 ++++++++-- .../frontend/debug/debug_chart_renderer.py | 5 +++ static/app/types/organization.tsx | 3 ++ .../test_organization_release_health_data.py | 7 +++- tests/sentry/snuba/metrics/test_snql.py | 5 +++ .../endpoints/test_organization_sessions.py | 33 ++++++++++++++++--- tests/snuba/sessions/test_sessions_v2.py | 10 ++++++ 9 files changed, 88 insertions(+), 9 deletions(-) diff --git a/src/sentry/snuba/metrics/fields/snql.py b/src/sentry/snuba/metrics/fields/snql.py index 85925024144292..2d307e1bb499ca 100644 --- a/src/sentry/snuba/metrics/fields/snql.py +++ b/src/sentry/snuba/metrics/fields/snql.py @@ -246,6 +246,20 @@ def all_users(org_id: int, metric_ids: Sequence[int], alias: str | None = None) return uniq_aggregation_on_metric(metric_ids, alias) +def unhandled_sessions( + org_id: int, metric_ids: Sequence[int], alias: str | None = None +) -> Function: + return _counter_sum_aggregation_on_session_status_factory( + org_id, session_status="unhandled", metric_ids=metric_ids, alias=alias + ) + + +def unhandled_users(org_id: int, metric_ids: Sequence[int], alias: str | None = None) -> Function: + return _set_uniq_aggregation_on_session_status_factory( + org_id, session_status="unhandled", metric_ids=metric_ids, alias=alias + ) + + def crashed_sessions(org_id: int, metric_ids: Sequence[int], alias: str | None = None) -> Function: return _counter_sum_aggregation_on_session_status_factory( org_id, session_status="crashed", metric_ids=metric_ids, alias=alias diff --git a/src/sentry/snuba/metrics/naming_layer/public.py b/src/sentry/snuba/metrics/naming_layer/public.py index 40d262b90fab0b..a470064ce5c6b7 100644 --- a/src/sentry/snuba/metrics/naming_layer/public.py +++ b/src/sentry/snuba/metrics/naming_layer/public.py @@ -36,6 +36,7 @@ class SessionMetricKey(Enum): DURATION = "session.duration" ALL = "session.all" ABNORMAL = "session.abnormal" + UNHANDLED = "session.unhandled" CRASHED = "session.crashed" CRASH_FREE = "session.crash_free" ERRORED = "session.errored" @@ -45,6 +46,7 @@ class SessionMetricKey(Enum): CRASH_FREE_RATE = "session.crash_free_rate" ALL_USER = "session.all_user" ABNORMAL_USER = "session.abnormal_user" + UNHANDLED_USER = "session.unhandled_user" CRASHED_USER = "session.crashed_user" CRASH_FREE_USER = "session.crash_free_user" ERRORED_USER = "session.errored_user" diff --git a/src/sentry/snuba/sessions_v2.py b/src/sentry/snuba/sessions_v2.py index 0eded1186366bd..3147204ea8dc79 100644 --- a/src/sentry/snuba/sessions_v2.py +++ b/src/sentry/snuba/sessions_v2.py @@ -120,11 +120,16 @@ def extract_from_row(self, row, group): return max(healthy_sessions, 0) if status == "abnormal": return row["sessions_abnormal"] + if status == "unhandled": + return row["sessions_unhandled"] if status == "crashed": return row["sessions_crashed"] if status == "errored": errored_sessions = ( - row["sessions_errored"] - row["sessions_crashed"] - row["sessions_abnormal"] + row["sessions_errored"] + - row["sessions_unhandled"] + - row["sessions_crashed"] + - row["sessions_abnormal"] ) return max(errored_sessions, 0) return 0 @@ -133,7 +138,7 @@ def extract_from_row(self, row, group): class UsersField: def get_snuba_columns(self, raw_groupby): if "session.status" in raw_groupby: - return ["users", "users_abnormal", "users_crashed", "users_errored"] + return ["users", "users_abnormal", "users_crashed", "users_errored", "users_unhandled"] return ["users"] def extract_from_row(self, row, group): @@ -147,10 +152,17 @@ def extract_from_row(self, row, group): return max(healthy_users, 0) if status == "abnormal": return row["users_abnormal"] + if status == "unhandled": + return row["users_unhandled"] if status == "crashed": return row["users_crashed"] if status == "errored": - errored_users = row["users_errored"] - row["users_crashed"] - row["users_abnormal"] + errored_users = ( + row["users_errored"] + - row["users_crashed"] + - row["users_abnormal"] + - row["users_unhandled"] + ) return max(errored_users, 0) return 0 diff --git a/src/sentry/web/frontend/debug/debug_chart_renderer.py b/src/sentry/web/frontend/debug/debug_chart_renderer.py index 7f54e7d8071dec..1f909b9705a675 100644 --- a/src/sentry/web/frontend/debug/debug_chart_renderer.py +++ b/src/sentry/web/frontend/debug/debug_chart_renderer.py @@ -653,6 +653,11 @@ "totals": {"sum(session)": 963}, "series": {"sum(session)": [185, 170, 147, 170, 105, 133, 53]}, }, + { + "by": {"session.status": "unhandled"}, + "totals": {"sum(session)": 0}, + "series": {"sum(session)": [0, 0, 0, 0, 0, 0, 0]}, + }, { "by": {"session.status": "crashed"}, "totals": {"sum(session)": 401}, diff --git a/static/app/types/organization.tsx b/static/app/types/organization.tsx index ed840d81a19ebc..5749c83e74f0b0 100644 --- a/static/app/types/organization.tsx +++ b/static/app/types/organization.tsx @@ -390,6 +390,8 @@ export enum SessionFieldWithOperation { SESSIONS = 'sum(session)', USERS = 'count_unique(user)', DURATION = 'p50(session.duration)', + UNHANDLED = 'sum(session.unhandled)', + UNHANDLED_USER = 'count_unique(session.unhandled_user)', CRASH_FREE_RATE_USERS = 'crash_free_rate(user)', CRASH_FREE_RATE_SESSIONS = 'crash_free_rate(session)', } @@ -397,6 +399,7 @@ export enum SessionFieldWithOperation { export enum SessionStatus { HEALTHY = 'healthy', ABNORMAL = 'abnormal', + UNHANDLED = 'unhandled', ERRORED = 'errored', CRASHED = 'crashed', } diff --git a/tests/sentry/api/endpoints/test_organization_release_health_data.py b/tests/sentry/api/endpoints/test_organization_release_health_data.py index c429cf1489e31d..68b42746622671 100644 --- a/tests/sentry/api/endpoints/test_organization_release_health_data.py +++ b/tests/sentry/api/endpoints/test_organization_release_health_data.py @@ -1765,6 +1765,7 @@ def test_errored_sessions(self): for tag_value, value in ( ("errored_preaggr", 10), ("crashed", 2), + ("unhandled", 1), ("abnormal", 4), ("init", 15), ): @@ -1787,7 +1788,7 @@ def test_errored_sessions(self): interval="1m", ) group = response.data["groups"][0] - assert group["totals"]["session.errored"] == 7 + assert group["totals"]["session.errored"] == 8 assert group["series"]["session.errored"] == [0, 4, 0, 0, 0, 3] def test_orderby_composite_entity_derived_metric(self): @@ -1839,6 +1840,10 @@ def test_abnormal_sessions(self): assert bar_group["totals"] == {"session.abnormal": 3} assert bar_group["series"] == {"session.abnormal": [0, 0, 0, 3, 0, 0]} + def test_unhandled_sessions(self): + # TODO: ryan953 + pass + def test_crashed_user_sessions(self): for tag_value, values in ( ("foo", [1, 2, 4]), diff --git a/tests/sentry/snuba/metrics/test_snql.py b/tests/sentry/snuba/metrics/test_snql.py index 6f2d6ee70923ff..8d66964b57a35a 100644 --- a/tests/sentry/snuba/metrics/test_snql.py +++ b/tests/sentry/snuba/metrics/test_snql.py @@ -33,6 +33,8 @@ session_duration_filters, subtraction, tolerated_count_transaction, + unhandled_sessions, + unhandled_users, uniq_aggregation_on_metric, uniq_if_column_snql, ) @@ -66,6 +68,7 @@ def setUp(self): self.org_id: { "abnormal", "crashed", + "unhandled", "errored_preaggr", "errored", "exited", @@ -98,6 +101,7 @@ def test_counter_sum_aggregation_on_session_status(self): ("crashed", crashed_sessions), ("errored_preaggr", errored_preaggr_sessions), ("abnormal", abnormal_sessions), + ("unhandled", unhandled_sessions), ]: assert func(self.org_id, self.metric_ids, alias=status) == Function( "sumIf", @@ -129,6 +133,7 @@ def test_set_uniq_aggregation_on_session_status(self): ("crashed", crashed_users), ("abnormal", abnormal_users), ("errored", errored_all_users), + ("unhandled", unhandled_users), ]: assert func(self.org_id, self.metric_ids, alias=status) == Function( "uniqIf", diff --git a/tests/snuba/api/endpoints/test_organization_sessions.py b/tests/snuba/api/endpoints/test_organization_sessions.py index 5ee25ef8794f10..7d5bef0c2cd239 100644 --- a/tests/snuba/api/endpoints/test_organization_sessions.py +++ b/tests/snuba/api/endpoints/test_organization_sessions.py @@ -539,7 +539,7 @@ def test_filter_unknown_release_in(self): "series": {"sum(session)": [0, 0]}, "totals": {"sum(session)": 0}, } - for status in ("abnormal", "crashed", "errored", "healthy") + for status in ("abnormal", "unhandled", "crashed", "errored", "healthy") ] @freeze_time(MOCK_DATETIME) @@ -649,6 +649,11 @@ def test_groupby_status(self): "series": {"sum(session)": [0, 0]}, "totals": {"sum(session)": 0}, }, + { + "by": {"session.status": "unhandled"}, + "series": {"sum(session)": [0, 0]}, + "totals": {"sum(session)": 0}, + }, { "by": {"session.status": "crashed"}, "series": {"sum(session)": [0, 1]}, @@ -739,6 +744,11 @@ def test_users_groupby(self): "series": {"count_unique(user)": [0, 0]}, "totals": {"count_unique(user)": 0}, }, + { + "by": {"session.status": "unhandled"}, + "series": {"count_unique(user)": [0, 0]}, + "totals": {"count_unique(user)": 0}, + }, { "by": {"session.status": "crashed"}, "series": {"count_unique(user)": [0, 0]}, @@ -798,9 +808,9 @@ def test_users_groupby_status_advanced(self): self.store_session( make_session(project, session_id=session2b, distinct_id=user2, status="ok") ) - self.store_session( - make_session(project, session_id=session2b, distinct_id=user2, status="abnormal") - ) + # self.store_session( + # make_session(project, session_id=session2b, distinct_id=user2, status="abnormal") + # ) self.store_session( make_session( @@ -848,6 +858,11 @@ def test_users_groupby_status_advanced(self): "series": {"count_unique(user)": [0, 1]}, "totals": {"count_unique(user)": 1}, }, + { + "by": {"session.status": "unhandled"}, + "series": {"count_unique(user)": [0, 1]}, + "totals": {"count_unique(user)": 1}, + }, { "by": {"session.status": "crashed"}, "series": {"count_unique(user)": [0, 1]}, @@ -935,7 +950,7 @@ def test_duration_percentiles_groupby(self): assert group["totals"] == {key: None for key in expected}, group["by"] assert group["series"] == {key: [None, None] for key in expected} - assert seen == {"abnormal", "crashed", "errored", "healthy"} + assert seen == {"abnormal", "unhandled", "crashed", "errored", "healthy"} @freeze_time(MOCK_DATETIME) def test_snuba_limit_exceeded(self): @@ -1007,6 +1022,14 @@ def test_snuba_limit_exceeded_groupby_status(self): "totals": {"sum(session)": 0, "count_unique(user)": 0}, "series": {"sum(session)": [0, 0, 0, 0], "count_unique(user)": [0, 0, 0, 0]}, }, + { + "by": { + "project": self.project1.id, + "release": "foo@1.0.0", + "session.status": "unhandled", + "environment": "production", + }, + }, { "by": { "project": self.project1.id, diff --git a/tests/snuba/sessions/test_sessions_v2.py b/tests/snuba/sessions/test_sessions_v2.py index b4393d72ac8594..196237f7c05987 100644 --- a/tests/snuba/sessions/test_sessions_v2.py +++ b/tests/snuba/sessions/test_sessions_v2.py @@ -654,6 +654,11 @@ def test_massage_virtual_groupby_timeseries(): "series": {"count_unique(user)": [0, 0, 0, 1], "sum(session)": [3, 4, 0, 1]}, "totals": {"count_unique(user)": 1, "sum(session)": 8}, }, + { + "by": {"session.status": "unhandled"}, + "series": {"count_unique(user)": [0, 0, 0, 0], "sum(session)": [0, 0, 0, 0]}, + "totals": {"count_unique(user)": 0, "sum(session)": 0}, + }, { "by": {"session.status": "errored"}, "series": {"count_unique(user)": [0, 0, 0, 0], "sum(session)": [0, 4, 0, 0]}, @@ -726,6 +731,11 @@ def test_clamping_in_massage_sessions_results_with_groupby_timeseries(): "series": {"count_unique(user)": [0, 2], "sum(session)": [0, 2]}, "totals": {"count_unique(user)": 0, "sum(session)": 0}, }, + { + "by": {"session.status": "unhandled"}, + "series": {"count_unique(user)": [0, 0], "sum(session)": [0, 0]}, + "totals": {"count_unique(user)": 0, "sum(session)": 0}, + }, { "by": {"session.status": "errored"}, "series": {"count_unique(user)": [10, 0], "sum(session)": [10, 0]},