Skip to content

Commit b78b032

Browse files
committed
Merge branch 'v2-mst-aptd-at-lcz-sty' into storybook
# Conflicts: # src/components/errors/DefaultErrorLayout.tsx
2 parents 48c2879 + 57c2c60 commit b78b032

File tree

11 files changed

+188
-46
lines changed

11 files changed

+188
-46
lines changed

.github/workflows/deploy-vercel-production.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ on:
3838
- 'main'
3939

4040
# Allow manual trigger via a button in github or a HTTP call - See https://docs.github.com/en/actions/configuring-and-managing-workflows/configuring-a-workflow#manually-running-a-workflow
41-
# XXX Read more about how to use it with NRN in .github/WORKFLOW_DISPATCH.md
41+
# XXX See https://unlyed.github.io/next-right-now/guides/ci-cd/gha-deploy-vercel#triggering-the-action-remotely-using-workflow_dispatch
4242
workflow_dispatch:
4343
inputs:
4444
customer:

.github/workflows/deploy-vercel-staging.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ on:
3939
- 'main'
4040

4141
# Allow manual trigger via a button in github or a HTTP call - See https://docs.github.com/en/actions/configuring-and-managing-workflows/configuring-a-workflow#manually-running-a-workflow
42-
# XXX Read more about how to use it with NRN in .github/WORKFLOW_DISPATCH.md
42+
# XXX See https://unlyed.github.io/next-right-now/guides/ci-cd/gha-deploy-vercel#triggering-the-action-remotely-using-workflow_dispatch
4343
workflow_dispatch:
4444
inputs:
4545
customer:

src/components/appBootstrap/MultiversalAppBootstrap.tsx

+119-22
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import {
3030
import { initCustomerTheme } from '../../utils/data/theme';
3131
import i18nextLocize from '../../utils/i18n/i18nextLocize';
3232
import { configureSentryI18n } from '../../utils/monitoring/sentry';
33+
import getComponentName from '../../utils/nextjs/getComponentName';
3334
import {
3435
startPreviewMode,
3536
stopPreviewMode,
@@ -38,7 +39,6 @@ import { detectLightHouse } from '../../utils/quality/lighthouse';
3839
import { detectCypress } from '../../utils/testing/cypress';
3940
import Loader from '../animations/Loader';
4041
import DefaultErrorLayout from '../errors/DefaultErrorLayout';
41-
import ErrorDebug from '../errors/ErrorDebug';
4242
import BrowserPageBootstrap, { BrowserPageBootstrapProps } from './BrowserPageBootstrap';
4343
import MultiversalGlobalStyles from './MultiversalGlobalStyles';
4444
import ServerPageBootstrap, { ServerPageBootstrapProps } from './ServerPageBootstrap';
@@ -65,19 +65,78 @@ const MultiversalAppBootstrap: React.FunctionComponent<Props> = (props): JSX.Ele
6565
} = props;
6666
// When using SSG with "fallback: true" and the page hasn't been generated yet then isSSGFallbackInitialBuild is true
6767
const [isSSGFallbackInitialBuild] = useState<boolean>(isEmpty(pageProps) && router?.isFallback === true);
68+
const pageComponentName = getComponentName(props.Component);
6869

6970
Sentry.addBreadcrumb({ // See https://docs.sentry.io/enriching-error-data/breadcrumbs
7071
category: fileLabel,
7172
message: `Rendering ${fileLabel}`,
7273
level: Sentry.Severity.Debug,
7374
});
7475

76+
// Configure meaningful Next.js props in Sentry for easier debugging (all errors will report the props being passed to the page)
77+
// Filter out all entities that are too large, which might cause Sentry to fail sending the request
78+
Sentry.configureScope((scope): void => {
79+
const {
80+
Component,
81+
pageProps,
82+
err,
83+
router,
84+
...restProps
85+
} = props;
86+
const {
87+
asPath,
88+
basePath,
89+
defaultLocale,
90+
isFallback,
91+
isSsr,
92+
locale,
93+
locales,
94+
pathname,
95+
query,
96+
...restRouter // Other router props aren't interesting to track and are being ignored
97+
} = router;
98+
const {
99+
serializedDataset, // Size might be too big
100+
i18nTranslations, // Size might be too big
101+
...restPageProps
102+
} = pageProps; // XXX Exclude all non-meaningful props that might be too large for Sentry to handle, to avoid "403 Entity too large"
103+
const serializedDatasetLength = (serializedDataset || '').length;
104+
105+
// Track meaningful _app.props + unknown props. Other props (router, pageProps) will be tracked in another Sentry context for readability (DX)
106+
scope.setContext('_app.props (filtered)', {
107+
pageComponentName: pageComponentName,
108+
err: props.err,
109+
...restProps,
110+
});
111+
scope.setTag('hasCaughtNextErr', !!props?.err);
112+
scope.setContext('_app.router (filtered)', {
113+
asPath,
114+
basePath,
115+
defaultLocale,
116+
isFallback,
117+
isSsr,
118+
locale,
119+
locales,
120+
pathname,
121+
query,
122+
});
123+
scope.setContext('_app.pageProps (filtered)', {
124+
serializedDatasetLength,
125+
...restPageProps,
126+
});
127+
scope.setContext('build', {
128+
buildTime: process.env.NEXT_PUBLIC_BUILD_TIME,
129+
buildTimeISO: (new Date(process.env.NEXT_PUBLIC_APP_BUILD_TIME || null)).toISOString(),
130+
buildId: process.env.NEXT_PUBLIC_APP_BUILD_ID,
131+
});
132+
});
133+
75134
if (isBrowser() && process.env.NEXT_PUBLIC_APP_STAGE !== 'production') { // Avoids log clutter on server
76135
console.debug('MultiversalAppBootstrap.props', props); // eslint-disable-line no-console
77136
}
78137

79138
// Display a loader (we could use a skeleton too) when this happens, so that the user doesn't face a white page until the page is generated and displayed
80-
if (isSSGFallbackInitialBuild && router.isFallback) { // When router.isFallback becomes "false", then it'll mean the page has been generated and rendered and we can display it, instead of the loader
139+
if (isSSGFallbackInitialBuild && router?.isFallback) { // When router.isFallback becomes "false", then it'll mean the page has been generated and rendered and we can display it, instead of the loader
81140
return (
82141
<Loader />
83142
);
@@ -96,18 +155,50 @@ const MultiversalAppBootstrap: React.FunctionComponent<Props> = (props): JSX.Ele
96155
}: SSGPageProps | SSRPageProps = pageProps;
97156
configureSentryI18n(lang, locale);
98157

158+
// Unrecoverable error, we can't even display the layout because we don't have the minimal required information to properly do so.
159+
// The reason can be a UI crash (something broke due to the user's interaction) and a top-level error was thrown in props.err.
160+
// Or, it can be because no serializedDataset was provided.
161+
// Either way, we display the error page, which will take care of reporting the error to Sentry and display an error message depending on the environment.
99162
if (typeof serializedDataset !== 'string') {
100-
return (
101-
<ErrorDebug
102-
error={new Error(`Fatal error - Unexpected "serializedDataset" passed as page props.\n
163+
// eslint-disable-next-line no-console
164+
console.log('props', props);
165+
166+
if (props.err) {
167+
const error = new Error(`Fatal error - A top-level error was thrown by the application, which caused the Page.props to be lost. \n
168+
The page cannot be shown to the end-user, an error page will be displayed.`);
169+
logger.error(error);
170+
171+
return (
172+
<ErrorPage
173+
err={props.err}
174+
statusCode={500}
175+
isReadyToRender={true}
176+
>
177+
<DefaultErrorLayout
178+
error={props.err}
179+
context={pageProps}
180+
/>
181+
</ErrorPage>
182+
);
183+
} else {
184+
const error = new Error(`Fatal error - Unexpected "serializedDataset" passed as page props.\n
103185
Expecting string, but got "${typeof serializedDataset}".\n
104186
This error is often caused by returning an invalid "serializedDataset" from a getStaticProps/getServerSideProps.\n
105-
Make sure you return a correct value, using "serializeSafe".`)}
106-
context={{
107-
pageProps,
108-
}}
109-
/>
110-
);
187+
Make sure you return a correct value, using "serializeSafe".`);
188+
189+
return (
190+
<ErrorPage
191+
err={error}
192+
statusCode={500}
193+
isReadyToRender={true}
194+
>
195+
<DefaultErrorLayout
196+
error={error}
197+
context={pageProps}
198+
/>
199+
</ErrorPage>
200+
);
201+
}
111202
}
112203

113204
if (process.env.NEXT_PUBLIC_APP_STAGE !== 'production') {
@@ -182,33 +273,39 @@ const MultiversalAppBootstrap: React.FunctionComponent<Props> = (props): JSX.Ele
182273
// E.g: This will happens when an instance was deployed for a customer, but the customer.ref was changed since then.
183274
if (process.env.NEXT_PUBLIC_CUSTOMER_REF !== customer?.ref) {
184275
error = new Error(process.env.NEXT_PUBLIC_APP_STAGE === 'production' ?
185-
`An error happened, the app cannot start. (customer doesn't match)` :
186-
`Fatal error when bootstraping the app. The "customer.ref" doesn't match (expected: "${process.env.NEXT_PUBLIC_CUSTOMER_REF}", received: "${customer?.ref}".`,
276+
`Fatal error - An error happened, the page cannot be displayed. (customer doesn't match)` :
277+
`Fatal error when bootstrapping the app. The "customer.ref" doesn't match (expected: "${process.env.NEXT_PUBLIC_CUSTOMER_REF}", received: "${customer?.ref}".`,
187278
);
188279
} else {
189280
error = new Error(process.env.NEXT_PUBLIC_APP_STAGE === 'production' ?
190-
`An error happened, the app cannot start.` :
191-
`Fatal error when bootstraping the app. It might happen when lang/locale/translations couldn't be resolved.`,
281+
`Fatal error - An error happened, the page cannot be displayed.` :
282+
`Fatal error when bootstrapping the app. It might happen when lang/locale/translations couldn't be resolved.`,
192283
);
193284
}
194-
195-
// If the error wasn't detected by Next, then we log it to Sentry to make sure we'll be notified
196-
Sentry.withScope((scope): void => {
197-
scope.setContext('props', props);
198-
Sentry.captureException(error);
199-
});
200285
} else {
201286
// If an error was detected by Next, then it means the current state is due to a top-level that was caught before
202287
// We don't have anything to do, as it's automatically logged into Sentry
288+
const error = new Error(`Fatal error - Misconfiguration detected, the page cannot be displayed.`);
289+
logger.error(error);
203290
}
204291

205292
return (
206-
<ErrorPage err={error} statusCode={500} isReadyToRender={true}>
293+
<ErrorPage
294+
err={error}
295+
statusCode={500}
296+
isReadyToRender={true}
297+
>
207298
<DefaultErrorLayout
208299
error={error}
300+
context={pageProps}
209301
/>
210302
</ErrorPage>
211303
);
304+
} else if (props?.err) {
305+
// If an error was caught by Next.js (but wasn't fatal since we reached this point), we log it to Sentry to make sure we'll be notified
306+
Sentry.withScope((scope): void => {
307+
Sentry.captureException(props.err);
308+
});
212309
}
213310

214311
const i18nextInstance: i18n = i18nextLocize(lang, i18nTranslations); // Apply i18next configuration with Locize backend

src/components/errors/DefaultErrorLayout.tsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@ import { css } from '@emotion/react';
22
import * as Sentry from '@sentry/node';
33
import * as React from 'react';
44
import { Button } from 'reactstrap';
5+
import { GenericObject } from '../../types/GenericObject';
56

67
import ErrorDebug from './ErrorDebug';
78

89
export type Props = {
910
error: Error;
10-
context?: any; // eslint-disable-line @typescript-eslint/no-explicit-any
11+
context?: GenericObject;
1112
}
1213

1314
/**

src/components/errors/ErrorDebug.tsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import { css } from '@emotion/react';
22
import * as React from 'react';
33
import { Fragment } from 'react';
4+
import { GenericObject } from '../../types/GenericObject';
45

56
type Props = {
67
error?: Error;
7-
context?: any; // eslint-disable-line @typescript-eslint/no-explicit-any
8+
context?: GenericObject;
89
}
910

1011
/**

src/components/i18n/I18nLink.test.tsx

+16-2
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,25 @@ import I18nLink from './I18nLink';
1010
* @group components
1111
*/
1212
describe('I18nLink', () => {
13+
beforeEach(() => {
14+
global.console = global.muteConsole();
15+
});
16+
1317
const I18nLinkTest = (props) => {
14-
const { locale = 'en', href, text = 'Text', ...rest } = props;
18+
const {
19+
locale = 'en',
20+
href,
21+
text = 'Text',
22+
...rest
23+
} = props;
1524

1625
return (
17-
<i18nContext.Provider value={{ lang: null, locale: locale }}>
26+
<i18nContext.Provider
27+
value={{
28+
lang: null,
29+
locale: locale,
30+
}}
31+
>
1832
<I18nLink
1933
href={href}
2034
{...rest}

src/components/pageLayouts/DefaultLayout.tsx

+4
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,10 @@ const DefaultLayout: React.FunctionComponent<Props> = (props): JSX.Element => {
6767
level: Sentry.Severity.Debug,
6868
});
6969

70+
Sentry.configureScope((scope): void => {
71+
scope.setTag('fileLabel', fileLabel);
72+
});
73+
7074
return (
7175
<Amplitude
7276
eventProperties={(inheritedProps): GenericObject => ({

src/pages/[locale]/examples/built-in-utilities/errors-handling.tsx

+24-15
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ import BuiltInUtilitiesSidebar from '../../../../components/doc/BuiltInUtilities
1212
import DocPage from '../../../../components/doc/DocPage';
1313
import I18nLink from '../../../../components/i18n/I18nLink';
1414
import DefaultLayout from '../../../../components/pageLayouts/DefaultLayout';
15+
import Btn from '../../../../components/utils/Btn';
1516
import Code from '../../../../components/utils/Code';
16-
import ExternalLink from '../../../../components/utils/ExternalLink';
1717
import { CommonServerSideParams } from '../../../../types/nextjs/CommonServerSideParams';
1818
import { OnlyBrowserPageProps } from '../../../../types/pageProps/OnlyBrowserPageProps';
1919
import { SSGPageProps } from '../../../../types/pageProps/SSGPageProps';
@@ -81,19 +81,18 @@ const ErrorsHandlingPage: NextPage<Props> = (props): JSX.Element => {
8181
This page doesn't exist and should display a 404 page.
8282
</Alert>
8383

84-
<Alert color={'danger'}>
85-
Clicking on the link doesn't do anything, I don't know if it's meant to be a feature, but
86-
<ExternalLink href={'https://github.com/vercel/next.js/issues/13516'} suffix={null}>this is probably a bug</ExternalLink>.
87-
</Alert>
88-
8984
<p>
90-
<I18nLink href={'/404-csr'}>This is a client-side navigation (CSR)</I18nLink>
85+
<Btn mode={'primary-outline'}>
86+
<I18nLink href={'/404-csr'}>This is a client-side navigation (CSR)</I18nLink>
87+
</Btn>
9188
</p>
9289

9390
<Code
9491
text={`
95-
<I18nLink href={'/404-csr'}>This is a client-side navigation (CSR)</I18nLink>
96-
`}
92+
<Btn mode={'primary-outline'}>
93+
<I18nLink href={'/404-csr'}>This is a client-side navigation (CSR)</I18nLink>
94+
</Btn>
95+
`}
9796
/>
9897
<br />
9998

@@ -105,12 +104,16 @@ const ErrorsHandlingPage: NextPage<Props> = (props): JSX.Element => {
105104

106105
<p>
107106
<i>This is not CSR, it's not necessarily SSR either, it can be either static rendering or SSR.</i><br />
108-
<a href={'/404-static'}>This is a normal navigation</a>
107+
<Btn mode={'primary-outline'}>
108+
<a href={'/404-static'}>This is a normal navigation</a>
109+
</Btn>
109110
</p>
110111

111112
<Code
112113
text={`
113-
<a href={'/404-static'}>This is a normal navigation</a>
114+
<Btn mode={'primary-outline'}>
115+
<a href={'/404-static'}>This is a normal navigation</a>
116+
</Btn>
114117
`}
115118
/>
116119
<br />
@@ -149,16 +152,22 @@ const ErrorsHandlingPage: NextPage<Props> = (props): JSX.Element => {
149152
<br />
150153

151154
<p>
152-
<I18nLink href={'/examples/built-in-utilities/top-level-500-error'}>This is a client-side navigation (CSR)</I18nLink><br />
153-
<a href={'/examples/built-in-utilities/top-level-500-error'}>This is a normal navigation</a>
155+
<Btn mode={'primary-outline'}>
156+
<I18nLink href={'/examples/built-in-utilities/top-level-500-error'}>This is a client-side navigation (CSR)</I18nLink><br />
157+
</Btn>
158+
<Btn mode={'primary-outline'}>
159+
<a href={'/examples/built-in-utilities/top-level-500-error'}>This is a normal navigation</a>
160+
</Btn>
154161
</p>
155162
<br />
156163

157164
<hr />
158165

159-
<h2>500 - Interactive error</h2>
166+
<h2>Interactive error (simulating User interaction)</h2>
160167

161-
<I18nLink href={'/examples/built-in-utilities/interactive-error'}>Go to interactive error page</I18nLink><br />
168+
<Btn mode={'primary-outline'}>
169+
<I18nLink href={'/examples/built-in-utilities/interactive-error'}>Go to interactive error page</I18nLink><br />
170+
</Btn>
162171

163172
<br />
164173

src/types/nextjs/MultiversalAppBootstrapProps.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import {
22
NextComponentType,
33
NextPageContext,
44
} from 'next';
5-
import { NextRouter } from 'next/router';
5+
import { Router } from 'next/router';
66

77
import { MultiversalPageProps } from '../pageProps/MultiversalPageProps';
88

@@ -16,7 +16,7 @@ export type MultiversalAppBootstrapProps<PP extends MultiversalPageProps = Multi
1616
Component?: NextComponentType<NextPageContext>; // Page component, not provided if pageProps.statusCode is 3xx or 4xx
1717
err?: Error; // Only defined if there was an error
1818
pageProps?: PP; // Props forwarded to the Page component
19-
router?: NextRouter;
19+
router?: Router;
2020

2121
// XXX Next.js internals (unstable API) - See https://github.com/vercel/next.js/discussions/12558#discussioncomment-9177
2222
__N_SSG?: boolean; // Stands for "server-side generated" or "static site generation", indicates the page was generated through getStaticProps

0 commit comments

Comments
 (0)