Commit 88b72813f078b78ff467ea13d0a7f0b94be04b2c

Authored by Nathan Broadbent
2 parents f1f109f7 cfc8f4f9
Exists in master and in 1 other branch production

Merge pull request #271 from martinciu/backtrace-extraction

Backtrace extraction
app/helpers/backtrace_helper.rb 0 → 100644
... ... @@ -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
... ...
app/helpers/backtrace_line_helper.rb 0 → 100644
... ... @@ -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  
... ...
app/models/backtrace.rb 0 → 100644
... ... @@ -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
... ...
app/models/backtrace_line.rb 0 → 100644
... ... @@ -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 +
... ...
app/models/backtrace_line_normalizer.rb 0 → 100644
... ... @@ -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 &#39;digest/sha1&#39;
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: &lt;%= app_problem_url problem.app, problem %&gt;
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 &nbsp;&nbsp;
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 &rarr;
9   - %span.method= line['method']
  9 + %span.method= line.method
... ...
app/views/problems/show.html.haml
... ... @@ -68,7 +68,7 @@
68 68  
69 69 #backtrace
70 70 %h3 Backtrace
71   - = render 'notices/backtrace', :lines => @notice.backtrace
  71 + = render 'notices/backtrace', :lines => @notice.backtrace_lines
72 72  
73 73 - if @notice.user_attributes.present?
74 74 #user_attributes
... ...
db/migrate/20121003223358_extract_backtraces.rb 0 → 100644
... ... @@ -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  
... ...
spec/models/backtrace_line_normalizer_spec.rb 0 → 100644
... ... @@ -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
... ...
spec/models/backtrace_spec.rb 0 → 100644
... ... @@ -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   -