From 9799a28fad377b3f9b076a320827fba2c05bf4d4 Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Mon, 12 Jun 2023 16:52:08 +0100 Subject: [PATCH] Nested folders: add analytics tracking for some features (#69901) * add analytics for folder creation * add interaction tracking for move/delete * add tracking for item clicked in the new browse view * review comments * emit counts instead --- .../BrowseDashboardsPage.tsx | 3 +- .../BrowseActions/BrowseActions.tsx | 19 +++++++ .../components/CreateNewButton.test.tsx | 28 +++++++---- .../components/CreateNewButton.tsx | 49 +++++++++++++------ .../components/FolderActionsButton.test.tsx | 21 ++------ .../components/FolderActionsButton.tsx | 16 +++++- .../browse-dashboards/components/NameCell.tsx | 9 +++- .../fixtures/folder.fixture.ts | 25 ++++++++++ 8 files changed, 123 insertions(+), 47 deletions(-) create mode 100644 public/app/features/browse-dashboards/fixtures/folder.fixture.ts diff --git a/public/app/features/browse-dashboards/BrowseDashboardsPage.tsx b/public/app/features/browse-dashboards/BrowseDashboardsPage.tsx index e2c1700633c..0401dd92c60 100644 --- a/public/app/features/browse-dashboards/BrowseDashboardsPage.tsx +++ b/public/app/features/browse-dashboards/BrowseDashboardsPage.tsx @@ -104,8 +104,7 @@ const BrowseDashboardsPage = memo(({ match }: Props) => { {folderDTO && } {(canCreateDashboards || canCreateFolder) && ( diff --git a/public/app/features/browse-dashboards/components/BrowseActions/BrowseActions.tsx b/public/app/features/browse-dashboards/components/BrowseActions/BrowseActions.tsx index a42c5ef9f97..28514bd6708 100644 --- a/public/app/features/browse-dashboards/components/BrowseActions/BrowseActions.tsx +++ b/public/app/features/browse-dashboards/components/BrowseActions/BrowseActions.tsx @@ -2,6 +2,7 @@ import { css } from '@emotion/css'; import React from 'react'; import { GrafanaTheme2 } from '@grafana/data'; +import { reportInteraction } from '@grafana/runtime'; import { Button, useStyles2 } from '@grafana/ui'; import appEvents from 'app/core/app_events'; import { useSearchStateManager } from 'app/features/search/state/SearchStateManager'; @@ -73,6 +74,7 @@ export function BrowseActions() { const dashboard = findItem(rootItems?.items ?? [], childrenByParentUID, dashboardUID); parentsToRefresh.add(dashboard?.parentUID); } + trackAction('delete', selectedDashboards, selectedFolders); onActionComplete(parentsToRefresh); }; @@ -97,6 +99,7 @@ export function BrowseActions() { const dashboard = findItem(rootItems?.items ?? [], childrenByParentUID, dashboardUID); parentsToRefresh.add(dashboard?.parentUID); } + trackAction('move', selectedDashboards, selectedFolders); onActionComplete(parentsToRefresh); }; @@ -144,3 +147,19 @@ const getStyles = (theme: GrafanaTheme2) => ({ marginBottom: theme.spacing(2), }), }); + +type actionType = 'move' | 'delete'; +const actionMap: Record = { + move: 'grafana_manage_dashboards_item_moved', + delete: 'grafana_manage_dashboards_item_deleted', +}; + +function trackAction(action: actionType, selectedDashboards: string[], selectedFolders: string[]) { + reportInteraction(actionMap[action], { + item_counts: { + folder: selectedFolders.length, + dashboard: selectedDashboards.length, + }, + source: 'tree_actions', + }); +} diff --git a/public/app/features/browse-dashboards/components/CreateNewButton.test.tsx b/public/app/features/browse-dashboards/components/CreateNewButton.test.tsx index d5811f9903d..2b6a1093aab 100644 --- a/public/app/features/browse-dashboards/components/CreateNewButton.test.tsx +++ b/public/app/features/browse-dashboards/components/CreateNewButton.test.tsx @@ -3,27 +3,36 @@ import userEvent from '@testing-library/user-event'; import React from 'react'; import { TestProvider } from 'test/helpers/TestProvider'; +import { FolderDTO } from 'app/types'; + +import { mockFolderDTO } from '../fixtures/folder.fixture'; + import CreateNewButton from './CreateNewButton'; +const mockParentFolder = mockFolderDTO(); + function render(...[ui, options]: Parameters) { rtlRender({ui}, options); } -async function renderAndOpen(folderUID?: string) { - render(); +async function renderAndOpen(folder?: FolderDTO) { + render(); const newButton = screen.getByText('New'); await userEvent.click(newButton); } describe('NewActionsButton', () => { - it('should display the correct urls with a given folderUID', async () => { - await renderAndOpen('123'); + it('should display the correct urls with a given parent folder', async () => { + await renderAndOpen(mockParentFolder); - expect(screen.getByText('New dashboard')).toHaveAttribute('href', '/dashboard/new?folderUid=123'); - expect(screen.getByText('Import')).toHaveAttribute('href', '/dashboard/import?folderUid=123'); + expect(screen.getByText('New dashboard')).toHaveAttribute( + 'href', + `/dashboard/new?folderUid=${mockParentFolder.uid}` + ); + expect(screen.getByText('Import')).toHaveAttribute('href', `/dashboard/import?folderUid=${mockParentFolder.uid}`); }); - it('should display urls without params when there is no folderUID', async () => { + it('should display urls without params when there is no parent folder', async () => { await renderAndOpen(); expect(screen.getByText('New dashboard')).toHaveAttribute('href', '/dashboard/new'); @@ -31,8 +40,7 @@ describe('NewActionsButton', () => { }); it('clicking the "New folder" button opens the drawer', async () => { - const mockParentFolderTitle = 'mockParentFolderTitle'; - render(); + render(); const newButton = screen.getByText('New'); await userEvent.click(newButton); @@ -41,7 +49,7 @@ describe('NewActionsButton', () => { const drawer = screen.getByRole('dialog', { name: 'Drawer title New folder' }); expect(drawer).toBeInTheDocument(); expect(within(drawer).getByRole('heading', { name: 'New folder' })).toBeInTheDocument(); - expect(within(drawer).getByText(`Location: ${mockParentFolderTitle}`)).toBeInTheDocument(); + expect(within(drawer).getByText(`Location: ${mockParentFolder.title}`)).toBeInTheDocument(); }); it('should only render dashboard items when folder creation is disabled', async () => { diff --git a/public/app/features/browse-dashboards/components/CreateNewButton.tsx b/public/app/features/browse-dashboards/components/CreateNewButton.tsx index 6d9b1367313..cfc8f8d3822 100644 --- a/public/app/features/browse-dashboards/components/CreateNewButton.tsx +++ b/public/app/features/browse-dashboards/components/CreateNewButton.tsx @@ -1,6 +1,8 @@ import React, { useState } from 'react'; import { connect, ConnectedProps } from 'react-redux'; +import { useLocation } from 'react-router-dom'; +import { reportInteraction } from '@grafana/runtime'; import { Button, Drawer, Dropdown, Icon, Menu, MenuItem } from '@grafana/ui'; import { createNewFolder } from 'app/features/folders/state/actions'; import { @@ -9,6 +11,7 @@ import { getImportPhrase, getNewPhrase, } from 'app/features/search/tempI18nPhrases'; +import { FolderDTO } from 'app/types'; import { NewFolderForm } from './NewFolderForm'; @@ -19,40 +22,54 @@ const mapDispatchToProps = { const connector = connect(null, mapDispatchToProps); interface OwnProps { - parentFolderTitle?: string; - /** - * Pass a folder UID in which the dashboard or folder will be created - */ - parentFolderUid?: string; + parentFolder?: FolderDTO; canCreateFolder: boolean; canCreateDashboard: boolean; } type Props = OwnProps & ConnectedProps; -function CreateNewButton({ - parentFolderTitle, - parentFolderUid, - canCreateDashboard, - canCreateFolder, - createNewFolder, -}: Props) { +function CreateNewButton({ parentFolder, canCreateDashboard, canCreateFolder, createNewFolder }: Props) { const [isOpen, setIsOpen] = useState(false); + const location = useLocation(); const [showNewFolderDrawer, setShowNewFolderDrawer] = useState(false); const onCreateFolder = (folderName: string) => { - createNewFolder(folderName, parentFolderUid); + createNewFolder(folderName, parentFolder?.uid); + const depth = parentFolder?.parents ? parentFolder.parents.length + 1 : 0; + reportInteraction('grafana_manage_dashboards_folder_created', { + is_subfolder: Boolean(parentFolder?.uid), + folder_depth: depth, + }); setShowNewFolderDrawer(false); }; const newMenu = ( {canCreateDashboard && ( - + + reportInteraction('grafana_menu_item_clicked', { + url: addFolderUidToUrl('/dashboard/new', parentFolder?.uid), + from: location.pathname, + }) + } + url={addFolderUidToUrl('/dashboard/new', parentFolder?.uid)} + /> )} {canCreateFolder && setShowNewFolderDrawer(true)} label={getNewFolderPhrase()} />} {canCreateDashboard && ( - + + reportInteraction('grafana_menu_item_clicked', { + url: addFolderUidToUrl('/dashboard/import', parentFolder?.uid), + from: location.pathname, + }) + } + url={addFolderUidToUrl('/dashboard/import', parentFolder?.uid)} + /> )} ); @@ -68,7 +85,7 @@ function CreateNewButton({ {showNewFolderDrawer && ( setShowNewFolderDrawer(false)} size="sm" diff --git a/public/app/features/browse-dashboards/components/FolderActionsButton.test.tsx b/public/app/features/browse-dashboards/components/FolderActionsButton.test.tsx index 2551b3fa49a..6ad0f7ad5a0 100644 --- a/public/app/features/browse-dashboards/components/FolderActionsButton.test.tsx +++ b/public/app/features/browse-dashboards/components/FolderActionsButton.test.tsx @@ -4,9 +4,11 @@ import React from 'react'; import { TestProvider } from 'test/helpers/TestProvider'; import { appEvents, contextSrv } from 'app/core/core'; -import { AccessControlAction, FolderDTO } from 'app/types'; +import { AccessControlAction } from 'app/types'; import { ShowModalReactEvent } from 'app/types/events'; +import { mockFolderDTO } from '../fixtures/folder.fixture'; + import { DeleteModal } from './BrowseActions/DeleteModal'; import { MoveModal } from './BrowseActions/MoveModal'; import { FolderActionsButton } from './FolderActionsButton'; @@ -21,22 +23,7 @@ jest.mock('app/core/components/AccessControl', () => ({ })); describe('browse-dashboards FolderActionsButton', () => { - const mockFolder: FolderDTO = { - canAdmin: true, - canDelete: true, - canEdit: true, - canSave: true, - created: '', - createdBy: '', - hasAcl: true, - id: 1, - title: 'myFolder', - uid: '12345', - updated: '', - updatedBy: '', - url: '', - version: 1, - }; + const mockFolder = mockFolderDTO(); beforeEach(() => { jest.spyOn(contextSrv, 'hasPermission').mockReturnValue(true); diff --git a/public/app/features/browse-dashboards/components/FolderActionsButton.tsx b/public/app/features/browse-dashboards/components/FolderActionsButton.tsx index 52c3c0b8ddf..c6188b4023c 100644 --- a/public/app/features/browse-dashboards/components/FolderActionsButton.tsx +++ b/public/app/features/browse-dashboards/components/FolderActionsButton.tsx @@ -1,6 +1,6 @@ import React, { useState } from 'react'; -import { locationService } from '@grafana/runtime'; +import { locationService, reportInteraction } from '@grafana/runtime'; import { Button, Drawer, Dropdown, Icon, Menu, MenuItem } from '@grafana/ui'; import { Permissions } from 'app/core/components/AccessControl'; import { appEvents, contextSrv } from 'app/core/core'; @@ -30,6 +30,13 @@ export function FolderActionsButton({ folder }: Props) { const onMove = async (destinationUID: string) => { await moveFolder({ folderUID: folder.uid, destinationUID }); + reportInteraction('grafana_manage_dashboards_item_moved', { + item_counts: { + folder: 1, + dashboard: 0, + }, + source: 'folder_actions', + }); dispatch(refetchChildren({ parentUID: destinationUID, pageSize: destinationUID ? PAGE_SIZE : ROOT_PAGE_SIZE })); if (folder.parentUid) { @@ -41,6 +48,13 @@ export function FolderActionsButton({ folder }: Props) { const onDelete = async () => { await dispatch(deleteFolder(folder.uid)); + reportInteraction('grafana_manage_dashboards_item_deleted', { + item_counts: { + folder: 1, + dashboard: 0, + }, + source: 'folder_actions', + }); if (folder.parentUid) { dispatch( refetchChildren({ parentUID: folder.parentUid, pageSize: folder.parentUid ? PAGE_SIZE : ROOT_PAGE_SIZE }) diff --git a/public/app/features/browse-dashboards/components/NameCell.tsx b/public/app/features/browse-dashboards/components/NameCell.tsx index 86f48ced71e..0d40a7ff574 100644 --- a/public/app/features/browse-dashboards/components/NameCell.tsx +++ b/public/app/features/browse-dashboards/components/NameCell.tsx @@ -4,6 +4,7 @@ import Skeleton from 'react-loading-skeleton'; import { CellProps } from 'react-table'; import { GrafanaTheme2 } from '@grafana/data'; +import { reportInteraction } from '@grafana/runtime'; import { IconButton, Link, Spinner, useStyles2 } from '@grafana/ui'; import { getSvgSize } from '@grafana/ui/src/components/Icon/utils'; import { Span } from '@grafana/ui/src/unstable'; @@ -82,7 +83,13 @@ export function NameCell({ row: { original: data }, onFolderClick }: NameCellPro )} {item.url ? ( - + { + reportInteraction('manage_dashboards_result_clicked'); + }} + href={item.url} + className={styles.link} + > {item.title} ) : ( diff --git a/public/app/features/browse-dashboards/fixtures/folder.fixture.ts b/public/app/features/browse-dashboards/fixtures/folder.fixture.ts new file mode 100644 index 00000000000..14cb3a83652 --- /dev/null +++ b/public/app/features/browse-dashboards/fixtures/folder.fixture.ts @@ -0,0 +1,25 @@ +import { Chance } from 'chance'; + +import { FolderDTO } from 'app/types'; + +export function mockFolderDTO(seed = 1, partial?: Partial): FolderDTO { + const random = Chance(seed); + const uid = random.guid(); + return { + canAdmin: true, + canDelete: true, + canEdit: true, + canSave: true, + created: new Date(random.timestamp()).toISOString(), + createdBy: '', + hasAcl: true, + id: 1, + title: random.sentence({ words: 3 }), + uid, + updated: new Date(random.timestamp()).toISOString(), + updatedBy: '', + url: `/dashboards/f/${uid}`, + version: 1, + ...partial, + }; +}