From a3459a761054b826f192159a8b1e8d620254a432 Mon Sep 17 00:00:00 2001 From: Joey Orlando Date: Wed, 18 Dec 2024 13:36:30 -0500 Subject: [PATCH 1/8] wip --- engine/apps/slack/scenarios/paging.py | 6 +++++- engine/settings/base.py | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/engine/apps/slack/scenarios/paging.py b/engine/apps/slack/scenarios/paging.py index f1981fc5e..59a0c1941 100644 --- a/engine/apps/slack/scenarios/paging.py +++ b/engine/apps/slack/scenarios/paging.py @@ -702,7 +702,7 @@ def _get_team_select_blocks( if not teams: direct_paging_info_msg["elements"][0][ "text" - ] += ". There are currently no teams which have a Direct Paging integration that is configured." + ] += ".\n\nThere are currently no teams which have a Direct Paging integration that is configured." blocks.append(direct_paging_info_msg) return blocks @@ -742,6 +742,10 @@ def _get_team_select_blocks( "optional": True, } + team_severity = { + "type": "checkboxes" + } + blocks.append(team_select) # No context block if no team selected diff --git a/engine/settings/base.py b/engine/settings/base.py index 0f73c8d5a..7c57368b7 100644 --- a/engine/settings/base.py +++ b/engine/settings/base.py @@ -721,7 +721,7 @@ class BrokerTypes: SLACK_CLIENT_OAUTH_ID = os.environ.get("SLACK_CLIENT_OAUTH_ID") SLACK_CLIENT_OAUTH_SECRET = os.environ.get("SLACK_CLIENT_OAUTH_SECRET") -SLACK_DIRECT_PAGING_SLASH_COMMAND = os.environ.get("SLACK_DIRECT_PAGING_SLASH_COMMAND", "/escalate").lstrip("/") +SLACK_DIRECT_PAGING_SLASH_COMMAND = os.environ.get("SLACK_DIRECT_PAGING_SLASH_COMMAND", "/escalate-local").lstrip("/") # it's a root command for unified slack - '/ incident new', '/ escalate' SLACK_IRM_ROOT_COMMAND = os.environ.get("SLACK_IRM_ROOT_COMMAND", "/grafana").lstrip("/") From 7efb55c053be6fd31f1511ebd0e4cd658c8920d6 Mon Sep 17 00:00:00 2001 From: Joey Orlando Date: Thu, 19 Dec 2024 11:49:34 -0500 Subject: [PATCH 2/8] feat: allow setting importance when direct paging a team --- .../integrations/references/manual/index.md | 4 + .../oncall-api-reference/escalation.md | 13 +- engine/apps/alerts/paging.py | 20 ++- engine/apps/alerts/tests/test_paging.py | 31 ++++- engine/apps/api/serializers/direct_paging.py | 1 + engine/apps/api/tests/test_direct_paging.py | 16 ++- engine/apps/api/views/direct_paging.py | 1 + .../apps/public_api/tests/test_escalation.py | 14 ++- engine/apps/public_api/views/escalation.py | 1 + engine/apps/slack/scenarios/paging.py | 115 ++++++++++++++++-- .../slack/tests/scenario_steps/test_paging.py | 99 +++++++++++++-- 11 files changed, 289 insertions(+), 26 deletions(-) diff --git a/docs/sources/configure/integrations/references/manual/index.md b/docs/sources/configure/integrations/references/manual/index.md index 42c1029f2..cad437dcf 100644 --- a/docs/sources/configure/integrations/references/manual/index.md +++ b/docs/sources/configure/integrations/references/manual/index.md @@ -99,3 +99,7 @@ Navigate to the **Integrations** page and find the "Direct paging" integration f integration's detail page, you can customize its settings, link it to an escalation chain, and configure associated ChatOps channels. To confirm that the integration is functioning as intended, [create a new alert group](#page-a-team) and select the same team for a test run. + +### Important escalations + +TODO: diff --git a/docs/sources/oncall-api-reference/escalation.md b/docs/sources/oncall-api-reference/escalation.md index b2b375d6e..e8c826d1f 100644 --- a/docs/sources/oncall-api-reference/escalation.md +++ b/docs/sources/oncall-api-reference/escalation.md @@ -18,6 +18,11 @@ refs: destination: /docs/oncall//configure/integrations/references/manual - pattern: /docs/grafana-cloud/ destination: /docs/grafana-cloud/configure/integrations/references/manual + manual-paging-team-important: + - pattern: /docs/oncall/ + destination: /docs/oncall//configure/integrations/references/manual#important-escalations + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana-cloud/configure/integrations/references/manual#important-escalations --- # Escalation HTTP API @@ -90,7 +95,8 @@ curl "{{API_URL}}/api/v1/escalation/" \ "title": "We are seeing a network outage in the datacenter", "message": "I need help investigating, can you join the investigation?", "source_url": "https://github.com/myorg/myrepo/issues/123", - "team": "TI73TDU19W48J" + "team": "TI73TDU19W48J", + "important_team_escalation": True }' ``` @@ -176,6 +182,7 @@ The above command returns JSON structured in the following way: | `team` | No | Yes (see [Things to Note](#things-to-note)) | Grafana OnCall team ID. If specified, will use the "Direct Paging" Integration associated with this Grafana OnCall team, to create the Alert Group. | | `users` | No | Yes (see [Things to Note](#things-to-note)) | List of user(s) to escalate to. See above request example for object schema. `id` represents the Grafana OnCall user's ID. `important` is a boolean representing whether to escalate the Alert Group using this user's default or important personal notification policy. | | `alert_group_id` | No | No | If specified, will escalate the specified users for this Alert Group. | +| `important_team_escalation` | No | No | Sets the value of `payload.oncall.important` to the value specified here (default is `False`; see [Things to Note](#things-to-note) for more details). | ## Things to note @@ -186,6 +193,10 @@ existing Alert Group if you are trying to escalate to a set of users on an existing Alert Group, you cannot update the `title`, `message`, or `source_url` of that Alert Group - If escalating to a set of users for an existing Alert Group, the Alert Group cannot be in a resolved state +- Regarding `important_team_escalation`; this can be useful to send an "important" escalation to the specified team. +Teams can configure their Direct Paging Integration to route to different escalation chains based on the value of +`payload.oncall.important`. See [Manual paging integration - important escalations](ref:manual-paging-team-important) +for more details. **HTTP request** diff --git a/engine/apps/alerts/paging.py b/engine/apps/alerts/paging.py index 5121d0179..867ac18cd 100644 --- a/engine/apps/alerts/paging.py +++ b/engine/apps/alerts/paging.py @@ -48,6 +48,7 @@ class DirectPagingAlertPayload(typing.TypedDict): def _trigger_alert( organization: Organization, team: Team | None, + important_team_escalation: bool, message: str, title: str, permalink: str | None, @@ -82,6 +83,13 @@ def _trigger_alert( "uid": str(uuid4()), # avoid grouping "author_username": from_user.username, "permalink": permalink, + # NOTE: this field is mostly being added for purposes of escalating to a team + # this field is provided via the web UI/API/slack as a checkbox, indicating that the user doing the paging + # would like to send an "important" page to the team. + # + # Teams can configure routing in their Direct Paging Integration to route based on this field to different + # escalation chains + "important": important_team_escalation, }, } @@ -128,6 +136,7 @@ def direct_paging( source_url: str | None = None, grafana_incident_id: str | None = None, team: Team | None = None, + important_team_escalation: bool = False, users: UserNotifications | None = None, alert_group: AlertGroup | None = None, ) -> AlertGroup | None: @@ -156,7 +165,16 @@ def direct_paging( # create alert group if needed with transaction.atomic(): if alert_group is None: - alert_group = _trigger_alert(organization, team, message, title, source_url, grafana_incident_id, from_user) + alert_group = _trigger_alert( + organization, + team, + important_team_escalation, + message, + title, + source_url, + grafana_incident_id, + from_user, + ) for u, important in users: alert_group.log_records.create( diff --git a/engine/apps/alerts/tests/test_paging.py b/engine/apps/alerts/tests/test_paging.py index d6bad7a2b..4b2e40695 100644 --- a/engine/apps/alerts/tests/test_paging.py +++ b/engine/apps/alerts/tests/test_paging.py @@ -1,4 +1,4 @@ -from unittest.mock import call, patch +from unittest.mock import ANY, call, patch import pytest from django.utils import timezone @@ -86,23 +86,46 @@ def test_direct_paging_user(make_organization, make_user_for_organization, djang assert_log_record(ag, f"{from_user.username} paged user {u.username}", expected_info=expected_info) +@pytest.mark.parametrize("important_team_escalation", [True, False]) @pytest.mark.django_db -def test_direct_paging_team(make_organization, make_team, make_user_for_organization): +def test_direct_paging_team(make_organization, make_team, make_user_for_organization, important_team_escalation): organization = make_organization() from_user = make_user_for_organization(organization) team = make_team(organization) + + from_author_username = from_user.username + source_url = "https://www.example.com" + title = f"{from_author_username} is paging {team.name} to join escalation" msg = "Fire" - direct_paging(organization, from_user, msg, team=team) + direct_paging( + organization, + from_user, + msg, + source_url=source_url, + team=team, + important_team_escalation=important_team_escalation, + ) # alert group created alert_groups = AlertGroup.objects.all() assert alert_groups.count() == 1 ag = alert_groups.get() alert = ag.alerts.get() - assert alert.title == f"{from_user.username} is paging {team.name} to join escalation" + assert alert.title == title assert alert.message == msg + assert alert.raw_request_data == { + "oncall": { + "title": title, + "message": msg, + "uid": ANY, + "author_username": from_author_username, + "permalink": source_url, + "important": important_team_escalation, + }, + } + assert ag.channel.verbal_name == f"Direct paging ({team.name} team)" assert ag.channel.team == team diff --git a/engine/apps/api/serializers/direct_paging.py b/engine/apps/api/serializers/direct_paging.py index 4cb1e646e..002438dc2 100644 --- a/engine/apps/api/serializers/direct_paging.py +++ b/engine/apps/api/serializers/direct_paging.py @@ -39,6 +39,7 @@ class BasePagingSerializer(serializers.Serializer): users = UserReferenceSerializer(many=True, required=False, default=list) team = TeamPrimaryKeyRelatedField(allow_null=True, default=CurrentTeamDefault()) + important_team_escalation = serializers.BooleanField(required=False, default=False) alert_group_id = serializers.CharField(required=False, default=None) alert_group = serializers.HiddenField(default=None) # set in DirectPagingSerializer.validate diff --git a/engine/apps/api/tests/test_direct_paging.py b/engine/apps/api/tests/test_direct_paging.py index be5290652..6496d1ec3 100644 --- a/engine/apps/api/tests/test_direct_paging.py +++ b/engine/apps/api/tests/test_direct_paging.py @@ -1,3 +1,5 @@ +from unittest.mock import ANY + import pytest from django.urls import reverse from rest_framework import status @@ -59,11 +61,13 @@ def test_direct_paging_new_alert_group( assert alert.message == message +@pytest.mark.parametrize("important_team_escalation", [True, False]) @pytest.mark.django_db def test_direct_paging_page_team( make_organization_and_user_with_plugin_token, make_team, make_user_auth_headers, + important_team_escalation, ): organization, user, token = make_organization_and_user_with_plugin_token(role=LegacyAccessControlRole.EDITOR) team = make_team(organization=organization) @@ -81,6 +85,7 @@ def test_direct_paging_page_team( "message": message, "source_url": source_url, "grafana_incident_id": grafana_incident_id, + "important_team_escalation": important_team_escalation, }, format="json", **make_user_auth_headers(user, token), @@ -92,7 +97,16 @@ def test_direct_paging_page_team( alert = alert_group.alerts.first() assert alert_group.grafana_incident_id == grafana_incident_id - assert alert.raw_request_data["oncall"]["permalink"] == source_url + assert alert.raw_request_data == { + "oncall": { + "title": ANY, + "message": message, + "uid": ANY, + "author_username": ANY, + "permalink": source_url, + "important": important_team_escalation, + }, + } @pytest.mark.django_db diff --git a/engine/apps/api/views/direct_paging.py b/engine/apps/api/views/direct_paging.py index b5d7a9eb0..eb923cdf7 100644 --- a/engine/apps/api/views/direct_paging.py +++ b/engine/apps/api/views/direct_paging.py @@ -40,6 +40,7 @@ def post(self, request): source_url=validated_data["source_url"], grafana_incident_id=validated_data["grafana_incident_id"], team=validated_data["team"], + important_team_escalation=validated_data["important_team_escalation"], users=[(user["instance"], user["important"]) for user in validated_data["users"]], alert_group=validated_data["alert_group"], ) diff --git a/engine/apps/public_api/tests/test_escalation.py b/engine/apps/public_api/tests/test_escalation.py index f6a7665da..025c672b6 100644 --- a/engine/apps/public_api/tests/test_escalation.py +++ b/engine/apps/public_api/tests/test_escalation.py @@ -89,11 +89,13 @@ def test_escalation_new_alert_group( assert alert.message == message +@pytest.mark.parametrize("important_team_escalation", [True, False]) @pytest.mark.django_db def test_escalation_team( make_organization_and_user_with_token, make_team, make_user_auth_headers, + important_team_escalation, ): organization, user, token = make_organization_and_user_with_token() team = make_team(organization=organization) @@ -110,6 +112,7 @@ def test_escalation_team( "team": team.public_primary_key, "message": message, "source_url": source_url, + "important_team_escalation": important_team_escalation, }, format="json", **make_user_auth_headers(user, token), @@ -120,7 +123,16 @@ def test_escalation_team( alert_group = AlertGroup.objects.get(public_primary_key=response.json()["id"]) alert = alert_group.alerts.first() - assert alert.raw_request_data["oncall"]["permalink"] == source_url + assert alert.raw_request_data == { + "oncall": { + "title": mock.ANY, + "message": message, + "uid": mock.ANY, + "author_username": mock.ANY, + "permalink": source_url, + "important": important_team_escalation, + }, + } @pytest.mark.django_db diff --git a/engine/apps/public_api/views/escalation.py b/engine/apps/public_api/views/escalation.py index be5459264..2d38dc0a1 100644 --- a/engine/apps/public_api/views/escalation.py +++ b/engine/apps/public_api/views/escalation.py @@ -41,6 +41,7 @@ def post(self, request): title=validated_data["title"], source_url=validated_data["source_url"], team=validated_data["team"], + important_team_escalation=validated_data["important_team_escalation"], users=[(user["instance"], user["important"]) for user in validated_data["users"]], alert_group=validated_data["alert_group"], ) diff --git a/engine/apps/slack/scenarios/paging.py b/engine/apps/slack/scenarios/paging.py index 59a0c1941..c83354758 100644 --- a/engine/apps/slack/scenarios/paging.py +++ b/engine/apps/slack/scenarios/paging.py @@ -37,12 +37,14 @@ from apps.slack.models import SlackTeamIdentity, SlackUserIdentity from apps.user_management.models import Organization, Team, User - DIRECT_PAGING_TEAM_SELECT_ID = "paging_team_select" +DIRECT_PAGING_TEAM_SEVERITY_CHECKBOXES_ID = "paging_team_severity_checkboxes" DIRECT_PAGING_ORG_SELECT_ID = "paging_org_select" DIRECT_PAGING_USER_SELECT_ID = "paging_user_select" DIRECT_PAGING_MESSAGE_INPUT_ID = "paging_message_input" +DIRECT_PAGING_TEAM_SEVERITY_CHECKBOX_VALUE = "important" + DEFAULT_TEAM_VALUE = "default_team" @@ -248,6 +250,7 @@ def process_scenario( from_user=user, message=message, team=selected_team, + important_team_escalation=_get_team_escalation_severity_from_payload(payload, input_id_prefix), users=selected_users, ) except DirectPagingUserTeamValidationError: @@ -331,6 +334,14 @@ def process_scenario( ) +class OnPagingTeamSeverityCheckboxChange(OnPagingTeamChange): + """ + Specify alert severity when escalating to a team. + + NOTE: we simply reuse `OnPagingTeamChange` step, since the behavior is the same. + """ + + class OnPagingUserChange(scenario_step.ScenarioStep): """Add selected to user to the list. @@ -491,6 +502,7 @@ def render_dialog( new_private_metadata["input_id_prefix"] = new_input_id_prefix selected_organization = predefined_org if predefined_org else available_organizations.first() is_team_selected, selected_team = False, None + is_team_escalation_important = False else: # setup form using data/state old_input_id_prefix, new_input_id_prefix, new_private_metadata = _get_and_change_input_id_prefix_from_metadata( @@ -502,6 +514,7 @@ def render_dialog( else _get_selected_org_from_payload(payload, old_input_id_prefix, slack_team_identity, slack_user_identity) ) is_team_selected, selected_team = _get_selected_team_from_payload(payload, old_input_id_prefix) + is_team_escalation_important = _get_team_escalation_severity_from_payload(payload, old_input_id_prefix) blocks: Block.AnyBlocks = [] @@ -523,9 +536,14 @@ def render_dialog( ) blocks.append(organization_select) - # Add team select and additional responders blocks + # Add team select/severity and additional responders blocks blocks += _get_team_select_blocks( - slack_user_identity, selected_organization, is_team_selected, selected_team, new_input_id_prefix + slack_user_identity, + selected_organization, + is_team_selected, + selected_team, + is_team_escalation_important, + new_input_id_prefix, ) blocks += _get_user_select_blocks(payload, selected_organization, new_input_id_prefix, error_msg) @@ -629,6 +647,25 @@ def _get_select_field_value(payload: EventPayload, prefix_id: str, routing_uid: return json.loads(field["value"])["id"] if field else None +def _get_first_selected_checkbox_option_value( + payload: EventPayload, + prefix_id: str, + routing_uid: str, + field_id: str, +) -> str | None: + """ + NOTE: if reusing this for other logic outside of the team severity checkboxes, note that this function + will only return the value of the first checkbox option... + """ + try: + selected_options = payload["view"]["state"]["values"][prefix_id + field_id][routing_uid]["selected_options"] + if not selected_options: + return None + return selected_options[0]["value"] + except KeyError: + return None + + def _get_selected_org_from_payload( payload: EventPayload, input_id_prefix: str, @@ -676,6 +713,7 @@ def _get_team_select_blocks( organization: "Organization", is_selected: bool, value: typing.Optional["Team"], + is_team_escalation_important: bool, input_id_prefix: str, ) -> Block.AnyBlocks: blocks: Block.AnyBlocks = [] @@ -742,10 +780,6 @@ def _get_team_select_blocks( "optional": True, } - team_severity = { - "type": "checkboxes" - } - blocks.append(team_select) # No context block if no team selected @@ -773,6 +807,57 @@ def _get_team_select_blocks( } ) + team_severity_important_checkbox_option: CompositionObjectOption = { + "text": { + "type": "mrkdwn", + "text": "Important escalation", + }, + "value": DIRECT_PAGING_TEAM_SEVERITY_CHECKBOX_VALUE, + } + + team_severity_checkboxes_element: Block.Section = { + "type": "section", + "block_id": input_id_prefix + DIRECT_PAGING_TEAM_SEVERITY_CHECKBOXES_ID, + "text": { + "type": "plain_text", + # NOTE: this is a bit of a hack. Slack requires us to specify this text object, and it cannot be empty + # hence the empty space. We do this so that we can render the text instead in a context block below + # (which allows us to render it in a slightly smaller font size) + # https://api.slack.com/reference/block-kit/blocks#section + "text": " ", + }, + "accessory": { + "type": "checkboxes", + "options": [team_severity_important_checkbox_option], + "action_id": OnPagingTeamSeverityCheckboxChange.routing_uid(), + }, + } + + if is_team_escalation_important: + # From the docs https://api.slack.com/reference/block-kit/block-elements#checkboxes__fields + # An array of option objects that EXACTLY matches one or more of the options within options + team_severity_checkboxes_element["accessory"]["initial_options"] = [team_severity_important_checkbox_option] + + blocks.extend( + [ + { + "type": "context", + "elements": [ + { + "type": "mrkdwn", + "text": ( + "Check the following box if you would like to escalate to this team as an 'important' " + "escalation. This will set a `payload.oncall.important` attribute in the alert to `true`. " + "Teams can configure their Direct Paging Integration to route to different escalation chains " + "based on this. " + ), + } + ], + }, + team_severity_checkboxes_element, + ] + ) + return blocks @@ -955,6 +1040,16 @@ def _get_selected_team_from_payload( return selected_team_id, Team.objects.filter(pk=selected_team_id).first() +def _get_team_escalation_severity_from_payload(payload: EventPayload, input_id_prefix: str) -> bool: + checkbox_value = _get_first_selected_checkbox_option_value( + payload, + input_id_prefix, + OnPagingTeamSeverityCheckboxChange.routing_uid(), + DIRECT_PAGING_TEAM_SEVERITY_CHECKBOXES_ID, + ) + return checkbox_value == DIRECT_PAGING_TEAM_SEVERITY_CHECKBOX_VALUE + + def _get_selected_user_from_payload(payload: EventPayload, input_id_prefix: str) -> typing.Optional["User"]: from apps.user_management.models import User @@ -1039,6 +1134,12 @@ def _generate_input_id_prefix() -> str: "block_action_id": OnPagingTeamChange.routing_uid(), "step": OnPagingTeamChange, }, + { + "payload_type": PayloadType.BLOCK_ACTIONS, + "block_action_type": BlockActionType.CHECKBOXES, + "block_action_id": OnPagingTeamSeverityCheckboxChange.routing_uid(), + "step": OnPagingTeamSeverityCheckboxChange, + }, { "payload_type": PayloadType.BLOCK_ACTIONS, "block_action_type": BlockActionType.STATIC_SELECT, diff --git a/engine/apps/slack/tests/scenario_steps/test_paging.py b/engine/apps/slack/tests/scenario_steps/test_paging.py index 46c32f3cf..ec852150a 100644 --- a/engine/apps/slack/tests/scenario_steps/test_paging.py +++ b/engine/apps/slack/tests/scenario_steps/test_paging.py @@ -12,12 +12,14 @@ DIRECT_PAGING_MESSAGE_INPUT_ID, DIRECT_PAGING_ORG_SELECT_ID, DIRECT_PAGING_TEAM_SELECT_ID, + DIRECT_PAGING_TEAM_SEVERITY_CHECKBOXES_ID, DIRECT_PAGING_USER_SELECT_ID, DataKey, FinishDirectPaging, OnPagingItemActionChange, OnPagingOrgChange, OnPagingTeamChange, + OnPagingTeamSeverityCheckboxChange, OnPagingUserChange, Policy, StartDirectPaging, @@ -28,7 +30,13 @@ def make_paging_view_slack_payload( - selected_org=None, predefined_org=None, team=None, user=None, current_users=None, actions=None + selected_org=None, + predefined_org=None, + team=None, + important_team_escalation=False, + user=None, + current_users=None, + actions=None, ): """ Helper function to create a payload for paging view. @@ -66,6 +74,15 @@ def make_paging_view_slack_payload( } } }, + DIRECT_PAGING_TEAM_SEVERITY_CHECKBOXES_ID: { + OnPagingTeamSeverityCheckboxChange.routing_uid(): { + "selected_options": [ + {"value": "important"}, + ] + if important_team_escalation + else [] + }, + }, DIRECT_PAGING_TEAM_SELECT_ID: { OnPagingTeamChange.routing_uid(): { "selected_option": {"value": make_value({"id": team.pk if team else None}, organization)} @@ -141,6 +158,7 @@ def test_page_team_with_predefined_org(make_organization_and_user_with_slack_ide from_user=user, message="The Message", team=team, + important_team_escalation=False, users=[], ) @@ -385,15 +403,21 @@ def test_trigger_paging_additional_responders(make_organization_and_user_with_sl from_user=user, message="The Message", team=team, + important_team_escalation=False, users=[(user, True)], ) +@pytest.mark.parametrize("important_team_escalation", [True, False]) @pytest.mark.django_db -def test_page_team(make_organization_and_user_with_slack_identities, make_team): +def test_page_team(make_organization_and_user_with_slack_identities, make_team, important_team_escalation): organization, user, slack_team_identity, slack_user_identity = make_organization_and_user_with_slack_identities() team = make_team(organization) - payload = make_paging_view_slack_payload(selected_org=organization, team=team) + payload = make_paging_view_slack_payload( + selected_org=organization, + team=team, + important_team_escalation=important_team_escalation, + ) step = FinishDirectPaging(slack_team_identity) with patch("apps.slack.scenarios.paging.direct_paging") as mock_direct_paging: @@ -405,6 +429,7 @@ def test_page_team(make_organization_and_user_with_slack_identities, make_team): from_user=user, message="The Message", team=team, + important_team_escalation=important_team_escalation, users=[], ) @@ -421,6 +446,7 @@ def test_get_organization_select(make_organization): assert select["element"]["options"][0]["text"]["text"] == "Organization (stack_slug)" +@pytest.mark.parametrize("is_team_escalation_important", [True, False]) @pytest.mark.django_db def test_get_team_select_blocks( make_organization_and_user_with_slack_identities, @@ -428,6 +454,7 @@ def test_get_team_select_blocks( make_alert_receive_channel, make_escalation_chain, make_channel_filter, + is_team_escalation_important, ): info_msg = ( "*Note*: You can only page teams which have a Direct Paging integration that is configured. " @@ -444,7 +471,14 @@ def _contstruct_team_option(team): # no team selected - no team direct paging integrations available organization, _, _, slack_user_identity = make_organization_and_user_with_slack_identities() - blocks = _get_team_select_blocks(slack_user_identity, organization, False, None, input_id_prefix) + blocks = _get_team_select_blocks( + slack_user_identity, + organization, + False, + None, + is_team_escalation_important, + input_id_prefix, + ) assert len(blocks) == 1 @@ -452,7 +486,7 @@ def _contstruct_team_option(team): assert context_block["type"] == "context" assert ( context_block["elements"][0]["text"] - == info_msg + ". There are currently no teams which have a Direct Paging integration that is configured." + == info_msg + ".\n\nThere are currently no teams which have a Direct Paging integration that is configured." ) # no team selected - 1 team direct paging integration available @@ -462,7 +496,14 @@ def _contstruct_team_option(team): escalation_chain = make_escalation_chain(organization) make_channel_filter(arc, is_default=True, escalation_chain=escalation_chain) - blocks = _get_team_select_blocks(slack_user_identity, organization, False, None, input_id_prefix) + blocks = _get_team_select_blocks( + slack_user_identity, + organization, + False, + None, + is_team_escalation_important, + input_id_prefix, + ) assert len(blocks) == 2 input_block, context_block = blocks @@ -472,7 +513,7 @@ def _contstruct_team_option(team): assert input_block["element"]["options"] == [_contstruct_team_option(team)] assert context_block["elements"][0]["text"] == info_msg - # team selected + # team selected - team severity checkbox should also now appear organization, _, _, slack_user_identity = make_organization_and_user_with_slack_identities() team1 = make_team(organization) team2 = make_team(organization) @@ -488,10 +529,25 @@ def _setup_direct_paging_integration(team): _setup_direct_paging_integration(team1) team2_direct_paging_arc = _setup_direct_paging_integration(team2) - blocks = _get_team_select_blocks(slack_user_identity, organization, True, team2, input_id_prefix) + blocks = _get_team_select_blocks( + slack_user_identity, + organization, + True, + team2, + is_team_escalation_important, + input_id_prefix, + ) + + assert len(blocks) == 4 + input_block, context_block, team_severity_context_block, team_severity_checkboxes = blocks - assert len(blocks) == 2 - input_block, context_block = blocks + team_severity_important_checkbox_option = { + "text": { + "type": "mrkdwn", + "text": "Important escalation", + }, + "value": "important", + } team1_option = _contstruct_team_option(team1) team2_option = _contstruct_team_option(team2) @@ -509,6 +565,20 @@ def _sort_team_options(options): == f"Integration <{team2_direct_paging_arc.web_link}|{team2_direct_paging_arc.verbal_name}> will be used for notification." ) + assert team_severity_context_block["elements"][0]["text"] == ( + "Check the following box if you would like to escalate to this team as an 'important' " + "escalation. This will set a `payload.oncall.important` attribute in the alert to `true`. " + "Teams can configure their Direct Paging Integration to route to different escalation chains " + "based on this. " + ) + assert team_severity_checkboxes["accessory"]["type"] == "checkboxes" + assert team_severity_checkboxes["accessory"]["options"] == [team_severity_important_checkbox_option] + + if is_team_escalation_important: + assert team_severity_checkboxes["accessory"]["initial_options"] == [team_severity_important_checkbox_option] + else: + assert "initial_options" not in team_severity_checkboxes["accessory"] + # team's direct paging integration has two routes associated with it # the team should only be displayed once organization, _, _, slack_user_identity = make_organization_and_user_with_slack_identities() @@ -519,7 +589,14 @@ def _sort_team_options(options): make_channel_filter(arc, is_default=True, escalation_chain=escalation_chain) make_channel_filter(arc, escalation_chain=escalation_chain) - blocks = _get_team_select_blocks(slack_user_identity, organization, False, None, input_id_prefix) + blocks = _get_team_select_blocks( + slack_user_identity, + organization, + False, + None, + is_team_escalation_important, + input_id_prefix, + ) assert len(blocks) == 2 input_block, context_block = blocks From eda3728be93bf83026cad34ad5c317c1a579ea97 Mon Sep 17 00:00:00 2001 From: Joey Orlando Date: Thu, 19 Dec 2024 12:11:28 -0500 Subject: [PATCH 3/8] mypy fix --- engine/apps/slack/scenarios/paging.py | 31 +++++++++++++++------------ 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/engine/apps/slack/scenarios/paging.py b/engine/apps/slack/scenarios/paging.py index c83354758..f831510db 100644 --- a/engine/apps/slack/scenarios/paging.py +++ b/engine/apps/slack/scenarios/paging.py @@ -840,20 +840,23 @@ def _get_team_select_blocks( blocks.extend( [ - { - "type": "context", - "elements": [ - { - "type": "mrkdwn", - "text": ( - "Check the following box if you would like to escalate to this team as an 'important' " - "escalation. This will set a `payload.oncall.important` attribute in the alert to `true`. " - "Teams can configure their Direct Paging Integration to route to different escalation chains " - "based on this. " - ), - } - ], - }, + typing.cast( + Block.Context, + { + "type": "context", + "elements": [ + { + "type": "mrkdwn", + "text": ( + "Check the following box if you would like to escalate to this team as an 'important' " + "escalation. This will set a `payload.oncall.important` attribute in the alert to `true`. " + "Teams can configure their Direct Paging Integration to route to different escalation chains " + "based on this. " + ), + }, + ], + }, + ), team_severity_checkboxes_element, ] ) From 9fd7d2e1e8b11539b066b1b2d6a8a5a2040b6eb3 Mon Sep 17 00:00:00 2001 From: Joey Orlando Date: Thu, 19 Dec 2024 12:50:36 -0500 Subject: [PATCH 4/8] fix test --- engine/settings/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/engine/settings/base.py b/engine/settings/base.py index 75d4cc95d..007779f19 100644 --- a/engine/settings/base.py +++ b/engine/settings/base.py @@ -721,7 +721,7 @@ class BrokerTypes: SLACK_CLIENT_OAUTH_ID = os.environ.get("SLACK_CLIENT_OAUTH_ID") SLACK_CLIENT_OAUTH_SECRET = os.environ.get("SLACK_CLIENT_OAUTH_SECRET") -SLACK_DIRECT_PAGING_SLASH_COMMAND = os.environ.get("SLACK_DIRECT_PAGING_SLASH_COMMAND", "/escalate-local").lstrip("/") +SLACK_DIRECT_PAGING_SLASH_COMMAND = os.environ.get("SLACK_DIRECT_PAGING_SLASH_COMMAND", "/escalate").lstrip("/") # it's a root command for unified slack - '/ incident new', '/ escalate' SLACK_IRM_ROOT_COMMAND = os.environ.get("SLACK_IRM_ROOT_COMMAND", "/grafana").lstrip("/") From d928b6d02f5f15b74c1d1fda88af9bfd64818a3c Mon Sep 17 00:00:00 2001 From: Joey Orlando Date: Thu, 19 Dec 2024 13:43:57 -0500 Subject: [PATCH 5/8] comment about slack wording/placement --- engine/apps/slack/scenarios/paging.py | 12 +++++++----- .../apps/slack/tests/scenario_steps/test_paging.py | 10 +++++----- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/engine/apps/slack/scenarios/paging.py b/engine/apps/slack/scenarios/paging.py index f831510db..7d18cee87 100644 --- a/engine/apps/slack/scenarios/paging.py +++ b/engine/apps/slack/scenarios/paging.py @@ -840,24 +840,26 @@ def _get_team_select_blocks( blocks.extend( [ + team_severity_checkboxes_element, typing.cast( Block.Context, { + # NOTE: we add this here instead of as a checkbox option description because those can only + # be defined as plain text (ie. not markdown where links are supported) "type": "context", "elements": [ { "type": "mrkdwn", "text": ( - "Check the following box if you would like to escalate to this team as an 'important' " - "escalation. This will set a `payload.oncall.important` attribute in the alert to `true`. " - "Teams can configure their Direct Paging Integration to route to different escalation chains " - "based on this. " + "Check the above box if you would like to escalate to this team as an 'important' " + "escalation. Teams can configure their Direct Paging Integration to route to different " + "escalation chains based on this. " + "" ), }, ], }, ), - team_severity_checkboxes_element, ] ) diff --git a/engine/apps/slack/tests/scenario_steps/test_paging.py b/engine/apps/slack/tests/scenario_steps/test_paging.py index ec852150a..e0800ee28 100644 --- a/engine/apps/slack/tests/scenario_steps/test_paging.py +++ b/engine/apps/slack/tests/scenario_steps/test_paging.py @@ -539,7 +539,7 @@ def _setup_direct_paging_integration(team): ) assert len(blocks) == 4 - input_block, context_block, team_severity_context_block, team_severity_checkboxes = blocks + input_block, context_block, team_severity_checkboxes, team_severity_context_block = blocks team_severity_important_checkbox_option = { "text": { @@ -566,10 +566,10 @@ def _sort_team_options(options): ) assert team_severity_context_block["elements"][0]["text"] == ( - "Check the following box if you would like to escalate to this team as an 'important' " - "escalation. This will set a `payload.oncall.important` attribute in the alert to `true`. " - "Teams can configure their Direct Paging Integration to route to different escalation chains " - "based on this. " + "Check the above box if you would like to escalate to this team as an 'important' " + "escalation. Teams can configure their Direct Paging Integration to route to different " + "escalation chains based on this. " + "" ) assert team_severity_checkboxes["accessory"]["type"] == "checkboxes" assert team_severity_checkboxes["accessory"]["options"] == [team_severity_important_checkbox_option] From 0da4d7999d831ca26983df2e6d2cad68f0e5cd84 Mon Sep 17 00:00:00 2001 From: Joey Orlando Date: Fri, 20 Dec 2024 11:59:36 -0500 Subject: [PATCH 6/8] feat: create direct paging integrations with two default routes (+ migrate existing ones) --- .../integrations/references/manual/index.md | 38 ++++++++- ...upsert_direct_paging_integration_routes.py | 84 +++++++++++++++++++ .../alerts/models/alert_receive_channel.py | 51 ++++++++--- .../tests/test_alert_receiver_channel.py | 38 +++++++-- 4 files changed, 187 insertions(+), 24 deletions(-) create mode 100644 engine/apps/alerts/migrations/0072_upsert_direct_paging_integration_routes.py diff --git a/docs/sources/configure/integrations/references/manual/index.md b/docs/sources/configure/integrations/references/manual/index.md index cad437dcf..bcf7fcd8e 100644 --- a/docs/sources/configure/integrations/references/manual/index.md +++ b/docs/sources/configure/integrations/references/manual/index.md @@ -89,9 +89,16 @@ to the team's ChatOps channels and start an appropriate escalation chain. ## Set up direct paging for a team -By default all teams will have a direct paging integration created for them. However, these are not configured by default. -If a team does not have their direct paging integration configured, such that it is "contactable" (ie. it has an -escalation chain assigned to it, or has at least one Chatops integration connected to send notifications to), you will +By default all teams will have a direct paging integration created for them. Each direct paging integration will be +created with two routes: + +- a non-default route which has a Jinja2 filtering term of `{{ payload.oncall.important }}` +(see [Important Escalations](#important-escalations) below for more details) +- a default route to capture all other alerts + +However, these integrations are not configured by default to be "contactable" (ie. their routes will have no +escalation chains assigned to them, nor any Chatops integrations connected to send notifications to). +If a team does not have their direct paging integration configured, such that it is "contactable" , you will not be able to direct page this team. If this happens, consider following the following steps for the team (or reach out to the relevant team and suggest doing so). @@ -102,4 +109,27 @@ and select the same team for a test run. ### Important escalations -TODO: +Sometimes you really need to get the attention of a particular team. When directly paging a team, it is possible to +page them using an "important escalation". Practically speaking, this will create an alert, using the specified team's +direct paging integration as such: + +```json +{ + "oncall": { + "title": "IRM is paging Network team to join escalation", + "message": "I really need someone from your team to come take a look! The k8s cluster is down!", + "uid": "8a20b8d1-56fd-482e-824e-43fbd1bd7b10", + "author_username": "irm", + "permalink": null, + "important": true + } +} +``` + +When you are directly paging a team, either via the web UI, chatops apps, or the API, you can specify that this +esclation be "important", which will effectively set the value of `oncall.important` to `true`. As mentioned above in +[Set up direct paging for a team](#set-up-direct-paging-for-a-team), direct paging integrations come pre-configured with +two routes, with the non-default route having a Jinja2 filtering term of `{{ payload.oncall.important }}`. + +This allows teams to be contacted via different escalation chains, depending on whether or not the user paging them +believes that this is an "important escalation". diff --git a/engine/apps/alerts/migrations/0072_upsert_direct_paging_integration_routes.py b/engine/apps/alerts/migrations/0072_upsert_direct_paging_integration_routes.py new file mode 100644 index 000000000..7345b3c6a --- /dev/null +++ b/engine/apps/alerts/migrations/0072_upsert_direct_paging_integration_routes.py @@ -0,0 +1,84 @@ +# Generated by Django 4.2.17 on 2024-12-20 14:19 + +import logging + +from django.db import migrations +from django.db.models import Count + +logger = logging.getLogger(__name__) + + +def upsert_direct_paging_integration_routes(apps, schema_editor): + AlertReceiveChannel = apps.get_model("alerts", "AlertReceiveChannel") + ChannelFilter = apps.get_model("alerts", "ChannelFilter") + + DIRECT_PAGING_INTEGRATION_TYPE = "direct_paging" + IMPORTANT_FILTERING_TERM = "{{ payload.oncall.important }}" + + # Fetch all direct paging integrations + logger.info("Fetching direct paging integrations which have not had their routes updated.") + + # Ignore updating Direct Paging integrations that have > 1 route, as this means that users have + # gone ahead and created their own routes. We don't want to overwrite these. + unedited_direct_paging_integrations = ( + AlertReceiveChannel.objects + .filter(integration=DIRECT_PAGING_INTEGRATION_TYPE) + .annotate(num_routes=Count("channel_filters")) + .filter(num_routes=1) + ) + + integration_count = unedited_direct_paging_integrations.count() + if integration_count == 0: + logger.info("No integrations found which meet this criteria. No routes will be upserted.") + return + + logger.info(f"Found {integration_count} direct paging integrations that meet this criteria.") + + # Direct Paging Integrations are currently created with a single default route (order=0) + # see AlertReceiveChannelManager.create_missing_direct_paging_integrations + # + # we first need to update this route to be order=1, and then we will subsequently bulk-create the + # non-default route (order=0) which will have a filtering term set + routes = ChannelFilter.objects.filter( + alert_receive_channel__in=unedited_direct_paging_integrations, + is_default=True, + order=0, + ) + + logger.info( + f"Swapping the order=0 value to order=1 for {routes.count()} Direct Paging Integrations default routes" + ) + + updated_rows = routes.update(order=1) + logger.info(f"Swapped order=0 to order=1 for {updated_rows} Direct Paging Integrations default routes") + + # Bulk create the new non-default routes + logger.info( + f"Creating new non-default routes for {len(unedited_direct_paging_integrations)} Direct Paging Integrations" + ) + created_objs = ChannelFilter.objects.bulk_create( + [ + ChannelFilter( + alert_receive_channel=integration, + filtering_term=IMPORTANT_FILTERING_TERM, + filtering_term_type=1, # 1 = ChannelFilter.FILTERING_TERM_TYPE_JINJA2 + is_default=False, + order=0, + ) for integration in unedited_direct_paging_integrations + ], + batch_size=5000, + ) + logger.info(f"Created {len(created_objs)} new non-default routes for Direct Paging Integrations") + + logger.info("Migration for direct paging integration routes completed.") + + +class Migration(migrations.Migration): + + dependencies = [ + ("alerts", "0071_migrate_labels"), + ] + + operations = [ + migrations.RunPython(upsert_direct_paging_integration_routes, migrations.RunPython.noop), + ] diff --git a/engine/apps/alerts/models/alert_receive_channel.py b/engine/apps/alerts/models/alert_receive_channel.py index 74fc5d237..a8089337d 100644 --- a/engine/apps/alerts/models/alert_receive_channel.py +++ b/engine/apps/alerts/models/alert_receive_channel.py @@ -126,6 +126,8 @@ class AlertReceiveChannelManager(models.Manager): def create_missing_direct_paging_integrations(organization: "Organization") -> None: from apps.alerts.models import ChannelFilter + logger.info(f"Starting create_missing_direct_paging_integrations for organization: {organization.id}") + # fetch teams without direct paging integration teams_missing_direct_paging = list( organization.teams.exclude( @@ -134,10 +136,17 @@ def create_missing_direct_paging_integrations(organization: "Organization") -> N ).values_list("team_id", flat=True) ) ) + number_of_teams_missing_direct_paging = len(teams_missing_direct_paging) + logger.info( + f"Found {number_of_teams_missing_direct_paging} teams missing direct paging integrations.", + ) + if not teams_missing_direct_paging: + logger.info("No missing direct paging integrations found. Exiting.") return # create missing integrations + logger.info(f"Creating missing direct paging integrations for {number_of_teams_missing_direct_paging} teams.") AlertReceiveChannel.objects.bulk_create( [ AlertReceiveChannel( @@ -151,29 +160,49 @@ def create_missing_direct_paging_integrations(organization: "Organization") -> N batch_size=5000, ignore_conflicts=True, # ignore if direct paging integration already exists for team ) + logger.info("Missing direct paging integrations creation step completed.") # fetch integrations for teams (some of them are created above, but some may already exist previously) alert_receive_channels = organization.alert_receive_channels.filter( team__in=teams_missing_direct_paging, integration=AlertReceiveChannel.INTEGRATION_DIRECT_PAGING ) + logger.info(f"Fetched {alert_receive_channels.count()} direct paging integrations for the specified teams.") + + # we create two routes for each Direct Paging Integration + # 1. route for important alerts (using the payload.oncall.important alert field value) - non-default + # 2. route for all other alerts - default + routes_to_create = [] + for alert_receive_channel in alert_receive_channels: + routes_to_create.extend( + [ + ChannelFilter( + alert_receive_channel=alert_receive_channel, + filtering_term="{{ payload.oncall.important }}", + filtering_term_type=ChannelFilter.FILTERING_TERM_TYPE_JINJA2, + is_default=False, + order=0, + ), + ChannelFilter( + alert_receive_channel=alert_receive_channel, + filtering_term=None, + is_default=True, + order=1, + ), + ] + ) - # create default routes + logger.info(f"Creating {len(routes_to_create)} channel filter routes.") ChannelFilter.objects.bulk_create( - [ - ChannelFilter( - alert_receive_channel=alert_receive_channel, - filtering_term=None, - is_default=True, - order=0, - ) - for alert_receive_channel in alert_receive_channels - ], + routes_to_create, batch_size=5000, - ignore_conflicts=True, # ignore if default route already exists for integration + ignore_conflicts=True, # ignore if routes already exist for integration ) + logger.info("Direct paging routes creation completed.") # add integrations to metrics cache + logger.info("Adding integrations to metrics cache.") metrics_add_integrations_to_cache(list(alert_receive_channels), organization) + logger.info("Integrations have been added to the metrics cache.") def get_queryset(self): return AlertReceiveChannelQueryset(self.model, using=self._db).filter( diff --git a/engine/apps/alerts/tests/test_alert_receiver_channel.py b/engine/apps/alerts/tests/test_alert_receiver_channel.py index 930239826..d1f6dc394 100644 --- a/engine/apps/alerts/tests/test_alert_receiver_channel.py +++ b/engine/apps/alerts/tests/test_alert_receiver_channel.py @@ -259,27 +259,47 @@ def test_create_missing_direct_paging_integrations( ): organization = make_organization() - # team with no direct paging integration + # two teams with no direct paging integration team1 = make_team(organization) + team2 = make_team(organization) # team with direct paging integration - team2 = make_team(organization) + team3 = make_team(organization) alert_receive_channel = make_alert_receive_channel( - organization, team=team2, integration=AlertReceiveChannel.INTEGRATION_DIRECT_PAGING + organization, team=team3, integration=AlertReceiveChannel.INTEGRATION_DIRECT_PAGING ) make_channel_filter(alert_receive_channel, is_default=True, order=0) # create missing direct paging integration for organization AlertReceiveChannel.objects.create_missing_direct_paging_integrations(organization) + assert organization.alert_receive_channels.count() == 3 + # check that missing integrations and default routes were created - assert organization.alert_receive_channels.count() == 2 - mock_metrics_add_integrations_to_cache.assert_called_once() + # + # NOTE: we explicitly don't test team3, it already has a Direct Paging integraiton associated with it + # and AlertReceiveChannel.objects.create_missing_direct_paging_integrations is not responsible for filling + # in missing routes. + # + # See apps/alerts/migrations/0072_upsert_direct_paging_integration_routes.py which is a data migration that does + # exactly this. for team in [team1, team2]: - alert_receive_channel = organization.alert_receive_channels.get( - team=team, integration=AlertReceiveChannel.INTEGRATION_DIRECT_PAGING - ) - assert alert_receive_channel.channel_filters.get().is_default + alert_receive_channel = organization.alert_receive_channels.get(team=team) + + direct_paging_integration_routes = alert_receive_channel.channel_filters.all() + + assert direct_paging_integration_routes.count() == 2 + + for route in direct_paging_integration_routes: + if route.is_default: + assert route.order == 1 + assert route.filtering_term is None + else: + assert route.order == 0 + assert route.filtering_term == "{{ payload.oncall.important }}" + assert route.filtering_term_type == route.FILTERING_TERM_TYPE_JINJA2 + + mock_metrics_add_integrations_to_cache.assert_called_once() @pytest.mark.django_db From d624c7f7e54b651a7aa936ad1430d1cbe26dedef Mon Sep 17 00:00:00 2001 From: Joey Orlando Date: Fri, 20 Dec 2024 12:15:33 -0500 Subject: [PATCH 7/8] update `test_sync_teams_for_organization` test --- .../apps/user_management/tests/test_sync.py | 24 +++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/engine/apps/user_management/tests/test_sync.py b/engine/apps/user_management/tests/test_sync.py index e381c62a4..3f5bdd9a5 100644 --- a/engine/apps/user_management/tests/test_sync.py +++ b/engine/apps/user_management/tests/test_sync.py @@ -203,23 +203,33 @@ def test_sync_teams_for_organization(make_organization, make_team, make_alert_re assert created_team.team_id == api_teams[2]["id"] assert created_team.name == api_teams[2]["name"] + def _assert_teams_direct_paging_integration_is_configured_properly(integration): + assert integration.channel_filters.count() == 2 + + for route in integration.channel_filters.all(): + if route.is_default: + assert route.order == 1 + assert route.filtering_term is None + else: + assert route.order == 0 + assert route.filtering_term == "{{ payload.oncall.important }}" + assert route.filtering_term_type == route.FILTERING_TERM_TYPE_JINJA2 + # check that direct paging is created for created team direct_paging_integration = AlertReceiveChannel.objects.get( organization=organization, integration=AlertReceiveChannel.INTEGRATION_DIRECT_PAGING, team=created_team, ) - assert direct_paging_integration.channel_filters.count() == 1 - assert direct_paging_integration.channel_filters.first().order == 0 - assert direct_paging_integration.channel_filters.first().is_default + _assert_teams_direct_paging_integration_is_configured_properly(direct_paging_integration) # check that direct paging is created for existing team direct_paging_integration = AlertReceiveChannel.objects.get( - organization=organization, integration=AlertReceiveChannel.INTEGRATION_DIRECT_PAGING, team=teams[2] + organization=organization, + integration=AlertReceiveChannel.INTEGRATION_DIRECT_PAGING, + team=teams[2], ) - assert direct_paging_integration.channel_filters.count() == 1 - assert direct_paging_integration.channel_filters.first().order == 0 - assert direct_paging_integration.channel_filters.first().is_default + _assert_teams_direct_paging_integration_is_configured_properly(direct_paging_integration) @pytest.mark.django_db From 8d039686a635686aa96f5aa49b5823cc5bf5d1ac Mon Sep 17 00:00:00 2001 From: Joey Orlando Date: Mon, 6 Jan 2025 10:30:02 -0500 Subject: [PATCH 8/8] Update docs/sources/oncall-api-reference/escalation.md Co-authored-by: Vadim Stepanov --- docs/sources/oncall-api-reference/escalation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/oncall-api-reference/escalation.md b/docs/sources/oncall-api-reference/escalation.md index e8c826d1f..a07547e2a 100644 --- a/docs/sources/oncall-api-reference/escalation.md +++ b/docs/sources/oncall-api-reference/escalation.md @@ -96,7 +96,7 @@ curl "{{API_URL}}/api/v1/escalation/" \ "message": "I need help investigating, can you join the investigation?", "source_url": "https://github.com/myorg/myrepo/issues/123", "team": "TI73TDU19W48J", - "important_team_escalation": True + "important_team_escalation": true }' ```