Improve sentry grouping (#12007)

This PR attemps at improving sentry grouping and filtering by 
- Using the exceptionCode as the fingerprint when the error is a
customException. For this to work in this PR we are now throwing
customExceptions instead of internalServerError deprived of their code.
They will still be converted to Internal server errors when sent back as
response
- Filtering 4xx issues where it was missing (for emailVerification
because errors were not handled, for invalid captcha and billing errors
because they are httpErrors and not graphqlErrors)

---------

Co-authored-by: Félix Malfait <felix@twenty.com>
This commit is contained in:
Marie
2025-05-14 11:00:06 +02:00
committed by GitHub
parent 6a1da5fe53
commit 9c2b88870f
57 changed files with 376 additions and 152 deletions

View File

@ -2,11 +2,17 @@ import camelCase from 'lodash.camelcase';
import { slugify } from 'transliteration';
import { isDefined } from 'twenty-shared/utils';
import { InvalidMetadataException } from 'src/engine/metadata-modules/utils/exceptions/invalid-metadata.exception';
import {
InvalidMetadataException,
InvalidMetadataExceptionCode,
} from 'src/engine/metadata-modules/utils/exceptions/invalid-metadata.exception';
export const computeMetadataNameFromLabel = (label: string): string => {
if (!isDefined(label)) {
throw new InvalidMetadataException('Label is required');
throw new InvalidMetadataException(
'Label is required',
InvalidMetadataExceptionCode.LABEL_REQUIRED,
);
}
const prefixedLabel = /^\d/.test(label) ? `n${label}` : label;
@ -22,7 +28,10 @@ export const computeMetadataNameFromLabel = (label: string): string => {
});
if (formattedString === '') {
throw new InvalidMetadataException(`Invalid label: "${label}"`);
throw new InvalidMetadataException(
`Invalid label: "${label}"`,
InvalidMetadataExceptionCode.INVALID_LABEL,
);
}
return camelCase(formattedString);

View File

@ -1,5 +0,0 @@
export class InvalidMetadataNameException extends Error {
constructor(message: string) {
super(message);
}
}

View File

@ -1,5 +1,19 @@
export class InvalidMetadataException extends Error {
constructor(message: string) {
super(message);
import { CustomException } from 'src/utils/custom-exception';
export class InvalidMetadataException extends CustomException {
constructor(message: string, code: InvalidMetadataExceptionCode) {
super(message, code);
}
}
export enum InvalidMetadataExceptionCode {
LABEL_REQUIRED = 'Label required',
INPUT_TOO_SHORT = 'Input too short',
EXCEEDS_MAX_LENGTH = 'Exceeds max length',
RESERVED_KEYWORD = 'Reserved keyword',
NOT_CAMEL_CASE = 'Not camel case',
INVALID_LABEL = 'Invalid label',
NAME_NOT_SYNCED_WITH_LABEL = 'Name not synced with label',
INVALID_STRING = 'Invalid string',
NOT_AVAILABLE = 'Name not available',
}

View File

@ -2,7 +2,10 @@ import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-meta
import { computeCompositeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { InvalidMetadataNameException } from 'src/engine/metadata-modules/utils/exceptions/invalid-metadata-name.exception';
import {
InvalidMetadataException,
InvalidMetadataExceptionCode,
} from 'src/engine/metadata-modules/utils/exceptions/invalid-metadata.exception';
const getReservedCompositeFieldNames = (
objectMetadata: ObjectMetadataEntity,
@ -33,10 +36,16 @@ export const validateFieldNameAvailabilityOrThrow = (
getReservedCompositeFieldNames(objectMetadata);
if (objectMetadata.fields.some((field) => field.name === name)) {
throw new InvalidMetadataNameException(`Name "${name}" is not available`);
throw new InvalidMetadataException(
`Name "${name}" is not available`,
InvalidMetadataExceptionCode.NOT_AVAILABLE,
);
}
if (reservedCompositeFieldsNames.includes(name)) {
throw new InvalidMetadataNameException(`Name "${name}" is not available`);
throw new InvalidMetadataException(
`Name "${name}" is not available`,
InvalidMetadataExceptionCode.RESERVED_KEYWORD,
);
}
};

View File

@ -1,11 +1,15 @@
import camelCase from 'lodash.camelcase';
import { InvalidMetadataNameException } from 'src/engine/metadata-modules/utils/exceptions/invalid-metadata-name.exception';
import {
InvalidMetadataException,
InvalidMetadataExceptionCode,
} from 'src/engine/metadata-modules/utils/exceptions/invalid-metadata.exception';
export const validateMetadataNameIsCamelCaseOrThrow = (name: string) => {
if (name !== camelCase(name)) {
throw new InvalidMetadataNameException(
throw new InvalidMetadataException(
`Name should be in camelCase: ${name}`,
InvalidMetadataExceptionCode.NOT_CAMEL_CASE,
);
}
};

View File

@ -1,4 +1,7 @@
import { InvalidMetadataNameException } from 'src/engine/metadata-modules/utils/exceptions/invalid-metadata-name.exception';
import {
InvalidMetadataException,
InvalidMetadataExceptionCode,
} from 'src/engine/metadata-modules/utils/exceptions/invalid-metadata.exception';
const coreObjectNames = [
'approvedAccessDomain',
@ -64,8 +67,9 @@ export const validateMetadataNameIsNotReservedKeywordOrThrow = (
name: string,
) => {
if (reservedKeywords.includes(name)) {
throw new InvalidMetadataNameException(
throw new InvalidMetadataException(
`The name "${name}" is not available`,
InvalidMetadataExceptionCode.RESERVED_KEYWORD,
);
}
};

View File

@ -1,10 +1,14 @@
import { InvalidMetadataNameException } from 'src/engine/metadata-modules/utils/exceptions/invalid-metadata-name.exception';
import {
InvalidMetadataException,
InvalidMetadataExceptionCode,
} from 'src/engine/metadata-modules/utils/exceptions/invalid-metadata.exception';
import { exceedsDatabaseIdentifierMaximumLength } from 'src/engine/metadata-modules/utils/validate-database-identifier-length.utils';
export const validateMetadataNameIsNotTooLongOrThrow = (name: string) => {
if (exceedsDatabaseIdentifierMaximumLength(name)) {
throw new InvalidMetadataNameException(
throw new InvalidMetadataException(
`String "${name}" exceeds 63 characters limit`,
InvalidMetadataExceptionCode.EXCEEDS_MAX_LENGTH,
);
}
};

View File

@ -1,8 +1,14 @@
import { InvalidMetadataNameException } from 'src/engine/metadata-modules/utils/exceptions/invalid-metadata-name.exception';
import {
InvalidMetadataException,
InvalidMetadataExceptionCode,
} from 'src/engine/metadata-modules/utils/exceptions/invalid-metadata.exception';
import { beneathDatabaseIdentifierMinimumLength } from 'src/engine/metadata-modules/utils/validate-database-identifier-length.utils';
export const validateMetadataNameIsNotTooShortOrThrow = (name: string) => {
if (beneathDatabaseIdentifierMinimumLength(name)) {
throw new InvalidMetadataNameException(`Input is too short: "${name}"`);
throw new InvalidMetadataException(
`Input is too short: "${name}"`,
InvalidMetadataExceptionCode.INPUT_TOO_SHORT,
);
}
};

View File

@ -1,4 +1,7 @@
import { InvalidMetadataNameException } from 'src/engine/metadata-modules/utils/exceptions/invalid-metadata-name.exception';
import {
InvalidMetadataException,
InvalidMetadataExceptionCode,
} from 'src/engine/metadata-modules/utils/exceptions/invalid-metadata.exception';
const STARTS_WITH_LOWER_CASE_AND_CONTAINS_ONLY_CAPS_AND_LOWER_LETTERS_AND_NUMBER_STRING_REGEX =
/^[a-z][a-zA-Z0-9]*$/;
@ -10,8 +13,9 @@ export const validateMetadataNameStartWithLowercaseLetterAndContainDigitsNorLett
STARTS_WITH_LOWER_CASE_AND_CONTAINS_ONLY_CAPS_AND_LOWER_LETTERS_AND_NUMBER_STRING_REGEX,
)
) {
throw new InvalidMetadataNameException(
throw new InvalidMetadataException(
`String "${name}" is not valid: must start with lowercase letter and contain only alphanumeric letters`,
InvalidMetadataExceptionCode.INVALID_STRING,
);
}
};

View File

@ -2,7 +2,10 @@ import camelCase from 'lodash.camelcase';
import { slugify } from 'transliteration';
import { isDefined } from 'twenty-shared/utils';
import { InvalidMetadataException } from 'src/engine/metadata-modules/utils/exceptions/invalid-metadata.exception';
import {
InvalidMetadataException,
InvalidMetadataExceptionCode,
} from 'src/engine/metadata-modules/utils/exceptions/invalid-metadata.exception';
export const validateNameAndLabelAreSyncOrThrow = (
label: string,
@ -13,13 +16,17 @@ export const validateNameAndLabelAreSyncOrThrow = (
if (name !== computedName) {
throw new InvalidMetadataException(
`Name is not synced with label. Expected name: "${computedName}", got ${name}`,
InvalidMetadataExceptionCode.NAME_NOT_SYNCED_WITH_LABEL,
);
}
};
export const computeMetadataNameFromLabel = (label: string): string => {
if (!isDefined(label)) {
throw new InvalidMetadataException('Label is required');
throw new InvalidMetadataException(
'Label is required',
InvalidMetadataExceptionCode.LABEL_REQUIRED,
);
}
const prefixedLabel = /^\d/.test(label) ? `n${label}` : label;
@ -35,7 +42,10 @@ export const computeMetadataNameFromLabel = (label: string): string => {
});
if (formattedString === '') {
throw new InvalidMetadataException(`Invalid label: "${label}"`);
throw new InvalidMetadataException(
`Invalid label: "${label}"`,
InvalidMetadataExceptionCode.INVALID_LABEL,
);
}
return camelCase(formattedString);