Which raw code statistics belong in a CSV export: building a developer metrics schema
When an engineering manager asks for "the raw numbers" on their team's code, what exactly should land in that CSV file?
Last quarter, our platform team shipped a developer metrics portal for Meridian — an 80-engineer organization where every squad lead wanted exportable data for quarterly reviews. The first version of our CSV export dumped everything we had: commit hashes, file paths, cyclomatic complexity per function, churn counts, review latencies, and a dozen other fields scraped from our analysis pipeline. The file was 47 columns wide. Three engineering managers opened it, stared, and immediately asked us to simplify.
That moment forced a question we should have asked before writing a single line of export code: which raw code statistics actually belong in a CSV export, and which ones belong somewhere else? What follows is the schema we settled on after three iterations, the reasoning behind each field, and the hard numbers that shaped our decisions.
What raw code statistics should a developer metrics CSV export include?
The answer depends on who opens the file. At Meridian, the CSV consumer was always an engineering manager or tech lead — never a data scientist. They wanted to slice team data in Excel, build pivot tables for skip-level reviews, and occasionally cross-reference with their own notes. That use case narrowed the field list dramatically.
Our final schema included 14 columns, organized into four logical groups:
The four metric categories we kept
Identity columns: developer_email, team_name, repository, date_range_start, date_range_end. These five fields let managers filter and group without leaving their spreadsheet.
Throughput statistics: commit_count, pr_count, lines_added, lines_removed, files_touched. Raw activity volume — not interpreted, not normalized.
Quality-adjacent statistics: review_iterations_avg, pr_merge_latency_hours, churn_ratio. These are the numbers that invite follow-up questions rather than deliver verdicts.
Complexity statistics: avg_cyclomatic_complexity, complexity_delta. One snapshot, one trend. Nothing more.
Fourteen columns. Down from forty-seven. The managers who complained about the first version called the final export "actually usable" — which, coming from people who'd rather not touch data exports at all, counted as high praise.
How do you separate signal from noise when choosing code metrics fields?
The temptation in building any metrics export is inclusiveness. You already computed the data. Why not ship it? We learned the hard way that a CSV export is not a data lake. Every extra column imposes a cognitive tax on the reader and a maintenance tax on the engineer who owns the schema.
Our filtering process came down to three questions we asked about every candidate field:
The three-question filter
1. Can a manager act on this number within five minutes of opening the file? If the answer was no, the field didn't make the cut. Cyclomatic complexity per function failed this test — a manager can't meaningfully act on 3,000 rows of per-function complexity data in a spreadsheet. Average cyclomatic complexity per developer, per sprint, passed.
2. Does this number mean something without a dashboard to visualize it? Trend data belongs in charts. CSV exports serve tabular thinkers. We dropped fields like "complexity_over_time" — a JSON array serialized into a single cell — because no one was ever going to parse that in Excel. Instead, we shipped complexity_delta: a single signed integer representing net change over the date range.
3. Will two engineers interpret this field the same way? "Lines of code" is notoriously ambiguous. Does it include comments? Blank lines? Generated code? We resolved this by being explicit in our column headers: lines_added_logical (excluding blanks and auto-generated blocks) rather than the generic "loc." Clarity in naming prevented the misreadings that would have undermined trust in the entire export.
Which commit-level statistics belong in a CSV schema versus an API payload?
This was the hardest distinction we drew, and it took two failed exports to get right. Our initial instinct was to include commit-level granularity — every commit as its own row — because that felt like "raw data." In practice, a two-week sprint for an 8-person team generated roughly 340 commits. That's 340 rows per team per sprint. For an organization running 10 squads, a quarterly export produced over 10,000 rows. Managers glazed over.
The aggregation boundary
We drew a clear line: CSV exports are aggregated to the developer-per-date-range grain. One row per developer, per repository, per sprint (or whatever date range the manager selects). If someone needs commit-level detail, they use the API endpoint that returns paginated JSON with full commit metadata. The CSV is for summary analysis; the API is for forensic investigation.
This decision eliminated several fields that had been polluting the export:
- commit_hash — useful for API lookups, meaningless in a spreadsheet summary
- commit_message — too verbose, impossible to aggregate, and frankly nobody read it in CSV form
- file_path — exploded row counts without adding analytical value at the summary level
- per_commit_churn — replaced by aggregate churn_ratio, which expressed the same insight in one number
The API payload retained all of these. The CSV retained none. Both consumers got what they needed.
A worked example: the churn_ratio calculation
To make this concrete, here's how we computed churn_ratio for one developer during Sprint 14. Developer "k.chen" across the two-week sprint:
- Total lines added across all commits: 1,247
- Lines removed within 48 hours of their own initial addition (self-churn): 384
- Lines modified by others within 48 hours of k.chen's commits (peer-churn): 96
Churn ratio = (384 + 96) / 1,247 = 0.384. We rounded to three decimal places in the CSV. The number told the manager that roughly 38% of what k.chen wrote was revised or reverted within two days — a signal worth a conversation, not a verdict. That single ratio replaced what would otherwise have been six raw columns of churn data.
How should you structure CSV columns for code churn and complexity metrics?
Column ordering matters more than people admit. Managers read CSVs left to right, top to bottom. The first columns set the mental context for everything that follows. We organized our 14 columns in a deliberate sequence that told a story: who, where, when, how much, how fast, how stable, how complex.
The final column order
Here's the exact schema we shipped, in order:
- developer_email — identity anchor, always first
- team_name — grouping dimension managers filter on immediately
- repository — context for all subsequent numbers
- date_range_start — ISO 8601, no ambiguity
- date_range_end — ISO 8601, paired with start
- commit_count — raw throughput baseline
- pr_count — collaboration throughput
- lines_added_logical — volume signal, excluding noise
- lines_removed_logical — paired with added for full picture
- files_touched — breadth of impact, distinct from volume
- review_iterations_avg — quality proxy, rounded to one decimal
- pr_merge_latency_hours — flow proxy, rounded to nearest hour
- churn_ratio — stability signal, three decimal places
- avg_cyclomatic_complexity — maintainability snapshot, one decimal
We deliberately omitted complexity_delta from the initial export and added it in version 2 after three managers independently requested it. That taught us another lesson: ship the minimal useful schema first, let real usage drive additions. Every field you add proactively is a field you'll hesitate to remove.
Why we excluded per-file complexity
One field we considered and rejected was max_cyclomatic_complexity — the highest complexity value across all functions the developer touched. The number sounds useful: "which developer wrote the most complex code this sprint?" In practice, it was misleading. A developer who reviewed one PR touching a legacy function with complexity 47 would appear worse than a developer who wrote three new functions at complexity 12 each. The aggregate average, combined with complexity_delta, told a more honest story.
What developer identity fields are essential for CSV export joins?
Identity sounds trivial. It isn't. The first version of our export included only a GitHub username. Six managers asked, within the first week, how to map "kchen-dev" to a real person in their HR system. We had assumed the GitHub handle was sufficient. It wasn't.
The identity trio we settled on
After that failure, we built a three-field identity chain:
developer_email served as the primary key. Every developer at Meridian had a company email, and every internal system — HR, payroll, Jira, Slack — keyed off it. Email was the universal join column.
team_name came from our team registry, not from GitHub organization membership. GitHub orgs at Meridian were messy — developers belonged to multiple orgs, and org names didn't match squad names. We maintained a separate mapping table that reconciled GitHub usernames to team assignments, updated monthly by engineering directors. The CSV export pulled team_name from that table, not from source control.
repository was the third identity field, and we debated its placement. Some argued it belonged in a separate "context" section rather than "identity." We kept it in the identity block because at Meridian, repository ownership was a team-level concern — knowing which repo a developer worked in was essential to interpreting their metrics. A developer with high churn in a legacy migration repo meant something different from the same churn in a greenfield service.
The field we refused to include
Two managers asked us to include a "performance_rating" column — a subjective score they'd maintain in a separate spreadsheet. We declined. Mixing subjective evaluations into a raw statistics export would have blurred the line between measured data and opinion, and would have made the CSV feel like a performance review document rather than an analytical tool. The CSV's value depended on its neutrality. Every field had to be machine-derived from source control data, not human-assigned.
How do you validate a developer metrics schema before shipping the export?
Validation is where most metrics projects quietly fail. The schema looks right. The numbers populate. The file downloads. Then someone opens it and finds that developer_email is empty for contractors who only have GitHub accounts, or that commit_count includes merge commits that inflate the total by 30%, or that date_range_end is formatted differently depending on who triggered the export.
We built a three-stage validation process after our first embarrassing bug — a churn_ratio that divided by zero when a developer had lines_removed but zero lines_added, producing a CSV full of "Infinity" strings.
Stage 1: Synthetic data tests
Before touching real repository data, we generated synthetic developer profiles with known values. Developer A: 10 commits, 200 lines added, 50 lines removed, 4 PRs, churn_ratio of 0.25. We ran the export pipeline against this synthetic input and verified every cell. This caught a rounding error in avg_cyclomatic_complexity that would have shipped complex numbers (as in, imaginary numbers) to managers' spreadsheets. Yes, really.
Stage 2: Historical reconciliation
We pulled export data for the previous quarter and manually reconciled five randomly selected developers against raw Git log output. For one developer, our commit_count showed 34 while git log showed 47. The discrepancy: our pipeline excluded merge commits, which was intentional, but we hadn't documented it. The number was correct by our definition — but the definition needed to be explicit. We added a header comment to the CSV file (a line beginning with #) explaining that commit_count excluded merge commits. Not every CSV reader respects comment lines, but the documentation mattered for our own team's sanity.
Stage 3: Manager preview and feedback loop
Before general release, we gave three engineering managers access to the export for their teams only. We asked them to do one thing: build a pivot table using the CSV and tell us what was missing or confusing. Two of the three independently asked for a "sprint_name" column — we'd been using date ranges, but managers thought in sprint names. We added sprint_name as a fifth identity field, derived from our sprint calendar mapping.
The third manager asked for something we didn't add: a "productivity_score" column that combined throughput, quality, and complexity into a single number. We explained why composite scores are dangerous in raw exports — they hide the component values that explain the score, and they imply a weighting that may not match every team's priorities. She accepted the reasoning. The raw fields, she admitted, gave her more to work with in conversation than a single number would have.
What did the final schema teach us about developer metrics exports?
Three lessons survived the entire process and now guide every metrics export we build:
Raw doesn't mean exhaustive. A raw code statistics export should contain measured, uninterpreted values — but only the ones a spreadsheet consumer can use. Exhaustiveness belongs in the API. The CSV is a curated summary, not a data dump.
Every column needs a defense, not just a source. Knowing where a number comes from is necessary but insufficient. You also need to defend why it belongs in a CSV versus an API response, why it's aggregated at this grain, and why it's named this way. If you can't defend all three, cut it.
The schema is never finished. We shipped version 1 with 14 columns. Version 2 added complexity_delta and sprint_name after manager feedback. Version 3 will likely add review_comment_count after two managers requested it last month. The schema evolves with the organization. Treating it as a one-time design decision guarantees it will drift from relevance.
The 80 engineers at Meridian never see the schema. They see quarterly conversations that reference numbers those managers pulled from a CSV. The schema's success isn't measured in field count or computational sophistication. It's measured in whether a manager can open a file, find a number, ask a better question, and close the file within ten minutes. That's the bar. Fourteen columns met it. Forty-seven didn't.