Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow pausing and resuming sessions #704

Merged
merged 5 commits into from
Oct 5, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
27 changes: 25 additions & 2 deletions lib/bugsnag.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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.
#
Expand Down
2 changes: 1 addition & 1 deletion lib/bugsnag/middleware/session_data.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
54 changes: 49 additions & 5 deletions lib/bugsnag/session_tracker.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand Down
59 changes: 59 additions & 0 deletions spec/bugsnag_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
73 changes: 73 additions & 0 deletions spec/session_tracker_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 2 additions & 0 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down