diff --git a/CHANGELOG.md b/CHANGELOG.md index b8780837b..100cebe3b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ **Features**: - Provide Clang-CL support. ([#1161](https://github.com/getsentry/sentry-native/pull/1161), [crashpad#100](https://github.com/getsentry/crashpad/pull/100)) +- Add `before_breadcrumb` hook ([#1166](https://github.com/getsentry/sentry-native/pull/1166)) **Thank you**: diff --git a/examples/example.c b/examples/example.c index 6ac07fde6..0c5c69678 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_breadcrumb( + 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 ef76721a6..befa532eb 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 2f801a57a..17a21aef0 100644 --- a/src/sentry_core.c +++ b/src/sentry_core.c @@ -678,6 +678,17 @@ 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_value_decref(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 86e6097d9..981e42d7b 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 67cffcbac..42791ab6b 100644 --- a/src/sentry_options.h +++ b/src/sentry_options.h @@ -57,6 +57,8 @@ 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..b6ac68fed 100644 --- a/tests/assertions.py +++ b/tests/assertions.py @@ -268,6 +268,17 @@ def assert_no_before_send(envelope): assert ("adapted_by", "before_send") not in event.items() +def assert_before_breadcrumb(envelope): + event = envelope.get_event() + breadcrumbs = event["breadcrumbs"] + assert all(b["category"] == "before_breadcrumb" for b in breadcrumbs) + + +def assert_discarding_before_breadcrumb(envelope): + event = envelope.get_event() + assert event["breadcrumbs"] == [] + + @dataclass(frozen=True) class CrashpadAttachments: event: dict diff --git a/tests/test_integration_http.py b/tests/test_integration_http.py index 5b3281705..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, @@ -179,6 +181,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)