diff --git a/engine/apps/alerts/models/alert_group.py b/engine/apps/alerts/models/alert_group.py index d93f4f72..8eea0c0e 100644 --- a/engine/apps/alerts/models/alert_group.py +++ b/engine/apps/alerts/models/alert_group.py @@ -511,17 +511,8 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models. @property def telegram_permalink(self) -> typing.Optional[str]: - """ - This property will attempt to access an attribute, `prefetched_telegram_messages`, representing a list of - prefetched telegram messages. If this attribute does not exist, it falls back to performing a query. - - See `apps.public_api.serializers.incidents.IncidentSerializer.PREFETCH_RELATED` as an example. - """ from apps.telegram.models.message import TelegramMessage - if hasattr(self, "prefetched_telegram_messages"): - return self.prefetched_telegram_messages[0].link if self.prefetched_telegram_messages else None - main_telegram_message = self.telegram_messages.filter( chat_id__startswith="-", message_type=TelegramMessage.ALERT_GROUP_MESSAGE ).first() diff --git a/engine/apps/google/client.py b/engine/apps/google/client.py index 906194ca..a0544ab3 100644 --- a/engine/apps/google/client.py +++ b/engine/apps/google/client.py @@ -5,6 +5,7 @@ import typing from django.conf import settings from google.oauth2.credentials import Credentials from googleapiclient.discovery import build +from googleapiclient.errors import HttpError from apps.google import constants, utils from apps.google.types import GoogleCalendarEvent as GoogleCalendarEventType @@ -23,6 +24,11 @@ class GoogleCalendarEvent: self.end_time_utc = self._end_time.astimezone(datetime.timezone.utc) +class GoogleCalendarHTTPError(Exception): + def __init__(self, http_error) -> None: + self.error = http_error + + class GoogleCalendarAPIClient: MAX_NUMBER_OF_CALENDAR_EVENTS_TO_FETCH = 250 """ @@ -68,17 +74,22 @@ class GoogleCalendarAPIClient: now + datetime.timedelta(days=constants.DAYS_IN_FUTURE_TO_CONSIDER_OUT_OF_OFFICE_EVENTS) ) - events_result = ( - self.service.events() - .list( - calendarId=self.CALENDAR_ID, - timeMin=time_min, - timeMax=time_max, - maxResults=self.MAX_NUMBER_OF_CALENDAR_EVENTS_TO_FETCH, - singleEvents=True, - orderBy="startTime", - eventTypes="outOfOffice", + try: + events_result = ( + self.service.events() + .list( + calendarId=self.CALENDAR_ID, + timeMin=time_min, + timeMax=time_max, + maxResults=self.MAX_NUMBER_OF_CALENDAR_EVENTS_TO_FETCH, + singleEvents=True, + orderBy="startTime", + eventTypes="outOfOffice", + ) + .execute() ) - .execute() - ) + except HttpError as e: + logger.error(f"GoogleCalendarAPIClient - Error fetching out of office events: {e}") + raise GoogleCalendarHTTPError(e) + return [GoogleCalendarEvent(event) for event in events_result.get("items", [])] diff --git a/engine/apps/google/tasks.py b/engine/apps/google/tasks.py index b1b601d4..77b3d967 100644 --- a/engine/apps/google/tasks.py +++ b/engine/apps/google/tasks.py @@ -3,7 +3,7 @@ import logging from celery.utils.log import get_task_logger from apps.google import constants -from apps.google.client import GoogleCalendarAPIClient +from apps.google.client import GoogleCalendarAPIClient, GoogleCalendarHTTPError from apps.google.models import GoogleOAuth2User from apps.schedules.models import OnCallSchedule, ShiftSwapRequest from common.custom_celery_tasks import shared_dedicated_queue_retry_task @@ -31,7 +31,13 @@ def sync_out_of_office_calendar_events_for_user(google_oauth2_user_pk: int) -> N if oncall_schedules_to_consider_for_shift_swaps: users_schedules = users_schedules.filter(public_primary_key__in=oncall_schedules_to_consider_for_shift_swaps) - for out_of_office_event in google_api_client.fetch_out_of_office_events(): + try: + out_of_office_events = google_api_client.fetch_out_of_office_events() + except GoogleCalendarHTTPError: + logger.info(f"Failed to fetch out of office events for user {user_id}") + return + + for out_of_office_event in out_of_office_events: raw_event = out_of_office_event.raw_event event_title = raw_event["summary"] diff --git a/engine/apps/google/tests/test_sync_out_of_office_calendar_events_for_user.py b/engine/apps/google/tests/test_sync_out_of_office_calendar_events_for_user.py index d930af13..69f1020b 100644 --- a/engine/apps/google/tests/test_sync_out_of_office_calendar_events_for_user.py +++ b/engine/apps/google/tests/test_sync_out_of_office_calendar_events_for_user.py @@ -3,6 +3,7 @@ from unittest.mock import patch import pytest from django.utils import timezone +from googleapiclient.errors import HttpError from apps.google import constants, tasks from apps.schedules.models import CustomOnCallShift, OnCallScheduleWeb, ShiftSwapRequest @@ -140,6 +141,28 @@ def test_setup( return _test_setup +class MockResponse: + def __init__(self, reason=None, status=200) -> None: + self.reason = reason or "" + self.status = status + + +@patch("apps.google.client.build") +@pytest.mark.django_db +def test_sync_out_of_office_calendar_events_for_user_httperror(mock_google_api_client_build, test_setup): + mock_response = MockResponse(reason="forbidden", status=403) + mock_google_api_client_build.return_value.events.return_value.list.return_value.execute.side_effect = HttpError( + resp=mock_response, content=b"error" + ) + + google_oauth2_user, schedule = test_setup([]) + user = google_oauth2_user.user + + tasks.sync_out_of_office_calendar_events_for_user(google_oauth2_user.pk) + + assert ShiftSwapRequest.objects.filter(beneficiary=user, schedule=schedule).count() == 0 + + @patch("apps.google.client.build") @pytest.mark.django_db def test_sync_out_of_office_calendar_events_for_user_no_ooo_events(mock_google_api_client_build, test_setup): diff --git a/engine/apps/public_api/serializers/incidents.py b/engine/apps/public_api/serializers/incidents.py index 90cf4abf..1633b984 100644 --- a/engine/apps/public_api/serializers/incidents.py +++ b/engine/apps/public_api/serializers/incidents.py @@ -1,8 +1,6 @@ -from django.db.models import Prefetch from rest_framework import serializers from apps.alerts.models import AlertGroup -from apps.telegram.models.message import TelegramMessage from common.api_helpers.custom_fields import UserIdField from common.api_helpers.mixins import EagerLoadingMixin @@ -19,14 +17,6 @@ class IncidentSerializer(EagerLoadingMixin, serializers.ModelSerializer): resolved_by = UserIdField(read_only=True, source="resolved_by_user") SELECT_RELATED = ["channel", "channel_filter", "slack_message", "channel__organization"] - PREFETCH_RELATED = [ - "alerts", - Prefetch( - "telegram_messages", - TelegramMessage.objects.filter(chat_id__startswith="-", message_type=TelegramMessage.ALERT_GROUP_MESSAGE), - to_attr="prefetched_telegram_messages", - ), - ] class Meta: model = AlertGroup @@ -50,7 +40,7 @@ class IncidentSerializer(EagerLoadingMixin, serializers.ModelSerializer): return obj.web_title_cache def get_alerts_count(self, obj): - return len(obj.alerts.all()) + return obj.alerts.count() def get_state(self, obj): return obj.state diff --git a/engine/apps/slack/errors.py b/engine/apps/slack/errors.py index ac750ac6..bf7fa819 100644 --- a/engine/apps/slack/errors.py +++ b/engine/apps/slack/errors.py @@ -68,6 +68,10 @@ class SlackAPIMessageNotFoundError(SlackAPIError): errors = ("message_not_found",) +class SlackAPICantUpdateMessageError(SlackAPIError): + errors = ("cant_update_message",) + + class SlackAPIUserNotFoundError(SlackAPIError): errors = ("user_not_found",) diff --git a/engine/apps/slack/models/slack_message.py b/engine/apps/slack/models/slack_message.py index 7f068720..789886e4 100644 --- a/engine/apps/slack/models/slack_message.py +++ b/engine/apps/slack/models/slack_message.py @@ -98,7 +98,7 @@ class SlackMessage(models.Model): @property def deep_link(self) -> str: - return f"slack://channel?team={self.slack_team_identity.slack_id}&id={self.channel_id}&message={self.slack_id}" + return f"https://slack.com/app_redirect?channel={self.channel_id}&team={self.slack_team_identity.slack_id}&message={self.slack_id}" def send_slack_notification(self, user, alert_group, notification_policy): from apps.base.models import UserNotificationPolicyLogRecord diff --git a/engine/apps/slack/scenarios/distribute_alerts.py b/engine/apps/slack/scenarios/distribute_alerts.py index c125b1ba..a67c1a5f 100644 --- a/engine/apps/slack/scenarios/distribute_alerts.py +++ b/engine/apps/slack/scenarios/distribute_alerts.py @@ -13,6 +13,7 @@ from apps.alerts.models import Alert, AlertGroup, AlertGroupLogRecord, AlertRece from apps.api.permissions import RBACPermission from apps.slack.constants import CACHE_UPDATE_INCIDENT_SLACK_MESSAGE_LIFETIME from apps.slack.errors import ( + SlackAPICantUpdateMessageError, SlackAPIChannelArchivedError, SlackAPIChannelInactiveError, SlackAPIChannelNotFoundError, @@ -947,6 +948,7 @@ class UpdateLogReportMessageStep(scenario_step.ScenarioStep): SlackAPIChannelArchivedError, SlackAPIChannelInactiveError, SlackAPIInvalidAuthError, + SlackAPICantUpdateMessageError, ): pass else: diff --git a/engine/apps/slack/tests/test_scenario_steps/test_distribute_alerts.py b/engine/apps/slack/tests/test_scenario_steps/test_distribute_alerts.py index 1eba2b9a..75b32aa8 100644 --- a/engine/apps/slack/tests/test_scenario_steps/test_distribute_alerts.py +++ b/engine/apps/slack/tests/test_scenario_steps/test_distribute_alerts.py @@ -1,11 +1,12 @@ from unittest.mock import patch import pytest +from django.utils import timezone from apps.alerts.models import AlertGroup -from apps.slack.errors import SlackAPIRestrictedActionError +from apps.slack.errors import SlackAPICantUpdateMessageError, SlackAPIRestrictedActionError from apps.slack.models import SlackMessage -from apps.slack.scenarios.distribute_alerts import AlertShootingStep +from apps.slack.scenarios.distribute_alerts import AlertShootingStep, UpdateLogReportMessageStep from apps.slack.scenarios.scenario_step import ScenarioStep from apps.slack.tests.conftest import build_slack_response @@ -64,3 +65,37 @@ def test_alert_shooting_no_channel_filter( mock_post_alert_group_to_slack.assert_called_once() assert mock_post_alert_group_to_slack.call_args[1]["channel_id"] == "DEFAULT_CHANNEL_ID" + + +@pytest.mark.django_db +def test_update_log_report_cant_update( + make_slack_team_identity, + make_organization, + make_alert_receive_channel, + make_alert_group, + make_alert, + make_slack_message, +): + slack_team_identity = make_slack_team_identity() + organization = make_organization( + slack_team_identity=slack_team_identity, general_log_channel_id="DEFAULT_CHANNEL_ID" + ) + alert_receive_channel = make_alert_receive_channel(organization) + + alert_group = make_alert_group(alert_receive_channel, channel_filter=None) + # alert = make_alert(alert_group, raw_request_data={}) + log_message = make_slack_message( + alert_group=alert_group, + channel_id="RANDOM_CHANNEL_ID", + slack_id="RANDOM_MESSAGE_ID", + last_updated=timezone.now() - timezone.timedelta(minutes=5), + ) + alert_group.slack_log_message = log_message + + step = UpdateLogReportMessageStep(slack_team_identity, organization) + with patch.object(step._slack_client, "api_call") as mock_slack_api_call: + mock_slack_api_call.side_effect = SlackAPICantUpdateMessageError( + response=build_slack_response({"error": "cant_update_message"}) + ) + # not raising error, will not retry + step.update_log_message(alert_group) diff --git a/engine/apps/slack/tests/test_slack_client.py b/engine/apps/slack/tests/test_slack_client.py index 7214aa96..e5b3def4 100644 --- a/engine/apps/slack/tests/test_slack_client.py +++ b/engine/apps/slack/tests/test_slack_client.py @@ -9,6 +9,7 @@ from slack_sdk.web import SlackResponse from apps.slack.client import SlackClient, server_error_retry_handler from apps.slack.errors import ( SlackAPICannotDMBotError, + SlackAPICantUpdateMessageError, SlackAPIChannelArchivedError, SlackAPIChannelInactiveError, SlackAPIChannelNotFoundError, @@ -116,6 +117,7 @@ def test_slack_client_generic_error(mock_request, monkeypatch, make_organization [ ("account_inactive", SlackAPITokenError), ("cannot_dm_bot", SlackAPICannotDMBotError), + ("cant_update_message", SlackAPICantUpdateMessageError), ("channel_not_found", SlackAPIChannelNotFoundError), ("fatal_error", SlackAPIServerError), ("fetch_members_failed", SlackAPIFetchMembersFailedError), diff --git a/engine/apps/slack/tests/test_slack_message.py b/engine/apps/slack/tests/test_slack_message.py index d3824214..3f1d8f74 100644 --- a/engine/apps/slack/tests/test_slack_message.py +++ b/engine/apps/slack/tests/test_slack_message.py @@ -56,5 +56,8 @@ def test_slack_message_deep_link( slack_channel = make_slack_channel(slack_team_identity) slack_message = make_slack_message(alert_group=alert_group, channel_id=slack_channel.slack_id) - expected = f"slack://channel?team={slack_team_identity.slack_id}&id={slack_channel.slack_id}&message={slack_message.slack_id}" + expected = ( + f"https://slack.com/app_redirect?channel={slack_channel.slack_id}" + f"&team={slack_team_identity.slack_id}&message={slack_message.slack_id}" + ) assert slack_message.deep_link == expected diff --git a/engine/pyproject.toml b/engine/pyproject.toml index f2ea3e21..a1bab7d7 100644 --- a/engine/pyproject.toml +++ b/engine/pyproject.toml @@ -58,6 +58,7 @@ module = [ "fcm_django.*", "firebase_admin.*", "googleapiclient.discovery.*", + "googleapiclient.errors.*", "google.oauth2.credentials.*", "httpretty.*", "humanize.*", diff --git a/grafana-plugin/src/models/filters/filters.helpers.ts b/grafana-plugin/src/models/filters/filters.helpers.ts index ba4a0372..531055a6 100644 --- a/grafana-plugin/src/models/filters/filters.helpers.ts +++ b/grafana-plugin/src/models/filters/filters.helpers.ts @@ -12,7 +12,7 @@ export const getApiPathByPage = (page: string) => { ); }; -export const convertFiltersToBackendFormat = (filters: FiltersValues, filterOptions: FilterOption[]) => { +export const convertFiltersToBackendFormat = (filters: FiltersValues = {}, filterOptions: FilterOption[] = []) => { const newFilters = { ...filters }; filterOptions.forEach((filterOption) => { if (filterOption.type === 'daterange' && newFilters[filterOption.name]) {