Skip to content

Commit 7b52cc1

Browse files
committed
feat(api): Support capturing user feedback
This adds an API to the Sentry Python SDK that captures user feedback via envelope. This is implemented very similiarly to how it is done for the JavaScript SDK, see getsentry/sentry-javascript#7729. Fixes GH-1064
1 parent 0ce9021 commit 7b52cc1

File tree

8 files changed

+90
-3
lines changed

8 files changed

+90
-3
lines changed

docs/api.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ Capturing Data
1212
.. autofunction:: sentry_sdk.api.capture_event
1313
.. autofunction:: sentry_sdk.api.capture_exception
1414
.. autofunction:: sentry_sdk.api.capture_message
15+
.. autofunction:: sentry_sdk.api.capture_user_feedback
1516

1617

1718
Enriching Events

sentry_sdk/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"capture_event",
2323
"capture_message",
2424
"capture_exception",
25+
"capture_user_feedback",
2526
"add_breadcrumb",
2627
"configure_scope",
2728
"push_scope",

sentry_sdk/_types.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
from typing import Tuple
2020
from typing import Type
2121
from typing import Union
22-
from typing_extensions import Literal
22+
from typing_extensions import Literal, TypedDict
2323

2424
ExcInfo = Tuple[
2525
Optional[Type[BaseException]], Optional[BaseException], Optional[TracebackType]
@@ -54,6 +54,7 @@
5454
"internal",
5555
"profile",
5656
"statsd",
57+
"user_report",
5758
]
5859
SessionStatus = Literal["ok", "exited", "crashed", "abnormal"]
5960
EndpointType = Literal["store", "envelope"]
@@ -116,3 +117,5 @@
116117
FlushedMetricValue = Union[int, float]
117118

118119
BucketKey = Tuple[MetricType, str, MeasurementUnit, MetricTagsInternal]
120+
121+
UserFeedback = TypedDict('UserFeedback', {"event_id": str, "email": str, "name": str, "comments": str})

sentry_sdk/api.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
BreadcrumbHint,
2323
ExcInfo,
2424
MeasurementUnit,
25+
UserFeedback,
2526
)
2627
from sentry_sdk.tracing import Span
2728

@@ -39,6 +40,7 @@ def overload(x):
3940
"capture_event",
4041
"capture_message",
4142
"capture_exception",
43+
"capture_user_feedback",
4244
"add_breadcrumb",
4345
"configure_scope",
4446
"push_scope",
@@ -109,6 +111,14 @@ def capture_exception(
109111
return Hub.current.capture_exception(error, scope=scope, **scope_args)
110112

111113

114+
@hubmethod
115+
def capture_user_feedback(
116+
feedback # type: UserFeedback
117+
):
118+
# type: (...) -> None
119+
return Hub.current.capture_user_feedback(feedback)
120+
121+
112122
@hubmethod
113123
def add_breadcrumb(
114124
crumb=None, # type: Optional[Breadcrumb]

sentry_sdk/client.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444
from typing import Sequence
4545

4646
from sentry_sdk.scope import Scope
47-
from sentry_sdk._types import Event, Hint
47+
from sentry_sdk._types import Event, Hint, UserFeedback
4848
from sentry_sdk.session import Session
4949

5050

@@ -633,6 +633,23 @@ def capture_session(
633633
else:
634634
self.session_flusher.add_session(session)
635635

636+
def capture_user_feedback(
637+
self,
638+
feedback, # type: UserFeedback
639+
):
640+
# type: (...) -> None
641+
"""Captures user feedback.
642+
643+
:param feedback: The user feedback to send to Sentry.
644+
"""
645+
headers = {
646+
"event_id": feedback["event_id"],
647+
"sent_at": format_timestamp(datetime_utcnow()),
648+
}
649+
envelope = Envelope(headers=headers)
650+
envelope.add_user_feedback(feedback)
651+
self.transport.capture_envelope(envelope)
652+
636653
def close(
637654
self,
638655
timeout=None, # type: Optional[float]

sentry_sdk/envelope.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from typing import List
1616
from typing import Iterator
1717

18-
from sentry_sdk._types import Event, EventDataCategory
18+
from sentry_sdk._types import Event, EventDataCategory, UserFeedback
1919

2020

2121
def parse_json(data):
@@ -94,6 +94,13 @@ def add_item(
9494
# type: (...) -> None
9595
self.items.append(item)
9696

97+
def add_user_feedback(
98+
self,
99+
feedback, # type: UserFeedback
100+
):
101+
# type: (...) -> None
102+
self.add_item(Item(payload=PayloadRef(json=feedback), type="user_report"))
103+
97104
def get_event(self):
98105
# type: (...) -> Optional[Event]
99106
for items in self.items:
@@ -258,6 +265,8 @@ def data_category(self):
258265
return "error"
259266
elif ty == "client_report":
260267
return "internal"
268+
elif ty == "user_report":
269+
return "user_report"
261270
elif ty == "profile":
262271
return "profile"
263272
elif ty == "statsd":

sentry_sdk/hub.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
Breadcrumb,
5252
BreadcrumbHint,
5353
ExcInfo,
54+
UserFeedback,
5455
)
5556
from sentry_sdk.consts import ClientConstructor
5657

@@ -403,6 +404,21 @@ def capture_exception(self, error=None, scope=None, **scope_args):
403404

404405
return None
405406

407+
def capture_user_feedback(self, feedback):
408+
# type: (UserFeedback) -> None
409+
"""
410+
Captures user feedback.
411+
412+
:param feedback: The user feedback to send to Sentry.
413+
414+
Alias of :py:meth:`sentry_sdk.Client.capture_user_feedback`.
415+
"""
416+
client, _ = self._stack[-1]
417+
if client is not None:
418+
client.capture_user_feedback(feedback)
419+
420+
return None
421+
406422
def _capture_internal_exception(
407423
self, exc_info # type: Any
408424
):

tests/test_client.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
capture_message,
1616
capture_exception,
1717
capture_event,
18+
capture_user_feedback,
1819
start_transaction,
1920
set_tag,
2021
)
@@ -591,6 +592,35 @@ def test_capture_event_works(sentry_init):
591592
pytest.raises(EventCapturedError, lambda: capture_event({}))
592593

593594

595+
def test_capture_user_feedback_works(sentry_init, capture_envelopes):
596+
expected_event_id = "test_event_id"
597+
expected_name = "test_name"
598+
expected_email = "test_email"
599+
expected_comments = "test_comments"
600+
601+
sentry_init(attach_stacktrace=False)
602+
envelopes = capture_envelopes()
603+
604+
capture_user_feedback({
605+
"event_id": expected_event_id,
606+
"email": expected_email,
607+
"comments": expected_comments,
608+
"name": expected_name,
609+
})
610+
611+
assert len(envelopes) == 1
612+
user_feedback_envelope = envelopes[0]
613+
assert user_feedback_envelope.headers["event_id"] == expected_event_id
614+
assert len(user_feedback_envelope.items) == 1
615+
user_feedback_item = user_feedback_envelope.items[0]
616+
assert user_feedback_item.data_category == "user_report"
617+
assert user_feedback_item.headers["type"] == "user_report"
618+
assert user_feedback_item.payload.json["event_id"] == expected_event_id
619+
assert user_feedback_item.payload.json["email"] == expected_email
620+
assert user_feedback_item.payload.json["name"] == expected_name
621+
assert user_feedback_item.payload.json["comments"] == expected_comments
622+
623+
594624
@pytest.mark.parametrize("num_messages", [10, 20])
595625
def test_atexit(tmpdir, monkeypatch, num_messages):
596626
app = tmpdir.join("app.py")

0 commit comments

Comments
 (0)