Convert UI to Sandy

Summary:
With proper notification, components and code clean up in place, time for the reward and giving the plugin a fresh look.

Changelog: CrashReporter plugin got a fresh look and several navigation issues were addressed.

Reviewed By: passy

Differential Revision: D28102398

fbshipit-source-id: 5721634e45c5b1fc5fba3fb0c0b8970635b80b46
This commit is contained in:
Michel Weststrate
2021-05-04 13:49:11 -07:00
committed by Facebook GitHub Bot
parent 01ea822341
commit e707fcc9f9
10 changed files with 246 additions and 514 deletions

View File

@@ -25,12 +25,17 @@ export function Toolbar({
children,
style,
wash,
right,
}: {
children?: React.ReactNode;
position?: 'bottom' | 'top';
compact?: boolean;
wash?: boolean;
style?: React.CSSProperties;
/**
* Additional children that are always right-aligned
*/
right?: React.ReactNode;
}) {
return (
<SandyToolbarContainer
@@ -39,6 +44,12 @@ export function Toolbar({
center
wash={wash}>
{children}
{right ? (
<>
<div style={{flexGrow: 1}}></div>
{right}
</>
) : null}
</SandyToolbarContainer>
);
}

View File

@@ -321,7 +321,7 @@ export const DataSourceRenderer: <T extends object, C>(
}) as any;
const TableContainer = styled.div({
overflowY: 'scroll',
overflowY: 'auto',
overflowX: 'hidden',
display: 'flex',
flex: 1,

View File

@@ -120,7 +120,7 @@ export const TableRow = memo(function TableRow<T>({
.filter((col) => col.visible)
.map((col) => {
const value = col.onRender
? (col as any).onRender(record, highlighted, itemIndex) // TODO: ever used?
? (col as any).onRender(record, highlighted, itemIndex)
: DataFormatter.format((record as any)[col.key], col.formatters);
return (

View File

@@ -0,0 +1,113 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
import React from 'react';
import {Button, Typography} from 'antd';
import {CoffeeOutlined, CopyOutlined, DeleteOutlined} from '@ant-design/icons';
import {
usePlugin,
useValue,
DataList,
Layout,
CodeBlock,
Toolbar,
} from 'flipper-plugin';
import {Crash, devicePlugin} from './index';
const {Text} = Typography;
export function Crashes() {
const plugin = usePlugin(devicePlugin);
const crashes = useValue(plugin.crashes);
const selectedCrashId = useValue(plugin.selectedCrash);
const selectedCrash = crashes.find(
(c) => c.notificationID === selectedCrashId,
);
return (
<Layout.Left resizable width={400}>
<DataList
items={crashes.map((crash) => ({
id: crash.notificationID,
title: crash.reason ?? crash.name,
description: `${crash.date.toLocaleString()} - ${crash.name}`,
}))}
selection={plugin.selectedCrash}
onRenderEmpty={null}
/>
{selectedCrash ? (
<CrashDetails crash={selectedCrash} />
) : (
<Layout.Horizontal center grow>
<Layout.Container center grow gap>
<CoffeeOutlined />
<Text type="secondary">
{crashes.length === 0
? 'No crashes detected so far!'
: 'No crash selected'}
</Text>
</Layout.Container>
</Layout.Horizontal>
)}
</Layout.Left>
);
}
function CrashDetails({crash}: {crash: Crash}) {
const plugin = usePlugin(devicePlugin);
return (
<Layout.Top>
<Toolbar
wash
right={
<Button
onClick={() => {
plugin.clearCrashes();
}}
title="Clear all crashes"
danger>
<DeleteOutlined />
</Button>
}>
<Button
onClick={() => {
plugin.copyCrashToClipboard(crash.callstack!);
}}>
<CopyOutlined />
</Button>
{plugin.isFB ? (
<Button
onClick={() => {
plugin.createPaste(crash.callstack!);
}}>
Create paste
</Button>
) : null}
<Button
disabled={!crash.callstack}
onClick={() => {
plugin.openInLogs(crash.callstack!);
}}>
Open In Logs
</Button>
</Toolbar>
<Layout.ScrollContainer pad vertical>
<CodeBlock>
<Text strong>{crash.name}</Text>
<br />
<br />
<Text strong>{crash.reason}</Text>
<br />
<br />
{crash.callstack}
</CodeBlock>
</Layout.ScrollContainer>
</Layout.Top>
);
}

View File

@@ -12,8 +12,12 @@ import {Crash} from '../index';
import {TestUtils} from 'flipper-plugin';
import {getPluginKey} from 'flipper';
import * as CrashReporterPlugin from '../index';
import {parseCrashLog} from '../crash-utils';
import {parsePath, shouldShowiOSCrashNotification} from '../ios-crash-utils';
import {
parseIosCrash,
parsePath,
shouldShowiOSCrashNotification,
} from '../ios-crash-utils';
import {parseAndroidCrash} from '../android-crash-utils';
function getCrash(
id: number,
@@ -42,7 +46,7 @@ function assertCrash(crash: Crash, expectedCrash: Crash) {
test('test the parsing of the date and crash info for the log which matches the predefined regex', () => {
const log =
'Blaa Blaaa \n Blaa Blaaa \n Exception Type: SIGSEGV \n Blaa Blaa \n Blaa Blaa Date/Time: 2019-03-21 12:07:00.861 +0000 \n Blaa balaaa';
const crash = parseCrashLog(log, 'iOS', undefined);
const crash = parseIosCrash(log);
expect(crash.callstack).toEqual(log);
expect(crash.reason).toEqual('SIGSEGV');
expect(crash.name).toEqual('SIGSEGV');
@@ -52,16 +56,15 @@ test('test the parsing of the date and crash info for the log which matches the
test('test the parsing of the reason for crash when log matches the crash regex, but there is no mention of date', () => {
const log =
'Blaa Blaaa \n Blaa Blaaa \n Exception Type: SIGSEGV \n Blaa Blaa \n Blaa Blaa';
const crash = parseCrashLog(log, 'iOS', undefined);
const crash = parseIosCrash(log);
expect(crash.callstack).toEqual(log);
expect(crash.reason).toEqual('SIGSEGV');
expect(crash.name).toEqual('SIGSEGV');
expect(crash.date).toBeUndefined();
});
test('test the parsing of the crash log when log does not match the predefined regex but is alphanumeric', () => {
const log = 'Blaa Blaaa \n Blaa Blaaa \n Blaa Blaaa';
const crash = parseCrashLog(log, 'iOS', undefined);
const crash = parseIosCrash(log);
expect(crash.callstack).toEqual(log);
expect(crash.reason).toEqual('Cannot figure out the cause');
expect(crash.name).toEqual('Cannot figure out the cause');
@@ -70,25 +73,23 @@ test('test the parsing of the crash log when log does not match the predefined r
test('test the parsing of the reason for crash when log does not match the predefined regex contains unicode character', () => {
const log =
'Blaa Blaaa \n Blaa Blaaa \n Exception Type: 🍕🐬 \n Blaa Blaa \n Blaa Blaa';
const crash = parseCrashLog(log, 'iOS', undefined);
const crash = parseIosCrash(log);
expect(crash.callstack).toEqual(log);
expect(crash.reason).toEqual('Cannot figure out the cause');
expect(crash.name).toEqual('Cannot figure out the cause');
expect(crash.date).toBeUndefined();
});
test('test the parsing of the reason for crash when log is empty', () => {
const log = '';
const crash = parseCrashLog(log, 'iOS', undefined);
const crash = parseIosCrash(log);
expect(crash.callstack).toEqual(log);
expect(crash.reason).toEqual('Cannot figure out the cause');
expect(crash.name).toEqual('Cannot figure out the cause');
expect(crash.date).toBeUndefined();
});
test('test the parsing of the Android crash log for the proper android crash format', () => {
const log =
'FATAL EXCEPTION: main\nProcess: com.facebook.flipper.sample, PID: 27026\njava.lang.IndexOutOfBoundsException: Index: 190, Size: 0\n\tat java.util.ArrayList.get(ArrayList.java:437)\n\tat com.facebook.flipper.sample.RootComponentSpec.hitGetRequest(RootComponentSpec.java:72)\n\tat com.facebook.flipper.sample.RootComponent.hitGetRequest(RootComponent.java:46)\n';
const date = new Date();
const crash = parseCrashLog(log, 'Android', date);
const crash = parseAndroidCrash(log, date);
expect(crash.callstack).toEqual(log);
expect(crash.reason).toEqual(
'java.lang.IndexOutOfBoundsException: Index: 190, Size: 0',
@@ -98,7 +99,7 @@ test('test the parsing of the Android crash log for the proper android crash for
});
test('test the parsing of the Android crash log for the unknown crash format and no date', () => {
const log = 'Blaa Blaa Blaa';
const crash = parseCrashLog(log, 'Android', undefined);
const crash = parseAndroidCrash(log, undefined);
expect(crash.callstack).toEqual(log);
expect(crash.reason).toEqual('Cannot figure out the cause');
expect(crash.name).toEqual('Cannot figure out the cause');
@@ -106,7 +107,7 @@ test('test the parsing of the Android crash log for the unknown crash format and
});
test('test the parsing of the Android crash log for the partial format matching the crash format', () => {
const log = 'First Line Break \n Blaa Blaa \n Blaa Blaa ';
const crash = parseCrashLog(log, 'Android', undefined);
const crash = parseAndroidCrash(log, undefined);
expect(crash.callstack).toEqual(log);
expect(crash.reason).toEqual('Cannot figure out the cause');
expect(crash.name).toEqual('First Line Break ');
@@ -114,7 +115,7 @@ test('test the parsing of the Android crash log for the partial format matching
test('test the parsing of the Android crash log with os being iOS', () => {
const log =
'FATAL EXCEPTION: main\nProcess: com.facebook.flipper.sample, PID: 27026\njava.lang.IndexOutOfBoundsException: Index: 190, Size: 0\n\tat java.util.ArrayList.get(ArrayList.java:437)\n\tat com.facebook.flipper.sample.RootComponentSpec.hitGetRequest(RootComponentSpec.java:72)\n\tat com.facebook.flipper.sample.RootComponent.hitGetRequest(RootComponent.java:46)\n';
const crash = parseCrashLog(log, 'iOS', undefined);
const crash = parseIosCrash(log);
expect(crash.callstack).toEqual(log);
expect(crash.reason).toEqual('Cannot figure out the cause');
expect(crash.name).toEqual('Cannot figure out the cause');
@@ -165,7 +166,7 @@ test('test getNewPersistedStateFromCrashLog for non-empty defaultPersistedState
const pluginStateCrash = getCrash(0, 'callstack', 'crash1', 'crash1');
plugin.instance.reportCrash(pluginStateCrash);
const content = 'Blaa Blaaa \n Blaa Blaaa';
plugin.instance.reportCrash(parseCrashLog(content, 'iOS', undefined));
plugin.instance.reportCrash(parseIosCrash(content));
const crashes = plugin.instance.crashes.get();
expect(crashes.length).toEqual(2);
assertCrash(crashes[0], pluginStateCrash);
@@ -180,18 +181,6 @@ test('test getNewPersistedStateFromCrashLog for non-empty defaultPersistedState
);
});
test('test getNewPersistedStateFromCrashLog when os is undefined', () => {
const plugin = TestUtils.startDevicePlugin(CrashReporterPlugin);
const content = 'Blaa Blaaa \n Blaa Blaaa';
expect(() => {
plugin.instance.reportCrash(
parseCrashLog(content, undefined as any, undefined),
);
}).toThrowErrorMatchingInlineSnapshot(`"Unsupported OS"`);
const crashes = plugin.instance.crashes.get();
expect(crashes.length).toEqual(0);
});
test('test parsing of path when inputs are correct', () => {
const content =
'Blaa Blaaa \n Blaa Blaaa \n Path: path/to/simulator/TH1S-15DEV1CE-1D/AppName.app/AppName \n Blaa Blaa \n Blaa Blaa';

View File

@@ -7,17 +7,14 @@
* @format
*/
import type {DeviceLogEntry} from 'flipper-plugin';
import type {CrashLog} from './index';
import type {DeviceLogEntry, DevicePluginClient} from 'flipper-plugin';
import {UNKNOWN_CRASH_REASON} from './crash-utils';
import type {Crash, CrashLog} from './index';
export function parseAndroidCrash(
content: string,
fallbackReason: string,
logDate?: Date,
) {
export function parseAndroidCrash(content: string, logDate?: Date) {
const regForName = /.*\n/;
const nameRegArr = regForName.exec(content);
let name = nameRegArr ? nameRegArr[0] : fallbackReason;
let name = nameRegArr ? nameRegArr[0] : UNKNOWN_CRASH_REASON;
const regForCallStack = /\tat[\w\s\n\.$&+,:;=?@#|'<>.^*()%!-]*$/;
const callStackArray = regForCallStack.exec(content);
const callStack = callStackArray ? callStackArray[0] : '';
@@ -29,8 +26,8 @@ export function parseAndroidCrash(
const reasonText =
remainingString.length > 0
? remainingString.split('\n').pop()
: fallbackReason;
const reason = reasonText ? reasonText : fallbackReason;
: UNKNOWN_CRASH_REASON;
const reason = reasonText ? reasonText : UNKNOWN_CRASH_REASON;
if (name[name.length - 1] === '\n') {
name = name.slice(0, -1);
}
@@ -53,3 +50,34 @@ export function shouldParseAndroidLog(
entry.type === 'fatal')
);
}
export function startAndroidCrashWatcher(
client: DevicePluginClient,
reportCrash: (payload: CrashLog | Crash) => void,
) {
const referenceDate = new Date();
let androidLog: string = '';
let androidLogUnderProcess = false;
let timer: null | NodeJS.Timeout = null;
client.device.onLogEntry((entry: DeviceLogEntry) => {
if (shouldParseAndroidLog(entry, referenceDate)) {
if (androidLogUnderProcess) {
androidLog += '\n' + entry.message;
androidLog = androidLog.trim();
if (timer) {
clearTimeout(timer);
}
} else {
androidLog = entry.message;
androidLogUnderProcess = true;
}
timer = setTimeout(() => {
if (androidLog.length > 0) {
reportCrash(parseAndroidCrash(androidLog, entry.date));
}
androidLogUnderProcess = false;
androidLog = '';
}, 50);
}
});
}

View File

@@ -8,32 +8,12 @@
*/
import unicodeSubstring from 'unicode-substring';
import type {CrashLog} from './index';
import {parseAndroidCrash} from './android-crash-utils';
import {parseIosCrash} from './ios-crash-utils';
import type {Crash} from './index';
import {DevicePluginClient} from 'flipper-plugin';
export const UNKNOWN_CRASH_REASON = 'Cannot figure out the cause';
export function parseCrashLog(
content: string,
os: string,
logDate?: Date,
): CrashLog {
const fallbackReason = UNKNOWN_CRASH_REASON;
switch (os) {
case 'iOS': {
return parseIosCrash(content, fallbackReason, logDate);
}
case 'Android': {
return parseAndroidCrash(content, fallbackReason, logDate);
}
default: {
throw new Error('Unsupported OS');
}
}
}
export function truncate(baseString: string, numOfChars: number): string {
function truncate(baseString: string, numOfChars: number): string {
if (baseString.length <= numOfChars) {
return baseString;
}
@@ -41,8 +21,40 @@ export function truncate(baseString: string, numOfChars: number): string {
return truncated_string + '\u2026';
}
export function trimCallStackIfPossible(callstack: string): string {
function trimCallStackIfPossible(callstack: string): string {
const regex = /Application Specific Information:/;
const query = regex.exec(callstack);
return query ? callstack.substring(0, query.index) : callstack;
}
export function showCrashNotification(
client: DevicePluginClient,
crash: Crash,
) {
const ignore = !crash.name && !crash.reason;
const unknownCrashCause = crash.reason === UNKNOWN_CRASH_REASON;
if (ignore || unknownCrashCause) {
console.warn('Ignored the notification for the crash', crash);
return;
}
let title: string = 'CRASH: ' + truncate(crash.name || crash.reason, 50);
title = `${
crash.name == crash.reason
? title
: title + 'Reason: ' + truncate(crash.reason, 50)
}`;
const callstack = crash.callstack
? trimCallStackIfPossible(crash.callstack)
: 'No callstack available';
const msg = `Callstack: ${truncate(callstack, 200)}`;
// TODO: fix client id
client.showNotification({
id: crash.notificationID,
message: msg,
severity: 'error',
title: title,
action: crash.notificationID,
category: crash.reason || 'Unknown reason',
});
}

View File

@@ -7,57 +7,11 @@
* @format
*/
import {
View,
styled,
FlexColumn,
FlexRow,
ContextMenu,
clipboard,
Button,
Text,
colors,
Toolbar,
Spacer,
Select,
} from 'flipper';
import React from 'react';
import {
createState,
DeviceLogEntry,
DevicePluginClient,
usePlugin,
useValue,
} from 'flipper-plugin';
import type {FSWatcher} from 'fs';
import {
parseCrashLog,
trimCallStackIfPossible,
truncate,
UNKNOWN_CRASH_REASON,
} from './crash-utils';
import {createState, DevicePluginClient} from 'flipper-plugin';
import {showCrashNotification} from './crash-utils';
import {addFileWatcherForiOSCrashLogs} from './ios-crash-utils';
import {shouldParseAndroidLog} from './android-crash-utils';
type Maybe<T> = T | null | undefined;
type HeaderRowProps = {
title: string;
value: string;
};
type openLogsCallbackType = () => void;
type CrashReporterBarProps = {
openLogsCallback?: openLogsCallbackType;
crashSelector: CrashSelectorProps;
};
type CrashSelectorProps = {
crashes?: {[key: string]: string};
orderedIDs?: Array<string>;
selectedCrashID?: string;
onCrashChange: (name: Maybe<string>) => void;
};
import {startAndroidCrashWatcher} from './android-crash-utils';
export type Crash = {
notificationID: string;
@@ -71,235 +25,9 @@ export type CrashLog = {
callstack: string;
reason: string;
name: string;
date: Maybe<Date>;
date?: Date | null;
};
const Padder = styled.div<{
paddingLeft?: number;
paddingRight?: number;
paddingBottom?: number;
paddingTop?: number;
}>(({paddingLeft, paddingRight, paddingBottom, paddingTop}) => ({
paddingLeft: paddingLeft || 0,
paddingRight: paddingRight || 0,
paddingBottom: paddingBottom || 0,
paddingTop: paddingTop || 0,
}));
const Title = styled(Text)({
fontWeight: 'bold',
color: colors.greyTint3,
height: 'auto',
width: 200,
textOverflow: 'ellipsis',
});
const Line = styled(View)({
backgroundColor: colors.greyTint2,
height: 1,
width: 'auto',
marginTop: 2,
flexShrink: 0,
});
const Container = styled(FlexColumn)({
overflow: 'auto',
flexShrink: 0,
});
const Value = styled(Text)({
fontWeight: 'bold',
color: colors.greyTint3,
height: 'auto',
maxHeight: 200,
flexGrow: 1,
textOverflow: 'ellipsis',
whiteSpace: 'normal',
wordWrap: 'break-word',
lineHeight: 2,
marginLeft: 8,
marginRight: 8,
overflow: 'hidden',
});
const FlexGrowColumn = styled(FlexColumn)({
flexGrow: 1,
});
const PluginRootContainer = styled(FlexColumn)({
height: '100%',
});
const ScrollableColumn = styled(FlexGrowColumn)({
overflow: 'auto',
height: 'auto',
});
const StyledFlexGrowColumn = styled(FlexColumn)({
flexGrow: 1,
});
const StyledFlexRowColumn = styled(FlexRow)({
aligItems: 'center',
justifyContent: 'center',
height: '100%',
});
const StyledFlexColumn = styled(StyledFlexGrowColumn)({
justifyContent: '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,
});
const StackTraceContainer = styled(FlexColumn)({
backgroundColor: colors.greyStackTraceTint,
flexShrink: 0,
});
class CrashSelector extends React.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 (orderedIDs && selectedCrashID) {
const index = orderedIDs.indexOf(selectedCrashID as string);
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 (orderedIDs && selectedCrashID) {
const index = orderedIDs.indexOf(selectedCrashID as string);
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'}}
onChangeWithKey={(key: string) => {
if (onCrashChange) {
onCrashChange(key);
}
}}
/>
</StyledSelectContainer>
</StyledFlexRowColumn>
);
}
}
class CrashReporterBar extends React.Component<CrashReporterBarProps> {
render() {
const {openLogsCallback, crashSelector} = this.props;
return (
<Toolbar>
<CrashSelector {...crashSelector} />
<Spacer />
<Button
disabled={Boolean(!openLogsCallback)}
onClick={openLogsCallback}>
Open In Logs
</Button>
</Toolbar>
);
}
}
class HeaderRow extends React.Component<HeaderRowProps> {
render() {
const {title, value} = this.props;
return (
<Padder paddingTop={8} paddingBottom={2} paddingLeft={8}>
<Container>
<FlexRow>
<Title>{title}</Title>
<ContextMenu
items={[
{
label: 'copy',
click: () => {
clipboard.writeText(value);
},
},
]}>
<Value code={true}>{value}</Value>
</ContextMenu>
</FlexRow>
<Line />
</Container>
</Padder>
);
}
}
type StackTraceComponentProps = {
stacktrace: string;
};
class StackTraceComponent extends React.Component<StackTraceComponentProps> {
render() {
const {stacktrace} = this.props;
return (
<StackTraceContainer>
<Padder paddingTop={8} paddingBottom={2} paddingLeft={8}>
<Value code={true}>{stacktrace}</Value>
</Padder>
<Line />
</StackTraceContainer>
);
}
}
export function devicePlugin(client: DevicePluginClient) {
let notificationID = -1;
let watcher: FSWatcher | undefined;
@@ -326,70 +54,18 @@ export function devicePlugin(client: DevicePluginClient) {
draft.push(crash);
});
// show notification?
const ignore = !crash.name && !crash.reason;
const unknownCrashCause = crash.reason === UNKNOWN_CRASH_REASON;
if (ignore || unknownCrashCause) {
console.warn('Ignored the notification for the crash', crash);
return;
}
let title: string = 'CRASH: ' + truncate(crash.name || crash.reason, 50);
title = `${
crash.name == crash.reason
? title
: title + 'Reason: ' + truncate(crash.reason, 50)
}`;
const callstack = crash.callstack
? trimCallStackIfPossible(crash.callstack)
: 'No callstack available';
const msg = `Callstack: ${truncate(callstack, 200)}`;
client.showNotification({
id: crash.notificationID,
message: msg,
severity: 'error',
title: title,
action: crash.notificationID,
category: crash.reason || 'Unknown reason',
});
showCrashNotification(client, crash);
}
// Startup logic to establish log monitoring
if (client.device.isConnected) {
if (client.device.os.includes('iOS')) {
watcher = addFileWatcherForiOSCrashLogs(
client.device.os,
client.device.serial,
reportCrash,
);
} else {
const referenceDate = new Date();
let androidLog: string = '';
let androidLogUnderProcess = false;
let timer: Maybe<NodeJS.Timeout> = null;
client.device.onLogEntry((entry: DeviceLogEntry) => {
if (shouldParseAndroidLog(entry, referenceDate)) {
if (androidLogUnderProcess) {
androidLog += '\n' + entry.message;
androidLog = androidLog.trim();
if (timer) {
clearTimeout(timer);
}
} else {
androidLog = entry.message;
androidLogUnderProcess = true;
}
timer = setTimeout(() => {
if (androidLog.length > 0) {
reportCrash(
parseCrashLog(androidLog, client.device.os, entry.date),
);
}
androidLogUnderProcess = false;
androidLog = '';
}, 50);
}
});
startAndroidCrashWatcher(client, reportCrash);
}
}
@@ -408,106 +84,15 @@ export function devicePlugin(client: DevicePluginClient) {
copyCrashToClipboard(callstack: string) {
client.writeTextToClipboard(callstack);
},
createPaste(callstack: string) {
client.createPaste(callstack);
},
isFB: client.isFB,
clearCrashes() {
crashes.set([]);
selectedCrash.set(undefined);
},
};
}
export function Component() {
const plugin = usePlugin(devicePlugin);
const selectedCrash = useValue(plugin.selectedCrash);
const crashes = useValue(plugin.crashes);
const crash =
crashes.find((c) => c.notificationID === selectedCrash) ??
crashes[crashes.length - 1] ??
undefined;
if (crash) {
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: Maybe<string>) => {
if (id) {
plugin.selectedCrash.set(id);
}
};
const callstackString = crash.callstack || '';
const children = callstackString.split('\n').map((str) => {
return {message: str};
});
const crashSelector: CrashSelectorProps = {
crashes: crashMap,
orderedIDs,
selectedCrashID,
onCrashChange,
};
const showReason = crash.reason !== UNKNOWN_CRASH_REASON;
return (
<PluginRootContainer>
{plugin.os == 'Android' ? (
<CrashReporterBar
crashSelector={crashSelector}
openLogsCallback={() => {
if (crash.callstack) {
plugin.openInLogs(crash.callstack);
}
}}
/>
) : (
<CrashReporterBar crashSelector={crashSelector} />
)}
<ScrollableColumn>
<HeaderRow title="Name" value={crash.name} />
{showReason ? (
<HeaderRow title="Reason" value={crash.reason} />
) : null}
<Padder paddingLeft={8} paddingTop={4} paddingBottom={2}>
<Title> Stacktrace </Title>
</Padder>
<ContextMenu
items={[
{
label: 'copy',
click: () => {
plugin.copyCrashToClipboard(callstackString);
},
},
]}>
<Line />
{children.map((child, index) => {
return (
<StackTraceComponent key={index} stacktrace={child.message} />
);
})}
</ContextMenu>
</ScrollableColumn>
</PluginRootContainer>
);
}
const crashSelector = {
crashes: undefined,
orderedIDs: undefined,
selectedCrashID: undefined,
onCrashChange: () => void {},
};
return (
<StyledFlexGrowColumn>
<CrashReporterBar crashSelector={crashSelector} />
<StyledFlexColumn>
<Padder paddingBottom={8}>
<Title>No Crashes Logged</Title>
</Padder>
</StyledFlexColumn>
</StyledFlexGrowColumn>
);
}
export {Crashes as Component} from './Crashes';

View File

@@ -8,35 +8,28 @@
*/
import type {Crash, CrashLog} from './index';
import {parseCrashLog} from './crash-utils';
import fs from 'fs';
import os from 'os';
import path from 'path';
import {promisify} from 'util';
import {UNKNOWN_CRASH_REASON} from './crash-utils';
export function parseIosCrash(
content: string,
fallbackReason: string,
logDate?: Date,
) {
export function parseIosCrash(content: string) {
const regex = /Exception Type: *\w*/;
const arr = regex.exec(content);
const exceptionString = arr ? arr[0] : '';
const exceptionRegex = /\w*$/;
const tmp = exceptionRegex.exec(exceptionString);
const exception = tmp && tmp[0].length ? tmp[0] : fallbackReason;
const exception = tmp && tmp[0].length ? tmp[0] : UNKNOWN_CRASH_REASON;
let date = logDate;
if (!date) {
const dateRegex = /Date\/Time: *[\w\s\.:-]*/;
const dateArr = dateRegex.exec(content);
const dateString = dateArr ? dateArr[0] : '';
const dateRegex2 = /[\w\s\.:-]*$/;
const tmp1 = dateRegex2.exec(dateString);
const extractedDateString: string | null =
tmp1 && tmp1[0].length ? tmp1[0] : null;
date = extractedDateString ? new Date(extractedDateString) : logDate;
}
const dateRegex = /Date\/Time: *[\w\s\.:-]*/;
const dateArr = dateRegex.exec(content);
const dateString = dateArr ? dateArr[0] : '';
const dateRegex2 = /[\w\s\.:-]*$/;
const tmp1 = dateRegex2.exec(dateString);
const extractedDateString: string | null =
tmp1 && tmp1[0].length ? tmp1[0] : null;
const date = extractedDateString ? new Date(extractedDateString) : new Date();
const crash: CrashLog = {
callstack: content,
@@ -70,7 +63,6 @@ export function parsePath(content: string): string | null {
}
export function addFileWatcherForiOSCrashLogs(
deviceOs: string,
serial: string,
reportCrash: (payload: CrashLog | Crash) => void,
) {
@@ -96,7 +88,7 @@ export function addFileWatcherForiOSCrashLogs(
return;
}
if (shouldShowiOSCrashNotification(serial, data)) {
reportCrash(parseCrashLog(data, deviceOs, undefined));
reportCrash(parseIosCrash(data));
}
});
});

View File

@@ -35,6 +35,8 @@
"unicode-substring": "^1.0.0"
},
"peerDependencies": {
"flipper-plugin": "0.0.0"
"@ant-design/icons": "*",
"ant-design": "*",
"flipper-plugin": "*"
}
}