From 22591572310e7d922f68f4fbaf59ebb12784b89f Mon Sep 17 00:00:00 2001 From: Mary Hipp Date: Tue, 15 Jul 2025 12:02:46 -0400 Subject: [PATCH] explicitly send details on image upload and enqueue failures --- .../listeners/batchEnqueued.ts | 20 ++++++++++++++++++- .../listeners/imageUploaded.ts | 12 +++++++++-- .../src/common/hooks/useImageUploadButton.tsx | 11 +++++++++- .../workflow/PublishWorkflowPanelContent.tsx | 16 +++++++++++++-- .../features/queue/hooks/useEnqueueCanvas.ts | 2 ++ .../queue/hooks/useEnqueueGenerate.ts | 2 ++ .../web/src/features/queue/hooks/useInvoke.ts | 13 ++++++++++-- .../web/src/features/system/store/actions.ts | 6 ++++++ .../src/features/system/store/zodSchemas.ts | 6 ++++++ 9 files changed, 80 insertions(+), 8 deletions(-) diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/batchEnqueued.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/batchEnqueued.ts index 20d459e3fcb..26f9feb7ac0 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/batchEnqueued.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/batchEnqueued.ts @@ -1,6 +1,7 @@ import { logger } from 'app/logging/logger'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; import { truncate } from 'es-toolkit/compat'; +import { trackErrorDetails } from 'features/system/store/actions'; import { zPydanticValidationError } from 'features/system/store/zodSchemas'; import { toast } from 'features/toast/toast'; import { t } from 'i18next'; @@ -34,7 +35,7 @@ export const addBatchEnqueuedListener = (startAppListening: AppStartListening) = // error startAppListening({ matcher: queueApi.endpoints.enqueueBatch.matchRejected, - effect: (action) => { + effect: (action, { dispatch }) => { const response = action.payload; const batchConfig = action.meta.arg.originalArgs; @@ -46,6 +47,13 @@ export const addBatchEnqueuedListener = (startAppListening: AppStartListening) = description: t('common.unknownError'), }); log.error({ batchConfig } as JsonObject, t('queue.batchFailedToQueue')); + dispatch( + trackErrorDetails({ + title: 'Enqueue Batch Rejected', + errorMessage: t('common.unknownError'), + description: null, + }) + ); return; } @@ -59,6 +67,9 @@ export const addBatchEnqueuedListener = (startAppListening: AppStartListening) = status: 'error', description, }); + dispatch( + trackErrorDetails({ title: 'Enqueue Batch Rejected', errorMessage: e.msg, description: description }) + ); }); } else if (response.status !== 403) { toast({ @@ -69,6 +80,13 @@ export const addBatchEnqueuedListener = (startAppListening: AppStartListening) = }); } log.error({ batchConfig, error: serializeError(response) } as JsonObject, t('queue.batchFailedToQueue')); + dispatch( + trackErrorDetails({ + title: 'Enqueue Batch Rejected', + errorMessage: t('common.unknownError'), + description: null, + }) + ); }, }); }; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts index c131ce0e9cc..6f5a3edcdce 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts @@ -6,6 +6,8 @@ import { omit } from 'es-toolkit/compat'; import { imageUploadedClientSide } from 'features/gallery/store/actions'; import { selectListBoardsQueryArgs } from 'features/gallery/store/gallerySelectors'; import { boardIdSelected, galleryViewChanged } from 'features/gallery/store/gallerySlice'; +import { trackErrorDetails } from 'features/system/store/actions'; +import { zPydanticValidationErrorWithDetail } from 'features/system/store/zodSchemas'; import { toast } from 'features/toast/toast'; import { t } from 'i18next'; import { boardsApi } from 'services/api/endpoints/boards'; @@ -130,7 +132,7 @@ export const addImageUploadedFulfilledListener = (startAppListening: AppStartLis startAppListening({ matcher: imagesApi.endpoints.uploadImage.matchRejected, - effect: (action) => { + effect: (action, { dispatch }) => { const sanitizedData = { arg: { ...omit(action.meta.arg.originalArgs, ['file', 'postUploadAction']), @@ -138,9 +140,15 @@ export const addImageUploadedFulfilledListener = (startAppListening: AppStartLis }, }; log.error({ ...sanitizedData }, 'Image upload failed'); + + const parsedError = zPydanticValidationErrorWithDetail.safeParse(action.payload); + const errorMessage = parsedError.success ? parsedError.data.data.detail : action.error.message; + + dispatch(trackErrorDetails({ title: 'Image Upload Rejected', errorMessage, description: null })); + toast({ title: t('toast.imageUploadFailed'), - description: action.error.message, + description: errorMessage, status: 'error', }); }, diff --git a/invokeai/frontend/web/src/common/hooks/useImageUploadButton.tsx b/invokeai/frontend/web/src/common/hooks/useImageUploadButton.tsx index ff53b8ec79d..df812c7f406 100644 --- a/invokeai/frontend/web/src/common/hooks/useImageUploadButton.tsx +++ b/invokeai/frontend/web/src/common/hooks/useImageUploadButton.tsx @@ -1,9 +1,11 @@ import type { ButtonProps, IconButtonProps, SystemStyleObject } from '@invoke-ai/ui-library'; import { Button, IconButton } from '@invoke-ai/ui-library'; import { logger } from 'app/logging/logger'; -import { useAppSelector } from 'app/store/storeHooks'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { selectAutoAddBoardId } from 'features/gallery/store/gallerySelectors'; +import { trackErrorDetails } from 'features/system/store/actions'; import { selectIsClientSideUploadEnabled } from 'features/system/store/configSlice'; +import { zPydanticValidationErrorWithDetail } from 'features/system/store/zodSchemas'; import { toast } from 'features/toast/toast'; import { memo, useCallback } from 'react'; import type { FileRejection } from 'react-dropzone'; @@ -65,6 +67,7 @@ export const useImageUploadButton = ({ const [uploadImage, request] = useUploadImageMutation(); const clientSideUpload = useClientSideUpload(); const { t } = useTranslation(); + const dispatch = useAppDispatch(); const onDropAccepted = useCallback( async (files: File[]) => { @@ -116,6 +119,11 @@ export const useImageUploadButton = ({ } } catch (error) { onError?.(error); + const parsedError = zPydanticValidationErrorWithDetail.safeParse(error); + const errorMessage = parsedError.success ? parsedError.data.data.detail : undefined; + + dispatch(trackErrorDetails({ title: 'Failed to upload image', errorMessage, description: null })); + toast({ id: 'UPLOAD_FAILED', title: t('toast.imageUploadFailed'), @@ -133,6 +141,7 @@ export const useImageUploadButton = ({ clientSideUpload, onError, t, + dispatch, ] ); diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/PublishWorkflowPanelContent.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/PublishWorkflowPanelContent.tsx index b2de652ff88..3c019d14c0b 100644 --- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/PublishWorkflowPanelContent.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/PublishWorkflowPanelContent.tsx @@ -13,7 +13,7 @@ import { import { useStore } from '@nanostores/react'; import { logger } from 'app/logging/logger'; import { $projectUrl } from 'app/store/nanostores/projectId'; -import { useAppSelector } from 'app/store/storeHooks'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent'; import { withResultAsync } from 'common/util/result'; import { parseify } from 'common/util/serialize'; @@ -40,6 +40,7 @@ import { useOutputFieldTemplate } from 'features/nodes/hooks/useOutputFieldTempl import { useZoomToNode } from 'features/nodes/hooks/useZoomToNode'; import { useEnqueueWorkflows } from 'features/queue/hooks/useEnqueueWorkflows'; import { $isReadyToEnqueue } from 'features/queue/store/readiness'; +import { trackErrorDetails } from 'features/system/store/actions'; import { selectAllowPublishWorkflows } from 'features/system/store/configSlice'; import { toast } from 'features/toast/toast'; import type { PropsWithChildren } from 'react'; @@ -209,6 +210,7 @@ const PublishWorkflowButton = memo(() => { const isSelectingOutputNode = useStore($isSelectingOutputNode); const inputs = usePublishInputs(); const allowPublishWorkflows = useAppSelector(selectAllowPublishWorkflows); + const dispatch = useAppDispatch(); const projectUrl = useStore($projectUrl); @@ -217,6 +219,9 @@ const PublishWorkflowButton = memo(() => { $isPublishing.set(true); const result = await withResultAsync(() => enqueue(true, true)); if (result.isErr()) { + dispatch( + trackErrorDetails({ title: 'Failed to enqueue batch', errorMessage: result.error.message, description: null }) + ); toast({ id: 'TOAST_PUBLISH_FAILED', status: 'error', @@ -225,6 +230,13 @@ const PublishWorkflowButton = memo(() => { duration: null, }); log.error({ error: serializeError(result.error) }, 'Failed to enqueue batch'); + dispatch( + trackErrorDetails({ + title: 'Failed to enqueue batch', + errorMessage: serializeError(result.error).message, + description: serializeError(result.error).stack?.toString() ?? null, + }) + ); } else { toast({ id: 'TOAST_PUBLISH_SUCCESSFUL', @@ -249,7 +261,7 @@ const PublishWorkflowButton = memo(() => { log.debug(parseify(result.value), 'Enqueued batch'); } $isPublishing.set(false); - }, [enqueue, projectUrl, t]); + }, [enqueue, projectUrl, t, dispatch]); const isDisabled = useMemo(() => { return ( diff --git a/invokeai/frontend/web/src/features/queue/hooks/useEnqueueCanvas.ts b/invokeai/frontend/web/src/features/queue/hooks/useEnqueueCanvas.ts index b69552847d5..541da3e8e22 100644 --- a/invokeai/frontend/web/src/features/queue/hooks/useEnqueueCanvas.ts +++ b/invokeai/frontend/web/src/features/queue/hooks/useEnqueueCanvas.ts @@ -21,6 +21,7 @@ import { buildSD3Graph } from 'features/nodes/util/graph/generation/buildSD3Grap import { buildSDXLGraph } from 'features/nodes/util/graph/generation/buildSDXLGraph'; import type { GraphBuilderArg } from 'features/nodes/util/graph/types'; import { UnsupportedGenerationModeError } from 'features/nodes/util/graph/types'; +import { trackErrorDetails } from 'features/system/store/actions'; import { toast } from 'features/toast/toast'; import { useCallback } from 'react'; import { serializeError } from 'serialize-error'; @@ -89,6 +90,7 @@ const enqueueCanvas = async (store: AppStore, canvasManager: CanvasManager, prep } const error = serializeError(buildGraphResult.error); log.error({ error }, 'Failed to build graph'); + dispatch(trackErrorDetails({ title, errorMessage: error.message, description })); toast({ status, title, diff --git a/invokeai/frontend/web/src/features/queue/hooks/useEnqueueGenerate.ts b/invokeai/frontend/web/src/features/queue/hooks/useEnqueueGenerate.ts index a6a1d2ecfec..1f7c9521672 100644 --- a/invokeai/frontend/web/src/features/queue/hooks/useEnqueueGenerate.ts +++ b/invokeai/frontend/web/src/features/queue/hooks/useEnqueueGenerate.ts @@ -19,6 +19,7 @@ import { buildSD3Graph } from 'features/nodes/util/graph/generation/buildSD3Grap import { buildSDXLGraph } from 'features/nodes/util/graph/generation/buildSDXLGraph'; import type { GraphBuilderArg } from 'features/nodes/util/graph/types'; import { UnsupportedGenerationModeError } from 'features/nodes/util/graph/types'; +import { trackErrorDetails } from 'features/system/store/actions'; import { toast } from 'features/toast/toast'; import { useCallback } from 'react'; import { serializeError } from 'serialize-error'; @@ -86,6 +87,7 @@ const enqueueGenerate = async (store: AppStore, prepend: boolean) => { status = 'warning'; } const error = serializeError(buildGraphResult.error); + dispatch(trackErrorDetails({ title, errorMessage: error.message, description })); log.error({ error }, 'Failed to build graph'); toast({ status, diff --git a/invokeai/frontend/web/src/features/queue/hooks/useInvoke.ts b/invokeai/frontend/web/src/features/queue/hooks/useInvoke.ts index 668b9d44401..dbe89461aa4 100644 --- a/invokeai/frontend/web/src/features/queue/hooks/useInvoke.ts +++ b/invokeai/frontend/web/src/features/queue/hooks/useInvoke.ts @@ -1,10 +1,11 @@ import { useStore } from '@nanostores/react'; import { logger } from 'app/logging/logger'; -import { useAppSelector } from 'app/store/storeHooks'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { withResultAsync } from 'common/util/result'; import { useIsWorkflowEditorLocked } from 'features/nodes/hooks/useIsWorkflowEditorLocked'; import { useEnqueueWorkflows } from 'features/queue/hooks/useEnqueueWorkflows'; import { $isReadyToEnqueue } from 'features/queue/store/readiness'; +import { trackErrorDetails } from 'features/system/store/actions'; import { navigationApi } from 'features/ui/layouts/navigation-api'; import { VIEWER_PANEL_ID, WORKSPACE_PANEL_ID } from 'features/ui/layouts/shared'; import { selectActiveTab } from 'features/ui/store/uiSelectors'; @@ -26,6 +27,7 @@ export const useInvoke = () => { const enqueueCanvas = useEnqueueCanvas(); const enqueueGenerate = useEnqueueGenerate(); const enqueueUpscaling = useEnqueueUpscaling(); + const dispatch = useAppDispatch(); const [_, { isLoading }] = useEnqueueBatchMutation({ ...enqueueMutationFixedCacheKeyOptions, @@ -55,9 +57,16 @@ export const useInvoke = () => { if (result.isErr()) { log.error({ error: serializeError(result.error) }, 'Failed to enqueue batch'); + dispatch( + trackErrorDetails({ + title: 'Failed to enqueue batch', + errorMessage: serializeError(result.error).message, + description: serializeError(result.error).stack?.toString() ?? null, + }) + ); } }, - [enqueueCanvas, enqueueGenerate, enqueueUpscaling, enqueueWorkflows, isReady, tabName] + [enqueueCanvas, enqueueGenerate, enqueueUpscaling, enqueueWorkflows, isReady, tabName, dispatch] ); const enqueueBack = useCallback(() => { diff --git a/invokeai/frontend/web/src/features/system/store/actions.ts b/invokeai/frontend/web/src/features/system/store/actions.ts index 4be0a516256..c579cec6f4d 100644 --- a/invokeai/frontend/web/src/features/system/store/actions.ts +++ b/invokeai/frontend/web/src/features/system/store/actions.ts @@ -2,3 +2,9 @@ import { createAction } from '@reduxjs/toolkit'; export const videoModalLinkClicked = createAction('system/videoModalLinkClicked'); export const videoModalOpened = createAction('system/videoModalOpened'); + +export const trackErrorDetails = createAction<{ + title: string; + errorMessage?: string; + description: string | null; +}>('system/trackErrorDetails'); diff --git a/invokeai/frontend/web/src/features/system/store/zodSchemas.ts b/invokeai/frontend/web/src/features/system/store/zodSchemas.ts index 4ea013b18aa..399f84c3a8d 100644 --- a/invokeai/frontend/web/src/features/system/store/zodSchemas.ts +++ b/invokeai/frontend/web/src/features/system/store/zodSchemas.ts @@ -12,3 +12,9 @@ export const zPydanticValidationError = z.object({ ), }), }); + +export const zPydanticValidationErrorWithDetail = z.object({ + data: z.object({ + detail: z.string(), + }), +});