@contentstack/cli-utilities
Version:
Utilities for contentstack projects
299 lines (298 loc) • 12.6 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.CLIErrorHandler = void 0;
const helpers_1 = require("../helpers");
const errorTypes_1 = require("../constants/errorTypes");
const helpers_2 = require("../helpers");
/**
* Handles errors in a CLI application by classifying, normalizing, and extracting
* relevant information for debugging and logging purposes.
*
* This class provides methods to:
* - Normalize unknown error types into standard `Error` objects.
* - Classify errors into predefined categories such as API errors, network errors,
* server errors, and more.
* - Extract detailed error payloads for logging, including HTTP request and response
* details when applicable.
* - Identify sensitive information in error messages to prevent accidental exposure.
* - Generate debug payloads for enhanced troubleshooting when debugging is enabled.
*
* @remarks
* This class is designed to handle a wide range of error types, including generic
* JavaScript errors, API errors, and custom error objects. It also supports
* optional debugging and context metadata for enhanced error reporting.
*
* @example
* ```typescript
* const errorHandler = new CLIErrorHandler();
*
* try {
* // Some operation that may throw an error
* } catch (error) {
* const classifiedError = errorHandler.classifyError(error, {
* operation: 'fetchData',
* component: 'DataService',
* });
* console.error(classifiedError);
* }
* ```
*
* @public
*/
class CLIErrorHandler {
constructor() { }
/**
* Classifies an error into a structured format for better handling and debugging.
*
* @param error - The error object to classify. Can be of any type.
* @param context - Optional additional context about the error.
* @param errMessage - Optional custom error message to override the default error message.
*
* @returns A `ClassifiedError` object containing essential error details in a clear,
* concise format optimized for debugging.
*/
classifyError(error, context, errMessage) {
try {
const normalized = this.normalizeToError(error);
const type = this.determineErrorType(normalized);
const hidden = this.containsSensitiveInfo(normalized);
const result = {
type,
message: errMessage || this.extractClearMessage(normalized),
error: this.extractErrorPayload(normalized),
meta: this.extractMeta(context, type),
hidden,
};
return result;
}
catch (e) {
return {
type: errorTypes_1.ERROR_TYPES.NORMALIZATION,
message: 'Failed to process error',
error: {
originalError: String(e),
errorType: typeof error,
},
meta: this.extractMeta(context, errorTypes_1.ERROR_TYPES.NORMALIZATION),
hidden: false,
};
}
}
/**
* Extracts a clear, concise error message from various error types.
*/
extractClearMessage(error) {
var _a, _b;
// Use existing formatError function for other cases
try {
const formattedMessage = (0, helpers_1.formatError)(error);
return formattedMessage || 'An error occurred. Please try again.';
}
catch (_c) {
// Fallback to basic error message extraction if formatError fails
if (typeof ((_b = (_a = error === null || error === void 0 ? void 0 : error.response) === null || _a === void 0 ? void 0 : _a.data) === null || _b === void 0 ? void 0 : _b.errorMessage) === 'string') {
return error.response.data.errorMessage;
}
if (typeof (error === null || error === void 0 ? void 0 : error.errorMessage) === 'string') {
return error.errorMessage;
}
}
}
/**
* Normalizes various error types into a standard Error object.
*
* @param error - The error to normalize
* @returns A normalized Error object
*/
normalizeToError(error) {
if (!error)
return new Error('Unknown error occurred');
if (error instanceof Error)
return error;
if (typeof error === 'string')
return new Error(error);
if (typeof error === 'object') {
try {
const errorObj = error;
const message = errorObj.message || errorObj.error || errorObj.statusText || 'Unknown error';
const normalizedError = new Error(message);
// Only copy essential properties
const essentialProps = [
'code',
'status',
'statusText',
'response',
'request',
'message',
'errorMessage',
'error_message',
'error',
'errors',
];
essentialProps.forEach((prop) => {
if (errorObj[prop] !== undefined) {
normalizedError[prop] = errorObj[prop];
}
});
return normalizedError;
}
catch (_a) {
return new Error(JSON.stringify(error));
}
}
return new Error(String(error));
}
/**
* Determines the type of error based on its characteristics.
*/
determineErrorType(error) {
const { status, code, name, response } = error;
const actualStatus = status || (response === null || response === void 0 ? void 0 : response.status);
// Network and timeout errors
if (['ECONNREFUSED', 'ENOTFOUND', 'ETIMEDOUT', 'ENETUNREACH'].includes(code)) {
return errorTypes_1.ERROR_TYPES.NETWORK;
}
// HTTP status-based classification
if (actualStatus) {
if (actualStatus >= 100 && actualStatus < 200)
return errorTypes_1.ERROR_TYPES.INFORMATIONAL;
if (actualStatus >= 300 && actualStatus < 400)
return errorTypes_1.ERROR_TYPES.REDIRECTION;
if (actualStatus >= 400 && actualStatus < 500)
return errorTypes_1.ERROR_TYPES.API_ERROR;
if (actualStatus >= 500)
return errorTypes_1.ERROR_TYPES.SERVER_ERROR;
}
// Specific error types
if (name === 'DatabaseError')
return errorTypes_1.ERROR_TYPES.DATABASE;
if (error.isAxiosError)
return errorTypes_1.ERROR_TYPES.NETWORK;
return errorTypes_1.ERROR_TYPES.APPLICATION;
}
/**
* Extracts only essential error payload information for clear debugging.
*/
extractErrorPayload(error) {
var _a, _b, _c, _d;
const { name, message, code, status, response, request, config, statusText } = error;
const payload = {
name,
message: this.extractClearMessage(error),
};
// Add error identifiers
if (code)
payload.code = code;
if (status || (response === null || response === void 0 ? void 0 : response.status))
payload.status = status || (response === null || response === void 0 ? void 0 : response.status);
// Add detailed field-level errors if available
if (((_a = response === null || response === void 0 ? void 0 : response.data) === null || _a === void 0 ? void 0 : _a.errors) && typeof response.data.errors === 'object') {
payload.errors = response.data.errors;
}
else if ((error === null || error === void 0 ? void 0 : error.errors) && typeof error.errors === 'object') {
payload.errors = error.errors;
}
// Add error code if available
if ((_b = response === null || response === void 0 ? void 0 : response.data) === null || _b === void 0 ? void 0 : _b.error_code) {
payload.errorCode = response.data.error_code;
}
else if (error === null || error === void 0 ? void 0 : error.error_code) {
payload.errorCode = error.error_code;
}
// Add request context with sensitive data redaction
if (request || config) {
const requestData = {
method: (request === null || request === void 0 ? void 0 : request.method) || (config === null || config === void 0 ? void 0 : config.method),
url: (request === null || request === void 0 ? void 0 : request.url) || (config === null || config === void 0 ? void 0 : config.url),
headers: (request === null || request === void 0 ? void 0 : request.headers) || (config === null || config === void 0 ? void 0 : config.headers),
data: (request === null || request === void 0 ? void 0 : request.data) || (config === null || config === void 0 ? void 0 : config.data),
timeout: config === null || config === void 0 ? void 0 : config.timeout,
baseURL: config === null || config === void 0 ? void 0 : config.baseURL,
params: config === null || config === void 0 ? void 0 : config.params,
};
// Use existing redactObject to mask sensitive data
payload.request = (0, helpers_2.redactObject)(requestData);
}
// Add response context with sensitive data redaction
if (response) {
const responseData = {
status,
statusText,
headers: response.headers,
data: response.data,
};
// Use existing redactObject to mask sensitive data
payload.response = (0, helpers_2.redactObject)(responseData);
}
// Extract user-friendly error message for API errors
if ((_c = response === null || response === void 0 ? void 0 : response.data) === null || _c === void 0 ? void 0 : _c.errorMessage) {
payload.userFriendlyMessage = response.data.errorMessage;
}
// Add stack trace only for non-API errors to avoid clutter
if (![errorTypes_1.ERROR_TYPES.API_ERROR, errorTypes_1.ERROR_TYPES.SERVER_ERROR].includes(this.determineErrorType(error))) {
payload.stack = (_d = error.stack) === null || _d === void 0 ? void 0 : _d.split('\n').slice(0, 5).join('\n');
}
return payload;
}
/**
* Extracts metadata from the error context and adds additional information.
*
* @param context - The error context to extract metadata from
* @param errorType - Optional error type to include in metadata
* @returns An object containing relevant metadata for debugging
*/
extractMeta(context, errorType) {
if (!context)
return {};
const baseMeta = {};
if (context.operation)
baseMeta.operation = context.operation;
if (context.component)
baseMeta.component = context.component;
if (context.userId)
baseMeta.userId = context.userId;
if (context.sessionId)
baseMeta.sessionId = context.sessionId;
if (context.orgId)
baseMeta.orgId = context.orgId;
if (errorType)
baseMeta.errorType = errorType;
if (context.email)
baseMeta.email = context.email;
return baseMeta;
}
/**
* Checks if error contains sensitive information.
*
* @param error - Error to check
* @returns True if sensitive info is found
*/
containsSensitiveInfo(error) {
try {
const content = `${error.message} ${error.stack || ''}`.toLowerCase();
const sensitiveTerms = [
'password',
'token',
'secret',
'credentials',
'api_key',
'api-key',
'authorization',
'sessionid',
'authtoken',
'x-api-key',
'tfa_token',
'otp',
'security_code',
'bearer',
'cookie',
];
return sensitiveTerms.some((term) => content.includes(term));
}
catch (_a) {
return false;
}
}
}
exports.default = CLIErrorHandler;
exports.CLIErrorHandler = CLIErrorHandler;