1- import { isPrimitive , isSyntheticEvent } from './is' ;
1+ import { Primitive } from '@sentry/types' ;
2+
3+ import { isError , isEvent , isNaN , isSyntheticEvent } from './is' ;
24import { memoBuilder , MemoFunc } from './memo' ;
35import { convertToPlainObject } from './object' ;
46import { getFunctionName } from './stacktrace' ;
57
6- type UnknownMaybeWithToJson = unknown & { toJSON ?: ( ) => string } ;
78type Prototype = { constructor : ( ...args : unknown [ ] ) => unknown } ;
9+ // This is a hack to placate TS, relying on the fact that technically, arrays are objects with integer keys. Normally we
10+ // think of those keys as actual numbers, but `arr['0']` turns out to work just as well as `arr[0]`, and doing it this
11+ // way lets us use a single type in the places where behave as if we are only dealing with objects, even if some of them
12+ // might be arrays.
13+ type ObjOrArray < T > = { [ key : string ] : T } ;
814
915/**
1016 * Recursively normalizes the given object.
@@ -28,7 +34,7 @@ type Prototype = { constructor: (...args: unknown[]) => unknown };
2834export function normalize ( input : unknown , depth : number = + Infinity , maxProperties : number = + Infinity ) : any {
2935 try {
3036 // since we're at the outermost level, there is no key
31- return walk ( '' , input as UnknownMaybeWithToJson , depth , maxProperties ) ;
37+ return visit ( '' , input , depth , maxProperties ) ;
3238 } catch ( _oO ) {
3339 return '**non-serializable**' ;
3440 }
@@ -52,80 +58,95 @@ export function normalizeToSize<T>(
5258}
5359
5460/**
55- * Walks an object to perform a normalization on it
61+ * Visits a node to perform normalization on it
5662 *
57- * @param key of object that's walked in current iteration
58- * @param value object to be walked
59- * @param depth Optional number indicating how deep should walking be performed
60- * @param maxProperties Optional maximum number of properties/elements included in any single object/array
63+ * @param key The key corresponding to the given node
64+ * @param value The node to be visited
65+ * @param depth Optional number indicating the maximum recursion depth
66+ * @param maxProperties Optional maximum number of properties/elements included in any single object/array
6167 * @param memo Optional Memo class handling decycling
6268 */
63- export function walk (
69+ function visit (
6470 key : string ,
65- value : UnknownMaybeWithToJson ,
71+ value : unknown ,
6672 depth : number = + Infinity ,
6773 maxProperties : number = + Infinity ,
6874 memo : MemoFunc = memoBuilder ( ) ,
69- ) : unknown {
75+ ) : Primitive | ObjOrArray < unknown > {
7076 const [ memoize , unmemoize ] = memo ;
7177
72- // If we reach the maximum depth, serialize whatever is left
73- if ( depth === 0 ) {
74- return stringifyValue ( key , value ) ;
78+ // If the value has a `toJSON` method, see if we can bail and let it do the work
79+ const valueWithToJSON = value as unknown & { toJSON ?: ( ) => Primitive | ObjOrArray < unknown > } ;
80+ if ( valueWithToJSON && typeof valueWithToJSON . toJSON === 'function' ) {
81+ try {
82+ return valueWithToJSON . toJSON ( ) ;
83+ } catch ( err ) {
84+ // pass (The built-in `toJSON` failed, but we can still try to do it ourselves)
85+ }
7586 }
7687
77- // If value implements `toJSON` method, call it and return early
78- if ( value !== null && value !== undefined && typeof value . toJSON === 'function' ) {
79- return value . toJSON ( ) ;
88+ // Get the simple cases out of the way first
89+ if ( value === null || ( [ 'number' , 'boolean' , 'string' ] . includes ( typeof value ) && ! isNaN ( value ) ) ) {
90+ return value as Primitive ;
8091 }
8192
82- // `makeSerializable` provides a string representation of certain non-serializable values. For all others, it's a
83- // pass-through. If what comes back is a primitive (either because it's been stringified or because it was primitive
84- // all along), we're done.
85- const serializable = stringifyValue ( key , value ) ;
86- if ( isPrimitive ( serializable ) ) {
87- return serializable ;
88- }
93+ const stringified = stringifyValue ( key , value ) ;
8994
90- // Create source that we will use for the next iteration. It will either be an objectified error object (`Error` type
91- // with extracted key:value pairs) or the input itself.
92- const source = convertToPlainObject ( value ) ;
95+ // Anything we could potentially dig into more (objects or arrays) will have come back as `"[object XXXX]"`.
96+ // Everything else will have already been serialized, so if we don't see that pattern, we're done.
97+ if ( ! stringified . startsWith ( '[object ' ) ) {
98+ return stringified ;
99+ }
93100
94- // Create an accumulator that will act as a parent for all future itterations of that branch
95- const acc : { [ key : string ] : any } = Array . isArray ( value ) ? [ ] : { } ;
101+ // We're also done if we've reached the max depth
102+ if ( depth === 0 ) {
103+ // At this point we know `serialized` is a string of the form `"[object XXXX]"`. Clean it up so it's just `"[XXXX]"`.
104+ return stringified . replace ( 'object ' , '' ) ;
105+ }
96106
97- // If we already walked that branch, bail out, as it's circular reference
107+ // If we've already visited this branch, bail out, as it's circular reference. If not, note that we're seeing it now.
98108 if ( memoize ( value ) ) {
99109 return '[Circular ~]' ;
100110 }
101111
102- let propertyCount = 0 ;
103- // Walk all keys of the source
104- for ( const innerKey in source ) {
112+ // At this point we know we either have an object or an array, we haven't seen it before, and we're going to recurse
113+ // because we haven't yet reached the max depth. Create an accumulator to hold the results of visiting each
114+ // property/entry, and keep track of the number of items we add to it.
115+ const normalized = ( Array . isArray ( value ) ? [ ] : { } ) as ObjOrArray < unknown > ;
116+ let numAdded = 0 ;
117+
118+ // Before we begin, convert`Error` and`Event` instances into plain objects, since some of each of their relevant
119+ // properties are non-enumerable and otherwise would get missed.
120+ const visitable = ( isError ( value ) || isEvent ( value ) ? convertToPlainObject ( value ) : value ) as ObjOrArray < unknown > ;
121+
122+ for ( const visitKey in visitable ) {
105123 // Avoid iterating over fields in the prototype if they've somehow been exposed to enumeration.
106- if ( ! Object . prototype . hasOwnProperty . call ( source , innerKey ) ) {
124+ if ( ! Object . prototype . hasOwnProperty . call ( visitable , visitKey ) ) {
107125 continue ;
108126 }
109127
110- if ( propertyCount >= maxProperties ) {
111- acc [ innerKey ] = '[MaxProperties ~]' ;
128+ if ( numAdded >= maxProperties ) {
129+ normalized [ visitKey ] = '[MaxProperties ~]' ;
112130 break ;
113131 }
114132
115- propertyCount += 1 ;
133+ // Recursively visit all the child nodes
134+ const visitValue = visitable [ visitKey ] ;
135+ normalized [ visitKey ] = visit ( visitKey , visitValue , depth - 1 , maxProperties , memo ) ;
116136
117- // Recursively walk through all the child nodes
118- const innerValue = source [ innerKey ] as UnknownMaybeWithToJson ;
119- acc [ innerKey ] = walk ( innerKey , innerValue , depth - 1 , maxProperties , memo ) ;
137+ numAdded += 1 ;
120138 }
121139
122- // Once walked through all the branches, remove the parent from memo storage
140+ // Once we've visited all the branches, remove the parent from memo storage
123141 unmemoize ( value ) ;
124142
125143 // Return accumulated values
126- return acc ;
144+ return normalized ;
127145}
128146
147+ // TODO remove this in v7 (this means the method will no longer be exported, under any name)
148+ export { visit as walk } ;
149+
129150/**
130151 * Stringify the given value. Handles various known special values and types.
131152 *
0 commit comments