Skip to content

Commit

Permalink
Add flag backend to tagstore
Browse files Browse the repository at this point in the history
  • Loading branch information
cmanallen committed Jan 22, 2025
1 parent ebcc1f3 commit ab5dd40
Show file tree
Hide file tree
Showing 9 changed files with 160 additions and 120 deletions.
10 changes: 8 additions & 2 deletions src/sentry/api/endpoints/project_tagkey_values.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,14 @@ def get(self, request: Request, project, key) -> Response:
# if the environment doesn't exist then the tag can't possibly exist
raise ResourceDoesNotExist

# Flags also autocomplete. We can switch the dataset we target.
if request.GET.get("useFlagsBackend") == "1":
backend = tagstore.flag_backend
else:
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 +60,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
12 changes: 10 additions & 2 deletions src/sentry/api/endpoints/project_tags.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,16 @@ def get(self, request: Request, project) -> Response:
if request.GET.get("onlySamplingTags") == "1":
kwargs["denylist"] = DS_DENYLIST

# Flags also autocomplete. We switch our backend and also enforce the
# events dataset. Flags are limited to errors.
use_flag_backend = request.GET.get("useFlagsBackend") == "1"
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 +56,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
100 changes: 0 additions & 100 deletions src/sentry/flags/docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -205,103 +205,3 @@ Any request content-type is acceptable (JSON, XML, binary-formats) so long as th
- Request

- Response 201

## Flag Keys [/organizations/<organization_id_or_slug>/flags/keys/]

- Parameters
- end (optional, string) - ISO 8601 format. Required if `start` is set.
- project (number) - The project to search.
- sort (string) - A field to sort by. Optionally prepended with a hyphen to indicate descending order.
- start (optional, string) - ISO 8601 format (`YYYY-MM-DDTHH:mm:ss.sssZ`)
- statsPeriod (optional, string) - A positive integer suffixed with a unit type.
- useCache (number) - Boolean number which determines if we should use the cache or not. 0 for false; 1 for true.
- cursor (optional, string)
- per_page (optional, number)
Default: 10
- offset (optional, number)
Default: 0

**Attributes**

| Column | Type | Description |
| ----------- | ------ | ----------- |
| key | string | |
| name | string | |
| totalValues | number | |

### Browse Flag Keys [GET]

Retrieve a collection of flag keys.

- Response 200

```json
[
{
"key": "sdk_name",
"name": "Sdk Name",
"totalValues": 2444
}
]
```

## Flag Key [/organizations/<organization_id_or_slug>/flags/keys/<key>/]

### Fetch Flag Key [GET]

Fetch a flag key.

- Response 200

```json
{
"key": "sdk_name",
"name": "Sdk Name",
"totalValues": 2444
}
```

## Flag Values [/organizations/<organization_id_or_slug>/flags/keys/<key>/values/]

- Parameters
- end (optional, string) - ISO 8601 format. Required if `start` is set.
- project (number) - The project to search.
- sort (string) - A field to sort by. Optionally prepended with a hyphen to indicate descending order.
- start (optional, string) - ISO 8601 format (`YYYY-MM-DDTHH:mm:ss.sssZ`)
- statsPeriod (optional, string) - A positive integer suffixed with a unit type.
- useCache (number) - Boolean number which determines if we should use the cache or not. 0 for false; 1 for true.
- cursor (optional, string)
- per_page (optional, number)
Default: 10
- offset (optional, number)
Default: 0

**Attributes**

| Column | Type | Description |
| --------- | ------ | ----------- |
| key | string | |
| name | string | |
| value | string | |
| count | number | |
| lastSeen | string | |
| firstSeen | string | |

### Browse Flag Values [GET]

Retrieve a collection of flag values.

- Response 200

```json
[
{
"key": "isCustomerDomain",
"name": "yes",
"value": "yes",
"count": 15525,
"lastSeen": "2025-01-22T15:59:13Z",
"firstSeen": "2025-01-21T15:59:02Z"
}
]
```
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):
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[{}]"


# 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}]"
36 changes: 36 additions & 0 deletions tests/sentry/api/endpoints/test_project_tagkey_values.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,3 +137,39 @@ def test_start_end_query(self):
assert response.status_code == 200
assert len(response.data) == 1
assert response.data[0]["value"] == "bar"

def test_simple_flags(self):
project = self.create_project()
self.store_event(
data={
"contexts": {"flags": {"values": [{"flag": "abc", "result": True}]}},
"timestamp": before_now(seconds=1).isoformat(),
},
project_id=project.id,
)
self.store_event(
data={
"contexts": {"flags": {"values": [{"flag": "abc", "result": False}]}},
"timestamp": before_now(seconds=1).isoformat(),
},
project_id=project.id,
)

self.login_as(user=self.user)

url = reverse(
"sentry-api-0-project-tagkey-values",
kwargs={
"organization_id_or_slug": project.organization.slug,
"project_id_or_slug": project.slug,
"key": "abc",
},
)

response = self.client.get(url + "?useFlagsBackend=1")
assert response.status_code == 200
assert len(response.data) == 2

results = sorted(response.data, key=lambda i: i["value"])
assert results[0]["value"] == "false"
assert results[1]["value"] == "true"
Loading

0 comments on commit ab5dd40

Please sign in to comment.