mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-04 19:21:51 +00:00
fix(ui): address sentry review feedback
This commit is contained in:
@@ -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" });
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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"),
|
||||
};
|
||||
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user