diff --git a/.github/codeowners-coverage-baseline.txt b/.github/codeowners-coverage-baseline.txt index c7b63d380a87..288c082025f4 100644 --- a/.github/codeowners-coverage-baseline.txt +++ b/.github/codeowners-coverage-baseline.txt @@ -2144,7 +2144,6 @@ tests/sentry/models/test_commitfilechange.py tests/sentry/models/test_dashboard.py tests/sentry/models/test_debugfile.py tests/sentry/models/test_deploy.py -tests/sentry/models/test_dynamicsampling.py tests/sentry/models/test_environment.py tests/sentry/models/test_eventattachment.py tests/sentry/models/test_eventerror.py diff --git a/src/sentry/dynamic_sampling/__init__.py b/src/sentry/dynamic_sampling/__init__.py index e43f7e7c529f..6431f4973034 100644 --- a/src/sentry/dynamic_sampling/__init__.py +++ b/src/sentry/dynamic_sampling/__init__.py @@ -14,7 +14,6 @@ RuleType, get_enabled_user_biases, get_redis_client_for_ds, - get_rule_hash, get_supported_biases_ids, get_user_biases, ) @@ -25,7 +24,6 @@ "get_user_biases", "get_enabled_user_biases", "get_redis_client_for_ds", - "get_rule_hash", "record_latest_release", "RuleType", "ExtendedBoostedRelease", diff --git a/src/sentry/dynamic_sampling/rules/utils.py b/src/sentry/dynamic_sampling/rules/utils.py index d8c51a96c636..8329911463ae 100644 --- a/src/sentry/dynamic_sampling/rules/utils.py +++ b/src/sentry/dynamic_sampling/rules/utils.py @@ -3,14 +3,14 @@ from enum import Enum from typing import Literal, NotRequired, TypedDict, Union -import orjson from django.conf import settings from redis import StrictRedis -from sentry.models.dynamicsampling import CUSTOM_RULE_START from sentry.relay.types import RuleCondition from sentry.utils import redis +CUSTOM_RULE_START = 3000 + BOOSTED_RELEASES_LIMIT = 10 LATEST_RELEASES_BOOST_FACTOR = 1.5 @@ -117,23 +117,6 @@ class DecayingRule(Rule): PolymorphicRule = Union[Rule, DecayingRule] -def get_rule_hash(rule: PolymorphicRule) -> int: - # We want to be explicit in what we use for computing the hash. In addition, we need to remove certain fields like - # the sampleRate. - return ( - orjson.dumps( - { - "id": rule["id"], - "type": rule["type"], - "condition": rule["condition"], - }, - option=orjson.OPT_SORT_KEYS, - ) - .decode() - .__hash__() - ) - - def get_user_biases(user_set_biases: list[ActivatableBias] | None) -> list[ActivatableBias]: if user_set_biases is None: return DEFAULT_BIASES diff --git a/src/sentry/models/dynamicsampling.py b/src/sentry/models/dynamicsampling.py index c4e48774fd03..9ed171a1bc25 100644 --- a/src/sentry/models/dynamicsampling.py +++ b/src/sentry/models/dynamicsampling.py @@ -1,67 +1,12 @@ from __future__ import annotations -import hashlib -from collections.abc import Mapping, Sequence -from datetime import datetime, timedelta -from typing import TYPE_CHECKING, Any - -from django.db import models, router, transaction -from django.db.models import F, IntegerField, Max, Q, Subquery, Value -from django.db.models.functions import Coalesce +from django.db import models +from django.db.models import Q from django.utils import timezone from sentry.backup.scopes import RelocationScope -from sentry.constants import ObjectStatus from sentry.db.models import FlexibleForeignKey, Model, cell_silo_model from sentry.db.models.fields.hybrid_cloud_foreign_key import HybridCloudForeignKey -from sentry.utils import json, metrics - -if TYPE_CHECKING: - from sentry.models.organization import Organization - from sentry.models.project import Project - -# max number of custom rules that can be created per organization -MAX_CUSTOM_RULES = 2000 -CUSTOM_RULE_START = 3000 -MAX_CUSTOM_RULES_PER_PROJECT = 50 -CUSTOM_RULE_DATE_FORMAT = "%Y-%m-%dT%H:%M:%S.%fZ" - - -class TooManyRules(ValueError): - """ - Raised when a there is already the max number of rules active for an organization - """ - - -def get_rule_hash(condition: Any, project_ids: Sequence[int]) -> str: - """ - Returns the hash of the rule based on the condition and projects - """ - condition_string = to_order_independent_string(condition) - project_string = to_order_independent_string(list(project_ids)) - rule_string = f"{condition_string}-{project_string}" - # make it a bit shorter - return hashlib.sha1(rule_string.encode("utf-8")).hexdigest() - - -def to_order_independent_string(val: Any) -> str: - """ - Converts a value in an order independent string and then hashes it - - Note: this will insure the same repr is generated for ['x', 'y'] and ['y', 'x'] - Also the same repr is generated for {'x': 1, 'y': 2} and {'y': 2, 'x': 1} - """ - ret_val = "" - if isinstance(val, Mapping): - for key in sorted(val.keys()): - ret_val += f"{key}:{to_order_independent_string(val[key])}-" - elif isinstance(val, (list, tuple)): - vals = sorted([to_order_independent_string(item) for item in val]) - for item in vals: - ret_val += f"{item}-" - else: - ret_val = str(val) - return ret_val @cell_silo_model @@ -114,16 +59,6 @@ class CustomDynamicSamplingRule(Model): created_by_id = HybridCloudForeignKey("sentry.User", on_delete="CASCADE", null=True, blank=True) notification_sent = models.BooleanField(null=True, blank=True) - @property - def external_rule_id(self) -> int: - """ - Returns the external rule id - - For external users, i.e. Relay, we need to shift the ids since the slot we - have allocated starts at the offset specified in RESERVED_IDS. - """ - return self.rule_id + CUSTOM_RULE_START - class Meta: app_label = "sentry" db_table = "sentry_customdynamicsamplingrule" @@ -137,230 +72,3 @@ class Meta: fields=["condition_hash"], name="condition_hash_idx", condition=Q(is_active=True) ), ] - - @staticmethod - def get_rule_for_org( - condition: Any, - organization_id: int, - project_ids: Sequence[int], - ) -> CustomDynamicSamplingRule | None: - """ - Returns an active rule for the given condition and organization if it exists otherwise None - - Note: There should not be more than one active rule for a given condition and organization - This function doesn't verify this condition, it just returns the first one. - """ - rule_hash = get_rule_hash(condition, project_ids) - rules = CustomDynamicSamplingRule.objects.filter( - organization_id=organization_id, - condition_hash=rule_hash, - is_active=True, - end_date__gt=timezone.now(), - )[:1] - - return rules[0] if rules else None - - @staticmethod - def update_or_create( - condition: Any, - start: datetime, - end: datetime, - project_ids: Sequence[int], - organization_id: int, - num_samples: int, - sample_rate: float, - query: str, - created_by_id: int | None = None, - ) -> CustomDynamicSamplingRule: - from sentry.models.organization import Organization - from sentry.models.project import Project - - with transaction.atomic(router.db_for_write(CustomDynamicSamplingRule)): - # check if rule already exists for this organization - existing_rule = CustomDynamicSamplingRule.get_rule_for_org( - condition, organization_id, project_ids - ) - - if existing_rule is not None: - # we already have an active rule for this condition and this organization - # update the expiration date and ensure that our projects are included - existing_rule.end_date = max(end, existing_rule.end_date) - existing_rule.num_samples = max(num_samples, existing_rule.num_samples) - existing_rule.sample_rate = max(sample_rate, existing_rule.sample_rate) - - # for org rules we don't need to do anything with the projects - existing_rule.save() - return existing_rule - else: - projects = Project.objects.get_many_from_cache(project_ids) - projects = list(projects) - organization = Organization.objects.get_from_cache(id=organization_id) - - if CustomDynamicSamplingRule.per_project_limit_reached(projects, organization): - raise TooManyRules() - - # create a new rule - rule_hash = get_rule_hash(condition, project_ids) - is_org_level = len(project_ids) == 0 - condition_str = json.dumps(condition) - rule = CustomDynamicSamplingRule.objects.create( - organization_id=organization_id, - condition=condition_str, - sample_rate=sample_rate, - start_date=start, - end_date=end, - num_samples=num_samples, - condition_hash=rule_hash, - is_active=True, - is_org_level=is_org_level, - query=query, - notification_sent=False, - created_by_id=created_by_id, - ) - - rule.save() - # now try to assign a rule id - id = rule.assign_rule_id() - if id > MAX_CUSTOM_RULES: - # we have too many rules, delete this one - rule.delete() - raise TooManyRules() - - # set the projects if not org level - for project in projects: - CustomDynamicSamplingRuleProject.objects.create( - custom_dynamic_sampling_rule=rule, project=project - ) - return rule - - def assign_rule_id(self) -> int: - """ - Assigns the smallest rule id that is not taken in the - current organization. - """ - if self.id is None: - raise ValueError("Cannot assign rule id to unsaved object") - if self.rule_id != 0: - raise ValueError("Cannot assign rule id to object that already has a rule id") - - now = timezone.now() - - base_qs = CustomDynamicSamplingRule.objects.filter( - organization_id=self.organization.id, end_date__gt=now, is_active=True - ) - - # We want to find the smallest free rule id. We do this by self-joining with rule_id + 1 and excluding the existing rule_ids. - # We then order by rule_id_plus_one and take the first value. - # This also works for the first rule, as it is pre-initialized with 0, and will thus end up with 1. - new_rule_id_subquery = Subquery( - base_qs.annotate(rule_id_plus_one=F("rule_id") + 1) - .exclude(rule_id_plus_one__in=base_qs.values_list("rule_id", flat=True)) - .order_by("rule_id_plus_one") - .values("rule_id_plus_one")[:1] - ) - - max_rule_id = base_qs.aggregate(Max("rule_id"))["rule_id__max"] or 0 - fallback_value = Value(max_rule_id + 1, output_field=IntegerField()) - - safe_new_rule_id = Coalesce(new_rule_id_subquery, fallback_value) - - # Update this instance with the new rule_id - CustomDynamicSamplingRule.objects.filter(id=self.id).update(rule_id=safe_new_rule_id) - self.refresh_from_db() - return self.rule_id - - @staticmethod - def deactivate_old_rules() -> None: - """ - Deactivates all rules expired rules (this is just an optimization to remove old rules from indexes). - - This should be called periodically to clean up old rules (it is not necessary to call it for correctness, - just for performance) - """ - CustomDynamicSamplingRule.objects.filter( - # give it a minute grace period to make sure we don't deactivate rules that are still active - end_date__lt=timezone.now() - timedelta(minutes=1), - ).update(is_active=False) - - @staticmethod - def get_project_rules( - project: Project, - ) -> Sequence[CustomDynamicSamplingRule]: - """ - Returns all active project rules - """ - now = timezone.now() - # org rules ( apply to all projects in the org) - org_rules = CustomDynamicSamplingRule.objects.filter( - is_active=True, - is_org_level=True, - organization=project.organization, - end_date__gt=now, - start_date__lt=now, - )[: MAX_CUSTOM_RULES_PER_PROJECT + 1] - - # project rules - project_rules = CustomDynamicSamplingRule.objects.filter( - is_active=True, - projects__in=[project], - end_date__gt=now, - start_date__lt=now, - )[: MAX_CUSTOM_RULES_PER_PROJECT + 1] - - rules = list(project_rules.union(org_rules)[: MAX_CUSTOM_RULES_PER_PROJECT + 1]) - - if len(rules) > MAX_CUSTOM_RULES_PER_PROJECT: - metrics.incr("dynamic_sampling.custom_rules.overflow") - - return rules[:MAX_CUSTOM_RULES_PER_PROJECT] - - @staticmethod - def deactivate_expired_rules() -> None: - """ - Deactivates all rules that have expired - """ - CustomDynamicSamplingRule.objects.filter( - end_date__lt=timezone.now(), is_active=True - ).update(is_active=False) - - @staticmethod - def num_active_rules_for_project(project: Project) -> int: - """ - Returns the number of active rules for the given project - """ - now = timezone.now() - - num_org_rules = CustomDynamicSamplingRule.objects.filter( - is_active=True, - is_org_level=True, - organization=project.organization, - end_date__gt=now, - start_date__lte=now, - ).count() - - num_proj_rules = CustomDynamicSamplingRule.objects.filter( - is_active=True, - is_org_level=False, - projects__in=[project], - end_date__gt=now, - start_date__lte=now, - ).count() - - return num_proj_rules + num_org_rules - - @staticmethod - def per_project_limit_reached(projects: Sequence[Project], organization: Organization) -> bool: - """ - Returns True if the rule limit is reached for any of the given projects (or all - the projects in the organization if org level rule) - """ - projects = list(projects) - if len(projects) == 0: - # an org rule check all the org projects - org_projects = organization.project_set.filter(status=ObjectStatus.ACTIVE) - projects = list(org_projects) - for project in projects: - num_rules = CustomDynamicSamplingRule.num_active_rules_for_project(project) - if num_rules >= MAX_CUSTOM_RULES_PER_PROJECT: - return True - return False diff --git a/src/sentry/organizations/services/organization/impl.py b/src/sentry/organizations/services/organization/impl.py index 2cdb2a01a3ed..5b7c8db37cf3 100644 --- a/src/sentry/organizations/services/organization/impl.py +++ b/src/sentry/organizations/services/organization/impl.py @@ -20,7 +20,6 @@ from sentry.incidents.models.incident import IncidentActivity from sentry.models.activity import Activity from sentry.models.dashboard import Dashboard, DashboardFavoriteUser -from sentry.models.dynamicsampling import CustomDynamicSamplingRule from sentry.models.groupassignee import GroupAssignee from sentry.models.groupbookmark import GroupBookmark from sentry.models.groupsearchview import GroupSearchView @@ -581,7 +580,6 @@ def merge_users(self, *, organization_id: int, from_user_id: int, to_user_id: in Activity, AlertRule, AlertRuleActivity, - CustomDynamicSamplingRule, Dashboard, DashboardFavoriteUser, GroupAssignee, diff --git a/src/sentry/testutils/helpers/backups.py b/src/sentry/testutils/helpers/backups.py index 51c6cadabfdd..f99e6fe1e45c 100644 --- a/src/sentry/testutils/helpers/backups.py +++ b/src/sentry/testutils/helpers/backups.py @@ -77,7 +77,10 @@ DashboardWidgetQueryOnDemand, DashboardWidgetTypes, ) -from sentry.models.dynamicsampling import CustomDynamicSamplingRule +from sentry.models.dynamicsampling import ( + CustomDynamicSamplingRule, + CustomDynamicSamplingRuleProject, +) from sentry.models.groupassignee import GroupAssignee from sentry.models.groupbookmark import GroupBookmark from sentry.models.groupsearchview import GroupSearchView, GroupSearchViewProject @@ -520,17 +523,6 @@ def create_exhaustive_organization( sent_initial_email_date=timezone.now(), sent_final_email_date=timezone.now(), ) - CustomDynamicSamplingRule.update_or_create( - created_by_id=owner_id, - condition={"op": "equals", "name": "environment", "value": "prod"}, - start=timezone.now(), - end=timezone.now() + timedelta(hours=1), - project_ids=[project.id], - organization_id=org.id, - num_samples=100, - sample_rate=0.5, - query="environment:prod event.type:transaction", - ) # Environment* self.create_environment(project=project) @@ -827,6 +819,19 @@ def create_exhaustive_organization( overrides={"write_key": "test_override_write_key"}, ) + custom_rule = CustomDynamicSamplingRule.objects.create( + organization=org, + condition='{"op":"and","inner":[]}', + end_date=timezone.now() + timedelta(days=1), + num_samples=100, + condition_hash="abc123def456abc123def456abc123def4560000", + sample_rate=0.5, + ) + CustomDynamicSamplingRuleProject.objects.create( + custom_dynamic_sampling_rule=custom_rule, + project=project, + ) + return org @assume_test_silo_mode(SiloMode.CONTROL) diff --git a/tests/sentry/models/test_dynamicsampling.py b/tests/sentry/models/test_dynamicsampling.py deleted file mode 100644 index 52bbd5737c4a..000000000000 --- a/tests/sentry/models/test_dynamicsampling.py +++ /dev/null @@ -1,363 +0,0 @@ -from datetime import datetime, timedelta - -import pytest -from django.utils import timezone - -from sentry.models.dynamicsampling import ( - MAX_CUSTOM_RULES_PER_PROJECT, - CustomDynamicSamplingRule, - TooManyRules, -) -from sentry.models.organization import Organization -from sentry.models.project import Project -from sentry.testutils.cases import TestCase -from sentry.testutils.helpers.datetime import freeze_time - - -def _create_rule_for_env( - env_idx: int, projects: list[Project], organization: Organization -) -> CustomDynamicSamplingRule: - condition = {"op": "equals", "name": "environment", "value": f"prod{env_idx}"} - return CustomDynamicSamplingRule.update_or_create( - condition=condition, - start=timezone.now(), - end=timezone.now() + timedelta(hours=1), - project_ids=[project.id for project in projects], - organization_id=organization.id, - num_samples=100, - sample_rate=0.5, - query=f"environment:prod{env_idx}", - ) - - -@freeze_time("2023-09-18") -class TestCustomDynamicSamplingRuleProject(TestCase): - def setUp(self) -> None: - super().setUp() - self.second_project = self.create_project() - self.second_organization = self.create_organization(owner=self.user) - self.third_project = self.create_project(organization=self.second_organization) - - def test_update_or_create(self) -> None: - condition = {"op": "equals", "name": "environment", "value": "prod"} - - end1 = timezone.now() + timedelta(hours=1) - - rule = CustomDynamicSamplingRule.update_or_create( - condition=condition, - start=timezone.now(), - end=end1, - project_ids=[self.project.id], - organization_id=self.organization.id, - num_samples=100, - sample_rate=0.5, - query="environment:prod", - ) - - end2 = timezone.now() + timedelta(hours=1) - updated_rule = CustomDynamicSamplingRule.update_or_create( - condition=condition, - start=timezone.now() + timedelta(minutes=1), - end=end2, - project_ids=[self.project.id], - organization_id=self.organization.id, - num_samples=100, - sample_rate=0.5, - query="environment:prod", - ) - - assert rule.id == updated_rule.id - projects = updated_rule.projects.all() - - assert len(projects) == 1 - assert self.project in projects - - assert updated_rule.end_date >= end1 - assert updated_rule.end_date >= end2 - - def test_assign_rule_id(self) -> None: - rule_ids = set() - rules = [] - for idx in range(3): - rule = _create_rule_for_env(idx, [self.project], self.organization) - rule_ids.add(rule.rule_id) - rules.append(rule) - - # all 3 rules have different rule ids - assert len(rule_ids) == 3 - - # make a rule obsolete and check that the rule id is reused - rules[1].is_active = False - rules[1].save() - - new_rule = _create_rule_for_env(4, [self.project], self.organization) - assert new_rule.rule_id == rules[1].rule_id - - # a new rule will take another slot (now that there is no free slot) - new_rule_2 = _create_rule_for_env(5, [self.project], self.organization) - assert new_rule_2.rule_id not in rule_ids - - # make again an empty slot ( this time by having the rule expire) - rules[2].start_date = timezone.now() - timedelta(hours=2) - rules[2].end_date = timezone.now() - timedelta(hours=1) - rules[2].save() - - # the new rule should take the empty slot - new_rule_3 = _create_rule_for_env(6, [self.project], self.organization) - assert new_rule_3.rule_id == rules[2].rule_id - - def test_deactivate_old_rules(self) -> None: - idx = 1 - - old_rules = [] - new_rules = [] - - def create_rule(is_old: bool, idx: int) -> CustomDynamicSamplingRule: - condition = {"op": "equals", "name": "environment", "value": f"prod{idx}"} - if is_old: - end_delta = -timedelta(hours=1) - else: - end_delta = timedelta(hours=1) - return CustomDynamicSamplingRule.update_or_create( - condition=condition, - start=timezone.now() - timedelta(hours=2), - end=timezone.now() + end_delta, - project_ids=[self.project.id], - organization_id=self.organization.id, - num_samples=100, - sample_rate=0.5, - query=f"environment:prod{idx}", - ) - - for i in range(10): - for is_old in [True, False]: - idx += 1 - rule = create_rule(is_old, idx) - if is_old: - old_rules.append(rule) - else: - new_rules.append(rule) - - CustomDynamicSamplingRule.deactivate_old_rules() - - # check that all old rules are inactive and all new rules are active - inactive_rules = list(CustomDynamicSamplingRule.objects.filter(is_active=False)) - assert len(inactive_rules) == 10 - for rule in old_rules: - assert rule in inactive_rules - - active_rules = list(CustomDynamicSamplingRule.objects.filter(is_active=True)) - assert len(active_rules) == 10 - for rule in new_rules: - assert rule in active_rules - - def test_get_rule_for_org(self) -> None: - """ - Test the get_rule_for_org method - """ - condition = {"op": "equals", "name": "environment", "value": "prod"} - - # check empty result - rule = CustomDynamicSamplingRule.get_rule_for_org( - condition, self.organization.id, [self.project.id] - ) - assert rule is None - - new_rule = CustomDynamicSamplingRule.update_or_create( - condition=condition, - start=timezone.now() - timedelta(hours=2), - end=timezone.now() + timedelta(hours=1), - project_ids=[self.project.id], - organization_id=self.organization.id, - num_samples=100, - sample_rate=0.5, - query="environment:prod", - ) - - rule = CustomDynamicSamplingRule.get_rule_for_org( - condition, self.organization.id, [self.project.id] - ) - assert rule == new_rule - - def test_get_project_rules(self) -> None: - """ - Tests that all valid rules (i.e. active and within the date range) that apply to a project - (i.e. that are either organization rules or apply to the project) are returned. - """ - - idx = [1] - - def create_rule( - project_ids: list[int], - org_id: int | None = None, - old: bool = False, - new: bool = False, - ) -> CustomDynamicSamplingRule: - idx[0] += 1 - condition = {"op": "equals", "name": "environment", "value": f"prod{idx[0]}"} - if old: - end_delta = -timedelta(hours=2) - else: - end_delta = timedelta(hours=2) - - if new: - start_delta = timedelta(hours=1) - else: - start_delta = -timedelta(hours=1) - - if org_id is None: - org_id = self.organization.id - - return CustomDynamicSamplingRule.update_or_create( - condition=condition, - start=timezone.now() + start_delta, - end=timezone.now() + end_delta, - project_ids=project_ids, - organization_id=org_id, - num_samples=100, - sample_rate=0.5, - query=f"environment:prod{idx[0]}", - ) - - valid_project_rule = create_rule([self.project.id, self.second_project.id]) - valid_org_rule = create_rule([]) - # rule for another project - create_rule([self.second_project.id]) - # rule for another org - create_rule([self.third_project.id], org_id=self.second_organization.id) - # old project rule ( already expired) - create_rule([self.project.id], old=True) - # new project rule ( not yet active) - create_rule([self.project.id], new=True) - # old org rule - create_rule([], old=True) - # new org rule - create_rule([], new=True) - - # we should only get valid_project_rule and valid_org_rule - rules = list(CustomDynamicSamplingRule.get_project_rules(self.project)) - assert len(rules) == 2 - assert valid_project_rule in rules - assert valid_org_rule in rules - - def test_separate_projects_create_different_rules(self) -> None: - """ - Tests that same condition for different projects create different rules - """ - condition = {"op": "equals", "name": "environment", "value": "prod"} - - end1 = timezone.now() + timedelta(hours=1) - - rule = CustomDynamicSamplingRule.update_or_create( - condition=condition, - start=timezone.now(), - end=end1, - project_ids=[self.project.id], - organization_id=self.organization.id, - num_samples=100, - sample_rate=0.5, - query="environment:prod", - ) - - end2 = timezone.now() + timedelta(hours=1) - second_rule = CustomDynamicSamplingRule.update_or_create( - condition=condition, - start=timezone.now() + timedelta(minutes=1), - end=end2, - project_ids=[self.second_project.id], - organization_id=self.organization.id, - num_samples=100, - sample_rate=0.5, - query="environment:prod", - ) - - assert rule.id != second_rule.id - - first_projects = rule.projects.all() - assert len(first_projects) == 1 - assert self.project == first_projects[0] - - second_projects = second_rule.projects.all() - assert len(second_projects) == 1 - assert self.second_project == second_projects[0] - - def test_deactivate_expired_rules(self) -> None: - """ - Tests that expired, and only expired, rules are deactivated - """ - - def create_rule( - env_idx: int, end: datetime, project_ids: list[int] - ) -> CustomDynamicSamplingRule: - condition = {"op": "equals", "name": "environment", "value": f"prod{env_idx}"} - return CustomDynamicSamplingRule.update_or_create( - condition=condition, - start=timezone.now() - timedelta(hours=5), - end=end, - project_ids=project_ids, - organization_id=self.organization.id, - num_samples=100, - sample_rate=0.5, - query=f"environment:prod{env_idx}", - ) - - env_idx = 1 - expired_rules: set[int] = set() - active_rules: set[int] = set() - - for projects in [ - [self.project], - [self.second_project], - [self.third_project], - [self.project, self.second_project, self.third_project], - [], - ]: - # create some expired rules - project_ids = [p.id for p in projects] - rule = create_rule(env_idx, timezone.now() - timedelta(minutes=5), project_ids) - expired_rules.add(rule.id) - env_idx += 1 - - # create some active rules - rule = create_rule(env_idx, timezone.now() + timedelta(minutes=5), project_ids) - active_rules.add(rule.id) - env_idx += 1 - - # check that all rules are active before deactivation - for rule in CustomDynamicSamplingRule.objects.all(): - assert rule.is_active - - CustomDynamicSamplingRule.deactivate_expired_rules() - - # check that all expired rules are inactive and all active rules are still active - for rule in CustomDynamicSamplingRule.objects.all(): - if rule.id in expired_rules: - assert not rule.is_active - else: - assert rule.is_active - assert rule.id in active_rules - - def test_per_project_limit(self) -> None: - """ - Tests that it is not possible to create more than MAX_CUSTOM_RULES_PER_PROJECT - for a project - """ - - # a few org rules - num_org_rules = 10 - for idx in range(num_org_rules): - _create_rule_for_env(idx, [], self.organization) - - # now add project rules (up to MAX_CUSTOM_RULES_PER_PROJECT) - for idx in range(num_org_rules, MAX_CUSTOM_RULES_PER_PROJECT): - _create_rule_for_env(idx, [self.project], self.organization) - _create_rule_for_env(idx, [self.second_project], self.organization) - - # we've reached the limit for both project and second_project next one should raise TooManyRules() - with pytest.raises(TooManyRules): - _create_rule_for_env(MAX_CUSTOM_RULES_PER_PROJECT, [self.project], self.organization) - - with pytest.raises(TooManyRules): - _create_rule_for_env( - MAX_CUSTOM_RULES_PER_PROJECT, [self.second_project], self.organization - ) diff --git a/tests/sentry/tasks/snapshots/PreprocessingTransferTest/test_success.pysnap b/tests/sentry/tasks/snapshots/PreprocessingTransferTest/test_success.pysnap index 84c6bfda4e2c..bb542cfeee47 100644 --- a/tests/sentry/tasks/snapshots/PreprocessingTransferTest/test_success.pysnap +++ b/tests/sentry/tasks/snapshots/PreprocessingTransferTest/test_success.pysnap @@ -96,7 +96,7 @@ steps: - -U - postgres - -c - - TRUNCATE sentry_controloption,sentry_integration,sentry_option,sentry_organization,sentry_organizationintegration,sentry_organizationoptions,sentry_projecttemplate,sentry_projecttemplateoption,sentry_relay,sentry_relayusage,sentry_repository,sentry_team,auth_user,sentry_userip,sentry_userpermission,sentry_userrole,sentry_userrole_users,workflow_engine_dataconditiongroup,workflow_engine_datasource,workflow_engine_datacondition,sentry_savedsearch,sentry_recentsearch,sentry_project,sentry_orgauthtoken,sentry_organizationmember,sentry_organizationaccessrequest,sentry_monitor,sentry_groupsearchview,sentry_environment,sentry_email,sentry_datasecrecywaiver,sentry_dashboardtombstone,sentry_dashboard,sentry_customdynamicsamplingrule,sentry_projectcounter,sentry_authprovider,sentry_authidentity,auth_authenticator,sentry_apikey,sentry_apiapplication,workflow_engine_workflow,workflow_engine_detector,workflow_engine_datasourcedetector,sentry_useroption,sentry_useremail,sentry_snubaquery,sentry_sentryapp,sentry_rule,sentry_querysubscription,sentry_projectteam,sentry_projectredirect,sentry_projectownership,sentry_projectoptions,sentry_projectkey,sentry_projectintegration,sentry_projectbookmark,sentry_organizationmember_teams,sentry_notificationaction,sentry_neglectedrule,sentry_environmentproject,sentry_dashboardwidget,sentry_dashboardpermissions,sentry_dashboardfavoriteuser,sentry_customdynamicsamplingruleproject,sentry_apitoken,sentry_apigrant,sentry_apiauthorization,sentry_alertrule,workflow_engine_workflowdataconditiongroup,workflow_engine_detectorworkflow,workflow_engine_alertruleworkflow,workflow_engine_alertruledetector,sentry_snubaqueryeventtype,sentry_sentryappinstallation,sentry_sentryappcomponent,sentry_rulesnooze,sentry_ruleactivity,sentry_notificationactionproject,sentry_dashboardwidgetquery,sentry_dashboardpermissionsteam,sentry_alertruletrigger,sentry_alertruleprojects,sentry_alertruleactivity,sentry_alertruleactivationcondition,workflow_engine_alertruletriggerdatacondition,sentry_servicehook,sentry_incident,sentry_dashboardwidgetqueryondemand,sentry_alertruletriggeraction,sentry_timeseriessnapshot,sentry_pendingincidentsnapshot,sentry_incidenttrigger,sentry_incidentsnapshot,sentry_incidentactivity + - TRUNCATE sentry_controloption,sentry_integration,sentry_option,sentry_organization,sentry_organizationintegration,sentry_organizationoptions,sentry_projecttemplate,sentry_projecttemplateoption,sentry_relay,sentry_relayusage,sentry_repository,sentry_team,auth_user,sentry_userip,sentry_userpermission,sentry_userrole,sentry_userrole_users,workflow_engine_dataconditiongroup,workflow_engine_datasource,workflow_engine_datacondition,sentry_savedsearch,sentry_recentsearch,sentry_project,sentry_orgauthtoken,sentry_organizationmember,sentry_organizationaccessrequest,sentry_monitor,sentry_groupsearchview,sentry_environment,sentry_email,sentry_datasecrecywaiver,sentry_dashboardtombstone,sentry_dashboard,sentry_projectcounter,sentry_authprovider,sentry_authidentity,auth_authenticator,sentry_apikey,sentry_apiapplication,workflow_engine_workflow,workflow_engine_detector,workflow_engine_datasourcedetector,sentry_useroption,sentry_useremail,sentry_snubaquery,sentry_sentryapp,sentry_rule,sentry_querysubscription,sentry_projectteam,sentry_projectredirect,sentry_projectownership,sentry_projectoptions,sentry_projectkey,sentry_projectintegration,sentry_projectbookmark,sentry_organizationmember_teams,sentry_notificationaction,sentry_neglectedrule,sentry_environmentproject,sentry_dashboardwidget,sentry_dashboardpermissions,sentry_dashboardfavoriteuser,sentry_apitoken,sentry_apigrant,sentry_apiauthorization,sentry_alertrule,workflow_engine_workflowdataconditiongroup,workflow_engine_detectorworkflow,workflow_engine_alertruleworkflow,workflow_engine_alertruledetector,sentry_snubaqueryeventtype,sentry_sentryappinstallation,sentry_sentryappcomponent,sentry_rulesnooze,sentry_ruleactivity,sentry_notificationactionproject,sentry_dashboardwidgetquery,sentry_dashboardpermissionsteam,sentry_alertruletrigger,sentry_alertruleprojects,sentry_alertruleactivity,sentry_alertruleactivationcondition,workflow_engine_alertruletriggerdatacondition,sentry_servicehook,sentry_incident,sentry_dashboardwidgetqueryondemand,sentry_alertruletriggeraction,sentry_timeseriessnapshot,sentry_pendingincidentsnapshot,sentry_incidenttrigger,sentry_incidentsnapshot,sentry_incidentactivity RESTART IDENTITY CASCADE; id: clear-database name: gcr.io/cloud-builders/docker diff --git a/tests/sentry/users/models/test_user.py b/tests/sentry/users/models/test_user.py index a34ab72712ae..4ad79b521c34 100644 --- a/tests/sentry/users/models/test_user.py +++ b/tests/sentry/users/models/test_user.py @@ -13,7 +13,6 @@ from sentry.models.activity import Activity from sentry.models.authidentity import AuthIdentity from sentry.models.dashboard import Dashboard, DashboardFavoriteUser -from sentry.models.dynamicsampling import CustomDynamicSamplingRule from sentry.models.groupassignee import GroupAssignee from sentry.models.groupbookmark import GroupBookmark from sentry.models.groupsearchview import GroupSearchView @@ -383,7 +382,6 @@ def test_duplicate_memberships(self, expected_models: list[type[Model]]) -> None Activity, AlertRule, AlertRuleActivity, - CustomDynamicSamplingRule, Dashboard, DashboardFavoriteUser, GroupAssignee, @@ -427,7 +425,6 @@ def test_only_source_user_is_member_of_organization( Activity, AlertRule, AlertRuleActivity, - CustomDynamicSamplingRule, Dashboard, DashboardFavoriteUser, GroupAssignee,