All Downloads are FREE. Search and download functionalities are using the official Maven repository.

helpers.report.tsx Maven / Gradle / Ivy

import merge from 'deepmerge';
import { T } from 'helpers/translator';
import { Notifier } from 'hooks/notification';
import React from 'react';
import Types from 'types/types';

import { ApolloError } from '@apollo/client';

import Notification, {
    NotificationProps, NotificationType
} from '../components/common/Notification';
import { t } from '../utils/i18n';
import { assertType, assertTypes } from '../utils/utils';

// TODO rename Report to Problem
export const enum ReportCategory { FRONTEND = 1, BACKEND = 2, INPUT = 3, USER = 4}
export const reportCategoryName = { 1: 'FRONTEND', 2: 'BACKEND', 3: 'INPUT', 4: 'USER' } as const

export const enum ReportLevel    {ERROR = 1, WARNING = 2, INFO = 3}
const ReportStatus = {
  frontend : {
    Whoops        : 1,
    NotSupported  : 2,
    Broken        : 3,
    Loading       : 4,
    NotAuthorized : 5,
    Apollo        : 6
  },
  backend : {
    Error   : 1,
    Offline : 2
  },
  input : {
    Error         : 1,
    loginError    : 2
  },
} as const

type ReportProps = {
  level:    ReportLevel     // ReportLevel
  category: ReportCategory  // Report Category
  status:   ReportStatus    // ReportStatus
  verbose:  string
  options:  ReportOptions
}

type ReportOptions = Partial<{
  component: string
}>

type ReportStatus = number

/** A report is used to store an issue. A status code is stored, issue level, and more.
 * A Report can be used to create a Notification, i.e., a visualisation of the issue that
 * is presented to the user.
 *
 */
class Report implements ReportProps  {
  options: ReportOptions
  verbose: string
  level: ReportLevel
  status: ReportStatus
  category: ReportCategory
  categoryName: string

  /**
  * @param {ReportLevel}    level    - the report level (ERROR, WARNING or INFO)
  * @param {ReportCategory} category - the report category (FRONTEND, BACKEND, USER)
  * @param {number}         status   - the status code (a number, e.g. 2 means operation not supported)
  * @param {object}         options  - additional information used for translation (an object containing e.g. a component name)
  * @param {string}         verbose  - additional html or string content (additional text that is displayed)
  */
  constructor(level: ReportLevel, category: ReportCategory, status: ReportStatus, options?: ReportOptions, verbose?: string) {
    this.options      = options || {}
    this.level        = level
    this.status       = status
    this.verbose      = verbose || ""
    this.category     = category
    this.categoryName = reportCategoryName[category]
  }

  static error(category: ReportCategory, status: ReportStatus, options?: ReportOptions, message?: string)   { return new Report(ReportLevel.ERROR,   category, status, options, message) }
  static warning(category: ReportCategory, status: ReportStatus, options?: ReportOptions, message?: string) { return new Report(ReportLevel.WARNING, category, status, options, message) }
  static info(category: ReportCategory, status: ReportStatus, options?: ReportOptions, message?: string)    { return new Report(ReportLevel.INFO,    category, status, options, message) }

  static get user()     { return ReportCategory.USER }
  static get frontend() { return ReportCategory.FRONTEND }
  static get backend()  { return ReportCategory.BACKEND }
  static get input()    { return ReportCategory.INPUT }

  static get code() { return ReportStatus }

  static isReport(obj: any) {
    return obj !== null && obj instanceof Report
  }

  static isJsError(error: any) {
    return Types.isObject(error) && (
      error.name === "RangeError"     || // a number is outside an allowable range of values
      error.name === "ReferenceError" || // variable is undefined
      error.name === "SyntaxError"    || // syntax error occurs during parsing/compile time
      error.name === "TypeError"      || //  occurs when an operation is performed on a wrong data type
      error.name === "URIError"       || // indicates that one of the global URI handling functions was used in a way that is incompatible with its definition.
      error.name === "EvalError"      || // identify errors when using the global eval() function.
      error.name === "InternalError"  || // This error occurs internally in the JS engine, especially when it has too much data to handle and the stack grows way over its critical limit."
      error.name === "AggregateError"    // an instance representing several errors wrapped in a single error when multiple errors need to be reported by an operation
    )
  }

  static toSpecErrors(error: any) {
    if (error instanceof ApolloError) {
      const graphQLErrors = error.graphQLErrors || []
      const errors        = graphQLErrors.filter(error => error?.extensions?.classification == "FROM_SPEC")

      return errors
    }
    else return []
  }

  /** create a suitable report given an error. */
  static from(error: any, translator: T | undefined, args: Partial, processKey?: string) {
    const t = (str: string) => {
      if (translator) {
        return translator.toError(processKey || "none", str, str)
      } else {
        str
      }
    }

    function toReport(error: any, args: Partial) {
      // create report

      const r = Report.defaults.Whoops.with(args)

      // prioritize spec errors
      if (error instanceof ApolloError) {
        const specErrors = Report.toSpecErrors(error)
        if (specErrors.length > 0) {
          const msg = specErrors[0].message
          return Report.error(Report.user, 1, {}, t(msg));
        } 
      }
      
      if (error instanceof Error) {
        // some reports need to be converted
        if (args.category == Report.backend) {
          if (error.message.match(/.*Invalid.*username.*password.*/i))
            return Report.error(Report.input, Report.code.input.loginError)
          else if (error.message.match(/failed to fetch.*/i))
            return Report.error(Report.backend, Report.code.backend.Offline)
          else if (error.message.match(/Internal Server Error.*/i))
            return Report.error(Report.backend, Report.code.backend.Error)
          else if (error.message.match(/exception while fetching.*/i))
            return Report.error(Report.frontend, Report.code.frontend.Apollo, {}, 
              t(error.message.replace(/exception while fetching(.*?):[\ ]*/i, "")))
          else
            return Report.error(Report.backend, Report.code.backend.Error, {}, t(error.message));
        }

        return new Report(r.level, r.category, r.status, r.options, t(error.message))
      } else if (typeof error == 'string') {
        return new Report(r.level, r.category, r.status, r.options, t(error))
      } else
        throw new Error("Cannot create report from provided data.")
    }

    // set defaults
    const defaultArgs = { category: Report.frontend }
    const nargs       = merge.all([defaultArgs, args])
    const report      = toReport(error, nargs)

    console.debug("From issue %o created %o", error.message, report.verboseMessage)
    return report
  }

  static equals(a: any, b: any) {
    if (Report.isReport(a) && Report.isReport(b))
      return a.status === b.status && a.category === b.category && a.level === b.level
    else
      return false
  }

  addToNotifier(notifier: Notifier, overrideOptions = {}) {
    const options = {
      type: this.notificationType,
      message: this.message,
      details: this.verbose,
      ...Types.asObject(overrideOptions)
    }

    switch (options.type) {
      case NotificationType.INFO:
        notifier.info(options.message)
        break
      case NotificationType.WARNING:
        notifier.warning(options.message)
        break
      case NotificationType.ERROR:
        notifier.error(options.message)
        break
      case NotificationType.CLOUD:
      default:
        notifier.message(options.message)
        break
    }
  }

  with(args: Partial = {}) {
    const ops = args?.options ? merge.all([this.options, args.options]) : this.options
    return new Report(
      args?.level || this.level,
      args?.category || this.category,
      args?.status || this.status,
      ops,
      args?.verbose == undefined ? this.verbose : args.verbose
    )
  }

  toNotification(options: Partial = {}) {
    const ops = Types.asObject(options)

    const notificationDefaults = {
      type:    this.notificationType,
      message: this.message,
      details: this.verbose
    }

    return 
  }

  get notificationType(): NotificationType { 
    if (this.category === ReportCategory.BACKEND)
      return NotificationType.CLOUD

    switch (this.level) {
      case ReportLevel.ERROR: return NotificationType.ERROR
      case ReportLevel.WARNING: return NotificationType.WARNING
      case ReportLevel.INFO: return NotificationType.INFO
    }
  }
  get code()             { return "error." + this.categoryName + "." + this.status }
  get verboseMessage()   { return this.message?.replace(/\.+$/, "") + (this.verbose ? ": " + this.verbose : "") }
  get message()          {

    if (this.category == Report.user) {
      return this.verbose
    } else {

      // first convert component name to allow translation
      const ops = this.options.component ? {...this.options, component: t('component.' + this.options.component) } : this.options

      // then translate the error
      const translation = t(this.code, ops)
      if (this.status === undefined)
        return "An unspecified error occured." // this option is for when translations have not been loaded.
      else if (translation == this.code)
        return "You are not authorized to view this page." // this option is for when translations have not been loaded.
      else
        return translation
    }
  }
  get details()          { return this.verbose } // TODO: this is not yet used

  static defaults = {
    Whoops:         Report.error(ReportCategory.FRONTEND, Report.code.frontend.Whoops),
    NotImplemented: Report.warning(ReportCategory.FRONTEND, Report.code.frontend.NotSupported),
    NotAuthorized:  Report.error(ReportCategory.FRONTEND, Report.code.frontend.NotAuthorized),
  }
}

export default Report




© 2015 - 2024 Weber Informatics LLC | Privacy Policy