diff --git a/src/sentry/release_health/base.py b/src/sentry/release_health/base.py index 9dde179b53c106..e0b30fee3d6a64 100644 --- a/src/sentry/release_health/base.py +++ b/src/sentry/release_health/base.py @@ -38,6 +38,7 @@ "foreground_anr_rate()", "unhandled_rate(session)", "unhandled_rate(user)", + "unhealthy_rate(session)", ] GroupByFieldName = Literal[ diff --git a/src/sentry/release_health/metrics_sessions_v2.py b/src/sentry/release_health/metrics_sessions_v2.py index 869a7e66cd48e7..ff251a8eacd667 100644 --- a/src/sentry/release_health/metrics_sessions_v2.py +++ b/src/sentry/release_health/metrics_sessions_v2.py @@ -347,6 +347,7 @@ class SimpleForwardingField(Field): "foreground_anr_rate()": SessionMRI.FOREGROUND_ANR_RATE, "unhandled_rate(session)": SessionMRI.UNHANDLED_RATE, "unhandled_rate(user)": SessionMRI.UNHANDLED_USER_RATE, + "unhealthy_rate(session)": SessionMRI.UNHEALTHY_RATE, } def __init__(self, name: str, raw_groupby: Sequence[str], status_filter: StatusFilter): @@ -387,6 +388,7 @@ def _get_metric_fields( "foreground_anr_rate()": SimpleForwardingField, "unhandled_rate(session)": SimpleForwardingField, "unhandled_rate(user)": SimpleForwardingField, + "unhealthy_rate(session)": SimpleForwardingField, } PREFLIGHT_QUERY_COLUMNS = {"release.timestamp"} VirtualOrderByName = Literal["release.timestamp"] diff --git a/src/sentry/snuba/metrics/fields/base.py b/src/sentry/snuba/metrics/fields/base.py index 404c2dd9317bca..a23f1ec6e08fc2 100644 --- a/src/sentry/snuba/metrics/fields/base.py +++ b/src/sentry/snuba/metrics/fields/base.py @@ -1587,6 +1587,15 @@ def generate_where_statements( unit="sessions", post_query_func=lambda init, errored: max(0, init - errored), ), + CompositeEntityDerivedMetric( + metric_mri=SessionMRI.UNHEALTHY_RATE.value, + metrics=[ + SessionMRI.ALL.value, + SessionMRI.ERRORED_ALL.value, + ], + unit="percentage", + post_query_func=lambda all, errored_all: (max(0, errored_all / all) if all else 0), + ), SingularEntityDerivedMetric( metric_mri=SessionMRI.HEALTHY_USER.value, metrics=[ diff --git a/src/sentry/snuba/metrics/naming_layer/mri.py b/src/sentry/snuba/metrics/naming_layer/mri.py index b19ef312ef3f95..96a07ef45362f1 100644 --- a/src/sentry/snuba/metrics/naming_layer/mri.py +++ b/src/sentry/snuba/metrics/naming_layer/mri.py @@ -82,6 +82,7 @@ class SessionMRI(Enum): 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" # includes handled and unhandled + UNHEALTHY_RATE = "e:sessions/unhealthy_rate@ratio" ALL_USER = "e:sessions/user.all@none" HEALTHY_USER = "e:sessions/user.healthy@none" ERRORED_USER = "e:sessions/user.errored@none" diff --git a/tests/snuba/api/endpoints/test_organization_sessions.py b/tests/snuba/api/endpoints/test_organization_sessions.py index a70dc64d68339d..1ec1e76467bb09 100644 --- a/tests/snuba/api/endpoints/test_organization_sessions.py +++ b/tests/snuba/api/endpoints/test_organization_sessions.py @@ -1823,6 +1823,62 @@ def req(**kwargs): }, ] + @freeze_time(MOCK_DATETIME) + def test_unhealthy_rate(self) -> None: + default_request = { + "project": [-1], + "statsPeriod": "1d", + "interval": "1d", + "field": ["unhealthy_rate(session)"], + } + + def req(**kwargs): + return self.do_request(dict(default_request, **kwargs)) + + # 1 - filter session.status + response = req( + query="session.status:[abnormal,crashed]", + ) + assert response.status_code == 400, response.content + assert response.data == { + "detail": "Cannot filter field unhealthy_rate(session) by session.status" + } + + # 2 - group by session.status + response = req( + groupBy="session.status", + ) + assert response.status_code == 400, response.content + assert response.data == { + "detail": "Cannot group field unhealthy_rate(session) by session.status" + } + + # 3 - fetch foo@1.0.0 release + response = req( + field=[ + "unhealthy_rate(session)", + "sum(session)", + ], + groupBy=["release", "environment"], + query="release:foo@1.0.0", + ) + assert response.status_code == 200, response.content + group = response.data["groups"][0] + assert group["totals"]["sum(session)"] == 8 + assert group["totals"]["unhealthy_rate(session)"] == pytest.approx(0.5) + + # 4 - fetch all + response = req( + field=[ + "unhealthy_rate(session)", + "sum(session)", + ], + ) + assert response.status_code == 200, response.content + group = response.data["groups"][0] + assert group["totals"]["sum(session)"] == 11 + assert group["totals"]["unhealthy_rate(session)"] == pytest.approx(5 / 11) + @freeze_time(MOCK_DATETIME) def test_pagination(self) -> None: def do_request(cursor):