Copilot Metrics — Why No Data After April
+
+ Daily Copilot analytics rows for org unic
+ stopped landing in the database on April 2, 2026. The cron is healthy. The token is valid.
+ The org policy is enabled. We were calling an endpoint that GitHub permanently retired on exactly that day.
+
+ GitHub sunset the legacy Copilot Metrics API on April 2, 2026
+ (announced 2026-01-29).
+ Our sync calls GET /orgs/{org}/copilot/metrics — the exact endpoint that was closed down.
+ Since April 3 it has returned HTTP 404 on every cron run. Org policy and active-user counts are not the cause (both were verified). The replacement is a redesigned Copilot usage metrics API with a different shape (report download links, not inline JSON) and requires a code migration — not a configuration toggle.
+
01Timeline
+Reconstructed from sync_events and copilot_usage_metrics. The break is sharp and dateable.
/orgs/{org}/copilot/metrics was closed down. Migration to the new Copilot usage metrics API is required.02Root cause — the endpoint was retired
+Two initial hypotheses (policy disabled, <5 active users) were both falsified during the investigation. The actual cause is an upstream API sunset on the exact day our data stops.
+ +GitHub sunset the legacy Copilot Metrics API on April 2, 2026
++ On 2026-01-29, GitHub announced the closure of three legacy Copilot metrics APIs, with the Copilot Metrics API itself sunset on April 2, 2026. The official notice: +
++ "These Copilot metrics endpoints were closed down on April 2, 2026. Use the Copilot usage metrics endpoints instead, which provide more depth and flexibility." ++
+ Our sync calls GET /orgs/{org}/copilot/metrics — exactly the path that was retired. Since April 3 (the first cron run on or after the sunset), every call has returned HTTP 404. The other Copilot endpoints we use (/copilot/billing and /copilot/billing/seats) were not part of the deprecation, which is why billing + seat sync keep succeeding on the same token.
+
"Copilot Metrics API access" org policy disabled
++ Plausible on paper — flipping that policy off produces 404 with no token error. But the org owner verified the policy is enabled (and re-toggled it off/on) and the 404 persists. Falsified. +
+Fewer than 5 members generated telemetry
++ The privacy gate would return 404 if the rolling 28-day window had <5 active users. The actual numbers are 24–34 engaged users/day through April 1, and the org continues to have well more than five active Copilot users today. Falsified. +
+
+ Why the May 18 fix didn't help. Commit 19b38dc reinterpreted 404 as "no data, not an error." That made sense under hypothesis A or B — both of which produce transient 404s. But the real upstream cause is permanent endpoint retirement, so swallowing the 404 just hides the symptom while the data flow remains broken. Re-enabling the org policy will not bring back a closed-down endpoint.
+
03The replacement API — what we have to migrate to
+The new Copilot usage metrics endpoints are a different shape from the retired ones. Migration is not a URL swap.
+ +| What we want | +Old endpoint (retired) | +New endpoint | +
|---|---|---|
| Latest 28-day org-level rollup | +GET /orgs/{org}/copilot/metrics |
+ GET /orgs/{org}/copilot/metrics/reports/organization-28-day/latest |
+
| Single-day org rollup (for daily cron) | +same endpoint, filter by date | +GET /orgs/{org}/copilot/metrics/reports/organization-1-day |
+
| Per-user breakdown | +not available on legacy | +GET /orgs/{org}/copilot/metrics/reports/users-1-day · ...users-28-day/latest |
+
| Team membership rollups | +not available on legacy | +GET /orgs/{org}/copilot/metrics/reports/user-teams-1-day |
+
+ Response shape is different. The retired endpoint returned inline JSON with daily metrics. The new endpoints return signed download links to report files — the caller follows the link to fetch the actual data. Field names also changed: the old copilot_ide_code_completions grouping is split out by language and model in the new schema, so there is no 1:1 column mapping.
+
+ Required scopes. Org-level reports require read:org (we already have manage_billing:copilot; double-check the token also includes read:org). Enterprise-level reports require read:enterprise and aren't needed for our single-org setup.
+
04Evidence — what's in the database
+123 days of copilot_usage_metrics rows from 2025-11-30 onward. Then it stops.
| Date | +Active users | +Engaged users | +Suggestions | +Acceptances | +Note | +
|---|---|---|---|---|---|
| 2026-04-01 | 32 | 24 | 393 | 140 | Last row written. |
| 2026-03-31 | 40 | 30 | 313 | 94 | |
| 2026-03-30 | 37 | 23 | 536 | 164 | |
| 2026-03-29 | 9 | 4 | 88 | 30 | Engaged briefly below 5 (single-day; doesn't trigger 404). |
| 2026-03-28 | 10 | 8 | 109 | 25 | |
| 2026-03-27 | 36 | 24 | 668 | 195 | |
| 2026-03-26 | 45 | 32 | 508 | 120 | |
| 2026-03-25 | 37 | 29 | 375 | 79 | |
| 2026-03-24 | 42 | 25 | 492 | 135 | |
| 2026-03-23 | 42 | 34 | 835 | 199 |
| id | +Started (UTC) | +Outcome | +Created | +Updated | +Error | +
|---|---|---|---|---|---|
| 405 | 2026-04-02 06:00 | success | 1 | 79 | Last successful metrics sync — wrote the Apr 1 row. |
| 454 | 2026-04-03 06:00 | partial | 0 | 78 | First 404. error="Metrics sync failed: Not Found". |
| — | Apr 3 → May 17 (45 runs) | partial | 0 | 76–80 | Same error string every day. Billing + seats keep updating. |
| — | 2026-05-18 | partial | 0 | 77 | Last day with the honest "partial" outcome. Commit 19b38dc lands. |
| — | 2026-05-19 06:00 | success | 0 | 78 | 404 now silently swallowed. No metric row written. |
| — | 2026-05-20 06:00 | success | 0 | 79 | Still no metric row written. |
| — | 2026-05-21 06:00 | success | 0 | 78 | Today. Still no metric row. |
05Code paths involved
+The cron is wired correctly. The orchestrator catches per-step. The 404 path used to fail loudly; as of May 18 it succeeds quietly.
+ +// src/lib/copilot-sync.ts:300-306 — `since` is latest_date + 1 +let since: string | undefined; +if (latestRow) { + const latestDate = new Date(latestRow.date); + latestDate.setUTCDate(latestDate.getUTCDate() + 1); + since = latestDate.toISOString().split("T")[0]; +}+
// src/lib/copilot-sync.ts:314-324 — 404 is now silently swallowed +if (metricsResponse.error || !metricsResponse.data) { + if (metricsResponse.status === 404) { + console.warn( + `[copilot-sync] Copilot metrics not available for org "${connection.orgLogin}" (404). The "Copilot Metrics API access" policy may be disabled, or the org has fewer than 5 users emitting telemetry.`, + ); + return { metricsProcessed: 0 }; + } + throw new Error(metricsResponse.error ?? "Failed to fetch Copilot usage metrics"); +}+
06Why the dashboard looks "empty after April"
+
+ The analytics dashboard default window is the last 28 days.
+ From 2026-05-21, that window is 2026-04-23 → 2026-05-21.
+ Since the last row in copilot_usage_metrics is 2026-04-01, the default window contains
+ zero rows.
+
+ Widening to 90 days, the curve flatlines after April 1. This is not a query bug — + the data genuinely doesn't exist in the database, because the upstream API stopped serving it. +
+07Recommended remediation
+There is no configuration fix. The endpoint is gone. This needs a small but real migration to the new Copilot usage metrics API.
+ +-
+
- Update
fetchCopilotMetricsinsrc/lib/copilot-api.ts:255-272to call the new endpoints. For our daily cron, callGET /orgs/{org}/copilot/metrics/reports/organization-1-day?date=YYYY-MM-DD(yesterday's date, since GitHub never serves today).
+ - The new endpoint returns signed download links, not inline JSON. Add a second fetch step to download the report file and parse it. Confirm format (CSV/JSON) from the docs at
docs.github.com/en/rest/copilot/copilot-usage-metricsbefore coding.
+ - Re-map the response into the existing
copilot_usage_metricsschema. The retired API'scopilot_ide_code_completionsgrouping is replaced by per-language / per-model breakdowns — field-by-field translation, not a trivial rename. Plan to either (a) preserve the existing table shape via aggregation in code, or (b) add a forward-compatiblelanguageBreakdown/editorBreakdownmapping (we already store these as JSONB, so option a is cheaper).
+ - Verify the connection token has
read:org(required for the new org-level endpoint). The current token works for billing/seats — confirm scopes viavalidateCopilotScopesbefore deploying.
+ - Backfill: the new endpoint supports a 28-day rolling window via the
organization-28-day/latestvariant. After deploying, trigger a one-shot backfill (operation_type=backfill, already supported by the sync framework) to pull approximately 2026-04-23 → 2026-05-20 in one go.
+
-
+
- The new API still only retains 28 days of history. Even after we ship the migration today (2026-05-21), GitHub will only return data from roughly 2026-04-23 onward. The gap 2026-04-02 → 2026-04-22 is permanently unrecoverable. Surface this on the analytics page (e.g., a footnote on the chart axis) so future readers don't read the dip as a real downturn. +
- If the migration ships later than 2026-06-18, the recoverable window shrinks day by day. Treat this PR as high priority. +
-
+
- Remove the 404-as-success path added in
19b38dc. With the new API, a 404 is no longer a benign "policy off" state — it's either auth or a genuine problem. Treat metric-side 404s aspartialoutcomes and surface them on the analytics page.
+ - Add an admin-visible health card "Copilot metrics stale — last data N days ago" on the analytics page when the latest row is older than 3 days. This single card would have surfaced the sunset within 72 hours instead of 49 days. +
- Track upstream deprecations: subscribe the team's #ops channel to GitHub's Copilot API changelog RSS feed. The sunset was announced 63 days before it happened. +