From fb02bf66ee02132bdfa52df147494b81e0668bbc Mon Sep 17 00:00:00 2001 From: Sean Teramae Date: Wed, 10 Jun 2026 17:06:18 -0700 Subject: [PATCH 1/7] fix(studio): Ensure all console statements are wrapped in websiteLogger Signed-off-by: Sean Teramae --- .../studio/src/api/datasets/useDatasetFileContent.ts | 3 ++- .../studio/src/components/DatasetInputFile/index.tsx | 5 ++++- .../studio/src/components/FilesetActionMenu/index.tsx | 3 ++- .../studio/src/components/ModelComparePrompts/index.tsx | 3 ++- .../studio/src/components/WorkspaceDropdown/index.tsx | 3 ++- .../components/evaluation/Configurations/form/InputFile.tsx | 3 ++- .../studio/src/components/evaluation/Jobs/ActionMenu.tsx | 3 ++- .../studio/src/components/evaluation/Jobs/DetailsPanel.tsx | 3 ++- .../evaluation/Jobs/EvaluationJobBulkDeleteModal.tsx | 3 ++- .../src/components/filesets/AddToFolderModal/index.tsx | 3 ++- .../filesets/FilesetFileExplorer/BulkDeleteModal/index.tsx | 3 ++- .../FilesetFileExplorer/DatasetFileDropzone/index.tsx | 5 +++-- .../studio/src/components/filesets/hooks/useBulkDownload.ts | 3 ++- .../studio/src/components/filesets/hooks/useBulkDuplicate.ts | 3 ++- .../sidePanels/AgentPanels/AgentPanel/DeploymentLogsView.tsx | 5 ++++- .../DeploymentsListRoute/useDeleteDeploymentAndConfig.ts | 3 ++- .../studio/src/routes/FilesetListRoute/ActionMenu/index.tsx | 3 ++- .../routes/FilesetListRoute/DatasetBulkDeleteModal/index.tsx | 3 ++- .../studio/src/routes/SafeSynthesizerNewRoute/index.tsx | 5 +++-- .../studio/src/routes/agents/AgentSuggestionsRoute/utils.ts | 3 ++- web/packages/studio/src/util/files.ts | 3 ++- web/packages/studio/src/util/llm.ts | 3 ++- web/packages/studio/src/util/strings.ts | 3 ++- 23 files changed, 52 insertions(+), 25 deletions(-) diff --git a/web/packages/studio/src/api/datasets/useDatasetFileContent.ts b/web/packages/studio/src/api/datasets/useDatasetFileContent.ts index 5b824a6011..bb169b1b99 100644 --- a/web/packages/studio/src/api/datasets/useDatasetFileContent.ts +++ b/web/packages/studio/src/api/datasets/useDatasetFileContent.ts @@ -10,6 +10,7 @@ import { import type { EntityIdentifier } from '@studio/api/common/types'; import { getDatasetFileContentQueryKey } from '@studio/api/datasets/invalidateDatasetCaches'; import { isBinaryExtension } from '@studio/util/binaryFile'; +import { websiteLogger } from '@studio/util/logger'; import { queryOptions, useQuery, UseQueryOptions, useSuspenseQuery } from '@tanstack/react-query'; import { parquetRead } from 'hyparquet'; @@ -77,7 +78,7 @@ export const datasetFileContentQueryOptions = ({ }); return data; } catch (err) { - console.error(err); + websiteLogger.error(String(err)); throw new Error('Invalid response while downloading parquet file'); } } else { diff --git a/web/packages/studio/src/components/DatasetInputFile/index.tsx b/web/packages/studio/src/components/DatasetInputFile/index.tsx index ea194b6863..95344ddd16 100644 --- a/web/packages/studio/src/components/DatasetInputFile/index.tsx +++ b/web/packages/studio/src/components/DatasetInputFile/index.tsx @@ -27,6 +27,7 @@ import type { import { useDatasetInputFileReducer } from '@studio/components/DatasetInputFile/useDatasetInputFileReducer'; import { useWorkspaceFromPath } from '@studio/hooks/useWorkspaceFromPath'; import { getDatasetDisplayNameFromFilesUrl } from '@studio/util/files'; +import { websiteLogger } from '@studio/util/logger'; import { useQueryClient } from '@tanstack/react-query'; import { Plus, CircleCheck, CircleHelp, Eye, File as FileIcon, Trash2 } from 'lucide-react'; import { FC, useCallback, useEffect, useRef } from 'react'; @@ -126,7 +127,9 @@ export const DatasetInputFile: FC = ({ const datasetWorkspace = file.dataset?.workspace; const datasetName = file.dataset?.name; if (!datasetWorkspace || !datasetName) { - console.error('Cannot load dataset file: missing workspace or name', file.dataset); + websiteLogger.error( + `Cannot load dataset file: missing workspace or name: ${JSON.stringify(file.dataset)}` + ); return; } diff --git a/web/packages/studio/src/components/FilesetActionMenu/index.tsx b/web/packages/studio/src/components/FilesetActionMenu/index.tsx index b82b60bea9..c2db472139 100644 --- a/web/packages/studio/src/components/FilesetActionMenu/index.tsx +++ b/web/packages/studio/src/components/FilesetActionMenu/index.tsx @@ -15,6 +15,7 @@ import { invalidateDatasetCaches } from '@studio/api/datasets/invalidateDatasetC import { DatasetCreateModal } from '@studio/components/DatasetCreateModal'; import { DatasetCreateModalMode } from '@studio/components/DatasetCreateModal/constants'; import { DeleteConfirmationModal } from '@studio/components/DeleteConfirmationModal'; +import { websiteLogger } from '@studio/util/logger'; import { EllipsisVertical } from 'lucide-react'; import { type FC, useState } from 'react'; @@ -49,7 +50,7 @@ export const FilesetActionMenu: FC = ({ onFilesetDeleted?.(fileset); return true; } catch (error) { - console.error('Failed to delete fileset:', error); + websiteLogger.error(`Failed to delete fileset: ${String(error)}`); return false; } }; diff --git a/web/packages/studio/src/components/ModelComparePrompts/index.tsx b/web/packages/studio/src/components/ModelComparePrompts/index.tsx index 5c352804fb..ed9a9875a3 100644 --- a/web/packages/studio/src/components/ModelComparePrompts/index.tsx +++ b/web/packages/studio/src/components/ModelComparePrompts/index.tsx @@ -12,6 +12,7 @@ import { Button, Flex, Modal, Stack, Text } from '@nvidia/foundations-react-core import { DatasetInputFile, type DatasetInputFileResult } from '@studio/components/DatasetInputFile'; import { FileSamplingMethodSelect } from '@studio/components/FileSamplingSnippet/FileSamplingMethodSelect'; import type { SharedModelEntry } from '@studio/routes/ModelCompareRoute/types'; +import { websiteLogger } from '@studio/util/logger'; import { Loader2, Maximize2, Play, Trash2 } from 'lucide-react'; import { type FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'; @@ -178,7 +179,7 @@ export const ModelComparePrompts: FC = ({ writeCell(row.sourceIndex, model.id, content); }) .catch((error) => { - console.error('Inference request failed:', error); + websiteLogger.error(`Inference request failed: ${String(error)}`); writeCell(row.sourceIndex, model.id, null); }) ); diff --git a/web/packages/studio/src/components/WorkspaceDropdown/index.tsx b/web/packages/studio/src/components/WorkspaceDropdown/index.tsx index 92abf9e407..7871b3362e 100644 --- a/web/packages/studio/src/components/WorkspaceDropdown/index.tsx +++ b/web/packages/studio/src/components/WorkspaceDropdown/index.tsx @@ -23,6 +23,7 @@ import { useRecentWorkspaces } from '@studio/components/WorkspaceDropdown/useRec import { DEFAULT_LARGE_PAGE_SIZE } from '@studio/constants/constants'; import { getWorkspaceDetailsDefaultRoute } from '@studio/routes/utils'; import { useBoolean } from '@studio/util/hooks/useBoolean'; +import { websiteLogger } from '@studio/util/logger'; import cn from 'classnames'; import { Plus, Filter } from 'lucide-react'; import { ChangeEvent, FC, lazy, useMemo, useState } from 'react'; @@ -117,7 +118,7 @@ export const WorkspaceDropdown: FC = ({ onValueChange }) => { // Open modal for new workspace openModal(); } else { - console.error('Workspace selected without name:', newWorkspace); + websiteLogger.error(`Workspace selected without name: ${JSON.stringify(newWorkspace)}`); } return; } diff --git a/web/packages/studio/src/components/evaluation/Configurations/form/InputFile.tsx b/web/packages/studio/src/components/evaluation/Configurations/form/InputFile.tsx index e1d16b94b7..a650914fde 100644 --- a/web/packages/studio/src/components/evaluation/Configurations/form/InputFile.tsx +++ b/web/packages/studio/src/components/evaluation/Configurations/form/InputFile.tsx @@ -34,6 +34,7 @@ import { import { useFileValidation } from '@studio/hooks/evaluation/useFileValidation'; import { useWorkspaceFromPath } from '@studio/hooks/useWorkspaceFromPath'; import { getDatasetDisplayNameFromFilesUrl } from '@studio/util/files'; +import { websiteLogger } from '@studio/util/logger'; import { useQueryClient } from '@tanstack/react-query'; import { Plus, CircleCheck, CircleHelp, File as FileIcon } from 'lucide-react'; import { FC, useState, useCallback, useEffect, useMemo } from 'react'; @@ -406,7 +407,7 @@ export const InputFile: FC = ({ updateFormFromFile(validationResult, undefined); } } catch (error) { - console.error('Failed to validate selected file:', error); + websiteLogger.error(`Failed to validate selected file: ${String(error)}`); // Create error validation result const errorResult: FileValidationResult = { isValid: false, diff --git a/web/packages/studio/src/components/evaluation/Jobs/ActionMenu.tsx b/web/packages/studio/src/components/evaluation/Jobs/ActionMenu.tsx index 1553a1f9e7..485f2db580 100644 --- a/web/packages/studio/src/components/evaluation/Jobs/ActionMenu.tsx +++ b/web/packages/studio/src/components/evaluation/Jobs/ActionMenu.tsx @@ -17,6 +17,7 @@ import { } from '@nvidia/foundations-react-core'; import { useWorkspaceFromPath } from '@studio/hooks/useWorkspaceFromPath'; import { getEvaluationJobName } from '@studio/selectors/evaluationJob'; +import { websiteLogger } from '@studio/util/logger'; import { ArrowRight, EllipsisVertical, Trash } from 'lucide-react'; import { FC, useState } from 'react'; @@ -45,7 +46,7 @@ export const ActionMenu: FC = ({ job, onNavigateToDetails, onJo onJobDeleted?.(job); handleModalClose(); } catch (error) { - console.error('Failed to delete evaluation job:', error); + websiteLogger.error(`Failed to delete evaluation job: ${String(error)}`); toast.error('Failed to delete evaluation job. Please try again.'); } finally { setIsDeleting(false); diff --git a/web/packages/studio/src/components/evaluation/Jobs/DetailsPanel.tsx b/web/packages/studio/src/components/evaluation/Jobs/DetailsPanel.tsx index 1acc20bfbe..12d2f1446e 100644 --- a/web/packages/studio/src/components/evaluation/Jobs/DetailsPanel.tsx +++ b/web/packages/studio/src/components/evaluation/Jobs/DetailsPanel.tsx @@ -24,6 +24,7 @@ import { Banner, Button, Flex, Modal, Panel, Stack, Text } from '@nvidia/foundat import { ButtonLaunchEvaluation } from '@studio/components/evaluation/ButtonLaunchEvaluation'; import { StatusLogsContent } from '@studio/components/evaluation/Jobs/StatusLogsContent'; import { useWorkspaceFromPath } from '@studio/hooks/useWorkspaceFromPath'; +import { websiteLogger } from '@studio/util/logger'; import { useQueryClient } from '@tanstack/react-query'; import { ChartBar, LayoutList, CircleX } from 'lucide-react'; import { useState } from 'react'; @@ -59,7 +60,7 @@ export const DetailsPanel = ({ evaluationJob, error }: DetailsPanelProps) => { setCancelModalOpen(false); } catch (error) { - console.error('Failed to cancel job:', error); + websiteLogger.error(`Failed to cancel job: ${String(error)}`); toast.error('Failed to cancel job. Please try again.'); } }; diff --git a/web/packages/studio/src/components/evaluation/Jobs/EvaluationJobBulkDeleteModal.tsx b/web/packages/studio/src/components/evaluation/Jobs/EvaluationJobBulkDeleteModal.tsx index b37771abc2..4cefcf0468 100644 --- a/web/packages/studio/src/components/evaluation/Jobs/EvaluationJobBulkDeleteModal.tsx +++ b/web/packages/studio/src/components/evaluation/Jobs/EvaluationJobBulkDeleteModal.tsx @@ -6,6 +6,7 @@ import { useEvaluatorDeleteEvaluateJob } from '@nemo/sdk/generated/evaluator/api import { Button, Flex, Modal } from '@nvidia/foundations-react-core'; import { useMutateMany } from '@studio/api/common/useMutateMany'; import { useWorkspaceFromPath } from '@studio/hooks/useWorkspaceFromPath'; +import { websiteLogger } from '@studio/util/logger'; import { Trash } from 'lucide-react'; import { FC, useState } from 'react'; @@ -38,7 +39,7 @@ export const EvaluationJobBulkDeleteModal: FC onConfirmSuccess(); setOpen(false); } catch (error) { - console.error('Failed to delete evaluation jobs:', error); + websiteLogger.error(`Failed to delete evaluation jobs: ${String(error)}`); toast.error('Failed to delete evaluation jobs. Please try again.'); } }; diff --git a/web/packages/studio/src/components/filesets/AddToFolderModal/index.tsx b/web/packages/studio/src/components/filesets/AddToFolderModal/index.tsx index 49ad25002d..893407e81e 100644 --- a/web/packages/studio/src/components/filesets/AddToFolderModal/index.tsx +++ b/web/packages/studio/src/components/filesets/AddToFolderModal/index.tsx @@ -28,6 +28,7 @@ import { FileSystemNode } from '@studio/components/FilesTable/utils'; import { useDatasetNavigator } from '@studio/hooks/useDatasetNavigator'; import { useWorkspaceFromPath } from '@studio/hooks/useWorkspaceFromPath'; import { getFilesetDetailsRoute } from '@studio/routes/utils'; +import { websiteLogger } from '@studio/util/logger'; import { FolderOpen, Info } from 'lucide-react'; import { type FC, useMemo } from 'react'; import { useForm, useWatch } from 'react-hook-form'; @@ -178,7 +179,7 @@ export const AddToFolderModal: FC = ({ const navigateToParentFolder = () => { if (!routeWorkspace) { - console.warn('AddToFolderModal: Cannot navigate - workspace context not available'); + websiteLogger.warn('AddToFolderModal: Cannot navigate - workspace context not available'); return; } diff --git a/web/packages/studio/src/components/filesets/FilesetFileExplorer/BulkDeleteModal/index.tsx b/web/packages/studio/src/components/filesets/FilesetFileExplorer/BulkDeleteModal/index.tsx index de123b61b8..be2e62db54 100644 --- a/web/packages/studio/src/components/filesets/FilesetFileExplorer/BulkDeleteModal/index.tsx +++ b/web/packages/studio/src/components/filesets/FilesetFileExplorer/BulkDeleteModal/index.tsx @@ -5,6 +5,7 @@ import { Button, Flex, Modal } from '@nvidia/foundations-react-core'; import { useDatasetFilesDelete } from '@studio/api/datasets/useDatasetFilesDelete'; import { extractFilePathsFromDirectory } from '@studio/components/filesets/FilesetFileExplorer/BulkDeleteModal/utils'; import { FileSystemNode } from '@studio/components/FilesTable/utils'; +import { websiteLogger } from '@studio/util/logger'; import { Trash } from 'lucide-react'; import { FC, ReactNode, useState } from 'react'; @@ -56,7 +57,7 @@ export const BulkDeleteModal: FC = ({ setOpen(false); } catch (error) { // Error handling is managed by the mutation hooks - console.error('Failed to delete items:', error); + websiteLogger.error(`Failed to delete items: ${String(error)}`); } }; diff --git a/web/packages/studio/src/components/filesets/FilesetFileExplorer/DatasetFileDropzone/index.tsx b/web/packages/studio/src/components/filesets/FilesetFileExplorer/DatasetFileDropzone/index.tsx index 48bea9bfbf..f69995d0b1 100644 --- a/web/packages/studio/src/components/filesets/FilesetFileExplorer/DatasetFileDropzone/index.tsx +++ b/web/packages/studio/src/components/filesets/FilesetFileExplorer/DatasetFileDropzone/index.tsx @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { Block, Flex, Text } from '@nvidia/foundations-react-core'; +import { websiteLogger } from '@studio/util/logger'; import { FC, useCallback, ReactNode } from 'react'; import { useDropzone, FileRejection } from 'react-dropzone'; @@ -27,14 +28,14 @@ export const DatasetFileDropzone: FC = ({ onUpload(acceptedFiles); } } catch (error) { - console.error('Error handling accepted files:', error); + websiteLogger.error(`Error handling accepted files: ${String(error)}`); } }, [onUpload] ); const handleDropRejected = useCallback((fileRejections: FileRejection[]) => { - console.error('Files rejected:', fileRejections); + websiteLogger.error(`Files rejected: ${JSON.stringify(fileRejections)}`); }, []); const { diff --git a/web/packages/studio/src/components/filesets/hooks/useBulkDownload.ts b/web/packages/studio/src/components/filesets/hooks/useBulkDownload.ts index f81207db0e..20fb8ef872 100644 --- a/web/packages/studio/src/components/filesets/hooks/useBulkDownload.ts +++ b/web/packages/studio/src/components/filesets/hooks/useBulkDownload.ts @@ -6,6 +6,7 @@ import { triggerDownload } from '@nemo/common/src/utils/file'; import { useDownloadFileAsArrayBuffer } from '@studio/components/filesets/hooks/useDownloadFileAsArrayBuffer'; import { FileSystemFile } from '@studio/components/FilesTable/utils'; import { getFileNameFromPath } from '@studio/util/files'; +import { websiteLogger } from '@studio/util/logger'; import { useCallback, useState } from 'react'; export interface UseBulkDownloadOptions { @@ -70,7 +71,7 @@ export function useBulkDownload(options: UseBulkDownloadOptions): UseBulkDownloa ); } } catch (err) { - console.error('Bulk download failed', err); + websiteLogger.error(`Bulk download failed: ${String(err)}`); toast.dismissToast(toastId); toast.error(files.length === 1 ? 'Failed to download file' : 'Failed to download files'); } finally { diff --git a/web/packages/studio/src/components/filesets/hooks/useBulkDuplicate.ts b/web/packages/studio/src/components/filesets/hooks/useBulkDuplicate.ts index 96f5899ac1..4143b81cdc 100644 --- a/web/packages/studio/src/components/filesets/hooks/useBulkDuplicate.ts +++ b/web/packages/studio/src/components/filesets/hooks/useBulkDuplicate.ts @@ -6,6 +6,7 @@ import { filesListFilesetFiles } from '@nemo/sdk/generated/platform/api'; import { useDatasetFilesUpload } from '@studio/api/datasets/useDatasetFilesUpload'; import { useDownloadFileAsArrayBuffer } from '@studio/components/filesets/hooks/useDownloadFileAsArrayBuffer'; import { FileSystemFile } from '@studio/components/FilesTable/utils'; +import { websiteLogger } from '@studio/util/logger'; import { useCallback, useState } from 'react'; export interface UseBulkDuplicateOptions { @@ -150,7 +151,7 @@ export function useBulkDuplicate(options: UseBulkDuplicateOptions): UseBulkDupli return false; } } catch (err) { - console.error('Bulk duplicate failed', err); + websiteLogger.error(`Bulk duplicate failed: ${String(err)}`); toast.dismissToast(toastId); toast.error(files.length === 1 ? 'Failed to duplicate file' : 'Failed to duplicate files'); return false; diff --git a/web/packages/studio/src/components/sidePanels/AgentPanels/AgentPanel/DeploymentLogsView.tsx b/web/packages/studio/src/components/sidePanels/AgentPanels/AgentPanel/DeploymentLogsView.tsx index 1457e3fd94..89b10cd73e 100644 --- a/web/packages/studio/src/components/sidePanels/AgentPanels/AgentPanel/DeploymentLogsView.tsx +++ b/web/packages/studio/src/components/sidePanels/AgentPanels/AgentPanel/DeploymentLogsView.tsx @@ -10,6 +10,7 @@ import type { AgentDeployment } from '@nemo/sdk/generated/agents/schema'; import type { PlatformJobLog } from '@nemo/sdk/generated/platform/schema'; import { Block, Select, Stack, Text } from '@nvidia/foundations-react-core'; import { PLATFORM_BASE_URL } from '@studio/constants/environment'; +import { websiteLogger } from '@studio/util/logger'; import { streamSse } from '@studio/util/sseStream'; import { type FC, useEffect, useMemo, useState } from 'react'; import { useAuth } from 'react-oidc-context'; @@ -135,7 +136,9 @@ const LogsForDeployment: FC = ({ workspace, deploymentNa } }, onError: (err) => { - console.warn(`Log stream interrupted for deployment ${deploymentName}; retrying.`, err); + websiteLogger.warn( + `Log stream interrupted for deployment ${deploymentName}; retrying. ${String(err)}` + ); }, }); return () => controller.abort(); diff --git a/web/packages/studio/src/routes/DeploymentsListRoute/useDeleteDeploymentAndConfig.ts b/web/packages/studio/src/routes/DeploymentsListRoute/useDeleteDeploymentAndConfig.ts index f729516ddc..4e26fcce80 100644 --- a/web/packages/studio/src/routes/DeploymentsListRoute/useDeleteDeploymentAndConfig.ts +++ b/web/packages/studio/src/routes/DeploymentsListRoute/useDeleteDeploymentAndConfig.ts @@ -37,6 +37,7 @@ import { HUGGING_FACE_DEPLOYMENT_SOURCE_VALUE, huggingFaceSourceFilesetName, } from '@studio/routes/DeploymentsListRoute/huggingFaceDeploymentArtifacts'; +import { websiteLogger } from '@studio/util/logger'; import { type QueryClient, useMutation, useQueryClient } from '@tanstack/react-query'; import { useCallback } from 'react'; @@ -214,7 +215,7 @@ export function useDeleteDeploymentAndConfig(workspace: string) { void deleteRelatedResourcesAfterDeploymentReleased(workspace, deployment) .catch((error: unknown) => { - console.error('Failed to delete deployment related resources:', error); + websiteLogger.error(`Failed to delete deployment related resources: ${String(error)}`); toast.error('Failed to delete related deployment resources. Please try again later.'); }) .finally(() => { diff --git a/web/packages/studio/src/routes/FilesetListRoute/ActionMenu/index.tsx b/web/packages/studio/src/routes/FilesetListRoute/ActionMenu/index.tsx index 0106f62f3d..1f930468fe 100644 --- a/web/packages/studio/src/routes/FilesetListRoute/ActionMenu/index.tsx +++ b/web/packages/studio/src/routes/FilesetListRoute/ActionMenu/index.tsx @@ -15,6 +15,7 @@ import { invalidateDatasetCaches } from '@studio/api/datasets/invalidateDatasetC import { DatasetCreateModal } from '@studio/components/DatasetCreateModal'; import { DatasetCreateModalMode } from '@studio/components/DatasetCreateModal/constants'; import { DeleteConfirmationModal } from '@studio/components/DeleteConfirmationModal'; +import { websiteLogger } from '@studio/util/logger'; import { EllipsisVertical } from 'lucide-react'; import { FC, useState } from 'react'; @@ -49,7 +50,7 @@ export const ActionMenu: FC = ({ onDatasetDeleted?.(dataset); return true; } catch (error) { - console.error('Failed to delete dataset:', error); + websiteLogger.error(`Failed to delete dataset: ${String(error)}`); return false; } }; diff --git a/web/packages/studio/src/routes/FilesetListRoute/DatasetBulkDeleteModal/index.tsx b/web/packages/studio/src/routes/FilesetListRoute/DatasetBulkDeleteModal/index.tsx index 285b30adee..0707916bf3 100644 --- a/web/packages/studio/src/routes/FilesetListRoute/DatasetBulkDeleteModal/index.tsx +++ b/web/packages/studio/src/routes/FilesetListRoute/DatasetBulkDeleteModal/index.tsx @@ -6,6 +6,7 @@ import { FilesetOutput } from '@nemo/sdk/generated/platform/schema'; import { Button, Flex, Modal } from '@nvidia/foundations-react-core'; import { useMutateMany } from '@studio/api/common/useMutateMany'; import { invalidateDatasetCaches } from '@studio/api/datasets/invalidateDatasetCaches'; +import { websiteLogger } from '@studio/util/logger'; import { Trash } from 'lucide-react'; import { FC, ReactNode, useState } from 'react'; @@ -47,7 +48,7 @@ export const DatasetBulkDeleteModal: FC = ({ onConfirmSuccess(); setOpen(false); } catch (error) { - console.error('Failed to delete datasets:', error); + websiteLogger.error(`Failed to delete datasets: ${String(error)}`); } }; diff --git a/web/packages/studio/src/routes/SafeSynthesizerNewRoute/index.tsx b/web/packages/studio/src/routes/SafeSynthesizerNewRoute/index.tsx index 8a33b34104..a0bd39f5ca 100644 --- a/web/packages/studio/src/routes/SafeSynthesizerNewRoute/index.tsx +++ b/web/packages/studio/src/routes/SafeSynthesizerNewRoute/index.tsx @@ -18,6 +18,7 @@ import { getSafeSynthesizerFormDefaults, } from '@studio/routes/SafeSynthesizerNewRoute/schema'; import { getSafeSynthesizerJobRoute, getSafeSynthesizerRoute } from '@studio/routes/utils'; +import { websiteLogger } from '@studio/util/logger'; import { FC, useState } from 'react'; import { FormProvider, useForm } from 'react-hook-form'; import { useNavigate } from 'react-router-dom'; @@ -61,7 +62,7 @@ export const SafeSynthesizerNewRoute: FC | null = SAFE_SYNTHESIZER_ENABLED } }, onError: (error) => { - console.error('Failed to create job:', error); + websiteLogger.error(`Failed to create job: ${String(error)}`); setErrorMessage( getErrorMessage(error, 'Failed to create job. Please check your input and try again.') ); @@ -94,7 +95,7 @@ export const SafeSynthesizerNewRoute: FC | null = SAFE_SYNTHESIZER_ENABLED }; const handleSubmitError = (errors: unknown) => { - console.error('Form validation errors:', errors); + websiteLogger.error(`Form validation errors: ${String(errors)}`); setErrorMessage('Please fix the form errors before submitting.'); }; diff --git a/web/packages/studio/src/routes/agents/AgentSuggestionsRoute/utils.ts b/web/packages/studio/src/routes/agents/AgentSuggestionsRoute/utils.ts index 0779f66b8c..180185db90 100644 --- a/web/packages/studio/src/routes/agents/AgentSuggestionsRoute/utils.ts +++ b/web/packages/studio/src/routes/agents/AgentSuggestionsRoute/utils.ts @@ -11,6 +11,7 @@ import type { SnapshotShape, SuggestionApplySpec, } from '@studio/routes/agents/AgentSuggestionsRoute/types'; +import { websiteLogger } from '@studio/util/logger'; /** Fileset name for an agent's eval bundle. */ export const evalFilesetForAgent = (agentName: string): string => `${agentName}-eval`; @@ -78,7 +79,7 @@ export const parseSuggestions = (text: string): OptimizationSuggestion[] => { try { results.push(JSON.parse(line) as OptimizationSuggestion); } catch { - console.warn('parseSuggestions: skipping malformed line', line); + websiteLogger.warn(`parseSuggestions: skipping malformed line: ${line}`); } } return results; diff --git a/web/packages/studio/src/util/files.ts b/web/packages/studio/src/util/files.ts index 2c0167503e..0d8c9f1a3e 100644 --- a/web/packages/studio/src/util/files.ts +++ b/web/packages/studio/src/util/files.ts @@ -3,6 +3,7 @@ import { ContentType } from '@nemo/common/src/components/CodeEditor/constants'; import { FileSystemDirectory, FileSystemNode } from '@studio/components/FilesTable/utils'; +import { websiteLogger } from '@studio/util/logger'; import { getTextWithCount, parseCSV } from '@studio/util/strings'; /** @@ -158,7 +159,7 @@ export const parseFileContent = ({ return rows; } catch { failures.push(row); - console.warn(`Invalid JSON row ignored: ${row}`); + websiteLogger.warn(`Invalid JSON row ignored: ${row}`); return rows; } }, diff --git a/web/packages/studio/src/util/llm.ts b/web/packages/studio/src/util/llm.ts index 5ef33b273c..b8168e510c 100644 --- a/web/packages/studio/src/util/llm.ts +++ b/web/packages/studio/src/util/llm.ts @@ -3,6 +3,7 @@ import type { ExcludedChatCompletionMessageParam } from '@nemo/common/src/types/chat'; import { Row } from '@studio/util/files'; +import { websiteLogger } from '@studio/util/logger'; import Handlebars from 'handlebars'; export const extractUserMessage = (props: { row: Row; template?: string }): string => { @@ -20,7 +21,7 @@ export const extractUserMessage = (props: { row: Row; template?: string }): stri const compiled = Handlebars.compile(template); return compiled(row); } catch (err) { - console.error('Failed to compile template', err); + websiteLogger.error(`Failed to compile template: ${String(err)}`); return ''; } } diff --git a/web/packages/studio/src/util/strings.ts b/web/packages/studio/src/util/strings.ts index edbb489ad3..049b15235d 100644 --- a/web/packages/studio/src/util/strings.ts +++ b/web/packages/studio/src/util/strings.ts @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { Row } from '@studio/util/files'; +import { websiteLogger } from '@studio/util/logger'; import Papa from 'papaparse'; /** @@ -48,7 +49,7 @@ export const parseCSV = (props: { csvString: string; options: Papa.ParseConfig } const { data, errors } = Papa.parse(csvString, options); if (errors.length) { - console.error('CSV Parse Errors:', errors); + websiteLogger.error(`CSV Parse Errors: ${JSON.stringify(errors)}`); return []; } From 41df4fb9f5d0621227d7b65f9429060c222cd06f Mon Sep 17 00:00:00 2001 From: Sean Teramae Date: Fri, 12 Jun 2026 10:33:24 -0700 Subject: [PATCH 2/7] add lint rule Signed-off-by: Sean Teramae --- web/eslint.config.js | 1 + web/packages/studio/src/mocks/node.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/web/eslint.config.js b/web/eslint.config.js index 41b2c2cec5..538c4977c4 100644 --- a/web/eslint.config.js +++ b/web/eslint.config.js @@ -157,6 +157,7 @@ export default [ ...hooksPlugin.configs.recommended.rules, ...baseRules, 'import/no-default-export': 'error', + 'no-console': 'error', 'no-restricted-imports': [ 'warn', { diff --git a/web/packages/studio/src/mocks/node.ts b/web/packages/studio/src/mocks/node.ts index 06e7572929..bb21395c40 100644 --- a/web/packages/studio/src/mocks/node.ts +++ b/web/packages/studio/src/mocks/node.ts @@ -1,6 +1,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 +/* eslint-disable no-console */ import { handlers } from '@studio/mocks/handlers'; import { handlers as iconsHandlers } from '@studio/mocks/icons'; import { setupServer } from 'msw/node'; From cbaf7edf184d1a15198e647bbf34e1859934c9ca Mon Sep 17 00:00:00 2001 From: Sean Teramae Date: Fri, 12 Jun 2026 10:40:03 -0700 Subject: [PATCH 3/7] fix script outlier Signed-off-by: Sean Teramae --- web/packages/studio/scripts/fetch-styles.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/web/packages/studio/scripts/fetch-styles.ts b/web/packages/studio/scripts/fetch-styles.ts index ee2c067929..62847b92e9 100644 --- a/web/packages/studio/scripts/fetch-styles.ts +++ b/web/packages/studio/scripts/fetch-styles.ts @@ -2,6 +2,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 +/* eslint-disable no-console -- CLI script */ /** * Fetches base-external.css + components.css from the Kaizen UI Foundations CDN * for the version of @nvidia/foundations-react-core pinned in pnpm-workspace.yaml, From 05e0c46c1f6d468563faba56e177c1857f1e773c Mon Sep 17 00:00:00 2001 From: Sean Teramae Date: Fri, 12 Jun 2026 14:28:08 -0700 Subject: [PATCH 4/7] include original error in logging Signed-off-by: Sean Teramae --- .../e2e-tests/pages/project-datasets.ts | 1 + .../src/api/datasets/useDatasetFileContent.ts | 2 +- .../components/FilesetActionMenu/index.tsx | 2 +- .../components/ModelComparePrompts/index.tsx | 2 +- .../Configurations/form/InputFile.tsx | 2 +- .../components/evaluation/Jobs/ActionMenu.tsx | 2 +- .../evaluation/Jobs/DetailsPanel.tsx | 2 +- .../Jobs/EvaluationJobBulkDeleteModal.tsx | 2 +- .../BulkDeleteModal/index.tsx | 2 +- .../DatasetFileDropzone/index.tsx | 2 +- .../filesets/hooks/useBulkDownload.ts | 2 +- .../filesets/hooks/useBulkDuplicate.ts | 2 +- .../AgentPanel/DeploymentLogsView.tsx | 3 +- .../useDeleteDeploymentAndConfig.ts | 2 +- .../FilesetListRoute/ActionMenu/index.tsx | 2 +- .../DatasetBulkDeleteModal/index.tsx | 2 +- .../routes/SafeSynthesizerNewRoute/index.tsx | 2 +- .../agents/AgentSuggestionsRoute/api.ts | 5 +- .../useOptimizerSuggestions.ts | 5 +- .../agents/ClaudeCodeChatRoute/stream.ts | 4 +- web/packages/studio/src/util/llm.ts | 2 +- web/packages/studio/src/util/logger.ts | 50 ++++++++++++------- .../studio/src/workers/LargeFileWorker.ts | 3 +- 23 files changed, 58 insertions(+), 45 deletions(-) diff --git a/web/packages/studio/e2e-tests/pages/project-datasets.ts b/web/packages/studio/e2e-tests/pages/project-datasets.ts index 5a7d779b77..00ed4af7f0 100644 --- a/web/packages/studio/e2e-tests/pages/project-datasets.ts +++ b/web/packages/studio/e2e-tests/pages/project-datasets.ts @@ -36,6 +36,7 @@ export class ProjectDatasetsPage { ).toBeVisible(); } } catch (e) { + // eslint-disable-next-line no-console console.error(e); } } diff --git a/web/packages/studio/src/api/datasets/useDatasetFileContent.ts b/web/packages/studio/src/api/datasets/useDatasetFileContent.ts index de7ecd515d..bbf858b9b5 100644 --- a/web/packages/studio/src/api/datasets/useDatasetFileContent.ts +++ b/web/packages/studio/src/api/datasets/useDatasetFileContent.ts @@ -88,7 +88,7 @@ export const datasetFileContentQueryOptions = ({ }); return data; } catch (err) { - websiteLogger.error(String(err)); + websiteLogger.error('Invalid response while downloading parquet file', err); throw new Error('Invalid response while downloading parquet file'); } } else { diff --git a/web/packages/studio/src/components/FilesetActionMenu/index.tsx b/web/packages/studio/src/components/FilesetActionMenu/index.tsx index c2db472139..e11248bee9 100644 --- a/web/packages/studio/src/components/FilesetActionMenu/index.tsx +++ b/web/packages/studio/src/components/FilesetActionMenu/index.tsx @@ -50,7 +50,7 @@ export const FilesetActionMenu: FC = ({ onFilesetDeleted?.(fileset); return true; } catch (error) { - websiteLogger.error(`Failed to delete fileset: ${String(error)}`); + websiteLogger.error('Failed to delete fileset', error); return false; } }; diff --git a/web/packages/studio/src/components/ModelComparePrompts/index.tsx b/web/packages/studio/src/components/ModelComparePrompts/index.tsx index ed9a9875a3..4eb85ae16a 100644 --- a/web/packages/studio/src/components/ModelComparePrompts/index.tsx +++ b/web/packages/studio/src/components/ModelComparePrompts/index.tsx @@ -179,7 +179,7 @@ export const ModelComparePrompts: FC = ({ writeCell(row.sourceIndex, model.id, content); }) .catch((error) => { - websiteLogger.error(`Inference request failed: ${String(error)}`); + websiteLogger.error('Inference request failed', error); writeCell(row.sourceIndex, model.id, null); }) ); diff --git a/web/packages/studio/src/components/evaluation/Configurations/form/InputFile.tsx b/web/packages/studio/src/components/evaluation/Configurations/form/InputFile.tsx index 0634be32ca..f33bc7512b 100644 --- a/web/packages/studio/src/components/evaluation/Configurations/form/InputFile.tsx +++ b/web/packages/studio/src/components/evaluation/Configurations/form/InputFile.tsx @@ -407,7 +407,7 @@ export const InputFile: FC = ({ updateFormFromFile(validationResult, undefined); } } catch (error) { - websiteLogger.error(`Failed to validate selected file: ${String(error)}`); + websiteLogger.error('Failed to validate selected file', error); // Create error validation result const errorResult: FileValidationResult = { isValid: false, diff --git a/web/packages/studio/src/components/evaluation/Jobs/ActionMenu.tsx b/web/packages/studio/src/components/evaluation/Jobs/ActionMenu.tsx index 485f2db580..87c61eb4d2 100644 --- a/web/packages/studio/src/components/evaluation/Jobs/ActionMenu.tsx +++ b/web/packages/studio/src/components/evaluation/Jobs/ActionMenu.tsx @@ -46,7 +46,7 @@ export const ActionMenu: FC = ({ job, onNavigateToDetails, onJo onJobDeleted?.(job); handleModalClose(); } catch (error) { - websiteLogger.error(`Failed to delete evaluation job: ${String(error)}`); + websiteLogger.error('Failed to delete evaluation job', error); toast.error('Failed to delete evaluation job. Please try again.'); } finally { setIsDeleting(false); diff --git a/web/packages/studio/src/components/evaluation/Jobs/DetailsPanel.tsx b/web/packages/studio/src/components/evaluation/Jobs/DetailsPanel.tsx index 12d2f1446e..c018d96d29 100644 --- a/web/packages/studio/src/components/evaluation/Jobs/DetailsPanel.tsx +++ b/web/packages/studio/src/components/evaluation/Jobs/DetailsPanel.tsx @@ -60,7 +60,7 @@ export const DetailsPanel = ({ evaluationJob, error }: DetailsPanelProps) => { setCancelModalOpen(false); } catch (error) { - websiteLogger.error(`Failed to cancel job: ${String(error)}`); + websiteLogger.error('Failed to cancel job', error); toast.error('Failed to cancel job. Please try again.'); } }; diff --git a/web/packages/studio/src/components/evaluation/Jobs/EvaluationJobBulkDeleteModal.tsx b/web/packages/studio/src/components/evaluation/Jobs/EvaluationJobBulkDeleteModal.tsx index 4cefcf0468..be39275f2b 100644 --- a/web/packages/studio/src/components/evaluation/Jobs/EvaluationJobBulkDeleteModal.tsx +++ b/web/packages/studio/src/components/evaluation/Jobs/EvaluationJobBulkDeleteModal.tsx @@ -39,7 +39,7 @@ export const EvaluationJobBulkDeleteModal: FC onConfirmSuccess(); setOpen(false); } catch (error) { - websiteLogger.error(`Failed to delete evaluation jobs: ${String(error)}`); + websiteLogger.error('Failed to delete evaluation jobs', error); toast.error('Failed to delete evaluation jobs. Please try again.'); } }; diff --git a/web/packages/studio/src/components/filesets/FilesetFileExplorer/BulkDeleteModal/index.tsx b/web/packages/studio/src/components/filesets/FilesetFileExplorer/BulkDeleteModal/index.tsx index be2e62db54..2d188d11a3 100644 --- a/web/packages/studio/src/components/filesets/FilesetFileExplorer/BulkDeleteModal/index.tsx +++ b/web/packages/studio/src/components/filesets/FilesetFileExplorer/BulkDeleteModal/index.tsx @@ -57,7 +57,7 @@ export const BulkDeleteModal: FC = ({ setOpen(false); } catch (error) { // Error handling is managed by the mutation hooks - websiteLogger.error(`Failed to delete items: ${String(error)}`); + websiteLogger.error('Failed to delete items', error); } }; diff --git a/web/packages/studio/src/components/filesets/FilesetFileExplorer/DatasetFileDropzone/index.tsx b/web/packages/studio/src/components/filesets/FilesetFileExplorer/DatasetFileDropzone/index.tsx index f69995d0b1..5567b9c20c 100644 --- a/web/packages/studio/src/components/filesets/FilesetFileExplorer/DatasetFileDropzone/index.tsx +++ b/web/packages/studio/src/components/filesets/FilesetFileExplorer/DatasetFileDropzone/index.tsx @@ -28,7 +28,7 @@ export const DatasetFileDropzone: FC = ({ onUpload(acceptedFiles); } } catch (error) { - websiteLogger.error(`Error handling accepted files: ${String(error)}`); + websiteLogger.error('Error handling accepted files', error); } }, [onUpload] diff --git a/web/packages/studio/src/components/filesets/hooks/useBulkDownload.ts b/web/packages/studio/src/components/filesets/hooks/useBulkDownload.ts index 20fb8ef872..d18a0e4e4b 100644 --- a/web/packages/studio/src/components/filesets/hooks/useBulkDownload.ts +++ b/web/packages/studio/src/components/filesets/hooks/useBulkDownload.ts @@ -71,7 +71,7 @@ export function useBulkDownload(options: UseBulkDownloadOptions): UseBulkDownloa ); } } catch (err) { - websiteLogger.error(`Bulk download failed: ${String(err)}`); + websiteLogger.error('Bulk download failed', err); toast.dismissToast(toastId); toast.error(files.length === 1 ? 'Failed to download file' : 'Failed to download files'); } finally { diff --git a/web/packages/studio/src/components/filesets/hooks/useBulkDuplicate.ts b/web/packages/studio/src/components/filesets/hooks/useBulkDuplicate.ts index 4143b81cdc..baefe05fbc 100644 --- a/web/packages/studio/src/components/filesets/hooks/useBulkDuplicate.ts +++ b/web/packages/studio/src/components/filesets/hooks/useBulkDuplicate.ts @@ -151,7 +151,7 @@ export function useBulkDuplicate(options: UseBulkDuplicateOptions): UseBulkDupli return false; } } catch (err) { - websiteLogger.error(`Bulk duplicate failed: ${String(err)}`); + websiteLogger.error('Bulk duplicate failed', err); toast.dismissToast(toastId); toast.error(files.length === 1 ? 'Failed to duplicate file' : 'Failed to duplicate files'); return false; diff --git a/web/packages/studio/src/components/sidePanels/AgentPanels/AgentPanel/DeploymentLogsView.tsx b/web/packages/studio/src/components/sidePanels/AgentPanels/AgentPanel/DeploymentLogsView.tsx index 89b10cd73e..09a49e935a 100644 --- a/web/packages/studio/src/components/sidePanels/AgentPanels/AgentPanel/DeploymentLogsView.tsx +++ b/web/packages/studio/src/components/sidePanels/AgentPanels/AgentPanel/DeploymentLogsView.tsx @@ -137,7 +137,8 @@ const LogsForDeployment: FC = ({ workspace, deploymentNa }, onError: (err) => { websiteLogger.warn( - `Log stream interrupted for deployment ${deploymentName}; retrying. ${String(err)}` + `Log stream interrupted for deployment ${deploymentName}; retrying`, + err ); }, }); diff --git a/web/packages/studio/src/routes/DeploymentsListRoute/useDeleteDeploymentAndConfig.ts b/web/packages/studio/src/routes/DeploymentsListRoute/useDeleteDeploymentAndConfig.ts index 4e26fcce80..1dc0b574ff 100644 --- a/web/packages/studio/src/routes/DeploymentsListRoute/useDeleteDeploymentAndConfig.ts +++ b/web/packages/studio/src/routes/DeploymentsListRoute/useDeleteDeploymentAndConfig.ts @@ -215,7 +215,7 @@ export function useDeleteDeploymentAndConfig(workspace: string) { void deleteRelatedResourcesAfterDeploymentReleased(workspace, deployment) .catch((error: unknown) => { - websiteLogger.error(`Failed to delete deployment related resources: ${String(error)}`); + websiteLogger.error('Failed to delete deployment related resources', error); toast.error('Failed to delete related deployment resources. Please try again later.'); }) .finally(() => { diff --git a/web/packages/studio/src/routes/FilesetListRoute/ActionMenu/index.tsx b/web/packages/studio/src/routes/FilesetListRoute/ActionMenu/index.tsx index 1f930468fe..93fc9f752f 100644 --- a/web/packages/studio/src/routes/FilesetListRoute/ActionMenu/index.tsx +++ b/web/packages/studio/src/routes/FilesetListRoute/ActionMenu/index.tsx @@ -50,7 +50,7 @@ export const ActionMenu: FC = ({ onDatasetDeleted?.(dataset); return true; } catch (error) { - websiteLogger.error(`Failed to delete dataset: ${String(error)}`); + websiteLogger.error('Failed to delete dataset', error); return false; } }; diff --git a/web/packages/studio/src/routes/FilesetListRoute/DatasetBulkDeleteModal/index.tsx b/web/packages/studio/src/routes/FilesetListRoute/DatasetBulkDeleteModal/index.tsx index 0707916bf3..511f90e2af 100644 --- a/web/packages/studio/src/routes/FilesetListRoute/DatasetBulkDeleteModal/index.tsx +++ b/web/packages/studio/src/routes/FilesetListRoute/DatasetBulkDeleteModal/index.tsx @@ -48,7 +48,7 @@ export const DatasetBulkDeleteModal: FC = ({ onConfirmSuccess(); setOpen(false); } catch (error) { - websiteLogger.error(`Failed to delete datasets: ${String(error)}`); + websiteLogger.error('Failed to delete datasets', error); } }; diff --git a/web/packages/studio/src/routes/SafeSynthesizerNewRoute/index.tsx b/web/packages/studio/src/routes/SafeSynthesizerNewRoute/index.tsx index a0bd39f5ca..d5006c7f7c 100644 --- a/web/packages/studio/src/routes/SafeSynthesizerNewRoute/index.tsx +++ b/web/packages/studio/src/routes/SafeSynthesizerNewRoute/index.tsx @@ -62,7 +62,7 @@ export const SafeSynthesizerNewRoute: FC | null = SAFE_SYNTHESIZER_ENABLED } }, onError: (error) => { - websiteLogger.error(`Failed to create job: ${String(error)}`); + websiteLogger.error('Failed to create job', error); setErrorMessage( getErrorMessage(error, 'Failed to create job. Please check your input and try again.') ); diff --git a/web/packages/studio/src/routes/agents/AgentSuggestionsRoute/api.ts b/web/packages/studio/src/routes/agents/AgentSuggestionsRoute/api.ts index 22aac686fe..451ac24c65 100644 --- a/web/packages/studio/src/routes/agents/AgentSuggestionsRoute/api.ts +++ b/web/packages/studio/src/routes/agents/AgentSuggestionsRoute/api.ts @@ -35,6 +35,7 @@ import { serializeSuggestions, suggestionIdentity, } from '@studio/routes/agents/AgentSuggestionsRoute/utils'; +import { toError } from '@studio/util/logger'; export const TELEMETRY_FILESET = 'nemo-agent-telemetry'; export const OPTIMIZER_FILESET = 'nemo-agent-optimizer'; @@ -450,9 +451,7 @@ export const applySuggestion = async ( try { safePath = validateApplySpec(step, ctx); } catch (err) { - throw new Error( - `Step ${i + 1}/${steps.length}: ${err instanceof Error ? err.message : String(err)}` - ); + throw new Error(`Step ${i + 1}/${steps.length}: ${toError(err).message}`); } const response = await customFetch<{ name?: string } | undefined>({ url: safePath, diff --git a/web/packages/studio/src/routes/agents/AgentSuggestionsRoute/useOptimizerSuggestions.ts b/web/packages/studio/src/routes/agents/AgentSuggestionsRoute/useOptimizerSuggestions.ts index 2a979333d3..f9caff84bd 100644 --- a/web/packages/studio/src/routes/agents/AgentSuggestionsRoute/useOptimizerSuggestions.ts +++ b/web/packages/studio/src/routes/agents/AgentSuggestionsRoute/useOptimizerSuggestions.ts @@ -37,6 +37,7 @@ import { suggestionIdentity, } from '@studio/routes/agents/AgentSuggestionsRoute/utils'; import { getAgentEvaluationDetailRoute } from '@studio/routes/utils'; +import { toError } from '@studio/util/logger'; import { useQuery, useQueryClient } from '@tanstack/react-query'; import { useCallback, useEffect, useRef, useState } from 'react'; @@ -243,7 +244,7 @@ export const useOptimizerSuggestions = (workspace: string) => { setRunState({ phase: 'failed', step: '', - error: err instanceof Error ? err : new Error(String(err)), + error: toError(err), }); } finally { if (abortRef.current === controller) abortRef.current = null; @@ -387,7 +388,7 @@ export const useOptimizerSuggestions = (workspace: string) => { } } catch (err) { if (isCanceledError(err)) return; - const message = err instanceof Error ? err.message : String(err); + const message = toError(err).message; setApplyErrors((prev) => new Map(prev).set(key, message)); } finally { applyControllersRef.current.delete(controller); diff --git a/web/packages/studio/src/routes/agents/ClaudeCodeChatRoute/stream.ts b/web/packages/studio/src/routes/agents/ClaudeCodeChatRoute/stream.ts index 19a269b45f..6018127a11 100644 --- a/web/packages/studio/src/routes/agents/ClaudeCodeChatRoute/stream.ts +++ b/web/packages/studio/src/routes/agents/ClaudeCodeChatRoute/stream.ts @@ -121,9 +121,7 @@ export const parseJsonObject = (value: string): unknown => { try { return JSON.parse(value) as unknown; } catch (error) { - websiteLogger.error( - `Failed to parse Claude Code stream JSON: ${error instanceof Error ? error.message : String(error)}` - ); + websiteLogger.error('Failed to parse Claude Code stream JSON', error); return undefined; } }; diff --git a/web/packages/studio/src/util/llm.ts b/web/packages/studio/src/util/llm.ts index b8168e510c..b6ce9b69c5 100644 --- a/web/packages/studio/src/util/llm.ts +++ b/web/packages/studio/src/util/llm.ts @@ -21,7 +21,7 @@ export const extractUserMessage = (props: { row: Row; template?: string }): stri const compiled = Handlebars.compile(template); return compiled(row); } catch (err) { - websiteLogger.error(`Failed to compile template: ${String(err)}`); + websiteLogger.error('Failed to compile template', err); return ''; } } diff --git a/web/packages/studio/src/util/logger.ts b/web/packages/studio/src/util/logger.ts index b39d68dcd2..c65ed9d36e 100644 --- a/web/packages/studio/src/util/logger.ts +++ b/web/packages/studio/src/util/logger.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 /* eslint-disable no-console */ -import { AnyValue, Logger, logs, SeverityNumber } from '@opentelemetry/api-logs'; +import { Logger, logs, SeverityNumber } from '@opentelemetry/api-logs'; import { OTEL_SERVICE_NAME, VERSION_SHA } from '@studio/constants/environment'; const otelLogger: Logger = logs.getLogger(OTEL_SERVICE_NAME); @@ -23,48 +23,60 @@ const otelLogger: Logger = logs.getLogger(OTEL_SERVICE_NAME); * ``` */ class WebsiteLogger { - private log(level: SeverityNumber, message: AnyValue) { - if (level === SeverityNumber.ERROR) { - console.error(message); - } else if (level === SeverityNumber.WARN) { - console.warn(message); - } else if (level === SeverityNumber.INFO) { - console.info(message); - } else if (level === SeverityNumber.DEBUG) { - console.debug(message); + private log(level: SeverityNumber, message: string, cause?: unknown) { + if (level === SeverityNumber.ERROR) console.error(message, cause); + else if (level === SeverityNumber.WARN) console.warn(message, cause); + else if (level === SeverityNumber.INFO) console.info(message, cause); + else if (level === SeverityNumber.DEBUG) console.debug(message, cause); + + const attributes: Record = {}; + if (cause instanceof Error) { + if (cause.message) attributes['error.message'] = cause.message; + if (cause.stack) attributes['error.stack'] = cause.stack; + } else if (cause !== undefined) { + attributes['error'] = String(cause); } + otelLogger.emit({ severityNumber: level, body: message, + ...(Object.keys(attributes).length > 0 && { attributes }), }); } - debug(message: AnyValue) { - this.log(SeverityNumber.DEBUG, message); + debug(message: string, cause?: unknown) { + this.log(SeverityNumber.DEBUG, message, cause); } - error(message: AnyValue) { - this.log(SeverityNumber.ERROR, message); + error(message: string, cause?: unknown) { + this.log(SeverityNumber.ERROR, message, cause); } - info(message: AnyValue) { - this.log(SeverityNumber.INFO, message); + info(message: string, cause?: unknown) { + this.log(SeverityNumber.INFO, message, cause); } - warn(message: AnyValue) { - this.log(SeverityNumber.WARN, message); + warn(message: string, cause?: unknown) { + this.log(SeverityNumber.WARN, message, cause); } } // Global instance of our logger that should be used by individual modules, in-place of `console.log`. export const websiteLogger = new WebsiteLogger(); +/** + * Converts a unknown error to an Error object. + */ +export function toError(err: unknown): Error { + return err instanceof Error ? err : new Error(String(err)); +} + /** * Handles a generic error by logging it to the website logger. */ export const handleGenericError = (error: Error | string) => { if (error instanceof Error) { - websiteLogger.error(JSON.stringify(error)); + websiteLogger.error(error.message, error); } else { websiteLogger.error(error); } diff --git a/web/packages/studio/src/workers/LargeFileWorker.ts b/web/packages/studio/src/workers/LargeFileWorker.ts index a34d3f68a9..9857c566a0 100644 --- a/web/packages/studio/src/workers/LargeFileWorker.ts +++ b/web/packages/studio/src/workers/LargeFileWorker.ts @@ -3,6 +3,7 @@ import { DEFAULT_WORKSPACE } from '@nemo/common/src/models/constants'; import { filesDownloadFile } from '@nemo/sdk/generated/platform/api'; +import { toError } from '@studio/util/logger'; import axios from 'axios'; export interface LargeFileWorkerMessage { @@ -45,6 +46,6 @@ self.onmessage = async function (e: MessageEvent) { const arrayBuffer = await response.arrayBuffer(); self.postMessage({ done: true, arrayBuffer }, { transfer: [arrayBuffer] }); } catch (error) { - self.postMessage({ done: true, error: String(error) }); + self.postMessage({ done: true, error: toError(error).message }); } }; From 57ecb1a3becf5ab87e910525a1c1009afca3bfde Mon Sep 17 00:00:00 2001 From: Sean Teramae Date: Fri, 12 Jun 2026 15:30:53 -0700 Subject: [PATCH 5/7] fix tests Signed-off-by: Sean Teramae --- .../evaluation/Jobs/ActionMenu.spec.tsx | 5 +--- .../EvaluationJobBulkDeleteModal.spec.tsx | 2 +- .../ActionMenu/index.spec.tsx | 5 +--- .../DatasetBulkDeleteModal/index.spec.tsx | 2 +- .../SafeSynthesizerNewRoute/index.test.tsx | 26 +++++++++++-------- .../agents/ClaudeCodeChatRoute/stream.spec.ts | 3 ++- web/packages/studio/src/util/logger.spec.ts | 4 +-- web/packages/studio/src/util/logger.ts | 9 ++++--- 8 files changed, 28 insertions(+), 28 deletions(-) diff --git a/web/packages/studio/src/components/evaluation/Jobs/ActionMenu.spec.tsx b/web/packages/studio/src/components/evaluation/Jobs/ActionMenu.spec.tsx index dcb553d720..eef3278313 100644 --- a/web/packages/studio/src/components/evaluation/Jobs/ActionMenu.spec.tsx +++ b/web/packages/studio/src/components/evaluation/Jobs/ActionMenu.spec.tsx @@ -216,10 +216,7 @@ describe('ActionMenu', () => { const confirmButton = await screen.findByRole('button', { name: 'Delete' }); await user.click(confirmButton); - expect(consoleSpy).toHaveBeenCalledWith( - 'Failed to delete evaluation job:', - expect.any(Error) - ); + expect(consoleSpy).toHaveBeenCalledWith('Failed to delete evaluation job', expect.any(Error)); expect(mockOnJobDeleted).not.toHaveBeenCalled(); consoleSpy.mockRestore(); diff --git a/web/packages/studio/src/components/evaluation/Jobs/EvaluationJobBulkDeleteModal.spec.tsx b/web/packages/studio/src/components/evaluation/Jobs/EvaluationJobBulkDeleteModal.spec.tsx index b6069ba6e1..891c178b7a 100644 --- a/web/packages/studio/src/components/evaluation/Jobs/EvaluationJobBulkDeleteModal.spec.tsx +++ b/web/packages/studio/src/components/evaluation/Jobs/EvaluationJobBulkDeleteModal.spec.tsx @@ -198,7 +198,7 @@ describe('EvaluationJobBulkDeleteModal', () => { await waitFor(() => { expect(consoleSpy).toHaveBeenCalledWith( - 'Failed to delete evaluation jobs:', + 'Failed to delete evaluation jobs', expect.any(Error) ); }); diff --git a/web/packages/studio/src/routes/FilesetListRoute/ActionMenu/index.spec.tsx b/web/packages/studio/src/routes/FilesetListRoute/ActionMenu/index.spec.tsx index 36a071aa20..ffeddcb698 100644 --- a/web/packages/studio/src/routes/FilesetListRoute/ActionMenu/index.spec.tsx +++ b/web/packages/studio/src/routes/FilesetListRoute/ActionMenu/index.spec.tsx @@ -314,10 +314,7 @@ describe('ActionMenu', () => { await user.click(confirmDeleteButton); await waitFor(() => { - expect(consoleErrorSpy).toHaveBeenCalledWith( - 'Failed to delete dataset:', - expect.any(Error) - ); + expect(consoleErrorSpy).toHaveBeenCalledWith('Failed to delete dataset', expect.any(Error)); }); expect(mockOnDatasetDeleted).not.toHaveBeenCalled(); diff --git a/web/packages/studio/src/routes/FilesetListRoute/DatasetBulkDeleteModal/index.spec.tsx b/web/packages/studio/src/routes/FilesetListRoute/DatasetBulkDeleteModal/index.spec.tsx index 20cc4e2501..26b589cf1e 100644 --- a/web/packages/studio/src/routes/FilesetListRoute/DatasetBulkDeleteModal/index.spec.tsx +++ b/web/packages/studio/src/routes/FilesetListRoute/DatasetBulkDeleteModal/index.spec.tsx @@ -265,7 +265,7 @@ describe('DatasetBulkDeleteModal', () => { // Wait for error handling await waitFor(() => { expect(consoleErrorSpy).toHaveBeenCalledWith( - 'Failed to delete datasets:', + 'Failed to delete datasets', expect.any(Error) ); }); diff --git a/web/packages/studio/src/routes/SafeSynthesizerNewRoute/index.test.tsx b/web/packages/studio/src/routes/SafeSynthesizerNewRoute/index.test.tsx index 6e05e7bb03..14b671c203 100644 --- a/web/packages/studio/src/routes/SafeSynthesizerNewRoute/index.test.tsx +++ b/web/packages/studio/src/routes/SafeSynthesizerNewRoute/index.test.tsx @@ -125,6 +125,10 @@ describe('SafeSynthesizerNewRoute', () => { it('should be defined and not null when SAFE_SYNTHESIZER_ENABLED is true', async () => { vi.doMock('@studio/constants/environment', () => ({ SAFE_SYNTHESIZER_ENABLED: true, + OTEL_SERVICE_NAME: 'test-service', + })); + vi.doMock('@studio/util/logger', () => ({ + websiteLogger: { debug: vi.fn(), error: vi.fn(), info: vi.fn(), warn: vi.fn() }, })); const module = await import('./index'); @@ -135,6 +139,10 @@ describe('SafeSynthesizerNewRoute', () => { it('should return null when SAFE_SYNTHESIZER_ENABLED is false', async () => { vi.doMock('@studio/constants/environment', () => ({ SAFE_SYNTHESIZER_ENABLED: false, + OTEL_SERVICE_NAME: 'test-service', + })); + vi.doMock('@studio/util/logger', () => ({ + websiteLogger: { debug: vi.fn(), error: vi.fn(), info: vi.fn(), warn: vi.fn() }, })); const module = await import('./index'); @@ -787,13 +795,16 @@ describe('SafeSynthesizerNewRoute', () => { it('should have form validation error handling configured', async () => { vi.doMock('@studio/constants/environment', () => ({ SAFE_SYNTHESIZER_ENABLED: true, + OTEL_SERVICE_NAME: 'test-service', })); vi.resetModules(); const { SafeSynthesizerNewRoute } = await import('./index'); if (!SafeSynthesizerNewRoute) return; - const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); + // Spy on console.error to suppress vitest-fail-on-console and assert the call. + // The real logger delegates to console.error, so we verify via the console spy. + const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); const user = userEvent.setup(); render( @@ -813,17 +824,10 @@ describe('SafeSynthesizerNewRoute', () => { ).toBeInTheDocument(); }); - // Verify console.error was called with validation errors - expect(consoleErrorSpy).toHaveBeenCalledWith( - 'Form validation errors:', - expect.objectContaining({ - spec: expect.objectContaining({ - data_source: expect.any(Object), - }), - }) - ); + // Verify logger.error was called with validation errors (real logger calls console.error) + expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Form validation errors:')); - consoleErrorSpy.mockRestore(); + consoleSpy.mockRestore(); }); }); diff --git a/web/packages/studio/src/routes/agents/ClaudeCodeChatRoute/stream.spec.ts b/web/packages/studio/src/routes/agents/ClaudeCodeChatRoute/stream.spec.ts index 9e5f51fa88..44af8e41a0 100644 --- a/web/packages/studio/src/routes/agents/ClaudeCodeChatRoute/stream.spec.ts +++ b/web/packages/studio/src/routes/agents/ClaudeCodeChatRoute/stream.spec.ts @@ -204,7 +204,8 @@ describe('Claude Code stream utilities', () => { expect(parseJsonObject('{')).toBeUndefined(); expect(loggerSpy).toHaveBeenCalledWith( - expect.stringContaining('Failed to parse Claude Code stream JSON') + expect.stringContaining('Failed to parse Claude Code stream JSON'), + expect.any(Error) ); loggerSpy.mockRestore(); diff --git a/web/packages/studio/src/util/logger.spec.ts b/web/packages/studio/src/util/logger.spec.ts index 2406fd8a82..8aa5706d9c 100644 --- a/web/packages/studio/src/util/logger.spec.ts +++ b/web/packages/studio/src/util/logger.spec.ts @@ -90,11 +90,11 @@ describe('handleGenericError', () => { mockEmit.mockClear(); }); - it('should stringify Error objects before logging', () => { + it('should log error message with original error as cause', () => { const spy = vi.spyOn(console, 'error').mockImplementation(() => {}); const error = new Error('test error'); handleGenericError(error); - expect(spy).toHaveBeenCalledWith(JSON.stringify(error)); + expect(spy).toHaveBeenCalledWith('test error', error); spy.mockRestore(); }); diff --git a/web/packages/studio/src/util/logger.ts b/web/packages/studio/src/util/logger.ts index c65ed9d36e..7f5e61bc80 100644 --- a/web/packages/studio/src/util/logger.ts +++ b/web/packages/studio/src/util/logger.ts @@ -24,10 +24,11 @@ const otelLogger: Logger = logs.getLogger(OTEL_SERVICE_NAME); */ class WebsiteLogger { private log(level: SeverityNumber, message: string, cause?: unknown) { - if (level === SeverityNumber.ERROR) console.error(message, cause); - else if (level === SeverityNumber.WARN) console.warn(message, cause); - else if (level === SeverityNumber.INFO) console.info(message, cause); - else if (level === SeverityNumber.DEBUG) console.debug(message, cause); + const logArgs: [string, ...unknown[]] = cause !== undefined ? [message, cause] : [message]; + if (level === SeverityNumber.ERROR) console.error(...logArgs); + else if (level === SeverityNumber.WARN) console.warn(...logArgs); + else if (level === SeverityNumber.INFO) console.info(...logArgs); + else if (level === SeverityNumber.DEBUG) console.debug(...logArgs); const attributes: Record = {}; if (cause instanceof Error) { From f899ff245ea236974e019636c8007c02d17569c9 Mon Sep 17 00:00:00 2001 From: Sean Teramae Date: Fri, 12 Jun 2026 15:36:02 -0700 Subject: [PATCH 6/7] fix logger Signed-off-by: Sean Teramae --- .../studio/src/routes/SafeSynthesizerNewRoute/index.test.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/web/packages/studio/src/routes/SafeSynthesizerNewRoute/index.test.tsx b/web/packages/studio/src/routes/SafeSynthesizerNewRoute/index.test.tsx index 14b671c203..aa84405756 100644 --- a/web/packages/studio/src/routes/SafeSynthesizerNewRoute/index.test.tsx +++ b/web/packages/studio/src/routes/SafeSynthesizerNewRoute/index.test.tsx @@ -793,6 +793,9 @@ describe('SafeSynthesizerNewRoute', () => { describe('Form validation errors', () => { it('should have form validation error handling configured', async () => { + // Remove any lingering vi.doMock for the logger (left by Feature flag tests) so + // the component gets the real logger and we can assert via console.error. + vi.doUnmock('@studio/util/logger'); vi.doMock('@studio/constants/environment', () => ({ SAFE_SYNTHESIZER_ENABLED: true, OTEL_SERVICE_NAME: 'test-service', From ae615e29f08ea45a4b210409446d9b0b24e89bbd Mon Sep 17 00:00:00 2001 From: Sean Teramae Date: Fri, 12 Jun 2026 15:46:49 -0700 Subject: [PATCH 7/7] fix last test Signed-off-by: Sean Teramae --- .../studio/src/routes/SafeSynthesizerNewRoute/index.test.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/web/packages/studio/src/routes/SafeSynthesizerNewRoute/index.test.tsx b/web/packages/studio/src/routes/SafeSynthesizerNewRoute/index.test.tsx index aa84405756..b617638a4d 100644 --- a/web/packages/studio/src/routes/SafeSynthesizerNewRoute/index.test.tsx +++ b/web/packages/studio/src/routes/SafeSynthesizerNewRoute/index.test.tsx @@ -837,12 +837,14 @@ describe('SafeSynthesizerNewRoute', () => { describe('Form reset behavior', () => { beforeEach(() => { // Suppress expected console.error from error handling and validation code paths - suppressConsoleError('Form validation errors:', 'Failed to create job:'); + suppressConsoleError('Form validation errors:', 'Failed to create job'); }); it('should clear error message when form is resubmitted', async () => { vi.doMock('@studio/constants/environment', () => ({ SAFE_SYNTHESIZER_ENABLED: true, + OTEL_SERVICE_NAME: 'test-service', + VERSION_SHA: 'test-sha', })); let onErrorCallback: ((error: AxiosError) => void) | undefined;