RUM Source Maps: Debug Minified Production Errors with Original Source Code




Try OpenObserve Cloud today for more efficient and performant observability.

You ship your application to production. Vite (or Webpack, or Rollup) bundles your code into optimized, minified files. Your beautiful src/components/CheckoutForm.vue becomes chunk-8f3a2b.js. Your descriptive variable names become single letters. Your 200 lines of checkout logic become a single wall of minified text.
Then an error happens in production:
TypeError: Cannot read properties of undefined (reading 'discount')
at setup/b/< @ https://app.example.com/assets/CheckoutForm-RC3okFHd.js:1:338
at setup/b/< @ https://app.example.com/assets/CheckoutForm-RC3okFHd.js:1:538
at b @ https://app.example.com/assets/CheckoutForm-RC3okFHd.js:1:542
Line 1, column 338. That's your entire application on one line. Good luck finding the bug.
This is where source maps change everything. OpenObserve's RUM module lets you upload your .map files and automatically transforms these cryptic stack traces into readable, debuggable code with original filenames, line numbers, function names, and even the surrounding source code context.
Source maps are JSON files (.js.map) generated by your build tool alongside the minified output. They contain a mapping between every position in the minified file and the corresponding position in the original source code.
When your bundler outputs:
dist/
CheckoutForm-RC3okFHd.js ← minified, deployed to production
CheckoutForm-RC3okFHd.js.map ← source map, NOT deployed to production
The .js.map file contains:
src/components/CheckoutForm.vue)Source maps are generated by every modern bundler Vite, Webpack, Rollup, esbuild, Parcel, and SWC all support them. You almost certainly generate them already; you just might not be using them for production debugging.
OpenObserve's source map system has three parts:
.js and .js.map files, tagged with service name, version, and environmentSource maps are matched to errors using three dimensions:
| Dimension | Purpose | Example |
|---|---|---|
| Service | Which application produced the error | web-app, admin-dashboard, mobile-web |
| Version | Which build/release of that application | 2.4.1, 1.0.0-beta.3, abc123 |
| Environment | Which deployment environment | production, staging, development |
This three-dimensional matching means you can:
When an error arrives, OpenObserve extracts the minified filename from the stack trace (e.g., CheckoutForm-RC3okFHd.js), then queries:
WHERE org = 'your-org'
AND source_file_name = 'CheckoutForm-RC3okFHd.js'
AND service = 'web-app' -- from error metadata
AND version = '2.4.1' -- from error metadata
AND env = 'production' -- from error metadata
If a match is found, the source map is fetched and used for deobfuscation. If not, the original (minified) stack trace is displayed unchanged.
This is why getting the service/version/environment values right is critical they must match between your SDK configuration and your source map upload.
When initializing @openobserve/browser-rum, set the service, env, and version fields to match your build:
import { openobserveRum } from '@openobserve/browser-rum';
openobserveRum.init({
applicationId: 'checkout-app',
clientToken: 'your-rum-token', // from Settings → RUM Token
site: 'https://cloud.openobserve.ai',
organizationIdentifier: 'your-org',
// These three fields MUST match your source map upload:
service: 'web-app',
env: 'production',
version: '2.4.1',
insecureHTTP: false,
apiVersion: 'v1',
});
Tip: Use your git commit SHA or a build-injected variable as the version so every deployment is uniquely identifiable and stays in sync automatically:
// Injected at build time via Vite's `define` or an environment variable
openobserveRum.init({
// ...
version: __APP_VERSION__, // e.g., 'a1b2c3d' (git short SHA) or '2.4.1'
});
Using package.json version works if you bump it on every release, but a git SHA is safer it guarantees uniqueness even for hotfix rebuilds of the same version.
Most bundlers generate source maps by default or with minimal configuration:
Vite (vite.config.ts):
export default defineConfig({
build: {
sourcemap: true, // generates .js.map files in dist/
},
});
Webpack (webpack.config.js):
module.exports = {
devtool: 'source-map', // generates .js.map files
};
Important: Do NOT deploy .map files to your production CDN/server. Source maps contain your original source code and should only be uploaded to OpenObserve, not served to browsers.
After your build completes, create a ZIP containing the minified .js files and their corresponding .js.map files:
cd dist/assets
zip -r sourcemaps.zip *.js *.js.map
The ZIP structure should look like:
sourcemaps.zip
├── CheckoutForm-RC3okFHd.js
├── CheckoutForm-RC3okFHd.js.map
├── vendor-B7kx9Dq2.js
├── vendor-B7kx9Dq2.js.map
├── index-Lm4nPq8R.js
└── index-Lm4nPq8R.js.map
Rules:
.js file must have a matching .js.map file (same name + .map extension).js.map file exists without a .js counterpart, it will be accepted (graceful fallback).js file has no .map file, the upload will fail with a validation error.zip format is supportedservice value (e.g., web-app)version value (e.g., 2.4.1)env value (e.g., production)The UI shows the selected filename and file size before upload. You can remove and re-select if needed.
For CI/CD integration, use the REST API:
curl -X POST "https://cloud.openobserve.ai/api/your-org/sourcemaps" \
-H "Authorization: Bearer $OPENOBSERVE_TOKEN" \
-F "service=web-app" \
-F "version=2.4.1" \
-F "env=production" \
-F "file=@dist/sourcemaps.zip"
This returns 201 Created on success. Integrate this into your deployment pipeline to ensure source maps are uploaded with every release.
CI/CD Example (GitHub Actions):
- name: Build
run: npm run build
- name: Package source maps
run: cd dist/assets && zip -r ../../sourcemaps.zip *.js *.js.map
- name: Upload source maps to OpenObserve
run: |
curl -X POST "${{ secrets.O2_URL }}/api/${{ secrets.O2_ORG }}/sourcemaps" \
-H "Authorization: Bearer ${{ secrets.O2_TOKEN }}" \
-F "service=web-app" \
-F "version=${{ github.sha }}" \
-F "env=production" \
-F "file=@sourcemaps.zip"
After uploading, go back to RUM → Source Maps. You should see your upload listed with:
Click the row to expand and see all individual file pairs (e.g., CheckoutForm-RC3okFHd.js + CheckoutForm-RC3okFHd.js.map).
In RUM → Error Tracking, you see an error with this stack trace:
TypeError: Cannot read properties of undefined (reading 'discount')
at setup/b/< @ https://app.example.com/assets/CheckoutForm-RC3okFHd.js:1:338
at setup/b/< @ https://app.example.com/assets/CheckoutForm-RC3okFHd.js:1:538
at b @ https://app.example.com/assets/CheckoutForm-RC3okFHd.js:1:542
Line 1, column 338. Every function is named b. You have no idea what's happening.
Click the "Pretty" tab on the stack trace panel. OpenObserve sends the stack trace to the backend, which:
CheckoutForm-RC3okFHd.js matching your service/version/envThe result:
TypeError: Cannot read properties of undefined (reading 'discount')
at applyDiscount @ src/components/CheckoutForm.vue:56:17
at calculateTotal @ src/components/CheckoutForm.vue:49:3
at handleSubmit @ src/components/CheckoutForm.vue:45:3
Now you know exactly where the error is: CheckoutForm.vue, line 56, in the applyDiscount function.
The Pretty Stack Trace view goes further than just file and line number. For each frame:
This means you can read the actual code around the error seeing what discount was supposed to be, where it was expected to come from, and what the surrounding logic looks like. All without opening your IDE.
Stack trace translations are cached on the frontend for 1 hour to avoid redundant API calls:
orgId::hashOfStackTrace::service::version::envThe RUM → Source Maps page shows all uploaded source maps grouped by service/version/environment. You can filter by:
Filter values are fetched dynamically from the API. Pagination supports 20, 50, 100, or 250 rows per page.
To delete source maps for a specific release:
This deletes all file pairs for that service/version/environment combination. Deletion is atomic you can't delete individual files within a group.
Via API:
curl -X DELETE "https://cloud.openobserve.ai/api/your-org/sourcemaps?service=web-app&version=2.4.1&env=production" \
-H "Authorization: Bearer $OPENOBSERVE_TOKEN"
All three parameters must match exactly for deletion to proceed this prevents accidental bulk deletes.
Under the hood, source maps are stored as follows:
.js.map file is assigned a UUID-based storage name (e.g., a1b2c3d4-e5f6.js.map)In enterprise cluster deployments, source maps are replicated across nodes:
Here's the actual flow demonstrated with a real stack trace from the OpenObserve test suite:
1. Original minified error:
TypeError: can't access property "nonExistent", e is undefined
at setup/b/< @ http://localhost:4173/assets/AboutView-RC3okFHd.js:1:338
at setup/b/< @ http://localhost:4173/assets/AboutView-RC3okFHd.js:1:538
at b @ http://localhost:4173/assets/AboutView-RC3okFHd.js:1:542
2. After source map translation:
TypeError: can't access property "nonExistent", e is undefined
at obj @ ../../src/components/ErrorDemo.vue:56:17
at fn3 @ ../../src/components/ErrorDemo.vue:49:3
at fn2 @ ../../src/components/ErrorDemo.vue:45:3
3. In the Pretty Stack Trace UI, the first frame expands to show:
51│
52│ const obj = (input: any) => {
53│ // This will throw a TypeError because input is undefined
54│ // and we're trying to access 'nonExistent' property
55│ const nested = input.nested;
56│ return nested.nonExistent.deep.value; // ← ERROR LINE (highlighted)
57│ };
58│
59│ const fn3 = () => {
60│ return obj(undefined);
61│ };
Now you can see: the function receives undefined as input, tries to access input.nested, gets undefined, then tries to access .nonExistent on undefined. The fix is obvious add a null check.
Never rely on manual uploads. Source maps should be uploaded automatically as part of every deployment:
# In your deploy script:
npm run build
cd dist/assets && zip -r ../../sourcemaps.zip *.js *.js.map && cd ../..
curl -X POST "$O2_URL/api/$O2_ORG/sourcemaps" \
-H "Authorization: Bearer $O2_TOKEN" \
-F "service=$SERVICE_NAME" \
-F "version=$(git rev-parse --short HEAD)" \
-F "env=$DEPLOY_ENV" \
-F "file=@sourcemaps.zip"
Using your git commit SHA (or short SHA) as the version ensures every build has a unique version identifier:
// vite.config.ts
export default defineConfig({
define: {
__APP_VERSION__: JSON.stringify(process.env.COMMIT_SHA || 'dev'),
},
});
// main.ts
openobserveRum.init({
version: __APP_VERSION__,
// ...
});
Source maps accumulate over time. Set up a periodic cleanup job to delete maps older than your retention window:
# Delete source maps for old versions via the management API
# Keep maps for the last N versions or last 30 days
Your build should generate source maps but not include them in the deployed assets. However, the .map files you upload to OpenObserve must contain embedded source content (sourcesContent) this is what powers the syntax-highlighted source code context in the Pretty Stack Trace view.
By default, most bundlers embed sourcesContent in .map files, so no extra configuration is needed:
// vite.config.ts
export default defineConfig({
build: {
sourcemap: true, // generates .map files with sourcesContent included by default
},
});
Important: Do NOT set
sourcemapExcludeSources: truein your Rollup/Vite config. This strips thesourcesContentfield from.mapfiles, which means OpenObserve can resolve file paths and line numbers but cannot display the surrounding source code context. If you need smaller.mapfiles for other tooling, generate two builds one with full source content for OpenObserve upload.
After building, upload the .map files to OpenObserve, then exclude them from your deployment artifact. Your production server should never serve .map files to browsers they contain your original source code.
If the Pretty Stack Trace shows a "No source maps" message with service/version badges:
2.4.1 vs v2.4.1)env in the SDK, did you also specify it during upload?.js filename in your ZIPThe most common issue is a version mismatch. If your SDK sends version: '2.4.1' but you uploaded with version: 'v2.4.1', no match will be found.
If some lines resolve but others don't:
node_modules) may not have source maps in your bundle.js file without a corresponding .map file. This fails the entire upload either add the missing map or remove the orphaned .js before zipping. Filter your ZIP to only include files that have matching pairs:cd dist/assets
for f in *.js; do [ -f "$f.map" ] && echo "$f" "$f.map"; done | xargs zip sourcemaps.zip
.map files cannot exceed 5 MB, and the total ZIP cannot exceed 100 MB. If your source maps are very large, consider splitting into multiple ZIPs per route/chunk.zip format is acceptedMinified stack traces are the single biggest obstacle to debugging production JavaScript errors quickly. Without source maps, you're reverse-engineering compressed code. With them, you're reading the original source with context, line numbers, and function names.
OpenObserve makes the source map workflow straightforward:
curl commandStop squinting at chunk-8f3a.js:1:28432. Upload your source maps and start debugging production errors in seconds.
Ready to get started?
Related posts: