diff --git a/engine/apps/alerts/migrations/0036_alertgroup_grafana_incident_id.py b/engine/apps/alerts/migrations/0036_alertgroup_grafana_incident_id.py new file mode 100644 index 00000000..7bf218cd --- /dev/null +++ b/engine/apps/alerts/migrations/0036_alertgroup_grafana_incident_id.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.6 on 2023-10-31 20:05 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('alerts', '0035_alter_alertreceivechannel_maintenance_author'), + ] + + operations = [ + migrations.AddField( + model_name='alertgroup', + name='grafana_incident_id', + field=models.CharField(default=None, max_length=100, null=True), + ), + ] diff --git a/engine/apps/alerts/models/alert_group.py b/engine/apps/alerts/models/alert_group.py index 983b0ba1..0087f9f4 100644 --- a/engine/apps/alerts/models/alert_group.py +++ b/engine/apps/alerts/models/alert_group.py @@ -392,6 +392,8 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models. # TODO: drop this column in an upcoming release is_restricted = models.BooleanField(default=False, null=True) + grafana_incident_id = models.CharField(max_length=100, null=True, default=None) + @staticmethod def get_silenced_state_filter(): """ diff --git a/engine/apps/alerts/paging.py b/engine/apps/alerts/paging.py index 694e88ed..42a996f7 100644 --- a/engine/apps/alerts/paging.py +++ b/engine/apps/alerts/paging.py @@ -44,7 +44,13 @@ class DirectPagingAlertPayload(typing.TypedDict): def _trigger_alert( - organization: Organization, team: Team | None, message: str, title: str, from_user: User + organization: Organization, + team: Team | None, + message: str, + title: str, + permalink: str | None, + grafana_incident_id: str | None, + from_user: User, ) -> AlertGroup: """Trigger manual integration alert from params.""" alert_receive_channel = AlertReceiveChannel.get_or_create_manual_integration( @@ -73,7 +79,7 @@ def _trigger_alert( "message": message, "uid": str(uuid4()), # avoid grouping "author_username": from_user.username, - "permalink": None, + "permalink": permalink, }, } @@ -87,7 +93,13 @@ def _trigger_alert( link_to_upstream_details=None, channel_filter=channel_filter, ) - return alert.group + alert_group = alert.group + + if grafana_incident_id is not None: + alert_group.grafana_incident_id = grafana_incident_id + alert_group.save(update_fields=["grafana_incident_id"]) + + return alert_group def _construct_title(from_user: User, team: Team | None, users: UserNotifications) -> str: @@ -111,6 +123,8 @@ def direct_paging( from_user: User, message: str, title: str | None = None, + source_url: str | None = None, + grafana_incident_id: str | None = None, team: Team | None = None, users: UserNotifications | None = None, alert_group: AlertGroup | None = None, @@ -135,7 +149,7 @@ def direct_paging( # create alert group if needed if alert_group is None: - alert_group = _trigger_alert(organization, team, message, title, from_user) + alert_group = _trigger_alert(organization, team, message, title, source_url, grafana_incident_id, from_user) for u, important in users: alert_group.log_records.create( diff --git a/engine/apps/api/serializers/alert_group.py b/engine/apps/api/serializers/alert_group.py index 8cf44181..1e8371ea 100644 --- a/engine/apps/api/serializers/alert_group.py +++ b/engine/apps/api/serializers/alert_group.py @@ -149,6 +149,7 @@ class AlertGroupListSerializer(EagerLoadingMixin, AlertGroupFieldsCacheSerialize "status", "declare_incident_link", "team", + "grafana_incident_id", ] @extend_schema_field( diff --git a/engine/apps/api/serializers/paging.py b/engine/apps/api/serializers/paging.py index 9c16087c..ab3584bb 100644 --- a/engine/apps/api/serializers/paging.py +++ b/engine/apps/api/serializers/paging.py @@ -43,15 +43,21 @@ class DirectPagingSerializer(serializers.Serializer): title = serializers.CharField(required=False, default=None) message = serializers.CharField(required=False, default=None, allow_null=True) + source_url = serializers.URLField(required=False, default=None, allow_null=True) + grafana_incident_id = serializers.CharField(required=False, default=None, allow_null=True) def validate(self, attrs): organization = self.context["organization"] alert_group_id = attrs["alert_group_id"] title = attrs["title"] message = attrs["message"] + source_url = attrs["source_url"] + grafana_incident_id = attrs["grafana_incident_id"] - if alert_group_id and (title or message): - raise serializers.ValidationError("alert_group_id and (title, message) are mutually exclusive") + if alert_group_id and (title or message or source_url or grafana_incident_id): + raise serializers.ValidationError( + "alert_group_id and (title, message, source_url, grafana_incident_id) are mutually exclusive" + ) if alert_group_id: try: diff --git a/engine/apps/api/tests/test_paging.py b/engine/apps/api/tests/test_paging.py index a45da9b8..24e5f32c 100644 --- a/engine/apps/api/tests/test_paging.py +++ b/engine/apps/api/tests/test_paging.py @@ -9,6 +9,8 @@ from apps.api.permissions import LegacyAccessControlRole title = "Custom title" message = "Testing direct paging with new alert group" +source_url = "https://www.example.com" +grafana_incident_id = "abcd1234" @pytest.mark.django_db @@ -77,6 +79,43 @@ def test_direct_paging_page_team( data={ "team": team.public_primary_key, "message": message, + "source_url": source_url, + "grafana_incident_id": grafana_incident_id, + }, + format="json", + **make_user_auth_headers(user, token), + ) + + assert response.status_code == status.HTTP_200_OK + + alert_group = AlertGroup.objects.get(public_primary_key=response.json()["alert_group_id"]) + alert = alert_group.alerts.first() + + assert alert_group.grafana_incident_id == grafana_incident_id + assert alert.raw_request_data["oncall"]["permalink"] == source_url + + +@pytest.mark.django_db +def test_direct_paging_page_from_grafana_incident( + make_organization_and_user_with_plugin_token, + make_team, + make_user_auth_headers, +): + organization, user, token = make_organization_and_user_with_plugin_token(role=LegacyAccessControlRole.EDITOR) + team = make_team(organization=organization) + + # user must be part of the team + user.teams.add(team) + + client = APIClient() + url = reverse("api-internal:direct_paging") + + response = client.post( + url, + data={ + "team": team.public_primary_key, + "message": message, + "grafana_incident_id": "asdf", }, format="json", **make_user_auth_headers(user, token), @@ -185,15 +224,26 @@ def test_direct_paging_no_user_or_team_specified( assert response.json()["detail"] == DirectPagingUserTeamValidationError.DETAIL +@pytest.mark.parametrize( + "field_name,field_value", + [ + ("title", title), + ("message", message), + ("source_url", source_url), + ("grafana_incident_id", grafana_incident_id), + ], +) @pytest.mark.django_db -def test_direct_paging_alert_group_id_and_message_or_title_are_mutually_exclusive( +def test_direct_paging_alert_group_id_and_other_fields_are_mutually_exclusive( make_organization_and_user_with_plugin_token, make_team, make_user_auth_headers, make_alert_receive_channel, make_alert_group, + field_name, + field_value, ): - error_msg = "alert_group_id and (title, message) are mutually exclusive" + error_msg = "alert_group_id and (title, message, source_url, grafana_incident_id) are mutually exclusive" organization, user, token = make_organization_and_user_with_plugin_token(role=LegacyAccessControlRole.EDITOR) team = make_team(organization=organization) @@ -207,17 +257,15 @@ def test_direct_paging_alert_group_id_and_message_or_title_are_mutually_exclusiv client = APIClient() url = reverse("api-internal:direct_paging") - base_data = {"team": team.public_primary_key, "alert_group_id": alert_group.public_primary_key} - response = client.post( - url, data={**base_data, "message": message}, format="json", **make_user_auth_headers(user, token) - ) - - assert response.status_code == status.HTTP_400_BAD_REQUEST - assert response.json()["non_field_errors"] == [error_msg] - - response = client.post( - url, data={**base_data, "title": title}, format="json", **make_user_auth_headers(user, token) + url, + data={ + "team": team.public_primary_key, + "alert_group_id": alert_group.public_primary_key, + field_name: field_value, + }, + format="json", + **make_user_auth_headers(user, token), ) assert response.status_code == status.HTTP_400_BAD_REQUEST diff --git a/engine/apps/api/views/paging.py b/engine/apps/api/views/paging.py index ff48a648..fda9791a 100644 --- a/engine/apps/api/views/paging.py +++ b/engine/apps/api/views/paging.py @@ -33,6 +33,8 @@ class DirectPagingAPIView(APIView): from_user=request.user, message=validated_data["message"], title=validated_data["title"], + source_url=validated_data["source_url"], + grafana_incident_id=validated_data["grafana_incident_id"], team=validated_data["team"], users=[(user["instance"], user["important"]) for user in validated_data["users"]], alert_group=validated_data["alert_group"], diff --git a/grafana-plugin/src/models/alertgroup/alertgroup.types.ts b/grafana-plugin/src/models/alertgroup/alertgroup.types.ts index 5514042e..32ccdedc 100644 --- a/grafana-plugin/src/models/alertgroup/alertgroup.types.ts +++ b/grafana-plugin/src/models/alertgroup/alertgroup.types.ts @@ -85,6 +85,7 @@ export interface Alert { alert_receive_channel: Partial; paged_users: PagedUser[]; team: GrafanaTeam['id']; + grafana_incident_id: string | null; // set by client loading?: boolean; diff --git a/grafana-plugin/src/pages/incident/Incident.tsx b/grafana-plugin/src/pages/incident/Incident.tsx index 42d2fc6d..cf50e337 100644 --- a/grafana-plugin/src/pages/incident/Incident.tsx +++ b/grafana-plugin/src/pages/incident/Incident.tsx @@ -177,13 +177,15 @@ class IncidentPage extends React.Component
- + {(!incident.resolved || incident.paged_users.length > 0) && ( + + )} {this.renderTimeline()}
@@ -400,13 +402,15 @@ class IncidentPage extends React.Component onSilence: this.getSilenceClickHandler(incident), onUnsilence: this.getUnsilenceClickHandler(incident), })} - - - - - + {incident.grafana_incident_id === null && ( + + + + + + )}