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 # frozen_string_literal: true
# WARNING: This finder does not check permissions!
#
# Arguments: # Arguments:
# params: # params:
# project: Project model - Find deployments for this project # project: Project model - Find deployments for this project
...@@ -27,11 +29,13 @@ class DeploymentsFinder ...@@ -27,11 +29,13 @@ class DeploymentsFinder
def execute def execute
items = init_collection items = init_collection
items = by_updated_at(items) items = by_updated_at(items)
items = by_finished_at(items)
items = by_environment(items) items = by_environment(items)
items = by_status(items) items = by_status(items)
items = preload_associations(items) items = preload_associations(items)
items = by_finished_between(items) items = sort(items)
sort(items)
items
end end
private private
...@@ -44,11 +48,9 @@ class DeploymentsFinder ...@@ -44,11 +48,9 @@ class DeploymentsFinder
end end
end end
# rubocop: disable CodeReuse/ActiveRecord
def sort(items) def sort(items)
items.order(sort_params) items.order(sort_params) # rubocop: disable CodeReuse/ActiveRecord
end end
# rubocop: enable CodeReuse/ActiveRecord
def by_updated_at(items) def by_updated_at(items)
items = items.updated_before(params[:updated_before]) if params[:updated_before].present? items = items.updated_before(params[:updated_before]) if params[:updated_before].present?
...@@ -57,6 +59,13 @@ class DeploymentsFinder ...@@ -57,6 +59,13 @@ class DeploymentsFinder
items items
end 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) def by_environment(items)
if params[:environment].present? if params[:environment].present?
items.for_environment_name(params[:environment]) items.for_environment_name(params[:environment])
...@@ -65,12 +74,6 @@ class DeploymentsFinder ...@@ -65,12 +74,6 @@ class DeploymentsFinder
end end
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) def by_status(items)
return items unless params[:status].present? return items unless params[:status].present?
......
...@@ -38,6 +38,7 @@ class Deployment < ApplicationRecord ...@@ -38,6 +38,7 @@ class Deployment < ApplicationRecord
scope :for_status, -> (status) { where(status: status) } scope :for_status, -> (status) { where(status: status) }
scope :for_project, -> (project_id) { where(project_id: project_id) } 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 :visible, -> { where(status: %i[running success failed canceled]) }
scope :stoppable, -> { where.not(on_stop: nil).where.not(deployable_id: nil).success } scope :stoppable, -> { where.not(on_stop: nil).where.not(deployable_id: nil).success }
...@@ -45,11 +46,8 @@ class Deployment < ApplicationRecord ...@@ -45,11 +46,8 @@ class Deployment < ApplicationRecord
scope :older_than, -> (deployment) { where('deployments.id < ?', deployment.id) } 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 :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 scope :finished_after, ->(date) { where('finished_at >= ?', date) }
selected = where('deployments.finished_at >= ?', start_date) scope :finished_before, ->(date) { where('finished_at < ?', date) }
selected = selected.where('deployments.finished_at < ?', end_date) if end_date
selected
end
FINISHED_STATUSES = %i[success failed canceled].freeze 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 redirect_to: 'dora4_project_analytics.md'
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
--- ---
# 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. <!-- 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 -->
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
}
]
```
...@@ -42,8 +42,8 @@ performance indicators for software development teams: ...@@ -42,8 +42,8 @@ performance indicators for software development teams:
production. production.
GitLab plans to add support for all the DORA4 metrics at the project and group levels. GitLab added 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) the first metric, deployment frequency, at the project and group scopes for [CI/CD charts](ci_cd_analytics.md#deployment-frequency-charts),
and the [API]( ../../api/project_analytics.md). the [Project API]( ../../api/dora4_project_analytics.md), and the [Group API]( ../../api/dora4_group_analytics.md).
## Deployment frequency charts **(ULTIMATE)** ## Deployment frequency charts **(ULTIMATE)**
......
...@@ -870,3 +870,13 @@ questions that you know someone might ask. ...@@ -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`. 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 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. --> 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: ...@@ -152,9 +152,9 @@ There are numerous [APIs](../../api/README.md) to use with your projects:
- [Traffic](../../api/project_statistics.md) - [Traffic](../../api/project_statistics.md)
- [Variables](../../api/project_level_variables.md) - [Variables](../../api/project_level_variables.md)
- [Aliases](../../api/project_aliases.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). > [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: ...@@ -162,4 +162,4 @@ Project details include the following analytics:
- Deployment Frequency - 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 # frozen_string_literal: true
# WARNING: This finder does not check permissions!
#
# Arguments: # Arguments:
# params: # params:
# group: Group model - Find deployments within a group (including subgroups) # group: Group model - Find deployments within a group (including subgroups)
......
...@@ -7,9 +7,9 @@ module EE ...@@ -7,9 +7,9 @@ module EE
override :should_render_deployment_frequency_charts override :should_render_deployment_frequency_charts
def 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 ::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 end
end end
...@@ -144,6 +144,7 @@ class License < ApplicationRecord ...@@ -144,6 +144,7 @@ class License < ApplicationRecord
dast dast
dependency_scanning dependency_scanning
devops_adoption devops_adoption
dora4_analytics
enforce_personal_access_token_expiration enforce_personal_access_token_expiration
enforce_ssh_key_expiration enforce_ssh_key_expiration
enterprise_templates enterprise_templates
...@@ -157,7 +158,6 @@ class License < ApplicationRecord ...@@ -157,7 +158,6 @@ class License < ApplicationRecord
jira_issue_association_enforcement jira_issue_association_enforcement
license_scanning license_scanning
personal_access_token_expiration_policy personal_access_token_expiration_policy
project_activity_analytics
prometheus_alerts prometheus_alerts
pseudonymizer pseudonymizer
quality_management quality_management
......
...@@ -37,6 +37,10 @@ module EE ...@@ -37,6 +37,10 @@ module EE
@subject.feature_available?(:group_activity_analytics) @subject.feature_available?(:group_activity_analytics)
end end
condition(:dora4_analytics_available) do
@subject.feature_available?(:dora4_analytics)
end
condition(:can_owners_manage_ldap, scope: :global) do condition(:can_owners_manage_ldap, scope: :global) do
::Gitlab::CurrentSettings.allow_group_owners_to_manage_ldap? ::Gitlab::CurrentSettings.allow_group_owners_to_manage_ldap?
end end
...@@ -160,6 +164,9 @@ module EE ...@@ -160,6 +164,9 @@ module EE
rule { has_access & group_activity_analytics_available } rule { has_access & group_activity_analytics_available }
.enable :read_group_activity_analytics .enable :read_group_activity_analytics
rule { reporter & dora4_analytics_available }
.enable :read_dora4_analytics
rule { reporter & group_repository_analytics_available } rule { reporter & group_repository_analytics_available }
.enable :read_group_repository_analytics .enable :read_group_repository_analytics
......
...@@ -52,8 +52,8 @@ module EE ...@@ -52,8 +52,8 @@ module EE
end end
with_scope :subject with_scope :subject
condition(:project_activity_analytics_available) do condition(:dora4_analytics_available) do
@subject.feature_available?(:project_activity_analytics) @subject.feature_available?(:dora4_analytics)
end end
condition(:project_merge_request_analytics_available) do condition(:project_merge_request_analytics_available) do
...@@ -382,8 +382,8 @@ module EE ...@@ -382,8 +382,8 @@ module EE
rule { can?(:read_merge_request) & code_review_analytics_enabled }.enable :read_code_review_analytics rule { can?(:read_merge_request) & code_review_analytics_enabled }.enable :read_code_review_analytics
rule { reporter & project_activity_analytics_available } rule { reporter & dora4_analytics_available }
.enable :read_project_activity_analytics .enable :read_dora4_analytics
rule { reporter & project_merge_request_analytics_available } rule { reporter & project_merge_request_analytics_available }
.enable :read_project_merge_request_analytics .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 ...@@ -4,12 +4,11 @@ module API
module Analytics module Analytics
class ProjectDeploymentFrequency < ::API::Base class ProjectDeploymentFrequency < ::API::Base
include Gitlab::Utils::StrongMemoize include Gitlab::Utils::StrongMemoize
include PaginationParams
QUARTER_DAYS = 3.months / 1.day QUARTER_DAYS = 3.months / 1.day
DEPLOYMENT_FREQUENCY_INTERVAL_ALL = 'all'.freeze DEPLOYMENT_FREQUENCY_INTERVAL_ALL = 'all'
DEPLOYMENT_FREQUENCY_INTERVAL_MONTHLY = 'monthly'.freeze DEPLOYMENT_FREQUENCY_INTERVAL_MONTHLY = 'monthly'
DEPLOYMENT_FREQUENCY_INTERVAL_DAILY = 'daily'.freeze DEPLOYMENT_FREQUENCY_INTERVAL_DAILY = 'daily'
DEPLOYMENT_FREQUENCY_DEFAULT_INTERVAL = DEPLOYMENT_FREQUENCY_INTERVAL_ALL DEPLOYMENT_FREQUENCY_DEFAULT_INTERVAL = DEPLOYMENT_FREQUENCY_INTERVAL_ALL
VALID_INTERVALS = [ VALID_INTERVALS = [
DEPLOYMENT_FREQUENCY_INTERVAL_ALL, DEPLOYMENT_FREQUENCY_INTERVAL_ALL,
...@@ -110,7 +109,7 @@ module API ...@@ -110,7 +109,7 @@ module API
get 'deployment_frequency' do get 'deployment_frequency' do
bad_request!("Parameter `to` is before the `from` date") if start_date > end_date 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 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 present deployment_frequencies, with: EE::API::Entities::Analytics::DeploymentFrequency
end end
end end
......
...@@ -43,6 +43,7 @@ module EE ...@@ -43,6 +43,7 @@ module EE
mount ::API::VisualReviewDiscussions mount ::API::VisualReviewDiscussions
mount ::API::Analytics::CodeReviewAnalytics mount ::API::Analytics::CodeReviewAnalytics
mount ::API::Analytics::GroupActivityAnalytics mount ::API::Analytics::GroupActivityAnalytics
mount ::API::Analytics::GroupDeploymentFrequency
mount ::API::Analytics::ProjectDeploymentFrequency mount ::API::Analytics::ProjectDeploymentFrequency
mount ::API::ProtectedEnvironments mount ::API::ProtectedEnvironments
mount ::API::ResourceWeightEvents mount ::API::ResourceWeightEvents
......
...@@ -5,9 +5,11 @@ module EE ...@@ -5,9 +5,11 @@ module EE
module Entities module Entities
module Analytics module Analytics
class DeploymentFrequency < Grape::Entity class DeploymentFrequency < Grape::Entity
format_with(:iso8601_date) { |datetime| datetime.to_date.iso8601 }
expose :value expose :value
expose :from expose :from, format_with: :iso8601_date
expose :to expose :to, format_with: :iso8601_date
end end
end end
end end
......
...@@ -3,18 +3,61 @@ ...@@ -3,18 +3,61 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe DeploymentsFinder do 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(:group) { create(:group) }
let_it_be(:subgroup) { create(:group, parent: 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) } with_them do
let_it_be(:project_in_subgroup) { create(:project, :repository, group: subgroup) } 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) } it 'avoids N+1 queries' do
let_it_be(:deployment_in_subgroup) { create(:deployment, status: :success, project: project_in_subgroup) } 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
end end
...@@ -30,7 +30,7 @@ RSpec.describe 'Project Analytics (JavaScript fixtures)' do ...@@ -30,7 +30,7 @@ RSpec.describe 'Project Analytics (JavaScript fixtures)' do
end end
before do before do
stub_licensed_features(project_activity_analytics: true) stub_licensed_features(dora4_analytics: true)
project.add_reporter(reporter) project.add_reporter(reporter)
sign_in(reporter) sign_in(reporter)
end end
......
...@@ -12,11 +12,11 @@ RSpec.describe EE::GraphHelper do ...@@ -12,11 +12,11 @@ RSpec.describe EE::GraphHelper do
let(:is_user_authorized) { true } let(:is_user_authorized) { true }
before do 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) stub_feature_flags(deployment_frequency_charts: is_flag_enabled)
self.instance_variable_set(:@current_user, current_user) self.instance_variable_set(:@current_user, current_user)
self.instance_variable_set(:@project, project) 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 end
shared_examples 'returns true' do shared_examples 'returns true' do
......
...@@ -184,6 +184,26 @@ RSpec.describe GroupPolicy do ...@@ -184,6 +184,26 @@ RSpec.describe GroupPolicy do
it { is_expected.not_to be_allowed(:read_group_contribution_analytics) } it { is_expected.not_to be_allowed(:read_group_contribution_analytics) }
end 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 context 'when group activity analytics is available' do
let(:current_user) { developer } let(:current_user) { developer }
......
...@@ -1388,24 +1388,24 @@ RSpec.describe ProjectPolicy do ...@@ -1388,24 +1388,24 @@ RSpec.describe ProjectPolicy do
it { is_expected.to be_disallowed(:read_group_timelogs) } it { is_expected.to be_disallowed(:read_group_timelogs) }
end end
context 'when project activity analytics is available' do context 'when dora4 analytics is available' do
let(:current_user) { developer } let(:current_user) { developer }
before do before do
stub_licensed_features(project_activity_analytics: true) stub_licensed_features(dora4_analytics: true)
end end
it { is_expected.to be_allowed(:read_project_activity_analytics) } it { is_expected.to be_allowed(:read_dora4_analytics) }
end end
context 'when project activity analytics is not available' do context 'when dora4 analytics is not available' do
let(:current_user) { developer } let(:current_user) { developer }
before do before do
stub_licensed_features(project_activity_analytics: false) stub_licensed_features(dora4_analytics: false)
end end
it { is_expected.not_to be_allowed(:read_project_activity_analytics) } it { is_expected.not_to be_allowed(:read_dora4_analytics) }
end end
describe ':read_code_review_analytics' do 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 ...@@ -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_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_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(:current_user) { reporter }
let(:params) { { from: Time.now, to: Time.now, interval: "all", environment: prod.name } } let(:params) { { from: Time.now, to: Time.now, interval: "all", environment: prod.name } }
let(:path) { api("/projects/#{project.id}/analytics/deployment_frequency", current_user) } let(:path) { api("/projects/#{project.id}/analytics/deployment_frequency", current_user) }
...@@ -50,7 +50,7 @@ RSpec.describe API::Analytics::ProjectDeploymentFrequency do ...@@ -50,7 +50,7 @@ RSpec.describe API::Analytics::ProjectDeploymentFrequency do
let(:request_time) { nil } let(:request_time) { nil }
before do before do
stub_licensed_features(project_activity_analytics: project_activity_analytics_enabled) stub_licensed_features(dora4_analytics: dora4_analytics_enabled)
if request_time if request_time
travel_to(request_time) { request } travel_to(request_time) { request }
...@@ -87,14 +87,14 @@ RSpec.describe API::Analytics::ProjectDeploymentFrequency do ...@@ -87,14 +87,14 @@ RSpec.describe API::Analytics::ProjectDeploymentFrequency do
end end
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(:request_time) { DateTime.new(2020, 4, 4) }
let(:params) { { environment: prod.name, from: DateTime.new(2020, 4, 2) } } let(:params) { { environment: prod.name, from: DateTime.new(2020, 4, 2) } }
it 'returns the expected deployment frequencies' do it 'returns the expected deployment frequencies' do
expect(response.parsed_body).to eq([{ expect(response.parsed_body).to eq([{
"from" => "2020-04-02T00:00:00.000+00:00", "from" => "2020-04-02",
"to" => "2020-04-04T00:00:00.000+00:00", "to" => "2020-04-04",
"value" => 1 "value" => 1
}]) }])
end end
...@@ -112,8 +112,8 @@ RSpec.describe API::Analytics::ProjectDeploymentFrequency do ...@@ -112,8 +112,8 @@ RSpec.describe API::Analytics::ProjectDeploymentFrequency do
it 'returns the expected deployment frequencies' do it 'returns the expected deployment frequencies' do
expect(response.parsed_body).to eq([{ expect(response.parsed_body).to eq([{
"from" => "2020-02-01T00:00:00.000+00:00", "from" => "2020-02-01",
"to" => "2020-04-01T00:00:00.000+00:00", "to" => "2020-04-01",
"value" => 8 "value" => 8
}]) }])
end end
...@@ -131,13 +131,13 @@ RSpec.describe API::Analytics::ProjectDeploymentFrequency do ...@@ -131,13 +131,13 @@ RSpec.describe API::Analytics::ProjectDeploymentFrequency do
it 'returns the expected deployment frequencies' do it 'returns the expected deployment frequencies' do
expect(response.parsed_body).to eq([ expect(response.parsed_body).to eq([
{ "from" => "2020-02-01T00:00:00.000Z", "to" => "2020-03-01T00:00:00.000Z", "value" => 4 }, { "from" => "2020-02-01", "to" => "2020-03-01", "value" => 4 },
{ "from" => "2020-03-01T00:00:00.000Z", "to" => "2020-04-01T00:00:00.000Z", "value" => 4 } { "from" => "2020-03-01", "to" => "2020-04-01", "value" => 4 }
]) ])
end end
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 let(:params) do
{ {
environment: prod.name, environment: prod.name,
...@@ -193,7 +193,7 @@ RSpec.describe API::Analytics::ProjectDeploymentFrequency do ...@@ -193,7 +193,7 @@ RSpec.describe API::Analytics::ProjectDeploymentFrequency do
end end
context 'when feature is not available in plan' do 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 context 'when user has access to the project' do
it 'returns `forbidden`' 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 "" ...@@ -9406,6 +9406,9 @@ msgstr ""
msgid "Date range cannot exceed %{maxDateRange} days." msgid "Date range cannot exceed %{maxDateRange} days."
msgstr "" msgstr ""
msgid "Date range is greater than %{quarter_days} days"
msgstr ""
msgid "Day of month" msgid "Day of month"
msgstr "" msgstr ""
...@@ -21345,6 +21348,15 @@ msgstr "" ...@@ -21345,6 +21348,15 @@ msgstr ""
msgid "Parameter \"job_id\" cannot exceed length of %{job_id_max_size}" msgid "Parameter \"job_id\" cannot exceed length of %{job_id_max_size}"
msgstr "" 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" msgid "Parent"
msgstr "" msgstr ""
...@@ -33532,6 +33544,9 @@ msgstr "" ...@@ -33532,6 +33544,9 @@ msgstr ""
msgid "You do not have any subscriptions yet" msgid "You do not have any subscriptions yet"
msgstr "" msgstr ""
msgid "You do not have permission to access deployment frequencies"
msgstr ""
msgid "You do not have permission to leave this %{namespaceType}." msgid "You do not have permission to leave this %{namespaceType}."
msgstr "" msgstr ""
......
...@@ -5,16 +5,8 @@ require 'spec_helper' ...@@ -5,16 +5,8 @@ require 'spec_helper'
RSpec.describe DeploymentsFinder do RSpec.describe DeploymentsFinder do
subject { described_class.new(params).execute } subject { described_class.new(params).execute }
let_it_be(:project) { create(:project, :public, :test_repo) }
let(:params) { { project: project } }
describe "#execute" do describe "#execute" do
it 'returns all deployments by default' do context 'when project or group is missing' do
deployments = create_list(:deployment, 2, :success, project: project)
is_expected.to match_array(deployments)
end
context 'when project is missing' do
let(:params) { {} } let(:params) { {} }
it 'returns nothing' do it 'returns nothing' do
...@@ -22,9 +14,13 @@ RSpec.describe DeploymentsFinder do ...@@ -22,9 +14,13 @@ RSpec.describe DeploymentsFinder do
end end
end end
context 'at project scope' do
let_it_be(:project) { create(:project, :public, :test_repo) }
let(:base_params) { { project: project } }
describe 'filtering' do describe 'filtering' do
context 'when updated_at filters are specified' do context 'when updated_at filters are specified' do
let(:params) { { project: project, updated_before: 1.day.ago, updated_after: 3.days.ago } } let(:params) { { **base_params, updated_before: 1.day.ago, updated_after: 3.days.ago } }
let!(:deployment_1) { create(:deployment, :success, project: project, updated_at: 2.days.ago) } let!(:deployment_1) { create(:deployment, :success, project: project, updated_at: 2.days.ago) }
let!(:deployment_2) { create(:deployment, :success, project: project, updated_at: 4.days.ago) } let!(:deployment_2) { create(:deployment, :success, project: project, updated_at: 4.days.ago) }
let!(:deployment_3) { create(:deployment, :success, project: project, updated_at: 1.hour.ago) } let!(:deployment_3) { create(:deployment, :success, project: project, updated_at: 1.hour.ago) }
...@@ -45,7 +41,7 @@ RSpec.describe DeploymentsFinder do ...@@ -45,7 +41,7 @@ RSpec.describe DeploymentsFinder do
create(:deployment, project: project, environment: environment2) create(:deployment, project: project, environment: environment2)
end end
let(:params) { { project: project, environment: environment1.name } } let(:params) { { **base_params, environment: environment1.name } }
it 'returns deployments for the given environment' do it 'returns deployments for the given environment' do
is_expected.to match_array([deployment1]) is_expected.to match_array([deployment1])
...@@ -55,7 +51,7 @@ RSpec.describe DeploymentsFinder do ...@@ -55,7 +51,7 @@ RSpec.describe DeploymentsFinder do
context 'when the deployment status is specified' do context 'when the deployment status is specified' do
let!(:deployment1) { create(:deployment, :success, project: project) } let!(:deployment1) { create(:deployment, :success, project: project) }
let!(:deployment2) { create(:deployment, :failed, project: project) } let!(:deployment2) { create(:deployment, :failed, project: project) }
let(:params) { { project: project, status: 'success' } } let(:params) { { **base_params, status: 'success' } }
it 'returns deployments for the given environment' do it 'returns deployments for the given environment' do
is_expected.to match_array([deployment1]) is_expected.to match_array([deployment1])
...@@ -63,48 +59,22 @@ RSpec.describe DeploymentsFinder do ...@@ -63,48 +59,22 @@ RSpec.describe DeploymentsFinder do
end end
context 'when using an invalid deployment status' do context 'when using an invalid deployment status' do
let(:params) { { project: project, status: 'kittens' } } let(:params) { { **base_params, status: 'kittens' } }
it 'raises ArgumentError' do it 'raises ArgumentError' do
expect { subject }.to raise_error(ArgumentError) expect { subject }.to raise_error(ArgumentError)
end end
end end
context 'when filtering by finished time' do
let!(:deployment_1) { create(:deployment, :success, project: project, finished_at: 2.days.ago) }
let!(:deployment_2) { create(:deployment, :success, project: project, finished_at: 4.days.ago) }
let!(:deployment_3) { create(:deployment, :success, project: project, finished_at: 5.hours.ago) }
context 'when filtering by finished_after and finished_before' do
let(:params) { { project: project, finished_after: 3.days.ago, finished_before: 1.day.ago } }
it { is_expected.to match_array([deployment_1]) }
end
context 'when the finished_before parameter is missing' do
let(:params) { { project: project, finished_after: 3.days.ago } }
it { is_expected.to match_array([deployment_1, deployment_3]) }
end
context 'when finished_after is missing' do
let(:params) { { project: project, finished_before: 1.day.ago } }
it 'does not apply any filters on finished time' do
is_expected.to match_array([deployment_1, deployment_2, deployment_3])
end
end
end
end end
describe 'ordering' do describe 'ordering' do
using RSpec::Parameterized::TableSyntax using RSpec::Parameterized::TableSyntax
let(:params) { { project: project, order_by: order_by, sort: sort } } let(:params) { { **base_params, order_by: order_by, sort: sort } }
let!(:deployment_1) { create(:deployment, :success, project: project, iid: 11, ref: 'master', created_at: 2.days.ago, updated_at: Time.now, finished_at: 3.hours.ago) } let!(:deployment_1) { create(:deployment, :success, project: project, iid: 11, ref: 'master', created_at: 2.days.ago, updated_at: Time.now, finished_at: Time.now) }
let!(:deployment_2) { create(:deployment, :success, project: project, iid: 12, ref: 'feature', created_at: 1.day.ago, updated_at: 2.hours.ago, finished_at: 1.hour.ago) } let!(:deployment_2) { create(:deployment, :success, project: project, iid: 12, ref: 'feature', created_at: 1.day.ago, updated_at: 2.hours.ago, finished_at: 2.hours.ago) }
let!(:deployment_3) { create(:deployment, :success, project: project, iid: 8, ref: 'video', created_at: Time.now, updated_at: 1.hour.ago, finished_at: 2.hours.ago) } let!(:deployment_3) { create(:deployment, :success, project: project, iid: 8, ref: 'video', created_at: Time.now, updated_at: 1.hour.ago, finished_at: 1.hour.ago) }
where(:order_by, :sort, :ordered_deployments) do where(:order_by, :sort, :ordered_deployments) do
'created_at' | 'asc' | [:deployment_1, :deployment_2, :deployment_3] 'created_at' | 'asc' | [:deployment_1, :deployment_2, :deployment_3]
...@@ -117,8 +87,8 @@ RSpec.describe DeploymentsFinder do ...@@ -117,8 +87,8 @@ RSpec.describe DeploymentsFinder do
'ref' | 'desc' | [:deployment_3, :deployment_1, :deployment_2] 'ref' | 'desc' | [:deployment_3, :deployment_1, :deployment_2]
'updated_at' | 'asc' | [:deployment_2, :deployment_3, :deployment_1] 'updated_at' | 'asc' | [:deployment_2, :deployment_3, :deployment_1]
'updated_at' | 'desc' | [:deployment_1, :deployment_3, :deployment_2] 'updated_at' | 'desc' | [:deployment_1, :deployment_3, :deployment_2]
'finished_at' | 'asc' | [:deployment_1, :deployment_3, :deployment_2] 'finished_at' | 'asc' | [:deployment_2, :deployment_3, :deployment_1]
'finished_at' | 'desc' | [:deployment_2, :deployment_3, :deployment_1] 'finished_at' | 'desc' | [:deployment_1, :deployment_3, :deployment_2]
'invalid' | 'asc' | [:deployment_1, :deployment_2, :deployment_3] 'invalid' | 'asc' | [:deployment_1, :deployment_2, :deployment_3]
'iid' | 'err' | [:deployment_3, :deployment_1, :deployment_2] 'iid' | 'err' | [:deployment_3, :deployment_1, :deployment_2]
end end
...@@ -131,7 +101,7 @@ RSpec.describe DeploymentsFinder do ...@@ -131,7 +101,7 @@ RSpec.describe DeploymentsFinder do
end end
describe 'transform `created_at` sorting to `id` sorting' do describe 'transform `created_at` sorting to `id` sorting' do
let(:params) { { project: project, order_by: 'created_at', sort: 'asc' } } let(:params) { { **base_params, order_by: 'created_at', sort: 'asc' } }
it 'sorts by only one column' do it 'sorts by only one column' do
expect(subject.order_values.size).to eq(1) expect(subject.order_values.size).to eq(1)
...@@ -142,8 +112,8 @@ RSpec.describe DeploymentsFinder do ...@@ -142,8 +112,8 @@ RSpec.describe DeploymentsFinder do
end end
end end
describe 'tie-breaker for `updated_at` sorting' do describe 'tie-breaker for `finished_at` sorting' do
let(:params) { { project: project, order_by: 'updated_at', sort: 'asc' } } let(:params) { { **base_params, order_by: 'updated_at', sort: 'asc' } }
it 'sorts by two columns' do it 'sorts by two columns' do
expect(subject.order_values.size).to eq(2) expect(subject.order_values.size).to eq(2)
...@@ -165,5 +135,30 @@ RSpec.describe DeploymentsFinder do ...@@ -165,5 +135,30 @@ RSpec.describe DeploymentsFinder do
expect(subject).to eq([deployment_3, deployment_2, deployment_1]) expect(subject).to eq([deployment_3, deployment_2, deployment_1])
end end
end end
context 'when filtering by finished time' do
let!(:deployment_1) { create(:deployment, :success, project: project, finished_at: 2.days.ago) }
let!(:deployment_2) { create(:deployment, :success, project: project, finished_at: 4.days.ago) }
let!(:deployment_3) { create(:deployment, :success, project: project, finished_at: 5.hours.ago) }
context 'when filtering by finished_after and finished_before' do
let(:params) { { **base_params, finished_after: 3.days.ago, finished_before: 1.day.ago } }
it { is_expected.to match_array([deployment_1]) }
end
context 'when the finished_before parameter is missing' do
let(:params) { { **base_params, finished_after: 3.days.ago } }
it { is_expected.to match_array([deployment_1, deployment_3]) }
end
context 'when finished_after is missing' do
let(:params) { { **base_params, finished_before: 3.days.ago } }
it { is_expected.to match_array([deployment_2]) }
end
end
end
end end
end end
...@@ -396,6 +396,26 @@ RSpec.describe Deployment do ...@@ -396,6 +396,26 @@ RSpec.describe Deployment do
end end
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 describe 'with_deployable' do
subject { described_class.with_deployable } subject { described_class.with_deployable }
...@@ -408,22 +428,6 @@ RSpec.describe Deployment do ...@@ -408,22 +428,6 @@ RSpec.describe Deployment do
end end
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 describe 'visible' do
subject { described_class.visible } 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