v1.3.77
This commit is contained in:
commit
1c710bd9dd
19 changed files with 224 additions and 27 deletions
|
|
@ -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
|
||||
|
|
|
|||
6
Tiltfile
6
Tiltfile
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
54
engine/apps/email/tests/test_inbound_email.py
Normal file
54
engine/apps/email/tests/test_inbound_email.py
Normal 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
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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():
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -7,6 +7,10 @@
|
|||
width: 40px;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.tabsBar {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue