Unminifying the Madness
30 januari 2026
30 januari 2026
Making React Native + ClojureScript Stack Traces Usable in Sentry
We run a React Native app written in ClojureScript, and we report errors to Sentry.
For native crashes, Sentry was basically plug-and-play: readable reports, clear signals, quick fixes. But for JavaScript errors we hit a wall. The stack traces were technically “processed”, yet still looked like minified soup: weird function names, no recognizable filenames, no ClojureScript namespaces, and no clear “open this file on this line” moment.
That’s the frustrating part: it looks like you did things correctly. Source maps exist. They’re uploaded. Sentry is applying them. And still, you can’t actually debug the issue.
The root cause turned out to be that our code goes through more than one transformation step, and each step produces its own source map. Sentry was only seeing the last one, so it couldn’t map errors all the way back to the original ClojureScript sources.
We don’t have this fully integrated into Sentry yet. What we do have is a reliable workflow: pull the raw stack frames from Sentry, compose the two source maps ourselves, and remap the frames back to ClojureScript. Once you can do that, Sentry errors become actionable instead of noise.
This post explains what was going wrong, how we confirmed it, and the tooling that got us from index.android.bundle:1:1087873 to analytics.cljs:42.
Our stack:
shadow-cljsIf you’ve used React Native with plain JS/TS, the usual pipeline is simple: Metro bundles your code, generates a source map, and Sentry uses that map to translate stack traces back to your source.
With ClojureScript in the mix, we actually have two steps *before* the code reaches the device:
So we end up with two maps that each cover part of the journey:
For our web app with a similar setup, this is fine because bundlers can usually consume earlier source maps and produce a final map that points all the way back to ClojureScript.
In our React Native pipeline, that chaining didn’t happen. Sentry only had the Metro-level map, which meant it could at best map errors back to the compiled/minified JS output of the ClojureScript step, not to the .cljs source.
Result: stack traces that were “mapped”, but still useless.
First question: are we even uploading source maps correctly?
Yes.
Sentry had artifacts for the release, and stack traces looked different when processed vs raw. So the issue wasn’t “no source maps” or “wrong release”.
The problem was: we were uploading a source map, but not a map that contained the information we actually needed.
At this point the working theory became:
So if we wanted ClojureScript namespaces/files/lines, we’d have to bridge the gap ourselves.
If you rely on Sentry’s UI, you often only see the “best effort” mapping it could apply. But for this problem we needed the raw truth: where in the bundle did the error occur?
With help from the Clojurians Slack, I found a super helpful gist by @frankitox that downloads raw stack frames from Sentry’s API.
This script pulls the raw frames so you’re not stuck with partial mappings.
Raw Sentry stack trace (before):
1:1087873 1:2385489 1:2385091 1:2383921 1:2299788 1:2303225
Recognizable? Not at all. But importantly: these are concrete positions in the bundle. That’s exactly what we need to remap correctly.
Now for the key insight: we don’t need Sentry to understand two maps if we can give it (or ourselves) one map that goes all the way back to ClojureScript.
We used sourcemap-cljs (huge thanks to @riverford).
The idea:
A simplified workflow looks like this:
This is a “tooling fix”, not a production-code fix: no runtime changes, no new error handling, just better mapping.
Once we remap through the composed source map, the stack trace becomes readable and useful:
{:source node_modules/react-native/Libraries/BatchedBridge/NativeModules.js
:line 106
:column 59}
{:source node_modules/@mccsoft/react-native-matomo/src/index.tsx
:line 56
:column 37}
{:source ***/analytics.cljs
:line 42
:column 6
:name js/shadow.js.shim.module$$mccsoft$react_native_matomo.trackEvent}
{:source ***/somewhere/hooks/use_some_flow.cljs
:line 220
:column 44}
...
Now it’s obvious:
That changes triage completely. Instead of guessing or reproducing locally, you can go straight to the source.
One recurring error had been bothering us:
That pattern is hard to reason about when stack traces are unreadable, you either ignore it or treat it as “maybe critical”.
With proper ClojureScript mapping, the story became clear:
So:
Readable stack traces turned “what is this and should we panic?” into “oh, fix that arg type; done.”
Right now, our flow still involves an external script and a couple of manual steps. The obvious next step is automation.
During the build:
If we can get that stable, the goal is: React Native production errors written in ClojureScript show up in Sentry just as cleanly as in our web app, no manual scripts, no extra clicking, no “download frames and run a tool”. And if/when we polish this into a clean pipeline, we’ll share it so others don’t have to rediscover the same workaround.
Huge thanks to:
Without those, we’d still be staring at index.android.bundle:1:???.