Skip to content

Commit 1d9d37b

Browse files
authored
chore(slack): correctly type issue message builder (#74876)
1 parent 718f63f commit 1d9d37b

File tree

8 files changed

+87
-112
lines changed

8 files changed

+87
-112
lines changed

pyproject.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -300,7 +300,6 @@ module = [
300300
"sentry.integrations.pipeline",
301301
"sentry.integrations.slack.actions.form",
302302
"sentry.integrations.slack.integration",
303-
"sentry.integrations.slack.message_builder.issues",
304303
"sentry.integrations.slack.message_builder.notifications.digest",
305304
"sentry.integrations.slack.message_builder.notifications.issues",
306305
"sentry.integrations.slack.notifications",

src/sentry/integrations/message_builder.py

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,17 @@
2020

2121

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

3030

31-
def format_actor_option(actor: Team | RpcUser, use_block_kit: bool = False) -> Mapping[str, str]:
31+
def format_actor_option(actor: Team | RpcUser, is_slack: bool = False) -> Mapping[str, str]:
3232
if isinstance(actor, RpcUser):
33-
if use_block_kit:
33+
if is_slack:
3434
return {
3535
"text": {
3636
"type": "plain_text",
@@ -40,8 +40,8 @@ def format_actor_option(actor: Team | RpcUser, use_block_kit: bool = False) -> M
4040
}
4141

4242
return {"text": actor.get_display_name(), "value": f"user:{actor.id}"}
43-
if isinstance(actor, Team):
44-
if use_block_kit:
43+
elif isinstance(actor, Team):
44+
if is_slack:
4545
return {
4646
"text": {
4747
"type": "plain_text",
@@ -51,8 +51,6 @@ def format_actor_option(actor: Team | RpcUser, use_block_kit: bool = False) -> M
5151
}
5252
return {"text": f"#{actor.slug}", "value": f"team:{actor.id}"}
5353

54-
raise NotImplementedError
55-
5654

5755
def build_attachment_title(obj: Group | GroupEvent) -> str:
5856
ev_metadata = obj.get_event_metadata()

src/sentry/integrations/slack/message_builder/issues.py

Lines changed: 44 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,15 @@
33
import logging
44
from collections.abc import Mapping, Sequence
55
from datetime import datetime
6-
from typing import Any
6+
from typing import Any, TypedDict
77

88
import orjson
99
from django.core.exceptions import ObjectDoesNotExist
1010
from sentry_relay.processing import parse_release
1111

1212
from sentry import tagstore
1313
from sentry.api.endpoints.group_details import get_group_global_count
14-
from sentry.constants import LOG_LEVELS_MAP
14+
from sentry.constants import LOG_LEVELS
1515
from sentry.eventstore.models import GroupEvent
1616
from sentry.identity.services.identity import RpcIdentity, identity_service
1717
from sentry.integrations.message_builder import (
@@ -109,9 +109,9 @@ def build_assigned_text(identity: RpcIdentity, assignee: str) -> str | None:
109109
except ObjectDoesNotExist:
110110
return None
111111

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

149149

150-
def build_tag_fields(
151-
event_for_tags: Any, tags: set[str] | None = None
152-
) -> Sequence[Mapping[str, str | bool]]:
153-
fields = []
154-
if tags:
155-
event_tags = event_for_tags.tags if event_for_tags else []
156-
for key, value in event_tags:
157-
std_key = tagstore.backend.get_standardized_key(key)
158-
if std_key not in tags:
159-
continue
160-
161-
labeled_value = tagstore.backend.get_tag_value_label(key, value)
162-
fields.append(
163-
{
164-
"title": std_key.encode("utf-8"),
165-
"value": labeled_value.encode("utf-8"),
166-
"short": True,
167-
}
168-
)
169-
return fields
170-
171-
172-
def format_release_tag(value: str, event: GroupEvent | Group):
150+
def format_release_tag(value: str, event: GroupEvent | None) -> str:
173151
"""Format the release tag using the short version and make it a link"""
152+
if not event:
153+
return ""
154+
174155
path = f"/releases/{value}/"
175156
url = event.project.organization.absolute_url(path)
176157
release_description = parse_release(value, json_loads=orjson.loads).get("description")
177158
return f"<{url}|{release_description}>"
178159

179160

180161
def get_tags(
181-
group: Group,
182-
event_for_tags: Any,
183-
tags: set[str] | None = None,
162+
event_for_tags: GroupEvent | None,
163+
tags: set[str] | list[tuple[str]] | None = None,
184164
) -> Sequence[Mapping[str, str | bool]]:
185165
"""Get tag keys and values for block kit"""
186166
fields = []
@@ -243,41 +223,30 @@ def get_context(group: Group) -> str:
243223
return context_text.rstrip()
244224

245225

246-
def get_option_groups_block_kit(group: Group) -> Sequence[Mapping[str, Any]]:
247-
all_members = group.project.get_members_as_rpc_users()
248-
members = list({m.id: m for m in all_members}.values())
249-
teams = group.project.teams.all()
250-
251-
option_groups = []
252-
if teams:
253-
team_options = format_actor_options(teams, True)
254-
option_groups.append(
255-
{"label": {"type": "plain_text", "text": "Teams"}, "options": team_options}
256-
)
226+
class OptionGroup(TypedDict):
227+
label: Mapping[str, str]
228+
options: Sequence[Mapping[str, Any]]
257229

258-
if members:
259-
member_options = format_actor_options(members, True)
260-
option_groups.append(
261-
{"label": {"type": "plain_text", "text": "People"}, "options": member_options}
262-
)
263-
return option_groups
264230

265-
266-
def get_group_assignees(group: Group) -> Sequence[Mapping[str, Any]]:
267-
"""Get teams and users that can be issue assignees for block kit"""
231+
def get_option_groups(group: Group) -> Sequence[OptionGroup]:
268232
all_members = group.project.get_members_as_rpc_users()
269233
members = list({m.id: m for m in all_members}.values())
270234
teams = group.project.teams.all()
271235

272236
option_groups = []
273237
if teams:
274-
for team in teams:
275-
option_groups.append({"label": team.slug, "value": f"team:{team.id}"})
238+
team_option_group: OptionGroup = {
239+
"label": {"type": "plain_text", "text": "Teams"},
240+
"options": format_actor_options(teams, True),
241+
}
242+
option_groups.append(team_option_group)
276243

277244
if members:
278-
for member in members:
279-
option_groups.append({"label": member.email, "value": f"user:{member.id}"})
280-
245+
member_option_group: OptionGroup = {
246+
"label": {"type": "plain_text", "text": "People"},
247+
"options": format_actor_options(members, True),
248+
}
249+
option_groups.append(member_option_group)
281250
return option_groups
282251

283252

@@ -298,20 +267,23 @@ def get_suggested_assignees(
298267
logger.info("Skipping suspect committers because release does not exist.")
299268
except Exception:
300269
logger.exception("Could not get suspect committers. Continuing execution.")
270+
301271
if suggested_assignees:
302272
suggested_assignees = dedupe_suggested_assignees(suggested_assignees)
303273
assignee_texts = []
274+
304275
for assignee in suggested_assignees:
305276
# skip over any suggested assignees that are the current assignee of the issue, if there is any
306-
if assignee.is_user and not (
307-
isinstance(current_assignee, RpcUser) and assignee.id == current_assignee.id
308-
):
309-
assignee_as_user = assignee.resolve()
310-
assignee_texts.append(assignee_as_user.get_display_name())
311-
elif assignee.is_team and not (
277+
if assignee.is_team and not (
312278
isinstance(current_assignee, Team) and assignee.id == current_assignee.id
313279
):
314280
assignee_texts.append(f"#{assignee.slug}")
281+
elif assignee.is_user and not (
282+
isinstance(current_assignee, RpcUser) and assignee.id == current_assignee.id
283+
):
284+
assignee_as_user = assignee.resolve()
285+
if isinstance(assignee_as_user, RpcUser):
286+
assignee_texts.append(assignee_as_user.get_display_name())
315287
return assignee_texts
316288
return []
317289

@@ -417,7 +389,7 @@ def _assign_button() -> MessageAction:
417389
label="Select Assignee...",
418390
type="select",
419391
selected_options=format_actor_options([assignee], True) if assignee else [],
420-
option_groups=get_option_groups_block_kit(group),
392+
option_groups=get_option_groups(group),
421393
)
422394
return assign_button
423395

@@ -477,10 +449,10 @@ def escape_text(self) -> bool:
477449

478450
def get_title_block(
479451
self,
480-
rule_id: int,
481-
notification_uuid: str,
482452
event_or_group: GroupEvent | Group,
483453
has_action: bool,
454+
rule_id: int | None = None,
455+
notification_uuid: str | None = None,
484456
) -> SlackBlock:
485457
title_link = get_title_link(
486458
self.group,
@@ -504,11 +476,7 @@ def get_title_block(
504476
else ACTIONED_CATEGORY_TO_EMOJI.get(self.group.issue_category)
505477
)
506478
elif is_error_issue:
507-
level_text = None
508-
for k, v in LOG_LEVELS_MAP.items():
509-
if self.group.level == v:
510-
level_text = k
511-
479+
level_text = LOG_LEVELS[self.group.level]
512480
title_emoji = LEVEL_TO_EMOJI.get(level_text)
513481
else:
514482
title_emoji = CATEGORY_TO_EMOJI.get(self.group.issue_category)
@@ -584,7 +552,8 @@ def build(self, notification_uuid: str | None = None) -> SlackBlock:
584552
# If an event is unspecified, use the tags of the latest event (if one exists).
585553
event_for_tags = self.event or self.group.get_latest_event()
586554

587-
obj = self.event if self.event is not None else self.group
555+
event_or_group: Group | GroupEvent = self.event if self.event is not None else self.group
556+
588557
action_text = ""
589558

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

608-
blocks = [self.get_title_block(rule_id, notification_uuid, obj, has_action)]
577+
blocks = [self.get_title_block(event_or_group, has_action, rule_id, notification_uuid)]
609578

610-
if culprit_block := self.get_culprit_block(obj):
579+
if culprit_block := self.get_culprit_block(event_or_group):
611580
blocks.append(culprit_block)
612581

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

622591
# build tags block
623-
tags = get_tags(self.group, event_for_tags, self.tags)
592+
tags = get_tags(event_for_tags, self.tags)
624593
if tags:
625594
blocks.append(self.get_tags_block(tags))
626595

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

688657
return self._build_blocks(
689658
*blocks,
690-
fallback_text=self.build_fallback_text(obj, project.slug),
691-
block_id=orjson.dumps(block_id).decode(),
659+
fallback_text=self.build_fallback_text(event_or_group, project.slug),
660+
block_id=block_id,
692661
skip_fallback=self.skip_fallback,
693662
)

src/sentry/integrations/slack/webhooks/options_load.py

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import re
44
from collections.abc import Mapping, Sequence
5-
from typing import Any
5+
from typing import Any, TypedDict
66

77
import orjson
88
from rest_framework import status
@@ -20,6 +20,11 @@
2020
from ..utils import logger
2121

2222

23+
class OptionGroup(TypedDict):
24+
label: Mapping[str, str]
25+
options: Sequence[Mapping[str, Any]]
26+
27+
2328
@region_silo_endpoint
2429
class SlackOptionsLoadEndpoint(Endpoint):
2530
owner = ApiOwner.ECOSYSTEM
@@ -69,15 +74,17 @@ def get_filtered_option_groups(
6974

7075
option_groups = []
7176
if filtered_teams:
72-
team_options = format_actor_options(filtered_teams, True)
73-
option_groups.append(
74-
{"label": {"type": "plain_text", "text": "Teams"}, "options": team_options}
75-
)
77+
team_options_group: OptionGroup = {
78+
"label": {"type": "plain_text", "text": "Teams"},
79+
"options": format_actor_options(filtered_teams, True),
80+
}
81+
option_groups.append(team_options_group)
7682
if filtered_members:
77-
member_options = format_actor_options(filtered_members, True)
78-
option_groups.append(
79-
{"label": {"type": "plain_text", "text": "People"}, "options": member_options}
80-
)
83+
member_options_group: OptionGroup = {
84+
"label": {"type": "plain_text", "text": "People"},
85+
"options": format_actor_options(filtered_members, True),
86+
}
87+
option_groups.append(member_options_group)
8188
return option_groups
8289

8390
# XXX(isabella): atm this endpoint is used only for the assignment dropdown on issue alerts

src/sentry/notifications/utils/participants.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from django.db.models import Q
99

1010
from sentry import features
11+
from sentry.eventstore.models import GroupEvent
1112
from sentry.integrations.types import ExternalProviders
1213
from sentry.integrations.utils.providers import get_provider_enum_from_string
1314
from sentry.models.commit import Commit
@@ -262,7 +263,7 @@ def get_owner_reason(
262263
return None
263264

264265

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

287288

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

291292

src/sentry/utils/committers.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -293,7 +293,7 @@ def get_event_file_committers(
293293

294294

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

299299
group_owners = GroupOwner.objects.filter(

tests/sentry/integrations/slack/notifications/test_issue_alert.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,8 @@
4040
old_get_tags = get_tags
4141

4242

43-
def fake_get_tags(group, event_for_tags, tags):
44-
return old_get_tags(group, event_for_tags, None)
43+
def fake_get_tags(event_for_tags, tags):
44+
return old_get_tags(event_for_tags, None)
4545

4646

4747
class SlackIssueAlertNotificationTest(SlackActivityNotificationTest, PerformanceIssueTestCase):

0 commit comments

Comments
 (0)