Skip to content

Commit

Permalink
Catch errors in react server components (#4857)
Browse files Browse the repository at this point in the history
* Catch errors in react server components

* clean ScoutProfile

* Log in the FE that it was an error

* Update
  • Loading branch information
valentinludu authored Oct 21, 2024
1 parent 0635898 commit 9edccb3
Show file tree
Hide file tree
Showing 12 changed files with 169 additions and 48 deletions.
11 changes: 6 additions & 5 deletions apps/scoutgame/app/(general)/notifications/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 <NotificationsPage notifications={notifications} />;
}
Original file line number Diff line number Diff line change
@@ -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 <ErrorSSRMessage />;
}

const [scout, { allTimePoints, seasonPoints, nftsPurchased }, scoutedBuilders] = data;

return (
<PublicScoutProfileContainer
Expand Down
17 changes: 17 additions & 0 deletions apps/scoutgame/components/common/ErrorSSRMessage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
'use client';

import { log } from '@charmverse/core/log';
import { Alert } from '@mui/material';
import { useEffect } from 'react';

export function ErrorSSRMessage({ message }: { message?: string }) {
useEffect(() => {
log.error('Error in SSR data fetching. Please refresh.');
}, []);

return (
<Alert severity='warning'>
{message || 'An error occured while loading your data. Please try to refresh or contact us on discord'}
</Alert>
);
}
Original file line number Diff line number Diff line change
@@ -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 <ErrorSSRMessage />;
}

return <BuildersCarousel builders={builders} />;
}
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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 <ActivityTable activities={activities} />;
}

if (tab === 'top-scouts') {
const topScouts = await getTopScouts({ limit: 200 });
const [, topScouts = []] = await safeAwaitSSRData(getTopScouts({ limit: 200 }));
return <TopScoutsTable scouts={topScouts} />;
}

if (tab === 'top-builders') {
const topBuilders = await getTopBuilders({ limit: 200 });
const [, topBuilders = []] = await safeAwaitSSRData(getTopBuilders({ limit: 200 }));
return <TopBuildersTable builders={topBuilders} />;
}

if (tab === 'leaderboard') {
const data = await getLeaderboard({ limit: 200 });
return <LeaderboardTable data={data} />;
const [, leaderboard = []] = await safeAwaitSSRData(getLeaderboard({ limit: 200 }));
return <LeaderboardTable data={leaderboard} />;
}
return null;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 <ErrorSSRMessage />;
}

const { totalClaimablePoints, weeklyRewards, bonusPartners } = data;

if (!totalClaimablePoints) {
return (
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <ErrorSSRMessage />;
}

const [seasonStats, scoutedBuilders] = data;

const nftsPurchasedThisSeason = scoutedBuilders.reduce((acc, builder) => acc + (builder.nftsSoldToScout || 0), 0);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<BuildersGalleryContainer
sort={sort}
Expand Down
12 changes: 12 additions & 0 deletions apps/scoutgame/lib/scouts/findScoutOrThrow.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { prisma } from '@charmverse/core/prisma-client';

import { BasicUserInfoSelect } from 'lib/users/queries';

export async function findScoutOrThrow(scoutId: string) {
return prisma.scout.findUniqueOrThrow({
where: {
id: scoutId
},
select: BasicUserInfoSelect
});
}
16 changes: 16 additions & 0 deletions apps/scoutgame/lib/scouts/getUserSeasonStats.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { prisma } from '@charmverse/core/prisma-client';
import { currentSeason } from '@packages/scoutgame/dates';

export async function getUserSeasonStats(_userId: string) {
return prisma.userSeasonStats.findUnique({
where: {
userId_season: {
userId: _userId,
season: currentSeason
}
},
select: {
pointsEarnedAsScout: true
}
});
}
45 changes: 45 additions & 0 deletions apps/scoutgame/lib/utils/async.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { UnknownError } from '@charmverse/core/errors';
import { log } from '@charmverse/core/log';
import { getSession } from '@connect-shared/lib/session/getSession';
import { isSystemError } from '@root/lib/middleware/isSystemError';
import { headers } from 'next/headers';

export type MaybePromise<T> = T | Promise<T>;

export async function safeAwait<T>(
promise: Promise<T>,
options?: { onSuccess?: (response: T) => MaybePromise<void>; onError?: (error: Error) => MaybePromise<void> }
): 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<T>(promise: Promise<T>) {
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);
}
});
}
6 changes: 5 additions & 1 deletion packages/scoutgame/src/notifications/getNotifications.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,11 @@ export type ScoutGameNotification =
| BuilderRecipientStrikeNotification
| ScoutRecipientStrikeNotification;

export async function getNotifications({ userId }: { userId: string }): Promise<ScoutGameNotification[]> {
export async function getNotifications({ userId }: { userId?: string }): Promise<ScoutGameNotification[]> {
if (!userId) {
return [];
}

if (!stringUtils.isUUID(userId)) {
throw new InvalidInputError(`userId required for notifications`);
}
Expand Down

0 comments on commit 9edccb3

Please sign in to comment.