Skip to content

Commit 0baf599

Browse files
scttcpershashjar
authored andcommitted
feat(issues): Include empty tags in group tagkey values endpoints (#102048)
Add support for including empty tag values in the group tagkey values endpoints when the organizations:issue-tags-include-empty-values feature flag is enabled. Currently will display an empty string and the count and will need to follow up with frontend fixes. <img width="924" height="341" alt="image" src="https://github.com/user-attachments/assets/abe4744e-ebbd-4340-bdaa-fe78f9af9957" /> part of https://linear.app/getsentry/issue/RTC-1181/
1 parent 9b7aa16 commit 0baf599

File tree

7 files changed

+272
-23
lines changed

7 files changed

+272
-23
lines changed

src/sentry/issues/endpoints/group_tagkey_details.py

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from rest_framework.request import Request
33
from rest_framework.response import Response
44

5-
from sentry import tagstore
5+
from sentry import features, tagstore
66
from sentry.api.api_owners import ApiOwner
77
from sentry.api.api_publish_status import ApiPublishStatus
88
from sentry.api.base import region_silo_endpoint
@@ -69,6 +69,11 @@ def get(self, request: Request, group, key) -> Response:
6969
"""
7070
lookup_key = tagstore.backend.prefix_reserved_key(key)
7171
tenant_ids = {"organization_id": group.project.organization_id}
72+
include_empty_values = features.has(
73+
"organizations:issue-tags-include-empty-values",
74+
group.project.organization,
75+
actor=request.user,
76+
)
7277
try:
7378
environment_id = get_environment_id(request, group.project.organization_id)
7479
except Environment.DoesNotExist:
@@ -81,18 +86,27 @@ def get(self, request: Request, group, key) -> Response:
8186
environment_id,
8287
lookup_key,
8388
tenant_ids=tenant_ids,
89+
include_empty_values=include_empty_values,
8490
)
8591
except tagstore.GroupTagKeyNotFound:
8692
raise ResourceDoesNotExist
8793

8894
if group_tag_key.count is None:
8995
group_tag_key.count = tagstore.backend.get_group_tag_value_count(
90-
group, environment_id, lookup_key, tenant_ids=tenant_ids
96+
group,
97+
environment_id,
98+
lookup_key,
99+
tenant_ids=tenant_ids,
100+
include_empty_values=include_empty_values,
91101
)
92102

93103
if group_tag_key.top_values is None:
94104
group_tag_key.top_values = tagstore.backend.get_top_group_tag_values(
95-
group, environment_id, lookup_key, tenant_ids=tenant_ids
105+
group,
106+
environment_id,
107+
lookup_key,
108+
tenant_ids=tenant_ids,
109+
include_empty_values=include_empty_values,
96110
)
97111

98112
return Response(serialize(group_tag_key, request.user, serializer=TagKeySerializer()))

src/sentry/issues/endpoints/group_tagkey_values.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from rest_framework.request import Request
33
from rest_framework.response import Response
44

5-
from sentry import analytics, tagstore
5+
from sentry import analytics, features, tagstore
66
from sentry.analytics.events.eventuser_endpoint_request import EventUserEndpointRequest
77
from sentry.api.api_owners import ApiOwner
88
from sentry.api.api_publish_status import ApiPublishStatus
@@ -81,12 +81,18 @@ def get(self, request: Request, group, key) -> Response:
8181

8282
environment_ids = [e.id for e in get_environments(request, group.project.organization)]
8383
tenant_ids = {"organization_id": group.project.organization_id}
84+
include_empty_values = features.has(
85+
"organizations:issue-tags-include-empty-values",
86+
group.project.organization,
87+
actor=request.user,
88+
)
8489
try:
8590
tagstore.backend.get_group_tag_key(
8691
group,
8792
None,
8893
lookup_key,
8994
tenant_ids=tenant_ids,
95+
include_empty_values=include_empty_values,
9096
)
9197
except tagstore.GroupTagKeyNotFound:
9298
raise ResourceDoesNotExist
@@ -106,7 +112,12 @@ def get(self, request: Request, group, key) -> Response:
106112
serializer_cls = None
107113

108114
paginator = tagstore.backend.get_group_tag_value_paginator(
109-
group, environment_ids, lookup_key, order_by=order_by, tenant_ids=tenant_ids
115+
group,
116+
environment_ids,
117+
lookup_key,
118+
order_by=order_by,
119+
tenant_ids=tenant_ids,
120+
include_empty_values=include_empty_values,
110121
)
111122

112123
return self.paginate(

src/sentry/tagstore/base.py

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,14 @@ def get_tag_values(self, project_id, environment_id, key: str, tenant_ids=None):
146146
"""
147147
raise NotImplementedError
148148

149-
def get_group_tag_key(self, group, environment_id, key: str, tenant_ids=None):
149+
def get_group_tag_key(
150+
self,
151+
group,
152+
environment_id,
153+
key: str,
154+
tenant_ids=None,
155+
include_empty_values=False,
156+
):
150157
"""
151158
>>> get_group_tag_key(group, 3, "key1")
152159
"""
@@ -220,14 +227,21 @@ def get_group_tag_value_iter(
220227
limit: int = 1000,
221228
offset: int = 0,
222229
tenant_ids: dict[str, int | str] | None = None,
230+
include_empty_values=False,
223231
) -> list[GroupTagValue]:
224232
"""
225233
>>> get_group_tag_value_iter(group, 2, 3, 'environment')
226234
"""
227235
raise NotImplementedError
228236

229237
def get_group_tag_value_paginator(
230-
self, group, environment_ids, key: str, order_by="-id", tenant_ids=None
238+
self,
239+
group,
240+
environment_ids,
241+
key: str,
242+
order_by="-id",
243+
tenant_ids=None,
244+
include_empty_values=False,
231245
):
232246
"""
233247
>>> get_group_tag_value_paginator(group, 3, 'environment')
@@ -262,14 +276,27 @@ def get_generic_groups_user_counts(
262276
) -> dict[int, int]:
263277
raise NotImplementedError
264278

265-
def get_group_tag_value_count(self, group, environment_id, key: str, tenant_ids=None):
279+
def get_group_tag_value_count(
280+
self,
281+
group,
282+
environment_id,
283+
key: str,
284+
tenant_ids=None,
285+
include_empty_values=False,
286+
):
266287
"""
267288
>>> get_group_tag_value_count(group, 3, 'key1')
268289
"""
269290
raise NotImplementedError
270291

271292
def get_top_group_tag_values(
272-
self, group, environment_id, key: str, limit=TOP_VALUES_DEFAULT_LIMIT, tenant_ids=None
293+
self,
294+
group,
295+
environment_id,
296+
key: str,
297+
limit=TOP_VALUES_DEFAULT_LIMIT,
298+
tenant_ids=None,
299+
include_empty_values=False,
273300
):
274301
"""
275302
>>> get_top_group_tag_values(group, 3, 'key1')

src/sentry/tagstore/snuba/backend.py

Lines changed: 57 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@ def __get_tag_key_and_top_values(
170170
limit=3,
171171
raise_on_empty=True,
172172
tenant_ids=None,
173+
include_empty_values=False,
173174
**kwargs,
174175
):
175176
tag = self.format_string.format(key)
@@ -180,7 +181,8 @@ def __get_tag_key_and_top_values(
180181
aggregations = kwargs.get("aggregations", [])
181182

182183
dataset, filters = self.apply_group_filters(group, filters)
183-
conditions.append([tag, "!=", ""])
184+
if not include_empty_values:
185+
conditions.append([tag, "!=", ""])
184186
aggregations += [
185187
["uniq", tag, "values_seen"],
186188
["count()", "", "count"],
@@ -500,14 +502,22 @@ def get_tag_values(self, project_id, environment_id, key, tenant_ids=None):
500502
)
501503
return set(key.top_values)
502504

503-
def get_group_tag_key(self, group, environment_id, key, tenant_ids=None):
505+
def get_group_tag_key(
506+
self,
507+
group,
508+
environment_id,
509+
key,
510+
tenant_ids=None,
511+
include_empty_values=None,
512+
):
504513
return self.__get_tag_key_and_top_values(
505514
group.project_id,
506515
group,
507516
environment_id,
508517
key,
509518
limit=TOP_VALUES_DEFAULT_LIMIT,
510519
tenant_ids=tenant_ids,
520+
include_empty_values=include_empty_values,
511521
)
512522

513523
def get_group_tag_keys(
@@ -651,12 +661,21 @@ def apply_group_filters(self, group: Group | None, filters):
651661
dataset = Dataset.IssuePlatform
652662
return dataset, filters
653663

654-
def get_group_tag_value_count(self, group, environment_id, key: str, tenant_ids=None):
664+
def get_group_tag_value_count(
665+
self,
666+
group,
667+
environment_id,
668+
key: str,
669+
tenant_ids=None,
670+
include_empty_values=False,
671+
):
655672
tag = self.format_string.format(key)
656673
filters = {"project_id": get_project_list(group.project_id)}
657674
if environment_id:
658675
filters["environment"] = [environment_id]
659676
conditions = [[tag, "!=", ""]]
677+
if include_empty_values:
678+
conditions = []
660679
aggregations = [["count()", "", "count"]]
661680
dataset, filters = self.apply_group_filters(group, filters)
662681

@@ -670,10 +689,22 @@ def get_group_tag_value_count(self, group, environment_id, key: str, tenant_ids=
670689
)
671690

672691
def get_top_group_tag_values(
673-
self, group, environment_id, key: str, limit=TOP_VALUES_DEFAULT_LIMIT, tenant_ids=None
692+
self,
693+
group,
694+
environment_id,
695+
key: str,
696+
limit=TOP_VALUES_DEFAULT_LIMIT,
697+
tenant_ids=None,
698+
include_empty_values=False,
674699
):
675700
tag = self.__get_tag_key_and_top_values(
676-
group.project_id, group, environment_id, key, limit, tenant_ids=tenant_ids
701+
group.project_id,
702+
group,
703+
environment_id,
704+
key,
705+
limit,
706+
tenant_ids=tenant_ids,
707+
include_empty_values=include_empty_values,
677708
)
678709
return tag.top_values
679710

@@ -1452,18 +1483,24 @@ def get_group_tag_value_iter(
14521483
limit: int = 1000,
14531484
offset: int = 0,
14541485
tenant_ids: dict[str, int | str] | None = None,
1486+
include_empty_values=False,
14551487
) -> list[GroupTagValue]:
1456-
filters = {
1488+
filters: dict[str, list[Any]] = {
14571489
"project_id": get_project_list(group.project_id),
14581490
self.key_column: [key],
14591491
}
1492+
# When you filter by tags_key = ["foo"], we're filtering to where the tags_key array contains "foo".
1493+
# In order to get the total count with empty values, we need to remove the filter.
1494+
if include_empty_values:
1495+
del filters[self.key_column]
14601496
dataset, filters = self.apply_group_filters(group, filters)
14611497

14621498
if environment_ids:
14631499
filters["environment"] = environment_ids
1500+
tag_expression = self.format_string.format(key)
14641501
results = snuba.query(
14651502
dataset=dataset,
1466-
groupby=[self.value_column],
1503+
groupby=[tag_expression],
14671504
filter_keys=filters,
14681505
conditions=[],
14691506
aggregations=[
@@ -1486,7 +1523,13 @@ def get_group_tag_value_iter(
14861523
return group_tag_values
14871524

14881525
def get_group_tag_value_paginator(
1489-
self, group, environment_ids, key: str, order_by="-id", tenant_ids=None
1526+
self,
1527+
group,
1528+
environment_ids,
1529+
key: str,
1530+
order_by="-id",
1531+
tenant_ids=None,
1532+
include_empty_values=False,
14901533
):
14911534
from sentry.api.paginator import SequencePaginator
14921535

@@ -1499,7 +1542,12 @@ def get_group_tag_value_paginator(
14991542
raise ValueError("Unsupported order_by: %s" % order_by)
15001543

15011544
group_tag_values = self.get_group_tag_value_iter(
1502-
group, environment_ids, key, orderby="-last_seen", tenant_ids=tenant_ids
1545+
group,
1546+
environment_ids,
1547+
key,
1548+
orderby="-last_seen",
1549+
tenant_ids=tenant_ids,
1550+
include_empty_values=include_empty_values,
15031551
)
15041552

15051553
desc = order_by.startswith("-")

tests/sentry/issues/endpoints/test_group_tagkey_details.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from sentry.models.group import Group
22
from sentry.testutils.cases import APITestCase, PerformanceIssueTestCase, SnubaTestCase
33
from sentry.testutils.helpers.datetime import before_now
4+
from sentry.testutils.helpers.features import with_feature
45

56

67
class GroupTagDetailsTest(APITestCase, SnubaTestCase, PerformanceIssueTestCase):
@@ -44,3 +45,65 @@ def test_simple_perf(self) -> None:
4445
assert response.status_code == 200, response.content
4546
assert response.data["key"] == "foo"
4647
assert response.data["totalValues"] == 2
48+
49+
def test_excludes_empty_values_by_default(self) -> None:
50+
for i in range(2):
51+
self.store_event(
52+
data={
53+
"tags": {"foo": ""},
54+
"fingerprint": ["group-empty-default"],
55+
"timestamp": before_now(seconds=1 + i).isoformat(),
56+
},
57+
project_id=self.project.id,
58+
assert_no_errors=False,
59+
)
60+
61+
event = self.store_event(
62+
data={
63+
"tags": {"foo": "baz"},
64+
"fingerprint": ["group-empty-default"],
65+
"timestamp": before_now(seconds=1).isoformat(),
66+
},
67+
project_id=self.project.id,
68+
)
69+
70+
self.login_as(user=self.user)
71+
72+
url = f"/api/0/issues/{event.group.id}/tags/foo/"
73+
response = self.client.get(url, format="json")
74+
assert response.status_code == 200, response.content
75+
assert response.data["totalValues"] == 1
76+
assert "" not in {value["value"] for value in response.data["topValues"]}
77+
78+
@with_feature({"organizations:issue-tags-include-empty-values": True})
79+
def test_includes_empty_values_with_feature(self) -> None:
80+
for i in range(2):
81+
self.store_event(
82+
data={
83+
"tags": {"foo": ""},
84+
"fingerprint": ["group-empty-flag"],
85+
"timestamp": before_now(seconds=1 + i).isoformat(),
86+
},
87+
project_id=self.project.id,
88+
assert_no_errors=False,
89+
)
90+
91+
event = self.store_event(
92+
data={
93+
"tags": {"foo": "baz"},
94+
"fingerprint": ["group-empty-flag"],
95+
"timestamp": before_now(seconds=1).isoformat(),
96+
},
97+
project_id=self.project.id,
98+
)
99+
100+
self.login_as(user=self.user)
101+
102+
url = f"/api/0/issues/{event.group.id}/tags/foo/"
103+
response = self.client.get(url, format="json")
104+
105+
assert response.status_code == 200, response.content
106+
assert response.data["totalValues"] == 3
107+
top_values = {value["value"]: value["count"] for value in response.data["topValues"]}
108+
assert top_values.get("") == 2
109+
assert top_values.get("baz") == 1

0 commit comments

Comments
 (0)