Files
2024-02-15 17:01:50 +00:00

2794 lines
104 KiB
Python

from __future__ import annotations
import abc
import time
import uuid
from datetime import datetime, timedelta, timezone
from hashlib import md5
from typing import Any
from unittest import mock
from unittest.mock import Mock, patch
import pytest
from django.db import router
from django.test import override_settings
from django.utils import timezone as django_timezone
from sentry import buffer
from sentry.buffer.redis import RedisBuffer
from sentry.eventstore.models import Event
from sentry.eventstore.processing import event_processing_store
from sentry.feedback.usecases.create_feedback import FeedbackCreationSource
from sentry.ingest.transaction_clusterer import ClustererNamespace
from sentry.integrations.mixins.commit_context import CommitInfo, FileBlameInfo
from sentry.issues.grouptype import (
FeedbackGroup,
GroupCategory,
PerformanceNPlusOneGroupType,
PerformanceP95EndpointRegressionGroupType,
ProfileFileIOGroupType,
)
from sentry.issues.ingest import save_issue_occurrence
from sentry.models.activity import Activity, ActivityIntegration
from sentry.models.group import GROUP_SUBSTATUS_TO_STATUS_MAP, Group, GroupStatus
from sentry.models.groupassignee import GroupAssignee
from sentry.models.groupinbox import GroupInbox, GroupInboxReason
from sentry.models.groupowner import (
ASSIGNEE_EXISTS_DURATION,
ASSIGNEE_EXISTS_KEY,
ISSUE_OWNERS_DEBOUNCE_DURATION,
ISSUE_OWNERS_DEBOUNCE_KEY,
GroupOwner,
GroupOwnerType,
)
from sentry.models.groupsnooze import GroupSnooze
from sentry.models.integrations.integration import Integration
from sentry.models.projectownership import ProjectOwnership
from sentry.models.projectteam import ProjectTeam
from sentry.ownership.grammar import Matcher, Owner, Rule, dump_schema
from sentry.replays.lib import kafka as replays_kafka
from sentry.replays.lib.kafka import clear_replay_publisher
from sentry.rules import init_registry
from sentry.rules.actions.base import EventAction
from sentry.services.hybrid_cloud.user.service import user_service
from sentry.silo import SiloMode, unguarded_write
from sentry.tasks.derive_code_mappings import SUPPORTED_LANGUAGES
from sentry.tasks.merge import merge_groups
from sentry.tasks.post_process import (
ISSUE_OWNERS_PER_PROJECT_PER_MIN_RATELIMIT,
feedback_filter_decorator,
locks,
post_process_group,
process_event,
run_post_process_job,
)
from sentry.testutils.cases import BaseTestCase, PerformanceIssueTestCase, SnubaTestCase, TestCase
from sentry.testutils.helpers import with_feature
from sentry.testutils.helpers.datetime import before_now, iso_format
from sentry.testutils.helpers.eventprocessing import write_event_to_cache
from sentry.testutils.helpers.options import override_options
from sentry.testutils.performance_issues.store_transaction import PerfIssueTransactionTestMixin
from sentry.testutils.silo import assume_test_silo_mode, region_silo_test
from sentry.testutils.skips import requires_snuba
from sentry.types.activity import ActivityType
from sentry.types.group import GroupSubStatus, PriorityLevel
from sentry.utils import json
from sentry.utils.cache import cache
from sentry.utils.sdk_crashes.sdk_crash_detection_config import SdkName
from tests.sentry.issues.test_utils import OccurrenceTestMixin
pytestmark = [requires_snuba]
class EventMatcher:
def __init__(self, expected, group=None):
self.expected = expected
self.expected_group = group
def __eq__(self, other):
matching_id = other.event_id == self.expected.event_id
if self.expected_group:
return (
matching_id
and self.expected_group == other.group
and self.expected_group.id == other.group_id
)
return matching_id
class BasePostProgressGroupMixin(BaseTestCase, metaclass=abc.ABCMeta):
@abc.abstractmethod
def create_event(self, data, project_id, assert_no_errors=True):
pass
@abc.abstractmethod
def call_post_process_group(
self, is_new, is_regression, is_new_group_environment, event, cache_key=None
):
pass
class CorePostProcessGroupTestMixin(BasePostProgressGroupMixin):
@patch("sentry.rules.processor.RuleProcessor")
@patch("sentry.tasks.servicehooks.process_service_hook")
@patch("sentry.tasks.sentry_apps.process_resource_change_bound.delay")
@patch("sentry.signals.event_processed.send_robust")
def test_issueless(
self,
mock_signal,
mock_process_resource_change_bound,
mock_process_service_hook,
mock_processor,
):
min_ago = iso_format(before_now(minutes=1))
event = self.store_event(
data={
"type": "transaction",
"timestamp": min_ago,
"start_timestamp": min_ago,
"contexts": {"trace": {"trace_id": "b" * 32, "span_id": "c" * 16, "op": ""}},
},
project_id=self.project.id,
)
cache_key = write_event_to_cache(event)
self.call_post_process_group(
is_new=True,
is_regression=False,
is_new_group_environment=True,
event=event,
cache_key=cache_key,
)
assert mock_processor.call_count == 0
assert mock_process_service_hook.call_count == 0
assert mock_process_resource_change_bound.call_count == 0
# transaction events do not call event.processed
assert mock_signal.call_count == 0
@patch("sentry.rules.processor.RuleProcessor")
def test_no_cache_abort(self, mock_processor):
event = self.create_event(data={}, project_id=self.project.id)
self.call_post_process_group(
is_new=True,
is_regression=False,
is_new_group_environment=True,
event=event,
cache_key="total-rubbish",
)
assert mock_processor.call_count == 0
def test_processing_cache_cleared(self):
event = self.create_event(data={}, project_id=self.project.id)
cache_key = self.call_post_process_group(
is_new=True,
is_regression=False,
is_new_group_environment=True,
event=event,
)
assert event_processing_store.get(cache_key) is None
def test_processing_cache_cleared_with_commits(self):
# Regression test to guard against suspect commit calculations breaking the
# cache
event = self.create_event(data={}, project_id=self.project.id)
self.create_commit(repo=self.create_repo())
cache_key = self.call_post_process_group(
is_new=True,
is_regression=False,
is_new_group_environment=True,
event=event,
)
assert event_processing_store.get(cache_key) is None
class DeriveCodeMappingsProcessGroupTestMixin(BasePostProgressGroupMixin):
def _create_event(
self,
data: dict[str, Any],
project_id: int | None = None,
) -> Event:
data.setdefault("platform", "javascript")
return self.store_event(data=data, project_id=project_id or self.project.id)
def _call_post_process_group(self, event: Event) -> None:
self.call_post_process_group(
is_new=True,
is_regression=False,
is_new_group_environment=True,
event=event,
)
@patch("sentry.tasks.derive_code_mappings.derive_code_mappings")
def test_derive_invalid_platform(self, mock_derive_code_mappings):
event = self._create_event({"platform": "elixir"})
self._call_post_process_group(event)
assert mock_derive_code_mappings.delay.call_count == 0
@patch("sentry.tasks.derive_code_mappings.derive_code_mappings")
def test_derive_supported_languages(self, mock_derive_code_mappings):
for platform in SUPPORTED_LANGUAGES:
event = self._create_event({"platform": platform})
self._call_post_process_group(event)
assert mock_derive_code_mappings.delay.call_count == 1
@patch("sentry.tasks.derive_code_mappings.derive_code_mappings")
def test_only_maps_a_given_project_once_per_hour(self, mock_derive_code_mappings):
dogs_project = self.create_project()
maisey_event = self._create_event(
{
"fingerprint": ["themaiseymasieydog"],
},
dogs_project.id,
)
charlie_event = self._create_event(
{
"fingerprint": ["charliebear"],
},
dogs_project.id,
)
cory_event = self._create_event(
{
"fingerprint": ["thenudge"],
},
dogs_project.id,
)
bodhi_event = self._create_event(
{
"fingerprint": ["theescapeartist"],
},
dogs_project.id,
)
self._call_post_process_group(maisey_event)
assert mock_derive_code_mappings.delay.call_count == 1
# second event from project should bail (no increase in call count)
self._call_post_process_group(charlie_event)
assert mock_derive_code_mappings.delay.call_count == 1
# advance the clock 59 minutes, and it should still bail
with patch("time.time", return_value=time.time() + 60 * 59):
self._call_post_process_group(cory_event)
assert mock_derive_code_mappings.delay.call_count == 1
# now advance the clock 61 minutes, and this time it should go through
with patch("time.time", return_value=time.time() + 60 * 61):
self._call_post_process_group(bodhi_event)
assert mock_derive_code_mappings.delay.call_count == 2
@patch("sentry.tasks.derive_code_mappings.derive_code_mappings")
def test_only_maps_a_given_issue_once_per_day(self, mock_derive_code_mappings):
dogs_project = self.create_project()
maisey_event1 = self._create_event(
{
"fingerprint": ["themaiseymaiseydog"],
},
dogs_project.id,
)
maisey_event2 = self._create_event(
{
"fingerprint": ["themaiseymaiseydog"],
},
dogs_project.id,
)
maisey_event3 = self._create_event(
{
"fingerprint": ["themaiseymaiseydog"],
},
dogs_project.id,
)
maisey_event4 = self._create_event(
{
"fingerprint": ["themaiseymaiseydog"],
},
dogs_project.id,
)
# because of the fingerprint, the events should always end up in the same group,
# but the rest of the test is bogus if they aren't, so let's be sure
assert maisey_event1.group_id == maisey_event2.group_id
assert maisey_event2.group_id == maisey_event3.group_id
assert maisey_event3.group_id == maisey_event4.group_id
self._call_post_process_group(maisey_event1)
assert mock_derive_code_mappings.delay.call_count == 1
# second event from group should bail (no increase in call count)
self._call_post_process_group(maisey_event2)
assert mock_derive_code_mappings.delay.call_count == 1
# advance the clock 23 hours and 59 minutes, and it should still bail
with patch("time.time", return_value=time.time() + (60 * 60 * 23) + (60 * 59)):
self._call_post_process_group(maisey_event3)
assert mock_derive_code_mappings.delay.call_count == 1
# now advance the clock 24 hours and 1 minute, and this time it should go through
with patch("time.time", return_value=time.time() + (60 * 60 * 24) + (60 * 1)):
self._call_post_process_group(maisey_event4)
assert mock_derive_code_mappings.delay.call_count == 2
@patch("sentry.tasks.derive_code_mappings.derive_code_mappings")
def test_skipping_an_issue_doesnt_mark_it_processed(self, mock_derive_code_mappings):
dogs_project = self.create_project()
maisey_event = self._create_event(
{
"fingerprint": ["themaiseymasieydog"],
},
dogs_project.id,
)
charlie_event1 = self._create_event(
{
"fingerprint": ["charliebear"],
},
dogs_project.id,
)
charlie_event2 = self._create_event(
{
"fingerprint": ["charliebear"],
},
dogs_project.id,
)
# because of the fingerprint, the two Charlie events should always end up in the same group,
# but the rest of the test is bogus if they aren't, so let's be sure
assert charlie_event1.group_id == charlie_event2.group_id
self._call_post_process_group(maisey_event)
assert mock_derive_code_mappings.delay.call_count == 1
# second event from project should bail (no increase in call count)
self._call_post_process_group(charlie_event1)
assert mock_derive_code_mappings.delay.call_count == 1
# now advance the clock 61 minutes (so the project should clear the cache), and another
# event from the Charlie group should go through
with patch("time.time", return_value=time.time() + 60 * 61):
self._call_post_process_group(charlie_event2)
assert mock_derive_code_mappings.delay.call_count == 2
class RuleProcessorTestMixin(BasePostProgressGroupMixin):
@patch("sentry.rules.processor.RuleProcessor")
def test_rule_processor_backwards_compat(self, mock_processor):
event = self.create_event(data={}, project_id=self.project.id)
mock_callback = Mock()
mock_futures = [Mock()]
mock_processor.return_value.apply.return_value = [(mock_callback, mock_futures)]
self.call_post_process_group(
is_new=True,
is_regression=False,
is_new_group_environment=True,
event=event,
)
mock_processor.assert_called_once_with(EventMatcher(event), True, False, True, False, False)
mock_processor.return_value.apply.assert_called_once_with()
mock_callback.assert_called_once_with(EventMatcher(event), mock_futures)
@patch("sentry.rules.processor.RuleProcessor")
def test_rule_processor(self, mock_processor):
event = self.create_event(data={"message": "testing"}, project_id=self.project.id)
mock_callback = Mock()
mock_futures = [Mock()]
mock_processor.return_value.apply.return_value = [(mock_callback, mock_futures)]
self.call_post_process_group(
is_new=True,
is_regression=False,
is_new_group_environment=True,
event=event,
)
mock_processor.return_value.apply.assert_called_once_with()
mock_callback.assert_called_once_with(EventMatcher(event), mock_futures)
def test_rule_processor_buffer_values(self):
# Test that pending buffer values for `times_seen` are applied to the group and that alerts
# fire as expected
from sentry.models.rule import Rule
MOCK_RULES = ("sentry.rules.filters.issue_occurrences.IssueOccurrencesFilter",)
redis_buffer = RedisBuffer()
with mock.patch("sentry.buffer.backend.get", redis_buffer.get), mock.patch(
"sentry.buffer.backend.incr", redis_buffer.incr
), patch("sentry.constants._SENTRY_RULES", MOCK_RULES), patch(
"sentry.rules.processor.rules", init_registry()
) as rules:
MockAction = mock.Mock()
MockAction.id = "tests.sentry.tasks.post_process.tests.MockAction"
MockAction.return_value = mock.Mock(spec=EventAction)
MockAction.return_value.after.return_value = []
rules.add(MockAction)
conditions = [
{
"id": "sentry.rules.filters.issue_occurrences.IssueOccurrencesFilter",
"value": 10,
},
]
actions = [{"id": "tests.sentry.tasks.post_process.tests.MockAction"}]
Rule.objects.filter(project=self.project).delete()
Rule.objects.create(
project=self.project, data={"conditions": conditions, "actions": actions}
)
event = self.create_event(
data={"message": "testing", "fingerprint": ["group-1"]}, project_id=self.project.id
)
event_2 = self.create_event(
data={"message": "testing", "fingerprint": ["group-1"]}, project_id=self.project.id
)
self.call_post_process_group(
is_new=True,
is_regression=False,
is_new_group_environment=True,
event=event,
)
event.group.update(times_seen=2)
assert MockAction.return_value.after.call_count == 0
buffer.backend.incr(Group, {"times_seen": 15}, filters={"id": event.group.id})
self.call_post_process_group(
is_new=True,
is_regression=False,
is_new_group_environment=True,
event=event_2,
)
assert MockAction.return_value.after.call_count == 1
@patch("sentry.rules.processor.RuleProcessor")
def test_group_refresh(self, mock_processor):
event = self.create_event(data={"message": "testing"}, project_id=self.project.id)
group1 = event.group
group2 = self.create_group(project=self.project)
assert event.group_id == group1.id
assert event.group == group1
with self.tasks():
merge_groups([group1.id], group2.id)
mock_callback = Mock()
mock_futures = [Mock()]
mock_processor.return_value.apply.return_value = [(mock_callback, mock_futures)]
self.call_post_process_group(
is_new=True,
is_regression=False,
is_new_group_environment=True,
event=event,
)
# Ensure that rule processing sees the merged group.
mock_processor.assert_called_with(
EventMatcher(event, group=group2), True, False, True, False, False
)
@patch("sentry.rules.processor.RuleProcessor")
def test_group_last_seen_buffer(self, mock_processor):
first_event_date = datetime.now(timezone.utc) - timedelta(days=90)
event1 = self.create_event(
data={"message": "testing"},
project_id=self.project.id,
)
group1 = event1.group
group1.update(last_seen=first_event_date)
event2 = self.create_event(data={"message": "testing"}, project_id=self.project.id)
# Mock set the last_seen value to the first event date
# To simulate the update to last_seen being buffered
event2.group.last_seen = first_event_date
event2.group.update(last_seen=first_event_date)
assert event2.group_id == group1.id
mock_callback = Mock()
mock_futures = [Mock()]
mock_processor.return_value.apply.return_value = [(mock_callback, mock_futures)]
self.call_post_process_group(
is_new=False,
is_regression=True,
is_new_group_environment=False,
event=event2,
)
mock_processor.assert_called_with(
EventMatcher(event2, group=group1), False, True, False, False, False
)
sent_group_date = mock_processor.call_args[0][0].group.last_seen
# Check that last_seen was updated to be at least the new event's date
self.assertAlmostEqual(sent_group_date, event2.datetime, delta=timedelta(seconds=10))
class ServiceHooksTestMixin(BasePostProgressGroupMixin):
@patch("sentry.tasks.servicehooks.process_service_hook")
def test_service_hook_fires_on_new_event(self, mock_process_service_hook):
event = self.create_event(data={}, project_id=self.project.id)
hook = self.create_service_hook(
project=self.project,
organization=self.project.organization,
actor=self.user,
events=["event.created"],
)
with self.feature("projects:servicehooks"):
self.call_post_process_group(
is_new=False,
is_regression=False,
is_new_group_environment=False,
event=event,
)
mock_process_service_hook.delay.assert_called_once_with(
servicehook_id=hook.id, event=EventMatcher(event)
)
@patch("sentry.tasks.servicehooks.process_service_hook")
@patch("sentry.rules.processor.RuleProcessor")
def test_service_hook_fires_on_alert(self, mock_processor, mock_process_service_hook):
event = self.create_event(data={}, project_id=self.project.id)
mock_callback = Mock()
mock_futures = [Mock()]
mock_processor.return_value.apply.return_value = [(mock_callback, mock_futures)]
hook = self.create_service_hook(
project=self.project,
organization=self.project.organization,
actor=self.user,
events=["event.alert"],
)
with self.feature("projects:servicehooks"):
self.call_post_process_group(
is_new=False,
is_regression=False,
is_new_group_environment=False,
event=event,
)
mock_process_service_hook.delay.assert_called_once_with(
servicehook_id=hook.id, event=EventMatcher(event)
)
@patch("sentry.tasks.servicehooks.process_service_hook")
@patch("sentry.rules.processor.RuleProcessor")
def test_service_hook_does_not_fire_without_alert(
self, mock_processor, mock_process_service_hook
):
event = self.create_event(data={}, project_id=self.project.id)
mock_processor.return_value.apply.return_value = []
self.create_service_hook(
project=self.project,
organization=self.project.organization,
actor=self.user,
events=["event.alert"],
)
with self.feature("projects:servicehooks"):
self.call_post_process_group(
is_new=False,
is_regression=False,
is_new_group_environment=False,
event=event,
)
assert not mock_process_service_hook.delay.mock_calls
@patch("sentry.tasks.servicehooks.process_service_hook")
def test_service_hook_does_not_fire_without_event(self, mock_process_service_hook):
event = self.create_event(data={}, project_id=self.project.id)
self.create_service_hook(
project=self.project, organization=self.project.organization, actor=self.user, events=[]
)
with self.feature("projects:servicehooks"):
self.call_post_process_group(
is_new=True,
is_regression=False,
is_new_group_environment=False,
event=event,
)
assert not mock_process_service_hook.delay.mock_calls
class ResourceChangeBoundsTestMixin(BasePostProgressGroupMixin):
@patch("sentry.tasks.sentry_apps.process_resource_change_bound.delay")
def test_processes_resource_change_task_on_new_group(self, delay):
event = self.create_event(data={}, project_id=self.project.id)
group = event.group
self.call_post_process_group(
is_new=True,
is_regression=False,
is_new_group_environment=False,
event=event,
)
delay.assert_called_once_with(action="created", sender="Group", instance_id=group.id)
@with_feature("organizations:integrations-event-hooks")
@patch("sentry.tasks.sentry_apps.process_resource_change_bound.delay")
def test_processes_resource_change_task_on_error_events(self, delay):
event = self.create_event(
data={
"message": "Foo bar",
"exception": {"type": "Foo", "value": "oh no"},
"level": "error",
"timestamp": iso_format(django_timezone.now()),
},
project_id=self.project.id,
assert_no_errors=False,
)
self.create_service_hook(
project=self.project,
organization=self.project.organization,
actor=self.user,
events=["error.created"],
)
self.call_post_process_group(
is_new=False,
is_regression=False,
is_new_group_environment=False,
event=event,
)
delay.assert_called_once_with(
action="created",
sender="Error",
instance_id=event.event_id,
instance=EventMatcher(event),
)
@with_feature("organizations:integrations-event-hooks")
@patch("sentry.tasks.sentry_apps.process_resource_change_bound.delay")
def test_processes_resource_change_task_not_called_for_non_errors(self, delay):
event = self.create_event(
data={
"message": "Foo bar",
"level": "info",
"timestamp": iso_format(django_timezone.now()),
},
project_id=self.project.id,
assert_no_errors=False,
)
self.call_post_process_group(
is_new=False,
is_regression=False,
is_new_group_environment=False,
event=event,
)
assert not delay.called
@patch("sentry.tasks.sentry_apps.process_resource_change_bound.delay")
def test_processes_resource_change_task_not_called_without_feature_flag(self, delay):
event = self.create_event(
data={
"message": "Foo bar",
"level": "info",
"timestamp": iso_format(django_timezone.now()),
},
project_id=self.project.id,
assert_no_errors=False,
)
self.call_post_process_group(
is_new=False,
is_regression=False,
is_new_group_environment=False,
event=event,
)
assert not delay.called
@with_feature("organizations:integrations-event-hooks")
@patch("sentry.tasks.sentry_apps.process_resource_change_bound.delay")
def test_processes_resource_change_task_not_called_without_error_created(self, delay):
event = self.create_event(
data={
"message": "Foo bar",
"level": "error",
"exception": {"type": "Foo", "value": "oh no"},
"timestamp": iso_format(django_timezone.now()),
},
project_id=self.project.id,
assert_no_errors=False,
)
self.create_service_hook(
project=self.project, organization=self.project.organization, actor=self.user, events=[]
)
self.call_post_process_group(
is_new=False,
is_regression=False,
is_new_group_environment=False,
event=event,
)
assert not delay.called
class InboxTestMixin(BasePostProgressGroupMixin):
@patch("sentry.rules.processor.RuleProcessor")
def test_group_inbox_regression(self, mock_processor):
new_event = self.create_event(data={"message": "testing"}, project_id=self.project.id)
group = new_event.group
assert group.status == GroupStatus.UNRESOLVED
assert group.substatus == GroupSubStatus.ONGOING
self.call_post_process_group(
is_new=True,
is_regression=True,
is_new_group_environment=False,
event=new_event,
)
assert GroupInbox.objects.filter(group=group, reason=GroupInboxReason.NEW.value).exists()
GroupInbox.objects.filter(
group=group
).delete() # Delete so it creates the .REGRESSION entry.
group.refresh_from_db()
assert group.status == GroupStatus.UNRESOLVED
assert group.substatus == GroupSubStatus.NEW
mock_processor.assert_called_with(EventMatcher(new_event), True, True, False, False, False)
# resolve the new issue so regression actually happens
group.status = GroupStatus.RESOLVED
group.substatus = None
group.active_at = group.active_at - timedelta(minutes=1)
group.save(update_fields=["status", "substatus", "active_at"])
# trigger a transition from resolved to regressed by firing an event that groups to that issue
regressed_event = self.create_event(data={"message": "testing"}, project_id=self.project.id)
assert regressed_event.group == new_event.group
group = regressed_event.group
group.refresh_from_db()
assert group.status == GroupStatus.UNRESOLVED
assert group.substatus == GroupSubStatus.REGRESSED
self.call_post_process_group(
is_new=False,
is_regression=True,
is_new_group_environment=False,
event=regressed_event,
)
mock_processor.assert_called_with(
EventMatcher(regressed_event), False, True, False, False, False
)
group.refresh_from_db()
assert group.status == GroupStatus.UNRESOLVED
assert group.substatus == GroupSubStatus.REGRESSED
assert GroupInbox.objects.filter(
group=group, reason=GroupInboxReason.REGRESSION.value
).exists()
class AssignmentTestMixin(BasePostProgressGroupMixin):
def make_ownership(self, extra_rules=None):
self.user_2 = self.create_user()
self.create_team_membership(team=self.team, user=self.user_2)
rules = [
Rule(Matcher("path", "src/app/*"), [Owner("team", self.team.name)]),
Rule(Matcher("path", "src/*"), [Owner("user", self.user.email)]),
Rule(Matcher("path", "tests/*"), [Owner("user", self.user_2.email)]),
]
if extra_rules:
rules.extend(extra_rules)
self.prj_ownership = ProjectOwnership.objects.create(
project_id=self.project.id,
schema=dump_schema(rules),
fallthrough=True,
auto_assignment=True,
)
def test_owner_assignment_order_precedence(self):
self.make_ownership()
event = self.create_event(
data={
"message": "oh no",
"platform": "python",
"stacktrace": {"frames": [{"filename": "src/app/example.py"}]},
},
project_id=self.project.id,
)
self.call_post_process_group(
is_new=False,
is_regression=False,
is_new_group_environment=False,
event=event,
)
assignee = event.group.assignee_set.first()
assert assignee.user_id == self.user.id
assert assignee.team is None
owners = list(GroupOwner.objects.filter(group=event.group))
assert len(owners) == 2
assert {(self.user.id, None), (None, self.team.id)} == {
(o.user_id, o.team_id) for o in owners
}
activity = Activity.objects.filter(group=event.group).first()
assert activity.data == {
"assignee": str(self.user.id),
"assigneeEmail": self.user.email,
"assigneeType": "user",
"integration": ActivityIntegration.PROJECT_OWNERSHIP.value,
"rule": str(Rule(Matcher("path", "src/*"), [Owner("user", self.user.email)])),
}
def test_owner_assignment_extra_groups(self):
extra_user = self.create_user()
self.create_team_membership(self.team, user=extra_user)
self.make_ownership(
[Rule(Matcher("path", "src/app/things/in/*"), [Owner("user", extra_user.email)])],
)
event = self.create_event(
data={
"message": "oh no",
"platform": "python",
"stacktrace": {"frames": [{"filename": "src/app/things/in/a/path/example2.py"}]},
},
project_id=self.project.id,
)
self.call_post_process_group(
is_new=False,
is_regression=False,
is_new_group_environment=False,
event=event,
)
assignee = event.group.assignee_set.first()
assert assignee.user_id == extra_user.id
assert assignee.team is None
owners = list(GroupOwner.objects.filter(group=event.group))
assert len(owners) == 2
assert {(extra_user.id, None), (self.user.id, None)} == {
(o.user_id, o.team_id) for o in owners
}
def test_owner_assignment_existing_owners(self):
extra_team = self.create_team()
ProjectTeam.objects.create(team=extra_team, project=self.project)
self.make_ownership(
[Rule(Matcher("path", "src/app/things/in/*"), [Owner("team", extra_team.slug)])],
)
GroupOwner.objects.create(
group=self.group,
project=self.project,
organization=self.organization,
user_id=self.user.id,
type=GroupOwnerType.OWNERSHIP_RULE.value,
)
event = self.create_event(
data={
"message": "oh no",
"platform": "python",
"stacktrace": {"frames": [{"filename": "src/app/things/in/a/path/example2.py"}]},
},
project_id=self.project.id,
)
self.call_post_process_group(
is_new=False,
is_regression=False,
is_new_group_environment=False,
event=event,
)
assignee = event.group.assignee_set.first()
assert assignee.user_id is None
assert assignee.team == extra_team
owners = list(GroupOwner.objects.filter(group=event.group))
assert {(None, extra_team.id), (self.user.id, None)} == {
(o.user_id, o.team_id) for o in owners
}
def test_owner_assignment_assign_user(self):
self.make_ownership()
event = self.create_event(
data={
"message": "oh no",
"platform": "python",
"stacktrace": {"frames": [{"filename": "src/app.py"}]},
},
project_id=self.project.id,
)
self.call_post_process_group(
is_new=False,
is_regression=False,
is_new_group_environment=False,
event=event,
)
assignee = event.group.assignee_set.first()
assert assignee.user_id == self.user.id
assert assignee.team is None
def test_owner_assignment_ownership_no_matching_owners(self):
event = self.create_event(
data={
"message": "oh no",
"platform": "python",
"stacktrace": {"frames": [{"filename": "src/app/example.py"}]},
},
project_id=self.project.id,
)
self.call_post_process_group(
is_new=False,
is_regression=False,
is_new_group_environment=False,
event=event,
)
assert not event.group.assignee_set.exists()
def test_owner_assignment_existing_assignment(self):
self.make_ownership()
event = self.create_event(
data={
"message": "oh no",
"platform": "python",
"stacktrace": {"frames": [{"filename": "src/app/example.py"}]},
},
project_id=self.project.id,
)
event.group.assignee_set.create(team=self.team, project=self.project)
self.call_post_process_group(
is_new=False,
is_regression=False,
is_new_group_environment=False,
event=event,
)
assignee = event.group.assignee_set.first()
assert assignee.user_id is None
assert assignee.team == self.team
def test_only_first_assignment_works(self):
self.make_ownership()
event = self.create_event(
data={
"message": "oh no",
"platform": "python",
"stacktrace": {"frames": [{"filename": "src/app/example.py"}]},
"fingerprint": ["group1"],
},
project_id=self.project.id,
)
self.call_post_process_group(
is_new=False,
is_regression=False,
is_new_group_environment=False,
event=event,
)
assignee = event.group.assignee_set.first()
assert assignee.user_id == self.user.id
assert assignee.team is None
event = self.create_event(
data={
"message": "oh no",
"platform": "python",
"stacktrace": {"frames": [{"filename": "tests/src/app/test_example.py"}]},
"fingerprint": ["group1"],
},
project_id=self.project.id,
)
self.call_post_process_group(
is_new=False,
is_regression=False,
is_new_group_environment=False,
event=event,
)
assignee = event.group.assignee_set.first()
# Assignment shouldn't change.
assert assignee.user_id == self.user.id
assert assignee.team is None
def test_owner_assignment_owner_is_gone(self):
self.make_ownership()
# Remove the team so the rule match will fail to resolve
self.team.delete()
event = self.create_event(
data={
"message": "oh no",
"platform": "python",
"stacktrace": {"frames": [{"filename": "src/app/example.py"}]},
},
project_id=self.project.id,
)
self.call_post_process_group(
is_new=False,
is_regression=False,
is_new_group_environment=False,
event=event,
)
assignee = event.group.assignee_set.first()
assert assignee is None
def test_suspect_committer_affect_cache_debouncing_issue_owners_calculations(self):
self.make_ownership()
committer = GroupOwner(
group=self.created_event.group,
project=self.created_event.project,
organization=self.created_event.project.organization,
type=GroupOwnerType.SUSPECT_COMMIT.value,
)
committer.save()
event = self.create_event(
data={
"message": "oh no",
"platform": "python",
"stacktrace": {"frames": [{"filename": "src/app/example.py"}]},
},
project_id=self.project.id,
)
event.group.assignee_set.create(team=self.team, project=self.project)
self.call_post_process_group(
is_new=False,
is_regression=False,
is_new_group_environment=False,
event=event,
)
assignee = event.group.assignee_set.first()
assert assignee.user_id is None
assert assignee.team == self.team
def test_owner_assignment_when_owners_have_been_unassigned(self):
"""
Test that ensures that if certain assignees get unassigned, and project rules are changed
then the new group assignees should be re-calculated and re-assigned
"""
# Create rules and check assignees
self.make_ownership()
event = self.create_event(
data={
"message": "oh no",
"platform": "python",
"stacktrace": {"frames": [{"filename": "src/app/example.py"}]},
},
project_id=self.project.id,
)
event_2 = self.create_event(
data={
"message": "Exception",
"platform": "python",
"stacktrace": {"frames": [{"filename": "src/app/integration.py"}]},
},
project_id=self.project.id,
)
self.call_post_process_group(
is_new=False,
is_regression=False,
is_new_group_environment=False,
event=event,
)
self.call_post_process_group(
is_new=False,
is_regression=False,
is_new_group_environment=False,
event=event_2,
)
assignee = event.group.assignee_set.first()
assert assignee.user_id == self.user.id
user_3 = self.create_user()
self.create_team_membership(self.team, user=user_3)
# De-assign group assignees
GroupAssignee.objects.deassign(event.group, self.user)
assert event.group.assignee_set.first() is None
# Change ProjectOwnership rules
rules = [
Rule(Matcher("path", "src/*"), [Owner("user", user_3.email)]),
]
self.prj_ownership.schema = dump_schema(rules)
self.prj_ownership.save()
self.call_post_process_group(
is_new=False,
is_regression=False,
is_new_group_environment=False,
event=event,
)
self.call_post_process_group(
is_new=False,
is_regression=False,
is_new_group_environment=False,
event=event_2,
)
# Group should be re-assigned to the new group owner
assignee = event.group.assignee_set.first()
assert assignee.user_id == user_3.id
# De-assign group assignees
GroupAssignee.objects.deassign(event.group, user_service.get_user(user_id=assignee.user_id))
assert event.group.assignee_set.first() is None
user_4 = self.create_user()
self.create_team_membership(self.team, user=user_4)
self.prj_ownership.schema = dump_schema([])
self.prj_ownership.save()
code_owners_rule = Rule(
Matcher("codeowners", "*.py"),
[Owner("user", user_4.email)],
)
self.code_mapping = self.create_code_mapping(project=self.project)
self.code_owners = self.create_codeowners(
self.project,
self.code_mapping,
schema=dump_schema([code_owners_rule]),
)
self.call_post_process_group(
is_new=False,
is_regression=False,
is_new_group_environment=False,
event=event,
)
self.call_post_process_group(
is_new=False,
is_regression=False,
is_new_group_environment=False,
event=event_2,
)
# Group should be re-assigned to the new group owner
assignee = event.group.assignee_set.first()
assert assignee.user_id == user_4.id
def test_auto_assignment_when_owners_have_been_unassigned(self):
"""
Test that ensures that if assignee gets unassigned and project rules are changed,
then the new group assignees should be re-calculated and re-assigned
"""
# Create rules and check assignees
self.make_ownership()
event = self.create_event(
data={
"message": "oh no",
"platform": "python",
"stacktrace": {"frames": [{"filename": "src/app/example.py"}]},
},
project_id=self.project.id,
)
self.call_post_process_group(
is_new=False,
is_regression=False,
is_new_group_environment=False,
event=event,
)
assignee = (
GroupOwner.objects.filter()
.exclude(user_id__isnull=True, team_id__isnull=True)
.order_by("type")
.first()
)
assert assignee.user_id == self.user.id
user_3 = self.create_user()
self.create_team_membership(self.team, user=user_3)
# Set assignee_exists cache to self.user
cache.set(ASSIGNEE_EXISTS_KEY(event.group_id), self.user, ASSIGNEE_EXISTS_DURATION)
# De-assign group assignees
GroupAssignee.objects.deassign(event.group, self.user)
assert event.group.assignee_set.first() is None
# Change ProjectOwnership rules
rules = [
Rule(Matcher("path", "src/*"), [Owner("user", user_3.email)]),
]
self.prj_ownership.schema = dump_schema(rules)
self.prj_ownership.save()
self.call_post_process_group(
is_new=False,
is_regression=False,
is_new_group_environment=False,
event=event,
)
# Mimic filter used in get_autoassigned_owner_cached to get the issue owner to be
# auto-assigned
assignee = (
GroupOwner.objects.filter()
.exclude(user_id__isnull=True, team_id__isnull=True)
.order_by("type")
.first()
)
# Group should be re-assigned to the new group owner
assert assignee.user_id == user_3.id
def test_ensure_when_assignees_and_owners_are_cached_does_not_cause_unbound_errors(self):
self.make_ownership()
event = self.create_event(
data={
"message": "oh no",
"platform": "python",
"stacktrace": {"frames": [{"filename": "src/app.py"}]},
},
project_id=self.project.id,
)
assignee_cache_key = "assignee_exists:1:%s" % event.group.id
owner_cache_key = "owner_exists:1:%s" % event.group.id
for key in [assignee_cache_key, owner_cache_key]:
cache.set(key, True)
self.call_post_process_group(
is_new=False,
is_regression=False,
is_new_group_environment=False,
event=event,
)
def test_auto_assignment_when_owners_are_invalid(self):
"""
Test that invalid group owners (that exist due to bugs) are deleted and not assigned
when no valid issue owner exists
"""
event = self.create_event(
data={
"message": "oh no",
"platform": "python",
"stacktrace": {"frames": [{"filename": "src/app/example.py"}]},
},
project_id=self.project.id,
)
# Hard code an invalid group owner
invalid_codeowner = GroupOwner(
group=event.group,
project=event.project,
organization=event.project.organization,
type=GroupOwnerType.CODEOWNERS.value,
context={"rule": "codeowners:/**/*.css " + self.user.email},
user_id=self.user.id,
)
invalid_codeowner.save()
self.call_post_process_group(
is_new=False,
is_regression=False,
is_new_group_environment=False,
event=event,
)
assignee = event.group.assignee_set.first()
assert assignee is None
assert len(GroupOwner.objects.filter(group_id=event.group)) == 0
@patch("sentry.tasks.post_process.logger")
def test_debounces_handle_owner_assignments(self, logger):
self.make_ownership()
event = self.create_event(
data={
"message": "oh no",
"platform": "python",
"stacktrace": {"frames": [{"filename": "src/app.py"}]},
},
project_id=self.project.id,
)
cache.set(ISSUE_OWNERS_DEBOUNCE_KEY(event.group_id), True, ISSUE_OWNERS_DEBOUNCE_DURATION)
self.call_post_process_group(
is_new=False,
is_regression=False,
is_new_group_environment=False,
event=event,
)
logger.info.assert_any_call(
"handle_owner_assignment.issue_owners_exist",
extra={
"event": event.event_id,
"group": event.group_id,
"project": event.project_id,
"organization": event.project.organization_id,
"reason": "issue_owners_exist",
},
)
@patch("sentry.tasks.post_process.logger")
def test_issue_owners_should_ratelimit(self, logger):
cache.set(
f"issue_owner_assignment_ratelimiter:{self.project.id}",
(set(range(0, ISSUE_OWNERS_PER_PROJECT_PER_MIN_RATELIMIT * 10, 10)), datetime.now()),
)
cache.set(f"commit-context-scm-integration:{self.project.organization_id}", True, 60)
event = self.create_event(
data={
"message": "oh no",
"platform": "python",
"stacktrace": {"frames": [{"filename": "src/app.py"}]},
},
project_id=self.project.id,
)
self.call_post_process_group(
is_new=False,
is_regression=False,
is_new_group_environment=False,
event=event,
)
logger.info.assert_any_call(
"handle_owner_assignment.ratelimited",
extra={
"event": event.event_id,
"group": event.group_id,
"project": event.project_id,
"organization": event.project.organization_id,
"reason": "ratelimited",
},
)
class ProcessCommitsTestMixin(BasePostProgressGroupMixin):
github_blame_return_value = {
"commitId": "asdfwreqr",
"committedDate": (datetime.now(timezone.utc) - timedelta(days=2)),
"commitMessage": "placeholder commit message",
"commitAuthorName": "",
"commitAuthorEmail": "admin@localhost",
}
def setUp(self):
self.created_event = self.create_event(
data={
"message": "Kaboom!",
"platform": "python",
"timestamp": iso_format(before_now(seconds=10)),
"stacktrace": {
"frames": [
{
"function": "handle_set_commits",
"abs_path": "/usr/src/sentry/src/sentry/tasks.py",
"module": "sentry.tasks",
"in_app": False,
"lineno": 30,
"filename": "sentry/tasks.py",
},
{
"function": "set_commits",
"abs_path": "/usr/src/sentry/src/sentry/models/release.py",
"module": "sentry.models.release",
"in_app": True,
"lineno": 39,
"filename": "sentry/models/release.py",
},
]
},
"fingerprint": ["put-me-in-the-control-group"],
},
project_id=self.project.id,
)
self.cache_key = write_event_to_cache(self.created_event)
self.repo = self.create_repo(
name="org/example", integration_id=self.integration.id, provider="integrations:github"
)
self.code_mapping = self.create_code_mapping(
repo=self.repo, project=self.project, stack_root="sentry/", source_root="sentry/"
)
self.commit_author = self.create_commit_author(project=self.project, user=self.user)
self.commit = self.create_commit(
project=self.project,
repo=self.repo,
author=self.commit_author,
key="asdfwreqr",
message="placeholder commit message",
)
self.github_blame_all_files_return_value = [
FileBlameInfo(
code_mapping=self.code_mapping,
lineno=39,
path="sentry/models/release.py",
ref="master",
repo=self.repo,
commit=CommitInfo(
commitId="asdfwreqr",
committedDate=(datetime.now(timezone.utc) - timedelta(days=2)),
commitMessage="placeholder commit message",
commitAuthorName="",
commitAuthorEmail="admin@localhost",
),
)
]
@with_feature("organizations:commit-context")
@patch(
"sentry.integrations.github.GitHubIntegration.get_commit_context_all_frames",
return_value=github_blame_return_value,
)
def test_logic_fallback_no_scm(self, mock_get_commit_context):
with assume_test_silo_mode(SiloMode.CONTROL):
with unguarded_write(using=router.db_for_write(Integration)):
Integration.objects.all().delete()
integration = self.create_provider_integration(provider="bitbucket")
integration.add_organization(self.organization)
with self.tasks():
self.call_post_process_group(
is_new=True,
is_regression=False,
is_new_group_environment=True,
event=self.created_event,
)
assert not mock_get_commit_context.called
@with_feature("organizations:commit-context")
@patch(
"sentry.integrations.github_enterprise.GitHubEnterpriseIntegration.get_commit_context_all_frames",
)
def test_github_enterprise(self, mock_get_commit_context):
mock_get_commit_context.return_value = self.github_blame_all_files_return_value
with assume_test_silo_mode(SiloMode.CONTROL):
with unguarded_write(using=router.db_for_write(Integration)):
Integration.objects.all().delete()
integration = self.create_provider_integration(
external_id="35.232.149.196:12345",
provider="github_enterprise",
metadata={
"domain_name": "35.232.149.196/baxterthehacker",
"installation_id": "12345",
"installation": {"id": "2", "private_key": "private_key", "verify_ssl": True},
},
)
organization_integration = integration.add_organization(self.organization)
self.repo.update(integration_id=integration.id, provider="integrations:github_enterprise")
self.code_mapping.update(organization_integration_id=organization_integration.id)
with self.tasks():
self.call_post_process_group(
is_new=True,
is_regression=False,
is_new_group_environment=True,
event=self.created_event,
)
assert GroupOwner.objects.get(
group=self.created_event.group,
project=self.created_event.project,
organization=self.created_event.project.organization,
type=GroupOwnerType.SUSPECT_COMMIT.value,
)
@with_feature("organizations:commit-context")
@patch("sentry.integrations.github.GitHubIntegration.get_commit_context_all_frames")
def test_skip_when_not_is_new(self, mock_get_commit_context):
"""
Tests that we do not process commit context if the group isn't new.
"""
with self.tasks():
self.call_post_process_group(
is_new=False,
is_regression=False,
is_new_group_environment=True,
event=self.created_event,
)
assert not mock_get_commit_context.called
assert not GroupOwner.objects.filter(
group=self.created_event.group,
project=self.created_event.project,
organization=self.created_event.project.organization,
type=GroupOwnerType.SUSPECT_COMMIT.value,
).exists()
@with_feature("organizations:commit-context")
@patch(
"sentry.integrations.github.GitHubIntegration.get_commit_context_all_frames",
)
def test_does_not_skip_when_is_new(self, mock_get_commit_context):
"""
Tests that the commit context should be processed when the group is new.
"""
mock_get_commit_context.return_value = self.github_blame_all_files_return_value
with self.tasks():
self.call_post_process_group(
is_new=True,
is_regression=False,
is_new_group_environment=True,
event=self.created_event,
)
assert mock_get_commit_context.called
assert GroupOwner.objects.get(
group=self.created_event.group,
project=self.created_event.project,
organization=self.created_event.project.organization,
type=GroupOwnerType.SUSPECT_COMMIT.value,
)
class SnoozeTestSkipSnoozeMixin(BasePostProgressGroupMixin):
@patch("sentry.signals.issue_unignored.send_robust")
@patch("sentry.rules.processor.RuleProcessor")
def test_invalidates_snooze_issue_platform(self, mock_processor, mock_send_unignored_robust):
event = self.create_event(data={"message": "testing"}, project_id=self.project.id)
group = event.group
should_detect_escalation = group.issue_type.should_detect_escalation(
self.project.organization
)
# Check for has_reappeared=False if is_new=True
self.call_post_process_group(
is_new=True,
is_regression=False,
is_new_group_environment=True,
event=event,
)
assert GroupInbox.objects.filter(group=group, reason=GroupInboxReason.NEW.value).exists()
GroupInbox.objects.filter(group=group).delete() # Delete so it creates the UNIGNORED entry.
Activity.objects.filter(group=group).delete()
mock_processor.assert_called_with(EventMatcher(event), True, False, True, False, False)
event = self.create_event(data={"message": "testing"}, project_id=self.project.id)
group.status = GroupStatus.IGNORED
group.substatus = GroupSubStatus.UNTIL_CONDITION_MET
group.save(update_fields=["status", "substatus"])
snooze = GroupSnooze.objects.create(
group=group, until=django_timezone.now() - timedelta(hours=1)
)
# Check for has_reappeared=True if is_new=False
self.call_post_process_group(
is_new=False,
is_regression=False,
is_new_group_environment=True,
event=event,
)
mock_processor.assert_called_with(EventMatcher(event), False, False, True, True, False)
if should_detect_escalation:
assert not GroupSnooze.objects.filter(id=snooze.id).exists()
else:
assert GroupSnooze.objects.filter(id=snooze.id).exists()
group.refresh_from_db()
if should_detect_escalation:
assert group.status == GroupStatus.UNRESOLVED
assert group.substatus == GroupSubStatus.ONGOING
assert GroupInbox.objects.filter(
group=group, reason=GroupInboxReason.ONGOING.value
).exists()
assert Activity.objects.filter(
group=group, project=group.project, type=ActivityType.SET_UNRESOLVED.value
).exists()
assert mock_send_unignored_robust.called
else:
assert group.status == GroupStatus.IGNORED
assert group.substatus == GroupSubStatus.UNTIL_CONDITION_MET
assert not GroupInbox.objects.filter(
group=group, reason=GroupInboxReason.ESCALATING.value
).exists()
assert not Activity.objects.filter(
group=group, project=group.project, type=ActivityType.SET_ESCALATING.value
).exists()
assert not mock_send_unignored_robust.called
class SnoozeTestMixin(BasePostProgressGroupMixin):
@patch("sentry.signals.issue_unignored.send_robust")
@patch("sentry.rules.processor.RuleProcessor")
def test_invalidates_snooze(self, mock_processor, mock_send_unignored_robust):
event = self.create_event(data={"message": "testing"}, project_id=self.project.id)
group = event.group
# Check for has_reappeared=False if is_new=True
self.call_post_process_group(
is_new=True,
is_regression=False,
is_new_group_environment=True,
event=event,
)
assert GroupInbox.objects.filter(group=group, reason=GroupInboxReason.NEW.value).exists()
GroupInbox.objects.filter(group=group).delete() # Delete so it creates the UNIGNORED entry.
Activity.objects.filter(group=group).delete()
mock_processor.assert_called_with(EventMatcher(event), True, False, True, False, False)
event = self.create_event(data={"message": "testing"}, project_id=self.project.id)
group.status = GroupStatus.IGNORED
group.substatus = GroupSubStatus.UNTIL_CONDITION_MET
group.save(update_fields=["status", "substatus"])
snooze = GroupSnooze.objects.create(
group=group, until=django_timezone.now() - timedelta(hours=1)
)
# Check for has_reappeared=True if is_new=False
self.call_post_process_group(
is_new=False,
is_regression=False,
is_new_group_environment=True,
event=event,
)
mock_processor.assert_called_with(EventMatcher(event), False, False, True, True, False)
assert not GroupSnooze.objects.filter(id=snooze.id).exists()
group.refresh_from_db()
assert group.status == GroupStatus.UNRESOLVED
assert group.substatus == GroupSubStatus.ONGOING
assert GroupInbox.objects.filter(
group=group, reason=GroupInboxReason.ONGOING.value
).exists()
assert Activity.objects.filter(
group=group, project=group.project, type=ActivityType.SET_UNRESOLVED.value
).exists()
assert mock_send_unignored_robust.called
@override_settings(SENTRY_BUFFER="sentry.buffer.redis.RedisBuffer")
@patch("sentry.signals.issue_unignored.send_robust")
@patch("sentry.rules.processor.RuleProcessor")
def test_invalidates_snooze_with_buffers(self, mock_processor, send_robust):
redis_buffer = RedisBuffer()
with mock.patch("sentry.buffer.backend.get", redis_buffer.get), mock.patch(
"sentry.buffer.backend.incr", redis_buffer.incr
):
event = self.create_event(
data={"message": "testing", "fingerprint": ["group-1"]}, project_id=self.project.id
)
event_2 = self.create_event(
data={"message": "testing", "fingerprint": ["group-1"]}, project_id=self.project.id
)
group = event.group
group.times_seen = 50
group.status = GroupStatus.IGNORED
group.substatus = GroupSubStatus.UNTIL_CONDITION_MET
group.save(update_fields=["times_seen", "status", "substatus"])
snooze = GroupSnooze.objects.create(group=group, count=100, state={"times_seen": 0})
self.call_post_process_group(
is_new=False,
is_regression=False,
is_new_group_environment=True,
event=event,
)
assert GroupSnooze.objects.filter(id=snooze.id).exists()
buffer.backend.incr(Group, {"times_seen": 60}, filters={"id": event.group.id})
self.call_post_process_group(
is_new=False,
is_regression=False,
is_new_group_environment=True,
event=event_2,
)
assert not GroupSnooze.objects.filter(id=snooze.id).exists()
@patch("sentry.rules.processor.RuleProcessor")
def test_maintains_valid_snooze(self, mock_processor):
event = self.create_event(data={}, project_id=self.project.id)
group = event.group
assert group.status == GroupStatus.UNRESOLVED
assert group.substatus == GroupSubStatus.ONGOING
snooze = GroupSnooze.objects.create(
group=group, until=django_timezone.now() + timedelta(hours=1)
)
self.call_post_process_group(
is_new=True,
is_regression=False,
is_new_group_environment=True,
event=event,
)
mock_processor.assert_called_with(EventMatcher(event), True, False, True, False, False)
assert GroupSnooze.objects.filter(id=snooze.id).exists()
group.refresh_from_db()
assert group.status == GroupStatus.UNRESOLVED
assert group.substatus == GroupSubStatus.NEW
@patch("sentry.issues.escalating.is_escalating", return_value=(True, 0))
def test_forecast_in_activity(self, mock_is_escalating):
"""
Test that the forecast is added to the activity for escalating issues that were
previously ignored until_escalating.
"""
event = self.create_event(data={"message": "testing"}, project_id=self.project.id)
group = event.group
group.status = GroupStatus.IGNORED
group.substatus = GroupSubStatus.UNTIL_ESCALATING
group.save()
self.call_post_process_group(
is_new=False,
is_regression=False,
is_new_group_environment=True,
event=event,
)
assert Activity.objects.filter(
group=group,
project=group.project,
type=ActivityType.SET_ESCALATING.value,
data={"event_id": event.event_id, "forecast": 0},
).exists()
@with_feature("projects:first-event-severity-new-escalation")
@patch("sentry.issues.escalating.is_escalating")
def test_skip_escalation_logic_for_new_groups(self, mock_is_escalating):
"""
Test that we skip checking escalation in the process_snoozes job if the group is less than
a day old.
"""
event = self.create_event(data={"message": "testing"}, project_id=self.project.id)
group = event.group
group.status = GroupStatus.IGNORED
group.substatus = GroupSubStatus.UNTIL_ESCALATING
group.update(first_seen=django_timezone.now() - timedelta(hours=1))
group.save()
self.call_post_process_group(
is_new=False,
is_regression=False,
is_new_group_environment=True,
event=event,
)
mock_is_escalating.assert_not_called()
@patch("sentry.utils.sdk_crashes.sdk_crash_detection.sdk_crash_detection")
class SDKCrashMonitoringTestMixin(BasePostProgressGroupMixin):
@with_feature("organizations:sdk-crash-detection")
@override_options(
{
"issues.sdk_crash_detection.cocoa.project_id": 1234,
"issues.sdk_crash_detection.cocoa.sample_rate": 1.0,
"issues.sdk_crash_detection.react-native.project_id": 12345,
"issues.sdk_crash_detection.react-native.sample_rate": 1.0,
"issues.sdk_crash_detection.react-native.organization_allowlist": [1],
}
)
def test_sdk_crash_monitoring_is_called(self, mock_sdk_crash_detection):
event = self.create_event(
data={"message": "testing"},
project_id=self.project.id,
)
self.call_post_process_group(
is_new=True,
is_regression=False,
is_new_group_environment=True,
event=event,
)
mock_sdk_crash_detection.detect_sdk_crash.assert_called_once()
args = mock_sdk_crash_detection.detect_sdk_crash.call_args[-1]
assert args["event"].project.id == event.project.id
assert len(args["configs"]) == 2
cocoa_config = args["configs"][0]
assert cocoa_config.sdk_name == SdkName.Cocoa
assert cocoa_config.project_id == 1234
assert cocoa_config.sample_rate == 1.0
assert cocoa_config.organization_allowlist == []
react_native_config = args["configs"][1]
assert react_native_config.sdk_name == SdkName.ReactNative
assert react_native_config.project_id == 12345
assert react_native_config.sample_rate == 1.0
assert react_native_config.organization_allowlist == [1]
@with_feature("organizations:sdk-crash-detection")
@override_options(
{
"issues.sdk_crash_detection.cocoa.project_id": 1234,
"issues.sdk_crash_detection.cocoa.sample_rate": 0.0,
}
)
def test_sdk_crash_monitoring_not_called_without_sample_rate(self, mock_sdk_crash_detection):
event = self.create_event(
data={"message": "testing"},
project_id=self.project.id,
)
self.call_post_process_group(
is_new=True,
is_regression=False,
is_new_group_environment=True,
event=event,
)
mock_sdk_crash_detection.detect_sdk_crash.assert_not_called()
def test_sdk_crash_monitoring_is_not_called_with_disabled_feature(
self, mock_sdk_crash_detection
):
event = self.create_event(
data={"message": "testing"},
project_id=self.project.id,
)
self.call_post_process_group(
is_new=True,
is_regression=False,
is_new_group_environment=True,
event=event,
)
mock_sdk_crash_detection.detect_sdk_crash.assert_not_called()
@override_options(
{
"issues.sdk_crash_detection.cocoa.project_id": None,
}
)
@with_feature("organizations:sdk-crash-detection")
def test_sdk_crash_monitoring_is_not_called_without_project_id(self, mock_sdk_crash_detection):
event = self.create_event(
data={"message": "testing"},
project_id=self.project.id,
)
self.call_post_process_group(
is_new=True,
is_regression=False,
is_new_group_environment=True,
event=event,
)
mock_sdk_crash_detection.detect_sdk_crash.assert_not_called()
@mock.patch.object(replays_kafka, "get_kafka_producer_cluster_options")
@mock.patch.object(replays_kafka, "KafkaPublisher")
@mock.patch("sentry.utils.metrics.incr")
class ReplayLinkageTestMixin(BasePostProgressGroupMixin):
def test_replay_linkage(self, incr, kafka_producer, kafka_publisher):
replay_id = uuid.uuid4().hex
event = self.create_event(
data={"message": "testing", "contexts": {"replay": {"replay_id": replay_id}}},
project_id=self.project.id,
)
with self.feature({"organizations:session-replay-event-linking": True}):
self.call_post_process_group(
is_new=True,
is_regression=False,
is_new_group_environment=True,
event=event,
)
assert kafka_producer.return_value.publish.call_count == 1
assert kafka_producer.return_value.publish.call_args[0][0] == "ingest-replay-events"
ret_value = json.loads(kafka_producer.return_value.publish.call_args[0][1])
assert ret_value["type"] == "replay_event"
assert ret_value["start_time"]
assert ret_value["replay_id"] == replay_id
assert ret_value["project_id"] == self.project.id
assert ret_value["segment_id"] is None
assert ret_value["retention_days"] == 90
# convert ret_value_payload which is a list of bytes to a string
ret_value_payload = json.loads(bytes(ret_value["payload"]).decode("utf-8"))
assert ret_value_payload == {
"type": "event_link",
"replay_id": replay_id,
"error_id": event.event_id,
"timestamp": int(event.datetime.timestamp()),
"event_hash": str(uuid.UUID(md5((event.event_id).encode("utf-8")).hexdigest())),
}
incr.assert_any_call("post_process.process_replay_link.id_sampled")
incr.assert_any_call("post_process.process_replay_link.id_exists")
def test_replay_linkage_with_tag(self, incr, kafka_producer, kafka_publisher):
replay_id = uuid.uuid4().hex
event = self.create_event(
data={"message": "testing", "tags": {"replayId": replay_id}},
project_id=self.project.id,
)
with self.feature({"organizations:session-replay-event-linking": True}):
self.call_post_process_group(
is_new=True,
is_regression=False,
is_new_group_environment=True,
event=event,
)
assert kafka_producer.return_value.publish.call_count == 1
assert kafka_producer.return_value.publish.call_args[0][0] == "ingest-replay-events"
ret_value = json.loads(kafka_producer.return_value.publish.call_args[0][1])
assert ret_value["type"] == "replay_event"
assert ret_value["start_time"]
assert ret_value["replay_id"] == replay_id
assert ret_value["project_id"] == self.project.id
assert ret_value["segment_id"] is None
assert ret_value["retention_days"] == 90
# convert ret_value_payload which is a list of bytes to a string
ret_value_payload = json.loads(bytes(ret_value["payload"]).decode("utf-8"))
assert ret_value_payload == {
"type": "event_link",
"replay_id": replay_id,
"error_id": event.event_id,
"timestamp": int(event.datetime.timestamp()),
"event_hash": str(uuid.UUID(md5((event.event_id).encode("utf-8")).hexdigest())),
}
incr.assert_any_call("post_process.process_replay_link.id_sampled")
incr.assert_any_call("post_process.process_replay_link.id_exists")
def test_replay_linkage_with_tag_pii_scrubbed(self, incr, kafka_producer, kafka_publisher):
event = self.create_event(
data={"message": "testing", "tags": {"replayId": "***"}},
project_id=self.project.id,
)
with self.feature({"organizations:session-replay-event-linking": True}):
self.call_post_process_group(
is_new=True,
is_regression=False,
is_new_group_environment=True,
event=event,
)
assert kafka_producer.return_value.publish.call_count == 0
def test_no_replay(self, incr, kafka_producer, kafka_publisher):
event = self.create_event(
data={"message": "testing"},
project_id=self.project.id,
)
with self.feature({"organizations:session-replay-event-linking": True}):
self.call_post_process_group(
is_new=True,
is_regression=False,
is_new_group_environment=True,
event=event,
)
assert kafka_producer.return_value.publish.call_count == 0
incr.assert_any_call("post_process.process_replay_link.id_sampled")
def test_0_sample_rate_replays(self, incr, kafka_producer, kafka_publisher):
event = self.create_event(
data={"message": "testing"},
project_id=self.project.id,
)
with self.feature({"organizations:session-replay-event-linking": False}):
self.call_post_process_group(
is_new=True,
is_regression=False,
is_new_group_environment=True,
event=event,
)
assert kafka_producer.return_value.publish.call_count == 0
for args, _ in incr.call_args_list:
self.assertNotEqual(args, ("post_process.process_replay_link.id_sampled"))
class DetectNewEscalationTestMixin(BasePostProgressGroupMixin):
@patch("sentry.tasks.post_process.run_post_process_job", side_effect=run_post_process_job)
@with_feature("projects:issue-priority")
def test_has_escalated(self, mock_run_post_process_job):
event = self.create_event(data={}, project_id=self.project.id)
group = event.group
group.update(
first_seen=django_timezone.now() - timedelta(hours=1),
times_seen=10,
priority=PriorityLevel.LOW,
)
event.group = Group.objects.get(id=group.id)
with self.feature("projects:first-event-severity-new-escalation"):
with patch("sentry.issues.issue_velocity.calculate_threshold", return_value=9):
self.call_post_process_group(
is_new=True,
is_regression=False,
is_new_group_environment=True,
event=event,
)
job = mock_run_post_process_job.call_args[0][0]
assert job["has_escalated"]
group.refresh_from_db()
assert group.substatus == GroupSubStatus.ESCALATING
assert group.priority == PriorityLevel.MEDIUM
@patch("sentry.issues.issue_velocity.get_latest_threshold", return_value=1)
@patch("sentry.tasks.post_process.run_post_process_job", side_effect=run_post_process_job)
@with_feature("projects:issue-priority")
def test_has_escalated_no_flag(self, mock_run_post_process_job, mock_threshold):
event = self.create_event(data={}, project_id=self.project.id)
group = event.group
group.update(first_seen=django_timezone.now() - timedelta(hours=1), times_seen=10000)
self.call_post_process_group(
is_new=True,
is_regression=False,
is_new_group_environment=True,
event=event,
)
job = mock_run_post_process_job.call_args[0][0]
assert not job["has_escalated"]
group.refresh_from_db()
assert group.substatus == GroupSubStatus.NEW
assert group.priority == PriorityLevel.HIGH
@patch("sentry.issues.issue_velocity.get_latest_threshold")
@patch("sentry.tasks.post_process.run_post_process_job", side_effect=run_post_process_job)
@with_feature("projects:issue-priority")
def test_has_escalated_old(self, mock_run_post_process_job, mock_threshold):
event = self.create_event(data={}, project_id=self.project.id)
group = event.group
group.update(first_seen=django_timezone.now() - timedelta(days=2), times_seen=10000)
with self.feature("projects:first-event-severity-new-escalation"):
self.call_post_process_group(
is_new=True,
is_regression=False,
is_new_group_environment=True,
event=event,
)
mock_threshold.assert_not_called()
job = mock_run_post_process_job.call_args[0][0]
assert not job["has_escalated"]
group.refresh_from_db()
assert group.substatus == GroupSubStatus.NEW
assert group.priority == PriorityLevel.HIGH
@patch("sentry.issues.issue_velocity.get_latest_threshold", return_value=11)
@patch("sentry.tasks.post_process.run_post_process_job", side_effect=run_post_process_job)
@with_feature("projects:issue-priority")
def test_has_not_escalated(self, mock_run_post_process_job, mock_threshold):
event = self.create_event(data={}, project_id=self.project.id)
group = event.group
group.update(
first_seen=django_timezone.now() - timedelta(hours=1),
times_seen=10,
priority=PriorityLevel.LOW,
)
with self.feature("projects:first-event-severity-new-escalation"):
self.call_post_process_group(
is_new=True,
is_regression=False,
is_new_group_environment=True,
event=event,
)
mock_threshold.assert_called()
job = mock_run_post_process_job.call_args[0][0]
assert not job["has_escalated"]
group.refresh_from_db()
assert group.substatus == GroupSubStatus.NEW
assert group.priority == PriorityLevel.LOW
@patch("sentry.issues.issue_velocity.get_latest_threshold")
@patch("sentry.tasks.post_process.run_post_process_job", side_effect=run_post_process_job)
def test_has_escalated_locked(self, mock_run_post_process_job, mock_threshold):
event = self.create_event(data={}, project_id=self.project.id)
group = event.group
group.update(first_seen=django_timezone.now() - timedelta(hours=1), times_seen=10000)
lock = locks.get(f"detect_escalation:{group.id}", duration=10, name="detect_escalation")
with self.feature("projects:first-event-severity-new-escalation"), lock.acquire():
self.call_post_process_group(
is_new=True,
is_regression=False,
is_new_group_environment=True,
event=event,
)
mock_threshold.assert_not_called()
job = mock_run_post_process_job.call_args[0][0]
assert not job["has_escalated"]
group.refresh_from_db()
assert group.substatus == GroupSubStatus.NEW
@patch("sentry.issues.issue_velocity.get_latest_threshold")
@patch("sentry.tasks.post_process.run_post_process_job", side_effect=run_post_process_job)
def test_has_escalated_already_escalated(self, mock_run_post_process_job, mock_threshold):
event = self.create_event(data={}, project_id=self.project.id)
group = event.group
self.call_post_process_group(
is_new=True,
is_regression=False,
is_new_group_environment=True,
event=event,
)
group.update(
first_seen=django_timezone.now() - timedelta(hours=1),
times_seen=10000,
substatus=GroupSubStatus.ESCALATING,
priority=PriorityLevel.MEDIUM,
)
with self.feature("projects:first-event-severity-new-escalation"):
self.call_post_process_group(
is_new=False,
is_regression=False,
is_new_group_environment=False,
event=event,
)
mock_threshold.assert_not_called()
job = mock_run_post_process_job.call_args[0][0]
assert not job["has_escalated"]
group.refresh_from_db()
assert group.substatus == GroupSubStatus.ESCALATING
assert group.priority == PriorityLevel.MEDIUM
@patch("sentry.issues.issue_velocity.get_latest_threshold")
@patch("sentry.tasks.post_process.run_post_process_job", side_effect=run_post_process_job)
def test_does_not_escalate_non_new_substatus(self, mock_run_post_process_job, mock_threshold):
for substatus, status in GROUP_SUBSTATUS_TO_STATUS_MAP.items():
if substatus == GroupSubStatus.NEW:
continue
event = self.create_event(data={}, project_id=self.project.id)
group = event.group
group.update(
first_seen=django_timezone.now() - timedelta(hours=1),
times_seen=10000,
status=status,
substatus=substatus,
)
group.save()
with self.feature("projects:first-event-severity-new-escalation"):
self.call_post_process_group(
is_new=False, # when true, post_process sets the substatus to NEW
is_regression=False,
is_new_group_environment=True,
event=event,
)
mock_threshold.assert_not_called()
job = mock_run_post_process_job.call_args[0][0]
assert not job["has_escalated"]
group.refresh_from_db()
assert group.substatus == substatus
@patch("sentry.issues.issue_velocity.get_latest_threshold", return_value=8)
@patch("sentry.tasks.post_process.run_post_process_job", side_effect=run_post_process_job)
def test_no_escalation_less_than_floor(self, mock_run_post_process_job, mock_threshold):
event = self.create_event(data={}, project_id=self.project.id)
group = event.group
group.update(first_seen=django_timezone.now() - timedelta(hours=1), times_seen=9)
event.group = Group.objects.get(id=group.id)
with self.feature("projects:first-event-severity-new-escalation"):
self.call_post_process_group(
is_new=True,
is_regression=False,
is_new_group_environment=True,
event=event,
)
mock_threshold.assert_not_called()
job = mock_run_post_process_job.call_args[0][0]
assert not job["has_escalated"]
group.refresh_from_db()
assert group.substatus == GroupSubStatus.NEW
@patch("sentry.issues.issue_velocity.get_latest_threshold", return_value=11)
@patch("sentry.tasks.post_process.run_post_process_job", side_effect=run_post_process_job)
def test_has_not_escalated_less_than_an_hour(self, mock_run_post_process_job, mock_threshold):
event = self.create_event(data={}, project_id=self.project.id)
group = event.group
# the group is less than an hour old, but we use 1 hr for the hourly event rate anyway
group.update(first_seen=django_timezone.now() - timedelta(minutes=1), times_seen=10)
event.group = Group.objects.get(id=group.id)
with self.feature("projects:first-event-severity-new-escalation"):
self.call_post_process_group(
is_new=True,
is_regression=False,
is_new_group_environment=True,
event=event,
)
job = mock_run_post_process_job.call_args[0][0]
assert not job["has_escalated"]
group.refresh_from_db()
assert group.substatus == GroupSubStatus.NEW
@patch("sentry.issues.issue_velocity.get_latest_threshold", return_value=0)
@patch("sentry.tasks.post_process.run_post_process_job", side_effect=run_post_process_job)
def test_zero_escalation_rate(self, mock_run_post_process_job, mock_threshold):
event = self.create_event(data={}, project_id=self.project.id)
group = event.group
group.update(first_seen=django_timezone.now() - timedelta(hours=1), times_seen=10000)
with self.feature("projects:first-event-severity-new-escalation"):
self.call_post_process_group(
is_new=True,
is_regression=False,
is_new_group_environment=True,
event=event,
)
mock_threshold.assert_called()
job = mock_run_post_process_job.call_args[0][0]
assert not job["has_escalated"]
group.refresh_from_db()
assert group.substatus == GroupSubStatus.NEW
@region_silo_test
class PostProcessGroupErrorTest(
TestCase,
AssignmentTestMixin,
ProcessCommitsTestMixin,
CorePostProcessGroupTestMixin,
DeriveCodeMappingsProcessGroupTestMixin,
InboxTestMixin,
ResourceChangeBoundsTestMixin,
RuleProcessorTestMixin,
ServiceHooksTestMixin,
SnoozeTestMixin,
SnoozeTestSkipSnoozeMixin,
SDKCrashMonitoringTestMixin,
ReplayLinkageTestMixin,
DetectNewEscalationTestMixin,
):
def setUp(self):
super().setUp()
clear_replay_publisher()
def create_event(self, data, project_id, assert_no_errors=True):
return self.store_event(data=data, project_id=project_id, assert_no_errors=assert_no_errors)
def call_post_process_group(
self, is_new, is_regression, is_new_group_environment, event, cache_key=None
):
if cache_key is None:
cache_key = write_event_to_cache(event)
post_process_group(
is_new=is_new,
is_regression=is_regression,
is_new_group_environment=is_new_group_environment,
cache_key=cache_key,
group_id=event.group_id,
project_id=event.project_id,
)
return cache_key
@with_feature("organizations:escalating-metrics-backend")
@patch("sentry.sentry_metrics.client.generic_metrics_backend.counter")
@patch("sentry.utils.metrics.incr")
@patch("sentry.utils.metrics.timer")
def test_generic_metrics_backend_counter(
self, metric_timer_mock, metric_incr_mock, generic_metrics_backend_mock
):
min_ago = iso_format(before_now(minutes=1))
event = self.create_event(
data={
"exception": {
"values": [
{
"type": "ZeroDivisionError",
"stacktrace": {"frames": [{"function": f} for f in ["a", "b"]]},
}
]
},
"timestamp": min_ago,
"start_timestamp": min_ago,
"contexts": {"trace": {"trace_id": "b" * 32, "span_id": "c" * 16, "op": ""}},
},
project_id=self.project.id,
)
self.call_post_process_group(
is_new=True, is_regression=False, is_new_group_environment=True, event=event
)
assert generic_metrics_backend_mock.call_count == 1
metric_incr_mock.assert_any_call(
"sentry.tasks.post_process.post_process_group.completed",
tags={"issue_category": "error", "pipeline": "process_rules"},
)
metric_timer_mock.assert_any_call(
"tasks.post_process.run_post_process_job.pipeline.duration",
tags={
"pipeline": "process_rules",
"issue_category": "error",
"is_reprocessed": False,
},
)
@region_silo_test
class PostProcessGroupPerformanceTest(
TestCase,
SnubaTestCase,
PerfIssueTransactionTestMixin,
CorePostProcessGroupTestMixin,
InboxTestMixin,
RuleProcessorTestMixin,
SnoozeTestMixin,
SnoozeTestSkipSnoozeMixin,
PerformanceIssueTestCase,
):
def create_event(self, data, project_id, assert_no_errors=True):
fingerprint = data["fingerprint"][0] if data.get("fingerprint") else "some_group"
fingerprint = f"{PerformanceNPlusOneGroupType.type_id}-{fingerprint}"
return self.create_performance_issue(fingerprint=fingerprint)
def call_post_process_group(
self, is_new, is_regression, is_new_group_environment, event, cache_key=None
):
group_states = (
[
{
"id": event.group_id,
"is_new": is_new,
"is_regression": is_regression,
"is_new_group_environment": is_new_group_environment,
}
]
if event.group_id
else None
)
if cache_key is None:
cache_key = write_event_to_cache(event)
with self.feature(PerformanceNPlusOneGroupType.build_post_process_group_feature_name()):
post_process_group(
is_new=is_new,
is_regression=is_regression,
is_new_group_environment=is_new_group_environment,
cache_key=cache_key,
group_states=group_states,
project_id=event.project_id,
)
return cache_key
@patch("sentry.sentry_metrics.client.generic_metrics_backend.counter")
@patch("sentry.tasks.post_process.run_post_process_job")
@patch("sentry.rules.processor.RuleProcessor")
@patch("sentry.signals.transaction_processed.send_robust")
@patch("sentry.signals.event_processed.send_robust")
def test_process_transaction_event_with_no_group(
self,
event_processed_signal_mock,
transaction_processed_signal_mock,
mock_processor,
run_post_process_job_mock,
generic_metrics_backend_mock,
):
min_ago = before_now(minutes=1).replace(tzinfo=timezone.utc)
event = self.store_transaction(
project_id=self.project.id,
user_id=self.create_user(name="user1").name,
fingerprint=[],
environment=None,
timestamp=min_ago,
)
assert len(event.groups) == 0
cache_key = write_event_to_cache(event)
post_process_group(
is_new=False,
is_regression=False,
is_new_group_environment=False,
cache_key=cache_key,
group_id=None,
group_states=None,
)
assert transaction_processed_signal_mock.call_count == 1
assert event_processed_signal_mock.call_count == 0
assert mock_processor.call_count == 0
assert run_post_process_job_mock.call_count == 0
assert generic_metrics_backend_mock.call_count == 0
@patch("sentry.tasks.post_process.handle_owner_assignment")
@patch("sentry.tasks.post_process.handle_auto_assignment")
@patch("sentry.tasks.post_process.process_rules")
@patch("sentry.tasks.post_process.run_post_process_job")
@patch("sentry.rules.processor.RuleProcessor")
@patch("sentry.signals.transaction_processed.send_robust")
@patch("sentry.signals.event_processed.send_robust")
def test_full_pipeline_with_group_states(
self,
event_processed_signal_mock,
transaction_processed_signal_mock,
mock_processor,
run_post_process_job_mock,
mock_process_rules,
mock_handle_auto_assignment,
mock_handle_owner_assignment,
):
event = self.create_performance_issue()
assert event.group
# cache_key = write_event_to_cache(event)
group_state = dict(
is_new=True,
is_regression=False,
is_new_group_environment=True,
)
# TODO(jangjodi): Fix this ordering test; side_effects should be a function (lambda),
# but because post-processing is async, this causes the assert to fail because it doesn't
# wait for the side effects to happen
call_order = [mock_handle_owner_assignment, mock_handle_auto_assignment, mock_process_rules]
mock_handle_owner_assignment.side_effect = None
mock_handle_auto_assignment.side_effect = None
mock_process_rules.side_effect = None
post_process_group(
**group_state,
cache_key="dummykey",
group_id=event.group_id,
group_states=[{"id": event.group.id, **group_state}],
occurrence_id=event.occurrence_id,
project_id=self.project.id,
)
assert transaction_processed_signal_mock.call_count == 1
assert event_processed_signal_mock.call_count == 0
assert mock_processor.call_count == 0
assert run_post_process_job_mock.call_count == 1
assert call_order == [
mock_handle_owner_assignment,
mock_handle_auto_assignment,
mock_process_rules,
]
@region_silo_test
class PostProcessGroupAggregateEventTest(
TestCase,
SnubaTestCase,
PerfIssueTransactionTestMixin,
CorePostProcessGroupTestMixin,
SnoozeTestSkipSnoozeMixin,
PerformanceIssueTestCase,
):
def create_event(self, data, project_id):
group = self.create_group(
type=PerformanceP95EndpointRegressionGroupType.type_id,
)
event = self.store_event(data=data, project_id=project_id)
event.group = group
event = event.for_group(group)
return event
def call_post_process_group(
self, is_new, is_regression, is_new_group_environment, event, cache_key=None
):
group_states = (
[
{
"id": event.group_id,
"is_new": is_new,
"is_regression": is_regression,
"is_new_group_environment": is_new_group_environment,
}
]
if event.group_id
else None
)
if cache_key is None:
cache_key = write_event_to_cache(event)
with self.feature(
PerformanceP95EndpointRegressionGroupType.build_post_process_group_feature_name()
):
post_process_group(
is_new=is_new,
is_regression=is_regression,
is_new_group_environment=is_new_group_environment,
cache_key=cache_key,
group_states=group_states,
project_id=event.project_id,
)
return cache_key
class TransactionClustererTestCase(TestCase, SnubaTestCase):
@patch("sentry.ingest.transaction_clusterer.datasource.redis._record_sample")
def test_process_transaction_event_clusterer(
self,
mock_store_transaction_name,
):
min_ago = before_now(minutes=1).replace(tzinfo=timezone.utc)
event = process_event(
data={
"project": self.project.id,
"event_id": "b" * 32,
"transaction": "foo",
"start_timestamp": str(min_ago),
"timestamp": str(min_ago),
"type": "transaction",
"transaction_info": {
"source": "url",
},
"contexts": {"trace": {"trace_id": "b" * 32, "span_id": "c" * 16, "op": ""}},
},
group_id=0,
)
cache_key = write_event_to_cache(event)
post_process_group(
is_new=False,
is_regression=False,
is_new_group_environment=False,
cache_key=cache_key,
group_id=None,
group_states=None,
)
assert mock_store_transaction_name.mock_calls == [
mock.call(ClustererNamespace.TRANSACTIONS, self.project, "foo")
]
@region_silo_test
class PostProcessGroupGenericTest(
TestCase,
SnubaTestCase,
OccurrenceTestMixin,
CorePostProcessGroupTestMixin,
InboxTestMixin,
RuleProcessorTestMixin,
SnoozeTestMixin,
):
def create_event(self, data, project_id, assert_no_errors=True):
data["type"] = "generic"
event = self.store_event(
data=data, project_id=project_id, assert_no_errors=assert_no_errors
)
occurrence_data = self.build_occurrence_data(event_id=event.event_id, project_id=project_id)
occurrence, group_info = save_issue_occurrence(occurrence_data, event)
assert group_info is not None
group_event = event.for_group(group_info.group)
group_event.occurrence = occurrence
return group_event
def call_post_process_group(
self, is_new, is_regression, is_new_group_environment, event, cache_key=None
):
with self.feature(ProfileFileIOGroupType.build_post_process_group_feature_name()):
post_process_group(
is_new=is_new,
is_regression=is_regression,
is_new_group_environment=is_new_group_environment,
cache_key=None,
group_id=event.group_id,
occurrence_id=event.occurrence.id,
project_id=event.group.project_id,
)
return cache_key
def test_issueless(self):
# Skip this test since there's no way to have issueless events in the issue platform
pass
def test_no_cache_abort(self):
# We don't use the cache for generic issues, so skip this test
pass
@patch("sentry.rules.processor.RuleProcessor")
def test_occurrence_deduping(self, mock_processor):
event = self.create_event(data={"message": "testing"}, project_id=self.project.id)
self.call_post_process_group(
is_new=True,
is_regression=True,
is_new_group_environment=False,
event=event,
)
assert mock_processor.call_count == 1
mock_processor.assert_called_with(EventMatcher(event), True, True, False, False, False)
# Calling this again should do nothing, since we've already processed this occurrence.
self.call_post_process_group(
is_new=False,
is_regression=True,
is_new_group_environment=False,
event=event,
)
# Make sure we haven't called this again, since we should exit early.
assert mock_processor.call_count == 1
@patch("sentry.tasks.post_process.handle_owner_assignment")
@patch("sentry.tasks.post_process.handle_auto_assignment")
@patch("sentry.tasks.post_process.process_rules")
@patch("sentry.tasks.post_process.run_post_process_job")
@patch("sentry.rules.processor.RuleProcessor")
@patch("sentry.signals.event_processed.send_robust")
@patch("sentry.utils.snuba.raw_query")
def test_full_pipeline_with_group_states(
self,
snuba_raw_query_mock,
event_processed_signal_mock,
mock_processor,
run_post_process_job_mock,
mock_process_rules,
mock_handle_auto_assignment,
mock_handle_owner_assignment,
):
event = self.create_event(data={"message": "testing"}, project_id=self.project.id)
call_order = [mock_handle_owner_assignment, mock_handle_auto_assignment, mock_process_rules]
mock_handle_owner_assignment.side_effect = None
mock_handle_auto_assignment.side_effect = None
mock_process_rules.side_effect = None
self.call_post_process_group(
is_new=False,
is_regression=True,
is_new_group_environment=False,
event=event,
)
assert event_processed_signal_mock.call_count == 0
assert mock_processor.call_count == 0
assert run_post_process_job_mock.call_count == 1
assert call_order == [
mock_handle_owner_assignment,
mock_handle_auto_assignment,
mock_process_rules,
]
assert snuba_raw_query_mock.call_count == 0
@pytest.mark.skip(reason="those tests do not work with the given call_post_process_group impl")
def test_processing_cache_cleared(self):
pass
@pytest.mark.skip(reason="those tests do not work with the given call_post_process_group impl")
def test_processing_cache_cleared_with_commits(self):
pass
@region_silo_test
class PostProcessGroupFeedbackTest(
TestCase,
SnubaTestCase,
OccurrenceTestMixin,
CorePostProcessGroupTestMixin,
InboxTestMixin,
RuleProcessorTestMixin,
SnoozeTestMixin,
):
def create_event(
self,
data,
project_id,
assert_no_errors=True,
feedback_type=FeedbackCreationSource.NEW_FEEDBACK_ENVELOPE,
):
data["type"] = "generic"
event = self.store_event(
data=data, project_id=project_id, assert_no_errors=assert_no_errors
)
occurrence_data = self.build_occurrence_data(
event_id=event.event_id,
project_id=project_id,
**{
"id": uuid.uuid4().hex,
"fingerprint": ["c" * 32],
"issue_title": "User Feedback",
"subtitle": "it was bad",
"culprit": "api/123",
"resource_id": "1234",
"evidence_data": {
"Test": 123,
"source": feedback_type.value,
},
"evidence_display": [
{"name": "hi", "value": "bye", "important": True},
{"name": "what", "value": "where", "important": False},
],
"type": FeedbackGroup.type_id,
"detection_time": datetime.now().timestamp(),
"level": "info",
},
)
occurrence, group_info = save_issue_occurrence(occurrence_data, event)
assert group_info is not None
group_event = event.for_group(group_info.group)
group_event.occurrence = occurrence
return group_event
def call_post_process_group(
self, is_new, is_regression, is_new_group_environment, event, cache_key=None
):
with self.feature(FeedbackGroup.build_post_process_group_feature_name()):
post_process_group(
is_new=is_new,
is_regression=is_regression,
is_new_group_environment=is_new_group_environment,
cache_key=None,
group_id=event.group_id,
occurrence_id=event.occurrence.id,
project_id=event.group.project_id,
)
return cache_key
def test_not_ran_if_crash_report(self):
event = self.create_event(
data={},
project_id=self.project.id,
feedback_type=FeedbackCreationSource.CRASH_REPORT_EMBED_FORM,
)
mock_process_func = Mock()
with patch(
"sentry.tasks.post_process.GROUP_CATEGORY_POST_PROCESS_PIPELINE",
{
GroupCategory.FEEDBACK: [
feedback_filter_decorator(mock_process_func),
]
},
):
self.call_post_process_group(
is_new=True,
is_regression=False,
is_new_group_environment=True,
event=event,
cache_key="total_rubbish",
)
# assert mock_process_rules is not called
assert mock_process_func.call_count == 0
def test_ran_if_crash_feedback_envelope(self):
event = self.create_event(
data={},
project_id=self.project.id,
feedback_type=FeedbackCreationSource.NEW_FEEDBACK_ENVELOPE,
)
mock_process_func = Mock()
with patch(
"sentry.tasks.post_process.GROUP_CATEGORY_POST_PROCESS_PIPELINE",
{
GroupCategory.FEEDBACK: [
feedback_filter_decorator(mock_process_func),
]
},
):
self.call_post_process_group(
is_new=True,
is_regression=False,
is_new_group_environment=True,
event=event,
cache_key="total_rubbish",
)
# assert mock_process_rules is not called
assert mock_process_func.call_count == 1
@pytest.mark.skip(
reason="Skip this test since there's no way to have issueless events in the issue platform"
)
def test_issueless(self):
...
def test_no_cache_abort(self):
# We don't use the cache for generic issues, so skip this test
pass
@pytest.mark.skip(reason="those tests do not work with the given call_post_process_group impl")
def test_processing_cache_cleared(self):
pass
@pytest.mark.skip(reason="those tests do not work with the given call_post_process_group impl")
def test_processing_cache_cleared_with_commits(self):
pass