bugsnag 6.4.0 → 6.5.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: b31c931ac12f1ea2ce6344810690398c3b51e768
4
- data.tar.gz: 33f72ca3d7f4e22088634dae7b785d57bc9c874a
3
+ metadata.gz: 75340d844c86f98e0ef2519d4ca1416bf9de8ab8
4
+ data.tar.gz: 7a384090af717661a8acdcd2e2b3184b138b77e9
5
5
  SHA512:
6
- metadata.gz: 973b51212b74835700a90056058d1ac5a21aff86c722b1055a7303bc6f3b570e64b653eaad2d3a7b337419af9d71144f8112c93cb998c30acf2925b99bd691c2
7
- data.tar.gz: ad90142cf2ae5dc7bb737c0c0156d9840cc39bc35e8b751028f945241b7678b54d00e840aa30bfd54bc25d706d95047fd87c4908c013891cf4f12484b0cee61d
6
+ metadata.gz: eb256344706e647174e3dc9abac3376b7b3966c6f706fdb77dbaf2ae9c50527490d4ee591265a14a0787678f018affde73a0b3a5c7a30ea676da27bc7731612f
7
+ data.tar.gz: 71b8138725c4b11278eafef1e9a02c61977fe4a6f0460684e582ca9276b1bc4d7841cd0130b643882e0fc7e640e0eb33df7456c6004f6bb5d3b1ee3c031bac0d
@@ -1,6 +1,14 @@
1
1
  Changelog
2
2
  =========
3
3
 
4
+ ## 6.5.0 (04 Jan 2018)
5
+
6
+ ### Enhancements
7
+
8
+ * Adds support for tracking sessions and crash rate by setting the configuration option `configuration.track_sessions` to `true`.
9
+ Sessions can be manually created using `Bugsnag.start_session`, and manually delivered using `Bugsnag.send_sessions`.
10
+ | [#411](https://github.com/bugsnag/bugsnag-ruby/pull/411)
11
+
4
12
  ## 6.4.0 (21 Dec 2017)
5
13
 
6
14
  ### Enhancements
data/VERSION CHANGED
@@ -1 +1 @@
1
- 6.4.0
1
+ 6.5.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
@@ -113,8 +116,8 @@ module Bugsnag
113
116
 
114
117
  # Deliver
115
118
  configuration.info("Notifying #{configuration.endpoint} of #{report.exceptions.last[:errorClass]}")
116
- payload_string = ::JSON.dump(Bugsnag::Helpers.trim_if_needed(report.as_json))
117
- Bugsnag::Delivery[configuration.delivery_method].deliver(configuration.endpoint, payload_string, configuration)
119
+ options = {:headers => report.headers, :trim_payload => true}
120
+ Bugsnag::Delivery[configuration.delivery_method].deliver(configuration.endpoint, report.as_json, configuration, options)
118
121
  end
119
122
  end
120
123
 
@@ -124,6 +127,11 @@ module Bugsnag
124
127
  @configuration || LOCK.synchronize { @configuration ||= Bugsnag::Configuration.new }
125
128
  end
126
129
 
130
+ def session_tracker
131
+ @session_tracker = nil unless defined?(@session_tracker)
132
+ @session_tracker || LOCK.synchronize { @session_tracker ||= Bugsnag::SessionTracker.new(configuration)}
133
+ end
134
+
127
135
  # Allow access to "before notify" callbacks
128
136
  def before_notify_callbacks
129
137
  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