feat: add usage-stats.sh — one-command adoption metrics - #98
Conversation
Tracks real usage across every public surface from a single command: crates.io downloads, Homebrew analytics, GitHub traffic, release binary downloads, public `uses:` of the action, and HN mentions. All signals come from public APIs — no user telemetry needed. Co-Authored-By: Claude <noreply@anthropic.com>
📝 WalkthroughWalkthroughAdded ChangesUsage metrics reporting
Estimated code review effort: 2 (Simple) | ~10 minutes 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@scripts/usage-stats.sh`:
- Around line 32-36: Update the Homebrew analytics jq expression in the
usage-stats section to bind `.analytics.install` as `$i`, then read the 30d,
90d, and 365d values independently from `$i` for their corresponding labels,
preserving 0 as the fallback for missing values.
- Line 17: Update scripts/usage-stats.sh so 403 responses from GitHub traffic
and clones requests do not terminate the script under set -e; catch those
endpoint failures, print an unavailable status, and continue producing the
remaining adoption metrics.
- Line 20: Update the usage-counting logic in scripts/usage-stats.sh to fetch
matching code-search items, require workflow uses: references to ACTION_REPO,
retain only public repositories, and deduplicate by .repository.full_name before
counting. Remove the hard-coded “minus 2-3” adjustment and ensure the query
references ACTION_REPO rather than a fixed action name.
- Around line 52-54: Update the Hacker News count assignment for hn to use jq’s
alternative operator, defaulting a missing or null .nbHits value to 0 before
assignment; keep the existing curl request and output behavior unchanged.
- Around line 45-46: Update the release download aggregation command in the
release binary downloads section to use gh api pagination with slurped results
before reducing over all release assets, ensuring the download total includes
every release page.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 6c9970a9-8f82-464d-9740-08698768c2dd
📒 Files selected for processing (1)
scripts/usage-stats.sh
| # and our own dogfood jobs inflate raw counts. The code-search hit count | ||
| # is the strongest signal — it means someone actually wired the action | ||
| # into their CI. Its hits include our own repos' dogfood jobs. | ||
| set -euo pipefail |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== files =="
git ls-files | rg '(^|/)usage-stats\.sh$|scripts/usage-stats.sh' || true
echo "== usage-stats.sh =="
if [ -f scripts/usage-stats.sh ]; then
cat -n scripts/usage-stats.sh
fi
echo "== gh command references =="
rg -n 'gh api|GH_|GITHUB_|traffic|views|traffic' scripts/usage-stats.sh .github README.md package.json 2>/dev/null | head -200 || true
echo "== diff stat/name-status if available (focused) =="
git diff -- scripts/usage-stats.sh | sed -n '1,220p' || trueRepository: FIERsity/envorigin
Length of output: 4957
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== bash set -e behavior probe (gh API not invoked) =="
bash -c 'set -e
function failing_cmd { echo >&2 gh: API error; return 4; }
echo before
failing_cmd || true
echo after'
bash -c 'set -e
function failing_cmd { echo >&2 gh: API error; return 4; }
echo before
failing_cmd
echo after' 2>&1; echo "probe_exit=$?"
echo "== gh API 403 behavior probe from current repo if gh available =="
if command -v gh >/dev/null 2>&1; then
git config --get remote.origin.url
echo "gh version:"
gh --version | head -3
echo "gh api /repos/FIERsity/envorigin/traffic/views:"
gh api "repos/FIERsity/envorigin/traffic/views" --jq '" views: \(.count) unique visitors: \(.uniques)"' 2>&1 || true
echo "gh api search code:"
gh api "search/code?q=FIERsity%2Fenvorigin-action&per_page=1" --jq '.total_count' 2>&1 || true
echo "gh api public crates download_count:"
gh api "repos/FIERsity/envorigin/releases" --jq 'reduce .[].assets[]? as $a (0; . + $a.download_count) | " prebuilt binaries downloaded: \(.)"' 2>&1 || true
else
echo "gh not available"
fiRepository: FIERsity/envorigin
Length of output: 258
🌐 Web query:
GitHub REST API views traffic clones access require read permission public repository https://docs.github.com/rest/traffic
💡 Result:
To access repository traffic data—such as clones (GET /repos/{owner}/{repo}/traffic/clones) and page views (GET /repos/{owner}/{repo}/traffic/views)—via the GitHub REST API, you must have push-level access to the repository [1][2][3]. Even for public repositories, read-only access is insufficient; the API requires you to be a repository collaborator or owner with push-equivalent permissions [3][4]. Requests made without these privileges will return a 403 Forbidden error indicating that push access is required [3][4][5]. When using personal access tokens (PATs), you must ensure your token has the necessary permissions. For fine-grained PATs, this typically requires Administration: Read access (or higher) for the repository [6][7][8]. For classic PATs, the repo scope is generally required [3][8]. Note that the traffic API only exposes data for the most recent 14 days [1][7][8].
Citations:
- 1: https://docs.github.com/en/rest/metrics/traffic
- 2: https://docs.github.com/en/enterprise-cloud@latest/rest/metrics/traffic
- 3: https://tarka.ai/playbook/fundamentals/github-traffic-api
- 4: https://stackoverflow.com/questions/66731092/error-in-retrieving-the-number-of-clones-from-one-of-my-git-repositories
- 5: https://docs.github.com/en/rest/metrics/traffic?apiVersion=2022-11-28
- 6: https://docs.github.com/en/rest/authentication/permissions-required-for-fine-grained-personal-access-tokens
- 7: https://github.com/gtapps/gh-traffic-stats
- 8: https://github.com/fivetran/fivetran_connector_sdk/tree/main/connectors/github_traffic
Do not treat missing GitHub traffic permissions as a script failure.
The traffic and clones endpoints require push-level GitHub access, even on public repositories. With set -e, a 403 from those calls exits scripts/usage-stats.sh before the rest of the adoption metrics print. Catch these results and print unavailable status, or document the required permissions in the script header.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@scripts/usage-stats.sh` at line 17, Update scripts/usage-stats.sh so 403
responses from GitHub traffic and clones requests do not terminate the script
under set -e; catch those endpoint failures, print an unavailable status, and
continue producing the remaining adoption metrics.
| set -euo pipefail | ||
|
|
||
| REPO="FIERsity/envorigin" | ||
| ACTION_REPO="FIERsity/envorigin-action" |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Tracked files matching usage-stats:"
fd -a 'usage-stats\.sh$' . || true
echo
if [ -f scripts/usage-stats.sh ]; then
echo "Line count:"
wc -l scripts/usage-stats.sh
echo
echo "Relevant source:"
cat -n scripts/usage-stats.sh | sed -n '1,120p'
fi
echo
echo "Search for ACTION_REPO/usage-count/action usage in repo:"
rg -n "ACTION_REPO|total_count|usage|code-search|api\.github\.com|/search/code" . || trueRepository: FIERsity/envorigin
Length of output: 4135
🌐 Web query:
GitHub REST Search Code API total_count matches count files is_inclusive private repositories search syntax context
💡 Result:
The GitHub REST API search code endpoint operates using specific constraints regarding results, indexing, and syntax as of August 6, 2026. Total Count and Results The REST API's total_count field indicates the estimated number of search results [1]. However, the API is limited to returning up to 1,000 results for any given search query [2]. Additionally, the API currently utilizes a legacy search engine that differs from the modern web-based search experience; consequently, total_count values returned by the API may differ from results observed in the GitHub web interface [3]. If a search takes too long, the API returns an incomplete_results boolean as true [2]. Private Repository Access To search private repositories, you must authenticate with a token (such as a Personal Access Token or GitHub App token) that has the appropriate repository permissions [4][5][6]. The API searches up to 4,000 private repositories to which you have access, specifically prioritizing the most recently updated ones among the first 10,000 accessible repositories [4]. Search Syntax and Behavior - Range Operators: The range operator (..) is inclusive on both sides [7][8]. For example, stars:1..3 includes repositories with 1, 2, or 3 stars [8]. - Excluding Results: - Qualifiers: You can exclude results matching a specific qualifier by prefixing it with a hyphen (-), such as -language:python [9][10]. - Keywords: You can exclude files containing specific words using the NOT operator (e.g., hello NOT world) [9][11]. - Search Scoping: Code searches require at least one user, organization, or repository qualifier to be specified in the query [4][6]. Context and Limitations The REST API code search endpoint is subject to strict rate limits (10 requests per minute) and requires authentication [1][2]. It also only indexes repositories with fewer than 500,000 files, only searches default branches, and restricts file size to under 384 KB [4]. Because the REST API still relies on legacy infrastructure, modern search features available on the GitHub website (like semantic search) may not be reflected in REST API results [3].
Citations:
- 1: https://docs.github.com/en/enterprise-cloud@latest/rest/search/search
- 2: https://github.com/github/docs/blob/main/content/rest/search/search.md
- 3: Searching for Code, Web and API differ github/rest-api-description#2956
- 4: https://docs.github.com/en/search-github/searching-on-github/searching-code
- 5: https://stackoverflow.com/questions/68280756/validation-failed-error-on-searching-in-private-repository-with-installation-o
- 6: https://stackoverflow.com/questions/24132790/how-to-search-for-code-in-github-with-github-api
- 7: Inaccurate explanation of date range queries github/docs#33953
- 8: https://stackoverflow.com/questions/14733082/is-there-an-and-syntax-for-github-search
- 9: https://docs.github.com/en/search-github/getting-started-with-searching-on-github/understanding-the-search-syntax
- 10: https://cli.github.com/manual/gh_search
- 11: Clarify information on excluding results from searches github/docs#23461
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Read search code matching item shape from installed gh help/docs if available:"
if command -v gh >/dev/null 2>&1; then
gh help api search/code --help 2>/dev/null || true
echo
gh search code --help 2>/dev/null || true
else
echo "gh not available"
fi
echo
echo "Behavioral probe: parse current jq query from script and inspect jq available fields without calling GitHub (static only)."
python3 - <<'PY'
import pathlib, re
p = pathlib.Path("scripts/usage-stats.sh")
s = p.read_text()
m = re.search(r'count=\$"\([^"$\n]*\.total_count"\)', s)
print(m.group(0) if m else "jq query containing .total_count not found")
print(s[s.index("== Public repos using the action"):s.index("== Hacker News mentions")])
PYRepository: FIERsity/envorigin
Length of output: 3609
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Inspect remaining actions workflow usage to understand context for dogfood adjustment"
find . -type f \( -path '*/.github/workflows/*' -o -path '*workflows*' \) -print | sort | head -200
rg -n --hidden --glob '!*.lock' --glob '!node_modules/**' 'envorigin-action|FIERsity/envorigin|envorigin' .github . || trueRepository: FIERsity/envorigin
Length of output: 23339
Count distinct public repositories that actually use the action.
search/code returns matching files, and total_count is not a distinct repository count. The query also does not require .github/workflows + uses: syntax, and an authenticated request can include private repositories available to the token. Fetch matching items, filter for public repositories with workflow uses: action entries, deduplicate .repository.full_name, and remove the hard-coded “minus 2-3” adjustment. Use ACTION_REPO for the action reference.
🧰 Tools
🪛 Shellcheck (0.11.0)
[warning] 20-20: ACTION_REPO appears unused. Verify use (or export if used externally).
(SC2034)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@scripts/usage-stats.sh` at line 20, Update the usage-counting logic in
scripts/usage-stats.sh to fetch matching code-search items, require workflow
uses: references to ACTION_REPO, retain only public repositories, and
deduplicate by .repository.full_name before counting. Remove the hard-coded
“minus 2-3” adjustment and ensure the query references ACTION_REPO rather than a
fixed action name.
Source: Linters/SAST tools
| section "== Homebrew (brew install envorigin) ==" | ||
| code="$(curl -s -o /dev/null -w '%{http_code}' https://formulae.brew.sh/api/formula/envorigin.json)" | ||
| if [ "$code" = "200" ]; then | ||
| curl -s https://formulae.brew.sh/api/formula/envorigin.json \ | ||
| | jq -r '.analytics.install["30d"] as $a | " 30d: \($a["envorigin"] // 0) 90d: \($a["envorigin"] // 0) 365d: \($a["envorigin"] // 0)"' |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '--- file excerpt ---\n'
sed -n '1,70p' scripts/usage-stats.sh
printf '\n--- jq expression target file shape (live read-only probe) ---\n'
python3 - <<'PY'
import json, urllib.request
url = "https://formulae.brew.sh/api/formula/envorigin.json"
with urllib.request.urlopen(url, timeout=20) as r:
data = json.load(r)
analytics = data.get("analytics", {}) or {}
install = analytics.get("install", {}) or {}
for k in ["30d", "90d", "365d"]:
v = install.get(k, {}) or {}
print(f"{k}:", v.get("envorigin"), type(v.get("envorigin")))
PYRepository: FIERsity/envorigin
Length of output: 247
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '--- file excerpt ---\n'
sed -n '1,70p' scripts/usage-stats.sh
printf '\n--- jq expression target file shape (live read-only probe) ---\n'
python3 - <<'PY'
import json, urllib.request
url = "https://formulae.brew.sh/api/formula/envorigin.json"
with urllib.request.urlopen(url, timeout=20) as r:
data = json.load(r)
analytics = data.get("analytics", {}) or {}
install = analytics.get("install", {}) or {}
for k in ["30d", "90d", "365d"]:
v = install.get(k, {}) or {}
print(f"{k}: {v.get('envorigin')} {type(v.get('envorigin'))}")
PYRepository: FIERsity/envorigin
Length of output: 247
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- file excerpt ---'
sed -n '1,70p' scripts/usage-stats.sh
printf '%s\n' ''
printf '%s\n' '--- jq expression target file shape read-only probe ---'
python3 - <<'PY'
import json
import urllib.request
url = "https://formulae.brew.sh/api/formula/envorigin.json"
with urllib.request.urlopen(url, timeout=20) as r:
data = json.load(r)
analytics = data.get("analytics") or {}
install = analytics.get("install") or {}
for k in ["30d", "90d", "365d"]:
v = install.get(k) or {}
print(f"{k}: {v.get('envorigin')} {type(v.get('envorigin')).__name__}")
PYRepository: FIERsity/envorigin
Length of output: 5331
🌐 Web query:
Homebrew formulae API analytics install object contains 30d 90d 365d
💡 Result:
The Homebrew Analytics JSON API provides aggregate usage data for formulae and other events, typically structured by category and a specified timeframe [1][2]. The "install" event category data is accessible via endpoints that incorporate the time range as a path parameter, specifically: 30d (30 days), 90d (90 days), and 365d (365 days) [1][3]. The general structure of these API endpoints follows the pattern: https://formulae.brew.sh/api/analytics/${CATEGORY}/${DAYS}.json [1] Where: - ${CATEGORY}: The event category (e.g., install, install-on-request, build-error) [1][4]. - ${DAYS}: The duration of the data period, which must be 30d, 90d, or 365d [1]. For example, to retrieve install analytics for the last 365 days, one would access: https://formulae.brew.sh/api/analytics/install/365d.json [5] Data is also available grouped by specific subsets, such as homebrew-core: https://formulae.brew.sh/api/analytics/${CATEGORY}/homebrew-core/${DAYS}.json [1][2] These reports are derived from raw events stored in InfluxDB, which Homebrew retains for 365 days [3][6]. The JSON responses typically contain metadata including the category, start and end dates, total event counts, and a breakdown of items (formulae) with their respective installation counts [1][6].
Citations:
- 1: https://formulae.brew.sh/docs/api/
- 2: https://formulae.brew.sh/analytics/
- 3: https://docs.brew.sh/Analytics
- 4: https://github.com/Homebrew/brew/blob/master/docs/Analytics.md
- 5: https://formulae.brew.sh/analytics/install/365d/
- 6: https://docs.brew.sh/rubydoc/Homebrew/DevCmd/FormulaAnalytics.html
Read each Homebrew analytics period independently.
Lines 34-36 bind .analytics.install["30d"] to $a, then reuses that value for the 90-day and 365-day labels. Read .analytics.install["90d"] and .analytics.install["365d"] from the same $i object instead.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@scripts/usage-stats.sh` around lines 32 - 36, Update the Homebrew analytics
jq expression in the usage-stats section to bind `.analytics.install` as `$i`,
then read the 30d, 90d, and 365d values independently from `$i` for their
corresponding labels, preserving 0 as the fallback for missing values.
| section "== Release binary downloads (all releases) ==" | ||
| gh api "repos/$REPO/releases" --jq 'reduce .[].assets[]? as $a (0; . + $a.download_count) | " prebuilt binaries downloaded: \(.)"' |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf 'Repository files matching usage-stats.sh:\n'
fd -a 'usage-stats\.sh$' . || true
printf '\nTarget file excerpt:\n'
if [ -f scripts/usage-stats.sh ]; then
nl -ba scripts/usage-stats.sh | sed -n '1,90p'
fi
printf '\nSearch for GH_API pagination flags in scripts:\n'
rg -n 'gh api|paginate|RELEASE|REPO' scripts || true
printf '\nTry locate gh (do not run repository code):\n'
command -v gh || true
printf '%s\n' 'gh version:'
gh --version 2>/dev/null || trueRepository: FIERsity/envorigin
Length of output: 302
🌐 Web query:
GitHub REST API /repos/{owner}/{repo}/releases default page size per_page pagination releases endpoint
💡 Result:
For the GitHub REST API endpoint /repos/{owner}/{repo}/releases, the default number of results returned per page is 30 [1][2][3][4]. Key details regarding pagination for this endpoint include: - Default per_page: 30 [1][2][3][4] - Maximum per_page: 100 [1][2][3][4][5] - Default page: 1 [1][2][3][4] If you request a per_page value higher than 100, the API will automatically cap the results to 100 without returning an error [5]. You can use the Link header provided in the API response to navigate through subsequent pages of results [5].
Citations:
- 1: https://docs.github.com/en/rest/releases/releases
- 2: https://docs.github.com/en/rest/releases/releases?apiVersion=2022-11-28
- 3: https://docs.github.com/en/enterprise-cloud@latest/rest/releases/releases
- 4: https://docs.github.com/v3/repos/releases
- 5: https://docs.github.com/rest/using-the-rest-api/using-pagination-in-the-rest-api
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf 'Target file excerpt:\n'
awk '{printf "%6d\t%s\n", NR, $0}' scripts/usage-stats.sh | sed -n '1,90p'
printf '\nSearch for GH_API pagination usage in scripts:\n'
rg -n 'gh api|--paginate|per_page|REPO|RELEASE' scripts || true
printf '\nGH availability:\n'
command -v gh || true
gh --version 2>/dev/null || trueRepository: FIERsity/envorigin
Length of output: 5718
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf 'current gh release count and first page artifact download counts:\n'
gh api "repos/FIERsity/envorigin/releases" -q '.[0:3] | length as $n | "first_page_count: \($n)"' --jq '.[].assets[].download_count' || true
printf '\nfirst_page_total assets asset download counts from first page only:\n'
gh api "repos/FIERsity/envorigin/releases" \
--jq 'reduce .[].assets[]? as $a (0; . + ($a.download_count // 0)) | "first_page_asset_total: \(.)"' || trueRepository: FIERsity/envorigin
Length of output: 329
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf 'release pagination count comparison:\n'
printf '%s\n' 'first_page:'
gh api "repos/FIERsity/envorigin/releases?per_page=1" --jq 'length'
printf '%s\n' 'first_2_pages:'
gh api --paginate --slurp "repos/FIERsity/envorigin/releases?per_page=1" --jq 'add | length'
printf '%s\n' 'first_page_sum_of_counts:'
gh api "repos/FIERsity/envorigin/releases?per_page=1" --jq 'reduce .[].assets[]? as $a (0; . + ($a.download_count // 0)) | "first_page: \(.)"'
printf '%s\n' 'all_pages_sum_of_counts:'
gh api --paginate --slurp "repos/FIERsity/envorigin/releases?per_page=1" --jq 'add | reduce .[].assets[]? as $a (0; . + ($a.download_count // 0)) | "all_pages: \(.)"'Repository: FIERsity/envorigin
Length of output: 1800
🏁 Script executed:
#!/bin/bash
set -euo pipefail
tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' EXIT
printf 'all release pages saved with --paginate --slurp:\n'
gh api --paginate --slurp "repos/FIERsity/envorigin/releases?per_page=1" > "$tmp/releases_slurp.json"
printf '%s\n' 'pages_length:'
jq 'length' "$tmp/releases_slurp.json"
printf '%s\n' 'first_page_sum_of_counts:'
gh api "repos/FIERsity/envorigin/releases?per_page=1" \
--jq 'reduce .[].assets[]? as $a (0; . + ($a.download_count // 0)) | "first_page: \(.)"'
printf '%s\n' 'all_pages_sum_of_counts:'
jq 'add | reduce .[].assets[]? as $a (0; . + ($a.download_count // 0))' "$tmp/releases_slurp.json"Repository: FIERsity/envorigin
Length of output: 291
Paginate the release list before summing downloads. Without gh api --paginate --slurp, this only processes the first page of releases, so repositories with more pages return an incomplete download total.
🧰 Tools
🪛 Shellcheck (0.11.0)
[info] 46-46: Expressions don't expand in single quotes, use double quotes for that.
(SC2016)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@scripts/usage-stats.sh` around lines 45 - 46, Update the release download
aggregation command in the release binary downloads section to use gh api
pagination with slurped results before reducing over all release assets,
ensuring the download total includes every release page.
| section "== Hacker News mentions ==" | ||
| hn="$(curl -sf "https://hn.algolia.com/api/v1/search?query=envorigin" | jq -r '.nbHits')" | ||
| echo " mentions: ${hn:-0}" |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== file context =="
sed -n '1,90p' scripts/usage-stats.sh || true
echo "== jq null behavior probe =="
if command -v jq >/dev/null 2>&1; then
jq -r '{} | {mentions: (.nbHits // 0)}'
jq -r '.nbHits // 0' <<EOF
{}
EOF
jq -r '.nbHits // 0' <<EOF
{"nbHits": null}
EOF
jq -r '.nbHits // 0' <<EOF
{"nbHits": 123}
EOF
else
echo "jq not installed"
fi
echo "== shell parameter expansion probe =="
python3 - <<'PY'
missing=""
present="null"
print("missing:", repr(missing or "0"))
print("present:", repr(present or "0"))
PYRepository: FIERsity/envorigin
Length of output: 3076
🌐 Web query:
jq manual null default operator // example .x // 0 jq null
💡 Result:
In jq, the // operator is the alternative operator, commonly used to provide a default value when a field is missing, null, or false [1][2][3]. The expression.x // 0 works as follows: If the expression.x produces any result other than false or null, that result is returned [4][1][3]. If.x results in false or null (or if the field.x is missing, which evaluates to null), the right-hand side of the operator (0) is returned instead [2][3]. Example usage: If your input is {"x": 10}, the filter.x // 0 returns 10. If your input is {"x": null} or {}, the filter.x // 0 returns 0 [2][3]. A subtle but important distinction exists when using generators (expressions that produce multiple values). The filter (a, b) // c will output a and b if they are not false or null; c is only output if no truthy values are produced by the left-hand side [4][5]. If you intend to provide a default for every single element in a stream, you should use the pipe operator: (a, b) |. // c [4].
Citations:
- 1: https://jqlang.org/manual/v1.4/
- 2: https://www.devtoolsdaily.com/blog/jq-null-values-and-default/
- 3: https://manpages.debian.org/bookworm/jq/jq.1.en.html
- 4: https://stackoverflow.com/questions/76984368/strange-behaviour-of-the-alternative-operator
- 5: Alternative operator // applied on a stream jqlang/jq#2189
🌐 Web query:
jq null string output -r
💡 Result:
In jq, when a query results in a null value or fails to find a specified key, the output is typically the literal string "null" (when using --raw-output/-r) or the JSON value null [1][2][3]. You can control this behavior using several common techniques depending on whether you want to suppress the output, replace it with an empty string, or provide a default value [1][4][5]. To suppress output when the result is null: Use the select filter to filter out null results: jq '.field | select(.!= null)' [4][6] Alternatively, you can use the empty keyword, which returns no results [3]: jq '.field // empty' [1][4] To replace null with an empty string: Use the alternative operator (//) to provide an empty string as a default [1][7][5]: jq -r '.field // ""' [7] To handle missing keys versus null values: Accessing a missing key in jq returns null [3][8]. If you need to distinguish between a key that is missing and a key that is explicitly set to null, use the has/1 function [2][5]: jq 'if has("field") then.field else "default" end' [2] When using jq in shell scripts, if you want to detect when a command returns null as a failure, you can use the --exit-status (-e) flag [1][2]. This causes jq to exit with a non-zero status if the final output is null or false [1][3].
Citations:
- 1: https://stackoverflow.com/questions/53135035/jq-returning-null-as-string-if-the-json-is-empty
- 2: https://stackoverflow.com/questions/57517345/how-to-check-jq-result-is-null-or-not
- 3: https://jqlang.org/manual/
- 4: How to avoid print null in
jqoutput? jqlang/jq#1968 - 5: https://www.devtoolsdaily.com/blog/jq-null-values-and-default/
- 6: https://blog.nem.ec/code-snippets/jq-ignore-nulls/
- 7: Return empty string instead of "null" with "jq --raw-output" ? jqlang/jq#354
- 8: How to avoid "null" value when filtering non-exist keys? jqlang/jq#1583
Convert a missing Hacker News count in jq.
When the API response omits .nbHits, jq -r '.nbHits' prints the string null; ${hn:-0} only handles an unset variable. Use jq’s alternative operator so the fallback is applied before assignment.
Proposed fix
- hn="$(curl -sf "https://hn.algolia.com/api/v1/search?query=envorigin" | jq -r '.nbHits')"
+ hn="$(curl -sf "https://hn.algolia.com/api/v1/search?query=envorigin" | jq -r '.nbHits // 0')"📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| section "== Hacker News mentions ==" | |
| hn="$(curl -sf "https://hn.algolia.com/api/v1/search?query=envorigin" | jq -r '.nbHits')" | |
| echo " mentions: ${hn:-0}" | |
| section "== Hacker News mentions ==" | |
| hn="$(curl -sf "https://hn.algolia.com/api/v1/search?query=envorigin" | jq -r '.nbHits // 0')" | |
| echo " mentions: ${hn:-0}" |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@scripts/usage-stats.sh` around lines 52 - 54, Update the Hacker News count
assignment for hn to use jq’s alternative operator, defaulting a missing or null
.nbHits value to 0 before assignment; keep the existing curl request and output
behavior unchanged.
What
Adds
scripts/usage-stats.sh, a one-command dashboard of adoption signals across every public surface of EnvOrigin:uses: FIERsity/envorigin-actionWhy
We need an adoption baseline before promoting the project. These are all public APIs — no user telemetry is collected. The script labels which numbers are inflated by our own CI, so the trend can be read honestly.
Notes
bash -n+ full run verified locally.scripts/is excluded from the published crate, so this does not affect packaging.🤖 Generated with Claude Code
Summary by CodeRabbit