You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

176 lines
6.6 KiB

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) {
// @ts-ignore: parseErrorStack accepts nullable string after RN 0.64 but @types/react-native does not updated yet.
const parsedStack = parseErrorStack(error?.stack);
let symbolicatedStack;
try {
// @ts-ignore: symbolicateStackTrace has different real/Flow declaration
// than the one in DefinitelyTyped.
symbolicatedStack = (await symbolicateStackTrace(parsedStack))?.stack ?? null;
}
catch {
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 ??
Constants.manifest2?.extra?.expoGo?.developer?.projectRoot ??
null);
}
export default {
serializeLogDataAsync,
};
//# sourceMappingURL=LogSerialization.js.map