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:
committed by
Facebook Github Bot
parent
759329bbc3
commit
0adc2ef52e
@@ -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';
|
||||||
|
|||||||
@@ -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;
|
||||||
|
expect(crashes).toBeDefined();
|
||||||
|
expect(crashes.length).toEqual(2);
|
||||||
|
assertCrash(crashes[0], pluginStateCrash);
|
||||||
|
assertCrash(
|
||||||
|
crashes[1],
|
||||||
getCrash(
|
getCrash(
|
||||||
1,
|
1,
|
||||||
content,
|
content,
|
||||||
'Cannot figure out the cause',
|
'Cannot figure out the cause',
|
||||||
'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);
|
||||||
|
|||||||
@@ -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[
|
) {
|
||||||
|
crash = this.props.persistedState.crashes[
|
||||||
this.props.persistedState.crashes.length - 1
|
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>
|
||||||
|
|||||||
Reference in New Issue
Block a user