Skip to content
Merged
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
9 changes: 6 additions & 3 deletions src/sentry/snuba/metrics/query_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ def parse_field(field: str) -> MetricField:
# These are only allowed because the parser in metrics_sessions_v2
# generates them. Long term we should not allow any functions, but rather
# a limited expression language with only AND, OR, IN and NOT IN
FUNCTION_ALLOWLIST = ("and", "or", "equals", "in")
FUNCTION_ALLOWLIST = ("and", "or", "equals", "in", "tuple")


def resolve_tags(
Expand All @@ -103,8 +103,11 @@ def resolve_tags(
if isinstance(input_, (list, tuple)):
elements = [resolve_tags(use_case_id, org_id, item, is_tag_value=True) for item in input_]
# Lists are either arguments to IN or NOT IN. In both cases, we can
# drop unknown strings:
return [x for x in elements if x != STRING_NOT_FOUND]
# drop unknown strings.
filtered_elements = [x for x in elements if x != STRING_NOT_FOUND]
# We check whether it is a list or tuple in order to know which type to return. This is needed
# because in the "tuple" function the parameters must be a list of tuples and not a list of lists.
return filtered_elements if isinstance(input_, list) else tuple(filtered_elements)
if isinstance(input_, Function):
if input_.function == "ifNull":
# This was wrapped automatically by QueryBuilder, remove wrapper
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from django.utils import timezone
from django.utils.datastructures import MultiValueDict
from freezegun import freeze_time
from snuba_sdk import Direction, Granularity, Limit, Offset
from snuba_sdk import Column, Condition, Direction, Function, Granularity, Limit, Offset, Op

from sentry.api.utils import InvalidParams
from sentry.sentry_metrics import indexer
Expand Down Expand Up @@ -349,6 +349,79 @@ def test_custom_measurement_query_with_invalid_mri(self):
use_case_id=UseCaseKey.PERFORMANCE,
)

@freeze_time("2022-09-29 11:30:00")
def test_query_with_tuple_condition(self):
now = timezone.now()

for value, transaction in ((10, "/foo"), (20, "/bar"), (30, "/lorem")):
self.store_metric(
org_id=self.organization.id,
project_id=self.project.id,
type="distribution",
name=TransactionMRI.DURATION.value,
tags={"transaction": transaction},
timestamp=(now - timedelta(seconds=1)).timestamp(),
value=value,
use_case_id=UseCaseKey.PERFORMANCE,
)

metrics_query = MetricsQuery(
org_id=self.organization.id,
project_ids=[self.project.id],
select=[
MetricField(
op="count",
metric_mri=TransactionMRI.DURATION.value,
),
],
start=now - timedelta(minutes=1),
end=now,
groupby=[],
where=[
Condition(
lhs=Function(
function="tuple",
parameters=[
Column(
name="tags[transaction]",
)
],
),
op=Op.IN,
rhs=Function(
function="tuple",
parameters=[("/foo",), ("/bar",)],
),
)
],
granularity=Granularity(granularity=60),
limit=Limit(limit=1),
offset=Offset(offset=0),
include_series=False,
)

data = get_series(
[self.project],
metrics_query=metrics_query,
include_meta=True,
use_case_id=UseCaseKey.PERFORMANCE,
)

groups = data["groups"]
assert len(groups) == 1

expected_count = 2
expected_alias = "count(transaction.duration)"
assert groups[0]["totals"] == {
expected_alias: expected_count,
}
assert data["meta"] == sorted(
[
{"name": expected_alias, "type": "UInt64"},
],
key=lambda elem: elem["name"],
)

@freeze_time("2022-09-22 10:01:09")
def test_count_transaction_with_valid_condition(self):
now = timezone.now()
Expand Down
106 changes: 106 additions & 0 deletions tests/sentry/snuba/metrics/test_query_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -1036,3 +1036,109 @@ def test_valid_latest_release_alias_filter(self):
rhs=["bar"],
)
]


class ResolveTagsTestCase(TestCase):
def setUp(self):
self.org_id = ORG_ID
self.use_case_id = UseCaseKey.PERFORMANCE

def test_resolve_tags_with_unary_tuple(self):
transactions = ["/foo", "/bar"]

for transaction in ["transaction"] + transactions:
indexer.record(use_case_id=self.use_case_id, org_id=self.org_id, string=transaction)

resolved_query = resolve_tags(
self.use_case_id,
self.org_id,
Condition(
lhs=Function(
function="tuple",
parameters=[
Column(
name="tags[transaction]",
)
],
),
op=Op.IN,
rhs=Function(
function="tuple",
parameters=[(transaction,) for transaction in transactions],
),
),
)

assert resolved_query == Condition(
lhs=Function(
function="tuple",
parameters=[
Column(
name=resolve_tag_key(self.use_case_id, self.org_id, "transaction"),
)
],
),
op=Op.IN,
rhs=Function(
function="tuple",
parameters=[
(resolve_tag_value(self.use_case_id, self.org_id, transaction),)
for transaction in transactions
],
),
)

def test_resolve_tags_with_binary_tuple(self):
tags = [("/foo", "ios"), ("/bar", "android")]

for transaction, platform in [("transaction", "platform")] + tags:
indexer.record(use_case_id=self.use_case_id, org_id=self.org_id, string=transaction)
indexer.record(use_case_id=self.use_case_id, org_id=self.org_id, string=platform)

resolved_query = resolve_tags(
self.use_case_id,
self.org_id,
Condition(
lhs=Function(
function="tuple",
parameters=[
Column(
name="tags[transaction]",
),
Column(
name="tags[platform]",
),
],
),
op=Op.IN,
rhs=Function(
function="tuple",
parameters=[(transaction, platform) for transaction, platform in tags],
),
),
)

assert resolved_query == Condition(
lhs=Function(
function="tuple",
parameters=[
Column(
name=resolve_tag_key(self.use_case_id, self.org_id, "transaction"),
),
Column(
name=resolve_tag_key(self.use_case_id, self.org_id, "platform"),
),
],
),
op=Op.IN,
rhs=Function(
function="tuple",
parameters=[
(
resolve_tag_value(self.use_case_id, self.org_id, transaction),
resolve_tag_value(self.use_case_id, self.org_id, platform),
)
for transaction, platform in tags
],
),
)