Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
a51e52bf5b
commit
fb553bbc18
37 changed files with 533 additions and 50 deletions
|
@ -768,6 +768,8 @@
|
|||
|
||||
.setup:rules:verify-tests-yml:
|
||||
rules:
|
||||
- <<: *if-not-ee
|
||||
when: never
|
||||
- <<: *if-default-refs
|
||||
changes: *code-backstage-patterns
|
||||
when: on_success
|
||||
|
|
2
Gemfile
2
Gemfile
|
@ -316,7 +316,7 @@ gem 'ruby_parser', '~> 3.8', require: false
|
|||
gem 'rails-i18n', '~> 6.0'
|
||||
gem 'gettext_i18n_rails', '~> 1.8.0'
|
||||
gem 'gettext_i18n_rails_js', '~> 1.3'
|
||||
gem 'gettext', '~> 3.2.2', require: false, group: :development
|
||||
gem 'gettext', '~> 3.3', require: false, group: :development
|
||||
|
||||
gem 'batch-loader', '~> 1.4.0'
|
||||
|
||||
|
|
|
@ -403,7 +403,7 @@ GEM
|
|||
json
|
||||
get_process_mem (0.2.5)
|
||||
ffi (~> 1.0)
|
||||
gettext (3.2.9)
|
||||
gettext (3.3.6)
|
||||
locale (>= 2.0.5)
|
||||
text (>= 1.3.0)
|
||||
gettext_i18n_rails (1.8.0)
|
||||
|
@ -653,7 +653,7 @@ GEM
|
|||
rb-fsevent (~> 0.9, >= 0.9.4)
|
||||
rb-inotify (~> 0.9, >= 0.9.7)
|
||||
ruby_dep (~> 1.2)
|
||||
locale (2.1.2)
|
||||
locale (2.1.3)
|
||||
lockbox (0.3.3)
|
||||
lograge (0.11.2)
|
||||
actionpack (>= 4)
|
||||
|
@ -1297,7 +1297,7 @@ DEPENDENCIES
|
|||
fugit (~> 1.2.1)
|
||||
fuubar (~> 2.2.0)
|
||||
gemojione (~> 3.3)
|
||||
gettext (~> 3.2.2)
|
||||
gettext (~> 3.3)
|
||||
gettext_i18n_rails (~> 1.8.0)
|
||||
gettext_i18n_rails_js (~> 1.3)
|
||||
gitaly (~> 13.3.0.pre.rc1)
|
||||
|
|
|
@ -72,8 +72,8 @@ export default class MilestoneSelect {
|
|||
return initDeprecatedJQueryDropdown($dropdown, {
|
||||
showMenuAbove,
|
||||
data: (term, callback) => {
|
||||
let contextId = $dropdown.get(0).dataset.projectId;
|
||||
let getMilestones = Api.projectMilestones;
|
||||
let contextId = parseInt($dropdown.get(0).dataset.projectId, 10);
|
||||
let getMilestones = Api.projectMilestones.bind(Api);
|
||||
const reqParams = { state: 'active', include_parent_milestones: true };
|
||||
|
||||
if (term) {
|
||||
|
@ -83,7 +83,7 @@ export default class MilestoneSelect {
|
|||
if (!contextId) {
|
||||
contextId = $dropdown.get(0).dataset.groupId;
|
||||
delete reqParams.include_parent_milestones;
|
||||
getMilestones = Api.groupMilestones;
|
||||
getMilestones = Api.groupMilestones.bind(Api);
|
||||
}
|
||||
|
||||
// We don't use $.data() as it caches initial value and never updates!
|
||||
|
|
|
@ -4,7 +4,7 @@ import { GlButton, GlLoadingIcon } from '@gitlab/ui';
|
|||
import { deprecatedCreateFlash as Flash } from '~/flash';
|
||||
import { __, sprintf } from '~/locale';
|
||||
import TitleField from '~/vue_shared/components/form/title.vue';
|
||||
import { redirectTo } from '~/lib/utils/url_utility';
|
||||
import { redirectTo, joinPaths } from '~/lib/utils/url_utility';
|
||||
import FormFooterActions from '~/vue_shared/components/form/form_footer_actions.vue';
|
||||
|
||||
import UpdateSnippetMutation from '../mutations/updateSnippet.mutation.graphql';
|
||||
|
@ -88,9 +88,7 @@ export default {
|
|||
},
|
||||
cancelButtonHref() {
|
||||
if (this.newSnippet) {
|
||||
return this.projectPath
|
||||
? `${gon.relative_url_root}${this.projectPath}/-/snippets`
|
||||
: `${gon.relative_url_root}/-/snippets`;
|
||||
return joinPaths('/', gon.relative_url_root, this.projectPath, '-/snippets');
|
||||
}
|
||||
return this.snippet.webUrl;
|
||||
},
|
||||
|
|
|
@ -17,6 +17,7 @@ import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
|
|||
import DeleteSnippetMutation from '../mutations/deleteSnippet.mutation.graphql';
|
||||
import CanCreatePersonalSnippet from '../queries/userPermissions.query.graphql';
|
||||
import CanCreateProjectSnippet from '../queries/projectPermissions.query.graphql';
|
||||
import { joinPaths } from '~/lib/utils/url_utility';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
@ -96,8 +97,8 @@ export default {
|
|||
condition: this.canCreateSnippet,
|
||||
text: __('New snippet'),
|
||||
href: this.snippet.project
|
||||
? `${this.snippet.project.webUrl}/-/snippets/new`
|
||||
: `${gon.relative_url_root}/-/snippets/new`,
|
||||
? joinPaths(this.snippet.project.webUrl, '-/snippets/new')
|
||||
: joinPaths('/', gon.relative_url_root, '/-/snippets/new'),
|
||||
variant: 'success',
|
||||
category: 'secondary',
|
||||
cssClass: 'ml-2',
|
||||
|
|
|
@ -6,8 +6,26 @@ export default {
|
|||
components: {
|
||||
GlDrawer,
|
||||
},
|
||||
props: {
|
||||
features: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState(['open']),
|
||||
parsedFeatures() {
|
||||
let features;
|
||||
|
||||
try {
|
||||
features = JSON.parse(this.$props.features) || [];
|
||||
} catch (err) {
|
||||
features = [];
|
||||
}
|
||||
|
||||
return features;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['closeDrawer']),
|
||||
|
@ -22,7 +40,12 @@ export default {
|
|||
<h4>{{ __("What's new at GitLab") }}</h4>
|
||||
</template>
|
||||
<template>
|
||||
<div></div>
|
||||
<ul>
|
||||
<li v-for="feature in parsedFeatures" :key="feature.title">
|
||||
<h5>{{ feature.title }}</h5>
|
||||
<p>{{ feature.body }}</p>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
</gl-drawer>
|
||||
</div>
|
||||
|
|
|
@ -4,16 +4,21 @@ import Trigger from './components/trigger.vue';
|
|||
import store from './store';
|
||||
|
||||
export default () => {
|
||||
const whatsNewElm = document.getElementById('whats-new-app');
|
||||
|
||||
// eslint-disable-next-line no-new
|
||||
new Vue({
|
||||
el: document.getElementById('whats-new-app'),
|
||||
el: whatsNewElm,
|
||||
store,
|
||||
components: {
|
||||
App,
|
||||
},
|
||||
|
||||
render(createElement) {
|
||||
return createElement('app');
|
||||
return createElement('app', {
|
||||
props: {
|
||||
features: whatsNewElm.getAttribute('data-features'),
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
|
|
44
app/controllers/concerns/redis_tracking.rb
Normal file
44
app/controllers/concerns/redis_tracking.rb
Normal file
|
@ -0,0 +1,44 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Example:
|
||||
#
|
||||
# # In controller include module
|
||||
# # Track event for index action
|
||||
#
|
||||
# include RedisTracking
|
||||
#
|
||||
# track_redis_hll_event :index, :show, name: 'i_analytics_dev_ops_score', feature: :my_feature
|
||||
module RedisTracking
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
class_methods do
|
||||
def track_redis_hll_event(*controller_actions, name:, feature:)
|
||||
after_action only: controller_actions, if: -> { request.format.html? && request.headers['DNT'] != '1' } do
|
||||
track_unique_redis_hll_event(name, feature)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def track_unique_redis_hll_event(event_name, feature)
|
||||
return unless metric_feature_enabled?(feature)
|
||||
return unless Gitlab::CurrentSettings.usage_ping_enabled?
|
||||
return unless visitor_id
|
||||
|
||||
Gitlab::UsageDataCounters::HLLRedisCounter.track_event(visitor_id, event_name)
|
||||
end
|
||||
|
||||
def metric_feature_enabled?(feature)
|
||||
Feature.enabled?(feature)
|
||||
end
|
||||
|
||||
def visitor_id
|
||||
return cookies[:visitor_id] if cookies[:visitor_id].present?
|
||||
return unless current_user
|
||||
|
||||
uuid = SecureRandom.uuid
|
||||
cookies[:visitor_id] = { value: uuid, expires: 24.months }
|
||||
uuid
|
||||
end
|
||||
end
|
24
app/helpers/whats_new_helper.rb
Normal file
24
app/helpers/whats_new_helper.rb
Normal file
|
@ -0,0 +1,24 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module WhatsNewHelper
|
||||
EMPTY_JSON = ''.to_json
|
||||
|
||||
def whats_new_most_recent_release_items
|
||||
YAML.load_file(most_recent_release_file_path).to_json
|
||||
|
||||
rescue => e
|
||||
Gitlab::ErrorTracking.track_exception(e, yaml_file_path: most_recent_release_file_path)
|
||||
|
||||
EMPTY_JSON
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def most_recent_release_file_path
|
||||
Dir.glob(files_path).max
|
||||
end
|
||||
|
||||
def files_path
|
||||
Rails.root.join('data', 'whats_new', '*.yml')
|
||||
end
|
||||
end
|
|
@ -100,7 +100,7 @@
|
|||
= sprite_icon('close', size: 12, css_class: 'close-icon js-navbar-toggle-left')
|
||||
|
||||
- if ::Feature.enabled?(:whats_new_drawer)
|
||||
#whats-new-app
|
||||
#whats-new-app{ data: { features: whats_new_most_recent_release_items } }
|
||||
|
||||
- if can?(current_user, :update_user_status, current_user)
|
||||
.js-set-status-modal-wrapper{ data: { current_emoji: current_user.status.present? ? current_user.status.emoji : '', current_message: current_user.status.present? ? current_user.status.message : '' } }
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
name: "issue[milestone_id]",
|
||||
"v-if" => "issue.milestone" }
|
||||
.dropdown
|
||||
%button.dropdown-menu-toggle.js-milestone-select.js-issue-board-sidebar{ type: "button", data: { toggle: "dropdown", show_no: "true", field_name: "issue[milestone_id]", milestones: milestones_filter_path(format: :json), ability_name: "issue", use_id: "true", default_no: "true" },
|
||||
%button.dropdown-menu-toggle.js-milestone-select.js-issue-board-sidebar{ type: "button", data: { toggle: "dropdown", show_no: "true", field_name: "issue[milestone_id]", ability_name: "issue", use_id: "true", default_no: "true" },
|
||||
":data-selected" => "milestoneTitle",
|
||||
":data-issuable-id" => "issue.iid",
|
||||
":data-project-id" => "issue.project_id" }
|
||||
|
|
|
@ -40,7 +40,7 @@
|
|||
.title
|
||||
= _('Milestone')
|
||||
.filter-item
|
||||
= dropdown_tag(_("Select milestone"), options: { title: _("Assign milestone"), toggle_class: "js-milestone-select js-extra-options js-filter-submit js-filter-bulk-update", filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", placeholder: _("Search milestones"), data: { show_no: true, field_name: "update[milestone_id]", project_id: @project.id, milestones: project_milestones_path(@project, :json), use_id: true, default_label: _("Milestone") } })
|
||||
= dropdown_tag(_("Select milestone"), options: { title: _("Assign milestone"), toggle_class: "js-milestone-select js-extra-options js-filter-submit js-filter-bulk-update", filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", placeholder: _("Search milestones"), data: { show_no: true, field_name: "update[milestone_id]", project_id: @project.id, use_id: true, default_label: _("Milestone") } })
|
||||
.block
|
||||
.title
|
||||
= _('Labels')
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
- if selected.present? || params[:milestone_title].present?
|
||||
= hidden_field_tag(name, name == :milestone_title ? selected_text : selected.id)
|
||||
= dropdown_tag(milestone_dropdown_label(selected_text), options: { title: dropdown_title, toggle_class: "qa-issuable-milestone-dropdown js-milestone-select js-filter-submit #{extra_class}", filter: true, dropdown_class: "qa-issuable-dropdown-menu-milestone dropdown-menu-selectable dropdown-menu-milestone",
|
||||
placeholder: "Search milestones", footer_content: project.present?, data: { show_no: true, show_menu_above: show_menu_above, show_any: show_any, show_upcoming: show_upcoming, show_started: show_started, field_name: name, selected: selected_text, project_id: project.try(:id), milestones: milestones_filter_dropdown_path, default_label: "Milestone" } }) do
|
||||
placeholder: "Search milestones", footer_content: project.present?, data: { show_no: true, show_menu_above: show_menu_above, show_any: show_any, show_upcoming: show_upcoming, show_started: show_started, field_name: name, selected: selected_text, project_id: project.try(:id), default_label: "Milestone" } }) do
|
||||
- if project
|
||||
%ul.dropdown-footer-list
|
||||
- if can? current_user, :admin_milestone, project
|
||||
|
|
|
@ -53,7 +53,7 @@
|
|||
|
||||
.selectbox.hide-collapsed
|
||||
= f.hidden_field 'milestone_id', value: milestone[:id], id: nil
|
||||
= dropdown_tag('Milestone', options: { title: _('Assign milestone'), toggle_class: 'js-milestone-select js-extra-options', filter: true, dropdown_class: 'dropdown-menu-selectable', placeholder: _('Search milestones'), data: { show_no: true, field_name: "#{issuable_type}[milestone_id]", project_id: issuable_sidebar[:project_id], issuable_id: issuable_sidebar[:id], milestones: issuable_sidebar[:project_milestones_path], ability_name: issuable_type, issue_update: issuable_sidebar[:issuable_json_path], use_id: true, default_no: true, selected: milestone[:title], null_default: true, display: 'static' }})
|
||||
= dropdown_tag('Milestone', options: { title: _('Assign milestone'), toggle_class: 'js-milestone-select js-extra-options', filter: true, dropdown_class: 'dropdown-menu-selectable', placeholder: _('Search milestones'), data: { show_no: true, field_name: "#{issuable_type}[milestone_id]", project_id: issuable_sidebar[:project_id], issuable_id: issuable_sidebar[:id], ability_name: issuable_type, issue_update: issuable_sidebar[:issuable_json_path], use_id: true, default_no: true, selected: milestone[:title], null_default: true, display: 'static' }})
|
||||
- if @project.group.present?
|
||||
= render_if_exists 'shared/issuable/iteration_select', { can_edit: can_edit_issuable, group_path: @project.group.full_path, project_path: issuable_sidebar[:project_full_path], issue_iid: issuable_sidebar[:iid], issuable_type: issuable_type }
|
||||
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add virtual actions tracker for Usage Ping
|
||||
merge_request: 39694
|
||||
author:
|
||||
type: added
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
name: track_editor_edit_actions
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/39694
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/240928
|
||||
group: group::editor
|
||||
type: development
|
||||
default_enabled: false
|
|
@ -4,3 +4,13 @@
|
|||
tmp/restart.txt
|
||||
tmp/caching-dev.txt
|
||||
).each { |path| Spring.watch(path) }
|
||||
|
||||
Spring.after_fork do
|
||||
if ENV['DEBUGGER_STORED_RUBYLIB']
|
||||
ENV['DEBUGGER_STORED_RUBYLIB'].split(File::PATH_SEPARATOR).each do |path|
|
||||
next unless path =~ /ruby-debug-ide/
|
||||
|
||||
load path + '/ruby-debug-ide/multiprocess/starter.rb'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -7,14 +7,13 @@ class SchedulePopulateResolvedOnDefaultBranchColumn < ActiveRecord::Migration[6.
|
|||
BATCH_SIZE = 100
|
||||
DELAY_INTERVAL = 5.minutes.to_i
|
||||
MIGRATION_CLASS = 'PopulateResolvedOnDefaultBranchColumn'
|
||||
BASE_MODEL = EE::Gitlab::BackgroundMigration::PopulateResolvedOnDefaultBranchColumn::Vulnerability
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
return unless run_migration?
|
||||
|
||||
BASE_MODEL.distinct.each_batch(of: BATCH_SIZE, column: :project_id) do |batch, index|
|
||||
EE::Gitlab::BackgroundMigration::PopulateResolvedOnDefaultBranchColumn::Vulnerability.distinct.each_batch(of: BATCH_SIZE, column: :project_id) do |batch, index|
|
||||
project_ids = batch.pluck(:project_id)
|
||||
migrate_in(index * DELAY_INTERVAL, MIGRATION_CLASS, project_ids)
|
||||
end
|
||||
|
|
|
@ -179,12 +179,14 @@ strace -tt -T -f -y -yy -s 1024 -p <pid>
|
|||
ps auwx | grep unicorn | awk '{ print " -p " $2}' | xargs strace -tt -T -f -y -yy -s 1024 -o /tmp/unicorn.txt
|
||||
```
|
||||
|
||||
See the [strace zine](https://wizardzines.com/zines/strace/) for a quick walkthrough.
|
||||
|
||||
Brendan Gregg has a more detailed explanation of [how to use strace](http://www.brendangregg.com/blog/2014-05-11/strace-wow-much-syscall.html).
|
||||
|
||||
Be aware that strace can have major impacts to system performance when it is running.
|
||||
|
||||
#### Strace Resources
|
||||
|
||||
- See the [strace zine](https://wizardzines.com/zines/strace/) for a quick walkthrough.
|
||||
- Brendan Gregg has a more detailed explanation of [how to use strace](http://www.brendangregg.com/blog/2014-05-11/strace-wow-much-syscall.html).
|
||||
- We have a [series of GitLab Unfiltered videos](https://www.youtube.com/playlist?list=PL05JrBw4t0KoC7cIkoAFcRhr4gsVesekg) on using strace to understand GitLab.
|
||||
|
||||
### The Strace Parser tool
|
||||
|
||||
Our [strace-parser tool](https://gitlab.com/wchandler/strace-parser) can be used to
|
||||
|
|
|
@ -256,7 +256,39 @@ Implemented using Redis methods [PFADD](https://redis.io/commands/pfadd) and [PF
|
|||
keys for data storage. For `daily` we keep a key for metric per day of the year, for `weekly` we
|
||||
keep a key for metric per week of the year.
|
||||
|
||||
1. Track event using `Gitlab::UsageDataCounters::HLLRedisCounter.track_event(entity_id, event_name)`.
|
||||
1. Track event in controller using `RedisTracking` module with `track_redis_hll_event(*controller_actions, name:, feature:)`.
|
||||
|
||||
Arguments:
|
||||
|
||||
- `controller_actions`: controller actions we want to track.
|
||||
- `name`: event name.
|
||||
- `feature`: feature name, all metrics we track should be under feature flag.
|
||||
|
||||
Example usage:
|
||||
|
||||
```ruby
|
||||
# controller
|
||||
class ProjectsController < Projects::ApplicationController
|
||||
include RedisTracking
|
||||
|
||||
skip_before_action :authenticate_user!, only: :show
|
||||
track_redis_hll_event :index, :show, name: 'i_analytics_dev_ops_score', feature: :g_compliance_dashboard_feature
|
||||
|
||||
def index
|
||||
render html: 'index'
|
||||
end
|
||||
|
||||
def new
|
||||
render html: 'new'
|
||||
end
|
||||
|
||||
def show
|
||||
render html: 'show'
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
1. Track event using base module `Gitlab::UsageDataCounters::HLLRedisCounter.track_event(entity_id, event_name)`.
|
||||
|
||||
Arguments:
|
||||
|
||||
|
|
|
@ -61,6 +61,8 @@ group is public.
|
|||
|
||||
#### Eligible Approvers
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/10294) in [GitLab Starter](https://about.gitlab.com/pricing/) 13.3, when an eligible approver comments on a merge request, it appears in the **Commented by** column of the Approvals widget.
|
||||
|
||||
The following users can approve merge requests:
|
||||
|
||||
- Users who have been added as approvers at the project or merge request levels with
|
||||
|
@ -84,8 +86,7 @@ if [**Prevent author approval**](#allowing-merge-request-authors-to-approve-thei
|
|||
and [**Prevent committers approval**](#prevent-approval-of-merge-requests-by-their-committers) (disabled by default)
|
||||
are enabled on the project settings.
|
||||
|
||||
[Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/10294) in [GitLab Starter](https://about.gitlab.com/pricing/) 13.3,
|
||||
when an eligible approver comments on a merge request, it appears in the **Commented by** column of the Approvals widget,
|
||||
When an eligible approver comments on a merge request, it appears in the **Commented by** column of the Approvals widget,
|
||||
indicating who has engaged in the merge request review. Authors and reviewers can also easily identify who they should reach out
|
||||
to if they have any questions or inputs about the content of the merge request.
|
||||
|
||||
|
|
49
lib/gitlab/usage_data_counters/editor_unique_counter.rb
Normal file
49
lib/gitlab/usage_data_counters/editor_unique_counter.rb
Normal file
|
@ -0,0 +1,49 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module UsageDataCounters
|
||||
module EditorUniqueCounter
|
||||
EDIT_BY_SNIPPET_EDITOR = :edit_by_snippet_editor
|
||||
EDIT_BY_SFE = :edit_by_sfe
|
||||
EDIT_BY_WEB_IDE = :edit_by_web_ide
|
||||
|
||||
class << self
|
||||
def track_web_ide_edit_action(author:, time: Time.zone.now)
|
||||
track_unique_action(EDIT_BY_WEB_IDE, author, time)
|
||||
end
|
||||
|
||||
def count_web_ide_edit_actions(date_from:, date_to:)
|
||||
count_unique(EDIT_BY_WEB_IDE, date_from, date_to)
|
||||
end
|
||||
|
||||
def track_sfe_edit_action(author:, time: Time.zone.now)
|
||||
track_unique_action(EDIT_BY_SFE, author, time)
|
||||
end
|
||||
|
||||
def count_sfe_edit_actions(date_from:, date_to:)
|
||||
count_unique(EDIT_BY_SFE, date_from, date_to)
|
||||
end
|
||||
|
||||
def track_snippet_editor_edit_action(author:, time: Time.zone.now)
|
||||
track_unique_action(EDIT_BY_SNIPPET_EDITOR, author, time)
|
||||
end
|
||||
|
||||
def count_snippet_editor_edit_actions(date_from:, date_to:)
|
||||
count_unique(EDIT_BY_SNIPPET_EDITOR, date_from, date_to)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def track_unique_action(action, author, time)
|
||||
return unless Feature.enabled?(:track_editor_edit_actions)
|
||||
|
||||
Gitlab::UsageDataCounters::TrackUniqueActions.track_action(action: action, author_id: author.id, time: time)
|
||||
end
|
||||
|
||||
def count_unique(action, date_from, date_to)
|
||||
Gitlab::UsageDataCounters::TrackUniqueActions.count_unique(action: action, date_from: date_from, date_to: date_to)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
36
lib/gitlab/usage_data_counters/track_unique_actions.rb
Normal file
36
lib/gitlab/usage_data_counters/track_unique_actions.rb
Normal file
|
@ -0,0 +1,36 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module UsageDataCounters
|
||||
module TrackUniqueActions
|
||||
KEY_EXPIRY_LENGTH = 29.days
|
||||
|
||||
class << self
|
||||
def track_action(action:, author_id:, time: Time.zone.now)
|
||||
return unless Gitlab::CurrentSettings.usage_ping_enabled
|
||||
|
||||
target_key = key(action, time)
|
||||
|
||||
add_key(target_key, author_id)
|
||||
end
|
||||
|
||||
def count_unique(action:, date_from:, date_to:)
|
||||
keys = (date_from.to_date..date_to.to_date).map { |date| key(action, date) }
|
||||
|
||||
Gitlab::Redis::HLL.count(keys: keys)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def key(action, date)
|
||||
year_day = date.strftime('%G-%j')
|
||||
"#{year_day}-{#{action}}"
|
||||
end
|
||||
|
||||
def add_key(key, value)
|
||||
Gitlab::Redis::HLL.add(key: key, value: value, expiry: KEY_EXPIRY_LENGTH)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -3,8 +3,6 @@
|
|||
module Gitlab
|
||||
module UsageDataCounters
|
||||
module TrackUniqueEvents
|
||||
KEY_EXPIRY_LENGTH = 29.days
|
||||
|
||||
WIKI_ACTION = :wiki_action
|
||||
DESIGN_ACTION = :design_action
|
||||
PUSH_ACTION = :project_action
|
||||
|
@ -27,21 +25,17 @@ module Gitlab
|
|||
|
||||
class << self
|
||||
def track_event(event_action:, event_target:, author_id:, time: Time.zone.now)
|
||||
return unless Gitlab::CurrentSettings.usage_ping_enabled
|
||||
return unless valid_target?(event_target)
|
||||
return unless valid_action?(event_action)
|
||||
|
||||
transformed_target = transform_target(event_target)
|
||||
transformed_action = transform_action(event_action, transformed_target)
|
||||
target_key = key(transformed_action, time)
|
||||
|
||||
Gitlab::Redis::HLL.add(key: target_key, value: author_id, expiry: KEY_EXPIRY_LENGTH)
|
||||
Gitlab::UsageDataCounters::TrackUniqueActions.track_action(action: transformed_action, author_id: author_id, time: time)
|
||||
end
|
||||
|
||||
def count_unique_events(event_action:, date_from:, date_to:)
|
||||
keys = (date_from.to_date..date_to.to_date).map { |date| key(event_action, date) }
|
||||
|
||||
Gitlab::Redis::HLL.count(keys: keys)
|
||||
Gitlab::UsageDataCounters::TrackUniqueActions.count_unique(action: event_action, date_from: date_from, date_to: date_to)
|
||||
end
|
||||
|
||||
private
|
||||
|
@ -61,11 +55,6 @@ module Gitlab
|
|||
def valid_action?(action)
|
||||
Event.actions.key?(action)
|
||||
end
|
||||
|
||||
def key(event_action, date)
|
||||
year_day = date.strftime('%G-%j')
|
||||
"#{year_day}-{#{event_action}}"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
require 'airborne'
|
||||
|
||||
module QA
|
||||
RSpec.describe 'Manage with IP rate limits', :requires_admin, quarantine: { only: { subdomain: :staging }, issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/240936', type: :investigating } do
|
||||
RSpec.describe 'Manage with IP rate limits', :requires_admin, :skip_live_env do
|
||||
describe 'Users API' do
|
||||
let(:api_client) { Runtime::API::Client.new(:gitlab, ip_limits: true) }
|
||||
let(:request) { Runtime::API::Request.new(api_client, '/users') }
|
||||
|
|
|
@ -20,7 +20,7 @@ module QA
|
|||
end
|
||||
end
|
||||
|
||||
it 'shows results for the original request and AJAX requests' do
|
||||
it 'shows results for the original request and AJAX requests', status_issue: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/478' do
|
||||
# Issue pages always make AJAX requests
|
||||
Resource::Issue.fabricate_via_browser_ui! do |issue|
|
||||
issue.title = 'Performance bar test'
|
||||
|
|
98
spec/controllers/concerns/redis_tracking_spec.rb
Normal file
98
spec/controllers/concerns/redis_tracking_spec.rb
Normal file
|
@ -0,0 +1,98 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "spec_helper"
|
||||
|
||||
RSpec.describe RedisTracking do
|
||||
let(:event_name) { 'g_compliance_dashboard' }
|
||||
let(:feature) { 'g_compliance_dashboard_feature' }
|
||||
let(:user) { create(:user) }
|
||||
|
||||
controller(ApplicationController) do
|
||||
include RedisTracking
|
||||
|
||||
skip_before_action :authenticate_user!, only: :show
|
||||
track_redis_hll_event :index, :show, name: 'i_analytics_dev_ops_score', feature: :g_compliance_dashboard_feature
|
||||
|
||||
def index
|
||||
render html: 'index'
|
||||
end
|
||||
|
||||
def new
|
||||
render html: 'new'
|
||||
end
|
||||
|
||||
def show
|
||||
render html: 'show'
|
||||
end
|
||||
end
|
||||
|
||||
context 'with feature disabled' do
|
||||
it 'does not track the event' do
|
||||
stub_feature_flags(feature => false)
|
||||
|
||||
expect(Gitlab::UsageDataCounters::HLLRedisCounter).not_to receive(:track_event)
|
||||
|
||||
get :index
|
||||
end
|
||||
end
|
||||
|
||||
context 'with usage ping disabled' do
|
||||
it 'does not track the event' do
|
||||
stub_feature_flags(feature => true)
|
||||
allow(Gitlab::CurrentSettings).to receive(:usage_ping_enabled?).and_return(false)
|
||||
|
||||
expect(Gitlab::UsageDataCounters::HLLRedisCounter).not_to receive(:track_event)
|
||||
|
||||
get :index
|
||||
end
|
||||
end
|
||||
|
||||
context 'with feature enabled and usage ping enabled' do
|
||||
before do
|
||||
stub_feature_flags(feature => true)
|
||||
allow(Gitlab::CurrentSettings).to receive(:usage_ping_enabled?).and_return(true)
|
||||
end
|
||||
|
||||
context 'when user is logged in' do
|
||||
it 'tracks the event' do
|
||||
sign_in(user)
|
||||
|
||||
expect(Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:track_event)
|
||||
|
||||
get :index
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user is not logged in and there is a visitor_id' do
|
||||
let(:visitor_id) { SecureRandom.uuid }
|
||||
|
||||
before do
|
||||
routes.draw { get 'show' => 'anonymous#show' }
|
||||
end
|
||||
|
||||
it 'tracks the event' do
|
||||
cookies[:visitor_id] = { value: visitor_id, expires: 24.months }
|
||||
|
||||
expect(Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:track_event)
|
||||
|
||||
get :show
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user is not logged in and there is no visitor_id' do
|
||||
it 'does not tracks the event' do
|
||||
expect(Gitlab::UsageDataCounters::HLLRedisCounter).not_to receive(:track_event)
|
||||
|
||||
get :index
|
||||
end
|
||||
end
|
||||
|
||||
context 'for untracked action' do
|
||||
it 'does not tracks the event' do
|
||||
expect(Gitlab::UsageDataCounters::HLLRedisCounter).not_to receive(:track_event)
|
||||
|
||||
get :new
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
2
spec/fixtures/whats_new/01.yml
vendored
Normal file
2
spec/fixtures/whats_new/01.yml
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
---
|
||||
- title: It's gonna be a bright
|
2
spec/fixtures/whats_new/02.yml
vendored
Normal file
2
spec/fixtures/whats_new/02.yml
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
---
|
||||
- title: bright
|
2
spec/fixtures/whats_new/05.yml
vendored
Normal file
2
spec/fixtures/whats_new/05.yml
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
---
|
||||
- title: bright and sunshinin' day
|
|
@ -200,8 +200,8 @@ describe('Snippet Edit app', () => {
|
|||
|
||||
it.each`
|
||||
projectPath | snippetArg | expectation
|
||||
${''} | ${[]} | ${`${relativeUrlRoot}/-/snippets`}
|
||||
${'project/path'} | ${[]} | ${`${relativeUrlRoot}project/path/-/snippets`}
|
||||
${''} | ${[]} | ${urlUtils.joinPaths('/', relativeUrlRoot, '-', 'snippets')}
|
||||
${'project/path'} | ${[]} | ${urlUtils.joinPaths('/', relativeUrlRoot, 'project/path/-', 'snippets')}
|
||||
${''} | ${[createTestSnippet()]} | ${TEST_WEB_URL}
|
||||
${'project/path'} | ${[createTestSnippet()]} | ${TEST_WEB_URL}
|
||||
`(
|
||||
|
|
|
@ -11,8 +11,9 @@ describe('App', () => {
|
|||
let store;
|
||||
let actions;
|
||||
let state;
|
||||
let propsData = { features: '[ {"title":"Whats New Drawer"} ]' };
|
||||
|
||||
beforeEach(() => {
|
||||
const buildWrapper = () => {
|
||||
actions = {
|
||||
closeDrawer: jest.fn(),
|
||||
};
|
||||
|
@ -29,7 +30,12 @@ describe('App', () => {
|
|||
wrapper = mount(App, {
|
||||
localVue,
|
||||
store,
|
||||
propsData,
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
buildWrapper();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
@ -54,4 +60,15 @@ describe('App', () => {
|
|||
|
||||
expect(getDrawer().props('open')).toBe(openState);
|
||||
});
|
||||
|
||||
it('renders features when provided as props', () => {
|
||||
expect(wrapper.find('h5').text()).toBe('Whats New Drawer');
|
||||
});
|
||||
|
||||
it('handles bad json argument gracefully', () => {
|
||||
propsData = { features: 'this is not json' };
|
||||
buildWrapper();
|
||||
|
||||
expect(getDrawer().exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
|
22
spec/helpers/whats_new_helper_spec.rb
Normal file
22
spec/helpers/whats_new_helper_spec.rb
Normal file
|
@ -0,0 +1,22 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe WhatsNewHelper do
|
||||
describe '#whats_new_most_recent_release_items' do
|
||||
let(:fixture_dir_glob) { Dir.glob(File.join('spec', 'fixtures', 'whats_new', '*.yml')) }
|
||||
|
||||
it 'returns json from the most recent file' do
|
||||
allow(Dir).to receive(:glob).with(Rails.root.join('data', 'whats_new', '*.yml')).and_return(fixture_dir_glob)
|
||||
|
||||
expect(helper.whats_new_most_recent_release_items).to include({ title: "bright and sunshinin' day" }.to_json)
|
||||
end
|
||||
|
||||
it 'fails gracefully and logs an error' do
|
||||
allow(YAML).to receive(:load_file).and_raise
|
||||
|
||||
expect(Gitlab::ErrorTracking).to receive(:track_exception)
|
||||
expect(helper.whats_new_most_recent_release_items).to eq(''.to_json)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,70 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::UsageDataCounters::EditorUniqueCounter, :clean_gitlab_redis_shared_state do
|
||||
shared_examples 'tracks and counts action' do
|
||||
let(:user1) { build(:user, id: 1) }
|
||||
let(:user2) { build(:user, id: 2) }
|
||||
let(:user3) { build(:user, id: 3) }
|
||||
let(:time) { Time.zone.now }
|
||||
|
||||
specify do
|
||||
stub_application_setting(usage_ping_enabled: true)
|
||||
|
||||
aggregate_failures do
|
||||
expect(track_action(author: user1)).to be_truthy
|
||||
expect(track_action(author: user1)).to be_truthy
|
||||
expect(track_action(author: user2)).to be_truthy
|
||||
expect(track_action(author: user3, time: time - 3.days)).to be_truthy
|
||||
|
||||
expect(count_unique(date_from: time, date_to: Date.today)).to eq(2)
|
||||
expect(count_unique(date_from: time - 5.days, date_to: Date.tomorrow)).to eq(3)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when feature flag track_editor_edit_actions is disabled' do
|
||||
it 'does not track edit actions' do
|
||||
stub_feature_flags(track_editor_edit_actions: false)
|
||||
|
||||
expect(track_action(author: user1)).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'for web IDE edit actions' do
|
||||
it_behaves_like 'tracks and counts action' do
|
||||
def track_action(params)
|
||||
described_class.track_web_ide_edit_action(params)
|
||||
end
|
||||
|
||||
def count_unique(params)
|
||||
described_class.count_web_ide_edit_actions(params)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'for SFE edit actions' do
|
||||
it_behaves_like 'tracks and counts action' do
|
||||
def track_action(params)
|
||||
described_class.track_sfe_edit_action(params)
|
||||
end
|
||||
|
||||
def count_unique(params)
|
||||
described_class.count_sfe_edit_actions(params)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'for snippet editor edit actions' do
|
||||
it_behaves_like 'tracks and counts action' do
|
||||
def track_action(params)
|
||||
described_class.track_snippet_editor_edit_action(params)
|
||||
end
|
||||
|
||||
def count_unique(params)
|
||||
described_class.count_snippet_editor_edit_actions(params)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,43 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::UsageDataCounters::TrackUniqueActions, :clean_gitlab_redis_shared_state do
|
||||
let(:time) { Time.zone.now }
|
||||
let(:action) { 'example_action' }
|
||||
|
||||
def track_action(params)
|
||||
described_class.track_action(params)
|
||||
end
|
||||
|
||||
def count_unique(params)
|
||||
described_class.count_unique(params)
|
||||
end
|
||||
|
||||
context 'tracking an event' do
|
||||
context 'when tracking successfully' do
|
||||
it 'tracks and counts the events as expected' do
|
||||
stub_application_setting(usage_ping_enabled: true)
|
||||
|
||||
aggregate_failures do
|
||||
expect(track_action(action: action, author_id: 1)).to be_truthy
|
||||
expect(track_action(action: action, author_id: 1)).to be_truthy
|
||||
expect(track_action(action: action, author_id: 2)).to be_truthy
|
||||
expect(track_action(action: action, author_id: 3, time: time - 3.days)).to be_truthy
|
||||
|
||||
expect(count_unique(action: action, date_from: time, date_to: Date.today)).to eq(2)
|
||||
expect(count_unique(action: action, date_from: time - 5.days, date_to: Date.tomorrow)).to eq(3)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when tracking unsuccessfully' do
|
||||
it 'does not track the event' do
|
||||
stub_application_setting(usage_ping_enabled: false)
|
||||
|
||||
expect(track_action(action: action, author_id: 2)).to be_nil
|
||||
expect(count_unique(action: action, date_from: time, date_to: Date.today)).to eq(0)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -6,7 +6,7 @@ RSpec.describe RemoteMirrorNotificationWorker, :mailer do
|
|||
let_it_be(:project) { create(:project, :repository, :remote_mirror) }
|
||||
let_it_be(:mirror) { project.remote_mirrors.first }
|
||||
|
||||
describe '#execute' do
|
||||
describe '#perform' do
|
||||
it 'calls NotificationService#remote_mirror_update_failed when the mirror exists' do
|
||||
mirror.update_column(:last_error, "There was a problem fetching")
|
||||
|
||||
|
|
Loading…
Reference in a new issue