Merge #2110 from gasi: Log Uncaught Errors & Unhandled Promise Rejections In Main Process
- [x] Add `electron-unhandled` dependency: - ~~Ensures errors are normalized~~ (disabled to prevent serializing non-errors that are thrown and leaking information) - Distinguishes between main and renderer processes - Allows suppression of error dialog - [x] Log uncaught errors and unhandled promise rejections in main process - [x] Tested using unguarded `throw new TyperError(…)` and `Promise.reject(…)` in `setTimeout` after `app` `ready` event. - [x] Extract `Privacy` module that centralizes how we redact sensitive information such as phone numbers, group IDs, and user file paths. - [x] Add `eslint-plugin-mocha` to disallow exclusive tests using `*.only`. Fixes #2019.pull/1/head
commit
8bd37b7f8d
@ -0,0 +1,17 @@
|
||||
const addUnhandledErrorHandler = require('electron-unhandled');
|
||||
|
||||
const Errors = require('./types/errors');
|
||||
|
||||
|
||||
// addHandler :: Unit -> Unit
|
||||
exports.addHandler = () => {
|
||||
addUnhandledErrorHandler({
|
||||
logger: (error) => {
|
||||
console.error(
|
||||
'Uncaught error or unhandled promise rejection:',
|
||||
Errors.toLogFormat(error)
|
||||
);
|
||||
},
|
||||
showDialog: false,
|
||||
});
|
||||
};
|
@ -0,0 +1,67 @@
|
||||
/* eslint-env node */
|
||||
|
||||
const Path = require('path');
|
||||
|
||||
const compose = require('lodash/fp/compose');
|
||||
const escapeRegExp = require('lodash/escapeRegExp');
|
||||
const isRegExp = require('lodash/isRegExp');
|
||||
const isString = require('lodash/isString');
|
||||
|
||||
|
||||
const PHONE_NUMBER_PATTERN = /\+\d{7,12}(\d{3})/g;
|
||||
const GROUP_ID_PATTERN = /(group\()([^)]+)(\))/g;
|
||||
|
||||
const APP_ROOT_PATH = Path.join(__dirname, '..', '..', '..');
|
||||
const APP_ROOT_PATH_PATTERN = (() => {
|
||||
try {
|
||||
// Safe `String::replaceAll`:
|
||||
// https://github.com/lodash/lodash/issues/1084#issuecomment-86698786
|
||||
return new RegExp(escapeRegExp(APP_ROOT_PATH), 'g');
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
})();
|
||||
|
||||
const REDACTION_PLACEHOLDER = '[REDACTED]';
|
||||
|
||||
// redactPhoneNumbers :: String -> String
|
||||
exports.redactPhoneNumbers = (text) => {
|
||||
if (!isString(text)) {
|
||||
throw new TypeError('`text` must be a string');
|
||||
}
|
||||
|
||||
return text.replace(PHONE_NUMBER_PATTERN, `+${REDACTION_PLACEHOLDER}$1`);
|
||||
};
|
||||
|
||||
// redactGroupIds :: String -> String
|
||||
exports.redactGroupIds = (text) => {
|
||||
if (!isString(text)) {
|
||||
throw new TypeError('`text` must be a string');
|
||||
}
|
||||
|
||||
return text.replace(
|
||||
GROUP_ID_PATTERN,
|
||||
(match, before, id, after) =>
|
||||
`${before}${REDACTION_PLACEHOLDER}${id.slice(-3)}${after}`
|
||||
);
|
||||
};
|
||||
|
||||
// redactSensitivePaths :: String -> String
|
||||
exports.redactSensitivePaths = (text) => {
|
||||
if (!isString(text)) {
|
||||
throw new TypeError('`text` must be a string');
|
||||
}
|
||||
|
||||
if (!isRegExp(APP_ROOT_PATH_PATTERN)) {
|
||||
return text;
|
||||
}
|
||||
|
||||
return text.replace(APP_ROOT_PATH_PATTERN, REDACTION_PLACEHOLDER);
|
||||
};
|
||||
|
||||
// redactAll :: String -> String
|
||||
exports.redactAll = compose(
|
||||
exports.redactSensitivePaths,
|
||||
exports.redactGroupIds,
|
||||
exports.redactPhoneNumbers
|
||||
);
|
@ -0,0 +1,12 @@
|
||||
// toLogFormat :: Error -> String
|
||||
exports.toLogFormat = (error) => {
|
||||
if (!error) {
|
||||
return error;
|
||||
}
|
||||
|
||||
if (error && error.stack) {
|
||||
return error.stack;
|
||||
}
|
||||
|
||||
return error.toString();
|
||||
};
|
@ -0,0 +1,56 @@
|
||||
const Path = require('path');
|
||||
|
||||
const { assert } = require('chai');
|
||||
|
||||
const Privacy = require('../../js/modules/privacy');
|
||||
|
||||
|
||||
const APP_ROOT_PATH = Path.join(__dirname, '..', '..', '..');
|
||||
|
||||
describe('Privacy', () => {
|
||||
describe('redactPhoneNumbers', () => {
|
||||
it('should redact all phone numbers', () => {
|
||||
const text = 'This is a log line with a phone number +12223334455\n' +
|
||||
'and another one +13334445566';
|
||||
|
||||
const actual = Privacy.redactPhoneNumbers(text);
|
||||
const expected = 'This is a log line with a phone number +[REDACTED]455\n' +
|
||||
'and another one +[REDACTED]566';
|
||||
assert.equal(actual, expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('redactGroupIds', () => {
|
||||
it('should redact all group IDs', () => {
|
||||
const text = 'This is a log line with two group IDs: group(123456789)\n' +
|
||||
'and group(abcdefghij)';
|
||||
|
||||
const actual = Privacy.redactGroupIds(text);
|
||||
const expected = 'This is a log line with two group IDs: group([REDACTED]789)\n' +
|
||||
'and group([REDACTED]hij)';
|
||||
assert.equal(actual, expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('redactAll', () => {
|
||||
it('should redact all sensitive information', () => {
|
||||
const text = 'This is a log line with sensitive information:\n' +
|
||||
`path1 ${APP_ROOT_PATH}/main.js\n` +
|
||||
'phone1 +12223334455 ipsum\n' +
|
||||
'group1 group(123456789) doloret\n' +
|
||||
`path2 file:///${APP_ROOT_PATH}/js/background.js.` +
|
||||
'phone2 +13334445566 lorem\n' +
|
||||
'group2 group(abcdefghij) doloret\n';
|
||||
|
||||
const actual = Privacy.redactAll(text);
|
||||
const expected = 'This is a log line with sensitive information:\n' +
|
||||
'path1 [REDACTED]/main.js\n' +
|
||||
'phone1 +[REDACTED]455 ipsum\n' +
|
||||
'group1 group([REDACTED]789) doloret\n' +
|
||||
'path2 file:///[REDACTED]/js/background.js.' +
|
||||
'phone2 +[REDACTED]566 lorem\n' +
|
||||
'group2 group([REDACTED]hij) doloret\n';
|
||||
assert.equal(actual, expected);
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,43 @@
|
||||
const Path = require('path');
|
||||
|
||||
const { assert } = require('chai');
|
||||
|
||||
const Errors = require('../../../js/modules/types/errors');
|
||||
|
||||
|
||||
const APP_ROOT_PATH = Path.join(__dirname, '..', '..', '..');
|
||||
|
||||
describe('Errors', () => {
|
||||
describe('toLogFormat', () => {
|
||||
it('should return error stack trace if present', () => {
|
||||
const error = new Error('boom');
|
||||
assert.typeOf(error, 'Error');
|
||||
|
||||
const formattedError = Errors.toLogFormat(error);
|
||||
assert.include(formattedError, 'errors_test.js');
|
||||
assert.include(formattedError, APP_ROOT_PATH, 'Formatted stack has app path');
|
||||
});
|
||||
|
||||
it('should return error string representation if stack is missing', () => {
|
||||
const error = new Error('boom');
|
||||
error.stack = null;
|
||||
assert.typeOf(error, 'Error');
|
||||
assert.isNull(error.stack);
|
||||
|
||||
const formattedError = Errors.toLogFormat(error);
|
||||
assert.strictEqual(formattedError, 'Error: boom');
|
||||
});
|
||||
|
||||
[
|
||||
0,
|
||||
false,
|
||||
null,
|
||||
undefined,
|
||||
].forEach((value) => {
|
||||
it(`should return \`${value}\` argument`, () => {
|
||||
const formattedNonError = Errors.toLogFormat(value);
|
||||
assert.strictEqual(formattedNonError, value);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue