feat: sentry integration (#51)

* feat: integrate sentry error reporting

* feat: report logger errors to sentry with 30s log context
This commit is contained in:
vin authored and GitHub committed 2026-01-30 11:48:49 +08:00
1 parent 1265d044f6
commit a785640d7a
22 files changed
+2143 -227

No files matched your search

+91 -9
View File
@@ -1,7 +1,12 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useCallback, useEffect, useRef } from 'react';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { toast } from '@/components/ui/use-toast';
import { ipc } from '@/ipc/manager';
import { AppConfig } from '@/types/config';
import { toast } from '@/components/ui/use-toast';
const SAVE_DEBOUNCE_MS = 400;
export function useAppConfig() {
const queryClient = useQueryClient();
@@ -20,28 +25,105 @@ export function useAppConfig() {
const updateConfig = useMutation({
mutationFn: async (newConfig: AppConfig) => {
await ipc.client.config.save(newConfig);
return newConfig;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['appConfig'] });
});
const latestConfigRef = useRef<AppConfig | null>(null);
const lastStableConfigRef = useRef<AppConfig | null>(null);
const pendingResolversRef = useRef<
Array<{ resolve: () => void; reject: (error: Error) => void }>
>([]);
const debounceTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
useEffect(() => {
if (config) {
lastStableConfigRef.current = config;
}
}, [config]);
const flushPendingSave = useCallback(async () => {
const pendingBatch = pendingResolversRef.current.splice(0);
const nextConfig = latestConfigRef.current;
if (!nextConfig) {
for (const item of pendingBatch) {
item.resolve();
}
return;
}
try {
const savedConfig = await updateConfig.mutateAsync(nextConfig);
queryClient.setQueryData(['appConfig'], savedConfig);
lastStableConfigRef.current = savedConfig;
toast({
title: 'Settings saved',
description: 'Your configuration has been updated.',
});
},
onError: (err) => {
for (const item of pendingBatch) {
item.resolve();
}
} catch (err) {
const error = err instanceof Error ? err : new Error('Failed to save settings');
const fallback = lastStableConfigRef.current;
if (fallback) {
queryClient.setQueryData(['appConfig'], fallback);
} else {
queryClient.invalidateQueries({ queryKey: ['appConfig'] });
}
toast({
title: 'Error saving settings',
description: err.message,
description: error.message,
variant: 'destructive',
});
for (const item of pendingBatch) {
item.reject(error);
}
}
}, [queryClient, updateConfig]);
const saveConfig = useCallback(
(newConfig: AppConfig) => {
latestConfigRef.current = newConfig;
queryClient.setQueryData(['appConfig'], newConfig);
if (debounceTimerRef.current) {
clearTimeout(debounceTimerRef.current);
}
return new Promise<void>((resolve, reject) => {
pendingResolversRef.current.push({ resolve, reject });
debounceTimerRef.current = setTimeout(() => {
debounceTimerRef.current = null;
flushPendingSave();
}, SAVE_DEBOUNCE_MS);
});
},
});
[flushPendingSave, queryClient],
);
useEffect(() => {
const handleBeforeUnload = () => {
if (!debounceTimerRef.current) {
return;
}
clearTimeout(debounceTimerRef.current);
debounceTimerRef.current = null;
void flushPendingSave();
};
window.addEventListener('beforeunload', handleBeforeUnload);
return () => {
window.removeEventListener('beforeunload', handleBeforeUnload);
handleBeforeUnload();
};
}, [flushPendingSave]);
return {
config,
isLoading,
error,
saveConfig: updateConfig.mutateAsync,
saveConfig,
isSaving: updateConfig.isPending,
};
}