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

feat(flags): Update issues search box to search for feature flags #83639

Open
wants to merge 16 commits into
base: master
Choose a base branch
from
Open
14 changes: 12 additions & 2 deletions src/sentry/api/endpoints/organization_tagkey_values.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from rest_framework.request import Request
from rest_framework.response import Response

from sentry import tagstore
from sentry import features, tagstore
from sentry.api.api_publish_status import ApiPublishStatus
from sentry.api.base import region_silo_endpoint
from sentry.api.bases import NoProjects, OrganizationEventsEndpointBase
Expand Down Expand Up @@ -54,7 +54,17 @@ def get(self, request: Request, organization, key) -> Response:
paginator: SequencePaginator[TagValue] = SequencePaginator([])
else:
with handle_query_errors():
paginator = tagstore.backend.get_tag_value_paginator_for_projects(
# Flags are stored on the same table as tags but on a different column. Ideally
# both could be queried in a single request. But at present we're not sure if we
# want to treat tags and flags as the same or different and in which context.
if request.GET.get("useFlagsBackend") == "1" and features.has(
"organizations:feature-flag-autocomplete", organization, actor=request.user
):
backend = tagstore.flag_backend
else:
backend = tagstore.backend

paginator = backend.get_tag_value_paginator_for_projects(
snuba_params.project_ids,
snuba_params.environment_ids,
key,
Expand Down
13 changes: 12 additions & 1 deletion src/sentry/api/endpoints/organization_tags.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,18 @@ def get(self, request: Request, organization) -> Response:
),
)

results = tagstore.backend.get_tag_keys_for_projects(
# Flags are stored on the same table as tags but on a different column. Ideally
# both could be queried in a single request. But at present we're not sure if we
# want to treat tags and flags as the same or different and in which context.
use_flag_backend = request.GET.get("useFlagsBackend") == "1" and features.has(
"organizations:feature-flag-autocomplete", organization, actor=request.user
)
if use_flag_backend:
backend = tagstore.flag_backend
else:
backend = tagstore.backend

results = backend.get_tag_keys_for_projects(
filter_params["project_id"],
filter_params.get("environment"),
start,
Expand Down
16 changes: 13 additions & 3 deletions src/sentry/api/endpoints/project_tagkey_values.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from rest_framework.request import Request
from rest_framework.response import Response

from sentry import tagstore
from sentry import features, tagstore
from sentry.api.api_owners import ApiOwner
from sentry.api.api_publish_status import ApiPublishStatus
from sentry.api.base import EnvironmentMixin, region_silo_endpoint
Expand Down Expand Up @@ -42,8 +42,18 @@ def get(self, request: Request, project, key) -> Response:
# if the environment doesn't exist then the tag can't possibly exist
raise ResourceDoesNotExist

# Flags are stored on the same table as tags but on a different column. Ideally both
# could be queried in a single request. But at present we're not sure if we want to
# treat tags and flags as the same or different and in which context.
if request.GET.get("useFlagsBackend") == "1" and features.has(
"organizations:feature-flag-autocomplete", project.organization, actor=request.user
):
backend = tagstore.flag_backend
else:
cmanallen marked this conversation as resolved.
Show resolved Hide resolved
backend = tagstore.backend

try:
tagkey = tagstore.backend.get_tag_key(
tagkey = backend.get_tag_key(
project.id,
environment_id,
lookup_key,
Expand All @@ -54,7 +64,7 @@ def get(self, request: Request, project, key) -> Response:

start, end = get_date_range_from_params(request.GET)

paginator = tagstore.backend.get_tag_value_paginator(
paginator = backend.get_tag_value_paginator(
project.id,
environment_id,
tagkey.key,
Expand Down
17 changes: 14 additions & 3 deletions src/sentry/api/endpoints/project_tags.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from rest_framework.request import Request
from rest_framework.response import Response

from sentry import tagstore
from sentry import features, tagstore
from sentry.api.api_owners import ApiOwner
from sentry.api.api_publish_status import ApiPublishStatus
from sentry.api.base import EnvironmentMixin, region_silo_endpoint
Expand All @@ -27,8 +27,19 @@ def get(self, request: Request, project) -> Response:
if request.GET.get("onlySamplingTags") == "1":
kwargs["denylist"] = DS_DENYLIST

# Flags are stored on the same table as tags but on a different column. Ideally both
# could be queried in a single request. But at present we're not sure if we want to
# treat tags and flags as the same or different and in which context.
use_flag_backend = request.GET.get("useFlagsBackend") == "1" and features.has(
"organizations:feature-flag-autocomplete", project.organization, actor=request.user
)
if use_flag_backend:
backend = tagstore.flag_backend
else:
backend = tagstore.backend

tag_keys = sorted(
tagstore.backend.get_tag_keys(
backend.get_tag_keys(
project.id,
environment_id,
tenant_ids={"organization_id": project.organization_id},
Expand All @@ -48,7 +59,7 @@ def get(self, request: Request, project) -> Response:
"key": tagstore.backend.get_standardized_key(tag_key.key),
"name": tagstore.backend.get_tag_key_label(tag_key.key),
"uniqueValues": tag_key.values_seen,
"canDelete": tag_key.key not in PROTECTED_TAG_KEYS,
"canDelete": tag_key.key not in PROTECTED_TAG_KEYS and not use_flag_backend,
}
)

Expand Down
4 changes: 4 additions & 0 deletions src/sentry/conf/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -1764,6 +1764,10 @@ def custom_parameter_sort(parameter: dict) -> tuple[str, int]:
SENTRY_TAGSTORE = os.environ.get("SENTRY_TAGSTORE", "sentry.tagstore.snuba.SnubaTagStorage")
SENTRY_TAGSTORE_OPTIONS: dict[str, Any] = {}

# Flag storage backend
SENTRY_FLAGSTORE = os.environ.get("SENTRY_FLAGSTORE", "sentry.tagstore.snuba.SnubaFlagStorage")
SENTRY_FLAGSTORE_OPTIONS: dict[str, Any] = {}

# Search backend
SENTRY_SEARCH = os.environ.get(
"SENTRY_SEARCH", "sentry.search.snuba.EventsDatasetSnubaSearchBackend"
Expand Down
8 changes: 8 additions & 0 deletions src/sentry/features/temporary.py
Original file line number Diff line number Diff line change
Expand Up @@ -628,3 +628,11 @@ def register_temporary_features(manager: FeatureManager):
FeatureHandlerStrategy.FLAGPOLE,
api_expose=True,
)

# Feature Flags Features.
Copy link
Member

@michellewzhang michellewzhang Jan 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do you mind moving the FF flags above this to under this section as well? (e.g. i see feature-flag-cta, feature-flag-audit-log, feature-flag-ui)

manager.add(
"organizations:feature-flag-autocomplete",
cmanallen marked this conversation as resolved.
Show resolved Hide resolved
OrganizationFeature,
FeatureHandlerStrategy.FLAGPOLE,
api_expose=True,
)
42 changes: 42 additions & 0 deletions src/sentry/search/snuba/executors.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
OrderBy,
Request,
)
from snuba_sdk.conditions import Or
from snuba_sdk.expressions import Expression
from snuba_sdk.query import Query
from snuba_sdk.relationships import Relationship
Expand Down Expand Up @@ -1810,6 +1811,16 @@ def query(
),
)
if condition is not None:
if features.has(
"organizations:feature-flag-autocomplete", organization
) and has_tags_filter(condition.lhs):
feature_condition = Condition(
lhs=substitute_tags_filter(condition.lhs),
op=condition.op,
rhs=condition.rhs,
)
condition = Or(conditions=[condition, feature_condition])

where_conditions.append(condition)

# handle types based on issue.type and issue.category
Expand Down Expand Up @@ -1940,3 +1951,34 @@ def query(
paginator_results.results = [groups[k] for k in paginator_results.results if k in groups]
# TODO: Add types to paginators and remove this
return cast(CursorResult[Group], paginator_results)


# This should update the search box so we can fetch the correct
# issues.
def has_tags_filter(condition: Column | Function) -> bool:
if isinstance(condition, Column):
if (
condition.entity.name == "events"
and condition.name.startswith("tags[")
and condition.name.endswith("]")
):
return True
elif isinstance(condition, Function):
for parameter in condition.parameters:
if isinstance(parameter, (Column, Function)):
return has_tags_filter(parameter)
return False


def substitute_tags_filter(condition: Column | Function) -> bool:
if isinstance(condition, Column):
if condition.name.startswith("tags[") and condition.name.endswith("]"):
return Column(
name=f"flags[{condition.name[5:-1]}]",
entity=condition.entity,
)
elif isinstance(condition, Function):
for parameter in condition.parameters:
if isinstance(parameter, (Column, Function)):
return substitute_tags_filter(parameter)
return condition
6 changes: 6 additions & 0 deletions src/sentry/tagstore/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,9 @@

backend = LazyServiceWrapper(TagStorage, settings.SENTRY_TAGSTORE, settings.SENTRY_TAGSTORE_OPTIONS)
backend.expose(locals())

# Searches the "flags" columns instead of "tags".
flag_backend = LazyServiceWrapper(
TagStorage, settings.SENTRY_FLAGSTORE, settings.SENTRY_FLAGSTORE_OPTIONS
)
flag_backend.expose(locals())
2 changes: 1 addition & 1 deletion src/sentry/tagstore/snuba/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
from .backend import SnubaTagStorage # NOQA
from .backend import SnubaFlagStorage, SnubaTagStorage # NOQA
69 changes: 54 additions & 15 deletions src/sentry/tagstore/snuba/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ class _OptimizeKwargs(TypedDict, total=False):
sample: int


class SnubaTagStorage(TagStorage):
class _SnubaTagStorage(TagStorage):
def __get_tag_key_and_top_values(
self,
project_id,
Expand All @@ -128,7 +128,7 @@ def __get_tag_key_and_top_values(
tenant_ids=None,
**kwargs,
):
tag = f"tags[{key}]"
tag = self.format_string.format(key)
filters = {"project_id": get_project_list(project_id)}
if environment_id:
filters["environment"] = [environment_id]
Expand Down Expand Up @@ -260,10 +260,10 @@ def __get_tag_keys_for_projects(
dataset, filters = self.apply_group_filters(group, filters)

if keys is not None:
filters["tags_key"] = sorted(keys)
filters[self.key_column] = sorted(keys)

if include_values_seen:
aggregations.append(["uniq", "tags_value", "values_seen"])
aggregations.append(["uniq", self.value_column, "values_seen"])

should_cache = use_cache and group is None
result = None
Expand Down Expand Up @@ -303,7 +303,7 @@ def __get_tag_keys_for_projects(
dataset=dataset,
start=start,
end=end,
groupby=["tags_key"],
groupby=[self.key_column],
conditions=[],
filter_keys=filters,
aggregations=aggregations,
Expand Down Expand Up @@ -480,7 +480,9 @@ def __get_group_list_tag_value(
filters = {"project_id": project_ids, "group_id": group_id_list}
if environment_ids:
filters["environment"] = environment_ids
conditions = (extra_conditions if extra_conditions else []) + [[f"tags[{key}]", "=", value]]
conditions = (extra_conditions if extra_conditions else []) + [
[self.format_string.format(key), "=", value]
]
aggregations = (extra_aggregations if extra_aggregations else []) + [
["count()", "", "times_seen"],
["min", SEEN_COLUMN, "first_seen"],
Expand Down Expand Up @@ -537,7 +539,7 @@ def get_generic_group_list_tag_value(
Condition(Column("group_id"), Op.IN, group_id_list),
Condition(Column("timestamp"), Op.LT, end),
Condition(Column("timestamp"), Op.GTE, start),
Condition(Column(f"tags[{key}]"), Op.EQ, value),
Condition(Column(self.format_string.format(key)), Op.EQ, value),
]
if translated_params.get("environment"):
Condition(Column("environment"), Op.IN, translated_params["environment"]),
Expand Down Expand Up @@ -582,7 +584,7 @@ def apply_group_filters(self, group: Group | None, filters):
return dataset, filters

def get_group_tag_value_count(self, group, environment_id, key, tenant_ids=None):
tag = f"tags[{key}]"
tag = self.format_string.format(key)
filters = {"project_id": get_project_list(group.project_id)}
if environment_id:
filters["environment"] = [environment_id]
Expand Down Expand Up @@ -633,7 +635,7 @@ def get_group_tag_keys_and_top_values(
if environment_ids:
filters["environment"] = environment_ids
if keys is not None:
filters["tags_key"] = keys
filters[self.key_column] = keys
dataset, filters = self.apply_group_filters(group, filters)
aggregations = kwargs.get("aggregations", [])
aggregations += [
Expand All @@ -646,12 +648,12 @@ def get_group_tag_keys_and_top_values(
dataset=dataset,
start=kwargs.get("start"),
end=kwargs.get("end"),
groupby=["tags_key", "tags_value"],
groupby=[self.key_column, self.value_column],
conditions=conditions,
filter_keys=filters,
aggregations=aggregations,
orderby="-count",
limitby=[value_limit, "tags_key"],
limitby=[value_limit, self.key_column],
referrer="tagstore._get_tag_keys_and_top_values",
tenant_ids=tenant_ids,
)
Expand Down Expand Up @@ -1047,6 +1049,9 @@ def _get_tag_values_for_releases_across_all_datasets(self, projects, environment
[(i, TagValue(RELEASE_ALIAS, v, None, None, None)) for i, v in enumerate(versions)]
)

def get_snuba_column_name(self, key: str, dataset: Dataset):
aliu39 marked this conversation as resolved.
Show resolved Hide resolved
return snuba.get_snuba_column_name(key, dataset=dataset)

def get_tag_value_paginator_for_projects(
self,
projects,
Expand Down Expand Up @@ -1077,7 +1082,7 @@ def get_tag_value_paginator_for_projects(
if include_replays:
dataset = Dataset.Replays

snuba_key = snuba.get_snuba_column_name(key, dataset=dataset)
snuba_key = self.get_snuba_column_name(key, dataset=dataset)

# We cannot search the values of these columns like we do other columns because they are
# a different type, and as such, LIKE and != do not work on them. Furthermore, because the
Expand Down Expand Up @@ -1192,7 +1197,7 @@ def get_tag_value_paginator_for_projects(
snuba_name = FIELD_ALIASES[USER_DISPLAY_ALIAS].get_field()
snuba.resolve_complex_column(snuba_name, resolver, [])
elif snuba_name in BLACKLISTED_COLUMNS:
snuba_name = f"tags[{key}]"
snuba_name = self.format_string.format(key)

if query:
query = query.replace("\\", "\\\\")
Expand Down Expand Up @@ -1299,15 +1304,15 @@ def get_group_tag_value_iter(
) -> list[GroupTagValue]:
filters = {
"project_id": get_project_list(group.project_id),
"tags_key": [key],
self.key_column: [key],
}
dataset, filters = self.apply_group_filters(group, filters)

if environment_ids:
filters["environment"] = environment_ids
results = snuba.query(
dataset=dataset,
groupby=["tags_value"],
groupby=[self.value_column],
filter_keys=filters,
conditions=[],
aggregations=[
Expand Down Expand Up @@ -1358,3 +1363,37 @@ def get_group_tag_value_paginator(
[(int(getattr(gtv, score_field).timestamp() * 1000), gtv) for gtv in group_tag_values],
reverse=desc,
)


class SnubaTagStorage(_SnubaTagStorage):
key_column = "tags_key"
value_column = "tags_value"
format_string = "tags[{}]"
cmanallen marked this conversation as resolved.
Show resolved Hide resolved


# Quick and dirty overload to support flag aggregations. This probably deserves
# a better refactor for now we're just raising within the functions we don't want
# to support. This sort of refactor couples flags behavior to a lot of tags
# specific behavior. The interfaces are compatible and flags are basically tags
# just with a different column to live on.


class SnubaFlagStorage(_SnubaTagStorage):
key_column = "flags_key"
value_column = "flags_value"
format_string = "flags[{}]"

def get_release_tags(self, *args, **kwargs):
raise NotImplementedError

def __get_groups_user_counts(self, *args, **kwargs):
raise NotImplementedError

def get_groups_user_counts(self, *args, **kwargs):
raise NotImplementedError

def get_generic_groups_user_counts(self, *args, **kwargs):
raise NotImplementedError

def get_snuba_column_name(self, key: str, dataset: Dataset):
return f"flags[{key}]"
Loading
Loading