diff --git a/CHANGELOG.md b/CHANGELOG.md index 50bed07b5..5f20a790f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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)) diff --git a/examples/example.c b/examples/example.c index 6abe54eae..36a84bb44 100644 --- a/examples/example.c +++ b/examples/example.c @@ -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 diff --git a/include/sentry.h b/include/sentry.h index bba0dd0e7..17fc8ec19 100644 --- a/include/sentry.h +++ b/include/sentry.h @@ -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. * diff --git a/src/sentry_core.c b/src/sentry_core.c index 24cf52539..c9ef8cd45 100644 --- a/src/sentry_core.c +++ b/src/sentry_core.c @@ -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) { @@ -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) { diff --git a/src/sentry_envelope.c b/src/sentry_envelope.c index 4b68a2787..b71242c1d 100644 --- a/src/sentry_envelope.c +++ b/src/sentry_envelope.c @@ -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) diff --git a/src/sentry_envelope.h b/src/sentry_envelope.h index b5a8f1ab0..8ea986827 100644 --- a/src/sentry_envelope.h +++ b/src/sentry_envelope.h @@ -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. */ diff --git a/src/sentry_value.c b/src/sentry_value.c index e46eb79f5..3c3e344c9 100644 --- a/src/sentry_value.c +++ b/src/sentry_value.c @@ -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) { diff --git a/tests/__init__.py b/tests/__init__.py index b34e1c2ea..d7374c5b4 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -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) diff --git a/tests/assertions.py b/tests/assertions.py index de97e9b6a..041188378 100644 --- a/tests/assertions.py +++ b/tests/assertions.py @@ -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", @@ -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"]) diff --git a/tests/test_integration_http.py b/tests/test_integration_http.py index ee915f00e..7c1e76cff 100644 --- a/tests/test_integration_http.py +++ b/tests/test_integration_http.py @@ -16,6 +16,7 @@ assert_exception, assert_inproc_crash, assert_session, + assert_user_feedback, assert_minidump, assert_breakpad_crash, ) @@ -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"}) diff --git a/tests/unit/test_envelopes.c b/tests/unit/test_envelopes.c index 7be59db4a..5aa1170d5 100644 --- a/tests/unit/test_envelopes.c +++ b/tests/unit/test_envelopes.c @@ -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"); diff --git a/tests/unit/test_value.c b/tests/unit/test_value.c index 92e195d58..d1012602b 100644 --- a/tests/unit/test_value.c +++ b/tests/unit/test_value.c @@ -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); +} diff --git a/tests/unit/tests.inc b/tests/unit/tests.inc index 1770edfc1..d7f9fae29 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -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) @@ -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)