diff --git a/src/sentry/models/options/project_option.py b/src/sentry/models/options/project_option.py index 47bb0a9aef6995..1caab683bcf077 100644 --- a/src/sentry/models/options/project_option.py +++ b/src/sentry/models/options/project_option.py @@ -88,7 +88,6 @@ "mail:subject_template", "filters:react-hydration-errors", "filters:chunk-load-error", - "relay.cardinality-limiter.limits", ] ) diff --git a/src/sentry/relay/config/__init__.py b/src/sentry/relay/config/__init__.py index 49c3ed63ba77cd..c61b1dc4f34e9a 100644 --- a/src/sentry/relay/config/__init__.py +++ b/src/sentry/relay/config/__init__.py @@ -4,7 +4,7 @@ import uuid from collections.abc import Iterable, Mapping, MutableMapping, Sequence from datetime import datetime, timezone -from typing import Any, Literal, NotRequired, TypedDict +from typing import Any, Literal, TypedDict import sentry_sdk from sentry_sdk import capture_exception @@ -46,7 +46,6 @@ from sentry.relay.datascrubbing import get_datascrubbing_settings, get_pii_config from sentry.relay.types.generic_filters import GenericFilter from sentry.relay.utils import to_camel_case_name -from sentry.sentry_metrics.use_case_id_registry import CARDINALITY_LIMIT_USE_CASES from sentry.utils import metrics from sentry.utils.http import get_origins from sentry.utils.options import sample_modulo @@ -218,103 +217,6 @@ def get_quotas(project: Project, keys: Iterable[ProjectKey] | None = None) -> li return computed_quotas -class SlidingWindow(TypedDict): - windowSeconds: int - granularitySeconds: int - - -class CardinalityLimit(TypedDict): - id: str - passive: NotRequired[bool] - window: SlidingWindow - limit: int - scope: Literal["organization", "project"] - namespace: str | None - - -class CardinalityLimitOption(TypedDict): - rollout_rate: NotRequired[float] - limit: CardinalityLimit - projects: NotRequired[list[int]] - - -def get_metrics_config(timeout: TimeChecker, project: Project) -> Mapping[str, Any] | None: - metrics_config = {} - - if cardinality_limits := get_cardinality_limits(timeout, project): - metrics_config["cardinalityLimits"] = cardinality_limits - - return metrics_config or None - - -def get_cardinality_limits(timeout: TimeChecker, project: Project) -> list[CardinalityLimit] | None: - if options.get("relay.cardinality-limiter.mode") == "disabled": - return None - - passive_limits = options.get("relay.cardinality-limiter.passive-limits-by-org").get( - str(project.organization.id), [] - ) - - existing_ids: set[str] = set() - cardinality_limits: list[CardinalityLimit] = [] - for namespace in CARDINALITY_LIMIT_USE_CASES: - timeout.check() - option = options.get(f"sentry-metrics.cardinality-limiter.limits.{namespace.value}.per-org") - if not option or not len(option) == 1: - # Multiple quotas are not supported - continue - - quota = option[0] - id = namespace.value - - limit: CardinalityLimit = { - "id": id, - "window": { - "windowSeconds": quota["window_seconds"], - "granularitySeconds": quota["granularity_seconds"], - }, - "limit": quota["limit"], - "scope": "organization", - "namespace": namespace.value, - } - if id in passive_limits: - limit["passive"] = True - cardinality_limits.append(limit) - existing_ids.add(id) - - project_limit_options: list[CardinalityLimitOption] = project.get_option( - "relay.cardinality-limiter.limits", [] - ) - organization_limit_options: list[CardinalityLimitOption] = project.organization.get_option( - "relay.cardinality-limiter.limits", [] - ) - option_limit_options: list[CardinalityLimitOption] = options.get( - "relay.cardinality-limiter.limits" - ) - - for clo in project_limit_options + organization_limit_options + option_limit_options: - rollout_rate = clo.get("rollout_rate", 1.0) - if (project.organization.id % 100000) / 100000 >= rollout_rate: - continue - - projects = clo.get("projects") - if projects is not None and project.id not in projects: - # projects list is defined but the current project is not in the list - continue - - try: - limit = clo["limit"] - if clo["limit"]["id"] in existing_ids: - # skip if a limit with the same id already exists - continue - cardinality_limits.append(limit) - existing_ids.add(clo["limit"]["id"]) - except KeyError: - pass - - return cardinality_limits - - def get_project_config( project: Project, project_keys: Iterable[ProjectKey] | None = None ) -> ProjectConfig: @@ -1046,8 +948,6 @@ def _get_project_config( config["breakdownsV2"] = project.get_option("sentry:breakdowns") - add_experimental_config(config, "metrics", get_metrics_config, project) - if _should_extract_transaction_metrics(project): add_experimental_config( config, diff --git a/tests/sentry/core/endpoints/test_project_details.py b/tests/sentry/core/endpoints/test_project_details.py index c38dac58c9f816..4512dfe63872ad 100644 --- a/tests/sentry/core/endpoints/test_project_details.py +++ b/tests/sentry/core/endpoints/test_project_details.py @@ -795,21 +795,6 @@ def test_options(self) -> None: assert project.get_option("filters:react-hydration-errors", "1") assert project.get_option("filters:chunk-load-error", "1") - self.project.update_option( - "relay.cardinality-limiter.limits", - [ - { - "limit": { - "id": "project-override-custom", - "window": {"windowSeconds": 3600, "granularitySeconds": 600}, - "limit": 1000, - "namespace": "custom", - "scope": "name", - } - } - ], - ) - def test_preprod_snapshot_pr_comments_option(self) -> None: self.get_success_response( self.org_slug, self.proj_slug, preprodSnapshotPrCommentsEnabled=False diff --git a/tests/sentry/relay/snapshots/test_config/test_project_config_cardinality_limits/False/REGION.pysnap b/tests/sentry/relay/snapshots/test_config/test_project_config_cardinality_limits/False/REGION.pysnap deleted file mode 100644 index c9a8bc377526d4..00000000000000 --- a/tests/sentry/relay/snapshots/test_config/test_project_config_cardinality_limits/False/REGION.pysnap +++ /dev/null @@ -1,46 +0,0 @@ ---- -created: '2025-02-12T13:41:13.692751+00:00' -creator: sentry -source: tests/sentry/relay/test_config.py ---- -cardinalityLimits: -- id: transactions - limit: 10 - namespace: transactions - scope: organization - window: - granularitySeconds: 100 - windowSeconds: 1000 -- id: sessions - limit: 20 - namespace: sessions - scope: organization - window: - granularitySeconds: 200 - windowSeconds: 2000 -- id: spans - limit: 30 - namespace: spans - scope: organization - window: - granularitySeconds: 300 - windowSeconds: 3000 -- id: test3 - limit: 90 - scope: name - window: - granularitySeconds: 900 - windowSeconds: 9000 -- id: test4 - limit: 100 - scope: name - window: - granularitySeconds: 1000 - windowSeconds: 10000 -- id: test2 - limit: 80 - report: true - scope: name - window: - granularitySeconds: 800 - windowSeconds: 8000 diff --git a/tests/sentry/relay/snapshots/test_config/test_project_config_cardinality_limits/False/REGION.pysnap.new b/tests/sentry/relay/snapshots/test_config/test_project_config_cardinality_limits/False/REGION.pysnap.new deleted file mode 100644 index ee01fd07609807..00000000000000 --- a/tests/sentry/relay/snapshots/test_config/test_project_config_cardinality_limits/False/REGION.pysnap.new +++ /dev/null @@ -1,46 +0,0 @@ ---- -created: '2025-02-12T13:05:37.025213+00:00' -creator: sentry -source: tests/sentry/relay/test_config.py ---- -cardinalityLimits: -- id: transactions - limit: 10 - namespace: transactions - scope: organization - window: - granularitySeconds: 100 - windowSeconds: 1000 -- id: sessions - limit: 20 - namespace: sessions - scope: organization - window: - granularitySeconds: 200 - windowSeconds: 2000 -- id: spans - limit: 30 - namespace: spans - scope: organization - window: - granularitySeconds: 300 - windowSeconds: 3000 -- id: test3 - limit: 90 - scope: name - window: - granularitySeconds: 900 - windowSeconds: 9000 -- id: test4 - limit: 100 - scope: name - window: - granularitySeconds: 1000 - windowSeconds: 10000 -- id: test2 - limit: 80 - report: true - scope: name - window: - granularitySeconds: 800 - windowSeconds: 8000 diff --git a/tests/sentry/relay/snapshots/test_config/test_project_config_cardinality_limits/True/REGION.pysnap b/tests/sentry/relay/snapshots/test_config/test_project_config_cardinality_limits/True/REGION.pysnap deleted file mode 100644 index 05776f3f22c7aa..00000000000000 --- a/tests/sentry/relay/snapshots/test_config/test_project_config_cardinality_limits/True/REGION.pysnap +++ /dev/null @@ -1,49 +0,0 @@ ---- -created: '2025-02-12T13:41:13.874417+00:00' -creator: sentry -source: tests/sentry/relay/test_config.py ---- -cardinalityLimits: -- id: transactions - limit: 10 - namespace: transactions - passive: true - scope: organization - window: - granularitySeconds: 100 - windowSeconds: 1000 -- id: sessions - limit: 20 - namespace: sessions - passive: true - scope: organization - window: - granularitySeconds: 200 - windowSeconds: 2000 -- id: spans - limit: 30 - namespace: spans - passive: true - scope: organization - window: - granularitySeconds: 300 - windowSeconds: 3000 -- id: test3 - limit: 90 - scope: name - window: - granularitySeconds: 900 - windowSeconds: 9000 -- id: test4 - limit: 100 - scope: name - window: - granularitySeconds: 1000 - windowSeconds: 10000 -- id: test2 - limit: 80 - report: true - scope: name - window: - granularitySeconds: 800 - windowSeconds: 8000 diff --git a/tests/sentry/relay/snapshots/test_config/test_project_config_cardinality_limits/True/REGION.pysnap.new b/tests/sentry/relay/snapshots/test_config/test_project_config_cardinality_limits/True/REGION.pysnap.new deleted file mode 100644 index 3a261bd3a04530..00000000000000 --- a/tests/sentry/relay/snapshots/test_config/test_project_config_cardinality_limits/True/REGION.pysnap.new +++ /dev/null @@ -1,49 +0,0 @@ ---- -created: '2025-02-12T13:05:37.259587+00:00' -creator: sentry -source: tests/sentry/relay/test_config.py ---- -cardinalityLimits: -- id: transactions - limit: 10 - namespace: transactions - passive: true - scope: organization - window: - granularitySeconds: 100 - windowSeconds: 1000 -- id: sessions - limit: 20 - namespace: sessions - passive: true - scope: organization - window: - granularitySeconds: 200 - windowSeconds: 2000 -- id: spans - limit: 30 - namespace: spans - passive: true - scope: organization - window: - granularitySeconds: 300 - windowSeconds: 3000 -- id: test3 - limit: 90 - scope: name - window: - granularitySeconds: 900 - windowSeconds: 9000 -- id: test4 - limit: 100 - scope: name - window: - granularitySeconds: 1000 - windowSeconds: 10000 -- id: test2 - limit: 80 - report: true - scope: name - window: - granularitySeconds: 800 - windowSeconds: 8000 diff --git a/tests/sentry/relay/test_config.py b/tests/sentry/relay/test_config.py index cd2ec78b303fd1..e8a89e99b3a518 100644 --- a/tests/sentry/relay/test_config.py +++ b/tests/sentry/relay/test_config.py @@ -1,6 +1,5 @@ import time from datetime import datetime, timedelta, timezone -from typing import Any from unittest import mock from unittest.mock import ANY, MagicMock, patch @@ -1353,229 +1352,6 @@ def test_mobile_performance_calculate_score(default_project) -> None: } -@django_db_all -@cell_silo_test -@pytest.mark.parametrize("passive", [False, True]) -def test_project_config_cardinality_limits( - default_project: Project, insta_snapshot: InstaSnapshotter, passive: bool -) -> None: - options: dict[Any, Any] = { - "relay.cardinality-limiter.mode": "enabled", - "sentry-metrics.cardinality-limiter.limits.transactions.per-org": [ - {"window_seconds": 1000, "granularity_seconds": 100, "limit": 10} - ], - "sentry-metrics.cardinality-limiter.limits.sessions.per-org": [ - {"window_seconds": 2000, "granularity_seconds": 200, "limit": 20} - ], - "sentry-metrics.cardinality-limiter.limits.spans.per-org": [ - {"window_seconds": 3000, "granularity_seconds": 300, "limit": 30} - ], - "sentry-metrics.cardinality-limiter.limits.custom.per-org": [ - {"window_seconds": 4000, "granularity_seconds": 400, "limit": 40} - ], - "sentry-metrics.cardinality-limiter.limits.generic-metrics.per-org": [ - {"window_seconds": 5000, "granularity_seconds": 500, "limit": 50} - ], - "sentry-metrics.cardinality-limiter.limits.profiles.per-org": [ - {"window_seconds": 3600, "granularity_seconds": 600, "limit": 60} - ], - } - - if passive: - options["relay.cardinality-limiter.passive-limits-by-org"] = { - str(default_project.organization.id): [ - "sessions", - "transactions", - "spans", - "profiles", - ] - } - - options["relay.cardinality-limiter.limits"] = [ - { - "rollout_rate": 0, - "limit": { - "id": "test1", - "window": {"windowSeconds": 7000, "granularitySeconds": 700}, - "limit": 70, - "scope": "name", - }, - }, - { - "rollout_rate": 1, - "limit": { - "id": "test2", - "window": {"windowSeconds": 8000, "granularitySeconds": 800}, - "limit": 80, - "scope": "name", - "report": True, - }, - }, - ] - - default_project.update_option( - "relay.cardinality-limiter.limits", - [ - { - "limit": { - "id": "test3", - "window": {"windowSeconds": 9000, "granularitySeconds": 900}, - "limit": 90, - "scope": "name", - } - } - ], - ) - - default_project.organization.update_option( - "relay.cardinality-limiter.limits", - [ - { - "limit": { - "id": "test4", - "window": {"windowSeconds": 10000, "granularitySeconds": 1000}, - "limit": 100, - "scope": "name", - } - } - ], - ) - - with override_options(options): - project_cfg = get_project_config(default_project) - - cfg = project_cfg.to_dict() - _validate_project_config(cfg["config"]) - - insta_snapshot(cfg["config"]["metrics"]) - - -@django_db_all -@cell_silo_test -def test_project_config_cardinality_limits_project_options_override_other_options( - default_project, -) -> None: - options: dict[Any, Any] = { - "relay.cardinality-limiter.mode": "enabled", - "sentry-metrics.cardinality-limiter.limits.transactions.per-org": None, - "sentry-metrics.cardinality-limiter.limits.sessions.per-org": None, - "sentry-metrics.cardinality-limiter.limits.spans.per-org": None, - "sentry-metrics.cardinality-limiter.limits.custom.per-org": None, - "sentry-metrics.cardinality-limiter.limits.generic-metrics.per-org": None, - "sentry-metrics.cardinality-limiter.limits.profiles.per-org": None, - } - - options["relay.cardinality-limiter.limits"] = [ - { - "limit": { - "id": "test1", - "window": {"windowSeconds": 1000, "granularitySeconds": 100}, - "limit": 10, - "scope": "name", - }, - }, - ] - - default_project.organization.update_option( - "relay.cardinality-limiter.limits", - [ - { - "limit": { - "id": "test1", - "window": {"windowSeconds": 2000, "granularitySeconds": 200}, - "limit": 20, - "scope": "name", - } - } - ], - ) - - default_project.update_option( - "relay.cardinality-limiter.limits", - [ - { - "limit": { - "id": "test1", - "window": {"windowSeconds": 3000, "granularitySeconds": 300}, - "limit": 30, - "scope": "project", - } - } - ], - ) - - with override_options(options): - project_cfg = get_project_config(default_project) - - cfg = project_cfg.to_dict() - _validate_project_config(cfg["config"]) - - assert cfg["config"]["metrics"]["cardinalityLimits"] == [ - { - "id": "test1", - "window": {"windowSeconds": 3000, "granularitySeconds": 300}, - "limit": 30, - "scope": "project", - } - ] - - -@django_db_all -@cell_silo_test -def test_project_config_cardinality_limits_organization_options_override_options( - default_project, -) -> None: - options: dict[Any, Any] = { - "relay.cardinality-limiter.mode": "enabled", - "sentry-metrics.cardinality-limiter.limits.transactions.per-org": None, - "sentry-metrics.cardinality-limiter.limits.sessions.per-org": None, - "sentry-metrics.cardinality-limiter.limits.spans.per-org": None, - "sentry-metrics.cardinality-limiter.limits.custom.per-org": None, - "sentry-metrics.cardinality-limiter.limits.generic-metrics.per-org": None, - "sentry-metrics.cardinality-limiter.limits.profiles.per-org": None, - } - - options["relay.cardinality-limiter.limits"] = [ - { - "limit": { - "id": "test1", - "window": {"windowSeconds": 1000, "granularitySeconds": 100}, - "limit": 10, - "scope": "name", - }, - }, - ] - - default_project.organization.update_option( - "relay.cardinality-limiter.limits", - [ - { - "limit": { - "id": "test1", - "window": {"windowSeconds": 2000, "granularitySeconds": 200}, - "limit": 20, - "scope": "project", - } - } - ], - ) - - with override_options(options): - project_cfg = get_project_config(default_project) - - cfg = project_cfg.to_dict() - _validate_project_config(cfg["config"]) - - assert cfg["config"]["metrics"]["cardinalityLimits"] == [ - { - "id": "test1", - "window": {"windowSeconds": 2000, "granularitySeconds": 200}, - "limit": 20, - "scope": "project", - } - ] - - @django_db_all @cell_silo_test def test_project_config_with_generic_filters(default_project) -> None: