This is an automated email from the ASF dual-hosted git repository.
eldrick pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/incubator-devlake.git
The following commit(s) were added to refs/heads/main by this push:
new 2e9cca0eb fix(gh-copilot): skip empty report responses for dates
without Copilot data (#8804)
2e9cca0eb is described below
commit 2e9cca0eb043bd13409f92f51ac3701bfa062a69
Author: Bryan Reymander <[email protected]>
AuthorDate: Mon Mar 30 11:53:09 2026 -0400
fix(gh-copilot): skip empty report responses for dates without Copilot data
(#8804)
GitHub's Copilot Usage Metrics Reports API returns HTTP 200 with body
"" for dates before usage data was available, instead of returning a
404. The existing ignore404 callback does not catch this, so the
ResponseParser attempts json.Unmarshal on "" which fails and crashes
the collector pipeline.
Add an isEmptyReport guard that detects empty/null bodies and returns
nil so the collector silently skips those days.
Fixes apache#8789
---
.../tasks/enterprise_metrics_collector.go | 4 +++-
.../gh-copilot/tasks/metrics_collector_test.go | 21 +++++++++++++++++++++
.../gh-copilot/tasks/org_metrics_collector.go | 3 +++
.../gh-copilot/tasks/report_download_helper.go | 8 ++++++++
.../gh-copilot/tasks/user_metrics_collector.go | 3 +++
5 files changed, 38 insertions(+), 1 deletion(-)
diff --git a/backend/plugins/gh-copilot/tasks/enterprise_metrics_collector.go
b/backend/plugins/gh-copilot/tasks/enterprise_metrics_collector.go
index 076b7df77..08ad03861 100644
--- a/backend/plugins/gh-copilot/tasks/enterprise_metrics_collector.go
+++ b/backend/plugins/gh-copilot/tasks/enterprise_metrics_collector.go
@@ -95,12 +95,14 @@ func CollectEnterpriseMetrics(taskCtx
plugin.SubTaskContext) errors.Error {
Concurrency: 1,
AfterResponse: ignore404,
ResponseParser: func(res *http.Response) ([]json.RawMessage,
errors.Error) {
- // Parse metadata response to get download links
body, readErr := io.ReadAll(res.Body)
res.Body.Close()
if readErr != nil {
return nil, errors.Default.Wrap(readErr,
"failed to read report metadata")
}
+ if isEmptyReport(body) {
+ return nil, nil
+ }
var meta reportMetadataResponse
if jsonErr := json.Unmarshal(body, &meta); jsonErr !=
nil {
diff --git a/backend/plugins/gh-copilot/tasks/metrics_collector_test.go
b/backend/plugins/gh-copilot/tasks/metrics_collector_test.go
index cfbab3040..e8b1dbecc 100644
--- a/backend/plugins/gh-copilot/tasks/metrics_collector_test.go
+++ b/backend/plugins/gh-copilot/tasks/metrics_collector_test.go
@@ -67,3 +67,24 @@ func TestComputeReportDateRangeClampsFutureSince(t
*testing.T) {
require.Equal(t, time.Date(2025, 1, 9, 0, 0, 0, 0, time.UTC), until)
require.Equal(t, time.Date(2025, 1, 9, 0, 0, 0, 0, time.UTC), start)
}
+
+func TestIsEmptyReport(t *testing.T) {
+ tests := []struct {
+ name string
+ body []byte
+ want bool
+ }{
+ {"empty JSON string", []byte(`""`), true},
+ {"null", []byte("null"), true},
+ {"empty body", []byte{}, true},
+ {"whitespace only", []byte(" "), true},
+ {"padded empty string", []byte(` "" `), true},
+ {"valid metadata",
[]byte(`{"download_links":["https://example.com/report.json"],"report_day":"2026-03-19"}`),
false},
+ {"valid metadata empty links",
[]byte(`{"download_links":[],"report_day":"2026-03-19"}`), false},
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ require.Equal(t, tt.want, isEmptyReport(tt.body))
+ })
+ }
+}
diff --git a/backend/plugins/gh-copilot/tasks/org_metrics_collector.go
b/backend/plugins/gh-copilot/tasks/org_metrics_collector.go
index 9eee0fe33..82f3fc36c 100644
--- a/backend/plugins/gh-copilot/tasks/org_metrics_collector.go
+++ b/backend/plugins/gh-copilot/tasks/org_metrics_collector.go
@@ -94,6 +94,9 @@ func CollectOrgMetrics(taskCtx plugin.SubTaskContext)
errors.Error {
if readErr != nil {
return nil, errors.Default.Wrap(readErr,
"failed to read report metadata")
}
+ if isEmptyReport(body) {
+ return nil, nil
+ }
var meta reportMetadataResponse
if jsonErr := json.Unmarshal(body, &meta); jsonErr !=
nil {
diff --git a/backend/plugins/gh-copilot/tasks/report_download_helper.go
b/backend/plugins/gh-copilot/tasks/report_download_helper.go
index 857712b8a..39da15f20 100644
--- a/backend/plugins/gh-copilot/tasks/report_download_helper.go
+++ b/backend/plugins/gh-copilot/tasks/report_download_helper.go
@@ -60,6 +60,14 @@ func ignore404(res *http.Response) errors.Error {
return nil
}
+// isEmptyReport returns true when the GitHub API returned an HTTP 200 but the
+// body carries no usable report data. For dates before Copilot usage data was
+// available the API responds with "" (empty JSON string) instead of a 404.
+func isEmptyReport(body []byte) bool {
+ b := bytes.TrimSpace(body)
+ return len(b) == 0 || string(b) == `""` || string(b) == "null"
+}
+
// reportMetadataResponse represents the JSON returned by the report metadata
endpoints.
type reportMetadataResponse struct {
DownloadLinks []string `json:"download_links"`
diff --git a/backend/plugins/gh-copilot/tasks/user_metrics_collector.go
b/backend/plugins/gh-copilot/tasks/user_metrics_collector.go
index f665a85e7..ef3e21bb8 100644
--- a/backend/plugins/gh-copilot/tasks/user_metrics_collector.go
+++ b/backend/plugins/gh-copilot/tasks/user_metrics_collector.go
@@ -99,6 +99,9 @@ func CollectUserMetrics(taskCtx plugin.SubTaskContext)
errors.Error {
if readErr != nil {
return nil, errors.Default.Wrap(readErr,
"failed to read report metadata")
}
+ if isEmptyReport(body) {
+ return nil, nil
+ }
var meta reportMetadataResponse
if jsonErr := json.Unmarshal(body, &meta); jsonErr !=
nil {