[SR] Instrument Snapshot Repositories with UI metrics (#36736) (#36933)

* Instrument Snapshot Repositories with UI metrics:
  * Repository list load
  * Repository create
  * Repository update
  * Repository delete, single
  * Repository delete, many
  * Repository show detail panel click
  * Repository detail panel verify repository button click
  * Snapshot list load
  * Snapshot show detail panel click
  * Snapshot detail panel summary tab click
  * Snapshot detail panel failed indices tab click
* Change detail panel click to link, add footer to snapshot details for close button
This commit is contained in:
Jen Huang 2019-05-23 11:38:02 -07:00 committed by GitHub
parent 25ea8d4ae3
commit 5298adaf78
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 177 additions and 26 deletions

View file

@ -30,3 +30,18 @@ export enum SNAPSHOT_STATE {
PARTIAL = 'PARTIAL',
INCOMPATIBLE = 'INCOMPATIBLE',
}
// UI Metric constants
export const UIM_APP_NAME = 'snapshot_restore';
export const UIM_REPOSITORY_LIST_LOAD = 'repository_list_load';
export const UIM_REPOSITORY_CREATE = 'repository_create';
export const UIM_REPOSITORY_UPDATE = 'repository_update';
export const UIM_REPOSITORY_DELETE = 'repository_delete';
export const UIM_REPOSITORY_DELETE_MANY = 'repository_delete_many';
export const UIM_REPOSITORY_SHOW_DETAILS_CLICK = 'repository_show_details_click';
export const UIM_REPOSITORY_DETAIL_PANEL_VERIFY = 'repository_detail_panel_verify';
export const UIM_SNAPSHOT_LIST_LOAD = 'snapshot_list_load';
export const UIM_SNAPSHOT_SHOW_DETAILS_CLICK = 'snapshot_show_details_click';
export const UIM_SNAPSHOT_DETAIL_PANEL_SUMMARY_TAB = 'snapshot_detail_panel_summary_tab';
export const UIM_SNAPSHOT_DETAIL_PANEL_FAILED_INDICES_TAB =
'snapshot_detail_panel_failed_indices_tab';

View file

@ -4,15 +4,16 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React, { Fragment } from 'react';
import React, { Fragment, useEffect } from 'react';
import { RouteComponentProps } from 'react-router-dom';
import { EuiButton, EuiEmptyPrompt } from '@elastic/eui';
import { Repository } from '../../../../../common/types';
import { SectionError, SectionLoading } from '../../../components';
import { BASE_PATH } from '../../../constants';
import { BASE_PATH, UIM_REPOSITORY_LIST_LOAD } from '../../../constants';
import { useAppDependencies } from '../../../index';
import { loadRepositories } from '../../../services/http';
import { uiMetricService } from '../../../services/ui_metric';
import { RepositoryDetails } from './repository_details';
import { RepositoryTable } from './repository_table';
@ -40,8 +41,10 @@ export const RepositoryList: React.FunctionComponent<RouteComponentProps<MatchPa
request: reload,
} = loadRepositories();
const openRepositoryDetails = (newRepositoryName: Repository['name']) => {
history.push(`${BASE_PATH}/repositories/${newRepositoryName}`);
const openRepositoryDetailsUrl = (newRepositoryName: Repository['name']): string => {
return history.createHref({
pathname: `${BASE_PATH}/repositories/${newRepositoryName}`,
});
};
const closeRepositoryDetails = () => {
@ -57,6 +60,12 @@ export const RepositoryList: React.FunctionComponent<RouteComponentProps<MatchPa
}
};
// Track component loaded
const { trackUiMetric } = uiMetricService;
useEffect(() => {
trackUiMetric(UIM_REPOSITORY_LIST_LOAD);
}, []);
let content;
if (loading) {
@ -124,7 +133,7 @@ export const RepositoryList: React.FunctionComponent<RouteComponentProps<MatchPa
<RepositoryTable
repositories={repositories || []}
reload={reload}
openRepositoryDetails={openRepositoryDetails}
openRepositoryDetailsUrl={openRepositoryDetailsUrl}
onRepositoryDeleted={onRepositoryDeleted}
/>
);

View file

@ -20,21 +20,22 @@ import {
import { REPOSITORY_TYPES } from '../../../../../../common/constants';
import { Repository, RepositoryType } from '../../../../../../common/types';
import { RepositoryDeleteProvider } from '../../../../components';
import { BASE_PATH } from '../../../../constants';
import { BASE_PATH, UIM_REPOSITORY_SHOW_DETAILS_CLICK } from '../../../../constants';
import { useAppDependencies } from '../../../../index';
import { textService } from '../../../../services/text';
import { uiMetricService } from '../../../../services/ui_metric';
interface Props extends RouteComponentProps {
repositories: Repository[];
reload: () => Promise<void>;
openRepositoryDetails: (name: Repository['name']) => void;
openRepositoryDetailsUrl: (name: Repository['name']) => string;
onRepositoryDeleted: (repositoriesDeleted: Array<Repository['name']>) => void;
}
const RepositoryTableUi: React.FunctionComponent<Props> = ({
repositories,
reload,
openRepositoryDetails,
openRepositoryDetailsUrl,
onRepositoryDeleted,
history,
}) => {
@ -42,6 +43,7 @@ const RepositoryTableUi: React.FunctionComponent<Props> = ({
core: { i18n },
} = useAppDependencies();
const { FormattedMessage } = i18n;
const { trackUiMetric } = uiMetricService;
const [selectedItems, setSelectedItems] = useState<Repository[]>([]);
const columns = [
@ -53,7 +55,14 @@ const RepositoryTableUi: React.FunctionComponent<Props> = ({
truncateText: true,
sortable: true,
render: (name: Repository['name'], repository: Repository) => {
return <EuiLink onClick={() => openRepositoryDetails(name)}>{name}</EuiLink>;
return (
<EuiLink
onClick={() => trackUiMetric(UIM_REPOSITORY_SHOW_DETAILS_CLICK)}
href={openRepositoryDetailsUrl(name)}
>
{name}
</EuiLink>
);
},
},
{

View file

@ -5,10 +5,12 @@
*/
import {
EuiButtonEmpty,
EuiFlexGroup,
EuiFlexItem,
EuiFlyout,
EuiFlyoutBody,
EuiFlyoutFooter,
EuiFlyoutHeader,
EuiLink,
EuiSpacer,
@ -22,8 +24,13 @@ import { RouteComponentProps, withRouter } from 'react-router-dom';
import { SectionError, SectionLoading } from '../../../../components';
import { useAppDependencies } from '../../../../index';
import {
UIM_SNAPSHOT_DETAIL_PANEL_SUMMARY_TAB,
UIM_SNAPSHOT_DETAIL_PANEL_FAILED_INDICES_TAB,
} from '../../../../constants';
import { loadSnapshot } from '../../../../services/http';
import { linkToRepository } from '../../../../services/navigation';
import { uiMetricService } from '../../../../services/ui_metric';
import { TabSummary, TabFailures } from './tabs';
interface Props extends RouteComponentProps {
@ -35,6 +42,11 @@ interface Props extends RouteComponentProps {
const TAB_SUMMARY = 'summary';
const TAB_FAILURES = 'failures';
const panelTypeToUiMetricMap: { [key: string]: string } = {
[TAB_SUMMARY]: UIM_SNAPSHOT_DETAIL_PANEL_SUMMARY_TAB,
[TAB_FAILURES]: UIM_SNAPSHOT_DETAIL_PANEL_FAILED_INDICES_TAB,
};
const SnapshotDetailsUi: React.FunctionComponent<Props> = ({
repositoryName,
snapshotId,
@ -45,7 +57,7 @@ const SnapshotDetailsUi: React.FunctionComponent<Props> = ({
i18n: { FormattedMessage, translate },
},
} = useAppDependencies();
const { trackUiMetric } = uiMetricService;
const { error, data: snapshotDetails } = loadSnapshot(repositoryName, snapshotId);
const [activeTab, setActiveTab] = useState<string>(TAB_SUMMARY);
@ -94,7 +106,10 @@ const SnapshotDetailsUi: React.FunctionComponent<Props> = ({
<EuiTabs>
{tabOptions.map(tab => (
<EuiTab
onClick={() => setActiveTab(tab.id)}
onClick={() => {
trackUiMetric(panelTypeToUiMetricMap[tab.id]);
setActiveTab(tab.id);
}}
isSelected={tab.id === activeTab}
key={tab.id}
data-test-subject={tab.testSubj}
@ -150,6 +165,26 @@ const SnapshotDetailsUi: React.FunctionComponent<Props> = ({
);
}
const renderFooter = () => {
return (
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
<EuiFlexItem grow={false}>
<EuiButtonEmpty
iconType="cross"
flush="left"
onClick={onClose}
data-test-subj="srSnapshotDetailsFlyoutCloseButton"
>
<FormattedMessage
id="xpack.snapshotRestore.snapshotDetails.closeButtonLabel"
defaultMessage="Close"
/>
</EuiButtonEmpty>
</EuiFlexItem>
</EuiFlexGroup>
);
};
return (
<EuiFlyout
onClose={onClose}
@ -187,6 +222,7 @@ const SnapshotDetailsUi: React.FunctionComponent<Props> = ({
</EuiFlyoutHeader>
<EuiFlyoutBody data-test-subj="srSnapshotDetailsContent">{content}</EuiFlyoutBody>
<EuiFlyoutFooter>{renderFooter()}</EuiFlyoutFooter>
</EuiFlyout>
);
};

View file

@ -11,11 +11,12 @@ import { parse } from 'querystring';
import { EuiButton, EuiCallOut, EuiIcon, EuiLink, EuiEmptyPrompt, EuiSpacer } from '@elastic/eui';
import { SectionError, SectionLoading } from '../../../components';
import { BASE_PATH } from '../../../constants';
import { BASE_PATH, UIM_SNAPSHOT_LIST_LOAD } from '../../../constants';
import { useAppDependencies } from '../../../index';
import { documentationLinksService } from '../../../services/documentation';
import { loadSnapshots } from '../../../services/http';
import { linkToRepositories } from '../../../services/navigation';
import { uiMetricService } from '../../../services/ui_metric';
import { SnapshotDetails } from './snapshot_details';
import { SnapshotTable } from './snapshot_table';
@ -45,12 +46,15 @@ export const SnapshotList: React.FunctionComponent<RouteComponentProps<MatchPara
request: reload,
} = loadSnapshots();
const openSnapshotDetails = (repositoryNameToOpen: string, snapshotIdToOpen: string) => {
history.push(
`${BASE_PATH}/snapshots/${encodeURIComponent(repositoryNameToOpen)}/${encodeURIComponent(
snapshotIdToOpen
)}`
);
const openSnapshotDetailsUrl = (
repositoryNameToOpen: string,
snapshotIdToOpen: string
): string => {
return history.createHref({
pathname: `${BASE_PATH}/snapshots/${encodeURIComponent(
repositoryNameToOpen
)}/${encodeURIComponent(snapshotIdToOpen)}`,
});
};
const closeSnapshotDetails = () => {
@ -69,6 +73,12 @@ export const SnapshotList: React.FunctionComponent<RouteComponentProps<MatchPara
}
}, []);
// Track component loaded
const { trackUiMetric } = uiMetricService;
useEffect(() => {
trackUiMetric(UIM_SNAPSHOT_LIST_LOAD);
}, []);
let content;
if (loading) {
@ -254,7 +264,7 @@ export const SnapshotList: React.FunctionComponent<RouteComponentProps<MatchPara
snapshots={snapshots}
repositories={repositories}
reload={reload}
openSnapshotDetails={openSnapshotDetails}
openSnapshotDetailsUrl={openSnapshotDetailsUrl}
repositoryFilter={filteredRepository}
/>
</Fragment>

View file

@ -8,17 +8,18 @@ import React from 'react';
import { EuiButton, EuiInMemoryTable, EuiLink, Query, EuiLoadingSpinner } from '@elastic/eui';
import { SnapshotDetails } from '../../../../../../common/types';
import { SNAPSHOT_STATE } from '../../../../constants';
import { SNAPSHOT_STATE, UIM_SNAPSHOT_SHOW_DETAILS_CLICK } from '../../../../constants';
import { useAppDependencies } from '../../../../index';
import { formatDate } from '../../../../services/text';
import { linkToRepository } from '../../../../services/navigation';
import { uiMetricService } from '../../../../services/ui_metric';
import { DataPlaceholder } from '../../../../components';
interface Props {
snapshots: SnapshotDetails[];
repositories: string[];
reload: () => Promise<void>;
openSnapshotDetails: (repositoryName: string, snapshotId: string) => void;
openSnapshotDetailsUrl: (repositoryName: string, snapshotId: string) => string;
repositoryFilter?: string;
}
@ -26,7 +27,7 @@ export const SnapshotTable: React.FunctionComponent<Props> = ({
snapshots,
repositories,
reload,
openSnapshotDetails,
openSnapshotDetailsUrl,
repositoryFilter,
}) => {
const {
@ -34,6 +35,7 @@ export const SnapshotTable: React.FunctionComponent<Props> = ({
i18n: { FormattedMessage, translate },
},
} = useAppDependencies();
const { trackUiMetric } = uiMetricService;
const columns = [
{
@ -44,7 +46,10 @@ export const SnapshotTable: React.FunctionComponent<Props> = ({
truncateText: true,
sortable: true,
render: (snapshotId: string, snapshot: SnapshotDetails) => (
<EuiLink onClick={() => openSnapshotDetails(snapshot.repository, snapshotId)}>
<EuiLink
onClick={() => trackUiMetric(UIM_SNAPSHOT_SHOW_DETAILS_CLICK)}
href={openSnapshotDetailsUrl(snapshot.repository, snapshotId)}
>
{snapshotId}
</EuiLink>
),

View file

@ -5,7 +5,14 @@
*/
import { API_BASE_PATH } from '../../../../common/constants';
import { Repository, EmptyRepository } from '../../../../common/types';
import { MINIMUM_TIMEOUT_MS } from '../../constants';
import {
MINIMUM_TIMEOUT_MS,
UIM_REPOSITORY_CREATE,
UIM_REPOSITORY_UPDATE,
UIM_REPOSITORY_DELETE,
UIM_REPOSITORY_DELETE_MANY,
UIM_REPOSITORY_DETAIL_PANEL_VERIFY,
} from '../../constants';
import { httpService } from './http';
import { sendRequest, useRequest } from './use_request';
@ -31,6 +38,7 @@ export const verifyRepository = (name: Repository['name']) => {
`${API_BASE_PATH}repositories/${encodeURIComponent(name)}/verify`
),
method: 'get',
uimActionType: UIM_REPOSITORY_DETAIL_PANEL_VERIFY,
});
};
@ -47,6 +55,7 @@ export const addRepository = async (newRepository: Repository | EmptyRepository)
path: httpService.addBasePath(`${API_BASE_PATH}repositories`),
method: 'put',
body: newRepository,
uimActionType: UIM_REPOSITORY_CREATE,
});
};
@ -57,6 +66,7 @@ export const editRepository = async (editedRepository: Repository | EmptyReposit
),
method: 'put',
body: editedRepository,
uimActionType: UIM_REPOSITORY_UPDATE,
});
};
@ -66,5 +76,6 @@ export const deleteRepositories = async (names: Array<Repository['name']>) => {
`${API_BASE_PATH}repositories/${names.map(name => encodeURIComponent(name)).join(',')}`
),
method: 'delete',
uimActionType: names.length > 1 ? UIM_REPOSITORY_DELETE_MANY : UIM_REPOSITORY_DELETE,
});
};

View file

@ -5,11 +5,13 @@
*/
import { useEffect, useState } from 'react';
import { httpService } from './index';
import { uiMetricService } from '../ui_metric';
interface SendRequest {
path: string;
method: string;
body?: any;
uimActionType?: string;
}
interface SendRequestResponse {
@ -17,10 +19,13 @@ interface SendRequestResponse {
error: Error;
}
const { trackUiMetric } = uiMetricService;
export const sendRequest = async ({
path,
method,
body,
uimActionType,
}: SendRequest): Promise<Partial<SendRequestResponse>> => {
try {
const response = await httpService.httpClient[method](path, body);
@ -29,6 +34,11 @@ export const sendRequest = async ({
throw new Error(response.statusText);
}
// Track successful request
if (uimActionType) {
trackUiMetric(uimActionType);
}
return {
data: response.data,
};
@ -45,7 +55,15 @@ interface UseRequest extends SendRequest {
timeout?: number;
}
export const useRequest = ({ path, method, body, interval, initialData, timeout }: UseRequest) => {
export const useRequest = ({
path,
method,
body,
interval,
initialData,
timeout,
uimActionType,
}: UseRequest) => {
const [error, setError] = useState<null | any>(null);
const [loading, setLoading] = useState<boolean>(true);
const [data, setData] = useState<any>(initialData);
@ -62,6 +80,7 @@ export const useRequest = ({ path, method, body, interval, initialData, timeout
path,
method,
body,
uimActionType,
};
let response;

View file

@ -0,0 +1,6 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export { uiMetricService } from './ui_metric';

View file

@ -0,0 +1,20 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { UIM_APP_NAME } from '../../constants';
class UiMetricService {
public track: any = () => {};
public init = (track: any): void => {
this.track = track;
};
public trackUiMetric = (actionType: string): any => {
return this.track(UIM_APP_NAME, actionType);
};
}
export const uiMetricService = new UiMetricService();

View file

@ -15,13 +15,14 @@ import { breadcrumbService } from './app/services/navigation';
import { documentationLinksService } from './app/services/documentation';
import { httpService } from './app/services/http';
import { textService } from './app/services/text';
import { uiMetricService } from './app/services/ui_metric';
const REACT_ROOT_ID = 'snapshotRestoreReactRoot';
export class Plugin {
public start(core: Core, plugins: Plugins): void {
const { i18n, routing, http, chrome, notification, documentation } = core;
const { management } = plugins;
const { management, uiMetric } = plugins;
// Register management section
const esSection = management.sections.getSection('elasticsearch');
@ -38,6 +39,7 @@ export class Plugin {
textService.init(i18n);
breadcrumbService.init(chrome, management.constants.BREADCRUMB);
documentationLinksService.init(documentation.esDocBasePath, documentation.esPluginDocBasePath);
uiMetricService.init(uiMetric.track);
const unmountReactApp = (): void => {
const elem = document.getElementById(REACT_ROOT_ID);

View file

@ -15,6 +15,9 @@ import routes from 'ui/routes';
import { HashRouter } from 'react-router-dom';
// @ts-ignore: allow traversal to fail on x-pack build
import { trackUiMetric as track } from '../../../../src/legacy/core_plugins/ui_metric/public';
export interface AppCore {
i18n: {
[i18nPackage: string]: any;
@ -57,6 +60,9 @@ export interface Plugins extends AppPlugins {
BREADCRUMB: typeof MANAGEMENT_BREADCRUMB;
};
};
uiMetric: {
track: typeof track;
};
}
export function createShim(): { core: Core; plugins: Plugins } {
@ -107,6 +113,9 @@ export function createShim(): { core: Core; plugins: Plugins } {
BREADCRUMB: MANAGEMENT_BREADCRUMB,
},
},
uiMetric: {
track,
},
},
};
}