← Milos Ozegovic

Case study

Measuring a team without misrepresenting it

A delivery-analytics pipeline over Jira and GitHub. The charts were the easy part; deciding what the numbers were allowed to claim was not.

Context~30-contributor engineering org
WindowTen weeks
StackJira API, GitHub GraphQL, vanilla JS

On the data shown here. Every figure and name in the charts below is synthetic. The methodology is real; the numbers are not, and the organisation is not identified.

The question

The questions that prompted this were ordinary ones, and they came up in the way they usually do — during planning, as assertions nobody could check:

Jira knew part of the answer and GitHub knew another part, and neither could see the other. So I built something that joined them.

Pulling the data

Two sources, both awkward in their own way.

JiraIssue search with pagination, plus per-week status-history queries to reconstruct what was in progress when. Every paged result asserted against its own count query, so a silently truncated page could not pass unnoticed.
GitHubGraphQL search per repository for merged and unmerged pull requests, pulling author, merger, reviews, and head/base branches.
OutputOne JSON dataset, rendered by a static page that runs from the local filesystem with no server, no build step and no dependencies.

The last row was a deliberate constraint. A report that needs a running service to be looked at is a report nobody looks at. This one is a folder you can open.

The decisions that actually mattered

Joining two systems means making judgement calls, and every one of them changes the numbers. These are the four that moved things most.

Who owns a ticket

Jira had both an Assignee and a custom Developer field, and they disagreed often — because on this team the assignee was frequently the QA engineer, not the person who wrote the code. Using Assignee would have quietly credited testing work as development work.

So: Developer first, Assignee as fallback, unassigned if neither. I recorded the coverage of each field so a reader could see how much the fallback was doing.

Not all pull requests are work

A meaningful share of PRs were branch plumbing — develop → master syncs and release-branch merges. They are real activity but they are not feature work, and they cluster heavily on whoever runs releases.

Left in, the release engineer looks like the most productive person on the team. I classified and excluded them from every feature-level figure, and stated both the count and the distortion in the methodology.

PRs per contributor — feature work vs branch plumbing (illustrative) AB CD EF ← release engineer feature plumbing
Contributor C does not write more code than everyone else. They run the release train.

Averages hide the problem

Lead time measured from creation to resolution — which includes sitting in the backlog, and is therefore not "how long the work took". I said so, and reported the median alongside the 90th percentile, plus the share of tickets over thirty days.

A mean would have been one number and a more comfortable one. The tail is where the actual problem lives.

Concurrency, not throughput

"Overwhelmed" is not a count of finished tickets. I measured how many distinct tickets a person had in progress during each week — work in flight, not work completed. Someone finishing a lot while juggling three things is in a different situation from someone finishing the same amount while juggling fifteen.

Median tickets in flight per week (illustrative) 0 8 16 wk 1 wk 10
Throughput held steady across this period. Concurrency did not — which is the finding.

The part I care about most

Some contributors carried a substantial number of tickets and showed no pull requests at all. Their work — data engineering, some operations — did not flow through the repositories the pipeline could see.

A chart does not know that. It renders an empty bar, and an empty bar next to your colleague's name is a claim about them.

Blank columns mean not visible, never did nothing.

That sentence went into the report, in the methodology section, next to the list of people it applied to and the reason the data was missing. I also documented which repository the access token could not see, so the gap was attributable rather than mysterious.

This is the part of the work I would defend hardest. Delivery metrics get read by people with authority over the careers of the people being measured. A number presented without its limits is not neutral — it is an argument, and someone will act on it. Stating what the data cannot support is not a disclaimer at the bottom. It is the difference between a tool and a weapon.

Building it

The rendering is hand-written: a small set of SVG chart primitives — axis-tick selection, path generation for bars, stacked and grouped layouts, a tooltip positioner — plus a shared stylesheet. No charting library, no build step, no network requests. The charts on this page are drawn the same way.

That was partly taste and partly durability. A report with no dependencies still opens in three years.

What it changed

Review loadReview was concentrated far more narrowly than anyone assumed. It had been invisible because review is not a ticket.
Untracked workA significant volume of work carried no fix version — largely a process gap rather than emergency work, which was the assumption going in.
OverwhelmConcurrency rose steadily while throughput stayed flat — the measurable version of a feeling people had already reported.

What I would do differently

I would agree the questions with the people being measured first. I built this to answer planning questions and was careful about how the numbers were framed. But being careful on someone's behalf is not the same as asking them, and people have a reasonable interest in how they are counted.

I would automate the refresh sooner. A point-in-time snapshot answers a question once. Most of these questions are only interesting as trends, and a report that requires someone to re-run it by hand becomes a report nobody re-runs.

Internal tooling, 2026. Figures and names shown here are synthetic. — Back to milosozegovic.com