sentry-ruby 5.3.1 → 5.16.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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)