Skip to content

Commit

Permalink
Merge branch 'v2-mst-aptd-at-lcz-sty' into storybook
Browse files Browse the repository at this point in the history
# Conflicts:
#	src/components/errors/DefaultErrorLayout.tsx
  • Loading branch information
Vadorequest committed Jan 14, 2021
2 parents 48c2879 + 57c2c60 commit b78b032
Show file tree
Hide file tree
Showing 11 changed files with 188 additions and 46 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/deploy-vercel-production.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ on:
- 'main'

# 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
# XXX Read more about how to use it with NRN in .github/WORKFLOW_DISPATCH.md
# XXX See https://unlyed.github.io/next-right-now/guides/ci-cd/gha-deploy-vercel#triggering-the-action-remotely-using-workflow_dispatch
workflow_dispatch:
inputs:
customer:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/deploy-vercel-staging.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ on:
- 'main'

# 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
# XXX Read more about how to use it with NRN in .github/WORKFLOW_DISPATCH.md
# XXX See https://unlyed.github.io/next-right-now/guides/ci-cd/gha-deploy-vercel#triggering-the-action-remotely-using-workflow_dispatch
workflow_dispatch:
inputs:
customer:
Expand Down
141 changes: 119 additions & 22 deletions src/components/appBootstrap/MultiversalAppBootstrap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
import { initCustomerTheme } from '../../utils/data/theme';
import i18nextLocize from '../../utils/i18n/i18nextLocize';
import { configureSentryI18n } from '../../utils/monitoring/sentry';
import getComponentName from '../../utils/nextjs/getComponentName';
import {
startPreviewMode,
stopPreviewMode,
Expand All @@ -38,7 +39,6 @@ import { detectLightHouse } from '../../utils/quality/lighthouse';
import { detectCypress } from '../../utils/testing/cypress';
import Loader from '../animations/Loader';
import DefaultErrorLayout from '../errors/DefaultErrorLayout';
import ErrorDebug from '../errors/ErrorDebug';
import BrowserPageBootstrap, { BrowserPageBootstrapProps } from './BrowserPageBootstrap';
import MultiversalGlobalStyles from './MultiversalGlobalStyles';
import ServerPageBootstrap, { ServerPageBootstrapProps } from './ServerPageBootstrap';
Expand All @@ -65,19 +65,78 @@ const MultiversalAppBootstrap: React.FunctionComponent<Props> = (props): JSX.Ele
} = props;
// When using SSG with "fallback: true" and the page hasn't been generated yet then isSSGFallbackInitialBuild is true
const [isSSGFallbackInitialBuild] = useState<boolean>(isEmpty(pageProps) && router?.isFallback === true);
const pageComponentName = getComponentName(props.Component);

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

// Configure meaningful Next.js props in Sentry for easier debugging (all errors will report the props being passed to the page)
// Filter out all entities that are too large, which might cause Sentry to fail sending the request
Sentry.configureScope((scope): void => {
const {
Component,
pageProps,
err,
router,
...restProps
} = props;
const {
asPath,
basePath,
defaultLocale,
isFallback,
isSsr,
locale,
locales,
pathname,
query,
...restRouter // Other router props aren't interesting to track and are being ignored
} = router;
const {
serializedDataset, // Size might be too big
i18nTranslations, // Size might be too big
...restPageProps
} = pageProps; // XXX Exclude all non-meaningful props that might be too large for Sentry to handle, to avoid "403 Entity too large"
const serializedDatasetLength = (serializedDataset || '').length;

// Track meaningful _app.props + unknown props. Other props (router, pageProps) will be tracked in another Sentry context for readability (DX)
scope.setContext('_app.props (filtered)', {
pageComponentName: pageComponentName,
err: props.err,
...restProps,
});
scope.setTag('hasCaughtNextErr', !!props?.err);
scope.setContext('_app.router (filtered)', {
asPath,
basePath,
defaultLocale,
isFallback,
isSsr,
locale,
locales,
pathname,
query,
});
scope.setContext('_app.pageProps (filtered)', {
serializedDatasetLength,
...restPageProps,
});
scope.setContext('build', {
buildTime: process.env.NEXT_PUBLIC_BUILD_TIME,
buildTimeISO: (new Date(process.env.NEXT_PUBLIC_APP_BUILD_TIME || null)).toISOString(),
buildId: process.env.NEXT_PUBLIC_APP_BUILD_ID,
});
});

if (isBrowser() && process.env.NEXT_PUBLIC_APP_STAGE !== 'production') { // Avoids log clutter on server
console.debug('MultiversalAppBootstrap.props', props); // eslint-disable-line no-console
}

// 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
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
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
return (
<Loader />
);
Expand All @@ -96,18 +155,50 @@ const MultiversalAppBootstrap: React.FunctionComponent<Props> = (props): JSX.Ele
}: SSGPageProps | SSRPageProps = pageProps;
configureSentryI18n(lang, locale);

// Unrecoverable error, we can't even display the layout because we don't have the minimal required information to properly do so.
// 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.
// Or, it can be because no serializedDataset was provided.
// 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.
if (typeof serializedDataset !== 'string') {
return (
<ErrorDebug
error={new Error(`Fatal error - Unexpected "serializedDataset" passed as page props.\n
// eslint-disable-next-line no-console
console.log('props', props);

if (props.err) {
const error = new Error(`Fatal error - A top-level error was thrown by the application, which caused the Page.props to be lost. \n
The page cannot be shown to the end-user, an error page will be displayed.`);
logger.error(error);

return (
<ErrorPage
err={props.err}
statusCode={500}
isReadyToRender={true}
>
<DefaultErrorLayout
error={props.err}
context={pageProps}
/>
</ErrorPage>
);
} else {
const error = new Error(`Fatal error - Unexpected "serializedDataset" passed as page props.\n
Expecting string, but got "${typeof serializedDataset}".\n
This error is often caused by returning an invalid "serializedDataset" from a getStaticProps/getServerSideProps.\n
Make sure you return a correct value, using "serializeSafe".`)}
context={{
pageProps,
}}
/>
);
Make sure you return a correct value, using "serializeSafe".`);

return (
<ErrorPage
err={error}
statusCode={500}
isReadyToRender={true}
>
<DefaultErrorLayout
error={error}
context={pageProps}
/>
</ErrorPage>
);
}
}

if (process.env.NEXT_PUBLIC_APP_STAGE !== 'production') {
Expand Down Expand Up @@ -182,33 +273,39 @@ const MultiversalAppBootstrap: React.FunctionComponent<Props> = (props): JSX.Ele
// E.g: This will happens when an instance was deployed for a customer, but the customer.ref was changed since then.
if (process.env.NEXT_PUBLIC_CUSTOMER_REF !== customer?.ref) {
error = new Error(process.env.NEXT_PUBLIC_APP_STAGE === 'production' ?
`An error happened, the app cannot start. (customer doesn't match)` :
`Fatal error when bootstraping the app. The "customer.ref" doesn't match (expected: "${process.env.NEXT_PUBLIC_CUSTOMER_REF}", received: "${customer?.ref}".`,
`Fatal error - An error happened, the page cannot be displayed. (customer doesn't match)` :
`Fatal error when bootstrapping the app. The "customer.ref" doesn't match (expected: "${process.env.NEXT_PUBLIC_CUSTOMER_REF}", received: "${customer?.ref}".`,
);
} else {
error = new Error(process.env.NEXT_PUBLIC_APP_STAGE === 'production' ?
`An error happened, the app cannot start.` :
`Fatal error when bootstraping the app. It might happen when lang/locale/translations couldn't be resolved.`,
`Fatal error - An error happened, the page cannot be displayed.` :
`Fatal error when bootstrapping the app. It might happen when lang/locale/translations couldn't be resolved.`,
);
}

// If the error wasn't detected by Next, then we log it to Sentry to make sure we'll be notified
Sentry.withScope((scope): void => {
scope.setContext('props', props);
Sentry.captureException(error);
});
} else {
// If an error was detected by Next, then it means the current state is due to a top-level that was caught before
// We don't have anything to do, as it's automatically logged into Sentry
const error = new Error(`Fatal error - Misconfiguration detected, the page cannot be displayed.`);
logger.error(error);
}

return (
<ErrorPage err={error} statusCode={500} isReadyToRender={true}>
<ErrorPage
err={error}
statusCode={500}
isReadyToRender={true}
>
<DefaultErrorLayout
error={error}
context={pageProps}
/>
</ErrorPage>
);
} else if (props?.err) {
// 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
Sentry.withScope((scope): void => {
Sentry.captureException(props.err);
});
}

const i18nextInstance: i18n = i18nextLocize(lang, i18nTranslations); // Apply i18next configuration with Locize backend
Expand Down
3 changes: 2 additions & 1 deletion src/components/errors/DefaultErrorLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@ import { css } from '@emotion/react';
import * as Sentry from '@sentry/node';
import * as React from 'react';
import { Button } from 'reactstrap';
import { GenericObject } from '../../types/GenericObject';

import ErrorDebug from './ErrorDebug';

export type Props = {
error: Error;
context?: any; // eslint-disable-line @typescript-eslint/no-explicit-any
context?: GenericObject;
}

/**
Expand Down
3 changes: 2 additions & 1 deletion src/components/errors/ErrorDebug.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { css } from '@emotion/react';
import * as React from 'react';
import { Fragment } from 'react';
import { GenericObject } from '../../types/GenericObject';

type Props = {
error?: Error;
context?: any; // eslint-disable-line @typescript-eslint/no-explicit-any
context?: GenericObject;
}

/**
Expand Down
18 changes: 16 additions & 2 deletions src/components/i18n/I18nLink.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,25 @@ import I18nLink from './I18nLink';
* @group components
*/
describe('I18nLink', () => {
beforeEach(() => {
global.console = global.muteConsole();
});

const I18nLinkTest = (props) => {
const { locale = 'en', href, text = 'Text', ...rest } = props;
const {
locale = 'en',
href,
text = 'Text',
...rest
} = props;

return (
<i18nContext.Provider value={{ lang: null, locale: locale }}>
<i18nContext.Provider
value={{
lang: null,
locale: locale,
}}
>
<I18nLink
href={href}
{...rest}
Expand Down
4 changes: 4 additions & 0 deletions src/components/pageLayouts/DefaultLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ const DefaultLayout: React.FunctionComponent<Props> = (props): JSX.Element => {
level: Sentry.Severity.Debug,
});

Sentry.configureScope((scope): void => {
scope.setTag('fileLabel', fileLabel);
});

return (
<Amplitude
eventProperties={(inheritedProps): GenericObject => ({
Expand Down
39 changes: 24 additions & 15 deletions src/pages/[locale]/examples/built-in-utilities/errors-handling.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ import BuiltInUtilitiesSidebar from '../../../../components/doc/BuiltInUtilities
import DocPage from '../../../../components/doc/DocPage';
import I18nLink from '../../../../components/i18n/I18nLink';
import DefaultLayout from '../../../../components/pageLayouts/DefaultLayout';
import Btn from '../../../../components/utils/Btn';
import Code from '../../../../components/utils/Code';
import ExternalLink from '../../../../components/utils/ExternalLink';
import { CommonServerSideParams } from '../../../../types/nextjs/CommonServerSideParams';
import { OnlyBrowserPageProps } from '../../../../types/pageProps/OnlyBrowserPageProps';
import { SSGPageProps } from '../../../../types/pageProps/SSGPageProps';
Expand Down Expand Up @@ -81,19 +81,18 @@ const ErrorsHandlingPage: NextPage<Props> = (props): JSX.Element => {
This page doesn't exist and should display a 404 page.
</Alert>

<Alert color={'danger'}>
Clicking on the link doesn't do anything, I don't know if it's meant to be a feature, but
<ExternalLink href={'https://github.com/vercel/next.js/issues/13516'} suffix={null}>this is probably a bug</ExternalLink>.
</Alert>

<p>
<I18nLink href={'/404-csr'}>This is a client-side navigation (CSR)</I18nLink>
<Btn mode={'primary-outline'}>
<I18nLink href={'/404-csr'}>This is a client-side navigation (CSR)</I18nLink>
</Btn>
</p>

<Code
text={`
<I18nLink href={'/404-csr'}>This is a client-side navigation (CSR)</I18nLink>
`}
<Btn mode={'primary-outline'}>
<I18nLink href={'/404-csr'}>This is a client-side navigation (CSR)</I18nLink>
</Btn>
`}
/>
<br />

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

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

<Code
text={`
<a href={'/404-static'}>This is a normal navigation</a>
<Btn mode={'primary-outline'}>
<a href={'/404-static'}>This is a normal navigation</a>
</Btn>
`}
/>
<br />
Expand Down Expand Up @@ -149,16 +152,22 @@ const ErrorsHandlingPage: NextPage<Props> = (props): JSX.Element => {
<br />

<p>
<I18nLink href={'/examples/built-in-utilities/top-level-500-error'}>This is a client-side navigation (CSR)</I18nLink><br />
<a href={'/examples/built-in-utilities/top-level-500-error'}>This is a normal navigation</a>
<Btn mode={'primary-outline'}>
<I18nLink href={'/examples/built-in-utilities/top-level-500-error'}>This is a client-side navigation (CSR)</I18nLink><br />
</Btn>
<Btn mode={'primary-outline'}>
<a href={'/examples/built-in-utilities/top-level-500-error'}>This is a normal navigation</a>
</Btn>
</p>
<br />

<hr />

<h2>500 - Interactive error</h2>
<h2>Interactive error (simulating User interaction)</h2>

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

<br />

Expand Down
4 changes: 2 additions & 2 deletions src/types/nextjs/MultiversalAppBootstrapProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {
NextComponentType,
NextPageContext,
} from 'next';
import { NextRouter } from 'next/router';
import { Router } from 'next/router';

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

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

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

1 comment on commit b78b032

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.