diff --git a/CHANGELOG.md b/CHANGELOG.md index 2196a9782..8e87533d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ **Features**: +- 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)) - Linux: support 32-bit ARM. ([#1659](https://github.com/getsentry/sentry-native/issues/1659)) **Fixes**: diff --git a/include/sentry.h b/include/sentry.h index 4b132d264..ac26b49a6 100644 --- a/include/sentry.h +++ b/include/sentry.h @@ -2316,6 +2316,48 @@ 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); + +/** + * Returns the organization ID previously set via `sentry_options_set_org_id`, + * or NULL if none was set. Does not fall back to the DSN-derived value. + */ +SENTRY_EXPERIMENTAL_API const char *sentry_options_get_org_id( + const sentry_options_t *opts); + +/** + * 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. @@ -2871,6 +2913,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 4b617c315..32fa989fd 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) @@ -247,7 +214,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_rebuild_dsc_from_options(scope, options); } if (backend && backend->user_consent_changed_func) { backend->user_consent_changed_func(backend); @@ -1180,15 +1147,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_rebuild_dsc_from_options(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_rebuild_dsc_from_options(scope, options); + } } } @@ -1260,6 +1236,50 @@ 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) { + const char *sdk_org + = sentry__options_get_effective_org_id(options); + const char *inc_org = sentry_value_as_string( + sentry_value_get_by_key(incoming, "org_id")); + if (!*inc_org) { + inc_org = NULL; + } + + if (sentry__trace_continuation_allowed( + sdk_org, inc_org, options->strict_trace_continuation)) { + // 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_from_incoming(scope, incoming); + } else { + sentry__scope_rebuild_dsc_from_options(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_rebuild_dsc_from_options(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( @@ -1269,7 +1289,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 d8a173278..f7b8a6634 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->cache_keep = false; @@ -122,6 +123,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); @@ -218,6 +220,39 @@ 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) +{ + return opts->org_id; +} + +const char * +sentry__options_get_effective_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) { @@ -922,6 +957,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 86ee949c2..6058991f7 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 @@ -107,4 +109,12 @@ struct sentry_options_s { */ sentry_options_t *sentry__options_incref(sentry_options_t *options); +/** + * Returns the effective 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_effective_org_id( + const sentry_options_t *options); + #endif diff --git a/src/sentry_scope.c b/src/sentry_scope.c index c14ab6f71..0f0c17923 100644 --- a/src/sentry_scope.c +++ b/src/sentry_scope.c @@ -189,6 +189,51 @@ sentry__scope_free(sentry_scope_t *scope) sentry_free(scope); } +void +sentry__scope_freeze_dsc_from_incoming( + 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_rebuild_dsc_from_options( + 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_effective_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..ec90500e7 100644 --- a/src/sentry_scope.h +++ b/src/sentry_scope.h @@ -128,6 +128,22 @@ 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_rebuild_dsc_from_options( + 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_from_incoming( + 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..d073aeca9 100644 --- a/src/sentry_tracing.c +++ b/src/sentry_tracing.c @@ -9,8 +9,34 @@ #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 sentry_value_t new_span_n(sentry_value_t parent, sentry_slice_t operation) { @@ -269,6 +295,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 +331,59 @@ 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_continuation_allowed( + const char *sdk_org_id, const char *incoming_org_id, bool strict) +{ + 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 !strict; + } + 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 +396,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 +890,47 @@ 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_t dsc = scope->dynamic_sampling_context; + size_t len = sentry_value_get_length(dsc); + for (size_t i = 0; i < len; i++) { + const char *k = sentry__value_object_key_at(dsc, i); + if (!k || strcmp(k, "trace_id") == 0) { + continue; + } + sentry_value_t v = sentry__value_object_value_at(dsc, i); + if (sentry_value_is_null(v)) { + continue; + } + char *vs = sentry__value_stringify(v); + if (!vs) { + continue; + } + sentry__stringbuilder_append(&sb, ",sentry-"); + sentry__stringbuilder_append(&sb, k); + sentry__stringbuilder_append_char(&sb, '='); + percent_encode_append(&sb, vs); + sentry_free(vs); + } + } + + 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..4a3dd5cc0 100644 --- a/src/sentry_tracing.h +++ b/src/sentry_tracing.h @@ -59,4 +59,15 @@ 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's organization + * ID, the incoming trace's organization ID, and the strict-trace-continuation + * flag. Either ID may be NULL or empty to indicate "absent". + * + * See + * https://develop.sentry.dev/sdk/foundations/trace-propagation/#strict-trace-continuation + */ +bool sentry__trace_continuation_allowed( + const char *sdk_org_id, const char *incoming_org_id, bool strict); + #endif diff --git a/src/sentry_utils.c b/src/sentry_utils.c index 136c09b13..5c72ecf6f 100644 --- a/src/sentry_utils.c +++ b/src/sentry_utils.c @@ -10,6 +10,7 @@ #include "sentry_random.h" +#include #include #include #include @@ -631,3 +632,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 75ea87d4c..aa758d9c3 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 @@ -249,4 +250,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 6fc1b030f..ca89512a6 100644 --- a/src/sentry_value.c +++ b/src/sentry_value.c @@ -934,6 +934,32 @@ sentry_value_get_by_index(sentry_value_t value, size_t index) return sentry_value_new_null(); } +const char * +sentry__value_object_key_at(sentry_value_t value, size_t idx) +{ + const thing_t *thing = value_as_thing(value); + if (thing && thing_get_type(thing) == THING_TYPE_OBJECT) { + const obj_t *o = thing->payload._ptr; + if (idx < o->len) { + return o->pairs[idx].k; + } + } + return NULL; +} + +sentry_value_t +sentry__value_object_value_at(sentry_value_t value, size_t idx) +{ + const thing_t *thing = value_as_thing(value); + if (thing && thing_get_type(thing) == THING_TYPE_OBJECT) { + const obj_t *o = thing->payload._ptr; + if (idx < o->len) { + return o->pairs[idx].v; + } + } + return sentry_value_new_null(); +} + 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..98338e3e4 100644 --- a/src/sentry_value.h +++ b/src/sentry_value.h @@ -67,6 +67,20 @@ sentry_value_t sentry__value_new_list_with_size(size_t size); */ sentry_value_t sentry__value_new_object_with_size(size_t size); +/** + * Returns the key of the object pair at the given index, or NULL if the value + * is not an object or the index is out of range. Use `sentry_value_get_length` + * to determine the number of pairs. + */ +const char *sentry__value_object_key_at(sentry_value_t value, size_t idx); + +/** + * Returns the value of the object pair at the given index, or `null` if the + * value is not an object or the index is out of range. The returned value is + * a borrowed reference (not increfed). + */ +sentry_value_t sentry__value_object_value_at(sentry_value_t value, size_t idx); + /** * 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..092435c36 100644 --- a/tests/test_integration_transactions.py +++ b/tests/test_integration_transactions.py @@ -255,7 +255,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 +300,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 diff --git a/tests/unit/test_envelopes.c b/tests/unit/test_envelopes.c index 25dd24c10..a88a11642 100644 --- a/tests/unit/test_envelopes.c +++ b/tests/unit/test_envelopes.c @@ -10,7 +10,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..75c489571 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,348 @@ SENTRY_TEST(traceparent_header_generation) sentry_close(); } +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(sentry__trace_continuation_allowed(NULL, NULL, false)); + TEST_CHECK(sentry__trace_continuation_allowed(NULL, NULL, true)); + TEST_CHECK(sentry__trace_continuation_allowed("1", "1", false)); + TEST_CHECK(sentry__trace_continuation_allowed("1", "1", true)); + // Empty string is treated as absent. + TEST_CHECK(sentry__trace_continuation_allowed("", "", true)); + + // Both present and differing: never continue. + TEST_CHECK(!sentry__trace_continuation_allowed("1", "2", false)); + TEST_CHECK(!sentry__trace_continuation_allowed("1", "2", true)); + + // Exactly one present: continue iff strict is false. + TEST_CHECK(sentry__trace_continuation_allowed("1", NULL, false)); + TEST_CHECK(sentry__trace_continuation_allowed(NULL, "1", false)); + TEST_CHECK(!sentry__trace_continuation_allowed("1", NULL, true)); + TEST_CHECK(!sentry__trace_continuation_allowed(NULL, "1", true)); +} + +SENTRY_TEST(effective_org_id_resolution) +{ + // No DSN, no option → NULL + SENTRY_TEST_OPTIONS_NEW(opts1); + TEST_CHECK(sentry__options_get_effective_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_effective_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_effective_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_effective_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_effective_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 4a95cef4b..29d4d5f37 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" @@ -464,3 +467,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/tests.inc b/tests/unit/tests.inc index a4d72ca20..fa182c740 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -8,6 +8,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) @@ -66,6 +74,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) @@ -104,6 +113,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) @@ -184,6 +194,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_current_exe) @@ -195,6 +206,9 @@ XX(path_joining_windows) XX(path_mtime) XX(path_relative_filename) XX(path_rename) +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) @@ -247,6 +261,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) @@ -256,10 +271,16 @@ 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(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)