Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/sentry/conf/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -2817,7 +2817,7 @@ def custom_parameter_sort(parameter: dict) -> tuple[str, int]:
BETA_GROUPING_CONFIG = ""

# How long the migration phase for grouping lasts
SENTRY_GROUPING_CONFIG_TRANSITION_DURATION = 30 * 24 * 3600 # 30 days
SENTRY_GROUPING_CONFIG_TRANSITION_DURATION = 90 * 24 * 3600 # 90 days, until groups age out

SENTRY_USE_GRANIAN = True

Expand Down
190 changes: 2 additions & 188 deletions tests/sentry/event_manager/test_event_manager_grouping.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,40 +6,34 @@
from time import time
from typing import Any
from unittest import mock
from unittest.mock import ANY, MagicMock, patch
from unittest.mock import MagicMock, patch

import pytest
from django.core.cache import cache

from sentry import audit_log
from sentry.conf.server import DEFAULT_GROUPING_CONFIG, SENTRY_GROUPING_CONFIG_TRANSITION_DURATION
from sentry.conf.server import DEFAULT_GROUPING_CONFIG
from sentry.event_manager import _get_updated_group_title
from sentry.eventtypes.base import DefaultEvent
from sentry.exceptions import HashDiscarded
from sentry.grouping.api import get_grouping_config_dict_for_project
from sentry.grouping.ingest.caching import (
get_grouphash_existence_cache_key,
get_grouphash_object_cache_key,
)
from sentry.grouping.ingest.config import update_or_set_grouping_config_if_needed
from sentry.grouping.ingest.hashing import (
GROUPHASH_CACHE_EXPIRY_SECONDS,
find_grouphash_with_group,
get_or_create_grouphashes,
)
from sentry.models.auditlogentry import AuditLogEntry
from sentry.models.group import Group
from sentry.models.grouphash import GroupHash
from sentry.models.grouptombstone import GroupTombstone
from sentry.models.options.project_option import ProjectOption
from sentry.models.project import Project
from sentry.services.eventstore.models import Event
from sentry.testutils.cases import TestCase
from sentry.testutils.helpers.eventprocessing import save_new_event
from sentry.testutils.helpers.options import override_options
from sentry.testutils.pytest.fixtures import django_db_all
from sentry.testutils.pytest.mocking import count_matching_calls
from sentry.testutils.silo import assume_test_silo_mode_of
from sentry.testutils.skips import requires_snuba
from tests.sentry.grouping import NO_MSG_PARAM_CONFIG

Expand Down Expand Up @@ -186,186 +180,6 @@ def test_updates_group_metadata(self) -> None:
assert group.message == event2.message
assert group.data["metadata"]["title"] == event2.title

def test_loads_default_config_if_stored_config_option_is_invalid(self) -> None:
self.project.update_option("sentry:grouping_config", "dogs.are.great")
config_dict = get_grouping_config_dict_for_project(self.project)
assert config_dict["id"] == DEFAULT_GROUPING_CONFIG

self.project.update_option("sentry:grouping_config", {"not": "a string"})
config_dict = get_grouping_config_dict_for_project(self.project)
assert config_dict["id"] == DEFAULT_GROUPING_CONFIG

def test_auto_updates_grouping_config_even_if_config_is_gone(self) -> None:
"""This tests that setups with deprecated configs will auto-upgrade."""
self.project.update_option("sentry:grouping_config", "non_existing_config")
save_new_event({"message": "foo"}, self.project)
assert self.project.get_option("sentry:grouping_config") == DEFAULT_GROUPING_CONFIG
assert self.project.get_option("sentry:secondary_grouping_config") is None

def test_auto_updates_grouping_config(self) -> None:
self.project.update_option("sentry:grouping_config", NO_MSG_PARAM_CONFIG)
# Set platform to prevent additional audit log entry from platform inference
self.project.platform = "python"
self.project.save()

save_new_event({"message": "Adopt don't shop"}, self.project)
assert self.project.get_option("sentry:grouping_config") == DEFAULT_GROUPING_CONFIG

with assume_test_silo_mode_of(AuditLogEntry):
audit_log_entry = AuditLogEntry.objects.get()

assert audit_log_entry.event == audit_log.get_event_id("PROJECT_EDIT")
assert audit_log_entry.actor_label == "Sentry"

assert audit_log_entry.data == {
"sentry:grouping_config": DEFAULT_GROUPING_CONFIG,
"sentry:secondary_grouping_config": NO_MSG_PARAM_CONFIG,
"sentry:secondary_grouping_expiry": ANY, # tested separately below
"id": self.project.id,
"slug": self.project.slug,
"name": self.project.name,
"status": 0,
"public": False,
}

# When the config upgrade is actually happening, the expiry value is set before the
# audit log entry is created, which means the expiry is based on a timestamp
# ever-so-slightly before the audit log entry's timestamp, making a one-second tolerance
# necessary.
actual_expiry = audit_log_entry.data["sentry:secondary_grouping_expiry"]
expected_expiry = (
int(audit_log_entry.datetime.timestamp()) + SENTRY_GROUPING_CONFIG_TRANSITION_DURATION
)
assert actual_expiry == expected_expiry or actual_expiry == expected_expiry - 1

@patch(
"sentry.event_manager.update_or_set_grouping_config_if_needed",
wraps=update_or_set_grouping_config_if_needed,
)
def test_sets_default_grouping_config_project_option_if_missing(
self, update_config_spy: MagicMock
):
# To start, the project defaults to the current config but doesn't have its own config
# option set in the DB
assert self.project.get_option("sentry:grouping_config") == DEFAULT_GROUPING_CONFIG
assert (
ProjectOption.objects.filter(
project_id=self.project.id, key="sentry:grouping_config"
).first()
is None
)

save_new_event({"message": "Dogs are great!"}, self.project)

update_config_spy.assert_called_with(self.project, "ingest")

# After the function has been called, the config still defaults to the current one (and no
# transition has started), but the project now has its own config record in the DB
assert self.project.get_option("sentry:grouping_config") == DEFAULT_GROUPING_CONFIG
assert self.project.get_option("sentry:secondary_grouping_config") is None
assert self.project.get_option("sentry:secondary_grouping_expiry") == 0
assert ProjectOption.objects.filter(
project_id=self.project.id, key="sentry:grouping_config"
).exists()

@patch(
"sentry.event_manager.update_or_set_grouping_config_if_needed",
wraps=update_or_set_grouping_config_if_needed,
)
def test_no_ops_if_grouping_config_project_option_exists_and_is_current(
self, update_config_spy: MagicMock
):
self.project.update_option("sentry:grouping_config", DEFAULT_GROUPING_CONFIG)

assert self.project.get_option("sentry:grouping_config") == DEFAULT_GROUPING_CONFIG
assert ProjectOption.objects.filter(
project_id=self.project.id, key="sentry:grouping_config"
).exists()

save_new_event({"message": "Dogs are great!"}, self.project)

update_config_spy.assert_called_with(self.project, "ingest")

# After the function has been called, the config still defaults to the current one and no
# transition has started
assert self.project.get_option("sentry:grouping_config") == DEFAULT_GROUPING_CONFIG
assert self.project.get_option("sentry:secondary_grouping_config") is None
assert self.project.get_option("sentry:secondary_grouping_expiry") == 0

@patch(
"sentry.event_manager.update_or_set_grouping_config_if_needed",
wraps=update_or_set_grouping_config_if_needed,
)
def test_no_ops_if_sample_rate_test_fails(self, update_config_spy: MagicMock):
with (
# Ensure our die roll will fall outside the sample rate
patch("sentry.grouping.ingest.config.random", return_value=0.1121),
override_options({"grouping.config_transition.config_upgrade_sample_rate": 0.0908}),
):
self.project.update_option("sentry:grouping_config", NO_MSG_PARAM_CONFIG)
assert self.project.get_option("sentry:grouping_config") == NO_MSG_PARAM_CONFIG

save_new_event({"message": "Dogs are great!"}, self.project)

update_config_spy.assert_called_with(self.project, "ingest")

# After the function has been called, the config hasn't changed and no transition has
# started
assert self.project.get_option("sentry:grouping_config") == NO_MSG_PARAM_CONFIG
assert self.project.get_option("sentry:secondary_grouping_config") is None
assert self.project.get_option("sentry:secondary_grouping_expiry") == 0

@patch(
"sentry.event_manager.update_or_set_grouping_config_if_needed",
wraps=update_or_set_grouping_config_if_needed,
)
def test_ignores_sample_rate_if_current_config_is_invalid(self, update_config_spy: MagicMock):
with (
# Ensure our die roll will fall outside the sample rate
patch("sentry.grouping.ingest.config.random", return_value=0.1121),
override_options({"grouping.config_transition.config_upgrade_sample_rate": 0.0908}),
):
self.project.update_option("sentry:grouping_config", "not_a_real_config")
assert self.project.get_option("sentry:grouping_config") == "not_a_real_config"

save_new_event({"message": "Dogs are great!"}, self.project)

update_config_spy.assert_called_with(self.project, "ingest")

# The config has been updated, but no transition has started because we can't calculate
# a secondary hash using a config that doesn't exist
assert self.project.get_option("sentry:grouping_config") == DEFAULT_GROUPING_CONFIG
assert self.project.get_option("sentry:secondary_grouping_config") is None
assert self.project.get_option("sentry:secondary_grouping_expiry") == 0

@patch(
"sentry.event_manager.update_or_set_grouping_config_if_needed",
wraps=update_or_set_grouping_config_if_needed,
)
def test_ignores_sample_rate_if_no_record_exists(self, update_config_spy: MagicMock):
with (
# Ensure our die roll will fall outside the sample rate
patch("sentry.grouping.ingest.config.random", return_value=0.1121),
override_options({"grouping.config_transition.config_upgrade_sample_rate": 0.0908}),
):
assert self.project.get_option("sentry:grouping_config") == DEFAULT_GROUPING_CONFIG
assert not ProjectOption.objects.filter(
project_id=self.project.id, key="sentry:grouping_config"
).exists()

save_new_event({"message": "Dogs are great!"}, self.project)

update_config_spy.assert_called_with(self.project, "ingest")

# The config hasn't been updated, but now the project has its own record. No transition
# has started because the config was already up to date.
assert self.project.get_option("sentry:grouping_config") == DEFAULT_GROUPING_CONFIG
assert ProjectOption.objects.filter(
project_id=self.project.id, key="sentry:grouping_config"
).exists()
assert self.project.get_option("sentry:secondary_grouping_config") is None
assert self.project.get_option("sentry:secondary_grouping_expiry") == 0


class GroupHashCachingTest(TestCase):
@contextmanager
Expand Down
Loading
Loading