Skip to content

feat(api): Support capturing user feedback #2442

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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