airbrake_tools 0.0.8 → 0.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/Gemfile CHANGED
@@ -1,6 +1,6 @@
1
1
  source :rubygems
2
2
  gemspec
3
3
 
4
- gem "bump"
4
+ gem "bump", "0.3.8"
5
5
  gem "rake"
6
6
  gem "rspec", "~>2"
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- airbrake_tools (0.0.8)
4
+ airbrake_tools (0.0.10)
5
5
  airbrake-api (>= 4.2.2)
6
6
 
7
7
  GEM
@@ -13,7 +13,7 @@ GEM
13
13
  mash
14
14
  multi_xml
15
15
  parallel (~> 0.5.0)
16
- bump (0.3.5)
16
+ bump (0.3.8)
17
17
  diff-lcs (1.1.3)
18
18
  faraday (0.8.4)
19
19
  multipart-post (~> 1.1)
@@ -39,6 +39,6 @@ PLATFORMS
39
39
 
40
40
  DEPENDENCIES
41
41
  airbrake_tools!
42
- bump
42
+ bump (= 0.3.8)
43
43
  rake
44
44
  rspec (~> 2)
data/Readme.md CHANGED
@@ -50,6 +50,7 @@ Shows all errors divided by pages
50
50
 
51
51
  - show details for 1 error (combines 150 notices)
52
52
  - show all different traces that caused this error (play with --compare-depth)
53
+ - shows blame for the line if it's in the project and you are running airrake-tools from the project root
53
54
 
54
55
  ```
55
56
  airbrake-tools your-account your-auth-token summary 51344729
@@ -63,7 +64,8 @@ Mysql2::Error: Lost connection to MySQL server at 'reading initial communication
63
64
 
64
65
  Trace 2: occurred 10 times e.g. 7145613107, 7145612108
65
66
  Mysql2::Error: Lost connection to MySQL server
66
- ./mysql2/lib/mysql2/client.rb:58:in `disconnect'
67
+ /usr/gems/mysql2/lib/mysql2/client.rb:58:in `disconnect'
68
+ lib/foo.rb:58:in `bar' acc8204 (<jcheatham@example.com> 2012-11-06 18:45:10 -0800 )
67
69
  ...
68
70
 
69
71
  Trace 3: occurred 5 times e.g. 7145609979, 7145609161
@@ -1,3 +1,3 @@
1
1
  module AirbrakeTools
2
- VERSION = "0.0.8"
2
+ VERSION = "0.0.10"
3
3
  end
@@ -6,8 +6,12 @@ module AirbrakeTools
6
6
  DEFAULT_HOT_PAGES = 1
7
7
  DEFAULT_NEW_PAGES = 1
8
8
  DEFAULT_SUMMARY_PAGES = 5
9
- DEFAULT_COMPARE_DEPTH = 7
9
+ DEFAULT_COMPARE_DEPTH_ADDITION = 3 # first line in project is 6 -> compare at 6 + x depth
10
10
  DEFAULT_ENVIRONMENT = "production"
11
+ COLORS = {
12
+ :gray => "\e[0;37m",
13
+ :clear => "\e[0m"
14
+ }
11
15
 
12
16
  class << self
13
17
  def cli(argv)
@@ -63,31 +67,78 @@ module AirbrakeTools
63
67
  end
64
68
 
65
69
  def summary(error_id, options)
66
- compare_depth = options[:compare_depth] || DEFAULT_COMPARE_DEPTH
67
70
  notices = AirbrakeAPI.notices(error_id, :pages => options[:pages] || DEFAULT_SUMMARY_PAGES)
68
71
 
69
72
  puts "last retrieved notice: #{((Time.now - notices.last.created_at) / (60 * 60)).round} hours ago at #{notices.last.created_at}"
70
73
  puts "last 2 hours: #{sparkline(notices, :slots => 60, :interval => 120)}"
71
74
  puts "last day: #{sparkline(notices, :slots => 24, :interval => 60 * 60)}"
72
75
 
73
- backtraces = notices.compact.select{|n| n.backtrace }.group_by do |notice|
74
- if notice.backtrace.is_a?(String) # no backtrace recorded...
75
- []
76
- else
77
- notice.backtrace.first[1][0..compare_depth]
78
- end
79
- end
80
-
81
- backtraces.sort_by{|_,notices| notices.size }.reverse.each_with_index do |(backtrace, notices), index|
76
+ grouped_backtraces(notices, options).sort_by{|_,notices| notices.size }.reverse.each_with_index do |(backtrace, notices), index|
82
77
  puts "Trace #{index + 1}: occurred #{notices.size} times e.g. #{notices[0..5].map(&:id).join(", ")}"
83
78
  puts notices.first.error_message
84
- puts backtrace.map{|line| line.sub("[PROJECT_ROOT]/", "./") }
79
+ puts backtrace.map{|line| present_line(line) }
85
80
  puts ""
86
81
  end
87
82
  end
88
83
 
89
84
  private
90
85
 
86
+ def present_line(line)
87
+ color = :gray if $stdout.tty? && !custom_file?(line)
88
+ line = line.sub("[PROJECT_ROOT]/", "")
89
+ line = add_blame(line)
90
+ if color
91
+ "#{COLORS.fetch(color)}#{line}#{COLORS.fetch(:clear)}"
92
+ else
93
+ line
94
+ end
95
+ end
96
+
97
+ def add_blame(backtrace_line)
98
+ file, line = backtrace_line.split(":", 2)
99
+ line = line.to_i
100
+ if not file.start_with?("/") and line > 0 and File.exist?(".git") and File.exist?(file)
101
+ result = `git blame #{file} -L #{line},#{line} --show-email -w 2>&1`
102
+ if $?.success?
103
+ result.sub!(/ #{line}\) .*/, " )") # cut of source code
104
+ backtrace_line += " -- #{result.strip}"
105
+ end
106
+ end
107
+ backtrace_line
108
+ end
109
+
110
+ def grouped_backtraces(notices, options)
111
+ notices = notices.compact.select { |n| backtrace(n) }
112
+
113
+ compare_depth = if options[:compare_depth]
114
+ options[:compare_depth]
115
+ else
116
+ average_first_project_line(notices.map { |n| backtrace(n) }) +
117
+ DEFAULT_COMPARE_DEPTH_ADDITION
118
+ end
119
+
120
+ notices.group_by do |notice|
121
+ backtrace(notice)[0..compare_depth]
122
+ end
123
+ end
124
+
125
+ def backtrace(notice)
126
+ return if notice.backtrace.is_a?(String)
127
+ notice.backtrace.first[1]
128
+ end
129
+
130
+ def average_first_project_line(backtraces)
131
+ depths = backtraces.map do |backtrace|
132
+ backtrace.index { |line| custom_file?(line) }
133
+ end.compact
134
+ return 0 if depths.size == 0
135
+ depths.inject(:+) / depths.size
136
+ end
137
+
138
+ def custom_file?(line)
139
+ line.start_with?("[PROJECT_ROOT]") && !line.start_with?("[PROJECT_ROOT]/vendor/")
140
+ end
141
+
91
142
  def add_notices_to_pages(errors)
92
143
  Parallel.map(errors, :in_threads => 10) do |error|
93
144
  begin
@@ -151,7 +202,7 @@ module AirbrakeTools
151
202
 
152
203
  Options:
153
204
  BANNER
154
- opts.on("-c NUM", "--compare-depth NUM", Integer, "How deep to compare backtraces in summary (default: #{DEFAULT_COMPARE_DEPTH})") {|s| options[:compare_depth] = s }
205
+ opts.on("-c NUM", "--compare-depth NUM", Integer, "How deep to compare backtraces in summary (default: first line in project + #{DEFAULT_COMPARE_DEPTH_ADDITION})") {|s| options[:compare_depth] = s }
155
206
  opts.on("-p NUM", "--pages NUM", Integer, "How maybe pages to iterate over (default: hot:#{DEFAULT_HOT_PAGES} new:#{DEFAULT_NEW_PAGES} summary:#{DEFAULT_SUMMARY_PAGES})") {|s| options[:pages] = s }
156
207
  opts.on("-e ENV", "--environment ENV", String, "Only show errors from this environment (default: #{DEFAULT_ENVIRONMENT})") {|s| options[:env] = s }
157
208
  opts.on("-h", "--help", "Show this.") { puts opts; exit }
@@ -1,75 +1,144 @@
1
1
  require 'yaml'
2
+ require 'stringio'
2
3
  require 'airbrake_tools'
3
4
 
4
5
  ROOT = File.expand_path('../../', __FILE__)
5
6
 
6
7
  describe "airbrake-tools" do
7
- def run(command, options={})
8
- result = `#{command} 2>&1`
9
- message = (options[:fail] ? "SUCCESS BUT SHOULD FAIL" : "FAIL")
10
- raise "[#{message}] #{result} [#{command}]" if $?.success? == !!options[:fail]
11
- result
12
- end
8
+ before { Dir.chdir ROOT }
13
9
 
14
- def airbrake_tools(args, options={})
15
- run "#{ROOT}/bin/airbrake-tools #{args}", options
16
- end
10
+ describe "CLI" do
11
+ def run(command, options={})
12
+ result = `#{command} 2>&1`
13
+ message = (options[:fail] ? "SUCCESS BUT SHOULD FAIL" : "FAIL")
14
+ raise "[#{message}] #{result} [#{command}]" if $?.success? == !!options[:fail]
15
+ result
16
+ end
17
17
 
18
- let(:config) { YAML.load(File.read("spec/fixtures.yml")) }
18
+ def airbrake_tools(args, options={})
19
+ run "#{ROOT}/bin/airbrake-tools #{args}", options
20
+ end
19
21
 
20
- before do
21
- Dir.chdir ROOT
22
- end
22
+ let(:config) { YAML.load(File.read("spec/fixtures.yml")) }
23
+
24
+ describe "basics" do
25
+ it "shows its usage without arguments" do
26
+ airbrake_tools("", :fail => true).should include("Usage")
27
+ end
28
+
29
+ it "shows its usage with -h" do
30
+ airbrake_tools("-h").should include("Usage")
31
+ end
23
32
 
24
- describe "basics" do
25
- it "shows its usage without arguments" do
26
- airbrake_tools("", :fail => true).should include("Usage")
33
+ it "shows its usage with --help" do
34
+ airbrake_tools("--help").should include("Usage")
35
+ end
36
+
37
+ it "shows its version with -v" do
38
+ airbrake_tools("-v").should =~ /^airbrake-tools \d+\.\d+\.\d+$/
39
+ end
40
+
41
+ it "shows its version with --version" do
42
+ airbrake_tools("-v").should =~ /^airbrake-tools \d+\.\d+\.\d+$/
43
+ end
27
44
  end
28
45
 
29
- it "shows its usage with -h" do
30
- airbrake_tools("-h").should include("Usage")
46
+ describe "hot" do
47
+ it "kinda works" do
48
+ output = airbrake_tools("#{config["subdomain"]} #{config["auth_token"]} hot")
49
+ output.should =~ /#\d+\s+\d+\.\d+\/hour\s+total:\d+/
50
+ end
31
51
  end
32
52
 
33
- it "shows its usage with --help" do
34
- airbrake_tools("--help").should include("Usage")
53
+ describe "list" do
54
+ it "kinda works" do
55
+ output = airbrake_tools("#{config["subdomain"]} #{config["auth_token"]} list")
56
+ output.should include("Page 1 ")
57
+ output.should =~ /^\d+/
58
+ end
35
59
  end
36
60
 
37
- it "shows its version with -v" do
38
- airbrake_tools("-v").should =~ /^airbrake-tools \d+\.\d+\.\d+$/
61
+ describe "summary" do
62
+ it "kinda works" do
63
+ output = airbrake_tools("#{config["subdomain"]} #{config["auth_token"]} summary 51344729")
64
+ output.should include("last retrieved notice: ")
65
+ output.should include("last 2 hours: ")
66
+ end
39
67
  end
40
68
 
41
- it "shows its version with --version" do
42
- airbrake_tools("-v").should =~ /^airbrake-tools \d+\.\d+\.\d+$/
69
+ describe "new" do
70
+ it "kinda works" do
71
+ output = airbrake_tools("#{config["subdomain"]} #{config["auth_token"]} new")
72
+ output.should =~ /#\d+\s+\d+\.\d+\/hour\s+total:\d+/
73
+ end
43
74
  end
44
75
  end
45
76
 
46
- describe "hot" do
47
- it "kinda works" do
48
- output = airbrake_tools("#{config["subdomain"]} #{config["auth_token"]} hot")
49
- output.should =~ /#\d+\s+\d+\.\d+\/hour\s+total:\d+/
77
+ describe ".average_first_project_line" do
78
+ it "is 0 for 0" do
79
+ AirbrakeTools.send(:average_first_project_line, []).should == 0
50
80
  end
51
- end
52
81
 
53
- describe "list" do
54
- it "kinda works" do
55
- output = airbrake_tools("#{config["subdomain"]} #{config["auth_token"]} list")
56
- output.should include("Page 1 ")
57
- output.should =~ /^\d+/
82
+ it "is 0 for no matching line" do
83
+ AirbrakeTools.send(:average_first_project_line, [["/usr/local/rvm/rubies/foo.rb"]]).should == 0
84
+ end
85
+
86
+ it "is the average of matching lines" do
87
+ gem = "/usr/local/rvm/foo.rb:123"
88
+ local = "[PROJECT_ROOT]/foo.rb:123"
89
+ AirbrakeTools.send(:average_first_project_line, [
90
+ [gem, local, local, local], # 1
91
+ [gem, gem, gem, local, gem], # 3
92
+ [gem, gem, gem, gem, gem, gem], # 0
93
+ ]).should == 2
58
94
  end
59
95
  end
60
96
 
61
- describe "summary" do
62
- it "kinda works" do
63
- output = airbrake_tools("#{config["subdomain"]} #{config["auth_token"]} summary 51344729")
64
- output.should include("last retrieved notice: ")
65
- output.should include("last 2 hours: ")
97
+ describe ".custom_file?" do
98
+ it "is custom if it's not a library" do
99
+ AirbrakeTools.send(:custom_file?, "[PROJECT_ROOT]/app/foo.rb:123").should == true
100
+ end
101
+
102
+ it "is not custom if it's a system gem" do
103
+ AirbrakeTools.send(:custom_file?, "/usr/local/rvm/foo.rb:123").should == false
104
+ end
105
+
106
+ it "is not custom if it's a vendored gem" do
107
+ AirbrakeTools.send(:custom_file?, "[PROJECT_ROOT]/vendor/bundle/xxx.rb:123").should == false
66
108
  end
67
109
  end
68
110
 
69
- describe "newest" do
70
- it "kinda works" do
71
- output = airbrake_tools("#{config["subdomain"]} #{config["auth_token"]} new")
72
- output.should =~ /#\d+\s+\d+\.\d+\/hour\s+total:\d+/
111
+ describe ".present_line" do
112
+ before do
113
+ $stdout.stub(:tty?).and_return false
114
+ end
115
+
116
+ it "does not add colors" do
117
+ AirbrakeTools.send(:present_line, "[PROJECT_ROOT]/vendor/bundle/foo.rb:123").should_not include("\e[")
118
+ AirbrakeTools.send(:present_line, "[PROJECT_ROOT]/app/foo.rb:123").should_not include("\e[")
119
+ end
120
+
121
+ context "on tty" do
122
+ before do
123
+ $stdout.stub(:tty?).and_return true
124
+ end
125
+
126
+ it "shows gray for vendor lines" do
127
+ AirbrakeTools.send(:present_line, "[PROJECT_ROOT]/vendor/bundle/foo.rb:123").should include("\e[")
128
+ end
129
+
130
+ it "does not add colors for project lines" do
131
+ AirbrakeTools.send(:present_line, "[PROJECT_ROOT]/app/foo.rb:123").should_not include("\e[")
132
+ end
133
+ end
134
+
135
+ it "adds blame if file exists" do
136
+ AirbrakeTools.send(:present_line, "[PROJECT_ROOT]/Gemfile:1 adasdsad").should ==
137
+ "Gemfile:1 adasdsad -- ^acc8204 (<jcheatham@zendesk.com> 2012-11-06 18:45:10 -0800 )"
138
+ end
139
+
140
+ it "does not add blame to system files" do
141
+ AirbrakeTools.send(:present_line, "/etc/hosts:1 adasdsad").should == "/etc/hosts:1 adasdsad"
73
142
  end
74
143
  end
75
144
 
@@ -125,4 +194,3 @@ describe "airbrake-tools" do
125
194
  end
126
195
  end
127
196
  end
128
-
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: airbrake_tools
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.8
4
+ version: 0.0.10
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-02-13 00:00:00.000000000 Z
12
+ date: 2013-02-28 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: airbrake-api
@@ -60,12 +60,18 @@ required_ruby_version: !ruby/object:Gem::Requirement
60
60
  - - ! '>='
61
61
  - !ruby/object:Gem::Version
62
62
  version: '0'
63
+ segments:
64
+ - 0
65
+ hash: -3539053511181789551
63
66
  required_rubygems_version: !ruby/object:Gem::Requirement
64
67
  none: false
65
68
  requirements:
66
69
  - - ! '>='
67
70
  - !ruby/object:Gem::Version
68
71
  version: '0'
72
+ segments:
73
+ - 0
74
+ hash: -3539053511181789551
69
75
  requirements: []
70
76
  rubyforge_project:
71
77
  rubygems_version: 1.8.23