From fcbbe2e4a3f99bba906325fa995bd56955697eef Mon Sep 17 00:00:00 2001 From: Aleksander <170264518+t-aleksander@users.noreply.github.com> Date: Wed, 18 Mar 2026 14:09:35 +0100 Subject: [PATCH 1/2] use tauri log in more places --- biome.json | 2 +- .../ApplicationUpdateManager.tsx | 4 +- src/components/AutoProvisioningManager.tsx | 4 +- src/main.tsx | 15 ++++ .../AddInstanceDeviceForm.tsx | 6 +- .../AddInstanceInitForm.tsx | 83 ++++++++++++------- .../components/EditTunnelFormCard.tsx | 11 ++- .../DeleteTunnelModal/DeleteTunnelModal.tsx | 6 +- .../LocationCardConnectButton.tsx | 8 +- .../LocationCardRoute/LocationCardRoute.tsx | 7 +- .../modals/MFAModal/MFAModal.tsx | 64 ++++++++++---- .../MfaMobileApprove/MfaMobileApprove.tsx | 4 +- .../DeleteInstanceModal.tsx | 8 +- .../components/UpdateInstanceModalForm.tsx | 56 ++++++++++--- .../components/GlobalLogs/GlobalLogs.tsx | 4 +- .../components/DesktopSetup/DesktopSetup.tsx | 5 +- .../steps/MfaSetupStep/MfaSetupStep.tsx | 1 - .../steps/SendFinishStep/SendFinishStep.tsx | 7 +- .../components/providers/DeepLinkProvider.tsx | 12 +-- src/shared/hooks/useClipboard.ts | 5 +- src/shared/utils/errorDetail.ts | 9 ++ 21 files changed, 233 insertions(+), 88 deletions(-) create mode 100644 src/shared/utils/errorDetail.ts diff --git a/biome.json b/biome.json index fc54fd2c..65913cff 100644 --- a/biome.json +++ b/biome.json @@ -1,5 +1,5 @@ { - "$schema": "https://biomejs.dev/schemas/2.3.13/schema.json", + "$schema": "https://biomejs.dev/schemas/2.4.4/schema.json", "vcs": { "enabled": false, "clientKind": "git", diff --git a/src/components/ApplicationUpdateManager/ApplicationUpdateManager.tsx b/src/components/ApplicationUpdateManager/ApplicationUpdateManager.tsx index 393ac619..f4d9f787 100644 --- a/src/components/ApplicationUpdateManager/ApplicationUpdateManager.tsx +++ b/src/components/ApplicationUpdateManager/ApplicationUpdateManager.tsx @@ -6,6 +6,7 @@ import { clientApi } from '../../pages/client/clientAPI/clientApi.ts'; import { useClientStore } from '../../pages/client/hooks/useClientStore'; import { TauriEventKey } from '../../pages/client/types'; import type { NewApplicationVersionInfo } from '../../shared/hooks/api/types'; +import { errorDetail } from '../../shared/utils/errorDetail'; import { type ApplicationUpdateStore, useApplicationUpdateStore, @@ -83,7 +84,8 @@ export const ApplicationUpdateManager = () => { dismissed: false, }); } catch (e) { - error(`Failed to check latest app version: ${e}`); + const detail = errorDetail(e); + error(`Failed to check latest app version (current: ${appVersion}): ${detail}`); } }; diff --git a/src/components/AutoProvisioningManager.tsx b/src/components/AutoProvisioningManager.tsx index 536418d3..aed98ba5 100644 --- a/src/components/AutoProvisioningManager.tsx +++ b/src/components/AutoProvisioningManager.tsx @@ -6,6 +6,7 @@ import type { ProvisioningConfig } from '../pages/client/clientAPI/types'; import { clientQueryKeys } from '../pages/client/query'; import { useToaster } from '../shared/defguard-ui/hooks/toasts/useToaster'; import useAddInstance from '../shared/hooks/useAddInstance'; +import { errorDetail } from '../shared/utils/errorDetail'; const { getProvisioningConfig } = clientApi; @@ -26,8 +27,9 @@ export default function AutoProvisioningManager({ children }: PropsWithChildren) token: config.enrollment_token, }); } catch (e) { + const detail = errorDetail(e); error( - `Failed to handle automatic client provisioning with ${JSON.stringify(config)}.\n Error: ${JSON.stringify(e)}`, + `Failed to handle automatic client provisioning (url: ${config.enrollment_url}): ${detail}`, ); toaster.error( 'Automatic client provisioning failed, please contact your administrator.', diff --git a/src/main.tsx b/src/main.tsx index 2a8a675c..96d5cc63 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,7 +1,22 @@ +import { error } from '@tauri-apps/plugin-log'; import { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; import { App } from './components/App/App'; +import { errorDetail } from './shared/utils/errorDetail'; + +// Forward uncaught JS errors to the Tauri backend log +window.onerror = (message, source, lineno, colno, err) => { + const detail = err?.stack ?? `${message} (${source}:${lineno}:${colno})`; + error(`[uncaught error] ${detail}`); + // returning false lets the error propagate to the browser DevTools console + return false; +}; + +// Forward unhandled promise rejections to the Tauri backend log +window.addEventListener('unhandledrejection', (event) => { + error(`[unhandled rejection] ${errorDetail(event.reason)}`); +}); const rootElement = document.getElementById('root') as HTMLElement; diff --git a/src/pages/client/pages/ClientAddInstancePage/components/AddInstanceFormCard/components/AddInstanceDeviceForm/AddInstanceDeviceForm.tsx b/src/pages/client/pages/ClientAddInstancePage/components/AddInstanceFormCard/components/AddInstanceDeviceForm/AddInstanceDeviceForm.tsx index f90527fe..e91f4c35 100644 --- a/src/pages/client/pages/ClientAddInstancePage/components/AddInstanceFormCard/components/AddInstanceDeviceForm/AddInstanceDeviceForm.tsx +++ b/src/pages/client/pages/ClientAddInstancePage/components/AddInstanceFormCard/components/AddInstanceDeviceForm/AddInstanceDeviceForm.tsx @@ -23,6 +23,7 @@ import type { CreateDeviceResponse, } from '../../../../../../../../shared/hooks/api/types'; import { routes } from '../../../../../../../../shared/routes'; +import { errorDetail } from '../../../../../../../../shared/utils/errorDetail'; import { generateWGKeys } from '../../../../../../../../shared/utils/generateWGKeys'; import { clientApi } from '../../../../../../clientAPI/clientApi'; import { useClientStore } from '../../../../../../hooks/useClientStore'; @@ -134,6 +135,8 @@ export const AddInstanceDeviceForm = () => { navigate(routes.client.instancePage, { replace: true }); }) .catch((e) => { + const detail = errorDetail(e); + error(`Failed to save device config: ${detail}`); toaster.error( LL.common.messages.errorWithMessage({ message: String(e), @@ -144,7 +147,8 @@ export const AddInstanceDeviceForm = () => { }); } catch (e) { setIsLoading(false); - console.error(e); + const detail = errorDetail(e); + error(`Device form submit failed for proxy ${proxyUrl}: ${detail}`); if (typeof e === 'string') { if (e.includes('Network Error')) { diff --git a/src/pages/client/pages/ClientAddInstancePage/components/AddInstanceFormCard/components/AddInstanceInitForm/AddInstanceInitForm.tsx b/src/pages/client/pages/ClientAddInstancePage/components/AddInstanceFormCard/components/AddInstanceInitForm/AddInstanceInitForm.tsx index 3252bab4..98ebf791 100644 --- a/src/pages/client/pages/ClientAddInstancePage/components/AddInstanceFormCard/components/AddInstanceInitForm/AddInstanceInitForm.tsx +++ b/src/pages/client/pages/ClientAddInstancePage/components/AddInstanceFormCard/components/AddInstanceInitForm/AddInstanceInitForm.tsx @@ -9,7 +9,6 @@ import { useMemo, useState } from 'react'; import { type SubmitHandler, useForm } from 'react-hook-form'; import { useNavigate } from 'react-router-dom'; import { z } from 'zod'; - import { useI18nContext } from '../../../../../../../../i18n/i18n-react'; import { FormInput } from '../../../../../../../../shared/defguard-ui/components/Form/FormInput/FormInput'; import { Button } from '../../../../../../../../shared/defguard-ui/components/Layout/Button/Button'; @@ -24,6 +23,7 @@ import type { EnrollmentStartResponse, } from '../../../../../../../../shared/hooks/api/types'; import { routes } from '../../../../../../../../shared/routes'; +import { errorDetail } from '../../../../../../../../shared/utils/errorDetail'; import { useEnrollmentStore } from '../../../../../../../enrollment/hooks/store/useEnrollmentStore'; import { clientApi } from '../../../../../../clientAPI/clientApi'; import { useClientStore } from '../../../../../../hooks/useClientStore'; @@ -96,7 +96,9 @@ export const AddInstanceInitForm = () => { .then(async (res: Response) => { if (!res.ok) { setIsLoading(false); - error(JSON.stringify(res.status)); + error( + `Enrollment start returned non-OK status ${res.status} for URL: ${endpointUrl}`, + ); const errorMessage = ((await res.json()) as EnrollmentError).error; switch (errorMessage) { @@ -120,9 +122,7 @@ export const AddInstanceInitForm = () => { if (!authCookie) { setIsLoading(false); error( - LL.common.messages.errorWithMessage({ - message: LL.common.messages.noCookie(), - }), + `Enrollment start response for ${endpointUrl} is missing defguard_proxy set-cookie header`, ); throw Error( LL.common.messages.errorWithMessage({ @@ -154,34 +154,57 @@ export const AddInstanceInitForm = () => { body: JSON.stringify({ pubkey: instance.pubkey, }), - }).then(async (res) => { - invoke('update_instance', { - instanceId: instance.id, - response: (await res.json()) as CreateDeviceResponse, - }) - .then(() => { - info('Configured device'); - toaster.success( - LL.pages.enrollment.steps.deviceSetup.desktopSetup.messages.deviceConfigured(), - ); - const _selectedInstance: SelectedInstance = { - id: instance.id, - type: ClientConnectionType.LOCATION, - }; - setClientState({ - selectedInstance: _selectedInstance, - }); - navigate(routes.client.base, { replace: true }); - }) - .catch((e) => { - error(e); + }) + .then(async (res) => { + if (!res.ok) { + const detail = `network_info returned status ${res.status} for instance ${instance.uuid}`; + error(`Failed to fetch network info: ${detail}`); toaster.error( LL.common.messages.errorWithMessage({ - message: String(e), + message: detail, }), ); - }); - }); + return; + } + invoke('update_instance', { + instanceId: instance.id, + response: (await res.json()) as CreateDeviceResponse, + }) + .then(() => { + info('Configured device'); + toaster.success( + LL.pages.enrollment.steps.deviceSetup.desktopSetup.messages.deviceConfigured(), + ); + const _selectedInstance: SelectedInstance = { + id: instance.id, + type: ClientConnectionType.LOCATION, + }; + setClientState({ + selectedInstance: _selectedInstance, + }); + navigate(routes.client.base, { replace: true }); + }) + .catch((e) => { + const detail = errorDetail(e); + error(`Failed to save config during instance add: ${detail}`); + toaster.error( + LL.common.messages.errorWithMessage({ + message: String(e), + }), + ); + }); + }) + .catch((e) => { + const detail = errorDetail(e); + error( + `Failed to reach network_info endpoint for instance ${instance.uuid}: ${detail}`, + ); + toaster.error( + LL.common.messages.errorWithMessage({ + message: String(e), + }), + ); + }); } // register new instance // is user in need of full enrollment ? @@ -220,6 +243,8 @@ export const AddInstanceInitForm = () => { }) .catch((e) => { setIsLoading(false); + const detail = errorDetail(e); + error(`Failed to initialize instance: ${detail}`); if (typeof e === 'string') { if (e.includes('Network Error')) { toaster.error(LL.common.messages.networkError()); diff --git a/src/pages/client/pages/ClientEditTunnelPage/components/EditTunnelFormCard.tsx b/src/pages/client/pages/ClientEditTunnelPage/components/EditTunnelFormCard.tsx index ac04559b..437a6245 100644 --- a/src/pages/client/pages/ClientEditTunnelPage/components/EditTunnelFormCard.tsx +++ b/src/pages/client/pages/ClientEditTunnelPage/components/EditTunnelFormCard.tsx @@ -1,9 +1,9 @@ import { zodResolver } from '@hookform/resolvers/zod'; +import { error } from '@tauri-apps/plugin-log'; import { useMemo, useState } from 'react'; import { type SubmitHandler, useForm } from 'react-hook-form'; import { useNavigate } from 'react-router-dom'; import { z } from 'zod'; - import { useI18nContext } from '../../../../../i18n/i18n-react'; import { FormInput } from '../../../../../shared/defguard-ui/components/Form/FormInput/FormInput'; import { ArrowSingle } from '../../../../../shared/defguard-ui/components/icons/ArrowSingle/ArrowSingle'; @@ -23,6 +23,7 @@ import { patternValidWireguardKey, } from '../../../../../shared/patterns'; import { routes } from '../../../../../shared/routes'; +import { errorDetail } from '../../../../../shared/utils/errorDetail'; import { validateIpOrDomainList } from '../../../../../shared/validators/tunnel'; import { clientApi } from '../../../clientAPI/clientApi'; import type { Tunnel } from '../../../types'; @@ -197,9 +198,11 @@ export const EditTunnelFormCard = ({ tunnel, submitRef }: Props) => { navigate(routes.client.base, { replace: true }); toaster.success(LL.pages.client.pages.editTunnelPage.messages.editSuccess()); }) - .catch(() => - toaster.error(LL.pages.client.pages.editTunnelPage.messages.editError()), - ); + .catch((e) => { + const detail = errorDetail(e); + error(`Failed to update tunnel: ${detail}`); + toaster.error(LL.pages.client.pages.editTunnelPage.messages.editError()); + }); }; const { handleSubmit, control } = useForm({ diff --git a/src/pages/client/pages/ClientEditTunnelPage/modals/DeleteTunnelModal/DeleteTunnelModal.tsx b/src/pages/client/pages/ClientEditTunnelPage/modals/DeleteTunnelModal/DeleteTunnelModal.tsx index aa314248..cfb0274e 100644 --- a/src/pages/client/pages/ClientEditTunnelPage/modals/DeleteTunnelModal/DeleteTunnelModal.tsx +++ b/src/pages/client/pages/ClientEditTunnelPage/modals/DeleteTunnelModal/DeleteTunnelModal.tsx @@ -1,13 +1,14 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { error } from '@tauri-apps/plugin-log'; import { isUndefined } from 'lodash-es'; import { useNavigate } from 'react-router-dom'; import { shallow } from 'zustand/shallow'; - import { useI18nContext } from '../../../../../../i18n/i18n-react'; import { ConfirmModal } from '../../../../../../shared/defguard-ui/components/Layout/modals/ConfirmModal/ConfirmModal'; import { ConfirmModalType } from '../../../../../../shared/defguard-ui/components/Layout/modals/ConfirmModal/types'; import { useToaster } from '../../../../../../shared/defguard-ui/hooks/toasts/useToaster'; import { routes } from '../../../../../../shared/routes'; +import { errorDetail } from '../../../../../../shared/utils/errorDetail'; import { clientApi } from '../../../../clientAPI/clientApi'; import { useClientStore } from '../../../../hooks/useClientStore'; import { clientQueryKeys } from '../../../../query'; @@ -59,7 +60,8 @@ export const DeleteTunnelModal = () => { message: String(e), }), ); - console.error(e); + const detail = errorDetail(e); + error(`Failed to delete tunnel "${tunnel?.name}" (id: ${tunnel?.id}): ${detail}`); }, }); diff --git a/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationCardConnectButton/LocationCardConnectButton.tsx b/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationCardConnectButton/LocationCardConnectButton.tsx index 88a14c84..1b416876 100644 --- a/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationCardConnectButton/LocationCardConnectButton.tsx +++ b/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationCardConnectButton/LocationCardConnectButton.tsx @@ -3,7 +3,6 @@ import './style.scss'; import { error } from '@tauri-apps/plugin-log'; import classNames from 'classnames'; import { useState } from 'react'; - import { useI18nContext } from '../../../../../../../../i18n/i18n-react'; import SvgIconCheckmarkSmall from '../../../../../../../../shared/components/svg/IconCheckmarkSmall'; import { Button } from '../../../../../../../../shared/defguard-ui/components/Layout/Button/Button'; @@ -13,6 +12,7 @@ import { } from '../../../../../../../../shared/defguard-ui/components/Layout/Button/types'; import SvgIconX from '../../../../../../../../shared/defguard-ui/components/svg/IconX'; import { useToaster } from '../../../../../../../../shared/defguard-ui/hooks/toasts/useToaster'; +import { errorDetail } from '../../../../../../../../shared/utils/errorDetail'; import { clientApi } from '../../../../../../clientAPI/clientApi'; import { type CommonWireguardFields, LocationMfaType } from '../../../../../../types'; import { useMFAModal } from '../../modals/MFAModal/useMFAModal'; @@ -66,8 +66,10 @@ export const LocationCardConnectButton = ({ location }: Props) => { message: String(e), }), ); - error(`Error handling interface: ${e}`); - console.error(e); + const detail = errorDetail(e); + error( + `Error handling interface for location ${location?.id} (${location?.active ? 'disconnect' : 'connect'}): ${detail}`, + ); } }; diff --git a/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationCardRoute/LocationCardRoute.tsx b/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationCardRoute/LocationCardRoute.tsx index e4214320..30ab35ce 100644 --- a/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationCardRoute/LocationCardRoute.tsx +++ b/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationCardRoute/LocationCardRoute.tsx @@ -5,6 +5,7 @@ import { useMemo } from 'react'; import { useI18nContext } from '../../../../../../../../i18n/i18n-react'; import { Toggle } from '../../../../../../../../shared/defguard-ui/components/Layout/Toggle/Toggle'; import type { ToggleOption } from '../../../../../../../../shared/defguard-ui/components/Layout/Toggle/types'; +import { errorDetail } from '../../../../../../../../shared/utils/errorDetail'; import { clientApi } from '../../../../../../clientAPI/clientApi'; import { ClientConnectionType, @@ -30,8 +31,10 @@ export const LocationCardRoute = ({ location, selectedDefguardInstance }: Props) }); } } catch (e) { - error(`Error handling routing: ${e}`); - console.error(e); + const detail = errorDetail(e); + error( + `Failed to update routing for location ${location?.id} (type: ${location?.connection_type}, routeAllTraffic: ${value}): ${detail}`, + ); } }; diff --git a/src/pages/client/pages/ClientInstancePage/components/LocationsList/modals/MFAModal/MFAModal.tsx b/src/pages/client/pages/ClientInstancePage/components/LocationsList/modals/MFAModal/MFAModal.tsx index 86cc148d..29a7769e 100644 --- a/src/pages/client/pages/ClientInstancePage/components/LocationsList/modals/MFAModal/MFAModal.tsx +++ b/src/pages/client/pages/ClientInstancePage/components/LocationsList/modals/MFAModal/MFAModal.tsx @@ -23,6 +23,7 @@ import { MessageBoxType } from '../../../../../../../../shared/defguard-ui/compo import { ModalWithTitle } from '../../../../../../../../shared/defguard-ui/components/Layout/modals/ModalWithTitle/ModalWithTitle'; import { useToaster } from '../../../../../../../../shared/defguard-ui/hooks/toasts/useToaster'; import { isPresent } from '../../../../../../../../shared/defguard-ui/utils/isPresent'; +import { errorDetail } from '../../../../../../../../shared/utils/errorDetail'; import { clientApi } from '../../../../../../clientAPI/clientApi'; import { useClientStore } from '../../../../../../hooks/useClientStore'; import { type DefguardInstance, LocationMfaType } from '../../../../../../types'; @@ -156,7 +157,9 @@ export const MFAModal = () => { return data; } else { const errorData = ((await response.json()) as unknown as MFAError).error; - error(`MFA failed to start with the following error: ${errorData}`); + error( + `MFA start returned non-OK for location ${location?.network_id} (method: ${method}): ${errorData}`, + ); if (method === 2) { setScreen('openid_unavailable'); return; @@ -171,7 +174,10 @@ export const MFAModal = () => { return; } } catch (rej) { - error(`Failed to execute proxy request: ${rej}`); + const detail = errorDetail(rej); + error( + `MFA start request to proxy failed for location ${location?.network_id} (method: ${method}): ${detail}`, + ); toaster.error(localLL.errors.mfaStartGeneric()); return; } @@ -456,15 +462,27 @@ const OpenIDMFAPending = ({ proxyUrl, token, resetState }: OpenIDMFAPendingProps } const body_token = { token }; - const response = await fetch(`${proxyUrl + CLIENT_MFA_ENDPOINT}/finish`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - CLIENT_VERSION_HEADER: platformInfo.client_version, - CLIENT_PLATFORM_HEADER: platformInfo.platform_info, - }, - body: JSON.stringify(body_token), - }); + let response: Awaited>; + try { + response = await fetch(`${proxyUrl + CLIENT_MFA_ENDPOINT}/finish`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + CLIENT_VERSION_HEADER: platformInfo.client_version, + CLIENT_PLATFORM_HEADER: platformInfo.platform_info, + }, + body: JSON.stringify(body_token), + }); + } catch (e) { + clearInterval(interval); + clearTimeout(timeoutId); + const detail = errorDetail(e); + error( + `OpenID MFA poll request failed (network error) for location ${location?.id}: ${detail}`, + ); + setErrorMessage(localLL.errors.mfaStartGeneric()); + return; + } if (response.ok) { clearInterval(interval); @@ -491,13 +509,17 @@ const OpenIDMFAPending = ({ proxyUrl, token, resetState }: OpenIDMFAPendingProps const { error: errorMessage } = data; if (errorMessage === 'invalid token') { - error(JSON.stringify(data, null, 2)); + error(`OpenID MFA poll failed: invalid token. Response: ${JSON.stringify(data)}`); setErrorMessage(localLL.errors.tokenExpired()); } else if (errorMessage === 'login session not found') { - error(JSON.stringify(data, null, 2)); + error( + `OpenID MFA poll failed: login session not found. Response: ${JSON.stringify(data)}`, + ); setErrorMessage(localLL.errors.sessionInvalidated()); } else { - error(JSON.stringify(data, null, 2)); + error( + `OpenID MFA poll failed with unhandled error. Response: ${JSON.stringify(data)}`, + ); setErrorMessage(localLL.errors.mfaStartGeneric()); } }; @@ -609,21 +631,27 @@ const MFACodeForm = ({ description, token, proxyUrl, resetState }: MFACodeForm) errorMessage === 'invalid token' || errorMessage === 'login session not found' ) { - console.error(data); toaster.error(localLL.errors.tokenExpired()); resetState(); - error(JSON.stringify(data)); + error( + `MFA code finish failed: session expired or invalid token for location ${location?.id}. Response: ${JSON.stringify(data)}`, + ); return; } else { toaster.error(localLL.errors.mfaFinishGeneric()); } setMFAError(message); - error(JSON.stringify(data)); + error( + `MFA code finish failed for location ${location?.id}. Response: ${JSON.stringify(data)}`, + ); return; } } catch (rej) { - error(`Failed to execute proxy request: ${rej}`); + const detail = errorDetail(rej); + error( + `MFA code finish request to proxy failed for location ${location?.id}: ${detail}`, + ); toaster.error(localLL.errors.mfaFinishGeneric()); return; } diff --git a/src/pages/client/pages/ClientInstancePage/components/LocationsList/modals/MFAModal/components/MfaMobileApprove/MfaMobileApprove.tsx b/src/pages/client/pages/ClientInstancePage/components/LocationsList/modals/MFAModal/components/MfaMobileApprove/MfaMobileApprove.tsx index a84da6b1..cbf09038 100644 --- a/src/pages/client/pages/ClientInstancePage/components/LocationsList/modals/MFAModal/components/MfaMobileApprove/MfaMobileApprove.tsx +++ b/src/pages/client/pages/ClientInstancePage/components/LocationsList/modals/MFAModal/components/MfaMobileApprove/MfaMobileApprove.tsx @@ -12,6 +12,7 @@ import './style.scss'; import { debug, error } from '@tauri-apps/plugin-log'; import { Button } from '../../../../../../../../../../shared/defguard-ui/components/Layout/Button/Button'; import { MessageBox } from '../../../../../../../../../../shared/defguard-ui/components/Layout/MessageBox/MessageBox'; +import { errorDetail } from '../../../../../../../../../../shared/utils/errorDetail'; type MfaMobileQrData = { token: string; @@ -96,7 +97,8 @@ export const MfaMobileApprove = ({ toaster.success('Connection authorized.'); }) .catch((e) => { - console.error(e); + const detail = errorDetail(e); + error(`MFA mobile connect failed for location ${location.id}: ${detail}`); }); } else { // catch possible changes in api diff --git a/src/pages/client/pages/ClientInstancePage/modals/DeleteInstanceModal/DeleteInstanceModal.tsx b/src/pages/client/pages/ClientInstancePage/modals/DeleteInstanceModal/DeleteInstanceModal.tsx index fb532e3b..cabf5d7c 100644 --- a/src/pages/client/pages/ClientInstancePage/modals/DeleteInstanceModal/DeleteInstanceModal.tsx +++ b/src/pages/client/pages/ClientInstancePage/modals/DeleteInstanceModal/DeleteInstanceModal.tsx @@ -1,14 +1,15 @@ import './style.scss'; import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { error } from '@tauri-apps/plugin-log'; import { isUndefined } from 'lodash-es'; import { useEffect } from 'react'; import { shallow } from 'zustand/shallow'; - import { useI18nContext } from '../../../../../../i18n/i18n-react'; import { ConfirmModal } from '../../../../../../shared/defguard-ui/components/Layout/modals/ConfirmModal/ConfirmModal'; import { ConfirmModalType } from '../../../../../../shared/defguard-ui/components/Layout/modals/ConfirmModal/types'; import { useToaster } from '../../../../../../shared/defguard-ui/hooks/toasts/useToaster'; +import { errorDetail } from '../../../../../../shared/utils/errorDetail'; import { clientApi } from '../../../../clientAPI/clientApi'; import { useClientStore } from '../../../../hooks/useClientStore'; import { clientQueryKeys } from '../../../../query'; @@ -58,7 +59,10 @@ export const DeleteInstanceModal = () => { message: String(e), }), ); - console.error(e); + const detail = errorDetail(e); + error( + `Failed to delete instance "${instance?.name}" (id: ${instance?.id}): ${detail}`, + ); }, }); diff --git a/src/pages/client/pages/ClientInstancePage/modals/UpdateInstanceModal/components/UpdateInstanceModalForm.tsx b/src/pages/client/pages/ClientInstancePage/modals/UpdateInstanceModal/components/UpdateInstanceModalForm.tsx index cae2a283..f20ecdc0 100644 --- a/src/pages/client/pages/ClientInstancePage/modals/UpdateInstanceModal/components/UpdateInstanceModalForm.tsx +++ b/src/pages/client/pages/ClientInstancePage/modals/UpdateInstanceModal/components/UpdateInstanceModalForm.tsx @@ -1,6 +1,7 @@ import { zodResolver } from '@hookform/resolvers/zod'; import { useQueryClient } from '@tanstack/react-query'; import { fetch } from '@tauri-apps/plugin-http'; +import { error } from '@tauri-apps/plugin-log'; import { useMemo } from 'react'; import { type SubmitHandler, useForm } from 'react-hook-form'; import { z } from 'zod'; @@ -16,6 +17,7 @@ import type { CreateDeviceResponse, EnrollmentStartResponse, } from '../../../../../../../shared/hooks/api/types'; +import { errorDetail } from '../../../../../../../shared/utils/errorDetail'; import { clientApi } from '../../../../../clientAPI/clientApi'; import { useClientStore } from '../../../../../hooks/useClientStore'; import { clientQueryKeys } from '../../../../../query'; @@ -93,11 +95,20 @@ export const UpdateInstanceModalForm = () => { token: values.token, }; - const res = await fetch(endpointUrl, { - method: 'POST', - headers, - body: JSON.stringify(data), - }); + let res: Awaited>; + try { + res = await fetch(endpointUrl, { + method: 'POST', + headers, + body: JSON.stringify(data), + }); + } catch (e) { + const detail = errorDetail(e); + error(`Failed to reach enrollment start endpoint (${endpointUrl}): ${detail}`); + toaster.error(LL.common.messages.errorWithMessage({ message: String(e) })); + return; + } + if (res.ok) { const enrollmentData = (await res.json()) as EnrollmentStartResponse; let proxy_api_url = values.url; @@ -111,6 +122,9 @@ export const UpdateInstanceModalForm = () => { .getSetCookie() .find((cookie) => cookie.startsWith('defguard_proxy=')); if (!authCookie) { + error( + `No auth cookie returned from enrollment start for instance ${instance.uuid}`, + ); toaster.error( LL.common.messages.errorWithMessage({ message: LL.common.messages.noCookie(), @@ -119,16 +133,25 @@ export const UpdateInstanceModalForm = () => { return; } headers.Cookie = authCookie; - const instanceInfoResponse = await fetch( - `${proxy_api_url}/enrollment/network_info`, - { + + let instanceInfoResponse: Awaited>; + try { + instanceInfoResponse = await fetch(`${proxy_api_url}/enrollment/network_info`, { method: 'POST', headers, body: JSON.stringify({ pubkey: instance.pubkey, }), - }, - ); + }); + } catch (e) { + const detail = errorDetail(e); + error( + `Failed to reach network_info endpoint for instance ${instance.uuid}: ${detail}`, + ); + toaster.error(LL.common.messages.errorWithMessage({ message: String(e) })); + return; + } + if (instanceInfoResponse.ok) { const data = (await instanceInfoResponse.json()) as CreateDeviceResponse; updateInstance({ @@ -149,11 +172,16 @@ export const UpdateInstanceModalForm = () => { ); closeModal(); }) - .catch(() => { + .catch((e) => { + const detail = errorDetail(e); + error(`Failed to update instance: ${detail}`); toaster.error(LL.common.messages.error()); }); } else { // Device does not match used enrollment token. + error( + `network_info returned non-ok status ${instanceInfoResponse.status} for instance ${instance.uuid}`, + ); toaster.error( LL.common.messages.errorWithMessage({ message: 'Token is not valid for this device', @@ -169,6 +197,9 @@ export const UpdateInstanceModalForm = () => { } } else { // Instance not found in client, use add instance. + error( + `Enrollment start returned unknown instance UUID: ${enrollmentData.instance.id}`, + ); toaster.error(localLL.messages.errorInstanceNotFound()); setError( 'token', @@ -182,6 +213,9 @@ export const UpdateInstanceModalForm = () => { } } else { // Token or URL is invalid. + error( + `Enrollment start returned non-ok status ${res.status} for URL ${endpointUrl}`, + ); toaster.error( LL.common.messages.errorWithMessage({ message: 'Token or URL is invalid', diff --git a/src/pages/client/pages/ClientSettingsPage/components/GlobalLogs/GlobalLogs.tsx b/src/pages/client/pages/ClientSettingsPage/components/GlobalLogs/GlobalLogs.tsx index a4367da2..e24d149e 100644 --- a/src/pages/client/pages/ClientSettingsPage/components/GlobalLogs/GlobalLogs.tsx +++ b/src/pages/client/pages/ClientSettingsPage/components/GlobalLogs/GlobalLogs.tsx @@ -11,6 +11,7 @@ import { ActionButton } from '../../../../../../shared/defguard-ui/components/La import { ActionButtonVariant } from '../../../../../../shared/defguard-ui/components/Layout/ActionButton/types'; import { Card } from '../../../../../../shared/defguard-ui/components/Layout/Card/Card'; import { Helper } from '../../../../../../shared/defguard-ui/components/Layout/Helper/Helper'; +import { errorDetail } from '../../../../../../shared/utils/errorDetail'; import { clientApi } from '../../../../clientAPI/clientApi'; import type { GlobalLogLevel, @@ -47,7 +48,8 @@ export const GlobalLogs = () => { throw new Error('No path selected'); } } catch (e) { - error(`Failed to save logs: ${e}`); + const detail = errorDetail(e); + error(`Failed to save logs to file: ${detail}`); } }; diff --git a/src/pages/enrollment/steps/DeviceStep/components/DesktopSetup/DesktopSetup.tsx b/src/pages/enrollment/steps/DeviceStep/components/DesktopSetup/DesktopSetup.tsx index af01aef9..52fa40d7 100644 --- a/src/pages/enrollment/steps/DeviceStep/components/DesktopSetup/DesktopSetup.tsx +++ b/src/pages/enrollment/steps/DeviceStep/components/DesktopSetup/DesktopSetup.tsx @@ -7,7 +7,6 @@ import { isUndefined } from 'lodash-es'; import { useMemo, useState } from 'react'; import { type SubmitHandler, useForm } from 'react-hook-form'; import { z } from 'zod'; - import { useI18nContext } from '../../../../../../i18n/i18n-react'; import { FormInput } from '../../../../../../shared/defguard-ui/components/Form/FormInput/FormInput'; import { Button } from '../../../../../../shared/defguard-ui/components/Layout/Button/Button'; @@ -18,6 +17,7 @@ import { import { Card } from '../../../../../../shared/defguard-ui/components/Layout/Card/Card'; import { useToaster } from '../../../../../../shared/defguard-ui/hooks/toasts/useToaster'; import type { CreateDeviceResponse } from '../../../../../../shared/hooks/api/types'; +import { errorDetail } from '../../../../../../shared/utils/errorDetail'; import { generateWGKeys } from '../../../../../../shared/utils/generateWGKeys'; import { EnrollmentStepIndicator } from '../../../../components/EnrollmentStepIndicator/EnrollmentStepIndicator'; import { EnrollmentStepKey } from '../../../../const'; @@ -47,7 +47,8 @@ export const DesktopSetup = () => { useMutation({ mutationFn: createDevice, onError: (e) => { - error(String(e)); + const detail = errorDetail(e); + error(`createDevice mutation failed during enrollment: ${detail}`); }, }); diff --git a/src/pages/enrollment/steps/MfaSetupStep/MfaSetupStep.tsx b/src/pages/enrollment/steps/MfaSetupStep/MfaSetupStep.tsx index 10f826c8..ac3d005a 100644 --- a/src/pages/enrollment/steps/MfaSetupStep/MfaSetupStep.tsx +++ b/src/pages/enrollment/steps/MfaSetupStep/MfaSetupStep.tsx @@ -134,7 +134,6 @@ const CodeForm = ({ inputRef }: CodeFormProps) => { }, ); error(`MFA configuration failed! \nReason: ${err.message}`); - console.error(err); }, }); diff --git a/src/pages/enrollment/steps/SendFinishStep/SendFinishStep.tsx b/src/pages/enrollment/steps/SendFinishStep/SendFinishStep.tsx index 64a108f6..ef033a95 100644 --- a/src/pages/enrollment/steps/SendFinishStep/SendFinishStep.tsx +++ b/src/pages/enrollment/steps/SendFinishStep/SendFinishStep.tsx @@ -10,6 +10,7 @@ import type { ActivateUserRequest, CreateDeviceResponse, } from '../../../../shared/hooks/api/types'; +import { errorDetail } from '../../../../shared/utils/errorDetail'; import { clientApi } from '../../../client/clientAPI/clientApi'; import { clientQueryKeys } from '../../../client/query'; import { useEnrollmentStore } from '../../hooks/store/useEnrollmentStore'; @@ -62,6 +63,8 @@ export const SendFinishStep = () => { }); }) .catch((e) => { + const detail = errorDetail(e); + error(`Failed to save config after user activation: ${detail}`); if (typeof e === 'string') { if (e.includes('Network Error')) { toaster.error(LL.common.messages.networkError()); @@ -101,8 +104,8 @@ export const SendFinishStep = () => { message: String(e), }), ); - console.error(e); - error(String(e)); + const detail = errorDetail(e); + error(`activateUser mutation failed during enrollment finish: ${detail}`); }, onSuccess: () => { setEnrollmentStore({ loading: false, step: EnrollmentStepKey.FINISH }); diff --git a/src/shared/components/providers/DeepLinkProvider.tsx b/src/shared/components/providers/DeepLinkProvider.tsx index 26658c13..2f0e9024 100644 --- a/src/shared/components/providers/DeepLinkProvider.tsx +++ b/src/shared/components/providers/DeepLinkProvider.tsx @@ -3,6 +3,7 @@ import { error } from '@tauri-apps/plugin-log'; import { type PropsWithChildren, useCallback, useEffect, useRef } from 'react'; import z, { string } from 'zod'; import useAddInstance from '../../hooks/useAddInstance'; +import { errorDetail } from '../../utils/errorDetail'; enum DeepLink { AddInstance = 'addinstance', @@ -92,9 +93,8 @@ export const DeepLinkProvider = ({ children }: PropsWithChildren) => { try { handleValidLink(payload, start[0]); } catch (e) { - error( - `Failed to handle valid deep link ${payload.link}!\n${JSON.stringify(e)}`, - ); + const detail = errorDetail(e); + error(`Failed to handle startup deep link "${payload.link}": ${detail}`); } } } @@ -106,8 +106,10 @@ export const DeepLinkProvider = ({ children }: PropsWithChildren) => { try { handleValidLink(payload); } catch (e) { - error(`Failed to handle valid deep link ${payload?.link} action!`); - error(JSON.stringify(e)); + const detail = errorDetail(e); + error( + `Failed to handle valid deep link "${payload?.link}" action: ${detail}`, + ); } } } diff --git a/src/shared/hooks/useClipboard.ts b/src/shared/hooks/useClipboard.ts index 2db90825..f02c7837 100644 --- a/src/shared/hooks/useClipboard.ts +++ b/src/shared/hooks/useClipboard.ts @@ -1,8 +1,10 @@ import { writeText } from '@tauri-apps/plugin-clipboard-manager'; +import { error } from '@tauri-apps/plugin-log'; import { useCallback } from 'react'; import { useI18nContext } from '../../i18n/i18n-react'; import { useToaster } from '../defguard-ui/hooks/toasts/useToaster'; +import { errorDetail } from '../utils/errorDetail'; export const useClipboard = () => { const { LL } = useI18nContext(); @@ -20,7 +22,8 @@ export const useClipboard = () => { } } catch (e) { toaster.error(LL.common.messages.clipboard.error()); - console.error(e); + const detail = errorDetail(e); + error(`Failed to write to clipboard: ${detail}`); } }, [LL.common.messages, toaster], diff --git a/src/shared/utils/errorDetail.ts b/src/shared/utils/errorDetail.ts new file mode 100644 index 00000000..59cf3bf8 --- /dev/null +++ b/src/shared/utils/errorDetail.ts @@ -0,0 +1,9 @@ +/** + * Extracts the most useful debug string from an unknown caught value. + * + * - For Error objects: returns the stack trace (includes message + call chain). + * Falls back to message if stack is unavailable. + * - For anything else (string, number, …): coerces to string. + */ +export const errorDetail = (e: unknown): string => + e instanceof Error ? (e.stack ?? e.message) : String(e); From 47713c2dfea33069aa27dfa6d5b63372441c5bbd Mon Sep 17 00:00:00 2001 From: Aleksander <170264518+t-aleksander@users.noreply.github.com> Date: Wed, 18 Mar 2026 14:27:44 +0100 Subject: [PATCH 2/2] sort --- .../components/MfaMobileApprove/MfaMobileApprove.tsx | 11 ++++++----- .../MfaRecoveryCodesStep/MfaRecoveryCodesStep.tsx | 1 + .../steps/SendFinishStep/SendFinishStep.tsx | 7 ++++--- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/pages/client/pages/ClientInstancePage/components/LocationsList/modals/MFAModal/components/MfaMobileApprove/MfaMobileApprove.tsx b/src/pages/client/pages/ClientInstancePage/components/LocationsList/modals/MFAModal/components/MfaMobileApprove/MfaMobileApprove.tsx index cbf09038..62b3f1f5 100644 --- a/src/pages/client/pages/ClientInstancePage/components/LocationsList/modals/MFAModal/components/MfaMobileApprove/MfaMobileApprove.tsx +++ b/src/pages/client/pages/ClientInstancePage/components/LocationsList/modals/MFAModal/components/MfaMobileApprove/MfaMobileApprove.tsx @@ -1,18 +1,19 @@ +import './style.scss'; + +import { debug, error } from '@tauri-apps/plugin-log'; import { fromUint8Array } from 'js-base64'; import { useEffect, useMemo } from 'react'; import QrCode from 'react-qr-code'; import useWebSocket from 'react-use-websocket'; import z from 'zod'; import { shallow } from 'zustand/shallow'; +import { Button } from '../../../../../../../../../../shared/defguard-ui/components/Layout/Button/Button'; +import { MessageBox } from '../../../../../../../../../../shared/defguard-ui/components/Layout/MessageBox/MessageBox'; import { useToaster } from '../../../../../../../../../../shared/defguard-ui/hooks/toasts/useToaster'; +import { errorDetail } from '../../../../../../../../../../shared/utils/errorDetail'; import { clientApi } from '../../../../../../../../clientAPI/clientApi'; import type { CommonWireguardFields } from '../../../../../../../../types'; import { useMFAModal } from '../../useMFAModal'; -import './style.scss'; -import { debug, error } from '@tauri-apps/plugin-log'; -import { Button } from '../../../../../../../../../../shared/defguard-ui/components/Layout/Button/Button'; -import { MessageBox } from '../../../../../../../../../../shared/defguard-ui/components/Layout/MessageBox/MessageBox'; -import { errorDetail } from '../../../../../../../../../../shared/utils/errorDetail'; type MfaMobileQrData = { token: string; diff --git a/src/pages/enrollment/steps/MfaRecoveryCodesStep/MfaRecoveryCodesStep.tsx b/src/pages/enrollment/steps/MfaRecoveryCodesStep/MfaRecoveryCodesStep.tsx index ad332097..158b007b 100644 --- a/src/pages/enrollment/steps/MfaRecoveryCodesStep/MfaRecoveryCodesStep.tsx +++ b/src/pages/enrollment/steps/MfaRecoveryCodesStep/MfaRecoveryCodesStep.tsx @@ -1,4 +1,5 @@ import './style.scss'; + import { useEffect } from 'react'; import { shallow } from 'zustand/shallow'; import { Button } from '../../../../shared/defguard-ui/components/Layout/Button/Button'; diff --git a/src/pages/enrollment/steps/SendFinishStep/SendFinishStep.tsx b/src/pages/enrollment/steps/SendFinishStep/SendFinishStep.tsx index ef033a95..e2f39f49 100644 --- a/src/pages/enrollment/steps/SendFinishStep/SendFinishStep.tsx +++ b/src/pages/enrollment/steps/SendFinishStep/SendFinishStep.tsx @@ -1,3 +1,5 @@ +import './style.scss'; + import { useMutation, useQueryClient } from '@tanstack/react-query'; import { debug, error, info } from '@tauri-apps/plugin-log'; import { useCallback } from 'react'; @@ -6,6 +8,7 @@ import { useI18nContext } from '../../../../i18n/i18n-react'; import { Card } from '../../../../shared/defguard-ui/components/Layout/Card/Card'; import { LoaderSpinner } from '../../../../shared/defguard-ui/components/Layout/LoaderSpinner/LoaderSpinner'; import { useToaster } from '../../../../shared/defguard-ui/hooks/toasts/useToaster'; +import useEffectOnce from '../../../../shared/defguard-ui/utils/useEffectOnce'; import type { ActivateUserRequest, CreateDeviceResponse, @@ -13,11 +16,9 @@ import type { import { errorDetail } from '../../../../shared/utils/errorDetail'; import { clientApi } from '../../../client/clientAPI/clientApi'; import { clientQueryKeys } from '../../../client/query'; +import { EnrollmentStepKey } from '../../const'; import { useEnrollmentStore } from '../../hooks/store/useEnrollmentStore'; import { useEnrollmentApi } from '../../hooks/useEnrollmentApi'; -import './style.scss'; -import useEffectOnce from '../../../../shared/defguard-ui/utils/useEffectOnce'; -import { EnrollmentStepKey } from '../../const'; const { saveConfig } = clientApi;