From acadae7dde92d2922eb2dd371bbb54e7a6ee167b Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Tue, 25 Apr 2023 13:21:52 +0200 Subject: [PATCH] feat(core-component): capture sidebar item clicks Signed-off-by: Camila Belo --- .../src/layout/Sidebar/Items.test.tsx | 86 ++++++++++++++++--- .../src/layout/Sidebar/Items.tsx | 37 +++++++- 2 files changed, 109 insertions(+), 14 deletions(-) diff --git a/packages/core-components/src/layout/Sidebar/Items.test.tsx b/packages/core-components/src/layout/Sidebar/Items.test.tsx index eafbf3827e..9479180180 100644 --- a/packages/core-components/src/layout/Sidebar/Items.test.tsx +++ b/packages/core-components/src/layout/Sidebar/Items.test.tsx @@ -15,7 +15,11 @@ */ import React from 'react'; -import { renderInTestApp } from '@backstage/test-utils'; +import { + MockAnalyticsApi, + TestApiProvider, + renderInTestApp, +} from '@backstage/test-utils'; import { createEvent, fireEvent, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import HomeIcon from '@material-ui/icons/Home'; @@ -24,6 +28,7 @@ import { Sidebar } from './Bar'; import { SidebarItem, SidebarSearchField, SidebarExpandButton } from './Items'; import { renderHook } from '@testing-library/react-hooks'; import { makeStyles } from '@material-ui/core/styles'; +import { analyticsApiRef } from '@backstage/core-plugin-api'; const useStyles = makeStyles({ spotlight: { @@ -31,27 +36,50 @@ const useStyles = makeStyles({ }, }); +let analyticsApiMock: MockAnalyticsApi; + +const handleSidebarItemClick = jest.fn(); + async function renderSidebar() { const { result } = renderHook(() => useStyles()); await renderInTestApp( - - {}} to="/search" /> - - {}} - text="Create..." - className={result.current.spotlight} - /> - - , + + + {}} to="/search" /> + + + + + + + , ); await userEvent.hover(screen.getByTestId('sidebar-root')); } describe('Items', () => { beforeEach(async () => { + jest.clearAllMocks(); + analyticsApiMock = new MockAnalyticsApi(); await renderSidebar(); }); @@ -73,6 +101,40 @@ describe('Items', () => { await screen.findByRole('button', { name: /create/i }), ).toHaveStyle(`background-color: transparent`); }); + + it('should send button clicks to analytics', async () => { + await userEvent.click( + await screen.findByRole('button', { name: /create/i }), + ); + expect(handleSidebarItemClick).toHaveBeenCalledTimes(1); + expect(analyticsApiMock.getEvents()).toHaveLength(1); + expect(analyticsApiMock.getEvents()[0]).toMatchObject({ + action: 'click', + subject: 'Create...', + context: { routeRef: 'unknown', pluginId: 'root', extension: 'App' }, + attributes: { to: '/' }, + }); + }); + + it('should send link clicks to analytics', async () => { + await userEvent.click(await screen.findByRole('link', { name: /docs/i })); + expect(handleSidebarItemClick).toHaveBeenCalledTimes(1); + expect(analyticsApiMock.getEvents()).toHaveLength(1); + expect(analyticsApiMock.getEvents()[0]).toMatchObject({ + action: 'click', + subject: 'Docs', + context: { routeRef: 'unknown', pluginId: 'root', extension: 'App' }, + attributes: { to: '/docs' }, + }); + }); + + it('should not send clicks to analytics when tracking is disabled', async () => { + await userEvent.click( + await screen.findByRole('link', { name: /explore/i }), + ); + expect(handleSidebarItemClick).toHaveBeenCalledTimes(1); + expect(analyticsApiMock.getEvents()).toHaveLength(0); + }); }); describe('SidebarSearchField', () => { it('should be defaultPrevented when enter is pressed', async () => { diff --git a/packages/core-components/src/layout/Sidebar/Items.tsx b/packages/core-components/src/layout/Sidebar/Items.tsx index 00d1871947..96c2b382e7 100644 --- a/packages/core-components/src/layout/Sidebar/Items.tsx +++ b/packages/core-components/src/layout/Sidebar/Items.tsx @@ -13,7 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { IconComponent, useElementFilter } from '@backstage/core-plugin-api'; +import { + IconComponent, + useAnalytics, + useElementFilter, +} from '@backstage/core-plugin-api'; import { BackstageTheme } from '@backstage/theme'; import Badge from '@material-ui/core/Badge'; import Box from '@material-ui/core/Box'; @@ -38,6 +42,7 @@ import React, { forwardRef, KeyboardEventHandler, ReactNode, + useCallback, useContext, useMemo, useState, @@ -264,6 +269,8 @@ type SidebarItemBaseProps = { hasSubmenu?: boolean; disableHighlight?: boolean; className?: string; + noTrack?: boolean; + onClick?: (ev: React.MouseEvent) => void; }; type SidebarItemButtonProps = SidebarItemBaseProps & { @@ -362,6 +369,7 @@ const SidebarItemBase = forwardRef((props, ref) => { hasSubmenu = false, disableHighlight = false, onClick, + noTrack, children, className, ...navLinkProps @@ -424,9 +432,33 @@ const SidebarItemBase = forwardRef((props, ref) => { ), }; + const analyticsApi = useAnalytics(); + const { pathname: to } = useResolvedPath( + !isButtonItem(props) && props.to ? props.to : '', + ); + + const handleClick = useCallback( + (event: React.MouseEvent) => { + if (!noTrack) { + const action = 'click'; + const subject = text ?? 'Sidebar Item'; + const options = to ? { attributes: { to } } : undefined; + analyticsApi.captureEvent(action, subject, options); + } + onClick?.(event); + }, + [analyticsApi, text, to, noTrack, onClick], + ); + if (isButtonItem(props)) { return ( - ); @@ -440,6 +472,7 @@ const SidebarItemBase = forwardRef((props, ref) => { ref={ref} aria-label={text ? text : props.to} {...navLinkProps} + onClick={handleClick} > {content}