This commit is contained in:
Joey Orlando 2023-12-11 14:26:00 -05:00 committed by GitHub
commit 1c710bd9dd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 224 additions and 27 deletions

View file

@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased
## v1.3.77 (2023-12-11)
### Fixed
- Fix schedules invalid dates issue ([#support-escalations/issues/8084](https://github.com/grafana/support-escalations/issues/8084))
- Fix issue related to updating alert group metrics when deleting an alert group via the public API by @joeyorlando ([#3544](https://github.com/grafana/oncall/pull/3544))
- Fix issue with `amazon_ses` inbound email ESP provider by @Lutseslav ([#3509](https://github.com/grafana/oncall/pull/3509))
## v1.3.76 (2023-12-11)
### Fixed

View file

@ -102,7 +102,11 @@ k8s_resource(
labels=["OnCallBackend"],
)
k8s_resource(workload="redis-master", labels=["OnCallDeps"])
k8s_resource(workload="mariadb", labels=["OnCallDeps"])
k8s_resource(
workload="mariadb",
port_forwards='3307:3306', # <host_port>:<container_port>
labels=["OnCallDeps"],
)
# name all tilt resources after the k8s object namespace + name

View file

@ -1191,8 +1191,6 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models.
def delete_by_user(self, user: User):
from apps.alerts.models import AlertGroupLogRecord
initial_state = self.state
self.stop_escalation()
# prevent creating multiple logs
# filter instead of get_or_create cause it can be multiple logs of this type due deleting error
@ -1222,10 +1220,9 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models.
dependent_alerts = list(self.dependent_alert_groups.all())
self.hard_delete()
# Update alert group state metric cache
self._update_metrics(organization_id=user.organization_id, previous_state=initial_state, state=None)
for dependent_alert_group in dependent_alerts: # unattach dependent incidents
# unattach dependent incidents
for dependent_alert_group in dependent_alerts:
dependent_alert_group.un_attach_by_delete()
def hard_delete(self):

View file

@ -24,6 +24,8 @@ def delete_alert_group(alert_group_pk, user_pk):
logger.debug("User not found, skipping delete_alert_group")
return
logger.debug(f"User {user} is deleting alert group {alert_group} (channel: {alert_group.channel})")
try:
alert_group.delete_by_user(user)
except SlackAPIRatelimitError as e:

View file

@ -577,3 +577,47 @@ def test_filter_active_alert_groups(
assert active_alert_groups.count() == 2
assert alert_group_active in active_alert_groups
assert alert_group_active_silenced in active_alert_groups
@patch("apps.alerts.models.AlertGroup.hard_delete")
@patch("apps.alerts.models.AlertGroup.un_attach_by_delete")
@patch("apps.alerts.models.AlertGroup.stop_escalation")
@patch("apps.alerts.models.alert_group.alert_group_action_triggered_signal")
@pytest.mark.django_db
def test_delete_by_user(
mock_alert_group_action_triggered_signal,
_mock_stop_escalation,
_mock_un_attach_by_delete,
_mock_hard_delete,
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)
alert_group = make_alert_group(alert_receive_channel)
# make a few dependent alert groups
dependent_alert_groups = [make_alert_group(alert_receive_channel, root_alert_group=alert_group) for _ in range(3)]
assert alert_group.log_records.filter(type=AlertGroupLogRecord.TYPE_DELETED).count() == 0
alert_group.delete_by_user(user)
assert alert_group.log_records.filter(type=AlertGroupLogRecord.TYPE_DELETED).count() == 1
deleted_log_record = alert_group.log_records.get(type=AlertGroupLogRecord.TYPE_DELETED)
alert_group.stop_escalation.assert_called_once_with()
mock_alert_group_action_triggered_signal.send.assert_called_once_with(
sender=alert_group.delete_by_user,
log_record=deleted_log_record.pk,
action_source=None,
force_sync=True,
)
alert_group.hard_delete.assert_called_once_with()
for dependent_alert_group in dependent_alert_groups:
dependent_alert_group.un_attach_by_delete.assert_called_with()

View file

@ -0,0 +1,54 @@
import json
import pytest
from django.urls import reverse
from rest_framework import status
from rest_framework.test import APIClient
@pytest.mark.django_db
def test_amazon_ses_provider_load(settings, make_organization_and_user_with_token, make_alert_receive_channel):
settings.INBOUND_EMAIL_ESP = "amazon_ses"
settings.INBOUND_EMAIL_DOMAIN = "example.com"
dummy_channel_token = "dummy-channel-token"
organization, _, token = make_organization_and_user_with_token()
_ = make_alert_receive_channel(organization, token=dummy_channel_token)
recipient = f"{dummy_channel_token}@example.com"
mime = f"""From: sender@example.com
Subject: Dummy email message
To: {recipient}
Content-Type: text/plain
Hello!
"""
message = {
"notificationType": "Received",
"receipt": {"action": {"type": "SNS"}, "recipients": [recipient]},
"content": mime,
}
dummy_sns_message_id = "22b80b92-fdea-4c2c-8f9d-bdfb0c7bf324"
dummy_sns_payload = {
"Type": "Notification",
"MessageId": dummy_sns_message_id,
"TopicArn": "arn:aws:sns:us-east-1:123456789012:MyTopic",
"Subject": "My First Message",
"Message": json.dumps(message),
}
client = APIClient()
response = client.post(
reverse("integrations:inbound_email_webhook"),
data=json.dumps(dummy_sns_payload),
content_type="application/json",
HTTP_AUTHORIZATION=token,
HTTP_X_AMZ_SNS_MESSAGE_TYPE="Notification",
HTTP_X_AMZ_SNS_MESSAGE_ID=dummy_sns_message_id,
)
assert response.status_code == status.HTTP_200_OK

View file

@ -167,6 +167,13 @@ class TelegramToUserConnector(models.Model):
notification_policy,
UserNotificationPolicyLogRecord.ERROR_NOTIFICATION_TELEGRAM_TOKEN_ERROR,
)
elif e.message == "Forbidden: user is deactivated":
TelegramToUserConnector.create_telegram_notification_error(
alert_group,
self.user,
notification_policy,
UserNotificationPolicyLogRecord.ERROR_NOTIFICATION_TELEGRAM_USER_IS_DEACTIVATED,
)
else:
raise e

View file

@ -226,8 +226,19 @@ def on_alert_group_action_triggered_async(log_record_id):
from .alert_group_representative import AlertGroupTelegramRepresentative
logger.info(f"AlertGroupTelegramRepresentative ACTION SIGNAL, log record {log_record_id}")
log_record = AlertGroupLogRecord.objects.get(pk=log_record_id)
# temporary solution to handle cases when alert group and related log records were deleted
try:
log_record = AlertGroupLogRecord.objects.get(pk=log_record_id)
except AlertGroupLogRecord.DoesNotExist as e:
retries_count = on_alert_group_action_triggered_async.request.retries
if retries_count >= 10:
logger.error(
f"AlertGroupTelegramRepresentative: was not able to get AlertGroupLogRecord, probably alert group "
f"was deleted. log record {log_record_id}, retries: {retries_count}"
)
return
else:
raise e
instance = AlertGroupTelegramRepresentative(log_record)
if instance.is_applicable():

View file

@ -3,7 +3,7 @@ from unittest.mock import patch
import pytest
from telegram import error
from apps.base.models import UserNotificationPolicy
from apps.base.models import UserNotificationPolicy, UserNotificationPolicyLogRecord
from apps.telegram.client import TelegramClient
from apps.telegram.models import TelegramMessage
@ -47,3 +47,52 @@ def test_personal_connector_replied_message_not_found(
text="One more notification about this 👆",
reply_to_message_id=telegram_message.message_id,
)
@pytest.mark.parametrize(
"side_effect,notification_error_code",
[
(
error.Unauthorized("Forbidden: bot was blocked by the user"),
UserNotificationPolicyLogRecord.ERROR_NOTIFICATION_TELEGRAM_BOT_IS_DELETED,
),
(error.Unauthorized("Invalid token"), UserNotificationPolicyLogRecord.ERROR_NOTIFICATION_TELEGRAM_TOKEN_ERROR),
(
error.Unauthorized("Forbidden: user is deactivated"),
UserNotificationPolicyLogRecord.ERROR_NOTIFICATION_TELEGRAM_USER_IS_DEACTIVATED,
),
],
)
@pytest.mark.django_db
def test_personal_connector_send_link_to_channel_message_handle_exceptions(
side_effect,
notification_error_code,
make_organization_and_user,
make_telegram_user_connector,
make_user_notification_policy,
make_alert_receive_channel,
make_alert_group,
):
# set up a user with Telegram account connected
organization, user = make_organization_and_user()
user_connector = make_telegram_user_connector(user)
notification_policy = make_user_notification_policy(
user,
UserNotificationPolicy.Step.NOTIFY,
notify_by=UserNotificationPolicy.NotificationChannel.TELEGRAM,
important=False,
)
# create an alert group with an existing Telegram message in user's DM
alert_receive_channel = make_alert_receive_channel(organization)
alert_group = make_alert_group(alert_receive_channel)
assert not user.personal_log_records.exists()
with patch.object(TelegramClient, "send_message", side_effect=side_effect) as mock_send_message:
user_connector.send_link_to_channel_message(alert_group, notification_policy)
mock_send_message.assert_called_once()
log_records = user.personal_log_records.filter(alert_group=alert_group)
assert log_records.count() == 1
assert log_records.first().notification_error_code == notification_error_code

View file

@ -45,6 +45,7 @@ opentelemetry-exporter-otlp-proto-grpc==1.15.0
django-dbconn-retry==0.1.7
django-ipware==4.0.2
django-anymail==8.6
django-amazon-ses==4.0.1
django-deprecate-fields==0.1.1
pymdown-extensions==10.0
requests==2.31.0

View file

@ -7,6 +7,7 @@ const HEARTBEAT_SETTINGS_FORM_TEST_ID = 'heartbeat-settings-form';
test.describe("updating an integration's heartbeat interval works", async () => {
const _openHeartbeatSettingsForm = async (page: Page) => {
await page.getByTestId('integration-settings-context-menu-wrapper').getByRole('img').click();
await page.waitForTimeout(1000);
await page.getByTestId('integration-heartbeat-settings').click();
};
@ -29,6 +30,8 @@ test.describe("updating an integration's heartbeat interval works", async () =>
await heartbeatSettingsForm.getByTestId('update-heartbeat').click();
await page.waitForTimeout(1000);
await _openHeartbeatSettingsForm(page);
const heartbeatIntervalValue = await heartbeatSettingsForm

View file

@ -20,13 +20,6 @@ export const createOnCallSchedule = async (page: Page, scheduleName: string, use
await clickButton({ page, buttonText: 'Add rotation' });
/**
* Drag the modal such that the "Create" button will always be visible within the viewport. We cannot scroll
* on the modal itself
* https://playwright.dev/docs/input#dragging-manually
*/
await page.locator('.ReactModal__Content .drag-handler').dragTo(page.locator('.page-header__logo'));
await selectDropdownValue({
page,
selectType: 'grafanaSelect',

View file

@ -122,7 +122,7 @@
"@grafana/data": "^9.2.4",
"@grafana/faro-web-sdk": "^1.0.0-beta4",
"@grafana/faro-web-tracing": "^1.0.0-beta4",
"@grafana/labels": "~1.4.2",
"@grafana/labels": "~1.4.3",
"@grafana/runtime": "9.3.0-beta1",
"@grafana/ui": "^10.2.0",
"@lifeomic/attempt": "^3.0.3",

View file

@ -3,8 +3,10 @@ import React from 'react';
import { PluginPageProps, PluginPage as RealPluginPage } from '@grafana/runtime';
import Header from 'navbar/Header/Header';
import RenderConditionally from 'components/RenderConditionally/RenderConditionally';
import { pages } from 'pages';
import { isTopNavbar } from 'plugin/GrafanaPluginRootPage.helpers';
import { DEFAULT_PAGE } from 'utils/consts';
interface AppPluginPageProps extends PluginPageProps {
page?: string;
@ -14,10 +16,14 @@ export const PluginPage = (isTopNavbar() ? RealPlugin : PluginPageFallback) as R
function RealPlugin(props: AppPluginPageProps): React.ReactNode {
const { page } = props;
const isDefaultPage = page === DEFAULT_PAGE;
return (
<RealPluginPage {...props}>
<Header />
<RenderConditionally shouldRender={isDefaultPage}>
<Header />
</RenderConditionally>
{pages[page]?.text && !pages[page]?.hideTitle && (
<h3 className="page-title" data-testid="page-title">
{pages[page].text}

View file

@ -4,7 +4,7 @@
.header-topnavbar {
padding-top: 0;
padding-bottom: 36px;
padding-bottom: 12px;
}
.navbar-heading {
@ -34,6 +34,7 @@
flex-direction: row;
column-gap: 8px;
row-gap: 8px;
margin-left: -50px;
}
.irm-icon {
@ -52,3 +53,12 @@
margin-bottom: 0;
}
}
.logo-container,
.page-header__img {
height: 32px;
}
.page-header__title {
margin-bottom: 8px;
}

View file

@ -23,8 +23,8 @@ const Header = observer(() => {
<div className={cx('root')}>
<div className={cx('page-header__inner', { 'header-topnavbar': isTopNavbar() })}>
<div className={cx('navbar-left')}>
<span className="page-header__logo">
<img className="page-header__img" src={logo} alt="Grafana OnCall" />
<span className={cx('page-header__logo', 'logo-container')}>
<img className={cx('page-header__img')} src={logo} alt="Grafana OnCall" />
</span>
<div className="page-header__info-block">{renderHeading()}</div>
</div>
@ -41,6 +41,7 @@ const Header = observer(() => {
<h1 className={cx('page-header__title')}>Grafana OnCall</h1>
<div className={cx('navbar-heading-container')}>
<div className={cx('page-header__sub-title')}>{APP_SUBTITLE}</div>
<Card heading={undefined} className={cx('navbar-heading')}>
<a
href="https://github.com/grafana/oncall"

View file

@ -7,6 +7,10 @@
width: 40px;
}
.title {
margin-bottom: 16px;
}
.tabsBar {
margin-bottom: 24px;
}

View file

@ -16,10 +16,13 @@ const mondayDayOffset = {
};
export const getWeekStartString = () => {
if (!config.bootData.user.weekStart || config.bootData.user.weekStart === 'browser') {
const weekStart = (config.bootData.user.weekStart || '').toLowerCase();
if (!weekStart || weekStart === 'browser') {
return 'monday';
}
return config.bootData.user.weekStart;
return weekStart;
};
export const getNow = (tz: Timezone) => {

View file

@ -2027,10 +2027,10 @@
"@opentelemetry/sdk-trace-web" "^1.8.0"
"@opentelemetry/semantic-conventions" "^1.8.0"
"@grafana/labels@~1.4.2":
version "1.4.2"
resolved "https://registry.yarnpkg.com/@grafana/labels/-/labels-1.4.2.tgz#3ce4fb4e06c86793df85622de9fd47793261a849"
integrity sha512-4d/+SnLxxBGCYGZI/BAtF1s6M/K5cxFmOEDmUORBs5sXTiUXHsvNXbsh1ACfcH/wCUlbypYY6FQULSAWs6pOeQ==
"@grafana/labels@~1.4.3":
version "1.4.3"
resolved "https://registry.yarnpkg.com/@grafana/labels/-/labels-1.4.3.tgz#1103ef41341c84cac8d7d7e0b36d4671d20311b6"
integrity sha512-ImmkKERHkbDqakjgFN1Tl6FmwQa+7/YTyV+G8vBtX6HlNWIPGso7glNuHOEvMdvhz1fuJgSFEQ9+nggZv1TW4g==
dependencies:
"@emotion/css" "^11.11.2"
"@grafana/ui" "^10.0.0"