Browse Source

feat(metrics): Add transaction satisfaction count derived metric (#33411)

Add a new metric to count the satisfying transaction, according to [apdex](https://docs.sentry.io/product/performance/metrics/#apdex). Intended for internal use.
Iker Barriocanal 2 years ago
parent
commit
54c953db89

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

@@ -45,6 +45,7 @@ from sentry.snuba.metrics.fields.snql import (
     errored_preaggr_sessions,
     failure_count_transaction,
     percentage,
+    satisfaction_count_transaction,
     session_duration_filters,
     sessions_errored_set,
     subtraction,
@@ -910,6 +911,14 @@ DERIVED_METRICS: Mapping[str, DerivedMetricExpression] = {
                 failure_count, tx_count, alias=alias
             ),
         ),
+        SingularEntityDerivedMetric(
+            metric_name=TransactionMRI.SATISFIED.value,
+            metrics=[TransactionMRI.DURATION.value],
+            unit="transactions",
+            snql=lambda *_, org_id, metric_ids, alias=None: satisfaction_count_transaction(
+                org_id=org_id, metric_ids=metric_ids, alias=alias
+            ),
+        ),
     ]
 }
 

+ 47 - 1
src/sentry/snuba/metrics/fields/snql.py

@@ -3,7 +3,11 @@ from typing import List
 from snuba_sdk import Column, Function
 
 from sentry.sentry_metrics.utils import resolve_weak
-from sentry.snuba.metrics.naming_layer.public import TransactionStatusTagValue, TransactionTagsKey
+from sentry.snuba.metrics.naming_layer.public import (
+    TransactionSatisfactionTagValue,
+    TransactionStatusTagValue,
+    TransactionTagsKey,
+)
 
 
 def _aggregation_on_session_status_func_factory(aggregate):
@@ -93,6 +97,42 @@ def _dist_count_aggregation_on_tx_status_factory(
     )
 
 
+def _aggregation_on_tx_satisfaction_func_factory(aggregate):
+    def _snql_on_tx_satisfaction_factory(org_id, satisfaction_value: str, metric_ids, alias=None):
+        return Function(
+            aggregate,
+            [
+                Column("value"),
+                Function(
+                    "and",
+                    [
+                        Function(
+                            "equals",
+                            [
+                                Column(
+                                    f"tags[{resolve_weak(org_id, TransactionTagsKey.TRANSACTION_SATISFACTION.value)}]"
+                                ),
+                                resolve_weak(org_id, satisfaction_value),
+                            ],
+                        ),
+                        Function("in", [Column("metric_id"), list(metric_ids)]),
+                    ],
+                ),
+            ],
+            alias,
+        )
+
+    return _snql_on_tx_satisfaction_factory
+
+
+def _dist_count_aggregation_on_tx_satisfaction_factory(
+    org_id, satisfaction: str, metric_ids, alias=None
+):
+    return _aggregation_on_tx_satisfaction_func_factory("countIf")(
+        org_id, satisfaction, metric_ids, alias
+    )
+
+
 def all_sessions(org_id: int, metric_ids, alias=None):
     return _counter_sum_aggregation_on_session_status_factory(
         org_id, session_status="init", metric_ids=metric_ids, alias=alias
@@ -181,6 +221,12 @@ def failure_count_transaction(org_id, metric_ids, alias=None):
     )
 
 
+def satisfaction_count_transaction(org_id, metric_ids, alias=None):
+    return _dist_count_aggregation_on_tx_satisfaction_factory(
+        org_id, TransactionSatisfactionTagValue.SATISFIED.value, metric_ids, alias
+    )
+
+
 def percentage(arg1_snql, arg2_snql, alias=None):
     return Function("minus", [1, Function("divide", [arg1_snql, arg2_snql])], alias)
 

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

@@ -79,3 +79,4 @@ class TransactionMRI(Enum):
     ALL = "e:transactions/all@none"
     FAILURE_COUNT = "e:transactions/failure_count@none"
     FAILURE_RATE = "e:transaction/failure_rate@ratio"
+    SATISFIED = "e:transactions/satisfied@none"

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

@@ -14,6 +14,7 @@ __all__ = (
     "TransactionMetricKey",
     "TransactionTagsKey",
     "TransactionStatusTagValue",
+    "TransactionSatisfactionTagValue",
 )
 
 from enum import Enum
@@ -73,10 +74,13 @@ class TransactionMetricKey(Enum):
     FAILURE_RATE = "transaction.failure_rate"
 
 
+# TODO: these tag keys and values below probably don't belong here, and should
+# be moved to another more private file.
 class TransactionTagsKey(Enum):
     """Identifier for a transaction-related tag."""
 
     TRANSACTION_STATUS = "transaction.status"
+    TRANSACTION_SATISFACTION = "satisfaction"
 
 
 class TransactionStatusTagValue(Enum):
@@ -90,3 +94,11 @@ class TransactionStatusTagValue(Enum):
     CANCELLED = "cancelled"
     UNKNOWN = "unknown"
     ABORTED = "aborted"
+
+
+class TransactionSatisfactionTagValue(Enum):
+    """Identifier value for the satisfaction of a transaction."""
+
+    SATISFIED = "satisfied"
+    TOLERATED = "tolerated"
+    FRUSTRATED = "frustrated"

+ 42 - 1
tests/sentry/snuba/metrics/test_snql.py

@@ -19,7 +19,12 @@ from sentry.snuba.metrics import (
     sessions_errored_set,
     subtraction,
 )
-from sentry.snuba.metrics.fields.snql import division_float, failure_count_transaction
+from sentry.snuba.metrics.fields.snql import (
+    division_float,
+    failure_count_transaction,
+    satisfaction_count_transaction,
+)
+from sentry.snuba.metrics.naming_layer.public import TransactionSatisfactionTagValue
 from sentry.testutils import TestCase
 
 
@@ -156,6 +161,42 @@ class DerivedMetricSnQLTestCase(TestCase):
             == expected_failed_txs
         )
 
+    def test_dist_count_aggregation_on_tx_satisfaction(self):
+        org_id = 1643
+
+        assert satisfaction_count_transaction(
+            org_id, self.metric_ids, "transaction.satisfied"
+        ) == Function(
+            "countIf",
+            [
+                Column("value"),
+                Function(
+                    "and",
+                    [
+                        Function(
+                            "equals",
+                            [
+                                Column(
+                                    f"tags[{resolve_weak(org_id, TransactionTagsKey.TRANSACTION_SATISFACTION.value)}]"
+                                ),
+                                resolve_weak(
+                                    org_id, TransactionSatisfactionTagValue.SATISFIED.value
+                                ),
+                            ],
+                        ),
+                        Function(
+                            "in",
+                            [
+                                Column("metric_id"),
+                                list(self.metric_ids),
+                            ],
+                        ),
+                    ],
+                ),
+            ],
+            "transaction.satisfied",
+        )
+
     def test_percentage_in_snql(self):
         org_id = 666
         alias = "foo.percentage"