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,57 @@
1
+ require 'sentry/core_ext/object/duplicable'
2
+
3
+ #########################################
4
+ # This file was copied from Rails 5.2 #
5
+ #########################################
6
+
7
+ class Object
8
+ # Returns a deep copy of object if it's duplicable. If it's
9
+ # not duplicable, returns +self+.
10
+ #
11
+ # object = Object.new
12
+ # dup = object.deep_dup
13
+ # dup.instance_variable_set(:@a, 1)
14
+ #
15
+ # object.instance_variable_defined?(:@a) # => false
16
+ # dup.instance_variable_defined?(:@a) # => true
17
+ def deep_dup
18
+ duplicable? ? dup : self
19
+ end
20
+ end
21
+
22
+ class Array
23
+ # Returns a deep copy of array.
24
+ #
25
+ # array = [1, [2, 3]]
26
+ # dup = array.deep_dup
27
+ # dup[1][2] = 4
28
+ #
29
+ # array[1][2] # => nil
30
+ # dup[1][2] # => 4
31
+ def deep_dup
32
+ map(&:deep_dup)
33
+ end
34
+ end
35
+
36
+ class Hash
37
+ # Returns a deep copy of hash.
38
+ #
39
+ # hash = { a: { b: 'b' } }
40
+ # dup = hash.deep_dup
41
+ # dup[:a][:c] = 'c'
42
+ #
43
+ # hash[:a][:c] # => nil
44
+ # dup[:a][:c] # => "c"
45
+ def deep_dup
46
+ hash = dup
47
+ each_pair do |key, value|
48
+ if key.frozen? && ::String === key
49
+ hash[key] = value.deep_dup
50
+ else
51
+ hash.delete(key)
52
+ hash[key.deep_dup] = value.deep_dup
53
+ end
54
+ end
55
+ hash
56
+ end
57
+ end
@@ -0,0 +1,153 @@
1
+ # frozen_string_literal: true
2
+
3
+ #########################################
4
+ # This file was copied from Rails 5.2 #
5
+ #########################################
6
+
7
+ #--
8
+ # Most objects are cloneable, but not all. For example you can't dup methods:
9
+ #
10
+ # method(:puts).dup # => TypeError: allocator undefined for Method
11
+ #
12
+ # Classes may signal their instances are not duplicable removing +dup+/+clone+
13
+ # or raising exceptions from them. So, to dup an arbitrary object you normally
14
+ # use an optimistic approach and are ready to catch an exception, say:
15
+ #
16
+ # arbitrary_object.dup rescue object
17
+ #
18
+ # Rails dups objects in a few critical spots where they are not that arbitrary.
19
+ # That rescue is very expensive (like 40 times slower than a predicate), and it
20
+ # is often triggered.
21
+ #
22
+ # That's why we hardcode the following cases and check duplicable? instead of
23
+ # using that rescue idiom.
24
+ #++
25
+ class Object
26
+ # Can you safely dup this object?
27
+ #
28
+ # False for method objects;
29
+ # true otherwise.
30
+ def duplicable?
31
+ true
32
+ end
33
+ end
34
+
35
+ class NilClass
36
+ begin
37
+ nil.dup
38
+ rescue TypeError
39
+ # +nil+ is not duplicable:
40
+ #
41
+ # nil.duplicable? # => false
42
+ # nil.dup # => TypeError: can't dup NilClass
43
+ def duplicable?
44
+ false
45
+ end
46
+ end
47
+ end
48
+
49
+ class FalseClass
50
+ begin
51
+ false.dup
52
+ rescue TypeError
53
+ # +false+ is not duplicable:
54
+ #
55
+ # false.duplicable? # => false
56
+ # false.dup # => TypeError: can't dup FalseClass
57
+ def duplicable?
58
+ false
59
+ end
60
+ end
61
+ end
62
+
63
+ class TrueClass
64
+ begin
65
+ true.dup
66
+ rescue TypeError
67
+ # +true+ is not duplicable:
68
+ #
69
+ # true.duplicable? # => false
70
+ # true.dup # => TypeError: can't dup TrueClass
71
+ def duplicable?
72
+ false
73
+ end
74
+ end
75
+ end
76
+
77
+ class Symbol
78
+ begin
79
+ :symbol.dup # Ruby 2.4.x.
80
+ "symbol_from_string".to_sym.dup # Some symbols can't `dup` in Ruby 2.4.0.
81
+ rescue TypeError
82
+ # Symbols are not duplicable:
83
+ #
84
+ # :my_symbol.duplicable? # => false
85
+ # :my_symbol.dup # => TypeError: can't dup Symbol
86
+ def duplicable?
87
+ false
88
+ end
89
+ end
90
+ end
91
+
92
+ class Numeric
93
+ begin
94
+ 1.dup
95
+ rescue TypeError
96
+ # Numbers are not duplicable:
97
+ #
98
+ # 3.duplicable? # => false
99
+ # 3.dup # => TypeError: can't dup Integer
100
+ def duplicable?
101
+ false
102
+ end
103
+ end
104
+ end
105
+
106
+ require "bigdecimal"
107
+ class BigDecimal
108
+ # BigDecimals are duplicable:
109
+ #
110
+ # BigDecimal("1.2").duplicable? # => true
111
+ # BigDecimal("1.2").dup # => #<BigDecimal:...,'0.12E1',18(18)>
112
+ def duplicable?
113
+ true
114
+ end
115
+ end
116
+
117
+ class Method
118
+ # Methods are not duplicable:
119
+ #
120
+ # method(:puts).duplicable? # => false
121
+ # method(:puts).dup # => TypeError: allocator undefined for Method
122
+ def duplicable?
123
+ false
124
+ end
125
+ end
126
+
127
+ class Complex
128
+ begin
129
+ Complex(1).dup
130
+ rescue TypeError
131
+ # Complexes are not duplicable:
132
+ #
133
+ # Complex(1).duplicable? # => false
134
+ # Complex(1).dup # => TypeError: can't copy Complex
135
+ def duplicable?
136
+ false
137
+ end
138
+ end
139
+ end
140
+
141
+ class Rational
142
+ begin
143
+ Rational(1).dup
144
+ rescue TypeError
145
+ # Rationals are not duplicable:
146
+ #
147
+ # Rational(1).duplicable? # => false
148
+ # Rational(1).dup # => TypeError: can't copy Rational
149
+ def duplicable?
150
+ false
151
+ end
152
+ end
153
+ end
@@ -0,0 +1,45 @@
1
+ require "uri"
2
+
3
+ module Sentry
4
+ class DSN
5
+ attr_reader :scheme, :project_id, :public_key, :secret_key, :host, :port, :path
6
+
7
+ def initialize(dsn_string)
8
+ @raw_value = dsn_string
9
+
10
+ uri = URI.parse(dsn_string)
11
+ uri_path = uri.path.split('/')
12
+
13
+ if uri.user
14
+ # DSN-style string
15
+ @project_id = uri_path.pop
16
+ @public_key = uri.user
17
+ @secret_key = !(uri.password.nil? || uri.password.empty?) ? uri.password : nil
18
+ end
19
+
20
+ @scheme = uri.scheme
21
+ @host = uri.host
22
+ @port = uri.port if uri.port
23
+ @path = uri_path.join('/')
24
+ end
25
+
26
+ def valid?
27
+ %w(host path public_key project_id).all? { |k| public_send(k) }
28
+ end
29
+
30
+ def to_s
31
+ @raw_value
32
+ end
33
+
34
+ def server
35
+ server = "#{scheme}://#{host}"
36
+ server += ":#{port}" unless port == { 'http' => 80, 'https' => 443 }[scheme]
37
+ server += path
38
+ server
39
+ end
40
+
41
+ def envelope_endpoint
42
+ "#{path}/api/#{project_id}/envelope/"
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,175 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'socket'
4
+ require 'securerandom'
5
+ require 'sentry/event/options'
6
+ require 'sentry/interface'
7
+ require 'sentry/backtrace'
8
+ require 'sentry/utils/deep_merge'
9
+ require 'sentry/utils/real_ip'
10
+
11
+ module Sentry
12
+ class Event
13
+ ATTRIBUTES = %i(
14
+ event_id level timestamp
15
+ release environment server_name modules
16
+ message user tags contexts extra
17
+ fingerprint breadcrumbs backtrace transaction
18
+ platform sdk
19
+ )
20
+
21
+ attr_accessor(*ATTRIBUTES)
22
+ attr_reader :id, :configuration
23
+
24
+ alias event_id id
25
+
26
+ def initialize(options:, configuration:)
27
+ # this needs to go first because some setters rely on configuration
28
+ @configuration = configuration
29
+
30
+ # Set some simple default values
31
+ @id = SecureRandom.uuid.delete("-")
32
+ @timestamp = Time.now.utc
33
+ @platform = :ruby
34
+ @sdk = Sentry.sdk_meta
35
+
36
+ @user = options.user
37
+ @extra = options.extra
38
+ @contexts = options.contexts
39
+ @tags = configuration.tags.merge(options.tags)
40
+
41
+ @fingerprint = options.fingerprint
42
+
43
+ @server_name = options.server_name || configuration.server_name
44
+ @environment = options.environment || configuration.current_environment
45
+ @release = options.release || configuration.release
46
+ @modules = list_gem_specs if configuration.send_modules
47
+
48
+ @message = options.message if options.message
49
+
50
+ self.level = options.level
51
+
52
+ if !options.backtrace.empty?
53
+ @stacktrace = Sentry::StacktraceInterface.new.tap do |int|
54
+ int.frames = stacktrace_interface_from(options.backtrace)
55
+ end
56
+ end
57
+ end
58
+
59
+ class << self
60
+ def get_log_message(event_hash)
61
+ message = event_hash[:message] || event_hash['message']
62
+ message = get_message_from_exception(event_hash) if message.empty?
63
+ message = '<no message value>' if message.empty?
64
+ message
65
+ end
66
+
67
+ def get_message_from_exception(event_hash)
68
+ (
69
+ event_hash &&
70
+ event_hash[:exception] &&
71
+ event_hash[:exception][:values] &&
72
+ event_hash[:exception][:values][0] &&
73
+ "#{event_hash[:exception][:values][0][:type]}: #{event_hash[:exception][:values][0][:value]}"
74
+ )
75
+ end
76
+ end
77
+
78
+ def timestamp=(time)
79
+ @timestamp = time.is_a?(Time) ? time.strftime('%Y-%m-%dT%H:%M:%S') : time
80
+ end
81
+
82
+ def level=(new_level) # needed to meet the Sentry spec
83
+ @level = new_level.to_s == "warn" ? :warning : new_level
84
+ end
85
+
86
+ def rack_env=(env)
87
+ unless @request || env.empty?
88
+ @request = Sentry::RequestInterface.new.tap do |int|
89
+ int.from_rack(env)
90
+ end
91
+
92
+ if configuration.send_default_pii && ip = calculate_real_ip_from_rack(env.dup)
93
+ user[:ip_address] = ip
94
+ end
95
+ end
96
+ end
97
+
98
+ def to_hash
99
+ data = ATTRIBUTES.each_with_object({}) do |att, memo|
100
+ memo[att] = public_send(att) if public_send(att)
101
+ end
102
+
103
+ data[:breadcrumbs] = breadcrumbs.to_hash if breadcrumbs
104
+ data[:stacktrace] = @stacktrace.to_hash if @stacktrace
105
+ data[:request] = @request.to_hash if @request
106
+ data[:exception] = @exception.to_hash if @exception
107
+
108
+ data
109
+ end
110
+
111
+ def to_json_compatible
112
+ JSON.parse(JSON.generate(to_hash))
113
+ end
114
+
115
+ def add_exception_interface(exc)
116
+ @exception = Sentry::ExceptionInterface.new.tap do |exc_int|
117
+ exceptions = Sentry::Utils::ExceptionCauseChain.exception_to_array(exc).reverse
118
+ backtraces = Set.new
119
+ exc_int.values = exceptions.map do |e|
120
+ SingleExceptionInterface.new.tap do |int|
121
+ int.type = e.class.to_s
122
+ int.value = e.to_s
123
+ int.module = e.class.to_s.split('::')[0...-1].join('::')
124
+
125
+ int.stacktrace =
126
+ if e.backtrace && !backtraces.include?(e.backtrace.object_id)
127
+ backtraces << e.backtrace.object_id
128
+ StacktraceInterface.new.tap do |stacktrace|
129
+ stacktrace.frames = stacktrace_interface_from(e.backtrace)
130
+ end
131
+ end
132
+ end
133
+ end
134
+ end
135
+ end
136
+
137
+ def stacktrace_interface_from(backtrace)
138
+ project_root = configuration.project_root.to_s
139
+
140
+ Backtrace.parse(backtrace, configuration: configuration).lines.reverse.each_with_object([]) do |line, memo|
141
+ frame = StacktraceInterface::Frame.new(project_root)
142
+ frame.abs_path = line.file if line.file
143
+ frame.function = line.method if line.method
144
+ frame.lineno = line.number
145
+ frame.in_app = line.in_app
146
+ frame.module = line.module_name if line.module_name
147
+
148
+ if configuration[:context_lines] && frame.abs_path
149
+ frame.pre_context, frame.context_line, frame.post_context = \
150
+ configuration.linecache.get_file_context(frame.abs_path, frame.lineno, configuration[:context_lines])
151
+ end
152
+
153
+ memo << frame if frame.filename
154
+ end
155
+ end
156
+
157
+ private
158
+
159
+ # When behind a proxy (or if the user is using a proxy), we can't use
160
+ # REMOTE_ADDR to determine the Event IP, and must use other headers instead.
161
+ def calculate_real_ip_from_rack(env)
162
+ Utils::RealIp.new(
163
+ :remote_addr => env["REMOTE_ADDR"],
164
+ :client_ip => env["HTTP_CLIENT_IP"],
165
+ :real_ip => env["HTTP_X_REAL_IP"],
166
+ :forwarded_for => env["HTTP_X_FORWARDED_FOR"]
167
+ ).calculate_ip
168
+ end
169
+
170
+ def list_gem_specs
171
+ # Older versions of Rubygems don't support iterating over all specs
172
+ Hash[Gem::Specification.map { |spec| [spec.name, spec.version.to_s] }] if Gem::Specification.respond_to?(:map)
173
+ end
174
+ end
175
+ end
@@ -0,0 +1,31 @@
1
+ module Sentry
2
+ class Event
3
+ class Options
4
+ attr_reader :message,
5
+ :user, :extra, :tags, :contexts,
6
+ :backtrace, :level, :fingerprint,
7
+ :server_name, :release, :environment
8
+
9
+ def initialize(
10
+ message: "",
11
+ user: {}, extra: {}, tags: {}, contexts: {},
12
+ backtrace: [], level: :error, fingerprint: [],
13
+ # nilable attributes because we'll fallback to the configuration's values
14
+ server_name: nil, release: nil, environment: nil
15
+ )
16
+ @message = message || ""
17
+ @user = user || {}
18
+ @extra = extra || {}
19
+ @tags = tags || {}
20
+ @contexts = contexts || {}
21
+ @backtrace = backtrace || []
22
+ @fingerprint = fingerprint || []
23
+ @level = level || :error
24
+ @server_name = server_name
25
+ @environment = environment
26
+ @release = release
27
+ end
28
+ end
29
+ end
30
+ end
31
+