@outputai/core 0.10.1-dev.b7b2fbe.0 → 0.10.1-next.1070949.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/package.json +8 -7
- package/src/bus.js +58 -20
- package/src/bus.spec.js +135 -77
- package/src/consts.js +5 -2
- package/src/errors.d.ts +12 -2
- package/src/errors.js +17 -3
- package/src/errors.spec.js +19 -0
- package/src/helpers/activity_options.js +21 -0
- package/src/helpers/activity_options.spec.js +55 -0
- package/src/helpers/error_serializer.js +236 -0
- package/src/helpers/error_serializer.spec.js +413 -0
- package/src/helpers/errors.js +23 -14
- package/src/helpers/errors.spec.js +31 -75
- package/src/helpers/fetch.js +1 -38
- package/src/helpers/fetch.spec.js +1 -39
- package/src/helpers/function.js +13 -0
- package/src/helpers/function.spec.js +23 -1
- package/src/helpers/object.js +5 -1
- package/src/helpers/object.spec.js +5 -0
- package/src/helpers/redact.js +51 -0
- package/src/helpers/redact.spec.js +66 -0
- package/src/helpers/string.js +10 -0
- package/src/helpers/string.spec.js +36 -1
- package/src/hooks/index.d.ts +76 -78
- package/src/hooks/index.js +57 -49
- package/src/hooks/index.spec.js +104 -105
- package/src/hooks/pending_hooks.js +24 -0
- package/src/hooks/pending_hooks.spec.js +62 -0
- package/src/interface/evaluator.d.ts +10 -9
- package/src/interface/evaluator.js +1 -6
- package/src/interface/evaluator.spec.js +10 -10
- package/src/interface/index.js +2 -1
- package/src/interface/step.d.ts +16 -15
- package/src/interface/step.js +1 -6
- package/src/interface/step.spec.js +10 -10
- package/src/interface/validations/index.js +33 -53
- package/src/interface/validations/index.spec.js +25 -17
- package/src/interface/webhook.js +1 -2
- package/src/interface/webhook.spec.js +25 -19
- package/src/interface/workflow.d.ts +25 -18
- package/src/interface/workflow.js +24 -52
- package/src/interface/workflow.spec.js +109 -108
- package/src/interface/workflow_activity_options.js +2 -5
- package/src/interface/workflow_utils.d.ts +15 -0
- package/src/interface/workflow_utils.js +19 -0
- package/src/interface/workflow_utils.spec.js +82 -1
- package/src/sdk/runtime/events.d.ts +4 -6
- package/src/sdk/runtime/events.js +2 -15
- package/src/sdk/runtime/events.spec.js +14 -92
- package/src/tracing/trace_engine.js +9 -11
- package/src/tracing/trace_engine.spec.js +67 -28
- package/src/worker/catalog_workflow/catalog_job.js +3 -3
- package/src/worker/catalog_workflow/catalog_job.spec.js +1 -1
- package/src/worker/configs.js +0 -3
- package/src/worker/global_functions.js +2 -2
- package/src/worker/global_functions.spec.js +3 -3
- package/src/worker/index.js +20 -11
- package/src/worker/index.spec.js +29 -16
- package/src/worker/interceptors/activity.js +34 -21
- package/src/worker/interceptors/activity.spec.js +77 -172
- package/src/worker/interceptors/workflow.js +53 -11
- package/src/worker/interceptors/workflow.spec.js +176 -49
- package/src/worker/loader/workflows.js +16 -3
- package/src/worker/loader/workflows.spec.js +37 -5
- package/src/worker/log_hooks.js +22 -19
- package/src/worker/log_hooks.spec.js +14 -6
- package/src/worker/sinks.js +8 -8
- package/src/worker/sinks.spec.js +9 -9
- package/src/worker/telemetry.js +2 -1
- package/src/worker/temporal_logger.js +33 -0
- package/src/worker/temporal_logger.spec.js +90 -0
- package/src/helpers/aggregations.js +0 -44
- package/src/helpers/aggregations.spec.js +0 -92
- package/src/tracing/tools/utils.js +0 -42
- package/src/tracing/tools/utils.spec.js +0 -83
- package/src/worker/proxy.js +0 -20
- package/src/worker/proxy.spec.js +0 -51
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { tryOrUndefined } from './function.js';
|
|
2
|
+
import { redactUrl } from './redact.js';
|
|
3
|
+
import { isUrl } from './string.js';
|
|
4
|
+
|
|
5
|
+
const MAX_VALUE_DEPTH = 10;
|
|
6
|
+
const MAX_PROTOTYPE_DEPTH = 10;
|
|
7
|
+
const MAX_STRING_LEN = 16_384;
|
|
8
|
+
const MAX_OBJECT_KEYS = 50;
|
|
9
|
+
const MAX_OBJECT_KEY_LEN = 256;
|
|
10
|
+
const MAX_ARRAY_SIZE = 50;
|
|
11
|
+
const MAX_SERIALIZED_VALUES = 1000;
|
|
12
|
+
const MAX_BIGINT = 10n ** BigInt( MAX_STRING_LEN - 1 ); // room for sign + trailing "n"
|
|
13
|
+
const GLOBAL_IGNORED_KEYS = [
|
|
14
|
+
'__proto__',
|
|
15
|
+
// Raw Temporal wire failure; duplicates normalized causes/details and may contain encoded payloads.
|
|
16
|
+
'failure',
|
|
17
|
+
// Raw failure retained by Nexus for lossless round-tripping; duplicates surfaced diagnostics and internal payloads.
|
|
18
|
+
'originalFailure',
|
|
19
|
+
// Protobufjs reflection metadata; points into a large cyclic schema graph with no diagnostic value.
|
|
20
|
+
'$type',
|
|
21
|
+
// HTTP request/response
|
|
22
|
+
/^request$/i,
|
|
23
|
+
/^request_?body$/i,
|
|
24
|
+
/^request_?body_?values$/i,
|
|
25
|
+
/^response$/i,
|
|
26
|
+
// Headers
|
|
27
|
+
/^headers$/i,
|
|
28
|
+
/^request_?headers$/i,
|
|
29
|
+
/^response_?headers$/i,
|
|
30
|
+
// Credentials
|
|
31
|
+
/authorization$/i,
|
|
32
|
+
/authentication$/i,
|
|
33
|
+
/cookie$/i,
|
|
34
|
+
/key$/i,
|
|
35
|
+
/token$/i,
|
|
36
|
+
/password$/i,
|
|
37
|
+
/secret$/i,
|
|
38
|
+
/credentials?$/i
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
const Marker = {
|
|
42
|
+
BigIntTooLarge: '[BigInt too large]',
|
|
43
|
+
Circular: '[Circular Reference]',
|
|
44
|
+
MaxDepth: '[Maximum Depth Reached]',
|
|
45
|
+
MaxKeys: '[Maximum Object Properties Reached]',
|
|
46
|
+
MaxSelfCalls: '[Serialization Limit Reached]',
|
|
47
|
+
Unserializable: '[Unserializable]',
|
|
48
|
+
InvalidDate: '[Invalid Date]',
|
|
49
|
+
TruncatedArray: '... [:v more items omitted]',
|
|
50
|
+
TruncatedString: '... [:v more characters omitted]'
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export const truncateString = ( v, max = MAX_STRING_LEN ) =>
|
|
54
|
+
v.length <= max ? v : ( v.slice( 0, max ) + Marker.TruncatedString.replace( ':v', v.length - max ) );
|
|
55
|
+
|
|
56
|
+
export const truncateArray = ( v, max = MAX_ARRAY_SIZE ) =>
|
|
57
|
+
v.length <= max ? v : v.slice( 0, max ).concat( Marker.TruncatedArray.replace( ':v', v.length - max ) );
|
|
58
|
+
|
|
59
|
+
/** Converts a prototype chain to a max sized array */
|
|
60
|
+
export const flattenPrototypeChain = ( target, depth = 0, result = [], maxDepth = MAX_PROTOTYPE_DEPTH ) =>
|
|
61
|
+
!target || depth >= maxDepth ? result :
|
|
62
|
+
flattenPrototypeChain( Object.getPrototypeOf( target ), depth + 1, result.concat( target ), maxDepth );
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Define the best "name" for an Error object
|
|
66
|
+
* Rules: Assigned .name property > inherited .name if not "Error" > constructor.name
|
|
67
|
+
*/
|
|
68
|
+
export const resolveErrorName = target => {
|
|
69
|
+
// if its not error, don't return anything
|
|
70
|
+
if ( !( target instanceof Error ) ) {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const name = tryOrUndefined( () => target.name );
|
|
75
|
+
// if name is an explicit property return it
|
|
76
|
+
if ( Object.hasOwn( target, 'name' ) ) {
|
|
77
|
+
return name; // eslint-disable-line consistent-return
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// if not explicit (resolves from parent) check if it not a default prop
|
|
81
|
+
if ( name !== undefined && name !== 'Error' ) {
|
|
82
|
+
return name; // eslint-disable-line consistent-return
|
|
83
|
+
}
|
|
84
|
+
// fallback to the constructor name, or undefined
|
|
85
|
+
return target.constructor?.name; // eslint-disable-line consistent-return
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
/** Check is target is in ancestor chain */
|
|
89
|
+
const isAncestor = ( ancestor, target ) =>
|
|
90
|
+
ancestor !== null && ( ancestor.value === target || isAncestor( ancestor.ancestor, target ) );
|
|
91
|
+
|
|
92
|
+
class GlobalCounter {
|
|
93
|
+
value = 0;
|
|
94
|
+
count() {
|
|
95
|
+
this.value++;
|
|
96
|
+
return this;
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Recursive serialize a value, recursion comes for Array-like values and objects */
|
|
101
|
+
const serializeValue = ( target, options, state = { depth: 0, seen: new GlobalCounter(), ancestor: null } ) => {
|
|
102
|
+
// If the same object was already seen in this branch, it is a circular reference
|
|
103
|
+
if ( isAncestor( state.ancestor, target ) ) {
|
|
104
|
+
return Marker.Circular;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Reached the maximum times the function can self call
|
|
108
|
+
if ( state.seen.value >= MAX_SERIALIZED_VALUES ) {
|
|
109
|
+
return Marker.MaxSelfCalls;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Depth control
|
|
113
|
+
if ( state.depth >= MAX_VALUE_DEPTH ) {
|
|
114
|
+
return Marker.MaxDepth;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const nextState = {
|
|
118
|
+
depth: state.depth + 1,
|
|
119
|
+
seen: state.seen.count(),
|
|
120
|
+
ancestor: { value: target, ancestor: state.ancestor }
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
if ( target instanceof Date ) {
|
|
125
|
+
return Number.isNaN( target.getTime() ) ? Marker.InvalidDate : target.toISOString();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if ( target instanceof RegExp ) {
|
|
129
|
+
return target.toString();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if ( typeof target === 'bigint' ) {
|
|
133
|
+
return ( target >= MAX_BIGINT || target <= -MAX_BIGINT ) ? Marker.BigIntTooLarge : `${target}n`;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if ( typeof target === 'symbol' ) {
|
|
137
|
+
return truncateString( target.toString() );
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if ( typeof target === 'function' ) {
|
|
141
|
+
return `[Function: ${target.name || '(anonymous)'}]`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Buffers, typed arrays, and raw ArrayBuffers
|
|
145
|
+
if ( ArrayBuffer.isView( target ) || target instanceof ArrayBuffer ) {
|
|
146
|
+
const sizeKey = target instanceof ArrayBuffer || target instanceof DataView ? 'byteLength' : 'length';
|
|
147
|
+
return `${target.constructor.name}(${target[sizeKey]})`;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if ( typeof target === 'string' ) {
|
|
151
|
+
return truncateString( isUrl( target ) ? redactUrl( target ) : target );
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Other primitives are returned as they are
|
|
155
|
+
if ( typeof target !== 'object' || target === null ) {
|
|
156
|
+
return target;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if ( target instanceof URL ) {
|
|
160
|
+
return truncateString( redactUrl( target ) );
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Maps/Sets use a helper function
|
|
164
|
+
if ( [ Map, Set ].some( I => target instanceof I ) ) {
|
|
165
|
+
const iterator = target instanceof Set ? target.values() : target.entries();
|
|
166
|
+
const values = iterator.take( MAX_ARRAY_SIZE ).toArray().map( v => serializeValue( v, options, nextState ) );
|
|
167
|
+
return values.concat( target.size > MAX_ARRAY_SIZE ? Marker.TruncatedArray.replace( ':v', target.size - MAX_ARRAY_SIZE ) : [] );
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Array as also recursively serialized
|
|
171
|
+
if ( Array.isArray( target ) ) {
|
|
172
|
+
return truncateArray( target ).map( value => serializeValue( value, options, nextState ) );
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Objects
|
|
176
|
+
const prototypes = flattenPrototypeChain( target );
|
|
177
|
+
const receiver = target;
|
|
178
|
+
|
|
179
|
+
const props = prototypes.reduce( ( projection, proto ) => {
|
|
180
|
+
if ( Object.keys( projection ).length > MAX_OBJECT_KEYS ) {
|
|
181
|
+
return projection;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const keys = Object.getOwnPropertyNames( proto );
|
|
185
|
+
for ( const key of keys ) {
|
|
186
|
+
if ( options.ignoredKeys.some( e => e.test ? e.test( key ) : e === key ) ) {
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const sanitizedKey = truncateString( key, MAX_OBJECT_KEY_LEN );
|
|
191
|
+
if ( Object.hasOwn( projection, sanitizedKey ) ) { // do not overwrite similar keys
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const value = tryOrUndefined( () => Reflect.get( proto, key, receiver ) );
|
|
196
|
+
if ( value === undefined || typeof value === 'function' ) {
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// after confirming the key is valid, check if it would break the limit
|
|
201
|
+
if ( Object.keys( projection ).length === MAX_OBJECT_KEYS ) {
|
|
202
|
+
delete projection['...']; // make sure this is the last added property
|
|
203
|
+
return Object.assign( projection, { ['...']: Marker.MaxKeys } );
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
projection[sanitizedKey] = serializeValue( value, options, nextState );
|
|
207
|
+
};
|
|
208
|
+
return projection;
|
|
209
|
+
}, {} );
|
|
210
|
+
|
|
211
|
+
// name resolver
|
|
212
|
+
if ( !options.ignoredKeys.includes( 'name' ) && target instanceof Error ) {
|
|
213
|
+
const name = resolveErrorName( target );
|
|
214
|
+
if ( name !== undefined ) {
|
|
215
|
+
props.name = name;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return props;
|
|
220
|
+
} catch {
|
|
221
|
+
return Marker.Unserializable;
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Converts an error-like value and its inherited properties into a plain object.
|
|
227
|
+
* Serializes recursively, including values inside Arrays, maps and sets.
|
|
228
|
+
* Has depth, prototype depth, max string size, max array/map/set size, max object props count limits.
|
|
229
|
+
*
|
|
230
|
+
* @param {*} target - Value to serialize
|
|
231
|
+
* @param {object} [options] - Serialization options
|
|
232
|
+
* @param {array} [options.dropKeys=[]] - Keys to exclude from the serialization (including recursion)
|
|
233
|
+
* @returns {*} Serialized value
|
|
234
|
+
*/
|
|
235
|
+
export const serializeError = ( target, { dropKeys } = {} ) =>
|
|
236
|
+
serializeValue( target, { ignoredKeys: GLOBAL_IGNORED_KEYS.concat( dropKeys ?? [] ) } );
|
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
flattenPrototypeChain,
|
|
4
|
+
resolveErrorName,
|
|
5
|
+
serializeError,
|
|
6
|
+
truncateArray,
|
|
7
|
+
truncateString
|
|
8
|
+
} from './error_serializer.js';
|
|
9
|
+
|
|
10
|
+
const redactUrlMock = vi.hoisted( () => vi.fn().mockReturnValue( '[Redacted URL]' ) );
|
|
11
|
+
const isUrlMock = vi.hoisted( () => vi.fn().mockReturnValue( false ) );
|
|
12
|
+
|
|
13
|
+
vi.mock( './redact.js', () => ( { redactUrl: redactUrlMock } ) );
|
|
14
|
+
vi.mock( './string.js', () => ( { isUrl: isUrlMock } ) );
|
|
15
|
+
|
|
16
|
+
describe( 'error serializer helpers', () => {
|
|
17
|
+
describe( 'truncateString', () => {
|
|
18
|
+
it( 'preserves values within the limit and marks omitted characters', () => {
|
|
19
|
+
expect( truncateString( 'exact', 5 ) ).toBe( 'exact' );
|
|
20
|
+
expect( truncateString( 'longer', 5 ) ).toBe( 'longe... [1 more characters omitted]' );
|
|
21
|
+
} );
|
|
22
|
+
} );
|
|
23
|
+
|
|
24
|
+
describe( 'truncateArray', () => {
|
|
25
|
+
it( 'preserves values within the limit and marks omitted items without mutating the source', () => {
|
|
26
|
+
const exact = [ 1, 2 ];
|
|
27
|
+
const long = [ 1, 2, 3 ];
|
|
28
|
+
|
|
29
|
+
expect( truncateArray( exact, 2 ) ).toBe( exact );
|
|
30
|
+
expect( truncateArray( long, 2 ) ).toEqual( [ 1, 2, '... [1 more items omitted]' ] );
|
|
31
|
+
expect( long ).toEqual( [ 1, 2, 3 ] );
|
|
32
|
+
} );
|
|
33
|
+
} );
|
|
34
|
+
|
|
35
|
+
describe( 'flattenPrototypeChain', () => {
|
|
36
|
+
it( 'returns child-first prototypes and honors the depth limit', () => {
|
|
37
|
+
const grandparent = { grandparent: true };
|
|
38
|
+
const parent = Object.create( grandparent );
|
|
39
|
+
const child = Object.create( parent );
|
|
40
|
+
|
|
41
|
+
expect( flattenPrototypeChain( child, 0, [], 2 ) ).toEqual( [ child, parent ] );
|
|
42
|
+
expect( flattenPrototypeChain( child, 0, [], 3 ) ).toEqual( [ child, parent, grandparent ] );
|
|
43
|
+
} );
|
|
44
|
+
} );
|
|
45
|
+
|
|
46
|
+
describe( 'resolveErrorName', () => {
|
|
47
|
+
it( 'uses explicit, inherited, and constructor names in precedence order', () => {
|
|
48
|
+
class ConstructorNamedError extends Error {}
|
|
49
|
+
class InheritedNamedError extends Error {}
|
|
50
|
+
InheritedNamedError.prototype.name = 'InheritedName';
|
|
51
|
+
class ExplicitlyNamedError extends Error {
|
|
52
|
+
name = 'ExplicitName';
|
|
53
|
+
}
|
|
54
|
+
class ExplicitlyGenericError extends Error {
|
|
55
|
+
name = 'Error';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
expect( resolveErrorName( new ConstructorNamedError() ) ).toBe( 'ConstructorNamedError' );
|
|
59
|
+
expect( resolveErrorName( new InheritedNamedError() ) ).toBe( 'InheritedName' );
|
|
60
|
+
expect( resolveErrorName( new ExplicitlyNamedError() ) ).toBe( 'ExplicitName' );
|
|
61
|
+
expect( resolveErrorName( new ExplicitlyGenericError() ) ).toBe( 'Error' );
|
|
62
|
+
expect( resolveErrorName( new Error() ) ).toBe( 'Error' );
|
|
63
|
+
expect( resolveErrorName( { name: 'ObjectName' } ) ).toBeUndefined();
|
|
64
|
+
} );
|
|
65
|
+
|
|
66
|
+
it( 'returns undefined when an explicit name getter throws', () => {
|
|
67
|
+
const error = new Error();
|
|
68
|
+
Object.defineProperty( error, 'name', {
|
|
69
|
+
get: () => {
|
|
70
|
+
throw new Error( 'name unavailable' );
|
|
71
|
+
}
|
|
72
|
+
} );
|
|
73
|
+
|
|
74
|
+
expect( resolveErrorName( error ) ).toBeUndefined();
|
|
75
|
+
} );
|
|
76
|
+
} );
|
|
77
|
+
} );
|
|
78
|
+
|
|
79
|
+
describe( 'serializeError', () => {
|
|
80
|
+
it( 'serializes descriptors and skips unusable properties', () => {
|
|
81
|
+
const error = new Error( 'step failed' );
|
|
82
|
+
Object.defineProperties( error, {
|
|
83
|
+
code: { value: 'ESTEP' },
|
|
84
|
+
hidden: { value: 'available for debugging' },
|
|
85
|
+
computed: { get: () => `computed from ${error.message}` },
|
|
86
|
+
broken: {
|
|
87
|
+
get: () => {
|
|
88
|
+
throw new Error( 'getter failed' );
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
callback: { value: () => 'not serializable' },
|
|
92
|
+
skipped: { value: undefined },
|
|
93
|
+
symbol: { value: Symbol( 'debug' ) },
|
|
94
|
+
__proto__: { value: { unsafe: true } }
|
|
95
|
+
} );
|
|
96
|
+
error.nullable = null;
|
|
97
|
+
|
|
98
|
+
const serialized = serializeError( error, { dropKeys: [ 'stack' ] } );
|
|
99
|
+
|
|
100
|
+
expect( serialized ).toEqual( {
|
|
101
|
+
name: 'Error',
|
|
102
|
+
message: 'step failed',
|
|
103
|
+
code: 'ESTEP',
|
|
104
|
+
hidden: 'available for debugging',
|
|
105
|
+
computed: 'computed from step failed',
|
|
106
|
+
nullable: null,
|
|
107
|
+
symbol: 'Symbol(debug)'
|
|
108
|
+
} );
|
|
109
|
+
expect( Object.hasOwn( serialized, '__proto__' ) ).toBe( false );
|
|
110
|
+
} );
|
|
111
|
+
|
|
112
|
+
it( 'applies custom dropped keys recursively and across prototypes', () => {
|
|
113
|
+
class DebugError extends Error {}
|
|
114
|
+
DebugError.prototype.internal = 'prototype value';
|
|
115
|
+
|
|
116
|
+
const cause = new DebugError( 'cause failed' );
|
|
117
|
+
cause.context = { internal: 'nested cause value', visible: 'cause context' };
|
|
118
|
+
const error = new DebugError( 'request failed', { cause } );
|
|
119
|
+
error.internal = 'instance value';
|
|
120
|
+
error.context = { internal: 'nested root value', visible: 'root context' };
|
|
121
|
+
|
|
122
|
+
const serialized = serializeError( error, { dropKeys: [ 'internal', 'stack' ] } );
|
|
123
|
+
|
|
124
|
+
expect( serialized ).not.toHaveProperty( 'internal' );
|
|
125
|
+
expect( serialized.context ).toEqual( { visible: 'root context' } );
|
|
126
|
+
expect( serialized.cause ).not.toHaveProperty( 'internal' );
|
|
127
|
+
expect( serialized.cause.context ).toEqual( { visible: 'cause context' } );
|
|
128
|
+
} );
|
|
129
|
+
|
|
130
|
+
it( 'merges prototype properties child-first without invoking overridden getters', () => {
|
|
131
|
+
const overriddenGetter = vi.fn( () => 'base' );
|
|
132
|
+
class BaseFailure extends Error {}
|
|
133
|
+
Object.defineProperties( BaseFailure.prototype, {
|
|
134
|
+
baseProperty: { value: 'base' },
|
|
135
|
+
overridden: { get: overriddenGetter }
|
|
136
|
+
} );
|
|
137
|
+
class RequestFailure extends BaseFailure {}
|
|
138
|
+
RequestFailure.prototype.requestProperty = 'request';
|
|
139
|
+
|
|
140
|
+
const error = new RequestFailure( 'request failed' );
|
|
141
|
+
Object.defineProperty( error, 'overridden', { value: 'instance' } );
|
|
142
|
+
|
|
143
|
+
expect( serializeError( error, { dropKeys: [ 'stack' ] } ) ).toEqual( {
|
|
144
|
+
name: 'RequestFailure',
|
|
145
|
+
message: 'request failed',
|
|
146
|
+
overridden: 'instance',
|
|
147
|
+
requestProperty: 'request',
|
|
148
|
+
baseProperty: 'base'
|
|
149
|
+
} );
|
|
150
|
+
expect( overriddenGetter ).not.toHaveBeenCalled();
|
|
151
|
+
} );
|
|
152
|
+
|
|
153
|
+
it( 'recursively serializes causes, nested objects, and arrays', () => {
|
|
154
|
+
const databaseError = new Error( 'database unavailable' );
|
|
155
|
+
databaseError.code = 'ECONNREFUSED';
|
|
156
|
+
const serviceError = new Error( 'service failed', { cause: databaseError } );
|
|
157
|
+
serviceError.context = {
|
|
158
|
+
attempts: [
|
|
159
|
+
{ number: 1, metadata: { retryable: true } },
|
|
160
|
+
{ number: 2, metadata: null }
|
|
161
|
+
]
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
expect( serializeError( serviceError, { dropKeys: [ 'stack' ] } ) ).toEqual( {
|
|
165
|
+
name: 'Error',
|
|
166
|
+
message: 'service failed',
|
|
167
|
+
cause: {
|
|
168
|
+
name: 'Error',
|
|
169
|
+
message: 'database unavailable',
|
|
170
|
+
code: 'ECONNREFUSED'
|
|
171
|
+
},
|
|
172
|
+
context: {
|
|
173
|
+
attempts: [
|
|
174
|
+
{ number: 1, metadata: { retryable: true } },
|
|
175
|
+
{ number: 2, metadata: null }
|
|
176
|
+
]
|
|
177
|
+
}
|
|
178
|
+
} );
|
|
179
|
+
} );
|
|
180
|
+
|
|
181
|
+
it( 'returns primitives as-is', () => {
|
|
182
|
+
expect( serializeError( undefined ) ).toBeUndefined();
|
|
183
|
+
expect( serializeError( null ) ).toBeNull();
|
|
184
|
+
expect( serializeError( 42 ) ).toBe( 42 );
|
|
185
|
+
expect( serializeError( true ) ).toBe( true );
|
|
186
|
+
} );
|
|
187
|
+
|
|
188
|
+
it( 'serializes Date values as ISO strings and marks invalid dates', () => {
|
|
189
|
+
expect( serializeError( new Date( '2026-07-22T16:30:00.000Z' ) ) ).toBe( '2026-07-22T16:30:00.000Z' );
|
|
190
|
+
expect( serializeError( new Date( 'foo' ) ) ).toBe( '[Invalid Date]' );
|
|
191
|
+
} );
|
|
192
|
+
|
|
193
|
+
it( 'serializes RegExp values with toString', () => {
|
|
194
|
+
expect( serializeError( /request-\d+/gi ) ).toBe( '/request-\\d+/gi' );
|
|
195
|
+
} );
|
|
196
|
+
|
|
197
|
+
it( 'serializes bigint values with an n suffix', () => {
|
|
198
|
+
expect( serializeError( 42n ) ).toBe( '42n' );
|
|
199
|
+
expect( serializeError( -42n ) ).toBe( '-42n' );
|
|
200
|
+
} );
|
|
201
|
+
|
|
202
|
+
it( 'marks bigints that would exceed the string length budget', () => {
|
|
203
|
+
const maxDigits = 16_383; // MAX_STRING_LEN - 1
|
|
204
|
+
const tooLarge = 10n ** BigInt( maxDigits );
|
|
205
|
+
|
|
206
|
+
expect( serializeError( tooLarge - 1n ) ).toBe( `${tooLarge - 1n}n` );
|
|
207
|
+
expect( serializeError( tooLarge ) ).toBe( '[BigInt too large]' );
|
|
208
|
+
expect( serializeError( -tooLarge ) ).toBe( '[BigInt too large]' );
|
|
209
|
+
expect( serializeError( 10n ** 20_000n ) ).toBe( '[BigInt too large]' );
|
|
210
|
+
} );
|
|
211
|
+
|
|
212
|
+
it( 'serializes symbol values', () => {
|
|
213
|
+
expect( serializeError( Symbol( 'request' ) ) ).toBe( 'Symbol(request)' );
|
|
214
|
+
} );
|
|
215
|
+
|
|
216
|
+
it( 'serializes functions by name', () => {
|
|
217
|
+
function namedFunction() {}
|
|
218
|
+
expect( serializeError( namedFunction ) ).toBe( '[Function: namedFunction]' );
|
|
219
|
+
expect( serializeError( () => undefined ) ).toBe( '[Function: (anonymous)]' );
|
|
220
|
+
} );
|
|
221
|
+
|
|
222
|
+
it( 'serializes ArrayBuffers and views as type and size without dumping contents', () => {
|
|
223
|
+
expect( serializeError( new Uint16Array( [ 1, 2 ] ) ) ).toBe( 'Uint16Array(2)' );
|
|
224
|
+
expect( serializeError( Buffer.from( [ 1, 2 ] ) ) ).toBe( 'Buffer(2)' );
|
|
225
|
+
expect( serializeError( new DataView( new ArrayBuffer( 8 ) ) ) ).toBe( 'DataView(8)' );
|
|
226
|
+
expect( serializeError( new ArrayBuffer( 16 ) ) ).toBe( 'ArrayBuffer(16)' );
|
|
227
|
+
} );
|
|
228
|
+
|
|
229
|
+
it( 'redacts URL strings and URL instances with redactUrl', () => {
|
|
230
|
+
const stringUrl = 'https://user:password@example.com/models?model=gpt-5&token=secret#debug';
|
|
231
|
+
const url = new URL( 'ftp://user:password@example.com/file.txt?download=true#details' );
|
|
232
|
+
redactUrlMock.mockClear();
|
|
233
|
+
isUrlMock.mockClear();
|
|
234
|
+
isUrlMock.mockReturnValueOnce( true );
|
|
235
|
+
|
|
236
|
+
expect( serializeError( {
|
|
237
|
+
stringUrl,
|
|
238
|
+
url
|
|
239
|
+
} ) ).toEqual( {
|
|
240
|
+
stringUrl: '[Redacted URL]',
|
|
241
|
+
url: '[Redacted URL]'
|
|
242
|
+
} );
|
|
243
|
+
expect( isUrlMock ).toHaveBeenCalledOnce();
|
|
244
|
+
expect( isUrlMock ).toHaveBeenCalledWith( stringUrl );
|
|
245
|
+
expect( redactUrlMock ).toHaveBeenCalledTimes( 2 );
|
|
246
|
+
expect( redactUrlMock ).toHaveBeenNthCalledWith( 1, stringUrl );
|
|
247
|
+
expect( redactUrlMock ).toHaveBeenNthCalledWith( 2, url );
|
|
248
|
+
} );
|
|
249
|
+
|
|
250
|
+
it( 'truncates string values and property names', () => {
|
|
251
|
+
const longValue = 'v'.repeat( 16_385 );
|
|
252
|
+
const sharedKeyPrefix = 'k'.repeat( 256 );
|
|
253
|
+
|
|
254
|
+
expect( serializeError( {
|
|
255
|
+
longValue,
|
|
256
|
+
[`${sharedKeyPrefix}a`]: 'first',
|
|
257
|
+
[`${sharedKeyPrefix}b`]: 'second'
|
|
258
|
+
} ) ).toEqual( {
|
|
259
|
+
longValue: `${'v'.repeat( 16_384 )}... [1 more characters omitted]`,
|
|
260
|
+
[`${sharedKeyPrefix}... [1 more characters omitted]`]: 'first'
|
|
261
|
+
} );
|
|
262
|
+
} );
|
|
263
|
+
|
|
264
|
+
it( 'limits arrays, maps, and sets while recursively serializing their values', () => {
|
|
265
|
+
const exactArray = Array.from( { length: 50 }, ( _, index ) => index );
|
|
266
|
+
const longArray = Array.from( { length: 52 }, ( _, index ) => index );
|
|
267
|
+
const longMap = new Map( Array.from( { length: 51 }, ( _, index ) => [ `key-${index}`, BigInt( index ) ] ) );
|
|
268
|
+
const longSet = new Set( Array.from( { length: 52 }, ( _, index ) => BigInt( index ) ) );
|
|
269
|
+
|
|
270
|
+
expect( serializeError( exactArray ) ).toEqual( exactArray );
|
|
271
|
+
expect( serializeError( longArray ) ).toEqual( [ ...exactArray, '... [2 more items omitted]' ] );
|
|
272
|
+
|
|
273
|
+
const serializedMap = serializeError( longMap );
|
|
274
|
+
expect( serializedMap ).toHaveLength( 51 );
|
|
275
|
+
expect( serializedMap[0] ).toEqual( [ 'key-0', '0n' ] );
|
|
276
|
+
expect( serializedMap.at( -1 ) ).toBe( '... [1 more items omitted]' );
|
|
277
|
+
|
|
278
|
+
const serializedSet = serializeError( longSet );
|
|
279
|
+
expect( serializedSet ).toHaveLength( 51 );
|
|
280
|
+
expect( serializedSet[0] ).toBe( '0n' );
|
|
281
|
+
expect( serializedSet.at( -1 ) ).toBe( '... [2 more items omitted]' );
|
|
282
|
+
} );
|
|
283
|
+
|
|
284
|
+
it( 'globally limits object properties while prioritizing own properties', () => {
|
|
285
|
+
const exactProperties = Object.fromEntries( Array.from( { length: 50 }, ( _, index ) => [ `property-${index}`, index ] ) );
|
|
286
|
+
expect( serializeError( exactProperties ) ).toEqual( exactProperties );
|
|
287
|
+
|
|
288
|
+
const prototype = { inherited: 'omitted' };
|
|
289
|
+
const instance = Object.assign(
|
|
290
|
+
Object.create( prototype ),
|
|
291
|
+
Object.fromEntries( Array.from( { length: 50 }, ( _, index ) => [ `own-${index}`, index ] ) )
|
|
292
|
+
);
|
|
293
|
+
const serialized = serializeError( instance );
|
|
294
|
+
|
|
295
|
+
expect( Object.keys( serialized ) ).toHaveLength( 51 );
|
|
296
|
+
expect( serialized['own-49'] ).toBe( 49 );
|
|
297
|
+
expect( serialized ).not.toHaveProperty( 'inherited' );
|
|
298
|
+
expect( serialized['...'] ).toBe( '[Maximum Object Properties Reached]' );
|
|
299
|
+
} );
|
|
300
|
+
|
|
301
|
+
it( 'retains error identity when properties reach the object limit', () => {
|
|
302
|
+
class DetailedError extends Error {}
|
|
303
|
+
const error = new DetailedError( 'failed' );
|
|
304
|
+
Object.assign( error, Object.fromEntries( Array.from( { length: 50 }, ( _, index ) => [ `field-${index}`, index ] ) ) );
|
|
305
|
+
|
|
306
|
+
const serialized = serializeError( error, { dropKeys: [ 'stack' ] } );
|
|
307
|
+
|
|
308
|
+
expect( serialized.name ).toBe( 'DetailedError' );
|
|
309
|
+
expect( serialized.message ).toBe( 'failed' );
|
|
310
|
+
expect( serialized['...'] ).toBe( '[Maximum Object Properties Reached]' );
|
|
311
|
+
} );
|
|
312
|
+
|
|
313
|
+
it( 'recursively excludes sensitive and internal properties while retaining similarly named diagnostics', () => {
|
|
314
|
+
const value = {
|
|
315
|
+
failure: { internal: 'Temporal protobuf' },
|
|
316
|
+
originalFailure: { details: 'Nexus failure' },
|
|
317
|
+
$type: { parent: 'protobuf reflection graph' },
|
|
318
|
+
request: { body: 'private request' },
|
|
319
|
+
response: { body: 'private response' },
|
|
320
|
+
headers: { Authorization: 'Bearer secret' },
|
|
321
|
+
Authorization: 'Bearer secret',
|
|
322
|
+
RequestBodyValues: { prompt: 'private prompt' },
|
|
323
|
+
RESPONSE_HEADERS: { Cookie: 'private cookie' },
|
|
324
|
+
APIKey: 'private key',
|
|
325
|
+
AccessToken: 'private token',
|
|
326
|
+
PASSWORD: 'private password',
|
|
327
|
+
Credential: 'private credential',
|
|
328
|
+
keyboard: 'preserved',
|
|
329
|
+
tokenCount: 42,
|
|
330
|
+
passwordPolicy: 'preserved',
|
|
331
|
+
secretCount: 1,
|
|
332
|
+
nested: {
|
|
333
|
+
Cookie: 'private cookie',
|
|
334
|
+
code: 'EFAILED'
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
expect( serializeError( value ) ).toEqual( {
|
|
339
|
+
keyboard: 'preserved',
|
|
340
|
+
tokenCount: 42,
|
|
341
|
+
passwordPolicy: 'preserved',
|
|
342
|
+
secretCount: 1,
|
|
343
|
+
nested: { code: 'EFAILED' }
|
|
344
|
+
} );
|
|
345
|
+
} );
|
|
346
|
+
|
|
347
|
+
it( 'includes stacks by default and supports dropping them recursively', () => {
|
|
348
|
+
const cause = new Error( 'database unavailable' );
|
|
349
|
+
const error = new Error( 'service failed', { cause } );
|
|
350
|
+
|
|
351
|
+
const withStacks = serializeError( error );
|
|
352
|
+
const withoutStacks = serializeError( error, { dropKeys: [ 'stack' ] } );
|
|
353
|
+
|
|
354
|
+
expect( withStacks.stack ).toContain( 'Error: service failed' );
|
|
355
|
+
expect( withStacks.cause.stack ).toContain( 'Error: database unavailable' );
|
|
356
|
+
expect( withoutStacks ).not.toHaveProperty( 'stack' );
|
|
357
|
+
expect( withoutStacks.cause ).not.toHaveProperty( 'stack' );
|
|
358
|
+
} );
|
|
359
|
+
|
|
360
|
+
it( 'marks circular references but serializes shared sibling values independently', () => {
|
|
361
|
+
const shared = { code: 'ESHARED' };
|
|
362
|
+
const objectCycle = {};
|
|
363
|
+
objectCycle.self = objectCycle;
|
|
364
|
+
const arrayCycle = [];
|
|
365
|
+
arrayCycle.push( arrayCycle );
|
|
366
|
+
const mapCycle = new Map();
|
|
367
|
+
mapCycle.set( 'self', mapCycle );
|
|
368
|
+
const setCycle = new Set();
|
|
369
|
+
setCycle.add( setCycle );
|
|
370
|
+
|
|
371
|
+
expect( serializeError( {
|
|
372
|
+
objectCycle,
|
|
373
|
+
arrayCycle,
|
|
374
|
+
mapCycle,
|
|
375
|
+
setCycle,
|
|
376
|
+
first: shared,
|
|
377
|
+
second: shared
|
|
378
|
+
} ) ).toEqual( {
|
|
379
|
+
objectCycle: { self: '[Circular Reference]' },
|
|
380
|
+
arrayCycle: [ '[Circular Reference]' ],
|
|
381
|
+
mapCycle: [ [ 'self', '[Circular Reference]' ] ],
|
|
382
|
+
setCycle: [ '[Circular Reference]' ],
|
|
383
|
+
first: { code: 'ESHARED' },
|
|
384
|
+
second: { code: 'ESHARED' }
|
|
385
|
+
} );
|
|
386
|
+
} );
|
|
387
|
+
|
|
388
|
+
it( 'marks values beyond the maximum recursive depth', () => {
|
|
389
|
+
const createNestedValue = depth => depth === 0 ? {} : { child: createNestedValue( depth - 1 ) };
|
|
390
|
+
const root = createNestedValue( 10 );
|
|
391
|
+
|
|
392
|
+
expect(
|
|
393
|
+
Array.from( { length: 9 } ).reduce( value => value.child, serializeError( root ) ).child
|
|
394
|
+
).toBe( '[Maximum Depth Reached]' );
|
|
395
|
+
} );
|
|
396
|
+
|
|
397
|
+
it( 'marks values after the total serialization limit is reached', () => {
|
|
398
|
+
const value = Array.from(
|
|
399
|
+
{ length: 50 },
|
|
400
|
+
() => Array.from( { length: 50 }, ( _, index ) => index )
|
|
401
|
+
);
|
|
402
|
+
|
|
403
|
+
expect( JSON.stringify( serializeError( value ) ) ).toContain( '[Serialization Limit Reached]' );
|
|
404
|
+
} );
|
|
405
|
+
|
|
406
|
+
it( 'returns an unserializable marker for exotic values that throw during reflection', () => {
|
|
407
|
+
const { proxy, revoke } = Proxy.revocable( {}, {} );
|
|
408
|
+
revoke();
|
|
409
|
+
|
|
410
|
+
expect( serializeError( proxy ) ).toBe( '[Unserializable]' );
|
|
411
|
+
expect( serializeError( { nested: proxy } ) ).toEqual( { nested: '[Unserializable]' } );
|
|
412
|
+
} );
|
|
413
|
+
} );
|
package/src/helpers/errors.js
CHANGED
|
@@ -1,16 +1,25 @@
|
|
|
1
|
-
import { ApplicationFailure } from '@temporalio/common';
|
|
2
|
-
|
|
3
1
|
/**
|
|
4
|
-
*
|
|
5
|
-
* @param {
|
|
6
|
-
* @param {
|
|
7
|
-
* @returns {
|
|
2
|
+
* Checks whether a value inherits from a constructor matching any supplied class name.
|
|
3
|
+
* @param {*} obj - Value whose prototype chain to inspect
|
|
4
|
+
* @param {string[]} classNames - Constructor names to match
|
|
5
|
+
* @returns {boolean} Whether a matching constructor exists
|
|
8
6
|
*/
|
|
9
|
-
export const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
7
|
+
export const inheritsFromAnyNamedType = ( obj, classNames ) => {
|
|
8
|
+
if ( classNames.length === 0 ) {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
if ( obj === null || ( typeof obj !== 'object' && typeof obj !== 'function' ) ) {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const prototype = Object.getPrototypeOf( obj );
|
|
16
|
+
if ( !prototype ) {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const constructor = Object.getOwnPropertyDescriptor( prototype, 'constructor' )?.value;
|
|
21
|
+
if ( constructor?.name && classNames.includes( constructor.name ) ) {
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
return inheritsFromAnyNamedType( prototype, classNames );
|
|
25
|
+
};
|