sentry-ruby-core 4.8.1 → 4.9.1

Sign up to get free protection for your applications and to get access to all the features.
data/lib/sentry/event.rb CHANGED
@@ -10,31 +10,40 @@ require 'sentry/utils/custom_inspection'
10
10
 
11
11
  module Sentry
12
12
  class Event
13
+ # These are readable attributes.
13
14
  SERIALIZEABLE_ATTRIBUTES = %i(
14
15
  event_id level timestamp
15
16
  release environment server_name modules
16
17
  message user tags contexts extra
17
- fingerprint breadcrumbs backtrace transaction
18
+ fingerprint breadcrumbs transaction
18
19
  platform sdk type
19
20
  )
20
21
 
22
+ # These are writable attributes.
21
23
  WRITER_ATTRIBUTES = SERIALIZEABLE_ATTRIBUTES - %i(type timestamp level)
22
24
 
23
25
  MAX_MESSAGE_SIZE_IN_BYTES = 1024 * 8
24
26
 
25
- SKIP_INSPECTION_ATTRIBUTES = [:@configuration, :@modules, :@backtrace]
27
+ SKIP_INSPECTION_ATTRIBUTES = [:@modules, :@stacktrace_builder, :@send_default_pii, :@trusted_proxies, :@rack_env_whitelist]
26
28
 
27
29
  include CustomInspection
28
30
 
29
31
  attr_writer(*WRITER_ATTRIBUTES)
30
32
  attr_reader(*SERIALIZEABLE_ATTRIBUTES)
31
33
 
32
- attr_reader :configuration, :request, :exception, :threads
34
+ # @return [RequestInterface]
35
+ attr_reader :request
33
36
 
34
- def initialize(configuration:, integration_meta: nil, message: nil)
35
- # this needs to go first because some setters rely on configuration
36
- @configuration = configuration
37
+ # @return [ExceptionInterface]
38
+ attr_reader :exception
39
+
40
+ # @return [ThreadsInterface]
41
+ attr_reader :threads
37
42
 
43
+ # @param configuration [Configuration]
44
+ # @param integration_meta [Hash, nil]
45
+ # @param message [String, nil]
46
+ def initialize(configuration:, integration_meta: nil, message: nil)
38
47
  # Set some simple default values
39
48
  @event_id = SecureRandom.uuid.delete("-")
40
49
  @timestamp = Sentry.utc_now.iso8601
@@ -48,17 +57,25 @@ module Sentry
48
57
 
49
58
  @fingerprint = []
50
59
 
60
+ # configuration data that's directly used by events
51
61
  @server_name = configuration.server_name
52
62
  @environment = configuration.environment
53
63
  @release = configuration.release
54
64
  @modules = configuration.gem_specs if configuration.send_modules
55
65
 
66
+ # configuration options to help events process data
67
+ @send_default_pii = configuration.send_default_pii
68
+ @trusted_proxies = configuration.trusted_proxies
69
+ @stacktrace_builder = configuration.stacktrace_builder
70
+ @rack_env_whitelist = configuration.rack_env_whitelist
71
+
56
72
  @message = (message || "").byteslice(0..MAX_MESSAGE_SIZE_IN_BYTES)
57
73
 
58
74
  self.level = :error
59
75
  end
60
76
 
61
77
  class << self
78
+ # @!visibility private
62
79
  def get_log_message(event_hash)
63
80
  message = event_hash[:message] || event_hash['message']
64
81
 
@@ -75,6 +92,7 @@ module Sentry
75
92
  '<no message value>'
76
93
  end
77
94
 
95
+ # @!visibility private
78
96
  def get_message_from_exception(event_hash)
79
97
  if exception = event_hash.dig(:exception, :values, 0)
80
98
  "#{exception[:type]}: #{exception[:value]}"
@@ -84,21 +102,35 @@ module Sentry
84
102
  end
85
103
  end
86
104
 
105
+ # @deprecated This method will be removed in v5.0.0. Please just use Sentry.configuration
106
+ # @return [Configuration]
107
+ def configuration
108
+ Sentry.configuration
109
+ end
110
+
111
+ # Sets the event's timestamp.
112
+ # @param time [Time, Float]
113
+ # @return [void]
87
114
  def timestamp=(time)
88
115
  @timestamp = time.is_a?(Time) ? time.to_f : time
89
116
  end
90
117
 
91
- def level=(new_level) # needed to meet the Sentry spec
92
- @level = new_level.to_s == "warn" ? :warning : new_level
118
+ # Sets the event's level.
119
+ # @param level [String, Symbol]
120
+ # @return [void]
121
+ def level=(level) # needed to meet the Sentry spec
122
+ @level = level.to_s == "warn" ? :warning : level
93
123
  end
94
124
 
125
+ # Sets the event's request environment data with RequestInterface.
126
+ # @see RequestInterface
127
+ # @param env [Hash]
128
+ # @return [void]
95
129
  def rack_env=(env)
96
130
  unless request || env.empty?
97
- env = env.dup
98
-
99
131
  add_request_interface(env)
100
132
 
101
- if configuration.send_default_pii
133
+ if @send_default_pii
102
134
  user[:ip_address] = calculate_real_ip_from_rack(env)
103
135
  end
104
136
 
@@ -108,6 +140,7 @@ module Sentry
108
140
  end
109
141
  end
110
142
 
143
+ # @return [Hash]
111
144
  def to_hash
112
145
  data = serialize_attributes
113
146
  data[:breadcrumbs] = breadcrumbs.to_hash if breadcrumbs
@@ -118,32 +151,35 @@ module Sentry
118
151
  data
119
152
  end
120
153
 
154
+ # @return [Hash]
121
155
  def to_json_compatible
122
156
  JSON.parse(JSON.generate(to_hash))
123
157
  end
124
158
 
125
- def add_request_interface(env)
126
- @request = Sentry::RequestInterface.build(env: env)
127
- end
128
-
159
+ # @!visibility private
129
160
  def add_threads_interface(backtrace: nil, **options)
130
161
  @threads = ThreadsInterface.build(
131
162
  backtrace: backtrace,
132
- stacktrace_builder: configuration.stacktrace_builder,
163
+ stacktrace_builder: @stacktrace_builder,
133
164
  **options
134
165
  )
135
166
  end
136
167
 
168
+ # @!visibility private
137
169
  def add_exception_interface(exception)
138
170
  if exception.respond_to?(:sentry_context)
139
171
  @extra.merge!(exception.sentry_context)
140
172
  end
141
173
 
142
- @exception = Sentry::ExceptionInterface.build(exception: exception, stacktrace_builder: configuration.stacktrace_builder)
174
+ @exception = Sentry::ExceptionInterface.build(exception: exception, stacktrace_builder: @stacktrace_builder)
143
175
  end
144
176
 
145
177
  private
146
178
 
179
+ def add_request_interface(env)
180
+ @request = Sentry::RequestInterface.new(env: env, send_default_pii: @send_default_pii, rack_env_whitelist: @rack_env_whitelist)
181
+ end
182
+
147
183
  def serialize_attributes
148
184
  self.class::SERIALIZEABLE_ATTRIBUTES.each_with_object({}) do |att, memo|
149
185
  if value = public_send(att)
@@ -160,7 +196,7 @@ module Sentry
160
196
  :client_ip => env["HTTP_CLIENT_IP"],
161
197
  :real_ip => env["HTTP_X_REAL_IP"],
162
198
  :forwarded_for => env["HTTP_X_FORWARDED_FOR"],
163
- :trusted_proxies => configuration.trusted_proxies
199
+ :trusted_proxies => @trusted_proxies
164
200
  ).calculate_ip
165
201
  end
166
202
  end
data/lib/sentry/hub.rb CHANGED
@@ -114,6 +114,9 @@ module Sentry
114
114
  options[:hint][:message] = message
115
115
  backtrace = options.delete(:backtrace)
116
116
  event = current_client.event_from_message(message, options[:hint], backtrace: backtrace)
117
+
118
+ return unless event
119
+
117
120
  capture_event(event, **options, &block)
118
121
  end
119
122
 
@@ -2,16 +2,7 @@
2
2
 
3
3
  module Sentry
4
4
  class Interface
5
- def self.inherited(klass)
6
- name = klass.name.split("::").last.downcase.gsub("interface", "")
7
- registered[name.to_sym] = klass
8
- super
9
- end
10
-
11
- def self.registered
12
- @@registered ||= {} # rubocop:disable Style/ClassVars
13
- end
14
-
5
+ # @return [Hash]
15
6
  def to_hash
16
7
  Hash[instance_variables.map { |name| [name[1..-1].to_sym, instance_variable_get(name)] }]
17
8
  end
@@ -2,16 +2,24 @@
2
2
 
3
3
  module Sentry
4
4
  class ExceptionInterface < Interface
5
- def initialize(values:)
6
- @values = values
5
+ # @param exceptions [Array<SingleExceptionInterface>]
6
+ def initialize(exceptions:)
7
+ @values = exceptions
7
8
  end
8
9
 
10
+ # @return [Hash]
9
11
  def to_hash
10
12
  data = super
11
13
  data[:values] = data[:values].map(&:to_hash) if data[:values]
12
14
  data
13
15
  end
14
16
 
17
+ # Builds ExceptionInterface with given exception and stacktrace_builder.
18
+ # @param exception [Exception]
19
+ # @param stacktrace_builder [StacktraceBuilder]
20
+ # @see SingleExceptionInterface#build_with_stacktrace
21
+ # @see SingleExceptionInterface#initialize
22
+ # @return [ExceptionInterface]
15
23
  def self.build(exception:, stacktrace_builder:)
16
24
  exceptions = Sentry::Utils::ExceptionCauseChain.exception_to_array(exception).reverse
17
25
  processed_backtrace_ids = Set.new
@@ -25,7 +33,7 @@ module Sentry
25
33
  end
26
34
  end
27
35
 
28
- new(values: exceptions)
36
+ new(exceptions: exceptions)
29
37
  end
30
38
  end
31
39
  end
@@ -15,29 +15,45 @@ module Sentry
15
15
  # https://github.com/getsentry/sentry/blob/master/src/sentry/conf/server.py
16
16
  MAX_BODY_LIMIT = 4096 * 4
17
17
 
18
- attr_accessor :url, :method, :data, :query_string, :cookies, :headers, :env
18
+ # @return [String]
19
+ attr_accessor :url
19
20
 
20
- def self.build(env:)
21
- env = clean_env(env)
22
- request = ::Rack::Request.new(env)
23
- self.new(request: request)
24
- end
21
+ # @return [String]
22
+ attr_accessor :method
23
+
24
+ # @return [Hash]
25
+ attr_accessor :data
26
+
27
+ # @return [String]
28
+ attr_accessor :query_string
29
+
30
+ # @return [String]
31
+ attr_accessor :cookies
32
+
33
+ # @return [Hash]
34
+ attr_accessor :headers
25
35
 
26
- def self.clean_env(env)
27
- unless Sentry.configuration.send_default_pii
36
+ # @return [Hash]
37
+ attr_accessor :env
38
+
39
+ # @param env [Hash]
40
+ # @param send_default_pii [Boolean]
41
+ # @param rack_env_whitelist [Array]
42
+ # @see Configuration#send_default_pii
43
+ # @see Configuration#rack_env_whitelist
44
+ def initialize(env:, send_default_pii:, rack_env_whitelist:)
45
+ env = env.dup
46
+
47
+ unless send_default_pii
28
48
  # need to completely wipe out ip addresses
29
49
  RequestInterface::IP_HEADERS.each do |header|
30
50
  env.delete(header)
31
51
  end
32
52
  end
33
53
 
34
- env
35
- end
36
-
37
- def initialize(request:)
38
- env = request.env
54
+ request = ::Rack::Request.new(env)
39
55
 
40
- if Sentry.configuration.send_default_pii
56
+ if send_default_pii
41
57
  self.data = read_data_from(request)
42
58
  self.cookies = request.cookies
43
59
  self.query_string = request.query_string
@@ -47,7 +63,7 @@ module Sentry
47
63
  self.method = request.request_method
48
64
 
49
65
  self.headers = filter_and_format_headers(env)
50
- self.env = filter_and_format_env(env)
66
+ self.env = filter_and_format_env(env, rack_env_whitelist)
51
67
  end
52
68
 
53
69
  private
@@ -116,11 +132,11 @@ module Sentry
116
132
  key == 'HTTP_VERSION' && value == protocol_version
117
133
  end
118
134
 
119
- def filter_and_format_env(env)
120
- return env if Sentry.configuration.rack_env_whitelist.empty?
135
+ def filter_and_format_env(env, rack_env_whitelist)
136
+ return env if rack_env_whitelist.empty?
121
137
 
122
138
  env.select do |k, _v|
123
- Sentry.configuration.rack_env_whitelist.include? k.to_s
139
+ rack_env_whitelist.include? k.to_s
124
140
  end
125
141
  end
126
142
  end
@@ -2,16 +2,20 @@
2
2
 
3
3
  module Sentry
4
4
  class StacktraceInterface
5
+ # @return [<Array[Frame]>]
5
6
  attr_reader :frames
6
7
 
8
+ # @param frames [<Array[Frame]>]
7
9
  def initialize(frames:)
8
10
  @frames = frames
9
11
  end
10
12
 
13
+ # @return [Hash]
11
14
  def to_hash
12
15
  { frames: @frames.map(&:to_hash) }
13
16
  end
14
17
 
18
+ # @return [String]
15
19
  def inspect
16
20
  @frames.map(&:to_s)
17
21
  end
@@ -2,8 +2,31 @@
2
2
 
3
3
  module Sentry
4
4
  class StacktraceBuilder
5
- attr_reader :project_root, :app_dirs_pattern, :linecache, :context_lines, :backtrace_cleanup_callback
5
+ # @return [String]
6
+ attr_reader :project_root
6
7
 
8
+ # @return [Regexp, nil]
9
+ attr_reader :app_dirs_pattern
10
+
11
+ # @return [LineCache]
12
+ attr_reader :linecache
13
+
14
+ # @return [Integer, nil]
15
+ attr_reader :context_lines
16
+
17
+ # @return [Proc, nil]
18
+ attr_reader :backtrace_cleanup_callback
19
+
20
+ # @param project_root [String]
21
+ # @param app_dirs_pattern [Regexp, nil]
22
+ # @param linecache [LineCache]
23
+ # @param context_lines [Integer, nil]
24
+ # @param backtrace_cleanup_callback [Proc, nil]
25
+ # @see Configuration#project_root
26
+ # @see Configuration#app_dirs_pattern
27
+ # @see Configuration#linecache
28
+ # @see Configuration#context_lines
29
+ # @see Configuration#backtrace_cleanup_callback
7
30
  def initialize(project_root:, app_dirs_pattern:, linecache:, context_lines:, backtrace_cleanup_callback: nil)
8
31
  @project_root = project_root
9
32
  @app_dirs_pattern = app_dirs_pattern
@@ -12,17 +35,21 @@ module Sentry
12
35
  @backtrace_cleanup_callback = backtrace_cleanup_callback
13
36
  end
14
37
 
15
- # you can pass a block to customize/exclude frames:
38
+ # Generates a StacktraceInterface with the given backtrace.
39
+ # You can pass a block to customize/exclude frames:
16
40
  #
17
- # ```ruby
18
- # builder.build(backtrace) do |frame|
19
- # if frame.module.match?(/a_gem/)
20
- # nil
21
- # else
22
- # frame
41
+ # @example
42
+ # builder.build(backtrace) do |frame|
43
+ # if frame.module.match?(/a_gem/)
44
+ # nil
45
+ # else
46
+ # frame
47
+ # end
23
48
  # end
24
- # end
25
- # ```
49
+ # @param backtrace [Array<String>]
50
+ # @param frame_callback [Proc]
51
+ # @yieldparam frame [StacktraceInterface::Frame]
52
+ # @return [StacktraceInterface]
26
53
  def build(backtrace:, &frame_callback)
27
54
  parsed_lines = parse_backtrace_lines(backtrace).select(&:file)
28
55
 
@@ -2,6 +2,8 @@
2
2
 
3
3
  module Sentry
4
4
  class ThreadsInterface
5
+ # @param crashed [Boolean]
6
+ # @param stacktrace [Array]
5
7
  def initialize(crashed: false, stacktrace: nil)
6
8
  @id = Thread.current.object_id
7
9
  @name = Thread.current.name
@@ -10,6 +12,7 @@ module Sentry
10
12
  @stacktrace = stacktrace
11
13
  end
12
14
 
15
+ # @return [Hash]
13
16
  def to_hash
14
17
  {
15
18
  values: [
@@ -24,8 +27,13 @@ module Sentry
24
27
  }
25
28
  end
26
29
 
27
- # patch this method if you want to change a threads interface's stacktrace frames
28
- # also see `StacktraceBuilder.build`.
30
+ # Builds the ThreadsInterface with given backtrace and stacktrace_builder.
31
+ # Patch this method if you want to change a threads interface's stacktrace frames.
32
+ # @see StacktraceBuilder.build
33
+ # @param backtrace [Array]
34
+ # @param stacktrace_builder [StacktraceBuilder]
35
+ # @param crashed [Hash]
36
+ # @return [ThreadsInterface]
29
37
  def self.build(backtrace:, stacktrace_builder:, **options)
30
38
  stacktrace = stacktrace_builder.build(backtrace: backtrace) if backtrace
31
39
  new(**options, stacktrace: stacktrace)
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Sentry
4
+ # @api private
4
5
  class LineCache
5
6
  def initialize
6
7
  @cache = {}
@@ -3,6 +3,7 @@
3
3
  require "net/http"
4
4
 
5
5
  module Sentry
6
+ # @api private
6
7
  module Net
7
8
  module HTTP
8
9
  OP_NAME = "net.http"
@@ -22,90 +23,67 @@ module Sentry
22
23
  # end
23
24
  # ```
24
25
  #
25
- # So when the entire flow looks like this:
26
- #
27
- # 1. #request is called.
28
- # - But because the request hasn't started yet, it calls #start (which then calls #do_start)
29
- # - At this moment @sentry_span is still nil, so #set_sentry_trace_header returns early
30
- # 2. #do_start then creates a new Span and assigns it to @sentry_span
31
- # 3. #request is called for the second time.
32
- # - This time @sentry_span should present. So #set_sentry_trace_header will set the sentry-trace header on the request object
33
- # 4. Once the request finished, it
34
- # - Records a breadcrumb if http_logger is set
35
- # - Finishes the Span inside @sentry_span and clears the instance variable
36
- #
26
+ # So we're only instrumenting request when `Net::HTTP` is already started
37
27
  def request(req, body = nil, &block)
38
- set_sentry_trace_header(req)
28
+ return super unless started?
29
+
30
+ sentry_span = start_sentry_span
31
+ set_sentry_trace_header(req, sentry_span)
39
32
 
40
33
  super.tap do |res|
41
34
  record_sentry_breadcrumb(req, res)
42
- record_sentry_span(req, res)
43
- end
44
- end
45
-
46
- def do_start
47
- super.tap do
48
- start_sentry_span
49
- end
50
- end
51
-
52
- def do_finish
53
- super.tap do
54
- finish_sentry_span
35
+ record_sentry_span(req, res, sentry_span)
55
36
  end
56
37
  end
57
38
 
58
39
  private
59
40
 
60
- def set_sentry_trace_header(req)
61
- return unless @sentry_span
41
+ def set_sentry_trace_header(req, sentry_span)
42
+ return unless sentry_span
62
43
 
63
- trace = Sentry.get_current_client.generate_sentry_trace(@sentry_span)
44
+ trace = Sentry.get_current_client.generate_sentry_trace(sentry_span)
64
45
  req[SENTRY_TRACE_HEADER_NAME] = trace if trace
65
46
  end
66
47
 
67
48
  def record_sentry_breadcrumb(req, res)
68
- if Sentry.initialized? && Sentry.configuration.breadcrumbs_logger.include?(:http_logger)
69
- return if from_sentry_sdk?
70
-
71
- request_info = extract_request_info(req)
72
- crumb = Sentry::Breadcrumb.new(
73
- level: :info,
74
- category: OP_NAME,
75
- type: :info,
76
- data: {
77
- method: request_info[:method],
78
- url: request_info[:url],
79
- status: res.code.to_i
80
- }
81
- )
82
- Sentry.add_breadcrumb(crumb)
83
- end
49
+ return unless Sentry.initialized? && Sentry.configuration.breadcrumbs_logger.include?(:http_logger)
50
+ return if from_sentry_sdk?
51
+
52
+ request_info = extract_request_info(req)
53
+
54
+ crumb = Sentry::Breadcrumb.new(
55
+ level: :info,
56
+ category: OP_NAME,
57
+ type: :info,
58
+ data: {
59
+ status: res.code.to_i,
60
+ **request_info
61
+ }
62
+ )
63
+ Sentry.add_breadcrumb(crumb)
84
64
  end
85
65
 
86
- def record_sentry_span(req, res)
87
- if Sentry.initialized? && @sentry_span
88
- request_info = extract_request_info(req)
89
- @sentry_span.set_description("#{request_info[:method]} #{request_info[:url]}")
90
- @sentry_span.set_data(:status, res.code.to_i)
91
- end
66
+ def record_sentry_span(req, res, sentry_span)
67
+ return unless Sentry.initialized? && sentry_span
68
+
69
+ request_info = extract_request_info(req)
70
+ sentry_span.set_description("#{request_info[:method]} #{request_info[:url]}")
71
+ sentry_span.set_data(:status, res.code.to_i)
72
+ finish_sentry_span(sentry_span)
92
73
  end
93
74
 
94
75
  def start_sentry_span
95
- if Sentry.initialized? && transaction = Sentry.get_current_scope.get_transaction
96
- return if from_sentry_sdk?
97
- return if transaction.sampled == false
76
+ return unless Sentry.initialized? && transaction = Sentry.get_current_scope.get_transaction
77
+ return if from_sentry_sdk?
78
+ return if transaction.sampled == false
98
79
 
99
- child_span = transaction.start_child(op: OP_NAME, start_timestamp: Sentry.utc_now.to_f)
100
- @sentry_span = child_span
101
- end
80
+ transaction.start_child(op: OP_NAME, start_timestamp: Sentry.utc_now.to_f)
102
81
  end
103
82
 
104
- def finish_sentry_span
105
- if Sentry.initialized? && @sentry_span
106
- @sentry_span.set_timestamp(Sentry.utc_now.to_f)
107
- @sentry_span = nil
108
- end
83
+ def finish_sentry_span(sentry_span)
84
+ return unless Sentry.initialized? && sentry_span
85
+
86
+ sentry_span.set_timestamp(Sentry.utc_now.to_f)
109
87
  end
110
88
 
111
89
  def from_sentry_sdk?
@@ -114,9 +92,17 @@ module Sentry
114
92
  end
115
93
 
116
94
  def extract_request_info(req)
117
- uri = req.uri
95
+ uri = req.uri || URI.parse("#{use_ssl? ? 'https' : 'http'}://#{address}#{req.path}")
118
96
  url = "#{uri.scheme}://#{uri.host}#{uri.path}" rescue uri.to_s
119
- { method: req.method, url: url }
97
+
98
+ result = { method: req.method, url: url }
99
+
100
+ if Sentry.configuration.send_default_pii
101
+ result[:url] = result[:url] + "?#{uri.query}"
102
+ result[:body] = req.body
103
+ end
104
+
105
+ result
120
106
  end
121
107
  end
122
108
  end
data/lib/sentry/rake.rb CHANGED
@@ -6,6 +6,7 @@ require "rake/task"
6
6
  module Sentry
7
7
  module Rake
8
8
  module Application
9
+ # @api private
9
10
  def display_error_message(ex)
10
11
  Sentry.capture_exception(ex) do |scope|
11
12
  task_name = top_level_tasks.join(' ')
@@ -18,6 +19,7 @@ module Sentry
18
19
  end
19
20
 
20
21
  module Task
22
+ # @api private
21
23
  def execute(args=nil)
22
24
  return super unless Sentry.initialized? && Sentry.get_current_hub
23
25
 
@@ -27,5 +29,13 @@ module Sentry
27
29
  end
28
30
  end
29
31
 
30
- Rake::Application.prepend(Sentry::Rake::Application)
31
- Rake::Task.prepend(Sentry::Rake::Task)
32
+ # @api private
33
+ module Rake
34
+ class Application
35
+ prepend(Sentry::Rake::Application)
36
+ end
37
+
38
+ class Task
39
+ prepend(Sentry::Rake::Task)
40
+ end
41
+ end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Sentry
4
+ # @api private
4
5
  class ReleaseDetector
5
6
  class << self
6
7
  def detect_release(project_root:, running_on_heroku:)