mirror of
https://github.com/hanzoai/sentinel.git
synced 2026-09-29 22:34:33 +00:00
420 lines
18 KiB
Python
420 lines
18 KiB
Python
from collections import namedtuple
|
|
from dataclasses import dataclass
|
|
from datetime import datetime, timedelta
|
|
from hashlib import md5
|
|
from unittest import mock
|
|
from unittest.mock import patch
|
|
|
|
from sentry.constants import LOG_LEVELS_MAP
|
|
from sentry.issues.grouptype import (
|
|
ErrorGroupType,
|
|
FeedbackGroup,
|
|
GroupCategory,
|
|
GroupType,
|
|
GroupTypeRegistry,
|
|
MonitorCheckInFailure,
|
|
NoiseConfig,
|
|
)
|
|
from sentry.issues.ingest import (
|
|
_create_issue_kwargs,
|
|
materialize_metadata,
|
|
save_issue_from_occurrence,
|
|
save_issue_occurrence,
|
|
send_issue_occurrence_to_eventstream,
|
|
)
|
|
from sentry.models.environment import Environment
|
|
from sentry.models.group import Group
|
|
from sentry.models.groupenvironment import GroupEnvironment
|
|
from sentry.models.grouprelease import GroupRelease
|
|
from sentry.models.release import Release, ReleaseProject
|
|
from sentry.models.releaseprojectenvironment import ReleaseProjectEnvironment
|
|
from sentry.ratelimits.sliding_windows import RequestedQuota
|
|
from sentry.receivers import create_default_projects
|
|
from sentry.snuba.dataset import Dataset
|
|
from sentry.testutils.cases import TestCase
|
|
from sentry.testutils.silo import region_silo_test
|
|
from sentry.testutils.skips import requires_snuba
|
|
from sentry.utils import json
|
|
from sentry.utils.samples import load_data
|
|
from sentry.utils.snuba import raw_query
|
|
from tests.sentry.issues.test_utils import OccurrenceTestMixin
|
|
|
|
pytestmark = [requires_snuba]
|
|
|
|
|
|
@region_silo_test
|
|
class SaveIssueOccurrenceTest(OccurrenceTestMixin, TestCase):
|
|
def test(self) -> None:
|
|
event = self.store_event(data={}, project_id=self.project.id)
|
|
occurrence = self.build_occurrence(event_id=event.event_id)
|
|
saved_occurrence, group_info = save_issue_occurrence(occurrence.to_dict(), event)
|
|
assert group_info is not None
|
|
self.assert_occurrences_identical(occurrence, saved_occurrence)
|
|
assert Group.objects.filter(grouphash__hash=saved_occurrence.fingerprint[0]).exists()
|
|
now = datetime.now()
|
|
result = raw_query(
|
|
dataset=Dataset.IssuePlatform,
|
|
start=now - timedelta(days=1),
|
|
end=now + timedelta(days=1),
|
|
selected_columns=["event_id", "group_id", "occurrence_id"],
|
|
groupby=None,
|
|
filter_keys={"project_id": [self.project.id], "event_id": [event.event_id]},
|
|
tenant_ids={"referrer": "r", "organization_id": 1},
|
|
)
|
|
assert len(result["data"]) == 1
|
|
assert result["data"][0]["group_id"] == group_info.group.id
|
|
assert result["data"][0]["event_id"] == occurrence.event_id
|
|
assert result["data"][0]["occurrence_id"] == occurrence.id
|
|
|
|
def test_new_group_release_env(self) -> None:
|
|
version = "test"
|
|
env_name = "some_env"
|
|
event = self.store_event(
|
|
data={"release": version, "environment": env_name}, project_id=self.project.id
|
|
)
|
|
release = Release.objects.get(organization_id=self.organization.id, version=version)
|
|
environment = Environment.objects.get(organization_id=self.organization.id, name=env_name)
|
|
release_project = ReleaseProject.objects.get(project=self.project, release=release)
|
|
assert release_project.new_groups == 0
|
|
release_project_env = ReleaseProjectEnvironment.objects.get(
|
|
project=self.project, release=release, environment=environment
|
|
)
|
|
assert release_project_env.new_issues_count == 0
|
|
occurrence_data = self.build_occurrence_data(event_id=event.event_id)
|
|
with self.tasks(), mock.patch("sentry.issues.ingest.eventstream") as eventstream:
|
|
occurrence, group_info = save_issue_occurrence(occurrence_data, event)
|
|
assert group_info is not None
|
|
group = group_info.group
|
|
assert group_info is not None
|
|
assert group_info.is_new
|
|
assert group_info.is_new_group_environment
|
|
assert group_info.group.first_release == release
|
|
assert GroupEnvironment.objects.filter(group=group, environment=environment)
|
|
release_project.refresh_from_db()
|
|
assert release_project.new_groups == 1
|
|
release_project_env.refresh_from_db()
|
|
assert release_project_env.new_issues_count == 1
|
|
assert GroupRelease.objects.filter(group_id=group.id, release_id=release.id).exists()
|
|
eventstream.backend.insert.assert_called_once_with(
|
|
event=event.for_group(group_info.group),
|
|
is_new=True,
|
|
is_regression=False,
|
|
is_new_group_environment=True,
|
|
primary_hash=occurrence.fingerprint[0],
|
|
received_timestamp=event.data.get("received") or event.datetime,
|
|
skip_consume=False,
|
|
group_states=[
|
|
{
|
|
"id": group_info.group.id,
|
|
"is_new": True,
|
|
"is_regression": False,
|
|
"is_new_group_environment": True,
|
|
}
|
|
],
|
|
)
|
|
|
|
def test_different_ids(self) -> None:
|
|
create_default_projects()
|
|
event_data = load_data("generic-event-profiling").data
|
|
project_id = event_data["event"].pop("project_id", self.project.id)
|
|
event_data["event"]["timestamp"] = datetime.utcnow().isoformat()
|
|
event = self.store_event(data=event_data["event"], project_id=project_id)
|
|
occurrence = self.build_occurrence()
|
|
with self.assertRaisesMessage(
|
|
ValueError, "IssueOccurrence must have the same event_id as the passed Event"
|
|
):
|
|
save_issue_occurrence(occurrence.to_dict(), event)
|
|
|
|
|
|
@region_silo_test
|
|
class ProcessOccurrenceDataTest(OccurrenceTestMixin, TestCase):
|
|
def test(self) -> None:
|
|
data = self.build_occurrence_data(fingerprint=["hi", "bye"])
|
|
assert data["fingerprint"] == [
|
|
md5(b"hi").hexdigest(),
|
|
md5(b"bye").hexdigest(),
|
|
]
|
|
|
|
|
|
@region_silo_test
|
|
class SaveIssueFromOccurrenceTest(OccurrenceTestMixin, TestCase):
|
|
def test_new_group(self) -> None:
|
|
occurrence = self.build_occurrence(type=ErrorGroupType.type_id)
|
|
event = self.store_event(
|
|
data={
|
|
"platform": "javascript",
|
|
"sdk": {"name": "sentry.javascript.nextjs", "version": "1.2.3"},
|
|
},
|
|
project_id=self.project.id,
|
|
)
|
|
|
|
with patch("sentry.issues.ingest.metrics.incr") as mock_metrics_incr:
|
|
group_info = save_issue_from_occurrence(occurrence, event, None)
|
|
assert group_info is not None
|
|
assert group_info.is_new
|
|
assert not group_info.is_regression
|
|
|
|
group = group_info.group
|
|
assert group.title == occurrence.issue_title
|
|
assert group.platform == event.platform
|
|
assert group.level == LOG_LEVELS_MAP.get(occurrence.level)
|
|
assert group.last_seen == event.datetime
|
|
assert group.first_seen == event.datetime
|
|
assert group.active_at == event.datetime
|
|
assert group.issue_type == occurrence.type
|
|
assert group.first_release is None
|
|
assert group.title == occurrence.issue_title
|
|
assert group.data["metadata"]["value"] == occurrence.subtitle
|
|
assert group.culprit == occurrence.culprit
|
|
assert group.message == "<unlabeled event> something bad happened it was bad api/123"
|
|
assert group.location() == event.location
|
|
mock_metrics_incr.assert_any_call(
|
|
"group.created",
|
|
skip_internal=True,
|
|
tags={
|
|
"platform": "javascript",
|
|
"type": ErrorGroupType.type_id,
|
|
"sdk": "sentry.javascript.nextjs",
|
|
},
|
|
)
|
|
|
|
def test_existing_group(self) -> None:
|
|
event = self.store_event(data={}, project_id=self.project.id)
|
|
occurrence = self.build_occurrence(fingerprint=["some-fingerprint"])
|
|
save_issue_from_occurrence(occurrence, event, None)
|
|
|
|
new_event = self.store_event(data={}, project_id=self.project.id)
|
|
new_occurrence = self.build_occurrence(
|
|
fingerprint=["some-fingerprint"], subtitle="new subtitle", issue_title="new title"
|
|
)
|
|
with self.tasks():
|
|
updated_group_info = save_issue_from_occurrence(new_occurrence, new_event, None)
|
|
assert updated_group_info is not None
|
|
updated_group = updated_group_info.group
|
|
updated_group.refresh_from_db()
|
|
assert updated_group_info.group.id == updated_group.id
|
|
assert not updated_group_info.is_new
|
|
assert not updated_group_info.is_regression
|
|
assert updated_group.title == new_occurrence.issue_title
|
|
assert updated_group.data["metadata"]["value"] == new_occurrence.subtitle
|
|
assert updated_group.culprit == new_occurrence.culprit
|
|
assert updated_group.location() == event.location
|
|
assert updated_group.times_seen == 2
|
|
assert updated_group.message == "<unlabeled event> new title new subtitle api/123"
|
|
|
|
def test_existing_group_different_category(self) -> None:
|
|
event = self.store_event(data={}, project_id=self.project.id)
|
|
occurrence = self.build_occurrence(fingerprint=["some-fingerprint"])
|
|
group_info = save_issue_from_occurrence(occurrence, event, None)
|
|
assert group_info is not None
|
|
|
|
new_event = self.store_event(data={}, project_id=self.project.id)
|
|
new_occurrence = self.build_occurrence(
|
|
fingerprint=["some-fingerprint"], type=MonitorCheckInFailure.type_id
|
|
)
|
|
with mock.patch("sentry.issues.ingest.logger") as logger:
|
|
assert save_issue_from_occurrence(new_occurrence, new_event, None) is None
|
|
logger.error.assert_called_once_with(
|
|
"save_issue_from_occurrence.category_mismatch",
|
|
extra={
|
|
"issue_category": group_info.group.issue_category,
|
|
"event_type": "platform",
|
|
"group_id": group_info.group.id,
|
|
},
|
|
)
|
|
|
|
def test_rate_limited(self) -> None:
|
|
MockGranted = namedtuple("MockGranted", ["granted"])
|
|
event = self.store_event(data={}, project_id=self.project.id)
|
|
occurrence = self.build_occurrence()
|
|
group_info = save_issue_from_occurrence(occurrence, event, None)
|
|
assert group_info is not None
|
|
|
|
new_event = self.store_event(data={}, project_id=self.project.id)
|
|
new_occurrence = self.build_occurrence(fingerprint=["another-fingerprint"])
|
|
with mock.patch("sentry.issues.ingest.metrics") as metrics, mock.patch(
|
|
"sentry.issues.ingest.issue_rate_limiter.check_and_use_quotas",
|
|
return_value=[MockGranted(granted=False)],
|
|
) as check_and_use_quotas:
|
|
assert save_issue_from_occurrence(new_occurrence, new_event, None) is None
|
|
metrics.incr.assert_called_once_with("issues.issue.dropped.rate_limiting")
|
|
assert check_and_use_quotas.call_count == 1
|
|
assert check_and_use_quotas.call_args[0][0] == [
|
|
RequestedQuota(
|
|
f"issue-platform-issues:{self.project.id}:{occurrence.type.slug}",
|
|
1,
|
|
[occurrence.type.creation_quota],
|
|
)
|
|
]
|
|
|
|
def test_noise_reduction(self) -> None:
|
|
with patch("sentry.issues.grouptype.registry", new=GroupTypeRegistry()):
|
|
|
|
@dataclass(frozen=True)
|
|
class TestGroupType(GroupType):
|
|
type_id = 1
|
|
slug = "test"
|
|
description = "Test"
|
|
category = GroupCategory.PROFILE.value
|
|
noise_config = NoiseConfig(ignore_limit=2)
|
|
|
|
event = self.store_event(data={}, project_id=self.project.id)
|
|
occurrence = self.build_occurrence(type=TestGroupType.type_id)
|
|
with mock.patch("sentry.issues.ingest.metrics") as metrics:
|
|
assert save_issue_from_occurrence(occurrence, event, None) is None
|
|
metrics.incr.assert_called_once_with("issues.issue.dropped.noise_reduction")
|
|
|
|
new_event = self.store_event(data={}, project_id=self.project.id)
|
|
new_occurrence = self.build_occurrence(type=TestGroupType.type_id)
|
|
group_info = save_issue_from_occurrence(new_occurrence, new_event, None)
|
|
assert group_info is not None
|
|
|
|
def test_frame_mix_metric_logged(self) -> None:
|
|
event = self.store_event(
|
|
data={
|
|
"platform": "javascript",
|
|
"sdk": {"name": "sentry.javascript.nextjs", "version": "1.2.3"},
|
|
},
|
|
project_id=self.project.id,
|
|
)
|
|
|
|
# Normally this is done by `normalize_stacktraces_for_grouping`, but that can't be mocked
|
|
# because it's imported inside its calling function to avoid circular imports
|
|
event.data.setdefault("metadata", {})
|
|
event.data["metadata"]["in_app_frame_mix"] = "in-app-only"
|
|
|
|
with patch("sentry.issues.ingest.metrics.incr") as mock_metrics_incr:
|
|
occurrence = self.build_occurrence()
|
|
save_issue_from_occurrence(occurrence, event, None)
|
|
|
|
mock_metrics_incr.assert_any_call(
|
|
"grouping.in_app_frame_mix",
|
|
sample_rate=1.0,
|
|
tags={
|
|
"platform": "javascript",
|
|
"frame_mix": "in-app-only",
|
|
"sdk": "sentry.javascript.nextjs",
|
|
},
|
|
)
|
|
|
|
def test_frame_mix_metric_not_logged(self) -> None:
|
|
event = self.store_event(data={}, project_id=self.project.id)
|
|
|
|
assert event.get_event_metadata().get("in_app_frame_mix") is None
|
|
|
|
with patch("sentry.issues.ingest.metrics.incr") as mock_metrics_incr:
|
|
occurrence = self.build_occurrence()
|
|
save_issue_from_occurrence(occurrence, event, None)
|
|
|
|
metrics_logged = [call.args[0] for call in mock_metrics_incr.mock_calls]
|
|
assert "grouping.in_app_frame_mix" not in metrics_logged
|
|
|
|
|
|
class CreateIssueKwargsTest(OccurrenceTestMixin, TestCase):
|
|
def test(self) -> None:
|
|
occurrence = self.build_occurrence()
|
|
event = self.store_event(data={}, project_id=self.project.id)
|
|
assert _create_issue_kwargs(occurrence, event, None) == {
|
|
"platform": event.platform,
|
|
"message": event.search_message,
|
|
"level": LOG_LEVELS_MAP.get(occurrence.level),
|
|
"culprit": occurrence.culprit,
|
|
"last_seen": event.datetime,
|
|
"first_seen": event.datetime,
|
|
"active_at": event.datetime,
|
|
"type": occurrence.type.type_id,
|
|
"first_release": None,
|
|
"data": materialize_metadata(occurrence, event),
|
|
"priority": occurrence.initial_issue_priority,
|
|
}
|
|
|
|
|
|
class MaterializeMetadataTest(OccurrenceTestMixin, TestCase):
|
|
def test_simple(self) -> None:
|
|
occurrence = self.build_occurrence()
|
|
event = self.store_event(data={}, project_id=self.project.id)
|
|
assert materialize_metadata(occurrence, event) == {
|
|
"type": "default",
|
|
"culprit": occurrence.culprit,
|
|
"metadata": {"title": occurrence.issue_title, "value": occurrence.subtitle},
|
|
"title": occurrence.issue_title,
|
|
"location": event.location,
|
|
"last_received": json.datetime_to_str(event.datetime),
|
|
"initial_priority": occurrence.initial_issue_priority,
|
|
}
|
|
|
|
def test_preserves_existing_metadata(self) -> None:
|
|
occurrence = self.build_occurrence()
|
|
event = self.store_event(data={}, project_id=self.project.id)
|
|
event.data.setdefault("metadata", {})
|
|
event.data["metadata"]["dogs"] = "are great" # should not get clobbered
|
|
|
|
materialized = materialize_metadata(occurrence, event)
|
|
assert materialized["metadata"] == {
|
|
"title": occurrence.issue_title,
|
|
"value": occurrence.subtitle,
|
|
"dogs": "are great",
|
|
}
|
|
|
|
def test_populates_feedback_metadata(self) -> None:
|
|
occurrence = self.build_occurrence(
|
|
type=FeedbackGroup.type_id,
|
|
evidence_data={
|
|
"contact_email": "[email protected]",
|
|
"message": "test",
|
|
"name": "Name Test",
|
|
"source": "crash report widget",
|
|
},
|
|
)
|
|
event = self.store_event(data={}, project_id=self.project.id)
|
|
event.data.setdefault("metadata", {})
|
|
event.data["metadata"]["dogs"] = "are great" # should not get clobbered
|
|
|
|
materialized = materialize_metadata(occurrence, event)
|
|
assert materialized["metadata"] == {
|
|
"title": occurrence.issue_title,
|
|
"value": occurrence.subtitle,
|
|
"dogs": "are great",
|
|
"contact_email": "[email protected]",
|
|
"message": "test",
|
|
"name": "Name Test",
|
|
"source": "crash report widget",
|
|
}
|
|
|
|
|
|
@region_silo_test
|
|
class SaveIssueOccurrenceToEventstreamTest(OccurrenceTestMixin, TestCase):
|
|
def test(self) -> None:
|
|
create_default_projects()
|
|
event_data = load_data("generic-event-profiling").data
|
|
project_id = event_data["event"].pop("project_id")
|
|
event_data["event"]["timestamp"] = datetime.utcnow().isoformat()
|
|
event = self.store_event(data=event_data["event"], project_id=project_id)
|
|
occurrence = self.build_occurrence(event_id=event.event_id)
|
|
group_info = save_issue_from_occurrence(occurrence, event, None)
|
|
assert group_info is not None
|
|
|
|
group_event = event.for_group(group_info.group)
|
|
with mock.patch("sentry.issues.ingest.eventstream") as eventstream, mock.patch.object(
|
|
event, "for_group", return_value=group_event
|
|
):
|
|
send_issue_occurrence_to_eventstream(event, occurrence, group_info)
|
|
eventstream.backend.insert.assert_called_once_with(
|
|
event=group_event,
|
|
is_new=group_info.is_new,
|
|
is_regression=group_info.is_regression,
|
|
is_new_group_environment=group_info.is_new_group_environment,
|
|
primary_hash=occurrence.fingerprint[0],
|
|
received_timestamp=group_event.data.get("received")
|
|
or group_event.datetime.timestamp(),
|
|
skip_consume=False,
|
|
group_states=[
|
|
{
|
|
"id": group_info.group.id,
|
|
"is_new": group_info.is_new,
|
|
"is_regression": group_info.is_regression,
|
|
"is_new_group_environment": group_info.is_new_group_environment,
|
|
}
|
|
],
|
|
)
|