bugsnag 6.2.0 → 6.3.0.beta.0

Sign up to get free protection for your applications and to get access to all the features.
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