Multiple crash support

Summary: This diff adds support to show multiple crashes in the crash reporter plugin. You can also select crashes from the list of the dropdown.

Reviewed By: danielbuechele

Differential Revision: D14513401

fbshipit-source-id: 621d32c5971519e5046daec76ec2f9b32ba4d8ce
This commit is contained in:
Pritesh Nandgaonkar
2019-03-25 04:50:07 -07:00
committed by Facebook Github Bot
parent 759329bbc3
commit 0adc2ef52e
3 changed files with 222 additions and 32 deletions

View File

@@ -16,7 +16,7 @@ export {
FlipperDevicePlugin, FlipperDevicePlugin,
callClient, callClient,
} from './plugin.js'; } from './plugin.js';
export type {PluginClient} from './plugin.js'; export type {PluginClient, Props} from './plugin.js';
export {default as Client} from './Client.js'; export {default as Client} from './Client.js';
export {clipboard} from 'electron'; export {clipboard} from 'electron';
export * from './fb-stubs/constants.js'; export * from './fb-stubs/constants.js';

View File

@@ -38,9 +38,19 @@ function getCrash(
callstack: callstack, callstack: callstack,
reason: reason, reason: reason,
name: name, name: name,
date: new Date(),
}; };
} }
function assertCrash(crash: Crash, expectedCrash: Crash) {
const {notificationID, callstack, reason, name, date} = crash;
expect(notificationID).toEqual(expectedCrash.notificationID);
expect(callstack).toEqual(expectedCrash.callstack);
expect(reason).toEqual(expectedCrash.reason);
expect(name).toEqual(expectedCrash.name);
expect(date.toDateString()).toEqual(expectedCrash.date.toDateString());
}
beforeEach(() => { beforeEach(() => {
setNotificationID(0); // Resets notificationID to 0 setNotificationID(0); // Resets notificationID to 0
setDefaultPersistedState({crashes: []}); // Resets defaultpersistedstate setDefaultPersistedState({crashes: []}); // Resets defaultpersistedstate
@@ -180,16 +190,28 @@ test('test getNewPersisitedStateFromCrashLog for non-empty defaultPersistedState
); );
const content = const content =
'Blaa Blaaa \n Blaa Blaaa \n Exception Type: SIGSEGV \n Blaa Blaa \n Blaa Blaa'; 'Blaa Blaaa \n Blaa Blaaa \n Exception Type: SIGSEGV \n Blaa Blaa \n Blaa Blaa';
expect(perisistedState).toEqual({crashes: [pluginStateCrash]}); expect(perisistedState).toBeDefined();
// $FlowFixMe: Checked if perisistedState is defined or not
const {crashes} = perisistedState;
expect(crashes).toBeDefined();
expect(crashes.length).toEqual(1);
expect(crashes[0]).toEqual(pluginStateCrash);
const newPersistedState = getNewPersisitedStateFromCrashLog( const newPersistedState = getNewPersisitedStateFromCrashLog(
perisistedState, perisistedState,
CrashReporterPlugin, CrashReporterPlugin,
content, content,
'iOS', 'iOS',
); );
expect(newPersistedState).toEqual({ expect(newPersistedState).toBeDefined();
crashes: [pluginStateCrash, getCrash(1, content, 'SIGSEGV', 'SIGSEGV')], // $FlowFixMe: Checked if perisistedState is defined or not
}); const newPersistedStateCrashes = newPersistedState.crashes;
expect(newPersistedStateCrashes).toBeDefined();
expect(newPersistedStateCrashes.length).toEqual(2);
assertCrash(newPersistedStateCrashes[0], pluginStateCrash);
assertCrash(
newPersistedStateCrashes[1],
getCrash(1, content, 'SIGSEGV', 'SIGSEGV'),
);
}); });
test('test getNewPersisitedStateFromCrashLog for non-empty defaultPersistedState and undefined pluginState', () => { test('test getNewPersisitedStateFromCrashLog for non-empty defaultPersistedState and undefined pluginState', () => {
setNotificationID(0); setNotificationID(0);
@@ -210,9 +232,13 @@ test('test getNewPersisitedStateFromCrashLog for non-empty defaultPersistedState
content, content,
'iOS', 'iOS',
); );
expect(newPersistedState).toEqual({ expect(newPersistedState).toBeDefined();
crashes: [crash, getCrash(1, content, 'SIGSEGV', 'SIGSEGV')], // $FlowFixMe: Checked if perisistedState is defined or not
}); const {crashes} = newPersistedState;
expect(crashes).toBeDefined();
expect(crashes.length).toEqual(2);
assertCrash(crashes[0], crash);
assertCrash(crashes[1], getCrash(1, content, 'SIGSEGV', 'SIGSEGV'));
}); });
test('test getNewPersisitedStateFromCrashLog for non-empty defaultPersistedState and defined pluginState and improper crash log', () => { test('test getNewPersisitedStateFromCrashLog for non-empty defaultPersistedState and defined pluginState and improper crash log', () => {
setNotificationID(0); setNotificationID(0);
@@ -234,17 +260,21 @@ test('test getNewPersisitedStateFromCrashLog for non-empty defaultPersistedState
content, content,
'iOS', 'iOS',
); );
expect(newPersistedState).toEqual({ expect(newPersistedState).toBeDefined();
crashes: [ // $FlowFixMe: Checked if perisistedState is defined or not
pluginStateCrash, const {crashes} = newPersistedState;
getCrash( expect(crashes).toBeDefined();
1, expect(crashes.length).toEqual(2);
content, assertCrash(crashes[0], pluginStateCrash);
'Cannot figure out the cause', assertCrash(
'Cannot figure out the cause', crashes[1],
), getCrash(
], 1,
}); content,
'Cannot figure out the cause',
'Cannot figure out the cause',
),
);
}); });
test('test getNewPersisitedStateFromCrashLog when os is undefined', () => { test('test getNewPersisitedStateFromCrashLog when os is undefined', () => {
setNotificationID(0); setNotificationID(0);

View File

@@ -26,13 +26,14 @@ import {
colors, colors,
Toolbar, Toolbar,
Spacer, Spacer,
Select,
} from 'flipper'; } from 'flipper';
import fs from 'fs'; import fs from 'fs';
import os from 'os'; import os from 'os';
import util from 'util'; import util from 'util';
import path from 'path'; import path from 'path';
import type {Notification} from '../../plugin'; import type {Notification} from '../../plugin';
import type {Store, DeviceLogEntry, OS} from 'flipper'; import type {Store, DeviceLogEntry, OS, Props} from 'flipper';
import {Component} from 'react'; import {Component} from 'react';
type HeaderRowProps = { type HeaderRowProps = {
@@ -43,6 +44,14 @@ type openLogsCallbackType = () => void;
type CrashReporterBarProps = {| type CrashReporterBarProps = {|
openLogsCallback?: openLogsCallbackType, openLogsCallback?: openLogsCallbackType,
crashSelector: CrashSelectorProps,
|};
type CrashSelectorProps = {|
crashes: ?{[key: string]: string},
orderedIDs: ?Array<string>,
selectedCrashID: ?string,
onCrashChange: ?(string) => void,
|}; |};
export type Crash = {| export type Crash = {|
@@ -50,6 +59,7 @@ export type Crash = {|
callstack: string, callstack: string,
reason: string, reason: string,
name: string, name: string,
date: Date,
|}; |};
export type CrashLog = {| export type CrashLog = {|
@@ -62,6 +72,10 @@ export type PersistedState = {
crashes: Array<Crash>, crashes: Array<Crash>,
}; };
type State = {
crash: ?Crash,
};
const Padder = styled('div')( const Padder = styled('div')(
({paddingLeft, paddingRight, paddingBottom, paddingTop}) => ({ ({paddingLeft, paddingRight, paddingBottom, paddingTop}) => ({
paddingLeft: paddingLeft || 0, paddingLeft: paddingLeft || 0,
@@ -111,11 +125,40 @@ const StyledFlexGrowColumn = styled(FlexColumn)({
flexGrow: 1, flexGrow: 1,
}); });
const StyledFlexRowColumn = styled(FlexRow)({
aligItems: 'center',
justifyContent: 'center',
height: '100%',
});
const StyledFlexColumn = styled(StyledFlexGrowColumn)({ const StyledFlexColumn = styled(StyledFlexGrowColumn)({
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
}); });
const MatchParentHeightComponent = styled(FlexRow)({
height: '100%',
});
const ButtonGroupContainer = styled(FlexRow)({
paddingLeft: 4,
paddingTop: 2,
paddingBottom: 2,
height: '100%',
});
const StyledSelectContainer = styled(FlexRow)({
paddingLeft: 8,
paddingTop: 2,
paddingBottom: 2,
height: '100%',
});
const StyledSelect = styled(Select)({
height: '100%',
maxWidth: 200,
});
export function getNewPersisitedStateFromCrashLog( export function getNewPersisitedStateFromCrashLog(
persistedState: ?PersistedState, persistedState: ?PersistedState,
persistingPlugin: Class<FlipperDevicePlugin<> | FlipperPlugin<>>, persistingPlugin: Class<FlipperDevicePlugin<> | FlipperPlugin<>>,
@@ -295,11 +338,75 @@ function addFileWatcherForiOSCrashLogs(
}); });
} }
class CrashSelector extends Component<CrashSelectorProps> {
render() {
const {crashes, selectedCrashID, orderedIDs, onCrashChange} = this.props;
return (
<StyledFlexRowColumn>
<ButtonGroupContainer>
<MatchParentHeightComponent>
<Button
disabled={Boolean(!orderedIDs || orderedIDs.length <= 1)}
compact={true}
onClick={() => {
if (onCrashChange && orderedIDs) {
const index = orderedIDs.indexOf(selectedCrashID);
const nextIndex =
index < 1 ? orderedIDs.length - 1 : index - 1;
const nextID = orderedIDs[nextIndex];
onCrashChange(nextID);
}
}}
icon="chevron-left"
iconSize={12}
title="Previous Crash"
/>
</MatchParentHeightComponent>
<MatchParentHeightComponent>
<Button
disabled={Boolean(!orderedIDs || orderedIDs.length <= 1)}
compact={true}
onClick={() => {
if (onCrashChange && orderedIDs) {
const index = orderedIDs.indexOf(selectedCrashID);
const nextIndex =
index >= orderedIDs.length - 1 ? 0 : index + 1;
const nextID = orderedIDs[nextIndex];
onCrashChange(nextID);
}
}}
icon="chevron-right"
iconSize={12}
title="Next Crash"
/>
</MatchParentHeightComponent>
</ButtonGroupContainer>
<StyledSelectContainer>
<StyledSelect
grow={true}
selected={selectedCrashID || 'NoCrashID'}
options={crashes || {NoCrashID: 'No Crash'}}
onChange={(title: string) => {
for (const key in crashes) {
if (crashes[key] === title && onCrashChange) {
onCrashChange(key);
return;
}
}
}}
/>
</StyledSelectContainer>
</StyledFlexRowColumn>
);
}
}
class CrashReporterBar extends Component<CrashReporterBarProps> { class CrashReporterBar extends Component<CrashReporterBarProps> {
render() { render() {
const {openLogsCallback} = this.props; const {openLogsCallback, crashSelector} = this.props;
return ( return (
<Toolbar> <Toolbar>
<CrashSelector {...crashSelector} />
<Spacer /> <Spacer />
<Button <Button
disabled={Boolean(!openLogsCallback)} disabled={Boolean(!openLogsCallback)}
@@ -331,8 +438,8 @@ class HeaderRow extends Component<HeaderRowProps> {
} }
export default class CrashReporterPlugin extends FlipperDevicePlugin< export default class CrashReporterPlugin extends FlipperDevicePlugin<
*, State,
*, void,
PersistedState, PersistedState,
> { > {
static defaultPersistedState = {crashes: []}; static defaultPersistedState = {crashes: []};
@@ -359,6 +466,7 @@ export default class CrashReporterPlugin extends FlipperDevicePlugin<
callstack: payload.callstack, callstack: payload.callstack,
name: payload.name, name: payload.name,
reason: payload.reason, reason: payload.reason,
date: payload.date || new Date(),
}, },
]), ]),
}; };
@@ -448,14 +556,18 @@ export default class CrashReporterPlugin extends FlipperDevicePlugin<
this.props.selectPlugin('DeviceLogs', callstack); this.props.selectPlugin('DeviceLogs', callstack);
}; };
render() { constructor(props: Props<PersistedState>) {
const currentCrash: ?Crash = // Required step: always call the parent class' constructor
super(props);
let crash: ?Crash = null;
if (
this.props.persistedState.crashes && this.props.persistedState.crashes &&
this.props.persistedState.crashes.length > 0 this.props.persistedState.crashes.length > 0
? this.props.persistedState.crashes[ ) {
this.props.persistedState.crashes.length - 1 crash = this.props.persistedState.crashes[
] this.props.persistedState.crashes.length - 1
: null; ];
}
let deeplinkedCrash = null; let deeplinkedCrash = null;
if (this.props.deepLinkPayload) { if (this.props.deepLinkPayload) {
@@ -467,24 +579,66 @@ export default class CrashReporterPlugin extends FlipperDevicePlugin<
deeplinkedCrash = this.props.persistedState.crashes[index]; deeplinkedCrash = this.props.persistedState.crashes[index];
} }
} }
// Set the state directly. Use props if necessary.
this.state = {
crash: deeplinkedCrash || crash,
};
}
const crash = deeplinkedCrash || currentCrash; render() {
let crashToBeInspected = this.state.crash;
if (!crashToBeInspected && this.props.persistedState.crashes.length > 0) {
crashToBeInspected = this.props.persistedState.crashes[
this.props.persistedState.crashes.length - 1
];
}
const crash = crashToBeInspected;
if (crash) { if (crash) {
const {crashes} = this.props.persistedState;
const crashMap = crashes.reduce(
(acc: {[key: string]: string}, persistedCrash: Crash) => {
const {notificationID, date} = persistedCrash;
const name = 'Crash at ' + date.toLocaleString();
acc[notificationID] = name;
return acc;
},
{},
);
const orderedIDs = crashes.map(
persistedCrash => persistedCrash.notificationID,
);
const selectedCrashID = crash.notificationID;
const onCrashChange = id => {
const newSelectedCrash = crashes.find(element => {
return element.notificationID === id;
});
this.setState({crash: newSelectedCrash});
console.log('onCrashChange called', id);
};
const callstackString = crash.callstack; const callstackString = crash.callstack;
const children = crash.callstack.split('\n').map(str => { const children = crash.callstack.split('\n').map(str => {
return {message: str}; return {message: str};
}); });
const crashSelector: CrashSelectorProps = {
crashes: crashMap,
orderedIDs,
selectedCrashID,
onCrashChange,
};
return ( return (
<FlexColumn> <FlexColumn>
{this.device.os == 'Android' ? ( {this.device.os == 'Android' ? (
<CrashReporterBar <CrashReporterBar
crashSelector={crashSelector}
openLogsCallback={() => { openLogsCallback={() => {
this.openInLogs(crash.callstack); this.openInLogs(crash.callstack);
}} }}
/> />
) : ( ) : (
<CrashReporterBar /> <CrashReporterBar crashSelector={crashSelector} />
)} )}
<ScrollableColumn> <ScrollableColumn>
<HeaderRow title="Name" value={crash.name} /> <HeaderRow title="Name" value={crash.name} />
@@ -513,9 +667,15 @@ export default class CrashReporterPlugin extends FlipperDevicePlugin<
</FlexColumn> </FlexColumn>
); );
} }
const crashSelector = {
crashes: null,
orderedIDs: null,
selectedCrashID: null,
onCrashChange: null,
};
return ( return (
<StyledFlexGrowColumn> <StyledFlexGrowColumn>
<CrashReporterBar /> <CrashReporterBar crashSelector={crashSelector} />
<StyledFlexColumn> <StyledFlexColumn>
<Padder paddingBottom={8}> <Padder paddingBottom={8}>
<Title>No Crashes Logged</Title> <Title>No Crashes Logged</Title>