heroku/heroku-apps

View on GitHub
src/commands/dashboard.js

Summary

Maintainability
C
1 day
Test Coverage
'use strict'

const co = require('co')
const cli = require('heroku-cli-util')
const {round, flatten, mean, groupBy, map, sum, sumBy, toPairs, sortBy, zip} = require('lodash')

let empty = (o) => Object.keys(o).length === 0

function displayFormation (formation) {
  formation = groupBy(formation, 'size')
  formation = map(formation, (p, size) => `${bold(sumBy(p, 'quantity'))} | ${size}`)
  cli.log(`  ${label('Dynos:')} ${formation.join(', ')}`)
}

function displayErrors (metrics) {
  let errors = []
  if (metrics.routerErrors) {
    errors = errors.concat(toPairs(metrics.routerErrors.data).map((e) => cli.color.red(`${sum(e[1])} ${e[0]}`)))
  }
  if (metrics.dynoErrors) {
    metrics.dynoErrors.filter((d) => d).forEach((dynoErrors) => {
      errors = errors.concat(toPairs(dynoErrors.data).map((e) => cli.color.red(`${sum(e[1])} ${e[0]}`)))
    })
  }
  if (errors.length > 0) cli.log(`  ${label('Errors:')} ${errors.join(dim(', '))} (see details with ${cli.color.cmd('heroku apps:errors')})`)
}

function displayMetrics (metrics) {
  function rpmSparkline () {
    if (['win32', 'windows'].includes(process.platform)) return ''
    let sparkline = require('sparkline')
    let points = []
    Object.values(metrics.routerStatus.data).forEach((cur) => {
      for (let i = 0; i < cur.length; i++) {
        let j = Math.floor(i / 3)
        points[j] = (points[j] || 0) + cur[i]
      }
    })
    points.pop()
    return dim(sparkline(points)) + ' last 24 hours rpm'
  }
  let ms = ''
  let rpm = ''
  if (metrics.routerLatency && !empty(metrics.routerLatency.data)) {
    let latency = metrics.routerLatency.data['latency.ms.p50']
    if (!empty(latency)) ms = `${round(mean(latency))} ms `
  }
  if (metrics.routerStatus && !empty(metrics.routerStatus.data)) {
    rpm = `${round(sum(flatten(Object.values(metrics.routerStatus.data))) / 24 / 60)} rpm ${rpmSparkline()}`
  }
  if (rpm || ms) cli.log(`  ${label('Metrics:')} ${ms}${rpm}`)
}

function displayNotifications (notifications) {
  if (!notifications) return

  notifications = notifications.filter((n) => !n.read)
  if (notifications.length > 0) {
    cli.log(`
You have ${cli.color.yellow(notifications.length)} unread notifications. Read them with ${cli.color.cmd('heroku notifications')}`)
  }
}

let dim = (s) => cli.color.dim(s)
let bold = (s) => cli.color.bold(s)
let label = (s) => cli.color.blue(s)

function displayApps (apps, appsMetrics) {
  const time = require('../time')

  let owner = (owner) => owner.email.endsWith('@herokumanager.com') ? owner.email.split('@')[0] : owner.email

  for (let a of zip(apps, appsMetrics)) {
    let app = a[0]
    let metrics = a[1]
    cli.log(cli.color.app(app.app.name))
    cli.log(`  ${label('Owner:')} ${owner(app.app.owner)}`)
    if (app.pipeline) {
      cli.log(`  ${label('Pipeline:')} ${app.pipeline.pipeline.name}`)
    }
    displayFormation(app.formation)
    cli.log(`  ${label('Last release:')} ${time.ago(new Date(app.app.released_at))}`)
    displayMetrics(metrics)
    displayErrors(metrics)
    cli.log()
  }
}

function * run (context, heroku) {
  const img = require('term-img')
  const path = require('path')

  // if not testing and not logged in
  if (!cli.raiseErrors && (!context.auth || !context.auth.password)) {
    let {execSync} = require('child_process')
    execSync('heroku help', {stdio: 'inherit'})
    return
  }

  function favoriteApps () {
    return heroku.request({
      host: 'longboard.heroku.com',
      path: '/favorites?type=app',
      headers: {Range: ''}
    }).then((apps) => apps.map((app) => app.app_name))
  }

  function fetchMetrics (apps) {
    const NOW = new Date().toISOString()
    const YESTERDAY = new Date(new Date().getTime() - (24 * 60 * 60 * 1000)).toISOString()
    let date = `start_time=${YESTERDAY}&end_time=${NOW}&step=1h`
    return apps.map((app) => {
      let types = app.formation.map((p) => p.type)
      return {
        dynoErrors: types.map((type) => heroku.request({host: 'api.metrics.herokai.com', path: `/apps/${app.app.name}/formation/${type}/metrics/errors?${date}`, headers: {Range: ''}}).catch(() => null)),
        routerLatency: heroku.request({host: 'api.metrics.herokai.com', path: `/apps/${app.app.name}/router-metrics/latency?${date}&process_type=${types[0]}`, headers: {Range: ''}}).catch(() => null),
        routerErrors: heroku.request({host: 'api.metrics.herokai.com', path: `/apps/${app.app.name}/router-metrics/errors?${date}&process_type=${types[0]}`, headers: {Range: ''}}).catch(() => null),
        routerStatus: heroku.request({host: 'api.metrics.herokai.com', path: `/apps/${app.app.name}/router-metrics/status?${date}&process_type=${types[0]}`, headers: {Range: ''}}).catch(() => null)
      }
    })
  }

  let apps, data, metrics

  try {
    img(path.join(__dirname, '..', '..', 'assets', 'heroku.png'), {fallback: () => {}})
  } catch (err) { }

  yield cli.action('Loading', {clear: true}, co(function * () {
    apps = yield favoriteApps()

    data = yield {
      orgs: heroku.request({path: '/organizations'}),
      notifications: heroku.request({host: 'telex.heroku.com', path: '/user/notifications'}).catch(() => null),
      apps: apps.map((app) => ({
        app: heroku.get(`/apps/${app}`),
        formation: heroku.get(`/apps/${app}/formation`),
        pipeline: heroku.get(`/apps/${app}/pipeline-couplings`).catch(() => null)
      }))
    }
    metrics = yield fetchMetrics(data.apps)
  }))

  if (apps.length > 0) displayApps(data.apps, metrics)
  else cli.warn(`Add apps to this dashboard by favoriting them with ${cli.color.cmd('heroku apps:favorites:add')}`)

  cli.log(`See all add-ons with ${cli.color.cmd('heroku addons')}`)
  let sampleOrg = sortBy(data.orgs.filter((o) => o.role !== 'collaborator'), (o) => new Date(o.created_at))[0]
  if (sampleOrg) cli.log(`See all apps in ${cli.color.yellow.dim(sampleOrg.name)} with ${cli.color.cmd('heroku apps --team ' + sampleOrg.name)}`)
  cli.log(`See all apps with ${cli.color.cmd('heroku apps --all')}`)
  displayNotifications(data.notifications)
  cli.log(`
See other CLI commands with ${cli.color.cmd('heroku help')}
`)
}

module.exports = {
  topic: 'dashboard',
  description: 'display information about favorite apps',
  hidden: true,
  needsAuth: true,
  run: cli.command(co.wrap(run))
}