From 0b990ca26d3d16b0548d81c97d9280c934a71ece Mon Sep 17 00:00:00 2001 From: Hiroshi Nishio Date: Wed, 25 Feb 2026 20:52:14 -0800 Subject: [PATCH] Add repo line count to coverage drip emails and fix dora.dev link - Include approximate line count (e.g. ~8K lines) next to repo name in coverage chart emails - Track repoMostNeedingCoverageLines through owner context pipeline - Improve email copy: "just came in at" instead of "is", "reached" instead of "has" - Fix dora.dev 301 redirect flagged by Ahrefs --- .../cron/drip-emails/build-owner-context.ts | 3 +++ .../templates/drip/drip-templates.test.ts | 10 +++++++--- .../drip/onboarding/02-coverage-charts.ts | 20 +++++++++++-------- .../2024-11-24-what-are-dora-metrics.mdx | 2 +- types/drip-emails.ts | 1 + 5 files changed, 24 insertions(+), 12 deletions(-) diff --git a/app/actions/cron/drip-emails/build-owner-context.ts b/app/actions/cron/drip-emails/build-owner-context.ts index c639a92c..0c712e2b 100644 --- a/app/actions/cron/drip-emails/build-owner-context.ts +++ b/app/actions/cron/drip-emails/build-owner-context.ts @@ -117,6 +117,7 @@ export const buildOwnerContext = (data: BatchQueryResults): OwnerLookups => { const coverageRepoCountByOwner: Record = {}; const repoMostNeedingCoverageByOwner: Record = {}; const repoMostNeedingCoveragePctByOwner: Record = {}; + const repoMostNeedingCoverageLinesByOwner: Record = {}; for (const [ownerIdStr, repos] of Object.entries(latestRepoCov)) { const ownerId = Number(ownerIdStr); coverageRepoCountByOwner[ownerId] = Object.keys(repos).length; @@ -135,6 +136,7 @@ export const buildOwnerContext = (data: BatchQueryResults): OwnerLookups => { repoMostNeedingCoveragePctByOwner[ownerId] = Math.round( (r.lines_covered / r.lines_total) * 100, ); + repoMostNeedingCoverageLinesByOwner[ownerId] = r.lines_total; } } @@ -188,6 +190,7 @@ export const buildOwnerContext = (data: BatchQueryResults): OwnerLookups => { unscheduledRepoNames: ownerUnscheduledRepoNames[ownerId] || [], repoMostNeedingCoverage: repoMostNeedingCoverageByOwner[ownerId] ?? null, repoMostNeedingCoveragePct: repoMostNeedingCoveragePctByOwner[ownerId] ?? null, + repoMostNeedingCoverageLines: repoMostNeedingCoverageLinesByOwner[ownerId] ?? null, coverageBenchmark: (() => { const repo = repoMostNeedingCoverageByOwner[ownerId]; const ownerRepos = latestRepoCov[ownerId]; diff --git a/app/actions/resend/templates/drip/drip-templates.test.ts b/app/actions/resend/templates/drip/drip-templates.test.ts index faae72a1..b8ff3374 100644 --- a/app/actions/resend/templates/drip/drip-templates.test.ts +++ b/app/actions/resend/templates/drip/drip-templates.test.ts @@ -23,6 +23,7 @@ const makeCtx = (overrides: Partial = {}): OwnerContext => ({ unscheduledRepoNames: ["api", "web"], repoMostNeedingCoverage: null, repoMostNeedingCoveragePct: null, + repoMostNeedingCoverageLines: null, coverageBenchmark: null, hasSetupPr: false, hasSetupPrMerged: false, @@ -67,14 +68,15 @@ describe("drip email templates", () => { ownerCoveragePct: 42.7, coverageRepoCount: 1, repoMostNeedingCoverage: "backend", + repoMostNeedingCoverageLines: 8200, coverageBenchmark: { linesTotal: 5303, coveragePct: 89 }, }); const text = generateCoverageChartsEmail("acme", "Alice", ctx); expect(text).toContain("Hi Alice"); expect(text).toContain("43%"); - expect(text).toContain("acme/backend"); + expect(text).toContain("acme/backend (~8K lines)"); expect(text).not.toContain("across"); - expect(text).toContain("5K-line project on GitAuto has 89% coverage"); + expect(text).toContain("5K-line project on GitAuto reached 89% coverage"); expect(text).toContain("/dashboard/charts"); expect(text).toContain("Wes"); }); @@ -85,12 +87,13 @@ describe("drip email templates", () => { ownerCoveragePct: 72, coverageRepoCount: 5, repoMostNeedingCoverage: "backend", + repoMostNeedingCoverageLines: 12000, }); const text = generateCoverageChartsEmail("acme", "Alice", ctx); expect(text).toContain("Hi Alice"); expect(text).toContain("72%"); expect(text).toContain("across 5 repos"); - expect(text).toContain("acme/backend"); + expect(text).toContain("acme/backend (~12K lines)"); expect(text).toContain("/dashboard/charts"); expect(text).toContain("Wes"); }); @@ -220,6 +223,7 @@ describe("drip email templates", () => { ownerCoveragePct: 99.9, coverageRepoCount: 99, repoMostNeedingCoverage: "long-repo-name", + repoMostNeedingCoverageLines: 999000, }); const text = generateCoverageChartsEmail("long-org-name", "Maximilian", ctx); expect(text.length).toBeLessThanOrEqual(CHAR_LIMIT); diff --git a/app/actions/resend/templates/drip/onboarding/02-coverage-charts.ts b/app/actions/resend/templates/drip/onboarding/02-coverage-charts.ts index 30918f03..76a6b63d 100644 --- a/app/actions/resend/templates/drip/onboarding/02-coverage-charts.ts +++ b/app/actions/resend/templates/drip/onboarding/02-coverage-charts.ts @@ -7,7 +7,7 @@ import { formatLines } from "@/utils/format-lines"; * Single repo (with benchmark): * Subject: Your test coverage is 15% * Body: - * Hi Alice - your test coverage is 15% for acme/backend. A 5K-line project on GitAuto has 89% coverage. See the chart: + * Hi Alice - I just measured your test coverage at 15% for acme/backend (~8K lines). For reference, a 5K-line project on GitAuto reached 89% coverage. See the chart: * * https://gitauto.ai/dashboard/charts * @@ -17,7 +17,7 @@ import { formatLines } from "@/utils/format-lines"; * Multi-repo (no benchmark): * Subject: Your test coverage across 3 repos is 72% * Body: - * Hi Alice - your weighted test coverage is 72% across 3 repos like acme/backend. See the chart: + * Hi Alice - I just measured your weighted test coverage at 72% across 3 repos like acme/backend (~8K lines). See the chart: * * https://gitauto.ai/dashboard/charts * @@ -37,17 +37,21 @@ export const generateCoverageChartsEmail = ( ) => { const pct = Math.round(ctx.ownerCoveragePct!); const multi = ctx.coverageRepoCount > 1; - const repo = ctx.repoMostNeedingCoverage ? `${ownerName}/${ctx.repoMostNeedingCoverage}` : ""; + const repoName = ctx.repoMostNeedingCoverage ? `${ownerName}/${ctx.repoMostNeedingCoverage}` : ""; + const repoWithLines = + repoName && ctx.repoMostNeedingCoverageLines + ? `${repoName} (~${formatLines(ctx.repoMostNeedingCoverageLines)} lines)` + : repoName; const repoDetail = multi - ? ` across ${ctx.coverageRepoCount} repos${repo ? ` like ${repo}` : ""}` - : repo - ? ` for ${repo}` + ? ` across ${ctx.coverageRepoCount} repos${repoWithLines ? ` like ${repoWithLines}` : ""}` + : repoWithLines + ? ` for ${repoWithLines}` : ""; const benchmarkLine = ctx.coverageBenchmark - ? ` A ${formatLines(ctx.coverageBenchmark.linesTotal)}-line project on GitAuto has ${ctx.coverageBenchmark.coveragePct}% coverage.` + ? ` For reference, a ${formatLines(ctx.coverageBenchmark.linesTotal)}-line project on GitAuto reached ${ctx.coverageBenchmark.coveragePct}% coverage.` : ""; - return `Hi ${firstName} - your ${multi ? "weighted " : ""}test coverage is ${pct}%${repoDetail}.${benchmarkLine} See the chart: + return `Hi ${firstName} - your ${multi ? "weighted " : ""}test coverage just came in at ${pct}%${repoDetail}.${benchmarkLine} See the chart: ${ABSOLUTE_URLS.GITAUTO.DASHBOARD.CHARTS} diff --git a/app/blog/posts/2024-11-24-what-are-dora-metrics.mdx b/app/blog/posts/2024-11-24-what-are-dora-metrics.mdx index 2b9cd4f1..35968d98 100644 --- a/app/blog/posts/2024-11-24-what-are-dora-metrics.mdx +++ b/app/blog/posts/2024-11-24-what-are-dora-metrics.mdx @@ -14,7 +14,7 @@ export const metadata = { # What are DORA Metrics? -[DORA (DevOps Research and Assessment) metrics](https://dora.dev/guides/dora-metrics-four-keys/) are a key benchmark for measuring software delivery performance. These metrics help teams assess their capabilities and drive improvements. +[DORA (DevOps Research and Assessment) metrics](https://dora.dev/guides/dora-metrics/) are a key benchmark for measuring software delivery performance. These metrics help teams assess their capabilities and drive improvements. ## The Four Key Metrics diff --git a/types/drip-emails.ts b/types/drip-emails.ts index 741205b5..26dac99c 100644 --- a/types/drip-emails.ts +++ b/types/drip-emails.ts @@ -29,6 +29,7 @@ export interface OwnerContext { unscheduledRepoNames: string[]; repoMostNeedingCoverage: string | null; repoMostNeedingCoveragePct: number | null; + repoMostNeedingCoverageLines: number | null; /** Anonymized similar-sized repo from another owner with higher coverage (>= 80%) */ coverageBenchmark: { linesTotal: number; coveragePct: number } | null; hasSetupPr: boolean;