<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Randolf J.</title>
    <description>The latest articles on DEV Community by Randolf J. (@jrandolf).</description>
    <link>https://dev.to/jrandolf</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3797455%2Fac131e8f-e37e-4876-8704-9597e2920958.jpeg</url>
      <title>DEV Community: Randolf J.</title>
      <link>https://dev.to/jrandolf</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jrandolf"/>
    <language>en</language>
    <item>
      <title>Readable stack traces in production: source maps + OpenTelemetry</title>
      <dc:creator>Randolf J.</dc:creator>
      <pubDate>Tue, 10 Mar 2026 10:35:11 +0000</pubDate>
      <link>https://dev.to/jrandolf/readable-stack-traces-in-production-source-maps-opentelemetry-1de3</link>
      <guid>https://dev.to/jrandolf/readable-stack-traces-in-production-source-maps-opentelemetry-1de3</guid>
      <description>&lt;p&gt;Two years ago I left Sentry and moved to OpenTelemetry. The one thing I had to rebuild was source map resolution. I built &lt;a href="https://github.com/jrandolf/smapped-traces" rel="noopener noreferrer"&gt;&lt;code&gt;smapped-traces&lt;/code&gt;&lt;/a&gt; internally to do it, and we are open sourcing it now that it has run in production for two years.&lt;/p&gt;




&lt;h2&gt;
  
  
  The problem with naive approaches
&lt;/h2&gt;

&lt;p&gt;Serving source maps from your build output exposes your source code publicly. Restricting access requires your error handler to authenticate against a source map server at resolution time, which is complexity you do not want in that path. Bundling source maps into your collector configuration does not work either; the collector has no stable way to associate a map with a minified file across deployments.&lt;/p&gt;




&lt;h2&gt;
  
  
  Debug IDs
&lt;/h2&gt;

&lt;p&gt;The approach I landed on uses debug IDs, a mechanism now supported by Turbopack natively and by webpack via the &lt;a href="https://github.com/tc39/ecma426/blob/main/proposals/debug-id.md" rel="noopener noreferrer"&gt;TC39 proposal&lt;/a&gt;. At build time, the bundler generates a UUID per source file and writes it into both the compiled output and the corresponding &lt;code&gt;.js.map&lt;/code&gt; as a &lt;code&gt;debugId&lt;/code&gt; field. It also injects a runtime global (&lt;code&gt;_debugIds&lt;/code&gt; for Turbopack, &lt;code&gt;__DEBUG_IDS__&lt;/code&gt; for webpack) mapping source URLs to those UUIDs.&lt;/p&gt;

&lt;p&gt;Any stack frame URL resolves to its source map without scanning or path matching.&lt;/p&gt;




&lt;h2&gt;
  
  
  Architecture
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Browser                     Your server             OTel collector
  │                              │                       │
  │  SourceMappedSpanExporter    │                       │
  │  (attaches debug IDs to      │                       │
  │   exception events)          │                       │
  │──────────────────────────────▶  createTracesHandler  │
  │                              │  (resolves stack      │
  │                              │   traces via store)   │
  │                              │──────────────────────▶│
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;@smapped-traces/nextjs&lt;/code&gt; hooks into &lt;code&gt;runAfterProductionCompile&lt;/code&gt;, extracts the &lt;code&gt;debugId&lt;/code&gt; from every &lt;code&gt;.js.map&lt;/code&gt;, uploads the content to a store, and deletes the map files from the build output.&lt;/p&gt;

&lt;p&gt;On the client, &lt;code&gt;SourceMappedSpanExporter&lt;/code&gt; wraps your span exporter. On exception events, it reads the debug ID globals, resolves each frame URL to a debug ID, and attaches them as span attributes before forwarding as standard OTLP.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;createTracesHandler()&lt;/code&gt; runs on your server. It receives OTLP traces, resolves each debug ID against the store, rewrites the stack frames with original file and line information, and forwards to your collector. The minified trace is preserved as &lt;code&gt;exception.stacktrace.original&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Setup (Next.js)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;smapped-traces @smapped-traces/nextjs @smapped-traces/sqlite
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// next.config.mjs&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;withSourceMaps&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@smapped-traces/nextjs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createSqliteStore&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@smapped-traces/sqlite&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;join&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;node:path&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;withSourceMaps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="cm"&gt;/* your existing config */&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;store&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;distDir&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;createSqliteStore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;distDir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sourcemaps.db&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// instrumentation-client.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;SourceMappedSpanExporter&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;smapped-traces/client&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;SimpleSpanProcessor&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@opentelemetry/sdk-trace-base&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;WebTracerProvider&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@opentelemetry/sdk-trace-web&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;exporter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;SourceMappedSpanExporter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/sourcemaps&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;WebTracerProvider&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;spanProcessors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;SimpleSpanProcessor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;exporter&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt;
&lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;register&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/api/sourcemaps/route.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;OTLPTraceExporter&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@opentelemetry/exporter-trace-otlp-http&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createTracesHandler&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;smapped-traces/route&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createSqliteStore&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@smapped-traces/sqlite&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;join&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;node:path&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;POST&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createTracesHandler&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;exporter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;OTLPTraceExporter&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;OTEL_EXPORTER_OTLP_ENDPOINT&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;http://localhost:4318&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/v1/traces`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;store&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;createSqliteStore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cwd&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.next/sourcemaps.db&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Storage
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;SourceMapStore&lt;/code&gt; is an interface over &lt;code&gt;get(debugId)&lt;/code&gt; and &lt;code&gt;put(debugId, content)&lt;/code&gt;. Three implementations are included:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;createSqliteStore(path)&lt;/code&gt; — local SQLite via &lt;code&gt;better-sqlite3&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;createS3Store({ client, bucket, prefix? })&lt;/code&gt; — any S3-compatible backend (AWS, Cloudflare R2, GCS)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;createHttpStore(url)&lt;/code&gt; — HTTP client against a &lt;code&gt;createStoreHandler()&lt;/code&gt; deployment&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For distributed or serverless environments, point both the build plugin and the handler at the same shared store.&lt;/p&gt;




&lt;h2&gt;
  
  
  Runtime compatibility
&lt;/h2&gt;

&lt;p&gt;The handler uses standard Web &lt;code&gt;Request&lt;/code&gt;/&lt;code&gt;Response&lt;/code&gt; and has no Node.js dependencies, so it runs in Bun, Deno, Cloudflare Workers, or any edge runtime. For non-Next.js builds, source map collection is a loop over build artifacts calling &lt;code&gt;store.put(debugId, content)&lt;/code&gt;.&lt;/p&gt;




&lt;p&gt;Requires OTel SDK v2+ and Next.js 16+ for &lt;code&gt;@smapped-traces/nextjs&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;GitHub: &lt;a href="https://github.com/jrandolf/smapped-traces" rel="noopener noreferrer"&gt;https://github.com/jrandolf/smapped-traces&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Turbopack and webpack are supported. Vite and esbuild are not; support depends on whether those bundlers implement the ECMA-426 debug ID spec.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>monitoring</category>
      <category>opensource</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
