Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add SnQL support for apdex #1

Open
wants to merge 1 commit into
base: ApdexSnQLSupportBase
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
156 changes: 150 additions & 6 deletions src/sentry/search/events/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -2179,6 +2179,55 @@ def __init__(self, dataset: Dataset, params: ParamsType):
),
default_result_type="integer",
),
SnQLFunction(
"apdex",
optional_args=[NullableNumberRange("satisfaction", 0, None)],
snql_aggregate=self._resolve_apdex_function,
default_result_type="number",
),
SnQLFunction(
"count_miserable",
# Using the generic FunctionArg here temporarily till we
# implement a resolver for count columns in SnQL. The only
# column to be passed through here for now is `user`.
required_args=[FunctionArg("column")],
optional_args=[NullableNumberRange("satisfaction", 0, None)],
calculated_args=[
{
"name": "tolerated",
"fn": lambda args: args["satisfaction"] * 4.0
if args["satisfaction"] is not None
else None,
}
],
snql_aggregate=self._resolve_count_miserable_function,
default_result_type="integer",
),
SnQLFunction(
"user_misery",
# To correct for sensitivity to low counts, User Misery is modeled as a Beta Distribution Function.
# With prior expectations, we have picked the expected mean user misery to be 0.05 and variance
# to be 0.0004. This allows us to calculate the alpha (5.8875) and beta (111.8625) parameters,
# with the user misery being adjusted for each fast/slow unique transaction. See:
# https://stats.stackexchange.com/questions/47771/what-is-the-intuition-behind-beta-distribution
# for an intuitive explanation of the Beta Distribution Function.
optional_args=[
NullableNumberRange("satisfaction", 0, None),
with_default(5.8875, NumberRange("alpha", 0, None)),
with_default(111.8625, NumberRange("beta", 0, None)),
],
calculated_args=[
{
"name": "tolerated",
"fn": lambda args: args["satisfaction"] * 4.0
if args["satisfaction"] is not None
else None,
},
{"name": "parameter_sum", "fn": lambda args: args["alpha"] + args["beta"]},
],
snql_aggregate=self._resolve_user_misery_function,
default_result_type="number",
),
SnQLFunction(
"count",
snql_aggregate=lambda _, alias: Function(
Expand Down Expand Up @@ -2225,14 +2274,8 @@ def __init__(self, dataset: Dataset, params: ParamsType):
SnQLFunction("p100", snql_aggregate=self._resolve_unimplemented_function),
SnQLFunction("eps", snql_aggregate=self._resolve_unimplemented_function),
SnQLFunction("epm", snql_aggregate=self._resolve_unimplemented_function),
SnQLFunction("apdex", snql_aggregate=self._resolve_unimplemented_function),
SnQLFunction(
"count_miserable", snql_aggregate=self._resolve_unimplemented_function
),
SnQLFunction("user_misery", snql_aggregate=self._resolve_unimplemented_function),
SnQLFunction("array_join", snql_aggregate=self._resolve_unimplemented_function),
SnQLFunction("histogram", snql_aggregate=self._resolve_unimplemented_function),
SnQLFunction("count_unique", snql_aggregate=self._resolve_unimplemented_function),
SnQLFunction("count_at_least", snql_aggregate=self._resolve_unimplemented_function),
SnQLFunction("min", snql_aggregate=self._resolve_unimplemented_function),
SnQLFunction("max", snql_aggregate=self._resolve_unimplemented_function),
Expand All @@ -2256,6 +2299,7 @@ def __init__(self, dataset: Dataset, params: ParamsType):
SnQLFunction(
"absolute_correlation", snql_aggregate=self._resolve_unimplemented_function
),
SnQLFunction("count_unique", snql_aggregate=self._resolve_unimplemented_function),
SnQLFunction("count_if", snql_aggregate=self._resolve_unimplemented_function),
SnQLFunction(
"compare_numeric_aggregate", snql_aggregate=self._resolve_unimplemented_function
Expand Down Expand Up @@ -2362,6 +2406,9 @@ def resolve_function(self, function: str, match: Optional[Match[str]] = None) ->

name, arguments, alias = self.parse_function(match)
snql_function = self.function_converter.get(name)

arguments = snql_function.format_as_arguments(name, arguments, self.params)

if snql_function.snql_aggregate is not None:
self.aggregates.append(snql_function.snql_aggregate(arguments, alias))
return snql_function.snql_aggregate(arguments, alias)
Expand Down Expand Up @@ -2615,6 +2662,103 @@ def _resolve_error_handled_alias(self, _: str) -> SelectType:
"cast", [self.column("error.handled"), "Array(Nullable(UInt8))"], ERROR_HANDLED_ALIAS
)

def _project_threshold_multi_if_function(self) -> SelectType:
"""Accessed by `_resolve_apdex_function` and `_resolve_count_miserable_function`,
this returns the right duration value (for example, lcp or duration) based
on project or transaction thresholds that have been configured by the user.
"""

return Function(
"multiIf",
[
Function(
"equals",
[
Function(
"tupleElement",
[self.resolve_field("project_threshold_config"), 1],
),
"lcp",
],
),
self.column("measurements.lcp"),
self.column("transaction.duration"),
],
)

def _resolve_apdex_function(self, args: Mapping[str, str], alias: str) -> SelectType:
if args["satisfaction"]:
function_args = [self.column("transaction.duration"), int(args["satisfaction"])]
else:
function_args = [
self._project_threshold_multi_if_function(),
Function("tupleElement", [self.resolve_field("project_threshold_config"), 2]),
]

return Function("apdex", function_args, alias)

def _resolve_count_miserable_function(self, args: Mapping[str, str], alias: str) -> SelectType:
if args["satisfaction"]:
lhs = self.column("transaction.duration")
rhs = int(args["tolerated"])
else:
lhs = self._project_threshold_multi_if_function()
rhs = Function(
"multiply",
[
Function(
"tupleElement",
[self.resolve_field("project_threshold_config"), 2],
),
4,
],
)
col = args["column"]

return Function(
"uniqIf",
[
self.resolve_field_alias(col) if self.is_field_alias(col) else self.column(col),
Function("greater", [lhs, rhs]),
],
alias,
)

def _resolve_user_misery_function(self, args: Mapping[str, str], alias: str) -> SelectType:
if args["satisfaction"]:
count_miserable_agg = self.resolve_function(
f"count_miserable(user,{args['satisfaction']})"
)
else:
count_miserable_agg = self.resolve_function("count_miserable(user)")

return Function(
"ifNull",
[
Function(
"divide",
[
Function(
"plus",
[
count_miserable_agg,
args["alpha"],
],
),
Function(
"plus",
[
Function("uniq", [self.column("user")]),
args["parameter_sum"],
],
),
],
),
0,
],
alias,
)

def _resolve_unimplemented_function(
self,
_: List[str],
Expand Down
Loading