
100% private · no tracking · works offline100% client-side/no data leaves your browser/no accounts/works offline
A practical guide to downloading GitHub Copilot usage reports, measuring third-party agent adoption, and avoiding double-counted or misleading metrics.
Free toolkit
85+ private dev tools
Everything runs in your browser. Zero tracking, no sign-up.
Browse toolsThe GitHub Copilot usage metrics API can now separate activity from recognized third-party agent apps instead of leaving all agent work in one bucket. That makes it possible to compare adoption of agents such as Claude and Codex, but the new fields are easy to misread: job starts are not prompts, sessions are not users, and the same-named top-level counter must not be added to the nested agent counter.
This guide shows how to retrieve the signed NDJSON reports, aggregate by stable agent ID, calculate useful adoption measures, and avoid dashboards that look precise while answering the wrong question.
On August 7, 2026, GitHub added an optional totals_by_3rd_party_agent array to enterprise, organization, enterprise-user, and organization-user reports. It is present in both one-day and 28-day report families.
Each recognized agent entry contains:
| Field | Meaning | Reporting detail |
|---|---|---|
agent_id |
Stable identifier for the agent | Use this as the join and grouping key |
agent_name |
Current display name | Useful for presentation, but it can change |
user_initiated_interaction_count |
User-initiated agent-app job starts | Not the same as ordinary Copilot prompts |
session_count |
Agent-app sessions | Aggregate organization and enterprise reports only |
Per-user records omit session_count. Aggregate records include it. Multiple apps associated with the same agent are collapsed into one agent entry, while activity from an agent that GitHub cannot identify is omitted.
The most important warning in GitHub’s agent-app metrics announcement is about a duplicate-looking field name. The nested agent counter measures job starts for that agent app. The top-level user_initiated_interaction_count measures explicit prompts from other supported telemetry. They are different populations and must not be summed into a supposed “total interactions” figure.
The new breakdown answers a useful operational question: which recognized agents are people actually starting through GitHub? It does not prove that those jobs succeeded, saved time, produced accepted code, or justified their cost. Pair adoption data with cost controls from the GitHub Copilot AI credits guide, then evaluate outcomes separately.
The Copilot usage metrics REST API does not return a single universal metrics document. It returns short-lived signed download links to report files, and report shape depends on scope, window, and granularity.
| Report | Best question | Relevant agent fields |
|---|---|---|
| Organization 1-day | What happened on a specific processed day? | Starts and sessions by agent |
| Organization 28-day | How is activity trending across the rolling window? | Daily starts and sessions in day_totals |
| Organization users 1-day | Which users tried each agent that day? | Starts by agent and user |
| Organization users 28-day | How many distinct people used each agent in the window? | Starts by agent and user |
| Repository report | What PR activity happened by repository? | Not the source for third-party agent adoption |
| User-teams report | Which teams should user metrics roll up to? | Join table, not agent activity by itself |
The aggregate 28-day report is a wrapper. Its day_totals array contains one aggregate record per day. The 28-day per-user report contains one record per user for the reporting period rather than 28 daily rows per user. This difference matters when you calculate reach.
Use the organization aggregate report for activity volume and session trends. Use the organization users report to count distinct users per agent. If leadership wants team-level adoption, retrieve the daily user-teams report and join on user_id; GitHub does not pre-aggregate team metrics. Teams with fewer than five seated Copilot users on a given day are excluded from that mapping report, so document that suppression before publishing a team leaderboard.
The Copilot usage metrics policy must be enabled. At organization scope, organization owners and users with an appropriate custom role can retrieve the reports. Fine-grained tokens need read access for “Organization Copilot metrics.” GitHub App user and installation tokens are supported. A classic personal access token needs read:org.
Prefer a GitHub App or a narrowly scoped fine-grained token for automation. Do not put an owner’s long-lived personal token in a CI variable simply because it makes the first request succeed. The access model for an analytics job should follow the same least-authority thinking as an AI coding-agent permission contract.
Set these variables locally:
export GITHUB_ORG="your-organization"
export GITHUB_TOKEN="your-fine-grained-token"
Test access with the latest organization report:
curl --fail-with-body --location \
--header "Accept: application/vnd.github+json" \
--header "Authorization: Bearer ${GITHUB_TOKEN}" \
--header "X-GitHub-Api-Version: 2026-03-10" \
"https://api.github.com/orgs/${GITHUB_ORG}/copilot/metrics/reports/organization-28-day/latest"
A successful response contains download_links plus report_start_day and report_end_day. A 204 response on a one-day endpoint means no content is available for that request. 403 usually points to policy or permission, while 404 can indicate the organization or report is unavailable to the caller.
The report metadata response is not the report. Each URL in download_links points to an NDJSON file and has limited validity. Fetch the links immediately, process every returned file, and store your derived results rather than treating the signed URL as a permanent data source.
This dependency-free Node.js 20 script downloads both the 28-day aggregate and per-user reports, parses all NDJSON partitions, and builds per-agent summaries:
const API_VERSION = "2026-03-10";
const org = process.env.GITHUB_ORG;
const token = process.env.GITHUB_TOKEN;
if (!org || !token) {
throw new Error("Set GITHUB_ORG and GITHUB_TOKEN");
}
async function githubJson(path) {
const response = await fetch(`https://api.github.com${path}`, {
headers: {
accept: "application/vnd.github+json",
authorization: `Bearer ${token}`,
"x-github-api-version": API_VERSION,
},
});
if (!response.ok) {
throw new Error(`GitHub ${response.status}: ${await response.text()}`);
}
return response.json();
}
async function downloadNdjson(url) {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Report download ${response.status}`);
}
const text = await response.text();
return text
.split(/\r?
/)
.filter((line) => line.trim() !== "")
.map((line) => JSON.parse(line));
}
async function getReport(reportName) {
const meta = await githubJson(
`/orgs/${encodeURIComponent(org)}/copilot/metrics/reports/${reportName}`,
);
const partitions = await Promise.all(
meta.download_links.map((url) => downloadNdjson(url)),
);
return { meta, records: partitions.flat() };
}
function dayRows(records) {
return records.flatMap((record) =>
Array.isArray(record.day_totals) ? record.day_totals : [record],
);
}
function aggregateOrganizationAgents(records) {
const agents = new Map();
for (const row of dayRows(records)) {
for (const item of row.totals_by_3rd_party_agent ?? []) {
const current = agents.get(item.agent_id) ?? {
agentId: item.agent_id,
agentName: item.agent_name,
jobStarts: 0,
sessions: 0,
activeDays: 0,
};
current.agentName = item.agent_name;
current.jobStarts += item.user_initiated_interaction_count ?? 0;
current.sessions += item.session_count ?? 0;
current.activeDays += 1;
agents.set(item.agent_id, current);
}
}
return [...agents.values()];
}
function aggregateAgentReach(userRecords) {
const agents = new Map();
for (const user of userRecords) {
for (const item of user.totals_by_3rd_party_agent ?? []) {
const current = agents.get(item.agent_id) ?? {
agentId: item.agent_id,
agentName: item.agent_name,
users: new Set(),
jobStarts: 0,
};
current.agentName = item.agent_name;
current.users.add(user.user_id);
current.jobStarts += item.user_initiated_interaction_count ?? 0;
agents.set(item.agent_id, current);
}
}
return [...agents.values()].map(({ users, ...agent }) => ({
...agent,
distinctUsers: users.size,
}));
}
const [organizationReport, usersReport] = await Promise.all([
getReport("organization-28-day/latest"),
getReport("users-28-day/latest"),
]);
const output = {
window: {
start: organizationReport.meta.report_start_day,
end: organizationReport.meta.report_end_day,
},
activity: aggregateOrganizationAgents(organizationReport.records),
reach: aggregateAgentReach(usersReport.records),
};
console.log(JSON.stringify(output, null, 2));
Save it as copilot-agent-metrics.mjs and run:
node copilot-agent-metrics.mjs > copilot-agent-metrics.json
The script intentionally groups by agent_id and refreshes agent_name for display. If a name changes, the historical series remains attached to the stable identifier.

There are three different aggregation operations in the script:
day_totals only for the organization 28-day report.agent_id.Set of user_id values in the per-user report to calculate distinct reach.Do not infer distinct users from session_count or job starts. Ten starts could be ten people trying an agent once or one person starting ten jobs. The per-user report distinguishes those cases.
Do not divide job starts by the top-level monthly active-user count and label the result “agent adoption.” That denominator includes Copilot activity that may have nothing to do with third-party agent apps. A clearer measure is:
agent reach = distinct users with activity for agent_id
÷ eligible users in the rollout cohort
The API report gives you the numerator. Your seat-assignment or rollout registry should supply the eligible cohort. If only half the organization was allowed to use an agent, dividing by every Copilot seat understates adoption.
The aggregate report can support an intensity measure:
starts per reached user = agent job starts ÷ distinct agent users
This shows repeat use, not quality. A high value might represent a useful agent, fragmented jobs, retries after failures, or a workflow that forces users to restart. Pair it with success and review data from the agent’s own operational telemetry.
A rollout dashboard should separate availability, trial, continued use, activity, cost, and outcome.
| Metric | Calculation | What it actually says |
|---|---|---|
| Eligible users | Users included in rollout policy | Who could use the agent |
| Reached users | Distinct user_id values for an agent_id |
Who started at least one job |
| Reach rate | Reached ÷ eligible | Breadth of trial or use |
| Job starts | Sum of nested agent interactions | How often users initiated work |
| Sessions | Sum of aggregate session_count |
Session volume, not users |
| Starts per reached user | Starts ÷ reached users | Usage intensity |
| Active days | Days with a reported agent entry | Continuity of organizational activity |
| AI credits per reached user | User credit total ÷ reached users | Directional consumption, not agent cost |
The last metric needs a warning. GitHub’s per-user ai_credits_used is an overall consumption signal. It is not broken down by feature, model, surface, or agent and is not an invoiced total. You cannot assign that entire number to one agent merely because the user also appears in totals_by_3rd_party_agent.
Likewise, lines added, pull requests merged, and job starts should not be turned into causal productivity claims. A team that adopts an agent may already be more active. Use pre-defined cohorts, compare similar teams or repositories, retain a pre-rollout baseline, and treat the result as observational unless the rollout design supports stronger inference.
For automated work that can modify repositories, adoption is only one side of the control system. The safe-output architecture for GitHub agentic workflows covers the review and permission boundary that usage dashboards cannot enforce.
totals_by_3rd_party_agent is optional. GitHub omits it entirely when there is no recognized agent activity during the report period. Code must use a missing-safe fallback such as ?? [] rather than assuming the field exists.
An omitted array does not always mean “nobody used agents.” It means the report contains no recognized agent-app activity. Activity from agents GitHub cannot identify is omitted, and multiple apps belonging to one recognized agent are consolidated.
Other gaps can affect comparisons:
agent_name instead of agent_id.Store the report window, extraction timestamp, API version, organization ID, agent ID, and current display name with every derived record. That metadata lets you explain a shifted chart instead of quietly rewriting history.
Per-user reports contain GitHub logins, stable user IDs, usage patterns, and AI-credit consumption. Treat them as employee analytics, not harmless product telemetry.
Keep the raw NDJSON in a restricted analytics location with a defined retention period. Publish team or organization aggregates by default. Limit user-level access to people who have a legitimate operational need, and avoid performance rankings based on starts, prompts, or lines of code.
Signed report URLs are temporary credentials to report data. Do not log them, commit them, or paste them into tickets. Download immediately over HTTPS, validate JSON parsing, then discard the URL. Keep tokens out of command history and source files; use a CI secret or workload identity appropriate to your GitHub App.
For ingestion, make the job idempotent on a key such as:
organization_id + report_end_day + report_type + agent_id
Upsert derived rows rather than appending duplicates every time the latest report is fetched. Preserve previous display names separately if audit history matters.
Start with four views:
day_totals records.Add cost and delivery outcomes only when their definitions are defensible. Useful companion signals include review turnaround, rollback rate, escaped defects, task completion, and human rework. Avoid one composite “AI productivity score”; it hides disagreements between adoption, cost, quality, and speed.
A production ingestion job should:
agent_id;Use agent_id. GitHub documents it as stable and warns that agent_name can change. Keep the name as display metadata only.
No. The nested totals_by_3rd_party_agent[].user_initiated_interaction_count counts agent-app job starts. The top-level field with the same name counts explicit prompts from other supported telemetry. Do not add or reconcile them.
totals_by_3rd_party_agent missing?#The optional array is omitted when the reporting period has no recognized agent-app activity. Unidentified-agent activity is also omitted, so absence should be reported as “no recognized activity in this report,” not as proof that no agent ran.
Yes, by using the per-user report. Group records by agent_id and count distinct user_id values. Do not use aggregate session or job-start counts as a proxy for people.
ai_credits_used?#Not accurately. The per-user credit field is an overall usage signal and is not broken down by agent, model, feature, or surface. Use it for directional consumption analysis, not per-agent billing attribution.
The new per-agent breakdown turns the GitHub Copilot usage metrics API into a practical source for agent-app rollout analytics. The reliable implementation is straightforward: retrieve both aggregate and per-user reports, process every NDJSON partition, group by agent_id, keep agent starts separate from top-level prompts, and publish metric definitions with the dashboard.
That yields honest adoption evidence without pretending usage volume is productivity, quality, or cost attribution. Those outcomes still require separate operational data and a rollout design capable of supporting the conclusions you want to make.
Comments
Sign in to join the discussion.
No comments yet. Be the first to share your thoughts.