From 9c7fd8f685140d57cddb77ec847158c3633f36e2 Mon Sep 17 00:00:00 2001 From: Ivan Tustanivskyi Date: Thu, 16 Oct 2025 09:46:58 +0300 Subject: [PATCH 01/10] add attachment support for user feedback via hints --- include/sentry.h | 81 ++++++++++++++++++++++++++++++ src/sentry_core.c | 24 ++++++++- src/sentry_feedback.c | 114 ++++++++++++++++++++++++++++++++++++++++++ src/sentry_feedback.h | 14 ++++++ 4 files changed, 231 insertions(+), 2 deletions(-) create mode 100644 src/sentry_feedback.c create mode 100644 src/sentry_feedback.h diff --git a/include/sentry.h b/include/sentry.h index 6de6f826f..f15a97370 100644 --- a/include/sentry.h +++ b/include/sentry.h @@ -2716,6 +2716,87 @@ 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. + * + * The hint can be used to attach files or binary data to feedback submissions. + * Must be freed with sentry_feedback_hint_free() if not passed to + * sentry_capture_feedback_with_hint(). + */ +SENTRY_API sentry_feedback_hint_t *sentry_feedback_hint_new(void); + +/** + * Frees a feedback hint. + * + * Note: You should NOT call this if the hint was passed to + * sentry_capture_feedback_with_hint(), as that function takes ownership. + */ +SENTRY_API void sentry_feedback_hint_free(sentry_feedback_hint_t *hint); + +/** + * 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 binary data 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 feedback with a hint (e.g., attachments). + * + * This function takes ownership of both the feedback value and the hint, + * which will be freed automatically. Users should NOT call + * sentry_value_decref() on the feedback or sentry_feedback_hint_free() + * on the hint after calling this function. + * + * 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/sentry_core.c b/src/sentry_core.c index 25d22d272..3c458d266 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,11 @@ prepare_user_feedback(sentry_value_t user_feedback) goto fail; } + // Add attachments from hint if present + if (hint && hint->attachments) { + sentry__envelope_add_attachments(envelope, hint->attachments); + } + return envelope; fail: @@ -1483,17 +1490,30 @@ 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); } } + + // Take ownership: free hint after use + 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..eaeb52018 --- /dev/null +++ b/src/sentry_feedback.c @@ -0,0 +1,114 @@ +#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 \ No newline at end of file diff --git a/src/sentry_feedback.h b/src/sentry_feedback.h new file mode 100644 index 000000000..cfc3ece79 --- /dev/null +++ b/src/sentry_feedback.h @@ -0,0 +1,14 @@ +#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; +}; + +#endif From a339ca6a6c17ff1708672d7a9fbb955a61fd037b Mon Sep 17 00:00:00 2001 From: Ivan Tustanivskyi Date: Thu, 16 Oct 2025 11:48:19 +0300 Subject: [PATCH 02/10] Add tests --- examples/example.c | 29 ++++++++ tests/test_integration_http.py | 34 ++++++++++ tests/unit/test_feedback.c | 119 +++++++++++++++++++++++++++++++++ tests/unit/tests.inc | 2 + 4 files changed, 184 insertions(+) create mode 100644 tests/unit/test_feedback.c 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/tests/test_integration_http.py b/tests/test_integration_http.py index 92ab58563..0eaf59087 100644 --- a/tests/test_integration_http.py +++ b/tests/test_integration_http.py @@ -177,6 +177,40 @@ 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"], + check=True, + 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/test_feedback.c b/tests/unit/test_feedback.c new file mode 100644 index 000000000..d5109e862 --- /dev/null +++ b/tests/unit/test_feedback.c @@ -0,0 +1,119 @@ +#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); +} + +SENTRY_TEST(feedback_with_file_attachment) +{ + 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_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; + 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_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); +} diff --git a/tests/unit/tests.inc b/tests/unit/tests.inc index 4f666f345..26cbdf71f 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -76,6 +76,8 @@ XX(empty_transport) XX(event_with_id) XX(exception_without_type_or_value_still_valid) XX(formatted_log_messages) +XX(feedback_with_bytes_attachment) +XX(feedback_with_file_attachment) XX(fuzz_json) XX(init_failure) XX(internal_uuid_api) From 812699245b9041f654fae7a03a1c7d9875329bc6 Mon Sep 17 00:00:00 2001 From: Ivan Tustanivskyi Date: Thu, 16 Oct 2025 13:01:51 +0300 Subject: [PATCH 03/10] Add new files to cmake --- src/CMakeLists.txt | 2 ++ tests/unit/CMakeLists.txt | 1 + 2 files changed, 3 insertions(+) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 5b8ccabc5..c291f2a84 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -12,6 +12,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/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 From 3f000f4f5398c911836f6bf2e668d6c80d74db7b Mon Sep 17 00:00:00 2001 From: Ivan Tustanivskyi Date: Thu, 16 Oct 2025 14:54:36 +0300 Subject: [PATCH 04/10] Clean up --- include/sentry.h | 16 +++------------- src/sentry_core.c | 8 +++----- src/sentry_feedback.c | 2 +- src/sentry_feedback.h | 7 ++++++- 4 files changed, 13 insertions(+), 20 deletions(-) diff --git a/include/sentry.h b/include/sentry.h index f15a97370..9bb2d2832 100644 --- a/include/sentry.h +++ b/include/sentry.h @@ -2732,14 +2732,6 @@ typedef struct sentry_feedback_hint_s sentry_feedback_hint_t; */ SENTRY_API sentry_feedback_hint_t *sentry_feedback_hint_new(void); -/** - * Frees a feedback hint. - * - * Note: You should NOT call this if the hint was passed to - * sentry_capture_feedback_with_hint(), as that function takes ownership. - */ -SENTRY_API void sentry_feedback_hint_free(sentry_feedback_hint_t *hint); - /** * Attaches a file to a feedback hint. * @@ -2752,7 +2744,7 @@ SENTRY_API sentry_attachment_t *sentry_feedback_hint_attach_file_n( sentry_feedback_hint_t *hint, const char *path, size_t path_len); /** - * Attaches binary data to a feedback hint. + * 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. @@ -2785,12 +2777,10 @@ SENTRY_API sentry_attachment_t *sentry_feedback_hint_attach_bytesw_n( #endif /** - * Captures feedback with a hint (e.g., attachments). + * 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. Users should NOT call - * sentry_value_decref() on the feedback or sentry_feedback_hint_free() - * on the hint after calling this function. + * which will be freed automatically. * * The hint parameter can be NULL if no additional context is needed. */ diff --git a/src/sentry_core.c b/src/sentry_core.c index 3c458d266..fdab6f9e4 100644 --- a/src/sentry_core.c +++ b/src/sentry_core.c @@ -767,8 +767,8 @@ prepare_user_report(sentry_value_t user_report) } static sentry_envelope_t * -prepare_user_feedback(sentry_value_t user_feedback, - sentry_feedback_hint_t *hint) +prepare_user_feedback( + sentry_value_t user_feedback, sentry_feedback_hint_t *hint) { sentry_envelope_t *envelope = NULL; @@ -778,7 +778,6 @@ prepare_user_feedback(sentry_value_t user_feedback, goto fail; } - // Add attachments from hint if present if (hint && hint->attachments) { sentry__envelope_add_attachments(envelope, hint->attachments); } @@ -1510,9 +1509,8 @@ sentry_capture_feedback_with_hint( } } - // Take ownership: free hint after use if (hint) { - sentry_feedback_hint_free(hint); + sentry__feedback_hint_free(hint); } } diff --git a/src/sentry_feedback.c b/src/sentry_feedback.c index eaeb52018..da0586230 100644 --- a/src/sentry_feedback.c +++ b/src/sentry_feedback.c @@ -19,7 +19,7 @@ sentry_feedback_hint_new(void) } void -sentry_feedback_hint_free(sentry_feedback_hint_t *hint) +sentry__feedback_hint_free(sentry_feedback_hint_t *hint) { if (!hint) { return; diff --git a/src/sentry_feedback.h b/src/sentry_feedback.h index cfc3ece79..8c9c3479f 100644 --- a/src/sentry_feedback.h +++ b/src/sentry_feedback.h @@ -5,10 +5,15 @@ /** * A sentry Feedback Hint used to pass additional data along with a feedback - * when it’s being captured. + * 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 From fccb567f956462bb846cf047c98d7786e5f9b0bc Mon Sep 17 00:00:00 2001 From: Ivan Tustanivskyi Date: Thu, 16 Oct 2025 15:13:22 +0300 Subject: [PATCH 05/10] Add more tests --- tests/unit/test_feedback.c | 130 ++++++++++++++++++++++++++++++++----- tests/unit/tests.inc | 5 +- 2 files changed, 117 insertions(+), 18 deletions(-) diff --git a/tests/unit/test_feedback.c b/tests/unit/test_feedback.c index d5109e862..e14953e2e 100644 --- a/tests/unit/test_feedback.c +++ b/tests/unit/test_feedback.c @@ -20,21 +20,73 @@ send_envelope_test_feedback(sentry_envelope_t *envelope, void *_data) sentry_envelope_free(envelope); } -SENTRY_TEST(feedback_with_file_attachment) +static void +setup_feedback_test(sentry_feedback_testdata_t *testdata) { - sentry_feedback_testdata_t testdata; - testdata.called = 0; - sentry__stringbuilder_init(&testdata.serialized_envelope); + 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_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"); @@ -75,18 +127,7 @@ SENTRY_TEST(feedback_with_file_attachment) SENTRY_TEST(feedback_with_bytes_attachment) { 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); + setup_feedback_test(&testdata); sentry_uuid_t event_id = sentry_uuid_from_string("4c035723-8638-4c3a-923f-2ab9d08b4018"); @@ -117,3 +158,58 @@ SENTRY_TEST(feedback_with_bytes_attachment) 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 26cbdf71f..572db3bce 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -76,8 +76,11 @@ XX(empty_transport) XX(event_with_id) XX(exception_without_type_or_value_still_valid) XX(formatted_log_messages) -XX(feedback_with_bytes_attachment) +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) From 2e74b67a1d8e9295c8222b152de4b29b1d955478 Mon Sep 17 00:00:00 2001 From: Ivan Tustanivskyi Date: Thu, 16 Oct 2025 15:35:48 +0300 Subject: [PATCH 06/10] Add missing empty line at the end of file --- src/sentry_feedback.c | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/sentry_feedback.c b/src/sentry_feedback.c index da0586230..3ad4e37d1 100644 --- a/src/sentry_feedback.c +++ b/src/sentry_feedback.c @@ -29,8 +29,7 @@ sentry__feedback_hint_free(sentry_feedback_hint_t *hint) } sentry_attachment_t * -sentry_feedback_hint_attach_file( - sentry_feedback_hint_t *hint, const char *path) +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)); @@ -43,9 +42,8 @@ sentry_feedback_hint_attach_file_n( if (!hint) { return NULL; } - return sentry__attachments_add_path( - &hint->attachments, sentry__path_from_str_n(path, path_len), ATTACHMENT, - NULL); + return sentry__attachments_add_path(&hint->attachments, + sentry__path_from_str_n(path, path_len), ATTACHMENT, NULL); } sentry_attachment_t * @@ -90,8 +88,8 @@ sentry_feedback_hint_attach_filew_n( } 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_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( @@ -111,4 +109,4 @@ sentry_feedback_hint_attach_bytesw_n(sentry_feedback_hint_t *hint, buf, buf_len, sentry__path_from_wstr_n(filename, filename_len)), ATTACHMENT, NULL); } -#endif \ No newline at end of file +#endif From 121b078dbf00621d25b9bcd7804c8758a21b86e5 Mon Sep 17 00:00:00 2001 From: Ivan Tustanivskyi Date: Thu, 16 Oct 2025 16:07:20 +0300 Subject: [PATCH 07/10] Update changelog --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b2ba3a97c..6937863a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +**Features**: + +- Add attachment support to user feedback ([#1414](https://github.com/getsentry/sentry-native/pull/1414)) + ## 0.11.3 **Features**: From eeefd2d7b13fab481cf904f83bd8b344d82f483d Mon Sep 17 00:00:00 2001 From: Ivan Tustanivskyi Date: Thu, 16 Oct 2025 16:50:59 +0300 Subject: [PATCH 08/10] Fix double decref --- src/sentry_core.c | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/sentry_core.c b/src/sentry_core.c index fdab6f9e4..7f39708ca 100644 --- a/src/sentry_core.c +++ b/src/sentry_core.c @@ -1504,8 +1504,6 @@ sentry_capture_feedback_with_hint( envelope = prepare_user_feedback(user_feedback, hint); if (envelope) { sentry__capture_envelope(options->transport, envelope); - } else { - sentry_value_decref(user_feedback); } } From 4c392e7c30cd38b4c9b353934dee66a34115bffc Mon Sep 17 00:00:00 2001 From: Ivan Tustanivskyi Date: Thu, 16 Oct 2025 16:51:05 +0300 Subject: [PATCH 09/10] Clean up comment --- include/sentry.h | 4 ---- 1 file changed, 4 deletions(-) diff --git a/include/sentry.h b/include/sentry.h index 9bb2d2832..5e766c621 100644 --- a/include/sentry.h +++ b/include/sentry.h @@ -2725,10 +2725,6 @@ typedef struct sentry_feedback_hint_s sentry_feedback_hint_t; /** * Creates a new feedback hint. - * - * The hint can be used to attach files or binary data to feedback submissions. - * Must be freed with sentry_feedback_hint_free() if not passed to - * sentry_capture_feedback_with_hint(). */ SENTRY_API sentry_feedback_hint_t *sentry_feedback_hint_new(void); From 9e98e1791c209e4956b11648e6c456d4ccaab271 Mon Sep 17 00:00:00 2001 From: Ivan Tustanivskyi Date: Wed, 29 Oct 2025 14:03:23 +0200 Subject: [PATCH 10/10] Fix tests --- tests/test_integration_http.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_integration_http.py b/tests/test_integration_http.py index b1f89ca8f..095ba63d6 100644 --- a/tests/test_integration_http.py +++ b/tests/test_integration_http.py @@ -188,7 +188,6 @@ def test_user_feedback_with_attachments_http(cmake, httpserver): tmp_path, "sentry_example", ["log", "capture-user-feedback-with-attachment"], - check=True, env=env, )