Plugin folders re-structuring

Summary:
Here I'm changing plugin repository structure to allow re-using of shared packages between both public and fb-internal plugins, and to ensure that public plugins has their own yarn.lock as this will be required to implement reproducible jobs checking plugin compatibility with released flipper versions.

Please note that there are a lot of moved files in this diff, make sure to click "Expand all" to see all that actually changed (there are not much of them actually).

New proposed structure for plugin packages:
```
- root
- node_modules - modules included into Flipper: flipper, flipper-plugin, react, antd, emotion
-- plugins
 --- node_modules - modules used by both public and fb-internal plugins (shared libs will be linked here, see D27034936)
 --- public
---- node_modules - modules used by public plugins
---- pluginA
----- node_modules - modules used by plugin A exclusively
---- pluginB
----- node_modules - modules used by plugin B exclusively
 --- fb
---- node_modules - modules used by fb-internal plugins
---- pluginC
----- node_modules - modules used by plugin C exclusively
---- pluginD
----- node_modules - modules used by plugin D exclusively
```
I've moved all public plugins under dir "plugins/public" and excluded them from root yarn workspaces. Instead, they will have their own yarn workspaces config and yarn.lock and they will use flipper modules as peer dependencies.

Reviewed By: mweststrate

Differential Revision: D27034108

fbshipit-source-id: c2310e3c5bfe7526033f51b46c0ae40199fd7586
This commit is contained in:
Anton Nikolaev
2021-04-09 05:15:14 -07:00
committed by Facebook GitHub Bot
parent 32bf4c32c2
commit b3274a8450
137 changed files with 2133 additions and 371 deletions

View File

@@ -0,0 +1,283 @@
/**
* 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 {BaseDevice} from 'flipper';
import {Crash, shouldShowiOSCrashNotification} from '../index';
import {parseCrashLog, parsePath} from '../index';
import {TestUtils} from 'flipper-plugin';
import {getPluginKey} from 'flipper';
import * as CrashReporterPlugin from '../index';
function getCrash(
id: number,
callstack: string,
name: string,
reason: string,
): Crash {
return {
notificationID: id.toString(),
callstack: callstack,
reason: reason,
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());
}
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', null);
expect(crash.callstack).toEqual(log);
expect(crash.reason).toEqual('SIGSEGV');
expect(crash.name).toEqual('SIGSEGV');
expect(crash.date).toEqual(new Date('2019-03-21 12:07:00.861'));
});
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);
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);
expect(crash.callstack).toEqual(log);
expect(crash.reason).toEqual('Cannot figure out the cause');
expect(crash.name).toEqual('Cannot figure out the cause');
});
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);
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);
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);
expect(crash.callstack).toEqual(log);
expect(crash.reason).toEqual(
'java.lang.IndexOutOfBoundsException: Index: 190, Size: 0',
);
expect(crash.name).toEqual('FATAL EXCEPTION: main');
expect(crash.date).toEqual(date);
});
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);
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 partial format matching the crash format', () => {
const log = 'First Line Break \n Blaa Blaa \n Blaa Blaa ';
const crash = parseCrashLog(log, 'Android', null);
expect(crash.callstack).toEqual(log);
expect(crash.reason).toEqual('Cannot figure out the cause');
expect(crash.name).toEqual('First Line Break ');
});
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', null);
expect(crash.callstack).toEqual(log);
expect(crash.reason).toEqual('Cannot figure out the cause');
expect(crash.name).toEqual('Cannot figure out the cause');
});
test('test the getter of pluginKey with proper input', () => {
const device = new BaseDevice('serial', 'emulator', 'test device', 'iOS');
const pluginKey = getPluginKey(null, device, 'CrashReporter');
expect(pluginKey).toEqual('serial#CrashReporter');
});
test('test the getter of pluginKey with undefined input', () => {
const pluginKey = getPluginKey(null, null, 'CrashReporter');
expect(pluginKey).toEqual('unknown#CrashReporter');
});
test('test the getter of pluginKey with defined selected app', () => {
const pluginKey = getPluginKey('selectedApp', null, 'CrashReporter');
expect(pluginKey).toEqual('selectedApp#CrashReporter');
});
test('test the getter of pluginKey with defined selected app and defined base device', () => {
const device = new BaseDevice('serial', 'emulator', 'test device', 'iOS');
const pluginKey = getPluginKey('selectedApp', device, 'CrashReporter');
expect(pluginKey).toEqual('selectedApp#CrashReporter');
});
test('test defaultPersistedState of CrashReporterPlugin', () => {
expect(
TestUtils.startDevicePlugin(CrashReporterPlugin).exportState(),
).toEqual({crashes: []});
});
test('test helper setdefaultPersistedState function', () => {
const crash = getCrash(0, 'callstack', 'crash0', 'crash0');
const plugin = TestUtils.startDevicePlugin(CrashReporterPlugin);
plugin.instance.reportCrash(crash);
expect(plugin.exportState()).toEqual({crashes: [crash]});
});
test('test getNewPersistedStateFromCrashLog for non-empty defaultPersistedState and defined pluginState', () => {
const crash = getCrash(0, 'callstack', 'crash0', 'crash0');
const plugin = TestUtils.startDevicePlugin(CrashReporterPlugin);
plugin.instance.reportCrash(crash);
const pluginStateCrash = getCrash(1, 'callstack', 'crash1', 'crash1');
plugin.instance.reportCrash(pluginStateCrash);
const crashes = plugin.instance.crashes.get();
expect(crashes).toBeDefined();
expect(crashes.length).toEqual(2);
expect(crashes[1]).toEqual(pluginStateCrash);
});
test('test getNewPersistedStateFromCrashLog for non-empty defaultPersistedState and defined pluginState and improper crash log', () => {
const plugin = TestUtils.startDevicePlugin(CrashReporterPlugin);
const pluginStateCrash = getCrash(0, 'callstack', 'crash1', 'crash1');
plugin.instance.reportCrash(pluginStateCrash);
const content = 'Blaa Blaaa \n Blaa Blaaa';
plugin.instance.reportCrash(parseCrashLog(content, 'iOS', null));
const crashes = plugin.instance.crashes.get();
expect(crashes.length).toEqual(2);
assertCrash(crashes[0], pluginStateCrash);
assertCrash(
crashes[1],
getCrash(
1,
content,
'Cannot figure out the cause',
'Cannot figure out the cause',
),
);
});
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, null));
}).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';
const id = parsePath(content);
expect(id).toEqual('path/to/simulator/TH1S-15DEV1CE-1D/AppName.app/AppName');
});
test('test parsing of path when path has special characters in it', () => {
let content =
'Blaa Blaaa \n Blaa Blaaa \n Path: path/to/simulator/TH1S-15DEV1CE-1D/App Name.app/App Name \n Blaa Blaa \n Blaa Blaa';
let id = parsePath(content);
expect(id).toEqual(
'path/to/simulator/TH1S-15DEV1CE-1D/App Name.app/App Name',
);
content =
'Blaa Blaaa \n Blaa Blaaa \n Path: path/to/simulator/TH1S-15DEV1CE-1D/App_Name.app/App_Name \n Blaa Blaa \n Blaa Blaa';
id = parsePath(content);
expect(id).toEqual(
'path/to/simulator/TH1S-15DEV1CE-1D/App_Name.app/App_Name',
);
content =
'Blaa Blaaa \n Blaa Blaaa \n Path: path/to/simulator/TH1S-15DEV1CE-1D/App%20Name.app/App%20Name \n Blaa Blaa \n Blaa Blaa';
id = parsePath(content);
expect(id).toEqual(
'path/to/simulator/TH1S-15DEV1CE-1D/App%20Name.app/App%20Name',
);
});
test('test parsing of path when a regex is not present', () => {
const content = 'Blaa Blaaa \n Blaa Blaaa \n Blaa Blaa \n Blaa Blaa';
const id = parsePath(content);
expect(id).toEqual(null);
});
test('test shouldShowCrashNotification function for all correct inputs', () => {
const device = new BaseDevice(
'TH1S-15DEV1CE-1D',
'emulator',
'test device',
'iOS',
);
const content =
'Blaa Blaaa \n Blaa Blaaa \n Path: path/to/simulator/TH1S-15DEV1CE-1D/App Name.app/App Name \n Blaa Blaa \n Blaa Blaa';
const shouldShowNotification = shouldShowiOSCrashNotification(
device.serial,
content,
);
expect(shouldShowNotification).toEqual(true);
});
test('test shouldShowiOSCrashNotification function for all correct inputs but incorrect id', () => {
const device = new BaseDevice(
'TH1S-15DEV1CE-1D',
'emulator',
'test device',
'iOS',
);
const content =
'Blaa Blaaa \n Blaa Blaaa \n Path: path/to/simulator/TH1S-1598DEV1CE-2D/App Name.app/App Name \n Blaa Blaa \n Blaa Blaa';
const shouldShowNotification = shouldShowiOSCrashNotification(
device.serial,
content,
);
expect(shouldShowNotification).toEqual(false);
});
test('test shouldShowiOSCrashNotification function for undefined device', () => {
const content =
'Blaa Blaaa \n Blaa Blaaa \n Path: path/to/simulator/TH1S-1598DEV1CE-2D/App Name.app/App Name \n Blaa Blaa \n Blaa Blaa';
const shouldShowNotification = shouldShowiOSCrashNotification(
null as any,
content,
);
expect(shouldShowNotification).toEqual(false);
});
test('only crashes from the correct device are picked up', () => {
const serial = 'AC9482A2-26A4-404F-A179-A9FB60B077F6';
const crash = `Process: Sample [87361]
Path: /Users/USER/Library/Developer/CoreSimulator/Devices/AC9482A2-26A4-404F-A179-A9FB60B077F6/data/Containers/Bundle/Application/9BF91EF9-F915-4745-BE91-EBA397451850/Sample.app/Sample
Identifier: Sample
Version: 1.0 (1)
Code Type: X86-64 (Native)
Parent Process: launchd_sim [70150]
Responsible: SimulatorTrampoline [1246]
User ID: 501`;
expect(shouldShowiOSCrashNotification(serial, crash)).toBe(true);
// wrong serial
expect(
shouldShowiOSCrashNotification(
'XC9482A2-26A4-404F-A179-A9FB60B077F6',
crash,
),
).toBe(false);
});

View File

@@ -0,0 +1,652 @@
/**
* 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 {
View,
styled,
FlexColumn,
FlexRow,
ContextMenu,
clipboard,
Button,
shouldParseAndroidLog,
Text,
colors,
Toolbar,
Spacer,
Select,
} from 'flipper';
import unicodeSubstring from 'unicode-substring';
import fs from 'fs';
import os from 'os';
import path from 'path';
import {promisify} from 'util';
import type {DeviceLogEntry} from 'flipper';
import React from 'react';
import {
createState,
DevicePluginClient,
usePlugin,
useValue,
} from 'flipper-plugin';
import type {FSWatcher} from 'fs';
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;
};
export type Crash = {
notificationID: string;
callstack?: string;
reason: string;
name: string;
date: Date;
};
export type CrashLog = {
callstack: string;
reason: string;
name: string;
date: Maybe<Date>;
};
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,
});
const UNKNOWN_CRASH_REASON = 'Cannot figure out the cause';
export function parseCrashLog(
content: string,
os: string,
logDate: Maybe<Date>,
): CrashLog {
const fallbackReason = UNKNOWN_CRASH_REASON;
switch (os) {
case 'iOS': {
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;
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: Maybe<string> =
tmp1 && tmp1[0].length ? tmp1[0] : null;
date = extractedDateString ? new Date(extractedDateString) : logDate;
}
const crash: CrashLog = {
callstack: content,
name: exception,
reason: exception,
date,
};
return crash;
}
case 'Android': {
const regForName = /.*\n/;
const nameRegArr = regForName.exec(content);
let name = nameRegArr ? nameRegArr[0] : fallbackReason;
const regForCallStack = /\tat[\w\s\n\.$&+,:;=?@#|'<>.^*()%!-]*$/;
const callStackArray = regForCallStack.exec(content);
const callStack = callStackArray ? callStackArray[0] : '';
let remainingString =
callStack.length > 0 ? content.replace(callStack, '') : '';
if (remainingString[remainingString.length - 1] === '\n') {
remainingString = remainingString.slice(0, -1);
}
const reasonText =
remainingString.length > 0
? remainingString.split('\n').pop()
: fallbackReason;
const reason = reasonText ? reasonText : fallbackReason;
if (name[name.length - 1] === '\n') {
name = name.slice(0, -1);
}
const crash: CrashLog = {
callstack: content,
name: name,
reason: reason,
date: logDate,
};
return crash;
}
default: {
throw new Error('Unsupported OS');
}
}
}
function truncate(baseString: string, numOfChars: number): string {
if (baseString.length <= numOfChars) {
return baseString;
}
const truncated_string = unicodeSubstring(baseString, 0, numOfChars - 1);
return truncated_string + '\u2026';
}
export function parsePath(content: string): Maybe<string> {
const regex = /(?<=.*Path: *)[^\n]*/;
const arr = regex.exec(content);
if (!arr || arr.length <= 0) {
return null;
}
const path = arr[0];
return path.trim();
}
function addFileWatcherForiOSCrashLogs(
deviceOs: string,
serial: string,
reportCrash: (payload: CrashLog | Crash) => void,
) {
const dir = path.join(os.homedir(), 'Library', 'Logs', 'DiagnosticReports');
if (!fs.existsSync(dir)) {
// Directory doesn't exist
return;
}
return fs.watch(dir, (_eventType, filename) => {
// We just parse the crash logs with extension `.crash`
const checkFileExtension = /.crash$/.exec(filename);
if (!filename || !checkFileExtension) {
return;
}
const filepath = path.join(dir, filename);
promisify(fs.exists)(filepath).then((exists) => {
if (!exists) {
return;
}
fs.readFile(filepath, 'utf8', function (err, data) {
if (err) {
console.warn('Failed to read crash file', err);
return;
}
if (shouldShowiOSCrashNotification(serial, data)) {
reportCrash(parseCrashLog(data, deviceOs, null));
}
});
});
});
}
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 (onCrashChange && 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 (onCrashChange && 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;
const crashes = createState<Crash[]>([], {persist: 'crashes'});
const selectedCrash = createState<string | undefined>();
client.onDeepLink((crashId) => {
selectedCrash.set(crashId as string);
});
function reportCrash(payload: CrashLog | Crash) {
notificationID++;
const crash = {
notificationID: notificationID.toString(),
callstack: payload.callstack,
name: payload.name,
reason: payload.reason,
date: payload.date || new Date(),
};
crashes.update((draft) => {
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',
});
}
// 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);
}
});
}
}
client.onDestroy(() => {
watcher?.close();
});
return {
reportCrash,
crashes,
selectedCrash,
openInLogs(callstack: string) {
client.selectPlugin('DeviceLogs', callstack);
},
os: client.device.os,
copyCrashToClipboard(callstack: string) {
client.writeTextToClipboard(callstack);
},
};
}
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>
);
}
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 shouldShowiOSCrashNotification(
serial: string,
content: string,
): boolean {
const appPath = parsePath(content);
if (!appPath || !appPath.includes(serial)) {
// Do not show notifications for the app which are not running on this device
return false;
}
return true;
}

View File

@@ -0,0 +1,40 @@
{
"$schema": "https://fbflipper.com/schemas/plugin-package/v2.json",
"name": "flipper-plugin-crash-reporter",
"id": "CrashReporter",
"pluginType": "device",
"supportedDevices": [
{
"os": "Android",
"type": "emulator"
},
{
"os": "Android",
"type": "physical"
},
{
"os": "iOS",
"type": "emulator"
}
],
"version": "0.0.0",
"description": "A plugin which will display a crash",
"main": "dist/bundle.js",
"flipperBundlerEntry": "index.tsx",
"repository": "https://github.com/facebook/flipper",
"license": "MIT",
"keywords": [
"flipper-plugin"
],
"title": "Crash Reporter",
"bugs": {
"email": "prit91@fb.com",
"url": "https://fb.workplace.com/groups/220760072184928/"
},
"dependencies": {
"unicode-substring": "^1.0.0"
},
"peerDependencies": {
"flipper-plugin": "0.0.0"
}
}

View File

@@ -0,0 +1,17 @@
/**
* 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
*/
declare module 'unicode-substring' {
const unicodeSubstring: (
string: string,
start: number,
end: number,
) => string;
export default unicodeSubstring;
}