diff --git a/app/models/namespace.rb b/app/models/namespace.rb index 80dd9b1f0b7f90066c9fbd3fa63e6c80527c22b2..f39c33655704ec2bff42d8ab1578364aa112e793 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -852,6 +852,13 @@ def namespace_details super.presence || build_namespace_details end + def user_role(user) + return unless user && !user_namespace? + + user_access = max_member_access_for_user(user) + Gitlab::Access.human_access(user_access)&.downcase + end + private def parent_organization_match diff --git a/app/models/namespaces/project_namespace.rb b/app/models/namespaces/project_namespace.rb index d837647907e15af3b53fb5bdb4408dc27c2b9cf9..69b1cbe7d32220b5f94072626fd002ebe9a7e648 100644 --- a/app/models/namespaces/project_namespace.rb +++ b/app/models/namespaces/project_namespace.rb @@ -78,6 +78,10 @@ def sync_attributes_from_project(project) def all_projects Project.where(id: project.id) end + + def max_member_access_for_user(user) + project.max_member_access_for_user(user) + end end end diff --git a/app/models/work_item.rb b/app/models/work_item.rb index 285c0a378d813918b5d50673d5cc1ced894473e5..a4eb3df4753af202b9dd5c3658851bda1e751f53 100644 --- a/app/models/work_item.rb +++ b/app/models/work_item.rb @@ -410,6 +410,18 @@ def max_depth_reached?(child_type) end end + def user_role_name(user) + return unless user + + user_access = if project + project.team.max_member_access(user.id) + else + namespace.max_member_access_for_user(user) + end + + Gitlab::Access.human_access(user_access)&.downcase + end + private override :parent_link_confidentiality diff --git a/app/services/work_items/create_service.rb b/app/services/work_items/create_service.rb index a6a6772ba305430ae414801964b433a2bb299469..68409b99ade24c8a9be964562ae93d00947c372e 100644 --- a/app/services/work_items/create_service.rb +++ b/app/services/work_items/create_service.rb @@ -3,6 +3,7 @@ module WorkItems class CreateService < Issues::CreateService include WidgetableService + include Gitlab::InternalEventsTracking def initialize(container:, perform_spam_check: true, current_user: nil, params: {}, widget_params: {}) super( @@ -36,6 +37,11 @@ def parent container end + def after_create(work_item) + publish_internal_event(work_item) + super + end + private def authorization_action @@ -46,6 +52,19 @@ def payload(work_item) { work_item: work_item } end + def publish_internal_event(work_item) + track_internal_event( + 'create_work_item', + user: current_user, + namespace: work_item.project&.namespace || work_item.namespace, + project: work_item.project, + additional_properties: { + label: work_item.work_item_type.name, + property: work_item.namespace.user_role(current_user) + } + ) + end + def skip_system_notes? false end diff --git a/config/events/create_work_item.yml b/config/events/create_work_item.yml new file mode 100644 index 0000000000000000000000000000000000000000..ebddd45411716b509fb434374d13ad4fe81309db --- /dev/null +++ b/config/events/create_work_item.yml @@ -0,0 +1,21 @@ +--- +description: work item created +internal_events: true +status: active +action: create_work_item +identifiers: +- project +- namespace +- user +additional_properties: + label: + description: work item type + property: + description: user role +product_group: product_planning +milestone: '18.5' +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/206613 +tiers: +- free +- premium +- ultimate diff --git a/config/metrics/counts_all/count_distinct_namespace_id_from_create_work_item.yml b/config/metrics/counts_all/count_distinct_namespace_id_from_create_work_item.yml new file mode 100644 index 0000000000000000000000000000000000000000..9b38b5c86eea4602cf0152ba57d5286df7f71a9a --- /dev/null +++ b/config/metrics/counts_all/count_distinct_namespace_id_from_create_work_item.yml @@ -0,0 +1,23 @@ +--- +key_path: redis_hll_counters.count_distinct_namespace_id_from_create_work_item +description: Count of unique namespaces in which a work item was created +product_group: product_planning +product_categories: +- team_planning +performance_indicator_type: [] +value_type: number +status: active +milestone: '18.5' +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/206613 +time_frame: +- 28d +- 7d +data_source: internal_events +data_category: optional +tiers: +- free +- premium +- ultimate +events: +- name: create_work_item + unique: namespace.id diff --git a/config/metrics/counts_all/count_distinct_user_id_from_create_work_item.yml b/config/metrics/counts_all/count_distinct_user_id_from_create_work_item.yml new file mode 100644 index 0000000000000000000000000000000000000000..785040750dc82c9a5c3f2733ea2b83ffe8f561d5 --- /dev/null +++ b/config/metrics/counts_all/count_distinct_user_id_from_create_work_item.yml @@ -0,0 +1,23 @@ +--- +key_path: redis_hll_counters.count_distinct_user_id_from_create_work_item +description: Count of unique users who have created a work item +product_group: product_planning +product_categories: +- team_planning +performance_indicator_type: [] +value_type: number +status: active +milestone: '18.5' +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/206613 +time_frame: +- 28d +- 7d +data_source: internal_events +data_category: optional +tiers: +- free +- premium +- ultimate +events: +- name: create_work_item + unique: user.id diff --git a/config/metrics/counts_all/count_total_create_work_item.yml b/config/metrics/counts_all/count_total_create_work_item.yml new file mode 100644 index 0000000000000000000000000000000000000000..26d07cede571c8ae74e81eebf43d1a45d3bcf6f9 --- /dev/null +++ b/config/metrics/counts_all/count_total_create_work_item.yml @@ -0,0 +1,23 @@ +--- +key_path: counts.count_total_create_work_item +description: Count of work items created +product_group: product_planning +product_categories: +- team_planning +performance_indicator_type: [] +value_type: number +status: active +milestone: '18.5' +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/206613 +time_frame: +- 28d +- 7d +- all +data_source: internal_events +data_category: optional +tiers: +- free +- premium +- ultimate +events: +- name: create_work_item diff --git a/ee/spec/models/work_item_spec.rb b/ee/spec/models/work_item_spec.rb index 58495e67503a25a675a35a507632f68b2e383e9c..0196ecf82a89c415ae45c603a7f8e7936db106ae 100644 --- a/ee/spec/models/work_item_spec.rb +++ b/ee/spec/models/work_item_spec.rb @@ -1791,4 +1791,42 @@ end end end + + describe '#user_role_name' do + let_it_be(:user) { create(:user) } + + context 'for namespace-level work item' do + let(:group) { create(:group) } + let(:work_item) { create(:work_item, :epic, namespace: group, project: nil, author: user) } + + using RSpec::Parameterized::TableSyntax + + where(:role, :expected_name) do + :guest | 'guest' + :reporter | 'reporter' + :developer | 'developer' + :maintainer | 'maintainer' + :owner | 'owner' + end + + with_them do + before do + stub_licensed_features(epics: true) + group.add_member(user, role) + end + + it 'returns the correct role name for group member' do + expect(work_item.user_role_name(user)).to eq(expected_name) + end + end + + context 'when user has no access to the group' do + let(:user_without_access) { create(:user) } + + it 'returns nil' do + expect(work_item.user_role_name(user_without_access)).to be_nil + end + end + end + end end diff --git a/ee/spec/services/work_items/create_service_spec.rb b/ee/spec/services/work_items/create_service_spec.rb index 41fc31a0f3806e50f5e8bfcf365d7510184dce2f..35eca8e38426e759f95f83f8f40acc9ba03d821e 100644 --- a/ee/spec/services/work_items/create_service_spec.rb +++ b/ee/spec/services/work_items/create_service_spec.rb @@ -388,4 +388,46 @@ end end end + + context 'when group level work item is created successfully' do + let(:group) { create(:group) } + let(:user) { create(:user) } + + let(:service) do + described_class.new( + container: group, + current_user: user, + params: params, + widget_params: {} + ) + end + + let(:params) do + { + title: 'Epic work item', + work_item_type: WorkItems::Type.default_by_type(:epic) + } + end + + subject(:service_result) { service.execute } + + before do + stub_licensed_features(epics: true) + group.add_developer(user) + end + + it 'triggers internal event with namespace only' do + expect { service_result } + .to trigger_internal_events('create_work_item') + .with( + user: user, + namespace: group, + project: nil, + additional_properties: { + label: 'Epic', + property: 'developer' + } + ) + end + end end diff --git a/spec/models/work_item_spec.rb b/spec/models/work_item_spec.rb index a597e827f9767f4c0f0b38c203d3656450e92abc..252ae17f681bf55e19d143b000c46d07b83b83eb 100644 --- a/spec/models/work_item_spec.rb +++ b/spec/models/work_item_spec.rb @@ -1044,4 +1044,47 @@ end end end + + describe '#user_role_name' do + let_it_be(:user) { create(:user) } + + context 'for project-level work item' do + let_it_be(:project) { create(:project) } + let_it_be(:work_item) { create(:work_item, project: project) } + + using RSpec::Parameterized::TableSyntax + + where(:role, :expected_name) do + :guest | 'guest' + :reporter | 'reporter' + :developer | 'developer' + :maintainer | 'maintainer' + :owner | 'owner' + end + + with_them do + before do + project.add_member(user, role) + end + + it 'returns the correct role name' do + expect(work_item.user_role_name(user)).to eq(expected_name) + end + end + + context 'when user has no access' do + let(:user_without_access) { create(:user) } + + it 'returns nil' do + expect(work_item.user_role_name(user_without_access)).to be_nil + end + end + + context 'when user is nil' do + it 'returns nil' do + expect(work_item.user_role_name(nil)).to be_nil + end + end + end + end end diff --git a/spec/services/work_items/create_service_spec.rb b/spec/services/work_items/create_service_spec.rb index 82cb5af2359733947d0bf4df9b26db00024567c1..64b0e67575638538ec9b5182f8cab0c344b40169 100644 --- a/spec/services/work_items/create_service_spec.rb +++ b/spec/services/work_items/create_service_spec.rb @@ -225,4 +225,45 @@ end end end + + context 'when work item is created successfully' do + let(:project) { create(:project) } + let(:user) { create(:user) } + + let(:service) do + described_class.new( + container: project, + current_user: user, + params: params, + widget_params: {} + ) + end + + let(:params) do + { + title: 'Awesome work_item', + description: 'please fix' + } + end + + subject(:service_result) { service.execute } + + before do + project.add_developer(user) + end + + it 'triggers create_work_item internal event' do + expect { service_result } + .to trigger_internal_events('create_work_item') + .with( + user: user, + namespace: project.namespace, + project: project, + additional_properties: { + label: 'Issue', + property: 'developer' + } + ) + end + end end