Skip to content
This repository was archived by the owner on Jan 21, 2024. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
.idea/
node_modules/
dist/
closure/
TraceKit.iml
38 changes: 29 additions & 9 deletions Gruntfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,31 @@ module.exports = function (grunt) {

// Project configuration.
grunt.initConfig({
'closureCompiler': {
clean: {
dist: ['dist']
},
closureCompiler: {
options: {
compilerFile: './closure/compiler.jar',
checkModified: true,
compilerOpts: {
compilation_level: 'ADVANCED_OPTIMIZATIONS',
compilation_level: 'SIMPLE_OPTIMIZATIONS',
warning_level: 'verbose',
jscomp_off: ['checkTypes', 'fileoverviewTags'],
summary_detail_level: 3,
output_wrapper: '"(function(){%output%}).call(this);"'
summary_detail_level: 3
},
execOpts: {
maxBuffer: 200 * 1024
}

},
'compile': {
compile: {
src: './tracekit.js',
dest: './tracekit.min.js'
dest: './dist/tracekit.noplugins.min.js'
},
compileWithPlugins: {
src: ['./tracekit.js', './plugins/*.js'],
dest: './dist/tracekit.min.js'
}
},
jshint: {
Expand Down Expand Up @@ -59,14 +65,28 @@ module.exports = function (grunt) {
ActiveXObject: false
},
lint: {
src: ['grunt.js', 'tracekit.js']
src: ['Gruntfile.js', 'tracekit.js', './plugins/*.js']
}
},
connect: {
test: {
options: {
port: 9001,
open: 'http://localhost:9001/tests/testBuild.html',
keepalive: true
}
}
}
});

grunt.loadNpmTasks('grunt-contrib-clean');
grunt.loadNpmTasks('grunt-contrib-connect');
grunt.loadNpmTasks('grunt-contrib-jshint');
grunt.loadNpmTasks('grunt-closure-tools');

grunt.registerTask('default', ['jshint:lint', 'closureCompiler:compile']);
grunt.registerTask('travis', ['jshint:lint', 'closureCompiler:compile']);
grunt.registerTask('default', ['clean', 'jshint:lint', 'closureCompiler']);
grunt.registerTask('travis', ['clean', 'jshint:lint', 'closureCompiler']);

// Launch the recursion test
grunt.registerTask('test', ['default', 'connect:test']);
};
35 changes: 34 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ bower install tracekit
```
This places TraceKit at `components/tracekit/tracekit.js`. Install [bower](http://twitter.github.com/bower/): `npm install bower -g`, download npm with Node: http://nodejs.org

Then include the `<script>` to your page
Then include the `<script>` in your page.

## Usage

Expand Down Expand Up @@ -74,6 +74,39 @@ TraceKit.collectWindowErrors = false;

View the source for more details and examples.

## Plugins

Because most browsers support a [very limited window.onerror](https://bugzilla.mozilla.org/show_bug.cgi?id=355430) that
does not pass an actual stack trace, TraceKit works around this with `TraceKit.wrap`. This is an easy mechanism for
wrapping async functions. Previous versions of TraceKit wrapped jQuery event handlers and setTimeout/setInterval by default.
That code is now in the plugins folder and can be used at will.

In most applications, you'll want to include all plugins to ensure full stacktraces for as many errors as possible.

Thankfully, in the newest version of the HTML5 Spec, and as of early September 2013, Chrome Canary [supports an
extended window.onerror signature](https://code.google.com/p/chromium/issues/detail?id=147127) that passes an actual
Error object along. In this case, TraceKit has the function `TraceKit.supportsExtendedWindowOnError`, which calls back
with a boolean. If true, `window.onerror` has superpowers and wrapping plugins should halt as their functionality
is no longer needed.

## Building

To get minified versions of source, clone the project, and:

```bash
npm install
wget http://closure-compiler.googlecode.com/files/compiler-latest.zip
unzip compiler-latest.zip -d closure
grunt
```

Built files will be stored in /dist.

`tracekit.noplugins.min.js` does not wrap setTimeout/setInterval or jQuery.

`tracekit.min.js` contains setTimeout/setInterval & jQuery wrapping. If you need one but not the other,
include the file in /plugins manually.

![Stacktrace or GTFO](http://i.imgur.com/jacoj.jpg)

## Contributing
Expand Down
26 changes: 14 additions & 12 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
{
"name": "TraceKit",
"version": "0.1.0",
"devDependencies": {
"grunt": "~0.4.1",
"grunt-contrib-jshint": "~0.6.2",
"grunt-cli": "~0.1.9",
"grunt-closure-tools": "~0.8.3"
},
"scripts": {
"test": "grunt --verbose travis"
}
}
"name": "TraceKit",
"version": "0.1.0",
"devDependencies": {
"grunt": "~0.4.1",
"grunt-contrib-jshint": "~0.6.2",
"grunt-cli": "~0.1.9",
"grunt-closure-tools": "~0.8.3",
"grunt-contrib-clean": "~0.5.0",
"grunt-contrib-connect": "~0.5.0"
},
"scripts": {
"test": "grunt --verbose travis"
}
}
34 changes: 34 additions & 0 deletions plugins/wrapAsync.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/**
* Extends support for global error handling for asynchronous browser
* functions. Adopted from Closure Library's errorhandler.js
*/
(function extendToAsynchronousCallbacks(window) {

// Bail out if window.onerror can do this for us.
if (window.TraceKit.supportsExtendedWindowOnError()) {
return;
}

var _helper = function _helper(fnName) {
var originalFn = window[fnName];
window[fnName] = function traceKitAsyncExtension() {
// Make a copy of the arguments
var args = [].slice.call(arguments);
var originalCallback = args[0];
if (typeof (originalCallback) === 'function') {
args[0] = window.TraceKit.wrap(originalCallback);
}
// IE < 9 doesn't support .call/.apply on setInterval/setTimeout, but it
// also only supports 2 argument and doesn't care what "this" is, so we
// can just call the original function directly.
if (originalFn.apply) {
return originalFn.apply(this, args);
} else {
return originalFn(args[0], args[1]);
}
};
};

_helper('setTimeout');
_helper('setInterval');
}(this));
82 changes: 82 additions & 0 deletions plugins/wrapJquery.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/**
* Extended support for backtraces and global error handling for most
* asynchronous jQuery functions.
*/
(function traceKitAsyncForjQuery(window) {

// quit if jQuery or TraceKit isn't on the page
var $ = window.$;
var TraceKit = window.TraceKit;
if (!$ || !TraceKit) {
if (window.console && !TraceKit.suppressWarnings) {
window.console.warn('Unable to load TraceKit jQuery plugin: TraceKit or jQuery is not available ' +
'at the time of invocation.');
}
return;
}
// Bail out if window.onerror can do this work for us
if (window.TraceKit.supportsExtendedWindowOnError()) {
return;
}

var _oldEventAdd = $.event.add;
$.event.add = function traceKitEventAdd(elem, types, handler, data, selector) {
var _handler;

if (handler.handler) {
_handler = handler.handler;
handler.handler = TraceKit.wrap(handler.handler);
} else {
_handler = handler;
handler = TraceKit.wrap(handler);
}

// If the handler we are attaching doesn’t have the same guid as
// the original, it will never be removed when someone tries to
// unbind the original function later. Technically as a result of
// this our guids are no longer globally unique, but whatever, that
// never hurt anybody RIGHT?!
if (_handler.guid) {
handler.guid = _handler.guid;
} else {
handler.guid = _handler.guid = $.guid++;
}

return _oldEventAdd.call(this, elem, types, handler, data, selector);
};

var _oldReady = $.fn.ready;
$.fn.ready = function traceKitjQueryReadyWrapper(fn) {
return _oldReady.call(this, TraceKit.wrap(fn));
};

var _oldAjax = $.ajax;
$.ajax = function traceKitAjaxWrapper(url, options) {
var keys = ['complete', 'error', 'success'], key;

// Taken from https://github.com/jquery/jquery/blob/eee2eaf1d7a189d99106423a4206c224ebd5b848/src/ajax.js#L311-L318
// If url is an object, simulate pre-1.5 signature
if (typeof url === 'object') {
options = url;
url = undefined;
}

// Force options to be an object
options = options || {};

while(key = keys.pop()) {
if ($.isFunction(options[key])) {
options[key] = TraceKit.wrap(options[key]);
}
}

try {
return _oldAjax.call(this, url, options);
} catch (e) {
TraceKit.report(e);
throw e;
}
};


}(this));
70 changes: 70 additions & 0 deletions tests/newOnError.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<!--
A simple test for extended window.onerror support.
See: https://mikewest.org/2013/08/debugging-runtime-errors-with-window-onerror
-->
<!doctype html>
<html>
<head>
<title>TraceKit OnError Test</title>
<script src="../tracekit.js"></script>
<script>
setupTraceKit()
function setupTraceKit(){
function indent(text){
return text.split('\n').map(function(line){
return ' ' + line
}).join('\n')
}

TraceKit.report.subscribe(function(stackInfo){
var out = [];
out.push(stackInfo.name + ' : ' + stackInfo.message)
var stack = stackInfo.stack
if (!stack || stack.length === 0) return
if (stack[0].context){
out.push(indent(stack[0].context.join('\n')))
}
for (var i = 0; i < stack.length; i++){
var line = stack[i]
out.push(' at ' + line.func + ' (' + [line.url, line.line, line.column].join(':') + ')')
}

// Append to page
var output = document.getElementById('errorOutput');
var innerContentWrapper = document.createElement('pre');
var innerContent = document.createElement('li');
innerContent.innerHTML = escapeHtml(out.join('\n'));
innerContentWrapper.appendChild(innerContent);
output.appendChild(innerContentWrapper);
})
}
var entityMap = {
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
'"': '&quot;',
"'": '&#39;',
"/": '&#x2F;'
};

function escapeHtml(string) {
return String(string).replace(/[&<>"'\/]/g, function (s) {
return entityMap[s];
});
}
// Throw a delayed error
setTimeout(function(){throw new Error('Delayed Error');}, 500);

// Throw an additional error if extended window.onerror is not supported.
TraceKit.supportsExtendedWindowOnError(function(supported) {
if (!supported) throw new Error("Extended window.onerror is not supported.");
});
</script>
</head>
<body>
<h1>TraceKit OnError Test</h1>
<h4>If your browser supports the new onError signature, you should see column numbers on traces
and headers starting with 'Error', not 'undefined'.</h4>
<div id="errorOutput"></div>
</body>
</html>
2 changes: 1 addition & 1 deletion tests/recursion.html
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
<script>
describe('crash', function(){

it('it should not go into an infinite loop', function(){
it('it should not go into an infinite loop - if you see output here, it worked correctly.', function(){
throw new Error('Boom!');
});

Expand Down
Loading