Commit db52afb7 authored by Mark Chao's avatar Mark Chao

Merge branch '297401-usage-tracking-for-jira-integration' into 'master'

Track usage of the Jira project service [RUN ALL RSPEC] [RUN AS-IF-FOSS]

See merge request gitlab-org/gitlab!52110
parents 8d2e3a28 854d3eea
# frozen_string_literal: true # frozen_string_literal: true
# Accessible as Project#external_issue_tracker
class JiraService < IssueTrackerService class JiraService < IssueTrackerService
extend ::Gitlab::Utils::Override extend ::Gitlab::Utils::Override
include Gitlab::Routing include Gitlab::Routing
...@@ -162,7 +163,7 @@ class JiraService < IssueTrackerService ...@@ -162,7 +163,7 @@ class JiraService < IssueTrackerService
jira_request { client.Issue.find(issue_key) } jira_request { client.Issue.find(issue_key) }
end end
def close_issue(entity, external_issue) def close_issue(entity, external_issue, current_user)
issue = find_issue(external_issue.iid) issue = find_issue(external_issue.iid)
return if issue.nil? || has_resolution?(issue) || !jira_issue_transition_id.present? return if issue.nil? || has_resolution?(issue) || !jira_issue_transition_id.present?
...@@ -179,6 +180,7 @@ class JiraService < IssueTrackerService ...@@ -179,6 +180,7 @@ class JiraService < IssueTrackerService
# if it is closed, so we don't have one comment for every commit. # if it is closed, so we don't have one comment for every commit.
issue = find_issue(issue.key) if transition_issue(issue) issue = find_issue(issue.key) if transition_issue(issue)
add_issue_solved_comment(issue, commit_id, commit_url) if has_resolution?(issue) add_issue_solved_comment(issue, commit_id, commit_url) if has_resolution?(issue)
log_usage(:close_issue, current_user)
end end
def create_cross_reference_note(mentioned, noteable, author) def create_cross_reference_note(mentioned, noteable, author)
...@@ -214,7 +216,7 @@ class JiraService < IssueTrackerService ...@@ -214,7 +216,7 @@ class JiraService < IssueTrackerService
} }
} }
add_comment(data, jira_issue) add_comment(data, jira_issue).tap { log_usage(:cross_reference, author) }
end end
def valid_connection? def valid_connection?
...@@ -275,6 +277,12 @@ class JiraService < IssueTrackerService ...@@ -275,6 +277,12 @@ class JiraService < IssueTrackerService
end end
end end
def log_usage(action, user)
key = "i_ecosystem_jira_service_#{action}"
Gitlab::UsageDataCounters::HLLRedisCounter.track_event(key, values: user.id)
end
def add_issue_solved_comment(issue, commit_id, commit_url) def add_issue_solved_comment(issue, commit_id, commit_url)
link_title = "Solved by commit #{commit_id}." link_title = "Solved by commit #{commit_id}."
comment = "Issue solved with [#{commit_id}|#{commit_url}]." comment = "Issue solved with [#{commit_id}|#{commit_url}]."
......
...@@ -55,7 +55,7 @@ module Issues ...@@ -55,7 +55,7 @@ module Issues
def close_external_issue(issue, closed_via) def close_external_issue(issue, closed_via)
return unless project.external_issue_tracker&.support_close_issue? return unless project.external_issue_tracker&.support_close_issue?
project.external_issue_tracker.close_issue(closed_via, issue) project.external_issue_tracker.close_issue(closed_via, issue, current_user)
todo_service.close_issue(issue, current_user) todo_service.close_issue(issue, current_user)
end end
......
---
name: usage_data_track_ecosystem_jira_service
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/52110
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/300447
milestone: '13.9'
type: development
group: group::ecosystem
default_enabled: false
...@@ -185,9 +185,8 @@ The Redis [`PFCOUNT`](https://redis.io/commands/pfcount), ...@@ -185,9 +185,8 @@ The Redis [`PFCOUNT`](https://redis.io/commands/pfcount),
[`PFMERGE`](https://redis.io/commands/pfmergge) commands operate on [`PFMERGE`](https://redis.io/commands/pfmergge) commands operate on
HyperLogLogs, a data structure that allows estimating the number of unique HyperLogLogs, a data structure that allows estimating the number of unique
elements with low memory usage. (In addition to the `PFCOUNT` documentation, elements with low memory usage. (In addition to the `PFCOUNT` documentation,
Thoughtbot's article on [HyperLogLogs in Thoughtbot's article on [HyperLogLogs in Redis](https://thoughtbot.com/blog/hyperloglogs-in-redis)
Redis](https://thoughtbot.com/blog/hyperloglogs-in-redis) provides a good provides a good background here.)
background here.)
[`Gitlab::Redis::HLL`](https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/redis/hll.rb) [`Gitlab::Redis::HLL`](https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/redis/hll.rb)
provides a convenient interface for adding and counting values in HyperLogLogs. provides a convenient interface for adding and counting values in HyperLogLogs.
......
...@@ -7,6 +7,11 @@ module Projects ...@@ -7,6 +7,11 @@ module Projects
include RecordUserLastActivity include RecordUserLastActivity
include SortingHelper include SortingHelper
include SortingPreference include SortingPreference
include RedisTracking
track_redis_hll_event :index,
name: 'i_ecosystem_jira_service_list_issues',
feature: :usage_data_track_ecosystem_jira_service
before_action :check_feature_enabled! before_action :check_feature_enabled!
before_action :check_issues_show_enabled!, only: :show before_action :check_issues_show_enabled!, only: :show
...@@ -43,6 +48,10 @@ module Projects ...@@ -43,6 +48,10 @@ module Projects
private private
def visitor_id
current_user&.id
end
def issues_json def issues_json
jira_issues = Kaminari.paginate_array( jira_issues = Kaminari.paginate_array(
finder.execute, finder.execute,
......
...@@ -53,7 +53,7 @@ module EE ...@@ -53,7 +53,7 @@ module EE
"#{url}/secure/CreateIssueDetails!init.jspa?pid=#{jira_project_id}&issuetype=#{vulnerabilities_issuetype}&summary=#{escaped_summary}&description=#{escaped_description}"[0..MAX_URL_LENGTH] "#{url}/secure/CreateIssueDetails!init.jspa?pid=#{jira_project_id}&issuetype=#{vulnerabilities_issuetype}&summary=#{escaped_summary}&description=#{escaped_description}"[0..MAX_URL_LENGTH]
end end
def create_issue(summary, description) def create_issue(summary, description, current_user)
return if client_url.blank? return if client_url.blank?
jira_request do jira_request do
...@@ -66,6 +66,7 @@ module EE ...@@ -66,6 +66,7 @@ module EE
description: description description: description
} }
) )
log_usage(:create_issue, current_user)
issue issue
end end
end end
......
...@@ -37,7 +37,7 @@ module VulnerabilityExternalIssueLinks ...@@ -37,7 +37,7 @@ module VulnerabilityExternalIssueLinks
summary = _('Investigate vulnerability: %{title}') % { title: vulnerability.title } summary = _('Investigate vulnerability: %{title}') % { title: vulnerability.title }
description = vulnerability.present.jira_issue_description description = vulnerability.present.jira_issue_description
external_provider_service.create_issue(summary, description) external_provider_service.create_issue(summary, description, user)
end end
def prepare_external_provider_service def prepare_external_provider_service
......
...@@ -38,6 +38,14 @@ RSpec.describe Projects::Integrations::Jira::IssuesController do ...@@ -38,6 +38,14 @@ RSpec.describe Projects::Integrations::Jira::IssuesController do
expect(response).to render_template(:index) expect(response).to render_template(:index)
end end
it 'tracks usage' do
expect(Gitlab::UsageDataCounters::HLLRedisCounter)
.to receive(:track_event)
.with('i_ecosystem_jira_service_list_issues', values: user.id)
get :index, params: { namespace_id: project.namespace, project_id: project }
end
context 'when project has moved' do context 'when project has moved' do
let(:new_project) { create(:project) } let(:new_project) { create(:project) }
......
...@@ -150,13 +150,23 @@ RSpec.describe JiraService do ...@@ -150,13 +150,23 @@ RSpec.describe JiraService do
end end
it 'creates issue in Jira API' do it 'creates issue in Jira API' do
issue = jira_service.create_issue("Special Summary!?", "*ID*: 2\n_Issue_: !") issue = jira_service.create_issue("Special Summary!?", "*ID*: 2\n_Issue_: !", build(:user))
expect(WebMock).to have_requested(:post, 'http://jira.example.com/rest/api/2/issue').with( expect(WebMock).to have_requested(:post, 'http://jira.example.com/rest/api/2/issue').with(
body: { fields: { project: { id: '11223' }, issuetype: { id: '10001' }, summary: 'Special Summary!?', description: "*ID*: 2\n_Issue_: !" } }.to_json body: { fields: { project: { id: '11223' }, issuetype: { id: '10001' }, summary: 'Special Summary!?', description: "*ID*: 2\n_Issue_: !" } }.to_json
).once ).once
expect(issue.id).to eq('10000') expect(issue.id).to eq('10000')
end end
it 'tracks usage' do
user = build_stubbed(:user)
expect(Gitlab::UsageDataCounters::HLLRedisCounter)
.to receive(:track_event)
.with('i_ecosystem_jira_service_create_issue', values: user.id)
jira_service.create_issue('x', 'y', user)
end
end end
context 'when there is an error in Jira' do context 'when there is an error in Jira' do
...@@ -167,7 +177,7 @@ RSpec.describe JiraService do ...@@ -167,7 +177,7 @@ RSpec.describe JiraService do
end end
it 'returns issue with errors' do it 'returns issue with errors' do
issue = jira_service.create_issue('', "*ID*: 2\n_Issue_: !") issue = jira_service.create_issue('', "*ID*: 2\n_Issue_: !", build(:user))
expect(WebMock).to have_requested(:post, 'http://jira.example.com/rest/api/2/issue').with( expect(WebMock).to have_requested(:post, 'http://jira.example.com/rest/api/2/issue').with(
body: { fields: { project: { id: '11223' }, issuetype: { id: '10001' }, summary: '', description: "*ID*: 2\n_Issue_: !" } }.to_json body: { fields: { project: { id: '11223' }, issuetype: { id: '10001' }, summary: '', description: "*ID*: 2\n_Issue_: !" } }.to_json
......
...@@ -55,7 +55,7 @@ RSpec.describe VulnerabilityExternalIssueLinks::CreateService do ...@@ -55,7 +55,7 @@ RSpec.describe VulnerabilityExternalIssueLinks::CreateService do
end end
it 'creates issue using jira service' do it 'creates issue using jira service' do
expect(jira_service).to receive(:create_issue).with("Investigate vulnerability: #{vulnerability.title}", kind_of(String)) expect(jira_service).to receive(:create_issue).with("Investigate vulnerability: #{vulnerability.title}", kind_of(String), user)
subject subject
end end
......
---
# Ecosystem category
- name: i_ecosystem_jira_service_close_issue
category: ecosystem
redis_slot: ecosystem
aggregation: weekly
feature_flag: usage_data_track_ecosystem_jira_service
- name: i_ecosystem_jira_service_cross_reference
category: ecosystem
redis_slot: ecosystem
aggregation: weekly
feature_flag: usage_data_track_ecosystem_jira_service
- name: i_ecosystem_jira_service_list_issues
category: ecosystem
redis_slot: ecosystem
aggregation: weekly
feature_flag: usage_data_track_ecosystem_jira_service
- name: i_ecosystem_jira_service_create_issue
category: ecosystem
redis_slot: ecosystem
aggregation: weekly
feature_flag: usage_data_track_ecosystem_jira_service
...@@ -27,6 +27,7 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s ...@@ -27,6 +27,7 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s
'deploy_token_packages', 'deploy_token_packages',
'user_packages', 'user_packages',
'compliance', 'compliance',
'ecosystem',
'analytics', 'analytics',
'ide_edit', 'ide_edit',
'search', 'search',
......
...@@ -6,6 +6,8 @@ RSpec.describe JiraService do ...@@ -6,6 +6,8 @@ RSpec.describe JiraService do
include AssetsHelpers include AssetsHelpers
let_it_be(:project) { create(:project, :repository) } let_it_be(:project) { create(:project, :repository) }
let(:current_user) { build_stubbed(:user) }
let(:url) { 'http://jira.example.com' } let(:url) { 'http://jira.example.com' }
let(:api_url) { 'http://api-jira.example.com' } let(:api_url) { 'http://api-jira.example.com' }
let(:username) { 'jira-username' } let(:username) { 'jira-username' }
...@@ -498,25 +500,38 @@ RSpec.describe JiraService do ...@@ -498,25 +500,38 @@ RSpec.describe JiraService do
WebMock.stub_request(:post, @remote_link_url).with(basic_auth: %w(gitlab_jira_username gitlab_jira_password)) WebMock.stub_request(:post, @remote_link_url).with(basic_auth: %w(gitlab_jira_username gitlab_jira_password))
end end
let(:external_issue) { ExternalIssue.new('JIRA-123', project) }
def close_issue
@jira_service.close_issue(resource, external_issue, current_user)
end
it 'calls Jira API' do it 'calls Jira API' do
@jira_service.close_issue(resource, ExternalIssue.new('JIRA-123', project)) close_issue
expect(WebMock).to have_requested(:post, @comment_url).with( expect(WebMock).to have_requested(:post, @comment_url).with(
body: /Issue solved with/ body: /Issue solved with/
).once ).once
end end
it 'tracks usage' do
expect(Gitlab::UsageDataCounters::HLLRedisCounter)
.to receive(:track_event)
.with('i_ecosystem_jira_service_close_issue', values: current_user.id)
close_issue
end
it 'does not fail if remote_link.all on issue returns nil' do it 'does not fail if remote_link.all on issue returns nil' do
allow(JIRA::Resource::Remotelink).to receive(:all).and_return(nil) allow(JIRA::Resource::Remotelink).to receive(:all).and_return(nil)
expect { @jira_service.close_issue(resource, ExternalIssue.new('JIRA-123', project)) } expect { close_issue }.not_to raise_error
.not_to raise_error
end end
# Check https://developer.atlassian.com/jiradev/jira-platform/guides/other/guide-jira-remote-issue-links/fields-in-remote-issue-links # Check https://developer.atlassian.com/jiradev/jira-platform/guides/other/guide-jira-remote-issue-links/fields-in-remote-issue-links
# for more information # for more information
it 'creates Remote Link reference in Jira for comment' do it 'creates Remote Link reference in Jira for comment' do
@jira_service.close_issue(resource, ExternalIssue.new('JIRA-123', project)) close_issue
favicon_path = "http://localhost/assets/#{find_asset('favicon.png').digest_path}" favicon_path = "http://localhost/assets/#{find_asset('favicon.png').digest_path}"
...@@ -540,7 +555,7 @@ RSpec.describe JiraService do ...@@ -540,7 +555,7 @@ RSpec.describe JiraService do
context 'when "comment_on_event_enabled" is set to false' do context 'when "comment_on_event_enabled" is set to false' do
it 'creates Remote Link reference but does not create comment' do it 'creates Remote Link reference but does not create comment' do
allow(@jira_service).to receive_messages(comment_on_event_enabled: false) allow(@jira_service).to receive_messages(comment_on_event_enabled: false)
@jira_service.close_issue(resource, ExternalIssue.new('JIRA-123', project)) close_issue
expect(WebMock).not_to have_requested(:post, @comment_url) expect(WebMock).not_to have_requested(:post, @comment_url)
expect(WebMock).to have_requested(:post, @remote_link_url) expect(WebMock).to have_requested(:post, @remote_link_url)
...@@ -562,7 +577,7 @@ RSpec.describe JiraService do ...@@ -562,7 +577,7 @@ RSpec.describe JiraService do
expect(remote_link).to receive(:save!) expect(remote_link).to receive(:save!)
@jira_service.close_issue(resource, ExternalIssue.new('JIRA-123', project)) close_issue
expect(WebMock).not_to have_requested(:post, @comment_url) expect(WebMock).not_to have_requested(:post, @comment_url)
end end
...@@ -571,7 +586,7 @@ RSpec.describe JiraService do ...@@ -571,7 +586,7 @@ RSpec.describe JiraService do
it 'does not send comment or remote links to issues already closed' do it 'does not send comment or remote links to issues already closed' do
allow_any_instance_of(JIRA::Resource::Issue).to receive(:resolution).and_return(true) allow_any_instance_of(JIRA::Resource::Issue).to receive(:resolution).and_return(true)
@jira_service.close_issue(resource, ExternalIssue.new('JIRA-123', project)) close_issue
expect(WebMock).not_to have_requested(:post, @comment_url) expect(WebMock).not_to have_requested(:post, @comment_url)
expect(WebMock).not_to have_requested(:post, @remote_link_url) expect(WebMock).not_to have_requested(:post, @remote_link_url)
...@@ -580,7 +595,7 @@ RSpec.describe JiraService do ...@@ -580,7 +595,7 @@ RSpec.describe JiraService do
it 'does not send comment or remote links to issues with unknown resolution' do it 'does not send comment or remote links to issues with unknown resolution' do
allow_any_instance_of(JIRA::Resource::Issue).to receive(:respond_to?).with(:resolution).and_return(false) allow_any_instance_of(JIRA::Resource::Issue).to receive(:respond_to?).with(:resolution).and_return(false)
@jira_service.close_issue(resource, ExternalIssue.new('JIRA-123', project)) close_issue
expect(WebMock).not_to have_requested(:post, @comment_url) expect(WebMock).not_to have_requested(:post, @comment_url)
expect(WebMock).not_to have_requested(:post, @remote_link_url) expect(WebMock).not_to have_requested(:post, @remote_link_url)
...@@ -589,7 +604,7 @@ RSpec.describe JiraService do ...@@ -589,7 +604,7 @@ RSpec.describe JiraService do
it 'references the GitLab commit' do it 'references the GitLab commit' do
stub_config_setting(base_url: custom_base_url) stub_config_setting(base_url: custom_base_url)
@jira_service.close_issue(resource, ExternalIssue.new('JIRA-123', project)) close_issue
expect(WebMock).to have_requested(:post, @comment_url).with( expect(WebMock).to have_requested(:post, @comment_url).with(
body: %r{#{custom_base_url}/#{project.full_path}/-/commit/#{commit_id}} body: %r{#{custom_base_url}/#{project.full_path}/-/commit/#{commit_id}}
...@@ -604,7 +619,7 @@ RSpec.describe JiraService do ...@@ -604,7 +619,7 @@ RSpec.describe JiraService do
{ script_name: '/gitlab' } { script_name: '/gitlab' }
end end
@jira_service.close_issue(resource, ExternalIssue.new('JIRA-123', project)) close_issue
expect(WebMock).to have_requested(:post, @comment_url).with( expect(WebMock).to have_requested(:post, @comment_url).with(
body: %r{#{Gitlab.config.gitlab.url}/#{project.full_path}/-/commit/#{commit_id}} body: %r{#{Gitlab.config.gitlab.url}/#{project.full_path}/-/commit/#{commit_id}}
...@@ -615,7 +630,7 @@ RSpec.describe JiraService do ...@@ -615,7 +630,7 @@ RSpec.describe JiraService do
allow(@jira_service).to receive(:log_error) allow(@jira_service).to receive(:log_error)
WebMock.stub_request(:post, @transitions_url).with(basic_auth: %w(gitlab_jira_username gitlab_jira_password)).and_raise("Bad Request") WebMock.stub_request(:post, @transitions_url).with(basic_auth: %w(gitlab_jira_username gitlab_jira_password)).and_raise("Bad Request")
@jira_service.close_issue(resource, ExternalIssue.new('JIRA-123', project)) close_issue
expect(@jira_service).to have_received(:log_error).with( expect(@jira_service).to have_received(:log_error).with(
"Issue transition failed", "Issue transition failed",
...@@ -628,7 +643,7 @@ RSpec.describe JiraService do ...@@ -628,7 +643,7 @@ RSpec.describe JiraService do
end end
it 'calls the api with jira_issue_transition_id' do it 'calls the api with jira_issue_transition_id' do
@jira_service.close_issue(resource, ExternalIssue.new('JIRA-123', project)) close_issue
expect(WebMock).to have_requested(:post, @transitions_url).with( expect(WebMock).to have_requested(:post, @transitions_url).with(
body: /999/ body: /999/
...@@ -639,7 +654,7 @@ RSpec.describe JiraService do ...@@ -639,7 +654,7 @@ RSpec.describe JiraService do
it 'calls the api with transition ids separated by comma' do it 'calls the api with transition ids separated by comma' do
allow(@jira_service).to receive_messages(jira_issue_transition_id: '1,2,3') allow(@jira_service).to receive_messages(jira_issue_transition_id: '1,2,3')
@jira_service.close_issue(resource, ExternalIssue.new('JIRA-123', project)) close_issue
1.upto(3) do |transition_id| 1.upto(3) do |transition_id|
expect(WebMock).to have_requested(:post, @transitions_url).with( expect(WebMock).to have_requested(:post, @transitions_url).with(
...@@ -651,7 +666,7 @@ RSpec.describe JiraService do ...@@ -651,7 +666,7 @@ RSpec.describe JiraService do
it 'calls the api with transition ids separated by semicolon' do it 'calls the api with transition ids separated by semicolon' do
allow(@jira_service).to receive_messages(jira_issue_transition_id: '1;2;3') allow(@jira_service).to receive_messages(jira_issue_transition_id: '1;2;3')
@jira_service.close_issue(resource, ExternalIssue.new('JIRA-123', project)) close_issue
1.upto(3) do |transition_id| 1.upto(3) do |transition_id|
expect(WebMock).to have_requested(:post, @transitions_url).with( expect(WebMock).to have_requested(:post, @transitions_url).with(
...@@ -702,6 +717,14 @@ RSpec.describe JiraService do ...@@ -702,6 +717,14 @@ RSpec.describe JiraService do
body: /mentioned this issue in/ body: /mentioned this issue in/
).once ).once
end end
it 'tracks usage' do
expect(Gitlab::UsageDataCounters::HLLRedisCounter)
.to receive(:track_event)
.with('i_ecosystem_jira_service_cross_reference', values: user.id)
subject
end
end end
context 'when resource is a commit' do context 'when resource is a commit' do
......
...@@ -161,7 +161,7 @@ RSpec.describe MergeRequests::MergeService do ...@@ -161,7 +161,7 @@ RSpec.describe MergeRequests::MergeService do
commit = double('commit', safe_message: "Fixes #{jira_issue.to_reference}") commit = double('commit', safe_message: "Fixes #{jira_issue.to_reference}")
allow(merge_request).to receive(:commits).and_return([commit]) allow(merge_request).to receive(:commits).and_return([commit])
expect_any_instance_of(JiraService).to receive(:close_issue).with(merge_request, jira_issue).once expect_any_instance_of(JiraService).to receive(:close_issue).with(merge_request, jira_issue, user).once
service.execute(merge_request) service.execute(merge_request)
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