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:
@@ -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" });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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],
|
||||||
|
|||||||
@@ -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 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
Reference in New Issue
Block a user