173 lines
6.4 KiB
JavaScript
173 lines
6.4 KiB
JavaScript
|
import Constants from 'expo-constants';
|
||
|
import prettyFormat from 'pretty-format';
|
||
|
import parseErrorStack from 'react-native/Libraries/Core/Devtools/parseErrorStack';
|
||
|
import symbolicateStackTrace from 'react-native/Libraries/Core/Devtools/symbolicateStackTrace';
|
||
|
import ReactNodeFormatter from './format/ReactNodeFormatter';
|
||
|
export const EXPO_CONSOLE_METHOD_NAME = '__expoConsoleLog';
|
||
|
async function serializeLogDataAsync(data, level) {
|
||
|
let serializedValues;
|
||
|
let includesStack = false;
|
||
|
if (_stackTraceLogsSupported()) {
|
||
|
if (_isUnhandledPromiseRejection(data, level)) {
|
||
|
const rawStack = data[0];
|
||
|
const syntheticError = { stack: rawStack };
|
||
|
const stack = await _symbolicateErrorAsync(syntheticError);
|
||
|
if (!stack.length) {
|
||
|
serializedValues = _stringifyLogData(data);
|
||
|
}
|
||
|
else {
|
||
|
// NOTE: This doesn't handle error messages with newlines
|
||
|
const errorMessage = rawStack.split('\n')[1];
|
||
|
serializedValues = [
|
||
|
{
|
||
|
message: `[Unhandled promise rejection: ${errorMessage}]`,
|
||
|
stack: _formatStack(stack),
|
||
|
},
|
||
|
];
|
||
|
includesStack = true;
|
||
|
}
|
||
|
}
|
||
|
else if (data.length === 1 && data[0] instanceof Error) {
|
||
|
// When there's only one argument to the log function and that argument is an error, we
|
||
|
// include the error's stack. If there's more than one argument then we don't include the
|
||
|
// stack because it's not easy to display nicely in our current UI.
|
||
|
const serializedError = await _serializeErrorAsync(data[0]);
|
||
|
serializedValues = [serializedError];
|
||
|
includesStack = serializedError.hasOwnProperty('stack');
|
||
|
}
|
||
|
else if (level === 'warn' || level === 'error') {
|
||
|
// For console.warn and console.error it is usually useful to know the stack that leads to the
|
||
|
// warning or error, so we provide this information to help out with debugging
|
||
|
const error = _captureConsoleStackTrace();
|
||
|
// ["hello", "world"] becomes "hello, world"
|
||
|
const errorMessage = _stringifyLogData(data).join(', ');
|
||
|
const serializedError = await _serializeErrorAsync(error, errorMessage);
|
||
|
serializedValues = [serializedError];
|
||
|
includesStack = serializedError.hasOwnProperty('stack');
|
||
|
}
|
||
|
else {
|
||
|
serializedValues = _stringifyLogData(data);
|
||
|
}
|
||
|
}
|
||
|
else {
|
||
|
serializedValues = _stringifyLogData(data);
|
||
|
}
|
||
|
return {
|
||
|
body: [...serializedValues],
|
||
|
includesStack,
|
||
|
};
|
||
|
}
|
||
|
function _stringifyLogData(data) {
|
||
|
return data.map(item => {
|
||
|
// define the max length for log msg to be first 10000 characters
|
||
|
const LOG_MESSAGE_MAX_LENGTH = 10000;
|
||
|
const result = typeof item === 'string' ? item : prettyFormat(item, { plugins: [ReactNodeFormatter] });
|
||
|
// check the size of string returned
|
||
|
if (result.length > LOG_MESSAGE_MAX_LENGTH) {
|
||
|
let truncatedResult = result.substring(0, LOG_MESSAGE_MAX_LENGTH);
|
||
|
// truncate the result string to the max length
|
||
|
truncatedResult += `...(truncated to the first ${LOG_MESSAGE_MAX_LENGTH} characters)`;
|
||
|
return truncatedResult;
|
||
|
}
|
||
|
else {
|
||
|
return result;
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
async function _serializeErrorAsync(error, message) {
|
||
|
if (message == null) {
|
||
|
message = error.message;
|
||
|
}
|
||
|
if (!error.stack || !error.stack.length) {
|
||
|
return prettyFormat(error);
|
||
|
}
|
||
|
const stack = await _symbolicateErrorAsync(error);
|
||
|
const formattedStack = _formatStack(stack);
|
||
|
return { message, stack: formattedStack };
|
||
|
}
|
||
|
async function _symbolicateErrorAsync(error) {
|
||
|
const parsedStack = parseErrorStack(error);
|
||
|
let symbolicatedStack;
|
||
|
try {
|
||
|
// @ts-ignore: symbolicateStackTrace has different real/Flow declaration
|
||
|
// than the one in DefinitelyTyped.
|
||
|
symbolicatedStack = (await symbolicateStackTrace(parsedStack))?.stack ?? null;
|
||
|
}
|
||
|
catch (error) {
|
||
|
return parsedStack;
|
||
|
}
|
||
|
// In this context an unsymbolicated stack is better than no stack
|
||
|
if (!symbolicatedStack) {
|
||
|
return parsedStack;
|
||
|
}
|
||
|
// Clean the stack trace
|
||
|
return symbolicatedStack.map(_removeProjectRoot);
|
||
|
}
|
||
|
function _formatStack(stack) {
|
||
|
return stack
|
||
|
.map(frame => {
|
||
|
let line = `${frame.file}:${frame.lineNumber}`;
|
||
|
if (frame.column != null) {
|
||
|
line += `:${frame.column}`;
|
||
|
}
|
||
|
line += ` in ${frame.methodName}`;
|
||
|
return line;
|
||
|
})
|
||
|
.join('\n');
|
||
|
}
|
||
|
function _removeProjectRoot(frame) {
|
||
|
let filename = frame.file;
|
||
|
if (filename == null) {
|
||
|
return frame;
|
||
|
}
|
||
|
const projectRoot = _getProjectRoot();
|
||
|
if (projectRoot == null) {
|
||
|
return frame;
|
||
|
}
|
||
|
if (filename.startsWith(projectRoot)) {
|
||
|
filename = filename.substring(projectRoot.length);
|
||
|
if (filename[0] === '/' || filename[0] === '\\') {
|
||
|
filename = filename.substring(1);
|
||
|
}
|
||
|
frame.file = filename;
|
||
|
}
|
||
|
return frame;
|
||
|
}
|
||
|
/**
|
||
|
* Returns whether the development server that served this project supports logs with a stack trace.
|
||
|
* Specifically, the version of Expo CLI that includes `projectRoot` in the manifest also accepts
|
||
|
* payloads of the form:
|
||
|
*
|
||
|
* {
|
||
|
* includesStack: boolean, body: [{ message: string, stack: string }],
|
||
|
* }
|
||
|
*/
|
||
|
function _stackTraceLogsSupported() {
|
||
|
return !!(__DEV__ && _getProjectRoot());
|
||
|
}
|
||
|
function _isUnhandledPromiseRejection(data, level) {
|
||
|
return (level === 'warn' &&
|
||
|
typeof data[0] === 'string' &&
|
||
|
/^Possible Unhandled Promise Rejection/.test(data[0]));
|
||
|
}
|
||
|
function _captureConsoleStackTrace() {
|
||
|
try {
|
||
|
throw new Error();
|
||
|
}
|
||
|
catch (error) {
|
||
|
let stackLines = error.stack.split('\n');
|
||
|
const consoleMethodIndex = stackLines.findIndex(frame => frame.includes(EXPO_CONSOLE_METHOD_NAME));
|
||
|
if (consoleMethodIndex !== -1) {
|
||
|
stackLines = stackLines.slice(consoleMethodIndex + 1);
|
||
|
error.stack = stackLines.join('\n');
|
||
|
}
|
||
|
return error;
|
||
|
}
|
||
|
}
|
||
|
function _getProjectRoot() {
|
||
|
return Constants.manifest?.developer?.projectRoot ?? null;
|
||
|
}
|
||
|
export default {
|
||
|
serializeLogDataAsync,
|
||
|
};
|
||
|
//# sourceMappingURL=LogSerialization.js.map
|