diff --git a/lib/pliny/error_reporters/sentry.rb b/lib/pliny/error_reporters/sentry.rb new file mode 100644 index 00000000..43f11bc4 --- /dev/null +++ b/lib/pliny/error_reporters/sentry.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +module Pliny + module ErrorReporters + class Sentry + def notify(exception, context:, rack_env:) + ::Sentry.with_scope do |scope| + configure_scope(scope, context: context, rack_env: rack_env) + ::Sentry.capture_exception(exception) + end + rescue Exception => e # rubocop:disable Lint/RescueException + ::Sentry.capture_exception(e) + raise + end + + private + + def configure_scope(scope, context:, rack_env:) + scope.set_context("custom", context) + + begin + person_data = extract_person_data_from_controller(rack_env) + if person_data && !person_data.empty? + scope.set_user( + id: person_data[:id], + email: person_data[:email], + username: person_data[:username], + ) + end + rescue => e + ::Sentry.capture_exception(e) + end + end + + def extract_person_data_from_controller(env) + env["sentry.person_data"] || {} + end + end + end +end diff --git a/lib/template/Gemfile b/lib/template/Gemfile index bf0dc99b..8ea247c9 100644 --- a/lib/template/Gemfile +++ b/lib/template/Gemfile @@ -11,6 +11,7 @@ gem "rack-ssl" gem "rack-timeout", "~> 0.6" gem "rake" gem "rollbar" +gem "sentry-ruby" gem "sequel", "~> 5.73" gem "sequel-paranoid" gem "sequel_pg", "~> 1.17", require: "sequel" diff --git a/lib/template/config/initializers/rollbar.rb b/lib/template/config/initializers/rollbar.rb deleted file mode 100644 index 365e584b..00000000 --- a/lib/template/config/initializers/rollbar.rb +++ /dev/null @@ -1,14 +0,0 @@ -# frozen_string_literal: true - -require "pliny/error_reporters/rollbar" - -Pliny::ErrorReporters.error_reporters << Pliny::ErrorReporters::Rollbar - -Rollbar.configure do |config| - config.enabled = ENV.key?("ROLLBAR_ACCESS_TOKEN") - config.disable_rack_monkey_patch = true - config.access_token = ENV["ROLLBAR_ACCESS_TOKEN"] - config.environment = ENV["ROLLBAR_ENV"] - config.logger = Pliny::RollbarLogger.new - config.use_sucker_punch -end diff --git a/lib/template/config/initializers/sentry.rb b/lib/template/config/initializers/sentry.rb new file mode 100644 index 00000000..c98ee496 --- /dev/null +++ b/lib/template/config/initializers/sentry.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +require "pliny/error_reporters/sentry" + +Pliny::ErrorReporters.error_reporters << Pliny::ErrorReporters::Sentry + +Sentry.init do |config| + config.dsn = ENV["SENTRY_DSN"] + config.environment = ENV["SENTRY_ENV"] || ENV["RACK_ENV"] + config.enabled_environments = ENV["SENTRY_ENABLED_ENVIRONMENTS"]&.split(",") || %w[production staging] + config.traces_sample_rate = ENV["SENTRY_TRACES_SAMPLE_RATE"]&.to_f || 0.1 +end + +Pliny.use Sentry::Rack::CaptureExceptions diff --git a/pliny.gemspec b/pliny.gemspec index 662618c5..2db8a2f7 100644 --- a/pliny.gemspec +++ b/pliny.gemspec @@ -34,6 +34,7 @@ Gem::Specification.new do |gem| gem.add_development_dependency "rake" gem.add_development_dependency "rollbar" gem.add_development_dependency "rspec" + gem.add_development_dependency "sentry-ruby" gem.add_development_dependency "rubocop" gem.add_development_dependency "sequel" gem.add_development_dependency "sinatra-contrib" diff --git a/spec/error_reporters/sentry_spec.rb b/spec/error_reporters/sentry_spec.rb new file mode 100644 index 00000000..7f960434 --- /dev/null +++ b/spec/error_reporters/sentry_spec.rb @@ -0,0 +1,83 @@ +# frozen_string_literal: true + +require "spec_helper" +require "sentry-ruby" +require "pliny/error_reporters/sentry" + +describe Pliny::ErrorReporters::Sentry do + subject(:reporter) { described_class.new } + + describe "#notify" do + let(:exception) { StandardError.new("Something went wrong") } + let(:context) { { step: :foo } } + let(:rack_env) { { "rack.input" => StringIO.new } } + + subject(:notify) do + reporter.notify(exception, context: context, rack_env: rack_env) + end + + before do + allow(::Sentry).to receive(:with_scope).and_yield(scope) + allow(::Sentry).to receive(:capture_exception) + end + + let(:scope) { instance_double("Sentry::Scope") } + + before do + allow(scope).to receive(:set_context) + allow(scope).to receive(:set_user) + end + + it "creates a sentry scope" do + notify + expect(::Sentry).to have_received(:with_scope).once + end + + it "sets custom context" do + notify + expect(scope).to have_received(:set_context).with("custom", { step: :foo }) + end + + it "captures the exception" do + notify + expect(::Sentry).to have_received(:capture_exception).with(exception) + end + + context "given a rack_env with sentry.person_data" do + let(:rack_env) { { "sentry.person_data" => { id: 123, email: "test@example.com", username: "testuser" }, "rack.input" => StringIO.new } } + + it "sets user context from sentry.person_data" do + notify + expect(scope).to have_received(:set_user).with(id: 123, email: "test@example.com", username: "testuser") + end + end + + context "given a rack_env with empty sentry.person_data" do + let(:rack_env) { { "sentry.person_data" => {}, "rack.input" => StringIO.new } } + + it "does not set user context" do + notify + expect(scope).not_to have_received(:set_user) + end + end + + context "given an empty rack_env" do + let(:rack_env) { {} } + + it "expects rack_env to be a hash" do + assert_kind_of(Hash, rack_env) + end + + it "sets only custom context" do + notify + expect(scope).to have_received(:set_context).once.with("custom", { step: :foo }) + expect(scope).not_to have_received(:set_user) + end + + it "captures the exception" do + notify + expect(::Sentry).to have_received(:capture_exception).with(exception) + end + end + end +end