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:
WORKSPACE=myworkspaceREPO=myrepoUse --all for complete reports, and --raw-output (or external jq -r) for raw strings or CSV.
countdescribes fetched results.bb pr liststops at--limit 25by default, so.countmaxes out at 25 and theUse --limit <n> or --all to see more.hint is suppressed under--json. Pass--allwhenever the number itself is the answer.--jqJSON-encodes its output. When you need a bare string for the shell or a file, add--raw-outputor pipe--jsoninto externaljq -r.
Count pull requests per state
Section titled “Count pull requests per state”bb pr list returns one state at a time, so fetch each separately and combine:
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"doneSample output:
OPEN 12MERGED 340DECLINED 8SUPERSEDED 2--state takes one value and defaults to OPEN. To group open PRs by target branch:
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)'Sum additions and deletions across PRs
Section titled “Sum additions and deletions across PRs”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/bashset -euo pipefail
WORKSPACE="${WORKSPACE:?set WORKSPACE}"REPO="${REPO:?set REPO}"total_added=0total_deleted=0pr_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 1done
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.
Group by author with counts
Section titled “Group by author with counts”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.
Top reviewers across merged PRs
Section titled “Top reviewers across merged PRs”Useful for identifying review load. Iterates over merged PRs and counts approvals per reviewer:
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)'CSV export
Section titled “CSV export”Use jq -r with @csv; without -r (or the built-in --raw-output), each CSV row comes out as a quoted JSON string.
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.csvSample 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"PR cycle time (created → merged)
Section titled “PR cycle time (created → merged)”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.
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.