Commit 88b72813f078b78ff467ea13d0a7f0b94be04b2c
Exists in
master
and in
1 other branch
Merge pull request #271 from martinciu/backtrace-extraction
Backtrace extraction
Showing
26 changed files
with
297 additions
and
148 deletions
Show diff stats
... | ... | @@ -0,0 +1,16 @@ |
1 | +module BacktraceHelper | |
2 | + # Group lines into sections of in-app files and external files | |
3 | + # (An implementation of Enumerable#chunk so we don't break 1.8.7 support.) | |
4 | + def grouped_lines(lines) | |
5 | + line_groups = [] | |
6 | + lines.each do |line| | |
7 | + in_app = line.in_app? | |
8 | + if line_groups.last && line_groups.last[0] == in_app | |
9 | + line_groups.last[1] << line | |
10 | + else | |
11 | + line_groups << [in_app, [line]] | |
12 | + end | |
13 | + end | |
14 | + line_groups | |
15 | + end | |
16 | +end | ... | ... |
... | ... | @@ -0,0 +1,38 @@ |
1 | +module BacktraceLineHelper | |
2 | + def link_to_source_file(line, &block) | |
3 | + text = capture_haml(&block) | |
4 | + line.in_app? ? link_to_in_app_source_file(line, text) : link_to_external_source_file(text) | |
5 | + end | |
6 | + | |
7 | + private | |
8 | + def link_to_in_app_source_file(line, text) | |
9 | + link_to_repo_source_file(line, text) || link_to_issue_tracker_file(line, text) | |
10 | + end | |
11 | + | |
12 | + def link_to_repo_source_file(line, text) | |
13 | + link_to_github(line, text) || link_to_bitbucket(line, text) | |
14 | + end | |
15 | + | |
16 | + def link_to_external_source_file(text) | |
17 | + text | |
18 | + end | |
19 | + | |
20 | + def link_to_github(line, text = nil) | |
21 | + return unless line.app.github_repo? | |
22 | + href = "%s#L%s" % [line.app.github_url_to_file(line.file), line.number] | |
23 | + link_to(text || line.file_name, href, :target => '_blank') | |
24 | + end | |
25 | + | |
26 | + def link_to_bitbucket(line, text = nil) | |
27 | + return unless line.app.bitbucket_repo? | |
28 | + href = "%s#cl-%s" % [line.app.bitbucket_url_to_file(line.file), line.number] | |
29 | + link_to(text || line.file_name, href, :target => '_blank') | |
30 | + end | |
31 | + | |
32 | + def link_to_issue_tracker_file(line, text = nil) | |
33 | + return unless line.app.issue_tracker && line.app.issue_tracker.respond_to?(:url_to_file) | |
34 | + href = line.app.issue_tracker.url_to_file(line.file, line.number) | |
35 | + link_to(text || line.file_name, href, :target => '_blank') | |
36 | + end | |
37 | + | |
38 | +end | ... | ... |
app/helpers/notices_helper.rb
1 | 1 | # encoding: utf-8 |
2 | 2 | module NoticesHelper |
3 | - def in_app_backtrace_line?(line) | |
4 | - !!(line['file'] =~ %r{^\[PROJECT_ROOT\]/(?!(vendor))}) | |
5 | - end | |
6 | - | |
7 | 3 | def notice_atom_summary(notice) |
8 | 4 | render "notices/atom_entry.html.haml", :notice => notice |
9 | 5 | end |
10 | - | |
11 | - def link_to_source_file(app, line, &block) | |
12 | - text = capture_haml(&block) | |
13 | - if in_app_backtrace_line?(line) | |
14 | - return link_to_github(app, line, text) if app.github_repo? | |
15 | - return link_to_bitbucket(app, line, text) if app.bitbucket_repo? | |
16 | - if app.issue_tracker && app.issue_tracker.respond_to?(:url_to_file) | |
17 | - # Return link to file on tracker if issue tracker supports this | |
18 | - return link_to_issue_tracker_file(app, line, text) | |
19 | - end | |
20 | - end | |
21 | - text | |
22 | - end | |
23 | - | |
24 | - def filepath_parts(file) | |
25 | - [file.split('/').last, file.gsub('[PROJECT_ROOT]', '')] | |
26 | - end | |
27 | - | |
28 | - def link_to_github(app, line, text = nil) | |
29 | - file_name, file_path = filepath_parts(line['file']) | |
30 | - href = "%s#L%s" % [app.github_url_to_file(file_path), line['number']] | |
31 | - link_to(text || file_name, href, :target => '_blank') | |
32 | - end | |
33 | - | |
34 | - def link_to_bitbucket(app, line, text = nil) | |
35 | - file_name, file_path = filepath_parts(line['file']) | |
36 | - href = "%s#cl-%s" % [app.bitbucket_url_to_file(file_path), line['number']] | |
37 | - link_to(text || file_name, href, :target => '_blank') | |
38 | - end | |
39 | - | |
40 | - def link_to_issue_tracker_file(app, line, text = nil) | |
41 | - file_name, file_path = filepath_parts(line['file']) | |
42 | - href = app.issue_tracker.url_to_file(file_path, line['number']) | |
43 | - link_to(text || file_name, href, :target => '_blank') | |
44 | - end | |
45 | - | |
46 | - # Group lines into sections of in-app files and external files | |
47 | - # (An implementation of Enumerable#chunk so we don't break 1.8.7 support.) | |
48 | - def grouped_lines(lines) | |
49 | - line_groups = [] | |
50 | - lines.each do |line| | |
51 | - in_app = in_app_backtrace_line?(line) | |
52 | - if line_groups.last && line_groups.last[0] == in_app | |
53 | - line_groups.last[1] << line | |
54 | - else | |
55 | - line_groups << [in_app, [line]] | |
56 | - end | |
57 | - end | |
58 | - line_groups | |
59 | - end | |
60 | - | |
61 | - def path_for_backtrace_line(line) | |
62 | - path = File.dirname(line['file']) | |
63 | - return '' if path == '.' | |
64 | - # Remove [PROJECT_ROOT] | |
65 | - path.gsub!('[PROJECT_ROOT]/', '') | |
66 | - # Make gem name bold if starts with [GEM_ROOT]/gems | |
67 | - path.gsub!(/\[GEM_ROOT\]\/gems\/([^\/]+)/, "<strong>\\1</strong>") | |
68 | - (path << '/').html_safe | |
69 | - end | |
70 | - | |
71 | - def file_for_backtrace_line(line) | |
72 | - file = File.basename(line['file']) | |
73 | - end | |
74 | 6 | end |
75 | 7 | ... | ... |
... | ... | @@ -0,0 +1,36 @@ |
1 | +class Backtrace | |
2 | + include Mongoid::Document | |
3 | + include Mongoid::Timestamps | |
4 | + | |
5 | + field :fingerprint | |
6 | + index :fingerprint | |
7 | + | |
8 | + has_many :notices | |
9 | + has_one :notice | |
10 | + | |
11 | + embeds_many :lines, :class_name => "BacktraceLine" | |
12 | + | |
13 | + after_initialize :generate_fingerprint | |
14 | + | |
15 | + delegate :app, :to => :notice | |
16 | + | |
17 | + def self.find_or_create(attributes = {}) | |
18 | + new(attributes).similar || create(attributes) | |
19 | + end | |
20 | + | |
21 | + def similar | |
22 | + Backtrace.first(:conditions => { :fingerprint => fingerprint } ) | |
23 | + end | |
24 | + | |
25 | + def raw=(raw) | |
26 | + raw.each do |raw_line| | |
27 | + lines << BacktraceLine.new(BacktraceLineNormalizer.new(raw_line).call) | |
28 | + end | |
29 | + end | |
30 | + | |
31 | + private | |
32 | + def generate_fingerprint | |
33 | + self.fingerprint = Digest::SHA1.hexdigest(lines.map(&:to_s).join) | |
34 | + end | |
35 | + | |
36 | +end | ... | ... |
... | ... | @@ -0,0 +1,42 @@ |
1 | +class BacktraceLine | |
2 | + include Mongoid::Document | |
3 | + IN_APP_PATH = %r{^\[PROJECT_ROOT\]\/(?!(vendor))} | |
4 | + GEMS_PATH = %r{\[GEM_ROOT\]\/gems\/([^\/]+)} | |
5 | + | |
6 | + field :number, :type => Integer | |
7 | + field :file | |
8 | + field :method | |
9 | + | |
10 | + embedded_in :backtrace | |
11 | + | |
12 | + scope :in_app, where(:file => IN_APP_PATH) | |
13 | + | |
14 | + delegate :app, :to => :backtrace | |
15 | + | |
16 | + def to_s | |
17 | + "#{file}:#{number}" | |
18 | + end | |
19 | + | |
20 | + def in_app? | |
21 | + !!(file =~ IN_APP_PATH) | |
22 | + end | |
23 | + | |
24 | + def path | |
25 | + File.dirname(file).gsub(/^\.$/, '') + "/" | |
26 | + end | |
27 | + | |
28 | + def file_relative | |
29 | + file.to_s.sub(IN_APP_PATH, '') | |
30 | + end | |
31 | + | |
32 | + def file_name | |
33 | + File.basename file | |
34 | + end | |
35 | + | |
36 | + def decorated_path | |
37 | + path.sub(BacktraceLine::IN_APP_PATH, ''). | |
38 | + sub(BacktraceLine::GEMS_PATH, "<strong>\\1</strong>") | |
39 | + end | |
40 | + | |
41 | +end | |
42 | + | ... | ... |
... | ... | @@ -0,0 +1,19 @@ |
1 | +class BacktraceLineNormalizer | |
2 | + def initialize(raw_line) | |
3 | + @raw_line = raw_line | |
4 | + end | |
5 | + | |
6 | + def call | |
7 | + @raw_line.merge 'file' => normalized_file, 'method' => normalized_method | |
8 | + end | |
9 | + | |
10 | + private | |
11 | + def normalized_file | |
12 | + @raw_line['file'].blank? ? "[unknown source]" : @raw_line['file'].to_s.gsub(/\[PROJECT_ROOT\]\/.*\/ruby\/[0-9.]+\/gems/, '[GEM_ROOT]/gems') | |
13 | + end | |
14 | + | |
15 | + def normalized_method | |
16 | + @raw_line['method'].gsub(/[0-9_]{10,}+/, "__FRAGMENT__") | |
17 | + end | |
18 | + | |
19 | +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.id, | |
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,17 @@ 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 | + delegate :app, :to => :err | |
16 | 17 | |
17 | 18 | belongs_to :err |
19 | + belongs_to :backtrace, :index => true | |
18 | 20 | index :created_at |
19 | 21 | index( |
20 | 22 | [ |
... | ... | @@ -90,17 +92,8 @@ class Notice |
90 | 92 | request['session'] || {} |
91 | 93 | end |
92 | 94 | |
93 | - # Backtrace containing only files from the app itself (ignore gems) | |
94 | - 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 | |
95 | + def in_app_backtrace_lines | |
96 | + backtrace_lines.in_app | |
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
... | ... | @@ -19,8 +19,8 @@ |
19 | 19 | <%= pretty_hash(notice.session) %> |
20 | 20 | |
21 | 21 | Backtrace |
22 | - <% for line in notice.backtrace %> | |
23 | - <%= line['number'] %>: <%= line['file'].to_s.sub(/^\[PROJECT_ROOT\]/, '') %> | |
22 | + <% for line in notice.backtrace_lines %> | |
23 | + <%= line.number %>: <%= line.file_relative %> | |
24 | 24 | <% end %> |
25 | 25 | |
26 | 26 | Environment | ... | ... |
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_relative %> -> **<%= 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_relative %> -> **<%= 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_relative} -> *#{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_relative %> | *<%= line.method %>* | | |
36 | 36 | <% end %> |
37 | 37 | |
38 | 38 | h2. Environment | ... | ... |
app/views/mailer/err_notification.html.haml
... | ... | @@ -27,16 +27,15 @@ |
27 | 27 | %p.heading WHERE: |
28 | 28 | %p.monospace |
29 | 29 | = @notice.where |
30 | - - if (app_backtrace = @notice.app_backtrace).any? | |
31 | - - app_backtrace.map {|l| "#{l['file']}:#{l['number']}" }.each do |line| | |
32 | - %p.backtrace= line | |
30 | + - @notice.in_app_backtrace_lines.each do |line| | |
31 | + %p.backtrace= line | |
33 | 32 | %br |
34 | 33 | %p.heading URL: |
35 | 34 | %p.monospace |
36 | 35 | - if @notice.request['url'].present? |
37 | 36 | = link_to @notice.request['url'], @notice.request['url'] |
38 | 37 | %p.heading BACKTRACE: |
39 | - - @notice.backtrace.map {|l| l ? "#{l['file']}:#{l['number']}" : nil }.compact.each do |line| | |
38 | + - @notice.backtrace_lines.each do |line| | |
40 | 39 | %p.backtrace= line |
41 | 40 | %br |
42 | 41 | ... | ... |
app/views/mailer/err_notification.text.erb
... | ... | @@ -14,7 +14,7 @@ WHERE: |
14 | 14 | |
15 | 15 | <%= @notice.where %> |
16 | 16 | |
17 | -<% @notice.app_backtrace.map {|l| "#{l['file']}:#{l['number']}" }.each do |line| %> | |
17 | +<% @notice.in_app_backtrace_lines.each do |line| %> | |
18 | 18 | <%= line %> |
19 | 19 | <% end %> |
20 | 20 | |
... | ... | @@ -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.each do |line| %> | |
30 | 30 | <%= line %> |
31 | 31 | <% end %> |
32 | 32 | ... | ... |
app/views/notices/_atom_entry.html.haml
... | ... | @@ -22,13 +22,13 @@ |
22 | 22 | |
23 | 23 | %h3 Backtrace |
24 | 24 | %table |
25 | - - for line in notice.backtrace | |
25 | + - for line in notice.backtrace_lines | |
26 | 26 | %tr |
27 | 27 | %td |
28 | - = "#{line['number']}:" | |
28 | + = "#{line.number}:" | |
29 | 29 | |
30 | 30 | %td |
31 | - = raw "#{h line['file'].sub(/^\[PROJECT_ROOT\]/, '')} -> #{content_tag :strong, h(line['method'])}" | |
31 | + = raw "#{h line.file_relative} -> #{content_tag :strong, h(line.method)}" | |
32 | 32 | |
33 | 33 | %h3 Environment |
34 | 34 | %table | ... | ... |
app/views/notices/_backtrace_line.html.haml
1 | -%tr{:class => defined?(row_class) ? row_class : nil} | |
2 | - %td.line{:class => (in_app_backtrace_line?(line) ? 'in-app' : nil)} | |
3 | - = link_to_source_file(@app, line) do | |
4 | - %span.path>= path_for_backtrace_line(line) | |
5 | - %span.file>= file_for_backtrace_line(line) | |
6 | - - if line['number'].present? | |
7 | - %span.number>= ":#{line['number']}" | |
1 | +%tr{:class => defined?(row_class) && row_class} | |
2 | + %td.line{:class => line.in_app? && 'in-app' } | |
3 | + = link_to_source_file(line) do | |
4 | + %span.path>=raw line.decorated_path | |
5 | + %span.file>= line.file_name | |
6 | + - if line.number.present? | |
7 | + %span.number>= ":#{line.number}" | |
8 | 8 | → |
9 | - %span.method= line['method'] | |
9 | + %span.method= line.method | ... | ... |
app/views/problems/show.html.haml
... | ... | @@ -0,0 +1,15 @@ |
1 | +class ExtractBacktraces < Mongoid::Migration | |
2 | + def self.up | |
3 | + say "It could take long time (hours if you have many Notices)" | |
4 | + Notice.unscoped.all.each do |notice| | |
5 | + backtrace = Backtrace.find_or_create(:raw => notice['backtrace']) | |
6 | + notice.backtrace = backtrace | |
7 | + notice['backtrace'] = nil | |
8 | + notice.save! | |
9 | + end | |
10 | + say "run `db.repairDatabase()` (in mongodb console) to recover deleted space" | |
11 | + end | |
12 | + | |
13 | + def self.down | |
14 | + end | |
15 | +end | ... | ... |
db/migrate/20121005142110_regenerate_err_fingerprints.rb
0 → 100644
... | ... | @@ -0,0 +1,19 @@ |
1 | +class RegenerateErrFingerprints < Mongoid::Migration | |
2 | + def self.up | |
3 | + Err.all.each do |err| | |
4 | + fingerprint_source = { | |
5 | + :backtrace => err.notices.first.backtrace_id, | |
6 | + :error_class => err.error_class, | |
7 | + :component => err.component, | |
8 | + :action => err.action, | |
9 | + :environment => err.environment, | |
10 | + :api_key => err.app.api_key | |
11 | + } | |
12 | + fingerprint = Digest::SHA1.hexdigest(fingerprint_source.to_s) | |
13 | + err.update_attribute(:fingerprint, fingerprint) | |
14 | + end | |
15 | + end | |
16 | + | |
17 | + def self.down | |
18 | + end | |
19 | +end | ... | ... |
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 | - |