Repository: xanzy/go-gitlab
Pull request #1931 (2024-05-04 16:26:24)

Add support for getting DORA metrics from projects and groups
Resolves https://github.com/xanzy/go-gitlab/issues/1557

I've taken the code changes from this pull request from 2022: https://github.com/xanzy/go-gitlab/pull/1582

It was not merged as there were unresolved changes requested following the code review. Hopefully I have addressed most of the requests.


Patch:
diff --git a/dora_metrics.go b/dora_metrics.go
new file mode 100644
index 0000000..a2ad418
--- /dev/null
+++ b/dora_metrics.go
@@ -0,0 +1,110 @@
+//
+// Copyright 2021, Sander van Harmelen
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package gitlab
+
+import (
+	"fmt"
+	"net/http"
+)
+
+// DORAMetricsService handles communication with the DORA metrics related methods
+// of the GitLab API.
+//
+// Gitlab API docs: https://docs.gitlab.com/ee/api/dora/metrics.html
+type DORAMetricsService struct {
+	client *Client
+}
+
+// DORAMetric represents a single DORA metric data point.
+//
+// Gitlab API docs: https://docs.gitlab.com/ee/api/dora/metrics.html
+type DORAMetric struct {
+	Date  string  `json:"date"`
+	Value float64 `json:"value"`
+}
+
+// Gets a string representation of a DORAMetric data point
+//
+// GitLab API docs: https://docs.gitlab.com/ee/api/dora/metrics.html
+func (m DORAMetric) String() string {
+	return Stringify(m)
+}
+
+// GetDORAMetricsOptions represent the request body options for getting
+// DORA metrics
+//
+// GitLab API docs: https://docs.gitlab.com/ee/api/dora/metrics.html
+type GetDORAMetricsOptions struct {
+	Metric           *DORAMetricType     `url:"metric,omitempty" json:"metric,omitempty"`
+	EndDate          *ISOTime            `url:"end_date,omitempty" json:"end_date,omitempty"`
+	EnvironmentTiers *[]string           `url:"environment_tiers,comma,omitempty" json:"environment_tiers,omitempty"`
+	Interval         *DORAMetricInterval `url:"interval,omitempty" json:"interval,omitempty"`
+	StartDate        *ISOTime            `url:"start_date,omitempty" json:"start_date,omitempty"`
+
+	// Deprecated, use environment tiers instead
+	EnvironmentTier *string `url:"environment_tier,omitempty" json:"environment_tier,omitempty"`
+}
+
+// GetProjectDORAMetrics gets the DORA metrics for a project.
+//
+// GitLab API Docs:
+// https://docs.gitlab.com/ee/api/dora/metrics.html#get-project-level-dora-metrics
+func (s *DORAMetricsService) GetProjectDORAMetrics(pid interface{}, opt GetDORAMetricsOptions, options ...RequestOptionFunc) ([]DORAMetric, *Response, error) {
+	project, err := parseID(pid)
+	if err != nil {
+		return nil, nil, err
+	}
+	u := fmt.Sprintf("projects/%s/dora/metrics", PathEscape(project))
+
+	req, err := s.client.NewRequest(http.MethodGet, u, opt, options)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	var metrics []DORAMetric
+	resp, err := s.client.Do(req, &metrics)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return metrics, resp, err
+}
+
+// GetGroupDORAMetrics gets the DORA metrics for a group.
+//
+// GitLab API Docs:
+// https://docs.gitlab.com/ee/api/dora/metrics.html#get-group-level-dora-metrics
+func (s *DORAMetricsService) GetGroupDORAMetrics(gid interface{}, opt GetDORAMetricsOptions, options ...RequestOptionFunc) ([]DORAMetric, *Response, error) {
+	group, err := parseID(gid)
+	if err != nil {
+		return nil, nil, err
+	}
+	u := fmt.Sprintf("groups/%s/dora/metrics", PathEscape(group))
+
+	req, err := s.client.NewRequest(http.MethodGet, u, opt, options)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	var metrics []DORAMetric
+	resp, err := s.client.Do(req, &metrics)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return metrics, resp, err
+}
diff --git a/gitlab.go b/gitlab.go
index e492050..6df108f 100644
--- a/gitlab.go
+++ b/gitlab.go
@@ -124,6 +124,7 @@ type Client struct {
 	Deployments                  *DeploymentsService
 	Discussions                  *DiscussionsService
 	DockerfileTemplate           *DockerfileTemplatesService
+	DORAMetrics                  *DORAMetricsService
 	DraftNotes                   *DraftNotesService
 	Environments                 *EnvironmentsService
 	EpicIssues                   *EpicIssuesService
@@ -358,6 +359,7 @@ func newClient(options ...ClientOptionFunc) (*Client, error) {
 	c.Deployments = &DeploymentsService{client: c}
 	c.Discussions = &DiscussionsService{client: c}
 	c.DockerfileTemplate = &DockerfileTemplatesService{client: c}
+	c.DORAMetrics = &DORAMetricsService{client: c}
 	c.DraftNotes = &DraftNotesService{client: c}
 	c.Environments = &EnvironmentsService{client: c}
 	c.EpicIssues = &EpicIssuesService{client: c}
diff --git a/types.go b/types.go
index 50e03ed..c87c248 100644
--- a/types.go
+++ b/types.go
@@ -62,7 +62,7 @@ func AccessControl(v AccessControlValue) *AccessControlValue {
 // GitLab API docs: https://docs.gitlab.com/ee/user/permissions.html
 type AccessLevelValue int
 
-// List of available access levels
+// List of available access levels.
 //
 // GitLab API docs: https://docs.gitlab.com/ee/user/permissions.html
 const (
@@ -113,7 +113,7 @@ func ApproverIDs(v interface{}) *ApproverIDsValue {
 	}
 }
 
-// EncodeValues implements the query.Encoder interface
+// EncodeValues implements the query.Encoder interface.
 func (a *ApproverIDsValue) EncodeValues(key string, v *url.Values) error {
 	switch value := a.value.(type) {
 	case UserIDValue:
@@ -128,12 +128,12 @@ func (a *ApproverIDsValue) EncodeValues(key string, v *url.Values) error {
 	return nil
 }
 
-// MarshalJSON implements the json.Marshaler interface
+// MarshalJSON implements the json.Marshaler interface.
 func (a ApproverIDsValue) MarshalJSON() ([]byte, error) {
 	return json.Marshal(a.value)
 }
 
-// UnmarshalJSON implements the json.Unmarshaler interface
+// UnmarshalJSON implements the json.Unmarshaler interface.
 func (a *ApproverIDsValue) UnmarshalJSON(bytes []byte) error {
 	return json.Unmarshal(bytes, a.value)
 }
@@ -153,7 +153,7 @@ func AssigneeID(v interface{}) *AssigneeIDValue {
 	}
 }
 
-// EncodeValues implements the query.Encoder interface
+// EncodeValues implements the query.Encoder interface.
 func (a *AssigneeIDValue) EncodeValues(key string, v *url.Values) error {
 	switch value := a.value.(type) {
 	case UserIDValue:
@@ -164,12 +164,12 @@ func (a *AssigneeIDValue) EncodeValues(key string, v *url.Values) error {
 	return nil
 }
 
-// MarshalJSON implements the json.Marshaler interface
+// MarshalJSON implements the json.Marshaler interface.
 func (a AssigneeIDValue) MarshalJSON() ([]byte, error) {
 	return json.Marshal(a.value)
 }
 
-// UnmarshalJSON implements the json.Unmarshaler interface
+// UnmarshalJSON implements the json.Unmarshaler interface.
 func (a *AssigneeIDValue) UnmarshalJSON(bytes []byte) error {
 	return json.Unmarshal(bytes, a.value)
 }
@@ -189,7 +189,7 @@ func ReviewerID(v interface{}) *ReviewerIDValue {
 	}
 }
 
-// EncodeValues implements the query.Encoder interface
+// EncodeValues implements the query.Encoder interface.
 func (a *ReviewerIDValue) EncodeValues(key string, v *url.Values) error {
 	switch value := a.value.(type) {
 	case UserIDValue:
@@ -200,12 +200,12 @@ func (a *ReviewerIDValue) EncodeValues(key string, v *url.Values) error {
 	return nil
 }
 
-// MarshalJSON implements the json.Marshaler interface
+// MarshalJSON implements the json.Marshaler interface.
 func (a ReviewerIDValue) MarshalJSON() ([]byte, error) {
 	return json.Marshal(a.value)
 }
 
-// UnmarshalJSON implements the json.Unmarshaler interface
+// UnmarshalJSON implements the json.Unmarshaler interface.
 func (a *ReviewerIDValue) UnmarshalJSON(bytes []byte) error {
 	return json.Unmarshal(bytes, a.value)
 }
@@ -285,12 +285,43 @@ func DeploymentStatus(v DeploymentStatusValue) *DeploymentStatusValue {
 	return Ptr(v)
 }
 
-// EventTypeValue represents actions type for contribution events
+// DORAMetricType represents all valid DORA metrics types.
+//
+// GitLab API docs: https://docs.gitlab.com/ee/api/dora/metrics.html
+type DORAMetricType string
+
+// List of available DORA metric type names.
+//
+// GitLab API docs: https://docs.gitlab.com/ee/api/dora/metrics.html
+const (
+	DORAMetricDeploymentFrequency  DORAMetricType = "deployment_frequency"
+	DORAMetricLeadTimeForChanges   DORAMetricType = "lead_time_for_changes"
+	DORAMetricTimeToRestoreService DORAMetricType = "time_to_restore_service"
+	DORAMetricChangeFailureRate    DORAMetricType = "change_failure_rate"
+)
+
+// DORAMetricInterval represents the time period over which the
+// metrics are aggregated.
+//
+// GitLab API docs: https://docs.gitlab.com/ee/api/dora/metrics.html
+type DORAMetricInterval string
+
+// List of available DORA metric interval types.
+//
+// GitLab API docs: https://docs.gitlab.com/ee/api/dora/metrics.html
+const (
+	DORAMetricIntervalDaily   DORAMetricInterval = "daily"
+	DORAMetricIntervalMonthly DORAMetricInterval = "monthly"
+	DORAMetricIntervalAll     DORAMetricInterval = "all"
+)
+
+// EventTypeValue represents actions type for contribution events.
 type EventTypeValue string
 
-// List of available action type
+// List of available action type.
 //
-// GitLab API docs: https://docs.gitlab.com/ee/user/profile/contributions_calendar.html#user-contribution-events
+// GitLab API docs:
+// https://docs.gitlab.com/ee/user/profile/contributions_calendar.html#user-contribution-events
 const (
 	CreatedEventType   EventTypeValue = "created"
 	UpdatedEventType   EventTypeValue = "updated"
@@ -305,10 +336,10 @@ const (
 	ExpiredEventType   EventTypeValue = "expired"
 )
 
-// EventTargetTypeValue represents actions type value for contribution events
+// EventTargetTypeValue represents actions type value for contribution events.
 type EventTargetTypeValue string
 
-// List of available action type
+// List of available action type.
 //
 // GitLab API docs: https://docs.gitlab.com/ee/api/events.html#target-types
 const (
@@ -323,7 +354,8 @@ const (
 
 // FileActionValue represents the available actions that can be performed on a file.
 //
-// GitLab API docs: https://docs.gitlab.com/ee/api/commits.html#create-a-commit-with-multiple-files-and-actions
+// GitLab API docs:
+// https://docs.gitlab.com/ee/api/commits.html#create-a-commit-with-multiple-files-and-actions
 type FileActionValue string
 
 // The available file actions.
@@ -379,7 +411,7 @@ func GenericPackageStatus(v GenericPackageStatusValue) *GenericPackageStatusValu
 // ISOTime represents an ISO 8601 formatted date.
 type ISOTime time.Time
 
-// ISO 8601 date format
+// ISO 8601 date format.
 const iso8601 = "2006-01-02"
 
 // ParseISOTime parses an ISO 8601 formatted date.
@@ -457,7 +489,7 @@ func (l *LabelOptions) UnmarshalJSON(data []byte) error {
 	return json.Unmarshal(data, (*alias)(l))
 }
 
-// EncodeValues implements the query.EncodeValues interface
+// EncodeValues implements the query.EncodeValues interface.
 func (l *LabelOptions) EncodeValues(key string, v *url.Values) error {
 	v.Set(key, strings.Join(*l, ","))
 	return nil
@@ -654,7 +686,8 @@ const (
 	DisabledAndOverridableSharedRunnersSettingValue   SharedRunnersSettingValue = "disabled_and_overridable"
 	DisabledAndUnoverridableSharedRunnersSettingValue SharedRunnersSettingValue = "disabled_and_unoverridable"
 
-	// Deprecated: DisabledWithOverrideSharedRunnersSettingValue is deprecated in favor of DisabledAndOverridableSharedRunnersSettingValue
+	// Deprecated: DisabledWithOverrideSharedRunnersSettingValue is deprecated
+	// in favor of DisabledAndOverridableSharedRunnersSettingValue.
 	DisabledWithOverrideSharedRunnersSettingValue SharedRunnersSettingValue = "disabled_with_override"
 )
 


Test patch:
diff --git a/dora_metrics_test.go b/dora_metrics_test.go
new file mode 100644
index 0000000..4e6ec09
--- /dev/null
+++ b/dora_metrics_test.go
@@ -0,0 +1,118 @@
+package gitlab
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestDORAMetrics_GetProjectDORAMetrics(t *testing.T) {
+	mux, client := setup(t)
+
+	mux.HandleFunc("/api/v4/projects/1/dora/metrics", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, http.MethodGet)
+		query := r.URL.Query()
+		for k, v := range map[string]string{
+			"metric":     "deployment_frequency",
+			"start_date": "2021-03-01",
+			"end_date":   "2021-03-08",
+		} {
+			if query.Get(k) != v {
+				t.Errorf("Query parameter %s: %s, want %s", k, query.Get(k), v)
+			}
+		}
+
+		fmt.Fprint(w, `
+			[
+				{ "date": "2021-03-01", "value": 3 },
+				{ "date": "2021-03-02", "value": 6 },
+				{ "date": "2021-03-03", "value": 0 },
+				{ "date": "2021-03-04", "value": 0 },
+				{ "date": "2021-03-05", "value": 0 },
+				{ "date": "2021-03-06", "value": 0 },
+				{ "date": "2021-03-07", "value": 0 },
+				{ "date": "2021-03-08", "value": 4 }
+			]
+		`)
+	})
+
+	want := []DORAMetric{
+		{Date: "2021-03-01", Value: 3},
+		{Date: "2021-03-02", Value: 6},
+		{Date: "2021-03-03", Value: 0},
+		{Date: "2021-03-04", Value: 0},
+		{Date: "2021-03-05", Value: 0},
+		{Date: "2021-03-06", Value: 0},
+		{Date: "2021-03-07", Value: 0},
+		{Date: "2021-03-08", Value: 4},
+	}
+
+	startDate := ISOTime(time.Date(2021, time.March, 1, 0, 0, 0, 0, time.UTC))
+	endDate := ISOTime(time.Date(2021, time.March, 8, 0, 0, 0, 0, time.UTC))
+
+	d, resp, err := client.DORAMetrics.GetProjectDORAMetrics(1, GetDORAMetricsOptions{
+		Metric:    Ptr(DORAMetricDeploymentFrequency),
+		StartDate: &startDate,
+		EndDate:   &endDate,
+	})
+	require.NoError(t, err)
+	require.NotNil(t, resp)
+	require.Equal(t, want, d)
+}
+
+func TestDORAMetrics_GetGroupDORAMetrics(t *testing.T) {
+	mux, client := setup(t)
+
+	mux.HandleFunc("/api/v4/groups/1/dora/metrics", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, http.MethodGet)
+		query := r.URL.Query()
+		for k, v := range map[string]string{
+			"metric":     "deployment_frequency",
+			"start_date": "2021-03-01",
+			"end_date":   "2021-03-08",
+		} {
+			if query.Get(k) != v {
+				t.Errorf("Query parameter %s: %s, want %s", k, query.Get(k), v)
+			}
+		}
+
+		fmt.Fprint(w, `
+			[
+				{ "date": "2021-03-01", "value": 3 },
+				{ "date": "2021-03-02", "value": 6 },
+				{ "date": "2021-03-03", "value": 0 },
+				{ "date": "2021-03-04", "value": 0 },
+				{ "date": "2021-03-05", "value": 0 },
+				{ "date": "2021-03-06", "value": 0 },
+				{ "date": "2021-03-07", "value": 0 },
+				{ "date": "2021-03-08", "value": 4 }
+			]
+		`)
+	})
+
+	want := []DORAMetric{
+		{Date: "2021-03-01", Value: 3},
+		{Date: "2021-03-02", Value: 6},
+		{Date: "2021-03-03", Value: 0},
+		{Date: "2021-03-04", Value: 0},
+		{Date: "2021-03-05", Value: 0},
+		{Date: "2021-03-06", Value: 0},
+		{Date: "2021-03-07", Value: 0},
+		{Date: "2021-03-08", Value: 4},
+	}
+
+	startDate := ISOTime(time.Date(2021, time.March, 1, 0, 0, 0, 0, time.UTC))
+	endDate := ISOTime(time.Date(2021, time.March, 8, 0, 0, 0, 0, time.UTC))
+
+	d, resp, err := client.DORAMetrics.GetGroupDORAMetrics(1, GetDORAMetricsOptions{
+		Metric:    Ptr(DORAMetricDeploymentFrequency),
+		StartDate: &startDate,
+		EndDate:   &endDate,
+	})
+	require.NoError(t, err)
+	require.NotNil(t, resp)
+	require.Equal(t, want, d)
+}