A production error arrives as TypeError: e is not a function at t (main.4f3a9c.js:1:48211), and nobody can say which file it came from. Stack traces are minified and unreadable because the build renamed the functions and packed the code into long lines. The source map never reached the error tracker, or it was published for anyone to download.

Stack traces are minified and unreadable: what error tracking is, and what makes a trace readable again

Minified stack traces become readable when the error tracker holds the source map for that exact build. The build renames functions and packs the code into long lines; the map records where each position came from. Upload it privately at build time and the tracker shows the original file, line and function.

The trace in the opener is an illustration, not a real incident. Readable traces are one part of the wider work of hardening SaaS applications for resilience. A stack trace is the list of function calls that were running when the error happened, each with a file, a line and a column. A production build minifies and bundles the code, so every one of those positions points into generated code that no person wrote.

A source map is the file that undoes that. In MDN’s definition of a source map, it is “a JSON file format that maps between minified or transformed code received by the browser and its original unmodified form”. An error tracker receives each error from the running app, groups repeats into one issue, applies the source map to the frames, labels the event with its environment and release, and alerts someone. Sentry’s docs put the goal plainly: upload your source maps “to enable readable stack traces in your errors”. In the stack, error tracking lives in three places: an SDK in the browser and on the server, an upload step in the build, and an alert rule in the tracker.

PartWhat it doesWhat breaks without it
Protected source mapsTurns each minified frame back into the original file, line and function, while the maps stay off the public siteEvery issue points into one generated file, or anyone can download the original source
Environment labels and releasesMarks each event as production, preview, staging or development, and ties it to the deploy it came fromTest errors page you, and real ones hide in the noise
AlertsTells a named person when a new production error appearsErrors are recorded and nobody reads them

Deliverable 6.3 of the Production Hardening Sprint installs error tracking such as Sentry with protected source maps, environment labels, and alerts.

What goes wrong without it

Each missing part fails in a way you can see from the outside, and the first is the one founders hear about from customers: users report bugs nobody noticed, because nothing recorded them.

What is missingThe symptom you seeWhat it costs you
No error tracker at allYou learn about errors from customer emails, or from customers who leave without writingEvery fault waits for a complaint
A tracker with no source mapsEvery issue points into the same generated file, so issues cannot be told apart by causeThe tracker gets ignored
Source maps published beside the bundleAnyone can download the original source, comments and internal route names includedYour code is public, and so is any secret that was in it
No environment labelStaging and local errors page you at night, or production errors drown in test noiseYou stop trusting the alerts
Alerts sent to an inbox nobody readsErrors pile up in the tracker and nobody actsThe tracker becomes an archive

The third row exposes code, not secrets, unless a secret was in the code, which is the separate problem of API keys exposed on the frontend. The fifth row is a routing problem: where alerts land, and how loud they are, belongs to Slack alerting.

Take a founder who turns on source maps in a Vite build so the error tracker can show readable traces, while the host deploys the build’s output folder as it is. With build.sourcemap set to true, Vite creates a separate map file for each bundle and leaves in the comment that points to it, so the live site now serves the original source to anyone who follows that comment. The 'hidden' setting keeps the map files but suppresses those comments; uploading the maps to the tracker and deleting them before deploy keeps the traces readable and the code private.

In my audits, 17 of the 21 third-party apps had no error tracking or alerting: when a user hits an error, nothing records it. Those 21 are the apps I audited in June and July 2026, 11 public vibe-coded apps audited across all 12 pillars and 10 held-out apps audited blind. That is a selected set, not a random sample, so the count describes those apps and is not a rate for AI-built apps in general. In the same audits, the Reliability & Correctness pillar averages 31.4 out of 100, scored on all 21, the lowest of the 12 pillars.

What it feels like when a customer finds the bug first is its own story, told in when your app fails silently and says 200 OK. An uptime check catches none of the five rows above, because uptime monitoring and error tracking do different jobs.

How to do it on Next.js, Vite and a mobile client

The steps below come from the build tools’ documentation and Sentry’s, not from a test run for this page. Installing the SDK itself is already written up in error logging best practices, so this section starts after the SDK is in. Plan prices, and the Sentry vs Datadog choice, are a separate topic. Two neighbors stay out too: retries, where the question is what exponential backoff is, and Python tracebacks on the server, which follow Python error handling best practices.

Upload source maps without exposing them

Source maps stay private when the build emits them without the sourceMappingURL comment, CI uploads them to the error tracker with a token kept as a CI secret, and the .map files are deleted before deploy. Then a request for the bundle’s .map URL on the live site should not return the map; that request is my check.

Sentry’s source maps guide starts with a wizard, npx @sentry/wizard@latest -i sourcemaps, and has plugins for webpack, Rollup, Vite and esbuild, with Sentry CLI for anything else. The same guide notes that browser apps can also host their maps publicly; the setup below is for keeping them where only the tracker reads them. The one setting that differs by build tool is how the maps are made:

Build toolSetting that emits maps without the public commentWhat the docs say
webpackdevtool: 'hidden-source-map'”Same as source-map, but doesn’t add a reference comment to the bundle”
Vitebuild.sourcemap: 'hidden'“‘hidden’ works like true except that the corresponding sourcemap comments in the bundled files are suppressed”
Next.jsNo setting: production builds make no browser maps, and turning on productionBrowserSourceMaps publishes them”Next.js will automatically serve these files when requested”
Next.js with Sentry’s SDKwithSentryConfig in next.config”When you run next build, source maps are generated and uploaded to Sentry”

The sources are webpack’s devtool options, Vite’s build.sourcemap option and Next.js productionBrowserSourceMaps. Next.js is the opposite case from the other two: its docs say browser maps are off in production builds “to prevent you leaking your source on the client”, so the flag alone is the leak, and on Next.js the tracker’s SDK makes and uploads the maps instead. The six steps, in my order:

  1. 01 Make the build emit maps without the sourceMappingURL comment: hidden-source-map on webpack, build.sourcemap set to hidden on Vite, and on Next.js leave productionBrowserSourceMaps off and let the Sentry SDK make the maps.
  2. 02 Upload the maps in CI with the bundler plugin or sentry-cli, using an organization auth token stored as the CI secret SENTRY_AUTH_TOKEN.
  3. 03 Tie each map to its build: Sentry links maps by debug IDs by default, and matching by release name is an optional, stricter extra that needs the same name in the upload and in Sentry.init.
  4. 04 Delete the .map files from the output folder before the deploy step: filesToDeleteAfterUpload on the Vite plugin, and on by default for client maps in the Next.js SDK.
  5. 05 Upload the maps of server bundles and functions too; the delete step is only for the folder the site serves to browsers.
  6. 06 Run the exposure check on the live site: the bundle URL with .map added must not return the map, and the bundle must not name a reachable map.

For step 2, Sentry’s Vite guide asks for an organization token, or a personal token with the “Project: Read & Write” and “Release: Admin” permissions, and recommends adding it “to your CI/CD environment as an environment variable”. Hosts that build for you, such as Vercel, run the build themselves, so SENTRY_AUTH_TOKEN goes into the project’s environment variables there; Sentry’s Next.js guide says that if traces still show minified code, “check that SENTRY_AUTH_TOKEN is set in your CI environment”, and it also mentions a Vercel integration for uploads during deployment. For step 3, Sentry’s CLI guide warns that release and dist values “will make the entire process less forgiving, which may lead to your code not being unminified by Sentry”, so my working rule is debug IDs alone, with a release name added only where something needs it.

Step 4 decides whether the maps leak. Sentry’s Vite guide says it directly: “Generating source maps may expose them to the public, potentially causing your source code to be leaked”, and names two fixes, denying access to .js.map files at the server or deleting the maps after upload. On Next.js, the SDK’s delete option is on by default: it removes the maps in .next/static/ and keeps those in .next/server/, which Sentry says “are not publicly accessible to users”. In my reading, that is step 5 in practice: server maps go to the tracker and stay off the public folder.

A Vite app built in CI, with build.sourcemap set to 'hidden', can do steps 2 to 4 in four commands. The CLI comes from the @sentry/cli npm package, dist is Vite’s default output folder, and the CLI reads SENTRY_ORG, SENTRY_PROJECT and SENTRY_AUTH_TOKEN from environment variables, which the CI fills from its secrets.

# CI job, after: npm install @sentry/cli
npm run build                                          # emits dist/ with hidden maps
node_modules/@sentry/cli/bin/sentry-cli sourcemaps inject ./dist   # adds debug IDs
node_modules/@sentry/cli/bin/sentry-cli sourcemaps upload ./dist   # sends maps to Sentry
find ./dist -name "*.map" -delete                      # nothing left to download
# deploy ./dist only after this line

Step 6 is my working rule: request the map URL from the live site with curl -i and read both the status and the body. A host that returns 404 passes. A host that rewrites unknown paths to the app answers 200 with the app’s HTML page, which in my reading also passes; only a JSON body means the map is public, since a source map is a JSON file. Browsers find a map through a SourceMap HTTP header or a sourceMappingURL annotation, so the bundle’s own response should carry no header that names a reachable map, and any map named in the bundle’s last lines must fail the same request.

curl -i https://your-app.example/assets/index-4f3a9c.js.map | head -n 20

Environment labels and releases: so an alert means production

Every error event needs an environment, a release set to the commit SHA, and an internal user id. Under my working rule, alerts filter on production only, so a broken preview build pages nobody, and a new error can be tied to the deploy that caused it.

The trap is the default. Sentry’s JavaScript SDK options list environment with the default production, so in my reading a preview deployment that never sets it can report its errors as production. Set the value from the host’s own variable, never as a hardcoded string. On Vercel, VERCEL_ENV can be production, preview or development, and VERCEL_GIT_COMMIT_SHA is the commit the deployment was triggered by; both are available at build and at runtime. The release is what lets Sentry “tell you about regressions between releases”, so each deploy should send its own release name.

TagValueWhere it is setWhat it enables
environmentproduction, preview, staging or developmentSentry.init, from the host’s variable (on Vercel, VERCEL_ENV, which can be production, preview or development)Alert rules that fire on production only
releaseThe commit SHA (VERCEL_GIT_COMMIT_SHA on Vercel)Sentry.init, the same name as any release-matched uploadA new error tied to the deploy that introduced it
User idYour internal id, never the emailSentry.setUser({ id: user.id }) after sign-inHow many users an error hits, without their email

Where the alert can go depends on the tracker’s plan. Sentry’s free Developer plan lists “Alerts and notifications via email” and is “Limited to one user”; “API & third-party integrations”, which a chat or on-call destination needs, start with the Team plan. Plan prices sit with the Sentry vs Datadog topic. What each event should carry beyond these three tags, and which errors deserve a page at all, are in the error logging article; where the alerts are routed is the Slack alerting topic above.

Crash reporting services that comply with GDPR: what to check before you pick one

No crash reporting service is GDPR compliant by itself; the app’s use of it is what counts. My checks: the processor contract Article 28 requires, the transfer mechanism, the data region, default PII collection switched off, scrubbing before send, a retention period, and the service listed as a sub-processor.

This applies to apps with users in the EU, under the EU GDPR, or in the UK, under the UK GDPR. Both texts set the same contract rule in Article 28(3); they differ in which law they name. The EU GDPR Article 28 says processing by a processor “shall be governed by a contract or other legal act under Union or Member State law”; the UK GDPR Article 28 says “under domestic law”. The first three checks below are legal requirements; the last four are my working rule.

  • A contract with the service as your processor that covers what Article 28(3) lists: the subject-matter and duration of the processing, its nature and purpose, the type of personal data, the categories of data subjects and the obligations and rights of the controller.
  • A transfer mechanism, if the service stores or reads events outside the EU or the UK.
  • The service on your sub-processor list and in a data map for GDPR; what a sub-processor is decides who belongs on that list.
  • The data region chosen when the account’s organization is created, where the service offers one. Sentry offers the US or the EU under “Create a New Organization”, and once selected the location “can’t be changed”.
  • The SDK’s automatic collection set to what you need. From version 11, Sentry’s JavaScript SDK collects user identity, cookies, headers and request bodies by default; each category can be turned off.
  • Request bodies, cookies and headers scrubbed in the app before the event is sent, with the service’s own scrubbing as a second layer.
  • A retention period for events, chosen and written down.

For the region check, Sentry’s data storage location page also lists some data that “may be stored in the US, regardless of your selected Data Storage Location”, user accounts and access tokens among it. For scrubbing, Sentry recommends its beforeSend hooks “to scrub any data before it is sent”, and Sentry’s server-side data scrubbing is “enabled by default” but covers only “a select number of fields”, such as values whose keys contain password, token or secret. The full list of what to scrub is in the logging article’s section on removing sensitive data.

On a phone, consent is the extra question. Firebase Crashlytics says that “by default, Crashlytics automatically collects crash reports for all your app’s users”, and its Crashlytics opt-in reporting setting turns that off. On Android, a firebase_crashlytics_collection_enabled meta-data tag set to false stops automatic collection, and a call to setCrashlyticsCollectionEnabled(true) after the user agrees turns it on; the choice persists across later launches. This is a list of checks, not legal advice.

If the app has a mobile client: crash reporting on the device

Mobile error logging needs the device’s version of a source map uploaded to the tracker for every build, plus app version and build number on every event, because old builds stay installed. My device rules: keep personal data out of breadcrumbs, and expect events sent late from devices that were offline.

Why a store build’s crash report is unreadable without symbols, on Google Play and the App Store, is covered under why an app crashes after publishing to the store. Here is only what the error tracker needs from each build:

PlatformWhat the tracker needs per buildWhere the docs describe the upload
iOS, native codeThe dSYM debug symbol filesFirebase Crashlytics: a run script that uploads dSYM files during the build phase
Android, code shrunk by R8, ProGuard or DexGuardThe mapping fileFirebase Crashlytics: its Gradle plugin uploads the mapping file when the build generates one
React Native, JavaScript CoreA source map for the JavaScript bundleSentry’s React Native SDK: automatic with Xcode and Gradle “if you do not use custom values”
React Native, HermesThe Hermes bytecode map composed with the Metro mapSentry’s Hermes guide: compose the two maps with React Native’s compose-source-maps.js, then upload

The setup pages are Sentry’s React Native source maps and Firebase’s guides to deobfuscated reports for each platform. Three device habits are my working rule. The first puts the app version and build number on every event, because an old build keeps sending errors long after you shipped a fix. The second keeps breadcrumbs to screen names and internal ids; Sentry’s own advice is “Do not log PII” when log statements become breadcrumbs. The third expects some events to arrive late: Sentry’s React Native SDK keeps envelopes in a local cache, 30 by default (maxCacheItems), and deletes the oldest when the cache is full, which in my reading means a phone that was offline for a while can send yesterday’s crash today, or lose the oldest ones.

Tracking an error across more than one app or service

A wrong value can show up in one app after another one wrote it: the web app, a background job, an automation tool or a webhook receiver. To track down where data errors are happening between apps, the tracker needs one id that travels with the request. My working rule is to generate a request id where the request first enters your system, pass it on in a header, write it into every log line, and attach it to every error event as a tag with Sentry.setTag. Then one search for that id finds the browser error, the API error and the failed job together.

Sentry’s distributed tracing does the same job with a trace id when every service runs its SDK: a trace is “the collection of all transactions and spans that share a trace_id value”, and it can follow a request “from the frontend to the backend and back”, including the background jobs it starts. In the browser SDK, the trace travels in two HTTP headers, sentry-trace and baggage, which your CORS settings and proxies must let through. The logging half, the request id on every line, is the subject of how to do logging; making an API fail loudly at its boundary, so the error is recorded where it starts, is part of API error handling best practices.

How to verify it

Error tracking is verified with one deliberate error in a production build: the tracker shows the original file and line, the environment reads production, the release matches the commit, the alert reaches a person, and the map URL does not return the map. A preview build throwing the same error pages nobody.

These six checks are my working rule; each ends with the evidence I keep.

  1. 01 Deploy a production build with a test route or button that throws a named error in the browser and a second one on the server; the route requires authorization and is removed afterwards. Keep both event ids.
  2. 02 Open each event in the tracker: the frames show the original file, line and function, not the bundle. Keep a screenshot of the resolved trace.
  3. 03 Read the event tags: environment is production and release equals the deployed commit SHA. Keep the tags.
  4. 04 Confirm the alert reached the named person and write down the minutes from throw to alert. Keep the alert and the time.
  5. 05 Request the map URL with curl: a 404, or behind a rewrite a body that is the app page and not JSON, and no reachable map named by the bundle. Keep the output with its first lines of body.
  6. 06 Throw the same error from a preview deployment: it is recorded as preview and pages nobody. Keep the event and the quiet channel.

For check 4, the channel depends on the plan: email on Sentry’s free Developer plan, and a chat or on-call tool only on a plan with integrations. Give the test error a new name on each run, because in my reading a rule that alerts on a new issue does not fire again for a repeat of an old one. The wider drill, with payload checks, failing jobs and a dead log destination, is the logging article’s failure drill. An error that a frontend boundary catches still has to be sent to the tracker, which is part of how to add a frontend error boundary.

In the Production Hardening Sprint, deliverable 6.3, error tracking, is verified this way: send a test error and verify symbolication, environment attribution, and alert delivery.

Where the sprint does this

Error tracking, deliverable 6.3 above, sits next to two deliverables from the sprint’s logging, monitoring and alerting area. Deliverable 8.4, actionable alert routing, routes alerts to the designated Slack or email destination and tunes thresholds to reduce noise; we verify it by sending test alerts and verifying their destination, context, and response instructions. Deliverable 8.1, structured, sanitized logs, adds structured request logs with correlation IDs and appropriate user references and excludes passwords, tokens, and unnecessary personal data; we verify it by tracing a test request across services and checking log content for sensitive fields. Hosting, paid tools, and API usage remain in your accounts. Deliverable 13.1, the production readiness report, delivers the result for every scope item, the work completed, and its verification evidence, and we verify it by accounting for all 123 IDs, keeping failures visible until resolved and explaining genuine non-applicable items. Every deliverable and its check is listed in the published scope.

Common questions about minified stack traces and error tracking

How to revert minified JS?

Apply the source map from the same build: for your own code, the error tracker reverts each event’s frames once the map is uploaded. For a single trace in a React or other JavaScript app, Mozilla’s source-map library can do it: the originalPositionFor method of its SourceMapConsumer “Returns the original source, line, and column information for the generated source’s line and column positions provided.” Without the map, the original names and comments cannot be recovered; a code formatter brings back line breaks and indentation, not the names.

How to interpret a stack trace?

Read it from the top down to the first frame in your own code. The first line gives the error type and the message; each line below it is a function that was running, with its file, line and column. In the illustrative trace at the top of this page, TypeError is the type, e is not a function the message, and main.4f3a9c.js:1:48211 a position in the bundle that only the source map can turn back into your file and line.

What are the best error monitoring tools?

The best tool for a small app is one that does three jobs well: applies your source maps without them being public, labels every event with environment and release, and sends alerts where a named person reads them. The examples on this page use Sentry. Choosing between trackers, and what each one costs, is the Sentry vs Datadog question, which this page does not answer.

What is the best way to report a bug?

Say what you did, what happened, what you expected, and when, and include the error id the app showed, if it showed one. That id lets the developer open the exact event in the error tracker, which is why my working rule is that an app’s error screen should show it.