Commit 76d2d848 authored by Joe Haddad's avatar Joe Haddad Committed by Dan Abramov
Browse files

Improve unmapper file heuristic, add limited warning support, and ignore internal errors (#2128)

* Browser sort is not stable

* Fix ordering of final message

* Register the warning capture

* Display only createElement warnings

* Use different method name

* Fix regression

* Ignore errors with only node_module files

* Ignore null files, too

* Revise count

* Revise warning

* Update overlay.js

* Add support for https://github.com/facebook/react/pull/9679

* Use absolute paths

* Trim path if it's absolute

* Make sure it's an absolute path

* Oops

* Tweak for new behavior

* Make it safer

* More resilient warnings

* Prettier output

* Fix flow
parent fa482962
Showing with 145 additions and 32 deletions
+145 -32
...@@ -127,13 +127,12 @@ function createFrame( ...@@ -127,13 +127,12 @@ function createFrame(
lastElement: boolean lastElement: boolean
) { ) {
const { compiled } = frameSetting; const { compiled } = frameSetting;
let { functionName } = frame; let { functionName, _originalFileName: sourceFileName } = frame;
const { const {
fileName, fileName,
lineNumber, lineNumber,
columnNumber, columnNumber,
_scriptCode: scriptLines, _scriptCode: scriptLines,
_originalFileName: sourceFileName,
_originalLineNumber: sourceLineNumber, _originalLineNumber: sourceLineNumber,
_originalColumnNumber: sourceColumnNumber, _originalColumnNumber: sourceColumnNumber,
_originalScriptCode: sourceLines, _originalScriptCode: sourceLines,
...@@ -149,6 +148,12 @@ function createFrame( ...@@ -149,6 +148,12 @@ function createFrame(
let url; let url;
if (!compiled && sourceFileName && sourceLineNumber) { if (!compiled && sourceFileName && sourceLineNumber) {
// Remove everything up to the first /src/
const trimMatch = /^[/|\\].*?[/|\\](src[/|\\].*)/.exec(sourceFileName);
if (trimMatch && trimMatch[1]) {
sourceFileName = trimMatch[1];
}
url = sourceFileName + ':' + sourceLineNumber; url = sourceFileName + ':' + sourceLineNumber;
if (sourceColumnNumber) { if (sourceColumnNumber) {
url += ':' + sourceColumnNumber; url += ':' + sourceColumnNumber;
......
...@@ -12,7 +12,7 @@ import type { SwitchCallback } from './additional'; ...@@ -12,7 +12,7 @@ import type { SwitchCallback } from './additional';
function createOverlay( function createOverlay(
document: Document, document: Document,
name: string, name: ?string,
message: string, message: string,
frames: StackFrame[], frames: StackFrame[],
contextSize: number, contextSize: number,
...@@ -52,14 +52,20 @@ function createOverlay( ...@@ -52,14 +52,20 @@ function createOverlay(
applyStyles(header, headerStyle); applyStyles(header, headerStyle);
// Make message prettier // Make message prettier
let finalMessage = message.match(/^\w*:/) ? message : name + ': ' + message; let finalMessage = message.match(/^\w*:/) || !name
? message
: name + ': ' + message;
finalMessage = finalMessage finalMessage = finalMessage
// TODO: maybe remove this prefix from fbjs? // TODO: maybe remove this prefix from fbjs?
// It's just scaring people // It's just scaring people
.replace('Invariant Violation: ', '') .replace(/^Invariant Violation:\s*/, '')
// This is not helpful either:
.replace(/^Warning:\s*/, '')
// Break the actionable part to the next line. // Break the actionable part to the next line.
// AFAIK React 16+ should already do this. // AFAIK React 16+ should already do this.
.replace(' Check the render method', '\n\nCheck the render method'); .replace(' Check the render method', '\n\nCheck the render method')
.replace(' Check your code at', '\n\nCheck your code at');
// Put it in the DOM // Put it in the DOM
header.appendChild(document.createTextNode(finalMessage)); header.appendChild(document.createTextNode(finalMessage));
......
/* @flow */ /* @flow */
type ConsoleProxyCallback = (message: string) => void;
type ReactFrame = {
fileName: string | null,
lineNumber: number | null,
functionName: string | null,
};
const reactFrameStack: Array<ReactFrame[]> = [];
export type { ReactFrame };
const registerReactStack = () => {
// $FlowFixMe
console.stack = frames => reactFrameStack.push(frames);
// $FlowFixMe
console.stackEnd = frames => reactFrameStack.pop();
};
const unregisterReactStack = () => {
// $FlowFixMe
console.stack = undefined;
// $FlowFixMe
console.stackEnd = undefined;
};
type ConsoleProxyCallback = (message: string, frames: ReactFrame[]) => void;
const permanentRegister = function proxyConsole( const permanentRegister = function proxyConsole(
type: string, type: string,
callback: ConsoleProxyCallback callback: ConsoleProxyCallback
) { ) {
const orig = console[type]; const orig = console[type];
console[type] = function __stack_frame_overlay_proxy_console__() { console[type] = function __stack_frame_overlay_proxy_console__() {
const message = [].slice.call(arguments).join(' '); try {
callback(message); const message = arguments[0];
if (typeof message === 'string' && reactFrameStack.length > 0) {
callback(message, reactFrameStack[reactFrameStack.length - 1]);
}
} catch (err) {
// Warnings must never crash. Rethrow with a clean stack.
setTimeout(function() {
throw err;
});
}
return orig.apply(this, arguments); return orig.apply(this, arguments);
}; };
}; };
export { permanentRegister }; export { permanentRegister, registerReactStack, unregisterReactStack };
...@@ -19,6 +19,12 @@ import { ...@@ -19,6 +19,12 @@ import {
register as registerStackTraceLimit, register as registerStackTraceLimit,
unregister as unregisterStackTraceLimit, unregister as unregisterStackTraceLimit,
} from './effects/stackTraceLimit'; } from './effects/stackTraceLimit';
import {
permanentRegister as permanentRegisterConsole,
registerReactStack,
unregisterReactStack,
} from './effects/proxyConsole';
import { massage as massageWarning } from './utils/warnings';
import { import {
consume as consumeError, consume as consumeError,
...@@ -66,7 +72,7 @@ const css = [ ...@@ -66,7 +72,7 @@ const css = [
'}', '}',
].join('\n'); ].join('\n');
function render(name: string, message: string, resolvedFrames: StackFrame[]) { function render(name: ?string, message: string, resolvedFrames: StackFrame[]) {
disposeCurrentView(); disposeCurrentView();
const iframe = window.document.createElement('iframe'); const iframe = window.document.createElement('iframe');
...@@ -156,6 +162,9 @@ function crash(error: Error, unhandledRejection = false) { ...@@ -156,6 +162,9 @@ function crash(error: Error, unhandledRejection = false) {
} }
consumeError(error, unhandledRejection, CONTEXT_SIZE) consumeError(error, unhandledRejection, CONTEXT_SIZE)
.then(ref => { .then(ref => {
if (ref == null) {
return;
}
errorReferences.push(ref); errorReferences.push(ref);
if (iframeReference !== null && additionalReference !== null) { if (iframeReference !== null && additionalReference !== null) {
updateAdditional( updateAdditional(
...@@ -205,6 +214,20 @@ function inject() { ...@@ -205,6 +214,20 @@ function inject() {
registerPromise(window, error => crash(error, true)); registerPromise(window, error => crash(error, true));
registerShortcuts(window, shortcutHandler); registerShortcuts(window, shortcutHandler);
registerStackTraceLimit(); registerStackTraceLimit();
registerReactStack();
permanentRegisterConsole('error', (warning, stack) => {
const data = massageWarning(warning, stack);
crash(
// $FlowFixMe
{
message: data.message,
stack: data.stack,
__unmap_source: '/static/js/bundle.js',
},
false
);
});
} }
function uninject() { function uninject() {
...@@ -212,6 +235,7 @@ function uninject() { ...@@ -212,6 +235,7 @@ function uninject() {
unregisterShortcuts(window); unregisterShortcuts(window);
unregisterPromise(window); unregisterPromise(window);
unregisterError(window); unregisterError(window);
unregisterReactStack();
} }
export { inject, uninject }; export { inject, uninject };
...@@ -19,7 +19,7 @@ function consume( ...@@ -19,7 +19,7 @@ function consume(
error: Error, error: Error,
unhandledRejection: boolean = false, unhandledRejection: boolean = false,
contextSize: number = 3 contextSize: number = 3
): Promise<ErrorRecordReference> { ): Promise<ErrorRecordReference | null> {
const parsedFrames = parse(error); const parsedFrames = parse(error);
let enhancedFramesPromise; let enhancedFramesPromise;
if (error.__unmap_source) { if (error.__unmap_source) {
...@@ -33,6 +33,13 @@ function consume( ...@@ -33,6 +33,13 @@ function consume(
enhancedFramesPromise = map(parsedFrames, contextSize); enhancedFramesPromise = map(parsedFrames, contextSize);
} }
return enhancedFramesPromise.then(enhancedFrames => { return enhancedFramesPromise.then(enhancedFrames => {
if (
enhancedFrames
.map(f => f._originalFileName)
.filter(f => f != null && f.indexOf('node_modules') === -1).length === 0
) {
return null;
}
enhancedFrames = enhancedFrames.filter( enhancedFrames = enhancedFrames.filter(
({ functionName }) => ({ functionName }) =>
functionName == null || functionName == null ||
......
...@@ -4,6 +4,19 @@ import { getSourceMap } from './getSourceMap'; ...@@ -4,6 +4,19 @@ import { getSourceMap } from './getSourceMap';
import { getLinesAround } from './getLinesAround'; import { getLinesAround } from './getLinesAround';
import path from 'path'; import path from 'path';
function count(search: string, string: string): number {
// Count starts at -1 becuse a do-while loop always runs at least once
let count = -1, index = -1;
do {
// First call or the while case evaluated true, meaning we have to make
// count 0 or we found a character
++count;
// Find the index of our search string, starting after the previous index
index = string.indexOf(search, index + 1);
} while (index !== -1);
return count;
}
/** /**
* Turns a set of mapped <code>StackFrame</code>s back into their generated code position and enhances them with code. * Turns a set of mapped <code>StackFrame</code>s back into their generated code position and enhances them with code.
* @param {string} fileUri The URI of the <code>bundle.js</code> file. * @param {string} fileUri The URI of the <code>bundle.js</code> file.
...@@ -39,28 +52,23 @@ async function unmap( ...@@ -39,28 +52,23 @@ async function unmap(
return frame; return frame;
} }
const fN: string = fileName; const fN: string = fileName;
const splitCache1: any = {}, splitCache2: any = {}, splitCache3: any = {};
const source = map const source = map
.getSources() .getSources()
.map(s => s.replace(/[\\]+/g, '/')) .map(s => s.replace(/[\\]+/g, '/'))
.filter(s => { .filter(p => {
s = path.normalize(s); p = path.normalize(p);
return s.indexOf(fN) === s.length - fN.length; const i = p.lastIndexOf(fN);
}) return i !== -1 && i === p.length - fN.length;
.sort((a, b) => {
let a2 = splitCache1[a] || (splitCache1[a] = a.split(path.sep)),
b2 = splitCache1[b] || (splitCache1[b] = b.split(path.sep));
return Math.sign(a2.length - b2.length);
})
.sort((a, b) => {
let a2 = splitCache2[a] || (splitCache2[a] = a.split('node_modules')),
b2 = splitCache2[b] || (splitCache2[b] = b.split('node_modules'));
return Math.sign(a2.length - b2.length);
}) })
.map(p => ({
token: p,
seps: count(path.sep, path.normalize(p)),
penalties: count('node_modules', p) + count('~', p),
}))
.sort((a, b) => { .sort((a, b) => {
let a2 = splitCache3[a] || (splitCache3[a] = a.split('~')), const s = Math.sign(a.seps - b.seps);
b2 = splitCache3[b] || (splitCache3[b] = b.split('~')); if (s !== 0) return s;
return Math.sign(a2.length - b2.length); return Math.sign(a.penalties - b.penalties);
}); });
if (source.length < 1 || lineNumber == null) { if (source.length < 1 || lineNumber == null) {
return new StackFrame( return new StackFrame(
...@@ -76,13 +84,14 @@ async function unmap( ...@@ -76,13 +84,14 @@ async function unmap(
null null
); );
} }
const sourceT = source[0].token;
const { line, column } = map.getGeneratedPosition( const { line, column } = map.getGeneratedPosition(
source[0], sourceT,
lineNumber, lineNumber,
// $FlowFixMe // $FlowFixMe
columnNumber columnNumber
); );
const originalSource = map.getSource(source[0]); const originalSource = map.getSource(sourceT);
return new StackFrame( return new StackFrame(
functionName, functionName,
fileUri, fileUri,
......
// @flow
import type { ReactFrame } from '../effects/proxyConsole';
function stripInlineStacktrace(message: string): string {
return message.split('\n').filter(line => !line.match(/^\s*in/)).join('\n'); // " in Foo"
}
function massage(
warning: string,
frames: ReactFrame[]
): { message: string, stack: string } {
let message = stripInlineStacktrace(warning);
// Reassemble the stack with full filenames provided by React
let stack = '';
for (let index = 0; index < frames.length; ++index) {
const { fileName, lineNumber } = frames[index];
if (fileName == null || lineNumber == null) {
continue;
}
let { functionName } = frames[index];
functionName = functionName || '(anonymous function)';
stack += `in ${functionName} (at ${fileName}:${lineNumber})\n`;
}
return { message, stack };
}
export { massage };
...@@ -77,7 +77,7 @@ module.exports = { ...@@ -77,7 +77,7 @@ module.exports = {
publicPath: publicPath, publicPath: publicPath,
// Point sourcemap entries to original disk location // Point sourcemap entries to original disk location
devtoolModuleFilenameTemplate: info => devtoolModuleFilenameTemplate: info =>
path.relative(paths.appSrc, info.absoluteResourcePath), path.resolve(info.absoluteResourcePath),
}, },
resolve: { resolve: {
// This allows you to set a fallback for where Webpack should look for modules. // This allows you to set a fallback for where Webpack should look for modules.
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment