From 22bdf63ba6195bddac0c6ec75f0abba248542988 Mon Sep 17 00:00:00 2001 From: Nasser Zahrani Date: Sat, 27 Sep 2025 13:38:14 -0400 Subject: [PATCH 1/7] Basic work_item_create instrumentation --- app/services/issues/create_service.rb | 14 ++++++++++++++ config/events/create_work_item.yml | 21 +++++++++++++++++++++ 2 files changed, 35 insertions(+) create mode 100644 config/events/create_work_item.yml diff --git a/app/services/issues/create_service.rb b/app/services/issues/create_service.rb index 2b8f0bb6304a99..81a35d11e36173 100644 --- a/app/services/issues/create_service.rb +++ b/app/services/issues/create_service.rb @@ -62,11 +62,17 @@ def after_create(issue) create_timeline_event(issue) try_to_associate_contacts(issue) publish_event(issue) + publish_internal_event(issue) super end def publish_event(issue) + # TODO: derive the namespace from the container passed in + # TODO: if there is a project, provide this as well + + # TODO: add label, the work item type + # TODO: add property, the current user role event = ::WorkItems::WorkItemCreatedEvent.new(data: { id: issue.id, namespace_id: issue.namespace_id @@ -77,6 +83,14 @@ def publish_event(issue) end end + def publish_internal_event(_issue) + track_internal_event( + 'work_item_create', + user: current_user + # namespace: + ) + end + def handle_changes(issue, options) super old_associations = options.fetch(:old_associations, {}) diff --git a/config/events/create_work_item.yml b/config/events/create_work_item.yml new file mode 100644 index 00000000000000..950732826e47da --- /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: TODO +tiers: +- free +- premium +- ultimate -- GitLab From c3e188d8062bfe1533d2223e6e61cdfb6cd3d917 Mon Sep 17 00:00:00 2001 From: Nasser Zahrani Date: Mon, 29 Sep 2025 22:09:07 -0400 Subject: [PATCH 2/7] Move event emission into work item create service --- app/services/issues/create_service.rb | 14 ------------ app/services/work_items/create_service.rb | 27 +++++++++++++++++++++++ 2 files changed, 27 insertions(+), 14 deletions(-) diff --git a/app/services/issues/create_service.rb b/app/services/issues/create_service.rb index 81a35d11e36173..2b8f0bb6304a99 100644 --- a/app/services/issues/create_service.rb +++ b/app/services/issues/create_service.rb @@ -62,17 +62,11 @@ def after_create(issue) create_timeline_event(issue) try_to_associate_contacts(issue) publish_event(issue) - publish_internal_event(issue) super end def publish_event(issue) - # TODO: derive the namespace from the container passed in - # TODO: if there is a project, provide this as well - - # TODO: add label, the work item type - # TODO: add property, the current user role event = ::WorkItems::WorkItemCreatedEvent.new(data: { id: issue.id, namespace_id: issue.namespace_id @@ -83,14 +77,6 @@ def publish_event(issue) end end - def publish_internal_event(_issue) - track_internal_event( - 'work_item_create', - user: current_user - # namespace: - ) - end - def handle_changes(issue, options) super old_associations = options.fetch(:old_associations, {}) diff --git a/app/services/work_items/create_service.rb b/app/services/work_items/create_service.rb index a6a6772ba30543..3d157bf43b64f4 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,27 @@ def payload(work_item) { work_item: work_item } end + def publish_internal_event(work_item) + user_access = if work_item.project + work_item.project.team.max_member_access(current_user.id) + else + work_item.namespace.max_member_access_for_user(current_user) + end + + user_role_name = Gitlab::Access.human_access(user_access)&.downcase + + 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: user_role_name + } + ) + end + def skip_system_notes? false end -- GitLab From 963672ae213ec34993da76ca373fade387748a52 Mon Sep 17 00:00:00 2001 From: Nasser Zahrani Date: Mon, 29 Sep 2025 22:21:53 -0400 Subject: [PATCH 3/7] Add metrics for work item creation event --- ...nct_namespace_id_from_create_work_item.yml | 23 +++++++++++++++++++ ...distinct_user_id_from_create_work_item.yml | 23 +++++++++++++++++++ .../count_total_create_work_item.yml | 23 +++++++++++++++++++ 3 files changed, 69 insertions(+) create mode 100644 config/metrics/counts_all/count_distinct_namespace_id_from_create_work_item.yml create mode 100644 config/metrics/counts_all/count_distinct_user_id_from_create_work_item.yml create mode 100644 config/metrics/counts_all/count_total_create_work_item.yml 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 00000000000000..e7d82c06923000 --- /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: TODO +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 00000000000000..0cecfa9e258753 --- /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: TODO +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 00000000000000..eddffa0187ef73 --- /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: TODO +time_frame: +- 28d +- 7d +- all +data_source: internal_events +data_category: optional +tiers: +- free +- premium +- ultimate +events: +- name: create_work_item -- GitLab From cd7d3ac0cc19b3ee7051a224b86953aa874d8522 Mon Sep 17 00:00:00 2001 From: Nasser Zahrani Date: Mon, 29 Sep 2025 22:24:17 -0400 Subject: [PATCH 4/7] Populate todo for new events/metrics --- config/events/create_work_item.yml | 2 +- .../count_distinct_namespace_id_from_create_work_item.yml | 2 +- .../counts_all/count_distinct_user_id_from_create_work_item.yml | 2 +- config/metrics/counts_all/count_total_create_work_item.yml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/config/events/create_work_item.yml b/config/events/create_work_item.yml index 950732826e47da..ebddd45411716b 100644 --- a/config/events/create_work_item.yml +++ b/config/events/create_work_item.yml @@ -14,7 +14,7 @@ additional_properties: description: user role product_group: product_planning milestone: '18.5' -introduced_by_url: TODO +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/206613 tiers: - free - premium 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 index e7d82c06923000..9b38b5c86eea46 100644 --- 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 @@ -8,7 +8,7 @@ performance_indicator_type: [] value_type: number status: active milestone: '18.5' -introduced_by_url: TODO +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/206613 time_frame: - 28d - 7d 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 index 0cecfa9e258753..785040750dc82c 100644 --- 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 @@ -8,7 +8,7 @@ performance_indicator_type: [] value_type: number status: active milestone: '18.5' -introduced_by_url: TODO +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/206613 time_frame: - 28d - 7d diff --git a/config/metrics/counts_all/count_total_create_work_item.yml b/config/metrics/counts_all/count_total_create_work_item.yml index eddffa0187ef73..26d07cede571c8 100644 --- a/config/metrics/counts_all/count_total_create_work_item.yml +++ b/config/metrics/counts_all/count_total_create_work_item.yml @@ -8,7 +8,7 @@ performance_indicator_type: [] value_type: number status: active milestone: '18.5' -introduced_by_url: TODO +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/206613 time_frame: - 28d - 7d -- GitLab From 20975d0f389e7afca430fcd324f9b19ab5c1f1d1 Mon Sep 17 00:00:00 2001 From: Nasser Zahrani Date: Wed, 1 Oct 2025 15:51:24 -0400 Subject: [PATCH 5/7] Move group level spec to ee --- .../work_items/create_service_spec.rb | 42 +++++++++++ .../work_items/create_service_spec.rb | 72 +++++++++++++++++++ 2 files changed, 114 insertions(+) diff --git a/ee/spec/services/work_items/create_service_spec.rb b/ee/spec/services/work_items/create_service_spec.rb index 41fc31a0f3806e..35eca8e38426e7 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/services/work_items/create_service_spec.rb b/spec/services/work_items/create_service_spec.rb index 82cb5af2359733..e6a5e81c64e4b6 100644 --- a/spec/services/work_items/create_service_spec.rb +++ b/spec/services/work_items/create_service_spec.rb @@ -225,4 +225,76 @@ 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 + + context 'with different user roles' do + using RSpec::Parameterized::TableSyntax + + where(:role, :expected_property) do + :guest | 'guest' + :reporter | 'reporter' + :maintainer | 'maintainer' + :owner | 'owner' + end + + with_them do + before do + project.add_member(user, role) + end + + it 'tracks correct user role' do + expect { service_result } + .to trigger_internal_events('create_work_item') + .with( + user: user, + namespace: project.namespace, + project: project, + additional_properties: { + label: 'Issue', + property: expected_property + } + ) + end + end + end + end end -- GitLab From c3c1c39bd6b734c0345cdcd5af51853901decc03 Mon Sep 17 00:00:00 2001 From: Nasser Zahrani Date: Wed, 1 Oct 2025 16:31:37 -0400 Subject: [PATCH 6/7] Move user role name to work item model --- app/models/work_item.rb | 12 ++++++ app/services/work_items/create_service.rb | 10 +---- ee/spec/models/work_item_spec.rb | 38 ++++++++++++++++ spec/models/work_item_spec.rb | 43 +++++++++++++++++++ .../work_items/create_service_spec.rb | 31 ------------- 5 files changed, 94 insertions(+), 40 deletions(-) diff --git a/app/models/work_item.rb b/app/models/work_item.rb index 285c0a378d8139..a4eb3df4753af2 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 3d157bf43b64f4..2be464c0104fe6 100644 --- a/app/services/work_items/create_service.rb +++ b/app/services/work_items/create_service.rb @@ -53,14 +53,6 @@ def payload(work_item) end def publish_internal_event(work_item) - user_access = if work_item.project - work_item.project.team.max_member_access(current_user.id) - else - work_item.namespace.max_member_access_for_user(current_user) - end - - user_role_name = Gitlab::Access.human_access(user_access)&.downcase - track_internal_event( 'create_work_item', user: current_user, @@ -68,7 +60,7 @@ def publish_internal_event(work_item) project: work_item.project, additional_properties: { label: work_item.work_item_type.name, - property: user_role_name + property: work_item.user_role_name(current_user) } ) end diff --git a/ee/spec/models/work_item_spec.rb b/ee/spec/models/work_item_spec.rb index 58495e67503a25..0196ecf82a89c4 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/spec/models/work_item_spec.rb b/spec/models/work_item_spec.rb index a597e827f9767f..252ae17f681bf5 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 e6a5e81c64e4b6..64b0e675756385 100644 --- a/spec/services/work_items/create_service_spec.rb +++ b/spec/services/work_items/create_service_spec.rb @@ -265,36 +265,5 @@ } ) end - - context 'with different user roles' do - using RSpec::Parameterized::TableSyntax - - where(:role, :expected_property) do - :guest | 'guest' - :reporter | 'reporter' - :maintainer | 'maintainer' - :owner | 'owner' - end - - with_them do - before do - project.add_member(user, role) - end - - it 'tracks correct user role' do - expect { service_result } - .to trigger_internal_events('create_work_item') - .with( - user: user, - namespace: project.namespace, - project: project, - additional_properties: { - label: 'Issue', - property: expected_property - } - ) - end - end - end end end -- GitLab From 629c1e894e2eef913286f51297db3daadeb4c7fb Mon Sep 17 00:00:00 2001 From: Nasser Zahrani Date: Thu, 2 Oct 2025 14:15:17 -0400 Subject: [PATCH 7/7] Move user role from work item to namespace --- app/models/namespace.rb | 7 +++++++ app/models/namespaces/project_namespace.rb | 4 ++++ app/services/work_items/create_service.rb | 2 +- 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/app/models/namespace.rb b/app/models/namespace.rb index 80dd9b1f0b7f90..f39c33655704ec 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 d837647907e15a..69b1cbe7d32220 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/services/work_items/create_service.rb b/app/services/work_items/create_service.rb index 2be464c0104fe6..68409b99ade24c 100644 --- a/app/services/work_items/create_service.rb +++ b/app/services/work_items/create_service.rb @@ -60,7 +60,7 @@ def publish_internal_event(work_item) project: work_item.project, additional_properties: { label: work_item.work_item_type.name, - property: work_item.user_role_name(current_user) + property: work_item.namespace.user_role(current_user) } ) end -- GitLab