Skip to content

Commit

Permalink
feat(escalating-issues): Allow slack messages to archive until escala…
Browse files Browse the repository at this point in the history
…ting (#49035)

## Objective
This PR allows users to archive issues from Slack. I also updated the
logic to support the Group substatus even for orgs that do not have the
escalating feature flag.
  • Loading branch information
NisanthanNanthakumar authored May 16, 2023
1 parent 9df6fcd commit 0eaa441
Show file tree
Hide file tree
Showing 4 changed files with 100 additions and 18 deletions.
28 changes: 16 additions & 12 deletions src/sentry/integrations/slack/message_builder/issues.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,9 @@ def build_assigned_text(identity: RpcIdentity, assignee: str) -> str | None:
return f"*Issue assigned to {assignee_text} by <@{identity.external_id}>*"


def build_action_text(identity: RpcIdentity, action: MessageAction) -> str | None:
def build_action_text(
identity: RpcIdentity, action: MessageAction, has_escalating: bool = False
) -> str | None:
if action.name == "assign":
selected_options = action.selected_options or []
if not len(selected_options):
Expand All @@ -65,7 +67,7 @@ def build_action_text(identity: RpcIdentity, action: MessageAction) -> str | Non

# Resolve actions have additional 'parameters' after ':'
status = STATUSES.get((action.value or "").split(":", 1)[0])

status = "archived" if status == "ignored" and has_escalating else status
# Action has no valid action text, ignore
if not status:
return None
Expand Down Expand Up @@ -123,17 +125,17 @@ def has_releases(project: Project) -> bool:


def get_action_text(
text: str,
actions: Sequence[Any],
identity: RpcIdentity,
text: str, actions: Sequence[Any], identity: RpcIdentity, has_escalating: bool = False
) -> str:
return (
text
+ "\n"
+ "\n".join(
[
action_text
for action_text in [build_action_text(identity, action) for action in actions]
for action_text in [
build_action_text(identity, action, has_escalating) for action in actions
]
if action_text
]
)
Expand All @@ -149,14 +151,16 @@ def build_actions(
identity: RpcIdentity | None = None,
) -> tuple[Sequence[MessageAction], str, str]:
"""Having actions means a button will be shown on the Slack message e.g. ignore, resolve, assign."""
has_escalating = features.has("organizations:escalating-issues", project.organization)

if actions and identity:
text = get_action_text(text, actions, identity)
text = get_action_text(text, actions, identity, has_escalating)
return [], text, "_actioned_issue"

ignore_button = MessageAction(
name="status",
label="Ignore",
value="ignored",
label="Archive" if has_escalating else "Ignore",
value="ignored:until_escalating" if has_escalating else "ignored:forever",
)

resolve_button = MessageAction(
Expand All @@ -178,14 +182,14 @@ def build_actions(
resolve_button = MessageAction(
name="status",
label="Unresolve",
value="unresolved",
value="unresolved:ongoing",
)

if status == GroupStatus.IGNORED:
ignore_button = MessageAction(
name="status",
label="Stop Ignoring",
value="unresolved",
label="Mark as Ongoing" if has_escalating else "Stop Ignoring",
value="unresolved:ongoing",
)

assignee = group.get_assignee()
Expand Down
7 changes: 6 additions & 1 deletion src/sentry/integrations/slack/webhooks/action.py
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,12 @@ def on_status(
if not len(status_data):
return

status: MutableMapping[str, Any] = {"status": status_data[0]}
status: MutableMapping[str, Any] = {
"status": status_data[0],
}

if len(status_data) > 1:
status["substatus"] = status_data[1]

resolve_type = status_data[-1]

Expand Down
22 changes: 21 additions & 1 deletion tests/sentry/integrations/slack/test_message_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ def build_test_message(
"color": "#E03E2F", # red for error level
"actions": [
{"name": "status", "text": "Resolve", "type": "button", "value": "resolved"},
{"name": "status", "text": "Ignore", "type": "button", "value": "ignored"},
{"name": "status", "text": "Ignore", "type": "button", "value": "ignored:forever"},
{
"option_groups": [
{
Expand Down Expand Up @@ -137,6 +137,26 @@ def test_build_group_attachment(self):
link_to_event=True,
)

with self.feature("organizations:escalating-issues"):
test_message = build_test_message(
teams={self.team},
users={self.user},
timestamp=group.last_seen,
group=group,
)
test_message["actions"] = [
action
if action["text"] != "Ignore"
else {
"name": "status",
"text": "Archive",
"type": "button",
"value": "ignored:until_escalating",
}
for action in test_message["actions"]
]
assert SlackIssuesMessageBuilder(group).build() == test_message

@patch(
"sentry.integrations.slack.message_builder.issues.get_option_groups",
wraps=get_option_groups,
Expand Down
61 changes: 57 additions & 4 deletions tests/sentry/integrations/slack/webhooks/actions/test_status.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
Group,
GroupAssignee,
GroupStatus,
GroupSubStatus,
Identity,
InviteStatus,
OrganizationMember,
Expand Down Expand Up @@ -58,7 +59,7 @@ def test_ignore_issue(self):
},
project_id=self.project.id,
)
status_action = {"name": "status", "value": "ignored", "type": "button"}
status_action = {"name": "status", "value": "ignored:forever", "type": "button"}
original_message = {
"type": "message",
"attachments": [
Expand All @@ -84,6 +85,55 @@ def test_ignore_issue(self):

assert resp.status_code == 200, resp.content
assert self.group.get_status() == GroupStatus.IGNORED
assert self.group.substatus == GroupSubStatus.FOREVER

expect_status = f"Identity not found.\n*Issue ignored by <@{self.external_id}>*"
assert resp.data["attachments"][0]["text"] == expect_status

def test_archive_issue(self):
event = self.store_event(
data={
"event_id": "a" * 32,
"message": "IntegrationError",
"fingerprint": ["group-1"],
"exception": {
"values": [
{
"type": "IntegrationError",
"value": "Identity not found.",
}
]
},
},
project_id=self.project.id,
)
status_action = {"name": "status", "value": "ignored:until_escalating", "type": "button"}
original_message = {
"type": "message",
"attachments": [
{
"id": 1,
"ts": 1681409875,
"color": "E03E2F",
"fallback": "[node] IntegrationError: Identity not found.",
"text": "Identity not found.",
"title": "IntegrationError",
"footer": "NODE-F via <http://localhost:8000/organizations/sentry/alerts/rules/node/3/details/|New Issue in #critical channel>",
"mrkdwn_in": ["text"],
}
],
}
resp = self.post_webhook(
action_data=[status_action],
original_message=original_message,
type="interactive_message",
callback_id=json.dumps({"issue": event.group.id}),
)
self.group = Group.objects.get(id=event.group.id)

assert resp.status_code == 200, resp.content
assert self.group.get_status() == GroupStatus.IGNORED
assert self.group.substatus == GroupSubStatus.UNTIL_ESCALATING

expect_status = f"Identity not found.\n*Issue ignored by <@{self.external_id}>*"
assert resp.data["attachments"][0]["text"] == expect_status
Expand All @@ -98,13 +148,14 @@ def test_ignore_issue_with_additional_user_auth(self):
)
AuthIdentity.objects.create(auth_provider=auth_idp, user=self.user)

status_action = {"name": "status", "value": "ignored", "type": "button"}
status_action = {"name": "status", "value": "ignored:forever", "type": "button"}

resp = self.post_webhook(action_data=[status_action])
self.group = Group.objects.get(id=self.group.id)

assert resp.status_code == 200, resp.content
assert self.group.get_status() == GroupStatus.IGNORED
assert self.group.substatus == GroupSubStatus.FOREVER

expect_status = f"*Issue ignored by <@{self.external_id}>*"
assert resp.data["text"].endswith(expect_status), resp.data["text"]
Expand Down Expand Up @@ -195,13 +246,15 @@ def test_assign_issue_user_has_identity(self):
assert resp.data["text"].endswith(expect_status), resp.data["text"]

def test_response_differs_on_bot_message(self):
status_action = {"name": "status", "value": "ignored", "type": "button"}
status_action = {"name": "status", "value": "ignored:forever", "type": "button"}

original_message = {"type": "message"}

resp = self.post_webhook(action_data=[status_action], original_message=original_message)
self.group = Group.objects.get(id=self.group.id)

assert self.group.get_status() == GroupStatus.IGNORED
assert self.group.substatus == GroupSubStatus.FOREVER
assert resp.status_code == 200, resp.content
assert "attachments" in resp.data
assert resp.data["attachments"][0]["title"] == self.group.title
Expand Down Expand Up @@ -298,7 +351,7 @@ def test_permission_denied(self):
user=user2,
)

status_action = {"name": "status", "value": "ignored", "type": "button"}
status_action = {"name": "status", "value": "ignored:forever", "type": "button"}

resp = self.post_webhook(
action_data=[status_action], slack_user={"id": user2_identity.external_id}
Expand Down

0 comments on commit 0eaa441

Please sign in to comment.