diff --git a/CHANGELOG.md b/CHANGELOG.md index f322c46d9..58bc8d069 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +**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 bf5a7c15e..8528a34f9 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)