diff --git a/CHANGELOG.md b/CHANGELOG.md index a3f379e72..d30d9a784 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ **Fixes**: +- Finish active trace on crash. ([#1667](https://github.com/getsentry/sentry-native/pull/1667)) - Native/macOS: fix module `image_size` computation, which could have caused the symbolicator to misattribute every frame to the lowest-addressed image (typically `dyld` or `libsystem`). ([#1740](https://github.com/getsentry/sentry-native/pull/1740)) - Native: raise `SENTRY_CRASH_MAX_MODULES` from `512` to `2048` so processes that load many shared libraries no longer have their minidump module list truncated, which left frames in unrecorded modules without a `debug_id` and unsymbolicatable. ([#1738](https://github.com/getsentry/sentry-native/pull/1738)) diff --git a/examples/example.c b/examples/example.c index 1c5ef292b..b665190e2 100644 --- a/examples/example.c +++ b/examples/example.c @@ -596,7 +596,8 @@ main(int argc, char **argv) options, sentry_transport_new(print_envelope)); } - if (has_arg(argc, argv, "capture-transaction")) { + if (has_arg(argc, argv, "capture-transaction") + || has_arg(argc, argv, "open-transaction")) { sentry_options_set_traces_sample_rate(options, 1.0); } @@ -1048,6 +1049,20 @@ main(int argc, char **argv) fflush(stdout); } + if (has_arg(argc, argv, "open-transaction")) { + // Leave a transaction + nested children unfinished; the crash + // auto-finalize should close them all. + sentry_transaction_context_t *otx_ctx + = sentry_transaction_context_new("open.tx", "op"); + sentry_transaction_t *otx + = sentry_transaction_start(otx_ctx, sentry_value_new_null()); + sentry_set_transaction_object(otx); + sentry_span_t *ospan + = sentry_transaction_start_child(otx, "open.span", NULL); + sentry_set_span( + sentry_span_start_child(ospan, "open.grand.span", NULL)); + } + if (has_arg(argc, argv, "crash")) { trigger_crash(); } diff --git a/src/backends/sentry_backend_breakpad.cpp b/src/backends/sentry_backend_breakpad.cpp index de10c55e1..730db0156 100644 --- a/src/backends/sentry_backend_breakpad.cpp +++ b/src/backends/sentry_backend_breakpad.cpp @@ -20,6 +20,7 @@ extern "C" { #include "sentry_session_replay.h" #include "sentry_string.h" #include "sentry_sync.h" +#include "sentry_tracing.h" #include "sentry_transport.h" #include "sentry_unix_pageallocator.h" #include "transports/sentry_disk_transport.h" @@ -139,6 +140,9 @@ breakpad_backend_callback(const google_breakpad::MinidumpDescriptor &descriptor, SENTRY_WITH_OPTIONS (options) { sentry__write_crash_marker(options); + sentry_value_t transaction + = sentry__trace_finish(SENTRY_SPAN_STATUS_ABORTED); + bool should_handle = true; if (options->on_crash_func) { @@ -240,12 +244,23 @@ breakpad_backend_callback(const google_breakpad::MinidumpDescriptor &descriptor, } if (!sentry__launch_external_crash_reporter(options, envelope)) { - // capture the envelope with the disk transport + // capture the envelopes with the disk transport sentry_transport_t *disk_transport = sentry_new_disk_transport(options->run); + if (!sentry_value_is_null(transaction)) { + sentry_envelope_t *tx_envelope + = sentry__prepare_transaction( + options, transaction, nullptr); + if (tx_envelope) { + sentry__capture_envelope( + disk_transport, tx_envelope, options); + } + } sentry__capture_envelope(disk_transport, envelope, options); sentry__transport_dump_queue(disk_transport, options->run); sentry_transport_free(disk_transport); + } else { + sentry_value_decref(transaction); } // now that the envelope was written, we can remove the temporary @@ -256,6 +271,7 @@ breakpad_backend_callback(const google_breakpad::MinidumpDescriptor &descriptor, SENTRY_SIGNAL_SAFE_LOG( "DEBUG event was discarded by the `on_crash` hook"); sentry_value_decref(event); + sentry_value_decref(transaction); } // after capturing the crash event, try to dump all the in-flight diff --git a/src/backends/sentry_backend_inproc.c b/src/backends/sentry_backend_inproc.c index f3f9e0e48..c71a8a775 100644 --- a/src/backends/sentry_backend_inproc.c +++ b/src/backends/sentry_backend_inproc.c @@ -18,6 +18,7 @@ #include "sentry_screenshot.h" #include "sentry_session_replay.h" #include "sentry_sync.h" +#include "sentry_tracing.h" #include "sentry_transport.h" #include "sentry_unix_pageallocator.h" #include "transports/sentry_disk_transport.h" @@ -1115,6 +1116,9 @@ process_ucontext_deferred(const sentry_ucontext_t *uctx, bool should_handle = true; sentry__write_crash_marker(options); + sentry_value_t transaction + = sentry__trace_finish(SENTRY_SPAN_STATUS_ABORTED); + if (options->on_crash_func && !skip_hooks) { SENTRY_DEBUG("invoking `on_crash` hook"); event = options->on_crash_func(uctx, event, options->on_crash_data); @@ -1144,9 +1148,6 @@ process_ucontext_deferred(const sentry_ucontext_t *uctx, sentry_envelope_t *envelope = sentry__prepare_event(options, event, NULL, !options->on_crash_func && !skip_hooks, NULL); - // TODO(tracing): Revisit when investigating transaction flushing - // during hard crashes. - sentry_session_t *session = sentry__end_current_session_with_status( SENTRY_SESSION_STATUS_CRASHED); sentry__envelope_add_session(envelope, session); @@ -1173,16 +1174,28 @@ process_ucontext_deferred(const sentry_ucontext_t *uctx, } if (!sentry__launch_external_crash_reporter(options, envelope)) { - // capture the envelope with the disk transport + // capture the envelopes with the disk transport sentry_transport_t *disk_transport = sentry_new_disk_transport(options->run); + if (!sentry_value_is_null(transaction)) { + sentry_envelope_t *tx_envelope + = sentry__prepare_transaction( + options, transaction, NULL); + if (tx_envelope) { + sentry__capture_envelope( + disk_transport, tx_envelope, options); + } + } sentry__capture_envelope(disk_transport, envelope, options); sentry__transport_dump_queue(disk_transport, options->run); sentry_transport_free(disk_transport); + } else { + sentry_value_decref(transaction); } } else { SENTRY_DEBUG("event was discarded by the `on_crash` hook"); sentry_value_decref(event); + sentry_value_decref(transaction); } // after capturing the crash event, dump all the envelopes to disk diff --git a/src/backends/sentry_backend_native.c b/src/backends/sentry_backend_native.c index fe1af9322..b186e46f6 100644 --- a/src/backends/sentry_backend_native.c +++ b/src/backends/sentry_backend_native.c @@ -37,6 +37,7 @@ #include "sentry_scope.h" #include "sentry_session.h" #include "sentry_sync.h" +#include "sentry_tracing.h" #include "sentry_transport.h" #include "transports/sentry_disk_transport.h" @@ -988,6 +989,9 @@ native_backend_except(sentry_backend_t *backend, const sentry_ucontext_t *uctx) // Write crash marker sentry__write_crash_marker(options); + sentry_value_t transaction + = sentry__trace_finish(SENTRY_SPAN_STATUS_ABORTED); + // Create crash event sentry_value_t event = sentry_value_new_event(); sentry_value_set_by_key( @@ -1092,26 +1096,33 @@ native_backend_except(sentry_backend_t *backend, const sentry_ucontext_t *uctx) = sentry__end_current_session_with_status( SENTRY_SESSION_STATUS_CRASHED); - if (session) { - sentry_envelope_t *envelope = sentry__envelope_new(); - if (envelope) { - sentry__envelope_add_session(envelope, session); - - // Write session envelope to disk - sentry_transport_t *disk_transport - = sentry_new_disk_transport(options->run); - if (disk_transport) { - // sentry__capture_envelope takes ownership of - // envelope - sentry__capture_envelope( - disk_transport, envelope, options); - sentry__transport_dump_queue( - disk_transport, options->run); - sentry_transport_free(disk_transport); - } else { - // Failed to create transport, free envelope - sentry_envelope_free(envelope); + if (session || !sentry_value_is_null(transaction)) { + sentry_transport_t *disk_transport + = sentry_new_disk_transport(options->run); + if (disk_transport) { + if (!sentry_value_is_null(transaction)) { + sentry_envelope_t *tx_envelope + = sentry__prepare_transaction( + options, transaction, NULL); + if (tx_envelope) { + sentry__capture_envelope( + disk_transport, tx_envelope, options); + } + } + if (session) { + sentry_envelope_t *envelope + = sentry__envelope_new(); + if (envelope) { + sentry__envelope_add_session(envelope, session); + sentry__capture_envelope( + disk_transport, envelope, options); + } } + sentry__transport_dump_queue( + disk_transport, options->run); + sentry_transport_free(disk_transport); + } else { + sentry_value_decref(transaction); } } @@ -1120,10 +1131,13 @@ native_backend_except(sentry_backend_t *backend, const sentry_ucontext_t *uctx) SENTRY_DEBUG("crash event and session written, daemon will " "create and send minidump"); + } else { + sentry_value_decref(transaction); } } else { SENTRY_DEBUG("event was discarded by the `on_crash` hook"); sentry_value_decref(event); + sentry_value_decref(transaction); } } } diff --git a/src/sentry_core.c b/src/sentry_core.c index 3019f7884..791020260 100644 --- a/src/sentry_core.c +++ b/src/sentry_core.c @@ -1329,6 +1329,20 @@ sentry_transaction_finish(sentry_transaction_t *opaque_tx) sentry_uuid_t sentry_transaction_finish_ts( sentry_transaction_t *opaque_tx, uint64_t timestamp) +{ + sentry_value_t tx = sentry__transaction_finish_value(opaque_tx, timestamp); + if (sentry_value_is_null(tx)) { + return sentry_uuid_nil(); + } + + // This takes ownership of the transaction, generates an event ID, merges + // scope + return sentry__capture_event(tx, NULL); +} + +sentry_value_t +sentry__transaction_finish_value( + sentry_transaction_t *opaque_tx, uint64_t timestamp) { if (!opaque_tx || sentry_value_is_null(opaque_tx->inner)) { SENTRY_WARN("no transaction available to finish"); @@ -1409,12 +1423,10 @@ sentry_transaction_finish_ts( sentry__transaction_decref(opaque_tx); - // This takes ownership of the transaction, generates an event ID, merges - // scope - return sentry__capture_event(tx, NULL); + return tx; fail: sentry__transaction_decref(opaque_tx); - return sentry_uuid_nil(); + return sentry_value_new_null(); } void @@ -1569,6 +1581,8 @@ sentry_span_finish_ts(sentry_span_t *opaque_span, uint64_t timestamp) goto fail; } + sentry__transaction_remove_child(opaque_root_transaction, opaque_span); + sentry_value_t root_transaction = opaque_root_transaction->inner; if (!sentry_value_is_true( diff --git a/src/sentry_core.h b/src/sentry_core.h index c0b6f60fb..ad3026e9b 100644 --- a/src/sentry_core.h +++ b/src/sentry_core.h @@ -89,6 +89,9 @@ sentry_uuid_t sentry__capture_event( sentry_envelope_t *sentry__prepare_transaction(const sentry_options_t *options, sentry_value_t transaction, sentry_uuid_t *event_id); +sentry_value_t sentry__transaction_finish_value( + sentry_transaction_t *opaque_tx, uint64_t timestamp); + /** * This function will submit the `envelope` to the given `transport`, first * checking for consent. diff --git a/src/sentry_sampling_context.h b/src/sentry_sampling_context.h index 59a069258..3d7ec5253 100644 --- a/src/sentry_sampling_context.h +++ b/src/sentry_sampling_context.h @@ -2,7 +2,8 @@ #ifndef SENTRY_SAMPLING_CONTEXT_H_INCLUDED #define SENTRY_SAMPLING_CONTEXT_H_INCLUDED -#include "sentry_tracing.h" +#include "sentry_boot.h" +#include "sentry_value.h" typedef struct sentry_sampling_context_s { sentry_transaction_context_t *transaction_context; diff --git a/src/sentry_scope.c b/src/sentry_scope.c index 48ede5524..1c055e8ba 100644 --- a/src/sentry_scope.c +++ b/src/sentry_scope.c @@ -436,13 +436,20 @@ sentry__scope_apply_to_event(const sentry_scope_t *scope, sentry__value_merge_objects(event_extra, scope->extra); } + bool is_transaction = sentry__event_is_transaction(event); sentry_value_t contexts = sentry__value_clone(scope->contexts); + if (is_transaction && !sentry_value_is_null(contexts)) { + sentry_value_remove_by_key(contexts, "trace"); + } // prep contexts sourced from scope; data about transaction on scope needs // to be extracted and inserted - sentry_value_t scoped_txn_or_span = get_span_or_transaction(scope); - sentry_value_t scope_trace - = sentry__value_get_trace_context(scoped_txn_or_span); + sentry_value_t scoped_txn_or_span = sentry_value_new_null(); + sentry_value_t scope_trace = sentry_value_new_null(); + if (!is_transaction) { + scoped_txn_or_span = get_span_or_transaction(scope); + scope_trace = sentry__value_get_trace_context(scoped_txn_or_span); + } if (!sentry_value_is_null(scope_trace)) { if (sentry_value_is_null(contexts)) { contexts = sentry_value_new_object(); @@ -461,7 +468,7 @@ sentry__scope_apply_to_event(const sentry_scope_t *scope, sentry_value_t event_contexts = sentry_value_get_by_key(event, "contexts"); if (sentry_value_is_null(event_contexts)) { // only merge in propagation context if there is no scoped span - if (sentry_value_is_null(scope_trace)) { + if (!is_transaction && sentry_value_is_null(scope_trace)) { sentry__value_merge_objects(contexts, scope->propagation_context); } PLACE_VALUE("contexts", contexts); diff --git a/src/sentry_tracing.c b/src/sentry_tracing.c index a5f69e1ce..8c32a97c9 100644 --- a/src/sentry_tracing.c +++ b/src/sentry_tracing.c @@ -452,6 +452,10 @@ sentry__transaction_new(sentry_value_t inner) } tx->inner = inner; + sentry__mutex_init(&tx->children_mutex); + tx->children = NULL; + tx->children_count = 0; + tx->children_cap = 0; return tx; } @@ -473,12 +477,30 @@ sentry__transaction_decref(sentry_transaction_t *tx) if (sentry_value_refcount(tx->inner) <= 1) { sentry_value_decref(tx->inner); + sentry_free(tx->children); + sentry__mutex_free(&tx->children_mutex); sentry_free(tx); } else { sentry_value_decref(tx->inner); } } +void +sentry__transaction_remove_child(sentry_transaction_t *tx, sentry_span_t *span) +{ + if (!tx || !span) { + return; + } + sentry__mutex_lock(&tx->children_mutex); + for (size_t i = 0; i < tx->children_count; i++) { + if (tx->children[i] == span) { + tx->children[i] = tx->children[--tx->children_count]; + break; + } + } + sentry__mutex_unlock(&tx->children_mutex); +} + void sentry__span_incref(sentry_span_t *span) { @@ -495,6 +517,7 @@ sentry__span_decref(sentry_span_t *span) } if (sentry_value_refcount(span->inner) <= 1) { + sentry__transaction_remove_child(span->transaction, span); sentry_value_decref(span->inner); sentry__transaction_decref(span->transaction); sentry_free(span); @@ -520,6 +543,28 @@ sentry__span_new(sentry_transaction_t *tx, sentry_value_t inner) sentry__transaction_incref(tx); span->transaction = tx; + sentry__mutex_lock(&tx->children_mutex); + if (tx->children_count == tx->children_cap) { + size_t new_cap = tx->children_cap ? tx->children_cap * 2 : 4; + sentry_span_t **new_children + = sentry_malloc(new_cap * sizeof(sentry_span_t *)); + if (new_children) { + if (tx->children) { + memcpy(new_children, tx->children, + tx->children_count * sizeof(sentry_span_t *)); + sentry_free(tx->children); + } + tx->children = new_children; + tx->children_cap = new_cap; + } else { + SENTRY_WARN("failed to track live span for crash auto-finalize"); + } + } + if (tx->children_count < tx->children_cap) { + tx->children[tx->children_count++] = span; + } + sentry__mutex_unlock(&tx->children_mutex); + return span; } @@ -967,3 +1012,95 @@ sentry_transaction_iter_headers(sentry_transaction_t *tx, sentry__span_iter_headers(tx->inner, callback, userdata); } } + +typedef struct { + sentry_span_t *saved_span; + sentry_transaction_t *saved_tx_obj; + sentry_transaction_t *active_tx; +} saved_trace_t; + +static saved_trace_t +save_active_trace(void) +{ + saved_trace_t s = { 0 }; + SENTRY_WITH_SCOPE (scope) { + if (scope->span) { + sentry__span_incref(scope->span); + s.saved_span = scope->span; + } + if (scope->transaction_object) { + sentry__transaction_incref(scope->transaction_object); + s.saved_tx_obj = scope->transaction_object; + } + } + s.active_tx = s.saved_span && s.saved_span->transaction + ? s.saved_span->transaction + : s.saved_tx_obj; + if (s.active_tx) { + sentry__transaction_incref(s.active_tx); + } + return s; +} + +static void +restore_active_trace(saved_trace_t *s) +{ + SENTRY_WITH_SCOPE_MUT (scope) { + if (!scope->span && s->saved_span) { + scope->span = s->saved_span; + s->saved_span = NULL; + } + if (!scope->transaction_object && s->saved_tx_obj) { + scope->transaction_object = s->saved_tx_obj; + s->saved_tx_obj = NULL; + } + } + sentry__span_decref(s->saved_span); + sentry__transaction_decref(s->saved_tx_obj); +} + +// Atomically swap the live-children list off `tx` and finish each span. +// The swap ensures `sentry_span_finish_ts`'s per-span remove-scan is a no-op. +static void +finish_children( + sentry_transaction_t *tx, sentry_span_status_t status, uint64_t end_ts) +{ + sentry__mutex_lock(&tx->children_mutex); + sentry_span_t **children = tx->children; + size_t count = tx->children_count; + tx->children = NULL; + tx->children_count = 0; + tx->children_cap = 0; + sentry__mutex_unlock(&tx->children_mutex); + + for (size_t i = count; i-- > 0;) { + sentry_span_t *child = children[i]; + sentry__span_incref(child); + sentry_span_set_status(child, status); + sentry_span_finish_ts(child, end_ts); + } + sentry_free(children); +} + +sentry_value_t +sentry__trace_finish(sentry_span_status_t status) +{ + // Save/restore scope around the finish so the crash event captured next + // still inherits the active trace context (cf. sentry-cocoa's + // `finishTracer:shouldCleanUp:NO`). Finished spans retain their ids; only + // `timestamp` is added. + saved_trace_t s = save_active_trace(); + if (!s.active_tx) { + restore_active_trace(&s); + return sentry_value_new_null(); + } + + uint64_t end_ts = sentry__usec_time(); + finish_children(s.active_tx, status, end_ts); + + sentry_transaction_set_status(s.active_tx, status); + sentry_value_t tx = sentry__transaction_finish_value(s.active_tx, end_ts); + + restore_active_trace(&s); + return tx; +} diff --git a/src/sentry_tracing.h b/src/sentry_tracing.h index d8910ba7f..5ba423d17 100644 --- a/src/sentry_tracing.h +++ b/src/sentry_tracing.h @@ -2,6 +2,7 @@ #define SENTRY_TRACING_H_INCLUDED #include "sentry_slice.h" +#include "sentry_sync.h" #include "sentry_value.h" // W3C traceparent header: 00--- @@ -33,6 +34,13 @@ struct sentry_transaction_context_s { */ struct sentry_transaction_s { sentry_value_t inner; + // Live (unfinished) child spans, so `sentry__trace_finish` can close them + // out on crash. Weak pointers: entries do not own a ref — spans remove + // themselves via `sentry__transaction_remove_child` on finish or decref. + sentry_mutex_t children_mutex; + sentry_span_t **children; + size_t children_count; + size_t children_cap; }; void sentry__transaction_context_free(sentry_transaction_context_t *tx_ctx); @@ -41,6 +49,22 @@ sentry_transaction_t *sentry__transaction_new(sentry_value_t inner); void sentry__transaction_incref(sentry_transaction_t *tx); void sentry__transaction_decref(sentry_transaction_t *tx); +/** + * Unlists `span` from the transaction's live-children list. No-op if not + * found. Does not decref (the list holds weak pointers). + */ +void sentry__transaction_remove_child( + sentry_transaction_t *tx, sentry_span_t *span); + +/** + * Finishes the active transaction (if any) with `status`, closing out every + * in-flight child span in leaf-first order and returning the tx payload. + * `scope->span` / `scope->transaction_object` are preserved so a + * subsequently-captured crash event still inherits the active trace context. + * Returns null if nothing is active. + */ +sentry_value_t sentry__trace_finish(sentry_span_status_t status); + void sentry__span_incref(sentry_span_t *span); void sentry__span_decref(sentry_span_t *span); diff --git a/tests/test_integration_http.py b/tests/test_integration_http.py index 0b3352665..a3b728717 100644 --- a/tests/test_integration_http.py +++ b/tests/test_integration_http.py @@ -854,6 +854,114 @@ def test_native_crash_http(cmake, httpserver): assert_attachment(envelope) +@pytest.mark.parametrize( + "backend", + [ + "inproc", + pytest.param( + "breakpad", + marks=pytest.mark.skipif( + not has_breakpad or is_qemu, reason="test needs breakpad backend" + ), + ), + pytest.param( + "native", + marks=pytest.mark.skipif( + not has_native or is_qemu or is_kcov, + reason="test needs native backend", + ), + ), + ], +) +def test_trace_finish_on_crash(cmake, httpserver, backend): + """The backend's crash handler calls `sentry__trace_finish`, so an + unfinished transaction on the scope ships alongside the crash.""" + tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": backend}) + + httpserver.expect_oneshot_request( + "/api/123456/envelope/", + headers={"x-sentry-auth": auth_header}, + ).respond_with_data("OK") + httpserver.expect_oneshot_request( + "/api/123456/envelope/", + headers={"x-sentry-auth": auth_header}, + ).respond_with_data("OK") + env = dict(os.environ, SENTRY_DSN=make_dsn(httpserver)) + + with httpserver.wait(timeout=10) as waiting: + run( + tmp_path, + "sentry_example", + ["log", "open-transaction", "crash"], + expect_failure=True, + env=env, + ) + if backend != "native": + # inproc/breakpad cache to disk; the next launch ships them. + run(tmp_path, "sentry_example", ["log", "no-setup"], env=env) + assert waiting.result + + envelopes = [Envelope.deserialize(req.get_data()) for req, _ in httpserver.log] + for envelope in envelopes: + item_types = [item.headers.get("type") for item in envelope.items] + assert not ("event" in item_types and "transaction" in item_types) + + tx_envelopes = [ + envelope + for envelope in envelopes + if any(item.headers.get("type") == "transaction" for item in envelope.items) + ] + assert tx_envelopes + tx_envelope = tx_envelopes[0] + tx_items = [ + item for item in tx_envelope.items if item.headers.get("type") == "transaction" + ] + assert tx_items + + tx = tx_items[0].payload.json + assert tx_envelope.headers["event_id"] == tx["event_id"] + tx_trace = tx["contexts"]["trace"] + assert tx_trace["status"] == "aborted" + assert "parent_span_id" not in tx_trace + tx_trace_header = tx_envelope.headers["trace"] + assert tx_trace_header["trace_id"] == tx_trace["trace_id"] + assert tx_trace_header["sampled"] == "true" + assert tx_trace_header["transaction"] == tx["transaction"] + spans = tx.get("spans", []) + child = next(s for s in spans if s.get("op") == "open.span") + grand = next(s for s in spans if s.get("op") == "open.grand.span") + assert child["parent_span_id"] == tx_trace["span_id"] + assert grand["parent_span_id"] == child["span_id"] + # Every in-flight child is finished, not just the deepest. + for op in ("open.span", "open.grand.span"): + span = next((s for s in spans if s.get("op") == op), None) + assert span is not None, f"missing {op} in {[s.get('op') for s in spans]}" + assert span.get("status") == "aborted" + assert span.get("timestamp") + + # The crash event nests under the deepest active span via matching + # trace_id + span_id. + event_envelopes = [ + envelope + for envelope in envelopes + if any(item.headers.get("type") == "event" for item in envelope.items) + ] + assert event_envelopes + event_envelope = event_envelopes[0] + event_items = [ + item for item in event_envelope.items if item.headers.get("type") == "event" + ] + assert event_items + if backend != "native": + event_trace_header = event_envelope.headers["trace"] + assert event_trace_header["trace_id"] == tx_trace["trace_id"] + assert event_trace_header["sampled"] == "true" + event = event_items[0].payload.json + assert event["contexts"]["trace"]["trace_id"] == tx_trace["trace_id"] + assert event["contexts"]["trace"]["span_id"] == grand["span_id"] + assert event.get("level") == "fatal" + + @pytest.mark.skipif(not has_files, reason="test needs a local filesystem") def test_http_retry_on_network_error(cmake, httpserver, unreachable_dsn): tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "inproc"}) diff --git a/tests/unit/test_tracing.c b/tests/unit/test_tracing.c index 9a32a0167..741a4c94f 100644 --- a/tests/unit/test_tracing.c +++ b/tests/unit/test_tracing.c @@ -1,5 +1,6 @@ #include "sentry_testsupport.h" +#include "sentry_core.h" #include "sentry_options.h" #include "sentry_scope.h" #include "sentry_string.h" @@ -778,6 +779,90 @@ check_spans(sentry_envelope_t *envelope, void *data) sentry_envelope_free(envelope); } +static void +count_envelope(sentry_envelope_t *envelope, void *data) +{ + uint64_t *called = data; + *called += 1; + sentry_envelope_free(envelope); +} + +SENTRY_TEST(trace_finish) +{ + // No active span/tx: no-op, no crash. + sentry_value_decref(sentry__trace_finish(SENTRY_SPAN_STATUS_ABORTED)); + + uint64_t called = 0; + SENTRY_TEST_OPTIONS_NEW(options); + sentry_options_set_dsn(options, "https://foo@sentry.invalid/42"); + sentry_options_set_auto_session_tracking(options, 0); + + sentry_transport_t *transport = sentry_transport_new(count_envelope); + sentry_transport_set_state(transport, &called); + sentry_options_set_transport(options, transport); + sentry_options_set_traces_sample_rate(options, 1.0); + sentry_init(options); + + sentry_transaction_context_t *ctx + = sentry_transaction_context_new("root", "op"); + sentry_transaction_t *tx + = sentry_transaction_start(ctx, sentry_value_new_null()); + sentry_transaction_set_data(tx, "tx-data", sentry_value_new_string("root")); + sentry_set_transaction_object(tx); + + sentry_span_t *child + = sentry_transaction_start_child(tx, "child-op", "child"); + sentry_span_t *grand = sentry_span_start_child(child, "grand-op", "grand"); + sentry_span_set_data(grand, "span-data", sentry_value_new_string("child")); + sentry_set_span(grand); + + sentry_value_t finished = sentry__trace_finish(SENTRY_SPAN_STATUS_ABORTED); + TEST_CHECK(!sentry_value_is_null(finished)); + CHECK_STRING_PROPERTY( + sentry_value_get_by_key( + sentry_value_get_by_key(finished, "contexts"), "trace"), + "status", "aborted"); + + sentry_value_t spans = sentry_value_get_by_key(finished, "spans"); + TEST_CHECK_INT_EQUAL(sentry_value_get_length(spans), 2); + for (size_t i = 0; i < sentry_value_get_length(spans); i++) { + sentry_value_t span = sentry_value_get_by_index(spans, i); + CHECK_STRING_PROPERTY(span, "status", "aborted"); + TEST_CHECK( + !sentry_value_is_null(sentry_value_get_by_key(span, "timestamp"))); + } + + sentry_envelope_t *envelope = NULL; + SENTRY_WITH_OPTIONS (runtime_options) { + envelope = sentry__prepare_transaction(runtime_options, finished, NULL); + } + TEST_ASSERT(envelope != NULL); + + sentry_value_t prepared = sentry_envelope_get_transaction(envelope); + sentry_value_t prepared_trace = sentry_value_get_by_key( + sentry_value_get_by_key(prepared, "contexts"), "trace"); + CHECK_STRING_PROPERTY(prepared_trace, "status", "aborted"); + TEST_CHECK(IS_NULL(prepared_trace, "parent_span_id")); + + sentry_value_t trace_data = sentry_value_get_by_key(prepared_trace, "data"); + CHECK_STRING_PROPERTY(trace_data, "tx-data", "root"); + TEST_CHECK(IS_NULL(trace_data, "span-data")); + sentry_envelope_free(envelope); + + // Scope still points at the (finished) span so a subsequent crash event + // inherits its trace context. + SENTRY_WITH_SCOPE (scope) { + TEST_CHECK(scope->span != NULL); + } + + sentry__span_decref(grand); + sentry__span_decref(child); + sentry__transaction_decref(tx); + + sentry_close(); + TEST_CHECK_INT_EQUAL(called, 0); +} + SENTRY_TEST(drop_unfinished_spans) { uint64_t called_transport = 0; diff --git a/tests/unit/tests.inc b/tests/unit/tests.inc index d673a13e9..b3099dab0 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -308,6 +308,7 @@ XX(symbolizer) XX(task_queue) XX(thread_without_name_still_valid) XX(trace_continuation_truth_table) +XX(trace_finish) XX(traceparent_header_disabled_by_default) XX(traceparent_header_generation) XX(transaction_name_backfill_on_finish)