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 (
pipeline_succeeded boolean NOT NULL,
deploy_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
......@@ -7870,6 +7870,7 @@ Snapshot.
| 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="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. |
......@@ -7880,6 +7881,7 @@ Snapshot.
| <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="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`
......
......@@ -23,6 +23,10 @@ module Types
description: 'At least one deployment succeeded.'
field :security_scan_succeeded, GraphQL::BOOLEAN_TYPE, null: false,
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,
description: 'The time the snapshot was recorded.'
field :start_time, Types::TimeType, null: false,
......
......@@ -13,6 +13,8 @@ class Analytics::DevopsAdoption::Snapshot < ApplicationRecord
validates :pipeline_succeeded, inclusion: { in: [true, false] }
validates :deploy_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
inner_select = model
......
......@@ -81,7 +81,7 @@ module EE
::Geo::RepositoryUpdatedService.new(self).execute
end
def code_owners_blob(ref: 'HEAD')
def code_owners_blob(ref:)
possible_code_owner_blobs = ::Gitlab::CodeOwners::FILE_PATHS.map { |path| [ref, path] }
blobs_at(possible_code_owner_blobs).compact.first
end
......
......@@ -4,19 +4,12 @@ module Analytics
module DevopsAdoption
module Snapshots
class UpdateService
ALLOWED_ATTRIBUTES = [
ALLOWED_ATTRIBUTES = ([
:segment,
:segment_id,
:end_time,
:recorded_at,
:issue_opened,
:merge_request_opened,
:merge_request_approved,
:runner_configured,
:pipeline_succeeded,
:deploy_succeeded,
:security_scan_succeeded
].freeze
:recorded_at
] + SnapshotCalculator::ADOPTION_METRICS).freeze
def initialize(snapshot:, params: {})
@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
pipeline_succeeded: booleans.sample,
deploy_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,
end_time: end_time
}
......
......@@ -5,7 +5,22 @@ module Analytics
class SnapshotCalculator
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)
@segment = segment
......@@ -17,8 +32,12 @@ module Analytics
def calculate
params = { recorded_at: Time.zone.now, end_time: range_end, segment: segment }
ADOPTION_FLAGS.each do |flag|
params[flag] = snapshot&.public_send(flag) || send(flag) # rubocop:disable GitlabSecurity/PublicSend
BOOLEAN_METRICS.each do |metric|
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
params
......@@ -32,10 +51,14 @@ module Analytics
# rubocop: disable CodeReuse/ActiveRecord
def snapshot_project_ids
@snapshot_project_ids ||= Project.in_namespace(snapshot_groups).pluck(:id)
@snapshot_project_ids ||= snapshot_projects.pluck(:id)
end
# rubocop: enable CodeReuse/ActiveRecord
def snapshot_projects
@snapshot_projects ||= Project.in_namespace(snapshot_groups)
end
def snapshot_merge_requests
@snapshot_merge_requests ||= MergeRequest.of_projects(snapshot_project_ids)
end
......@@ -76,6 +99,18 @@ module Analytics
.exists?
end
# 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
......@@ -5,7 +5,7 @@ module Gitlab
class Loader
include ::Gitlab::Utils::StrongMemoize
def initialize(project, ref, paths)
def initialize(project, ref, paths = [])
@project = project
@ref = ref
@paths = Array(paths)
......
......@@ -128,19 +128,53 @@ RSpec.describe Analytics::DevopsAdoption::SnapshotCalculator do
it { is_expected.to eq false }
end
context 'when snapshot already exists' do
let_it_be(:snapshot) { create :devops_adoption_snapshot, segment: segment, issue_opened: true, merge_request_opened: false }
describe 'total_projects_count' do
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 }
let!(:fresh_merge_request) { create(:merge_request, source_project: project, created_at: 3.weeks.ago(range_end)) }
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)) }
it 'calculates metrics which are not true yet' do
expect(data[:merge_request_opened]).to eq true
end
it 'calculates metrics which are not true yet' do
expect(data[:merge_request_opened]).to eq true
it "doesn't change metrics which are true already" do
expect(data[:issue_opened]).to eq true
end
end
it "doesn't change metrics which are true already" do
expect(data[:issue_opened]).to eq true
context 'for numeric metrics' do
it 'always recalculates metric' do
expect(data[:total_projects_count]).to eq 2
end
end
end
end
......@@ -173,12 +173,12 @@ RSpec.describe Repository do
it 'requests the CODOWNER blobs in batch in the correct order' do
expect(repository).to receive(:blobs_at)
.with([%w(HEAD CODEOWNERS),
%w(HEAD docs/CODEOWNERS),
%w(HEAD .gitlab/CODEOWNERS)])
.with([%w(master CODEOWNERS),
%w(master docs/CODEOWNERS),
%w(master .gitlab/CODEOWNERS)])
.and_call_original
repository.code_owners_blob
repository.code_owners_blob(ref: 'master')
end
end
......
......@@ -7,9 +7,14 @@ RSpec.describe Analytics::DevopsAdoption::Snapshots::CreateService do
let(:snapshot) { service_response.payload[:snapshot] }
let(:params) do
params = Analytics::DevopsAdoption::SnapshotCalculator::ADOPTION_FLAGS.each_with_object({}) do |attribute, result|
result[attribute] = rand(2).odd?
params = {}
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
params[:recorded_at] = Time.zone.now
params[:end_time] = 1.month.ago.end_of_month
params[:segment] = segment
......
......@@ -9,8 +9,12 @@ RSpec.describe Analytics::DevopsAdoption::Snapshots::UpdateService do
let(:segment) { create(:devops_adoption_segment, last_recorded_at: 1.year.ago) }
let(:params) do
params = Analytics::DevopsAdoption::SnapshotCalculator::ADOPTION_FLAGS.each_with_object({}) do |attribute, result|
result[attribute] = rand(2).odd?
params = {}
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
params[:recorded_at] = Time.zone.now
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