Improvise UI of crash reporter plugin

Summary:
- New improved UI
- Instead of sending the callstack as a string from android, now sending it as an array
- Deeplink to Logs support just for android. In iOS crash is not automatically logged in Logs plugin, atleast thats what happens in sample app

Reviewed By: jknoxville

Differential Revision: D13216477

fbshipit-source-id: d8b77549c83572d0442e431ce88a8f01f42c9565
This commit is contained in:
Pritesh Nandgaonkar
2018-11-30 05:26:46 -08:00
committed by Facebook Github Bot
parent d37fa7ba95
commit fd022e3c73
8 changed files with 168 additions and 13 deletions

View File

@@ -6,6 +6,9 @@
*/ */
import type {FlipperPlugin, FlipperBasePlugin} from './plugin.js'; import type {FlipperPlugin, FlipperBasePlugin} from './plugin.js';
import {FlipperDevicePlugin} from './plugin.js';
import type BaseDevice from './devices/BaseDevice.js';
import type {App} from './App.js'; import type {App} from './App.js';
import type Logger from './fb-stubs/Logger.js'; import type Logger from './fb-stubs/Logger.js';
import type {Store} from './reducers/index.js'; import type {Store} from './reducers/index.js';
@@ -67,6 +70,12 @@ export default class Client extends EventEmitter {
}, },
}); });
} }
getDevice = (): ?BaseDevice =>
this.store
.getState()
.connections.devices.find(
(device: BaseDevice) => device.serial === this.query.device_id,
);
on: ((event: 'plugins-change', callback: () => void) => void) & on: ((event: 'plugins-change', callback: () => void) => void) &
((event: 'close', callback: () => void) => void); ((event: 'close', callback: () => void) => void);
@@ -157,7 +166,12 @@ export default class Client extends EventEmitter {
this.store.getState().plugins.clientPlugins.get(params.api) || this.store.getState().plugins.clientPlugins.get(params.api) ||
this.store.getState().plugins.devicePlugins.get(params.api); this.store.getState().plugins.devicePlugins.get(params.api);
if (persistingPlugin && persistingPlugin.persistedStateReducer) { if (persistingPlugin && persistingPlugin.persistedStateReducer) {
const pluginKey = `${this.id}#${params.api}`; let pluginKey = `${this.id}#${params.api}`;
//$FlowFixMe
if (persistingPlugin.prototype instanceof FlipperDevicePlugin) {
// For device plugins, we are just using the device id instead of client id as the prefix.
pluginKey = `${this.getDevice()?.serial || ''}#${params.api}`;
}
const persistedState = { const persistedState = {
...persistingPlugin.defaultPersistedState, ...persistingPlugin.defaultPersistedState,
...this.store.getState().pluginStates[pluginKey], ...this.store.getState().pluginStates[pluginKey],

View File

@@ -6,7 +6,7 @@
*/ */
import type {FlipperPlugin, FlipperDevicePlugin} from './plugin.js'; import type {FlipperPlugin, FlipperDevicePlugin} from './plugin.js';
import type LogManager from './fb-stubs/Logger'; import type LogManager from './fb-stubs/Logger';
import type BaseDevice from './devices/BaseDevice.js'; import BaseDevice from './devices/BaseDevice.js';
import type {Props as PluginProps} from './plugin'; import type {Props as PluginProps} from './plugin';
import Client from './Client.js'; import Client from './Client.js';
@@ -128,7 +128,6 @@ class PluginContainer extends Component<Props, State> {
if (!activePlugin || !target) { if (!activePlugin || !target) {
return null; return null;
} }
const props: PluginProps<Object> = { const props: PluginProps<Object> = {
key: pluginKey, key: pluginKey,
logger: this.props.logger, logger: this.props.logger,
@@ -150,6 +149,9 @@ class PluginContainer extends Component<Props, State> {
) { ) {
this.props.selectPlugin({selectedPlugin: pluginID, deepLinkPayload}); this.props.selectPlugin({selectedPlugin: pluginID, deepLinkPayload});
return true; return true;
} else if (target instanceof BaseDevice) {
this.props.selectPlugin({selectedPlugin: pluginID, deepLinkPayload});
return true;
} else { } else {
return false; return false;
} }

View File

@@ -170,6 +170,7 @@ type MainSidebarProps = {|
selectPlugin: (payload: { selectPlugin: (payload: {
selectedPlugin: ?string, selectedPlugin: ?string,
selectedApp: ?string, selectedApp: ?string,
deepLinkPayload: ?string,
}) => void, }) => void,
clients: Array<Client>, clients: Array<Client>,
uninitializedClients: Array<{ uninitializedClients: Array<{
@@ -220,6 +221,7 @@ class MainSidebar extends Component<MainSidebarProps> {
selectPlugin({ selectPlugin({
selectedPlugin: 'notifications', selectedPlugin: 'notifications',
selectedApp: null, selectedApp: null,
deepLinkPayload: null,
}) })
}> }>
<PluginIcon <PluginIcon
@@ -250,6 +252,7 @@ class MainSidebar extends Component<MainSidebarProps> {
selectPlugin({ selectPlugin({
selectedPlugin: plugin.id, selectedPlugin: plugin.id,
selectedApp: null, selectedApp: null,
deepLinkPayload: null,
}) })
} }
plugin={plugin} plugin={plugin}
@@ -282,6 +285,7 @@ class MainSidebar extends Component<MainSidebarProps> {
selectPlugin({ selectPlugin({
selectedPlugin: plugin.id, selectedPlugin: plugin.id,
selectedApp: client.id, selectedApp: client.id,
deepLinkPayload: null,
}) })
} }
plugin={plugin} plugin={plugin}

View File

@@ -30,7 +30,12 @@ export default (store: Store, logger: Logger) => {
}); });
store.dispatch({ store.dispatch({
type: 'CLEAR_PLUGIN_STATE', type: 'CLEAR_PLUGIN_STATE',
payload: id, payload: {
id,
devicePlugins: new Set([
...store.getState().plugins.devicePlugins.keys(),
]),
},
}); });
}); });

View File

@@ -14,8 +14,11 @@ export {
FlipperPlugin, FlipperPlugin,
FlipperDevicePlugin, FlipperDevicePlugin,
} from './plugin.js'; } from './plugin.js';
export {clipboard} from 'electron';
export * from './fb-stubs/constants.js'; export * from './fb-stubs/constants.js';
export * from './utils/createPaste.js';
export {connect} from 'react-redux';
export {selectPlugin} from './reducers/connections';
export { export {
default as SidebarExtensions, default as SidebarExtensions,

View File

@@ -6,7 +6,17 @@
* @flow * @flow
*/ */
import {FlipperDevicePlugin, Device} from 'flipper'; import {
FlipperDevicePlugin,
Device,
View,
styled,
FlexColumn,
FlexRow,
ContextMenu,
clipboard,
Button,
} from 'flipper';
import type {Notification} from '../../plugin'; import type {Notification} from '../../plugin';
type Crash = {| type Crash = {|
@@ -19,6 +29,34 @@ type PersistedState = {|
crashes: Array<Crash>, crashes: Array<Crash>,
|}; |};
const Title = styled(View)({
fontWeight: 'bold',
fontSize: '100%',
color: 'red',
});
const Value = styled(View)({
paddingLeft: '8px',
fontSize: '100%',
fontFamily: 'Monospace',
});
const RootColumn = styled(FlexColumn)({
paddingLeft: '16px',
paddingRight: '16px',
paddingTop: '8px',
});
const CrashRow = styled(FlexRow)({
paddingTop: '8px',
});
const CallStack = styled('pre')({
fontFamily: 'Monospace',
fontSize: '100%',
paddingLeft: '8px',
});
export default class CrashReporterPlugin extends FlipperDevicePlugin { export default class CrashReporterPlugin extends FlipperDevicePlugin {
static title = 'Crash Reporter'; static title = 'Crash Reporter';
static id = 'CrashReporter'; static id = 'CrashReporter';
@@ -67,11 +105,70 @@ export default class CrashReporterPlugin extends FlipperDevicePlugin {
message: crash.callStack, message: crash.callStack,
severity: 'error', severity: 'error',
title: 'CRASH: ' + crash.name + ' ' + crash.reason, title: 'CRASH: ' + crash.name + ' ' + crash.reason,
action: 'Inspect',
}; };
}); });
}; };
convertCallstackToString(crash: Crash): string {
return crash.callStack.reduce((acc, val) => acc.concat('\n', val));
}
render() { render() {
return 'Dedicated space to debug crashes. Look out for crash notifications.'; if (this.props.persistedState.crashes.length > 0) {
const crash = this.props.persistedState.crashes.slice(-1)[0];
const callStackString = this.convertCallstackToString(crash);
return (
<RootColumn>
<CrashRow>
<Title>Name</Title>
<Value>{crash.name}</Value>
</CrashRow>
<CrashRow>
<Title>Reason</Title>
<Value>{crash.reason}</Value>
</CrashRow>
<CrashRow>
<Title>CallStack</Title>
</CrashRow>
<CrashRow>
<ContextMenu
items={[
{
label: 'copy',
click: () => {
clipboard.writeText(callStackString);
},
},
]}>
<CallStack>{callStackString}</CallStack>
</ContextMenu>
</CrashRow>
{this.device.os == 'Android' && (
<CrashRow>
<Button
onClick={event => {
this.props.selectPlugin(
'DeviceLogs',
JSON.stringify({
...crash,
callStackString: callStackString,
}),
);
}}>
Deeplink to Logs
</Button>
</CrashRow>
)}
</RootColumn>
);
}
return (
<RootColumn>
<Title>
Dedicated space to debug crashes. Look out for crash notifications
</Title>
</RootColumn>
);
} }
} }

View File

@@ -280,6 +280,30 @@ export default class LogTable extends FlipperDevicePlugin<
})); }));
}; };
calculateHighlightedRows = (
deeplinkPayload: ?string,
rows: Array<TableBodyRow>,
): Array<string> => {
if (!deeplinkPayload) {
return [];
}
const crash = JSON.parse(deeplinkPayload);
let highlightedRows = rows.filter(x => {
//$FlowFixMe: x.filterValue is not undefined
let matched = x.filterValue.includes(crash.name);
if (!matched) {
return matched;
}
for (let i = 0; i < crash.callStack.length && matched; ++i) {
//$FlowFixMe: x.filterValue is not undefined
matched = x.filterValue.includes(crash.callStack[i]);
}
return matched;
});
highlightedRows = highlightedRows.map(x => x.key);
return [highlightedRows.pop()];
};
state = { state = {
rows: [], rows: [],
entries: [], entries: [],
@@ -390,7 +414,6 @@ export default class LogTable extends FlipperDevicePlugin<
key: String(this.counter++), key: String(this.counter++),
}, },
}; };
// batch up logs to be processed every 250ms, if we have lots of log // batch up logs to be processed every 250ms, if we have lots of log
// messages coming in, then calling an setState 200+ times is actually // messages coming in, then calling an setState 200+ times is actually
// pretty expensive // pretty expensive
@@ -425,11 +448,15 @@ export default class LogTable extends FlipperDevicePlugin<
this.addRowIfNeeded(rows, row, entry, previousEntry); this.addRowIfNeeded(rows, row, entry, previousEntry);
} }
const highlightedRows = this.calculateHighlightedRows(
this.props.deepLinkPayload,
rows,
);
return { return {
entries, entries,
rows, rows,
key2entry, key2entry,
highlightedRows: highlightedRows,
}; };
}); });
}, 100); }, 100);
@@ -547,8 +574,7 @@ export default class LogTable extends FlipperDevicePlugin<
}); });
render() { render() {
const {rows} = this.state; const {rows, highlightedRows} = this.state;
const contextMenuItems = [ const contextMenuItems = [
{ {
type: 'separator', type: 'separator',
@@ -568,6 +594,9 @@ export default class LogTable extends FlipperDevicePlugin<
columnOrder={this.columnOrder} columnOrder={this.columnOrder}
columns={this.columns} columns={this.columns}
rows={rows} rows={rows}
highlightedRows={
highlightedRows ? new Set(highlightedRows) : new Set([])
}
onRowHighlighted={this.onRowHighlighted} onRowHighlighted={this.onRowHighlighted}
multiHighlight={true} multiHighlight={true}
defaultFilters={DEFAULT_FILTERS} defaultFilters={DEFAULT_FILTERS}

View File

@@ -19,7 +19,7 @@ export type Action =
} }
| { | {
type: 'CLEAR_PLUGIN_STATE', type: 'CLEAR_PLUGIN_STATE',
payload: string, payload: {id: string, devicePlugins: Set<string>},
}; };
const INITIAL_STATE: State = {}; const INITIAL_STATE: State = {};
@@ -41,7 +41,8 @@ export default function reducer(
return Object.keys(state).reduce((newState, pluginKey) => { return Object.keys(state).reduce((newState, pluginKey) => {
// Only add the pluginState, if its from a plugin other than the one that // Only add the pluginState, if its from a plugin other than the one that
// was removed. pluginKeys are in the form of ${clientID}#${pluginID}. // was removed. pluginKeys are in the form of ${clientID}#${pluginID}.
if (!pluginKey.startsWith(payload)) { const pluginId = pluginKey.split('#').pop();
if (pluginId !== payload.id || payload.devicePlugins.has(pluginId)) {
newState[pluginKey] = state[pluginKey]; newState[pluginKey] = state[pluginKey];
} }
return newState; return newState;