From 260dd75461bf27188c21614f33d9b1c798fa96bf Mon Sep 17 00:00:00 2001 From: Jack Williams Date: Mon, 16 Oct 2023 21:29:47 +0100 Subject: [PATCH] INN-2152 Slim down "pretty errors" before sending to Inngest (#360) ## Summary Slims down "pretty errors," whose name is already subjective, before sending them to Inngest, by stripping ANSI codes and removing immediate debugging information. | ![Before](https://github.com/inngest/inngest-js/assets/1736957/5cc6e965-4806-483e-9ae5-c0c38ff5d960) | ![image](https://github.com/inngest/inngest-js/assets/1736957/8e7f22bf-6f7c-4c80-b8ab-9b0f231a4326) | | - | - | | Before | After | The UI could access the code being sent (e.g. `AUTOMATIC_PARALLEL_INDEXING`), but it's probably nicer to send this code inside serialized errors in the future, allowing the UI to provide hints and links to documentation, inclusive of short links such as `https://innge.st/ERR_CODE_HERE`. ## Checklist - [ ] ~~Added a [docs PR](https://github.com/inngest/website) that references this PR~~ N/A Improvement - [x] Added unit/integration tests - [x] Added changesets if applicable ## Related - INN-2152 --- .changeset/cold-hairs-carry.md | 5 ++ packages/inngest/package.json | 1 + .../inngest/src/components/execution/v1.ts | 3 +- packages/inngest/src/helpers/errors.test.ts | 64 ++++++++++++++++- packages/inngest/src/helpers/errors.ts | 69 ++++++++++++++++++- pnpm-lock.yaml | 15 ++++ 6 files changed, 152 insertions(+), 5 deletions(-) create mode 100644 .changeset/cold-hairs-carry.md diff --git a/.changeset/cold-hairs-carry.md b/.changeset/cold-hairs-carry.md new file mode 100644 index 00000000..1b9ea8a8 --- /dev/null +++ b/.changeset/cold-hairs-carry.md @@ -0,0 +1,5 @@ +--- +"inngest": patch +--- + +Inngest errors now appear more succintly in UIs, free of ANSI codes and verbose information diff --git a/packages/inngest/package.json b/packages/inngest/package.json index 7c231815..1bdaf39b 100644 --- a/packages/inngest/package.json +++ b/packages/inngest/package.json @@ -125,6 +125,7 @@ "json-stringify-safe": "^5.0.1", "ms": "^2.1.3", "serialize-error-cjs": "^0.1.3", + "strip-ansi": "^5.2.0", "type-fest": "^3.13.1", "zod": "~3.22.3" }, diff --git a/packages/inngest/src/components/execution/v1.ts b/packages/inngest/src/components/execution/v1.ts index 5904e416..3e206842 100644 --- a/packages/inngest/src/components/execution/v1.ts +++ b/packages/inngest/src/components/execution/v1.ts @@ -5,6 +5,7 @@ import { logPrefix } from "../../helpers/consts"; import { ErrCode, deserializeError, + minifyPrettyError, prettyError, serializeError, } from "../../helpers/errors"; @@ -459,7 +460,7 @@ class V1InngestExecution extends InngestExecution implements IInngestExecution { retriable = error.retryAfter; } - const serializedError = serializeError(error); + const serializedError = minifyPrettyError(serializeError(error)); return { type: "function-rejected", error: serializedError, retriable }; } diff --git a/packages/inngest/src/helpers/errors.test.ts b/packages/inngest/src/helpers/errors.test.ts index 9f729638..19b5c363 100644 --- a/packages/inngest/src/helpers/errors.test.ts +++ b/packages/inngest/src/helpers/errors.test.ts @@ -1,4 +1,11 @@ -import { isSerializedError, serializeError } from "@local/helpers/errors"; +import { + ErrCode, + fixEventKeyMissingSteps, + isSerializedError, + minifyPrettyError, + prettyError, + serializeError, +} from "@local/helpers/errors"; interface ErrorTests { name: string; @@ -103,3 +110,58 @@ describe("serializeError", () => { tests: { message: "test" }, }); }); + +describe("minifyPrettyError", () => { + describe("should minify a pretty error", () => { + const originalErr = serializeError( + new Error( + prettyError({ + whatHappened: "Failed to send event", + consequences: "Your event or events were not sent to Inngest.", + why: "We couldn't find an event key to use to send events to Inngest.", + toFixNow: fixEventKeyMissingSteps, + }) + ) + ); + + const err = minifyPrettyError(originalErr); + + const expected = "Failed to send event"; + + test("sets message", () => { + expect(err.message).toBe(expected); + }); + + test("sets stack", () => { + expect(err.stack).toMatch(`Error: ${expected}\n`); + expect(err.stack).toMatch(originalErr.stack); + }); + }); + + describe("should prepend code", () => { + const originalErr = serializeError( + new Error( + prettyError({ + whatHappened: "Failed to send event", + consequences: "Your event or events were not sent to Inngest.", + why: "We couldn't find an event key to use to send events to Inngest.", + toFixNow: fixEventKeyMissingSteps, + code: ErrCode.NESTING_STEPS, + }) + ) + ); + + const err = minifyPrettyError(originalErr); + + const expected = `${ErrCode.NESTING_STEPS} - Failed to send event`; + + test("sets message", () => { + expect(err.message).toBe(expected); + }); + + test("sets stack", () => { + expect(err.stack).toMatch(`Error: ${expected}\n`); + expect(err.stack).toMatch(originalErr.stack); + }); + }); +}); diff --git a/packages/inngest/src/helpers/errors.ts b/packages/inngest/src/helpers/errors.ts index f638ad7d..0f3ff25e 100644 --- a/packages/inngest/src/helpers/errors.ts +++ b/packages/inngest/src/helpers/errors.ts @@ -6,6 +6,7 @@ import { errorConstructors, type SerializedError as CjsSerializedError, } from "serialize-error-cjs"; +import stripAnsi from "strip-ansi"; import { z } from "zod"; import { type Inngest } from "../components/Inngest"; import { NonRetriableError } from "../components/NonRetriableError"; @@ -274,6 +275,69 @@ export interface PrettyError { code?: ErrCode; } +export const prettyErrorSplitter = + "================================================="; + +/** + * Given an unknown `err`, mutate it to minify any pretty errors that it + * contains. + */ +export const minifyPrettyError = (err: T): T => { + try { + if (!isError(err)) { + return err; + } + + const isPrettyError = err.message.includes(prettyErrorSplitter); + if (!isPrettyError) { + return err; + } + + const sanitizedMessage = stripAnsi(err.message); + + const message = + sanitizedMessage.split(" ")[1]?.split("\n")[0]?.trim() || err.message; + const code = + sanitizedMessage.split("\n\nCode: ")[1]?.split("\n\n")[0]?.trim() || + undefined; + + err.message = [code, message].filter(Boolean).join(" - "); + + if (err.stack) { + const sanitizedStack = stripAnsi(err.stack); + const stackRest = sanitizedStack + .split(`${prettyErrorSplitter}\n`) + .slice(2) + .join("\n"); + + err.stack = `${err.name}: ${err.message}\n${stackRest}`; + } + + return err; + } catch (noopErr) { + return err; + } +}; + +/** + * Given an `err`, return a boolean representing whether it is in the shape of + * an `Error` or not. + */ +const isError = (err: unknown): err is Error => { + try { + if (err instanceof Error) { + return true; + } + + const hasName = Object.prototype.hasOwnProperty.call(err, "name"); + const hasMessage = Object.prototype.hasOwnProperty.call(err, "message"); + + return hasName && hasMessage; + } catch (noopErr) { + return false; + } +}; + /** * Given a {@link PrettyError}, return a nicely-formatted string ready to log * or throw. @@ -302,7 +366,6 @@ export const prettyError = ({ > )[type]; - const splitter = "================================================="; let header = `${icon} ${chalk.bold.underline(whatHappened.trim())}`; if (stack) { header += @@ -333,12 +396,12 @@ export const prettyError = ({ const trailer = [otherwise?.trim()].filter(Boolean).join(" "); const message = [ - splitter, + prettyErrorSplitter, header, body, trailer, code ? `Code: ${code}` : "", - splitter, + prettyErrorSplitter, ] .filter(Boolean) .join("\n\n"); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e275e98b..445c6309 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -53,6 +53,9 @@ importers: serialize-error-cjs: specifier: ^0.1.3 version: 0.1.3 + strip-ansi: + specifier: ^5.2.0 + version: 5.2.0 type-fest: specifier: ^3.13.1 version: 3.13.1 @@ -2104,6 +2107,11 @@ packages: type-fest: 0.21.3 dev: true + /ansi-regex@4.1.1: + resolution: {integrity: sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==} + engines: {node: '>=6'} + dev: false + /ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} @@ -6302,6 +6310,13 @@ packages: safe-buffer: 5.2.1 dev: true + /strip-ansi@5.2.0: + resolution: {integrity: sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==} + engines: {node: '>=6'} + dependencies: + ansi-regex: 4.1.1 + dev: false + /strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'}