Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/citest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ on:

jobs:
test-oldpython:
runs-on: "ubuntu-20.04"
runs-on: "ubuntu-22.04"
steps:
- uses: actions/checkout@v4
- name: Set up Python 3.5
- name: Set up Python 3.7
uses: actions/setup-python@v5
with:
python-version: "3.5"
python-version: "3.7"
env:
PIP_TRUSTED_HOST: "pypi.python.org pypi.org files.pythonhosted.org"
# workaround for certificate incompatibility
Expand Down
105 changes: 105 additions & 0 deletions .github/workflows/copilot-metrics.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
name: Copilot Metrics Collector

on:
schedule:
# Runs every day at 06:00 UTC (after GitHub's nightly data refresh)
- cron: "0 6 * * *"
workflow_dispatch:
# Also allows manual triggering from the Actions tab

permissions:
contents: write # needed to commit the updated metrics file back to the repo

jobs:
fetch-metrics:
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3"

- name: Fetch Copilot usage metrics
env:
COPILOT_METRICS_TOKEN: ${{ secrets.COPILOT_METRICS_TOKEN }}
ORG: DewDropstempest
run: |
python - <<'PYEOF'
import json
import os
import urllib.request
import urllib.error
from datetime import datetime, timezone

token = os.environ["COPILOT_METRICS_TOKEN"]
org = os.environ["ORG"]

url = f"https://api.github.com/orgs/{org}/copilot/usage"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Use supported Copilot metrics API route

The workflow calls https://api.github.com/orgs/{org}/copilot/usage, but GitHub’s Copilot metrics endpoints changed and the legacy set was shut down on April 2, 2026; organization usage data is now retrieved through /orgs/{org}/copilot/metrics/reports/.... In this job, a non-200 response triggers SystemExit, so the scheduled run never writes new data and the dashboard remains stale/empty.

Useful? React with 👍 / 👎.


req = urllib.request.Request(
url,
headers={
"Authorization": f"Bearer {token}",
"Accept": "application/vnd.github+json",
"X-GitHub-Api-Version": "2022-11-28",
},
)

try:
with urllib.request.urlopen(req) as resp:
data = json.loads(resp.read().decode())
except urllib.error.HTTPError as e:
body = e.read().decode()
print(f"HTTP {e.code}: {body}")
raise SystemExit(f"Failed to fetch metrics: {e.code}")

# ── Normalise field names ──────────────────────────────────────────
# The API returns either the v1 shape (total_suggestions_count, etc.)
# or the v2 shape (totals.suggestions, etc.). We normalise to v1.
normalised = []
for day in data:
entry = {"day": day.get("day", day.get("date", "unknown"))}
if "total_suggestions_count" in day:
# v1 shape – already in the format we want
entry["total_active_users"] = day.get("total_active_users", 0)
entry["total_suggestions_count"] = day.get("total_suggestions_count", 0)
entry["total_acceptances_count"] = day.get("total_acceptances_count", 0)
entry["total_lines_suggested"] = day.get("total_lines_suggested", 0)
entry["total_lines_accepted"] = day.get("total_lines_accepted", 0)
elif "copilot_ide_code_completions" in day:
# v2 / newer shape
cc = day.get("copilot_ide_code_completions") or {}
entry["total_active_users"] = day.get("total_active_users", 0)
entry["total_suggestions_count"] = cc.get("total_suggestions_count", 0)
entry["total_acceptances_count"] = cc.get("total_acceptances_count", 0)
entry["total_lines_suggested"] = cc.get("total_lines_suggested", 0)
entry["total_lines_accepted"] = cc.get("total_lines_accepted", 0)
Comment on lines +77 to +80
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Map v2 completion counters from correct fields

In the copilot_ide_code_completions branch, the code reads total_*_count keys directly from cc, but the v2 path described in the comments stores metrics under nested totals (or deeper structures), so these lookups fall back to 0. When v2 payloads are returned, suggestions/acceptances/line KPIs and charts will be silently underreported as zero.

Useful? React with 👍 / 👎.

else:
# Fallback – store whatever we got so the dashboard can adapt
entry.update({k: v for k, v in day.items() if k != "breakdown"})
normalised.append(entry)

out_path = "copilot-dashboard/data/metrics.json"
os.makedirs(os.path.dirname(out_path), exist_ok=True)

with open(out_path, "w") as f:
json.dump(normalised, f, indent=2)

print(f"Wrote {len(normalised)} day(s) of metrics to {out_path}")
PYEOF

- name: Commit updated metrics
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add copilot-dashboard/data/metrics.json
if git diff --cached --quiet; then
echo "No changes to metrics data – skipping commit."
else
git commit -m "chore: update Copilot metrics [skip ci]"
git push
fi
30 changes: 15 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ publicsuffixlist
[Public Suffix List](https://publicsuffix.org/) parser implementation for
Python 3.5+.

- Compliant with [TEST DATA](https://raw.githubusercontent.com/publicsuffix/list/master/tests/test_psl.txt)
- Compliant with [TEST DATA](https://raw.githubusercontent.com/publicsuffix/list/master/tests/test_psl.txt).
- Supports IDN (unicode and punycoded).
- Supports Python3.5+
- Supports Python3.5+.
- Shipped with built-in PSL and an updater script.
- Written in Pure Python with no library dependencies.

Expand All @@ -33,19 +33,19 @@ from publicsuffixlist import PublicSuffixList
psl = PublicSuffixList()
# Uses built-in PSL file

print(psl.publicsuffix("www.example.com")) # "com"
# the longest public suffix part
print(psl.publicsuffix("www.example.com")) # "com"
# The longest public suffix part

print(psl.privatesuffix("www.example.com")) # "example.com"
# the shortest domain assigned for a registrant
print(psl.privatesuffix("www.example.com")) # "example.com"
# The shortest domain assigned for a registrant

print(psl.privatesuffix("com")) # None
# Returns None if no private (non-public) part found

print(psl.publicsuffix("www.example.unknownnewtld")) # "unknownnewtld"
# New TLDs are valid public suffix by default

print(psl.publicsuffix("www.example.香港")) #"香港"
print(psl.publicsuffix("www.example.香港")) # "香港"
# Accepts unicode

print(psl.publicsuffix("www.example.xn--j6w193g")) # "xn--j6w193g"
Expand All @@ -54,7 +54,7 @@ print(psl.publicsuffix("www.example.xn--j6w193g")) # "xn--j6w193g"
print(psl.privatesuffix("WWW.EXAMPLE.COM")) # "example.com"
# Returns in lowercase by default

print(psl.privatesuffix("WWW.EXAMPLE.COM", keep_case=True) # "EXAMPLE.COM"
print(psl.privatesuffix("WWW.EXAMPLE.COM", keep_case=True)) # "EXAMPLE.COM"
# kwarg `keep_case=True` to disable the case conversion
```

Expand All @@ -75,8 +75,8 @@ $ python -m publicsuffixlist.update
Additional convenient methods:

```python
print(psl.is_private("example.com")) # True
print(psl.is_public("example.com")) # False
print(psl.is_private("example.com")) # True
print(psl.is_public("example.com")) # False
print(psl.privateparts("aaa.www.example.com")) # ("aaa", "www", "example.com")
print(psl.subdomain("aaa.www.example.com", depth=1)) # "www.example.com"
```
Expand All @@ -86,7 +86,7 @@ Limitation

#### Domain Label Validation

`publicsuffixlist` do NOT provide domain name and label validation.
`publicsuffixlist` does NOT provide domain name and label validation.
In the DNS protocol, most 8-bit characters are acceptable as labels of domain
names. While ICANN-compliant registries do not accept domain names containing
underscores (_), hostnames may include them. For example, DMARC records can
Expand All @@ -97,7 +97,7 @@ based on their specific context.
Partially encoded (Unicode-mixed) Punycode is not supported due to very slow
Punycode encoding/decoding and unpredictable encoding results. If you are
unsure whether an input is valid Punycode, you should use:
`unknowndomain.encode("idna").decode("ascii")`. This method, converting to idna
`unknowndomain.encode("idna").decode("ascii")`. This method, converting to IDNA
is idempotent.

#### Handling Arbitrary Binary
Expand All @@ -106,13 +106,13 @@ tuple of bytes. Note that the returned bytes may include byte patterns that
cannot be decoded or represented as a standard domain name.
Example:
```python
psl.privatesuffix((b"a.a", b"a.example\xff", b"com")) # (b"a.example\xff", b"com")
psl.privatesuffix((b"a.a", b"a.example\xff", b"com")) # (b"a.example\xff", b"com")

# Note that IDNs must be punycoded when passed as tuple of bytes.
psl = PublicSuffixList("例.example")
psl.publicsuffix((b"xn--fsq", b"example")) # (b"xn--fsq", b"example")
psl.publicsuffix((b"xn--fsq", b"example")) # (b"xn--fsq", b"example")
# UTF-8 encoded bytes of "例" do not match.
psl.publicsuffix((b"\xe4\xbe\x8b", b"example")) # (b"example",)
psl.publicsuffix((b"\xe4\xbe\x8b", b"example")) # (b"example",)
```

License
Expand Down
84 changes: 84 additions & 0 deletions copilot-dashboard/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# Copilot Metrics Dashboard

A self-hosted dashboard that pulls GitHub Copilot usage data daily and
displays it as interactive charts — no third-party service required.

---

## How it works

1. A GitHub Actions workflow (`copilot-metrics.yml`) runs every day at 06:00 UTC.
2. It calls the [GitHub Copilot Usage API](https://docs.github.com/en/rest/copilot/copilot-usage) and saves the results to `data/metrics.json`.
3. The dashboard (`index.html`) reads that JSON file and renders charts automatically.

---

## ✅ One-time setup (the only manual step)

You need to create a secret called **`COPILOT_METRICS_TOKEN`** in this repository.

### Step 1 — Create a Personal Access Token

1. Go to **GitHub.com → your profile → Settings → Developer settings → Personal access tokens → Fine-grained tokens**
2. Click **"Generate new token"**
3. Set:
- **Token name**: `copilot-metrics-dashboard` (or anything you like)
- **Resource owner**: `DewDropstempest` (the organization)
- **Repository access**: `Only select repositories` → pick this repo
- **Permissions → Organization permissions → GitHub Copilot Business → Access: Read-only**
4. Click **"Generate token"** and **copy the token value** (you only see it once)

> If Fine-grained tokens don't show a Copilot permission yet, create a **Classic token** with the `manage_billing:copilot` scope instead.

### Step 2 — Add the secret to this repository

1. Go to **this repository → Settings → Secrets and variables → Actions**
2. Click **"New repository secret"**
3. Name: `COPILOT_METRICS_TOKEN`
4. Value: paste the token you just copied
5. Click **"Add secret"**

That's it — you're done!

---

## Viewing the dashboard

### Option A — GitHub Pages (recommended, zero extra cost)

1. Go to **this repository → Settings → Pages**
2. Under **"Source"** choose **"Deploy from a branch"**
3. Branch: `main` (or whichever branch this code is on), Folder: `/copilot-dashboard`
4. Click **Save**

Your dashboard will be live at:
```
https://DewDropstempest.github.io/psl/
```

### Option B — Open locally

Just open `copilot-dashboard/index.html` in any modern browser (after the first
workflow run has populated `data/metrics.json`).

---

## Triggering the first run

Don't want to wait until 06:00 UTC? Run it now:

1. Go to **this repository → Actions → Copilot Metrics Collector**
2. Click **"Run workflow" → Run workflow**

The `data/metrics.json` file will be committed automatically and the dashboard will show data within a minute or two.

---

## Troubleshooting

| Symptom | Likely cause | Fix |
|---------|-------------|-----|
| Workflow fails with `HTTP 403` | Token missing or wrong scope | Re-check Step 1 — ensure Copilot read permission is set |
| Workflow succeeds but dashboard shows "No data yet" | API returned an empty array | Make sure Copilot is enabled for the org and at least one seat is active |
| Workflow fails with `HTTP 404` | Org doesn't have Copilot Business/Enterprise | Purchase a Copilot plan for the org |
| Dashboard shows stale data | Workflow hasn't run yet today | Trigger manually (see above) |
1 change: 1 addition & 0 deletions copilot-dashboard/data/metrics.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[]
Loading
Loading