Skip to content

Add loader + integration tests #1559

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Sep 24, 2018
Merged
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
8 changes: 4 additions & 4 deletions packages/browser/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,10 @@
"fix": "run-s fix:tslint fix:prettier",
"fix:prettier": "prettier --write '{src,test}/**/*.ts'",
"fix:tslint": "tslint --fix -t stylish -p .",
"test": "karma start karma/karma.unit.config.js",
"test:watch": "karma start karma/karma.unit.config.js --auto-watch --no-single-run",
"test:integration": "karma start karma/karma.integration.config.js",
"test:integration:watch": "karma start karma/karma.integration.config.js --auto-watch --no-single-run",
"test": "karma start test/karma/karma.unit.config.js",
"test:watch": "karma start test/karma/karma.unit.config.js --auto-watch --no-single-run",
"test:integration": "karma start test/karma/karma.integration.config.js",
"test:integration:watch": "karma start test/karma/karma.integration.config.js --auto-watch --no-single-run",
"size:check": "cat build/bundle.min.js | gzip -9 | wc -c | awk '{$1=$1/1024; print $1,\"kB\";}'",
"version": "node ../../scripts/versionbump.js src/version.ts"
},
Expand Down
203 changes: 203 additions & 0 deletions packages/browser/src/loader.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
(function(
_window,
_document,
_script,
_onerror,
_onunhandledrejection,
_namespace,
_publicKey,
_sdkBundleUrl,
_config,
) {
var lazy = true;
var forceLoad = false;

for (var i = 0; i < document.scripts.length; i++) {
if (document.scripts[i].src.indexOf(_publicKey) > -1) {
lazy = !(document.scripts[i].dataset.lazy === 'no');
break;
}
}

var injected = false;
var onLoadCallback;

// Create a namespace and attach function that will store captured exception
// Because functions are also objects, we can attach the queue itself straight to it and save some bytes
var queue = function(content) {
// content.e = error
// content.p = promise rejection
// content.f = function call the Sentry
if (
(content.e ||
content.p ||
(content.f && content.f.indexOf('capture') > -1) ||
(content.f && content.f.indexOf('showReportDialog') > -1)) &&
lazy
) {
// We only want to lazy inject/load the sdk bundle if
// an error or promise rejection occured
// OR someone called `capture...` on the SDK
injectSdk(onLoadCallback);
}
queue.data.push(content);
};
queue.data = [];

function injectSdk(callback) {
if (injected) {
return;
}
injected = true;

// Create a `script` tag with provided SDK `url` and attach it just before the first, already existing `script` tag
// Scripts that are dynamically created and added to the document are async by default,
// they don't block rendering and execute as soon as they download, meaning they could
// come out in the wrong order. Because of that we don't need async=1 as GA does.
// it was probably(?) a legacy behavior that they left to not modify few years old snippet
// https://www.html5rocks.com/en/tutorials/speed/script-loading/
var _currentScriptTag = _document.getElementsByTagName(_script)[0];
var _newScriptTag = _document.createElement(_script);
_newScriptTag.src = _sdkBundleUrl;
_newScriptTag.crossorigin = 'anonymous';

// Once our SDK is loaded
_newScriptTag.addEventListener('load', function() {
try {
// Restore onerror/onunhandledrejection handlers
_window[_onerror] = _oldOnerror;
_window[_onunhandledrejection] = _oldOnunhandledrejection;

var SDK = _window[_namespace];

var oldInit = SDK.init;

// Configure it using provided DSN and config object
SDK.init = function(options) {
var target = _config;
for (var key in options) {
if (Object.prototype.hasOwnProperty.call(options, key)) {
target[key] = options[key];
}
}
oldInit(target);
};

sdkLoaded(callback, SDK);
} catch (o_O) {
console.error(o_O);
}
});

_currentScriptTag.parentNode.insertBefore(_newScriptTag, _currentScriptTag);
}

function sdkLoaded(callback, SDK) {
try {
if (callback) {
callback();
}
var data = queue.data;

// We want to replay all calls to Sentry first to make sure init is called before
// we call all our internal error handlers
var firstInitCall = false;
var calledSentry = false;
for (var i = 0; i < data.length; i++) {
if (data[i].f) {
calledSentry = true;
var call = data[i];
if (firstInitCall === false && call.f !== 'init') {
// First call always has to be init, this is a conveniece for the user
// so call to init is optional
SDK.init();
}
firstInitCall = true;
SDK[call.f].apply(SDK, call.a);
}
}
if (calledSentry === false) {
// Sentry has never been called but we need Sentry.init() so call it
SDK.init();
}
// Because we installed the SDK, at this point we have an access to TraceKit's handler,
// which can take care of browser differences (eg. missing exception argument in onerror)
var tracekitErrorHandler = _window[_onerror];

// And now capture all previously caught exceptions
for (var i = 0; i < data.length; i++) {
if (data[i].e) {
tracekitErrorHandler.apply(_window, data[i].e);
} else if (data[i].p) {
SDK.captureException(data[i].p);
}
}
} catch (o_O) {
console.error(o_O);
}
}

// We don't want to _window.Sentry = _window.Sentry || { ... } since we want to make sure
// that the first Sentry "instance" is our with onLoad
_window[_namespace] = {
onLoad: function(callback) {
if (lazy && !forceLoad) {
onLoadCallback = callback;
} else {
injectSdk(callback);
}
},
forceLoad: function() {
forceLoad = true;
if (lazy) {
setTimeout(function() {
injectSdk(onLoadCallback);
});
}
},
};

[
'init',
'addBreadcrumb',
'captureMessage',
'captureException',
'captureEvent',
'configureScope',
'withScope',
'showReportDialog',
].forEach(function(f) {
_window[_namespace][f] = function() {
queue({ f: f, a: arguments });
};
});

// Store reference to the old `onerror` handler and override it with our own function
// that will just push exceptions to the queue and call through old handler if we found one
var _oldOnerror = _window[_onerror];
_window[_onerror] = function(message, source, lineno, colno, exception) {
// Use keys as "data type" to save some characters"
queue({
e: [].slice.call(arguments),
});

if (_oldOnerror) _oldOnerror.apply(_window, arguments);
};

// Do the same store/queue/call operations for `onunhandledrejection` event
var _oldOnunhandledrejection = _window[_onunhandledrejection];
_window[_onunhandledrejection] = function(exception) {
queue({
p: exception.reason,
});
if (_oldOnunhandledrejection) _oldOnunhandledrejection.apply(_window, arguments);
};

if (!lazy) {
setTimeout(function() {
injectSdk(onLoadCallback);
});
}
})(window, document, 'script', 'onerror', 'onunhandledrejection', 'Sentry', 'loader.js', '../../build/bundle.js', {
dsn: 'https://public@example.com/1',
});
80 changes: 80 additions & 0 deletions packages/browser/test/integration/common.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/**
* requestAnimationFrame polyfill by Erik Möller. fixes from Paul Irish and Tino Zijdel
*
* http://paulirish.com/2011/requestanimationframe-for-smart-animating/
* http://my.opera.com/emoller/blog/2011/12/20/requestanimationframe-for-smart-er-animating
*
* MIT license
*/
(function() {
var lastTime = 0;
var vendors = ['ms', 'moz', 'webkit', 'o'];
for (var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
window.requestAnimationFrame = window[vendors[x] + 'RequestAnimationFrame'];
window.cancelAnimationFrame =
window[vendors[x] + 'CancelAnimationFrame'] || window[vendors[x] + 'CancelRequestAnimationFrame'];
}

if (!window.requestAnimationFrame)
window.requestAnimationFrame = function(callback, element) {
var currTime = new Date().getTime();
var timeToCall = Math.max(0, 16 - (currTime - lastTime));
var id = window.setTimeout(function() {
callback(currTime + timeToCall);
}, timeToCall);
lastTime = currTime + timeToCall;
return id;
};

if (!window.cancelAnimationFrame)
window.cancelAnimationFrame = function(id) {
clearTimeout(id);
};
})();

/**
* DOM4 MouseEvent and KeyboardEvent Polyfills
*
* References:
* https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/MouseEvent
* https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/MouseEvent#Polyfill
* https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/KeyboardEvent
*/
(function() {
try {
new MouseEvent('click');
return false; // No need to polyfill
} catch (e) {
// Need to polyfill - fall through
}

var MouseEvent = function(eventType) {
var mouseEvent = document.createEvent('MouseEvent');
mouseEvent.initMouseEvent(eventType, true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
return mouseEvent;
};

MouseEvent.prototype = Event.prototype;
window.MouseEvent = MouseEvent;
})();

(function() {
try {
new KeyboardEvent('keypress');
return false; // No need to polyfill
} catch (e) {
// Need to polyfill - fall through
}

var KeyboardEvent = function(eventType) {
var keyboardEvent = document.createEvent('KeyboardEvent');
if (keyboardEvent.initKeyboardEvent)
keyboardEvent.initKeyboardEvent(eventType, true, true, window, false, false, false, false, 'a', 0);
if (keyboardEvent.initKeyEvent)
keyboardEvent.initKeyEvent(eventType, true, true, window, false, false, false, false, 'a');
return keyboardEvent;
};

KeyboardEvent.prototype = Event.prototype;
window.KeyboardEvent = KeyboardEvent;
})();
Loading