Merge pull request #4330 from grafana/dev

v1.4.6
This commit is contained in:
Matias Bordese 2024-05-09 14:14:14 -03:00 committed by GitHub
commit f2be57169b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 107 additions and 39 deletions

View file

@ -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()

View file

@ -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", [])]

View file

@ -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"]

View file

@ -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):

View file

@ -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

View file

@ -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",)

View file

@ -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

View file

@ -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:

View file

@ -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)

View file

@ -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),

View file

@ -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

View file

@ -58,6 +58,7 @@ module = [
"fcm_django.*",
"firebase_admin.*",
"googleapiclient.discovery.*",
"googleapiclient.errors.*",
"google.oauth2.credentials.*",
"httpretty.*",
"humanize.*",

View file

@ -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]) {