Commit 7fb94850 authored by Thong Kuah's avatar Thong Kuah

Merge branch 'ag-code-analytics-feature-flag' into 'master'

Add controller and feature flag for code analytics

See merge request gitlab-org/gitlab!17618
parents eeba0ecb 9f0867fb
...@@ -6,6 +6,8 @@ class Analytics::AnalyticsController < Analytics::ApplicationController ...@@ -6,6 +6,8 @@ class Analytics::AnalyticsController < Analytics::ApplicationController
redirect_to analytics_productivity_analytics_path redirect_to analytics_productivity_analytics_path
elsif Gitlab::Analytics.cycle_analytics_enabled? elsif Gitlab::Analytics.cycle_analytics_enabled?
redirect_to analytics_cycle_analytics_path redirect_to analytics_cycle_analytics_path
elsif Gitlab::Analytics.code_analytics_enabled?
redirect_to analytics_code_analytics_path
else else
render_404 render_404
end end
......
# frozen_string_literal: true
class Analytics::CodeAnalyticsController < Analytics::ApplicationController
check_feature_flag Gitlab::Analytics::CODE_ANALYTICS_FEATURE_FLAG
before_action :load_group
before_action :load_project
before_action -> {
check_feature_availability!(:code_analytics)
}, if: -> { request.format.json? }
before_action -> {
authorize_view_by_action!(:view_code_analytics)
}
before_action :validate_params, if: -> { request.format.json? }
def show
respond_to do |format|
format.html
format.json { render json: Analytics::CodeAnalytics::RepositoryFileCommitCountEntity.represent(top_files) }
end
end
private
def validate_params
render(json: { message: 'Invalid parameters', errors: request_params.errors }, status: :unprocessable_entity) if request_params.invalid?
end
def request_params
@request_params ||= Gitlab::Analytics::CodeAnalytics::RequestParams.new(allowed_params)
end
def top_files
Analytics::CodeAnalyticsFinder.new(
project: @project,
file_count: request_params.file_count,
from: request_params.from,
to: request_params.to
).execute
end
def allowed_params
params.permit(:file_count)
end
end
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
module Analytics module Analytics
class CodeAnalyticsFinder class CodeAnalyticsFinder
RepositoryFileCommitCount = Struct.new(:repository_file, :count)
def initialize(project:, from:, to:, file_count: nil) def initialize(project:, from:, to:, file_count: nil)
@project = project @project = project
@from = from @from = from
...@@ -10,7 +12,18 @@ module Analytics ...@@ -10,7 +12,18 @@ module Analytics
end end
def execute def execute
Analytics::CodeAnalytics::RepositoryFileCommit.top_files( result.map do |(id, file_path), count|
RepositoryFileCommitCount.new(
Analytics::CodeAnalytics::RepositoryFile.new(id: id, file_path: file_path),
count
)
end
end
private
def result
@result ||= Analytics::CodeAnalytics::RepositoryFileCommit.top_files(
project: @project, project: @project,
from: @from, from: @from,
to: @to, to: @to,
......
...@@ -23,11 +23,11 @@ module Analytics ...@@ -23,11 +23,11 @@ module Analytics
raise TopFilesLimitError if file_count > MAX_FILE_COUNT raise TopFilesLimitError if file_count > MAX_FILE_COUNT
joins(:analytics_repository_file) joins(:analytics_repository_file)
.select(files_table[:file_path]) .select(files_table[:id], files_table[:file_path])
.where(project_id: project.id) .where(project_id: project.id)
.where(arel_table[:committed_date].gteq(from)) .where(arel_table[:committed_date].gteq(from))
.where(arel_table[:committed_date].lteq(to)) .where(arel_table[:committed_date].lteq(to))
.group(files_table[:file_path]) .group(files_table[:id], files_table[:file_path])
.order(arel_table[:commit_count].sum) .order(arel_table[:commit_count].sum)
.limit(file_count) .limit(file_count)
.sum(arel_table[:commit_count]) .sum(arel_table[:commit_count])
......
...@@ -48,6 +48,7 @@ class License < ApplicationRecord ...@@ -48,6 +48,7 @@ class License < ApplicationRecord
board_milestone_lists board_milestone_lists
ci_cd_projects ci_cd_projects
cluster_deployments cluster_deployments
code_analytics
code_owner_approval_required code_owner_approval_required
commit_committer_check commit_committer_check
cross_project_pipelines cross_project_pipelines
......
# frozen_string_literal: true
module Analytics
module CodeAnalytics
class RepositoryFileCommitCountEntity < Grape::Entity
expose(:id) { |model| model.repository_file.id }
expose(:name) { |model| model.repository_file.file_path }
expose :count
end
end
end
...@@ -31,4 +31,16 @@ ...@@ -31,4 +31,16 @@
= link_to analytics_cycle_analytics_path do = link_to analytics_cycle_analytics_path do
%strong.fly-out-top-item-name %strong.fly-out-top-item-name
= _('Cycle Analytics') = _('Cycle Analytics')
- if Gitlab::Analytics.code_analytics_enabled?
= nav_link(controller: :code_analytics) do
= link_to analytics_code_analytics_path, class: 'qa-sidebar-code-analytics' do
.nav-icon-container
= sprite_icon('code')
%span.nav-item-name
= _('Code Analytics')
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(controller: :code_analytics, html_options: { class: "fly-out-top-item qa-sidebar-code-analytics-fly-out" } ) do
= link_to analytics_code_analytics_path do
%strong.fly-out-top-item-name
= _('Code Analytics')
= render 'shared/sidebar_toggle_button' = render 'shared/sidebar_toggle_button'
...@@ -19,4 +19,8 @@ namespace :analytics do ...@@ -19,4 +19,8 @@ namespace :analytics do
resource :tasks_by_type, controller: :tasks_by_type, only: :show resource :tasks_by_type, controller: :tasks_by_type, only: :show
end end
end end
constraints(::Constraints::FeatureConstrainer.new(Gitlab::Analytics::CODE_ANALYTICS_FEATURE_FLAG)) do
resource :code_analytics, only: :show
end
end end
...@@ -3,11 +3,13 @@ ...@@ -3,11 +3,13 @@
module Gitlab module Gitlab
module Analytics module Analytics
# Normally each analytics feature should be guarded with a feature flag. # Normally each analytics feature should be guarded with a feature flag.
CODE_ANALYTICS_FEATURE_FLAG = :code_analytics
CYCLE_ANALYTICS_FEATURE_FLAG = :cycle_analytics CYCLE_ANALYTICS_FEATURE_FLAG = :cycle_analytics
PRODUCTIVITY_ANALYTICS_FEATURE_FLAG = :productivity_analytics PRODUCTIVITY_ANALYTICS_FEATURE_FLAG = :productivity_analytics
TASKS_BY_TYPE_CHART_FEATURE_FLAG = :tasks_by_type_chart TASKS_BY_TYPE_CHART_FEATURE_FLAG = :tasks_by_type_chart
FEATURE_FLAGS = [ FEATURE_FLAGS = [
CODE_ANALYTICS_FEATURE_FLAG,
CYCLE_ANALYTICS_FEATURE_FLAG, CYCLE_ANALYTICS_FEATURE_FLAG,
PRODUCTIVITY_ANALYTICS_FEATURE_FLAG, PRODUCTIVITY_ANALYTICS_FEATURE_FLAG,
TASKS_BY_TYPE_CHART_FEATURE_FLAG TASKS_BY_TYPE_CHART_FEATURE_FLAG
...@@ -17,6 +19,10 @@ module Gitlab ...@@ -17,6 +19,10 @@ module Gitlab
FEATURE_FLAGS.any? { |flag| Feature.enabled?(flag) } FEATURE_FLAGS.any? { |flag| Feature.enabled?(flag) }
end end
def self.code_analytics_enabled?
Feature.enabled?(CODE_ANALYTICS_FEATURE_FLAG)
end
def self.cycle_analytics_enabled? def self.cycle_analytics_enabled?
Feature.enabled?(CYCLE_ANALYTICS_FEATURE_FLAG) Feature.enabled?(CYCLE_ANALYTICS_FEATURE_FLAG)
end end
......
# frozen_string_literal: true
module Gitlab
module Analytics
module CodeAnalytics
class RequestParams
include ActiveModel::Model
include ActiveModel::Validations
attr_writer :file_count
validates :file_count, presence: true, numericality: {
only_integer: true,
greater_than: 0,
less_than_or_equal_to: ::Analytics::CodeAnalytics::RepositoryFileCommit::MAX_FILE_COUNT
}
# The date range will be customizable later, for now we load data for the last 30 days
def from
30.days.ago
end
def to
Date.today
end
def file_count
Integer(@file_count) if @file_count
end
end
end
end
end
...@@ -9,12 +9,12 @@ describe Analytics::AnalyticsController do ...@@ -9,12 +9,12 @@ describe Analytics::AnalyticsController do
before do before do
sign_in(user) sign_in(user)
disable_all_analytics_feature_flags
end end
describe 'GET index' do describe 'GET index' do
describe 'redirects to the first enabled analytics page' do describe 'redirects to the first enabled analytics page' do
it 'redirects to cycle analytics' do it 'redirects to cycle analytics' do
disable_all_analytics_feature_flags
stub_feature_flags(Gitlab::Analytics::CYCLE_ANALYTICS_FEATURE_FLAG => true) stub_feature_flags(Gitlab::Analytics::CYCLE_ANALYTICS_FEATURE_FLAG => true)
get :index get :index
...@@ -23,18 +23,23 @@ describe Analytics::AnalyticsController do ...@@ -23,18 +23,23 @@ describe Analytics::AnalyticsController do
end end
it 'redirects to productivity analytics' do it 'redirects to productivity analytics' do
disable_all_analytics_feature_flags
stub_feature_flags(Gitlab::Analytics::PRODUCTIVITY_ANALYTICS_FEATURE_FLAG => true) stub_feature_flags(Gitlab::Analytics::PRODUCTIVITY_ANALYTICS_FEATURE_FLAG => true)
get :index get :index
expect(response).to redirect_to(analytics_productivity_analytics_path) expect(response).to redirect_to(analytics_productivity_analytics_path)
end end
it 'redirects to code analytics' do
stub_feature_flags(Gitlab::Analytics::CODE_ANALYTICS_FEATURE_FLAG => true)
get :index
expect(response).to redirect_to(analytics_code_analytics_path)
end
end end
it 'renders 404 all the analytics feature flags are disabled' do it 'renders 404 all the analytics feature flags are disabled' do
disable_all_analytics_feature_flags
get :index get :index
expect(response).to have_gitlab_http_status(404) expect(response).to have_gitlab_http_status(404)
......
# frozen_string_literal: true
require 'spec_helper'
describe Analytics::CodeAnalyticsController do
let_it_be(:current_user) { create(:user) }
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, group: group) }
before do
group.add_reporter(current_user)
stub_licensed_features(code_analytics: true)
sign_in(current_user)
end
describe 'GET show' do
subject { get :show, format: :html, params: {} }
it 'renders successfully without license' do
stub_feature_flags(Gitlab::Analytics::CODE_ANALYTICS_FEATURE_FLAG => true)
stub_licensed_features(code_analytics: false)
subject
expect(response).to have_gitlab_http_status(:ok)
end
it 'renders successfully with license' do
stub_feature_flags(Gitlab::Analytics::CODE_ANALYTICS_FEATURE_FLAG => true)
stub_licensed_features(code_analytics: true)
subject
expect(response).to have_gitlab_http_status(:ok)
end
it 'renders `not_found` when feature flag is disabled' do
stub_licensed_features(code_analytics: true)
stub_feature_flags(Gitlab::Analytics::CODE_ANALYTICS_FEATURE_FLAG => false)
subject
expect(response).to have_gitlab_http_status(:not_found)
end
end
describe 'GET `show` as json' do
let(:params) { { group_id: group.full_path, project_id: project.full_path, file_count: 15 } }
subject { get :show, format: :json, params: params }
it 'renders `forbidden` without proper license' do
stub_feature_flags(Gitlab::Analytics::CODE_ANALYTICS_FEATURE_FLAG => true)
stub_licensed_features(code_analytics: false)
subject
expect(response).to have_gitlab_http_status(:forbidden)
end
it 'renders `not_found` when feature flag is disabled' do
stub_licensed_features(code_analytics: true)
stub_feature_flags(Gitlab::Analytics::CODE_ANALYTICS_FEATURE_FLAG => false)
subject
expect(response).to have_gitlab_http_status(:not_found)
end
context 'when user has lower access than reporter' do
before do
stub_feature_flags(Gitlab::Analytics::CODE_ANALYTICS_FEATURE_FLAG => true)
GroupMember.where(user: current_user).delete_all
group.add_guest(current_user)
end
it 'renders `forbidden`' do
subject
expect(response).to have_gitlab_http_status(:forbidden)
end
end
context 'when valid parameters are given' do
let_it_be(:file_commit) { create(:analytics_repository_file_commit, committed_date: 2.days.ago, project: project) }
it { expect(response).to be_successful }
it 'renders files with commit count' do
subject
first_repository_file = json_response.first
expect(first_repository_file['name']).to eq(file_commit.analytics_repository_file.file_path)
expect(first_repository_file['count']).to eq(file_commit.commit_count)
end
end
context 'when invalid parameters are given' do
context 'when `file_count` is missing' do
before do
params.delete(:file_count)
end
it 'renders error response' do
subject
expect(json_response['errors']['file_count']).not_to be_empty
end
end
context 'when `file_count` is over the limit' do
before do
params[:file_count] = Analytics::CodeAnalytics::RepositoryFileCommit::MAX_FILE_COUNT + 1
end
it 'renders error response' do
subject
expect(json_response['errors']['file_count']).not_to be_empty
end
end
end
end
end
...@@ -22,14 +22,18 @@ describe Analytics::CodeAnalyticsFinder do ...@@ -22,14 +22,18 @@ describe Analytics::CodeAnalyticsFinder do
subject { described_class.new(params).execute } subject { described_class.new(params).execute }
def find_file_count(result, file_path)
result.find { |r| r.repository_file.file_path.eql?(file_path) }
end
context 'with no commits in the given date range' do context 'with no commits in the given date range' do
before do before do
params[:from] = 5.years.ago params[:from] = 5.years.ago
params[:to] = 4.years.ago params[:to] = 4.years.ago
end end
it 'returns empty hash' do it 'returns empty array' do
expect(subject).to eq({}) expect(subject).to eq([])
end end
end end
...@@ -40,11 +44,11 @@ describe Analytics::CodeAnalyticsFinder do ...@@ -40,11 +44,11 @@ describe Analytics::CodeAnalyticsFinder do
end end
it 'sums up the gemfile commits' do it 'sums up the gemfile commits' do
expect(subject[gemfile.file_path]).to eq(3) expect(find_file_count(subject, gemfile.file_path).count).to eq(3)
end end
it 'includes the user model commit' do it 'includes the user model commit' do
expect(subject[user_model.file_path]).to eq(5) expect(find_file_count(subject, user_model.file_path).count).to eq(5)
end end
it 'verifies that the out of range record is persisted' do it 'verifies that the out of range record is persisted' do
...@@ -53,11 +57,13 @@ describe Analytics::CodeAnalyticsFinder do ...@@ -53,11 +57,13 @@ describe Analytics::CodeAnalyticsFinder do
end end
it 'does not include items outside of the date range' do it 'does not include items outside of the date range' do
expect(subject).not_to have_key(app_controller.file_path) expect(find_file_count(subject, app_controller.file_path)).to be_nil
end end
it 'orders the results by commit count' do it 'orders the results by commit count' do
expect(subject.keys).to eq([gemfile.file_path, user_model.file_path]) result_file_paths = subject.map { |item| item.repository_file.file_path }
expect(result_file_paths).to eq([gemfile.file_path, user_model.file_path])
end end
context 'when `file_count` is given' do context 'when `file_count` is given' do
......
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::Analytics::CodeAnalytics::RequestParams do
let(:params) { { file_count: 5 } }
subject { described_class.new(params) }
it 'is valid' do
expect(subject).to be_valid
end
context 'when `file_count` is invalid' do
before do
params[:file_count] = -1
end
it 'is invalid' do
expect(subject).not_to be_valid
expect(subject.errors[:file_count]).not_to be_empty
end
end
end
...@@ -22,7 +22,7 @@ describe Analytics::CodeAnalytics::RepositoryFileCommit do ...@@ -22,7 +22,7 @@ describe Analytics::CodeAnalytics::RepositoryFileCommit do
let!(:file_commit1) { create(:analytics_repository_file_commit, { project: project, analytics_repository_file: file, committed_date: 1.day.ago, commit_count: 2 }) } let!(:file_commit1) { create(:analytics_repository_file_commit, { project: project, analytics_repository_file: file, committed_date: 1.day.ago, commit_count: 2 }) }
let!(:file_commit2) { create(:analytics_repository_file_commit, { project: project, analytics_repository_file: file, committed_date: 2.days.ago, commit_count: 2 }) } let!(:file_commit2) { create(:analytics_repository_file_commit, { project: project, analytics_repository_file: file, committed_date: 2.days.ago, commit_count: 2 }) }
it { expect(subject[file.file_path]).to eq(4) } it { expect(subject[[file.id, file.file_path]]).to eq(4) }
end end
context 'when the `file_count` is higher than allowed' do context 'when the `file_count` is higher than allowed' do
......
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