Skip to content

Commit

Permalink
Revert "chore(slack): correctly type issue message builder (#74876)"
Browse files Browse the repository at this point in the history
This reverts commit 1d9d37b.

Co-authored-by: cathteng <70817427+cathteng@users.noreply.github.com>
  • Loading branch information
2 people authored and roaga committed Jul 31, 2024
1 parent 1b9b392 commit d85728c
Show file tree
Hide file tree
Showing 8 changed files with 112 additions and 87 deletions.
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,7 @@ module = [
"sentry.integrations.pipeline",
"sentry.integrations.slack.actions.form",
"sentry.integrations.slack.integration",
"sentry.integrations.slack.message_builder.issues",
"sentry.integrations.slack.message_builder.notifications.digest",
"sentry.integrations.slack.message_builder.notifications.issues",
"sentry.integrations.slack.notifications",
Expand Down
16 changes: 9 additions & 7 deletions src/sentry/integrations/message_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,17 @@


def format_actor_options(
actors: Sequence[Team | RpcUser], is_slack: bool = False
actors: Sequence[Team | RpcUser], use_block_kit: bool = False
) -> Sequence[Mapping[str, str]]:
sort_func: Callable[[Mapping[str, str]], Any] = lambda actor: actor["text"]
if is_slack:
if use_block_kit:
sort_func = lambda actor: actor["text"]["text"]
return sorted((format_actor_option(actor, is_slack) for actor in actors), key=sort_func)
return sorted((format_actor_option(actor, use_block_kit) for actor in actors), key=sort_func)


def format_actor_option(actor: Team | RpcUser, is_slack: bool = False) -> Mapping[str, str]:
def format_actor_option(actor: Team | RpcUser, use_block_kit: bool = False) -> Mapping[str, str]:
if isinstance(actor, RpcUser):
if is_slack:
if use_block_kit:
return {
"text": {
"type": "plain_text",
Expand All @@ -40,8 +40,8 @@ def format_actor_option(actor: Team | RpcUser, is_slack: bool = False) -> Mappin
}

return {"text": actor.get_display_name(), "value": f"user:{actor.id}"}
elif isinstance(actor, Team):
if is_slack:
if isinstance(actor, Team):
if use_block_kit:
return {
"text": {
"type": "plain_text",
Expand All @@ -51,6 +51,8 @@ def format_actor_option(actor: Team | RpcUser, is_slack: bool = False) -> Mappin
}
return {"text": f"#{actor.slug}", "value": f"team:{actor.id}"}

raise NotImplementedError


def build_attachment_title(obj: Group | GroupEvent) -> str:
ev_metadata = obj.get_event_metadata()
Expand Down
119 changes: 75 additions & 44 deletions src/sentry/integrations/slack/message_builder/issues.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@
import logging
from collections.abc import Mapping, Sequence
from datetime import datetime
from typing import Any, TypedDict
from typing import Any

import orjson
from django.core.exceptions import ObjectDoesNotExist
from sentry_relay.processing import parse_release

from sentry import tagstore
from sentry.api.endpoints.group_details import get_group_global_count
from sentry.constants import LOG_LEVELS
from sentry.constants import LOG_LEVELS_MAP
from sentry.eventstore.models import GroupEvent
from sentry.identity.services.identity import RpcIdentity, identity_service
from sentry.integrations.message_builder import (
Expand Down Expand Up @@ -109,9 +109,9 @@ def build_assigned_text(identity: RpcIdentity, assignee: str) -> str | None:
except ObjectDoesNotExist:
return None

if isinstance(assigned_actor, Team):
if actor.is_team:
assignee_text = f"#{assigned_actor.slug}"
elif isinstance(assigned_actor, RpcUser):
elif actor.is_user:
assignee_identity = identity_service.get_identity(
filter={
"provider_id": identity.idp_id,
Expand Down Expand Up @@ -147,20 +147,40 @@ def build_action_text(identity: RpcIdentity, action: MessageAction) -> str | Non
return f"*Issue {status} by <@{identity.external_id}>*"


def format_release_tag(value: str, event: GroupEvent | None) -> str:
"""Format the release tag using the short version and make it a link"""
if not event:
return ""
def build_tag_fields(
event_for_tags: Any, tags: set[str] | None = None
) -> Sequence[Mapping[str, str | bool]]:
fields = []
if tags:
event_tags = event_for_tags.tags if event_for_tags else []
for key, value in event_tags:
std_key = tagstore.backend.get_standardized_key(key)
if std_key not in tags:
continue

labeled_value = tagstore.backend.get_tag_value_label(key, value)
fields.append(
{
"title": std_key.encode("utf-8"),
"value": labeled_value.encode("utf-8"),
"short": True,
}
)
return fields


def format_release_tag(value: str, event: GroupEvent | Group):
"""Format the release tag using the short version and make it a link"""
path = f"/releases/{value}/"
url = event.project.organization.absolute_url(path)
release_description = parse_release(value, json_loads=orjson.loads).get("description")
return f"<{url}|{release_description}>"


def get_tags(
event_for_tags: GroupEvent | None,
tags: set[str] | list[tuple[str]] | None = None,
group: Group,
event_for_tags: Any,
tags: set[str] | None = None,
) -> Sequence[Mapping[str, str | bool]]:
"""Get tag keys and values for block kit"""
fields = []
Expand Down Expand Up @@ -223,30 +243,41 @@ def get_context(group: Group) -> str:
return context_text.rstrip()


class OptionGroup(TypedDict):
label: Mapping[str, str]
options: Sequence[Mapping[str, Any]]
def get_option_groups_block_kit(group: Group) -> Sequence[Mapping[str, Any]]:
all_members = group.project.get_members_as_rpc_users()
members = list({m.id: m for m in all_members}.values())
teams = group.project.teams.all()

option_groups = []
if teams:
team_options = format_actor_options(teams, True)
option_groups.append(
{"label": {"type": "plain_text", "text": "Teams"}, "options": team_options}
)

if members:
member_options = format_actor_options(members, True)
option_groups.append(
{"label": {"type": "plain_text", "text": "People"}, "options": member_options}
)
return option_groups

def get_option_groups(group: Group) -> Sequence[OptionGroup]:

def get_group_assignees(group: Group) -> Sequence[Mapping[str, Any]]:
"""Get teams and users that can be issue assignees for block kit"""
all_members = group.project.get_members_as_rpc_users()
members = list({m.id: m for m in all_members}.values())
teams = group.project.teams.all()

option_groups = []
if teams:
team_option_group: OptionGroup = {
"label": {"type": "plain_text", "text": "Teams"},
"options": format_actor_options(teams, True),
}
option_groups.append(team_option_group)
for team in teams:
option_groups.append({"label": team.slug, "value": f"team:{team.id}"})

if members:
member_option_group: OptionGroup = {
"label": {"type": "plain_text", "text": "People"},
"options": format_actor_options(members, True),
}
option_groups.append(member_option_group)
for member in members:
option_groups.append({"label": member.email, "value": f"user:{member.id}"})

return option_groups


Expand All @@ -267,23 +298,20 @@ def get_suggested_assignees(
logger.info("Skipping suspect committers because release does not exist.")
except Exception:
logger.exception("Could not get suspect committers. Continuing execution.")

if suggested_assignees:
suggested_assignees = dedupe_suggested_assignees(suggested_assignees)
assignee_texts = []

for assignee in suggested_assignees:
# skip over any suggested assignees that are the current assignee of the issue, if there is any
if assignee.is_team and not (
isinstance(current_assignee, Team) and assignee.id == current_assignee.id
):
assignee_texts.append(f"#{assignee.slug}")
elif assignee.is_user and not (
if assignee.is_user and not (
isinstance(current_assignee, RpcUser) and assignee.id == current_assignee.id
):
assignee_as_user = assignee.resolve()
if isinstance(assignee_as_user, RpcUser):
assignee_texts.append(assignee_as_user.get_display_name())
assignee_texts.append(assignee_as_user.get_display_name())
elif assignee.is_team and not (
isinstance(current_assignee, Team) and assignee.id == current_assignee.id
):
assignee_texts.append(f"#{assignee.slug}")
return assignee_texts
return []

Expand Down Expand Up @@ -389,7 +417,7 @@ def _assign_button() -> MessageAction:
label="Select Assignee...",
type="select",
selected_options=format_actor_options([assignee], True) if assignee else [],
option_groups=get_option_groups(group),
option_groups=get_option_groups_block_kit(group),
)
return assign_button

Expand Down Expand Up @@ -449,10 +477,10 @@ def escape_text(self) -> bool:

def get_title_block(
self,
rule_id: int,
notification_uuid: str,
event_or_group: GroupEvent | Group,
has_action: bool,
rule_id: int | None = None,
notification_uuid: str | None = None,
) -> SlackBlock:
title_link = get_title_link(
self.group,
Expand All @@ -476,7 +504,11 @@ def get_title_block(
else ACTIONED_CATEGORY_TO_EMOJI.get(self.group.issue_category)
)
elif is_error_issue:
level_text = LOG_LEVELS[self.group.level]
level_text = None
for k, v in LOG_LEVELS_MAP.items():
if self.group.level == v:
level_text = k

title_emoji = LEVEL_TO_EMOJI.get(level_text)
else:
title_emoji = CATEGORY_TO_EMOJI.get(self.group.issue_category)
Expand Down Expand Up @@ -552,8 +584,7 @@ def build(self, notification_uuid: str | None = None) -> SlackBlock:
# If an event is unspecified, use the tags of the latest event (if one exists).
event_for_tags = self.event or self.group.get_latest_event()

event_or_group: Group | GroupEvent = self.event if self.event is not None else self.group

obj = self.event if self.event is not None else self.group
action_text = ""

if not self.issue_details or (self.recipient and self.recipient.is_team):
Expand All @@ -574,9 +605,9 @@ def build(self, notification_uuid: str | None = None) -> SlackBlock:
action_text = get_action_text(self.actions, self.identity)
has_action = True

blocks = [self.get_title_block(event_or_group, has_action, rule_id, notification_uuid)]
blocks = [self.get_title_block(rule_id, notification_uuid, obj, has_action)]

if culprit_block := self.get_culprit_block(event_or_group):
if culprit_block := self.get_culprit_block(obj):
blocks.append(culprit_block)

# build up text block
Expand All @@ -589,7 +620,7 @@ def build(self, notification_uuid: str | None = None) -> SlackBlock:
blocks.append(self.get_markdown_block(action_text))

# build tags block
tags = get_tags(event_for_tags, self.tags)
tags = get_tags(self.group, event_for_tags, self.tags)
if tags:
blocks.append(self.get_tags_block(tags))

Expand Down Expand Up @@ -656,7 +687,7 @@ def build(self, notification_uuid: str | None = None) -> SlackBlock:

return self._build_blocks(
*blocks,
fallback_text=self.build_fallback_text(event_or_group, project.slug),
block_id=block_id,
fallback_text=self.build_fallback_text(obj, project.slug),
block_id=orjson.dumps(block_id).decode(),
skip_fallback=self.skip_fallback,
)
25 changes: 9 additions & 16 deletions src/sentry/integrations/slack/webhooks/options_load.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import re
from collections.abc import Mapping, Sequence
from typing import Any, TypedDict
from typing import Any

import orjson
from rest_framework import status
Expand All @@ -20,11 +20,6 @@
from ..utils import logger


class OptionGroup(TypedDict):
label: Mapping[str, str]
options: Sequence[Mapping[str, Any]]


@region_silo_endpoint
class SlackOptionsLoadEndpoint(Endpoint):
owner = ApiOwner.ECOSYSTEM
Expand Down Expand Up @@ -74,17 +69,15 @@ def get_filtered_option_groups(

option_groups = []
if filtered_teams:
team_options_group: OptionGroup = {
"label": {"type": "plain_text", "text": "Teams"},
"options": format_actor_options(filtered_teams, True),
}
option_groups.append(team_options_group)
team_options = format_actor_options(filtered_teams, True)
option_groups.append(
{"label": {"type": "plain_text", "text": "Teams"}, "options": team_options}
)
if filtered_members:
member_options_group: OptionGroup = {
"label": {"type": "plain_text", "text": "People"},
"options": format_actor_options(filtered_members, True),
}
option_groups.append(member_options_group)
member_options = format_actor_options(filtered_members, True)
option_groups.append(
{"label": {"type": "plain_text", "text": "People"}, "options": member_options}
)
return option_groups

# XXX(isabella): atm this endpoint is used only for the assignment dropdown on issue alerts
Expand Down
5 changes: 2 additions & 3 deletions src/sentry/notifications/utils/participants.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
from django.db.models import Q

from sentry import features
from sentry.eventstore.models import GroupEvent
from sentry.integrations.types import ExternalProviders
from sentry.integrations.utils.providers import get_provider_enum_from_string
from sentry.models.commit import Commit
Expand Down Expand Up @@ -263,7 +262,7 @@ def get_owner_reason(
return None


def get_suspect_commit_users(project: Project, event: Event | GroupEvent) -> list[RpcUser]:
def get_suspect_commit_users(project: Project, event: Event) -> list[RpcUser]:
"""
Returns a list of users that are suspect committers for the given event.
Expand All @@ -286,7 +285,7 @@ def get_suspect_commit_users(project: Project, event: Event | GroupEvent) -> lis
return [committer for committer in suspect_committers if committer.id in in_project_user_ids]


def dedupe_suggested_assignees(suggested_assignees: Iterable[Actor]) -> list[Actor]:
def dedupe_suggested_assignees(suggested_assignees: Iterable[Actor]) -> Iterable[Actor]:
return list({assignee.id: assignee for assignee in suggested_assignees}.values())


Expand Down
2 changes: 1 addition & 1 deletion src/sentry/utils/committers.py
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,7 @@ def get_event_file_committers(


def get_serialized_event_file_committers(
project: Project, event: Event | GroupEvent, frame_limit: int = 25
project: Project, event: Event, frame_limit: int = 25
) -> Sequence[AuthorCommitsSerialized]:

group_owners = GroupOwner.objects.filter(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@
old_get_tags = get_tags


def fake_get_tags(event_for_tags, tags):
return old_get_tags(event_for_tags, None)
def fake_get_tags(group, event_for_tags, tags):
return old_get_tags(group, event_for_tags, None)


class SlackIssueAlertNotificationTest(SlackActivityNotificationTest, PerformanceIssueTestCase):
Expand Down
Loading

0 comments on commit d85728c

Please sign in to comment.