Repository: getsentry/sentry

Issue:
Show metric stats in admin outcomes table
A new use case called `metric_stats` has been added and contains metrics about metrics. Such metrics can be queried via the `metrics/query` endpoint.

We would like to show a new outcomes table in the admin panel, similar to here:
![Image](https://github.com/getsentry/sentry/assets/15819674/c382dc0a-8813-4f76-abf9-b3acdbcde323)

The data shown there should be taken from the `metrics/query` endpoint and use a MRI of the metrics of metrics.


Discussion:
The metric that we want to query is:
![Image](https://github.com/getsentry/sentry/assets/15819674/d707731f-3aae-412c-81c5-b913d0fd7bef)

This metric refers to the total number of `statsd` envelope items that were captured.

Resolving patch:
diff --git a/src/sentry/api/endpoints/organization_stats_v2.py b/src/sentry/api/endpoints/organization_stats_v2.py
--- a/src/sentry/api/endpoints/organization_stats_v2.py
+++ b/src/sentry/api/endpoints/organization_stats_v2.py
@@ -8,6 +8,7 @@
 from rest_framework.request import Request
 from rest_framework.response import Response
 
+from sentry import features
 from sentry.api.api_owners import ApiOwner
 from sentry.api.api_publish_status import ApiPublishStatus
 from sentry.api.base import region_silo_endpoint
@@ -26,6 +27,7 @@
     GROUPBY_MAP,
     QueryDefinition,
     massage_outcomes_result,
+    run_metrics_outcomes_query,
     run_outcomes_query_timeseries,
     run_outcomes_query_totals,
 )
@@ -164,6 +166,18 @@ def get(self, request: Request, organization) -> Response:
         Select a field, define a date range, and group or filter by columns.
         """
         with self.handle_query_errors():
+
+            if features.has("organizations:metrics-stats", organization):
+                if request.GET.get("category") == "metrics":
+                    # TODO(metrics): align project resolution
+                    result = run_metrics_outcomes_query(
+                        request.GET,
+                        organization,
+                        self.get_projects(request, organization, include_all_accessible=True),
+                        self.get_environments(request, organization),
+                    )
+                    return Response(result, status=200)
+
             tenant_ids = {"organization_id": organization.id}
             with sentry_sdk.start_span(op="outcomes.endpoint", description="build_outcomes_query"):
                 query = self.build_outcomes_query(
diff --git a/src/sentry/sentry_metrics/querying/data/transformation/stats.py b/src/sentry/sentry_metrics/querying/data/transformation/stats.py
new file mode 100644
--- /dev/null
+++ b/src/sentry/sentry_metrics/querying/data/transformation/stats.py
@@ -0,0 +1,39 @@
+from collections.abc import Mapping, Sequence
+from typing import Any
+
+from sentry.sentry_metrics.querying.data.execution import QueryResult
+from sentry.sentry_metrics.querying.data.transformation.base import QueryResultsTransformer
+
+
+class MetricsStatsTransformer(QueryResultsTransformer[Mapping[str, Any]]):
+    def transform_result(self, result: Sequence[Mapping[str, Any]]) -> Sequence[Mapping[str, Any]]:
+        ret_val = []
+
+        for item in result:
+            ret_val_item = {}
+            for key in item:
+                if key == "outcome.id":
+                    ret_val_item["outcome"] = int(item[key])
+                elif key in "aggregate_value":
+                    ret_val_item["quantity"] = item[key]
+                else:
+                    ret_val_item[key] = item[key]
+
+            ret_val.append(ret_val_item)
+
+        return ret_val
+
+    def transform(self, query_results: Sequence[QueryResult]) -> Mapping[str, Any]:
+        """
+        Transforms the query results into the format returned by outcomes queries.
+        Performs necessary mappings to match that format such as outcome.id -> outcome
+
+        """
+
+        if not query_results or len(query_results) == 0:
+            return {"series": [], "totals": []}
+
+        series = self.transform_result(query_results[0].series)
+        totals = self.transform_result(query_results[0].totals)
+
+        return {"series": series, "totals": totals}
diff --git a/src/sentry/snuba/outcomes.py b/src/sentry/snuba/outcomes.py
--- a/src/sentry/snuba/outcomes.py
+++ b/src/sentry/snuba/outcomes.py
@@ -13,15 +13,20 @@
 from snuba_sdk.function import Function
 from snuba_sdk.query import Query
 
+from sentry.api.utils import get_date_range_from_params
 from sentry.constants import DataCategory
 from sentry.release_health.base import AllowedResolution
 from sentry.search.utils import InvalidQuery
+from sentry.sentry_metrics.querying.data import MetricsQueriesPlan, run_metrics_queries_plan
+from sentry.sentry_metrics.querying.data.transformation.stats import MetricsStatsTransformer
+from sentry.snuba.referrer import Referrer
 from sentry.snuba.sessions_v2 import (
     InvalidField,
     SimpleGroupBy,
     get_constrained_date_range,
     massage_sessions_result,
 )
+from sentry.utils.dates import parse_stats_period
 from sentry.utils.outcomes import Outcome
 from sentry.utils.snuba import raw_snql_query
 
@@ -133,7 +138,7 @@ def resolve_filter(self, raw_filter: Sequence[str]) -> list[DataCategory]:
             # combine DEFAULT, ERROR, and SECURITY as errors.
             # see relay: py/sentry_relay/consts.py and relay-cabi/include/relay.h
             parsed_category = DataCategory.parse(category)
-            if parsed_category is None:
+            if parsed_category is None and parsed_category != "metrics":
                 raise InvalidField(f'Invalid category: "{category}"')
             elif parsed_category == DataCategory.ERROR:
                 resolved_categories.update(DataCategory.error_categories())
@@ -460,3 +465,34 @@ def massage_outcomes_result(
         del result["intervals"]
     del result["query"]
     return result
+
+
+def run_metrics_outcomes_query(
+    query: QueryDict, organization, projects, environments
+) -> dict[str, list]:
+    start, end = get_date_range_from_params(query)
+    interval = parse_stats_period(query.get("interval", "1h"))
+
+    # TODO(metrics): add support for reason tag
+    plan = MetricsQueriesPlan().apply_formula("sum(c:metric_stats/volume@none) by (outcome.id)")
+    rows = run_metrics_queries_plan(
+        plan,
+        start=start,
+        end=end,
+        interval=int(3600 if interval is None else interval.total_seconds()),
+        organization=organization,
+        projects=projects,
+        environments=environments,
+        referrer=Referrer.OUTCOMES_TIMESERIES.value,
+    ).apply_transformer(MetricsStatsTransformer())
+
+    # Dummy query definition to pass to the _format_rows, as it expects a QueryDefinition object
+    # TODO(metrics): add a `metrics` category or refactor _format_rows
+    copied = query.copy()
+    copied["category"] = "error"
+    query_def = QueryDefinition.from_query_dict(copied, {"organization_id": organization.id})
+
+    series = _format_rows(rows["series"], query_def)
+    totals = _format_rows(rows["totals"], query_def)
+
+    return massage_outcomes_result(query_def, totals, series)


Test patch: