Commit 8cb21845 authored by Mayra Cabrera's avatar Mayra Cabrera

Merge branch 'group-level-dora-metrics-rest-api' into 'master'

Group-level DORA metrics API

See merge request gitlab-org/gitlab!56059
parents 29f1f5bd 45fb12bc
...@@ -31,8 +31,8 @@ module Dora ...@@ -31,8 +31,8 @@ module Dora
return error(_('The start date must be ealier than the end date.'), :bad_request) return error(_('The start date must be ealier than the end date.'), :bad_request)
end end
unless project? unless project? || group?
return error(_('Container must be a project.'), :bad_request) return error(_('Container must be a project or a group.'), :bad_request)
end end
unless ::Dora::DailyMetrics::AVAILABLE_INTERVALS.include?(interval) unless ::Dora::DailyMetrics::AVAILABLE_INTERVALS.include?(interval)
...@@ -58,13 +58,32 @@ module Dora ...@@ -58,13 +58,32 @@ module Dora
end end
def environments def environments
Environment.for_project(container).for_tier(environment_tier) Environment.for_project(target_projects).for_tier(environment_tier)
end
def target_projects
if project?
[container]
elsif group?
# The actor definitely has read permission in all subsequent projects of the group by the following reasons:
# - DORA metrics can be read by reporter (or above) at project-level.
# - With `read_dora4_analytics` permission check, we make sure that the
# user is at-least reporter role at group-level.
# - In the subsequent projects, the assigned role at the group-level
# can't be lowered. For example, if the user is reporter at group-level,
# the user can be developer in subsequent projects, but can't be guest.
container.all_projects
end
end end
def project? def project?
container.is_a?(Project) container.is_a?(Project)
end end
def group?
container.is_a?(Group)
end
def start_date def start_date
params[:start_date] || 3.months.ago.to_date params[:start_date] || 3.months.ago.to_date
end end
......
...@@ -5,6 +5,30 @@ module API ...@@ -5,6 +5,30 @@ module API
class Metrics < ::API::Base class Metrics < ::API::Base
feature_category :continuous_delivery feature_category :continuous_delivery
helpers do
params :dora_metrics_params do
requires :metric, type: String, desc: 'The metric type.'
optional :start_date, type: Date, desc: 'Date range to start from.'
optional :end_date, type: Date, desc: 'Date range to end at.'
optional :interval, type: String, desc: "The bucketing interval."
optional :environment_tier, type: String, desc: "The tier of the environment."
end
def fetch!(container)
not_found! unless ::Feature.enabled?(:dora_daily_metrics, container, default_enabled: :yaml)
result = ::Dora::AggregateMetricsService
.new(container: container, current_user: current_user, params: declared_params(include_missing: false))
.execute
if result[:status] == :success
present result[:data]
else
render_api_error!(result[:message], result[:http_status])
end
end
end
params do params do
requires :id, type: String, desc: 'The ID of the project' requires :id, type: String, desc: 'The ID of the project'
end end
...@@ -12,11 +36,7 @@ module API ...@@ -12,11 +36,7 @@ module API
namespace ':id/dora/metrics' do namespace ':id/dora/metrics' do
desc 'Fetch the project-level DORA metrics' desc 'Fetch the project-level DORA metrics'
params do params do
requires :metric, type: String, desc: 'The metric type.' use :dora_metrics_params
optional :start_date, type: Date, desc: 'Date range to start from.'
optional :end_date, type: Date, desc: 'Date range to end at.'
optional :interval, type: String, desc: "The bucketing interval."
optional :environment_tier, type: String, desc: "The tier of the environment."
end end
get do get do
fetch!(user_project) fetch!(user_project)
...@@ -24,18 +44,17 @@ module API ...@@ -24,18 +44,17 @@ module API
end end
end end
helpers do params do
def fetch!(container) requires :id, type: String, desc: 'The ID of the group'
not_found! unless ::Feature.enabled?(:dora_daily_metrics, container, default_enabled: :yaml) end
resource :groups, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
result = ::Dora::AggregateMetricsService namespace ':id/dora/metrics' do
.new(container: container, current_user: current_user, params: declared_params(include_missing: false)) desc 'Fetch the group-level DORA metrics'
.execute params do
use :dora_metrics_params
if result[:status] == :success end
present result[:data] get do
else fetch!(user_group)
render_api_error!(result[:message], result[:http_status])
end end
end end
end end
......
...@@ -62,4 +62,65 @@ RSpec.describe API::Dora::Metrics do ...@@ -62,4 +62,65 @@ RSpec.describe API::Dora::Metrics do
end end
end end
end end
describe 'GET /groups/:id/dora/metrics' do
subject { get api(url, user), params: params }
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, group: group) }
let_it_be(:production) { create(:environment, :production, project: project) }
let_it_be(:maintainer) { create(:user) }
let_it_be(:guest) { create(:user) }
let(:url) { "/groups/#{group.id}/dora/metrics" }
let(:params) { { metric: :deployment_frequency } }
let(:user) { maintainer }
around do |example|
freeze_time do
example.run
end
end
before_all do
group.add_maintainer(maintainer)
group.add_guest(guest)
create(:dora_daily_metrics, deployment_frequency: 1, environment: production, date: 1.day.ago.to_date)
create(:dora_daily_metrics, deployment_frequency: 2, environment: production, date: Time.current.to_date)
end
before do
stub_licensed_features(dora4_analytics: true)
end
it 'returns data' do
subject
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to eq([{ 1.day.ago.to_date.to_s => 1 },
{ Time.current.to_date.to_s => 2 }])
end
context 'when user is guest' do
let(:user) { guest }
it 'returns authorization error' do
subject
expect(response).to have_gitlab_http_status(:unauthorized)
expect(json_response['message']).to eq('You do not have permission to access dora metrics.')
end
end
context 'when dora_daily_metrics feature flag is disabled' do
before do
stub_feature_flags(dora_daily_metrics: false)
end
it 'returns not found' do
subject
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
end end
...@@ -22,6 +22,71 @@ RSpec.describe Dora::AggregateMetricsService do ...@@ -22,6 +22,71 @@ RSpec.describe Dora::AggregateMetricsService do
end end
end end
shared_examples_for 'correct validations' do
context 'when data range is too wide' do
let(:extra_params) { { start_date: 1.year.ago.to_date } }
it_behaves_like 'request failure' do
let(:message) { "Date range must be shorter than #{described_class::MAX_RANGE} days." }
let(:http_status) { :bad_request }
end
end
context 'when start date is later than end date' do
let(:extra_params) { { end_date: 1.year.ago.to_date } }
it_behaves_like 'request failure' do
let(:message) { 'The start date must be ealier than the end date.' }
let(:http_status) { :bad_request }
end
end
context 'when interval is invalid' do
let(:extra_params) { { interval: 'unknown' } }
it_behaves_like 'request failure' do
let(:message) { "The interval must be one of #{::Dora::DailyMetrics::AVAILABLE_INTERVALS.join(',')}." }
let(:http_status) { :bad_request }
end
end
context 'when metric is invalid' do
let(:extra_params) { { metric: 'unknown' } }
it_behaves_like 'request failure' do
let(:message) { "The metric must be one of #{::Dora::DailyMetrics::AVAILABLE_METRICS.join(',')}." }
let(:http_status) { :bad_request }
end
end
context 'when params is empty' do
let(:params) { {} }
it_behaves_like 'request failure' do
let(:message) { "The metric must be one of #{::Dora::DailyMetrics::AVAILABLE_METRICS.join(',')}." }
let(:http_status) { :bad_request }
end
end
context 'when environment tier is invalid' do
let(:extra_params) { { environment_tier: 'unknown' } }
it_behaves_like 'request failure' do
let(:message) { "The environment tier must be one of #{Environment.tiers.keys.join(',')}." }
let(:http_status) { :bad_request }
end
end
context 'when guest user' do
let(:user) { guest }
it_behaves_like 'request failure' do
let(:message) { 'You do not have permission to access dora metrics.' }
let(:http_status) { :unauthorized }
end
end
end
context 'when container is project' do context 'when container is project' do
let_it_be(:project) { create(:project) } let_it_be(:project) { create(:project) }
let_it_be(:production) { create(:environment, :production, project: project) } let_it_be(:production) { create(:environment, :production, project: project) }
...@@ -45,6 +110,8 @@ RSpec.describe Dora::AggregateMetricsService do ...@@ -45,6 +110,8 @@ RSpec.describe Dora::AggregateMetricsService do
stub_licensed_features(dora4_analytics: true) stub_licensed_features(dora4_analytics: true)
end end
it_behaves_like 'correct validations'
it 'returns the aggregated data' do it 'returns the aggregated data' do
expect(subject[:status]).to eq(:success) expect(subject[:status]).to eq(:success)
expect(subject[:data]).to eq([{ Time.current.to_date.to_s => 2 }]) expect(subject[:data]).to eq([{ Time.current.to_date.to_s => 2 }])
...@@ -76,81 +143,66 @@ RSpec.describe Dora::AggregateMetricsService do ...@@ -76,81 +143,66 @@ RSpec.describe Dora::AggregateMetricsService do
expect(subject[:data]).to eq([{ Time.current.to_date.to_s => 1 }]) expect(subject[:data]).to eq([{ Time.current.to_date.to_s => 1 }])
end end
end end
end
context 'when data range is too wide' do context 'when container is a group' do
let(:extra_params) { { start_date: 1.year.ago.to_date } } let_it_be(:group) { create(:group) }
let_it_be(:project_1) { create(:project, group: group) }
it_behaves_like 'request failure' do let_it_be(:project_2) { create(:project, group: group) }
let(:message) { "Date range must be shorter than #{described_class::MAX_RANGE} days." } let_it_be(:production_1) { create(:environment, :production, project: project_1) }
let(:http_status) { :bad_request } let_it_be(:production_2) { create(:environment, :production, project: project_2) }
end let_it_be(:maintainer) { create(:user) }
end let_it_be(:guest) { create(:user) }
let(:container) { group }
context 'when start date is later than end date' do let(:user) { maintainer }
let(:extra_params) { { end_date: 1.year.ago.to_date } } let(:params) { { metric: 'deployment_frequency' }.merge(extra_params) }
let(:extra_params) { {} }
it_behaves_like 'request failure' do
let(:message) { 'The start date must be ealier than the end date.' }
let(:http_status) { :bad_request }
end
end
context 'when interval is invalid' do before_all do
let(:extra_params) { { interval: 'unknown' } } group.add_maintainer(maintainer)
group.add_guest(guest)
it_behaves_like 'request failure' do create(:dora_daily_metrics, deployment_frequency: 2, environment: production_1)
let(:message) { "The interval must be one of #{::Dora::DailyMetrics::AVAILABLE_INTERVALS.join(',')}." } create(:dora_daily_metrics, deployment_frequency: 1, environment: production_2)
let(:http_status) { :bad_request }
end
end end
context 'when metric is invalid' do before do
let(:extra_params) { { metric: 'unknown' } } stub_licensed_features(dora4_analytics: true)
it_behaves_like 'request failure' do
let(:message) { "The metric must be one of #{::Dora::DailyMetrics::AVAILABLE_METRICS.join(',')}." }
let(:http_status) { :bad_request }
end
end end
context 'when params is empty' do it_behaves_like 'correct validations'
let(:params) { {} }
it_behaves_like 'request failure' do it 'returns the aggregated data' do
let(:message) { "The metric must be one of #{::Dora::DailyMetrics::AVAILABLE_METRICS.join(',')}." } expect(subject[:status]).to eq(:success)
let(:http_status) { :bad_request } expect(subject[:data]).to eq([{ Time.current.to_date.to_s => 3 }])
end
end end
context 'when environment tier is invalid' do context 'when interval is monthly' do
let(:extra_params) { { environment_tier: 'unknown' } } let(:extra_params) { { interval: Dora::DailyMetrics::INTERVAL_MONTHLY } }
it_behaves_like 'request failure' do it 'returns the aggregated data' do
let(:message) { "The environment tier must be one of #{Environment.tiers.keys.join(',')}." } expect(subject[:status]).to eq(:success)
let(:http_status) { :bad_request } expect(subject[:data]).to eq([{ Time.current.beginning_of_month.to_date.to_s => 3 }])
end end
end end
context 'when guest user' do context 'when interval is all' do
let(:user) { guest } let(:extra_params) { { interval: Dora::DailyMetrics::INTERVAL_ALL } }
it_behaves_like 'request failure' do it 'returns the aggregated data' do
let(:message) { 'You do not have permission to access dora metrics.' } expect(subject[:status]).to eq(:success)
let(:http_status) { :unauthorized } expect(subject[:data]).to eq(3)
end end
end end
end end
context 'when container is group' do context 'when container is nil' do
let_it_be(:group) { create(:group) } let(:container) { nil }
let_it_be(:maintainer) { create(:user) } let(:user) { nil }
let_it_be(:guest) { create(:user) } let(:params) { {} }
let(:container) { group }
let(:user) { maintainer }
let(:params) { { metric: 'deployment_frequency' } }
it_behaves_like 'request failure' do it_behaves_like 'request failure' do
let(:message) { 'Container must be a project.' } let(:message) { 'Container must be a project or a group.' }
let(:http_status) { :bad_request } let(:http_status) { :bad_request }
end end
end end
......
...@@ -8023,7 +8023,7 @@ msgstr "" ...@@ -8023,7 +8023,7 @@ msgstr ""
msgid "Container does not exist" msgid "Container does not exist"
msgstr "" msgstr ""
msgid "Container must be a project." msgid "Container must be a project or a group."
msgstr "" msgstr ""
msgid "Container registry images" msgid "Container registry images"
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment