Search code examples
vue.jswebpackvue-clisentry

How do I load Sentry before the main JS bundle loads?


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?


Solution

  • 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