Debuggable JavaScript in production with Source Maps

These days, the code you use to write your application isn’t usually the same code that’s deployed in production and interpreted by browsers. Perhaps you’re writing your source code in a language that “compiles” to JavaScript – like CoffeeScript, TypeScript, or the latest standards-body approved version of JavaScript, ECMAScript 2015 (ES6). Or perhaps more likely, you’re minifying your source code in order to reduce the filesize of your deployed scripts. Say, by using a tool like UglifyJS, or Google Closure Compiler.

Such transformation tools are often referred to as transpilers – tools that transform source code from one language into either a) the same language, or b) another similar high-level language. Their output: transpiled code, that while functional in the target environment (e.g. browser-compatible JavaScript), typically bears little resemblance to the code from which it was generated.

This presents a problem: when debugging code in the browser, or inspecting stack traces generated from errors in your application, you are looking at transpiled and (typically) hard-to-read JavaScript. Not the original source code you used to write your application. This can make identifying and fixing bugs hard.

The solution to this problem is a nifty browser feature called Source Maps. Let’s learn more.

Source Maps

Source maps are JSON files that contain information on how to map your transpiled source code back to their original source. If you’ve ever done programming in a compiled language like Objective-C, you can think of source maps as JavaScript’s version of debug symbols.

Here’s an example source map:

{
    version : 3,
    file: "app.min.js",
    sourceRoot : "",
    sources: ["foo.js", "bar.js"],
    names: ["src", "maps", "are", "fun"],
    mappings: "AAgBC,SAAQ,CAAEA"
}

You’ll probably never have to create these files yourself, but it can’t hurt to understand what’s inside:

  • version – the version of the source map spec this file represents (should be “3”)
  • file – the generated filename this source map is associated with
  • sourceRoot – the url root from which all sources are relative (optional)
  • sources – an array of URLs to the original source files
  • names – an array of variable/method names found in your code
  • mappings – the actual source code mappings, represented as base64-encoded VLQ values

If this seems like a lot to know – don’t worry. We’ll explain how to use tools to generate these files for you.

sourceMappingURL

To indicate to browsers that a source map is available for a transpiled file, the sourceMappingURL directive needs to be added to the end of that file:

// app.min.js
$(function () {
    // your application ...
});
//# sourceMappingURL=/path/to/app.min.js.map

When modern browsers see the sourceMappingURL directive, they download the source map from the provided location and use the mapping information inside to corroborate the running client-code with the original source code.

Firefox Debugger w/ Source Maps

Above: Stepping through Sentry’s original ES6 + JSX code in Firefox using Source Maps.

Note: Browsers only download and apply source maps when developer tools are open. There is no performance impact for regular users.

Generating the Source Map

Okay, we know how source maps roughly work, and how to get the browser to download and use them. But how do we go about actually generating them and referencing them from our transpiled files?

Good news: basically every modern JavaScript transpiler has a command-line option for generating an associated source map. Let’s take a look at a few common ones.

UglifyJS

UglifyJS is a popular tool for minifying your source code for production. It can dramatically reduce the size of your files by eliminating whitespace, rewriting variable names, removing dead code branches, and more.

If you are using UglifyJS to minify your source code, the following command will additionally generate a source map mapping the minified code back to the original source:

$ uglifyjs app.js -o app.min.js --source-map app.min.js.map

If you take a look at the generated output file (app.min.js), you’ll notice that the final line includes the sourceMappingURL directive pointed to our newly generated source map.

//# sourceMappingURL=app.min.js.map

Note that this is a relative URL. In order for the browser to download the associated source map, it must be uploaded to (and served from) the same destination directory as the Uglified file (app.min.js). That means if app.min.js is served from http://example.org/static/app.min.js, so too must your source map be served from http://example.org/static/app.min.js.map.

Relative URLs aren’t the only way of specifying sourceMappingURL. You can give Uglify an absolute URL via the --source-map-url <url> option. Or you can even include the entire source map inline (not recommended). Take a look at Uglify’s command-line options for more information.

Webpack

Webpack

Webpack is a powerful build tool that resolves and bundles your JavaScript modules into files fit for running in the browser. The Sentry project itself uses Webpack (along with Babel) to assemble and transpile its ES6 + JSX codebase into browser-compatible JavaScript.

Generating source maps with Webpack is super simple. Simply specify the outputSourcemapFilename property in your output configuration (docs):

// webpack.config.js
module.exports = {
    // ...
    entry: {
      "app": "src/app.js"
    },
    output: {
      path: path.join(__dirname, 'dist'),
      filename: "[name].js",
      sourceMapFilename: "[name].js.map",
    },
    // ...
};

Now when you run the webpack command-line program, Webpack will assemble your files, generate a source map, and reference that source map in the built JavaScript file via the sourceMappingUrl directive.

Private Source Maps

Up until this point, all of our examples assume that your source maps are publicly available, and served from the same server as your executing JavaScript code. In which case, any developer can use them to obtain your original source code.

To prevent this, instead of providing a publicly-accessible sourceMappingURL, you can instead serve your source maps from a server that is only accessible to your development team. For example, a server that is only reachable from your company’s VPN.

//# sourceMappingURL: http://company.intranet/app/static/app.min.js.map

When a non-team member visits your application with developer tools open, they will attempt to download this source map but get a 404 (or 403) HTTP error, and the source map will not be applied.

Source Maps and Sentry

If you use Sentry to track exceptions in your client-side JavaScript applications, we have good news: Sentry automatically fetches and applies source maps to stack traces generated by your errors. This means that you’ll see your original source code, and not minified and/or transpiled code.

Above: A stack trace in Sentry mapped back to its original ES6 + JSX source code.

Basically, if you’ve followed the steps in this little guide, and your deploy now has:

  • Generated, uploaded source maps
  • Transpiled files with sourceMappingURL directives pointing to those source maps

Then there’s nothing more to do. Sentry will do the rest.

Sentry and offline Source Maps

Alternatively, instead of hosting source maps yourself, you can upload them directly to Sentry.

Why would you want to do that? A few reasons:

  • In case Sentry has difficulty reaching your servers (e.g. source maps are hosted on VPN)
  • Latency; source maps are inside Sentry before an exception is thrown
  • You’re developing an application that runs natively on a device, e.g. using React Native or PhoneGap, whose source code/maps cannot be reached over the internet
  • Avoids version mismatches where fetched source map doesn’t match code where error was thrown

For more information on uploading source maps to Sentry, check out our official documentation.

Wrapping up

You just learned how Source Maps can save your skin by making your transpiled code easier to debug in production. Since your build tools likely already support source map generation, it won’t take very long to configure, and the results are very much worth it.

Happy debugging.