Giter VIP home page Giter VIP logo

remix-effect-errors's Introduction

remix-effect-errors

Toying with remix and effect to get some fancy errors reporting using effect-errors.

           

⚡ So how does that work?

We basically need two things on remix to achieve our goal:

  • A custom remix loader accepting an effect and throwing effect errors details.
  • An Error boundary to display that information if an error occurs.

🔶 Creating a custom loader

import { type LoaderFunctionArgs } from '@remix-run/server-runtime';
import { Effect, pipe } from 'effect';
import { captureErrors, prettyPrint } from 'effect-errors';

import { getSpansDuration } from './logic/get-spans-duration';
import { remixThrow } from './logic/remix-throw';

export const effectLoader =
  <A, E>(effect: (args: LoaderFunctionArgs) => Effect.Effect<A, E>) =>
  async (args: LoaderFunctionArgs) =>
    await Effect.runPromise(
      pipe(
        effect(args),
        Effect.map((data) => ({ _tag: 'success' as const, data })),
        Effect.sandbox,
        Effect.catchAll((cause) => {
          // Serverside logging
          const errorsText = prettyPrint(cause, { stripCwd: true });
          console.error(errorsText);

          // Getting errors data to display it client side
          const { errors } = captureErrors(cause, {
            reverseSpans: true,
            stripCwd: true,
          });

          // Computing spans duration ...
          const errorsWithSpanDuration = errors.map(
            ({ errorType, message, stack, spans }) => ({
              type: errorType,
              message,
              stack,
              spans: getSpansDuration(spans),
            }),
          );

          return Effect.succeed({
            _tag: 'error' as const,
            data: errorsWithSpanDuration,
          });
        }),
      ),
    ).then(remixThrow);

We need to pipe on the promise because remix expects us to throw a json function result from the loader for errors:

import { json } from '@remix-run/server-runtime';
import { match } from 'ts-pattern';

import {
  EffectLoaderError,
  EffectLoaderSuccess,
} from '../types/effect-loader.types';

import { stringifyErrorsMessage } from './stringify-errors-message';

type RemixThrowInput<A> = EffectLoaderSuccess<A> | EffectLoaderError;

const effectHasSucceeded = <A>(
  p: RemixThrowInput<A>,
): p is EffectLoaderSuccess<A> => p._tag === 'success';

export const remixThrow = <A>(input: RemixThrowInput<A>) =>
  match(input)
    .when(
      (p) => effectHasSucceeded(p),
      ({ data }) => data,
    )
    .otherwise(({ data }) => {
      throw json(
        { type: 'effect', errors: stringifyErrorsMessage(data as never) },
        { status: 500 },
      );
    });

🔶 Creating an error boundary to display effect errors details

First, let's create a hook to get errors data:

import {
  isRouteErrorResponse,
  useLocation,
  useRouteError,
} from '@remix-run/react';

export interface EffectError {
  type?: string;
  message: string;
  stack?: string;
  spans?: {
    name: string;
    attributes: Record<string, unknown>;
    duration: bigint | undefined;
  }[];
}

const isEffectError = (
  error: unknown,
): error is {
  data: {
    type: 'effect';
    errors: EffectError[];
  };
} => (error as { data?: { type?: 'effect' } })?.data?.type === 'effect';

export const useErrorDetails = () => {
  const { pathname } = useLocation();
  const error = useRouteError();

  if (isEffectError(error)) {
    return {
      _tag: 'effect' as const,
      path: pathname,
      errors: error.data.errors,
    };
  }

  const isRoute = isRouteErrorResponse(error);
  if (isRoute) {
    return {
      _tag: 'route' as const,
      path: pathname,
      errors: [
        {
          message: `${error.statusText}`,
        },
      ],
    };
  }

  if (error instanceof Error) {
    return {
      _tag: 'error' as const,
      path: pathname,
      errors: [error],
    };
  }

  return {
    _tag: 'unknown' as const,
    path: pathname,
    errors: [{ message: 'Unknown Error' }],
  };
};

We can then focus on data display...

import { match } from 'ts-pattern';

import type { EffectError } from './useErrorDetails.code-sample';
import { useErrorDetails } from './useErrorDetails.code-sample';

type EffectErrorDetailsProps = Pick<EffectError, 'type' | 'message' | 'spans'>;

const EffectErrorDetails = ({
  type,
  message,
  spans,
}: EffectErrorDetailsProps) => (
  <li>
    {type} {message}{' '}
    {spans?.map(({ name, duration, attributes }, spanIndex) => (
      <div key={spanIndex}>
        <div>{name}</div>
        <div>{duration !== undefined ? `~ ${duration} ms` : ''}</div>
        <div>
          {Object.entries(attributes)
            .filter(([, value]) => value !== null)
            .map(([key, value], attributeNumber) => (
              <div key={attributeNumber}>
                <span>{key}</span>: {JSON.stringify(value)}
              </div>
            ))}
        </div>
      </div>
    ))}
  </li>
);

const isEffectErrors = (
  p: ReturnType<typeof useErrorDetails>,
): p is { _tag: 'effect'; path: string; errors: EffectError[] } =>
  p._tag === 'effect';

export const ErrorBoundary = () => {
  const errors = useErrorDetails();

  return match(errors)
    .when(isEffectErrors, ({ errors }) => (
      <ul>
        {errors.map((e, errorIndex) => (
          <EffectErrorDetails key={errorIndex} {...e} />
        ))}
      </ul>
    ))
    .otherwise(({ errors }) => (
      <ul>
        {errors.map(({ message }, errorIndex) => (
          <li key={errorIndex}>{message}</li>
        ))}
      </ul>
    ));
};

remix-effect-errors's People

Contributors

jpb06 avatar

Watchers

 avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.