Skip to content

Commit

Permalink
Add user feedback capability to the Native SDK (#966)
Browse files Browse the repository at this point in the history
  • Loading branch information
tustanivsky authored Mar 21, 2024
1 parent 9b1bc43 commit fda2a37
Show file tree
Hide file tree
Showing 13 changed files with 238 additions and 3 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Unreleased

**Features**

Add user feedback capability to the Native SDK ([#966](https://github.com/getsentry/sentry-native/pull/966))

**Internal**:

- Remove the `CRASHPAD_WER_ENABLED` build flag. The WER module is now built for all supported Windows targets, and registration is conditional on runtime Windows version checks. ([#950](https://github.com/getsentry/sentry-native/pull/950), [crashpad#96](https://github.com/getsentry/crashpad/pull/96))
Expand Down
10 changes: 10 additions & 0 deletions examples/example.c
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,16 @@ main(int argc, char **argv)

sentry_capture_event(event);
}
if (has_arg(argc, argv, "capture-user-feedback")) {
sentry_value_t event = sentry_value_new_message_event(
SENTRY_LEVEL_INFO, "my-logger", "Hello user feedback!");
sentry_uuid_t event_id = sentry_capture_event(event);

sentry_value_t user_feedback = sentry_value_new_user_feedback(
&event_id, "some-name", "some-email", "some-comment");

sentry_capture_user_feedback(user_feedback);
}

if (has_arg(argc, argv, "capture-transaction")) {
sentry_transaction_context_t *tx_ctx
Expand Down
21 changes: 21 additions & 0 deletions include/sentry.h
Original file line number Diff line number Diff line change
Expand Up @@ -1888,6 +1888,27 @@ SENTRY_EXPERIMENTAL_API void sentry_transaction_set_name(
SENTRY_EXPERIMENTAL_API void sentry_transaction_set_name_n(
sentry_transaction_t *transaction, const char *name, size_t name_len);

/**
* Creates a new User Feedback with a specific name, email and comments.
*
* See https://develop.sentry.dev/sdk/envelopes/#user-feedback
*
* User Feedback has to be associated with a specific event that has been
* sent to Sentry earlier.
*/
SENTRY_API sentry_value_t sentry_value_new_user_feedback(
const sentry_uuid_t *uuid, const char *name, const char *email,
const char *comments);
SENTRY_API sentry_value_t sentry_value_new_user_feedback_n(
const sentry_uuid_t *uuid, const char *name, size_t name_len,
const char *email, size_t email_len, const char *comments,
size_t comments_len);

/**
* Captures a manually created User Feedback and sends it to Sentry.
*/
SENTRY_API void sentry_capture_user_feedback(sentry_value_t user_feedback);

/**
* The status of a Span or Transaction.
*
Expand Down
34 changes: 34 additions & 0 deletions src/sentry_core.c
Original file line number Diff line number Diff line change
Expand Up @@ -550,6 +550,26 @@ sentry__prepare_transaction(const sentry_options_t *options,
return NULL;
}

sentry_envelope_t *
sentry__prepare_user_feedback(sentry_value_t user_feedback)
{
sentry_envelope_t *envelope = NULL;

envelope = sentry__envelope_new();
if (!envelope
|| !sentry__envelope_add_user_feedback(envelope, user_feedback)) {
goto fail;
}

return envelope;

fail:
SENTRY_WARN("dropping user feedback");
sentry_envelope_free(envelope);
sentry_value_decref(user_feedback);
return NULL;
}

void
sentry_handle_exception(const sentry_ucontext_t *uctx)
{
Expand Down Expand Up @@ -1120,6 +1140,20 @@ sentry_span_finish(sentry_span_t *opaque_span)
sentry__span_decref(opaque_span);
}

void
sentry_capture_user_feedback(sentry_value_t user_feedback)
{
sentry_envelope_t *envelope = NULL;

SENTRY_WITH_OPTIONS (options) {
envelope = sentry__prepare_user_feedback(user_feedback);
if (envelope) {
sentry__capture_envelope(options->transport, envelope);
}
}
sentry_value_decref(user_feedback);
}

int
sentry_get_crashed_last_run(void)
{
Expand Down
30 changes: 30 additions & 0 deletions src/sentry_envelope.c
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,36 @@ sentry__envelope_add_transaction(
return item;
}

sentry_envelope_item_t *
sentry__envelope_add_user_feedback(
sentry_envelope_t *envelope, sentry_value_t user_feedback)
{
sentry_envelope_item_t *item = envelope_add_item(envelope);
if (!item) {
return NULL;
}

sentry_jsonwriter_t *jw = sentry__jsonwriter_new(NULL);
if (!jw) {
return NULL;
}

sentry_value_t event_id = sentry__ensure_event_id(user_feedback, NULL);

sentry__jsonwriter_write_value(jw, user_feedback);
item->payload = sentry__jsonwriter_into_string(jw, &item->payload_len);

sentry__envelope_item_set_header(
item, "type", sentry_value_new_string("user_report"));
sentry_value_t length = sentry_value_new_int32((int32_t)item->payload_len);
sentry__envelope_item_set_header(item, "length", length);

sentry_value_incref(event_id);
sentry__envelope_set_header(envelope, "event_id", event_id);

return item;
}

sentry_envelope_item_t *
sentry__envelope_add_session(
sentry_envelope_t *envelope, const sentry_session_t *session)
Expand Down
6 changes: 6 additions & 0 deletions src/sentry_envelope.h
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@ sentry_envelope_item_t *sentry__envelope_add_event(
sentry_envelope_item_t *sentry__envelope_add_transaction(
sentry_envelope_t *envelope, sentry_value_t transaction);

/**
* Add a user feedback to this envelope.
*/
sentry_envelope_item_t *sentry__envelope_add_user_feedback(
sentry_envelope_t *envelope, sentry_value_t user_feedback);

/**
* Add a session to this envelope.
*/
Expand Down
36 changes: 36 additions & 0 deletions src/sentry_value.c
Original file line number Diff line number Diff line change
Expand Up @@ -1265,6 +1265,42 @@ sentry_value_new_stacktrace(void **ips, size_t len)
return stacktrace;
}

sentry_value_t
sentry_value_new_user_feedback(const sentry_uuid_t *uuid, const char *name,
const char *email, const char *comments)
{
size_t name_len = name ? strlen(name) : 0;
size_t email_len = email ? strlen(email) : 0;
size_t comments_len = email ? strlen(comments) : 0;
return sentry_value_new_user_feedback_n(
uuid, name, name_len, email, email_len, comments, comments_len);
}

sentry_value_t
sentry_value_new_user_feedback_n(const sentry_uuid_t *uuid, const char *name,
size_t name_len, const char *email, size_t email_len, const char *comments,
size_t comments_len)
{
sentry_value_t rv = sentry_value_new_object();

sentry_value_set_by_key(rv, "event_id", sentry__value_new_uuid(uuid));

if (name) {
sentry_value_set_by_key(
rv, "name", sentry_value_new_string_n(name, name_len));
}
if (email) {
sentry_value_set_by_key(
rv, "email", sentry_value_new_string_n(email, email_len));
}
if (comments) {
sentry_value_set_by_key(
rv, "comments", sentry_value_new_string_n(comments, comments_len));
}

return rv;
}

static sentry_value_t
sentry__get_or_insert_values_list(sentry_value_t parent, const char *key)
{
Expand Down
2 changes: 1 addition & 1 deletion tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@ def deserialize_from(
headers = json.loads(line)
length = headers["length"]
payload = f.read(length)
if headers.get("type") in ["event", "session", "transaction"]:
if headers.get("type") in ["event", "session", "transaction", "user_report"]:
rv = cls(headers=headers, payload=PayloadRef(json=json.loads(payload)))
else:
rv = cls(headers=headers, payload=payload)
Expand Down
16 changes: 14 additions & 2 deletions tests/assertions.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,18 @@ def assert_session(envelope, extra_assertion=None):
assert_matches(session, extra_assertion)


def assert_user_feedback(envelope):
user_feedback = None
for item in envelope:
if item.headers.get("type") == "user_report" and item.payload.json is not None:
user_feedback = item.payload.json

assert user_feedback is not None
assert user_feedback["name"] == "some-name"
assert user_feedback["email"] == "some-email"
assert user_feedback["comments"] == "some-comment"


def assert_meta(
envelope,
release="test-example-release",
Expand Down Expand Up @@ -177,12 +189,12 @@ def assert_timestamp(ts, now=datetime.utcnow()):
assert ts[:11] == now.isoformat()[:11]


def assert_event(envelope):
def assert_event(envelope, message="Hello World!"):
event = envelope.get_event()
expected = {
"level": "info",
"logger": "my-logger",
"message": {"formatted": "Hello World!"},
"message": {"formatted": message},
}
assert_matches(event, expected)
assert_timestamp(event["timestamp"])
Expand Down
30 changes: 30 additions & 0 deletions tests/test_integration_http.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
assert_exception,
assert_inproc_crash,
assert_session,
assert_user_feedback,
assert_minidump,
assert_breakpad_crash,
)
Expand Down Expand Up @@ -117,6 +118,35 @@ def test_capture_and_session_http(cmake, httpserver):
assert_session(envelope, {"status": "exited", "errors": 0})


def test_user_feedback_http(cmake, httpserver):
tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "none"})

httpserver.expect_request(
"/api/123456/envelope/",
headers={"x-sentry-auth": auth_header},
).respond_with_data("OK")
env = dict(os.environ, SENTRY_DSN=make_dsn(httpserver))

run(
tmp_path,
"sentry_example",
["log", "capture-user-feedback"],
check=True,
env=env,
)

assert len(httpserver.log) == 2
output = httpserver.log[0][0].get_data()
envelope = Envelope.deserialize(output)

assert_event(envelope, "Hello user feedback!")

output = httpserver.log[1][0].get_data()
envelope = Envelope.deserialize(output)

assert_user_feedback(envelope)


def test_exception_and_session_http(cmake, httpserver):
tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "none"})

Expand Down
29 changes: 29 additions & 0 deletions tests/unit/test_envelopes.c
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,35 @@ SENTRY_TEST(basic_http_request_preparation_for_transaction)
sentry__dsn_decref(dsn);
}

SENTRY_TEST(basic_http_request_preparation_for_user_feedback)
{
sentry_dsn_t *dsn = sentry__dsn_new("https://foo@sentry.invalid/42");

sentry_uuid_t event_id
= sentry_uuid_from_string("c993afb6-b4ac-48a6-b61b-2558e601d65d");
sentry_envelope_t *envelope = sentry__envelope_new();
sentry_value_t user_feedback = sentry_value_new_user_feedback(
&event_id, "some-name", "some-email", "some-comment");
sentry__envelope_add_user_feedback(envelope, user_feedback);

sentry_prepared_http_request_t *req
= sentry__prepare_http_request(envelope, dsn, NULL, NULL);
TEST_CHECK_STRING_EQUAL(req->method, "POST");
TEST_CHECK_STRING_EQUAL(
req->url, "https://sentry.invalid:443/api/42/envelope/");
TEST_CHECK_STRING_EQUAL(req->body,
"{\"event_id\":\"c993afb6-b4ac-48a6-b61b-2558e601d65d\"}\n"
"{\"type\":\"user_report\",\"length\":117}\n"
"{\"event_id\":\"c993afb6-b4ac-48a6-b61b-2558e601d65d\",\"name\":"
"\"some-name\",\"email\":\"some-email\",\"comments\":"
"\"some-comment\"}");
sentry__prepared_http_request_free(req);
sentry_value_decref(user_feedback);
sentry_envelope_free(envelope);

sentry__dsn_decref(dsn);
}

SENTRY_TEST(basic_http_request_preparation_for_event_with_attachment)
{
sentry_dsn_t *dsn = sentry__dsn_new("https://foo@sentry.invalid/42");
Expand Down
21 changes: 21 additions & 0 deletions tests/unit/test_value.c
Original file line number Diff line number Diff line change
Expand Up @@ -772,3 +772,24 @@ SENTRY_TEST(thread_without_name_still_valid)
test_name);
sentry_value_decref(thread);
}

SENTRY_TEST(user_feedback_is_valid)
{
sentry_uuid_t event_id
= sentry_uuid_from_string("c993afb6-b4ac-48a6-b61b-2558e601d65d");
sentry_value_t user_feedback = sentry_value_new_user_feedback(
&event_id, "some-name", "some-email", "some-comment");

TEST_CHECK(!sentry_value_is_null(user_feedback));
TEST_CHECK_STRING_EQUAL(
sentry_value_as_string(sentry_value_get_by_key(user_feedback, "name")),
"some-name");
TEST_CHECK_STRING_EQUAL(
sentry_value_as_string(sentry_value_get_by_key(user_feedback, "email")),
"some-email");
TEST_CHECK_STRING_EQUAL(sentry_value_as_string(sentry_value_get_by_key(
user_feedback, "comments")),
"some-comment");

sentry_value_decref(user_feedback);
}
2 changes: 2 additions & 0 deletions tests/unit/tests.inc
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ XX(basic_http_request_preparation_for_event)
XX(basic_http_request_preparation_for_event_with_attachment)
XX(basic_http_request_preparation_for_minidump)
XX(basic_http_request_preparation_for_transaction)
XX(basic_http_request_preparation_for_user_feedback)
XX(basic_spans)
XX(basic_tracing_context)
XX(basic_transaction)
Expand Down Expand Up @@ -107,6 +108,7 @@ XX(update_from_header_null_ctx)
XX(url_parsing_complete)
XX(url_parsing_invalid)
XX(url_parsing_partial)
XX(user_feedback_is_valid)
XX(uuid_api)
XX(uuid_v4)
XX(value_bool)
Expand Down

0 comments on commit fda2a37

Please sign in to comment.