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.
@@ -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
@@ -0,0 +1,4 @@
1
+ lib/**/*.rb
2
+ README.rdoc
3
+ ChangeLog.rdoc
4
+ LICENSE.txt
@@ -0,0 +1,4 @@
1
+ Gemfile.lock
2
+ html/
3
+ pkg/
4
+ vendor/cache/*.gem
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --colour --format documentation
@@ -0,0 +1,4 @@
1
+ === 0.1.0 / ...
2
+
3
+ * Initial release:
4
+
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
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.
@@ -0,0 +1,37 @@
1
+ # Pavlov [![Build Status](https://api.travis-ci.org/marten/ohnoes.png?branch=master)](http://travis-ci.org/marten/ohnoes) [![Gem Version](https://badge.fury.io/rb/ohnoes.png)](http://badge.fury.io/rb/ohnoes) [![Dependency Status](https://gemnasium.com/marten/ohnoes.png)](https://gemnasium.com/marten/ohnoes) [![Code Climate](https://codeclimate.com/github/marten/ohnoes.png)](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.
@@ -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
@@ -0,0 +1,4 @@
1
+ Ohnoes::Engine.routes.draw do
2
+ post 'notifier_api/notices' => 'frontend_exceptions#create'
3
+
4
+ end
@@ -0,0 +1,2 @@
1
+ //= require ohnoes/notifier
2
+
@@ -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, '&#38;').replace(/</g, '&#60;').replace(/>/g, '&#62;')
465
+ .replace(/'/g, '&#39;').replace(/"/g, '&#34;');
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);