bugsnag 6.2.0 → 6.3.0.beta.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
  SHA1:
3
- metadata.gz: 115e70bf10e40eb20d373f99d89703779a3ab9d4
4
- data.tar.gz: f147bcba11749fb91ec9ff6c2ece8ef458c022ad
3
+ metadata.gz: b0643f03a70da437fa3983ee21f1f9ec4466085e
4
+ data.tar.gz: bdcdb50f10d93babed837ad0f6ec3ad174c765b6
5
5
  SHA512:
6
- metadata.gz: 3c733148ef6e837b4494627fdcbfdc3ebeac294d5cf77e0c52587ca865cbefaae6c7bfe503e90002cf36dc217b8597aac7848007fbdc74b1a2a4f9afd6f82b29
7
- data.tar.gz: 287eb4af362032892fea941b3dc360a59ac37fa20f86edc11a25e17ba1afaac5210d6cd3faf39585209c44fc208fe5f5b566819abd4f661554e71bb761935ede
6
+ metadata.gz: 4d4341b5a7e9566d16c4f9fee869d570824ee89a8092260f54c19791984241f163237beb564622850cb163479f5dff81d34860917f91f16eb639461bcca755f7
7
+ data.tar.gz: 541e02bf501da119d9ddb4acde6ca3df1c2805c4513c7ab201a837d5d18e54a9eefbf95205fa5dd433b0f811d56c801fec3b53d871a696e9c7b8738db1b12c35
data/VERSION CHANGED
@@ -1 +1 @@
1
- 6.2.0
1
+ 6.3.0.beta.0
@@ -7,6 +7,7 @@ require "bugsnag/meta_data"
7
7
  require "bugsnag/report"
8
8
  require "bugsnag/cleaner"
9
9
  require "bugsnag/helpers"
10
+ require "bugsnag/session_tracker"
10
11
 
11
12
  require "bugsnag/delivery"
12
13
  require "bugsnag/delivery/synchronous"
@@ -38,6 +39,8 @@ module Bugsnag
38
39
  configuration.warn("No valid API key has been set, notifications will not be sent")
39
40
  @key_warning = true
40
41
  end
42
+
43
+ session_tracker.config = configuration
41
44
  end
42
45
 
43
46
  # Explicitly notify of an exception
@@ -108,8 +111,8 @@ module Bugsnag
108
111
 
109
112
  # Deliver
110
113
  configuration.info("Notifying #{configuration.endpoint} of #{report.exceptions.last[:errorClass]}")
111
- payload_string = ::JSON.dump(Bugsnag::Helpers.trim_if_needed(report.as_json))
112
- Bugsnag::Delivery[configuration.delivery_method].deliver(configuration.endpoint, payload_string, configuration)
114
+ options = {:headers => report.headers, :trim_payload => true}
115
+ Bugsnag::Delivery[configuration.delivery_method].deliver(configuration.endpoint, report.as_json, configuration, options)
113
116
  end
114
117
  end
115
118
 
@@ -119,6 +122,11 @@ module Bugsnag
119
122
  @configuration || LOCK.synchronize { @configuration ||= Bugsnag::Configuration.new }
120
123
  end
121
124
 
125
+ def session_tracker
126
+ @session_tracker = nil unless defined?(@session_tracker)
127
+ @session_tracker || LOCK.synchronize { @session_tracker ||= Bugsnag::SessionTracker.new(configuration)}
128
+ end
129
+
122
130
  # Allow access to "before notify" callbacks
123
131
  def before_notify_callbacks
124
132
  Bugsnag.configuration.request_data[:before_callbacks] ||= []
@@ -7,6 +7,7 @@ require "bugsnag/middleware/exception_meta_data"
7
7
  require "bugsnag/middleware/ignore_error_class"
8
8
  require "bugsnag/middleware/suggestion_data"
9
9
  require "bugsnag/middleware/classify_error"
10
+ require "bugsnag/middleware/session_data"
10
11
 
11
12
  module Bugsnag
12
13
  class Configuration
@@ -22,7 +23,7 @@ module Bugsnag
22
23
  attr_accessor :app_type
23
24
  attr_accessor :meta_data_filters
24
25
  attr_accessor :endpoint
25
- attr_accessor :logger
26
+ attr_accessor :logger
26
27
  attr_accessor :middleware
27
28
  attr_accessor :internal_middleware
28
29
  attr_accessor :proxy_host
@@ -32,10 +33,13 @@ module Bugsnag
32
33
  attr_accessor :timeout
33
34
  attr_accessor :hostname
34
35
  attr_accessor :ignore_classes
36
+ attr_accessor :track_sessions
37
+ attr_accessor :session_endpoint
35
38
 
36
39
  API_KEY_REGEX = /[0-9a-f]{32}/i
37
40
  THREAD_LOCAL_NAME = "bugsnag_req_data"
38
41
  DEFAULT_ENDPOINT = "https://notify.bugsnag.com"
42
+ DEFAULT_SESSION_ENDPOINT = "https://sessions.bugsnag.com"
39
43
 
40
44
  DEFAULT_META_DATA_FILTERS = [
41
45
  /authorization/i,
@@ -57,6 +61,8 @@ module Bugsnag
57
61
  self.hostname = default_hostname
58
62
  self.timeout = 15
59
63
  self.notify_release_stages = nil
64
+ self.track_sessions = false
65
+ self.session_endpoint = DEFAULT_SESSION_ENDPOINT
60
66
 
61
67
  # SystemExit and Interrupt are common Exception types seen with successful
62
68
  # exits and are not automatically reported to Bugsnag
@@ -81,6 +87,7 @@ module Bugsnag
81
87
  self.internal_middleware.use Bugsnag::Middleware::IgnoreErrorClass
82
88
  self.internal_middleware.use Bugsnag::Middleware::SuggestionData
83
89
  self.internal_middleware.use Bugsnag::Middleware::ClassifyError
90
+ self.internal_middleware.use Bugsnag::Middleware::SessionData
84
91
 
85
92
  self.middleware = Bugsnag::MiddlewareStack.new
86
93
  self.middleware.use Bugsnag::Middleware::Callbacks
@@ -4,13 +4,19 @@ require "uri"
4
4
  module Bugsnag
5
5
  module Delivery
6
6
  class Synchronous
7
- HEADERS = {"Content-Type" => "application/json"}
7
+ BACKOFF_THREADS = {}
8
+ BACKOFF_REQUESTS = {}
9
+ BACKOFF_LOCK = Mutex.new
8
10
 
9
11
  class << self
10
- def deliver(url, body, configuration)
12
+ def deliver(url, body, configuration, options={})
11
13
  begin
12
- response = request(url, body, configuration)
14
+ response = request(url, body, configuration, options)
13
15
  configuration.debug("Request to #{url} completed, status: #{response.code}")
16
+ success = options[:success] || '200'
17
+ if options[:backoff] && !(response.code == success)
18
+ backoff(url, body, configuration, options)
19
+ end
14
20
  rescue StandardError => e
15
21
  # KLUDGE: Since we don't re-raise http exceptions, this breaks rspec
16
22
  raise if e.class.to_s == "RSpec::Expectations::ExpectationNotMetError"
@@ -22,9 +28,14 @@ module Bugsnag
22
28
 
23
29
  private
24
30
 
25
- def request(url, body, configuration)
31
+ def request(url, body, configuration, options)
26
32
  uri = URI.parse(url)
27
33
 
34
+ if options[:trim_payload]
35
+ body = Bugsnag::Helpers.trim_if_needed(body)
36
+ end
37
+ payload = ::JSON.dump(body)
38
+
28
39
  if configuration.proxy_host
29
40
  http = Net::HTTP.new(uri.host, uri.port, configuration.proxy_host, configuration.proxy_port, configuration.proxy_user, configuration.proxy_password)
30
41
  else
@@ -39,14 +50,104 @@ module Bugsnag
39
50
  http.ca_file = configuration.ca_file if configuration.ca_file
40
51
  end
41
52
 
42
- request = Net::HTTP::Post.new(path(uri), HEADERS)
43
- request.body = body
53
+ headers = options.key?(:headers) ? options[:headers] : {}
54
+ headers.merge!(default_headers)
55
+
56
+ request = Net::HTTP::Post.new(path(uri), headers)
57
+ request.body = payload
58
+
44
59
  http.request(request)
45
60
  end
46
61
 
62
+ def backoff(url, body, configuration, options)
63
+ # Ensure we have the latest configuration for making these requests
64
+ @latest_configuration = configuration
65
+
66
+ BACKOFF_LOCK.lock
67
+ begin
68
+ # Define an exit function once to handle outstanding requests
69
+ @registered_at_exit = false unless defined?(@registered_at_exit)
70
+ if !@registered_at_exit
71
+ @registered_at_exit = true
72
+ at_exit do
73
+ backoff_exit
74
+ end
75
+ end
76
+ if BACKOFF_REQUESTS[url] && !BACKOFF_REQUESTS[url].empty?
77
+ last_request = BACKOFF_REQUESTS[url].last
78
+ new_body_length = ::JSON.dump(body).length
79
+ old_body_length = ::JSON.dump(last_request[:body]).length
80
+ if new_body_length + old_body_length >= Bugsnag::Helpers::MAX_PAYLOAD_LENGTH
81
+ BACKOFF_REQUESTS[url].push({:body => body, :options => options})
82
+ else
83
+ Bugsnag::Helpers::deep_merge!(last_request, {:body => body, :options => options})
84
+ end
85
+ else
86
+ BACKOFF_REQUESTS[url] = [{:body => body, :options => options}]
87
+ end
88
+ if !(BACKOFF_THREADS[url] && BACKOFF_THREADS[url].status)
89
+ spawn_backoff_thread(url)
90
+ end
91
+ ensure
92
+ BACKOFF_LOCK.unlock
93
+ end
94
+ end
95
+
96
+ def backoff_exit
97
+ # Kill existing threads
98
+ BACKOFF_THREADS.each do |url, thread|
99
+ thread.exit
100
+ end
101
+ # Retry outstanding requests once, then exit
102
+ BACKOFF_REQUESTS.each do |url, requests|
103
+ requests.map! do |req|
104
+ response = request(url, req[:body], @latest_configuration, req[:options])
105
+ success = req[:options][:success] || '200'
106
+ response.code == success
107
+ end
108
+ requests.reject! { |i| i }
109
+ @latest_configuration.warn("Requests to #{url} finished, #{requests.size} failed")
110
+ end
111
+ end
112
+
113
+ def spawn_backoff_thread(url)
114
+ new_thread = Thread.new(url) do |url|
115
+ interval = 2
116
+ while BACKOFF_REQUESTS[url].size > 0
117
+ sleep(interval)
118
+ interval = interval * 2
119
+ interval = 600 if interval > 600
120
+ BACKOFF_LOCK.lock
121
+ begin
122
+ BACKOFF_REQUESTS[url].map! do |req|
123
+ response = request(url, req[:body], @latest_configuration, req[:options])
124
+ success = req[:options][:success] || '200'
125
+ if response.code == success
126
+ @latest_configuration.debug("Request to #{url} completed, status: #{response.code}")
127
+ false
128
+ else
129
+ req
130
+ end
131
+ end
132
+ BACKOFF_REQUESTS[url].reject! { |i| !i }
133
+ ensure
134
+ BACKOFF_LOCK.unlock
135
+ end
136
+ end
137
+ end
138
+ BACKOFF_THREADS[url] = new_thread
139
+ end
140
+
47
141
  def path(uri)
48
142
  uri.path == "" ? "/" : uri.path
49
143
  end
144
+
145
+ def default_headers
146
+ {
147
+ "Content-Type" => "application/json",
148
+ "Bugsnag-Sent-At" => Time.now().utc().strftime('%Y-%m-%dT%H:%M:%S')
149
+ }
150
+ end
50
151
  end
51
152
  end
52
153
  end
@@ -8,7 +8,7 @@ module Bugsnag
8
8
  MUTEX = Mutex.new
9
9
 
10
10
  class << self
11
- def deliver(url, body, configuration)
11
+ def deliver(url, body, configuration, options={})
12
12
  @configuration = configuration
13
13
 
14
14
  start_once!
@@ -19,7 +19,7 @@ module Bugsnag
19
19
  end
20
20
 
21
21
  # Add delivery to the worker thread
22
- @queue.push proc { super(url, body, configuration) }
22
+ @queue.push proc { super(url, body, configuration, options) }
23
23
  end
24
24
 
25
25
  private
@@ -23,6 +23,30 @@ module Bugsnag
23
23
  remove_metadata_from_events(reduced_value)
24
24
  end
25
25
 
26
+ def self.deep_merge(l_hash, r_hash)
27
+ l_hash.merge(r_hash) do |key, l_val, r_val|
28
+ if l_val.is_a?(Hash) && r_val.is_a?(Hash)
29
+ deep_merge(l_val, r_val)
30
+ elsif l_val.is_a?(Array) && r_val.is_a?(Array)
31
+ l_val.concat(r_val)
32
+ else
33
+ r_val
34
+ end
35
+ end
36
+ end
37
+
38
+ def self.deep_merge!(l_hash, r_hash)
39
+ l_hash.merge!(r_hash) do |key, l_val, r_val|
40
+ if l_val.is_a?(Hash) && r_val.is_a?(Hash)
41
+ deep_merge(l_val, r_val)
42
+ elsif l_val.is_a?(Array) && r_val.is_a?(Array)
43
+ l_val.concat(r_val)
44
+ else
45
+ r_val
46
+ end
47
+ end
48
+ end
49
+
26
50
  private
27
51
 
28
52
  TRUNCATION_INFO = '[TRUNCATED]'
@@ -34,6 +34,7 @@ module Bugsnag
34
34
  def call(env)
35
35
  # Set the request data for bugsnag middleware to use
36
36
  Bugsnag.configuration.set_request_data(:rack_env, env)
37
+ Bugsnag.session_tracker.create_session
37
38
 
38
39
  begin
39
40
  response = @app.call(env)
@@ -5,6 +5,7 @@ module Bugsnag::Rails
5
5
  end
6
6
 
7
7
  module ClassMethods
8
+
8
9
  private
9
10
  def before_bugsnag_notify(*methods, &block)
10
11
  _add_bugsnag_notify_callback(:before_callbacks, *methods, &block)
@@ -0,0 +1,21 @@
1
+ module Bugsnag::Middleware
2
+ class SessionData
3
+ def initialize(bugsnag)
4
+ @bugsnag = bugsnag
5
+ end
6
+
7
+ def call(report)
8
+ session = Bugsnag::SessionTracker.get_current_session
9
+ unless session.nil?
10
+ if report.unhandled
11
+ session[:events][:unhandled] += 1
12
+ else
13
+ session[:events][:handled] += 1
14
+ end
15
+ report.session = session
16
+ end
17
+
18
+ @bugsnag.call(report)
19
+ end
20
+ end
21
+ end
@@ -17,8 +17,9 @@ module Bugsnag
17
17
 
18
18
  MAX_EXCEPTIONS_TO_UNWRAP = 5
19
19
 
20
- CURRENT_PAYLOAD_VERSION = "2"
20
+ CURRENT_PAYLOAD_VERSION = "4.0"
21
21
 
22
+ attr_reader :unhandled
22
23
  attr_accessor :api_key
23
24
  attr_accessor :app_type
24
25
  attr_accessor :app_version
@@ -31,6 +32,7 @@ module Bugsnag
31
32
  attr_accessor :meta_data
32
33
  attr_accessor :raw_exceptions
33
34
  attr_accessor :release_stage
35
+ attr_accessor :session
34
36
  attr_accessor :severity
35
37
  attr_accessor :severity_reason
36
38
  attr_accessor :user
@@ -92,7 +94,7 @@ module Bugsnag
92
94
  },
93
95
  exceptions: exceptions,
94
96
  groupingHash: grouping_hash,
95
- payloadVersion: CURRENT_PAYLOAD_VERSION,
97
+ session: session,
96
98
  severity: severity,
97
99
  severityReason: severity_reason,
98
100
  unhandled: @unhandled,
@@ -108,7 +110,6 @@ module Bugsnag
108
110
 
109
111
  # return the payload hash
110
112
  {
111
- :apiKey => api_key,
112
113
  :notifier => {
113
114
  :name => NOTIFIER_NAME,
114
115
  :version => NOTIFIER_VERSION,
@@ -118,6 +119,14 @@ module Bugsnag
118
119
  }
119
120
  end
120
121
 
122
+ def headers
123
+ {
124
+ "Bugsnag-Api-Key" => api_key,
125
+ "Bugsnag-Payload-Version" => CURRENT_PAYLOAD_VERSION,
126
+ "Bugsnag-Sent-At" => Time.now().utc().strftime('%Y-%m-%dT%H:%M:%S')
127
+ }
128
+ end
129
+
121
130
  def ignore?
122
131
  @should_ignore
123
132
  end
@@ -0,0 +1,157 @@
1
+ require 'thread'
2
+ require 'time'
3
+ require 'securerandom'
4
+
5
+ module Bugsnag
6
+ class SessionTracker
7
+
8
+ THREAD_SESSION = "bugsnag_session"
9
+ TIME_THRESHOLD = 60
10
+ FALLBACK_TIME = 300
11
+ MAXIMUM_SESSION_COUNT = 50
12
+ SESSION_PAYLOAD_VERSION = "1.0"
13
+
14
+ attr_reader :session_counts
15
+ attr_writer :config
16
+
17
+ def self.set_current_session(session)
18
+ Thread.current[THREAD_SESSION] = session
19
+ end
20
+
21
+ def self.get_current_session
22
+ Thread.current[THREAD_SESSION]
23
+ end
24
+
25
+ def initialize(configuration)
26
+ @session_counts = {}
27
+ @config = configuration
28
+ @mutex = Mutex.new
29
+ @last_sent = Time.now
30
+ end
31
+
32
+ def create_session
33
+ return unless @config.track_sessions
34
+ start_time = Time.now().utc().strftime('%Y-%m-%dT%H:%M:00')
35
+ new_session = {
36
+ :id => SecureRandom.uuid,
37
+ :startedAt => start_time,
38
+ :events => {
39
+ :handled => 0,
40
+ :unhandled => 0
41
+ }
42
+ }
43
+ SessionTracker.set_current_session(new_session)
44
+ add_thread = Thread.new { add_session(start_time) }
45
+ add_thread.join()
46
+ end
47
+
48
+ def send_sessions
49
+ @mutex.lock
50
+ begin
51
+ deliver_sessions
52
+ ensure
53
+ @mutex.unlock
54
+ end
55
+ end
56
+
57
+ private
58
+ def add_session(min)
59
+ @mutex.lock
60
+ begin
61
+ @registered_at_exit = false unless defined?(@registered_at_exit)
62
+ if !@registered_at_exit
63
+ @registered_at_exit = true
64
+ at_exit do
65
+ if !@deliver_fallback.nil? && @deliver_fallback.status == 'sleep'
66
+ @deliver_fallback.terminate
67
+ end
68
+ deliver_sessions
69
+ end
70
+ end
71
+ @session_counts[min] ||= 0
72
+ @session_counts[min] += 1
73
+ if Time.now() - @last_sent > TIME_THRESHOLD
74
+ deliver_sessions
75
+ end
76
+ ensure
77
+ @mutex.unlock
78
+ end
79
+ end
80
+
81
+ def deliver_sessions
82
+ return unless @config.track_sessions
83
+ sessions = []
84
+ @session_counts.each do |min, count|
85
+ sessions << {
86
+ :startedAt => min,
87
+ :sessionsStarted => count
88
+ }
89
+ if sessions.size >= MAXIMUM_SESSION_COUNT
90
+ deliver(sessions)
91
+ sessions = []
92
+ end
93
+ end
94
+ @session_counts = {}
95
+ reset_delivery_thread
96
+ deliver(sessions)
97
+ end
98
+
99
+ def reset_delivery_thread
100
+ if !@deliver_fallback.nil? && @deliver_fallback.status == 'sleep'
101
+ @deliver_fallback.terminate
102
+ end
103
+ @deliver_fallback = Thread.new do
104
+ sleep(FALLBACK_TIME)
105
+ deliver_sessions
106
+ end
107
+ end
108
+
109
+ def deliver(sessionCounts)
110
+ if sessionCounts.length == 0
111
+ @config.debug("No sessions to deliver")
112
+ return
113
+ end
114
+
115
+ if !@config.valid_api_key?
116
+ @config.debug("Not delivering sessions due to an invalid api_key")
117
+ return
118
+ end
119
+
120
+ if !@config.should_notify_release_stage?
121
+ @config.debug("Not delivering sessions due to notify_release_stages :#{@config.notify_release_stages.inspect}")
122
+ return
123
+ end
124
+
125
+ if @config.delivery_method != :thread_queue
126
+ @config.debug("Not delivering sessions due to asynchronous delivery being disabled")
127
+ return
128
+ end
129
+
130
+ payload = {
131
+ :notifier => {
132
+ :name => Bugsnag::Report::NOTIFIER_NAME,
133
+ :url => Bugsnag::Report::NOTIFIER_URL,
134
+ :version => Bugsnag::Report::NOTIFIER_VERSION
135
+ },
136
+ :device => {
137
+ :hostname => @config.hostname
138
+ },
139
+ :app => {
140
+ :version => @config.app_version,
141
+ :releaseStage => @config.release_stage,
142
+ :type => @config.app_type
143
+ },
144
+ :sessionCounts => sessionCounts
145
+ }
146
+
147
+ headers = {
148
+ "Bugsnag-Api-Key" => @config.api_key,
149
+ "Bugsnag-Payload-Version" => SESSION_PAYLOAD_VERSION
150
+ }
151
+
152
+ options = {:headers => headers, :backoff => true, :success => '202'}
153
+ @last_sent = Time.now
154
+ Bugsnag::Delivery[@config.delivery_method].deliver(@config.session_endpoint, payload, @config, options)
155
+ end
156
+ end
157
+ end