airbrake_tools 0.0.8 → 0.0.10

Sign up to get free protection for your applications and to get access to all the features.
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