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(inbound-filters): Move filters to generic filters #73840

Merged
merged 16 commits into from
Jul 9, 2024
121 changes: 121 additions & 0 deletions src/sentry/ingest/inbound_filters.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
from collections.abc import Callable, Sequence
from typing import cast

from rest_framework import serializers

from sentry.models.options.project_option import ProjectOption
from sentry.models.project import Project
from sentry.relay.types import GenericFilter, GenericFiltersConfig, RuleCondition
from sentry.relay.utils import to_camel_case_name
from sentry.signals import inbound_filter_toggled
from sentry.tsdb.base import TSDBModel

GENERIC_FILTERS_VERSION = 1


class FilterStatKeys:
"""
Expand Down Expand Up @@ -290,3 +297,117 @@ class _LegacyBrowserFilterSerializer(_FilterSerializer):
serializer_cls=None,
config_name="ignoreTransactions",
)


def _error_message_condition(values: Sequence[tuple[str | None, str | None]]) -> RuleCondition:
"""
Condition that expresses error message matching for an inbound filter.
"""
conditions = []

for ty, value in values:
ty_and_value: list[RuleCondition] = []

if ty is not None:
ty_and_value.append({"op": "glob", "name": "ty", "value": [ty]})
if value is not None:
ty_and_value.append({"op": "glob", "name": "value", "value": [value]})

if len(ty_and_value) == 1:
conditions.append(ty_and_value[0])
elif len(ty_and_value) == 2:
conditions.append(
{
"op": "and",
"inner": ty_and_value,
}
)

return cast(
RuleCondition,
{
"op": "any",
"name": "event.exceptions",
"inner": {
"op": "or",
"inner": conditions,
},
},
)


def _chunk_load_error_filter() -> RuleCondition:
"""
Filters out chunk load errors.

Example:
ChunkLoadError: Loading chunk 3662 failed.\n(error:
https://domain.com/_next/static/chunks/29107295-0151559bd23117ba.js)
"""
values = [
("ChunkLoadError", "Loading chunk *"),
("*Uncaught *", "ChunkLoadError: Loading chunk *"),
]

return _error_message_condition(values)


def _hydration_error_filter() -> RuleCondition:
"""
Filters out hydration errors.

Example:
418 - Hydration failed because the initial UI does not match what was rendered on the server.
419 - The server could not finish this Suspense boundary, likely due to an error during server rendering.
Switched to client rendering.
422 - There was an error while hydrating this Suspense boundary. Switched to client rendering.
423 - There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire
root will switch to client rendering.
425 - Text content does not match server-rendered HTML.
"""
values = [
(None, "*https://reactjs.org/docs/error-decoder.html?invariant={418,419,422,423,425}*"),
(None, "*https://react.dev/errors/{418,419,422,423,425}*"),
]

return _error_message_condition(values)


# List of all active generic filters that Sentry currently sends to Relay.
ACTIVE_GENERIC_FILTERS: Sequence[tuple[str, Callable[[], RuleCondition]]] = [
("chunk-load-error", _chunk_load_error_filter),
("react-hydration-errors", _hydration_error_filter),
]


def get_generic_filters(project: Project) -> GenericFiltersConfig | None:
"""
Computes the generic inbound filters configuration for inbound filters.

Generic inbound filters are able to express arbitrary filtering conditions on an event, using
Relay's `RuleCondition` DSL. They differ from static inbound filters which filter events based on a
hardcoded set of rules, specific to each type.
"""
generic_filters: list[GenericFilter] = []

for generic_filter_id, generic_filter_fn in ACTIVE_GENERIC_FILTERS:
if project.get_option(f"filters:{generic_filter_id}") not in ("1", True):
continue

condition = generic_filter_fn()
if condition is not None:
generic_filters.append(
{
"id": generic_filter_id,
"isEnabled": True,
"condition": condition,
}
)

if not generic_filters:
return None

return {
"version": GENERIC_FILTERS_VERSION,
"filters": generic_filters,
}
3 changes: 3 additions & 0 deletions src/sentry/options/defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -1103,6 +1103,9 @@
"relay.compute-metrics-summaries.sample-rate", default=0.0, flags=FLAG_AUTOMATOR_MODIFIABLE
)

# Controls whether generic inbound filters are sent to Relay.
register("relay.emit-generic-inbound-filters", default=False, flags=FLAG_AUTOMATOR_MODIFIABLE)

# Write new kafka headers in eventstream
register("eventstream:kafka-headers", default=True, flags=FLAG_AUTOMATOR_MODIFIABLE)

Expand Down
44 changes: 16 additions & 28 deletions src/sentry/relay/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
_FilterSpec,
get_all_filter_specs,
get_filter_key,
get_generic_filters,
)
from sentry.ingest.transaction_clusterer import ClustererNamespace
from sentry.ingest.transaction_clusterer.meta import get_clusterer_meta
Expand All @@ -36,7 +37,6 @@
get_metric_conditional_tagging_rules,
get_metric_extraction_config,
)
from sentry.relay.types import RuleCondition
from sentry.relay.utils import to_camel_case_name
from sentry.sentry_metrics.use_case_id_registry import CARDINALITY_LIMIT_USE_CASES
from sentry.sentry_metrics.visibility import get_metrics_blocking_state_for_relay_config
Expand Down Expand Up @@ -142,6 +142,9 @@ def get_filter_settings(project: Project) -> Mapping[str, Any]:

error_messages += project.get_option(f"sentry:{FilterTypes.ERROR_MESSAGES}") or []

# TODO: remove both error message filters when the generic filters implementation is proved to be on par when it
# comes to filtering capabilities. When both generic and non-generic filters are applied, the generic ones take
# precedence.
# This option was defaulted to string but was changed at runtime to a boolean due to an error in the
# implementation. In order to bring it back to a string, we need to repair on read stored options. This is
# why the value true is determined by either "1" or True.
Expand Down Expand Up @@ -179,37 +182,22 @@ def get_filter_settings(project: Project) -> Mapping[str, Any]:
if csp_disallowed_sources:
filter_settings["csp"] = {"disallowedSources": csp_disallowed_sources}

try:
generic_filters = _get_generic_project_filters()
except Exception:
logger.exception(
"Exception while building Relay project config: error building generic filters"
)
else:
if generic_filters and len(generic_filters["filters"]) > 0:
filter_settings["generic"] = generic_filters
if options.get("relay.emit-generic-inbound-filters"):
try:
# At the end we compute the generic inbound filters, which are inbound filters expressible with a
# conditional DSL that Relay understands.
generic_filters = get_generic_filters(project)
if generic_filters is not None:
filter_settings["generic"] = generic_filters
except Exception as e:
sentry_sdk.capture_exception(e)
logger.exception(
"Exception while building Relay project config: error building generic filters"
)

return filter_settings


class GenericFilter(TypedDict):
id: str
isEnabled: bool
condition: RuleCondition


class GenericFiltersConfig(TypedDict):
version: int
filters: Sequence[GenericFilter]


def _get_generic_project_filters() -> GenericFiltersConfig:
return {
"version": 1,
"filters": [],
}


def get_quotas(project: Project, keys: Iterable[ProjectKey] | None = None) -> list[str]:
try:
computed_quotas = [
Expand Down
2 changes: 1 addition & 1 deletion src/sentry/relay/globalconfig.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
from typing import Any, TypedDict

import sentry.options
from sentry.relay.config import GenericFiltersConfig
from sentry.relay.config.ai_model_costs import AIModelCosts, ai_model_costs_config
from sentry.relay.config.measurements import MeasurementsConfig, get_measurements_config
from sentry.relay.config.metric_extraction import (
MetricExtractionGroups,
global_metric_extraction_groups,
)
from sentry.relay.types import GenericFiltersConfig
from sentry.utils import metrics

# List of options to include in the global config.
Expand Down
5 changes: 3 additions & 2 deletions src/sentry/relay/types/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from sentry.relay.types.rule_condition import RuleCondition
from .generic_filters import GenericFilter, GenericFiltersConfig
from .rule_condition import RuleCondition

__all__ = ["RuleCondition"]
__all__ = ["GenericFilter", "GenericFiltersConfig", "RuleCondition"]
19 changes: 19 additions & 0 deletions src/sentry/relay/types/generic_filters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from collections.abc import Sequence
from typing import TypedDict

from .rule_condition import RuleCondition


class GenericFilter(TypedDict):
"""Configuration for a generic filter that filters incoming events."""

id: str
isEnabled: bool
condition: RuleCondition


class GenericFiltersConfig(TypedDict):
"""Top-level configuration for generic filters."""

version: int
filters: Sequence[GenericFilter]
25 changes: 4 additions & 21 deletions tests/sentry/relay/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -1266,28 +1266,11 @@ def test_project_config_cardinality_limits_organization_options_override_options
]


@patch(
"sentry.relay.config._get_generic_project_filters",
lambda *args, **kwargs: {
"version": 1,
"filters": [
{
"id": "test-id",
"isEnabled": True,
"condition": {
"op": "not",
"inner": {
"op": "eq",
"name": "event.contexts.browser.name",
"value": "Firefox",
},
},
}
],
},
)
@django_db_all
@region_silo_test
def test_project_config_valid_with_generic_filters(default_project):
@override_options({"relay.emit-generic-inbound-filters": True})
def test_project_config_with_generic_filters(default_project):
config = get_project_config(default_project).to_dict()
_validate_project_config(config["config"])

assert config["config"]["filterSettings"]["generic"]["filters"]
Loading