All files / src/utils KeymanSentry.ts

34.95% Statements 36/103
100% Branches 0/0
0% Functions 0/7
34.95% Lines 36/103

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 1031x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x                   1x 1x                   1x 1x               1x 1x                   1x 1x             1x 1x 1x 1x 1x 1x 1x 1x                                       1x 1x                 1x
import Sentry from "@sentry/node";
import KEYMAN_VERSION from "@keymanapp/keyman-version";
import { getOption } from "./options.js";
 
export type SentryNodeOptions = Sentry.NodeOptions;
 
/**
 * Maximum delay on shutdown of process to send pending events
 * to Sentry, in msec
 */
const CLOSE_TIMEOUT = 2000;
 
let isInit = false;
 
export class KeymanSentry {
 
  static isEnabled() {
    if(process.argv.includes('--no-error-reporting')) {
      return false;
    }
    if(process.argv.includes('--error-reporting')) {
      return true;
    }

    return getOption('automatically report errors', true);
  }
 
  static init(options?: SentryNodeOptions) {
    options = options ?? {};
    Sentry.init({
      ...options,
      dsn: 'https://39b25a09410349a58fe12aaf721565af@o1005580.ingest.sentry.io/5983519',  // Keyman Developer
      environment: KEYMAN_VERSION.VERSION_ENVIRONMENT,
      release: KEYMAN_VERSION.VERSION_GIT_TAG,
    });
    isInit = true;
  }
 
  private static writeSentryMessage(eventId: string) {
    process.stderr.write(`
    This error has been automatically reported to the Keyman team.
      Identifier:  ${eventId}
      Application: Keyman Developer
      Reported at: https://sentry.io/organizations/keyman/projects/keyman-developer/events/${eventId}/
    `);
  }
 
  static async reportException(e: any, silent: boolean = true) {
    if(isInit) {
      const eventId = await Sentry.captureException(e);
      if(!silent) {
        this.writeSentryMessage(eventId);
      }
      return eventId;
    }
    return null;
  }
 
  static captureMessage(message: string) {
    if(isInit) {
      return Sentry.captureMessage(message);
    } else {
      return null;
    }
  }
 
  /**
   * capture an exception for Sentry; note that in local environments, this will normally
   * throw the exception rather than sending to Sentry
   * @param e
   * @param force if true, reports to Sentry even in local environments
   */
  static async captureException(e: any, force?: boolean): Promise<never> {
    if(isInit) {
      // For local development, we don't want to bury the trace; we need the cast to avoid
      // TS2367 (comparison appears to be unintentional)
      if(!force && (KEYMAN_VERSION.VERSION_ENVIRONMENT as string) == 'local') {
        throw e;
      }

      const eventId = Sentry.captureException(e);
      process.stderr.write(`
      Fatal error: ${(e??'').toString()}
      `);
      this.writeSentryMessage(eventId);
      await this.close();

      process.exit(1);
    } else {
      throw e;
    }
  }
 
  static async close() {
    if(isInit) {
      try {
        await Sentry.close(CLOSE_TIMEOUT);
      } catch(e) {
        // ignore any errors here
      }
    }
  }
}