Source

plugins/error-logger.js

import { isUnitTest } from '@/utils/unit.js'
import Vue from 'vue'
import axios from 'axios'
import api from '@/api'
import * as Sentry from '@sentry/browser'
import { Integrations } from '@sentry/tracing'
import { default as createMitt } from 'mitt'
import envService from '@/services/env-service.js'
import { getQueryStringValue } from '@/utils/querystring'
import APIUrls from '@/config/simpliciti-apis.js'

//Disabled due to bad performance from APIV3 and auth errors
const IS_APIV3_ERROR_LOGGING_ENABLED = false
const shouldLog = (getQueryStringValue('verbose') || '').includes('1')
const mitt = createMitt()

export const errorLogger = createSimplicitiErrorLogger()

Vue.use(SimplicitiLoggerPlugin())
Vue.use(SentryLoggerPlugin())

/**
 *  This plugin will automatically catch all errors. and send them to server (APIV3)
 *
 *
 *  If the error states 'Script Error', take a look at:
 *  https://scotch.io/bar-talk/what-the-heck-is-script-error
 *
 *  @deprecated server logging disabled due to Sentry being enough (and some APIV3 issues)
 *  @example
 *
 *  Usage (From outside components)
 *
 *  Vue.ErrorLogger.logError(error)
 *
 *  Usage (From inside components)
 *
 *  this.$logError(error)
 *
 *  Usage (Listen errors)
 *  Vue.ErrorLogger.$on("error")
 *
 */
function SimplicitiLoggerPlugin() {
  return {
    install: function () {
      Vue.ErrorLogger = errorLogger
      Vue.prototype.$logError = (message, metadata) =>
        errorLogger.logError(message, metadata)
      Vue.prototype.$logError._sentry = Sentry
      axios.interceptors.response.use(
        function (response) {
          return response
        },
        function (error) {
          error.stack = `${error.request.status} (${error.request.statusText}) at ${error.request.responseURL} : ${error.stack}`
          return Promise.reject(error)
        }
      )

      errorLogger.bindToBrowserErrors()
      errorLogger.bindToVueErrors()
    },
  }
}

function SentryLoggerPlugin() {
  return {
    install() {
      let isLocalhost = window.location.origin.indexOf('localhost') !== -1
      if (
        !isLocalhost &&
        !!process.env.VUE_APP_SENTRY_DSN &&
        process.env.VUE_APP_SENTRY_ENABLED === '1'
      ) {
        Sentry.init({
          Vue,
          dsn: envService.getEnvValue('VUE_APP_SENTRY_DSN'),
          autoSessionTracking: true,
          integrations: [new Integrations.BrowserTracing()],
          logErrors: true,
          tracesSampleRate: 0.05,
          release:
            envService.getEnvValue('VUE_APP_RELEASE_NAME') || 'development',
          environment: envService.getEnvName(),
        })
        shouldLog && console.log('SENTRY_LOGGER_INIT_OK')
      }
    },
  }
}

/**
 * Custom error logger. Initially, we use to send errors to server (APIV3) but this was disabled in favor of Sentry.
 *
 * @returns
 */
function createSimplicitiErrorLogger() {
  const self = {
    $on(name, callback) {
      mitt.on(name, callback)
    },
    $emit(name, payload) {
      mitt.emit(name, payload)
    },
    $once(name, callback) {
      let wasCalled = false
      let handler = (payload) => {
        if (wasCalled) {
          return
        } else {
          wasCalled = true
        }
        mitt.off(handler)
        callback(payload)
      }
      mitt.on(name, handler)
    },
    shouldLogErrorsToServer: IS_APIV3_ERROR_LOGGING_ENABLED,
    bindToVueErrors() {
      //Skip errors binding if unit test mode
      if (isUnitTest()) {
        return
      }
      Vue.config.errorHandler = (err) => {
        self.logNonManuallyTriggeredError(err)
        throw err
      }
    },
    bindToBrowserErrors() {
      window.addEventListener('error', (err) =>
        self.logNonManuallyTriggeredError(err)
      )

      window.addEventListener('unhandledrejection', (err) =>
        self.logNonManuallyTriggeredError(err)
      )
    },
    logNonManuallyTriggeredError(error) {
      self.logError(
        error,
        {},
        {
          wasManuallyTriggered: false,
        }
      )
    },
    logError(error, metadata = {}, options = {}) {
      if (!error) return

      metadata = typeof metadata !== 'object' ? {} : metadata

      if (error instanceof ErrorEvent && error.error) {
        error = error.error
      }

      if (typeof PromiseRejectionEvent !== 'undefined') {
        if (error instanceof PromiseRejectionEvent) {
          return logErrorInternal(
            (error.reason && error.reason.message) ||
              error.message ||
              'PromiseRejectionEvent',
            {
              ...metadata,
              stack: (error.reason && error.reason.stack) || error.stack || '',
            }
          )
        }
      }

      if (error instanceof Error) {
        metadata.stack = error.stack
        return logErrorInternal(error.message || 'Error', metadata)
      }

      metadata.stack = metadata.stack || new Error('GENERIC').stack

      if (typeof error === 'string') {
        return logErrorInternal(error || 'Error', metadata)
      }

      if (typeof error === 'number') {
        return logErrorInternal('Error', {
          ...metadata,
          status: error,
        })
      }

      return logError('Error', {
        ...metadata,
      })
    },
  }

  /**
   * Manually triggered errors are also send to Sentry.
   *
   * @param {String} message
   * @param {Object} metadata
   * @param {Object} options
   * @returns
   */
  async function logErrorInternal(message, metadata = {}, options = {}) {
    //Use case: A new error is triggered while logging to server
    if (
      metadata &&
      metadata.stack &&
      metadata.stack.indexOf('Network Error') !== -1
    ) {
      return //Avoid Overflow
    }

    //Send manually triggered errors to Sentry
    if (options.wasManuallyTriggered !== false && !!options.errorObject) {
      Sentry.captureException(options.errorObject)
    }

    if (self.shouldLogErrorsToServer) {
      try {
        api.v3.post(APIUrls.APIV3_JAVASCRIPT_ERRORS, {
          message,
          metadata,
        })
      } catch (err) {
        console.error('ERROR', 'Unable to log error', message, metadata)
      }
    } else {
      console.error('ERROR', message, metadata)
    }
    self.$emit('error', {
      message,
      metadata,
    })
  }

  return self
}