From ca91338b7276e3cb71e39bf9022abe9c861da721 Mon Sep 17 00:00:00 2001 From: Kyrylo Silin Date: Wed, 27 Jun 2018 03:33:39 +0800 Subject: [PATCH] Implement GitRevisionFilter When the airbrake-ruby config doesn't specify `app_version` (99% of Rails apps), then `GitRevisionFilter` tries to find current revision of the app and attach it as `context/version`. --- CHANGELOG.md | 3 + lib/airbrake-ruby.rb | 1 + .../filters/git_revision_filter.rb | 57 ++++++++ lib/airbrake-ruby/notifier.rb | 6 + spec/filters/git_revision_filter_spec.rb | 128 ++++++++++++++++++ 5 files changed, 195 insertions(+) create mode 100644 lib/airbrake-ruby/filters/git_revision_filter.rb create mode 100644 spec/filters/git_revision_filter_spec.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index 93bd5353..52619cab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,9 @@ Airbrake Ruby Changelog ### master +* Added `GitRevisionFilter` + ([#333](https://github.com/airbrake/airbrake-ruby/pull/333)) + ### [v2.10.0][v2.10.0] (May 3, 2018) * Added the `versions` option diff --git a/lib/airbrake-ruby.rb b/lib/airbrake-ruby.rb index b04744da..86f4e429 100644 --- a/lib/airbrake-ruby.rb +++ b/lib/airbrake-ruby.rb @@ -26,6 +26,7 @@ require 'airbrake-ruby/filters/context_filter' require 'airbrake-ruby/filters/exception_attributes_filter' require 'airbrake-ruby/filters/dependency_filter' +require 'airbrake-ruby/filters/git_revision_filter' require 'airbrake-ruby/filter_chain' require 'airbrake-ruby/notifier' require 'airbrake-ruby/code_hunk' diff --git a/lib/airbrake-ruby/filters/git_revision_filter.rb b/lib/airbrake-ruby/filters/git_revision_filter.rb new file mode 100644 index 00000000..8e3350bd --- /dev/null +++ b/lib/airbrake-ruby/filters/git_revision_filter.rb @@ -0,0 +1,57 @@ +module Airbrake + module Filters + # Attaches current git revision to `context`. + # @api private + # @since v2.11.0 + class GitRevisionFilter + # @return [Integer] + attr_reader :weight + + # @return [String] + PREFIX = 'ref: '.freeze + + def initialize(root_directory) + @git_path = File.join(root_directory, '.git') + @weight = 120 + end + + # @macro call_filter + def call(notice) + return if notice[:context].key?(:version) + return unless File.exist?(@git_path) + + revision = find_revision + notice[:context][:version] = revision if revision + end + + private + + def find_revision + head_path = File.join(@git_path, 'HEAD') + return unless File.exist?(head_path) + + head = File.read(head_path) + return head unless head.start_with?(PREFIX) + head = head.chomp[PREFIX.size..-1] + + ref_path = File.join(@git_path, head) + return File.read(ref_path).chomp if File.exist?(ref_path) + + find_from_packed_refs(head) + end + + def find_from_packed_refs(head) + packed_refs_path = File.join(@git_path, 'packed-refs') + return head unless File.exist?(packed_refs_path) + + File.readlines(packed_refs_path).each do |line| + next if %w[# ^].include?(line[0]) + next unless (parts = line.split(' ')).size == 2 + return parts.first if parts.last == head + end + + nil + end + end + end +end diff --git a/lib/airbrake-ruby/notifier.rb b/lib/airbrake-ruby/notifier.rb index b993c9e4..f98e1ac9 100644 --- a/lib/airbrake-ruby/notifier.rb +++ b/lib/airbrake-ruby/notifier.rb @@ -143,6 +143,7 @@ def clean_backtrace clean_bt end + # rubocop:disable Metrics/AbcSize def add_default_filters if (whitelist_keys = @config.whitelist_keys).any? @filter_chain.add_filter( @@ -165,6 +166,11 @@ def add_default_filters @filter_chain.add_filter( Airbrake::Filters::RootDirectoryFilter.new(root_directory) ) + + @filter_chain.add_filter( + Airbrake::Filters::GitRevisionFilter.new(root_directory) + ) end + # rubocop:enable Metrics/AbcSize end end diff --git a/spec/filters/git_revision_filter_spec.rb b/spec/filters/git_revision_filter_spec.rb new file mode 100644 index 00000000..e937b3b5 --- /dev/null +++ b/spec/filters/git_revision_filter_spec.rb @@ -0,0 +1,128 @@ +require 'spec_helper' + +RSpec.describe Airbrake::Filters::GitRevisionFilter do + subject { described_class.new('root/dir') } + + let(:notice) do + Airbrake::Notice.new(Airbrake::Config.new, AirbrakeTestError.new) + end + + context "when context/version is defined" do + it "doesn't attach anything to context/version" do + notice[:context][:version] = '1.2.3' + subject.call(notice) + expect(notice[:context][:version]).to eq('1.2.3') + end + end + + context "when .git directory doesn't exist" do + it "doesn't attach anything to context/version" do + subject.call(notice) + expect(notice[:context][:version]).to be_nil + end + end + + context "when .git directory exists" do + before do + expect(File).to receive(:exist?).with('root/dir/.git').and_return(true) + end + + context "and when HEAD doesn't exist" do + before do + expect(File).to receive(:exist?).with('root/dir/.git/HEAD').and_return(false) + end + + it "doesn't attach anything to context/version" do + subject.call(notice) + expect(notice[:context][:version]).to be_nil + end + end + + context "and when HEAD exists" do + before do + expect(File).to receive(:exist?).with('root/dir/.git/HEAD').and_return(true) + end + + context "and also when HEAD doesn't start with 'ref: '" do + before do + expect(File).to( + receive(:read).with('root/dir/.git/HEAD').and_return('refs/foo') + ) + end + + it "attaches the content of HEAD to context/version" do + subject.call(notice) + expect(notice[:context][:version]).to eq('refs/foo') + end + end + + context "and also when HEAD starts with 'ref: " do + before do + expect(File).to( + receive(:read).with('root/dir/.git/HEAD').and_return("ref: refs/foo\n") + ) + end + + context "when the ref exists" do + before do + expect(File).to( + receive(:exist?).with('root/dir/.git/refs/foo').and_return(true) + ) + expect(File).to( + receive(:read).with('root/dir/.git/refs/foo').and_return("d34db33f\n") + ) + end + + it "attaches the revision from the ref to context/version" do + subject.call(notice) + expect(notice[:context][:version]).to eq('d34db33f') + end + end + + context "when the ref doesn't exist" do + before do + expect(File).to( + receive(:exist?).with('root/dir/.git/refs/foo').and_return(false) + ) + end + + context "and when '.git/packed-refs' exists" do + before do + expect(File).to( + receive(:exist?).with('root/dir/.git/packed-refs').and_return(true) + ) + expect(File).to( + receive(:readlines).with('root/dir/.git/packed-refs').and_return( + [ + "# pack-refs with: peeled fully-peeled\n", + "ccb316eecff79c7528d1ad43e5fa165f7a44d52e refs/tags/v3.0.30\n", + "^d358900f73ee5bfd6ca3a592cf23ac6e82df83c1", + "d34db33f refs/foo\n" + ] + ) + ) + end + + it "attaches the revision from 'packed-refs' to context/version" do + subject.call(notice) + expect(notice[:context][:version]).to eq('d34db33f') + end + end + + context "and when '.git/packed-refs' doesn't exist" do + before do + expect(File).to( + receive(:exist?).with('root/dir/.git/packed-refs').and_return(false) + ) + end + + it "attaches the content of HEAD to context/version" do + subject.call(notice) + expect(notice[:context][:version]).to eq('refs/foo') + end + end + end + end + end + end +end