ohnoes 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.document +4 -0
- data/.gitignore +4 -0
- data/.rspec +1 -0
- data/ChangeLog.rdoc +4 -0
- data/Gemfile +3 -0
- data/LICENSE +28 -0
- data/README.md +37 -0
- data/Rakefile +35 -0
- data/app/controllers/ohnoes/frontend_exceptions_controller.rb +47 -0
- data/config/routes.rb +4 -0
- data/lib/assets/javascripts/ohnoes.js +2 -0
- data/lib/assets/javascripts/ohnoes/notifier.js +984 -0
- data/lib/ohnoes.rb +4 -0
- data/lib/ohnoes/engine.rb +5 -0
- data/lib/ohnoes/frontend_exception.rb +20 -0
- data/lib/ohnoes/frontend_formatter.rb +13 -0
- data/lib/ohnoes/version.rb +4 -0
- data/ohnoes.gemspec +27 -0
- data/spec/ohnoes_spec.rb +8 -0
- data/spec/spec_helper.rb +4 -0
- metadata +151 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: beb1bd7a48ef82d8ef05dfd02f86e6633bae0338
|
4
|
+
data.tar.gz: 280a126767773dbf312e24ec9cdba2704cd8eaa0
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 6c18f1038459ebef009f8351576383b2a0b29d4682a59ece2609e19da53ef82142e75ff8b6cce5436b738df3272703c4644fe8802bf659055e8426a5ad69d6be
|
7
|
+
data.tar.gz: 6d2cb9fb25b92fc98c13f10c06610c0e7ab4fee699e27073ced9c0eb602ad313545de6945ef6c705a13332cdf295047d9d8db3770f5a66d915cfe710df9bd44d
|
data/.document
ADDED
data/.gitignore
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--colour --format documentation
|
data/ChangeLog.rdoc
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
Copyright (c) 2007-2014, Marten Veldthuis, Mark IJbema, Airbrake, Parakey Inc, Eric Wendelin, Luke Smith, Loic Dachary, Johan Euphrosine, Øyvind Sean Kinsey, Victor Homyakov
|
2
|
+
|
3
|
+
All rights reserved.
|
4
|
+
|
5
|
+
Redistribution and use in source and binary forms, with or without modification,
|
6
|
+
are permitted provided that the following conditions are met:
|
7
|
+
|
8
|
+
* Redistributions of source code must retain the above copyright notice,
|
9
|
+
this list of conditions and the following disclaimer.
|
10
|
+
* Redistributions in binary form must reproduce the above copyright notice,
|
11
|
+
this list of conditions and the following disclaimer in the documentation
|
12
|
+
and/or other materials provided with the distribution.
|
13
|
+
* Neither the name of Parakey Inc. nor the names of its
|
14
|
+
contributors may be used to endorse or promote products
|
15
|
+
derived from this software without specific prior
|
16
|
+
written permission of Parakey Inc.
|
17
|
+
|
18
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
19
|
+
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
20
|
+
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
21
|
+
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
|
22
|
+
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
23
|
+
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
24
|
+
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
25
|
+
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
26
|
+
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
27
|
+
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
28
|
+
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
data/README.md
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
# Pavlov [](http://travis-ci.org/marten/ohnoes) [](http://badge.fury.io/rb/ohnoes) [](https://gemnasium.com/marten/ohnoes) [](https://codeclimate.com/github/marten/ohnoes)
|
2
|
+
|
3
|
+
Ohnoes is a gem to report frontend errors using your existing backend error reporting.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
gem 'ohnoes'
|
11
|
+
```
|
12
|
+
|
13
|
+
Then add the following to your application.js
|
14
|
+
|
15
|
+
```javascript
|
16
|
+
//= require 'ohnoes'
|
17
|
+
```
|
18
|
+
|
19
|
+
## Is it any good?
|
20
|
+
|
21
|
+
Yes.
|
22
|
+
|
23
|
+
## Contributing
|
24
|
+
|
25
|
+
1. Fork it
|
26
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
27
|
+
3. Run bundle, before starting development.
|
28
|
+
4. Implement your feature/bugfix and corresponding tests.
|
29
|
+
5. Make sure your tests run against the latest stable mri.
|
30
|
+
6. Commit your changes (`git commit -am 'Add some feature'`)
|
31
|
+
7. Push to the branch (`git push origin my-new-feature`)
|
32
|
+
8. Create new Pull Request
|
33
|
+
|
34
|
+
## License
|
35
|
+
|
36
|
+
Copyright (c) 2007-2014, Marten Veldthuis, Mark IJbema, Airbrake, Parakey Inc, Eric Wendelin, Luke Smith, Loic Dachary, Johan Euphrosine, Øyvind Sean Kinsey, Victor Homyakov
|
37
|
+
Licensed under the BSD 3-clause license, with the third clause referring to Parakey Inc.
|
data/Rakefile
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
|
5
|
+
begin
|
6
|
+
require 'bundler'
|
7
|
+
rescue LoadError => e
|
8
|
+
warn e.message
|
9
|
+
warn "Run `gem install bundler` to install Bundler."
|
10
|
+
exit -1
|
11
|
+
end
|
12
|
+
|
13
|
+
begin
|
14
|
+
Bundler.setup(:development)
|
15
|
+
rescue Bundler::BundlerError => e
|
16
|
+
warn e.message
|
17
|
+
warn "Run `bundle install` to install missing gems."
|
18
|
+
exit e.status_code
|
19
|
+
end
|
20
|
+
|
21
|
+
require 'rake'
|
22
|
+
|
23
|
+
require 'rdoc/task'
|
24
|
+
RDoc::Task.new do |rdoc|
|
25
|
+
rdoc.title = "ohnoes"
|
26
|
+
end
|
27
|
+
task :doc => :rdoc
|
28
|
+
|
29
|
+
require 'rspec/core/rake_task'
|
30
|
+
RSpec::Core::RakeTask.new
|
31
|
+
|
32
|
+
task :test => :spec
|
33
|
+
task :default => :spec
|
34
|
+
|
35
|
+
require "bundler/gem_tasks"
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module Ohnoes
|
2
|
+
class FrontendExceptionsController < ActionController::Base
|
3
|
+
skip_before_action :verify_authenticity_token
|
4
|
+
|
5
|
+
def create
|
6
|
+
notice = Hash.from_xml(CGI.unescape(request.body.read))
|
7
|
+
|
8
|
+
error = notice["notice"]["error"]
|
9
|
+
backtrace = error["backtrace"]["line"]
|
10
|
+
|
11
|
+
frontend_exception = FrontendException.new(error["class"], error["message"], backtrace)
|
12
|
+
|
13
|
+
# inlined somewhat from Appsignal::Rack::Listener
|
14
|
+
key = SecureRandom.uuid
|
15
|
+
|
16
|
+
transaction = Appsignal::Transaction.new(key, env)
|
17
|
+
Appsignal.transactions[key] = transaction
|
18
|
+
|
19
|
+
event = ActiveSupport::Notifications::Event.new('frontend_action', Time.now, Time.now, key, raw_payload(env))
|
20
|
+
transaction.set_process_action_event(event)
|
21
|
+
|
22
|
+
|
23
|
+
transaction.add_exception(frontend_exception)
|
24
|
+
transaction.set_tags(frontend_exception: 'frontend_exception')
|
25
|
+
|
26
|
+
def transaction.to_hash
|
27
|
+
FrontendFormatter.new(self).to_hash
|
28
|
+
end
|
29
|
+
|
30
|
+
transaction.complete!
|
31
|
+
|
32
|
+
render json: notice
|
33
|
+
end
|
34
|
+
|
35
|
+
def raw_payload(env)
|
36
|
+
request = ::Rack::Request.new(env)
|
37
|
+
{
|
38
|
+
:controller => "foocontroller",
|
39
|
+
:action => "baraction",
|
40
|
+
:params => request.params,
|
41
|
+
:method => request.request_method,
|
42
|
+
:path => request.path
|
43
|
+
}
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
end
|
data/config/routes.rb
ADDED
@@ -0,0 +1,984 @@
|
|
1
|
+
// Airbrake notifier JavaScript code taken from
|
2
|
+
// https://github.com/airbrake/airbrake-js/blob/v0.1.2-JSON/dist/notifier.js
|
3
|
+
// Parts licenced under BSD license,
|
4
|
+
// Parts licenced under MIT license.
|
5
|
+
//
|
6
|
+
// Airbrake JavaScript Notifier Bundle
|
7
|
+
(function(window, document, undefined) {
|
8
|
+
// Domain Public by Eric Wendelin http://eriwen.com/ (2008)
|
9
|
+
// Luke Smith http://lucassmith.name/ (2008)
|
10
|
+
// Loic Dachary <loic@dachary.org> (2008)
|
11
|
+
// Johan Euphrosine <proppy@aminche.com> (2008)
|
12
|
+
// Øyvind Sean Kinsey http://kinsey.no/blog (2010)
|
13
|
+
// Victor Homyakov (2010)
|
14
|
+
//
|
15
|
+
// Information and discussions
|
16
|
+
// http://jspoker.pokersource.info/skin/test-printstacktrace.html
|
17
|
+
// http://eriwen.com/javascript/js-stack-trace/
|
18
|
+
// http://eriwen.com/javascript/stacktrace-update/
|
19
|
+
// http://pastie.org/253058
|
20
|
+
//
|
21
|
+
// guessFunctionNameFromLines comes from firebug
|
22
|
+
//
|
23
|
+
// Software License Agreement (BSD License)
|
24
|
+
//
|
25
|
+
// Copyright (c) 2007, Parakey Inc.
|
26
|
+
// All rights reserved.
|
27
|
+
//
|
28
|
+
// Redistribution and use of this software in source and binary forms, with or without modification,
|
29
|
+
// are permitted provided that the following conditions are met:
|
30
|
+
//
|
31
|
+
// * Redistributions of source code must retain the above
|
32
|
+
// copyright notice, this list of conditions and the
|
33
|
+
// following disclaimer.
|
34
|
+
//
|
35
|
+
// * Redistributions in binary form must reproduce the above
|
36
|
+
// copyright notice, this list of conditions and the
|
37
|
+
// following disclaimer in the documentation and/or other
|
38
|
+
// materials provided with the distribution.
|
39
|
+
//
|
40
|
+
// * Neither the name of Parakey Inc. nor the names of its
|
41
|
+
// contributors may be used to endorse or promote products
|
42
|
+
// derived from this software without specific prior
|
43
|
+
// written permission of Parakey Inc.
|
44
|
+
//
|
45
|
+
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
|
46
|
+
// IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
47
|
+
// FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
|
48
|
+
// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
49
|
+
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
50
|
+
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
|
51
|
+
// IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
|
52
|
+
// OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
53
|
+
|
54
|
+
/**
|
55
|
+
* Main function giving a function stack trace with a forced or passed in Error
|
56
|
+
*
|
57
|
+
* @cfg {Error} e The error to create a stacktrace from (optional)
|
58
|
+
* @cfg {Boolean} guess If we should try to resolve the names of anonymous functions
|
59
|
+
* @return {Array} of Strings with functions, lines, files, and arguments where possible
|
60
|
+
*/
|
61
|
+
function printStackTrace(options) {
|
62
|
+
var ex = (options && options.e) ? options.e : null;
|
63
|
+
var guess = options ? !!options.guess : true;
|
64
|
+
|
65
|
+
var p = new printStackTrace.implementation();
|
66
|
+
var result = p.run(ex);
|
67
|
+
return (guess) ? p.guessFunctions(result) : result;
|
68
|
+
}
|
69
|
+
|
70
|
+
printStackTrace.implementation = function() {};
|
71
|
+
|
72
|
+
printStackTrace.implementation.prototype = {
|
73
|
+
run: function(ex) {
|
74
|
+
ex = ex ||
|
75
|
+
(function() {
|
76
|
+
try {
|
77
|
+
var _err = __undef__ << 1;
|
78
|
+
} catch (e) {
|
79
|
+
return e;
|
80
|
+
}
|
81
|
+
})();
|
82
|
+
// Use either the stored mode, or resolve it
|
83
|
+
var mode = this._mode || this.mode(ex);
|
84
|
+
if (mode === 'other') {
|
85
|
+
return this.other(arguments.callee);
|
86
|
+
} else {
|
87
|
+
return this[mode](ex);
|
88
|
+
}
|
89
|
+
},
|
90
|
+
|
91
|
+
/**
|
92
|
+
* @return {String} mode of operation for the environment in question.
|
93
|
+
*/
|
94
|
+
mode: function(e) {
|
95
|
+
if (e['arguments']) {
|
96
|
+
return (this._mode = 'chrome');
|
97
|
+
} else if (window.opera && e.stacktrace) {
|
98
|
+
return (this._mode = 'opera10');
|
99
|
+
} else if (e.stack) {
|
100
|
+
return (this._mode = 'firefox');
|
101
|
+
} else if (window.opera && !('stacktrace' in e)) { //Opera 9-
|
102
|
+
return (this._mode = 'opera');
|
103
|
+
}
|
104
|
+
return (this._mode = 'other');
|
105
|
+
},
|
106
|
+
|
107
|
+
/**
|
108
|
+
* Given a context, function name, and callback function, overwrite it so that it calls
|
109
|
+
* printStackTrace() first with a callback and then runs the rest of the body.
|
110
|
+
*
|
111
|
+
* @param {Object} context of execution (e.g. window)
|
112
|
+
* @param {String} functionName to instrument
|
113
|
+
* @param {Function} function to call with a stack trace on invocation
|
114
|
+
*/
|
115
|
+
instrumentFunction: function(context, functionName, callback) {
|
116
|
+
context = context || window;
|
117
|
+
context['_old' + functionName] = context[functionName];
|
118
|
+
context[functionName] = function() {
|
119
|
+
callback.call(this, printStackTrace());
|
120
|
+
return context['_old' + functionName].apply(this, arguments);
|
121
|
+
};
|
122
|
+
context[functionName]._instrumented = true;
|
123
|
+
},
|
124
|
+
|
125
|
+
/**
|
126
|
+
* Given a context and function name of a function that has been
|
127
|
+
* instrumented, revert the function to it's original (non-instrumented)
|
128
|
+
* state.
|
129
|
+
*
|
130
|
+
* @param {Object} context of execution (e.g. window)
|
131
|
+
* @param {String} functionName to de-instrument
|
132
|
+
*/
|
133
|
+
deinstrumentFunction: function(context, functionName) {
|
134
|
+
if (context[functionName].constructor === Function &&
|
135
|
+
context[functionName]._instrumented &&
|
136
|
+
context['_old' + functionName].constructor === Function) {
|
137
|
+
context[functionName] = context['_old' + functionName];
|
138
|
+
}
|
139
|
+
},
|
140
|
+
|
141
|
+
/**
|
142
|
+
* Given an Error object, return a formatted Array based on Chrome's stack string.
|
143
|
+
*
|
144
|
+
* @param e - Error object to inspect
|
145
|
+
* @return Array<String> of function calls, files and line numbers
|
146
|
+
*/
|
147
|
+
chrome: function(e) {
|
148
|
+
return e.stack.replace(/^[^\(]+?[\n$]/gm, '').replace(/^\s+at\s+/gm, '').replace(/^Object.<anonymous>\s*\(/gm, '{anonymous}()@').split('\n');
|
149
|
+
},
|
150
|
+
|
151
|
+
/**
|
152
|
+
* Given an Error object, return a formatted Array based on Firefox's stack string.
|
153
|
+
*
|
154
|
+
* @param e - Error object to inspect
|
155
|
+
* @return Array<String> of function calls, files and line numbers
|
156
|
+
*/
|
157
|
+
firefox: function(e) {
|
158
|
+
return e.stack.replace(/(?:\n@:0)?\s+$/m, '').replace(/^\(/gm, '{anonymous}(').split('\n');
|
159
|
+
},
|
160
|
+
|
161
|
+
/**
|
162
|
+
* Given an Error object, return a formatted Array based on Opera 10's stacktrace string.
|
163
|
+
*
|
164
|
+
* @param e - Error object to inspect
|
165
|
+
* @return Array<String> of function calls, files and line numbers
|
166
|
+
*/
|
167
|
+
opera10: function(e) {
|
168
|
+
var stack = e.stacktrace;
|
169
|
+
var lines = stack.split('\n'), ANON = '{anonymous}',
|
170
|
+
lineRE = /.*line (\d+), column (\d+) in ((<anonymous function\:?\s*(\S+))|([^\(]+)\([^\)]*\))(?: in )?(.*)\s*$/i, i, j, len;
|
171
|
+
for (i = 2, j = 0, len = lines.length; i < len - 2; i++) {
|
172
|
+
if (lineRE.test(lines[i])) {
|
173
|
+
var location = RegExp.$6 + ':' + RegExp.$1 + ':' + RegExp.$2;
|
174
|
+
var fnName = RegExp.$3;
|
175
|
+
fnName = fnName.replace(/<anonymous function\:?\s?(\S+)?>/g, ANON);
|
176
|
+
lines[j++] = fnName + '@' + location;
|
177
|
+
}
|
178
|
+
}
|
179
|
+
|
180
|
+
lines.splice(j, lines.length - j);
|
181
|
+
return lines;
|
182
|
+
},
|
183
|
+
|
184
|
+
// Opera 7.x-9.x only!
|
185
|
+
opera: function(e) {
|
186
|
+
var lines = e.message.split('\n'), ANON = '{anonymous}',
|
187
|
+
lineRE = /Line\s+(\d+).*script\s+(http\S+)(?:.*in\s+function\s+(\S+))?/i,
|
188
|
+
i, j, len;
|
189
|
+
|
190
|
+
for (i = 4, j = 0, len = lines.length; i < len; i += 2) {
|
191
|
+
//TODO: RegExp.exec() would probably be cleaner here
|
192
|
+
if (lineRE.test(lines[i])) {
|
193
|
+
lines[j++] = (RegExp.$3 ? RegExp.$3 + '()@' + RegExp.$2 + RegExp.$1 : ANON + '()@' + RegExp.$2 + ':' + RegExp.$1) + ' -- ' + lines[i + 1].replace(/^\s+/, '');
|
194
|
+
}
|
195
|
+
}
|
196
|
+
|
197
|
+
lines.splice(j, lines.length - j);
|
198
|
+
return lines;
|
199
|
+
},
|
200
|
+
|
201
|
+
// Safari, IE, and others
|
202
|
+
other: function(curr) {
|
203
|
+
var ANON = '{anonymous}', fnRE = /function\s*([\w\-$]+)?\s*\(/i,
|
204
|
+
stack = [], fn, args, maxStackSize = 10;
|
205
|
+
|
206
|
+
while (curr && stack.length < maxStackSize) {
|
207
|
+
fn = fnRE.test(curr.toString()) ? RegExp.$1 || ANON : ANON;
|
208
|
+
args = Array.prototype.slice.call(curr['arguments']);
|
209
|
+
stack[stack.length] = fn + '(' + this.stringifyArguments(args) + ')';
|
210
|
+
curr = curr.caller;
|
211
|
+
}
|
212
|
+
return stack;
|
213
|
+
},
|
214
|
+
|
215
|
+
/**
|
216
|
+
* Given arguments array as a String, subsituting type names for non-string types.
|
217
|
+
*
|
218
|
+
* @param {Arguments} object
|
219
|
+
* @return {Array} of Strings with stringified arguments
|
220
|
+
*/
|
221
|
+
stringifyArguments: function(args) {
|
222
|
+
for (var i = 0; i < args.length; ++i) {
|
223
|
+
var arg = args[i];
|
224
|
+
if (arg === undefined) {
|
225
|
+
args[i] = 'undefined';
|
226
|
+
} else if (arg === null) {
|
227
|
+
args[i] = 'null';
|
228
|
+
} else if (arg.constructor) {
|
229
|
+
if (arg.constructor === Array) {
|
230
|
+
if (arg.length < 3) {
|
231
|
+
args[i] = '[' + this.stringifyArguments(arg) + ']';
|
232
|
+
} else {
|
233
|
+
args[i] = '[' + this.stringifyArguments(Array.prototype.slice.call(arg, 0, 1)) + '...' + this.stringifyArguments(Array.prototype.slice.call(arg, -1)) + ']';
|
234
|
+
}
|
235
|
+
} else if (arg.constructor === Object) {
|
236
|
+
args[i] = '#object';
|
237
|
+
} else if (arg.constructor === Function) {
|
238
|
+
args[i] = '#function';
|
239
|
+
} else if (arg.constructor === String) {
|
240
|
+
args[i] = '"' + arg + '"';
|
241
|
+
}
|
242
|
+
}
|
243
|
+
}
|
244
|
+
return args.join(',');
|
245
|
+
},
|
246
|
+
|
247
|
+
sourceCache: {},
|
248
|
+
|
249
|
+
/**
|
250
|
+
* @return the text from a given URL.
|
251
|
+
*/
|
252
|
+
ajax: function(url) {
|
253
|
+
var req = this.createXMLHTTPObject();
|
254
|
+
if (!req) {
|
255
|
+
return;
|
256
|
+
}
|
257
|
+
req.open('GET', url, false);
|
258
|
+
req.setRequestHeader('User-Agent', 'XMLHTTP/1.0');
|
259
|
+
req.send('');
|
260
|
+
return req.responseText;
|
261
|
+
},
|
262
|
+
|
263
|
+
/**
|
264
|
+
* Try XHR methods in order and store XHR factory.
|
265
|
+
*
|
266
|
+
* @return <Function> XHR function or equivalent
|
267
|
+
*/
|
268
|
+
createXMLHTTPObject: function() {
|
269
|
+
var xmlhttp, XMLHttpFactories = [
|
270
|
+
function() {
|
271
|
+
return new XMLHttpRequest();
|
272
|
+
}, function() {
|
273
|
+
return new ActiveXObject('Msxml2.XMLHTTP');
|
274
|
+
}, function() {
|
275
|
+
return new ActiveXObject('Msxml3.XMLHTTP');
|
276
|
+
}, function() {
|
277
|
+
return new ActiveXObject('Microsoft.XMLHTTP');
|
278
|
+
}
|
279
|
+
];
|
280
|
+
for (var i = 0; i < XMLHttpFactories.length; i++) {
|
281
|
+
try {
|
282
|
+
xmlhttp = XMLHttpFactories[i]();
|
283
|
+
// Use memoization to cache the factory
|
284
|
+
this.createXMLHTTPObject = XMLHttpFactories[i];
|
285
|
+
return xmlhttp;
|
286
|
+
} catch (e) {}
|
287
|
+
}
|
288
|
+
},
|
289
|
+
|
290
|
+
/**
|
291
|
+
* Given a URL, check if it is in the same domain (so we can get the source
|
292
|
+
* via Ajax).
|
293
|
+
*
|
294
|
+
* @param url <String> source url
|
295
|
+
* @return False if we need a cross-domain request
|
296
|
+
*/
|
297
|
+
isSameDomain: function(url) {
|
298
|
+
return url.indexOf(location.hostname) !== -1;
|
299
|
+
},
|
300
|
+
|
301
|
+
/**
|
302
|
+
* Get source code from given URL if in the same domain.
|
303
|
+
*
|
304
|
+
* @param url <String> JS source URL
|
305
|
+
* @return <Array> Array of source code lines
|
306
|
+
*/
|
307
|
+
getSource: function(url) {
|
308
|
+
if (!(url in this.sourceCache)) {
|
309
|
+
this.sourceCache[url] = this.ajax(url).split('\n');
|
310
|
+
}
|
311
|
+
return this.sourceCache[url];
|
312
|
+
},
|
313
|
+
|
314
|
+
guessFunctions: function(stack) {
|
315
|
+
for (var i = 0; i < stack.length; ++i) {
|
316
|
+
var reStack = /\{anonymous\}\(.*\)@(\w+:\/\/([\-\w\.]+)+(:\d+)?[^:]+):(\d+):?(\d+)?/;
|
317
|
+
var frame = stack[i], m = reStack.exec(frame);
|
318
|
+
if (m) {
|
319
|
+
var file = m[1], lineno = m[4]; //m[7] is character position in Chrome
|
320
|
+
if (file && this.isSameDomain(file) && lineno) {
|
321
|
+
var functionName = this.guessFunctionName(file, lineno);
|
322
|
+
stack[i] = frame.replace('{anonymous}', functionName);
|
323
|
+
}
|
324
|
+
}
|
325
|
+
}
|
326
|
+
return stack;
|
327
|
+
},
|
328
|
+
|
329
|
+
guessFunctionName: function(url, lineNo) {
|
330
|
+
try {
|
331
|
+
return this.guessFunctionNameFromLines(lineNo, this.getSource(url));
|
332
|
+
} catch (e) {
|
333
|
+
return 'getSource failed with url: ' + url + ', exception: ' + e.toString();
|
334
|
+
}
|
335
|
+
},
|
336
|
+
|
337
|
+
guessFunctionNameFromLines: function(lineNo, source) {
|
338
|
+
var reFunctionArgNames = /function ([^(]*)\(([^)]*)\)/;
|
339
|
+
var reGuessFunction = /['"]?([0-9A-Za-z_]+)['"]?\s*[:=]\s*(function|eval|new Function)/;
|
340
|
+
// Walk backwards from the first line in the function until we find the line which
|
341
|
+
// matches the pattern above, which is the function definition
|
342
|
+
var line = "", maxLines = 10;
|
343
|
+
for (var i = 0; i < maxLines; ++i) {
|
344
|
+
line = source[lineNo - i] + line;
|
345
|
+
if (line !== undefined) {
|
346
|
+
var m = reGuessFunction.exec(line);
|
347
|
+
if (m && m[1]) {
|
348
|
+
return m[1];
|
349
|
+
} else {
|
350
|
+
m = reFunctionArgNames.exec(line);
|
351
|
+
if (m && m[1]) {
|
352
|
+
return m[1];
|
353
|
+
}
|
354
|
+
}
|
355
|
+
}
|
356
|
+
}
|
357
|
+
return '(?)';
|
358
|
+
}
|
359
|
+
};// Airbrake JavaScript Notifier
|
360
|
+
(function() {
|
361
|
+
"use strict";
|
362
|
+
|
363
|
+
var NOTICE_XML = '<?xml version="1.0" encoding="UTF-8"?>' +
|
364
|
+
'<notice version="2.0">' +
|
365
|
+
'<notifier>' +
|
366
|
+
'<name>ohnoes</name>' +
|
367
|
+
'<version>0.0.1</version>' +
|
368
|
+
'<url>https://github.com/marten/ohnoes</url>' +
|
369
|
+
'</notifier>' +
|
370
|
+
'<error>' +
|
371
|
+
'<class>{exception_class}</class>' +
|
372
|
+
'<message>{exception_message}</message>' +
|
373
|
+
'<backtrace>{backtrace_lines}</backtrace>' +
|
374
|
+
'</error>' +
|
375
|
+
'<request>' +
|
376
|
+
'<url>{request_url}</url>' +
|
377
|
+
'<component>{request_component}</component>' +
|
378
|
+
'<action>{request_action}</action>' +
|
379
|
+
'{request}' +
|
380
|
+
'</request>' +
|
381
|
+
'</notice>',
|
382
|
+
REQUEST_VARIABLE_GROUP_XML = '<{group_name}>{inner_content}</{group_name}>',
|
383
|
+
REQUEST_VARIABLE_XML = '<var key="{key}">{value}</var>',
|
384
|
+
BACKTRACE_LINE_XML = '<line method="{method}" file="{file}" number="{number}" />',
|
385
|
+
Config,
|
386
|
+
Global,
|
387
|
+
Util,
|
388
|
+
_publicAPI,
|
389
|
+
|
390
|
+
NOTICE_JSON = {
|
391
|
+
"version": "2.0",
|
392
|
+
"api-key": "{key}",
|
393
|
+
"notifier": {
|
394
|
+
"name": "airbrake_js",
|
395
|
+
"version": "0.2.0",
|
396
|
+
"url": "http://airbrake.io"
|
397
|
+
},
|
398
|
+
"error": {
|
399
|
+
"class": "{exception_class}",
|
400
|
+
"message": "{exception_message}",
|
401
|
+
"backtrace": []
|
402
|
+
},
|
403
|
+
"request": {
|
404
|
+
"url": "{request_url}",
|
405
|
+
"component": "{request_component}",
|
406
|
+
"action": "{request_action}"
|
407
|
+
},
|
408
|
+
"server-environment": {
|
409
|
+
"project-root": "{project_root}",
|
410
|
+
"environment-name": "{environment}"
|
411
|
+
}
|
412
|
+
};
|
413
|
+
|
414
|
+
Util = {
|
415
|
+
/*
|
416
|
+
* Merge a number of objects into one.
|
417
|
+
*
|
418
|
+
* Usage example:
|
419
|
+
* var obj1 = {
|
420
|
+
* a: 'a'
|
421
|
+
* },
|
422
|
+
* obj2 = {
|
423
|
+
* b: 'b'
|
424
|
+
* },
|
425
|
+
* obj3 = {
|
426
|
+
* c: 'c'
|
427
|
+
* },
|
428
|
+
* mergedObj = Util.merge(obj1, obj2, obj3);
|
429
|
+
*
|
430
|
+
* mergedObj is: {
|
431
|
+
* a: 'a',
|
432
|
+
* b: 'b',
|
433
|
+
* c: 'c'
|
434
|
+
* }
|
435
|
+
*
|
436
|
+
*/
|
437
|
+
merge: (function() {
|
438
|
+
function processProperty (key, dest, src) {
|
439
|
+
if (src.hasOwnProperty(key)) {
|
440
|
+
dest[key] = src[key];
|
441
|
+
}
|
442
|
+
}
|
443
|
+
|
444
|
+
return function() {
|
445
|
+
var objects = Array.prototype.slice.call(arguments),
|
446
|
+
obj,
|
447
|
+
key,
|
448
|
+
result = {};
|
449
|
+
|
450
|
+
while (obj = objects.shift()) {
|
451
|
+
for (key in obj) {
|
452
|
+
processProperty(key, result, obj);
|
453
|
+
}
|
454
|
+
}
|
455
|
+
|
456
|
+
return result;
|
457
|
+
};
|
458
|
+
})(),
|
459
|
+
|
460
|
+
/*
|
461
|
+
* Replace &, <, >, ', " characters with correspondent HTML entities.
|
462
|
+
*/
|
463
|
+
escape: function (text) {
|
464
|
+
return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
465
|
+
.replace(/'/g, ''').replace(/"/g, '"');
|
466
|
+
},
|
467
|
+
|
468
|
+
/*
|
469
|
+
* Remove leading and trailing space characters.
|
470
|
+
*/
|
471
|
+
trim: function (text) {
|
472
|
+
return text.toString().replace(/^\s+/, '').replace(/\s+$/, '');
|
473
|
+
},
|
474
|
+
|
475
|
+
/*
|
476
|
+
* Fill 'text' pattern with 'data' values.
|
477
|
+
*
|
478
|
+
* e.g. Utils.substitute('<{tag}></{tag}>', {tag: 'div'}, true) will return '<div></div>'
|
479
|
+
*
|
480
|
+
* emptyForUndefinedData - a flag, if true, all matched {<name>} without data.<name> value specified will be
|
481
|
+
* replaced with empty string.
|
482
|
+
*/
|
483
|
+
substitute: function (text, data, emptyForUndefinedData) {
|
484
|
+
return text.replace(/{([\w_.-]+)}/g, function(match, key) {
|
485
|
+
return (key in data) ? data[key] : (emptyForUndefinedData ? '' : match);
|
486
|
+
});
|
487
|
+
},
|
488
|
+
|
489
|
+
/*
|
490
|
+
* Perform pattern rendering for an array of data objects.
|
491
|
+
* Returns a concatenation of rendered strings of all objects in array.
|
492
|
+
*/
|
493
|
+
substituteArr: function (text, dataArr, emptyForUndefinedData) {
|
494
|
+
var _i = 0, _l = 0,
|
495
|
+
returnStr = '';
|
496
|
+
|
497
|
+
for (_i = 0, _l = dataArr.length; _i < _l; _i += 1) {
|
498
|
+
returnStr += this.substitute(text, dataArr[_i], emptyForUndefinedData);
|
499
|
+
}
|
500
|
+
|
501
|
+
return returnStr;
|
502
|
+
},
|
503
|
+
|
504
|
+
/*
|
505
|
+
* Add hook for jQuery.fn.on function, to manualy call window.Airbrake.captureException() method
|
506
|
+
* for every exception occurred.
|
507
|
+
*
|
508
|
+
* Let function 'f' be binded as an event handler:
|
509
|
+
*
|
510
|
+
* $(window).on 'click', f
|
511
|
+
*
|
512
|
+
* If an exception is occurred inside f's body, it will be catched here
|
513
|
+
* and forwarded to captureException method.
|
514
|
+
*
|
515
|
+
* processjQueryEventHandlerWrapping is called every time window.Airbrake.setTrackJQ method is used,
|
516
|
+
* if it switches previously setted value.
|
517
|
+
*/
|
518
|
+
processjQueryEventHandlerWrapping: function () {
|
519
|
+
if (Config.options.trackJQ === true) {
|
520
|
+
Config.jQuery_fn_on_original = Config.jQuery_fn_on_original || jQuery.fn.on;
|
521
|
+
|
522
|
+
jQuery.fn.on = function () {
|
523
|
+
var args = Array.prototype.slice.call(arguments),
|
524
|
+
fnArgIdx = 4;
|
525
|
+
|
526
|
+
// Search index of function argument
|
527
|
+
while((--fnArgIdx > -1) && (typeof args[fnArgIdx] !== 'function'));
|
528
|
+
|
529
|
+
// If the function is not found, then subscribe original event handler function
|
530
|
+
if (fnArgIdx === -1) {
|
531
|
+
return Config.jQuery_fn_on_original.apply(this, arguments);
|
532
|
+
}
|
533
|
+
|
534
|
+
// If the function is found, then subscribe wrapped event handler function
|
535
|
+
args[fnArgIdx] = (function (fnOriginHandler) {
|
536
|
+
return function() {
|
537
|
+
try {
|
538
|
+
fnOriginHandler.apply(this, arguments);
|
539
|
+
} catch (e) {
|
540
|
+
Global.captureException(e);
|
541
|
+
}
|
542
|
+
};
|
543
|
+
})(args[fnArgIdx]);
|
544
|
+
|
545
|
+
// Call original jQuery.fn.on, with the same list of arguments, but
|
546
|
+
// a function replaced with a proxy.
|
547
|
+
return Config.jQuery_fn_on_original.apply(this, args);
|
548
|
+
};
|
549
|
+
} else {
|
550
|
+
// Recover original jQuery.fn.on if Config.options.trackJQ is set to false
|
551
|
+
(typeof Config.jQuery_fn_on_original === 'function') && (jQuery.fn.on = Config.jQuery_fn_on_original);
|
552
|
+
}
|
553
|
+
},
|
554
|
+
|
555
|
+
isjQueryPresent: function () {
|
556
|
+
// Currently only 1.7.x version supported
|
557
|
+
return (typeof jQuery === 'function') && ('fn' in jQuery) && ('jquery' in jQuery.fn)
|
558
|
+
&& (jQuery.fn.jquery.indexOf('1.7') === 0)
|
559
|
+
},
|
560
|
+
|
561
|
+
/*
|
562
|
+
* Make first letter in a string capital. e.g. 'guessFunctionName' -> 'GuessFunctionName'
|
563
|
+
* Is used to generate getter and setter method names.
|
564
|
+
*/
|
565
|
+
capitalizeFirstLetter: function (str) {
|
566
|
+
return str[0].toUpperCase() + str.slice(1);
|
567
|
+
},
|
568
|
+
|
569
|
+
/*
|
570
|
+
* Generate public API from an array of specifically formated objects, e.g.
|
571
|
+
*
|
572
|
+
* - this will generate 'setEnvironment' and 'getEnvironment' API methods for configObj.xmlData.environment variable:
|
573
|
+
* {
|
574
|
+
* variable: 'environment',
|
575
|
+
* namespace: 'xmlData'
|
576
|
+
* }
|
577
|
+
*
|
578
|
+
* - this will define 'method' function as 'captureException' API method
|
579
|
+
* {
|
580
|
+
* methodName: 'captureException',
|
581
|
+
* method: (function (...) {...});
|
582
|
+
* }
|
583
|
+
*
|
584
|
+
*/
|
585
|
+
generatePublicAPI: (function () {
|
586
|
+
function _generateSetter (variable, namespace, configObj) {
|
587
|
+
return function (value) {
|
588
|
+
configObj[namespace][variable] = value;
|
589
|
+
};
|
590
|
+
}
|
591
|
+
|
592
|
+
function _generateGetter (variable, namespace, configObj) {
|
593
|
+
return function (value) {
|
594
|
+
return configObj[namespace][variable];
|
595
|
+
};
|
596
|
+
}
|
597
|
+
|
598
|
+
/*
|
599
|
+
* publicAPI: array of specifically formated objects
|
600
|
+
* configObj: inner configuration object
|
601
|
+
*/
|
602
|
+
return function (publicAPI, configObj) {
|
603
|
+
var _i = 0, _m = null, _capitalized = '',
|
604
|
+
returnObj = {};
|
605
|
+
|
606
|
+
for (_i = 0; _i < publicAPI.length; _i += 1) {
|
607
|
+
_m = publicAPI[_i];
|
608
|
+
|
609
|
+
switch (true) {
|
610
|
+
case (typeof _m.variable !== 'undefined') && (typeof _m.methodName === 'undefined'):
|
611
|
+
_capitalized = Util.capitalizeFirstLetter(_m.variable)
|
612
|
+
returnObj['set' + _capitalized] = _generateSetter(_m.variable, _m.namespace, configObj);
|
613
|
+
returnObj['get' + _capitalized] = _generateGetter(_m.variable, _m.namespace, configObj);
|
614
|
+
|
615
|
+
break;
|
616
|
+
case (typeof _m.methodName !== 'undefined') && (typeof _m.method !== 'undefined'):
|
617
|
+
returnObj[_m.methodName] = _m.method
|
618
|
+
|
619
|
+
break;
|
620
|
+
|
621
|
+
default:
|
622
|
+
}
|
623
|
+
}
|
624
|
+
|
625
|
+
return returnObj;
|
626
|
+
};
|
627
|
+
} ())
|
628
|
+
};
|
629
|
+
|
630
|
+
/*
|
631
|
+
* The object to store settings. Allocated from the Global (windows scope) so that users can change settings
|
632
|
+
* only through the methods, rather than through a direct change of the object fileds. So that we can to handle
|
633
|
+
* change settings event (in setter method).
|
634
|
+
*/
|
635
|
+
Config = {
|
636
|
+
xmlData: {
|
637
|
+
environment: 'environment'
|
638
|
+
},
|
639
|
+
|
640
|
+
options: {
|
641
|
+
trackJQ: false, // jQuery.fn.jquery
|
642
|
+
host: 'localhost:3000',
|
643
|
+
errorDefaults: {},
|
644
|
+
guessFunctionName: false,
|
645
|
+
requestType: 'POST', // Can be 'POST' or 'GET'
|
646
|
+
outputFormat: 'XML' // Can be 'XML' or 'JSON'
|
647
|
+
}
|
648
|
+
};
|
649
|
+
|
650
|
+
/*
|
651
|
+
* The public API definition object. If no 'methodName' and 'method' values specified,
|
652
|
+
* getter and setter for 'variable' will be defined.
|
653
|
+
*/
|
654
|
+
_publicAPI = [
|
655
|
+
{
|
656
|
+
variable: 'environment',
|
657
|
+
namespace: 'xmlData'
|
658
|
+
}, {
|
659
|
+
variable: 'key',
|
660
|
+
namespace: 'xmlData'
|
661
|
+
}, {
|
662
|
+
variable: 'host',
|
663
|
+
namespace: 'options'
|
664
|
+
}, {
|
665
|
+
variable: 'errorDefaults',
|
666
|
+
namespace: 'options'
|
667
|
+
}, {
|
668
|
+
variable: 'guessFunctionName',
|
669
|
+
namespace: 'options'
|
670
|
+
}, {
|
671
|
+
variable: 'requestType',
|
672
|
+
namespace: 'options'
|
673
|
+
}, {
|
674
|
+
variable: 'outputFormat',
|
675
|
+
namespace: 'options'
|
676
|
+
}, {
|
677
|
+
methodName: 'setTrackJQ',
|
678
|
+
variable: 'trackJQ',
|
679
|
+
namespace: 'options',
|
680
|
+
method: (function (value) {
|
681
|
+
if (!Util.isjQueryPresent()) {
|
682
|
+
throw Error('Please do not call \'Airbrake.setTrackJQ\' if jQuery does\'t present');
|
683
|
+
}
|
684
|
+
|
685
|
+
value = !!value;
|
686
|
+
|
687
|
+
if (Config.options.trackJQ === value) {
|
688
|
+
return;
|
689
|
+
}
|
690
|
+
|
691
|
+
Config.options.trackJQ = value;
|
692
|
+
|
693
|
+
Util.processjQueryEventHandlerWrapping();
|
694
|
+
})
|
695
|
+
}, {
|
696
|
+
methodName: 'captureException',
|
697
|
+
method: (function (e) {
|
698
|
+
new Notifier().notify({
|
699
|
+
message: e.message,
|
700
|
+
stack: e.stack
|
701
|
+
});
|
702
|
+
})
|
703
|
+
}
|
704
|
+
];
|
705
|
+
|
706
|
+
// Share to global scope as Airbrake ("window.Hoptoad" for backward compatibility)
|
707
|
+
Global = window.Airbrake = window.Hoptoad = Util.generatePublicAPI(_publicAPI, Config);
|
708
|
+
|
709
|
+
function Notifier() {
|
710
|
+
this.options = Util.merge({}, Config.options);
|
711
|
+
this.xmlData = Util.merge(this.DEF_XML_DATA, Config.xmlData);
|
712
|
+
}
|
713
|
+
|
714
|
+
Notifier.prototype = {
|
715
|
+
constructor: Notifier,
|
716
|
+
VERSION: '0.2.0',
|
717
|
+
ROOT: window.location.protocol + '//' + window.location.host,
|
718
|
+
BACKTRACE_MATCHER: /^(.*)\@(.*)\:(\d+)$/,
|
719
|
+
backtrace_filters: [/notifier\.js/],
|
720
|
+
DEF_XML_DATA: {
|
721
|
+
request: {}
|
722
|
+
},
|
723
|
+
|
724
|
+
notify: (function () {
|
725
|
+
function _sendPOSTRequest (url, data) {
|
726
|
+
var request = new XMLHttpRequest();
|
727
|
+
|
728
|
+
request.open('POST', url, true);
|
729
|
+
|
730
|
+
request.send(data);
|
731
|
+
}
|
732
|
+
|
733
|
+
return function (error) {
|
734
|
+
var outputData = '',
|
735
|
+
/*
|
736
|
+
* Should be changed to url = '//' + ...
|
737
|
+
* to use the protocol of current page (http or https)
|
738
|
+
*/
|
739
|
+
url = '/ohnoes/notifier_api/notices';
|
740
|
+
|
741
|
+
switch (this.options['outputFormat']) {
|
742
|
+
case 'XML':
|
743
|
+
outputData = escape(this.generateXML(this.generateDataJSON(error)));
|
744
|
+
|
745
|
+
break;
|
746
|
+
case 'JSON':
|
747
|
+
outputData = JSON.stringify(this.generateJSON(this.generateDataJSON(error)));
|
748
|
+
|
749
|
+
break;
|
750
|
+
default:
|
751
|
+
}
|
752
|
+
|
753
|
+
// switch (this.options['requestType']) {
|
754
|
+
// case 'POST':
|
755
|
+
_sendPOSTRequest(url, outputData);
|
756
|
+
// break;
|
757
|
+
// }
|
758
|
+
};
|
759
|
+
} ()),
|
760
|
+
|
761
|
+
/*
|
762
|
+
* Generate inner JSON representation of exception data that can be rendered as XML or JSON.
|
763
|
+
*/
|
764
|
+
generateDataJSON: (function () {
|
765
|
+
/*
|
766
|
+
* Generate variables array for inputObj object.
|
767
|
+
*
|
768
|
+
* e.g.
|
769
|
+
*
|
770
|
+
* _generateVariables({a: 'a'}) -> [{key: 'a', value: 'a'}]
|
771
|
+
*
|
772
|
+
*/
|
773
|
+
function _generateVariables (inputObj) {
|
774
|
+
var key = '', returnArr = [];
|
775
|
+
|
776
|
+
for (key in inputObj) {
|
777
|
+
if (inputObj.hasOwnProperty(key)) {
|
778
|
+
returnArr.push({
|
779
|
+
key: key,
|
780
|
+
value: inputObj[key]
|
781
|
+
});
|
782
|
+
}
|
783
|
+
}
|
784
|
+
|
785
|
+
return returnArr;
|
786
|
+
}
|
787
|
+
|
788
|
+
/*
|
789
|
+
* Generate Request part of notification.
|
790
|
+
*/
|
791
|
+
function _composeRequestObj (methods, errorObj) {
|
792
|
+
var _i = 0,
|
793
|
+
returnObj = {},
|
794
|
+
type = '';
|
795
|
+
|
796
|
+
for (_i = 0; _i < methods.length; _i += 1) {
|
797
|
+
type = methods[_i];
|
798
|
+
if (typeof errorObj[type] !== 'undefined') {
|
799
|
+
returnObj[type] = _generateVariables(errorObj[type]);
|
800
|
+
}
|
801
|
+
}
|
802
|
+
|
803
|
+
return returnObj;
|
804
|
+
}
|
805
|
+
|
806
|
+
return function (errorWithoutDefaults) {
|
807
|
+
/*
|
808
|
+
* A constructor line:
|
809
|
+
*
|
810
|
+
* this.xmlData = Util.merge(this.DEF_XML_DATA, Config.xmlData);
|
811
|
+
*/
|
812
|
+
var outputData = this.xmlData,
|
813
|
+
error = Util.merge(this.options.errorDefaults, errorWithoutDefaults),
|
814
|
+
|
815
|
+
component = error.component || '',
|
816
|
+
request_url = (error.url || '' + location.hash),
|
817
|
+
|
818
|
+
methods = ['cgi-data', 'params', 'session'],
|
819
|
+
_outputData = null;
|
820
|
+
|
821
|
+
_outputData = {
|
822
|
+
request_url: request_url,
|
823
|
+
request_action: (error.action || ''),
|
824
|
+
request_component: component,
|
825
|
+
request: (function () {
|
826
|
+
if (request_url || component) {
|
827
|
+
error['cgi-data'] = error['cgi-data'] || {};
|
828
|
+
error['cgi-data'].HTTP_USER_AGENT = navigator.userAgent;
|
829
|
+
return Util.merge(outputData.request, _composeRequestObj(methods, error));
|
830
|
+
} else {
|
831
|
+
return {}
|
832
|
+
}
|
833
|
+
} ()),
|
834
|
+
|
835
|
+
project_root: this.ROOT,
|
836
|
+
exception_class: (error.type || 'Error'),
|
837
|
+
exception_message: (error.message || 'Unknown error.'),
|
838
|
+
backtrace_lines: this.generateBacktrace(error)
|
839
|
+
}
|
840
|
+
|
841
|
+
outputData = Util.merge(outputData, _outputData);
|
842
|
+
|
843
|
+
return outputData;
|
844
|
+
};
|
845
|
+
} ()),
|
846
|
+
|
847
|
+
/*
|
848
|
+
* Generate XML notification from inner JSON representation.
|
849
|
+
* NOTICE_XML is used as pattern.
|
850
|
+
*/
|
851
|
+
generateXML: (function () {
|
852
|
+
function _generateRequestVariableGroups (requestObj) {
|
853
|
+
var _group = '',
|
854
|
+
returnStr = '';
|
855
|
+
|
856
|
+
for (_group in requestObj) {
|
857
|
+
if (requestObj.hasOwnProperty(_group)) {
|
858
|
+
returnStr += Util.substitute(REQUEST_VARIABLE_GROUP_XML, {
|
859
|
+
group_name: _group,
|
860
|
+
inner_content: Util.substituteArr(REQUEST_VARIABLE_XML, requestObj[_group], true)
|
861
|
+
}, true);
|
862
|
+
}
|
863
|
+
}
|
864
|
+
|
865
|
+
return returnStr;
|
866
|
+
}
|
867
|
+
|
868
|
+
return function (JSONdataObj) {
|
869
|
+
JSONdataObj.request = _generateRequestVariableGroups(JSONdataObj.request);
|
870
|
+
JSONdataObj.backtrace_lines = Util.substituteArr(BACKTRACE_LINE_XML, JSONdataObj.backtrace_lines, true);
|
871
|
+
|
872
|
+
return Util.substitute(NOTICE_XML, JSONdataObj, true);
|
873
|
+
};
|
874
|
+
} ()),
|
875
|
+
|
876
|
+
/*
|
877
|
+
* Generate JSON notification from inner JSON representation.
|
878
|
+
* NOTICE_JSON is used as pattern.
|
879
|
+
*/
|
880
|
+
generateJSON: function (JSONdataObj) {
|
881
|
+
// Pattern string is JSON.stringify(NOTICE_JSON)
|
882
|
+
// The rendered string is parsed back as JSON.
|
883
|
+
var outputJSON = JSON.parse(Util.substitute(JSON.stringify(NOTICE_JSON), JSONdataObj, true));
|
884
|
+
|
885
|
+
outputJSON.request = Util.merge(outputJSON.request, JSONdataObj.request);
|
886
|
+
outputJSON.error.backtrace = JSONdataObj.backtrace_lines;
|
887
|
+
|
888
|
+
return outputJSON;
|
889
|
+
},
|
890
|
+
|
891
|
+
generateBacktrace: function (error) {
|
892
|
+
var backtrace = [],
|
893
|
+
file,
|
894
|
+
i,
|
895
|
+
matches,
|
896
|
+
stacktrace;
|
897
|
+
|
898
|
+
error = error || {};
|
899
|
+
|
900
|
+
if (typeof error.stack !== 'string') {
|
901
|
+
try {
|
902
|
+
(0)();
|
903
|
+
} catch (e) {
|
904
|
+
error.stack = e.stack;
|
905
|
+
}
|
906
|
+
}
|
907
|
+
|
908
|
+
stacktrace = this.getStackTrace(error);
|
909
|
+
|
910
|
+
for (i = 0; i < stacktrace.length; i++) {
|
911
|
+
matches = stacktrace[i].match(this.BACKTRACE_MATCHER);
|
912
|
+
|
913
|
+
if (matches && this.validBacktraceLine(stacktrace[i])) {
|
914
|
+
file = matches[2].replace(this.ROOT, '[PROJECT_ROOT]');
|
915
|
+
|
916
|
+
if (i === 0 && matches[2].match(document.location.href)) {
|
917
|
+
// backtrace.push('<line method="" file="internal: " number=""/>');
|
918
|
+
|
919
|
+
backtrace.push({
|
920
|
+
method: '',
|
921
|
+
file: 'internal: ',
|
922
|
+
number: ''
|
923
|
+
});
|
924
|
+
}
|
925
|
+
|
926
|
+
// backtrace.push('<line method="' + Util.escape(matches[1]) + '" file="' + Util.escape(file) +
|
927
|
+
// '" number="' + matches[3] + '" />');
|
928
|
+
|
929
|
+
backtrace.push({
|
930
|
+
method: matches[1],
|
931
|
+
file: file,
|
932
|
+
number: matches[3]
|
933
|
+
});
|
934
|
+
}
|
935
|
+
}
|
936
|
+
|
937
|
+
return backtrace;
|
938
|
+
},
|
939
|
+
|
940
|
+
getStackTrace: function (error) {
|
941
|
+
var i,
|
942
|
+
stacktrace = printStackTrace({
|
943
|
+
e: error,
|
944
|
+
guess: this.options.guessFunctionName
|
945
|
+
});
|
946
|
+
|
947
|
+
for (i = 0; i < stacktrace.length; i++) {
|
948
|
+
if (stacktrace[i].match(/\:\d+$/)) {
|
949
|
+
continue;
|
950
|
+
}
|
951
|
+
|
952
|
+
if (stacktrace[i].indexOf('@') === -1) {
|
953
|
+
stacktrace[i] += '@unsupported.js';
|
954
|
+
}
|
955
|
+
|
956
|
+
stacktrace[i] += ':0';
|
957
|
+
}
|
958
|
+
|
959
|
+
return stacktrace;
|
960
|
+
},
|
961
|
+
|
962
|
+
validBacktraceLine: function (line) {
|
963
|
+
for (var i = 0; i < this.backtrace_filters.length; i++) {
|
964
|
+
if (line.match(this.backtrace_filters[i])) {
|
965
|
+
return false;
|
966
|
+
}
|
967
|
+
}
|
968
|
+
|
969
|
+
return true;
|
970
|
+
}
|
971
|
+
};
|
972
|
+
|
973
|
+
window.onerror = function (message, file, line) {
|
974
|
+
setTimeout(function () {
|
975
|
+
new Notifier().notify({
|
976
|
+
message: message,
|
977
|
+
stack: '()@' + file + ':' + line
|
978
|
+
});
|
979
|
+
}, 0);
|
980
|
+
|
981
|
+
return true;
|
982
|
+
};
|
983
|
+
})();
|
984
|
+
})(window, document);
|