Skip to content

Commit 88cc982

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 6906dad commit 88cc982

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

@@ -604,6 +604,23 @@ def capture_session(
604604
else:
605605
self.session_flusher.add_session(session)
606606

607+
def capture_user_feedback(
608+
self,
609+
feedback, # type: UserFeedback
610+
):
611+
# type: (...) -> None
612+
"""Captures user feedback.
613+
614+
:param feedback: The user feedback to send to Sentry.
615+
"""
616+
headers = {
617+
"event_id": feedback["event_id"],
618+
"sent_at": format_timestamp(datetime_utcnow()),
619+
}
620+
envelope = Envelope(headers=headers)
621+
envelope.add_user_feedback(feedback)
622+
self.transport.capture_envelope(envelope)
623+
607624
def close(
608625
self,
609626
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
)
@@ -585,6 +586,35 @@ def test_capture_event_works(sentry_init):
585586
pytest.raises(EventCapturedError, lambda: capture_event({}))
586587

587588

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

0 commit comments

Comments
 (0)