Browse Source

feat(issue_alert_status): Add issue alert status page links to email alerts (#32302)

This adds links to the new issue alert status pages to both the digest and individual alert emails.
Dan Fuller 3 years ago
parent
commit
90e4eb2d81

+ 2 - 1
src/sentry/mail/adapter.py

@@ -2,6 +2,7 @@ import logging
 from typing import Any, Mapping, Optional, Sequence
 
 from sentry import digests
+from sentry.digests import Digest
 from sentry.digests import get_option_key as get_digest_option_key
 from sentry.digests.notifications import event_to_record, unsplit_key
 from sentry.models import NotificationSetting, Project, ProjectOption
@@ -103,7 +104,7 @@ class MailAdapter:
     @staticmethod
     def notify_digest(
         project: Project,
-        digest: Any,
+        digest: Digest,
         target_type: ActionTargetType,
         target_identifier: Optional[int] = None,
     ) -> None:

+ 11 - 1
src/sentry/notifications/notifications/digest.py

@@ -4,6 +4,7 @@ import logging
 from collections import defaultdict
 from typing import TYPE_CHECKING, Any, Mapping, MutableMapping
 
+from sentry import features
 from sentry.digests import Digest
 from sentry.digests.utils import (
     get_digest_as_context,
@@ -15,7 +16,7 @@ from sentry.eventstore.models import Event
 from sentry.notifications.notifications.base import ProjectNotification
 from sentry.notifications.notify import notify
 from sentry.notifications.types import ActionTargetType
-from sentry.notifications.utils import get_integration_link, has_alert_integration
+from sentry.notifications.utils import get_integration_link, get_rules, has_alert_integration
 from sentry.notifications.utils.digest import (
     get_digest_subject,
     send_as_alert_notification,
@@ -78,11 +79,20 @@ class DigestNotification(ProjectNotification):
         return self.project
 
     def get_context(self) -> MutableMapping[str, Any]:
+        alert_status_page_enabled = features.has(
+            "organizations:alert-rule-status-page", self.project.organization
+        )
+        rules_details = {
+            rule.id: rule
+            for rule in get_rules(list(self.digest.keys()), self.project.organization, self.project)
+        }
         return {
             **get_digest_as_context(self.digest),
             "has_alert_integration": has_alert_integration(self.project),
             "project": self.project,
             "slack_link": get_integration_link(self.organization, "slack"),
+            "alert_status_page_enabled": alert_status_page_enabled,
+            "rules_details": rules_details,
         }
 
     def get_extra_context(

+ 5 - 0
src/sentry/notifications/notifications/rules.py

@@ -5,6 +5,7 @@ from typing import Any, Iterable, Mapping, MutableMapping
 
 import pytz
 
+from sentry import features
 from sentry.models import Team, User, UserOption
 from sentry.notifications.notifications.base import ProjectNotification
 from sentry.notifications.types import ActionTargetType, NotificationSettingTypes
@@ -86,6 +87,9 @@ class AlertRuleNotification(ProjectNotification):
     def get_context(self) -> MutableMapping[str, Any]:
         environment = self.event.get_tag("environment")
         enhanced_privacy = self.organization.flags.enhanced_privacy
+        alert_status_page_enabled = features.has(
+            "organizations:alert-rule-status-page", self.project.organization
+        )
         context = {
             "project_label": self.project.get_full_name(),
             "group": self.group,
@@ -98,6 +102,7 @@ class AlertRuleNotification(ProjectNotification):
             "environment": environment,
             "slack_link": get_integration_link(self.organization, "slack"),
             "has_alert_integration": has_alert_integration(self.project),
+            "alert_status_page_enabled": alert_status_page_enabled,
         }
 
         # if the organization has enabled enhanced privacy controls we don't send

+ 16 - 2
src/sentry/notifications/utils/__init__.py

@@ -1,6 +1,7 @@
 from __future__ import annotations
 
 import logging
+from dataclasses import dataclass
 from typing import TYPE_CHECKING, Any, Iterable, Mapping, MutableMapping, Sequence, cast
 
 from django.db.models import Count
@@ -141,11 +142,24 @@ def get_integration_link(organization: Organization, integration_slug: str) -> s
     return integration_link
 
 
+@dataclass
+class NotificationRuleDetails:
+    id: int
+    label: str
+    url: str
+    status_url: str
+
+
 def get_rules(
     rules: Sequence[Rule], organization: Organization, project: Project
-) -> Sequence[tuple[str, str]]:
+) -> Sequence[NotificationRuleDetails]:
     return [
-        (rule.label, f"/organizations/{organization.slug}/alerts/rules/{project.slug}/{rule.id}/")
+        NotificationRuleDetails(
+            rule.id,
+            rule.label,
+            f"/organizations/{organization.slug}/alerts/rules/{project.slug}/{rule.id}/",
+            f"/organizations/{organization.slug}/alerts/rules/{project.slug}/{rule.id}/details/",
+        )
         for rule in rules
     ]
 

+ 7 - 1
src/sentry/templates/sentry/emails/digests/body.html

@@ -32,7 +32,13 @@
 
     <div class="rule">
       <div class="container">
-        {{ rule.label }}
+        {% if alert_status_page_enabled %}
+            {% with rule_details=rules_details|get_item:rule.id %}
+                You’re receiving this email because you’re subscribed to notifications for <a href="{% absolute_uri rule_details.status_url %}">{{ rule_details.label }}</a>
+            {% endwith %}
+        {% else %}
+            {{ rule.label }}
+        {% endif %}
       </div>
     </div>
 

+ 13 - 3
src/sentry/templates/sentry/emails/error.html

@@ -68,6 +68,16 @@
         {% if environment %} in {{ environment }}{% endif %}
     </h2>
 
+    {% if rules and alert_status_page_enabled %}
+        <p class="via">
+            You’re receiving this email because you’re subscribed to notifications for:
+            {% for rule in rules %}
+                <a href="{% absolute_uri rule.status_url %}">{{ rule.label }}</a>{% if not forloop.last %}, {% endif %}
+            {% endfor %}
+        </p>
+    {% endif %}
+
+
     {% if enhanced_privacy %}
       <div class="event">
         <div class="event-id">ID: {{ event.event_id }}</div>
@@ -195,11 +205,11 @@
       {% endif %}
     {% endif %}
 
-    {% if rules %}
+    {% if rules and not alert_status_page_enabled %}
         <p class="via">
             You are receiving this email due to matching rules:
-            {% for rule_label, rule_link in rules %}
-                <a href="{% absolute_uri rule_link %}">{{ rule_label }}</a>{% if not forloop.last %}, {% endif %}
+            {% for rule in rules %}
+                <a href="{% absolute_uri rule.url %}">{{ rule.label }}</a>{% if not forloop.last %}, {% endif %}
             {% endfor %}
         </p>
     {% endif %}

+ 5 - 0
src/sentry/templatetags/sentry_helpers.py

@@ -285,3 +285,8 @@ def random_int(a, b=None):
     if b is None:
         a, b = 0, a
     return randint(a, b)
+
+
+@register.filter
+def get_item(dictionary, key):
+    return dictionary.get(key, "")

+ 7 - 2
src/sentry/web/frontend/debug/mail.py

@@ -16,7 +16,7 @@ from django.utils import timezone
 from django.utils.safestring import mark_safe
 from django.views.generic import View
 
-from sentry import eventstore
+from sentry import eventstore, features
 from sentry.app import tsdb
 from sentry.constants import LOG_LEVELS
 from sentry.digests import Record
@@ -40,6 +40,7 @@ from sentry.models import (
 from sentry.notifications.notifications.activity import EMAIL_CLASSES_BY_TYPE
 from sentry.notifications.notifications.base import BaseNotification
 from sentry.notifications.types import GroupSubscriptionReason
+from sentry.notifications.utils import get_rules
 from sentry.utils import loremipsum
 from sentry.utils.dates import to_datetime, to_timestamp
 from sentry.utils.email import inline_css
@@ -266,7 +267,7 @@ def alert(request):
     group.message = event.search_message
     group.data = {"type": event_type.key, "metadata": event_type.get_metadata(data)}
 
-    rule = Rule(label="An example rule")
+    rule = Rule(id=1, label="An example rule")
 
     # XXX: this interface_list code needs to be the same as in
     #      src/sentry/mail/adapter.py
@@ -283,6 +284,7 @@ def alert(request):
         text_template="sentry/emails/error.txt",
         context={
             "rule": rule,
+            "rules": get_rules([rule], org, project),
             "group": group,
             "event": event,
             "timezone": pytz.timezone("Europe/Vienna"),
@@ -290,6 +292,7 @@ def alert(request):
             "interfaces": interface_list,
             "tags": event.tags,
             "project_label": project.slug,
+            "alert_status_page_enabled": features.has("organizations:alert-rule-status-page", org),
             "commits": [
                 {
                     # TODO(dcramer): change to use serializer
@@ -402,6 +405,8 @@ def digest(request):
         "start": start,
         "end": end,
         "referrer": "digest_email",
+        "alert_status_page_enabled": features.has("organizations:alert-rule-status-page", org),
+        "rules_details": {rule.id: rule for rule in get_rules(rules.values(), org, project)},
     }
     add_unsubscribe_link(context)
 

+ 14 - 0
tests/sentry/templatetags/test_sentry_helpers.py

@@ -84,3 +84,17 @@ def test_date_handle_date_and_datetime():
     )
 
     assert result == "\n".join(["2021-04-16", "2021-04-17"])
+
+
+@pytest.mark.parametrize(
+    "a_dict,key,expected",
+    (
+        ({}, "", ""),
+        ({}, "hi", ""),
+        ({"hello": 1}, "hello", "1"),
+    ),
+)
+def test_get_item(a_dict, key, expected):
+    prefix = '{% load sentry_helpers %} {{ something|get_item:"' + key + '" }}'
+    result = engines["django"].from_string(prefix).render(context={"something": a_dict}).strip()
+    assert result == expected