33import logging
44from collections .abc import Mapping , Sequence
55from datetime import datetime
6- from typing import Any
6+ from typing import Any , TypedDict
77
88import orjson
99from django .core .exceptions import ObjectDoesNotExist
1010from sentry_relay .processing import parse_release
1111
1212from sentry import tagstore
1313from 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
1515from sentry .eventstore .models import GroupEvent
1616from sentry .identity .services .identity import RpcIdentity , identity_service
1717from 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
180161def 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 )
0 commit comments