Skip to content

Commit f2148f4

Browse files
feat(occurrences on eap): Implement tagstore EAP query for group list tag value (#111963)
Implements double reads of occurrences from EAP for `get_group_list_tag_value` & `get_generic_group_list_tag_value` in `src/sentry/tagstore/snuba/backend.py`. --------- Co-authored-by: getsantry[bot] <66042841+getsantry[bot]@users.noreply.github.com>
1 parent b4f2897 commit f2148f4

2 files changed

Lines changed: 333 additions & 3 deletions

File tree

src/sentry/tagstore/snuba/backend.py

Lines changed: 183 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,32 @@ def _reasonable_release_tags_match(control: set[TagValue], experimental: set[Tag
164164
)
165165

166166

167+
def _reasonable_group_list_tag_value_match(
168+
control: dict[int, GroupTagValue],
169+
experimental: dict[int, GroupTagValue],
170+
) -> bool:
171+
if not set(experimental.keys()).issubset(set(control.keys())):
172+
return False
173+
for group_id in experimental:
174+
ctrl = control[group_id]
175+
exp = experimental[group_id]
176+
if exp.times_seen > ctrl.times_seen:
177+
return False
178+
if (
179+
exp.first_seen is not None
180+
and ctrl.first_seen is not None
181+
and exp.first_seen < ctrl.first_seen
182+
):
183+
return False
184+
if (
185+
exp.last_seen is not None
186+
and ctrl.last_seen is not None
187+
and exp.last_seen > ctrl.last_seen
188+
):
189+
return False
190+
return True
191+
192+
167193
class _OptimizeKwargs(TypedDict, total=False):
168194
turbo: bool
169195
sample: int
@@ -825,7 +851,7 @@ def get_group_list_tag_value(
825851
value,
826852
tenant_ids=None,
827853
):
828-
return self.__get_group_list_tag_value(
854+
snuba_result = self.__get_group_list_tag_value(
829855
project_ids,
830856
group_id_list,
831857
environment_ids,
@@ -834,9 +860,38 @@ def get_group_list_tag_value(
834860
Dataset.Events,
835861
[DEFAULT_TYPE_CONDITION],
836862
[],
837-
"tagstore.get_group_list_tag_value",
863+
Referrer.TAGSTORE_GET_GROUP_LIST_TAG_VALUE.value,
838864
tenant_ids=tenant_ids,
839865
)
866+
result = snuba_result
867+
868+
callsite = "SnubaTagStorage::get_group_list_tag_value"
869+
if EAPOccurrencesComparator.should_check_experiment(callsite):
870+
eap_result = self._eap_get_group_list_tag_value(
871+
project_ids,
872+
group_id_list,
873+
environment_ids,
874+
key,
875+
value,
876+
referrer=Referrer.TAGSTORE_GET_GROUP_LIST_TAG_VALUE.value,
877+
occurrence_category=OccurrenceCategory.ERROR,
878+
)
879+
result = EAPOccurrencesComparator.check_and_choose(
880+
control_data=snuba_result,
881+
experimental_data=eap_result,
882+
callsite=callsite,
883+
is_experimental_data_a_null_result=len(eap_result) == 0,
884+
reasonable_match_comparator=_reasonable_group_list_tag_value_match,
885+
debug_context={
886+
"project_ids": list(project_ids),
887+
"group_ids": list(group_id_list),
888+
"environment_ids": list(environment_ids) if environment_ids else None,
889+
"key": key,
890+
"value": value,
891+
},
892+
)
893+
894+
return result
840895

841896
def get_generic_group_list_tag_value(
842897
self,
@@ -890,12 +945,137 @@ def get_generic_group_list_tag_value(
890945
result_snql["data"], ["group_id"], ["times_seen", "first_seen", "last_seen"]
891946
)
892947

893-
return {
948+
snuba_result = {
894949
group_id: GroupTagValue(
895950
group_id=group_id, key=key, value=value, **fix_tag_value_data(data)
896951
)
897952
for group_id, data in nested_groups.items()
898953
}
954+
result = snuba_result
955+
956+
callsite = "SnubaTagStorage::get_generic_group_list_tag_value"
957+
if EAPOccurrencesComparator.should_check_experiment(callsite):
958+
eap_result = self._eap_get_group_list_tag_value(
959+
project_ids,
960+
group_id_list,
961+
environment_ids,
962+
key,
963+
value,
964+
referrer=Referrer.TAGSTORE_GET_GENERIC_GROUP_LIST_TAG_VALUE.value,
965+
occurrence_category=OccurrenceCategory.ISSUE_PLATFORM,
966+
)
967+
result = EAPOccurrencesComparator.check_and_choose(
968+
control_data=snuba_result,
969+
experimental_data=eap_result,
970+
callsite=callsite,
971+
is_experimental_data_a_null_result=len(eap_result) == 0,
972+
reasonable_match_comparator=_reasonable_group_list_tag_value_match,
973+
debug_context={
974+
"project_ids": list(project_ids),
975+
"group_ids": list(group_id_list),
976+
"environment_ids": list(environment_ids) if environment_ids else None,
977+
"key": key,
978+
"value": value,
979+
},
980+
)
981+
982+
return result
983+
984+
def _eap_get_group_list_tag_value(
985+
self,
986+
project_ids: Sequence[int],
987+
group_id_list: Sequence[int],
988+
environment_ids: Sequence[int] | None,
989+
key: str,
990+
value: str,
991+
referrer: str,
992+
occurrence_category: OccurrenceCategory,
993+
) -> dict[int, GroupTagValue]:
994+
organization_id = get_organization_id_from_project_ids(project_ids)
995+
996+
now = datetime.now(tz=timezone.utc)
997+
resolved_start = now - timedelta(days=90)
998+
resolved_end = now
999+
1000+
try:
1001+
organization = Organization.objects.get_from_cache(id=organization_id)
1002+
except Organization.DoesNotExist:
1003+
return {}
1004+
1005+
projects = list(Project.objects.filter(id__in=project_ids, organization_id=organization_id))
1006+
if not projects:
1007+
return {}
1008+
1009+
environments = (
1010+
list(Environment.objects.filter(id__in=environment_ids)) if environment_ids else []
1011+
)
1012+
1013+
group_id_filter = f"group_id:[{','.join(str(gid) for gid in group_id_list)}]"
1014+
tag_filter = build_escaped_term_filter(key, [str(value)])
1015+
query_string = f"{group_id_filter} {tag_filter}"
1016+
1017+
snuba_params = SnubaParams(
1018+
start=resolved_start,
1019+
end=resolved_end,
1020+
organization=organization,
1021+
projects=projects,
1022+
environments=environments,
1023+
)
1024+
1025+
try:
1026+
result = Occurrences.run_table_query(
1027+
params=snuba_params,
1028+
query_string=query_string,
1029+
selected_columns=["group_id", "count()", "min(timestamp)", "last_seen()"],
1030+
orderby=None,
1031+
offset=0,
1032+
limit=len(group_id_list),
1033+
referrer=referrer,
1034+
config=SearchResolverConfig(),
1035+
occurrence_category=occurrence_category,
1036+
)
1037+
1038+
output: dict[int, GroupTagValue] = {}
1039+
for row in result.get("data", []):
1040+
group_id = row.get("group_id")
1041+
if group_id is None:
1042+
continue
1043+
first_seen_raw = row.get("min(timestamp)")
1044+
last_seen_raw = row.get("last_seen()")
1045+
output[int(group_id)] = GroupTagValue(
1046+
group_id=int(group_id),
1047+
key=key,
1048+
value=value,
1049+
times_seen=int(row.get("count()", 0)),
1050+
first_seen=(
1051+
parse_datetime(datetime_processor(first_seen_raw)).replace(
1052+
tzinfo=timezone.utc
1053+
)
1054+
if first_seen_raw is not None
1055+
else None
1056+
),
1057+
last_seen=(
1058+
parse_datetime(datetime_processor(last_seen_raw)).replace(
1059+
tzinfo=timezone.utc
1060+
)
1061+
if last_seen_raw is not None
1062+
else None
1063+
),
1064+
)
1065+
return output
1066+
except Exception:
1067+
logger.exception(
1068+
"EAP get_group_list_tag_value query failed",
1069+
extra={
1070+
"organization_id": organization_id,
1071+
"project_ids": list(project_ids),
1072+
"group_ids": list(group_id_list),
1073+
"key": key,
1074+
"value": value,
1075+
"occurrence_category": occurrence_category.value,
1076+
},
1077+
)
1078+
return {}
8991079

9001080
def apply_group_filters(self, group: Group | None, filters: MutableMapping[str, Sequence[Any]]):
9011081
dataset = Dataset.Events

tests/snuba/tagstore/test_tagstore_backend.py

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1911,3 +1911,153 @@ def test_eap_and_snuba_release_tags_match(self) -> None:
19111911
assert snuba_by_value["1.0"].last_seen == eap_by_value["1.0"].last_seen
19121912
assert snuba_by_value["2.0"].first_seen == eap_by_value["2.0"].first_seen
19131913
assert snuba_by_value["2.0"].last_seen == eap_by_value["2.0"].last_seen
1914+
1915+
@freeze_time(FROZEN_TIME)
1916+
def test_eap_and_snuba_group_list_tag_value_match_multiple_groups(self) -> None:
1917+
ts = (self.FROZEN_TIME - timedelta(minutes=5)).timestamp()
1918+
env = self.create_environment(project=self.project, name="production")
1919+
1920+
# Group A: 2 events in "production"
1921+
self.store_events_to_snuba_and_eap(
1922+
"group-a", count=1, timestamp=ts, extra_event_data={"environment": env.name}
1923+
)
1924+
events_a = self.store_events_to_snuba_and_eap(
1925+
"group-a", count=1, timestamp=ts, extra_event_data={"environment": env.name}
1926+
)
1927+
group_a = events_a[0].group
1928+
assert group_a is not None
1929+
1930+
# Group B: 1 event in "production"
1931+
events_b = self.store_events_to_snuba_and_eap(
1932+
"group-b", count=1, timestamp=ts, extra_event_data={"environment": env.name}
1933+
)
1934+
group_b = events_b[0].group
1935+
assert group_b is not None
1936+
1937+
group_ids = [group_a.id, group_b.id]
1938+
1939+
snuba_result = self.ts.get_group_list_tag_value(
1940+
[self.project.id],
1941+
group_ids,
1942+
[env.id],
1943+
"environment",
1944+
env.name,
1945+
tenant_ids={"referrer": "r", "organization_id": self.project.organization_id},
1946+
)
1947+
eap_result = self.ts._eap_get_group_list_tag_value(
1948+
[self.project.id],
1949+
group_ids,
1950+
[env.id],
1951+
"environment",
1952+
env.name,
1953+
referrer="tagstore.get_group_list_tag_value",
1954+
occurrence_category=OccurrenceCategory.ERROR,
1955+
)
1956+
1957+
assert set(snuba_result.keys()) == {group_a.id, group_b.id}
1958+
assert set(eap_result.keys()) == set(snuba_result.keys())
1959+
assert snuba_result[group_a.id].times_seen == eap_result[group_a.id].times_seen == 2
1960+
assert snuba_result[group_b.id].times_seen == eap_result[group_b.id].times_seen == 1
1961+
assert snuba_result[group_a.id].first_seen == eap_result[group_a.id].first_seen
1962+
assert snuba_result[group_a.id].last_seen == eap_result[group_a.id].last_seen
1963+
assert snuba_result[group_b.id].first_seen == eap_result[group_b.id].first_seen
1964+
assert snuba_result[group_b.id].last_seen == eap_result[group_b.id].last_seen
1965+
1966+
@freeze_time(FROZEN_TIME)
1967+
def test_eap_and_snuba_group_list_tag_value_with_environment_filter(self) -> None:
1968+
ts = (self.FROZEN_TIME - timedelta(minutes=5)).timestamp()
1969+
env_prod = self.create_environment(project=self.project, name="production")
1970+
env_staging = self.create_environment(project=self.project, name="staging")
1971+
1972+
# 2 events in "production", 1 event in "staging"
1973+
self.store_events_to_snuba_and_eap(
1974+
"group-a", count=1, timestamp=ts, extra_event_data={"environment": env_prod.name}
1975+
)
1976+
self.store_events_to_snuba_and_eap(
1977+
"group-a", count=1, timestamp=ts, extra_event_data={"environment": env_prod.name}
1978+
)
1979+
events = self.store_events_to_snuba_and_eap(
1980+
"group-a", count=1, timestamp=ts, extra_event_data={"environment": env_staging.name}
1981+
)
1982+
group_a = events[0].group
1983+
assert group_a is not None
1984+
1985+
# Filter by production only
1986+
snuba_result = self.ts.get_group_list_tag_value(
1987+
[self.project.id],
1988+
[group_a.id],
1989+
[env_prod.id],
1990+
"environment",
1991+
env_prod.name,
1992+
tenant_ids={"referrer": "r", "organization_id": self.project.organization_id},
1993+
)
1994+
eap_result = self.ts._eap_get_group_list_tag_value(
1995+
[self.project.id],
1996+
[group_a.id],
1997+
[env_prod.id],
1998+
"environment",
1999+
env_prod.name,
2000+
referrer="tagstore.get_group_list_tag_value",
2001+
occurrence_category=OccurrenceCategory.ERROR,
2002+
)
2003+
2004+
assert group_a.id in snuba_result
2005+
assert group_a.id in eap_result
2006+
assert snuba_result[group_a.id].times_seen == eap_result[group_a.id].times_seen == 2
2007+
assert snuba_result[group_a.id].first_seen == eap_result[group_a.id].first_seen
2008+
assert snuba_result[group_a.id].last_seen == eap_result[group_a.id].last_seen
2009+
2010+
@freeze_time(FROZEN_TIME)
2011+
def test_eap_group_list_tag_value_issue_platform(self) -> None:
2012+
ts = self.FROZEN_TIME - timedelta(minutes=5)
2013+
env = self.create_environment(project=self.project, name="production")
2014+
2015+
group = self.create_group(project=self.project)
2016+
2017+
eap_item_1 = self.create_eap_occurrence(
2018+
group_id=group.id,
2019+
timestamp=ts,
2020+
environment=env.name,
2021+
issue_occurrence_id=uuid4().hex,
2022+
)
2023+
eap_item_2 = self.create_eap_occurrence(
2024+
group_id=group.id,
2025+
timestamp=ts + timedelta(minutes=1),
2026+
environment=env.name,
2027+
issue_occurrence_id=uuid4().hex,
2028+
)
2029+
self.store_eap_items([eap_item_1, eap_item_2])
2030+
2031+
eap_result = self.ts._eap_get_group_list_tag_value(
2032+
[self.project.id],
2033+
[group.id],
2034+
[env.id],
2035+
"environment",
2036+
env.name,
2037+
referrer="tagstore.get_generic_group_list_tag_value",
2038+
occurrence_category=OccurrenceCategory.ISSUE_PLATFORM,
2039+
)
2040+
2041+
assert group.id in eap_result
2042+
assert eap_result[group.id].times_seen == 2
2043+
assert eap_result[group.id].key == "environment"
2044+
assert eap_result[group.id].value == env.name
2045+
assert eap_result[group.id].first_seen is not None
2046+
assert eap_result[group.id].last_seen is not None
2047+
assert eap_result[group.id].first_seen <= eap_result[group.id].last_seen
2048+
2049+
@freeze_time(FROZEN_TIME)
2050+
def test_eap_group_list_tag_value_empty_result(self) -> None:
2051+
group = self.create_group(project=self.project)
2052+
2053+
eap_result = self.ts._eap_get_group_list_tag_value(
2054+
[self.project.id],
2055+
[group.id],
2056+
None,
2057+
"environment",
2058+
"nonexistent",
2059+
referrer="tagstore.get_group_list_tag_value",
2060+
occurrence_category=OccurrenceCategory.ERROR,
2061+
)
2062+
2063+
assert eap_result == {}

0 commit comments

Comments
 (0)