From 9c7303c67cba499417eb7a6efefca03953db03a5 Mon Sep 17 00:00:00 2001 From: Ivan Tustanivskyi Date: Mon, 3 Mar 2025 16:25:26 +0200 Subject: [PATCH 1/9] Add before_breadcrumb hook --- examples/example.c | 35 +++++++++++++++++++++++++ include/sentry.h | 22 ++++++++++++++++ src/sentry_core.c | 10 +++++++ src/sentry_options.c | 8 ++++++ src/sentry_options.h | 2 ++ tests/assertions.py | 11 ++++++++ tests/test_integration_http.py | 48 ++++++++++++++++++++++++++++++++++ tests/unit/test_basic.c | 28 ++++++++++++++++++++ tests/unit/tests.inc | 1 + 9 files changed, 165 insertions(+) diff --git a/examples/example.c b/examples/example.c index 6ac07fde6..39f4d98bf 100644 --- a/examples/example.c +++ b/examples/example.c @@ -104,6 +104,31 @@ on_crash_callback( return event; } +static sentry_value_t +before_breadcrumb_callback(sentry_value_t breadcrumb, void *hint, void *closure) +{ + (void)hint; + (void)closure; + + // make our mark on the breadcrumbs + sentry_value_set_by_key( + breadcrumb, "category", sentry_value_new_string("before_breadcrumb")); + + return breadcrumb; +} + +static sentry_value_t +discarding_before_breadcrumb_callback( + sentry_value_t breadcrumb, void *hint, void *closure) +{ + (void)hint; + (void)closure; + + // discard breadcrumb + sentry_value_decref(breadcrumb); + return sentry_value_new_null(); +} + static void print_envelope(sentry_envelope_t *envelope, void *unused_state) { @@ -260,6 +285,16 @@ main(int argc, char **argv) options, discarding_on_crash_callback, NULL); } + if (has_arg(argc, argv, "before-breadcrumb")) { + sentry_options_set_before_send( + options, before_breadcrumb_callback, NULL); + } + + if (has_arg(argc, argv, "discarding-before-breadcrumb")) { + sentry_options_set_before_breadcrumb( + options, discarding_before_breadcrumb_callback, NULL); + } + if (has_arg(argc, argv, "traces-sampler")) { sentry_options_set_traces_sampler(options, traces_sampler_callback); } diff --git a/include/sentry.h b/include/sentry.h index a58d09da9..9d9281ca2 100644 --- a/include/sentry.h +++ b/include/sentry.h @@ -893,6 +893,28 @@ typedef sentry_value_t (*sentry_crash_function_t)( SENTRY_API void sentry_options_set_on_crash( sentry_options_t *opts, sentry_crash_function_t func, void *data); +/** + * Type of the `on_breadcrumb` callback. + * + * This function is called with a breadcrumb before it is added to be sent + * in case of an event. + * + * The callback takes ownership of the `breadcrumb`, and + * should usually return that same breadcrumb. In case the breadcrumb should be + * discarded, the callback needs to call `sentry_value_decref` on the provided + * breadcrumb, and return a `sentry_value_new_null()` instead. + */ +typedef sentry_value_t (*sentry_breadcrumb_function_t)( + sentry_value_t breadcrumb, void *hint, void *closure); + +/** + * Sets the `sentry_options_set_before_breadcrumb` callback. + * + * See the `sentry_breadcrumb_function_t` typedef above for more information. + */ +SENTRY_API void sentry_options_set_before_breadcrumb( + sentry_options_t *opts, sentry_breadcrumb_function_t func, void *data); + /** * Sets the DSN. */ diff --git a/src/sentry_core.c b/src/sentry_core.c index da0aab282..69d72a735 100644 --- a/src/sentry_core.c +++ b/src/sentry_core.c @@ -662,6 +662,16 @@ sentry_add_breadcrumb(sentry_value_t breadcrumb) { size_t max_breadcrumbs = SENTRY_BREADCRUMBS_MAX; SENTRY_WITH_OPTIONS (options) { + if (options->on_breadcrumb_func) { + SENTRY_DEBUG("invoking `before_breadcrumb` hook"); + breadcrumb = options->on_breadcrumb_func( + breadcrumb, NULL, options->on_breadcrumb_data); + if (sentry_value_is_null(breadcrumb)) { + SENTRY_DEBUG( + "breadcrumb was discarded by the `before_breadcrumb` hook"); + return; + } + } if (options->backend && options->backend->add_breadcrumb_func) { // the hook will *not* take ownership options->backend->add_breadcrumb_func( diff --git a/src/sentry_options.c b/src/sentry_options.c index b4fdea8a6..a0d6d1cb1 100644 --- a/src/sentry_options.c +++ b/src/sentry_options.c @@ -140,6 +140,14 @@ sentry_options_set_on_crash( opts->on_crash_data = data; } +void +sentry_options_set_before_breadcrumb( + sentry_options_t *opts, sentry_breadcrumb_function_t func, void *data) +{ + opts->on_breadcrumb_func = func; + opts->on_breadcrumb_data = data; +} + void sentry_options_set_dsn_n( sentry_options_t *opts, const char *raw_dsn, size_t raw_dsn_len) diff --git a/src/sentry_options.h b/src/sentry_options.h index 521e9e5e1..e8cc32b9a 100644 --- a/src/sentry_options.h +++ b/src/sentry_options.h @@ -58,6 +58,8 @@ typedef struct sentry_options_s { void *before_send_data; sentry_crash_function_t on_crash_func; void *on_crash_data; + sentry_breadcrumb_function_t on_breadcrumb_func; + void *on_breadcrumb_data; /* Experimentally exposed */ double traces_sample_rate; diff --git a/tests/assertions.py b/tests/assertions.py index b306b5b31..47462af3e 100644 --- a/tests/assertions.py +++ b/tests/assertions.py @@ -267,6 +267,17 @@ def assert_no_before_send(envelope): event = envelope.get_event() assert ("adapted_by", "before_send") not in event.items() +def assert_before_breadcrumb(envelope): + event = envelope.get_event() + breadcrumbs = event["breadcrumbs"] + assert all( + breadcrumb["category"] == "before_breadcrumb" + for breadcrumb in breadcrumbs + ) + +def assert_discarding_before_breadcrumb(envelope): + event = envelope.get_event() + assert event["breadcrumbs"] is None @dataclass(frozen=True) class CrashpadAttachments: diff --git a/tests/test_integration_http.py b/tests/test_integration_http.py index 5b3281705..83799e9e0 100644 --- a/tests/test_integration_http.py +++ b/tests/test_integration_http.py @@ -179,6 +179,54 @@ def test_user_feedback_http(cmake, httpserver): assert_user_feedback(envelope) +def test_before_breadcrumb_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", "before-breadcrumb", "capture-event"], + check=True, + env=env, + ) + + assert len(httpserver.log) == 1 + output = httpserver.log[0][0].get_data() + envelope = Envelope.deserialize(output) + + assert_before_breadcrumb(envelope) + + +def test_discarding_before_breadcrumb_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", "discarding-before-breadcrumb", "capture-event"], + check=True, + env=env, + ) + + assert len(httpserver.log) == 1 + output = httpserver.log[0][0].get_data() + envelope = Envelope.deserialize(output) + + assert_discarding_before_breadcrumb(envelope) + + def test_exception_and_session_http(cmake, httpserver): tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "none"}) diff --git a/tests/unit/test_basic.c b/tests/unit/test_basic.c index 4abec7d51..ffe8af770 100644 --- a/tests/unit/test_basic.c +++ b/tests/unit/test_basic.c @@ -141,6 +141,34 @@ SENTRY_TEST(discarding_before_send) TEST_CHECK_INT_EQUAL(called_beforesend, 1); } +static sentry_value_t +before_breadcrumb(sentry_value_t breadcrumb, void *UNUSED(hint), void *data) +{ + uint64_t *called = data; + *called += 1; + + return breadcrumb; +} + +SENTRY_TEST(calling_before_breadcrumb) +{ + uint64_t called_before_breadcrumb = 0; + + SENTRY_TEST_OPTIONS_NEW(options); + sentry_options_set_dsn(options, "https://foo@sentry.invalid/42"); + sentry_options_set_before_breadcrumb( + options, before_breadcrumb, &called_before_breadcrumb); + sentry_init(options); + + sentry_add_breadcrumb(sentry_value_new_breadcrumb("foo", "bar")); + sentry_add_breadcrumb(sentry_value_new_breadcrumb("error", "app failed")); + sentry_add_breadcrumb(sentry_value_new_breadcrumb("spam", "hello world")); + + sentry_close(); + + TEST_CHECK_INT_EQUAL(called_before_breadcrumb, 3); +} + SENTRY_TEST(crash_marker) { SENTRY_TEST_OPTIONS_NEW(options); diff --git a/tests/unit/tests.inc b/tests/unit/tests.inc index d5ac3ca7d..0e17a64ed 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -18,6 +18,7 @@ XX(basic_write_envelope_to_file) XX(bgworker_flush) XX(breadcrumb_without_type_or_message_still_valid) XX(build_id_parser) +XX(calling_before_breadcrumb) XX(capture_minidump_basic) XX(capture_minidump_invalid_path) XX(capture_minidump_null_path) From 7be53ec7b43eade41e3f074b3e273f25c424724c Mon Sep 17 00:00:00 2001 From: Ivan Tustanivskyi Date: Mon, 3 Mar 2025 16:45:21 +0200 Subject: [PATCH 2/9] Update readme --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5fb14743c..d96314524 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## Unreleased + +- Add `before_breadcrumb` hook ([#1166](https://github.com/getsentry/sentry-native/pull/1166)) + ## 0.8.1 **Features**: From c084902c79bf1997b94bb9fc738c8284d423d0f3 Mon Sep 17 00:00:00 2001 From: Ivan Tustanivskyi Date: Mon, 3 Mar 2025 16:45:31 +0200 Subject: [PATCH 3/9] Fix linting errors --- tests/assertions.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/assertions.py b/tests/assertions.py index 47462af3e..7677050ed 100644 --- a/tests/assertions.py +++ b/tests/assertions.py @@ -267,14 +267,15 @@ def assert_no_before_send(envelope): event = envelope.get_event() assert ("adapted_by", "before_send") not in event.items() + def assert_before_breadcrumb(envelope): event = envelope.get_event() breadcrumbs = event["breadcrumbs"] assert all( - breadcrumb["category"] == "before_breadcrumb" - for breadcrumb in breadcrumbs + breadcrumb["category"] == "before_breadcrumb" for breadcrumb in breadcrumbs ) + def assert_discarding_before_breadcrumb(envelope): event = envelope.get_event() assert event["breadcrumbs"] is None From 6d91d6134bad4c163e374a738b69da2f27c9bb21 Mon Sep 17 00:00:00 2001 From: Ivan Tustanivskyi Date: Mon, 3 Mar 2025 17:02:45 +0200 Subject: [PATCH 4/9] Add missing imports --- tests/assertions.py | 1 + tests/test_integration_http.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/tests/assertions.py b/tests/assertions.py index 7677050ed..4a71aecec 100644 --- a/tests/assertions.py +++ b/tests/assertions.py @@ -280,6 +280,7 @@ def assert_discarding_before_breadcrumb(envelope): event = envelope.get_event() assert event["breadcrumbs"] is None + @dataclass(frozen=True) class CrashpadAttachments: event: dict diff --git a/tests/test_integration_http.py b/tests/test_integration_http.py index 83799e9e0..6ff7463dc 100644 --- a/tests/test_integration_http.py +++ b/tests/test_integration_http.py @@ -24,6 +24,8 @@ assert_attachment, assert_meta, assert_breadcrumb, + assert_before_breadcrumb, + assert_discarding_before_breadcrumb, assert_stacktrace, assert_event, assert_exception, From 5b8e869877fca11c2fdf2257e703800e1fcd2e0b Mon Sep 17 00:00:00 2001 From: Ivan Tustanivskyi Date: Mon, 3 Mar 2025 17:21:48 +0200 Subject: [PATCH 5/9] Fix tests --- examples/example.c | 2 +- tests/assertions.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/example.c b/examples/example.c index 39f4d98bf..0c5c69678 100644 --- a/examples/example.c +++ b/examples/example.c @@ -286,7 +286,7 @@ main(int argc, char **argv) } if (has_arg(argc, argv, "before-breadcrumb")) { - sentry_options_set_before_send( + sentry_options_set_before_breadcrumb( options, before_breadcrumb_callback, NULL); } diff --git a/tests/assertions.py b/tests/assertions.py index 4a71aecec..869f87fa7 100644 --- a/tests/assertions.py +++ b/tests/assertions.py @@ -272,13 +272,13 @@ def assert_before_breadcrumb(envelope): event = envelope.get_event() breadcrumbs = event["breadcrumbs"] assert all( - breadcrumb["category"] == "before_breadcrumb" for breadcrumb in breadcrumbs + b["category"] == "before_breadcrumb" for b in breadcrumbs ) def assert_discarding_before_breadcrumb(envelope): event = envelope.get_event() - assert event["breadcrumbs"] is None + assert event["breadcrumbs"] is [] @dataclass(frozen=True) From 6b4e2192f75ff91c720b4d20e849a6dc593a111c Mon Sep 17 00:00:00 2001 From: Ivan Tustanivskyi Date: Mon, 3 Mar 2025 18:01:51 +0200 Subject: [PATCH 6/9] Fix lint and test --- tests/assertions.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/assertions.py b/tests/assertions.py index 869f87fa7..b6ac68fed 100644 --- a/tests/assertions.py +++ b/tests/assertions.py @@ -271,14 +271,12 @@ def assert_no_before_send(envelope): def assert_before_breadcrumb(envelope): event = envelope.get_event() breadcrumbs = event["breadcrumbs"] - assert all( - b["category"] == "before_breadcrumb" for b in breadcrumbs - ) + assert all(b["category"] == "before_breadcrumb" for b in breadcrumbs) def assert_discarding_before_breadcrumb(envelope): event = envelope.get_event() - assert event["breadcrumbs"] is [] + assert event["breadcrumbs"] == [] @dataclass(frozen=True) From c67711c8d6b8bbb3a014516ce2be7d39019b08af Mon Sep 17 00:00:00 2001 From: Ivan Tustanivskyi Date: Mon, 3 Mar 2025 19:57:21 +0200 Subject: [PATCH 7/9] Test check --- src/sentry_core.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/sentry_core.c b/src/sentry_core.c index 69d72a735..5abf8d847 100644 --- a/src/sentry_core.c +++ b/src/sentry_core.c @@ -680,6 +680,10 @@ sentry_add_breadcrumb(sentry_value_t breadcrumb) max_breadcrumbs = options->max_breadcrumbs; } + if (sentry_value_is_null(breadcrumb)) { + return; + } + // the `no_flush` will avoid triggering *both* scope-change and // breadcrumb-add events. SENTRY_WITH_SCOPE_MUT_NO_FLUSH (scope) { From 8b02a18a511792f017b3a2b7b233af4c8c8d4c4d Mon Sep 17 00:00:00 2001 From: Ivan Tustanivskyi Date: Mon, 3 Mar 2025 20:32:06 +0200 Subject: [PATCH 8/9] Remove test check --- src/sentry_core.c | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/sentry_core.c b/src/sentry_core.c index 5abf8d847..69d72a735 100644 --- a/src/sentry_core.c +++ b/src/sentry_core.c @@ -680,10 +680,6 @@ sentry_add_breadcrumb(sentry_value_t breadcrumb) max_breadcrumbs = options->max_breadcrumbs; } - if (sentry_value_is_null(breadcrumb)) { - return; - } - // the `no_flush` will avoid triggering *both* scope-change and // breadcrumb-add events. SENTRY_WITH_SCOPE_MUT_NO_FLUSH (scope) { From 1097f040303ea65b035ff29e90ceb4e17be2605e Mon Sep 17 00:00:00 2001 From: Ivan Tustanivskyi Date: Thu, 6 Mar 2025 13:49:22 +0200 Subject: [PATCH 9/9] Free up memory --- src/sentry_core.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/sentry_core.c b/src/sentry_core.c index 69d72a735..721cfab87 100644 --- a/src/sentry_core.c +++ b/src/sentry_core.c @@ -667,6 +667,7 @@ sentry_add_breadcrumb(sentry_value_t breadcrumb) breadcrumb = options->on_breadcrumb_func( breadcrumb, NULL, options->on_breadcrumb_data); if (sentry_value_is_null(breadcrumb)) { + sentry_value_decref(breadcrumb); SENTRY_DEBUG( "breadcrumb was discarded by the `before_breadcrumb` hook"); return;