Skip to content

Commit 248d5cf

Browse files
shruthilayajandrewshie-sentry
authored andcommitted
feat(explore): Add support for apdex and user misery (#94919)
Adds support for apdex and user misery. Note that the is_transaction filter is built into the formulas.
1 parent 91ab100 commit 248d5cf

File tree

2 files changed

+581
-1
lines changed

2 files changed

+581
-1
lines changed

src/sentry/search/eap/spans/formulas.py

Lines changed: 263 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,11 @@
3636
)
3737
from sentry.search.eap.types import SearchResolverConfig
3838
from sentry.search.eap.utils import literal_validator
39-
from sentry.search.events.constants import WEB_VITALS_PERFORMANCE_SCORE_WEIGHTS
39+
from sentry.search.events.constants import (
40+
MISERY_ALPHA,
41+
MISERY_BETA,
42+
WEB_VITALS_PERFORMANCE_SCORE_WEIGHTS,
43+
)
4044
from sentry.snuba import spans_rpc
4145
from sentry.snuba.referrer import Referrer
4246

@@ -795,6 +799,234 @@ def eps(_: ResolvedArguments, settings: ResolverSettings) -> Column.BinaryFormul
795799
)
796800

797801

802+
def apdex(args: ResolvedArguments, settings: ResolverSettings) -> Column.BinaryFormula:
803+
"""
804+
Calculate Apdex score based on response time field and threshold.
805+
806+
Apdex = (Satisfactory + Tolerable/2) / Total Requests
807+
Where:
808+
- Satisfactory: response time ≤ T
809+
- Tolerable: response time > T and ≤ 4T
810+
- Frustrated: response time > 4T
811+
"""
812+
extrapolation_mode = settings["extrapolation_mode"]
813+
814+
# Get the response time field and threshold
815+
response_time_field = cast(AttributeKey, args[0])
816+
threshold = cast(float, args[1])
817+
818+
# Calculate 4T for tolerable range
819+
tolerable_threshold = threshold * 4
820+
821+
# Satisfactory requests: response time ≤ T and is_transaction = True
822+
satisfactory = Column(
823+
conditional_aggregation=AttributeConditionalAggregation(
824+
aggregate=Function.FUNCTION_COUNT,
825+
key=response_time_field,
826+
filter=TraceItemFilter(
827+
and_filter=AndFilter(
828+
filters=[
829+
TraceItemFilter(
830+
comparison_filter=ComparisonFilter(
831+
key=response_time_field,
832+
op=ComparisonFilter.OP_LESS_THAN_OR_EQUALS,
833+
value=AttributeValue(val_double=threshold),
834+
)
835+
),
836+
TraceItemFilter(
837+
comparison_filter=ComparisonFilter(
838+
key=AttributeKey(
839+
type=AttributeKey.TYPE_BOOLEAN, name="sentry.is_segment"
840+
),
841+
op=ComparisonFilter.OP_EQUALS,
842+
value=AttributeValue(val_bool=True),
843+
)
844+
),
845+
]
846+
)
847+
),
848+
extrapolation_mode=extrapolation_mode,
849+
)
850+
)
851+
852+
# Tolerable requests: response time > T and ≤ 4T and is_transaction = True
853+
tolerable = Column(
854+
conditional_aggregation=AttributeConditionalAggregation(
855+
aggregate=Function.FUNCTION_COUNT,
856+
key=response_time_field,
857+
filter=TraceItemFilter(
858+
and_filter=AndFilter(
859+
filters=[
860+
TraceItemFilter(
861+
comparison_filter=ComparisonFilter(
862+
key=response_time_field,
863+
op=ComparisonFilter.OP_GREATER_THAN,
864+
value=AttributeValue(val_double=threshold),
865+
)
866+
),
867+
TraceItemFilter(
868+
comparison_filter=ComparisonFilter(
869+
key=response_time_field,
870+
op=ComparisonFilter.OP_LESS_THAN_OR_EQUALS,
871+
value=AttributeValue(val_double=tolerable_threshold),
872+
)
873+
),
874+
TraceItemFilter(
875+
comparison_filter=ComparisonFilter(
876+
key=AttributeKey(
877+
type=AttributeKey.TYPE_BOOLEAN, name="sentry.is_segment"
878+
),
879+
op=ComparisonFilter.OP_EQUALS,
880+
value=AttributeValue(val_bool=True),
881+
)
882+
),
883+
]
884+
)
885+
),
886+
extrapolation_mode=extrapolation_mode,
887+
)
888+
)
889+
890+
# Total requests: count of all requests with the response time field and is_transaction = True
891+
total = Column(
892+
conditional_aggregation=AttributeConditionalAggregation(
893+
aggregate=Function.FUNCTION_COUNT,
894+
key=response_time_field,
895+
filter=TraceItemFilter(
896+
comparison_filter=ComparisonFilter(
897+
key=AttributeKey(type=AttributeKey.TYPE_BOOLEAN, name="sentry.is_segment"),
898+
op=ComparisonFilter.OP_EQUALS,
899+
value=AttributeValue(val_bool=True),
900+
)
901+
),
902+
extrapolation_mode=extrapolation_mode,
903+
)
904+
)
905+
906+
# Calculate (Satisfactory + Tolerable/2) / Total
907+
numerator = Column(
908+
formula=Column.BinaryFormula(
909+
left=satisfactory,
910+
op=Column.BinaryFormula.OP_ADD,
911+
right=Column(
912+
formula=Column.BinaryFormula(
913+
left=tolerable,
914+
op=Column.BinaryFormula.OP_DIVIDE,
915+
right=Column(literal=LiteralValue(val_double=2.0)),
916+
)
917+
),
918+
)
919+
)
920+
921+
return Column.BinaryFormula(
922+
left=numerator,
923+
op=Column.BinaryFormula.OP_DIVIDE,
924+
right=total,
925+
)
926+
927+
928+
def user_misery(args: ResolvedArguments, settings: ResolverSettings) -> Column.BinaryFormula:
929+
"""
930+
Calculate User Misery score based on response time field and threshold.
931+
932+
User Misery = (miserable_users + α) / (total_unique_users + α + β)
933+
Where:
934+
- miserable_users: unique users with response time > 4T
935+
- total_unique_users: total unique users with response time field
936+
- α (MISERY_ALPHA) = 5.8875
937+
- β (MISERY_BETA) = 111.8625
938+
"""
939+
extrapolation_mode = settings["extrapolation_mode"]
940+
941+
# Get the response time field and threshold
942+
response_time_field = cast(AttributeKey, args[0])
943+
threshold = cast(float, args[1])
944+
945+
# Calculate 4T for miserable threshold
946+
miserable_threshold = threshold * 4
947+
948+
# Count miserable users: unique users with response time > 4T and is_transaction = True
949+
miserable_users = Column(
950+
conditional_aggregation=AttributeConditionalAggregation(
951+
aggregate=Function.FUNCTION_UNIQ,
952+
key=AttributeKey(type=AttributeKey.TYPE_STRING, name="sentry.user"),
953+
filter=TraceItemFilter(
954+
and_filter=AndFilter(
955+
filters=[
956+
TraceItemFilter(
957+
comparison_filter=ComparisonFilter(
958+
key=response_time_field,
959+
op=ComparisonFilter.OP_GREATER_THAN,
960+
value=AttributeValue(val_double=miserable_threshold),
961+
)
962+
),
963+
TraceItemFilter(
964+
comparison_filter=ComparisonFilter(
965+
key=AttributeKey(
966+
type=AttributeKey.TYPE_BOOLEAN, name="sentry.is_segment"
967+
),
968+
op=ComparisonFilter.OP_EQUALS,
969+
value=AttributeValue(val_bool=True),
970+
)
971+
),
972+
]
973+
)
974+
),
975+
extrapolation_mode=extrapolation_mode,
976+
)
977+
)
978+
979+
# Count total unique users: unique users with response time field and is_transaction = True
980+
total_unique_users = Column(
981+
conditional_aggregation=AttributeConditionalAggregation(
982+
aggregate=Function.FUNCTION_UNIQ,
983+
key=AttributeKey(type=AttributeKey.TYPE_STRING, name="sentry.user"),
984+
filter=TraceItemFilter(
985+
and_filter=AndFilter(
986+
filters=[
987+
TraceItemFilter(
988+
exists_filter=ExistsFilter(key=response_time_field),
989+
),
990+
TraceItemFilter(
991+
comparison_filter=ComparisonFilter(
992+
key=AttributeKey(
993+
type=AttributeKey.TYPE_BOOLEAN, name="sentry.is_segment"
994+
),
995+
op=ComparisonFilter.OP_EQUALS,
996+
value=AttributeValue(val_bool=True),
997+
)
998+
),
999+
]
1000+
)
1001+
),
1002+
extrapolation_mode=extrapolation_mode,
1003+
)
1004+
)
1005+
1006+
# Calculate (miserable_users + α) / (total_unique_users + α + β)
1007+
numerator = Column(
1008+
formula=Column.BinaryFormula(
1009+
left=miserable_users,
1010+
op=Column.BinaryFormula.OP_ADD,
1011+
right=Column(literal=LiteralValue(val_double=MISERY_ALPHA)),
1012+
)
1013+
)
1014+
1015+
denominator = Column(
1016+
formula=Column.BinaryFormula(
1017+
left=total_unique_users,
1018+
op=Column.BinaryFormula.OP_ADD,
1019+
right=Column(literal=LiteralValue(val_double=MISERY_ALPHA + MISERY_BETA)),
1020+
)
1021+
)
1022+
1023+
return Column.BinaryFormula(
1024+
left=numerator,
1025+
op=Column.BinaryFormula.OP_DIVIDE,
1026+
right=denominator,
1027+
)
1028+
1029+
7981030
SPAN_FORMULA_DEFINITIONS = {
7991031
"http_response_rate": FormulaDefinition(
8001032
default_search_type="percentage",
@@ -987,4 +1219,34 @@ def eps(_: ResolvedArguments, settings: ResolverSettings) -> Column.BinaryFormul
9871219
"eps": FormulaDefinition(
9881220
default_search_type="rate", arguments=[], formula_resolver=eps, is_aggregate=True
9891221
),
1222+
"apdex": FormulaDefinition(
1223+
default_search_type="number",
1224+
infer_search_type_from_arguments=False,
1225+
arguments=[
1226+
AttributeArgumentDefinition(
1227+
attribute_types={
1228+
"duration",
1229+
*constants.DURATION_TYPE,
1230+
},
1231+
),
1232+
ValueArgumentDefinition(argument_types={"number"}),
1233+
],
1234+
formula_resolver=apdex,
1235+
is_aggregate=True,
1236+
),
1237+
"user_misery": FormulaDefinition(
1238+
default_search_type="number",
1239+
infer_search_type_from_arguments=False,
1240+
arguments=[
1241+
AttributeArgumentDefinition(
1242+
attribute_types={
1243+
"duration",
1244+
*constants.DURATION_TYPE,
1245+
},
1246+
),
1247+
ValueArgumentDefinition(argument_types={"number"}),
1248+
],
1249+
formula_resolver=user_misery,
1250+
is_aggregate=True,
1251+
),
9901252
}

0 commit comments

Comments
 (0)