Forráskód Böngészése

refactor error handling (#1183)

* refactor error handling

* improve storage providers fetch error handling

* add user info to log events

* add header margin
Klaudiusz Dembler 3 éve
szülő
commit
f396a02fd1
50 módosított fájl, 462 hozzáadás és 525 törlés
  1. 0 1
      package.json
  2. 19 11
      src/App.tsx
  3. 8 15
      src/MainLayout.tsx
  4. 1 1
      src/components/ChannelLink/ChannelLink.tsx
  5. 5 3
      src/components/Dialogs/ImageCropDialog/ImageCropDialog.tsx
  6. 1 1
      src/components/Dialogs/ImageCropDialog/cropper.ts
  7. 0 31
      src/components/ErrorFallback.tsx
  8. 6 4
      src/components/InfiniteGrids/InfiniteChannelGrid.tsx
  9. 6 4
      src/components/InfiniteGrids/InfiniteVideoGrid.tsx
  10. 18 3
      src/components/InfiniteGrids/useInfiniteGrid.ts
  11. 10 6
      src/components/VideoHero/VideoHeroData.ts
  12. 1 1
      src/components/VideoTile.tsx
  13. 57 24
      src/components/ViewErrorFallback.tsx
  14. 1 3
      src/components/index.ts
  15. 1 6
      src/index.tsx
  16. 7 21
      src/joystream-lib/api.ts
  17. 16 2
      src/providers/assets/assetsManager.tsx
  18. 5 1
      src/providers/editVideoSheet/hooks.tsx
  19. 1 1
      src/providers/joystream/provider.tsx
  20. 17 19
      src/providers/storageProviders.tsx
  21. 1 1
      src/providers/transactionManager/transactionManager.tsx
  22. 14 7
      src/providers/transactionManager/useTransaction.ts
  23. 11 4
      src/providers/uploadsManager/useStartFileUpload.tsx
  24. 23 11
      src/providers/user/user.tsx
  25. 7 5
      src/shared/components/VideoPlayer/VideoPlayer.tsx
  26. 0 47
      src/shared/illustrations/EmptyVideosIllustration.tsx
  27. 0 8
      src/shared/illustrations/TheaterMaskIllustration.tsx
  28. 0 15
      src/shared/illustrations/WellErrorIllustration.tsx
  29. 0 3
      src/shared/illustrations/index.tsx
  30. 0 13
      src/shared/illustrations/svgs/empty-videos-illustration.svg
  31. 0 65
      src/shared/illustrations/svgs/theater-mask-illustration.svg
  32. 0 39
      src/shared/illustrations/svgs/well-error-illustration.svg
  33. 1 1
      src/store/index.ts
  34. 1 2
      src/utils/image.ts
  35. 3 3
      src/utils/localStorage.ts
  36. 103 16
      src/utils/logger.ts
  37. 1 1
      src/views/admin/AdminView.tsx
  38. 9 2
      src/views/studio/CreateEditChannelView/CreateEditChannelView.tsx
  39. 11 4
      src/views/studio/CreateMemberView/CreateMemberView.tsx
  40. 11 10
      src/views/studio/EditVideoSheet/EditVideoForm/EditVideoForm.tsx
  41. 1 1
      src/views/studio/EditVideoSheet/EditVideoSheet.tsx
  42. 7 3
      src/views/studio/MyVideosView/MyVideosView.tsx
  43. 2 5
      src/views/studio/StudioLayout.tsx
  44. 29 15
      src/views/viewer/ChannelView/ChannelView.tsx
  45. 11 12
      src/views/viewer/HomeView.tsx
  46. 14 13
      src/views/viewer/SearchOverlayView/SearchResults/SearchResults.tsx
  47. 6 4
      src/views/viewer/VideoView/VideoView.tsx
  48. 14 26
      src/views/viewer/VideosView/VideosView.tsx
  49. 2 2
      src/views/viewer/ViewerLayout.tsx
  50. 0 29
      yarn.lock

+ 0 - 1
package.json

@@ -52,7 +52,6 @@
     "@joystream/types": "~0.16.1",
     "@loadable/component": "^5.14.1",
     "@polkadot/extension-dapp": "~0.37.3-17",
-    "@sentry/integrations": "^6.11.0",
     "@sentry/react": "^6.11.0",
     "@tippyjs/react": "^4.2.5",
     "apollo": "^2.30.2",

+ 19 - 11
src/App.tsx

@@ -1,7 +1,10 @@
 import { ApolloProvider } from '@apollo/client'
 import React from 'react'
+import { BrowserRouter } from 'react-router-dom'
 
 import { createApolloClient } from '@/api'
+import { GlobalStyle } from '@/shared/components'
+import { routingTransitions } from '@/styles/routingTransitions'
 
 import { MainLayout } from './MainLayout'
 import { AssetsManager, DialogProvider, OverlayManagerProvider, Snackbars, StorageProvidersProvider } from './providers'
@@ -12,16 +15,21 @@ export const App = () => {
   const apolloClient = createApolloClient()
 
   return (
-    <ApolloProvider client={apolloClient}>
-      <OverlayManagerProvider>
-        <StorageProvidersProvider>
-          <DialogProvider>
-            <MainLayout />
-            <Snackbars />
-            <AssetsManager />
-          </DialogProvider>
-        </StorageProvidersProvider>
-      </OverlayManagerProvider>
-    </ApolloProvider>
+    <>
+      <GlobalStyle additionalStyles={[routingTransitions]} />
+      <ApolloProvider client={apolloClient}>
+        <BrowserRouter>
+          <OverlayManagerProvider>
+            <StorageProvidersProvider>
+              <DialogProvider>
+                <MainLayout />
+                <Snackbars />
+                <AssetsManager />
+              </DialogProvider>
+            </StorageProvidersProvider>
+          </OverlayManagerProvider>
+        </BrowserRouter>
+      </ApolloProvider>
+    </>
   )
 }

+ 8 - 15
src/MainLayout.tsx

@@ -1,11 +1,9 @@
 import loadable from '@loadable/component'
 import React, { useEffect } from 'react'
-import { BrowserRouter, Route, Routes } from 'react-router-dom'
+import { Route, Routes } from 'react-router-dom'
 
 import { StudioLoading, TopbarBase } from '@/components'
 import { BASE_PATHS } from '@/config/routes'
-import { GlobalStyle } from '@/shared/components'
-import { routingTransitions } from '@/styles/routingTransitions'
 import { isBrowserOutdated } from '@/utils/browser'
 
 import { useDialog } from './providers'
@@ -43,17 +41,12 @@ export const MainLayout: React.FC = () => {
   }, [openDialog])
 
   return (
-    <>
-      <GlobalStyle additionalStyles={[routingTransitions]} />
-      <BrowserRouter>
-        <Routes>
-          <Route path={BASE_PATHS.viewer + '/*'} element={<ViewerLayout />} />
-          <Route path={BASE_PATHS.legal + '/*'} element={<LegalLayout />} />
-          <Route path={BASE_PATHS.studio + '/*'} element={<LoadableStudioLayout />} />
-          <Route path={BASE_PATHS.playground + '/*'} element={<PlaygroundLayout />} />
-          <Route path={BASE_PATHS.admin + '/*'} element={<AdminView />} />
-        </Routes>
-      </BrowserRouter>
-    </>
+    <Routes>
+      <Route path={BASE_PATHS.viewer + '/*'} element={<ViewerLayout />} />
+      <Route path={BASE_PATHS.legal + '/*'} element={<LegalLayout />} />
+      <Route path={BASE_PATHS.studio + '/*'} element={<LoadableStudioLayout />} />
+      <Route path={BASE_PATHS.playground + '/*'} element={<PlaygroundLayout />} />
+      <Route path={BASE_PATHS.admin + '/*'} element={<AdminView />} />
+    </Routes>
   )
 }

+ 1 - 1
src/components/ChannelLink/ChannelLink.tsx

@@ -36,7 +36,7 @@ export const ChannelLink: React.FC<ChannelLinkProps> = ({
   const { channel } = useBasicChannel(id || '', {
     skip: !id,
     onCompleted: (data) => !data && onNotFound?.(),
-    onError: (error) => Logger.error('Failed to fetch channel', error),
+    onError: (error) => Logger.captureError('Failed to fetch channel', 'ChannelLink', error, { channel: { id } }),
   })
   const { url: avatarPhotoUrl } = useAsset({
     entity: channel,

+ 5 - 3
src/components/Dialogs/ImageCropDialog/ImageCropDialog.tsx

@@ -86,18 +86,20 @@ const ImageCropDialogComponent: React.ForwardRefRenderFunction<
   const handleFileChange = async () => {
     const files = inputRef.current?.files
     if (!files?.length) {
-      Logger.error('no files selected')
+      Logger.captureError('No files selected for image cropping', 'ImageCropDialog')
       return
     }
+    const selectedFile = files[0]
     try {
-      const selectedFile = files[0]
       await validateImage(selectedFile)
       const fileUrl = URL.createObjectURL(selectedFile)
       setEditedImageHref(fileUrl)
       setShowDialog(true)
     } catch (error) {
       onError?.(error)
-      Logger.error(error)
+      Logger.captureError('Failed to load image for image cropping', 'ImageCropDialog', error, {
+        file: { name: selectedFile.name, type: selectedFile.type, size: selectedFile.size },
+      })
     }
   }
 

+ 1 - 1
src/components/Dialogs/ImageCropDialog/cropper.ts

@@ -165,7 +165,7 @@ export const useCropper = ({ imageEl, imageType, cropData }: UseCropperOpts) =>
       }
       canvas.toBlob((blob) => {
         if (!blob) {
-          Logger.error('Empty blob from cropped canvas', { blob })
+          Logger.captureError('Got an empty blob from cropped canvas', 'ImageCropDialog')
           return
         }
         const url = URL.createObjectURL(blob)

+ 0 - 31
src/components/ErrorFallback.tsx

@@ -1,31 +0,0 @@
-import styled from '@emotion/styled'
-import { FallbackRender } from '@sentry/react/dist/errorboundary'
-import React from 'react'
-
-import { Button, Text } from '@/shared/components'
-import { colors, sizes } from '@/shared/theme'
-import { Logger } from '@/utils/logger'
-
-const Container = styled.div`
-  padding: ${sizes(4)};
-  color: ${colors.gray[400]};
-  display: grid;
-  place-items: center;
-`
-
-const StyledButton = styled(Button)`
-  color: ${colors.white};
-`
-type FallbackProps = Partial<Parameters<FallbackRender>[0]>
-
-export const ErrorFallback: React.FC<FallbackProps> = ({ error, componentStack, resetError }) => {
-  Logger.error(`An error occurred in ${componentStack}`, error)
-  return (
-    <Container>
-      <Text>Something went wrong...</Text>
-      <StyledButton variant="tertiary" onClick={resetError}>
-        Try again
-      </StyledButton>
-    </Container>
-  )
-}

+ 6 - 4
src/components/InfiniteGrids/InfiniteChannelGrid.tsx

@@ -9,6 +9,7 @@ import {
 } from '@/api/queries'
 import { Grid, Text } from '@/shared/components'
 import { sizes } from '@/shared/theme'
+import { Logger } from '@/utils/logger'
 
 import { useInfiniteGrid } from './useInfiniteGrid'
 
@@ -56,12 +57,9 @@ export const InfiniteChannelGrid: React.FC<InfiniteChannelGridProps> = ({
     targetRowsCount,
     dataAccessor: (rawData) => rawData?.channelsConnection,
     itemsPerRow: channelsPerRow,
+    onError: (error) => Logger.captureError('Failed to fetch channels', 'InfiniteChannelsGrid', error),
   })
 
-  if (error) {
-    throw error
-  }
-
   const placeholderItems = Array.from({ length: placeholdersCount }, () => ({ id: undefined }))
   const gridContent = (
     <>
@@ -72,6 +70,10 @@ export const InfiniteChannelGrid: React.FC<InfiniteChannelGridProps> = ({
     </>
   )
 
+  if (error) {
+    return null
+  }
+
   if (displayedItems.length <= 0 && placeholdersCount <= 0) {
     return null
   }

+ 6 - 4
src/components/InfiniteGrids/InfiniteVideoGrid.tsx

@@ -10,6 +10,7 @@ import {
 } from '@/api/queries'
 import { Grid, SkeletonLoader, Text } from '@/shared/components'
 import { sizes } from '@/shared/theme'
+import { Logger } from '@/utils/logger'
 
 import { useInfiniteGrid } from './useInfiniteGrid'
 
@@ -103,12 +104,9 @@ export const InfiniteVideoGrid: React.FC<InfiniteVideoGridProps> = ({
       return rawData?.videosConnection
     },
     itemsPerRow: videosPerRow,
+    onError: (error) => Logger.captureError('Failed to fetch videos', 'InfiniteVideoGrid', error),
   })
 
-  if (error) {
-    throw error
-  }
-
   // handle category change
   // TODO potentially move into useInfiniteGrid as a general rule - keep separate targetRowsCount per serialized queryVariables
   useEffect(() => {
@@ -146,6 +144,10 @@ export const InfiniteVideoGrid: React.FC<InfiniteVideoGridProps> = ({
     </>
   )
 
+  if (error) {
+    return null
+  }
+
   if (displayedItems.length <= 0 && placeholdersCount <= 0) {
     return null
   }

+ 18 - 3
src/components/InfiniteGrids/useInfiniteGrid.ts

@@ -36,6 +36,7 @@ type UseInfiniteGridParams<TRawData, TPaginatedData extends PaginatedData<unknow
   itemsPerRow: number
   skipCount: number
   onScrollToBottom: () => void
+  onError?: (error: unknown) => void
   queryVariables: TArgs
 }
 
@@ -57,6 +58,7 @@ export const useInfiniteGrid = <
   itemsPerRow,
   skipCount,
   onScrollToBottom,
+  onError,
   queryVariables,
 }: UseInfiniteGridParams<TRawData, TPaginatedData, TArgs>): UseInfiniteGridReturn<TPaginatedData> => {
   const targetDisplayedItemsCount = targetRowsCount * itemsPerRow
@@ -69,6 +71,7 @@ export const useInfiniteGrid = <
       ...queryVariables,
       first: targetLoadedItemsCount,
     },
+    onError,
   })
 
   const data = dataAccessor(rawData)
@@ -79,7 +82,7 @@ export const useInfiniteGrid = <
 
   // handle fetching more items
   useEffect(() => {
-    if (loading || !isReady || !fetchMore || allItemsLoaded) {
+    if (loading || error || !isReady || !fetchMore || allItemsLoaded) {
       return
     }
 
@@ -92,10 +95,22 @@ export const useInfiniteGrid = <
     fetchMore({
       variables: { ...queryVariables, first: missingItemsCount, after: endCursor },
     })
-  }, [loading, fetchMore, allItemsLoaded, queryVariables, targetLoadedItemsCount, loadedItemsCount, endCursor, isReady])
+  }, [
+    loading,
+    error,
+    fetchMore,
+    allItemsLoaded,
+    queryVariables,
+    targetLoadedItemsCount,
+    loadedItemsCount,
+    endCursor,
+    isReady,
+  ])
 
   // handle scroll to bottom
   useEffect(() => {
+    if (error) return
+
     const scrollHandler = debounce(() => {
       const scrolledToBottom =
         window.innerHeight + document.documentElement.scrollTop >= document.documentElement.offsetHeight
@@ -106,7 +121,7 @@ export const useInfiniteGrid = <
 
     window.addEventListener('scroll', scrollHandler)
     return () => window.removeEventListener('scroll', scrollHandler)
-  }, [isReady, loading, allItemsLoaded, onScrollToBottom])
+  }, [error, isReady, loading, allItemsLoaded, onScrollToBottom])
 
   const displayedEdges = data?.edges.slice(skipCount, targetLoadedItemsCount) ?? []
   const displayedItems = displayedEdges.map((edge) => edge.node)

+ 10 - 6
src/components/VideoHero/VideoHeroData.ts

@@ -23,11 +23,13 @@ type CoverInfo =
 
 export const useVideoHero = (): CoverInfo => {
   const [fetchedCoverInfo, setFetchedCoverInfo] = useState<RawCoverInfo | null>(null)
-  const { video, error } = useVideo(fetchedCoverInfo?.videoId || '', { skip: !fetchedCoverInfo?.videoId })
-
-  if (error) {
-    throw error
-  }
+  const { video } = useVideo(fetchedCoverInfo?.videoId || '', {
+    skip: !fetchedCoverInfo?.videoId,
+    onError: (error) =>
+      Logger.captureError('Failed to fetch video hero', 'VideoHero', error, {
+        video: { id: fetchedCoverInfo?.videoId },
+      }),
+  })
 
   useEffect(() => {
     const fetchInfo = async () => {
@@ -35,7 +37,9 @@ export const useVideoHero = (): CoverInfo => {
         const response = await axios.get<RawCoverInfo>(COVER_VIDEO_INFO_URL)
         setFetchedCoverInfo(response.data)
       } catch (e) {
-        Logger.error(`Failed to fetch cover info from ${COVER_VIDEO_INFO_URL}. Using backup`, e)
+        Logger.captureError('Failed to fetch video hero info', 'VideoHero', e, {
+          videoHero: { url: COVER_VIDEO_INFO_URL },
+        })
         setFetchedCoverInfo(backupVideoHeroInfo)
       }
     }

+ 1 - 1
src/components/VideoTile.tsx

@@ -97,7 +97,7 @@ const useVideoSharedLogic = ({ id, isDraft, onNotFound }: UseVideoSharedLogicOpt
   const { video, loading } = useVideo(id ?? '', {
     skip: !id || isDraft,
     onCompleted: (data) => !data && onNotFound?.(),
-    onError: (error) => Logger.error('Failed to fetch video', error),
+    onError: (error) => Logger.captureError('Failed to fetch video', 'VideoTile', error, { video: { id } }),
   })
   const internalIsLoadingState = loading || !id
   const videoHref = id ? absoluteRoutes.viewer.video(id) : undefined

+ 57 - 24
src/components/ViewErrorFallback.tsx

@@ -1,12 +1,55 @@
 import styled from '@emotion/styled'
 import { FallbackRender } from '@sentry/react/dist/errorboundary'
 import React from 'react'
+import { useNavigate } from 'react-router-dom'
 
-import { Button, Text } from '@/shared/components'
-import { SvgWellErrorIllustration } from '@/shared/illustrations'
-import { colors, sizes } from '@/shared/theme'
+import { absoluteRoutes } from '@/config/routes'
+import { JOYSTREAM_DISCORD_URL } from '@/config/urls'
+import { AnimatedError, Button, Text } from '@/shared/components'
+import { media, sizes } from '@/shared/theme'
 import { Logger } from '@/utils/logger'
 
+// this isn't a react component, just a function that will be executed once to get a react element
+export const ViewErrorBoundary: FallbackRender = ({ error, resetError }) => {
+  Logger.captureError('Unhandled exception was thrown', 'ErrorBoundary', error)
+  return <ViewErrorFallback onResetClick={resetError} />
+}
+
+type ViewErrorFallbackProps = {
+  onResetClick?: () => void
+}
+
+export const ViewErrorFallback: React.FC<ViewErrorFallbackProps> = ({ onResetClick }) => {
+  const navigate = useNavigate()
+
+  const handleResetClick = () => {
+    if (onResetClick) {
+      onResetClick()
+    } else {
+      navigate(absoluteRoutes.viewer.index())
+    }
+  }
+
+  return (
+    <Container>
+      <AnimatedError />
+      <Message>
+        <Header variant="h3">Oops! An error occurred.</Header>
+        <Text variant="body1" secondary>
+          Something bad happened and the app broke. This has been logged and we&apos;ll try to resolve it as soon as
+          possible. You can find support in our Discord community.
+        </Text>
+      </Message>
+      <ButtonsContainer>
+        <Button to={JOYSTREAM_DISCORD_URL} variant="secondary">
+          Open Discord
+        </Button>
+        <Button onClick={handleResetClick}>Return to home page</Button>
+      </ButtonsContainer>
+    </Container>
+  )
+}
+
 const Container = styled.div`
   margin: ${sizes(20)} auto 0;
   display: grid;
@@ -21,29 +64,19 @@ const Message = styled.div`
   display: flex;
   flex-direction: column;
   text-align: center;
-  margin-top: 90px;
-  margin-bottom: ${sizes(10)};
+  margin-top: 50px;
+  ${media.small} {
+    max-width: 70%;
+  }
 `
 
-const Title = styled(Text)`
-  line-height: 1.25;
+const Header = styled(Text)`
+  margin-bottom: ${sizes(2)};
 `
 
-const Subtitle = styled(Text)`
-  line-height: 1.75;
-  color: ${colors.gray[300]};
+const ButtonsContainer = styled.div`
+  margin-top: 50px;
+  display: grid;
+  grid-template-columns: auto auto;
+  grid-gap: 16px;
 `
-
-export const ErrorFallback: FallbackRender = ({ error, componentStack, resetError }) => {
-  Logger.error('An error occurred.', { componentStack, error })
-  return (
-    <Container>
-      <SvgWellErrorIllustration />
-      <Message>
-        <Title variant="h3">Oops! An Error occurred.</Title>
-        <Subtitle>We could not acquire expected results. Please try reloading or return to the home page.</Subtitle>
-      </Message>
-      <Button onClick={resetError}>Return to home page</Button>
-    </Container>
-  )
-}

+ 1 - 3
src/components/index.ts

@@ -8,13 +8,11 @@ export * from './SkeletonLoaderVideoGrid'
 export * from './VideoTile'
 export * from './ChannelCard'
 export * from './ChannelGrid'
-export { ErrorFallback as ViewErrorFallback } from './ViewErrorFallback'
-export * from './ErrorFallback'
+export * from './ViewErrorFallback'
 export * from './ChannelLink'
 export * from './BackgroundPattern'
 export * from './InfiniteGrids'
 export * from './Sidenav'
-
 export * from './InterruptedVideosGallery'
 export * from './ViewWrapper'
 export * from './Portal'

+ 1 - 6
src/index.tsx

@@ -1,4 +1,3 @@
-import { CaptureConsole } from '@sentry/integrations'
 import * as Sentry from '@sentry/react'
 import React from 'react'
 import ReactDOM from 'react-dom'
@@ -22,11 +21,7 @@ const initApp = async () => {
   if (BUILD_ENV === 'production') {
     Sentry.init({
       dsn: SENTRY_DSN,
-      integrations: [
-        new CaptureConsole({
-          levels: ['error'],
-        }),
-      ],
+      ignoreErrors: ['ResizeObserver loop limit exceeded'],
     })
   }
   ReactDOM.render(<App />, document.getElementById('root'))

+ 7 - 21
src/joystream-lib/api.ts

@@ -87,27 +87,14 @@ export class JoystreamJs {
 
   destroy() {
     this.api.disconnect()
-    this.log('Destroyed')
-  }
-
-  /* Private utilities */
-  private log(msg: string) {
-    Logger.log(`[JoystreamJS] ${msg}`)
-  }
-
-  private logWarn(msg: string) {
-    Logger.warn(`[JoystreamJS] ${msg}`)
-  }
-
-  private logError(msg: string) {
-    Logger.error(`[JoystreamJS] ${msg}`)
+    Logger.log('[JoystreamJs] Destroyed')
   }
 
   private async ensureApi() {
     try {
       await this.api.isReady
     } catch (e) {
-      Logger.error('Polkadot API init error', e)
+      Logger.captureError('Failed to initialize Polkadot API', 'JoystreamJs', e)
       throw new ApiNotConnectedError()
     }
   }
@@ -115,7 +102,7 @@ export class JoystreamJs {
   private async logConnectionData(endpoint: string) {
     await this.ensureApi()
     const chain = await this.api.rpc.system.chain()
-    this.log(`Connected to chain "${chain}" via "${endpoint}"`)
+    Logger.log(`[JoystreamJs] Connected to chain "${chain}" via "${endpoint}"`)
   }
 
   private async sendExtrinsic(
@@ -164,7 +151,6 @@ export class JoystreamJs {
                       // In this case - continue (we'll just display dispatchError.toString())
                     }
                   }
-                  this.logError(`Extrinsic failed: "${errorMsg}"`)
                   reject(new ExtrinsicFailedError(event, errorMsg))
                 } else if (event.method === 'ExtrinsicSuccess') {
                   const blockHash = status.asFinalized
@@ -173,8 +159,9 @@ export class JoystreamJs {
                     .then(({ number }) => resolve({ block: number.toNumber(), data: unpackedEvents }))
                     .catch((reason) => reject(new ExtrinsicFailedError(reason)))
                 } else {
-                  Logger.warn('Unknown event method')
-                  Logger.warn('Event:', event)
+                  Logger.captureMessage('Unknown extrinsic event', 'JoystreamJs', 'warning', {
+                    event: { method: event.method },
+                  })
                 }
               })
           }
@@ -187,7 +174,6 @@ export class JoystreamJs {
           reject(new ExtrinsicSignCancelledError())
           return
         }
-        this.logError(`Unknown sendExtrinsic error: ${e}`)
         reject(e)
       }
     })
@@ -410,7 +396,7 @@ export class JoystreamJs {
       this.api.setSigner({})
       return
     } else if (!signer) {
-      this.logError('Missing signer on active account set')
+      Logger.captureError('Missing signer for setActiveAccount', 'JoystreamJs')
       return
     }
 

+ 16 - 2
src/providers/assets/assetsManager.tsx

@@ -48,10 +48,24 @@ export const AssetsManager: React.FC = () => {
           removeAssetBeingResolved(contentId)
           return
         } catch (e) {
-          Logger.error(`Failed to load ${resolutionData.assetType}`, { contentId, assetUrl })
+          // don't capture every single asset timeout as error, just log it
+          Logger.error('Failed to load asset', {
+            contentId,
+            type: resolutionData.assetType,
+            storageProviderId: storageProvider.workerId,
+            storageProviderUrl: storageProvider.metadata,
+            assetUrl,
+          })
         }
       }
-      Logger.error(`No storage provider was able to provide asset`, { contentId })
+      Logger.captureError('No storage provider was able to provide asset', 'AssetsManager', null, {
+        asset: {
+          contentId,
+          type: resolutionData.assetType,
+          storageProviderIds: storageProvidersToTry.map((sp) => sp.workerId),
+          storageProviderUrls: storageProvidersToTry.map((sp) => sp.metadata),
+        },
+      })
     })
   }, [
     addAsset,

+ 5 - 1
src/providers/editVideoSheet/hooks.tsx

@@ -7,6 +7,7 @@ import { useNavigate } from 'react-router-dom'
 import { useVideo } from '@/api/hooks'
 import { absoluteRoutes } from '@/config/routes'
 import { RoutingState } from '@/types/routing'
+import { Logger } from '@/utils/logger'
 
 import { EditVideoSheetContext } from './provider'
 import { EditVideoAssets, EditVideoFormFields, EditVideoSheetState, EditVideoSheetTab } from './types'
@@ -25,7 +26,10 @@ export const useEditVideoSheetTabData = (tab?: EditVideoSheetTab) => {
   const { activeChannelId } = useAuthorizedUser()
   const drafts = useDraftStore(channelDraftsSelector(activeChannelId))
   const { selectedVideoTabCachedAssets } = useEditVideoSheet()
-  const { video, loading, error } = useVideo(tab?.id ?? '', { skip: tab?.isDraft })
+  const { video, loading, error } = useVideo(tab?.id ?? '', {
+    skip: tab?.isDraft,
+    onError: (error) => Logger.captureError('Failed to fetch video', 'useEditVideoSheetTabData', error),
+  })
 
   if (!tab) {
     return {

+ 1 - 1
src/providers/joystream/provider.tsx

@@ -38,7 +38,7 @@ export const JoystreamProvider: React.FC = ({ children }) => {
         joystream.onNodeConnectionUpdate = handleNodeConnectionUpdate
       } catch (e) {
         handleNodeConnectionUpdate(false)
-        Logger.error('Failed to create JoystreamJs instance', e)
+        Logger.captureError('Failed to create JoystreamJS instance', 'JoystreamProvider', e)
       }
     }
 

+ 17 - 19
src/providers/storageProviders.tsx

@@ -9,6 +9,7 @@ import {
   GetWorkersQuery,
   GetWorkersQueryVariables,
 } from '@/api/queries/__generated__/workers.generated'
+import { ViewErrorFallback } from '@/components'
 import { Logger } from '@/utils/logger'
 import { getRandomIntInclusive } from '@/utils/number'
 
@@ -22,21 +23,10 @@ type StorageProvidersContextValue = {
 const StorageProvidersContext = React.createContext<StorageProvidersContextValue | undefined>(undefined)
 StorageProvidersContext.displayName = 'StorageProvidersContext'
 
-class NoStorageProviderError extends Error {
-  storageProviders: string[]
-  notWorkingStorageProviders: string[]
-
-  constructor(message: string, storageProviders: string[], notWorkingStorageProviders: string[]) {
-    super(message)
-
-    this.storageProviders = storageProviders
-    this.notWorkingStorageProviders = notWorkingStorageProviders
-  }
-}
-
 // ¯\_(ツ)_/¯ for the name
 export const StorageProvidersProvider: React.FC = ({ children }) => {
   const [notWorkingStorageProvidersIds, setNotWorkingStorageProvidersIds] = useState<string[]>([])
+  const [storageProvidersError, setStorageProvidersError] = useState<unknown>(null)
   const storageProvidersPromiseRef = useRef<StorageProvidersPromise>()
 
   const client = useApolloClient()
@@ -51,9 +41,16 @@ export const StorageProvidersProvider: React.FC = ({ children }) => {
       },
     })
     storageProvidersPromiseRef.current = promise
-    promise.catch((error) => Logger.error('Failed to fetch storage providers list', error))
+    promise.catch((error) => {
+      Logger.captureError('Failed to fetch storage providers list', 'StorageProvidersProvider', error)
+      setStorageProvidersError(error)
+    })
   }, [client])
 
+  if (storageProvidersError) {
+    return <ViewErrorFallback />
+  }
+
   return (
     <StorageProvidersContext.Provider
       value={{
@@ -91,11 +88,12 @@ export const useStorageProviders = () => {
     )
 
     if (!workingStorageProviders.length) {
-      throw new NoStorageProviderError(
-        'No storage provider available',
-        storageProviders.map(({ workerId }) => workerId),
-        notWorkingStorageProvidersIds
-      )
+      Logger.captureError('No storage provider available', 'StorageProvidersProvider', null, {
+        providers: {
+          allIds: storageProviders.map(({ workerId }) => workerId),
+          notWorkingIds: notWorkingStorageProvidersIds,
+        },
+      })
     }
 
     return workingStorageProviders
@@ -103,7 +101,7 @@ export const useStorageProviders = () => {
 
   const getRandomStorageProvider = useCallback(async () => {
     const workingStorageProviders = await getStorageProviders()
-    if (!workingStorageProviders) {
+    if (!workingStorageProviders || !workingStorageProviders.length) {
       return null
     }
     const randomStorageProviderIdx = getRandomIntInclusive(0, workingStorageProviders.length - 1)

+ 1 - 1
src/providers/transactionManager/transactionManager.tsx

@@ -24,7 +24,7 @@ export const TransactionManager: React.FC = () => {
         try {
           action.callback()
         } catch (e) {
-          Logger.error('Failed to execute tx sync callback', e)
+          Logger.captureError('Failed to execute tx sync callback', 'TransactionManager', e)
         }
       })
 

+ 14 - 7
src/providers/transactionManager/useTransaction.ts

@@ -1,4 +1,4 @@
-import { ExtrinsicResult, ExtrinsicSignCancelledError, ExtrinsicStatus } from '@/joystream-lib'
+import { ExtrinsicFailedError, ExtrinsicResult, ExtrinsicSignCancelledError, ExtrinsicStatus } from '@/joystream-lib'
 import { TransactionDialogStep, useConnectionStatusStore, useDialog, useSnackbar } from '@/providers'
 import { Logger } from '@/utils/logger'
 
@@ -56,7 +56,7 @@ export const useTransaction = (): HandleTransactionFn => {
         try {
           await preProcess()
         } catch (e) {
-          Logger.error('Failed transaction preprocess', e)
+          Logger.captureError('Failed transaction preprocess', 'TransactionManager', e)
           return false
         }
       }
@@ -65,7 +65,9 @@ export const useTransaction = (): HandleTransactionFn => {
       setDialogStep(ExtrinsicStatus.Unsigned)
       const { data: txData, block } = await txFactory(setDialogStep)
       if (onTxFinalize) {
-        onTxFinalize(txData).catch((e) => Logger.error('Failed transaction finalize callback', e))
+        onTxFinalize(txData).catch((e) =>
+          Logger.captureError('Failed transaction finalize callback', 'TransactionManager', e)
+        )
       }
 
       setDialogStep(ExtrinsicStatus.Syncing)
@@ -75,7 +77,7 @@ export const useTransaction = (): HandleTransactionFn => {
             try {
               await onTxSync(txData)
             } catch (e) {
-              Logger.error('Failed transaction sync callback', e)
+              Logger.captureError('Failed transaction sync callback', 'TransactionManager', e)
             }
           }
           resolve()
@@ -112,11 +114,16 @@ export const useTransaction = (): HandleTransactionFn => {
           iconType: 'warning',
           timeout: TX_SIGN_CANCELLED_SNACKBAR_TIMEOUT,
         })
+        return false
+      }
+
+      if (e instanceof ExtrinsicFailedError) {
+        Logger.captureError('Extrinsic failed', 'TransactionManager', e)
       } else {
-        Logger.error(e)
-        setDialogStep(null)
-        openErrorDialog()
+        Logger.captureError('Unknown sendExtrinsic error', 'TransactionManager', e)
       }
+      setDialogStep(null)
+      openErrorDialog()
       return false
     }
   }

+ 11 - 4
src/providers/uploadsManager/useStartFileUpload.tsx

@@ -84,16 +84,21 @@ export const useStartFileUpload = () => {
       try {
         const storageProvider = await getRandomStorageProvider()
         if (!storageProvider) {
+          Logger.captureError('No storage provider available for upload', 'UploadsManager')
           return
         }
         storageUrl = storageProvider.url
         storageProviderId = storageProvider.id
       } catch (e) {
-        Logger.error('Failed to find storage provider', e)
+        Logger.captureError('Failed to get storage provider for upload', 'UploadsManager', e)
         return
       }
 
-      Logger.debug(`Uploading to ${storageUrl}`)
+      Logger.debug('Starting file upload', {
+        contentId: asset.contentId,
+        storageProviderId,
+        storageProviderUrl: storageUrl,
+      })
 
       const setAssetStatus = (status: Partial<UploadStatus>) => {
         setUploadStatus(asset.contentId, status)
@@ -104,13 +109,13 @@ export const useStartFileUpload = () => {
       }
 
       const assetKey = `${asset.parentObject.type}-${asset.parentObject.id}`
+      const assetUrl = createStorageNodeUrl(asset.contentId, storageUrl)
 
       try {
         if (!fileInState && !file) {
           throw Error('File was not provided nor found')
         }
         rax.attach()
-        const assetUrl = createStorageNodeUrl(asset.contentId, storageUrl)
         if (!opts?.isReUpload && !opts?.changeHost && file) {
           addAsset({ ...asset, size: file.size })
         }
@@ -162,7 +167,9 @@ export const useStartFileUpload = () => {
           (assetsNotificationsCount.current.uploaded[assetKey] || 0) + 1
         displayUploadedNotification.current(assetKey)
       } catch (e) {
-        Logger.error('Failed to upload to storage provider', { storageUrl, error: e })
+        Logger.captureError('Failed to upload asset', 'UploadsManager', e, {
+          asset: { contentId: asset.contentId, storageProviderId, storageProviderUrl: storageUrl, assetUrl },
+        })
         setAssetStatus({ lastStatus: 'error', progress: 0 })
 
         const axiosError = e as AxiosError

+ 23 - 11
src/providers/user/user.tsx

@@ -3,6 +3,7 @@ import { InjectedAccountWithMeta } from '@polkadot/extension-inject/types'
 import React, { useContext, useEffect, useState } from 'react'
 
 import { useMembership, useMemberships } from '@/api/hooks'
+import { ViewErrorFallback } from '@/components'
 import { WEB3_APP_NAME } from '@/config/urls'
 import { AccountId } from '@/joystream-lib'
 import { Logger } from '@/utils/logger'
@@ -47,7 +48,16 @@ export const ActiveUserProvider: React.FC = ({ children }) => {
     loading: membershipsLoading,
     error: membershipsError,
     refetch: refetchMemberships,
-  } = useMemberships({ where: { controllerAccount_in: accountsIds } }, { skip: !accounts || !accounts.length })
+  } = useMemberships(
+    { where: { controllerAccount_in: accountsIds } },
+    {
+      skip: !accounts || !accounts.length,
+      onError: (error) =>
+        Logger.captureError('Failed to fetch memberships', 'ActiveUserProvider', error, {
+          accounts: { ids: accountsIds },
+        }),
+    }
+  )
 
   // use previous values when doing the refetch, so the app doesn't think we don't have any memberships
   const memberships = membershipsData || membershipPreviousData?.memberships
@@ -57,15 +67,13 @@ export const ActiveUserProvider: React.FC = ({ children }) => {
     loading: activeMembershipLoading,
     error: activeMembershipError,
     refetch: refetchActiveMembership,
-  } = useMembership({ where: { id: activeUserState.memberId } }, { skip: !activeUserState.memberId })
-
-  if (membershipsError) {
-    throw membershipsError
-  }
-
-  if (activeMembershipError) {
-    throw activeMembershipError
-  }
+  } = useMembership(
+    { where: { id: activeUserState.memberId } },
+    {
+      skip: !activeUserState.memberId,
+      onError: (error) => Logger.captureError('Failed to fetch active membership', 'ActiveUserProvider', error),
+    }
+  )
 
   // handle polkadot extension
   useEffect(() => {
@@ -97,7 +105,7 @@ export const ActiveUserProvider: React.FC = ({ children }) => {
         setExtensionConnected(true)
       } catch (e) {
         setExtensionConnected(false)
-        Logger.error('Unknown polkadot extension error', e)
+        Logger.captureError('Failed to initialize Polkadot signer extension', 'ActiveUserProvider', e)
       }
     }
 
@@ -143,6 +151,10 @@ export const ActiveUserProvider: React.FC = ({ children }) => {
     userInitialized,
   }
 
+  if (membershipsError || activeMembershipError) {
+    return <ViewErrorFallback />
+  }
+
   return <ActiveUserContext.Provider value={contextValue}>{children}</ActiveUserContext.Provider>
 }
 

+ 7 - 5
src/shared/components/VideoPlayer/VideoPlayer.tsx

@@ -130,13 +130,15 @@ const VideoPlayerComponent: React.ForwardRefRenderFunction<HTMLVideoElement, Vid
     if (playPromise) {
       playPromise.catch((e) => {
         if (e.name === 'NotAllowedError') {
-          Logger.warn('Video play failed:', e)
+          Logger.warn('Video playback failed', e)
         } else {
-          Logger.error('Video play failed:', e)
+          Logger.captureError('Video playback failed', 'VideoPlayer', e, {
+            video: { id: videoId, url: videoJsConfig.src },
+          })
         }
       })
     }
-  }, [player])
+  }, [player, videoId, videoJsConfig.src])
 
   // handle video loading
   useEffect(() => {
@@ -194,7 +196,7 @@ const VideoPlayerComponent: React.ForwardRefRenderFunction<HTMLVideoElement, Vid
     const playPromise = player.play()
     if (playPromise) {
       playPromise.catch((e) => {
-        Logger.warn('Autoplay failed:', e)
+        Logger.warn('Video autoplay failed', e)
       })
     }
   }, [player, isLoaded, autoplay])
@@ -398,7 +400,7 @@ const VideoPlayerComponent: React.ForwardRefRenderFunction<HTMLVideoElement, Vid
       if (document.pictureInPictureEnabled) {
         // @ts-ignore @types/video.js is outdated and doesn't provide types for some newer video.js features
         player.requestPictureInPicture().catch((e) => {
-          Logger.warn('Picture in picture failed:', e)
+          Logger.warn('Picture in picture failed', e)
         })
       }
     }

A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 0 - 47
src/shared/illustrations/EmptyVideosIllustration.tsx


A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 0 - 8
src/shared/illustrations/TheaterMaskIllustration.tsx


A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 0 - 15
src/shared/illustrations/WellErrorIllustration.tsx


+ 0 - 3
src/shared/illustrations/index.tsx

@@ -4,12 +4,9 @@ export * from './AvatarSilhouette'
 export * from './BgPattern'
 export * from './CoinsIllustration'
 export * from './EmptyStateIllustration'
-export * from './EmptyVideosIllustration'
 export * from './JoystreamFullLogo'
 export * from './JoystreamLogo'
 export * from './JoystreamOneLetterLogo'
 export * from './PolkadotLogo'
 export * from './SigninIllustration'
-export * from './TheaterMaskIllustration'
 export * from './TransactionIllustration'
-export * from './WellErrorIllustration'

A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 0 - 13
src/shared/illustrations/svgs/empty-videos-illustration.svg


A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 0 - 65
src/shared/illustrations/svgs/theater-mask-illustration.svg


A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 0 - 39
src/shared/illustrations/svgs/well-error-illustration.svg


+ 1 - 1
src/store/index.ts

@@ -61,7 +61,7 @@ export const createStore = <TState extends object, TActions extends object>(
         try {
           return config.migrate(oldState, oldVersion, storageValue) as CommonStore<TState, TActions>
         } catch (e) {
-          Logger.error(`Failed to migrate store "${config.key}"`, e)
+          Logger.captureError(`Failed to migrate store "${config.key}"`, 'createStore', e)
           return {} as CommonStore<TState, TActions>
         }
       },

+ 1 - 2
src/utils/image.ts

@@ -4,7 +4,6 @@ export const validateImage = async (file: File): Promise<File> => {
     const fileURL = URL.createObjectURL(file)
     img.src = fileURL
     img.onload = () => resolve(file)
-    img.onerror = () =>
-      reject(new Error('There was an error loading the image please try again or choose another image'))
+    img.onerror = () => reject(new Error('Image could not be loaded'))
   })
 }

+ 3 - 3
src/utils/localStorage.ts

@@ -6,9 +6,9 @@ export const readFromLocalStorage = <T>(key: string, { deserialize = JSON.parse
     try {
       return deserialize(valueInLocalStorage) as T
     } catch (error) {
-      Logger.error(
-        `An error occured when deserializing a value from Local Storage. Did you pass the correct serializer to readFromLocalStorage?`
-      )
+      Logger.captureError('Failed to deserialize value from localStorage', 'readFromLocalStorage', error, {
+        localStorage: { key, value: valueInLocalStorage },
+      })
       throw error
     }
   }

+ 103 - 16
src/utils/logger.ts

@@ -1,18 +1,105 @@
 /* eslint-disable no-console */
-export const Logger = {
-  log: (message: string, details?: unknown) => {
-    console.log(message, details || '')
-  },
-
-  warn: (message: string, details?: unknown) => {
-    console.warn(message, details || '')
-  },
-
-  error: (message: string, details?: unknown) => {
-    console.error(message, details || '')
-  },
-
-  debug: (message: string, details?: unknown) => {
-    console.debug(message, details || '')
-  },
+import * as Sentry from '@sentry/react'
+import { Severity } from '@sentry/react'
+
+import { useActiveUserStore } from '@/providers/user/store'
+
+class CustomError extends Error {
+  name: string
+  message: string
+
+  constructor(name: string, message: string) {
+    super()
+    this.name = name
+    this.message = message
+  }
+}
+
+type LogContexts = Record<string, Record<string, unknown>>
+type LogFn = (message: string, details?: unknown) => void
+type LogMessageLevel = 'info' | 'warning' | 'error'
+
+const getLogArgs = (message: string, details?: unknown) => {
+  if (details) {
+    return [message, details]
+  }
+  return [message]
+}
+
+export class Logger {
+  static log: LogFn = (message, details) => {
+    console.log(...getLogArgs(message, details))
+  }
+
+  static warn: LogFn = (message, details) => {
+    console.warn(...getLogArgs(message, details))
+  }
+
+  static error: LogFn = (message, details) => {
+    console.error(...getLogArgs(message, details))
+  }
+
+  static debug: LogFn = (message, details) => {
+    console.debug(...getLogArgs(message, details))
+  }
+
+  static captureError = (
+    title: string,
+    source: string,
+    // eslint-disable-next-line @typescript-eslint/no-explicit-any
+    rawError?: any,
+    contexts?: LogContexts
+  ) => {
+    let error = rawError
+    const tags: Record<string, string | number> = {
+      source,
+    }
+
+    let rawGraphQLError = error?.graphQLErrors?.[0]?.originalError
+    if (rawGraphQLError?.graphQLErrors?.[0]?.originalError) {
+      rawGraphQLError = rawGraphQLError.graphQLErrors[0].originalError
+    }
+    if (rawGraphQLError) {
+      error = {
+        ...error,
+        graphQLError: rawGraphQLError.result?.errors[0],
+        url: rawGraphQLError.response?.url,
+      }
+    }
+
+    const statusCode = error?.statusCode || error?.response?.status || rawGraphQLError?.statusCode
+    if (statusCode) {
+      tags.statusCode = statusCode
+    }
+
+    const message = rawError?.message || rawGraphQLError?.message || ''
+
+    Logger.error(!message ? title : `${title}: ${message}`, { ...error, ...contexts })
+
+    Sentry.captureException(new CustomError(title, message), {
+      contexts: {
+        error,
+        ...contexts,
+      },
+      tags,
+      user: getUserInfo(),
+    })
+  }
+
+  static captureMessage = (message: string, source: string, level: LogMessageLevel, contexts?: LogContexts) => {
+    Sentry.captureMessage(message, {
+      level: Severity.fromString(level),
+      contexts,
+      tags: { source },
+      user: getUserInfo(),
+    })
+  }
+}
+
+const getUserInfo = (): Record<string, unknown> => {
+  const { actions, ...userState } = useActiveUserStore.getState()
+  return {
+    ip_address: '{{auto}}',
+    ...userState,
+  }
 }

+ 1 - 1
src/views/admin/AdminView.tsx

@@ -58,7 +58,7 @@ export const AdminView = () => {
         iconType: 'success',
       })
     } catch (error) {
-      Logger.error(error)
+      Logger.captureError('Failed to import local state', 'AdminView', error)
       displaySnackbar({
         title: 'JSON file seems to be corrupted',
         description: 'Please try again with different file',

+ 9 - 2
src/views/studio/CreateEditChannelView/CreateEditChannelView.tsx

@@ -10,6 +10,7 @@ import {
   ImageCropDialogImperativeHandle,
   ImageCropDialogProps,
   LimitedWidthContainer,
+  ViewErrorFallback,
 } from '@/components'
 import { languages } from '@/config/languages'
 import { absoluteRoutes } from '@/config/routes'
@@ -94,6 +95,10 @@ export const CreateEditChannelView: React.FC<CreateEditChannelViewProps> = ({ ne
 
   const { channel, loading, error, refetch: refetchChannel } = useChannel(activeChannelId || '', {
     skip: newChannel || !activeChannelId,
+    onError: (error) =>
+      Logger.captureError('Failed to fetch channel', 'CreateEditChannelView', error, {
+        channel: { id: activeChannelId },
+      }),
   })
   const startFileUpload = useStartFileUpload()
 
@@ -291,7 +296,9 @@ export const CreateEditChannelView: React.FC<CreateEditChannelViewProps> = ({ ne
         })
         uploadPromises.push(uploadPromise)
       }
-      Promise.all(uploadPromises).catch((e) => Logger.error('Failed assets upload', e))
+      Promise.all(uploadPromises).catch((e) =>
+        Logger.captureError('Unexpected upload failure', 'CreateEditChannelView', e)
+      )
     }
 
     const refetchDataAndCacheAssets = async (channelId: ChannelId) => {
@@ -332,7 +339,7 @@ export const CreateEditChannelView: React.FC<CreateEditChannelViewProps> = ({ ne
   }
 
   if (error) {
-    throw error
+    return <ViewErrorFallback />
   }
 
   const progressDrawerSteps = [

+ 11 - 4
src/views/studio/CreateMemberView/CreateMemberView.tsx

@@ -7,6 +7,7 @@ import { useNavigate } from 'react-router'
 
 import { useQueryNodeStateSubscription } from '@/api/hooks'
 import { GetMembershipDocument, GetMembershipQuery, GetMembershipQueryVariables } from '@/api/queries'
+import { ViewErrorFallback } from '@/components'
 import { MEMBERSHIP_NAME_PATTERN, URL_PATTERN } from '@/config/regex'
 import { absoluteRoutes } from '@/config/routes'
 import { FAUCET_URL } from '@/config/urls'
@@ -64,9 +65,11 @@ export const CreateMemberView = () => {
   const [openErrorDialog, closeErrorDialog] = useDialog()
 
   const { queryNodeState, error: queryNodeStateError } = useQueryNodeStateSubscription({ skip: !membershipBlock })
-  if (queryNodeStateError) {
-    throw queryNodeStateError
-  }
+  // subscription doesn't allow 'onError' callback
+  useEffect(() => {
+    if (!queryNodeStateError) return
+    Logger.captureError('Failed to subscribe to query node state', 'CreateMemberView', queryNodeStateError)
+  }, [queryNodeStateError])
 
   const client = useApolloClient()
 
@@ -144,6 +147,10 @@ export const CreateMemberView = () => {
     }, 500)
   )
 
+  if (queryNodeStateError) {
+    return <ViewErrorFallback />
+  }
+
   return (
     <Wrapper>
       <Header>
@@ -223,7 +230,7 @@ export const createNewMember = async (accountId: string, inputs: Inputs) => {
     const response = await axios.post<NewMemberResponse>(FAUCET_URL, body)
     return response.data
   } catch (error) {
-    Logger.error('Failed to create a new member', error)
+    Logger.captureError('Failed to create a membership', 'CreateMemberView', error)
     throw error
   }
 }

+ 11 - 10
src/views/studio/EditVideoSheet/EditVideoForm/EditVideoForm.tsx

@@ -6,6 +6,7 @@ import useMeasure from 'react-use-measure'
 
 import { useCategories } from '@/api/hooks'
 import { License } from '@/api/queries'
+import { ViewErrorFallback } from '@/components'
 import { languages } from '@/config/languages'
 import knownLicenses from '@/data/knownLicenses.json'
 import { useDeleteVideo } from '@/hooks'
@@ -101,19 +102,15 @@ export const EditVideoForm: React.FC<EditVideoFormProps> = ({
     sheetState,
   } = useEditVideoSheet()
   const { updateDraft, addDraft } = useDraftStore((state) => state.actions)
-  const { categories, error: categoriesError } = useCategories()
-  const { tabData, loading: tabDataLoading, error: tabDataError } = useEditVideoSheetTabData(selectedVideoTab)
+
   const nodeConnectionStatus = useConnectionStatusStore((state) => state.nodeConnectionStatus)
 
   const deleteVideo = useDeleteVideo()
 
-  if (categoriesError) {
-    throw categoriesError
-  }
-
-  if (tabDataError) {
-    throw tabDataError
-  }
+  const { categories, error: categoriesError } = useCategories(undefined, {
+    onError: (error) => Logger.captureError('Failed to fetch categories', 'EditVideoSheet', error),
+  })
+  const { tabData, loading: tabDataLoading, error: tabDataError } = useEditVideoSheetTabData(selectedVideoTab)
 
   const {
     register,
@@ -399,7 +396,7 @@ export const EditVideoForm: React.FC<EditVideoFormProps> = ({
     } else if (errorCode === 'file-too-large') {
       setFileSelectError('File too large')
     } else {
-      Logger.error('Unknown file select error', errorCode)
+      Logger.captureError('Unknown file select error', 'EditVideoForm', null, { error: { code: errorCode } })
       setFileSelectError('Unknown error')
     }
   }
@@ -414,6 +411,10 @@ export const EditVideoForm: React.FC<EditVideoFormProps> = ({
       value: c.id,
     })) || []
 
+  if (tabDataError || categoriesError) {
+    return <ViewErrorFallback />
+  }
+
   return (
     <>
       <FormScrolling actionBarHeight={actionBarBounds.height}>

+ 1 - 1
src/views/studio/EditVideoSheet/EditVideoSheet.tsx

@@ -197,7 +197,7 @@ export const EditVideoSheet: React.FC = () => {
         })
         uploadPromises.push(uploadPromise)
       }
-      Promise.all(uploadPromises).catch((e) => Logger.error('Failed assets upload', e))
+      Promise.all(uploadPromises).catch((e) => Logger.captureError('Unexpected upload failure', 'EditVideoSheet', e))
     }
 
     const refetchDataAndCacheAssets = async (videoId: VideoId) => {

+ 7 - 3
src/views/studio/MyVideosView/MyVideosView.tsx

@@ -3,7 +3,7 @@ import { useNavigate } from 'react-router-dom'
 
 import { useVideosConnection } from '@/api/hooks'
 import { VideoOrderByInput } from '@/api/queries'
-import { LimitedWidthContainer, VideoTilePublisher } from '@/components'
+import { LimitedWidthContainer, VideoTilePublisher, ViewErrorFallback } from '@/components'
 import { absoluteRoutes } from '@/config/routes'
 import { SORT_OPTIONS } from '@/config/sorting'
 import { useDeleteVideo } from '@/hooks'
@@ -18,6 +18,7 @@ import {
 } from '@/providers'
 import { Button, EmptyFallback, Grid, Pagination, Select, Tabs, Text } from '@/shared/components'
 import { SvgGlyphUpload } from '@/shared/icons'
+import { Logger } from '@/utils/logger'
 
 import {
   PaginationContainer,
@@ -72,7 +73,10 @@ export const MyVideosView = () => {
         isPublic_eq,
       },
     },
-    { notifyOnNetworkStatusChange: true }
+    {
+      notifyOnNetworkStatusChange: true,
+      onError: (error) => Logger.captureError('Failed to fetch videos', 'MyVideosView', error),
+    }
   )
   const [openDeleteDraftDialog, closeDeleteDraftDialog] = useDialog()
   const deleteVideo = useDeleteVideo()
@@ -245,7 +249,7 @@ export const MyVideosView = () => {
       ))
 
   if (error) {
-    throw error
+    return <ViewErrorFallback />
   }
 
   const mappedTabs = TABS.map((tab) => ({ name: tab, badgeNumber: tab === 'Drafts' ? unseenDrafts.length : 0 }))

+ 2 - 5
src/views/studio/StudioLayout.tsx

@@ -12,7 +12,7 @@ import {
   StudioSidenav,
   StudioTopbar,
   TOP_NAVBAR_HEIGHT,
-  ViewErrorFallback,
+  ViewErrorBoundary,
 } from '@/components'
 import { absoluteRoutes, relativeRoutes } from '@/config/routes'
 import {
@@ -84,9 +84,6 @@ const StudioLayout = () => {
     }
   }, [closeUnsupportedBrowserDialog, openUnsupportedBrowserDialog])
 
-  // TODO: add route transition
-  // TODO: remove dependency on PersonalDataProvider
-  //  we need PersonalDataProvider because DismissibleBanner in video drafts depends on it
   return (
     <>
       <NoConnectionIndicator
@@ -166,7 +163,7 @@ const StudioLayoutWrapper: React.FC = () => {
   const navigate = useNavigate()
   return (
     <ErrorBoundary
-      fallback={ViewErrorFallback}
+      fallback={ViewErrorBoundary}
       onReset={() => {
         navigate(absoluteRoutes.studio.index())
       }}

+ 29 - 15
src/views/viewer/ChannelView/ChannelView.tsx

@@ -16,7 +16,7 @@ import {
   VideoOrderByInput,
   useSearchLazyQuery,
 } from '@/api/queries'
-import { LimitedWidthContainer, VideoTile, ViewWrapper } from '@/components'
+import { LimitedWidthContainer, VideoTile, ViewErrorFallback, ViewWrapper } from '@/components'
 import { absoluteRoutes } from '@/config/routes'
 import { SORT_OPTIONS } from '@/config/sorting'
 import { AssetType, useAsset, useDialog, usePersonalDataStore } from '@/providers'
@@ -59,7 +59,9 @@ export const ChannelView: React.FC = () => {
   const [openUnfollowDialog, closeUnfollowDialog] = useDialog()
   const { id } = useParams()
   const [searchParams, setSearchParams] = useSearchParams()
-  const { channel, loading, error } = useChannel(id)
+  const { channel, loading, error } = useChannel(id, {
+    onError: (error) => Logger.captureError('Failed to fetch channel', 'ChannelView', error, { channel: { id } }),
+  })
   const {
     searchVideos,
     loadingSearch,
@@ -71,7 +73,13 @@ export const ChannelView: React.FC = () => {
     search,
     errorSearch,
     searchQuery,
-  } = useSearchVideos({ id })
+  } = useSearchVideos({
+    id,
+    onError: (error) =>
+      Logger.captureError('Failed to search channel videos', 'ChannelView', error, {
+        search: { channelId: id, query: searchQuery },
+      }),
+  })
   const { followChannel } = useFollowChannel()
   const { unfollowChannel } = useUnfollowChannel()
   const followedChannels = usePersonalDataStore((state) => state.followedChannels)
@@ -106,9 +114,14 @@ export const ChannelView: React.FC = () => {
         mediaAvailability_eq: AssetAvailability.Accepted,
       },
     },
-    { notifyOnNetworkStatusChange: true }
+    {
+      notifyOnNetworkStatusChange: true,
+      onError: (error) => Logger.captureError('Failed to fetch videos', 'ChannelView', error, { channel: { id } }),
+    }
   )
-  const { videoCount: videosLastMonth } = useChannelVideoCount(id, DATE_ONE_MONTH_PAST)
+  const { videoCount: videosLastMonth } = useChannelVideoCount(id, DATE_ONE_MONTH_PAST, {
+    onError: (error) => Logger.captureError('Failed to fetch videos', 'ChannelView', error, { channel: { id } }),
+  })
   useEffect(() => {
     const isFollowing = followedChannels.some((channel) => channel.id === id)
     setFollowing(isFollowing)
@@ -156,16 +169,9 @@ export const ChannelView: React.FC = () => {
         setFollowing(true)
       }
     } catch (error) {
-      Logger.warn('Failed to update Channel following', { error })
+      Logger.captureError('Failed to update channel following', 'ChannelView', error, { channel: { id } })
     }
   }
-  if (videosError) {
-    throw videosError
-  } else if (error) {
-    throw error
-  } else if (errorSearch) {
-    throw errorSearch
-  }
 
   const handleSetCurrentTab = async (tab: number) => {
     if (TABS[tab] === 'Videos' && isSearching) {
@@ -259,6 +265,10 @@ export const ChannelView: React.FC = () => {
     // eslint-disable-next-line react-hooks/exhaustive-deps
   }, [])
 
+  if (videosError || error || errorSearch) {
+    return <ViewErrorFallback />
+  }
+
   if (!loading && !channel) {
     return (
       <NotFoundChannelContainer>
@@ -273,6 +283,7 @@ export const ChannelView: React.FC = () => {
       </NotFoundChannelContainer>
     )
   }
+
   return (
     <ViewWrapper>
       <ChannelCover assetUrl={coverPhotoUrl} />
@@ -354,12 +365,15 @@ const getVideosFromSearch = (loading: boolean, data: SearchQuery['search'] | und
 }
 type UseSearchVideosParams = {
   id: string
+  onError: (error: unknown) => void
 }
-const useSearchVideos = ({ id }: UseSearchVideosParams) => {
+const useSearchVideos = ({ id, onError }: UseSearchVideosParams) => {
   const [isSearchInputOpen, setIsSearchingInputOpen] = useState(false)
   const [isSearching, setIsSearching] = useState(false)
   const [searchQuery, setSearchQuery] = useState('')
-  const [searchVideo, { loading: loadingSearch, data: searchData, error: errorSearch }] = useSearchLazyQuery()
+  const [searchVideo, { loading: loadingSearch, data: searchData, error: errorSearch }] = useSearchLazyQuery({
+    onError,
+  })
   const searchInputRef = useRef<HTMLInputElement>(null)
   const search = useCallback(
     (searchQuery: string) => {

+ 11 - 12
src/views/viewer/HomeView.tsx

@@ -1,18 +1,18 @@
 import styled from '@emotion/styled'
-import { ErrorBoundary } from '@sentry/react'
 import { sub } from 'date-fns'
 import React from 'react'
 
 import useVideosConnection from '@/api/hooks/videosConnection'
 import {
-  ErrorFallback,
   InfiniteVideoGrid,
   InterruptedVideosGallery,
   LimitedWidthContainer,
   VideoHero,
+  ViewErrorFallback,
 } from '@/components'
 import { usePersonalDataStore } from '@/providers'
 import { transitions } from '@/shared/theme'
+import { Logger } from '@/utils/logger'
 
 const MIN_FOLLOWED_CHANNELS_VIDEOS = 16
 // last three months
@@ -31,7 +31,7 @@ export const HomeView: React.FC = () => {
         createdAt_gte: MIN_DATE_FOLLOWED_CHANNELS_VIDEOS,
       },
     },
-    { skip: !anyFollowedChannels }
+    { skip: !anyFollowedChannels, onError: (error) => Logger.captureError('Failed to fetch videos', 'HomeView', error) }
   )
 
   const followedChannelsVideosCount = videosConnection?.totalCount
@@ -39,21 +39,20 @@ export const HomeView: React.FC = () => {
     followedChannelsVideosCount && followedChannelsVideosCount > MIN_FOLLOWED_CHANNELS_VIDEOS
 
   if (error) {
-    throw error
+    return <ViewErrorFallback />
   }
+
   return (
     <LimitedWidthContainer big>
       <VideoHero />
       <Container className={transitions.names.slide}>
         <InterruptedVideosGallery />
-        <ErrorBoundary fallback={ErrorFallback}>
-          <StyledInfiniteVideoGrid
-            title={shouldShowFollowedChannels ? 'Recent Videos From Followed Channels' : 'Recent Videos'}
-            channelIdIn={shouldShowFollowedChannels ? channelIdIn : null}
-            createdAtGte={shouldShowFollowedChannels ? MIN_DATE_FOLLOWED_CHANNELS_VIDEOS : null}
-            ready={!loading}
-          />
-        </ErrorBoundary>
+        <StyledInfiniteVideoGrid
+          title={shouldShowFollowedChannels ? 'Recent Videos From Followed Channels' : 'Recent Videos'}
+          channelIdIn={shouldShowFollowedChannels ? channelIdIn : null}
+          createdAtGte={shouldShowFollowedChannels ? MIN_DATE_FOLLOWED_CHANNELS_VIDEOS : null}
+          ready={!loading}
+        />
       </Container>
     </LimitedWidthContainer>
   )

+ 14 - 13
src/views/viewer/SearchOverlayView/SearchResults/SearchResults.tsx

@@ -3,10 +3,11 @@ import React, { useMemo, useState } from 'react'
 
 import { useSearch } from '@/api/hooks'
 import { AssetAvailability, SearchQuery } from '@/api/queries'
-import { ChannelGrid, SkeletonLoaderVideoGrid, VideoGrid, ViewWrapper } from '@/components'
+import { ChannelGrid, SkeletonLoaderVideoGrid, VideoGrid, ViewErrorFallback, ViewWrapper } from '@/components'
 import { usePersonalDataStore } from '@/providers'
 import { Tabs } from '@/shared/components'
 import { sizes } from '@/shared/theme'
+import { Logger } from '@/utils/logger'
 
 import { AllResultsTab } from './AllResultsTab'
 import { EmptyFallback } from './EmptyFallback'
@@ -18,15 +19,18 @@ const tabs = ['all results', 'videos', 'channels']
 
 export const SearchResults: React.FC<SearchResultsProps> = ({ query }) => {
   const [selectedIndex, setSelectedIndex] = useState(0)
-  const { data, loading, error } = useSearch({
-    text: query,
-    limit: 50,
-    whereVideo: {
-      mediaAvailability_eq: AssetAvailability.Accepted,
-      thumbnailPhotoAvailability_eq: AssetAvailability.Accepted,
+  const { data, loading, error } = useSearch(
+    {
+      text: query,
+      limit: 50,
+      whereVideo: {
+        mediaAvailability_eq: AssetAvailability.Accepted,
+        thumbnailPhotoAvailability_eq: AssetAvailability.Accepted,
+      },
+      whereChannel: {},
     },
-    whereChannel: {},
-  })
+    { onError: (error) => Logger.captureError('Failed to fetch search results', 'SearchResults', error) }
+  )
 
   const getChannelsAndVideos = (loading: boolean, data: SearchQuery['search'] | undefined) => {
     if (loading || !data) {
@@ -48,10 +52,7 @@ export const SearchResults: React.FC<SearchResultsProps> = ({ query }) => {
     updateRecentSearches(id, 'channel')
   }
   if (error) {
-    throw error
-  }
-  if (!loading && !data) {
-    throw new Error(`There was a problem with your search...`)
+    return <ViewErrorFallback />
   }
 
   if (!loading && channels.length === 0 && videos.length === 0) {

+ 6 - 4
src/views/viewer/VideoView/VideoView.tsx

@@ -3,7 +3,7 @@ import React, { useCallback, useEffect, useState } from 'react'
 import { useParams } from 'react-router-dom'
 
 import { useAddVideoView, useVideo } from '@/api/hooks'
-import { ChannelLink, InfiniteVideoGrid } from '@/components'
+import { ChannelLink, InfiniteVideoGrid, ViewErrorFallback } from '@/components'
 import { absoluteRoutes } from '@/config/routes'
 import knownLicenses from '@/data/knownLicenses.json'
 import { useRouterQuery } from '@/hooks'
@@ -32,7 +32,9 @@ import {
 
 export const VideoView: React.FC = () => {
   const { id } = useParams()
-  const { loading, video, error } = useVideo(id)
+  const { loading, video, error } = useVideo(id, {
+    onError: (error) => Logger.captureError('Failed to load video data', 'VideoView', error),
+  })
   const { addVideoView } = useAddVideoView()
   const watchedVideos = usePersonalDataStore((state) => state.watchedVideos)
   const updateWatchedVideos = usePersonalDataStore((state) => state.actions.updateWatchedVideos)
@@ -76,7 +78,7 @@ export const VideoView: React.FC = () => {
         channelId,
       },
     }).catch((error) => {
-      Logger.warn('Failed to increase video views', { error })
+      Logger.captureError('Failed to increase video views', 'VideoView', error)
     })
   }, [addVideoView, videoId, channelId])
 
@@ -100,7 +102,7 @@ export const VideoView: React.FC = () => {
   }, [video?.id, handleTimeUpdate, updateWatchedVideos])
 
   if (error) {
-    throw error
+    return <ViewErrorFallback />
   }
 
   if (!loading && !video) {

+ 14 - 26
src/views/viewer/VideosView/VideosView.tsx

@@ -1,12 +1,12 @@
-import { ErrorBoundary } from '@sentry/react'
 import React, { useRef, useState } from 'react'
 import { useInView } from 'react-intersection-observer'
 
 import { useCategories, useVideos } from '@/api/hooks'
 import { VideoOrderByInput } from '@/api/queries'
-import { BackgroundPattern, ErrorFallback, TOP_NAVBAR_HEIGHT, VideoGallery } from '@/components'
+import { BackgroundPattern, TOP_NAVBAR_HEIGHT, VideoGallery, ViewErrorFallback } from '@/components'
 import { Text } from '@/shared/components'
 import { transitions } from '@/shared/theme'
+import { Logger } from '@/utils/logger'
 
 import {
   CategoriesVideosContainer,
@@ -21,20 +21,20 @@ import {
 
 export const VideosView: React.FC = () => {
   const [selectedCategoryId, setSelectedCategoryId] = useState<string | null>(null)
-  const { loading: categoriesLoading, categories, error: categoriesError } = useCategories()
-  const {
-    loading: featuredVideosLoading,
-    videos: featuredVideos,
-    error: featuredVideosError,
-    refetch: refetchFeaturedVideos,
-  } = useVideos(
+  const { loading: categoriesLoading, categories, error: categoriesError } = useCategories(undefined, {
+    onError: (error) => Logger.captureError('Failed to fetch categories', 'VideosView', error),
+  })
+  const { loading: featuredVideosLoading, videos: featuredVideos, error: videosError } = useVideos(
     {
       where: {
         isFeatured_eq: true,
       },
       orderBy: VideoOrderByInput.CreatedAtDesc,
     },
-    { notifyOnNetworkStatusChange: true }
+    {
+      notifyOnNetworkStatusChange: true,
+      onError: (error) => Logger.captureError('Failed to fetch videos', 'VideosView', error),
+    }
   )
 
   const topicsRef = useRef<HTMLHeadingElement>(null)
@@ -53,16 +53,10 @@ export const VideosView: React.FC = () => {
     }
   }
 
-  if (categoriesError) {
-    throw categoriesError
+  if (videosError || categoriesError) {
+    return <ViewErrorFallback />
   }
 
-  if (featuredVideosError) {
-    throw featuredVideosError
-  }
-
-  const hasFeaturedVideosError = featuredVideosError && !featuredVideosLoading
-
   return (
     <StyledViewWrapper>
       <BackgroundPattern />
@@ -70,11 +64,7 @@ export const VideosView: React.FC = () => {
         <Header variant="hero">Videos</Header>
         {featuredVideosLoading || featuredVideos?.length ? (
           <FeaturedVideosContainer>
-            {!hasFeaturedVideosError ? (
-              <VideoGallery title="Featured" loading={featuredVideosLoading} videos={featuredVideos || []} />
-            ) : (
-              <ErrorFallback error={featuredVideosError} resetError={() => refetchFeaturedVideos()} />
-            )}
+            <VideoGallery title="Featured" loading={featuredVideosLoading} videos={featuredVideos || []} />
           </FeaturedVideosContainer>
         ) : null}
         <CategoriesVideosContainer>
@@ -89,9 +79,7 @@ export const VideosView: React.FC = () => {
             onChange={handleCategoryChange}
             isAtTop={inView}
           />
-          <ErrorBoundary fallback={ErrorFallback}>
-            <StyledInfiniteVideoGrid categoryId={selectedCategoryId || undefined} ready={!!categories} />
-          </ErrorBoundary>
+          <StyledInfiniteVideoGrid categoryId={selectedCategoryId || undefined} ready={!!categories} />
         </CategoriesVideosContainer>
       </div>
     </StyledViewWrapper>

+ 2 - 2
src/views/viewer/ViewerLayout.tsx

@@ -5,7 +5,7 @@ import React, { useEffect, useState } from 'react'
 import { Route, Routes, useLocation, useMatch, useNavigate } from 'react-router-dom'
 import { CSSTransition, SwitchTransition } from 'react-transition-group'
 
-import { TOP_NAVBAR_HEIGHT, ViewErrorFallback, ViewerSidenav, ViewerTopbar } from '@/components'
+import { TOP_NAVBAR_HEIGHT, ViewErrorBoundary, ViewerSidenav, ViewerTopbar } from '@/components'
 import { absoluteRoutes, relativeRoutes } from '@/config/routes'
 import { transitions } from '@/shared/theme'
 import { RoutingState } from '@/types/routing'
@@ -56,7 +56,7 @@ export const ViewerLayout: React.FC = () => {
       <ViewerSidenav />
       <MainContainer>
         <ErrorBoundary
-          fallback={ViewErrorFallback}
+          fallback={ViewErrorBoundary}
           onReset={() => {
             navigate(absoluteRoutes.viewer.index())
           }}

+ 0 - 29
yarn.lock

@@ -3699,16 +3699,6 @@
     "@sentry/utils" "6.11.0"
     tslib "^1.9.3"
 
-"@sentry/integrations@^6.11.0":
-  version "6.11.0"
-  resolved "https://registry.yarnpkg.com/@sentry/integrations/-/integrations-6.11.0.tgz#da0706306b5ff13eaae5771095d7fc82c9f4c68b"
-  integrity sha512-CtSMW+PJlw+EunDJ+9LCWxRcXQIsUDlcUZOhFyAdmM5YIbg2V0IqHtnYg8X2sSGRO3aTEc7lOG5nF4lqr3R0yg==
-  dependencies:
-    "@sentry/types" "6.11.0"
-    "@sentry/utils" "6.11.0"
-    localforage "^1.8.1"
-    tslib "^1.9.3"
-
 "@sentry/minimal@6.11.0":
   version "6.11.0"
   resolved "https://registry.yarnpkg.com/@sentry/minimal/-/minimal-6.11.0.tgz#806d5512658370e40827b3e3663061db708fff33"
@@ -12067,11 +12057,6 @@ immediate@^3.2.3:
   resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.3.0.tgz#1aef225517836bcdf7f2a2de2600c79ff0269266"
   integrity sha512-HR7EVodfFUdQCTIeySw+WDRFJlPcLOJbXfwwZ7Oom6tjsvZ3bOkCDJHehQC3nxJrv7+f9XecwazynjU8e4Vw3Q==
 
-immediate@~3.0.5:
-  version "3.0.6"
-  resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b"
-  integrity sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=
-
 immediate@~3.2.3:
   version "3.2.3"
   resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.2.3.tgz#d140fa8f614659bd6541233097ddaac25cdd991c"
@@ -14163,13 +14148,6 @@ levn@~0.3.0:
     prelude-ls "~1.1.2"
     type-check "~0.3.2"
 
-lie@3.1.1:
-  version "3.1.1"
-  resolved "https://registry.yarnpkg.com/lie/-/lie-3.1.1.tgz#9a436b2cc7746ca59de7a41fa469b3efb76bd87e"
-  integrity sha1-mkNrLMd0bKWd56QfpGmz77dr2H4=
-  dependencies:
-    immediate "~3.0.5"
-
 lines-and-columns@^1.1.6:
   version "1.1.6"
   resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00"
@@ -14307,13 +14285,6 @@ loader-utils@^1.0.2, loader-utils@^1.1.0, loader-utils@^1.2.3, loader-utils@^1.4
     emojis-list "^3.0.0"
     json5 "^1.0.1"
 
-localforage@^1.8.1:
-  version "1.9.0"
-  resolved "https://registry.yarnpkg.com/localforage/-/localforage-1.9.0.tgz#f3e4d32a8300b362b4634cc4e066d9d00d2f09d1"
-  integrity sha512-rR1oyNrKulpe+VM9cYmcFn6tsHuokyVHFaCM3+osEmxaHTbEk8oQu6eGDfS6DQLWi/N67XRmB8ECG37OES368g==
-  dependencies:
-    lie "3.1.1"
-
 locate-path@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e"

Nem az összes módosított fájl került megjelenítésre, mert túl sok fájl változott