Skip to content

🥅 server: fingerprint service errors by name and message#834

Open
cruzdanilo wants to merge 1 commit intomainfrom
fingerprint
Open

🥅 server: fingerprint service errors by name and message#834
cruzdanilo wants to merge 1 commit intomainfrom
fingerprint

Conversation

@cruzdanilo
Copy link
Member

@cruzdanilo cruzdanilo commented Feb 23, 2026


Open with Devin

Greptile Summary

replaces complex error message parsing logic with clean ServiceError type checking for sentry fingerprinting. the old approach parsed error messages to extract http status codes and json payloads (splitting on "Error:", regex matching status codes, json parsing). the new approach uses ServiceError.name and ServiceError.message directly when available, and falls back to generic fingerprinting for any error with a status property in instrument.cjs.

  • removed 27 lines of string manipulation logic in server/index.ts
  • added simple instanceof check for ServiceError
  • added beforeSend hook in instrument.cjs to fingerprint errors with status property using error.name and error.message
  • renamed error to revert in the contract error handling loop to avoid variable shadowing

Confidence Score: 5/5

  • safe to merge with no risk
  • simplifies error handling by removing fragile string parsing and using type-safe checks instead. the change is well-tested (ServiceError has comprehensive test coverage), reduces complexity significantly, and maintains backward compatibility by keeping undefined fingerprint for non-service errors
  • no files require special attention

Important Files Changed

Filename Overview
.changeset/bright-falcon-dance.md adds changeset for patch release with correct emoji and lowercase format
server/index.ts replaces complex string parsing with simple ServiceError instance check for error fingerprinting
server/instrument.cjs adds generic error fingerprinting for errors with status property, renames variable to avoid shadowing

Last reviewed commit: 6e7a38e

Summary by CodeRabbit

  • Bug Fixes
    • Enhanced error tracking and identification for service errors, improving debugging and error reporting capabilities.

@changeset-bot
Copy link

changeset-bot bot commented Feb 23, 2026

🦋 Changeset detected

Latest commit: 6e7a38e

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@exactly/server Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@gemini-code-assist
Copy link

Summary of Changes

Hello @cruzdanilo, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request refines the error reporting strategy, particularly for service-level errors. By leveraging error names and messages, it aims to improve the grouping and identification of recurring issues in Sentry, making error monitoring more effective and actionable.

Highlights

  • Enhanced Error Fingerprinting: Implemented a more precise error fingerprinting mechanism for service errors, utilizing error.name and error.message for better grouping in Sentry.
  • ServiceError Integration: Integrated ServiceError into the main application error handler (app.onError) to specifically fingerprint instances of this custom error type.
  • Sentry beforeSend Hook Update: Modified the Sentry beforeSend hook to apply default fingerprinting based on error name and message for errors with a status property, ensuring consistent error grouping.
Changelog
  • @exactly/server
    • 🥅 fingerprint service errors by name and message
Activity
  • No specific activity has been recorded for this pull request yet.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@coderabbitai
Copy link

coderabbitai bot commented Feb 23, 2026

Walkthrough

This PR refactors error fingerprinting logic in @exactly/server by simplifying how errors are classified and fingerprinted. The changes focus on ServiceError instances with a consistent fingerprint tuple format ["{{ default }}", error.name, error.message], removing previous complex parsing logic across both the main error handler and the instrumentation layer.

Changes

Cohort / File(s) Summary
Release metadata
.changeset/bright-falcon-dance.md
Adds patch release note for @exactly/server documenting error fingerprinting by name and message.
Error fingerprinting refactoring
server/index.ts, server/instrument.cjs
Simplifies error fingerprinting logic by restricting to ServiceError instances and using consistent tuple format. Removes complex error message parsing and JSON body extraction. Renames loop variable and updates property accesses in instrumentation layer for clarity.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • nfmelendez
🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: fingerprinting service errors by name and message, which is reflected across all modified files.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fingerprint

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

The pull request refactors error fingerprinting in Sentry to use ServiceError for more structured error reporting. It removes complex manual parsing of error messages in app.onError and introduces a ServiceError check to directly use error.name and error.message for fingerprinting. Additionally, it enhances the beforeSend hook in instrument.cjs to capture error.name and error.message for errors that have a status property but lack an existing fingerprint, and refactors the revert error handling for clarity. Overall, the changes improve the consistency and clarity of error reporting to Sentry.

Comment on lines 276 to -305
app.onError((error, c) => {
let fingerprint: string[] | undefined;
if (error instanceof Error) {
const message = error.message
.split("Error:")
.reduce((result, part) => (result ? `${result}Error:${part}` : part.trimStart()), "");
const status = message.slice(0, 3);
const hasStatus = /^\d{3}$/.test(status);
const hasBodyFormat = message.length === 3 || message[3] === " ";
const body = hasBodyFormat && message.length > 3 ? message.slice(4).trim() : undefined;
if (hasStatus && hasBodyFormat) fingerprint = ["{{ default }}", status];
if (hasStatus && hasBodyFormat && body) {
try {
const json = JSON.parse(body) as { code?: unknown; error?: unknown; message?: unknown };
fingerprint = [
"{{ default }}",
status,
...("code" in json
? [String(json.code)]
: typeof json.message === "string"
? [json.message]
: typeof json.error === "string"
? [json.error]
: [body]),
];
} catch {
fingerprint = ["{{ default }}", status, body];
}
}
}
captureException(error, { level: "error", tags: { unhandled: true }, fingerprint });

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The removal of the complex error message parsing logic is a significant improvement. Relying on ServiceError for structured error data simplifies the onError handler and makes error fingerprinting more robust and maintainable. This change directly addresses the goal of fingerprinting service errors by name and message, making the Sentry events more actionable.

Comment on lines +277 to +281
captureException(error, {
level: "error",
tags: { unhandled: true },
fingerprint: error instanceof ServiceError ? ["{{ default }}", error.name, error.message] : undefined,
});

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This updated captureException call is much cleaner and leverages the ServiceError class effectively. It directly uses error.name and error.message for fingerprinting, which is a more reliable and structured approach than the previous manual parsing.

],
beforeSend: (event, hint) => {
const exception = event.exception?.values?.[0];
if (!exception) return event;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Adding a check for !exception at the beginning of beforeSend ensures that subsequent logic only runs when there's an actual exception to process, preventing potential undefined errors and improving robustness.

Comment on lines +32 to +39
const error = hint.originalException;
if (
error instanceof Error &&
typeof (/** @type {{ status?: unknown }} */ (error).status) === "number" &&
!(event.fingerprint && event.fingerprint.length > 1)
) {
event.fingerprint = ["{{ default }}", error.name, error.message];
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This new block correctly identifies errors that have a status property (likely ServiceError instances or similar) and assigns a default fingerprint using error.name and error.message if one isn't already present. This ensures consistent fingerprinting for these types of errors, aligning with the PR's goal.

Comment on lines +32 to +35
const error = hint.originalException;
if (
error instanceof Error &&
typeof (/** @type {{ status?: unknown }} */ (error).status) === "number" &&
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: The new fingerprinting logic in instrument.cjs checks for error.status, but PandaError uses statusCode, causing these errors to be missed and not grouped correctly in Sentry.
Severity: MEDIUM

Suggested Fix

Update the check in server/instrument.cjs to look for error.statusCode in addition to error.status, or change the PandaError class to use a status property instead of statusCode for consistency.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location: server/instrument.cjs#L32-L35

Potential issue: The fingerprinting logic in `server/instrument.cjs` is designed to
create custom Sentry fingerprints for errors that have a numeric `status` property.
However, `PandaError` instances, which are used for card transactions and refunds, are
defined with a `statusCode` property instead. When a `PandaError` is thrown and
captured, it is sent to Sentry without a pre-defined fingerprint. The `beforeSend` hook
then fails to apply a custom fingerprint because its check for `error.status` fails.
This results in `PandaError` events not being grouped correctly in Sentry, hindering
monitoring and debugging efforts.

Did we get this right? 👍 / 👎 to inform future reviews.

Copy link

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 2 potential issues.

View 2 additional findings in Devin Review.

Open in Devin Review

Comment on lines +33 to +39
if (
error instanceof Error &&
typeof (/** @type {{ status?: unknown }} */ (error).status) === "number" &&
!(event.fingerprint && event.fingerprint.length > 1)
) {
event.fingerprint = ["{{ default }}", error.name, error.message];
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚩 beforeSend fingerprinting applies to all errors with numeric status, not just ServiceError

The new beforeSend logic at server/instrument.cjs:33-38 fingerprints any Error with a numeric status property, which is broader than just ServiceError. This means if any third-party library throws an error with a numeric status (e.g., HTTP client libraries, ORMs), it will also get fingerprinted by ["{{ default }}", error.name, error.message]. If error.message contains high-cardinality data (request IDs, timestamps, UUIDs), this could create excessive unique fingerprints in Sentry, degrading grouping quality. ServiceError controls this via its constructor's parsing logic (falls back to status code for long messages >200 chars at server/utils/ServiceError.ts:26), but other error types have no such protection. This may be intentional to cast a wide net, but worth confirming.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines 276 to 282
app.onError((error, c) => {
let fingerprint: string[] | undefined;
if (error instanceof Error) {
const message = error.message
.split("Error:")
.reduce((result, part) => (result ? `${result}Error:${part}` : part.trimStart()), "");
const status = message.slice(0, 3);
const hasStatus = /^\d{3}$/.test(status);
const hasBodyFormat = message.length === 3 || message[3] === " ";
const body = hasBodyFormat && message.length > 3 ? message.slice(4).trim() : undefined;
if (hasStatus && hasBodyFormat) fingerprint = ["{{ default }}", status];
if (hasStatus && hasBodyFormat && body) {
try {
const json = JSON.parse(body) as { code?: unknown; error?: unknown; message?: unknown };
fingerprint = [
"{{ default }}",
status,
...("code" in json
? [String(json.code)]
: typeof json.message === "string"
? [json.message]
: typeof json.error === "string"
? [json.error]
: [body]),
];
} catch {
fingerprint = ["{{ default }}", status, body];
}
}
}
captureException(error, { level: "error", tags: { unhandled: true }, fingerprint });
captureException(error, {
level: "error",
tags: { unhandled: true },
fingerprint: error instanceof ServiceError ? ["{{ default }}", error.name, error.message] : undefined,
});
return c.json({ code: "unexpected error", legacy: "unexpected error" }, 555 as UnofficialStatusCode);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚩 Loss of fingerprinting for non-ServiceError status-formatted errors reaching app.onError

The old index.ts app.onError handler parsed error messages matching the pattern NNN body (e.g., "404 Not Found") and fingerprinted them with the extracted status code and optionally parsed JSON fields. The new code only fingerprints ServiceError instances. Any non-ServiceError Error that previously matched the old pattern (e.g., a plain Error('500 Internal Server Error')) will no longer be fingerprinted at the captureException call site. However, the beforeSend in instrument.cjs compensates for errors with a numeric status property. Plain Error objects without a status property that had status-formatted messages will lose their fingerprinting. This is likely acceptable since the codebase has been migrated to use ServiceError for service call failures, but it's a behavioral change.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@sentry
Copy link

sentry bot commented Feb 23, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 69.00%. Comparing base (3b2b810) to head (6e7a38e).
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #834      +/-   ##
==========================================
+ Coverage   68.95%   69.00%   +0.04%     
==========================================
  Files         211      211              
  Lines        7444     7455      +11     
  Branches     2385     2391       +6     
==========================================
+ Hits         5133     5144      +11     
  Misses       2096     2096              
  Partials      215      215              
Flag Coverage Δ
e2e 68.87% <33.33%> (+17.88%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2


ℹ️ Review info

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3b2b810 and 6e7a38e.

📒 Files selected for processing (3)
  • .changeset/bright-falcon-dance.md
  • server/index.ts
  • server/instrument.cjs

Comment on lines 276 to +281
app.onError((error, c) => {
let fingerprint: string[] | undefined;
if (error instanceof Error) {
const message = error.message
.split("Error:")
.reduce((result, part) => (result ? `${result}Error:${part}` : part.trimStart()), "");
const status = message.slice(0, 3);
const hasStatus = /^\d{3}$/.test(status);
const hasBodyFormat = message.length === 3 || message[3] === " ";
const body = hasBodyFormat && message.length > 3 ? message.slice(4).trim() : undefined;
if (hasStatus && hasBodyFormat) fingerprint = ["{{ default }}", status];
if (hasStatus && hasBodyFormat && body) {
try {
const json = JSON.parse(body) as { code?: unknown; error?: unknown; message?: unknown };
fingerprint = [
"{{ default }}",
status,
...("code" in json
? [String(json.code)]
: typeof json.message === "string"
? [json.message]
: typeof json.error === "string"
? [json.error]
: [body]),
];
} catch {
fingerprint = ["{{ default }}", status, body];
}
}
}
captureException(error, { level: "error", tags: { unhandled: true }, fingerprint });
captureException(error, {
level: "error",
tags: { unhandled: true },
fingerprint: error instanceof ServiceError ? ["{{ default }}", error.name, error.message] : undefined,
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add trailing comma in fingerprint array.

Line 280’s array should include a trailing comma per repo style.

🔧 Proposed fix
-    fingerprint: error instanceof ServiceError ? ["{{ default }}", error.name, error.message] : undefined,
+    fingerprint: error instanceof ServiceError ? ["{{ default }}", error.name, error.message,] : undefined,
As per coding guidelines "Use trailing commas in arrays, objects, and function arguments for diff-friendliness".
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
app.onError((error, c) => {
let fingerprint: string[] | undefined;
if (error instanceof Error) {
const message = error.message
.split("Error:")
.reduce((result, part) => (result ? `${result}Error:${part}` : part.trimStart()), "");
const status = message.slice(0, 3);
const hasStatus = /^\d{3}$/.test(status);
const hasBodyFormat = message.length === 3 || message[3] === " ";
const body = hasBodyFormat && message.length > 3 ? message.slice(4).trim() : undefined;
if (hasStatus && hasBodyFormat) fingerprint = ["{{ default }}", status];
if (hasStatus && hasBodyFormat && body) {
try {
const json = JSON.parse(body) as { code?: unknown; error?: unknown; message?: unknown };
fingerprint = [
"{{ default }}",
status,
...("code" in json
? [String(json.code)]
: typeof json.message === "string"
? [json.message]
: typeof json.error === "string"
? [json.error]
: [body]),
];
} catch {
fingerprint = ["{{ default }}", status, body];
}
}
}
captureException(error, { level: "error", tags: { unhandled: true }, fingerprint });
captureException(error, {
level: "error",
tags: { unhandled: true },
fingerprint: error instanceof ServiceError ? ["{{ default }}", error.name, error.message] : undefined,
});
app.onError((error, c) => {
captureException(error, {
level: "error",
tags: { unhandled: true },
fingerprint: error instanceof ServiceError ? ["{{ default }}", error.name, error.message,] : undefined,
});

Comment on lines +31 to +39
if (!exception) return event;
const error = hint.originalException;
if (
error instanceof Error &&
typeof (/** @type {{ status?: unknown }} */ (error).status) === "number" &&
!(event.fingerprint && event.fingerprint.length > 1)
) {
event.fingerprint = ["{{ default }}", error.name, error.message];
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Remove JSDoc cast and add trailing comma.

JSDoc casts are disallowed, and the fingerprint array needs a trailing comma.

🔧 Proposed fix
-    if (
-      error instanceof Error &&
-      typeof (/** `@type` {{ status?: unknown }} */ (error).status) === "number" &&
-      !(event.fingerprint && event.fingerprint.length > 1)
-    ) {
-      event.fingerprint = ["{{ default }}", error.name, error.message];
-    }
+    if (
+      error instanceof Error &&
+      typeof error.status === "number" &&
+      !(event.fingerprint && event.fingerprint.length > 1)
+    ) {
+      event.fingerprint = ["{{ default }}", error.name, error.message,];
+    }
As per coding guidelines "Use only static analysis annotations (...) do not use JSDoc" and "Use trailing commas in arrays, objects, and function arguments for diff-friendliness".

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant