@contentstack/cli-utilities
Version:
Utilities for contentstack projects
218 lines (217 loc) • 9.09 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.CLIErrorHandler = void 0;
const helpers_1 = require("../helpers");
const errorTypes_1 = require("../constants/errorTypes");
/**
* 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(true);
*
* 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(isDebug = false) {
this.isDebug = isDebug;
}
/**
* 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, typically used to provide
* more information about where or why the error occurred.
*
* @returns A `ClassifiedError` object containing details about the error, including its type,
* message, payload, context, metadata, and whether it contains sensitive information.
* If the error is an API error or debugging is enabled, additional debug information
* is included.
*
* @throws This method handles its own errors and will return a `ClassifiedError` with type
* `ERROR_TYPES.NORMALIZATION` if it fails to normalize or classify the input error.
*/
classifyError(error, context) {
try {
const normalized = this.normalizeToError(error);
const isApi = this.isApiError(normalized);
const type = this.determineErrorType(normalized);
const hidden = this.containsSensitiveInfo(normalized);
const result = {
type,
message: normalized.message || 'Unhandled error',
error: this.extractErrorPayload(normalized),
context: context ? JSON.stringify(context) : undefined,
meta: this.extractMeta(context),
hidden,
};
if (isApi || this.isDebug) {
result.debug = this.extractDebugPayload(normalized, context);
}
return result;
}
catch (e) {
return {
type: errorTypes_1.ERROR_TYPES.NORMALIZATION,
message: 'Failed to normalize or classify error',
error: { message: String(e) },
context: context ? JSON.stringify(context) : undefined,
meta: this.extractMeta(context),
hidden: false,
};
}
}
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 msg = error.message;
const err = new Error(msg || 'Unknown error');
Object.assign(err, error);
return err;
}
catch (_a) {
return new Error(JSON.stringify(error));
}
}
return new Error(String(error));
}
isApiError(error) {
return (error.isAxiosError ||
typeof error.status === 'number' ||
typeof error.statusText === 'string' ||
error.request !== undefined);
}
determineErrorType(error) {
var _a;
const status = error.status || ((_a = error.response) === null || _a === void 0 ? void 0 : _a.status);
//Ignore 4XX errors
if (status >= 400 && status < 500) {
return errorTypes_1.ERROR_TYPES.API_ERROR;
}
//Server-side HTTP errors
if (status >= 500) {
return errorTypes_1.ERROR_TYPES.SERVER_ERROR;
}
//Network-related error
if (error.code === 'ECONNREFUSED' || error.code === 'ENOTFOUND' || error.code === 'ETIMEDOUT') {
return errorTypes_1.ERROR_TYPES.NETWORK;
}
//Database error
if (error.name === 'DatabaseError') {
return errorTypes_1.ERROR_TYPES.DATABASE;
}
//Axios errors without 4XX
if (error.isAxiosError) {
return errorTypes_1.ERROR_TYPES.NETWORK;
}
//Default
return errorTypes_1.ERROR_TYPES.APPLICATION;
}
extractErrorPayload(error) {
var _a, _b, _c, _d, _e, _f;
const code = error.code || error.errorCode;
const status = error.status || ((_a = error.response) === null || _a === void 0 ? void 0 : _a.status);
const statusText = error.statusText || ((_b = error.response) === null || _b === void 0 ? void 0 : _b.statusText);
const method = ((_c = error.request) === null || _c === void 0 ? void 0 : _c.method) || ((_d = error.config) === null || _d === void 0 ? void 0 : _d.method) || 'UNKNOWN';
const url = ((_e = error.request) === null || _e === void 0 ? void 0 : _e.url) || ((_f = error.config) === null || _f === void 0 ? void 0 : _f.url);
const endpoint = url ? new URL(url, 'http://dummy').pathname : 'UNKNOWN';
const payload = {
name: error.name,
message: (0, helpers_1.formatError)(error),
code,
status,
statusText,
method,
endpoint,
};
if (this.isDebug) {
payload.stack = error.stack;
}
return payload;
}
extractDebugPayload(error, context) {
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
const method = ((_a = error.request) === null || _a === void 0 ? void 0 : _a.method) || ((_b = error.config) === null || _b === void 0 ? void 0 : _b.method);
const url = ((_c = error.request) === null || _c === void 0 ? void 0 : _c.url) || ((_d = error.config) === null || _d === void 0 ? void 0 : _d.url);
const status = error.status || ((_e = error.response) === null || _e === void 0 ? void 0 : _e.status);
const statusText = error.statusText || ((_f = error.response) === null || _f === void 0 ? void 0 : _f.statusText);
const data = error.data || ((_g = error.response) === null || _g === void 0 ? void 0 : _g.data) || error.errors || error.error;
return {
command: context === null || context === void 0 ? void 0 : context.operation,
module: context === null || context === void 0 ? void 0 : context.component,
request: {
method,
url,
headers: (_h = error.request) === null || _h === void 0 ? void 0 : _h.headers,
data: (_j = error.request) === null || _j === void 0 ? void 0 : _j.data,
},
response: {
status,
statusText,
data,
},
};
}
extractMeta(context) {
return {
email: context === null || context === void 0 ? void 0 : context.email,
sessionId: context === null || context === void 0 ? void 0 : context.sessionId,
userId: context === null || context === void 0 ? void 0 : context.userId,
apiKey: context === null || context === void 0 ? void 0 : context.apiKey,
orgId: context === null || context === void 0 ? void 0 : context.orgId,
};
}
containsSensitiveInfo(error) {
try {
const content = `${error.message} ${error.stack || ''}`.toLowerCase();
return [
'password',
'token',
'secret',
'credentials',
'api_key',
'api-key',
'authorization',
'sessionid',
'email',
].some((term) => content.includes(term));
}
catch (_a) {
return false;
}
}
}
exports.default = CLIErrorHandler;
exports.CLIErrorHandler = CLIErrorHandler;