Skip to contentVibraUI

Deploy frequency

Production deploys a week over the last twelve, the ones that shipped clean against the ones that failed on the way out; reads deploysByWeek().

Preview

Install

npx shadcn@latest add @vibra/widget-engineering-overview-deploy-frequency

Needs the @vibra registry in your components.json — set it up once.

Source

app/engineering/components/deploy-frequency.tsx
"use client"

import { formatNumber } from "@/lib/format"
import { BarChart } from "@/components/ui/bar-chart"
import { ChartCard } from "@/components/ui/chart-card"

import { type WeekPoint } from "../data"

// Green against red, in that order, so the eye reads the week's outcome
// before it reads the week's volume.
const SERIES = [
  { key: "succeeded", label: "Succeeded", color: "chart-2" as const },
  { key: "failed", label: "Failed", color: "chart-4" as const },
]

export type DeployFrequencyProps = { weeks: WeekPoint[] }

/** Production deploys a week, green against red. The weeks arrive counted from the server page. */
export function DeployFrequency({ weeks }: DeployFrequencyProps) {
  const succeeded = weeks.reduce((total, week) => total + week.succeeded, 0)
  const failed = weeks.reduce((total, week) => total + week.failed, 0)

  return (
    <ChartCard
      data-widget="widget-engineering-overview-deploy-frequency"
      title="Deploy frequency"
      description="Production deploys a week over the last twelve"
      height={360}
      className="h-full"
      footer={`${formatNumber(succeeded, { maximumFractionDigits: 0 })} shipped clean, ${formatNumber(failed, { maximumFractionDigits: 0 })} failed on the way out.`}
    >
      <BarChart
        data={weeks}
        index="week"
        series={SERIES}
        height={360}
        showYAxis
        valueFormatter={(value) => formatNumber(value, { maximumFractionDigits: 0 })}
      />
    </ChartCard>
  )
}
app/engineering/data.ts
/**
 * What this page reads. `db.deployments` is the whole pipeline history, and
 * three of the four headline numbers fall straight out of it: how often
 * production ships, how often a production deploy fails, and how long the
 * next green one takes to arrive after a red one.
 *
 * Two things the entity does not record are derived, and neither is a literal.
 * Lead time is a *rule* over the row db does have — a deploy off `main` was
 * cut hours after its commit, a feature branch days, with a jitter fixed by
 * `seeded("dashboard-engineering")` so the same deploy always reads the same.
 * The open pull requests have no entity at all, so they are generated once
 * from the same generator over the branches in `db.deployments` and the people
 * in `db.members`. "Now" is `REFERENCE_DATE`; nothing here reads a clock.
 */
import { formatDuration, formatNumber, formatPercent, getInitials } from "@/lib/format"
import {
  db,
  intBetween,
  REFERENCE_DATE,
  seeded,
  type Deployment,
  type Member,
} from "@/lib/sample-data"

export type { Deployment }

const HOUR_MS = 3_600_000
const DAY_MS = 86_400_000

/** The window the four headline numbers are measured over. */
export const WINDOW_DAYS = 30

const WINDOW_START = REFERENCE_DATE.getTime() - WINDOW_DAYS * DAY_MS

// One series for the whole block: one lead time per deployment first, then the
// pull requests.
const SEED = "dashboard-engineering"
const rand = seeded(SEED)

const BY_TIME = db.deployments.all().sort((a, b) => a.startedAt.getTime() - b.startedAt.getTime())
const PRODUCTION = BY_TIME.filter((deployment) => deployment.environment === "production")

// How long a change waits before it ships, in hours: work on main goes out the
// same day, a feature branch takes days. The jitter is drawn once per
// deployment, in time order, so a row's lead time never moves.
const LEAD_HOURS = new Map<string, number>(
  BY_TIME.map((deployment) => [
    deployment.id,
    deployment.branch === "main" ? 2 + rand() * 7 : 14 + rand() * 60,
  ])
)

/** How long this change waited between its first commit and this deploy. */
export function leadTimeHours(deployment: Deployment): number {
  return LEAD_HOURS.get(deployment.id) ?? 0
}

const inWindow = (deployment: Deployment) => deployment.startedAt.getTime() >= WINDOW_START

const median = (values: number[]): number => {
  if (values.length === 0) return 0
  const sorted = [...values].sort((a, b) => a - b)
  return sorted[Math.floor(sorted.length / 2)]
}

/**
 * How long production stayed broken: from a failed production deploy inside
 * the window to the next successful one, in milliseconds. The recovery itself
 * may land outside the window — what is windowed is the failure. A failure
 * with nothing green after it is still open and cannot be measured.
 */
function recoveryTimes(): number[] {
  const times: number[] = []
  PRODUCTION.forEach((deployment, index) => {
    if (deployment.status !== "failed" || !inWindow(deployment)) return
    const fixed = PRODUCTION.slice(index + 1).find((next) => next.status === "success")
    if (!fixed) return
    times.push(fixed.startedAt.getTime() - deployment.startedAt.getTime())
  })
  return times
}

// Below this many incidents a median is one or two readings wearing a
// statistic's clothes, so the card says how many it stands on.
const THIN_SAMPLE = 5

export type EngineeringStat = {
  key: string
  label: string
  value: string
  /** Trails the value in quieter type — the sample a thin median stands on. */
  note?: string
  description: string
}

/** Deploy frequency, lead time, change failure rate and time to restore. */
export function engineeringStats(): EngineeringStat[] {
  const shipped = PRODUCTION.filter(inWindow)
  // A cancelled deploy never reached production, so it is neither a change
  // that shipped nor a change that failed: it leaves the ratio entirely.
  const completed = shipped.filter(
    (deployment) => deployment.status === "success" || deployment.status === "failed"
  )
  const failed = completed.filter((deployment) => deployment.status === "failed")
  const recoveries = recoveryTimes()

  return [
    {
      key: "frequency",
      label: "Deploy frequency",
      value: `${formatNumber(shipped.length / WINDOW_DAYS, { maximumFractionDigits: 1 })}/day`,
      description: `${shipped.length} production deploys in ${WINDOW_DAYS} days`,
    },
    {
      key: "lead-time",
      label: "Lead time",
      value: formatDuration(median(shipped.map(leadTimeHours)) * HOUR_MS),
      description: "median, first commit to production",
    },
    {
      key: "failure-rate",
      label: "Change failure rate",
      value: formatPercent(completed.length === 0 ? 0 : failed.length / completed.length, {
        maximumFractionDigits: 1,
      }),
      description: `${failed.length} of ${completed.length} completed deploys failed`,
    },
    {
      key: "mttr",
      label: "Time to restore",
      value: formatDuration(median(recoveries)),
      ...(recoveries.length < THIN_SAMPLE
        ? { note: `${recoveries.length} incident${recoveries.length === 1 ? "" : "s"}` }
        : {}),
      description: "median, failed deploy to the next green one",
    },
  ]
}

export type WeekPoint = { week: string; succeeded: number; failed: number }

const CHART_WEEKS = 12

const WEEK_LABEL = new Intl.DateTimeFormat("en-US", {
  month: "short",
  day: "numeric",
  timeZone: "UTC",
})

/** Production deploys per week over the last twelve, green against red. */
export function deploysByWeek(): WeekPoint[] {
  const end = REFERENCE_DATE.getTime()
  return Array.from({ length: CHART_WEEKS }, (_, index) => {
    const from = end - (CHART_WEEKS - index) * 7 * DAY_MS
    const to = from + 7 * DAY_MS
    const week = PRODUCTION.filter(
      (deployment) =>
        deployment.startedAt.getTime() >= from && deployment.startedAt.getTime() < to
    )
    return {
      week: WEEK_LABEL.format(new Date(from)),
      succeeded: week.filter((deployment) => deployment.status === "success").length,
      failed: week.filter((deployment) => deployment.status === "failed").length,
    }
  })
}

/** The last production deploys, newest first. */
export function productionTimeline(limit = 6): Deployment[] {
  return [...PRODUCTION].reverse().slice(0, limit)
}

/** The last deploys to any environment, newest first. */
export function recentDeployments(limit = 8): Deployment[] {
  return [...BY_TIME].reverse().slice(0, limit)
}

export type PullRequest = {
  id: string
  number: number
  title: string
  branch: string
  author: Member
  additions: number
  deletions: number
  checks: "passing" | "failing" | "running"
  reviews: number
  openedAt: Date
}

// How a branch name reads as a pull request title: "feat/sso-login" becomes
// "Sso login". The branch is the record; the sentence is derived from it.
function titleFor(branch: string): string {
  const words = branch.split("/").slice(1).join(" ").replace(/-/g, " ")
  return words.charAt(0).toUpperCase() + words.slice(1)
}

const PR_COUNT = 6

function generatePullRequests(): PullRequest[] {
  // The pull requests come after the lead times in the block's series: that
  // part of it is replayed per request, so the authors are the members as they
  // are now, a rename or a deactivation included, and every other figure is
  // the one it always was.
  const rand = seeded(SEED)
  for (let drawn = 0; drawn < BY_TIME.length; drawn++) rand()

  // Every branch the pipeline has seen, except main, newest first.
  const branches = [
    ...new Set(
      [...BY_TIME].reverse().map((deployment) => deployment.branch).filter((branch) => branch !== "main")
    ),
  ].slice(0, PR_COUNT)
  const authors = db.members.all().filter((member) => member.status === "active")

  return branches.map((branch, index) => {
    const checks = rand()
    return {
      id: branch,
      number: 1200 + index * intBetween(rand, 3, 19),
      title: titleFor(branch),
      branch,
      author: authors[intBetween(rand, 0, authors.length - 1)],
      additions: intBetween(rand, 12, 940),
      deletions: intBetween(rand, 4, 420),
      checks: checks < 0.68 ? "passing" : checks < 0.86 ? "failing" : "running",
      reviews: intBetween(rand, 0, 3),
      openedAt: new Date(REFERENCE_DATE.getTime() - intBetween(rand, 2, 260) * HOUR_MS),
    }
  })
}

/** What is waiting for review, oldest first. */
export function openPullRequests(): PullRequest[] {
  return generatePullRequests().sort((a, b) => a.openedAt.getTime() - b.openedAt.getTime())
}

/** How long a deploy took, written out. */
export function deployDuration(deployment: Deployment): string {
  return formatDuration(deployment.durationSec * 1000)
}

/** The moment every "ago" on this page is measured against. */
export const NOW = REFERENCE_DATE

/** How long ago something happened, in whole minutes, hours or days. */
export function since(at: Date): string {
  const ms = REFERENCE_DATE.getTime() - at.getTime()
  if (ms < HOUR_MS) return `${Math.max(1, Math.round(ms / 60_000))}m ago`
  return ms < DAY_MS
    ? `${Math.round(ms / HOUR_MS)}h ago`
    : `${Math.round(ms / DAY_MS)}d ago`
}

/** The bell's contents: the newest notifications, unread first in the panel. */
export function shellNotifications() {
  return db.notifications
    .all()
    .sort((a, b) => b.at.getTime() - a.at.getTime())
    .slice(0, 6)
    .map(({ id, title, description, at, read, href }) => ({ id, title, description, at, read, href }))
}

function ownerRow(): Member {
  return db.members.all().find((member) => member.role === "owner") ?? db.members.all()[0]
}

/** The person looking at the page: whoever owns this workspace. */
export function currentUser() {
  const owner = ownerRow()
  return { name: owner.name, email: owner.email, initials: getInitials(owner.name), avatarUrl: owner.avatarUrl }
}

// Fixed to UTC so the line reads the same wherever the page is rendered.
const WINDOW_LABEL = new Intl.DateTimeFormat("en-US", { dateStyle: "medium", timeZone: "UTC" })

/** The window the headline numbers cover, written out. */
export function windowLabel(): string {
  return `${WINDOW_LABEL.format(new Date(WINDOW_START))} – ${WINDOW_LABEL.format(REFERENCE_DATE)} UTC`
}

Its page

On its page the card sits among the rest of the dashboard and shares its range and its data with them.

From the Engineering dashboard page