Commit 46d77a8b authored by Alex Buijs's avatar Alex Buijs

Add create pipeline namespace onboarding action

For tracking onboarding progress
parent e970bf06
...@@ -438,6 +438,10 @@ class Namespace < ApplicationRecord ...@@ -438,6 +438,10 @@ class Namespace < ApplicationRecord
end end
end end
def root?
!has_parent?
end
private private
def all_projects_with_pages def all_projects_with_pages
......
...@@ -10,9 +10,15 @@ class NamespaceOnboardingAction < ApplicationRecord ...@@ -10,9 +10,15 @@ class NamespaceOnboardingAction < ApplicationRecord
git_write: 2, git_write: 2,
merge_request_created: 3, merge_request_created: 3,
git_read: 4, git_read: 4,
pipeline_created: 5,
user_added: 6 user_added: 6
}.freeze }.freeze
# The monitoring window prevents the recording of a namespace_onboarding_action if a namespace is created before this
# time span. We are not interested in older namspaces, because the purpose of this table is to monitor and act on the
# progress of newly created namespaces or namespaces that already have at least one recorded action.
MONITORING_WINDOW = 90.days
enum action: ACTIONS enum action: ACTIONS
class << self class << self
...@@ -21,6 +27,9 @@ class NamespaceOnboardingAction < ApplicationRecord ...@@ -21,6 +27,9 @@ class NamespaceOnboardingAction < ApplicationRecord
end end
def create_action(namespace, action) def create_action(namespace, action)
return unless namespace.root?
return if namespace.created_at < MONITORING_WINDOW.ago && !namespace.namespace_onboarding_actions.exists?
NamespaceOnboardingAction.safe_find_or_create_by(namespace: namespace, action: action) NamespaceOnboardingAction.safe_find_or_create_by(namespace: namespace, action: action)
end end
end end
......
...@@ -84,6 +84,7 @@ module Ci ...@@ -84,6 +84,7 @@ module Ci
if pipeline.persisted? if pipeline.persisted?
schedule_head_pipeline_update schedule_head_pipeline_update
record_conversion_event record_conversion_event
create_namespace_onboarding_action
end end
# If pipeline is not persisted, try to recover IID # If pipeline is not persisted, try to recover IID
...@@ -123,6 +124,10 @@ module Ci ...@@ -123,6 +124,10 @@ module Ci
Experiments::RecordConversionEventWorker.perform_async(:ci_syntax_templates, current_user.id) Experiments::RecordConversionEventWorker.perform_async(:ci_syntax_templates, current_user.id)
end end
def create_namespace_onboarding_action
Namespaces::OnboardingPipelineCreatedWorker.perform_async(project.namespace_id)
end
def extra_options(content: nil, dry_run: false) def extra_options(content: nil, dry_run: false)
{ content: content, dry_run: dry_run } { content: content, dry_run: dry_run }
end end
......
...@@ -2,10 +2,12 @@ ...@@ -2,10 +2,12 @@
class OnboardingProgressService class OnboardingProgressService
def initialize(namespace) def initialize(namespace)
@namespace = namespace.root_ancestor @namespace = namespace&.root_ancestor
end end
def execute(action:) def execute(action:)
return unless @namespace
NamespaceOnboardingAction.create_action(@namespace, action) NamespaceOnboardingAction.create_action(@namespace, action)
end end
end end
...@@ -1795,6 +1795,14 @@ ...@@ -1795,6 +1795,14 @@
:weight: 1 :weight: 1
:idempotent: :idempotent:
:tags: [] :tags: []
- :name: namespaces_onboarding_pipeline_created
:feature_category: :subgroups
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent: true
:tags: []
- :name: namespaces_onboarding_user_added - :name: namespaces_onboarding_user_added
:feature_category: :users :feature_category: :users
:has_external_dependencies: :has_external_dependencies:
......
# frozen_string_literal: true
module Namespaces
class OnboardingPipelineCreatedWorker
include ApplicationWorker
feature_category :subgroups
urgency :low
deduplicate :until_executing
idempotent!
def perform(namespace_id)
namespace = Namespace.find_by_id(namespace_id)
return unless namespace
OnboardingProgressService.new(namespace).execute(action: :pipeline_created)
end
end
end
...@@ -200,6 +200,8 @@ ...@@ -200,6 +200,8 @@
- 1 - 1
- - namespaceless_project_destroy - - namespaceless_project_destroy
- 1 - 1
- - namespaces_onboarding_pipeline_created
- 1
- - namespaces_onboarding_user_added - - namespaces_onboarding_user_added
- 1 - 1
- - new_epic - - new_epic
......
...@@ -245,10 +245,6 @@ module EE ...@@ -245,10 +245,6 @@ module EE
@ci_minutes_quota ||= ::Ci::Minutes::Quota.new(self) @ci_minutes_quota ||= ::Ci::Minutes::Quota.new(self)
end end
def root?
!has_parent?
end
# The same method name is used also at project and job level # The same method name is used also at project and job level
def shared_runners_minutes_limit_enabled? def shared_runners_minutes_limit_enabled?
ci_minutes_quota.enabled? ci_minutes_quota.enabled?
......
...@@ -646,26 +646,6 @@ RSpec.describe Namespace do ...@@ -646,26 +646,6 @@ RSpec.describe Namespace do
end end
end end
describe '#root?' do
subject { namespace.root? }
context 'when is subgroup' do
before do
namespace.parent = build(:group)
end
it 'returns false' do
is_expected.to eq(false)
end
end
context 'when is root' do
it 'returns true' do
is_expected.to eq(true)
end
end
end
describe '#shared_runners_minutes_limit_enabled?' do describe '#shared_runners_minutes_limit_enabled?' do
subject { namespace.shared_runners_minutes_limit_enabled? } subject { namespace.shared_runners_minutes_limit_enabled? }
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe NamespaceOnboardingAction do RSpec.describe NamespaceOnboardingAction do
let(:namespace) { build(:namespace) } let(:namespace) { create(:namespace) }
describe 'associations' do describe 'associations' do
it { is_expected.to belong_to(:namespace).required } it { is_expected.to belong_to(:namespace).required }
...@@ -46,6 +46,32 @@ RSpec.describe NamespaceOnboardingAction do ...@@ -46,6 +46,32 @@ RSpec.describe NamespaceOnboardingAction do
expect { create_action }.to change { count_namespace_actions }.by(0) expect { create_action }.to change { count_namespace_actions }.by(0)
end end
context 'when the namespace is created outside the monitoring window' do
let(:namespace) { create(:namespace, created_at: (NamespaceOnboardingAction::MONITORING_WINDOW + 1.day).ago) }
it 'does not create an action for the namespace' do
expect { create_action }.not_to change { count_namespace_actions }
end
context 'when an action has already been recorded for the namespace' do
before do
described_class.create!(namespace: namespace, action: :git_write)
end
it 'creates an action for the namespace' do
expect { create_action }.to change { count_namespace_actions }.by(1)
end
end
end
context 'when the namespace is not a root' do
let(:namespace) { create(:namespace, parent: build(:namespace)) }
it 'does not create an action for the namespace' do
expect { create_action }.not_to change { count_namespace_actions }
end
end
def count_namespace_actions def count_namespace_actions
described_class.where(namespace: namespace, action: action).count described_class.where(namespace: namespace, action: action).count
end end
......
...@@ -1500,4 +1500,24 @@ RSpec.describe Namespace do ...@@ -1500,4 +1500,24 @@ RSpec.describe Namespace do
end end
end end
end end
describe '#root?' do
subject { namespace.root? }
context 'when is subgroup' do
before do
namespace.parent = build(:group)
end
it 'returns false' do
is_expected.to eq(false)
end
end
context 'when is root' do
it 'returns true' do
is_expected.to eq(true)
end
end
end
end end
...@@ -489,6 +489,7 @@ RSpec.describe Ci::CreatePipelineService do ...@@ -489,6 +489,7 @@ RSpec.describe Ci::CreatePipelineService do
expect(execute_service).not_to be_persisted expect(execute_service).not_to be_persisted
expect(Ci::Pipeline.count).to eq(0) expect(Ci::Pipeline.count).to eq(0)
expect(Namespaces::OnboardingPipelineCreatedWorker).not_to receive(:perform_async)
end end
shared_examples 'a failed pipeline' do shared_examples 'a failed pipeline' do
...@@ -1426,6 +1427,13 @@ RSpec.describe Ci::CreatePipelineService do ...@@ -1426,6 +1427,13 @@ RSpec.describe Ci::CreatePipelineService do
pipeline pipeline
end end
it 'schedules a namespace onboarding create action worker' do
expect(Namespaces::OnboardingPipelineCreatedWorker)
.to receive(:perform_async).with(project.namespace_id)
pipeline
end
context 'when target sha is specified' do context 'when target sha is specified' do
let(:target_sha) { merge_request.target_branch_sha } let(:target_sha) { merge_request.target_branch_sha }
......
...@@ -5,29 +5,39 @@ require 'spec_helper' ...@@ -5,29 +5,39 @@ require 'spec_helper'
RSpec.describe OnboardingProgressService do RSpec.describe OnboardingProgressService do
describe '#execute' do describe '#execute' do
let(:namespace) { create(:namespace, parent: root_namespace) } let(:namespace) { create(:namespace, parent: root_namespace) }
let(:root_namespace) { nil }
let(:action) { :namespace_action }
subject(:execute_service) { described_class.new(namespace).execute(action: :subscription_created) } subject(:execute_service) { described_class.new(namespace).execute(action: :subscription_created) }
context 'when the namespace is a root' do context 'when the namespace is a root' do
let(:root_namespace) { nil } it 'records a namespace onboarding progress action fot the given namespace' do
it 'records a namespace onboarding progress action for the given namespace' do
expect(NamespaceOnboardingAction).to receive(:create_action) expect(NamespaceOnboardingAction).to receive(:create_action)
.with(namespace, :subscription_created).and_call_original .with(namespace, :subscription_created).and_call_original
expect { execute_service }.to change(NamespaceOnboardingAction, :count).by(1) expect { execute_service }.to change(NamespaceOnboardingAction, :count).by(1)
end end
end end
context 'when the namespace is not the root' do context 'when the namespace is not the root' do
let_it_be(:root_namespace) { build(:namespace) } let(:root_namespace) { build(:namespace) }
it 'records a namespace onboarding progress action for the root namespace' do it 'records a namespace onboarding progress action for the root namespace' do
expect(NamespaceOnboardingAction).to receive(:create_action) expect(NamespaceOnboardingAction).to receive(:create_action)
.with(root_namespace, :subscription_created).and_call_original .with(root_namespace, :subscription_created).and_call_original
expect { execute_service }.to change(NamespaceOnboardingAction, :count).by(1) expect { execute_service }.to change(NamespaceOnboardingAction, :count).by(1)
end end
end end
context 'when no namespace is passed' do
let(:namespace) { nil }
it 'does not record a namespace onboarding progress action' do
expect(NamespaceOnboardingAction).not_to receive(:create_action)
execute_service
end
end
end end
end end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Namespaces::OnboardingPipelineCreatedWorker, '#perform' do
include AfterNextHelpers
let_it_be(:ci_pipeline) { create(:ci_pipeline) }
it 'records the event' do
expect_next(OnboardingProgressService, ci_pipeline.project.namespace)
.to receive(:execute).with(action: :pipeline_created).and_call_original
expect do
subject.perform(ci_pipeline.project.namespace_id)
end.to change(NamespaceOnboardingAction, :count).by(1)
end
context "when a namespace doesn't exist" do
it "does nothing" do
expect_next(OnboardingProgressService, ci_pipeline.project.namespace).not_to receive(:execute)
expect { subject.perform(nil) }.not_to change(NamespaceOnboardingAction, :count)
end
end
end
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