sentry-ruby 0.1.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.
- 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
|