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 {

Reply via email to