Commit 9b26541f authored by Tiger Watson's avatar Tiger Watson

Merge branch '326008-add-codeowners-to-devops-adoption' into 'master'

Add code owners metric to DevOps adoption page [RUN ALL RSPEC] [RUN AS-IF-FOSS]

See merge request gitlab-org/gitlab!59874
parents 6a17dfb0 3710b23a
---
title: Add code owners metric to DevOps adoption page
merge_request: 59874
author:
type: added
# frozen_string_literal: true
class AddCodeownersDevopsAdoptionSnapshot < ActiveRecord::Migration[6.0]
def change
add_column :analytics_devops_adoption_snapshots, :total_projects_count, :integer
add_column :analytics_devops_adoption_snapshots, :code_owners_used_count, :integer
end
end
9049dc22e97261115ba935a059beb5b4f2eb810f1fdcc0881f96d4b6a501ab09
\ No newline at end of file
...@@ -9084,7 +9084,9 @@ CREATE TABLE analytics_devops_adoption_snapshots ( ...@@ -9084,7 +9084,9 @@ CREATE TABLE analytics_devops_adoption_snapshots (
pipeline_succeeded boolean NOT NULL, pipeline_succeeded boolean NOT NULL,
deploy_succeeded boolean NOT NULL, deploy_succeeded boolean NOT NULL,
security_scan_succeeded boolean NOT NULL, security_scan_succeeded boolean NOT NULL,
end_time timestamp with time zone NOT NULL end_time timestamp with time zone NOT NULL,
total_projects_count integer,
code_owners_used_count integer
); );
CREATE SEQUENCE analytics_devops_adoption_snapshots_id_seq CREATE SEQUENCE analytics_devops_adoption_snapshots_id_seq
...@@ -7870,6 +7870,7 @@ Snapshot. ...@@ -7870,6 +7870,7 @@ Snapshot.
| Name | Type | Description | | Name | Type | Description |
| ---- | ---- | ----------- | | ---- | ---- | ----------- |
| <a id="devopsadoptionsnapshotcodeownersusedcount"></a>`codeOwnersUsedCount` | [`Int`](#int) | Total number of projects with existing CODEOWNERS file. |
| <a id="devopsadoptionsnapshotdeploysucceeded"></a>`deploySucceeded` | [`Boolean!`](#boolean) | At least one deployment succeeded. | | <a id="devopsadoptionsnapshotdeploysucceeded"></a>`deploySucceeded` | [`Boolean!`](#boolean) | At least one deployment succeeded. |
| <a id="devopsadoptionsnapshotendtime"></a>`endTime` | [`Time!`](#time) | The end time for the snapshot where the data points were collected. | | <a id="devopsadoptionsnapshotendtime"></a>`endTime` | [`Time!`](#time) | The end time for the snapshot where the data points were collected. |
| <a id="devopsadoptionsnapshotissueopened"></a>`issueOpened` | [`Boolean!`](#boolean) | At least one issue was opened. | | <a id="devopsadoptionsnapshotissueopened"></a>`issueOpened` | [`Boolean!`](#boolean) | At least one issue was opened. |
...@@ -7880,6 +7881,7 @@ Snapshot. ...@@ -7880,6 +7881,7 @@ Snapshot.
| <a id="devopsadoptionsnapshotrunnerconfigured"></a>`runnerConfigured` | [`Boolean!`](#boolean) | At least one runner was used. | | <a id="devopsadoptionsnapshotrunnerconfigured"></a>`runnerConfigured` | [`Boolean!`](#boolean) | At least one runner was used. |
| <a id="devopsadoptionsnapshotsecurityscansucceeded"></a>`securityScanSucceeded` | [`Boolean!`](#boolean) | At least one security scan succeeded. | | <a id="devopsadoptionsnapshotsecurityscansucceeded"></a>`securityScanSucceeded` | [`Boolean!`](#boolean) | At least one security scan succeeded. |
| <a id="devopsadoptionsnapshotstarttime"></a>`startTime` | [`Time!`](#time) | The start time for the snapshot where the data points were collected. | | <a id="devopsadoptionsnapshotstarttime"></a>`startTime` | [`Time!`](#time) | The start time for the snapshot where the data points were collected. |
| <a id="devopsadoptionsnapshottotalprojectscount"></a>`totalProjectsCount` | [`Int`](#int) | Total number of projects. |
### `DiffPosition` ### `DiffPosition`
......
...@@ -23,6 +23,10 @@ module Types ...@@ -23,6 +23,10 @@ module Types
description: 'At least one deployment succeeded.' description: 'At least one deployment succeeded.'
field :security_scan_succeeded, GraphQL::BOOLEAN_TYPE, null: false, field :security_scan_succeeded, GraphQL::BOOLEAN_TYPE, null: false,
description: 'At least one security scan succeeded.' description: 'At least one security scan succeeded.'
field :code_owners_used_count, GraphQL::INT_TYPE, null: true,
description: 'Total number of projects with existing CODEOWNERS file.'
field :total_projects_count, GraphQL::INT_TYPE, null: true,
description: 'Total number of projects.'
field :recorded_at, Types::TimeType, null: false, field :recorded_at, Types::TimeType, null: false,
description: 'The time the snapshot was recorded.' description: 'The time the snapshot was recorded.'
field :start_time, Types::TimeType, null: false, field :start_time, Types::TimeType, null: false,
......
...@@ -13,6 +13,8 @@ class Analytics::DevopsAdoption::Snapshot < ApplicationRecord ...@@ -13,6 +13,8 @@ class Analytics::DevopsAdoption::Snapshot < ApplicationRecord
validates :pipeline_succeeded, inclusion: { in: [true, false] } validates :pipeline_succeeded, inclusion: { in: [true, false] }
validates :deploy_succeeded, inclusion: { in: [true, false] } validates :deploy_succeeded, inclusion: { in: [true, false] }
validates :security_scan_succeeded, inclusion: { in: [true, false] } validates :security_scan_succeeded, inclusion: { in: [true, false] }
validates :total_projects_count, numericality: { only_integer: true, greater_than_or_equal_to: 0 }, allow_nil: true
validates :code_owners_used_count, numericality: { only_integer: true, greater_than_or_equal_to: 0 }, allow_nil: true
scope :latest_snapshot_for_segment_ids, -> (ids) do scope :latest_snapshot_for_segment_ids, -> (ids) do
inner_select = model inner_select = model
......
...@@ -81,7 +81,7 @@ module EE ...@@ -81,7 +81,7 @@ module EE
::Geo::RepositoryUpdatedService.new(self).execute ::Geo::RepositoryUpdatedService.new(self).execute
end end
def code_owners_blob(ref: 'HEAD') def code_owners_blob(ref:)
possible_code_owner_blobs = ::Gitlab::CodeOwners::FILE_PATHS.map { |path| [ref, path] } possible_code_owner_blobs = ::Gitlab::CodeOwners::FILE_PATHS.map { |path| [ref, path] }
blobs_at(possible_code_owner_blobs).compact.first blobs_at(possible_code_owner_blobs).compact.first
end end
......
...@@ -4,19 +4,12 @@ module Analytics ...@@ -4,19 +4,12 @@ module Analytics
module DevopsAdoption module DevopsAdoption
module Snapshots module Snapshots
class UpdateService class UpdateService
ALLOWED_ATTRIBUTES = [ ALLOWED_ATTRIBUTES = ([
:segment, :segment,
:segment_id, :segment_id,
:end_time, :end_time,
:recorded_at, :recorded_at
:issue_opened, ] + SnapshotCalculator::ADOPTION_METRICS).freeze
:merge_request_opened,
:merge_request_approved,
:runner_configured,
:pipeline_succeeded,
:deploy_succeeded,
:security_scan_succeeded
].freeze
def initialize(snapshot:, params: {}) def initialize(snapshot:, params: {})
@snapshot = snapshot @snapshot = snapshot
......
---
name: analytics_devops_adoption_codeowners
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/59874
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/328542
milestone: '13.12'
type: development
group: group::optimize
default_enabled: true
...@@ -35,6 +35,8 @@ Gitlab::Seeder.quiet do ...@@ -35,6 +35,8 @@ Gitlab::Seeder.quiet do
pipeline_succeeded: booleans.sample, pipeline_succeeded: booleans.sample,
deploy_succeeded: booleans.sample, deploy_succeeded: booleans.sample,
security_scan_succeeded: booleans.sample, security_scan_succeeded: booleans.sample,
code_owners_used_count: rand(10),
total_projects_count: rand(10..19),
recorded_at: [end_time + 1.day, Time.zone.now].min, recorded_at: [end_time + 1.day, Time.zone.now].min,
end_time: end_time end_time: end_time
} }
......
...@@ -5,7 +5,22 @@ module Analytics ...@@ -5,7 +5,22 @@ module Analytics
class SnapshotCalculator class SnapshotCalculator
attr_reader :segment, :range_end, :range_start, :snapshot attr_reader :segment, :range_end, :range_start, :snapshot
ADOPTION_FLAGS = %i[issue_opened merge_request_opened merge_request_approved runner_configured pipeline_succeeded deploy_succeeded security_scan_succeeded].freeze BOOLEAN_METRICS = [
:issue_opened,
:merge_request_opened,
:merge_request_approved,
:runner_configured,
:pipeline_succeeded,
:deploy_succeeded,
:security_scan_succeeded
].freeze
NUMERIC_METRICS = [
:code_owners_used_count,
:total_projects_count
].freeze
ADOPTION_METRICS = BOOLEAN_METRICS + NUMERIC_METRICS
def initialize(segment:, range_end:, snapshot: nil) def initialize(segment:, range_end:, snapshot: nil)
@segment = segment @segment = segment
...@@ -17,8 +32,12 @@ module Analytics ...@@ -17,8 +32,12 @@ module Analytics
def calculate def calculate
params = { recorded_at: Time.zone.now, end_time: range_end, segment: segment } params = { recorded_at: Time.zone.now, end_time: range_end, segment: segment }
ADOPTION_FLAGS.each do |flag| BOOLEAN_METRICS.each do |metric|
params[flag] = snapshot&.public_send(flag) || send(flag) # rubocop:disable GitlabSecurity/PublicSend params[metric] = snapshot&.public_send(metric) || send(metric) # rubocop:disable GitlabSecurity/PublicSend
end
NUMERIC_METRICS.each do |metric|
params[metric] = send(metric) # rubocop:disable GitlabSecurity/PublicSend
end end
params params
...@@ -32,10 +51,14 @@ module Analytics ...@@ -32,10 +51,14 @@ module Analytics
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
def snapshot_project_ids def snapshot_project_ids
@snapshot_project_ids ||= Project.in_namespace(snapshot_groups).pluck(:id) @snapshot_project_ids ||= snapshot_projects.pluck(:id)
end end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
def snapshot_projects
@snapshot_projects ||= Project.in_namespace(snapshot_groups)
end
def snapshot_merge_requests def snapshot_merge_requests
@snapshot_merge_requests ||= MergeRequest.of_projects(snapshot_project_ids) @snapshot_merge_requests ||= MergeRequest.of_projects(snapshot_project_ids)
end end
...@@ -76,6 +99,18 @@ module Analytics ...@@ -76,6 +99,18 @@ module Analytics
.exists? .exists?
end end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
def total_projects_count
snapshot_project_ids.count
end
def code_owners_used_count
return unless Feature.enabled?(:analytics_devops_adoption_codeowners, segment.namespace, default_enabled: :yaml)
snapshot_projects.count do |project|
!Gitlab::CodeOwners::Loader.new(project, project.default_branch).empty_code_owners?
end
end
end end
end end
end end
...@@ -5,7 +5,7 @@ module Gitlab ...@@ -5,7 +5,7 @@ module Gitlab
class Loader class Loader
include ::Gitlab::Utils::StrongMemoize include ::Gitlab::Utils::StrongMemoize
def initialize(project, ref, paths) def initialize(project, ref, paths = [])
@project = project @project = project
@ref = ref @ref = ref
@paths = Array(paths) @paths = Array(paths)
......
...@@ -128,11 +128,38 @@ RSpec.describe Analytics::DevopsAdoption::SnapshotCalculator do ...@@ -128,11 +128,38 @@ RSpec.describe Analytics::DevopsAdoption::SnapshotCalculator do
it { is_expected.to eq false } it { is_expected.to eq false }
end end
context 'when snapshot already exists' do describe 'total_projects_count' do
let_it_be(:snapshot) { create :devops_adoption_snapshot, segment: segment, issue_opened: true, merge_request_opened: false } subject { data[:total_projects_count] }
it { is_expected.to eq 2 }
end
describe 'code_owners_used_count' do
let!(:project_with_code_owners) { create(:project, :repository, group: subgroup)}
subject { data[:code_owners_used_count] }
before do
allow_any_instance_of(Project).to receive(:default_branch).and_return('with-codeowners') # rubocop:disable RSpec/AnyInstanceOf
end
it { is_expected.to eq 1 }
context 'when feature is disabled' do
before do
stub_feature_flags(analytics_devops_adoption_codeowners: false)
end
it { is_expected.to eq nil }
end
end
context 'when snapshot already exists' do
subject(:data) { described_class.new(segment: segment, range_end: range_end, snapshot: snapshot).calculate } subject(:data) { described_class.new(segment: segment, range_end: range_end, snapshot: snapshot).calculate }
let(:snapshot) { create :devops_adoption_snapshot, segment: segment, issue_opened: true, merge_request_opened: false, total_projects_count: 1}
context 'for boolean metrics' do
let!(:fresh_merge_request) { create(:merge_request, source_project: project, created_at: 3.weeks.ago(range_end)) } let!(:fresh_merge_request) { create(:merge_request, source_project: project, created_at: 3.weeks.ago(range_end)) }
it 'calculates metrics which are not true yet' do it 'calculates metrics which are not true yet' do
...@@ -143,4 +170,11 @@ RSpec.describe Analytics::DevopsAdoption::SnapshotCalculator do ...@@ -143,4 +170,11 @@ RSpec.describe Analytics::DevopsAdoption::SnapshotCalculator do
expect(data[:issue_opened]).to eq true expect(data[:issue_opened]).to eq true
end end
end end
context 'for numeric metrics' do
it 'always recalculates metric' do
expect(data[:total_projects_count]).to eq 2
end
end
end
end end
...@@ -173,12 +173,12 @@ RSpec.describe Repository do ...@@ -173,12 +173,12 @@ RSpec.describe Repository do
it 'requests the CODOWNER blobs in batch in the correct order' do it 'requests the CODOWNER blobs in batch in the correct order' do
expect(repository).to receive(:blobs_at) expect(repository).to receive(:blobs_at)
.with([%w(HEAD CODEOWNERS), .with([%w(master CODEOWNERS),
%w(HEAD docs/CODEOWNERS), %w(master docs/CODEOWNERS),
%w(HEAD .gitlab/CODEOWNERS)]) %w(master .gitlab/CODEOWNERS)])
.and_call_original .and_call_original
repository.code_owners_blob repository.code_owners_blob(ref: 'master')
end end
end end
......
...@@ -7,9 +7,14 @@ RSpec.describe Analytics::DevopsAdoption::Snapshots::CreateService do ...@@ -7,9 +7,14 @@ RSpec.describe Analytics::DevopsAdoption::Snapshots::CreateService do
let(:snapshot) { service_response.payload[:snapshot] } let(:snapshot) { service_response.payload[:snapshot] }
let(:params) do let(:params) do
params = Analytics::DevopsAdoption::SnapshotCalculator::ADOPTION_FLAGS.each_with_object({}) do |attribute, result| params = {}
result[attribute] = rand(2).odd? Analytics::DevopsAdoption::SnapshotCalculator::BOOLEAN_METRICS.each.with_index do |attribute, i|
params[attribute] = i.odd?
end end
Analytics::DevopsAdoption::SnapshotCalculator::NUMERIC_METRICS.each.with_index do |attribute, i|
params[attribute] = i
end
params[:recorded_at] = Time.zone.now params[:recorded_at] = Time.zone.now
params[:end_time] = 1.month.ago.end_of_month params[:end_time] = 1.month.ago.end_of_month
params[:segment] = segment params[:segment] = segment
......
...@@ -9,8 +9,12 @@ RSpec.describe Analytics::DevopsAdoption::Snapshots::UpdateService do ...@@ -9,8 +9,12 @@ RSpec.describe Analytics::DevopsAdoption::Snapshots::UpdateService do
let(:segment) { create(:devops_adoption_segment, last_recorded_at: 1.year.ago) } let(:segment) { create(:devops_adoption_segment, last_recorded_at: 1.year.ago) }
let(:params) do let(:params) do
params = Analytics::DevopsAdoption::SnapshotCalculator::ADOPTION_FLAGS.each_with_object({}) do |attribute, result| params = {}
result[attribute] = rand(2).odd? Analytics::DevopsAdoption::SnapshotCalculator::BOOLEAN_METRICS.each.with_index do |attribute, i|
params[attribute] = i.odd?
end
Analytics::DevopsAdoption::SnapshotCalculator::NUMERIC_METRICS.each.with_index do |attribute, i|
params[attribute] = i
end end
params[:recorded_at] = Time.zone.now params[:recorded_at] = Time.zone.now
params[:segment] = segment params[:segment] = segment
......
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