Uploading Source Maps for Datadog RUM
When a production JavaScript error lands in Datadog RUM’s Error Tracking view, the stack trace points at main.a1b2c3.js:1:48210 instead of the function and line in your original source. This how-to walks through using the datadog-ci sourcemaps upload command to attach source maps to the exact release version RUM tags on every event, so minified frames resolve back to readable code. It is a companion to Integrating Observability SDKs: Sentry, Datadog, OpenTelemetry and part of the wider Core JavaScript Error Handling & Boundaries reference. If you also transmit sensitive data in error strings, pair this with Scrubbing PII from Datadog RUM Error Payloads.
Symptom / Trigger
You ship a minified bundle, an error fires in production, and Datadog RUM’s Error Tracking console shows a stack trace built entirely from column offsets in the transpiled file. There is no way to tell which component or function actually threw.
TypeError: Cannot read properties of undefined (reading 'total')
at main.a1b2c3d4.js:1:48210
at main.a1b2c3d4.js:1:12904
at Array.forEach (<anonymous>)
at main.a1b2c3d4.js:1:12760
Source map not found for this stack frame.
Upload a source map for version 2026.7.24-a1b2c3d4 (service: web-store) to symbolicate.
The message Source map not found for this stack frame is Datadog telling you it received the error, matched it to a version and service, but has no .js.map stored under that pair. The symbolication is a lookup — and the lookup key is missing. This is distinct from an error that arrives with no stack at all; here the stack is intact, but every frame resolves to the transpiled artifact rather than your authored source. Alerting on this class of error is nearly impossible, because two completely different bugs in Cart.tsx and Checkout.tsx may both collapse to main.a1b2c3d4.js:1, defeating Datadog’s fingerprint grouping.
The symptom is intermittent by design. Frames from files that never had a map uploaded stay minified, while frames from a chunk you did upload resolve cleanly. A single error can therefore mix readable and opaque frames, which is often the first clue that the problem is a per-version or per-chunk gap rather than a global misconfiguration.
Root Cause Explanation
Datadog RUM symbolication is a join between two independent uploads. The browser SDK stamps every error event with the version and service you pass to datadogRum.init. Separately, your build must upload source maps tagged with the same version and service. If the two values disagree by even one character — a Git SHA in one place and a semver tag in the other — the join fails and frames stay minified.
// Broken pattern: the SDK version and the uploaded map version drift apart.
datadogRum.init({
applicationId: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx',
clientToken: 'pubXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX',
service: 'web-store',
version: '2026.7.24-a1b2c3d4', // SDK reports this on every event
});
// ...meanwhile the CI upload runs with a different version:
// datadog-ci sourcemaps upload ./dist --release-version=main --service=web-store
// RUM has no map stored under "2026.7.24-a1b2c3d4", so the join misses.
The second requirement is the //# sourceMappingURL= footer and the minified-path-prefix. Datadog matches a stack frame’s URL path (for example /static/main.a1b2c3d4.js) against the prefix you declare at upload time. Get the prefix wrong and even a correctly versioned map will not attach to the frame. Internally Datadog concatenates the prefix with the relative path of each .js.map inside the uploaded directory to reconstruct the full public URL of the minified file. That reconstructed URL is then compared to the URL Datadog observed in the stack frame. The comparison is exact — a trailing slash, an http versus https scheme, or a www subdomain difference is enough to break it.
Because the two uploads are decoupled, nothing in the SDK enforces that they agree. The browser is happy to report version: '2026.7.24-a1b2c3d4' whether or not a matching map exists, and datadog-ci will happily accept an upload under any release string you hand it. The join is only evaluated later, at read time, when you open the error. That deferral is exactly why the problem so often reaches production undetected: everything looks green until someone clicks into a stack trace.
Step-by-Step Fix
1. Emit source maps and hidden sourceMappingURL footers from your bundler
// vite.config.js — produce .js.map files and keep the footer out of served JS
export default {
build: {
sourcemap: 'hidden', // writes maps but strips the //# sourceMappingURL comment
rollupOptions: { output: { entryFileNames: 'static/[name].[hash].js' } },
},
};
The 'hidden' setting is the important detail. It still writes the .js.map alongside each chunk so the upload step has something to send, but it removes the //# sourceMappingURL comment from the served JavaScript. Browsers therefore never fetch the map, your original source stays off the public CDN, and Datadog still symbolicates because it holds its own copy.
2. Pin one release version and expose it to both the SDK and CI
# Compute the version ONCE and reuse it everywhere (build, SDK, upload).
export DD_VERSION="2026.7.24-$(git rev-parse --short HEAD)" # e.g. 2026.7.24-a1b2c3d4
echo "DD_VERSION=$DD_VERSION" >> "$GITHUB_ENV" # share across CI steps
Treat this line as the single source of truth for the release identity. Every downstream consumer — the environment variable injected into the bundle, the value passed to datadogRum.init, and the --release-version flag on the upload — must read from $DD_VERSION rather than recomputing its own string. The moment two steps derive the version independently, they can diverge, and a diverged version is the most common reason maps fail to attach.
3. Initialize the RUM SDK with that exact version and service
// The version here MUST equal $DD_VERSION used at upload time.
datadogRum.init({
applicationId: import.meta.env.VITE_DD_APP_ID,
clientToken: import.meta.env.VITE_DD_CLIENT_TOKEN,
service: 'web-store', // must match --service at upload
version: import.meta.env.VITE_DD_VERSION, // injected build-time = $DD_VERSION
});
4. Run datadog-ci sourcemaps upload keyed to the release version
# Upload every .js.map in ./dist, keyed to the release and matched by URL prefix.
DATADOG_API_KEY="$DD_API_KEY" DATADOG_SITE="datadoghq.com" \
npx @datadog/datadog-ci sourcemaps upload ./dist \
--service=web-store \
--release-version="$DD_VERSION" \
--minified-path-prefix=https://cdn.example.com/static/
The command walks the directory you pass, finds every .js.map, reads the sibling minified filename from each map’s metadata, and uploads the pair tagged with your service and release. It authenticates with DATADOG_API_KEY (not the client token used in the browser) and respects DATADOG_SITE, which must point at the same region as your RUM application — datadoghq.eu for EU organizations, us5.datadoghq.com for the US5 region, and so on. A mismatched site uploads maps into an organization that never receives your events.
5. Verify the upload count before promoting the deploy
# The command prints how many maps were accepted; fail the job if zero.
npx @datadog/datadog-ci sourcemaps upload ./dist \
--service=web-store --release-version="$DD_VERSION" \
--minified-path-prefix=https://cdn.example.com/static/ \
| tee upload.log && grep -q "Handled [1-9]" upload.log # non-zero maps required
The --minified-path-prefix is the single most error-prone flag. It must be the URL prefix under which browsers actually load your JavaScript, not a filesystem path. If your bundle is served from https://cdn.example.com/static/main.a1b2c3d4.js, the prefix is https://cdn.example.com/static/ and the map’s relative path inside ./dist completes the URL. When your assets are served from the same origin as the app, you may pass a path-only prefix such as /static/.
Verification
After the upload, trigger a known error in production, then open the event in Datadog Error Tracking. A symbolicated frame shows the original file, function, and line. You can also confirm the map is stored by re-running the upload in dry-run mode.
# Dry run reports which frames WOULD resolve without re-uploading.
npx @datadog/datadog-ci sourcemaps upload ./dist \
--service=web-store --release-version="$DD_VERSION" \
--minified-path-prefix=https://cdn.example.com/static/ \
--dry-run
# Expected tail of output:
# [DRYRUN] Uploading sourcemaps for version 2026.7.24-a1b2c3d4
# Handled 1 file with success in 0.4s.
In the Error Tracking view, the previously opaque frame now reads at renderCart (src/components/Cart.tsx:84:19) instead of at main.a1b2c3d4.js:1:48210. Symbolication applies to events ingested after the map is stored; it does not retroactively rewrite errors that arrived before the upload. If you are validating against a historical error, trigger a fresh occurrence so the read-time join runs against the newly stored map. It is worth adding the dry-run check to your deploy pipeline as a gate: because it fails loudly when zero maps match, it catches a broken --minified-path-prefix before the release reaches users rather than during the next incident.
Edge Cases & Gotchas
- Version drift is the number one failure. If the SDK’s
versionis a semver tag but CI uploads with a Git branch name, every join misses. Compute oneDD_VERSIONvalue and inject it into both the bundle and the upload step from a single source of truth. - The path prefix must be a URL, not a folder.
--minified-path-prefix=./distnever works; Datadog matches against the URL in the stack frame. Use the public URL prefix (https://cdn.example.com/static/or/static/). - Upload before the CDN goes live, or race conditions hide early errors. Errors that fire in the seconds between deploy and upload arrive with no stored map. Run the upload step before flipping traffic to the new release.
- Ship maps privately, not on the CDN. Use
sourcemap: 'hidden'so the//# sourceMappingURLfooter is stripped from served JS. The map goes only to Datadog, never to the public bundle, keeping your source private.
FAQ
How do I confirm a source map actually reached Datadog?
Re-run datadog-ci sourcemaps upload with the --dry-run flag and read the summary line. It reports Handled N file(s) and the exact version key it used. If N is zero, the glob matched no .js.map files — check that your bundler emitted maps and that you pointed the command at the right output directory.
Do I need to keep the .js.map files served on my CDN?
No, and you should not. Build with sourcemap: 'hidden' so the maps are generated locally for the upload but excluded from the deployed assets. Datadog stores its own copy at upload time and symbolicates server-side, so exposing maps publicly only leaks your original source.
Why does symbolication work for some frames but not others?
That pattern almost always means a partial version match. Third-party or lazily loaded chunks may carry a different version or service, or a vendor bundle may have no map at all. Upload maps for every emitted .js.map, and confirm each chunk’s URL falls under the declared --minified-path-prefix.
Related
- Integrating Observability SDKs: Sentry, Datadog, OpenTelemetry — the parent guide covering SDK wiring across vendors.
- Core JavaScript Error Handling & Boundaries — the top-level reference for browser error capture.
- Scrubbing PII from Datadog RUM Error Payloads — redact sensitive data before events transmit.
- Configuring Error Sampling Rates in Sentry Browser SDK — control event volume once symbolication works.