diff --git a/packages/stack-shared/src/utils/errors.tsx b/packages/stack-shared/src/utils/errors.tsx index 66c7c253e0..83702c6fc4 100644 --- a/packages/stack-shared/src/utils/errors.tsx +++ b/packages/stack-shared/src/utils/errors.tsx @@ -82,10 +82,7 @@ StackAssertionError.prototype.name = "StackAssertionError"; export function errorToNiceString(error: unknown): string { if (!(error instanceof Error)) return `${typeof error}<${nicify(error)}>`; - let stack = error.stack ?? ""; - const toString = error.toString(); - if (!stack.startsWith(toString)) stack = `${toString}\n${stack}`; // some browsers don't include the error message in the stack, some do - return `${stack} ${nicify(Object.fromEntries(Object.entries(error)), { maxDepth: 8 })}`; + return nicify(error, { maxDepth: 8 }); } diff --git a/packages/stack-shared/src/utils/hashes.tsx b/packages/stack-shared/src/utils/hashes.tsx index 4dd8fdbe04..82d0fa1372 100644 --- a/packages/stack-shared/src/utils/hashes.tsx +++ b/packages/stack-shared/src/utils/hashes.tsx @@ -17,7 +17,7 @@ export async function hashPassword(password: string) { return await bcrypt.hash(password, salt); } -export async function comparePassword(password: string, hash: string) { +export async function comparePassword(password: string, hash: string): Promise { switch (await getPasswordHashAlgorithm(hash)) { case "bcrypt": { return await bcrypt.compare(password, hash); diff --git a/packages/stack-shared/src/utils/results.tsx b/packages/stack-shared/src/utils/results.tsx index 41a0c79cb2..73a2e8192e 100644 --- a/packages/stack-shared/src/utils/results.tsx +++ b/packages/stack-shared/src/utils/results.tsx @@ -1,5 +1,5 @@ import { wait } from "./promises"; -import { deindent } from "./strings"; +import { deindent, nicify } from "./strings"; export type Result = | { @@ -305,7 +305,7 @@ import.meta.vitest?.test("mapResult", ({ expect }) => { class RetryError extends AggregateError { constructor(public readonly errors: unknown[]) { - const strings = errors.map(e => String(e)); + const strings = errors.map(e => nicify(e)); const isAllSame = strings.length > 1 && strings.every(s => s === strings[0]); super( errors, @@ -314,10 +314,10 @@ class RetryError extends AggregateError { ${isAllSame ? deindent` Attempts 1-${errors.length}: - ${errors[0]} - ` : errors.map((e, i) => deindent` + ${strings[0]} + ` : strings.map((s, i) => deindent` Attempt ${i + 1}: - ${e} + ${s} `).join("\n\n")} `, { cause: errors[errors.length - 1] } diff --git a/packages/stack-shared/src/utils/strings.nicify.test.ts b/packages/stack-shared/src/utils/strings.nicify.test.ts new file mode 100644 index 0000000000..a105ceaf02 --- /dev/null +++ b/packages/stack-shared/src/utils/strings.nicify.test.ts @@ -0,0 +1,245 @@ +import { describe, expect, test } from "vitest"; +import { NicifyOptions, deindent, nicify } from "./strings"; + +describe("nicify", () => { + describe("primitive values", () => { + test("numbers", () => { + expect(nicify(123)).toBe("123"); + expect(nicify(123n)).toBe("123n"); + }); + + test("strings", () => { + expect(nicify("hello")).toBe('"hello"'); + }); + + test("booleans", () => { + expect(nicify(true)).toBe("true"); + expect(nicify(false)).toBe("false"); + }); + + test("null and undefined", () => { + expect(nicify(null)).toBe("null"); + expect(nicify(undefined)).toBe("undefined"); + }); + + test("symbols", () => { + expect(nicify(Symbol("test"))).toBe("Symbol(test)"); + }); + }); + + describe("arrays", () => { + test("empty array", () => { + expect(nicify([])).toBe("[]"); + }); + + test("single-element array", () => { + expect(nicify([1])).toBe("[1]"); + }); + + test("single-element array with long content", () => { + expect(nicify(["123123123123123"])).toBe('["123123123123123"]'); + }); + + test("flat array", () => { + expect(nicify([1, 2, 3])).toBe("[1, 2, 3]"); + }); + + test("longer array", () => { + expect(nicify([10000, 2, 3])).toBe(deindent` + [ + 10000, + 2, + 3, + ] + `); + }); + + test("nested array", () => { + expect(nicify([1, [2, 3]])).toBe(deindent` + [ + 1, + [2, 3], + ] + `); + }); + }); + + describe("objects", () => { + test("empty object", () => { + expect(nicify({})).toBe("{}"); + }); + + test("simple object", () => { + expect(nicify({ a: 1 })).toBe('{ "a": 1 }'); + }); + + test("multiline object", () => { + expect(nicify({ a: 1, b: 2 })).toBe(deindent` + { + "a": 1, + "b": 2, + } + `); + }); + }); + + describe("custom classes", () => { + test("class instance", () => { + class TestClass { + constructor(public value: number) {} + } + expect(nicify(new TestClass(42))).toBe('TestClass { "value": 42 }'); + }); + }); + + describe("built-in objects", () => { + test("URL", () => { + expect(nicify(new URL("https://example.com"))).toBe('URL("https://example.com/")'); + }); + + test("TypedArrays", () => { + expect(nicify(new Uint8Array([1, 2, 3]))).toBe("Uint8Array([1,2,3])"); + expect(nicify(new Int32Array([1, 2, 3]))).toBe("Int32Array([1,2,3])"); + }); + + test("Error objects", () => { + const error = new Error("test error"); + const nicifiedError = nicify({ error }); + expect(nicifiedError).toMatch(new RegExp(deindent` + ^\{ + "error": Error: test error + Stack: + at (.|\\n)* + \}$ + `)); + }); + + test("Error objects with cause and an extra property", () => { + const error = new Error("test error", { cause: new Error("cause") }); + (error as any).extra = "something"; + const nicifiedError = nicify(error, { lineIndent: "--" }); + expect(nicifiedError).toMatch(new RegExp(deindent` + ^Error: test error + --Stack: + ----at (.|\\n)+ + --Extra properties: \{ "extra": "something" \} + --Cause: + ----Error: cause + ------Stack: + --------at (.|\\n)+$ + `)); + }); + + test("Headers", () => { + const headers = new Headers(); + headers.append("Content-Type", "application/json"); + headers.append("Accept", "text/plain"); + expect(nicify(headers)).toBe(deindent` + Headers { + "accept": "text/plain", + "content-type": "application/json", + }` + ); + }); + }); + + describe("multiline strings", () => { + test("basic multiline", () => { + expect(nicify("line1\nline2")).toBe('deindent`\n line1\n line2\n`'); + }); + + test("multiline with trailing newline", () => { + expect(nicify("line1\nline2\n")).toBe('deindent`\n line1\n line2\n` + "\\n"'); + }); + }); + + describe("circular references", () => { + test("object with self reference", () => { + const circular: any = { a: 1 }; + circular.self = circular; + expect(nicify(circular)).toBe(deindent` + { + "a": 1, + "self": Ref, + }` + ); + }); + }); + + describe("configuration options", () => { + test("maxDepth", () => { + const deep = { a: { b: { c: { d: { e: 1 } } } } }; + expect(nicify(deep, { maxDepth: 2 })).toBe('{ "a": { "b": { ... } } }'); + }); + + test("lineIndent", () => { + expect(nicify({ a: 1, b: 2 }, { lineIndent: " " })).toBe(deindent` + { + "a": 1, + "b": 2, + } + `); + }); + + test("hideFields", () => { + expect(nicify({ a: 1, b: 2, secret: "hidden" }, { hideFields: ["secret"] })).toBe(deindent` + { + "a": 1, + "b": 2, +