mirror of
https://github.com/serverless/serverless.git
synced 2026-01-25 15:07:39 +00:00
feat: Seclude main error handler to standalone util
This commit is contained in:
parent
b697e667e7
commit
847fa3412d
@ -1,32 +1,11 @@
|
||||
'use strict';
|
||||
|
||||
const chalk = require('chalk');
|
||||
const { inspect } = require('util');
|
||||
const { isError } = require('lodash');
|
||||
const slsVersion = require('./../../package').version;
|
||||
const isStandaloneExecutable = require('../utils/isStandaloneExecutable');
|
||||
const sfeVersion = require('@serverless/enterprise-plugin/package.json').version;
|
||||
const { sdkVersion } = require('@serverless/enterprise-plugin');
|
||||
|
||||
const consoleLog = (message) => {
|
||||
console.log(message); // eslint-disable-line no-console
|
||||
};
|
||||
|
||||
const resolveExceptionMeta = (exception) => {
|
||||
if (isError(exception)) {
|
||||
return {
|
||||
name: exception.name,
|
||||
title: exception.name.replace(/([A-Z])/g, ' $1'),
|
||||
stack: exception.stack,
|
||||
message: exception.message,
|
||||
};
|
||||
}
|
||||
return {
|
||||
title: 'Exception',
|
||||
message: inspect(exception),
|
||||
};
|
||||
};
|
||||
|
||||
const writeMessage = (title, message) => {
|
||||
let line = '';
|
||||
while (line.length < 56 - title.length) {
|
||||
@ -62,75 +41,6 @@ module.exports.ServerlessError = ServerlessError;
|
||||
// Deprecated - use ServerlessError instead
|
||||
module.exports.SError = ServerlessError;
|
||||
|
||||
const userErrorNames = new Set(['ServerlessError', 'YAMLException']);
|
||||
|
||||
module.exports.logError = (exception, { forceExit = false, serverless } = {}) => {
|
||||
const exceptionMeta = resolveExceptionMeta(exception);
|
||||
const isUserError = userErrorNames.has(exceptionMeta.name);
|
||||
|
||||
const possiblyExit = () => {
|
||||
if (forceExit) process.exit();
|
||||
};
|
||||
|
||||
writeMessage(
|
||||
exceptionMeta.title,
|
||||
exceptionMeta.stack && (!isUserError || process.env.SLS_DEBUG)
|
||||
? exceptionMeta.stack
|
||||
: exceptionMeta.message
|
||||
);
|
||||
|
||||
if (!isUserError && !process.env.SLS_DEBUG) {
|
||||
const debugInfo = [
|
||||
' ',
|
||||
' For debugging logs, run again after setting the',
|
||||
' "SLS_DEBUG=*" environment variable.',
|
||||
].join('');
|
||||
consoleLog(chalk.red(debugInfo));
|
||||
consoleLog(' ');
|
||||
}
|
||||
|
||||
const platform = process.platform;
|
||||
const nodeVersion = process.version.replace(/^[v|V]/, '');
|
||||
|
||||
consoleLog(chalk.yellow(' Get Support --------------------------------------------'));
|
||||
consoleLog(`${chalk.yellow(' Docs: ')}${'docs.serverless.com'}`);
|
||||
consoleLog(`${chalk.yellow(' Bugs: ')}${'github.com/serverless/serverless/issues'}`);
|
||||
consoleLog(`${chalk.yellow(' Issues: ')}${'forum.serverless.com'}`);
|
||||
|
||||
consoleLog(' ');
|
||||
consoleLog(chalk.yellow(' Your Environment Information ---------------------------'));
|
||||
consoleLog(chalk.yellow(` Operating System: ${platform}`));
|
||||
consoleLog(chalk.yellow(` Node Version: ${nodeVersion}`));
|
||||
|
||||
const installationModePostfix = (() => {
|
||||
if (isStandaloneExecutable) return ' (standalone)';
|
||||
if (serverless && serverless.isLocallyInstalled) return ' (local)';
|
||||
return '';
|
||||
})();
|
||||
consoleLog(
|
||||
chalk.yellow(` Framework Version: ${slsVersion}${installationModePostfix}`)
|
||||
);
|
||||
consoleLog(chalk.yellow(` Plugin Version: ${sfeVersion}`));
|
||||
consoleLog(chalk.yellow(` SDK Version: ${sdkVersion}`));
|
||||
|
||||
// only show components version if user is running Node 8+
|
||||
const userNodeVersion = Number(process.version.split('.')[0].slice(1));
|
||||
if (userNodeVersion >= 8) {
|
||||
const componentsVersion = (() => {
|
||||
try {
|
||||
return require('@serverless/components/package').version;
|
||||
} catch (error) {
|
||||
return 'Unavailable';
|
||||
}
|
||||
})();
|
||||
consoleLog(chalk.yellow(` Components Version: ${componentsVersion}`));
|
||||
}
|
||||
consoleLog(' ');
|
||||
|
||||
process.exitCode = 1;
|
||||
possiblyExit();
|
||||
};
|
||||
|
||||
module.exports.logWarning = (message) => {
|
||||
if (process.env.SLS_WARNING_DISABLE) {
|
||||
return;
|
||||
|
||||
110
lib/cli/handle-error.js
Normal file
110
lib/cli/handle-error.js
Normal file
@ -0,0 +1,110 @@
|
||||
'use strict';
|
||||
|
||||
const path = require('path');
|
||||
const { inspect } = require('util');
|
||||
const isError = require('type/error/is');
|
||||
const isObject = require('type/object/is');
|
||||
const chalk = require('chalk');
|
||||
const isStandaloneExecutable = require('../utils/isStandaloneExecutable');
|
||||
const resolveLocalServerlessPath = require('./resolve-local-serverless-path');
|
||||
const slsVersion = require('./../../package').version;
|
||||
const sfeVersion = require('@serverless/enterprise-plugin/package.json').version;
|
||||
const { sdkVersion } = require('@serverless/enterprise-plugin');
|
||||
|
||||
const userErrorNames = new Set(['ServerlessError', 'YAMLException']);
|
||||
const serverlessPath = path.resolve(__dirname, '../Serverless.js');
|
||||
|
||||
const resolveExceptionMeta = (exception) => {
|
||||
if (isError(exception)) {
|
||||
return {
|
||||
name: exception.name,
|
||||
title: exception.name.replace(/([A-Z])/g, ' $1'),
|
||||
stack: exception.stack,
|
||||
message: exception.message,
|
||||
};
|
||||
}
|
||||
return {
|
||||
title: 'Exception',
|
||||
message: inspect(exception),
|
||||
};
|
||||
};
|
||||
|
||||
const consoleLog = (message) => process.stdout.write(`${message}\n`);
|
||||
|
||||
const writeMessage = (title, message) => {
|
||||
let line = '';
|
||||
while (line.length < 56 - title.length) {
|
||||
line = `${line}-`;
|
||||
}
|
||||
|
||||
process.stdout.write(' \n');
|
||||
consoleLog(chalk.yellow(` ${title} ${line}`));
|
||||
consoleLog(' ');
|
||||
|
||||
if (message) {
|
||||
consoleLog(` ${message.split('\n').join('\n ')}`);
|
||||
}
|
||||
|
||||
consoleLog(' ');
|
||||
};
|
||||
|
||||
module.exports = async (exception, options = {}) => {
|
||||
if (!isObject(options)) options = {};
|
||||
const { isUncaughtException, isLocallyInstalled } = options;
|
||||
const exceptionMeta = resolveExceptionMeta(exception);
|
||||
const isUserError = !isUncaughtException && userErrorNames.has(exceptionMeta.name);
|
||||
|
||||
writeMessage(
|
||||
exceptionMeta.title,
|
||||
exceptionMeta.stack && (!isUserError || process.env.SLS_DEBUG)
|
||||
? exceptionMeta.stack
|
||||
: exceptionMeta.message
|
||||
);
|
||||
|
||||
if (!isUserError && !process.env.SLS_DEBUG) {
|
||||
const debugInfo = [
|
||||
' ',
|
||||
' For debugging logs, run again after setting the',
|
||||
' "SLS_DEBUG=*" environment variable.',
|
||||
].join('');
|
||||
consoleLog(chalk.red(debugInfo));
|
||||
consoleLog(' ');
|
||||
}
|
||||
|
||||
const platform = process.platform;
|
||||
const nodeVersion = process.version.replace(/^[v|V]/, '');
|
||||
|
||||
consoleLog(chalk.yellow(' Get Support --------------------------------------------'));
|
||||
consoleLog(`${chalk.yellow(' Docs: ')}docs.serverless.com`);
|
||||
consoleLog(`${chalk.yellow(' Bugs: ')}github.com/serverless/serverless/issues`);
|
||||
consoleLog(`${chalk.yellow(' Issues: ')}forum.serverless.com`);
|
||||
|
||||
consoleLog(' ');
|
||||
consoleLog(chalk.yellow(' Your Environment Information ---------------------------'));
|
||||
consoleLog(chalk.yellow(` Operating System: ${platform}`));
|
||||
consoleLog(chalk.yellow(` Node Version: ${nodeVersion}`));
|
||||
|
||||
const installationModePostfix = await (async () => {
|
||||
if (isStandaloneExecutable) return ' (standalone)';
|
||||
if (isLocallyInstalled != null) return isLocallyInstalled ? ' (local)' : '';
|
||||
return serverlessPath === (await resolveLocalServerlessPath()) ? ' (local)' : '';
|
||||
})();
|
||||
consoleLog(
|
||||
chalk.yellow(` Framework Version: ${slsVersion}${installationModePostfix}`)
|
||||
);
|
||||
consoleLog(chalk.yellow(` Plugin Version: ${sfeVersion}`));
|
||||
consoleLog(chalk.yellow(` SDK Version: ${sdkVersion}`));
|
||||
|
||||
const componentsVersion = (() => {
|
||||
try {
|
||||
return require('@serverless/components/package').version;
|
||||
} catch (error) {
|
||||
return 'Unavailable';
|
||||
}
|
||||
})();
|
||||
consoleLog(chalk.yellow(` Components Version: ${componentsVersion}`));
|
||||
consoleLog(' ');
|
||||
|
||||
process.exitCode = 1;
|
||||
if (isUncaughtException) process.exit();
|
||||
};
|
||||
@ -13,56 +13,62 @@ if (require('../lib/utils/tabCompletion/isSupported') && process.argv[2] === 'co
|
||||
return;
|
||||
}
|
||||
|
||||
const logError = require('../lib/classes/Error').logError;
|
||||
const handleError = require('../lib/cli/handle-error');
|
||||
|
||||
let serverless;
|
||||
|
||||
process.on('uncaughtException', (error) => logError(error, { forceExit: true, serverless }));
|
||||
process.once('uncaughtException', (error) =>
|
||||
handleError(error, {
|
||||
isUncaughtException: true,
|
||||
isLocallyInstalled: serverless && serverless.isLocallyInstalled,
|
||||
})
|
||||
);
|
||||
|
||||
const processSpanPromise = (async () => {
|
||||
const wait = require('timers-ext/promise/sleep');
|
||||
await wait(); // Ensure access to "processSpanPromise"
|
||||
require('../lib/utils/analytics').sendPending({
|
||||
serverlessExecutionSpan: processSpanPromise,
|
||||
});
|
||||
|
||||
const BbPromise = require('bluebird');
|
||||
const uuid = require('uuid');
|
||||
|
||||
const invocationId = uuid.v4();
|
||||
if (process.env.SLS_DEBUG) {
|
||||
// For performance reasons enabled only in SLS_DEBUG mode
|
||||
BbPromise.config({
|
||||
longStackTraces: true,
|
||||
});
|
||||
}
|
||||
|
||||
const Serverless = require('../lib/Serverless');
|
||||
serverless = new Serverless();
|
||||
|
||||
try {
|
||||
serverless.onExitPromise = processSpanPromise;
|
||||
serverless.invocationId = invocationId;
|
||||
await serverless.init();
|
||||
if (serverless.invokedInstance) serverless = serverless.invokedInstance;
|
||||
await serverless.run();
|
||||
} catch (error) {
|
||||
// If Enterprise Plugin, capture error
|
||||
let enterpriseErrorHandler = null;
|
||||
serverless.pluginManager.plugins.forEach((p) => {
|
||||
if (p.enterprise && p.enterprise.errorHandler) {
|
||||
enterpriseErrorHandler = p.enterprise.errorHandler;
|
||||
}
|
||||
const wait = require('timers-ext/promise/sleep');
|
||||
await wait(); // Ensure access to "processSpanPromise"
|
||||
require('../lib/utils/analytics').sendPending({
|
||||
serverlessExecutionSpan: processSpanPromise,
|
||||
});
|
||||
if (!enterpriseErrorHandler) {
|
||||
logError(error, { serverless });
|
||||
return;
|
||||
|
||||
const BbPromise = require('bluebird');
|
||||
const uuid = require('uuid');
|
||||
|
||||
const invocationId = uuid.v4();
|
||||
if (process.env.SLS_DEBUG) {
|
||||
// For performance reasons enabled only in SLS_DEBUG mode
|
||||
BbPromise.config({
|
||||
longStackTraces: true,
|
||||
});
|
||||
}
|
||||
|
||||
const Serverless = require('../lib/Serverless');
|
||||
serverless = new Serverless();
|
||||
|
||||
try {
|
||||
await enterpriseErrorHandler(error, invocationId);
|
||||
} catch (enterpriseErrorHandlerError) {
|
||||
process.stdout.write(`${enterpriseErrorHandlerError.stack}\n`);
|
||||
serverless.onExitPromise = processSpanPromise;
|
||||
serverless.invocationId = invocationId;
|
||||
await serverless.init();
|
||||
if (serverless.invokedInstance) serverless = serverless.invokedInstance;
|
||||
await serverless.run();
|
||||
} catch (error) {
|
||||
// If Enterprise Plugin, capture error
|
||||
let enterpriseErrorHandler = null;
|
||||
serverless.pluginManager.plugins.forEach((p) => {
|
||||
if (p.enterprise && p.enterprise.errorHandler) {
|
||||
enterpriseErrorHandler = p.enterprise.errorHandler;
|
||||
}
|
||||
});
|
||||
if (!enterpriseErrorHandler) throw error;
|
||||
try {
|
||||
await enterpriseErrorHandler(error, invocationId);
|
||||
} catch (enterpriseErrorHandlerError) {
|
||||
process.stdout.write(`${enterpriseErrorHandlerError.stack}\n`);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
logError(error, { serverless });
|
||||
} catch (error) {
|
||||
handleError(error);
|
||||
}
|
||||
})();
|
||||
|
||||
@ -2,9 +2,7 @@
|
||||
|
||||
const expect = require('chai').expect;
|
||||
const sandbox = require('sinon');
|
||||
const overrideEnv = require('process-utils/override-env');
|
||||
const ServerlessError = require('../../../../lib/classes/Error').ServerlessError;
|
||||
const logError = require('../../../../lib/classes/Error').logError;
|
||||
const logWarning = require('../../../../lib/classes/Error').logWarning;
|
||||
|
||||
describe('ServerlessError', () => {
|
||||
@ -53,7 +51,7 @@ describe('ServerlessError', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error', () => {
|
||||
describe('#logWarning()', () => {
|
||||
let consoleLogSpy;
|
||||
|
||||
beforeEach(() => {
|
||||
@ -64,117 +62,13 @@ describe('Error', () => {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
describe('#logError()', () => {
|
||||
let restoreEnv;
|
||||
it('should log warning and proceed', () => {
|
||||
logWarning('a message');
|
||||
|
||||
beforeEach(() => ({ restoreEnv } = overrideEnv()));
|
||||
const message = consoleLogSpy.args.join('\n');
|
||||
|
||||
afterEach(() => restoreEnv());
|
||||
|
||||
it('should log error and exit', () => {
|
||||
const error = new ServerlessError('a message', 'a status code');
|
||||
logError(error);
|
||||
|
||||
// TODO @David Not sure how to make async test for this
|
||||
// If tracking enabled, the process exits in a callback and is not defined yet
|
||||
// expect(this.processExitCodes.length).to.be.equal(1);
|
||||
// expect(this.processExitCodes).gt(0);
|
||||
|
||||
const message = consoleLogSpy.args.join('\n');
|
||||
|
||||
expect(consoleLogSpy.called).to.equal(true);
|
||||
expect(message).to.have.string('Serverless Error');
|
||||
expect(message).to.have.string('a message');
|
||||
});
|
||||
|
||||
it('should log environment information', () => {
|
||||
const error = new ServerlessError('a message', 'a status code');
|
||||
logError(error);
|
||||
|
||||
const message = consoleLogSpy.args.join('\n');
|
||||
|
||||
expect(consoleLogSpy.called).to.equal(true);
|
||||
|
||||
expect(message).to.have.string('Serverless Error');
|
||||
expect(message).to.have.string('a message');
|
||||
expect(message).to.have.string('Your Environment Information');
|
||||
expect(message).to.have.string('Operating System:');
|
||||
expect(message).to.have.string('Node Version:');
|
||||
expect(message).to.have.string('Framework Version:');
|
||||
expect(message).to.have.string('Plugin Version:');
|
||||
expect(message).to.have.string('SDK Version:');
|
||||
});
|
||||
|
||||
it('should capture the exception and exit the process with 1 if errorReporter is setup', () => {
|
||||
const error = new Error('an unexpected error');
|
||||
logError(error);
|
||||
|
||||
expect(process.exitCode).to.equal(1);
|
||||
});
|
||||
|
||||
it('should notify about SLS_DEBUG and ask report for unexpected errors', () => {
|
||||
const error = new Error('an unexpected error');
|
||||
logError(error);
|
||||
|
||||
const message = consoleLogSpy.args.join('\n');
|
||||
|
||||
expect(consoleLogSpy.called).to.equal(true);
|
||||
expect(message).to.have.string('SLS_DEBUG=*');
|
||||
});
|
||||
|
||||
it('should hide warnings if SLS_WARNING_DISABLE is defined', () => {
|
||||
process.env.SLS_WARNING_DISABLE = '*';
|
||||
|
||||
logWarning('This is a warning');
|
||||
logWarning('This is another warning');
|
||||
logError(new Error('an error'));
|
||||
|
||||
const message = consoleLogSpy.args.join('\n');
|
||||
|
||||
expect(consoleLogSpy.called).to.equal(true);
|
||||
expect(message).to.have.string('an error');
|
||||
expect(message).not.to.have.string('This is a warning');
|
||||
});
|
||||
|
||||
it('should print stack trace with SLS_DEBUG', () => {
|
||||
process.env.SLS_DEBUG = '1';
|
||||
const error = new ServerlessError('a message');
|
||||
logError(error);
|
||||
|
||||
const message = consoleLogSpy.args.join('\n');
|
||||
|
||||
expect(consoleLogSpy.called).to.equal(true);
|
||||
expect(message).to.have.string(error.stack.split('\n').join('\n '));
|
||||
});
|
||||
|
||||
it('should not print stack trace without SLS_DEBUG', () => {
|
||||
const error = new ServerlessError('a message');
|
||||
logError(error);
|
||||
|
||||
const message = consoleLogSpy.args.join('\n');
|
||||
|
||||
expect(consoleLogSpy.called).to.equal(true);
|
||||
expect(message).to.not.have.string('Stack Trace');
|
||||
expect(message).to.not.have.string(error.stack);
|
||||
});
|
||||
|
||||
it('should handle non-error objects', () => {
|
||||
logError('NON-ERROR INPUT');
|
||||
const message = consoleLogSpy.args.join('\n');
|
||||
|
||||
expect(message).to.have.string('NON-ERROR INPUT');
|
||||
});
|
||||
});
|
||||
|
||||
describe('#logWarning()', () => {
|
||||
it('should log warning and proceed', () => {
|
||||
logWarning('a message');
|
||||
|
||||
const message = consoleLogSpy.args.join('\n');
|
||||
|
||||
expect(consoleLogSpy.called).to.equal(true);
|
||||
expect(message).to.have.string('Serverless Warning');
|
||||
expect(message).to.have.string('a message');
|
||||
});
|
||||
expect(consoleLogSpy.called).to.equal(true);
|
||||
expect(message).to.have.string('Serverless Warning');
|
||||
expect(message).to.have.string('a message');
|
||||
});
|
||||
});
|
||||
|
||||
97
test/unit/lib/cli/handle-error.test.js
Normal file
97
test/unit/lib/cli/handle-error.test.js
Normal file
@ -0,0 +1,97 @@
|
||||
'use strict';
|
||||
|
||||
const { expect } = require('chai');
|
||||
const sinon = require('sinon');
|
||||
|
||||
const path = require('path');
|
||||
const overrideStdoutWrite = require('process-utils/override-stdout-write');
|
||||
const handleError = require('../../../../lib/cli/handle-error');
|
||||
const isStandaloneExecutable = require('../../../../lib/utils/isStandaloneExecutable');
|
||||
const { ServerlessError } = require('../../../../lib/classes/Error');
|
||||
|
||||
describe('test/unit/lib/cli/handle-error.test.js', () => {
|
||||
it('should log environment information', async () => {
|
||||
let stdoutData = '';
|
||||
await overrideStdoutWrite(
|
||||
(data) => (stdoutData += data),
|
||||
() => handleError(new ServerlessError('Test error'))
|
||||
);
|
||||
expect(stdoutData).to.have.string('Serverless Error');
|
||||
expect(stdoutData).to.have.string('Test error');
|
||||
expect(stdoutData).to.have.string('Your Environment Information');
|
||||
expect(stdoutData).to.have.string('Operating System:');
|
||||
expect(stdoutData).to.have.string('Node Version:');
|
||||
expect(stdoutData).to.have.string('Framework Version:');
|
||||
expect(stdoutData).to.have.string('Plugin Version:');
|
||||
expect(stdoutData).to.have.string('Components Version:');
|
||||
});
|
||||
|
||||
it('should support `isUncaughtException` option', async () => {
|
||||
const processExitStub = sinon.stub(process, 'exit').returns();
|
||||
try {
|
||||
let stdoutData = '';
|
||||
await overrideStdoutWrite(
|
||||
(data) => (stdoutData += data),
|
||||
() => handleError(new ServerlessError('Test error'), { isUncaughtException: true })
|
||||
);
|
||||
expect(processExitStub.called).to.be.true;
|
||||
} finally {
|
||||
processExitStub.restore();
|
||||
}
|
||||
});
|
||||
|
||||
if (isStandaloneExecutable) {
|
||||
it('should report standalone installation', async () => {
|
||||
let stdoutData = '';
|
||||
await overrideStdoutWrite(
|
||||
(data) => (stdoutData += data),
|
||||
() => handleError(new ServerlessError('Test error'))
|
||||
);
|
||||
expect(stdoutData).to.have.string('(standalone)');
|
||||
});
|
||||
} else {
|
||||
it('should support `isLocallyInstalled` option', async () => {
|
||||
let stdoutData = '';
|
||||
await overrideStdoutWrite(
|
||||
(data) => (stdoutData += data),
|
||||
() => handleError(new ServerlessError('Test error'), { isLocallyInstalled: false })
|
||||
);
|
||||
expect(stdoutData).to.not.have.string('(local)');
|
||||
stdoutData = '';
|
||||
await overrideStdoutWrite(
|
||||
(data) => (stdoutData += data),
|
||||
() => handleError(new ServerlessError('Test error'), { isLocallyInstalled: true })
|
||||
);
|
||||
expect(stdoutData).to.have.string('(local)');
|
||||
});
|
||||
}
|
||||
|
||||
it('should print stack trace with SLS_DEBUG', async () => {
|
||||
let stdoutData = '';
|
||||
process.env.SLS_DEBUG = '1';
|
||||
await overrideStdoutWrite(
|
||||
(data) => (stdoutData += data),
|
||||
() => handleError(new ServerlessError('Test error'))
|
||||
);
|
||||
expect(stdoutData).to.have.string(path.basename(__filename));
|
||||
});
|
||||
|
||||
it('should not print stack trace without SLS_DEBUG', async () => {
|
||||
let stdoutData = '';
|
||||
delete process.env.SLS_DEBUG;
|
||||
await overrideStdoutWrite(
|
||||
(data) => (stdoutData += data),
|
||||
() => handleError(new ServerlessError('Test error'))
|
||||
);
|
||||
expect(stdoutData).to.not.have.string(path.basename(__filename));
|
||||
});
|
||||
|
||||
it('should handle non-error objects', async () => {
|
||||
let stdoutData = '';
|
||||
await overrideStdoutWrite(
|
||||
(data) => (stdoutData += data),
|
||||
() => handleError('NON-ERROR')
|
||||
);
|
||||
expect(stdoutData).to.have.string('NON-ERROR');
|
||||
});
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user