Skip to content
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

Add user feedback capability to the Native SDK #966

Merged
merged 20 commits into from
Mar 21, 2024
Merged
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
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
Loading