bugsnag 4.2.1 → 6.27.1
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 +5 -5
- data/.yardopts +12 -0
- data/CHANGELOG.md +814 -0
- data/README.md +21 -25
- data/VERSION +1 -1
- data/bugsnag.gemspec +19 -8
- data/lib/bugsnag/breadcrumb_type.rb +14 -0
- data/lib/bugsnag/breadcrumbs/breadcrumb.rb +109 -0
- data/lib/bugsnag/breadcrumbs/breadcrumbs.rb +13 -0
- data/lib/bugsnag/breadcrumbs/on_breadcrumb_callback_list.rb +48 -0
- data/lib/bugsnag/breadcrumbs/validator.rb +29 -0
- data/lib/bugsnag/cleaner.rb +170 -59
- data/lib/bugsnag/code_extractor.rb +137 -0
- data/lib/bugsnag/configuration.rb +670 -45
- data/lib/bugsnag/delivery/synchronous.rb +31 -14
- data/lib/bugsnag/delivery/thread_queue.rb +23 -6
- data/lib/bugsnag/delivery.rb +13 -0
- data/lib/bugsnag/endpoint_configuration.rb +11 -0
- data/lib/bugsnag/endpoint_validator.rb +80 -0
- data/lib/bugsnag/error.rb +25 -0
- data/lib/bugsnag/event.rb +5 -0
- data/lib/bugsnag/feature_flag.rb +74 -0
- data/lib/bugsnag/helpers.rb +121 -25
- data/lib/bugsnag/integrations/delayed_job.rb +51 -0
- data/lib/bugsnag/integrations/mailman.rb +43 -0
- data/lib/bugsnag/integrations/mongo.rb +133 -0
- data/lib/bugsnag/integrations/que.rb +53 -0
- data/lib/bugsnag/integrations/rack.rb +83 -0
- data/lib/bugsnag/integrations/rails/active_job.rb +100 -0
- data/lib/bugsnag/{rails → integrations/rails}/active_record_rescue.rb +10 -1
- data/lib/bugsnag/{rails → integrations/rails}/controller_methods.rb +1 -9
- data/lib/bugsnag/integrations/rails/rails_breadcrumbs.rb +115 -0
- data/lib/bugsnag/integrations/railtie.rb +153 -0
- data/lib/bugsnag/integrations/rake.rb +74 -0
- data/lib/bugsnag/integrations/resque.rb +94 -0
- data/lib/bugsnag/integrations/shoryuken.rb +50 -0
- data/lib/bugsnag/integrations/sidekiq.rb +68 -0
- data/lib/bugsnag/meta_data.rb +1 -0
- data/lib/bugsnag/middleware/active_job.rb +18 -0
- data/lib/bugsnag/middleware/breadcrumbs.rb +21 -0
- data/lib/bugsnag/middleware/callbacks.rb +6 -8
- data/lib/bugsnag/middleware/classify_error.rb +50 -0
- data/lib/bugsnag/middleware/clearance_user.rb +33 -0
- data/lib/bugsnag/middleware/delayed_job.rb +93 -0
- data/lib/bugsnag/middleware/discard_error_class.rb +30 -0
- data/lib/bugsnag/middleware/exception_meta_data.rb +42 -0
- data/lib/bugsnag/middleware/ignore_error_class.rb +26 -0
- data/lib/bugsnag/middleware/mailman.rb +6 -4
- data/lib/bugsnag/middleware/rack_request.rb +126 -30
- data/lib/bugsnag/middleware/rails3_request.rb +15 -17
- data/lib/bugsnag/middleware/rake.rb +7 -5
- data/lib/bugsnag/middleware/session_data.rb +25 -0
- data/lib/bugsnag/middleware/sidekiq.rb +9 -4
- data/lib/bugsnag/middleware/suggestion_data.rb +34 -0
- data/lib/bugsnag/middleware/warden_user.rb +11 -6
- data/lib/bugsnag/middleware_stack.rb +62 -9
- data/lib/bugsnag/on_error_callbacks.rb +33 -0
- data/lib/bugsnag/report.rb +516 -0
- data/lib/bugsnag/session_tracker.rb +182 -0
- data/lib/bugsnag/stacktrace.rb +82 -0
- data/lib/bugsnag/tasks/bugsnag.rake +2 -70
- data/lib/bugsnag/utility/circular_buffer.rb +62 -0
- data/lib/bugsnag/utility/duplicator.rb +124 -0
- data/lib/bugsnag/utility/feature_data_store.rb +41 -0
- data/lib/bugsnag/utility/feature_flag_delegate.rb +89 -0
- data/lib/bugsnag/utility/metadata_delegate.rb +102 -0
- data/lib/bugsnag.rb +528 -80
- metadata +61 -123
- data/.document +0 -5
- data/.gitignore +0 -52
- data/.rspec +0 -3
- data/.travis.yml +0 -14
- data/CONTRIBUTING.md +0 -47
- data/Gemfile +0 -2
- data/Rakefile +0 -29
- data/lib/bugsnag/capistrano.rb +0 -7
- data/lib/bugsnag/capistrano2.rb +0 -32
- data/lib/bugsnag/delay/resque.rb +0 -21
- data/lib/bugsnag/delayed_job.rb +0 -57
- data/lib/bugsnag/deploy.rb +0 -34
- data/lib/bugsnag/mailman.rb +0 -28
- data/lib/bugsnag/middleware/rails2_request.rb +0 -52
- data/lib/bugsnag/notification.rb +0 -459
- data/lib/bugsnag/rack.rb +0 -53
- data/lib/bugsnag/rails/action_controller_rescue.rb +0 -62
- data/lib/bugsnag/rails.rb +0 -66
- data/lib/bugsnag/railtie.rb +0 -80
- data/lib/bugsnag/rake.rb +0 -25
- data/lib/bugsnag/resque.rb +0 -40
- data/lib/bugsnag/sidekiq.rb +0 -42
- data/lib/bugsnag/tasks/bugsnag.cap +0 -48
- data/rails/init.rb +0 -7
- data/spec/cleaner_spec.rb +0 -138
- data/spec/code_spec.rb +0 -86
- data/spec/fixtures/crashes/end_of_file.rb +0 -9
- data/spec/fixtures/crashes/short_file.rb +0 -1
- data/spec/fixtures/crashes/start_of_file.rb +0 -9
- data/spec/fixtures/middleware/internal_info_setter.rb +0 -11
- data/spec/fixtures/middleware/public_info_setter.rb +0 -11
- data/spec/fixtures/tasks/Rakefile +0 -15
- data/spec/helper_spec.rb +0 -163
- data/spec/integration_spec.rb +0 -132
- data/spec/middleware_spec.rb +0 -181
- data/spec/notification_spec.rb +0 -877
- data/spec/rack_spec.rb +0 -56
- data/spec/spec_helper.rb +0 -53
@@ -0,0 +1,516 @@
|
|
1
|
+
require "pathname"
|
2
|
+
require "bugsnag/error"
|
3
|
+
require "bugsnag/stacktrace"
|
4
|
+
|
5
|
+
module Bugsnag
|
6
|
+
# rubocop:todo Metrics/ClassLength
|
7
|
+
class Report
|
8
|
+
include Utility::FeatureDataStore
|
9
|
+
|
10
|
+
NOTIFIER_NAME = "Ruby Bugsnag Notifier"
|
11
|
+
NOTIFIER_VERSION = Bugsnag::VERSION
|
12
|
+
NOTIFIER_URL = "https://www.bugsnag.com"
|
13
|
+
|
14
|
+
UNHANDLED_EXCEPTION = "unhandledException"
|
15
|
+
UNHANDLED_EXCEPTION_MIDDLEWARE = "unhandledExceptionMiddleware"
|
16
|
+
ERROR_CLASS = "errorClass"
|
17
|
+
HANDLED_EXCEPTION = "handledException"
|
18
|
+
USER_SPECIFIED_SEVERITY = "userSpecifiedSeverity"
|
19
|
+
USER_CALLBACK_SET_SEVERITY = "userCallbackSetSeverity"
|
20
|
+
|
21
|
+
MAX_EXCEPTIONS_TO_UNWRAP = 5
|
22
|
+
|
23
|
+
CURRENT_PAYLOAD_VERSION = "4.0"
|
24
|
+
|
25
|
+
# Whether this report is for a handled or unhandled error
|
26
|
+
# @return [Boolean]
|
27
|
+
attr_reader :unhandled
|
28
|
+
|
29
|
+
# Your Integration API Key
|
30
|
+
# @see Configuration#api_key
|
31
|
+
# @return [String, nil]
|
32
|
+
attr_accessor :api_key
|
33
|
+
|
34
|
+
# The type of application executing the current code
|
35
|
+
# @see Configuration#app_type
|
36
|
+
# @return [String, nil]
|
37
|
+
attr_accessor :app_type
|
38
|
+
|
39
|
+
# The current version of your application
|
40
|
+
# @return [String, nil]
|
41
|
+
attr_accessor :app_version
|
42
|
+
|
43
|
+
# The list of breadcrumbs attached to this report
|
44
|
+
# @return [Array<Breadcrumb>]
|
45
|
+
attr_accessor :breadcrumbs
|
46
|
+
|
47
|
+
# @api private
|
48
|
+
# @return [Configuration]
|
49
|
+
attr_accessor :configuration
|
50
|
+
|
51
|
+
# The delivery method that will be used for this report
|
52
|
+
# @see Configuration#delivery_method
|
53
|
+
# @return [Symbol]
|
54
|
+
attr_accessor :delivery_method
|
55
|
+
|
56
|
+
# The list of exceptions in this report
|
57
|
+
# @deprecated Use {#errors} instead
|
58
|
+
# @return [Array<Hash>]
|
59
|
+
attr_accessor :exceptions
|
60
|
+
|
61
|
+
# @see Configuration#hostname
|
62
|
+
# @return [String]
|
63
|
+
attr_accessor :hostname
|
64
|
+
|
65
|
+
# @api private
|
66
|
+
# @see Configuration#runtime_versions
|
67
|
+
# @return [Hash{String => String}]
|
68
|
+
attr_accessor :runtime_versions
|
69
|
+
|
70
|
+
# All errors with the same grouping hash will be grouped in the Bugsnag app
|
71
|
+
# @return [String]
|
72
|
+
attr_accessor :grouping_hash
|
73
|
+
|
74
|
+
# Arbitrary metadata attached to this report
|
75
|
+
# @deprecated Use {#metadata} instead
|
76
|
+
# @return [Hash]
|
77
|
+
attr_accessor :meta_data
|
78
|
+
|
79
|
+
# The raw Exception instances for this report
|
80
|
+
# @deprecated Use {#original_error} instead
|
81
|
+
# @see #exceptions
|
82
|
+
# @return [Array<Exception>]
|
83
|
+
attr_accessor :raw_exceptions
|
84
|
+
|
85
|
+
# The current stage of the release process, e.g. 'development', production'
|
86
|
+
# @see Configuration#release_stage
|
87
|
+
# @return [String, nil]
|
88
|
+
attr_accessor :release_stage
|
89
|
+
|
90
|
+
# The session that active when this report was generated
|
91
|
+
# @see SessionTracker#get_current_session
|
92
|
+
# @return [Hash]
|
93
|
+
attr_accessor :session
|
94
|
+
|
95
|
+
# The severity of this report, e.g. 'error', 'warning'
|
96
|
+
# @return [String]
|
97
|
+
attr_accessor :severity
|
98
|
+
|
99
|
+
# @api private
|
100
|
+
# @return [Hash]
|
101
|
+
attr_accessor :severity_reason
|
102
|
+
|
103
|
+
# The current user when this report was generated
|
104
|
+
# @return [Hash]
|
105
|
+
attr_accessor :user
|
106
|
+
|
107
|
+
# A list of errors in this report
|
108
|
+
# @return [Array<Error>]
|
109
|
+
attr_reader :errors
|
110
|
+
|
111
|
+
# The Exception instance this report was created for
|
112
|
+
# @return [Exception]
|
113
|
+
attr_reader :original_error
|
114
|
+
|
115
|
+
##
|
116
|
+
# Initializes a new report from an exception.
|
117
|
+
def initialize(exception, passed_configuration, auto_notify=false)
|
118
|
+
# store the creation time for use as device.time
|
119
|
+
@created_at = Time.now.utc.iso8601(3)
|
120
|
+
|
121
|
+
@should_ignore = false
|
122
|
+
@unhandled = auto_notify
|
123
|
+
@initial_unhandled = @unhandled
|
124
|
+
|
125
|
+
self.configuration = passed_configuration
|
126
|
+
|
127
|
+
@original_error = exception
|
128
|
+
self.raw_exceptions = generate_raw_exceptions(exception)
|
129
|
+
self.exceptions = generate_exception_list
|
130
|
+
@errors = generate_error_list
|
131
|
+
|
132
|
+
self.api_key = configuration.api_key
|
133
|
+
self.app_type = configuration.app_type
|
134
|
+
self.app_version = configuration.app_version
|
135
|
+
self.breadcrumbs = []
|
136
|
+
self.context = configuration.context if configuration.context_set?
|
137
|
+
self.delivery_method = configuration.delivery_method
|
138
|
+
self.hostname = configuration.hostname
|
139
|
+
self.runtime_versions = configuration.runtime_versions.dup
|
140
|
+
self.meta_data = Utility::Duplicator.duplicate(configuration.metadata)
|
141
|
+
self.release_stage = configuration.release_stage
|
142
|
+
self.severity = auto_notify ? "error" : "warning"
|
143
|
+
self.severity_reason = auto_notify ? {:type => UNHANDLED_EXCEPTION} : {:type => HANDLED_EXCEPTION}
|
144
|
+
self.user = {}
|
145
|
+
|
146
|
+
@metadata_delegate = Utility::MetadataDelegate.new
|
147
|
+
@feature_flag_delegate = Bugsnag.feature_flag_delegate.dup
|
148
|
+
end
|
149
|
+
|
150
|
+
##
|
151
|
+
# Additional context for this report
|
152
|
+
# @!attribute context
|
153
|
+
# @return [String, nil]
|
154
|
+
def context
|
155
|
+
return @context if defined?(@context)
|
156
|
+
|
157
|
+
@automatic_context
|
158
|
+
end
|
159
|
+
|
160
|
+
attr_writer :context
|
161
|
+
|
162
|
+
##
|
163
|
+
# Context set automatically by Bugsnag uses this attribute, which prevents
|
164
|
+
# it from overwriting the user-supplied context
|
165
|
+
# @api private
|
166
|
+
# @return [String, nil]
|
167
|
+
attr_accessor :automatic_context
|
168
|
+
|
169
|
+
##
|
170
|
+
# Add a new metadata tab to this notification.
|
171
|
+
#
|
172
|
+
# @param name [String, #to_s] The name of the tab to add
|
173
|
+
# @param value [Hash, Object] The value to add to the tab. If the tab already
|
174
|
+
# exists, this will be merged with the existing values. If a Hash is not
|
175
|
+
# given, the value will be placed into the 'custom' tab
|
176
|
+
# @return [void]
|
177
|
+
#
|
178
|
+
# @deprecated Use {#add_metadata} instead
|
179
|
+
def add_tab(name, value)
|
180
|
+
return if name.nil?
|
181
|
+
|
182
|
+
if value.is_a? Hash
|
183
|
+
meta_data[name] ||= {}
|
184
|
+
meta_data[name].merge! value
|
185
|
+
else
|
186
|
+
meta_data["custom"] = {} unless meta_data["custom"]
|
187
|
+
|
188
|
+
meta_data["custom"][name.to_s] = value
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
##
|
193
|
+
# Removes a metadata tab from this notification.
|
194
|
+
#
|
195
|
+
# @param name [String]
|
196
|
+
# @return [void]
|
197
|
+
#
|
198
|
+
# @deprecated Use {#clear_metadata} instead
|
199
|
+
def remove_tab(name)
|
200
|
+
return if name.nil?
|
201
|
+
|
202
|
+
meta_data.delete(name)
|
203
|
+
end
|
204
|
+
|
205
|
+
##
|
206
|
+
# Builds and returns the exception payload for this notification.
|
207
|
+
#
|
208
|
+
# @return [Hash]
|
209
|
+
def as_json
|
210
|
+
# Build the payload's exception event
|
211
|
+
payload_event = {
|
212
|
+
app: {
|
213
|
+
version: app_version,
|
214
|
+
releaseStage: release_stage,
|
215
|
+
type: app_type
|
216
|
+
},
|
217
|
+
breadcrumbs: breadcrumbs.map(&:to_h),
|
218
|
+
context: context,
|
219
|
+
device: {
|
220
|
+
hostname: hostname,
|
221
|
+
runtimeVersions: runtime_versions,
|
222
|
+
time: @created_at
|
223
|
+
},
|
224
|
+
exceptions: exceptions,
|
225
|
+
featureFlags: @feature_flag_delegate.as_json,
|
226
|
+
groupingHash: grouping_hash,
|
227
|
+
metaData: meta_data,
|
228
|
+
session: session,
|
229
|
+
severity: severity,
|
230
|
+
severityReason: severity_reason,
|
231
|
+
unhandled: @unhandled,
|
232
|
+
user: user
|
233
|
+
}
|
234
|
+
|
235
|
+
payload_event.reject! {|k, v| v.nil? }
|
236
|
+
|
237
|
+
# return the payload hash
|
238
|
+
{
|
239
|
+
:apiKey => api_key,
|
240
|
+
:notifier => {
|
241
|
+
:name => NOTIFIER_NAME,
|
242
|
+
:version => NOTIFIER_VERSION,
|
243
|
+
:url => NOTIFIER_URL
|
244
|
+
},
|
245
|
+
:payloadVersion => CURRENT_PAYLOAD_VERSION,
|
246
|
+
:events => [payload_event]
|
247
|
+
}
|
248
|
+
end
|
249
|
+
|
250
|
+
##
|
251
|
+
# Returns the headers required for the notification.
|
252
|
+
#
|
253
|
+
# @return [Hash{String => String}]
|
254
|
+
def headers
|
255
|
+
{
|
256
|
+
"Bugsnag-Api-Key" => api_key,
|
257
|
+
"Bugsnag-Payload-Version" => CURRENT_PAYLOAD_VERSION,
|
258
|
+
"Bugsnag-Sent-At" => Time.now.utc.iso8601(3)
|
259
|
+
}
|
260
|
+
end
|
261
|
+
|
262
|
+
##
|
263
|
+
# Whether this report should be ignored and not sent.
|
264
|
+
#
|
265
|
+
# @return [Boolean]
|
266
|
+
def ignore?
|
267
|
+
@should_ignore
|
268
|
+
end
|
269
|
+
|
270
|
+
##
|
271
|
+
# Data set on the configuration to be attached to every error notification.
|
272
|
+
#
|
273
|
+
# @return [Hash]
|
274
|
+
def request_data
|
275
|
+
configuration.request_data
|
276
|
+
end
|
277
|
+
|
278
|
+
##
|
279
|
+
# Tells the client this report should not be sent.
|
280
|
+
#
|
281
|
+
# @return [void]
|
282
|
+
def ignore!
|
283
|
+
@should_ignore = true
|
284
|
+
end
|
285
|
+
|
286
|
+
##
|
287
|
+
# Generates a summary to be attached as a breadcrumb
|
288
|
+
#
|
289
|
+
# @return [Hash] a Hash containing the report's error class, error message, and severity
|
290
|
+
def summary
|
291
|
+
# Guard against the exceptions array being removed/changed or emptied here
|
292
|
+
if exceptions.respond_to?(:first) && exceptions.first
|
293
|
+
{
|
294
|
+
:error_class => exceptions.first[:errorClass],
|
295
|
+
:message => exceptions.first[:message],
|
296
|
+
:severity => severity
|
297
|
+
}
|
298
|
+
else
|
299
|
+
{
|
300
|
+
:error_class => "Unknown",
|
301
|
+
:severity => severity
|
302
|
+
}
|
303
|
+
end
|
304
|
+
end
|
305
|
+
|
306
|
+
# A Hash containing arbitrary metadata
|
307
|
+
# @!attribute metadata
|
308
|
+
# @return [Hash]
|
309
|
+
def metadata
|
310
|
+
@meta_data
|
311
|
+
end
|
312
|
+
|
313
|
+
# @param metadata [Hash]
|
314
|
+
# @return [void]
|
315
|
+
def metadata=(metadata)
|
316
|
+
@meta_data = metadata
|
317
|
+
end
|
318
|
+
|
319
|
+
##
|
320
|
+
# Data from the current HTTP request. May be nil if no data has been recorded
|
321
|
+
#
|
322
|
+
# @return [Hash, nil]
|
323
|
+
def request
|
324
|
+
@meta_data[:request]
|
325
|
+
end
|
326
|
+
|
327
|
+
##
|
328
|
+
# Add values to metadata
|
329
|
+
#
|
330
|
+
# @overload add_metadata(section, data)
|
331
|
+
# Merges data into the given section of metadata
|
332
|
+
# @param section [String, Symbol]
|
333
|
+
# @param data [Hash]
|
334
|
+
#
|
335
|
+
# @overload add_metadata(section, key, value)
|
336
|
+
# Sets key to value in the given section of metadata. If the value is nil
|
337
|
+
# the key will be deleted
|
338
|
+
# @param section [String, Symbol]
|
339
|
+
# @param key [String, Symbol]
|
340
|
+
# @param value
|
341
|
+
#
|
342
|
+
# @return [void]
|
343
|
+
def add_metadata(section, key_or_data, *args)
|
344
|
+
@metadata_delegate.add_metadata(@meta_data, section, key_or_data, *args)
|
345
|
+
end
|
346
|
+
|
347
|
+
##
|
348
|
+
# Clear values from metadata
|
349
|
+
#
|
350
|
+
# @overload clear_metadata(section)
|
351
|
+
# Clears the given section of metadata
|
352
|
+
# @param section [String, Symbol]
|
353
|
+
#
|
354
|
+
# @overload clear_metadata(section, key)
|
355
|
+
# Clears the key in the given section of metadata
|
356
|
+
# @param section [String, Symbol]
|
357
|
+
# @param key [String, Symbol]
|
358
|
+
#
|
359
|
+
# @return [void]
|
360
|
+
def clear_metadata(section, *args)
|
361
|
+
@metadata_delegate.clear_metadata(@meta_data, section, *args)
|
362
|
+
end
|
363
|
+
|
364
|
+
# Get the array of stored feature flags
|
365
|
+
#
|
366
|
+
# @return [Array<Bugsnag::FeatureFlag>]
|
367
|
+
def feature_flags
|
368
|
+
@feature_flag_delegate.to_a
|
369
|
+
end
|
370
|
+
|
371
|
+
##
|
372
|
+
# Set information about the current user
|
373
|
+
#
|
374
|
+
# Additional user fields can be added as metadata in a "user" section
|
375
|
+
#
|
376
|
+
# Setting a field to 'nil' will remove it from the user data
|
377
|
+
#
|
378
|
+
# @param id [String, nil]
|
379
|
+
# @param email [String, nil]
|
380
|
+
# @param name [String, nil]
|
381
|
+
# @return [void]
|
382
|
+
def set_user(id = nil, email = nil, name = nil)
|
383
|
+
new_user = { id: id, email: email, name: name }
|
384
|
+
new_user.reject! { |key, value| value.nil? }
|
385
|
+
|
386
|
+
@user = new_user
|
387
|
+
end
|
388
|
+
|
389
|
+
def unhandled=(new_unhandled)
|
390
|
+
# fix the handled/unhandled counts in the current session
|
391
|
+
update_handled_counts(new_unhandled, @unhandled)
|
392
|
+
|
393
|
+
@unhandled = new_unhandled
|
394
|
+
end
|
395
|
+
|
396
|
+
##
|
397
|
+
# Returns true if the unhandled flag has been changed from its initial value
|
398
|
+
#
|
399
|
+
# @api private
|
400
|
+
# @return [Boolean]
|
401
|
+
def unhandled_overridden?
|
402
|
+
@unhandled != @initial_unhandled
|
403
|
+
end
|
404
|
+
|
405
|
+
private
|
406
|
+
|
407
|
+
attr_reader :feature_flag_delegate
|
408
|
+
|
409
|
+
def update_handled_counts(is_unhandled, was_unhandled)
|
410
|
+
# do nothing if there is no session to update
|
411
|
+
return if @session.nil?
|
412
|
+
|
413
|
+
# increment the counts for the current unhandled value
|
414
|
+
if is_unhandled
|
415
|
+
@session[:events][:unhandled] += 1
|
416
|
+
else
|
417
|
+
@session[:events][:handled] += 1
|
418
|
+
end
|
419
|
+
|
420
|
+
# decrement the counts for the previous unhandled value
|
421
|
+
if was_unhandled
|
422
|
+
@session[:events][:unhandled] -= 1
|
423
|
+
else
|
424
|
+
@session[:events][:handled] -= 1
|
425
|
+
end
|
426
|
+
end
|
427
|
+
|
428
|
+
def generate_exception_list
|
429
|
+
raw_exceptions.map do |exception|
|
430
|
+
class_name = error_class(exception)
|
431
|
+
|
432
|
+
{
|
433
|
+
errorClass: class_name,
|
434
|
+
message: error_message(exception, class_name),
|
435
|
+
stacktrace: Stacktrace.process(exception.backtrace, configuration)
|
436
|
+
}
|
437
|
+
end
|
438
|
+
end
|
439
|
+
|
440
|
+
def generate_error_list
|
441
|
+
exceptions.map do |exception|
|
442
|
+
Error.new(exception[:errorClass], exception[:message], exception[:stacktrace])
|
443
|
+
end
|
444
|
+
end
|
445
|
+
|
446
|
+
def error_class(exception)
|
447
|
+
# The "Class" check is for some strange exceptions like Timeout::Error
|
448
|
+
# which throw the error class instead of an instance
|
449
|
+
(exception.is_a? Class) ? exception.name : exception.class.name
|
450
|
+
end
|
451
|
+
|
452
|
+
def error_message(exception, class_name)
|
453
|
+
# Ruby 3.2 added Exception#detailed_message for Gems like "Did you mean"
|
454
|
+
# to annotate an exception's message
|
455
|
+
return exception.message unless exception.respond_to?(:detailed_message)
|
456
|
+
|
457
|
+
# the "highlight" argument may add terminal escape codes to the output,
|
458
|
+
# which we don't want to include
|
459
|
+
# it _should_ always be present but it's possible to forget to add it or
|
460
|
+
# to have implemented this method before Ruby 3.2
|
461
|
+
message =
|
462
|
+
begin
|
463
|
+
exception.detailed_message(highlight: false)
|
464
|
+
rescue ArgumentError
|
465
|
+
exception.detailed_message
|
466
|
+
end
|
467
|
+
|
468
|
+
# the string returned by 'detailed_message' defaults to 'ASCII_8BIT' but
|
469
|
+
# is actually UTF-8 encoded; we can't convert the encoding normally as its
|
470
|
+
# internal encoding doesn't match its actual encoding
|
471
|
+
message.force_encoding(::Encoding::UTF_8) if message.encoding == ::Encoding::ASCII_8BIT
|
472
|
+
|
473
|
+
# remove the class name to be consistent with Exception#message
|
474
|
+
message.sub!(" (#{class_name})".encode(message.encoding), "") rescue nil
|
475
|
+
|
476
|
+
message
|
477
|
+
end
|
478
|
+
|
479
|
+
def generate_raw_exceptions(exception)
|
480
|
+
exceptions = []
|
481
|
+
|
482
|
+
ex = exception
|
483
|
+
while ex != nil && !exceptions.include?(ex) && exceptions.length < MAX_EXCEPTIONS_TO_UNWRAP
|
484
|
+
|
485
|
+
unless ex.is_a? Exception
|
486
|
+
if ex.respond_to?(:to_exception)
|
487
|
+
ex = ex.to_exception
|
488
|
+
elsif ex.respond_to?(:exception)
|
489
|
+
ex = ex.exception
|
490
|
+
end
|
491
|
+
end
|
492
|
+
|
493
|
+
unless ex.is_a?(Exception) || (defined?(Java::JavaLang::Throwable) && ex.is_a?(Java::JavaLang::Throwable))
|
494
|
+
configuration.warn("Converting non-Exception to RuntimeError: #{ex.inspect}")
|
495
|
+
ex = RuntimeError.new(ex.to_s)
|
496
|
+
ex.set_backtrace caller
|
497
|
+
end
|
498
|
+
|
499
|
+
exceptions << ex
|
500
|
+
|
501
|
+
if ex.respond_to?(:cause) && ex.cause
|
502
|
+
ex = ex.cause
|
503
|
+
elsif ex.respond_to?(:continued_exception) && ex.continued_exception
|
504
|
+
ex = ex.continued_exception
|
505
|
+
elsif ex.respond_to?(:original_exception) && ex.original_exception
|
506
|
+
ex = ex.original_exception
|
507
|
+
else
|
508
|
+
ex = nil
|
509
|
+
end
|
510
|
+
end
|
511
|
+
|
512
|
+
exceptions
|
513
|
+
end
|
514
|
+
end
|
515
|
+
# rubocop:enable Metrics/ClassLength
|
516
|
+
end
|
@@ -0,0 +1,182 @@
|
|
1
|
+
require 'thread'
|
2
|
+
require 'time'
|
3
|
+
require 'securerandom'
|
4
|
+
|
5
|
+
module Bugsnag
|
6
|
+
class SessionTracker
|
7
|
+
THREAD_SESSION = "bugsnag_session"
|
8
|
+
SESSION_PAYLOAD_VERSION = "1.0"
|
9
|
+
MUTEX = Mutex.new
|
10
|
+
|
11
|
+
attr_reader :session_counts
|
12
|
+
|
13
|
+
##
|
14
|
+
# Sets the session information for this thread.
|
15
|
+
def self.set_current_session(session)
|
16
|
+
Thread.current[THREAD_SESSION] = session
|
17
|
+
end
|
18
|
+
|
19
|
+
##
|
20
|
+
# Returns the session information for this thread.
|
21
|
+
def self.get_current_session
|
22
|
+
Thread.current[THREAD_SESSION]
|
23
|
+
end
|
24
|
+
|
25
|
+
##
|
26
|
+
# Initializes the session tracker.
|
27
|
+
def initialize
|
28
|
+
require 'concurrent'
|
29
|
+
|
30
|
+
@session_counts = Concurrent::Hash.new(0)
|
31
|
+
end
|
32
|
+
|
33
|
+
##
|
34
|
+
# Starts a new session, storing it on the current thread.
|
35
|
+
#
|
36
|
+
# This allows Bugsnag to track error rates for a release.
|
37
|
+
#
|
38
|
+
# @return [void]
|
39
|
+
def start_session
|
40
|
+
return unless Bugsnag.configuration.enable_sessions && Bugsnag.configuration.should_notify_release_stage?
|
41
|
+
|
42
|
+
start_delivery_thread
|
43
|
+
start_time = Time.now().utc().strftime('%Y-%m-%dT%H:%M:00')
|
44
|
+
new_session = {
|
45
|
+
id: SecureRandom.uuid,
|
46
|
+
startedAt: start_time,
|
47
|
+
paused?: false,
|
48
|
+
events: {
|
49
|
+
handled: 0,
|
50
|
+
unhandled: 0
|
51
|
+
}
|
52
|
+
}
|
53
|
+
SessionTracker.set_current_session(new_session)
|
54
|
+
add_session(start_time)
|
55
|
+
end
|
56
|
+
|
57
|
+
alias_method :create_session, :start_session
|
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
|
+
|
100
|
+
##
|
101
|
+
# Delivers the current session_counts lists to the session endpoint.
|
102
|
+
def send_sessions
|
103
|
+
sessions = []
|
104
|
+
counts = @session_counts
|
105
|
+
@session_counts = Concurrent::Hash.new(0)
|
106
|
+
counts.each do |min, count|
|
107
|
+
sessions << {
|
108
|
+
:startedAt => min,
|
109
|
+
:sessionsStarted => count
|
110
|
+
}
|
111
|
+
end
|
112
|
+
deliver(sessions)
|
113
|
+
end
|
114
|
+
|
115
|
+
private
|
116
|
+
def start_delivery_thread
|
117
|
+
MUTEX.synchronize do
|
118
|
+
@started = nil unless defined?(@started)
|
119
|
+
return if @started == Process.pid
|
120
|
+
@started = Process.pid
|
121
|
+
at_exit do
|
122
|
+
if !@delivery_thread.nil?
|
123
|
+
@delivery_thread.execute
|
124
|
+
@delivery_thread.shutdown
|
125
|
+
else
|
126
|
+
if @session_counts.size > 0
|
127
|
+
send_sessions
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
@delivery_thread = Concurrent::TimerTask.execute(execution_interval: 10) do
|
132
|
+
if @session_counts.size > 0
|
133
|
+
send_sessions
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
def add_session(min)
|
140
|
+
@session_counts[min] += 1
|
141
|
+
end
|
142
|
+
|
143
|
+
def deliver(session_payload)
|
144
|
+
if session_payload.length == 0
|
145
|
+
Bugsnag.configuration.debug("No sessions to deliver")
|
146
|
+
return
|
147
|
+
end
|
148
|
+
|
149
|
+
if !Bugsnag.configuration.valid_api_key?
|
150
|
+
Bugsnag.configuration.debug("Not delivering sessions due to an invalid api_key")
|
151
|
+
return
|
152
|
+
end
|
153
|
+
|
154
|
+
body = {
|
155
|
+
:notifier => {
|
156
|
+
:name => Bugsnag::Report::NOTIFIER_NAME,
|
157
|
+
:url => Bugsnag::Report::NOTIFIER_URL,
|
158
|
+
:version => Bugsnag::Report::NOTIFIER_VERSION
|
159
|
+
},
|
160
|
+
:device => {
|
161
|
+
:hostname => Bugsnag.configuration.hostname,
|
162
|
+
:runtimeVersions => Bugsnag.configuration.runtime_versions
|
163
|
+
},
|
164
|
+
:app => {
|
165
|
+
:version => Bugsnag.configuration.app_version,
|
166
|
+
:releaseStage => Bugsnag.configuration.release_stage,
|
167
|
+
:type => Bugsnag.configuration.app_type
|
168
|
+
},
|
169
|
+
:sessionCounts => session_payload
|
170
|
+
}
|
171
|
+
payload = ::JSON.dump(body)
|
172
|
+
|
173
|
+
headers = {
|
174
|
+
"Bugsnag-Api-Key" => Bugsnag.configuration.api_key,
|
175
|
+
"Bugsnag-Payload-Version" => SESSION_PAYLOAD_VERSION
|
176
|
+
}
|
177
|
+
|
178
|
+
options = {:headers => headers}
|
179
|
+
Bugsnag::Delivery[Bugsnag.configuration.delivery_method].deliver(Bugsnag.configuration.session_endpoint, payload, Bugsnag.configuration, options)
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|