import * as Sentry from "@sentry/nextjs";
import { NextPageContext } from "next";
import NextErrorComponent, { ErrorProps } from "next/error";

interface ErrorPageProps {
  err: Error;
  statusCode: number;
  hasGetInitialPropsRun: boolean;
}

interface AppErrorProps extends ErrorProps {
  err?: Error;
  hasGetInitialPropsRun?: boolean;
}

function MyError({ statusCode, hasGetInitialPropsRun, err }: ErrorPageProps) {
  if (!hasGetInitialPropsRun && err) {
    /*
     * getInitialProps is not called when an exception is thrown
     * at the top level of a module while it is being loaded.
     * As a workaround, we pass err via _app.js so it can be captured
     * Read more: https://github.com/vercel/next.js/issues/8592.
     */
    Sentry.captureException(err);
    // Flushing is not required in this case as it only happens on the client
  }

  return <NextErrorComponent statusCode={statusCode} />;
}

MyError.getInitialProps = async (context: NextPageContext) => {
  const errorInitialProps: AppErrorProps = await NextErrorComponent.getInitialProps(context);

  const { res, err, asPath } = context;

  /*
   * Workaround for https://github.com/vercel/next.js/issues/8592, mark when
   * getInitialProps has run
   */
  errorInitialProps.hasGetInitialPropsRun = true;

  // Returning early because we don't want to log 404 errors to Sentry.
  if (res?.statusCode === 404) {
    return errorInitialProps;
  }

  /*
   * Running on the server, the response object (`res`) is available.
   *
   * Next.js will pass an err on the server if a page's data fetching methods
   * threw or returned a Promise that rejected
   *
   * Running on the client (browser), Next.js will provide an err if:
   *
   *  - a page's `getInitialProps` threw or returned a Promise that rejected
   *  - an exception was thrown somewhere in the React lifecycle (render,
   *    componentDidMount, etc) that was caught by Next.js's React Error
   *    Boundary. Read more about what types of exceptions are caught by Error
   *    Boundaries: https://reactjs.org/docs/error-boundaries.html
   */

  if (err) {
    Sentry.captureException(err);

    /*
     * Flushing before returning is necessary if deploying to Vercel, see
     * https://vercel.com/docs/platform/limits#streaming-responses
     */
    await Sentry.flush(2000);

    return errorInitialProps;
  }

  /*
   * If this point is reached, getInitialProps was called without any
   * information about what the error might be. This is unexpected and may
   * indicate a bug introduced in Next.js, so record it in Sentry
   */
  Sentry.captureException(new Error(`_error.js getInitialProps missing data at path: ${asPath}`));
  await Sentry.flush(2000);

  return errorInitialProps;
};

export default MyError;
