sentry-ruby 5.3.1 → 5.16.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (76) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +11 -0
  3. data/.rspec +2 -0
  4. data/.yardopts +2 -0
  5. data/CHANGELOG.md +313 -0
  6. data/Gemfile +26 -0
  7. data/Makefile +4 -0
  8. data/README.md +11 -8
  9. data/Rakefile +20 -0
  10. data/bin/console +18 -0
  11. data/bin/setup +8 -0
  12. data/lib/sentry/background_worker.rb +79 -0
  13. data/lib/sentry/backpressure_monitor.rb +75 -0
  14. data/lib/sentry/backtrace.rb +124 -0
  15. data/lib/sentry/baggage.rb +70 -0
  16. data/lib/sentry/breadcrumb/sentry_logger.rb +90 -0
  17. data/lib/sentry/breadcrumb.rb +76 -0
  18. data/lib/sentry/breadcrumb_buffer.rb +64 -0
  19. data/lib/sentry/check_in_event.rb +60 -0
  20. data/lib/sentry/client.rb +248 -0
  21. data/lib/sentry/configuration.rb +650 -0
  22. data/lib/sentry/core_ext/object/deep_dup.rb +61 -0
  23. data/lib/sentry/core_ext/object/duplicable.rb +155 -0
  24. data/lib/sentry/cron/configuration.rb +23 -0
  25. data/lib/sentry/cron/monitor_check_ins.rb +75 -0
  26. data/lib/sentry/cron/monitor_config.rb +53 -0
  27. data/lib/sentry/cron/monitor_schedule.rb +42 -0
  28. data/lib/sentry/dsn.rb +53 -0
  29. data/lib/sentry/envelope.rb +93 -0
  30. data/lib/sentry/error_event.rb +38 -0
  31. data/lib/sentry/event.rb +156 -0
  32. data/lib/sentry/exceptions.rb +9 -0
  33. data/lib/sentry/hub.rb +316 -0
  34. data/lib/sentry/integrable.rb +32 -0
  35. data/lib/sentry/interface.rb +16 -0
  36. data/lib/sentry/interfaces/exception.rb +43 -0
  37. data/lib/sentry/interfaces/request.rb +134 -0
  38. data/lib/sentry/interfaces/single_exception.rb +67 -0
  39. data/lib/sentry/interfaces/stacktrace.rb +87 -0
  40. data/lib/sentry/interfaces/stacktrace_builder.rb +79 -0
  41. data/lib/sentry/interfaces/threads.rb +42 -0
  42. data/lib/sentry/linecache.rb +47 -0
  43. data/lib/sentry/logger.rb +20 -0
  44. data/lib/sentry/net/http.rb +106 -0
  45. data/lib/sentry/profiler.rb +233 -0
  46. data/lib/sentry/propagation_context.rb +134 -0
  47. data/lib/sentry/puma.rb +32 -0
  48. data/lib/sentry/rack/capture_exceptions.rb +79 -0
  49. data/lib/sentry/rack.rb +5 -0
  50. data/lib/sentry/rake.rb +28 -0
  51. data/lib/sentry/redis.rb +108 -0
  52. data/lib/sentry/release_detector.rb +39 -0
  53. data/lib/sentry/scope.rb +360 -0
  54. data/lib/sentry/session.rb +33 -0
  55. data/lib/sentry/session_flusher.rb +90 -0
  56. data/lib/sentry/span.rb +273 -0
  57. data/lib/sentry/test_helper.rb +84 -0
  58. data/lib/sentry/transaction.rb +359 -0
  59. data/lib/sentry/transaction_event.rb +80 -0
  60. data/lib/sentry/transport/configuration.rb +98 -0
  61. data/lib/sentry/transport/dummy_transport.rb +21 -0
  62. data/lib/sentry/transport/http_transport.rb +206 -0
  63. data/lib/sentry/transport/spotlight_transport.rb +50 -0
  64. data/lib/sentry/transport.rb +225 -0
  65. data/lib/sentry/utils/argument_checking_helper.rb +19 -0
  66. data/lib/sentry/utils/custom_inspection.rb +14 -0
  67. data/lib/sentry/utils/encoding_helper.rb +22 -0
  68. data/lib/sentry/utils/exception_cause_chain.rb +20 -0
  69. data/lib/sentry/utils/logging_helper.rb +26 -0
  70. data/lib/sentry/utils/real_ip.rb +84 -0
  71. data/lib/sentry/utils/request_id.rb +18 -0
  72. data/lib/sentry/version.rb +5 -0
  73. data/lib/sentry-ruby.rb +580 -0
  74. data/sentry-ruby-core.gemspec +23 -0
  75. data/sentry-ruby.gemspec +24 -0
  76. metadata +75 -16
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sentry
4
+ class StacktraceBuilder
5
+ # @return [String]
6
+ attr_reader :project_root
7
+
8
+ # @return [Regexp, nil]
9
+ attr_reader :app_dirs_pattern
10
+
11
+ # @return [LineCache]
12
+ attr_reader :linecache
13
+
14
+ # @return [Integer, nil]
15
+ attr_reader :context_lines
16
+
17
+ # @return [Proc, nil]
18
+ attr_reader :backtrace_cleanup_callback
19
+
20
+ # @param project_root [String]
21
+ # @param app_dirs_pattern [Regexp, nil]
22
+ # @param linecache [LineCache]
23
+ # @param context_lines [Integer, nil]
24
+ # @param backtrace_cleanup_callback [Proc, nil]
25
+ # @see Configuration#project_root
26
+ # @see Configuration#app_dirs_pattern
27
+ # @see Configuration#linecache
28
+ # @see Configuration#context_lines
29
+ # @see Configuration#backtrace_cleanup_callback
30
+ def initialize(project_root:, app_dirs_pattern:, linecache:, context_lines:, backtrace_cleanup_callback: nil)
31
+ @project_root = project_root
32
+ @app_dirs_pattern = app_dirs_pattern
33
+ @linecache = linecache
34
+ @context_lines = context_lines
35
+ @backtrace_cleanup_callback = backtrace_cleanup_callback
36
+ end
37
+
38
+ # Generates a StacktraceInterface with the given backtrace.
39
+ # You can pass a block to customize/exclude frames:
40
+ #
41
+ # @example
42
+ # builder.build(backtrace) do |frame|
43
+ # if frame.module.match?(/a_gem/)
44
+ # nil
45
+ # else
46
+ # frame
47
+ # end
48
+ # end
49
+ # @param backtrace [Array<String>]
50
+ # @param frame_callback [Proc]
51
+ # @yieldparam frame [StacktraceInterface::Frame]
52
+ # @return [StacktraceInterface]
53
+ def build(backtrace:, &frame_callback)
54
+ parsed_lines = parse_backtrace_lines(backtrace).select(&:file)
55
+
56
+ frames = parsed_lines.reverse.map do |line|
57
+ frame = convert_parsed_line_into_frame(line)
58
+ frame = frame_callback.call(frame) if frame_callback
59
+ frame
60
+ end.compact
61
+
62
+ StacktraceInterface.new(frames: frames)
63
+ end
64
+
65
+ private
66
+
67
+ def convert_parsed_line_into_frame(line)
68
+ frame = StacktraceInterface::Frame.new(project_root, line)
69
+ frame.set_context(linecache, context_lines) if context_lines
70
+ frame
71
+ end
72
+
73
+ def parse_backtrace_lines(backtrace)
74
+ Backtrace.parse(
75
+ backtrace, project_root, app_dirs_pattern, &backtrace_cleanup_callback
76
+ ).lines
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sentry
4
+ class ThreadsInterface
5
+ # @param crashed [Boolean]
6
+ # @param stacktrace [Array]
7
+ def initialize(crashed: false, stacktrace: nil)
8
+ @id = Thread.current.object_id
9
+ @name = Thread.current.name
10
+ @current = true
11
+ @crashed = crashed
12
+ @stacktrace = stacktrace
13
+ end
14
+
15
+ # @return [Hash]
16
+ def to_hash
17
+ {
18
+ values: [
19
+ {
20
+ id: @id,
21
+ name: @name,
22
+ crashed: @crashed,
23
+ current: @current,
24
+ stacktrace: @stacktrace&.to_hash
25
+ }
26
+ ]
27
+ }
28
+ end
29
+
30
+ # Builds the ThreadsInterface with given backtrace and stacktrace_builder.
31
+ # Patch this method if you want to change a threads interface's stacktrace frames.
32
+ # @see StacktraceBuilder.build
33
+ # @param backtrace [Array]
34
+ # @param stacktrace_builder [StacktraceBuilder]
35
+ # @param crashed [Hash]
36
+ # @return [ThreadsInterface]
37
+ def self.build(backtrace:, stacktrace_builder:, **options)
38
+ stacktrace = stacktrace_builder.build(backtrace: backtrace) if backtrace
39
+ new(**options, stacktrace: stacktrace)
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sentry
4
+ # @api private
5
+ class LineCache
6
+ def initialize
7
+ @cache = {}
8
+ end
9
+
10
+ # Any linecache you provide to Sentry must implement this method.
11
+ # Returns an Array of Strings representing the lines in the source
12
+ # file. The number of lines retrieved is (2 * context) + 1, the middle
13
+ # line should be the line requested by lineno. See specs for more information.
14
+ def get_file_context(filename, lineno, context)
15
+ return nil, nil, nil unless valid_path?(filename)
16
+
17
+ lines = Array.new(2 * context + 1) do |i|
18
+ getline(filename, lineno - context + i)
19
+ end
20
+ [lines[0..(context - 1)], lines[context], lines[(context + 1)..-1]]
21
+ end
22
+
23
+ private
24
+
25
+ def valid_path?(path)
26
+ lines = getlines(path)
27
+ !lines.nil?
28
+ end
29
+
30
+ def getlines(path)
31
+ @cache[path] ||= begin
32
+ IO.readlines(path)
33
+ rescue
34
+ nil
35
+ end
36
+ end
37
+
38
+ def getline(path, n)
39
+ return nil if n < 1
40
+
41
+ lines = getlines(path)
42
+ return nil if lines.nil?
43
+
44
+ lines[n - 1]
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'logger'
4
+
5
+ module Sentry
6
+ class Logger < ::Logger
7
+ LOG_PREFIX = "** [Sentry] "
8
+ PROGNAME = "sentry"
9
+
10
+ def initialize(*)
11
+ super
12
+ @level = ::Logger::INFO
13
+ original_formatter = ::Logger::Formatter.new
14
+ @default_formatter = proc do |severity, datetime, _progname, msg|
15
+ msg = "#{LOG_PREFIX}#{msg}"
16
+ original_formatter.call(severity, datetime, PROGNAME, msg)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "resolv"
5
+
6
+ module Sentry
7
+ # @api private
8
+ module Net
9
+ module HTTP
10
+ OP_NAME = "http.client"
11
+ BREADCRUMB_CATEGORY = "net.http"
12
+
13
+ # To explain how the entire thing works, we need to know how the original Net::HTTP#request works
14
+ # Here's part of its definition. As you can see, it usually calls itself inside a #start block
15
+ #
16
+ # ```
17
+ # def request(req, body = nil, &block)
18
+ # unless started?
19
+ # start {
20
+ # req['connection'] ||= 'close'
21
+ # return request(req, body, &block) # <- request will be called for the second time from the first call
22
+ # }
23
+ # end
24
+ # # .....
25
+ # end
26
+ # ```
27
+ #
28
+ # So we're only instrumenting request when `Net::HTTP` is already started
29
+ def request(req, body = nil, &block)
30
+ return super unless started? && Sentry.initialized?
31
+ return super if from_sentry_sdk?
32
+
33
+ Sentry.with_child_span(op: OP_NAME, start_timestamp: Sentry.utc_now.to_f) do |sentry_span|
34
+ request_info = extract_request_info(req)
35
+
36
+ if propagate_trace?(request_info[:url], Sentry.configuration)
37
+ set_propagation_headers(req)
38
+ end
39
+
40
+ super.tap do |res|
41
+ record_sentry_breadcrumb(request_info, res)
42
+
43
+ if sentry_span
44
+ sentry_span.set_description("#{request_info[:method]} #{request_info[:url]}")
45
+ sentry_span.set_data(Span::DataConventions::URL, request_info[:url])
46
+ sentry_span.set_data(Span::DataConventions::HTTP_METHOD, request_info[:method])
47
+ sentry_span.set_data(Span::DataConventions::HTTP_QUERY, request_info[:query]) if request_info[:query]
48
+ sentry_span.set_data(Span::DataConventions::HTTP_STATUS_CODE, res.code.to_i)
49
+ end
50
+ end
51
+ end
52
+ end
53
+
54
+ private
55
+
56
+ def set_propagation_headers(req)
57
+ Sentry.get_trace_propagation_headers&.each { |k, v| req[k] = v }
58
+ end
59
+
60
+ def record_sentry_breadcrumb(request_info, res)
61
+ return unless Sentry.initialized? && Sentry.configuration.breadcrumbs_logger.include?(:http_logger)
62
+
63
+ crumb = Sentry::Breadcrumb.new(
64
+ level: :info,
65
+ category: BREADCRUMB_CATEGORY,
66
+ type: :info,
67
+ data: {
68
+ status: res.code.to_i,
69
+ **request_info
70
+ }
71
+ )
72
+ Sentry.add_breadcrumb(crumb)
73
+ end
74
+
75
+ def from_sentry_sdk?
76
+ dsn = Sentry.configuration.dsn
77
+ dsn && dsn.host == self.address
78
+ end
79
+
80
+ def extract_request_info(req)
81
+ # IPv6 url could look like '::1/path', and that won't parse without
82
+ # wrapping it in square brackets.
83
+ hostname = address =~ Resolv::IPv6::Regex ? "[#{address}]" : address
84
+ uri = req.uri || URI.parse("#{use_ssl? ? 'https' : 'http'}://#{hostname}#{req.path}")
85
+ url = "#{uri.scheme}://#{uri.host}#{uri.path}" rescue uri.to_s
86
+
87
+ result = { method: req.method, url: url }
88
+
89
+ if Sentry.configuration.send_default_pii
90
+ result[:query] = uri.query
91
+ result[:body] = req.body
92
+ end
93
+
94
+ result
95
+ end
96
+
97
+ def propagate_trace?(url, configuration)
98
+ url &&
99
+ configuration.propagate_traces &&
100
+ configuration.trace_propagation_targets.any? { |target| url.match?(target) }
101
+ end
102
+ end
103
+ end
104
+ end
105
+
106
+ Sentry.register_patch(:http, Sentry::Net::HTTP, Net::HTTP)
@@ -0,0 +1,233 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+
5
+ module Sentry
6
+ class Profiler
7
+ VERSION = '1'
8
+ PLATFORM = 'ruby'
9
+ # 101 Hz in microseconds
10
+ DEFAULT_INTERVAL = 1e6 / 101
11
+ MICRO_TO_NANO_SECONDS = 1e3
12
+ MIN_SAMPLES_REQUIRED = 2
13
+
14
+ attr_reader :sampled, :started, :event_id
15
+
16
+ def initialize(configuration)
17
+ @event_id = SecureRandom.uuid.delete('-')
18
+ @started = false
19
+ @sampled = nil
20
+
21
+ @profiling_enabled = defined?(StackProf) && configuration.profiling_enabled?
22
+ @profiles_sample_rate = configuration.profiles_sample_rate
23
+ @project_root = configuration.project_root
24
+ @app_dirs_pattern = configuration.app_dirs_pattern || Backtrace::APP_DIRS_PATTERN
25
+ @in_app_pattern = Regexp.new("^(#{@project_root}/)?#{@app_dirs_pattern}")
26
+ end
27
+
28
+ def start
29
+ return unless @sampled
30
+
31
+ @started = StackProf.start(interval: DEFAULT_INTERVAL,
32
+ mode: :wall,
33
+ raw: true,
34
+ aggregate: false)
35
+
36
+ @started ? log('Started') : log('Not started since running elsewhere')
37
+ end
38
+
39
+ def stop
40
+ return unless @sampled
41
+ return unless @started
42
+
43
+ StackProf.stop
44
+ log('Stopped')
45
+ end
46
+
47
+ # Sets initial sampling decision of the profile.
48
+ # @return [void]
49
+ def set_initial_sample_decision(transaction_sampled)
50
+ unless @profiling_enabled
51
+ @sampled = false
52
+ return
53
+ end
54
+
55
+ unless transaction_sampled
56
+ @sampled = false
57
+ log('Discarding profile because transaction not sampled')
58
+ return
59
+ end
60
+
61
+ case @profiles_sample_rate
62
+ when 0.0
63
+ @sampled = false
64
+ log('Discarding profile because sample_rate is 0')
65
+ return
66
+ when 1.0
67
+ @sampled = true
68
+ return
69
+ else
70
+ @sampled = Random.rand < @profiles_sample_rate
71
+ end
72
+
73
+ log('Discarding profile due to sampling decision') unless @sampled
74
+ end
75
+
76
+ def to_hash
77
+ unless @sampled
78
+ record_lost_event(:sample_rate)
79
+ return {}
80
+ end
81
+
82
+ return {} unless @started
83
+
84
+ results = StackProf.results
85
+
86
+ if !results || results.empty? || results[:samples] == 0 || !results[:raw]
87
+ record_lost_event(:insufficient_data)
88
+ return {}
89
+ end
90
+
91
+ frame_map = {}
92
+
93
+ frames = results[:frames].to_enum.with_index.map do |frame, idx|
94
+ frame_id, frame_data = frame
95
+
96
+ # need to map over stackprof frame ids to ours
97
+ frame_map[frame_id] = idx
98
+
99
+ file_path = frame_data[:file]
100
+ in_app = in_app?(file_path)
101
+ filename = compute_filename(file_path, in_app)
102
+ function, mod = split_module(frame_data[:name])
103
+
104
+ frame_hash = {
105
+ abs_path: file_path,
106
+ function: function,
107
+ filename: filename,
108
+ in_app: in_app
109
+ }
110
+
111
+ frame_hash[:module] = mod if mod
112
+ frame_hash[:lineno] = frame_data[:line] if frame_data[:line] && frame_data[:line] >= 0
113
+
114
+ frame_hash
115
+ end
116
+
117
+ idx = 0
118
+ stacks = []
119
+ num_seen = []
120
+
121
+ # extract stacks from raw
122
+ # raw is a single array of [.., len_stack, *stack_frames(len_stack), num_stack_seen , ..]
123
+ while (len = results[:raw][idx])
124
+ idx += 1
125
+
126
+ # our call graph is reversed
127
+ stack = results[:raw].slice(idx, len).map { |id| frame_map[id] }.compact.reverse
128
+ stacks << stack
129
+
130
+ num_seen << results[:raw][idx + len]
131
+ idx += len + 1
132
+
133
+ log('Unknown frame in stack') if stack.size != len
134
+ end
135
+
136
+ idx = 0
137
+ elapsed_since_start_ns = 0
138
+ samples = []
139
+
140
+ num_seen.each_with_index do |n, i|
141
+ n.times do
142
+ # stackprof deltas are in microseconds
143
+ delta = results[:raw_timestamp_deltas][idx]
144
+ elapsed_since_start_ns += (delta * MICRO_TO_NANO_SECONDS).to_i
145
+ idx += 1
146
+
147
+ # Not sure why but some deltas are very small like 0/1 values,
148
+ # they pollute our flamegraph so just ignore them for now.
149
+ # Open issue at https://github.com/tmm1/stackprof/issues/201
150
+ next if delta < 10
151
+
152
+ samples << {
153
+ stack_id: i,
154
+ # TODO-neel-profiler we need to patch rb_profile_frames and write our own C extension to enable threading info.
155
+ # Till then, on multi-threaded servers like puma, we will get frames from other active threads when the one
156
+ # we're profiling is idle/sleeping/waiting for IO etc.
157
+ # https://bugs.ruby-lang.org/issues/10602
158
+ thread_id: '0',
159
+ elapsed_since_start_ns: elapsed_since_start_ns.to_s
160
+ }
161
+ end
162
+ end
163
+
164
+ log('Some samples thrown away') if samples.size != results[:samples]
165
+
166
+ if samples.size <= MIN_SAMPLES_REQUIRED
167
+ log('Not enough samples, discarding profiler')
168
+ record_lost_event(:insufficient_data)
169
+ return {}
170
+ end
171
+
172
+ profile = {
173
+ frames: frames,
174
+ stacks: stacks,
175
+ samples: samples
176
+ }
177
+
178
+ {
179
+ event_id: @event_id,
180
+ platform: PLATFORM,
181
+ version: VERSION,
182
+ profile: profile
183
+ }
184
+ end
185
+
186
+ private
187
+
188
+ def log(message)
189
+ Sentry.logger.debug(LOGGER_PROGNAME) { "[Profiler] #{message}" }
190
+ end
191
+
192
+ def in_app?(abs_path)
193
+ abs_path.match?(@in_app_pattern)
194
+ end
195
+
196
+ # copied from stacktrace.rb since I don't want to touch existing code
197
+ # TODO-neel-profiler try to fetch this from stackprof once we patch
198
+ # the native extension
199
+ def compute_filename(abs_path, in_app)
200
+ return nil if abs_path.nil?
201
+
202
+ under_project_root = @project_root && abs_path.start_with?(@project_root)
203
+
204
+ prefix =
205
+ if under_project_root && in_app
206
+ @project_root
207
+ else
208
+ longest_load_path = $LOAD_PATH.select { |path| abs_path.start_with?(path.to_s) }.max_by(&:size)
209
+
210
+ if under_project_root
211
+ longest_load_path || @project_root
212
+ else
213
+ longest_load_path
214
+ end
215
+ end
216
+
217
+ prefix ? abs_path[prefix.to_s.chomp(File::SEPARATOR).length + 1..-1] : abs_path
218
+ end
219
+
220
+ def split_module(name)
221
+ # last module plus class/instance method
222
+ i = name.rindex('::')
223
+ function = i ? name[(i + 2)..-1] : name
224
+ mod = i ? name[0...i] : nil
225
+
226
+ [function, mod]
227
+ end
228
+
229
+ def record_lost_event(reason)
230
+ Sentry.get_current_client&.transport&.record_lost_event(reason, 'profile')
231
+ end
232
+ end
233
+ end
@@ -0,0 +1,134 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+ require "sentry/baggage"
5
+
6
+ module Sentry
7
+ class PropagationContext
8
+ SENTRY_TRACE_REGEXP = Regexp.new(
9
+ "^[ \t]*" + # whitespace
10
+ "([0-9a-f]{32})?" + # trace_id
11
+ "-?([0-9a-f]{16})?" + # span_id
12
+ "-?([01])?" + # sampled
13
+ "[ \t]*$" # whitespace
14
+ )
15
+
16
+ # An uuid that can be used to identify a trace.
17
+ # @return [String]
18
+ attr_reader :trace_id
19
+ # An uuid that can be used to identify the span.
20
+ # @return [String]
21
+ attr_reader :span_id
22
+ # Span parent's span_id.
23
+ # @return [String, nil]
24
+ attr_reader :parent_span_id
25
+ # The sampling decision of the parent transaction.
26
+ # @return [Boolean, nil]
27
+ attr_reader :parent_sampled
28
+ # Is there an incoming trace or not?
29
+ # @return [Boolean]
30
+ attr_reader :incoming_trace
31
+ # This is only for accessing the current baggage variable.
32
+ # Please use the #get_baggage method for interfacing outside this class.
33
+ # @return [Baggage, nil]
34
+ attr_reader :baggage
35
+
36
+ def initialize(scope, env = nil)
37
+ @scope = scope
38
+ @parent_span_id = nil
39
+ @parent_sampled = nil
40
+ @baggage = nil
41
+ @incoming_trace = false
42
+
43
+ if env
44
+ sentry_trace_header = env["HTTP_SENTRY_TRACE"] || env[SENTRY_TRACE_HEADER_NAME]
45
+ baggage_header = env["HTTP_BAGGAGE"] || env[BAGGAGE_HEADER_NAME]
46
+
47
+ if sentry_trace_header
48
+ sentry_trace_data = self.class.extract_sentry_trace(sentry_trace_header)
49
+
50
+ if sentry_trace_data
51
+ @trace_id, @parent_span_id, @parent_sampled = sentry_trace_data
52
+
53
+ @baggage = if baggage_header && !baggage_header.empty?
54
+ Baggage.from_incoming_header(baggage_header)
55
+ else
56
+ # If there's an incoming sentry-trace but no incoming baggage header,
57
+ # for instance in traces coming from older SDKs,
58
+ # baggage will be empty and frozen and won't be populated as head SDK.
59
+ Baggage.new({})
60
+ end
61
+
62
+ @baggage.freeze!
63
+ @incoming_trace = true
64
+ end
65
+ end
66
+ end
67
+
68
+ @trace_id ||= SecureRandom.uuid.delete("-")
69
+ @span_id = SecureRandom.uuid.delete("-").slice(0, 16)
70
+ end
71
+
72
+ # Extract the trace_id, parent_span_id and parent_sampled values from a sentry-trace header.
73
+ #
74
+ # @param sentry_trace [String] the sentry-trace header value from the previous transaction.
75
+ # @return [Array, nil]
76
+ def self.extract_sentry_trace(sentry_trace)
77
+ match = SENTRY_TRACE_REGEXP.match(sentry_trace)
78
+ return nil if match.nil?
79
+
80
+ trace_id, parent_span_id, sampled_flag = match[1..3]
81
+ parent_sampled = sampled_flag.nil? ? nil : sampled_flag != "0"
82
+
83
+ [trace_id, parent_span_id, parent_sampled]
84
+ end
85
+
86
+ # Returns the trace context that can be used to embed in an Event.
87
+ # @return [Hash]
88
+ def get_trace_context
89
+ {
90
+ trace_id: trace_id,
91
+ span_id: span_id,
92
+ parent_span_id: parent_span_id
93
+ }
94
+ end
95
+
96
+ # Returns the sentry-trace header from the propagation context.
97
+ # @return [String]
98
+ def get_traceparent
99
+ "#{trace_id}-#{span_id}"
100
+ end
101
+
102
+ # Returns the Baggage from the propagation context or populates as head SDK if empty.
103
+ # @return [Baggage, nil]
104
+ def get_baggage
105
+ populate_head_baggage if @baggage.nil? || @baggage.mutable
106
+ @baggage
107
+ end
108
+
109
+ # Returns the Dynamic Sampling Context from the baggage.
110
+ # @return [String, nil]
111
+ def get_dynamic_sampling_context
112
+ get_baggage&.dynamic_sampling_context
113
+ end
114
+
115
+ private
116
+
117
+ def populate_head_baggage
118
+ return unless Sentry.initialized?
119
+
120
+ configuration = Sentry.configuration
121
+
122
+ items = {
123
+ "trace_id" => trace_id,
124
+ "environment" => configuration.environment,
125
+ "release" => configuration.release,
126
+ "public_key" => configuration.dsn&.public_key,
127
+ "user_segment" => @scope.user && @scope.user["segment"]
128
+ }
129
+
130
+ items.compact!
131
+ @baggage = Baggage.new(items, mutable: false)
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ return unless defined?(Puma::Server)
4
+
5
+ module Sentry
6
+ module Puma
7
+ module Server
8
+ PUMA_4_AND_PRIOR = Gem::Version.new(::Puma::Const::PUMA_VERSION) < Gem::Version.new("5.0.0")
9
+
10
+ def lowlevel_error(e, env, status=500)
11
+ result =
12
+ if PUMA_4_AND_PRIOR
13
+ super(e, env)
14
+ else
15
+ super
16
+ end
17
+
18
+ begin
19
+ Sentry.capture_exception(e) do |scope|
20
+ scope.set_rack_env(env)
21
+ end
22
+ rescue
23
+ # if anything happens, we don't want to break the app
24
+ end
25
+
26
+ result
27
+ end
28
+ end
29
+ end
30
+ end
31
+
32
+ Sentry.register_patch(:puma, Sentry::Puma::Server, Puma::Server)