From 88dceb9363e4b9eaee5a938eb75f613261353796 Mon Sep 17 00:00:00 2001 From: Thomas Randolph Date: Mon, 10 Mar 2025 18:45:16 -0600 Subject: [PATCH 1/6] Add telemetry event to opening the reviewer sidebar panel --- .../reviewers/sidebar_reviewers.vue | 3 +++ .../open_reviewer_sidebar_panel_in_mr.yml | 16 ++++++++++++++ ...otal_open_reviewer_sidebar_panel_in_mr.yml | 22 +++++++++++++++++++ 3 files changed, 41 insertions(+) create mode 100644 ee/config/events/open_reviewer_sidebar_panel_in_mr.yml create mode 100644 ee/config/metrics/counts_all/count_total_open_reviewer_sidebar_panel_in_mr.yml diff --git a/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue b/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue index 09ae9a38cdfe28..ad70563bc0b05e 100644 --- a/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue +++ b/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue @@ -7,6 +7,7 @@ import { GlButton } from '@gitlab/ui'; import { createAlert } from '~/alert'; import { TYPE_ISSUE } from '~/issues/constants'; import { __ } from '~/locale'; +import { InternalEvents } from '~/tracking'; import { isGid, getIdFromGraphQLId } from '~/graphql_shared/utils'; import { fetchUserCounts } from '~/super_sidebar/user_counts_fetch'; import ReviewerDrawer from '~/merge_requests/components/reviewers/reviewer_drawer.vue'; @@ -35,6 +36,7 @@ export default { ApprovalSummary: () => import('ee_component/merge_requests/components/reviewers/approval_summary.vue'), }, + mixins: [InternalEvents.mixin()], props: { mediator: { type: Object, @@ -190,6 +192,7 @@ export default { } }, toggleDrawerOpen(drawerOpen = !this.drawerOpen) { + this.trackEvent('open_reviewer_sidebar_panel_in_mr'); this.drawerOpen = drawerOpen; }, }, diff --git a/ee/config/events/open_reviewer_sidebar_panel_in_mr.yml b/ee/config/events/open_reviewer_sidebar_panel_in_mr.yml new file mode 100644 index 00000000000000..8a945025371641 --- /dev/null +++ b/ee/config/events/open_reviewer_sidebar_panel_in_mr.yml @@ -0,0 +1,16 @@ +--- +description: User opens the reviewer sidebar panel on an MR +internal_events: true +action: open_reviewer_sidebar_panel_in_mr +identifiers: +- project +- namespace +- user +product_group: code_review +product_categories: +- code_review_workflow +milestone: '17.10' +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/183774 +tiers: +- premium +- ultimate diff --git a/ee/config/metrics/counts_all/count_total_open_reviewer_sidebar_panel_in_mr.yml b/ee/config/metrics/counts_all/count_total_open_reviewer_sidebar_panel_in_mr.yml new file mode 100644 index 00000000000000..6d5d4150e48468 --- /dev/null +++ b/ee/config/metrics/counts_all/count_total_open_reviewer_sidebar_panel_in_mr.yml @@ -0,0 +1,22 @@ +--- +key_path: counts.count_total_open_reviewer_sidebar_panel_in_mr +description: Count of the reviewer sidebar panel in an MR being opened by a user +product_group: code_review +product_categories: +- code_review_workflow +performance_indicator_type: [] +value_type: number +status: active +milestone: '17.10' +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/183774 +time_frame: +- 28d +- 7d +- all +data_source: internal_events +data_category: optional +tiers: +- premium +- ultimate +events: +- name: open_reviewer_sidebar_panel_in_mr -- GitLab From 9c2d8d317c8ef93fd79d25db734a974e111f09e8 Mon Sep 17 00:00:00 2001 From: Thomas Randolph Date: Mon, 10 Mar 2025 20:04:35 -0600 Subject: [PATCH 2/6] Test that the event is sent on premium/ultimate instances --- .../reviewers/sidebar_reviewers.vue | 2 +- .../reviewers/sidebar_reviewers_spec.js | 83 +++++++++++++++++++ .../reviewers/sidebar_reviewers_spec.js | 12 ++- 3 files changed, 92 insertions(+), 5 deletions(-) create mode 100644 ee/spec/frontend/sidebar/components/reviewers/sidebar_reviewers_spec.js diff --git a/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue b/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue index ad70563bc0b05e..01bc1ed1be3fe9 100644 --- a/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue +++ b/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue @@ -216,7 +216,7 @@ export default { category="tertiary" variant="confirm" class="gl-ml-2 !gl-text-sm" - data-testid="sidebar-reviewers-assign-buton" + data-testid="sidebar-reviewers-assign-button" @click="toggleDrawerOpen()" > {{ __('Assign') }} diff --git a/ee/spec/frontend/sidebar/components/reviewers/sidebar_reviewers_spec.js b/ee/spec/frontend/sidebar/components/reviewers/sidebar_reviewers_spec.js new file mode 100644 index 00000000000000..c7531a091c8409 --- /dev/null +++ b/ee/spec/frontend/sidebar/components/reviewers/sidebar_reviewers_spec.js @@ -0,0 +1,83 @@ +import { GlButton } from '@gitlab/ui'; +import Vue from 'vue'; +import axios from 'axios'; +import AxiosMockAdapter from 'axios-mock-adapter'; +import VueApollo from 'vue-apollo'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import { useMockInternalEventsTracking } from 'helpers/tracking_internal_events_helper'; +import SidebarReviewers from '~/sidebar/components/reviewers/sidebar_reviewers.vue'; +import SidebarMediator from '~/sidebar/sidebar_mediator'; + +const { bindInternalEventDocument } = useMockInternalEventsTracking(); + +Vue.use(VueApollo); + +function findAssignButton(wrapper) { + return wrapper.findByTestId('sidebar-reviewers-assign-button'); +} + +describe('sidebar reviewers', () => { + const apolloMock = createMockApollo(); + let trackEventSpy; + let wrapper; + let mediator; + let axiosMock; + + const createComponent = ({ props = {}, data = {} } = {}) => { + const defaultData = { ...data }; + + wrapper = shallowMountExtended(SidebarReviewers, { + apolloProvider: apolloMock, + propsData: { + issuableIid: '1', + issuableId: 1, + mediator, + field: '', + projectPath: 'projectPath', + changing: false, + ...props, + }, + data() { + return defaultData; + }, + provide: { + projectPath: 'projectPath', + issuableId: 1, + issuableIid: 1, + multipleApprovalRulesAvailable: false, + }, + stubs: { + ApprovalSummary: true, + GlButton, + }, + // Attaching to document is required because this component emits something from the parent element :/ + attachTo: document.body, + }); + + ({ trackEventSpy } = bindInternalEventDocument(wrapper.element)); + }; + + beforeEach(() => { + axiosMock = new AxiosMockAdapter(axios); + mediator = new SidebarMediator({ currentUser: {} }); + }); + + afterEach(() => { + axiosMock.restore(); + }); + + it('sends the telemetry event when the reviewers panel is opened', () => { + createComponent({ + data: { + issuable: { userPermissions: { adminMergeRequest: true } }, + }, + }); + + const assign = findAssignButton(wrapper); + + assign.trigger('click'); + + expect(trackEventSpy).toHaveBeenCalledWith('open_reviewer_sidebar_panel_in_mr', {}, undefined); + }); +}); diff --git a/spec/frontend/sidebar/components/reviewers/sidebar_reviewers_spec.js b/spec/frontend/sidebar/components/reviewers/sidebar_reviewers_spec.js index 2e9126da815585..6e95b5130e090f 100644 --- a/spec/frontend/sidebar/components/reviewers/sidebar_reviewers_spec.js +++ b/spec/frontend/sidebar/components/reviewers/sidebar_reviewers_spec.js @@ -1,9 +1,9 @@ -import { shallowMount } from '@vue/test-utils'; import Vue, { nextTick } from 'vue'; import axios from 'axios'; import AxiosMockAdapter from 'axios-mock-adapter'; import VueApollo from 'vue-apollo'; import createMockApollo from 'helpers/mock_apollo_helper'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import SidebarReviewers from '~/sidebar/components/reviewers/sidebar_reviewers.vue'; import SidebarService from '~/sidebar/services/sidebar_service'; import SidebarMediator from '~/sidebar/sidebar_mediator'; @@ -15,6 +15,10 @@ jest.mock('~/super_sidebar/user_counts_fetch'); Vue.use(VueApollo); +function findAssignButton(wrapper) { + return wrapper.findByTestId('sidebar-reviewers-assign-button'); +} + describe('sidebar reviewers', () => { const apolloMock = createMockApollo(); let wrapper; @@ -22,7 +26,7 @@ describe('sidebar reviewers', () => { let axiosMock; const createComponent = (props) => { - wrapper = shallowMount(SidebarReviewers, { + wrapper = shallowMountExtended(SidebarReviewers, { apolloProvider: apolloMock, propsData: { issuableIid: '1', @@ -67,7 +71,7 @@ describe('sidebar reviewers', () => { ${'shows'} | ${true} | ${true} ${'does not show'} | ${false} | ${false} `('$copy Assign button when canUpdate is $canUpdate', ({ canUpdate, expected }) => { - wrapper = shallowMount(SidebarReviewers, { + wrapper = shallowMountExtended(SidebarReviewers, { apolloProvider: apolloMock, propsData: { issuableIid: '1', @@ -93,7 +97,7 @@ describe('sidebar reviewers', () => { }, }); - expect(wrapper.find('[data-testid="sidebar-reviewers-assign-buton"]').exists()).toBe(expected); + expect(findAssignButton(wrapper).exists()).toBe(expected); }); it('calls the mediator when it saves the reviewers', () => { -- GitLab From 734331d986cd9379523adfee5f193c635f3ccd4b Mon Sep 17 00:00:00 2001 From: Thomas Randolph Date: Mon, 10 Mar 2025 20:29:31 -0600 Subject: [PATCH 3/6] Only send the tracking event when the drawer has been opened --- .../sidebar/components/reviewers/sidebar_reviewers.vue | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue b/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue index 01bc1ed1be3fe9..aeeb2654836b05 100644 --- a/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue +++ b/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue @@ -192,8 +192,11 @@ export default { } }, toggleDrawerOpen(drawerOpen = !this.drawerOpen) { - this.trackEvent('open_reviewer_sidebar_panel_in_mr'); this.drawerOpen = drawerOpen; + + if (drawerOpen) { + this.trackEvent('open_reviewer_sidebar_panel_in_mr'); + } }, }, }; -- GitLab From dec3f78a6a8e2a932230d831a0648d4caa83ef45 Mon Sep 17 00:00:00 2001 From: Thomas Randolph Date: Tue, 11 Mar 2025 11:38:52 -0600 Subject: [PATCH 4/6] Switch from shallowMount to full mount with stubs --- .../components/reviewers/sidebar_reviewers_spec.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/ee/spec/frontend/sidebar/components/reviewers/sidebar_reviewers_spec.js b/ee/spec/frontend/sidebar/components/reviewers/sidebar_reviewers_spec.js index c7531a091c8409..ba5683fd79fd43 100644 --- a/ee/spec/frontend/sidebar/components/reviewers/sidebar_reviewers_spec.js +++ b/ee/spec/frontend/sidebar/components/reviewers/sidebar_reviewers_spec.js @@ -1,10 +1,10 @@ -import { GlButton } from '@gitlab/ui'; import Vue from 'vue'; import axios from 'axios'; import AxiosMockAdapter from 'axios-mock-adapter'; import VueApollo from 'vue-apollo'; import createMockApollo from 'helpers/mock_apollo_helper'; -import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import stubChildren from 'helpers/stub_children'; +import { mountExtended } from 'helpers/vue_test_utils_helper'; import { useMockInternalEventsTracking } from 'helpers/tracking_internal_events_helper'; import SidebarReviewers from '~/sidebar/components/reviewers/sidebar_reviewers.vue'; import SidebarMediator from '~/sidebar/sidebar_mediator'; @@ -27,7 +27,7 @@ describe('sidebar reviewers', () => { const createComponent = ({ props = {}, data = {} } = {}) => { const defaultData = { ...data }; - wrapper = shallowMountExtended(SidebarReviewers, { + wrapper = mountExtended(SidebarReviewers, { apolloProvider: apolloMock, propsData: { issuableIid: '1', @@ -48,8 +48,8 @@ describe('sidebar reviewers', () => { multipleApprovalRulesAvailable: false, }, stubs: { - ApprovalSummary: true, - GlButton, + ...stubChildren(SidebarReviewers), + GlButton: false, }, // Attaching to document is required because this component emits something from the parent element :/ attachTo: document.body, -- GitLab From bf29e8de01fca8bbe7d150d66b8ec1fbfd5fb77f Mon Sep 17 00:00:00 2001 From: Thomas Randolph Date: Wed, 12 Mar 2025 13:32:41 -0600 Subject: [PATCH 5/6] Switch tests to use arrow function --- .../components/reviewers/sidebar_reviewers_spec.js | 8 +++----- .../components/reviewers/sidebar_reviewers_spec.js | 8 +++----- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/ee/spec/frontend/sidebar/components/reviewers/sidebar_reviewers_spec.js b/ee/spec/frontend/sidebar/components/reviewers/sidebar_reviewers_spec.js index ba5683fd79fd43..64fbb4665b92ca 100644 --- a/ee/spec/frontend/sidebar/components/reviewers/sidebar_reviewers_spec.js +++ b/ee/spec/frontend/sidebar/components/reviewers/sidebar_reviewers_spec.js @@ -13,10 +13,6 @@ const { bindInternalEventDocument } = useMockInternalEventsTracking(); Vue.use(VueApollo); -function findAssignButton(wrapper) { - return wrapper.findByTestId('sidebar-reviewers-assign-button'); -} - describe('sidebar reviewers', () => { const apolloMock = createMockApollo(); let trackEventSpy; @@ -24,6 +20,8 @@ describe('sidebar reviewers', () => { let mediator; let axiosMock; + const findAssignButton = () => wrapper.findByTestId('sidebar-reviewers-assign-button'); + const createComponent = ({ props = {}, data = {} } = {}) => { const defaultData = { ...data }; @@ -74,7 +72,7 @@ describe('sidebar reviewers', () => { }, }); - const assign = findAssignButton(wrapper); + const assign = findAssignButton(); assign.trigger('click'); diff --git a/spec/frontend/sidebar/components/reviewers/sidebar_reviewers_spec.js b/spec/frontend/sidebar/components/reviewers/sidebar_reviewers_spec.js index 6e95b5130e090f..c541f0357bcc69 100644 --- a/spec/frontend/sidebar/components/reviewers/sidebar_reviewers_spec.js +++ b/spec/frontend/sidebar/components/reviewers/sidebar_reviewers_spec.js @@ -15,16 +15,14 @@ jest.mock('~/super_sidebar/user_counts_fetch'); Vue.use(VueApollo); -function findAssignButton(wrapper) { - return wrapper.findByTestId('sidebar-reviewers-assign-button'); -} - describe('sidebar reviewers', () => { const apolloMock = createMockApollo(); let wrapper; let mediator; let axiosMock; + const findAssignButton = () => wrapper.findByTestId('sidebar-reviewers-assign-button'); + const createComponent = (props) => { wrapper = shallowMountExtended(SidebarReviewers, { apolloProvider: apolloMock, @@ -97,7 +95,7 @@ describe('sidebar reviewers', () => { }, }); - expect(findAssignButton(wrapper).exists()).toBe(expected); + expect(findAssignButton().exists()).toBe(expected); }); it('calls the mediator when it saves the reviewers', () => { -- GitLab From df9e08bb1da69d168b3149284c3691ea3a843e75 Mon Sep 17 00:00:00 2001 From: Thomas Randolph Date: Thu, 13 Mar 2025 13:56:01 -0600 Subject: [PATCH 6/6] Mock apollo data so we don't have to mock data data --- .../reviewers/sidebar_reviewers_spec.js | 33 +++++++++++-------- ee/spec/frontend/sidebar/mock_data.js | 32 ++++++++++++++++++ 2 files changed, 52 insertions(+), 13 deletions(-) diff --git a/ee/spec/frontend/sidebar/components/reviewers/sidebar_reviewers_spec.js b/ee/spec/frontend/sidebar/components/reviewers/sidebar_reviewers_spec.js index 64fbb4665b92ca..f60c057e0ed9d1 100644 --- a/ee/spec/frontend/sidebar/components/reviewers/sidebar_reviewers_spec.js +++ b/ee/spec/frontend/sidebar/components/reviewers/sidebar_reviewers_spec.js @@ -2,29 +2,42 @@ import Vue from 'vue'; import axios from 'axios'; import AxiosMockAdapter from 'axios-mock-adapter'; import VueApollo from 'vue-apollo'; +import { createMockSubscription as createMockApolloSubscription } from 'mock-apollo-client'; import createMockApollo from 'helpers/mock_apollo_helper'; import stubChildren from 'helpers/stub_children'; import { mountExtended } from 'helpers/vue_test_utils_helper'; +import waitForPromises from 'helpers/wait_for_promises'; import { useMockInternalEventsTracking } from 'helpers/tracking_internal_events_helper'; +import getMergeRequestReviewersQuery from '~/sidebar/queries/get_merge_request_reviewers.query.graphql'; +import mergeRequestReviewersUpdatedSubscription from '~/sidebar/queries/merge_request_reviewers.subscription.graphql'; import SidebarReviewers from '~/sidebar/components/reviewers/sidebar_reviewers.vue'; import SidebarMediator from '~/sidebar/sidebar_mediator'; +import { mockGetMergeRequestReviewers } from '../../mock_data'; const { bindInternalEventDocument } = useMockInternalEventsTracking(); Vue.use(VueApollo); describe('sidebar reviewers', () => { - const apolloMock = createMockApollo(); + const mockGQLQueries = [ + [getMergeRequestReviewersQuery, jest.fn().mockResolvedValue(mockGetMergeRequestReviewers)], + ]; + + const mockedSubscription = createMockApolloSubscription(); + const apolloMock = createMockApollo(mockGQLQueries); let trackEventSpy; let wrapper; let mediator; let axiosMock; - const findAssignButton = () => wrapper.findByTestId('sidebar-reviewers-assign-button'); + apolloMock.defaultClient.setRequestHandler( + mergeRequestReviewersUpdatedSubscription, + () => mockedSubscription, + ); - const createComponent = ({ props = {}, data = {} } = {}) => { - const defaultData = { ...data }; + const findAssignButton = () => wrapper.findByTestId('sidebar-reviewers-assign-button'); + const createComponent = ({ props = {} } = {}) => { wrapper = mountExtended(SidebarReviewers, { apolloProvider: apolloMock, propsData: { @@ -36,9 +49,6 @@ describe('sidebar reviewers', () => { changing: false, ...props, }, - data() { - return defaultData; - }, provide: { projectPath: 'projectPath', issuableId: 1, @@ -65,12 +75,9 @@ describe('sidebar reviewers', () => { axiosMock.restore(); }); - it('sends the telemetry event when the reviewers panel is opened', () => { - createComponent({ - data: { - issuable: { userPermissions: { adminMergeRequest: true } }, - }, - }); + it('sends the telemetry event when the reviewers panel is opened', async () => { + createComponent(); + await waitForPromises(); const assign = findAssignButton(); diff --git a/ee/spec/frontend/sidebar/mock_data.js b/ee/spec/frontend/sidebar/mock_data.js index 35a4f8325785d8..c3ea18e298f917 100644 --- a/ee/spec/frontend/sidebar/mock_data.js +++ b/ee/spec/frontend/sidebar/mock_data.js @@ -447,3 +447,35 @@ export const getHealthStatusQueryResponse = ({ state = 'opened', healthStatus = }, }; }; + +export const mockGetMergeRequestReviewers = { + data: { + workspace: { + id: 'gid://gitlab/Project/1', + issuable: { + id: 'gid://gitlab/MergeRequest/1', + reviewers: { + nodes: [ + { + id: 'gid://gitlab/User/1', + avatarUrl: 'image', + name: 'User', + username: 'username', + webUrl: 'https://example.com/username', + webPath: '/username', + status: null, + mergeRequestInteraction: { + canMerge: true, + canUpdate: true, + approved: true, + reviewState: 'APPROVED', + applicableApprovalRules: [{ id: 'gid://gitlab/ApprovalMergeRequestRule/1' }], + }, + }, + ], + }, + userPermissions: { adminMergeRequest: true }, + }, + }, + }, +}; -- GitLab