bugsnag/bugsnag-js

View on GitHub
packages/electron-filestore/filestore.js

Summary

Maintainability
A
45 mins
Test Coverage
const fs = require('fs')
const { readFileSync, writeFileSync, mkdirSync } = fs
const { unlink, readdir, access, mkdir } = fs.promises
const { F_OK } = fs.constants
const { dirname, join, basename } = require('path')
const { getIdentifier, createIdentifier, identifierKey } = require('./lib/minidump-io')

class FileStore {
  constructor (apiKey, storageDir, crashDir) {
    const base = join(storageDir, 'bugsnag', apiKey)
    this._paths = {
      events: join(base, 'events'),
      sessions: join(base, 'sessions'),
      runinfo: join(base, 'runinfo'),
      device: join(base, 'device.json'),
      lastRunInfo: join(base, 'last-run-info.json'),
      minidumps: crashDir
    }

    this._appRunMetadata = { [identifierKey]: createIdentifier() }
  }

  // Create directory layout
  async init () {
    await mkdir(this._paths.events, { recursive: true })
    await mkdir(this._paths.sessions, { recursive: true })
    await mkdir(this._paths.runinfo, { recursive: true })
  }

  getPaths () {
    return this._paths
  }

  async listMinidumps () {
    return this._listMinidumpFiles()
      .then(minidumpPaths => {
        const minidumps = minidumpPaths
          .map(async minidumpPath => {
            const eventPath = await getIdentifier(minidumpPath)
              .then(async id => {
                let path = this.getEventInfoPath(id)
                if (await fileExists(path)) {
                  return path
                }
                path = this.getBackgroundEventInfoPath(minidumpPath)
                return await fileExists(path) ? path : null
              })
              .catch(() => null)
            return new Minidump(minidumpPath, eventPath)
          })

        return Promise.all(minidumps)
      })
      .catch((e) => {
        console.log(e)
        return []
      })
  }

  async _listMinidumpFiles () {
    const dirs = [this._paths.minidumps]
    const minidumpFiles = []
    while (dirs.length) {
      const dir = dirs.pop()
      const entries = await readdir(dir, { withFileTypes: true })
      for (const entry of entries) {
        if (entry.isFile() && entry.name.match(/\.dmp$/)) {
          minidumpFiles.push(join(dir, entry.name))
        } else if (entry.isDirectory()) {
          dirs.push(join(dir, entry.name))
        }
      }
    }

    return minidumpFiles
  }

  getEventInfoPath (appRunID) {
    return join(this._paths.runinfo, appRunID)
  }

  getBackgroundEventInfoPath (minidumpPath) {
    return join(this._paths.runinfo, `${basename(minidumpPath)}.info`)
  }

  async getAppRunID (minidumpPath) {
    return getIdentifier(minidumpPath)
  }

  async deleteMinidump (minidump) {
    await unlink(minidump.minidumpPath)
    if (minidump.eventPath) {
      await unlink(minidump.eventPath)
    }
  }

  async clearEventInfoPaths () {
    const base = this._paths.runinfo
    await readdir(base, { withFileTypes: true })
      .then(async entries => {
        await Promise.all(entries
          .filter(e => e.isFile())
          .map(async e => unlink(join(base, e.name))))
      })
      .catch(() => {})
  }

  /*
   * Loads persisted device info. If none is present, it generates an identifier
   * for the device and persists it prior to returning device info.
   */
  getDeviceInfo () {
    let device

    try {
      // Do this one sync routine upon startup because if we use the async fs operations,
      // it doesn't get scheduled until at least 500ms after the process has started.
      // Bugsnag needs the device ID before sending any events or sessions so it's
      // important we get it in a timely manner
      const contents = readFileSync(this._paths.device)
      device = JSON.parse(contents)
    } catch (e) {
      // either the file could not be read or wasn't valid JSON
    }

    try {
      // attempt to create or update the device.json file with
      // a) the device data we retrieved or
      // b) a new auto generated id
      return this.setDeviceInfo(device)
    } catch (e) {
      // in the event of a failure we don't want to return the device
      // id we have in memory because it won't have been persisted,
      // and so won't be stable across app launches
      return {}
    }
  }

  setDeviceInfo (device = {}) {
    if (!device.id) {
      device.id = createIdentifier()
    }
    mkdirSync(dirname(this._paths.device), { recursive: true })
    writeFileSync(this._paths.device, JSON.stringify(device))
    return device
  }

  getLastRunInfo () {
    try {
      // similar to getDeviceInfo - the lastRunInfo must be available during tha app-launch phase
      // as such we use readFileSync to ensure that the data is loaded immediately
      const contents = readFileSync(this._paths.lastRunInfo)
      const lastRunInfo = JSON.parse(contents)

      if (typeof lastRunInfo.crashed === 'boolean' &&
        typeof lastRunInfo.crashedDuringLaunch === 'boolean' &&
        typeof lastRunInfo.consecutiveLaunchCrashes === 'number') {
        return lastRunInfo
      }
    } catch (e) {
    }

    return null
  }

  setLastRunInfo (lastRunInfo = { crashed: false, crashedDuringLaunch: false, consecutiveLaunchCrashes: 0 }) {
    try {
      mkdirSync(dirname(this._paths.lastRunInfo), { recursive: true })
      writeFileSync(this._paths.lastRunInfo, JSON.stringify(lastRunInfo))
    } catch (e) {
    }
    return lastRunInfo
  }

  getAppRunMetadata () {
    return this._appRunMetadata
  }
}

class Minidump {
  constructor (minidumpPath, eventPath) {
    this.minidumpPath = minidumpPath
    this.eventPath = eventPath
  }
}

const fileExists = async (filepath) => {
  try {
    await access(filepath, F_OK)
    return true
  } catch (e) {
    return false
  }
}

module.exports = { FileStore }