File: transaction.rb

package info (click to toggle)
ruby-sentry-ruby-core 5.3.0-2
  • links: PTS, VCS
  • area: main
  • in suites: bookworm, forky, sid, trixie
  • size: 412 kB
  • sloc: ruby: 3,030; makefile: 8; sh: 4
file content (206 lines) | stat: -rw-r--r-- 6,241 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
# frozen_string_literal: true

module Sentry
  class Transaction < Span
    SENTRY_TRACE_REGEXP = Regexp.new(
      "^[ \t]*" +  # whitespace
      "([0-9a-f]{32})?" +  # trace_id
      "-?([0-9a-f]{16})?" +  # span_id
      "-?([01])?" +  # sampled
      "[ \t]*$"  # whitespace
    )
    UNLABELD_NAME = "<unlabeled transaction>".freeze
    MESSAGE_PREFIX = "[Tracing]"

    include LoggingHelper

    # The name of the transaction.
    # @return [String]
    attr_reader :name

    # The sampling decision of the parent transaction, which will be considered when making the current transaction's sampling decision.
    # @return [String]
    attr_reader :parent_sampled

    # @deprecated Use Sentry.get_current_hub instead.
    attr_reader :hub

    # @deprecated Use Sentry.configuration instead.
    attr_reader :configuration

    # @deprecated Use Sentry.logger instead.
    attr_reader :logger

    def initialize(name: nil, parent_sampled: nil, hub:, **options)
      super(**options)

      @name = name
      @parent_sampled = parent_sampled
      @transaction = self
      @hub = hub
      @configuration = hub.configuration # to be removed
      @tracing_enabled = hub.configuration.tracing_enabled?
      @traces_sampler = hub.configuration.traces_sampler
      @traces_sample_rate = hub.configuration.traces_sample_rate
      @logger = hub.configuration.logger
      init_span_recorder
    end

    # Initalizes a Transaction instance with a Sentry trace string from another transaction (usually from an external request).
    #
    # The original transaction will become the parent of the new Transaction instance. And they will share the same `trace_id`.
    #
    # The child transaction will also store the parent's sampling decision in its `parent_sampled` attribute.
    # @param sentry_trace [String] the trace string from the previous transaction.
    # @param hub [Hub] the hub that'll be responsible for sending this transaction when it's finished.
    # @param options [Hash] the options you want to use to initialize a Transaction instance.
    # @return [Transaction, nil]
    def self.from_sentry_trace(sentry_trace, hub: Sentry.get_current_hub, **options)
      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]

      parent_sampled =
        if sampled_flag.nil?
          nil
        else
          sampled_flag != "0"
        end

      new(trace_id: trace_id, parent_span_id: parent_span_id, parent_sampled: parent_sampled, hub: hub, **options)
    end

    # @return [Hash]
    def to_hash
      hash = super
      hash.merge!(name: @name, sampled: @sampled, parent_sampled: @parent_sampled)
      hash
    end

    # @return [Transaction]
    def deep_dup
      copy = super
      copy.init_span_recorder(@span_recorder.max_length)

      @span_recorder.spans.each do |span|
        # span_recorder's first span is the current span, which should not be added to the copy's spans
        next if span == self
        copy.span_recorder.add(span.dup)
      end

      copy
    end

    # Sets initial sampling decision of the transaction.
    # @param sampling_context [Hash] a context Hash that'll be passed to `traces_sampler` (if provided).
    # @return [void]
    def set_initial_sample_decision(sampling_context:)
      unless @tracing_enabled
        @sampled = false
        return
      end

      return unless @sampled.nil?

      sample_rate =
        if @traces_sampler.is_a?(Proc)
          @traces_sampler.call(sampling_context)
        elsif !sampling_context[:parent_sampled].nil?
          sampling_context[:parent_sampled]
        else
          @traces_sample_rate
        end

      transaction_description = generate_transaction_description

      unless [true, false].include?(sample_rate) || (sample_rate.is_a?(Numeric) && sample_rate >= 0.0 && sample_rate <= 1.0)
        @sampled = false
        log_warn("#{MESSAGE_PREFIX} Discarding #{transaction_description} because of invalid sample_rate: #{sample_rate}")
        return
      end

      if sample_rate == 0.0 || sample_rate == false
        @sampled = false
        log_debug("#{MESSAGE_PREFIX} Discarding #{transaction_description} because traces_sampler returned 0 or false")
        return
      end

      if sample_rate == true
        @sampled = true
      else
        @sampled = Random.rand < sample_rate
      end

      if @sampled
        log_debug("#{MESSAGE_PREFIX} Starting #{transaction_description}")
      else
        log_debug(
          "#{MESSAGE_PREFIX} Discarding #{transaction_description} because it's not included in the random sample (sampling rate = #{sample_rate})"
        )
      end
    end

    # Finishes the transaction's recording and send it to Sentry.
    # @param hub [Hub] the hub that'll send this transaction. (Deprecated)
    # @return [TransactionEvent]
    def finish(hub: nil)
      if hub
        log_warn(
          <<~MSG
            Specifying a different hub in `Transaction#finish` will be deprecated in version 5.0.
            Please use `Hub#start_transaction` with the designated hub.
          MSG
        )
      end

      hub ||= @hub

      super() # Span#finish doesn't take arguments

      if @name.nil?
        @name = UNLABELD_NAME
      end

      if @sampled
        event = hub.current_client.event_from_transaction(self)
        hub.capture_event(event)
      else
        hub.current_client.transport.record_lost_event(:sample_rate, 'transaction')
      end
    end

    protected

    def init_span_recorder(limit = 1000)
      @span_recorder = SpanRecorder.new(limit)
      @span_recorder.add(self)
    end

    private

    def generate_transaction_description
      result = op.nil? ? "" : "<#{@op}> "
      result += "transaction"
      result += " <#{@name}>" if @name
      result
    end

    class SpanRecorder
      attr_reader :max_length, :spans

      def initialize(max_length)
        @max_length = max_length
        @spans = []
      end

      def add(span)
        if @spans.count < @max_length
          @spans << span
        end
      end
    end
  end
end