From d88fb2bd65b5629fa8e74bb9e357b7a6738fe78f Mon Sep 17 00:00:00 2001 From: Thomas Trompette Date: Tue, 14 Apr 2026 00:11:57 +0200 Subject: [PATCH] Clean event creation exception (#19561) https://twenty-v7.sentry.io/issues/7351816489/?environment=prod&project=4507072499810304&query=is%3Aunresolved&referrer=issue-stream Those are expected error that should not reach sentry. These happen when stream TTL expires or user session ends --------- Co-authored-by: Charles Bochet --- .../components/SSEQuerySubscribeEffect.tsx | 14 +++------- .../hooks/useTriggerEventStreamCreation.ts | 28 +++++++++++-------- .../isGracefullyHandledEventStreamError.ts | 21 ++++++++++++++ 3 files changed, 41 insertions(+), 22 deletions(-) create mode 100644 packages/twenty-front/src/modules/sse-db-event/utils/isGracefullyHandledEventStreamError.ts diff --git a/packages/twenty-front/src/modules/sse-db-event/components/SSEQuerySubscribeEffect.tsx b/packages/twenty-front/src/modules/sse-db-event/components/SSEQuerySubscribeEffect.tsx index 802ca8b205..aa1a31751b 100644 --- a/packages/twenty-front/src/modules/sse-db-event/components/SSEQuerySubscribeEffect.tsx +++ b/packages/twenty-front/src/modules/sse-db-event/components/SSEQuerySubscribeEffect.tsx @@ -5,10 +5,12 @@ import { requiredQueryListenersState } from '@/sse-db-event/states/requiredQuery import { shouldDestroyEventStreamState } from '@/sse-db-event/states/shouldDestroyEventStreamState'; import { sseEventStreamIdState } from '@/sse-db-event/states/sseEventStreamIdState'; import { sseEventStreamReadyState } from '@/sse-db-event/states/sseEventStreamReadyState'; +import { isGracefullyHandledEventStreamError } from '@/sse-db-event/utils/isGracefullyHandledEventStreamError'; import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; -import { useMutation } from '@apollo/client/react'; import { CombinedGraphQLErrors } from '@apollo/client/errors'; +import { useMutation } from '@apollo/client/react'; import { isNonEmptyString } from '@sniptt/guards'; +import { useStore } from 'jotai'; import { useCallback, useEffect } from 'react'; import { compareArraysOfObjectsByProperty, @@ -19,7 +21,6 @@ import { type AddQuerySubscriptionInput, type RemoveQueryFromEventStreamInput, } from '~/generated-metadata/graphql'; -import { useStore } from 'jotai'; export const SSEQuerySubscribeEffect = () => { const store = useStore(); @@ -90,14 +91,7 @@ export const SSEQuerySubscribeEffect = () => { const subCode = error.errors[0]?.extensions?.subCode; const code = error.errors[0]?.extensions?.code; - const isRecoverable = - subCode === 'EVENT_STREAM_DOES_NOT_EXIST' || - subCode === 'EVENT_STREAM_ALREADY_EXISTS' || - subCode === 'NOT_AUTHORIZED' || - code === 'UNAUTHENTICATED' || - code === 'FORBIDDEN'; - - if (isRecoverable) { + if (isGracefullyHandledEventStreamError({ subCode, code })) { store.set(activeQueryListenersState.atom, []); store.set(shouldDestroyEventStreamState.atom, true); return; diff --git a/packages/twenty-front/src/modules/sse-db-event/hooks/useTriggerEventStreamCreation.ts b/packages/twenty-front/src/modules/sse-db-event/hooks/useTriggerEventStreamCreation.ts index 697d20ef92..80aed5f1c0 100644 --- a/packages/twenty-front/src/modules/sse-db-event/hooks/useTriggerEventStreamCreation.ts +++ b/packages/twenty-front/src/modules/sse-db-event/hooks/useTriggerEventStreamCreation.ts @@ -9,6 +9,7 @@ import { shouldDestroyEventStreamState } from '@/sse-db-event/states/shouldDestr import { sseClientState } from '@/sse-db-event/states/sseClientState'; import { sseEventStreamIdState } from '@/sse-db-event/states/sseEventStreamIdState'; import { sseEventStreamReadyState } from '@/sse-db-event/states/sseEventStreamReadyState'; +import { isGracefullyHandledEventStreamError } from '@/sse-db-event/utils/isGracefullyHandledEventStreamError'; import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState'; import { captureException } from '@sentry/react'; import { isNonEmptyString } from '@sniptt/guards'; @@ -80,9 +81,17 @@ export const useTriggerEventStreamCreation = () => { }>, ) => { if (isDefined(value?.errors) && Array.isArray(value.errors)) { - captureException( - new Error(`SSE subscription error: ${value.errors[0]?.message}`), - ); + const subCode = value.errors[0]?.extensions?.subCode; + const code = value.errors[0]?.extensions?.code; + + if (!isGracefullyHandledEventStreamError({ subCode, code })) { + captureException( + new Error( + `SSE subscription error: ${value.errors[0]?.message}`, + ), + ); + } + store.set(shouldDestroyEventStreamState.atom, true); return; @@ -129,16 +138,11 @@ export const useTriggerEventStreamCreation = () => { if (event === 'next') { if (isDefined(result?.errors)) { const subCode = result.errors[0]?.extensions?.subCode; + const code = result.errors[0]?.extensions?.code; - switch (subCode) { - case 'EVENT_STREAM_ALREADY_EXISTS': { - store.set(shouldDestroyEventStreamState.atom, true); - break; - } - default: { - for (const error of result.errors) { - captureException(error); - } + if (!isGracefullyHandledEventStreamError({ subCode, code })) { + for (const error of result.errors) { + captureException(error); } } diff --git a/packages/twenty-front/src/modules/sse-db-event/utils/isGracefullyHandledEventStreamError.ts b/packages/twenty-front/src/modules/sse-db-event/utils/isGracefullyHandledEventStreamError.ts new file mode 100644 index 0000000000..a272170c11 --- /dev/null +++ b/packages/twenty-front/src/modules/sse-db-event/utils/isGracefullyHandledEventStreamError.ts @@ -0,0 +1,21 @@ +import { isNonEmptyString } from '@sniptt/guards'; + +export const isGracefullyHandledEventStreamError = ({ + subCode, + code, +}: { + subCode?: unknown; + code?: unknown; +}) => { + if (!isNonEmptyString(subCode) && !isNonEmptyString(code)) { + return false; + } + + return ( + subCode === 'EVENT_STREAM_DOES_NOT_EXIST' || + subCode === 'EVENT_STREAM_ALREADY_EXISTS' || + subCode === 'NOT_AUTHORIZED' || + code === 'UNAUTHENTICATED' || + code === 'FORBIDDEN' + ); +};