Uploading source maps to Elastic APM transforms minified and mangled JavaScript stack traces into human-readable, source-code-level representations.
Here’s a minimal Node.js Express app that simulates a JavaScript error and sends it to Elastic APM:
const express = require('express');
const apm = require('elastic-apm-node').start({
serviceName: 'my-js-app',
serverUrl: 'http://localhost:8200', // Replace with your APM Server URL
secretToken: '', // Replace with your secret token if configured
});
const app = express();
app.get('/', (req, res) => {
// Simulate an error
setTimeout(() => {
throw new Error('Something went wrong in the async operation!');
}, 100);
res.send('Hello World!');
});
app.listen(3000, () => {
console.log('App listening on port 3000');
});
When an error like the one above occurs in production, your browser typically sends a stack trace that looks something like this:
Error: Something went wrong in the async operation!
at http://localhost:3000/script.js:10:15
at http://localhost:3000/script.js:5:2
at http://localhost:3000/script.js:15:3
This is almost useless for debugging because script.js is likely a minified and bundled version of your original source code. The line numbers and column numbers (e.g., 10:15) refer to this generated file, not your original TypeScript or ES6+ code.
This is where source maps come in. A source map is a file that maps the generated code back to the original source code. When you build your JavaScript application using tools like Webpack, Rollup, or Parcel, you can configure them to generate a .js.map file alongside your .js file. This map contains the original file names, line numbers, and column numbers.
Elastic APM can then use these source maps to de-obfuscate the stack traces it receives. When an error is reported to the APM Server, it checks if a source map is available for the corresponding JavaScript file. If it is, APM reconstructs the stack trace, showing you the original file names and line numbers, making debugging significantly faster and more accurate.
To enable this, you need to ensure two things:
-
Source Maps are Generated: During your build process, make sure your bundler is configured to output source maps. For Webpack, this typically involves setting
devtoolto something likesource-maporhidden-source-mapin yourwebpack.config.js. For example:// webpack.config.js module.exports = { // ... other config devtool: 'source-map', // Or 'hidden-source-map' for production // ... };hidden-source-mapis often preferred in production as it doesn’t include the source map link in the generated JS file itself, which can be a minor security consideration. -
Source Maps are Uploaded to Elastic APM: The
elastic-apm-nodeagent can automatically upload source maps if configured correctly. You need to tell the agent where to find them and how to associate them with your application. This is typically done by setting thesource_map_parent_pathandsource_map_include_patternsoptions in your APM agent configuration.// In your Node.js app, where you start the APM agent: const apm = require('elastic-apm-node').start({ serviceName: 'my-js-app', serverUrl: 'http://localhost:8200', // ... other options sourceMaps: { enabled: true, // Path to the directory containing your built JS files and source maps. // This is relative to the APM agent's working directory. // If your build output is in a 'dist' folder, this would be 'dist'. parentPath: './dist', // Glob patterns to include specific source map files. // This is useful if you only want to upload maps for certain modules. include: ['**/*.js.map'], }, });The
parentPathtells the agent the root directory where your built JavaScript files and their corresponding source maps reside. Theincludepatterns specify which files within that directory should be considered for upload. The agent will then upload these source maps to the APM Server.Alternatively, you can use the Elastic APM CLI (
apm-sourcemap-uploader) for manual or scripted uploads, which is often more flexible for CI/CD pipelines.# Example using the CLI apm-sourcemap-uploader \ --service-name my-js-app \ --service-version 1.0.0 \ --url http://localhost:8200 \ --api-key <your_api_key> \ ./dist/bundle.js \ ./dist/bundle.js.mapThe key here is that the
serviceNameandserviceVersion(if used) must match what the APM agent is reporting for the corresponding transactions. The APM agent automatically sets theserviceVersionif you define it during startup:elastic-apm-node.start({ serviceVersion: '1.0.0' }).
The most surprising thing about source maps is that they are not just for debugging errors; they are also crucial for accurately attributing transaction timings and performance metrics back to your original code. Without them, APM might report that a significant chunk of time was spent in an anonymous or minified function, obscuring which part of your actual application logic was the bottleneck. The APM agent, when it detects a transaction with a JavaScript stack trace, will attempt to resolve that trace using available source maps. If it can resolve it, the performance breakdown in the APM UI will show the original function names and file paths, providing a much clearer picture of where your application spends its time.
Once source maps are correctly generated and uploaded, when an error occurs, Elastic APM will display a stack trace that looks like this in the UI:
Error: Something went wrong in the async operation!
at Object. (webpack:///src/index.js?:10:15)
at __webpack_require__ (webpack:///webpack/bootstrap:19:31)
at webpack:///src/index.js?:5:2
at __webpack_require__ (webpack:///webpack/bootstrap:19:31)
at webpack:///src/index.js?:15:3
You’ll see references to your original file names (e.g., src/index.js) and their original line/column numbers, allowing you to pinpoint the exact line of code that caused the issue.
The next hurdle will be managing source map uploads across multiple service versions and environments in a robust CI/CD pipeline.