Skip to content

Reporting & analytics with jq

The Scripting guide lists starter jq patterns. This recipe expands them into a small analytics toolkit you can paste into a weekly report job.

All recipes assume:

Terminal window
WORKSPACE=myworkspace
REPO=myrepo

Use --all for complete reports, and --raw-output (or external jq -r) for raw strings or CSV.

  • count describes fetched results. bb pr list stops at --limit 25 by default, so .count maxes out at 25 and the Use --limit <n> or --all to see more. hint is suppressed under --json. Pass --all whenever the number itself is the answer.
  • --jq JSON-encodes its output. When you need a bare string for the shell or a file, add --raw-output or pipe --json into external jq -r.

bb pr list returns one state at a time, so fetch each separately and combine:

Terminal window
for state in OPEN MERGED DECLINED SUPERSEDED; do
count=$(bb pr list -w "$WORKSPACE" -r "$REPO" -s "$state" --all --json --jq '.count')
printf "%-12s %s\n" "$state" "$count"
done

Sample output:

OPEN 12
MERGED 340
DECLINED 8
SUPERSEDED 2

--state takes one value and defaults to OPEN. To group open PRs by target branch:

Terminal window
bb pr list -w "$WORKSPACE" -r "$REPO" --all --json --jq '
.pullRequests
| group_by(.destination.branch.name)
| map({ branch: .[0].destination.branch.name, count: length })
| sort_by(-.count)
'

bb pr diff --stat --json totals only its first diffstat page. For complete reports, fetch every page through bb api --paginate and sum the API’s lines_added and lines_removed fields.

#!/bin/bash
set -euo pipefail
WORKSPACE="${WORKSPACE:?set WORKSPACE}"
REPO="${REPO:?set REPO}"
total_added=0
total_deleted=0
pr_ids=$(bb pr list -w "$WORKSPACE" -r "$REPO" -s MERGED --all --json \
--jq '.pullRequests[].id')
for pr_id in $pr_ids; do
stats=$(bb api "/repositories/$WORKSPACE/$REPO/pullrequests/$pr_id/diffstat" \
--paginate --json)
totals=$(jq -er '
if (.values | type == "array") and all(.values[];
(.lines_added | type == "number") and
(.lines_removed | type == "number"))
then [([.values[].lines_added] | add // 0),
([.values[].lines_removed] | add // 0)] | @tsv
else error("Unexpected diffstat response") end
' <<< "$stats")
IFS=$'\t' read -r added deleted <<< "$totals"
total_added=$(( total_added + added ))
total_deleted=$(( total_deleted + deleted ))
sleep 1
done
printf 'Total added: %s\nTotal deleted: %s\n' "$total_added" "$total_deleted"

A PR can require several HTTP requests. Each changed line counts once per PR, so overlapping PRs can count the same line more than once. These totals measure PR diff sizes, not net repository growth.

Terminal window
bb pr list -w "$WORKSPACE" -r "$REPO" --all --json --jq '
.pullRequests
| group_by(.author.account_id // .author.uuid)
| map({
author: .[0].author.display_name,
account: (.[0].author.account_id // .[0].author.uuid),
count: length,
ids: [.[].id]
})
| sort_by(-.count)
'

This sorts authors by descending PR count and includes the IDs for drilldown.

Useful for identifying review load. Iterates over merged PRs and counts approvals per reviewer:

Terminal window
bb pr list -w "$WORKSPACE" -r "$REPO" -s MERGED --all --json --jq '
[
.pullRequests[].participants[]
| select(.approved == true)
| {account: (.user.account_id // .user.uuid), name: .user.display_name}
]
| group_by(.account)
| map({ reviewer: .[0].name, account: .[0].account, approvals: length })
| sort_by(-.approvals)
'

Use jq -r with @csv; without -r (or the built-in --raw-output), each CSV row comes out as a quoted JSON string.

Terminal window
bb pr list -w "$WORKSPACE" -r "$REPO" --all --json | jq -r '
["id","title","author","state","created_on","source","destination"],
(
.pullRequests[]
| [
.id,
.title,
(.author.nickname // .author.display_name),
.state,
.created_on,
.source.branch.name,
.destination.branch.name
]
)
| @csv
' > prs.csv
head -3 prs.csv

Sample output:

"id","title","author","state","created_on","source","destination"
42,"Add login button","alice","OPEN","2025-01-04T09:11:00.993890+00:00","feat/login","main"
43,"Fix typo in README","bob","OPEN","2025-01-04T11:22:41.118201+00:00","fix/typo","main"

The filter below measures creation to last update for merged PRs. It is an approximation of cycle time: comments and other edits after merging can move updated_on forward. Do not label it as the exact merge duration.

The filter handles fractional seconds and the UTC +00:00 offset used in these examples. Other timezone offsets need a timestamp parser that supports them. jq’s fromdateiso8601 expects whole seconds and a trailing Z.

Terminal window
bb pr list -w "$WORKSPACE" -r "$REPO" -s MERGED --all --json --jq '
def ts: sub("\\.[0-9]+"; "") | sub("\\+00:00"; "Z") | fromdateiso8601;
[
.pullRequests[]
| {
id,
title,
hoursToLastUpdate: ((.updated_on | ts) - (.created_on | ts)) / 3600
}
]
| sort_by(.hoursToLastUpdate)
'

The filter also accepts timestamps without fractional seconds.

For exact merge durations, first establish which activity field your repository’s API response uses for the transition to MERGED. Inspect every activity page with bb pr activity <id> --all --json. Do not substitute a merge-commit timestamp or assume .merge.date is always present.