Server-side decoding of JavaScript sourcemaps

I’ve had client-to-server logging for a while now (wonderfully easy, thanks to the magic of Sente and Timbre), but I recently added a top-level “error” listener to catch anything that might ever go wrong in my client-side code.

As it turned out, things do sometimes go wrong, which I did not know about. But now I am left with logs containing undecipherable backtraces from code compiled with :advanced optimizations.

“That’s what source maps are for!” — I hear you shout — and indeed it is, but it seems everybody uses them in a browser. I started looking for a library that would decode my backtraces, or at least help me parse the .map file, thinking this must be a solved problem, but it seems it isn’t. The best I could find was a C# library and mozilla’s source-map npm package.

How do people deal with this? Is there a Clojure (or Java) library that could help?

1 Like

Haven’t tried lumo yet, but since there’s a npm library for this it might be a good fit.

The Closure Compiler actually has a full suite of tools for dealing with Source Maps. Unfortunately they are pretty much not documented at all so it takes a little bit of digging around.

It is however pretty easy to use once you figure out how.

(ns demo.sm-lookup
  (:require [clojure.java.io :as io])
  (:import [com.google.debugging.sourcemap SourceMapConsumerV3]))

(defn lookup [map-name line column]
  (let [sm-consumer (SourceMapConsumerV3.)
        sm-file (io/file map-name)]
    (.parse sm-consumer (slurp sm-file))
    (when-let [mapping (.getMappingForLine sm-consumer line column)]
      {:original (.getOriginalFile mapping)
       :line (.getLineNumber mapping)
       :column (.getColumnPosition mapping)}
      )))

(demo.sm-lookup/lookup "path/to/your/source.js.map" 80 1)
=> {:original "cljs/core.cljs", :line 1099, :column 1}

There is also some code in cljs.stacktrace and cljs.source-map but I don’t know anything about those.

3 Likes

@thheller wow, thanks! This is a good starting point. And a quick look at ClojureScript shows that indeed there is source map decoding support in there as well (using Closure library).

1 Like

Just to report back, I’m having limited success so far. The Closure SourceMapConsumerV3 doesn’t work for me at all (.parse returns nil and that’s pretty much it). cljs.source-map/decode works better, it reads the map correctly. But it seems that the map isn’t all that useful: very few locations in my stacktraces are decodable, and those are usually in cljs, not the namespaces I’m most interested in (my application’s).

It’s better than nothing, but my stacktraces are still very cryptic.

The sm-consumer is a stateful object so you call functions on that after calling .parse. .parse itself doesn’t return anything (ie. void) since it just modifies the sm-consumer instance.

If you don’t get a proper location back that might be due to CLJS not including any source maps for any foreign libs (ie. cljsjs).

You can try looking at your source map with source-map-explorer. If it complains about a lot of unmapped space thats due to foreign libs.

1 Like

Well, I do feel stupid now (about trying to use the result of .parse). On the other hand, this shows what many years of writing Clojure and ClojureScript does to you: it didn’t even cross my mind that you had to hold on to sm-consumer and return that :slight_smile:

Anyway — this is exactly what I ended up using and it works fine. I can get at least some information out of the stacktraces, which makes them much more useful. And it’s such an easy win once you have client-side logging implemented — the .map file usually even sits right there in resources, so it’s easily accessible by just calling io/resource.

Thank you for your help!

1 Like

@jwr this sounds super useful! Do you have any code you can share for people that want to try this approach. Just some snippets in a gist would already be very useful. e.g. how do you hook up Sente and Timbre, and how does your code for decoding the stack traces look?

1 Like

I will try to extract this, but it turns out to be quite a bit of work (there are bits of this code all over the place, and most are very app-specific). So it might take a while :slight_smile: