Home Reference Source

js/helpers/ErrorManager.js

/**
 * Centralizes error handling
 */

import Data from '~/models/Data';
import Analytics from '~/models/Analytics';
import { Bugsnag } from '~/helpers/Bugsnag';

const ErrorList = [];

/**
 * Generic error type
 */
export class AnyError {
    /**
     * @param {string} message Error descrpition
     * @param {Symbol|string} id Id of error
     */
    constructor(message, id) {
        this.message = message;
        this.id = typeof id === 'symbol' ? id.toString().slice(7, -1) : id;

        // Stores stack trace
        this.jsError = new Error(message);
        this.jsError.name = this.id;
    }

    get idString() {
        return this.id;
    }

    get name() { return this.id; }

    toString() {
        return this.id + ": " + this.message;
    }

    /**
     * Reports the error with some args.
     * @param {Array} args anything
     */
    report(...args) {
        ErrorList.push(this);
        console.error(`%c${this.idString}:%c ${this.message}`, 'font-weight: 700', '', ...args);
        console.log(
            `This error has been reported, your instance id is %c${Data.shared.dataId}%c.`,
            'font-family: Menlo, "Fira Mono", monospace;', ''
        );
    }
}

export class ErrorManager {
    /**
     * Raises an error with native throw.
     * @param {string} message
     * @param {Symbol|string} id - Error id string or symbol.
     */
    raise(message, id) {
        const error = new AnyError(message, id);
        Analytics.shared.reportError('error', error, { critical: true });
        throw error;
    }

    /**
     * Warns an AnyError
     * @param {string} message
     * @param {Symbol|string} id Describes the type
     */
    warn(message, id) {
        const error = new AnyError(message, id);
        Analytics.shared.reportError('warning', error);
        console.warn(error.toString());
    }

    /**
     * A handled or non-critical error. Logs to console.
     *
     * @param {Error|AnyError} error - The error object caught.
     * @param {string} message - Describes the error
     * @param {Array} args - Any other spread arguments that should be provided.
     */
    silent(error, message, ...args) {
        let title = "Error";
        if (error instanceof AnyError) {
            message = error.message + '; ' + message;
            title = error.idString;
        } else if (error.name) {
            title = error.name;
            args.unshift(error);
        }

        const err = new AnyError(message, title);
        if (error.stack) {
            err.jsError = error;
        }

        Analytics.shared.reportError('warning', err);

        err.report(...args);
    }

    /**
     * Reports an error
     * @param {Error|AnyError} error - An error to directly report
     */
    report(error) {
        if (error instanceof AnyError) {
            Analytics.shared.reportError('error', error);
            error.report();
        } else {
            this.unhandled(error);
        }
    }

    /**
     * Pass unhandled errors here
     * @param {Error|AnyError} error - An unhandled error to report.
     */
    unhandled(error) {
        Analytics.shared.reportError('error', error, { critical: true });
        new AnyError(error.message, `Unhandled Error (${error.name})`).report(error, error.stack);
    }

    /**
     * Returns errors
     * @return {string}
     */
    dump() {
        return `Error Dump for instance ${Data.shared.dataId}:\n\n` +
            ErrorList.map((error, indexNum) => {
                let index = String(indexNum);

                return ` ${indexNum + 1}. ${error.idString}\n` +
                    ` ${" ".repeat(index.length)}  ${error.message}`;
            }).join(`\n\n`);
    }

    /**
     * Dumps in new context
     */
    dumpText() {
        window.open(
            `data:text/plain,${encodeURIComponent(this.dump())}`,
            '_blank'
        );
    }

    /**
     * Dumps error log to console.
     */
    dumpConsole() {
        console.log(
            `%cError Dump%c for instance (%c${Data.shared.dataId}%c):%c\n` +
            ErrorList.map(
                (error, indexNum) => {
                    let index = String(indexNum);

                    return `%c${indexNum + 1}. %c${error.idString}%c\n` +
                        `%c${" ".repeat(index.length)}  ${error.message}%c`;
                }
            ).join('\n'),
            'font-size: 24px; font-weight: bold;', 'font-size: 24px;',
            'font-size: 24px; font-weight: bold; text-decoration: underline', 'font-size: 24px;', '',
            ...[].concat(...ErrorList.map(_ => [
                'font-size: 16px;',
                'color: red; font-weight: bold; font-size: 16px;', '',
                'font-size: 14px;', ''
            ]))
        );
    }
}

ErrorManager.shared = new ErrorManager();
export default ErrorManager.shared;
export const HandleUnhandledPromise = (reason) => ErrorManager.shared.unhandled(reason);