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:
- How is work actually distributed across the team?
- Who is absorbing code review?
- How much work is shipping without ever being tagged to a release?
- Is the team overwhelmed, or does it just feel that way?
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.
| Jira | Issue 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. |
| GitHub | GraphQL search per repository for merged and unmerged pull requests, pulling author, merger, reviews, and head/base branches. |
| Output | One 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.
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.
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 load | Review was concentrated far more narrowly than anyone assumed. It had been invisible because review is not a ticket. |
| Untracked work | A significant volume of work carried no fix version — largely a process gap rather than emergency work, which was the assumption going in. |
| Overwhelm | Concurrency 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.