@exodus/errors 3.3.0 → 3.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/lib/index.d.ts +1 -0
- package/lib/index.js +1 -0
- package/lib/safe-context/index.d.ts +30 -0
- package/lib/safe-context/index.js +12 -0
- package/lib/safe-context/schemas.d.ts +29 -0
- package/lib/safe-context/schemas.js +19 -0
- package/lib/safe-error.d.ts +15 -9
- package/lib/safe-error.js +28 -3
- package/package.json +6 -3
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,20 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
## [3.5.0](https://github.com/ExodusMovement/exodus-hydra/compare/@exodus/errors@3.4.0...@exodus/errors@3.5.0) (2025-11-04)
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
- feat: allow exporting `traceId` in errors context (#14099)
|
|
11
|
+
|
|
12
|
+
- feat: handle `cause` properties of errors (#14307)
|
|
13
|
+
|
|
14
|
+
## [3.4.0](https://github.com/ExodusMovement/exodus-hydra/compare/@exodus/errors@3.3.0...@exodus/errors@3.4.0) (2025-10-14)
|
|
15
|
+
|
|
16
|
+
### Features
|
|
17
|
+
|
|
18
|
+
- feat: support capturing error context (#13933)
|
|
19
|
+
|
|
6
20
|
## [3.3.0](https://github.com/ExodusMovement/exodus-hydra/compare/@exodus/errors@3.2.0...@exodus/errors@3.3.0) (2025-08-08)
|
|
7
21
|
|
|
8
22
|
### Features
|
package/lib/index.d.ts
CHANGED
package/lib/index.js
CHANGED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type SafeContextType } from './schemas.js';
|
|
2
|
+
export declare const SafeContext: {
|
|
3
|
+
parse(unsafeContext: unknown): SafeContextType;
|
|
4
|
+
getSchema: () => ReturnType<() => import("@exodus/zod").ZodOptional<import("@exodus/zod").ZodNullable<import("@exodus/zod").ZodObject<{
|
|
5
|
+
navigation: import("@exodus/zod").ZodOptional<import("@exodus/zod").ZodNullable<import("@exodus/zod").ZodObject<{
|
|
6
|
+
currentRouteName: import("@exodus/zod").ZodEffects<import("@exodus/zod").ZodString, string, string>;
|
|
7
|
+
previousRouteName: import("@exodus/zod").ZodOptional<import("@exodus/zod").ZodNullable<import("@exodus/zod").ZodEffects<import("@exodus/zod").ZodString, string, string>>>;
|
|
8
|
+
}, "strict", import("@exodus/zod").ZodTypeAny, {
|
|
9
|
+
currentRouteName: string;
|
|
10
|
+
previousRouteName?: string | null | undefined;
|
|
11
|
+
}, {
|
|
12
|
+
currentRouteName: string;
|
|
13
|
+
previousRouteName?: string | null | undefined;
|
|
14
|
+
}>>>;
|
|
15
|
+
traceId: import("@exodus/zod").ZodOptional<import("@exodus/zod").ZodNullable<import("@exodus/zod").ZodEffects<import("@exodus/zod").ZodString, string, string>>>;
|
|
16
|
+
}, "strict", import("@exodus/zod").ZodTypeAny, {
|
|
17
|
+
navigation?: {
|
|
18
|
+
currentRouteName: string;
|
|
19
|
+
previousRouteName?: string | null | undefined;
|
|
20
|
+
} | null | undefined;
|
|
21
|
+
traceId?: string | null | undefined;
|
|
22
|
+
}, {
|
|
23
|
+
navigation?: {
|
|
24
|
+
currentRouteName: string;
|
|
25
|
+
previousRouteName?: string | null | undefined;
|
|
26
|
+
} | null | undefined;
|
|
27
|
+
traceId?: string | null | undefined;
|
|
28
|
+
}>>>>;
|
|
29
|
+
};
|
|
30
|
+
export type { SafeContextType } from './schemas.js';
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { z } from '@exodus/zod';
|
|
2
|
+
declare const createContextSchema: () => z.ZodOptional<z.ZodNullable<z.ZodObject<{
|
|
3
|
+
navigation: z.ZodOptional<z.ZodNullable<z.ZodObject<{
|
|
4
|
+
currentRouteName: z.ZodEffects<z.ZodString, string, string>;
|
|
5
|
+
previousRouteName: z.ZodOptional<z.ZodNullable<z.ZodEffects<z.ZodString, string, string>>>;
|
|
6
|
+
}, "strict", z.ZodTypeAny, {
|
|
7
|
+
currentRouteName: string;
|
|
8
|
+
previousRouteName?: string | null | undefined;
|
|
9
|
+
}, {
|
|
10
|
+
currentRouteName: string;
|
|
11
|
+
previousRouteName?: string | null | undefined;
|
|
12
|
+
}>>>;
|
|
13
|
+
traceId: z.ZodOptional<z.ZodNullable<z.ZodEffects<z.ZodString, string, string>>>;
|
|
14
|
+
}, "strict", z.ZodTypeAny, {
|
|
15
|
+
navigation?: {
|
|
16
|
+
currentRouteName: string;
|
|
17
|
+
previousRouteName?: string | null | undefined;
|
|
18
|
+
} | null | undefined;
|
|
19
|
+
traceId?: string | null | undefined;
|
|
20
|
+
}, {
|
|
21
|
+
navigation?: {
|
|
22
|
+
currentRouteName: string;
|
|
23
|
+
previousRouteName?: string | null | undefined;
|
|
24
|
+
} | null | undefined;
|
|
25
|
+
traceId?: string | null | undefined;
|
|
26
|
+
}>>>;
|
|
27
|
+
declare const getContextSchema: () => ReturnType<typeof createContextSchema>;
|
|
28
|
+
export type SafeContextType = z.infer<ReturnType<typeof createContextSchema>>;
|
|
29
|
+
export { getContextSchema };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { z } from '@exodus/zod';
|
|
2
|
+
import { isSafe } from '@exodus/safe-string';
|
|
3
|
+
import { memoize } from '@exodus/basic-utils';
|
|
4
|
+
const createContextSchema = () => z
|
|
5
|
+
.object({
|
|
6
|
+
navigation: z
|
|
7
|
+
.object({
|
|
8
|
+
currentRouteName: z.string().refine(isSafe),
|
|
9
|
+
previousRouteName: z.string().refine(isSafe).nullish(),
|
|
10
|
+
})
|
|
11
|
+
.strict()
|
|
12
|
+
.nullish(),
|
|
13
|
+
traceId: z.string().refine(isSafe).nullish(),
|
|
14
|
+
})
|
|
15
|
+
.strict()
|
|
16
|
+
.nullish();
|
|
17
|
+
// Memoize the factory for lazy loading.
|
|
18
|
+
const getContextSchema = memoize(createContextSchema);
|
|
19
|
+
export { getContextSchema };
|
package/lib/safe-error.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Frame } from './types.js';
|
|
2
2
|
import type { CommonErrorString } from './common-errors.js';
|
|
3
|
+
export declare const MAX_LINKED_ERRORS_DEPTH = 5;
|
|
3
4
|
type ReadonlySetValues<S> = S extends ReadonlySet<infer T> ? T : never;
|
|
4
5
|
declare const SAFE_NAMES_SET: ReadonlySet<"Error" | "AssertionError" | "TypeError" | "RangeError" | "UnknownError" | "SafeErrorFailedToParse" | "TimeoutError" | "SyntaxError">;
|
|
5
6
|
type StaticAllowlistString = string & {
|
|
@@ -13,14 +14,23 @@ export type SafeCode = string & {
|
|
|
13
14
|
__branded_type: 'SafeCodeOutcome';
|
|
14
15
|
};
|
|
15
16
|
export type SafeString = CommonErrorString | StaticAllowlistString | RuntimeSafeString;
|
|
17
|
+
export type SafeErrorJSON = {
|
|
18
|
+
name: SafeName;
|
|
19
|
+
code?: SafeCode;
|
|
20
|
+
hint?: SafeString;
|
|
21
|
+
stack?: string;
|
|
22
|
+
linkedErrors?: SafeErrorJSON[];
|
|
23
|
+
timestamp: number;
|
|
24
|
+
};
|
|
16
25
|
type UnknownError = Error & {
|
|
17
26
|
hint?: unknown;
|
|
18
27
|
code?: unknown;
|
|
28
|
+
cause?: unknown;
|
|
19
29
|
};
|
|
20
30
|
declare const FACTORY_SYMBOL: unique symbol;
|
|
21
31
|
export declare class SafeError {
|
|
22
32
|
#private;
|
|
23
|
-
static from<T extends UnknownError>(err: T): SafeError;
|
|
33
|
+
static from<T extends UnknownError>(err: T, depth?: number): SafeError;
|
|
24
34
|
get name(): "Error" | "AssertionError" | "TypeError" | "RangeError" | "UnknownError" | "SafeErrorFailedToParse" | "TimeoutError" | "SyntaxError";
|
|
25
35
|
get code(): SafeCode | undefined;
|
|
26
36
|
get hint(): SafeString | undefined;
|
|
@@ -36,19 +46,15 @@ export declare class SafeError {
|
|
|
36
46
|
__proto__: null;
|
|
37
47
|
}[] | undefined;
|
|
38
48
|
get stack(): string | undefined;
|
|
49
|
+
get linkedErrors(): SafeError[] | undefined;
|
|
39
50
|
get timestamp(): number;
|
|
40
|
-
toJSON():
|
|
41
|
-
|
|
42
|
-
code?: SafeCode;
|
|
43
|
-
hint?: SafeString;
|
|
44
|
-
stack?: string;
|
|
45
|
-
timestamp: number;
|
|
46
|
-
};
|
|
47
|
-
constructor({ name, code, hint, stack, initSymbol, }: {
|
|
51
|
+
toJSON(): SafeErrorJSON;
|
|
52
|
+
constructor({ name, code, hint, stack, linkedErrors, initSymbol, }: {
|
|
48
53
|
name: SafeName;
|
|
49
54
|
code?: SafeCode;
|
|
50
55
|
hint?: SafeString;
|
|
51
56
|
stack?: Frame[];
|
|
57
|
+
linkedErrors?: SafeError[];
|
|
52
58
|
initSymbol: typeof FACTORY_SYMBOL;
|
|
53
59
|
});
|
|
54
60
|
}
|
package/lib/safe-error.js
CHANGED
|
@@ -3,6 +3,7 @@ import assert from 'minimalistic-assert';
|
|
|
3
3
|
import parseStackTraceNatively, { stackFramesToString } from './stack.js';
|
|
4
4
|
import { omitUndefined } from './utils.js';
|
|
5
5
|
import { toCommonErrorMessage } from './common-errors.js';
|
|
6
|
+
export const MAX_LINKED_ERRORS_DEPTH = 5;
|
|
6
7
|
function makeReadonlySet(values) {
|
|
7
8
|
return new Set(values);
|
|
8
9
|
}
|
|
@@ -29,12 +30,13 @@ function isSafeCode(value) {
|
|
|
29
30
|
const staticAllowlist = getAllowlist();
|
|
30
31
|
const FACTORY_SYMBOL = Symbol('SafeError');
|
|
31
32
|
export class SafeError {
|
|
32
|
-
static from(err) {
|
|
33
|
+
static from(err, depth = 0) {
|
|
33
34
|
let safeName;
|
|
34
35
|
let safeCode;
|
|
35
36
|
let safeHint;
|
|
37
|
+
let linkedSafeErrors;
|
|
36
38
|
try {
|
|
37
|
-
const { name, message, hint, code } = err;
|
|
39
|
+
const { name, message, hint, code, cause } = err;
|
|
38
40
|
safeName = isSafeName(name) ? name : 'UnknownError';
|
|
39
41
|
const safeCodeCandidate = `${code}`;
|
|
40
42
|
safeCode = isSafeCode(safeCodeCandidate) ? safeCodeCandidate : undefined;
|
|
@@ -45,11 +47,27 @@ export class SafeError {
|
|
|
45
47
|
staticAllowlist.find((safePrefix) => hintCandidates.some((str) => str.startsWith(safePrefix))) ||
|
|
46
48
|
toCommonErrorMessage(message);
|
|
47
49
|
const safeStack = parseStackTraceNatively(err);
|
|
50
|
+
if (cause instanceof Error && depth === 0) {
|
|
51
|
+
linkedSafeErrors = [];
|
|
52
|
+
let currentCause = cause;
|
|
53
|
+
while (currentCause instanceof Error && ++depth <= MAX_LINKED_ERRORS_DEPTH) {
|
|
54
|
+
try {
|
|
55
|
+
linkedSafeErrors.push(SafeError.from(currentCause, depth));
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
linkedSafeErrors.push(FAILED_TO_PARSE_ERROR);
|
|
59
|
+
}
|
|
60
|
+
finally {
|
|
61
|
+
currentCause = currentCause.cause;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
48
65
|
return new SafeError({
|
|
49
66
|
name: safeName,
|
|
50
67
|
code: safeCode,
|
|
51
68
|
hint: safeHint,
|
|
52
69
|
stack: safeStack,
|
|
70
|
+
linkedErrors: linkedSafeErrors,
|
|
53
71
|
initSymbol: FACTORY_SYMBOL,
|
|
54
72
|
});
|
|
55
73
|
}
|
|
@@ -60,6 +78,7 @@ export class SafeError {
|
|
|
60
78
|
name: safeName,
|
|
61
79
|
code: safeCode,
|
|
62
80
|
hint: safeHint,
|
|
81
|
+
linkedErrors: linkedSafeErrors,
|
|
63
82
|
initSymbol: FACTORY_SYMBOL,
|
|
64
83
|
})
|
|
65
84
|
: FAILED_TO_PARSE_ERROR;
|
|
@@ -73,6 +92,7 @@ export class SafeError {
|
|
|
73
92
|
#code;
|
|
74
93
|
#hint;
|
|
75
94
|
#stackFrames;
|
|
95
|
+
#linkedErrors;
|
|
76
96
|
#timestamp;
|
|
77
97
|
get name() {
|
|
78
98
|
return this.#name;
|
|
@@ -92,6 +112,9 @@ export class SafeError {
|
|
|
92
112
|
? `${this.name}: ${this.code || this.hint || 'unknownHint'}\n${stackTrace}`
|
|
93
113
|
: undefined;
|
|
94
114
|
}
|
|
115
|
+
get linkedErrors() {
|
|
116
|
+
return this.#linkedErrors;
|
|
117
|
+
}
|
|
95
118
|
get timestamp() {
|
|
96
119
|
return this.#timestamp;
|
|
97
120
|
}
|
|
@@ -102,15 +125,17 @@ export class SafeError {
|
|
|
102
125
|
code: this.code,
|
|
103
126
|
hint: this.hint,
|
|
104
127
|
stack: this.stack,
|
|
128
|
+
linkedErrors: this.linkedErrors?.map((linkedError) => linkedError.toJSON()),
|
|
105
129
|
timestamp: this.timestamp,
|
|
106
130
|
});
|
|
107
131
|
}
|
|
108
|
-
constructor({ name, code, hint, stack, initSymbol, }) {
|
|
132
|
+
constructor({ name, code, hint, stack, linkedErrors, initSymbol, }) {
|
|
109
133
|
assert(initSymbol === FACTORY_SYMBOL, 'SafeError: use SafeError.from()');
|
|
110
134
|
this.#name = name;
|
|
111
135
|
this.#code = code;
|
|
112
136
|
this.#hint = hint;
|
|
113
137
|
this.#stackFrames = stack?.map((frame) => ({ ...frame }));
|
|
138
|
+
this.#linkedErrors = linkedErrors;
|
|
114
139
|
this.#timestamp = Date.now();
|
|
115
140
|
}
|
|
116
141
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exodus/errors",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "3.
|
|
4
|
+
"version": "3.5.0",
|
|
5
5
|
"description": "Utilities for error handling in client code, such as sanitization",
|
|
6
6
|
"author": "Exodus Movement, Inc.",
|
|
7
7
|
"repository": {
|
|
@@ -34,7 +34,9 @@
|
|
|
34
34
|
"test:hermes": "EXODUS_TEST_COVERAGE=0 run -T exodus-test --engine hermes:bundle --esbuild --jest src/__tests__/hermes/*.test.ts src/__tests__/engine-agnostic/*.test.ts"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
+
"@exodus/basic-utils": "^3.0.1",
|
|
37
38
|
"@exodus/safe-string": "^1.2.0",
|
|
39
|
+
"@exodus/zod": "^3.24.2",
|
|
38
40
|
"minimalistic-assert": "^1.0.1"
|
|
39
41
|
},
|
|
40
42
|
"devDependencies": {
|
|
@@ -42,7 +44,8 @@
|
|
|
42
44
|
"@types/minimalistic-assert": "^1.0.1"
|
|
43
45
|
},
|
|
44
46
|
"publishConfig": {
|
|
45
|
-
"access": "public"
|
|
47
|
+
"access": "public",
|
|
48
|
+
"provenance": false
|
|
46
49
|
},
|
|
47
|
-
"gitHead": "
|
|
50
|
+
"gitHead": "32d28daa40c3e8c600440732d2792699448e1be8"
|
|
48
51
|
}
|