Ivan Vanderbyl

Debugging SourceMaps

Or more precisely, debugging your debugging tools.

I recently began fixing a number of errors on DigitalOcean's public website which stemmed from years of technical debt and poorly understood code.
The short of which was that the JS minifier we were using was producing invalid Javascript due to syntax errors, which for the most part were from code being pasted from StackOverflow posts.

That aside, once I'd linted all the code and cleaned up the obvious errors, which saw a huge reduction of errors in production, we were still seeing quite a few errors.

Now the problem with minified JS is that you get error reports which are one line long, and 9982 columns deep. Which basically makes them useless for debugging. To work around this there's an awesome feature of modern browsers called SourceMaps.

SourceMaps, as the name suggests, map sources. So you can map each function in your minified JS to actual code, even transpiled ES6 -> ES5 code or Coffeescript. This makes debugging errors in production much easier, especially because error reporting services like Bugsnag actually pull these in for you, seamlessly. So you get rich backtraces with information you can use locally.

I'm not going to go in to detail about how to set up SourceMaps, you can read all about SourceMaps with WebPack, the compiler we use. Instead I want to talk about a not so documented issue with the way browsers handle SourceMaps served separately to your source files.

The first thing to know about SourceMaps is that they're just JSON. But the file extension of .map often has no default mapping on most servers, so they get served with a MIME type of application/javascript or something else undetermined, which for Firefox and Chrome means they will parse it as Javascript or not at all, but you won't know this because they silently fail on this and report no errors back to you. Safari parses them as JSON no matter what Content-Type you serve them as.

This was infuriating because the Network Inspector in Chrome showed that my SourceMaps were being loaded successfully, but they weren't working as I expected.

Unfortunately there isn't a way to really debug the internals of Chrome like you debug your own JS apps, however there is a way to do this with Firefox, it's called Firefox Developer Edition, which includes a console for inspecting the live Browser called Browser Toolbox.

The first thing you notice here is that we're debugging actual Javascript, which is running the browser, that's right, the internals of Firefox responsible for loading and parsing resources are written in Javascript.

Invalid JSON in SourceMaps

ThreadSources.prototype._fetchSourceMap threw an exception: SyntaxError: JSON.parse: unexpected non-whitespace character after JSON data at line 2146 column 1 of the JSON data  
Stack: SourceMapConsumer@resource://gre/modules/devtools/SourceMap.jsm:67:19  
ThreadSources.prototype._fetchSourceMap/fetching<@resource://gre/modules/commonjs/toolkit/loader.js -> resource://gre/modules/devtools/server/actors/script.js:5453:19  
Handler.prototype.process@resource://gre/modules/Promise.jsm -> resource://gre/modules/Promise-backend.js:867:23  
...

Reloading our site in Firefox Developer Edition gave me exactly what I needed to start investigating this further. As you can see above, the SourceMap parser is crapping itself on an invalid token which it expects to be JSON. Well that's weird, because surely Webpack is outputting valid SourceMaps right? To verify this I spun up a Node REPL and loaded my SourceMaps from disk:

$ node
> JSON.parse(fs.readFileSync('source/assets/js/application.bundle.js.map', 'utf8'))
{ version: 3,
  sources: ...

Seems legit.

So we're in the right, but the browser is still failing. I added some breakpoints to Firefox's SourceMap parser and drilled down to the fetch function which handles loading SourceMaps. After stepping through to the end I noticed that our contentType was set to application/javascript, which indicates that Middleman (our static site generator) was serving this file incorrectly.
Firefox Browser Debugger

Fixing Middleman option #1

The first way to fix this is to make Middleman serve .map files as JSON, this is really easy. In our Middleman config.rb just add this to the top:

Rack::Mime::MIME_TYPES.merge!({".map" => "application/json"})  

Rinse and reload. Done.

Fixing everything else at once, option #2

Option #1 won't fix our issue in production because we serve everything through Nginx, which would require me to reconfigure Nginx's MIME config. Too hard right now, too much Chef involved.

A simple fix, just change our SourceMap filename in Webpack:

webpack --config webpack.config.js -p --devtool sourcemap --output-source-map-file "[file].map.json"  

This will output our SourceMaps ending in .map.json, and update the references in each compiled file to match.

Problem solved. Now Nginx and Middleman both serve as application/json.

Conclusion

Sometimes taking the long way around to debug something can give you some valuable insights in to how the tools you work with actually work.

So long story short, serve your SourceMaps as application/json if you can't get them to work for no obvious reason.