import * as Sentry from '@sentry/node'; import universalLanguageDetect from '@unly/universal-language-detector'; import { ERROR_LEVELS } from '@unly/universal-language-detector/lib/utils/error'; import { isBrowser } from '@unly/utils'; import { createLogger } from '@unly/utils-simple-logger'; import classnames from 'classnames'; import get from 'lodash.get'; import { NextPageContext } from 'next'; import NextCookies from 'next-cookies'; import Document, { DocumentContext, DocumentProps, Head, Main, NextScript } from 'next/document'; import React from 'react'; import { Cookies } from '../types/Cookies'; import { DocumentInitialProps } from '../types/DocumentInitialProps'; import { LANG_EN, SUPPORTED_LANGUAGES } from '../utils/i18n'; const fileLabel = 'pages/_document'; const logger = createLogger({ label: fileLabel, }); /** * Send to Sentry all unhandled rejections. * * If such error happens in this file, it will completely crash the server and render "Internal Server Error" on the client. * @see https://leerob.io/blog/configuring-sentry-for-nextjs-apps */ process.on('unhandledRejection', (e: Error): void => { Sentry.captureException(e); }); /** * Send to Sentry all uncaught exceptions. * * If such error happens in this file, it will completely crash the server and render "Internal Server Error" on the client. * @see https://leerob.io/blog/configuring-sentry-for-nextjs-apps */ process.on('uncaughtException', (e: Error): void => { Sentry.captureException(e); }); /** * XXX Is only rendered on the server side and not on the client side * * Used to inject <html lang=""> tag * * See https://github.com/zeit/next.js/#custom-document */ class NRNDocument extends Document<Props> { static async getInitialProps(ctx: DocumentContext): Promise<DocumentInitialProps> { try { Sentry.addBreadcrumb({ // See https://docs.sentry.io/enriching-error-data/breadcrumbs category: fileLabel, message: `Preparing document (${isBrowser() ? 'browser' : 'server'})`, level: Sentry.Severity.Debug, }); const initialProps: DocumentInitialProps = await Document.getInitialProps(ctx); const { req }: NextPageContext = ctx; const cookies: Cookies = NextCookies(ctx); // Parses Next.js cookies in a universal way (server + client) const lang: string = universalLanguageDetect({ supportedLanguages: SUPPORTED_LANGUAGES, // Whitelist of supported languages, will be used to filter out languages that aren't supported fallbackLanguage: LANG_EN, // Fallback language in case the user's language cannot be resolved acceptLanguageHeader: get(req, 'headers.accept-language'), // Optional - Accept-language header will be used when resolving the language on the server side serverCookies: cookies, // Optional - Cookie "i18next" takes precedence over navigator configuration (ex: "i18next: fr"), will only be used on the server side errorHandler: (error: Error, level: ERROR_LEVELS, origin: string, context: object): void => { Sentry.withScope((scope): void => { scope.setExtra('level', level); scope.setExtra('origin', origin); scope.setContext('context', context); Sentry.captureException(error); }); logger.error(error.message); }, }); return { ...initialProps, lang }; } catch (e) { // If an error happens, log it and then try to render the page again with minimal processing // This way, it'll render something on the client. (otherwise, it'd completely crash the server and render "Internal Server Error" on the client) Sentry.captureException(e); const initialProps: DocumentInitialProps = await Document.getInitialProps(ctx); return { ...initialProps }; } } render(): JSX.Element { const { lang }: DocumentProps & { lang: string } = this.props; return ( <html lang={this.props.lang}> <Head /> <body className={classnames('nrn', `lang-${lang}`)}> <Main /> <NextScript /> </body> </html> ); } } type Props = { lang: string; } & DocumentProps export default NRNDocument;