From 795bdfc2865c15bd0ef07a3035b648f3eac6e3cc Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 17 Apr 2026 02:55:23 +0000 Subject: [PATCH] refactor(errors): route silent catches through logError/reportError MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every catch now either surfaces, re-throws, or calls logError with a scope tag. No more empty catches or bare console.error: - auth-store: signInToFirebase / restoreSession / signOut paths gain logError context. Behavior is unchanged (best-effort local sign-out, fall back to login on restore failure). - use-stream-autoplay: Audio.play() and download-URL fetches log their failures instead of dropping silently (both are nice-to-haves so UX stays silent — but we can now trace "why didn't autoplay trigger?"). - pusher-client: ws errors / parse failures / server errors / listener crashes all routed through logError, and listener bugs (which silently break user flows) now go through reportError so they're actually surfaced in observability. - settings-page: email-notifications toggle now toasts on failure instead of silently reverting with no explanation. - huddle-app: screen-share failures use logError. --- js/src/features/settings-page.tsx | 7 +++++-- js/src/hooks/use-stream-autoplay.ts | 13 +++++++++---- js/src/huddle_window/HuddleApp.tsx | 3 ++- js/src/lib/pusher-client.ts | 17 +++++++++++------ js/src/stores/auth-store.ts | 20 +++++++++++++------- 5 files changed, 40 insertions(+), 20 deletions(-) diff --git a/js/src/features/settings-page.tsx b/js/src/features/settings-page.tsx index 6d9a706..d6988a9 100644 --- a/js/src/features/settings-page.tsx +++ b/js/src/features/settings-page.tsx @@ -11,6 +11,8 @@ import { ScrollArea } from "@/components/ui/scroll-area"; import { CopyableEmail } from "@/components/copyable-email"; import { useAuthStore } from "@/stores/auth-store"; import { apiClient } from "@/api/client"; +import { logError, toUserMessage } from "@/lib/errors"; +import { toast } from "sonner"; import { PRIVACY_URL, SUPPORT_EMAIL, TERMS_URL } from "@/lib/constants"; import { ArrowLeft } from "lucide-react"; @@ -84,12 +86,13 @@ export default function SettingsPage() { })); try { await apiClient.updateSettings({ email_notifications_enabled: checked }); - } catch { - // Revert on failure + } catch (err) { setEmailNotifications(!checked); useAuthStore.setState((state) => ({ user: state.user ? { ...state.user, email_notifications_enabled: !checked } : null, })); + toast.error(toUserMessage(err)); + logError(err, { scope: "settings.emailNotifications" }); } }; diff --git a/js/src/hooks/use-stream-autoplay.ts b/js/src/hooks/use-stream-autoplay.ts index 16f8235..05abeee 100644 --- a/js/src/hooks/use-stream-autoplay.ts +++ b/js/src/hooks/use-stream-autoplay.ts @@ -5,6 +5,7 @@ import { apiClient } from "@/api/client"; import { useAuthStore } from "@/stores/auth-store"; import { useAutoplayStore } from "@/stores/autoplay-store"; import { resolveHumanDisplay } from "@/lib/humans"; +import { logError } from "@/lib/errors"; /** * Triggers autoplay when a stream's latest child changes to a new media particle. @@ -37,7 +38,11 @@ export function useStreamAutoplay( if (useAutoplayStore.getState().muted) return; if (latestChild.type === "text") { - new Audio(beepSound).play().catch(() => {}); + // Browser autoplay policy can block this before user interaction; that's + // fine — the beep is a nice-to-have, not a critical signal. + new Audio(beepSound).play().catch((err) => + logError(err, { scope: "autoplay.beep" }), + ); return; } @@ -60,8 +65,8 @@ export function useStreamAutoplay( senderName: displayName, senderInitials: initials, }); - }).catch(() => { - // Failed to get download URL — skip autoplay silently - }); + }).catch((err) => + logError(err, { scope: "autoplay.fetchUrl", particleId: particle.id }), + ); }, [latestChild?.id]); // eslint-disable-line react-hooks/exhaustive-deps } diff --git a/js/src/huddle_window/HuddleApp.tsx b/js/src/huddle_window/HuddleApp.tsx index 6ce85ff..3f85480 100644 --- a/js/src/huddle_window/HuddleApp.tsx +++ b/js/src/huddle_window/HuddleApp.tsx @@ -23,6 +23,7 @@ import { import { RoomEvent, Track } from 'livekit-client'; import { useState, useEffect, useRef, useCallback } from 'react'; import { ScreenSourcePicker } from '@/components/screen-source-picker'; +import { logError } from '@/lib/errors'; function readConnectionFromHash(): { token: string; serverUrl: string } | null { const params = new URLSearchParams(window.location.hash.slice(1)); @@ -144,7 +145,7 @@ function HuddleContent() { setIsSharing(true); videoTrack.onended = () => stopScreenShare(); } catch (err) { - console.error('Failed to start screen share:', err); + logError(err, { scope: "huddle.screenShare" }); } }, [room], diff --git a/js/src/lib/pusher-client.ts b/js/src/lib/pusher-client.ts index 1ec4e7b..631e182 100644 --- a/js/src/lib/pusher-client.ts +++ b/js/src/lib/pusher-client.ts @@ -4,6 +4,8 @@ * channel subscriptions, and event dispatching. */ +import { logError, reportError } from "@/lib/errors"; + export type ConnectionState = | "disconnected" | "connecting" @@ -100,8 +102,8 @@ export class PusherClient { }; this.ws.onerror = (event) => { - console.warn("[pusher] websocket error", event); - // onclose will fire after onerror, so reconnection is handled there + // onclose fires after onerror — reconnection is handled there. + logError(event, { scope: "pusher.ws" }); }; this.ws.onmessage = (event) => { @@ -182,13 +184,15 @@ export class PusherClient { let msg: ServerMessage; try { msg = JSON.parse(data); - } catch { - console.warn("[pusher] failed to parse message", data); + } catch (err) { + logError(err, { scope: "pusher.parse", data }); return; } if (msg.type === "error") { - console.warn("[pusher] server error:", msg.message); + logError(new Error(msg.message ?? "pusher server error"), { + scope: "pusher.server", + }); return; } @@ -204,7 +208,8 @@ export class PusherClient { try { cb(msg); } catch (err) { - console.error("[pusher] listener error", err); + // Listener bugs silently break user flows — escalate to reportError. + reportError(err, { scope: "pusher.listener", channel: msg.channel }); } } } diff --git a/js/src/stores/auth-store.ts b/js/src/stores/auth-store.ts index 426cb20..427157d 100644 --- a/js/src/stores/auth-store.ts +++ b/js/src/stores/auth-store.ts @@ -3,14 +3,17 @@ import { signInWithCustomToken, signOut as firebaseSignOut } from "firebase/auth import { apiClient, ApiError } from "@/api/client"; import type { Human } from "@/api/types"; import { firebaseAuth } from "@/firebase"; +import { logError } from "@/lib/errors"; import { useSessionStore } from "./session-store"; async function signInToFirebase() { try { const { token } = await apiClient.getFirebaseToken(); await signInWithCustomToken(firebaseAuth, token); - } catch (e) { - console.error("Failed to sign in to Firebase", e); + } catch (err) { + // Firestore subscriptions will fail until the next successful sign-in; the + // rest of the app keeps working against Orion. + logError(err, { scope: "auth.firebase" }); } } @@ -50,7 +53,9 @@ export const useAuthStore = create((set) => ({ const user = await apiClient.me(); await signInToFirebase(); set({ status: "authenticated", user }); - } catch { + } catch (err) { + // Expected on expired/invalid tokens — fall back to the login screen. + logError(err, { scope: "auth.restore" }); useSessionStore.getState().clearToken(); set({ status: "unauthenticated", user: null }); } @@ -91,11 +96,12 @@ export const useAuthStore = create((set) => ({ set({ isSigningOut: true }); try { await apiClient.signOut(); - } catch { - // Best-effort — sign out locally regardless + } catch (err) { + // Best-effort — sign out locally regardless. + logError(err, { scope: "auth.signOut" }); } finally { - await firebaseSignOut(firebaseAuth).catch((e) => - console.error("Firebase sign-out failed", e), + await firebaseSignOut(firebaseAuth).catch((err) => + logError(err, { scope: "auth.firebaseSignOut" }), ); useSessionStore.getState().clearToken(); set({