Skip to content

Commit

Permalink
properly serialize axios errors in report error, and skip non mod log…
Browse files Browse the repository at this point in the history
…s for idb
  • Loading branch information
fungairino committed Sep 24, 2024
1 parent 9d4cf78 commit 8392ce1
Show file tree
Hide file tree
Showing 8 changed files with 72 additions and 49 deletions.
8 changes: 3 additions & 5 deletions src/components/errors/NetworkErrorDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,11 @@ import {
} from "@fortawesome/free-solid-svg-icons";
import JsonTree from "@/components/jsonTree/JsonTree";
import { selectAbsoluteUrl } from "@/utils/urlUtils";
import {
safeGuessStatusText,
type SerializableAxiosError,
} from "@/errors/networkErrorHelpers";
import { safeGuessStatusText } from "@/errors/networkErrorHelpers";
import styles from "./ErrorDetail.module.scss";
import useAsyncState from "@/hooks/useAsyncState";
import AsyncStateGate from "@/components/AsyncStateGate";
import { type AxiosError } from "axios";

function tryParse(value: unknown): unknown {
if (typeof value === "string") {
Expand All @@ -45,7 +43,7 @@ function tryParse(value: unknown): unknown {
}

const NetworkErrorDetail: React.FunctionComponent<{
error: SerializableAxiosError;
error: AxiosError;
}> = ({ error }) => {
const absoluteUrl = selectAbsoluteUrl(error.config);

Expand Down
9 changes: 2 additions & 7 deletions src/data/service/errorService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,8 @@ export function shouldIgnoreError(error: Error): boolean {
*/
export async function reportToErrorService(
error: Error,
flatContext: MessageContext,
flatContext: MessageContext &
Required<Pick<MessageContext, "modComponentId">>,
message: string,
): Promise<void> {
expectContext(
Expand All @@ -141,12 +142,6 @@ export async function reportToErrorService(
return;
}

if (flatContext.modComponentId == null) {
// Only report errors that occurred within a user-defined extension/blueprint. Other errors only go to Application error telemetry.
// (They're problems with our software.)
return;
}

if (shouldIgnoreError(error)) {
return;
}
Expand Down
14 changes: 4 additions & 10 deletions src/data/service/requestErrorUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,15 @@
import { isErrorObject } from "@/errors/errorHelpers";
import { testMatchPatterns } from "@/bricks/available";
import { getBaseURL } from "@/data/service/baseService";
import {
isAxiosError,
type SerializableAxiosError,
} from "@/errors/networkErrorHelpers";
import { isAxiosError } from "@/errors/networkErrorHelpers";
import { selectAbsoluteUrl, withoutTrailingSlash } from "@/utils/urlUtils";
import { DEFAULT_SERVICE_URL } from "@/urlConstants";
import { type AxiosError } from "axios";

/**
* Return true iff the error corresponds to a request to PixieBrix API.
*/
export async function isAppRequestError(
error: SerializableAxiosError,
): Promise<boolean> {
export async function isAppRequestError(error: AxiosError): Promise<boolean> {
const baseURL = await getBaseURL();
const requestUrl = selectAbsoluteUrl(error.config);
const patterns = [baseURL, DEFAULT_SERVICE_URL].map(
Expand All @@ -42,9 +38,7 @@ export async function isAppRequestError(
/**
* Return the AxiosError associated with an error, or null if error is not associated with an AxiosError
*/
export function selectAxiosError(
error: unknown,
): SerializableAxiosError | null {
export function selectAxiosError(error: unknown): AxiosError | null {
if (isAxiosError(error)) {
return error;
}
Expand Down
6 changes: 3 additions & 3 deletions src/errors/clientRequestErrors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
*/

import { BusinessError } from "@/errors/businessErrors";
import { type SerializableAxiosError } from "@/errors/networkErrorHelpers";
import { type AxiosError } from "axios";

/**
* @file ONLY KEEP ACTUAL ERRORS IN HERE.
Expand All @@ -30,9 +30,9 @@ import { type SerializableAxiosError } from "@/errors/networkErrorHelpers";
export class ClientRequestError extends BusinessError {
override name = "ClientRequestError";
// Specialize the cause type
override readonly cause: SerializableAxiosError;
override readonly cause: AxiosError;

constructor(message: string, options: { cause: SerializableAxiosError }) {
constructor(message: string, options: { cause: AxiosError }) {
super(message, options);
// This assignment seems to be required in Chrome 102 to ensure the cause is serialized by serialize-error
// https://github.com/pixiebrix/pixiebrix-extension/issues/3613
Expand Down
10 changes: 2 additions & 8 deletions src/errors/networkErrorHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,14 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

import { type SetRequired, type Except } from "type-fest";
import { type SetRequired } from "type-fest";
import { type AxiosError, type AxiosResponse } from "axios";
import { isEmpty } from "lodash";
import { getReasonPhrase } from "http-status-codes";
import { isObject } from "@/utils/objectUtils";

/**
* Axios offers its own serialization method, but it doesn't include the response.
* By deleting toJSON, the serialize-error library will use its default serialization
*/
export type SerializableAxiosError = Except<AxiosError, "toJSON">;

// Copy of axios.isAxiosError, without risking to import the whole untreeshakeable axios library
export function isAxiosError(error: unknown): error is SerializableAxiosError {
export function isAxiosError(error: unknown): error is AxiosError {
return (
isObject(error) &&
// To deal with original AxiosError as well as a serialized error
Expand Down
37 changes: 25 additions & 12 deletions src/telemetry/logging.ts
Original file line number Diff line number Diff line change
Expand Up @@ -465,6 +465,11 @@ export async function reportToApplicationErrorTelemetry(
} satisfies RecordErrorMessage);
}

export type ContextWithMod = MessageContext &
Required<Pick<MessageContext, "modComponentId">>;
const isContextWithMod = (context: MessageContext): context is ContextWithMod =>
context.modComponentId != null;

/** @deprecated Use instead: `import reportError from "@/telemetry/reportError"` */
export async function recordError(
this: MessengerMeta, // Enforce usage via Messenger only
Expand All @@ -485,19 +490,27 @@ export async function recordError(
const errorMessage = getErrorMessage(error);
const flatContext = flattenContext(error, context);

// Only record entries that occurred within a user-defined extension/blueprint.
// Other errors only go to Application error telemetry. (They're problems with our software.)
const reportModErrorPromises = isContextWithMod(flatContext)
? [
reportToErrorService(error, flatContext, errorMessage),
appendEntry({
uuid: uuidv4(),
timestamp: Date.now().toString(),
level: "error",
context: flatContext,
message: errorMessage,
data,
// Ensure the object is fully serialized. Required because it will be stored in IDB and flow through the Redux state
error: serializedError,
}),
]
: [];

await Promise.all([
reportToApplicationErrorTelemetry(error, flatContext, errorMessage),
reportToErrorService(error, flatContext, errorMessage),
appendEntry({
uuid: uuidv4(),
timestamp: Date.now().toString(),
level: "error",
context: flatContext,
message: errorMessage,
data,
// Ensure the object is fully serialized. Required because it will be stored in IDB and flow through the Redux state
error: serializedError,
}),
...reportModErrorPromises,
]);
} catch (recordErrorError) {
console.error("An error occurred while recording another error", {
Expand Down Expand Up @@ -571,7 +584,7 @@ export async function clearModComponentDebugLogs(
}
}
}, IDB_OPERATION.CLEAR_MOD_COMPONENT_DEBUG_LOGS).catch((_error) => {
// Swallow error because we've reported it to application error telemetry and
// Swallow error because we've reported it to application error telemetry, and
// we don't want to interrupt the execution of mod pipeline
});
}
Expand Down
23 changes: 23 additions & 0 deletions src/telemetry/reportError.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
*/

import { getNotifier } from "webext-messenger";
import { AxiosError } from "axios";

jest.mock("webext-messenger");

Expand Down Expand Up @@ -80,6 +81,28 @@ describe("reportError", () => {
);
});

it("should serialize AxiosErrors correctly", () => {
const error = new AxiosError("Test error", "code", {
validateStatus: () => true,
});
reportError(error);
expect(_recordMock).toHaveBeenCalledWith(
{
message: "Test error",
code: "code",
name: "AxiosError",
config: {}, // Expect any functions to be stripped
stack: expect.any(String),
},
{
connectionType: "unknown",
pageName: "extension",
referrer: "",
url: "http://localhost/",
},
);
});

it("should log error to console if an error occurs while reporting the error", () => {
const error = new Error("Test error");
const reportingError = new Error("Reporting error");
Expand Down
14 changes: 10 additions & 4 deletions src/telemetry/reportError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { serializeError } from "serialize-error";
import { selectError, shouldErrorBeIgnored } from "@/errors/errorHelpers";
import { expectContext } from "@/utils/expectContext";
import { getContextName } from "webext-detect";
import { isAxiosError } from "@/errors/networkErrorHelpers";

expectContext(
"extension",
Expand Down Expand Up @@ -94,10 +95,15 @@ export default function reportError(
}

try {
_record(serializeError(selectError(errorLike)), {
...context,
...getReportErrorAdditionalContext(),
});
_record(
serializeError(selectError(errorLike), {
useToJSON: !isAxiosError(errorLike),
}),
{
...context,
...getReportErrorAdditionalContext(),
},
);
} catch (reportingError) {
// The messenger does not throw async errors on "notifiers" but if this is
// called in the background the call will be executed directly, and it could
Expand Down

0 comments on commit 8392ce1

Please sign in to comment.