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
|