Browse Source

feat(metrics): Add team_key_transaction fake mri transformation (#41215)

Riccardo Busetti 2 years ago
parent
commit
c0d0571dbc

+ 1 - 1
src/sentry/search/events/datasets/metrics_layer.py

@@ -416,7 +416,7 @@ class MetricsLayerDatasetConfig(MetricsDatasetConfig):
             team_key_transactions = [(-1, "")]
         return Function(
             function="team_key_transaction",
-            parameters=[Column("d:transactions/duration@millisecond"), team_key_transactions],
+            parameters=[Column("e:transactions/team_key_transaction@none"), team_key_transactions],
             alias="team_key_transaction",
         )
 

+ 22 - 0
src/sentry/snuba/metrics/fields/base.py

@@ -1562,6 +1562,28 @@ DERIVED_OPS: Mapping[MetricOperationType, DerivedOp] = {
             snql_func=count_transaction_name_snql_factory,
             default_null_value=0,
         ),
+        # This specific derived operation doesn't require a metric_mri supplied to the MetricField but
+        # in order to avoid breaking the contract we should always pass it. When using it in the orderby
+        # clause you should put a metric mri with the same entity as the only entity used in the select.
+        # E.g. if you have a select with user_misery which is a set entity and team_key_transaction and you want
+        # to order by team_key_transaction, you will have to supply to the team_key_transaction MetricField
+        # an mri that has the set entity.
+        #
+        # OrderBy(
+        #     field=MetricField(
+        #         op="team_key_transaction",
+        #         # This has entity type set, which is the entity type of the select (in the select you can only have
+        #         one entity type across selections if you use the team_key_transaction in the order by).
+        #         metric_mri=TransactionMRI.USER.value,
+        #         params={
+        #             "team_key_condition_rhs": [
+        #                 (self.project.id, "foo_transaction"),
+        #             ]
+        #         },
+        #         alias="team_key_transactions",
+        #     ),
+        #     direction=Direction.DESC,
+        # )
         DerivedOp(
             op="team_key_transaction",
             can_orderby=True,

+ 145 - 0
src/sentry/snuba/metrics/mqb_query_transformer.py

@@ -4,11 +4,13 @@ from snuba_sdk import AliasedExpression, Column, Condition, Function, Granularit
 from snuba_sdk.query import Query
 
 from sentry.api.utils import InvalidParams
+from sentry.sentry_metrics.configuration import UseCaseKey
 from sentry.snuba.metrics import (
     FIELD_ALIAS_MAPPINGS,
     FILTERABLE_TAGS,
     OPERATIONS,
     DerivedMetricException,
+    TransactionMRI,
 )
 from sentry.snuba.metrics.fields.base import DERIVED_OPS, metric_object_factory
 from sentry.snuba.metrics.query import (
@@ -17,9 +19,12 @@ from sentry.snuba.metrics.query import (
     MetricGroupByField,
     MetricsQuery,
 )
+from sentry.snuba.metrics.query import OrderBy
 from sentry.snuba.metrics.query import OrderBy as MetricOrderBy
 from sentry.snuba.metrics.query_builder import FUNCTION_ALLOWLIST
 
+TEAM_KEY_TRANSACTION_OP = "team_key_transaction"
+
 
 class MQBQueryTransformationException(Exception):
     ...
@@ -235,6 +240,142 @@ def _transform_orderby(query_orderby):
     return mq_orderby if len(mq_orderby) > 0 else None
 
 
+def _derive_mri_to_apply(project_ids, select, orderby):
+    mri_dictionary = {
+        "generic_metrics_distributions": TransactionMRI.DURATION.value,
+        "generic_metrics_sets": TransactionMRI.USER.value,
+    }
+    mri_to_apply = TransactionMRI.DURATION.value
+
+    # We first check if there is an order by field that has the team_key_transaction, otherwise
+    # we just use the default mri of duration.
+    has_order_by_team_key_transaction = False
+    if orderby is not None:
+        for orderby_field in orderby:
+            if orderby_field.field.op == TEAM_KEY_TRANSACTION_OP:
+                has_order_by_team_key_transaction = True
+                break
+
+    if has_order_by_team_key_transaction:
+        # TODO: add here optimization that gets an entity from the select (either set of distribution) and sets it
+        #  to all tkt in the query.
+        entities = set()
+        for orderby_field in orderby:
+            if orderby_field.field.op != TEAM_KEY_TRANSACTION_OP:
+                expr = metric_object_factory(orderby_field.field.op, orderby_field.field.metric_mri)
+                entity = expr.get_entity(project_ids, use_case_id=UseCaseKey.PERFORMANCE)
+                if isinstance(entity, str):
+                    entities.add(entity)
+
+        if len(entities) > 1:
+            raise InvalidParams("The orderby cannot have fields with multiple entities.")
+
+        if len(entities) != 0:
+            mri_to_apply = mri_dictionary[entities.pop()]
+
+    return mri_to_apply
+
+
+def _transform_team_key_transaction_in_select(mri_to_apply, select):
+    if select is None:
+        return select
+
+    def _select_predicate(select_field):
+        if select_field.op == TEAM_KEY_TRANSACTION_OP:
+            return MetricField(
+                op=select_field.op,
+                metric_mri=mri_to_apply,
+                params=select_field.params,
+                alias=select_field.alias,
+            )
+
+        return select_field
+
+    return list(map(_select_predicate, select))
+
+
+def _transform_team_key_transaction_in_where(mri_to_apply, where):
+    if where is None:
+        return where
+
+    def _where_predicate(where_field):
+        if (
+            isinstance(where_field, MetricConditionField)
+            and where_field.lhs.op == TEAM_KEY_TRANSACTION_OP
+        ):
+            return MetricConditionField(
+                lhs=MetricField(
+                    op=where_field.lhs.op,
+                    metric_mri=mri_to_apply,
+                    params=where_field.lhs.params,
+                    alias=where_field.lhs.alias,
+                ),
+                op=where_field.op,
+                rhs=where_field.rhs,
+            )
+
+        return where_field
+
+    return list(map(_where_predicate, where))
+
+
+def _transform_team_key_transaction_in_groupby(mri_to_apply, groupby):
+    if groupby is None:
+        return groupby
+
+    def _groupby_predicate(groupby_field):
+        if (
+            isinstance(groupby_field.field, MetricField)
+            and groupby_field.field.op == TEAM_KEY_TRANSACTION_OP
+        ):
+            return MetricGroupByField(
+                field=MetricField(
+                    op=groupby_field.field.op,
+                    metric_mri=mri_to_apply,
+                    params=groupby_field.field.params,
+                    alias=groupby_field.field.alias,
+                ),
+            )
+
+        return groupby_field
+
+    return list(map(_groupby_predicate, groupby))
+
+
+def _transform_team_key_transaction_in_orderby(mri_to_apply, orderby):
+    if orderby is None:
+        return orderby
+
+    def _orderby_predicate(orderby_field):
+        if orderby_field.field.op == TEAM_KEY_TRANSACTION_OP:
+            return OrderBy(
+                field=MetricField(
+                    op=orderby_field.field.op,
+                    metric_mri=mri_to_apply,
+                    params=orderby_field.field.params,
+                    alias=orderby_field.field.alias,
+                ),
+                direction=orderby_field.direction,
+            )
+
+        return orderby_field
+
+    return list(map(_orderby_predicate, orderby))
+
+
+def _transform_team_key_transaction_fake_mri(mq_dict):
+    mri_to_apply = _derive_mri_to_apply(
+        mq_dict["project_ids"], mq_dict["select"], mq_dict["orderby"]
+    )
+
+    return {
+        "select": _transform_team_key_transaction_in_select(mri_to_apply, mq_dict["select"]),
+        "where": _transform_team_key_transaction_in_where(mri_to_apply, mq_dict["where"]),
+        "groupby": _transform_team_key_transaction_in_groupby(mri_to_apply, mq_dict["groupby"]),
+        "orderby": _transform_team_key_transaction_in_orderby(mri_to_apply, mq_dict["orderby"]),
+    }
+
+
 def transform_mqb_query_to_metrics_query(query: Query) -> MetricsQuery:
     # Validate that we only support this transformation for the generic_metrics dataset
     if query.match.name not in {"generic_metrics_distributions", "generic_metrics_sets"}:
@@ -261,4 +402,8 @@ def transform_mqb_query_to_metrics_query(query: Query) -> MetricsQuery:
         "interval": interval,
         **_get_mq_dict_params_from_where(query.where),
     }
+
+    # This code is just an edge case specific for the team_key_transaction derived operation.
+    mq_dict.update(**_transform_team_key_transaction_fake_mri(mq_dict))
+
     return MetricsQuery(**mq_dict)

+ 1 - 0
src/sentry/snuba/metrics/naming_layer/mri.py

@@ -110,6 +110,7 @@ class TransactionMRI(Enum):
     MISERABLE_USER = "e:transactions/user.miserable@none"
     ALL_USER = "e:transactions/user.all@none"
     USER_MISERY = "e:transactions/user_misery@ratio"
+    TEAM_KEY_TRANSACTION = "e:transactions/team_key_transaction@none"
 
 
 @dataclass

+ 1 - 0
src/sentry/snuba/metrics/naming_layer/public.py

@@ -86,6 +86,7 @@ class TransactionMetricKey(Enum):
     MISERABLE_USER = "transaction.miserable_user"
     USER_MISERY = "transaction.user_misery"
     FAILURE_COUNT = "transaction.failure_count"
+    TEAM_KEY_TRANSACTION = "transactions.team_key_transaction"
 
 
 # TODO: these tag keys and values below probably don't belong here, and should

+ 0 - 1
src/sentry/snuba/metrics/query_builder.py

@@ -108,7 +108,6 @@ def transform_null_transaction_to_unparameterized(use_case_id, org_id, alias=Non
         function="transform",
         parameters=[
             Column(resolve_tag_key(use_case_id, org_id, "transaction")),
-            # This will be removed once the removal of tags values as ints is merged.
             [""],
             [resolve_tag_value(use_case_id, org_id, "<< unparameterized >>")],
         ],

+ 170 - 4
tests/sentry/snuba/metrics/test_mqb_query_transformer.py

@@ -13,6 +13,7 @@ from snuba_sdk.query import Query
 
 from sentry.snuba.metrics import MetricConditionField, MetricField, MetricGroupByField, MetricsQuery
 from sentry.snuba.metrics import OrderBy as MetricsOrderBy
+from sentry.snuba.metrics import TransactionMRI
 from sentry.snuba.metrics.mqb_query_transformer import (
     MQBQueryTransformationException,
     transform_mqb_query_to_metrics_query,
@@ -207,7 +208,7 @@ VALID_QUERIES_INTEGRATION_TEST_CASES = [
                 Function(
                     function="team_key_transaction",
                     parameters=[
-                        Column("d:transactions/duration@millisecond"),
+                        Column(TransactionMRI.TEAM_KEY_TRANSACTION.value),
                         [(13, "foo_transaction")],
                     ],
                     alias="team_key_transaction",
@@ -221,7 +222,7 @@ VALID_QUERIES_INTEGRATION_TEST_CASES = [
                 Function(
                     function="team_key_transaction",
                     parameters=[
-                        Column("d:transactions/duration@millisecond"),
+                        Column(TransactionMRI.TEAM_KEY_TRANSACTION.value),
                         [(13, "foo_transaction")],
                     ],
                     alias="team_key_transaction",
@@ -263,7 +264,7 @@ VALID_QUERIES_INTEGRATION_TEST_CASES = [
                     lhs=Function(
                         function="team_key_transaction",
                         parameters=[
-                            Column("d:transactions/duration@millisecond"),
+                            Column(TransactionMRI.TEAM_KEY_TRANSACTION.value),
                             [(13, "foo_transaction")],
                         ],
                         alias="team_key_transaction",
@@ -286,7 +287,7 @@ VALID_QUERIES_INTEGRATION_TEST_CASES = [
                     exp=Function(
                         function="team_key_transaction",
                         parameters=[
-                            Column("d:transactions/duration@millisecond"),
+                            Column(TransactionMRI.TEAM_KEY_TRANSACTION.value),
                             [(13, "foo_transaction")],
                         ],
                         alias="team_key_transaction",
@@ -380,6 +381,171 @@ VALID_QUERIES_INTEGRATION_TEST_CASES = [
         ),
         id="team_key_transaction + groupby & select aliasing test case",
     ),
+    pytest.param(
+        Query(
+            match=Entity("generic_metrics_distributions"),
+            select=[
+                Function(
+                    function="p95",
+                    parameters=[Column("d:transactions/duration@millisecond")],
+                    alias="p95",
+                ),
+                Function(
+                    function="team_key_transaction",
+                    parameters=[
+                        Column(TransactionMRI.TEAM_KEY_TRANSACTION.value),
+                        [(13, "foo_transaction")],
+                    ],
+                    alias="team_key_transaction",
+                ),
+            ],
+            groupby=[
+                Function(
+                    function="team_key_transaction",
+                    parameters=[
+                        Column(TransactionMRI.TEAM_KEY_TRANSACTION.value),
+                        [(13, "foo_transaction")],
+                    ],
+                    alias="team_key_transaction",
+                ),
+            ],
+            array_join=None,
+            where=[
+                Condition(
+                    lhs=Column(
+                        name="timestamp",
+                    ),
+                    op=Op.GTE,
+                    rhs=datetime.datetime(2022, 3, 24, 11, 11, 36, 75132),
+                ),
+                Condition(
+                    lhs=Column(
+                        name="timestamp",
+                    ),
+                    op=Op.LT,
+                    rhs=datetime.datetime(2022, 6, 22, 11, 11, 36, 75132),
+                ),
+                Condition(
+                    lhs=Column(
+                        name="project_id",
+                    ),
+                    op=Op.IN,
+                    rhs=[13],
+                ),
+                Condition(
+                    lhs=Column(
+                        name="org_id",
+                    ),
+                    op=Op.EQ,
+                    rhs=14,
+                ),
+                Condition(
+                    lhs=Function(
+                        function="team_key_transaction",
+                        parameters=[
+                            Column(TransactionMRI.TEAM_KEY_TRANSACTION.value),
+                            [(13, "foo_transaction")],
+                        ],
+                        alias="team_key_transaction",
+                    ),
+                    op=Op.EQ,
+                    rhs=1,
+                ),
+            ],
+            having=[],
+            orderby=[
+                OrderBy(
+                    exp=Function(
+                        function="team_key_transaction",
+                        parameters=[
+                            Column(TransactionMRI.TEAM_KEY_TRANSACTION.value),
+                            [(13, "foo_transaction")],
+                        ],
+                        alias="team_key_transaction",
+                    ),
+                    direction=Direction.ASC,
+                ),
+                OrderBy(
+                    exp=Function(
+                        function="p95",
+                        parameters=[Column("d:transactions/duration@millisecond")],
+                        alias="p95",
+                    ),
+                    direction=Direction.ASC,
+                ),
+            ],
+            limitby=None,
+            limit=Limit(limit=51),
+            offset=Offset(offset=0),
+            granularity=Granularity(granularity=86400),
+            totals=None,
+        ),
+        MetricsQuery(
+            org_id=14,
+            project_ids=[13],
+            select=[
+                MetricField(
+                    op="p95",
+                    metric_mri="d:transactions/duration@millisecond",
+                    alias="p95",
+                ),
+                MetricField(
+                    op="team_key_transaction",
+                    metric_mri="d:transactions/duration@millisecond",
+                    params={"team_key_condition_rhs": [(13, "foo_transaction")]},
+                    alias="team_key_transaction",
+                ),
+            ],
+            start=datetime.datetime(2022, 3, 24, 11, 11, 36, 75132),
+            end=datetime.datetime(2022, 6, 22, 11, 11, 36, 75132),
+            granularity=Granularity(granularity=86400),
+            where=[
+                MetricConditionField(
+                    lhs=MetricField(
+                        op="team_key_transaction",
+                        metric_mri="d:transactions/duration@millisecond",
+                        params={"team_key_condition_rhs": [(13, "foo_transaction")]},
+                        alias="team_key_transaction",
+                    ),
+                    op=Op.EQ,
+                    rhs=1,
+                )
+            ],
+            groupby=[
+                MetricGroupByField(
+                    MetricField(
+                        op="team_key_transaction",
+                        metric_mri="d:transactions/duration@millisecond",
+                        params={"team_key_condition_rhs": [(13, "foo_transaction")]},
+                        alias="team_key_transaction",
+                    ),
+                ),
+            ],
+            orderby=[
+                MetricsOrderBy(
+                    field=MetricField(
+                        op="team_key_transaction",
+                        metric_mri="d:transactions/duration@millisecond",
+                        params={"team_key_condition_rhs": [(13, "foo_transaction")]},
+                        alias="team_key_transaction",
+                    ),
+                    direction=Direction.ASC,
+                ),
+                MetricsOrderBy(
+                    field=MetricField(
+                        op="p95",
+                        metric_mri="d:transactions/duration@millisecond",
+                        alias="p95",
+                    ),
+                    direction=Direction.ASC,
+                ),
+            ],
+            limit=Limit(limit=51),
+            offset=Offset(offset=0),
+            include_series=False,
+        ),
+        id="team_key_transaction transformation with p95 in select",
+    ),
     pytest.param(
         Query(
             match=Entity("generic_metrics_distributions"),

+ 98 - 0
tests/snuba/api/endpoints/test_organization_events_mep.py

@@ -442,6 +442,104 @@ class OrganizationEventsMetricsEnhancedPerformanceEndpointTest(MetricsEnhancedPe
             assert field_meta["failure_rate()"] == "percentage"
             assert field_meta["failure_count()"] == "integer"
 
+    def test_user_misery_and_team_key_sort(self):
+        self.store_transaction_metric(
+            1,
+            tags={
+                "transaction": "foo_transaction",
+                constants.METRIC_SATISFACTION_TAG_KEY: constants.METRIC_SATISFIED_TAG_VALUE,
+            },
+            timestamp=self.min_ago,
+        )
+        self.store_transaction_metric(
+            1,
+            "measurements.fcp",
+            tags={"transaction": "foo_transaction"},
+            timestamp=self.min_ago,
+        )
+        self.store_transaction_metric(
+            2,
+            "measurements.lcp",
+            tags={"transaction": "foo_transaction"},
+            timestamp=self.min_ago,
+        )
+        self.store_transaction_metric(
+            3,
+            "measurements.fid",
+            tags={"transaction": "foo_transaction"},
+            timestamp=self.min_ago,
+        )
+        self.store_transaction_metric(
+            4,
+            "measurements.cls",
+            tags={"transaction": "foo_transaction"},
+            timestamp=self.min_ago,
+        )
+        self.store_transaction_metric(
+            1,
+            "user",
+            tags={
+                "transaction": "foo_transaction",
+                constants.METRIC_SATISFACTION_TAG_KEY: constants.METRIC_FRUSTRATED_TAG_VALUE,
+            },
+            timestamp=self.min_ago,
+        )
+        response = self.do_request(
+            {
+                "field": [
+                    "team_key_transaction",
+                    "transaction",
+                    "project",
+                    "tpm()",
+                    "p75(measurements.fcp)",
+                    "p75(measurements.lcp)",
+                    "p75(measurements.fid)",
+                    "p75(measurements.cls)",
+                    "count_unique(user)",
+                    "apdex()",
+                    "count_miserable(user)",
+                    "user_misery()",
+                    "failure_rate()",
+                    "failure_count()",
+                ],
+                "orderby": ["team_key_transaction", "user_misery()"],
+                "query": "event.type:transaction",
+                "dataset": "metrics",
+                "per_page": 50,
+            }
+        )
+
+        assert response.status_code == 200, response.content
+        assert len(response.data["data"]) == 1
+        data = response.data["data"][0]
+        meta = response.data["meta"]
+        field_meta = meta["fields"]
+
+        assert data["transaction"] == "foo_transaction"
+        assert data["project"] == self.project.slug
+        assert data["p75(measurements.fcp)"] == 1.0
+        assert data["p75(measurements.lcp)"] == 2.0
+        assert data["p75(measurements.fid)"] == 3.0
+        assert data["p75(measurements.cls)"] == 4.0
+        assert data["apdex()"] == 1.0
+        assert data["count_miserable(user)"] == 1.0
+        assert data["user_misery()"] == 0.058
+        assert data["failure_rate()"] == 1
+        assert data["failure_count()"] == 1
+
+        assert meta["isMetricsData"]
+        assert field_meta["transaction"] == "string"
+        assert field_meta["project"] == "string"
+        assert field_meta["p75(measurements.fcp)"] == "duration"
+        assert field_meta["p75(measurements.lcp)"] == "duration"
+        assert field_meta["p75(measurements.fid)"] == "duration"
+        assert field_meta["p75(measurements.cls)"] == "number"
+        assert field_meta["apdex()"] == "number"
+        assert field_meta["count_miserable(user)"] == "integer"
+        assert field_meta["user_misery()"] == "number"
+        assert field_meta["failure_rate()"] == "percentage"
+        assert field_meta["failure_count()"] == "integer"
+
     def test_no_team_key_transactions(self):
         self.store_transaction_metric(
             1, tags={"transaction": "foo_transaction"}, timestamp=self.min_ago