#Next.js#React

React Error Tracking: A Practical Sentry Integration

Users see a blank screen but you do not know about it. Sentry can catch errors before users report them. Learn frontend error tracking: Sentry SDK setup, Error Boundaries, source map uploads for production source lines, release tracking, performance monitoring, and privacy redaction.

Key Points

  • Topic Focus: Next.js、React
  • Suitable for: indie hackers, full-stack engineers, AI application developers
  • Core value: quickly understand background, practical paths, common pitfalls, and engineering advice
Author @admin·Published July 26, 2026·Updated July 26, 2026

A user reports, “The page is blank.” What is your first reaction? SSH into a machine and inspect logs, only to realize that frontend code runs in the user's browser and your server recorded nothing. Ask the user, “What error is in the console?” and they send you a blank-screen screenshot without even knowing what F12 is. By the time you reproduce it locally three days later, the impact has already spread to 5% of users.

This is the frontend team's most classic blind spot: your code runs in thousands of browsers, and you know nothing about its runtime state. The backend has logs and APM, so it knows first when something fails. When the frontend breaks, users always discover it first and support asks first. This article does not explain how to register a Sentry account. It only covers how to implement error tracking for a production-grade React application: SDK initialization, Error Boundaries, source map uploads, release association, performance monitoring, privacy redaction, the full Next.js setup, and alerting rules. Every item is something you will regret not doing on launch day.

1. Why the Frontend Must Have Error Tracking

First, accept a harsh fact: bugs reported by users are only the tip of the iceberg. When most users encounter an error, they do not report it. They refresh; if it works, they continue; if it does not, they quietly close the page and switch to a competitor. Some data shows that fewer than 5% of users who encounter severe errors proactively report them. In other words, a bug-discovery process driven by “user feedback” misses 95% of problems.

The “silence” of frontend errors is even worse. A backend 500 error immediately triggers monitoring alerts. A frontend undefined is not a function leaves the user staring at a blank screen while everything is silent on your side, unless you actively capture it. This is the fundamental reason Error Tracking exists: turn “we rely on users to report an incident” into “the system knows first when an incident occurs,” ideally catching and fixing it before users notice.

A mature frontend error-tracking system must answer four questions:

  1. What went wrong: error type, call stack, and error message.
  2. Who encountered it: user environment (browser, version, device) and activity path (which pages they visited and which APIs they called).
  3. Where it happened: the precise source line, not compressed chunk-a3f.js:1:28471.
  4. When it was introduced: which version and which release caused it, so you can roll back or locate the commit.

You can hardly answer all four with hand-rolled logs. You cannot log into user browsers, call stacks are minified, and nobody records version information. The purpose of tools such as Sentry is to solve these four issues as a package. This article uses Sentry as the example. Bugsnag, Rollbar, and Glitchtip follow the same idea, and explains how to integrate it properly.

2. Sentry.init: DSN and Sampling Rates, Get It Wired Up First

The first integration step is installing and initializing the SDK. In its simplest form, pass a DSN (Data Source Name, the reporting address for a Sentry project), and the SDK begins automatically capturing uncaught global exceptions.

bash
pnpm add @sentry/react
ts
// src/lib/sentry.ts
import * as Sentry from "@sentry/react";

export function initSentry() {
  Sentry.init({
    dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
    // Key: environment identifier, so Sentry can distinguish prod / staging / dev
    environment: process.env.NODE_ENV,
    // Release version, used for release tracking (see section 5)
    release: process.env.NEXT_PUBLIC_SENTRY_RELEASE,
    // Sampling rate; explained below
    tracesSampleRate: 0.2,
    // Defaults to 1.0: report 100% of error events. Only high-QPS sites need to lower it.
    sampleRate: 1.0,
    // Disable automatic reporting in development so data is not polluted
    enabled: process.env.NODE_ENV === "production",
  });
}

You must think through several key parameters instead of copying defaults and moving on:

A DSN is the project's unique identifier, and exposing it is not a secret leak. A DSN is a reporting address, not a secret. A frontend SDK must embed the DSN in client code, so Sentry uses it for routing on the server side, not authentication. To control “who can send data to this project,” use Sentry's inbound filters in the console, such as domain and IP limits, rather than hiding the DSN. Many people get this backward and think a leaked DSN is catastrophic. The real problem is failing to protect your quota, allowing someone to flood the project and consume your allowance.

tracesSampleRate is the sampling rate for performance data, not error sampling. This is one of the easiest traps for beginners. In Sentry, transactions/traces and error events are different things: errors are reported at 100% by default and controlled by sampleRate; transactions, used for performance monitoring and Web Vitals collection, are controlled by tracesSampleRate. A single page load can create many spans, so reporting all of them is both expensive and unnecessary. For a site with hundreds of thousands of daily page views, setting tracesSampleRate to 1.0 will make the bill make you question your life. Set it to 0.1–0.2 to retain statistical value while controlling cost.

You must pass environment explicitly. Without it, errors from local development, CI, pre-production, and production are mixed together, making it impossible to filter by environment in the console. I have seen a team aggressively report a local bug and generate one hundred thousand events in a day, burying the real production issues, simply because environments were not separated.

Error events themselves can use a function for more granular sampling decisions:

ts
Sentry.init({
  dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
  tracesSampler: (samplingContext) => {
    // Fully sample critical pages; sample peripheral pages at a lower rate
    if (samplingContext.transactionName?.includes("/checkout")) {
      return 1.0; // Payment flow: do not miss a single event
    }
    if (samplingContext.transactionName?.includes("/api/")) {
      return 0; // Do not separately sample API calls
    }
    return 0.1; // The remaining 10%
  },
});

Principle: capture every critical flow and sample peripheral pages. A function is much smarter than one fixed number.

3. Error Boundary: The Last Line of Defense for React Rendering Errors

After installing Sentry.init, the SDK automatically handles global synchronous exceptions, uncaught Promise rejections, and window.onerror. But React rendering errors are an exception: errors thrown while rendering a React component tree bubble to the nearest Error Boundary rather than to window.onerror. In other words, if a component's rendering fails and you have not installed an Error Boundary, the SDK receives nothing. The user sees the entire subtree blank out or the page unmount.

So initialization alone is not enough. You must use an Error Boundary to feed rendering errors to Sentry as well. @sentry/react includes a ready-made wrapper component, but writing your own is more common because you need to render a fallback UI for users:

tsx
// src/components/ErrorBoundary.tsx
import * as React from "react";
import * as Sentry from "@sentry/react";

interface Props {
  children: React.ReactNode;
  fallback: (error: Error, reset: () => void) => React.ReactNode;
  /** Distinguishes page-level from section-level boundaries for easier aggregation in the console */
  scope?: string;
}

interface State {
  error: Error | null;
}

export class ErrorBoundary extends React.Component<Props, State> {
  state: State = { error: null };

  static getDerivedStateFromError(error: Error): State {
    // Update state and trigger fallback UI rendering
    return { error };
  }

  componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void {
    // Report the error together with the React component stack
    Sentry.withScope((scope) => {
      scope.setTag("error_boundary", this.props.scope ?? "unknown");
      // componentStack is unique gold for Error Boundaries; it pinpoints components better than an ordinary call stack
      scope.setContext("react", { componentStack: errorInfo.componentStack });
      Sentry.captureException(error);
    });
  }

  reset = () => this.setState({ error: null });

  render() {
    if (this.state.error) {
      return this.props.fallback(this.state.error, this.reset);
    }
    return this.props.children;
  }
}

When using it, wrap at two levels: “page-level + key section-level.”

tsx
// Outer page fallback: even if the entire page fails, users still get an explanation
<ErrorBoundary
  scope="page"
  fallback={(error, reset) => (
    <CrashScreen error={error} onRetry={reset} />
  )}
>
  <App />

  {/* Local fallback: a failed widget does not affect the entire page */}
  <ErrorBoundary
    scope="recommendation-widget"
    fallback={() => <div>Recommendations are temporarily unavailable</div>}
  >
    <RecommendationWidget />
  </ErrorBoundary>
</ErrorBoundary>

There are two practical lessons here. First, layer Error Boundaries; do not wrap the entire application with only one. If the whole page has only one boundary, one small component failure blanks the whole page and users cannot even use the navigation bar. Wrap non-critical modules such as sidebars, recommendation slots, and comments individually so they degrade gracefully while the main flow remains usable. Second, componentStack is gold for React error tracking. A normal call stack tells you which function failed; componentStack tells you the full component path from the root to the failing component, making diagnosis an order of magnitude faster. Do not forget to put it in the scope.

4. Source Map Uploads: Map Minified Errors Back to Source Lines

After connecting the SDK and wrapping Error Boundaries, you begin receiving error events. You open one and see a call stack like this:

text
at e.exports (chunk-A3F9B2.js:1:28471)
at t.render (chunk-A3F9B2.js:1:18903)
at Object.Lc (framework-7c1.js:22:91023)

It is completely unreadable. Production code is minified; variable names become e, t, and Lc, and line numbers are compressed into the first line. You cannot tell which file and source line failed. That is what source maps solve.

A source map is a mapping file generated during the build that maps “minified code → source code” (.js.map). The browser still runs minified code, but you upload the source map to Sentry. Note that you upload it to Sentry's servers, not to the CDN alongside minified code, which would expose your source. When Sentry receives an error event, it uses the corresponding source map to reverse the mapping, and the console displays the original src/components/Payment.tsx:42.

Step one: enable source maps during the build. Bundler configuration varies slightly, but the key is to generate them without inlining them, and do not deploy them to the production CDN.

ts
// next.config.ts (excerpt; @sentry/nextjs takes over, see section 8)
// For pure Vite/Webpack projects, the idea is the same: output .map files during the build
const config = {
  // Key: make the bundler produce source maps
  productionBrowserSourceMaps: true, // Next.js
  // Vite: build.sourcemap = 'hidden' (hidden means the bundle does not reference .map,
  // so browsers cannot obtain the map, while it still exists on disk for upload)
};

The hidden value is critical: it produces .map files but does not write a //# sourceMappingURL= comment at the end of JavaScript files. Browsers therefore do not download the maps, so users cannot see the source, while the maps remain on disk for upload.

Step two: upload source maps to Sentry. Use @sentry/cli or a bundler plugin:

bash
# Upload manually with the CLI; suitable for scripting
SENTRY_AUTH_TOKEN=xxx sentry-cli sourcemaps upload \
  --org my-org \
  --project my-frontend \
  --release $(git rev-parse HEAD) \
  ./dist

A more production-ready approach uploads automatically as part of the build pipeline. Upload immediately after CI packages the build and associate it with the current release:

yaml
# .github/workflows/deploy.yml excerpt
- name: Build
  run: pnpm build            # Produces artifacts with source maps
- name: Upload sourcemaps to Sentry
  env:
    SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
  run: |
    sentry-cli sourcemaps upload \
      --release ${{ github.sha }} \
      --url-prefix "~/static" \  # Key: tells Sentry the production URL prefix for these files
      ./out/_next/static
- name: Deploy
  run: ./deploy.sh           # Deploy only afterward

--url-prefix is the most failure-prone part here. When Sentry receives an error, it gets the failing script URL from the call stack, such as https://cdn.app.com/_next/static/chunks/A3F9B2.js. It needs that URL to match the source map you uploaded. --url-prefix "~/static" means “the maps I uploaded correspond to the ~/static/ prefix of production URLs,” where ~ represents any domain. If the prefixes do not match, the source map is “uploaded but cannot be matched,” and the call stack remains garbled. After uploading, always open any error in the console and confirm that the call stack has become source paths. Only then is this step complete.

Another pitfall: do not commit source maps to Git or send them to the CDN. Committing them bloats the repository; sending them to the CDN exposes your source code. The correct approach is to upload to Sentry after CI builds, then remove .map files from the build artifacts before deployment.

5. Release Tracking: Identify Which Version Introduced a Bug

A large number of new errors suddenly arrive in production. Your first question is always: which version introduced them? Without release tracking, you can only look at the time: “we released yesterday, so it was probably that,” but you cannot be sure. With release tracking, Sentry directly tells you, “this error first appeared in v2.4.1 and accounts for 100%; it did not exist in v2.4.0.” That identifies the culprit in one sentence.

The approach is to carry a “version number” through the entire pipeline:

ts
// 1. Define a release identifier (using a commit SHA is recommended because it is absolutely unique)
// Inject in CI: export NEXT_PUBLIC_SENTRY_RELEASE=$(git rev-parse HEAD)

// 2. Include it in Sentry.init
Sentry.init({
  dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
  release: process.env.NEXT_PUBLIC_SENTRY_RELEASE, // Mentioned in section 4
});

// 3. Bind the same release when uploading source maps (the --release in the section 4 CLI command)
// 4. (Optional) Create a commit after release so Sentry knows about this deployment
bash
# After release, tell Sentry that this release was deployed
sentry-cli releases new $RELEASE
sentry-cli releases set-commits $RELEASE --auto   # Associate the corresponding Git commit
sentry-cli releases finalize $RELEASE

Once connected, the Sentry console gains several valuable views: New Issues, errors that appeared for the first time. This is the core quality metric for a release, making it immediately clear how many errors each release added. It also provides error trends by release. If error volume spikes after a release, you can make a rollback decision immediately. Without release tracking, you perceive release quality by whether users report that something broke. With it, you can see the health of a release in the console within 10 minutes. This is standard modern frontend CI, not an optional extra.

6. Performance Monitoring: Web Vitals Are Not Only for the Backend

Error tracking only answers “did it crash?” But “slow” is also a bug, and a more subtle one. Users will not tell you, “this page became 800 ms slower to load.” They will only feel that “this site is getting harder to use” and leave. Sentry Performance automatically collects browser-side Web Vitals and fills this gap.

Once enabled (tracesSampleRate > 0), the SDK automatically collects Core Web Vitals without manual instrumentation:

  • LCP (Largest Contentful Paint): time to paint the largest content, measuring “how long until users see the main content.”
  • INP (Interaction to Next Paint): interaction response delay. It replaces the outdated FID and is now the core measure of whether interactions feel smooth.
  • CLS (Cumulative Layout Shift): cumulative layout shift, measuring whether the page jumps around.

It also collects a waterfall breakdown of page loading: how much time DNS, TCP, TLS, TTFB, DOM parsing, and script execution each consume. This layer is the most valuable: you no longer need to guess where performance is slow. The console directly gives you a flame graph for slow requests. If a page has a p75 LCP of 3.2 seconds and you open it to find “waiting for the API response took 2.1 seconds,” the investigation immediately becomes focused.

If you want to manually track a specific business process, such as “add to cart through checkout completion,” you can start a transaction yourself:

ts
import * as Sentry from "@sentry/react";

async function checkout(cartId: string) {
  // Start a manual performance transaction to track the full checkout flow
  const transaction = Sentry.startTransaction({
    name: "checkout-flow",
    op: "checkout",
  });
  Sentry.getCurrentHub().configureScope((scope) => scope.setSpan(transaction));

  try {
    const span1 = transaction.startChild({ op: "validate", description: "Validate inventory" });
    await validateStock(cartId);
    span1.finish();

    const span2 = transaction.startChild({ op: "pay", description: "Call payment" });
    await pay(cartId);
    span2.finish();
  } finally {
    transaction.finish(); // Transactions that do not call finish are not reported; this is a common pitfall
  }
}

Principle: start with automatic Web Vitals, which cover most “slow page” problems. Only start manual transactions for specific business flows. Do not wrap every function in a span; that is noise, not data.

7. Privacy Redaction: Use beforeSend to Keep PII Out

This section is a hard compliance and security requirement, not an optional feature. Sentry reports a great deal of context by default: user information, request URLs, breadcrumbs showing what users clicked and which APIs they called, and, in some cases, serialized DOM elements. These can easily contain PII (personally identifiable information), such as email addresses, phone numbers, national ID numbers, tokens, and order numbers. Once PII enters Sentry, it is a compliance incident. GDPR and personal-information protection laws do not take it lightly.

The fallback redaction entry point is the beforeSend hook. Every event passes through it before reporting, and you can modify, delete, or discard it there:

ts
import * as Sentry from "@sentry/react";

Sentry.init({
  dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
  beforeSend(event) {
    return scrubEvent(event);
  },
});

const PII_PATTERNS: Array<[RegExp, string]> = [
  [/1[3-9]\d{9}/g, "[PHONE]"], // Mobile number
  [/\b[\w.+-]+@[\w-]+\.[\w.-]+\b/g, "[EMAIL]"], // Email address
  [/\b\d{15}(\d{2}[\dXx])?\b/g, "[ID]"], // National ID number
  [/\"(?:token|password|secret|apiKey)\"\s*:\s*\"[^\"]*\"/gi, '\"$1\":\"[REDACTED]\"'],
];

function scrubEvent(event: Sentry.Event): Sentry.Event | null {
  // 1. Remove query parameters from request URLs, which commonly contain tokens
  if (event.request?.url) {
    event.request.url = stripQueryParams(event.request.url);
  }

  // 2. Remove PII from breadcrumbs, including API URLs and message text
  event.breadcrumbs = event.breadcrumbs?.map((b) => ({
    ...b,
    data: b.data && scrubObject(b.data),
  }));

  // 3. Remove extra fields from error objects
  if (event.exception?.values) {
    for (const ex of event.exception.values) {
      ex.stacktrace?.frames?.forEach((f) => {
        if (f.vars) f.vars = scrubObject(f.vars);
      });
    }
  }

  // 4. User-provided identity information; do not report exact identities unless necessary
  if (event.user) {
    event.user.email = event.user.email ? "[EMAIL]" : undefined;
    event.user.ip_address = "[REDACTED]";
    // Retain user.id (an internal ID) for user aggregation, but do not include real identities
  }

  return event;
}

function scrubObject(obj: Record<string, unknown>): Record<string, unknown> {
  const out: Record<string, unknown> = {};
  for (const [key, value] of Object.entries(obj)) {
    if (/token|password|secret|authorization/i.test(key)) {
      out[key] = "[REDACTED]";
      continue;
    }
    if (typeof value === "string") {
      out[key] = PII_PATTERNS.reduce((s, [re, tag]) => s.replace(re, tag), value);
    } else {
      out[key] = value;
    }
  }
  return out;
}

function stripQueryParams(url: string): string {
  try {
    const u = new URL(url);
    u.searchParams.forEach((_, key) => {
      if (/token|auth|key|email|phone/i.test(key)) {
        u.searchParams.set(key, "[REDACTED]");
      }
    });
    return u.toString();
  } catch {
    return url;
  }
}

Three principles, in order of importance:

First, allowlists are better than blocklists. Rather than listing “which fields are PII and must be redacted,” list “which fields are allowed to be reported.” In reality, blocklist coverage in beforeSend is faster to implement, so make the blocklist broad and review it regularly. Second, do not put token values in URLs. Countless historical incidents happened because ?access_token=xxx was written into logs or monitoring systems. No matter how strict redaction is, it is better to put authentication in headers from the beginning and keep sensitive parameters out of URLs. Third, use internal IDs for user identity instead of reporting email addresses or phone numbers. Sentry's user feedback feature needs an email address, but that is a separate case. Default error events should only include user.id, your system's internal UUID. That is enough to aggregate “how many times this user encountered an error” without leaking identity.

Finally, if an event category should not be reported at all, such as test-environment noise or known third-party script errors, simply return null from beforeSend to discard it. It can redact data and serve as the final filter.

8. The Full Next.js Setup: @sentry/nextjs Configures Server, Client, and Edge Together

Everything above describes an approach for a pure React SPA. A Next.js application is mixed: it has client-side code, such as browser-run components; server-side code, such as API Routes, Server Actions, and Server Components; and the Edge Runtime used by middleware. These three runtimes have different contexts, and manually integrating Sentry separately for each becomes messy. @sentry/nextjs is designed for this: one package handles all three runtimes.

bash
pnpm add @sentry/nextjs
# The official wizard generates configuration files and updates next.config
npx @sentry/wizard@latest -i nextjs

The wizard generates three configuration files, one for each runtime, each calling Sentry.init:

ts
// sentry.client.config.ts -- browser
import * as Sentry from "@sentry/nextjs";
Sentry.init({
  dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
  tracesSampleRate: 0.2,
  replaysSessionSampleRate: 0.1,    // Session Replay: 10% sampling under normal conditions
  replaysOnErrorSampleRate: 1.0,    // Replay 100% of sessions with an error
});

// sentry.server.config.ts -- Node server (API Route / Server Action)
import * as Sentry from "@sentry/nextjs";
Sentry.init({
  dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
  tracesSampleRate: 0.2,
});

// sentry.edge.config.ts -- Edge Runtime (middleware)
import * as Sentry from "@sentry/nextjs";
Sentry.init({
  dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
  tracesSampleRate: 0.2,
});

These three files are not automatically loaded by Next.js. You must manually import them in instrumentation.ts, which is Next.js's conventional entry point and runs once at startup:

ts
// instrumentation.ts -- project root
export async function register() {
  if (process.env.NEXT_RUNTIME === "nodejs") {
    await import("./sentry.server.config");
  }
  if (process.env.NEXT_RUNTIME === "edge") {
    await import("./sentry.edge.config");
  }
  // The client configuration is injected automatically by Next.js and is not handled here
}

Then wrap next.config.ts with withSentryConfig. Configure source map upload here as well, without separately running the CLI:

ts
// next.config.ts
import { withSentryConfig } from "@sentry/nextjs";

const nextConfig = { /* your other configuration */ };

export default withSentryConfig(nextConfig, {
  // Upload source maps automatically during builds; depends on the SENTRY_AUTH_TOKEN environment variable
  sourcemaps: {
    deleteAfterCompile: true, // Delete from build artifacts after upload so they do not reach the CDN
  },
  // Release identifier
  release: { name: process.env.NEXT_PUBLIC_SENTRY_RELEASE },
  // Disable submitting commits to Sentry during releases to avoid CI permission issues
  disableClientWebpackPlugin: false,
  // Hide default org/project configuration other than console.log
  org: process.env.SENTRY_ORG,
  project: process.env.SENTRY_PROJECT,
});

With this configuration, a single pnpm build completes three things: builds artifacts for all three runtimes, uploads source maps automatically, and associates the release. This is the correct way to integrate Sentry in a Next.js project: do not hand-roll three SDK setups. Let @sentry/nextjs manage runtime distribution and source maps consistently. Session Replay is also an underrated feature. It records user actions in sessions with errors, so you can replay “what exactly the user clicked that caused the blank screen” in the console as if watching a video. It is a dramatic advantage for identifying reproduction conditions.

9. Alert Rules: Send New Errors to Slack Immediately

After implementing the previous eight sections, error events quietly sit in the Sentry console waiting for you to look at them. But nobody watches a console all day. Alerts are the output of error tracking. Otherwise, no matter how complete the data is, it only makes you wise after the fact. The core idea of Sentry alerts and rules is: alert only on “new things” and “abnormal growth,” not known existing issues.

The most valuable alert rule is “new errors”:

Rule: When a project has a newly reported error (New Issue), send a notification to Slack channel #frontend-alerts.

This covers the most important scenario: someone immediately knows about errors newly introduced after a release. Sentry deduplicates through the concept of an Issue. Identical call stacks are aggregated into one Issue, so the same error reported by 1,000 users still counts as one Issue. Therefore, a “new Issue” means “an error we have not seen before,” not “someone reported it again.” The latter creates alert fatigue; the former does not.

For a more advanced setup, add several more specific rules:

  • Error spike: an Issue's event count is five times higher than normal within one hour → alert, indicating sudden deterioration.
  • High-impact error: an Issue affects more than 100 users within one hour → alert, indicating a broad impact.
  • Any error in payment/checkout flows: a tag matches error_boundary:payment or a transaction matches /checkout → alert immediately, because core flows have zero tolerance.
ts
// Add tags in code so alert rules can match precisely
Sentry.withScope((scope) => {
  scope.setTag("module", "payment");
  scope.setTag("severity", "revenue-impacting");
  Sentry.captureException(err);
});

Alert channels include Slack, email, PagerDuty for P0 escalation, WeCom, and DingTalk. One recommended division of responsibility: ordinary new errors go to a Slack channel, where the team can see them without a mandatory response; core-flow errors go to PagerDuty, which requires an on-call response. Do not send every error to PagerDuty. Once alert fatigue sets in, nobody notices P0 incidents either. This is the lesson covered in the previous article.

One final practical lesson: alerts should carry enough context, but not the full text. A good alert includes an error summary as the title, affected-user count, first-seen time, release version, and a link to the Sentry console. Open the link for details instead of stuffing an entire call stack into Slack. Nobody reads a 50-line stack trace in a chat window. Sentry's Slack integration already works this way by default, so do not change it.

Summary

  • The fundamental value of frontend error tracking is turning reactive work into proactive work. Since 95% of users will not report bugs, waiting for user feedback means giving up 95% of your visibility. Set up error tracking so you can catch issues before users notice them.
  • Think through Sentry.init parameters: tracesSampleRate controls performance transactions rather than errors, which default to 100%; always pass environment explicitly; use a tracesSampler function for differentiated sampling on critical flows.
  • An Error Boundary is the only place to catch React rendering errors. Without one, the SDK cannot receive rendering errors. Layer boundaries rather than wrapping the entire page only once. componentStack is gold for locating components, so do not forget to report it.
  • Source maps are a prerequisite for readable call stacks. Build maps, upload them to Sentry, and align production URLs with --url-prefix. All three steps are required. Do not commit maps to Git or deploy them to a CDN.
  • Release tracking answers “which version introduced this?” in one sentence and makes release health visible within 10 minutes. Performance monitoring automatically collects Web Vitals, while waterfall charts show where slowness occurs.
  • Use beforeSend as a privacy-redaction fallback. Make blocklists broad, keep tokens out of URLs, and use internal IDs instead of email addresses or phone numbers for user identity. Next.js projects should use @sentry/nextjs to manage all three runtimes consistently rather than hand-rolling them.
  • Alert only on “new errors” and “abnormal growth.” Send core-flow errors to PagerDuty and ordinary new errors to Slack. Alerts should include a summary and a link, not a full stack trace.

In one sentence: error tracking is not a tool for after an incident; it is a system that lets you know an incident is coming. When users see a blank screen, your team should learn about it from a Slack alert rather than a support ticket. That difference is the difference in frontend engineering maturity. The return on investing in this system, like backend monitoring, is never visible on calm days. It appears when the next overnight outage happens.

Related

Related Content