diff --git a/dora_metrics.go b/dora_metrics.go new file mode 100644 index 000000000..a2ad418eb --- /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/dora_metrics_test.go b/dora_metrics_test.go new file mode 100644 index 000000000..4e6ec0952 --- /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) +} diff --git a/gitlab.go b/gitlab.go index e49205019..6df108fd8 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 50e03ed5d..c87c248e5 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" )