refactor(errors): route silent catches through logError/reportError

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.
This commit is contained in:
Claude
2026-04-17 02:55:23 +00:00
parent 8632c98725
commit 795bdfc286
5 changed files with 40 additions and 20 deletions
+5 -2
View File
@@ -11,6 +11,8 @@ import { ScrollArea } from "@/components/ui/scroll-area";
import { CopyableEmail } from "@/components/copyable-email"; import { CopyableEmail } from "@/components/copyable-email";
import { useAuthStore } from "@/stores/auth-store"; import { useAuthStore } from "@/stores/auth-store";
import { apiClient } from "@/api/client"; 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 { PRIVACY_URL, SUPPORT_EMAIL, TERMS_URL } from "@/lib/constants";
import { ArrowLeft } from "lucide-react"; import { ArrowLeft } from "lucide-react";
@@ -84,12 +86,13 @@ export default function SettingsPage() {
})); }));
try { try {
await apiClient.updateSettings({ email_notifications_enabled: checked }); await apiClient.updateSettings({ email_notifications_enabled: checked });
} catch { } catch (err) {
// Revert on failure
setEmailNotifications(!checked); setEmailNotifications(!checked);
useAuthStore.setState((state) => ({ useAuthStore.setState((state) => ({
user: state.user ? { ...state.user, email_notifications_enabled: !checked } : null, user: state.user ? { ...state.user, email_notifications_enabled: !checked } : null,
})); }));
toast.error(toUserMessage(err));
logError(err, { scope: "settings.emailNotifications" });
} }
}; };
+9 -4
View File
@@ -5,6 +5,7 @@ import { apiClient } from "@/api/client";
import { useAuthStore } from "@/stores/auth-store"; import { useAuthStore } from "@/stores/auth-store";
import { useAutoplayStore } from "@/stores/autoplay-store"; import { useAutoplayStore } from "@/stores/autoplay-store";
import { resolveHumanDisplay } from "@/lib/humans"; import { resolveHumanDisplay } from "@/lib/humans";
import { logError } from "@/lib/errors";
/** /**
* Triggers autoplay when a stream's latest child changes to a new media particle. * 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 (useAutoplayStore.getState().muted) return;
if (latestChild.type === "text") { 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; return;
} }
@@ -60,8 +65,8 @@ export function useStreamAutoplay(
senderName: displayName, senderName: displayName,
senderInitials: initials, senderInitials: initials,
}); });
}).catch(() => { }).catch((err) =>
// Failed to get download URL — skip autoplay silently logError(err, { scope: "autoplay.fetchUrl", particleId: particle.id }),
}); );
}, [latestChild?.id]); // eslint-disable-line react-hooks/exhaustive-deps }, [latestChild?.id]); // eslint-disable-line react-hooks/exhaustive-deps
} }
+2 -1
View File
@@ -23,6 +23,7 @@ import {
import { RoomEvent, Track } from 'livekit-client'; import { RoomEvent, Track } from 'livekit-client';
import { useState, useEffect, useRef, useCallback } from 'react'; import { useState, useEffect, useRef, useCallback } from 'react';
import { ScreenSourcePicker } from '@/components/screen-source-picker'; import { ScreenSourcePicker } from '@/components/screen-source-picker';
import { logError } from '@/lib/errors';
function readConnectionFromHash(): { token: string; serverUrl: string } | null { function readConnectionFromHash(): { token: string; serverUrl: string } | null {
const params = new URLSearchParams(window.location.hash.slice(1)); const params = new URLSearchParams(window.location.hash.slice(1));
@@ -144,7 +145,7 @@ function HuddleContent() {
setIsSharing(true); setIsSharing(true);
videoTrack.onended = () => stopScreenShare(); videoTrack.onended = () => stopScreenShare();
} catch (err) { } catch (err) {
console.error('Failed to start screen share:', err); logError(err, { scope: "huddle.screenShare" });
} }
}, },
[room], [room],
+11 -6
View File
@@ -4,6 +4,8 @@
* channel subscriptions, and event dispatching. * channel subscriptions, and event dispatching.
*/ */
import { logError, reportError } from "@/lib/errors";
export type ConnectionState = export type ConnectionState =
| "disconnected" | "disconnected"
| "connecting" | "connecting"
@@ -100,8 +102,8 @@ export class PusherClient {
}; };
this.ws.onerror = (event) => { this.ws.onerror = (event) => {
console.warn("[pusher] websocket error", event); // onclose fires after onerror — reconnection is handled there.
// onclose will fire after onerror, so reconnection is handled there logError(event, { scope: "pusher.ws" });
}; };
this.ws.onmessage = (event) => { this.ws.onmessage = (event) => {
@@ -182,13 +184,15 @@ export class PusherClient {
let msg: ServerMessage; let msg: ServerMessage;
try { try {
msg = JSON.parse(data); msg = JSON.parse(data);
} catch { } catch (err) {
console.warn("[pusher] failed to parse message", data); logError(err, { scope: "pusher.parse", data });
return; return;
} }
if (msg.type === "error") { if (msg.type === "error") {
console.warn("[pusher] server error:", msg.message); logError(new Error(msg.message ?? "pusher server error"), {
scope: "pusher.server",
});
return; return;
} }
@@ -204,7 +208,8 @@ export class PusherClient {
try { try {
cb(msg); cb(msg);
} catch (err) { } 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 });
} }
} }
} }
+13 -7
View File
@@ -3,14 +3,17 @@ import { signInWithCustomToken, signOut as firebaseSignOut } from "firebase/auth
import { apiClient, ApiError } from "@/api/client"; import { apiClient, ApiError } from "@/api/client";
import type { Human } from "@/api/types"; import type { Human } from "@/api/types";
import { firebaseAuth } from "@/firebase"; import { firebaseAuth } from "@/firebase";
import { logError } from "@/lib/errors";
import { useSessionStore } from "./session-store"; import { useSessionStore } from "./session-store";
async function signInToFirebase() { async function signInToFirebase() {
try { try {
const { token } = await apiClient.getFirebaseToken(); const { token } = await apiClient.getFirebaseToken();
await signInWithCustomToken(firebaseAuth, token); await signInWithCustomToken(firebaseAuth, token);
} catch (e) { } catch (err) {
console.error("Failed to sign in to Firebase", e); // 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<AuthState>((set) => ({
const user = await apiClient.me(); const user = await apiClient.me();
await signInToFirebase(); await signInToFirebase();
set({ status: "authenticated", user }); 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(); useSessionStore.getState().clearToken();
set({ status: "unauthenticated", user: null }); set({ status: "unauthenticated", user: null });
} }
@@ -91,11 +96,12 @@ export const useAuthStore = create<AuthState>((set) => ({
set({ isSigningOut: true }); set({ isSigningOut: true });
try { try {
await apiClient.signOut(); await apiClient.signOut();
} catch { } catch (err) {
// Best-effort — sign out locally regardless // Best-effort — sign out locally regardless.
logError(err, { scope: "auth.signOut" });
} finally { } finally {
await firebaseSignOut(firebaseAuth).catch((e) => await firebaseSignOut(firebaseAuth).catch((err) =>
console.error("Firebase sign-out failed", e), logError(err, { scope: "auth.firebaseSignOut" }),
); );
useSessionStore.getState().clearToken(); useSessionStore.getState().clearToken();
set({ set({