Browse Source

feat(sessions): Expose crash free rate (#34045)

Expose session/user crash (free) rate on the sessions API. Metrics
implementation only.

These are the new fields:

crash_rate(session)
crash_rate(user)
crash_free_rate(session)
crash_free_rate(user)

When one of these is requested, attempting to group / filter by
session.status will result in a 400 response.
Joris Bayer 2 years ago
parent
commit
ef16973036

+ 4 - 0
src/sentry/release_health/base.py

@@ -40,6 +40,10 @@ SessionsQueryFunction = Literal[
     "p95(session.duration)",
     "p99(session.duration)",
     "max(session.duration)",
+    "crash_rate(session)",
+    "crash_rate(user)",
+    "crash_free_rate(session)",
+    "crash_free_rate(user)",
 ]
 
 GroupByFieldName = Literal[

+ 37 - 0
src/sentry/release_health/metrics_sessions_v2.py

@@ -329,6 +329,39 @@ class DurationField(Field):
         return value
 
 
+class SimpleForwardingField(Field):
+    """A field that forwards a metrics API field 1:1.
+
+    On this type of field, grouping and filtering by session.status is impossible
+    """
+
+    field_name_to_metric_name = {
+        "crash_rate(session)": SessionMetricKey.CRASH_RATE,
+        "crash_rate(user)": SessionMetricKey.CRASH_USER_RATE,
+        "crash_free_rate(session)": SessionMetricKey.CRASH_FREE_RATE,
+        "crash_free_rate(user)": SessionMetricKey.CRASH_FREE_USER_RATE,
+    }
+
+    def __init__(self, name: str, raw_groupby: Sequence[str], status_filter: StatusFilter):
+        if "session.status" in raw_groupby:
+            raise InvalidParams(f"Cannot group field {name} by session.status")
+        if status_filter is not None:
+            raise InvalidParams(f"Cannot filter field {name} by session.status")
+
+        metric_name = self.field_name_to_metric_name[name].value
+        self._metric_field = MetricField(None, metric_name)
+
+        super().__init__(name, raw_groupby, status_filter)
+
+    def _get_session_status(self, metric_field: MetricField) -> Optional[SessionStatus]:
+        return None
+
+    def _get_metric_fields(
+        self, raw_groupby: Sequence[str], status_filter: StatusFilter
+    ) -> Sequence[MetricField]:
+        return [self._metric_field]
+
+
 FIELD_MAP: Mapping[SessionsQueryFunction, Type[Field]] = {
     "sum(session)": SumSessionField,
     "count_unique(user)": CountUniqueUser,
@@ -339,6 +372,10 @@ FIELD_MAP: Mapping[SessionsQueryFunction, Type[Field]] = {
     "p95(session.duration)": DurationField,
     "p99(session.duration)": DurationField,
     "max(session.duration)": DurationField,
+    "crash_rate(session)": SimpleForwardingField,
+    "crash_rate(user)": SimpleForwardingField,
+    "crash_free_rate(session)": SimpleForwardingField,
+    "crash_free_rate(user)": SimpleForwardingField,
 }
 
 

+ 6 - 0
src/sentry/snuba/sessions_v2.py

@@ -269,6 +269,12 @@ class QueryDefinition:
         self.fields = {}
         for key in raw_fields:
             if key not in COLUMN_MAP:
+                from sentry.release_health.metrics_sessions_v2 import FIELD_MAP
+
+                if key in FIELD_MAP:
+                    # HACK : Do not raise an error for metrics-only fields,
+                    # Simply ignore them instead.
+                    continue
                 raise InvalidField(f'Invalid field: "{key}"')
             self.fields[key] = COLUMN_MAP[key]
 

+ 76 - 0
tests/snuba/api/endpoints/test_organization_sessions.py

@@ -1218,6 +1218,82 @@ class OrganizationSessionsEndpointMetricsTest(
         assert response.status_code == 400, response.content
         assert response.data == {"detail": "Cannot order by sum(session) with the current filters"}
 
+    @freeze_time(MOCK_DATETIME)
+    def test_crash_rate(self):
+        default_request = {
+            "project": [-1],
+            "statsPeriod": "1d",
+            "interval": "1d",
+            "field": ["crash_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 crash_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 crash_rate(session) by session.status"
+        }
+
+        # 4 - fetch all
+        response = req(
+            field=[
+                "crash_rate(session)",
+                "crash_rate(user)",
+                "crash_free_rate(session)",
+                "crash_free_rate(user)",
+            ],
+            groupBy=["release", "environment"],
+            orderBy=["crash_free_rate(session)"],
+            query="release:foo@1.0.0",
+        )
+        assert response.status_code == 200, response.content
+        assert response.data["groups"] == [
+            {
+                "by": {"environment": "production", "release": "foo@1.0.0"},
+                "series": {
+                    "crash_free_rate(session)": [0.8333333333333334],
+                    "crash_free_rate(user)": [1.0],
+                    "crash_rate(session)": [0.16666666666666666],
+                    "crash_rate(user)": [0.0],
+                },
+                "totals": {
+                    "crash_free_rate(session)": 0.8333333333333334,
+                    "crash_free_rate(user)": 1.0,
+                    "crash_rate(session)": 0.16666666666666666,
+                    "crash_rate(user)": 0.0,
+                },
+            },
+            {
+                "by": {"environment": "development", "release": "foo@1.0.0"},
+                "series": {
+                    "crash_free_rate(session)": [1.0],
+                    "crash_free_rate(user)": [None],
+                    "crash_rate(session)": [0.0],
+                    "crash_rate(user)": [None],
+                },
+                "totals": {
+                    "crash_free_rate(session)": 1.0,
+                    "crash_free_rate(user)": None,
+                    "crash_rate(session)": 0.0,
+                    "crash_rate(user)": None,
+                },
+            },
+        ]
+
     @freeze_time(MOCK_DATETIME)
     def test_pagination(self):
         def do_request(cursor):