Skip to content

Instantly share code, notes, and snippets.

@maxmilton
Last active April 27, 2020 15:43
Show Gist options
  • Save maxmilton/e2338b02b7381fc7bef2ccd96f434201 to your computer and use it in GitHub Desktop.
Save maxmilton/e2338b02b7381fc7bef2ccd96f434201 to your computer and use it in GitHub Desktop.
How to load the Sentry JavaScript error tracking script 'raven.js' asynchronously and still capture errors before the script is loaded.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Asynchronous Sentry JavaScript Error Tracking</title>
<!-- putting your CSS near the top is always a good idea -->
<link rel="stylesheet" href="/app.css"></link>
<!--
load raven.js asynchronously
NOTE: crossorigin is recommended on any external scripts so you can capture
errors they throw correctly and to prevent sending credentials when fetching.
-->
<script async src="https://cdn.ravenjs.com/3.22.0/raven.min.js" crossorigin id="raven"></script>
<!--
resolve sentry.io DNS and do TLS+TCP handshakes so the first request is faster
NOTE: If you're using the self-hosted version of Sentry then replace this URL
with your sentry endpoint.
-->
<link rel="preconnect" href="https://sentry.io">
</head>
<body>
<div id="app"></div>
<!-- Javascript error tracking with Sentry -->
<script>
/* global Raven *//* eslint-disable func-names, strict, no-param-reassign, no-multi-assign, prefer-arrow-callback */
(function (window, queue, loaded, script) {
'use strict';
/**
* Optionally expose the loaded state and error queue globally to push
* errors from your app.
*/
// window.sentry.loaded = loaded;
// window.sentry.queue = queue;
// capture and store any errors before raven.js is loaded
window.onerror = function e(message, file, lineNo, columnNo, error) {
if (loaded) return;
queue.push([message, file, lineNo, columnNo, error]);
};
/**
* raven.js doesn't capture unhandled Promise rejections by default due
* to limited compatibility; only Chrome 49+ and some promise libraries
* (e.g. core-js, bluebird) are known to trigger this event.
*/
window.onunhandledrejection = function e(error) {
if (loaded) return;
queue.push([
error.reason.reason || error.reason.message,
error.type,
JSON.stringify(error.reason),
]);
};
/**
* Optionally also track handled Promise rejections (useful for development
* or testing environments). Replace the above onunhandledrejection with:
*/
// window.onunhandledrejection = window.onunhandledrejection =
// function e(error) {
// if (loaded) return;
// queue.push([
// error.reason.reason || error.reason.message,
// error.type,
// JSON.stringify(error.reason),
// ]);
// };
// once raven.js is loaded then install
script.onreadystatechange = script.onload = function () {
if (loaded) return;
loaded = true;
Raven.config('___PUBLIC_DSN___', {
environment: '<%= process.env.NODE_ENV %>', // if the page is a lodash template then the value is injected
release: '<%= process.env.APP_RELEASE %>', // same as above, these are just an example
tags: { app: 'example' },
}).install();
// same compatibility caveats as the above Promise error events
window.onunhandledrejection = function e(error) {
Raven.captureException(new Error(error.reason.reason || error.reason.message), {
extra: {
type: error.type,
reason: JSON.stringify(error.reason),
},
});
};
/** Alternatively also track handled Promise rejections: */
// window.onrejectionhandled = window.onunhandledrejection =
// function e(error) {
// Raven.captureException(new Error(error.reason.reason || error.reason.message), {
// extra: {
// type: error.type,
// reason: JSON.stringify(error.reason),
// },
// });
// };
// report any previously stored errors
queue.forEach(function (error) {
/**
* error[0] = message
* error[1] = file, url, or type
* error[2] = line number or event
* error[3] = column number (old browsers may not emit this)
* error[4] = Error Object (old browsers may not emit this either!)
*/
Raven.captureException(error[4] || new Error(error[0]), {
extra: {
file: error[1],
line: error[2],
col: error[3],
},
});
});
};
}(window, [], false, document.getElementById('raven')));
</script>
<!-- your JavaScript should come after the above code so we can its track errors -->
<script src="/vendor.js"></script>
<script src="/app.js"></script>
</body>
</html>
@maxmilton
Copy link
Author

maxmilton commented Jan 14, 2018

If you prefer a quick copy+paste, here's the minified version:

<!-- Sentry JS error tracking -->
<script async src="https://cdn.ravenjs.com/3.22.0/raven.min.js" crossorigin id="raven"></script>
<script>
(function(b,e,c,d){b.onerror=function(a,b,d,f,g){c||e.push([a,b,d,f,g])};b.onunhandledrejection=function(a){c||e.push([a.reason.reason||a.reason.message,a.type,JSON.stringify(a.reason)])};d.onreadystatechange=d.onload=function(){c||(c=!0,
Raven.config("___PUBLIC_DSN___").install(),
b.onunhandledrejection=function(a){Raven.captureException(Error(a.reason.reason||a.reason.message),{extra:{type:a.type,reason:JSON.stringify(a.reason)}})},e.forEach(function(a){Raven.captureException(a[4]||Error(a[0]),{extra:{file:a[1],line:a[2],col:a[3]}})}))}})(window,[],!1,document.getElementById("raven"));
</script>

<link rel="preconnect" href="https://sentry.io">

Replace ___PUBLIC_DSN___ with your DSN and paste this somewhere in the head near your closing </head> tag. Or if you're a hipster who doesn't use <head> and <body> tags anymore then just paste it near the top after any critical/app resources (e.g. CSS). Ideally it should be before any other JavaScript so you can capture errors from scripts loaded after this code.

@maxmilton
Copy link
Author

maxmilton commented Jan 15, 2018

For my future reference, here's the Google Closure Compiler service + the settings used for the minified version:

https://closure-compiler.appspot.com/home#code%3D%252F%252F%2520%253D%253DClosureCompiler%253D%253D%250A%252F%252F%2520%2540compilation_level%2520SIMPLE_OPTIMIZATIONS%250A%252F%252F%2520%2540output_file_name%2520ravenjs-async.min.js%250A%252F%252F%2520%2540js_externs%2520var%2520Raven%2520%253D%2520%257B%257D%253B%2520Raven.config%2520%253D%2520function%2520(text)%257B%257D%253B%2520Raven.captureException%2520%253D%2520function%2520(err%252C%2520object)%257B%257D%253B%250A%252F%252F%2520%253D%253D%252FClosureCompiler%253D%253D%250A%250A(function%2520(window%252C%2520queue%252C%2520loaded%252C%2520script)%2520%257B%250A%2520%2520'use%2520strict'%253B%250A%250A%2520%2520window.onerror%2520%253D%2520function%2520e(message%252C%2520file%252C%2520lineNo%252C%2520columnNo%252C%2520error)%2520%257B%250A%2520%2520%2520%2520if%2520(loaded)%2520return%253B%250A%2520%2520%2520%2520queue.push(%255Bmessage%252C%2520file%252C%2520lineNo%252C%2520columnNo%252C%2520error%255D)%253B%250A%2520%2520%257D%253B%250A%250A%2520%2520window.onunhandledrejection%2520%253D%2520function%2520e(error)%2520%257B%250A%2520%2520%2520%2520if%2520(loaded)%2520return%253B%250A%2520%2520%2520%2520queue.push(%255B%250A%2520%2520%2520%2520%2520%2520error.reason.reason%2520%257C%257C%2520error.reason.message%252C%250A%2520%2520%2520%2520%2520%2520error.type%252C%250A%2520%2520%2520%2520%2520%2520JSON.stringify(error.reason)%252C%250A%2520%2520%2520%2520%255D)%253B%250A%2520%2520%257D%253B%250A%250A%2520%2520script.onreadystatechange%2520%253D%2520script.onload%2520%253D%2520function%2520()%2520%257B%250A%2520%2520%2520%2520if%2520(loaded)%2520return%253B%250A%2520%2520%2520%2520loaded%2520%253D%2520true%253B%250A%250A%2520%2520%2520%2520Raven.config('___PUBLIC_DSN___').install()%253B%250A%250A%2520%2520%2520%2520window.onunhandledrejection%2520%253D%2520function%2520e(error)%2520%257B%250A%2520%2520%2520%2520%2520%2520Raven.captureException(new%2520Error(error.reason.reason%2520%257C%257C%2520error.reason.message)%252C%2520%257B%250A%2520%2520%2520%2520%2520%2520%2520%2520extra%253A%2520%257B%250A%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520type%253A%2520error.type%252C%250A%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520reason%253A%2520JSON.stringify(error.reason)%252C%250A%2520%2520%2520%2520%2520%2520%2520%2520%257D%252C%250A%2520%2520%2520%2520%2520%2520%257D)%253B%250A%2520%2520%2520%2520%257D%253B%250A%250A%2520%2520%2520%2520queue.forEach(function%2520(error)%2520%257B%250A%2520%2520%2520%2520%2520%2520Raven.captureException(error%255B4%255D%2520%257C%257C%2520new%2520Error(error%255B0%255D)%252C%2520%257B%250A%2520%2520%2520%2520%2520%2520%2520%2520extra%253A%2520%257B%250A%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520file%253A%2520error%255B1%255D%252C%250A%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520line%253A%2520error%255B2%255D%252C%250A%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520col%253A%2520error%255B3%255D%252C%250A%2520%2520%2520%2520%2520%2520%2520%2520%257D%252C%250A%2520%2520%2520%2520%2520%2520%257D)%253B%250A%2520%2520%2520%2520%257D)%253B%250A%2520%2520%257D%253B%250A%257D(window%252C%2520%255B%255D%252C%2520false%252C%2520document.getElementById('raven')))%253B%250A

It compiles in advanced mode but has issues so I stuck with the simple mode since the byte size difference was insignificant.

@phaistonian
Copy link

The problem with this approach is that the point of reference is the captureException not the error itself (in Sentry, using the source map).

@maxmilton
Copy link
Author

@phaistonian yeah I agree it's actually a rather big downside of this kind of approach. Better than nothing and definitely better than blocking the page load waiting for the sync script, at least in my case.

Do you have any ideas on how to report the error better, with it's true origin?

@micahjon
Copy link

Great gist, thanks for sharing.

Keep in mind that Promises can reject with a reason of any type (not just another Reason or an Error object).
https://developer.mozilla.org/en-US/docs/Web/API/PromiseRejectionEvent/reason

Unless you're sure that all your Promises rejecting with objects, it might make sense to do something like this:

var reason = error.reason;
if (reason) reason = reason.reason || reason.message || reason;
    queue.push([
        reason,
        ...

@imposibrus
Copy link

Correct me if I am wrong, but for recents versions of SDK you can use it like this:

<script src="https://browser.sentry-cdn.com/5.6.1/bundle.min.js" integrity="sha384-pGTFmbQfua2KiaV2+ZLlfowPdd5VMT2xU4zCBcuJr7TVQozMO+I1FmPuVHY3u8KB" crossorigin="anonymous" async id="sentry-sdk"></script>
<script>
    document.getElementById('sentry-sdk').addEventListener('load', function () {
        Sentry.init({ dsn: 'https://<key>@sentry.io/<project>' });
    }, false);
</script>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment