pages/_app.tsx
// Polyfills
import 'intersection-observer';
import { useEffect, PropsWithChildren } from 'react';
import * as Sentry from '@sentry/nextjs';
import Router from 'next/router';
import Fingerprint2 from 'fingerprintjs2';
import FontFaceObserver from 'fontfaceobserver';
import hash from 'object-hash';
import LogRocket from 'logrocket';
import setupLogRocketReact from 'logrocket-react';
import ScrollUpButton from 'react-scroll-up-button';
import { clientTokens } from 'common/config/environment';
import { gtag } from 'common/utils/thirdParty/gtag';
import Nav from 'components/Nav/Nav';
import Footer from 'components/Footer/Footer';
import 'common/styles/globals.css';
import { AppProps } from 'next/app';
import NextErrorComponent from 'next/error';
const isProduction = process.env.NODE_ENV === 'production';
const fonts = [
{
fontFamily: 'Encode Sans',
url: 'https://fonts.googleapis.com/css?family=Encode+Sans:400,700',
},
{
fontFamily: 'DIN Condensed Bold',
// loading of this font is being handled by the @font-face rule on
// the global style sheet.
url: null,
},
];
function Layout({ children }: PropsWithChildren<unknown>) {
return (
<div>
<Nav />
<main>{children}</main>
<Footer />
<ScrollUpButton />
</div>
);
}
// Same test used by EFF for identifying users
// https://panopticlick.eff.org/
const setLogRocketFingerprint = () => {
Fingerprint2.get(components => {
const fingerprint = hash(components);
LogRocket.identify(fingerprint);
});
};
Router.events.on('routeChangeComplete', url => gtag.pageView(url));
// Fixes Next CSS route change bug: https://github.com/vercel/next-plugins/issues/282
if (!isProduction) {
Router.events.on('routeChangeComplete', () => {
const path = '/_next/static/chunks/styles.chunk.module.css';
const chunksSelector = `link[href*="${path}"]:not([rel=preload])`;
const chunksNodes: NodeListOf<HTMLAnchorElement> = document.querySelectorAll(chunksSelector);
if (chunksNodes.length) {
const timestamp = new Date().valueOf();
chunksNodes[0].href = `${path}?ts=${timestamp}`;
}
});
}
const App = ({ Component, pageProps, err }: AppProps & { err: NextErrorComponent }) => {
useEffect(() => {
/* Analytics */
// TODO: Leverage prod-build-time-only env vars instead of window check
if (isProduction && window.location.host.includes('operationcode.org')) {
LogRocket.init(`${clientTokens.LOGROCKET}/operation-code`);
// Every crash report will have a LogRocket session URL.
LogRocket.getSessionURL(sessionURL => {
Sentry.configureScope(scope => {
scope.setExtra('sessionURL', sessionURL);
});
});
setupLogRocketReact(LogRocket);
// Per library docs, Fingerprint2 should not run immediately
if ('requestIdleCallback' in window) {
requestIdleCallback(setLogRocketFingerprint);
} else {
setTimeout(setLogRocketFingerprint, 500);
}
}
/* Non-render blocking font load */
const observers = fonts.map(font => {
if (font.url) {
const link = document.createElement('link');
link.href = font.url;
link.rel = 'stylesheet';
document.head.append(link);
}
const observer = new FontFaceObserver(font.fontFamily);
return observer.load(null, 10000); // increase the max timeout from default 3s to 10s
});
Promise.all(observers)
.then(() => {
document.documentElement.classList.add('fonts-loaded');
})
.catch(() =>
Sentry.captureException('FontFaceObserver took too long to resolve. Ignore this.'),
);
}, []);
return (
<Layout>
{/** @see // Workaround for https://github.com/vercel/next.js/issues/8592 */}
<Component {...pageProps} err={err} />
</Layout>
);
};
export default App;