bugsnag 6.23.0 → 6.24.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 763fc185ada81aaefdfb19edc7ca88788921c4577ec3e5a76addadcd8e13227c
4
- data.tar.gz: 47661582b92e719b81f9023678dac8fcd558dd641ae124e0305b3f32157ccdd7
3
+ metadata.gz: ee1b5d5046b6ba8e68cf45b4ccac412c02051c774bcf8e205c97b4254d058468
4
+ data.tar.gz: 787717ba06a5bba3266c0030e8de943bf02fe4c6a9c6d979ec4bf543aa7d82bd
5
5
  SHA512:
6
- metadata.gz: 5de707cdf88e2c4d46cbfcd6fde2dad857c584c366788ea31dffa0b62c80d9a8dea9e8cf030306f70e793bff3999b92ae5c165309eb48d4fab2d77453160cd33
7
- data.tar.gz: f23a0d06302491091fc15d209bd09e45f1a47017467f163e1ea36fd4702dbadf1c395e36fb71283261d78c505dc6f2459ad7a7da9052989f81200f6df3c0b7ba
6
+ metadata.gz: e53bfdf8c6c23f2a9e4412fbca888884936c3c26ff5f1262f80aa5081a8edb3ba51a6e0a64065c41490efc20c1fe32d8795ec841f09f733062d3691c0c007e2e
7
+ data.tar.gz: 3fe002c3a094014f0fbb56a1863ed309b50b4e733cd651df0136ad8af2f72c6759d122baf7f6bbb34607baf18631395000bd9fd7312eec432b7dce14482d9cef
data/CHANGELOG.md CHANGED
@@ -1,6 +1,32 @@
1
1
  Changelog
2
2
  =========
3
3
 
4
+ ## v6.24.0 (6 October 2021)
5
+
6
+ ### Enhancements
7
+
8
+ * Allow overriding an event's unhandled flag
9
+ | [#698](https://github.com/bugsnag/bugsnag-ruby/pull/698)
10
+ * Add the ability to store metadata globally
11
+ | [#699](https://github.com/bugsnag/bugsnag-ruby/pull/699)
12
+ * Add `cookies`, `body` and `httpVersion` to the automatically captured request data for Rack apps
13
+ | [#700](https://github.com/bugsnag/bugsnag-ruby/pull/700)
14
+ * Add `Configuration#endpoints` for reading the notify and sessions endpoints and `Configuration#endpoints=` for setting them
15
+ | [#701](https://github.com/bugsnag/bugsnag-ruby/pull/701)
16
+ * Add `Configuration#redacted_keys`. This is like `meta_data_filters` but matches strings with case-insensitive equality, rather than matching based on inclusion
17
+ | [#703](https://github.com/bugsnag/bugsnag-ruby/pull/703)
18
+ * Allow pausing and resuming sessions, giving more control over the stability score
19
+ | [#704](https://github.com/bugsnag/bugsnag-ruby/pull/704)
20
+ * Add `Configuration#vendor_paths` to replace `Configuration#vendor_path`
21
+ | [#705](https://github.com/bugsnag/bugsnag-ruby/pull/705)
22
+
23
+ ### Deprecated
24
+
25
+ * In the next major release, `params` will only contain query string parameters. Currently it also contains the request body for form data requests, but this is deprecated in favour of the new `body` property
26
+ * The `Configuration#set_endpoints` method is now deprecated in favour of `Configuration#endpoints=`
27
+ * The `Configuration#meta_data_filters` option is now deprecated in favour of `Configuration#redacted_keys`
28
+ * The `Configuration#vendor_path` option is now deprecated in favour of `Configuration#vendor_paths`
29
+
4
30
  ## v6.23.0 (21 September 2021)
5
31
 
6
32
  ### Enhancements
data/VERSION CHANGED
@@ -1 +1 @@
1
- 6.23.0
1
+ 6.24.0
@@ -25,7 +25,7 @@ module Bugsnag
25
25
  # @param url [String]
26
26
  # @return [String]
27
27
  def clean_url(url)
28
- return url if @configuration.meta_data_filters.empty?
28
+ return url if @configuration.meta_data_filters.empty? && @configuration.redacted_keys.empty?
29
29
 
30
30
  uri = URI(url)
31
31
  return url unless uri.query
@@ -43,6 +43,33 @@ module Bugsnag
43
43
  uri.to_s
44
44
  end
45
45
 
46
+ ##
47
+ # @param key [String, #to_s]
48
+ # @return [Boolean]
49
+ def filters_match?(key)
50
+ str = key.to_s
51
+
52
+ matched = @configuration.meta_data_filters.any? do |filter|
53
+ case filter
54
+ when Regexp
55
+ str.match(filter)
56
+ else
57
+ str.include?(filter.to_s)
58
+ end
59
+ end
60
+
61
+ return true if matched
62
+
63
+ @configuration.redacted_keys.any? do |redaction_pattern|
64
+ case redaction_pattern
65
+ when Regexp
66
+ str.match(redaction_pattern)
67
+ when String
68
+ str.downcase == redaction_pattern.downcase
69
+ end
70
+ end
71
+ end
72
+
46
73
  private
47
74
 
48
75
  ##
@@ -54,9 +81,11 @@ module Bugsnag
54
81
  #
55
82
  # @return [Boolean]
56
83
  def deep_filters?
57
- @configuration.meta_data_filters.any? do |filter|
84
+ is_deep_filter = proc do |filter|
58
85
  filter.is_a?(Regexp) && filter.to_s.include?("\\.".freeze)
59
86
  end
87
+
88
+ @configuration.meta_data_filters.any?(&is_deep_filter) || @configuration.redacted_keys.any?(&is_deep_filter)
60
89
  end
61
90
 
62
91
  def clean_string(str)
@@ -137,22 +166,6 @@ module Bugsnag
137
166
  value
138
167
  end
139
168
 
140
- ##
141
- # @param key [String, #to_s]
142
- # @return [Boolean]
143
- def filters_match?(key)
144
- str = key.to_s
145
-
146
- @configuration.meta_data_filters.any? do |filter|
147
- case filter
148
- when Regexp
149
- str.match(filter)
150
- else
151
- str.include?(filter.to_s)
152
- end
153
- end
154
- end
155
-
156
169
  ##
157
170
  # If someone has a Rails filter like /^stuff\.secret/, it won't match
158
171
  # "request.params.stuff.secret", so we try it both with and without the
@@ -13,6 +13,8 @@ require "bugsnag/middleware/breadcrumbs"
13
13
  require "bugsnag/utility/circular_buffer"
14
14
  require "bugsnag/breadcrumbs/breadcrumbs"
15
15
  require "bugsnag/breadcrumbs/on_breadcrumb_callback_list"
16
+ require "bugsnag/endpoint_configuration"
17
+ require "bugsnag/endpoint_validator"
16
18
 
17
19
  module Bugsnag
18
20
  class Configuration
@@ -54,9 +56,20 @@ module Bugsnag
54
56
 
55
57
  # A list of keys that should be filtered out from the report and breadcrumb
56
58
  # metadata before sending them to Bugsnag
59
+ # @deprecated Use {#redacted_keys} instead
57
60
  # @return [Set<String, Regexp>]
58
61
  attr_accessor :meta_data_filters
59
62
 
63
+ # A set of keys that should be redacted from the report and breadcrumb
64
+ # metadata before sending them to Bugsnag
65
+ #
66
+ # When adding strings, keys that are equal to the string (ignoring case)
67
+ # will be redacted. When adding regular expressions, any keys which match
68
+ # the regular expression will be redacted
69
+ #
70
+ # @return [Set<String, Regexp>]
71
+ attr_accessor :redacted_keys
72
+
60
73
  # The logger to use for Bugsnag log messages
61
74
  # @return [Logger]
62
75
  attr_accessor :logger
@@ -114,16 +127,17 @@ module Bugsnag
114
127
  # @return [Set<Class, Proc>]
115
128
  attr_accessor :ignore_classes
116
129
 
117
- # The URL error notifications will be delivered to
118
- # @return [String]
119
- attr_reader :notify_endpoint
120
- alias :endpoint :notify_endpoint
130
+ # The URLs to send events and sessions to
131
+ # @return [EndpointConfiguration]
132
+ attr_reader :endpoints
121
133
 
122
- # The URL session notifications will be delivered to
123
- # @return [String]
124
- attr_reader :session_endpoint
134
+ # Whether events will be delivered
135
+ # @api private
136
+ # @return [Boolean]
137
+ attr_reader :enable_events
125
138
 
126
139
  # Whether sessions will be delivered
140
+ # @api private
127
141
  # @return [Boolean]
128
142
  attr_reader :enable_sessions
129
143
 
@@ -141,15 +155,28 @@ module Bugsnag
141
155
  # @return [Integer]
142
156
  attr_reader :max_breadcrumbs
143
157
 
144
- #
158
+ # @deprecated Use {vendor_paths} instead
145
159
  # @return [Regexp]
146
160
  attr_accessor :vendor_path
147
161
 
162
+ # An array of paths within the {project_root} that should not be considered
163
+ # as "in project"
164
+ #
165
+ # These paths should be relative to the {project_root} and will only match
166
+ # whole directory names
167
+ #
168
+ # @return [Array<String>]
169
+ attr_accessor :vendor_paths
170
+
148
171
  # The default context for all future events
149
172
  # Setting this will disable automatic context setting
150
173
  # @return [String, nil]
151
174
  attr_accessor :context
152
175
 
176
+ # Global metadata added to every event
177
+ # @return [Hash]
178
+ attr_reader :metadata
179
+
153
180
  # @api private
154
181
  # @return [Array<String>]
155
182
  attr_reader :scopes_to_filter
@@ -194,6 +221,7 @@ module Bugsnag
194
221
  self.send_environment = false
195
222
  self.send_code = true
196
223
  self.meta_data_filters = Set.new(DEFAULT_META_DATA_FILTERS)
224
+ @redacted_keys = Set.new
197
225
  self.scopes_to_filter = DEFAULT_SCOPES_TO_FILTER
198
226
  self.hostname = default_hostname
199
227
  self.runtime_versions = {}
@@ -213,11 +241,14 @@ module Bugsnag
213
241
  # to avoid infinite recursion when creating breadcrumb buffer
214
242
  @max_breadcrumbs = DEFAULT_MAX_BREADCRUMBS
215
243
 
216
- # These are set exclusively using the "set_endpoints" method
217
- @notify_endpoint = DEFAULT_NOTIFY_ENDPOINT
218
- @session_endpoint = DEFAULT_SESSION_ENDPOINT
244
+ @endpoints = EndpointConfiguration.new(DEFAULT_NOTIFY_ENDPOINT, DEFAULT_SESSION_ENDPOINT)
245
+
246
+ @enable_events = true
219
247
  @enable_sessions = true
220
248
 
249
+ @metadata = {}
250
+ @metadata_delegate = Utility::MetadataDelegate.new
251
+
221
252
  # SystemExit and SignalException are common Exception types seen with
222
253
  # successful exits and are not automatically reported to Bugsnag
223
254
  # TODO move these defaults into `discard_classes` when `ignore_classes`
@@ -237,6 +268,7 @@ module Bugsnag
237
268
  # Stacktrace lines that matches regex will be marked as "out of project"
238
269
  # will only appear in the full trace.
239
270
  self.vendor_path = DEFAULT_VENDOR_PATH
271
+ @vendor_paths = []
240
272
 
241
273
  # Set up logging
242
274
  self.logger = Logger.new(STDOUT)
@@ -459,26 +491,44 @@ module Bugsnag
459
491
  request_data[:breadcrumbs] ||= Bugsnag::Utility::CircularBuffer.new(@max_breadcrumbs)
460
492
  end
461
493
 
494
+ # The URL error notifications will be delivered to
495
+ # @!attribute notify_endpoint
496
+ # @return [String]
497
+ # @deprecated Use {#endpoints} instead
498
+ def notify_endpoint
499
+ @endpoints.notify
500
+ end
501
+
502
+ alias :endpoint :notify_endpoint
503
+
462
504
  # Sets the notification endpoint
463
505
  #
464
- # @deprecated Use {#set_endpoints} instead
506
+ # @deprecated Use {#endpoints} instead
465
507
  #
466
508
  # @param new_notify_endpoint [String] The URL to deliver error notifications to
467
509
  # @return [void]
468
510
  def endpoint=(new_notify_endpoint)
469
- warn("The 'endpoint' configuration option is deprecated. The 'set_endpoints' method should be used instead")
511
+ warn("The 'endpoint' configuration option is deprecated. Set both endpoints with the 'endpoints=' method instead")
470
512
  set_endpoints(new_notify_endpoint, session_endpoint) # Pass the existing session_endpoint through so it doesn't get overwritten
471
513
  end
472
514
 
515
+ # The URL session notifications will be delivered to
516
+ # @!attribute session_endpoint
517
+ # @return [String]
518
+ # @deprecated Use {#endpoints} instead
519
+ def session_endpoint
520
+ @endpoints.sessions
521
+ end
522
+
473
523
  ##
474
524
  # Sets the sessions endpoint
475
525
  #
476
- # @deprecated Use {#set_endpoints} instead
526
+ # @deprecated Use {#endpoints} instead
477
527
  #
478
528
  # @param new_session_endpoint [String] The URL to deliver session notifications to
479
529
  # @return [void]
480
530
  def session_endpoint=(new_session_endpoint)
481
- warn("The 'session_endpoint' configuration option is deprecated. The 'set_endpoints' method should be used instead")
531
+ warn("The 'session_endpoint' configuration option is deprecated. Set both endpoints with the 'endpoints=' method instead")
482
532
  set_endpoints(notify_endpoint, new_session_endpoint) # Pass the existing notify_endpoint through so it doesn't get overwritten
483
533
  end
484
534
 
@@ -488,9 +538,26 @@ module Bugsnag
488
538
  # @param new_notify_endpoint [String] The URL to deliver error notifications to
489
539
  # @param new_session_endpoint [String] The URL to deliver session notifications to
490
540
  # @return [void]
541
+ # @deprecated Use {#endpoints} instead
491
542
  def set_endpoints(new_notify_endpoint, new_session_endpoint)
492
- @notify_endpoint = new_notify_endpoint
493
- @session_endpoint = new_session_endpoint
543
+ self.endpoints = EndpointConfiguration.new(new_notify_endpoint, new_session_endpoint)
544
+ end
545
+
546
+ def endpoints=(endpoint_configuration)
547
+ result = EndpointValidator.validate(endpoint_configuration)
548
+
549
+ if result.valid?
550
+ @enable_events = true
551
+ @enable_sessions = true
552
+ else
553
+ warn(result.reason)
554
+
555
+ @enable_events = result.keep_events_enabled_for_backwards_compatibility?
556
+ @enable_sessions = false
557
+ end
558
+
559
+ # use the given endpoints even if they are invalid
560
+ @endpoints = endpoint_configuration
494
561
  end
495
562
 
496
563
  ##
@@ -556,6 +623,47 @@ module Bugsnag
556
623
  @on_breadcrumb_callbacks.remove(callback)
557
624
  end
558
625
 
626
+ ##
627
+ # Add values to metadata
628
+ #
629
+ # @overload add_metadata(section, data)
630
+ # Merges data into the given section of metadata
631
+ # @param section [String, Symbol]
632
+ # @param data [Hash]
633
+ #
634
+ # @overload add_metadata(section, key, value)
635
+ # Sets key to value in the given section of metadata. If the value is nil
636
+ # the key will be deleted
637
+ # @param section [String, Symbol]
638
+ # @param key [String, Symbol]
639
+ # @param value
640
+ #
641
+ # @return [void]
642
+ def add_metadata(section, key_or_data, *args)
643
+ @mutex.synchronize do
644
+ @metadata_delegate.add_metadata(@metadata, section, key_or_data, *args)
645
+ end
646
+ end
647
+
648
+ ##
649
+ # Clear values from metadata
650
+ #
651
+ # @overload clear_metadata(section)
652
+ # Clears the given section of metadata
653
+ # @param section [String, Symbol]
654
+ #
655
+ # @overload clear_metadata(section, key)
656
+ # Clears the key in the given section of metadata
657
+ # @param section [String, Symbol]
658
+ # @param key [String, Symbol]
659
+ #
660
+ # @return [void]
661
+ def clear_metadata(section, *args)
662
+ @mutex.synchronize do
663
+ @metadata_delegate.clear_metadata(@metadata, section, *args)
664
+ end
665
+ end
666
+
559
667
  ##
560
668
  # Has the context been explicitly set?
561
669
  #
@@ -0,0 +1,11 @@
1
+ module Bugsnag
2
+ class EndpointConfiguration
3
+ attr_reader :notify
4
+ attr_reader :sessions
5
+
6
+ def initialize(notify, sessions)
7
+ @notify = notify
8
+ @sessions = sessions
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,80 @@
1
+ module Bugsnag
2
+ # @api private
3
+ class EndpointValidator
4
+ def self.validate(endpoints)
5
+ # ensure we have an EndpointConfiguration object
6
+ return Result.missing_urls unless endpoints.is_a?(EndpointConfiguration)
7
+
8
+ # check for missing URLs
9
+ return Result.missing_urls if endpoints.notify.nil? && endpoints.sessions.nil?
10
+ return Result.missing_notify if endpoints.notify.nil?
11
+ return Result.missing_session if endpoints.sessions.nil?
12
+
13
+ # check for empty URLs
14
+ return Result.invalid_urls if endpoints.notify.empty? && endpoints.sessions.empty?
15
+ return Result.invalid_notify if endpoints.notify.empty?
16
+ return Result.invalid_session if endpoints.sessions.empty?
17
+
18
+ Result.valid
19
+ end
20
+
21
+ # @api private
22
+ class Result
23
+ # rubocop:disable Layout/LineLength
24
+ MISSING_URLS = "Invalid configuration. endpoints must be set with both a notify and session URL. Bugsnag will not send any requests.".freeze
25
+ MISSING_NOTIFY_URL = "Invalid configuration. endpoints.sessions cannot be set without also setting endpoints.notify. Bugsnag will not send any requests.".freeze
26
+ MISSING_SESSION_URL = "Invalid configuration. endpoints.notify cannot be set without also setting endpoints.sessions. Bugsnag will not send any sessions.".freeze
27
+
28
+ INVALID_URLS = "Invalid configuration. endpoints should be valid URLs, got empty strings. Bugsnag will not send any requests.".freeze
29
+ INVALID_NOTIFY_URL = "Invalid configuration. endpoints.notify should be a valid URL, got empty string. Bugsnag will not send any requests.".freeze
30
+ INVALID_SESSION_URL = "Invalid configuration. endpoints.sessions should be a valid URL, got empty string. Bugsnag will not send any sessions.".freeze
31
+ # rubocop:enable Layout/LineLength
32
+
33
+ attr_reader :reason
34
+
35
+ def initialize(valid, keep_events_enabled_for_backwards_compatibility = true, reason = nil)
36
+ @valid = valid
37
+ @keep_events_enabled_for_backwards_compatibility = keep_events_enabled_for_backwards_compatibility
38
+ @reason = reason
39
+ end
40
+
41
+ def valid?
42
+ @valid
43
+ end
44
+
45
+ def keep_events_enabled_for_backwards_compatibility?
46
+ @keep_events_enabled_for_backwards_compatibility
47
+ end
48
+
49
+ # factory functions
50
+
51
+ def self.valid
52
+ new(true)
53
+ end
54
+
55
+ def self.missing_urls
56
+ new(false, false, MISSING_URLS)
57
+ end
58
+
59
+ def self.missing_notify
60
+ new(false, false, MISSING_NOTIFY_URL)
61
+ end
62
+
63
+ def self.missing_session
64
+ new(false, true, MISSING_SESSION_URL)
65
+ end
66
+
67
+ def self.invalid_urls
68
+ new(false, false, INVALID_URLS)
69
+ end
70
+
71
+ def self.invalid_notify
72
+ new(false, false, INVALID_NOTIFY_URL)
73
+ end
74
+
75
+ def self.invalid_session
76
+ new(false, true, INVALID_SESSION_URL)
77
+ end
78
+ end
79
+ end
80
+ end
@@ -1,8 +1,11 @@
1
+ require "json"
2
+
1
3
  module Bugsnag::Middleware
2
4
  ##
3
5
  # Extracts and attaches rack data to an error report
4
6
  class RackRequest
5
7
  SPOOF = "[SPOOF]".freeze
8
+ COOKIE_HEADER = "Cookie".freeze
6
9
 
7
10
  def initialize(bugsnag)
8
11
  @bugsnag = bugsnag
@@ -42,22 +45,6 @@ module Bugsnag::Middleware
42
45
  Bugsnag.configuration.warn "RackRequest - Rescued error while cleaning request.referer: #{stde}"
43
46
  end
44
47
 
45
- headers = {}
46
-
47
- env.each_pair do |key, value|
48
- if key.to_s.start_with?("HTTP_")
49
- header_key = key[5..-1]
50
- elsif ["CONTENT_TYPE", "CONTENT_LENGTH"].include?(key)
51
- header_key = key
52
- else
53
- next
54
- end
55
-
56
- headers[header_key.split("_").map {|s| s.capitalize}.join("-")] = value
57
- end
58
-
59
- headers["Referer"] = referer if headers["Referer"]
60
-
61
48
  # Add a request tab
62
49
  report.add_tab(:request, {
63
50
  :url => url,
@@ -65,9 +52,17 @@ module Bugsnag::Middleware
65
52
  :params => params.to_hash,
66
53
  :referer => referer,
67
54
  :clientIp => client_ip,
68
- :headers => headers
55
+ :headers => format_headers(env, referer)
69
56
  })
70
57
 
58
+ # add the HTTP version if present
59
+ if env["SERVER_PROTOCOL"]
60
+ report.add_metadata(:request, :httpVersion, env["SERVER_PROTOCOL"])
61
+ end
62
+
63
+ add_request_body(report, request, env)
64
+ add_cookies(report, request)
65
+
71
66
  # Add an environment tab
72
67
  if report.configuration.send_environment
73
68
  report.add_tab(:environment, env)
@@ -87,5 +82,75 @@ module Bugsnag::Middleware
87
82
 
88
83
  @bugsnag.call(report)
89
84
  end
85
+
86
+ private
87
+
88
+ def format_headers(env, referer)
89
+ headers = {}
90
+
91
+ env.each_pair do |key, value|
92
+ if key.to_s.start_with?("HTTP_")
93
+ header_key = key[5..-1]
94
+ elsif ["CONTENT_TYPE", "CONTENT_LENGTH"].include?(key)
95
+ header_key = key
96
+ else
97
+ next
98
+ end
99
+
100
+ headers[header_key.split("_").map {|s| s.capitalize}.join("-")] = value
101
+ end
102
+
103
+ headers["Referer"] = referer if headers["Referer"]
104
+
105
+ headers
106
+ end
107
+
108
+ def add_request_body(report, request, env)
109
+ body = parsed_request_body(request, env)
110
+
111
+ # this request may not have a body
112
+ return unless body.is_a?(Hash) && !body.empty?
113
+
114
+ report.add_metadata(:request, :body, body)
115
+ end
116
+
117
+ def parsed_request_body(request, env)
118
+ return request.POST rescue nil if request.form_data?
119
+
120
+ content_type = env["CONTENT_TYPE"]
121
+
122
+ return nil if content_type.nil?
123
+
124
+ if content_type.include?('/json') || content_type.include?('+json')
125
+ begin
126
+ body = request.body
127
+
128
+ return JSON.parse(body.read)
129
+ rescue StandardError
130
+ return nil
131
+ ensure
132
+ # the body must be rewound so other things can read it after we do
133
+ body.rewind
134
+ end
135
+ end
136
+
137
+ nil
138
+ end
139
+
140
+ def add_cookies(report, request)
141
+ return unless record_cookies?
142
+
143
+ cookies = request.cookies rescue nil
144
+
145
+ return unless cookies.is_a?(Hash) && !cookies.empty?
146
+
147
+ report.add_metadata(:request, :cookies, cookies)
148
+ end
149
+
150
+ def record_cookies?
151
+ # only record cookies in the request if none of the filters match "Cookie"
152
+ # the "Cookie" header will be filtered as normal
153
+ !Bugsnag.cleaner.filters_match?(COOKIE_HEADER)
154
+ end
90
155
  end
91
156
  end
@@ -8,12 +8,14 @@ module Bugsnag::Middleware
8
8
 
9
9
  def call(report)
10
10
  session = Bugsnag::SessionTracker.get_current_session
11
- unless session.nil?
11
+
12
+ if session && !session[:paused?]
12
13
  if report.unhandled
13
14
  session[:events][:unhandled] += 1
14
15
  else
15
16
  session[:events][:handled] += 1
16
17
  end
18
+
17
19
  report.session = session
18
20
  end
19
21
 
@@ -119,6 +119,7 @@ module Bugsnag
119
119
 
120
120
  @should_ignore = false
121
121
  @unhandled = auto_notify
122
+ @initial_unhandled = @unhandled
122
123
 
123
124
  self.configuration = passed_configuration
124
125
 
@@ -135,7 +136,7 @@ module Bugsnag
135
136
  self.delivery_method = configuration.delivery_method
136
137
  self.hostname = configuration.hostname
137
138
  self.runtime_versions = configuration.runtime_versions.dup
138
- self.meta_data = {}
139
+ self.meta_data = Utility::Duplicator.duplicate(configuration.metadata)
139
140
  self.release_stage = configuration.release_stage
140
141
  self.severity = auto_notify ? "error" : "warning"
141
142
  self.severity_reason = auto_notify ? {:type => UNHANDLED_EXCEPTION} : {:type => HANDLED_EXCEPTION}
@@ -374,8 +375,43 @@ module Bugsnag
374
375
  @user = new_user
375
376
  end
376
377
 
378
+ def unhandled=(new_unhandled)
379
+ # fix the handled/unhandled counts in the current session
380
+ update_handled_counts(new_unhandled, @unhandled)
381
+
382
+ @unhandled = new_unhandled
383
+ end
384
+
385
+ ##
386
+ # Returns true if the unhandled flag has been changed from its initial value
387
+ #
388
+ # @api private
389
+ # @return [Boolean]
390
+ def unhandled_overridden?
391
+ @unhandled != @initial_unhandled
392
+ end
393
+
377
394
  private
378
395
 
396
+ def update_handled_counts(is_unhandled, was_unhandled)
397
+ # do nothing if there is no session to update
398
+ return if @session.nil?
399
+
400
+ # increment the counts for the current unhandled value
401
+ if is_unhandled
402
+ @session[:events][:unhandled] += 1
403
+ else
404
+ @session[:events][:handled] += 1
405
+ end
406
+
407
+ # decrement the counts for the previous unhandled value
408
+ if was_unhandled
409
+ @session[:events][:unhandled] -= 1
410
+ else
411
+ @session[:events][:handled] -= 1
412
+ end
413
+ end
414
+
379
415
  def generate_exception_list
380
416
  raw_exceptions.map do |exception|
381
417
  {
@@ -34,17 +34,20 @@ module Bugsnag
34
34
  # Starts a new session, storing it on the current thread.
35
35
  #
36
36
  # This allows Bugsnag to track error rates for a release.
37
+ #
38
+ # @return [void]
37
39
  def start_session
38
40
  return unless Bugsnag.configuration.enable_sessions && Bugsnag.configuration.should_notify_release_stage?
39
41
 
40
42
  start_delivery_thread
41
43
  start_time = Time.now().utc().strftime('%Y-%m-%dT%H:%M:00')
42
44
  new_session = {
43
- :id => SecureRandom.uuid,
44
- :startedAt => start_time,
45
- :events => {
46
- :handled => 0,
47
- :unhandled => 0
45
+ id: SecureRandom.uuid,
46
+ startedAt: start_time,
47
+ paused?: false,
48
+ events: {
49
+ handled: 0,
50
+ unhandled: 0
48
51
  }
49
52
  }
50
53
  SessionTracker.set_current_session(new_session)
@@ -53,6 +56,47 @@ module Bugsnag
53
56
 
54
57
  alias_method :create_session, :start_session
55
58
 
59
+ ##
60
+ # Stop any events being attributed to the current session until it is
61
+ # resumed or a new session is started
62
+ #
63
+ # @see resume_session
64
+ #
65
+ # @return [void]
66
+ def pause_session
67
+ current_session = SessionTracker.get_current_session
68
+
69
+ return unless current_session
70
+
71
+ current_session[:paused?] = true
72
+ end
73
+
74
+ ##
75
+ # Resume the current session if it was previously paused. If there is no
76
+ # current session, a new session will be started
77
+ #
78
+ # @see pause_session
79
+ #
80
+ # @return [Boolean] true if a paused session was resumed
81
+ def resume_session
82
+ current_session = SessionTracker.get_current_session
83
+
84
+ if current_session
85
+ # if the session is paused then resume it, otherwise we don't need to
86
+ # do anything
87
+ if current_session[:paused?]
88
+ current_session[:paused?] = false
89
+
90
+ return true
91
+ end
92
+ else
93
+ # if there's no current session, start a new one
94
+ start_session
95
+ end
96
+
97
+ false
98
+ end
99
+
56
100
  ##
57
101
  # Delivers the current session_counts lists to the session endpoint.
58
102
  def send_sessions
@@ -43,7 +43,7 @@ module Bugsnag
43
43
  if defined?(configuration.project_root) && configuration.project_root.to_s != ''
44
44
  trace_hash[:inProject] = true if file.start_with?(configuration.project_root.to_s)
45
45
  file.sub!(/#{configuration.project_root}\//, "")
46
- trace_hash.delete(:inProject) if file.match(configuration.vendor_path)
46
+ trace_hash.delete(:inProject) if vendor_path?(configuration, file)
47
47
  end
48
48
 
49
49
  # Strip common gem path prefixes
@@ -67,5 +67,14 @@ module Bugsnag
67
67
 
68
68
  processed_backtrace
69
69
  end
70
+
71
+ # @api private
72
+ def self.vendor_path?(configuration, file_path)
73
+ return true if configuration.vendor_path && file_path.match(configuration.vendor_path)
74
+
75
+ configuration.vendor_paths.any? do |vendor_path|
76
+ file_path.start_with?("#{vendor_path.sub(/\/$/, '')}/")
77
+ end
78
+ end
70
79
  end
71
80
  end
@@ -0,0 +1,124 @@
1
+ module Bugsnag::Utility
2
+ # @api private
3
+ class Duplicator
4
+ class << self
5
+ ##
6
+ # Duplicate (deep clone) the given object
7
+ #
8
+ # @param object [Object]
9
+ # @param seen_objects [Hash<String, Object>]
10
+ # @return [Object]
11
+ def duplicate(object, seen_objects = {})
12
+ case object
13
+ # return immutable & non-duplicatable objects as-is
14
+ when Symbol, Numeric, Method, TrueClass, FalseClass, NilClass
15
+ object
16
+ when Array
17
+ duplicate_array(object, seen_objects)
18
+ when Hash
19
+ duplicate_hash(object, seen_objects)
20
+ when Range
21
+ duplicate_range(object, seen_objects)
22
+ when Struct
23
+ duplicate_struct(object, seen_objects)
24
+ else
25
+ duplicate_generic_object(object, seen_objects)
26
+ end
27
+ rescue StandardError
28
+ object
29
+ end
30
+
31
+ private
32
+
33
+ def duplicate_array(array, seen_objects)
34
+ id = array.object_id
35
+
36
+ return seen_objects[id] if seen_objects.key?(id)
37
+
38
+ copy = array.dup
39
+ seen_objects[id] = copy
40
+
41
+ copy.map! do |value|
42
+ duplicate(value, seen_objects)
43
+ end
44
+
45
+ copy
46
+ end
47
+
48
+ def duplicate_hash(hash, seen_objects)
49
+ id = hash.object_id
50
+
51
+ return seen_objects[id] if seen_objects.key?(id)
52
+
53
+ copy = {}
54
+ seen_objects[id] = copy
55
+
56
+ hash.each do |key, value|
57
+ copy[duplicate(key, seen_objects)] = duplicate(value, seen_objects)
58
+ end
59
+
60
+ copy
61
+ end
62
+
63
+ ##
64
+ # Ranges are immutable but the values they contain may not be
65
+ #
66
+ # For example, a range of "a".."z" can be mutated: range.first.upcase!
67
+ def duplicate_range(range, seen_objects)
68
+ id = range.object_id
69
+
70
+ return seen_objects[id] if seen_objects.key?(id)
71
+
72
+ begin
73
+ copy = range.class.new(
74
+ duplicate(range.first, seen_objects),
75
+ duplicate(range.last, seen_objects),
76
+ range.exclude_end?
77
+ )
78
+ rescue StandardError
79
+ copy = range.dup
80
+ end
81
+
82
+ seen_objects[id] = copy
83
+ end
84
+
85
+ def duplicate_struct(struct, seen_objects)
86
+ id = struct.object_id
87
+
88
+ return seen_objects[id] if seen_objects.key?(id)
89
+
90
+ copy = struct.dup
91
+ seen_objects[id] = copy
92
+
93
+ struct.each_pair do |attribute, value|
94
+ begin
95
+ copy.send("#{attribute}=", duplicate(value, seen_objects))
96
+ rescue StandardError # rubocop:todo Lint/SuppressedException
97
+ end
98
+ end
99
+
100
+ copy
101
+ end
102
+
103
+ def duplicate_generic_object(object, seen_objects)
104
+ id = object.object_id
105
+
106
+ return seen_objects[id] if seen_objects.key?(id)
107
+
108
+ copy = object.dup
109
+ seen_objects[id] = copy
110
+
111
+ begin
112
+ copy.instance_variables.each do |variable|
113
+ value = copy.instance_variable_get(variable)
114
+
115
+ copy.instance_variable_set(variable, duplicate(value, seen_objects))
116
+ end
117
+ rescue StandardError # rubocop:todo Lint/SuppressedException
118
+ end
119
+
120
+ copy
121
+ end
122
+ end
123
+ end
124
+ end
data/lib/bugsnag.rb CHANGED
@@ -34,6 +34,7 @@ require "bugsnag/breadcrumbs/validator"
34
34
  require "bugsnag/breadcrumbs/breadcrumb"
35
35
  require "bugsnag/breadcrumbs/breadcrumbs"
36
36
 
37
+ require "bugsnag/utility/duplicator"
37
38
  require "bugsnag/utility/metadata_delegate"
38
39
 
39
40
  # rubocop:todo Metrics/ModuleLength
@@ -136,6 +137,11 @@ module Bugsnag
136
137
  report.severity_reason = initial_reason
137
138
  end
138
139
 
140
+ if report.unhandled_overridden?
141
+ # let the dashboard know that the unhandled flag was overridden
142
+ report.severity_reason[:unhandledOverridden] = true
143
+ end
144
+
139
145
  deliver_notification(report)
140
146
  end
141
147
  end
@@ -191,13 +197,36 @@ module Bugsnag
191
197
  end
192
198
 
193
199
  ##
194
- # Starts a session.
200
+ # Starts a new session, which allows Bugsnag to track error rates across
201
+ # releases
195
202
  #
196
- # Allows Bugsnag to track error rates across releases.
203
+ # @return [void]
197
204
  def start_session
198
205
  session_tracker.start_session
199
206
  end
200
207
 
208
+ ##
209
+ # Stop any events being attributed to the current session until it is
210
+ # resumed or a new session is started
211
+ #
212
+ # @see resume_session
213
+ #
214
+ # @return [void]
215
+ def pause_session
216
+ session_tracker.pause_session
217
+ end
218
+
219
+ ##
220
+ # Resume the current session if it was previously paused. If there is no
221
+ # current session, a new session will be started
222
+ #
223
+ # @see pause_session
224
+ #
225
+ # @return [Boolean] true if a paused session was resumed
226
+ def resume_session
227
+ session_tracker.resume_session
228
+ end
229
+
201
230
  ##
202
231
  # Allow access to "before notify" callbacks as an array.
203
232
  #
@@ -352,9 +381,56 @@ module Bugsnag
352
381
  end
353
382
  end
354
383
 
384
+ ##
385
+ # Global metadata added to every event
386
+ #
387
+ # @return [Hash]
388
+ def metadata
389
+ configuration.metadata
390
+ end
391
+
392
+ ##
393
+ # Add values to metadata
394
+ #
395
+ # @overload add_metadata(section, data)
396
+ # Merges data into the given section of metadata
397
+ # @param section [String, Symbol]
398
+ # @param data [Hash]
399
+ #
400
+ # @overload add_metadata(section, key, value)
401
+ # Sets key to value in the given section of metadata. If the value is nil
402
+ # the key will be deleted
403
+ # @param section [String, Symbol]
404
+ # @param key [String, Symbol]
405
+ # @param value
406
+ #
407
+ # @return [void]
408
+ def add_metadata(section, key_or_data, *args)
409
+ configuration.add_metadata(section, key_or_data, *args)
410
+ end
411
+
412
+ ##
413
+ # Clear values from metadata
414
+ #
415
+ # @overload clear_metadata(section)
416
+ # Clears the given section of metadata
417
+ # @param section [String, Symbol]
418
+ #
419
+ # @overload clear_metadata(section, key)
420
+ # Clears the key in the given section of metadata
421
+ # @param section [String, Symbol]
422
+ # @param key [String, Symbol]
423
+ #
424
+ # @return [void]
425
+ def clear_metadata(section, *args)
426
+ configuration.clear_metadata(section, *args)
427
+ end
428
+
355
429
  private
356
430
 
357
431
  def should_deliver_notification?(exception, auto_notify)
432
+ return false unless configuration.enable_events
433
+
358
434
  reason = abort_reason(exception, auto_notify)
359
435
  configuration.debug(reason) unless reason.nil?
360
436
  reason.nil?
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bugsnag
3
3
  version: !ruby/object:Gem::Version
4
- version: 6.23.0
4
+ version: 6.24.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - James Smith
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-09-21 00:00:00.000000000 Z
11
+ date: 2021-10-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: concurrent-ruby
@@ -51,6 +51,8 @@ files:
51
51
  - lib/bugsnag/delivery.rb
52
52
  - lib/bugsnag/delivery/synchronous.rb
53
53
  - lib/bugsnag/delivery/thread_queue.rb
54
+ - lib/bugsnag/endpoint_configuration.rb
55
+ - lib/bugsnag/endpoint_validator.rb
54
56
  - lib/bugsnag/error.rb
55
57
  - lib/bugsnag/event.rb
56
58
  - lib/bugsnag/helpers.rb
@@ -94,6 +96,7 @@ files:
94
96
  - lib/bugsnag/tasks.rb
95
97
  - lib/bugsnag/tasks/bugsnag.rake
96
98
  - lib/bugsnag/utility/circular_buffer.rb
99
+ - lib/bugsnag/utility/duplicator.rb
97
100
  - lib/bugsnag/utility/metadata_delegate.rb
98
101
  - lib/bugsnag/version.rb
99
102
  - lib/generators/bugsnag/bugsnag_generator.rb