sentry-ruby 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.craft.yml +18 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.travis.yml +6 -0
- data/CHANGELOG.md +10 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +11 -0
- data/LICENSE.txt +21 -0
- data/README.md +44 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/sentry.rb +97 -0
- data/lib/sentry/backtrace.rb +128 -0
- data/lib/sentry/breadcrumb.rb +25 -0
- data/lib/sentry/breadcrumb/sentry_logger.rb +103 -0
- data/lib/sentry/breadcrumb_buffer.rb +50 -0
- data/lib/sentry/client.rb +85 -0
- data/lib/sentry/configuration.rb +401 -0
- data/lib/sentry/core_ext/object/deep_dup.rb +57 -0
- data/lib/sentry/core_ext/object/duplicable.rb +153 -0
- data/lib/sentry/dsn.rb +45 -0
- data/lib/sentry/event.rb +175 -0
- data/lib/sentry/event/options.rb +31 -0
- data/lib/sentry/hub.rb +126 -0
- data/lib/sentry/interface.rb +22 -0
- data/lib/sentry/interfaces/exception.rb +11 -0
- data/lib/sentry/interfaces/request.rb +104 -0
- data/lib/sentry/interfaces/single_exception.rb +14 -0
- data/lib/sentry/interfaces/stacktrace.rb +57 -0
- data/lib/sentry/linecache.rb +44 -0
- data/lib/sentry/logger.rb +20 -0
- data/lib/sentry/rack.rb +4 -0
- data/lib/sentry/rack/capture_exception.rb +45 -0
- data/lib/sentry/ruby.rb +1 -0
- data/lib/sentry/scope.rb +192 -0
- data/lib/sentry/transport.rb +110 -0
- data/lib/sentry/transport/configuration.rb +28 -0
- data/lib/sentry/transport/dummy_transport.rb +14 -0
- data/lib/sentry/transport/http_transport.rb +62 -0
- data/lib/sentry/transport/state.rb +40 -0
- data/lib/sentry/utils/deep_merge.rb +22 -0
- data/lib/sentry/utils/exception_cause_chain.rb +20 -0
- data/lib/sentry/utils/real_ip.rb +70 -0
- data/lib/sentry/version.rb +3 -0
- data/sentry-ruby.gemspec +26 -0
- metadata +107 -0
data/lib/sentry/ruby.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "sentry"
|
data/lib/sentry/scope.rb
ADDED
@@ -0,0 +1,192 @@
|
|
1
|
+
require "sentry/breadcrumb_buffer"
|
2
|
+
require "etc"
|
3
|
+
|
4
|
+
module Sentry
|
5
|
+
class Scope
|
6
|
+
ATTRIBUTES = [:transaction_names, :contexts, :extra, :tags, :user, :level, :breadcrumbs, :fingerprint, :event_processors, :rack_env]
|
7
|
+
|
8
|
+
attr_reader(*ATTRIBUTES)
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
set_default_value
|
12
|
+
end
|
13
|
+
|
14
|
+
def clear
|
15
|
+
set_default_value
|
16
|
+
end
|
17
|
+
|
18
|
+
def apply_to_event(event)
|
19
|
+
event.tags = tags.merge(event.tags)
|
20
|
+
event.user = user.merge(event.user)
|
21
|
+
event.extra = extra.merge(event.extra)
|
22
|
+
event.contexts = contexts.merge(event.contexts)
|
23
|
+
event.fingerprint = fingerprint
|
24
|
+
event.level ||= level
|
25
|
+
event.transaction = transaction_names.last
|
26
|
+
event.breadcrumbs = breadcrumbs
|
27
|
+
event.rack_env = rack_env
|
28
|
+
|
29
|
+
unless @event_processors.empty?
|
30
|
+
@event_processors.each do |processor_block|
|
31
|
+
event = processor_block.call(event)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
event
|
36
|
+
end
|
37
|
+
|
38
|
+
def add_breadcrumb(breadcrumb)
|
39
|
+
breadcrumbs.record(breadcrumb)
|
40
|
+
end
|
41
|
+
|
42
|
+
def clear_breadcrumbs
|
43
|
+
@breadcrumbs = BreadcrumbBuffer.new
|
44
|
+
end
|
45
|
+
|
46
|
+
def dup
|
47
|
+
copy = super
|
48
|
+
copy.breadcrumbs = breadcrumbs.dup
|
49
|
+
copy.contexts = contexts.deep_dup
|
50
|
+
copy.extra = extra.deep_dup
|
51
|
+
copy.tags = tags.deep_dup
|
52
|
+
copy.user = user.deep_dup
|
53
|
+
copy.transaction_names = transaction_names.deep_dup
|
54
|
+
copy.fingerprint = fingerprint.deep_dup
|
55
|
+
copy
|
56
|
+
end
|
57
|
+
|
58
|
+
def update_from_scope(scope)
|
59
|
+
self.breadcrumbs = scope.breadcrumbs
|
60
|
+
self.contexts = scope.contexts
|
61
|
+
self.extra = scope.extra
|
62
|
+
self.tags = scope.tags
|
63
|
+
self.user = scope.user
|
64
|
+
self.transaction_names = scope.transaction_names
|
65
|
+
self.fingerprint = scope.fingerprint
|
66
|
+
end
|
67
|
+
|
68
|
+
def update_from_options(
|
69
|
+
contexts: nil,
|
70
|
+
extra: nil,
|
71
|
+
tags: nil,
|
72
|
+
user: nil,
|
73
|
+
level: nil,
|
74
|
+
fingerprint: nil
|
75
|
+
)
|
76
|
+
self.contexts.merge!(contexts) if contexts
|
77
|
+
self.extra.merge!(extra) if extra
|
78
|
+
self.tags.merge!(tags) if tags
|
79
|
+
self.user = user if user
|
80
|
+
self.level = level if level
|
81
|
+
self.fingerprint = fingerprint if fingerprint
|
82
|
+
end
|
83
|
+
|
84
|
+
def set_rack_env(env)
|
85
|
+
env = env || {}
|
86
|
+
@rack_env = env
|
87
|
+
end
|
88
|
+
|
89
|
+
def set_user(user_hash)
|
90
|
+
check_argument_type!(user_hash, Hash)
|
91
|
+
@user = user_hash
|
92
|
+
end
|
93
|
+
|
94
|
+
def set_extras(extras_hash)
|
95
|
+
check_argument_type!(extras_hash, Hash)
|
96
|
+
@extra.merge!(extras_hash)
|
97
|
+
end
|
98
|
+
|
99
|
+
def set_extra(key, value)
|
100
|
+
@extra.merge!(key => value)
|
101
|
+
end
|
102
|
+
|
103
|
+
def set_tags(tags_hash)
|
104
|
+
check_argument_type!(tags_hash, Hash)
|
105
|
+
@tags.merge!(tags_hash)
|
106
|
+
end
|
107
|
+
|
108
|
+
def set_tag(key, value)
|
109
|
+
@tags.merge!(key => value)
|
110
|
+
end
|
111
|
+
|
112
|
+
def set_contexts(contexts_hash)
|
113
|
+
check_argument_type!(contexts_hash, Hash)
|
114
|
+
@contexts = contexts_hash
|
115
|
+
end
|
116
|
+
|
117
|
+
def set_context(key, value)
|
118
|
+
@contexts.merge!(key => value)
|
119
|
+
end
|
120
|
+
|
121
|
+
def set_level(level)
|
122
|
+
@level = level
|
123
|
+
end
|
124
|
+
|
125
|
+
def set_transaction_name(transaction_name)
|
126
|
+
@transaction_names << transaction_name
|
127
|
+
end
|
128
|
+
|
129
|
+
def transaction_name
|
130
|
+
@transaction_names.last
|
131
|
+
end
|
132
|
+
|
133
|
+
def set_fingerprint(fingerprint)
|
134
|
+
check_argument_type!(fingerprint, Array)
|
135
|
+
|
136
|
+
@fingerprint = fingerprint
|
137
|
+
end
|
138
|
+
|
139
|
+
def add_event_processor(&block)
|
140
|
+
@event_processors << block
|
141
|
+
end
|
142
|
+
|
143
|
+
protected
|
144
|
+
|
145
|
+
# for duplicating scopes internally
|
146
|
+
attr_writer(*ATTRIBUTES)
|
147
|
+
|
148
|
+
private
|
149
|
+
|
150
|
+
def check_argument_type!(argument, expected_type)
|
151
|
+
unless argument.is_a?(expected_type)
|
152
|
+
raise ArgumentError, "expect the argument to be a #{expected_type}, got #{argument.class} (#{argument})"
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
def set_default_value
|
157
|
+
@breadcrumbs = BreadcrumbBuffer.new
|
158
|
+
@contexts = { :os => self.class.os_context, :runtime => self.class.runtime_context }
|
159
|
+
@extra = {}
|
160
|
+
@tags = {}
|
161
|
+
@user = {}
|
162
|
+
@level = :error
|
163
|
+
@fingerprint = []
|
164
|
+
@transaction_names = []
|
165
|
+
@event_processors = []
|
166
|
+
@rack_env = {}
|
167
|
+
end
|
168
|
+
|
169
|
+
class << self
|
170
|
+
def os_context
|
171
|
+
@os_context ||=
|
172
|
+
begin
|
173
|
+
uname = Etc.uname
|
174
|
+
{
|
175
|
+
name: uname[:sysname] || RbConfig::CONFIG["host_os"],
|
176
|
+
version: uname[:version],
|
177
|
+
build: uname[:release],
|
178
|
+
kernel_version: uname[:version]
|
179
|
+
}
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
def runtime_context
|
184
|
+
@runtime_context ||= {
|
185
|
+
name: RbConfig::CONFIG["ruby_install_name"],
|
186
|
+
version: RUBY_DESCRIPTION || Sentry.sys_command("ruby -v")
|
187
|
+
}
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
end
|
192
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
require "json"
|
2
|
+
require "base64"
|
3
|
+
require "sentry/transport/state"
|
4
|
+
|
5
|
+
module Sentry
|
6
|
+
class Transport
|
7
|
+
PROTOCOL_VERSION = '5'
|
8
|
+
USER_AGENT = "sentry-ruby/#{Sentry::VERSION}"
|
9
|
+
CONTENT_TYPE = 'application/json'
|
10
|
+
|
11
|
+
attr_accessor :configuration, :state
|
12
|
+
|
13
|
+
def initialize(configuration)
|
14
|
+
@configuration = configuration
|
15
|
+
@transport_configuration = configuration.transport
|
16
|
+
@dsn = configuration.dsn
|
17
|
+
@state = State.new
|
18
|
+
end
|
19
|
+
|
20
|
+
def send_data(data, options = {})
|
21
|
+
raise NotImplementedError
|
22
|
+
end
|
23
|
+
|
24
|
+
def send_event(event)
|
25
|
+
content_type, encoded_data = prepare_encoded_event(event)
|
26
|
+
|
27
|
+
return nil unless encoded_data
|
28
|
+
|
29
|
+
begin
|
30
|
+
if configuration.async?
|
31
|
+
begin
|
32
|
+
# We have to convert to a JSON-like hash, because background job
|
33
|
+
# processors (esp ActiveJob) may not like weird types in the event hash
|
34
|
+
configuration.async.call(event.to_json_compatible)
|
35
|
+
rescue => e
|
36
|
+
configuration.logger.error(LOGGER_PROGNAME) { "async event sending failed: #{e.message}" }
|
37
|
+
send_data(encoded_data, content_type: content_type)
|
38
|
+
end
|
39
|
+
else
|
40
|
+
send_data(encoded_data, content_type: content_type)
|
41
|
+
end
|
42
|
+
|
43
|
+
state.success
|
44
|
+
rescue => e
|
45
|
+
failed_for_exception(e, event)
|
46
|
+
return
|
47
|
+
end
|
48
|
+
|
49
|
+
event
|
50
|
+
end
|
51
|
+
|
52
|
+
def generate_auth_header
|
53
|
+
now = Time.now.to_i.to_s
|
54
|
+
fields = {
|
55
|
+
'sentry_version' => PROTOCOL_VERSION,
|
56
|
+
'sentry_client' => USER_AGENT,
|
57
|
+
'sentry_timestamp' => now,
|
58
|
+
'sentry_key' => @dsn.public_key
|
59
|
+
}
|
60
|
+
fields['sentry_secret'] = @dsn.secret_key if @dsn.secret_key
|
61
|
+
'Sentry ' + fields.map { |key, value| "#{key}=#{value}" }.join(', ')
|
62
|
+
end
|
63
|
+
|
64
|
+
def encode(event_hash)
|
65
|
+
event_id = event_hash[:event_id] || event_hash['event_id']
|
66
|
+
|
67
|
+
envelope = <<~ENVELOPE
|
68
|
+
{"event_id":"#{event_id}","dsn":"#{configuration.dsn.to_s}","sdk":#{Sentry.sdk_meta.to_json},"sent_at":"#{DateTime.now.rfc3339}"}
|
69
|
+
{"type":"event","content_type":"application/json"}
|
70
|
+
#{event_hash.to_json}
|
71
|
+
ENVELOPE
|
72
|
+
|
73
|
+
[CONTENT_TYPE, envelope]
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
def prepare_encoded_event(event)
|
79
|
+
# Convert to hash
|
80
|
+
event_hash = event.to_hash
|
81
|
+
|
82
|
+
unless @state.should_try?
|
83
|
+
failed_for_previous_failure(event_hash)
|
84
|
+
return
|
85
|
+
end
|
86
|
+
|
87
|
+
event_id = event_hash[:event_id] || event_hash['event_id']
|
88
|
+
configuration.logger.info(LOGGER_PROGNAME) { "Sending event #{event_id} to Sentry" }
|
89
|
+
encode(event_hash)
|
90
|
+
end
|
91
|
+
|
92
|
+
def failed_for_exception(e, event)
|
93
|
+
@state.failure
|
94
|
+
configuration.logger.warn(LOGGER_PROGNAME) { "Unable to record event with remote Sentry server (#{e.class} - #{e.message}):\n#{e.backtrace[0..10].join("\n")}" }
|
95
|
+
log_not_sending(event)
|
96
|
+
end
|
97
|
+
|
98
|
+
def failed_for_previous_failure(event)
|
99
|
+
configuration.logger.warn(LOGGER_PROGNAME) { "Not sending event due to previous failure(s)." }
|
100
|
+
log_not_sending(event)
|
101
|
+
end
|
102
|
+
|
103
|
+
def log_not_sending(event)
|
104
|
+
configuration.logger.warn(LOGGER_PROGNAME) { "Failed to submit event: #{Event.get_log_message(event.to_hash)}" }
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
require "sentry/transport/dummy_transport"
|
110
|
+
require "sentry/transport/http_transport"
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Sentry
|
2
|
+
class Transport
|
3
|
+
class Configuration
|
4
|
+
attr_accessor :timeout, :open_timeout, :proxy, :ssl, :ssl_ca_file, :ssl_verification, :encoding, :http_adapter, :faraday_builder, :transport_class
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
@ssl_verification = true
|
8
|
+
@open_timeout = 1
|
9
|
+
@timeout = 2
|
10
|
+
@encoding = 'gzip'
|
11
|
+
end
|
12
|
+
|
13
|
+
def encoding=(encoding)
|
14
|
+
raise(Error, 'Unsupported encoding') unless %w(gzip json).include? encoding
|
15
|
+
|
16
|
+
@encoding = encoding
|
17
|
+
end
|
18
|
+
|
19
|
+
def transport_class=(klass)
|
20
|
+
unless klass.is_a?(Class)
|
21
|
+
raise Sentry::Error.new("config.transport.transport_class must a class. got: #{klass.class}")
|
22
|
+
end
|
23
|
+
|
24
|
+
@transport_class = klass
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'faraday'
|
2
|
+
|
3
|
+
module Sentry
|
4
|
+
class HTTPTransport < Transport
|
5
|
+
attr_reader :conn, :adapter
|
6
|
+
|
7
|
+
def initialize(*args)
|
8
|
+
super
|
9
|
+
@adapter = @transport_configuration.http_adapter || Faraday.default_adapter
|
10
|
+
@conn = set_conn
|
11
|
+
@endpoint = @dsn.envelope_endpoint
|
12
|
+
end
|
13
|
+
|
14
|
+
def send_data(data, options = {})
|
15
|
+
unless configuration.sending_allowed?
|
16
|
+
logger.debug(LOGGER_PROGNAME) { "Event not sent: #{configuration.error_messages}" }
|
17
|
+
end
|
18
|
+
|
19
|
+
conn.post @endpoint do |req|
|
20
|
+
req.headers['Content-Type'] = options[:content_type]
|
21
|
+
req.headers['X-Sentry-Auth'] = generate_auth_header
|
22
|
+
req.body = data
|
23
|
+
end
|
24
|
+
rescue Faraday::Error => e
|
25
|
+
error_info = e.message
|
26
|
+
if e.response && e.response[:headers]['x-sentry-error']
|
27
|
+
error_info += " Error in headers is: #{e.response[:headers]['x-sentry-error']}"
|
28
|
+
end
|
29
|
+
raise Sentry::Error, error_info
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def set_conn
|
35
|
+
server = @dsn.server
|
36
|
+
|
37
|
+
configuration.logger.debug(LOGGER_PROGNAME) { "Sentry HTTP Transport connecting to #{server}" }
|
38
|
+
|
39
|
+
Faraday.new(server, :ssl => ssl_configuration, :proxy => @transport_configuration.proxy) do |builder|
|
40
|
+
@transport_configuration.faraday_builder&.call(builder)
|
41
|
+
builder.response :raise_error
|
42
|
+
builder.options.merge! faraday_opts
|
43
|
+
builder.headers[:user_agent] = "sentry-ruby/#{Sentry::VERSION}"
|
44
|
+
builder.adapter(*adapter)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# TODO: deprecate and replace where possible w/Faraday Builder
|
49
|
+
def faraday_opts
|
50
|
+
[:timeout, :open_timeout].each_with_object({}) do |opt, memo|
|
51
|
+
memo[opt] = @transport_configuration.public_send(opt) if @transport_configuration.public_send(opt)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def ssl_configuration
|
56
|
+
(@transport_configuration.ssl || {}).merge(
|
57
|
+
:verify => @transport_configuration.ssl_verification,
|
58
|
+
:ca_file => @transport_configuration.ssl_ca_file
|
59
|
+
)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Sentry
|
2
|
+
class Transport
|
3
|
+
class State
|
4
|
+
def initialize
|
5
|
+
reset
|
6
|
+
end
|
7
|
+
|
8
|
+
def should_try?
|
9
|
+
return true if @status == :online
|
10
|
+
|
11
|
+
interval = @retry_after || [@retry_number, 6].min**2
|
12
|
+
return true if Time.now - @last_check >= interval
|
13
|
+
|
14
|
+
false
|
15
|
+
end
|
16
|
+
|
17
|
+
def failure(retry_after = nil)
|
18
|
+
@status = :error
|
19
|
+
@retry_number += 1
|
20
|
+
@last_check = Time.now
|
21
|
+
@retry_after = retry_after
|
22
|
+
end
|
23
|
+
|
24
|
+
def success
|
25
|
+
reset
|
26
|
+
end
|
27
|
+
|
28
|
+
def reset
|
29
|
+
@status = :online
|
30
|
+
@retry_number = 0
|
31
|
+
@last_check = nil
|
32
|
+
@retry_after = nil
|
33
|
+
end
|
34
|
+
|
35
|
+
def failed?
|
36
|
+
@status == :error
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|