Commit d015914f11243a333d1b3717d03711e5bfaec92f

Authored by Stephen Crosby
2 parents b03a0e45 bff20d76
Exists in master and in 1 other branch production

Merge remote-tracking branch 'errbit/master' into update_deployment_config

Conflicts:
	config/deploy.example.rb
.env.default
... ... @@ -18,5 +18,4 @@ MONGO_URL='mongodb://localhost'
18 18 GITHUB_URL='https://github.com'
19 19 GITHUB_AUTHENTICATION=true
20 20 GITHUB_ACCESS_SCOPE='[repo]'
21   -EMAIL_DELIVERY_METHOD=sendmail
22 21 DEVISE_MODULES='[database_authenticatable,recoverable,rememberable,trackable,validatable,omniauthable]'
... ...
Gemfile.lock
1 1 GIT
2 2 remote: git://github.com/errbit/errbit_github_plugin.git
3   - revision: 5900200e6d460fe94feaea3e59824437d54f1029
  3 + revision: 7eb48d540b94d2fa4b8f9805ec0148322ae2bffe
4 4 specs:
5 5 errbit_github_plugin (0.1.0)
6 6 errbit_plugin
... ...
app/controllers/users/omniauth_callbacks_controller.rb
... ... @@ -8,7 +8,7 @@ class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
8 8 # See if they are a member of the organization that we have access for
9 9 # If they are, automatically create an account
10 10 client = Octokit::Client.new(access_token: github_token)
11   - org_ids = client.organizations.map { |org| org.id.to_s }
  11 + org_ids = client.organizations.map { |org| org.id }
12 12 if org_ids.include?(github_org_id)
13 13 github_user = User.create(name: env["omniauth.auth"].extra.raw_info.name, email: env["omniauth.auth"].extra.raw_info.email)
14 14 end
... ...
app/models/issue.rb
... ... @@ -2,15 +2,21 @@ class Issue
2 2 include ActiveModel::Model
3 3 attr_accessor :problem, :user, :title, :body
4 4  
5   - def intialize(problem: nil, user: nil, title: nil, body: nil)
6   - @problem, @user, @title, @body = problem, user, title, body
7   - end
8   -
9 5 def issue_tracker
10 6 problem.app.issue_tracker
11 7 end
12 8  
13 9 def save
  10 + unless body
  11 + errors.add :base, "The issue has no body"
  12 + return false
  13 + end
  14 +
  15 + unless title
  16 + errors.add :base, "The issue has no title"
  17 + return false
  18 + end
  19 +
14 20 if issue_tracker
15 21 issue_tracker.tracker.errors.each do |k, err|
16 22 errors.add k, err
... ...
app/models/issue_tracker.rb
... ... @@ -13,6 +13,7 @@ class IssueTracker
13 13 @tracker ||=
14 14 begin
15 15 klass = ErrbitPlugin::Registry.issue_trackers[self.type_tracker] || ErrbitPlugin::NoneIssueTracker
  16 + # TODO: we need to find out a better way to pass those config to the issue tracker
16 17 klass.new(options.merge(github_repo: app.github_repo, bitbucket_repo: app.bitbucket_repo))
17 18 end
18 19 end
... ...
app/views/issue_trackers/bitbucket_issues_body.txt.erb
... ... @@ -1,58 +0,0 @@
1   -[[<%= app_problem_url problem.app, problem %>| [See this exception on Errbit]]]
2   -
3   -----
4   -
5   -<% if notice = problem.notices.first %>
6   - <%= notice.message %>
7   -
8   -----
9   -
10   - == Summary ==
11   - <% if notice.request['url'].present? %>
12   - === URL ===
13   - [[<%= notice.request['url'] %>]]
14   - <% end %>
15   -
16   -----
17   -
18   - === Where ===
19   - <%= notice.where %>
20   -
21   -----
22   -
23   - === Occured ===
24   - <%= notice.created_at.to_s(:micro) %>
25   -
26   -----
27   -
28   - === Similar ===
29   - <%= (notice.problem.notices_count - 1).to_s %>
30   -
31   -----
32   -
33   - == Params ==
34   -{{{
35   -<%= pretty_hash(notice.params) %>
36   -}}}
37   -
38   -----
39   -
40   - == Session ==
41   -{{{
42   -<%= pretty_hash(notice.session) %>
43   -}}}
44   -
45   -----
46   -
47   - == Backtrace ==
48   - <% notice.backtrace_lines.each do |line| %>| <%= line['number'] %>: | <%= line['file'].to_s.sub(/^\[PROJECT_ROOT\]/, '') %> -> **<%= line['method'] %>** |
49   - <% end %>
50   -
51   -----
52   -
53   - == Environment ==
54   - <% for key, val in notice.env_vars %>
55   - | <%= key %>: | <%= val %> |
56   - <% end %>
57   -<% end %>
58   -
app/views/issue_trackers/fogbugz_body.txt.erb
... ... @@ -1,31 +0,0 @@
1   -"See this exception on Errbit": <%= app_problem_url(problem.app, problem) %>
2   -<% if notice = problem.notices.first %>
3   - <%= notice.message %>
4   -
5   - Summary
6   - - Where
7   - <%= notice.where %>
8   -
9   - - Occured
10   - <%= notice.created_at.to_s(:micro) %>
11   -
12   - - Similar
13   - <%= (notice.problem.notices_count - 1).to_s %>
14   -
15   - Params
16   - <%= pretty_hash(notice.params) %>
17   -
18   - Session
19   - <%= pretty_hash(notice.session) %>
20   -
21   - Backtrace
22   - <% notice.backtrace_lines.each do |line| %>
23   - <%= line.number %>: <%= line.file_relative %>
24   - <% end %>
25   -
26   - Environment
27   - <% for key, val in notice.env_vars %>
28   - <%= key %>: <%= val %>
29   - <% end %>
30   -<% end %>
31   -
app/views/issue_trackers/github_issues_body.txt.erb
... ... @@ -1,45 +0,0 @@
1   -[See this exception on Errbit](<%= app_problem_url problem.app, problem %> "See this exception on Errbit")
2   -<% if notice = problem.notices.first %>
3   -# <%= notice.message %> #
4   -## Summary ##
5   -<% if notice.request['url'].present? %>
6   - ### URL ###
7   - [<%= notice.request['url'] %>](<%= notice.request['url'] %>)"
8   -<% end %>
9   -### Where ###
10   -<%= notice.where %>
11   -
12   -### Occured ###
13   -<%= notice.created_at.to_s(:micro) %>
14   -
15   -### Similar ###
16   -<%= (notice.problem.notices_count - 1).to_s %>
17   -
18   -## Params ##
19   -```
20   -<%= pretty_hash(notice.params) %>
21   -```
22   -
23   -## Session ##
24   -```
25   -<%= pretty_hash(notice.session) %>
26   -```
27   -
28   -## Backtrace ##
29   -```
30   -<% notice.backtrace_lines.each do |line| %><%= line.number %>: <%= line.file_relative %> -> **<%= line.method %>**
31   -<% end %>
32   -```
33   -
34   -## Environment ##
35   -
36   -<table>
37   -<% for key, val in notice.env_vars %>
38   - <tr>
39   - <td><%= key %>:</td>
40   - <td><%= val %></td>
41   - </tr>
42   -<% end %>
43   -</table>
44   -<% end %>
45   -
app/views/issue_trackers/gitlab_body.txt.erb
... ... @@ -1,29 +0,0 @@
1   -<% if notice = problem.notices.first %>
2   -## Params ##
3   -```
4   -<%= pretty_hash(notice.params) %>
5   -```
6   -
7   -## Session ##
8   -```
9   -<%= pretty_hash(notice.session) %>
10   -```
11   -
12   -## Backtrace ##
13   -```
14   -<% notice.backtrace_lines.each do |line| %><%= line.number %>: <%= line.file_relative %> -> **<%= line.method %>**
15   -<% end %>
16   -```
17   -
18   -## Environment ##
19   -
20   -<table>
21   -<% for key, val in notice.env_vars %>
22   - <tr>
23   - <td><%= key %>:</td>
24   - <td><%= val %></td>
25   - </tr>
26   -<% end %>
27   -</table>
28   -<% end %>
29   -
app/views/issue_trackers/gitlab_summary.txt.erb
... ... @@ -1,17 +0,0 @@
1   -[See this exception on Errbit](<%= app_problem_url problem.app, problem %> "See this exception on Errbit")
2   -<% if notice = problem.notices.first %>
3   -# <%= notice.message %> #
4   -## Summary ##
5   -<% if notice.request['url'].present? %>
6   - ### URL ###
7   - [<%= notice.request['url'] %>](<%= notice.request['url'] %>)"
8   -<% end %>
9   -### Where ###
10   -<%= notice.where %>
11   -
12   -### Occured ###
13   -<%= notice.created_at.to_s(:micro) %>
14   -
15   -### Similar ###
16   -<%= (notice.problem.notices_count - 1).to_s %>
17   -<% end %>
18 0 \ No newline at end of file
app/views/issue_trackers/jira_body.txt.erb
... ... @@ -1,17 +0,0 @@
1   -<% if notice = problem.notices.first %>
2   -h2. Summary
3   -<% if notice.request['url'].present? %>
4   -h3. URL
5   -
6   -"<%= notice.request['url'] %>":<%= notice.request['url'] %>
7   -<% end %>
8   -h3. Where
9   -
10   -<%= notice.where %>
11   -
12   -h3. When
13   -
14   -<%= notice.created_at.to_s(:micro) %>
15   -
16   -"More Details on Errbit":<%= app_problem_url problem.app, problem %>
17   -<% end %>
18 0 \ No newline at end of file
app/views/issue_trackers/lighthouseapp_body.txt.erb
... ... @@ -1,35 +0,0 @@
1   -[See this exception on Errbit](<%= app_problem_url problem.app, problem %> "See this exception on Errbit")
2   -<% if notice = problem.notices.first %>
3   - # <%= notice.message %> #
4   - ## Summary ##
5   - <% if notice.request['url'].present? %>
6   - ### URL ###
7   - [<%= notice.request['url'] %>](<%= notice.request['url'] %>)"
8   - <% end %>
9   - ### Where ###
10   - <%= notice.where %>
11   -
12   - ### Occured ###
13   - <%= notice.created_at.to_s(:micro) %>
14   -
15   - ### Similar ###
16   - <%= (notice.problem.notices_count - 1).to_s %>
17   -
18   - ## Params ##
19   - <code><%= pretty_hash(notice.params) %></code>
20   -
21   - ## Session ##
22   - <code><%= pretty_hash(notice.session) %></code>
23   -
24   - ## Backtrace ##
25   - <code>
26   - <% notice.backtrace_lines.each do |line| %><%= line.number %>: <%= line.file_relative %> -> **<%= line.method %>**
27   - <% end %>
28   - </code>
29   -
30   - ## Environment ##
31   - <% for key, val in notice.env_vars %>
32   - <%= key %>: <%= val %>
33   - <% end %>
34   -<% end %>
35   -
app/views/issue_trackers/pivotal_body.txt.erb
... ... @@ -1,17 +0,0 @@
1   -See this exception on Errbit: <%= app_problem_url problem.app, problem %>
2   -<% if notice = problem.notices.first %>
3   - <% if notice.request['url'].present? %>URL: <%= notice.request['url'] %><% end %>
4   - Where: <%= notice.where %>
5   - Occurred: <%= notice.created_at.to_s :micro %>
6   - Similar: <%= (notice.problem.notices_count - 1).to_s %>
7   -
8   - Params:
9   - <%= pretty_hash notice.params %>
10   -
11   - Session:
12   - <%= pretty_hash notice.session %>
13   -
14   - Backtrace:
15   - <%= notice.backtrace_lines[0..4].map { |line| "#{line.number}: #{line.file_relative} -> *#{line.method}*" }.join "\n" %>
16   -<% end %>
17   -
app/views/issue_trackers/redmine_body.txt.erb
... ... @@ -1,17 +0,0 @@
1   -<% if notice = problem.notices.first %>
2   -h2. Summary
3   -<% if notice.request['url'].present? %>
4   -h3. URL
5   -
6   -"<%= notice.request['url'] %>":<%= notice.request['url'] %>
7   -<% end %>
8   -h3. Where
9   -
10   -<%= notice.where %>
11   -
12   -h3. When
13   -
14   -<%= notice.created_at.to_s(:micro) %>
15   -
16   -"More Details on Errbit":<%= app_problem_url problem.app, problem %>
17   -<% end %>
18 0 \ No newline at end of file
app/views/issue_trackers/textile_body.txt.erb
... ... @@ -1,44 +0,0 @@
1   -<% if notice = problem.notices.first %>
2   -h1. <%= notice.message %>
3   -
4   -h3. "See this exception on Errbit":<%= app_problem_url problem.app, problem %>
5   -
6   -h2. Summary
7   -<% if notice.request['url'].present? %>
8   -h3. URL
9   -
10   -"<%= notice.request['url'] %>":<%= notice.request['url'] %>
11   -<% end %>
12   -h3. Where
13   -
14   -<%= notice.where %>
15   -
16   -h3. Occurred
17   -
18   -<%= notice.created_at.to_s(:micro) %>
19   -
20   -h3. Similar
21   -
22   -<%= (notice.problem.notices_count - 1).to_s %>
23   -
24   -h2. Params
25   -
26   -<pre><%= pretty_hash(notice.params) %></pre>
27   -
28   -h2. Session
29   -
30   -<pre><%= pretty_hash(notice.session) %></pre>
31   -
32   -h2. Backtrace
33   -
34   -| Line | File | Method |
35   -<% notice.backtrace_lines.each do |line| %>| <%= line.number %> | <%= line.file_relative %> | *<%= line.method %>* |
36   -<% end %>
37   -
38   -h2. Environment
39   -
40   -<% for key, val in notice.env_vars %>| <%= key %> | <%= val %> |
41   -<% end %>
42   -
43   -<% end %>
44   -
db/seeds.rb
  1 +require 'securerandom'
  2 +
1 3 puts "Seeding database"
2 4 puts "-------------------------------"
3 5  
4 6 # Create an initial Admin User
5 7 admin_username = "errbit"
6 8 admin_email = "errbit@#{Errbit::Config.host}"
7   -admin_pass = 'password'
  9 +admin_pass = SecureRandom.urlsafe_base64(12)[0,12]
8 10  
9 11 puts "Creating an initial admin user:"
10 12 puts "-- username: #{admin_username}" if Errbit::Config.user_has_username
11 13 puts "-- email: #{admin_email}"
12 14 puts "-- password: #{admin_pass}"
13 15 puts ""
14   -puts "Be sure to change these credentials ASAP!"
  16 +puts "Be sure to note down these credentials now!"
15 17 user = User.find_or_initialize_by(:email => admin_email) do |u|
16 18 u.name = 'Errbit Admin'
17 19 u.password = admin_pass
... ...
docs/configuration.md
... ... @@ -74,8 +74,7 @@ In order of precedence Errbit uses:
74 74 <dd>OAuth scope to request from users when they sign-in through github
75 75 <dd>defaults to [repo]
76 76 <dt>EMAIL_DELIVERY_METHOD
77   -<dd>SMTP or sendmail, depending on how you want Errbit to send email
78   -<dd>defaults to sendmail
  77 +<dd>:smtp or :sendmail, depending on how you want Errbit to send email
79 78 <dt>SMTP_SERVER
80 79 <dd>Server address for outgoing SMTP messages
81 80 <dt>SMTP_PORT
... ...
lib/airbrake_api/v3/notice_parser.rb
... ... @@ -32,7 +32,7 @@ module AirbrakeApi
32 32 end
33 33  
34 34 def backtrace
35   - error['backtrace'].map do |backtrace_line|
  35 + (error['backtrace'] || []).map do |backtrace_line|
36 36 {
37 37 method: backtrace_line['function'],
38 38 file: backtrace_line['file'],
... ...
public/javascripts/notifier.js
... ... @@ -1,1218 +0,0 @@
1   -// Airbrake JavaScript Notifier Bundle
2   -(function(window, document, undefined) {
3   -// Domain Public by Eric Wendelin http://eriwen.com/ (2008)
4   -// Luke Smith http://lucassmith.name/ (2008)
5   -// Loic Dachary <loic@dachary.org> (2008)
6   -// Johan Euphrosine <proppy@aminche.com> (2008)
7   -// Øyvind Sean Kinsey http://kinsey.no/blog (2010)
8   -// Victor Homyakov (2010)
9   -//
10   -// Information and discussions
11   -// http://jspoker.pokersource.info/skin/test-printstacktrace.html
12   -// http://eriwen.com/javascript/js-stack-trace/
13   -// http://eriwen.com/javascript/stacktrace-update/
14   -// http://pastie.org/253058
15   -//
16   -// guessFunctionNameFromLines comes from firebug
17   -//
18   -// Software License Agreement (BSD License)
19   -//
20   -// Copyright (c) 2007, Parakey Inc.
21   -// All rights reserved.
22   -//
23   -// Redistribution and use of this software in source and binary forms, with or without modification,
24   -// are permitted provided that the following conditions are met:
25   -//
26   -// * Redistributions of source code must retain the above
27   -// copyright notice, this list of conditions and the
28   -// following disclaimer.
29   -//
30   -// * Redistributions in binary form must reproduce the above
31   -// copyright notice, this list of conditions and the
32   -// following disclaimer in the documentation and/or other
33   -// materials provided with the distribution.
34   -//
35   -// * Neither the name of Parakey Inc. nor the names of its
36   -// contributors may be used to endorse or promote products
37   -// derived from this software without specific prior
38   -// written permission of Parakey Inc.
39   -//
40   -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
41   -// IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
42   -// FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
43   -// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
44   -// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
45   -// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
46   -// IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
47   -// OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
48   -
49   -/**
50   - * Main function giving a function stack trace with a forced or passed in Error
51   - *
52   - * @cfg {Error} e The error to create a stacktrace from (optional)
53   - * @cfg {Boolean} guess If we should try to resolve the names of anonymous functions
54   - * @return {Array} of Strings with functions, lines, files, and arguments where possible
55   - */
56   -function printStackTrace(options) {
57   - options = options || {guess: true};
58   - var ex = options.e || null, guess = !!options.guess;
59   - var p = new printStackTrace.implementation(), result = p.run(ex);
60   - return (guess) ? p.guessAnonymousFunctions(result) : result;
61   -}
62   -
63   -if (typeof module !== "undefined" && module.exports) {
64   - module.exports = printStackTrace;
65   -}
66   -
67   -printStackTrace.implementation = function() {
68   -};
69   -
70   -printStackTrace.implementation.prototype = {
71   - /**
72   - * @param {Error} ex The error to create a stacktrace from (optional)
73   - * @param {String} mode Forced mode (optional, mostly for unit tests)
74   - */
75   - run: function(ex, mode) {
76   - ex = ex || this.createException();
77   - // examine exception properties w/o debugger
78   - //for (var prop in ex) {alert("Ex['" + prop + "']=" + ex[prop]);}
79   - mode = mode || this.mode(ex);
80   - if (mode === 'other') {
81   - return this.other(arguments.callee);
82   - } else {
83   - return this[mode](ex);
84   - }
85   - },
86   -
87   - createException: function() {
88   - try {
89   - this.undef();
90   - } catch (e) {
91   - return e;
92   - }
93   - },
94   -
95   - /**
96   - * Mode could differ for different exception, e.g.
97   - * exceptions in Chrome may or may not have arguments or stack.
98   - *
99   - * @return {String} mode of operation for the exception
100   - */
101   - mode: function(e) {
102   - if (e['arguments'] && e.stack) {
103   - return 'chrome';
104   - } else if (e.stack && e.sourceURL) {
105   - return 'safari';
106   - } else if (e.stack && e.number) {
107   - return 'ie';
108   - } else if (typeof e.message === 'string' && typeof window !== 'undefined' && window.opera) {
109   - // e.message.indexOf("Backtrace:") > -1 -> opera
110   - // !e.stacktrace -> opera
111   - if (!e.stacktrace) {
112   - return 'opera9'; // use e.message
113   - }
114   - // 'opera#sourceloc' in e -> opera9, opera10a
115   - if (e.message.indexOf('\n') > -1 && e.message.split('\n').length > e.stacktrace.split('\n').length) {
116   - return 'opera9'; // use e.message
117   - }
118   - // e.stacktrace && !e.stack -> opera10a
119   - if (!e.stack) {
120   - return 'opera10a'; // use e.stacktrace
121   - }
122   - // e.stacktrace && e.stack -> opera10b
123   - if (e.stacktrace.indexOf("called from line") < 0) {
124   - return 'opera10b'; // use e.stacktrace, format differs from 'opera10a'
125   - }
126   - // e.stacktrace && e.stack -> opera11
127   - return 'opera11'; // use e.stacktrace, format differs from 'opera10a', 'opera10b'
128   - } else if (e.stack && !e.fileName) {
129   - // Chrome 27 does not have e.arguments as earlier versions,
130   - // but still does not have e.fileName as Firefox
131   - return 'chrome';
132   - } else if (e.stack) {
133   - return 'firefox';
134   - }
135   - return 'other';
136   - },
137   -
138   - /**
139   - * Given a context, function name, and callback function, overwrite it so that it calls
140   - * printStackTrace() first with a callback and then runs the rest of the body.
141   - *
142   - * @param {Object} context of execution (e.g. window)
143   - * @param {String} functionName to instrument
144   - * @param {Function} callback function to call with a stack trace on invocation
145   - */
146   - instrumentFunction: function(context, functionName, callback) {
147   - context = context || window;
148   - var original = context[functionName];
149   - context[functionName] = function instrumented() {
150   - callback.call(this, printStackTrace().slice(4));
151   - return context[functionName]._instrumented.apply(this, arguments);
152   - };
153   - context[functionName]._instrumented = original;
154   - },
155   -
156   - /**
157   - * Given a context and function name of a function that has been
158   - * instrumented, revert the function to it's original (non-instrumented)
159   - * state.
160   - *
161   - * @param {Object} context of execution (e.g. window)
162   - * @param {String} functionName to de-instrument
163   - */
164   - deinstrumentFunction: function(context, functionName) {
165   - if (context[functionName].constructor === Function &&
166   - context[functionName]._instrumented &&
167   - context[functionName]._instrumented.constructor === Function) {
168   - context[functionName] = context[functionName]._instrumented;
169   - }
170   - },
171   -
172   - /**
173   - * Given an Error object, return a formatted Array based on Chrome's stack string.
174   - *
175   - * @param e - Error object to inspect
176   - * @return Array<String> of function calls, files and line numbers
177   - */
178   - chrome: function(e) {
179   - var stack = (e.stack + '\n').replace(/^\S[^\(]+?[\n$]/gm, '').
180   - replace(/^\s+(at eval )?at\s+/gm, '').
181   - replace(/^([^\(]+?)([\n$])/gm, '{anonymous}()@$1$2').
182   - replace(/^Object.<anonymous>\s*\(([^\)]+)\)/gm, '{anonymous}()@$1').split('\n');
183   - stack.pop();
184   - return stack;
185   - },
186   -
187   - /**
188   - * Given an Error object, return a formatted Array based on Safari's stack string.
189   - *
190   - * @param e - Error object to inspect
191   - * @return Array<String> of function calls, files and line numbers
192   - */
193   - safari: function(e) {
194   - return e.stack.replace(/\[native code\]\n/m, '')
195   - .replace(/^(?=\w+Error\:).*$\n/m, '')
196   - .replace(/^@/gm, '{anonymous}()@')
197   - .split('\n');
198   - },
199   -
200   - /**
201   - * Given an Error object, return a formatted Array based on IE's stack string.
202   - *
203   - * @param e - Error object to inspect
204   - * @return Array<String> of function calls, files and line numbers
205   - */
206   - ie: function(e) {
207   - var lineRE = /^.*at (\w+) \(([^\)]+)\)$/gm;
208   - return e.stack.replace(/at Anonymous function /gm, '{anonymous}()@')
209   - .replace(/^(?=\w+Error\:).*$\n/m, '')
210   - .replace(lineRE, '$1@$2')
211   - .split('\n');
212   - },
213   -
214   - /**
215   - * Given an Error object, return a formatted Array based on Firefox's stack string.
216   - *
217   - * @param e - Error object to inspect
218   - * @return Array<String> of function calls, files and line numbers
219   - */
220   - firefox: function(e) {
221   - return e.stack.replace(/(?:\n@:0)?\s+$/m, '').replace(/^[\(@]/gm, '{anonymous}()@').split('\n');
222   - },
223   -
224   - opera11: function(e) {
225   - var ANON = '{anonymous}', lineRE = /^.*line (\d+), column (\d+)(?: in (.+))? in (\S+):$/;
226   - var lines = e.stacktrace.split('\n'), result = [];
227   -
228   - for (var i = 0, len = lines.length; i < len; i += 2) {
229   - var match = lineRE.exec(lines[i]);
230   - if (match) {
231   - var location = match[4] + ':' + match[1] + ':' + match[2];
232   - var fnName = match[3] || "global code";
233   - fnName = fnName.replace(/<anonymous function: (\S+)>/, "$1").replace(/<anonymous function>/, ANON);
234   - result.push(fnName + '@' + location + ' -- ' + lines[i + 1].replace(/^\s+/, ''));
235   - }
236   - }
237   -
238   - return result;
239   - },
240   -
241   - opera10b: function(e) {
242   - // "<anonymous function: run>([arguments not available])@file://localhost/G:/js/stacktrace.js:27\n" +
243   - // "printStackTrace([arguments not available])@file://localhost/G:/js/stacktrace.js:18\n" +
244   - // "@file://localhost/G:/js/test/functional/testcase1.html:15"
245   - var lineRE = /^(.*)@(.+):(\d+)$/;
246   - var lines = e.stacktrace.split('\n'), result = [];
247   -
248   - for (var i = 0, len = lines.length; i < len; i++) {
249   - var match = lineRE.exec(lines[i]);
250   - if (match) {
251   - var fnName = match[1]? (match[1] + '()') : "global code";
252   - result.push(fnName + '@' + match[2] + ':' + match[3]);
253   - }
254   - }
255   -
256   - return result;
257   - },
258   -
259   - /**
260   - * Given an Error object, return a formatted Array based on Opera 10's stacktrace string.
261   - *
262   - * @param e - Error object to inspect
263   - * @return Array<String> of function calls, files and line numbers
264   - */
265   - opera10a: function(e) {
266   - // " Line 27 of linked script file://localhost/G:/js/stacktrace.js\n"
267   - // " Line 11 of inline#1 script in file://localhost/G:/js/test/functional/testcase1.html: In function foo\n"
268   - var ANON = '{anonymous}', lineRE = /Line (\d+).*script (?:in )?(\S+)(?:: In function (\S+))?$/i;
269   - var lines = e.stacktrace.split('\n'), result = [];
270   -
271   - for (var i = 0, len = lines.length; i < len; i += 2) {
272   - var match = lineRE.exec(lines[i]);
273   - if (match) {
274   - var fnName = match[3] || ANON;
275   - result.push(fnName + '()@' + match[2] + ':' + match[1] + ' -- ' + lines[i + 1].replace(/^\s+/, ''));
276   - }
277   - }
278   -
279   - return result;
280   - },
281   -
282   - // Opera 7.x-9.2x only!
283   - opera9: function(e) {
284   - // " Line 43 of linked script file://localhost/G:/js/stacktrace.js\n"
285   - // " Line 7 of inline#1 script in file://localhost/G:/js/test/functional/testcase1.html\n"
286   - var ANON = '{anonymous}', lineRE = /Line (\d+).*script (?:in )?(\S+)/i;
287   - var lines = e.message.split('\n'), result = [];
288   -
289   - for (var i = 2, len = lines.length; i < len; i += 2) {
290   - var match = lineRE.exec(lines[i]);
291   - if (match) {
292   - result.push(ANON + '()@' + match[2] + ':' + match[1] + ' -- ' + lines[i + 1].replace(/^\s+/, ''));
293   - }
294   - }
295   -
296   - return result;
297   - },
298   -
299   - // Safari 5-, IE 9-, and others
300   - other: function(curr) {
301   - var ANON = '{anonymous}', fnRE = /function\s*([\w\-$]+)?\s*\(/i, stack = [], fn, args, maxStackSize = 10;
302   - while (curr && curr['arguments'] && stack.length < maxStackSize) {
303   - fn = fnRE.test(curr.toString()) ? RegExp.$1 || ANON : ANON;
304   - args = Array.prototype.slice.call(curr['arguments'] || []);
305   - stack[stack.length] = fn + '(' + this.stringifyArguments(args) + ')';
306   - curr = curr.caller;
307   - }
308   - return stack;
309   - },
310   -
311   - /**
312   - * Given arguments array as a String, substituting type names for non-string types.
313   - *
314   - * @param {Arguments,Array} args
315   - * @return {String} stringified arguments
316   - */
317   - stringifyArguments: function(args) {
318   - var result = [];
319   - var slice = Array.prototype.slice;
320   - for (var i = 0; i < args.length; ++i) {
321   - var arg = args[i];
322   - if (arg === undefined) {
323   - result[i] = 'undefined';
324   - } else if (arg === null) {
325   - result[i] = 'null';
326   - } else if (arg.constructor) {
327   - if (arg.constructor === Array) {
328   - if (arg.length < 3) {
329   - result[i] = '[' + this.stringifyArguments(arg) + ']';
330   - } else {
331   - result[i] = '[' + this.stringifyArguments(slice.call(arg, 0, 1)) + '...' + this.stringifyArguments(slice.call(arg, -1)) + ']';
332   - }
333   - } else if (arg.constructor === Object) {
334   - result[i] = '#object';
335   - } else if (arg.constructor === Function) {
336   - result[i] = '#function';
337   - } else if (arg.constructor === String) {
338   - result[i] = '"' + arg + '"';
339   - } else if (arg.constructor === Number) {
340   - result[i] = arg;
341   - }
342   - }
343   - }
344   - return result.join(',');
345   - },
346   -
347   - sourceCache: {},
348   -
349   - /**
350   - * @return the text from a given URL
351   - */
352   - ajax: function(url) {
353   - var req = this.createXMLHTTPObject();
354   - if (req) {
355   - try {
356   - req.open('GET', url, false);
357   - //req.overrideMimeType('text/plain');
358   - //req.overrideMimeType('text/javascript');
359   - req.send(null);
360   - //return req.status == 200 ? req.responseText : '';
361   - return req.responseText;
362   - } catch (e) {
363   - }
364   - }
365   - return '';
366   - },
367   -
368   - /**
369   - * Try XHR methods in order and store XHR factory.
370   - *
371   - * @return <Function> XHR function or equivalent
372   - */
373   - createXMLHTTPObject: function() {
374   - var xmlhttp, XMLHttpFactories = [
375   - function() {
376   - return new XMLHttpRequest();
377   - }, function() {
378   - return new ActiveXObject('Msxml2.XMLHTTP');
379   - }, function() {
380   - return new ActiveXObject('Msxml3.XMLHTTP');
381   - }, function() {
382   - return new ActiveXObject('Microsoft.XMLHTTP');
383   - }
384   - ];
385   - for (var i = 0; i < XMLHttpFactories.length; i++) {
386   - try {
387   - xmlhttp = XMLHttpFactories[i]();
388   - // Use memoization to cache the factory
389   - this.createXMLHTTPObject = XMLHttpFactories[i];
390   - return xmlhttp;
391   - } catch (e) {
392   - }
393   - }
394   - },
395   -
396   - /**
397   - * Given a URL, check if it is in the same domain (so we can get the source
398   - * via Ajax).
399   - *
400   - * @param url <String> source url
401   - * @return <Boolean> False if we need a cross-domain request
402   - */
403   - isSameDomain: function(url) {
404   - return typeof location !== "undefined" && url.indexOf(location.hostname) !== -1; // location may not be defined, e.g. when running from nodejs.
405   - },
406   -
407   - /**
408   - * Get source code from given URL if in the same domain.
409   - *
410   - * @param url <String> JS source URL
411   - * @return <Array> Array of source code lines
412   - */
413   - getSource: function(url) {
414   - // TODO reuse source from script tags?
415   - if (!(url in this.sourceCache)) {
416   - this.sourceCache[url] = this.ajax(url).split('\n');
417   - }
418   - return this.sourceCache[url];
419   - },
420   -
421   - guessAnonymousFunctions: function(stack) {
422   - for (var i = 0; i < stack.length; ++i) {
423   - var reStack = /\{anonymous\}\(.*\)@(.*)/,
424   - reRef = /^(.*?)(?::(\d+))(?::(\d+))?(?: -- .+)?$/,
425   - frame = stack[i], ref = reStack.exec(frame);
426   -
427   - if (ref) {
428   - var m = reRef.exec(ref[1]);
429   - if (m) { // If falsey, we did not get any file/line information
430   - var file = m[1], lineno = m[2], charno = m[3] || 0;
431   - if (file && this.isSameDomain(file) && lineno) {
432   - var functionName = this.guessAnonymousFunction(file, lineno, charno);
433   - stack[i] = frame.replace('{anonymous}', functionName);
434   - }
435   - }
436   - }
437   - }
438   - return stack;
439   - },
440   -
441   - guessAnonymousFunction: function(url, lineNo, charNo) {
442   - var ret;
443   - try {
444   - ret = this.findFunctionName(this.getSource(url), lineNo);
445   - } catch (e) {
446   - ret = 'getSource failed with url: ' + url + ', exception: ' + e.toString();
447   - }
448   - return ret;
449   - },
450   -
451   - findFunctionName: function(source, lineNo) {
452   - // FIXME findFunctionName fails for compressed source
453   - // (more than one function on the same line)
454   - // function {name}({args}) m[1]=name m[2]=args
455   - var reFunctionDeclaration = /function\s+([^(]*?)\s*\(([^)]*)\)/;
456   - // {name} = function ({args}) TODO args capture
457   - // /['"]?([0-9A-Za-z_]+)['"]?\s*[:=]\s*function(?:[^(]*)/
458   - var reFunctionExpression = /['"]?([$_A-Za-z][$_A-Za-z0-9]*)['"]?\s*[:=]\s*function\b/;
459   - // {name} = eval()
460   - var reFunctionEvaluation = /['"]?([$_A-Za-z][$_A-Za-z0-9]*)['"]?\s*[:=]\s*(?:eval|new Function)\b/;
461   - // Walk backwards in the source lines until we find
462   - // the line which matches one of the patterns above
463   - var code = "", line, maxLines = Math.min(lineNo, 20), m, commentPos;
464   - for (var i = 0; i < maxLines; ++i) {
465   - // lineNo is 1-based, source[] is 0-based
466   - line = source[lineNo - i - 1];
467   - commentPos = line.indexOf('//');
468   - if (commentPos >= 0) {
469   - line = line.substr(0, commentPos);
470   - }
471   - // TODO check other types of comments? Commented code may lead to false positive
472   - if (line) {
473   - code = line + code;
474   - m = reFunctionExpression.exec(code);
475   - if (m && m[1]) {
476   - return m[1];
477   - }
478   - m = reFunctionDeclaration.exec(code);
479   - if (m && m[1]) {
480   - //return m[1] + "(" + (m[2] || "") + ")";
481   - return m[1];
482   - }
483   - m = reFunctionEvaluation.exec(code);
484   - if (m && m[1]) {
485   - return m[1];
486   - }
487   - }
488   - }
489   - return '(?)';
490   - }
491   -};// Airbrake JavaScript Notifier
492   -(function() {
493   - "use strict";
494   -
495   - var NOTICE_XML = '<?xml version="1.0" encoding="UTF-8"?>' +
496   - '<notice version="2.0">' +
497   - '<api-key>{key}</api-key>' +
498   - '<notifier>' +
499   - '<name>airbrake_js</name>' +
500   - '<version>0.2.0</version>' +
501   - '<url>http://airbrake.io</url>' +
502   - '</notifier>' +
503   - '<error>' +
504   - '<class>{exception_class}</class>' +
505   - '<message><![CDATA[{exception_message}]]></message>' +
506   - '<backtrace>{backtrace_lines}</backtrace>' +
507   - '</error>' +
508   - '<request>' +
509   - '<url><![CDATA[{request_url}]]></url>' +
510   - '<component>{request_component}</component>' +
511   - '<action>{request_action}</action>' +
512   - '{request}' +
513   - '</request>' +
514   - '<server-environment>' +
515   - '<project-root>{project_root}</project-root>' +
516   - '<environment-name>{environment}</environment-name>' +
517   - '<app-version>{appVersion}</app-version>' +
518   - '</server-environment>' +
519   - '<current-user>' +
520   - '<id>{user_id}</id>' +
521   - '<name>{user_name}</name>' +
522   - '<email>{user_email}</email>' +
523   - '</current-user>' +
524   - '</notice>',
525   - REQUEST_VARIABLE_GROUP_XML = '<{group_name}>{inner_content}</{group_name}>',
526   - REQUEST_VARIABLE_XML = '<var key="{key}">{value}</var>',
527   - BACKTRACE_LINE_XML = '<line method="{function}" file="{file}" number="{line}" />',
528   - Config,
529   - Global,
530   - Util,
531   - _publicAPI,
532   -
533   - NOTICE_JSON = {
534   - "notifier": {
535   - "name": "airbrake_js",
536   - "version": "0.2.0",
537   - "url": "http://airbrake.io"
538   - },
539   - "error": [
540   - {
541   - "type": "{exception_class}",
542   - "message": "{exception_message}",
543   - "backtrace": []
544   -
545   - }
546   - ],
547   - "context": {
548   - "language": "JavaScript",
549   - "environment": "{environment}",
550   -
551   - "version": "1.1.1",
552   - "url": "{request_url}",
553   - "rootDirectory": "{project_root}",
554   - "action": "{request_action}",
555   - "app-version": "{appVersion}",
556   -
557   - "userId": "{user_id}",
558   - "userName": "{user_name}",
559   - "userEmail": "{user_email}"
560   - },
561   - "environment": {},
562   - //"session": "",
563   - "params": {}
564   - };
565   -
566   - Util = {
567   - /*
568   - * Merge a number of objects into one.
569   - *
570   - * Usage example:
571   - * var obj1 = {
572   - * a: 'a'
573   - * },
574   - * obj2 = {
575   - * b: 'b'
576   - * },
577   - * obj3 = {
578   - * c: 'c'
579   - * },
580   - * mergedObj = Util.merge(obj1, obj2, obj3);
581   - *
582   - * mergedObj is: {
583   - * a: 'a',
584   - * b: 'b',
585   - * c: 'c'
586   - * }
587   - *
588   - */
589   - merge: (function() {
590   - function processProperty (key, dest, src) {
591   - if (src.hasOwnProperty(key)) {
592   - dest[key] = src[key];
593   - }
594   - }
595   -
596   - return function() {
597   - var objects = Array.prototype.slice.call(arguments),
598   - obj,
599   - key,
600   - result = {};
601   -
602   - while (obj = objects.shift()) {
603   - for (key in obj) {
604   - processProperty(key, result, obj);
605   - }
606   - }
607   -
608   - return result;
609   - };
610   - })(),
611   -
612   - /*
613   - * Replace &, <, >, ', " characters with correspondent HTML entities.
614   - */
615   - escape: function (text) {
616   - return text.replace(/&/g, '&#38;').replace(/</g, '&#60;').replace(/>/g, '&#62;')
617   - .replace(/'/g, '&#39;').replace(/"/g, '&#34;');
618   - },
619   -
620   - /*
621   - * Remove leading and trailing space characters.
622   - */
623   - trim: function (text) {
624   - return text.toString().replace(/^\s+/, '').replace(/\s+$/, '');
625   - },
626   -
627   - /*
628   - * Fill 'text' pattern with 'data' values.
629   - *
630   - * e.g. Utils.substitute('<{tag}></{tag}>', {tag: 'div'}, true) will return '<div></div>'
631   - *
632   - * emptyForUndefinedData - a flag, if true, all matched {<name>} without data.<name> value specified will be
633   - * replaced with empty string.
634   - */
635   - substitute: function (text, data, emptyForUndefinedData) {
636   - return text.replace(/{([\w_.-]+)}/g, function(match, key) {
637   - return (key in data) ? data[key] : (emptyForUndefinedData ? '' : match);
638   - });
639   - },
640   -
641   - /*
642   - * Perform pattern rendering for an array of data objects.
643   - * Returns a concatenation of rendered strings of all objects in array.
644   - */
645   - substituteArr: function (text, dataArr, emptyForUndefinedData) {
646   - var _i = 0, _l = 0,
647   - returnStr = '';
648   -
649   - for (_i = 0, _l = dataArr.length; _i < _l; _i += 1) {
650   - returnStr += this.substitute(text, dataArr[_i], emptyForUndefinedData);
651   - }
652   -
653   - return returnStr;
654   - },
655   -
656   - /*
657   - * Add hook for jQuery.fn.on function, to manualy call window.Airbrake.captureException() method
658   - * for every exception occurred.
659   - *
660   - * Let function 'f' be binded as an event handler:
661   - *
662   - * $(window).on 'click', f
663   - *
664   - * If an exception is occurred inside f's body, it will be catched here
665   - * and forwarded to captureException method.
666   - *
667   - * processjQueryEventHandlerWrapping is called every time window.Airbrake.setTrackJQ method is used,
668   - * if it switches previously setted value.
669   - */
670   - processjQueryEventHandlerWrapping: function () {
671   - if (Config.options.trackJQ === true) {
672   - Config.jQuery_fn_on_original = Config.jQuery_fn_on_original || jQuery.fn.on;
673   -
674   - jQuery.fn.on = function () {
675   - var args = Array.prototype.slice.call(arguments),
676   - fnArgIdx = 4;
677   -
678   - // Search index of function argument
679   - while((--fnArgIdx > -1) && (typeof args[fnArgIdx] !== 'function'));
680   -
681   - // If the function is not found, then subscribe original event handler function
682   - if (fnArgIdx === -1) {
683   - return Config.jQuery_fn_on_original.apply(this, arguments);
684   - }
685   -
686   - // If the function is found, then subscribe wrapped event handler function
687   - args[fnArgIdx] = (function (fnOriginHandler) {
688   - return function() {
689   - try {
690   - fnOriginHandler.apply(this, arguments);
691   - } catch (e) {
692   - Global.captureException(e);
693   - }
694   - };
695   - })(args[fnArgIdx]);
696   -
697   - // Call original jQuery.fn.on, with the same list of arguments, but
698   - // a function replaced with a proxy.
699   - return Config.jQuery_fn_on_original.apply(this, args);
700   - };
701   - } else {
702   - // Recover original jQuery.fn.on if Config.options.trackJQ is set to false
703   - (typeof Config.jQuery_fn_on_original === 'function') && (jQuery.fn.on = Config.jQuery_fn_on_original);
704   - }
705   - },
706   -
707   - isjQueryPresent: function () {
708   - // Currently only 1.7.x version supported
709   - return (typeof jQuery === 'function') && ('fn' in jQuery) && ('jquery' in jQuery.fn)
710   - && (jQuery.fn.jquery.indexOf('1.7') === 0)
711   - },
712   -
713   - /*
714   - * Make first letter in a string capital. e.g. 'guessFunctionName' -> 'GuessFunctionName'
715   - * Is used to generate getter and setter method names.
716   - */
717   - capitalizeFirstLetter: function (str) {
718   - return str.charAt(0).toUpperCase() + str.slice(1);
719   - },
720   -
721   - /*
722   - * Generate public API from an array of specifically formated objects, e.g.
723   - *
724   - * - this will generate 'setEnvironment' and 'getEnvironment' API methods for configObj.xmlData.environment variable:
725   - * {
726   - * variable: 'environment',
727   - * namespace: 'xmlData'
728   - * }
729   - *
730   - * - this will define 'method' function as 'captureException' API method
731   - * {
732   - * methodName: 'captureException',
733   - * method: (function (...) {...});
734   - * }
735   - *
736   - */
737   - generatePublicAPI: (function () {
738   - function _generateSetter (variable, namespace, configObj) {
739   - return function (value) {
740   - configObj[namespace][variable] = value;
741   - };
742   - }
743   -
744   - function _generateGetter (variable, namespace, configObj) {
745   - return function (value) {
746   - return configObj[namespace][variable];
747   - };
748   - }
749   -
750   - /*
751   - * publicAPI: array of specifically formated objects
752   - * configObj: inner configuration object
753   - */
754   - return function (publicAPI, configObj) {
755   - var _i = 0, _m = null, _capitalized = '',
756   - returnObj = {};
757   -
758   - for (_i = 0; _i < publicAPI.length; _i += 1) {
759   - _m = publicAPI[_i];
760   -
761   - switch (true) {
762   - case (typeof _m.variable !== 'undefined') && (typeof _m.methodName === 'undefined'):
763   - _capitalized = Util.capitalizeFirstLetter(_m.variable)
764   - returnObj['set' + _capitalized] = _generateSetter(_m.variable, _m.namespace, configObj);
765   - returnObj['get' + _capitalized] = _generateGetter(_m.variable, _m.namespace, configObj);
766   -
767   - break;
768   - case (typeof _m.methodName !== 'undefined') && (typeof _m.method !== 'undefined'):
769   - returnObj[_m.methodName] = _m.method
770   -
771   - break;
772   -
773   - default:
774   - }
775   - }
776   -
777   - return returnObj;
778   - };
779   - } ())
780   - };
781   -
782   - /*
783   - * The object to store settings. Allocated from the Global (windows scope) so that users can change settings
784   - * only through the methods, rather than through a direct change of the object fileds. So that we can to handle
785   - * change settings event (in setter method).
786   - */
787   - Config = {
788   - xmlData: {
789   - environment: 'environment'
790   - },
791   -
792   - options: {
793   - trackJQ: false, // jQuery.fn.jquery
794   - host: 'api.airbrake.io',
795   - errorDefaults: {},
796   - guessFunctionName: false,
797   - requestType: 'GET', // Can be 'POST' or 'GET'
798   - outputFormat: 'XML' // Can be 'XML' or 'JSON'
799   - }
800   - };
801   -
802   - /*
803   - * The public API definition object. If no 'methodName' and 'method' values specified,
804   - * getter and setter for 'variable' will be defined.
805   - */
806   - _publicAPI = [
807   - {
808   - variable: 'environment',
809   - namespace: 'xmlData'
810   - }, {
811   - variable: 'key',
812   - namespace: 'xmlData'
813   - }, {
814   - variable: 'host',
815   - namespace: 'options'
816   - },{
817   - variable: 'projectId',
818   - namespace: 'options'
819   - },{
820   - variable: 'errorDefaults',
821   - namespace: 'options'
822   - }, {
823   - variable: 'guessFunctionName',
824   - namespace: 'options'
825   - }, {
826   - variable: 'outputFormat',
827   - namespace: 'options'
828   - }, {
829   - methodName: 'setCurrentUser',
830   - method: (function (value) {
831   - for (var key in value) {
832   - if (value.hasOwnProperty(key)) {
833   - Config.xmlData['user_' + key] = value[key];
834   - }
835   - }
836   - })
837   - }, {
838   - methodName: 'setTrackJQ',
839   - variable: 'trackJQ',
840   - namespace: 'options',
841   - method: (function (value) {
842   - if (!Util.isjQueryPresent()) {
843   - throw Error('Please do not call \'Airbrake.setTrackJQ\' if jQuery does\'t present');
844   - }
845   -
846   - value = !!value;
847   -
848   - if (Config.options.trackJQ === value) {
849   - return;
850   - }
851   -
852   - Config.options.trackJQ = value;
853   -
854   - Util.processjQueryEventHandlerWrapping();
855   - })
856   - }, {
857   - methodName: 'captureException',
858   - method: (function (e) {
859   - new Notifier().notify({
860   - message: e.message,
861   - stack: e.stack
862   - });
863   - })
864   - }, {
865   - variable: 'appVersion',
866   - namespace: 'xmlData'
867   - }
868   - ];
869   -
870   - // Share to global scope as Airbrake ("window.Hoptoad" for backward compatibility)
871   - Global = window.Airbrake = window.Hoptoad = Util.generatePublicAPI(_publicAPI, Config);
872   -
873   - Global._filters = [];
874   -
875   - Global.addFilter = function (cb) {
876   - Global._filters.push(cb);
877   - };
878   -
879   - function Notifier() {
880   - this.options = Util.merge({}, Config.options);
881   - this.xmlData = Util.merge(this.DEF_XML_DATA, Config.xmlData);
882   - }
883   -
884   - Notifier.prototype = {
885   - constructor: Notifier,
886   - VERSION: '0.2.0',
887   - ROOT: window.location.protocol + '//' + window.location.host,
888   - BACKTRACE_MATCHER: /^(.*)\@(.*)\:(\d+)$/,
889   - backtrace_filters: [/notifier\.js/],
890   - DEF_XML_DATA: {
891   - request: {}
892   - },
893   -
894   - notify: (function () {
895   - /*
896   - * Emit GET request via <iframe> element.
897   - * Data is transmited as a part of query string.
898   - */
899   - function _sendGETRequest (url, data) {
900   - var request = document.createElement('iframe');
901   -
902   - request.style.display = 'none';
903   - request.src = url + '?data=' + data;
904   -
905   - // When request has been sent, delete iframe
906   - request.onload = function () {
907   - // To avoid infinite progress indicator
908   - setTimeout(function() {
909   - document.body.removeChild(request);
910   - }, 0);
911   - };
912   -
913   - document.body.appendChild(request);
914   - }
915   -
916   - /*
917   - * Cross-domain AJAX POST request.
918   - *
919   - * It requires a server setup as described in Cross-Origin Resource Sharing spec:
920   - * http://www.w3.org/TR/cors/
921   - */
922   - function _sendPOSTRequest (url, data) {
923   - var request = new XMLHttpRequest();
924   - request.open('POST', url, true);
925   - request.setRequestHeader('Content-Type', 'application/json');
926   - request.send(data);
927   - }
928   -
929   - return function (error) {
930   - var outputData = '', jsonData,
931   - url = '';
932   - //
933   -
934   - /*
935   - * Should be changed to url = '//' + ...
936   - * to use the protocol of current page (http or https). Only sends 'secure' if page is secure.
937   - * XML uses V2 API. http://collect.airbrake.io/notifier_api/v2/notices
938   - */
939   -
940   -
941   - switch (this.options['outputFormat']) {
942   - case 'XML':
943   - jsonData = this.generateDataJSON(error);
944   - if (this.shouldSendData(jsonData)){
945   - outputData = encodeURIComponent(this.generateXML(jsonData));
946   - url = ('https:' == document.location.protocol ? 'https://' : 'http://') + this.options.host + '/notifier_api/v2/notices';
947   - _sendGETRequest(url, outputData);
948   - }
949   - break;
950   -
951   - case 'JSON':
952   - /*
953   - * JSON uses API V3. Needs project in URL.
954   - * http://collect.airbrake.io/api/v3/projects/[PROJECT_ID]/notices?key=[API_KEY]
955   - * url = window.location.protocol + '://' + this.options.host + '/api/v3/projects' + this.options.projectId + '/notices?key=' + this.options.key;
956   - */
957   - jsonData = this.generateDataJSON(error);
958   - if (this.shouldSendData(jsonData)){
959   - outputData = JSON.stringify(this.generateJSON(jsonData));
960   - url = ('https:' == document.location.protocol ? 'https://' : 'http://') + this.options.host + '/api/v3/projects/' + this.options.projectId + '/notices?key=' + this.xmlData.key;
961   - _sendPOSTRequest(url, outputData);
962   - }
963   - break;
964   -
965   - default:
966   - }
967   -
968   - };
969   - } ()),
970   -
971   - /*
972   - * Generate inner JSON representation of exception data that can be rendered as XML or JSON.
973   - */
974   - generateDataJSON: (function () {
975   - /*
976   - * Generate variables array for inputObj object.
977   - *
978   - * e.g.
979   - *
980   - * _generateVariables({a: 'a'}) -> [{key: 'a', value: 'a'}]
981   - *
982   - */
983   - function _generateVariables (inputObj) {
984   - var key = '', returnArr = [];
985   -
986   - for (key in inputObj) {
987   - if (inputObj.hasOwnProperty(key)) {
988   - returnArr.push({
989   - key: key,
990   - value: inputObj[key]
991   - });
992   - }
993   - }
994   -
995   - return returnArr;
996   - }
997   -
998   - /*
999   - * Generate Request part of notification.
1000   - */
1001   - function _composeRequestObj (methods, errorObj) {
1002   - var _i = 0,
1003   - returnObj = {},
1004   - type = '';
1005   -
1006   - for (_i = 0; _i < methods.length; _i += 1) {
1007   - type = methods[_i];
1008   - if (typeof errorObj[type] !== 'undefined') {
1009   - returnObj[type] = _generateVariables(errorObj[type]);
1010   - }
1011   - }
1012   -
1013   - return returnObj;
1014   - }
1015   -
1016   - return function (errorWithoutDefaults) {
1017   - /*
1018   - * A constructor line:
1019   - *
1020   - * this.xmlData = Util.merge(this.DEF_XML_DATA, Config.xmlData);
1021   - */
1022   - var outputData = this.xmlData,
1023   - error = Util.merge(this.options.errorDefaults, errorWithoutDefaults),
1024   -
1025   - component = error.component || '',
1026   - request_url = (error.url || '' + location.href),
1027   -
1028   - methods = ['cgi-data', 'params', 'session'],
1029   - _outputData = null;
1030   -
1031   - _outputData = {
1032   - request_url: request_url,
1033   - request_action: (error.action || ''),
1034   - request_component: component,
1035   - request: (function () {
1036   - if (request_url || component) {
1037   - error['cgi-data'] = error['cgi-data'] || {};
1038   - error['cgi-data'].HTTP_USER_AGENT = navigator.userAgent;
1039   - return Util.merge(outputData.request, _composeRequestObj(methods, error));
1040   - } else {
1041   - return {}
1042   - }
1043   - } ()),
1044   -
1045   - project_root: this.ROOT,
1046   - exception_class: (error.type || errorWithoutDefaults.type ||
1047   - (errorWithoutDefaults.constructor.name != "Object" ? errorWithoutDefaults.constructor.name : 'Error')),
1048   - exception_message: (error.message || errorWithoutDefaults.message || 'Unknown error.'),
1049   - backtrace_lines: this.generateBacktrace(errorWithoutDefaults)
1050   - }
1051   -
1052   - outputData = Util.merge(outputData, _outputData);
1053   -
1054   - return outputData;
1055   - };
1056   - } ()),
1057   -
1058   - /*
1059   - * Generate XML notification from inner JSON representation.
1060   - * NOTICE_XML is used as pattern.
1061   - */
1062   - generateXML: (function () {
1063   - function _generateRequestVariableGroups (requestObj) {
1064   - var _group = '',
1065   - returnStr = '';
1066   -
1067   - for (_group in requestObj) {
1068   - if (requestObj.hasOwnProperty(_group)) {
1069   - returnStr += Util.substitute(REQUEST_VARIABLE_GROUP_XML, {
1070   - group_name: _group,
1071   - inner_content: Util.substituteArr(REQUEST_VARIABLE_XML, requestObj[_group], true)
1072   - }, true);
1073   - }
1074   - }
1075   -
1076   - return returnStr;
1077   - }
1078   -
1079   - return function (JSONdataObj) {
1080   - JSONdataObj.request = _generateRequestVariableGroups(JSONdataObj.request);
1081   - JSONdataObj.backtrace_lines = Util.substituteArr(BACKTRACE_LINE_XML, JSONdataObj.backtrace_lines, true);
1082   -
1083   - return Util.substitute(NOTICE_XML, JSONdataObj, true);
1084   - };
1085   - } ()),
1086   -
1087   - /*
1088   - * Generate JSON notification from inner JSON representation.
1089   - * NOTICE_JSON is used as pattern.
1090   - */
1091   - generateJSON: function (JSONdataObj) {
1092   - // Pattern string is JSON.stringify(NOTICE_JSON)
1093   - // The rendered string is parsed back as JSON.
1094   - var outputJSON = JSON.parse(Util.substitute(JSON.stringify(NOTICE_JSON), JSONdataObj, true));
1095   -
1096   - // REMOVED - Request from JSON.
1097   - outputJSON.request = Util.merge(outputJSON.request, JSONdataObj.request);
1098   - outputJSON.error.backtrace = JSONdataObj.backtrace_lines;
1099   -
1100   - return outputJSON;
1101   - },
1102   -
1103   - generateBacktrace: function (error) {
1104   - var backtrace = [],
1105   - file,
1106   - i,
1107   - matches,
1108   - stacktrace;
1109   -
1110   - error = error || {};
1111   -
1112   - if (typeof error.stack !== 'string') {
1113   - try {
1114   - (0)();
1115   - } catch (e) {
1116   - error.stack = e.stack;
1117   - }
1118   - }
1119   -
1120   - stacktrace = this.getStackTrace(error);
1121   -
1122   - for (i = 0; i < stacktrace.length; i++) {
1123   - matches = stacktrace[i].match(this.BACKTRACE_MATCHER);
1124   -
1125   - if (matches && this.validBacktraceLine(stacktrace[i])) {
1126   - file = matches[2].replace(this.ROOT, '[PROJECT_ROOT]');
1127   -
1128   - if (i === 0 && matches[2].match(document.location.href)) {
1129   - // backtrace.push('<line method="" file="internal: " number=""/>');
1130   -
1131   - backtrace.push({
1132   - // Updated to fit in with V3 new terms for Backtrace data.
1133   - 'function': '',
1134   - file: 'internal: ',
1135   - line: ''
1136   - });
1137   - }
1138   -
1139   - // backtrace.push('<line method="' + Util.escape(matches[1]) + '" file="' + Util.escape(file) +
1140   - // '" number="' + matches[3] + '" />');
1141   -
1142   - backtrace.push({
1143   - 'function': Util.escape(matches[1]),
1144   - file: Util.escape(file),
1145   - line: matches[3]
1146   - });
1147   - }
1148   - }
1149   -
1150   - return backtrace;
1151   - },
1152   -
1153   - getStackTrace: function (error) {
1154   - var i,
1155   - stacktrace = printStackTrace({
1156   - e: error,
1157   - guess: this.options.guessFunctionName
1158   - });
1159   -
1160   - for (i = 0; i < stacktrace.length; i++) {
1161   - if (stacktrace[i].match(/\:\d+$/)) {
1162   - continue;
1163   - }
1164   -
1165   - // Special case for sprocket coffee stacktrace:
1166   - // "Function.foo (http://host/file.js?body=1:666:42)" becomes "Function.foo @http://host/file.js?body=1:666"
1167   - if (stacktrace[i].match(/\([^\s]+:(\d+):(\d+)\)$/)) {
1168   - stacktrace[i] = stacktrace[i].replace(/\((.+):(\d+):(\d+)\)$/, '@$1:$2')
1169   - continue;
1170   - }
1171   -
1172   - if (stacktrace[i].indexOf('@') === -1) {
1173   - stacktrace[i] += '@unsupported.js';
1174   - }
1175   -
1176   - stacktrace[i] += ':0';
1177   - }
1178   -
1179   - return stacktrace;
1180   - },
1181   -
1182   - validBacktraceLine: function (line) {
1183   - for (var i = 0; i < this.backtrace_filters.length; i++) {
1184   - if (line.match(this.backtrace_filters[i])) {
1185   - return false;
1186   - }
1187   - }
1188   -
1189   - return true;
1190   - },
1191   -
1192   - shouldSendData: function (jsonData) {
1193   - var shouldSend = true, i;
1194   -
1195   - for ( i = 0; i < Global._filters.length; i++ ) {
1196   - if ( ! Global._filters[i](jsonData) ){
1197   - shouldSend = false;
1198   - }
1199   - }
1200   -
1201   - return shouldSend;
1202   - }
1203   - };
1204   -
1205   - var oldOnerror = window.onerror;
1206   - window.onerror = function (message, file, line, code, error) {
1207   - setTimeout(function () {
1208   - var e = error || {stack: '()@' + file + ':' + line}
1209   - e.message = message
1210   - new Notifier().notify(e);
1211   - }, 0);
1212   - if (oldOnerror) {
1213   - return oldOnerror(message, file, line, code, error);
1214   - }
1215   - return true;
1216   - };
1217   -})();
1218   -})(window, document);
spec/fixtures/api_v3_request_without_backtrace.json 0 → 100644
... ... @@ -0,0 +1,28 @@
  1 +{
  2 + "notifier":{"name":"airbrake-js-v8","version":"0.3.10","url":"https://github.com/airbrake/airbrake-js"},
  3 + "errors":[
  4 + {
  5 + "type":"Error",
  6 + "message":"Error: TestError",
  7 + "backtrace":null
  8 + }
  9 + ],
  10 + "context":{
  11 + "language":"JavaScript",
  12 + "sourceMapEnabled":true,
  13 + "userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.99 Safari/537.36",
  14 + "url":"http://localhost:3000/kontakt",
  15 + "userId":1,"userUsername":"john",
  16 + "userName":"John Doe",
  17 + "userUsername": "john",
  18 + "userEmail":"john.doe@example.org",
  19 + "version":"1.0",
  20 + "component":"ContactsController",
  21 + "action":"show"
  22 + },
  23 + "params":{"returnTo":"dashboard"},
  24 + "environment":{"navigator_vendor":"Google Inc."},
  25 + "session":{"isAdmin":true},
  26 + "key":"a3969bfbf65f073921e",
  27 + "project_id":"a3969bfbf65f073921e"
  28 +}
0 29 \ No newline at end of file
... ...
spec/initializers/action_mailer_spec.rb 0 → 100644
... ... @@ -0,0 +1,43 @@
  1 +describe 'initializers/action_mailer' do
  2 + def load_initializer
  3 + load File.join(Rails.root, 'config', 'initializers', 'action_mailer.rb')
  4 + end
  5 +
  6 + describe 'delivery method' do
  7 + it 'sets the delivery method to :smtp' do
  8 + allow(Errbit::Config).to receive(:email_delivery_method).and_return(:smtp)
  9 + load_initializer
  10 +
  11 + expect(ActionMailer::Base.delivery_method).to be(:smtp)
  12 + end
  13 +
  14 + it 'sets the delivery method to :sendmail' do
  15 + allow(Errbit::Config).to receive(:email_delivery_method).and_return(:sendmail)
  16 + load_initializer
  17 +
  18 + expect(ActionMailer::Base.delivery_method).to be(:sendmail)
  19 + end
  20 + end
  21 +
  22 + describe 'smtp settings' do
  23 + it 'lets smtp settings be set' do
  24 + allow(Errbit::Config).to receive(:email_delivery_method).and_return(:smtp)
  25 + allow(Errbit::Config).to receive(:smtp_address).and_return('smtp.somedomain.com')
  26 + allow(Errbit::Config).to receive(:smtp_port).and_return(998)
  27 + allow(Errbit::Config).to receive(:smtp_authentication).and_return(:login)
  28 + allow(Errbit::Config).to receive(:smtp_user_name).and_return('my-username')
  29 + allow(Errbit::Config).to receive(:smtp_password).and_return('my-password')
  30 + allow(Errbit::Config).to receive(:smtp_domain).and_return('someotherdomain.com')
  31 + load_initializer
  32 +
  33 + expect(ActionMailer::Base.smtp_settings).to eq({
  34 + address: 'smtp.somedomain.com',
  35 + port: 998,
  36 + authentication: :login,
  37 + user_name: 'my-username',
  38 + password: 'my-password',
  39 + domain: 'someotherdomain.com',
  40 + })
  41 + end
  42 + end
  43 +end
... ...
spec/lib/airbrake_api/v3/notice_parser_spec.rb
... ... @@ -36,6 +36,19 @@ describe AirbrakeApi::V3::NoticeParser do
36 36 expect(report).to be_valid
37 37 end
38 38  
  39 + it 'parses JSON payload with missing backtrace' do
  40 + json = Rails.root.join('spec', 'fixtures', 'api_v3_request_without_backtrace.json').read
  41 + params = JSON.parse(json)
  42 + params['key'] = app.api_key
  43 +
  44 + report = AirbrakeApi::V3::NoticeParser.new(params).report
  45 + notice = report.generate_notice!
  46 +
  47 + expect(report.error_class).to eq('Error')
  48 + expect(report.message).to eq('Error: TestError')
  49 + expect(report.backtrace.lines.size).to eq(0)
  50 + end
  51 +
39 52 def build_params(options = {})
40 53 json = Rails.root.join('spec', 'fixtures', 'api_v3_request.json').read
41 54 data = JSON.parse(json)
... ...
spec/lib/configurator_spec.rb
... ... @@ -34,4 +34,28 @@ describe Configurator do
34 34 })
35 35 expect(result.one).to eq('zoom')
36 36 end
  37 +
  38 + it 'extracts symbol values' do
  39 + allow(ENV).to receive(:[]).with('MYSYMBOL').and_return(':asymbol')
  40 + result = Configurator.run({ mysymbol: ['MYSYMBOL'] })
  41 + expect(result.mysymbol).to be(:asymbol)
  42 + end
  43 +
  44 + it 'extracts array values' do
  45 + allow(ENV).to receive(:[]).with('MYARRAY').and_return('[one,two,three]')
  46 + result = Configurator.run({ myarray: ['MYARRAY'] })
  47 + expect(result.myarray).to eq(['one', 'two', 'three'])
  48 + end
  49 +
  50 + it 'extracts booleans' do
  51 + allow(ENV).to receive(:[]).with('MYBOOLEAN').and_return('true')
  52 + result = Configurator.run({ myboolean: ['MYBOOLEAN'] })
  53 + expect(result.myboolean).to be(true)
  54 + end
  55 +
  56 + it 'extracts numbers' do
  57 + allow(ENV).to receive(:[]).with('MYNUMBER').and_return('0')
  58 + result = Configurator.run({ mynumber: ['MYNUMBER'] })
  59 + expect(result.mynumber).to be(0)
  60 + end
37 61 end
... ...
spec/models/issue_spec.rb
... ... @@ -28,15 +28,35 @@ describe Issue, type: &#39;model&#39; do
28 28 end
29 29  
30 30 context "when has no title" do
  31 + let(:title) { nil }
31 32 let(:body) { "barrr" }
32 33  
33   - pending "returns an error"
  34 + context "#save" do
  35 + it "returns false" do
  36 + expect(issue.save).to be false
  37 + end
  38 +
  39 + it "returns an error" do
  40 + issue.save
  41 + expect(errors).to include("The issue has no title")
  42 + end
  43 + end
34 44 end
35 45  
36 46 context "when has no body" do
37 47 let(:title) { "Foo" }
  48 + let(:body) { nil }
38 49  
39   - pending "returns an error"
  50 + context "#save" do
  51 + it "returns false" do
  52 + expect(issue.save).to be false
  53 + end
  54 +
  55 + it "returns an error" do
  56 + issue.save
  57 + expect(errors).to include("The issue has no body")
  58 + end
  59 + end
40 60 end
41 61  
42 62 context "when app has a issue tracker" do
... ...