Skip to content

Commit e8bbf84

Browse files
feat(feedback): feedback email type (#68141)
Figma: https://www.figma.com/file/nLS7ZsjVmuSoCmEkMG5nHn/Specs%3A-Alerting-Emails-for-Feedback-v1?type=design&node-id=67-96&mode=design&t=uNuoqR4pcKz4W4Xo-0 screenshot: <img width="709" alt="Screenshot 2024-04-02 at 4 10 33 PM" src="https://github.com/getsentry/sentry/assets/1976777/19eed01c-a18e-4752-9c23-842debedbca1"> differences: - the email field in the figma is blue and linked, but i'm not sure what that would link to - i didn't move the important tags up (replay, url), let me know if we feel strongly about this - changed 'description' to 'message' - haven't added screenshots yet, can do that in a follow up. --------- Co-authored-by: Michelle Zhang <56095982+michellewzhang@users.noreply.github.com>
1 parent 2a0863b commit e8bbf84

File tree

7 files changed

+389
-5
lines changed

7 files changed

+389
-5
lines changed

src/sentry/issues/grouptype.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,11 @@ class GroupCategory(Enum):
3131
FEEDBACK = 6
3232

3333

34-
GROUP_CATEGORIES_CUSTOM_EMAIL = (GroupCategory.ERROR, GroupCategory.PERFORMANCE)
34+
GROUP_CATEGORIES_CUSTOM_EMAIL = (
35+
GroupCategory.ERROR,
36+
GroupCategory.PERFORMANCE,
37+
GroupCategory.FEEDBACK,
38+
)
3539
# GroupCategories which have customized email templates. If not included here, will fall back to a generic template.
3640

3741
DEFAULT_IGNORE_LIMIT: int = 3
Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
{% extends "sentry/emails/base.html" %}
2+
3+
{% load timezone from tz %}
4+
{% load sentry_avatars %}
5+
{% load sentry_helpers %}
6+
{% load sentry_features %}
7+
{% load sentry_assets %}
8+
{% load i18n static %}
9+
10+
{% block head %}
11+
{{ block.super }}
12+
<style type="text/css" inline="false">
13+
@media only screen and (max-device-width: 480px) {
14+
/* Hide CTA in header on mobile */
15+
.header-buttons .integration-link {
16+
display: none !important;
17+
}
18+
.banner {
19+
display: none !important;
20+
}
21+
}
22+
@media only screen and (max-device-width: 768px) {
23+
.text-desktop-only {
24+
display: none !important;
25+
}
26+
}
27+
</style>
28+
{% endblock %}
29+
30+
{% block preheader %}
31+
{{ group_header }} from {{ project_label }}.
32+
{% endblock %}
33+
34+
{% block header %}
35+
<div class="header-with-buttons">
36+
{{ block.super }}
37+
<div class="header-buttons">
38+
{% if not has_alert_integration %}
39+
<a href="{{ slack_link }}" class="btn btn-default integration-link">
40+
<img src="{% absolute_asset_url 'sentry' 'images/email/slack-logo.png' %}" class="logo-small" />
41+
Set up in Slack
42+
</a>
43+
{% endif %}
44+
<a href="{{ link }}" class="btn view-on-sentry">View on Sentry</a>
45+
</div>
46+
</div>
47+
{% endblock %}
48+
49+
{% block content %}
50+
51+
<div class="container">
52+
<div class="inner">
53+
<h2 {% if notification_reason %}style="margin-bottom: 4px"{% else %}style="margin-bottom: 15px"{% endif %}>
54+
{% if has_issue_states and group_header %}
55+
{{ group_header }}
56+
{% else %}
57+
New Feedback from <a href="{{group.project.get_absolute_url}}">{{ project_label }}</a>
58+
{% if environment %} in {{ environment }}{% endif %}
59+
{% endif %}
60+
</h2>
61+
{% if notification_reason %}
62+
<div class="event-notification-reason">
63+
{{ notification_reason }}
64+
</div>
65+
{% endif %}
66+
67+
{% if enhanced_privacy %}
68+
<div class="event">
69+
<div class="event-id">ID: {{ event.event_id }}</div>
70+
{% if timezone %}
71+
<div class="event-date">{{ event.datetime|timezone:timezone|date:"N j, Y, g:i:s a e"}}</div>
72+
{% else %}
73+
<div class="event-date">{{ event.datetime|date:"N j, Y, g:i:s a e"}}</div>
74+
{% endif %}
75+
</div>
76+
77+
<div class="notice">Details about this feedback are not shown in this notification since enhanced privacy
78+
controls are enabled. For more details about this feedback, <a href="{{ link }}">view this feedback on Sentry</a>.</div>
79+
{% else %}
80+
<table class="event-list">
81+
<tr>
82+
<th colspan="2">User Feedback</th>
83+
</tr>
84+
<tr>
85+
<td class="event-detail">
86+
<div class="issue">
87+
{% with type=event.get_event_type metadata=group.get_event_metadata transaction=event.transaction %}
88+
<div class="event-type default">
89+
<h3>
90+
{% if issue_title %}
91+
<a href="{% absolute_uri link %}">{{ issue_title|truncatechars:40 }}</a>
92+
{% else %}
93+
<a href="{% absolute_uri link %}">{{ event.title|truncatechars:40 }}</a>
94+
{% endif %}
95+
{% if culprit %}
96+
<span class="event-subtitle">{{ culprit }}</span>
97+
{% elif transaction %}
98+
<span class="event-subtitle">{{ transaction }}</span>
99+
{% endif %}
100+
<br />
101+
<br />
102+
{% if metadata.name and metadata.contact_email %}
103+
<p><strong>{{ metadata.name }}</strong> <a href="mailto:{{ metadata.contact_email }}">{{ metadata.contact_email }}</a></p>
104+
{% elif metadata.name %}
105+
<p><strong>{{ metadata.name }}</strong></p>
106+
{% elif metadata.contact_email %}
107+
<p><a href="mailto:{{ metadata.contact_email }}">{{ metadata.contact_email }}</a></p>
108+
{% endif %}
109+
<br />
110+
<div class="event">
111+
<div class="event-id">ID: {{ event.event_id }}</div>
112+
{% if timezone %}
113+
<div class="event-date">{{ event.datetime|timezone:timezone|date:"N j, Y, g:i:s a e"}}</div>
114+
{% else %}
115+
<div class="event-date">{{ event.datetime|date:"N j, Y, g:i:s a e"}}</div>
116+
{% endif %}
117+
</div>
118+
{% if subtitle %}
119+
<h3>Message</h3>
120+
<div class="inner"><div class="note-body"><p>{{ subtitle }}</p></div></div>
121+
{% endif %}
122+
</h3>
123+
</div>
124+
{% endwith %}
125+
</div>
126+
</td>
127+
</tr>
128+
</table>
129+
130+
131+
{% if has_issue_states %}
132+
<div class="interface">
133+
<table>
134+
<colgroup>
135+
<col style="width:130px;">
136+
</colgroup>
137+
<tbody>
138+
<tr>
139+
<th>project</th>
140+
<td><a href="{{group.project.get_absolute_url}}">{{ project_label }}</a></td>
141+
</tr>
142+
{% if environment %}
143+
<tr>
144+
<th>environment</th>
145+
<td>{{ environment }}</td>
146+
</tr>
147+
{% endif %}
148+
<tr>
149+
<th>level</th>
150+
<td>{{ group.get_level_display }}</td>
151+
</tr>
152+
</tbody>
153+
</table>
154+
</div>
155+
{% endif %}
156+
157+
158+
{% block table %}{% endblock %}
159+
160+
{% if issue_replays_url %}
161+
<div class="interface">
162+
<h3 class="title">Session Replay</h3>
163+
<table>
164+
<colgroup>
165+
<col style="width:130px;">
166+
</colgroup>
167+
<tbody>
168+
<tr>
169+
<th>Session Replay:</th>
170+
<td>
171+
<a href="{{ issue_replays_url }}">
172+
<img src="{% absolute_asset_url 'sentry' 'images/email/icon-play.png' %}" width="12px" height="12px"
173+
alt="Session Replay">
174+
See a replay of this feedback
175+
</a>
176+
</td>
177+
</tr>
178+
</tbody>
179+
</table>
180+
</div>
181+
{% endif %}
182+
183+
184+
{% block stacktrace %}{% endblock %}
185+
186+
{% if tags %}
187+
<h3>Tags</h3>
188+
189+
<ul class="tag-list">
190+
{% for tag_key, tag_value in tags %}
191+
<li>
192+
<strong>{{ tag_key|as_tag_alias }}</strong>
193+
<em>=</em>
194+
<span>
195+
{% with query=tag_key|as_tag_alias|add:":\""|add:tag_value|add:"\""|urlencode %}
196+
<a href="{% absolute_uri '/organizations/{}/issues/' group.project.organization.slug %}?project={{ group.project.id }}&query={{ query }}">{{ tag_value|truncatechars:50 }}</a> {% if tag_value|is_url %}<a href="{{ tag_value }}" class="icon-share"></a>{% endif %}
197+
{% endwith %}
198+
</span>
199+
</li>
200+
{% endfor %}
201+
</ul>
202+
{% endif %}
203+
{% endif %}
204+
205+
<p class="info-box">
206+
{% if snooze_alert %}
207+
<a class='mute' href="{% absolute_uri snooze_alert_url %}">Mute this alert</a>
208+
{% endif %}
209+
This email was triggered by
210+
{% for rule in rules %}
211+
<a href="{% absolute_uri rule.status_url %}">{{ rule.label }}</a>{% if not forloop.last %}, {% endif %}
212+
{% endfor %}
213+
</p>
214+
215+
{% if not has_integrations %}
216+
<div class="logo-container">
217+
<img src="{% static 'sentry/images/logos/logo-slack.svg' %}" class="logo" alt="Slack"/>
218+
<img src="{% static 'sentry/images/logos/logo-pagerduty.svg' %}" class="logo" alt="PagerDuty"/>
219+
<img src="{% static 'sentry/images/logos/logo-msteams.svg' %}" class="logo" alt="MS Teams"/>
220+
<img src="{% static 'sentry/images/logos/logo-opsgenie.svg' %}" class="logo" alt="OpsGenie"/>
221+
<img src="{% static 'sentry/images/logos/logo-twilio.svg' %}" class="logo" alt="Twilio"/>
222+
<img src="{% static 'sentry/images/logos/logo-victorops.svg' %}" class="logo" alt="VictorOps"/>
223+
</div>
224+
<p class="align-center">
225+
<a href="{% absolute_uri 'settings/{}/integrations/?referrer=alert_email' group.project.organization.slug %}">{{ "Get this alert wherever you work" }}</a>
226+
</p>
227+
{% endif %}
228+
229+
{# support for gmail actions #}
230+
<div itemscope itemtype="http://schema.org/EmailMessage">
231+
<meta itemprop="description" content="View Feedback Details in Sentry"/>
232+
<div itemprop="action" itemscope itemtype="http://schema.org/ViewAction">
233+
<link itemprop="url" href="{{ link }}"/>
234+
<meta itemprop="name" content="View in Sentry"/>
235+
</div>
236+
<div itemprop="publisher" itemscope itemtype="http://schema.org/Organization">
237+
<meta itemprop="name" content="GetSentry"/>
238+
<link itemprop="url" href="https://sentry.io/"/>
239+
</div>
240+
</div>
241+
</div>
242+
</div>
243+
{% endblock %}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
{% spaceless %}
2+
{% autoescape off %}
3+
{% if enhanced_privacy %}
4+
Details about this feedback are not shown in this notification since enhanced
5+
privacy controls are enabled. For more details about this feedback, view this
6+
issue on Sentry.
7+
Details
8+
-------
9+
10+
{{ link }}
11+
{% else %}
12+
Details
13+
-------
14+
15+
{{ link }}
16+
17+
18+
{% if generic_issue_data %}
19+
User Feedback
20+
----------
21+
{% for label, html, _ in generic_issue_data %}
22+
{{ label }} {{ html }}
23+
{% endfor %}{% endif %}
24+
25+
Tags
26+
----
27+
{% for tag_key, tag_value in tags %}
28+
* {{ tag_key }} = {{ tag_value }}{% endfor %}
29+
30+
{% if interfaces %}{% for label, _, text in interfaces %}
31+
{{ label }}
32+
-----------
33+
34+
{{ text }}
35+
36+
{% endfor %}
37+
{% endif %}{% endif %}
38+
39+
Unsubscribe: {{ unsubscribe_link }}
40+
{% endautoescape %}
41+
{% endspaceless %}

src/sentry/testutils/helpers/notifications.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from typing import Any
77

88
from sentry.issues.grouptype import (
9+
FeedbackGroup,
910
PerformanceNPlusOneAPICallsGroupType,
1011
PerformanceNPlusOneGroupType,
1112
PerformanceRenderBlockingAssetSpanGroupType,
@@ -119,6 +120,33 @@ def get_title_link(self, *args):
119120
"info",
120121
"/api/123/",
121122
)
123+
TEST_FEEDBACK_ISSUE_OCCURENCE = IssueOccurrence(
124+
id=uuid.uuid4().hex,
125+
project_id=1,
126+
event_id=uuid.uuid4().hex,
127+
fingerprint=["c" * 32],
128+
issue_title="User Feedback",
129+
subtitle="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed vel aliquam velit, nec condimentum mi. Maecenas accumsan, nunc ac venenatis hendrerit, mi libero facilisis nunc, fringilla molestie dui est vulputate diam. Duis ac justo euismod, sagittis est at, bibendum purus. Praesent nec tortor vel ante accumsan lobortis. Morbi mollis augue nec dolor feugiat congue. Nullam eget blandit nisi. Sed in arcu odio. Aenean malesuada tortor quis felis dapibus congue.d",
130+
culprit="api/123",
131+
resource_id="1234",
132+
evidence_data={
133+
"contact_email": "test@test.com",
134+
"message": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed vel aliquam velit, nec condimentum mi. Maecenas accumsan, nunc ac venenatis hendrerit, mi libero facilisis nunc, fringilla molestie dui est vulputate diam. Duis ac justo euismod, sagittis est at, bibendum purus. Praesent nec tortor vel ante accumsan lobortis. Morbi mollis augue nec dolor feugiat congue. Nullam eget blandit nisi. Sed in arcu odio. Aenean malesuada tortor quis felis dapibus congue.",
135+
"name": "Test Name",
136+
},
137+
evidence_display=[
138+
IssueEvidence("contact_email", "test@test.com", False),
139+
IssueEvidence(
140+
"message",
141+
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed vel aliquam velit, nec condimentum mi. Maecenas accumsan, nunc ac venenatis hendrerit, mi libero facilisis nunc, fringilla molestie dui est vulputate diam. Duis ac justo euismod, sagittis est at, bibendum purus. Praesent nec tortor vel ante accumsan lobortis. Morbi mollis augue nec dolor feugiat congue. Nullam eget blandit nisi. Sed in arcu odio. Aenean malesuada tortor quis felis dapibus congue.",
142+
True,
143+
),
144+
IssueEvidence("name", "Test Name", False),
145+
],
146+
type=FeedbackGroup,
147+
detection_time=datetime.now(UTC),
148+
level="info",
149+
)
122150
TEST_PERF_ISSUE_OCCURRENCE = IssueOccurrence(
123151
uuid.uuid4().hex,
124152
1,

src/sentry/web/debug_urls.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
)
1818
from sentry.web.frontend.debug.debug_cron_muted_monitor_email import DebugCronMutedMonitorEmailView
1919
from sentry.web.frontend.debug.debug_error_embed import DebugErrorPageEmbedView
20+
from sentry.web.frontend.debug.debug_feedback_issue import DebugFeedbackIssueEmailView
2021
from sentry.web.frontend.debug.debug_generic_issue import DebugGenericIssueEmailView
2122
from sentry.web.frontend.debug.debug_incident_activity_email import DebugIncidentActivityEmailView
2223
from sentry.web.frontend.debug.debug_incident_trigger_email import DebugIncidentTriggerEmailView
@@ -83,6 +84,7 @@
8384

8485
urlpatterns = [
8586
re_path(r"^debug/mail/error-alert/$", sentry.web.frontend.debug.mail.alert),
87+
re_path(r"^debug/mail/feedback-alert/$", DebugFeedbackIssueEmailView.as_view()),
8688
re_path(
8789
r"^debug/mail/performance-alert/(?P<sample_name>[^\/]+)?/$",
8890
DebugPerformanceIssueEmailView.as_view(),

0 commit comments

Comments
 (0)