From 9edccb378ca493b484f2b7dc2a985d439de58dcb Mon Sep 17 00:00:00 2001 From: Valentin Date: Tue, 22 Oct 2024 00:10:05 +0300 Subject: [PATCH] Catch errors in react server components (#4857) * Catch errors in react server components * clean ScoutProfile * Log in the FE that it was an error * Update --- .../app/(general)/notifications/page.tsx | 11 ++--- .../PublicScoutProfile/PublicScoutProfile.tsx | 24 +++++----- .../components/common/ErrorSSRMessage.tsx | 17 +++++++ .../TodaysHotBuildersCarousel.tsx | 9 +++- .../HomePageTable/HomePageTable.tsx | 13 +++--- .../PointsClaimScreen/PointsClaimScreen.tsx | 10 ++++- .../components/ScoutProfile/ScoutProfile.tsx | 30 ++++++------- .../components/ScoutPageBuildersGallery.tsx | 24 +++++++--- apps/scoutgame/lib/scouts/findScoutOrThrow.ts | 12 +++++ .../lib/scouts/getUserSeasonStats.ts | 16 +++++++ apps/scoutgame/lib/utils/async.ts | 45 +++++++++++++++++++ .../src/notifications/getNotifications.ts | 6 ++- 12 files changed, 169 insertions(+), 48 deletions(-) create mode 100644 apps/scoutgame/components/common/ErrorSSRMessage.tsx create mode 100644 apps/scoutgame/lib/scouts/findScoutOrThrow.ts create mode 100644 apps/scoutgame/lib/scouts/getUserSeasonStats.ts create mode 100644 apps/scoutgame/lib/utils/async.ts diff --git a/apps/scoutgame/app/(general)/notifications/page.tsx b/apps/scoutgame/app/(general)/notifications/page.tsx index fc9903bffe..98a5d77cb0 100644 --- a/apps/scoutgame/app/(general)/notifications/page.tsx +++ b/apps/scoutgame/app/(general)/notifications/page.tsx @@ -2,15 +2,16 @@ import { getSession } from '@connect-shared/lib/session/getSession'; import { getNotifications } from '@packages/scoutgame/notifications/getNotifications'; import { NotificationsPage } from 'components/notifications/NotificationsPage'; +import { safeAwaitSSRData } from 'lib/utils/async'; export default async function Page() { const { scoutId } = await getSession(); - const notifications = scoutId - ? await getNotifications({ - userId: scoutId - }) - : []; + const [, notifications = []] = await safeAwaitSSRData( + getNotifications({ + userId: scoutId + }) + ); return ; } diff --git a/apps/scoutgame/components/[username]/components/PublicScoutProfile/PublicScoutProfile.tsx b/apps/scoutgame/components/[username]/components/PublicScoutProfile/PublicScoutProfile.tsx index 62d30112f5..16976f6cfe 100644 --- a/apps/scoutgame/components/[username]/components/PublicScoutProfile/PublicScoutProfile.tsx +++ b/apps/scoutgame/components/[username]/components/PublicScoutProfile/PublicScoutProfile.tsx @@ -1,23 +1,27 @@ -import { prisma } from '@charmverse/core/prisma-client'; +import 'server-only'; +import { ErrorSSRMessage } from 'components/common/ErrorSSRMessage'; +import { findScoutOrThrow } from 'lib/scouts/findScoutOrThrow'; import { getScoutedBuilders } from 'lib/scouts/getScoutedBuilders'; import { getScoutStats } from 'lib/scouts/getScoutStats'; import type { BasicUserInfo } from 'lib/users/interfaces'; -import { BasicUserInfoSelect } from 'lib/users/queries'; +import { safeAwaitSSRData } from 'lib/utils/async'; import { PublicScoutProfileContainer } from './PublicScoutProfileContainer'; export async function PublicScoutProfile({ publicUser }: { publicUser: BasicUserInfo }) { - const [scout, { allTimePoints, seasonPoints, nftsPurchased }, scoutedBuilders] = await Promise.all([ - prisma.scout.findUniqueOrThrow({ - where: { - id: publicUser.id - }, - select: BasicUserInfoSelect - }), + const allPromises = [ + findScoutOrThrow(publicUser.id), getScoutStats(publicUser.id), getScoutedBuilders({ scoutId: publicUser.id }) - ]); + ] as const; + const [error, data] = await safeAwaitSSRData(Promise.all(allPromises)); + + if (error) { + return ; + } + + const [scout, { allTimePoints, seasonPoints, nftsPurchased }, scoutedBuilders] = data; return ( { + log.error('Error in SSR data fetching. Please refresh.'); + }, []); + + return ( + + {message || 'An error occured while loading your data. Please try to refresh or contact us on discord'} + + ); +} diff --git a/apps/scoutgame/components/home/components/BuildersCarousel/TodaysHotBuildersCarousel.tsx b/apps/scoutgame/components/home/components/BuildersCarousel/TodaysHotBuildersCarousel.tsx index ed8bac2cb5..593bdf3ad0 100644 --- a/apps/scoutgame/components/home/components/BuildersCarousel/TodaysHotBuildersCarousel.tsx +++ b/apps/scoutgame/components/home/components/BuildersCarousel/TodaysHotBuildersCarousel.tsx @@ -1,10 +1,17 @@ 'use server'; +import { ErrorSSRMessage } from 'components/common/ErrorSSRMessage'; import { getTodaysHotBuilders } from 'lib/builders/getTodaysHotBuilders'; +import { safeAwaitSSRData } from 'lib/utils/async'; import { BuildersCarousel } from './BuildersCarousel'; export async function TodaysHotBuildersCarousel() { - const builders = await getTodaysHotBuilders(); + const [error, builders] = await safeAwaitSSRData(getTodaysHotBuilders()); + + if (error) { + return ; + } + return ; } diff --git a/apps/scoutgame/components/home/components/HomePageTable/HomePageTable.tsx b/apps/scoutgame/components/home/components/HomePageTable/HomePageTable.tsx index 82474b9f50..ee751b371d 100644 --- a/apps/scoutgame/components/home/components/HomePageTable/HomePageTable.tsx +++ b/apps/scoutgame/components/home/components/HomePageTable/HomePageTable.tsx @@ -1,9 +1,10 @@ -'use server'; +import 'server-only'; import { getBuilderActivities } from 'lib/builders/getBuilderActivities'; import { getLeaderboard } from 'lib/builders/getLeaderboard'; import { getTopBuilders } from 'lib/builders/getTopBuilders'; import { getTopScouts } from 'lib/scouts/getTopScouts'; +import { safeAwaitSSRData } from 'lib/utils/async'; import { ActivityTable } from './components/ActivityTable'; import { LeaderboardTable } from './components/LeaderboardTable'; @@ -12,23 +13,23 @@ import { TopScoutsTable } from './components/TopScoutsTable'; export async function HomeTab({ tab }: { tab: string }) { if (tab === 'activity') { - const activities = await getBuilderActivities({ limit: 100 }); + const [, activities = []] = await safeAwaitSSRData(getBuilderActivities({ limit: 100 })); return ; } if (tab === 'top-scouts') { - const topScouts = await getTopScouts({ limit: 200 }); + const [, topScouts = []] = await safeAwaitSSRData(getTopScouts({ limit: 200 })); return ; } if (tab === 'top-builders') { - const topBuilders = await getTopBuilders({ limit: 200 }); + const [, topBuilders = []] = await safeAwaitSSRData(getTopBuilders({ limit: 200 })); return ; } if (tab === 'leaderboard') { - const data = await getLeaderboard({ limit: 200 }); - return ; + const [, leaderboard = []] = await safeAwaitSSRData(getLeaderboard({ limit: 200 })); + return ; } return null; } diff --git a/apps/scoutgame/components/profile/components/PointsClaimScreen/PointsClaimScreen.tsx b/apps/scoutgame/components/profile/components/PointsClaimScreen/PointsClaimScreen.tsx index 23277e1b9a..afd4f5e1da 100644 --- a/apps/scoutgame/components/profile/components/PointsClaimScreen/PointsClaimScreen.tsx +++ b/apps/scoutgame/components/profile/components/PointsClaimScreen/PointsClaimScreen.tsx @@ -3,14 +3,22 @@ import { Box, Paper, Typography, Stack } from '@mui/material'; import Image from 'next/image'; +import { ErrorSSRMessage } from 'components/common/ErrorSSRMessage'; import { getClaimablePointsWithEvents } from 'lib/points/getClaimablePointsWithEvents'; +import { safeAwaitSSRData } from 'lib/utils/async'; import { BonusPartnersDisplay } from './BonusPartnersDisplay'; import { PointsClaimButton } from './PointsClaimButton'; import { QualifiedActionsTable } from './QualifiedActionsTable'; export async function PointsClaimScreen({ userId, username }: { userId: string; username: string }) { - const { totalClaimablePoints, weeklyRewards, bonusPartners } = await getClaimablePointsWithEvents(userId); + const [error, data] = await safeAwaitSSRData(getClaimablePointsWithEvents(userId)); + + if (error) { + return ; + } + + const { totalClaimablePoints, weeklyRewards, bonusPartners } = data; if (!totalClaimablePoints) { return ( diff --git a/apps/scoutgame/components/profile/components/ScoutProfile/ScoutProfile.tsx b/apps/scoutgame/components/profile/components/ScoutProfile/ScoutProfile.tsx index 4108db0b30..6e867b2104 100644 --- a/apps/scoutgame/components/profile/components/ScoutProfile/ScoutProfile.tsx +++ b/apps/scoutgame/components/profile/components/ScoutProfile/ScoutProfile.tsx @@ -1,29 +1,25 @@ 'use server'; -import { prisma } from '@charmverse/core/prisma-client'; import { Typography, Stack } from '@mui/material'; -import { currentSeason } from '@packages/scoutgame/dates'; +import { ErrorSSRMessage } from 'components/common/ErrorSSRMessage'; import { BuildersGallery } from 'components/common/Gallery/BuildersGallery'; import { getScoutedBuilders } from 'lib/scouts/getScoutedBuilders'; +import { getUserSeasonStats } from 'lib/scouts/getUserSeasonStats'; +import { safeAwaitSSRData } from 'lib/utils/async'; import { ScoutStats } from './ScoutStats'; -export async function ScoutProfile({ userId }: { userId: string }) { - const [seasonStats, scoutedBuilders] = await Promise.all([ - prisma.userSeasonStats.findUnique({ - where: { - userId_season: { - userId, - season: currentSeason - } - }, - select: { - pointsEarnedAsScout: true - } - }), - getScoutedBuilders({ scoutId: userId }) - ]); +export async function ScoutProfile({ userId, isMobile }: { userId: string; isMobile?: boolean }) { + const [error, data] = await safeAwaitSSRData( + Promise.all([getUserSeasonStats(userId), getScoutedBuilders({ scoutId: userId })]) + ); + + if (error) { + return ; + } + + const [seasonStats, scoutedBuilders] = data; const nftsPurchasedThisSeason = scoutedBuilders.reduce((acc, builder) => acc + (builder.nftsSoldToScout || 0), 0); diff --git a/apps/scoutgame/components/scout/components/ScoutPageBuildersGallery.tsx b/apps/scoutgame/components/scout/components/ScoutPageBuildersGallery.tsx index 4bdb3d5e01..61f75c88d7 100644 --- a/apps/scoutgame/components/scout/components/ScoutPageBuildersGallery.tsx +++ b/apps/scoutgame/components/scout/components/ScoutPageBuildersGallery.tsx @@ -2,19 +2,29 @@ import { currentSeason, getCurrentWeek } from '@packages/scoutgame/dates'; import type { BuildersSort } from 'lib/builders/getSortedBuilders'; import { getSortedBuilders } from 'lib/builders/getSortedBuilders'; +import { safeAwaitSSRData } from 'lib/utils/async'; import { BuildersGalleryContainer } from './BuildersGalleryContainer'; export const dynamic = 'force-dynamic'; export async function ScoutPageBuildersGallery({ sort, showHotIcon }: { sort: BuildersSort; showHotIcon: boolean }) { - const { builders, nextCursor } = await getSortedBuilders({ - sort: sort as BuildersSort, - limit: 15, - week: getCurrentWeek(), - season: currentSeason, - cursor: null - }); + const [error, data] = await safeAwaitSSRData( + getSortedBuilders({ + sort: sort as BuildersSort, + limit: 15, + week: getCurrentWeek(), + season: currentSeason, + cursor: null + }) + ); + + if (error) { + return null; + } + + const { builders, nextCursor } = data; + return ( = T | Promise; + +export async function safeAwait( + promise: Promise, + options?: { onSuccess?: (response: T) => MaybePromise; onError?: (error: Error) => MaybePromise } +): Promise<[error: Error] | [error: null, data: T]> { + try { + const response = await promise; + await options?.onSuccess?.(response); + return [null, response]; + } catch (_error: any) { + await options?.onError?.(_error); + return [_error]; + } +} + +export async function safeAwaitSSRData(promise: Promise) { + return safeAwait(promise, { + onError: async (err) => { + const session = await getSession(); + const headersList = headers(); + const fullUrl = headersList.get('referer') || ''; + + const isValidSystemError = isSystemError(err); + + const errorAsSystemError = isValidSystemError ? err : new UnknownError(err.stack ?? err); + + const loggedInfo = { + error: errorAsSystemError, + stack: err.stack, + userId: session.scoutId, + url: fullUrl, + ssr: true // Flag to identify that this error was thrown during SSR and can be looked up in DD + }; + + log.error('Server error fetching SSR data', loggedInfo); + } + }); +} diff --git a/packages/scoutgame/src/notifications/getNotifications.ts b/packages/scoutgame/src/notifications/getNotifications.ts index 7e14ba740c..30e5e8e36d 100644 --- a/packages/scoutgame/src/notifications/getNotifications.ts +++ b/packages/scoutgame/src/notifications/getNotifications.ts @@ -72,7 +72,11 @@ export type ScoutGameNotification = | BuilderRecipientStrikeNotification | ScoutRecipientStrikeNotification; -export async function getNotifications({ userId }: { userId: string }): Promise { +export async function getNotifications({ userId }: { userId?: string }): Promise { + if (!userId) { + return []; + } + if (!stringUtils.isUUID(userId)) { throw new InvalidInputError(`userId required for notifications`); }