diff --git a/CHANGELOG.md b/CHANGELOG.md index 34ca9aea..12421982 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ Changelog | [#700](https://github.com/bugsnag/bugsnag-ruby/pull/700) * Add `Configuration#endpoints` for reading the notify and sessions endpoints and `Configuration#endpoints=` for setting them | [#701](https://github.com/bugsnag/bugsnag-ruby/pull/701) +* Allow pausing and resuming sessions, giving more control over stability score + | [#704](https://github.com/bugsnag/bugsnag-ruby/pull/704) ### Deprecated diff --git a/lib/bugsnag.rb b/lib/bugsnag.rb index a2786c97..4d94ce8c 100644 --- a/lib/bugsnag.rb +++ b/lib/bugsnag.rb @@ -197,13 +197,36 @@ def session_tracker end ## - # Starts a session. + # Starts a new session, which allows Bugsnag to track error rates across + # releases # - # Allows Bugsnag to track error rates across releases. + # @return [void] def start_session session_tracker.start_session end + ## + # Stop any events being attributed to the current session until it is + # resumed or a new session is started + # + # @see resume_session + # + # @return [void] + def pause_session + session_tracker.pause_session + end + + ## + # Resume the current session if it was previously paused. If there is no + # current session, a new session will be started + # + # @see pause_session + # + # @return [Boolean] true if a paused session was resumed + def resume_session + session_tracker.resume_session + end + ## # Allow access to "before notify" callbacks as an array. # diff --git a/lib/bugsnag/middleware/session_data.rb b/lib/bugsnag/middleware/session_data.rb index 4e7c029b..908e9852 100644 --- a/lib/bugsnag/middleware/session_data.rb +++ b/lib/bugsnag/middleware/session_data.rb @@ -9,7 +9,7 @@ def initialize(bugsnag) def call(report) session = Bugsnag::SessionTracker.get_current_session - if session + if session && !session[:paused?] if report.unhandled session[:events][:unhandled] += 1 else diff --git a/lib/bugsnag/session_tracker.rb b/lib/bugsnag/session_tracker.rb index 810816f0..3445ee24 100644 --- a/lib/bugsnag/session_tracker.rb +++ b/lib/bugsnag/session_tracker.rb @@ -34,17 +34,20 @@ def initialize # Starts a new session, storing it on the current thread. # # This allows Bugsnag to track error rates for a release. + # + # @return [void] def start_session return unless Bugsnag.configuration.enable_sessions && Bugsnag.configuration.should_notify_release_stage? start_delivery_thread 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 + id: SecureRandom.uuid, + startedAt: start_time, + paused?: false, + events: { + handled: 0, + unhandled: 0 } } SessionTracker.set_current_session(new_session) @@ -53,6 +56,47 @@ def start_session alias_method :create_session, :start_session + ## + # Stop any events being attributed to the current session until it is + # resumed or a new session is started + # + # @see resume_session + # + # @return [void] + def pause_session + current_session = SessionTracker.get_current_session + + return unless current_session + + current_session[:paused?] = true + end + + ## + # Resume the current session if it was previously paused. If there is no + # current session, a new session will be started + # + # @see pause_session + # + # @return [Boolean] true if a paused session was resumed + def resume_session + current_session = SessionTracker.get_current_session + + if current_session + # if the session is paused then resume it, otherwise we don't need to + # do anything + if current_session[:paused?] + current_session[:paused?] = false + + return true + end + else + # if there's no current session, start a new one + start_session + end + + false + end + ## # Delivers the current session_counts lists to the session endpoint. def send_sessions diff --git a/spec/bugsnag_spec.rb b/spec/bugsnag_spec.rb index 1ab82baf..99cc0656 100644 --- a/spec/bugsnag_spec.rb +++ b/spec/bugsnag_spec.rb @@ -685,6 +685,65 @@ module Kernel }) end + it "does not attach session information when the session is paused" do + Bugsnag.configure do |config| + config.auto_capture_sessions = true + end + + Bugsnag.start_session + Bugsnag.pause_session + + Bugsnag.notify(BugsnagTestException.new("It crashed"), true) + + expect(Bugsnag).to(have_sent_notification { |payload, headers| + expect(payload["events"][0]["session"]).to be(nil) + + expect(Bugsnag::SessionTracker.get_current_session[:events]).to eq({ + handled: 0, + unhandled: 0, + }) + }) + end + + it "attaches session information when the session is resumed" do + Bugsnag.configure do |config| + config.auto_capture_sessions = true + end + + Bugsnag.start_session + + Bugsnag.notify(BugsnagTestException.new("one handled")) + + Bugsnag.pause_session + + Bugsnag.notify(BugsnagTestException.new("this unhandled error is not counted"), true) + Bugsnag.notify(BugsnagTestException.new("this handled error is not counted")) + + # reset WebMock's stored requests so we only assert against the last one + # as "have_sent_notification" doesn't support finding a specific request + WebMock::RequestRegistry.instance.reset! + + Bugsnag.resume_session + + Bugsnag.notify(BugsnagTestException.new("one unhandled"), true) + + expect(Bugsnag).to(have_sent_notification { |payload, headers| + session = payload["events"][0]["session"] + + expect(session["id"]).to match(session_id_regex) + expect(session["startedAt"]).to match(session_timestamp_regex) + expect(session["events"]).to eq({ + "handled" => 1, + "unhandled" => 1, + }) + + expect(Bugsnag::SessionTracker.get_current_session[:events]).to eq({ + handled: 1, + unhandled: 1, + }) + }) + end + it "allows changing an event from handled to unhandled" do Bugsnag.configure do |config| config.auto_capture_sessions = true diff --git a/spec/session_tracker_spec.rb b/spec/session_tracker_spec.rb index 23f1557d..81bcb657 100644 --- a/spec/session_tracker_spec.rb +++ b/spec/session_tracker_spec.rb @@ -151,4 +151,77 @@ expect(device["hostname"]).to eq(Bugsnag.configuration.hostname) expect(device["runtimeVersions"]["ruby"]).to eq(Bugsnag.configuration.runtime_versions["ruby"]) end + + context "#pause_session" do + it "does nothing if there is no current session" do + Bugsnag.pause_session + + expect(Bugsnag::SessionTracker.get_current_session).to be(nil) + end + + it "marks the current session as paused if one exists" do + Bugsnag.start_session + + expect(Bugsnag::SessionTracker.get_current_session[:paused?]).to be(false) + + Bugsnag.pause_session + + expect(Bugsnag::SessionTracker.get_current_session[:paused?]).to be(true) + end + + it "does nothing if the current session is already paused" do + Bugsnag.start_session + + expect(Bugsnag::SessionTracker.get_current_session[:paused?]).to be(false) + + Bugsnag.pause_session + + expect(Bugsnag::SessionTracker.get_current_session[:paused?]).to be(true) + + Bugsnag.pause_session + + expect(Bugsnag::SessionTracker.get_current_session[:paused?]).to be(true) + end + end + + context "#resume_session" do + it "returns false and does nothing when there is a current session" do + Bugsnag.start_session + + expect(Bugsnag::SessionTracker.get_current_session[:paused?]).to be(false) + + expect(Bugsnag.resume_session).to be(false) + + expect(Bugsnag::SessionTracker.get_current_session[:paused?]).to be(false) + end + + it "returns false and does nothing when a session is started after one has been paused" do + Bugsnag.start_session + Bugsnag.pause_session + Bugsnag.start_session + + expect(Bugsnag::SessionTracker.get_current_session[:paused?]).to be(false) + + expect(Bugsnag.resume_session).to be(false) + + expect(Bugsnag::SessionTracker.get_current_session[:paused?]).to be(false) + end + + it "returns false and starts a new session when there is no current or paused session" do + expect(Bugsnag.resume_session).to be(false) + + expect(Bugsnag::SessionTracker.get_current_session).not_to be(nil) + end + + it "returns true and makes the paused session the active session when there is no current session" do + Bugsnag.start_session + Bugsnag.pause_session + + expect(Bugsnag::SessionTracker.get_current_session[:paused?]).to be(true) + + expect(Bugsnag.resume_session).to be(true) + + expect(Bugsnag::SessionTracker.get_current_session).not_to be(nil) + end + end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index e754d3d5..2575a46d 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -59,6 +59,8 @@ def ruby_version_greater_equal?(target_version) Bugsnag.instance_variable_set(:@session_tracker, Bugsnag::SessionTracker.new) Bugsnag.instance_variable_set(:@cleaner, Bugsnag::Cleaner.new(Bugsnag.configuration)) + Thread.current[Bugsnag::SessionTracker::THREAD_SESSION] = nil + Bugsnag.configure do |bugsnag| bugsnag.api_key = "c9d60ae4c7e70c4b6c4ebd3e8056d2b8" bugsnag.release_stage = "production"