DEV Community

Sattyam Jain
Sattyam Jain

Posted on

Measure which of your GitHub issues actually got a reply, in one script

I filed 68 issues in repos I do not own over six weeks. 42 got zero replies.

When I sorted them by what the issue was asking for, the split was sharp: issues that pointed at a defect in the maintainer's system got answered 9 times out of 12, and issues that offered to do work for the maintainer got answered 5 times out of 13. I wrote up why I think that happens in the companion piece. This post is just the script, so you can run the same count on yourself.

No dependencies beyond the standard library. No token needed for public data, though you will hit the unauthenticated rate limit fast without one.

The script

#!/usr/bin/env python3
"""Count which of your GitHub issues got a reply, and which died.

Usage:
    python issue_reply_rate.py <username> [--since 2026-08-04] [--token $GITHUB_TOKEN]

Counts only issues in repos you do NOT own, because replying to yourself
is not the thing being measured.
"""
import argparse
import json
import os
import sys
import urllib.error
import urllib.request

API = "https://api.github.com/search/issues"


def fetch(username, since, token):
    query = f"author:{username} type:issue created:>{since}"
    url = f"{API}?q={urllib.parse.quote(query)}&per_page=100"
    req = urllib.request.Request(url, headers={
        "Accept": "application/vnd.github+json",
        "User-Agent": f"issue-reply-rate/{username}",
    })
    if token:
        req.add_header("Authorization", f"Bearer {token}")
    try:
        with urllib.request.urlopen(req, timeout=30) as resp:
            return json.load(resp)
    except urllib.error.HTTPError as exc:
        if exc.code == 403:
            sys.exit("Rate limited. Pass --token, or wait an hour.")
        raise


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("username")
    ap.add_argument("--since", default="2026-08-04")
    ap.add_argument("--token", default=os.environ.get("GITHUB_TOKEN"))
    args = ap.parse_args()

    data = fetch(args.username, args.since, args.token)
    total = data.get("total_count", 0)
    items = data.get("items", [])

    external = []
    for item in items:
        repo = item["repository_url"].split("/repos/", 1)[1]
        owner = repo.split("/", 1)[0]
        if owner.lower() == args.username.lower():
            continue
        external.append({
            "repo": repo,
            "number": item["number"],
            "comments": item["comments"],
            "state": item["state"],
            "created": item["created_at"][:10],
            "title": item["title"],
        })

    silent = [i for i in external if i["comments"] == 0]
    answered = [i for i in external if i["comments"] > 0]

    print(f"search total_count : {total} (API returns at most 100 per page)")
    print(f"in repos you own   : {len(items) - len(external)}")
    print(f"in others' repos   : {len(external)}")
    if not external:
        return
    pct = 100 * len(silent) / len(external)
    print(f"  zero replies     : {len(silent)}  ({pct:.0f}%)")
    print(f"  got a reply      : {len(answered)}")
    print()
    print("THE ZEROS, oldest first. Read the titles and ask what you wanted them to DO:")
    for i in sorted(silent, key=lambda x: x["created"]):
        print(f"  {i['created']}  {i['repo']}#{i['number']}  {i['title'][:70]}")
    print()
    print("THE ONES THAT LANDED, by reply count:")
    for i in sorted(answered, key=lambda x: -x["comments"]):
        print(f"  {i['comments']:2d} replies  {i['repo']}#{i['number']}  {i['title'][:64]}")


if __name__ == "__main__":
    import urllib.parse
    main()
Enter fullscreen mode Exit fullscreen mode

Running it

export GITHUB_TOKEN=ghp_...          # optional, but you will want it
python issue_reply_rate.py <your-username> --since 2026-08-04
Enter fullscreen mode Exit fullscreen mode

Output against my own account this morning:

search total_count : 108 (API returns at most 100 per page) in repos you own : 32 in others' repos : 68 zero replies : 42 (62%) got a reply : 26

Three caveats, because the number is easy to misread

The search API caps at 100 results per page. If your total_count is higher than 100, this script is only seeing the first page. Paginate with &page=2 if you need the rest.

comments counts your own comments too. One of my threads shows eight comments, five of which are mine and none of which are a maintainer. If you want the honest version, fetch each issue's comment list and filter out your own author id. That turns one API call into N and will eat your rate limit, so I left it out of the default path.

A reply is not an outcome. The thread above produced a pull request that has now sat open for thirteen days with zero review comments. Reply rate measures whether you got attention, not whether anything moved. It is the cheap metric and it is worth knowing that while you read it.

The part worth doing after you run it

Sort your zeros and read them, not the titles, the bodies. For each one, work out what you were asking the person on the other end to decide.

Mine split cleanly. The zeros were mostly me offering to write code for someone, which asks a stranger to commit to reviewing work that does not exist yet. The replies were mostly me pointing at something specific and checkable in code they already understand.

The first one feels more generous. It is the one that goes unanswered.

Top comments (0)