From bc1912a4003170f1ec01053fd43a4a8dab864db6 Mon Sep 17 00:00:00 2001 From: Alex Moinet Date: Thu, 4 Jan 2018 16:56:45 +0000 Subject: [PATCH] Session tracking (#411) * Added session tracking basics to notifier * Added session config to config * Added session tracking mechanisms * Send bugsnag properties in headers * Added backoff to delivery * Minor updates and fixes * Added tests for session-tracker * Ensured success code is correctly checked before backoff * Added hook to rails * Ensured deliver doesn't send empty sessions * Replaced rails sessions with rack to service both * Complete re-do of backoff throttling * Changed success criteria on sessions * Added checks on asynchronous delivery * Updated tests * Added maximum amount of sessions to be sent at one time * Added final attempt to send sessions at exit * Style update * Changed to sessionCounts * Registered final at_exit block to send sessions * Updated tests * Fixed config ref * Ensured thread sessions accessed through accessor * Refactored some backoff functionality into functions * Fixed issue with accessing get/set session * Fixed issue with get_current_session calls * Added session-tracking to rails-51 example * Removed unnecessary info * Added fallback to send sessions every 5 minutes * Ensured deliver_fallback only terminated if exists * Ensured headers are backwards compatible * v6.3.0.beta.0 --- .../rails-51/config/initializers/bugsnag.rb | 3 +- lib/bugsnag.rb | 12 +- lib/bugsnag/configuration.rb | 9 +- lib/bugsnag/delivery/synchronous.rb | 113 ++++++++++++- lib/bugsnag/delivery/thread_queue.rb | 4 +- lib/bugsnag/helpers.rb | 24 +++ lib/bugsnag/integrations/rack.rb | 1 + .../integrations/rails/controller_methods.rb | 1 + lib/bugsnag/middleware/session_data.rb | 21 +++ lib/bugsnag/report.rb | 15 +- lib/bugsnag/session_tracker.rb | 157 ++++++++++++++++++ spec/configuration_spec.rb | 5 + spec/integrations/sidekiq_spec.rb | 2 +- spec/middleware_spec.rb | 20 +-- spec/rack_spec.rb | 4 +- spec/rails3_request_spec.rb | 4 +- spec/report_spec.rb | 122 +++++++------- spec/session_tracker_spec.rb | 153 +++++++++++++++++ spec/spec_helper.rb | 18 +- spec/stacktrace_spec.rb | 10 +- 20 files changed, 601 insertions(+), 97 deletions(-) create mode 100644 lib/bugsnag/middleware/session_data.rb create mode 100644 lib/bugsnag/session_tracker.rb create mode 100644 spec/session_tracker_spec.rb diff --git a/example/rails-51/config/initializers/bugsnag.rb b/example/rails-51/config/initializers/bugsnag.rb index 58c57fbf6..27b3a501b 100644 --- a/example/rails-51/config/initializers/bugsnag.rb +++ b/example/rails-51/config/initializers/bugsnag.rb @@ -1,3 +1,4 @@ Bugsnag.configure do |config| - config.api_key = "YOUR_API_KEY_HERE" + config.api_key = "YOUR_API_KEY" + config.track_sessions = true end diff --git a/lib/bugsnag.rb b/lib/bugsnag.rb index f3f57085b..eac7452b2 100644 --- a/lib/bugsnag.rb +++ b/lib/bugsnag.rb @@ -7,6 +7,7 @@ require "bugsnag/report" require "bugsnag/cleaner" require "bugsnag/helpers" +require "bugsnag/session_tracker" require "bugsnag/delivery" require "bugsnag/delivery/synchronous" @@ -38,6 +39,8 @@ def configure configuration.warn("No valid API key has been set, notifications will not be sent") @key_warning = true end + + session_tracker.config = configuration end # Explicitly notify of an exception @@ -113,8 +116,8 @@ def notify(exception, auto_notify=false, &block) # Deliver configuration.info("Notifying #{configuration.endpoint} of #{report.exceptions.last[:errorClass]}") - payload_string = ::JSON.dump(Bugsnag::Helpers.trim_if_needed(report.as_json)) - Bugsnag::Delivery[configuration.delivery_method].deliver(configuration.endpoint, payload_string, configuration) + options = {:headers => report.headers, :trim_payload => true} + Bugsnag::Delivery[configuration.delivery_method].deliver(configuration.endpoint, report.as_json, configuration, options) end end @@ -124,6 +127,11 @@ def configuration @configuration || LOCK.synchronize { @configuration ||= Bugsnag::Configuration.new } end + def session_tracker + @session_tracker = nil unless defined?(@session_tracker) + @session_tracker || LOCK.synchronize { @session_tracker ||= Bugsnag::SessionTracker.new(configuration)} + end + # Allow access to "before notify" callbacks def before_notify_callbacks Bugsnag.configuration.request_data[:before_callbacks] ||= [] diff --git a/lib/bugsnag/configuration.rb b/lib/bugsnag/configuration.rb index 1e02263af..2cb3fd580 100644 --- a/lib/bugsnag/configuration.rb +++ b/lib/bugsnag/configuration.rb @@ -7,6 +7,7 @@ require "bugsnag/middleware/ignore_error_class" require "bugsnag/middleware/suggestion_data" require "bugsnag/middleware/classify_error" +require "bugsnag/middleware/session_data" module Bugsnag class Configuration @@ -22,7 +23,7 @@ class Configuration attr_accessor :app_type attr_accessor :meta_data_filters attr_accessor :endpoint - attr_accessor :logger + attr_accessor :logger attr_accessor :middleware attr_accessor :internal_middleware attr_accessor :proxy_host @@ -32,10 +33,13 @@ class Configuration attr_accessor :timeout attr_accessor :hostname attr_accessor :ignore_classes + attr_accessor :track_sessions + attr_accessor :session_endpoint API_KEY_REGEX = /[0-9a-f]{32}/i THREAD_LOCAL_NAME = "bugsnag_req_data" DEFAULT_ENDPOINT = "https://notify.bugsnag.com" + DEFAULT_SESSION_ENDPOINT = "https://sessions.bugsnag.com" DEFAULT_META_DATA_FILTERS = [ /authorization/i, @@ -57,6 +61,8 @@ def initialize self.hostname = default_hostname self.timeout = 15 self.notify_release_stages = nil + self.track_sessions = false + self.session_endpoint = DEFAULT_SESSION_ENDPOINT # SystemExit and Interrupt are common Exception types seen with successful # exits and are not automatically reported to Bugsnag @@ -81,6 +87,7 @@ def initialize self.internal_middleware.use Bugsnag::Middleware::IgnoreErrorClass self.internal_middleware.use Bugsnag::Middleware::SuggestionData self.internal_middleware.use Bugsnag::Middleware::ClassifyError + self.internal_middleware.use Bugsnag::Middleware::SessionData self.middleware = Bugsnag::MiddlewareStack.new self.middleware.use Bugsnag::Middleware::Callbacks diff --git a/lib/bugsnag/delivery/synchronous.rb b/lib/bugsnag/delivery/synchronous.rb index 27fda965b..2e44ee68d 100644 --- a/lib/bugsnag/delivery/synchronous.rb +++ b/lib/bugsnag/delivery/synchronous.rb @@ -4,13 +4,19 @@ module Bugsnag module Delivery class Synchronous - HEADERS = {"Content-Type" => "application/json"} + BACKOFF_THREADS = {} + BACKOFF_REQUESTS = {} + BACKOFF_LOCK = Mutex.new class << self - def deliver(url, body, configuration) + def deliver(url, body, configuration, options={}) begin - response = request(url, body, configuration) + response = request(url, body, configuration, options) configuration.debug("Request to #{url} completed, status: #{response.code}") + success = options[:success] || '200' + if options[:backoff] && !(response.code == success) + backoff(url, body, configuration, options) + end rescue StandardError => e # KLUDGE: Since we don't re-raise http exceptions, this breaks rspec raise if e.class.to_s == "RSpec::Expectations::ExpectationNotMetError" @@ -22,9 +28,14 @@ def deliver(url, body, configuration) private - def request(url, body, configuration) + def request(url, body, configuration, options) uri = URI.parse(url) + if options[:trim_payload] + body = Bugsnag::Helpers.trim_if_needed(body) + end + payload = ::JSON.dump(body) + if configuration.proxy_host http = Net::HTTP.new(uri.host, uri.port, configuration.proxy_host, configuration.proxy_port, configuration.proxy_user, configuration.proxy_password) else @@ -39,14 +50,104 @@ def request(url, body, configuration) http.ca_file = configuration.ca_file if configuration.ca_file end - request = Net::HTTP::Post.new(path(uri), HEADERS) - request.body = body + headers = options.key?(:headers) ? options[:headers] : {} + headers.merge!(default_headers) + + request = Net::HTTP::Post.new(path(uri), headers) + request.body = payload + http.request(request) end + def backoff(url, body, configuration, options) + # Ensure we have the latest configuration for making these requests + @latest_configuration = configuration + + BACKOFF_LOCK.lock + begin + # Define an exit function once to handle outstanding requests + @registered_at_exit = false unless defined?(@registered_at_exit) + if !@registered_at_exit + @registered_at_exit = true + at_exit do + backoff_exit + end + end + if BACKOFF_REQUESTS[url] && !BACKOFF_REQUESTS[url].empty? + last_request = BACKOFF_REQUESTS[url].last + new_body_length = ::JSON.dump(body).length + old_body_length = ::JSON.dump(last_request[:body]).length + if new_body_length + old_body_length >= Bugsnag::Helpers::MAX_PAYLOAD_LENGTH + BACKOFF_REQUESTS[url].push({:body => body, :options => options}) + else + Bugsnag::Helpers::deep_merge!(last_request, {:body => body, :options => options}) + end + else + BACKOFF_REQUESTS[url] = [{:body => body, :options => options}] + end + if !(BACKOFF_THREADS[url] && BACKOFF_THREADS[url].status) + spawn_backoff_thread(url) + end + ensure + BACKOFF_LOCK.unlock + end + end + + def backoff_exit + # Kill existing threads + BACKOFF_THREADS.each do |url, thread| + thread.exit + end + # Retry outstanding requests once, then exit + BACKOFF_REQUESTS.each do |url, requests| + requests.map! do |req| + response = request(url, req[:body], @latest_configuration, req[:options]) + success = req[:options][:success] || '200' + response.code == success + end + requests.reject! { |i| i } + @latest_configuration.warn("Requests to #{url} finished, #{requests.size} failed") + end + end + + def spawn_backoff_thread(url) + new_thread = Thread.new(url) do |url| + interval = 2 + while BACKOFF_REQUESTS[url].size > 0 + sleep(interval) + interval = interval * 2 + interval = 600 if interval > 600 + BACKOFF_LOCK.lock + begin + BACKOFF_REQUESTS[url].map! do |req| + response = request(url, req[:body], @latest_configuration, req[:options]) + success = req[:options][:success] || '200' + if response.code == success + @latest_configuration.debug("Request to #{url} completed, status: #{response.code}") + false + else + req + end + end + BACKOFF_REQUESTS[url].reject! { |i| !i } + ensure + BACKOFF_LOCK.unlock + end + end + end + BACKOFF_THREADS[url] = new_thread + end + def path(uri) uri.path == "" ? "/" : uri.path end + + def default_headers + { + "Content-Type" => "application/json", + "Bugsnag-Sent-At" => Time.now().utc().strftime('%Y-%m-%dT%H:%M:%S') + } + end end end end diff --git a/lib/bugsnag/delivery/thread_queue.rb b/lib/bugsnag/delivery/thread_queue.rb index 52b65f697..93e0f755e 100644 --- a/lib/bugsnag/delivery/thread_queue.rb +++ b/lib/bugsnag/delivery/thread_queue.rb @@ -8,7 +8,7 @@ class ThreadQueue < Synchronous MUTEX = Mutex.new class << self - def deliver(url, body, configuration) + def deliver(url, body, configuration, options={}) @configuration = configuration start_once! @@ -19,7 +19,7 @@ def deliver(url, body, configuration) end # Add delivery to the worker thread - @queue.push proc { super(url, body, configuration) } + @queue.push proc { super(url, body, configuration, options) } end private diff --git a/lib/bugsnag/helpers.rb b/lib/bugsnag/helpers.rb index 5c12f8df2..c4ee56fdf 100644 --- a/lib/bugsnag/helpers.rb +++ b/lib/bugsnag/helpers.rb @@ -23,6 +23,30 @@ def self.trim_if_needed(value) remove_metadata_from_events(reduced_value) end + def self.deep_merge(l_hash, r_hash) + l_hash.merge(r_hash) do |key, l_val, r_val| + if l_val.is_a?(Hash) && r_val.is_a?(Hash) + deep_merge(l_val, r_val) + elsif l_val.is_a?(Array) && r_val.is_a?(Array) + l_val.concat(r_val) + else + r_val + end + end + end + + def self.deep_merge!(l_hash, r_hash) + l_hash.merge!(r_hash) do |key, l_val, r_val| + if l_val.is_a?(Hash) && r_val.is_a?(Hash) + deep_merge(l_val, r_val) + elsif l_val.is_a?(Array) && r_val.is_a?(Array) + l_val.concat(r_val) + else + r_val + end + end + end + private TRUNCATION_INFO = '[TRUNCATED]' diff --git a/lib/bugsnag/integrations/rack.rb b/lib/bugsnag/integrations/rack.rb index a6e520eb2..93a08fe45 100644 --- a/lib/bugsnag/integrations/rack.rb +++ b/lib/bugsnag/integrations/rack.rb @@ -34,6 +34,7 @@ def initialize(app) def call(env) # Set the request data for bugsnag middleware to use Bugsnag.configuration.set_request_data(:rack_env, env) + Bugsnag.session_tracker.create_session begin response = @app.call(env) diff --git a/lib/bugsnag/integrations/rails/controller_methods.rb b/lib/bugsnag/integrations/rails/controller_methods.rb index 76739daa0..fbc522d93 100644 --- a/lib/bugsnag/integrations/rails/controller_methods.rb +++ b/lib/bugsnag/integrations/rails/controller_methods.rb @@ -5,6 +5,7 @@ def self.included(base) end module ClassMethods + private def before_bugsnag_notify(*methods, &block) _add_bugsnag_notify_callback(:before_callbacks, *methods, &block) diff --git a/lib/bugsnag/middleware/session_data.rb b/lib/bugsnag/middleware/session_data.rb new file mode 100644 index 000000000..602f439b2 --- /dev/null +++ b/lib/bugsnag/middleware/session_data.rb @@ -0,0 +1,21 @@ +module Bugsnag::Middleware + class SessionData + def initialize(bugsnag) + @bugsnag = bugsnag + end + + def call(report) + session = Bugsnag::SessionTracker.get_current_session + unless session.nil? + if report.unhandled + session[:events][:unhandled] += 1 + else + session[:events][:handled] += 1 + end + report.session = session + end + + @bugsnag.call(report) + end + end +end diff --git a/lib/bugsnag/report.rb b/lib/bugsnag/report.rb index 3ed2d78e0..650849c94 100644 --- a/lib/bugsnag/report.rb +++ b/lib/bugsnag/report.rb @@ -17,8 +17,9 @@ class Report MAX_EXCEPTIONS_TO_UNWRAP = 5 - CURRENT_PAYLOAD_VERSION = "2" + CURRENT_PAYLOAD_VERSION = "4.0" + attr_reader :unhandled attr_accessor :api_key attr_accessor :app_type attr_accessor :app_version @@ -31,6 +32,7 @@ class Report attr_accessor :meta_data attr_accessor :raw_exceptions attr_accessor :release_stage + attr_accessor :session attr_accessor :severity attr_accessor :severity_reason attr_accessor :user @@ -92,7 +94,7 @@ def as_json }, exceptions: exceptions, groupingHash: grouping_hash, - payloadVersion: CURRENT_PAYLOAD_VERSION, + session: session, severity: severity, severityReason: severity_reason, unhandled: @unhandled, @@ -108,7 +110,6 @@ def as_json # return the payload hash { - :apiKey => api_key, :notifier => { :name => NOTIFIER_NAME, :version => NOTIFIER_VERSION, @@ -118,6 +119,14 @@ def as_json } end + def headers + { + "Bugsnag-Api-Key" => api_key, + "Bugsnag-Payload-Version" => CURRENT_PAYLOAD_VERSION, + "Bugsnag-Sent-At" => Time.now().utc().strftime('%Y-%m-%dT%H:%M:%S') + } + end + def ignore? @should_ignore end diff --git a/lib/bugsnag/session_tracker.rb b/lib/bugsnag/session_tracker.rb new file mode 100644 index 000000000..7f3dbf3d7 --- /dev/null +++ b/lib/bugsnag/session_tracker.rb @@ -0,0 +1,157 @@ +require 'thread' +require 'time' +require 'securerandom' + +module Bugsnag + class SessionTracker + + THREAD_SESSION = "bugsnag_session" + TIME_THRESHOLD = 60 + FALLBACK_TIME = 300 + MAXIMUM_SESSION_COUNT = 50 + SESSION_PAYLOAD_VERSION = "1.0" + + attr_reader :session_counts + attr_writer :config + + def self.set_current_session(session) + Thread.current[THREAD_SESSION] = session + end + + def self.get_current_session + Thread.current[THREAD_SESSION] + end + + def initialize(configuration) + @session_counts = {} + @config = configuration + @mutex = Mutex.new + @last_sent = Time.now + end + + def create_session + return unless @config.track_sessions + start_time = Time.now().utc().strftime('%Y-%m-%dT%H:%M:00') + new_session = { + :id => SecureRandom.uuid, + :startedAt => start_time, + :events => { + :handled => 0, + :unhandled => 0 + } + } + SessionTracker.set_current_session(new_session) + add_thread = Thread.new { add_session(start_time) } + add_thread.join() + end + + def send_sessions + @mutex.lock + begin + deliver_sessions + ensure + @mutex.unlock + end + end + + private + def add_session(min) + @mutex.lock + begin + @registered_at_exit = false unless defined?(@registered_at_exit) + if !@registered_at_exit + @registered_at_exit = true + at_exit do + if !@deliver_fallback.nil? && @deliver_fallback.status == 'sleep' + @deliver_fallback.terminate + end + deliver_sessions + end + end + @session_counts[min] ||= 0 + @session_counts[min] += 1 + if Time.now() - @last_sent > TIME_THRESHOLD + deliver_sessions + end + ensure + @mutex.unlock + end + end + + def deliver_sessions + return unless @config.track_sessions + sessions = [] + @session_counts.each do |min, count| + sessions << { + :startedAt => min, + :sessionsStarted => count + } + if sessions.size >= MAXIMUM_SESSION_COUNT + deliver(sessions) + sessions = [] + end + end + @session_counts = {} + reset_delivery_thread + deliver(sessions) + end + + def reset_delivery_thread + if !@deliver_fallback.nil? && @deliver_fallback.status == 'sleep' + @deliver_fallback.terminate + end + @deliver_fallback = Thread.new do + sleep(FALLBACK_TIME) + deliver_sessions + end + end + + def deliver(sessionCounts) + if sessionCounts.length == 0 + @config.debug("No sessions to deliver") + return + end + + if !@config.valid_api_key? + @config.debug("Not delivering sessions due to an invalid api_key") + return + end + + if !@config.should_notify_release_stage? + @config.debug("Not delivering sessions due to notify_release_stages :#{@config.notify_release_stages.inspect}") + return + end + + if @config.delivery_method != :thread_queue + @config.debug("Not delivering sessions due to asynchronous delivery being disabled") + return + end + + payload = { + :notifier => { + :name => Bugsnag::Report::NOTIFIER_NAME, + :url => Bugsnag::Report::NOTIFIER_URL, + :version => Bugsnag::Report::NOTIFIER_VERSION + }, + :device => { + :hostname => @config.hostname + }, + :app => { + :version => @config.app_version, + :releaseStage => @config.release_stage, + :type => @config.app_type + }, + :sessionCounts => sessionCounts + } + + headers = { + "Bugsnag-Api-Key" => @config.api_key, + "Bugsnag-Payload-Version" => SESSION_PAYLOAD_VERSION + } + + options = {:headers => headers, :backoff => true, :success => '202'} + @last_sent = Time.now + Bugsnag::Delivery[@config.delivery_method].deliver(@config.session_endpoint, payload, @config, options) + end + end +end \ No newline at end of file diff --git a/spec/configuration_spec.rb b/spec/configuration_spec.rb index f5c755891..5cffe6714 100644 --- a/spec/configuration_spec.rb +++ b/spec/configuration_spec.rb @@ -22,6 +22,11 @@ subject.delivery_method = :wow expect(subject.delivery_method).to eq(:wow) end + + it "should have sensible defaults for session tracking" do + expect(subject.session_endpoint).to eq("https://sessions.bugsnag.com") + expect(subject.track_sessions).to be false + end end it "should have exit exception classes ignored by default" do diff --git a/spec/integrations/sidekiq_spec.rb b/spec/integrations/sidekiq_spec.rb index f4993dd9a..3939d44bd 100644 --- a/spec/integrations/sidekiq_spec.rb +++ b/spec/integrations/sidekiq_spec.rb @@ -23,7 +23,7 @@ def perform(value) rescue end - expect(Bugsnag).to have_sent_notification {|payload| + expect(Bugsnag).to have_sent_notification {|payload, headers| event = get_event_from_payload(payload) expect(event["metaData"]["sidekiq"]["msg"]["class"]).to eq("FailingWorker") expect(event["metaData"]["sidekiq"]["msg"]["args"]).to eq([-0]) diff --git a/spec/middleware_spec.rb b/spec/middleware_spec.rb index e8b3d46fe..b6d67612a 100644 --- a/spec/middleware_spec.rb +++ b/spec/middleware_spec.rb @@ -16,7 +16,7 @@ Bugsnag.notify(BugsnagTestException.new("It crashed")) expect(callback_run_count).to eq(1) - expect(Bugsnag).to have_sent_notification{ |payload| + expect(Bugsnag).to have_sent_notification{ |payload, headers| event = get_event_from_payload(payload) expect(event["metaData"]["some_tab"]).not_to be_nil expect(event["metaData"]["some_tab"]["info"]).to eq("here") @@ -37,7 +37,7 @@ Bugsnag.notify(BugsnagTestException.new("It crashed")) expect(callback_run_count).to eq(1) - expect(Bugsnag).to have_sent_notification{ |payload| + expect(Bugsnag).to have_sent_notification{ |payload, headers| event = get_event_from_payload(payload) expect(event["metaData"]["custom"]).not_to be_nil expect(event["metaData"]["custom"]["info"]).to eq("here") @@ -56,7 +56,7 @@ Bugsnag.notify(BugsnagTestException.new("It crashed")) expect(callback_run_count).to eq(1) - expect(Bugsnag).to have_sent_notification{ |payload| + expect(Bugsnag).to have_sent_notification{ |payload, headers| event = get_event_from_payload(payload) expect(event["user"]).not_to be_nil expect(event["user"]["id"]).to eq("here") @@ -73,7 +73,7 @@ report.meta_data.merge!({custom: {info: 'overridden'}}) end - expect(Bugsnag).to have_sent_notification{ |payload| + expect(Bugsnag).to have_sent_notification{ |payload, headers| event = get_event_from_payload(payload) expect(event["metaData"]["custom"]).not_to be_nil expect(event["metaData"]["custom"]["info"]).to eq("overridden") @@ -87,7 +87,7 @@ report.meta_data.merge!({custom: {info: 'overridden'}}) end - expect(Bugsnag).to have_sent_notification{ |payload| + expect(Bugsnag).to have_sent_notification{ |payload, headers| event = get_event_from_payload(payload) expect(event["metaData"]["custom"]).not_to be_nil expect(event["metaData"]["custom"]["info"]).to eq("overridden") @@ -97,7 +97,7 @@ it "does not have have before callbacks by default" do expect(Bugsnag.before_notify_callbacks.size).to eq(0) Bugsnag.notify(BugsnagTestException.new("It crashed")) - expect(Bugsnag).to have_sent_notification{ |payload| + expect(Bugsnag).to have_sent_notification{ |payload, headers| event = get_event_from_payload(payload) expect(event["metaData"].size).to eq(0) } @@ -172,7 +172,7 @@ def call(report) Bugsnag.notify(BugsnagTestException.new("It crashed")) - expect(Bugsnag).to have_sent_notification{ |payload| + expect(Bugsnag).to have_sent_notification{ |payload, headers| event = get_event_from_payload(payload) expect(event["metaData"]['test']['value']).to eq("abcdef*****3456") } @@ -187,7 +187,7 @@ def call(report) Bugsnag.notify(e) end - expect(Bugsnag).to have_sent_notification{ |payload| + expect(Bugsnag).to have_sent_notification{ |payload, headers| event = get_event_from_payload(payload) expect(event["metaData"]["error"]).to_not be_nil expect(event["metaData"]["error"]).to eq({"suggestion" => "prepend"}) @@ -205,7 +205,7 @@ def call(report) Bugsnag.notify(e) end - expect(Bugsnag).to have_sent_notification{ |payload| + expect(Bugsnag).to have_sent_notification{ |payload, headers| event = get_event_from_payload(payload) expect(event["metaData"]["error"]).to be_nil } @@ -240,7 +240,7 @@ def call(report) } end - expect(Bugsnag).to have_sent_notification{ |payload| + expect(Bugsnag).to have_sent_notification{ |payload, headers| event = get_event_from_payload(payload) expect(event["unhandled"]).to be true expect(event["severityReason"]).to eq({ diff --git a/spec/rack_spec.rb b/spec/rack_spec.rb index 3f72e399e..caa75ee42 100644 --- a/spec/rack_spec.rb +++ b/spec/rack_spec.rb @@ -25,7 +25,7 @@ it "delivers an exception if auto_notify is enabled" do rack_stack.call(rack_env) rescue nil - expect(Bugsnag).to have_sent_notification{ |payload| + expect(Bugsnag).to have_sent_notification{ |payload, headers| exception_class = payload["events"].first["exceptions"].first["errorClass"] expect(exception_class).to eq(exception.class.to_s) } @@ -35,7 +35,7 @@ it "applies the correct severity reason" do rack_stack.call(rack_env) rescue nil - expect(Bugsnag).to have_sent_notification{ |payload| + expect(Bugsnag).to have_sent_notification{ |payload, headers| event = get_event_from_payload(payload) expect(event["unhandled"]).to be true expect(event["severityReason"]).to eq({ diff --git a/spec/rails3_request_spec.rb b/spec/rails3_request_spec.rb index 722dcf669..ce6bd03ca 100644 --- a/spec/rails3_request_spec.rb +++ b/spec/rails3_request_spec.rb @@ -13,7 +13,7 @@ }) Bugsnag.notify(BugsnagTestException.new('Grimbles')) - expect(Bugsnag).to have_sent_notification { |payload| + expect(Bugsnag).to have_sent_notification { |payload, headers| event = get_event_from_payload(payload) puts event["metaData"].inspect expect(event["metaData"]["request"]).to eq({ @@ -38,7 +38,7 @@ def to_s Bugsnag.notify(BugsnagTestException.new('Grimbles')) - expect(Bugsnag).to have_sent_notification { |payload| + expect(Bugsnag).to have_sent_notification { |payload, headers| event = get_event_from_payload(payload) puts event["metaData"].inspect expect(event["metaData"]["request"]).to eq({ diff --git a/spec/report_spec.rb b/spec/report_spec.rb index 71bae5465..d82b0e765 100644 --- a/spec/report_spec.rb +++ b/spec/report_spec.rb @@ -31,8 +31,8 @@ def gloops it "should contain an api_key if one is set" do Bugsnag.notify(BugsnagTestException.new("It crashed")) - expect(Bugsnag).to have_sent_notification{ |payload| - expect(payload["apiKey"]).to eq("c9d60ae4c7e70c4b6c4ebd3e8056d2b8") + expect(Bugsnag).to have_sent_notification{ |payload, headers| + expect(headers["Bugsnag-Api-Key"]).to eq("c9d60ae4c7e70c4b6c4ebd3e8056d2b8") } end @@ -57,8 +57,8 @@ def gloops report.api_key = "9d84383f9be2ca94902e45c756a9979d" end - expect(Bugsnag).to have_sent_notification{ |payload| - expect(payload["apiKey"]).to eq("9d84383f9be2ca94902e45c756a9979d") + expect(Bugsnag).to have_sent_notification{ |payload, headers| + expect(headers["Bugsnag-Api-Key"]).to eq("9d84383f9be2ca94902e45c756a9979d") } end @@ -68,7 +68,7 @@ def gloops report.grouping_hash = "this is my grouping hash" end - expect(Bugsnag).to have_sent_notification{ |payload| + expect(Bugsnag).to have_sent_notification{ |payload, headers| event = get_event_from_payload(payload) expect(event["groupingHash"]).to eq("this is my grouping hash") } @@ -85,15 +85,15 @@ def gloops Bugsnag.notify(BugsnagTestException.new("It crashed")) - expect(Bugsnag).to have_sent_notification{ |payload| - expect(payload["apiKey"]).to eq("c9d60ae4c7e70c4b6c4ebd3e8056d2b9") + expect(Bugsnag).to have_sent_notification{ |payload, headers| + expect(headers["Bugsnag-Api-Key"]).to eq("c9d60ae4c7e70c4b6c4ebd3e8056d2b9") } end it "has the right exception class" do Bugsnag.notify(BugsnagTestException.new("It crashed")) - expect(Bugsnag).to have_sent_notification{ |payload| + expect(Bugsnag).to have_sent_notification{ |payload, headers| exception = get_exception_from_payload(payload) expect(exception["errorClass"]).to eq("BugsnagTestException") } @@ -102,7 +102,7 @@ def gloops it "has the right exception message" do Bugsnag.notify(BugsnagTestException.new("It crashed")) - expect(Bugsnag).to have_sent_notification{ |payload| + expect(Bugsnag).to have_sent_notification{ |payload, headers| exception = get_exception_from_payload(payload) expect(exception["message"]).to eq("It crashed") } @@ -111,7 +111,7 @@ def gloops it "has a valid stacktrace" do Bugsnag.notify(BugsnagTestException.new("It crashed")) - expect(Bugsnag).to have_sent_notification{ |payload| + expect(Bugsnag).to have_sent_notification{ |payload, headers| exception = get_exception_from_payload(payload) expect(exception["stacktrace"].length).to be > 0 } @@ -120,7 +120,7 @@ def gloops it "uses correct unhandled defaults" do Bugsnag.notify(BugsnagTestException.new("It crashed")) - expect(Bugsnag).to have_sent_notification{ |payload| + expect(Bugsnag).to have_sent_notification{ |payload, headers| event = get_event_from_payload(payload) expect(event["unhandled"]).to be false expect(event["severity"]).to eq("warning") @@ -134,7 +134,7 @@ def gloops Bugsnag.notify(BugsnagTestException.new("It crashed")) do |notification| notification.severity = "info" end - expect(Bugsnag).to have_sent_notification{ |payload| + expect(Bugsnag).to have_sent_notification{ |payload, headers| event = get_event_from_payload(payload) expect(event["unhandled"]).to be false expect(event["severity"]).to eq("info") @@ -146,7 +146,7 @@ def gloops it "sets correct severity and reason for specific error classes" do Bugsnag.notify(SignalException.new("TERM")) - expect(Bugsnag).to have_sent_notification{ |payload| + expect(Bugsnag).to have_sent_notification{ |payload, headers| event = get_event_from_payload(payload) expect(event["unhandled"]).to be false expect(event["severity"]).to eq("info") @@ -171,7 +171,7 @@ def gloops }) end - expect(Bugsnag).to have_sent_notification{ |payload| + expect(Bugsnag).to have_sent_notification{ |payload, headers| event = get_event_from_payload(payload) expect(event["metaData"]["some_tab"]).to eq( "info" => "here", @@ -191,7 +191,7 @@ def gloops Bugsnag.notify(exception) - expect(Bugsnag).to have_sent_notification{ |payload| + expect(Bugsnag).to have_sent_notification{ |payload, headers| event = get_event_from_payload(payload) expect(event["metaData"]["some_tab"]).to eq( "info" => "here", @@ -213,7 +213,7 @@ def gloops report.remove_tab(:some_tab) end - expect(Bugsnag).to have_sent_notification{ |payload| + expect(Bugsnag).to have_sent_notification{ |payload, headers| event = get_event_from_payload(payload) expect(event["metaData"]["some_tab"]).to be_nil } @@ -232,7 +232,7 @@ def gloops report.remove_tab(nil) end - expect(Bugsnag).to have_sent_notification{ |payload| + expect(Bugsnag).to have_sent_notification{ |payload, headers| event = get_event_from_payload(payload) expect(event["metaData"]["some_tab"]).to eq( "info" => "here", @@ -248,7 +248,7 @@ def gloops report.add_tab(:some_tab, "added") end - expect(Bugsnag).to have_sent_notification{ |payload| + expect(Bugsnag).to have_sent_notification{ |payload, headers| event = get_event_from_payload(payload) expect(event["metaData"]["custom"]).to eq( "some_tab" => "added", @@ -269,7 +269,7 @@ def gloops report.add_tab(:some_tab, {:info => "overridden"}) end - expect(Bugsnag).to have_sent_notification{ |payload| + expect(Bugsnag).to have_sent_notification{ |payload, headers| event = get_event_from_payload(payload) expect(event["metaData"]["some_tab"]).to eq( "info" => "overridden", @@ -284,7 +284,7 @@ def gloops Bugsnag.notify(exception) - expect(Bugsnag).to have_sent_notification{ |payload| + expect(Bugsnag).to have_sent_notification{ |payload, headers| event = get_event_from_payload(payload) expect(event["user"]["id"]).to eq("exception_user_id") } @@ -298,7 +298,7 @@ def gloops report.user.merge!({:id => "override_user_id"}) end - expect(Bugsnag).to have_sent_notification{ |payload| + expect(Bugsnag).to have_sent_notification{ |payload, headers| event = get_event_from_payload(payload) expect(event["user"]["id"]).to eq("override_user_id") } @@ -310,7 +310,7 @@ def gloops Bugsnag.notify(exception) - expect(Bugsnag).to have_sent_notification{ |payload| + expect(Bugsnag).to have_sent_notification{ |payload, headers| event = get_event_from_payload(payload) expect(event["context"]).to eq("exception_context") } @@ -322,7 +322,7 @@ def gloops Bugsnag.notify(exception) - expect(Bugsnag).to have_sent_notification{ |payload| + expect(Bugsnag).to have_sent_notification{ |payload, headers| event = get_event_from_payload(payload) expect(event["groupingHash"]).to eq("exception_hash") } @@ -337,7 +337,7 @@ def gloops report.context = "override_context" end - expect(Bugsnag).to have_sent_notification{ |payload| + expect(Bugsnag).to have_sent_notification{ |payload, headers| event = get_event_from_payload(payload) expect(event["context"]).to eq("override_context") } @@ -353,7 +353,7 @@ def gloops }) end - expect(Bugsnag).to have_sent_notification{ |payload| + expect(Bugsnag).to have_sent_notification{ |payload, headers| event = get_event_from_payload(payload) expect(event["metaData"]["some_tab"]).to eq( "info" => "here", @@ -372,7 +372,7 @@ def gloops }) end - expect(Bugsnag).to have_sent_notification{ |payload| + expect(Bugsnag).to have_sent_notification{ |payload, headers| # Truncated body should be no bigger than # 2 truncated hashes (4096*2) + rest of payload (20000) expect(::JSON.dump(payload).length).to be < 4096*2 + 20000 @@ -389,7 +389,7 @@ def gloops }) end - expect(Bugsnag).to have_sent_notification{ |payload| + expect(Bugsnag).to have_sent_notification{ |payload, headers| # Truncated body should be no bigger than # 2 truncated hashes (4096*2) + rest of payload (20000) expect(::JSON.dump(payload).length).to be < 4096*2 + 20000 @@ -403,7 +403,7 @@ def gloops ex.set_backtrace(stacktrace) Bugsnag.notify(ex) - expect(Bugsnag).to have_sent_notification{ |payload| + expect(Bugsnag).to have_sent_notification{ |payload, headers| # Truncated body should be no bigger than # 400 stacktrace lines * approx 60 chars per line + rest of payload (20000) expect(::JSON.dump(payload).length).to be < 400*60 + 20000 @@ -415,7 +415,7 @@ def gloops report.severity = "info" end - expect(Bugsnag).to have_sent_notification{ |payload| + expect(Bugsnag).to have_sent_notification{ |payload, headers| event = get_event_from_payload(payload) expect(event["severity"]).to eq("info") } @@ -425,7 +425,7 @@ def gloops it "defaults to warning severity" do Bugsnag.notify(BugsnagTestException.new("It crashed")) - expect(Bugsnag).to have_sent_notification{ |payload| + expect(Bugsnag).to have_sent_notification{ |payload, headers| event = get_event_from_payload(payload) expect(event["severity"]).to eq("warning") } @@ -436,7 +436,7 @@ def gloops report.context = 'test_context' end - expect(Bugsnag).to have_sent_notification{ |payload| + expect(Bugsnag).to have_sent_notification{ |payload, headers| event = get_event_from_payload(payload) expect(event["context"]).to eq("test_context") } @@ -447,7 +447,7 @@ def gloops report.user = {id: 'test_user'} end - expect(Bugsnag).to have_sent_notification{ |payload| + expect(Bugsnag).to have_sent_notification{ |payload, headers| event = get_event_from_payload(payload) expect(event["user"]["id"]).to eq("test_user") } @@ -470,7 +470,7 @@ def gloops Bugsnag.notify(BugsnagTestException.new("It crashed")) - expect(Bugsnag).to have_sent_notification{ |payload| + expect(Bugsnag).to have_sent_notification{ |payload, headers| event = get_event_from_payload(payload) expect(event["app"]["releaseStage"]).to eq("production") } @@ -490,7 +490,7 @@ def gloops Bugsnag.configuration.notify_release_stages = ["development"] Bugsnag.notify(BugsnagTestException.new("It crashed")) - expect(Bugsnag).to have_sent_notification{ |payload| + expect(Bugsnag).to have_sent_notification{ |payload, headers| event = get_event_from_payload(payload) expect(event["exceptions"].length).to eq(1) } @@ -506,7 +506,7 @@ def gloops Bugsnag.configuration.project_root = "/Random/location/here" Bugsnag.notify(BugsnagTestException.new("It crashed")) - expect(Bugsnag).to have_sent_notification{ |payload| + expect(Bugsnag).to have_sent_notification{ |payload, headers| exception = get_exception_from_payload(payload) expect(exception["stacktrace"].size).to be >= 1 expect(exception["stacktrace"].first["inProject"]).to be_nil @@ -517,7 +517,7 @@ def gloops Bugsnag.configuration.project_root = File.expand_path File.dirname(__FILE__) Bugsnag.notify(BugsnagTestException.new("It crashed")) - expect(Bugsnag).to have_sent_notification{ |payload| + expect(Bugsnag).to have_sent_notification{ |payload, headers| exception = get_exception_from_payload(payload) expect(exception["stacktrace"].size).to be >= 1 expect(exception["stacktrace"].first["inProject"]).to eq(true) @@ -539,7 +539,7 @@ def gloops "(pry):3:in `__pry__'" ]} Bugsnag.notify(ex) - expect(Bugsnag).to have_sent_notification{ |payload| + expect(Bugsnag).to have_sent_notification{ |payload, headers| exception = get_exception_from_payload(payload) expect(exception["stacktrace"][0]["inProject"]).to be_nil @@ -556,7 +556,7 @@ def gloops Bugsnag.configuration.app_version = "1.1.1" Bugsnag.notify(BugsnagTestException.new("It crashed")) - expect(Bugsnag).to have_sent_notification{ |payload| + expect(Bugsnag).to have_sent_notification{ |payload, headers| event = get_event_from_payload(payload) expect(event["app"]["version"]).to eq("1.1.1") } @@ -568,7 +568,7 @@ def gloops report.meta_data.merge!({:request => {:params => {:password => "1234", :other_password => "12345", :other_data => "123456"}}}) end - expect(Bugsnag).to have_sent_notification{ |payload| + expect(Bugsnag).to have_sent_notification{ |payload, headers| event = get_event_from_payload(payload) expect(event["metaData"]).not_to be_nil expect(event["metaData"]["request"]).not_to be_nil @@ -586,7 +586,7 @@ def gloops report.meta_data.merge!({:request => {:params => {:password => "1234", :other_password => "123456", :other_data => "123456"}}}) end - expect(Bugsnag).to have_sent_notification{ |payload| + expect(Bugsnag).to have_sent_notification{ |payload, headers| event = get_event_from_payload(payload) expect(event["metaData"]).not_to be_nil expect(event["metaData"]["request"]).not_to be_nil @@ -604,7 +604,7 @@ def gloops report.meta_data.merge!({:request => {:params => {:password => "1234", :other_password => "123456", :other_data => "123456"}}}) end - expect(Bugsnag).to have_sent_notification{ |payload| + expect(Bugsnag).to have_sent_notification{ |payload, headers| event = get_event_from_payload(payload) expect(event["metaData"]).not_to be_nil expect(event["metaData"]["request"]).not_to be_nil @@ -622,7 +622,7 @@ def gloops report.meta_data.merge!({:request => {:params => {:password => "1234", :other_password => "123456", :other_data => "123456"}}}) end - expect(Bugsnag).to have_sent_notification{ |payload| + expect(Bugsnag).to have_sent_notification{ |payload, headers| event = get_event_from_payload(payload) expect(event["metaData"]).not_to be_nil expect(event["metaData"]["request"]).not_to be_nil @@ -638,7 +638,7 @@ def gloops report.meta_data.merge!({:request => {:params => {:nil_param => nil}}}) end - expect(Bugsnag).to have_sent_notification{ |payload| + expect(Bugsnag).to have_sent_notification{ |payload, headers| event = get_event_from_payload(payload) expect(event["metaData"]).not_to be_nil expect(event["metaData"]["request"]).not_to be_nil @@ -700,7 +700,7 @@ def gloops Bugsnag.notify $! end - expect(Bugsnag).to have_sent_notification{ |payload| + expect(Bugsnag).to have_sent_notification{ |payload, headers| event = get_event_from_payload(payload) expect(event["exceptions"].size).to eq(2) } @@ -712,7 +712,7 @@ def gloops Bugsnag.notify(ex) - expect(Bugsnag).to have_sent_notification{ |payload| + expect(Bugsnag).to have_sent_notification{ |payload, headers| event = get_event_from_payload(payload) expect(event["exceptions"].size).to eq(1) } @@ -726,7 +726,7 @@ def gloops end Bugsnag.notify(first_ex) - expect(Bugsnag).to have_sent_notification{ |payload| + expect(Bugsnag).to have_sent_notification{ |payload, headers| event = get_event_from_payload(payload) expect(event["exceptions"].size).to eq(5) } @@ -735,7 +735,7 @@ def gloops it "calls to_exception on i18n error objects" do Bugsnag.notify(OpenStruct.new(:to_exception => BugsnagTestException.new("message"))) - expect(Bugsnag).to have_sent_notification{ |payload| + expect(Bugsnag).to have_sent_notification{ |payload, headers| exception = get_exception_from_payload(payload) expect(exception["errorClass"]).to eq("BugsnagTestException") expect(exception["message"]).to eq("message") @@ -745,7 +745,7 @@ def gloops it "generates runtimeerror for non exceptions" do notify_test_exception - expect(Bugsnag).to have_sent_notification{ |payload| + expect(Bugsnag).to have_sent_notification{ |payload, headers| exception = get_exception_from_payload(payload) expect(exception["errorClass"]).to eq("RuntimeError") expect(exception["message"]).to eq("test message") @@ -761,7 +761,7 @@ def gloops Bugsnag.notify(ex) - expect(Bugsnag).to have_sent_notification{ |payload| + expect(Bugsnag).to have_sent_notification{ |payload, headers| exception = get_exception_from_payload(payload) expect(exception["stacktrace"].length).to eq(2) @@ -786,7 +786,7 @@ def gloops Bugsnag.notify(ex) - expect(Bugsnag).to have_sent_notification{ |payload| + expect(Bugsnag).to have_sent_notification{ |payload, headers| exception = get_exception_from_payload(payload) expect(exception["stacktrace"].length).to eq(2) @@ -810,7 +810,7 @@ def gloops report.meta_data.merge!({fluff: {fluff: invalid_data}}) end - expect(Bugsnag).to have_sent_notification{ |payload| + expect(Bugsnag).to have_sent_notification{ |payload, headers| event = get_event_from_payload(payload) if defined?(Encoding::UTF_8) expect(event['metaData']['fluff']['fluff']).to match(/fl�ff/) @@ -831,7 +831,7 @@ def gloops Bugsnag.notify $! end - expect(Bugsnag).to have_sent_notification { |payload| + expect(Bugsnag).to have_sent_notification { |payload, headers| if defined?(Encoding::UTF_8) expect(payload.to_json).to match(/foo�bar/) else @@ -853,7 +853,7 @@ def gloops end end - expect(Bugsnag).to have_sent_notification { |payload| + expect(Bugsnag).to have_sent_notification { |payload, headers| if defined?(Encoding::UTF_8) expect(payload.to_json).to match(/foo�bar/) else @@ -876,7 +876,7 @@ def gloops Bugsnag.notify $! end - expect(Bugsnag).to have_sent_notification { |payload| + expect(Bugsnag).to have_sent_notification { |payload, headers| if defined?(Encoding::UTF_8) expect(payload.to_json).to match(/foo�bar/) else @@ -899,7 +899,7 @@ def gloops Bugsnag.notify $! end - expect(Bugsnag).to have_sent_notification { |payload| + expect(Bugsnag).to have_sent_notification { |payload, headers| if defined?(Encoding::UTF_8) expect(payload.to_json).to match(/foo�bar/) else @@ -925,7 +925,7 @@ def gloops Bugsnag.notify $! end - expect(Bugsnag).to have_sent_notification { |payload| + expect(Bugsnag).to have_sent_notification { |payload, headers| if defined?(Encoding::UTF_8) expect(payload.to_json).to match(/foo�bar/) else @@ -943,7 +943,7 @@ def gloops Bugsnag.notify $! end - expect(Bugsnag).to have_sent_notification { |payload| + expect(Bugsnag).to have_sent_notification { |payload, headers| exception = get_exception_from_payload(payload) expect(exception['stacktrace'].size).to be > 0 } @@ -952,7 +952,7 @@ def gloops it 'should use defaults when notify is called' do Bugsnag.notify(BugsnagTestException.new("It crashed")) - expect(Bugsnag).to have_sent_notification{ |payload| + expect(Bugsnag).to have_sent_notification{ |payload, headers| event = payload["events"][0] expect(event["unhandled"]).to be false expect(event["severityReason"]).to eq({"type" => "handledException"}) @@ -969,7 +969,7 @@ def gloops } end - expect(Bugsnag).to have_sent_notification{ |payload| + expect(Bugsnag).to have_sent_notification{ |payload, headers| event = payload["events"][0] expect(event["severityReason"]).to eq( { @@ -993,7 +993,7 @@ def gloops } end - expect(Bugsnag).to have_sent_notification{ |payload| + expect(Bugsnag).to have_sent_notification{ |payload, headers| event = payload["events"][0] expect(event["unhandled"]).to be false expect(event["severityReason"]).to eq({"type" => "handledException"}) @@ -1017,7 +1017,7 @@ def gloops Bugsnag.notify $! end - expect(Bugsnag).to have_sent_notification{ |payload| + expect(Bugsnag).to have_sent_notification{ |payload, headers| exception = get_exception_from_payload(payload) expect(exception["errorClass"]).to eq('Java::JavaLang::NullPointerException') expect(exception["message"]).to eq("") diff --git a/spec/session_tracker_spec.rb b/spec/session_tracker_spec.rb new file mode 100644 index 000000000..6af7220a0 --- /dev/null +++ b/spec/session_tracker_spec.rb @@ -0,0 +1,153 @@ +# encoding: utf-8 +require 'webrick' +require 'spec_helper' +require 'json' + +describe Bugsnag::SessionTracker do + server = nil + queue = Queue.new + + before do + @config = Bugsnag::Configuration.new + @config.track_sessions = true + server = WEBrick::HTTPServer.new :Port => 0, :Logger => WEBrick::Log.new("/dev/null"), :AccessLog => [] + server.mount_proc '/' do |req, res| + headers = [] + req.each { |header| headers << header } + queue << [JSON.parse(req.body), headers] + res.status = 202 + res.body = "OK\n" + end + Thread.new{ server.start } + end + + after do + Bugsnag.configure do |conf| + conf.track_sessions = false + conf.delivery_method = :synchronous + end + server.stop + queue.clear + end + + it 'does not create session object if disabled' do + config = Bugsnag::Configuration.new + config.track_sessions = false + tracker = Bugsnag::SessionTracker.new(config) + tracker.create_session + expect(tracker.session_counts.size).to eq(0) + end + + it 'adds session object to queue' do + tracker = Bugsnag::SessionTracker.new(@config) + tracker.create_session + expect(tracker.session_counts.size).to eq(1) + time = tracker.session_counts.keys.last + count = tracker.session_counts[time] + + expect(count).to eq(1) + end + + it 'stores session in thread' do + tracker = Bugsnag::SessionTracker.new(@config) + tracker.create_session + session = Thread.current[Bugsnag::SessionTracker::THREAD_SESSION] + expect(session.include? :id).to be true + expect(session.include? :startedAt).to be true + expect(session.include? :events).to be true + expect(session[:events].include? :handled).to be true + expect(session[:events].include? :unhandled).to be true + expect(session[:events][:handled]).to eq(0) + expect(session[:events][:unhandled]).to eq(0) + end + + it 'gives unique ids to each session' do + tracker = Bugsnag::SessionTracker.new(@config) + tracker.create_session + session_one = Thread.current[Bugsnag::SessionTracker::THREAD_SESSION] + tracker.create_session + session_two = Thread.current[Bugsnag::SessionTracker::THREAD_SESSION] + expect(session_one[:id]).to_not eq(session_two[:id]) + end + + it 'sends sessions when send_sessions is called' do + Bugsnag.configure do |conf| + conf.track_sessions = true + conf.delivery_method = :thread_queue + conf.session_endpoint = "http://localhost:#{server.config[:Port]}" + end + WebMock.allow_net_connect! + Bugsnag.session_tracker.create_session + expect(Bugsnag.session_tracker.session_counts.size).to eq(1) + Bugsnag.session_tracker.send_sessions + expect(Bugsnag.session_tracker.session_counts.size).to eq(0) + while queue.empty? + sleep(0.05) + end + payload, headers = queue.pop + expect(payload.include?("app")).to be true + expect(payload.include?("notifier")).to be true + expect(payload.include?("device")).to be true + expect(payload.include?("sessionCounts")).to be true + expect(payload["sessionCounts"].size).to eq(1) + end + + it 'sets details from config' do + Bugsnag.configure do |conf| + conf.track_sessions = true + conf.release_stage = "test_stage" + conf.delivery_method = :thread_queue + conf.session_endpoint = "http://localhost:#{server.config[:Port]}" + end + WebMock.allow_net_connect! + Bugsnag.session_tracker.create_session + expect(Bugsnag.session_tracker.session_counts.size).to eq(1) + Bugsnag.session_tracker.send_sessions + expect(Bugsnag.session_tracker.session_counts.size).to eq(0) + while queue.empty? + sleep(0.05) + end + payload, headers = queue.pop + notifier = payload["notifier"] + expect(notifier.include?("name")).to be true + expect(notifier["name"]).to eq(Bugsnag::Report::NOTIFIER_NAME) + expect(notifier.include?("url")).to be true + expect(notifier["url"]).to eq(Bugsnag::Report::NOTIFIER_URL) + expect(notifier.include?("version")).to be true + expect(notifier["version"]).to eq(Bugsnag::Report::NOTIFIER_VERSION) + + app = payload["app"] + expect(app.include?("releaseStage")).to be true + expect(app["releaseStage"]).to eq(Bugsnag.configuration.release_stage) + expect(app.include?("version")).to be true + expect(app["version"]).to eq(Bugsnag.configuration.app_version) + expect(app.include?("type")).to be true + expect(app["type"]).to eq(Bugsnag.configuration.app_type) + + device = payload["device"] + expect(device.include?("hostname")).to be true + expect(device["hostname"]).to eq(Bugsnag.configuration.hostname) + end + + it 'uses middleware to attach session to notification' do + Bugsnag.configure do |conf| + conf.track_sessions = true + conf.release_stage = "test_stage" + end + Bugsnag.session_tracker.create_session + Bugsnag.notify(BugsnagTestException.new("It crashed")) + expect(Bugsnag).to have_sent_notification{ |payload, headers| + event = payload["events"][0] + expect(event.include?("session")).to be true + session = event["session"] + expect(session.include?("id")).to be true + expect(session.include?("startedAt")).to be true + expect(session.include?("events")).to be true + sesevents = session['events'] + expect(sesevents.include?("unhandled")).to be true + expect(sesevents["unhandled"]).to eq(0) + expect(sesevents.include?("handled")).to be true + expect(sesevents["handled"]).to eq(1) + } + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index a1440b30c..109e7d8df 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -23,6 +23,10 @@ def get_event_from_payload(payload) payload["events"].first end +def get_headers_from_payload(payload) + +end + def get_exception_from_payload(payload) event = get_event_from_payload(payload) expect(event["exceptions"].size).to eq(1) @@ -46,6 +50,7 @@ def ruby_version_greater_equal?(version) config.before(:each) do WebMock.stub_request(:post, "https://notify.bugsnag.com/") + WebMock.stub_request(:post, "https://sessions.bugsnag.com/") Bugsnag.instance_variable_set(:@configuration, Bugsnag::Configuration.new) Bugsnag.configure do |bugsnag| @@ -62,10 +67,21 @@ def ruby_version_greater_equal?(version) end end +def have_sent_sessions(&matcher) + have_requested(:post, "https://sessions.bugsnag.com/").with do |request| + if matcher + matcher.call([JSON.parse(request.body), request.headers]) + true + else + raise "no matcher provided to have_sent_sessions (did you use { })" + end + end +end + def have_sent_notification(&matcher) have_requested(:post, "https://notify.bugsnag.com/").with do |request| if matcher - matcher.call JSON.parse(request.body) + matcher.call([JSON.parse(request.body), request.headers]) true else raise "no matcher provided to have_sent_notification (did you use { })" diff --git a/spec/stacktrace_spec.rb b/spec/stacktrace_spec.rb index b9b1e5134..749477fbd 100644 --- a/spec/stacktrace_spec.rb +++ b/spec/stacktrace_spec.rb @@ -10,7 +10,7 @@ _e = 5 _f = 6 - expect(Bugsnag).to have_sent_notification{ |payload| + expect(Bugsnag).to have_sent_notification{ |payload, headers| exception = get_exception_from_payload(payload) starting_line = __LINE__ - 10 expect(exception["stacktrace"][1]["code"]).to eq({ @@ -30,7 +30,7 @@ notify_test_exception - expect(Bugsnag).to have_sent_notification{ |payload| + expect(Bugsnag).to have_sent_notification{ |payload, headers| exception = get_exception_from_payload(payload) expect(exception["stacktrace"][1]["code"]).to eq(nil) } @@ -39,7 +39,7 @@ it 'should send the first 7 lines of the file for exceptions near the top' do load 'spec/fixtures/crashes/start_of_file.rb' rescue Bugsnag.notify $! - expect(Bugsnag).to have_sent_notification{ |payload| + expect(Bugsnag).to have_sent_notification{ |payload, headers| exception = get_exception_from_payload(payload) expect(exception["stacktrace"][0]["code"]).to eq({ @@ -57,7 +57,7 @@ it 'should send the last 7 lines of the file for exceptions near the bottom' do load 'spec/fixtures/crashes/end_of_file.rb' rescue Bugsnag.notify $! - expect(Bugsnag).to have_sent_notification{ |payload| + expect(Bugsnag).to have_sent_notification{ |payload, headers| exception = get_exception_from_payload(payload) expect(exception["stacktrace"][0]["code"]).to eq({ @@ -75,7 +75,7 @@ it 'should send the last 7 lines of the file for exceptions near the bottom' do load 'spec/fixtures/crashes/short_file.rb' rescue Bugsnag.notify $! - expect(Bugsnag).to have_sent_notification{ |payload| + expect(Bugsnag).to have_sent_notification{ |payload, headers| exception = get_exception_from_payload(payload) expect(exception["stacktrace"][0]["code"]).to eq({