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 +4 -4
- data/CHANGELOG.md +26 -0
- data/VERSION +1 -1
- data/lib/bugsnag/cleaner.rb +31 -18
- data/lib/bugsnag/configuration.rb +125 -17
- data/lib/bugsnag/endpoint_configuration.rb +11 -0
- data/lib/bugsnag/endpoint_validator.rb +80 -0
- data/lib/bugsnag/middleware/rack_request.rb +82 -17
- data/lib/bugsnag/middleware/session_data.rb +3 -1
- data/lib/bugsnag/report.rb +37 -1
- data/lib/bugsnag/session_tracker.rb +49 -5
- data/lib/bugsnag/stacktrace.rb +10 -1
- data/lib/bugsnag/utility/duplicator.rb +124 -0
- data/lib/bugsnag.rb +78 -2
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ee1b5d5046b6ba8e68cf45b4ccac412c02051c774bcf8e205c97b4254d058468
|
4
|
+
data.tar.gz: 787717ba06a5bba3266c0030e8de943bf02fe4c6a9c6d979ec4bf543aa7d82bd
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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.
|
1
|
+
6.24.0
|
data/lib/bugsnag/cleaner.rb
CHANGED
@@ -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
|
-
|
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
|
118
|
-
# @return [
|
119
|
-
attr_reader :
|
120
|
-
alias :endpoint :notify_endpoint
|
130
|
+
# The URLs to send events and sessions to
|
131
|
+
# @return [EndpointConfiguration]
|
132
|
+
attr_reader :endpoints
|
121
133
|
|
122
|
-
#
|
123
|
-
# @
|
124
|
-
|
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
|
-
|
217
|
-
|
218
|
-
@
|
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 {#
|
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.
|
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 {#
|
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.
|
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
|
-
|
493
|
-
|
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,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 =>
|
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
|
-
|
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
|
|
data/lib/bugsnag/report.rb
CHANGED
@@ -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
|
-
:
|
44
|
-
:
|
45
|
-
|
46
|
-
|
47
|
-
:
|
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
|
data/lib/bugsnag/stacktrace.rb
CHANGED
@@ -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
|
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
|
-
#
|
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.
|
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-
|
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
|