Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
G
gitlab-ce
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
1
Merge Requests
1
Analytics
Analytics
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Commits
Issue Boards
Open sidebar
nexedi
gitlab-ce
Commits
5a4f5cd3
Commit
5a4f5cd3
authored
Aug 01, 2019
by
Pavel Shutsin
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Add productivity analytics data endpoints
Data will be used to show productivity charts
parent
4b32a15a
Changes
19
Hide whitespace changes
Inline
Side-by-side
Showing
19 changed files
with
923 additions
and
29 deletions
+923
-29
ee/app/controllers/analytics/application_controller.rb
ee/app/controllers/analytics/application_controller.rb
+2
-0
ee/app/controllers/analytics/productivity_analytics_controller.rb
...ontrollers/analytics/productivity_analytics_controller.rb
+89
-0
ee/app/finders/productivity_analytics_finder.rb
ee/app/finders/productivity_analytics_finder.rb
+62
-0
ee/app/models/license.rb
ee/app/models/license.rb
+1
-0
ee/app/models/productivity_analytics.rb
ee/app/models/productivity_analytics.rb
+70
-0
ee/app/policies/ee/global_policy.rb
ee/app/policies/ee/global_policy.rb
+2
-0
ee/app/policies/ee/group_policy.rb
ee/app/policies/ee/group_policy.rb
+2
-0
ee/app/serializers/productivity_analytics_merge_request_entity.rb
...erializers/productivity_analytics_merge_request_entity.rb
+15
-0
ee/app/views/layouts/nav/sidebar/_analytics.html.haml
ee/app/views/layouts/nav/sidebar/_analytics.html.haml
+24
-22
ee/changelogs/unreleased/12079-productivity-analytics-mvp.yml
...hangelogs/unreleased/12079-productivity-analytics-mvp.yml
+5
-0
ee/config/routes/analytics.rb
ee/config/routes/analytics.rb
+8
-3
ee/db/fixtures/development/90_productivity_analytics.rb
ee/db/fixtures/development/90_productivity_analytics.rb
+155
-0
ee/spec/controllers/analytics/productivity_analytics_controller_spec.rb
...llers/analytics/productivity_analytics_controller_spec.rb
+112
-4
ee/spec/factories/merge_requests.rb
ee/spec/factories/merge_requests.rb
+13
-0
ee/spec/finders/productivity_analytics_finder_spec.rb
ee/spec/finders/productivity_analytics_finder_spec.rb
+94
-0
ee/spec/models/productivity_analytics_spec.rb
ee/spec/models/productivity_analytics_spec.rb
+212
-0
ee/spec/policies/global_policy_spec.rb
ee/spec/policies/global_policy_spec.rb
+12
-0
ee/spec/policies/group_policy_spec.rb
ee/spec/policies/group_policy_spec.rb
+18
-0
ee/spec/serializers/productivity_analytics_merge_request_entity_spec.rb
...izers/productivity_analytics_merge_request_entity_spec.rb
+27
-0
No files found.
ee/app/controllers/analytics/application_controller.rb
View file @
5a4f5cd3
# frozen_string_literal: true
class
Analytics::ApplicationController
<
ApplicationController
include
RoutableActions
layout
'analytics'
end
ee/app/controllers/analytics/productivity_analytics_controller.rb
View file @
5a4f5cd3
# frozen_string_literal: true
class
Analytics::ProductivityAnalyticsController
<
Analytics
::
ApplicationController
before_action
:load_group
before_action
:load_project
before_action
:check_feature_availability!
before_action
:authorize_view_productivity_analytics!
include
IssuableCollections
def
show
respond_to
do
|
format
|
format
.
html
format
.
json
do
metric
=
params
.
fetch
(
'metric_type'
,
ProductivityAnalytics
::
DEFAULT_TYPE
)
data
=
case
params
[
'chart_type'
]
when
'scatterplot'
productivity_analytics
.
scatterplot_data
(
type:
metric
)
when
'histogram'
productivity_analytics
.
histogram_data
(
type:
metric
)
else
include_relations
(
paginate
(
productivity_analytics
.
merge_requests_extended
)).
map
do
|
merge_request
|
serializer
.
represent
(
merge_request
,
{},
ProductivityAnalyticsMergeRequestEntity
)
end
end
render
json:
data
,
status: :ok
end
end
end
private
def
paginate
(
merge_requests
)
merge_requests
.
page
(
params
[
:page
]).
per
(
params
[
:per_page
]).
tap
do
|
paginated_data
|
response
.
set_header
(
'X-Per-Page'
,
paginated_data
.
limit_value
.
to_s
)
response
.
set_header
(
'X-Page'
,
paginated_data
.
current_page
.
to_s
)
response
.
set_header
(
'X-Next-Page'
,
paginated_data
.
next_page
.
to_s
)
response
.
set_header
(
'X-Prev-Page'
,
paginated_data
.
prev_page
.
to_s
)
response
.
set_header
(
'X-Total'
,
paginated_data
.
total_count
.
to_s
)
response
.
set_header
(
'X-Total-Pages'
,
paginated_data
.
total_pages
.
to_s
)
end
end
def
authorize_view_productivity_analytics!
return
render_403
unless
can?
(
current_user
,
:view_productivity_analytics
,
@group
||
:global
)
end
def
check_feature_availability!
return
render_404
unless
::
License
.
feature_available?
(
:productivity_analytics
)
return
render_404
if
@group
&&
!
@group
.
root_ancestor
.
feature_available?
(
:productivity_analytics
)
end
def
load_group
return
unless
params
[
'group_id'
]
@group
=
find_routable!
(
Group
,
params
[
'group_id'
])
end
def
load_project
return
unless
@group
&&
params
[
'project_id'
]
@project
=
find_routable!
(
@group
.
projects
,
params
[
'project_id'
])
end
def
serializer
@serializer
||=
BaseSerializer
.
new
(
current_user:
current_user
)
end
def
finder_type
ProductivityAnalyticsFinder
end
def
default_state
'merged'
end
def
productivity_analytics
@productivity_analytics
||=
ProductivityAnalytics
.
new
(
merge_requests:
finder
.
execute
,
sort:
params
[
:sort
])
end
# rubocop: disable CodeReuse/ActiveRecord
def
include_relations
(
paginated_mrs
)
# Due to Rails bug: https://github.com/rails/rails/issues/34889 we can't use .includes statement
# to avoid N+1 call when we load custom columns.
# So we load relations manually here.
preloader
=
ActiveRecord
::
Associations
::
Preloader
.
new
preloader
.
preload
(
paginated_mrs
,
{
author:
[],
target_project:
{
namespace: :route
}
})
paginated_mrs
end
# rubocop: enable CodeReuse/ActiveRecord
end
ee/app/finders/productivity_analytics_finder.rb
0 → 100644
View file @
5a4f5cd3
# frozen_string_literal: true
class
ProductivityAnalyticsFinder
<
MergeRequestsFinder
def
self
.
array_params
super
.
merge
(
days_to_merge:
[])
end
def
self
.
scalar_params
@scalar_params
||=
super
+
[
:merged_at_before
,
:merged_at_after
]
end
def
filter_items
(
_items
)
items
=
by_days_to_merge
(
super
)
by_merged_at
(
items
)
end
private
def
metrics_table
MergeRequest
::
Metrics
.
arel_table
.
alias
(
MergeRequest
::
Metrics
.
table_name
)
end
# rubocop: disable CodeReuse/ActiveRecord
def
by_days_to_merge
(
items
)
return
items
unless
params
[
:days_to_merge
].
present?
items
.
joins
(
:metrics
).
where
(
"
#{
days_to_merge_column
}
IN (?)"
,
params
[
:days_to_merge
].
flatten
.
map
(
&
:to_i
))
end
# rubocop: enable CodeReuse/ActiveRecord
def
days_to_merge_column
"date_part('day',merge_request_metrics.merged_at - merge_requests.created_at)"
end
# rubocop: disable CodeReuse/ActiveRecord
def
by_merged_at
(
items
)
return
items
unless
params
[
:merged_at_after
]
||
params
[
:merged_at_before
]
items
=
items
.
joins
(
:metrics
)
items
=
items
.
where
(
metrics_table
[
:merged_at
].
gteq
(
merged_at_between
[
:from
]))
if
merged_at_between
[
:from
]
items
=
items
.
where
(
metrics_table
[
:merged_at
].
lteq
(
merged_at_between
[
:to
]))
if
merged_at_between
[
:to
]
items
end
# rubocop: enable CodeReuse/ActiveRecord
def
merged_at_between
@merged_at_between
||=
begin
if
merged_at_period
{
from:
Time
.
zone
.
now
.
ago
(
merged_at_period
.
days
)
}
else
{
from:
params
[
:merged_at_after
],
to:
params
[
:merged_at_before
]
}
end
end
end
def
merged_at_period
matches
=
params
[
:merged_at_after
]
&
.
match
(
/^(?<days>\d+)days?$/
)
matches
&&
matches
[
:days
].
to_i
end
end
ee/app/models/license.rb
View file @
5a4f5cd3
...
...
@@ -83,6 +83,7 @@ class License < ApplicationRecord
object_storage
operations_dashboard
packages
productivity_analytics
project_aliases
protected_environments
reject_unsigned_commits
...
...
ee/app/models/productivity_analytics.rb
0 → 100644
View file @
5a4f5cd3
# frozen_string_literal: true
class
ProductivityAnalytics
attr_reader
:merge_requests
,
:sort
METRIC_COLUMNS
=
{
'days_to_merge'
=>
"DATE_PART('day', merge_request_metrics.merged_at - merge_requests.created_at)"
,
'time_to_first_comment'
=>
"DATE_PART('day', merge_request_metrics.first_comment_at - merge_requests.created_at)*24+DATE_PART('hour', merge_request_metrics.first_comment_at - merge_requests.created_at)"
,
'time_to_last_commit'
=>
"DATE_PART('day', merge_request_metrics.last_commit_at - merge_request_metrics.first_comment_at)*24+DATE_PART('hour', merge_request_metrics.last_commit_at - merge_request_metrics.first_comment_at)"
,
'time_to_merge'
=>
"DATE_PART('day', merge_request_metrics.merged_at - merge_request_metrics.last_commit_at)*24+DATE_PART('hour', merge_request_metrics.merged_at - merge_request_metrics.last_commit_at)"
,
'commits_count'
=>
'commits_count'
,
'loc_per_commit'
=>
'(diff_size/commits_count)'
,
'files_touched'
=>
'modified_paths_size'
}.
freeze
METRIC_TYPES
=
METRIC_COLUMNS
.
keys
.
freeze
DEFAULT_TYPE
=
'days_to_merge'
.
freeze
def
initialize
(
merge_requests
:,
sort:
nil
)
@merge_requests
=
merge_requests
.
joins
(
:metrics
)
@sort
=
sort
end
def
histogram_data
(
type
:)
return
unless
column
=
METRIC_COLUMNS
[
type
]
histogram_query
(
column
).
map
do
|
data
|
[
data
[
:metric
]
&
.
to_i
,
data
[
:mr_count
]]
end
.
to_h
end
def
scatterplot_data
(
type
:)
return
unless
column
=
METRIC_COLUMNS
[
type
]
scatterplot_query
(
column
).
map
do
|
data
|
[
data
.
id
,
{
metric:
data
[
:metric
],
merged_at:
data
[
:merged_at
]
}]
end
.
to_h
end
def
merge_requests_extended
columns
=
METRIC_COLUMNS
.
map
do
|
type
,
column
|
Arel
::
Nodes
::
As
.
new
(
Arel
.
sql
(
column
),
Arel
.
sql
(
type
)).
to_sql
end
columns
.
unshift
(
MergeRequest
.
arel_table
[
Arel
.
star
])
mrs
=
merge_requests
.
select
(
columns
)
mrs
=
mrs
.
reorder
(
custom_sorting
)
if
custom_sorting
mrs
end
private
def
histogram_query
(
column
)
merge_requests
.
except
(
:select
).
select
(
"
#{
column
}
as metric, count(*) as mr_count"
).
group
(
column
).
reorder
(
nil
)
end
def
scatterplot_query
(
column
)
merge_requests
.
except
(
:select
).
select
(
"
#{
column
}
as metric, merge_requests.id, merge_request_metrics.merged_at"
).
reorder
(
"merge_request_metrics.merged_at ASC"
)
end
def
custom_sorting
return
unless
sort
column
,
direction
=
sort
.
split
(
/_(asc|desc)$/i
)
return
unless
column
.
in?
(
METRIC_TYPES
)
Arel
.
sql
(
"
#{
column
}
#{
direction
}
"
)
end
end
ee/app/policies/ee/global_policy.rb
View file @
5a4f5cd3
...
...
@@ -16,6 +16,8 @@ module EE
end
rule
{
support_bot
}.
prevent
:use_quick_actions
rule
{
~
anonymous
}.
enable
:view_productivity_analytics
end
end
end
ee/app/policies/ee/group_policy.rb
View file @
5a4f5cd3
...
...
@@ -131,6 +131,8 @@ module EE
rule
{
ip_enforcement_prevents_access
&
~
owner
}.
policy
do
prevent
:read_group
end
rule
{
reporter
}.
enable
:view_productivity_analytics
end
override
:lookup_access_level!
...
...
ee/app/serializers/productivity_analytics_merge_request_entity.rb
0 → 100644
View file @
5a4f5cd3
# frozen_string_literal: true
class
ProductivityAnalyticsMergeRequestEntity
<
IssuableEntity
ProductivityAnalytics
::
METRIC_TYPES
.
each
do
|
type
|
expose
(
type
)
{
|
mr
|
mr
.
attributes
[
type
]
}
end
expose
:author_avatar_url
do
|
merge_request
|
merge_request
.
author
&
.
avatar_url
end
expose
:merge_request_url
do
|
merge_request
|
project_merge_request_url
(
merge_request
.
target_project
,
merge_request
)
end
end
ee/app/views/layouts/nav/sidebar/_analytics.html.haml
View file @
5a4f5cd3
...
...
@@ -6,28 +6,30 @@
=
sprite_icon
(
'log'
,
size:
24
)
.sidebar-context-title
=
_
(
'Analytics'
)
%ul
.sidebar-top-level-items
=
nav_link
(
controller: :productivity_analytics
)
do
=
link_to
analytics_productivity_analytics_path
,
class:
'qa-sidebar-productivity-analytics'
do
.nav-icon-container
=
sprite_icon
(
'comment'
)
%span
.nav-item-name
=
_
(
'Productivity Analytics'
)
%ul
.sidebar-sub-level-items.is-fly-out-only
=
nav_link
(
controller: :productivity_analytics
,
html_options:
{
class:
"fly-out-top-item qa-sidebar-productivity-analytics-fly-out"
}
)
do
=
link_to
analytics_productivity_analytics_path
do
%strong
.fly-out-top-item-name
=
_
(
'Productivity Analytics'
)
-
if
Feature
.
enabled?
(
:productivity_analytics
)
=
nav_link
(
controller: :productivity_analytics
)
do
=
link_to
analytics_productivity_analytics_path
,
class:
'qa-sidebar-productivity-analytics'
do
.nav-icon-container
=
sprite_icon
(
'comment'
)
%span
.nav-item-name
=
_
(
'Productivity Analytics'
)
%ul
.sidebar-sub-level-items.is-fly-out-only
=
nav_link
(
controller: :productivity_analytics
,
html_options:
{
class:
"fly-out-top-item qa-sidebar-productivity-analytics-fly-out"
}
)
do
=
link_to
analytics_productivity_analytics_path
do
%strong
.fly-out-top-item-name
=
_
(
'Productivity Analytics'
)
=
nav_link
(
controller: :cycle_analytics
)
do
=
link_to
analytics_cycle_analytics_path
,
class:
'qa-sidebar-cycle-analytics'
do
.nav-icon-container
=
sprite_icon
(
'repeat'
)
%span
.nav-item-name
=
_
(
'Cycle Analytics'
)
%ul
.sidebar-sub-level-items.is-fly-out-only
=
nav_link
(
controller: :cycle_analytics
,
html_options:
{
class:
"fly-out-top-item qa-sidebar-cycle-analytics-fly-out"
}
)
do
=
link_to
analytics_cycle_analytics_path
do
%strong
.fly-out-top-item-name
=
_
(
'Cycle Analytics'
)
-
if
Feature
.
enabled?
(
:cycle_analytics
)
=
nav_link
(
controller: :cycle_analytics
)
do
=
link_to
analytics_cycle_analytics_path
,
class:
'qa-sidebar-cycle-analytics'
do
.nav-icon-container
=
sprite_icon
(
'repeat'
)
%span
.nav-item-name
=
_
(
'Cycle Analytics'
)
%ul
.sidebar-sub-level-items.is-fly-out-only
=
nav_link
(
controller: :cycle_analytics
,
html_options:
{
class:
"fly-out-top-item qa-sidebar-cycle-analytics-fly-out"
}
)
do
=
link_to
analytics_cycle_analytics_path
do
%strong
.fly-out-top-item-name
=
_
(
'Cycle Analytics'
)
=
render
'shared/sidebar_toggle_button'
ee/changelogs/unreleased/12079-productivity-analytics-mvp.yml
0 → 100644
View file @
5a4f5cd3
---
title
:
add Productivity Analytics page with basic charts
merge_request
:
14772
author
:
type
:
added
ee/config/routes/analytics.rb
View file @
5a4f5cd3
# frozen_string_literal: true
namespace
:analytics
do
root
to:
redirect
(
'-/analytics/productivity_analytics'
)
constraints
(
::
Constraints
::
FeatureConstrainer
.
new
(
:productivity_analytics
))
do
root
to:
redirect
(
'-/analytics/productivity_analytics'
)
resource
:productivity_analytics
,
only: :show
resource
:cycle_analytics
,
only: :show
resource
:productivity_analytics
,
only: :show
end
constraints
(
::
Constraints
::
FeatureConstrainer
.
new
(
:cycle_analytics
))
do
resource
:cycle_analytics
,
only: :show
end
end
ee/db/fixtures/development/90_productivity_analytics.rb
0 → 100644
View file @
5a4f5cd3
# frozen_string_literal: true
require
'./spec/support/sidekiq'
class
Gitlab::Seeder::ProductivityAnalytics
def
initialize
(
project
)
@project
=
project
@user
=
User
.
admins
.
first
@issue_count
=
100
end
def
seed!
Sidekiq
::
Worker
.
skipping_transaction_check
do
Sidekiq
::
Testing
.
inline!
do
Timecop
.
travel
90
.
days
.
ago
issues
=
create_issues
print
'.'
Timecop
.
travel
10
.
days
.
from_now
add_milestones_and_list_labels
(
issues
)
print
'.'
Timecop
.
travel
10
.
days
.
from_now
branches
=
mention_in_commits
(
issues
)
print
'.'
Timecop
.
travel
10
.
days
.
from_now
merge_requests
=
create_merge_requests_closing_issues
(
issues
,
branches
)
print
'.'
Timecop
.
travel
10
.
days
.
from_now
create_notes
(
merge_requests
)
Timecop
.
travel
10
.
days
.
from_now
merge_merge_requests
(
merge_requests
)
print
'.'
end
end
print
'.'
end
private
def
create_issues
Array
.
new
(
@issue_count
)
do
issue_params
=
{
title:
"Productivity Analytics:
#{
FFaker
::
Lorem
.
sentence
(
6
)
}
"
,
description:
FFaker
::
Lorem
.
sentence
,
state:
'opened'
,
assignees:
[
@project
.
team
.
users
.
sample
]
}
Timecop
.
travel
rand
(
10
).
days
.
from_now
do
Issues
::
CreateService
.
new
(
@project
,
@project
.
team
.
users
.
sample
,
issue_params
).
execute
end
end
end
def
add_milestones_and_list_labels
(
issues
)
issues
.
shuffle
.
map
.
with_index
do
|
issue
,
index
|
Timecop
.
travel
12
.
hours
.
from_now
do
if
index
.
even?
issue
.
update
(
milestone:
@project
.
milestones
.
sample
)
else
label_name
=
"
#{
FFaker
::
Product
.
brand
}
-
#{
FFaker
::
Product
.
brand
}
-
#{
rand
(
1000
)
}
"
list_label
=
FactoryBot
.
create
(
:label
,
title:
label_name
,
project:
issue
.
project
)
FactoryBot
.
create
(
:list
,
board:
FactoryBot
.
create
(
:board
,
project:
issue
.
project
),
label:
list_label
)
issue
.
update
(
labels:
[
list_label
])
end
issue
end
end
end
def
mention_in_commits
(
issues
)
issues
.
map
do
|
issue
|
branch_name
=
filename
=
"
#{
FFaker
::
Product
.
brand
}
-
#{
FFaker
::
Product
.
brand
}
-
#{
rand
(
1000
)
}
"
Timecop
.
travel
12
.
hours
.
from_now
do
issue
.
project
.
repository
.
add_branch
(
@user
,
branch_name
,
'master'
)
commit_sha
=
issue
.
project
.
repository
.
create_file
(
@user
,
filename
,
"content"
,
message:
"Commit for
#{
issue
.
to_reference
}
"
,
branch_name:
branch_name
)
issue
.
project
.
repository
.
commit
(
commit_sha
)
::
Git
::
BranchPushService
.
new
(
issue
.
project
,
@user
,
oldrev:
issue
.
project
.
repository
.
commit
(
"master"
).
sha
,
newrev:
commit_sha
,
ref:
'refs/heads/master'
).
execute
end
branch_name
end
end
def
create_merge_requests_closing_issues
(
issues
,
branches
)
issues
.
zip
(
branches
).
map
do
|
issue
,
branch
|
opts
=
{
title:
'Productivity Analytics merge_request'
,
description:
"Fixes
#{
issue
.
to_reference
}
"
,
source_branch:
branch
,
target_branch:
'master'
}
Timecop
.
travel
issue
.
created_at
do
MergeRequests
::
CreateService
.
new
(
issue
.
project
,
@user
,
opts
).
execute
end
end
end
def
create_notes
(
merge_requests
)
merge_requests
.
each
do
|
merge_request
|
Timecop
.
travel
merge_request
.
created_at
+
rand
(
5
).
days
do
Note
.
create!
(
author:
@user
,
project:
merge_request
.
project
,
noteable:
merge_request
,
note:
FFaker
::
Lorem
.
sentence
(
rand
(
5
))
)
end
end
end
def
merge_merge_requests
(
merge_requests
)
merge_requests
.
each
do
|
merge_request
|
Timecop
.
travel
rand
(
15
).
days
.
from_now
do
MergeRequests
::
MergeService
.
new
(
merge_request
.
project
,
@user
).
execute
(
merge_request
)
end
end
end
end
Gitlab
::
Seeder
.
quiet
do
flag
=
'SEED_PRODUCTIVITY_ANALYTICS'
if
ENV
[
flag
]
Project
.
find_each
do
|
project
|
# This seed naively assumes that every project has a repository, and every
# repository has a `master` branch, which may be the case for a pristine
# GDK seed, but is almost never true for a GDK that's actually had
# development performed on it.
next
unless
project
.
repository_exists?
&&
project
.
repository
.
commit
(
'master'
)
seeder
=
Gitlab
::
Seeder
::
ProductivityAnalytics
.
new
(
project
)
seeder
.
seed!
puts
"Productivity analytics seeded for project
#{
project
.
full_path
}
"
break
end
else
puts
"Skipped. Use the `
#{
flag
}
` environment variable to enable."
end
end
ee/spec/controllers/analytics/productivity_analytics_controller_spec.rb
View file @
5a4f5cd3
...
...
@@ -3,17 +3,125 @@
require
'spec_helper'
describe
Analytics
::
ProductivityAnalyticsController
do
let
(
:user
)
{
create
(
:user
)
}
let
(
:
current_
user
)
{
create
(
:user
)
}
before
do
sign_in
(
user
)
sign_in
(
current_user
)
if
current_user
stub_licensed_features
(
productivity_analytics:
true
)
end
describe
'GET show'
do
it
'renders `show` template'
do
get
:show
subject
{
get
:show
}
it
'checks for premium license'
do
stub_licensed_features
(
productivity_analytics:
false
)
subject
expect
(
response
.
code
).
to
eq
'404'
end
it
'authorizes for ability to view analytics'
do
expect
(
Ability
).
to
receive
(
:allowed?
).
with
(
current_user
,
:view_productivity_analytics
,
:global
).
and_return
(
false
)
subject
expect
(
response
.
code
).
to
eq
'403'
end
it
'renders show template'
do
subject
expect
(
response
).
to
render_template
:show
end
end
describe
'GET show.json'
do
subject
{
get
:show
,
format: :json
,
params:
params
}
let
(
:params
)
{
{}
}
let
(
:analytics_mock
)
{
instance_double
(
'ProductivityAnalytics'
)
}
before
do
merge_requests
=
double
allow_any_instance_of
(
ProductivityAnalyticsFinder
).
to
receive
(
:execute
).
and_return
(
merge_requests
)
allow
(
ProductivityAnalytics
)
.
to
receive
(
:new
)
.
with
(
merge_requests:
merge_requests
,
sort:
params
[
:sort
])
.
and_return
(
analytics_mock
)
end
context
'with non-existing group_id'
do
let
(
:params
)
{
{
group_id:
'SOMETHING_THAT_DOES_NOT_EXIST'
}
}
it
'renders 404'
do
subject
expect
(
response
.
code
).
to
eq
'404'
end
end
context
'with non-existing project_id'
do
let
(
:group
)
{
create
:group
}
let
(
:params
)
{
{
group_id:
group
.
full_path
,
project_id:
'SOMETHING_THAT_DOES_NOT_EXIST'
}
}
it
'renders 404'
do
subject
expect
(
response
.
code
).
to
eq
'404'
end
end
context
'for list of MRs'
do
let!
(
:merge_request
)
{
create
:merge_request
,
:merged
}
let
(
:serializer_mock
)
{
instance_double
(
'BaseSerializer'
)
}
before
do
allow
(
BaseSerializer
).
to
receive
(
:new
).
with
(
current_user:
current_user
).
and_return
(
serializer_mock
)
allow
(
analytics_mock
).
to
receive
(
:merge_requests_extended
).
and_return
(
MergeRequest
.
all
)
allow
(
serializer_mock
).
to
receive
(
:represent
)
.
with
(
merge_request
,
{},
ProductivityAnalyticsMergeRequestEntity
)
.
and_return
(
'mr_representation'
)
end
it
'serializes whatever analytics returns with ProductivityAnalyticsMergeRequestEntity'
do
subject
expect
(
response
.
body
).
to
eq
'["mr_representation"]'
end
it
'sets pagination headers'
do
subject
expect
(
response
.
headers
[
'X-Per-Page'
]).
to
eq
'20'
expect
(
response
.
headers
[
'X-Page'
]).
to
eq
'1'
expect
(
response
.
headers
[
'X-Next-Page'
]).
to
eq
''
expect
(
response
.
headers
[
'X-Prev-Page'
]).
to
eq
''
expect
(
response
.
headers
[
'X-Total'
]).
to
eq
'1'
expect
(
response
.
headers
[
'X-Total-Pages'
]).
to
eq
'1'
end
end
context
'for scatterplot charts'
do
let
(
:params
)
{
{
chart_type:
'scatterplot'
,
metric_type:
'commits_count'
}
}
it
'renders whatever analytics returns for scatterplot'
do
allow
(
analytics_mock
).
to
receive
(
:scatterplot_data
).
with
(
type:
'commits_count'
).
and_return
(
'scatterplot_data'
)
subject
expect
(
response
.
body
).
to
eq
'scatterplot_data'
end
end
context
'for histogram charts'
do
let
(
:params
)
{
{
chart_type:
'histogram'
,
metric_type:
'commits_count'
}
}
it
'renders whatever analytics returns for histogram'
do
allow
(
analytics_mock
).
to
receive
(
:histogram_data
).
with
(
type:
'commits_count'
).
and_return
(
'histogram_data'
)
subject
expect
(
response
.
body
).
to
eq
'histogram_data'
end
end
end
end
ee/spec/factories/merge_requests.rb
View file @
5a4f5cd3
...
...
@@ -30,6 +30,19 @@ FactoryBot.modify do
merge_user
{
author
}
end
trait
:with_productivity_metrics
do
transient
do
metrics_data
{}
end
after
:build
do
|
mr
,
evaluator
|
next
if
evaluator
.
metrics_data
.
empty?
mr
.
build_metrics
unless
mr
.
metrics
mr
.
metrics
.
assign_attributes
evaluator
.
metrics_data
end
end
transient
do
approval_groups
[]
approval_users
[]
...
...
ee/spec/finders/productivity_analytics_finder_spec.rb
0 → 100644
View file @
5a4f5cd3
# frozen_string_literal: true
require
'spec_helper'
describe
ProductivityAnalyticsFinder
do
subject
{
described_class
.
new
(
current_user
,
search_params
.
merge
(
state: :merged
))
}
let
(
:current_user
)
{
create
(
:admin
)
}
let
(
:search_params
)
{
{}
}
describe
'.array_params'
do
subject
{
described_class
.
array_params
}
it
{
is_expected
.
to
include
(
:days_to_merge
)
}
end
describe
'.scalar_params'
do
subject
{
described_class
.
scalar_params
}
it
{
is_expected
.
to
include
(
:merged_at_before
,
:merged_at_after
)
}
end
describe
'#execute'
do
let
(
:long_mr
)
do
metrics_data
=
{
merged_at:
1
.
day
.
ago
}
create
(
:merge_request
,
:merged
,
:with_productivity_metrics
,
created_at:
31
.
days
.
ago
,
metrics_data:
metrics_data
)
end
let
(
:short_mr
)
do
metrics_data
=
{
merged_at:
28
.
days
.
ago
}
create
(
:merge_request
,
:merged
,
:with_productivity_metrics
,
created_at:
31
.
days
.
ago
,
metrics_data:
metrics_data
)
end
context
'allows to filter by days_to_merge'
do
let
(
:search_params
)
{
{
days_to_merge:
[
30
]
}
}
it
'returns all MRs with merged_at - created_at IN specified values'
do
Timecop
.
freeze
do
long_mr
short_mr
expect
(
subject
.
execute
).
to
match_array
([
long_mr
])
end
end
end
context
'allows to filter by merged_at'
do
around
do
|
example
|
Timecop
.
freeze
{
example
.
run
}
end
context
'with merged_at_after specified as timestamp'
do
let
(
:search_params
)
do
{
merged_at_after:
25
.
days
.
ago
.
to_s
}
end
it
'returns all MRs with merged date later than specified timestamp'
do
long_mr
short_mr
expect
(
subject
.
execute
).
to
match_array
([
long_mr
])
end
end
context
'with merged_at_after specified as days-range'
do
let
(
:search_params
)
do
{
merged_at_after:
'11days'
}
end
it
'returns all MRs with merged date later than Xdays ago'
do
long_mr
short_mr
expect
(
subject
.
execute
).
to
match_array
([
long_mr
])
end
end
context
'with merged_at_after and merged_at_before specified'
do
let
(
:search_params
)
do
{
merged_at_after:
30
.
days
.
ago
.
to_s
,
merged_at_before:
20
.
days
.
ago
.
to_s
}
end
it
'returns all MRs with merged date later than specified timestamp'
do
long_mr
short_mr
expect
(
subject
.
execute
).
to
match_array
([
short_mr
])
end
end
end
end
end
ee/spec/models/productivity_analytics_spec.rb
0 → 100644
View file @
5a4f5cd3
# frozen_string_literal: true
require
'spec_helper'
describe
ProductivityAnalytics
do
subject
(
:analytics
)
{
described_class
.
new
(
merge_requests:
MergeRequest
.
all
,
sort:
custom_sort
)
}
let
(
:custom_sort
)
{
nil
}
let
(
:long_mr
)
do
metrics_data
=
{
merged_at:
1
.
day
.
ago
,
first_comment_at:
31
.
days
.
ago
,
last_commit_at:
2
.
days
.
ago
,
commits_count:
20
,
diff_size:
310
,
modified_paths_size:
15
}
create
(
:merge_request
,
:merged
,
:with_productivity_metrics
,
created_at:
31
.
days
.
ago
,
metrics_data:
metrics_data
)
end
let
(
:medium_mr
)
do
metrics_data
=
{
merged_at:
1
.
day
.
ago
,
first_comment_at:
15
.
days
.
ago
,
last_commit_at:
2
.
days
.
ago
,
commits_count:
5
,
diff_size:
84
,
modified_paths_size:
3
}
create
(
:merge_request
,
:merged
,
:with_productivity_metrics
,
created_at:
15
.
days
.
ago
,
metrics_data:
metrics_data
)
end
let
(
:short_mr
)
do
metrics_data
=
{
merged_at:
28
.
days
.
ago
,
first_comment_at:
30
.
days
.
ago
,
last_commit_at:
28
.
days
.
ago
,
commits_count:
1
,
diff_size:
14
,
modified_paths_size:
3
}
create
(
:merge_request
,
:merged
,
:with_productivity_metrics
,
created_at:
31
.
days
.
ago
,
metrics_data:
metrics_data
)
end
let
(
:short_mr_2
)
do
metrics_data
=
{
merged_at:
28
.
days
.
ago
,
first_comment_at:
31
.
days
.
ago
,
last_commit_at:
29
.
days
.
ago
,
commits_count:
1
,
diff_size:
5
,
modified_paths_size:
1
}
create
(
:merge_request
,
:merged
,
:with_productivity_metrics
,
created_at:
31
.
days
.
ago
,
metrics_data:
metrics_data
)
end
before
do
Timecop
.
freeze
do
long_mr
medium_mr
short_mr
short_mr_2
end
end
describe
'#histogram_data'
do
subject
{
analytics
.
histogram_data
(
type:
metric
)
}
context
'days_to_merge metric'
do
let
(
:metric
)
{
'days_to_merge'
}
it
'returns aggregated data per days to merge from MR creation date'
do
expect
(
subject
).
to
eq
(
3
=>
2
,
14
=>
1
,
30
=>
1
)
end
end
context
'time_to_first_comment metric'
do
let
(
:metric
)
{
'time_to_first_comment'
}
it
'returns aggregated data per hours from MR creation to first comment'
do
expect
(
subject
).
to
eq
(
0
=>
3
,
24
=>
1
)
end
end
context
'time_to_last_commit metric'
do
let
(
:metric
)
{
'time_to_last_commit'
}
it
'returns aggregated data per hours from first comment to last commit'
do
expect
(
subject
).
to
eq
(
13
*
24
=>
1
,
29
*
24
=>
1
,
2
*
24
=>
2
)
end
end
context
'time_to_merge metric'
do
let
(
:metric
)
{
'time_to_merge'
}
it
'returns aggregated data per hours from last commit to merge'
do
expect
(
subject
).
to
eq
(
24
=>
3
,
0
=>
1
)
end
end
context
'commits_count metric'
do
let
(
:metric
)
{
'commits_count'
}
it
'returns aggregated data per number of commits'
do
expect
(
subject
).
to
eq
(
1
=>
2
,
5
=>
1
,
20
=>
1
)
end
end
context
'loc_per_commit metric'
do
let
(
:metric
)
{
'loc_per_commit'
}
it
'returns aggregated data per number of LoC/commits_count'
do
expect
(
subject
).
to
eq
(
15
=>
1
,
16
=>
1
,
14
=>
1
,
5
=>
1
)
end
end
context
'files_touched metric'
do
let
(
:metric
)
{
'files_touched'
}
it
'returns aggregated data per number of modified files'
do
expect
(
subject
).
to
eq
(
15
=>
1
,
3
=>
2
,
1
=>
1
)
end
end
context
'for invalid metric'
do
let
(
:metric
)
{
'something_invalid'
}
it
{
is_expected
.
to
eq
nil
}
end
end
# Test coverage depends on #histogram_data tests. We want to avoid duplication here, so test only for 1 metric.
describe
'#scatterplot_data'
do
subject
{
analytics
.
scatterplot_data
(
type:
'days_to_merge'
)
}
it
'returns metric values for each MR'
do
expect
(
subject
).
to
match
(
short_mr
.
id
=>
{
metric:
3
,
merged_at:
be_like_time
(
short_mr
.
merged_at
)
},
short_mr_2
.
id
=>
{
metric:
3
,
merged_at:
be_like_time
(
short_mr_2
.
merged_at
)
},
medium_mr
.
id
=>
{
metric:
14
,
merged_at:
be_like_time
(
medium_mr
.
merged_at
)
},
long_mr
.
id
=>
{
metric:
30
,
merged_at:
be_like_time
(
long_mr
.
merged_at
)
}
)
end
end
describe
'#merge_requests_extended'
do
subject
{
analytics
.
merge_requests_extended
}
it
'returns MRs data with all the metrics calculated'
do
expected_data
=
{
long_mr
.
id
=>
{
'days_to_merge'
=>
30
,
'time_to_first_comment'
=>
0
,
'time_to_last_commit'
=>
29
*
24
,
'time_to_merge'
=>
24
,
'commits_count'
=>
20
,
'loc_per_commit'
=>
15
,
'files_touched'
=>
15
},
medium_mr
.
id
=>
{
'days_to_merge'
=>
14
,
'time_to_first_comment'
=>
0
,
'time_to_last_commit'
=>
13
*
24
,
'time_to_merge'
=>
24
,
'commits_count'
=>
5
,
'loc_per_commit'
=>
16
,
'files_touched'
=>
3
},
short_mr
.
id
=>
{
'days_to_merge'
=>
3
,
'time_to_first_comment'
=>
24
,
'time_to_last_commit'
=>
2
*
24
,
'time_to_merge'
=>
0
,
'commits_count'
=>
1
,
'loc_per_commit'
=>
14
,
'files_touched'
=>
3
},
short_mr_2
.
id
=>
{
'days_to_merge'
=>
3
,
'time_to_first_comment'
=>
0
,
'time_to_last_commit'
=>
2
*
24
,
'time_to_merge'
=>
24
,
'commits_count'
=>
1
,
'loc_per_commit'
=>
5
,
'files_touched'
=>
1
}
}
expected_data
.
each
do
|
mr_id
,
expected_attributes
|
expect
(
subject
.
detect
{
|
mr
|
mr
.
id
==
mr_id
}.
attributes
).
to
include
(
expected_attributes
)
end
end
context
'with custom sorting'
do
let
(
:custom_sort
)
{
'loc_per_commit_asc'
}
it
'reorders MRs according to custom sorting'
do
expect
(
subject
).
to
eq
[
short_mr_2
,
short_mr
,
long_mr
,
medium_mr
]
end
context
'with unknown sorting'
do
let
(
:custom_sort
)
{
'weird_stuff'
}
it
'does not apply custom sorting'
do
expect
(
subject
).
to
eq
[
long_mr
,
medium_mr
,
short_mr
,
short_mr_2
]
end
end
end
end
end
ee/spec/policies/global_policy_spec.rb
View file @
5a4f5cd3
...
...
@@ -31,4 +31,16 @@ describe GlobalPolicy do
it
{
expect
(
described_class
.
new
(
create
(
:admin
),
[
user
])).
to
be_allowed
(
:read_licenses
)
}
it
{
expect
(
described_class
.
new
(
create
(
:admin
),
[
user
])).
to
be_allowed
(
:destroy_licenses
)
}
describe
'view_productivity_analytics'
do
context
'for admins'
do
let
(
:current_user
)
{
create
(
:admin
)
}
it
{
is_expected
.
to
be_allowed
(
:view_productivity_analytics
)
}
end
context
'for non-admins'
do
it
{
is_expected
.
not_to
be_allowed
(
:view_productivity_analytics
)
}
end
end
end
ee/spec/policies/group_policy_spec.rb
View file @
5a4f5cd3
...
...
@@ -403,4 +403,22 @@ describe GroupPolicy do
end
end
end
describe
'view_productivity_analytics'
do
%w[admin owner]
.
each
do
|
role
|
context
"for
#{
role
}
"
do
let
(
:current_user
)
{
public_send
(
role
)
}
it
{
is_expected
.
to
be_allowed
(
:view_productivity_analytics
)
}
end
end
%w[maintainer developer reporter guest]
.
each
do
|
role
|
context
"for
#{
role
}
"
do
let
(
:current_user
)
{
public_send
(
role
)
}
it
{
is_expected
.
to
be_disallowed
(
:view_productivity_analytics
)
}
end
end
end
end
ee/spec/serializers/productivity_analytics_merge_request_entity_spec.rb
0 → 100644
View file @
5a4f5cd3
# frozen_string_literal: true
require
'spec_helper'
describe
ProductivityAnalyticsMergeRequestEntity
do
subject
{
described_class
.
represent
(
merge_request
).
as_json
.
with_indifferent_access
}
let
(
:merge_request
)
{
create
(
:merge_request
)
}
before
do
ProductivityAnalytics
::
METRIC_TYPES
.
each
.
with_index
do
|
type
,
i
|
allow
(
merge_request
).
to
receive
(
type
).
and_return
(
i
)
end
end
it
'exposes all additional metrics'
do
expect
(
subject
.
keys
).
to
include
(
*
ProductivityAnalytics
::
METRIC_TYPES
)
end
it
'exposes author_avatar_url'
do
expect
(
subject
[
:author_avatar_url
]).
to
eq
merge_request
.
author
.
avatar_url
end
it
'exposes merge_request_url'
do
expect
(
subject
[
:merge_request_url
])
.
to
eq
Gitlab
::
Routing
.
url_helpers
.
project_merge_request_url
(
merge_request
.
project
,
merge_request
)
end
end
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment