Misc. of sentry improvements (#12233)

This PR mixes various initiatives to improve visibility on sentry 

**1. Catch errors on workflow jobs**
commit [catch workflowTriggerExceptions in job
handle](https://github.com/twentyhq/twenty/commit/1dbba8c9e27fed8fe01c5ac91d492720c45ba7a5)
@thomtrp 

**2. Fix type in messagingImportExceptionHandler** 
commit [fix type issue on
messagingImportExceptionHandler](https://github.com/twentyhq/twenty/commit/919bb3844c2e11f929a01dff0539e0a2ec7e4b00)
@guillim 

**3. Catch invalid uuid errors thrown by Postgres by rightfully typing
expected id as uuid**
commits [use UUIDFilter instead of IDFilter to get graphqlError in case
of malformed
id](https://github.com/twentyhq/twenty/commit/57cc315efeb0365b1cd589f7487dbfc886338d26),
[use UUIDFilter
(2)](https://github.com/twentyhq/twenty/commit/304553d770426362ee8bfcd3c2e1f0066abe2c04),
[fix ids typed as UUID instead of
ID](https://github.com/twentyhq/twenty/commit/f95d6319cf9290a5ae1d59eb634617a511411633)
@Weiko 
⚠️⚠️⚠️ when we deploy this PR we need to flush the schema types from
redis as this PR changes them ⚠️⚠️⚠️


**4. Do not group UNKNOWN errors together**
commit [do not group unknown errors
together](https://github.com/twentyhq/twenty/commit/c299b39c8fdcc4d6656dc5ce58437e4365cd60b0)
Some CustomException classes have introduced UNKNOWN error codes as a
default fallback error code. We use CustomException codes to group
issues together, but we don't want to do it with UNKNOWN error as they
may not have anything in common. For exemple [this sentry for UNKNOWN
code](https://twenty-v7.sentry.io/issues/6605750776/events/a72272d8941b4fa2add9b1f39c196d3f/?environment=prod&environment=prod-eu&project=4507072499810304&query=Unknown&referrer=next-event&stream_index=0)
groups together "Unknown error importing calendar events for calendar
channel...", "Insufficent permissions...", to name a few.

**5. Improve postgres error grouping**
commit [group together postgres
errors](https://github.com/twentyhq/twenty/commit/567c25495e37cdccfc9010f346145f7d5cc46a54)
Postgres error are thrown by typeORM as QueryFailedError. we have a lot
of them on sentry where they are badly grouped They are currently
grouped on sentry according to the stack trace, which leads them to
sometimes be grouped even if they don't have anything in common : for
exemple [this sentry for
QueryFailedError](https://twenty-v7.sentry.io/issues/6563624590/events/2d636821e27a448595b647b4b5a7d6a8/?environment=prod&environment=prod-eu&project=4507072499810304&query=is%3Aunresolved%20%21issue.type%3A%5Bperformance_consecutive_db_queries%2Cperformance_consecutive_http%2Cperformance_file_io_main_thread%2Cperformance_db_main_thread%2Cperformance_n_plus_one_db_queries%2Cperformance_n_plus_one_api_calls%2Cperformance_p95_endpoint_regression%2Cperformance_slow_db_query%2Cperformance_render_blocking_asset_span%2Cperformance_uncompressed_assets%2Cperformance_http_overhead%2Cperformance_large_http_payload%5D%20timesSeen%3A%3E10&referrer=previous-event&sort=date&stream_index=0)
groups together "user mapping not found for "postgres" and "invalide
type for uuid: 'fallback-id'" to name a few. I attempted to improve the
grouping by grouping them with a new custom fingerPrint composed of the
[code returned by
Postgres](https://www.postgresql.org/docs/current/errcodes-appendix.html)
+ the truncated operation name (Find, Aggregate, Check...). This is
still not ideal as postgres code are quite broad - we could have the
same error code for two Find operations with different causes. let's
give this a try !
This commit is contained in:
Marie
2025-05-23 13:36:02 +00:00
committed by GitHub
parent 5da446d7f7
commit 362d540aac
38 changed files with 470 additions and 133 deletions
@@ -84,7 +84,7 @@ Twenty uses a combination of Recoil for global state and Apollo Client for serve
`;
export const GET_USER = gql`
query GetUser($id: ID!) {
query GetUser($id: UUID!) {
user(id: $id) {
...UserFields
}
@@ -19,7 +19,7 @@ export const deleteWorkflow = async ({
operationName: 'DeleteOneWorkflow',
variables: { idToDelete: workflowId },
query:
'mutation DeleteOneWorkflow($idToDelete: ID!) {\n deleteWorkflow(id: $idToDelete) {\n __typename\n deletedAt\n id\n }\n}',
'mutation DeleteOneWorkflow($idToDelete: UUID!) {\n deleteWorkflow(id: $idToDelete) {\n __typename\n deletedAt\n id\n }\n}',
},
});
};
@@ -19,7 +19,7 @@ export const destroyWorkflow = async ({
operationName: 'DestroyOneWorkflow',
variables: { idToDestroy: workflowId },
query:
'mutation DestroyOneWorkflow($idToDestroy: ID!) {\n destroyWorkflow(id: $idToDestroy) {\n id\n __typename\n }\n}',
'mutation DestroyOneWorkflow($idToDestroy: UUID!) {\n destroyWorkflow(id: $idToDestroy) {\n id\n __typename\n }\n}',
},
});
};
+12 -12
View File
@@ -736,17 +736,6 @@ export enum HealthIndicatorId {
worker = 'worker'
}
export type IdFilter = {
eq?: InputMaybe<Scalars['ID']>;
gt?: InputMaybe<Scalars['ID']>;
gte?: InputMaybe<Scalars['ID']>;
in?: InputMaybe<Array<Scalars['ID']>>;
is?: InputMaybe<FilterIs>;
lt?: InputMaybe<Scalars['ID']>;
lte?: InputMaybe<Scalars['ID']>;
neq?: InputMaybe<Scalars['ID']>;
};
export enum IdentityProviderType {
OIDC = 'OIDC',
SAML = 'SAML'
@@ -1414,7 +1403,7 @@ export type ObjectRecordFilterInput = {
and?: InputMaybe<Array<ObjectRecordFilterInput>>;
createdAt?: InputMaybe<DateFilter>;
deletedAt?: InputMaybe<DateFilter>;
id?: InputMaybe<IdFilter>;
id?: InputMaybe<UuidFilter>;
not?: InputMaybe<ObjectRecordFilterInput>;
or?: InputMaybe<Array<ObjectRecordFilterInput>>;
updatedAt?: InputMaybe<DateFilter>;
@@ -2084,6 +2073,17 @@ export type TransientToken = {
transientToken: AuthToken;
};
export type UuidFilter = {
eq?: InputMaybe<Scalars['UUID']>;
gt?: InputMaybe<Scalars['UUID']>;
gte?: InputMaybe<Scalars['UUID']>;
in?: InputMaybe<Array<Scalars['UUID']>>;
is?: InputMaybe<FilterIs>;
lt?: InputMaybe<Scalars['UUID']>;
lte?: InputMaybe<Scalars['UUID']>;
neq?: InputMaybe<Scalars['UUID']>;
};
export type UuidFilterComparison = {
eq?: InputMaybe<Scalars['UUID']>;
gt?: InputMaybe<Scalars['UUID']>;
@@ -27,7 +27,7 @@ const mocks: MockedResponse[] = [
{
request: {
query: gql`
mutation UpdateOneTask($idToUpdate: ID!, $input: TaskUpdateInput!) {
mutation UpdateOneTask($idToUpdate: UUID!, $input: TaskUpdateInput!) {
updateTask(id: $idToUpdate, data: $input) {
__typename
assignee {
@@ -12,7 +12,7 @@ const mocks = [
{
request: {
query: gql`
query FindOneMessageThread($objectRecordId: ID!) {
query FindOneMessageThread($objectRecordId: UUID!) {
messageThread(filter: { id: { eq: $objectRecordId } }) {
__typename
id
@@ -101,7 +101,7 @@ export const sortedFavorites = [
];
const UPDATE_ONE_FAVORITE_MUTATION = gql`
mutation UpdateOneFavorite($idToUpdate: ID!, $input: FavoriteUpdateInput!) {
mutation UpdateOneFavorite($idToUpdate: UUID!, $input: FavoriteUpdateInput!) {
updateFavorite(id: $idToUpdate, data: $input) {
__typename
company {
@@ -859,7 +859,7 @@ export const mocks = [
{
request: {
query: gql`
mutation DeleteOneFavorite($idToDelete: ID!) {
mutation DeleteOneFavorite($idToDelete: UUID!) {
deleteFavorite(id: $idToDelete) {
__typename
deletedAt
@@ -1,7 +1,7 @@
import { gql } from '@apollo/client';
export const query = gql`
mutation DeleteOnePerson($idToDelete: ID!) {
mutation DeleteOnePerson($idToDelete: UUID!) {
deletePerson(id: $idToDelete) {
__typename
deletedAt
@@ -5,7 +5,7 @@ import { getPeopleRecordConnectionMock } from '~/testing/mock-data/people';
const peopleMock = getPeopleRecordConnectionMock();
export const query = gql`
query FindDuplicatePerson($ids: [ID!]!) {
query FindDuplicatePerson($ids: [UUID!]!) {
personDuplicates(ids: $ids) {
edges {
node {
@@ -4,7 +4,7 @@ import { PERSON_FRAGMENT_WITH_DEPTH_ONE_RELATIONS } from '@/object-record/hooks/
import { responseData as person } from './useUpdateOneRecord';
export const query = gql`
query FindOnePerson($objectRecordId: ID!) {
query FindOnePerson($objectRecordId: UUID!) {
person(filter: { id: { eq: $objectRecordId } }) {
${PERSON_FRAGMENT_WITH_DEPTH_ONE_RELATIONS}
}
@@ -2,7 +2,7 @@ import { PERSON_FRAGMENT_WITH_DEPTH_ONE_RELATIONS } from '@/object-record/hooks/
import { gql } from '@apollo/client';
export const query = gql`
mutation UpdateOnePerson($idToUpdate: ID!, $input: PersonUpdateInput!) {
mutation UpdateOnePerson($idToUpdate: UUID!, $input: PersonUpdateInput!) {
updatePerson(id: $idToUpdate, data: $input) {
${PERSON_FRAGMENT_WITH_DEPTH_ONE_RELATIONS}
}
@@ -5,7 +5,7 @@ import { useDeleteOneRecordMutation } from '@/object-record/hooks/useDeleteOneRe
import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper';
const expectedQueryTemplate = `
mutation DeleteOnePerson($idToDelete: ID!) {
mutation DeleteOnePerson($idToDelete: UUID!) {
deletePerson(id: $idToDelete) {
__typename
deletedAt
@@ -6,7 +6,7 @@ import { useFindDuplicateRecordsQuery } from '@/object-record/hooks/useFindDupli
import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper';
const expectedQueryTemplate = `
query FindDuplicatePerson($ids: [ID!]!) {
query FindDuplicatePerson($ids: [UUID!]!) {
personDuplicates(ids: $ids) {
edges {
node {
@@ -6,7 +6,7 @@ import { useFindOneRecordQuery } from '@/object-record/hooks/useFindOneRecordQue
import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper';
const expectedQueryTemplate = `
query FindOnePerson($objectRecordId: ID!) {
query FindOnePerson($objectRecordId: UUID!) {
person(filter: { id: { eq: $objectRecordId } }) {
${PERSON_FRAGMENT_WITH_DEPTH_ZERO_RELATIONS}
}
@@ -7,7 +7,7 @@ import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMeta
import { normalizeGQLQuery } from '~/utils/normalizeGQLQuery';
const expectedQueryTemplate = `
mutation UpdateOnePerson($idToUpdate: ID!, $input: PersonUpdateInput!) {
mutation UpdateOnePerson($idToUpdate: UUID!, $input: PersonUpdateInput!) {
updatePerson(id: $idToUpdate, data: $input) {
${PERSON_FRAGMENT_WITH_DEPTH_ZERO_RELATIONS}
}
@@ -4,8 +4,8 @@ import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadata
import { mapSoftDeleteFieldsToGraphQLQuery } from '@/object-metadata/utils/mapSoftDeleteFieldsToGraphQLQuery';
import { EMPTY_MUTATION } from '@/object-record/constants/EmptyMutation';
import { getDeleteOneRecordMutationResponseField } from '@/object-record/utils/getDeleteOneRecordMutationResponseField';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
import { capitalize } from 'twenty-shared/utils';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
export const useDeleteOneRecordMutation = ({
objectNameSingular,
@@ -27,7 +27,7 @@ export const useDeleteOneRecordMutation = ({
);
const deleteOneRecordMutation = gql`
mutation DeleteOne${capitalizedObjectName}($idToDelete: ID!) {
mutation DeleteOne${capitalizedObjectName}($idToDelete: UUID!) {
${mutationResponseField}(id: $idToDelete)
${mapSoftDeleteFieldsToGraphQLQuery(objectMetadataItem)}
}
@@ -3,8 +3,8 @@ import gql from 'graphql-tag';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { EMPTY_MUTATION } from '@/object-record/constants/EmptyMutation';
import { getDestroyOneRecordMutationResponseField } from '@/object-record/utils/getDestroyOneRecordMutationResponseField';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
import { capitalize } from 'twenty-shared/utils';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
export const useDestroyOneRecordMutation = ({
objectNameSingular,
@@ -26,7 +26,7 @@ export const useDestroyOneRecordMutation = ({
);
const destroyOneRecordMutation = gql`
mutation DestroyOne${capitalizedObjectName}($idToDestroy: ID!) {
mutation DestroyOne${capitalizedObjectName}($idToDestroy: UUID!) {
${mutationResponseField}(id: $idToDestroy) {
id
}
@@ -22,7 +22,7 @@ export const useFindDuplicateRecordsQuery = ({
const findDuplicateRecordsQuery = gql`
query FindDuplicate${capitalize(
objectMetadataItem.nameSingular,
)}($ids: [ID!]!) {
)}($ids: [UUID!]!) {
${getFindDuplicateRecordsQueryResponseField(
objectMetadataItem.nameSingular,
)}(ids: $ids) {
@@ -25,7 +25,7 @@ export const useFindOneRecordQuery = ({
const findOneRecordQuery = gql`
query FindOne${capitalize(
objectMetadataItem.nameSingular,
)}($objectRecordId: ID!) {
)}($objectRecordId: UUID!) {
${objectMetadataItem.nameSingular}(filter: {
${
withSoftDeleted
@@ -8,8 +8,8 @@ import { EMPTY_MUTATION } from '@/object-record/constants/EmptyMutation';
import { RecordGqlOperationGqlRecordFields } from '@/object-record/graphql/types/RecordGqlOperationGqlRecordFields';
import { generateDepthOneRecordGqlFields } from '@/object-record/graphql/utils/generateDepthOneRecordGqlFields';
import { getUpdateOneRecordMutationResponseField } from '@/object-record/utils/getUpdateOneRecordMutationResponseField';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
import { capitalize } from 'twenty-shared/utils';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
export const useUpdateOneRecordMutation = ({
objectNameSingular,
@@ -43,7 +43,7 @@ export const useUpdateOneRecordMutation = ({
);
const updateOneRecordMutation = gql`
mutation UpdateOne${capitalizedObjectName}($idToUpdate: ID!, $input: ${capitalizedObjectName}UpdateInput!) {
mutation UpdateOne${capitalizedObjectName}($idToUpdate: UUID!, $input: ${capitalizedObjectName}UpdateInput!) {
${mutationResponseField}(id: $idToUpdate, data: $input) ${mapObjectMetadataToGraphQLQuery(
{
objectMetadataItems,
@@ -23,7 +23,7 @@ import { recordStoreFamilySelector } from '@/object-record/record-store/states/s
import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper';
const query = gql`
mutation UpdateOnePerson($idToUpdate: ID!, $input: PersonUpdateInput!) {
mutation UpdateOnePerson($idToUpdate: UUID!, $input: PersonUpdateInput!) {
updatePerson(id: $idToUpdate, data: $input) {
${PERSON_FRAGMENT_WITH_DEPTH_ONE_RELATIONS}
}
@@ -22,7 +22,7 @@ const mocks: MockedResponse[] = [
request: {
query: gql`
mutation UpdateOneCompany(
$idToUpdate: ID!
$idToUpdate: UUID!
$input: CompanyUpdateInput!
) {
updateCompany(id: $idToUpdate, data: $input) {
@@ -0,0 +1,263 @@
// From https://www.postgresql.org/docs/current/errcodes-appendix.html
export const POSTGRESQL_ERROR_CODES = [
'00000', // successful_completion
'01000', // warning
'0100C', // dynamic_result_sets_returned
'01008', // implicit_zero_bit_padding
'01003', // null_value_eliminated_in_set_function
'01007', // privilege_not_granted
'01006', // privilege_not_revoked
'01004', // string_data_right_truncation
'01P01', // deprecated_feature
'02000', // no_data
'02001', // no_additional_dynamic_result_sets_returned
'03000', // sql_statement_not_yet_complete
'08000', // connection_exception
'08003', // connection_does_not_exist
'08006', // connection_failure
'08001', // sqlclient_unable_to_establish_sqlconnection
'08004', // sqlserver_rejected_establishment_of_sqlconnection
'08007', // transaction_resolution_unknown
'08P01', // protocol_violation
'09000', // triggered_action_exception
'0A000', // feature_not_supported
'0B000', // invalid_transaction_initiation
'0F000', // locator_exception
'0F001', // invalid_locator_specification
'0L000', // invalid_grantor
'0LP01', // invalid_grant_operation
'0P000', // invalid_role_specification
'0Z000', // diagnostics_exception
'0Z002', // stacked_diagnostics_accessed_without_active_handler
'20000', // case_not_found
'21000', // cardinality_violation
'22000', // data_exception
'2202E', // array_subscript_error
'22021', // character_not_in_repertoire
'22008', // datetime_field_overflow
'22012', // division_by_zero
'22005', // error_in_assignment
'2200B', // escape_character_conflict
'22022', // indicator_overflow
'22015', // interval_field_overflow
'2201E', // invalid_argument_for_logarithm
'22014', // invalid_argument_for_ntile_function
'22016', // invalid_argument_for_nth_value_function
'2201F', // invalid_argument_for_power_function
'2201G', // invalid_argument_for_width_bucket_function
'22018', // invalid_character_value_for_cast
'22007', // invalid_datetime_format
'22019', // invalid_escape_character
'2200D', // invalid_escape_octet
'22025', // invalid_escape_sequence
'22P06', // nonstandard_use_of_escape_character
'22010', // invalid_indicator_parameter_value
'22023', // invalid_parameter_value
'22013', // invalid_preceding_or_following_size
'2201B', // invalid_regular_expression
'2201W', // invalid_row_count_in_limit_clause
'2201X', // invalid_row_count_in_result_offset_clause
'2202H', // invalid_tablesample_argument
'2202G', // invalid_tablesample_repeat
'22009', // invalid_time_zone_displacement_value
'2200C', // invalid_use_of_escape_character
'2200G', // most_specific_type_mismatch
'22004', // null_value_not_allowed
'22002', // null_value_no_indicator_parameter
'22003', // numeric_value_out_of_range
'2200H', // sequence_generator_limit_exceeded
'22026', // string_data_length_mismatch
'22001', // string_data_right_truncation
'22011', // substring_error
'22027', // trim_error
'22024', // unterminated_c_string
'2200F', // zero_length_character_string
'22P01', // floating_point_exception
'22P02', // invalid_text_representation
'22P03', // invalid_binary_representation
'22P04', // bad_copy_file_format
'22P05', // untranslatable_character
'2200L', // not_an_xml_document
'2200M', // invalid_xml_document
'2200N', // invalid_xml_content
'2200S', // invalid_xml_comment
'2200T', // invalid_xml_processing_instruction
'22030', // duplicate_json_object_key_value
'22031', // invalid_argument_for_sql_json_datetime_function
'22032', // invalid_json_text
'22033', // invalid_sql_json_subscript
'22034', // more_than_one_sql_json_item
'22035', // no_sql_json_item
'22036', // non_numeric_sql_json_item
'22037', // non_unique_keys_in_a_json_object
'22038', // singleton_sql_json_item_required
'22039', // sql_json_array_not_found
'2203A', // sql_json_member_not_found
'2203B', // sql_json_number_not_found
'2203C', // sql_json_object_not_found
'2203D', // too_many_json_array_elements
'2203E', // too_many_json_object_members
'2203F', // sql_json_scalar_required
'2203G', // sql_json_item_cannot_be_cast_to_target_type
'23000', // integrity_constraint_violation
'23001', // restrict_violation
'23502', // not_null_violation
'23503', // foreign_key_violation
'23505', // unique_violation
'23514', // check_violation
'23P01', // exclusion_violation
'24000', // invalid_cursor_state
'25000', // invalid_transaction_state
'25001', // active_sql_transaction
'25002', // branch_transaction_already_active
'25008', // held_cursor_requires_same_isolation_level
'25003', // inappropriate_access_mode_for_branch_transaction
'25004', // inappropriate_isolation_level_for_branch_transaction
'25005', // no_active_sql_transaction_for_branch_transaction
'25006', // read_only_sql_transaction
'25007', // schema_and_data_statement_mixing_not_supported
'25P01', // no_active_sql_transaction
'25P02', // in_failed_sql_transaction
'25P03', // idle_in_transaction_session_timeout
'25P04', // transaction_timeout
'26000', // invalid_sql_statement_name
'27000', // triggered_data_change_violation
'28000', // invalid_authorization_specification
'28P01', // invalid_password
'2B000', // dependent_privilege_descriptors_still_exist
'2BP01', // dependent_objects_still_exist
'2D000', // invalid_transaction_termination
'2F000', // sql_routine_exception
'2F005', // function_executed_no_return_statement
'2F002', // modifying_sql_data_not_permitted
'2F003', // prohibited_sql_statement_attempted
'2F004', // reading_sql_data_not_permitted
'34000', // invalid_cursor_name
'38000', // external_routine_exception
'38001', // containing_sql_not_permitted
'38002', // modifying_sql_data_not_permitted
'38003', // prohibited_sql_statement_attempted
'38004', // reading_sql_data_not_permitted
'39000', // external_routine_invocation_exception
'39001', // invalid_sqlstate_returned
'39004', // null_value_not_allowed
'39P01', // trigger_protocol_violated
'39P02', // srf_protocol_violated
'39P03', // event_trigger_protocol_violated
'3B000', // savepoint_exception
'3B001', // invalid_savepoint_specification
'3D000', // invalid_catalog_name
'3F000', // invalid_schema_name
'40000', // transaction_rollback
'40002', // transaction_integrity_constraint_violation
'40001', // serialization_failure
'40003', // statement_completion_unknown
'40P01', // deadlock_detected
'42000', // syntax_error_or_access_rule_violation
'42601', // syntax_error
'42501', // insufficient_privilege
'42846', // cannot_coerce
'42803', // grouping_error
'42P20', // windowing_error
'42P19', // invalid_recursion
'42830', // invalid_foreign_key
'42602', // invalid_name
'42622', // name_too_long
'42939', // reserved_name
'42804', // datatype_mismatch
'42P18', // indeterminate_datatype
'42P21', // collation_mismatch
'42P22', // indeterminate_collation
'42809', // wrong_object_type
'428C9', // generated_always
'42703', // undefined_column
'42883', // undefined_function
'42P01', // undefined_table
'42P02', // undefined_parameter
'42704', // undefined_object
'42701', // duplicate_column
'42P03', // duplicate_cursor
'42P04', // duplicate_database
'42723', // duplicate_function
'42P05', // duplicate_prepared_statement
'42P06', // duplicate_schema
'42P07', // duplicate_table
'42712', // duplicate_alias
'42710', // duplicate_object
'42702', // ambiguous_column
'42725', // ambiguous_function
'42P08', // ambiguous_parameter
'42P09', // ambiguous_alias
'42P10', // invalid_column_reference
'42611', // invalid_column_definition
'42P11', // invalid_cursor_definition
'42P12', // invalid_database_definition
'42P13', // invalid_function_definition
'42P14', // invalid_prepared_statement_definition
'42P15', // invalid_schema_definition
'42P16', // invalid_table_definition
'42P17', // invalid_object_definition
'44000', // with_check_option_violation
'53000', // insufficient_resources
'53100', // disk_full
'53200', // out_of_memory
'53300', // too_many_connections
'53400', // configuration_limit_exceeded
'54000', // program_limit_exceeded
'54001', // statement_too_complex
'54011', // too_many_columns
'54023', // too_many_arguments
'55000', // object_not_in_prerequisite_state
'55006', // object_in_use
'55P02', // cant_change_runtime_param
'55P03', // lock_not_available
'55P04', // unsafe_new_enum_value_usage
'57000', // operator_intervention
'57014', // query_canceled
'57P01', // admin_shutdown
'57P02', // crash_shutdown
'57P03', // cannot_connect_now
'57P04', // database_dropped
'57P05', // idle_session_timeout
'58000', // system_error
'58030', // io_error
'58P01', // undefined_file
'58P02', // duplicate_file
'F0000', // config_file_error
'F0001', // lock_file_exists
'HV000', // fdw_error
'HV005', // fdw_column_name_not_found
'HV002', // fdw_dynamic_parameter_value_needed
'HV010', // fdw_function_sequence_error
'HV021', // fdw_inconsistent_descriptor_information
'HV024', // fdw_invalid_attribute_value
'HV007', // fdw_invalid_column_name
'HV008', // fdw_invalid_column_number
'HV004', // fdw_invalid_data_type
'HV006', // fdw_invalid_data_type_descriptors
'HV091', // fdw_invalid_descriptor_field_identifier
'HV00B', // fdw_invalid_handle
'HV00C', // fdw_invalid_option_index
'HV00D', // fdw_invalid_option_name
'HV090', // fdw_invalid_string_length_or_buffer_length
'HV00A', // fdw_invalid_string_format
'HV009', // fdw_invalid_use_of_null_pointer
'HV014', // fdw_too_many_handles
'HV001', // fdw_out_of_memory
'HV00P', // fdw_no_schemas
'HV00J', // fdw_option_name_not_found
'HV00K', // fdw_reply_handle
'HV00Q', // fdw_schema_not_found
'HV00R', // fdw_table_not_found
'HV00L', // fdw_unable_to_create_execution
'HV00M', // fdw_unable_to_create_reply
'HV00N', // fdw_unable_to_establish_connection
'P0000', // plpgsql_error
'P0001', // raise_exception
'P0002', // no_data_found
'P0003', // too_many_rows
'P0004', // assert_failure
'XX000', // internal_error
'XX001', // data_corrupted
'XX002', // index_corrupted
];
@@ -0,0 +1,7 @@
export class PostgresException extends Error {
readonly code: string;
constructor(message: string, code: string) {
super(message);
this.code = code;
}
}
@@ -3,8 +3,10 @@ import { QueryFailedError } from 'typeorm';
import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
import { GraphqlQueryRunnerException } from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
import { POSTGRESQL_ERROR_CODES } from 'src/engine/api/graphql/workspace-query-runner/constants/postgres-error-codes.constants';
import { graphqlQueryRunnerExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/graphql-query-runner-exception-handler.util';
import { handleDuplicateKeyError } from 'src/engine/api/graphql/workspace-query-runner/utils/handle-duplicate-key-error.util';
import { PostgresException } from 'src/engine/api/graphql/workspace-query-runner/utils/postgres-exception';
import { workspaceExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-exception-handler.util';
import { WorkspaceQueryRunnerException } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.exception';
import { RecordTransformerException } from 'src/engine/core-modules/record-transformer/record-transformer.exception';
@@ -12,8 +14,12 @@ import { recordTransformerGraphqlApiExceptionHandler } from 'src/engine/core-mod
import { PermissionsException } from 'src/engine/metadata-modules/permissions/permissions.exception';
import { permissionGraphqlApiExceptionHandler } from 'src/engine/metadata-modules/permissions/utils/permission-graphql-api-exception-handler.util';
interface QueryFailedErrorWithCode extends QueryFailedError {
code: string;
}
export const workspaceQueryRunnerGraphqlApiExceptionHandler = (
error: Error,
error: QueryFailedErrorWithCode,
context: WorkspaceQueryRunnerOptions,
) => {
switch (true) {
@@ -23,6 +29,11 @@ export const workspaceQueryRunnerGraphqlApiExceptionHandler = (
) {
return handleDuplicateKeyError(error, context);
}
const errorCode = (error as QueryFailedErrorWithCode).code;
if (POSTGRESQL_ERROR_CODES.includes(errorCode)) {
throw new PostgresException(error.message, errorCode);
}
throw error;
}
case error instanceof RecordTransformerException:
@@ -0,0 +1,18 @@
import { GraphQLInputObjectType, GraphQLList } from 'graphql';
import { FilterIs } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/input/filter-is.input-type';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
export const UUIDFilterType = new GraphQLInputObjectType({
name: 'UUIDFilter',
fields: {
eq: { type: UUIDScalarType },
gt: { type: UUIDScalarType },
gte: { type: UUIDScalarType },
in: { type: new GraphQLList(UUIDScalarType) },
lt: { type: UUIDScalarType },
lte: { type: UUIDScalarType },
neq: { type: UUIDScalarType },
is: { type: FilterIs },
},
});
@@ -27,10 +27,10 @@ import {
RawJsonFilterType,
StringFilterType,
} from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/input';
import { IDFilterType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/input/id-filter.input-type';
import { MultiSelectFilterType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/input/multi-select-filter.input-type';
import { RichTextV2FilterType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/input/rich-text.input-type';
import { SelectFilterType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/input/select-filter.input-type';
import { UUIDFilterType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/input/uuid-filter.input-type';
import {
BigFloatScalarType,
UUIDScalarType,
@@ -95,14 +95,14 @@ export class TypeMapperService {
isIdField?: boolean,
): GraphQLInputObjectType | GraphQLScalarType | undefined {
if (isIdField || fieldMetadataType === FieldMetadataType.RELATION) {
return IDFilterType;
return UUIDFilterType;
}
const typeFilterMapping = new Map<
FieldMetadataType,
GraphQLInputObjectType | GraphQLScalarType
>([
[FieldMetadataType.UUID, IDFilterType],
[FieldMetadataType.UUID, UUIDFilterType],
[FieldMetadataType.TEXT, StringFilterType],
[FieldMetadataType.DATE_TIME, DateFilterType],
[FieldMetadataType.DATE, DateFilterType],
@@ -1,8 +1,9 @@
import { GraphQLBoolean, GraphQLID, GraphQLInt, GraphQLString } from 'graphql';
import { GraphQLBoolean, GraphQLInt, GraphQLString } from 'graphql';
import { WorkspaceResolverBuilderMethodNames } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { InputTypeDefinitionKind } from 'src/engine/api/graphql/workspace-schema-builder/factories/input-type-definition.factory';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { getResolverArgs } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-resolver-args.util';
describe('getResolverArgs', () => {
@@ -44,11 +45,11 @@ describe('getResolverArgs', () => {
},
},
updateOne: {
id: { type: GraphQLID, isNullable: false },
id: { type: UUIDScalarType, isNullable: false },
data: { kind: InputTypeDefinitionKind.Update, isNullable: false },
},
deleteOne: {
id: { type: GraphQLID, isNullable: false },
id: { type: UUIDScalarType, isNullable: false },
},
restoreMany: {
filter: { kind: InputTypeDefinitionKind.Filter, isNullable: false },
@@ -1,9 +1,10 @@
import { GraphQLBoolean, GraphQLID, GraphQLInt, GraphQLString } from 'graphql';
import { GraphQLBoolean, GraphQLInt, GraphQLString } from 'graphql';
import { WorkspaceResolverBuilderMethodNames } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { ArgMetadata } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/param-metadata.interface';
import { InputTypeDefinitionKind } from 'src/engine/api/graphql/workspace-schema-builder/factories/input-type-definition.factory';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
export const getResolverArgs = (
type: WorkspaceResolverBuilderMethodNames,
@@ -77,7 +78,7 @@ export const getResolverArgs = (
case 'updateOne':
return {
id: {
type: GraphQLID,
type: UUIDScalarType,
isNullable: false,
},
data: {
@@ -88,7 +89,7 @@ export const getResolverArgs = (
case 'findDuplicates':
return {
ids: {
type: GraphQLID,
type: UUIDScalarType,
isNullable: true,
isArray: true,
},
@@ -101,14 +102,14 @@ export const getResolverArgs = (
case 'deleteOne':
return {
id: {
type: GraphQLID,
type: UUIDScalarType,
isNullable: false,
},
};
case 'destroyOne':
return {
id: {
type: GraphQLID,
type: UUIDScalarType,
isNullable: false,
},
};
@@ -133,7 +134,7 @@ export const getResolverArgs = (
case 'restoreOne':
return {
id: {
type: GraphQLID,
type: UUIDScalarType,
isNullable: false,
},
};
@@ -1,7 +1,9 @@
import * as Sentry from '@sentry/node';
import { isDefined } from 'twenty-shared/utils';
import { ExceptionHandlerOptions } from 'src/engine/core-modules/exception-handler/interfaces/exception-handler-options.interface';
import { PostgresException } from 'src/engine/api/graphql/workspace-query-runner/utils/postgres-exception';
import { ExceptionHandlerDriverInterface } from 'src/engine/core-modules/exception-handler/interfaces';
import { CustomException } from 'src/utils/custom-exception';
@@ -55,7 +57,10 @@ export class ExceptionHandlerSentryDriver
});
}
if (exception instanceof CustomException) {
if (
exception instanceof CustomException &&
exception.code !== 'UNKNOWN'
) {
scope.setTag('customExceptionCode', exception.code);
scope.setFingerprint([exception.code]);
exception.name = exception.code
@@ -67,6 +72,19 @@ export class ExceptionHandlerSentryDriver
.join(' ');
}
if (exception instanceof PostgresException) {
scope.setTag('postgresSqlErrorCode', exception.code);
const fingerPrint = [exception.code];
const genericOperationName = // truncates to first word: FindOnePerson -> Find, AggregateCompanies -> Aggregate, ...
options?.operation?.name?.match(/^[A-Z][a-z]*/)?.[0];
if (isDefined(genericOperationName)) {
fingerPrint.push(genericOperationName);
}
scope.setFingerprint(fingerPrint);
exception.name = exception.message;
}
const eventId = Sentry.captureException(exception, {
contexts: {
GraphQL: {
@@ -1,10 +1,13 @@
import { Field, ID, InputType, registerEnumType } from '@nestjs/graphql';
import { Field, InputType, registerEnumType } from '@nestjs/graphql';
import { IsArray, IsOptional } from 'class-validator';
import { ObjectRecordFilter } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
import { DateScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import {
DateScalarType,
UUIDScalarType,
} from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
@InputType()
export class ObjectRecordFilterInput implements Partial<ObjectRecordFilter> {
@@ -22,9 +25,9 @@ export class ObjectRecordFilterInput implements Partial<ObjectRecordFilter> {
@IsArray()
or?: ObjectRecordFilterInput[];
@Field(() => IDFilterType, { nullable: true })
@Field(() => UUIDFilterType, { nullable: true })
@IsOptional()
id?: IDFilterType | null;
id?: UUIDFilterType | null;
@Field(() => DateFilterType, { nullable: true })
createdAt?: DateFilterType | null;
@@ -36,33 +39,33 @@ export class ObjectRecordFilterInput implements Partial<ObjectRecordFilter> {
deletedAt?: DateFilterType | null;
}
@InputType('IDFilter')
class IDFilterType {
@Field(() => ID, { nullable: true })
@InputType('UUIDFilter')
class UUIDFilterType {
@Field(() => UUIDScalarType, { nullable: true })
@IsOptional()
eq?: string;
@Field(() => ID, { nullable: true })
@Field(() => UUIDScalarType, { nullable: true })
@IsOptional()
gt?: string;
@Field(() => ID, { nullable: true })
@Field(() => UUIDScalarType, { nullable: true })
@IsOptional()
gte?: string;
@Field(() => [ID], { nullable: true })
@Field(() => [UUIDScalarType], { nullable: true })
@IsOptional()
in?: string[];
@Field(() => ID, { nullable: true })
@Field(() => UUIDScalarType, { nullable: true })
@IsOptional()
lt?: string;
@Field(() => ID, { nullable: true })
@Field(() => UUIDScalarType, { nullable: true })
@IsOptional()
lte?: string;
@Field(() => ID, { nullable: true })
@Field(() => UUIDScalarType, { nullable: true })
@IsOptional()
neq?: string;
@@ -9,28 +9,34 @@ import {
WorkflowTriggerExceptionCode,
} from 'src/modules/workflow/workflow-trigger/exceptions/workflow-trigger.exception';
export const handleWorkflowTriggerException = (
exception: WorkflowTriggerException,
) => {
switch (exception.code) {
case WorkflowTriggerExceptionCode.INVALID_INPUT:
case WorkflowTriggerExceptionCode.INVALID_WORKFLOW_VERSION:
case WorkflowTriggerExceptionCode.INVALID_ACTION_TYPE:
case WorkflowTriggerExceptionCode.INVALID_WORKFLOW_TRIGGER:
case WorkflowTriggerExceptionCode.INVALID_WORKFLOW_STATUS:
case WorkflowTriggerExceptionCode.FORBIDDEN:
throw new UserInputError(exception.message);
case WorkflowTriggerExceptionCode.NOT_FOUND:
throw new NotFoundError(exception.message);
case WorkflowTriggerExceptionCode.INTERNAL_ERROR:
throw exception;
default: {
const _exhaustiveCheck: never = exception.code;
throw exception;
}
}
};
@Catch(WorkflowTriggerException)
export class WorkflowTriggerGraphqlApiExceptionFilter
implements ExceptionFilter
{
catch(exception: WorkflowTriggerException) {
switch (exception.code) {
case WorkflowTriggerExceptionCode.INVALID_INPUT:
case WorkflowTriggerExceptionCode.INVALID_WORKFLOW_VERSION:
case WorkflowTriggerExceptionCode.INVALID_ACTION_TYPE:
case WorkflowTriggerExceptionCode.INVALID_WORKFLOW_TRIGGER:
case WorkflowTriggerExceptionCode.INVALID_WORKFLOW_STATUS:
case WorkflowTriggerExceptionCode.FORBIDDEN:
throw new UserInputError(exception.message);
case WorkflowTriggerExceptionCode.NOT_FOUND:
throw new NotFoundError(exception.message);
case WorkflowTriggerExceptionCode.INTERNAL_ERROR:
throw exception;
default: {
const _exhaustiveCheck: never = exception.code;
throw exception;
}
}
handleWorkflowTriggerException(exception);
}
}
@@ -1,5 +1,7 @@
import { Injectable } from '@nestjs/common';
import { isDefined } from 'class-validator';
import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
import { MessageChannelSyncStatusService } from 'src/modules/messaging/common/services/message-channel-sync-status.service';
@@ -35,7 +37,7 @@ export class MessageImportExceptionHandlerService {
) {}
public async handleDriverException(
exception: MessageImportDriverException,
exception: MessageImportDriverException | Error,
syncStep: MessageImportSyncStep,
messageChannel: Pick<
MessageChannelWorkspaceEntity,
@@ -43,49 +45,53 @@ export class MessageImportExceptionHandlerService {
>,
workspaceId: string,
): Promise<void> {
switch (exception.code) {
case MessageImportDriverExceptionCode.NOT_FOUND:
await this.handleNotFoundException(
syncStep,
messageChannel,
workspaceId,
);
break;
case MessageImportDriverExceptionCode.TEMPORARY_ERROR:
case MessageNetworkExceptionCode.ECONNABORTED:
case MessageNetworkExceptionCode.ENOTFOUND:
case MessageNetworkExceptionCode.ECONNRESET:
case MessageNetworkExceptionCode.ETIMEDOUT:
case MessageNetworkExceptionCode.ERR_NETWORK:
await this.handleTemporaryException(
syncStep,
messageChannel,
workspaceId,
exception,
);
break;
case MessageImportDriverExceptionCode.INSUFFICIENT_PERMISSIONS:
await this.handleInsufficientPermissionsException(
messageChannel,
workspaceId,
);
break;
case MessageImportDriverExceptionCode.SYNC_CURSOR_ERROR:
await this.handlePermanentException(
exception,
messageChannel,
workspaceId,
);
break;
case MessageImportDriverExceptionCode.UNKNOWN:
case MessageImportDriverExceptionCode.UNKNOWN_NETWORK_ERROR:
default:
await this.handleUnknownException(
exception,
messageChannel,
workspaceId,
);
break;
if (exception instanceof MessageImportDriverException) {
switch (exception.code) {
case MessageImportDriverExceptionCode.NOT_FOUND:
await this.handleNotFoundException(
syncStep,
messageChannel,
workspaceId,
);
break;
case MessageImportDriverExceptionCode.TEMPORARY_ERROR:
case MessageNetworkExceptionCode.ECONNABORTED:
case MessageNetworkExceptionCode.ENOTFOUND:
case MessageNetworkExceptionCode.ECONNRESET:
case MessageNetworkExceptionCode.ETIMEDOUT:
case MessageNetworkExceptionCode.ERR_NETWORK:
await this.handleTemporaryException(
syncStep,
messageChannel,
workspaceId,
exception,
);
break;
case MessageImportDriverExceptionCode.INSUFFICIENT_PERMISSIONS:
await this.handleInsufficientPermissionsException(
messageChannel,
workspaceId,
);
break;
case MessageImportDriverExceptionCode.SYNC_CURSOR_ERROR:
await this.handlePermanentException(
exception,
messageChannel,
workspaceId,
);
break;
case MessageImportDriverExceptionCode.UNKNOWN:
case MessageImportDriverExceptionCode.UNKNOWN_NETWORK_ERROR:
default:
await this.handleUnknownException(
exception,
messageChannel,
workspaceId,
);
break;
}
} else {
await this.handleUnknownException(exception, messageChannel, workspaceId);
}
}
@@ -172,7 +178,7 @@ export class MessageImportExceptionHandlerService {
}
private async handleUnknownException(
exception: MessageImportDriverException,
exception: MessageImportDriverException | Error,
messageChannel: Pick<MessageChannelWorkspaceEntity, 'id'>,
workspaceId: string,
): Promise<void> {
@@ -183,7 +189,9 @@ export class MessageImportExceptionHandlerService {
);
const messageImportException = new MessageImportException(
exception.message,
isDefined(exception.name)
? `${exception.name}: ${exception.message}`
: exception.message,
MessageImportExceptionCode.UNKNOWN,
);
@@ -8,6 +8,7 @@ import { Process } from 'src/engine/core-modules/message-queue/decorators/proces
import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator';
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
import { handleWorkflowTriggerException } from 'src/engine/core-modules/workflow/filters/workflow-trigger-graphql-api-exception.filter';
import { FieldActorSource } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type';
import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
import {
@@ -106,7 +107,7 @@ export class WorkflowTriggerJob {
jobName: WorkflowTriggerJob.name,
jobId: data.workflowId,
});
throw e;
handleWorkflowTriggerException(e);
}
}
}
@@ -13,7 +13,7 @@ export const deleteOneOperationFactory = ({
recordId,
}: DeleteOneOperationFactoryParams) => ({
query: gql`
mutation Delete${capitalize(objectMetadataSingularName)}($${objectMetadataSingularName}Id: ID!) {
mutation Delete${capitalize(objectMetadataSingularName)}($${objectMetadataSingularName}Id: UUID!) {
delete${capitalize(objectMetadataSingularName)}(id: $${objectMetadataSingularName}Id) {
${gqlFields}
}
@@ -13,7 +13,7 @@ export const destroyOneOperationFactory = ({
recordId,
}: DestroyOneOperationFactoryParams) => ({
query: gql`
mutation Destroy${capitalize(objectMetadataSingularName)}($${objectMetadataSingularName}Id: ID!) {
mutation Destroy${capitalize(objectMetadataSingularName)}($${objectMetadataSingularName}Id: UUID!) {
destroy${capitalize(objectMetadataSingularName)}(id: $${objectMetadataSingularName}Id) {
${gqlFields}
}
@@ -13,7 +13,7 @@ export const restoreOneOperationFactory = ({
recordId,
}: RestoreOneOperationFactoryParams) => ({
query: gql`
mutation Restore${capitalize(objectMetadataSingularName)}($${objectMetadataSingularName}Id: ID!) {
mutation Restore${capitalize(objectMetadataSingularName)}($${objectMetadataSingularName}Id: UUID!) {
restore${capitalize(objectMetadataSingularName)}(id: $${objectMetadataSingularName}Id) {
${gqlFields}
}
@@ -15,7 +15,7 @@ export const updateOneOperationFactory = ({
recordId,
}: UpdateOneOperationFactoryParams) => ({
query: gql`
mutation Update${capitalize(objectMetadataSingularName)}($${objectMetadataSingularName}Id: ID, $data: ${capitalize(objectMetadataSingularName)}UpdateInput) {
mutation Update${capitalize(objectMetadataSingularName)}($${objectMetadataSingularName}Id: UUID, $data: ${capitalize(objectMetadataSingularName)}UpdateInput) {
update${capitalize(objectMetadataSingularName)}(id: $${objectMetadataSingularName}Id, data: $data) {
${gqlFields}
}