diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-11-16 15:07:22 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-11-16 15:07:22 +0300 |
commit | 77ded523f119396c72e4bcbcd008ff6b84134ef4 (patch) | |
tree | f9fc698ab476ab8b0e1d0b879ebc48560583fce3 | |
parent | 34a59635a9f25499193aa71a2cae3581f9892cbb (diff) |
Add latest changes from gitlab-org/gitlab@master
32 files changed, 605 insertions, 571 deletions
diff --git a/.gitlab/ci/review-apps/main.gitlab-ci.yml b/.gitlab/ci/review-apps/main.gitlab-ci.yml index dfcd65238ec..a1378629270 100644 --- a/.gitlab/ci/review-apps/main.gitlab-ci.yml +++ b/.gitlab/ci/review-apps/main.gitlab-ci.yml @@ -150,7 +150,7 @@ review-deploy-sample-projects: .review-stop-base: extends: .review-workflow-base - timeout: 15min + timeout: 30min environment: action: stop variables: diff --git a/app/assets/javascripts/diffs/components/merge_conflict_warning.vue b/app/assets/javascripts/diffs/components/merge_conflict_warning.vue deleted file mode 100644 index 6cb1ed4cbcf..00000000000 --- a/app/assets/javascripts/diffs/components/merge_conflict_warning.vue +++ /dev/null @@ -1,62 +0,0 @@ -<script> -import { GlButton, GlAlert, GlModalDirective } from '@gitlab/ui'; - -export default { - components: { - GlAlert, - GlButton, - }, - directives: { - GlModalDirective, - }, - props: { - mergeable: { - type: Boolean, - required: true, - }, - resolutionPath: { - type: String, - required: true, - }, - }, -}; -</script> - -<template> - <div> - <gl-alert - :dismissible="false" - :title="__('There are merge conflicts')" - variant="warning" - class="gl-mb-5" - > - <p class="gl-mb-2"> - {{ __('The comparison view may be inaccurate due to merge conflicts.') }} - </p> - <p class="gl-mb-0"> - {{ - __( - 'Resolve these conflicts, or ask someone with write access to this repository to resolve them locally.', - ) - }} - </p> - <template #actions> - <gl-button - v-if="resolutionPath" - :href="resolutionPath" - variant="confirm" - class="gl-mr-3 gl-alert-action" - > - {{ __('Resolve conflicts') }} - </gl-button> - <gl-button - v-if="mergeable" - v-gl-modal-directive="'modal-merge-info'" - class="gl-alert-action" - > - {{ __('Resolve locally') }} - </gl-button> - </template> - </gl-alert> - </div> -</template> diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index 29189e3ac2f..b41f306eabd 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -28,7 +28,6 @@ import initLogoAnimation from './logo'; import initBreadcrumbs from './breadcrumb'; import initPersistentUserCallouts from './persistent_user_callouts'; import { initUserTracking, initDefaultTrackers } from './tracking'; -import { initSidebarTracking } from './pages/shared/nav/sidebar_tracking'; import GlFieldErrors from './gl_field_errors'; import initUserPopovers from './user_popovers'; import initBroadcastNotifications from './broadcast_notification'; @@ -96,7 +95,6 @@ function deferredInitialisation() { initBroadcastNotifications(); initPersistentUserCallouts(); initDefaultTrackers(); - initSidebarTracking(); initFeatureHighlight(); initCopyCodeButton(); initGitlabVersionCheck(); diff --git a/app/assets/javascripts/pages/shared/nav/sidebar_tracking.js b/app/assets/javascripts/pages/shared/nav/sidebar_tracking.js deleted file mode 100644 index 47aae36ecbb..00000000000 --- a/app/assets/javascripts/pages/shared/nav/sidebar_tracking.js +++ /dev/null @@ -1,44 +0,0 @@ -function onSidebarLinkClick() { - const setDataTrackAction = (element, action) => { - element.dataset.trackAction = action; - }; - - const setDataTrackExtra = (element, value) => { - const SIDEBAR_COLLAPSED = 'Collapsed'; - const SIDEBAR_EXPANDED = 'Expanded'; - const sidebarCollapsed = document - .querySelector('.nav-sidebar') - .classList.contains('js-sidebar-collapsed') - ? SIDEBAR_COLLAPSED - : SIDEBAR_EXPANDED; - - element.dataset.trackExtra = JSON.stringify({ - sidebar_display: sidebarCollapsed, - menu_display: value, - }); - }; - - const EXPANDED = 'Expanded'; - const FLY_OUT = 'Fly out'; - const CLICK_MENU_ACTION = 'click_menu'; - const CLICK_MENU_ITEM_ACTION = 'click_menu_item'; - const parentElement = this.parentNode; - const subMenuList = parentElement.closest('.sidebar-sub-level-items'); - - if (subMenuList) { - const isFlyOut = subMenuList.classList.contains('fly-out-list') ? FLY_OUT : EXPANDED; - - setDataTrackExtra(parentElement, isFlyOut); - setDataTrackAction(parentElement, CLICK_MENU_ITEM_ACTION); - } else { - const isFlyOut = parentElement.classList.contains('is-showing-fly-out') ? FLY_OUT : EXPANDED; - - setDataTrackExtra(parentElement, isFlyOut); - setDataTrackAction(parentElement, CLICK_MENU_ACTION); - } -} -export const initSidebarTracking = () => { - document.querySelectorAll('.nav-sidebar li[data-track-label] > a').forEach((link) => { - link.addEventListener('click', onSidebarLinkClick); - }); -}; diff --git a/app/finders/projects_finder.rb b/app/finders/projects_finder.rb index 1aa5245590e..2a781c037f6 100644 --- a/app/finders/projects_finder.rb +++ b/app/finders/projects_finder.rb @@ -29,6 +29,7 @@ # repository_storage: string # not_aimed_for_deletion: boolean # full_paths: string[] +# organization_id: int # class ProjectsFinder < UnionFinder include CustomAttributesFilter @@ -95,6 +96,7 @@ class ProjectsFinder < UnionFinder collection = by_language(collection) collection = by_feature_availability(collection) collection = by_updated_at(collection) + collection = by_organization_id(collection) by_repository_storage(collection) end @@ -293,6 +295,10 @@ class ProjectsFinder < UnionFinder items end + def by_organization_id(items) + params[:organization_id].present? ? items.in_organization(params[:organization_id]) : items + end + def finder_params return {} unless min_access_level? diff --git a/app/models/event.rb b/app/models/event.rb index 7de7ad8ccd6..3ff5a46f2d9 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -89,10 +89,26 @@ class Event < ApplicationRecord scope :for_wiki_page, -> { where(target_type: 'WikiPage::Meta') } scope :for_design, -> { where(target_type: 'DesignManagement::Design') } scope :for_issue, -> { where(target_type: ISSUE_TYPES) } + scope :for_merge_request, -> { where(target_type: 'MergeRequest') } scope :for_fingerprint, ->(fingerprint) do fingerprint.present? ? where(fingerprint: fingerprint) : none end scope :for_action, ->(action) { where(action: action) } + scope :created_between, ->(start_time, end_time) { where(created_at: start_time..end_time) } + scope :count_by_dates, ->(date_interval) { group("DATE(created_at + #{date_interval})").count } + + scope :contributions, -> do + contribution_actions = [actions[:pushed], actions[:commented]] + + contributable_target_types = %w[MergeRequest Issue WorkItem] + target_contribution_actions = [actions[:created], actions[:closed], actions[:merged], actions[:approved]] + + where( + 'action IN (?) OR (target_type IN (?) AND action IN (?))', + contribution_actions, + contributable_target_types, target_contribution_actions + ) + end scope :with_associations, -> do # We're using preload for "push_event_payload" as otherwise the association @@ -133,15 +149,6 @@ class Event < ApplicationRecord end end - # Update Gitlab::ContributionsCalendar#activity_dates if this changes - def contributions - where( - 'action IN (?) OR (target_type IN (?) AND action IN (?))', - [actions[:pushed], actions[:commented]], - %w[MergeRequest Issue WorkItem], [actions[:created], actions[:closed], actions[:merged]] - ) - end - def limit_recent(limit = 20, offset = nil) recent.limit(limit).offset(offset) end diff --git a/app/models/project.rb b/app/models/project.rb index 4aec4207ece..37e86df9b7c 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -781,6 +781,8 @@ class Project < ApplicationRecord .order(id: :desc) end + scope :in_organization, -> (organization) { where(organization: organization) } + enum auto_cancel_pending_pipelines: { disabled: 0, enabled: 1 } chronic_duration_attr :build_timeout_human_readable, :build_timeout, diff --git a/app/models/user.rb b/app/models/user.rb index fbdaa110435..109b0506648 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1767,7 +1767,7 @@ class User < MainClusterwide::ApplicationRecord def contributed_projects events = Event.select(:project_id) .contributions.where(author_id: self) - .where("created_at > ?", Time.current - 1.year) + .created_after(Time.current - 1.year) .distinct .reorder(nil) diff --git a/config/feature_flags/development/contributions_calendar_refactoring.yml b/config/feature_flags/development/contributions_calendar_refactoring.yml new file mode 100644 index 00000000000..e779939cc28 --- /dev/null +++ b/config/feature_flags/development/contributions_calendar_refactoring.yml @@ -0,0 +1,8 @@ +--- +name: contributions_calendar_refactoring +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/134991 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/429648 +milestone: '16.6' +type: development +group: group::tenant scale +default_enabled: false diff --git a/doc/ci/services/index.md b/doc/ci/services/index.md index d0481482d00..42528effb46 100644 --- a/doc/ci/services/index.md +++ b/doc/ci/services/index.md @@ -445,7 +445,7 @@ First start with creating a file named `build_script`: cat <<EOF > build_script git clone https://gitlab.com/gitlab-org/gitlab-runner.git /builds/gitlab-org/gitlab-runner cd /builds/gitlab-org/gitlab-runner -make +make runner-bin-host EOF ``` @@ -456,8 +456,7 @@ Instead of `make`, you could run the command which is specific to your project. Then create some service containers: ```shell -docker run -d --name service-mysql mysql:latest -docker run -d --name service-postgres postgres:latest +docker run -d --name service-redis redis:latest ``` The previous commands create two service containers. The service container named `service-mysql` uses the latest MySQL image. The one named `service-postgres` uses the latest PostgreSQL image. Both service containers run in the background (`-d`). @@ -466,7 +465,7 @@ Finally, create a build container by executing the `build_script` file we created earlier: ```shell -docker run --name build -i --link=service-mysql:mysql --link=service-postgres:postgres ruby:2.6 /bin/bash < build_script +docker run --name build -i --link=service-redis:redis golang:latest /bin/bash < build_script ``` The above command creates a container named `build` that's spawned from diff --git a/doc/development/internal_analytics/internal_event_instrumentation/quick_start.md b/doc/development/internal_analytics/internal_event_instrumentation/quick_start.md index 79323263b07..fc146ab884d 100644 --- a/doc/development/internal_analytics/internal_event_instrumentation/quick_start.md +++ b/doc/development/internal_analytics/internal_event_instrumentation/quick_start.md @@ -63,6 +63,12 @@ Gitlab::InternalEvents.track_event( This method automatically increments all RedisHLL metrics relating to the event `i_code_review_user_apply_suggestion`, and sends a corresponding Snowplow event with all named arguments and standard context (SaaS only). +If you have defined a metric with a `unique` property such as `unique: project.id` it is required that you provide the `project` argument. + +It is encouraged to fill out as many of `user`, `namespace` and `project` as possible as it increases the data quality and make it easier to define metrics in the future. + +If a `project` but no `namespace` is provided, the `project.namespace` is used as the `namespace` for the event. + ### Frontend tracking #### Vue components diff --git a/doc/subscriptions/self_managed/index.md b/doc/subscriptions/self_managed/index.md index 745e1f63e85..563cafd0c29 100644 --- a/doc/subscriptions/self_managed/index.md +++ b/doc/subscriptions/self_managed/index.md @@ -336,13 +336,13 @@ period is prorated from the date of purchase through the end of the subscription To add seats to a subscription: -1. Log in to the [Customers Portal](https://customers.gitlab.com/). -1. Navigate to the **Manage Purchases** page. +1. Sign in to the [Customers Portal](https://customers.gitlab.com/). +1. Go to the **Manage Purchases** page. 1. Select **Add more seats** on the relevant subscription card. 1. Enter the number of additional users. -1. Select **Proceed to checkout**. -1. Review the **Subscription Upgrade Detail**. The system lists the total price for all users on the system and a credit for what you've already paid. You are only be charged for the net change. -1. Select **Confirm Upgrade**. +1. Review the **Purchase summary** section. The system lists the total price for all users on the system and a credit for what you've already paid. You are only charged for the net change. +1. Enter your payment information. +1. Select **Purchase seats**. A payment receipt is emailed to you, which you can also access in the Customers Portal under [**View invoices**](https://customers.gitlab.com/receipts). @@ -359,9 +359,7 @@ You should follow these steps during renewal: 1. Prior to the renewal date, prune any inactive or unwanted users by [blocking them](../../administration/moderate_users.md#block-a-user). 1. Determine if you have a need for user growth in the upcoming subscription. -1. Log in to the [Customers Portal](https://customers.gitlab.com/customers/sign_in) and select the **Renew** button beneath your existing subscription. -The **Renew** button remains disabled (grayed-out) until 15 days before a subscription expires. -You can hover your mouse on the **Renew** button to see the date when it will become active. +1. Sign in to the [Customers Portal](https://customers.gitlab.com/customers/sign_in) and beneath your existing subscription, select **Renew**. The **Renew** button displays only 15 days before a subscription expires. If there are more than 15 days before the subscription expires, select **Subscription actions** (**{ellipsis_v}**), then select **Renew subscription** to view the date when you can renew. NOTE: If you need to change your [GitLab tier](https://about.gitlab.com/pricing/), contact our sales team with [the sales contact form](https://about.gitlab.com/sales/) for assistance as this can't be done in the Customers Portal. @@ -396,10 +394,8 @@ You can view and download your renewal invoice on the Customers Portal [View inv To view or change automatic subscription renewal (at the same tier as the previous period), sign in to the [Customers Portal](https://customers.gitlab.com/customers/sign_in), and: -- If a **Turn on auto-renew** button is displayed, your subscription was canceled - previously. Select it to resume automatic renewal. -- If a **Cancel subscription** button is displayed, your subscription is set to automatically - renew at the end of the subscription period. Select it to cancel automatic renewal. +- If the subscription card displays `Expires on DATE`, your subscription is not set to automatically renew. To enable automatic renewal, in **Subscription actions** (**{ellipsis_v}**), select **Turn on auto-renew**. +- If the subscription card displays `Autorenews on DATE`, your subscription is set to automatically renew at the end of the subscription period. To cancel automatic renewal, in **Subscription actions** (**{ellipsis_v}**), select **Cancel subscription**. If you have difficulty during the renewal process, contact the [Support team](https://support.gitlab.com/hc/en-us/requests/new?ticket_form_id=360000071293) for assistance. @@ -416,9 +412,8 @@ There are several options to renew a subscription for fewer seats, as long as th To upgrade your [GitLab tier](https://about.gitlab.com/pricing/): -1. Log in to the [Customers Portal](https://customers.gitlab.com/customers/sign_in). -1. Select the **Upgrade** button on the relevant subscription card on the - [Manage purchases](https://customers.gitlab.com/subscriptions) page. +1. Sign in to the [Customers Portal](https://customers.gitlab.com/customers/sign_in). +1. Select **Upgrade** on the relevant subscription card. 1. Select the desired upgrade. 1. Confirm the active form of payment, or add a new form of payment. 1. Select the **I accept the Privacy Policy and Terms of Service** checkbox. diff --git a/lib/api/ml/mlflow/model_versions.rb b/lib/api/ml/mlflow/model_versions.rb index 989b79e5774..04ada5204fd 100644 --- a/lib/api/ml/mlflow/model_versions.rb +++ b/lib/api/ml/mlflow/model_versions.rb @@ -6,7 +6,7 @@ module API class ModelVersions < ::API::Base feature_category :mlops - resource :model_versions do + resource 'model-versions' do desc 'Fetch model version by name and version' do success Entities::Ml::Mlflow::ModelVersions::Responses::Get detail 'https://mlflow.org/docs/2.6.0/rest-api.html#get-modelversion' diff --git a/lib/gitlab/contributions_calendar.rb b/lib/gitlab/contributions_calendar.rb index 2068a9ae7d5..c91fdd59c57 100644 --- a/lib/gitlab/contributions_calendar.rb +++ b/lib/gitlab/contributions_calendar.rb @@ -3,6 +3,7 @@ module Gitlab class ContributionsCalendar include TimeZoneHelper + include ::Gitlab::Utils::StrongMemoize attr_reader :contributor attr_reader :current_user @@ -16,18 +17,23 @@ module Gitlab .execute(current_user, ignore_visibility: @contributor.include_private_contributions?) end - # rubocop: disable CodeReuse/ActiveRecord def activity_dates - return {} if @projects.empty? - return @activity_dates if @activity_dates.present? + return {} if projects.empty? start_time = @contributor_time_instance.years_ago(1).beginning_of_day end_time = @contributor_time_instance.end_of_day date_interval = "INTERVAL '#{@contributor_time_instance.utc_offset} seconds'" + if Feature.enabled?(:contributions_calendar_refactoring) + return contributions_between(start_time, end_time).count_by_dates(date_interval) + end + + # TODO: Remove after feature flag `contributions_calendar_refactoring` is rolled out + # See https://gitlab.com/gitlab-org/gitlab/-/issues/429648 + # rubocop: disable CodeReuse/ActiveRecord -- will be removed # Can't use Event.contributions here because we need to check 3 different - # project_features for the (currently) 3 different contribution types + # project_features for the (currently) 4 different contribution types repo_events = events_created_between(start_time, end_time, :repository) .where(action: :pushed) issue_events = events_created_between(start_time, end_time, :issues) @@ -42,55 +48,110 @@ module Gitlab .from_union([repo_events, issue_events, mr_events, note_events], remove_duplicates: false) .group(:date) .map(&:attributes) + # rubocop: enable CodeReuse/ActiveRecord - @activity_dates = events.each_with_object(Hash.new { |h, k| h[k] = 0 }) do |event, activities| + events.each_with_object(Hash.new { |h, k| h[k] = 0 }) do |event, activities| activities[event["date"]] += event["num_events"] end end - # rubocop: enable CodeReuse/ActiveRecord - # rubocop: disable CodeReuse/ActiveRecord def events_by_date(date) return Event.none unless can_read_cross_project? date_in_time_zone = date.in_time_zone(@contributor_time_instance.time_zone) + if Feature.enabled?(:contributions_calendar_refactoring) + return contributions_between(date_in_time_zone.beginning_of_day, date_in_time_zone.end_of_day).with_associations + end + + # TODO: Remove after feature flag `contributions_calendar_refactoring` is rolled out + # See https://gitlab.com/gitlab-org/gitlab/-/issues/429648 + # rubocop: disable CodeReuse/ActiveRecord -- will be removed Event.contributions.where(author_id: contributor.id) .where(created_at: date_in_time_zone.beginning_of_day..date_in_time_zone.end_of_day) .where(project_id: projects) .with_associations + # rubocop: enable CodeReuse/ActiveRecord end - # rubocop: enable CodeReuse/ActiveRecord - def starting_year - @contributor_time_instance.years_ago(1).year - end + private - def starting_month - @contributor_time_instance.month + def contributions_between(start_time, end_time) + # Can't use Event.contributions here because we need to check 3 different + # project_features for the (currently) 4 different contribution types + repo_events = + project_events_created_between(start_time, end_time, features: :repository) + .for_action(:pushed) + + issue_events = + project_events_created_between(start_time, end_time, features: :issues) + .for_issue + .for_action(%i[created closed]) + + mr_events = + project_events_created_between(start_time, end_time, features: :merge_requests) + .for_merge_request + .for_action(%i[merged created closed approved]) + + note_events = + project_events_created_between(start_time, end_time, features: %i[issues merge_requests]) + .for_action(:commented) + + Event.from_union([repo_events, issue_events, mr_events, note_events], remove_duplicates: false) end - private - def can_read_cross_project? Ability.allowed?(current_user, :read_cross_project) end - # rubocop: disable CodeReuse/ActiveRecord - def events_created_between(start_time, end_time, feature) + # rubocop: disable CodeReuse/ActiveRecord -- no need to move this to ActiveRecord model + def project_events_created_between(start_time, end_time, features:) + Array(features).reduce(Event.none) do |events, feature| + events.or(contribution_events(start_time, end_time).where(project_id: authed_projects(feature))) + end + end + # rubocop: enable CodeReuse/ActiveRecord + + def authed_projects(feature) + strong_memoize("#{feature}_projects") do + # no need to check features access of current user, if the contributor opted-in + # to show all private events anyway - otherwise they would get filtered out again + next contributed_project_ids if contributor.include_private_contributions? + + # rubocop: disable CodeReuse/ActiveRecord -- no need to move this to ActiveRecord model + ProjectFeature + .with_feature_available_for_user(feature, current_user) + .where(project_id: contributed_project_ids) + .pluck(:project_id) + # rubocop: enable CodeReuse/ActiveRecord + end + end + + # rubocop: disable CodeReuse/ActiveRecord -- no need to move this to ActiveRecord model + def contributed_project_ids # re-running the contributed projects query in each union is expensive, so # use IN(project_ids...) instead. It's the intersection of two users so # the list will be (relatively) short @contributed_project_ids ||= projects.distinct.pluck(:id) + end + # rubocop: enable CodeReuse/ActiveRecord + def contribution_events(start_time, end_time) + contributor.events.created_between(start_time, end_time) + end + + # TODO: Remove after feature flag `contributions_calendar_refactoring` is rolled out + # See https://gitlab.com/gitlab-org/gitlab/-/issues/429648 + # rubocop: disable CodeReuse/ActiveRecord -- will be removed + def events_created_between(start_time, end_time, feature) # no need to check feature access of current user, if the contributor opted-in # to show all private events anyway - otherwise they would get filtered out again - authed_projects = if @contributor.include_private_contributions? - @contributed_project_ids + authed_projects = if contributor.include_private_contributions? + contributed_project_ids else ProjectFeature .with_feature_available_for_user(feature, current_user) - .where(project_id: @contributed_project_ids) + .where(project_id: contributed_project_ids) .reorder(nil) .select(:project_id) end diff --git a/lib/gitlab/error_tracking.rb b/lib/gitlab/error_tracking.rb index 2b00fe48951..239aee97378 100644 --- a/lib/gitlab/error_tracking.rb +++ b/lib/gitlab/error_tracking.rb @@ -70,8 +70,8 @@ module Gitlab # returns a Hash, then the return value of that method will be merged into # `extra`. Exceptions can use this mechanism to provide structured data # to sentry in addition to their message and back-trace. - def track_and_raise_exception(exception, extra = {}) - process_exception(exception, extra: extra) + def track_and_raise_exception(exception, extra = {}, tags = {}) + process_exception(exception, extra: extra, tags: tags) raise exception end @@ -90,8 +90,8 @@ module Gitlab # # Provide an issue URL for follow up. # as `issue_url: 'http://gitlab.com/gitlab-org/gitlab/issues/111'` - def track_and_raise_for_dev_exception(exception, extra = {}) - process_exception(exception, extra: extra) + def track_and_raise_for_dev_exception(exception, extra = {}, tags = {}) + process_exception(exception, extra: extra, tags: tags) raise exception if should_raise_for_dev? end @@ -102,8 +102,8 @@ module Gitlab # returns a Hash, then the return value of that method will be merged into # `extra`. Exceptions can use this mechanism to provide structured data # to sentry in addition to their message and back-trace. - def track_exception(exception, extra = {}) - process_exception(exception, extra: extra) + def track_exception(exception, extra = {}, tags = {}) + process_exception(exception, extra: extra, tags: tags) end # This should be used when you only want to log the exception, @@ -157,8 +157,8 @@ module Gitlab end end - def process_exception(exception, extra:, trackers: default_trackers) - context_payload = Gitlab::ErrorTracking::ContextPayloadGenerator.generate(exception, extra) + def process_exception(exception, extra:, tags: {}, trackers: default_trackers) + context_payload = Gitlab::ErrorTracking::ContextPayloadGenerator.generate(exception, extra, tags) trackers.each do |tracker| tracker.capture_exception(exception, **context_payload) diff --git a/lib/gitlab/error_tracking/context_payload_generator.rb b/lib/gitlab/error_tracking/context_payload_generator.rb index 3d0a707608f..23dd2e33a58 100644 --- a/lib/gitlab/error_tracking/context_payload_generator.rb +++ b/lib/gitlab/error_tracking/context_payload_generator.rb @@ -3,14 +3,14 @@ module Gitlab module ErrorTracking class ContextPayloadGenerator - def self.generate(exception, extra = {}) - new.generate(exception, extra) + def self.generate(exception, extra = {}, tags = {}) + new.generate(exception, extra, tags) end - def generate(exception, extra = {}) + def generate(exception, extra = {}, tags = {}) { extra: extra_payload(exception, extra), - tags: tags_payload, + tags: tags_payload(tags), user: user_payload } end @@ -31,12 +31,14 @@ module Gitlab filter.filter(parameters) end - def tags_payload - extra_tags_from_env.merge!( - program: Gitlab.process_name, - locale: I18n.locale, - feature_category: current_context['meta.feature_category'], - Labkit::Correlation::CorrelationId::LOG_KEY.to_sym => Labkit::Correlation::CorrelationId.current_id + def tags_payload(tags) + tags.merge( + extra_tags_from_env.merge!( + program: Gitlab.process_name, + locale: I18n.locale, + feature_category: current_context['meta.feature_category'], + Labkit::Correlation::CorrelationId::LOG_KEY.to_sym => Labkit::Correlation::CorrelationId.current_id + ) ) end diff --git a/lib/gitlab/i18n.rb b/lib/gitlab/i18n.rb index 96e3d90c139..02afdedb4be 100644 --- a/lib/gitlab/i18n.rb +++ b/lib/gitlab/i18n.rb @@ -44,8 +44,8 @@ module Gitlab TRANSLATION_LEVELS = { 'bg' => 0, 'cs_CZ' => 0, - 'da_DK' => 29, - 'de' => 97, + 'da_DK' => 28, + 'de' => 95, 'en' => 100, 'eo' => 0, 'es' => 28, @@ -56,18 +56,18 @@ module Gitlab 'it' => 1, 'ja' => 98, 'ko' => 23, - 'nb_NO' => 21, + 'nb_NO' => 20, 'nl_NL' => 0, 'pl_PL' => 3, - 'pt_BR' => 57, - 'ro_RO' => 76, + 'pt_BR' => 60, + 'ro_RO' => 74, 'ru' => 21, - 'si_LK' => 12, + 'si_LK' => 11, 'tr_TR' => 8, - 'uk' => 52, + 'uk' => 51, 'zh_CN' => 99, 'zh_HK' => 1, - 'zh_TW' => 100 + 'zh_TW' => 99 }.freeze private_constant :TRANSLATION_LEVELS diff --git a/lib/gitlab/internal_events.rb b/lib/gitlab/internal_events.rb index a8f28615cd3..c775182fb40 100644 --- a/lib/gitlab/internal_events.rb +++ b/lib/gitlab/internal_events.rb @@ -4,7 +4,7 @@ module Gitlab module InternalEvents UnknownEventError = Class.new(StandardError) InvalidPropertyError = Class.new(StandardError) - InvalidMethodError = Class.new(StandardError) + InvalidPropertyTypeError = Class.new(StandardError) class << self include Gitlab::Tracking::Helpers @@ -12,6 +12,13 @@ module Gitlab def track_event(event_name, send_snowplow_event: true, **kwargs) raise UnknownEventError, "Unknown event: #{event_name}" unless EventDefinitions.known_event?(event_name) + validate_property!(kwargs, :user, User) + validate_property!(kwargs, :namespace, Namespaces::UserNamespace, Group) + validate_property!(kwargs, :project, Project) + + project = kwargs[:project] + kwargs[:namespace] ||= project.namespace if project + increase_total_counter(event_name) increase_weekly_total_counter(event_name) update_unique_counter(event_name, kwargs) @@ -23,6 +30,14 @@ module Gitlab private + def validate_property!(kwargs, property_name, *class_names) + return unless kwargs.has_key?(property_name) + return if kwargs[property_name].nil? + return if class_names.include?(kwargs[property_name].class) + + raise InvalidPropertyTypeError, "#{property_name} should be an instance of #{class_names.join(', ')}" + end + def increase_total_counter(event_name) redis_counter_key = Gitlab::Usage::Metrics::Instrumentations::TotalCountMetric.redis_key(event_name) @@ -45,10 +60,6 @@ module Gitlab raise InvalidPropertyError, "#{event_name} should be triggered with a named parameter '#{unique_property}'." end - unless kwargs[unique_property].respond_to?(unique_method) - raise InvalidMethodError, "'#{unique_property}' should have a '#{unique_method}' method." - end - unique_value = kwargs[unique_property].public_send(unique_method) # rubocop:disable GitlabSecurity/PublicSend UsageDataCounters::HLLRedisCounter.track_event(event_name, values: unique_value) diff --git a/lib/gitlab/puma/error_handler.rb b/lib/gitlab/puma/error_handler.rb index 4efc4866431..9eabe0731e2 100644 --- a/lib/gitlab/puma/error_handler.rb +++ b/lib/gitlab/puma/error_handler.rb @@ -18,10 +18,11 @@ module Gitlab # https://github.com/puma/puma/pull/3094 status_code ||= 500 - if Raven.configuration.capture_allowed? - Raven.capture_exception(ex, tags: { handler: 'puma_low_level' }, - extra: { puma_env: env, status_code: status_code }) - end + Gitlab::ErrorTracking.track_exception( + ex, + { puma_env: env, status_code: status_code }, + { handler: 'puma_low_level' } + ) # note the below is just a Rack response [status_code, {}, message] diff --git a/locale/gitlab.pot b/locale/gitlab.pot index a4b94710133..ef6c1150fe2 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -40680,9 +40680,6 @@ msgstr "" msgid "Resolve locally" msgstr "" -msgid "Resolve these conflicts, or ask someone with write access to this repository to resolve them locally." -msgstr "" - msgid "Resolve thread" msgstr "" @@ -48156,9 +48153,6 @@ msgstr "" msgid "The commit does not exist" msgstr "" -msgid "The comparison view may be inaccurate due to merge conflicts." -msgstr "" - msgid "The complete DevOps platform. One application with endless possibilities. Organizations rely on GitLab’s source code management, CI/CD, security, and more to deliver software rapidly." msgstr "" @@ -48652,9 +48646,6 @@ msgstr "" msgid "There are currently no target branch rules" msgstr "" -msgid "There are merge conflicts" -msgstr "" - msgid "There are no GPG keys associated with this account." msgstr "" diff --git a/scripts/review_apps/review-apps.sh b/scripts/review_apps/review-apps.sh index ab1675871ee..523087671ed 100755 --- a/scripts/review_apps/review-apps.sh +++ b/scripts/review_apps/review-apps.sh @@ -72,12 +72,34 @@ function delete_helm_release() { fi if deploy_exists "${namespace}" "${release}"; then + echoinfo "[$(date '+%H:%M:%S')] Release exists. Deleting it first." helm uninstall --namespace="${namespace}" "${release}" fi if namespace_exists "${namespace}"; then - echoinfo "Deleting namespace ${namespace}..." true - kubectl delete namespace "${namespace}" --wait + echoinfo "[$(date '+%H:%M:%S')] Deleting namespace ${namespace}..." true + + # Capture status of command, and check whether the status was successful. + if ! kubectl delete namespace "${namespace}" --wait --timeout=1200s; then + echoerr + echoerr "Could not delete the namespace ${namespace} in time." + echoerr + echoerr "It can happen that some resources cannot be deleted right away, causing a delay in the namespace deletion." + echoerr + echoerr "You can see further below which resources are blocking the deletion of the namespace (under DEBUG information)" + echoerr + echoerr "If retrying the job does not fix the issue, please contact Engineering Productivity for further investigation." + echoerr + echoerr "DEBUG INFORMATION:" + echoerr + echoerr "RUNBOOK: https://gitlab.com/gitlab-org/quality/engineering-productivity/team/-/blob/main/runbooks/review-apps.md#namespace-stuck-in-terminating-state" + echoerr + kubectl describe namespace "${namespace}" + echoinfo + kubectl api-resources --verbs=list --namespaced -o name | xargs -n 1 kubectl get -n "${namespace}" + + exit 1 + fi fi } diff --git a/spec/finders/projects_finder_spec.rb b/spec/finders/projects_finder_spec.rb index f7afd96fa09..e570b49e1da 100644 --- a/spec/finders/projects_finder_spec.rb +++ b/spec/finders/projects_finder_spec.rb @@ -482,6 +482,19 @@ RSpec.describe ProjectsFinder, feature_category: :groups_and_projects do it { is_expected.to match_array([internal_project]) } end + describe 'filter by organization_id' do + let_it_be(:organization) { create(:organization) } + let_it_be(:organization_project) { create(:project, organization: organization) } + + let(:params) { { organization_id: organization.id } } + + before do + organization_project.add_maintainer(current_user) + end + + it { is_expected.to match_array([organization_project]) } + end + describe 'when with_issues_enabled is true' do let(:params) { { with_issues_enabled: true } } diff --git a/spec/frontend/diffs/components/merge_conflict_warning_spec.js b/spec/frontend/diffs/components/merge_conflict_warning_spec.js deleted file mode 100644 index 715912b361f..00000000000 --- a/spec/frontend/diffs/components/merge_conflict_warning_spec.js +++ /dev/null @@ -1,58 +0,0 @@ -import { shallowMount, mount } from '@vue/test-utils'; -import MergeConflictWarning from '~/diffs/components/merge_conflict_warning.vue'; - -const propsData = { - limited: true, - mergeable: true, - resolutionPath: 'a-path', -}; - -function findResolveButton(wrapper) { - return wrapper.find('.gl-alert-actions a.gl-button:first-child'); -} -function findLocalMergeButton(wrapper) { - return wrapper.find('.gl-alert-actions button.gl-button:last-child'); -} - -describe('MergeConflictWarning', () => { - let wrapper; - - const createComponent = (props = {}, { full } = { full: false }) => { - const mounter = full ? mount : shallowMount; - - wrapper = mounter(MergeConflictWarning, { - propsData: { ...propsData, ...props }, - }); - }; - - it.each` - present | resolutionPath - ${false} | ${''} - ${true} | ${'some-path'} - `( - 'toggles the resolve conflicts button based on the provided resolutionPath "$resolutionPath"', - ({ present, resolutionPath }) => { - createComponent({ resolutionPath }, { full: true }); - const resolveButton = findResolveButton(wrapper); - - expect(resolveButton.exists()).toBe(present); - if (present) { - expect(resolveButton.attributes('href')).toBe(resolutionPath); - } - }, - ); - - it.each` - present | mergeable - ${false} | ${false} - ${true} | ${true} - `( - 'toggles the local merge button based on the provided mergeable property "$mergable"', - ({ present, mergeable }) => { - createComponent({ mergeable }, { full: true }); - const localMerge = findLocalMergeButton(wrapper); - - expect(localMerge.exists()).toBe(present); - }, - ); -}); diff --git a/spec/frontend/pages/shared/nav/sidebar_tracking_spec.js b/spec/frontend/pages/shared/nav/sidebar_tracking_spec.js deleted file mode 100644 index 04f53e048ed..00000000000 --- a/spec/frontend/pages/shared/nav/sidebar_tracking_spec.js +++ /dev/null @@ -1,160 +0,0 @@ -import { setHTMLFixture } from 'helpers/fixtures'; -import { initSidebarTracking } from '~/pages/shared/nav/sidebar_tracking'; - -describe('~/pages/shared/nav/sidebar_tracking.js', () => { - beforeEach(() => { - setHTMLFixture(` - <aside class="nav-sidebar"> - <div class="nav-sidebar-inner-scroll"> - <ul class="sidebar-top-level-items"> - <li data-track-label="project_information_menu" class="home"> - <a aria-label="Project information" class="shortcuts-project-information has-sub-items" href=""> - <span class="nav-icon-container"> - <svg class="s16" data-testid="project-icon"> - <use xlink:href="/assets/icons-1b2dadc4c3d49797908ba67b8f10da5d63dd15d859bde28d66fb60bbb97a4dd5.svg#project"></use> - </svg> - </span> - <span class="nav-item-name">Project information</span> - </a> - <ul class="sidebar-sub-level-items"> - <li class="fly-out-top-item"> - <a aria-label="Project information" href="#"> - <strong class="fly-out-top-item-name">Project information</strong> - </a> - </li> - <li class="divider fly-out-top-item"></li> - <li data-track-label="activity" class=""> - <a aria-label="Activity" class="shortcuts-project-activity" href=#"> - <span>Activity</span> - </a> - </li> - <li data-track-label="labels" class=""> - <a aria-label="Labels" href="#"> - <span>Labels</span> - </a> - </li> - <li data-track-label="members" class=""> - <a aria-label="Members" href="#"> - <span>Members</span> - </a> - </li> - </ul> - </li> - </ul> - </div> - </aside> - `); - - initSidebarTracking(); - }); - - describe('sidebar is not collapsed', () => { - describe('menu is not expanded', () => { - it('sets the proper data tracking attributes when clicking on menu', () => { - const menu = document.querySelector('li[data-track-label="project_information_menu"]'); - const menuLink = menu.querySelector('a'); - - menu.classList.add('is-over', 'is-showing-fly-out'); - menuLink.click(); - - expect(menu).toHaveTrackingAttributes({ - action: 'click_menu', - extra: JSON.stringify({ - sidebar_display: 'Expanded', - menu_display: 'Fly out', - }), - }); - }); - - it('sets the proper data tracking attributes when clicking on submenu', () => { - const menu = document.querySelector('li[data-track-label="activity"]'); - const menuLink = menu.querySelector('a'); - const submenuList = document.querySelector('ul.sidebar-sub-level-items'); - - submenuList.classList.add('fly-out-list'); - menuLink.click(); - - expect(menu).toHaveTrackingAttributes({ - action: 'click_menu_item', - extra: JSON.stringify({ - sidebar_display: 'Expanded', - menu_display: 'Fly out', - }), - }); - }); - }); - - describe('menu is expanded', () => { - it('sets the proper data tracking attributes when clicking on menu', () => { - const menu = document.querySelector('li[data-track-label="project_information_menu"]'); - const menuLink = menu.querySelector('a'); - - menu.classList.add('active'); - menuLink.click(); - - expect(menu).toHaveTrackingAttributes({ - action: 'click_menu', - extra: JSON.stringify({ - sidebar_display: 'Expanded', - menu_display: 'Expanded', - }), - }); - }); - - it('sets the proper data tracking attributes when clicking on submenu', () => { - const menu = document.querySelector('li[data-track-label="activity"]'); - const menuLink = menu.querySelector('a'); - - menu.classList.add('active'); - menuLink.click(); - - expect(menu).toHaveTrackingAttributes({ - action: 'click_menu_item', - extra: JSON.stringify({ - sidebar_display: 'Expanded', - menu_display: 'Expanded', - }), - }); - }); - }); - }); - - describe('sidebar is collapsed', () => { - beforeEach(() => { - document.querySelector('aside.nav-sidebar').classList.add('js-sidebar-collapsed'); - }); - - it('sets the proper data tracking attributes when clicking on menu', () => { - const menu = document.querySelector('li[data-track-label="project_information_menu"]'); - const menuLink = menu.querySelector('a'); - - menu.classList.add('is-over', 'is-showing-fly-out'); - menuLink.click(); - - expect(menu).toHaveTrackingAttributes({ - action: 'click_menu', - extra: JSON.stringify({ - sidebar_display: 'Collapsed', - menu_display: 'Fly out', - }), - }); - }); - - it('sets the proper data tracking attributes when clicking on submenu', () => { - const menu = document.querySelector('li[data-track-label="activity"]'); - const menuLink = menu.querySelector('a'); - const submenuList = document.querySelector('ul.sidebar-sub-level-items'); - - submenuList.classList.add('fly-out-list'); - menuLink.click(); - - expect(menu).toHaveTrackingAttributes({ - action: 'click_menu_item', - extra: JSON.stringify({ - sidebar_display: 'Collapsed', - menu_display: 'Fly out', - }), - }); - }); - }); -}); diff --git a/spec/lib/gitlab/contributions_calendar_spec.rb b/spec/lib/gitlab/contributions_calendar_spec.rb index 326e27fa716..732b7cb3e59 100644 --- a/spec/lib/gitlab/contributions_calendar_spec.rb +++ b/spec/lib/gitlab/contributions_calendar_spec.rb @@ -19,9 +19,9 @@ RSpec.describe Gitlab::ContributionsCalendar, feature_category: :user_profile do end end - let_it_be(:feature_project) do + let_it_be(:public_project_with_private_issues) do create(:project, :public, :issues_private) do |project| - create(:project_member, user: contributor, project: project).project + create(:project_member, user: contributor, project: project) end end @@ -45,7 +45,12 @@ RSpec.describe Gitlab::ContributionsCalendar, feature_category: :user_profile do end def create_event(project, day, hour = 0, action = :created, target_symbol = :issue) - targets[project] ||= create(target_symbol, project: project, author: contributor) + targets[project] ||= + if target_symbol == :merge_request + create(:merge_request, source_project: project, author: contributor) + else + create(target_symbol, project: project, author: contributor) + end Event.create!( project: project, @@ -58,49 +63,65 @@ RSpec.describe Gitlab::ContributionsCalendar, feature_category: :user_profile do end describe '#activity_dates', :aggregate_failures do - it "returns a hash of date => count" do - create_event(public_project, last_week) - create_event(public_project, last_week) - create_event(public_project, today) - work_item_event = create_event(private_project, today, 0, :created, :work_item) + shared_examples_for 'returns a hash of date => count' do + specify do + create_event(public_project, last_week) + create_event(public_project, last_week) + create_event(public_project, today) + work_item_event = create_event(private_project, today, 0, :created, :work_item) - # make sure the target is a work item as we want to include those in the count - expect(work_item_event.target_type).to eq('WorkItem') - expect(calendar(contributor).activity_dates).to eq(last_week => 2, today => 2) + # make sure the target is a work item as we want to include those in the count + expect(work_item_event.target_type).to eq('WorkItem') + expect(calendar(contributor).activity_dates).to eq(last_week => 2, today => 2) + end end - context "when the user has opted-in for private contributions" do - before do - contributor.update_column(:include_private_contributions, true) - end + shared_examples 'private contribution events' do + context "when the user has opted-in for private contributions" do + before do + contributor.update_column(:include_private_contributions, true) + end - it "shows private and public events to all users" do - create_event(private_project, today) - create_event(public_project, today) + it "shows private and public events to all users" do + create_event(private_project, today) + create_event(public_project, today) - expect(calendar.activity_dates[today]).to eq(2) - expect(calendar(user).activity_dates[today]).to eq(2) - expect(calendar(contributor).activity_dates[today]).to eq(2) - end + expect(calendar.activity_dates[today]).to eq(2) + expect(calendar(user).activity_dates[today]).to eq(2) + expect(calendar(contributor).activity_dates[today]).to eq(2) + end + + # tests for bug https://gitlab.com/gitlab-org/gitlab/-/merge_requests/74826 + it "still counts correct with feature access levels set to private" do + create_event(private_project, today) - # tests for bug https://gitlab.com/gitlab-org/gitlab/-/merge_requests/74826 - it "still counts correct with feature access levels set to private" do - create_event(private_project, today) + private_project.project_feature.update_attribute(:issues_access_level, ProjectFeature::PRIVATE) + private_project.project_feature.update_attribute(:repository_access_level, ProjectFeature::PRIVATE) + private_project.project_feature.update_attribute(:merge_requests_access_level, ProjectFeature::PRIVATE) - private_project.project_feature.update_attribute(:issues_access_level, ProjectFeature::PRIVATE) - private_project.project_feature.update_attribute(:repository_access_level, ProjectFeature::PRIVATE) - private_project.project_feature.update_attribute(:merge_requests_access_level, ProjectFeature::PRIVATE) + expect(calendar.activity_dates[today]).to eq(1) + expect(calendar(user).activity_dates[today]).to eq(1) + expect(calendar(contributor).activity_dates[today]).to eq(1) + end - expect(calendar.activity_dates[today]).to eq(1) - expect(calendar(user).activity_dates[today]).to eq(1) - expect(calendar(contributor).activity_dates[today]).to eq(1) + it "does not fail if there are no contributed projects" do + expect(calendar.activity_dates[today]).to eq(nil) + end end + end - it "does not fail if there are no contributed projects" do - expect(calendar.activity_dates[today]).to eq(nil) + context 'when contributions_calendar_refactoring feature flag is disabled' do + before do + stub_feature_flags(contributions_calendar_refactoring: false) end + + it_behaves_like 'returns a hash of date => count' + include_examples 'private contribution events' end + it_behaves_like 'returns a hash of date => count' + include_examples 'private contribution events' + it "counts the diff notes on merge request" do create_event(public_project, today, 0, :commented, :diff_note_on_merge_request) @@ -114,6 +135,15 @@ RSpec.describe Gitlab::ContributionsCalendar, feature_category: :user_profile do expect(calendar(contributor).activity_dates[today]).to eq(2) end + it "counts merge request events" do + create_event(public_project, today, 0, :created, :merge_request) + create_event(public_project, today, 1, :closed, :merge_request) + create_event(public_project, today, 2, :approved, :merge_request) + create_event(public_project, today, 3, :merged, :merge_request) + + expect(calendar(contributor).activity_dates[today]).to eq(4) + end + context "when events fall under different dates depending on the system time zone" do before do create_event(public_project, today, 1) @@ -189,65 +219,56 @@ RSpec.describe Gitlab::ContributionsCalendar, feature_category: :user_profile do it "only shows private events to authorized users" do e1 = create_event(public_project, today) e2 = create_event(private_project, today) - e3 = create_event(feature_project, today) + e3 = create_event(public_project_with_private_issues, today, 0, :created, :issue) create_event(public_project, last_week) - expect(calendar.events_by_date(today)).to contain_exactly(e1, e3) - expect(calendar(contributor).events_by_date(today)).to contain_exactly(e1, e2, e3) - end - - it "includes diff notes on merge request" do - e1 = create_event(public_project, today, 0, :commented, :diff_note_on_merge_request) - expect(calendar.events_by_date(today)).to contain_exactly(e1) + expect(calendar(contributor).events_by_date(today)).to contain_exactly(e1, e2, e3) end - context 'when the user cannot read cross project' do + context 'when contributions_calendar_refactoring feature flag is disabled' do before do - allow(Ability).to receive(:allowed?).and_call_original - expect(Ability).to receive(:allowed?).with(user, :read_cross_project) { false } + stub_feature_flags(contributions_calendar_refactoring: false) end - it 'does not return any events' do - create_event(public_project, today) + it "only shows private events to authorized users" do + e1 = create_event(public_project, today) + e2 = create_event(private_project, today) + e3 = create_event(public_project_with_private_issues, today, 0, :created, :issue) + create_event(public_project, last_week) - expect(calendar(user).events_by_date(today)).to be_empty + expect(calendar.events_by_date(today)).to contain_exactly(e1, e3) + expect(calendar(contributor).events_by_date(today)).to contain_exactly(e1, e2, e3) end end - end - describe '#starting_year' do - let(:travel_time) { Time.find_zone('UTC').local(2020, 12, 31, 19, 0, 0) } + it "includes diff notes on merge request" do + e1 = create_event(public_project, today, 0, :commented, :diff_note_on_merge_request) - context "when the contributor's timezone is not set" do - it "is the start of last year in the system timezone" do - expect(calendar.starting_year).to eq(2019) - end + expect(calendar.events_by_date(today)).to contain_exactly(e1) end - context "when the contributor's timezone is set to Sydney" do - let(:contributor) { create(:user, { timezone: 'Sydney' }) } + it 'includes merge request events' do + mr_created_event = create_event(public_project, today, 0, :created, :merge_request) + mr_closed_event = create_event(public_project, today, 1, :closed, :merge_request) + mr_approved_event = create_event(public_project, today, 2, :approved, :merge_request) + mr_merged_event = create_event(public_project, today, 3, :merged, :merge_request) - it "is the start of last year in Sydney" do - expect(calendar.starting_year).to eq(2020) - end + expect(calendar.events_by_date(today)).to contain_exactly( + mr_created_event, mr_closed_event, mr_approved_event, mr_merged_event + ) end - end - describe '#starting_month' do - let(:travel_time) { Time.find_zone('UTC').local(2020, 12, 31, 19, 0, 0) } - - context "when the contributor's timezone is not set" do - it "is the start of this month in the system timezone" do - expect(calendar.starting_month).to eq(12) + context 'when the user cannot read cross project' do + before do + allow(Ability).to receive(:allowed?).and_call_original + expect(Ability).to receive(:allowed?).with(user, :read_cross_project) { false } end - end - context "when the contributor's timezone is set to Sydney" do - let(:contributor) { create(:user, { timezone: 'Sydney' }) } + it 'does not return any events' do + create_event(public_project, today) - it "is the start of this month in Sydney" do - expect(calendar.starting_month).to eq(1) + expect(calendar(user).events_by_date(today)).to be_empty end end end diff --git a/spec/lib/gitlab/error_tracking/context_payload_generator_spec.rb b/spec/lib/gitlab/error_tracking/context_payload_generator_spec.rb index 38745fe0cde..932c1b2fb4c 100644 --- a/spec/lib/gitlab/error_tracking/context_payload_generator_spec.rb +++ b/spec/lib/gitlab/error_tracking/context_payload_generator_spec.rb @@ -64,8 +64,11 @@ RSpec.describe Gitlab::ErrorTracking::ContextPayloadGenerator do end context 'when the GITLAB_SENTRY_EXTRA_TAGS env is a JSON hash' do - it 'includes those tags in all events' do + before do stub_env('GITLAB_SENTRY_EXTRA_TAGS', { foo: 'bar', baz: 'quux' }.to_json) + end + + it 'includes those tags in all events' do payload = {} Gitlab::ApplicationContext.with_context(feature_category: 'feature_a') do @@ -87,6 +90,26 @@ RSpec.describe Gitlab::ErrorTracking::ContextPayloadGenerator do generator.generate(exception, extra) end + + context 'with generated tags' do + it 'includes all tags' do + payload = {} + + Gitlab::ApplicationContext.with_context(feature_category: 'feature_a') do + payload = generator.generate(exception, extra, { 'mytag' => '123' }) + end + + expect(payload[:tags]).to eql( + correlation_id: 'cid', + locale: 'en', + program: 'test', + feature_category: 'feature_a', + 'foo' => 'bar', + 'baz' => 'quux', + 'mytag' => '123' + ) + end + end end context 'when the GITLAB_SENTRY_EXTRA_TAGS env is not a JSON hash' do diff --git a/spec/lib/gitlab/error_tracking_spec.rb b/spec/lib/gitlab/error_tracking_spec.rb index 79016335a40..c9b2e21d934 100644 --- a/spec/lib/gitlab/error_tracking_spec.rb +++ b/spec/lib/gitlab/error_tracking_spec.rb @@ -97,6 +97,27 @@ RSpec.describe Gitlab::ErrorTracking, feature_category: :shared do ) end.to raise_error(RuntimeError, /boom/) end + + context 'with tags' do + let(:tags) { { 'mytag' => 2 } } + + before do + sentry_payload[:tags].merge!(tags) + end + + it 'includes additional tags' do + expect(Raven).to receive(:capture_exception).with(exception, sentry_payload) + expect(Sentry).to receive(:capture_exception).with(exception, sentry_payload) + + expect do + described_class.track_and_raise_for_dev_exception( + exception, + { issue_url: issue_url, some_other_info: 'info' }, + tags + ) + end.to raise_error(RuntimeError, /boom/) + end + end end context 'when exceptions for dev should not be raised' do @@ -181,8 +202,10 @@ RSpec.describe Gitlab::ErrorTracking, feature_category: :shared do end describe '.track_exception' do + let(:tags) { {} } + subject(:track_exception) do - described_class.track_exception(exception, extra) + described_class.track_exception(exception, extra, tags) end before do @@ -207,6 +230,18 @@ RSpec.describe Gitlab::ErrorTracking, feature_category: :shared do expect(Gitlab::ErrorTracking::Logger).to have_received(:error).with(logger_payload) end + context 'with tags' do + let(:tags) { { 'mytag' => 2 } } + + it 'includes the tags' do + track_exception + + expect(Gitlab::ErrorTracking::Logger).to have_received(:error).with( + hash_including({ 'tags.mytag' => 2 }) + ) + end + end + context 'with filterable parameters' do let(:extra) { { test: 1, my_token: 'test' } } diff --git a/spec/lib/gitlab/internal_events_spec.rb b/spec/lib/gitlab/internal_events_spec.rb index 1cad9e7e5cf..68d6f3700af 100644 --- a/spec/lib/gitlab/internal_events_spec.rb +++ b/spec/lib/gitlab/internal_events_spec.rb @@ -12,11 +12,23 @@ RSpec.describe Gitlab::InternalEvents, :snowplow, feature_category: :product_ana allow(redis).to receive(:incr) allow(Gitlab::Redis::SharedState).to receive(:with).and_yield(redis) allow(Gitlab::Tracking).to receive(:tracker).and_return(fake_snowplow) - allow(Gitlab::InternalEvents::EventDefinitions).to receive(:unique_property).and_return(:user) + allow(Gitlab::InternalEvents::EventDefinitions).to receive(:unique_property).and_return(unique_property) allow(fake_snowplow).to receive(:event) end - def expect_redis_hll_tracking(event_name) + shared_examples 'an event that logs an error' do + it 'logs an error' do + described_class.track_event(event_name, **event_kwargs) + + expect(Gitlab::ErrorTracking).to have_received(:track_and_raise_for_dev_exception) + .with(described_class::InvalidPropertyTypeError, + event_name: event_name, + kwargs: event_kwargs + ) + end + end + + def expect_redis_hll_tracking expect(Gitlab::UsageDataCounters::HLLRedisCounter).to have_received(:track_event) .with(event_name, values: unique_value) end @@ -29,7 +41,7 @@ RSpec.describe Gitlab::InternalEvents, :snowplow, feature_category: :product_ana end end - def expect_snowplow_tracking(event_name) + def expect_snowplow_tracking(expected_namespace = nil) service_ping_context = Gitlab::Tracking::ServicePingContext .new(data_source: :redis_hll, event: event_name) .to_context @@ -38,34 +50,125 @@ RSpec.describe Gitlab::InternalEvents, :snowplow, feature_category: :product_ana expect(SnowplowTracker::SelfDescribingJson).to have_received(:new) .with(service_ping_context[:schema], service_ping_context[:data]).at_least(:once) - # Add test for creation of both contexts - contexts = [instance_of(SnowplowTracker::SelfDescribingJson), instance_of(SnowplowTracker::SelfDescribingJson)] + expect(fake_snowplow).to have_received(:event) do |category, provided_event_name, args| + expect(category).to eq('InternalEventTracking') + expect(provided_event_name).to eq(event_name) + + contexts = args[:context]&.map(&:to_json) + + # Verify Standard Context + standard_context = contexts.find do |c| + c[:schema] == Gitlab::Tracking::StandardContext::GITLAB_STANDARD_SCHEMA_URL + end + + validate_standard_context(standard_context, expected_namespace) - expect(fake_snowplow).to have_received(:event) - .with('InternalEventTracking', event_name, context: contexts) + # Verify Service Ping context + service_ping_context = contexts.find { |c| c[:schema] == Gitlab::Tracking::ServicePingContext::SCHEMA_URL } + + validate_service_ping_context(service_ping_context) + end + end + + def validate_standard_context(standard_context, expected_namespace) + namespace = expected_namespace || project&.namespace + expect(standard_context).not_to eq(nil) + expect(standard_context[:data][:user_id]).to eq(user&.id) + expect(standard_context[:data][:namespace_id]).to eq(namespace&.id) + expect(standard_context[:data][:project_id]).to eq(project&.id) + end + + def validate_service_ping_context(service_ping_context) + expect(service_ping_context).not_to eq(nil) + expect(service_ping_context[:data][:data_source]).to eq(:redis_hll) + expect(service_ping_context[:data][:event_name]).to eq(event_name) end let_it_be(:user) { build(:user, id: 1) } - let_it_be(:project) { build(:project, id: 2) } - let_it_be(:namespace) { project.namespace } + let_it_be(:project_namespace) { build(:namespace, id: 2) } + let_it_be(:project) { build(:project, id: 3, namespace: project_namespace) } let(:redis) { instance_double('Redis') } let(:fake_snowplow) { instance_double(Gitlab::Tracking::Destinations::Snowplow) } let(:event_name) { 'g_edit_by_web_ide' } + let(:unique_property) { :user } let(:unique_value) { user.id } let(:redis_arguments) { [event_name, Date.today.strftime('%G-%V')] } + context 'when only user is passed' do + let(:project) { nil } + let(:namespace) { nil } + + it 'updated all tracking methods' do + described_class.track_event(event_name, user: user) + + expect_redis_tracking + expect_redis_hll_tracking + expect_snowplow_tracking + end + end + + context 'when namespace is passed' do + let(:namespace) { build(:namespace, id: 4) } + + it 'uses id from namespace' do + described_class.track_event(event_name, user: user, project: project, namespace: namespace) + + expect_redis_tracking + expect_redis_hll_tracking + expect_snowplow_tracking(namespace) + end + end + + context 'when namespace is not passed' do + let(:unique_property) { :namespace } + let(:unique_value) { project.namespace.id } + + it 'uses id from projects namespace' do + described_class.track_event(event_name, user: user, project: project) + + expect_redis_tracking + expect_redis_hll_tracking + expect_snowplow_tracking(project.namespace) + end + end + + context 'when arguments are invalid' do + context 'when user is not an instance of User' do + let(:user) { 'a_string' } + + it_behaves_like 'an event that logs an error' do + let(:event_kwargs) { { user: user, project: project } } + end + end + + context 'when project is not an instance of Project' do + let(:project) { 42 } + + it_behaves_like 'an event that logs an error' do + let(:event_kwargs) { { user: user, project: project } } + end + end + + context 'when namespace is not an instance of Namespace' do + let(:namespace) { false } + + it_behaves_like 'an event that logs an error' do + let(:event_kwargs) { { user: user, namespace: namespace } } + end + end + end + it 'updates Redis, RedisHLL and Snowplow', :aggregate_failures do - params = { user: user, project: project, namespace: namespace } - described_class.track_event(event_name, **params) + described_class.track_event(event_name, user: user, project: project) expect_redis_tracking - expect_redis_hll_tracking(event_name) - expect_snowplow_tracking(event_name) # Add test for arguments + expect_redis_hll_tracking + expect_snowplow_tracking end it 'rescues error', :aggregate_failures do - params = { user: user, project: project, namespace: namespace } + params = { user: user, project: project } error = StandardError.new("something went wrong") allow(fake_snowplow).to receive(:event).and_raise(error) @@ -123,8 +226,8 @@ RSpec.describe Gitlab::InternalEvents, :snowplow, feature_category: :product_ana described_class.track_event(event_name, user: user, project: project) expect_redis_tracking - expect_redis_hll_tracking(event_name) - expect_snowplow_tracking(event_name) + expect_redis_hll_tracking + expect_snowplow_tracking end context 'when property is missing' do @@ -136,22 +239,12 @@ RSpec.describe Gitlab::InternalEvents, :snowplow, feature_category: :product_ana end end - context 'when method does not exist on property', :aggregate_failures do - it 'logs error on missing method' do - expect { described_class.track_event(event_name, project: "a_string") }.not_to raise_error - - expect_redis_tracking - expect(Gitlab::ErrorTracking).to have_received(:track_and_raise_for_dev_exception) - .with(described_class::InvalidMethodError, event_name: event_name, kwargs: { project: 'a_string' }) - end - end - context 'when send_snowplow_event is false' do it 'logs to Redis and RedisHLL but not Snowplow' do described_class.track_event(event_name, send_snowplow_event: false, user: user, project: project) expect_redis_tracking - expect_redis_hll_tracking(event_name) + expect_redis_hll_tracking expect(fake_snowplow).not_to have_received(:event) end end @@ -170,7 +263,7 @@ RSpec.describe Gitlab::InternalEvents, :snowplow, feature_category: :product_ana described_class.track_event(event_name, user: user, project: project) expect_redis_tracking - expect_snowplow_tracking(event_name) + expect_snowplow_tracking(project.namespace) expect(Gitlab::UsageDataCounters::HLLRedisCounter).not_to have_received(:track_event) end end diff --git a/spec/lib/gitlab/puma/error_handler_spec.rb b/spec/lib/gitlab/puma/error_handler_spec.rb index 5b7cdf37af1..bfcbf32e899 100644 --- a/spec/lib/gitlab/puma/error_handler_spec.rb +++ b/spec/lib/gitlab/puma/error_handler_spec.rb @@ -12,11 +12,10 @@ RSpec.describe Gitlab::Puma::ErrorHandler, feature_category: :shared do describe '#execute' do it 'captures the exception and returns a Rack response' do - allow(Raven.configuration).to receive(:capture_allowed?).and_return(true) - expect(Raven).to receive(:capture_exception).with( + expect(Gitlab::ErrorTracking).to receive(:track_exception).with( ex, - tags: { handler: 'puma_low_level' }, - extra: { puma_env: env, status_code: status_code } + { puma_env: env, status_code: status_code }, + { handler: 'puma_low_level' } ).and_call_original status, headers, message = subject.execute(ex, env, status_code) @@ -26,25 +25,10 @@ RSpec.describe Gitlab::Puma::ErrorHandler, feature_category: :shared do expect(message).to eq(described_class::PROD_ERROR_MESSAGE) end - context 'when capture is not allowed' do - it 'returns a Rack response without capturing the exception' do - allow(Raven.configuration).to receive(:capture_allowed?).and_return(false) - expect(Raven).not_to receive(:capture_exception) - - status, headers, message = subject.execute(ex, env, status_code) - - expect(status).to eq(500) - expect(headers).to eq({}) - expect(message).to eq(described_class::PROD_ERROR_MESSAGE) - end - end - context 'when not in production' do let(:is_production) { false } it 'returns a Rack response with dev error message' do - allow(Raven.configuration).to receive(:capture_allowed?).and_return(true) - status, headers, message = subject.execute(ex, env, status_code) expect(status).to eq(500) @@ -57,9 +41,6 @@ RSpec.describe Gitlab::Puma::ErrorHandler, feature_category: :shared do let(:status_code) { 500 } it 'defaults to error 500' do - allow(Raven.configuration).to receive(:capture_allowed?).and_return(false) - expect(Raven).not_to receive(:capture_exception) - status, headers, message = subject.execute(ex, env, status_code) expect(status).to eq(500) @@ -72,8 +53,6 @@ RSpec.describe Gitlab::Puma::ErrorHandler, feature_category: :shared do let(:status_code) { 404 } it 'uses the provided status code in the response' do - allow(Raven.configuration).to receive(:capture_allowed?).and_return(true) - status, headers, message = subject.execute(ex, env, status_code) expect(status).to eq(404) diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb index 4c2d44a3e12..6430fc2ffc8 100644 --- a/spec/models/event_spec.rb +++ b/spec/models/event_spec.rb @@ -115,6 +115,18 @@ RSpec.describe Event, feature_category: :user_profile do end end + describe '.for_merge_request' do + let(:mr_event) { create(:event, :for_merge_request, project: project) } + + before do + create(:event, :for_issue, project: project) + end + + it 'returns events for MergeRequest target_type' do + expect(described_class.for_merge_request).to contain_exactly(mr_event) + end + end + describe '.created_at' do it 'can find the right event' do time = 1.day.ago @@ -128,6 +140,21 @@ RSpec.describe Event, feature_category: :user_profile do end end + describe '.created_between' do + it 'returns events created between given timestamps' do + start_time = 2.days.ago + end_time = Date.today + + create(:event, created_at: 3.days.ago) + e1 = create(:event, created_at: 2.days.ago) + e2 = create(:event, created_at: 1.day.ago) + + found = described_class.created_between(start_time, end_time) + + expect(found).to contain_exactly(e1, e2) + end + end + describe '.for_fingerprint' do let_it_be(:with_fingerprint) { create(:event, fingerprint: 'aaa', project: project) } @@ -152,16 +179,28 @@ RSpec.describe Event, feature_category: :user_profile do end describe '.contributions' do - let!(:merge_request_event) { create(:event, :created, :for_merge_request, project: project) } - let!(:issue_event) { create(:event, :created, :for_issue, project: project) } + let!(:merge_request_events) do + %i[created closed merged approved].map do |action| + create(:event, :for_merge_request, action: action, project: project) + end + end + let!(:work_item_event) { create(:event, :created, :for_work_item, project: project) } - let!(:design_event) { create(:design_event, project: project) } + let!(:issue_events) do + %i[created closed].map { |action| create(:event, :for_issue, action: action, project: project) } + end + + let!(:push_event) { create_push_event(project, project.owner) } + let!(:comment_event) { create(:event, :commented, project: project) } + + before do + create(:design_event, project: project) # should not be in scope + end - it 'returns events for MergeRequest, Issue and WorkItem' do + it 'returns events for MergeRequest, Issue, WorkItem and push, comment events' do expect(described_class.contributions).to contain_exactly( - merge_request_event, - issue_event, - work_item_event + *merge_request_events, *issue_events, work_item_event, + push_event, comment_event ) end end diff --git a/spec/requests/api/graphql/ci/runners_spec.rb b/spec/requests/api/graphql/ci/runners_spec.rb index 0e2712d742d..9b45e16178a 100644 --- a/spec/requests/api/graphql/ci/runners_spec.rb +++ b/spec/requests/api/graphql/ci/runners_spec.rb @@ -35,17 +35,19 @@ RSpec.describe 'Query.runners', feature_category: :runner_fleet do end context 'with filters' do - shared_examples 'a working graphql query returning expected runner' do + shared_examples 'a working graphql query returning expected runners' do it_behaves_like 'a working graphql query' do before do post_graphql(query, current_user: current_user) end end - it 'returns expected runner' do + it 'returns expected runners' do post_graphql(query, current_user: current_user) - expect(runners_graphql_data['nodes']).to contain_exactly(a_graphql_entity_for(expected_runner)) + expect(runners_graphql_data['nodes']).to contain_exactly( + *Array(expected_runners).map { |expected_runner| a_graphql_entity_for(expected_runner) } + ) end it 'does not execute more queries per runner', :aggregate_failures do @@ -95,24 +97,36 @@ RSpec.describe 'Query.runners', feature_category: :runner_fleet do let(:runner_type) { 'INSTANCE_TYPE' } let(:status) { 'ACTIVE' } - let!(:expected_runner) { instance_runner } + let(:expected_runners) { instance_runner } - it_behaves_like 'a working graphql query returning expected runner' + it_behaves_like 'a working graphql query returning expected runners' end context 'runner_type is PROJECT_TYPE and status is NEVER_CONTACTED' do let(:runner_type) { 'PROJECT_TYPE' } let(:status) { 'NEVER_CONTACTED' } - let!(:expected_runner) { project_runner } + let(:expected_runners) { project_runner } - it_behaves_like 'a working graphql query returning expected runner' + it_behaves_like 'a working graphql query returning expected runners' end end context 'when filtered on version prefix' do - let_it_be(:version_runner) { create(:ci_runner, :project, active: false, description: 'Runner with machine') } - let_it_be(:version_runner_machine) { create(:ci_runner_machine, runner: version_runner, version: '15.11.0') } + let_it_be(:runner_15_10_1) { create_ci_runner(version: '15.10.1') } + + let_it_be(:runner_15_11_0) { create_ci_runner(version: '15.11.0') } + let_it_be(:runner_15_11_1) { create_ci_runner(version: '15.11.1') } + + let_it_be(:runner_16_1_0) { create_ci_runner(version: '16.1.0') } + + let(:fields) do + <<~QUERY + nodes { + id + } + QUERY + end let(:query) do %( @@ -124,12 +138,44 @@ RSpec.describe 'Query.runners', feature_category: :runner_fleet do ) end - context 'version_prefix is "15."' do + context 'when version_prefix is "15."' do let(:version_prefix) { '15.' } - let!(:expected_runner) { version_runner } + it_behaves_like 'a working graphql query returning expected runners' do + let(:expected_runners) { [runner_15_10_1, runner_15_11_0, runner_15_11_1] } + end + end + + context 'when version_prefix is "15.11."' do + let(:version_prefix) { '15.11.' } - it_behaves_like 'a working graphql query returning expected runner' + it_behaves_like 'a working graphql query returning expected runners' do + let(:expected_runners) { [runner_15_11_0, runner_15_11_1] } + end + end + + context 'when version_prefix is "15.11.0"' do + let(:version_prefix) { '15.11.0' } + + it_behaves_like 'a working graphql query returning expected runners' do + let(:expected_runners) { runner_15_11_0 } + end + end + + context 'when version_prefix is not digits' do + let(:version_prefix) { 'a.b' } + + it_behaves_like 'a working graphql query returning expected runners' do + let(:expected_runners) do + [instance_runner, project_runner, runner_15_10_1, runner_15_11_0, runner_15_11_1, runner_16_1_0] + end + end + end + + def create_ci_runner(args = {}, version:) + create(:ci_runner, :project, **args).tap do |runner| + create(:ci_runner_machine, runner: runner, version: version) + end end end end diff --git a/spec/requests/api/ml/mlflow/model_versions_spec.rb b/spec/requests/api/ml/mlflow/model_versions_spec.rb index f59888ec70f..9813ed95ab3 100644 --- a/spec/requests/api/ml/mlflow/model_versions_spec.rb +++ b/spec/requests/api/ml/mlflow/model_versions_spec.rb @@ -35,9 +35,9 @@ RSpec.describe API::Ml::Mlflow::ModelVersions, feature_category: :mlops do response end - describe 'GET /projects/:id/ml/mlflow/api/2.0/mlflow/model_versions/get' do + describe 'GET /projects/:id/ml/mlflow/api/2.0/mlflow/model-versions/get' do let(:route) do - "/projects/#{project_id}/ml/mlflow/api/2.0/mlflow/model_versions/get?name=#{name}&version=#{version}" + "/projects/#{project_id}/ml/mlflow/api/2.0/mlflow/model-versions/get?name=#{name}&version=#{version}" end it 'returns the model version', :aggregate_failures do @@ -51,7 +51,7 @@ RSpec.describe API::Ml::Mlflow::ModelVersions, feature_category: :mlops do context 'when has access' do context 'and model name in incorrect' do let(:route) do - "/projects/#{project_id}/ml/mlflow/api/2.0/mlflow/model_versions/get?name=--&version=#{version}" + "/projects/#{project_id}/ml/mlflow/api/2.0/mlflow/model-versions/get?name=--&version=#{version}" end it_behaves_like 'MLflow|Not Found - Resource Does Not Exist' @@ -59,7 +59,7 @@ RSpec.describe API::Ml::Mlflow::ModelVersions, feature_category: :mlops do context 'and version in incorrect' do let(:route) do - "/projects/#{project_id}/ml/mlflow/api/2.0/mlflow/model_versions/get?name=#{name}&version=--" + "/projects/#{project_id}/ml/mlflow/api/2.0/mlflow/model-versions/get?name=#{name}&version=--" end it_behaves_like 'MLflow|Not Found - Resource Does Not Exist' |