accounts-frontend/packages/app/services/logger/abbreviate.ts

211 lines
4.5 KiB
TypeScript

const STRING_MAX_LENGTH = 128 * 1024;
type Filter = (key: string | undefined, value: Record<string, any>) => 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<string, any>,
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<string, any> | Array<any> = 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,
});
}