const STRING_MAX_LENGTH = 128 * 1024; type Filter = (key: string | undefined, value: Record) => any; interface State { sizeLeft: number; } /** * Create a copy of any object without non-serializable elements to make result safe for JSON.stringify(). * Guaranteed to never throw. * * @param {any} obj Any data structure * @param {object} options * @param {Function=} options.filter - callback that is called on every object's key with (key,value) and should return * value to use (may return undefined to remove unwanted keys). See nodeFilter and browserFilter. * @param {number=} options.depth - maximum recursion depth. Elements deeper than that are stringified with util.inspect() * @param {number=} options.maxSize - roughly maximum allowed size of data after JSON serialisation (but it's not guaranteed * that it won't exceed the limit) * * @see https://github.com/ftlabs/js-abbreviate * * @returns {object} */ function abbreviate( obj: Record, options: { filter?: Filter; depth?: number; maxSize?: number; } = {}, ): string { const filter: Filter = options.filter || ((key, value) => value); const maxDepth: number = options.depth || 10; const maxSize: number = options.maxSize || 1 * 1024 * 1024; return abbreviateRecursive(undefined, obj, filter, { sizeLeft: maxSize }, maxDepth); } function limitStringLength(str: string): string { if (str.length > STRING_MAX_LENGTH) { return `${str.substring(0, STRING_MAX_LENGTH / 2)} … ${str.substring(str.length - STRING_MAX_LENGTH / 2)}`; } return str; } function abbreviateRecursive(key: string | undefined, obj: any, filter: Filter, state: State, maxDepth: number): any { // TODO: return type if (state.sizeLeft < 0) { return '**skipped**'; } state.sizeLeft -= 5; // rough approximation of JSON overhead obj = filter(key, obj); try { switch (typeof obj) { case 'object': { if (obj === null) { return null; } if (maxDepth < 0) { break; // fall back to stringification } const newobj: Record | Array = Array.isArray(obj) ? [] : {}; for (const i in obj) { if (!obj.hasOwnProperty(i)) { continue; } // @ts-ignore newobj[i] = abbreviateRecursive(i, obj[i], filter, state, maxDepth - 1); if (state.sizeLeft < 0) { break; } } return newobj; } case 'string': obj = limitStringLength(obj); state.sizeLeft -= obj.length; return obj; case 'number': case 'boolean': case 'undefined': default: return obj; } } catch (err) { /* fall back to inspect*/ } try { obj = limitStringLength(`${obj}`); state.sizeLeft -= obj.length; return obj; } catch (err) { return '**non-serializable**'; } } function commonFilter(key: any, val: any): any { if (typeof val === 'function') { return undefined; } if (val instanceof Date) { return `**Date** ${val}`; } if (val instanceof Error) { const err = { // These properties are implemented as magical getters and don't show up in for in stack: val.stack, message: val.message, name: val.name, }; for (const i in val) { if (!val.hasOwnProperty(i)) { continue; } // @ts-ignore err[i] = val[i]; } return err; } return val; } function nodeFilter(key: any, val: any): any { // domain objects are huge and have circular references if (key === 'domain' && typeof val === 'object' && val._events) { return '**domain ignored**'; } if (key === 'domainEmitter') { return '**domainEmitter ignored**'; } if (val === global) { return '**global**'; } return commonFilter(key, val); } function browserFilter(key: any, val: any): any { if (val === window) { return '**window**'; } if (val === document) { return '**document**'; } if (val instanceof HTMLElement) { const { outerHTML } = val; if (typeof outerHTML !== 'undefined') { return `**HTMLElement** ${outerHTML}`; } } return commonFilter(key, val); } export { abbreviate, nodeFilter, browserFilter }; export default function (obj: any): string { return abbreviate(obj, { filter: browserFilter, }); }