mirror of
https://github.com/hanzoai/sentinel.git
synced 2026-09-29 22:34:33 +00:00
305 lines
12 KiB
Python
305 lines
12 KiB
Python
import uuid
|
|
from datetime import datetime
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
from sentry.issues.ingest import process_occurrence_data
|
|
from sentry.issues.issue_occurrence import IssueOccurrence
|
|
from sentry.issues.producer import PayloadType, produce_occurrence_to_kafka
|
|
from sentry.issues.status_change_message import StatusChangeMessage
|
|
from sentry.models.activity import Activity
|
|
from sentry.models.group import GroupStatus
|
|
from sentry.models.grouphistory import STRING_TO_STATUS_LOOKUP, GroupHistory, GroupHistoryStatus
|
|
from sentry.testutils.cases import TestCase
|
|
from sentry.testutils.helpers.datetime import before_now, iso_format
|
|
from sentry.testutils.helpers.features import apply_feature_flag_on_cls
|
|
from sentry.testutils.skips import requires_snuba
|
|
from sentry.types.activity import ActivityType
|
|
from sentry.types.group import GROUP_SUBSTATUS_TO_GROUP_HISTORY_STATUS, GroupSubStatus
|
|
from sentry.utils.samples import load_data
|
|
from tests.sentry.issues.test_utils import OccurrenceTestMixin
|
|
|
|
pytestmark = [requires_snuba]
|
|
|
|
|
|
@apply_feature_flag_on_cls("organizations:profile-file-io-main-thread-ingest")
|
|
class TestProduceOccurrenceToKafka(TestCase, OccurrenceTestMixin):
|
|
def test_event_id_mismatch(self) -> None:
|
|
with self.assertRaisesMessage(
|
|
ValueError, "Event id on occurrence and event_data must be the same"
|
|
):
|
|
produce_occurrence_to_kafka(
|
|
payload_type=PayloadType.OCCURRENCE,
|
|
occurrence=self.build_occurrence(),
|
|
event_data={"event_id": uuid.uuid4().hex},
|
|
)
|
|
|
|
def test_with_event(self) -> None:
|
|
occurrence = self.build_occurrence(project_id=self.project.id)
|
|
produce_occurrence_to_kafka(
|
|
payload_type=PayloadType.OCCURRENCE,
|
|
occurrence=occurrence,
|
|
event_data={
|
|
"event_id": occurrence.event_id,
|
|
"project_id": self.project.id,
|
|
"title": "some problem",
|
|
"platform": "python",
|
|
"tags": {"my_tag": "2"},
|
|
"timestamp": before_now(minutes=1).isoformat(),
|
|
"received": before_now(minutes=1).isoformat(),
|
|
},
|
|
)
|
|
stored_occurrence = IssueOccurrence.fetch(occurrence.id, occurrence.project_id)
|
|
assert stored_occurrence
|
|
assert occurrence.event_id == stored_occurrence.event_id
|
|
|
|
def test_without_payload_type(self) -> None:
|
|
# Ensure the occurrence is processes without a payload_type too.
|
|
occurrence = self.build_occurrence(project_id=self.project.id)
|
|
produce_occurrence_to_kafka(
|
|
occurrence=occurrence,
|
|
event_data={
|
|
"event_id": occurrence.event_id,
|
|
"project_id": self.project.id,
|
|
"title": "some problem",
|
|
"platform": "python",
|
|
"tags": {"my_tag": "2"},
|
|
"timestamp": before_now(minutes=1).isoformat(),
|
|
"received": before_now(minutes=1).isoformat(),
|
|
},
|
|
)
|
|
stored_occurrence = IssueOccurrence.fetch(occurrence.id, occurrence.project_id)
|
|
assert stored_occurrence
|
|
assert occurrence.event_id == stored_occurrence.event_id
|
|
|
|
def test_with_only_occurrence(self) -> None:
|
|
event = self.store_event(data=load_data("transaction"), project_id=self.project.id)
|
|
occurrence = self.build_occurrence(event_id=event.event_id, project_id=self.project.id)
|
|
produce_occurrence_to_kafka(
|
|
payload_type=PayloadType.OCCURRENCE,
|
|
occurrence=occurrence,
|
|
)
|
|
stored_occurrence = IssueOccurrence.fetch(occurrence.id, occurrence.project_id)
|
|
assert stored_occurrence
|
|
assert occurrence.event_id == stored_occurrence.event_id
|
|
|
|
|
|
class TestProduceOccurrenceForStatusChange(TestCase, OccurrenceTestMixin):
|
|
def setUp(self):
|
|
self.fingerprint = ["group-1"]
|
|
self.event = self.store_event(
|
|
data={
|
|
"event_id": "a" * 32,
|
|
"message": "oh no",
|
|
"timestamp": iso_format(datetime.now()),
|
|
"fingerprint": self.fingerprint,
|
|
},
|
|
project_id=self.project.id,
|
|
)
|
|
self.group = self.event.group
|
|
assert self.group
|
|
self.initial_status = self.group.status
|
|
self.initial_substatus = self.group.substatus
|
|
|
|
def test_with_invalid_payloads(self) -> None:
|
|
with pytest.raises(ValueError, match="occurrence must be provided"):
|
|
# Should raise an error because the occurrence is not provided for the OCCURRENCE payload type.
|
|
produce_occurrence_to_kafka(
|
|
payload_type=PayloadType.OCCURRENCE,
|
|
)
|
|
|
|
with pytest.raises(ValueError, match="status_change must be provided"):
|
|
# Should raise an error because the status_change object is not provided for the STATUS_CHANGE payload type.
|
|
produce_occurrence_to_kafka(
|
|
payload_type=PayloadType.STATUS_CHANGE,
|
|
)
|
|
|
|
with pytest.raises(NotImplementedError, match="Unknown payload type: invalid"):
|
|
# Should raise an error because the payload type is not supported.
|
|
produce_occurrence_to_kafka(payload_type="invalid") # type: ignore
|
|
|
|
def test_with_no_status_change(self) -> None:
|
|
status_change = StatusChangeMessage(
|
|
fingerprint=self.fingerprint,
|
|
project_id=self.group.project_id,
|
|
new_status=self.initial_status,
|
|
new_substatus=self.initial_substatus,
|
|
)
|
|
produce_occurrence_to_kafka(
|
|
payload_type=PayloadType.STATUS_CHANGE,
|
|
status_change=status_change,
|
|
)
|
|
self.group.refresh_from_db()
|
|
assert self.group.status == self.initial_status
|
|
assert self.group.substatus == self.initial_substatus
|
|
|
|
assert not Activity.objects.filter(group=self.group).exists()
|
|
assert not GroupHistory.objects.filter(group=self.group).exists()
|
|
|
|
def test_with_status_change_resolved(self) -> None:
|
|
status_change = StatusChangeMessage(
|
|
fingerprint=self.fingerprint,
|
|
project_id=self.group.project_id,
|
|
new_status=GroupStatus.RESOLVED,
|
|
new_substatus=None,
|
|
)
|
|
produce_occurrence_to_kafka(
|
|
payload_type=PayloadType.STATUS_CHANGE,
|
|
status_change=status_change,
|
|
)
|
|
self.group.refresh_from_db()
|
|
assert self.group.status == GroupStatus.RESOLVED
|
|
assert self.group.substatus is None
|
|
|
|
assert Activity.objects.filter(
|
|
group=self.group, type=ActivityType.SET_RESOLVED.value
|
|
).exists()
|
|
assert GroupHistory.objects.filter(
|
|
group=self.group, status=GroupHistoryStatus.RESOLVED
|
|
).exists()
|
|
|
|
def test_with_status_change_archived(self) -> None:
|
|
for substatus in [
|
|
GroupSubStatus.UNTIL_ESCALATING,
|
|
GroupSubStatus.UNTIL_CONDITION_MET,
|
|
GroupSubStatus.FOREVER,
|
|
]:
|
|
status_change = StatusChangeMessage(
|
|
fingerprint=self.fingerprint,
|
|
project_id=self.group.project_id,
|
|
new_status=GroupStatus.IGNORED,
|
|
new_substatus=substatus,
|
|
)
|
|
produce_occurrence_to_kafka(
|
|
payload_type=PayloadType.STATUS_CHANGE,
|
|
status_change=status_change,
|
|
)
|
|
self.group.refresh_from_db()
|
|
assert self.group.status == GroupStatus.IGNORED
|
|
assert self.group.substatus == substatus
|
|
|
|
assert Activity.objects.filter(
|
|
group=self.group, type=ActivityType.SET_IGNORED.value
|
|
).exists()
|
|
|
|
gh_status = GROUP_SUBSTATUS_TO_GROUP_HISTORY_STATUS[substatus]
|
|
assert GroupHistory.objects.filter(
|
|
group=self.group,
|
|
status=STRING_TO_STATUS_LOOKUP[gh_status],
|
|
).exists()
|
|
|
|
def test_with_status_change_unresolved(self):
|
|
# We modify a single group through different substatuses that are supported in the UI
|
|
# to ensure the status change is processed correctly.
|
|
self.group.update(status=GroupStatus.IGNORED, substatus=GroupSubStatus.UNTIL_ESCALATING)
|
|
for substatus, activity_type in [
|
|
(GroupSubStatus.ESCALATING, ActivityType.SET_ESCALATING),
|
|
(GroupSubStatus.ONGOING, ActivityType.AUTO_SET_ONGOING),
|
|
(GroupSubStatus.REGRESSED, ActivityType.SET_REGRESSION),
|
|
(GroupSubStatus.ONGOING, ActivityType.SET_UNRESOLVED),
|
|
]:
|
|
# Produce the status change message and process it
|
|
status_change = StatusChangeMessage(
|
|
fingerprint=self.fingerprint,
|
|
project_id=self.group.project_id,
|
|
new_status=GroupStatus.UNRESOLVED,
|
|
new_substatus=substatus,
|
|
)
|
|
produce_occurrence_to_kafka(
|
|
payload_type=PayloadType.STATUS_CHANGE,
|
|
status_change=status_change,
|
|
)
|
|
self.group.refresh_from_db()
|
|
assert self.group.status == GroupStatus.UNRESOLVED
|
|
assert self.group.substatus == substatus
|
|
|
|
assert Activity.objects.filter(group=self.group, type=activity_type.value).exists()
|
|
|
|
gh_status = GROUP_SUBSTATUS_TO_GROUP_HISTORY_STATUS[substatus]
|
|
assert GroupHistory.objects.filter(
|
|
group=self.group,
|
|
status=STRING_TO_STATUS_LOOKUP[gh_status],
|
|
).exists()
|
|
|
|
@patch("sentry.issues.status_change_consumer.logger.error")
|
|
def test_with_invalid_status_change(self, mock_logger_error: MagicMock) -> None:
|
|
for status, substatus, error_msg in [
|
|
(
|
|
GroupStatus.RESOLVED,
|
|
GroupSubStatus.FOREVER,
|
|
"group.update_status.unexpected_substatus",
|
|
),
|
|
(GroupStatus.IGNORED, None, "group.update_status.missing_substatus"),
|
|
(
|
|
GroupStatus.IGNORED,
|
|
GroupSubStatus.REGRESSED,
|
|
"group.update_status.invalid_substatus",
|
|
),
|
|
(GroupStatus.UNRESOLVED, GroupSubStatus.NEW, "group.update_status.invalid_substatus"),
|
|
]:
|
|
bad_status_change = StatusChangeMessage(
|
|
fingerprint=self.fingerprint,
|
|
project_id=self.group.project_id,
|
|
new_status=status,
|
|
new_substatus=substatus,
|
|
)
|
|
produce_occurrence_to_kafka(
|
|
payload_type=PayloadType.STATUS_CHANGE,
|
|
status_change=bad_status_change,
|
|
)
|
|
processed_fingerprint = {"fingerprint": ["group-1"]}
|
|
process_occurrence_data(processed_fingerprint)
|
|
|
|
self.group.refresh_from_db()
|
|
mock_logger_error.assert_called_with(
|
|
error_msg,
|
|
extra={
|
|
"project_id": self.group.project_id,
|
|
"fingerprint": processed_fingerprint["fingerprint"],
|
|
"new_status": status,
|
|
"new_substatus": substatus,
|
|
},
|
|
)
|
|
assert self.group.status == self.initial_status
|
|
assert self.group.substatus == self.initial_substatus
|
|
|
|
@patch("sentry.issues.status_change_consumer.logger.error")
|
|
def test_invalid_hashes(self, mock_logger_error) -> None:
|
|
event = self.store_event(
|
|
data={
|
|
"event_id": "a" * 32,
|
|
"message": "oh no",
|
|
"timestamp": iso_format(datetime.now()),
|
|
"fingerprint": ["group-2"],
|
|
},
|
|
project_id=self.project.id,
|
|
)
|
|
group = event.group
|
|
assert group
|
|
initial_status = group.status
|
|
initial_substatus = group.substatus
|
|
wrong_fingerprint = {"fingerprint": ["wronghash"]}
|
|
process_occurrence_data(wrong_fingerprint)
|
|
|
|
bad_status_change_resolve = StatusChangeMessage(
|
|
fingerprint=["wronghash"],
|
|
project_id=group.project_id,
|
|
new_status=GroupStatus.RESOLVED,
|
|
new_substatus=GroupSubStatus.FOREVER,
|
|
)
|
|
produce_occurrence_to_kafka(
|
|
payload_type=PayloadType.STATUS_CHANGE,
|
|
status_change=bad_status_change_resolve,
|
|
)
|
|
group.refresh_from_db()
|
|
mock_logger_error.assert_called_with(
|
|
"grouphash.not_found",
|
|
extra={
|
|
"project_id": group.project_id,
|
|
"fingerprint": wrong_fingerprint["fingerprint"][0],
|
|
},
|
|
)
|
|
assert group.status == initial_status
|
|
assert group.substatus == initial_substatus
|