4 Reasons Why Your Source Maps are Broken & How to Fix Them
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, source maps 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.
Reason 1: 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
.
Reason 2: 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.
Reason 3: 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.
Reason 4: 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 might have missed? We look forward to your email.
We also highly recommend checking out the Sentry documentation.