diff --git a/js/web/src/components/dashboard/stream-monitor.tsx b/js/web/src/components/dashboard/stream-monitor.tsx index f8852098..20cd614c 100644 --- a/js/web/src/components/dashboard/stream-monitor.tsx +++ b/js/web/src/components/dashboard/stream-monitor.tsx @@ -1,3 +1,4 @@ +import { captureError } from "@/lib/log"; import { getStreamplaceUrl } from "@/lib/streamplace-url"; import type { LivestreamStore } from "@streamplace/core"; import { Eye, EyeOff, Radio, Wifi, WifiOff } from "lucide-react"; @@ -65,7 +66,15 @@ export function StreamMonitorWidget({ {/* Video area */}
{visible && isLive ? ( - + + captureError(message, { user, source: "stream-monitor" }) + } + /> ) : (
{isLive ? ( diff --git a/js/web/src/components/stream/video-section.tsx b/js/web/src/components/stream/video-section.tsx index db49e7c8..b9d22f46 100644 --- a/js/web/src/components/stream/video-section.tsx +++ b/js/web/src/components/stream/video-section.tsx @@ -6,6 +6,7 @@ import { useShallow } from "zustand/react/shallow"; import { Player } from "../../components/player/player"; import { useFullscreen } from "../../contexts/fullscreen-context"; import type { Liveness } from "../../hooks/use-liveness-state"; +import { captureError } from "../../lib/log"; import { getStreamplaceUrl } from "../../lib/streamplace-url"; import { DanmuOverlay } from "./danmu-overlay"; import { UserOffline } from "./user-offline"; @@ -146,6 +147,7 @@ export function VideoSectionInner({ mode={mode} showDanmu={showDanmu} onShowDanmuChange={onShowDanmuChange} + onError={(message) => captureError(message, { user, mode })} /> )} diff --git a/js/web/src/lib/log.ts b/js/web/src/lib/log.ts new file mode 100644 index 00000000..3aa59869 --- /dev/null +++ b/js/web/src/lib/log.ts @@ -0,0 +1,44 @@ +// Sentry capture helper. +// +// The web app initialises @sentry/react conditionally in main.tsx (only +// when VITE_SENTRY_DSN is set). When uninitialised, captureException is +// a no-op, so we don't need to gate the call ourselves. +// +// `captureError` is the one place code that catches a known error should +// route through when it wants the error to be reported. It builds a +// real Error so Sentry can group by stack, but preserves the original +// message and a caller-supplied context bag. +// +// `log.warn` / `log.error` are for one-off developer logs that aren't +// necessarily errors. They go to console only — use `captureError` if +// the message should reach Sentry. +import * as Sentry from "@sentry/react"; + +export function captureError( + message: string, + context?: Record, +): void { + if (import.meta.env.DEV) { + // Don't spam Sentry with dev noise — console is enough. + console.error(message, context); + return; + } + const err = new Error(message); + if (context) { + Sentry.withScope((scope) => { + scope.setExtras(context); + Sentry.captureException(err); + }); + } else { + Sentry.captureException(err); + } +} + +export const log = { + warn: (message: string, ...rest: unknown[]) => { + console.warn(message, ...rest); + }, + error: (message: string, ...rest: unknown[]) => { + console.error(message, ...rest); + }, +}; diff --git a/js/web/src/routes/embed/$user/index.tsx b/js/web/src/routes/embed/$user/index.tsx index c6ba4a72..d9e51eeb 100644 --- a/js/web/src/routes/embed/$user/index.tsx +++ b/js/web/src/routes/embed/$user/index.tsx @@ -1,4 +1,5 @@ import { Player } from "@/components/player/player"; +import { captureError } from "@/lib/log"; import { getStreamplaceUrl } from "@/lib/streamplace-url"; import { createFileRoute } from "@tanstack/react-router"; import { useMemo } from "react"; @@ -20,7 +21,15 @@ function EmbedLive() { return (
- + + captureError(message, { user, source: "embed-live" }) + } + />
); } diff --git a/js/web/src/routes/embed/$user/video/$tid.tsx b/js/web/src/routes/embed/$user/video/$tid.tsx index 1e537259..e65ded00 100644 --- a/js/web/src/routes/embed/$user/video/$tid.tsx +++ b/js/web/src/routes/embed/$user/video/$tid.tsx @@ -1,4 +1,5 @@ import { Player } from "@/components/player/player"; +import { captureError } from "@/lib/log"; import { getStreamplaceUrl } from "@/lib/streamplace-url"; import { createFileRoute } from "@tanstack/react-router"; import { useMemo } from "react"; @@ -21,7 +22,15 @@ function EmbedVideo() { return (
- + + captureError(message, { user, tid, source: "embed-vod" }) + } + />
); }