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,206 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "zlib"
5
+
6
+ module Sentry
7
+ class HTTPTransport < Transport
8
+ GZIP_ENCODING = "gzip"
9
+ GZIP_THRESHOLD = 1024 * 30
10
+ CONTENT_TYPE = 'application/x-sentry-envelope'
11
+
12
+ DEFAULT_DELAY = 60
13
+ RETRY_AFTER_HEADER = "retry-after"
14
+ RATE_LIMIT_HEADER = "x-sentry-rate-limits"
15
+ USER_AGENT = "sentry-ruby/#{Sentry::VERSION}"
16
+
17
+ # The list of errors ::Net::HTTP is known to raise
18
+ # See https://github.com/ruby/ruby/blob/b0c639f249165d759596f9579fa985cb30533de6/lib/bundler/fetcher.rb#L281-L286
19
+ HTTP_ERRORS = [
20
+ Timeout::Error, EOFError, SocketError, Errno::ENETDOWN, Errno::ENETUNREACH,
21
+ Errno::EINVAL, Errno::ECONNRESET, Errno::ETIMEDOUT, Errno::EAGAIN,
22
+ Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError, Net::ProtocolError,
23
+ Zlib::BufError, Errno::EHOSTUNREACH, Errno::ECONNREFUSED
24
+ ].freeze
25
+
26
+
27
+ def initialize(*args)
28
+ super
29
+ log_debug("Sentry HTTP Transport will connect to #{@dsn.server}") if @dsn
30
+ end
31
+
32
+ def send_data(data)
33
+ encoding = ""
34
+
35
+ if should_compress?(data)
36
+ data = Zlib.gzip(data)
37
+ encoding = GZIP_ENCODING
38
+ end
39
+
40
+ headers = {
41
+ 'Content-Type' => CONTENT_TYPE,
42
+ 'Content-Encoding' => encoding,
43
+ 'User-Agent' => USER_AGENT
44
+ }
45
+
46
+ auth_header = generate_auth_header
47
+ headers['X-Sentry-Auth'] = auth_header if auth_header
48
+
49
+ response = conn.start do |http|
50
+ request = ::Net::HTTP::Post.new(endpoint, headers)
51
+ request.body = data
52
+ http.request(request)
53
+ end
54
+
55
+ if response.code.match?(/\A2\d{2}/)
56
+ handle_rate_limited_response(response) if has_rate_limited_header?(response)
57
+ elsif response.code == "429"
58
+ log_debug("the server responded with status 429")
59
+ handle_rate_limited_response(response)
60
+ else
61
+ error_info = "the server responded with status #{response.code}"
62
+ error_info += "\nbody: #{response.body}"
63
+ error_info += " Error in headers is: #{response['x-sentry-error']}" if response['x-sentry-error']
64
+
65
+ raise Sentry::ExternalError, error_info
66
+ end
67
+ rescue SocketError, *HTTP_ERRORS => e
68
+ on_error if respond_to?(:on_error)
69
+ raise Sentry::ExternalError.new(e&.message)
70
+ end
71
+
72
+ def endpoint
73
+ @dsn.envelope_endpoint
74
+ end
75
+
76
+ def generate_auth_header
77
+ return nil unless @dsn
78
+
79
+ now = Sentry.utc_now.to_i
80
+ fields = {
81
+ 'sentry_version' => PROTOCOL_VERSION,
82
+ 'sentry_client' => USER_AGENT,
83
+ 'sentry_timestamp' => now,
84
+ 'sentry_key' => @dsn.public_key
85
+ }
86
+ fields['sentry_secret'] = @dsn.secret_key if @dsn.secret_key
87
+ 'Sentry ' + fields.map { |key, value| "#{key}=#{value}" }.join(', ')
88
+ end
89
+
90
+ def conn
91
+ server = URI(@dsn.server)
92
+
93
+ # connection respects proxy setting from @transport_configuration, or environment variables (HTTP_PROXY, HTTPS_PROXY, NO_PROXY)
94
+ # Net::HTTP will automatically read the env vars.
95
+ # See https://ruby-doc.org/3.2.2/stdlibs/net/Net/HTTP.html#class-Net::HTTP-label-Proxies
96
+ connection =
97
+ if proxy = normalize_proxy(@transport_configuration.proxy)
98
+ ::Net::HTTP.new(server.hostname, server.port, proxy[:uri].hostname, proxy[:uri].port, proxy[:user], proxy[:password])
99
+ else
100
+ ::Net::HTTP.new(server.hostname, server.port)
101
+ end
102
+
103
+ connection.use_ssl = server.scheme == "https"
104
+ connection.read_timeout = @transport_configuration.timeout
105
+ connection.write_timeout = @transport_configuration.timeout if connection.respond_to?(:write_timeout)
106
+ connection.open_timeout = @transport_configuration.open_timeout
107
+
108
+ ssl_configuration.each do |key, value|
109
+ connection.send("#{key}=", value)
110
+ end
111
+
112
+ connection
113
+ end
114
+
115
+ private
116
+
117
+ def has_rate_limited_header?(headers)
118
+ headers[RETRY_AFTER_HEADER] || headers[RATE_LIMIT_HEADER]
119
+ end
120
+
121
+ def handle_rate_limited_response(headers)
122
+ rate_limits =
123
+ if rate_limits = headers[RATE_LIMIT_HEADER]
124
+ parse_rate_limit_header(rate_limits)
125
+ elsif retry_after = headers[RETRY_AFTER_HEADER]
126
+ # although Sentry doesn't send a date string back
127
+ # based on HTTP specification, this could be a date string (instead of an integer)
128
+ retry_after = retry_after.to_i
129
+ retry_after = DEFAULT_DELAY if retry_after == 0
130
+
131
+ { nil => Time.now + retry_after }
132
+ else
133
+ { nil => Time.now + DEFAULT_DELAY }
134
+ end
135
+
136
+ rate_limits.each do |category, limit|
137
+ if current_limit = @rate_limits[category]
138
+ if current_limit < limit
139
+ @rate_limits[category] = limit
140
+ end
141
+ else
142
+ @rate_limits[category] = limit
143
+ end
144
+ end
145
+ end
146
+
147
+ def parse_rate_limit_header(rate_limit_header)
148
+ time = Time.now
149
+
150
+ result = {}
151
+
152
+ limits = rate_limit_header.split(",")
153
+ limits.each do |limit|
154
+ next if limit.nil? || limit.empty?
155
+
156
+ begin
157
+ retry_after, categories = limit.strip.split(":").first(2)
158
+ retry_after = time + retry_after.to_i
159
+ categories = categories.split(";")
160
+
161
+ if categories.empty?
162
+ result[nil] = retry_after
163
+ else
164
+ categories.each do |category|
165
+ result[category] = retry_after
166
+ end
167
+ end
168
+ rescue StandardError
169
+ end
170
+ end
171
+
172
+ result
173
+ end
174
+
175
+ def should_compress?(data)
176
+ @transport_configuration.encoding == GZIP_ENCODING && data.bytesize >= GZIP_THRESHOLD
177
+ end
178
+
179
+ # @param proxy [String, URI, Hash] Proxy config value passed into `config.transport`.
180
+ # Accepts either a URI formatted string, URI, or a hash with the `uri`, `user`, and `password` keys.
181
+ # @return [Hash] Normalized proxy config that will be passed into `Net::HTTP`
182
+ def normalize_proxy(proxy)
183
+ return proxy unless proxy
184
+
185
+ case proxy
186
+ when String
187
+ uri = URI(proxy)
188
+ { uri: uri, user: uri.user, password: uri.password }
189
+ when URI
190
+ { uri: proxy, user: proxy.user, password: proxy.password }
191
+ when Hash
192
+ proxy
193
+ end
194
+ end
195
+
196
+ def ssl_configuration
197
+ configuration = {
198
+ verify: @transport_configuration.ssl_verification,
199
+ ca_file: @transport_configuration.ssl_ca_file
200
+ }.merge(@transport_configuration.ssl || {})
201
+
202
+ configuration[:verify_mode] = configuration.delete(:verify) ? OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE
203
+ configuration
204
+ end
205
+ end
206
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "zlib"
5
+
6
+ module Sentry
7
+ # Designed to just report events to Spotlight in development.
8
+ class SpotlightTransport < HTTPTransport
9
+ DEFAULT_SIDECAR_URL = "http://localhost:8969/stream"
10
+ MAX_FAILED_REQUESTS = 3
11
+
12
+ def initialize(configuration)
13
+ super
14
+ @sidecar_url = configuration.spotlight.is_a?(String) ? configuration.spotlight : DEFAULT_SIDECAR_URL
15
+ @failed = 0
16
+ @logged = false
17
+
18
+ log_debug("[Spotlight] initialized for url #{@sidecar_url}")
19
+ end
20
+
21
+ def endpoint
22
+ "/stream"
23
+ end
24
+
25
+ def send_data(data)
26
+ if @failed >= MAX_FAILED_REQUESTS
27
+ unless @logged
28
+ log_debug("[Spotlight] disabling because of too many request failures")
29
+ @logged = true
30
+ end
31
+
32
+ return
33
+ end
34
+
35
+ super
36
+ end
37
+
38
+ def on_error
39
+ @failed += 1
40
+ end
41
+
42
+ # Similar to HTTPTransport connection, but does not support Proxy and SSL
43
+ def conn
44
+ sidecar = URI(@sidecar_url)
45
+ connection = ::Net::HTTP.new(sidecar.hostname, sidecar.port, nil)
46
+ connection.use_ssl = false
47
+ connection
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,225 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "base64"
5
+ require "sentry/envelope"
6
+
7
+ module Sentry
8
+ class Transport
9
+ PROTOCOL_VERSION = '7'
10
+ USER_AGENT = "sentry-ruby/#{Sentry::VERSION}"
11
+ CLIENT_REPORT_INTERVAL = 30
12
+
13
+ # https://develop.sentry.dev/sdk/client-reports/#envelope-item-payload
14
+ CLIENT_REPORT_REASONS = [
15
+ :ratelimit_backoff,
16
+ :queue_overflow,
17
+ :cache_overflow, # NA
18
+ :network_error,
19
+ :sample_rate,
20
+ :before_send,
21
+ :event_processor,
22
+ :insufficient_data,
23
+ :backpressure
24
+ ]
25
+
26
+ include LoggingHelper
27
+
28
+ attr_reader :rate_limits, :discarded_events, :last_client_report_sent
29
+
30
+ # @deprecated Use Sentry.logger to retrieve the current logger instead.
31
+ attr_reader :logger
32
+
33
+ def initialize(configuration)
34
+ @logger = configuration.logger
35
+ @transport_configuration = configuration.transport
36
+ @dsn = configuration.dsn
37
+ @rate_limits = {}
38
+ @send_client_reports = configuration.send_client_reports
39
+
40
+ if @send_client_reports
41
+ @discarded_events = Hash.new(0)
42
+ @last_client_report_sent = Time.now
43
+ end
44
+ end
45
+
46
+ def send_data(data, options = {})
47
+ raise NotImplementedError
48
+ end
49
+
50
+ def send_event(event)
51
+ envelope = envelope_from_event(event)
52
+ send_envelope(envelope)
53
+
54
+ event
55
+ end
56
+
57
+ def send_envelope(envelope)
58
+ reject_rate_limited_items(envelope)
59
+
60
+ return if envelope.items.empty?
61
+
62
+ data, serialized_items = serialize_envelope(envelope)
63
+
64
+ if data
65
+ log_info("[Transport] Sending envelope with items [#{serialized_items.map(&:type).join(', ')}] #{envelope.event_id} to Sentry")
66
+ send_data(data)
67
+ end
68
+ end
69
+
70
+ def serialize_envelope(envelope)
71
+ serialized_items = []
72
+ serialized_results = []
73
+
74
+ envelope.items.each do |item|
75
+ result, oversized = item.serialize
76
+
77
+ if oversized
78
+ log_debug("Envelope item [#{item.type}] is still oversized after size reduction: {#{item.size_breakdown}}")
79
+
80
+ next
81
+ end
82
+
83
+ serialized_results << result
84
+ serialized_items << item
85
+ end
86
+
87
+ data = [JSON.generate(envelope.headers), *serialized_results].join("\n") unless serialized_results.empty?
88
+
89
+ [data, serialized_items]
90
+ end
91
+
92
+ def is_rate_limited?(item_type)
93
+ # check category-specific limit
94
+ category_delay =
95
+ case item_type
96
+ when "transaction"
97
+ @rate_limits["transaction"]
98
+ when "sessions"
99
+ @rate_limits["session"]
100
+ else
101
+ @rate_limits["error"]
102
+ end
103
+
104
+ # check universal limit if not category limit
105
+ universal_delay = @rate_limits[nil]
106
+
107
+ delay =
108
+ if category_delay && universal_delay
109
+ if category_delay > universal_delay
110
+ category_delay
111
+ else
112
+ universal_delay
113
+ end
114
+ elsif category_delay
115
+ category_delay
116
+ else
117
+ universal_delay
118
+ end
119
+
120
+ !!delay && delay > Time.now
121
+ end
122
+
123
+ def any_rate_limited?
124
+ @rate_limits.values.any? { |t| t && t > Time.now }
125
+ end
126
+
127
+ def envelope_from_event(event)
128
+ # Convert to hash
129
+ event_payload = event.to_hash
130
+ event_id = event_payload[:event_id] || event_payload["event_id"]
131
+ item_type = event_payload[:type] || event_payload["type"]
132
+
133
+ envelope_headers = {
134
+ event_id: event_id,
135
+ dsn: @dsn.to_s,
136
+ sdk: Sentry.sdk_meta,
137
+ sent_at: Sentry.utc_now.iso8601
138
+ }
139
+
140
+ if event.is_a?(Event) && event.dynamic_sampling_context
141
+ envelope_headers[:trace] = event.dynamic_sampling_context
142
+ end
143
+
144
+ envelope = Envelope.new(envelope_headers)
145
+
146
+ envelope.add_item(
147
+ { type: item_type, content_type: 'application/json' },
148
+ event_payload
149
+ )
150
+
151
+ if event.is_a?(TransactionEvent) && event.profile
152
+ envelope.add_item(
153
+ { type: 'profile', content_type: 'application/json' },
154
+ event.profile
155
+ )
156
+ end
157
+
158
+ client_report_headers, client_report_payload = fetch_pending_client_report
159
+ envelope.add_item(client_report_headers, client_report_payload) if client_report_headers
160
+
161
+ envelope
162
+ end
163
+
164
+ def record_lost_event(reason, item_type)
165
+ return unless @send_client_reports
166
+ return unless CLIENT_REPORT_REASONS.include?(reason)
167
+
168
+ @discarded_events[[reason, item_type]] += 1
169
+ end
170
+
171
+ def flush
172
+ client_report_headers, client_report_payload = fetch_pending_client_report(force: true)
173
+ return unless client_report_headers
174
+
175
+ envelope = Envelope.new
176
+ envelope.add_item(client_report_headers, client_report_payload)
177
+ send_envelope(envelope)
178
+ end
179
+
180
+ private
181
+
182
+ def fetch_pending_client_report(force: false)
183
+ return nil unless @send_client_reports
184
+ return nil if !force && @last_client_report_sent > Time.now - CLIENT_REPORT_INTERVAL
185
+ return nil if @discarded_events.empty?
186
+
187
+ discarded_events_hash = @discarded_events.map do |key, val|
188
+ reason, type = key
189
+
190
+ # 'event' has to be mapped to 'error'
191
+ category = type == 'event' ? 'error' : type
192
+
193
+ { reason: reason, category: category, quantity: val }
194
+ end
195
+
196
+ item_header = { type: 'client_report' }
197
+ item_payload = {
198
+ timestamp: Sentry.utc_now.iso8601,
199
+ discarded_events: discarded_events_hash
200
+ }
201
+
202
+ @discarded_events = Hash.new(0)
203
+ @last_client_report_sent = Time.now
204
+
205
+ [item_header, item_payload]
206
+ end
207
+
208
+ def reject_rate_limited_items(envelope)
209
+ envelope.items.reject! do |item|
210
+ if is_rate_limited?(item.type)
211
+ log_debug("[Transport] Envelope item [#{item.type}] not sent: rate limiting")
212
+ record_lost_event(:ratelimit_backoff, item.type)
213
+
214
+ true
215
+ else
216
+ false
217
+ end
218
+ end
219
+ end
220
+ end
221
+ end
222
+
223
+ require "sentry/transport/dummy_transport"
224
+ require "sentry/transport/http_transport"
225
+ require "sentry/transport/spotlight_transport"
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sentry
4
+ module ArgumentCheckingHelper
5
+ private
6
+
7
+ def check_argument_type!(argument, *expected_types)
8
+ unless expected_types.any? { |t| argument.is_a?(t) }
9
+ raise ArgumentError, "expect the argument to be a #{expected_types.join(' or ')}, got #{argument.class} (#{argument.inspect})"
10
+ end
11
+ end
12
+
13
+ def check_argument_includes!(argument, values)
14
+ unless values.include?(argument)
15
+ raise ArgumentError, "expect the argument to be one of #{values.map(&:inspect).join(' or ')}, got #{argument.inspect}"
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sentry
4
+ module CustomInspection
5
+ def inspect
6
+ attr_strings = (instance_variables - self.class::SKIP_INSPECTION_ATTRIBUTES).each_with_object([]) do |attr, result|
7
+ value = instance_variable_get(attr)
8
+ result << "#{attr}=#{value.inspect}" if value
9
+ end
10
+
11
+ "#<#{self.class.name} #{attr_strings.join(", ")}>"
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sentry
4
+ module Utils
5
+ module EncodingHelper
6
+ def self.encode_to_utf_8(value)
7
+ if value.encoding != Encoding::UTF_8 && value.respond_to?(:force_encoding)
8
+ value = value.dup.force_encoding(Encoding::UTF_8)
9
+ end
10
+
11
+ value = value.scrub unless value.valid_encoding?
12
+ value
13
+ end
14
+
15
+ def self.valid_utf_8?(value)
16
+ return true unless value.respond_to?(:force_encoding)
17
+
18
+ value.dup.force_encoding(Encoding::UTF_8).valid_encoding?
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sentry
4
+ module Utils
5
+ module ExceptionCauseChain
6
+ def self.exception_to_array(exception)
7
+ exceptions = [exception]
8
+
9
+ while exception.cause
10
+ exception = exception.cause
11
+ break if exceptions.any? { |e| e.object_id == exception.object_id }
12
+
13
+ exceptions << exception
14
+ end
15
+
16
+ exceptions
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sentry
4
+ module LoggingHelper
5
+ def log_error(message, exception, debug: false)
6
+ message = "#{message}: #{exception.message}"
7
+ message += "\n#{exception.backtrace.join("\n")}" if debug
8
+
9
+ @logger.error(LOGGER_PROGNAME) do
10
+ message
11
+ end
12
+ end
13
+
14
+ def log_info(message)
15
+ @logger.info(LOGGER_PROGNAME) { message }
16
+ end
17
+
18
+ def log_debug(message)
19
+ @logger.debug(LOGGER_PROGNAME) { message }
20
+ end
21
+
22
+ def log_warn(message)
23
+ @logger.warn(LOGGER_PROGNAME) { message }
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ipaddr'
4
+
5
+ # Based on ActionDispatch::RemoteIp. All security-related precautions from that
6
+ # middleware have been removed, because the Event IP just needs to be accurate,
7
+ # and spoofing an IP here only makes data inaccurate, not insecure. Don't re-use
8
+ # this module if you have to *trust* the IP address.
9
+ module Sentry
10
+ module Utils
11
+ class RealIp
12
+ LOCAL_ADDRESSES = [
13
+ "127.0.0.1", # localhost IPv4
14
+ "::1", # localhost IPv6
15
+ "fc00::/7", # private IPv6 range fc00::/7
16
+ "10.0.0.0/8", # private IPv4 range 10.x.x.x
17
+ "172.16.0.0/12", # private IPv4 range 172.16.0.0 .. 172.31.255.255
18
+ "192.168.0.0/16", # private IPv4 range 192.168.x.x
19
+ ]
20
+
21
+ attr_reader :ip
22
+
23
+ def initialize(
24
+ remote_addr: nil,
25
+ client_ip: nil,
26
+ real_ip: nil,
27
+ forwarded_for: nil,
28
+ trusted_proxies: []
29
+ )
30
+ @remote_addr = remote_addr
31
+ @client_ip = client_ip
32
+ @real_ip = real_ip
33
+ @forwarded_for = forwarded_for
34
+ @trusted_proxies = (LOCAL_ADDRESSES + Array(trusted_proxies)).map do |proxy|
35
+ if proxy.is_a?(IPAddr)
36
+ proxy
37
+ else
38
+ IPAddr.new(proxy.to_s)
39
+ end
40
+ end.uniq
41
+ end
42
+
43
+ def calculate_ip
44
+ # CGI environment variable set by Rack
45
+ remote_addr = ips_from(@remote_addr).last
46
+
47
+ # Could be a CSV list and/or repeated headers that were concatenated.
48
+ client_ips = ips_from(@client_ip)
49
+ real_ips = ips_from(@real_ip)
50
+
51
+ # The first address in this list is the original client, followed by
52
+ # the IPs of successive proxies. We want to search starting from the end
53
+ # until we find the first proxy that we do not trust.
54
+ forwarded_ips = ips_from(@forwarded_for).reverse
55
+
56
+ ips = [client_ips, real_ips, forwarded_ips, remote_addr].flatten.compact
57
+
58
+ # If every single IP option is in the trusted list, just return REMOTE_ADDR
59
+ @ip = filter_trusted_proxy_addresses(ips).first || remote_addr
60
+ end
61
+
62
+ protected
63
+
64
+ def ips_from(header)
65
+ # Split the comma-separated list into an array of strings
66
+ ips = header ? header.strip.split(/[,\s]+/) : []
67
+ ips.select do |ip|
68
+ begin
69
+ # Only return IPs that are valid according to the IPAddr#new method
70
+ range = IPAddr.new(ip).to_range
71
+ # we want to make sure nobody is sneaking a netmask in
72
+ range.begin == range.end
73
+ rescue ArgumentError
74
+ nil
75
+ end
76
+ end
77
+ end
78
+
79
+ def filter_trusted_proxy_addresses(ips)
80
+ ips.reject { |ip| @trusted_proxies.any? { |proxy| proxy === ip } }
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sentry
4
+ module Utils
5
+ module RequestId
6
+ REQUEST_ID_HEADERS = %w(action_dispatch.request_id HTTP_X_REQUEST_ID).freeze
7
+
8
+ # Request ID based on ActionDispatch::RequestId
9
+ def self.read_from(env)
10
+ REQUEST_ID_HEADERS.each do |key|
11
+ request_id = env[key]
12
+ return request_id if request_id
13
+ end
14
+ nil
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sentry
4
+ VERSION = "5.16.1"
5
+ end