[next] Error and Logging Improvements (#2152)

* feat: added locale support for Tenant

* feat: added secret scrubbing to logs

* chore: cleanup logger

* chore: logger improvements

* feat: re-introduce scoped pretty logger

* feat: added initial error support

* refactor: replace trace-error.TraceError with talk.InternalError

* fix: fixed error logging

* refactor: replaced Error with VError

* fix: repaired issue with error management on api

* fix: patched bug with not found handler

* feat: added translations

* feat: added location path to invalid entries

* refactor: refactored error handling on graph

* fix: moved indexing operations to master node

* refactor: added throw for when the message isn't found in testing

* fix: removed duplicate log

* fix: fixed naming on environment variable
This commit is contained in:
Wyatt Johnson
2019-02-06 23:42:17 +00:00
committed by GitHub
parent 9b0e6ed53b
commit 9fa5900acc
63 changed files with 1780 additions and 373 deletions
+27
View File
@@ -0,0 +1,27 @@
import { VError } from "verror";
import { DuplicateUserError, InternalError, TalkError } from ".";
it("has the right inheritance chain", () => {
const err = new DuplicateUserError();
expect(err).toBeInstanceOf(DuplicateUserError);
expect(err).toBeInstanceOf(TalkError);
expect(err).toBeInstanceOf(VError);
expect(err).toBeInstanceOf(Error);
expect(err.name).toEqual("DuplicateUserError");
});
it("provides an accurate stack", () => {
const err = new InternalError(
new Error("this is a test"),
"this is the reason"
);
expect(err).toBeInstanceOf(InternalError);
expect(err).toBeInstanceOf(TalkError);
expect(err).toBeInstanceOf(VError);
expect(err).toBeInstanceOf(Error);
expect(err.stack).toBeDefined();
});
+386
View File
@@ -0,0 +1,386 @@
// tslint:disable:max-classes-per-file
import { FluentBundle } from "fluent/compat";
import uuid from "uuid";
import { VError } from "verror";
import { ERROR_CODES } from "talk-common/errors";
import { translate } from "talk-server/services/i18n";
import { ERROR_TRANSLATIONS } from "./translations";
/**
* TalkErrorTypes associates a class of errors with a specific code.
*/
export type TalkErrorTypes = "invalid_request_error";
/**
* TalkErrorExtensions is the different extension data that is associated with
* a given error. This data is surfaced in the GraphQL, REST error response as
* well as via logs.
*/
export interface TalkErrorExtensions {
/**
* id identifies this specific error that was thrown, allowing offline tracing
* to occur.
*/
readonly id: string;
/**
* code is the identifier specific to this Error. No other TalkError should
* share the same code.
*/
readonly code: ERROR_CODES;
/**
* type represents the class of errors that this error is associated with.
*/
readonly type: TalkErrorTypes;
/**
* message is the (optionally translated) message that can be shown to users.
*/
readonly message: string;
/**
* param, if set, references the fieldSpec to which the error is related to.
* If for example an error occurred during email processing, this field could
* be `input.email` to denote the specific input field that caused the error.
*/
param?: string;
}
export interface TalkErrorContext {
/**
* pub stores information that is used by the translation framework
* to provide context to the error being emitted to pass publicly. Sensitive
* information should not be passed via this method.
*/
pub: Record<string, any>;
/**
* pvt stores information that is logged out by the logging
* framework to provide context for bug reporting software in the event that
* the error is unexpected.
*/
pvt: Record<string, any>;
}
/**
* TalkErrorOptions describes the options used to create a TalkError.
*/
export interface TalkErrorOptions {
/**
* code is the identifier specific to this Error. No other TalkError should
* share the same code.
*/
code: ERROR_CODES;
/**
* context stores the public and private details about the error.
*/
context?: Partial<TalkErrorContext>;
/**
* status is the number sent via the REST error responses. GraphQL responses
* do not involve this number.
*/
status?: number;
/**
* type represents the class of errors that this error is associated with.
*/
type?: TalkErrorTypes;
/**
* cause is the error that provides the root cause of the underlying error
* that is thrown.
*/
cause?: Error;
/**
* param, if set, references the fieldSpec to which the error is related to.
* If for example an error occurred during email processing, this field could
* be `input.email` to denote the specific input field that caused the error.
*/
param?: string;
}
export class TalkError extends VError {
/**
* id identifies this specific error that was thrown, allowing offline tracing
* to occur.
*/
public readonly id: string;
/**
* code is the identifier specific to this Error. No other TalkError should
* share the same code.
*/
public readonly code: ERROR_CODES;
/**
* status is the number sent via the REST error responses. GraphQL responses
* do not involve this number.
*/
public readonly status: number;
/**
* type represents the class of errors that this error is associated with.
*/
public readonly type: TalkErrorTypes;
/**
* param, if set, references the fieldSpec to which the error is related to.
* If for example an error occurred during email processing, this field could
* be `input.email` to denote the specific input field that caused the error.
*/
public param?: string;
/**
* context stores the public and private details about the error.
*/
public readonly context: Readonly<TalkErrorContext>;
constructor({
code,
context = {},
status = 500,
type = "invalid_request_error",
cause,
param,
}: TalkErrorOptions) {
// Call the super method with the right arguments depending on if we're
// supposed to be handling a causal error or not.
if (cause) {
super(cause, code);
} else {
super(code);
}
// Rename the error to have the name of the error that this extends.
this.name = new.target.name;
// Assign a unique ID to this error.
const id = uuid.v1();
this.status = status;
// Capture the context for the error.
const { pub = {}, pvt = {} } = context;
this.context = { pub, pvt };
// Capture the extension parameters.
this.id = id;
this.code = code;
this.type = type;
this.param = param;
}
public serializeExtensions(bundle: FluentBundle): TalkErrorExtensions {
const message = translate(
bundle,
this.code,
ERROR_TRANSLATIONS[this.code],
this.context.pub
);
return {
id: this.id,
code: this.code,
type: this.type,
message,
param: this.param,
};
}
}
export class StoryURLInvalidError extends TalkError {
constructor(properties: { storyURL: string; tenantDomains: string[] }) {
super({
code: ERROR_CODES.STORY_URL_NOT_PERMITTED,
context: { pub: properties },
});
}
}
export class DuplicateUserError extends TalkError {
constructor() {
super({ code: ERROR_CODES.DUPLICATE_USER });
}
}
export class EmailNotSetError extends TalkError {
constructor() {
super({ code: ERROR_CODES.EMAIL_NOT_SET });
}
}
export class DuplicateStoryURLError extends TalkError {
constructor(url: string) {
super({ code: ERROR_CODES.DUPLICATE_STORY_URL, context: { pvt: { url } } });
}
}
export class DuplicateUsernameError extends TalkError {
constructor(username: string) {
super({
code: ERROR_CODES.DUPLICATE_USERNAME,
context: { pvt: { username } },
});
}
}
export class DuplicateEmailError extends TalkError {
constructor(email: string) {
super({ code: ERROR_CODES.DUPLICATE_EMAIL, context: { pvt: { email } } });
}
}
export class UsernameAlreadySetError extends TalkError {
constructor() {
super({ code: ERROR_CODES.USERNAME_ALREADY_SET });
}
}
export class EmailAlreadySetError extends TalkError {
constructor() {
super({ code: ERROR_CODES.EMAIL_ALREADY_SET });
}
}
export class LocalProfileNotSetError extends TalkError {
constructor() {
super({ code: ERROR_CODES.LOCAL_PROFILE_NOT_SET });
}
}
export class LocalProfileAlreadySetError extends TalkError {
constructor() {
super({ code: ERROR_CODES.LOCAL_PROFILE_ALREADY_SET });
}
}
export class UsernameContainsInvalidCharactersError extends TalkError {
constructor() {
super({ code: ERROR_CODES.USERNAME_CONTAINS_INVALID_CHARACTERS });
}
}
export class UsernameExceedsMaxLengthError extends TalkError {
constructor(length: number, max: number) {
super({
code: ERROR_CODES.USERNAME_EXCEEDS_MAX_LENGTH,
context: { pub: { length, max } },
});
}
}
export class UsernameTooShortError extends TalkError {
constructor(length: number, min: number) {
super({
code: ERROR_CODES.USERNAME_TOO_SHORT,
context: { pub: { length, min } },
});
}
}
export class DisplayNameExceedsMaxLengthError extends TalkError {
constructor(length: number, max: number) {
super({
code: ERROR_CODES.DISPLAY_NAME_EXCEEDS_MAX_LENGTH,
context: { pub: { length, max } },
});
}
}
export class PasswordTooShortError extends TalkError {
constructor(length: number, min: number) {
super({
code: ERROR_CODES.PASSWORD_TOO_SHORT,
context: { pub: { length, min } },
});
}
}
export class EmailInvalidFormatError extends TalkError {
constructor() {
super({ code: ERROR_CODES.EMAIL_INVALID_FORMAT });
}
}
export class EmailExceedsMaxLengthError extends TalkError {
constructor(length: number, max: number) {
super({
code: ERROR_CODES.EMAIL_EXCEEDS_MAX_LENGTH,
context: { pub: { length, max } },
});
}
}
export class TokenNotFoundError extends TalkError {
constructor() {
super({ code: ERROR_CODES.TOKEN_NOT_FOUND });
}
}
export class TokenInvalidError extends TalkError {
constructor(token: string, reason: string) {
super({
code: ERROR_CODES.TOKEN_INVALID,
context: { pub: { token }, pvt: { reason } },
status: 401,
});
}
}
export class UserForbiddenError extends TalkError {
constructor(reason: string, resource: string, userID: string | null) {
super({
code: ERROR_CODES.USER_NOT_ENTITLED,
context: { pvt: { reason, userID, resource } },
status: 403,
});
}
}
export class UserNotFoundError extends TalkError {
constructor(userID: string) {
super({ code: ERROR_CODES.USER_NOT_FOUND, context: { pvt: { userID } } });
}
}
export class TenantNotFoundError extends TalkError {
constructor(hostname: string) {
super({
code: ERROR_CODES.TENANT_NOT_FOUND,
context: { pub: { hostname } },
});
}
}
export class InternalError extends TalkError {
constructor(cause: Error, reason: string) {
super({
code: ERROR_CODES.INTERNAL_ERROR,
cause,
context: { pvt: { reason } },
status: 500,
});
}
}
export class NotFoundError extends TalkError {
constructor(method: string, path: string) {
super({
code: ERROR_CODES.NOT_FOUND,
status: 404,
context: { pub: { method, path } },
});
}
}
export class TenantInstalledAlreadyError extends TalkError {
constructor() {
super({ code: ERROR_CODES.TENANT_INSTALLED_ALREADY, status: 400 });
}
}
+30
View File
@@ -0,0 +1,30 @@
import { ERROR_CODES } from "talk-common/errors";
export const ERROR_TRANSLATIONS: Record<ERROR_CODES, string> = {
STORY_URL_NOT_PERMITTED: "error-storyURLNotPermitted",
TOKEN_NOT_FOUND: "error-tokenNotFound",
DUPLICATE_STORY_URL: "error-duplicateStoryURL",
EMAIL_ALREADY_SET: "error-emailAlreadySet",
EMAIL_NOT_SET: "error-emailNotSet",
TENANT_NOT_FOUND: "error-tenantNotFound",
DUPLICATE_USER: "error-duplicateUser",
DUPLICATE_USERNAME: "error-duplicateUsername",
DUPLICATE_EMAIL: "error-duplicateEmail",
LOCAL_PROFILE_ALREADY_SET: "error-localProfileAlreadySet",
LOCAL_PROFILE_NOT_SET: "error-localProfileNotSet",
USERNAME_ALREADY_SET: "error-usernameAlreadySet",
USERNAME_CONTAINS_INVALID_CHARACTERS:
"error-usernameContainsInvalidCharacters",
USERNAME_EXCEEDS_MAX_LENGTH: "error-usernameExceedsMaxLength",
USERNAME_TOO_SHORT: "error-usernameTooShort",
PASSWORD_TOO_SHORT: "error-passwordTooShort",
DISPLAY_NAME_EXCEEDS_MAX_LENGTH: "error-displayNameExceedsMaxLength",
EMAIL_INVALID_FORMAT: "error-emailInvalidFormat",
EMAIL_EXCEEDS_MAX_LENGTH: "error-emailExceedsMaxLength",
USER_NOT_FOUND: "error-userNotFound",
NOT_FOUND: "error-notFound",
INTERNAL_ERROR: "error-internalError",
TOKEN_INVALID: "error-tokenInvalid",
TENANT_INSTALLED_ALREADY: "error-tenantInstalledAlready",
USER_NOT_ENTITLED: "error-userNotEntitled",
};