From 2f7fffab48743d228a8e8728567572af4c533012 Mon Sep 17 00:00:00 2001 From: Michi Hoffmann Date: Fri, 7 Nov 2025 08:54:19 +0100 Subject: [PATCH 01/10] chore: X handle update (#1445) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9858a50b1..883bd7cd0 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ Using the `sentry-native` SDK in a standalone use case is currently an experimen - [SDK Documentation](https://docs.sentry.io/platforms/native/) - [Discord](https://discord.gg/ez5KZN7) server for project discussions -- Follow [@getsentry](https://twitter.com/getsentry) on Twitter for updates +- Follow [@sentry](https://x.com/sentry) on X for updates ## Table of Contents From c5416e6f38021f445b50b415bf42f6bbf14b2fc0 Mon Sep 17 00:00:00 2001 From: JoshuaMoelans <60878493+JoshuaMoelans@users.noreply.github.com> Date: Fri, 14 Nov 2025 09:51:28 +0100 Subject: [PATCH 02/10] feat(logs): custom attributes API (#1435) * initial attributes value implementation * add logs custom attributes + option * CHANGELOG.md * format * format + cleanup * fix memleaks * update example.c log callback to use attribute creator * add custom-attributes X format string POC * infer attribute type from sentry_value_t value * fix attribute memleak * logs take ownership of custom attributes * fix unused variable * don't let default attributes overwrite custom ones * cleanup * decref duplicate attributes * cleanup TODOs * fix for 32-bit windows test --- CHANGELOG.md | 4 ++ examples/example.c | 49 ++++++++++++--- include/sentry.h | 39 +++++++++++- src/sentry_logs.c | 88 +++++++++++++++++---------- src/sentry_options.c | 13 ++++ src/sentry_options.h | 3 + src/sentry_value.c | 45 ++++++++++++++ tests/test_integration_http.py | 93 +++++++++++++++++++++++++++++ tests/unit/test_logs.c | 56 ++++++++++++++++++ tests/unit/test_value.c | 105 +++++++++++++++++++++++++++++++++ tests/unit/tests.inc | 2 + 11 files changed, 457 insertions(+), 40 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 356b9ede8..f070a71ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## 0.12.1 +**Features**: + +- Add custom attributes API for logs. When `logs_with_attributes` is set to `true`, treats the first `varg` passed into `sentry_logs_X(message,...)` as a `sentry_value_t` object of attributes. ([#1435](https://github.com/getsentry/sentry-native/pull/1435)) + **Fixes**: - PS5/Switch compilation regression (`sentry__process_spawn` signature change) ([#1436](https://github.com/getsentry/sentry-native/pull/1436)) diff --git a/examples/example.c b/examples/example.c index 7b97b21c4..d6902fb41 100644 --- a/examples/example.c +++ b/examples/example.c @@ -163,11 +163,8 @@ static sentry_value_t before_send_log_callback(sentry_value_t log, void *user_data) { (void)user_data; - sentry_value_t attribute = sentry_value_new_object(); - sentry_value_set_by_key( - attribute, "value", sentry_value_new_string("little")); - sentry_value_set_by_key( - attribute, "type", sentry_value_new_string("string")); + sentry_value_t attribute + = sentry_value_new_attribute(sentry_value_new_string("little"), NULL); sentry_value_set_by_key(sentry_value_get_by_key(log, "attributes"), "coffeepot.size", attribute); return log; @@ -503,11 +500,47 @@ main(int argc, char **argv) options, "./sentry_crash_reporter"); #endif } + if (has_arg(argc, argv, "log-attributes")) { + sentry_options_set_logs_with_attributes(options, true); + } if (0 != sentry_init(options)) { return EXIT_FAILURE; } + sentry_get_crashed_last_run(); + + if (has_arg(argc, argv, "log-attributes")) { + sentry_value_t attributes = sentry_value_new_object(); + sentry_value_t attr = sentry_value_new_attribute( + sentry_value_new_string("my_attribute"), NULL); + sentry_value_t attr_2 = sentry_value_new_attribute( + sentry_value_new_int64(INT64_MAX), "fermions"); + sentry_value_t attr_3 = sentry_value_new_attribute( + sentry_value_new_int64(INT64_MIN), "bosons"); + sentry_value_set_by_key(attributes, "my.custom.attribute", attr); + sentry_value_set_by_key(attributes, "number.first", attr_2); + sentry_value_set_by_key(attributes, "number.second", attr_3); + // testing multiple attributes + sentry_log_debug( + "logging with %d custom attributes", attributes, (int64_t)3); + // testing no attributes + sentry_log_debug("logging with %s custom attributes", + sentry_value_new_object(), "no"); + // testing overwriting default attributes + sentry_value_t param_attributes = sentry_value_new_object(); + sentry_value_t param_attr = sentry_value_new_attribute( + sentry_value_new_string("parameter"), NULL); + sentry_value_t param_attr_2 = sentry_value_new_attribute( + sentry_value_new_string("custom-sdk-name"), NULL); + sentry_value_set_by_key( + param_attributes, "sentry.message.parameter.0", param_attr); + sentry_value_set_by_key( + param_attributes, "sentry.sdk.name", param_attr_2); + sentry_log_fatal( + "logging with a custom parameter attribute", param_attributes); + } + if (has_arg(argc, argv, "attachment")) { sentry_attachment_t *bytes = sentry_attach_bytes("\xc0\xff\xee", 3, "bytes.bin"); @@ -571,10 +604,8 @@ main(int argc, char **argv) context, "name", sentry_value_new_string("testing-runtime")); sentry_set_context("runtime", context); - sentry_value_t user = sentry_value_new_object(); - sentry_value_set_by_key(user, "id", sentry_value_new_string("42")); - sentry_value_set_by_key( - user, "username", sentry_value_new_string("some_name")); + sentry_value_t user + = sentry_value_new_user("42", "some_name", NULL, NULL); sentry_set_user(user); sentry_value_t default_crumb diff --git a/include/sentry.h b/include/sentry.h index 7e69e0105..77dc2df39 100644 --- a/include/sentry.h +++ b/include/sentry.h @@ -324,6 +324,20 @@ SENTRY_API sentry_value_t sentry_value_new_user_n(const char *id, size_t id_len, const char *username, size_t username_len, const char *email, size_t email_len, const char *ip_address, size_t ip_address_len); +/** + * Creates a new attribute object. + * value` is required, `unit` is optional. + * + *'value' must be a bool, int, double or string (not null, list, object) + * + * Moves ownership of `value` into the object. The caller does not + * have to call `sentry_value_decref` on it. + */ +SENTRY_API sentry_value_t sentry_value_new_attribute( + sentry_value_t value, const char *unit); +SENTRY_API sentry_value_t sentry_value_new_attribute_n( + sentry_value_t value, const char *unit, size_t unit_len); + /** * Returns the type of the value passed. */ @@ -1992,13 +2006,27 @@ SENTRY_EXPERIMENTAL_API int sentry_options_get_propagate_traceparent( /** * Enables or disables the structured logging feature. - * When disabled, all calls to sentry_logger_X() are no-ops. + * When disabled, all calls to `sentry_log_X()` are no-ops. */ SENTRY_EXPERIMENTAL_API void sentry_options_set_enable_logs( sentry_options_t *opts, int enable_logs); SENTRY_EXPERIMENTAL_API int sentry_options_get_enable_logs( const sentry_options_t *opts); +/** + * Enables or disables custom attributes parsing for structured logging. + * + * When enabled, all `sentry_log_X()` functions expect a `sentry_value_t` object + * as the first variadic argument for custom log attributes. Remaining + * arguments are used for format string substitution. + * + * Disabled by default. + */ +SENTRY_EXPERIMENTAL_API void sentry_options_set_logs_with_attributes( + sentry_options_t *opts, int logs_with_attributes); +SENTRY_EXPERIMENTAL_API int sentry_options_get_logs_with_attributes( + const sentry_options_t *opts); + /** * The potential returns of calling any of the sentry_log_X functions * - Success means a log was enqueued @@ -2036,6 +2064,15 @@ typedef enum { * * Flags, width, and precision specifiers are parsed but currently ignored for * parameter extraction purposes. + * + * When the option `logs_with_attributes` is enabled, the first varg is parsed + * as a `sentry_value_t` object containing the initial attributes for the log. + * You can pass `sentry_value_new_null()` to logs which don't need attributes. + * + * Ownership of the attributes is transferred to the log function. + * + * To re-use the same attributes, call `sentry_value_incref` on it + * before passing the attributes to the log function. */ SENTRY_EXPERIMENTAL_API log_return_value_t sentry_log_trace( const char *message, ...); diff --git a/src/sentry_logs.c b/src/sentry_logs.c index add1d295a..88377ca10 100644 --- a/src/sentry_logs.c +++ b/src/sentry_logs.c @@ -546,15 +546,15 @@ static * This function assumes that `value` is owned, so we have to make sure that the * `value` was created or cloned by the caller or even better inc_refed. * - * Replaces attributes[name] if it already exists. + * No-op if 'name' already exists in the attributes. */ static void add_attribute(sentry_value_t attributes, sentry_value_t value, const char *type, const char *name) { if (!sentry_value_is_null(sentry_value_get_by_key(attributes, name))) { - // already exists, so we remove and create a new one - sentry_value_remove_by_key(attributes, name); + sentry_value_decref(value); + return; } sentry_value_t param_obj = sentry_value_new_object(); sentry_value_set_by_key(param_obj, "value", value); @@ -648,10 +648,6 @@ add_scope_and_options_data(sentry_value_t log, sentry_value_t attributes) } } - // fallback in case options doesn't set it - add_attribute(attributes, sentry_value_new_string(SENTRY_SDK_NAME), - "string", "sentry.sdk.name"); - SENTRY_WITH_OPTIONS (options) { if (options->environment) { add_attribute(attributes, @@ -677,25 +673,65 @@ construct_log(sentry_level_t level, const char *message, va_list args) sentry_value_t log = sentry_value_new_object(); sentry_value_t attributes = sentry_value_new_object(); - va_list args_copy_1, args_copy_2, args_copy_3; - va_copy(args_copy_1, args); - va_copy(args_copy_2, args); - va_copy(args_copy_3, args); - int len = vsnprintf(NULL, 0, message, args_copy_1) + 1; - va_end(args_copy_1); - size_t size = (size_t)len; - char *fmt_message = sentry_malloc(size); - if (!fmt_message) { + SENTRY_WITH_OPTIONS (options) { + // Extract custom attributes if the option is enabled + if (sentry_options_get_logs_with_attributes(options)) { + va_list args_copy; + va_copy(args_copy, args); + sentry_value_t custom_attributes + = va_arg(args_copy, sentry_value_t); + va_end(args_copy); + if (sentry_value_get_type(custom_attributes) + == SENTRY_VALUE_TYPE_OBJECT) { + sentry_value_decref(attributes); + attributes = sentry__value_clone(custom_attributes); + } else { + SENTRY_DEBUG("Discarded custom attributes on log: non-object " + "sentry_value_t passed in"); + } + sentry_value_decref(custom_attributes); + } + + // Format the message with remaining args (or all args if not using + // custom attributes) + va_list args_copy_1, args_copy_2, args_copy_3; + va_copy(args_copy_1, args); + va_copy(args_copy_2, args); + va_copy(args_copy_3, args); + + // Skip the first argument (attributes) if using custom attributes + if (sentry_options_get_logs_with_attributes(options)) { + va_arg(args_copy_1, sentry_value_t); + va_arg(args_copy_2, sentry_value_t); + va_arg(args_copy_3, sentry_value_t); + } + + int len = vsnprintf(NULL, 0, message, args_copy_1) + 1; + va_end(args_copy_1); + size_t size = (size_t)len; + char *fmt_message = sentry_malloc(size); + if (!fmt_message) { + va_end(args_copy_2); + va_end(args_copy_3); + return sentry_value_new_null(); + } + + vsnprintf(fmt_message, size, message, args_copy_2); va_end(args_copy_2); + + sentry_value_set_by_key( + log, "body", sentry_value_new_string(fmt_message)); + sentry_free(fmt_message); + + // Parse variadic arguments and add them to attributes + if (populate_message_parameters(attributes, message, args_copy_3)) { + // only add message template if we have parameters + add_attribute(attributes, sentry_value_new_string(message), + "string", "sentry.message.template"); + } va_end(args_copy_3); - return sentry_value_new_null(); } - vsnprintf(fmt_message, size, message, args_copy_2); - va_end(args_copy_2); - - sentry_value_set_by_key(log, "body", sentry_value_new_string(fmt_message)); - sentry_free(fmt_message); sentry_value_set_by_key( log, "level", sentry_value_new_string(level_as_string(level))); @@ -708,14 +744,6 @@ construct_log(sentry_level_t level, const char *message, va_list args) // to the log add_scope_and_options_data(log, attributes); - // Parse variadic arguments and add them to attributes - if (populate_message_parameters(attributes, message, args_copy_3)) { - // only add message template if we have parameters - add_attribute(attributes, sentry_value_new_string(message), "string", - "sentry.message.template"); - } - va_end(args_copy_3); - sentry_value_set_by_key(log, "attributes", attributes); return log; diff --git a/src/sentry_options.c b/src/sentry_options.c index 5cc69c4df..b9b6ea4c6 100644 --- a/src/sentry_options.c +++ b/src/sentry_options.c @@ -744,6 +744,19 @@ sentry_options_get_enable_logs(const sentry_options_t *opts) return opts->enable_logs; } +void +sentry_options_set_logs_with_attributes( + sentry_options_t *opts, int logs_with_attributes) +{ + opts->logs_with_attributes = !!logs_with_attributes; +} + +int +sentry_options_get_logs_with_attributes(const sentry_options_t *opts) +{ + return opts->logs_with_attributes; +} + #ifdef SENTRY_PLATFORM_LINUX sentry_handler_strategy_t diff --git a/src/sentry_options.h b/src/sentry_options.h index bbcc8c93e..280703d40 100644 --- a/src/sentry_options.h +++ b/src/sentry_options.h @@ -65,6 +65,9 @@ struct sentry_options_s { void *traces_sampler_data; size_t max_spans; bool enable_logs; + // takes the first varg as a `sentry_value_t` object containing attributes + // if no custom attributes are to be passed, use `sentry_value_new_object()` + bool logs_with_attributes; /* everything from here on down are options which are stored here but not exposed through the options API */ diff --git a/src/sentry_value.c b/src/sentry_value.c index af4c8709b..fb217e699 100644 --- a/src/sentry_value.c +++ b/src/sentry_value.c @@ -509,6 +509,51 @@ sentry_value_new_user(const char *id, const char *username, const char *email, ip_address, ip_address ? strlen(ip_address) : 0); } +sentry_value_t +sentry_value_new_attribute_n( + sentry_value_t value, const char *unit, size_t unit_len) +{ + char *type; + switch (sentry_value_get_type(value)) { + case SENTRY_VALUE_TYPE_BOOL: + type = "boolean"; + break; + case SENTRY_VALUE_TYPE_INT32: + case SENTRY_VALUE_TYPE_INT64: + case SENTRY_VALUE_TYPE_UINT64: + type = "integer"; + break; + case SENTRY_VALUE_TYPE_DOUBLE: + type = "double"; + break; + case SENTRY_VALUE_TYPE_STRING: + type = "string"; + break; + case SENTRY_VALUE_TYPE_NULL: + case SENTRY_VALUE_TYPE_LIST: + case SENTRY_VALUE_TYPE_OBJECT: + default: + sentry_value_decref(value); + return sentry_value_new_null(); + } + sentry_value_t attribute = sentry_value_new_object(); + + sentry_value_set_by_key( + attribute, "type", sentry_value_new_string_n(type, strlen(type))); + sentry_value_set_by_key(attribute, "value", value); + if (unit && unit_len) { + sentry_value_set_by_key( + attribute, "unit", sentry_value_new_string_n(unit, unit_len)); + } + return attribute; +} + +sentry_value_t +sentry_value_new_attribute(sentry_value_t value, const char *unit) +{ + return sentry_value_new_attribute_n(value, unit, unit ? strlen(unit) : 0); +} + sentry_value_type_t sentry_value_get_type(sentry_value_t value) { diff --git a/tests/test_integration_http.py b/tests/test_integration_http.py index 62d890856..4599fed33 100644 --- a/tests/test_integration_http.py +++ b/tests/test_integration_http.py @@ -1641,3 +1641,96 @@ def test_breakpad_logs_on_crash(cmake, httpserver): assert logs_envelope is not None assert_logs(logs_envelope, 1) + + +def test_logs_with_custom_attributes(cmake, httpserver): + tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "none"}) + + httpserver.expect_oneshot_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", "enable-logs", "log-attributes"], + env=env, + ) + + assert len(httpserver.log) == 1 + req = httpserver.log[0][0] + body = req.get_data() + + envelope = Envelope.deserialize(body) + + # Show what the envelope looks like if the test fails + envelope.print_verbose() + + # Extract the log item + (log_item,) = envelope.items + + assert log_item.headers["type"] == "log" + payload = log_item.payload.json + + # We expect 3 log entries based on the example + assert len(payload["items"]) == 3 + + # Test 1: Log with custom attributes and format string + log_entry_0 = payload["items"][0] + assert log_entry_0["body"] == "logging with 3 custom attributes" + attributes_0 = log_entry_0["attributes"] + + # Check custom attributes exist + assert "my.custom.attribute" in attributes_0 + assert attributes_0["my.custom.attribute"]["value"] == "my_attribute" + assert attributes_0["my.custom.attribute"]["type"] == "string" + + assert "number.first" in attributes_0 + assert attributes_0["number.first"]["value"] == 2**63 - 1 # INT64_MAX + assert attributes_0["number.first"]["type"] == "integer" + assert attributes_0["number.first"]["unit"] == "fermions" + + assert "number.second" in attributes_0 + assert attributes_0["number.second"]["value"] == -(2**63) # INT64_MIN + assert attributes_0["number.second"]["type"] == "integer" + assert attributes_0["number.second"]["unit"] == "bosons" + + # Check that format parameters were parsed + assert "sentry.message.parameter.0" in attributes_0 + assert attributes_0["sentry.message.parameter.0"]["value"] == 3 + assert attributes_0["sentry.message.parameter.0"]["type"] == "integer" + + # Check that default attributes are still present + assert "sentry.sdk.name" in attributes_0 + assert "sentry.sdk.version" in attributes_0 + + # Test 2: Log with empty custom attributes object + log_entry_1 = payload["items"][1] + assert log_entry_1["body"] == "logging with no custom attributes" + attributes_1 = log_entry_1["attributes"] + + # Should still have default attributes + assert "sentry.sdk.name" in attributes_1 + assert "sentry.sdk.version" in attributes_1 + + # Check that format string parameter was parsed + assert "sentry.message.parameter.0" in attributes_1 + assert attributes_1["sentry.message.parameter.0"]["value"] == "no" + assert attributes_1["sentry.message.parameter.0"]["type"] == "string" + + # Test 3: Log with custom attributes that override defaults + log_entry_2 = payload["items"][2] + assert log_entry_2["body"] == "logging with a custom parameter attribute" + attributes_2 = log_entry_2["attributes"] + + # Check custom attribute exists + assert "sentry.message.parameter.0" in attributes_2 + assert attributes_2["sentry.message.parameter.0"]["value"] == "parameter" + assert attributes_2["sentry.message.parameter.0"]["type"] == "string" + + # Check that sentry.sdk.name was overwritten by custom attribute + assert "sentry.sdk.name" in attributes_2 + assert attributes_2["sentry.sdk.name"]["value"] == "custom-sdk-name" + assert attributes_2["sentry.sdk.name"]["type"] == "string" diff --git a/tests/unit/test_logs.c b/tests/unit/test_logs.c index d28ed72e4..3dc546d40 100644 --- a/tests/unit/test_logs.c +++ b/tests/unit/test_logs.c @@ -287,3 +287,59 @@ SENTRY_TEST(logs_param_types) uint64_t g = 0xDEADBEEFDEADBEEF; test_param_conversion_types("%u %d %f %c %s %p %x", a, b, c, d, e, f, g); } + +SENTRY_TEST(logs_custom_attributes_with_format_strings) +{ + transport_validation_data_t validation_data = { 0, false }; + + SENTRY_TEST_OPTIONS_NEW(options); + sentry_options_set_dsn(options, "https://foo@sentry.invalid/42"); + sentry_options_set_enable_logs(options, true); + sentry_options_set_logs_with_attributes(options, true); + + sentry_transport_t *transport + = sentry_transport_new(validate_logs_envelope); + sentry_transport_set_state(transport, &validation_data); + sentry_options_set_transport(options, transport); + + sentry_init(options); + sentry__logs_wait_for_thread_startup(); + + // Test 1: Custom attributes with format string + sentry_value_t attributes1 = sentry_value_new_object(); + sentry_value_t attr1 = sentry_value_new_attribute( + sentry_value_new_string("custom_value"), NULL); + sentry_value_set_by_key(attributes1, "my.custom.attribute", attr1); + TEST_CHECK_INT_EQUAL(sentry_log_info("User %s logged in with code %d", + attributes1, "Alice", 200), + 0); + + // Test 2: Null attributes with format string (should still work) + TEST_CHECK_INT_EQUAL(sentry_log_warn("No custom attrs: %s has %d items", + sentry_value_new_null(), "cart", 5), + 0); + + // Test 3: Custom attributes with no format parameters + sentry_value_t attributes2 = sentry_value_new_object(); + sentry_value_t attr2 + = sentry_value_new_attribute(sentry_value_new_int32(42), NULL); + sentry_value_set_by_key(attributes2, "special.number", attr2); + TEST_CHECK_INT_EQUAL( + sentry_log_error("Simple message with custom attrs", attributes2), 0); + + // Test 4: Custom attributes with multiple format types + sentry_value_t attributes3 = sentry_value_new_object(); + sentry_value_t attr3 + = sentry_value_new_attribute(sentry_value_new_string("tracking"), NULL); + sentry_value_set_by_key(attributes3, "event.type", attr3); + TEST_CHECK_INT_EQUAL( + sentry_log_debug("Processing item %d of %d (%.1f%% complete)", + attributes3, 3, 10, 30.0), + 0); + + sentry_close(); + + // Validate that logs were sent + TEST_CHECK(!validation_data.has_validation_error); + TEST_CHECK_INT_EQUAL(validation_data.called_count, 1); +} diff --git a/tests/unit/test_value.c b/tests/unit/test_value.c index 7c46708ff..09389be67 100644 --- a/tests/unit/test_value.c +++ b/tests/unit/test_value.c @@ -433,6 +433,111 @@ SENTRY_TEST(value_user) sentry_value_decref(user_empty_str); } +SENTRY_TEST(value_attribute) +{ + // Test valid attribute types + sentry_value_t string_attr = sentry_value_new_attribute( + sentry_value_new_string("test_value"), NULL); + TEST_CHECK(sentry_value_get_type(string_attr) == SENTRY_VALUE_TYPE_OBJECT); + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key(string_attr, "type")), + "string"); + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key(string_attr, "value")), + "test_value"); + TEST_CHECK( + sentry_value_is_null(sentry_value_get_by_key(string_attr, "unit"))); + sentry_value_decref(string_attr); + + sentry_value_t integer_attr + = sentry_value_new_attribute(sentry_value_new_int32(42), NULL); + TEST_CHECK(sentry_value_get_type(integer_attr) == SENTRY_VALUE_TYPE_OBJECT); + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key(integer_attr, "type")), + "integer"); + TEST_CHECK( + sentry_value_as_int32(sentry_value_get_by_key(integer_attr, "value")) + == 42); + TEST_CHECK( + sentry_value_is_null(sentry_value_get_by_key(integer_attr, "unit"))); + sentry_value_decref(integer_attr); + + sentry_value_t double_attr + = sentry_value_new_attribute(sentry_value_new_double(3.14), NULL); + TEST_CHECK(sentry_value_get_type(double_attr) == SENTRY_VALUE_TYPE_OBJECT); + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key(double_attr, "type")), + "double"); + TEST_CHECK( + sentry_value_as_double(sentry_value_get_by_key(double_attr, "value")) + == 3.14); + TEST_CHECK( + sentry_value_is_null(sentry_value_get_by_key(double_attr, "unit"))); + sentry_value_decref(double_attr); + + sentry_value_t boolean_attr + = sentry_value_new_attribute(sentry_value_new_bool(true), NULL); + TEST_CHECK(sentry_value_get_type(boolean_attr) == SENTRY_VALUE_TYPE_OBJECT); + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key(boolean_attr, "type")), + "boolean"); + TEST_CHECK( + sentry_value_is_true(sentry_value_get_by_key(boolean_attr, "value"))); + TEST_CHECK( + sentry_value_is_null(sentry_value_get_by_key(boolean_attr, "unit"))); + sentry_value_decref(boolean_attr); + + // Test attribute with unit + sentry_value_t attr_with_unit + = sentry_value_new_attribute(sentry_value_new_int32(100), "percent"); + TEST_CHECK( + sentry_value_get_type(attr_with_unit) == SENTRY_VALUE_TYPE_OBJECT); + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key(attr_with_unit, "type")), + "integer"); + TEST_CHECK( + sentry_value_as_int32(sentry_value_get_by_key(attr_with_unit, "value")) + == 100); + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key(attr_with_unit, "unit")), + "percent"); + sentry_value_decref(attr_with_unit); + + // Test invalid sentry_value_t types + sentry_value_t invalid_attr + = sentry_value_new_attribute(sentry_value_new_list(), NULL); + TEST_CHECK(sentry_value_is_null(invalid_attr)); + sentry_value_decref(invalid_attr); + + // Test NULL type + sentry_value_t null_type_attr + = sentry_value_new_attribute(sentry_value_new_null(), NULL); + TEST_CHECK(sentry_value_is_null(null_type_attr)); + sentry_value_decref(null_type_attr); + + // Test object type + sentry_value_t object_type_attr + = sentry_value_new_attribute(sentry_value_new_object(), NULL); + TEST_CHECK(sentry_value_is_null(object_type_attr)); + sentry_value_decref(object_type_attr); + + // Test _n version with explicit lengths + sentry_value_t string_attr_n = sentry_value_new_attribute_n( + sentry_value_new_string("test_n"), "bytes", 5); + TEST_CHECK( + sentry_value_get_type(string_attr_n) == SENTRY_VALUE_TYPE_OBJECT); + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key(string_attr_n, "type")), + "string"); + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key(string_attr_n, "value")), + "test_n"); + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key(string_attr_n, "unit")), + "bytes"); + sentry_value_decref(string_attr_n); +} + SENTRY_TEST(value_freezing) { sentry_value_t val = sentry_value_new_list(); diff --git a/tests/unit/tests.inc b/tests/unit/tests.inc index cdce6a715..135a05b67 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -87,6 +87,7 @@ XX(iso_time) XX(lazy_attachments) XX(logger_enable_disable_functionality) XX(logger_level) +XX(logs_custom_attributes_with_format_strings) XX(logs_disabled_by_default) XX(logs_param_conversion) XX(logs_param_types) @@ -192,6 +193,7 @@ XX(user_feedback_with_null_args) XX(user_report_is_valid) XX(uuid_api) XX(uuid_v4) +XX(value_attribute) XX(value_bool) XX(value_double) XX(value_freezing) From cbd88e51c05b7d89647fa28c493772e96d397575 Mon Sep 17 00:00:00 2001 From: Ivan Tustanivskyi Date: Mon, 17 Nov 2025 15:08:50 +0200 Subject: [PATCH 03/10] feat: add runtime API to query user consent requirement (#1443) * feat: add runtime API to query user consent requirement * update changelog * Add test for init without explicit consent configuration --- CHANGELOG.md | 6 ++++++ include/sentry.h | 10 ++++++++++ src/sentry_core.c | 10 ++++++++++ tests/unit/test_consent.c | 39 +++++++++++++++++++++++++++++++++++++++ tests/unit/tests.inc | 1 + 5 files changed, 66 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f070a71ef..cb4d2f51c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +**Features**: + +- Add runtime API to query user consent requirement ([#1443](https://github.com/getsentry/sentry-native/pull/1443)) + ## 0.12.1 **Features**: diff --git a/include/sentry.h b/include/sentry.h index 77dc2df39..f9870d5aa 100644 --- a/include/sentry.h +++ b/include/sentry.h @@ -1725,6 +1725,16 @@ SENTRY_API void sentry_user_consent_reset(void); */ SENTRY_API sentry_user_consent_t sentry_user_consent_get(void); +/** + * Checks whether user consent is required. + * + * This returns the value that was configured via + * `sentry_options_set_require_user_consent` during initialization. + * + * Returns 1 if user consent is required, 0 otherwise. + */ +SENTRY_API int sentry_user_consent_is_required(void); + /** * A sentry Scope. * diff --git a/src/sentry_core.c b/src/sentry_core.c index 85b6185ed..a5f8c6004 100644 --- a/src/sentry_core.c +++ b/src/sentry_core.c @@ -456,6 +456,16 @@ sentry_user_consent_get(void) return rv; } +int +sentry_user_consent_is_required(void) +{ + int required = 0; + SENTRY_WITH_OPTIONS (options) { + required = options->require_user_consent; + } + return required; +} + void sentry__capture_envelope( sentry_transport_t *transport, sentry_envelope_t *envelope) diff --git a/tests/unit/test_consent.c b/tests/unit/test_consent.c index 7986fc9d3..2d4b20388 100644 --- a/tests/unit/test_consent.c +++ b/tests/unit/test_consent.c @@ -11,6 +11,16 @@ init_consenting_sentry(void) sentry_init(opts); } +static void +init_not_consenting_sentry(void) +{ + sentry_options_t *opts = sentry_options_new(); + sentry_options_set_database_path(opts, SENTRY_TEST_PATH_PREFIX ".test-db"); + sentry_options_set_dsn(opts, "http://foo@127.0.0.1/42"); + sentry_options_set_require_user_consent(opts, false); + sentry_init(opts); +} + SENTRY_TEST(basic_consent_tracking) { sentry_path_t *path @@ -54,3 +64,32 @@ SENTRY_TEST(basic_consent_tracking) sentry__path_remove_all(path); sentry__path_free(path); } + +SENTRY_TEST(query_consent_requirement) +{ + sentry_path_t *path + = sentry__path_from_str(SENTRY_TEST_PATH_PREFIX ".test-db"); + TEST_ASSERT(!!path); + sentry__path_remove_all(path); + + // Test default behavior when require_user_consent is not set + sentry_options_t *opts = sentry_options_new(); + sentry_options_set_database_path(opts, SENTRY_TEST_PATH_PREFIX ".test-db"); + sentry_options_set_dsn(opts, "http://foo@127.0.0.1/42"); + sentry_init(opts); + TEST_CHECK_INT_EQUAL(sentry_user_consent_is_required(), 0); + sentry_close(); + + // Test when consent is explicitly NOT required + init_not_consenting_sentry(); + TEST_CHECK_INT_EQUAL(sentry_user_consent_is_required(), 0); + sentry_close(); + + // Test when consent IS required + init_consenting_sentry(); + TEST_CHECK_INT_EQUAL(sentry_user_consent_is_required(), 1); + sentry_close(); + + sentry__path_remove_all(path); + sentry__path_free(path); +} diff --git a/tests/unit/tests.inc b/tests/unit/tests.inc index 135a05b67..84f74f6c6 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -8,6 +8,7 @@ XX(attachments_bytes) XX(attachments_extend) XX(background_worker) XX(basic_consent_tracking) +XX(query_consent_requirement) XX(basic_function_transport) XX(basic_function_transport_transaction) XX(basic_function_transport_transaction_ts) From 0a7a531b1c35290379fc1085a3f645c6cc101f4b Mon Sep 17 00:00:00 2001 From: JoshuaMoelans <60878493+JoshuaMoelans@users.noreply.github.com> Date: Mon, 17 Nov 2025 15:33:04 +0100 Subject: [PATCH 04/10] chore: parametrize clang/gcc install for ubuntu CI (#1452) * update ci * update ci * remove CC/CXX env export in Test * check whether CC/CXX was set * add early-fail else on unknown CC value * cleanup - clang19->20 - add clang-11 ubuntu 22.04 runner - remove libstdc++ specific version * separate kcov install * update crashpad --- .github/workflows/ci.yml | 91 +++++++++++++++++++++++++++------------- external/crashpad | 2 +- 2 files changed, 62 insertions(+), 31 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b2af65145..290daadac 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,16 +39,28 @@ jobs: fail-fast: false matrix: include: + # toolchain per runner-image availability: + # ubuntu 22.04 + # GCC: 9-12 + # clang: 11-15 + # kcov: 9-12 + # ubuntu 24.04 + # GCC: 9-14 + # clang: 16-20 + # kcov: 9-14 + # note: + # Since available GCCs on 22.04 are a strict + # subset of the ones available on 24.04, we + # run all GCC tests on the latter version. - name: Linux (GCC 9.5.0, 32-bit) - os: ubuntu-22.04 + os: ubuntu-24.04 CC: gcc-9 CXX: g++-9 TEST_X86: 1 - name: Linux (GCC 9.5.0) - os: ubuntu-22.04 + os: ubuntu-24.04 CC: gcc-9 CXX: g++-9 - # ERROR_ON_WARNINGS: 1 - name: Linux (GCC 12.3.0) os: ubuntu-24.04 CC: gcc-12 @@ -71,37 +83,43 @@ jobs: CC: gcc-14 CXX: g++-14 RUN_ANALYZER: tsan - - name: Linux (clang 19 + asan + llvm-cov) + - name: Linux (clang 11) + os: ubuntu-22.04 + CC: clang-11 + CXX: clang++-11 + - name: Linux (clang 20 + asan + llvm-cov) os: ubuntu-24.04 - CC: clang-19 - CXX: clang++-19 + CC: clang-20 + CXX: clang++-20 ERROR_ON_WARNINGS: 1 RUN_ANALYZER: asan,llvm-cov - - name: Linux Arm64 (clang 19 + asan + llvm-cov) + - name: Linux Arm64 (clang 20 + asan + llvm-cov) os: ubuntu-24.04-arm - CC: clang-19 - CXX: clang++-19 + CC: clang-20 + CXX: clang++-20 ERROR_ON_WARNINGS: 1 RUN_ANALYZER: asan,llvm-cov - - name: Linux (clang 19 + tsan) + - name: Linux (clang 20 + tsan) os: ubuntu-24.04 - CC: clang-19 - CXX: clang++-19 + CC: clang-20 + CXX: clang++-20 ERROR_ON_WARNINGS: 1 RUN_ANALYZER: tsan - - name: Linux Arm64 (clang 19 + tsan) + - name: Linux Arm64 (clang 20 + tsan) os: ubuntu-24.04-arm - CC: clang-19 - CXX: clang++-19 + CC: clang-20 + CXX: clang++-20 ERROR_ON_WARNINGS: 1 RUN_ANALYZER: tsan - - name: Linux (clang 19 + kcov) + - name: Linux (clang 20 + kcov) os: ubuntu-24.04 - CC: clang-19 - CXX: clang++-19 + CC: clang-20 + CXX: clang++-20 ERROR_ON_WARNINGS: 1 RUN_ANALYZER: kcov - name: Linux (GCC 13.3.0 + code-checker + valgrind) + CC: gcc-13 + CXX: g++-13 os: ubuntu-24.04 RUN_ANALYZER: code-checker,valgrind - name: Linux (GCC + musl + libunwind) @@ -174,6 +192,8 @@ jobs: container: ${{ matrix.container }} env: + CC: ${{ matrix.CC }} + CXX: ${{ matrix.CXX }} TEST_X86: ${{ matrix.TEST_X86 }} TEST_MINGW: ${{ matrix.TEST_MINGW }} ERROR_ON_WARNINGS: ${{ matrix.ERROR_ON_WARNINGS }} @@ -196,13 +216,32 @@ jobs: python-version: ${{ !env['SYSTEM_PYTHON'] && '3.12' || '' }} cache: "pip" + - name: Check Linux CC/CXX + if: ${{ runner.os == 'Linux' && !matrix.container }} + run: | + [ -n "$CC" ] && [ -n "$CXX" ] || { echo "Ubuntu runner configurations require toolchain selection via CC and CXX" >&2; exit 1; } + - name: Installing Linux Dependencies - if: ${{ runner.os == 'Linux' && matrix.os != 'ubuntu-22.04' && !env['TEST_X86'] && !matrix.container }} + if: ${{ runner.os == 'Linux' && !env['TEST_X86'] && !matrix.container }} run: | sudo apt update - sudo apt install cmake clang-19 llvm g++-12 valgrind zlib1g-dev libcurl4-openssl-dev + # Install common dependencies + sudo apt install cmake llvm valgrind zlib1g-dev libcurl4-openssl-dev + # For GCC, install both gcc-X and g++-X. For Clang, only install clang-X (includes C++ compiler) + if [[ "$CC" == gcc-* ]]; then + sudo apt install "${CC}" "${CXX}" + elif [[ "$CC" == clang-* ]]; then + sudo apt install "${CC}" + else + echo "Unknown CC: $CC" >&2 + exit 1 + fi + + - name: Installing kcov dependencies + if: ${{ contains(env['RUN_ANALYZER'], 'kcov') }} + run: | # Install kcov from source - sudo apt-get install binutils-dev libssl-dev libelf-dev libstdc++-12-dev libdw-dev libiberty-dev + sudo apt-get install binutils-dev libssl-dev libelf-dev libstdc++-14-dev libdw-dev libiberty-dev git clone https://github.com/SimonKagstrom/kcov.git cd kcov # pin to a known good version with the coveralls git integration and performance bottlenecks fixed @@ -213,18 +252,12 @@ jobs: make sudo make install - - name: Installing Linux GCC 9.4.0 Dependencies - if: ${{ runner.os == 'Linux' && matrix.os == 'ubuntu-22.04' && !env['TEST_X86'] && !matrix.container }} - run: | - sudo apt update - sudo apt install cmake llvm kcov g++ valgrind zlib1g-dev libcurl4-openssl-dev - - name: Installing Linux 32-bit Dependencies if: ${{ runner.os == 'Linux' && env['TEST_X86'] && !matrix.container }} run: | sudo dpkg --add-architecture i386 sudo apt update - sudo apt install cmake gcc-9-multilib g++-9-multilib zlib1g-dev:i386 libssl-dev:i386 libcurl4-openssl-dev:i386 + sudo apt install cmake "${CC}-multilib" "${CXX}-multilib" zlib1g-dev:i386 libssl-dev:i386 libcurl4-openssl-dev:i386 - name: Installing Alpine Linux Dependencies if: ${{ contains(matrix.container, 'alpine') }} @@ -325,8 +358,6 @@ jobs: shell: bash run: | pip install --upgrade --requirement tests/requirements.txt - [ "${{ matrix.CC }}" ] && export CC="${{ matrix.CC }}" - [ "${{ matrix.CXX }}" ] && export CXX="${{ matrix.CXX }}" pytest --capture=no --verbose tests - name: Check NDK diff --git a/external/crashpad b/external/crashpad index d8990d2f6..60dd8995c 160000 --- a/external/crashpad +++ b/external/crashpad @@ -1 +1 @@ -Subproject commit d8990d2f686b8827a21532748c6c42add21c21ea +Subproject commit 60dd8995c6a8539718c878f9b41063604abe737c From 6f406100a5a6dbd4004f141936959faf1e69bd0e Mon Sep 17 00:00:00 2001 From: JoshuaMoelans <60878493+JoshuaMoelans@users.noreply.github.com> Date: Mon, 17 Nov 2025 16:27:49 +0100 Subject: [PATCH 05/10] feat(logs): add `sentry__logs_force_flush` on calling `sentry_flush` (#1434) * add sentry__logs_force_flush on calling sentry_flush * update CHANGELOG.md * add wait for in-progress flushing * cleanup example.c * update CHANGELOG.md --- CHANGELOG.md | 8 +++----- examples/example.c | 2 -- src/sentry_core.c | 3 +++ src/sentry_logs.c | 9 +++++++++ src/sentry_logs.h | 2 ++ tests/unit/test_logs.c | 36 ++++++++++++++++++++++++++++++++++++ tests/unit/tests.inc | 1 + 7 files changed, 54 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cb4d2f51c..7eaa1b765 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,14 +4,12 @@ **Features**: -- Add runtime API to query user consent requirement ([#1443](https://github.com/getsentry/sentry-native/pull/1443)) +- Add custom attributes API for logs. When `logs_with_attributes` is set to `true`, treats the first `varg` passed into `sentry_logs_X(message,...)` as a `sentry_value_t` object of attributes. ([#1435](https://github.com/getsentry/sentry-native/pull/1435)) +- Add runtime API to query user consent requirement. ([#1443](https://github.com/getsentry/sentry-native/pull/1443)) +- Add logs flush on `sentry_flush()`. ([#1434](https://github.com/getsentry/sentry-native/pull/1434)) ## 0.12.1 -**Features**: - -- Add custom attributes API for logs. When `logs_with_attributes` is set to `true`, treats the first `varg` passed into `sentry_logs_X(message,...)` as a `sentry_value_t` object of attributes. ([#1435](https://github.com/getsentry/sentry-native/pull/1435)) - **Fixes**: - PS5/Switch compilation regression (`sentry__process_spawn` signature change) ([#1436](https://github.com/getsentry/sentry-native/pull/1436)) diff --git a/examples/example.c b/examples/example.c index d6902fb41..32fbc5587 100644 --- a/examples/example.c +++ b/examples/example.c @@ -508,8 +508,6 @@ main(int argc, char **argv) return EXIT_FAILURE; } - sentry_get_crashed_last_run(); - if (has_arg(argc, argv, "log-attributes")) { sentry_value_t attributes = sentry_value_new_object(); sentry_value_t attr = sentry_value_new_attribute( diff --git a/src/sentry_core.c b/src/sentry_core.c index a5f8c6004..16b3146f0 100644 --- a/src/sentry_core.c +++ b/src/sentry_core.c @@ -314,6 +314,9 @@ sentry_flush(uint64_t timeout) { int rv = 0; SENTRY_WITH_OPTIONS (options) { + if (options->enable_logs) { + sentry__logs_force_flush(); + } rv = sentry__transport_flush(options->transport, timeout); } return rv; diff --git a/src/sentry_logs.c b/src/sentry_logs.c index 88377ca10..c8cbfff75 100644 --- a/src/sentry_logs.c +++ b/src/sentry_logs.c @@ -932,6 +932,15 @@ sentry__logs_flush_crash_safe(void) SENTRY_DEBUG("crash-safe logs flush complete"); } +void +sentry__logs_force_flush(void) +{ + while (sentry__atomic_fetch(&g_logs_state.flushing)) { + sentry__cpu_relax(); + } + flush_logs_queue(false); +} + #ifdef SENTRY_UNITTEST /** * Wait for the logs batching thread to be ready. diff --git a/src/sentry_logs.h b/src/sentry_logs.h index b25b4fcfe..866dd526c 100644 --- a/src/sentry_logs.h +++ b/src/sentry_logs.h @@ -23,6 +23,8 @@ void sentry__logs_shutdown(uint64_t timeout); */ void sentry__logs_flush_crash_safe(void); +void sentry__logs_force_flush(void); + #ifdef SENTRY_UNITTEST int populate_message_parameters( sentry_value_t attributes, const char *message, va_list args); diff --git a/tests/unit/test_logs.c b/tests/unit/test_logs.c index 3dc546d40..f90f59f27 100644 --- a/tests/unit/test_logs.c +++ b/tests/unit/test_logs.c @@ -288,6 +288,42 @@ SENTRY_TEST(logs_param_types) test_param_conversion_types("%u %d %f %c %s %p %x", a, b, c, d, e, f, g); } +SENTRY_TEST(logs_force_flush) +{ + transport_validation_data_t validation_data = { 0, false }; + + SENTRY_TEST_OPTIONS_NEW(options); + sentry_options_set_dsn(options, "https://foo@sentry.invalid/42"); + sentry_options_set_enable_logs(options, true); + + sentry_transport_t *transport + = sentry_transport_new(validate_logs_envelope); + sentry_transport_set_state(transport, &validation_data); + sentry_options_set_transport(options, transport); + + sentry_init(options); + sentry__logs_wait_for_thread_startup(); + + // These should not crash and should respect the enable_logs option + TEST_CHECK_INT_EQUAL(sentry_log_trace("Trace message"), 0); + sentry_flush(5000); + TEST_CHECK_INT_EQUAL(sentry_log_debug("Debug message"), 0); + sentry_flush(5000); + TEST_CHECK_INT_EQUAL(sentry_log_info("Info message"), 0); + sentry_flush(5000); + TEST_CHECK_INT_EQUAL(sentry_log_warn("Warning message"), 0); + sentry_flush(5000); + TEST_CHECK_INT_EQUAL(sentry_log_error("Error message"), 0); + sentry_flush(5000); + TEST_CHECK_INT_EQUAL(sentry_log_fatal("Fatal message"), 0); + sentry_flush(5000); + sentry_close(); + + // Validate results on main thread (no race condition) + TEST_CHECK(!validation_data.has_validation_error); + TEST_CHECK_INT_EQUAL(validation_data.called_count, 6); +} + SENTRY_TEST(logs_custom_attributes_with_format_strings) { transport_validation_data_t validation_data = { 0, false }; diff --git a/tests/unit/tests.inc b/tests/unit/tests.inc index 84f74f6c6..f8fa3c920 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -90,6 +90,7 @@ XX(logger_enable_disable_functionality) XX(logger_level) XX(logs_custom_attributes_with_format_strings) XX(logs_disabled_by_default) +XX(logs_force_flush) XX(logs_param_conversion) XX(logs_param_types) XX(message_with_null_text_is_valid) From 96b3d348c311e806f870239ff26156f990146c9e Mon Sep 17 00:00:00 2001 From: JoshuaMoelans <60878493+JoshuaMoelans@users.noreply.github.com> Date: Tue, 18 Nov 2025 18:10:32 +0100 Subject: [PATCH 06/10] chore: remove soon-to-be-deprecated macos-13 runner (#1454) https://github.com/actions/runner-images/issues/13046 --- .github/workflows/ci.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 290daadac..37fd37697 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -140,10 +140,6 @@ jobs: os: macos-14 ERROR_ON_WARNINGS: 1 SYSTEM_VERSION_COMPAT: 0 - - name: macOS 13 (xcode llvm) - os: macos-13 - ERROR_ON_WARNINGS: 1 - SYSTEM_VERSION_COMPAT: 0 - name: macOS 14 (xcode llvm + universal) os: macos-14 ERROR_ON_WARNINGS: 1 From 634e8a320de1086ad3b2a7b1598a420e4c5b0cc3 Mon Sep 17 00:00:00 2001 From: JoshuaMoelans <60878493+JoshuaMoelans@users.noreply.github.com> Date: Thu, 27 Nov 2025 13:46:43 +0100 Subject: [PATCH 07/10] chore: update Alpine Linux Dockerfile to install .NET 10.0 (#1458) --- .github/docker/alpine/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/docker/alpine/Dockerfile b/.github/docker/alpine/Dockerfile index c6cc1a9a0..b4cf4b8c2 100644 --- a/.github/docker/alpine/Dockerfile +++ b/.github/docker/alpine/Dockerfile @@ -27,7 +27,7 @@ RUN apk add \ tree \ wget -RUN curl -sSL --retry 5 https://dot.net/v1/dotnet-install.sh | bash -eo pipefail /dev/stdin --channel 8.0 --install-dir /usr/share/dotnet +RUN curl -sSL --retry 5 https://dot.net/v1/dotnet-install.sh | bash -eo pipefail /dev/stdin --channel 10.0 --install-dir /usr/share/dotnet RUN ln -s /usr/share/dotnet/dotnet /usr/local/bin/dotnet # https://github.com/actions/runner-images/blob/main/images/ubuntu/scripts/build/install-python.sh From 079aacc4ecbd54235b3196c1be0f24e8ec6cff0c Mon Sep 17 00:00:00 2001 From: JoshuaMoelans <60878493+JoshuaMoelans@users.noreply.github.com> Date: Mon, 1 Dec 2025 10:00:31 +0100 Subject: [PATCH 08/10] feat: implement global attributes API (#1450) * initial API * implement + test * add global attributes to logs * fix memleak in test * update CHANGELOG.md * fix memleak * initialize attributes * add list-type attribute support * cleanup * add explicit enum cases * move scope-based attributes into non-public API * cleanup * add attribute list test cases * update CHANGELOG.md * rookie mistake (lint) --- CHANGELOG.md | 1 + include/sentry.h | 18 ++- src/sentry_core.c | 33 ++++++ src/sentry_logs.c | 5 +- src/sentry_scope.c | 34 ++++++ src/sentry_scope.h | 16 +++ src/sentry_value.c | 60 +++++++--- tests/unit/test_scope.c | 233 +++++++++++++++++++++++++++++++++++++++ tests/unit/test_uninit.c | 3 + tests/unit/test_value.c | 78 +++++++++++++ tests/unit/tests.inc | 4 +- 11 files changed, 467 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7eaa1b765..3e8caed84 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - Add custom attributes API for logs. When `logs_with_attributes` is set to `true`, treats the first `varg` passed into `sentry_logs_X(message,...)` as a `sentry_value_t` object of attributes. ([#1435](https://github.com/getsentry/sentry-native/pull/1435)) - Add runtime API to query user consent requirement. ([#1443](https://github.com/getsentry/sentry-native/pull/1443)) - Add logs flush on `sentry_flush()`. ([#1434](https://github.com/getsentry/sentry-native/pull/1434)) +- Add global attributes API. These are added to all `sentry_log_X` calls. ([#1450](https://github.com/getsentry/sentry-native/pull/1450)) ## 0.12.1 diff --git a/include/sentry.h b/include/sentry.h index f9870d5aa..ca5874f68 100644 --- a/include/sentry.h +++ b/include/sentry.h @@ -326,11 +326,12 @@ SENTRY_API sentry_value_t sentry_value_new_user_n(const char *id, size_t id_len, /** * Creates a new attribute object. - * value` is required, `unit` is optional. + * value is required, unit is optional. * - *'value' must be a bool, int, double or string (not null, list, object) + * value must be a bool, int, double or string `sentry_value_t` + * OR a list of bool, int, double or string (with all items being the same type) * - * Moves ownership of `value` into the object. The caller does not + * Moves ownership of value into the object. The caller does not * have to call `sentry_value_decref` on it. */ SENTRY_API sentry_value_t sentry_value_new_attribute( @@ -1858,6 +1859,17 @@ SENTRY_API void sentry_scope_set_extra_n(sentry_scope_t *scope, const char *key, SENTRY_API void sentry_remove_extra(const char *key); SENTRY_API void sentry_remove_extra_n(const char *key, size_t key_len); +/** + * Sets attributes created with `sentry_value_new_attribute` to be applied to + * all: + * - logs + */ +SENTRY_API void sentry_set_attribute(const char *key, sentry_value_t attribute); +SENTRY_API void sentry_set_attribute_n( + const char *key, size_t key_len, sentry_value_t attribute); +SENTRY_API void sentry_remove_attribute(const char *key); +SENTRY_API void sentry_remove_attribute_n(const char *key, size_t key_len); + /** * Sets a context object. */ diff --git a/src/sentry_core.c b/src/sentry_core.c index 16b3146f0..465379813 100644 --- a/src/sentry_core.c +++ b/src/sentry_core.c @@ -941,6 +941,39 @@ sentry_remove_extra_n(const char *key, size_t key_len) } } +void +sentry_set_attribute(const char *key, sentry_value_t attribute) +{ + SENTRY_WITH_SCOPE_MUT (scope) { + sentry__scope_set_attribute(scope, key, attribute); + } +} + +void +sentry_set_attribute_n( + const char *key, size_t key_len, sentry_value_t attribute) +{ + SENTRY_WITH_SCOPE_MUT (scope) { + sentry__scope_set_attribute_n(scope, key, key_len, attribute); + } +} + +void +sentry_remove_attribute(const char *key) +{ + SENTRY_WITH_SCOPE_MUT (scope) { + sentry__scope_remove_attribute(scope, key); + } +} + +void +sentry_remove_attribute_n(const char *key, size_t key_len) +{ + SENTRY_WITH_SCOPE_MUT (scope) { + sentry__scope_remove_attribute_n(scope, key, key_len); + } +} + void sentry_set_context(const char *key, sentry_value_t value) { diff --git a/src/sentry_logs.c b/src/sentry_logs.c index c8cbfff75..50dfec020 100644 --- a/src/sentry_logs.c +++ b/src/sentry_logs.c @@ -671,7 +671,10 @@ static sentry_value_t construct_log(sentry_level_t level, const char *message, va_list args) { sentry_value_t log = sentry_value_new_object(); - sentry_value_t attributes = sentry_value_new_object(); + sentry_value_t attributes = sentry_value_new_null(); + SENTRY_WITH_SCOPE (scope) { + attributes = sentry__value_clone(scope->attributes); + } SENTRY_WITH_OPTIONS (options) { // Extract custom attributes if the option is enabled diff --git a/src/sentry_scope.c b/src/sentry_scope.c index 07bfeb4bb..9ae5575a9 100644 --- a/src/sentry_scope.c +++ b/src/sentry_scope.c @@ -74,6 +74,7 @@ init_scope(sentry_scope_t *scope) scope->user = sentry_value_new_null(); scope->tags = sentry_value_new_object(); scope->extra = sentry_value_new_object(); + scope->attributes = sentry_value_new_object(); scope->contexts = sentry_value_new_object(); scope->propagation_context = sentry_value_new_object(); scope->breadcrumbs = sentry__ringbuffer_new(SENTRY_BREADCRUMBS_MAX); @@ -110,6 +111,7 @@ cleanup_scope(sentry_scope_t *scope) sentry_value_decref(scope->user); sentry_value_decref(scope->tags); sentry_value_decref(scope->extra); + sentry_value_decref(scope->attributes); sentry_value_decref(scope->contexts); sentry_value_decref(scope->propagation_context); sentry__ringbuffer_free(scope->breadcrumbs); @@ -588,6 +590,38 @@ sentry_scope_set_extra_n(sentry_scope_t *scope, const char *key, size_t key_len, sentry_value_set_by_key_n(scope->extra, key, key_len, value); } +void +sentry__scope_set_attribute( + sentry_scope_t *scope, const char *key, sentry_value_t attribute) +{ + sentry__scope_set_attribute_n(scope, key, strlen(key), attribute); +} + +void +sentry__scope_set_attribute_n(sentry_scope_t *scope, const char *key, + size_t key_len, sentry_value_t attribute) +{ + if (sentry_value_is_null(sentry_value_get_by_key(attribute, "value")) + || sentry_value_is_null(sentry_value_get_by_key(attribute, "type"))) { + SENTRY_DEBUG("Cannot set attribute with missing 'value' or 'type'"); + return; + } + sentry_value_set_by_key_n(scope->attributes, key, key_len, attribute); +} + +void +sentry__scope_remove_attribute(sentry_scope_t *scope, const char *key) +{ + sentry_value_remove_by_key(scope->attributes, key); +} + +void +sentry__scope_remove_attribute_n( + sentry_scope_t *scope, const char *key, size_t key_len) +{ + sentry_value_remove_by_key_n(scope->attributes, key, key_len); +} + void sentry_scope_set_context( sentry_scope_t *scope, const char *key, sentry_value_t value) diff --git a/src/sentry_scope.h b/src/sentry_scope.h index ba1b0f683..38b5c76cd 100644 --- a/src/sentry_scope.h +++ b/src/sentry_scope.h @@ -17,6 +17,7 @@ struct sentry_scope_s { sentry_value_t user; sentry_value_t tags; sentry_value_t extra; + sentry_value_t attributes; sentry_value_t contexts; sentry_value_t propagation_context; sentry_ringbuffer_t *breadcrumbs; @@ -96,6 +97,21 @@ void sentry__scope_set_fingerprint_va( void sentry__scope_set_fingerprint_nva(sentry_scope_t *scope, const char *fingerprint, size_t fingerprint_len, va_list va); +/** + * Internal scope-based attribute functions. + * For now, these are only used by the non-scope API functions that operate + * on the global scope. + * Once we have attributes for events or scope-based logs/metrics/spans APIs + * these can become part of the public API too. + */ +void sentry__scope_set_attribute( + sentry_scope_t *scope, const char *key, sentry_value_t attribute); +void sentry__scope_set_attribute_n(sentry_scope_t *scope, const char *key, + size_t key_len, sentry_value_t attribute); +void sentry__scope_remove_attribute(sentry_scope_t *scope, const char *key); +void sentry__scope_remove_attribute_n( + sentry_scope_t *scope, const char *key, size_t key_len); + /** * These are convenience macros to automatically lock/unlock the global scope * inside a code block. diff --git a/src/sentry_value.c b/src/sentry_value.c index fb217e699..3d6e3a08d 100644 --- a/src/sentry_value.c +++ b/src/sentry_value.c @@ -509,33 +509,67 @@ sentry_value_new_user(const char *id, const char *username, const char *email, ip_address, ip_address ? strlen(ip_address) : 0); } -sentry_value_t -sentry_value_new_attribute_n( - sentry_value_t value, const char *unit, size_t unit_len) +/** + * Converts a sentry_value_t attribute to its type string representation. + * For lists, checks the first element to determine the array type. + * Returns NULL for unsupported types (NULL, OBJECT). + * https://develop.sentry.dev/sdk/telemetry/spans/span-protocol/#attribute-object-properties + */ +static const char * +attribute_value_type_to_str(sentry_value_t value) { - char *type; switch (sentry_value_get_type(value)) { case SENTRY_VALUE_TYPE_BOOL: - type = "boolean"; - break; + return "boolean"; case SENTRY_VALUE_TYPE_INT32: case SENTRY_VALUE_TYPE_INT64: case SENTRY_VALUE_TYPE_UINT64: - type = "integer"; - break; + return "integer"; case SENTRY_VALUE_TYPE_DOUBLE: - type = "double"; - break; + return "double"; case SENTRY_VALUE_TYPE_STRING: - type = "string"; - break; + return "string"; + case SENTRY_VALUE_TYPE_LIST: { + sentry_value_t first_item = sentry_value_get_by_index(value, 0); + if (sentry_value_is_null(first_item)) { + return NULL; + } + // Determine type based on first element + switch (sentry_value_get_type(first_item)) { + case SENTRY_VALUE_TYPE_BOOL: + return "boolean[]"; + case SENTRY_VALUE_TYPE_INT32: + case SENTRY_VALUE_TYPE_INT64: + case SENTRY_VALUE_TYPE_UINT64: + return "integer[]"; + case SENTRY_VALUE_TYPE_DOUBLE: + return "double[]"; + case SENTRY_VALUE_TYPE_STRING: + return "string[]"; + case SENTRY_VALUE_TYPE_NULL: + case SENTRY_VALUE_TYPE_OBJECT: + case SENTRY_VALUE_TYPE_LIST: + default: + return NULL; + } + } case SENTRY_VALUE_TYPE_NULL: - case SENTRY_VALUE_TYPE_LIST: case SENTRY_VALUE_TYPE_OBJECT: default: + return NULL; + } +} + +sentry_value_t +sentry_value_new_attribute_n( + sentry_value_t value, const char *unit, size_t unit_len) +{ + const char *type = attribute_value_type_to_str(value); + if (!type) { sentry_value_decref(value); return sentry_value_new_null(); } + sentry_value_t attribute = sentry_value_new_object(); sentry_value_set_by_key( diff --git a/tests/unit/test_scope.c b/tests/unit/test_scope.c index aba4c808f..83a6cdfd8 100644 --- a/tests/unit/test_scope.c +++ b/tests/unit/test_scope.c @@ -602,3 +602,236 @@ SENTRY_TEST(scope_breadcrumbs) sentry_close(); } + +SENTRY_TEST(scope_global_attributes) +{ + SENTRY_TEST_OPTIONS_NEW(options); + sentry_init(options); + + // Test setting a valid attribute on the global scope + sentry_value_t valid_attr = sentry_value_new_attribute( + sentry_value_new_string("test_value"), NULL); + sentry_set_attribute("valid_key", valid_attr); + + SENTRY_WITH_SCOPE (scope) { + sentry_value_t attributes = scope->attributes; + sentry_value_t retrieved_attr + = sentry_value_get_by_key(attributes, "valid_key"); + + // Check that the attribute was set + TEST_CHECK(!sentry_value_is_null(retrieved_attr)); + TEST_CHECK_STRING_EQUAL(sentry_value_as_string(sentry_value_get_by_key( + retrieved_attr, "type")), + "string"); + TEST_CHECK_STRING_EQUAL(sentry_value_as_string(sentry_value_get_by_key( + retrieved_attr, "value")), + "test_value"); + } + + // Test that invalid attributes (missing 'value' or 'type') are not set + sentry_value_t invalid_attr_no_value = sentry_value_new_object(); + sentry_value_set_by_key( + invalid_attr_no_value, "type", sentry_value_new_string("string")); + // Missing 'value' field + sentry_set_attribute("invalid_no_value", invalid_attr_no_value); + + SENTRY_WITH_SCOPE (scope) { + sentry_value_t attributes = scope->attributes; + sentry_value_t retrieved_attr + = sentry_value_get_by_key(attributes, "invalid_no_value"); + + // Check that the attribute was NOT set + TEST_CHECK(sentry_value_is_null(retrieved_attr)); + } + sentry_value_decref(invalid_attr_no_value); + + // Test invalid attribute missing 'type' + sentry_value_t invalid_attr_no_type = sentry_value_new_object(); + sentry_value_set_by_key( + invalid_attr_no_type, "value", sentry_value_new_string("some_value")); + // Missing 'type' field + sentry_set_attribute("invalid_no_type", invalid_attr_no_type); + + SENTRY_WITH_SCOPE (scope) { + sentry_value_t attributes = scope->attributes; + sentry_value_t retrieved_attr + = sentry_value_get_by_key(attributes, "invalid_no_type"); + + // Check that the attribute was NOT set + TEST_CHECK(sentry_value_is_null(retrieved_attr)); + } + sentry_value_decref(invalid_attr_no_type); + + // Test removing an attribute + sentry_remove_attribute("valid_key"); + + SENTRY_WITH_SCOPE (scope) { + sentry_value_t attributes = scope->attributes; + sentry_value_t retrieved_attr + = sentry_value_get_by_key(attributes, "valid_key"); + + // Check that the attribute was removed + TEST_CHECK(sentry_value_is_null(retrieved_attr)); + } + + // Test setting attribute with _n variant + sentry_value_t attr_n + = sentry_value_new_attribute(sentry_value_new_int32(42), "percent"); + sentry_set_attribute_n("key_n", 5, attr_n); + + SENTRY_WITH_SCOPE (scope) { + sentry_value_t attributes = scope->attributes; + sentry_value_t retrieved_attr + = sentry_value_get_by_key(attributes, "key_n"); + + // Check that the attribute was set + TEST_CHECK(!sentry_value_is_null(retrieved_attr)); + TEST_CHECK_STRING_EQUAL(sentry_value_as_string(sentry_value_get_by_key( + retrieved_attr, "type")), + "integer"); + TEST_CHECK(sentry_value_as_int32( + sentry_value_get_by_key(retrieved_attr, "value")) + == 42); + TEST_CHECK_STRING_EQUAL(sentry_value_as_string(sentry_value_get_by_key( + retrieved_attr, "unit")), + "percent"); + } + + sentry_close(); +} + +SENTRY_TEST(scope_local_attributes) +{ + SENTRY_TEST_OPTIONS_NEW(options); + sentry_init(options); + + // global: + // {"all":"global","scope":"global","global":"global"} + sentry_set_attribute("all", + sentry_value_new_attribute(sentry_value_new_string("global"), NULL)); + sentry_set_attribute("global", + sentry_value_new_attribute(sentry_value_new_string("global"), NULL)); + sentry_set_attribute("scope", + sentry_value_new_attribute(sentry_value_new_string("global"), NULL)); + + SENTRY_WITH_SCOPE (global_scope) { + sentry_value_t attributes = global_scope->attributes; + + // Verify global attributes are set + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key( + sentry_value_get_by_key(attributes, "all"), "value")), + "global"); + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key( + sentry_value_get_by_key(attributes, "global"), "value")), + "global"); + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key( + sentry_value_get_by_key(attributes, "scope"), "value")), + "global"); + } + + SENTRY_WITH_SCOPE (global_scope) { + // local: + // {"all":"local","scope":"local","local":"local"} + sentry_scope_t *local_scope = sentry_local_scope_new(); + sentry__scope_set_attribute(local_scope, "all", + sentry_value_new_attribute(sentry_value_new_string("local"), NULL)); + sentry__scope_set_attribute(local_scope, "local", + sentry_value_new_attribute(sentry_value_new_string("local"), NULL)); + sentry__scope_set_attribute(local_scope, "scope", + sentry_value_new_attribute(sentry_value_new_string("local"), NULL)); + + sentry_value_t local_attributes = local_scope->attributes; + + // Verify local attributes are set + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key( + sentry_value_get_by_key(local_attributes, "all"), "value")), + "local"); + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key( + sentry_value_get_by_key(local_attributes, "local"), "value")), + "local"); + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key( + sentry_value_get_by_key(local_attributes, "scope"), "value")), + "local"); + + // Verify global scope still has its own attributes + sentry_value_t global_attributes = global_scope->attributes; + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key( + sentry_value_get_by_key(global_attributes, "all"), "value")), + "global"); + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key( + sentry_value_get_by_key(global_attributes, "global"), "value")), + "global"); + + sentry__scope_free(local_scope); + } + + // Test removing attributes from global scope + sentry_remove_attribute("all"); + + SENTRY_WITH_SCOPE (scope) { + sentry_value_t attributes = scope->attributes; + TEST_CHECK( + sentry_value_is_null(sentry_value_get_by_key(attributes, "all"))); + // Other attributes should still exist + TEST_CHECK(!sentry_value_is_null( + sentry_value_get_by_key(attributes, "global"))); + TEST_CHECK(!sentry_value_is_null( + sentry_value_get_by_key(attributes, "scope"))); + } + + // Test _n variants with local scope + SENTRY_WITH_SCOPE (global_scope) { + sentry_scope_t *local_scope = sentry_local_scope_new(); + sentry__scope_set_attribute_n(local_scope, "test_key", 8, + sentry_value_new_attribute(sentry_value_new_int32(100), "percent")); + + sentry_value_t local_attributes = local_scope->attributes; + sentry_value_t attr + = sentry_value_get_by_key(local_attributes, "test_key"); + + TEST_CHECK(!sentry_value_is_null(attr)); + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key(attr, "type")), + "integer"); + TEST_CHECK(sentry_value_as_int32(sentry_value_get_by_key(attr, "value")) + == 100); + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key(attr, "unit")), + "percent"); + + // Remove using _n variant + sentry__scope_remove_attribute_n(local_scope, "test_key", 8); + TEST_CHECK(sentry_value_is_null( + sentry_value_get_by_key(local_attributes, "test_key"))); + + sentry__scope_free(local_scope); + } + + // Test that invalid attributes are not set on local scope + SENTRY_WITH_SCOPE (global_scope) { + sentry_scope_t *local_scope = sentry_local_scope_new(); + + // Try to set invalid attribute (missing 'value') + sentry_value_t invalid_attr = sentry_value_new_object(); + sentry_value_set_by_key( + invalid_attr, "type", sentry_value_new_string("string")); + sentry__scope_set_attribute(local_scope, "invalid", invalid_attr); + + sentry_value_t local_attributes = local_scope->attributes; + TEST_CHECK(sentry_value_is_null( + sentry_value_get_by_key(local_attributes, "invalid"))); + sentry_value_decref(invalid_attr); + + sentry__scope_free(local_scope); + } + + sentry_close(); +} diff --git a/tests/unit/test_uninit.c b/tests/unit/test_uninit.c index 55b7fb688..03ff3209d 100644 --- a/tests/unit/test_uninit.c +++ b/tests/unit/test_uninit.c @@ -19,6 +19,9 @@ SENTRY_TEST(uninitialized) sentry_remove_tag("foo"); sentry_set_extra("foo", sentry_value_new_null()); sentry_remove_extra("foo"); + sentry_set_attribute("foo", + sentry_value_new_attribute(sentry_value_new_string("bar"), NULL)); + sentry_remove_attribute("foo"); sentry_set_context("foo", sentry_value_new_object()); sentry_remove_context("foo"); sentry_set_fingerprint("foo", "bar", NULL); diff --git a/tests/unit/test_value.c b/tests/unit/test_value.c index 09389be67..318a6e9f9 100644 --- a/tests/unit/test_value.c +++ b/tests/unit/test_value.c @@ -536,6 +536,84 @@ SENTRY_TEST(value_attribute) sentry_value_as_string(sentry_value_get_by_key(string_attr_n, "unit")), "bytes"); sentry_value_decref(string_attr_n); + + // Test list attribute types (arrays) + sentry_value_t string_list = sentry_value_new_list(); + sentry_value_append(string_list, sentry_value_new_string("foo")); + sentry_value_append(string_list, sentry_value_new_string("bar")); + sentry_value_t string_list_attr + = sentry_value_new_attribute(string_list, NULL); + TEST_CHECK( + sentry_value_get_type(string_list_attr) == SENTRY_VALUE_TYPE_OBJECT); + TEST_CHECK_STRING_EQUAL(sentry_value_as_string(sentry_value_get_by_key( + string_list_attr, "type")), + "string[]"); + sentry_value_t string_list_value + = sentry_value_get_by_key(string_list_attr, "value"); + TEST_CHECK( + sentry_value_get_type(string_list_value) == SENTRY_VALUE_TYPE_LIST); + TEST_CHECK(sentry_value_get_length(string_list_value) == 2); + sentry_value_decref(string_list_attr); + + sentry_value_t integer_list = sentry_value_new_list(); + sentry_value_append(integer_list, sentry_value_new_int32(1)); + sentry_value_append(integer_list, sentry_value_new_int32(2)); + sentry_value_append(integer_list, sentry_value_new_int32(3)); + sentry_value_t integer_list_attr + = sentry_value_new_attribute(integer_list, NULL); + TEST_CHECK( + sentry_value_get_type(integer_list_attr) == SENTRY_VALUE_TYPE_OBJECT); + TEST_CHECK_STRING_EQUAL(sentry_value_as_string(sentry_value_get_by_key( + integer_list_attr, "type")), + "integer[]"); + sentry_value_decref(integer_list_attr); + + sentry_value_t double_list = sentry_value_new_list(); + sentry_value_append(double_list, sentry_value_new_double(1.1)); + sentry_value_append(double_list, sentry_value_new_double(2.2)); + sentry_value_t double_list_attr + = sentry_value_new_attribute(double_list, NULL); + TEST_CHECK( + sentry_value_get_type(double_list_attr) == SENTRY_VALUE_TYPE_OBJECT); + TEST_CHECK_STRING_EQUAL(sentry_value_as_string(sentry_value_get_by_key( + double_list_attr, "type")), + "double[]"); + sentry_value_decref(double_list_attr); + + sentry_value_t boolean_list = sentry_value_new_list(); + sentry_value_append(boolean_list, sentry_value_new_bool(true)); + sentry_value_append(boolean_list, sentry_value_new_bool(false)); + sentry_value_t boolean_list_attr + = sentry_value_new_attribute(boolean_list, NULL); + TEST_CHECK( + sentry_value_get_type(boolean_list_attr) == SENTRY_VALUE_TYPE_OBJECT); + TEST_CHECK_STRING_EQUAL(sentry_value_as_string(sentry_value_get_by_key( + boolean_list_attr, "type")), + "boolean[]"); + sentry_value_decref(boolean_list_attr); + + // Test empty list (should return null since first element is null) + sentry_value_t empty_list = sentry_value_new_list(); + sentry_value_t empty_list_attr + = sentry_value_new_attribute(empty_list, NULL); + TEST_CHECK(sentry_value_is_null(empty_list_attr)); + sentry_value_decref(empty_list_attr); + + // Test list with nested list (unsupported, should return null) + sentry_value_t nested_list = sentry_value_new_list(); + sentry_value_append(nested_list, sentry_value_new_list()); + sentry_value_t nested_list_attr + = sentry_value_new_attribute(nested_list, NULL); + TEST_CHECK(sentry_value_is_null(nested_list_attr)); + sentry_value_decref(nested_list_attr); + + // Test list with object (unsupported, should return null) + sentry_value_t object_list = sentry_value_new_list(); + sentry_value_append(object_list, sentry_value_new_object()); + sentry_value_t object_list_attr + = sentry_value_new_attribute(object_list, NULL); + TEST_CHECK(sentry_value_is_null(object_list_attr)); + sentry_value_decref(object_list_attr); } SENTRY_TEST(value_freezing) diff --git a/tests/unit/tests.inc b/tests/unit/tests.inc index f8fa3c920..e3619c762 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -8,7 +8,6 @@ XX(attachments_bytes) XX(attachments_extend) XX(background_worker) XX(basic_consent_tracking) -XX(query_consent_requirement) XX(basic_function_transport) XX(basic_function_transport_transaction) XX(basic_function_transport_transaction_ts) @@ -122,6 +121,7 @@ XX(process_invalid) XX(process_spawn) XX(procmaps_parser) XX(propagation_context_init) +XX(query_consent_requirement) XX(rate_limit_parsing) XX(read_envelope_from_file) XX(read_write_envelope_to_file_null) @@ -143,7 +143,9 @@ XX(scope_breadcrumbs) XX(scope_contexts) XX(scope_extra) XX(scope_fingerprint) +XX(scope_global_attributes) XX(scope_level) +XX(scope_local_attributes) XX(scope_tags) XX(scope_user) XX(scoped_txn) From 2ec8ebdbf3ec53164259689a58baa6ca377cb47d Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Mon, 1 Dec 2025 22:46:10 +1300 Subject: [PATCH 09/10] chore: bump dotnet signal tests to .net 10 (#1457) * Bump dotnet signal tests to .net 10 * add dotnet install for ARM64 runner * Apply suggestion from @JoshuaMoelans --------- Co-authored-by: JoshuaMoelans <60878493+JoshuaMoelans@users.noreply.github.com> --- .github/workflows/ci.yml | 9 +++++++++ tests/fixtures/dotnet_signal/test_dotnet.csproj | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 37fd37697..e25393bcc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -260,6 +260,15 @@ jobs: run: | apk update apk add curl-dev libunwind-dev libunwind-static xz-dev + # install manually until available by default (also for future .NET updates) + - name: Install .NET 10 SDK for ARM64 runners + if: ${{ runner.os == 'Linux' && runner.arch == 'ARM64' && !matrix.container }} + run: | + wget https://dot.net/v1/dotnet-install.sh -O dotnet-install.sh + chmod +x dotnet-install.sh + ./dotnet-install.sh --channel 10.0 --install-dir $HOME/.dotnet + echo "$HOME/.dotnet" >> $GITHUB_PATH + echo "DOTNET_ROOT=$HOME/.dotnet" >> $GITHUB_ENV # https://github.com/actions/runner-images/issues/9491 - name: Decrease vm.mmap_rnd_bit to prevent ASLR ASAN issues diff --git a/tests/fixtures/dotnet_signal/test_dotnet.csproj b/tests/fixtures/dotnet_signal/test_dotnet.csproj index 64e34a8d4..238f157e2 100644 --- a/tests/fixtures/dotnet_signal/test_dotnet.csproj +++ b/tests/fixtures/dotnet_signal/test_dotnet.csproj @@ -1,7 +1,7 @@ Exe - net8.0 + net10.0 enable enable From 61a2667bfffee48d7962affac9c5230cda93dce9 Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Mon, 1 Dec 2025 12:00:06 +0000 Subject: [PATCH 10/10] release: 0.12.2 --- CHANGELOG.md | 2 +- include/sentry.h | 2 +- ndk/gradle.properties | 2 +- tests/__init__.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e8caed84..902669d1b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## Unreleased +## 0.12.2 **Features**: diff --git a/include/sentry.h b/include/sentry.h index ca5874f68..3e16ac0af 100644 --- a/include/sentry.h +++ b/include/sentry.h @@ -100,7 +100,7 @@ extern "C" { # endif #endif #ifndef SENTRY_SDK_VERSION -# define SENTRY_SDK_VERSION "0.12.1" +# define SENTRY_SDK_VERSION "0.12.2" #endif #define SENTRY_SDK_USER_AGENT SENTRY_SDK_NAME "/" SENTRY_SDK_VERSION diff --git a/ndk/gradle.properties b/ndk/gradle.properties index c215d5082..2587c205d 100644 --- a/ndk/gradle.properties +++ b/ndk/gradle.properties @@ -7,7 +7,7 @@ org.gradle.parallel=true android.useAndroidX=true # Release information, used for maven publishing -versionName=0.12.1 +versionName=0.12.2 # disable renderscript, it's enabled by default android.defaults.buildfeatures.renderscript=false diff --git a/tests/__init__.py b/tests/__init__.py index 9a25197cc..36fb648e2 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -15,7 +15,7 @@ # https://docs.pytest.org/en/latest/assert.html#assert-details pytest.register_assert_rewrite("tests.assertions") -SENTRY_VERSION = "0.12.1" +SENTRY_VERSION = "0.12.2" def make_dsn(httpserver, auth="uiaeosnrtdy", id=123456, proxy_host=False):