Skip to content

Commit

Permalink
feat(api): Support capturing user feedback
Browse files Browse the repository at this point in the history
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 getsentryGH-1064
  • Loading branch information
theCapypara committed Oct 30, 2023
1 parent 0ce9021 commit 7b52cc1
Show file tree
Hide file tree
Showing 8 changed files with 90 additions and 3 deletions.
1 change: 1 addition & 0 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Capturing Data
.. autofunction:: sentry_sdk.api.capture_event
.. autofunction:: sentry_sdk.api.capture_exception
.. autofunction:: sentry_sdk.api.capture_message
.. autofunction:: sentry_sdk.api.capture_user_feedback


Enriching Events
Expand Down
1 change: 1 addition & 0 deletions sentry_sdk/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"capture_event",
"capture_message",
"capture_exception",
"capture_user_feedback",
"add_breadcrumb",
"configure_scope",
"push_scope",
Expand Down
5 changes: 4 additions & 1 deletion sentry_sdk/_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
from typing import Tuple
from typing import Type
from typing import Union
from typing_extensions import Literal
from typing_extensions import Literal, TypedDict

ExcInfo = Tuple[
Optional[Type[BaseException]], Optional[BaseException], Optional[TracebackType]
Expand Down Expand Up @@ -54,6 +54,7 @@
"internal",
"profile",
"statsd",
"user_report",
]
SessionStatus = Literal["ok", "exited", "crashed", "abnormal"]
EndpointType = Literal["store", "envelope"]
Expand Down Expand Up @@ -116,3 +117,5 @@
FlushedMetricValue = Union[int, float]

BucketKey = Tuple[MetricType, str, MeasurementUnit, MetricTagsInternal]

UserFeedback = TypedDict('UserFeedback', {"event_id": str, "email": str, "name": str, "comments": str})
10 changes: 10 additions & 0 deletions sentry_sdk/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
BreadcrumbHint,
ExcInfo,
MeasurementUnit,
UserFeedback,
)
from sentry_sdk.tracing import Span

Expand All @@ -39,6 +40,7 @@ def overload(x):
"capture_event",
"capture_message",
"capture_exception",
"capture_user_feedback",
"add_breadcrumb",
"configure_scope",
"push_scope",
Expand Down Expand Up @@ -109,6 +111,14 @@ def capture_exception(
return Hub.current.capture_exception(error, scope=scope, **scope_args)


@hubmethod
def capture_user_feedback(
feedback # type: UserFeedback
):
# type: (...) -> None
return Hub.current.capture_user_feedback(feedback)


@hubmethod
def add_breadcrumb(
crumb=None, # type: Optional[Breadcrumb]
Expand Down
19 changes: 18 additions & 1 deletion sentry_sdk/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
from typing import Sequence

from sentry_sdk.scope import Scope
from sentry_sdk._types import Event, Hint
from sentry_sdk._types import Event, Hint, UserFeedback
from sentry_sdk.session import Session


Expand Down Expand Up @@ -633,6 +633,23 @@ def capture_session(
else:
self.session_flusher.add_session(session)

def capture_user_feedback(
self,
feedback, # type: UserFeedback
):
# type: (...) -> None
"""Captures user feedback.
:param feedback: The user feedback to send to Sentry.
"""
headers = {
"event_id": feedback["event_id"],
"sent_at": format_timestamp(datetime_utcnow()),
}
envelope = Envelope(headers=headers)
envelope.add_user_feedback(feedback)
self.transport.capture_envelope(envelope)

def close(
self,
timeout=None, # type: Optional[float]
Expand Down
11 changes: 10 additions & 1 deletion sentry_sdk/envelope.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from typing import List
from typing import Iterator

from sentry_sdk._types import Event, EventDataCategory
from sentry_sdk._types import Event, EventDataCategory, UserFeedback


def parse_json(data):
Expand Down Expand Up @@ -94,6 +94,13 @@ def add_item(
# type: (...) -> None
self.items.append(item)

def add_user_feedback(
self,
feedback, # type: UserFeedback
):
# type: (...) -> None
self.add_item(Item(payload=PayloadRef(json=feedback), type="user_report"))

def get_event(self):
# type: (...) -> Optional[Event]
for items in self.items:
Expand Down Expand Up @@ -258,6 +265,8 @@ def data_category(self):
return "error"
elif ty == "client_report":
return "internal"
elif ty == "user_report":
return "user_report"
elif ty == "profile":
return "profile"
elif ty == "statsd":
Expand Down
16 changes: 16 additions & 0 deletions sentry_sdk/hub.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
Breadcrumb,
BreadcrumbHint,
ExcInfo,
UserFeedback,
)
from sentry_sdk.consts import ClientConstructor

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

return None

def capture_user_feedback(self, feedback):
# type: (UserFeedback) -> None
"""
Captures user feedback.
:param feedback: The user feedback to send to Sentry.
Alias of :py:meth:`sentry_sdk.Client.capture_user_feedback`.
"""
client, _ = self._stack[-1]
if client is not None:
client.capture_user_feedback(feedback)

return None

def _capture_internal_exception(
self, exc_info # type: Any
):
Expand Down
30 changes: 30 additions & 0 deletions tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
capture_message,
capture_exception,
capture_event,
capture_user_feedback,
start_transaction,
set_tag,
)
Expand Down Expand Up @@ -591,6 +592,35 @@ def test_capture_event_works(sentry_init):
pytest.raises(EventCapturedError, lambda: capture_event({}))


def test_capture_user_feedback_works(sentry_init, capture_envelopes):
expected_event_id = "test_event_id"
expected_name = "test_name"
expected_email = "test_email"
expected_comments = "test_comments"

sentry_init(attach_stacktrace=False)
envelopes = capture_envelopes()

capture_user_feedback({
"event_id": expected_event_id,
"email": expected_email,
"comments": expected_comments,
"name": expected_name,
})

assert len(envelopes) == 1
user_feedback_envelope = envelopes[0]
assert user_feedback_envelope.headers["event_id"] == expected_event_id
assert len(user_feedback_envelope.items) == 1
user_feedback_item = user_feedback_envelope.items[0]
assert user_feedback_item.data_category == "user_report"
assert user_feedback_item.headers["type"] == "user_report"
assert user_feedback_item.payload.json["event_id"] == expected_event_id
assert user_feedback_item.payload.json["email"] == expected_email
assert user_feedback_item.payload.json["name"] == expected_name
assert user_feedback_item.payload.json["comments"] == expected_comments


@pytest.mark.parametrize("num_messages", [10, 20])
def test_atexit(tmpdir, monkeypatch, num_messages):
app = tmpdir.join("app.py")
Expand Down

0 comments on commit 7b52cc1

Please sign in to comment.