Commit 60dfc259 authored by Amy Troschinetz's avatar Amy Troschinetz Committed by Mayra Cabrera

Add API for Group Deployment Frequency

- **ee/changelogs/unreleased/
    group-level-deployment-frequency-api.yml:**

Changelog.

- **doc/api/dora4_group_analytics.md:**
- **doc/user/group/index.md:**
- **doc/user/analytics/ci_cd_analytics.md:**

Docs.

- **ee/app/models/license.rb:**
- **ee/lib/api/analytics/group_deployment_frequency.rb:**
- **ee/lib/ee/api/api.rb:**
- **ee/spec/finders/analytics/deployments_finder_spec.rb:**

Add support for groups.

- **ee/spec/policies/group_policy_spec.rb:**
- **ee/spec/requests/api/analytics/group_deployment_frequency_spec.rb:**
- **app/finders/deployments_finder.rb:**

**config/feature_flags/development/
  dora4_sorted_group_deployment_frequency.yml:**

New feature flag to gate sorting deployments by finished_at for API
response at the group scope.

New tests.

- **doc/api/dora4_project_analytics.md:**
- **doc/api/project_analytics.md:**
- **doc/user/project/index.md:**
- **ee/app/helpers/ee/graph_helper.rb:**
- **ee/app/policies/ee/group_policy.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:**

Updated name to prevent conflicts.

- **config/feature_flags/development/
    dora4_sorted_group_deployment_frequency.yml:**

New feature flag for potentially slow query.

- **ee/lib/ee/api/entities/analytics/deployment_frequency.rb:**

Updated to add proper datetime formatting.

- **app/models/deployment.rb:**
- **spec/models/deployment_spec.rb:**

Added support for finished_between and finished_after.

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

Adds a service to aggregate frequency deployments.

- **locale/gitlab.pot:**

Updated.
parent db64746a
# frozen_string_literal: true
# WARNING: This finder does not check permissions!
#
# Arguments:
# params:
# project: Project model - Find deployments for this project
......@@ -27,11 +29,13 @@ class DeploymentsFinder
def execute
items = init_collection
items = by_updated_at(items)
items = by_finished_at(items)
items = by_environment(items)
items = by_status(items)
items = preload_associations(items)
items = by_finished_between(items)
sort(items)
items = sort(items)
items
end
private
......@@ -44,11 +48,9 @@ class DeploymentsFinder
end
end
# rubocop: disable CodeReuse/ActiveRecord
def sort(items)
items.order(sort_params)
items.order(sort_params) # rubocop: disable CodeReuse/ActiveRecord
end
# rubocop: enable CodeReuse/ActiveRecord
def by_updated_at(items)
items = items.updated_before(params[:updated_before]) if params[:updated_before].present?
......@@ -57,6 +59,13 @@ class DeploymentsFinder
items
end
def by_finished_at(items)
items = items.finished_before(params[:finished_before]) if params[:finished_before].present?
items = items.finished_after(params[:finished_after]) if params[:finished_after].present?
items
end
def by_environment(items)
if params[:environment].present?
items.for_environment_name(params[:environment])
......@@ -65,12 +74,6 @@ class DeploymentsFinder
end
end
def by_finished_between(items)
items = items.finished_between(params[:finished_after], params[:finished_before].presence) if params[:finished_after].present?
items
end
def by_status(items)
return items unless params[:status].present?
......
......@@ -38,6 +38,7 @@ class Deployment < ApplicationRecord
scope :for_status, -> (status) { where(status: status) }
scope :for_project, -> (project_id) { where(project_id: project_id) }
scope :for_projects, -> (projects) { where(project: projects) }
scope :visible, -> { where(status: %i[running success failed canceled]) }
scope :stoppable, -> { where.not(on_stop: nil).where.not(deployable_id: nil).success }
......@@ -45,11 +46,8 @@ class Deployment < ApplicationRecord
scope :older_than, -> (deployment) { where('deployments.id < ?', deployment.id) }
scope :with_deployable, -> { joins('INNER JOIN ci_builds ON ci_builds.id = deployments.deployable_id').preload(:deployable) }
scope :finished_between, -> (start_date, end_date = nil) do
selected = where('deployments.finished_at >= ?', start_date)
selected = selected.where('deployments.finished_at < ?', end_date) if end_date
selected
end
scope :finished_after, ->(date) { where('finished_at >= ?', date) }
scope :finished_before, ->(date) { where('finished_at < ?', date) }
FINISHED_STATUSES = %i[success failed canceled].freeze
......
---
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 Group API **(ULTIMATE ONLY)**
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/291747) in GitLab 13.9.
> - It's [deployed behind a feature flag](../user/feature_flags.md), disabled by default.
> - It's disabled on GitLab.com.
> - It's not recommended for production use.
> - To use it in GitLab self-managed instances, ask a GitLab administrator to [enable it](#enable-or-disable-dora4-analytics-group-api). **(ULTIMATE ONLY)**
WARNING:
This feature might not be available to you. Check the **version history** note above for details.
All methods require reporter authorization.
## List group deployment frequencies
Get a list of all group deployment frequencies:
```plaintext
GET /groups/:id/analytics/deployment_frequency?environment=:environment&from=:from&to=:to&interval=:interval
```
Attributes:
| Attribute | Type | Required | Description |
|--------------|--------|----------|-----------------------|
| `id` | string | yes | The ID of the group. |
Parameters:
| 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/groups/: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
}
]
```
## Enable or disable DORA4 Analytics Group API **(ULTIMATE ONLY)**
DORA4 Analytics Group API is under development and not ready for production use. It is
deployed behind a feature flag that is **disabled by default**.
[GitLab administrators with access to the GitLab Rails console](../administration/feature_flags.md)
can enable it.
To enable it:
```ruby
Feature.enable(:dora4_group_deployment_frequency_api)
```
To disable it:
```ruby
Feature.disable(:dora4_group_deployment_frequency_api)
```
---
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
}
]
```
---
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 -->
......@@ -42,8 +42,8 @@ performance indicators for software development teams:
production.
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).
the first metric, deployment frequency, at the project and group scopes for [CI/CD charts](ci_cd_analytics.md#deployment-frequency-charts),
the [Project API]( ../../api/dora4_project_analytics.md), and the [Group API]( ../../api/dora4_group_analytics.md).
## Deployment frequency charts **(ULTIMATE)**
......
......@@ -870,3 +870,13 @@ questions that you know someone might ask.
Each scenario can be a third-level heading, e.g. `### Getting error message X`.
If you have none to add when creating a doc, leave this section in place
but commented out to help encourage others to add to it in the future. -->
## DORA4 analytics overview **(ULTIMATE ONLY)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/291747) in GitLab [Ultimate](https://about.gitlab.com/pricing/) 13.9 as a [Beta feature](https://about.gitlab.com/handbook/product/gitlab-the-product/#beta).
Group details include the following analytics:
- Deployment Frequency
For more information, see [DORA4 Project Analytics API](../../api/dora4_group_analytics.md).
......@@ -152,9 +152,9 @@ 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)
## 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).
......@@ -162,4 +162,4 @@ Project details include the following analytics:
- Deployment Frequency
For more information, see [Project Analytics API](../../api/project_analytics.md).
For more information, see [DORA4 Project Analytics API](../../api/dora4_project_analytics.md).
# frozen_string_literal: true
# WARNING: This finder does not check permissions!
#
# Arguments:
# params:
# group: Group model - Find deployments within a group (including subgroups)
......
......@@ -7,9 +7,9 @@ module EE
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
......@@ -144,6 +144,7 @@ class License < ApplicationRecord
dast
dependency_scanning
devops_adoption
dora4_analytics
enforce_personal_access_token_expiration
enforce_ssh_key_expiration
enterprise_templates
......@@ -157,7 +158,6 @@ class License < ApplicationRecord
jira_issue_association_enforcement
license_scanning
personal_access_token_expiration_policy
project_activity_analytics
prometheus_alerts
pseudonymizer
quality_management
......
......@@ -37,6 +37,10 @@ module EE
@subject.feature_available?(:group_activity_analytics)
end
condition(:dora4_analytics_available) do
@subject.feature_available?(:dora4_analytics)
end
condition(:can_owners_manage_ldap, scope: :global) do
::Gitlab::CurrentSettings.allow_group_owners_to_manage_ldap?
end
......@@ -160,6 +164,9 @@ module EE
rule { has_access & group_activity_analytics_available }
.enable :read_group_activity_analytics
rule { reporter & dora4_analytics_available }
.enable :read_dora4_analytics
rule { reporter & group_repository_analytics_available }
.enable :read_group_repository_analytics
......
......@@ -52,8 +52,8 @@ module EE
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
......@@ -382,8 +382,8 @@ module EE
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
......
# frozen_string_literal: true
module Analytics
module Deployments
module Frequency
# This class is to aggregate deployments data at project-level or group-level
# for calculating the frequency.
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
frequencies = deployments_grouped.map do |grouped_start_date, grouped_deploys|
{
value: grouped_deploys.count,
from: grouped_start_date,
to: deployments_grouped_end_date(grouped_start_date)
}
end
success(frequencies: frequencies)
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 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)
error(_("You do not have permission to access deployment frequencies"), :forbidden)
end
end
def interval
params[:interval] || INTERVAL_ALL
end
def start_date
params[:from]
end
def end_date
strong_memoize(:end_date) do
params[:to] || DateTime.current
end
end
def days_between
(end_date - start_date).to_i
end
def deployments_grouped
case interval
when INTERVAL_ALL
{ start_date => deployments }
when INTERVAL_MONTHLY
deployments.group_by { |d| d.finished_at.beginning_of_month }
when INTERVAL_DAILY
deployments.group_by { |d| d.finished_at.to_date }
end
end
def deployments_grouped_end_date(deployments_grouped_start_date)
case interval
when INTERVAL_ALL
end_date
when INTERVAL_MONTHLY
deployments_grouped_start_date + 1.month
when INTERVAL_DAILY
deployments_grouped_start_date + 1.day
end
end
def container_params
if container.is_a?(Project)
{ project: container }
elsif container.is_a?(Group)
{ group: container }
else
{}
end
end
def deployments
::DeploymentsFinder.new(
**container_params,
environment: params[:environment],
status: :success,
finished_before: end_date,
finished_after: start_date,
order_by: :finished_at,
sort: :asc
).execute
end
end
end
end
end
---
title: Adds API support for Group Deployment Frequency
merge_request: 51938
author:
type: added
---
name: dora4_group_deployment_frequency_api
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/51938
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/300239
milestone: '13.9'
type: development
group: group::release
default_enabled: false
# frozen_string_literal: true
module API
module Analytics
class GroupDeploymentFrequency < ::API::Base
feature_category :continuous_delivery
before do
authenticate!
not_found! unless ::Feature.enabled?(:dora4_group_deployment_frequency_api, user_group)
end
params do
requires :id, type: String, desc: 'The ID of the group'
end
resource :groups, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
namespace ':id/analytics' do
desc 'List deployment frequencies for the group'
params do
requires :environment, type: String, desc: 'The name of the environment to filter by'
requires :from, type: DateTime, desc: 'Datetime range to start from. Inclusive, ISO 8601 format (`YYYY-MM-DDTHH:MM:SSZ`)'
optional :to, type: DateTime, desc: 'Datetime range to end at. Exclusive, ISO 8601 format (`YYYY-MM-DDTHH:MM:SSZ`)'
optional :interval, type: String, desc: 'The bucketing interval (`all`, `monthly`, `daily`)'
end
get 'deployment_frequency' do
result = ::Analytics::Deployments::Frequency::AggregateService
.new(container: user_group,
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[:frequencies], with: EE::API::Entities::Analytics::DeploymentFrequency
end
end
end
end
end
end
......@@ -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,
......@@ -110,7 +109,7 @@ module API
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
......
......@@ -43,6 +43,7 @@ module EE
mount ::API::VisualReviewDiscussions
mount ::API::Analytics::CodeReviewAnalytics
mount ::API::Analytics::GroupActivityAnalytics
mount ::API::Analytics::GroupDeploymentFrequency
mount ::API::Analytics::ProjectDeploymentFrequency
mount ::API::ProtectedEnvironments
mount ::API::ResourceWeightEvents
......
......@@ -5,9 +5,11 @@ module EE
module Entities
module Analytics
class DeploymentFrequency < Grape::Entity
format_with(:iso8601_date) { |datetime| datetime.to_date.iso8601 }
expose :value
expose :from
expose :to
expose :from, format_with: :iso8601_date
expose :to, format_with: :iso8601_date
end
end
end
......
......@@ -3,18 +3,61 @@
require 'spec_helper'
RSpec.describe DeploymentsFinder do
context 'when filtering by group' do
subject { described_class.new(params).execute }
context 'at group scope' do
let_it_be(:group) { create(:group) }
let_it_be(:subgroup) { create(:group, parent: group) }
let(:group_project_1) { create(:project, :public, :test_repo, group: group) }
let(:group_project_2) { create(:project, :public, :test_repo, group: group) }
let(:subgroup_project_1) { create(:project, :public, :test_repo, group: subgroup) }
let(:base_params) { { group: group } }
describe 'ordering' do
using RSpec::Parameterized::TableSyntax
let(:params) { { **base_params, order_by: order_by, sort: sort } }
let!(:group_project_1_deployment) { create(:deployment, :success, project: group_project_1, iid: 11, ref: 'master', created_at: 2.days.ago, updated_at: Time.now, finished_at: Time.now) }
let!(:group_project_2_deployment) { create(:deployment, :success, project: group_project_2, iid: 12, ref: 'feature', created_at: 1.day.ago, updated_at: 2.hours.ago, finished_at: 2.hours.ago) }
let!(:subgroup_project_1_deployment) { create(:deployment, :success, project: subgroup_project_1, iid: 8, ref: 'video', created_at: Time.now, updated_at: 1.hour.ago, finished_at: 1.hour.ago) }
where(:order_by, :sort) do
'created_at' | 'asc'
'created_at' | 'desc'
'id' | 'asc'
'id' | 'desc'
'iid' | 'asc'
'iid' | 'desc'
'ref' | 'asc'
'ref' | 'desc'
'updated_at' | 'asc'
'updated_at' | 'desc'
'finished_at' | 'asc'
'finished_at' | 'desc'
'invalid' | 'asc'
'iid' | 'err'
end
let_it_be(:project_in_group) { create(:project, :repository, group: group) }
let_it_be(:project_in_subgroup) { create(:project, :repository, group: subgroup) }
with_them do
it 'returns the deployments unordered' do
expect(subject.to_a).to contain_exactly(group_project_1_deployment,
group_project_2_deployment,
subgroup_project_1_deployment)
end
end
end
let_it_be(:deployment_in_group) { create(:deployment, status: :success, project: project_in_group) }
let_it_be(:deployment_in_subgroup) { create(:deployment, status: :success, project: project_in_subgroup) }
it 'avoids N+1 queries' do
execute_queries = -> { described_class.new({ group: group }).execute.first }
control_count = ActiveRecord::QueryRecorder.new { execute_queries }.count
subject { described_class.new(group: group).execute }
new_project = create(:project, :repository, group: group)
new_env = create(:environment, project: new_project, name: "production")
create_list(:deployment, 2, status: :success, project: new_project, environment: new_env)
group.reload
it { is_expected.to match_array([deployment_in_group, deployment_in_subgroup]) }
expect { execute_queries }.not_to exceed_query_limit(control_count)
end
end
end
......@@ -30,7 +30,7 @@ RSpec.describe 'Project Analytics (JavaScript fixtures)' do
end
before do
stub_licensed_features(project_activity_analytics: true)
stub_licensed_features(dora4_analytics: true)
project.add_reporter(reporter)
sign_in(reporter)
end
......
......@@ -12,11 +12,11 @@ RSpec.describe EE::GraphHelper do
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
......
......@@ -184,6 +184,26 @@ RSpec.describe GroupPolicy do
it { is_expected.not_to be_allowed(:read_group_contribution_analytics) }
end
context 'when dora4 analytics is available' do
let(:current_user) { developer }
before do
stub_licensed_features(dora4_analytics: true)
end
it { is_expected.to be_allowed(:read_dora4_analytics) }
end
context 'when dora4 analytics is not available' do
let(:current_user) { developer }
before do
stub_licensed_features(dora4_analytics: false)
end
it { is_expected.not_to be_allowed(:read_dora4_analytics) }
end
context 'when group activity analytics is available' do
let(:current_user) { developer }
......
......@@ -1388,24 +1388,24 @@ RSpec.describe ProjectPolicy do
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
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe API::Analytics::GroupDeploymentFrequency do
let_it_be(:group) { create(:group, :private) }
let_it_be(:project1) { create(:project, :repository, namespace: group) }
let_it_be(:project2) { create(:project, :repository, namespace: group) }
let_it_be(:prod_env_name) { "prod" }
let_it_be(:prod1) { create(:environment, project: project1, name: prod_env_name) }
let_it_be(:dev1) { create(:environment, project: project1, name: "dev") }
let_it_be(:prod2) { create(:environment, project: project2, name: prod_env_name) }
let_it_be(:dev2) { create(:environment, project: project2, name: "dev") }
let_it_be(:anonymous_user) { create(:user) }
let_it_be(:reporter) { create(:user).tap { |u| group.add_reporter(u) } }
def make_deployment(finished_at, env, proj)
create(:deployment,
status: :success,
project: proj,
environment: env,
finished_at: finished_at)
end
let_it_be(:deployment_2020_01_01) { make_deployment(DateTime.new(2020, 1, 1), prod1, project1) }
let_it_be(:deployment_2020_01_02) { make_deployment(DateTime.new(2020, 1, 2), prod2, project2) }
let_it_be(:deployment_2020_01_03) { make_deployment(DateTime.new(2020, 1, 3), dev1, project1) }
let_it_be(:deployment_2020_01_04) { make_deployment(DateTime.new(2020, 1, 4), prod2, project2) }
let_it_be(:deployment_2020_01_05) { make_deployment(DateTime.new(2020, 1, 5), prod1, project1) }
let_it_be(:deployment_2020_02_01) { make_deployment(DateTime.new(2020, 2, 1), prod2, project2) }
let_it_be(:deployment_2020_02_02) { make_deployment(DateTime.new(2020, 2, 2), prod1, project1) }
let_it_be(:deployment_2020_02_03) { make_deployment(DateTime.new(2020, 2, 3), dev2, project2) }
let_it_be(:deployment_2020_02_04) { make_deployment(DateTime.new(2020, 2, 4), prod1, project1) }
let_it_be(:deployment_2020_02_05) { make_deployment(DateTime.new(2020, 2, 5), prod2, project2) }
let_it_be(:deployment_2020_03_01) { make_deployment(DateTime.new(2020, 3, 1), prod1, project1) }
let_it_be(:deployment_2020_03_02) { make_deployment(DateTime.new(2020, 3, 2), prod2, project2) }
let_it_be(:deployment_2020_03_03) { make_deployment(DateTime.new(2020, 3, 3), dev1, project1) }
let_it_be(:deployment_2020_03_04) { make_deployment(DateTime.new(2020, 3, 4), prod2, project2) }
let_it_be(:deployment_2020_03_05) { make_deployment(DateTime.new(2020, 3, 5), prod1, project1) }
let_it_be(:deployment_2020_04_01) { make_deployment(DateTime.new(2020, 4, 1), prod2, project2) }
let_it_be(:deployment_2020_04_02) { make_deployment(DateTime.new(2020, 4, 2), prod1, project1) }
let_it_be(:deployment_2020_04_03) { make_deployment(DateTime.new(2020, 4, 3), dev2, project2) }
let_it_be(:deployment_2020_04_04) { make_deployment(DateTime.new(2020, 4, 4), prod1, project1) }
let_it_be(:deployment_2020_04_05) { make_deployment(DateTime.new(2020, 4, 5), prod2, project2) }
let(:dora4_analytics_enabled) { true }
let(:api_feature_flag_enabled) { true }
let(:current_user) { reporter }
let(:params) { { from: 1.day.ago, to: Time.now, interval: "all", environment: prod_env_name } }
let(:path) { api("/groups/#{group.id}/analytics/deployment_frequency", current_user) }
let(:request) { get path, params: params }
let(:request_time) { nil }
before do
stub_licensed_features(dora4_analytics: dora4_analytics_enabled)
stub_feature_flags(dora4_group_deployment_frequency_api: api_feature_flag_enabled)
if request_time
travel_to(request_time) { request }
else
request
end
end
context 'when user has access to the group' 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) { { environment: prod_env_name, 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
{ environment: prod_env_name, 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) { { environment: prod_env_name, from: DateTime.new(2020, 4, 2) } }
it 'returns the expected deployment frequencies' do
expect(response.parsed_body).to eq([{
"from" => "2020-04-02",
"to" => "2020-04-04",
"value" => 1
}])
end
end
context 'with params: from 2020/02/01 to 2020/04/01 by all' do
let(:params) do
{
environment: prod_env_name,
from: DateTime.new(2020, 2, 1),
to: DateTime.new(2020, 4, 1),
interval: "all"
}
end
it 'returns the expected deployment frequencies' do
expect(response.parsed_body).to eq([{
"from" => "2020-02-01",
"to" => "2020-04-01",
"value" => 8
}])
end
end
context 'with params: from 2020/02/01 to 2020/04/01 by month' do
let(:params) do
{
environment: prod_env_name,
from: DateTime.new(2020, 2, 1),
to: DateTime.new(2020, 4, 1),
interval: "monthly"
}
end
it 'returns the expected deployment frequencies' do
expect(response.parsed_body).to eq([
{ "from" => "2020-02-01", "to" => "2020-03-01", "value" => 4 },
{ "from" => "2020-03-01", "to" => "2020-04-01", "value" => 4 }
])
end
end
context 'with params: from 2020/02/01 to 2020/04/01 by day' do
let(:params) do
{
environment: prod_env_name,
from: DateTime.new(2020, 2, 1),
to: DateTime.new(2020, 4, 1),
interval: "daily"
}
end
it 'returns the expected deployment frequencies' do
expect(response.parsed_body).to eq([
{ "from" => "2020-02-01", "to" => "2020-02-02", "value" => 1 },
{ "from" => "2020-02-02", "to" => "2020-02-03", "value" => 1 },
{ "from" => "2020-02-04", "to" => "2020-02-05", "value" => 1 },
{ "from" => "2020-02-05", "to" => "2020-02-06", "value" => 1 },
{ "from" => "2020-03-01", "to" => "2020-03-02", "value" => 1 },
{ "from" => "2020-03-02", "to" => "2020-03-03", "value" => 1 },
{ "from" => "2020-03-04", "to" => "2020-03-05", "value" => 1 },
{ "from" => "2020-03-05", "to" => "2020-03-06", "value" => 1 }
])
end
end
context 'with params: invalid interval' do
let(:params) do
{
environment: prod_env_name,
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) { { environment: prod_env_name, 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 group' 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 group' do
it 'returns `forbidden`' do
expect(response).to have_gitlab_http_status(:forbidden)
end
end
context 'when user does not have access to the group' do
let(:current_user) { anonymous_user }
it 'returns `not_found`' do
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
context 'when feature flag dora4_group_deployment_frequency_api is disabled' do
let(:api_feature_flag_enabled) { false }
context 'when user has access to the group' do
it 'returns `not_found`' do
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
end
......@@ -42,7 +42,7 @@ RSpec.describe API::Analytics::ProjectDeploymentFrequency do
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) }
......@@ -50,7 +50,7 @@ RSpec.describe API::Analytics::ProjectDeploymentFrequency do
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 }
......@@ -87,14 +87,14 @@ RSpec.describe API::Analytics::ProjectDeploymentFrequency do
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) } }
it 'returns the expected deployment frequencies' do
expect(response.parsed_body).to eq([{
"from" => "2020-04-02T00:00:00.000+00:00",
"to" => "2020-04-04T00:00:00.000+00:00",
"from" => "2020-04-02",
"to" => "2020-04-04",
"value" => 1
}])
end
......@@ -112,8 +112,8 @@ RSpec.describe API::Analytics::ProjectDeploymentFrequency do
it 'returns the expected deployment frequencies' do
expect(response.parsed_body).to eq([{
"from" => "2020-02-01T00:00:00.000+00:00",
"to" => "2020-04-01T00:00:00.000+00:00",
"from" => "2020-02-01",
"to" => "2020-04-01",
"value" => 8
}])
end
......@@ -131,13 +131,13 @@ RSpec.describe API::Analytics::ProjectDeploymentFrequency do
it 'returns the expected deployment frequencies' do
expect(response.parsed_body).to eq([
{ "from" => "2020-02-01T00:00:00.000Z", "to" => "2020-03-01T00:00:00.000Z", "value" => 4 },
{ "from" => "2020-03-01T00:00:00.000Z", "to" => "2020-04-01T00:00:00.000Z", "value" => 4 }
{ "from" => "2020-02-01", "to" => "2020-03-01", "value" => 4 },
{ "from" => "2020-03-01", "to" => "2020-04-01", "value" => 4 }
])
end
end
context 'with params: from 2017 to 2019 by day' do
context 'with params: from 2020/02/01 to 2020/04/01 by day' do
let(:params) do
{
environment: prod.name,
......@@ -193,7 +193,7 @@ RSpec.describe API::Analytics::ProjectDeploymentFrequency do
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
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Analytics::Deployments::Frequency::AggregateService do
let_it_be(:group) { create(:group) }
let_it_be(:subgroup) { create(:group, parent: group) }
let_it_be(:group_project, refind: true) { create(:project, :repository, group: group) }
let_it_be(:subgroup_project, refind: true) { create(:project, :repository, group: subgroup) }
let_it_be(:developer) { create(:user) }
let_it_be(:guest) { create(:user) }
let(:container) { group_project }
let(:actor) { developer }
let(:service) { described_class.new(container: container, current_user: actor, params: params) }
let(:params) { { from: 4.days.ago.to_datetime } }
before_all do
group.add_developer(developer)
group.add_guest(guest)
end
before do
stub_licensed_features(dora4_analytics: true)
end
around do |example|
freeze_time { example.run }
end
describe '#execute' do
subject { service.execute }
let_it_be(:group_production) { create(:environment, project: group_project, name: 'production') }
let_it_be(:group_staging) { create(:environment, project: group_project, name: 'staging') }
let_it_be(:subgroup_production) { create(:environment, project: subgroup_project, name: 'production') }
let_it_be(:subgroup_staging) { create(:environment, project: subgroup_project, name: 'staging') }
before_all do
create(:deployment, :success, project: group_project, environment: group_production, finished_at: 7.days.ago)
create(:deployment, :failed, project: group_project, environment: group_production, finished_at: 3.days.ago)
create(:deployment, :success, project: group_project, environment: group_production, finished_at: 1.day.ago)
create(:deployment, :success, project: group_project, environment: group_staging, finished_at: 1.day.ago)
create(:deployment, :success, project: subgroup_project, environment: subgroup_production, finished_at: 7.days.ago)
create(:deployment, :failed, project: subgroup_project, environment: subgroup_production, finished_at: 3.days.ago)
create(:deployment, :success, project: subgroup_project, environment: subgroup_production, finished_at: 1.day.ago)
create(:deployment, :success, project: subgroup_project, environment: subgroup_staging, finished_at: 1.day.ago)
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 deployment frequencies' do
result = subject
expect(result[:status]).to eq(:success)
expect(result[:frequencies]).to eq(
[
{
from: params[:from],
to: DateTime.current,
value: 2
}
]
)
end
context 'when date range is specified' do
let(:params) { { from: 10.days.ago.to_datetime, to: 5.days.from_now.to_datetime } }
it 'returns deployment frequencies' do
result = subject
expect(result[:status]).to eq(:success)
expect(result[:frequencies]).to eq(
[
{
from: params[:from],
to: params[:to],
value: 3
}
]
)
end
end
context 'when environment name is specified' do
let(:params) { { from: 3.days.ago.to_datetime, environment: 'production' } }
it 'returns frequencies that related to production environment' do
result = subject
expect(result[:status]).to eq(:success)
expect(result[:frequencies]).to eq(
[
{
from: params[:from],
to: DateTime.current,
value: 1
}
]
)
end
end
context 'when the container is group' do
let(:container) { group }
it 'returns frequencies that related to production environment' do
result = subject
expect(result[:status]).to eq(:success)
expect(result[:frequencies]).to eq(
[
{
from: params[:from],
to: DateTime.current,
value: 4
}
]
)
end
end
context 'when parameter is empty' do
let(:params) { {} }
it_behaves_like 'validation error' do
let(:message) { 'Parameter `from` must be specified' }
end
end
context 'when start_date is eariler than end_date' 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 deployment frequencies' }
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 deployment frequencies' }
end
end
end
end
......@@ -9406,6 +9406,9 @@ msgstr ""
msgid "Date range cannot exceed %{maxDateRange} days."
msgstr ""
msgid "Date range is greater than %{quarter_days} days"
msgstr ""
msgid "Day of month"
msgstr ""
......@@ -21345,6 +21348,15 @@ msgstr ""
msgid "Parameter \"job_id\" cannot exceed length of %{job_id_max_size}"
msgstr ""
msgid "Parameter `from` must be specified"
msgstr ""
msgid "Parameter `interval` must be one of (\"%{valid_intervals}\")"
msgstr ""
msgid "Parameter `to` is before the `from` date"
msgstr ""
msgid "Parent"
msgstr ""
......@@ -33532,6 +33544,9 @@ msgstr ""
msgid "You do not have any subscriptions yet"
msgstr ""
msgid "You do not have permission to access deployment frequencies"
msgstr ""
msgid "You do not have permission to leave this %{namespaceType}."
msgstr ""
......
This diff is collapsed.
......@@ -396,6 +396,26 @@ RSpec.describe Deployment do
end
end
describe '.finished_before' do
let!(:deployment1) { create(:deployment, finished_at: 1.day.ago) }
let!(:deployment2) { create(:deployment, finished_at: Time.current) }
it 'filters deployments by finished_at' do
expect(described_class.finished_before(1.hour.ago))
.to eq([deployment1])
end
end
describe '.finished_after' do
let!(:deployment1) { create(:deployment, finished_at: 1.day.ago) }
let!(:deployment2) { create(:deployment, finished_at: Time.current) }
it 'filters deployments by finished_at' do
expect(described_class.finished_after(1.hour.ago))
.to eq([deployment2])
end
end
describe 'with_deployable' do
subject { described_class.with_deployable }
......@@ -408,22 +428,6 @@ RSpec.describe Deployment do
end
end
describe 'finished_between' do
subject { described_class.finished_between(start_time, end_time) }
let_it_be(:start_time) { DateTime.new(2017) }
let_it_be(:end_time) { DateTime.new(2019) }
let_it_be(:deployment_2016) { create(:deployment, finished_at: DateTime.new(2016)) }
let_it_be(:deployment_2017) { create(:deployment, finished_at: DateTime.new(2017)) }
let_it_be(:deployment_2018) { create(:deployment, finished_at: DateTime.new(2018)) }
let_it_be(:deployment_2019) { create(:deployment, finished_at: DateTime.new(2019)) }
let_it_be(:deployment_2020) { create(:deployment, finished_at: DateTime.new(2020)) }
it 'retrieves deployments that finished between the specified times' do
is_expected.to contain_exactly(deployment_2017, deployment_2018)
end
end
describe 'visible' do
subject { described_class.visible }
......
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