Commit 123a994d authored by Shinya Maeda's avatar Shinya Maeda

Merge branch 'nfriend-group-level-graphql-dora-metrics' into 'master'

Add group-level DORA metrics to GraphQL

See merge request gitlab-org/gitlab!65279
parents 59b7b40c b23f8149
...@@ -9237,6 +9237,7 @@ four standard [pagination arguments](#connection-pagination-arguments): ...@@ -9237,6 +9237,7 @@ four standard [pagination arguments](#connection-pagination-arguments):
| <a id="groupcustomemoji"></a>`customEmoji` | [`CustomEmojiConnection`](#customemojiconnection) | Custom emoji within this namespace. Available only when feature flag `custom_emoji` is enabled. (see [Connections](#connections)) | | <a id="groupcustomemoji"></a>`customEmoji` | [`CustomEmojiConnection`](#customemojiconnection) | Custom emoji within this namespace. Available only when feature flag `custom_emoji` is enabled. (see [Connections](#connections)) |
| <a id="groupdescription"></a>`description` | [`String`](#string) | Description of the namespace. | | <a id="groupdescription"></a>`description` | [`String`](#string) | Description of the namespace. |
| <a id="groupdescriptionhtml"></a>`descriptionHtml` | [`String`](#string) | The GitLab Flavored Markdown rendering of `description`. | | <a id="groupdescriptionhtml"></a>`descriptionHtml` | [`String`](#string) | The GitLab Flavored Markdown rendering of `description`. |
| <a id="groupdora"></a>`dora` | [`Dora`](#dora) | The group's DORA metrics. |
| <a id="groupemailsdisabled"></a>`emailsDisabled` | [`Boolean`](#boolean) | Indicates if a group has email notifications disabled. | | <a id="groupemailsdisabled"></a>`emailsDisabled` | [`Boolean`](#boolean) | Indicates if a group has email notifications disabled. |
| <a id="groupepicboards"></a>`epicBoards` | [`EpicBoardConnection`](#epicboardconnection) | Find epic boards. (see [Connections](#connections)) | | <a id="groupepicboards"></a>`epicBoards` | [`EpicBoardConnection`](#epicboardconnection) | Find epic boards. (see [Connections](#connections)) |
| <a id="groupepicsenabled"></a>`epicsEnabled` | [`Boolean`](#boolean) | Indicates if Epics are enabled for namespace. | | <a id="groupepicsenabled"></a>`epicsEnabled` | [`Boolean`](#boolean) | Indicates if Epics are enabled for namespace. |
......
...@@ -86,6 +86,12 @@ module EE ...@@ -86,6 +86,12 @@ module EE
field :billable_members_count, ::GraphQL::INT_TYPE, field :billable_members_count, ::GraphQL::INT_TYPE,
null: true, null: true,
description: 'The number of billable users in the group.' description: 'The number of billable users in the group.'
field :dora,
::Types::DoraType,
null: true,
method: :itself,
description: "The group's DORA metrics."
end end
end end
end end
......
...@@ -74,4 +74,10 @@ RSpec.describe GitlabSchema.types['Group'] do ...@@ -74,4 +74,10 @@ RSpec.describe GitlabSchema.types['Group'] do
expect(vulnerabilities.first['severity']).to eq('CRITICAL') expect(vulnerabilities.first['severity']).to eq('CRITICAL')
end end
end end
describe 'dora field' do
subject { described_class.fields['dora'] }
it { is_expected.to have_graphql_type(Types::DoraType) }
end
end end
...@@ -7,7 +7,8 @@ RSpec.describe Resolvers::DoraMetricsResolver do ...@@ -7,7 +7,8 @@ RSpec.describe Resolvers::DoraMetricsResolver do
let_it_be(:guest) { create(:user) } let_it_be(:guest) { create(:user) }
let_it_be(:reporter) { create(:user) } let_it_be(:reporter) { create(:user) }
let_it_be(:project) { create(:project) } let_it_be_with_refind(:group) { create(:group) }
let_it_be_with_refind(:project) { create(:project, group: group) }
let_it_be(:production) { create(:environment, :production, project: project) } let_it_be(:production) { create(:environment, :production, project: project) }
let_it_be(:staging) { create(:environment, :staging, project: project) } let_it_be(:staging) { create(:environment, :staging, project: project) }
...@@ -21,8 +22,8 @@ RSpec.describe Resolvers::DoraMetricsResolver do ...@@ -21,8 +22,8 @@ RSpec.describe Resolvers::DoraMetricsResolver do
end end
before_all do before_all do
project.add_guest(guest) group.add_guest(guest)
project.add_reporter(reporter) group.add_reporter(reporter)
create(:dora_daily_metrics, deployment_frequency: 20, lead_time_for_changes_in_seconds: nil, environment: production, date: '2020-01-01') create(:dora_daily_metrics, deployment_frequency: 20, lead_time_for_changes_in_seconds: nil, environment: production, date: '2020-01-01')
create(:dora_daily_metrics, deployment_frequency: 19, lead_time_for_changes_in_seconds: nil, environment: production, date: '2021-01-01') create(:dora_daily_metrics, deployment_frequency: 19, lead_time_for_changes_in_seconds: nil, environment: production, date: '2021-01-01')
...@@ -44,176 +45,190 @@ RSpec.describe Resolvers::DoraMetricsResolver do ...@@ -44,176 +45,190 @@ RSpec.describe Resolvers::DoraMetricsResolver do
stub_licensed_features(dora4_analytics: true) stub_licensed_features(dora4_analytics: true)
end end
describe '#resolve' do shared_examples 'dora metrics' do
context 'when the current users does not have access to query DORA metrics' do describe '#resolve' do
let(:current_user) { guest } context 'when the current users does not have access to query DORA metrics' do
let(:current_user) { guest }
it 'returns no metrics' do it 'returns no metrics' do
expect(resolve_metrics).to be_nil expect(resolve_metrics).to be_nil
end
end end
end
context 'when DORA metrics are not licensed' do context 'when DORA metrics are not licensed' do
before do before do
stub_licensed_features(dora4_analytics: false) stub_licensed_features(dora4_analytics: false)
end
it 'returns no metrics' do
expect(resolve_metrics).to be_nil
end
end end
it 'returns no metrics' do context 'with metric: "deployment_frequency"' do
expect(resolve_metrics).to be_nil let(:args) { { metric: 'deployment_frequency' } }
it 'returns metrics from production for the last 3 months from the production environment, grouped by day' do
expect(resolve_metrics).to eq([
{ 'date' => '2021-03-01', 'value' => 18 },
{ 'date' => '2021-04-01', 'value' => 17 },
{ 'date' => '2021-04-02', 'value' => 16 },
{ 'date' => '2021-04-03', 'value' => 15 },
{ 'date' => '2021-04-04', 'value' => 14 },
{ 'date' => '2021-04-05', 'value' => 13 },
{ 'date' => '2021-04-06', 'value' => 12 },
{ 'date' => '2021-04-07', 'value' => nil }
])
end
end end
end
end
context 'with metric: "deployment_frequency"' do context 'with interval: "daily"' do
let(:args) { { metric: 'deployment_frequency' } } let(:args) { { metric: 'deployment_frequency', interval: 'daily' } }
it 'returns metrics from production for the last 3 months from the production environment, grouped by day' do it 'returns the metrics grouped by day (the default)' do
expect(resolve_metrics).to eq([ expect(resolve_metrics).to eq([
{ 'date' => '2021-03-01', 'value' => 18 }, { 'date' => '2021-03-01', 'value' => 18 },
{ 'date' => '2021-04-01', 'value' => 17 }, { 'date' => '2021-04-01', 'value' => 17 },
{ 'date' => '2021-04-02', 'value' => 16 }, { 'date' => '2021-04-02', 'value' => 16 },
{ 'date' => '2021-04-03', 'value' => 15 }, { 'date' => '2021-04-03', 'value' => 15 },
{ 'date' => '2021-04-04', 'value' => 14 }, { 'date' => '2021-04-04', 'value' => 14 },
{ 'date' => '2021-04-05', 'value' => 13 }, { 'date' => '2021-04-05', 'value' => 13 },
{ 'date' => '2021-04-06', 'value' => 12 }, { 'date' => '2021-04-06', 'value' => 12 },
{ 'date' => '2021-04-07', 'value' => nil } { 'date' => '2021-04-07', 'value' => nil }
]) ])
end end
end end
context 'with interval: "daily"' do context 'with interval: "monthly"' do
let(:args) { { metric: 'deployment_frequency', interval: 'daily' } } let(:args) { { metric: 'deployment_frequency', interval: 'monthly' } }
it 'returns the metrics grouped by day (the default)' do
expect(resolve_metrics).to eq([
{ 'date' => '2021-03-01', 'value' => 18 },
{ 'date' => '2021-04-01', 'value' => 17 },
{ 'date' => '2021-04-02', 'value' => 16 },
{ 'date' => '2021-04-03', 'value' => 15 },
{ 'date' => '2021-04-04', 'value' => 14 },
{ 'date' => '2021-04-05', 'value' => 13 },
{ 'date' => '2021-04-06', 'value' => 12 },
{ 'date' => '2021-04-07', 'value' => nil }
])
end
end
context 'with interval: "monthly"' do it 'returns the metrics grouped by month' do
let(:args) { { metric: 'deployment_frequency', interval: 'monthly' } } expect(resolve_metrics).to eq([
{ 'date' => '2021-03-01', 'value' => 18 },
{ 'date' => '2021-04-01', 'value' => 87 }
])
end
end
it 'returns the metrics grouped by month' do context 'with interval: "all"' do
expect(resolve_metrics).to eq([ let(:args) { { metric: 'deployment_frequency', interval: 'all' } }
{ 'date' => '2021-03-01', 'value' => 18 },
{ 'date' => '2021-04-01', 'value' => 87 }
])
end
end
context 'with interval: "all"' do it 'returns the metrics grouped into a single bucket with a nil date' do
let(:args) { { metric: 'deployment_frequency', interval: 'all' } } expect(resolve_metrics).to eq([
{ 'date' => nil, 'value' => 105 }
])
end
end
it 'returns the metrics grouped into a single bucket with a nil date' do context 'with a start_date' do
expect(resolve_metrics).to eq([ let(:args) { { metric: 'deployment_frequency', start_date: '2021-04-03'.to_datetime } }
{ 'date' => nil, 'value' => 105 }
]) it 'returns metrics for data on or after the provided date' do
end expect(resolve_metrics).to eq([
end { 'date' => '2021-04-03', 'value' => 15 },
{ 'date' => '2021-04-04', 'value' => 14 },
{ 'date' => '2021-04-05', 'value' => 13 },
{ 'date' => '2021-04-06', 'value' => 12 },
{ 'date' => '2021-04-07', 'value' => nil }
])
end
end
context 'with a start_date' do context 'with an end_date' do
let(:args) { { metric: 'deployment_frequency', start_date: '2021-04-03'.to_datetime } } let(:args) { { metric: 'deployment_frequency', end_date: '2021-04-03'.to_datetime } }
it 'returns metrics for data on or after the provided date' do it 'returns metrics for data on or before the provided date' do
expect(resolve_metrics).to eq([ expect(resolve_metrics).to eq([
{ 'date' => '2021-04-03', 'value' => 15 }, { 'date' => '2021-03-01', 'value' => 18 },
{ 'date' => '2021-04-04', 'value' => 14 }, { 'date' => '2021-04-01', 'value' => 17 },
{ 'date' => '2021-04-05', 'value' => 13 }, { 'date' => '2021-04-02', 'value' => 16 },
{ 'date' => '2021-04-06', 'value' => 12 }, { 'date' => '2021-04-03', 'value' => 15 }
{ 'date' => '2021-04-07', 'value' => nil } ])
]) end
end end
end
context 'with an end_date' do context 'with both a start_date and an end_date' do
let(:args) { { metric: 'deployment_frequency', end_date: '2021-04-03'.to_datetime } } let(:args) { { metric: 'deployment_frequency', start_date: '2021-04-01'.to_datetime, end_date: '2021-04-03'.to_datetime } }
it 'returns metrics for data on or before the provided date' do it 'returns metrics between the provided dates (inclusive)' do
expect(resolve_metrics).to eq([ expect(resolve_metrics).to eq([
{ 'date' => '2021-03-01', 'value' => 18 }, { 'date' => '2021-04-01', 'value' => 17 },
{ 'date' => '2021-04-01', 'value' => 17 }, { 'date' => '2021-04-02', 'value' => 16 },
{ 'date' => '2021-04-02', 'value' => 16 }, { 'date' => '2021-04-03', 'value' => 15 }
{ 'date' => '2021-04-03', 'value' => 15 } ])
]) end
end end
end
context 'with both a start_date and an end_date' do context 'when the requested date range is too large' do
let(:args) { { metric: 'deployment_frequency', start_date: '2021-04-01'.to_datetime, end_date: '2021-04-03'.to_datetime } } let(:args) { { metric: 'deployment_frequency', start_date: '2020-01-01'.to_datetime, end_date: '2021-05-01'.to_datetime } }
it 'returns metrics between the provided dates (inclusive)' do it 'raises an error' do
expect(resolve_metrics).to eq([ expect { resolve_metrics }.to raise_error('Date range must be shorter than 92 days.')
{ 'date' => '2021-04-01', 'value' => 17 }, end
{ 'date' => '2021-04-02', 'value' => 16 }, end
{ 'date' => '2021-04-03', 'value' => 15 }
])
end
end
context 'when the requested date range is too large' do context 'when the start date equal to or later than the end date' do
let(:args) { { metric: 'deployment_frequency', start_date: '2020-01-01'.to_datetime, end_date: '2021-05-01'.to_datetime } } let(:args) { { metric: 'deployment_frequency', start_date: '2021-04-01'.to_datetime, end_date: '2021-03-01'.to_datetime } }
it 'raises an error' do it 'raises an error' do
expect { resolve_metrics }.to raise_error('Date range must be shorter than 92 days.') expect { resolve_metrics }.to raise_error('The start date must be ealier than the end date.')
end end
end end
context 'when the start date equal to or later than the end date' do context 'with no metric parameter' do
let(:args) { { metric: 'deployment_frequency', start_date: '2021-04-01'.to_datetime, end_date: '2021-03-01'.to_datetime } } let(:args) { {} }
it 'raises an error' do it 'raises an error' do
expect { resolve_metrics }.to raise_error('The start date must be ealier than the end date.') expect { resolve_metrics }.to raise_error(/wrong number of arguments/)
end end
end end
context 'with metric: "lead_time_for_changes"' do
let(:args) { { metric: 'lead_time_for_changes' } }
it 'returns lead time metrics' do
expect(resolve_metrics).to eq([
{ 'date' => '2021-03-01', 'value' => nil },
{ 'date' => '2021-04-01', 'value' => 99 },
{ 'date' => '2021-04-02', 'value' => 98 },
{ 'date' => '2021-04-03', 'value' => 97 },
{ 'date' => '2021-04-04', 'value' => nil },
{ 'date' => '2021-04-05', 'value' => nil },
{ 'date' => '2021-04-06', 'value' => nil },
{ 'date' => '2021-04-07', 'value' => nil }
])
end
end
context 'with no metric parameter' do context 'with environment_tier: "staging"' do
let(:args) { {} } let(:args) { { metric: 'deployment_frequency', environment_tier: 'staging' } }
it 'raises an error' do it 'returns metrics for the staging environment' do
expect { resolve_metrics }.to raise_error(/wrong number of arguments/) expect(resolve_metrics).to eq([
{ 'date' => '2021-04-01', 'value' => 10 },
{ 'date' => '2021-04-02', 'value' => nil }
])
end
end
end end
end end
context 'with metric: "lead_time_for_changes"' do context 'when the user is querying for project-level metrics' do
let(:args) { { metric: 'lead_time_for_changes' } } let(:obj) { project }
it 'returns lead time metrics' do it_behaves_like 'dora metrics'
expect(resolve_metrics).to eq([
{ 'date' => '2021-03-01', 'value' => nil },
{ 'date' => '2021-04-01', 'value' => 99 },
{ 'date' => '2021-04-02', 'value' => 98 },
{ 'date' => '2021-04-03', 'value' => 97 },
{ 'date' => '2021-04-04', 'value' => nil },
{ 'date' => '2021-04-05', 'value' => nil },
{ 'date' => '2021-04-06', 'value' => nil },
{ 'date' => '2021-04-07', 'value' => nil }
])
end
end end
context 'with environment_tier: "staging"' do context 'when the user is querying for group-level metrics' do
let(:args) { { metric: 'deployment_frequency', environment_tier: 'staging' } } let(:obj) { group }
it 'returns metrics for the staging environment' do it_behaves_like 'dora metrics'
expect(resolve_metrics).to eq([
{ 'date' => '2021-04-01', 'value' => 10 },
{ 'date' => '2021-04-02', 'value' => nil }
])
end
end end
private private
def resolve_metrics def resolve_metrics
context = { current_user: current_user } context = { current_user: current_user }
resolve(described_class, obj: project, args: args, ctx: context) resolve(described_class, obj: obj, args: args, ctx: context)
end end
end end
...@@ -6,61 +6,103 @@ RSpec.describe 'Query.project(fullPath).dora.metrics' do ...@@ -6,61 +6,103 @@ RSpec.describe 'Query.project(fullPath).dora.metrics' do
include GraphqlHelpers include GraphqlHelpers
let_it_be(:reporter) { create(:user) } let_it_be(:reporter) { create(:user) }
let_it_be(:project) { create(:project) } let_it_be(:group) { create(:group) }
let_it_be(:production) { create(:environment, :production, project: project) } let_it_be(:project_1) { create(:project, group: group) }
let_it_be(:project_2) { create(:project, group: group) }
let(:query) do let_it_be(:project_not_in_group) { create(:project) }
graphql_query_for(:project, { fullPath: project.full_path }, let_it_be(:production_in_project_1) { create(:environment, :production, project: project_1) }
<<~QUERY let_it_be(:staging_in_project_1) { create(:environment, :staging, project: project_1) }
dora { let_it_be(:production_in_project_2) { create(:environment, :production, project: project_2) }
metrics(metric: DEPLOYMENT_FREQUENCY) { let_it_be(:production_not_in_group) { create(:environment, :production, project: project_not_in_group) }
date
value
}
}
QUERY
)
end
let(:post_query) { post_graphql(query, current_user: reporter) } let(:post_query) { post_graphql(query, current_user: reporter) }
let(:path_prefix) { %w[project dora metrics] }
let(:data) { graphql_data.dig(*path_prefix) } let(:data) { graphql_data.dig(*path_prefix) }
let(:query_body) do
<<~QUERY
dora {
metrics(metric: DEPLOYMENT_FREQUENCY) {
date
value
}
}
QUERY
end
around do |example| around do |example|
travel_to '2021-01-08'.to_time do travel_to '2021-02-01'.to_time do
example.run example.run
end end
end end
before_all do before_all do
project.add_reporter(reporter) group.add_reporter(reporter)
create(:dora_daily_metrics, deployment_frequency: 3, environment: production, date: '2021-01-01') create(:dora_daily_metrics, deployment_frequency: 3, environment: production_in_project_1, date: '2021-01-01')
create(:dora_daily_metrics, deployment_frequency: 3, environment: production, date: '2021-01-02') create(:dora_daily_metrics, deployment_frequency: 3, environment: production_in_project_1, date: '2021-01-02')
create(:dora_daily_metrics, deployment_frequency: 2, environment: production, date: '2021-01-03') create(:dora_daily_metrics, deployment_frequency: 2, environment: production_in_project_1, date: '2021-01-03')
create(:dora_daily_metrics, deployment_frequency: 2, environment: production, date: '2021-01-04') create(:dora_daily_metrics, deployment_frequency: 2, environment: production_in_project_1, date: '2021-01-04')
create(:dora_daily_metrics, deployment_frequency: 1, environment: production, date: '2021-01-05') create(:dora_daily_metrics, deployment_frequency: 1, environment: production_in_project_1, date: '2021-01-05')
create(:dora_daily_metrics, deployment_frequency: 1, environment: production, date: '2021-01-06') create(:dora_daily_metrics, deployment_frequency: 1, environment: production_in_project_1, date: '2021-01-06')
create(:dora_daily_metrics, deployment_frequency: nil, environment: production, date: '2021-01-07') create(:dora_daily_metrics, deployment_frequency: nil, environment: production_in_project_1, date: '2021-01-07')
create(:dora_daily_metrics, deployment_frequency: 4, environment: staging_in_project_1, date: '2021-01-08')
create(:dora_daily_metrics, deployment_frequency: 4, environment: production_in_project_2, date: '2021-01-09')
create(:dora_daily_metrics, deployment_frequency: 5, environment: production_not_in_group, date: '2021-01-10')
end end
before do before do
stub_licensed_features(dora4_analytics: true) stub_licensed_features(dora4_analytics: true)
end end
it 'returns the expected DORA metrics' do context 'when querying for project-level metrics' do
post_query let(:path_prefix) { %w[project dora metrics] }
expect(data).to eq( let(:query) do
[ graphql_query_for(:project, { fullPath: project_1.full_path }, query_body)
{ 'value' => 3, 'date' => '2021-01-01' }, end
{ 'value' => 3, 'date' => '2021-01-02' },
{ 'value' => 2, 'date' => '2021-01-03' }, it 'returns the expected project-level DORA metrics' do
{ 'value' => 2, 'date' => '2021-01-04' }, post_query
{ 'value' => 1, 'date' => '2021-01-05' },
{ 'value' => 1, 'date' => '2021-01-06' }, expect(data).to eq(
{ 'value' => nil, 'date' => '2021-01-07' } [
] { 'value' => 3, 'date' => '2021-01-01' },
) { 'value' => 3, 'date' => '2021-01-02' },
{ 'value' => 2, 'date' => '2021-01-03' },
{ 'value' => 2, 'date' => '2021-01-04' },
{ 'value' => 1, 'date' => '2021-01-05' },
{ 'value' => 1, 'date' => '2021-01-06' },
{ 'value' => nil, 'date' => '2021-01-07' }
]
)
end
end
context 'when querying for group-level metrics' do
let(:path_prefix) { %w[group dora metrics] }
let(:query) do
graphql_query_for(:group, { fullPath: group.full_path }, query_body)
end
it 'returns the expected group-level DORA metrics' do
post_query
expect(data).to eq(
[
{ 'value' => 3, 'date' => '2021-01-01' },
{ 'value' => 3, 'date' => '2021-01-02' },
{ 'value' => 2, 'date' => '2021-01-03' },
{ 'value' => 2, 'date' => '2021-01-04' },
{ 'value' => 1, 'date' => '2021-01-05' },
{ 'value' => 1, 'date' => '2021-01-06' },
{ 'value' => nil, 'date' => '2021-01-07' },
{ 'value' => 4, 'date' => '2021-01-09' }
]
)
end
end end
end end
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