4 Reasons Why Your Source Maps are Broken

October 18, 2018 0 Comments

4 Reasons Why Your Source Maps are Broken

 

 

Source maps are awesome. Namely, because they are used to display your original JavaScript while debugging, which is a lot easier to look at than minified production code. In a sense, source maps are the decoder ring to your secret (minified) code.

However, they can be tricky to get working properly. If you’ve run into some trouble, the tips below will hopefully help you get everything in working order.

If you’re looking to get started with source maps for the first time, check out our earlier post, Debugging Minified JavaScript with Source Maps, before continuing.

Missing or incorrect source map directive


We’re going to presume that you’ve already produced a source map using a tool like UglifyJS or Webpack. But generating a source map isn’t worth diddly if the browser can’t find it. To do that, browser agents expect your bundled JavaScript files to contain either a sourceMappingURL comment or return a SourceMap HTTP header that points to the location of the source map.

To verify your source map directive is present and working, you need to:

Locate the sourceMappingURL comment at the very end of the file on its own line:

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

This value must be a valid URI. If it is a relative URL, it is relative to the path location of the bundled JavaScript file (e.g., script.min.js). Most source map generation tools generate this value automatically for you, but also provide an option for overriding it.

Using UglifyJS, you can generate this comment by defining url=script.min.js.map in your source map options:

# Using UglifyJS 3.3 
$ uglifyjs --source-map url=script.min.js.map,includeSources \ --output script.min.js script.js

Using Webpack, specifying devtool: "source-map" in your Webpack config will enable source maps, and Webpack will output a sourceMappingURL directive in your final, minified file. You can customize the source map filename itself by specifying sourceMapFilename.

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

Note that even if you generate sourceMappingURL properly, it’s possible that it isn’t appearing once you serve your final version in production. For example, another tool at the end of front-end build toolchain might be stripping comments — which would have the effect of removing //# sourceMappingURL.

Or your CDN might be doing something clever like stripping comments unknowingly; Cloudflare’s Autominify feature has stripped these comments in the past. Double-check your file in production to make sure your comment is there!

Alternatively: Ensure your server returns a valid SourceMap HTTP header

Instead of this magic sourceMappingURL comment, you can alternatively indicate the location of the source map by returning a SourceMap HTTP header value when requesting your minified file.

 SourceMap: /path/to/script.min.js.map 

Just like sourceMappingURL, if this value is a relative path, it is relative to the path location of the bundled JavaScript file. Browser agents interpret the SourceMap HTTP header and sourceMappingURL identically.

Note that to serve this header, you need to configure your web server or CDN to do so. Many JavaScript developers may not have the capability of setting arbitrary headers on their static assets in production, so for most, it’s easier to just generate and use sourceMappingURL.

Missing original source files


Let’s assume that you’ve properly generated your source map, and your sourceMappingURL (or SourceMap header) is present and correct. Parts of the transformation are clearly working; for example, error stack traces now mention your original filenames and contain sensible line and column locations. But despite this improvement, there’s a big piece missing — you still can’t browse your original source files using your browser’s debug tools.

This likely means that your source map doesn’t contain or link to your original source files. Without your original source files, you’re still stuck stepping through minified code in your debugger. Ouch.

There are a handful of solutions for making your original source files available:

Inline your original source code into your source map via sourcesContent

It’s possible to inline your entire original source code into the source map itself. Inside the source map, this is referred to as sourcesContent. This can make for really large source map files (on the order of megabytes) but has the advantage of being really simple for browser agents to locate and link up all your original source files. If you’re struggling to get your browser to find your original sources, we recommend trying this.

If you’re using UglifyJS, you can inline your original source code into your source map’s sourcesContent property using the includeSources CLI option:

 uglifyjs --source-map url=script.min.js.map,includeSources --output script.min.js script.js 

If you’re using Webpack, there’s nothing to do here — Webpack inlines your original source code into your source maps by default (assuming devtool:"source-map" is enabled).

Host your original source files and serve them publicly

Instead of inlining your original source files, you can instead host them on your web server such that they can be individually downloaded by browser agents. If you’re concerned about security — these are your original source files, after all — you can serve the files via localhost or make sure they’re protected behind a VPN (e.g., the files are only reachable via your company’s internal network).

Sentry users only: upload your source files as artifacts

If you’re a Sentry user and your primary goal is making sure source maps are available so that Sentry can unminify your stack traces and provide surrounding source code, you have a third option: upload your source files as artifacts using sentry-cli or directly via our API.

Of course, if you do either of the first two options — either inlining your original files or hosting them publicly — Sentry will find the content that way too. It’s your call.

Bad source maps caused by multiple transformations


If you’re using two or more JavaScript compilers invoked separately (e.g., Babel + UglifyJS), it’s possible to produce a source map that points to an intermediate transformed state of your code, rather than the original source code. This means that when you debug in the browser, the code you’re stepping through isn’t minified — an improvement for sure — but it’s still not a 1:1 match with your original source code.

For example, let’s say you used Babel to convert your ES2018 code to ES2015, then ran the output through UglifyJS:

# Using Babel 7.1 and UglifyJS 3.3 
$ babel-cli script.js --presets=@babel/env | uglifyjs -o script.min.js \ --source-map "filename=app.min.js.map"
$ ls script*
script.js script.min.js script.min.js.map

If you were to use the source map generated by this command, you’ll notice it won’t be accurate. That’s because the source map only converts from the minified (Uglified) code back to the code generated by Babel. It does not point back to your original source code.

Note this is also common if you’re using a task manager like Gulp or Grunt.

To fix this, there are two possible solutions:

Use a bundler like Webpack to manage all your transformations

Instead of using Babel or UglifyJS separately, use them instead as Webpack plugins (e.g., babel-loader and uglifyjs-webpack-plugin). Webpack will produce a single source map that transforms from the final result back to your original source code, even though there are multiple transformations taking place under the hood.

Use a library to “stitch” together source maps between transformations

If you’re committed to using multiple compilers separately, you can use a library like source-map-merger, or the source-map-loader Webpack plugin, to feed the results of an earlier source map into a subsequent transformation.

If you have a choice in the matter, we’d recommend pursuing the first option — just use Webpack and save yourself some grief.

Files are incorrectly versioned or missing versions


Let’s say you’ve followed all of the above. Your sourceMappingURL (or SourceMap HTTP header) is present and properly declared. Your source maps include your original source files (or they’re publicly accessible). And you’re using Webpack end-to-end to manage your transformations. And yet, your source maps still periodically create mappings that don’t match.

There’s a remaining possibility: bad transforms caused by mismatched compiled files and source maps.

This happens when a browser or tool downloads a compiled file (e.g., script.min.js), then attempts to fetch its corresponding source map (script.min.js.map), but the source map it downloads is “newer” and corresponds to a different version of that compiled file.

This situation is uncommon but can occur if a deploy is triggered while you’re debugging, or you’re debugging using browser-cached assets that are about to expire.

To solve this, you need to version your files and your source maps, either by:

  • Versioning each filename, e.g., script.abc123.min.js
  • Versioning the URL’s query string, e.g., script.min.js?abc123
  • Versioning a parent folder, e.g., abc123/script.min.js

Whichever you strategy you choose doesn’t matter, only that you use it consistently for all your JavaScript assets. It’s expected that each compiled file → source map share the same version and version scheme, like the following example:

// script.abc123.min.js 
for(var a=[i=0];++i<20;a[i]=i);
//# sourceMappingURL=script.abc123.min.js.map

Versioning this way will ensure that each browser agent downloads the source map that belongs to each compiled file, avoiding version mismatches.

If you’ve read this far, you should be fairly equipped to fix source map transformations and debug like a pro. Nice work. Think of something we missed? We look forward to your email.

We also highly recommend checking out the Sentry documentation.


Tag cloud