chore: random slack code cleanup (#5307)
# What this PR does Related to https://github.com/grafana/oncall/pull/5287 Few random "clean-ups", type improvements, etc. Additionally, fixes a change made in #5292; we should wait to read from `slack_message.channel.slack_id`, until we've performed the data-migration mentioned in that PR (in the mean-time we should continue to use `slack_message._channel_id`). ## Checklist - [x] Unit, integration, and e2e (if applicable) tests updated - [x] Documentation added (or `pr:no public docs` PR label added if not required) - [x] Added the relevant release notes label (see labels prefixed w/ `release:`). These labels dictate how your PR will show up in the autogenerated release notes.
This commit is contained in:
parent
417f9787de
commit
5227ee3798
28 changed files with 313 additions and 259 deletions
|
|
@ -674,7 +674,7 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models.
|
|||
organization_id = user.organization_id if user else self.channel.organization_id
|
||||
logger.debug(f"Started acknowledge_by_user_or_backsync for alert_group {self.pk}")
|
||||
|
||||
# if incident was silenced or resolved, unsilence/unresolve it without starting escalation
|
||||
# if alert group was silenced or resolved, unsilence/unresolve it without starting escalation
|
||||
if self.silenced:
|
||||
self.un_silence()
|
||||
self.log_records.create(
|
||||
|
|
@ -1990,6 +1990,13 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models.
|
|||
|
||||
@property
|
||||
def slack_message(self) -> typing.Optional["SlackMessage"]:
|
||||
"""
|
||||
`slack_message` property returns the first `SlackMessage` for the `AlertGroup`. This corresponds to the
|
||||
Slack message representing the main message in Slack (ie. not a message in a thread).
|
||||
|
||||
This should not be confused with `slack_messages`, which is a `RelatedManager` that returns all `SlackMessage`
|
||||
instances for the `AlertGroup`.
|
||||
"""
|
||||
try:
|
||||
# prefetched_slack_messages could be set in apps.api.serializers.alert_group.AlertGroupListSerializer
|
||||
return self.prefetched_slack_messages[0] if self.prefetched_slack_messages else None
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ if typing.TYPE_CHECKING:
|
|||
|
||||
from apps.alerts.models import AlertGroup, ChannelFilter
|
||||
from apps.labels.models import AlertReceiveChannelAssociatedLabel
|
||||
from apps.user_management.models import Organization, Team
|
||||
from apps.user_management.models import Organization, Team, User
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -391,7 +391,7 @@ class AlertReceiveChannel(IntegrationOptionsMixin, MaintainableObject):
|
|||
|
||||
return super().save(*args, **kwargs)
|
||||
|
||||
def change_team(self, team_id, user):
|
||||
def change_team(self, team_id: int, user: "User") -> None:
|
||||
if team_id == self.team_id:
|
||||
raise TeamCanNotBeChangedError("Integration is already in this team")
|
||||
|
||||
|
|
@ -409,26 +409,26 @@ class AlertReceiveChannel(IntegrationOptionsMixin, MaintainableObject):
|
|||
return GrafanaAlertingSyncManager(self)
|
||||
|
||||
@property
|
||||
def is_alerting_integration(self):
|
||||
def is_alerting_integration(self) -> bool:
|
||||
return self.integration in {
|
||||
AlertReceiveChannel.INTEGRATION_GRAFANA_ALERTING,
|
||||
AlertReceiveChannel.INTEGRATION_LEGACY_GRAFANA_ALERTING,
|
||||
}
|
||||
|
||||
@cached_property
|
||||
def team_name(self):
|
||||
def team_name(self) -> str:
|
||||
return self.team.name if self.team else "No team"
|
||||
|
||||
@cached_property
|
||||
def team_id_or_no_team(self):
|
||||
def team_id_or_no_team(self) -> str:
|
||||
return self.team_id if self.team else "no_team"
|
||||
|
||||
@cached_property
|
||||
def emojized_verbal_name(self):
|
||||
def emojized_verbal_name(self) -> str:
|
||||
return emoji.emojize(self.verbal_name, language="alias")
|
||||
|
||||
@property
|
||||
def new_incidents_web_link(self):
|
||||
def new_incidents_web_link(self) -> str:
|
||||
from apps.alerts.models import AlertGroup
|
||||
|
||||
return UIURLBuilder(self.organization).alert_groups(
|
||||
|
|
@ -436,7 +436,7 @@ class AlertReceiveChannel(IntegrationOptionsMixin, MaintainableObject):
|
|||
)
|
||||
|
||||
@property
|
||||
def is_rate_limited_in_slack(self):
|
||||
def is_rate_limited_in_slack(self) -> bool:
|
||||
return (
|
||||
self.rate_limited_in_slack_at is not None
|
||||
and self.rate_limited_in_slack_at + SLACK_RATE_LIMIT_TIMEOUT > timezone.now()
|
||||
|
|
@ -450,11 +450,11 @@ class AlertReceiveChannel(IntegrationOptionsMixin, MaintainableObject):
|
|||
post_slack_rate_limit_message.apply_async((self.pk,), countdown=delay, task_id=task_id)
|
||||
|
||||
@property
|
||||
def alert_groups_count(self):
|
||||
def alert_groups_count(self) -> int:
|
||||
return self.alert_groups.count()
|
||||
|
||||
@property
|
||||
def alerts_count(self):
|
||||
def alerts_count(self) -> int:
|
||||
from apps.alerts.models import Alert
|
||||
|
||||
return Alert.objects.filter(group__channel=self).count()
|
||||
|
|
@ -464,7 +464,7 @@ class AlertReceiveChannel(IntegrationOptionsMixin, MaintainableObject):
|
|||
return self.config.is_able_to_autoresolve
|
||||
|
||||
@property
|
||||
def is_demo_alert_enabled(self):
|
||||
def is_demo_alert_enabled(self) -> bool:
|
||||
return self.config.is_demo_alert_enabled
|
||||
|
||||
@property
|
||||
|
|
@ -513,7 +513,7 @@ class AlertReceiveChannel(IntegrationOptionsMixin, MaintainableObject):
|
|||
return alert_receive_channel
|
||||
|
||||
@property
|
||||
def short_name(self):
|
||||
def short_name(self) -> str:
|
||||
if self.verbal_name is None:
|
||||
return self.created_name + "" if self.deleted_at is None else "(Deleted)"
|
||||
elif self.verbal_name == self.created_name:
|
||||
|
|
@ -548,14 +548,14 @@ class AlertReceiveChannel(IntegrationOptionsMixin, MaintainableObject):
|
|||
return create_engine_url(f"integrations/v1/{slug}/{self.token}/")
|
||||
|
||||
@property
|
||||
def inbound_email(self):
|
||||
def inbound_email(self) -> typing.Optional[str]:
|
||||
if self.integration != AlertReceiveChannel.INTEGRATION_INBOUND_EMAIL:
|
||||
return None
|
||||
|
||||
return f"{self.token}@{live_settings.INBOUND_EMAIL_DOMAIN}"
|
||||
|
||||
@property
|
||||
def default_channel_filter(self):
|
||||
def default_channel_filter(self) -> typing.Optional["ChannelFilter"]:
|
||||
return self.channel_filters.filter(is_default=True).first()
|
||||
|
||||
# Templating
|
||||
|
|
@ -590,7 +590,7 @@ class AlertReceiveChannel(IntegrationOptionsMixin, MaintainableObject):
|
|||
}
|
||||
|
||||
@property
|
||||
def is_available_for_custom_templates(self):
|
||||
def is_available_for_custom_templates(self) -> bool:
|
||||
return True
|
||||
|
||||
# Maintenance
|
||||
|
|
|
|||
|
|
@ -1,4 +0,0 @@
|
|||
def compare_escalations(request_id, active_escalation_id):
|
||||
if request_id == active_escalation_id:
|
||||
return True
|
||||
return False
|
||||
|
|
@ -6,7 +6,6 @@ from kombu.utils.uuid import uuid as celery_uuid
|
|||
|
||||
from common.custom_celery_tasks import shared_dedicated_queue_retry_task
|
||||
|
||||
from .compare_escalations import compare_escalations
|
||||
from .task_logger import task_logger
|
||||
|
||||
|
||||
|
|
@ -29,7 +28,7 @@ def escalate_alert_group(alert_group_pk):
|
|||
except IndexError:
|
||||
return f"Alert group with pk {alert_group_pk} doesn't exist"
|
||||
|
||||
if not compare_escalations(escalate_alert_group.request.id, alert_group.active_escalation_id):
|
||||
if escalate_alert_group.request.id != alert_group.active_escalation_id:
|
||||
return "Active escalation ID mismatch. Duplication or non-active escalation triggered. Active: {}".format(
|
||||
alert_group.active_escalation_id
|
||||
)
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ from apps.metrics_exporter.tasks import update_metrics_for_user
|
|||
from apps.phone_notifications.phone_backend import PhoneBackend
|
||||
from common.custom_celery_tasks import shared_dedicated_queue_retry_task
|
||||
|
||||
from .compare_escalations import compare_escalations
|
||||
from .task_logger import task_logger
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
|
|
@ -618,7 +617,7 @@ def send_bundled_notification(user_notification_bundle_id: int):
|
|||
)
|
||||
return
|
||||
|
||||
if not compare_escalations(send_bundled_notification.request.id, user_notification_bundle.notification_task_id):
|
||||
if send_bundled_notification.request.id != user_notification_bundle.notification_task_id:
|
||||
task_logger.info(
|
||||
f"send_bundled_notification: notification_task_id mismatch. "
|
||||
f"Duplication or non-active notification triggered. "
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ from django.db import transaction
|
|||
|
||||
from common.custom_celery_tasks import shared_dedicated_queue_retry_task
|
||||
|
||||
from .compare_escalations import compare_escalations
|
||||
from .send_alert_group_signal import send_alert_group_signal
|
||||
from .task_logger import task_logger
|
||||
|
||||
|
|
@ -17,17 +16,20 @@ def unsilence_task(alert_group_pk):
|
|||
from apps.alerts.models import AlertGroup, AlertGroupLogRecord
|
||||
|
||||
task_logger.info(f"Start unsilence_task for alert_group {alert_group_pk}")
|
||||
|
||||
with transaction.atomic():
|
||||
try:
|
||||
alert_group = AlertGroup.objects.filter(pk=alert_group_pk).select_for_update()[0] # Lock alert_group:
|
||||
except IndexError:
|
||||
task_logger.info(f"unsilence_task. alert_group {alert_group_pk} doesn't exist")
|
||||
return
|
||||
if not compare_escalations(unsilence_task.request.id, alert_group.unsilence_task_uuid):
|
||||
|
||||
if unsilence_task.request.id != alert_group.unsilence_task_uuid:
|
||||
task_logger.info(
|
||||
f"unsilence_task. alert_group {alert_group.pk}.ID mismatch.Active: {alert_group.unsilence_task_uuid}"
|
||||
)
|
||||
return
|
||||
|
||||
if alert_group.status == AlertGroup.SILENCED and alert_group.is_root_alert_group:
|
||||
initial_state = alert_group.state
|
||||
task_logger.info(f"unsilence alert_group {alert_group_pk} and start escalation if needed")
|
||||
|
|
|
|||
|
|
@ -152,11 +152,11 @@ def test_delete(
|
|||
# Check that appropriate Slack API calls are made
|
||||
assert mock_chat_delete.call_count == 2
|
||||
assert mock_chat_delete.call_args_list[0] == call(
|
||||
channel=resolution_note_1.slack_channel_id, ts=resolution_note_1.ts
|
||||
channel=resolution_note_1.slack_channel.slack_id, ts=resolution_note_1.ts
|
||||
)
|
||||
assert mock_chat_delete.call_args_list[1] == call(channel=slack_message.channel.slack_id, ts=slack_message.slack_id)
|
||||
mock_reactions_remove.assert_called_once_with(
|
||||
channel=resolution_note_2.slack_channel_id, name="memo", timestamp=resolution_note_2.ts
|
||||
channel=resolution_note_2.slack_channel.slack_id, name="memo", timestamp=resolution_note_2.ts
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -16,9 +16,7 @@ def maintenance_test_setup(
|
|||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_start_maintenance_integration(
|
||||
maintenance_test_setup, make_alert_receive_channel, mock_start_disable_maintenance_task
|
||||
):
|
||||
def test_start_maintenance_integration(maintenance_test_setup, make_alert_receive_channel):
|
||||
organization, user = maintenance_test_setup
|
||||
|
||||
alert_receive_channel = make_alert_receive_channel(
|
||||
|
|
@ -37,9 +35,7 @@ def test_start_maintenance_integration(
|
|||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_start_maintenance_integration_multiple_previous_instances(
|
||||
maintenance_test_setup, make_alert_receive_channel, mock_start_disable_maintenance_task
|
||||
):
|
||||
def test_start_maintenance_integration_multiple_previous_instances(maintenance_test_setup, make_alert_receive_channel):
|
||||
organization, user = maintenance_test_setup
|
||||
|
||||
alert_receive_channel = make_alert_receive_channel(
|
||||
|
|
@ -64,9 +60,7 @@ def test_start_maintenance_integration_multiple_previous_instances(
|
|||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_maintenance_integration_will_not_start_twice(
|
||||
maintenance_test_setup, make_alert_receive_channel, mock_start_disable_maintenance_task
|
||||
):
|
||||
def test_maintenance_integration_will_not_start_twice(maintenance_test_setup, make_alert_receive_channel):
|
||||
organization, user = maintenance_test_setup
|
||||
|
||||
alert_receive_channel = make_alert_receive_channel(
|
||||
|
|
@ -91,7 +85,6 @@ def test_alert_attached_to_maintenance_incident_integration(
|
|||
maintenance_test_setup,
|
||||
make_alert_receive_channel,
|
||||
make_alert_with_custom_create_method,
|
||||
mock_start_disable_maintenance_task,
|
||||
):
|
||||
organization, user = maintenance_test_setup
|
||||
|
||||
|
|
@ -122,7 +115,6 @@ def test_stop_maintenance(
|
|||
maintenance_test_setup,
|
||||
make_alert_receive_channel,
|
||||
make_alert_with_custom_create_method,
|
||||
mock_start_disable_maintenance_task,
|
||||
):
|
||||
organization, user = maintenance_test_setup
|
||||
alert_receive_channel = make_alert_receive_channel(
|
||||
|
|
|
|||
|
|
@ -530,47 +530,73 @@ def test_send_bundle_notification(
|
|||
alert_group_1 = make_alert_group(alert_receive_channel=alert_receive_channel)
|
||||
alert_group_2 = make_alert_group(alert_receive_channel=alert_receive_channel)
|
||||
alert_group_3 = make_alert_group(alert_receive_channel=alert_receive_channel)
|
||||
|
||||
task_id = "test_task_id"
|
||||
notification_bundle = make_user_notification_bundle(
|
||||
user, UserNotificationPolicy.NotificationChannel.SMS, notification_task_id="test_task_id", eta=timezone.now()
|
||||
user, UserNotificationPolicy.NotificationChannel.SMS, notification_task_id=task_id, eta=timezone.now()
|
||||
)
|
||||
|
||||
notification_bundle.append_notification(alert_group_1, notification_policy)
|
||||
notification_bundle.append_notification(alert_group_2, notification_policy)
|
||||
notification_bundle.append_notification(alert_group_3, notification_policy)
|
||||
|
||||
assert notification_bundle.notifications.filter(bundle_uuid__isnull=True).count() == 3
|
||||
|
||||
alert_group_3.resolve()
|
||||
with patch("apps.alerts.tasks.notify_user.compare_escalations", return_value=True):
|
||||
# send notification for 2 active alert groups
|
||||
send_bundled_notification(notification_bundle.id)
|
||||
assert f"alert_group {alert_group_3.id} is not active, skip notification" in caplog.text
|
||||
assert "perform bundled notification for alert groups with ids:" in caplog.text
|
||||
# check bundle_uuid was set, notification for resolved alert group was deleted
|
||||
assert notification_bundle.notifications.filter(bundle_uuid__isnull=True).count() == 0
|
||||
assert notification_bundle.notifications.all().count() == 2
|
||||
assert not notification_bundle.notifications.filter(alert_group=alert_group_3).exists()
|
||||
|
||||
# send notification for 1 active alert group
|
||||
notification_bundle.notifications.update(bundle_uuid=None)
|
||||
alert_group_2.resolve()
|
||||
send_bundled_notification(notification_bundle.id)
|
||||
assert f"alert_group {alert_group_2.id} is not active, skip notification" in caplog.text
|
||||
assert (
|
||||
f"there is only one alert group in bundled notification, perform regular notification. "
|
||||
f"alert_group {alert_group_1.id}"
|
||||
) in caplog.text
|
||||
# check bundle_uuid was set
|
||||
assert notification_bundle.notifications.filter(bundle_uuid__isnull=True).count() == 0
|
||||
assert notification_bundle.notifications.all().count() == 1
|
||||
# cleanup notifications
|
||||
notification_bundle.notifications.all().delete()
|
||||
# send notification for 2 active alert groups
|
||||
send_bundled_notification.apply((notification_bundle.id,), task_id=task_id)
|
||||
|
||||
# send notification for 0 active alert group
|
||||
notification_bundle.append_notification(alert_group_1, notification_policy)
|
||||
alert_group_1.resolve()
|
||||
send_bundled_notification(notification_bundle.id)
|
||||
assert f"alert_group {alert_group_1.id} is not active, skip notification" in caplog.text
|
||||
assert f"no alert groups to notify about or notification is not allowed for user {user.id}" in caplog.text
|
||||
# check all notifications were deleted
|
||||
assert notification_bundle.notifications.all().count() == 0
|
||||
assert f"alert_group {alert_group_3.id} is not active, skip notification" in caplog.text
|
||||
assert "perform bundled notification for alert groups with ids:" in caplog.text
|
||||
|
||||
# check bundle_uuid was set, notification for resolved alert group was deleted
|
||||
assert notification_bundle.notifications.filter(bundle_uuid__isnull=True).count() == 0
|
||||
assert notification_bundle.notifications.all().count() == 2
|
||||
assert not notification_bundle.notifications.filter(alert_group=alert_group_3).exists()
|
||||
|
||||
# send notification for 1 active alert group
|
||||
notification_bundle.notifications.update(bundle_uuid=None)
|
||||
|
||||
# since we're calling send_bundled_notification several times within this test, we need to reset task_id
|
||||
# because it gets set to None after the first call
|
||||
notification_bundle.notification_task_id = task_id
|
||||
notification_bundle.save()
|
||||
|
||||
alert_group_2.resolve()
|
||||
|
||||
send_bundled_notification.apply((notification_bundle.id,), task_id=task_id)
|
||||
|
||||
assert f"alert_group {alert_group_2.id} is not active, skip notification" in caplog.text
|
||||
assert (
|
||||
f"there is only one alert group in bundled notification, perform regular notification. "
|
||||
f"alert_group {alert_group_1.id}"
|
||||
) in caplog.text
|
||||
|
||||
# check bundle_uuid was set
|
||||
assert notification_bundle.notifications.filter(bundle_uuid__isnull=True).count() == 0
|
||||
assert notification_bundle.notifications.all().count() == 1
|
||||
|
||||
# cleanup notifications
|
||||
notification_bundle.notifications.all().delete()
|
||||
|
||||
# send notification for 0 active alert group
|
||||
notification_bundle.append_notification(alert_group_1, notification_policy)
|
||||
|
||||
# since we're calling send_bundled_notification several times within this test, we need to reset task_id
|
||||
# because it gets set to None after the first call
|
||||
notification_bundle.notification_task_id = task_id
|
||||
notification_bundle.save()
|
||||
|
||||
alert_group_1.resolve()
|
||||
|
||||
send_bundled_notification.apply((notification_bundle.id,), task_id=task_id)
|
||||
|
||||
assert f"alert_group {alert_group_1.id} is not active, skip notification" in caplog.text
|
||||
assert f"no alert groups to notify about or notification is not allowed for user {user.id}" in caplog.text
|
||||
|
||||
# check all notifications were deleted
|
||||
assert notification_bundle.notifications.all().count() == 0
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
|
|
|
|||
|
|
@ -5,14 +5,8 @@ from apps.alerts.models import AlertReceiveChannel
|
|||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_silence_alert_group(
|
||||
make_organization_and_user,
|
||||
make_alert_receive_channel,
|
||||
make_alert_group,
|
||||
make_alert,
|
||||
mock_start_disable_maintenance_task,
|
||||
):
|
||||
organization, user = make_organization_and_user()
|
||||
def test_silence_alert_group(make_organization_and_user, make_alert_receive_channel, make_alert_group):
|
||||
organization, _ = make_organization_and_user()
|
||||
alert_receive_channel = make_alert_receive_channel(
|
||||
organization, integration=AlertReceiveChannel.INTEGRATION_GRAFANA
|
||||
)
|
||||
|
|
@ -24,14 +18,8 @@ def test_silence_alert_group(
|
|||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_silence_by_user_alert_group(
|
||||
make_organization_and_user,
|
||||
make_alert_receive_channel,
|
||||
make_alert_group,
|
||||
make_alert,
|
||||
mock_start_disable_maintenance_task,
|
||||
):
|
||||
organization, user = make_organization_and_user()
|
||||
def test_silence_by_user_alert_group(make_organization_and_user, make_alert_receive_channel, make_alert_group):
|
||||
organization, _ = make_organization_and_user()
|
||||
alert_receive_channel = make_alert_receive_channel(
|
||||
organization, integration=AlertReceiveChannel.INTEGRATION_GRAFANA
|
||||
)
|
||||
|
|
@ -43,13 +31,7 @@ def test_silence_by_user_alert_group(
|
|||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_unsilence_alert_group(
|
||||
make_organization_and_user,
|
||||
make_alert_receive_channel,
|
||||
make_alert_group,
|
||||
make_alert,
|
||||
mock_start_disable_maintenance_task,
|
||||
):
|
||||
def test_unsilence_alert_group(make_organization_and_user, make_alert_receive_channel, make_alert_group):
|
||||
organization, user = make_organization_and_user()
|
||||
alert_receive_channel = make_alert_receive_channel(
|
||||
organization, integration=AlertReceiveChannel.INTEGRATION_GRAFANA
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import pytest
|
|||
from django.utils import timezone
|
||||
|
||||
from apps.slack.client import SlackClient
|
||||
from apps.slack.scenarios.distribute_alerts import AlertShootingStep
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
|
|
@ -22,7 +21,7 @@ def mock_slack_api_call(monkeypatch):
|
|||
|
||||
|
||||
@pytest.fixture()
|
||||
def make_resolved_ack_new_silenced_alert_groups(make_alert_group, make_alert_receive_channel, make_alert):
|
||||
def make_resolved_ack_new_silenced_alert_groups(make_alert_group, make_alert):
|
||||
def _make_alert_groups_all_statuses(alert_receive_channel, channel_filter, alert_raw_request_data, **kwargs):
|
||||
resolved_alert_group = make_alert_group(
|
||||
alert_receive_channel,
|
||||
|
|
@ -56,11 +55,3 @@ def make_resolved_ack_new_silenced_alert_groups(make_alert_group, make_alert_rec
|
|||
return resolved_alert_group, ack_alert_group, new_alert_group, silenced_alert_group
|
||||
|
||||
return _make_alert_groups_all_statuses
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def mock_alert_shooting_step_post_alert_group_to_slack(monkeypatch):
|
||||
def mock_post_alert_group_to_slack(*args, **kwargs):
|
||||
return None
|
||||
|
||||
monkeypatch.setattr(AlertShootingStep, "_post_alert_group_to_slack", mock_post_alert_group_to_slack)
|
||||
|
|
|
|||
|
|
@ -414,12 +414,7 @@ def test_update_alert_receive_channel(alert_receive_channel_internal_api_setup,
|
|||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_integration_filter_by_maintenance(
|
||||
alert_receive_channel_internal_api_setup,
|
||||
make_user_auth_headers,
|
||||
mock_start_disable_maintenance_task,
|
||||
mock_alert_shooting_step_post_alert_group_to_slack,
|
||||
):
|
||||
def test_integration_filter_by_maintenance(alert_receive_channel_internal_api_setup, make_user_auth_headers):
|
||||
user, token, alert_receive_channel = alert_receive_channel_internal_api_setup
|
||||
client = APIClient()
|
||||
mode = AlertReceiveChannel.MAINTENANCE
|
||||
|
|
@ -437,12 +432,7 @@ def test_integration_filter_by_maintenance(
|
|||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_integration_filter_by_debug(
|
||||
alert_receive_channel_internal_api_setup,
|
||||
make_user_auth_headers,
|
||||
mock_start_disable_maintenance_task,
|
||||
mock_alert_shooting_step_post_alert_group_to_slack,
|
||||
):
|
||||
def test_integration_filter_by_debug(alert_receive_channel_internal_api_setup, make_user_auth_headers):
|
||||
user, token, alert_receive_channel = alert_receive_channel_internal_api_setup
|
||||
client = APIClient()
|
||||
mode = AlertReceiveChannel.DEBUG_MAINTENANCE
|
||||
|
|
@ -1141,7 +1131,6 @@ def test_start_maintenance_integration(
|
|||
|
||||
@pytest.mark.django_db
|
||||
def test_stop_maintenance_integration(
|
||||
mock_start_disable_maintenance_task,
|
||||
make_user_auth_headers,
|
||||
make_organization_and_user_with_plugin_token,
|
||||
make_escalation_chain,
|
||||
|
|
|
|||
|
|
@ -39,7 +39,10 @@ class AlertGroupSlackService:
|
|||
|
||||
try:
|
||||
self._slack_client.chat_update(
|
||||
channel=alert_group.slack_message.channel.slack_id,
|
||||
# TODO: once _channel_id has been fully migrated to channel, remove _channel_id
|
||||
# see https://raintank-corp.slack.com/archives/C06K1MQ07GS/p173255546
|
||||
# channel=alert_group.slack_message.channel.slack_id,
|
||||
channel=alert_group.slack_message._channel_id,
|
||||
ts=alert_group.slack_message.slack_id,
|
||||
attachments=alert_group.render_slack_attachments(),
|
||||
blocks=alert_group.render_slack_blocks(),
|
||||
|
|
@ -78,7 +81,10 @@ class AlertGroupSlackService:
|
|||
|
||||
try:
|
||||
result = self._slack_client.chat_postMessage(
|
||||
channel=slack_message.channel.slack_id,
|
||||
# TODO: once _channel_id has been fully migrated to channel, remove _channel_id
|
||||
# see https://raintank-corp.slack.com/archives/C06K1MQ07GS/p173255546
|
||||
# channel=slack_message.channel.slack_id,
|
||||
channel=slack_message._channel_id,
|
||||
text=text,
|
||||
attachments=attachments,
|
||||
thread_ts=slack_message.slack_id,
|
||||
|
|
|
|||
|
|
@ -70,10 +70,9 @@ class SlackMessage(models.Model):
|
|||
|
||||
ack_reminder_message_ts = models.CharField(max_length=100, null=True, default=None)
|
||||
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
cached_permalink = models.URLField(max_length=250, null=True, default=None)
|
||||
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
last_updated = models.DateTimeField(null=True, default=None)
|
||||
|
||||
alert_group = models.ForeignKey(
|
||||
|
|
@ -84,8 +83,10 @@ class SlackMessage(models.Model):
|
|||
related_name="slack_messages",
|
||||
)
|
||||
|
||||
# ID of a latest celery task to update the message
|
||||
active_update_task_id = models.CharField(max_length=100, null=True, default=None)
|
||||
"""
|
||||
ID of the latest celery task to update the message
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
# slack_id is unique within the context of a channel or conversation
|
||||
|
|
@ -105,7 +106,11 @@ class SlackMessage(models.Model):
|
|||
|
||||
try:
|
||||
result = SlackClient(self.slack_team_identity).chat_getPermalink(
|
||||
channel=self.channel.slack_id, message_ts=self.slack_id
|
||||
# TODO: once _channel_id has been fully migrated to channel, remove _channel_id
|
||||
# see https://raintank-corp.slack.com/archives/C06K1MQ07GS/p173255546
|
||||
# channel=self.channel.slack_id,
|
||||
channel=self._channel_id,
|
||||
message_ts=self.slack_id,
|
||||
)
|
||||
except SlackAPIError:
|
||||
return None
|
||||
|
|
@ -117,7 +122,9 @@ class SlackMessage(models.Model):
|
|||
|
||||
@property
|
||||
def deep_link(self) -> str:
|
||||
return f"https://slack.com/app_redirect?channel={self.channel.slack_id}&team={self.slack_team_identity.slack_id}&message={self.slack_id}"
|
||||
# TODO: once _channel_id has been fully migrated to channel, remove _channel_id
|
||||
# see https://raintank-corp.slack.com/archives/C06K1MQ07GS/p173255546
|
||||
return f"https://slack.com/app_redirect?channel={self._channel_id}&team={self.slack_team_identity.slack_id}&message={self.slack_id}"
|
||||
|
||||
@classmethod
|
||||
def send_slack_notification(
|
||||
|
|
@ -131,7 +138,6 @@ class SlackMessage(models.Model):
|
|||
|
||||
Still some more investigation needed to confirm this, but for now, we'll pass in the `alert_group` as an argument
|
||||
"""
|
||||
|
||||
from apps.base.models import UserNotificationPolicyLogRecord
|
||||
|
||||
slack_message = alert_group.slack_message
|
||||
|
|
|
|||
|
|
@ -132,9 +132,12 @@ class SlackUserIdentity(models.Model):
|
|||
"elements": [
|
||||
{
|
||||
"type": "mrkdwn",
|
||||
# TODO: once _channel_id has been fully migrated to channel, remove _channel_id
|
||||
# see https://raintank-corp.slack.com/archives/C06K1MQ07GS/p173255546
|
||||
# f"<#{slack_message.channel.slack_id}>.\n"
|
||||
"text": (
|
||||
f"You received this message because you're not a member of "
|
||||
f"<#{slack_message.channel.slack_id}>.\n"
|
||||
f"<#{slack_message._channel_id}>.\n"
|
||||
"Please join the channel to get notified right in the alert group thread."
|
||||
),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,8 +43,8 @@ def on_create_alert_slack_representative_async(alert_pk):
|
|||
logger.debug(
|
||||
f"Process on_create_alert_slack_representative for alert {alert_pk} from alert_group {alert.group_id}"
|
||||
)
|
||||
AlertShootingStep = ScenarioStep.get_step("distribute_alerts", "AlertShootingStep")
|
||||
step = AlertShootingStep(organization.slack_team_identity, organization)
|
||||
IncomingAlertStep = ScenarioStep.get_step("distribute_alerts", "IncomingAlertStep")
|
||||
step = IncomingAlertStep(organization.slack_team_identity, organization)
|
||||
step.process_signal(alert)
|
||||
else:
|
||||
logger.debug(
|
||||
|
|
@ -91,7 +91,7 @@ def on_alert_group_action_triggered_async(log_record_id):
|
|||
|
||||
|
||||
class AlertGroupSlackRepresentative(AlertGroupAbstractRepresentative):
|
||||
def __init__(self, log_record):
|
||||
def __init__(self, log_record: "AlertGroupLogRecord"):
|
||||
self.log_record = log_record
|
||||
|
||||
def is_applicable(self):
|
||||
|
|
|
|||
|
|
@ -87,7 +87,10 @@ class UpdateAppearanceStep(scenario_step.ScenarioStep):
|
|||
slack_message = alert_group.slack_message
|
||||
|
||||
self._slack_client.chat_update(
|
||||
channel=slack_message.channel.slack_id,
|
||||
# TODO: once _channel_id has been fully migrated to channel, remove _channel_id
|
||||
# see https://raintank-corp.slack.com/archives/C06K1MQ07GS/p173255546
|
||||
# channel=slack_message.channel.slack_id,
|
||||
channel=slack_message._channel_id,
|
||||
ts=slack_message.slack_id,
|
||||
attachments=alert_group.render_slack_attachments(),
|
||||
blocks=alert_group.render_slack_blocks(),
|
||||
|
|
|
|||
|
|
@ -50,69 +50,147 @@ logger = logging.getLogger(__name__)
|
|||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
|
||||
class AlertShootingStep(scenario_step.ScenarioStep):
|
||||
class IncomingAlertStep(scenario_step.ScenarioStep):
|
||||
def process_signal(self, alert: Alert) -> None:
|
||||
"""
|
||||
🐉 Here lay dragons 🐉
|
||||
|
||||
Below is some added context as to why we have the explicit `AlertGroup.objects.filter(...).update(...)` calls.
|
||||
|
||||
For example:
|
||||
```
|
||||
num_updated_rows = AlertGroup.objects.filter(
|
||||
pk=alert.group.pk, slack_message_sent=False,
|
||||
).update(slack_message_sent=True)
|
||||
|
||||
if num_updated_rows == 1:
|
||||
# post new message
|
||||
else:
|
||||
# update existing message
|
||||
```
|
||||
|
||||
This piece of code guarantees that when 2 alerts are created concurrently we don't end up posting a
|
||||
message twice. We rely on the fact that the `UPDATE` SQL statements are atomic. This is NOT the same as:
|
||||
|
||||
```
|
||||
if not alert_group.slack_message_sent:
|
||||
# post new message
|
||||
alert_group.slack_message_sent = True
|
||||
else:
|
||||
# update existing message
|
||||
```
|
||||
|
||||
This would end up posting multiple Slack messages for a single alert group (classic race condition). And then
|
||||
all kinds of unexpected behaviours would arise, because some parts of the codebase assume there can only be 1
|
||||
`SlackMessage` per `AlertGroup`.
|
||||
|
||||
```
|
||||
AlertGroup.objects.filter(pk=alert.group.pk, slack_message_sent=False).update(slack_message_sent=True)
|
||||
```
|
||||
|
||||
The power of this 👆, is that it doesn't actually do `SELECT ...;` and then `UPDATE ...;` it actually does a
|
||||
single `UPDATE alerts.alert_group WHERE id=<ID> AND slack_message_sent IS FALSE`;
|
||||
which makes it atomic and concurrency-safe.
|
||||
|
||||
See this conversation for more context https://raintank-corp.slack.com/archives/C06K1MQ07GS/p1732800180834819?thread_ts=1732748893.183939&cid=C06K1MQ07GS
|
||||
"""
|
||||
|
||||
alert_group = alert.group
|
||||
|
||||
if not alert_group:
|
||||
# this case should hypothetically never happen, it's mostly to appease mypy with the
|
||||
# fact that alert.group can "technically" be None
|
||||
logger.warning(
|
||||
f"Skip IncomingAlertStep.process_signal because alert {alert.pk} doesn't actually "
|
||||
"have an alert group associated with it"
|
||||
)
|
||||
return
|
||||
|
||||
alert_group_pk = alert_group.pk
|
||||
alert_receive_channel = alert_group.channel
|
||||
should_skip_escalation_in_slack = alert_group.skip_escalation_in_slack
|
||||
slack_team_identity = self.slack_team_identity
|
||||
slack_team_identity_pk = slack_team_identity.pk
|
||||
|
||||
# do not try to post alert group message to slack if its channel is rate limited
|
||||
if alert.group.channel.is_rate_limited_in_slack:
|
||||
if alert_receive_channel.is_rate_limited_in_slack:
|
||||
logger.info("Skip posting or updating alert_group in Slack due to rate limit")
|
||||
AlertGroup.objects.filter(
|
||||
pk=alert.group.pk,
|
||||
pk=alert_group_pk,
|
||||
slack_message_sent=False,
|
||||
).update(slack_message_sent=True, reason_to_skip_escalation=AlertGroup.RATE_LIMITED)
|
||||
return
|
||||
|
||||
num_updated_rows = AlertGroup.objects.filter(pk=alert.group.pk, slack_message_sent=False).update(
|
||||
num_updated_rows = AlertGroup.objects.filter(pk=alert_group_pk, slack_message_sent=False).update(
|
||||
slack_message_sent=True
|
||||
)
|
||||
|
||||
if num_updated_rows == 1:
|
||||
# this will be the case in the event that we haven't yet created a Slack message for this alert group
|
||||
|
||||
slack_channel = (
|
||||
alert_group.channel_filter.slack_channel
|
||||
if alert_group.channel_filter
|
||||
# if channel filter is deleted mid escalation, use default Slack channel
|
||||
else alert_receive_channel.organization.default_slack_channel
|
||||
)
|
||||
slack_channel_id = slack_channel.slack_id
|
||||
|
||||
try:
|
||||
slack_channel = (
|
||||
alert.group.channel_filter.slack_channel
|
||||
if alert.group.channel_filter
|
||||
# if channel filter is deleted mid escalation, use default Slack channel
|
||||
else alert.group.channel.organization.default_slack_channel
|
||||
self._post_alert_group_to_slack(
|
||||
slack_team_identity=slack_team_identity,
|
||||
alert_group=alert_group,
|
||||
alert=alert,
|
||||
attachments=alert_group.render_slack_attachments(),
|
||||
slack_channel=slack_channel,
|
||||
blocks=alert_group.render_slack_blocks(),
|
||||
)
|
||||
self._send_first_alert(alert, slack_channel)
|
||||
except (SlackAPIError, TimeoutError):
|
||||
AlertGroup.objects.filter(pk=alert.group.pk).update(slack_message_sent=False)
|
||||
AlertGroup.objects.filter(pk=alert_group_pk).update(slack_message_sent=False)
|
||||
raise
|
||||
|
||||
if alert.group.channel.maintenance_mode == AlertReceiveChannel.DEBUG_MAINTENANCE:
|
||||
self._send_debug_mode_notice(alert.group, slack_channel)
|
||||
if alert_receive_channel.maintenance_mode == AlertReceiveChannel.DEBUG_MAINTENANCE:
|
||||
# send debug mode notice
|
||||
text = "Escalations are silenced due to Debug mode"
|
||||
self._slack_client.chat_postMessage(
|
||||
channel=slack_channel_id,
|
||||
text=text,
|
||||
attachments=[],
|
||||
thread_ts=alert_group.slack_message.slack_id,
|
||||
mrkdwn=True,
|
||||
blocks=[
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "mrkdwn",
|
||||
"text": text,
|
||||
},
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
if not alert_group.is_maintenance_incident and not should_skip_escalation_in_slack:
|
||||
send_message_to_thread_if_bot_not_in_channel.apply_async(
|
||||
(alert_group_pk, slack_team_identity_pk, slack_channel_id),
|
||||
countdown=1, # delay for message so that the log report is published first
|
||||
)
|
||||
|
||||
if alert.group.is_maintenance_incident:
|
||||
# not sending log report message for maintenance incident
|
||||
pass
|
||||
else:
|
||||
# check if alert group was posted to slack before posting message to thread
|
||||
if not alert.group.skip_escalation_in_slack:
|
||||
self._send_message_to_thread_if_bot_not_in_channel(alert.group, slack_channel)
|
||||
else:
|
||||
# check if alert group was posted to slack before updating its message
|
||||
if not alert.group.skip_escalation_in_slack:
|
||||
# if a new alert comes in, and is grouped to an alert group that has already been posted to Slack,
|
||||
# then we will update that existing Slack message
|
||||
if not should_skip_escalation_in_slack:
|
||||
update_task_id = update_incident_slack_message.apply_async(
|
||||
(self.slack_team_identity.pk, alert.group.pk),
|
||||
(self.slack_team_identity.pk, alert_group_pk),
|
||||
countdown=10,
|
||||
)
|
||||
cache.set(
|
||||
get_cache_key_update_incident_slack_message(alert.group.pk),
|
||||
get_cache_key_update_incident_slack_message(alert_group_pk),
|
||||
update_task_id,
|
||||
timeout=CACHE_UPDATE_INCIDENT_SLACK_MESSAGE_LIFETIME,
|
||||
)
|
||||
else:
|
||||
logger.info("Skip updating alert_group in Slack due to rate limit")
|
||||
|
||||
def _send_first_alert(self, alert: Alert, slack_channel: typing.Optional[SlackChannel]) -> None:
|
||||
self._post_alert_group_to_slack(
|
||||
slack_team_identity=self.slack_team_identity,
|
||||
alert_group=alert.group,
|
||||
alert=alert,
|
||||
attachments=alert.group.render_slack_attachments(),
|
||||
slack_channel=slack_channel,
|
||||
blocks=alert.group.render_slack_blocks(),
|
||||
)
|
||||
|
||||
def _post_alert_group_to_slack(
|
||||
self,
|
||||
slack_team_identity: SlackTeamIdentity,
|
||||
|
|
@ -182,31 +260,6 @@ class AlertShootingStep(scenario_step.ScenarioStep):
|
|||
finally:
|
||||
alert.save()
|
||||
|
||||
def _send_debug_mode_notice(self, alert_group: AlertGroup, slack_channel: SlackChannel) -> None:
|
||||
blocks: Block.AnyBlocks = []
|
||||
|
||||
text = "Escalations are silenced due to Debug mode"
|
||||
blocks.append({"type": "section", "text": {"type": "mrkdwn", "text": text}})
|
||||
|
||||
self._slack_client.chat_postMessage(
|
||||
channel=slack_channel.slack_id,
|
||||
text=text,
|
||||
attachments=[],
|
||||
thread_ts=alert_group.slack_message.slack_id,
|
||||
mrkdwn=True,
|
||||
blocks=blocks,
|
||||
)
|
||||
|
||||
def _send_message_to_thread_if_bot_not_in_channel(
|
||||
self,
|
||||
alert_group: AlertGroup,
|
||||
slack_channel: SlackChannel,
|
||||
) -> None:
|
||||
send_message_to_thread_if_bot_not_in_channel.apply_async(
|
||||
(alert_group.pk, self.slack_team_identity.pk, slack_channel.slack_id),
|
||||
countdown=1, # delay for message so that the log report is published first
|
||||
)
|
||||
|
||||
def process_scenario(
|
||||
self,
|
||||
slack_user_identity: SlackUserIdentity,
|
||||
|
|
@ -472,8 +525,11 @@ class AttachGroupStep(AlertGroupActionsMixin, scenario_step.ScenarioStep):
|
|||
|
||||
if slack_user_identity:
|
||||
self._slack_client.chat_postEphemeral(
|
||||
# TODO: once _channel_id has been fully migrated to channel, remove _channel_id
|
||||
# see https://raintank-corp.slack.com/archives/C06K1MQ07GS/p173255546
|
||||
# channel=alert_group.slack_message.channel.slack_id,
|
||||
channel=alert_group.slack_message._channel_id,
|
||||
user=slack_user_identity.slack_id,
|
||||
channel=alert_group.slack_message.channel.slack_id,
|
||||
text="{}{}".format(ephemeral_text[:1].upper(), ephemeral_text[1:]),
|
||||
unfurl_links=True,
|
||||
)
|
||||
|
|
@ -714,7 +770,10 @@ class UnAcknowledgeGroupStep(AlertGroupActionsMixin, scenario_step.ScenarioStep)
|
|||
if slack_message.ack_reminder_message_ts:
|
||||
try:
|
||||
self._slack_client.chat_update(
|
||||
channel=slack_message.channel.slack_id,
|
||||
# TODO: once _channel_id has been fully migrated to channel, remove _channel_id
|
||||
# see https://raintank-corp.slack.com/archives/C06K1MQ07GS/p173255546
|
||||
# channel=slack_message.channel.slack_id,
|
||||
channel=slack_message._channel_id,
|
||||
ts=slack_message.ack_reminder_message_ts,
|
||||
text=text,
|
||||
attachments=message_attachments,
|
||||
|
|
@ -788,12 +847,14 @@ class AcknowledgeConfirmationStep(AcknowledgeGroupStep):
|
|||
from apps.user_management.models import Organization
|
||||
|
||||
alert_group = log_record.alert_group
|
||||
slack_channel = alert_group.slack_message.channel
|
||||
organization = alert_group.channel.organization
|
||||
slack_message = alert_group.slack_message
|
||||
slack_channel = slack_message.channel
|
||||
|
||||
user_verbal = log_record.author.get_username_with_slack_verbal(mention=True)
|
||||
text = f"{user_verbal}, please confirm that you're still working on this Alert Group."
|
||||
|
||||
if alert_group.channel.organization.unacknowledge_timeout != Organization.UNACKNOWLEDGE_TIMEOUT_NEVER:
|
||||
if organization.unacknowledge_timeout != Organization.UNACKNOWLEDGE_TIMEOUT_NEVER:
|
||||
try:
|
||||
response = self._slack_client.chat_postMessage(
|
||||
channel=slack_channel.slack_id,
|
||||
|
|
@ -815,14 +876,12 @@ class AcknowledgeConfirmationStep(AcknowledgeGroupStep):
|
|||
"text": "Confirm",
|
||||
"type": "button",
|
||||
"style": "primary",
|
||||
"value": make_value(
|
||||
{"alert_group_pk": alert_group.pk}, alert_group.channel.organization
|
||||
),
|
||||
"value": make_value({"alert_group_pk": alert_group.pk}, organization),
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
thread_ts=alert_group.slack_message.slack_id,
|
||||
thread_ts=slack_message.slack_id,
|
||||
)
|
||||
except (SlackAPITokenError, SlackAPIChannelArchivedError, SlackAPIChannelNotFoundError):
|
||||
pass
|
||||
|
|
@ -831,13 +890,13 @@ class AcknowledgeConfirmationStep(AcknowledgeGroupStep):
|
|||
# see https://raintank-corp.slack.com/archives/C06K1MQ07GS/p1732555465144099
|
||||
alert_group.slack_messages.create(
|
||||
slack_id=response["ts"],
|
||||
organization=alert_group.channel.organization,
|
||||
organization=organization,
|
||||
_channel_id=slack_channel.slack_id,
|
||||
channel=slack_channel,
|
||||
)
|
||||
|
||||
alert_group.slack_message.ack_reminder_message_ts = response["ts"]
|
||||
alert_group.slack_message.save(update_fields=["ack_reminder_message_ts"])
|
||||
slack_message.ack_reminder_message_ts = response["ts"]
|
||||
slack_message.save(update_fields=["ack_reminder_message_ts"])
|
||||
else:
|
||||
text = f"This is a reminder that the Alert Group is still acknowledged by {user_verbal}"
|
||||
self.alert_group_slack_service.publish_message_to_alert_group_thread(alert_group, text=text)
|
||||
|
|
@ -846,9 +905,12 @@ class AcknowledgeConfirmationStep(AcknowledgeGroupStep):
|
|||
class WipeGroupStep(scenario_step.ScenarioStep):
|
||||
def process_signal(self, log_record: AlertGroupLogRecord) -> None:
|
||||
alert_group = log_record.alert_group
|
||||
user_verbal = log_record.author.get_username_with_slack_verbal()
|
||||
text = f"Wiped by {user_verbal}"
|
||||
self.alert_group_slack_service.publish_message_to_alert_group_thread(alert_group, text=text)
|
||||
|
||||
self.alert_group_slack_service.publish_message_to_alert_group_thread(
|
||||
alert_group,
|
||||
text=f"Wiped by {log_record.author.get_username_with_slack_verbal()}",
|
||||
)
|
||||
|
||||
self.alert_group_slack_service.update_alert_group_slack_message(alert_group)
|
||||
|
||||
|
||||
|
|
@ -859,7 +921,9 @@ class DeleteGroupStep(scenario_step.ScenarioStep):
|
|||
# Remove "memo" emoji from resolution note messages
|
||||
for message in alert_group.resolution_note_slack_messages.filter(added_to_resolution_note=True):
|
||||
try:
|
||||
self._slack_client.reactions_remove(channel=message.slack_channel_id, name="memo", timestamp=message.ts)
|
||||
self._slack_client.reactions_remove(
|
||||
channel=message.slack_channel_slack_id, name="memo", timestamp=message.ts
|
||||
)
|
||||
except SlackAPIRatelimitError:
|
||||
# retries on ratelimit are handled in apps.alerts.tasks.delete_alert_group.delete_alert_group
|
||||
raise
|
||||
|
|
@ -870,7 +934,7 @@ class DeleteGroupStep(scenario_step.ScenarioStep):
|
|||
# Remove resolution note messages posted by OnCall bot
|
||||
for message in alert_group.resolution_note_slack_messages.filter(posted_by_bot=True):
|
||||
try:
|
||||
self._slack_client.chat_delete(channel=message.slack_channel_id, ts=message.ts)
|
||||
self._slack_client.chat_delete(channel=message.slack_channel_slack_id, ts=message.ts)
|
||||
except SlackAPIRatelimitError:
|
||||
# retries on ratelimit are handled in apps.alerts.tasks.delete_alert_group.delete_alert_group
|
||||
raise
|
||||
|
|
@ -881,7 +945,13 @@ class DeleteGroupStep(scenario_step.ScenarioStep):
|
|||
# Remove alert group Slack messages
|
||||
for message in alert_group.slack_messages.all():
|
||||
try:
|
||||
self._slack_client.chat_delete(channel=message.channel.slack_id, ts=message.slack_id)
|
||||
self._slack_client.chat_delete(
|
||||
# TODO: once _channel_id has been fully migrated to channel, remove _channel_id
|
||||
# see https://raintank-corp.slack.com/archives/C06K1MQ07GS/p173255546
|
||||
# channel=message.channel.slack_id,
|
||||
channel=message._channel_id,
|
||||
ts=message.slack_id,
|
||||
)
|
||||
except SlackAPIRatelimitError:
|
||||
# retries on ratelimit are handled in apps.alerts.tasks.delete_alert_group.delete_alert_group
|
||||
raise
|
||||
|
|
|
|||
|
|
@ -18,7 +18,11 @@ class NotificationDeliveryStep(scenario_step.ScenarioStep):
|
|||
|
||||
user = log_record.author
|
||||
alert_group = log_record.alert_group
|
||||
slack_channel_id = alert_group.slack_message.channel.slack_id
|
||||
|
||||
# TODO: once _channel_id has been fully migrated to channel, remove _channel_id
|
||||
# see https://raintank-corp.slack.com/archives/C06K1MQ07GS/p173255546
|
||||
# slack_channel_id = alert_group_slack_message.channel.slack_id
|
||||
slack_channel_id = alert_group.slack_message._channel_id
|
||||
|
||||
user_verbal_with_mention = user.get_username_with_slack_verbal(mention=True)
|
||||
|
||||
|
|
|
|||
|
|
@ -236,9 +236,7 @@ class UpdateResolutionNoteStep(scenario_step.ScenarioStep):
|
|||
else:
|
||||
self.post_or_update_resolution_note_in_thread(resolution_note)
|
||||
|
||||
self.update_alert_group_resolution_note_button(
|
||||
alert_group=alert_group,
|
||||
)
|
||||
self.update_alert_group_resolution_note_button(alert_group)
|
||||
|
||||
def remove_resolution_note_slack_message(self, resolution_note: "ResolutionNote") -> None:
|
||||
if (resolution_note_slack_message := resolution_note.resolution_note_slack_message) is not None:
|
||||
|
|
@ -263,9 +261,13 @@ class UpdateResolutionNoteStep(scenario_step.ScenarioStep):
|
|||
resolution_note_slack_message = resolution_note.resolution_note_slack_message
|
||||
alert_group = resolution_note.alert_group
|
||||
alert_group_slack_message = alert_group.slack_message
|
||||
slack_channel_id = alert_group_slack_message.channel.slack_id
|
||||
blocks = self.get_resolution_note_blocks(resolution_note)
|
||||
|
||||
# TODO: once _channel_id has been fully migrated to channel, remove _channel_id
|
||||
# see https://raintank-corp.slack.com/archives/C06K1MQ07GS/p173255546
|
||||
# slack_channel_id = alert_group_slack_message.channel.slack_id
|
||||
slack_channel_id = alert_group_slack_message._channel_id
|
||||
|
||||
slack_channel = SlackChannel.objects.get(
|
||||
slack_id=slack_channel_id, slack_team_identity=self.slack_team_identity
|
||||
)
|
||||
|
|
@ -655,11 +657,6 @@ class ResolutionNoteModalStep(AlertGroupActionsMixin, scenario_step.ScenarioStep
|
|||
]
|
||||
|
||||
|
||||
class ReadEditPostmortemStep(ResolutionNoteModalStep):
|
||||
# Left for backward compatibility with slack messages created before postmortems -> resolution note change
|
||||
pass
|
||||
|
||||
|
||||
class AddRemoveThreadMessageStep(UpdateResolutionNoteStep, scenario_step.ScenarioStep):
|
||||
def process_scenario(
|
||||
self,
|
||||
|
|
@ -687,6 +684,7 @@ class AddRemoveThreadMessageStep(UpdateResolutionNoteStep, scenario_step.Scenari
|
|||
if add_to_resolution_note and slack_thread_message is not None:
|
||||
slack_thread_message.added_to_resolution_note = True
|
||||
slack_thread_message.save(update_fields=["added_to_resolution_note"])
|
||||
|
||||
if resolution_note is None:
|
||||
ResolutionNote(
|
||||
alert_group=alert_group,
|
||||
|
|
@ -696,6 +694,7 @@ class AddRemoveThreadMessageStep(UpdateResolutionNoteStep, scenario_step.Scenari
|
|||
).save()
|
||||
else:
|
||||
resolution_note.recreate()
|
||||
|
||||
self.add_resolution_note_reaction(slack_thread_message)
|
||||
elif not add_to_resolution_note:
|
||||
# Check if resolution_note can be removed
|
||||
|
|
@ -716,14 +715,16 @@ class AddRemoveThreadMessageStep(UpdateResolutionNoteStep, scenario_step.Scenari
|
|||
else:
|
||||
if resolution_note_pk is not None and resolution_note is None: # old version of step
|
||||
resolution_note = ResolutionNote.objects.get(pk=resolution_note_pk)
|
||||
|
||||
resolution_note.delete()
|
||||
|
||||
if slack_thread_message:
|
||||
slack_thread_message.added_to_resolution_note = False
|
||||
slack_thread_message.save(update_fields=["added_to_resolution_note"])
|
||||
self.remove_resolution_note_reaction(slack_thread_message)
|
||||
self.update_alert_group_resolution_note_button(
|
||||
alert_group,
|
||||
)
|
||||
|
||||
self.update_alert_group_resolution_note_button(alert_group)
|
||||
|
||||
resolution_note_data = json.loads(payload["actions"][0]["value"])
|
||||
resolution_note_data["resolution_note_window_action"] = "edit_update"
|
||||
ResolutionNoteModalStep(slack_team_identity, self.organization, self.user).process_scenario(
|
||||
|
|
@ -735,12 +736,6 @@ class AddRemoveThreadMessageStep(UpdateResolutionNoteStep, scenario_step.Scenari
|
|||
|
||||
|
||||
STEPS_ROUTING: ScenarioRoute.RoutingSteps = [
|
||||
{
|
||||
"payload_type": PayloadType.BLOCK_ACTIONS,
|
||||
"block_action_type": BlockActionType.BUTTON,
|
||||
"block_action_id": ReadEditPostmortemStep.routing_uid(),
|
||||
"step": ReadEditPostmortemStep,
|
||||
},
|
||||
{
|
||||
"payload_type": PayloadType.BLOCK_ACTIONS,
|
||||
"block_action_type": BlockActionType.BUTTON,
|
||||
|
|
|
|||
|
|
@ -186,8 +186,11 @@ class BaseShiftSwapRequestStep(scenario_step.ScenarioStep):
|
|||
if not shift_swap_request.slack_message:
|
||||
return
|
||||
|
||||
# TODO: once _channel_id has been fully migrated to channel, remove _channel_id
|
||||
# see https://raintank-corp.slack.com/archives/C06K1MQ07GS/p173255546
|
||||
self._slack_client.chat_postMessage(
|
||||
channel=shift_swap_request.slack_message.channel.slack_id,
|
||||
# channel=shift_swap_request.slack_message.channel.slack_id,
|
||||
channel=shift_swap_request.slack_message._channel_id,
|
||||
thread_ts=shift_swap_request.slack_message.slack_id,
|
||||
reply_broadcast=reply_broadcast,
|
||||
blocks=blocks,
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ from django.conf import settings
|
|||
from django.core.cache import cache
|
||||
from django.utils import timezone
|
||||
|
||||
from apps.alerts.tasks.compare_escalations import compare_escalations
|
||||
from apps.slack.alert_group_slack_service import AlertGroupSlackService
|
||||
from apps.slack.client import SlackClient
|
||||
from apps.slack.constants import CACHE_UPDATE_INCIDENT_SLACK_MESSAGE_LIFETIME, SLACK_BOT_ID
|
||||
|
|
@ -296,7 +295,7 @@ def post_slack_rate_limit_message(integration_id):
|
|||
logger.warning(f"AlertReceiveChannel {integration_id} doesn't exist")
|
||||
return
|
||||
|
||||
if not compare_escalations(post_slack_rate_limit_message.request.id, integration.rate_limit_message_task_id):
|
||||
if post_slack_rate_limit_message.request.id != integration.rate_limit_message_task_id:
|
||||
logger.info(
|
||||
f"post_slack_rate_limit_message. integration {integration_id}. ID mismatch. "
|
||||
f"Active: {integration.rate_limit_message_task_id}"
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import pytest
|
|||
from apps.alerts.models import AlertGroup
|
||||
from apps.slack.errors import get_error_class
|
||||
from apps.slack.models import SlackMessage
|
||||
from apps.slack.scenarios.distribute_alerts import AlertShootingStep
|
||||
from apps.slack.scenarios.distribute_alerts import IncomingAlertStep
|
||||
from apps.slack.scenarios.scenario_step import ScenarioStep
|
||||
from apps.slack.tests.conftest import build_slack_response
|
||||
|
||||
|
|
@ -28,7 +28,7 @@ def test_skip_escalations_error(
|
|||
reason,
|
||||
slack_error,
|
||||
):
|
||||
SlackAlertShootingStep = ScenarioStep.get_step("distribute_alerts", "AlertShootingStep")
|
||||
SlackIncomingAlertStep = ScenarioStep.get_step("distribute_alerts", "IncomingAlertStep")
|
||||
organization, _, slack_team_identity, _ = make_organization_and_user_with_slack_identities()
|
||||
alert_receive_channel = make_alert_receive_channel(organization)
|
||||
alert_group = make_alert_group(alert_receive_channel)
|
||||
|
|
@ -36,7 +36,7 @@ def test_skip_escalations_error(
|
|||
|
||||
slack_channel = make_slack_channel(slack_team_identity)
|
||||
|
||||
step = SlackAlertShootingStep(slack_team_identity)
|
||||
step = SlackIncomingAlertStep(slack_team_identity)
|
||||
|
||||
with patch.object(step._slack_client, "api_call") as mock_slack_api_call:
|
||||
error_response = build_slack_response({"error": slack_error})
|
||||
|
|
@ -66,7 +66,7 @@ def test_timeout_error(
|
|||
make_alert_group,
|
||||
make_alert,
|
||||
):
|
||||
SlackAlertShootingStep = ScenarioStep.get_step("distribute_alerts", "AlertShootingStep")
|
||||
SlackIncomingAlertStep = ScenarioStep.get_step("distribute_alerts", "IncomingAlertStep")
|
||||
slack_team_identity = make_slack_team_identity()
|
||||
slack_channel = make_slack_channel(slack_team_identity)
|
||||
organization = make_organization(slack_team_identity=slack_team_identity, default_slack_channel=slack_channel)
|
||||
|
|
@ -74,7 +74,7 @@ def test_timeout_error(
|
|||
alert_group = make_alert_group(alert_receive_channel)
|
||||
alert = make_alert(alert_group, raw_request_data="{}")
|
||||
|
||||
step = SlackAlertShootingStep(slack_team_identity)
|
||||
step = SlackIncomingAlertStep(slack_team_identity)
|
||||
|
||||
with pytest.raises(TimeoutError):
|
||||
with patch.object(step._slack_client, "api_call") as mock_slack_api_call:
|
||||
|
|
@ -89,9 +89,9 @@ def test_timeout_error(
|
|||
assert not alert.delivered
|
||||
|
||||
|
||||
@patch.object(AlertShootingStep, "_post_alert_group_to_slack")
|
||||
@patch.object(IncomingAlertStep, "_post_alert_group_to_slack")
|
||||
@pytest.mark.django_db
|
||||
def test_alert_shooting_no_channel_filter(
|
||||
def test_incoming_alert_no_channel_filter(
|
||||
mock_post_alert_group_to_slack,
|
||||
make_slack_team_identity,
|
||||
make_slack_channel,
|
||||
|
|
@ -109,7 +109,7 @@ def test_alert_shooting_no_channel_filter(
|
|||
alert_group = make_alert_group(alert_receive_channel, channel_filter=None)
|
||||
alert = make_alert(alert_group, raw_request_data={})
|
||||
|
||||
step = AlertShootingStep(slack_team_identity, organization)
|
||||
step = IncomingAlertStep(slack_team_identity, organization)
|
||||
step.process_signal(alert)
|
||||
|
||||
assert mock_post_alert_group_to_slack.call_args[1]["slack_channel"] == slack_channel
|
||||
|
|
|
|||
|
|
@ -347,17 +347,18 @@ def test_resolution_notes_modal_closed_before_update(
|
|||
assert call_args[0] == "views.update"
|
||||
|
||||
|
||||
@patch.object(SlackClient, "reactions_add")
|
||||
@patch.object(SlackClient, "chat_getPermalink", return_value={"permalink": "https://example.com"})
|
||||
@pytest.mark.django_db
|
||||
def test_add_to_resolution_note(
|
||||
_,
|
||||
_mock_chat_getPermalink,
|
||||
mock_reactions_add,
|
||||
make_organization_and_user_with_slack_identities,
|
||||
make_alert_receive_channel,
|
||||
make_alert_group,
|
||||
make_alert,
|
||||
make_slack_message,
|
||||
make_slack_channel,
|
||||
settings,
|
||||
):
|
||||
organization, user, slack_team_identity, slack_user_identity = make_organization_and_user_with_slack_identities()
|
||||
alert_receive_channel = make_alert_receive_channel(organization)
|
||||
|
|
@ -382,8 +383,7 @@ def test_add_to_resolution_note(
|
|||
|
||||
AddToResolutionNoteStep = ScenarioStep.get_step("resolution_note", "AddToResolutionNoteStep")
|
||||
step = AddToResolutionNoteStep(organization=organization, user=user, slack_team_identity=slack_team_identity)
|
||||
with patch.object(SlackClient, "reactions_add") as mock_reactions_add:
|
||||
step.process_scenario(slack_user_identity, slack_team_identity, payload)
|
||||
step.process_scenario(slack_user_identity, slack_team_identity, payload)
|
||||
|
||||
mock_reactions_add.assert_called_once()
|
||||
assert alert_group.resolution_notes.get().text == "Test resolution note"
|
||||
|
|
|
|||
|
|
@ -386,12 +386,7 @@ class TestSlackChannelMessageEventStep:
|
|||
def test_delete_thread_message_from_resolution_note_no_slack_user_identity(
|
||||
self, MockResolutionNoteSlackMessage, make_organization_and_user_with_slack_identities
|
||||
) -> None:
|
||||
(
|
||||
organization,
|
||||
user,
|
||||
slack_team_identity,
|
||||
slack_user_identity,
|
||||
) = make_organization_and_user_with_slack_identities()
|
||||
organization, user, slack_team_identity, _ = make_organization_and_user_with_slack_identities()
|
||||
|
||||
step = SlackChannelMessageEventStep(slack_team_identity, organization, user)
|
||||
step.delete_thread_message_from_resolution_note(None, {})
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ from apps.alerts.models import (
|
|||
Alert,
|
||||
AlertGroupLogRecord,
|
||||
AlertReceiveChannel,
|
||||
MaintainableObject,
|
||||
ResolutionNote,
|
||||
listen_for_alertgrouplogrecord,
|
||||
listen_for_alertreceivechannel_model_save,
|
||||
|
|
@ -825,14 +824,6 @@ def make_slack_channel():
|
|||
return _make_slack_channel
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def mock_start_disable_maintenance_task(monkeypatch):
|
||||
def mocked_start_disable_maintenance_task(*args, **kwargs):
|
||||
return uuid.uuid4()
|
||||
|
||||
monkeypatch.setattr(MaintainableObject, "start_disable_maintenance_task", mocked_start_disable_maintenance_task)
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def make_organization_and_user_with_plugin_token(make_organization_and_user, make_token_for_organization):
|
||||
def _make_organization_and_user_with_plugin_token(role: typing.Optional[LegacyAccessControlRole] = None):
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ django-cors-headers==3.7.0
|
|||
# pyroscope-io==0.8.1
|
||||
django-dbconn-retry==0.1.7
|
||||
django-debug-toolbar==4.1
|
||||
django-deprecate-fields==0.1.1
|
||||
django-filter==2.4.0
|
||||
django-ipware==4.0.2
|
||||
django-log-request-id==1.6.0
|
||||
|
|
|
|||
|
|
@ -82,7 +82,6 @@ django==4.2.16
|
|||
# django-anymail
|
||||
# django-cors-headers
|
||||
# django-debug-toolbar
|
||||
# django-deprecate-fields
|
||||
# django-filter
|
||||
# django-log-request-id
|
||||
# django-migration-linter
|
||||
|
|
@ -106,8 +105,6 @@ django-dbconn-retry==0.1.7
|
|||
# via -r requirements.in
|
||||
django-debug-toolbar==4.1.0
|
||||
# via -r requirements.in
|
||||
django-deprecate-fields==0.1.1
|
||||
# via -r requirements.in
|
||||
django-filter==2.4.0
|
||||
# via -r requirements.in
|
||||
django-ipware==4.0.2
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue