sentry-ruby 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +7 -0
  2. data/.craft.yml +18 -0
  3. data/.gitignore +11 -0
  4. data/.rspec +3 -0
  5. data/.travis.yml +6 -0
  6. data/CHANGELOG.md +10 -0
  7. data/CODE_OF_CONDUCT.md +74 -0
  8. data/Gemfile +11 -0
  9. data/LICENSE.txt +21 -0
  10. data/README.md +44 -0
  11. data/Rakefile +6 -0
  12. data/bin/console +14 -0
  13. data/bin/setup +8 -0
  14. data/lib/sentry.rb +97 -0
  15. data/lib/sentry/backtrace.rb +128 -0
  16. data/lib/sentry/breadcrumb.rb +25 -0
  17. data/lib/sentry/breadcrumb/sentry_logger.rb +103 -0
  18. data/lib/sentry/breadcrumb_buffer.rb +50 -0
  19. data/lib/sentry/client.rb +85 -0
  20. data/lib/sentry/configuration.rb +401 -0
  21. data/lib/sentry/core_ext/object/deep_dup.rb +57 -0
  22. data/lib/sentry/core_ext/object/duplicable.rb +153 -0
  23. data/lib/sentry/dsn.rb +45 -0
  24. data/lib/sentry/event.rb +175 -0
  25. data/lib/sentry/event/options.rb +31 -0
  26. data/lib/sentry/hub.rb +126 -0
  27. data/lib/sentry/interface.rb +22 -0
  28. data/lib/sentry/interfaces/exception.rb +11 -0
  29. data/lib/sentry/interfaces/request.rb +104 -0
  30. data/lib/sentry/interfaces/single_exception.rb +14 -0
  31. data/lib/sentry/interfaces/stacktrace.rb +57 -0
  32. data/lib/sentry/linecache.rb +44 -0
  33. data/lib/sentry/logger.rb +20 -0
  34. data/lib/sentry/rack.rb +4 -0
  35. data/lib/sentry/rack/capture_exception.rb +45 -0
  36. data/lib/sentry/ruby.rb +1 -0
  37. data/lib/sentry/scope.rb +192 -0
  38. data/lib/sentry/transport.rb +110 -0
  39. data/lib/sentry/transport/configuration.rb +28 -0
  40. data/lib/sentry/transport/dummy_transport.rb +14 -0
  41. data/lib/sentry/transport/http_transport.rb +62 -0
  42. data/lib/sentry/transport/state.rb +40 -0
  43. data/lib/sentry/utils/deep_merge.rb +22 -0
  44. data/lib/sentry/utils/exception_cause_chain.rb +20 -0
  45. data/lib/sentry/utils/real_ip.rb +70 -0
  46. data/lib/sentry/version.rb +3 -0
  47. data/sentry-ruby.gemspec +26 -0
  48. metadata +107 -0
@@ -0,0 +1 @@
1
+ require "sentry"
@@ -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,14 @@
1
+ module Sentry
2
+ class DummyTransport < Transport
3
+ attr_accessor :events
4
+
5
+ def initialize(*)
6
+ super
7
+ @events = []
8
+ end
9
+
10
+ def send_event(event)
11
+ @events << event
12
+ end
13
+ end
14
+ 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