Commit 21db9a55 authored by Felipe Artur's avatar Felipe Artur Committed by Sean McGivern

Track JIRA dev panel integration

parent defefe4f
......@@ -2215,6 +2215,14 @@ ActiveRecord::Schema.define(version: 20190124200344) do
t.string "encrypted_token_iv"
end
create_table "project_feature_usages", primary_key: "project_id", id: :integer, force: :cascade do |t|
t.datetime "jira_dvcs_cloud_last_sync_at"
t.datetime "jira_dvcs_server_last_sync_at"
t.index ["jira_dvcs_cloud_last_sync_at", "project_id"], name: "idx_proj_feat_usg_on_jira_dvcs_cloud_last_sync_at_and_proj_id", where: "(jira_dvcs_cloud_last_sync_at IS NOT NULL)", using: :btree
t.index ["jira_dvcs_server_last_sync_at", "project_id"], name: "idx_proj_feat_usg_on_jira_dvcs_server_last_sync_at_and_proj_id", where: "(jira_dvcs_server_last_sync_at IS NOT NULL)", using: :btree
t.index ["project_id"], name: "index_project_feature_usages_on_project_id", using: :btree
end
create_table "project_features", force: :cascade do |t|
t.integer "project_id", null: false
t.integer "merge_requests_access_level"
......@@ -3462,6 +3470,7 @@ ActiveRecord::Schema.define(version: 20190124200344) do
add_foreign_key "project_deploy_tokens", "deploy_tokens", on_delete: :cascade
add_foreign_key "project_deploy_tokens", "projects", on_delete: :cascade
add_foreign_key "project_error_tracking_settings", "projects", on_delete: :cascade
add_foreign_key "project_feature_usages", "projects", on_delete: :cascade
add_foreign_key "project_features", "projects", name: "fk_18513d9b92", on_delete: :cascade
add_foreign_key "project_group_links", "projects", name: "fk_daa8cee94c", on_delete: :cascade
add_foreign_key "project_import_data", "projects", name: "fk_ffb9ee3a10", on_delete: :cascade
......
......@@ -40,6 +40,7 @@ module EE
has_one :github_service
has_one :gitlab_slack_application_service
has_one :tracing_setting, class_name: 'ProjectTracingSetting'
has_one :feature_usage, class_name: 'ProjectFeatureUsage'
has_many :reviews, inverse_of: :project
has_many :approvers, as: :target, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
......@@ -99,6 +100,8 @@ module EE
:ever_updated_successfully?, :hard_failed?,
to: :import_state, prefix: :mirror, allow_nil: true
delegate :log_jira_dvcs_integration_usage, :jira_dvcs_server_last_sync_at, :jira_dvcs_cloud_last_sync_at, to: :feature_usage
validates :repository_size_limit,
numericality: { only_integer: true, greater_than_or_equal_to: 0, allow_nil: true }
......@@ -504,6 +507,10 @@ module EE
geo_primary_http_url_to_repo(self)
end
def feature_usage
super.presence || build_feature_usage
end
private
def set_override_pull_mirror_available
......
# frozen_string_literal: true
class ProjectFeatureUsage < ActiveRecord::Base
self.primary_key = :project_id
JIRA_DVCS_CLOUD_FIELD = 'jira_dvcs_cloud_last_sync_at'.freeze
JIRA_DVCS_SERVER_FIELD = 'jira_dvcs_server_last_sync_at'.freeze
belongs_to :project
validates :project, presence: true
scope :with_jira_dvcs_integration_enabled, -> (cloud: true) do
where.not(jira_dvcs_integration_field(cloud: cloud) => nil)
end
class << self
def jira_dvcs_integration_field(cloud: true)
cloud ? JIRA_DVCS_CLOUD_FIELD : JIRA_DVCS_SERVER_FIELD
end
end
def log_jira_dvcs_integration_usage(cloud: true)
transaction(requires_new: true) do
save unless persisted?
touch(self.class.jira_dvcs_integration_field(cloud: cloud))
end
rescue ActiveRecord::RecordNotUnique
reload
retry
end
end
---
title: Gather JIRA DVCS integration usage data
merge_request: 8949
author:
type: other
# frozen_string_literal: true
class CreateProjectFeatureUsage < ActiveRecord::Migration[5.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def change
create_table :project_feature_usages, id: false, primary_key: :project_id do |t|
t.references :project,
foreign_key: { on_delete: :cascade },
null: false,
primary_key: true
t.timestamp :jira_dvcs_cloud_last_sync_at
t.timestamp :jira_dvcs_server_last_sync_at
t.index [:jira_dvcs_cloud_last_sync_at, :project_id], name: "idx_proj_feat_usg_on_jira_dvcs_cloud_last_sync_at_and_proj_id", where: "(jira_dvcs_cloud_last_sync_at IS NOT NULL)"
t.index [:jira_dvcs_server_last_sync_at, :project_id], name: "idx_proj_feat_usg_on_jira_dvcs_server_last_sync_at_and_proj_id", where: "(jira_dvcs_server_last_sync_at IS NOT NULL)"
end
end
end
......@@ -13,6 +13,11 @@ module API
NAMESPACE_ENDPOINT_REQUIREMENTS = { namespace: NO_SLASH_URL_PART_REGEX }.freeze
PROJECT_ENDPOINT_REQUIREMENTS = NAMESPACE_ENDPOINT_REQUIREMENTS.merge(project: NO_SLASH_URL_PART_REGEX).freeze
# Used to differentiate JIRA cloud requests from JIRA server requests
# JIRA cloud user agent format: JIRA DVCS Connector Vertigo/version
# JIRA server user agent format: JIRA DVCS Connector/version
JIRA_DVCS_CLOUD_USER_AGENT = 'JIRA DVCS Connector Vertigo'.freeze
include PaginationParams
before do
......@@ -30,6 +35,18 @@ module API
not_found! unless Gitlab::Jira::Middleware.jira_dvcs_connector?(request.env)
end
def update_project_feature_usage_for(project)
# Prevent errors on GitLab Geo not allowing
# UPDATE statements to happen in GET requests.
return if Gitlab::Database.read_only?
project.log_jira_dvcs_integration_usage(cloud: jira_cloud?)
end
def jira_cloud?
request.env['HTTP_USER_AGENT'].include?(JIRA_DVCS_CLOUD_USER_AGENT)
end
def find_project_with_access(params)
project = find_project!(
::Gitlab::Jira::Dvcs.restore_full_path(params.slice(:namespace, :project).symbolize_keys)
......@@ -162,6 +179,8 @@ module API
get ':namespace/:project/branches', requirements: PROJECT_ENDPOINT_REQUIREMENTS do
user_project = find_project_with_access(params)
update_project_feature_usage_for(user_project)
branches = ::Kaminari.paginate_array(user_project.repository.branches.sort_by(&:name))
present paginate(branches), with: ::API::Github::Entities::Branch, project: user_project
......
......@@ -113,6 +113,14 @@ module EE
usage_data
end
override :jira_usage
def jira_usage
super.merge(
projects_jira_dvcs_cloud_active: count(ProjectFeatureUsage.with_jira_dvcs_integration_enabled),
projects_jira_dvcs_server_active: count(ProjectFeatureUsage.with_jira_dvcs_integration_enabled(cloud: false))
)
end
def epics_deepest_relationship_level
{ epics_deepest_relationship_level: ::Epic.deepest_relationship_level }
end
......
......@@ -51,6 +51,7 @@ project:
- jenkins_service
- jenkins_deprecated_service
- index_status
- feature_usage
- approval_rules
- approvers
- pages_domains
......
......@@ -67,6 +67,8 @@ describe Gitlab::UsageData do
projects_with_prometheus_alerts
projects_with_packages
projects_with_tracing_enabled
projects_jira_dvcs_cloud_active
projects_jira_dvcs_server_active
))
expect(count_data[:projects_with_prometheus_alerts]).to eq(2)
......
# frozen_string_literal: true
require 'rails_helper'
describe ProjectFeatureUsage, type: :model do
describe '.jira_dvcs_integrations_enabled_count' do
it 'returns count of projects with JIRA DVCS cloud enabled' do
create(:project).feature_usage.log_jira_dvcs_integration_usage
create(:project).feature_usage.log_jira_dvcs_integration_usage
expect(described_class.with_jira_dvcs_integration_enabled.count).to eq(2)
end
it 'returns count of projects with JIRA DVCS server enabled' do
create(:project).feature_usage.log_jira_dvcs_integration_usage(cloud: false)
create(:project).feature_usage.log_jira_dvcs_integration_usage(cloud: false)
expect(described_class.with_jira_dvcs_integration_enabled(cloud: false).count).to eq(2)
end
end
describe '#log_jira_dvcs_integration_usage' do
let(:project) { create(:project) }
subject { project.feature_usage }
it 'logs JIRA DVCS cloud last sync' do
Timecop.freeze do
subject.log_jira_dvcs_integration_usage
expect(subject.jira_dvcs_server_last_sync_at).to be_nil
expect(subject.jira_dvcs_cloud_last_sync_at).to be_like_time(Time.now)
end
end
it 'logs JIRA DVCS server last sync' do
Timecop.freeze do
subject.log_jira_dvcs_integration_usage(cloud: false)
expect(subject.jira_dvcs_server_last_sync_at).to be_like_time(Time.now)
expect(subject.jira_dvcs_cloud_last_sync_at).to be_nil
end
end
context 'when log_jira_dvcs_integration_usage is called simultaneously for the same project' do
it 'logs the latest call' do
feature_usage = project.feature_usage
feature_usage.log_jira_dvcs_integration_usage
first_logged_at = feature_usage.jira_dvcs_cloud_last_sync_at
Timecop.freeze(1.hour.from_now) do
ProjectFeatureUsage.new(project_id: project.id).log_jira_dvcs_integration_usage
end
expect(feature_usage.reload.jira_dvcs_cloud_last_sync_at).to be > first_logged_at
end
end
end
end
......@@ -6,7 +6,6 @@ describe API::V3::Github do
let!(:project2) { create(:project, :repository, creator: user) }
before do
allow(Gitlab::Jira::Middleware).to receive(:jira_dvcs_connector?) { true }
project.add_maintainer(user)
project2.add_maintainer(user)
end
......@@ -15,7 +14,7 @@ describe API::V3::Github do
it 'returns an empty array' do
group = create(:group)
get v3_api("/orgs/#{group.path}/repos", user)
jira_get v3_api("/orgs/#{group.path}/repos", user)
expect(response).to have_gitlab_http_status(200)
expect(json_response).to eq([])
......@@ -24,7 +23,7 @@ describe API::V3::Github do
it 'returns 200 when namespace path include a dot' do
group = create(:group, path: 'foo.bar')
get v3_api("/orgs/#{group.path}/repos", user)
jira_get v3_api("/orgs/#{group.path}/repos", user)
expect(response).to have_gitlab_http_status(200)
end
......@@ -32,7 +31,7 @@ describe API::V3::Github do
describe 'GET /user/repos' do
it 'returns an empty array' do
get v3_api('/user/repos', user)
jira_get v3_api('/user/repos', user)
expect(response).to have_gitlab_http_status(200)
expect(json_response).to eq([])
......@@ -42,7 +41,7 @@ describe API::V3::Github do
shared_examples_for 'Jira-specific mimicked GitHub endpoints' do
describe 'GET /repos/.../events' do
it 'returns an empty array' do
get v3_api("/repos/#{path}/events", user)
jira_get v3_api("/repos/#{path}/events", user)
expect(response).to have_gitlab_http_status(200)
expect(json_response).to eq([])
......@@ -61,7 +60,7 @@ describe API::V3::Github do
it 'returns an array of notes' do
stub_licensed_features(jira_dev_panel_integration: true)
get v3_api("/repos/#{path}/issues/#{merge_request.id}/comments", user)
jira_get v3_api("/repos/#{path}/issues/#{merge_request.id}/comments", user)
expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an(Array)
......@@ -85,7 +84,7 @@ describe API::V3::Github do
it 'returns 404' do
stub_licensed_features(jira_dev_panel_integration: true)
get v3_api("/repos/#{path}/issues/#{merge_request.id}/comments", user)
jira_get v3_api("/repos/#{path}/issues/#{merge_request.id}/comments", user)
expect(response).to have_gitlab_http_status(404)
end
......@@ -94,7 +93,7 @@ describe API::V3::Github do
describe 'GET /.../pulls/:id/commits' do
it 'returns an empty array' do
get v3_api("/repos/#{path}/pulls/xpto/commits", user)
jira_get v3_api("/repos/#{path}/pulls/xpto/commits", user)
expect(response).to have_gitlab_http_status(200)
expect(json_response).to eq([])
......@@ -103,7 +102,7 @@ describe API::V3::Github do
describe 'GET /.../pulls/:id/comments' do
it 'returns an empty array' do
get v3_api("/repos/#{path}/pulls/xpto/comments", user)
jira_get v3_api("/repos/#{path}/pulls/xpto/comments", user)
expect(response).to have_gitlab_http_status(200)
expect(json_response).to eq([])
......@@ -138,7 +137,7 @@ describe API::V3::Github do
it 'returns an array of merge requests with github format' do
stub_licensed_features(jira_dev_panel_integration: true)
get v3_api('/repos/-/jira/pulls', user)
jira_get v3_api('/repos/-/jira/pulls', user)
expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an(Array)
......@@ -151,7 +150,7 @@ describe API::V3::Github do
it 'returns an array of merge requests for the proper project in github format' do
stub_licensed_features(jira_dev_panel_integration: true)
get v3_api("/repos/#{project.namespace.path}/#{project.path}/pulls", user)
jira_get v3_api("/repos/#{project.namespace.path}/#{project.path}/pulls", user)
expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an(Array)
......@@ -165,7 +164,7 @@ describe API::V3::Github do
let(:group) { create(:group, name: 'foo') }
def expect_project_under_namespace(projects, namespace, user)
get v3_api("/users/#{namespace.path}/repos", user)
jira_get v3_api("/users/#{namespace.path}/repos", user)
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
......@@ -239,7 +238,7 @@ describe API::V3::Github do
context 'unauthenticated' do
it 'returns 401' do
get v3_api("/users/foo/repos", nil)
jira_get v3_api("/users/foo/repos", nil)
expect(response).to have_gitlab_http_status(401)
end
......@@ -262,7 +261,7 @@ describe API::V3::Github do
it 'responds with not found status' do
stub_licensed_features(jira_dev_panel_integration: true)
get v3_api("/users/noo/repos", user)
jira_get v3_api("/users/noo/repos", user)
expect(response).to have_gitlab_http_status(404)
end
......@@ -275,8 +274,30 @@ describe API::V3::Github do
stub_licensed_features(jira_dev_panel_integration: true)
end
context 'updating project feature usage' do
it 'counts JIRA cloud integration as enabled' do
user_agent = 'JIRA DVCS Connector Vertigo/4.42.0'
Timecop.freeze do
jira_get v3_api("/repos/#{project.namespace.path}/#{project.path}/branches", user), user_agent
expect(project.reload.jira_dvcs_cloud_last_sync_at).to be_like_time(Time.now)
end
end
it 'counts JIRA server integration as enabled' do
user_agent = 'JIRA DVCS Connector/3.2.4'
Timecop.freeze do
jira_get v3_api("/repos/#{project.namespace.path}/#{project.path}/branches", user), user_agent
expect(project.reload.jira_dvcs_server_last_sync_at).to be_like_time(Time.now)
end
end
end
it 'returns an array of project branches with github format' do
get v3_api("/repos/#{project.namespace.path}/#{project.path}/branches", user)
jira_get v3_api("/repos/#{project.namespace.path}/#{project.path}/branches", user)
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
......@@ -288,7 +309,7 @@ describe API::V3::Github do
it 'returns 200 when project path include a dot' do
project.update!(path: 'foo.bar')
get v3_api("/repos/#{project.namespace.path}/#{project.path}/branches", user)
jira_get v3_api("/repos/#{project.namespace.path}/#{project.path}/branches", user)
expect(response).to have_gitlab_http_status(200)
end
......@@ -298,7 +319,7 @@ describe API::V3::Github do
project = create(:project, :repository, group: group)
project.add_reporter(user)
get v3_api("/repos/#{group.path}/#{project.path}/branches", user)
jira_get v3_api("/repos/#{group.path}/#{project.path}/branches", user)
expect(response).to have_gitlab_http_status(200)
end
......@@ -308,7 +329,7 @@ describe API::V3::Github do
it 'returns 401' do
stub_licensed_features(jira_dev_panel_integration: true)
get v3_api("/repos/#{project.namespace.path}/#{project.path}/branches", nil)
jira_get v3_api("/repos/#{project.namespace.path}/#{project.path}/branches", nil)
expect(response).to have_gitlab_http_status(401)
end
......@@ -320,7 +341,7 @@ describe API::V3::Github do
unauthorized_user = create(:user)
project.add_reporter(unauthorized_user)
get v3_api("/repos/#{project.namespace.path}/#{project.path}/branches", unauthorized_user)
jira_get v3_api("/repos/#{project.namespace.path}/#{project.path}/branches", unauthorized_user)
expect(response).to have_gitlab_http_status(404)
end
......@@ -337,7 +358,7 @@ describe API::V3::Github do
end
it 'returns commit with github format' do
get v3_api("/repos/#{project.namespace.path}/#{project.path}/commits/#{commit_id}", user)
jira_get v3_api("/repos/#{project.namespace.path}/#{project.path}/commits/#{commit_id}", user)
expect(response).to have_gitlab_http_status(200)
expect(response).to match_response_schema('entities/github/commit', dir: 'ee')
......@@ -346,7 +367,7 @@ describe API::V3::Github do
it 'returns 200 when project path include a dot' do
project.update!(path: 'foo.bar')
get v3_api("/repos/#{project.namespace.path}/#{project.path}/commits/#{commit_id}", user)
jira_get v3_api("/repos/#{project.namespace.path}/#{project.path}/commits/#{commit_id}", user)
expect(response).to have_gitlab_http_status(200)
end
......@@ -356,7 +377,7 @@ describe API::V3::Github do
project = create(:project, :repository, group: group)
project.add_reporter(user)
get v3_api("/repos/#{group.path}/#{project.path}/commits/#{commit_id}", user)
jira_get v3_api("/repos/#{group.path}/#{project.path}/commits/#{commit_id}", user)
expect(response).to have_gitlab_http_status(200)
end
......@@ -364,7 +385,7 @@ describe API::V3::Github do
context 'unauthenticated' do
it 'returns 401' do
get v3_api("/repos/#{project.namespace.path}/#{project.path}/commits/#{commit_id}", nil)
jira_get v3_api("/repos/#{project.namespace.path}/#{project.path}/commits/#{commit_id}", nil)
expect(response).to have_gitlab_http_status(401)
end
......@@ -375,7 +396,7 @@ describe API::V3::Github do
unauthorized_user = create(:user)
project.add_guest(unauthorized_user)
get v3_api("/repos/#{project.namespace.path}/#{project.path}/commits/#{commit_id}",
jira_get v3_api("/repos/#{project.namespace.path}/#{project.path}/commits/#{commit_id}",
unauthorized_user)
expect(response).to have_gitlab_http_status(404)
......@@ -386,7 +407,7 @@ describe API::V3::Github do
unauthorized_user = create(:user)
project.add_reporter(unauthorized_user)
get v3_api("/repos/#{project.namespace.path}/#{project.path}/commits/#{commit_id}",
jira_get v3_api("/repos/#{project.namespace.path}/#{project.path}/commits/#{commit_id}",
unauthorized_user)
expect(response).to have_gitlab_http_status(404)
......@@ -394,6 +415,10 @@ describe API::V3::Github do
end
end
def jira_get(path, user_agent = 'JIRA DVCS Connector/3.2.4')
get path, headers: { 'User-Agent' => user_agent }
end
def v3_api(path, user = nil, personal_access_token: nil, oauth_access_token: nil)
api(
path,
......
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