bugsnag/bugsnag-js

View on GitHub
packages/plugin-electron-app/app.js

Summary

Maintainability
C
1 day
Test Coverage
const native = require('bindings')('bugsnag_plugin_electron_app_bindings')
const { schema } = require('@bugsnag/core/config')
const intRange = require('@bugsnag/core/lib/validators/int-range')

const isNativeClientEnabled = client => client._config.autoDetectErrors && client._config.enabledErrorTypes.nativeCrashes

const noop = () => {}

const osToAppType = new Map([
  ['darwin', 'macOS'],
  ['linux', 'Linux'],
  ['win32', 'Windows']
])

const createAppUpdater = (client, NativeClient, app) => {
  if (!isNativeClientEnabled(client)) {
    return newProperties => Object.assign(app, newProperties)
  }

  return newProperties => {
    Object.assign(app, newProperties)

    try {
      NativeClient.setApp(app)
    } catch (err) {
      client._logger.error(err)
    }
  }
}

const createLastRunInfoUpdater = (client, NativeClient) => {
  if (!isNativeClientEnabled(client)) {
    return noop
  }

  return lastRunInfo => {
    try {
      NativeClient.setLastRunInfo(JSON.stringify(lastRunInfo))
    } catch (err) {
      client._logger.error(err)
    }
  }
}

const getInstalledFromStore = process => {
  if (process.mas) {
    return 'mac'
  }

  if (process.windowsStore) {
    return 'windows'
  }

  return undefined
}

module.exports = (NativeClient, process, electronApp, BrowserWindow, filestore, NativeApp = native) => ({
  name: 'electronApp',
  load (client) {
    const app = {}
    const lastRunInfo = filestore.getLastRunInfo()
    const updateApp = createAppUpdater(client, NativeClient, app)
    const updateNextCrashLastRunInfo = createLastRunInfoUpdater(client, NativeClient)

    client.lastRunInfo = lastRunInfo

    updateNextCrashLastRunInfo({
      crashed: true,
      crashedDuringLaunch: true,
      consecutiveLaunchCrashes: lastRunInfo && lastRunInfo.consecutiveLaunchCrashes
        ? lastRunInfo.consecutiveLaunchCrashes + 1
        : 1
    })

    const markLaunchComplete = () => {
      if (app.isLaunching) {
        filestore.setLastRunInfo({
          crashed: false,
          crashedDuringLaunch: false,
          consecutiveLaunchCrashes: 0
        })

        updateApp({ isLaunching: false })
        // mark lastRunInfo for possible crash in the NativeClient - only applied for a native crash
        updateNextCrashLastRunInfo({
          crashed: true,
          crashedDuringLaunch: false,
          consecutiveLaunchCrashes: 0
        })
      }
    }

    // mark the launch complete after the configured time
    if (client._config.launchDurationMillis > 0) {
      setTimeout(markLaunchComplete, client._config.launchDurationMillis)
    }

    // 'getCreationTime' can return null so fallback to the current time
    // the creation time can include microseconds (depending on the platform)
    // so we round it to the nearest millisecond
    const appStart = Math.round(process.getCreationTime() || Date.now())
    let lastEnteredForeground = appStart

    updateApp({
      inForeground: BrowserWindow.getFocusedWindow() !== null,
      isLaunching: true,
      releaseStage: client._config.releaseStage,
      type: client._config.appType || osToAppType.get(process.platform),
      version: client._config.appVersion
    })

    client.addMetadata('app', {
      installedFromStore: getInstalledFromStore(process),
      name: electronApp.getName(),
      CFBundleVersion: NativeApp.getBundleVersion() || undefined
    })

    electronApp.on('browser-window-focus', () => {
      if (app.inForeground === false) {
        lastEnteredForeground = Date.now()

        updateApp({ inForeground: true })
      }
    })

    electronApp.on('browser-window-blur', () => {
      // switching focus between windows will result in both a blur & focus event
      // but the focused window will always be set when this happens
      if (BrowserWindow.getFocusedWindow() === null) {
        updateApp({ inForeground: false })
      }
    })

    // keep track of the number of windows that exist so we can mark the app as
    // in the background when there are no windows left
    const allWindows = BrowserWindow.getAllWindows()
    let numberOfWindows = allWindows.length

    const onBrowserWindowClosed = () => {
      --numberOfWindows

      if (numberOfWindows === 0) {
        updateApp({ inForeground: false })
      }
    }

    allWindows.forEach(window => { window.on('closed', onBrowserWindowClosed) })

    electronApp.on('browser-window-created', (_event, newWindow) => {
      // the focus event will fire for the new window so we don't need to update
      // inForeground here
      ++numberOfWindows

      newWindow.on('closed', onBrowserWindowClosed)
    })

    client.addOnError(event => {
      const now = Date.now()

      event.app = Object.assign(
        {},
        event.app,
        app,
        {
          duration: now - appStart,
          durationInForeground: app.inForeground ? now - lastEnteredForeground : undefined
        }
      )
    }, true)

    client.addOnSession(session => {
      session.app = Object.assign(
        {},
        session.app,
        app,
        // these values don't go in sessions
        { inForeground: undefined, isLaunching: undefined }
      )
    })

    client._app = app
    return { markLaunchComplete }
  },
  configSchema: {
    appVersion: {
      ...schema.appVersion,
      defaultValue: () => NativeApp.getPackageVersion() || electronApp.getVersion() || undefined
    },
    launchDurationMillis: {
      defaultValue: () => 5000,
      message: 'should be an integer ≥0',
      validate: intRange(0)
    }
  }
})