sentry-ruby 5.3.0 → 5.8.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +11 -0
  3. data/.rspec +2 -0
  4. data/.yardopts +2 -0
  5. data/CHANGELOG.md +313 -0
  6. data/CODE_OF_CONDUCT.md +74 -0
  7. data/Gemfile +31 -0
  8. data/Makefile +4 -0
  9. data/README.md +10 -6
  10. data/Rakefile +13 -0
  11. data/bin/console +18 -0
  12. data/bin/setup +8 -0
  13. data/lib/sentry/background_worker.rb +72 -0
  14. data/lib/sentry/backtrace.rb +124 -0
  15. data/lib/sentry/baggage.rb +81 -0
  16. data/lib/sentry/breadcrumb/sentry_logger.rb +90 -0
  17. data/lib/sentry/breadcrumb.rb +70 -0
  18. data/lib/sentry/breadcrumb_buffer.rb +64 -0
  19. data/lib/sentry/client.rb +207 -0
  20. data/lib/sentry/configuration.rb +543 -0
  21. data/lib/sentry/core_ext/object/deep_dup.rb +61 -0
  22. data/lib/sentry/core_ext/object/duplicable.rb +155 -0
  23. data/lib/sentry/dsn.rb +53 -0
  24. data/lib/sentry/envelope.rb +96 -0
  25. data/lib/sentry/error_event.rb +38 -0
  26. data/lib/sentry/event.rb +178 -0
  27. data/lib/sentry/exceptions.rb +9 -0
  28. data/lib/sentry/hub.rb +241 -0
  29. data/lib/sentry/integrable.rb +26 -0
  30. data/lib/sentry/interface.rb +16 -0
  31. data/lib/sentry/interfaces/exception.rb +43 -0
  32. data/lib/sentry/interfaces/request.rb +134 -0
  33. data/lib/sentry/interfaces/single_exception.rb +65 -0
  34. data/lib/sentry/interfaces/stacktrace.rb +87 -0
  35. data/lib/sentry/interfaces/stacktrace_builder.rb +79 -0
  36. data/lib/sentry/interfaces/threads.rb +42 -0
  37. data/lib/sentry/linecache.rb +47 -0
  38. data/lib/sentry/logger.rb +20 -0
  39. data/lib/sentry/net/http.rb +103 -0
  40. data/lib/sentry/rack/capture_exceptions.rb +82 -0
  41. data/lib/sentry/rack.rb +5 -0
  42. data/lib/sentry/rake.rb +41 -0
  43. data/lib/sentry/redis.rb +107 -0
  44. data/lib/sentry/release_detector.rb +39 -0
  45. data/lib/sentry/scope.rb +339 -0
  46. data/lib/sentry/session.rb +33 -0
  47. data/lib/sentry/session_flusher.rb +90 -0
  48. data/lib/sentry/span.rb +236 -0
  49. data/lib/sentry/test_helper.rb +78 -0
  50. data/lib/sentry/transaction.rb +345 -0
  51. data/lib/sentry/transaction_event.rb +53 -0
  52. data/lib/sentry/transport/configuration.rb +25 -0
  53. data/lib/sentry/transport/dummy_transport.rb +21 -0
  54. data/lib/sentry/transport/http_transport.rb +175 -0
  55. data/lib/sentry/transport.rb +214 -0
  56. data/lib/sentry/utils/argument_checking_helper.rb +13 -0
  57. data/lib/sentry/utils/custom_inspection.rb +14 -0
  58. data/lib/sentry/utils/encoding_helper.rb +22 -0
  59. data/lib/sentry/utils/exception_cause_chain.rb +20 -0
  60. data/lib/sentry/utils/logging_helper.rb +26 -0
  61. data/lib/sentry/utils/real_ip.rb +84 -0
  62. data/lib/sentry/utils/request_id.rb +18 -0
  63. data/lib/sentry/version.rb +5 -0
  64. data/lib/sentry-ruby.rb +511 -0
  65. data/sentry-ruby-core.gemspec +23 -0
  66. data/sentry-ruby.gemspec +24 -0
  67. metadata +66 -16
@@ -0,0 +1,155 @@
1
+ # frozen_string_literal: true
2
+
3
+ return if Object.method_defined?(:duplicable?)
4
+
5
+ #########################################
6
+ # This file was copied from Rails 5.2 #
7
+ #########################################
8
+
9
+ #--
10
+ # Most objects are cloneable, but not all. For example you can't dup methods:
11
+ #
12
+ # method(:puts).dup # => TypeError: allocator undefined for Method
13
+ #
14
+ # Classes may signal their instances are not duplicable removing +dup+/+clone+
15
+ # or raising exceptions from them. So, to dup an arbitrary object you normally
16
+ # use an optimistic approach and are ready to catch an exception, say:
17
+ #
18
+ # arbitrary_object.dup rescue object
19
+ #
20
+ # Rails dups objects in a few critical spots where they are not that arbitrary.
21
+ # That rescue is very expensive (like 40 times slower than a predicate), and it
22
+ # is often triggered.
23
+ #
24
+ # That's why we hardcode the following cases and check duplicable? instead of
25
+ # using that rescue idiom.
26
+ #++
27
+ class Object
28
+ # Can you safely dup this object?
29
+ #
30
+ # False for method objects;
31
+ # true otherwise.
32
+ def duplicable?
33
+ true
34
+ end
35
+ end
36
+
37
+ class NilClass
38
+ begin
39
+ nil.dup
40
+ rescue TypeError
41
+ # +nil+ is not duplicable:
42
+ #
43
+ # nil.duplicable? # => false
44
+ # nil.dup # => TypeError: can't dup NilClass
45
+ def duplicable?
46
+ false
47
+ end
48
+ end
49
+ end
50
+
51
+ class FalseClass
52
+ begin
53
+ false.dup
54
+ rescue TypeError
55
+ # +false+ is not duplicable:
56
+ #
57
+ # false.duplicable? # => false
58
+ # false.dup # => TypeError: can't dup FalseClass
59
+ def duplicable?
60
+ false
61
+ end
62
+ end
63
+ end
64
+
65
+ class TrueClass
66
+ begin
67
+ true.dup
68
+ rescue TypeError
69
+ # +true+ is not duplicable:
70
+ #
71
+ # true.duplicable? # => false
72
+ # true.dup # => TypeError: can't dup TrueClass
73
+ def duplicable?
74
+ false
75
+ end
76
+ end
77
+ end
78
+
79
+ class Symbol
80
+ begin
81
+ :symbol.dup # Ruby 2.4.x.
82
+ "symbol_from_string".to_sym.dup # Some symbols can't `dup` in Ruby 2.4.0.
83
+ rescue TypeError
84
+ # Symbols are not duplicable:
85
+ #
86
+ # :my_symbol.duplicable? # => false
87
+ # :my_symbol.dup # => TypeError: can't dup Symbol
88
+ def duplicable?
89
+ false
90
+ end
91
+ end
92
+ end
93
+
94
+ class Numeric
95
+ begin
96
+ 1.dup
97
+ rescue TypeError
98
+ # Numbers are not duplicable:
99
+ #
100
+ # 3.duplicable? # => false
101
+ # 3.dup # => TypeError: can't dup Integer
102
+ def duplicable?
103
+ false
104
+ end
105
+ end
106
+ end
107
+
108
+ require "bigdecimal"
109
+ class BigDecimal
110
+ # BigDecimals are duplicable:
111
+ #
112
+ # BigDecimal("1.2").duplicable? # => true
113
+ # BigDecimal("1.2").dup # => #<BigDecimal:...,'0.12E1',18(18)>
114
+ def duplicable?
115
+ true
116
+ end
117
+ end
118
+
119
+ class Method
120
+ # Methods are not duplicable:
121
+ #
122
+ # method(:puts).duplicable? # => false
123
+ # method(:puts).dup # => TypeError: allocator undefined for Method
124
+ def duplicable?
125
+ false
126
+ end
127
+ end
128
+
129
+ class Complex
130
+ begin
131
+ Complex(1).dup
132
+ rescue TypeError
133
+ # Complexes are not duplicable:
134
+ #
135
+ # Complex(1).duplicable? # => false
136
+ # Complex(1).dup # => TypeError: can't copy Complex
137
+ def duplicable?
138
+ false
139
+ end
140
+ end
141
+ end
142
+
143
+ class Rational
144
+ begin
145
+ Rational(1).dup
146
+ rescue TypeError
147
+ # Rationals are not duplicable:
148
+ #
149
+ # Rational(1).duplicable? # => false
150
+ # Rational(1).dup # => TypeError: can't copy Rational
151
+ def duplicable?
152
+ false
153
+ end
154
+ end
155
+ end
data/lib/sentry/dsn.rb ADDED
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+
5
+ module Sentry
6
+ class DSN
7
+ PORT_MAP = { 'http' => 80, 'https' => 443 }.freeze
8
+ REQUIRED_ATTRIBUTES = %w(host path public_key project_id).freeze
9
+
10
+ attr_reader :scheme, :secret_key, :port, *REQUIRED_ATTRIBUTES
11
+
12
+ def initialize(dsn_string)
13
+ @raw_value = dsn_string
14
+
15
+ uri = URI.parse(dsn_string)
16
+ uri_path = uri.path.split('/')
17
+
18
+ if uri.user
19
+ # DSN-style string
20
+ @project_id = uri_path.pop
21
+ @public_key = uri.user
22
+ @secret_key = !(uri.password.nil? || uri.password.empty?) ? uri.password : nil
23
+ end
24
+
25
+ @scheme = uri.scheme
26
+ @host = uri.host
27
+ @port = uri.port if uri.port
28
+ @path = uri_path.join('/')
29
+ end
30
+
31
+ def valid?
32
+ REQUIRED_ATTRIBUTES.all? { |k| public_send(k) }
33
+ end
34
+
35
+ def to_s
36
+ @raw_value
37
+ end
38
+
39
+ def server
40
+ server = "#{scheme}://#{host}"
41
+ server += ":#{port}" unless port == PORT_MAP[scheme]
42
+ server
43
+ end
44
+
45
+ def csp_report_uri
46
+ "#{server}/api/#{project_id}/security/?sentry_key=#{public_key}"
47
+ end
48
+
49
+ def envelope_endpoint
50
+ "#{path}/api/#{project_id}/envelope/"
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sentry
4
+ # @api private
5
+ class Envelope
6
+ class Item
7
+ STACKTRACE_FRAME_LIMIT_ON_OVERSIZED_PAYLOAD = 500
8
+ MAX_SERIALIZED_PAYLOAD_SIZE = 1024 * 200
9
+
10
+ attr_accessor :headers, :payload
11
+
12
+ def initialize(headers, payload)
13
+ @headers = headers
14
+ @payload = payload
15
+ end
16
+
17
+ def type
18
+ @headers[:type] || 'event'
19
+ end
20
+
21
+ def to_s
22
+ <<~ITEM
23
+ #{JSON.generate(@headers)}
24
+ #{JSON.generate(@payload)}
25
+ ITEM
26
+ end
27
+
28
+ def serialize
29
+ result = to_s
30
+
31
+ if result.bytesize > MAX_SERIALIZED_PAYLOAD_SIZE
32
+ remove_breadcrumbs!
33
+ result = to_s
34
+ end
35
+
36
+ if result.bytesize > MAX_SERIALIZED_PAYLOAD_SIZE
37
+ reduce_stacktrace!
38
+ result = to_s
39
+ end
40
+
41
+ [result, result.bytesize > MAX_SERIALIZED_PAYLOAD_SIZE]
42
+ end
43
+
44
+ def size_breakdown
45
+ payload.map do |key, value|
46
+ "#{key}: #{JSON.generate(value).bytesize}"
47
+ end.join(", ")
48
+ end
49
+
50
+ private
51
+
52
+ def remove_breadcrumbs!
53
+ if payload.key?(:breadcrumbs)
54
+ payload.delete(:breadcrumbs)
55
+ elsif payload.key?("breadcrumbs")
56
+ payload.delete("breadcrumbs")
57
+ end
58
+ end
59
+
60
+ def reduce_stacktrace!
61
+ if exceptions = payload.dig(:exception, :values) || payload.dig("exception", "values")
62
+ exceptions.each do |exception|
63
+ # in most cases there is only one exception (2 or 3 when have multiple causes), so we won't loop through this double condition much
64
+ traces = exception.dig(:stacktrace, :frames) || exception.dig("stacktrace", "frames")
65
+
66
+ if traces && traces.size > STACKTRACE_FRAME_LIMIT_ON_OVERSIZED_PAYLOAD
67
+ size_on_both_ends = STACKTRACE_FRAME_LIMIT_ON_OVERSIZED_PAYLOAD / 2
68
+ traces.replace(
69
+ traces[0..(size_on_both_ends - 1)] + traces[-size_on_both_ends..-1],
70
+ )
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
76
+
77
+ attr_accessor :headers, :items
78
+
79
+ def initialize(headers = {})
80
+ @headers = headers
81
+ @items = []
82
+ end
83
+
84
+ def add_item(headers, payload)
85
+ @items << Item.new(headers, payload)
86
+ end
87
+
88
+ def item_types
89
+ @items.map(&:type)
90
+ end
91
+
92
+ def event_id
93
+ @headers[:event_id]
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sentry
4
+ # ErrorEvent represents error or normal message events.
5
+ class ErrorEvent < Event
6
+ # @return [ExceptionInterface]
7
+ attr_reader :exception
8
+
9
+ # @return [ThreadsInterface]
10
+ attr_reader :threads
11
+
12
+ # @return [Hash]
13
+ def to_hash
14
+ data = super
15
+ data[:threads] = threads.to_hash if threads
16
+ data[:exception] = exception.to_hash if exception
17
+ data
18
+ end
19
+
20
+ # @!visibility private
21
+ def add_threads_interface(backtrace: nil, **options)
22
+ @threads = ThreadsInterface.build(
23
+ backtrace: backtrace,
24
+ stacktrace_builder: @stacktrace_builder,
25
+ **options
26
+ )
27
+ end
28
+
29
+ # @!visibility private
30
+ def add_exception_interface(exception)
31
+ if exception.respond_to?(:sentry_context)
32
+ @extra.merge!(exception.sentry_context)
33
+ end
34
+
35
+ @exception = Sentry::ExceptionInterface.build(exception: exception, stacktrace_builder: @stacktrace_builder)
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,178 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'socket'
4
+ require 'securerandom'
5
+ require 'sentry/interface'
6
+ require 'sentry/backtrace'
7
+ require 'sentry/utils/real_ip'
8
+ require 'sentry/utils/request_id'
9
+ require 'sentry/utils/custom_inspection'
10
+
11
+ module Sentry
12
+ # This is an abstract class that defines the shared attributes of an event.
13
+ # Please don't use it directly. The user-facing classes are its child classes.
14
+ class Event
15
+ TYPE = "event"
16
+ # These are readable attributes.
17
+ SERIALIZEABLE_ATTRIBUTES = %i(
18
+ event_id level timestamp
19
+ release environment server_name modules
20
+ message user tags contexts extra
21
+ fingerprint breadcrumbs transaction transaction_info
22
+ platform sdk type
23
+ )
24
+
25
+ # These are writable attributes.
26
+ WRITER_ATTRIBUTES = SERIALIZEABLE_ATTRIBUTES - %i(type timestamp level)
27
+
28
+ MAX_MESSAGE_SIZE_IN_BYTES = 1024 * 8
29
+
30
+ SKIP_INSPECTION_ATTRIBUTES = [:@modules, :@stacktrace_builder, :@send_default_pii, :@trusted_proxies, :@rack_env_whitelist]
31
+
32
+ include CustomInspection
33
+
34
+ attr_writer(*WRITER_ATTRIBUTES)
35
+ attr_reader(*SERIALIZEABLE_ATTRIBUTES)
36
+
37
+ # @return [RequestInterface]
38
+ attr_reader :request
39
+
40
+ # @param configuration [Configuration]
41
+ # @param integration_meta [Hash, nil]
42
+ # @param message [String, nil]
43
+ def initialize(configuration:, integration_meta: nil, message: nil)
44
+ # Set some simple default values
45
+ @event_id = SecureRandom.uuid.delete("-")
46
+ @timestamp = Sentry.utc_now.iso8601
47
+ @platform = :ruby
48
+ @type = self.class::TYPE
49
+ @sdk = integration_meta || Sentry.sdk_meta
50
+
51
+ @user = {}
52
+ @extra = {}
53
+ @contexts = {}
54
+ @tags = {}
55
+
56
+ @fingerprint = []
57
+
58
+ # configuration data that's directly used by events
59
+ @server_name = configuration.server_name
60
+ @environment = configuration.environment
61
+ @release = configuration.release
62
+ @modules = configuration.gem_specs if configuration.send_modules
63
+
64
+ # configuration options to help events process data
65
+ @send_default_pii = configuration.send_default_pii
66
+ @trusted_proxies = configuration.trusted_proxies
67
+ @stacktrace_builder = configuration.stacktrace_builder
68
+ @rack_env_whitelist = configuration.rack_env_whitelist
69
+
70
+ @message = (message || "").byteslice(0..MAX_MESSAGE_SIZE_IN_BYTES)
71
+ end
72
+
73
+ class << self
74
+ # @!visibility private
75
+ def get_log_message(event_hash)
76
+ message = event_hash[:message] || event_hash['message']
77
+
78
+ return message unless message.nil? || message.empty?
79
+
80
+ message = get_message_from_exception(event_hash)
81
+
82
+ return message unless message.nil? || message.empty?
83
+
84
+ message = event_hash[:transaction] || event_hash["transaction"]
85
+
86
+ return message unless message.nil? || message.empty?
87
+
88
+ '<no message value>'
89
+ end
90
+
91
+ # @!visibility private
92
+ def get_message_from_exception(event_hash)
93
+ if exception = event_hash.dig(:exception, :values, 0)
94
+ "#{exception[:type]}: #{exception[:value]}"
95
+ elsif exception = event_hash.dig("exception", "values", 0)
96
+ "#{exception["type"]}: #{exception["value"]}"
97
+ end
98
+ end
99
+ end
100
+
101
+ # @deprecated This method will be removed in v5.0.0. Please just use Sentry.configuration
102
+ # @return [Configuration]
103
+ def configuration
104
+ Sentry.configuration
105
+ end
106
+
107
+ # Sets the event's timestamp.
108
+ # @param time [Time, Float]
109
+ # @return [void]
110
+ def timestamp=(time)
111
+ @timestamp = time.is_a?(Time) ? time.to_f : time
112
+ end
113
+
114
+ # Sets the event's level.
115
+ # @param level [String, Symbol]
116
+ # @return [void]
117
+ def level=(level) # needed to meet the Sentry spec
118
+ @level = level.to_s == "warn" ? :warning : level
119
+ end
120
+
121
+ # Sets the event's request environment data with RequestInterface.
122
+ # @see RequestInterface
123
+ # @param env [Hash]
124
+ # @return [void]
125
+ def rack_env=(env)
126
+ unless request || env.empty?
127
+ add_request_interface(env)
128
+
129
+ if @send_default_pii
130
+ user[:ip_address] = calculate_real_ip_from_rack(env)
131
+ end
132
+
133
+ if request_id = Utils::RequestId.read_from(env)
134
+ tags[:request_id] = request_id
135
+ end
136
+ end
137
+ end
138
+
139
+ # @return [Hash]
140
+ def to_hash
141
+ data = serialize_attributes
142
+ data[:breadcrumbs] = breadcrumbs.to_hash if breadcrumbs
143
+ data[:request] = request.to_hash if request
144
+ data
145
+ end
146
+
147
+ # @return [Hash]
148
+ def to_json_compatible
149
+ JSON.parse(JSON.generate(to_hash))
150
+ end
151
+
152
+ private
153
+
154
+ def add_request_interface(env)
155
+ @request = Sentry::RequestInterface.new(env: env, send_default_pii: @send_default_pii, rack_env_whitelist: @rack_env_whitelist)
156
+ end
157
+
158
+ def serialize_attributes
159
+ self.class::SERIALIZEABLE_ATTRIBUTES.each_with_object({}) do |att, memo|
160
+ if value = public_send(att)
161
+ memo[att] = value
162
+ end
163
+ end
164
+ end
165
+
166
+ # When behind a proxy (or if the user is using a proxy), we can't use
167
+ # REMOTE_ADDR to determine the Event IP, and must use other headers instead.
168
+ def calculate_real_ip_from_rack(env)
169
+ Utils::RealIp.new(
170
+ :remote_addr => env["REMOTE_ADDR"],
171
+ :client_ip => env["HTTP_CLIENT_IP"],
172
+ :real_ip => env["HTTP_X_REAL_IP"],
173
+ :forwarded_for => env["HTTP_X_FORWARDED_FOR"],
174
+ :trusted_proxies => @trusted_proxies
175
+ ).calculate_ip
176
+ end
177
+ end
178
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sentry
4
+ class Error < StandardError
5
+ end
6
+
7
+ class ExternalError < Error
8
+ end
9
+ end