From 11bd568a6b1abd2f256d58096249028764f1b081 Mon Sep 17 00:00:00 2001 From: Koper Date: Thu, 31 Aug 2023 19:11:22 +0700 Subject: [PATCH 1/4] More robust error handling & Display errors to user --- www/app/(errors)/errorContext.tsx | 31 +++++++++++++++ www/app/(errors)/errorMessage.tsx | 29 ++++++++++++++ www/app/layout.tsx | 27 ++++++++----- www/app/transcripts/new/page.tsx | 1 + www/app/transcripts/useTranscript.ts | 7 ++-- www/app/transcripts/useWebRTC.ts | 28 ++++++++++++-- www/app/transcripts/useWebSockets.ts | 57 +++++++++++++++++----------- 7 files changed, 139 insertions(+), 41 deletions(-) create mode 100644 www/app/(errors)/errorContext.tsx create mode 100644 www/app/(errors)/errorMessage.tsx diff --git a/www/app/(errors)/errorContext.tsx b/www/app/(errors)/errorContext.tsx new file mode 100644 index 00000000..31c5c505 --- /dev/null +++ b/www/app/(errors)/errorContext.tsx @@ -0,0 +1,31 @@ +"use client"; +import React, { createContext, useContext, useState } from "react"; + +interface ErrorContextProps { + error: string; + setError: React.Dispatch>; +} + +const ErrorContext = createContext(undefined); + +export const useError = () => { + const context = useContext(ErrorContext); + if (!context) { + throw new Error("useError must be used within an ErrorProvider"); + } + return context; +}; + +interface ErrorProviderProps { + children: React.ReactNode; +} + +export const ErrorProvider: React.FC = ({ children }) => { + const [error, setError] = useState(""); + + return ( + + {children} + + ); +}; diff --git a/www/app/(errors)/errorMessage.tsx b/www/app/(errors)/errorMessage.tsx new file mode 100644 index 00000000..874a5b8f --- /dev/null +++ b/www/app/(errors)/errorMessage.tsx @@ -0,0 +1,29 @@ +"use client"; +import { useError } from "./errorContext"; +import { useEffect, useState } from "react"; + +const ErrorMessage: React.FC = () => { + const { error, setError } = useError(); + const [isVisible, setIsVisible] = useState(false); + + useEffect(() => { + if (error) setIsVisible(true); + }, [error]); + + if (!isVisible || !error) return null; + + return ( +
{ + setIsVisible(false); + setError(""); + }} + className="max-w-xs z-50 fixed top-16 right-10 bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded transition-opacity duration-300 ease-out opacity-100 hover:opacity-75 cursor-pointer transform hover:scale-105" + role="alert" + > + {error} +
+ ); +}; + +export default ErrorMessage; diff --git a/www/app/layout.tsx b/www/app/layout.tsx index fc7913f5..c15974a2 100644 --- a/www/app/layout.tsx +++ b/www/app/layout.tsx @@ -3,6 +3,8 @@ import { Roboto } from "next/font/google"; import { Metadata } from "next"; import FiefWrapper from "./(auth)/fiefWrapper"; import UserInfo from "./(auth)/userInfo"; +import { ErrorProvider } from "./(errors)/errorContext"; +import ErrorMessage from "./(errors)/errorMessage"; const roboto = Roboto({ subsets: ["latin"], weight: "400" }); @@ -55,19 +57,24 @@ export default function RootLayout({ children }) { -
-
- + + +
+
+ -
-

Reflector

-

- Capture The Signal, Not The Noise -

+
+

+ Reflector +

+

+ Capture The Signal, Not The Noise +

+
+ {children}
- {children}
-
+
diff --git a/www/app/transcripts/new/page.tsx b/www/app/transcripts/new/page.tsx index 85e7deb8..dbf5c653 100644 --- a/www/app/transcripts/new/page.tsx +++ b/www/app/transcripts/new/page.tsx @@ -9,6 +9,7 @@ import useAudioDevice from "../useAudioDevice"; import "../../styles/button.css"; import { Topic } from "../webSocketTypes"; import getApi from "../../lib/getApi"; +import { useError } from "../../(errors)/errorContext"; const App = () => { const [stream, setStream] = useState(null); diff --git a/www/app/transcripts/useTranscript.ts b/www/app/transcripts/useTranscript.ts index 07b614f4..8b50c889 100644 --- a/www/app/transcripts/useTranscript.ts +++ b/www/app/transcripts/useTranscript.ts @@ -1,19 +1,18 @@ import { useEffect, useState } from "react"; import { DefaultApi, V1TranscriptsCreateRequest } from "../api/apis/DefaultApi"; import { GetTranscript } from "../api"; -import getApi from "../lib/getApi"; +import { useError } from "../(errors)/errorContext"; type UseTranscript = { response: GetTranscript | null; loading: boolean; - error: string | null; createTranscript: () => void; }; const useTranscript = (api: DefaultApi): UseTranscript => { const [response, setResponse] = useState(null); const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); + const { setError } = useError(); const createTranscript = () => { setLoading(true); @@ -48,7 +47,7 @@ const useTranscript = (api: DefaultApi): UseTranscript => { createTranscript(); }, []); - return { response, loading, error, createTranscript }; + return { response, loading, createTranscript }; }; export default useTranscript; diff --git a/www/app/transcripts/useWebRTC.ts b/www/app/transcripts/useWebRTC.ts index 62694de8..387c5ac4 100644 --- a/www/app/transcripts/useWebRTC.ts +++ b/www/app/transcripts/useWebRTC.ts @@ -4,7 +4,7 @@ import { DefaultApi, V1TranscriptRecordWebrtcRequest, } from "../api/apis/DefaultApi"; -import { Configuration } from "../api/runtime"; +import { useError } from "../(errors)/errorContext"; const useWebRTC = ( stream: MediaStream | null, @@ -12,13 +12,25 @@ const useWebRTC = ( api: DefaultApi, ): Peer => { const [peer, setPeer] = useState(null); + const { setError } = useError(); useEffect(() => { if (!stream || !transcriptId) { return; } - let p: Peer = new Peer({ initiator: true, stream: stream }); + let p: Peer; + + try { + p = new Peer({ initiator: true, stream: stream }); + } catch (error) { + setError(`Failed to create WebRTC Peer: ${error.message}`); + return; + } + + p.on("error", (err) => { + setError(`WebRTC error: ${err.message}`); + }); p.on("signal", (data: any) => { if ("sdp" in data) { @@ -33,10 +45,18 @@ const useWebRTC = ( api .v1TranscriptRecordWebrtc(requestParameters) .then((answer) => { - p.signal(answer); + try { + p.signal(answer); + } catch (error) { + setError(`Failed to signal answer: ${error.message}`); + } }) .catch((err) => { - console.error("WebRTC signaling error:", err); + const errorString = + "WebRTC signaling error: " + + (err.response || err.message || "Unknown error"); + setError(errorString); + console.error(errorString); }); } }); diff --git a/www/app/transcripts/useWebSockets.ts b/www/app/transcripts/useWebSockets.ts index 726aed22..ce8b2ea3 100644 --- a/www/app/transcripts/useWebSockets.ts +++ b/www/app/transcripts/useWebSockets.ts @@ -1,5 +1,6 @@ import { useEffect, useState } from "react"; import { Topic, FinalSummary, Status } from "./webSocketTypes"; +import { useError } from "../(errors)/errorContext"; type UseWebSockets = { transcriptText: string; @@ -15,6 +16,7 @@ export const useWebSockets = (transcriptId: string | null): UseWebSockets => { summary: "", }); const [status, setStatus] = useState({ value: "disconnected" }); + const { setError } = useError(); useEffect(() => { document.onkeyup = (e) => { @@ -77,41 +79,50 @@ export const useWebSockets = (transcriptId: string | null): UseWebSockets => { ws.onmessage = (event) => { const message = JSON.parse(event.data); - switch (message.event) { - case "TRANSCRIPT": - if (message.data.text) { - setTranscriptText((message.data.text ?? "").trim()); - console.debug("TRANSCRIPT event:", message.data); - } - break; + try { + switch (message.event) { + case "TRANSCRIPT": + if (message.data.text) { + setTranscriptText((message.data.text ?? "").trim()); + console.debug("TRANSCRIPT event:", message.data); + } + break; - case "TOPIC": - setTopics((prevTopics) => [...prevTopics, message.data]); - console.debug("TOPIC event:", message.data); - break; + case "TOPIC": + setTopics((prevTopics) => [...prevTopics, message.data]); + console.debug("TOPIC event:", message.data); + break; - case "FINAL_SUMMARY": - if (message.data) { - setFinalSummary(message.data); - console.debug("FINAL_SUMMARY event:", message.data); - } - break; + case "FINAL_SUMMARY": + if (message.data) { + setFinalSummary(message.data); + console.debug("FINAL_SUMMARY event:", message.data); + } + break; - case "STATUS": - setStatus(message.data); - break; + case "STATUS": + setStatus(message.data); + break; - default: - console.error("Unknown event:", message.event); + default: + console.error("Unknown event:", message.event); + setError(`Received unknown WebSocket event: ${message.event}`); + } + } catch (error) { + setError(`Failed to process WebSocket message: ${error.message}`); } }; ws.onerror = (error) => { console.error("WebSocket error:", error); + setError("A WebSocket error occurred."); }; - ws.onclose = () => { + ws.onclose = (event) => { console.debug("WebSocket connection closed"); + if (event.code !== 1000) { + setError(`WebSocket closed unexpectedly with code: ${event.code}`); + } }; return () => { From 7662d54c1458c0dde10826ed496d22d122c57835 Mon Sep 17 00:00:00 2001 From: Koper Date: Thu, 31 Aug 2023 20:02:04 +0700 Subject: [PATCH 2/4] Added sentry logging --- www/app/(errors)/handleError.ts | 17 +++++++++++++++++ www/app/transcripts/useTranscript.ts | 3 ++- www/app/transcripts/useWebRTC.ts | 17 +++++++++++++---- www/app/transcripts/useWebSockets.ts | 19 +++++++++++++++---- 4 files changed, 47 insertions(+), 9 deletions(-) create mode 100644 www/app/(errors)/handleError.ts diff --git a/www/app/(errors)/handleError.ts b/www/app/(errors)/handleError.ts new file mode 100644 index 00000000..09226dda --- /dev/null +++ b/www/app/(errors)/handleError.ts @@ -0,0 +1,17 @@ +import * as Sentry from "@sentry/react"; + +const handleError = ( + setError: Function, + errorString: string, + errorObj?: any, +) => { + setError(errorString); + + if (errorObj) { + Sentry.captureException(errorObj); + } else { + Sentry.captureMessage(errorString); + } +}; + +export default handleError; diff --git a/www/app/transcripts/useTranscript.ts b/www/app/transcripts/useTranscript.ts index 8b50c889..cc0d4bde 100644 --- a/www/app/transcripts/useTranscript.ts +++ b/www/app/transcripts/useTranscript.ts @@ -2,6 +2,7 @@ import { useEffect, useState } from "react"; import { DefaultApi, V1TranscriptsCreateRequest } from "../api/apis/DefaultApi"; import { GetTranscript } from "../api"; import { useError } from "../(errors)/errorContext"; +import handleError from "../(errors)/handleError"; type UseTranscript = { response: GetTranscript | null; @@ -37,7 +38,7 @@ const useTranscript = (api: DefaultApi): UseTranscript => { }) .catch((err) => { const errorString = err.response || err.message || "Unknown error"; - setError(errorString); + handleError(setError, errorString, err); setLoading(false); console.error("Error creating transcript:", errorString); }); diff --git a/www/app/transcripts/useWebRTC.ts b/www/app/transcripts/useWebRTC.ts index 387c5ac4..c5b55315 100644 --- a/www/app/transcripts/useWebRTC.ts +++ b/www/app/transcripts/useWebRTC.ts @@ -5,6 +5,7 @@ import { V1TranscriptRecordWebrtcRequest, } from "../api/apis/DefaultApi"; import { useError } from "../(errors)/errorContext"; +import handleError from "../(errors)/handleError"; const useWebRTC = ( stream: MediaStream | null, @@ -24,12 +25,16 @@ const useWebRTC = ( try { p = new Peer({ initiator: true, stream: stream }); } catch (error) { - setError(`Failed to create WebRTC Peer: ${error.message}`); + handleError( + setError, + `Failed to create WebRTC Peer: ${error.message}`, + error, + ); return; } p.on("error", (err) => { - setError(`WebRTC error: ${err.message}`); + handleError(setError, `WebRTC error: ${err.message}`, err); }); p.on("signal", (data: any) => { @@ -48,14 +53,18 @@ const useWebRTC = ( try { p.signal(answer); } catch (error) { - setError(`Failed to signal answer: ${error.message}`); + handleError( + setError, + `Failed to signal answer: ${error.message}`, + error, + ); } }) .catch((err) => { const errorString = "WebRTC signaling error: " + (err.response || err.message || "Unknown error"); - setError(errorString); + handleError(setError, errorString, err); console.error(errorString); }); } diff --git a/www/app/transcripts/useWebSockets.ts b/www/app/transcripts/useWebSockets.ts index ce8b2ea3..f058f91f 100644 --- a/www/app/transcripts/useWebSockets.ts +++ b/www/app/transcripts/useWebSockets.ts @@ -1,6 +1,7 @@ import { useEffect, useState } from "react"; import { Topic, FinalSummary, Status } from "./webSocketTypes"; import { useError } from "../(errors)/errorContext"; +import handleError from "../(errors)/handleError"; type UseWebSockets = { transcriptText: string; @@ -106,22 +107,32 @@ export const useWebSockets = (transcriptId: string | null): UseWebSockets => { default: console.error("Unknown event:", message.event); - setError(`Received unknown WebSocket event: ${message.event}`); + handleError( + useError, + `Received unknown WebSocket event: ${message.event}`, + ); } } catch (error) { - setError(`Failed to process WebSocket message: ${error.message}`); + handleError( + useError, + `Failed to process WebSocket message: ${error.message}`, + error, + ); } }; ws.onerror = (error) => { console.error("WebSocket error:", error); - setError("A WebSocket error occurred."); + handleError(useError, "A WebSocket error occurred.", error); }; ws.onclose = (event) => { console.debug("WebSocket connection closed"); if (event.code !== 1000) { - setError(`WebSocket closed unexpectedly with code: ${event.code}`); + handleError( + useError, + `WebSocket closed unexpectedly with code: ${event.code}`, + ); } }; From df4dc841fca04e353f716a5ce3e3d5e40aa561e6 Mon Sep 17 00:00:00 2001 From: Koper Date: Thu, 31 Aug 2023 22:07:58 +0700 Subject: [PATCH 3/4] Fixed setError/useError typos & added stronger typing --- www/app/(errors)/handleError.ts | 3 ++- www/app/transcripts/new/page.tsx | 1 - www/app/transcripts/useWebSockets.ts | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/www/app/(errors)/handleError.ts b/www/app/(errors)/handleError.ts index 09226dda..1b9f64e3 100644 --- a/www/app/(errors)/handleError.ts +++ b/www/app/(errors)/handleError.ts @@ -1,7 +1,8 @@ import * as Sentry from "@sentry/react"; +import { Dispatch, SetStateAction } from "react"; const handleError = ( - setError: Function, + setError: Dispatch>, errorString: string, errorObj?: any, ) => { diff --git a/www/app/transcripts/new/page.tsx b/www/app/transcripts/new/page.tsx index dbf5c653..85e7deb8 100644 --- a/www/app/transcripts/new/page.tsx +++ b/www/app/transcripts/new/page.tsx @@ -9,7 +9,6 @@ import useAudioDevice from "../useAudioDevice"; import "../../styles/button.css"; import { Topic } from "../webSocketTypes"; import getApi from "../../lib/getApi"; -import { useError } from "../../(errors)/errorContext"; const App = () => { const [stream, setStream] = useState(null); diff --git a/www/app/transcripts/useWebSockets.ts b/www/app/transcripts/useWebSockets.ts index f058f91f..4e0cc4b3 100644 --- a/www/app/transcripts/useWebSockets.ts +++ b/www/app/transcripts/useWebSockets.ts @@ -108,13 +108,13 @@ export const useWebSockets = (transcriptId: string | null): UseWebSockets => { default: console.error("Unknown event:", message.event); handleError( - useError, + setError, `Received unknown WebSocket event: ${message.event}`, ); } } catch (error) { handleError( - useError, + setError, `Failed to process WebSocket message: ${error.message}`, error, ); @@ -123,14 +123,14 @@ export const useWebSockets = (transcriptId: string | null): UseWebSockets => { ws.onerror = (error) => { console.error("WebSocket error:", error); - handleError(useError, "A WebSocket error occurred.", error); + handleError(setError, "A WebSocket error occurred.", error); }; ws.onclose = (event) => { console.debug("WebSocket connection closed"); if (event.code !== 1000) { handleError( - useError, + setError, `WebSocket closed unexpectedly with code: ${event.code}`, ); } From 41ca80358c67cd2721054383dffd5236f1cd2b33 Mon Sep 17 00:00:00 2001 From: Koper Date: Fri, 1 Sep 2023 12:36:13 +0700 Subject: [PATCH 4/4] Refactoring to use Error instead of string in the useError hook state variable --- www/app/(errors)/errorContext.tsx | 6 +++--- www/app/(errors)/errorMessage.tsx | 11 ++++++++--- www/app/(errors)/handleError.ts | 18 ------------------ www/app/transcripts/useTranscript.ts | 6 +----- www/app/transcripts/useWebRTC.ts | 23 +++++------------------ www/app/transcripts/useWebSockets.ts | 20 ++++++-------------- 6 files changed, 23 insertions(+), 61 deletions(-) delete mode 100644 www/app/(errors)/handleError.ts diff --git a/www/app/(errors)/errorContext.tsx b/www/app/(errors)/errorContext.tsx index 31c5c505..d8a80c04 100644 --- a/www/app/(errors)/errorContext.tsx +++ b/www/app/(errors)/errorContext.tsx @@ -2,8 +2,8 @@ import React, { createContext, useContext, useState } from "react"; interface ErrorContextProps { - error: string; - setError: React.Dispatch>; + error: Error | null; + setError: React.Dispatch>; } const ErrorContext = createContext(undefined); @@ -21,7 +21,7 @@ interface ErrorProviderProps { } export const ErrorProvider: React.FC = ({ children }) => { - const [error, setError] = useState(""); + const [error, setError] = useState(null); return ( diff --git a/www/app/(errors)/errorMessage.tsx b/www/app/(errors)/errorMessage.tsx index 874a5b8f..d5109733 100644 --- a/www/app/(errors)/errorMessage.tsx +++ b/www/app/(errors)/errorMessage.tsx @@ -1,13 +1,18 @@ "use client"; import { useError } from "./errorContext"; import { useEffect, useState } from "react"; +import * as Sentry from "@sentry/react"; const ErrorMessage: React.FC = () => { const { error, setError } = useError(); const [isVisible, setIsVisible] = useState(false); useEffect(() => { - if (error) setIsVisible(true); + if (error) { + setIsVisible(true); + Sentry.captureException(error); + console.error("Error", error.message, error); + } }, [error]); if (!isVisible || !error) return null; @@ -16,12 +21,12 @@ const ErrorMessage: React.FC = () => {
{ setIsVisible(false); - setError(""); + setError(null); }} className="max-w-xs z-50 fixed top-16 right-10 bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded transition-opacity duration-300 ease-out opacity-100 hover:opacity-75 cursor-pointer transform hover:scale-105" role="alert" > - {error} + {error?.message}
); }; diff --git a/www/app/(errors)/handleError.ts b/www/app/(errors)/handleError.ts deleted file mode 100644 index 1b9f64e3..00000000 --- a/www/app/(errors)/handleError.ts +++ /dev/null @@ -1,18 +0,0 @@ -import * as Sentry from "@sentry/react"; -import { Dispatch, SetStateAction } from "react"; - -const handleError = ( - setError: Dispatch>, - errorString: string, - errorObj?: any, -) => { - setError(errorString); - - if (errorObj) { - Sentry.captureException(errorObj); - } else { - Sentry.captureMessage(errorString); - } -}; - -export default handleError; diff --git a/www/app/transcripts/useTranscript.ts b/www/app/transcripts/useTranscript.ts index cc0d4bde..f2188f62 100644 --- a/www/app/transcripts/useTranscript.ts +++ b/www/app/transcripts/useTranscript.ts @@ -2,7 +2,6 @@ import { useEffect, useState } from "react"; import { DefaultApi, V1TranscriptsCreateRequest } from "../api/apis/DefaultApi"; import { GetTranscript } from "../api"; import { useError } from "../(errors)/errorContext"; -import handleError from "../(errors)/handleError"; type UseTranscript = { response: GetTranscript | null; @@ -37,10 +36,7 @@ const useTranscript = (api: DefaultApi): UseTranscript => { console.debug("New transcript created:", result); }) .catch((err) => { - const errorString = err.response || err.message || "Unknown error"; - handleError(setError, errorString, err); - setLoading(false); - console.error("Error creating transcript:", errorString); + setError(err); }); }; diff --git a/www/app/transcripts/useWebRTC.ts b/www/app/transcripts/useWebRTC.ts index c5b55315..b99382ab 100644 --- a/www/app/transcripts/useWebRTC.ts +++ b/www/app/transcripts/useWebRTC.ts @@ -5,7 +5,6 @@ import { V1TranscriptRecordWebrtcRequest, } from "../api/apis/DefaultApi"; import { useError } from "../(errors)/errorContext"; -import handleError from "../(errors)/handleError"; const useWebRTC = ( stream: MediaStream | null, @@ -25,16 +24,12 @@ const useWebRTC = ( try { p = new Peer({ initiator: true, stream: stream }); } catch (error) { - handleError( - setError, - `Failed to create WebRTC Peer: ${error.message}`, - error, - ); + setError(error); return; } p.on("error", (err) => { - handleError(setError, `WebRTC error: ${err.message}`, err); + setError(new Error(`WebRTC error: ${err}`)); }); p.on("signal", (data: any) => { @@ -53,19 +48,11 @@ const useWebRTC = ( try { p.signal(answer); } catch (error) { - handleError( - setError, - `Failed to signal answer: ${error.message}`, - error, - ); + setError(error); } }) - .catch((err) => { - const errorString = - "WebRTC signaling error: " + - (err.response || err.message || "Unknown error"); - handleError(setError, errorString, err); - console.error(errorString); + .catch((error) => { + setError(error); }); } }); diff --git a/www/app/transcripts/useWebSockets.ts b/www/app/transcripts/useWebSockets.ts index 4e0cc4b3..7bde6d9b 100644 --- a/www/app/transcripts/useWebSockets.ts +++ b/www/app/transcripts/useWebSockets.ts @@ -1,7 +1,6 @@ import { useEffect, useState } from "react"; import { Topic, FinalSummary, Status } from "./webSocketTypes"; import { useError } from "../(errors)/errorContext"; -import handleError from "../(errors)/handleError"; type UseWebSockets = { transcriptText: string; @@ -106,32 +105,25 @@ export const useWebSockets = (transcriptId: string | null): UseWebSockets => { break; default: - console.error("Unknown event:", message.event); - handleError( - setError, - `Received unknown WebSocket event: ${message.event}`, + setError( + new Error(`Received unknown WebSocket event: ${message.event}`), ); } } catch (error) { - handleError( - setError, - `Failed to process WebSocket message: ${error.message}`, - error, - ); + setError(error); } }; ws.onerror = (error) => { console.error("WebSocket error:", error); - handleError(setError, "A WebSocket error occurred.", error); + setError(new Error("A WebSocket error occurred.")); }; ws.onclose = (event) => { console.debug("WebSocket connection closed"); if (event.code !== 1000) { - handleError( - setError, - `WebSocket closed unexpectedly with code: ${event.code}`, + setError( + new Error(`WebSocket closed unexpectedly with code: ${event.code}`), ); } };