private-dreamnet/dreamtime

View on GitHub
src/modules/consola/log.js

Summary

Maintainability
A
3 hrs
Test Coverage
// DreamTime.
// Copyright (C) DreamNet. All rights reserved.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License 3.0 as published by
// the Free Software Foundation. See <https://www.gnu.org/licenses/gpl-3.0.html>
//
// Written by Ivan Bravo Bravo <ivan@opendreamnet.com>, 2019.

import {
  isNil, isError, isString, toString, isBoolean, isPlainObject, cloneDeep,
} from 'lodash'
import he from 'he'
import Swal from 'sweetalert2/dist/sweetalert2'
import Logger from '@dreamnet/logplease'
import { mapStackTrace } from 'sourcemapped-stacktrace'
import { HandledError } from './errors'

const LEVELS = [
  'debug',
  'info',
  'warn',
  'error',
]

export class Log {
  /**
   * @type {Logger.Logger}
   */
  logger

  /**
   * @type {string}
   */
  title

  /**
   * @type {string}
   */
  message

  /**
   * @type {boolean}
   */
  quiet = false

  /**
   * @type {string}
   */
  level = 'debug'

  /**
   * @type {Error}
   */
  error

  /**
   * @type {Object}
   */
  extra = {}

  /**
   * @type {boolean}
   */
  reported = false

  /**
   * @type {boolean}
   */
  showed = false

  /**
   * @type {boolean}
   */
  get isError() {
    return this.level === 'error'
  }

  /**
   * @type {boolean}
   */
  get isWarning() {
    return this.level === 'warn'
  }

  /**
   * @type {HandledError}
   */
  get throwable() {
    return new HandledError(this.message)
  }

  /**
   * @param {Array} args
   */
  parseArgs(args) {
    args.forEach((value) => {
      if (isNil(value)) {
        // YEP
      } else if (value instanceof Logger.Logger) {
        this.logger = value
      } else if (isError(value)) {
        this.error = value
      } else if (value instanceof ErrorEvent) {
        this.error = value.error
      } else if (LEVELS.includes(value)) {
        this.level = value
      } else if (isNil(this.message)) {
        this.message = value
      } else if (isNil(this.title) && isString(this.message)) {
        this.title = this.message
        this.message = value
      } else if (isPlainObject(value)) {
        this.extra = value
      } else if (isBoolean(value)) {
        this.quiet = value
      } else {
        // eslint-disable-next-line no-console
        console.warn('Unknown argument:', value)
      }
    })

    if (isNil(this.message) && isError(this.error)) {
      this.message = this.error.message
    }

    if (isNil(this.message)) {
      throw new HandledError('Invalid log.')
    }
  }

  /**
   *
   * @param {Logger.Logger} logger
   * @param {string} level
   * @param {string} message
   * @param {LogOptions} options
   */
  constructor(...args) {
    try {
      this.parseArgs(args)
    } catch (err) {
      // eslint-disable-next-line no-console
      console.warn('Log could not be handled:', err)
    }

    if (this.error instanceof HandledError) {
      return
    }

    this.handle().catch((err) => {
      // eslint-disable-next-line no-console
      console.warn('Log could not be handled:', err)
    })

    if (this.isError) {
      throw this.throwable
    }
  }

  /**
   *
   */
  async handle() {
    // logger.
    this.logger[this.level](this.message)

    if (this.isError) {
      await this.getSourceMapError()

      this.logger.debug({
        level: this.level,
        title: this.title,
        message: this.message,
        quiet: this.quiet,
        error: this.error ? {
          name: this.error.name,
          message: this.error.message,
          stack: this.error.stack,
        } : null,
        extra: this.extra,
      })

      this.report()

      if (!this.quiet) {
        this.show()
      }
    }
  }

  /**
   *
   */
  async report() {
    if (this.reported) {
      return this
    }

    const { rollbar, logrocket, dreamtrack } = require('../services')

    if (!rollbar.enabled) {
      return this
    }

    const { error } = this

    const snapshotUrl = await dreamtrack.takeSnapshot()
    const sessionUrl = logrocket.sessionURL
    let rollbarUrl

    // Bug tracking.
    try {
      const response = rollbar[this.level](this.title || this.message, error, {
        ...this.extra,
        sessionUrl,
        snapshotUrl,
      })

      rollbarUrl = `https://rollbar.com/occurrence/uuid/?uuid=${response?.uuid}`

      this.reported = true
    } catch (err) {
      this.logger.warn('Rollbar report fail!', err)
    }

    // Session tracking.
    if (logrocket.enabled && isError(error)) {
      try {
        logrocket.captureException(error, {
          extra: {
            rollbarUrl,
            snapshotUrl,
          },
        })

        this.reported = true
      } catch (err) {
        this.logger.warn('LogRocket report fail!', err)
      }
    }

    const urls = {
      snapshotUrl,
      sessionUrl,
      rollbarUrl,
    }

    dreamtrack.track('ERROR', cloneDeep(urls))

    consola.debug(urls)

    return this
  }

  async getSourceMapError() {
    if (!isError(this.error)) {
      return
    }

    const { error } = this

    const getStack = () => new Promise((resolve) => {
      mapStackTrace(error.stack, (stack) => {
        resolve(`${error.message}\n${stack.join('\n')}`)
      }, { cacheGlobally: true })
    })

    error.stack = await getStack()
  }

  /**
   *
   */
  show(title) {
    if (this.showed) {
      return this
    }

    let html = toString(this.message)

    if (isError(this.error)) {
      const stack = he.encode(toString(this.error.stack))
      html = `${html}<details><summary>More information</summary><pre>${stack}</pre></details>`
    }

    Swal.fire({
      title: title || this.title || 'Unexpected problem!',
      html,
      icon: this.isError ? 'error' : 'warning',
    })

    this.showed = true

    return this
  }
}