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.
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