sentry-ruby 5.3.0 → 5.8.0

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