diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ff7359bf..9e408af0b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ **Features**: - Add an option to use the stack pointer as an upper limit for the stack capture range in `crashpad` on Windows. This is useful for targets like Proton/Wine, where one can't rely on the TEB-derived upper bound being correctly maintained by the system, leading to overly large stack captures per thread. ([#1427](https://github.com/getsentry/sentry-native/pull/1427), [crashpad#137](https://github.com/getsentry/crashpad/pull/137)) +- Add attachment support to user feedback ([#1414](https://github.com/getsentry/sentry-native/pull/1414)) **Fixes**: diff --git a/examples/example.c b/examples/example.c index 7b97b21c4..bb9f97cf5 100644 --- a/examples/example.c +++ b/examples/example.c @@ -731,6 +731,35 @@ main(int argc, char **argv) sentry_capture_feedback(user_feedback); } + if (has_arg(argc, argv, "capture-user-feedback-with-attachment")) { + sentry_value_t user_feedback = sentry_value_new_feedback( + "some-message", "some-email", "some-name", NULL); + + // Create a hint and attach both file and byte data + sentry_feedback_hint_t *hint = sentry_feedback_hint_new(); + + // Create a temporary file for the attachment + const char *attachment_path = ".sentry-test-feedback-attachment"; + FILE *f = fopen(attachment_path, "w"); + if (f) { + fprintf(f, "This is feedback attachment content"); + fclose(f); + } + + // Attach a file + sentry_feedback_hint_attach_file(hint, attachment_path); + + // Attach bytes data (e.g., binary data from memory) + const char *binary_data = "binary attachment data"; + sentry_feedback_hint_attach_bytes( + hint, binary_data, strlen(binary_data), "additional-info.txt"); + + // Capture feedback with attachments + sentry_capture_feedback_with_hint(user_feedback, hint); + + // Clean up the temporary file + remove(attachment_path); + } if (has_arg(argc, argv, "capture-user-report")) { sentry_value_t event = sentry_value_new_message_event( SENTRY_LEVEL_INFO, "my-logger", "Hello user feedback!"); diff --git a/include/sentry.h b/include/sentry.h index 4e65ca767..6da6432a0 100644 --- a/include/sentry.h +++ b/include/sentry.h @@ -2763,6 +2763,73 @@ SENTRY_API sentry_value_t sentry_value_new_feedback_n(const char *message, */ SENTRY_API void sentry_capture_feedback(sentry_value_t user_feedback); +/** + * A hint that can be passed to feedback capture to provide additional context, + * such as attachments. + */ +struct sentry_feedback_hint_s; +typedef struct sentry_feedback_hint_s sentry_feedback_hint_t; + +/** + * Creates a new feedback hint. + */ +SENTRY_API sentry_feedback_hint_t *sentry_feedback_hint_new(void); + +/** + * Attaches a file to a feedback hint. + * + * The file will be read and sent when the feedback is captured. + * Returns a pointer to the attachment, or NULL on error. + */ +SENTRY_API sentry_attachment_t *sentry_feedback_hint_attach_file( + sentry_feedback_hint_t *hint, const char *path); +SENTRY_API sentry_attachment_t *sentry_feedback_hint_attach_file_n( + sentry_feedback_hint_t *hint, const char *path, size_t path_len); + +/** + * Attaches bytes to a feedback hint. + * + * The data is copied internally and will be sent when the feedback is captured. + * Returns a pointer to the attachment, or NULL on error. + */ +SENTRY_API sentry_attachment_t *sentry_feedback_hint_attach_bytes( + sentry_feedback_hint_t *hint, const char *buf, size_t buf_len, + const char *filename); +SENTRY_API sentry_attachment_t *sentry_feedback_hint_attach_bytes_n( + sentry_feedback_hint_t *hint, const char *buf, size_t buf_len, + const char *filename, size_t filename_len); + +#ifdef SENTRY_PLATFORM_WINDOWS +/** + * Wide char version of `sentry_feedback_hint_attach_file`. + */ +SENTRY_API sentry_attachment_t *sentry_feedback_hint_attach_filew( + sentry_feedback_hint_t *hint, const wchar_t *path); +SENTRY_API sentry_attachment_t *sentry_feedback_hint_attach_filew_n( + sentry_feedback_hint_t *hint, const wchar_t *path, size_t path_len); + +/** + * Wide char version of `sentry_feedback_hint_attach_bytes`. + */ +SENTRY_API sentry_attachment_t *sentry_feedback_hint_attach_bytesw( + sentry_feedback_hint_t *hint, const char *buf, size_t buf_len, + const wchar_t *filename); +SENTRY_API sentry_attachment_t *sentry_feedback_hint_attach_bytesw_n( + sentry_feedback_hint_t *hint, const char *buf, size_t buf_len, + const wchar_t *filename, size_t filename_len); +#endif + +/** + * Captures a manually created feedback with a hint and sends it to Sentry. + * + * This function takes ownership of both the feedback value and the hint, + * which will be freed automatically. + * + * The hint parameter can be NULL if no additional context is needed. + */ +SENTRY_API void sentry_capture_feedback_with_hint( + sentry_value_t user_feedback, sentry_feedback_hint_t *hint); + /** * The status of a Span or Transaction. * diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 738d1c110..e24e9d169 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -13,6 +13,8 @@ sentry_target_sources_cwd(sentry sentry_database.h sentry_envelope.c sentry_envelope.h + sentry_feedback.c + sentry_feedback.h sentry_info.c sentry_json.c sentry_json.h diff --git a/src/sentry_core.c b/src/sentry_core.c index 85b6185ed..e4f9bf5b2 100644 --- a/src/sentry_core.c +++ b/src/sentry_core.c @@ -8,6 +8,7 @@ #include "sentry_core.h" #include "sentry_database.h" #include "sentry_envelope.h" +#include "sentry_feedback.h" #include "sentry_logs.h" #include "sentry_options.h" #include "sentry_path.h" @@ -766,7 +767,8 @@ prepare_user_report(sentry_value_t user_report) } static sentry_envelope_t * -prepare_user_feedback(sentry_value_t user_feedback) +prepare_user_feedback( + sentry_value_t user_feedback, sentry_feedback_hint_t *hint) { sentry_envelope_t *envelope = NULL; @@ -776,6 +778,10 @@ prepare_user_feedback(sentry_value_t user_feedback) goto fail; } + if (hint && hint->attachments) { + sentry__envelope_add_attachments(envelope, hint->attachments); + } + return envelope; fail: @@ -1483,17 +1489,27 @@ sentry_capture_user_feedback(sentry_value_t user_report) void sentry_capture_feedback(sentry_value_t user_feedback) +{ + // Reuse the implementation with NULL hint + sentry_capture_feedback_with_hint(user_feedback, NULL); +} + +void +sentry_capture_feedback_with_hint( + sentry_value_t user_feedback, sentry_feedback_hint_t *hint) { sentry_envelope_t *envelope = NULL; SENTRY_WITH_OPTIONS (options) { - envelope = prepare_user_feedback(user_feedback); + envelope = prepare_user_feedback(user_feedback, hint); if (envelope) { sentry__capture_envelope(options->transport, envelope); - } else { - sentry_value_decref(user_feedback); } } + + if (hint) { + sentry__feedback_hint_free(hint); + } } bool diff --git a/src/sentry_feedback.c b/src/sentry_feedback.c new file mode 100644 index 000000000..3ad4e37d1 --- /dev/null +++ b/src/sentry_feedback.c @@ -0,0 +1,112 @@ +#include "sentry_feedback.h" + +#include "sentry_alloc.h" +#include "sentry_attachment.h" +#include "sentry_path.h" +#include "sentry_string.h" + +#include + +sentry_feedback_hint_t * +sentry_feedback_hint_new(void) +{ + sentry_feedback_hint_t *hint = SENTRY_MAKE(sentry_feedback_hint_t); + if (!hint) { + return NULL; + } + memset(hint, 0, sizeof(sentry_feedback_hint_t)); + return hint; +} + +void +sentry__feedback_hint_free(sentry_feedback_hint_t *hint) +{ + if (!hint) { + return; + } + sentry__attachments_free(hint->attachments); + sentry_free(hint); +} + +sentry_attachment_t * +sentry_feedback_hint_attach_file(sentry_feedback_hint_t *hint, const char *path) +{ + return sentry_feedback_hint_attach_file_n( + hint, path, sentry__guarded_strlen(path)); +} + +sentry_attachment_t * +sentry_feedback_hint_attach_file_n( + sentry_feedback_hint_t *hint, const char *path, size_t path_len) +{ + if (!hint) { + return NULL; + } + return sentry__attachments_add_path(&hint->attachments, + sentry__path_from_str_n(path, path_len), ATTACHMENT, NULL); +} + +sentry_attachment_t * +sentry_feedback_hint_attach_bytes(sentry_feedback_hint_t *hint, const char *buf, + size_t buf_len, const char *filename) +{ + return sentry_feedback_hint_attach_bytes_n( + hint, buf, buf_len, filename, sentry__guarded_strlen(filename)); +} + +sentry_attachment_t * +sentry_feedback_hint_attach_bytes_n(sentry_feedback_hint_t *hint, + const char *buf, size_t buf_len, const char *filename, size_t filename_len) +{ + if (!hint) { + return NULL; + } + return sentry__attachments_add(&hint->attachments, + sentry__attachment_from_buffer( + buf, buf_len, sentry__path_from_str_n(filename, filename_len)), + ATTACHMENT, NULL); +} + +#ifdef SENTRY_PLATFORM_WINDOWS +sentry_attachment_t * +sentry_feedback_hint_attach_filew( + sentry_feedback_hint_t *hint, const wchar_t *path) +{ + size_t path_len = path ? wcslen(path) : 0; + return sentry_feedback_hint_attach_filew_n(hint, path, path_len); +} + +sentry_attachment_t * +sentry_feedback_hint_attach_filew_n( + sentry_feedback_hint_t *hint, const wchar_t *path, size_t path_len) +{ + if (!hint) { + return NULL; + } + return sentry__attachments_add_path(&hint->attachments, + sentry__path_from_wstr_n(path, path_len), ATTACHMENT, NULL); +} + +sentry_attachment_t * +sentry_feedback_hint_attach_bytesw(sentry_feedback_hint_t *hint, + const char *buf, size_t buf_len, const wchar_t *filename) +{ + size_t filename_len = filename ? wcslen(filename) : 0; + return sentry_feedback_hint_attach_bytesw_n( + hint, buf, buf_len, filename, filename_len); +} + +sentry_attachment_t * +sentry_feedback_hint_attach_bytesw_n(sentry_feedback_hint_t *hint, + const char *buf, size_t buf_len, const wchar_t *filename, + size_t filename_len) +{ + if (!hint) { + return NULL; + } + return sentry__attachments_add(&hint->attachments, + sentry__attachment_from_buffer( + buf, buf_len, sentry__path_from_wstr_n(filename, filename_len)), + ATTACHMENT, NULL); +} +#endif diff --git a/src/sentry_feedback.h b/src/sentry_feedback.h new file mode 100644 index 000000000..8c9c3479f --- /dev/null +++ b/src/sentry_feedback.h @@ -0,0 +1,19 @@ +#ifndef SENTRY_FEEDBACK_H_INCLUDED +#define SENTRY_FEEDBACK_H_INCLUDED + +#include "sentry_boot.h" + +/** + * A sentry Feedback Hint used to pass additional data along with a feedback + * when it's being captured. + */ +struct sentry_feedback_hint_s { + sentry_attachment_t *attachments; +}; + +/** + * Frees a feedback hint (internal use only). + */ +void sentry__feedback_hint_free(sentry_feedback_hint_t *hint); + +#endif diff --git a/tests/test_integration_http.py b/tests/test_integration_http.py index f1aefbff1..095ba63d6 100644 --- a/tests/test_integration_http.py +++ b/tests/test_integration_http.py @@ -175,6 +175,39 @@ def test_user_feedback_http(cmake, httpserver): assert_user_feedback(envelope) +def test_user_feedback_with_attachments_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-with-attachment"], + env=env, + ) + + assert len(httpserver.log) == 1 + output = httpserver.log[0][0].get_data() + envelope = Envelope.deserialize(output) + + # Verify the feedback is present + assert_user_feedback(envelope) + + # Verify attachments are present + attachment_count = 0 + for item in envelope: + if item.headers.get("type") == "attachment": + attachment_count += 1 + + # Should have 2 attachments (one file, one bytes) + assert attachment_count == 2 + + def test_user_report_http(cmake, httpserver): tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "none"}) diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt index b6c8dc0fc..c889b9320 100644 --- a/tests/unit/CMakeLists.txt +++ b/tests/unit/CMakeLists.txt @@ -28,6 +28,7 @@ add_executable(sentry_test_unit test_embedded_info.c test_envelopes.c test_failures.c + test_feedback.c test_fuzzfailures.c test_info.c test_logger.c diff --git a/tests/unit/test_feedback.c b/tests/unit/test_feedback.c new file mode 100644 index 000000000..e14953e2e --- /dev/null +++ b/tests/unit/test_feedback.c @@ -0,0 +1,215 @@ +#include "sentry_attachment.h" +#include "sentry_envelope.h" +#include "sentry_feedback.h" +#include "sentry_path.h" +#include "sentry_testsupport.h" +#include "sentry_transport.h" + +typedef struct { + uint64_t called; + sentry_stringbuilder_t serialized_envelope; +} sentry_feedback_testdata_t; + +static void +send_envelope_test_feedback(sentry_envelope_t *envelope, void *_data) +{ + sentry_feedback_testdata_t *data = _data; + data->called += 1; + sentry__envelope_serialize_into_stringbuilder( + envelope, &data->serialized_envelope); + sentry_envelope_free(envelope); +} + +static void +setup_feedback_test(sentry_feedback_testdata_t *testdata) +{ + testdata->called = 0; + sentry__stringbuilder_init(&testdata->serialized_envelope); + + SENTRY_TEST_OPTIONS_NEW(options); + sentry_options_set_auto_session_tracking(options, false); + sentry_options_set_dsn(options, "https://foo@sentry.invalid/42"); + sentry_transport_t *transport + = sentry_transport_new(send_envelope_test_feedback); + sentry_transport_set_state(transport, testdata); + sentry_options_set_transport(options, transport); + + sentry_init(options); +} + +SENTRY_TEST(feedback_without_hint) +{ + sentry_feedback_testdata_t testdata; + setup_feedback_test(&testdata); + + sentry_uuid_t event_id + = sentry_uuid_from_string("4c035723-8638-4c3a-923f-2ab9d08b4018"); + sentry_value_t feedback = sentry_value_new_feedback( + "test message", "test@example.com", "Test User", &event_id); + + sentry_capture_feedback(feedback); + + char *serialized + = sentry_stringbuilder_take_string(&testdata.serialized_envelope); + TEST_CHECK(strstr(serialized, "\"type\":\"feedback\"") != NULL); + TEST_CHECK(strstr(serialized, "\"type\":\"attachment\"") == NULL); + sentry_free(serialized); + + sentry_close(); + + TEST_CHECK_INT_EQUAL(testdata.called, 1); +} + +SENTRY_TEST(feedback_with_null_hint) +{ + sentry_feedback_testdata_t testdata; + setup_feedback_test(&testdata); + + sentry_uuid_t event_id + = sentry_uuid_from_string("4c035723-8638-4c3a-923f-2ab9d08b4018"); + sentry_value_t feedback = sentry_value_new_feedback( + "test message", "test@example.com", "Test User", &event_id); + + sentry_capture_feedback_with_hint(feedback, NULL); + + char *serialized + = sentry_stringbuilder_take_string(&testdata.serialized_envelope); + TEST_CHECK(strstr(serialized, "\"type\":\"feedback\"") != NULL); + TEST_CHECK(strstr(serialized, "\"type\":\"attachment\"") == NULL); + sentry_free(serialized); + + sentry_close(); + + TEST_CHECK_INT_EQUAL(testdata.called, 1); +} + +SENTRY_TEST(feedback_with_file_attachment) +{ + sentry_feedback_testdata_t testdata; + setup_feedback_test(&testdata); + + sentry_path_t *attachment_path + = sentry__path_from_str(SENTRY_TEST_PATH_PREFIX ".feedback-attachment"); + sentry__path_write_buffer(attachment_path, "feedback data", 13); + + sentry_uuid_t event_id + = sentry_uuid_from_string("4c035723-8638-4c3a-923f-2ab9d08b4018"); + sentry_value_t feedback = sentry_value_new_feedback( + "test message", "test@example.com", "Test User", &event_id); + + sentry_feedback_hint_t *hint = sentry_feedback_hint_new(); + TEST_CHECK(hint != NULL); + + sentry_attachment_t *attachment = sentry_feedback_hint_attach_file( + hint, SENTRY_TEST_PATH_PREFIX ".feedback-attachment"); + TEST_CHECK(attachment != NULL); + + sentry_capture_feedback_with_hint(feedback, hint); + + char *serialized + = sentry_stringbuilder_take_string(&testdata.serialized_envelope); + TEST_CHECK(strstr(serialized, "\"type\":\"feedback\"") != NULL); + TEST_CHECK(strstr(serialized, + "{\"type\":\"attachment\",\"length\":13," + "\"filename\":\".feedback-attachment\"}\n" + "feedback data") + != NULL); + sentry_free(serialized); + + sentry_close(); + + sentry__path_remove(attachment_path); + sentry__path_free(attachment_path); + + TEST_CHECK_INT_EQUAL(testdata.called, 1); +} + +SENTRY_TEST(feedback_with_bytes_attachment) +{ + sentry_feedback_testdata_t testdata; + setup_feedback_test(&testdata); + + sentry_uuid_t event_id + = sentry_uuid_from_string("4c035723-8638-4c3a-923f-2ab9d08b4018"); + sentry_value_t feedback = sentry_value_new_feedback( + "test message", "test@example.com", "Test User", &event_id); + + sentry_feedback_hint_t *hint = sentry_feedback_hint_new(); + TEST_CHECK(hint != NULL); + + const char binary_data[] = "binary\0data\0here"; + sentry_attachment_t *attachment = sentry_feedback_hint_attach_bytes( + hint, binary_data, sizeof(binary_data) - 1, "binary.dat"); + TEST_CHECK(attachment != NULL); + + sentry_capture_feedback_with_hint(feedback, hint); + + char *serialized + = sentry_stringbuilder_take_string(&testdata.serialized_envelope); + TEST_CHECK(strstr(serialized, "\"type\":\"feedback\"") != NULL); + TEST_CHECK(strstr(serialized, + "{\"type\":\"attachment\",\"length\":16," + "\"filename\":\"binary.dat\"}") + != NULL); + TEST_CHECK(strstr(serialized, "binary") != NULL); + sentry_free(serialized); + + sentry_close(); + + TEST_CHECK_INT_EQUAL(testdata.called, 1); +} + +SENTRY_TEST(feedback_with_multiple_attachments) +{ + sentry_feedback_testdata_t testdata; + setup_feedback_test(&testdata); + + sentry_path_t *file1 + = sentry__path_from_str(SENTRY_TEST_PATH_PREFIX ".feedback-file1"); + sentry_path_t *file2 + = sentry__path_from_str(SENTRY_TEST_PATH_PREFIX ".feedback-file2"); + sentry__path_write_buffer(file1, "file1 content", 13); + sentry__path_write_buffer(file2, "file2 content", 13); + + sentry_uuid_t event_id + = sentry_uuid_from_string("4c035723-8638-4c3a-923f-2ab9d08b4018"); + sentry_value_t feedback = sentry_value_new_feedback( + "test message", "test@example.com", "Test User", &event_id); + + sentry_feedback_hint_t *hint = sentry_feedback_hint_new(); + TEST_CHECK(hint != NULL); + + sentry_attachment_t *attachment1 = sentry_feedback_hint_attach_file( + hint, SENTRY_TEST_PATH_PREFIX ".feedback-file1"); + TEST_CHECK(attachment1 != NULL); + + sentry_attachment_t *attachment2 = sentry_feedback_hint_attach_bytes( + hint, "bytes content", 13, "bytes.txt"); + TEST_CHECK(attachment2 != NULL); + + sentry_attachment_t *attachment3 = sentry_feedback_hint_attach_file( + hint, SENTRY_TEST_PATH_PREFIX ".feedback-file2"); + TEST_CHECK(attachment3 != NULL); + + sentry_capture_feedback_with_hint(feedback, hint); + + char *serialized + = sentry_stringbuilder_take_string(&testdata.serialized_envelope); + TEST_CHECK(strstr(serialized, "\"type\":\"feedback\"") != NULL); + TEST_CHECK(strstr(serialized, "\"filename\":\".feedback-file1\"") != NULL); + TEST_CHECK(strstr(serialized, "\"filename\":\"bytes.txt\"") != NULL); + TEST_CHECK(strstr(serialized, "\"filename\":\".feedback-file2\"") != NULL); + TEST_CHECK(strstr(serialized, "file1 content") != NULL); + TEST_CHECK(strstr(serialized, "bytes content") != NULL); + TEST_CHECK(strstr(serialized, "file2 content") != NULL); + sentry_free(serialized); + + sentry_close(); + + sentry__path_remove(file1); + sentry__path_remove(file2); + sentry__path_free(file1); + sentry__path_free(file2); + + TEST_CHECK_INT_EQUAL(testdata.called, 1); +} diff --git a/tests/unit/tests.inc b/tests/unit/tests.inc index cdce6a715..16cba35a5 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -78,6 +78,11 @@ XX(empty_transport) XX(event_with_id) XX(exception_without_type_or_value_still_valid) XX(formatted_log_messages) +XX(feedback_without_hint) +XX(feedback_with_null_hint) +XX(feedback_with_file_attachment) +XX(feedback_with_bytes_attachment) +XX(feedback_with_multiple_attachments) XX(fuzz_json) XX(init_failure) XX(internal_uuid_api)