Commit 5d02ba79907cc025d986395e6ab462237bd19736
1 parent
f761cda4
Exists in
master
and in
1 other branch
backtrace extraced to it's class
Showing
20 changed files
with
150 additions
and
64 deletions
Show diff stats
... | ... | @@ -0,0 +1,32 @@ |
1 | +class Backtrace | |
2 | + include Mongoid::Document | |
3 | + include Mongoid::Timestamps | |
4 | + | |
5 | + field :fingerprint | |
6 | + index :fingerprint | |
7 | + | |
8 | + has_many :notices | |
9 | + embeds_many :lines, :class_name => "BacktraceLine" | |
10 | + | |
11 | + after_initialize :generate_fingerprint | |
12 | + | |
13 | + def self.find_or_create(attributes = {}) | |
14 | + new(attributes).similar || create(attributes) | |
15 | + end | |
16 | + | |
17 | + def similar | |
18 | + Backtrace.first(:conditions => { :fingerprint => fingerprint } ) | |
19 | + end | |
20 | + | |
21 | + def raw=(raw) | |
22 | + raw.each do |raw_line| | |
23 | + lines << BacktraceLine.new(BacktraceLineNormalizer.new(raw_line).call) | |
24 | + end | |
25 | + end | |
26 | + | |
27 | + private | |
28 | + def generate_fingerprint | |
29 | + self.fingerprint = Digest::SHA1.hexdigest(lines.join) | |
30 | + end | |
31 | + | |
32 | +end | ... | ... |
... | ... | @@ -0,0 +1,11 @@ |
1 | +class BacktraceLineNormalizer | |
2 | + def initialize(raw_line) | |
3 | + @raw_line = raw_line | |
4 | + end | |
5 | + | |
6 | + def call | |
7 | + @raw_line.merge! 'file' => "[unknown source]" if @raw_line['file'].blank? | |
8 | + @raw_line.merge! 'method' => @raw_line['method'].gsub(/[0-9_]{10,}+/, "__FRAGMENT__") | |
9 | + end | |
10 | + | |
11 | +end | ... | ... |
app/models/error_report.rb
... | ... | @@ -2,7 +2,7 @@ require 'digest/sha1' |
2 | 2 | require 'hoptoad_notifier' |
3 | 3 | |
4 | 4 | class ErrorReport |
5 | - attr_reader :error_class, :message, :backtrace, :request, :server_environment, :api_key, :notifier, :user_attributes, :current_user | |
5 | + attr_reader :error_class, :message, :request, :server_environment, :api_key, :notifier, :user_attributes, :current_user | |
6 | 6 | |
7 | 7 | def initialize(xml_or_attributes) |
8 | 8 | @attributes = (xml_or_attributes.is_a?(String) ? Hoptoad.parse_xml!(xml_or_attributes) : xml_or_attributes).with_indifferent_access |
... | ... | @@ -29,11 +29,15 @@ class ErrorReport |
29 | 29 | @app ||= App.find_by_api_key!(api_key) |
30 | 30 | end |
31 | 31 | |
32 | + def backtrace | |
33 | + @normalized_backtrace ||= Backtrace.find_or_create(:raw => @backtrace) | |
34 | + end | |
35 | + | |
32 | 36 | def generate_notice! |
33 | 37 | notice = Notice.new( |
34 | 38 | :message => message, |
35 | 39 | :error_class => error_class, |
36 | - :backtrace => backtrace, | |
40 | + :backtrace_id => backtrace.id, | |
37 | 41 | :request => request, |
38 | 42 | :server_environment => server_environment, |
39 | 43 | :notifier => notifier, |
... | ... | @@ -55,7 +59,7 @@ class ErrorReport |
55 | 59 | private |
56 | 60 | def fingerprint_source |
57 | 61 | { |
58 | - :backtrace => normalized_backtrace.to_s, | |
62 | + :backtrace => backtrace.lines[0..3], | |
59 | 63 | :error_class => error_class, |
60 | 64 | :component => component, |
61 | 65 | :action => action, |
... | ... | @@ -64,11 +68,5 @@ class ErrorReport |
64 | 68 | } |
65 | 69 | end |
66 | 70 | |
67 | - def normalized_backtrace | |
68 | - backtrace[0...3].map do |trace| | |
69 | - trace.merge 'method' => trace['method'].gsub(/[0-9_]{10,}+/, "__FRAGMENT__") | |
70 | - end | |
71 | - end | |
72 | - | |
73 | 71 | end |
74 | 72 | ... | ... |
app/models/notice.rb
... | ... | @@ -6,15 +6,16 @@ class Notice |
6 | 6 | include Mongoid::Timestamps |
7 | 7 | |
8 | 8 | field :message |
9 | - field :backtrace, :type => Array | |
10 | 9 | field :server_environment, :type => Hash |
11 | 10 | field :request, :type => Hash |
12 | 11 | field :notifier, :type => Hash |
13 | 12 | field :user_attributes, :type => Hash |
14 | 13 | field :current_user, :type => Hash |
15 | 14 | field :error_class |
15 | + delegate :lines, :to => :backtrace, :prefix => true | |
16 | 16 | |
17 | 17 | belongs_to :err |
18 | + belongs_to :backtrace, :index => true | |
18 | 19 | index :created_at |
19 | 20 | index( |
20 | 21 | [ |
... | ... | @@ -28,7 +29,7 @@ class Notice |
28 | 29 | before_save :sanitize |
29 | 30 | before_destroy :decrease_counter_cache, :remove_cached_attributes_from_problem |
30 | 31 | |
31 | - validates_presence_of :backtrace, :server_environment, :notifier | |
32 | + validates_presence_of :server_environment, :notifier, :backtrace | |
32 | 33 | |
33 | 34 | scope :ordered, order_by(:created_at.asc) |
34 | 35 | scope :reverse_ordered, order_by(:created_at.desc) |
... | ... | @@ -92,15 +93,7 @@ class Notice |
92 | 93 | |
93 | 94 | # Backtrace containing only files from the app itself (ignore gems) |
94 | 95 | def app_backtrace |
95 | - backtrace.select { |l| l && l['file'] && l['file'].include?("[PROJECT_ROOT]") } | |
96 | - end | |
97 | - | |
98 | - def backtrace | |
99 | - # If gems are vendored into project, treat vendored gem dir as [GEM_ROOT] | |
100 | - (read_attribute(:backtrace) || []).map do |line| | |
101 | - # Changes "[PROJECT_ROOT]/rubygems/ruby/1.9.1/gems" to "[GEM_ROOT]/gems" | |
102 | - line.merge 'file' => line['file'].to_s.gsub(/\[PROJECT_ROOT\]\/.*\/ruby\/[0-9.]+\/gems/, '[GEM_ROOT]/gems') | |
103 | - end | |
96 | + backtrace_lines.select { |l| l && l['file'] && l['file'].include?("[PROJECT_ROOT]") } | |
104 | 97 | end |
105 | 98 | |
106 | 99 | protected |
... | ... | @@ -129,8 +122,6 @@ class Notice |
129 | 122 | [:server_environment, :request, :notifier].each do |h| |
130 | 123 | send("#{h}=",sanitize_hash(send(h))) |
131 | 124 | end |
132 | - # Set unknown backtrace files | |
133 | - read_attribute(:backtrace).each{|line| line['file'] = "[unknown source]" if line['file'].blank? } | |
134 | 125 | end |
135 | 126 | |
136 | 127 | def sanitize_hash(h) | ... | ... |
app/views/issue_trackers/fogbugz_body.txt.erb
app/views/issue_trackers/github_issues_body.txt.erb
... | ... | @@ -27,7 +27,7 @@ |
27 | 27 | |
28 | 28 | ## Backtrace ## |
29 | 29 | ``` |
30 | -<% for line in notice.backtrace %><%= line['number'] %>: <%= line['file'].to_s.sub(/^\[PROJECT_ROOT\]/, '') %> -> **<%= line['method'] %>** | |
30 | +<% for line in notice.backtrace_lines %><%= line['number'] %>: <%= line['file'].to_s.sub(/^\[PROJECT_ROOT\]/, '') %> -> **<%= line['method'] %>** | |
31 | 31 | <% end %> |
32 | 32 | ``` |
33 | 33 | ... | ... |
app/views/issue_trackers/lighthouseapp_body.txt.erb
... | ... | @@ -23,7 +23,7 @@ |
23 | 23 | |
24 | 24 | ## Backtrace ## |
25 | 25 | <code> |
26 | - <% for line in notice.backtrace %><%= line['number'] %>: <%= line['file'].to_s.sub(/^\[PROJECT_ROOT\]/, '') %> -> **<%= line['method'] %>** | |
26 | + <% for line in notice.backtrace_lines %><%= line['number'] %>: <%= line['file'].to_s.sub(/^\[PROJECT_ROOT\]/, '') %> -> **<%= line['method'] %>** | |
27 | 27 | <% end %> |
28 | 28 | </code> |
29 | 29 | ... | ... |
app/views/issue_trackers/pivotal_body.txt.erb
... | ... | @@ -12,6 +12,6 @@ See this exception on Errbit: <%= app_problem_url problem.app, problem %> |
12 | 12 | <%= pretty_hash notice.session %> |
13 | 13 | |
14 | 14 | Backtrace: |
15 | - <%= notice.backtrace[0..4].map { |line| "#{line['number']}: #{line['file'].to_s.sub(/^\[PROJECT_ROOT\]/, '')} -> *#{line['method']}*" }.join "\n" %> | |
15 | + <%= notice.backtrace_lines[0..4].map { |line| "#{line['number']}: #{line['file'].to_s.sub(/^\[PROJECT_ROOT\]/, '')} -> *#{line['method']}*" }.join "\n" %> | |
16 | 16 | <% end %> |
17 | 17 | ... | ... |
app/views/issue_trackers/textile_body.txt.erb
... | ... | @@ -32,7 +32,7 @@ h2. Session |
32 | 32 | h2. Backtrace |
33 | 33 | |
34 | 34 | | Line | File | Method | |
35 | -<% for line in notice.backtrace %>| <%= line['number'] %> | <%= line['file'].to_s.sub(/^\[PROJECT_ROOT\]/, '') %> | *<%= line['method'] %>* | | |
35 | +<% for line in notice.backtrace_lines %>| <%= line['number'] %> | <%= line['file'].to_s.sub(/^\[PROJECT_ROOT\]/, '') %> | *<%= line['method'] %>* | | |
36 | 36 | <% end %> |
37 | 37 | |
38 | 38 | h2. Environment | ... | ... |
app/views/mailer/err_notification.html.haml
... | ... | @@ -36,7 +36,7 @@ |
36 | 36 | - if @notice.request['url'].present? |
37 | 37 | = link_to @notice.request['url'], @notice.request['url'] |
38 | 38 | %p.heading BACKTRACE: |
39 | - - @notice.backtrace.map {|l| l ? "#{l['file']}:#{l['number']}" : nil }.compact.each do |line| | |
39 | + - @notice.backtrace_lines.map {|l| l ? "#{l['file']}:#{l['number']}" : nil }.compact.each do |line| | |
40 | 40 | %p.backtrace= line |
41 | 41 | %br |
42 | 42 | ... | ... |
app/views/mailer/err_notification.text.erb
... | ... | @@ -26,7 +26,7 @@ URL: |
26 | 26 | |
27 | 27 | BACKTRACE: |
28 | 28 | |
29 | -<% @notice.backtrace.map {|l| l ? "#{l['file']}:#{l['number']}" : nil }.compact.each do |line| %> | |
29 | +<% @notice.backtrace_lines.map {|l| l ? "#{l['file']}:#{l['number']}" : nil }.compact.each do |line| %> | |
30 | 30 | <%= line %> |
31 | 31 | <% end %> |
32 | 32 | ... | ... |
app/views/notices/_atom_entry.html.haml
app/views/problems/show.html.haml
spec/fabricators/err_fabricator.rb
... | ... | @@ -9,19 +9,19 @@ end |
9 | 9 | Fabricator :notice do |
10 | 10 | err! |
11 | 11 | message 'FooError: Too Much Bar' |
12 | - backtrace { random_backtrace } | |
12 | + backtrace! | |
13 | 13 | server_environment { {'environment-name' => 'production'} } |
14 | 14 | request {{ 'component' => 'foo', 'action' => 'bar' }} |
15 | 15 | notifier {{ 'name' => 'Notifier', 'version' => '1', 'url' => 'http://toad.com' }} |
16 | 16 | end |
17 | 17 | |
18 | -def random_backtrace | |
19 | - backtrace = [] | |
20 | - 99.times {|t| backtrace << { | |
21 | - 'number' => rand(999), | |
22 | - 'file' => "/path/to/file/#{SecureRandom.hex(4)}.rb", | |
23 | - 'method' => ActiveSupport.methods.shuffle.first | |
24 | - }} | |
25 | - backtrace | |
18 | +Fabricator :backtrace do | |
19 | + fingerprint "fingerprint" | |
20 | + lines(:count => 99) { Fabricate.build(:backtrace_line) } | |
26 | 21 | end |
27 | 22 | |
23 | +Fabricator :backtrace_line do | |
24 | + number { rand(999) } | |
25 | + file { "/path/to/file/#{SecureRandom.hex(4)}.rb" } | |
26 | + method(:method) { ActiveSupport.methods.shuffle.first } | |
27 | +end | ... | ... |
spec/models/app_spec.rb
... | ... | @@ -200,8 +200,8 @@ describe App do |
200 | 200 | |
201 | 201 | it 'captures the backtrace' do |
202 | 202 | @notice = App.report_error!(@xml) |
203 | - @notice.backtrace.size.should == 73 | |
204 | - @notice.backtrace.last['file'].should == '[GEM_ROOT]/bin/rake' | |
203 | + @notice.backtrace_lines.size.should == 73 | |
204 | + @notice.backtrace_lines.last['file'].should == '[GEM_ROOT]/bin/rake' | |
205 | 205 | end |
206 | 206 | |
207 | 207 | it 'captures the server_environment' do |
... | ... | @@ -228,7 +228,7 @@ describe App do |
228 | 228 | it "should handle params with only a single line of backtrace" do |
229 | 229 | xml = Rails.root.join('spec','fixtures','hoptoad_test_notice_with_one_line_of_backtrace.xml').read |
230 | 230 | lambda { @notice = App.report_error!(xml) }.should_not raise_error |
231 | - @notice.backtrace.length.should == 1 | |
231 | + @notice.backtrace_lines.length.should == 1 | |
232 | 232 | end |
233 | 233 | |
234 | 234 | it 'captures the current_user' do |
... | ... | @@ -238,7 +238,7 @@ describe App do |
238 | 238 | @notice.current_user['email'].should == 'mr.bean@example.com' |
239 | 239 | @notice.current_user['username'].should == 'mrbean' |
240 | 240 | end |
241 | - | |
241 | + | |
242 | 242 | end |
243 | 243 | |
244 | 244 | ... | ... |
... | ... | @@ -0,0 +1,14 @@ |
1 | +require 'spec_helper' | |
2 | + | |
3 | +describe BacktraceLineNormalizer do | |
4 | + subject { described_class.new(raw_line).call } | |
5 | + | |
6 | + describe "sanitize file" do | |
7 | + let(:raw_line) { { 'number' => rand(999), 'file' => nil, 'method' => ActiveSupport.methods.shuffle.first.to_s } } | |
8 | + | |
9 | + it "should replace nil file with [unknown source]" do | |
10 | + subject['file'].should == "[unknown source]" | |
11 | + end | |
12 | + | |
13 | + end | |
14 | +end | ... | ... |
... | ... | @@ -0,0 +1,46 @@ |
1 | +require 'spec_helper' | |
2 | + | |
3 | +describe Backtrace do | |
4 | + subject { described_class.new } | |
5 | + | |
6 | + its(:fingerprint) { should be_present } | |
7 | + | |
8 | + describe "#similar" do | |
9 | + context "no similar backtrace" do | |
10 | + its(:similar) { should be_nil } | |
11 | + end | |
12 | + | |
13 | + context "similar backtrace exist" do | |
14 | + let!(:similar_backtrace) { Fabricate(:backtrace, :fingerprint => fingerprint) } | |
15 | + let(:fingerprint) { "fingerprint" } | |
16 | + | |
17 | + before { subject.stub(:fingerprint => fingerprint) } | |
18 | + | |
19 | + its(:similar) { should == similar_backtrace } | |
20 | + end | |
21 | + end | |
22 | + | |
23 | + describe "find_or_create" do | |
24 | + subject { described_class.find_or_create(attributes) } | |
25 | + let(:attributes) { mock :attributes } | |
26 | + let(:backtrace) { mock :backtrace } | |
27 | + | |
28 | + before { described_class.stub(:new => backtrace) } | |
29 | + | |
30 | + context "no similar backtrace" do | |
31 | + before { backtrace.stub(:similar => nil) } | |
32 | + it "create new backtrace" do | |
33 | + described_class.should_receive(:create).with(attributes) | |
34 | + | |
35 | + described_class.find_or_create(attributes) | |
36 | + end | |
37 | + end | |
38 | + | |
39 | + context "similar backtrace exist" do | |
40 | + let(:similar_backtrace) { mock :similar_backtrace } | |
41 | + before { backtrace.stub(:similar => similar_backtrace) } | |
42 | + | |
43 | + it { should == similar_backtrace } | |
44 | + end | |
45 | + end | |
46 | +end | ... | ... |
spec/models/notice_observer_spec.rb
... | ... | @@ -46,6 +46,7 @@ describe NoticeObserver do |
46 | 46 | describe "should send a notification if a notification service is configured" do |
47 | 47 | let(:app) { Fabricate(:app, :email_at_notices => [1], :notification_service => Fabricate(:campfire_notification_service))} |
48 | 48 | let(:err) { Fabricate(:err, :problem => Fabricate(:problem, :app => app, :notices_count => 100)) } |
49 | + let(:backtrace) { Fabricate(:backtrace) } | |
49 | 50 | |
50 | 51 | before do |
51 | 52 | Errbit::Config.per_app_email_at_notices = true |
... | ... | @@ -59,13 +60,14 @@ describe NoticeObserver do |
59 | 60 | app.notification_service.should_receive(:create_notification) |
60 | 61 | |
61 | 62 | Notice.create!(:err => err, :message => 'FooError: Too Much Bar', :server_environment => {'environment-name' => 'production'}, |
62 | - :backtrace => [{ :error => 'Le Broken' }], :notifier => { 'name' => 'Notifier', 'version' => '1', 'url' => 'http://toad.com' }) | |
63 | + :backtrace => backtrace, :notifier => { 'name' => 'Notifier', 'version' => '1', 'url' => 'http://toad.com' }) | |
63 | 64 | end |
64 | 65 | end |
65 | 66 | |
66 | 67 | describe "should not send a notification if a notification service is not configured" do |
67 | 68 | let(:app) { Fabricate(:app, :email_at_notices => [1], :notification_service => Fabricate(:notification_service))} |
68 | 69 | let(:err) { Fabricate(:err, :problem => Fabricate(:problem, :app => app, :notices_count => 100)) } |
70 | + let(:backtrace) { Fabricate(:backtrace) } | |
69 | 71 | |
70 | 72 | before do |
71 | 73 | Errbit::Config.per_app_email_at_notices = true |
... | ... | @@ -79,7 +81,7 @@ describe NoticeObserver do |
79 | 81 | app.notification_service.should_not_receive(:create_notification) |
80 | 82 | |
81 | 83 | Notice.create!(:err => err, :message => 'FooError: Too Much Bar', :server_environment => {'environment-name' => 'production'}, |
82 | - :backtrace => [{ :error => 'Le Broken' }], :notifier => { 'name' => 'Notifier', 'version' => '1', 'url' => 'http://toad.com' }) | |
84 | + :backtrace => backtrace, :notifier => { 'name' => 'Notifier', 'version' => '1', 'url' => 'http://toad.com' }) | |
83 | 85 | end |
84 | 86 | end |
85 | 87 | ... | ... |
spec/views/notices/_backtrace.html.haml_spec.rb
... | ... | @@ -1,18 +0,0 @@ |
1 | -require 'spec_helper' | |
2 | - | |
3 | -describe "notices/_backtrace.html.haml" do | |
4 | - describe 'missing file in backtrace' do | |
5 | - let(:notice) do | |
6 | - backtrace = { 'number' => rand(999), 'file' => nil, 'method' => ActiveSupport.methods.shuffle.first } | |
7 | - Fabricate(:notice, :backtrace => [backtrace]) | |
8 | - end | |
9 | - | |
10 | - it "should replace nil file with [unknown source]" do | |
11 | - assign :app, notice.err.app | |
12 | - | |
13 | - render "notices/backtrace", :lines => notice.backtrace | |
14 | - rendered.should match(/\[unknown source\]/) | |
15 | - end | |
16 | - end | |
17 | -end | |
18 | - |