diff --git a/packages/twenty-server/src/engine/core-modules/exception-handler/drivers/sentry.driver.ts b/packages/twenty-server/src/engine/core-modules/exception-handler/drivers/sentry.driver.ts index 95adbb126..742477766 100644 --- a/packages/twenty-server/src/engine/core-modules/exception-handler/drivers/sentry.driver.ts +++ b/packages/twenty-server/src/engine/core-modules/exception-handler/drivers/sentry.driver.ts @@ -27,6 +27,10 @@ export class ExceptionHandlerSentryDriver scope.setExtra('workspace', options.workspace); } + if (options?.additionalData) { + scope.setExtra('additionalData', options.additionalData); + } + if (options?.user) { scope.setUser({ id: options.user.id, diff --git a/packages/twenty-server/src/engine/core-modules/exception-handler/interfaces/exception-handler-options.interface.ts b/packages/twenty-server/src/engine/core-modules/exception-handler/interfaces/exception-handler-options.interface.ts index 8cf7d102e..94c05fd95 100644 --- a/packages/twenty-server/src/engine/core-modules/exception-handler/interfaces/exception-handler-options.interface.ts +++ b/packages/twenty-server/src/engine/core-modules/exception-handler/interfaces/exception-handler-options.interface.ts @@ -9,6 +9,7 @@ export interface ExceptionHandlerOptions { name: string; }; document?: string; + additionalData?: Record; user?: ExceptionHandlerUser | null; workspace?: ExceptionHandlerWorkspace | null; } diff --git a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/services/calendar-event-import-exception-handler.service.ts b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/services/calendar-event-import-exception-handler.service.ts index 7a5f915c8..3234d4255 100644 --- a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/services/calendar-event-import-exception-handler.service.ts +++ b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/services/calendar-event-import-exception-handler.service.ts @@ -85,10 +85,24 @@ export class CalendarEventImportErrorHandlerService { workspaceId, ); - throw new CalendarEventImportException( - `Unknown temporary error occurred while importing calendar events for calendar channel ${calendarChannel.id} in workspace ${workspaceId} with throttleFailureCount${calendarChannel.throttleFailureCount}`, + const calendarEventImportException = new CalendarEventImportException( + `Temporary error occurred ${CALENDAR_THROTTLE_MAX_ATTEMPTS} times while importing calendar events for calendar channel ${calendarChannel.id.slice(0, 5)}... in workspace ${workspaceId} with throttleFailureCount ${calendarChannel.throttleFailureCount}`, CalendarEventImportExceptionCode.UNKNOWN, ); + + this.exceptionHandlerService.captureExceptions( + [calendarEventImportException], + { + additionalData: { + calendarChannelId: calendarChannel.id, + }, + workspace: { + id: workspaceId, + }, + }, + ); + + throw calendarEventImportException; } const calendarChannelRepository = @@ -149,13 +163,16 @@ export class CalendarEventImportErrorHandlerService { ); const calendarEventImportException = new CalendarEventImportException( - `Unknown error importing calendar events for calendar channel ${calendarChannel.id} in workspace ${workspaceId}: ${exception.message}`, + `Unknown error importing calendar events for calendar channel ${calendarChannel.id.slice(0, 5)}... in workspace ${workspaceId}: ${exception.message}`, CalendarEventImportExceptionCode.UNKNOWN, ); this.exceptionHandlerService.captureExceptions( [calendarEventImportException], { + additionalData: { + calendarChannelId: calendarChannel.id, + }, workspace: { id: workspaceId, }, diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/microsoft/services/microsoft-fetch-by-batch.service.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/microsoft/services/microsoft-fetch-by-batch.service.ts index 3f4ae6a0b..9b723e575 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/microsoft/services/microsoft-fetch-by-batch.service.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/microsoft/services/microsoft-fetch-by-batch.service.ts @@ -1,8 +1,10 @@ import { Injectable } from '@nestjs/common'; import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; +import { MicrosoftImportDriverException } from 'src/modules/messaging/message-import-manager/drivers/microsoft/exceptions/microsoft-import-driver.exception'; import { MicrosoftClientProvider } from 'src/modules/messaging/message-import-manager/drivers/microsoft/providers/microsoft-client.provider'; import { MicrosoftGraphBatchResponse } from 'src/modules/messaging/message-import-manager/drivers/microsoft/services/microsoft-get-messages.interface'; +import { isMicrosoftClientTemporaryError } from 'src/modules/messaging/message-import-manager/drivers/microsoft/utils/is-temporary-error.utils'; @Injectable() export class MicrosoftFetchByBatchService { @@ -42,11 +44,23 @@ export class MicrosoftFetchByBatchService { }, })); - const batchResponse = await client - .api('/$batch') - .post({ requests: batchRequests }); + try { + const batchResponse = await client + .api('/$batch') + .post({ requests: batchRequests }); - batchResponses.push(batchResponse); + batchResponses.push(batchResponse); + } catch (error) { + if ( + error.body && + typeof error.body === 'string' && + isMicrosoftClientTemporaryError(error.body) + ) { + throw new MicrosoftImportDriverException(error.body, error.code, 429); + } else { + throw error; + } + } } return { @@ -54,4 +68,21 @@ export class MicrosoftFetchByBatchService { batchResponses, }; } + + /** + * Microsoft client.api.post sometimes throws (hard to catch) temporary errors like this one: + * + * { + * statusCode: 200, + * code: "SyntaxError", + * requestId: null, + * date: "2025-05-14T11:43:02.024Z", + * body: "SyntaxError: Unexpected token < in JSON at position 19341", + * headers: { + * }, + * } + */ + private isTemporaryError(error: any): boolean { + return error?.body?.includes('Unexpected token < in JSON at position'); + } } diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/microsoft/utils/__tests__/is-temporary-error.utils.spec.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/microsoft/utils/__tests__/is-temporary-error.utils.spec.ts new file mode 100644 index 000000000..4fcb1450a --- /dev/null +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/microsoft/utils/__tests__/is-temporary-error.utils.spec.ts @@ -0,0 +1,29 @@ +import { isMicrosoftClientTemporaryError } from 'src/modules/messaging/message-import-manager/drivers/microsoft/utils/is-temporary-error.utils'; + +describe('isMicrosoftClientTemporaryError', () => { + it('should return true when body contains the expected text', () => { + const error = { + statusCode: 200, + code: 'SyntaxError', + requestId: null, + date: '2025-05-14T11:43:02.024Z', + body: 'SyntaxError: Unexpected token < in JSON at position 19341', + headers: {}, + }; + + expect(isMicrosoftClientTemporaryError(error.body)).toBe(true); + }); + + it('should return false when body does not contain it', () => { + const error = { + statusCode: 400, + code: 'AuthError', + requestId: '123456', + date: '2025-05-14T11:43:02.024Z', + body: 'Authentication failed', + headers: {}, + }; + + expect(isMicrosoftClientTemporaryError(error.body)).toBe(false); + }); +}); diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/microsoft/utils/is-temporary-error.utils.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/microsoft/utils/is-temporary-error.utils.ts new file mode 100644 index 000000000..d470a157a --- /dev/null +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/microsoft/utils/is-temporary-error.utils.ts @@ -0,0 +1,3 @@ +export const isMicrosoftClientTemporaryError = (body: string): boolean => { + return body.includes('Unexpected token < in JSON at position'); +}; diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-import-exception-handler.service.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-import-exception-handler.service.ts index 0870da04f..d7a831fc3 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-import-exception-handler.service.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-import-exception-handler.service.ts @@ -55,6 +55,7 @@ export class MessageImportExceptionHandlerService { syncStep, messageChannel, workspaceId, + exception, ); break; case MessageImportDriverExceptionCode.INSUFFICIENT_PERMISSIONS: @@ -89,6 +90,7 @@ export class MessageImportExceptionHandlerService { 'id' | 'throttleFailureCount' >, workspaceId: string, + exception: MessageImportDriverException, ): Promise { if ( messageChannel.throttleFailureCount >= MESSAGING_THROTTLE_MAX_ATTEMPTS @@ -98,8 +100,21 @@ export class MessageImportExceptionHandlerService { workspaceId, MessageChannelSyncStatus.FAILED_UNKNOWN, ); + + this.exceptionHandlerService.captureExceptions( + [ + `Temporary error occurred ${MESSAGING_THROTTLE_MAX_ATTEMPTS} times while importing messages for message channel ${messageChannel.id.slice(0, 5)}... in workspace ${workspaceId}: ${exception?.message}`, + ], + { + additionalData: { + messageChannelId: messageChannel.id, + }, + workspace: { id: workspaceId }, + }, + ); + throw new MessageImportException( - `Unknown temporary error occurred multiple times while importing messages for message channel ${messageChannel.id} in workspace ${workspaceId}`, + `Temporary error occurred multiple times while importing messages for message channel ${messageChannel.id} in workspace ${workspaceId}: ${exception?.message}`, MessageImportExceptionCode.UNKNOWN, ); } @@ -161,17 +176,19 @@ export class MessageImportExceptionHandlerService { MessageChannelSyncStatus.FAILED_UNKNOWN, ); - this.exceptionHandlerService.captureExceptions([ - new MessageImportException( - `Unknown error importing messages for message channel ${messageChannel.id} in workspace ${workspaceId}: ${exception.message}`, - MessageImportExceptionCode.UNKNOWN, - ), - ]); - - throw new MessageImportException( + const messageImportException = new MessageImportException( exception.message, MessageImportExceptionCode.UNKNOWN, ); + + this.exceptionHandlerService.captureExceptions([messageImportException], { + additionalData: { + messageChannelId: messageChannel.id, + }, + workspace: { id: workspaceId }, + }); + + throw messageImportException; } private async handlePermanentException(