Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
7c862041c6
commit
f607152a08
|
@ -9,6 +9,7 @@ module Analytics
|
||||||
belongs_to :project
|
belongs_to :project
|
||||||
|
|
||||||
alias_attribute :parent, :project
|
alias_attribute :parent, :project
|
||||||
|
alias_attribute :parent_id, :project_id
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -47,11 +47,17 @@ module Analytics
|
||||||
!custom
|
!custom
|
||||||
end
|
end
|
||||||
|
|
||||||
# The model that is going to be queried, Issue or MergeRequest
|
# The model class that is going to be queried, Issue or MergeRequest
|
||||||
def subject_model
|
def subject_class
|
||||||
start_event.object_type
|
start_event.object_type
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def matches_with_stage_params?(stage_params)
|
||||||
|
default_stage? &&
|
||||||
|
start_event_identifier.to_s.eql?(stage_params[:start_event_identifier].to_s) &&
|
||||||
|
end_event_identifier.to_s.eql?(stage_params[:end_event_identifier].to_s)
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def validate_stage_event_pairs
|
def validate_stage_event_pairs
|
||||||
|
|
|
@ -591,3 +591,5 @@ class MergeRequestDiff < ApplicationRecord
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
MergeRequestDiff.prepend_if_ee('EE::MergeRequestDiff')
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class IndexTimestampColumnsForIssueMetrics < ActiveRecord::Migration[5.2]
|
||||||
|
include Gitlab::Database::MigrationHelpers
|
||||||
|
|
||||||
|
DOWNTIME = false
|
||||||
|
|
||||||
|
disable_ddl_transaction!
|
||||||
|
|
||||||
|
def up
|
||||||
|
add_concurrent_index(*index_arguments)
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
remove_concurrent_index(*index_arguments)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def index_arguments
|
||||||
|
[
|
||||||
|
:issue_metrics,
|
||||||
|
[:issue_id, :first_mentioned_in_commit_at, :first_associated_with_milestone_at, :first_added_to_board_at],
|
||||||
|
{
|
||||||
|
name: 'index_issue_metrics_on_issue_id_and_timestamps'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,29 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class IndexTimestampColumnsForMergeRequestsCreationDate < ActiveRecord::Migration[5.2]
|
||||||
|
include Gitlab::Database::MigrationHelpers
|
||||||
|
|
||||||
|
DOWNTIME = false
|
||||||
|
|
||||||
|
disable_ddl_transaction!
|
||||||
|
|
||||||
|
def up
|
||||||
|
add_concurrent_index(*index_arguments)
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
remove_concurrent_index(*index_arguments)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def index_arguments
|
||||||
|
[
|
||||||
|
:merge_requests,
|
||||||
|
[:target_project_id, :created_at],
|
||||||
|
{
|
||||||
|
name: 'index_merge_requests_target_project_id_created_at'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
end
|
||||||
|
end
|
|
@ -1816,6 +1816,7 @@ ActiveRecord::Schema.define(version: 2019_10_04_134055) do
|
||||||
t.datetime "first_added_to_board_at"
|
t.datetime "first_added_to_board_at"
|
||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
t.datetime "updated_at", null: false
|
t.datetime "updated_at", null: false
|
||||||
|
t.index ["issue_id", "first_mentioned_in_commit_at", "first_associated_with_milestone_at", "first_added_to_board_at"], name: "index_issue_metrics_on_issue_id_and_timestamps"
|
||||||
t.index ["issue_id"], name: "index_issue_metrics"
|
t.index ["issue_id"], name: "index_issue_metrics"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -2226,6 +2227,7 @@ ActiveRecord::Schema.define(version: 2019_10_04_134055) do
|
||||||
t.index ["source_project_id", "source_branch"], name: "index_merge_requests_on_source_project_id_and_source_branch"
|
t.index ["source_project_id", "source_branch"], name: "index_merge_requests_on_source_project_id_and_source_branch"
|
||||||
t.index ["state", "merge_status"], name: "index_merge_requests_on_state_and_merge_status", where: "(((state)::text = 'opened'::text) AND ((merge_status)::text = 'can_be_merged'::text))"
|
t.index ["state", "merge_status"], name: "index_merge_requests_on_state_and_merge_status", where: "(((state)::text = 'opened'::text) AND ((merge_status)::text = 'can_be_merged'::text))"
|
||||||
t.index ["target_branch"], name: "index_merge_requests_on_target_branch"
|
t.index ["target_branch"], name: "index_merge_requests_on_target_branch"
|
||||||
|
t.index ["target_project_id", "created_at"], name: "index_merge_requests_target_project_id_created_at"
|
||||||
t.index ["target_project_id", "iid"], name: "index_merge_requests_on_target_project_id_and_iid", unique: true
|
t.index ["target_project_id", "iid"], name: "index_merge_requests_on_target_project_id_and_iid", unique: true
|
||||||
t.index ["target_project_id", "iid"], name: "index_merge_requests_on_target_project_id_and_iid_opened", where: "((state)::text = 'opened'::text)"
|
t.index ["target_project_id", "iid"], name: "index_merge_requests_on_target_project_id_and_iid_opened", where: "((state)::text = 'opened'::text)"
|
||||||
t.index ["target_project_id", "merge_commit_sha", "id"], name: "index_merge_requests_on_tp_id_and_merge_commit_sha_and_id"
|
t.index ["target_project_id", "merge_commit_sha", "id"], name: "index_merge_requests_on_tp_id_and_merge_commit_sha_and_id"
|
||||||
|
|
|
@ -44,7 +44,7 @@ For instance, consider the following workflow:
|
||||||
First of all, you need to define a job in your `.gitlab-ci.yml` file that generates the
|
First of all, you need to define a job in your `.gitlab-ci.yml` file that generates the
|
||||||
[Performance report artifact](../../../ci/yaml/README.md#artifactsreportsperformance-premium).
|
[Performance report artifact](../../../ci/yaml/README.md#artifactsreportsperformance-premium).
|
||||||
For more information on how the Performance job should look like, check the
|
For more information on how the Performance job should look like, check the
|
||||||
example on [Testing Browser Performance](../../../ci/examples/browser_performance.md).
|
example on [Configuring Browser Performance Testing](#configuring-browser-performance-testing).
|
||||||
|
|
||||||
GitLab then checks this report, compares key performance metrics for each page
|
GitLab then checks this report, compares key performance metrics for each page
|
||||||
between the source and target branches, and shows the information right on the merge request.
|
between the source and target branches, and shows the information right on the merge request.
|
||||||
|
@ -60,11 +60,6 @@ report will be shown properly.
|
||||||
|
|
||||||
## Configuring Browser Performance Testing
|
## Configuring Browser Performance Testing
|
||||||
|
|
||||||
NOTE: **Note:**
|
|
||||||
The job definition shown below is supported in GitLab 11.5 and later versions.
|
|
||||||
It also requires GitLab Runner 11.5 or later. For earlier versions, use the
|
|
||||||
[previous job definitions](#previous-job-definitions).
|
|
||||||
|
|
||||||
This example shows how to run the [sitespeed.io container](https://hub.docker.com/r/sitespeedio/sitespeed.io/)
|
This example shows how to run the [sitespeed.io container](https://hub.docker.com/r/sitespeedio/sitespeed.io/)
|
||||||
on your code by using GitLab CI/CD and [sitespeed.io](https://www.sitespeed.io)
|
on your code by using GitLab CI/CD and [sitespeed.io](https://www.sitespeed.io)
|
||||||
using Docker-in-Docker.
|
using Docker-in-Docker.
|
||||||
|
@ -73,29 +68,35 @@ First, you need GitLab Runner with
|
||||||
[docker-in-docker build](../../../ci/docker/using_docker_build.md#use-docker-in-docker-workflow-with-docker-executor).
|
[docker-in-docker build](../../../ci/docker/using_docker_build.md#use-docker-in-docker-workflow-with-docker-executor).
|
||||||
|
|
||||||
Once you set up the Runner, add a new job to `.gitlab-ci.yml` that generates the
|
Once you set up the Runner, add a new job to `.gitlab-ci.yml` that generates the
|
||||||
expected report:
|
expected report.
|
||||||
|
|
||||||
|
For GitLab 12.4 and later, to define the `performance` job, you must
|
||||||
|
[include](../../../ci/yaml/README.md#includetemplate) the
|
||||||
|
[`Browser-Performance.gitlab-ci.yml` template](https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Verify/Browser-Performance.gitlab-ci.yml)
|
||||||
|
that's provided as a part of your GitLab installation.
|
||||||
|
For GitLab versions earlier than 12.4, you can copy and use the job as defined
|
||||||
|
in that template.
|
||||||
|
|
||||||
|
CAUTION: **Caution:**
|
||||||
|
The job definition provided by the template does not support Kubernetes yet. For a complete example of a more complex setup
|
||||||
|
that works in Kubernetes, see [here](https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Jobs/Browser-Performance-Testing.gitlab-ci.yml).
|
||||||
|
|
||||||
|
Add the following to your `.gitlab-ci.yml` file:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
|
include:
|
||||||
|
template: Verify/Browser-Performance.gitlab-ci.yml
|
||||||
|
|
||||||
performance:
|
performance:
|
||||||
stage: performance
|
|
||||||
image: docker:git
|
|
||||||
variables:
|
variables:
|
||||||
URL: https://example.com
|
URL: https://example.com
|
||||||
services:
|
|
||||||
- docker:stable-dind
|
|
||||||
script:
|
|
||||||
- mkdir gitlab-exporter
|
|
||||||
- wget -O ./gitlab-exporter/index.js https://gitlab.com/gitlab-org/gl-performance/raw/master/index.js
|
|
||||||
- mkdir sitespeed-results
|
|
||||||
- docker run --shm-size=1g --rm -v "$(pwd)":/sitespeed.io sitespeedio/sitespeed.io:6.3.1 --plugins.add ./gitlab-exporter --outputFolder sitespeed-results $URL
|
|
||||||
- mv sitespeed-results/data/performance.json performance.json
|
|
||||||
artifacts:
|
|
||||||
paths:
|
|
||||||
- sitespeed-results/
|
|
||||||
reports:
|
|
||||||
performance: performance.json
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
CAUTION: **Caution:**
|
||||||
|
The job definition provided by the template is supported in GitLab 11.5 and later versions.
|
||||||
|
It also requires GitLab Runner 11.5 or later. For earlier versions, use the
|
||||||
|
[previous job definitions](#previous-job-definitions).
|
||||||
|
|
||||||
The above example will create a `performance` job in your CI/CD pipeline and will run
|
The above example will create a `performance` job in your CI/CD pipeline and will run
|
||||||
sitespeed.io against the webpage you defined in `URL` to gather key metrics.
|
sitespeed.io against the webpage you defined in `URL` to gather key metrics.
|
||||||
The [GitLab plugin for sitespeed.io](https://gitlab.com/gitlab-org/gl-performance)
|
The [GitLab plugin for sitespeed.io](https://gitlab.com/gitlab-org/gl-performance)
|
||||||
|
@ -106,6 +107,20 @@ take the latest Performance artifact available.
|
||||||
The full HTML sitespeed.io report will also be saved as an artifact, and if you have
|
The full HTML sitespeed.io report will also be saved as an artifact, and if you have
|
||||||
[GitLab Pages](../pages/index.md) enabled, it can be viewed directly in your browser.
|
[GitLab Pages](../pages/index.md) enabled, it can be viewed directly in your browser.
|
||||||
|
|
||||||
|
It is also possible to customize options by setting the `SITESPEED_OPTIONS` variable.
|
||||||
|
For example, this is how to override the number of runs sitespeed.io
|
||||||
|
will make on the given URL:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
include:
|
||||||
|
template: Verify/Browser-Performance.gitlab-ci.yml
|
||||||
|
|
||||||
|
performance:
|
||||||
|
variables:
|
||||||
|
URL: https://example.com
|
||||||
|
SITESPEED_OPTIONS: -n 5
|
||||||
|
```
|
||||||
|
|
||||||
For further customization options for sitespeed.io, including the ability to provide a
|
For further customization options for sitespeed.io, including the ability to provide a
|
||||||
list of URLs to test, please see the [Sitespeed.io Configuration](https://www.sitespeed.io/documentation/sitespeed.io/configuration/)
|
list of URLs to test, please see the [Sitespeed.io Configuration](https://www.sitespeed.io/documentation/sitespeed.io/configuration/)
|
||||||
documentation.
|
documentation.
|
||||||
|
@ -126,8 +141,9 @@ set this up:
|
||||||
as an artifact is as simple as `echo $CI_ENVIRONMENT_URL > environment_url.txt`
|
as an artifact is as simple as `echo $CI_ENVIRONMENT_URL > environment_url.txt`
|
||||||
in your job's `script`.
|
in your job's `script`.
|
||||||
1. In the `performance` job, read the previous artifact into an environment
|
1. In the `performance` job, read the previous artifact into an environment
|
||||||
variable, like `$CI_ENVIRONMENT_URL`, and use it to parameterize the test
|
variable, in this case `$URL` because this is what our sitespeed.io command
|
||||||
URLs.
|
uses for the URL parameter. Because Review App URLs are dynamic, we define
|
||||||
|
the `URL` variable through `before_script` instead of `variables`.
|
||||||
1. You can now run the sitespeed.io container against the desired hostname and
|
1. You can now run the sitespeed.io container against the desired hostname and
|
||||||
paths.
|
paths.
|
||||||
|
|
||||||
|
@ -138,6 +154,9 @@ stages:
|
||||||
- deploy
|
- deploy
|
||||||
- performance
|
- performance
|
||||||
|
|
||||||
|
include:
|
||||||
|
template: Verify/Browser-Performance.gitlab-ci.yml
|
||||||
|
|
||||||
review:
|
review:
|
||||||
stage: deploy
|
stage: deploy
|
||||||
environment:
|
environment:
|
||||||
|
@ -155,28 +174,12 @@ review:
|
||||||
- master
|
- master
|
||||||
|
|
||||||
performance:
|
performance:
|
||||||
stage: performance
|
|
||||||
image: docker:git
|
|
||||||
services:
|
|
||||||
- docker:stable-dind
|
|
||||||
dependencies:
|
dependencies:
|
||||||
- review
|
- review
|
||||||
script:
|
before_script:
|
||||||
- export CI_ENVIRONMENT_URL=$(cat environment_url.txt)
|
- export URL=$(cat environment_url.txt)
|
||||||
- mkdir gitlab-exporter
|
|
||||||
- wget -O ./gitlab-exporter/index.js https://gitlab.com/gitlab-org/gl-performance/raw/master/index.js
|
|
||||||
- mkdir sitespeed-results
|
|
||||||
- docker run --shm-size=1g --rm -v "$(pwd)":/sitespeed.io sitespeedio/sitespeed.io:6.3.1 --plugins.add ./gitlab-exporter --outputFolder sitespeed-results "$CI_ENVIRONMENT_URL"
|
|
||||||
- mv sitespeed-results/data/performance.json performance.json
|
|
||||||
artifacts:
|
|
||||||
paths:
|
|
||||||
- sitespeed-results/
|
|
||||||
reports:
|
|
||||||
performance: performance.json
|
|
||||||
```
|
```
|
||||||
|
|
||||||
A complete example can be found in our [Auto DevOps CI YML](https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml).
|
|
||||||
|
|
||||||
### Previous job definitions
|
### Previous job definitions
|
||||||
|
|
||||||
CAUTION: **Caution:**
|
CAUTION: **Caution:**
|
||||||
|
|
|
@ -0,0 +1,70 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Gitlab
|
||||||
|
module Analytics
|
||||||
|
module CycleAnalytics
|
||||||
|
class BaseQueryBuilder
|
||||||
|
include Gitlab::CycleAnalytics::MetricsTables
|
||||||
|
|
||||||
|
delegate :subject_class, to: :stage
|
||||||
|
|
||||||
|
# rubocop: disable CodeReuse/ActiveRecord
|
||||||
|
|
||||||
|
def initialize(stage:, params: {})
|
||||||
|
@stage = stage
|
||||||
|
@params = params
|
||||||
|
end
|
||||||
|
|
||||||
|
def build
|
||||||
|
query = subject_class
|
||||||
|
query = filter_by_parent_model(query)
|
||||||
|
query = filter_by_time_range(query)
|
||||||
|
query = stage.start_event.apply_query_customization(query)
|
||||||
|
query = stage.end_event.apply_query_customization(query)
|
||||||
|
query.where(duration_condition)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
attr_reader :stage, :params
|
||||||
|
|
||||||
|
def duration_condition
|
||||||
|
stage.end_event.timestamp_projection.gteq(stage.start_event.timestamp_projection)
|
||||||
|
end
|
||||||
|
|
||||||
|
def filter_by_parent_model(query)
|
||||||
|
if parent_class.eql?(Project)
|
||||||
|
if subject_class.eql?(Issue)
|
||||||
|
query.where(project_id: stage.parent_id)
|
||||||
|
elsif subject_class.eql?(MergeRequest)
|
||||||
|
query.where(target_project_id: stage.parent_id)
|
||||||
|
else
|
||||||
|
raise ArgumentError, "unknown subject_class: #{subject_class}"
|
||||||
|
end
|
||||||
|
else
|
||||||
|
raise ArgumentError, "unknown parent_class: #{parent_class}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def filter_by_time_range(query)
|
||||||
|
from = params.fetch(:from, 30.days.ago)
|
||||||
|
to = params[:to]
|
||||||
|
|
||||||
|
query = query.where(subject_table[:created_at].gteq(from))
|
||||||
|
query = query.where(subject_table[:created_at].lteq(to)) if to
|
||||||
|
query
|
||||||
|
end
|
||||||
|
|
||||||
|
def subject_table
|
||||||
|
subject_class.arel_table
|
||||||
|
end
|
||||||
|
|
||||||
|
def parent_class
|
||||||
|
stage.parent.class
|
||||||
|
end
|
||||||
|
|
||||||
|
# rubocop: enable CodeReuse/ActiveRecord
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,42 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Gitlab
|
||||||
|
module Analytics
|
||||||
|
module CycleAnalytics
|
||||||
|
# Arguments:
|
||||||
|
# stage - an instance of CycleAnalytics::ProjectStage or CycleAnalytics::GroupStage
|
||||||
|
# params:
|
||||||
|
# current_user: an instance of User
|
||||||
|
# from: DateTime
|
||||||
|
# to: DateTime
|
||||||
|
class DataCollector
|
||||||
|
include Gitlab::Utils::StrongMemoize
|
||||||
|
|
||||||
|
def initialize(stage:, params: {})
|
||||||
|
@stage = stage
|
||||||
|
@params = params
|
||||||
|
end
|
||||||
|
|
||||||
|
def records_fetcher
|
||||||
|
strong_memoize(:records_fetcher) do
|
||||||
|
RecordsFetcher.new(stage: stage, query: query, params: params)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def median
|
||||||
|
strong_memoize(:median) do
|
||||||
|
Median.new(stage: stage, query: query)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
attr_reader :stage, :params
|
||||||
|
|
||||||
|
def query
|
||||||
|
BaseQueryBuilder.new(stage: stage, params: params).build
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -92,8 +92,8 @@ module Gitlab
|
||||||
name: 'production',
|
name: 'production',
|
||||||
custom: false,
|
custom: false,
|
||||||
relative_position: 7,
|
relative_position: 7,
|
||||||
start_event_identifier: :merge_request_merged,
|
start_event_identifier: :issue_created,
|
||||||
end_event_identifier: :merge_request_first_deployed_to_production
|
end_event_identifier: :production_stage_end
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Gitlab
|
||||||
|
module Analytics
|
||||||
|
module CycleAnalytics
|
||||||
|
class Median
|
||||||
|
include StageQueryHelpers
|
||||||
|
|
||||||
|
def initialize(stage:, query:)
|
||||||
|
@stage = stage
|
||||||
|
@query = query
|
||||||
|
end
|
||||||
|
|
||||||
|
def seconds
|
||||||
|
@query = @query.select(median_duration_in_seconds.as('median'))
|
||||||
|
result = execute_query(@query).first || {}
|
||||||
|
|
||||||
|
result['median'] ? result['median'].to_i : nil
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
attr_reader :stage
|
||||||
|
|
||||||
|
def percentile_cont
|
||||||
|
percentile_cont_ordering = Arel::Nodes::UnaryOperation.new(Arel::Nodes::SqlLiteral.new('ORDER BY'), duration)
|
||||||
|
Arel::Nodes::NamedFunction.new(
|
||||||
|
'percentile_cont(0.5) WITHIN GROUP',
|
||||||
|
[percentile_cont_ordering]
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def median_duration_in_seconds
|
||||||
|
Arel::Nodes::Extract.new(percentile_cont, :epoch)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,132 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Gitlab
|
||||||
|
module Analytics
|
||||||
|
module CycleAnalytics
|
||||||
|
class RecordsFetcher
|
||||||
|
include Gitlab::Utils::StrongMemoize
|
||||||
|
include StageQueryHelpers
|
||||||
|
include Gitlab::CycleAnalytics::MetricsTables
|
||||||
|
|
||||||
|
MAX_RECORDS = 20
|
||||||
|
|
||||||
|
MAPPINGS = {
|
||||||
|
Issue => {
|
||||||
|
finder_class: IssuesFinder,
|
||||||
|
serializer_class: AnalyticsIssueSerializer,
|
||||||
|
includes_for_query: { project: [:namespace], author: [] },
|
||||||
|
columns_for_select: %I[title iid id created_at author_id project_id]
|
||||||
|
},
|
||||||
|
MergeRequest => {
|
||||||
|
finder_class: MergeRequestsFinder,
|
||||||
|
serializer_class: AnalyticsMergeRequestSerializer,
|
||||||
|
includes_for_query: { target_project: [:namespace], author: [] },
|
||||||
|
columns_for_select: %I[title iid id created_at author_id state target_project_id]
|
||||||
|
}
|
||||||
|
}.freeze
|
||||||
|
|
||||||
|
delegate :subject_class, to: :stage
|
||||||
|
|
||||||
|
def initialize(stage:, query:, params: {})
|
||||||
|
@stage = stage
|
||||||
|
@query = query
|
||||||
|
@params = params
|
||||||
|
end
|
||||||
|
|
||||||
|
def serialized_records
|
||||||
|
strong_memoize(:serialized_records) do
|
||||||
|
# special case (legacy): 'Test' and 'Staging' stages should show Ci::Build records
|
||||||
|
if default_test_stage? || default_staging_stage?
|
||||||
|
AnalyticsBuildSerializer.new.represent(ci_build_records.map { |e| e['build'] })
|
||||||
|
else
|
||||||
|
records.map do |record|
|
||||||
|
project = record.project
|
||||||
|
attributes = record.attributes.merge({
|
||||||
|
project_path: project.path,
|
||||||
|
namespace_path: project.namespace.path,
|
||||||
|
author: record.author
|
||||||
|
})
|
||||||
|
serializer.represent(attributes)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
attr_reader :stage, :query, :params
|
||||||
|
|
||||||
|
def finder_query
|
||||||
|
MAPPINGS
|
||||||
|
.fetch(subject_class)
|
||||||
|
.fetch(:finder_class)
|
||||||
|
.new(params.fetch(:current_user), finder_params.fetch(stage.parent.class))
|
||||||
|
.execute
|
||||||
|
end
|
||||||
|
|
||||||
|
def columns
|
||||||
|
MAPPINGS.fetch(subject_class).fetch(:columns_for_select).map do |column_name|
|
||||||
|
subject_class.arel_table[column_name]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# EE will override this to include Group rules
|
||||||
|
def finder_params
|
||||||
|
{
|
||||||
|
Project => { project_id: stage.parent_id }
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def default_test_stage?
|
||||||
|
stage.matches_with_stage_params?(Gitlab::Analytics::CycleAnalytics::DefaultStages.params_for_test_stage)
|
||||||
|
end
|
||||||
|
|
||||||
|
def default_staging_stage?
|
||||||
|
stage.matches_with_stage_params?(Gitlab::Analytics::CycleAnalytics::DefaultStages.params_for_staging_stage)
|
||||||
|
end
|
||||||
|
|
||||||
|
def serializer
|
||||||
|
MAPPINGS.fetch(subject_class).fetch(:serializer_class).new
|
||||||
|
end
|
||||||
|
|
||||||
|
# Loading Ci::Build records instead of MergeRequest records
|
||||||
|
# rubocop: disable CodeReuse/ActiveRecord
|
||||||
|
def ci_build_records
|
||||||
|
ci_build_join = mr_metrics_table
|
||||||
|
.join(build_table)
|
||||||
|
.on(mr_metrics_table[:pipeline_id].eq(build_table[:commit_id]))
|
||||||
|
.join_sources
|
||||||
|
|
||||||
|
q = ordered_and_limited_query
|
||||||
|
.joins(ci_build_join)
|
||||||
|
.select(build_table[:id], round_duration_to_seconds.as('total_time'))
|
||||||
|
|
||||||
|
results = execute_query(q).to_a
|
||||||
|
|
||||||
|
Gitlab::CycleAnalytics::Updater.update!(results, from: 'id', to: 'build', klass: ::Ci::Build.includes({ project: [:namespace], user: [], pipeline: [] }))
|
||||||
|
end
|
||||||
|
|
||||||
|
def ordered_and_limited_query
|
||||||
|
query
|
||||||
|
.reorder(stage.end_event.timestamp_projection.desc)
|
||||||
|
.limit(MAX_RECORDS)
|
||||||
|
end
|
||||||
|
|
||||||
|
def records
|
||||||
|
results = finder_query
|
||||||
|
.merge(ordered_and_limited_query)
|
||||||
|
.select(*columns, round_duration_to_seconds.as('total_time'))
|
||||||
|
|
||||||
|
# using preloader instead of includes to avoid AR generating a large column list
|
||||||
|
ActiveRecord::Associations::Preloader.new.preload(
|
||||||
|
results,
|
||||||
|
MAPPINGS.fetch(subject_class).fetch(:includes_for_query)
|
||||||
|
)
|
||||||
|
|
||||||
|
results
|
||||||
|
end
|
||||||
|
# rubocop: enable CodeReuse/ActiveRecord
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,28 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Gitlab
|
||||||
|
module Analytics
|
||||||
|
module CycleAnalytics
|
||||||
|
module StageQueryHelpers
|
||||||
|
def execute_query(query)
|
||||||
|
ActiveRecord::Base.connection.execute(query.to_sql)
|
||||||
|
end
|
||||||
|
|
||||||
|
def zero_interval
|
||||||
|
Arel::Nodes::NamedFunction.new("CAST", [Arel.sql("'0' AS INTERVAL")])
|
||||||
|
end
|
||||||
|
|
||||||
|
def round_duration_to_seconds
|
||||||
|
Arel::Nodes::Extract.new(duration, :epoch)
|
||||||
|
end
|
||||||
|
|
||||||
|
def duration
|
||||||
|
Arel::Nodes::Subtraction.new(
|
||||||
|
stage.end_event.timestamp_projection,
|
||||||
|
stage.start_event.timestamp_projection
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,29 @@
|
||||||
|
# Read more about the feature here: https://docs.gitlab.com/ee/user/project/merge_requests/browser_performance_testing.html
|
||||||
|
|
||||||
|
stages:
|
||||||
|
- build
|
||||||
|
- test
|
||||||
|
- deploy
|
||||||
|
- performance
|
||||||
|
|
||||||
|
performance:
|
||||||
|
stage: performance
|
||||||
|
image: docker:git
|
||||||
|
variables:
|
||||||
|
URL: https://example.com
|
||||||
|
SITESPEED_VERSION: 6.3.1
|
||||||
|
SITESPEED_OPTIONS: ''
|
||||||
|
services:
|
||||||
|
- docker:stable-dind
|
||||||
|
script:
|
||||||
|
- mkdir gitlab-exporter
|
||||||
|
- wget -O ./gitlab-exporter/index.js https://gitlab.com/gitlab-org/gl-performance/raw/master/index.js
|
||||||
|
- mkdir sitespeed-results
|
||||||
|
- docker run --shm-size=1g --rm -v "$(pwd)":/sitespeed.io sitespeedio/sitespeed.io:$SITESPEED_VERSION --plugins.add ./gitlab-exporter --outputFolder sitespeed-results $URL $SITESPEED_OPTIONS
|
||||||
|
- mv sitespeed-results/data/performance.json performance.json
|
||||||
|
artifacts:
|
||||||
|
paths:
|
||||||
|
- performance.json
|
||||||
|
- sitespeed-results/
|
||||||
|
reports:
|
||||||
|
performance: performance.json
|
|
@ -6030,6 +6030,15 @@ msgstr ""
|
||||||
msgid "Environments|An error occurred while fetching the environments."
|
msgid "Environments|An error occurred while fetching the environments."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Environments|An error occurred while fetching the logs"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Environments|An error occurred while fetching the logs - Error: %{message}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Environments|An error occurred while fetching the logs for this environment or pod. Please try again"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Environments|An error occurred while making the request."
|
msgid "Environments|An error occurred while making the request."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -6075,9 +6084,6 @@ msgstr ""
|
||||||
msgid "Environments|No deployments yet"
|
msgid "Environments|No deployments yet"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Environments|No pod name has been specified"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Environments|Note that this action will stop the environment, but it will %{emphasisStart}not%{emphasisEnd} have an effect on any existing deployment due to no “stop environment action” being defined in the %{ciConfigLinkStart}.gitlab-ci.yml%{ciConfigLinkEnd} file."
|
msgid "Environments|Note that this action will stop the environment, but it will %{emphasisStart}not%{emphasisEnd} have an effect on any existing deployment due to no “stop environment action” being defined in the %{ciConfigLinkStart}.gitlab-ci.yml%{ciConfigLinkEnd} file."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -14986,9 +14992,6 @@ msgstr ""
|
||||||
msgid "Something went wrong on our end."
|
msgid "Something went wrong on our end."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Something went wrong on our end. %{message}"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Something went wrong on our end. Please try again!"
|
msgid "Something went wrong on our end. Please try again!"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
FactoryBot.define do
|
||||||
|
factory :cycle_analytics_project_stage, class: Analytics::CycleAnalytics::ProjectStage do
|
||||||
|
project
|
||||||
|
sequence(:name) { |n| "Stage ##{n}" }
|
||||||
|
hidden { false }
|
||||||
|
issue_stage
|
||||||
|
|
||||||
|
trait :issue_stage do
|
||||||
|
start_event_identifier { Gitlab::Analytics::CycleAnalytics::StageEvents::IssueCreated.identifier }
|
||||||
|
end_event_identifier { Gitlab::Analytics::CycleAnalytics::StageEvents::IssueStageEnd.identifier }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,29 +1,102 @@
|
||||||
<div class="js-kubernetes-logs" data-logs-path="/root/kubernetes-app/environments/1/logs">
|
<div
|
||||||
<div class="build-page">
|
class="js-kubernetes-logs"
|
||||||
|
data-current-environment-name="production"
|
||||||
|
data-environments-path="/root/my-project/environments.json"
|
||||||
|
data-logs-page="/root/my-project/environments/1/logs"
|
||||||
|
data-logs-path="/root/my-project/environments/1/logs.json"
|
||||||
|
>
|
||||||
|
<div class="build-page-pod-logs">
|
||||||
<div class="build-trace-container prepend-top-default">
|
<div class="build-trace-container prepend-top-default">
|
||||||
<div class="top-bar js-top-bar">
|
<div class="top-bar js-top-bar d-flex">
|
||||||
<div class="truncated-info hidden-xs pull-left"></div>
|
<div class="row">
|
||||||
<div class="dropdown prepend-left-10 js-pod-dropdown">
|
<div class="form-group col-6" role="group">
|
||||||
<button aria-expanded="false" class="dropdown-menu-toggle" data-toggle="dropdown" type="button">
|
<label class="d-block col-form-label-sm col-form-label">
|
||||||
<i class="fa fa-chevron-down"></i>
|
Environment
|
||||||
</button>
|
</label>
|
||||||
<div class="dropdown-menu dropdown-menu-selectable dropdown-menu-drop-up"></div>
|
<div class="dropdown js-environment-dropdown d-flex">
|
||||||
|
<button
|
||||||
|
aria-expanded="false"
|
||||||
|
class="dropdown-menu-toggle d-flex align-content-center align-self-center"
|
||||||
|
data-toggle="dropdown"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<i aria-hidden="true" data-hidden="true" class="fa fa-chevron-down"></i>
|
||||||
|
<div class="dropdown-toggle-text">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<div class="dropdown-menu dropdown-menu-selectable dropdown-menu-drop-up"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group col-6" role="group">
|
||||||
|
<label class="d-block col-form-label-sm col-form-label">
|
||||||
|
Pod logs from
|
||||||
|
</label>
|
||||||
|
<div class="dropdown js-pod-dropdown d-flex">
|
||||||
|
<button
|
||||||
|
aria-expanded="false"
|
||||||
|
class="dropdown-menu-toggle d-flex align-content-center align-self-center"
|
||||||
|
data-toggle="dropdown"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<i aria-hidden="true" data-hidden="true" class="fa fa-chevron-down"></i>
|
||||||
|
<div class="dropdown-toggle-text">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<div class="dropdown-menu dropdown-menu-selectable dropdown-menu-drop-up"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="controllers pull-right">
|
<div class="controllers align-self-end">
|
||||||
<div class="has-tooltip controllers-buttons" data-container="body" data-placement="top" title="Scroll to top">
|
<div
|
||||||
<button class="js-scroll-up btn-scroll btn-transparent btn-blank" disabled type="button"></button>
|
class="has-tooltip controllers-buttons"
|
||||||
|
data-container="body"
|
||||||
|
data-placement="top"
|
||||||
|
title="Scroll to top"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="js-scroll-up btn-scroll btn-transparent btn-blank"
|
||||||
|
disabled
|
||||||
|
type="button"
|
||||||
|
></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="has-tooltip controllers-buttons" data-container="body" data-placement="top" title="Scroll to bottom">
|
<div
|
||||||
<button class="js-scroll-down btn-scroll btn-transparent btn-blank" disabled type="button"></button>
|
class="has-tooltip controllers-buttons"
|
||||||
|
data-container="body"
|
||||||
|
data-placement="top"
|
||||||
|
title="Scroll to bottom"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="js-scroll-down btn-scroll btn-transparent btn-blank"
|
||||||
|
disabled
|
||||||
|
type="button"
|
||||||
|
></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="refresh-control pull-right">
|
<div class="refresh-control">
|
||||||
<div class="has-tooltip controllers-buttons" data-container="body" data-placement="top" title="Refresh">
|
<div
|
||||||
<button class="js-refresh-log btn-default btn-refresh" disabled type="button"></button>
|
class="has-tooltip controllers-buttons"
|
||||||
|
data-container="body"
|
||||||
|
data-placement="top"
|
||||||
|
title="Refresh"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="js-refresh-log btn btn-default btn-refresh h-32-px"
|
||||||
|
disabled
|
||||||
|
type="button"
|
||||||
|
></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<pre class="build-trace" id="build-trace"><code class="bash js-build-output"><div class="build-loader-animation js-build-refresh"></div></code></pre>
|
<pre class="build-trace" id="build-trace">
|
||||||
|
<code class="bash js-build-output"></code>
|
||||||
|
<div class="build-loader-animation js-build-refresh">
|
||||||
|
<div class="dot"></div>
|
||||||
|
<div class="dot"></div>
|
||||||
|
<div class="dot"></div>
|
||||||
|
</div>
|
||||||
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -0,0 +1,62 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
describe Gitlab::Analytics::CycleAnalytics::BaseQueryBuilder do
|
||||||
|
let_it_be(:project) { create(:project, :empty_repo) }
|
||||||
|
let_it_be(:mr1) { create(:merge_request, target_project: project, source_project: project, allow_broken: true, created_at: 3.months.ago) }
|
||||||
|
let_it_be(:mr2) { create(:merge_request, target_project: project, source_project: project, allow_broken: true, created_at: 1.month.ago) }
|
||||||
|
let(:params) { {} }
|
||||||
|
let(:records) do
|
||||||
|
stage = build(:cycle_analytics_project_stage, {
|
||||||
|
start_event_identifier: :merge_request_created,
|
||||||
|
end_event_identifier: :merge_request_merged,
|
||||||
|
project: project
|
||||||
|
})
|
||||||
|
described_class.new(stage: stage, params: params).build.to_a
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
mr1.metrics.update!(merged_at: 1.month.ago)
|
||||||
|
mr2.metrics.update!(merged_at: Time.now)
|
||||||
|
end
|
||||||
|
|
||||||
|
around do |example|
|
||||||
|
Timecop.freeze { example.run }
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'date range parameters' do
|
||||||
|
context 'when filters by only the `from` parameter' do
|
||||||
|
before do
|
||||||
|
params[:from] = 4.months.ago
|
||||||
|
end
|
||||||
|
|
||||||
|
it { expect(records.size).to eq(2) }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when filters by both `from` and `to` parameters' do
|
||||||
|
before do
|
||||||
|
params.merge!(from: 4.months.ago, to: 2.months.ago)
|
||||||
|
end
|
||||||
|
|
||||||
|
it { expect(records.size).to eq(1) }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'invalid date range is provided' do
|
||||||
|
before do
|
||||||
|
params.merge!(from: 1.month.ago, to: 10.months.ago)
|
||||||
|
end
|
||||||
|
|
||||||
|
it { expect(records.size).to eq(0) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'scopes query within the target project' do
|
||||||
|
other_mr = create(:merge_request, source_project: create(:project), allow_broken: true, created_at: 2.months.ago)
|
||||||
|
other_mr.metrics.update!(merged_at: 1.month.ago)
|
||||||
|
|
||||||
|
params[:from] = 1.year.ago
|
||||||
|
|
||||||
|
expect(records.size).to eq(2)
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,131 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
describe Gitlab::Analytics::CycleAnalytics::RecordsFetcher do
|
||||||
|
around do |example|
|
||||||
|
Timecop.freeze { example.run }
|
||||||
|
end
|
||||||
|
|
||||||
|
let_it_be(:project) { create(:project, :empty_repo) }
|
||||||
|
let_it_be(:user) { create(:user) }
|
||||||
|
|
||||||
|
subject do
|
||||||
|
Gitlab::Analytics::CycleAnalytics::DataCollector.new(
|
||||||
|
stage: stage,
|
||||||
|
params: {
|
||||||
|
from: 1.year.ago,
|
||||||
|
current_user: user
|
||||||
|
}
|
||||||
|
).records_fetcher.serialized_records
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#serialized_records' do
|
||||||
|
shared_context 'when records are loaded by maintainer' do
|
||||||
|
before do
|
||||||
|
project.add_user(user, Gitlab::Access::MAINTAINER)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns all records' do
|
||||||
|
expect(subject.size).to eq(2)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'for issue based stage' do
|
||||||
|
let_it_be(:issue1) { create(:issue, project: project) }
|
||||||
|
let_it_be(:issue2) { create(:issue, project: project, confidential: true) }
|
||||||
|
let(:stage) do
|
||||||
|
build(:cycle_analytics_project_stage, {
|
||||||
|
start_event_identifier: :plan_stage_start,
|
||||||
|
end_event_identifier: :issue_first_mentioned_in_commit,
|
||||||
|
project: project
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
issue1.metrics.update(first_added_to_board_at: 3.days.ago, first_mentioned_in_commit_at: 2.days.ago)
|
||||||
|
issue2.metrics.update(first_added_to_board_at: 3.days.ago, first_mentioned_in_commit_at: 2.days.ago)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when records are loaded by guest' do
|
||||||
|
before do
|
||||||
|
project.add_user(user, Gitlab::Access::GUEST)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'filters out confidential issues' do
|
||||||
|
expect(subject.size).to eq(1)
|
||||||
|
expect(subject.first[:iid].to_s).to eq(issue1.iid.to_s)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
include_context 'when records are loaded by maintainer'
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'for merge request based stage' do
|
||||||
|
let(:mr1) { create(:merge_request, created_at: 5.days.ago, source_project: project, allow_broken: true) }
|
||||||
|
let(:mr2) { create(:merge_request, created_at: 4.days.ago, source_project: project, allow_broken: true) }
|
||||||
|
let(:stage) do
|
||||||
|
build(:cycle_analytics_project_stage, {
|
||||||
|
start_event_identifier: :merge_request_created,
|
||||||
|
end_event_identifier: :merge_request_merged,
|
||||||
|
project: project
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
mr1.metrics.update(merged_at: 3.days.ago)
|
||||||
|
mr2.metrics.update(merged_at: 3.days.ago)
|
||||||
|
end
|
||||||
|
|
||||||
|
include_context 'when records are loaded by maintainer'
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'special case' do
|
||||||
|
let(:mr1) { create(:merge_request, source_project: project, allow_broken: true, created_at: 20.days.ago) }
|
||||||
|
let(:mr2) { create(:merge_request, source_project: project, allow_broken: true, created_at: 19.days.ago) }
|
||||||
|
let(:ci_build1) { create(:ci_build) }
|
||||||
|
let(:ci_build2) { create(:ci_build) }
|
||||||
|
let(:default_stages) { Gitlab::Analytics::CycleAnalytics::DefaultStages }
|
||||||
|
let(:stage) { build(:cycle_analytics_project_stage, default_stages.params_for_test_stage.merge(project: project)) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
mr1.metrics.update!({
|
||||||
|
merged_at: 5.days.ago,
|
||||||
|
first_deployed_to_production_at: 1.day.ago,
|
||||||
|
latest_build_started_at: 5.days.ago,
|
||||||
|
latest_build_finished_at: 1.day.ago,
|
||||||
|
pipeline: ci_build1.pipeline
|
||||||
|
})
|
||||||
|
mr2.metrics.update!({
|
||||||
|
merged_at: 10.days.ago,
|
||||||
|
first_deployed_to_production_at: 5.days.ago,
|
||||||
|
latest_build_started_at: 9.days.ago,
|
||||||
|
latest_build_finished_at: 7.days.ago,
|
||||||
|
pipeline: ci_build2.pipeline
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'returns build records' do
|
||||||
|
shared_examples 'orders build records by `latest_build_finished_at`' do
|
||||||
|
it 'orders by `latest_build_finished_at`' do
|
||||||
|
build_ids = subject.map { |item| item[:id] }
|
||||||
|
|
||||||
|
expect(build_ids).to eq([ci_build1.id, ci_build2.id])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when requesting records for default test stage' do
|
||||||
|
include_examples 'orders build records by `latest_build_finished_at`'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when requesting records for default staging stage' do
|
||||||
|
before do
|
||||||
|
stage.assign_attributes(default_stages.params_for_staging_stage)
|
||||||
|
end
|
||||||
|
|
||||||
|
include_examples 'orders build records by `latest_build_finished_at`'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -26,6 +26,13 @@ describe Gitlab::CycleAnalytics::CodeStage do
|
||||||
|
|
||||||
it_behaves_like 'base stage'
|
it_behaves_like 'base stage'
|
||||||
|
|
||||||
|
context 'when using the new query backend' do
|
||||||
|
include_examples 'Gitlab::Analytics::CycleAnalytics::DataCollector backend examples' do
|
||||||
|
let(:expected_record_count) { 2 }
|
||||||
|
let(:expected_ordered_attribute_values) { [mr_2.title, mr_1.title] }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe '#project_median' do
|
describe '#project_median' do
|
||||||
around do |example|
|
around do |example|
|
||||||
Timecop.freeze { example.run }
|
Timecop.freeze { example.run }
|
||||||
|
|
|
@ -21,6 +21,13 @@ describe Gitlab::CycleAnalytics::IssueStage do
|
||||||
|
|
||||||
it_behaves_like 'base stage'
|
it_behaves_like 'base stage'
|
||||||
|
|
||||||
|
context 'when using the new query backend' do
|
||||||
|
include_examples 'Gitlab::Analytics::CycleAnalytics::DataCollector backend examples' do
|
||||||
|
let(:expected_record_count) { 3 }
|
||||||
|
let(:expected_ordered_attribute_values) { [issue_3.title, issue_2.title, issue_1.title] }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe '#median' do
|
describe '#median' do
|
||||||
around do |example|
|
around do |example|
|
||||||
Timecop.freeze { example.run }
|
Timecop.freeze { example.run }
|
||||||
|
|
|
@ -21,6 +21,13 @@ describe Gitlab::CycleAnalytics::PlanStage do
|
||||||
|
|
||||||
it_behaves_like 'base stage'
|
it_behaves_like 'base stage'
|
||||||
|
|
||||||
|
context 'when using the new query backend' do
|
||||||
|
include_examples 'Gitlab::Analytics::CycleAnalytics::DataCollector backend examples' do
|
||||||
|
let(:expected_record_count) { 2 }
|
||||||
|
let(:expected_ordered_attribute_values) { [issue_1.title, issue_2.title] }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe '#project_median' do
|
describe '#project_median' do
|
||||||
around do |example|
|
around do |example|
|
||||||
Timecop.freeze { example.run }
|
Timecop.freeze { example.run }
|
||||||
|
|
|
@ -52,3 +52,21 @@ shared_examples 'calculate #median with date range' do
|
||||||
it { expect(stage.project_median).to eq(nil) }
|
it { expect(stage.project_median).to eq(nil) }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
shared_examples 'Gitlab::Analytics::CycleAnalytics::DataCollector backend examples' do
|
||||||
|
let(:stage_params) { Gitlab::Analytics::CycleAnalytics::DefaultStages.send("params_for_#{stage_name}_stage").merge(project: project) }
|
||||||
|
let(:stage) { Analytics::CycleAnalytics::ProjectStage.new(stage_params) }
|
||||||
|
let(:data_collector) { Gitlab::Analytics::CycleAnalytics::DataCollector.new(stage: stage, params: { from: stage_options[:from], current_user: project.creator }) }
|
||||||
|
let(:attribute_to_verify) { :title }
|
||||||
|
|
||||||
|
context 'provides the same results as the old implementation' do
|
||||||
|
it 'for the median' do
|
||||||
|
expect(data_collector.median.seconds).to eq(ISSUES_MEDIAN)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'for the list of event records' do
|
||||||
|
records = data_collector.records_fetcher.serialized_records
|
||||||
|
expect(records.map { |event| event[attribute_to_verify] }).to eq(expected_ordered_attribute_values)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
|
@ -12,17 +12,20 @@ describe Gitlab::CycleAnalytics::TestStage do
|
||||||
it_behaves_like 'base stage'
|
it_behaves_like 'base stage'
|
||||||
|
|
||||||
describe '#median' do
|
describe '#median' do
|
||||||
|
let(:mr_1) { create(:merge_request, :closed, source_project: project, created_at: 60.minutes.ago) }
|
||||||
|
let(:mr_2) { create(:merge_request, :closed, source_project: project, created_at: 40.minutes.ago, source_branch: 'A') }
|
||||||
|
let(:mr_3) { create(:merge_request, source_project: project, created_at: 10.minutes.ago, source_branch: 'B') }
|
||||||
|
let(:mr_4) { create(:merge_request, source_project: project, created_at: 10.minutes.ago, source_branch: 'C') }
|
||||||
|
let(:mr_5) { create(:merge_request, source_project: project, created_at: 10.minutes.ago, source_branch: 'D') }
|
||||||
|
let(:ci_build1) { create(:ci_build, project: project) }
|
||||||
|
let(:ci_build2) { create(:ci_build, project: project) }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
issue_1 = create(:issue, project: project, created_at: 90.minutes.ago)
|
issue_1 = create(:issue, project: project, created_at: 90.minutes.ago)
|
||||||
issue_2 = create(:issue, project: project, created_at: 60.minutes.ago)
|
issue_2 = create(:issue, project: project, created_at: 60.minutes.ago)
|
||||||
issue_3 = create(:issue, project: project, created_at: 60.minutes.ago)
|
issue_3 = create(:issue, project: project, created_at: 60.minutes.ago)
|
||||||
mr_1 = create(:merge_request, :closed, source_project: project, created_at: 60.minutes.ago)
|
mr_1.metrics.update!(latest_build_started_at: 32.minutes.ago, latest_build_finished_at: 2.minutes.ago, pipeline_id: ci_build1.commit_id)
|
||||||
mr_2 = create(:merge_request, :closed, source_project: project, created_at: 40.minutes.ago, source_branch: 'A')
|
mr_2.metrics.update!(latest_build_started_at: 62.minutes.ago, latest_build_finished_at: 32.minutes.ago, pipeline_id: ci_build2.commit_id)
|
||||||
mr_3 = create(:merge_request, source_project: project, created_at: 10.minutes.ago, source_branch: 'B')
|
|
||||||
mr_4 = create(:merge_request, source_project: project, created_at: 10.minutes.ago, source_branch: 'C')
|
|
||||||
mr_5 = create(:merge_request, source_project: project, created_at: 10.minutes.ago, source_branch: 'D')
|
|
||||||
mr_1.metrics.update!(latest_build_started_at: 32.minutes.ago, latest_build_finished_at: 2.minutes.ago)
|
|
||||||
mr_2.metrics.update!(latest_build_started_at: 62.minutes.ago, latest_build_finished_at: 32.minutes.ago)
|
|
||||||
mr_3.metrics.update!(latest_build_started_at: nil, latest_build_finished_at: nil)
|
mr_3.metrics.update!(latest_build_started_at: nil, latest_build_finished_at: nil)
|
||||||
mr_4.metrics.update!(latest_build_started_at: nil, latest_build_finished_at: nil)
|
mr_4.metrics.update!(latest_build_started_at: nil, latest_build_finished_at: nil)
|
||||||
mr_5.metrics.update!(latest_build_started_at: nil, latest_build_finished_at: nil)
|
mr_5.metrics.update!(latest_build_started_at: nil, latest_build_finished_at: nil)
|
||||||
|
@ -43,5 +46,13 @@ describe Gitlab::CycleAnalytics::TestStage do
|
||||||
end
|
end
|
||||||
|
|
||||||
include_examples 'calculate #median with date range'
|
include_examples 'calculate #median with date range'
|
||||||
|
|
||||||
|
context 'when using the new query backend' do
|
||||||
|
include_examples 'Gitlab::Analytics::CycleAnalytics::DataCollector backend examples' do
|
||||||
|
let(:expected_record_count) { 2 }
|
||||||
|
let(:attribute_to_verify) { :id }
|
||||||
|
let(:expected_ordered_attribute_values) { [mr_1.metrics.pipeline.builds.first.id, mr_2.metrics.pipeline.builds.first.id] }
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -55,11 +55,11 @@ shared_examples_for 'cycle analytics stage' do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#subject_model' do
|
describe '#subject_class' do
|
||||||
it 'infers the model from the start event' do
|
it 'infers the model from the start event' do
|
||||||
stage = described_class.new(valid_params)
|
stage = described_class.new(valid_params)
|
||||||
|
|
||||||
expect(stage.subject_model).to eq(MergeRequest)
|
expect(stage.subject_class).to eq(MergeRequest)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -78,4 +78,30 @@ shared_examples_for 'cycle analytics stage' do
|
||||||
expect(stage.end_event).to be_a_kind_of(Gitlab::Analytics::CycleAnalytics::StageEvents::MergeRequestMerged)
|
expect(stage.end_event).to be_a_kind_of(Gitlab::Analytics::CycleAnalytics::StageEvents::MergeRequestMerged)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe '#matches_with_stage_params?' do
|
||||||
|
let(:params) { Gitlab::Analytics::CycleAnalytics::DefaultStages.params_for_test_stage }
|
||||||
|
|
||||||
|
it 'matches with default stage params' do
|
||||||
|
stage = described_class.new(params)
|
||||||
|
|
||||||
|
expect(stage).to be_default_stage
|
||||||
|
expect(stage).to be_matches_with_stage_params(params)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "mismatches when the stage is custom" do
|
||||||
|
stage = described_class.new(params.merge(custom: true))
|
||||||
|
|
||||||
|
expect(stage).not_to be_default_stage
|
||||||
|
expect(stage).not_to be_matches_with_stage_params(params)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#parent_id' do
|
||||||
|
it "delegates to 'parent_name'_id attribute" do
|
||||||
|
stage = described_class.new(parent: parent)
|
||||||
|
|
||||||
|
expect(stage.parent_id).to eq(parent.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
Loading…
Reference in New Issue