diff --git a/CHANGELOG.md b/CHANGELOG.md index b85251c3a..b5890e239 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ **Features**: - Auto-populate `event.user.id` with a persistent per-installation UUID when no explicit user ID is set. ([#1661](https://github.com/getsentry/sentry-native/pull/1661)) +- Add [strict trace continuation](https://develop.sentry.dev/sdk/foundations/trace-propagation/#strict-trace-continuation) via `sentry_options_set_strict_trace_continuation`. ([#1663](https://github.com/getsentry/sentry-native/pull/1663)) ## 0.14.0 diff --git a/examples/example.c b/examples/example.c index 54cf5cd96..0ddd27c84 100644 --- a/examples/example.c +++ b/examples/example.c @@ -600,6 +600,14 @@ main(int argc, char **argv) sentry_options_set_traces_sample_rate(options, 1.0); } + if (has_arg(argc, argv, "org-id")) { + sentry_options_set_org_id(options, "123456"); + } + + if (has_arg(argc, argv, "strict-trace-continuation")) { + sentry_options_set_strict_trace_continuation(options, 1); + } + if (has_arg(argc, argv, "child-spans")) { sentry_options_set_max_spans(options, 5); } @@ -1162,6 +1170,20 @@ main(int argc, char **argv) sentry_transaction_context_update_from_header( tx_ctx, "sentry-trace", trace_header); } + if (has_arg(argc, argv, "incoming-trace")) { + sentry_transaction_context_update_from_header(tx_ctx, + "sentry-trace", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-bbbbbbbbbbbbbbbb-1"); + } + if (has_arg(argc, argv, "incoming-baggage")) { + sentry_transaction_context_update_from_header(tx_ctx, "baggage", + "sentry-org_id=123456,sentry-environment=upstream," + "sentry-release=upstream-app%401.0"); + } + if (has_arg(argc, argv, "incoming-baggage-mismatch")) { + sentry_transaction_context_update_from_header(tx_ctx, "baggage", + "sentry-org_id=99999,sentry-environment=upstream"); + } sentry_transaction_t *tx = sentry_transaction_start(tx_ctx, custom_sampling_ctx); diff --git a/include/sentry.h b/include/sentry.h index 210381223..0fda1ed5c 100644 --- a/include/sentry.h +++ b/include/sentry.h @@ -2328,6 +2328,41 @@ SENTRY_EXPERIMENTAL_API void sentry_options_set_propagate_traceparent( SENTRY_EXPERIMENTAL_API int sentry_options_get_propagate_traceparent( const sentry_options_t *opts); +/** + * Overrides the organization ID derived from the DSN host + * (e.g. `o123456.ingest.sentry.io` → `123456`). Typically only required for + * self-hosted setups where the DSN host does not encode the organization ID. + * + * The value is passed through as a string; no validation is performed. + */ +SENTRY_EXPERIMENTAL_API void sentry_options_set_org_id( + sentry_options_t *opts, const char *org_id); +SENTRY_EXPERIMENTAL_API void sentry_options_set_org_id_n( + sentry_options_t *opts, const char *org_id, size_t org_id_len); + +/** + * Enables or disables strict trace continuation. + * + * Controls whether to continue an incoming trace when either the trace or the + * SDK has an organization ID (derived from the DSN), but not both. When set + * to true, a new trace is started in that case; when false, the incoming + * trace is continued. If both organization IDs are present and differ, the + * trace is never continued regardless of this setting. + * + * See + * https://develop.sentry.dev/sdk/foundations/trace-propagation/#strict-trace-continuation + * + * This is disabled by default. + */ +SENTRY_EXPERIMENTAL_API void sentry_options_set_strict_trace_continuation( + sentry_options_t *opts, int strict_trace_continuation); + +/** + * Returns whether strict trace continuation is enabled. + */ +SENTRY_EXPERIMENTAL_API int sentry_options_get_strict_trace_continuation( + const sentry_options_t *opts); + /** * Enables or disables the structured logging feature. * When disabled, all calls to `sentry_log_X()` are no-ops. @@ -2904,6 +2939,10 @@ SENTRY_EXPERIMENTAL_API void sentry_transaction_context_remove_sampled( * services. Therefore, the headers of incoming requests should be fed into this * function so that sentry is able to continue a trace that was started by an * upstream service. + * + * Recognized header keys are `sentry-trace` and `baggage` (case-insensitive); + * other keys are ignored. Feed both when available so that strict trace + * continuation can consult the incoming `sentry-org_id`. */ SENTRY_EXPERIMENTAL_API void sentry_transaction_context_update_from_header( sentry_transaction_context_t *tx_ctx, const char *key, const char *value); diff --git a/src/sentry_core.c b/src/sentry_core.c index a556d1c74..6f035074d 100644 --- a/src/sentry_core.c +++ b/src/sentry_core.c @@ -98,39 +98,6 @@ generate_propagation_context(sentry_value_t propagation_context) sentry_value_get_by_key(propagation_context, "trace")); } -static void -set_dynamic_sampling_context( - const sentry_options_t *options, sentry_scope_t *scope) -{ - sentry_value_decref(scope->dynamic_sampling_context); - // add the Dynamic Sampling Context to the `trace` header - sentry_value_t dsc = sentry_value_new_object(); - - if (options->dsn) { - sentry_value_set_by_key(dsc, "public_key", - sentry_value_new_string(options->dsn->public_key)); - sentry_value_set_by_key( - dsc, "org_id", sentry_value_new_string(options->dsn->org_id)); - } - sentry_value_set_by_key(dsc, "sample_rate", - sentry_value_new_double(options->traces_sample_rate)); - if (options->traces_sampler) { - sentry_value_set_by_key( - dsc, "sample_rate", sentry_value_new_double(1.0)); - } - sentry_value_t sample_rand = sentry_value_get_by_key( - sentry_value_get_by_key(scope->propagation_context, "trace"), - "sample_rand"); - sentry_value_set_by_key(dsc, "sample_rand", sample_rand); - sentry_value_incref(sample_rand); - sentry_value_set_by_key( - dsc, "release", sentry_value_new_string(scope->release)); - sentry_value_set_by_key( - dsc, "environment", sentry_value_new_string(scope->environment)); - - scope->dynamic_sampling_context = dsc; -} - #if defined(SENTRY_PLATFORM_NX) || defined(SENTRY_PLATFORM_PS) int sentry__native_init(sentry_options_t *options) @@ -250,7 +217,7 @@ sentry_init(sentry_options_t *options) sentry__ringbuffer_set_max_size( scope->breadcrumbs, options->max_breadcrumbs); - set_dynamic_sampling_context(options, scope); + sentry__scope_update_dsc(scope, options); } if (backend && backend->user_consent_changed_func) { backend->user_consent_changed_func(backend); @@ -1206,15 +1173,24 @@ sentry_set_trace_n(const char *trace_id, size_t trace_id_len, sentry__generate_sample_rand(context); sentry__set_propagation_context("trace", context); + + SENTRY_WITH_OPTIONS (options) { + SENTRY_WITH_SCOPE_MUT (scope) { + sentry__scope_update_dsc(scope, options); + } + } } } void sentry_regenerate_trace(void) { - SENTRY_WITH_SCOPE_MUT (scope) { - generate_propagation_context(scope->propagation_context); - scope->trace_managed = false; + SENTRY_WITH_OPTIONS (options) { + SENTRY_WITH_SCOPE_MUT (scope) { + generate_propagation_context(scope->propagation_context); + scope->trace_managed = false; + sentry__scope_update_dsc(scope, options); + } } } @@ -1286,6 +1262,41 @@ sentry_transaction_start_ts(sentry_transaction_context_t *opaque_tx_ctx, sentry_value_remove_by_key(tx, "timestamp"); sentry__value_merge_objects(tx, tx_ctx); + + sentry_value_t incoming = sentry_value_get_by_key(tx, "incoming_dsc"); + if (!sentry_value_is_null(incoming)) { + SENTRY_WITH_OPTIONS (options) { + SENTRY_WITH_SCOPE_MUT (scope) { + if (sentry__trace_can_continue(incoming, options)) { + // Freeze only when the upstream actually sent DSC values; + // a sentry-trace-only signal leaves incoming empty, in + // which case the SDK builds its own DSC. + if (sentry_value_get_length(incoming) > 0) { + sentry__scope_freeze_dsc(scope, incoming); + } else { + sentry__scope_update_dsc(scope, options); + } + } else { + // Fork: ignore upstream trace, become head of a new trace. + // Regenerate the scope's propagation context so events + // captured outside this transaction also carry the new + // trace_id, and align the tx's trace_id with it. + generate_propagation_context(scope->propagation_context); + sentry_value_t scope_trace_id = sentry_value_get_by_key( + sentry_value_get_by_key( + scope->propagation_context, "trace"), + "trace_id"); + sentry_value_incref(scope_trace_id); + sentry_value_set_by_key(tx, "trace_id", scope_trace_id); + sentry_value_remove_by_key(tx, "parent_span_id"); + sentry_value_remove_by_key(tx, "sampled"); + sentry__scope_update_dsc(scope, options); + } + } + } + } + sentry_value_remove_by_key(tx, "incoming_dsc"); + double sample_rand = 1.0; SENTRY_WITH_SCOPE (scope) { sample_rand = sentry_value_as_double(sentry_value_get_by_key( @@ -1295,7 +1306,7 @@ sentry_transaction_start_ts(sentry_transaction_context_t *opaque_tx_ctx, sentry_sampling_context_t sampling_ctx = { opaque_tx_ctx, custom_sampling_ctx, NULL, sample_rand }; - bool should_sample = sentry__should_send_transaction(tx_ctx, &sampling_ctx); + bool should_sample = sentry__should_send_transaction(tx, &sampling_ctx); sentry_value_set_by_key( tx, "sampled", sentry_value_new_bool(should_sample)); sentry_value_decref(custom_sampling_ctx); diff --git a/src/sentry_options.c b/src/sentry_options.c index 100015cfe..9fd79b4e5 100644 --- a/src/sentry_options.c +++ b/src/sentry_options.c @@ -63,6 +63,7 @@ sentry_options_new(void) opts->enable_logging_when_crashed = true; #endif opts->propagate_traceparent = false; + opts->strict_trace_continuation = false; opts->crashpad_limit_stack_capture_to_sp = false; opts->enable_metrics = true; opts->enable_logs = true; @@ -124,6 +125,7 @@ sentry_options_free(sentry_options_t *opts) sentry_free(opts->dist); sentry_free(opts->proxy); sentry_free(opts->ca_certs); + sentry_free(opts->org_id); sentry_free(opts->transport_thread_name); sentry__path_free(opts->database_path); sentry__path_free(opts->handler_path); @@ -220,6 +222,33 @@ sentry_options_get_dsn(const sentry_options_t *opts) return opts->dsn ? opts->dsn->raw : NULL; } +void +sentry_options_set_org_id_n( + sentry_options_t *opts, const char *org_id, size_t org_id_len) +{ + sentry_free(opts->org_id); + opts->org_id = sentry__string_clone_n(org_id, org_id_len); +} + +void +sentry_options_set_org_id(sentry_options_t *opts, const char *org_id) +{ + sentry_free(opts->org_id); + opts->org_id = sentry__string_clone(org_id); +} + +const char * +sentry__options_get_org_id(const sentry_options_t *opts) +{ + if (opts->org_id && *opts->org_id) { + return opts->org_id; + } + if (opts->dsn && opts->dsn->org_id && *opts->dsn->org_id) { + return opts->dsn->org_id; + } + return NULL; +} + void sentry_options_set_sample_rate(sentry_options_t *opts, double sample_rate) { @@ -937,6 +966,19 @@ sentry_options_get_propagate_traceparent(const sentry_options_t *opts) return opts->propagate_traceparent; } +void +sentry_options_set_strict_trace_continuation( + sentry_options_t *opts, int strict_trace_continuation) +{ + opts->strict_trace_continuation = !!strict_trace_continuation; +} + +int +sentry_options_get_strict_trace_continuation(const sentry_options_t *opts) +{ + return opts->strict_trace_continuation; +} + void sentry_options_set_send_client_reports(sentry_options_t *opts, int val) { diff --git a/src/sentry_options.h b/src/sentry_options.h index 0a063a3aa..4a45027c5 100644 --- a/src/sentry_options.h +++ b/src/sentry_options.h @@ -46,6 +46,7 @@ struct sentry_options_s { bool crashpad_wait_for_upload; bool enable_logging_when_crashed; bool propagate_traceparent; + bool strict_trace_continuation; bool crashpad_limit_stack_capture_to_sp; bool cache_keep; @@ -72,6 +73,7 @@ struct sentry_options_s { double traces_sample_rate; sentry_traces_sampler_function traces_sampler; void *traces_sampler_data; + char *org_id; size_t max_spans; bool enable_logs; // takes the first varg as a `sentry_value_t` object containing attributes @@ -108,4 +110,11 @@ struct sentry_options_s { */ sentry_options_t *sentry__options_incref(sentry_options_t *options); +/** + * Returns the organization ID used for trace propagation: the `org_id` option + * if set and non-empty, otherwise the DSN-derived value if non-empty, + * otherwise NULL. + */ +const char *sentry__options_get_org_id(const sentry_options_t *options); + #endif diff --git a/src/sentry_scope.c b/src/sentry_scope.c index c7020a978..9dfec8bf2 100644 --- a/src/sentry_scope.c +++ b/src/sentry_scope.c @@ -189,6 +189,49 @@ sentry__scope_free(sentry_scope_t *scope) sentry_free(scope); } +void +sentry__scope_freeze_dsc(sentry_scope_t *scope, sentry_value_t incoming) +{ + sentry_value_decref(scope->dynamic_sampling_context); + sentry_value_t dsc = sentry_value_new_object(); + sentry__value_merge_objects(dsc, incoming); + sentry_value_freeze(dsc); + scope->dynamic_sampling_context = dsc; +} + +void +sentry__scope_update_dsc(sentry_scope_t *scope, const sentry_options_t *options) +{ + sentry_value_decref(scope->dynamic_sampling_context); + sentry_value_t dsc = sentry_value_new_object(); + + if (options->dsn) { + sentry_value_set_by_key(dsc, "public_key", + sentry_value_new_string(options->dsn->public_key)); + } + const char *org_id = sentry__options_get_org_id(options); + if (org_id) { + sentry_value_set_by_key(dsc, "org_id", sentry_value_new_string(org_id)); + } + sentry_value_set_by_key(dsc, "sample_rate", + sentry_value_new_double(options->traces_sample_rate)); + if (options->traces_sampler) { + sentry_value_set_by_key( + dsc, "sample_rate", sentry_value_new_double(1.0)); + } + sentry_value_t sample_rand = sentry_value_get_by_key( + sentry_value_get_by_key(scope->propagation_context, "trace"), + "sample_rand"); + sentry_value_set_by_key(dsc, "sample_rand", sample_rand); + sentry_value_incref(sample_rand); + sentry_value_set_by_key( + dsc, "release", sentry_value_new_string(scope->release)); + sentry_value_set_by_key( + dsc, "environment", sentry_value_new_string(scope->environment)); + + scope->dynamic_sampling_context = dsc; +} + #if !defined(SENTRY_PLATFORM_NX) static void sentry__foreach_stacktrace( diff --git a/src/sentry_scope.h b/src/sentry_scope.h index 6deb1d11a..6047fa803 100644 --- a/src/sentry_scope.h +++ b/src/sentry_scope.h @@ -128,6 +128,21 @@ void sentry__scope_remove_attribute_n( for (sentry_scope_t *Scope = sentry__scope_lock(); Scope; \ sentry__scope_unlock(), Scope = NULL) +/** + * Rebuilds the scope's dynamic sampling context (DSC) from the SDK options + * and the current propagation context. The previous DSC is discarded. + */ +void sentry__scope_update_dsc( + sentry_scope_t *scope, const sentry_options_t *options); + +/** + * Replaces the scope's dynamic sampling context (DSC) with a verbatim copy + * of the incoming object. Used when continuing an upstream trace: per the + * trace-propagation spec, the receiving SDK MUST treat the incoming DSC as + * frozen and propagate its values "as is". + */ +void sentry__scope_freeze_dsc(sentry_scope_t *scope, sentry_value_t incoming); + /** * Adds scoped attributes to the telemetry attributes object. */ diff --git a/src/sentry_tracing.c b/src/sentry_tracing.c index 8ef7690bc..80f571d51 100644 --- a/src/sentry_tracing.c +++ b/src/sentry_tracing.c @@ -9,8 +9,54 @@ #include "sentry_string.h" #include "sentry_utils.h" #include "sentry_value.h" +#include +#include #include +static inline bool +isalnum_c(unsigned char c) +{ + return (c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') + || (c >= 'a' && c <= 'z'); +} + +static void +percent_encode_append(sentry_stringbuilder_t *sb, const char *value) +{ + // Encode every byte that isn't an RFC 3986 unreserved character + // (ALPHA / DIGIT / "-" / "." / "_" / "~") as %XX. + static const char hex[] = "0123456789ABCDEF"; + for (const unsigned char *p = (const unsigned char *)value; *p; p++) { + unsigned char c = *p; + if (isalnum_c(c) || c == '-' || c == '.' || c == '_' || c == '~') { + sentry__stringbuilder_append_char(sb, (char)c); + } else { + char esc[3] = { '%', hex[c >> 4], hex[c & 0xF] }; + sentry__stringbuilder_append_buf(sb, esc, 3); + } + } +} + +static void +append_baggage_member(const char *key, sentry_value_t value, void *userdata) +{ + if (!key || strcmp(key, "trace_id") == 0 || sentry_value_is_null(value)) { + return; + } + + char *value_str = sentry__value_stringify(value); + if (!value_str) { + return; + } + + sentry_stringbuilder_t *sb = userdata; + sentry__stringbuilder_append(sb, ",sentry-"); + sentry__stringbuilder_append(sb, key); + sentry__stringbuilder_append_char(sb, '='); + percent_encode_append(sb, value_str); + sentry_free(value_str); +} + static sentry_value_t new_span_n(sentry_value_t parent, sentry_slice_t operation) { @@ -269,6 +315,15 @@ parse_sentry_trace( sentry_value_t trace_id = sentry__value_new_string_owned(s); sentry_value_set_by_key(inner, "trace_id", trace_id); + // Mark that an upstream trace was received. `incoming_dsc` doubles as this + // marker so the strict-continuation check fires even when no `baggage` + // arrives; baggage parsing merges into the same object regardless of + // header order. + if (sentry_value_is_null(sentry_value_get_by_key(inner, "incoming_dsc"))) { + sentry_value_set_by_key( + inner, "incoming_dsc", sentry_value_new_object()); + } + const char *span_id_start = trace_id_end + 1; const char *span_id_end = strchr(span_id_start, '-'); if (!span_id_end) { @@ -296,6 +351,62 @@ parse_sentry_trace( sentry_value_set_by_key(inner, "sampled", sentry_value_new_bool(sampled)); } +static void +parse_baggage( + sentry_transaction_context_t *tx_ctx, const char *value, size_t value_len) +{ + // https://www.w3.org/TR/baggage/ — Sentry-prefixed members are kept and + // percent-decoded; non-sentry members are ignored. + static const char sentry_prefix[] = "sentry-"; + static const size_t sentry_prefix_len = sizeof(sentry_prefix) - 1; + + sentry_value_t inner = tx_ctx->inner; + sentry_value_t incoming = sentry_value_get_by_key(inner, "incoming_dsc"); + if (sentry_value_is_null(incoming)) { + incoming = sentry_value_new_object(); + sentry_value_set_by_key(inner, "incoming_dsc", incoming); + incoming = sentry_value_get_by_key(inner, "incoming_dsc"); + } + + sentry_slice_t remaining = { value, value_len }; + sentry_slice_t key, val; + while (sentry__baggage_iter_next(&remaining, &key, &val)) { + if (key.len <= sentry_prefix_len + || memcmp(key.ptr, sentry_prefix, sentry_prefix_len) != 0) { + continue; + } + const char *sub_key = key.ptr + sentry_prefix_len; + size_t sub_key_len = key.len - sentry_prefix_len; + + char *decoded = sentry__string_clone_n(val.ptr, val.len); + if (!decoded) { + continue; + } + size_t decoded_len = sentry__percent_decode_inplace(decoded, val.len); + decoded[decoded_len] = '\0'; + sentry_value_set_by_key_n(incoming, sub_key, sub_key_len, + sentry__value_new_string_owned(decoded)); + } +} + +bool +sentry__trace_can_continue( + sentry_value_t incoming, const sentry_options_t *options) +{ + const char *sdk_org_id = sentry__options_get_org_id(options); + const char *incoming_org_id + = sentry_value_as_string(sentry_value_get_by_key(incoming, "org_id")); + bool sdk_has = sdk_org_id && *sdk_org_id; + bool inc_has = incoming_org_id && *incoming_org_id; + if (sdk_has && inc_has) { + return strcmp(sdk_org_id, incoming_org_id) == 0; + } + if (sdk_has != inc_has) { + return !options->strict_trace_continuation; + } + return true; +} + void sentry_transaction_context_update_from_header_n( sentry_transaction_context_t *tx_ctx, const char *key, size_t key_len, @@ -308,10 +419,16 @@ sentry_transaction_context_update_from_header_n( // do case-insensitive header key comparison const char sentry_trace[] = "sentry-trace"; const size_t sentry_trace_len = sizeof(sentry_trace) - 1; - bool is_sentry_trace - = compare_header_key(key, key_len, sentry_trace, sentry_trace_len); - if (is_sentry_trace) { + if (compare_header_key(key, key_len, sentry_trace, sentry_trace_len)) { parse_sentry_trace(tx_ctx, value, value_len); + return; + } + + const char baggage[] = "baggage"; + const size_t baggage_len = sizeof(baggage) - 1; + if (compare_header_key(key, key_len, baggage, baggage_len)) { + parse_baggage(tx_ctx, value, value_len); + return; } } @@ -796,8 +913,28 @@ sentry__span_iter_headers(sentry_value_t span, sentry_value_is_true(sampled) ? "1" : "0"); callback("sentry-trace", buf, userdata); - // TODO propagate dsc into outgoing bagage header - // https://develop.sentry.dev/sdk/telemetry/traces/dynamic-sampling-context/#baggage-header + // Outgoing baggage: build from the scope DSC (frozen from upstream when + // the trace was continued, otherwise from the SDK's own options). The + // span's own trace_id is preferred over any DSC trace_id to keep the + // baggage trace_id consistent with the `sentry-trace` header above. + // https://develop.sentry.dev/sdk/telemetry/traces/dynamic-sampling-context/#baggage-header + { + sentry_stringbuilder_t sb; + sentry__stringbuilder_init(&sb); + sentry__stringbuilder_append(&sb, "sentry-trace_id="); + sentry__stringbuilder_append(&sb, sentry_value_as_string(trace_id)); + + SENTRY_WITH_SCOPE (scope) { + sentry__value_foreach_key_value( + scope->dynamic_sampling_context, append_baggage_member, &sb); + } + + char *baggage = sentry__stringbuilder_into_string(&sb); + if (baggage) { + callback("baggage", baggage, userdata); + sentry_free(baggage); + } + } SENTRY_WITH_OPTIONS (options) { if (options->propagate_traceparent) { diff --git a/src/sentry_tracing.h b/src/sentry_tracing.h index f72836f6f..d8910ba7f 100644 --- a/src/sentry_tracing.h +++ b/src/sentry_tracing.h @@ -59,4 +59,14 @@ sentry_span_t *sentry__span_new( */ sentry_value_t sentry__value_get_trace_context(sentry_value_t span); +/** + * Returns whether to continue an incoming trace given the SDK options and the + * incoming dynamic sampling context. + * + * See + * https://develop.sentry.dev/sdk/foundations/trace-propagation/#strict-trace-continuation + */ +bool sentry__trace_can_continue( + sentry_value_t incoming, const sentry_options_t *options); + #endif diff --git a/src/sentry_utils.c b/src/sentry_utils.c index 870b56a12..f0d5e7d85 100644 --- a/src/sentry_utils.c +++ b/src/sentry_utils.c @@ -10,6 +10,7 @@ #include "sentry_random.h" +#include #include #include #include @@ -662,3 +663,58 @@ sentry__generate_sample_rand(sentry_value_t context) sentry_value_set_by_key( context, "sample_rand", sentry_value_new_double(sample_rand)); } + +bool +sentry__baggage_iter_next( + sentry_slice_t *remaining, sentry_slice_t *key, sentry_slice_t *value) +{ + while (remaining->len > 0) { + size_t comma = sentry__slice_find(*remaining, ','); + sentry_slice_t member; + if (comma == (size_t)-1) { + member = *remaining; + *remaining = sentry__slice_advance(*remaining, remaining->len); + } else { + member = (sentry_slice_t) { remaining->ptr, comma }; + *remaining = sentry__slice_advance(*remaining, comma + 1); + } + member = sentry__slice_trim(member); + + size_t eq = sentry__slice_find(member, '='); + if (eq == (size_t)-1) { + continue; + } + sentry_slice_t k + = sentry__slice_trim((sentry_slice_t) { member.ptr, eq }); + if (k.len == 0) { + continue; + } + sentry_slice_t v = { member.ptr + eq + 1, member.len - eq - 1 }; + size_t semi = sentry__slice_find(v, ';'); + if (semi != (size_t)-1) { + v.len = semi; + } + *key = k; + *value = sentry__slice_trim(v); + return true; + } + return false; +} + +size_t +sentry__percent_decode_inplace(char *s, size_t len) +{ + size_t r = 0; + size_t w = 0; + while (r < len) { + if (s[r] == '%' && r + 2 < len && isxdigit((unsigned char)s[r + 1]) + && isxdigit((unsigned char)s[r + 2])) { + char hex[3] = { s[r + 1], s[r + 2], '\0' }; + s[w++] = (char)strtol(hex, NULL, 16); + r += 3; + } else { + s[w++] = s[r++]; + } + } + return w; +} diff --git a/src/sentry_utils.h b/src/sentry_utils.h index b3b791127..74f3dc99a 100644 --- a/src/sentry_utils.h +++ b/src/sentry_utils.h @@ -2,6 +2,7 @@ #define SENTRY_UTILS_H_INCLUDED #include "sentry_boot.h" +#include "sentry_slice.h" #ifdef SENTRY_PLATFORM_DARWIN # include @@ -289,4 +290,25 @@ bool sentry__check_min_version( */ void sentry__generate_sample_rand(sentry_value_t context); +/** + * Yields the next W3C Baggage member from `remaining`, advancing it past the + * yielded member. `key` and `value` are borrowed slices into the original + * buffer with surrounding whitespace trimmed; any property suffix (`;...`) + * after the value is stripped. Values are not percent-decoded; use + * `sentry__percent_decode_inplace` on a mutable copy if needed. + * + * Malformed members (missing `=`, empty key) are skipped silently. Returns + * false when `remaining` is exhausted. + */ +bool sentry__baggage_iter_next( + sentry_slice_t *remaining, sentry_slice_t *key, sentry_slice_t *value); + +/** + * Decodes `%XX` percent-escapes in the first `len` bytes of `s` in place. + * Malformed escapes (non-hex or truncated at the end) are passed through + * verbatim. Returns the new length; the caller is responsible for writing a + * terminating NUL if one is required. + */ +size_t sentry__percent_decode_inplace(char *s, size_t len); + #endif diff --git a/src/sentry_value.c b/src/sentry_value.c index 046b06171..c2ca6bd5f 100644 --- a/src/sentry_value.c +++ b/src/sentry_value.c @@ -934,6 +934,21 @@ sentry_value_get_by_index(sentry_value_t value, size_t index) return sentry_value_new_null(); } +void +sentry__value_foreach_key_value(sentry_value_t value, + void (*callback)(const char *key, sentry_value_t value, void *userdata), + void *userdata) +{ + const thing_t *thing = value_as_thing(value); + if (!thing || thing_get_type(thing) != THING_TYPE_OBJECT) { + return; + } + const obj_t *o = thing->payload._ptr; + for (size_t i = 0; i < o->len; i++) { + callback(o->pairs[i].k, o->pairs[i].v, userdata); + } +} + sentry_value_t sentry_value_get_by_index_owned(sentry_value_t value, size_t index) { diff --git a/src/sentry_value.h b/src/sentry_value.h index 7048b0bef..f8fb96fd2 100644 --- a/src/sentry_value.h +++ b/src/sentry_value.h @@ -67,6 +67,14 @@ sentry_value_t sentry__value_new_list_with_size(size_t size); */ sentry_value_t sentry__value_new_object_with_size(size_t size); +/** + * Iterates over the key/value pairs of an object value. The callback receives a + * borrowed reference for each value. Does nothing if `value` is not an object. + */ +void sentry__value_foreach_key_value(sentry_value_t value, + void (*callback)(const char *key, sentry_value_t value, void *userdata), + void *userdata); + /** * This will parse the Value into a UUID, or return a `nil` UUID on error. * See also `sentry_uuid_from_string`. diff --git a/tests/test_integration_transactions.py b/tests/test_integration_transactions.py index e8bf0ed2b..c9f32b8fd 100644 --- a/tests/test_integration_transactions.py +++ b/tests/test_integration_transactions.py @@ -21,6 +21,8 @@ pytestmark = pytest.mark.skipif(not has_http, reason="tests need http") RFC3339_FORMAT = "%Y-%m-%dT%H:%M:%S.%fZ" +UPSTREAM_TRACE_ID = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" +UPSTREAM_PARENT_SPAN_ID = "bbbbbbbbbbbbbbbb" @pytest.mark.parametrize( @@ -255,7 +257,6 @@ def test_transaction_trace_header(cmake, httpserver): del trace_header["sample_rand"] assert trace_header == { "environment": "development", - "org_id": "", "public_key": "uiaeosnrtdy", "release": "test-example-release", "sample_rate": 1, @@ -301,7 +302,6 @@ def test_event_trace_header(cmake, httpserver): del trace_header["sample_rand"] assert trace_header == { "environment": "development", - "org_id": "", "public_key": "uiaeosnrtdy", "release": "test-example-release", "sample_rate": 0, # since we don't capture-transaction @@ -466,3 +466,96 @@ def test_set_trace_transaction_update_from_header_event(cmake, httpserver): # tx gets parent span_id from update_from_header assert trace_context["parent_span_id"] assert trace_context["parent_span_id"] == expected_parent_span_id + + +@pytest.mark.parametrize( + "strict,incoming_baggage_flag,continues,expected_dsc", + [ + pytest.param( + True, + "incoming-baggage", + True, + { + "environment": "upstream", + "org_id": "123456", + "release": "upstream-app@1.0", + }, + id="strict-matching-org-continues", + ), + pytest.param( + False, + "incoming-baggage-mismatch", + False, + { + "environment": "development", + "org_id": "123456", + "release": "test-example-release", + }, + id="lenient-mismatched-org-forks", + ), + pytest.param( + True, + None, + False, + { + "environment": "development", + "org_id": "123456", + "release": "test-example-release", + }, + id="strict-no-baggage-forks", + ), + pytest.param( + False, + None, + True, + { + "environment": "development", + "org_id": "123456", + "release": "test-example-release", + }, + id="lenient-no-baggage-continues", + ), + ], +) +def test_strict_trace_continuation( + cmake, httpserver, strict, incoming_baggage_flag, continues, expected_dsc +): + tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "none"}) + + httpserver.expect_oneshot_request("/api/123456/envelope/").respond_with_data("OK") + env = dict(os.environ, SENTRY_DSN=make_dsn(httpserver), SENTRY_RELEASE="🤮🚀") + + args = [ + "log", + "org-id", + "capture-transaction", + "incoming-trace", + ] + if strict: + args.append("strict-trace-continuation") + if incoming_baggage_flag: + args.append(incoming_baggage_flag) + + run(tmp_path, "sentry_example", args, env=env) + + assert len(httpserver.log) == 1 + event_envelope = Envelope.deserialize(httpserver.log[0][0].get_data()) + event_envelope.print_verbose() + + (event,) = event_envelope.items + assert event.headers["type"] == "transaction" + payload = event.payload.json + trace_context = payload["contexts"]["trace"] + trace_header = event_envelope.headers["trace"] + + assert "incoming_dsc" not in payload + assert trace_header["trace_id"] == trace_context["trace_id"] + for key, value in expected_dsc.items(): + assert trace_header[key] == value + + if continues: + assert trace_context["trace_id"] == UPSTREAM_TRACE_ID + assert trace_context["parent_span_id"] == UPSTREAM_PARENT_SPAN_ID + else: + assert trace_context["trace_id"] != UPSTREAM_TRACE_ID + assert "parent_span_id" not in trace_context diff --git a/tests/unit/test_envelopes.c b/tests/unit/test_envelopes.c index 9cd4017ee..49c9cfaa1 100644 --- a/tests/unit/test_envelopes.c +++ b/tests/unit/test_envelopes.c @@ -15,7 +15,7 @@ static char *const SERIALIZED_ENVELOPE_STR = "{\"dsn\":\"https://foo@sentry.invalid/42\"," "\"event_id\":\"c993afb6-b4ac-48a6-b61b-2558e601d65d\",\"trace\":{" - "\"public_key\":\"foo\",\"org_id\":\"\",\"sample_rate\":0,\"sample_" + "\"public_key\":\"foo\",\"sample_rate\":0,\"sample_" "rand\":0.01006918276309107,\"release\":\"test-release\",\"environment\":" "\"production\",\"sampled\":\"false\"}}\n" "{\"type\":\"event\",\"length\":71}\n" diff --git a/tests/unit/test_tracing.c b/tests/unit/test_tracing.c index 96843011b..9a32a0167 100644 --- a/tests/unit/test_tracing.c +++ b/tests/unit/test_tracing.c @@ -1,8 +1,10 @@ #include "sentry_testsupport.h" +#include "sentry_options.h" #include "sentry_scope.h" #include "sentry_string.h" #include "sentry_tracing.h" +#include "sentry_utils.h" #include "sentry_uuid.h" #define IS_NULL(Src, Field) \ @@ -1945,5 +1947,369 @@ SENTRY_TEST(traceparent_header_generation) sentry_close(); } +static bool +trace_can_continue( + const char *sdk_org_id, const char *incoming_org_id, bool strict) +{ + SENTRY_TEST_OPTIONS_NEW(options); + sentry_options_set_strict_trace_continuation(options, strict); + if (sdk_org_id) { + sentry_options_set_org_id(options, sdk_org_id); + } + + sentry_value_t incoming = sentry_value_new_object(); + if (incoming_org_id) { + sentry_value_set_by_key( + incoming, "org_id", sentry_value_new_string(incoming_org_id)); + } + + bool rv = sentry__trace_can_continue(incoming, options); + + sentry_value_decref(incoming); + sentry_options_free(options); + return rv; +} + +SENTRY_TEST(trace_continuation_truth_table) +{ + // Per + // https://develop.sentry.dev/sdk/foundations/trace-propagation/#strict-trace-continuation + // Both absent or both present-equal: continue regardless of strict. + TEST_CHECK(trace_can_continue(NULL, NULL, false)); + TEST_CHECK(trace_can_continue(NULL, NULL, true)); + TEST_CHECK(trace_can_continue("1", "1", false)); + TEST_CHECK(trace_can_continue("1", "1", true)); + // Empty string is treated as absent. + TEST_CHECK(trace_can_continue("", "", true)); + + // Both present and differing: never continue. + TEST_CHECK(!trace_can_continue("1", "2", false)); + TEST_CHECK(!trace_can_continue("1", "2", true)); + + // Exactly one present: continue iff strict is false. + TEST_CHECK(trace_can_continue("1", NULL, false)); + TEST_CHECK(trace_can_continue(NULL, "1", false)); + TEST_CHECK(!trace_can_continue("1", NULL, true)); + TEST_CHECK(!trace_can_continue(NULL, "1", true)); +} + +SENTRY_TEST(effective_org_id_resolution) +{ + // No DSN, no option → NULL + SENTRY_TEST_OPTIONS_NEW(opts1); + TEST_CHECK(sentry__options_get_org_id(opts1) == NULL); + sentry_options_free(opts1); + + // DSN with org → DSN value + SENTRY_TEST_OPTIONS_NEW(opts2); + sentry_options_set_dsn(opts2, "https://k@o123456.ingest.sentry.io/1"); + TEST_CHECK_STRING_EQUAL(sentry__options_get_org_id(opts2), "123456"); + sentry_options_free(opts2); + + // DSN without org_id-encoded host → NULL + SENTRY_TEST_OPTIONS_NEW(opts3); + sentry_options_set_dsn(opts3, "https://k@self-hosted.example.com/1"); + TEST_CHECK(sentry__options_get_org_id(opts3) == NULL); + sentry_options_free(opts3); + + // Option overrides DSN + SENTRY_TEST_OPTIONS_NEW(opts4); + sentry_options_set_dsn(opts4, "https://k@o123456.ingest.sentry.io/1"); + sentry_options_set_org_id(opts4, "999"); + TEST_CHECK_STRING_EQUAL(sentry__options_get_org_id(opts4), "999"); + sentry_options_free(opts4); + + // Empty option falls back to DSN + SENTRY_TEST_OPTIONS_NEW(opts5); + sentry_options_set_dsn(opts5, "https://k@o123456.ingest.sentry.io/1"); + sentry_options_set_org_id(opts5, ""); + TEST_CHECK_STRING_EQUAL(sentry__options_get_org_id(opts5), "123456"); + sentry_options_free(opts5); +} + +SENTRY_TEST(parse_baggage_basic_and_filtering) +{ + sentry_transaction_context_t *tx_ctx + = sentry_transaction_context_new("t", "op"); + sentry_transaction_context_update_from_header(tx_ctx, "baggage", + "sentry-org_id=123456 , sentry-environment=upstream,nonsentry=skip," + " sentry-release=app%401.0 ,malformed"); + + sentry_value_t inner + = sentry_value_get_by_key(tx_ctx->inner, "incoming_dsc"); + TEST_CHECK(!sentry_value_is_null(inner)); + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key(inner, "org_id")), + "123456"); + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key(inner, "environment")), + "upstream"); + // percent-decoded value + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key(inner, "release")), + "app@1.0"); + // non-sentry member ignored + TEST_CHECK( + sentry_value_is_null(sentry_value_get_by_key(inner, "nonsentry"))); + + sentry__transaction_context_free(tx_ctx); +} + +typedef struct { + char sentry_trace[64]; + char baggage[1024]; +} continuation_collector_t; + +static void +collect_continuation_headers(const char *key, const char *value, void *userdata) +{ + continuation_collector_t *c = (continuation_collector_t *)userdata; + if (strcmp(key, "sentry-trace") == 0) { + snprintf(c->sentry_trace, sizeof(c->sentry_trace), "%s", value); + } else if (strcmp(key, "baggage") == 0) { + snprintf(c->baggage, sizeof(c->baggage), "%s", value); + } +} + +#define UPSTREAM_TRACE_ID "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" +#define UPSTREAM_PARENT_SPAN_ID "bbbbbbbbbbbbbbbb" +#define UPSTREAM_SENTRY_TRACE UPSTREAM_TRACE_ID "-" UPSTREAM_PARENT_SPAN_ID "-1" + +static void +discard_envelope(sentry_envelope_t *envelope, void *state) +{ + (void)state; + sentry_envelope_free(envelope); +} + +static sentry_transaction_t * +start_tx_with_upstream(const char *baggage) +{ + sentry_transaction_context_t *tx_ctx + = sentry_transaction_context_new("t", "op"); + sentry_transaction_context_update_from_header( + tx_ctx, "sentry-trace", UPSTREAM_SENTRY_TRACE); + if (baggage) { + sentry_transaction_context_update_from_header( + tx_ctx, "baggage", baggage); + } + return sentry_transaction_start(tx_ctx, sentry_value_new_null()); +} + +SENTRY_TEST(strict_continuation_matching_org_continues) +{ + SENTRY_TEST_OPTIONS_NEW(options); + sentry_options_set_dsn(options, "https://k@o123456.ingest.sentry.io/1"); + sentry_options_set_transport( + options, sentry_transport_new(discard_envelope)); + sentry_options_set_traces_sample_rate(options, 1.0); + sentry_options_set_strict_trace_continuation(options, 1); + sentry_init(options); + + sentry_transaction_t *tx = start_tx_with_upstream( + "sentry-org_id=123456,sentry-environment=upstream," + "sentry-release=upstream-app%401.0"); + + // Trace continued: trace_id and parent_span_id preserved. + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key(tx->inner, "trace_id")), + UPSTREAM_TRACE_ID); + TEST_CHECK_STRING_EQUAL(sentry_value_as_string(sentry_value_get_by_key( + tx->inner, "parent_span_id")), + UPSTREAM_PARENT_SPAN_ID); + // incoming_dsc must not leak into the event. + TEST_CHECK(sentry_value_is_null( + sentry_value_get_by_key(tx->inner, "incoming_dsc"))); + + // Late local updates must not mutate the frozen incoming DSC. + sentry_set_release("local-app@3.0"); + sentry_set_environment("local"); + + // Outgoing baggage echoes the upstream environment / release verbatim. + continuation_collector_t c = { 0 }; + sentry_transaction_iter_headers(tx, collect_continuation_headers, &c); + TEST_CHECK(strstr(c.baggage, "sentry-trace_id=" UPSTREAM_TRACE_ID) != NULL); + TEST_CHECK(strstr(c.baggage, "sentry-org_id=123456") != NULL); + TEST_CHECK(strstr(c.baggage, "sentry-environment=upstream") != NULL); + TEST_CHECK(strstr(c.baggage, "sentry-environment=local") == NULL); + // Percent-encoded as it came in. + TEST_CHECK(strstr(c.baggage, "sentry-release=upstream-app%401.0") != NULL); + TEST_CHECK(strstr(c.baggage, "sentry-release=local-app%403.0") == NULL); + + sentry_transaction_finish(tx); + sentry_close(); +} + +SENTRY_TEST(strict_continuation_org_mismatch_forks) +{ + SENTRY_TEST_OPTIONS_NEW(options); + sentry_options_set_dsn(options, "https://k@o123456.ingest.sentry.io/1"); + sentry_options_set_transport( + options, sentry_transport_new(discard_envelope)); + // sample_rate=0 + upstream sentry-trace ending in `-1`: only the fork + // dropping the inherited sampling decision lets the local rate win. + sentry_options_set_traces_sample_rate(options, 0.0); + // Strict OFF: mismatch must still fork (spec MUST). + sentry_init(options); + + sentry_transaction_t *tx + = start_tx_with_upstream("sentry-org_id=99999,sentry-environment=up"); + + const char *trace_id = sentry_value_as_string( + sentry_value_get_by_key(tx->inner, "trace_id")); + TEST_CHECK(strcmp(trace_id, UPSTREAM_TRACE_ID) != 0); + TEST_CHECK(sentry_value_is_null( + sentry_value_get_by_key(tx->inner, "parent_span_id"))); + TEST_CHECK( + !sentry_value_is_true(sentry_value_get_by_key(tx->inner, "sampled"))); + + // Outgoing baggage carries the SDK's own org_id, not upstream's. + continuation_collector_t c = { 0 }; + sentry_transaction_iter_headers(tx, collect_continuation_headers, &c); + TEST_CHECK(strstr(c.baggage, "sentry-org_id=123456") != NULL); + TEST_CHECK(strstr(c.baggage, "sentry-org_id=99999") == NULL); + TEST_CHECK(strstr(c.baggage, "sentry-environment=up") == NULL); + + sentry_transaction_finish(tx); + sentry_close(); +} + +SENTRY_TEST(strict_continuation_asymmetric_with_strict_forks) +{ + SENTRY_TEST_OPTIONS_NEW(options); + sentry_options_set_dsn(options, "https://k@o123456.ingest.sentry.io/1"); + sentry_options_set_transport( + options, sentry_transport_new(discard_envelope)); + sentry_options_set_traces_sample_rate(options, 1.0); + sentry_options_set_strict_trace_continuation(options, 1); + sentry_init(options); + + // Upstream baggage with no org_id, SDK has 123456 → fork. + sentry_transaction_t *tx + = start_tx_with_upstream("sentry-environment=upstream"); + + const char *trace_id = sentry_value_as_string( + sentry_value_get_by_key(tx->inner, "trace_id")); + TEST_CHECK(strcmp(trace_id, UPSTREAM_TRACE_ID) != 0); + + sentry_transaction_finish(tx); + sentry_close(); +} + +SENTRY_TEST(strict_continuation_asymmetric_lenient_continues) +{ + SENTRY_TEST_OPTIONS_NEW(options); + sentry_options_set_dsn(options, "https://k@o123456.ingest.sentry.io/1"); + sentry_options_set_transport( + options, sentry_transport_new(discard_envelope)); + sentry_options_set_traces_sample_rate(options, 1.0); + // Strict OFF. + sentry_init(options); + + sentry_transaction_t *tx + = start_tx_with_upstream("sentry-environment=upstream"); + + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key(tx->inner, "trace_id")), + UPSTREAM_TRACE_ID); + + sentry_transaction_finish(tx); + sentry_close(); +} + +SENTRY_TEST(strict_continuation_no_baggage_forks) +{ + SENTRY_TEST_OPTIONS_NEW(options); + sentry_options_set_dsn(options, "https://k@o123456.ingest.sentry.io/1"); + sentry_options_set_transport( + options, sentry_transport_new(discard_envelope)); + sentry_options_set_traces_sample_rate(options, 1.0); + sentry_options_set_strict_trace_continuation(options, 1); + sentry_init(options); + + // Only sentry-trace is received; no baggage at all. SDK has org_id, + // incoming has none (baggage absent) → strict MUST fork. + sentry_transaction_t *tx = start_tx_with_upstream(NULL); + + const char *trace_id = sentry_value_as_string( + sentry_value_get_by_key(tx->inner, "trace_id")); + TEST_CHECK(strcmp(trace_id, UPSTREAM_TRACE_ID) != 0); + TEST_CHECK(sentry_value_is_null( + sentry_value_get_by_key(tx->inner, "parent_span_id"))); + + // Scope propagation follows the fork: no lingering upstream trace_id. + SENTRY_WITH_SCOPE (scope) { + const char *scope_trace_id + = sentry_value_as_string(sentry_value_get_by_key( + sentry_value_get_by_key(scope->propagation_context, "trace"), + "trace_id")); + TEST_CHECK(strcmp(scope_trace_id, UPSTREAM_TRACE_ID) != 0); + TEST_CHECK_STRING_EQUAL(scope_trace_id, trace_id); + } + + sentry_transaction_finish(tx); + sentry_close(); +} + +SENTRY_TEST(continuation_no_baggage_uses_sdk_dsc) +{ + SENTRY_TEST_OPTIONS_NEW(options); + sentry_options_set_dsn(options, "https://k@o123456.ingest.sentry.io/1"); + sentry_options_set_transport( + options, sentry_transport_new(discard_envelope)); + sentry_options_set_release(options, "sdk-app@2.0"); + sentry_options_set_traces_sample_rate(options, 1.0); + // Strict OFF + no baggage + SDK has org → continue; DSC built by SDK. + sentry_init(options); + + sentry_transaction_t *tx = start_tx_with_upstream(NULL); + + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key(tx->inner, "trace_id")), + UPSTREAM_TRACE_ID); + + continuation_collector_t c = { 0 }; + sentry_transaction_iter_headers(tx, collect_continuation_headers, &c); + TEST_CHECK(strstr(c.baggage, "sentry-trace_id=" UPSTREAM_TRACE_ID) != NULL); + TEST_CHECK(strstr(c.baggage, "sentry-org_id=123456") != NULL); + TEST_CHECK(strstr(c.baggage, "sentry-release=sdk-app%402.0") != NULL); + + sentry_transaction_finish(tx); + sentry_close(); +} + +SENTRY_TEST(set_trace_rebuilds_dsc_sample_rand) +{ + SENTRY_TEST_OPTIONS_NEW(options); + sentry_options_set_dsn(options, "https://k@o123456.ingest.sentry.io/1"); + sentry_options_set_transport( + options, sentry_transport_new(discard_envelope)); + sentry_options_set_traces_sample_rate(options, 1.0); + sentry_init(options); + + double init_sample_rand = 0.0; + SENTRY_WITH_SCOPE (scope) { + init_sample_rand = sentry_value_as_double(sentry_value_get_by_key( + scope->dynamic_sampling_context, "sample_rand")); + } + + sentry_set_trace("11112222333344445555666677778888", "1234567812345678"); + + double new_sample_rand = -1.0; + SENTRY_WITH_SCOPE (scope) { + new_sample_rand = sentry_value_as_double(sentry_value_get_by_key( + scope->dynamic_sampling_context, "sample_rand")); + } + // sample_rand is regenerated for the new trace, so the DSC must reflect + // the fresh value, not the init-time one. + TEST_CHECK(new_sample_rand != init_sample_rand); + + sentry_close(); +} + +#undef UPSTREAM_SENTRY_TRACE +#undef UPSTREAM_PARENT_SPAN_ID +#undef UPSTREAM_TRACE_ID + #undef IS_NULL #undef CHECK_STRING_PROPERTY diff --git a/tests/unit/test_utils.c b/tests/unit/test_utils.c index 406844cde..c8f736a54 100644 --- a/tests/unit/test_utils.c +++ b/tests/unit/test_utils.c @@ -1,8 +1,11 @@ #include "sentry_os.h" +#include "sentry_slice.h" +#include "sentry_string.h" #include "sentry_testsupport.h" #include "sentry_utils.h" #include "sentry_value.h" #include +#include #ifdef SENTRY_PLATFORM_UNIX # include "sentry_unix_pageallocator.h" @@ -486,3 +489,227 @@ SENTRY_TEST(getenv_double) TEST_CHECK(sentry__getenv_double("SENTRY_TEST_DOUBLE", 42.0) == 42.0); #endif } + +#define CHECK_SLICE_EQ(Slice, Str) \ + do { \ + TEST_CHECK_INT_EQUAL((Slice).len, strlen(Str)); \ + TEST_CHECK((Slice).len == strlen(Str) \ + && memcmp((Slice).ptr, (Str), (Slice).len) == 0); \ + } while (0) + +SENTRY_TEST(baggage_iter_basic) +{ + const char *hdr = "a=1,b=2,c=3"; + sentry_slice_t remaining = { hdr, strlen(hdr) }; + sentry_slice_t key, val; + + TEST_CHECK(sentry__baggage_iter_next(&remaining, &key, &val)); + CHECK_SLICE_EQ(key, "a"); + CHECK_SLICE_EQ(val, "1"); + TEST_CHECK(sentry__baggage_iter_next(&remaining, &key, &val)); + CHECK_SLICE_EQ(key, "b"); + CHECK_SLICE_EQ(val, "2"); + TEST_CHECK(sentry__baggage_iter_next(&remaining, &key, &val)); + CHECK_SLICE_EQ(key, "c"); + CHECK_SLICE_EQ(val, "3"); + TEST_CHECK(!sentry__baggage_iter_next(&remaining, &key, &val)); +} + +SENTRY_TEST(baggage_iter_ows_trimmed) +{ + // Per W3C baggage, optional whitespace around keys, values, and commas + // must be ignored. + const char *hdr = " a = 1 ,\tb=2 , c =\t3\t"; + sentry_slice_t remaining = { hdr, strlen(hdr) }; + sentry_slice_t key, val; + + TEST_CHECK(sentry__baggage_iter_next(&remaining, &key, &val)); + CHECK_SLICE_EQ(key, "a"); + CHECK_SLICE_EQ(val, "1"); + TEST_CHECK(sentry__baggage_iter_next(&remaining, &key, &val)); + CHECK_SLICE_EQ(key, "b"); + CHECK_SLICE_EQ(val, "2"); + TEST_CHECK(sentry__baggage_iter_next(&remaining, &key, &val)); + CHECK_SLICE_EQ(key, "c"); + CHECK_SLICE_EQ(val, "3"); + TEST_CHECK(!sentry__baggage_iter_next(&remaining, &key, &val)); +} + +SENTRY_TEST(baggage_iter_empty_and_malformed_skipped) +{ + // Missing `=`, empty keys, and bare commas are all skipped; valid + // members on either side still yield. + const char *hdr = ",malformed, ,=orphan,a=1,=,bare,b=2,"; + sentry_slice_t remaining = { hdr, strlen(hdr) }; + sentry_slice_t key, val; + + TEST_CHECK(sentry__baggage_iter_next(&remaining, &key, &val)); + CHECK_SLICE_EQ(key, "a"); + CHECK_SLICE_EQ(val, "1"); + TEST_CHECK(sentry__baggage_iter_next(&remaining, &key, &val)); + CHECK_SLICE_EQ(key, "b"); + CHECK_SLICE_EQ(val, "2"); + TEST_CHECK(!sentry__baggage_iter_next(&remaining, &key, &val)); +} + +SENTRY_TEST(baggage_iter_empty_value_allowed) +{ + // Empty values are valid per spec. + const char *hdr = "a=,b=x"; + sentry_slice_t remaining = { hdr, strlen(hdr) }; + sentry_slice_t key, val; + + TEST_CHECK(sentry__baggage_iter_next(&remaining, &key, &val)); + CHECK_SLICE_EQ(key, "a"); + TEST_CHECK_INT_EQUAL(val.len, 0); + TEST_CHECK(sentry__baggage_iter_next(&remaining, &key, &val)); + CHECK_SLICE_EQ(key, "b"); + CHECK_SLICE_EQ(val, "x"); + TEST_CHECK(!sentry__baggage_iter_next(&remaining, &key, &val)); +} + +SENTRY_TEST(baggage_iter_properties_stripped) +{ + // Value ends at the first `;`; property text is discarded. + const char *hdr = "a=1;prop=x;q,b=2;meta,c=3"; + sentry_slice_t remaining = { hdr, strlen(hdr) }; + sentry_slice_t key, val; + + TEST_CHECK(sentry__baggage_iter_next(&remaining, &key, &val)); + CHECK_SLICE_EQ(key, "a"); + CHECK_SLICE_EQ(val, "1"); + TEST_CHECK(sentry__baggage_iter_next(&remaining, &key, &val)); + CHECK_SLICE_EQ(key, "b"); + CHECK_SLICE_EQ(val, "2"); + TEST_CHECK(sentry__baggage_iter_next(&remaining, &key, &val)); + CHECK_SLICE_EQ(key, "c"); + CHECK_SLICE_EQ(val, "3"); + TEST_CHECK(!sentry__baggage_iter_next(&remaining, &key, &val)); +} + +SENTRY_TEST(baggage_iter_equals_in_value) +{ + // Only the first `=` separates key from value; subsequent ones are + // part of the value. + const char *hdr = "a=x=y=z"; + sentry_slice_t remaining = { hdr, strlen(hdr) }; + sentry_slice_t key, val; + + TEST_CHECK(sentry__baggage_iter_next(&remaining, &key, &val)); + CHECK_SLICE_EQ(key, "a"); + CHECK_SLICE_EQ(val, "x=y=z"); + TEST_CHECK(!sentry__baggage_iter_next(&remaining, &key, &val)); +} + +SENTRY_TEST(baggage_iter_empty_input) +{ + sentry_slice_t remaining = { "", 0 }; + sentry_slice_t key, val; + TEST_CHECK(!sentry__baggage_iter_next(&remaining, &key, &val)); + + const char *hdr = " "; + remaining = (sentry_slice_t) { hdr, strlen(hdr) }; + TEST_CHECK(!sentry__baggage_iter_next(&remaining, &key, &val)); + + const char *only_commas = ",,,"; + remaining = (sentry_slice_t) { only_commas, strlen(only_commas) }; + TEST_CHECK(!sentry__baggage_iter_next(&remaining, &key, &val)); +} + +SENTRY_TEST(baggage_iter_case_preserved) +{ + // Baggage keys are case-sensitive and the iterator must preserve case. + const char *hdr = "Sentry-Foo=Bar,sentry-foo=baz"; + sentry_slice_t remaining = { hdr, strlen(hdr) }; + sentry_slice_t key, val; + + TEST_CHECK(sentry__baggage_iter_next(&remaining, &key, &val)); + CHECK_SLICE_EQ(key, "Sentry-Foo"); + CHECK_SLICE_EQ(val, "Bar"); + TEST_CHECK(sentry__baggage_iter_next(&remaining, &key, &val)); + CHECK_SLICE_EQ(key, "sentry-foo"); + CHECK_SLICE_EQ(val, "baz"); + TEST_CHECK(!sentry__baggage_iter_next(&remaining, &key, &val)); +} + +static char * +decode_to_owned(const char *src) +{ + size_t len = strlen(src); + char *buf = sentry__string_clone_n(src, len); + size_t new_len = sentry__percent_decode_inplace(buf, len); + buf[new_len] = '\0'; + return buf; +} + +SENTRY_TEST(percent_decode_basic) +{ + char *s; + + s = decode_to_owned(""); + TEST_CHECK_STRING_EQUAL(s, ""); + sentry_free(s); + + s = decode_to_owned("no-escapes_here~."); + TEST_CHECK_STRING_EQUAL(s, "no-escapes_here~."); + sentry_free(s); + + s = decode_to_owned("a%40b%2Cc"); + TEST_CHECK_STRING_EQUAL(s, "a@b,c"); + sentry_free(s); + + // Both lower and upper case hex digits decode the same. + s = decode_to_owned("%2f%2F"); + TEST_CHECK_STRING_EQUAL(s, "//"); + sentry_free(s); + + // %XX decodes to one byte even when that byte is high-ASCII. + s = decode_to_owned("%E2%98%83"); + TEST_CHECK_INT_EQUAL((unsigned char)s[0], 0xE2); + TEST_CHECK_INT_EQUAL((unsigned char)s[1], 0x98); + TEST_CHECK_INT_EQUAL((unsigned char)s[2], 0x83); + TEST_CHECK_INT_EQUAL(s[3], '\0'); + sentry_free(s); +} + +SENTRY_TEST(percent_decode_malformed_passed_through) +{ + char *s; + + // Non-hex digits: left as-is. + s = decode_to_owned("%GG"); + TEST_CHECK_STRING_EQUAL(s, "%GG"); + sentry_free(s); + + s = decode_to_owned("a%Zbc"); + TEST_CHECK_STRING_EQUAL(s, "a%Zbc"); + sentry_free(s); + + // Truncated escape at end of string: left as-is. + s = decode_to_owned("abc%"); + TEST_CHECK_STRING_EQUAL(s, "abc%"); + sentry_free(s); + + s = decode_to_owned("abc%4"); + TEST_CHECK_STRING_EQUAL(s, "abc%4"); + sentry_free(s); + + // Mid-string escape followed by non-hex: left as-is, then resumes. + s = decode_to_owned("%4X%40"); + TEST_CHECK_STRING_EQUAL(s, "%4X@"); + sentry_free(s); +} + +SENTRY_TEST(percent_decode_does_not_read_past_len) +{ + // The decoder must respect `len` even when the buffer is longer; a + // trailing `%XX` after `len` must not be touched. + char buf[] = "a%40b%41"; + size_t new_len = sentry__percent_decode_inplace(buf, 3); + TEST_CHECK_INT_EQUAL(new_len, 3); + TEST_CHECK(memcmp(buf, "a%4", 3) == 0); + // Bytes past `len` are untouched. + TEST_CHECK_STRING_EQUAL(buf + 3, "0b%41"); +} + +#undef CHECK_SLICE_EQ diff --git a/tests/unit/test_value.c b/tests/unit/test_value.c index 9b5c67f6e..787f34cd8 100644 --- a/tests/unit/test_value.c +++ b/tests/unit/test_value.c @@ -15,6 +15,25 @@ breadcrumb_with_ts(const char *message, const char *timestamp) return breadcrumb; } +typedef struct { + const char *keys[4]; + sentry_value_t values[4]; + size_t count; +} value_foreach_key_value_collector_t; + +static void +collect_value_pair(const char *key, sentry_value_t value, void *userdata) +{ + value_foreach_key_value_collector_t *collector + = (value_foreach_key_value_collector_t *)userdata; + if (collector->count >= 4) { + return; + } + collector->keys[collector->count] = key; + collector->values[collector->count] = value; + collector->count++; +} + SENTRY_TEST(value_null) { sentry_value_t val = sentry_value_new_null(); @@ -656,6 +675,33 @@ SENTRY_TEST(value_freezing) sentry_value_decref(val); } +SENTRY_TEST(value_foreach_key_value) +{ + sentry_value_t value = sentry_value_new_object(); + sentry_value_set_by_key(value, "first", sentry_value_new_string("one")); + sentry_value_set_by_key(value, "second", sentry_value_new_int32(2)); + sentry_value_set_by_key(value, "third", sentry_value_new_bool(true)); + + value_foreach_key_value_collector_t collector = { 0 }; + sentry__value_foreach_key_value(value, collect_value_pair, &collector); + + TEST_CHECK_INT_EQUAL(collector.count, 3); + TEST_CHECK_STRING_EQUAL(collector.keys[0], "first"); + TEST_CHECK_STRING_EQUAL(sentry_value_as_string(collector.values[0]), "one"); + TEST_CHECK_STRING_EQUAL(collector.keys[1], "second"); + TEST_CHECK_INT_EQUAL(sentry_value_as_int32(collector.values[1]), 2); + TEST_CHECK_STRING_EQUAL(collector.keys[2], "third"); + TEST_CHECK(sentry_value_is_true(collector.values[2])); + + value_foreach_key_value_collector_t ignored = { 0 }; + sentry_value_t not_object = sentry_value_new_string("not-object"); + sentry__value_foreach_key_value(not_object, collect_value_pair, &ignored); + TEST_CHECK_INT_EQUAL(ignored.count, 0); + sentry_value_decref(not_object); + + sentry_value_decref(value); +} + SENTRY_TEST(value_stringify) { #define STRINGIFY_AND_CHECK(Val, Expected) \ diff --git a/tests/unit/tests.inc b/tests/unit/tests.inc index 8be3d7181..9c7b1adb4 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -16,6 +16,14 @@ XX(attachments_bytes) XX(attachments_extend) XX(attachments_more_than_ten) XX(background_worker) +XX(baggage_iter_basic) +XX(baggage_iter_case_preserved) +XX(baggage_iter_empty_and_malformed_skipped) +XX(baggage_iter_empty_input) +XX(baggage_iter_empty_value_allowed) +XX(baggage_iter_equals_in_value) +XX(baggage_iter_ows_trimmed) +XX(baggage_iter_properties_stripped) XX(basic_consent_tracking) XX(basic_function_transport) XX(basic_function_transport_transaction) @@ -79,6 +87,7 @@ XX(client_report_restore) XX(client_report_save_raw_envelope) XX(concurrent_init) XX(concurrent_uninit) +XX(continuation_no_baggage_uses_sdk_dsc) XX(count_sampled_events) XX(crash_context_handler_path_propagation) XX(crash_context_null_options) @@ -118,6 +127,7 @@ XX(dsn_with_ending_forward_slash_will_be_cleaned) XX(dsn_with_non_http_scheme_is_invalid) XX(dsn_without_project_id_is_invalid) XX(dsn_without_url_scheme_is_invalid) +XX(effective_org_id_resolution) XX(embedded_info_basic) XX(embedded_info_build_id) XX(embedded_info_disabled) @@ -201,6 +211,7 @@ XX(os_release_non_existent_files) XX(os_releases_snapshot) XX(overflow_spans) XX(page_allocator) +XX(parse_baggage_basic_and_filtering) XX(path_basename) XX(path_basics) XX(path_copy) @@ -214,6 +225,9 @@ XX(path_mtime) XX(path_relative_filename) XX(path_rename) XX(path_unique) +XX(percent_decode_basic) +XX(percent_decode_does_not_read_past_len) +XX(percent_decode_malformed_passed_through) XX(process_invalid) XX(process_spawn) XX(procmaps_parser) @@ -269,6 +283,7 @@ XX(set_trace) XX(set_trace_id_before_scoped_txn) XX(set_trace_id_twice) XX(set_trace_id_with_txn) +XX(set_trace_rebuilds_dsc_sample_rand) XX(set_trace_update_from_header) XX(slice) XX(span_data) @@ -278,12 +293,18 @@ XX(span_tagging_n) XX(spans_on_scope) XX(stack_guarantee) XX(stack_guarantee_auto_init) +XX(strict_continuation_asymmetric_lenient_continues) +XX(strict_continuation_asymmetric_with_strict_forks) +XX(strict_continuation_matching_org_continues) +XX(strict_continuation_no_baggage_forks) +XX(strict_continuation_org_mismatch_forks) XX(string_address_format) XX(stringbuilder_append_overflow) XX(stringbuilder_reserve_overflow) XX(symbolizer) XX(task_queue) XX(thread_without_name_still_valid) +XX(trace_continuation_truth_table) XX(traceparent_header_disabled_by_default) XX(traceparent_header_generation) XX(transaction_name_backfill_on_finish) @@ -322,6 +343,7 @@ XX(uuid_v4) XX(value_attribute) XX(value_bool) XX(value_double) +XX(value_foreach_key_value) XX(value_freezing) XX(value_from_msgpack_bool) XX(value_from_msgpack_double)