1*26ad19fcSEvan Bacon/** 2*26ad19fcSEvan Bacon * Copyright (c) 650 Industries. 3*26ad19fcSEvan Bacon * Copyright (c) Meta Platforms, Inc. and affiliates. 4*26ad19fcSEvan Bacon * 5*26ad19fcSEvan Bacon * This source code is licensed under the MIT license found in the 6*26ad19fcSEvan Bacon * LICENSE file in the root directory of this source tree. 7*26ad19fcSEvan Bacon */ 8*26ad19fcSEvan Bacon 9*26ad19fcSEvan Bacon/** 10*26ad19fcSEvan Bacon * Tries to stringify with JSON.stringify and toString, but catches exceptions 11*26ad19fcSEvan Bacon * (e.g. from circular objects) and always returns a string and never throws. 12*26ad19fcSEvan Bacon */ 13*26ad19fcSEvan Baconexport function createStringifySafeWithLimits(limits: { 14*26ad19fcSEvan Bacon maxDepth?: number; 15*26ad19fcSEvan Bacon maxStringLimit?: number; 16*26ad19fcSEvan Bacon maxArrayLimit?: number; 17*26ad19fcSEvan Bacon maxObjectKeysLimit?: number; 18*26ad19fcSEvan Bacon}): (foo: any) => string { 19*26ad19fcSEvan Bacon const { 20*26ad19fcSEvan Bacon maxDepth = Number.POSITIVE_INFINITY, 21*26ad19fcSEvan Bacon maxStringLimit = Number.POSITIVE_INFINITY, 22*26ad19fcSEvan Bacon maxArrayLimit = Number.POSITIVE_INFINITY, 23*26ad19fcSEvan Bacon maxObjectKeysLimit = Number.POSITIVE_INFINITY, 24*26ad19fcSEvan Bacon } = limits; 25*26ad19fcSEvan Bacon const stack: any[] = []; 26*26ad19fcSEvan Bacon function replacer(this: unknown, _key: string, value: any): any { 27*26ad19fcSEvan Bacon while (stack.length && this !== stack[0]) { 28*26ad19fcSEvan Bacon stack.shift(); 29*26ad19fcSEvan Bacon } 30*26ad19fcSEvan Bacon 31*26ad19fcSEvan Bacon if (typeof value === 'string') { 32*26ad19fcSEvan Bacon const truncatedString = '...(truncated)...'; 33*26ad19fcSEvan Bacon if (value.length > maxStringLimit + truncatedString.length) { 34*26ad19fcSEvan Bacon return value.substring(0, maxStringLimit) + truncatedString; 35*26ad19fcSEvan Bacon } 36*26ad19fcSEvan Bacon return value; 37*26ad19fcSEvan Bacon } 38*26ad19fcSEvan Bacon if (typeof value !== 'object' || value === null) { 39*26ad19fcSEvan Bacon return value; 40*26ad19fcSEvan Bacon } 41*26ad19fcSEvan Bacon 42*26ad19fcSEvan Bacon let retval = value; 43*26ad19fcSEvan Bacon if (Array.isArray(value)) { 44*26ad19fcSEvan Bacon if (stack.length >= maxDepth) { 45*26ad19fcSEvan Bacon retval = `[ ... array with ${value.length} values ... ]`; 46*26ad19fcSEvan Bacon } else if (value.length > maxArrayLimit) { 47*26ad19fcSEvan Bacon retval = value 48*26ad19fcSEvan Bacon .slice(0, maxArrayLimit) 49*26ad19fcSEvan Bacon .concat([`... extra ${value.length - maxArrayLimit} values truncated ...`]); 50*26ad19fcSEvan Bacon } 51*26ad19fcSEvan Bacon } else { 52*26ad19fcSEvan Bacon // Add refinement after Array.isArray call. 53*26ad19fcSEvan Bacon if (typeof value !== 'object') { 54*26ad19fcSEvan Bacon throw new Error('This was already found earlier'); 55*26ad19fcSEvan Bacon } 56*26ad19fcSEvan Bacon const keys = Object.keys(value); 57*26ad19fcSEvan Bacon if (stack.length >= maxDepth) { 58*26ad19fcSEvan Bacon retval = `{ ... object with ${keys.length} keys ... }`; 59*26ad19fcSEvan Bacon } else if (keys.length > maxObjectKeysLimit) { 60*26ad19fcSEvan Bacon // Return a sample of the keys. 61*26ad19fcSEvan Bacon retval = {}; 62*26ad19fcSEvan Bacon for (const k of keys.slice(0, maxObjectKeysLimit)) { 63*26ad19fcSEvan Bacon retval[k] = value[k]; 64*26ad19fcSEvan Bacon } 65*26ad19fcSEvan Bacon const truncatedKey = '...(truncated keys)...'; 66*26ad19fcSEvan Bacon retval[truncatedKey] = keys.length - maxObjectKeysLimit; 67*26ad19fcSEvan Bacon } 68*26ad19fcSEvan Bacon } 69*26ad19fcSEvan Bacon stack.unshift(retval); 70*26ad19fcSEvan Bacon return retval; 71*26ad19fcSEvan Bacon } 72*26ad19fcSEvan Bacon 73*26ad19fcSEvan Bacon return function stringifySafe(arg: any): string { 74*26ad19fcSEvan Bacon if (arg === undefined) { 75*26ad19fcSEvan Bacon return 'undefined'; 76*26ad19fcSEvan Bacon } else if (arg === null) { 77*26ad19fcSEvan Bacon return 'null'; 78*26ad19fcSEvan Bacon } else if (typeof arg === 'function') { 79*26ad19fcSEvan Bacon try { 80*26ad19fcSEvan Bacon return arg.toString(); 81*26ad19fcSEvan Bacon } catch { 82*26ad19fcSEvan Bacon return '[function unknown]'; 83*26ad19fcSEvan Bacon } 84*26ad19fcSEvan Bacon } else if (arg instanceof Error) { 85*26ad19fcSEvan Bacon return arg.name + ': ' + arg.message; 86*26ad19fcSEvan Bacon } else { 87*26ad19fcSEvan Bacon // Perform a try catch, just in case the object has a circular 88*26ad19fcSEvan Bacon // reference or stringify throws for some other reason. 89*26ad19fcSEvan Bacon try { 90*26ad19fcSEvan Bacon const ret = JSON.stringify(arg, replacer); 91*26ad19fcSEvan Bacon if (ret === undefined) { 92*26ad19fcSEvan Bacon return '["' + typeof arg + '" failed to stringify]'; 93*26ad19fcSEvan Bacon } 94*26ad19fcSEvan Bacon return ret; 95*26ad19fcSEvan Bacon } catch { 96*26ad19fcSEvan Bacon if (typeof arg.toString === 'function') { 97*26ad19fcSEvan Bacon try { 98*26ad19fcSEvan Bacon // $FlowFixMe[incompatible-use] : toString shouldn't take any arguments in general. 99*26ad19fcSEvan Bacon return arg.toString(); 100*26ad19fcSEvan Bacon } catch {} 101*26ad19fcSEvan Bacon } 102*26ad19fcSEvan Bacon } 103*26ad19fcSEvan Bacon } 104*26ad19fcSEvan Bacon return '["' + typeof arg + '" failed to stringify]'; 105*26ad19fcSEvan Bacon }; 106*26ad19fcSEvan Bacon} 107*26ad19fcSEvan Bacon 108*26ad19fcSEvan Baconconst stringifySafe = createStringifySafeWithLimits({ 109*26ad19fcSEvan Bacon maxDepth: 10, 110*26ad19fcSEvan Bacon maxStringLimit: 100, 111*26ad19fcSEvan Bacon maxArrayLimit: 50, 112*26ad19fcSEvan Bacon maxObjectKeysLimit: 50, 113*26ad19fcSEvan Bacon}); 114*26ad19fcSEvan Bacon 115*26ad19fcSEvan Baconexport default stringifySafe; 116