Skip to content

Telling a real incident from a bot, a browser extension or a stale tab

Most of a new project's error feed is not your bug. The four signatures of noise, how to filter each one at the right layer, and the three signals that mean it is real.

Sentry5 min readships at docs/solutions/sentry/telling-a-real-incident-from-a-bot.md

Tags: sentry · triage · noise · bots · incident-response

You wire up Sentry on a Friday. By Monday there are 3,000 events across 60 issues, and the first four you open are:

  • TypeError: Cannot read properties of null (reading 'querySelector') from a file called inject.js
  • Loading chunk 4821 failed
  • Error: ENOENT: no such file or directory, open '/.env'
  • AbortError: The operation was aborted

None of them is a bug in your app. If you spend the morning on them you will learn nothing and, worse, you will start ignoring the feed, which is how the real incident on Thursday goes unnoticed for six hours.

The four signatures of noise

1. Browser extensions and injected scripts

Looks like: stack frames from chrome-extension://, moz-extension://, safari-web-extension://, or files you have never heard of (inject.js, content.js, gtm.js). Errors about DOM elements that do not exist in your markup. Often a wide spread of browsers and a single event per user.

Why it happens: an extension runs in the page, throws, and your global handler catches it. It is genuinely not your code.

Filter at: denyUrls in src/instrumentation-client.ts.

denyUrls: [/extensions\//i, /^chrome:\/\//i, /^moz-extension:\/\//i, /^safari-web-extension:\/\//i],

Do not dismiss too fast: an extension error that only happens on your checkout page can still mean your DOM broke an assumption it makes for everyone. Check whether the issue is spread across routes or concentrated on one.

2. Stale tabs after a deploy

Looks like: Loading chunk N failed, Failed to fetch dynamically imported module, Unexpected token '<' from a .js URL. A sharp spike immediately after each deploy, then decay over a few hours.

Why it happens: a user's tab has been open since before the deploy. It asks for a chunk hash that no longer exists, gets a 404 (or your HTML 404 page, hence the <), and fails.

Filter at: ignoreErrors. It is already there in this configuration.

But also fix the product problem: the user's tab is broken. Catch chunk load errors in the client and prompt a reload, or use a router-level error boundary that recovers by navigating. The error is noise; the broken session is not.

3. Bots, scanners and vulnerability probes

Looks like: server-side errors on paths you never wrote, /wp-admin, /.env, /phpmyadmin, /.git/config, /api/v1/../../etc/passwd. Malformed JSON bodies. null user on every event. A handful of IPs, or a thousand. Often a burst at a strange hour.

Why it happens: the internet scans everything. Within a day of a domain going live it is being probed continuously.

Filter at: ideally before your app: a WAF or platform-level rule. Failing that, ignoreErrors on the specific parse errors, plus making sure your 404 path does not throw.

The thing to actually check: a scanner hitting a route that exists and getting a 500 is a real finding. /api/users?id=1%20OR%201=1 returning a database error means your route is not validating input, and the bot just did your security review for you.

4. Users who left

Looks like: AbortError, The operation was aborted, ECONNRESET, ERR_STREAM_PREMATURE_CLOSE, ClientClosedRequest. Frequently on long requests: streaming responses, uploads, slow searches.

Why it happens: the client navigated away or closed the tab. The in-flight request was cancelled. Nothing is wrong.

Filter at: ignoreErrors on the server config, and check error.name === "AbortError" before capturing in your own handlers.

But: a rise in abort rate on one route is a real signal. Users abort requests that are too slow. If aborts on /search triple, that is a latency regression wearing a disguise.

The three signals that it is real

Against those four, the shape of a genuine incident:

1. It correlates with a deploy. Sentry shows first-seen release. If an issue's first event is the release you shipped twenty minutes ago and the count is climbing, stop reading and go look at that diff. This single signal resolves most real incidents.

2. Multiple distinct users, in a short window, on one route. Noise is usually one user many times, or many users once across many routes. A real break is many users, on one path, right now. Sort by user count, not event count.

3. The stack trace is in your code. The top frames are your files, your function names, your line numbers, not a vendor bundle, not an extension. Combined with signal 1, this is close to conclusive.

A triage rule that holds up

For each new issue at the top of the feed, in order:

  1. Is the top frame my code? No → probably noise; classify it as one of the four and filter it at the right layer.
  2. Is it new since a recent release? Yes → treat as a regression until proven otherwise. Read the diff before the stack trace.
  3. How many distinct users? Many → incident. One → interesting, not urgent.
  4. Is it growing, flat or decaying? Growing → act now. Decaying → probably a stale-tab spike, already over.
  5. Can I reproduce it? No → do not ship a speculative fix. A wrong fix changes the stack trace, which creates a new issue and makes it look resolved.

Suppress with a reason, and review it

Every filter you add is a decision to be blind to something. That is fine, and it needs a comment:

ignoreErrors: [
  // A user navigated away mid-request. Not actionable.
  "AbortError",
  // Stale tab holding a pre-deploy chunk hash. See the reload prompt in
  // src/components/chunk-error-boundary.tsx.
  "Loading chunk",
],

An ignoreErrors entry with no explanation is how a real error gets silently dropped two years from now, when the string happens to match something new.

Re-read the list every few months. Filters accumulate, browsers change, and the entry that was noise in one release can be masking a bug in the next.

Get to zero, then keep it there

The goal is not a small number of issues; it is a feed where every unresolved issue is something you intend to act on. That state is worth real effort to reach, because it changes what an alert means: a notification from a clean project is information, and a notification from a noisy one is a reflex you have already learned to ignore.