Commit a1565a82 authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent 40254b9a
...@@ -74,7 +74,7 @@ module Referable ...@@ -74,7 +74,7 @@ module Referable
#{Regexp.escape(Gitlab.config.gitlab.url)} #{Regexp.escape(Gitlab.config.gitlab.url)}
\/#{Project.reference_pattern} \/#{Project.reference_pattern}
(?:\/\-)? (?:\/\-)?
\/#{Regexp.escape(route)} \/#{route.is_a?(Regexp) ? route : Regexp.escape(route)}
\/#{pattern} \/#{pattern}
(?<path> (?<path>
(\/[a-z0-9_=-]+)* (\/[a-z0-9_=-]+)*
......
---
description: "Learn how long your open merge requests have spent in code review, and what distinguishes the longest-running." # Up to ~200 chars long. They will be displayed in Google Search snippets. It may help to write the page intro first, and then reuse it here.
---
# Code Review Analytics **(STARTER)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/38062) in GitLab ([Starter](https://about.gitlab.com/pricing/)) 12.7.
Want to learn how long your open merge requests have spent in code review? Or what distinguishes your longest-running code reviews? These are some of the questions Code Review Analytics is designed to answer.
NOTE: **Note:**
Initially no data will appear. Data will populate as users comment on open merge requests.
## Overview
Code Review Analytics displays a collection of merge requests in a table. These are all the open merge requests that are considered to be in code review. This feature considers code review to begin when a merge request receives its first comment from someone other than the author. The rows of the table are sorted by review time so the longest reviews appear at the top. There are also columns to display the author, approvers, comment count, and line -/+ counts.
This feature is designed for [development team leaders](https://about.gitlab.com/handbook/marketing/product-marketing/roles-personas/#delaney-development-team-lead) and others who want to understand broad code review dynamics, and identify patterns to help explain them. You can use Code Review Analytics to expose your team's unique challenges with code review, and identify improvements that might substantially accelerate your development cycle.
## Use cases
Perhaps your team agrees that code review is moving too slow, or the [Cycle Analytics feature](https://docs.gitlab.com/ee/user/analytics/cycle_analytics.html) shows that "Review" is your team's most time-consuming step. You can use Code Review Analytics to see what is currently moving slowest, and analyze the patterns and trends between them. Lots of comments or commits? Maybe the code is too complex. A particular author is involved? Maybe more training is advisable. Few comments and approvers? Maybe your team is understaffed.
## Permissions
- On [Starter or Bronze tier](https://about.gitlab.com/pricing/) and above.
- By users with [Reporter access] and above.
## Feature flag
Code Review Analytics is [currently protected by a feature flag](https://gitlab.com/gitlab-org/gitlab/issues/194165) that defaults to "enabled" - meaning the feature is available. If you experience performance problems or otherwise wish to disable the feature, a GitLab administrator can execute a command in a Rails console:
```ruby
Feature.disable(:code_review_analytics)
```
...@@ -15,6 +15,8 @@ Once enabled, click on **Analytics** from the top navigation bar. ...@@ -15,6 +15,8 @@ Once enabled, click on **Analytics** from the top navigation bar.
From the centralized analytics workspace, the following analytics are available: From the centralized analytics workspace, the following analytics are available:
- [Code Review Analytics](code_review_analytics.md), enabled with the `code_review_analytics`
[feature flag](../../development/feature_flags/development.html#enabling-a-feature-flag-in-development). **(STARTER)**
- [Cycle Analytics](cycle_analytics.md), enabled with the `cycle_analytics` - [Cycle Analytics](cycle_analytics.md), enabled with the `cycle_analytics`
[feature flag](../../development/feature_flags/development.html#enabling-a-feature-flag-in-development). **(PREMIUM)** [feature flag](../../development/feature_flags/development.html#enabling-a-feature-flag-in-development). **(PREMIUM)**
- [Productivity Analytics](productivity_analytics.md), enabled with the `productivity_analytics` - [Productivity Analytics](productivity_analytics.md), enabled with the `productivity_analytics`
......
...@@ -53,11 +53,12 @@ From error list, users can navigate to the error details page by clicking the ti ...@@ -53,11 +53,12 @@ From error list, users can navigate to the error details page by clicking the ti
This page has: This page has:
- A link to the Sentry issue. - A link to the Sentry issue.
- A link to the GitLab commit if the Sentry [release id/version](https://docs.sentry.io/workflow/releases/?platform=javascript#configure-sdk) on the Sentry Issue's first release matches a commit SHA in your GitLab hosted project.
- Other details about the issue, including a full stack trace. - Other details about the issue, including a full stack trace.
If the error has not been linked to an existing GitLab issue, a 'Create issue' button will be visible: By default, a **Create issue** button is displayed. Once you have used it to create an issue, the button is hidden.
![Error Details without Issue Link](img/error_details_v12_6.png) ![Error Details without Issue Link](img/error_details_v12_7.png)
If a link does exist, it will be shown in the details and the 'Create issue' button will change to a 'View issue' button: If a link does exist, it will be shown in the details and the 'Create issue' button will change to a 'View issue' button:
......
...@@ -297,8 +297,8 @@ module Banzai ...@@ -297,8 +297,8 @@ module Banzai
@references_per[parent_type] ||= begin @references_per[parent_type] ||= begin
refs = Hash.new { |hash, key| hash[key] = Set.new } refs = Hash.new { |hash, key| hash[key] = Set.new }
regex = [ regex = [
object_class.reference_pattern, object_class.link_reference_pattern,
object_class.link_reference_pattern object_class.reference_pattern
].compact.reduce { |a, b| Regexp.union(a, b) } ].compact.reduce { |a, b| Regexp.union(a, b) }
nodes.each do |node| nodes.each do |node|
......
...@@ -10,31 +10,6 @@ module Gitlab ...@@ -10,31 +10,6 @@ module Gitlab
def finalize(records) def finalize(records)
# Optional: Called with the actual set of records # Optional: Called with the actual set of records
end end
private
def per_page
@per_page ||= params[:per_page]
end
def base_request_uri
@base_request_uri ||= URI.parse(request.url).tap do |uri|
uri.host = Gitlab.config.gitlab.host
uri.port = Gitlab.config.gitlab.port
end
end
def build_page_url(query_params:)
base_request_uri.tap do |uri|
uri.query = query_params
end.to_s
end
def page_href(next_page_params = {})
query_params = params.merge(**next_page_params, per_page: per_page).to_query
build_page_url(query_params: query_params)
end
end end
end end
end end
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
module Gitlab module Gitlab
module Pagination module Pagination
module Keyset module Keyset
class Pager class Pager < Gitlab::Pagination::Base
attr_reader :request attr_reader :request
def initialize(request) def initialize(request)
......
...@@ -72,6 +72,29 @@ module Gitlab ...@@ -72,6 +72,29 @@ module Gitlab
def data_without_counts?(paginated_data) def data_without_counts?(paginated_data)
paginated_data.is_a?(Kaminari::PaginatableWithoutCount) paginated_data.is_a?(Kaminari::PaginatableWithoutCount)
end end
def base_request_uri
@base_request_uri ||= URI.parse(request.url).tap do |uri|
uri.host = Gitlab.config.gitlab.host
uri.port = Gitlab.config.gitlab.port
end
end
def build_page_url(query_params:)
base_request_uri.tap do |uri|
uri.query = query_params
end.to_s
end
def page_href(next_page_params = {})
query_params = params.merge(**next_page_params, per_page: per_page).to_query
build_page_url(query_params: query_params)
end
def per_page
@per_page ||= params[:per_page]
end
end end
end end
end end
...@@ -9,7 +9,6 @@ describe('Monitoring Component', () => { ...@@ -9,7 +9,6 @@ describe('Monitoring Component', () => {
const createWrapper = () => { const createWrapper = () => {
wrapper = shallowMount(MonitoringComponent, { wrapper = shallowMount(MonitoringComponent, {
attachToDocument: true,
propsData: { propsData: {
monitoringUrl, monitoringUrl,
}, },
......
...@@ -13,7 +13,6 @@ describe('Rollback Component', () => { ...@@ -13,7 +13,6 @@ describe('Rollback Component', () => {
isLastDeployment: true, isLastDeployment: true,
environment: {}, environment: {},
}, },
attachToDocument: true,
}); });
expect(wrapper.element).toHaveSpriteIcon('repeat'); expect(wrapper.element).toHaveSpriteIcon('repeat');
...@@ -26,7 +25,6 @@ describe('Rollback Component', () => { ...@@ -26,7 +25,6 @@ describe('Rollback Component', () => {
isLastDeployment: false, isLastDeployment: false,
environment: {}, environment: {},
}, },
attachToDocument: true,
}); });
expect(wrapper.element).toHaveSpriteIcon('redo'); expect(wrapper.element).toHaveSpriteIcon('redo');
......
...@@ -11,7 +11,6 @@ describe('Stop Component', () => { ...@@ -11,7 +11,6 @@ describe('Stop Component', () => {
const createWrapper = () => { const createWrapper = () => {
wrapper = shallowMount(StopComponent, { wrapper = shallowMount(StopComponent, {
attachToDocument: true,
propsData: { propsData: {
environment: {}, environment: {},
}, },
......
...@@ -7,7 +7,6 @@ describe('Stop Component', () => { ...@@ -7,7 +7,6 @@ describe('Stop Component', () => {
const mountWithProps = props => { const mountWithProps = props => {
wrapper = shallowMount(TerminalComponent, { wrapper = shallowMount(TerminalComponent, {
attachToDocument: true,
propsData: props, propsData: props,
}); });
}; };
......
...@@ -59,7 +59,6 @@ describe('Time series component', () => { ...@@ -59,7 +59,6 @@ describe('Time series component', () => {
default: mockWidgets, default: mockWidgets,
}, },
store, store,
attachToDocument: true,
}); });
}); });
......
...@@ -63,7 +63,7 @@ describe('Dashboard', () => { ...@@ -63,7 +63,7 @@ describe('Dashboard', () => {
beforeEach(() => { beforeEach(() => {
mock.onGet(mockApiEndpoint).reply(statusCodes.OK, metricsGroupsAPIResponse); mock.onGet(mockApiEndpoint).reply(statusCodes.OK, metricsGroupsAPIResponse);
createShallowWrapper({}, { attachToDocument: true }); createShallowWrapper();
}); });
afterEach(() => { afterEach(() => {
...@@ -79,7 +79,7 @@ describe('Dashboard', () => { ...@@ -79,7 +79,7 @@ describe('Dashboard', () => {
beforeEach(done => { beforeEach(done => {
mock.onGet(mockApiEndpoint).reply(statusCodes.OK, metricsGroupsAPIResponse); mock.onGet(mockApiEndpoint).reply(statusCodes.OK, metricsGroupsAPIResponse);
createShallowWrapper({}, { attachToDocument: true }); createShallowWrapper();
wrapper.vm.$nextTick(done); wrapper.vm.$nextTick(done);
}); });
...@@ -99,7 +99,7 @@ describe('Dashboard', () => { ...@@ -99,7 +99,7 @@ describe('Dashboard', () => {
}); });
it('shows up a loading state', done => { it('shows up a loading state', done => {
createShallowWrapper({ hasMetrics: true }, { attachToDocument: true }); createShallowWrapper({ hasMetrics: true });
wrapper.vm wrapper.vm
.$nextTick() .$nextTick()
...@@ -114,7 +114,7 @@ describe('Dashboard', () => { ...@@ -114,7 +114,7 @@ describe('Dashboard', () => {
it('hides the group panels when showPanels is false', done => { it('hides the group panels when showPanels is false', done => {
createMountedWrapper( createMountedWrapper(
{ hasMetrics: true, showPanels: false }, { hasMetrics: true, showPanels: false },
{ attachToDocument: true, stubs: ['graph-group', 'panel-type'] }, { stubs: ['graph-group', 'panel-type'] },
); );
setupComponentStore(wrapper); setupComponentStore(wrapper);
...@@ -136,10 +136,7 @@ describe('Dashboard', () => { ...@@ -136,10 +136,7 @@ describe('Dashboard', () => {
it('fetches the metrics data with proper time window', done => { it('fetches the metrics data with proper time window', done => {
jest.spyOn(store, 'dispatch'); jest.spyOn(store, 'dispatch');
createMountedWrapper( createMountedWrapper({ hasMetrics: true }, { stubs: ['graph-group', 'panel-type'] });
{ hasMetrics: true },
{ attachToDocument: true, stubs: ['graph-group', 'panel-type'] },
);
wrapper.vm.$store.commit( wrapper.vm.$store.commit(
`monitoringDashboard/${types.RECEIVE_ENVIRONMENTS_DATA_SUCCESS}`, `monitoringDashboard/${types.RECEIVE_ENVIRONMENTS_DATA_SUCCESS}`,
...@@ -161,10 +158,7 @@ describe('Dashboard', () => { ...@@ -161,10 +158,7 @@ describe('Dashboard', () => {
beforeEach(() => { beforeEach(() => {
mock.onGet(mockApiEndpoint).reply(statusCodes.OK, metricsGroupsAPIResponse); mock.onGet(mockApiEndpoint).reply(statusCodes.OK, metricsGroupsAPIResponse);
createMountedWrapper( createMountedWrapper({ hasMetrics: true }, { stubs: ['graph-group', 'panel-type'] });
{ hasMetrics: true },
{ attachToDocument: true, stubs: ['graph-group', 'panel-type'] },
);
setupComponentStore(wrapper); setupComponentStore(wrapper);
}); });
...@@ -216,10 +210,7 @@ describe('Dashboard', () => { ...@@ -216,10 +210,7 @@ describe('Dashboard', () => {
}); });
it('hides the environments dropdown list when there is no environments', done => { it('hides the environments dropdown list when there is no environments', done => {
createMountedWrapper( createMountedWrapper({ hasMetrics: true }, { stubs: ['graph-group', 'panel-type'] });
{ hasMetrics: true },
{ attachToDocument: true, stubs: ['graph-group', 'panel-type'] },
);
wrapper.vm.$store.commit( wrapper.vm.$store.commit(
`monitoringDashboard/${types.RECEIVE_METRICS_DATA_SUCCESS}`, `monitoringDashboard/${types.RECEIVE_METRICS_DATA_SUCCESS}`,
...@@ -244,10 +235,7 @@ describe('Dashboard', () => { ...@@ -244,10 +235,7 @@ describe('Dashboard', () => {
}); });
it('renders the datetimepicker dropdown', done => { it('renders the datetimepicker dropdown', done => {
createMountedWrapper( createMountedWrapper({ hasMetrics: true }, { stubs: ['graph-group', 'panel-type'] });
{ hasMetrics: true },
{ attachToDocument: true, stubs: ['graph-group', 'panel-type'] },
);
setupComponentStore(wrapper); setupComponentStore(wrapper);
...@@ -264,7 +252,7 @@ describe('Dashboard', () => { ...@@ -264,7 +252,7 @@ describe('Dashboard', () => {
beforeEach(done => { beforeEach(done => {
mock.onGet(mockApiEndpoint).reply(200, metricsGroupsAPIResponse); mock.onGet(mockApiEndpoint).reply(200, metricsGroupsAPIResponse);
createShallowWrapper({ hasMetrics: true }, { attachToDocument: true }); createShallowWrapper({ hasMetrics: true });
setupComponentStore(wrapper); setupComponentStore(wrapper);
wrapper.vm.$nextTick(done); wrapper.vm.$nextTick(done);
...@@ -298,7 +286,7 @@ describe('Dashboard', () => { ...@@ -298,7 +286,7 @@ describe('Dashboard', () => {
}); });
beforeEach(done => { beforeEach(done => {
createShallowWrapper({ hasMetrics: true }, { attachToDocument: true }); createShallowWrapper({ hasMetrics: true });
setupComponentStore(wrapper); setupComponentStore(wrapper);
...@@ -415,7 +403,7 @@ describe('Dashboard', () => { ...@@ -415,7 +403,7 @@ describe('Dashboard', () => {
beforeEach(done => { beforeEach(done => {
mock.onGet(mockApiEndpoint).reply(statusCodes.OK, metricsGroupsAPIResponse); mock.onGet(mockApiEndpoint).reply(statusCodes.OK, metricsGroupsAPIResponse);
createShallowWrapper({ hasMetrics: true }, { attachToDocument: true }); createShallowWrapper({ hasMetrics: true });
wrapper.vm.$store.commit( wrapper.vm.$store.commit(
`monitoringDashboard/${types.SET_ALL_DASHBOARDS}`, `monitoringDashboard/${types.SET_ALL_DASHBOARDS}`,
...@@ -452,10 +440,7 @@ describe('Dashboard', () => { ...@@ -452,10 +440,7 @@ describe('Dashboard', () => {
beforeEach(() => { beforeEach(() => {
mock.onGet(mockApiEndpoint).reply(200, metricsGroupsAPIResponse); mock.onGet(mockApiEndpoint).reply(200, metricsGroupsAPIResponse);
createMountedWrapper( createMountedWrapper({ hasMetrics: true }, { stubs: ['graph-group', 'panel-type'] });
{ hasMetrics: true },
{ attachToDocument: true, stubs: ['graph-group', 'panel-type'] },
);
wrapper.vm.$store.commit( wrapper.vm.$store.commit(
`monitoringDashboard/${types.SET_ALL_DASHBOARDS}`, `monitoringDashboard/${types.SET_ALL_DASHBOARDS}`,
...@@ -487,7 +472,7 @@ describe('Dashboard', () => { ...@@ -487,7 +472,7 @@ describe('Dashboard', () => {
showTimeWindowDropdown: false, showTimeWindowDropdown: false,
externalDashboardUrl: '/mockUrl', externalDashboardUrl: '/mockUrl',
}, },
{ attachToDocument: true, stubs: ['graph-group', 'panel-type'] }, { stubs: ['graph-group', 'panel-type'] },
); );
}); });
...@@ -517,7 +502,7 @@ describe('Dashboard', () => { ...@@ -517,7 +502,7 @@ describe('Dashboard', () => {
beforeEach(done => { beforeEach(done => {
mock.onGet(mockApiEndpoint).reply(200, metricsGroupsAPIResponse); mock.onGet(mockApiEndpoint).reply(200, metricsGroupsAPIResponse);
createShallowWrapper({ hasMetrics: true, currentDashboard }, { attachToDocument: true }); createShallowWrapper({ hasMetrics: true, currentDashboard });
setTimeout(done); setTimeout(done);
}); });
......
...@@ -38,10 +38,7 @@ describe('dashboard invalid url parameters', () => { ...@@ -38,10 +38,7 @@ describe('dashboard invalid url parameters', () => {
}); });
it('shows an error message if invalid url parameters are passed', done => { it('shows an error message if invalid url parameters are passed', done => {
createMountedWrapper( createMountedWrapper({ hasMetrics: true }, { stubs: ['graph-group', 'panel-type'] });
{ hasMetrics: true },
{ attachToDocument: true, stubs: ['graph-group', 'panel-type'] },
);
wrapper.vm wrapper.vm
.$nextTick() .$nextTick()
......
...@@ -45,10 +45,7 @@ describe('dashboard time window', () => { ...@@ -45,10 +45,7 @@ describe('dashboard time window', () => {
it('shows an error message if invalid url parameters are passed', done => { it('shows an error message if invalid url parameters are passed', done => {
mock.onGet(mockApiEndpoint).reply(statusCodes.OK, metricsGroupsAPIResponse); mock.onGet(mockApiEndpoint).reply(statusCodes.OK, metricsGroupsAPIResponse);
createComponentWrapperMounted( createComponentWrapperMounted({ hasMetrics: true }, { stubs: ['graph-group', 'panel-type'] });
{ hasMetrics: true },
{ attachToDocument: true, stubs: ['graph-group', 'panel-type'] },
);
setupComponentStore(wrapper); setupComponentStore(wrapper);
......
...@@ -26,7 +26,6 @@ describe('Panel Type component', () => { ...@@ -26,7 +26,6 @@ describe('Panel Type component', () => {
...props, ...props,
}, },
store, store,
attachToDocument: true,
}); });
beforeEach(() => { beforeEach(() => {
...@@ -151,7 +150,6 @@ describe('Panel Type component', () => { ...@@ -151,7 +150,6 @@ describe('Panel Type component', () => {
graphData: graphDataPrometheusQueryRange, graphData: graphDataPrometheusQueryRange,
}, },
store, store,
attachToDocument: true,
}); });
panelType.vm.$nextTick(done); panelType.vm.$nextTick(done);
}); });
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
require 'spec_helper' require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20190924152703_migrate_issue_trackers_data.rb') require Rails.root.join('db', 'post_migrate', '20190924152703_migrate_issue_trackers_data.rb')
describe MigrateIssueTrackersData, :migration do describe MigrateIssueTrackersData, :migration, :sidekiq do
let(:services) { table(:services) } let(:services) { table(:services) }
let(:migration_class) { Gitlab::BackgroundMigration::MigrateIssueTrackersSensitiveData } let(:migration_class) { Gitlab::BackgroundMigration::MigrateIssueTrackersSensitiveData }
let(:migration_name) { migration_class.to_s.demodulize } let(:migration_name) { migration_class.to_s.demodulize }
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment