bugsnag 6.23.0 → 6.24.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.
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