diff --git a/.gitlab/ci/rules.gitlab-ci.yml b/.gitlab/ci/rules.gitlab-ci.yml index b2836d2600e..fbadec1f63d 100644 --- a/.gitlab/ci/rules.gitlab-ci.yml +++ b/.gitlab/ci/rules.gitlab-ci.yml @@ -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 diff --git a/Gemfile b/Gemfile index 05f7801d0f7..1bf60a4098d 100644 --- a/Gemfile +++ b/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' diff --git a/Gemfile.lock b/Gemfile.lock index 13165a1e8b2..d36f9b70ca8 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -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) diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js index 53598165384..52f6786ca28 100644 --- a/app/assets/javascripts/milestone_select.js +++ b/app/assets/javascripts/milestone_select.js @@ -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! diff --git a/app/assets/javascripts/snippets/components/edit.vue b/app/assets/javascripts/snippets/components/edit.vue index 5bca486b92e..227d9043d7f 100644 --- a/app/assets/javascripts/snippets/components/edit.vue +++ b/app/assets/javascripts/snippets/components/edit.vue @@ -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; }, diff --git a/app/assets/javascripts/snippets/components/snippet_header.vue b/app/assets/javascripts/snippets/components/snippet_header.vue index 05a09347d06..0ca69f3161a 100644 --- a/app/assets/javascripts/snippets/components/snippet_header.vue +++ b/app/assets/javascripts/snippets/components/snippet_header.vue @@ -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', diff --git a/app/assets/javascripts/whats_new/components/app.vue b/app/assets/javascripts/whats_new/components/app.vue index d974556cb9e..9a06383a57b 100644 --- a/app/assets/javascripts/whats_new/components/app.vue +++ b/app/assets/javascripts/whats_new/components/app.vue @@ -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 {

{{ __("What's new at GitLab") }}

diff --git a/app/assets/javascripts/whats_new/index.js b/app/assets/javascripts/whats_new/index.js index c9ee3404d2a..a6519ab63b1 100644 --- a/app/assets/javascripts/whats_new/index.js +++ b/app/assets/javascripts/whats_new/index.js @@ -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'), + }, + }); }, }); diff --git a/app/controllers/concerns/redis_tracking.rb b/app/controllers/concerns/redis_tracking.rb new file mode 100644 index 00000000000..0d8b3e12ebb --- /dev/null +++ b/app/controllers/concerns/redis_tracking.rb @@ -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 diff --git a/app/helpers/whats_new_helper.rb b/app/helpers/whats_new_helper.rb new file mode 100644 index 00000000000..f0044daa645 --- /dev/null +++ b/app/helpers/whats_new_helper.rb @@ -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 diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index 56b70c463d0..8a05768109b 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -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 : '' } } diff --git a/app/views/shared/boards/components/sidebar/_milestone.html.haml b/app/views/shared/boards/components/sidebar/_milestone.html.haml index 23d63fde671..2c894e9b1b3 100644 --- a/app/views/shared/boards/components/sidebar/_milestone.html.haml +++ b/app/views/shared/boards/components/sidebar/_milestone.html.haml @@ -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" } diff --git a/app/views/shared/issuable/_bulk_update_sidebar.html.haml b/app/views/shared/issuable/_bulk_update_sidebar.html.haml index 0c15d20bfe0..09abe9e89c4 100644 --- a/app/views/shared/issuable/_bulk_update_sidebar.html.haml +++ b/app/views/shared/issuable/_bulk_update_sidebar.html.haml @@ -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') diff --git a/app/views/shared/issuable/_milestone_dropdown.html.haml b/app/views/shared/issuable/_milestone_dropdown.html.haml index c2da363b8c6..f58156b7c08 100644 --- a/app/views/shared/issuable/_milestone_dropdown.html.haml +++ b/app/views/shared/issuable/_milestone_dropdown.html.haml @@ -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 diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index 987e875674d..c2fe16be257 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -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 } diff --git a/changelogs/unreleased/232824-fj-track-unique-virtual-actions.yml b/changelogs/unreleased/232824-fj-track-unique-virtual-actions.yml new file mode 100644 index 00000000000..d7ee320a3ba --- /dev/null +++ b/changelogs/unreleased/232824-fj-track-unique-virtual-actions.yml @@ -0,0 +1,5 @@ +--- +title: Add virtual actions tracker for Usage Ping +merge_request: 39694 +author: +type: added diff --git a/config/feature_flags/development/track_editor_edit_actions.yml b/config/feature_flags/development/track_editor_edit_actions.yml new file mode 100644 index 00000000000..662dd9f600b --- /dev/null +++ b/config/feature_flags/development/track_editor_edit_actions.yml @@ -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 diff --git a/config/spring.rb b/config/spring.rb index c9119b40c08..0092d0fd1b0 100644 --- a/config/spring.rb +++ b/config/spring.rb @@ -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 diff --git a/db/post_migrate/20200806100713_schedule_populate_resolved_on_default_branch_column.rb b/db/post_migrate/20200806100713_schedule_populate_resolved_on_default_branch_column.rb index 818d4e5704b..396b95257e8 100644 --- a/db/post_migrate/20200806100713_schedule_populate_resolved_on_default_branch_column.rb +++ b/db/post_migrate/20200806100713_schedule_populate_resolved_on_default_branch_column.rb @@ -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 diff --git a/doc/administration/troubleshooting/linux_cheat_sheet.md b/doc/administration/troubleshooting/linux_cheat_sheet.md index 06c49d67f40..f24234e1aff 100644 --- a/doc/administration/troubleshooting/linux_cheat_sheet.md +++ b/doc/administration/troubleshooting/linux_cheat_sheet.md @@ -179,12 +179,14 @@ strace -tt -T -f -y -yy -s 1024 -p 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 diff --git a/doc/development/telemetry/usage_ping.md b/doc/development/telemetry/usage_ping.md index 383fb4a126b..7a3a501fa94 100644 --- a/doc/development/telemetry/usage_ping.md +++ b/doc/development/telemetry/usage_ping.md @@ -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: diff --git a/doc/user/project/merge_requests/merge_request_approvals.md b/doc/user/project/merge_requests/merge_request_approvals.md index 407fc5db425..cbfa87651f2 100644 --- a/doc/user/project/merge_requests/merge_request_approvals.md +++ b/doc/user/project/merge_requests/merge_request_approvals.md @@ -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. diff --git a/lib/gitlab/usage_data_counters/editor_unique_counter.rb b/lib/gitlab/usage_data_counters/editor_unique_counter.rb new file mode 100644 index 00000000000..251c83d3eed --- /dev/null +++ b/lib/gitlab/usage_data_counters/editor_unique_counter.rb @@ -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 diff --git a/lib/gitlab/usage_data_counters/track_unique_actions.rb b/lib/gitlab/usage_data_counters/track_unique_actions.rb new file mode 100644 index 00000000000..97e85bef9a5 --- /dev/null +++ b/lib/gitlab/usage_data_counters/track_unique_actions.rb @@ -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 diff --git a/lib/gitlab/usage_data_counters/track_unique_events.rb b/lib/gitlab/usage_data_counters/track_unique_events.rb index db18200f059..f2a217e980e 100644 --- a/lib/gitlab/usage_data_counters/track_unique_events.rb +++ b/lib/gitlab/usage_data_counters/track_unique_events.rb @@ -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 diff --git a/qa/qa/specs/features/api/1_manage/rate_limits_spec.rb b/qa/qa/specs/features/api/1_manage/rate_limits_spec.rb index f253923619b..49d670433ca 100644 --- a/qa/qa/specs/features/api/1_manage/rate_limits_spec.rb +++ b/qa/qa/specs/features/api/1_manage/rate_limits_spec.rb @@ -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') } diff --git a/qa/qa/specs/features/browser_ui/non_devops/performance_bar_spec.rb b/qa/qa/specs/features/browser_ui/non_devops/performance_bar_spec.rb index 73bb6aeb5fd..06aa59d3573 100644 --- a/qa/qa/specs/features/browser_ui/non_devops/performance_bar_spec.rb +++ b/qa/qa/specs/features/browser_ui/non_devops/performance_bar_spec.rb @@ -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' diff --git a/spec/controllers/concerns/redis_tracking_spec.rb b/spec/controllers/concerns/redis_tracking_spec.rb new file mode 100644 index 00000000000..09ea9bd726f --- /dev/null +++ b/spec/controllers/concerns/redis_tracking_spec.rb @@ -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 diff --git a/spec/fixtures/whats_new/01.yml b/spec/fixtures/whats_new/01.yml new file mode 100644 index 00000000000..06db95be44f --- /dev/null +++ b/spec/fixtures/whats_new/01.yml @@ -0,0 +1,2 @@ +--- +- title: It's gonna be a bright diff --git a/spec/fixtures/whats_new/02.yml b/spec/fixtures/whats_new/02.yml new file mode 100644 index 00000000000..91b0bd7036e --- /dev/null +++ b/spec/fixtures/whats_new/02.yml @@ -0,0 +1,2 @@ +--- +- title: bright diff --git a/spec/fixtures/whats_new/05.yml b/spec/fixtures/whats_new/05.yml new file mode 100644 index 00000000000..5b8939a2bc6 --- /dev/null +++ b/spec/fixtures/whats_new/05.yml @@ -0,0 +1,2 @@ +--- +- title: bright and sunshinin' day diff --git a/spec/frontend/snippets/components/edit_spec.js b/spec/frontend/snippets/components/edit_spec.js index 648285bd2f1..dff9bf088c0 100644 --- a/spec/frontend/snippets/components/edit_spec.js +++ b/spec/frontend/snippets/components/edit_spec.js @@ -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} `( diff --git a/spec/frontend/whats_new/components/app_spec.js b/spec/frontend/whats_new/components/app_spec.js index a349aad9f1c..3b989f3c297 100644 --- a/spec/frontend/whats_new/components/app_spec.js +++ b/spec/frontend/whats_new/components/app_spec.js @@ -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); + }); }); diff --git a/spec/helpers/whats_new_helper_spec.rb b/spec/helpers/whats_new_helper_spec.rb new file mode 100644 index 00000000000..db880163454 --- /dev/null +++ b/spec/helpers/whats_new_helper_spec.rb @@ -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 diff --git a/spec/lib/gitlab/usage_data_counters/editor_unique_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/editor_unique_counter_spec.rb new file mode 100644 index 00000000000..ef2435b9cb8 --- /dev/null +++ b/spec/lib/gitlab/usage_data_counters/editor_unique_counter_spec.rb @@ -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 diff --git a/spec/lib/gitlab/usage_data_counters/track_unique_actions_spec.rb b/spec/lib/gitlab/usage_data_counters/track_unique_actions_spec.rb new file mode 100644 index 00000000000..d86b8a23dc7 --- /dev/null +++ b/spec/lib/gitlab/usage_data_counters/track_unique_actions_spec.rb @@ -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 diff --git a/spec/workers/remote_mirror_notification_worker_spec.rb b/spec/workers/remote_mirror_notification_worker_spec.rb index c6fd614fdea..e415e72645c 100644 --- a/spec/workers/remote_mirror_notification_worker_spec.rb +++ b/spec/workers/remote_mirror_notification_worker_spec.rb @@ -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")