diff --git a/src/sentry/api/serializers/models/rule.py b/src/sentry/api/serializers/models/rule.py index 317d86db1c776c..0244282db11d62 100644 --- a/src/sentry/api/serializers/models/rule.py +++ b/src/sentry/api/serializers/models/rule.py @@ -11,6 +11,8 @@ from sentry.api.serializers import Serializer, register from sentry.constants import ObjectStatus from sentry.db.models.manager.base_query_set import BaseQuerySet +from sentry.integrations.services.integration.model import RpcIntegration +from sentry.integrations.services.integration.service import integration_service from sentry.models.environment import Environment from sentry.models.project import Project from sentry.models.rule import NeglectedRule, Rule, RuleActivity, RuleActivityType @@ -591,6 +593,22 @@ def get_attrs(self, item_list: Sequence[Workflow], user, **kwargs): ) actions_by_dcg = self._fetch_actions_by_dcg(all_dcg_ids) + # Batch-fetch integrations for all actions to avoid per-action RPC calls in render_label + all_integration_ids: set[int] = set() + for action_list in actions_by_dcg.values(): + for action in action_list: + if action.integration_id is not None: + all_integration_ids.add(action.integration_id) + + integration_cache: dict[int, RpcIntegration] = {} + if all_integration_ids and item_list: + integrations = integration_service.get_integrations( + integration_ids=list(all_integration_ids), + organization_id=item_list[0].organization_id, + status=ObjectStatus.ACTIVE, + ) + integration_cache = {i.id: i for i in integrations} + last_triggered_lookup: dict[int, datetime] = {} if "lastTriggered" in self.expand: last_triggered_lookup = self._fetch_workflow_last_triggered(item_list) @@ -667,7 +685,7 @@ def get_attrs(self, item_list: Sequence[Workflow], user, **kwargs): continue action_data["name"] = action_to_handler[action].render_label( - workflow.organization_id, action_data + workflow.organization_id, action_data, integration_cache=integration_cache ) installation_uuid = action_data.get("sentryAppInstallationUuid") install_context = None diff --git a/src/sentry/notifications/notification_action/issue_alert_registry/handlers/discord_issue_alert_handler.py b/src/sentry/notifications/notification_action/issue_alert_registry/handlers/discord_issue_alert_handler.py index 474f9038b9e6b8..37834b8dc07e6d 100644 --- a/src/sentry/notifications/notification_action/issue_alert_registry/handlers/discord_issue_alert_handler.py +++ b/src/sentry/notifications/notification_action/issue_alert_registry/handlers/discord_issue_alert_handler.py @@ -2,6 +2,7 @@ from sentry.constants import ObjectStatus from sentry.integrations.services.integration import integration_service +from sentry.integrations.services.integration.model import RpcIntegration from sentry.notifications.notification_action.registry import issue_alert_handler_registry from sentry.notifications.notification_action.types import BaseIssueAlertHandler from sentry.workflow_engine.models import Action @@ -20,12 +21,20 @@ def get_target_display(cls, action: Action, mapping: ActionFieldMapping) -> dict return {} @classmethod - def render_label(cls, organization_id: int, blob: dict[str, Any]) -> str: - integration = integration_service.get_integration( - integration_id=blob["server"], - organization_id=organization_id, - status=ObjectStatus.ACTIVE, - ) + def render_label( + cls, + organization_id: int, + blob: dict[str, Any], + integration_cache: dict[int, RpcIntegration] | None = None, + ) -> str: + integration_id = blob["server"] + integration = cls._get_cached_integration(integration_id, integration_cache) + if integration is None: + integration = integration_service.get_integration( + integration_id=integration_id, + organization_id=organization_id, + status=ObjectStatus.ACTIVE, + ) if not integration: return "" diff --git a/src/sentry/notifications/notification_action/issue_alert_registry/handlers/email_issue_alert_handler.py b/src/sentry/notifications/notification_action/issue_alert_registry/handlers/email_issue_alert_handler.py index 21ceb2c3112a1d..d7a77138bba817 100644 --- a/src/sentry/notifications/notification_action/issue_alert_registry/handlers/email_issue_alert_handler.py +++ b/src/sentry/notifications/notification_action/issue_alert_registry/handlers/email_issue_alert_handler.py @@ -58,7 +58,9 @@ def get_additional_fields(cls, action: Action, mapping: ActionFieldMapping) -> d return final_blob @classmethod - def render_label(cls, organization_id: int, blob: dict[str, Any]) -> str: + def render_label( + cls, organization_id: int, blob: dict[str, Any], integration_cache: Any = None + ) -> str: target_type = blob["targetType"] fallthrough_type = blob.get("fallthrough_type", FallthroughChoiceType.ACTIVE_MEMBERS.value) label = f"Send a notification to {target_type} and if none can be found then send a notification to {fallthrough_type}" diff --git a/src/sentry/notifications/notification_action/issue_alert_registry/handlers/msteams_issue_alert_handler.py b/src/sentry/notifications/notification_action/issue_alert_registry/handlers/msteams_issue_alert_handler.py index 77902ccd8f249f..80a6ff2203ac64 100644 --- a/src/sentry/notifications/notification_action/issue_alert_registry/handlers/msteams_issue_alert_handler.py +++ b/src/sentry/notifications/notification_action/issue_alert_registry/handlers/msteams_issue_alert_handler.py @@ -2,6 +2,7 @@ from sentry.constants import ObjectStatus from sentry.integrations.services.integration import integration_service +from sentry.integrations.services.integration.model import RpcIntegration from sentry.notifications.notification_action.registry import issue_alert_handler_registry from sentry.notifications.notification_action.types import BaseIssueAlertHandler from sentry.workflow_engine.models import Action @@ -10,12 +11,20 @@ @issue_alert_handler_registry.register(Action.Type.MSTEAMS) class MSTeamsIssueAlertHandler(BaseIssueAlertHandler): @classmethod - def render_label(cls, organization_id: int, blob: dict[str, Any]) -> str: - integration = integration_service.get_integration( - integration_id=blob["team"], - organization_id=organization_id, - status=ObjectStatus.ACTIVE, - ) + def render_label( + cls, + organization_id: int, + blob: dict[str, Any], + integration_cache: dict[int, RpcIntegration] | None = None, + ) -> str: + integration_id = blob["team"] + integration = cls._get_cached_integration(integration_id, integration_cache) + if integration is None: + integration = integration_service.get_integration( + integration_id=integration_id, + organization_id=organization_id, + status=ObjectStatus.ACTIVE, + ) if not integration: return "" diff --git a/src/sentry/notifications/notification_action/issue_alert_registry/handlers/opsgenie_issue_alert_handler.py b/src/sentry/notifications/notification_action/issue_alert_registry/handlers/opsgenie_issue_alert_handler.py index f4200d838a098f..042731fadb4532 100644 --- a/src/sentry/notifications/notification_action/issue_alert_registry/handlers/opsgenie_issue_alert_handler.py +++ b/src/sentry/notifications/notification_action/issue_alert_registry/handlers/opsgenie_issue_alert_handler.py @@ -24,7 +24,9 @@ def get_additional_fields(cls, action: Action, mapping: ActionFieldMapping) -> d return {"priority": blob.priority} @classmethod - def render_label(cls, organization_id: int, blob: dict[str, Any]) -> str: + def render_label( + cls, organization_id: int, blob: dict[str, Any], integration_cache: Any = None + ) -> str: result = integration_service.organization_context( organization_id=organization_id, integration_id=blob["account"], diff --git a/src/sentry/notifications/notification_action/issue_alert_registry/handlers/pagerduty_issue_alert_handler.py b/src/sentry/notifications/notification_action/issue_alert_registry/handlers/pagerduty_issue_alert_handler.py index 4aaa6b540f847a..f6dbfdbf1df18d 100644 --- a/src/sentry/notifications/notification_action/issue_alert_registry/handlers/pagerduty_issue_alert_handler.py +++ b/src/sentry/notifications/notification_action/issue_alert_registry/handlers/pagerduty_issue_alert_handler.py @@ -24,7 +24,9 @@ def get_additional_fields(cls, action: Action, mapping: ActionFieldMapping) -> d return {"severity": blob.priority} @classmethod - def render_label(cls, organization_id: int, blob: dict[str, Any]) -> str: + def render_label( + cls, organization_id: int, blob: dict[str, Any], integration_cache: Any = None + ) -> str: result = integration_service.organization_context( organization_id=organization_id, integration_id=blob["account"], diff --git a/src/sentry/notifications/notification_action/issue_alert_registry/handlers/plugin_issue_alert_handler.py b/src/sentry/notifications/notification_action/issue_alert_registry/handlers/plugin_issue_alert_handler.py index e3d4229abea96d..2c4018832afc8f 100644 --- a/src/sentry/notifications/notification_action/issue_alert_registry/handlers/plugin_issue_alert_handler.py +++ b/src/sentry/notifications/notification_action/issue_alert_registry/handlers/plugin_issue_alert_handler.py @@ -23,5 +23,7 @@ def get_target_display(cls, action: Action, mapping: ActionFieldMapping) -> dict return {} @classmethod - def render_label(cls, organization_id: int, blob: dict[str, Any]) -> str: + def render_label( + cls, organization_id: int, blob: dict[str, Any], integration_cache: Any = None + ) -> str: return "Send a notification (for all legacy integrations)" diff --git a/src/sentry/notifications/notification_action/issue_alert_registry/handlers/sentry_app_issue_alert_handler.py b/src/sentry/notifications/notification_action/issue_alert_registry/handlers/sentry_app_issue_alert_handler.py index 947e5ec5edc7b7..0c1224f3e0ce9f 100644 --- a/src/sentry/notifications/notification_action/issue_alert_registry/handlers/sentry_app_issue_alert_handler.py +++ b/src/sentry/notifications/notification_action/issue_alert_registry/handlers/sentry_app_issue_alert_handler.py @@ -81,7 +81,9 @@ def get_additional_fields(cls, action: Action, mapping: ActionFieldMapping) -> d return {} @classmethod - def render_label(cls, organization_id: int, blob: dict[str, Any]) -> str: + def render_label( + cls, organization_id: int, blob: dict[str, Any], integration_cache: Any = None + ) -> str: """ blob: { 'id': 'sentry.rules.actions.notify_event_sentry_app.NotifyEventSentryAppAction', diff --git a/src/sentry/notifications/notification_action/issue_alert_registry/handlers/slack_issue_alert_handler.py b/src/sentry/notifications/notification_action/issue_alert_registry/handlers/slack_issue_alert_handler.py index b305402155e780..96ad09e3f7a0cf 100644 --- a/src/sentry/notifications/notification_action/issue_alert_registry/handlers/slack_issue_alert_handler.py +++ b/src/sentry/notifications/notification_action/issue_alert_registry/handlers/slack_issue_alert_handler.py @@ -2,6 +2,7 @@ from sentry.constants import ObjectStatus from sentry.integrations.services.integration import integration_service +from sentry.integrations.services.integration.model import RpcIntegration from sentry.notifications.notification_action.registry import issue_alert_handler_registry from sentry.notifications.notification_action.types import BaseIssueAlertHandler from sentry.workflow_engine.models import Action @@ -19,12 +20,20 @@ def get_additional_fields(cls, action: Action, mapping: ActionFieldMapping) -> d } @classmethod - def render_label(cls, organization_id: int, blob: dict[str, Any]) -> str: - integration = integration_service.get_integration( - integration_id=blob["workspace"], - organization_id=organization_id, - status=ObjectStatus.ACTIVE, - ) + def render_label( + cls, + organization_id: int, + blob: dict[str, Any], + integration_cache: dict[int, RpcIntegration] | None = None, + ) -> str: + integration_id = blob["workspace"] + integration = cls._get_cached_integration(integration_id, integration_cache) + if integration is None: + integration = integration_service.get_integration( + integration_id=integration_id, + organization_id=organization_id, + status=ObjectStatus.ACTIVE, + ) if not integration: return "" diff --git a/src/sentry/notifications/notification_action/issue_alert_registry/handlers/webhook_issue_alert_handler.py b/src/sentry/notifications/notification_action/issue_alert_registry/handlers/webhook_issue_alert_handler.py index cf4ba95ab4487e..2a09cff0aa605f 100644 --- a/src/sentry/notifications/notification_action/issue_alert_registry/handlers/webhook_issue_alert_handler.py +++ b/src/sentry/notifications/notification_action/issue_alert_registry/handlers/webhook_issue_alert_handler.py @@ -17,5 +17,7 @@ def get_target_display(cls, action: Action, mapping: ActionFieldMapping) -> dict return {} @classmethod - def render_label(cls, organization_id: int, blob: dict[str, Any]) -> str: + def render_label( + cls, organization_id: int, blob: dict[str, Any], integration_cache: Any = None + ) -> str: return "Send a notification via webhooks" diff --git a/src/sentry/notifications/notification_action/types.py b/src/sentry/notifications/notification_action/types.py index fc5bfe53f66a54..8b14c31ace5f61 100644 --- a/src/sentry/notifications/notification_action/types.py +++ b/src/sentry/notifications/notification_action/types.py @@ -156,8 +156,26 @@ def get_action_mapping(cls, action: Action) -> ActionFieldMapping: raise ValueError(f"No mapping found for action type: {action.type}") return mapping + @staticmethod + def _get_cached_integration( + integration_id: Any, + integration_cache: dict[int, RpcIntegration] | None, + ) -> RpcIntegration | None: + """Look up an integration from the pre-fetched cache, safely coercing the ID to int.""" + if integration_cache is None or integration_id is None: + return None + try: + return integration_cache.get(int(integration_id)) + except (ValueError, TypeError): + return None + @classmethod - def render_label(cls, organization_id: int, blob: dict[str, Any]) -> str: + def render_label( + cls, + organization_id: int, + blob: dict[str, Any], + integration_cache: dict[int, RpcIntegration] | None = None, + ) -> str: return "Send a notification" @classmethod @@ -345,12 +363,20 @@ class TicketingIssueAlertHandler(BaseIssueAlertHandler): label_template = "Create a ticket in {integration}" @classmethod - def render_label(cls, organization_id: int, blob: dict[str, Any]) -> str: - integration = integration_service.get_integration( - integration_id=blob.get("integration"), - organization_id=organization_id, - status=ObjectStatus.ACTIVE, - ) + def render_label( + cls, + organization_id: int, + blob: dict[str, Any], + integration_cache: dict[int, RpcIntegration] | None = None, + ) -> str: + integration_id = blob.get("integration") + integration = cls._get_cached_integration(integration_id, integration_cache) + if integration is None: + integration = integration_service.get_integration( + integration_id=integration_id, + organization_id=organization_id, + status=ObjectStatus.ACTIVE, + ) integration_name = integration.name if integration else "[removed]" return cls.label_template.format(integration=integration_name)