1 | import Constants from 'expo-constants';
|
2 | import prettyFormat from 'pretty-format';
|
3 | import parseErrorStack from 'react-native/Libraries/Core/Devtools/parseErrorStack';
|
4 | import symbolicateStackTrace from 'react-native/Libraries/Core/Devtools/symbolicateStackTrace';
|
5 | import ReactNodeFormatter from './format/ReactNodeFormatter';
|
6 | export const EXPO_CONSOLE_METHOD_NAME = '__expoConsoleLog';
|
7 | async function serializeLogDataAsync(data, level) {
|
8 | let serializedValues;
|
9 | let includesStack = false;
|
10 | if (_stackTraceLogsSupported()) {
|
11 | if (_isUnhandledPromiseRejection(data, level)) {
|
12 | const rawStack = data[0];
|
13 | const syntheticError = { stack: rawStack };
|
14 | const stack = await _symbolicateErrorAsync(syntheticError);
|
15 | if (!stack.length) {
|
16 | serializedValues = _stringifyLogData(data);
|
17 | }
|
18 | else {
|
19 |
|
20 | const errorMessage = rawStack.split('\n')[1];
|
21 | serializedValues = [
|
22 | {
|
23 | message: `[Unhandled promise rejection: ${errorMessage}]`,
|
24 | stack: _formatStack(stack),
|
25 | },
|
26 | ];
|
27 | includesStack = true;
|
28 | }
|
29 | }
|
30 | else if (data.length === 1 && data[0] instanceof Error) {
|
31 |
|
32 |
|
33 |
|
34 | const serializedError = await _serializeErrorAsync(data[0]);
|
35 | serializedValues = [serializedError];
|
36 | includesStack = serializedError.hasOwnProperty('stack');
|
37 | }
|
38 | else if (level === 'warn' || level === 'error') {
|
39 |
|
40 |
|
41 | const error = _captureConsoleStackTrace();
|
42 |
|
43 | const errorMessage = _stringifyLogData(data).join(', ');
|
44 | const serializedError = await _serializeErrorAsync(error, errorMessage);
|
45 | serializedValues = [serializedError];
|
46 | includesStack = serializedError.hasOwnProperty('stack');
|
47 | }
|
48 | else {
|
49 | serializedValues = _stringifyLogData(data);
|
50 | }
|
51 | }
|
52 | else {
|
53 | serializedValues = _stringifyLogData(data);
|
54 | }
|
55 | return {
|
56 | body: [...serializedValues],
|
57 | includesStack,
|
58 | };
|
59 | }
|
60 | function _stringifyLogData(data) {
|
61 | return data.map(item => {
|
62 | if (typeof item === 'string') {
|
63 | return item;
|
64 | }
|
65 | else {
|
66 |
|
67 | const LOG_MESSAGE_MAX_LENGTH = 10000;
|
68 | const result = prettyFormat(item, { plugins: [ReactNodeFormatter] });
|
69 |
|
70 | if (result.length > LOG_MESSAGE_MAX_LENGTH) {
|
71 | let truncatedResult = result.substring(0, LOG_MESSAGE_MAX_LENGTH);
|
72 |
|
73 | truncatedResult += `...(truncated to the first ${LOG_MESSAGE_MAX_LENGTH} characters)`;
|
74 | return truncatedResult;
|
75 | }
|
76 | else {
|
77 | return result;
|
78 | }
|
79 | }
|
80 | });
|
81 | }
|
82 | async function _serializeErrorAsync(error, message) {
|
83 | if (message == null) {
|
84 | message = error.message;
|
85 | }
|
86 |
|
87 |
|
88 |
|
89 | const messageParts = message.split('\n');
|
90 | const firstUselessLine = messageParts.indexOf('This error is located at:');
|
91 | if (firstUselessLine > 0) {
|
92 | message = messageParts.slice(0, firstUselessLine - 1).join('\n');
|
93 | }
|
94 | if (!error.stack || !error.stack.length) {
|
95 | return prettyFormat(error);
|
96 | }
|
97 | const stack = await _symbolicateErrorAsync(error);
|
98 | const formattedStack = _formatStack(stack);
|
99 | return { message, stack: formattedStack };
|
100 | }
|
101 | async function _symbolicateErrorAsync(error) {
|
102 | const parsedStack = parseErrorStack(error);
|
103 | let symbolicatedStack;
|
104 | try {
|
105 |
|
106 |
|
107 | symbolicatedStack = (await symbolicateStackTrace(parsedStack))?.stack ?? null;
|
108 | }
|
109 | catch (error) {
|
110 | return parsedStack;
|
111 | }
|
112 |
|
113 | if (!symbolicatedStack) {
|
114 | return parsedStack;
|
115 | }
|
116 |
|
117 | return symbolicatedStack.map(_removeProjectRoot);
|
118 | }
|
119 | function _formatStack(stack) {
|
120 | return stack
|
121 | .map(frame => {
|
122 | let line = `${frame.file}:${frame.lineNumber}`;
|
123 | if (frame.column != null) {
|
124 | line += `:${frame.column}`;
|
125 | }
|
126 | line += ` in ${frame.methodName}`;
|
127 | return line;
|
128 | })
|
129 | .join('\n');
|
130 | }
|
131 | function _removeProjectRoot(frame) {
|
132 | let filename = frame.file;
|
133 | if (filename == null) {
|
134 | return frame;
|
135 | }
|
136 | const projectRoot = _getProjectRoot();
|
137 | if (projectRoot == null) {
|
138 | return frame;
|
139 | }
|
140 | if (filename.startsWith(projectRoot)) {
|
141 | filename = filename.substring(projectRoot.length);
|
142 | if (filename[0] === '/' || filename[0] === '\\') {
|
143 | filename = filename.substring(1);
|
144 | }
|
145 | frame.file = filename;
|
146 | }
|
147 | return frame;
|
148 | }
|
149 |
|
150 |
|
151 |
|
152 |
|
153 |
|
154 |
|
155 |
|
156 |
|
157 |
|
158 | function _stackTraceLogsSupported() {
|
159 | return !!(__DEV__ && _getProjectRoot());
|
160 | }
|
161 | function _isUnhandledPromiseRejection(data, level) {
|
162 | return (level === 'warn' &&
|
163 | typeof data[0] === 'string' &&
|
164 | /^Possible Unhandled Promise Rejection/.test(data[0]));
|
165 | }
|
166 | function _captureConsoleStackTrace() {
|
167 | try {
|
168 | throw new Error();
|
169 | }
|
170 | catch (error) {
|
171 | let stackLines = error.stack.split('\n');
|
172 | const consoleMethodIndex = stackLines.findIndex(frame => frame.includes(EXPO_CONSOLE_METHOD_NAME));
|
173 | if (consoleMethodIndex !== -1) {
|
174 | stackLines = stackLines.slice(consoleMethodIndex + 1);
|
175 | error.stack = stackLines.join('\n');
|
176 | }
|
177 | return error;
|
178 | }
|
179 | }
|
180 | function _getProjectRoot() {
|
181 | return Constants.manifest?.developer?.projectRoot ?? null;
|
182 | }
|
183 | export default {
|
184 | serializeLogDataAsync,
|
185 | };
|
186 |
|
\ | No newline at end of file |