diff --git a/CHANGELOG.md b/CHANGELOG.md index f318d44c2..0f5d18088 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - Expose `span_id` in `Span` constructor [#1945](https://github.com/getsentry/sentry-ruby/pull/1945) - Expose `end_timestamp` in `Span#finish` and `Transaction#finish` [#1946](https://github.com/getsentry/sentry-ruby/pull/1946) +- Add `Transaction#set_context` api [#1947](https://github.com/getsentry/sentry-ruby/pull/1947) - Add OpenTelemetry support with new `sentry-opentelemetry` gem - Add `config.instrumenter` to switch between `:sentry` and `:otel` instrumentation [#1944](https://github.com/getsentry/sentry-ruby/pull/1944) diff --git a/sentry-ruby/lib/sentry/transaction.rb b/sentry-ruby/lib/sentry/transaction.rb index e237b5a9b..55a4b9e81 100644 --- a/sentry-ruby/lib/sentry/transaction.rb +++ b/sentry-ruby/lib/sentry/transaction.rb @@ -50,6 +50,10 @@ class Transaction < Span # @return [Float, nil] attr_reader :effective_sample_rate + # Additional contexts stored directly on the transaction object. + # @return [Hash] + attr_reader :contexts + def initialize( hub:, name: nil, @@ -60,8 +64,7 @@ def initialize( ) super(transaction: self, **options) - @name = name - @source = SOURCES.include?(source) ? source.to_sym : :custom + set_name(name, source: source) @parent_sampled = parent_sampled @hub = hub @baggage = baggage @@ -74,6 +77,7 @@ def initialize( @environment = hub.configuration.environment @dsn = hub.configuration.dsn @effective_sample_rate = nil + @contexts = {} init_span_recorder end @@ -91,16 +95,10 @@ def self.from_sentry_trace(sentry_trace, baggage: nil, hub: Sentry.get_current_h return unless hub.configuration.tracing_enabled? return unless sentry_trace - match = SENTRY_TRACE_REGEXP.match(sentry_trace) - return if match.nil? - trace_id, parent_span_id, sampled_flag = match[1..3] + sentry_trace_data = extract_sentry_trace(sentry_trace) + return unless sentry_trace_data - parent_sampled = - if sampled_flag.nil? - nil - else - sampled_flag != "0" - end + trace_id, parent_span_id, parent_sampled = sentry_trace_data baggage = if baggage && !baggage.empty? Baggage.from_incoming_header(baggage) @@ -123,6 +121,20 @@ def self.from_sentry_trace(sentry_trace, baggage: nil, hub: Sentry.get_current_h ) end + # Extract the trace_id, parent_span_id and parent_sampled values from a sentry-trace header. + # + # @param sentry_trace [String] the sentry-trace header value from the previous transaction. + # @return [Array, nil] + def self.extract_sentry_trace(sentry_trace) + match = SENTRY_TRACE_REGEXP.match(sentry_trace) + return nil if match.nil? + + trace_id, parent_span_id, sampled_flag = match[1..3] + parent_sampled = sampled_flag.nil? ? nil : sampled_flag != "0" + + [trace_id, parent_span_id, parent_sampled] + end + # @return [Hash] def to_hash hash = super @@ -244,6 +256,24 @@ def get_baggage @baggage end + # Set the transaction name directly. + # Considered internal api since it bypasses the usual scope logic. + # @param name [String] + # @param source [Symbol] + # @return [void] + def set_name(name, source: :custom) + @name = name + @source = SOURCES.include?(source) ? source.to_sym : :custom + end + + # Set contexts directly on the transaction. + # @param key [String, Symbol] + # @param value [Object] + # @return [void] + def set_context(key, value) + @contexts[key] = value + end + protected def init_span_recorder(limit = 1000) diff --git a/sentry-ruby/lib/sentry/transaction_event.rb b/sentry-ruby/lib/sentry/transaction_event.rb index 06f89488f..389ca45b0 100644 --- a/sentry-ruby/lib/sentry/transaction_event.rb +++ b/sentry-ruby/lib/sentry/transaction_event.rb @@ -19,6 +19,7 @@ def initialize(transaction:, **options) self.transaction = transaction.name self.transaction_info = { source: transaction.source } + self.contexts.merge!(transaction.contexts) self.contexts.merge!(trace: transaction.get_trace_context) self.timestamp = transaction.timestamp self.start_timestamp = transaction.start_timestamp diff --git a/sentry-ruby/spec/sentry/client_spec.rb b/sentry-ruby/spec/sentry/client_spec.rb index a0ee904f8..0ae7edd63 100644 --- a/sentry-ruby/spec/sentry/client_spec.rb +++ b/sentry-ruby/spec/sentry/client_spec.rb @@ -160,6 +160,12 @@ def sentry_context "trace_id" => transaction.trace_id }) end + + it "adds explicitly added contexts to event" do + transaction.set_context(:foo, { bar: 42 }) + event = subject.event_from_transaction(transaction) + expect(event.contexts).to include({ foo: { bar: 42 } }) + end end describe "#event_from_exception" do diff --git a/sentry-ruby/spec/sentry/hub_spec.rb b/sentry-ruby/spec/sentry/hub_spec.rb index f73852a05..3b43000a2 100644 --- a/sentry-ruby/spec/sentry/hub_spec.rb +++ b/sentry-ruby/spec/sentry/hub_spec.rb @@ -241,6 +241,25 @@ end end + context "when event is a transaction" do + it "transaction.set_context merges and takes precedence over scope.set_context" do + scope.set_context(:foo, { val: 42 }) + scope.set_context(:bar, { val: 43 }) + + transaction = Sentry::Transaction.new(hub: subject, name: 'test') + transaction.set_context(:foo, { val: 44 }) + transaction.set_context(:baz, { val: 45 }) + + transaction_event = subject.current_client.event_from_transaction(transaction) + subject.capture_event(transaction_event) + + event = transport.events.last + expect(event.contexts[:foo]). to eq({ val: 44 }) + expect(event.contexts[:bar]). to eq({ val: 43 }) + expect(event.contexts[:baz]). to eq({ val: 45 }) + end + end + it_behaves_like "capture_helper" do let(:capture_helper) { :capture_event } let(:capture_subject) { event } diff --git a/sentry-ruby/spec/sentry/transaction_spec.rb b/sentry-ruby/spec/sentry/transaction_spec.rb index da7f54a68..665095aa1 100644 --- a/sentry-ruby/spec/sentry/transaction_spec.rb +++ b/sentry-ruby/spec/sentry/transaction_spec.rb @@ -559,4 +559,19 @@ end end end + + describe "#set_name" do + it "sets name and source directly" do + subject.set_name("bar", source: :url) + expect(subject.name).to eq("bar") + expect(subject.source).to eq(:url) + end + end + + describe "#set_context" do + it "sets arbitrary context" do + subject.set_context(:foo, { bar: 42 }) + expect(subject.contexts).to eq({ foo: { bar: 42 } }) + end + end end