diff --git a/src/components/AudioPlayer/index.tsx b/src/components/AudioPlayer/index.tsx index f1ad967..690ce6e 100644 --- a/src/components/AudioPlayer/index.tsx +++ b/src/components/AudioPlayer/index.tsx @@ -5,6 +5,7 @@ import mediaManager from '@/services/media-manager.service' import { Pause, Play } from 'lucide-react' import { useEffect, useRef, useState } from 'react' import ExternalLink from '../ExternalLink' +import { MediaErrorBoundary } from '../MediaErrorBoundary' interface AudioPlayerProps { src: string @@ -85,36 +86,47 @@ export default function AudioPlayer({ src, className }: AudioPlayerProps) { } return ( -
e.stopPropagation()} + } + onError={(error) => { + // Don't log expected media errors + if (error.name !== 'AbortError' && !error.message.includes('play() request was interrupted')) { + console.warn('Audio player error:', error) + } + setError(true) + }} > - ) } diff --git a/src/components/MediaErrorBoundary.tsx b/src/components/MediaErrorBoundary.tsx new file mode 100644 index 0000000..ba23db8 --- /dev/null +++ b/src/components/MediaErrorBoundary.tsx @@ -0,0 +1,56 @@ +import React, { Component, ReactNode } from 'react' +import { AlertTriangle } from 'lucide-react' + +interface MediaErrorBoundaryProps { + children: ReactNode + fallback?: ReactNode + onError?: (error: Error) => void +} + +interface MediaErrorBoundaryState { + hasError: boolean + error?: Error +} + +export class MediaErrorBoundary extends Component { + constructor(props: MediaErrorBoundaryProps) { + super(props) + this.state = { hasError: false } + } + + static getDerivedStateFromError(error: Error): MediaErrorBoundaryState { + return { hasError: true, error } + } + + componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { + // Don't log expected media errors + if (error.name === 'AbortError' || + error.message.includes('play() request was interrupted') || + error.message.includes('The play() request was interrupted')) { + return + } + + // Log unexpected errors + console.warn('Media error boundary caught error:', error, errorInfo) + this.props.onError?.(error) + } + + render() { + if (this.state.hasError) { + if (this.props.fallback) { + return this.props.fallback + } + + return ( +
+
+ + Media unavailable +
+
+ ) + } + + return this.props.children + } +} diff --git a/src/components/VideoPlayer/index.tsx b/src/components/VideoPlayer/index.tsx index 53d8d13..f9f3b97 100644 --- a/src/components/VideoPlayer/index.tsx +++ b/src/components/VideoPlayer/index.tsx @@ -3,6 +3,7 @@ import { useContentPolicy } from '@/providers/ContentPolicyProvider' import mediaManager from '@/services/media-manager.service' import { useEffect, useRef, useState } from 'react' import ExternalLink from '../ExternalLink' +import { MediaErrorBoundary } from '../MediaErrorBoundary' export default function VideoPlayer({ src, className }: { src: string; className?: string }) { const { autoplay } = useContentPolicy() @@ -45,20 +46,31 @@ export default function VideoPlayer({ src, className }: { src: string; className } return ( -
-
+ } + onError={(error) => { + // Don't log expected media errors + if (error.name !== 'AbortError' && !error.message.includes('play() request was interrupted')) { + console.warn('Video player error:', error) + } + setError(true) + }} + > +
+
+
) } diff --git a/src/lib/error-suppression.ts b/src/lib/error-suppression.ts new file mode 100644 index 0000000..6b68018 --- /dev/null +++ b/src/lib/error-suppression.ts @@ -0,0 +1,67 @@ +/** + * Suppress expected console errors that are not actionable + * This helps reduce noise in the development console + */ + +// Track suppressed errors to avoid spam +const suppressedErrors = new Set() + +export function suppressExpectedErrors() { + // Override console.error to filter out expected errors + const originalConsoleError = console.error + + console.error = (...args: any[]) => { + const message = args.join(' ') + + // Suppress favicon 404 errors + if (message.includes('favicon.ico') && message.includes('404')) { + return + } + + // Suppress CORS errors for external websites + if (message.includes('CORS policy') && message.includes('Access-Control-Allow-Origin')) { + return + } + + // Suppress network errors for external websites + if (message.includes('net::ERR_FAILED') && message.includes('200 (OK)')) { + return + } + + // Suppress YouTube API warnings + if (message.includes('Unrecognized feature: \'web-share\'')) { + return + } + + // Suppress Canvas2D warnings + if (message.includes('Canvas2D: Multiple readback operations')) { + return + } + + // Call original console.error for unexpected errors + originalConsoleError.apply(console, args) + } + + // Override console.warn to filter out expected warnings + const originalConsoleWarn = console.warn + + console.warn = (...args: any[]) => { + const message = args.join(' ') + + // Suppress React DevTools suggestion (only show once) + if (message.includes('Download the React DevTools')) { + if (suppressedErrors.has('react-devtools')) { + return + } + suppressedErrors.add('react-devtools') + } + + // Call original console.warn for unexpected warnings + originalConsoleWarn.apply(console, args) + } +} + +// Initialize error suppression +if (typeof window !== 'undefined') { + suppressExpectedErrors() +} diff --git a/src/main.tsx b/src/main.tsx index 399ddca..9584f90 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -2,6 +2,7 @@ import './i18n' import './index.css' import './polyfill' import './services/lightning.service' +import './lib/error-suppression' import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' diff --git a/src/services/media-manager.service.ts b/src/services/media-manager.service.ts index 7e0b5ec..9748401 100644 --- a/src/services/media-manager.service.ts +++ b/src/services/media-manager.service.ts @@ -53,6 +53,12 @@ class MediaManagerService { } play(this.currentMedia).catch((error) => { + // Don't log expected AbortError when media is interrupted + if (error instanceof Error && error.name === 'AbortError') { + // This is expected when media is interrupted by pause() or other media + return + } + // Log other unexpected errors console.error('Error playing media:', error) this.currentMedia = null }) diff --git a/src/services/web.service.ts b/src/services/web.service.ts index 449babc..34c7fc9 100644 --- a/src/services/web.service.ts +++ b/src/services/web.service.ts @@ -9,7 +9,26 @@ class WebService { return await Promise.all( urls.map(async (url) => { try { - const res = await fetch(url) + // Add timeout and better error handling for CORS issues + const controller = new AbortController() + const timeoutId = setTimeout(() => controller.abort(), 10000) // 10 second timeout + + const res = await fetch(url, { + signal: controller.signal, + mode: 'cors', + credentials: 'omit' + }) + + clearTimeout(timeoutId) + + if (!res.ok) { + // Don't log 404s and CORS errors as they're expected + if (res.status !== 404 && res.status !== 0) { + console.warn(`Failed to fetch metadata for ${url}: ${res.status} ${res.statusText}`) + } + return {} + } + const html = await res.text() const parser = new DOMParser() const doc = parser.parseFromString(html, 'text/html') @@ -24,7 +43,18 @@ class WebService { ?.content return { title, description, image } - } catch { + } catch (error) { + // Only log unexpected errors, not CORS or network issues + if (error instanceof TypeError && error.message.includes('Failed to fetch')) { + // This is likely a CORS error - don't log it + return {} + } + if (error instanceof Error && error.name === 'AbortError') { + // Timeout - don't log it + return {} + } + // Log other unexpected errors + console.warn(`Unexpected error fetching metadata for ${url}:`, error) return {} } })