Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 33 additions & 39 deletions src/sentry/digests/notifications.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,23 @@
import logging
from collections import defaultdict
from collections.abc import Mapping, Sequence
from dataclasses import replace
from typing import Any, NamedTuple, TypeAlias

import sentry_sdk

from sentry import tsdb
from sentry.digests.types import IdentifierKey, Notification, Record, RecordWithRuleObjects
from sentry.models.group import Group, GroupStatus
from sentry.models.project import Project
from sentry.models.rule import Rule
from sentry.notifications.types import ActionTargetType, FallthroughChoiceType
from sentry.notifications.utils.rules import get_rule_or_workflow_id
from sentry.notifications.types import ActionTargetType, FallthroughChoiceType, NotificationRule
from sentry.services.eventstore.models import Event, GroupEvent
from sentry.tsdb.base import TSDBModel
from sentry.workflow_engine.models import Workflow
from sentry.workflow_engine.models.alertrule_workflow import AlertRuleWorkflow

logger = logging.getLogger("sentry.digests")

Digest: TypeAlias = dict[Rule, dict[Group, list[RecordWithRuleObjects]]]
Digest: TypeAlias = dict[NotificationRule, dict[Group, list[RecordWithRuleObjects]]]


class DigestInfo(NamedTuple):
Expand Down Expand Up @@ -78,7 +76,7 @@ def unsplit_key(

def event_to_record(
event: Event | GroupEvent,
rules: Sequence[Rule],
rules: Sequence[NotificationRule],
notification_uuid: str | None = None,
identifier_key: IdentifierKey = IdentifierKey.RULE,
) -> Record:
Expand All @@ -88,7 +86,11 @@ def event_to_record(
# TODO(iamrajjoshi): The typing on this function is wrong, the type should be GroupEvent
# TODO(iamrajjoshi): Creating a PR to fix this
assert event.group is not None
rule_ids = [int(get_rule_or_workflow_id(rule)[1]) for rule in rules]
rule_ids = []
for rule in rules:
rule_id = rule.legacy_rule_id if identifier_key == IdentifierKey.RULE else rule.workflow_id
assert rule_id is not None
rule_ids.append(rule_id)
return Record(
event.event_id,
Notification(event, rule_ids, notification_uuid, identifier_key),
Expand All @@ -97,7 +99,7 @@ def event_to_record(


def _bind_records(
records: Sequence[Record], groups: dict[int, Group], rules: dict[int, Rule]
records: Sequence[Record], groups: dict[int, Group], rules: dict[int, NotificationRule]
) -> list[RecordWithRuleObjects]:
ret = []
for record in records:
Expand All @@ -123,7 +125,9 @@ def _bind_records(


def _group_records(
records: Sequence[RecordWithRuleObjects], groups: dict[int, Group], rules: dict[int, Rule]
records: Sequence[RecordWithRuleObjects],
groups: dict[int, Group],
rules: dict[int, NotificationRule],
) -> Digest:
grouped: Digest = defaultdict(lambda: defaultdict(list))
for record in records:
Expand Down Expand Up @@ -161,7 +165,7 @@ def _sort_digest(
def _build_digest_impl(
records: Sequence[Record],
groups: dict[int, Group],
rules: dict[int, Rule],
rules: dict[int, NotificationRule],
event_counts: dict[int, int],
user_counts: Mapping[Any, int],
) -> Digest:
Expand All @@ -171,8 +175,10 @@ def _build_digest_impl(
return _sort_digest(grouped, event_counts=event_counts, user_counts=user_counts)


def get_rules_from_workflows(project: Project, workflow_ids: set[int]) -> dict[int, Rule]:
rules: dict[int, Rule] = {}
def get_rules_from_workflows(
project: Project, workflow_ids: set[int]
) -> dict[int, NotificationRule]:
rules: dict[int, NotificationRule] = {}
if not workflow_ids:
return rules

Expand All @@ -194,27 +200,22 @@ def get_rules_from_workflows(project: Project, workflow_ids: set[int]) -> dict[i
if alert_workflow:
if rule := bulk_rules.get(alert_workflow.rule_id):
assert rule.project_id == project.id, "Rule must belong to Project"
rule.environment_id = workflow.environment_id
try:
rule.data["actions"][0]["legacy_rule_id"] = rule.id
rule.data["actions"][0]["workflow_id"] = workflow_id
except KeyError:
# This shouldn't happen, but isn't a deal breaker if it does
sentry_sdk.capture_exception(
Exception(f"Rule {rule.id} does not have a legacy_rule_id"),
level="warning",
)
rules[workflow_id] = rule
rules[workflow_id] = replace(
NotificationRule.from_deprecated_legacy_rule(
rule, project=project, workflow_id=workflow_id
),
environment_id=workflow.environment_id,
)
continue

# Create synthetic Rule when no AlertRuleWorkflow or no Rule found
rules[workflow_id] = Rule(
rules[workflow_id] = NotificationRule(
label=workflow.name,
id=workflow_id,
project_id=project.id,
action_id=None,
project=project,
environment_id=workflow.environment_id,
# We need to do this so that the links are built correctly downstream
data={"actions": [{"workflow_id": workflow_id}]},
workflow_id=workflow_id,
legacy_rule_id=None,
)

return rules
Expand Down Expand Up @@ -245,17 +246,10 @@ def build_digest(project: Project, records: Sequence[Record]) -> DigestInfo:

groups = Group.objects.in_bulk(record.value.event.group_id for record in records)
group_ids = list(groups)
rules = Rule.objects.in_bulk(rule_ids)

for rule in rules.values():
try:
rule.data["actions"][0]["legacy_rule_id"] = rule.id
except KeyError:
# This shouldn't happen, but isn't a deal breaker if it does
sentry_sdk.capture_exception(
Exception(f"Rule {rule.id} does not have a legacy_rule_id"),
level="warning",
)
rules = {
rule_id: NotificationRule.from_deprecated_legacy_rule(rule, project=project)
for rule_id, rule in Rule.objects.in_bulk(rule_ids).items()
}

rules.update(get_rules_from_workflows(project, workflow_ids))

Expand Down
8 changes: 4 additions & 4 deletions src/sentry/digests/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from sentry.utils.dates import to_datetime

if TYPE_CHECKING:
from sentry.models.rule import Rule
from sentry.notifications.types import NotificationRule
from sentry.services.eventstore.models import Event, GroupEvent


Expand All @@ -23,7 +23,7 @@ class Notification(NamedTuple):
notification_uuid: str | None = None
identifier_key: IdentifierKey = IdentifierKey.RULE

def with_rules(self, rules: list[Rule]) -> NotificationWithRuleObjects:
def with_rules(self, rules: list[NotificationRule]) -> NotificationWithRuleObjects:
return NotificationWithRuleObjects(
event=self.event,
rules=rules,
Expand All @@ -41,7 +41,7 @@ class Record(NamedTuple):
def datetime(self) -> datetime_mod.datetime:
return to_datetime(self.timestamp)

def with_rules(self, rules: list[Rule]) -> RecordWithRuleObjects:
def with_rules(self, rules: list[NotificationRule]) -> RecordWithRuleObjects:
return RecordWithRuleObjects(
key=self.key,
value=self.value.with_rules(rules),
Expand All @@ -51,7 +51,7 @@ def with_rules(self, rules: list[Rule]) -> RecordWithRuleObjects:

class NotificationWithRuleObjects(NamedTuple):
event: Event | GroupEvent
rules: list[Rule]
rules: list[NotificationRule]
notification_uuid: str | None


Expand Down
5 changes: 2 additions & 3 deletions src/sentry/digests/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@
from sentry.models.group import Group
from sentry.models.project import Project
from sentry.models.projectownership import ProjectOwnership
from sentry.models.rule import Rule
from sentry.notifications.types import ActionTargetType, FallthroughChoiceType
from sentry.notifications.types import ActionTargetType, FallthroughChoiceType, NotificationRule
from sentry.notifications.utils.participants import get_send_to
from sentry.services.eventstore.models import Event, GroupEvent
from sentry.types.actor import Actor
Expand Down Expand Up @@ -166,7 +165,7 @@ def sort_func(record: Record) -> datetime:
return sorted(records, key=sort_func, reverse=True)


def get_groups(digest: Digest) -> Sequence[tuple[Rule, Group, Event | GroupEvent]]:
def get_groups(digest: Digest) -> Sequence[tuple[NotificationRule, Group, Event | GroupEvent]]:
"""
Split a digest into groups and return it as a tuple of: the applicable
rule, the group, and the group's first event.
Expand Down
63 changes: 32 additions & 31 deletions src/sentry/integrations/discord/message_builder/issues.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@
from sentry.integrations.types import ExternalProviders
from sentry.models.group import Group, GroupStatus
from sentry.models.project import Project
from sentry.models.rule import Rule
from sentry.notifications.notifications.base import ProjectNotification
from sentry.notifications.utils.rules import RuleIdType, get_rule_or_workflow_id
from sentry.notifications.types import NotificationRule
from sentry.notifications.utils.rules import get_rule_or_workflow_id
from sentry.services.eventstore.models import GroupEvent

from ..message_builder.base.component import DiscordComponentCustomIds as CustomIds
Expand All @@ -37,7 +37,7 @@ def __init__(
group: Group,
event: GroupEvent | None = None,
tags: set[str] | None = None,
rules: list[Rule] | None = None,
rules: list[NotificationRule] | None = None,
link_to_event: bool = False,
issue_details: bool = False,
notification: ProjectNotification | None = None,
Expand All @@ -57,39 +57,40 @@ def build(self, notification_uuid: str | None = None) -> DiscordMessage:
max(self.group.last_seen, self.event.datetime) if self.event else self.group.last_seen
)
obj: Group | GroupEvent = self.event if self.event is not None else self.group
rule_id = None
rule_id: int | None = None
rule_environment_id = None
key: RuleIdType = "legacy_rule_id"
is_workflow = False
if self.rules:
rule_environment_id = self.rules[0].environment_id
key, rule_id = get_rule_or_workflow_id(self.rules[0], prefer="workflow_id")
key, rule_id_value = get_rule_or_workflow_id(self.rules[0], prefer="workflow_id")
is_workflow = key == "workflow_id"
rule_id = int(rule_id_value)

url = None
match key:
case "workflow_id":
url = get_title_link_workflow_engine_ui(
self.group,
self.event,
self.link_to_event,
self.issue_details,
self.notification,
ExternalProviders.DISCORD,
int(rule_id) if rule_id else None,
rule_environment_id,
notification_uuid=notification_uuid,
)
case "legacy_rule_id":
url = get_title_link(
self.group,
self.event,
self.link_to_event,
self.issue_details,
self.notification,
ExternalProviders.DISCORD,
int(rule_id) if rule_id else None,
rule_environment_id,
notification_uuid=notification_uuid,
)
if is_workflow:
url = get_title_link_workflow_engine_ui(
self.group,
self.event,
self.link_to_event,
self.issue_details,
self.notification,
ExternalProviders.DISCORD,
rule_id,
rule_environment_id,
notification_uuid=notification_uuid,
)
else:
url = get_title_link(
self.group,
self.event,
self.link_to_event,
self.issue_details,
self.notification,
ExternalProviders.DISCORD,
rule_id,
rule_environment_id,
notification_uuid=notification_uuid,
)

embeds = [
DiscordMessageEmbed(
Expand Down
2 changes: 1 addition & 1 deletion src/sentry/integrations/github/actions/create_ticket.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,4 @@ class GitHubCreateTicketAction(TicketEventAction):
provider = IntegrationProviderSlug.GITHUB.value

def generate_footer(self, rule_url: str) -> str:
return f"\nThis issue was automatically created by Sentry via [{self.rule.label}]({absolute_uri(rule_url)})"
return f"\nThis issue was automatically created by Sentry via [{self.rule_context.label}]({absolute_uri(rule_url)})"
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,4 @@ class GitHubEnterpriseCreateTicketAction(TicketEventAction):
provider = IntegrationProviderSlug.GITHUB_ENTERPRISE.value

def generate_footer(self, rule_url: str) -> str:
return f"\nThis issue was automatically created by Sentry via [{self.rule.label}]({absolute_uri(rule_url)})"
return f"\nThis issue was automatically created by Sentry via [{self.rule_context.label}]({absolute_uri(rule_url)})"
2 changes: 1 addition & 1 deletion src/sentry/integrations/jira/actions/create_ticket.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ def __init__(self, *args: Any, **kwargs: Any) -> None:
self.data["fixVersions"] = [fix_versions]

def generate_footer(self, rule_url: str) -> str:
return f"This ticket was automatically created by Sentry via [{self.rule.label}|{absolute_uri(rule_url)}]"
return f"This ticket was automatically created by Sentry via [{self.rule_context.label}|{absolute_uri(rule_url)}]"

def translate_integration(self, integration: RpcIntegration) -> str:
name = integration.metadata.get("domain_name", integration.name)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ def __init__(self, *args: Any, **kwargs: Any) -> None:
self.data["fixVersions"] = [fix_versions]

def generate_footer(self, rule_url: str) -> str:
return f"This ticket was automatically created by Sentry via [{self.rule.label}|{absolute_uri(rule_url)}]"
return f"This ticket was automatically created by Sentry via [{self.rule_context.label}|{absolute_uri(rule_url)}]"

def translate_integration(self, integration: RpcIntegration) -> str:
return integration.metadata.get("domain_name", integration.name)
Expand Down
11 changes: 6 additions & 5 deletions src/sentry/integrations/messaging/message_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@
from sentry.models.environment import Environment
from sentry.models.group import Group
from sentry.models.project import Project
from sentry.models.rule import Rule
from sentry.models.team import Team
from sentry.notifications.notifications.base import BaseNotification
from sentry.notifications.notifications.rules import AlertRuleNotification
from sentry.notifications.types import NotificationRule
from sentry.notifications.utils.links import create_link_to_workflow
from sentry.notifications.utils.rules import get_key_from_rule_data, get_rule_or_workflow_id
from sentry.notifications.utils.rules import get_rule_or_workflow_id
from sentry.services.eventstore.models import Event, GroupEvent
from sentry.users.services.user import RpcUser
from sentry.utils.http import absolute_uri
Expand Down Expand Up @@ -250,10 +250,11 @@ def build_attachment_replay_link(
return None


def build_rule_url(rule: Any, group: Group, project: Project) -> str:
def build_rule_url(rule: NotificationRule, group: Group, project: Project) -> str:
org_slug = group.organization.slug
project_slug = project.slug
rule_id = get_key_from_rule_data(rule, "legacy_rule_id")
rule_id = rule.legacy_rule_id
assert rule_id is not None
rule_url = f"/organizations/{org_slug}/issues/alerts/rules/{project_slug}/{rule_id}/details/"

return absolute_uri(rule_url)
Expand All @@ -263,7 +264,7 @@ def build_footer(
group: Group,
project: Project,
url_format: str,
rules: Sequence[Rule] | None = None,
rules: Sequence[NotificationRule] | None = None,
) -> str:
footer = f"{group.qualified_short_id}"
if rules:
Expand Down
Loading
Loading