I use Sentry to track client-side errors. However, I loaded my web app in the Edge browser today only to find a blank page. Edge raised a TextEncoder is not defined
error because one of the libraries in my bundle referenced TextEncoder
which it does not support. Sentry did not report the error because the error occurred before Sentry was initialized.
I use vue-cli to create a Vue project with Sentry being initialized near the top of the main file:
import { init } from '@sentry/browser';
import { environment } from '@/constants';
import { Vue as VueIntegration } from '@sentry/integrations';
export default function(Vue) {
const debug = environment !== 'production';
init({
dsn: 'redacted',
environment,
debug,
integrations: [new VueIntegration({ Vue, logErrors: debug })],
});
}
I've been thinking of initializing Sentry manually with a script tag near the start of the <body>
tag. However, the fact that I use the VueIntegration
plugin complicates things. Would it be safe to initialize Sentry twice? Once before the main bundle loads and once as I'm doing in the example above?
I noticed there's something in the docs about managing multiple Sentry clients but I'm not sure if that's relevant to my specific case.
One idea I have is just a barebones window.onerror
hook before anything else loads but I'm not really sure how to interact with Sentry without pulling in their @sentry/browser
package. Ideally I would just communicate with their service using a simple XHR request and my DSN.
My question is what is the recommended way to track errors that occur before Sentry is initialized in the main JS bundle?
I ended up solving it by adding a barebones window.onerror
hook that loads inline before the main bundle arrives. The error is immediately sent to our API and then to our Slack #alerts channel. I added rate limiting so people dont abuse it (too much).
index.html (generated by vue-cli except for the new script tag):
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
/>
<title>App title</title>
</head>
<body>
<script>
// This gives us basic error tracking until the main app bundle loads and
// initializes Sentry. Allows us to catch errors that would surface before
// Sentry has a chance to catch them like Edge's `TextEncoder is not defined`.
(function() {
function sendBasicClientError(message, error) {
var xhr = new XMLHttpRequest();
var domain =
window.location.hostname === 'localhost'
? 'http://localhost:5000'
: 'https://example.com';
xhr.open('POST', domain + '/api/v1/basic_client_errors');
xhr.setRequestHeader(
'Content-Type',
'application/vnd.api+json; charset=utf-8'
);
xhr.send(
JSON.stringify({
data: {
type: 'basic_client_error',
attributes: {
error_message: 'Init error: ' + message + ' ' + navigator.userAgent,
error: error
? JSON.parse(
JSON.stringify(error, Object.getOwnPropertyNames(error))
)
: null,
},
},
})
);
}
window.onerror = function(message, filename, lineno, colno, error) {
sendBasicClientError(
message + ' ' + filename + ':' + lineno + ':' + colno,
error
);
};
})();
</script>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>
Right before Sentry loads we clear the hook:
// Clears the simple `window.onerror` from `index.html` so that Sentry can
// take over now that it's ready.
window.onerror = () => {};
init({
dsn: 'redacted',
environment,
debug,
integrations: [new VueIntegration({ Vue, logErrors: debug })],
});
Rails controller:
module Api
module V1
class BasicClientErrorsController < ApplicationController
def create
# Can comment out if not using the `pundit` gem.
skip_authorization
# We use `sidekiq` and `slack-ruby-client` gems here.
# Substitute whatever internal error tracking tool you use.
SlackNotifierWorker.perform_async(
basic_client_error_params[:error_message],
'#alerts'
)
head :accepted
end
private
def basic_client_error_params
# We use the `restful-jsonapi` gem to parse the JSON:API format.
restify_param(:basic_client_error).require(:basic_client_error).permit(
:error_message
)
end
end
end
end
Rate limiting with the rack-attack
gem:
Rack::Attack.throttle('limit public basic client errors endpoint', limit: 1, period: 60.seconds.to_i) do |req|
req.ip if req.path.end_with?('/basic_client_errors') && req.post?
end