diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb index 7d3e71b003e31b4f3ff25bff7ea19fd9db69001b..a3bc871edf9701101d39c30c45b987f725898bb9 100644 --- a/app/services/ci/create_pipeline_service.rb +++ b/app/services/ci/create_pipeline_service.rb @@ -36,6 +36,7 @@ class CreatePipelineService < BaseService Gitlab::Ci::Pipeline::Chain::CancelPendingPipelines, Gitlab::Ci::Pipeline::Chain::Metrics, Gitlab::Ci::Pipeline::Chain::TemplateUsage, + Gitlab::Ci::Pipeline::Chain::ComponentUsage, Gitlab::Ci::Pipeline::Chain::Pipeline::Process].freeze # Create a new pipeline in the specified project. diff --git a/config/events/ci_catalog_component_included.yml b/config/events/ci_catalog_component_included.yml new file mode 100644 index 0000000000000000000000000000000000000000..1c43c3b15bc3a14ad770212477c1c0a3a141e051 --- /dev/null +++ b/config/events/ci_catalog_component_included.yml @@ -0,0 +1,20 @@ +--- +description: When a CI/CD Catalog component is included in a pipeline. +internal_events: true +action: ci_catalog_component_included +identifiers: +- project +- namespace +- user +product_section: ci +product_stage: verify +product_group: pipeline_authoring +milestone: '16.11' +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/146834 +distributions: +- ce +- ee +tiers: +- free +- premium +- ultimate diff --git a/config/feature_flags/gitlab_com_derisk/ci_track_catalog_component_usage.yml b/config/feature_flags/gitlab_com_derisk/ci_track_catalog_component_usage.yml new file mode 100644 index 0000000000000000000000000000000000000000..78e36c0733ce8604626c81aee1d8a64c6775c200 --- /dev/null +++ b/config/feature_flags/gitlab_com_derisk/ci_track_catalog_component_usage.yml @@ -0,0 +1,9 @@ +--- +name: ci_track_catalog_component_usage +feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/440382 +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/146834 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/446290 +milestone: '16.11' +group: group::pipeline authoring +type: gitlab_com_derisk +default_enabled: false diff --git a/config/metrics/counts_28d/count_distinct_project_id_from_ci_catalog_component_included_monthly.yml b/config/metrics/counts_28d/count_distinct_project_id_from_ci_catalog_component_included_monthly.yml new file mode 100644 index 0000000000000000000000000000000000000000..ab3e980ff5798a036ec5fe590d7762dcadf89a4f --- /dev/null +++ b/config/metrics/counts_28d/count_distinct_project_id_from_ci_catalog_component_included_monthly.yml @@ -0,0 +1,24 @@ +--- +key_path: redis_hll_counters.count_distinct_project_id_from_ci_catalog_component_included_monthly +description: Monthly count of unique projects that included a CI/CD Catalog component in a pipeline. +product_section: ci +product_stage: verify +product_group: pipeline_authoring +performance_indicator_type: [] +value_type: number +status: active +milestone: '16.11' +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/146834 +time_frame: 28d +data_source: internal_events +data_category: optional +distribution: +- ce +- ee +tier: +- free +- premium +- ultimate +events: +- name: ci_catalog_component_included + unique: project.id diff --git a/config/metrics/counts_7d/count_distinct_project_id_from_ci_catalog_component_included_weekly.yml b/config/metrics/counts_7d/count_distinct_project_id_from_ci_catalog_component_included_weekly.yml new file mode 100644 index 0000000000000000000000000000000000000000..63a2edad6367c01e1c19c1b07de14f2fe9474ebe --- /dev/null +++ b/config/metrics/counts_7d/count_distinct_project_id_from_ci_catalog_component_included_weekly.yml @@ -0,0 +1,24 @@ +--- +key_path: redis_hll_counters.count_distinct_project_id_from_ci_catalog_component_included_weekly +description: Weekly count of unique projects that included a CI/CD Catalog component in a pipeline. +product_section: ci +product_stage: verify +product_group: pipeline_authoring +performance_indicator_type: [] +value_type: number +status: active +milestone: '16.11' +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/146834 +time_frame: 7d +data_source: internal_events +data_category: optional +distribution: +- ce +- ee +tier: +- free +- premium +- ultimate +events: +- name: ci_catalog_component_included + unique: project.id diff --git a/lib/gitlab/ci/pipeline/chain/component_usage.rb b/lib/gitlab/ci/pipeline/chain/component_usage.rb new file mode 100644 index 0000000000000000000000000000000000000000..79b8ea2d9db0f7fd7e11e5658a18eac840b5ab3b --- /dev/null +++ b/lib/gitlab/ci/pipeline/chain/component_usage.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + module Pipeline + module Chain + class ComponentUsage < Chain::Base + def perform! + return unless Feature.enabled?(:ci_track_catalog_component_usage, project) + + included_catalog_components.each do |component| + track_event(component) + end + end + + def break? + false + end + + private + + def track_event(component) + Gitlab::InternalEvents.track_event( + 'ci_catalog_component_included', + namespace: project.namespace, + project: project, + user: current_user, + additional_properties: { + label: component.project.full_path, + property: "#{component.name}@#{component.version.name}", + value: component.resource_type_before_type_cast + } + ) + end + + def included_catalog_components + command.yaml_processor_result.included_components.filter_map do |hash| + ::Ci::Catalog::ComponentsProject.new(hash[:component_project], hash[:component_sha]) + .find_catalog_component(hash[:component_name]) + end + end + end + end + end + end +end diff --git a/spec/lib/gitlab/ci/pipeline/chain/component_usage_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/component_usage_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..f4291f4938f64f7a3c692fd090efd54d4102b31c --- /dev/null +++ b/spec/lib/gitlab/ci/pipeline/chain/component_usage_spec.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Pipeline::Chain::ComponentUsage, feature_category: :pipeline_composition do + let_it_be(:project) { create(:project) } + let_it_be(:user) { create(:user) } + let_it_be(:pipeline) { create(:ci_pipeline, project: project) } + let_it_be(:resource) { create(:ci_catalog_resource) } + + let_it_be(:version) do + create(:release, :with_catalog_resource_version, project: resource.project, tag: '1.2.0', sha: 'my_component_sha') + .catalog_resource_version + end + + let_it_be(:component) { create(:ci_catalog_resource_component, version: version, name: 'my_component') } + + let(:dry_run) { false } + let(:command) { Gitlab::Ci::Pipeline::Chain::Command.new(project: project, current_user: user, dry_run: dry_run) } + let(:step) { described_class.new(pipeline, command) } + + describe '#perform!' do + subject(:perform) { step.perform! } + + before do + allow(command).to receive(:yaml_processor_result) + .and_return(instance_double(Gitlab::Ci::YamlProcessor::Result, + included_components: [{ + component_project: component.project, + component_sha: version.sha, + component_name: component.name + }] + )) + end + + it_behaves_like 'internal event tracking' do + let(:event) { 'ci_catalog_component_included' } + let(:label) { component.project.full_path } + let(:property) { 'my_component@1.2.0' } + let(:value) { 1 } # Default resource_type + end + + context 'when the FF `ci_track_catalog_component_usage` is disabled' do + before do + stub_feature_flags(ci_track_catalog_component_usage: false) + end + + it 'does not track an internal event' do + expect(Gitlab::InternalEvents).not_to receive(:track_event) + + perform + end + end + end +end diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb index 33c4e6a88d5550321e147320bbb2738a92dd2385..0ba3b4572f0905a28681c3bafd50263a0b63565e 100644 --- a/spec/services/ci/create_pipeline_service_spec.rb +++ b/spec/services/ci/create_pipeline_service_spec.rb @@ -146,6 +146,14 @@ def execute_service( execute_service end + it 'tracks included catalog component usage' do + expect_next_instance_of(Gitlab::Ci::Pipeline::Chain::ComponentUsage) do |instance| + expect(instance).to receive(:perform!) + end + + execute_service + end + context 'when merge requests already exist for this source branch' do let!(:merge_request_1) do create(:merge_request, source_branch: 'feature', target_branch: "master", source_project: project) diff --git a/spec/support/shared_examples/controllers/internal_event_tracking_examples.rb b/spec/support/shared_examples/controllers/internal_event_tracking_examples.rb index 57b92907c01f782021e28f928cba91867c2fdc46..e421611e0705f790efba35adbecd9e6af4e2d787 100644 --- a/spec/support/shared_examples/controllers/internal_event_tracking_examples.rb +++ b/spec/support/shared_examples/controllers/internal_event_tracking_examples.rb @@ -26,6 +26,12 @@ namespace = try(:namespace) || project&.namespace category = try(:category) || 'InternalEventTracking' + additional_properties = { + label: try(:label), + property: try(:property), + value: try(:value) + }.compact + expect(Gitlab::Tracking::StandardContext) .to have_received(:new) .with( @@ -47,7 +53,8 @@ context: [ an_instance_of(SnowplowTracker::SelfDescribingJson), an_instance_of(SnowplowTracker::SelfDescribingJson) - ] + ], + **additional_properties ) end end