Skip to content

feat: Add typed Copilot metrics download helpers#4177

Open
kyungseopk1m wants to merge 4 commits intogoogle:masterfrom
kyungseopk1m:feat/copilot-metrics-download-helpers
Open

feat: Add typed Copilot metrics download helpers#4177
kyungseopk1m wants to merge 4 commits intogoogle:masterfrom
kyungseopk1m:feat/copilot-metrics-download-helpers

Conversation

@kyungseopk1m
Copy link
Copy Markdown

Fixes #4136

Summary

The new Copilot usage metrics endpoints (/copilot/metrics/reports/*) return download links whose payloads do not match the CopilotMetrics shape, so the existing DownloadCopilotMetrics helper fails to decode live data from the eight Get*MetricsReport methods on public GitHub.

This PR adds four typed helpers and deprecates the legacy one:

Link returned by Use
Get{Enterprise,Organization}DailyMetricsReport DownloadDailyMetrics
Get{Enterprise,Organization}MetricsReport DownloadPeriodicMetrics
Get{Enterprise,Organization}UsersDailyMetricsReport DownloadUserDailyMetrics
Get{Enterprise,Organization}UsersMetricsReport DownloadUserPeriodicMetrics

User metrics payloads are newline-delimited JSON; aggregate payloads are JSON objects (1-day) or a { day_totals: [...] } wrapper (28-day).

Payload schemas

GitHub's public OpenAPI and REST documentation only describe the top-level response (download_links, report_start_day/end_day) and do not publish the schema of the downloaded files. The struct definitions in this PR are based on the live-data structs posted by @Roemer in #4149 (comment) and the official Copilot usage metrics documentation. Follow-up PRs can adjust any field names that differ from what live endpoints emit.

Notes

  • DownloadCopilotMetrics is kept with a Deprecated: comment so GitHub Enterprise Server installations that may still serve the legacy shape continue to work.
  • The four new helpers are registered in tools/metadata/metadata.go skip list, following the same pattern as the existing DownloadCopilotMetrics (helper methods without direct GitHub v3 API correspondence).
  • Most of the added lines are auto-generated accessors via script/generate.sh for the new typed structs.

Testing

  • script/fmt.sh, script/generate.sh, script/lint.sh, script/test.sh all pass locally.
  • Each helper covers success, HTTP status error, invalid URL, invalid scheme, malformed JSON; the NDJSON helpers additionally cover empty-body → nil slice behaviour.

cc @gmlewis

Add DownloadDailyMetrics, DownloadPeriodicMetrics,
DownloadUserDailyMetrics, and DownloadUserPeriodicMetrics to
decode the payloads served at the download links returned by
the new Get*MetricsReport endpoints. User metrics payloads are
newline-delimited JSON; aggregate payloads are JSON objects
(1-day) or a { day_totals: [...] } wrapper (28-day).

Deprecate DownloadCopilotMetrics because the []*CopilotMetrics
shape it produces does not match the new payloads. It is kept
for GitHub Enterprise Server installations that may still serve
the legacy shape.

Fixes google#4136
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 23, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 93.73%. Comparing base (3430163) to head (39e8e1b).
⚠️ Report is 1 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #4177      +/-   ##
==========================================
+ Coverage   93.18%   93.73%   +0.55%     
==========================================
  Files         210      210              
  Lines       24568    19804    -4764     
==========================================
- Hits        22894    18564    -4330     
+ Misses       1478     1044     -434     
  Partials      196      196              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Copy Markdown
Collaborator

@gmlewis gmlewis left a comment

Choose a reason for hiding this comment

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

Thank you, @kyungseopk1m!
I'm stopping the review now, but please address the concerns and I will continue later when I have more time.

Comment thread github/copilot.go Outdated
Comment thread github/copilot.go Outdated
Comment thread github/copilot.go Outdated
Comment thread github/copilot.go Outdated
Comment thread github/copilot.go Outdated
Comment thread github/copilot.go Outdated
Comment thread github/copilot.go Outdated
Comment thread github/copilot.go Outdated
Go initialism convention requires acronyms to be all caps. Rename
the Go identifiers introduced for the Copilot metrics download
helpers so that "CLI" is written as an initialism:

- `CopilotMetricsCli` -> `CopilotMetricsCLI`
- `CopilotMetricsCliVersion` -> `CopilotMetricsCLIVersion`
- `CopilotMetricsCliTokenUsage` -> `CopilotMetricsCLITokenUsage`
- `CliVersion`, `LastKnownCliVersion`, `DailyActiveCliUsers`,
  `TotalsByCli`, and `UsedCli` field renames accordingly

JSON tags on the wire (`cli_version`, `used_cli`, `totals_by_cli`,
`last_known_cli_version`, `daily_active_cli_users`) are left
unchanged so the decoder still matches the GitHub API payload.

Also add `"CLI"` to the `structfield` linter's initialism list so
the rule catches future regressions. Accessors are regenerated.
@kyungseopk1m
Copy link
Copy Markdown
Author

Thanks for the review, @gmlewis! I've addressed all the points in 4cd1270 on this branch.

Copy link
Copy Markdown
Collaborator

@gmlewis gmlewis left a comment

Choose a reason for hiding this comment

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

Thank you, @kyungseopk1m!
LGTM.
Awaiting second LGTM+Approval from any other contributor to this repo before merging.

cc: @stevehipwell - @alexandear - @zyfy29 - @Not-Dhananjay-Mishra - @munlicode

@gmlewis gmlewis added the NeedsReview PR is awaiting a review before merging. label Apr 23, 2026
Comment thread github/copilot.go Outdated
Comment on lines +879 to +882
LocSuggestedToAddSum int `json:"loc_suggested_to_add_sum"`
LocSuggestedToDeleteSum int `json:"loc_suggested_to_delete_sum"`
LocAddedSum int `json:"loc_added_sum"`
LocDeletedSum int `json:"loc_deleted_sum"`
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LOC is the abbreviation for "Lines of code":

Suggested change
LocSuggestedToAddSum int `json:"loc_suggested_to_add_sum"`
LocSuggestedToDeleteSum int `json:"loc_suggested_to_delete_sum"`
LocAddedSum int `json:"loc_added_sum"`
LocDeletedSum int `json:"loc_deleted_sum"`
LOCSuggestedToAddSum int `json:"loc_suggested_to_add_sum"`
LOCSuggestedToDeleteSum int `json:"loc_suggested_to_delete_sum"`
LOCAddedSum int `json:"loc_added_sum"`
LOCDeletedSum int `json:"loc_deleted_sum"`

Comment thread github/copilot.go Outdated
Comment on lines +913 to +918
CodeGenerationActivityCount int `json:"code_generation_activity_count"`
CodeAcceptanceActivityCount int `json:"code_acceptance_activity_count"`
LocSuggestedToAddSum int `json:"loc_suggested_to_add_sum"`
LocSuggestedToDeleteSum int `json:"loc_suggested_to_delete_sum"`
LocAddedSum int `json:"loc_added_sum"`
LocDeletedSum int `json:"loc_deleted_sum"`
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

These fields are the same for CopilotMetricsIDE, CopilotMetricsLanguageFeature, CopilotMetricsModelFeature. Maybe we can move them to a separate struct?

Comment thread github/copilot.go
Comment on lines +1111 to +1122
resp, r, err := s.fetchMetricsReport(ctx, url)
if err != nil {
return nil, r, err
}
defer resp.Body.Close()

var metrics *CopilotDailyMetrics
if err := json.NewDecoder(resp.Body).Decode(&metrics); err != nil {
return nil, r, err
}

return metrics, r, nil
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'm curious: can we use the same implementation style for download metrics endpoints as for other endpoints in the repo?

Something like:

Suggested change
resp, r, err := s.fetchMetricsReport(ctx, url)
if err != nil {
return nil, r, err
}
defer resp.Body.Close()
var metrics *CopilotDailyMetrics
if err := json.NewDecoder(resp.Body).Decode(&metrics); err != nil {
return nil, r, err
}
return metrics, r, nil
req, err := s.client.NewRequest("GET", url, nil)
if err != nil {
return nil, nil, err
}
var metrics *CopilotDailyMetrics
resp, err := s.client.Do(ctx, req, &metrics)
if err != nil {
return nil, resp, err
}
return metrics, resp, nil

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

The pattern matches DownloadCopilotMetrics (copilot.go:837) and DownloadContentsWithMeta (repos_contents.go:178) — both download from a server-returned URL using raw http.NewRequestWithContext + s.client.client.Do(req), and I wanted these four to stay consistent with that. Two of them are also NDJSON, which s.client.Do(ctx, req, &v) can't decode in a single call. Happy to revisit if you'd prefer otherwise.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@kyungseopk1m thanks for the explanation.

@gmlewis is it possible to hit rate limits when downloading metrics? The raw http.NewRequestWithContext + s.client.client.Do(req) approach doesn't handle that. That's why I proposed using our custom HTTP client.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Great question. Sorry, but I don't know.

This question should probably be sent to the official GitHub tech support team.

@kyungseopk1m
Copy link
Copy Markdown
Author

@alexandear Done in 90c1208 — Loc* → LOC* (added LOC to the initialism map) and CopilotMetricsCodeActivity embedded in the six smaller per-breakdown structs. Kept CopilotDailyMetrics, CopilotUserDailyMetrics, CopilotUserPeriodicMetrics inline since embedding a 6-field chunk in the middle of a 20+ field struct didn't help readability. Let me know if you'd prefer them uniform too.

Copy link
Copy Markdown
Contributor

@Not-Dhananjay-Mishra Not-Dhananjay-Mishra left a comment

Choose a reason for hiding this comment

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

I am curious why aren't we using * + omitempty for all fields since there is no official response schema present, so we don't know which fields will always be returned.
example - DailyActiveCLIUsers is present in struct but it is absent from comment.

and some fields like chat_panel_agent_mode, chat_panel_ask_mode, chat_panel_custom_mode, chat_panel_edit_mode and chat_panel_unknown_mode are missing from structs but present in Data available in Copilot usage metrics docs

@kyungseopk1m
Copy link
Copy Markdown
Author

Fair points, thanks. Will switch the int/bool fields to *T + omitempty to match CopilotMetrics, and add the missing chat_panel_* fields.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

NeedsReview PR is awaiting a review before merging.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Handling of the new Copilot Metrics Endpoints

4 participants