fix(ui): address sentry review feedback

This commit is contained in:
Alan Buscaglia
2026-06-25 13:32:40 +02:00
parent 722bc3f3f6
commit 3ea6858591
4 changed files with 80 additions and 10 deletions
+48
View File
@@ -123,6 +123,53 @@ describe("server-actions-helper", () => {
.calls[0]?.[0];
expect(isErrorAlreadyReported(capturedError)).toBe(true);
});
it("should fingerprint server errors by pathname without query string", async () => {
// Given
const response = new Response(
JSON.stringify({ message: "backend down" }),
{
status: 500,
statusText: "Internal Server Error",
},
);
Object.defineProperty(response, "url", {
value: "https://api.prowler.test/api/v1/providers?tenant=123",
});
// When
await expect(handleApiResponse(response)).rejects.toThrow("backend down");
// Then
expect(Sentry.captureException).toHaveBeenCalledWith(
expect.any(Error),
expect.objectContaining({
fingerprint: ["api-server-error", "500", "/api/v1/providers"],
}),
);
});
it("should fingerprint unexpected client failures by pathname without query string", async () => {
// Given
const response = new Response(
JSON.stringify({ message: "Unexpected API contract failure" }),
{ status: 429, statusText: "Too Many Requests" },
);
Object.defineProperty(response, "url", {
value: "https://api.prowler.test/api/v1/scans?page=2",
});
// When
await handleApiResponse(response);
// Then
expect(Sentry.captureException).toHaveBeenCalledWith(
expect.any(Error),
expect.objectContaining({
fingerprint: ["api-client-contract-error", "429", "/api/v1/scans"],
}),
);
});
});
describe("handleApiError", () => {
@@ -159,6 +206,7 @@ describe("server-actions-helper", () => {
}),
}),
);
expect(isErrorAlreadyReported(error)).toBe(true);
expect(result).toEqual({ error: "Request failed unexpectedly" });
});
+17 -2
View File
@@ -20,6 +20,7 @@ const KNOWN_NON_ACTIONABLE_CLIENT_ERROR_MESSAGES = [
"already exists",
"duplicate",
] as const;
const UNKNOWN_URL_PATH_FINGERPRINT = "unknown-url-path";
/**
* Helper function to handle API responses consistently
@@ -89,7 +90,7 @@ export const handleApiResponse = async (
fingerprint: [
"api-server-error",
response.status.toString(),
response.url,
getSentryFingerprintUrlPath(response.url),
],
});
markErrorAsReported(serverError);
@@ -170,6 +171,7 @@ export const handleApiError = (error: unknown): { error: string } => {
},
},
});
markErrorAsReported(error);
} else {
// Capture non-Error objects
Sentry.captureMessage(
@@ -185,6 +187,7 @@ export const handleApiError = (error: unknown): { error: string } => {
},
},
);
markErrorAsReported(error);
}
}
@@ -238,11 +241,23 @@ function captureUnexpectedApiClientFailure(
fingerprint: [
"api-client-contract-error",
response.status.toString(),
response.url,
getSentryFingerprintUrlPath(response.url),
],
});
}
function getSentryFingerprintUrlPath(url: string) {
if (url.trim() === "") {
return UNKNOWN_URL_PATH_FINGERPRINT;
}
try {
return new URL(url).pathname || UNKNOWN_URL_PATH_FINGERPRINT;
} catch {
return UNKNOWN_URL_PATH_FINGERPRINT;
}
}
function hasStructuredApiErrors(errorsArray: unknown[] | undefined) {
return Array.isArray(errorsArray) && errorsArray.length > 0;
}
+12 -5
View File
@@ -1,5 +1,10 @@
import { describe, expect, it } from "vitest";
import type {
SentryEventHint,
SentryEventPolicyOptions,
SentryPolicyEvent,
} from "./event-policy";
import {
applySentryEventPolicy,
isErrorAlreadyReported,
@@ -10,12 +15,14 @@ describe("applySentryEventPolicy", () => {
describe("when events are actionable", () => {
it("should keep error events", () => {
// Given
const event = { level: "error", message: "Unexpected failure" };
const event = {
level: "error",
message: "Unexpected failure",
} satisfies SentryPolicyEvent & { level: string; message: string };
const options: SentryEventPolicyOptions = { source: "client" };
// When
const result = applySentryEventPolicy(event, undefined, {
source: "client",
});
const result = applySentryEventPolicy(event, undefined, options);
// Then
expect(result).toBe(event);
@@ -206,7 +213,7 @@ describe("applySentryEventPolicy", () => {
it("should drop AbortError events", () => {
// Given
const event = { level: "error", message: "The operation was aborted" };
const hint = {
const hint: SentryEventHint = {
originalException: new DOMException("Aborted", "AbortError"),
};
+3 -3
View File
@@ -41,15 +41,15 @@ type SentryEventSource =
(typeof SENTRY_EVENT_SOURCE)[keyof typeof SENTRY_EVENT_SOURCE];
type SentryPolicyTagValue = string | number | boolean | null | undefined;
interface SentryEventPolicyOptions {
export interface SentryEventPolicyOptions {
source?: SentryEventSource;
}
interface SentryEventHint {
export interface SentryEventHint {
originalException?: unknown;
}
interface SentryPolicyEvent {
export interface SentryPolicyEvent {
tags?: Record<string, SentryPolicyTagValue>;
}