@web/browser-logs
Version:
Capture browser logs for logging in NodeJS
136 lines (120 loc) • 4.84 kB
text/typescript
import { ParseStackTraceOptions, parseStackTrace } from './parseStackTrace.js';
const KEY_WTR_TYPE = '__WTR_TYPE__';
const KEY_CONSTRUCTOR_NAME = '__WTR_CONSTRUCTOR_NAME__';
const ASYNC_DESERIALIZE_WRAPPER = Symbol('ASYNC_DESERIALIZE_WRAPPER');
const BOUND_NAME_FUNCTION_REGEX = /^bound\s+/;
function createReviver(promises: Promise<unknown>[], options?: DeserializeOptions) {
const undefinedPropsPerObject = new Map();
return function reviver(this: any, key: string, value: any) {
if (value == null || typeof value !== 'object') {
return value;
}
const undefinedKeysForObject = undefinedPropsPerObject.get(value);
if (undefinedKeysForObject) {
for (const undefinedKey of undefinedKeysForObject) {
value[undefinedKey] = undefined;
}
}
if (Array.isArray(value)) {
return value;
}
/**
* Revive special serialized values, such as functions and regexp
*/
if (hasOwnProperty.call(value, KEY_WTR_TYPE)) {
switch (value[KEY_WTR_TYPE]) {
case 'undefined':
{
let keys = undefinedPropsPerObject.get(this);
if (!keys) {
keys = [];
undefinedPropsPerObject.set(this, keys);
}
keys.push(key);
}
return;
case 'Function':
if (value.name.includes('-')) {
const { name } = value;
// eslint-disable-next-line
const placeholder = { [name]: () => {} };
return placeholder[name];
}
// Create a fake function with the same name. We don't log the function implementation.
return new Function(
`return function ${value.name.replace(
BOUND_NAME_FUNCTION_REGEX,
'',
)}() { /* implementation hidden */ }`,
)();
case 'RegExp':
// Create a new RegExp using the same parameters
return new RegExp(value.source, value.flags);
case 'Error': {
let errorMsg = `${value.name}: ${value.message}`;
if (value.stack) {
const parsePromise = parseStackTrace(value.message, value.stack, options)
.then(parsedStack => {
if (parsedStack) {
// set the async deserialized error msg
errorMsg = `${errorMsg}\n${parsedStack}`;
if (this[key][ASYNC_DESERIALIZE_WRAPPER]) {
// replace the returned wrapper with the async value
// this only works when the error appears somewhere in an object
// or array, ex. deserialize({ myError: new Error('...') }) not when the
// top level object is the error: deserialize(new Error('...')) this case
// is handled in the serialize function
this[key] = this[key].value();
}
}
})
.catch(error => {
console.error(error);
});
promises.push(parsePromise);
}
// deserializing an error is async, return a wrapper that is unpacked later
return { [ASYNC_DESERIALIZE_WRAPPER]: true, value: () => errorMsg };
}
case 'Promise':
// Create a fake new Promise. Just to show that its a Promise.
return `Promise { }`;
default:
throw new Error(`Unknown serialized type: ${value[KEY_WTR_TYPE]}`);
}
}
/**
* Objects in the browser are serialized to a simple object. We preserve the
* constructor name and assign a fake prototpe to it here so that the name
* appears in the logs.
*/
if (hasOwnProperty.call(value, KEY_CONSTRUCTOR_NAME)) {
const constructorName = value[KEY_CONSTRUCTOR_NAME];
const ConstructorFunction = new Function(`return function ${constructorName}(){}`)();
Object.setPrototypeOf(value, new ConstructorFunction());
delete value[KEY_CONSTRUCTOR_NAME];
return value;
}
return value;
};
}
const { hasOwnProperty } = Object.prototype;
interface DeserializeOptions extends ParseStackTraceOptions {}
export async function deserialize(value: string, options?: DeserializeOptions) {
try {
const promises: Promise<unknown>[] = [];
const parsed = JSON.parse(value, createReviver(promises, options));
// wait for any async work to finish
await Promise.all(promises);
if (parsed != null && parsed[ASYNC_DESERIALIZE_WRAPPER]) {
// if deserialization of the top level object was async,
// return the wrapped value which was provided async
return parsed.value();
}
return parsed;
} catch (error) {
console.error('Error while deserializing browser logs.');
console.error(error);
return null;
}
}