mirror of
https://github.com/hanzoai/sentinel.git
synced 2026-09-29 22:34:33 +00:00
1970 lines
77 KiB
Python
1970 lines
77 KiB
Python
import uuid
|
|
import zoneinfo
|
|
from collections import Counter
|
|
from collections.abc import Mapping, Sequence
|
|
from datetime import datetime, timedelta, timezone
|
|
from functools import cached_property
|
|
from unittest import mock
|
|
from unittest.mock import ANY
|
|
|
|
from django.contrib.auth.models import AnonymousUser
|
|
from django.core import mail
|
|
from django.core.mail.message import EmailMultiAlternatives
|
|
from django.db.models import F
|
|
from django.utils import timezone as django_timezone
|
|
|
|
from sentry.api.serializers import serialize
|
|
from sentry.api.serializers.models.userreport import UserReportWithGroupSerializer
|
|
from sentry.digests.notifications import build_digest, event_to_record
|
|
from sentry.event_manager import EventManager, get_event_type
|
|
from sentry.issues.grouptype import MonitorCheckInFailure
|
|
from sentry.issues.issue_occurrence import IssueEvidence, IssueOccurrence
|
|
from sentry.mail import build_subject_prefix, mail_adapter
|
|
from sentry.models.activity import Activity
|
|
from sentry.models.grouprelease import GroupRelease
|
|
from sentry.models.notificationsettingoption import NotificationSettingOption
|
|
from sentry.models.notificationsettingprovider import NotificationSettingProvider
|
|
from sentry.models.options.project_option import ProjectOption
|
|
from sentry.models.options.user_option import UserOption
|
|
from sentry.models.organization import Organization
|
|
from sentry.models.organizationmember import OrganizationMember
|
|
from sentry.models.organizationmemberteam import OrganizationMemberTeam
|
|
from sentry.models.project import Project
|
|
from sentry.models.projectownership import ProjectOwnership
|
|
from sentry.models.repository import Repository
|
|
from sentry.models.rule import Rule
|
|
from sentry.models.useremail import UserEmail
|
|
from sentry.models.userreport import UserReport
|
|
from sentry.notifications.notifications.rules import AlertRuleNotification
|
|
from sentry.notifications.types import ActionTargetType, FallthroughChoiceType
|
|
from sentry.notifications.utils.digest import get_digest_subject
|
|
from sentry.ownership import grammar
|
|
from sentry.ownership.grammar import Matcher, Owner, dump_schema
|
|
from sentry.plugins.base import Notification
|
|
from sentry.replays.testutils import mock_replay
|
|
from sentry.services.hybrid_cloud.actor import RpcActor
|
|
from sentry.silo import SiloMode
|
|
from sentry.testutils.cases import PerformanceIssueTestCase, ReplaysSnubaTestCase, TestCase
|
|
from sentry.testutils.helpers.datetime import before_now, iso_format
|
|
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
|
|
from sentry.types.rules import RuleFuture
|
|
from sentry.utils.dates import ensure_aware
|
|
from sentry.utils.email import MessageBuilder, get_email_addresses
|
|
from sentry_plugins.opsgenie.plugin import OpsGeniePlugin
|
|
from tests.sentry.mail import make_event_data, mock_notify
|
|
|
|
pytestmark = requires_snuba
|
|
|
|
|
|
class BaseMailAdapterTest(TestCase, PerformanceIssueTestCase):
|
|
@cached_property
|
|
def adapter(self):
|
|
return mail_adapter
|
|
|
|
def assert_notify(
|
|
self,
|
|
event,
|
|
emails_sent_to,
|
|
target_type=ActionTargetType.ISSUE_OWNERS,
|
|
target_identifier=None,
|
|
):
|
|
mail.outbox = []
|
|
with self.options({"system.url-prefix": "http://example.com"}), self.tasks():
|
|
self.adapter.notify(Notification(event=event), target_type, target_identifier)
|
|
assert sorted(email.to[0] for email in mail.outbox) == sorted(emails_sent_to)
|
|
|
|
|
|
@region_silo_test
|
|
class MailAdapterGetSendableUsersTest(BaseMailAdapterTest):
|
|
def test_get_sendable_user_objects(self):
|
|
user = self.create_user(email="[email protected]", is_active=True)
|
|
user2 = self.create_user(email="[email protected]", is_active=True)
|
|
self.create_user(email="[email protected]", is_active=True)
|
|
|
|
# user with inactive account
|
|
self.create_user(email="[email protected]", is_active=False)
|
|
# user not in any groups
|
|
self.create_user(email="[email protected]", is_active=True)
|
|
|
|
organization = self.create_organization(owner=user)
|
|
team = self.create_team(organization=organization)
|
|
|
|
project = self.create_project(name="Test", teams=[team])
|
|
OrganizationMemberTeam.objects.create(
|
|
organizationmember=OrganizationMember.objects.get(
|
|
user_id=user.id, organization=organization
|
|
),
|
|
team=team,
|
|
)
|
|
self.create_member(user=user2, organization=organization, teams=[team])
|
|
|
|
# all members
|
|
users = self.adapter.get_sendable_user_objects(project)
|
|
assert sorted({user.id, user2.id}) == sorted(user.id for user in users)
|
|
|
|
# disabled user2
|
|
with assume_test_silo_mode(SiloMode.CONTROL):
|
|
NotificationSettingOption.objects.create(
|
|
user_id=user2.id,
|
|
scope_type="project",
|
|
scope_identifier=project.id,
|
|
type="alerts",
|
|
value="never",
|
|
)
|
|
assert user2 not in self.adapter.get_sendable_user_objects(project)
|
|
|
|
user4 = self.create_user(username="baz4", email="[email protected]", is_active=True)
|
|
self.create_member(user=user4, organization=organization, teams=[team])
|
|
assert user4.id in {u.id for u in self.adapter.get_sendable_user_objects(project)}
|
|
|
|
# disabled by default user4
|
|
with assume_test_silo_mode(SiloMode.CONTROL):
|
|
NotificationSettingOption.objects.create(
|
|
user_id=user4.id,
|
|
scope_type="user",
|
|
scope_identifier=user4.id,
|
|
type="alerts",
|
|
value="never",
|
|
)
|
|
|
|
assert user4 not in self.adapter.get_sendable_user_objects(project)
|
|
|
|
with assume_test_silo_mode(SiloMode.CONTROL):
|
|
NotificationSettingOption.objects.filter(
|
|
user_id=user4.id,
|
|
scope_type="user",
|
|
scope_identifier=user4.id,
|
|
type="alerts",
|
|
).update(value="never")
|
|
|
|
assert user4.id not in {u.id for u in self.adapter.get_sendable_user_objects(project)}
|
|
|
|
|
|
@region_silo_test
|
|
class MailAdapterBuildSubjectPrefixTest(BaseMailAdapterTest):
|
|
def test_default_prefix(self):
|
|
assert build_subject_prefix(self.project) == "[Sentry]"
|
|
|
|
def test_project_level_prefix(self):
|
|
prefix = "[Example prefix]"
|
|
ProjectOption.objects.set_value(
|
|
project=self.project, key="mail:subject_prefix", value=prefix
|
|
)
|
|
assert build_subject_prefix(self.project) == prefix
|
|
|
|
|
|
@region_silo_test
|
|
class MailAdapterNotifyTest(BaseMailAdapterTest):
|
|
@mock.patch("sentry.analytics.record")
|
|
def test_simple_notification(self, mock_record):
|
|
event = self.store_event(
|
|
data={"message": "Hello world", "level": "error"}, project_id=self.project.id
|
|
)
|
|
|
|
rule = Rule.objects.create(project=self.project, label="my rule")
|
|
ProjectOwnership.objects.create(project_id=self.project.id, fallthrough=True)
|
|
|
|
notification = Notification(event=event, rule=rule)
|
|
|
|
with self.options({"system.url-prefix": "http://example.com"}), self.tasks():
|
|
self.adapter.notify(
|
|
notification,
|
|
ActionTargetType.ISSUE_OWNERS,
|
|
fallthrough_choice=FallthroughChoiceType.ACTIVE_MEMBERS,
|
|
)
|
|
|
|
msg = mail.outbox[0]
|
|
assert isinstance(msg, EmailMultiAlternatives)
|
|
assert msg.subject == "[Sentry] BAR-1 - Hello world"
|
|
assert isinstance(msg.alternatives[0][0], str)
|
|
assert "my rule" in msg.alternatives[0][0]
|
|
assert "notification_uuid" in msg.body
|
|
mock_record.assert_any_call(
|
|
"integrations.email.notification_sent",
|
|
category="issue_alert",
|
|
target_type=ANY,
|
|
target_identifier=None,
|
|
project_id=self.project.id,
|
|
organization_id=self.organization.id,
|
|
group_id=event.group_id,
|
|
user_id=ANY,
|
|
id=ANY,
|
|
actor_type="User",
|
|
notification_uuid=ANY,
|
|
alert_id=rule.id,
|
|
)
|
|
mock_record.assert_called_with(
|
|
"alert.sent",
|
|
organization_id=self.organization.id,
|
|
project_id=self.project.id,
|
|
provider="email",
|
|
alert_id=rule.id,
|
|
alert_type="issue_alert",
|
|
external_id=ANY,
|
|
notification_uuid=ANY,
|
|
)
|
|
|
|
def test_notification_with_environment(self):
|
|
environment = self.create_environment(self.project, name="production")
|
|
event = self.store_event(
|
|
data={"message": "Hello world", "level": "error", "environment": environment.name},
|
|
project_id=self.project.id,
|
|
)
|
|
|
|
rule = Rule.objects.create(
|
|
project=self.project, label="my rule", environment_id=environment.id
|
|
)
|
|
ProjectOwnership.objects.create(project_id=self.project.id)
|
|
|
|
notification = Notification(event=event, rule=rule)
|
|
|
|
with self.options({"system.url-prefix": "http://example.com"}), self.tasks():
|
|
self.adapter.notify(
|
|
notification,
|
|
ActionTargetType.ISSUE_OWNERS,
|
|
fallthrough_choice=FallthroughChoiceType.ACTIVE_MEMBERS,
|
|
)
|
|
|
|
msg = mail.outbox[0]
|
|
assert isinstance(msg, EmailMultiAlternatives)
|
|
assert msg.subject == "[Sentry] BAR-1 - Hello world"
|
|
assert isinstance(msg.alternatives[0][0], str)
|
|
assert "my rule" in msg.alternatives[0][0]
|
|
assert f"&environment={environment.name}" in msg.body
|
|
assert "notification_uuid" in msg.body
|
|
|
|
def test_simple_snooze(self):
|
|
"""Test that notification for alert snoozed by user is not send to that user."""
|
|
event = self.store_event(
|
|
data={"message": "Hello world", "level": "error"}, project_id=self.project.id
|
|
)
|
|
|
|
rule = self.create_project_rule(project=self.project)
|
|
self.snooze_rule(user_id=self.user.id, owner_id=self.user.id, rule=rule)
|
|
ProjectOwnership.objects.create(project_id=self.project.id, fallthrough=True)
|
|
|
|
notification = Notification(event=event, rule=rule)
|
|
|
|
with self.options({"system.url-prefix": "http://example.com"}), self.tasks():
|
|
self.adapter.notify(notification, ActionTargetType.ISSUE_OWNERS)
|
|
|
|
assert len(mail.outbox) == 0
|
|
|
|
def test_snooze_for_all(self):
|
|
"""Test that notification for alert snoozed for everyone is not send to user."""
|
|
event = self.store_event(
|
|
data={"message": "Hello world", "level": "error"}, project_id=self.project.id
|
|
)
|
|
|
|
rule = self.create_project_rule(project=self.project)
|
|
self.snooze_rule(owner_id=self.user.id, rule=rule)
|
|
ProjectOwnership.objects.create(project_id=self.project.id, fallthrough=True)
|
|
|
|
notification = Notification(event=event, rule=rule)
|
|
|
|
with self.options({"system.url-prefix": "http://example.com"}), self.tasks():
|
|
self.adapter.notify(notification, ActionTargetType.ISSUE_OWNERS)
|
|
|
|
assert len(mail.outbox) == 0
|
|
|
|
def test_someone_else_snoozes_themself(self):
|
|
"""Test that notification for alert snoozed by user2 for themself is sent to user"""
|
|
event = self.store_event(
|
|
data={"message": "Hello world", "level": "error"}, project_id=self.project.id
|
|
)
|
|
|
|
rule = self.create_project_rule(project=self.project)
|
|
user2 = self.create_user(email="[email protected]")
|
|
self.snooze_rule(user_id=user2.id, owner_id=user2.id, rule=rule)
|
|
ProjectOwnership.objects.create(project_id=self.project.id, fallthrough=True)
|
|
|
|
notification = Notification(event=event, rule=rule)
|
|
|
|
with self.options({"system.url-prefix": "http://example.com"}), self.tasks():
|
|
self.adapter.notify(
|
|
notification,
|
|
ActionTargetType.ISSUE_OWNERS,
|
|
fallthrough_choice=FallthroughChoiceType.ACTIVE_MEMBERS,
|
|
)
|
|
|
|
assert len(mail.outbox) == 1
|
|
msg = mail.outbox[0]
|
|
assert msg.subject == "[Sentry] BAR-1 - Hello world"
|
|
|
|
def test_someone_else_snoozes_everyone(self):
|
|
"""Test that notification for alert snoozed by user2 for everyone is not sent to user"""
|
|
event = self.store_event(
|
|
data={"message": "Hello world", "level": "error"}, project_id=self.project.id
|
|
)
|
|
|
|
rule = self.create_project_rule(project=self.project)
|
|
user2 = self.create_user(email="[email protected]")
|
|
self.snooze_rule(owner_id=user2.id, rule=rule)
|
|
ProjectOwnership.objects.create(project_id=self.project.id, fallthrough=True)
|
|
|
|
notification = Notification(event=event, rule=rule)
|
|
|
|
with self.options({"system.url-prefix": "http://example.com"}), self.tasks():
|
|
self.adapter.notify(notification, ActionTargetType.ISSUE_OWNERS)
|
|
|
|
assert len(mail.outbox) == 0
|
|
|
|
def test_simple_notification_generic(self):
|
|
"""Test that an issue that is neither error nor performance type renders a generic email template"""
|
|
orig_event = self.store_event(
|
|
data={"message": "Hello world", "level": "error"}, project_id=self.project.id
|
|
)
|
|
event = orig_event.for_group(orig_event.groups[0])
|
|
occurrence = IssueOccurrence(
|
|
uuid.uuid4().hex,
|
|
self.project.id,
|
|
uuid.uuid4().hex,
|
|
["some-fingerprint"],
|
|
"something bad happened",
|
|
"it was bad",
|
|
"1234",
|
|
{"Test": 123},
|
|
[
|
|
IssueEvidence("Evidence 1", "Value 1", True),
|
|
IssueEvidence("Evidence 2", "Value 2", False),
|
|
IssueEvidence("Evidence 3", "Value 3", False),
|
|
],
|
|
MonitorCheckInFailure,
|
|
ensure_aware(datetime.now()),
|
|
"info",
|
|
"/api/123",
|
|
)
|
|
occurrence.save()
|
|
event.occurrence = occurrence
|
|
|
|
event.group.type = MonitorCheckInFailure.type_id
|
|
|
|
rule = Rule.objects.create(project=self.project, label="my rule")
|
|
ProjectOwnership.objects.create(project_id=self.project.id, fallthrough=True)
|
|
|
|
notification = Notification(event=event, rule=rule)
|
|
|
|
with self.options({"system.url-prefix": "http://example.com"}), self.tasks():
|
|
self.adapter.notify(
|
|
notification,
|
|
ActionTargetType.ISSUE_OWNERS,
|
|
fallthrough_choice=FallthroughChoiceType.ACTIVE_MEMBERS,
|
|
)
|
|
|
|
msg = mail.outbox[0]
|
|
assert isinstance(msg, EmailMultiAlternatives)
|
|
assert msg.subject == f"[Sentry] BAR-1 - {occurrence.issue_title}"
|
|
checked_values = [
|
|
"Issue Data",
|
|
"Evidence 1",
|
|
"Value 1",
|
|
"Evidence 2",
|
|
"Value 2",
|
|
"Evidence 3",
|
|
"Value 3",
|
|
]
|
|
for checked_value in checked_values:
|
|
assert isinstance(msg.alternatives[0][0], str)
|
|
assert (
|
|
checked_value in msg.alternatives[0][0]
|
|
), f"{checked_value} not present in message"
|
|
|
|
def test_simple_notification_generic_no_evidence(self):
|
|
"""Test that an issue with no evidence that is neither error nor performance type renders a generic email template"""
|
|
orig_event = self.store_event(
|
|
data={"message": "Hello world", "level": "error"}, project_id=self.project.id
|
|
)
|
|
event = orig_event.for_group(orig_event.groups[0])
|
|
occurrence = IssueOccurrence(
|
|
uuid.uuid4().hex,
|
|
self.project.id,
|
|
uuid.uuid4().hex,
|
|
["some-fingerprint"],
|
|
"something bad happened",
|
|
"it was bad",
|
|
"1234",
|
|
{"Test": 123},
|
|
[], # no evidence
|
|
MonitorCheckInFailure,
|
|
ensure_aware(datetime.now()),
|
|
"info",
|
|
"/api/123",
|
|
)
|
|
occurrence.save()
|
|
event.occurrence = occurrence
|
|
|
|
event.group.type = MonitorCheckInFailure.type_id
|
|
|
|
rule = Rule.objects.create(project=self.project, label="my rule")
|
|
ProjectOwnership.objects.create(project_id=self.project.id, fallthrough=True)
|
|
|
|
notification = Notification(event=event, rule=rule)
|
|
|
|
with self.options({"system.url-prefix": "http://example.com"}), self.tasks():
|
|
self.adapter.notify(
|
|
notification,
|
|
ActionTargetType.ISSUE_OWNERS,
|
|
fallthrough_choice=FallthroughChoiceType.ACTIVE_MEMBERS,
|
|
)
|
|
|
|
msg = mail.outbox[0]
|
|
assert isinstance(msg, EmailMultiAlternatives)
|
|
assert msg.subject == "[Sentry] BAR-1 - something bad happened"
|
|
assert isinstance(msg.alternatives[0][0], str)
|
|
assert "Issue Data" not in msg.alternatives[0][0]
|
|
|
|
def test_simple_notification_perf(self):
|
|
event = self.create_performance_issue()
|
|
rule = Rule.objects.create(project=self.project, label="my rule")
|
|
ProjectOwnership.objects.create(project_id=self.project.id, fallthrough=True)
|
|
|
|
notification = Notification(event=event, rule=rule)
|
|
|
|
with self.options({"system.url-prefix": "http://example.com"}), self.tasks():
|
|
self.adapter.notify(
|
|
notification,
|
|
ActionTargetType.ISSUE_OWNERS,
|
|
fallthrough_choice=FallthroughChoiceType.ACTIVE_MEMBERS,
|
|
)
|
|
|
|
msg = mail.outbox[0]
|
|
assert isinstance(msg, EmailMultiAlternatives)
|
|
assert msg.subject == "[Sentry] BAR-1 - N+1 Query"
|
|
checked_values = [
|
|
"Transaction Name",
|
|
# TODO: Not sure if this is right
|
|
"db - SELECT `books_author`.`id`, `books_author`.`",
|
|
"Parent Span",
|
|
"django.view - index",
|
|
"Repeating Spans (10)",
|
|
"db - SELECT `books_author`.`id`, `books_author`.`name` FROM `books_autho...",
|
|
]
|
|
for checked_value in checked_values:
|
|
assert isinstance(msg.alternatives[0][0], str)
|
|
assert (
|
|
checked_value in msg.alternatives[0][0]
|
|
), f"{checked_value} not present in message"
|
|
assert "notification_uuid" in msg.body
|
|
|
|
@mock.patch("sentry.interfaces.stacktrace.Stacktrace.get_title")
|
|
@mock.patch("sentry.interfaces.stacktrace.Stacktrace.to_email_html")
|
|
def test_notify_users_renders_interfaces_with_utf8(self, _to_email_html, _get_title):
|
|
_to_email_html.return_value = "רונית מגן"
|
|
_get_title.return_value = "Stacktrace"
|
|
|
|
event = self.store_event(
|
|
data={"message": "Soubor ji\xc5\xbe existuje", "stacktrace": {"frames": [{}]}},
|
|
project_id=self.project.id,
|
|
)
|
|
|
|
notification = Notification(event=event)
|
|
ProjectOwnership.objects.create(project_id=self.project.id, fallthrough=True)
|
|
|
|
with self.options({"system.url-prefix": "http://example.com"}):
|
|
self.adapter.notify(
|
|
notification,
|
|
ActionTargetType.ISSUE_OWNERS,
|
|
fallthrough_choice=FallthroughChoiceType.ACTIVE_MEMBERS,
|
|
)
|
|
|
|
_get_title.assert_called_once_with()
|
|
_to_email_html.assert_called_once_with(event)
|
|
|
|
@mock_notify
|
|
@mock.patch("sentry.notifications.notifications.rules.logger")
|
|
def test_notify_users_does_email(self, mock_logger, mock_func):
|
|
self.create_user_option(user=self.user, key="timezone", value="Europe/Vienna")
|
|
event_manager = EventManager({"message": "hello world", "level": "error"})
|
|
event_manager.normalize()
|
|
event_data = event_manager.get_data()
|
|
event_type = get_event_type(event_data)
|
|
event_data["type"] = event_type.key
|
|
event_data["metadata"] = event_type.get_metadata(event_data)
|
|
|
|
event = event_manager.save(self.project.id)
|
|
group = event.group
|
|
|
|
self.create_notification_settings_provider(
|
|
user_id=self.user.id,
|
|
scope_type="user",
|
|
scope_identifier=self.user.id,
|
|
provider="slack",
|
|
type="alerts",
|
|
value="never",
|
|
)
|
|
ProjectOwnership.objects.create(project_id=self.project.id, fallthrough=True)
|
|
|
|
with self.tasks():
|
|
AlertRuleNotification(
|
|
Notification(event=event),
|
|
ActionTargetType.ISSUE_OWNERS,
|
|
fallthrough_choice=FallthroughChoiceType.ACTIVE_MEMBERS,
|
|
).send()
|
|
|
|
assert mock_func.call_count == 1
|
|
|
|
args, kwargs = mock_func.call_args
|
|
notification = args[1]
|
|
|
|
recipient_context = notification.get_recipient_context(
|
|
RpcActor.from_orm_user(self.user), {}
|
|
)
|
|
assert recipient_context["timezone"] == zoneinfo.ZoneInfo("Europe/Vienna")
|
|
|
|
self.assertEqual(notification.project, self.project)
|
|
self.assertEqual(notification.reference, group)
|
|
assert notification.get_subject() == "BAR-1 - hello world"
|
|
|
|
assert group
|
|
mock_logger.info.assert_called_with(
|
|
"mail.adapter.notify",
|
|
extra={
|
|
"target_type": "IssueOwners",
|
|
"target_identifier": None,
|
|
"group": group.id,
|
|
"project_id": group.project.id,
|
|
"organization": group.organization.id,
|
|
"fallthrough_choice": "ActiveMembers",
|
|
"notification_uuid": mock.ANY,
|
|
},
|
|
)
|
|
|
|
@mock_notify
|
|
def test_email_notification_is_not_sent_to_deleted_email(self, mock_func):
|
|
"""
|
|
Test that ensures if we still have some stale emails in UserOption, then upon attempting
|
|
to send an email notification to those emails, these stale `UserOption` instances are
|
|
deleted
|
|
"""
|
|
# Initial Creation
|
|
self.organization = self.create_organization()
|
|
self.team = self.create_team(organization=self.organization)
|
|
user = self.create_user(email="[email protected]", is_active=True)
|
|
self.create_member(user=user, organization=self.organization, teams=[self.team])
|
|
|
|
with assume_test_silo_mode(SiloMode.CONTROL):
|
|
UserOption.objects.create(
|
|
user=user, key="mail:email", value="[email protected]", project_id=self.project.id
|
|
)
|
|
# disable slack
|
|
NotificationSettingProvider.objects.create(
|
|
user_id=user.id,
|
|
scope_type="user",
|
|
scope_identifier=user.id,
|
|
provider="slack",
|
|
type="alerts",
|
|
value="never",
|
|
)
|
|
ProjectOwnership.objects.create(project_id=self.project.id, fallthrough=True)
|
|
|
|
with assume_test_silo_mode(SiloMode.CONTROL):
|
|
# New secondary email is created
|
|
useremail = UserEmail.objects.create(
|
|
user=user, email="[email protected]", is_verified=True
|
|
)
|
|
# Set secondary email to be primary
|
|
user.email = useremail.email
|
|
user.save()
|
|
|
|
# Delete first email
|
|
old_useremail = UserEmail.objects.get(email="[email protected]")
|
|
old_useremail.delete()
|
|
|
|
event_manager = EventManager({"message": "hello world", "level": "error"})
|
|
event_manager.normalize()
|
|
event_data = event_manager.get_data()
|
|
event_type = get_event_type(event_data)
|
|
event_data["type"] = event_type.key
|
|
event_data["metadata"] = event_type.get_metadata(event_data)
|
|
|
|
event = event_manager.save(self.project.id)
|
|
|
|
with self.tasks():
|
|
AlertRuleNotification(
|
|
Notification(event=event),
|
|
ActionTargetType.ISSUE_OWNERS,
|
|
fallthrough_choice=FallthroughChoiceType.ACTIVE_MEMBERS,
|
|
).send()
|
|
|
|
assert mock_func.call_count == 1
|
|
|
|
args, kwargs = mock_func.call_args
|
|
notification = args[1]
|
|
|
|
user_ids = []
|
|
for user in list(notification.get_participants().values())[0]:
|
|
user_ids.append(user.id)
|
|
assert "[email protected]" in get_email_addresses(user_ids, self.project).values()
|
|
|
|
with assume_test_silo_mode(SiloMode.CONTROL):
|
|
assert not len(UserOption.objects.filter(key="mail:email", value="[email protected]"))
|
|
|
|
@mock_notify
|
|
def test_multiline_error(self, mock_func):
|
|
event_manager = EventManager({"message": "hello world\nfoo bar", "level": "error"})
|
|
event_manager.normalize()
|
|
event_data = event_manager.get_data()
|
|
event_type = get_event_type(event_data)
|
|
event_data["type"] = event_type.key
|
|
event_data["metadata"] = event_type.get_metadata(event_data)
|
|
|
|
# disable slack
|
|
with assume_test_silo_mode(SiloMode.CONTROL):
|
|
NotificationSettingProvider.objects.create(
|
|
user_id=self.user.id,
|
|
scope_type="user",
|
|
scope_identifier=self.user.id,
|
|
provider="slack",
|
|
type="alerts",
|
|
value="never",
|
|
)
|
|
ProjectOwnership.objects.create(project_id=self.project.id, fallthrough=True)
|
|
|
|
event = event_manager.save(self.project.id)
|
|
with self.tasks():
|
|
AlertRuleNotification(
|
|
Notification(event=event),
|
|
ActionTargetType.ISSUE_OWNERS,
|
|
fallthrough_choice=FallthroughChoiceType.ACTIVE_MEMBERS,
|
|
).send()
|
|
|
|
assert mock_func.call_count == 1
|
|
args, kwargs = mock_func.call_args
|
|
notification = args[1]
|
|
assert notification.get_subject() == "BAR-1 - hello world"
|
|
|
|
def test_notify_users_with_utf8_subject(self):
|
|
event = self.store_event(
|
|
data={"message": "רונית מגן", "level": "error"}, project_id=self.project.id
|
|
)
|
|
|
|
notification = Notification(event=event)
|
|
ProjectOwnership.objects.create(project_id=self.project.id, fallthrough=True)
|
|
|
|
with self.options({"system.url-prefix": "http://example.com"}), self.tasks():
|
|
self.adapter.notify(
|
|
notification,
|
|
ActionTargetType.ISSUE_OWNERS,
|
|
fallthrough_choice=FallthroughChoiceType.ACTIVE_MEMBERS,
|
|
)
|
|
|
|
assert len(mail.outbox) == 1
|
|
msg = mail.outbox[0]
|
|
assert msg.subject == "[Sentry] BAR-1 - רונית מגן"
|
|
|
|
def test_notify_users_with_their_timezones(self):
|
|
"""
|
|
Test that ensures that datetime in issue alert email is in the user's timezone
|
|
"""
|
|
from django.template.defaultfilters import date
|
|
|
|
timestamp = datetime.now(tz=timezone.utc)
|
|
local_timestamp_s = django_timezone.localtime(timestamp, zoneinfo.ZoneInfo("Europe/Vienna"))
|
|
local_timestamp = date(local_timestamp_s, "N j, Y, g:i:s a e")
|
|
|
|
with assume_test_silo_mode(SiloMode.CONTROL):
|
|
UserOption.objects.create(user=self.user, key="timezone", value="Europe/Vienna")
|
|
|
|
event = self.store_event(
|
|
data={"message": "foobar", "level": "error", "timestamp": iso_format(timestamp)},
|
|
project_id=self.project.id,
|
|
)
|
|
|
|
notification = Notification(event=event)
|
|
ProjectOwnership.objects.create(project_id=self.project.id, fallthrough=True)
|
|
|
|
with self.options({"system.url-prefix": "http://example.com"}), self.tasks():
|
|
self.adapter.notify(
|
|
notification,
|
|
ActionTargetType.ISSUE_OWNERS,
|
|
fallthrough_choice=FallthroughChoiceType.ACTIVE_MEMBERS,
|
|
)
|
|
|
|
assert len(mail.outbox) == 1
|
|
msg = mail.outbox[0]
|
|
assert isinstance(msg, EmailMultiAlternatives)
|
|
assert local_timestamp in str(msg.alternatives)
|
|
|
|
def _test_invalid_timezone(self, s: str) -> None:
|
|
with assume_test_silo_mode(SiloMode.CONTROL):
|
|
UserOption.objects.create(user=self.user, key="timezone", value=s)
|
|
|
|
event = self.store_event(
|
|
data={"message": "foobar", "level": "error"},
|
|
project_id=self.project.id,
|
|
)
|
|
notification = AlertRuleNotification(
|
|
Notification(event=event), ActionTargetType.ISSUE_OWNERS
|
|
)
|
|
recipient_context = notification.get_recipient_context(
|
|
RpcActor.from_orm_user(self.user), {}
|
|
)
|
|
assert recipient_context["timezone"] == timezone.utc
|
|
|
|
def test_context_invalid_timezone_empty_string(self):
|
|
self._test_invalid_timezone("")
|
|
|
|
def test_context_invalid_timezone_garbage_value(self):
|
|
self._test_invalid_timezone("not/a/real/timezone")
|
|
|
|
def test_notify_with_suspect_commits(self):
|
|
repo = Repository.objects.create(
|
|
organization_id=self.organization.id, name=self.organization.id
|
|
)
|
|
ProjectOwnership.objects.create(project_id=self.project.id, fallthrough=True)
|
|
release = self.create_release(project=self.project, version="v12")
|
|
release.set_commits(
|
|
[
|
|
{
|
|
"id": "a" * 40,
|
|
"repository": repo.name,
|
|
"author_email": "[email protected]",
|
|
"author_name": "Bob",
|
|
"message": "i fixed a bug",
|
|
"patch_set": [{"path": "src/sentry/models/release.py", "type": "M"}],
|
|
}
|
|
]
|
|
)
|
|
|
|
event = self.store_event(
|
|
data={
|
|
"message": "Kaboom!",
|
|
"platform": "python",
|
|
"timestamp": iso_format(before_now(seconds=1)),
|
|
"stacktrace": {
|
|
"frames": [
|
|
{
|
|
"function": "handle_set_commits",
|
|
"abs_path": "/usr/src/sentry/src/sentry/tasks.py",
|
|
"module": "sentry.tasks",
|
|
"in_app": True,
|
|
"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",
|
|
},
|
|
]
|
|
},
|
|
"tags": {"sentry:release": release.version},
|
|
},
|
|
project_id=self.project.id,
|
|
)
|
|
assert event.group is not None
|
|
GroupRelease.objects.create(
|
|
group_id=event.group.id, project_id=self.project.id, release_id=self.release.id
|
|
)
|
|
|
|
with self.tasks():
|
|
notification = Notification(event=event)
|
|
|
|
self.adapter.notify(
|
|
notification,
|
|
ActionTargetType.ISSUE_OWNERS,
|
|
fallthrough_choice=FallthroughChoiceType.ACTIVE_MEMBERS,
|
|
)
|
|
|
|
assert len(mail.outbox) >= 1
|
|
|
|
msg = mail.outbox[-1]
|
|
|
|
assert "Suspect Commits" in msg.body
|
|
|
|
def test_notify_with_replay_id(self):
|
|
project = self.project
|
|
organization = project.organization
|
|
event = self.store_event(
|
|
data={
|
|
"contexts": {"replay": {"replay_id": "46eb3948be25448abd53fe36b5891ff2"}},
|
|
"message": "Kaboom!",
|
|
"platform": "python",
|
|
"timestamp": iso_format(before_now(seconds=1)),
|
|
"tags": [("level", "error")],
|
|
"request": {"url": "example.com"},
|
|
},
|
|
project_id=project.id,
|
|
)
|
|
assert event.group is not None
|
|
event.group.substatus = GroupSubStatus.REGRESSED
|
|
event.group.save()
|
|
|
|
features = ["organizations:session-replay", "organizations:session-replay-issue-emails"]
|
|
with self.feature(features):
|
|
with self.tasks():
|
|
notification = Notification(event=event)
|
|
self.adapter.notify(
|
|
notification,
|
|
ActionTargetType.ISSUE_OWNERS,
|
|
fallthrough_choice=FallthroughChoiceType.ACTIVE_MEMBERS,
|
|
)
|
|
|
|
assert len(mail.outbox) >= 1
|
|
|
|
msg = mail.outbox[-1]
|
|
|
|
expected_url = f"/organizations/{organization.slug}/issues/{event.group.id}/replays/?referrer=issue_alert-email"
|
|
|
|
assert isinstance(msg, EmailMultiAlternatives)
|
|
assert isinstance(msg.alternatives[0][0], str)
|
|
assert expected_url in msg.alternatives[0][0]
|
|
|
|
def test_slack_link(self):
|
|
project = self.project
|
|
organization = project.organization
|
|
event = self.store_event(data=make_event_data("foo.jx"), project_id=project.id)
|
|
ProjectOwnership.objects.create(project_id=self.project.id, fallthrough=True)
|
|
|
|
with self.tasks():
|
|
notification = Notification(event=event)
|
|
self.adapter.notify(
|
|
notification,
|
|
ActionTargetType.ISSUE_OWNERS,
|
|
fallthrough_choice=FallthroughChoiceType.ACTIVE_MEMBERS,
|
|
)
|
|
|
|
assert len(mail.outbox) >= 1
|
|
|
|
msg = mail.outbox[-1]
|
|
assert isinstance(msg, EmailMultiAlternatives)
|
|
assert isinstance(msg.alternatives[0][0], str)
|
|
assert (
|
|
f"/settings/{organization.slug}/integrations/slack/?referrer=alert_email"
|
|
in msg.alternatives[0][0]
|
|
)
|
|
assert "notification_uuid" in msg.body
|
|
|
|
def test_slack_link_with_integration(self):
|
|
project = self.project
|
|
organization = project.organization
|
|
event = self.store_event(data=make_event_data("foo.jx"), project_id=project.id)
|
|
ProjectOwnership.objects.create(project_id=self.project.id, fallthrough=True)
|
|
|
|
with assume_test_silo_mode(SiloMode.CONTROL):
|
|
integration = self.create_provider_integration(provider="msteams")
|
|
integration.add_organization(organization)
|
|
|
|
with self.tasks():
|
|
notification = Notification(event=event)
|
|
self.adapter.notify(
|
|
notification,
|
|
ActionTargetType.ISSUE_OWNERS,
|
|
fallthrough_choice=FallthroughChoiceType.ACTIVE_MEMBERS,
|
|
)
|
|
|
|
assert len(mail.outbox) >= 1
|
|
|
|
msg = mail.outbox[-1]
|
|
assert isinstance(msg, EmailMultiAlternatives)
|
|
assert isinstance(msg.alternatives[0][0], str)
|
|
assert (
|
|
f"/settings/{organization.slug}/integrations/slack/?referrer=alert_email"
|
|
not in msg.alternatives[0][0]
|
|
)
|
|
assert "notification_uuid" in msg.body
|
|
|
|
def test_slack_link_with_plugin(self):
|
|
project = self.project
|
|
organization = project.organization
|
|
event = self.store_event(data=make_event_data("foo.jx"), project_id=project.id)
|
|
ProjectOwnership.objects.create(project_id=self.project.id, fallthrough=True)
|
|
|
|
OpsGeniePlugin().enable(project)
|
|
|
|
with self.tasks():
|
|
notification = Notification(event=event)
|
|
self.adapter.notify(
|
|
notification,
|
|
ActionTargetType.ISSUE_OWNERS,
|
|
fallthrough_choice=FallthroughChoiceType.ACTIVE_MEMBERS,
|
|
)
|
|
|
|
assert len(mail.outbox) >= 1
|
|
|
|
msg = mail.outbox[-1]
|
|
assert isinstance(msg, EmailMultiAlternatives)
|
|
assert isinstance(msg.alternatives[0][0], str)
|
|
assert (
|
|
f"/settings/{organization.slug}/integrations/slack/?referrer=alert_email"
|
|
not in msg.alternatives[0][0]
|
|
)
|
|
|
|
def test_notify_team_members(self):
|
|
"""Test that each member of a team is notified"""
|
|
|
|
user = self.create_user(email="[email protected]", is_active=True)
|
|
user2 = self.create_user(email="[email protected]", is_active=True)
|
|
team = self.create_team(organization=self.organization, members=[user, user2])
|
|
project = self.create_project(teams=[team])
|
|
event = self.store_event(data=make_event_data("foo.py"), project_id=project.id)
|
|
self.assert_notify(event, [user.email, user2.email], ActionTargetType.TEAM, str(team.id))
|
|
|
|
def test_notify_user(self):
|
|
user = self.create_user(email="[email protected]", is_active=True)
|
|
self.create_team(organization=self.organization, members=[user])
|
|
event = self.store_event(data=make_event_data("foo.py"), project_id=self.project.id)
|
|
self.assert_notify(event, [user.email], ActionTargetType.MEMBER, str(user.id))
|
|
|
|
|
|
@region_silo_test
|
|
class MailAdapterNotifyIssueOwnersTest(BaseMailAdapterTest):
|
|
def create_assert_delete_projectownership(
|
|
self,
|
|
proj: Project,
|
|
rules: Sequence[grammar.Rule],
|
|
data: Mapping,
|
|
asserted_emails_fired: Sequence[str],
|
|
):
|
|
po = ProjectOwnership.objects.create(
|
|
project_id=proj.id, schema=dump_schema(rules), fallthrough=False
|
|
)
|
|
self.assert_notify(
|
|
self.store_event(data=data, project_id=proj.id),
|
|
asserted_emails_fired,
|
|
)
|
|
po.delete()
|
|
|
|
def test_notify_with_path(self):
|
|
user = self.create_user(email="[email protected]", is_active=True)
|
|
user2 = self.create_user(email="[email protected]", is_active=True)
|
|
|
|
organization = self.create_organization(owner=user)
|
|
team = self.create_team(organization=organization)
|
|
project = self.create_project(name="Test", teams=[team])
|
|
OrganizationMemberTeam.objects.create(
|
|
organizationmember=OrganizationMember.objects.get(
|
|
user_id=user.id, organization=organization
|
|
),
|
|
team=team,
|
|
)
|
|
self.create_member(user=user2, organization=organization, teams=[team])
|
|
self.group = self.create_group(
|
|
first_seen=django_timezone.now(),
|
|
last_seen=django_timezone.now(),
|
|
project=project,
|
|
message="hello world",
|
|
logger="root",
|
|
)
|
|
ProjectOwnership.objects.create(
|
|
project_id=project.id,
|
|
schema=dump_schema(
|
|
[
|
|
grammar.Rule(Matcher("path", "*.py"), [Owner("team", team.slug)]),
|
|
grammar.Rule(Matcher("path", "*.jx"), [Owner("user", user2.email)]),
|
|
grammar.Rule(
|
|
Matcher("path", "*.cbl"),
|
|
[Owner("user", user.email), Owner("user", user2.email)],
|
|
),
|
|
]
|
|
),
|
|
fallthrough=True,
|
|
)
|
|
|
|
with self.feature("organizations:notification-all-recipients"):
|
|
event_all_users = self.store_event(
|
|
data=make_event_data("foo.cbl"), project_id=project.id
|
|
)
|
|
self.assert_notify(event_all_users, [user.email, user2.email])
|
|
|
|
event_team = self.store_event(data=make_event_data("foo.py"), project_id=project.id)
|
|
self.assert_notify(event_team, [user.email, user2.email])
|
|
|
|
event_single_user = self.store_event(data=make_event_data("foo.jx"), project_id=project.id)
|
|
self.assert_notify(event_single_user, [user2.email])
|
|
|
|
with assume_test_silo_mode(SiloMode.CONTROL):
|
|
# Make sure that disabling mail alerts works as expected
|
|
NotificationSettingOption.objects.create(
|
|
user_id=user2.id,
|
|
scope_type="project",
|
|
scope_identifier=project.id,
|
|
type="alerts",
|
|
value="never",
|
|
)
|
|
|
|
with self.feature("organizations:notification-all-recipients"):
|
|
event_all_users = self.store_event(
|
|
data=make_event_data("foo.cbl"), project_id=project.id
|
|
)
|
|
self.assert_notify(event_all_users, [user.email])
|
|
|
|
def test_notify_with_release_tag(self):
|
|
owner = self.create_user(email="[email protected]", is_active=True)
|
|
organization = self.create_organization(owner=owner)
|
|
team = self.create_team(organization=organization, name="awesome")
|
|
team2 = self.create_team(organization=organization, name="sauce")
|
|
project = self.create_project(name="Test", teams=[team, team2])
|
|
|
|
user = self.create_user(email="[email protected]", is_active=True)
|
|
user2 = self.create_user(email="[email protected]", is_active=True)
|
|
|
|
user3 = self.create_user(email="[email protected]", is_active=True)
|
|
user4 = self.create_user(email="[email protected]", is_active=True)
|
|
user5 = self.create_user(email="[email protected]", is_active=True)
|
|
|
|
[self.create_member(user=u, organization=organization, teams=[team]) for u in [user, user2]]
|
|
[
|
|
self.create_member(user=u, organization=organization, teams=[team2])
|
|
for u in [user3, user4, user5]
|
|
]
|
|
with assume_test_silo_mode(SiloMode.CONTROL):
|
|
for u in [user, user2, user3, user4, user5]:
|
|
# disable slack
|
|
NotificationSettingProvider.objects.create(
|
|
user_id=u.id,
|
|
scope_type="user",
|
|
scope_identifier=u.id,
|
|
provider="slack",
|
|
type="alerts",
|
|
value="never",
|
|
)
|
|
with self.feature("organizations:notification-all-recipients"):
|
|
self.create_assert_delete_projectownership(
|
|
project,
|
|
[
|
|
grammar.Rule(
|
|
Matcher("tags.release", "*"),
|
|
[Owner("user", user.email)],
|
|
),
|
|
grammar.Rule(
|
|
Matcher("tags.release", "1"),
|
|
[Owner("user", user2.email)],
|
|
),
|
|
],
|
|
{"release": "1"},
|
|
[user.email, user2.email],
|
|
)
|
|
|
|
self.create_assert_delete_projectownership(
|
|
project,
|
|
[
|
|
grammar.Rule(
|
|
Matcher("tags.release", "*"),
|
|
[Owner("user", user.email)],
|
|
),
|
|
grammar.Rule(
|
|
Matcher("tags.release", "2"),
|
|
[Owner("team", team2.slug)],
|
|
),
|
|
],
|
|
{"release": "2"},
|
|
[user.email, user3.email, user4.email, user5.email],
|
|
)
|
|
|
|
def test_notify_with_dist_tag(self):
|
|
owner = self.create_user(email="[email protected]", is_active=True)
|
|
organization = self.create_organization(owner=owner)
|
|
team = self.create_team(organization=organization, name="awesome")
|
|
team2 = self.create_team(organization=organization, name="sauce")
|
|
project = self.create_project(name="Test", teams=[team, team2])
|
|
|
|
user = self.create_user(email="[email protected]", is_active=True)
|
|
user2 = self.create_user(email="[email protected]", is_active=True)
|
|
|
|
user3 = self.create_user(email="[email protected]", is_active=True)
|
|
user4 = self.create_user(email="[email protected]", is_active=True)
|
|
user5 = self.create_user(email="[email protected]", is_active=True)
|
|
|
|
[self.create_member(user=u, organization=organization, teams=[team]) for u in [user, user2]]
|
|
[
|
|
self.create_member(user=u, organization=organization, teams=[team2])
|
|
for u in [user3, user4, user5]
|
|
]
|
|
|
|
with assume_test_silo_mode(SiloMode.CONTROL):
|
|
for u in [user, user2, user3, user4, user5]:
|
|
NotificationSettingProvider.objects.create(
|
|
user_id=u.id,
|
|
scope_type="user",
|
|
scope_identifier=u.id,
|
|
provider="slack",
|
|
type="alerts",
|
|
value="never",
|
|
)
|
|
|
|
with self.feature("organizations:notification-all-recipients"):
|
|
self.create_assert_delete_projectownership(
|
|
project,
|
|
[
|
|
grammar.Rule(
|
|
Matcher("tags.dist", "*"),
|
|
[Owner("user", user.email)],
|
|
),
|
|
grammar.Rule(
|
|
Matcher("tags.dist", "rc1"),
|
|
[Owner("user", user2.email)],
|
|
),
|
|
],
|
|
{"dist": "rc1", "release": "1"},
|
|
[user.email, user2.email],
|
|
)
|
|
|
|
self.create_assert_delete_projectownership(
|
|
project,
|
|
[
|
|
grammar.Rule(
|
|
Matcher("tags.dist", "*"),
|
|
[Owner("user", user.email)],
|
|
),
|
|
grammar.Rule(
|
|
Matcher("tags.dist", "lenny"),
|
|
[Owner("team", team2.slug)],
|
|
),
|
|
],
|
|
{"dist": "lenny", "release": "1"},
|
|
[user.email, user3.email, user4.email, user5.email],
|
|
)
|
|
|
|
def test_dont_notify_with_dist_if_no_rule(self):
|
|
owner = self.create_user(email="[email protected]", is_active=True)
|
|
organization = self.create_organization(owner=owner)
|
|
team = self.create_team(organization=organization, name="awesome")
|
|
project = self.create_project(name="Test", teams=[team])
|
|
user = self.create_user(email="[email protected]", is_active=True)
|
|
self.create_member(user=user, organization=organization, teams=[team])
|
|
|
|
with self.feature("organizations:notification-all-recipients"):
|
|
self.create_assert_delete_projectownership(
|
|
project,
|
|
[
|
|
grammar.Rule(
|
|
Matcher("tags.abc", "hello"),
|
|
[Owner("user", user.email)],
|
|
),
|
|
],
|
|
{"dist": "hello", "release": "1"},
|
|
[],
|
|
)
|
|
|
|
def test_notify_with_user_tag(self):
|
|
owner = self.create_user(email="[email protected]", is_active=True)
|
|
organization = self.create_organization(owner=owner)
|
|
team = self.create_team(organization=organization, name="sentry")
|
|
project = self.create_project(name="Test", teams=[team])
|
|
|
|
user_by_id = self.create_user(email="[email protected]", is_active=True)
|
|
user_by_username = self.create_user(email="[email protected]", is_active=True)
|
|
user_by_email = self.create_user(email="[email protected]", is_active=True)
|
|
user_by_ip = self.create_user(email="[email protected]", is_active=True)
|
|
user_by_sub = self.create_user(email="[email protected]", is_active=True)
|
|
user_by_extra = self.create_user(email="[email protected]", is_active=True)
|
|
[
|
|
self.create_member(user=u, organization=organization, teams=[team])
|
|
for u in [
|
|
user_by_id,
|
|
user_by_username,
|
|
user_by_email,
|
|
user_by_ip,
|
|
user_by_sub,
|
|
user_by_extra,
|
|
]
|
|
]
|
|
|
|
with self.feature("organizations:notification-all-recipients"):
|
|
self.create_assert_delete_projectownership(
|
|
project,
|
|
[
|
|
grammar.Rule(
|
|
Matcher("tags.user.id", "unique_id"),
|
|
[Owner("user", user_by_id.email)],
|
|
),
|
|
grammar.Rule(
|
|
Matcher("tags.user.username", "my_user"),
|
|
[Owner("user", user_by_username.email)],
|
|
),
|
|
grammar.Rule(
|
|
Matcher("tags.user.email", "[email protected]"),
|
|
[Owner("user", user_by_email.email)],
|
|
),
|
|
grammar.Rule(
|
|
Matcher("tags.user.ip_address", "127.0.0.1"),
|
|
[Owner("user", user_by_ip.email)],
|
|
),
|
|
grammar.Rule(
|
|
Matcher("tags.user.subscription", "basic"),
|
|
[Owner("user", user_by_sub.email)],
|
|
),
|
|
grammar.Rule(
|
|
Matcher("tags.user.extra", "detail"),
|
|
[Owner("user", user_by_extra.email)],
|
|
),
|
|
],
|
|
{
|
|
"user": {
|
|
"id": "unique_id",
|
|
"username": "my_user",
|
|
"email": "[email protected]",
|
|
"ip_address": "127.0.0.1",
|
|
"subscription": "basic",
|
|
"extra": "detail",
|
|
}
|
|
},
|
|
[
|
|
user_by_id.email,
|
|
user_by_username.email,
|
|
user_by_email.email,
|
|
user_by_ip.email,
|
|
user_by_sub.email,
|
|
user_by_extra.email,
|
|
],
|
|
)
|
|
|
|
def test_notify_with_user_tag_edge_cases(self):
|
|
owner = self.create_user(email="[email protected]", is_active=True)
|
|
organization = self.create_organization(owner=owner)
|
|
team = self.create_team(organization=organization, name="sentry")
|
|
project = self.create_project(name="Test", teams=[team])
|
|
|
|
user = self.create_user(email="[email protected]", is_active=True)
|
|
user_star = self.create_user(email="[email protected]", is_active=True)
|
|
user_username = self.create_user(email="[email protected]", is_active=True)
|
|
user_username_star = self.create_user(
|
|
email="[email protected]", is_active=True
|
|
)
|
|
users = [user, user_star, user_username, user_username_star]
|
|
for u in users:
|
|
self.create_member(user=u, organization=organization, teams=[team])
|
|
with assume_test_silo_mode(SiloMode.CONTROL):
|
|
for u in users:
|
|
NotificationSettingProvider.objects.create(
|
|
user_id=self.user.id,
|
|
scope_type="user",
|
|
scope_identifier=self.user.id,
|
|
provider="slack",
|
|
type="alerts",
|
|
value="never",
|
|
)
|
|
|
|
"""
|
|
tags.user.username:[email protected] [email protected]
|
|
tags.user:[email protected] [email protected]
|
|
|
|
tags.user:* [email protected]
|
|
|
|
tags.user.username:* [email protected]
|
|
tags.user:username [email protected]
|
|
|
|
tags.user:*someemail* #sentry
|
|
"""
|
|
with self.feature("organizations:notification-all-recipients"):
|
|
dat = {"user": {"username": "[email protected]"}}
|
|
self.create_assert_delete_projectownership(
|
|
project,
|
|
[
|
|
grammar.Rule(
|
|
Matcher("tags.user.username", "[email protected]"),
|
|
[Owner("user", user_username.email)],
|
|
)
|
|
],
|
|
dat,
|
|
[user_username.email],
|
|
)
|
|
|
|
self.create_assert_delete_projectownership(
|
|
project,
|
|
[
|
|
grammar.Rule(
|
|
Matcher("tags.user", "[email protected]"), [Owner("user", user.email)]
|
|
)
|
|
],
|
|
dat,
|
|
[],
|
|
)
|
|
|
|
self.create_assert_delete_projectownership(
|
|
project,
|
|
[grammar.Rule(Matcher("tags.user", "*"), [Owner("user", user_star.email)])],
|
|
dat,
|
|
[user_star.email],
|
|
)
|
|
|
|
self.create_assert_delete_projectownership(
|
|
project,
|
|
[
|
|
grammar.Rule(
|
|
Matcher("tags.user.username", "*"),
|
|
[Owner("user", user_username_star.email)],
|
|
)
|
|
],
|
|
dat,
|
|
[user_username_star.email],
|
|
)
|
|
|
|
self.create_assert_delete_projectownership(
|
|
project,
|
|
[grammar.Rule(Matcher("tags.user", "username"), [Owner("user", user.email)])],
|
|
dat,
|
|
[],
|
|
)
|
|
|
|
self.create_assert_delete_projectownership(
|
|
project,
|
|
[grammar.Rule(Matcher("tags.user", "*someemail*"), [Owner("team", team.slug)])],
|
|
dat,
|
|
[u.email for u in [user, user_star, user_username, user_username_star]],
|
|
)
|
|
|
|
self.create_assert_delete_projectownership(
|
|
project,
|
|
[
|
|
grammar.Rule(
|
|
Matcher("tags.user.email", "someemail*"), [Owner("team", team.slug)]
|
|
)
|
|
],
|
|
{"user": {"username": "[email protected]"}},
|
|
[],
|
|
)
|
|
|
|
def test_group_substatus_header(self):
|
|
event = self.store_event(
|
|
data={"message": "Hello world", "level": "error"}, project_id=self.project.id
|
|
)
|
|
# Header is based on the group substatus
|
|
assert event.group is not None
|
|
event.group.substatus = GroupSubStatus.REGRESSED
|
|
event.group.save()
|
|
|
|
rule = Rule.objects.create(project=self.project, label="my rule")
|
|
ProjectOwnership.objects.create(project_id=self.project.id, fallthrough=True)
|
|
|
|
notification = Notification(event=event, rule=rule)
|
|
|
|
with self.options({"system.url-prefix": "http://example.com"}), self.tasks():
|
|
self.adapter.notify(
|
|
notification,
|
|
ActionTargetType.ISSUE_OWNERS,
|
|
fallthrough_choice=FallthroughChoiceType.ACTIVE_MEMBERS,
|
|
)
|
|
|
|
msg = mail.outbox[0]
|
|
assert isinstance(msg, EmailMultiAlternatives)
|
|
assert msg.subject == "[Sentry] BAR-1 - Hello world"
|
|
assert isinstance(msg.alternatives[0][0], str)
|
|
assert "Regressed issue" in msg.alternatives[0][0]
|
|
|
|
|
|
@region_silo_test
|
|
class MailAdapterGetDigestSubjectTest(BaseMailAdapterTest):
|
|
def test_get_digest_subject(self):
|
|
assert (
|
|
get_digest_subject(
|
|
mock.Mock(qualified_short_id="BAR-1"),
|
|
Counter({mock.sentinel.group: 3}),
|
|
datetime(2016, 9, 19, 1, 2, 3, tzinfo=timezone.utc),
|
|
)
|
|
== "BAR-1 - 1 new alert since Sept. 19, 2016, 1:02 a.m. UTC"
|
|
)
|
|
|
|
|
|
@region_silo_test
|
|
class MailAdapterNotifyDigestTest(BaseMailAdapterTest, ReplaysSnubaTestCase):
|
|
@mock.patch.object(mail_adapter, "notify", side_effect=mail_adapter.notify, autospec=True)
|
|
def test_notify_digest(self, notify):
|
|
project = self.project
|
|
timestamp = iso_format(before_now(minutes=1))
|
|
event = self.store_event(
|
|
data={"timestamp": timestamp, "fingerprint": ["group-1"]},
|
|
project_id=project.id,
|
|
)
|
|
event2 = self.store_event(
|
|
data={"timestamp": timestamp, "fingerprint": ["group-2"]},
|
|
project_id=project.id,
|
|
)
|
|
|
|
rule = project.rule_set.all()[0]
|
|
ProjectOwnership.objects.create(project_id=self.project.id, fallthrough=True)
|
|
digest = build_digest(
|
|
project, (event_to_record(event, (rule,)), event_to_record(event2, (rule,)))
|
|
)[0]
|
|
|
|
with self.tasks():
|
|
self.adapter.notify_digest(
|
|
project,
|
|
digest,
|
|
ActionTargetType.ISSUE_OWNERS,
|
|
fallthrough_choice=FallthroughChoiceType.ACTIVE_MEMBERS,
|
|
)
|
|
|
|
assert notify.call_count == 0
|
|
assert len(mail.outbox) == 1
|
|
|
|
message = mail.outbox[0]
|
|
assert "List-ID" in message.message()
|
|
assert isinstance(message, EmailMultiAlternatives)
|
|
assert isinstance(message.alternatives[0][0], str)
|
|
assert "notification_uuid" in message.alternatives[0][0]
|
|
|
|
@mock.patch.object(mail_adapter, "notify", side_effect=mail_adapter.notify, autospec=True)
|
|
def test_notify_digest_replay_id(self, notify):
|
|
project = self.project
|
|
self.project.flags.has_replays = True
|
|
self.project.save()
|
|
|
|
timestamp = iso_format(before_now(minutes=1))
|
|
replay1_id = "46eb3948be25448abd53fe36b5891ff2"
|
|
event = self.store_event(
|
|
data={
|
|
"timestamp": timestamp,
|
|
"fingerprint": ["group-1"],
|
|
"contexts": {"replay": {"replay_id": replay1_id}},
|
|
},
|
|
project_id=project.id,
|
|
)
|
|
event2 = self.store_event(
|
|
data={
|
|
"timestamp": timestamp,
|
|
"fingerprint": ["group-2"],
|
|
"contexts": {"replay": {"replay_id": replay1_id}},
|
|
},
|
|
project_id=project.id,
|
|
)
|
|
|
|
self.store_replays(
|
|
mock_replay(
|
|
datetime.now() - timedelta(seconds=22),
|
|
self.project.id,
|
|
replay1_id,
|
|
)
|
|
)
|
|
|
|
rule = project.rule_set.all()[0]
|
|
ProjectOwnership.objects.create(project_id=self.project.id, fallthrough=True)
|
|
digest = build_digest(
|
|
project, (event_to_record(event, (rule,)), event_to_record(event2, (rule,)))
|
|
)[0]
|
|
|
|
features = ["organizations:session-replay", "organizations:session-replay-issue-emails"]
|
|
with self.feature(features), self.tasks():
|
|
self.adapter.notify_digest(
|
|
project,
|
|
digest,
|
|
ActionTargetType.ISSUE_OWNERS,
|
|
fallthrough_choice=FallthroughChoiceType.ACTIVE_MEMBERS,
|
|
)
|
|
|
|
assert notify.call_count == 0
|
|
assert len(mail.outbox) == 1
|
|
|
|
message = mail.outbox[0]
|
|
assert "View Replays" in message.message().as_string()
|
|
assert isinstance(message, EmailMultiAlternatives)
|
|
assert isinstance(message.alternatives[0][0], str)
|
|
assert "notification_uuid" in message.alternatives[0][0]
|
|
|
|
@mock.patch.object(mail_adapter, "notify", side_effect=mail_adapter.notify, autospec=True)
|
|
def test_dont_notify_digest_snoozed(self, notify):
|
|
"""Test that a digest for an alert snoozed by user is not sent."""
|
|
project = self.project
|
|
timestamp = iso_format(before_now(minutes=1))
|
|
event = self.store_event(
|
|
data={"timestamp": timestamp, "fingerprint": ["group-1"]},
|
|
project_id=project.id,
|
|
)
|
|
event2 = self.store_event(
|
|
data={"timestamp": timestamp, "fingerprint": ["group-2"]},
|
|
project_id=project.id,
|
|
)
|
|
|
|
rule = project.rule_set.all()[0]
|
|
self.snooze_rule(user_id=self.user.id, owner_id=self.user.id, rule=rule)
|
|
ProjectOwnership.objects.create(project_id=self.project.id, fallthrough=True)
|
|
digest = build_digest(
|
|
project, (event_to_record(event, (rule,)), event_to_record(event2, (rule,)))
|
|
)[0]
|
|
|
|
with self.tasks():
|
|
self.adapter.notify_digest(
|
|
project,
|
|
digest,
|
|
ActionTargetType.ISSUE_OWNERS,
|
|
fallthrough_choice=FallthroughChoiceType.ACTIVE_MEMBERS,
|
|
)
|
|
|
|
assert notify.call_count == 0
|
|
assert len(mail.outbox) == 0
|
|
|
|
@mock.patch.object(mail_adapter, "notify", side_effect=mail_adapter.notify, autospec=True)
|
|
def test_notify_digest_snooze_one_rule(self, notify):
|
|
"""Test that a digest is sent containing only notifications about an unsnoozed alert."""
|
|
user2 = self.create_user(email="[email protected]", is_active=True)
|
|
self.create_member(user=user2, organization=self.organization, teams=[self.team])
|
|
project = self.project
|
|
timestamp = iso_format(before_now(minutes=1))
|
|
event = self.store_event(
|
|
data={"timestamp": timestamp, "fingerprint": ["group-1"]},
|
|
project_id=project.id,
|
|
)
|
|
event2 = self.store_event(
|
|
data={"timestamp": timestamp, "fingerprint": ["group-2"]},
|
|
project_id=project.id,
|
|
)
|
|
|
|
rule = project.rule_set.all()[0]
|
|
rule2 = Rule.objects.create(project=project, label="my rule")
|
|
# mute the first rule only for self.user, not user2
|
|
self.snooze_rule(user_id=self.user.id, owner_id=self.user.id, rule=rule)
|
|
|
|
ProjectOwnership.objects.create(project_id=project.id, fallthrough=True)
|
|
digest = build_digest(
|
|
project, (event_to_record(event, (rule,)), event_to_record(event2, (rule2,)))
|
|
)[0]
|
|
|
|
with self.tasks():
|
|
self.adapter.notify_digest(
|
|
project,
|
|
digest,
|
|
ActionTargetType.ISSUE_OWNERS,
|
|
fallthrough_choice=FallthroughChoiceType.ACTIVE_MEMBERS,
|
|
)
|
|
|
|
assert notify.call_count == 0
|
|
assert len(mail.outbox) == 2 # we send it to 2 users
|
|
messages = sorted(mail.outbox, key=lambda message: message.to[0])
|
|
|
|
message1 = messages[0]
|
|
message2 = messages[1]
|
|
|
|
# self.user only receives a digest about one alert, since a rule was muted
|
|
assert message1.to[0] == self.user.email
|
|
assert "1 new alert since" in message1.subject
|
|
|
|
# user2 receives a digest about both alerts, since no rules were muted
|
|
assert message2.to[0] == user2.email
|
|
assert "2 new alerts since" in message2.subject
|
|
|
|
@mock.patch.object(mail_adapter, "notify", side_effect=mail_adapter.notify, autospec=True)
|
|
def test_dont_notify_digest_snoozed_multiple_rules(self, notify):
|
|
"""Test that a digest is only sent to the user who hasn't snoozed the rules."""
|
|
user2 = self.create_user(email="[email protected]", is_active=True)
|
|
self.create_member(user=user2, organization=self.organization, teams=[self.team])
|
|
project = self.project
|
|
timestamp = iso_format(before_now(minutes=1))
|
|
event = self.store_event(
|
|
data={"timestamp": timestamp, "fingerprint": ["group-1"]},
|
|
project_id=project.id,
|
|
)
|
|
event2 = self.store_event(
|
|
data={"timestamp": timestamp, "fingerprint": ["group-2"]},
|
|
project_id=project.id,
|
|
)
|
|
|
|
rule = project.rule_set.all()[0]
|
|
rule2 = Rule.objects.create(project=project, label="my rule")
|
|
# mute the rules for self.user, not user2
|
|
self.snooze_rule(user_id=self.user.id, owner_id=self.user.id, rule=rule)
|
|
self.snooze_rule(user_id=self.user.id, owner_id=self.user.id, rule=rule2)
|
|
|
|
ProjectOwnership.objects.create(project_id=project.id, fallthrough=True)
|
|
digest = build_digest(
|
|
project, (event_to_record(event, (rule,)), event_to_record(event2, (rule2,)))
|
|
)[0]
|
|
|
|
with self.tasks():
|
|
self.adapter.notify_digest(
|
|
project,
|
|
digest,
|
|
ActionTargetType.ISSUE_OWNERS,
|
|
fallthrough_choice=FallthroughChoiceType.ACTIVE_MEMBERS,
|
|
)
|
|
|
|
assert notify.call_count == 0
|
|
assert len(mail.outbox) == 1 # we send it to only 1 user
|
|
message = mail.outbox[0]
|
|
|
|
# user2 receives a digest about both alerts, since no rules were muted
|
|
assert message.to[0] == user2.email
|
|
assert "2 new alerts since" in message.subject
|
|
|
|
@mock.patch.object(mail_adapter, "notify", side_effect=mail_adapter.notify, autospec=True)
|
|
def test_dont_notify_digest_snoozed_multiple_rules_global_snooze(self, notify):
|
|
"""Test that a digest with only one rule is only sent to the user who didn't snooze one rule."""
|
|
user2 = self.create_user(email="[email protected]", is_active=True)
|
|
self.create_member(user=user2, organization=self.organization, teams=[self.team])
|
|
project = self.project
|
|
timestamp = iso_format(before_now(minutes=1))
|
|
event = self.store_event(
|
|
data={"timestamp": timestamp, "fingerprint": ["group-1"]},
|
|
project_id=project.id,
|
|
)
|
|
event2 = self.store_event(
|
|
data={"timestamp": timestamp, "fingerprint": ["group-2"]},
|
|
project_id=project.id,
|
|
)
|
|
|
|
rule = project.rule_set.all()[0]
|
|
rule2 = Rule.objects.create(project=project, label="my rule")
|
|
# mute the first rule for self.user, not user2
|
|
self.snooze_rule(user_id=self.user.id, owner_id=self.user.id, rule=rule)
|
|
# mute the 2nd rule for both
|
|
self.snooze_rule(owner_id=self.user.id, rule=rule2)
|
|
|
|
ProjectOwnership.objects.create(project_id=project.id, fallthrough=True)
|
|
digest = build_digest(
|
|
project, (event_to_record(event, (rule,)), event_to_record(event2, (rule2,)))
|
|
)[0]
|
|
|
|
with self.tasks():
|
|
self.adapter.notify_digest(
|
|
project,
|
|
digest,
|
|
ActionTargetType.ISSUE_OWNERS,
|
|
fallthrough_choice=FallthroughChoiceType.ACTIVE_MEMBERS,
|
|
)
|
|
|
|
assert notify.call_count == 0
|
|
assert len(mail.outbox) == 1 # we send it to only 1 user
|
|
message = mail.outbox[0]
|
|
|
|
# user2 receives a digest about only one alert
|
|
assert message.to[0] == user2.email
|
|
assert "1 new alert since" in message.subject
|
|
|
|
@mock.patch.object(mail_adapter, "notify", side_effect=mail_adapter.notify, autospec=True)
|
|
@mock.patch.object(MessageBuilder, "send_async", autospec=True)
|
|
def test_notify_digest_single_record(self, send_async, notify):
|
|
event = self.store_event(data={}, project_id=self.project.id)
|
|
rule = self.project.rule_set.all()[0]
|
|
ProjectOwnership.objects.create(project_id=self.project.id, fallthrough=True)
|
|
digest = build_digest(self.project, (event_to_record(event, (rule,)),))[0]
|
|
self.adapter.notify_digest(
|
|
self.project,
|
|
digest,
|
|
ActionTargetType.ISSUE_OWNERS,
|
|
fallthrough_choice=FallthroughChoiceType.ACTIVE_MEMBERS,
|
|
)
|
|
assert send_async.call_count == 1
|
|
assert notify.call_count == 1
|
|
|
|
def test_notify_digest_subject_prefix(self):
|
|
ProjectOption.objects.set_value(
|
|
project=self.project, key="mail:subject_prefix", value="[Example prefix] "
|
|
)
|
|
ProjectOwnership.objects.create(project_id=self.project.id, fallthrough=True)
|
|
timestamp = iso_format(before_now(minutes=1))
|
|
event = self.store_event(
|
|
data={"timestamp": timestamp, "fingerprint": ["group-1"]},
|
|
project_id=self.project.id,
|
|
)
|
|
event2 = self.store_event(
|
|
data={"timestamp": timestamp, "fingerprint": ["group-2"]},
|
|
project_id=self.project.id,
|
|
)
|
|
|
|
rule = self.project.rule_set.all()[0]
|
|
|
|
digest = build_digest(
|
|
self.project, (event_to_record(event, (rule,)), event_to_record(event2, (rule,)))
|
|
)[0]
|
|
|
|
with self.tasks():
|
|
self.adapter.notify_digest(
|
|
self.project,
|
|
digest,
|
|
ActionTargetType.ISSUE_OWNERS,
|
|
fallthrough_choice=FallthroughChoiceType.ACTIVE_MEMBERS,
|
|
)
|
|
|
|
assert len(mail.outbox) == 1
|
|
|
|
msg = mail.outbox[0]
|
|
|
|
assert msg.subject.startswith("[Example prefix]")
|
|
|
|
@mock.patch.object(mail_adapter, "notify", side_effect=mail_adapter.notify, autospec=True)
|
|
def test_notify_digest_user_does_not_exist(self, notify):
|
|
"""Test that in the event a rule has been created with an action to send to a user who
|
|
no longer exists, we don't blow up when getting users in get_send_to
|
|
"""
|
|
project = self.project
|
|
timestamp = iso_format(before_now(minutes=1))
|
|
event = self.store_event(
|
|
data={"timestamp": timestamp, "fingerprint": ["group-1"]},
|
|
project_id=project.id,
|
|
)
|
|
event2 = self.store_event(
|
|
data={"timestamp": timestamp, "fingerprint": ["group-2"]},
|
|
project_id=project.id,
|
|
)
|
|
|
|
action_data = {
|
|
"id": "sentry.mail.actions.NotifyEmailAction",
|
|
"targetType": "Member",
|
|
"targetIdentifier": str(444),
|
|
}
|
|
rule = Rule.objects.create(
|
|
project=self.project,
|
|
label="a rule",
|
|
data={
|
|
"match": "all",
|
|
"actions": [action_data],
|
|
},
|
|
)
|
|
|
|
digest = build_digest(
|
|
project, (event_to_record(event, (rule,)), event_to_record(event2, (rule,)))
|
|
)[0]
|
|
|
|
with self.tasks():
|
|
self.adapter.notify_digest(project, digest, ActionTargetType.MEMBER, 444)
|
|
|
|
assert notify.call_count == 0
|
|
assert len(mail.outbox) == 0
|
|
|
|
|
|
@region_silo_test
|
|
class MailAdapterRuleNotifyTest(BaseMailAdapterTest):
|
|
@mock.patch("sentry.mail.adapter.logger")
|
|
def test_normal(self, mock_logger):
|
|
event = self.store_event(data={}, project_id=self.project.id)
|
|
rule = Rule.objects.create(project=self.project, label="my rule")
|
|
futures = [RuleFuture(rule, {})]
|
|
with mock.patch.object(self.adapter, "notify") as notify:
|
|
self.adapter.rule_notify(event, futures, ActionTargetType.ISSUE_OWNERS)
|
|
assert notify.call_count == 1
|
|
assert event.group
|
|
mock_logger.info.assert_called_with(
|
|
"mail.adapter.notification.%s",
|
|
"dispatched",
|
|
extra={
|
|
"event_id": event.event_id,
|
|
"group_id": event.group.id,
|
|
"is_from_mail_action_adapter": True,
|
|
"target_type": "IssueOwners",
|
|
"target_identifier": None,
|
|
"fallthrough_choice": None,
|
|
"notification_uuid": mock.ANY,
|
|
"rule_id": rule.id,
|
|
"project_id": event.group.project.id,
|
|
},
|
|
)
|
|
|
|
@mock.patch("sentry.mail.adapter.digests")
|
|
@mock.patch("sentry.mail.adapter.logger")
|
|
def test_digest(self, mock_logger, digests):
|
|
digests.backend.enabled.return_value = True
|
|
|
|
event = self.store_event(data={}, project_id=self.project.id)
|
|
rule = self.create_project_rule(project=self.project)
|
|
|
|
futures = [RuleFuture(rule, {})]
|
|
self.adapter.rule_notify(event, futures, ActionTargetType.ISSUE_OWNERS)
|
|
assert digests.backend.add.call_count == 1
|
|
assert event.group
|
|
mock_logger.info.assert_called_with(
|
|
"mail.adapter.notification.%s",
|
|
"dispatched",
|
|
extra={
|
|
"event_id": event.event_id,
|
|
"group_id": event.group.id,
|
|
"is_from_mail_action_adapter": True,
|
|
"target_type": "IssueOwners",
|
|
"target_identifier": None,
|
|
"fallthrough_choice": None,
|
|
"notification_uuid": mock.ANY,
|
|
"rule_id": rule.id,
|
|
"project_id": event.group.project.id,
|
|
"digest_key": mock.ANY,
|
|
},
|
|
)
|
|
|
|
@mock.patch("sentry.mail.adapter.digests")
|
|
def test_digest_with_perf_issue(self, digests):
|
|
digests.backend.enabled.return_value = True
|
|
event = self.create_performance_issue()
|
|
rule = self.create_project_rule(project=self.project)
|
|
|
|
futures = [RuleFuture(rule, {})]
|
|
self.adapter.rule_notify(event, futures, ActionTargetType.ISSUE_OWNERS)
|
|
assert digests.backend.add.call_count == 1
|
|
|
|
def test_notify_includes_uuid(self):
|
|
event = self.store_event(data={}, project_id=self.project.id)
|
|
rule = Rule.objects.create(project=self.project, label="my rule")
|
|
futures = [RuleFuture(rule, {})]
|
|
notification_uuid = str(uuid.uuid4())
|
|
with mock.patch.object(self.adapter, "notify") as notify:
|
|
self.adapter.rule_notify(
|
|
event, futures, ActionTargetType.ISSUE_OWNERS, notification_uuid=notification_uuid
|
|
)
|
|
notify.assert_called_once_with(
|
|
mock.ANY, ActionTargetType.ISSUE_OWNERS, None, None, notification_uuid
|
|
)
|
|
|
|
|
|
@region_silo_test
|
|
class MailAdapterNotifyAboutActivityTest(BaseMailAdapterTest):
|
|
def test_assignment(self):
|
|
with assume_test_silo_mode(SiloMode.CONTROL):
|
|
NotificationSettingOption.objects.create(
|
|
user_id=self.user.id,
|
|
scope_type="user",
|
|
scope_identifier=self.user.id,
|
|
type="workflow",
|
|
value="always",
|
|
)
|
|
activity = Activity.objects.create(
|
|
project=self.project,
|
|
group=self.group,
|
|
type=ActivityType.ASSIGNED.value,
|
|
user_id=self.create_user("[email protected]").id,
|
|
data={"assignee": str(self.user.id), "assigneeType": "user"},
|
|
)
|
|
|
|
with self.tasks():
|
|
self.adapter.notify_about_activity(activity)
|
|
|
|
assert len(mail.outbox) == 1
|
|
|
|
msg = mail.outbox[0]
|
|
|
|
assert msg.subject == "Re: [Sentry] BAR-1 - こんにちは"
|
|
assert msg.to == [self.user.email]
|
|
assert "notification_uuid" in msg.body
|
|
|
|
def test_assignment_team(self):
|
|
with assume_test_silo_mode(SiloMode.CONTROL):
|
|
NotificationSettingOption.objects.create(
|
|
user_id=self.user.id,
|
|
scope_type="user",
|
|
scope_identifier=self.user.id,
|
|
type="workflow",
|
|
value="always",
|
|
)
|
|
activity = Activity.objects.create(
|
|
project=self.project,
|
|
group=self.group,
|
|
type=ActivityType.ASSIGNED.value,
|
|
user_id=self.create_user("[email protected]").id,
|
|
data={"assignee": str(self.project.teams.first().id), "assigneeType": "team"},
|
|
)
|
|
|
|
with self.tasks():
|
|
self.adapter.notify_about_activity(activity)
|
|
|
|
assert len(mail.outbox) == 1
|
|
|
|
msg = mail.outbox[0]
|
|
|
|
assert msg.subject == "Re: [Sentry] BAR-1 - こんにちは"
|
|
assert msg.to == [self.user.email]
|
|
assert "notification_uuid" in msg.body
|
|
|
|
def test_note(self):
|
|
user_foo = self.create_user("[email protected]")
|
|
with assume_test_silo_mode(SiloMode.CONTROL):
|
|
NotificationSettingOption.objects.create(
|
|
user_id=self.user.id,
|
|
scope_type="user",
|
|
scope_identifier=self.user.id,
|
|
type="workflow",
|
|
value="always",
|
|
)
|
|
activity = Activity.objects.create(
|
|
project=self.project,
|
|
group=self.group,
|
|
type=ActivityType.NOTE.value,
|
|
user_id=user_foo.id,
|
|
data={"text": "sup guise"},
|
|
)
|
|
|
|
self.project.teams.first().organization.member_set.create(user_id=user_foo.id)
|
|
|
|
with self.tasks():
|
|
self.adapter.notify_about_activity(activity)
|
|
|
|
assert len(mail.outbox) >= 1
|
|
|
|
msg = mail.outbox[-1]
|
|
|
|
assert msg.subject == "Re: [Sentry] BAR-1 - こんにちは"
|
|
assert msg.to == [self.user.email]
|
|
assert "notification_uuid" in msg.body
|
|
|
|
|
|
@region_silo_test
|
|
class MailAdapterHandleSignalTest(BaseMailAdapterTest):
|
|
def create_report(self):
|
|
user_foo = self.create_user("[email protected]")
|
|
self.project.teams.first().organization.member_set.create(user_id=user_foo.id)
|
|
|
|
return UserReport.objects.create(
|
|
project_id=self.project.id,
|
|
group_id=self.group.id,
|
|
name="Homer Simpson",
|
|
email="[email protected]",
|
|
)
|
|
|
|
def test_user_feedback(self):
|
|
with assume_test_silo_mode(SiloMode.CONTROL):
|
|
NotificationSettingOption.objects.create(
|
|
user_id=self.user.id,
|
|
scope_type="user",
|
|
scope_identifier=self.user.id,
|
|
type="workflow",
|
|
value="always",
|
|
)
|
|
report = self.create_report()
|
|
with self.tasks():
|
|
self.adapter.handle_user_report(
|
|
project=self.project,
|
|
report=serialize(report, AnonymousUser(), UserReportWithGroupSerializer()),
|
|
)
|
|
|
|
assert len(mail.outbox) == 1
|
|
msg = mail.outbox[0]
|
|
assert isinstance(msg, EmailMultiAlternatives)
|
|
|
|
# email includes issue metadata
|
|
assert isinstance(msg.alternatives[0][0], str)
|
|
assert "group-header" in msg.alternatives[0][0]
|
|
assert "enhanced privacy" not in msg.body
|
|
|
|
assert (
|
|
msg.subject
|
|
== f"[Sentry] {self.group.qualified_short_id} - New Feedback from Homer Simpson"
|
|
)
|
|
assert msg.to == [self.user.email]
|
|
assert "notification_uuid" in msg.body
|
|
|
|
def test_user_feedback__enhanced_privacy(self):
|
|
with assume_test_silo_mode(SiloMode.CONTROL):
|
|
NotificationSettingOption.objects.create(
|
|
user_id=self.user.id,
|
|
scope_type="user",
|
|
scope_identifier=self.user.id,
|
|
type="workflow",
|
|
value="always",
|
|
)
|
|
self.organization.update(flags=F("flags").bitor(Organization.flags.enhanced_privacy))
|
|
assert self.organization.flags.enhanced_privacy.is_set is True
|
|
report = self.create_report()
|
|
|
|
with self.tasks():
|
|
self.adapter.handle_user_report(
|
|
project=self.project,
|
|
report=serialize(report, AnonymousUser(), UserReportWithGroupSerializer()),
|
|
)
|
|
|
|
assert len(mail.outbox) == 1
|
|
msg = mail.outbox[0]
|
|
assert isinstance(msg, EmailMultiAlternatives)
|
|
|
|
# email does not include issue metadata
|
|
assert isinstance(msg.alternatives[0][0], str)
|
|
assert "group-header" not in msg.alternatives[0][0]
|
|
assert "enhanced privacy" in msg.body
|
|
|
|
assert (
|
|
msg.subject
|
|
== f"[Sentry] {self.group.qualified_short_id} - New Feedback from Homer Simpson"
|
|
)
|
|
assert msg.to == [self.user.email]
|
|
assert "notification_uuid" in msg.body
|