Commit b23f8149 authored by Nathan Friend's avatar Nathan Friend

Add group-level DORA metrics to GraphQL endpoint

This update exposes group-level DORA metrics through the GraphQL
endpoint. Previously, these metrics were only available (in GraphQL)
at the project level.

Changelog: added
EE: true
parent 0f58dab1
......@@ -9210,6 +9210,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="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="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="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. |
......
......@@ -86,6 +86,12 @@ module EE
field :billable_members_count, ::GraphQL::INT_TYPE,
null: true,
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
......
......@@ -74,4 +74,10 @@ RSpec.describe GitlabSchema.types['Group'] do
expect(vulnerabilities.first['severity']).to eq('CRITICAL')
end
end
describe 'dora field' do
subject { described_class.fields['dora'] }
it { is_expected.to have_graphql_type(Types::DoraType) }
end
end
......@@ -7,7 +7,8 @@ RSpec.describe Resolvers::DoraMetricsResolver do
let_it_be(:guest) { 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(:staging) { create(:environment, :staging, project: project) }
......@@ -21,8 +22,8 @@ RSpec.describe Resolvers::DoraMetricsResolver do
end
before_all do
project.add_guest(guest)
project.add_reporter(reporter)
group.add_guest(guest)
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: 19, lead_time_for_changes_in_seconds: nil, environment: production, date: '2021-01-01')
......@@ -44,176 +45,190 @@ RSpec.describe Resolvers::DoraMetricsResolver do
stub_licensed_features(dora4_analytics: true)
end
describe '#resolve' do
context 'when the current users does not have access to query DORA metrics' do
let(:current_user) { guest }
shared_examples 'dora metrics' do
describe '#resolve' do
context 'when the current users does not have access to query DORA metrics' do
let(:current_user) { guest }
it 'returns no metrics' do
expect(resolve_metrics).to be_nil
it 'returns no metrics' do
expect(resolve_metrics).to be_nil
end
end
end
context 'when DORA metrics are not licensed' do
before do
stub_licensed_features(dora4_analytics: false)
context 'when DORA metrics are not licensed' do
before do
stub_licensed_features(dora4_analytics: false)
end
it 'returns no metrics' do
expect(resolve_metrics).to be_nil
end
end
it 'returns no metrics' do
expect(resolve_metrics).to be_nil
context 'with metric: "deployment_frequency"' do
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
context 'with metric: "deployment_frequency"' do
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
context 'with interval: "daily"' do
let(:args) { { metric: 'deployment_frequency', interval: 'daily' } }
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: "daily"' do
let(:args) { { metric: 'deployment_frequency', interval: 'daily' } }
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
let(:args) { { metric: 'deployment_frequency', interval: 'monthly' } }
context 'with interval: "monthly"' do
let(:args) { { metric: 'deployment_frequency', interval: 'monthly' } }
it 'returns the metrics grouped by month' do
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
expect(resolve_metrics).to eq([
{ 'date' => '2021-03-01', 'value' => 18 },
{ 'date' => '2021-04-01', 'value' => 87 }
])
end
end
context 'with interval: "all"' do
let(:args) { { metric: 'deployment_frequency', interval: 'all' } }
context 'with interval: "all"' do
let(:args) { { metric: 'deployment_frequency', interval: 'all' } }
it 'returns the metrics grouped into a single bucket with a nil date' do
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
expect(resolve_metrics).to eq([
{ 'date' => nil, 'value' => 105 }
])
end
end
context 'with a start_date' do
let(:args) { { metric: 'deployment_frequency', start_date: '2021-04-03'.to_datetime } }
it 'returns metrics for data on or after the provided date' do
expect(resolve_metrics).to eq([
{ '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
let(:args) { { metric: 'deployment_frequency', start_date: '2021-04-03'.to_datetime } }
it 'returns metrics for data on or after the provided date' do
expect(resolve_metrics).to eq([
{ '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 an end_date' do
let(:args) { { metric: 'deployment_frequency', end_date: '2021-04-03'.to_datetime } }
it 'returns metrics for data on or before the provided date' 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 }
])
end
end
context 'with an end_date' do
let(:args) { { metric: 'deployment_frequency', end_date: '2021-04-03'.to_datetime } }
context 'with both a start_date and an end_date' do
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
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 }
])
end
end
it 'returns metrics between the provided dates (inclusive)' do
expect(resolve_metrics).to eq([
{ 'date' => '2021-04-01', 'value' => 17 },
{ 'date' => '2021-04-02', 'value' => 16 },
{ 'date' => '2021-04-03', 'value' => 15 }
])
end
end
context 'with both a start_date and an end_date' do
let(:args) { { metric: 'deployment_frequency', start_date: '2021-04-01'.to_datetime, end_date: '2021-04-03'.to_datetime } }
context 'when the requested date range is too large' do
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
expect(resolve_metrics).to eq([
{ 'date' => '2021-04-01', 'value' => 17 },
{ 'date' => '2021-04-02', 'value' => 16 },
{ 'date' => '2021-04-03', 'value' => 15 }
])
end
end
it 'raises an error' do
expect { resolve_metrics }.to raise_error('Date range must be shorter than 92 days.')
end
end
context 'when the requested date range is too large' do
let(:args) { { metric: 'deployment_frequency', start_date: '2020-01-01'.to_datetime, end_date: '2021-05-01'.to_datetime } }
context 'when the start date equal to or later than the end date' do
let(:args) { { metric: 'deployment_frequency', start_date: '2021-04-01'.to_datetime, end_date: '2021-03-01'.to_datetime } }
it 'raises an error' do
expect { resolve_metrics }.to raise_error('Date range must be shorter than 92 days.')
end
end
it 'raises an error' do
expect { resolve_metrics }.to raise_error('The start date must be ealier than the end date.')
end
end
context 'when the start date equal to or later than the end date' do
let(:args) { { metric: 'deployment_frequency', start_date: '2021-04-01'.to_datetime, end_date: '2021-03-01'.to_datetime } }
context 'with no metric parameter' do
let(:args) { {} }
it 'raises an error' do
expect { resolve_metrics }.to raise_error('The start date must be ealier than the end date.')
end
end
it 'raises an error' do
expect { resolve_metrics }.to raise_error(/wrong number of arguments/)
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
let(:args) { {} }
context 'with environment_tier: "staging"' do
let(:args) { { metric: 'deployment_frequency', environment_tier: 'staging' } }
it 'raises an error' do
expect { resolve_metrics }.to raise_error(/wrong number of arguments/)
it 'returns metrics for the staging environment' do
expect(resolve_metrics).to eq([
{ 'date' => '2021-04-01', 'value' => 10 },
{ 'date' => '2021-04-02', 'value' => nil }
])
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
context 'when the user is querying for project-level metrics' do
let(:obj) { project }
it_behaves_like 'dora metrics'
end
context 'with environment_tier: "staging"' do
let(:args) { { metric: 'deployment_frequency', environment_tier: 'staging' } }
context 'when the user is querying for group-level metrics' do
let(:obj) { group }
it 'returns metrics for the staging environment' do
expect(resolve_metrics).to eq([
{ 'date' => '2021-04-01', 'value' => 10 },
{ 'date' => '2021-04-02', 'value' => nil }
])
end
it_behaves_like 'dora metrics'
end
private
def resolve_metrics
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
......@@ -6,61 +6,103 @@ RSpec.describe 'Query.project(fullPath).dora.metrics' do
include GraphqlHelpers
let_it_be(:reporter) { create(:user) }
let_it_be(:project) { create(:project) }
let_it_be(:production) { create(:environment, :production, project: project) }
let(:query) do
graphql_query_for(:project, { fullPath: project.full_path },
<<~QUERY
dora {
metrics(metric: DEPLOYMENT_FREQUENCY) {
date
value
}
}
QUERY
)
end
let_it_be(:group) { create(:group) }
let_it_be(:project_1) { create(:project, group: group) }
let_it_be(:project_2) { create(:project, group: group) }
let_it_be(:project_not_in_group) { create(:project) }
let_it_be(:production_in_project_1) { create(:environment, :production, project: project_1) }
let_it_be(:staging_in_project_1) { create(:environment, :staging, project: project_1) }
let_it_be(:production_in_project_2) { create(:environment, :production, project: project_2) }
let_it_be(:production_not_in_group) { create(:environment, :production, project: project_not_in_group) }
let(:post_query) { post_graphql(query, current_user: reporter) }
let(:path_prefix) { %w[project dora metrics] }
let(:data) { graphql_data.dig(*path_prefix) }
let(:query_body) do
<<~QUERY
dora {
metrics(metric: DEPLOYMENT_FREQUENCY) {
date
value
}
}
QUERY
end
around do |example|
travel_to '2021-01-08'.to_time do
travel_to '2021-02-01'.to_time do
example.run
end
end
before_all do
project.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, 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, 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, date: '2021-01-06')
create(:dora_daily_metrics, deployment_frequency: nil, environment: production, date: '2021-01-07')
group.add_reporter(reporter)
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_in_project_1, date: '2021-01-02')
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_in_project_1, date: '2021-01-04')
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_in_project_1, date: '2021-01-06')
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
before do
stub_licensed_features(dora4_analytics: true)
end
it 'returns the expected 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' }
]
)
context 'when querying for project-level metrics' do
let(:path_prefix) { %w[project dora metrics] }
let(:query) do
graphql_query_for(:project, { fullPath: project_1.full_path }, query_body)
end
it 'returns the expected project-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' }
]
)
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
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