From 483eebef7e9c017a5e450762e04e9cb9eb8788f7 Mon Sep 17 00:00:00 2001 From: Rayeed Zaman Date: Sat, 14 Nov 2026 17:04:54 -0500 Subject: [PATCH] fix(tracing): finish child spans when blocks unwind Finish child spans in ensure so nonlocal return, break, throw, and exceptions outside StandardError cannot leave unfinished spans that disappear from transaction events. Preserve block control flow, exception propagation, and the direct Span return value. Co-Authored-By: GPT-6 --- sentry-ruby/lib/sentry/span.rb | 15 ++++++---- sentry-ruby/spec/sentry_spec.rb | 51 +++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 6 deletions(-) diff --git a/sentry-ruby/lib/sentry/span.rb b/sentry-ruby/lib/sentry/span.rb index 9b0c418d6..034e834b3 100644 --- a/sentry-ruby/lib/sentry/span.rb +++ b/sentry-ruby/lib/sentry/span.rb @@ -240,13 +240,16 @@ def start_child(**attributes) def with_child_span(**attributes, &block) child_span = start_child(**attributes) - yield(child_span) + begin + yield(child_span) + rescue + child_span.set_http_status(500) + raise + ensure + child_span.finish + end - child_span.finish - rescue - child_span.set_http_status(500) - child_span.finish - raise + child_span end def deep_dup diff --git a/sentry-ruby/spec/sentry_spec.rb b/sentry-ruby/spec/sentry_spec.rb index 77430216b..b70e00638 100644 --- a/sentry-ruby/spec/sentry_spec.rb +++ b/sentry-ruby/spec/sentry_spec.rb @@ -784,6 +784,57 @@ end end + context "when the block exits early" do + let(:transaction) { described_class.start_transaction(name: "request", sampled: true) } + + before do + perform_basic_setup { |config| config.traces_sample_rate = 1.0 } + described_class.get_current_scope.set_span(transaction) + end + + def return_from_child_span + Sentry.with_child_span(op: "child") { return :returned } + end + + def expect_finished_child_span + expect(Sentry.get_current_scope.get_span).to equal(transaction) + transaction.finish + expect(last_sentry_event.spans.size).to eq(1) + expect(last_sentry_event.spans.first).to include(op: "child", timestamp: be_a(Float)) + end + + it "records the span when the block breaks" do + result = described_class.with_child_span(op: "child") { break :returned } + + expect(result).to eq(:returned) + expect_finished_child_span + end + + it "records the span when the block returns from its enclosing method" do + expect(return_from_child_span).to eq(:returned) + expect_finished_child_span + end + + it "records the span when the block throws" do + result = catch(:finished) do + described_class.with_child_span(op: "child") { throw :finished, :returned } + end + + expect(result).to eq(:returned) + expect_finished_child_span + end + + it "finishes the span and propagates exceptions outside StandardError" do + exception = Class.new(Exception).new("stop") + + expect do + described_class.with_child_span(op: "child") { raise exception } + end.to raise_error { |raised| expect(raised).to equal(exception) } + + expect_finished_child_span + end + end + context "when the current span is present" do let(:parent_span) do transaction = described_class.start_transaction(op: "foo")