sentry-ruby 5.3.0 → 5.8.0
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.
- checksums.yaml +4 -4
- data/.gitignore +11 -0
- data/.rspec +2 -0
- data/.yardopts +2 -0
- data/CHANGELOG.md +313 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +31 -0
- data/Makefile +4 -0
- data/README.md +10 -6
- data/Rakefile +13 -0
- data/bin/console +18 -0
- data/bin/setup +8 -0
- data/lib/sentry/background_worker.rb +72 -0
- data/lib/sentry/backtrace.rb +124 -0
- data/lib/sentry/baggage.rb +81 -0
- data/lib/sentry/breadcrumb/sentry_logger.rb +90 -0
- data/lib/sentry/breadcrumb.rb +70 -0
- data/lib/sentry/breadcrumb_buffer.rb +64 -0
- data/lib/sentry/client.rb +207 -0
- data/lib/sentry/configuration.rb +543 -0
- data/lib/sentry/core_ext/object/deep_dup.rb +61 -0
- data/lib/sentry/core_ext/object/duplicable.rb +155 -0
- data/lib/sentry/dsn.rb +53 -0
- data/lib/sentry/envelope.rb +96 -0
- data/lib/sentry/error_event.rb +38 -0
- data/lib/sentry/event.rb +178 -0
- data/lib/sentry/exceptions.rb +9 -0
- data/lib/sentry/hub.rb +241 -0
- data/lib/sentry/integrable.rb +26 -0
- data/lib/sentry/interface.rb +16 -0
- data/lib/sentry/interfaces/exception.rb +43 -0
- data/lib/sentry/interfaces/request.rb +134 -0
- data/lib/sentry/interfaces/single_exception.rb +65 -0
- data/lib/sentry/interfaces/stacktrace.rb +87 -0
- data/lib/sentry/interfaces/stacktrace_builder.rb +79 -0
- data/lib/sentry/interfaces/threads.rb +42 -0
- data/lib/sentry/linecache.rb +47 -0
- data/lib/sentry/logger.rb +20 -0
- data/lib/sentry/net/http.rb +103 -0
- data/lib/sentry/rack/capture_exceptions.rb +82 -0
- data/lib/sentry/rack.rb +5 -0
- data/lib/sentry/rake.rb +41 -0
- data/lib/sentry/redis.rb +107 -0
- data/lib/sentry/release_detector.rb +39 -0
- data/lib/sentry/scope.rb +339 -0
- data/lib/sentry/session.rb +33 -0
- data/lib/sentry/session_flusher.rb +90 -0
- data/lib/sentry/span.rb +236 -0
- data/lib/sentry/test_helper.rb +78 -0
- data/lib/sentry/transaction.rb +345 -0
- data/lib/sentry/transaction_event.rb +53 -0
- data/lib/sentry/transport/configuration.rb +25 -0
- data/lib/sentry/transport/dummy_transport.rb +21 -0
- data/lib/sentry/transport/http_transport.rb +175 -0
- data/lib/sentry/transport.rb +214 -0
- data/lib/sentry/utils/argument_checking_helper.rb +13 -0
- data/lib/sentry/utils/custom_inspection.rb +14 -0
- data/lib/sentry/utils/encoding_helper.rb +22 -0
- data/lib/sentry/utils/exception_cause_chain.rb +20 -0
- data/lib/sentry/utils/logging_helper.rb +26 -0
- data/lib/sentry/utils/real_ip.rb +84 -0
- data/lib/sentry/utils/request_id.rb +18 -0
- data/lib/sentry/version.rb +5 -0
- data/lib/sentry-ruby.rb +511 -0
- data/sentry-ruby-core.gemspec +23 -0
- data/sentry-ruby.gemspec +24 -0
- metadata +66 -16
@@ -0,0 +1,214 @@
|
|
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
|
+
]
|
23
|
+
|
24
|
+
include LoggingHelper
|
25
|
+
|
26
|
+
attr_reader :rate_limits, :discarded_events, :last_client_report_sent
|
27
|
+
|
28
|
+
# @deprecated Use Sentry.logger to retrieve the current logger instead.
|
29
|
+
attr_reader :logger
|
30
|
+
|
31
|
+
def initialize(configuration)
|
32
|
+
@logger = configuration.logger
|
33
|
+
@transport_configuration = configuration.transport
|
34
|
+
@dsn = configuration.dsn
|
35
|
+
@rate_limits = {}
|
36
|
+
@send_client_reports = configuration.send_client_reports
|
37
|
+
|
38
|
+
if @send_client_reports
|
39
|
+
@discarded_events = Hash.new(0)
|
40
|
+
@last_client_report_sent = Time.now
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def send_data(data, options = {})
|
45
|
+
raise NotImplementedError
|
46
|
+
end
|
47
|
+
|
48
|
+
def send_event(event)
|
49
|
+
envelope = envelope_from_event(event)
|
50
|
+
send_envelope(envelope)
|
51
|
+
|
52
|
+
event
|
53
|
+
end
|
54
|
+
|
55
|
+
def send_envelope(envelope)
|
56
|
+
reject_rate_limited_items(envelope)
|
57
|
+
|
58
|
+
return if envelope.items.empty?
|
59
|
+
|
60
|
+
data, serialized_items = serialize_envelope(envelope)
|
61
|
+
|
62
|
+
if data
|
63
|
+
log_info("[Transport] Sending envelope with items [#{serialized_items.map(&:type).join(', ')}] #{envelope.event_id} to Sentry")
|
64
|
+
send_data(data)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def serialize_envelope(envelope)
|
69
|
+
serialized_items = []
|
70
|
+
serialized_results = []
|
71
|
+
|
72
|
+
envelope.items.each do |item|
|
73
|
+
result, oversized = item.serialize
|
74
|
+
|
75
|
+
if oversized
|
76
|
+
log_info("Envelope item [#{item.type}] is still oversized after size reduction: {#{item.size_breakdown}}")
|
77
|
+
|
78
|
+
next
|
79
|
+
end
|
80
|
+
|
81
|
+
serialized_results << result
|
82
|
+
serialized_items << item
|
83
|
+
end
|
84
|
+
|
85
|
+
data = [JSON.generate(envelope.headers), *serialized_results].join("\n") unless serialized_results.empty?
|
86
|
+
|
87
|
+
[data, serialized_items]
|
88
|
+
end
|
89
|
+
|
90
|
+
def is_rate_limited?(item_type)
|
91
|
+
# check category-specific limit
|
92
|
+
category_delay =
|
93
|
+
case item_type
|
94
|
+
when "transaction"
|
95
|
+
@rate_limits["transaction"]
|
96
|
+
when "sessions"
|
97
|
+
@rate_limits["session"]
|
98
|
+
else
|
99
|
+
@rate_limits["error"]
|
100
|
+
end
|
101
|
+
|
102
|
+
# check universal limit if not category limit
|
103
|
+
universal_delay = @rate_limits[nil]
|
104
|
+
|
105
|
+
delay =
|
106
|
+
if category_delay && universal_delay
|
107
|
+
if category_delay > universal_delay
|
108
|
+
category_delay
|
109
|
+
else
|
110
|
+
universal_delay
|
111
|
+
end
|
112
|
+
elsif category_delay
|
113
|
+
category_delay
|
114
|
+
else
|
115
|
+
universal_delay
|
116
|
+
end
|
117
|
+
|
118
|
+
!!delay && delay > Time.now
|
119
|
+
end
|
120
|
+
|
121
|
+
def generate_auth_header
|
122
|
+
now = Sentry.utc_now.to_i
|
123
|
+
fields = {
|
124
|
+
'sentry_version' => PROTOCOL_VERSION,
|
125
|
+
'sentry_client' => USER_AGENT,
|
126
|
+
'sentry_timestamp' => now,
|
127
|
+
'sentry_key' => @dsn.public_key
|
128
|
+
}
|
129
|
+
fields['sentry_secret'] = @dsn.secret_key if @dsn.secret_key
|
130
|
+
'Sentry ' + fields.map { |key, value| "#{key}=#{value}" }.join(', ')
|
131
|
+
end
|
132
|
+
|
133
|
+
def envelope_from_event(event)
|
134
|
+
# Convert to hash
|
135
|
+
event_payload = event.to_hash
|
136
|
+
event_id = event_payload[:event_id] || event_payload["event_id"]
|
137
|
+
item_type = event_payload[:type] || event_payload["type"]
|
138
|
+
|
139
|
+
envelope_headers = {
|
140
|
+
event_id: event_id,
|
141
|
+
dsn: @dsn.to_s,
|
142
|
+
sdk: Sentry.sdk_meta,
|
143
|
+
sent_at: Sentry.utc_now.iso8601
|
144
|
+
}
|
145
|
+
|
146
|
+
if event.is_a?(TransactionEvent) && event.dynamic_sampling_context
|
147
|
+
envelope_headers[:trace] = event.dynamic_sampling_context
|
148
|
+
end
|
149
|
+
|
150
|
+
envelope = Envelope.new(envelope_headers)
|
151
|
+
|
152
|
+
envelope.add_item(
|
153
|
+
{ type: item_type, content_type: 'application/json' },
|
154
|
+
event_payload
|
155
|
+
)
|
156
|
+
|
157
|
+
client_report_headers, client_report_payload = fetch_pending_client_report
|
158
|
+
envelope.add_item(client_report_headers, client_report_payload) if client_report_headers
|
159
|
+
|
160
|
+
envelope
|
161
|
+
end
|
162
|
+
|
163
|
+
def record_lost_event(reason, item_type)
|
164
|
+
return unless @send_client_reports
|
165
|
+
return unless CLIENT_REPORT_REASONS.include?(reason)
|
166
|
+
|
167
|
+
@discarded_events[[reason, item_type]] += 1
|
168
|
+
end
|
169
|
+
|
170
|
+
private
|
171
|
+
|
172
|
+
def fetch_pending_client_report
|
173
|
+
return nil unless @send_client_reports
|
174
|
+
return nil if @last_client_report_sent > Time.now - CLIENT_REPORT_INTERVAL
|
175
|
+
return nil if @discarded_events.empty?
|
176
|
+
|
177
|
+
discarded_events_hash = @discarded_events.map do |key, val|
|
178
|
+
reason, type = key
|
179
|
+
|
180
|
+
# 'event' has to be mapped to 'error'
|
181
|
+
category = type == 'transaction' ? 'transaction' : 'error'
|
182
|
+
|
183
|
+
{ reason: reason, category: category, quantity: val }
|
184
|
+
end
|
185
|
+
|
186
|
+
item_header = { type: 'client_report' }
|
187
|
+
item_payload = {
|
188
|
+
timestamp: Sentry.utc_now.iso8601,
|
189
|
+
discarded_events: discarded_events_hash
|
190
|
+
}
|
191
|
+
|
192
|
+
@discarded_events = Hash.new(0)
|
193
|
+
@last_client_report_sent = Time.now
|
194
|
+
|
195
|
+
[item_header, item_payload]
|
196
|
+
end
|
197
|
+
|
198
|
+
def reject_rate_limited_items(envelope)
|
199
|
+
envelope.items.reject! do |item|
|
200
|
+
if is_rate_limited?(item.type)
|
201
|
+
log_info("[Transport] Envelope item [#{item.type}] not sent: rate limiting")
|
202
|
+
record_lost_event(:ratelimit_backoff, item.type)
|
203
|
+
|
204
|
+
true
|
205
|
+
else
|
206
|
+
false
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
require "sentry/transport/dummy_transport"
|
214
|
+
require "sentry/transport/http_transport"
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sentry
|
4
|
+
module ArgumentCheckingHelper
|
5
|
+
private
|
6
|
+
|
7
|
+
def check_argument_type!(argument, expected_type)
|
8
|
+
unless argument.is_a?(expected_type)
|
9
|
+
raise ArgumentError, "expect the argument to be a #{expected_type}, got #{argument.class} (#{argument.inspect})"
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
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
|