Commit 272281e4 authored by Douwe Maan's avatar Douwe Maan

Merge branch 'usage_consent' into 'master'

Ask user explicitly about usage stats agreement

Closes #48962

See merge request gitlab-org/gitlab-ce!21423
parents 1f765ce9 a415a904
...@@ -29,6 +29,7 @@ import './milestone_select'; ...@@ -29,6 +29,7 @@ import './milestone_select';
import './frequent_items'; import './frequent_items';
import initBreadcrumbs from './breadcrumb'; import initBreadcrumbs from './breadcrumb';
import initDispatcher from './dispatcher'; import initDispatcher from './dispatcher';
import initUsagePingConsent from './usage_ping_consent';
// expose jQuery as global (TODO: remove these) // expose jQuery as global (TODO: remove these)
window.jQuery = jQuery; window.jQuery = jQuery;
...@@ -78,6 +79,7 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -78,6 +79,7 @@ document.addEventListener('DOMContentLoaded', () => {
initImporterStatus(); initImporterStatus();
initTodoToggle(); initTodoToggle();
initLogoAnimation(); initLogoAnimation();
initUsagePingConsent();
// Set the default path for all cookies to GitLab's root directory // Set the default path for all cookies to GitLab's root directory
Cookies.defaults.path = gon.relative_url_root || '/'; Cookies.defaults.path = gon.relative_url_root || '/';
......
import $ from 'jquery';
import axios from './lib/utils/axios_utils';
import Flash, { hideFlash } from './flash';
import { convertPermissionToBoolean } from './lib/utils/common_utils';
export default () => {
$('body').on('click', '.js-usage-consent-action', (e) => {
e.preventDefault();
e.stopImmediatePropagation(); // overwrite rails listener
const { url, checkEnabled, pingEnabled } = e.target.dataset;
const data = {
application_setting: {
version_check_enabled: convertPermissionToBoolean(checkEnabled),
usage_ping_enabled: convertPermissionToBoolean(pingEnabled),
},
};
const hideConsentMessage = () => hideFlash(document.querySelector('.ping-consent-message'));
axios.put(url, data)
.then(() => {
hideConsentMessage();
})
.catch(() => {
hideConsentMessage();
Flash('Something went wrong. Try again later.');
});
});
};
...@@ -69,10 +69,14 @@ body { ...@@ -69,10 +69,14 @@ body {
float: right; float: right;
} }
/* Center alert text and alert action links on smaller screens */ .flex-alert {
@include media-breakpoint-down(sm) { @include media-breakpoint-up(lg) {
.alert { display: flex;
text-align: center;
.alert-message {
flex: 1;
padding-right: 40px;
}
} }
.alert-link-group { .alert-link-group {
...@@ -80,6 +84,13 @@ body { ...@@ -80,6 +84,13 @@ body {
} }
} }
@include media-breakpoint-down(sm) {
.alert-link-group {
float: none;
margin-top: $gl-padding-8;
}
}
/* Stripe the background colors so that adjacent alert-warnings are distinct from one another */ /* Stripe the background colors so that adjacent alert-warnings are distinct from one another */
.alert-warning { .alert-warning {
transition: background-color 0.15s, border-color 0.15s; transition: background-color 0.15s, border-color 0.15s;
......
...@@ -9,11 +9,18 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController ...@@ -9,11 +9,18 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
.new(@application_setting, current_user, application_setting_params) .new(@application_setting, current_user, application_setting_params)
.execute .execute
if recheck_user_consent?
session[:ask_for_usage_stats_consent] = current_user.requires_usage_stats_consent?
end
respond_to do |format|
if successful if successful
redirect_to admin_application_settings_path, format.json { head :ok }
notice: 'Application settings saved successfully' format.html { redirect_to admin_application_settings_path, notice: 'Application settings saved successfully' }
else else
render :show format.json { head :bad_request }
format.html { render :show }
end
end end
end end
...@@ -76,6 +83,13 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController ...@@ -76,6 +83,13 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
) )
end end
def recheck_user_consent?
return false unless session[:ask_for_usage_stats_consent]
return false unless params[:application_setting]
params[:application_setting].key?(:usage_ping_enabled) || params[:application_setting].key?(:version_check_enabled)
end
def visible_application_setting_attributes def visible_application_setting_attributes
ApplicationSettingsHelper.visible_attributes + [ ApplicationSettingsHelper.visible_attributes + [
:domain_blacklist_file, :domain_blacklist_file,
......
...@@ -22,6 +22,7 @@ class ApplicationController < ActionController::Base ...@@ -22,6 +22,7 @@ class ApplicationController < ActionController::Base
before_action :add_gon_variables, unless: [:peek_request?, :json_request?] before_action :add_gon_variables, unless: [:peek_request?, :json_request?]
before_action :configure_permitted_parameters, if: :devise_controller? before_action :configure_permitted_parameters, if: :devise_controller?
before_action :require_email, unless: :devise_controller? before_action :require_email, unless: :devise_controller?
before_action :set_usage_stats_consent_flag
around_action :set_locale around_action :set_locale
...@@ -434,4 +435,29 @@ class ApplicationController < ActionController::Base ...@@ -434,4 +435,29 @@ class ApplicationController < ActionController::Base
!(peek_request? || devise_controller?) !(peek_request? || devise_controller?)
end end
def set_usage_stats_consent_flag
return unless current_user
return if sessionless_user?
return if session.has_key?(:ask_for_usage_stats_consent)
session[:ask_for_usage_stats_consent] = current_user.requires_usage_stats_consent?
if session[:ask_for_usage_stats_consent]
disable_usage_stats
end
end
def disable_usage_stats
application_setting_params = {
usage_ping_enabled: false,
version_check_enabled: false,
skip_usage_stats_user: true
}
settings = Gitlab::CurrentSettings.current_application_settings
ApplicationSettings::UpdateService
.new(settings, current_user, application_setting_params)
.execute
end
end end
module VersionCheckHelper module VersionCheckHelper
def version_status_badge def version_status_badge
if Rails.env.production? && Gitlab::CurrentSettings.version_check_enabled return unless Rails.env.production?
return unless Gitlab::CurrentSettings.version_check_enabled
return if User.single_user&.requires_usage_stats_consent?
image_url = VersionCheck.new.url image_url = VersionCheck.new.url
image_tag image_url, class: 'js-version-status-badge' image_tag image_url, class: 'js-version-status-badge'
end end
end
end end
...@@ -302,7 +302,8 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -302,7 +302,8 @@ class ApplicationSetting < ActiveRecord::Base
instance_statistics_visibility_private: false, instance_statistics_visibility_private: false,
user_default_external: false, user_default_external: false,
user_default_internal_regex: nil, user_default_internal_regex: nil,
user_show_add_ssh_key_message: true user_show_add_ssh_key_message: true,
usage_stats_set_by_user_id: nil
} }
end end
......
...@@ -490,6 +490,16 @@ class User < ActiveRecord::Base ...@@ -490,6 +490,16 @@ class User < ActiveRecord::Base
u.name = 'Ghost User' u.name = 'Ghost User'
end end
end end
# Return true if there is only single non-internal user in the deployment,
# ghost user is ignored.
def single_user?
User.non_internal.limit(2).count == 1
end
def single_user
User.non_internal.first if single_user?
end
end end
def full_path def full_path
...@@ -1287,6 +1297,10 @@ class User < ActiveRecord::Base ...@@ -1287,6 +1297,10 @@ class User < ActiveRecord::Base
!terms_accepted? !terms_accepted?
end end
def requires_usage_stats_consent?
!consented_usage_stats? && 7.days.ago > self.created_at && !has_current_license? && User.single_user?
end
# @deprecated # @deprecated
alias_method :owned_or_masters_groups, :owned_or_maintainers_groups alias_method :owned_or_masters_groups, :owned_or_maintainers_groups
...@@ -1301,6 +1315,14 @@ class User < ActiveRecord::Base ...@@ -1301,6 +1315,14 @@ class User < ActiveRecord::Base
private private
def has_current_license?
false
end
def consented_usage_stats?
Gitlab::CurrentSettings.usage_stats_set_by_user_id == self.id
end
def owned_projects_union def owned_projects_union
Gitlab::SQL::Union.new([ Gitlab::SQL::Union.new([
Project.where(namespace: namespace), Project.where(namespace: namespace),
......
...@@ -11,11 +11,19 @@ module ApplicationSettings ...@@ -11,11 +11,19 @@ module ApplicationSettings
params[:performance_bar_allowed_group_id] = performance_bar_allowed_group_id params[:performance_bar_allowed_group_id] = performance_bar_allowed_group_id
end end
if usage_stats_updated? && !params.delete(:skip_usage_stats_user)
params[:usage_stats_set_by_user_id] = current_user.id
end
@application_setting.update(@params) @application_setting.update(@params)
end end
private private
def usage_stats_updated?
params.key?(:usage_ping_enabled) || params.key?(:version_check_enabled)
end
def update_terms(terms) def update_terms(terms)
return unless terms.present? return unless terms.present?
......
...@@ -15,6 +15,7 @@ class SubmitUsagePingService ...@@ -15,6 +15,7 @@ class SubmitUsagePingService
def execute def execute
return false unless Gitlab::CurrentSettings.usage_ping_enabled? return false unless Gitlab::CurrentSettings.usage_ping_enabled?
return false if User.single_user&.requires_usage_stats_consent?
response = Gitlab::HTTP.post( response = Gitlab::HTTP.post(
URL, URL,
......
...@@ -8,6 +8,7 @@ ...@@ -8,6 +8,7 @@
= render "layouts/broadcast" = render "layouts/broadcast"
= render 'layouts/header/read_only_banner' = render 'layouts/header/read_only_banner'
= yield :flash_message = yield :flash_message
= render "shared/ping_consent"
- unless @hide_breadcrumbs - unless @hide_breadcrumbs
= render "layouts/nav/breadcrumbs" = render "layouts/nav/breadcrumbs"
= render "layouts/flash" = render "layouts/flash"
......
- if session[:ask_for_usage_stats_consent]
.ping-consent-message.alert.alert-warning.flex-alert
- settings_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer" class="alert-link">'.html_safe % { url: admin_application_settings_path(anchor: 'js-usage-settings') }
- info_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer" class="alert-link">'.html_safe % { url: help_page_path('user/admin_area/settings/usage_statistics.md') }
.alert-message
= s_('To help improve GitLab, we would like to periodically collect usage information. This can be changed at any time in %{settings_link_start}Settings%{link_end}. %{info_link_start}More Information%{link_end}').html_safe % { settings_link_start: settings_link_start, info_link_start: info_link_start, link_end: '</a>'.html_safe }
.alert-link-group
- send_usage_data_path = admin_application_settings_path(application_setting: { version_check_enabled: 1, usage_ping_enabled: 1 })
- not_now_path = admin_application_settings_path(application_setting: { version_check_enabled: 0, usage_ping_enabled: 0 })
= link_to _("Send usage data"), send_usage_data_path, 'data-url' => admin_application_settings_path, method: :put, 'data-check-enabled': true, 'data-ping-enabled': true, class: 'alert-link js-usage-consent-action'
|
= link_to _('Not now'), not_now_path, 'data-url' => admin_application_settings_path, method: :put, 'data-check-enabled': false, 'data-ping-enabled': false, class: 'hide-ping-consent-message alert-link js-usage-consent-action'
---
title: Ask user explicitly about usage stats agreement on single user deployments.
merge_request: 21423
author:
type: added
# frozen_string_literal: true
class AddUserPingConsentToApplicationSettings < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
add_column :application_settings, :usage_stats_set_by_user_id, :integer
add_concurrent_foreign_key :application_settings, :users, column: :usage_stats_set_by_user_id, on_delete: :nullify
end
def down
remove_foreign_key :application_settings, column: :usage_stats_set_by_user_id
remove_column :application_settings, :usage_stats_set_by_user_id
end
end
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20180901200537) do ActiveRecord::Schema.define(version: 20180906101639) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
...@@ -172,6 +172,7 @@ ActiveRecord::Schema.define(version: 20180901200537) do ...@@ -172,6 +172,7 @@ ActiveRecord::Schema.define(version: 20180901200537) do
t.boolean "instance_statistics_visibility_private", default: false, null: false t.boolean "instance_statistics_visibility_private", default: false, null: false
t.boolean "web_ide_clientside_preview_enabled", default: false, null: false t.boolean "web_ide_clientside_preview_enabled", default: false, null: false
t.boolean "user_show_add_ssh_key_message", default: true, null: false t.boolean "user_show_add_ssh_key_message", default: true, null: false
t.integer "usage_stats_set_by_user_id"
end end
create_table "audit_events", force: :cascade do |t| create_table "audit_events", force: :cascade do |t|
...@@ -2275,6 +2276,7 @@ ActiveRecord::Schema.define(version: 20180901200537) do ...@@ -2275,6 +2276,7 @@ ActiveRecord::Schema.define(version: 20180901200537) do
add_index "web_hooks", ["project_id"], name: "index_web_hooks_on_project_id", using: :btree add_index "web_hooks", ["project_id"], name: "index_web_hooks_on_project_id", using: :btree
add_index "web_hooks", ["type"], name: "index_web_hooks_on_type", using: :btree add_index "web_hooks", ["type"], name: "index_web_hooks_on_type", using: :btree
add_foreign_key "application_settings", "users", column: "usage_stats_set_by_user_id", name: "fk_964370041d", on_delete: :nullify
add_foreign_key "badges", "namespaces", column: "group_id", on_delete: :cascade add_foreign_key "badges", "namespaces", column: "group_id", on_delete: :cascade
add_foreign_key "badges", "projects", on_delete: :cascade add_foreign_key "badges", "projects", on_delete: :cascade
add_foreign_key "boards", "namespaces", column: "group_id", on_delete: :cascade add_foreign_key "boards", "namespaces", column: "group_id", on_delete: :cascade
......
...@@ -4004,6 +4004,9 @@ msgstr "" ...@@ -4004,6 +4004,9 @@ msgstr ""
msgid "Not enough data" msgid "Not enough data"
msgstr "" msgstr ""
msgid "Not now"
msgstr ""
msgid "Note that the master branch is automatically protected. %{link_to_protected_branches}" msgid "Note that the master branch is automatically protected. %{link_to_protected_branches}"
msgstr "" msgstr ""
...@@ -5177,6 +5180,9 @@ msgstr "" ...@@ -5177,6 +5180,9 @@ msgstr ""
msgid "Send email" msgid "Send email"
msgstr "" msgstr ""
msgid "Send usage data"
msgstr ""
msgid "Sep" msgid "Sep"
msgstr "" msgstr ""
...@@ -6145,6 +6151,9 @@ msgstr "" ...@@ -6145,6 +6151,9 @@ msgstr ""
msgid "To help improve GitLab and its user experience, GitLab will periodically collect usage information." msgid "To help improve GitLab and its user experience, GitLab will periodically collect usage information."
msgstr "" msgstr ""
msgid "To help improve GitLab, we would like to periodically collect usage information. This can be changed at any time in %{settings_link_start}Settings%{link_end}. %{info_link_start}More Information%{link_end}"
msgstr ""
msgid "To import GitHub repositories, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to import." msgid "To import GitHub repositories, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to import."
msgstr "" msgstr ""
......
# frozen_string_literal: true
require 'spec_helper'
describe 'Usage stats consent' do
context 'when signed in' do
let(:user) { create(:admin, created_at: 8.days.ago) }
let(:message) { 'To help improve GitLab, we would like to periodically collect usage information.' }
before do
allow(user).to receive(:has_current_license?).and_return false
gitlab_sign_in(user)
end
it 'hides the banner permanently when sets usage stats' do
visit root_dashboard_path
expect(page).to have_content(message)
click_link 'Send usage data'
expect(page).not_to have_content(message)
expect(page).to have_content('Application settings saved successfully')
gitlab_sign_out
gitlab_sign_in(user)
visit root_dashboard_path
expect(page).not_to have_content(message)
end
it 'shows banner on next session if user did not set usage stats' do
visit root_dashboard_path
expect(page).to have_content(message)
gitlab_sign_out
gitlab_sign_in(user)
visit root_dashboard_path
expect(page).to have_content(message)
end
end
end
...@@ -2957,6 +2957,48 @@ describe User do ...@@ -2957,6 +2957,48 @@ describe User do
end end
end end
describe '#requires_usage_stats_consent?' do
let(:user) { create(:user, created_at: 8.days.ago) }
before do
allow(user).to receive(:has_current_license?).and_return false
end
context 'in single-user environment' do
it 'requires user consent after one week' do
create(:user, ghost: true)
expect(user.requires_usage_stats_consent?).to be true
end
it 'requires user consent after one week if there is another ghost user' do
expect(user.requires_usage_stats_consent?).to be true
end
it 'does not require consent in the first week' do
user.created_at = 6.days.ago
expect(user.requires_usage_stats_consent?).to be false
end
it 'does not require consent if usage stats were set by this user' do
allow(Gitlab::CurrentSettings).to receive(:usage_stats_set_by_user_id).and_return(user.id)
expect(user.requires_usage_stats_consent?).to be false
end
end
context 'in multi-user environment' do
before do
create(:user)
end
it 'does not require consent' do
expect(user.requires_usage_stats_consent?).to be false
end
end
end
context 'with uploads' do context 'with uploads' do
it_behaves_like 'model with mounted uploader', false do it_behaves_like 'model with mounted uploader', false do
let(:model_object) { create(:user, :with_avatar) } let(:model_object) { create(:user, :with_avatar) }
......
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