-
Notifications
You must be signed in to change notification settings - Fork 0
feat: add usage-stats.sh — one-command adoption metrics #98
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,54 @@ | ||||||||||||||
| #!/usr/bin/env bash | ||||||||||||||
| # usage-stats.sh — one-command adoption metrics for EnvOrigin. | ||||||||||||||
| # | ||||||||||||||
| # Tracks real usage signals across every public surface: | ||||||||||||||
| # crates.io downloads — installs via cargo install / Cargo.toml | ||||||||||||||
| # Homebrew analytics — installs via `brew install envorigin` | ||||||||||||||
| # GitHub traffic — page views and clones (last 14 days) | ||||||||||||||
| # Release binaries — prebuilt binary downloads (all releases) | ||||||||||||||
| # Code search — public repos with `uses: FIERsity/envorigin-action` | ||||||||||||||
| # HN Algolia — mentions on Hacker News | ||||||||||||||
| # | ||||||||||||||
| # No telemetry is collected from users — these are all public APIs. | ||||||||||||||
| # Read the numbers as a trend, not an exact headcount: CI runs, mirrors, | ||||||||||||||
| # 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 | ||||||||||||||
|
|
||||||||||||||
| REPO="FIERsity/envorigin" | ||||||||||||||
| ACTION_REPO="FIERsity/envorigin-action" | ||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 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:
💡 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:
🏁 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.
🧰 Tools🪛 Shellcheck (0.11.0)[warning] 20-20: ACTION_REPO appears unused. Verify use (or export if used externally). (SC2034) 🤖 Prompt for AI AgentsSource: Linters/SAST tools |
||||||||||||||
| UA="envorigin-usage-stats" | ||||||||||||||
|
|
||||||||||||||
| section() { printf '\n\033[1m%s\033[0m\n' "$1"; } | ||||||||||||||
|
|
||||||||||||||
| section "== crates.io (cargo install / Cargo.toml) ==" | ||||||||||||||
| crates="$(curl -sf -H "User-Agent: $UA" https://crates.io/api/v1/crates/envorigin \ | ||||||||||||||
| | jq -r '"total downloads: \(.crate.downloads) last 90d: \(.crate.recent_downloads) versions: \(.versions|length)"')" | ||||||||||||||
| printf '%s\n' "$crates" | ||||||||||||||
| curl -sf -H "User-Agent: $UA" https://crates.io/api/v1/crates/envorigin \ | ||||||||||||||
| | jq -r '.versions[0:3][] | " v\(.num): \(.downloads) downloads"' | ||||||||||||||
|
|
||||||||||||||
| 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)"' | ||||||||||||||
|
Comment on lines
+32
to
+36
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 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:
💡 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:
Read each Homebrew analytics period independently. Lines 34-36 bind 🤖 Prompt for AI Agents |
||||||||||||||
| else | ||||||||||||||
| echo " formula not on formulae.brew.sh yet (HTTP $code) — appears after the next release" | ||||||||||||||
| fi | ||||||||||||||
|
|
||||||||||||||
| section "== GitHub traffic (last 14 days) ==" | ||||||||||||||
| gh api "repos/$REPO/traffic/views" --jq '" views: \(.count) unique visitors: \(.uniques)"' | ||||||||||||||
| gh api "repos/$REPO/traffic/clones" --jq '" clones: \(.count) unique cloners: \(.uniques)"' | ||||||||||||||
|
|
||||||||||||||
| section "== Release binary downloads (all releases) ==" | ||||||||||||||
| gh api "repos/$REPO/releases" --jq 'reduce .[].assets[]? as $a (0; . + $a.download_count) | " prebuilt binaries downloaded: \(.)"' | ||||||||||||||
|
Comment on lines
+45
to
+46
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 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:
💡 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:
🏁 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 🧰 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 |
||||||||||||||
|
|
||||||||||||||
| section "== Public repos using the action (uses: FIERsity/envorigin-action) ==" | ||||||||||||||
| count="$(gh api "search/code?q=FIERsity%2Fenvorigin-action&per_page=1" --jq '.total_count')" | ||||||||||||||
| echo " hits: $count (includes our own dogfood jobs; minus 2-3 for real adoption)" | ||||||||||||||
|
|
||||||||||||||
| section "== Hacker News mentions ==" | ||||||||||||||
| hn="$(curl -sf "https://hn.algolia.com/api/v1/search?query=envorigin" | jq -r '.nbHits')" | ||||||||||||||
| echo " mentions: ${hn:-0}" | ||||||||||||||
|
Comment on lines
+52
to
+54
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 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:
💡 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:
🌐 Web query:
💡 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:
Convert a missing Hacker News count in jq. When the API response omits 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: FIERsity/envorigin
Length of output: 4957
🏁 Script executed:
Repository: 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, thereposcope is generally required [3][8]. Note that the traffic API only exposes data for the most recent 14 days [1][7][8].Citations:
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, a403from those calls exitsscripts/usage-stats.shbefore 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