Skip to content
Snippets Groups Projects
Commit fdfaf2f6 authored by Amy Troschinetz's avatar Amy Troschinetz
Browse files

Resolve "API support for Lead time for MRs to be merged"

- **app/models/merge_request/metrics.rb:**

Fixed a bug in date range calculations.

- **doc/api/dora4_project_analytics.md:**
- **doc/user/project/index.md:**

Adds documentation for the new API.

- **ee/app/services/analytics/merge_requests/lead_time/
    aggregate_service.rb:**

Adds aggregate service for our API to utilize.

- **ee/changelogs/unreleased/dora4-lead-time-rest-api.yml:**

Changelog.

- **ee/lib/api/analytics/project_lead_time.rb:**
- **ee/lib/ee/api/api.rb:**
- **ee/lib/ee/api/entities/analytics/lead_time.rb:**

The new API for Project scope Average Merge Request Lead Time.

- **ee/spec/requests/api/analytics/project_lead_time_spec.rb:**
- **ee/spec/services/analytics/merge_requests/lead_time/
    aggregate_service_spec.rb:**

Spec tests.

- **ee/spec/services/analytics/deployments/frequency/
    aggregate_service_spec.rb:**

Fixes a typo.

- **locale/gitlab.pot:**

Updated locale file.

- **doc/user/project/index.md:**
- **ee/app/helpers/ee/graph_helper.rb:**
- **ee/app/models/license.rb:**
- **ee/app/policies/ee/project_policy.rb:**
- **ee/lib/api/analytics/project_deployment_frequency.rb:**
- **ee/spec/frontend/fixtures/analytics/project_analytics.rb:**
- **ee/spec/helpers/ee/graph_helper_spec.rb:**
- **ee/spec/policies/project_policy_spec.rb:**
- **ee/spec/requests/api/analytics/
    project_deployment_frequency_spec.rb:**

Some changes pulled in from
https://gitlab.com/gitlab-org/gitlab/-/merge_requests/51938.
parent ec62c37e
No related branches found
No related tags found
No related merge requests found
Showing
with 709 additions and 78 deletions
Loading
Loading
@@ -10,7 +10,7 @@ class MergeRequest::Metrics < ApplicationRecord
before_save :ensure_target_project_id
 
scope :merged_after, ->(date) { where(arel_table[:merged_at].gteq(date)) }
scope :merged_before, ->(date) { where(arel_table[:merged_at].lteq(date)) }
scope :merged_before, ->(date) { where(arel_table[:merged_at].lt(date)) }
scope :with_valid_time_to_merge, -> { where(arel_table[:merged_at].gt(arel_table[:created_at])) }
scope :by_target_project, ->(project) { where(target_project_id: project) }
 
Loading
Loading
---
stage: Release
group: Release
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
type: reference, api
---
# DORA4 Analytics Project API **(ULTIMATE ONLY)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/279039) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 13.7.
All methods require reporter authorization.
## List project deployment frequencies
Get a list of all project deployment frequencies, sorted by date:
```plaintext
GET /projects/:id/analytics/deployment_frequency?environment=:environment&from=:from&to=:to&interval=:interval
```
| Attribute | Type | Required | Description |
|--------------|--------|----------|-----------------------|
| `id` | string | yes | The ID of the project |
| Parameter | Type | Required | Description |
|--------------|--------|----------|-----------------------|
| `environment`| string | yes | The name of the environment to filter by |
| `from` | string | yes | Datetime range to start from, inclusive, ISO 8601 format (`YYYY-MM-DDTHH:MM:SSZ`) |
| `to` | string | no | Datetime range to end at, exclusive, ISO 8601 format (`YYYY-MM-DDTHH:MM:SSZ`) |
| `interval` | string | no | The bucketing interval (`all`, `monthly`, `daily`) |
Example request:
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/:id/analytics/deployment_frequency?environment=:environment&from=:from&to=:to&interval=:interval"
```
Example response:
```json
[
{
"from": "2017-01-01",
"to": "2017-01-02",
"value": 106
},
{
"from": "2017-01-02",
"to": "2017-01-03",
"value": 55
}
]
```
## List project merge request lead times
Get a list of all project merge request lead times:
```plaintext
GET /projects/:id/analytics/lead_time?from=:from&to=:to&interval=:interval
```
| Attribute | Type | Required | Description |
|--------------|--------|----------|-----------------------|
| `id` | string | yes | The ID of the project |
| Parameter | Type | Required | Description |
|--------------|--------|----------|-----------------------|
| `from` | string | yes | Datetime range to start from, inclusive, ISO 8601 format (`YYYY-MM-DDTHH:MM:SSZ`) |
| `to` | string | no | Datetime range to end at, exclusive, ISO 8601 format (`YYYY-MM-DDTHH:MM:SSZ`) |
| `interval` | string | no | The bucketing interval (`all`, `monthly`, `daily`) |
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/:id/analytics/lead_time?from=:from&to=:to&interval=:interval"
```
Example response:
Response values are given as the average number of minutes between merge request creation and merge time.
```json
[
{
"from": "2017-01-01",
"to": "2017-01-02",
"value": 24
},
{
"from": "2017-01-02",
"to": "2017-01-03",
"value": 8
}
]
```
---
stage: Release
group: Release
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
type: reference, api
redirect_to: 'dora4_project_analytics.md'
---
 
# Project Analytics API **(ULTIMATE SELF)**
This document was moved to [another location](dora4_project_analytics.md).
 
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/279039) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 13.7.
All methods require reporter authorization.
## List project deployment frequencies
Get a list of all project aliases:
```plaintext
GET /projects/:id/analytics/deployment_frequency?environment=:environment&from=:from&to=:to&interval=:interval
```
| Attribute | Type | Required | Description |
|--------------|--------|----------|-----------------------|
| `id` | string | yes | The ID of the project |
| Parameter | Type | Required | Description |
|--------------|--------|----------|-----------------------|
| `environment`| string | yes | The name of the environment to filter by |
| `from` | string | yes | Datetime range to start from, inclusive, ISO 8601 format (`YYYY-MM-DDTHH:MM:SSZ`) |
| `to` | string | no | Datetime range to end at, exclusive, ISO 8601 format (`YYYY-MM-DDTHH:MM:SSZ`) |
| `interval` | string | no | The bucketing interval (`all`, `monthly`, `daily`) |
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/:id/analytics/deployment_frequency?from=:from&to=:to&interval=:interval"
```
Example response:
```json
[
{
"from": "2017-01-01",
"to": "2017-01-02",
"value": 106
},
{
"from": "2017-01-02",
"to": "2017-01-03",
"value": 55
}
]
```
<!-- This redirect file can be deleted after <2021-04-25>. -->
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->
Loading
Loading
@@ -43,7 +43,7 @@ performance indicators for software development teams:
 
GitLab plans to add support for all the DORA4 metrics at the project and group levels. GitLab added
the first metric, deployment frequency, at the project level for [CI/CD charts](ci_cd_analytics.md#deployment-frequency-charts)
and the [API]( ../../api/project_analytics.md).
and the [API]( ../../api/dora4_project_analytics.md).
 
## Deployment frequency charts **(ULTIMATE)**
 
Loading
Loading
Loading
Loading
@@ -376,15 +376,16 @@ project `https://gitlab.com/gitlab-org/gitlab`), you can clone the repository
with the alias (e.g `git clone git@gitlab.com:gitlab.git` instead of
`git clone git@gitlab.com:gitlab-org/gitlab.git`).
 
## Project activity analytics overview **(ULTIMATE SELF)**
## DORA4 analytics overview **(ULTIMATE ONLY)**
 
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/279039) in GitLab [Ultimate](https://about.gitlab.com/pricing/) 13.7 as a [Beta feature](https://about.gitlab.com/handbook/product/gitlab-the-product/#beta).
 
Project details include the following analytics:
 
- Deployment Frequency
- Merge Request Lead Time
 
For more information, see [Project Analytics API](../../api/project_analytics.md).
For more information, see [DORA4 Project Analytics API](../../api/dora4_project_analytics.md).
 
## Project APIs
 
Loading
Loading
@@ -406,4 +407,4 @@ There are numerous [APIs](../../api/README.md) to use with your projects:
- [Traffic](../../api/project_statistics.md)
- [Variables](../../api/project_level_variables.md)
- [Aliases](../../api/project_aliases.md)
- [Analytics](../../api/project_analytics.md)
- [DORA4 Analytics](../../api/dora4_project_analytics.md)
Loading
Loading
@@ -7,9 +7,9 @@ module GraphHelper
override :should_render_deployment_frequency_charts
def should_render_deployment_frequency_charts
return false unless ::Feature.enabled?(:deployment_frequency_charts, @project, default_enabled: true)
return false unless @project.feature_available?(:project_activity_analytics)
return false unless @project.feature_available?(:dora4_analytics)
 
can?(current_user, :read_project_activity_analytics, @project)
can?(current_user, :read_dora4_analytics, @project)
end
end
end
Loading
Loading
@@ -143,6 +143,7 @@ class License < ApplicationRecord
dast
dependency_scanning
devops_adoption
dora4_analytics
enforce_pat_expiration
enforce_ssh_key_expiration
enterprise_templates
Loading
Loading
@@ -155,7 +156,6 @@ class License < ApplicationRecord
jira_vulnerabilities_integration
license_scanning
personal_access_token_expiration_policy
project_activity_analytics
prometheus_alerts
pseudonymizer
quality_management
Loading
Loading
Loading
Loading
@@ -52,8 +52,8 @@ module ProjectPolicy
end
 
with_scope :subject
condition(:project_activity_analytics_available) do
@subject.feature_available?(:project_activity_analytics)
condition(:dora4_analytics_available) do
@subject.feature_available?(:dora4_analytics)
end
 
condition(:project_merge_request_analytics_available) do
Loading
Loading
@@ -369,8 +369,8 @@ module ProjectPolicy
 
rule { can?(:read_merge_request) & code_review_analytics_enabled }.enable :read_code_review_analytics
 
rule { reporter & project_activity_analytics_available }
.enable :read_project_activity_analytics
rule { reporter & dora4_analytics_available }
.enable :read_dora4_analytics
 
rule { reporter & project_merge_request_analytics_available }
.enable :read_project_merge_request_analytics
Loading
Loading
# frozen_string_literal: true
module Analytics
module MergeRequests
module LeadTime
# This class is to aggregate merge requests data at project-level or group-level
# for calculating the lead time.
class AggregateService < BaseContainerService
include Gitlab::Utils::StrongMemoize
QUARTER_DAYS = 3.months / 1.day
INTERVAL_ALL = 'all'
INTERVAL_MONTHLY = 'monthly'
INTERVAL_DAILY = 'daily'
VALID_INTERVALS = [
INTERVAL_ALL,
INTERVAL_MONTHLY,
INTERVAL_DAILY
].freeze
def execute
if error = validate
return error
end
success(lead_times: lead_times)
end
private
def validate
unless start_date
return error(_('Parameter `from` must be specified'), :bad_request)
end
if start_date > end_date
return error(_('Parameter `to` is before the `from` date'), :bad_request)
end
if days_between > QUARTER_DAYS
return error(_('Date range is greater than %{quarter_days} days') % { quarter_days: QUARTER_DAYS },
:bad_request)
end
unless container.is_a?(Project)
return error(_('Only project level aggregation is supported'))
end
unless VALID_INTERVALS.include?(interval)
return error(_("Parameter `interval` must be one of (\"%{valid_intervals}\")") % { valid_intervals: VALID_INTERVALS.join('", "') }, :bad_request)
end
unless can?(current_user, :read_dora4_analytics, container)
return error(_('You do not have permission to access lead times'), :forbidden)
end
nil
end
def interval
params[:interval] || INTERVAL_ALL
end
def start_date
params[:from]
end
def end_date
params[:to] || DateTime.current
end
def days_between
(end_date - start_date).to_i
end
def lead_times
strong_memoize(:lead_times) do
merge_requests_grouped.map do |grouped_start_date, grouping|
{
value: average_lead_time(grouping),
from: grouped_start_date,
to: merge_requests_grouped_end_date(grouped_start_date)
}
end
end
end
def merge_requests_grouped
strong_memoize(:merge_requests_grouped) do
case interval
when INTERVAL_ALL
{ start_date => merge_requests }
when INTERVAL_MONTHLY
merge_requests.group_by { |d| d.merged_at.beginning_of_month }
when INTERVAL_DAILY
merge_requests.group_by { |d| d.merged_at.to_date }
end
end
end
def average_lead_time(grouping)
size = grouping.count
return 0 if size == 0
total = grouping.inject(0) do |sum, mr|
sum + (mr.merged_at - mr.merge_request.created_at).to_i / 1.minute
end
total / size
end
def merge_requests_grouped_end_date(merge_requests_grouped_start_date)
case interval
when INTERVAL_ALL
end_date
when INTERVAL_MONTHLY
merge_requests_grouped_start_date + 1.month
when INTERVAL_DAILY
merge_requests_grouped_start_date + 1.day
end
end
def merge_requests
strong_memoize(:merge_requests) do
# rubocop: disable CodeReuse/ActiveRecord
::MergeRequest::Metrics.by_target_project(container)
.merged_after(start_date)
.merged_before(end_date)
.order('merged_at')
# rubocop: enable CodeReuse/ActiveRecord
end
end
end
end
end
end
---
title: Adds API support for Project Lead Time
merge_request: 52243
author:
type: added
Loading
Loading
@@ -4,12 +4,11 @@ module API
module Analytics
class ProjectDeploymentFrequency < ::API::Base
include Gitlab::Utils::StrongMemoize
include PaginationParams
 
QUARTER_DAYS = 3.months / 1.day
DEPLOYMENT_FREQUENCY_INTERVAL_ALL = 'all'.freeze
DEPLOYMENT_FREQUENCY_INTERVAL_MONTHLY = 'monthly'.freeze
DEPLOYMENT_FREQUENCY_INTERVAL_DAILY = 'daily'.freeze
DEPLOYMENT_FREQUENCY_INTERVAL_ALL = 'all'
DEPLOYMENT_FREQUENCY_INTERVAL_MONTHLY = 'monthly'
DEPLOYMENT_FREQUENCY_INTERVAL_DAILY = 'daily'
DEPLOYMENT_FREQUENCY_DEFAULT_INTERVAL = DEPLOYMENT_FREQUENCY_INTERVAL_ALL
VALID_INTERVALS = [
DEPLOYMENT_FREQUENCY_INTERVAL_ALL,
Loading
Loading
@@ -110,7 +109,7 @@ def deployment_frequencies
get 'deployment_frequency' do
bad_request!("Parameter `to` is before the `from` date") if start_date > end_date
bad_request!("Date range is greater than #{QUARTER_DAYS} days") if days_between > QUARTER_DAYS
authorize! :read_project_activity_analytics, user_project
authorize! :read_dora4_analytics, user_project
present deployment_frequencies, with: EE::API::Entities::Analytics::DeploymentFrequency
end
end
Loading
Loading
# frozen_string_literal: true
module API
module Analytics
class ProjectLeadTime < ::API::Base
feature_category :continuous_delivery
before do
authenticate!
end
params do
requires :id, type: String, desc: 'The ID of the project'
end
resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
namespace ':id/analytics' do
desc 'List merge request lead times for the project'
params do
requires :from, type: DateTime, desc: 'Datetime to start from, inclusive'
optional :to, type: DateTime, desc: 'Datetime to end at, exclusive'
optional :interval, type: String, desc: 'Interval to roll-up data by'
end
get 'lead_time' do
result = ::Analytics::MergeRequests::LeadTime::AggregateService
.new(container: user_project,
current_user: current_user,
params: declared_params(include_missing: false))
.execute
unless result[:status] == :success
render_api_error!(result[:message], result[:http_status])
end
present result[:lead_times], with: EE::API::Entities::Analytics::LeadTime
end
end
end
end
end
end
Loading
Loading
@@ -44,6 +44,7 @@ module API
mount ::API::Analytics::CodeReviewAnalytics
mount ::API::Analytics::GroupActivityAnalytics
mount ::API::Analytics::ProjectDeploymentFrequency
mount ::API::Analytics::ProjectLeadTime
mount ::API::ProtectedEnvironments
mount ::API::ResourceWeightEvents
mount ::API::ResourceIterationEvents
Loading
Loading
# frozen_string_literal: true
module EE
module API
module Entities
module Analytics
class LeadTime < Grape::Entity
format_with(:iso8601_date) { |datetime| datetime.to_date.iso8601 }
expose :value
expose :from, format_with: :iso8601_date
expose :to, format_with: :iso8601_date
end
end
end
end
end
Loading
Loading
@@ -30,7 +30,7 @@
end
 
before do
stub_licensed_features(project_activity_analytics: true)
stub_licensed_features(dora4_analytics: true)
project.add_reporter(reporter)
sign_in(reporter)
end
Loading
Loading
Loading
Loading
@@ -12,11 +12,11 @@
let(:is_user_authorized) { true }
 
before do
stub_licensed_features(project_activity_analytics: is_feature_licensed)
stub_licensed_features(dora4_analytics: is_feature_licensed)
stub_feature_flags(deployment_frequency_charts: is_flag_enabled)
self.instance_variable_set(:@current_user, current_user)
self.instance_variable_set(:@project, project)
allow(self).to receive(:can?).with(current_user, :read_project_activity_analytics, project).and_return(is_user_authorized)
allow(self).to receive(:can?).with(current_user, :read_dora4_analytics, project).and_return(is_user_authorized)
end
 
shared_examples 'returns true' do
Loading
Loading
Loading
Loading
@@ -1388,24 +1388,24 @@
it { is_expected.to be_disallowed(:read_group_timelogs) }
end
 
context 'when project activity analytics is available' do
context 'when dora4 analytics is available' do
let(:current_user) { developer }
 
before do
stub_licensed_features(project_activity_analytics: true)
stub_licensed_features(dora4_analytics: true)
end
 
it { is_expected.to be_allowed(:read_project_activity_analytics) }
it { is_expected.to be_allowed(:read_dora4_analytics) }
end
 
context 'when project activity analytics is not available' do
context 'when dora4 analytics is not available' do
let(:current_user) { developer }
 
before do
stub_licensed_features(project_activity_analytics: false)
stub_licensed_features(dora4_analytics: false)
end
 
it { is_expected.not_to be_allowed(:read_project_activity_analytics) }
it { is_expected.not_to be_allowed(:read_dora4_analytics) }
end
 
describe ':read_code_review_analytics' do
Loading
Loading
Loading
Loading
@@ -42,7 +42,7 @@ def make_deployment(finished_at, env)
let_it_be(:deployment_2020_04_04) { make_deployment(DateTime.new(2020, 4, 4), prod) }
let_it_be(:deployment_2020_04_05) { make_deployment(DateTime.new(2020, 4, 5), prod) }
 
let(:project_activity_analytics_enabled) { true }
let(:dora4_analytics_enabled) { true }
let(:current_user) { reporter }
let(:params) { { from: Time.now, to: Time.now, interval: "all", environment: prod.name } }
let(:path) { api("/projects/#{project.id}/analytics/deployment_frequency", current_user) }
Loading
Loading
@@ -50,7 +50,7 @@ def make_deployment(finished_at, env)
let(:request_time) { nil }
 
before do
stub_licensed_features(project_activity_analytics: project_activity_analytics_enabled)
stub_licensed_features(dora4_analytics: dora4_analytics_enabled)
 
if request_time
travel_to(request_time) { request }
Loading
Loading
@@ -87,7 +87,7 @@ def make_deployment(finished_at, env)
end
end
 
context 'with params: from 2020/04/01 to request time' do
context 'with params: from 2020/04/02 to request time' do
let(:request_time) { DateTime.new(2020, 4, 4) }
let(:params) { { environment: prod.name, from: DateTime.new(2020, 4, 2) } }
 
Loading
Loading
@@ -193,7 +193,7 @@ def make_deployment(finished_at, env)
end
 
context 'when feature is not available in plan' do
let(:project_activity_analytics_enabled) { false }
let(:dora4_analytics_enabled) { false }
 
context 'when user has access to the project' do
it 'returns `forbidden`' do
Loading
Loading
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe API::Analytics::ProjectLeadTime do
let_it_be(:group) { create(:group, :private) }
let_it_be(:project) { create(:project, :repository, namespace: group) }
let_it_be(:anonymous_user) { create(:user) }
let_it_be(:reporter) { create(:user).tap { |u| project.add_reporter(u) } }
def make_mr(proj, created_at, merged_at)
updated_at = merged_at || created_at
create(:merge_request,
:unique_branches,
state: merged_at ? 'merged' : 'opened',
created_at: created_at,
updated_at: updated_at,
source_project: proj,
target_project: proj).metrics.tap do |metrics|
# MR metrics are created after the fact by batch processing in a task, simulate that
# by setting our metrics' created_at time a bit after the MR's last update time.
metrics_created_at = updated_at + 6.hours
metrics.update!(
created_at: metrics_created_at,
updated_at: metrics_created_at,
merged_at: merged_at
)
end
end
let_it_be(:mr_2020_01_01) { make_mr(project, DateTime.new(2020, 1, 1), DateTime.new(2020, 1, 1)) }
let_it_be(:mr_2020_01_02) { make_mr(project, DateTime.new(2020, 1, 1), DateTime.new(2020, 1, 2)) }
let_it_be(:mr_2020_01_03) { make_mr(project, DateTime.new(2020, 1, 3), nil) }
let_it_be(:mr_2020_01_04) { make_mr(project, DateTime.new(2020, 1, 1), DateTime.new(2020, 1, 4)) }
let_it_be(:mr_2020_01_05) { make_mr(project, DateTime.new(2020, 1, 1), DateTime.new(2020, 1, 5)) }
let_it_be(:mr_2020_02_01) { make_mr(project, DateTime.new(2020, 2, 1), DateTime.new(2020, 2, 1)) }
let_it_be(:mr_2020_02_02) { make_mr(project, DateTime.new(2020, 2, 1), DateTime.new(2020, 2, 2)) }
let_it_be(:mr_2020_02_03) { make_mr(project, DateTime.new(2020, 2, 3), nil) }
let_it_be(:mr_2020_02_04) { make_mr(project, DateTime.new(2020, 2, 1), DateTime.new(2020, 2, 4)) }
let_it_be(:mr_2020_02_05) { make_mr(project, DateTime.new(2020, 2, 1), DateTime.new(2020, 2, 5)) }
let_it_be(:mr_2020_03_01) { make_mr(project, DateTime.new(2020, 3, 1), DateTime.new(2020, 3, 1)) }
let_it_be(:mr_2020_03_02) { make_mr(project, DateTime.new(2020, 3, 1), DateTime.new(2020, 3, 2)) }
let_it_be(:mr_2020_03_03) { make_mr(project, DateTime.new(2020, 3, 3), nil) }
let_it_be(:mr_2020_03_04) { make_mr(project, DateTime.new(2020, 3, 1), DateTime.new(2020, 3, 4)) }
let_it_be(:mr_2020_03_05) { make_mr(project, DateTime.new(2020, 3, 1), DateTime.new(2020, 3, 5)) }
let_it_be(:mr_2020_04_01) { make_mr(project, DateTime.new(2020, 4, 1), DateTime.new(2020, 4, 1)) }
let_it_be(:mr_2020_04_02) { make_mr(project, DateTime.new(2020, 4, 1), DateTime.new(2020, 4, 2)) }
let_it_be(:mr_2020_04_03) { make_mr(project, DateTime.new(2020, 4, 3), nil) }
let_it_be(:mr_2020_04_04) { make_mr(project, DateTime.new(2020, 4, 1), DateTime.new(2020, 4, 4)) }
let_it_be(:mr_2020_04_05) { make_mr(project, DateTime.new(2020, 4, 1), DateTime.new(2020, 4, 5)) }
let(:dora4_analytics_enabled) { true }
let(:current_user) { reporter }
let(:params) { { from: Time.now, to: Time.now, interval: "all" } }
let(:path) { api("/projects/#{project.id}/analytics/lead_time", current_user) }
let(:request) { get path, params: params }
let(:request_time) { nil }
before do
stub_licensed_features(dora4_analytics: dora4_analytics_enabled)
if request_time
travel_to(request_time) { request }
else
request
end
end
context 'when user has access to the project' do
it 'returns `ok`' do
expect(response).to have_gitlab_http_status(:ok)
end
end
context 'with params: from 2017 to 2019' do
let(:params) { { from: DateTime.new(2017), to: DateTime.new(2019) } }
it 'returns `bad_request` with expected message' do
expect(response.parsed_body).to eq({
"message" => "Date range is greater than 91 days"
})
end
end
context 'with params: from 2019 to 2017' do
let(:params) do
{ from: DateTime.new(2019), to: DateTime.new(2017) }
end
it 'returns `bad_request` with expected message' do
expect(response.parsed_body).to eq({
"message" => "Parameter `to` is before the `from` date"
})
end
end
context 'with params: from 2020/04/02 to request time' do
let(:request_time) { DateTime.new(2020, 4, 4) }
let(:params) { { from: DateTime.new(2020, 4, 2) } }
it 'returns the expected lead times' do
expect(response.parsed_body).to eq([{
"from" => "2020-04-02",
"to" => "2020-04-04",
"value" => 1440
}])
end
end
context 'with params: from 2020/02/01 to 2020/04/01 by all' do
let(:params) do
{
from: DateTime.new(2020, 2, 1),
to: DateTime.new(2020, 4, 1),
interval: "all"
}
end
it 'returns the expected lead times' do
expect(response.parsed_body).to eq([{
"from" => "2020-02-01",
"to" => "2020-04-01",
"value" => 2880
}])
end
end
context 'with params: from 2020/02/01 to 2020/04/01 by month' do
let(:params) do
{
from: DateTime.new(2020, 2, 1),
to: DateTime.new(2020, 4, 1),
interval: "monthly"
}
end
it 'returns the expected lead times' do
expect(response.parsed_body).to eq([
{ "from" => "2020-02-01", "to" => "2020-03-01", "value" => 2880 },
{ "from" => "2020-03-01", "to" => "2020-04-01", "value" => 2880 }
])
end
end
context 'with params: from 2020/02/01 to 2020/04/01 by day' do
let(:params) do
{
from: DateTime.new(2020, 2, 1),
to: DateTime.new(2020, 4, 1),
interval: "daily"
}
end
it 'returns the expected lead times' do
expect(response.parsed_body).to eq([
{ "from" => "2020-02-01", "to" => "2020-02-02", "value" => 0 },
{ "from" => "2020-02-02", "to" => "2020-02-03", "value" => 1440 },
{ "from" => "2020-02-04", "to" => "2020-02-05", "value" => 4320 },
{ "from" => "2020-02-05", "to" => "2020-02-06", "value" => 5760 },
{ "from" => "2020-03-01", "to" => "2020-03-02", "value" => 0 },
{ "from" => "2020-03-02", "to" => "2020-03-03", "value" => 1440 },
{ "from" => "2020-03-04", "to" => "2020-03-05", "value" => 4320 },
{ "from" => "2020-03-05", "to" => "2020-03-06", "value" => 5760 }
])
end
end
context 'with params: invalid interval' do
let(:params) do
{
from: DateTime.new(2020, 1),
to: DateTime.new(2020, 2),
interval: "invalid"
}
end
it 'returns `bad_request`' do
expect(response).to have_gitlab_http_status(:bad_request)
end
end
context 'with params: missing from' do
let(:params) { { to: DateTime.new(2019), interval: "all" } }
it 'returns `bad_request`' do
expect(response).to have_gitlab_http_status(:bad_request)
end
end
context 'when user does not have access to the project' do
let(:current_user) { anonymous_user }
it 'returns `not_found`' do
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'when feature is not available in plan' do
let(:dora4_analytics_enabled) { false }
context 'when user has access to the project' do
it 'returns `forbidden`' do
expect(response).to have_gitlab_http_status(:forbidden)
end
end
context 'when user does not have access to the project' do
let(:current_user) { anonymous_user }
it 'returns `not_found`' do
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Analytics::MergeRequests::LeadTime::AggregateService do
let_it_be(:group) { create(:group) }
let_it_be(:project, refind: true) { create(:project, :repository, group: group) }
let_it_be(:developer) { create(:user) }
let_it_be(:guest) { create(:user) }
let(:container) { project }
let(:actor) { developer }
let(:service) { described_class.new(container: container, current_user: actor, params: params) }
let(:request_time) { DateTime.new(2020, 5, 1) }
let(:params) { { from: DateTime.new(2020, 4, 1), interval: "all" } }
def make_mr(proj, created_at, merged_at)
updated_at = merged_at || created_at
create(:merge_request,
:unique_branches,
state: merged_at ? 'merged' : 'opened',
created_at: created_at,
updated_at: updated_at,
source_project: proj,
target_project: proj).metrics.tap do |metrics|
# NOTE: MR::Metrics are created almost immediately after the MR itself,
# however the merged_at is only updated later via a background task.
metrics.update!(
created_at: created_at,
updated_at: merged_at || created_at,
merged_at: merged_at
)
end
end
before_all do
group.add_developer(developer)
group.add_guest(guest)
end
before do
stub_licensed_features(dora4_analytics: true)
end
around do |example|
travel_to(request_time) { example.run }
end
describe '#execute' do
subject { service.execute }
before do
make_mr(project, DateTime.new(2020, 4, 1), DateTime.new(2020, 4, 1))
make_mr(project, DateTime.new(2020, 4, 1), DateTime.new(2020, 4, 2))
make_mr(project, DateTime.new(2020, 4, 3), nil)
make_mr(project, DateTime.new(2020, 4, 1), DateTime.new(2020, 4, 4))
make_mr(project, DateTime.new(2020, 4, 1), DateTime.new(2020, 4, 5))
end
shared_examples_for 'validation error' do
it 'returns an error with message' do
result = subject
expect(result[:status]).to eq(:error)
expect(result[:message]).to eq(message)
end
end
it 'returns lead times' do
result = subject
expect(result[:status]).to eq(:success)
expect(result[:lead_times]).to eq(
[
{
from: params[:from],
to: request_time,
value: 2880
}
]
)
end
context 'when date range is specified' do
let(:params) { { from: DateTime.new(2020, 4, 1), to: DateTime.new(2020, 4, 4) } }
it 'returns lead times' do
result = subject
expect(result[:status]).to eq(:success)
expect(result[:lead_times]).to eq(
[
{
from: params[:from],
to: params[:to],
value: 720
}
]
)
end
end
context 'when the container is group' do
let(:container) { create(:group) }
it_behaves_like 'validation error' do
let(:message) { 'Only project level aggregation is supported' }
end
end
context 'when paramer is empty' do
let(:params) { {} }
it_behaves_like 'validation error' do
let(:message) { 'Parameter `from` must be specified' }
end
end
context 'when to is eariler than from' do
let(:params) { { from: 3.days.ago.to_datetime, to: 4.days.ago.to_datetime } }
it_behaves_like 'validation error' do
let(:message) { 'Parameter `to` is before the `from` date' }
end
end
context 'when the date range is too broad' do
let(:params) { { from: 1.year.ago.to_datetime } }
it_behaves_like 'validation error' do
let(:message) { 'Date range is greater than 91 days' }
end
end
context 'when the interval is not supported' do
let(:params) { { from: 3.days.ago.to_datetime, interval: 'unknown' } }
it_behaves_like 'validation error' do
let(:message) { 'Parameter `interval` must be one of ("all", "monthly", "daily")' }
end
end
context 'when the actor does not have permission to read DORA4 metrics' do
let(:actor) { guest }
it_behaves_like 'validation error' do
let(:message) { 'You do not have permission to access lead times' }
end
end
context 'when license is insufficient' do
before do
stub_licensed_features(dora4_analytics: false)
end
it_behaves_like 'validation error' do
let(:message) { 'You do not have permission to access lead times' }
end
end
end
end
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment