diff --git a/packages/react-native/Libraries/LogBox/Data/LogBoxLog.js b/packages/react-native/Libraries/LogBox/Data/LogBoxLog.js index 32cc3323776..be475215db8 100644 --- a/packages/react-native/Libraries/LogBox/Data/LogBoxLog.js +++ b/packages/react-native/Libraries/LogBox/Data/LogBoxLog.js @@ -79,6 +79,7 @@ class LogBoxLog { count: number; level: LogLevel; codeFrame: ?CodeFrame; + componentCodeFrame: ?CodeFrame; isComponentError: boolean; extraData: mixed | void; symbolicated: @@ -140,8 +141,18 @@ class LogBoxLog { } retrySymbolicate(callback?: (status: SymbolicationStatus) => void): void { + let retry = false; if (this.symbolicated.status !== 'COMPLETE') { LogBoxSymbolication.deleteStack(this.stack); + retry = true; + } + if (this.symbolicatedComponentStack.status !== 'COMPLETE') { + LogBoxSymbolication.deleteStack( + convertComponentStateToStack(this.componentStack), + ); + retry = true; + } + if (retry) { this.handleSymbolicate(callback); } } @@ -153,7 +164,10 @@ class LogBoxLog { } handleSymbolicate(callback?: (status: SymbolicationStatus) => void): void { - if (this.symbolicated.status !== 'PENDING') { + if ( + this.symbolicated.status !== 'PENDING' && + this.symbolicated.status !== 'COMPLETE' + ) { this.updateStatus(null, null, null, callback); LogBoxSymbolication.symbolicate(this.stack, this.extraData).then( data => { @@ -163,25 +177,30 @@ class LogBoxLog { this.updateStatus(error, null, null, callback); }, ); - if (this.componentStack != null && this.componentStackType === 'stack') { - this.updateComponentStackStatus(null, null, null, callback); - const componentStackFrames = convertComponentStateToStack( - this.componentStack, - ); - LogBoxSymbolication.symbolicate(componentStackFrames, []).then( - data => { - this.updateComponentStackStatus( - null, - convertStackToComponentStack(data.stack), - null, - callback, - ); - }, - error => { - this.updateComponentStackStatus(error, null, null, callback); - }, - ); - } + } + if ( + this.componentStack != null && + this.componentStackType === 'stack' && + this.symbolicatedComponentStack.status !== 'PENDING' && + this.symbolicatedComponentStack.status !== 'COMPLETE' + ) { + this.updateComponentStackStatus(null, null, null, callback); + const componentStackFrames = convertComponentStateToStack( + this.componentStack, + ); + LogBoxSymbolication.symbolicate(componentStackFrames, []).then( + data => { + this.updateComponentStackStatus( + null, + convertStackToComponentStack(data.stack), + data?.codeFrame, + callback, + ); + }, + error => { + this.updateComponentStackStatus(error, null, null, callback); + }, + ); } } @@ -235,6 +254,9 @@ class LogBoxLog { status: 'FAILED', }; } else if (componentStack != null) { + if (codeFrame) { + this.componentCodeFrame = codeFrame; + } this.symbolicatedComponentStack = { error: null, componentStack, diff --git a/packages/react-native/Libraries/LogBox/Data/__tests__/LogBoxLog-test.js b/packages/react-native/Libraries/LogBox/Data/__tests__/LogBoxLog-test.js index a6d34be82c3..364af49122a 100644 --- a/packages/react-native/Libraries/LogBox/Data/__tests__/LogBoxLog-test.js +++ b/packages/react-native/Libraries/LogBox/Data/__tests__/LogBoxLog-test.js @@ -13,12 +13,36 @@ import type {SymbolicatedStackTrace} from '../../../Core/Devtools/symbolicateStackTrace'; import type {StackFrame} from '../../../Core/NativeExceptionsManager'; +import type {CodeFrame} from '../parseLogBoxLog'; jest.mock('../LogBoxSymbolication', () => { return {__esModule: true, symbolicate: jest.fn(), deleteStack: jest.fn()}; }); -function getLogBoxLog() { +type CodeCodeFrame = $ReadOnly<{ + content: string, + location: ?{ + row: number, + column: number, + ... + }, + fileName: string, +}>; + +const STACK_CODE_FRAME: CodeCodeFrame = { + fileName: '/path/to/Stack.js', + location: {row: 199, column: 0}, + content: '', +}; + +const COMPONENT_CODE_FRAME: CodeCodeFrame = { + fileName: '/path/to/Component.js', + location: {row: 199, column: 0}, + content: 'Component', +}; + +// We can delete this when we delete legacy component stack types. +function getLogBoxLogLegacy() { return new (require('../LogBoxLog').default)({ level: 'warn', isComponentError: false, @@ -40,6 +64,19 @@ function getLogBoxLog() { }); } +function getLogBoxLog() { + return new (require('../LogBoxLog').default)({ + level: 'warn', + isComponentError: false, + message: {content: '...', substitutions: []}, + stack: createStack(['A', 'B', 'C']), + category: 'Message category...', + componentStackType: 'stack', + componentStack: createComponentStack(['A', 'B', 'C']), + codeFrame: null, + }); +} + function getLogBoxSymbolication(): { symbolicate: JestMockFn< $ReadOnlyArray>, @@ -51,12 +88,54 @@ function getLogBoxSymbolication(): { const createStack = (methodNames: Array) => methodNames.map((methodName): StackFrame => ({ - column: null, + column: 0, file: 'file://path/to/file.js', lineNumber: 1, methodName, })); +const createStackForComponentStack = (methodNames: Array) => + methodNames.map((methodName): StackFrame => ({ + column: 0, + file: 'file://path/to/component.js', + lineNumber: 1, + methodName, + })); + +const createComponentStack = (methodNames: Array) => + methodNames.map((methodName): CodeFrame => ({ + collapse: false, + content: methodName, + location: { + row: 1, + column: 0, + }, + fileName: 'file://path/to/component.js', + })); + +function mockSymbolicate( + stack: $ReadOnlyArray, + stackCodeFrame: ?CodeCodeFrame, + componentCodeFrame: ?CodeCodeFrame, +): SymbolicatedStackTrace { + const firstFrame = stack[0]; + if ( + firstFrame != null && + firstFrame.file != null && + firstFrame.file.indexOf('component.js') > 0 + ) { + return { + stack: createStackForComponentStack( + stack.map(frame => `C(${frame.methodName})`), + ), + codeFrame: COMPONENT_CODE_FRAME, + }; + } + return { + stack: createStack(stack.map(frame => `S(${frame.methodName})`)), + codeFrame: STACK_CODE_FRAME, + }; +} // Adds a new task to the end of the microtask queue, so that awaiting this // function will run all queued immediates const runMicrotasks = async () => {}; @@ -65,244 +144,733 @@ describe('LogBoxLog', () => { beforeEach(() => { jest.resetModules(); - getLogBoxSymbolication().symbolicate.mockImplementation(async stack => ({ - stack: createStack(stack.map(frame => `S(${frame.methodName})`)), - codeFrame: null, - })); + getLogBoxSymbolication().symbolicate.mockImplementation(async stack => + mockSymbolicate(stack), + ); }); - it('creates a LogBoxLog object', () => { - const log = getLogBoxLog(); + describe('symbolicate legacy component stacks (no symbolication)', () => { + it('creates a LogBoxLog object', () => { + const log = getLogBoxLogLegacy(); - expect(log.level).toEqual('warn'); - expect(log.message).toEqual({content: '...', substitutions: []}); - expect(log.stack).toEqual(createStack(['A', 'B', 'C'])); - expect(log.category).toEqual('Message category...'); - expect(log.componentStack).toEqual([ - { - content: 'LogBoxLog', - fileName: 'LogBoxLog.js', - location: {column: -1, row: 1}, - }, - ]); - expect(log.codeFrame).toEqual({ - fileName: '/path/to/RKJSModules/Apps/CrashReact/CrashReactApp.js', - location: {row: 199, column: 0}, - content: '', + expect(log.level).toEqual('warn'); + expect(log.message).toEqual({content: '...', substitutions: []}); + expect(log.stack).toEqual(createStack(['A', 'B', 'C'])); + expect(log.category).toEqual('Message category...'); + expect(log.componentStack).toEqual([ + { + content: 'LogBoxLog', + fileName: 'LogBoxLog.js', + location: {column: -1, row: 1}, + }, + ]); + expect(log.codeFrame).toEqual({ + fileName: '/path/to/RKJSModules/Apps/CrashReact/CrashReactApp.js', + location: {row: 199, column: 0}, + content: '', + }); + }); + + it('increments LogBoxLog count', () => { + const log = getLogBoxLogLegacy(); + + expect(log.count).toEqual(1); + + log.incrementCount(); + + expect(log.count).toEqual(2); + }); + + it('starts without a symbolicated stack', () => { + const log = getLogBoxLogLegacy(); + + expect(log.symbolicated).toEqual({ + error: null, + stack: null, + status: 'NONE', + }); + }); + + it('updates when symbolication is in progress', () => { + const log = getLogBoxLogLegacy(); + + const callback = jest.fn(); + log.symbolicate(callback); + + expect(callback).toBeCalledTimes(1); + expect(callback).toBeCalledWith('PENDING'); + expect(getLogBoxSymbolication().symbolicate).toBeCalledTimes(1); + expect(log.symbolicated).toEqual({ + error: null, + stack: null, + status: 'PENDING', + }); + + // Symbolicating while pending should not make more requests. + callback.mockClear(); + getLogBoxSymbolication().symbolicate.mockClear(); + + log.symbolicate(callback); + expect(callback).not.toBeCalled(); + expect(getLogBoxSymbolication().symbolicate).not.toBeCalled(); + }); + + it('updates when symbolication finishes', async () => { + const log = getLogBoxLogLegacy(); + + const callback = jest.fn(); + log.symbolicate(callback); + expect(callback).toBeCalledTimes(1); + expect(callback).toBeCalledWith('PENDING'); + expect(getLogBoxSymbolication().symbolicate).toBeCalled(); + + await runMicrotasks(); + + expect(callback).toBeCalledTimes(2); + expect(callback).toBeCalledWith('COMPLETE'); + expect(log.symbolicated).toEqual({ + error: null, + stack: createStack(['S(A)', 'S(B)', 'S(C)']), + status: 'COMPLETE', + }); + + // Do not symbolicate again. + callback.mockClear(); + getLogBoxSymbolication().symbolicate.mockClear(); + + log.symbolicate(callback); + + await runMicrotasks(); + + expect(callback).toBeCalledTimes(0); + expect(getLogBoxSymbolication().symbolicate).not.toBeCalled(); + }); + + it('updates when symbolication fails', async () => { + const error = new Error('...'); + getLogBoxSymbolication().symbolicate.mockImplementation(async stack => { + throw error; + }); + + const log = getLogBoxLogLegacy(); + + const callback = jest.fn(); + log.symbolicate(callback); + expect(callback).toBeCalledTimes(1); + expect(callback).toBeCalledWith('PENDING'); + expect(getLogBoxSymbolication().symbolicate).toBeCalled(); + + await runMicrotasks(); + + expect(callback).toBeCalledTimes(2); + expect(callback).toBeCalledWith('FAILED'); + expect(log.symbolicated).toEqual({ + error, + stack: null, + status: 'FAILED', + }); + + // Do not symbolicate again, retry if needed. + callback.mockClear(); + getLogBoxSymbolication().symbolicate.mockClear(); + + log.symbolicate(callback); + + await runMicrotasks(); + + expect(callback).toBeCalledTimes(0); + expect(getLogBoxSymbolication().symbolicate).not.toBeCalled(); + }); + + it('retry updates when symbolication is in progress', () => { + const log = getLogBoxLogLegacy(); + + const callback = jest.fn(); + log.retrySymbolicate(callback); + + expect(callback).toBeCalledTimes(1); + expect(callback).toBeCalledWith('PENDING'); + expect(getLogBoxSymbolication().symbolicate).toBeCalledTimes(1); + expect(log.symbolicated).toEqual({ + error: null, + stack: null, + status: 'PENDING', + }); + + // Symbolicating while pending should not make more requests. + callback.mockClear(); + getLogBoxSymbolication().symbolicate.mockClear(); + + log.symbolicate(callback); + expect(callback).not.toBeCalled(); + expect(getLogBoxSymbolication().symbolicate).not.toBeCalled(); + }); + + it('retry updates when symbolication finishes', async () => { + const log = getLogBoxLogLegacy(); + + const callback = jest.fn(); + log.retrySymbolicate(callback); + expect(callback).toBeCalledTimes(1); + expect(callback).toBeCalledWith('PENDING'); + expect(getLogBoxSymbolication().symbolicate).toBeCalled(); + + await runMicrotasks(); + + expect(callback).toBeCalledTimes(2); + expect(callback).toBeCalledWith('COMPLETE'); + expect(log.symbolicated).toEqual({ + error: null, + stack: createStack(['S(A)', 'S(B)', 'S(C)']), + status: 'COMPLETE', + }); + + // Do not symbolicate again + callback.mockClear(); + getLogBoxSymbolication().symbolicate.mockClear(); + + log.retrySymbolicate(callback); + jest.runAllTicks(); + + expect(callback).toBeCalledTimes(0); + expect(getLogBoxSymbolication().symbolicate).not.toBeCalled(); + }); + + it('retry updates when symbolication fails', async () => { + const error = new Error('...'); + getLogBoxSymbolication().symbolicate.mockImplementation(async stack => { + throw error; + }); + + const log = getLogBoxLogLegacy(); + + const callback = jest.fn(); + log.retrySymbolicate(callback); + expect(callback).toBeCalledTimes(1); + expect(callback).toBeCalledWith('PENDING'); + expect(getLogBoxSymbolication().symbolicate).toBeCalled(); + + await runMicrotasks(); + + expect(callback).toBeCalledTimes(2); + expect(callback).toBeCalledWith('FAILED'); + expect(log.symbolicated).toEqual({ + error, + stack: null, + status: 'FAILED', + }); + + // Retry to symbolicate again. + callback.mockClear(); + getLogBoxSymbolication().symbolicate.mockClear(); + getLogBoxSymbolication().symbolicate.mockImplementation(async stack => ({ + stack: createStack(stack.map(frame => `S(${frame.methodName})`)), + codeFrame: null, + })); + + log.retrySymbolicate(callback); + + expect(callback).toBeCalledTimes(1); + expect(callback).toBeCalledWith('PENDING'); + expect(getLogBoxSymbolication().symbolicate).toBeCalled(); + + await runMicrotasks(); + + expect(callback).toBeCalledTimes(2); + expect(callback).toBeCalledWith('COMPLETE'); + expect(log.symbolicated).toEqual({ + error: null, + stack: createStack(['S(A)', 'S(B)', 'S(C)']), + status: 'COMPLETE', + }); }); }); - it('increments LogBoxLog count', () => { - const log = getLogBoxLog(); + describe('symbolicate component stacks', () => { + it('creates a LogBoxLog object', () => { + const log = getLogBoxLog(); - expect(log.count).toEqual(1); - - log.incrementCount(); - - expect(log.count).toEqual(2); - }); - - it('starts without a symbolicated stack', () => { - const log = getLogBoxLog(); - - expect(log.symbolicated).toEqual({ - error: null, - stack: null, - status: 'NONE', - }); - }); - - it('updates when symbolication is in progress', () => { - const log = getLogBoxLog(); - - const callback = jest.fn(); - log.symbolicate(callback); - - expect(callback).toBeCalledTimes(1); - expect(callback).toBeCalledWith('PENDING'); - expect(getLogBoxSymbolication().symbolicate).toBeCalledTimes(1); - expect(log.symbolicated).toEqual({ - error: null, - stack: null, - status: 'PENDING', + expect(log.level).toEqual('warn'); + expect(log.message).toEqual({content: '...', substitutions: []}); + expect(log.stack).toEqual(createStack(['A', 'B', 'C'])); + expect(log.category).toEqual('Message category...'); + expect(log.componentStack).toEqual(createComponentStack(['A', 'B', 'C'])); + expect(log.codeFrame).toEqual(null); + expect(log.componentCodeFrame).toEqual(undefined); }); - // Symbolicating while pending should not make more requests. - callback.mockClear(); - getLogBoxSymbolication().symbolicate.mockClear(); + it('increments LogBoxLog count', () => { + const log = getLogBoxLog(); - log.symbolicate(callback); - expect(callback).not.toBeCalled(); - expect(getLogBoxSymbolication().symbolicate).not.toBeCalled(); - }); + expect(log.count).toEqual(1); - it('updates when symbolication finishes', async () => { - const log = getLogBoxLog(); + log.incrementCount(); - const callback = jest.fn(); - log.symbolicate(callback); - expect(callback).toBeCalledTimes(1); - expect(callback).toBeCalledWith('PENDING'); - expect(getLogBoxSymbolication().symbolicate).toBeCalled(); - - await runMicrotasks(); - - expect(callback).toBeCalledTimes(2); - expect(callback).toBeCalledWith('COMPLETE'); - expect(log.symbolicated).toEqual({ - error: null, - stack: createStack(['S(A)', 'S(B)', 'S(C)']), - status: 'COMPLETE', + expect(log.count).toEqual(2); }); - // Do not symbolicate again. - callback.mockClear(); - getLogBoxSymbolication().symbolicate.mockClear(); + it('starts without a symbolicated stack', () => { + const log = getLogBoxLog(); - log.symbolicate(callback); - - await runMicrotasks(); - - expect(callback).toBeCalledTimes(0); - expect(getLogBoxSymbolication().symbolicate).not.toBeCalled(); - }); - - it('updates when symbolication fails', async () => { - const error = new Error('...'); - getLogBoxSymbolication().symbolicate.mockImplementation(async stack => { - throw error; + expect(log.symbolicated).toEqual({ + error: null, + stack: null, + status: 'NONE', + }); + expect(log.symbolicatedComponentStack).toEqual({ + error: null, + componentStack: null, + status: 'NONE', + }); }); - const log = getLogBoxLog(); + it('updates when symbolication is in progress', () => { + const log = getLogBoxLog(); - const callback = jest.fn(); - log.symbolicate(callback); - expect(callback).toBeCalledTimes(1); - expect(callback).toBeCalledWith('PENDING'); - expect(getLogBoxSymbolication().symbolicate).toBeCalled(); + const callback = jest.fn(); + log.symbolicate(callback); - await runMicrotasks(); + expect(callback).toBeCalledTimes(2); + expect(callback.mock.calls[0][0]).toBe('PENDING'); + expect(callback.mock.calls[1][0]).toBe('PENDING'); + expect(getLogBoxSymbolication().symbolicate).toBeCalledTimes(2); + expect(log.symbolicated).toEqual({ + error: null, + stack: null, + status: 'PENDING', + }); + expect(log.symbolicatedComponentStack).toEqual({ + error: null, + componentStack: null, + status: 'PENDING', + }); - expect(callback).toBeCalledTimes(2); - expect(callback).toBeCalledWith('FAILED'); - expect(log.symbolicated).toEqual({ - error, - stack: null, - status: 'FAILED', + // Symbolicating while pending should not make more requests. + callback.mockClear(); + getLogBoxSymbolication().symbolicate.mockClear(); + + log.symbolicate(callback); + expect(callback).not.toBeCalled(); + expect(getLogBoxSymbolication().symbolicate).not.toBeCalled(); }); - // Do not symbolicate again, retry if needed. - callback.mockClear(); - getLogBoxSymbolication().symbolicate.mockClear(); + it('updates when symbolication finishes', async () => { + const log = getLogBoxLog(); - log.symbolicate(callback); + const callback = jest.fn(); + log.symbolicate(callback); + expect(callback).toBeCalledTimes(2); + expect(callback.mock.calls[0][0]).toBe('PENDING'); + expect(callback.mock.calls[1][0]).toBe('PENDING'); + expect(getLogBoxSymbolication().symbolicate).toBeCalledTimes(2); + callback.mockClear(); - await runMicrotasks(); + await runMicrotasks(); - expect(callback).toBeCalledTimes(0); - expect(getLogBoxSymbolication().symbolicate).not.toBeCalled(); - }); + expect(callback).toBeCalledTimes(2); + expect(callback.mock.calls[0][0]).toBe('COMPLETE'); + expect(callback.mock.calls[1][0]).toBe('COMPLETE'); + expect(log.symbolicated).toEqual({ + error: null, + stack: createStack(['S(A)', 'S(B)', 'S(C)']), + status: 'COMPLETE', + }); + expect(log.codeFrame).toBe(STACK_CODE_FRAME); + expect(log.symbolicatedComponentStack).toEqual({ + error: null, + componentStack: createComponentStack(['C(A)', 'C(B)', 'C(C)']), + status: 'COMPLETE', + }); + expect(log.componentCodeFrame).toBe(COMPONENT_CODE_FRAME); - it('retry updates when symbolication is in progress', () => { - const log = getLogBoxLog(); + // Do not symbolicate again. + callback.mockClear(); + getLogBoxSymbolication().symbolicate.mockClear(); - const callback = jest.fn(); - log.retrySymbolicate(callback); + log.symbolicate(callback); - expect(callback).toBeCalledTimes(1); - expect(callback).toBeCalledWith('PENDING'); - expect(getLogBoxSymbolication().symbolicate).toBeCalledTimes(1); - expect(log.symbolicated).toEqual({ - error: null, - stack: null, - status: 'PENDING', + await runMicrotasks(); + + expect(callback).toBeCalledTimes(0); + expect(getLogBoxSymbolication().symbolicate).not.toBeCalled(); }); - // Symbolicating while pending should not make more requests. - callback.mockClear(); - getLogBoxSymbolication().symbolicate.mockClear(); + it('updates when first symbolication fails', async () => { + const error = new Error('...'); + let count = 0; + getLogBoxSymbolication().symbolicate.mockImplementation(async stack => { + count += 1; + if (count === 1) { + throw error; + } + return mockSymbolicate(stack); + }); - log.symbolicate(callback); - expect(callback).not.toBeCalled(); - expect(getLogBoxSymbolication().symbolicate).not.toBeCalled(); - }); + const log = getLogBoxLog(); - it('retry updates when symbolication finishes', async () => { - const log = getLogBoxLog(); + const callback = jest.fn(); + log.symbolicate(callback); + expect(callback).toBeCalledTimes(2); + expect(callback.mock.calls[0][0]).toBe('PENDING'); + expect(callback.mock.calls[1][0]).toBe('PENDING'); + expect(getLogBoxSymbolication().symbolicate).toBeCalled(); + callback.mockClear(); - const callback = jest.fn(); - log.retrySymbolicate(callback); - expect(callback).toBeCalledTimes(1); - expect(callback).toBeCalledWith('PENDING'); - expect(getLogBoxSymbolication().symbolicate).toBeCalled(); + await runMicrotasks(); - await runMicrotasks(); + expect(callback).toBeCalledTimes(2); + expect(callback.mock.calls[0][0]).toBe('FAILED'); + expect(callback.mock.calls[1][0]).toBe('COMPLETE'); + expect(log.symbolicated).toEqual({ + error, + stack: null, + status: 'FAILED', + }); + expect(log.symbolicatedComponentStack).toEqual({ + error: null, + componentStack: createComponentStack(['C(A)', 'C(B)', 'C(C)']), + status: 'COMPLETE', + }); - expect(callback).toBeCalledTimes(2); - expect(callback).toBeCalledWith('COMPLETE'); - expect(log.symbolicated).toEqual({ - error: null, - stack: createStack(['S(A)', 'S(B)', 'S(C)']), - status: 'COMPLETE', + expect(log.componentCodeFrame).toBe(COMPONENT_CODE_FRAME); + + // Do not symbolicate again, retry if needed. + callback.mockClear(); + getLogBoxSymbolication().symbolicate.mockClear(); + + log.symbolicate(callback); + + await runMicrotasks(); + + expect(callback).toBeCalledTimes(0); + expect(getLogBoxSymbolication().symbolicate).not.toBeCalled(); }); - // Do not symbolicate again - callback.mockClear(); - getLogBoxSymbolication().symbolicate.mockClear(); + it('updates when second symbolication fails', async () => { + const error = new Error('...'); + let count = 0; + getLogBoxSymbolication().symbolicate.mockImplementation(async stack => { + count += 1; + if (count === 2) { + throw error; + } + return mockSymbolicate(stack); + }); - log.retrySymbolicate(callback); - jest.runAllTicks(); + const log = getLogBoxLog(); - expect(callback).toBeCalledTimes(0); - expect(getLogBoxSymbolication().symbolicate).not.toBeCalled(); - }); + const callback = jest.fn(); + log.symbolicate(callback); + expect(callback).toBeCalledTimes(2); + expect(callback.mock.calls[0][0]).toBe('PENDING'); + expect(callback.mock.calls[1][0]).toBe('PENDING'); + expect(getLogBoxSymbolication().symbolicate).toBeCalled(); + callback.mockClear(); - it('retry updates when symbolication fails', async () => { - const error = new Error('...'); - getLogBoxSymbolication().symbolicate.mockImplementation(async stack => { - throw error; + await runMicrotasks(); + + expect(callback).toBeCalledTimes(2); + expect(callback.mock.calls[0][0]).toBe('COMPLETE'); + expect(callback.mock.calls[1][0]).toBe('FAILED'); + expect(log.symbolicated).toEqual({ + error: null, + stack: createStack(['S(A)', 'S(B)', 'S(C)']), + status: 'COMPLETE', + }); + expect(log.codeFrame).toBe(STACK_CODE_FRAME); + expect(log.symbolicatedComponentStack).toEqual({ + error, + componentStack: null, + status: 'FAILED', + }); + + // Do not symbolicate again, retry if needed. + callback.mockClear(); + getLogBoxSymbolication().symbolicate.mockClear(); + + log.symbolicate(callback); + + await runMicrotasks(); + + expect(callback).toBeCalledTimes(0); + expect(getLogBoxSymbolication().symbolicate).not.toBeCalled(); }); - const log = getLogBoxLog(); + it('retry updates when symbolication is in progress', () => { + const log = getLogBoxLog(); - const callback = jest.fn(); - log.retrySymbolicate(callback); - expect(callback).toBeCalledTimes(1); - expect(callback).toBeCalledWith('PENDING'); - expect(getLogBoxSymbolication().symbolicate).toBeCalled(); + const callback = jest.fn(); + log.retrySymbolicate(callback); - await runMicrotasks(); + expect(callback).toBeCalledTimes(2); + expect(callback.mock.calls[0][0]).toBe('PENDING'); + expect(callback.mock.calls[1][0]).toBe('PENDING'); + expect(getLogBoxSymbolication().symbolicate).toBeCalledTimes(2); + expect(log.symbolicated).toEqual({ + error: null, + stack: null, + status: 'PENDING', + }); + expect(log.symbolicatedComponentStack).toEqual({ + error: null, + componentStack: null, + status: 'PENDING', + }); - expect(callback).toBeCalledTimes(2); - expect(callback).toBeCalledWith('FAILED'); - expect(log.symbolicated).toEqual({ - error, - stack: null, - status: 'FAILED', + // Symbolicating while pending should not make more requests. + callback.mockClear(); + getLogBoxSymbolication().symbolicate.mockClear(); + + log.symbolicate(callback); + expect(callback).not.toBeCalled(); + expect(getLogBoxSymbolication().symbolicate).not.toBeCalled(); }); - // Retry to symbolicate again. - callback.mockClear(); - getLogBoxSymbolication().symbolicate.mockClear(); - getLogBoxSymbolication().symbolicate.mockImplementation(async stack => ({ - stack: createStack(stack.map(frame => `S(${frame.methodName})`)), - codeFrame: null, - })); + it('retry updates when symbolication finishes', async () => { + const log = getLogBoxLog(); - log.retrySymbolicate(callback); + const callback = jest.fn(); + log.retrySymbolicate(callback); + expect(callback).toBeCalledTimes(2); + expect(callback.mock.calls[0][0]).toBe('PENDING'); + expect(callback.mock.calls[1][0]).toBe('PENDING'); + expect(getLogBoxSymbolication().symbolicate).toBeCalledTimes(2); + callback.mockClear(); - expect(callback).toBeCalledTimes(1); - expect(callback).toBeCalledWith('PENDING'); - expect(getLogBoxSymbolication().symbolicate).toBeCalled(); + await runMicrotasks(); - await runMicrotasks(); + expect(callback).toBeCalledTimes(2); + expect(callback.mock.calls[0][0]).toBe('COMPLETE'); + expect(callback.mock.calls[1][0]).toBe('COMPLETE'); + expect(log.symbolicated).toEqual({ + error: null, + stack: createStack(['S(A)', 'S(B)', 'S(C)']), + status: 'COMPLETE', + }); + expect(log.codeFrame).toBe(STACK_CODE_FRAME); + expect(log.symbolicatedComponentStack).toEqual({ + error: null, + componentStack: createComponentStack(['C(A)', 'C(B)', 'C(C)']), + status: 'COMPLETE', + }); + expect(log.componentCodeFrame).toBe(COMPONENT_CODE_FRAME); - expect(callback).toBeCalledTimes(2); - expect(callback).toBeCalledWith('COMPLETE'); - expect(log.symbolicated).toEqual({ - error: null, - stack: createStack(['S(A)', 'S(B)', 'S(C)']), - status: 'COMPLETE', + // Do not symbolicate again + callback.mockClear(); + getLogBoxSymbolication().symbolicate.mockClear(); + + log.retrySymbolicate(callback); + jest.runAllTicks(); + + expect(callback).toBeCalledTimes(0); + expect(getLogBoxSymbolication().symbolicate).not.toBeCalled(); + }); + + it('retry updates when both symbolications fail', async () => { + const error = new Error('...'); + getLogBoxSymbolication().symbolicate.mockImplementation(async stack => { + throw error; + }); + + const log = getLogBoxLog(); + + const callback = jest.fn(); + log.retrySymbolicate(callback); + expect(callback).toBeCalledTimes(2); + expect(callback.mock.calls[0][0]).toBe('PENDING'); + expect(callback.mock.calls[1][0]).toBe('PENDING'); + expect(getLogBoxSymbolication().symbolicate).toBeCalledTimes(2); + callback.mockClear(); + + await runMicrotasks(); + + expect(callback).toBeCalledTimes(2); + expect(callback.mock.calls[0][0]).toBe('FAILED'); + expect(callback.mock.calls[1][0]).toBe('FAILED'); + expect(log.symbolicated).toEqual({ + error, + stack: null, + status: 'FAILED', + }); + expect(log.symbolicatedComponentStack).toEqual({ + error, + componentStack: null, + status: 'FAILED', + }); + + // Retry to symbolicate again. + callback.mockClear(); + getLogBoxSymbolication().symbolicate.mockClear(); + getLogBoxSymbolication().symbolicate.mockImplementation(async stack => + mockSymbolicate(stack), + ); + + log.retrySymbolicate(callback); + + expect(callback).toBeCalledTimes(2); + expect(callback.mock.calls[0][0]).toBe('PENDING'); + expect(callback.mock.calls[1][0]).toBe('PENDING'); + expect(getLogBoxSymbolication().symbolicate).toBeCalledTimes(2); + callback.mockClear(); + + await runMicrotasks(); + + expect(callback.mock.calls[0][0]).toBe('COMPLETE'); + expect(callback.mock.calls[1][0]).toBe('COMPLETE'); + expect(log.symbolicated).toEqual({ + error: null, + stack: createStack(['S(A)', 'S(B)', 'S(C)']), + status: 'COMPLETE', + }); + expect(log.codeFrame).toBe(STACK_CODE_FRAME); + expect(log.symbolicatedComponentStack).toEqual({ + error: null, + componentStack: createComponentStack(['C(A)', 'C(B)', 'C(C)']), + status: 'COMPLETE', + }); + expect(log.componentCodeFrame).toBe(COMPONENT_CODE_FRAME); + }); + + it('retry updates when stack symbolication fails', async () => { + const error = new Error('...'); + let count = 0; + getLogBoxSymbolication().symbolicate.mockImplementation(async stack => { + count += 1; + if (count === 1) { + throw error; + } + return mockSymbolicate(stack); + }); + + const log = getLogBoxLog(); + + const callback = jest.fn(); + log.retrySymbolicate(callback); + expect(callback).toBeCalledTimes(2); + expect(callback.mock.calls[0][0]).toBe('PENDING'); + expect(callback.mock.calls[1][0]).toBe('PENDING'); + expect(getLogBoxSymbolication().symbolicate).toBeCalledTimes(2); + callback.mockClear(); + + await runMicrotasks(); + + expect(callback).toBeCalledTimes(2); + expect(callback.mock.calls[0][0]).toBe('FAILED'); + expect(callback.mock.calls[1][0]).toBe('COMPLETE'); + expect(log.symbolicated).toEqual({ + error, + stack: null, + status: 'FAILED', + }); + expect(log.symbolicatedComponentStack).toEqual({ + error: null, + componentStack: createComponentStack(['C(A)', 'C(B)', 'C(C)']), + status: 'COMPLETE', + }); + expect(log.componentCodeFrame).toBe(COMPONENT_CODE_FRAME); + + // Retry to symbolicate again. + callback.mockClear(); + getLogBoxSymbolication().symbolicate.mockClear(); + getLogBoxSymbolication().symbolicate.mockImplementation(async stack => + mockSymbolicate(stack), + ); + + log.retrySymbolicate(callback); + + // Since only one symbolication failed, we should only have one pending. + expect(callback).toBeCalledTimes(1); + expect(callback.mock.calls[0][0]).toBe('PENDING'); + expect(getLogBoxSymbolication().symbolicate).toBeCalledTimes(1); + callback.mockClear(); + + await runMicrotasks(); + + expect(callback).toBeCalledTimes(1); + expect(callback.mock.calls[0][0]).toBe('COMPLETE'); + expect(log.symbolicated).toEqual({ + error: null, + stack: createStack(['S(A)', 'S(B)', 'S(C)']), + status: 'COMPLETE', + }); + expect(log.codeFrame).toBe(STACK_CODE_FRAME); + expect(log.symbolicatedComponentStack).toEqual({ + error: null, + componentStack: createComponentStack(['C(A)', 'C(B)', 'C(C)']), + status: 'COMPLETE', + }); + expect(log.componentCodeFrame).toBe(COMPONENT_CODE_FRAME); + }); + + it('retry updates when component symbolication fails', async () => { + const error = new Error('...'); + let count = 0; + getLogBoxSymbolication().symbolicate.mockImplementation(async stack => { + count += 1; + if (count === 2) { + throw error; + } + return mockSymbolicate(stack); + }); + + const log = getLogBoxLog(); + + const callback = jest.fn(); + log.retrySymbolicate(callback); + expect(callback).toBeCalledTimes(2); + expect(callback.mock.calls[0][0]).toBe('PENDING'); + expect(callback.mock.calls[1][0]).toBe('PENDING'); + expect(getLogBoxSymbolication().symbolicate).toBeCalledTimes(2); + callback.mockClear(); + + await runMicrotasks(); + + expect(callback).toBeCalledTimes(2); + expect(callback.mock.calls[0][0]).toBe('COMPLETE'); + expect(callback.mock.calls[1][0]).toBe('FAILED'); + expect(log.symbolicated).toEqual({ + error: null, + stack: createStack(['S(A)', 'S(B)', 'S(C)']), + status: 'COMPLETE', + }); + expect(log.codeFrame).toBe(STACK_CODE_FRAME); + expect(log.symbolicatedComponentStack).toEqual({ + error, + componentStack: null, + status: 'FAILED', + }); + + // Retry to symbolicate again. + callback.mockClear(); + getLogBoxSymbolication().symbolicate.mockClear(); + getLogBoxSymbolication().symbolicate.mockImplementation(async stack => + mockSymbolicate(stack), + ); + + log.retrySymbolicate(callback); + + // Since only one symbolication failed, we should only have one pending. + expect(callback).toBeCalledTimes(1); + expect(callback.mock.calls[0][0]).toBe('PENDING'); + expect(getLogBoxSymbolication().symbolicate).toBeCalledTimes(1); + callback.mockClear(); + + await runMicrotasks(); + + expect(callback).toBeCalledTimes(1); + expect(callback.mock.calls[0][0]).toBe('COMPLETE'); + expect(log.symbolicated).toEqual({ + error: null, + stack: createStack(['S(A)', 'S(B)', 'S(C)']), + status: 'COMPLETE', + }); + expect(log.codeFrame).toBe(STACK_CODE_FRAME); + expect(log.symbolicatedComponentStack).toEqual({ + error: null, + componentStack: createComponentStack(['C(A)', 'C(B)', 'C(C)']), + status: 'COMPLETE', + }); + expect(log.componentCodeFrame).toBe(COMPONENT_CODE_FRAME); }); }); }); diff --git a/packages/react-native/Libraries/LogBox/UI/LogBoxInspectorBody.js b/packages/react-native/Libraries/LogBox/UI/LogBoxInspectorBody.js index 65af7fb2cdd..f9059fa82d1 100644 --- a/packages/react-native/Libraries/LogBox/UI/LogBoxInspectorBody.js +++ b/packages/react-native/Libraries/LogBox/UI/LogBoxInspectorBody.js @@ -52,7 +52,10 @@ export default function LogBoxInspectorBody(props: { title={headerTitle} /> - + @@ -68,7 +71,10 @@ export default function LogBoxInspectorBody(props: { level={props.log.level} title={headerTitle} /> - + diff --git a/packages/react-native/Libraries/LogBox/UI/LogBoxInspectorCodeFrame.js b/packages/react-native/Libraries/LogBox/UI/LogBoxInspectorCodeFrame.js index 8061eaace4a..0b4606cfd19 100644 --- a/packages/react-native/Libraries/LogBox/UI/LogBoxInspectorCodeFrame.js +++ b/packages/react-native/Libraries/LogBox/UI/LogBoxInspectorCodeFrame.js @@ -22,16 +22,13 @@ import LogBoxButton from './LogBoxButton'; import LogBoxInspectorSection from './LogBoxInspectorSection'; import * as LogBoxStyle from './LogBoxStyle'; import * as React from 'react'; + type Props = $ReadOnly<{ + componentCodeFrame: ?CodeFrame, codeFrame: ?CodeFrame, }>; -function LogBoxInspectorCodeFrame(props: Props): React.Node { - const codeFrame = props.codeFrame; - if (codeFrame == null) { - return null; - } - +function CodeFrameDisplay({codeFrame}: {codeFrame: CodeFrame}): React.Node { function getFileName() { // $FlowFixMe[incompatible-use] const matches = /[^/]*$/.exec(codeFrame.fileName); @@ -56,30 +53,52 @@ function LogBoxInspectorCodeFrame(props: Props): React.Node { } return ( - }> - - - - - - - { - openFileInEditor(codeFrame.fileName, codeFrame.location?.row ?? 0); - }}> - - {getFileName()} - {getLocation()} - - + + + + + + { + openFileInEditor(codeFrame.fileName, codeFrame.location?.row ?? 0); + }}> + + {getFileName()} + {getLocation()} + + + + ); +} + +function LogBoxInspectorCodeFrame(props: Props): React.Node { + const {codeFrame, componentCodeFrame} = props; + let sources = []; + if (codeFrame != null) { + sources.push(codeFrame); + } + if ( + componentCodeFrame != null && + componentCodeFrame?.content !== codeFrame?.content + ) { + sources.push(componentCodeFrame); + } + if (sources.length === 0) { + return null; + } + return ( + 1 ? 'Sources' : 'Source'} + action={}> + {sources.map((frame, index) => ( + + ))} ); } diff --git a/packages/react-native/Libraries/LogBox/UI/__tests__/LogBoxInspectorCodeFrame-test.js b/packages/react-native/Libraries/LogBox/UI/__tests__/LogBoxInspectorCodeFrame-test.js index 302fb4508b0..a9a64308815 100644 --- a/packages/react-native/Libraries/LogBox/UI/__tests__/LogBoxInspectorCodeFrame-test.js +++ b/packages/react-native/Libraries/LogBox/UI/__tests__/LogBoxInspectorCodeFrame-test.js @@ -37,7 +37,7 @@ jest.mock('../LogBoxInspectorSection', () => ({ describe('LogBoxInspectorCodeFrame', () => { it('should render null for no code frame', async () => { const output = await render.create( - , + , ); expect(output).toMatchSnapshot(); @@ -46,6 +46,7 @@ describe('LogBoxInspectorCodeFrame', () => { it('should render a code frame', async () => { const output = await render.create( { expect(output).toMatchSnapshot(); }); + it('should render both a code frame and a component frame', async () => { + const output = await render.create( + 91 | return ; + | ^ + 92 | } + 93 | + 94 |`, + location: {row: 90, column: 10}, + fileName: '/path/to/RKJSModules/Apps/CrashReact/CrashReactApp.js', + }} + codeFrame={{ + fileName: '/path/to/RKJSModules/Apps/CrashReact/CrashReactApp.js', + location: {row: 64, column: 16}, + content: ` 62 | + 63 | function ConsoleWithThrow() { +> 64 | console.error('hit'); + | ^ + 65 | throw new Error('test'); + 66 | } + 67 |`, + }} + />, + ); + + expect(output).toMatchSnapshot(); + }); + + it('should dedupe if code frames are the same', async () => { + const output = await render.create( + 65 | throw new Error('test'); + | ^ + 66 | } + 67 | + 68 |`, + location: {row: 65, column: 18}, + fileName: '/path/to/RKJSModules/Apps/CrashReact/CrashReactApp.js', + }} + codeFrame={{ + content: ` 63 | function ConsoleWithThrow() { + 64 | console.error('hit'); +> 65 | throw new Error('test'); + | ^ + 66 | } + 67 | + 68 |`, + location: {row: 65, column: 18}, + fileName: '/path/to/RKJSModules/Apps/CrashReact/CrashReactApp.js', + }} + />, + ); + + expect(output).toMatchSnapshot(); + }); + it('should render a code frame without a location', async () => { const output = await render.create( } + heading="Source" +> + + + + + + + + + CrashReactApp.js + (65:19) + + + + +`; + exports[`LogBoxInspectorCodeFrame should render a code frame 1`] = ` } @@ -173,4 +262,174 @@ exports[`LogBoxInspectorCodeFrame should render a code frame without a location `; +exports[`LogBoxInspectorCodeFrame should render both a code frame and a component frame 1`] = ` +} + heading="Sources" +> + + + + + + + + + CrashReactApp.js + (64:17) + + + + + + + + + + + + CrashReactApp.js + (90:11) + + + + +`; + exports[`LogBoxInspectorCodeFrame should render null for no code frame 1`] = `null`; diff --git a/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap b/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap index 970e247e3a8..cb26bad89d2 100644 --- a/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap +++ b/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap @@ -6064,6 +6064,7 @@ declare class LogBoxLog { count: number; level: LogLevel; codeFrame: ?CodeFrame; + componentCodeFrame: ?CodeFrame; isComponentError: boolean; extraData: mixed | void; symbolicated: @@ -6257,6 +6258,7 @@ exports[`public API should not change unintentionally Libraries/LogBox/UI/LogBox exports[`public API should not change unintentionally Libraries/LogBox/UI/LogBoxInspectorCodeFrame.js 1`] = ` "type Props = $ReadOnly<{ + componentCodeFrame: ?CodeFrame, codeFrame: ?CodeFrame, }>; declare function LogBoxInspectorCodeFrame(props: Props): React.Node;