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.
Files changed (106) hide show
  1. checksums.yaml +5 -5
  2. data/.yardopts +12 -0
  3. data/CHANGELOG.md +814 -0
  4. data/README.md +21 -25
  5. data/VERSION +1 -1
  6. data/bugsnag.gemspec +19 -8
  7. data/lib/bugsnag/breadcrumb_type.rb +14 -0
  8. data/lib/bugsnag/breadcrumbs/breadcrumb.rb +109 -0
  9. data/lib/bugsnag/breadcrumbs/breadcrumbs.rb +13 -0
  10. data/lib/bugsnag/breadcrumbs/on_breadcrumb_callback_list.rb +48 -0
  11. data/lib/bugsnag/breadcrumbs/validator.rb +29 -0
  12. data/lib/bugsnag/cleaner.rb +170 -59
  13. data/lib/bugsnag/code_extractor.rb +137 -0
  14. data/lib/bugsnag/configuration.rb +670 -45
  15. data/lib/bugsnag/delivery/synchronous.rb +31 -14
  16. data/lib/bugsnag/delivery/thread_queue.rb +23 -6
  17. data/lib/bugsnag/delivery.rb +13 -0
  18. data/lib/bugsnag/endpoint_configuration.rb +11 -0
  19. data/lib/bugsnag/endpoint_validator.rb +80 -0
  20. data/lib/bugsnag/error.rb +25 -0
  21. data/lib/bugsnag/event.rb +5 -0
  22. data/lib/bugsnag/feature_flag.rb +74 -0
  23. data/lib/bugsnag/helpers.rb +121 -25
  24. data/lib/bugsnag/integrations/delayed_job.rb +51 -0
  25. data/lib/bugsnag/integrations/mailman.rb +43 -0
  26. data/lib/bugsnag/integrations/mongo.rb +133 -0
  27. data/lib/bugsnag/integrations/que.rb +53 -0
  28. data/lib/bugsnag/integrations/rack.rb +83 -0
  29. data/lib/bugsnag/integrations/rails/active_job.rb +100 -0
  30. data/lib/bugsnag/{rails → integrations/rails}/active_record_rescue.rb +10 -1
  31. data/lib/bugsnag/{rails → integrations/rails}/controller_methods.rb +1 -9
  32. data/lib/bugsnag/integrations/rails/rails_breadcrumbs.rb +115 -0
  33. data/lib/bugsnag/integrations/railtie.rb +153 -0
  34. data/lib/bugsnag/integrations/rake.rb +74 -0
  35. data/lib/bugsnag/integrations/resque.rb +94 -0
  36. data/lib/bugsnag/integrations/shoryuken.rb +50 -0
  37. data/lib/bugsnag/integrations/sidekiq.rb +68 -0
  38. data/lib/bugsnag/meta_data.rb +1 -0
  39. data/lib/bugsnag/middleware/active_job.rb +18 -0
  40. data/lib/bugsnag/middleware/breadcrumbs.rb +21 -0
  41. data/lib/bugsnag/middleware/callbacks.rb +6 -8
  42. data/lib/bugsnag/middleware/classify_error.rb +50 -0
  43. data/lib/bugsnag/middleware/clearance_user.rb +33 -0
  44. data/lib/bugsnag/middleware/delayed_job.rb +93 -0
  45. data/lib/bugsnag/middleware/discard_error_class.rb +30 -0
  46. data/lib/bugsnag/middleware/exception_meta_data.rb +42 -0
  47. data/lib/bugsnag/middleware/ignore_error_class.rb +26 -0
  48. data/lib/bugsnag/middleware/mailman.rb +6 -4
  49. data/lib/bugsnag/middleware/rack_request.rb +126 -30
  50. data/lib/bugsnag/middleware/rails3_request.rb +15 -17
  51. data/lib/bugsnag/middleware/rake.rb +7 -5
  52. data/lib/bugsnag/middleware/session_data.rb +25 -0
  53. data/lib/bugsnag/middleware/sidekiq.rb +9 -4
  54. data/lib/bugsnag/middleware/suggestion_data.rb +34 -0
  55. data/lib/bugsnag/middleware/warden_user.rb +11 -6
  56. data/lib/bugsnag/middleware_stack.rb +62 -9
  57. data/lib/bugsnag/on_error_callbacks.rb +33 -0
  58. data/lib/bugsnag/report.rb +516 -0
  59. data/lib/bugsnag/session_tracker.rb +182 -0
  60. data/lib/bugsnag/stacktrace.rb +82 -0
  61. data/lib/bugsnag/tasks/bugsnag.rake +2 -70
  62. data/lib/bugsnag/utility/circular_buffer.rb +62 -0
  63. data/lib/bugsnag/utility/duplicator.rb +124 -0
  64. data/lib/bugsnag/utility/feature_data_store.rb +41 -0
  65. data/lib/bugsnag/utility/feature_flag_delegate.rb +89 -0
  66. data/lib/bugsnag/utility/metadata_delegate.rb +102 -0
  67. data/lib/bugsnag.rb +528 -80
  68. metadata +61 -123
  69. data/.document +0 -5
  70. data/.gitignore +0 -52
  71. data/.rspec +0 -3
  72. data/.travis.yml +0 -14
  73. data/CONTRIBUTING.md +0 -47
  74. data/Gemfile +0 -2
  75. data/Rakefile +0 -29
  76. data/lib/bugsnag/capistrano.rb +0 -7
  77. data/lib/bugsnag/capistrano2.rb +0 -32
  78. data/lib/bugsnag/delay/resque.rb +0 -21
  79. data/lib/bugsnag/delayed_job.rb +0 -57
  80. data/lib/bugsnag/deploy.rb +0 -34
  81. data/lib/bugsnag/mailman.rb +0 -28
  82. data/lib/bugsnag/middleware/rails2_request.rb +0 -52
  83. data/lib/bugsnag/notification.rb +0 -459
  84. data/lib/bugsnag/rack.rb +0 -53
  85. data/lib/bugsnag/rails/action_controller_rescue.rb +0 -62
  86. data/lib/bugsnag/rails.rb +0 -66
  87. data/lib/bugsnag/railtie.rb +0 -80
  88. data/lib/bugsnag/rake.rb +0 -25
  89. data/lib/bugsnag/resque.rb +0 -40
  90. data/lib/bugsnag/sidekiq.rb +0 -42
  91. data/lib/bugsnag/tasks/bugsnag.cap +0 -48
  92. data/rails/init.rb +0 -7
  93. data/spec/cleaner_spec.rb +0 -138
  94. data/spec/code_spec.rb +0 -86
  95. data/spec/fixtures/crashes/end_of_file.rb +0 -9
  96. data/spec/fixtures/crashes/short_file.rb +0 -1
  97. data/spec/fixtures/crashes/start_of_file.rb +0 -9
  98. data/spec/fixtures/middleware/internal_info_setter.rb +0 -11
  99. data/spec/fixtures/middleware/public_info_setter.rb +0 -11
  100. data/spec/fixtures/tasks/Rakefile +0 -15
  101. data/spec/helper_spec.rb +0 -163
  102. data/spec/integration_spec.rb +0 -132
  103. data/spec/middleware_spec.rb +0 -181
  104. data/spec/notification_spec.rb +0 -877
  105. data/spec/rack_spec.rb +0 -56
  106. 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