The right way to fingerprint errors so you don't drown in duplicates
Default fingerprints over-group library noise and under-group your real bugs. Here's a fingerprint strategy that mirrors how engineers actually triage.

It's 3:00 AM and your phone is vibrating off the nightstand. You check PagerDuty: 450 new errors in the last 10 minutes. You rush to your dashboard, heart racing, only to realize they are all the exact same NullReferenceException you saw yesterday. Because you deployed a minor CSS change or added a comment at the top of a file, the line numbers shifted, the hash changed, and your error tracker treated it as a brand-new "critical" event. This isn't an "error" problem; it's a fingerprinting problem.
Most engineering teams treat error grouping as a black box. They install an SDK, point it at a DSN, and hope for the best. When the dashboard gets noisy, they blame the tool. But the reality is that default fingerprinting is a naive heuristic. To run a stable production environment, you need to move beyond simple stack-trace hashing and implement semantic grouping. You need to ensure that one root cause equals one issue, regardless of line number shifts or varying error messages.
Why default fingerprinting is a "noisy" neighbor
Most error tracking SDKs—including Sentry and GlitchReplay—generate a default fingerprint by hashing the stack trace. They look at the function names, the filenames, and the line numbers of the top few frames. If any of those pieces of data change, the hash changes. This creates the "Noisy Neighbor" effect, where your issue tracker becomes cluttered with duplicates that distract from real regressions.
The "Line Shift" Trap
The most common cause of duplicate errors is the line shift. Imagine you have a bug on line 42 of api.ts. You haven't fixed it yet, but you do a deployment to add a new feature. In that deployment, you add a five-line comment at the top of api.ts to document a different function. Now, that same bug occurs on line 47. To a human, it's the same bug. To a default hashing algorithm, it's a unique event. You now have two distinct issues in your dashboard, two sets of alerts, and two different places where developers are leaving comments. This fragmentation makes it impossible to track the actual frequency of the bug over time.
The "Generic Error" Problem
The inverse problem is "over-grouping." This happens when a generic error message, like DatabaseError: Connection Timeout, is thrown from multiple parts of the application. If your SDK groups by message alone, or if the stack traces happen to look similar (for example, if they both pass through a common database middleware), they get merged into one. You might think you have one occasional timeout in your auth service, but you're actually missing a catastrophic failure in your payment processing logic because it's being hidden inside the same issue bucket.
The Anatomy of a Fingerprint
To fix this, you have to understand what actually goes into a grouping decision. A fingerprint is essentially a string (or an array of strings) that tells the server how to categorize the event. If two events have the same fingerprint, they belong to the same issue. If they don't, they are separated.
By default, the SDK constructs a hash using a hierarchy of data:
- Stack Trace: The primary source. The SDK hashes the filenames and function names.
- Exception Type: For example,
TypeErrororRangeError. - Exception Value: The message string, like "cannot read property 'id' of undefined."
When you provide a custom fingerprint, you are telling the SDK to ignore its default logic and use your specific value instead. In Sentry-compatible SDKs, this is usually done via an array of strings. For example, ["database-connection-failure"]. If you want to keep the default behavior but add a modifier, you can use the {{ default }} placeholder: ["{{ default }}", "high-priority"].
Strategy 1: Semantic Fingerprinting (Grouping by Intent)
The most effective way to reduce noise is to move away from "where it happened" and toward "why it happened." This is semantic fingerprinting. Instead of letting the SDK guess based on the stack trace, you explicitly label the error based on the intent of the code.
Using Error Codes over Messages
One of the biggest mistakes developers make is putting dynamic data into error messages. For example, Error: User 123 failed to login. Every time a different user fails to login, you get a new issue because the message string is different. The first step is to use parameterized messages, but the better step is to use internal error codes. If your system throws an error with a code property like ERR_AUTH_001, you should use that code as the fingerprint.
try {
await login(user);
} catch (error) {
Sentry.withScope((scope) => {
if (error.code) {
scope.setFingerprint([error.code]);
}
Sentry.captureException(error);
});
}
In this scenario, it doesn't matter what the message says or what line the error was thrown on. If the code is ERR_AUTH_001, it goes into the same bucket. This is incredibly powerful for API-driven applications where you want to group by the failure reason rather than the specific request that triggered it.
Parameterized Fingerprints and Scrubbing
If you cannot use error codes, you must at least scrub your error messages before they reach the server. High-volume applications often suffer from "UUID pollution," where unique identifiers in the message string prevent effective grouping. You can use a beforeSend hook in your SDK configuration to regex-replace IDs, hashes, and email addresses with placeholders.
Sentry.init({
dsn: "https://example@glitchreplay.com/1",
beforeSend(event) {
const exception = event.exception?.values?.[0];
if (exception && exception.value) {
// Replace UUIDs with a placeholder
exception.value = exception.value.replace(/[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-.../, '<uuid>');
}
return event;
}
});
Strategy 2: Dealing with Third-Party Noise
If you run a client-side JavaScript application, you know that 80% of your errors come from code you didn't write. Analytics scripts, ad trackers, and chat bots are notorious for throwing unhandled exceptions that have nothing to do with your product. These "phantom" errors are the primary source of alert fatigue.
The "In-App" Frame Filter
The SDK usually tries to distinguish between "in-app" frames (your code) and "out-of-app" frames (node_modules or third-party CDNs). You can explicitly instruct the tracker to ignore stack frames that originate from specific domains. This ensures that an error in a Facebook tracking pixel doesn't get grouped as a failure in your checkout logic just because the pixel was triggered by a button click on the checkout page.
Global Fingerprint Collapsing
Sometimes, a third-party script will throw a generic Script error. which provides zero context due to Cross-Origin Resource Sharing (CORS) restrictions. Instead of having hundreds of "Script error" issues, you should collapse them all into a single, low-priority bucket. This keeps your main triage view clean.
beforeSend(event) {
if (event.exception?.values?.[0]?.value === 'Script error.') {
event.fingerprint = ['third-party-script-error'];
event.level = 'info'; // Downgrade priority
}
return event;
}
Strategy 3: Architecture-Aware Grouping
Your fingerprinting strategy should mirror your system design. If you have a microservices architecture, a Timeout in your Auth service is a fundamentally different event than a Timeout in your Search service, even if they share the same database driver code.
Grouping by Route or Controller
One effective strategy for web applications is to include the route or the controller name in the fingerprint. This ensures that failures are isolated to the specific feature they affect. A 500 Internal Server Error on the /blog page might be a low priority, but a 500 on /checkout is an immediate emergency. By appending the route to the fingerprint, you create this natural isolation.
scope.setFingerprint(['{{ default }}', routeName]);
The "Version-First" Approach
Sometimes you want to force a split between errors occurring in different versions of your app. While most trackers allow you to filter by release, including the major version in the fingerprint can be useful if you've done a massive refactor. This prevents "Legacy Bug A" from being merged with "New Architecture Bug B" simply because the error message stayed the same. However, use this sparingly; you generally want to know if a bug has persisted across multiple releases. You can read more about how source maps impact this in our guide on how source maps affect stack trace accuracy.
How Session Replay Validates your Fingerprints
Fingerprinting is a hypothesis. You are guessing that two events are the same. Session Replay is how you prove it. When you have integrated session replay, you can jump from a grouped issue directly into a video of the user's session. This provides the "Visual Proof" that text-heavy logs lack. The differences between session replay and traditional logging really shine here, and grouping is where that synergy pays off.
Spotting "False Positives"
If you see an issue with 10,000 events, watch three different replays from that group. If one user is clicking a "Delete" button and the other is just loading a page, your fingerprint is too broad. You've accidentally merged two different bugs because they happened to hit the same generic catch-all. Without the visual context, you might spend hours trying to reproduce a "delete" bug by looking at "page load" data.
Fixing "Over-grouping"
When you realize you have a "Mega-Issue" that contains multiple distinct bugs, you can use the evidence from GlitchReplay to refine your fingerprinting logic. You might notice that the users who are failing on "Delete" all have a specific metadata tag or are hitting a specific API endpoint. You can then update your code to include that endpoint in the fingerprint, effectively splitting the mega-issue into actionable, specific tasks.
Checklist: The "Triage-First" Fingerprint Audit
If you feel like you're drowning in noise, don't try to fix everything at once. Follow this audit checklist to clean up your dashboard in a single afternoon:
- Audit your "Top 10": Look at the 10 most frequent issues by volume. Are they actually 10 different bugs, or is the same bug appearing 5 times?
- Identify "One-off" duplicates: Search for identical error messages that aren't grouped together. Check their stack traces. Is a line shift causing the split?
- Implement a FingerprintBuilder: Create a utility function in your codebase that centralizes your grouping logic. Don't scatter
setFingerprintcalls everywhere. - Scrub UUIDs and Emails: Add a
beforeSendregex scrubber to handle dynamic data in messages. - Bucket Third-Party Noise: Force all "Script error" and "Extension failed" messages into a single low-priority bucket.
The goal of error tracking isn't to see every single thing that happens. The goal is to see every unique thing that happens. Every duplicate alert is a tax on your team's attention and a step toward burnout. By taking control of your fingerprinting strategy, you turn your error tracker from a noisy firehose into a precise diagnostic tool.
At GlitchReplay, we built our tool specifically for teams who are tired of being penalized for their own noise. Because we offer flat-rate pricing rather than charging per-event, you don't have to worry about a "Line Shift" trap blowing up your monthly bill. You can learn more about our philosophy on flat-rate pricing vs per-event charges here. Stop drowning in duplicates and start fixing bugs—switch to a tracking strategy that values your time as much as your data.
GlitchReplay is Sentry-SDK compatible, includes session replay and security signals, and never charges per event. Free to start, five minutes to first event.