Skip to content

Commit c9c5eeb

Browse files
committed
Add feedback button to the flyout of suggest anomaly detector
Signed-off-by: gaobinlong <gbinlong@amazon.com>
1 parent ec02b63 commit c9c5eeb

File tree

6 files changed

+181
-9
lines changed

6 files changed

+181
-9
lines changed

opensearch_dashboards.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88
"optionalPlugins": [
99
"dataSource",
1010
"dataSourceManagement",
11-
"assistantDashboards"
11+
"assistantDashboards",
12+
"usageCollection",
13+
"telemetry"
1214
],
1315
"requiredPlugins": [
1416
"opensearchDashboardsUtils",

public/components/DiscoverAction/SuggestAnomalyDetector.test.tsx

+102-2
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import configureStore from '../../redux/configureStore';
1919
import SuggestAnomalyDetector from './SuggestAnomalyDetector';
2020
import userEvent from '@testing-library/user-event';
2121
import { HttpFetchOptionsWithPath } from '../../../../../src/core/public';
22-
import { getAssistantClient, getQueryService } from '../../services';
22+
import { getAssistantClient, getQueryService, getUsageCollection } from '../../services';
2323

2424
const notifications = {
2525
toasts: {
@@ -42,7 +42,8 @@ jest.mock('../../services', () => ({
4242
}),
4343
getAssistantClient: jest.fn().mockReturnValue({
4444
executeAgentByName: jest.fn(),
45-
})
45+
}),
46+
getUsageCollection: jest.fn(),
4647
}));
4748

4849
const renderWithRouter = () => ({
@@ -246,9 +247,108 @@ describe('GenerateAnomalyDetector spec', () => {
246247
expect(queryByText('Detector details')).not.toBeNull();
247248
expect(queryByText('Advanced configuration')).not.toBeNull();
248249
expect(queryByText('Model Features')).not.toBeNull();
250+
expect(queryByText('Was this helpful?')).not.toBeNull();
249251
});
250252
});
253+
});
254+
255+
describe('Test feedback', () => {
256+
let reportUiStatsMock: any;
257+
258+
beforeEach(() => {
259+
const queryService = getQueryService();
260+
queryService.queryString.getQuery.mockReturnValue({
261+
dataset: {
262+
id: 'test-pattern',
263+
title: 'test-pattern',
264+
type: 'INDEX_PATTERN',
265+
timeFieldName: '@timestamp',
266+
},
267+
});
251268

269+
reportUiStatsMock = jest.fn();
270+
(getUsageCollection as jest.Mock).mockReturnValue({
271+
reportUiStats: reportUiStatsMock,
272+
METRIC_TYPE: {
273+
CLICK: 'click',
274+
},
275+
});
276+
});
277+
278+
afterEach(() => {
279+
jest.clearAllMocks();
280+
});
281+
282+
it('should call reportMetric with thumbup when thumbs up is clicked', async () => {
283+
(getAssistantClient().executeAgentByName as jest.Mock).mockResolvedValueOnce({
284+
body: {
285+
inference_results: [
286+
{
287+
output: [
288+
{ result: "{\"index\":\"opensearch_dashboards_sample_data_logs\",\"categoryField\":\"ip\",\"aggregationField\":\"responseLatency,response\",\"aggregationMethod\":\"avg,sum\",\"dateFields\":\"utc_time,timestamp\"}" }
289+
]
290+
}
291+
]
292+
}
293+
});
294+
295+
const { queryByText, getByLabelText } = renderWithRouter();
296+
expect(queryByText('Suggested anomaly detector')).not.toBeNull();
297+
298+
await waitFor(() => {
299+
expect(queryByText('Create detector')).not.toBeNull();
300+
expect(queryByText('Was this helpful?')).not.toBeNull();
301+
});
302+
303+
userEvent.click(getByLabelText('feedback thumbs up'));
304+
expect(reportUiStatsMock).toHaveBeenCalled();
305+
expect(reportUiStatsMock).toHaveBeenCalledWith(
306+
'suggestAD',
307+
'click',
308+
expect.stringContaining('generated-')
309+
);
310+
expect(reportUiStatsMock).toHaveBeenCalledWith(
311+
'suggestAD',
312+
'click',
313+
expect.stringContaining('thumbup-')
314+
);
315+
});
316+
317+
318+
it('should call reportMetric with thumbdown when thumbs down is clicked', async () => {
319+
(getAssistantClient().executeAgentByName as jest.Mock).mockResolvedValueOnce({
320+
body: {
321+
inference_results: [
322+
{
323+
output: [
324+
{ result: "{\"index\":\"opensearch_dashboards_sample_data_logs\",\"categoryField\":\"ip\",\"aggregationField\":\"responseLatency,response\",\"aggregationMethod\":\"avg,sum\",\"dateFields\":\"utc_time,timestamp\"}" }
325+
]
326+
}
327+
]
328+
}
329+
});
330+
331+
const { queryByText, getByLabelText } = renderWithRouter();
332+
expect(queryByText('Suggested anomaly detector')).not.toBeNull();
333+
334+
await waitFor(() => {
335+
expect(queryByText('Create detector')).not.toBeNull();
336+
expect(queryByText('Was this helpful?')).not.toBeNull();
337+
});
338+
339+
userEvent.click(getByLabelText('feedback thumbs down'));
340+
expect(reportUiStatsMock).toHaveBeenCalled();
341+
expect(reportUiStatsMock).toHaveBeenCalledWith(
342+
'suggestAD',
343+
'click',
344+
expect.stringContaining('generated-')
345+
);
346+
expect(reportUiStatsMock).toHaveBeenCalledWith(
347+
'suggestAD',
348+
'click',
349+
expect.stringContaining('thumbdown-')
350+
);
351+
});
252352
});
253353

254354
describe('Test API calls', () => {

public/components/DiscoverAction/SuggestAnomalyDetector.tsx

+60-3
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* SPDX-License-Identifier: Apache-2.0
44
*/
55

6-
import React, { useState, useEffect, Fragment } from 'react';
6+
import React, { useState, useEffect, Fragment, useCallback } from 'react';
77
import {
88
EuiFlyoutHeader,
99
EuiFlyoutBody,
@@ -22,6 +22,7 @@ import {
2222
EuiButtonEmpty,
2323
EuiPanel,
2424
EuiComboBox,
25+
EuiSmallButtonIcon,
2526
} from '@elastic/eui';
2627
import '../FeatureAnywhereContextMenu/CreateAnomalyDetector/styles.scss';
2728
import { useDispatch, useSelector } from 'react-redux';
@@ -62,8 +63,8 @@ import {
6263
import { formikToDetector } from '../../pages/ReviewAndCreate/utils/helpers';
6364
import { FormattedFormRow } from '../FormattedFormRow/FormattedFormRow';
6465
import { FeatureAccordion } from '../../pages/ConfigureModel/components/FeatureAccordion';
65-
import { AD_DOCS_LINK, DEFAULT_SHINGLE_SIZE, MAX_FEATURE_NUM, PLUGIN_NAME } from '../../utils/constants';
66-
import { getAssistantClient, getNotifications, getQueryService } from '../../services';
66+
import { AD_DOCS_LINK, DEFAULT_SHINGLE_SIZE, MAX_FEATURE_NUM, PLUGIN_NAME, SUGGEST_ANOMALY_DETECTOR_METRIC_TYPE } from '../../utils/constants';
67+
import { getAssistantClient, getNotifications, getQueryService, getUsageCollection } from '../../services';
6768
import { prettifyErrorMessage } from '../../../server/utils/helpers';
6869
import EnhancedAccordion from '../FeatureAnywhereContextMenu/EnhancedAccordion';
6970
import MinimalAccordion from '../FeatureAnywhereContextMenu/MinimalAccordion';
@@ -75,6 +76,7 @@ import { mountReactNode } from '../../../../../src/core/public/utils';
7576
import { formikToDetectorName } from '../FeatureAnywhereContextMenu/CreateAnomalyDetector/helpers';
7677
import { DEFAULT_DATA } from '../../../../../src/plugins/data/common';
7778
import { AppState } from '../../redux/reducers';
79+
import { v4 as uuidv4 } from 'uuid';
7880

7981
export interface GeneratedParameters {
8082
categoryField: string;
@@ -89,6 +91,7 @@ function SuggestAnomalyDetector({
8991
}) {
9092
const dispatch = useDispatch();
9193
const notifications = getNotifications();
94+
const usageCollection = getUsageCollection();
9295
const assistantClient = getAssistantClient();
9396

9497
const queryString = getQueryService().queryString;
@@ -136,12 +139,15 @@ function SuggestAnomalyDetector({
136139
const dateNanoFields = get(indexDataTypes, 'date_nanos', []) as string[];
137140
const allDateFields = dateFields.concat(dateNanoFields);
138141

142+
const [feedbackResult, setFeedbackResult] = useState<boolean | undefined>(undefined);
143+
139144
// let LLM to generate parameters for creating anomaly detector
140145
async function getParameters() {
141146
try {
142147
const executeAgentResponse = await
143148
assistantClient.executeAgentByName(SUGGEST_ANOMALY_DETECTOR_CONFIG_ID, { index: indexName }, { dataSourceId }
144149
);
150+
reportMetric(SUGGEST_ANOMALY_DETECTOR_METRIC_TYPE.GENERATED);
145151
const rawGeneratedParameters = executeAgentResponse?.body?.inference_results?.[0]?.output?.[0]?.result;
146152
if (!rawGeneratedParameters) {
147153
throw new Error('Cannot get generated parameters!');
@@ -240,6 +246,28 @@ function SuggestAnomalyDetector({
240246
setDelayValue(e.target.value);
241247
};
242248

249+
const reportMetric = usageCollection
250+
? (metric: string) => {
251+
usageCollection.reportUiStats(
252+
`suggestAD`,
253+
usageCollection.METRIC_TYPE.CLICK,
254+
metric + '-' + uuidv4()
255+
);
256+
}
257+
: () => { };
258+
259+
const feedbackOutput = useCallback(
260+
(correct: boolean, result: boolean | undefined) => {
261+
// No repeated feedback.
262+
if (result !== undefined) {
263+
return;
264+
}
265+
reportMetric(correct ? SUGGEST_ANOMALY_DETECTOR_METRIC_TYPE.THUMBUP : SUGGEST_ANOMALY_DETECTOR_METRIC_TYPE.THUMBDOWN);
266+
setFeedbackResult(correct);
267+
},
268+
[]
269+
);
270+
243271
const handleValidationAndSubmit = (formikProps: any) => {
244272
if (formikProps.values.featureList.length !== 0) {
245273
formikProps.setFieldTouched('featureList', true);
@@ -264,6 +292,7 @@ function SuggestAnomalyDetector({
264292
const detectorToCreate = formikToDetector(formikProps.values);
265293
await dispatch(createDetector(detectorToCreate, dataSourceId))
266294
.then(async (response: any) => {
295+
reportMetric(SUGGEST_ANOMALY_DETECTOR_METRIC_TYPE.CREATED);
267296
const detectorId = response.response.id;
268297
dispatch(startDetector(detectorId, dataSourceId))
269298
.then(() => { })
@@ -839,6 +868,34 @@ function SuggestAnomalyDetector({
839868
<EuiFlexItem grow={false}>
840869
<EuiButtonEmpty onClick={closeFlyout}>Cancel</EuiButtonEmpty>
841870
</EuiFlexItem>
871+
<EuiFlexGroup alignItems="center" gutterSize='none' justifyContent="flexEnd">
872+
<EuiFlexItem grow={false}>
873+
<EuiText size='s'>Was this helpful?</EuiText>
874+
</EuiFlexItem>
875+
{feedbackResult !== false ? (
876+
<EuiFlexItem grow={false}>
877+
<EuiSmallButtonIcon
878+
aria-label="feedback thumbs up"
879+
color={feedbackResult === true ? 'primary' : 'text'}
880+
iconType="thumbsUp"
881+
disabled={isLoading}
882+
onClick={() => feedbackOutput(true, feedbackResult)}
883+
/>
884+
</EuiFlexItem>
885+
) : null}
886+
{feedbackResult !== true ? (
887+
<EuiFlexItem grow={false}>
888+
<EuiSmallButtonIcon
889+
aria-label="feedback thumbs down"
890+
color={feedbackResult === false ? 'primary' : 'text'}
891+
iconType="thumbsDown"
892+
disabled={isLoading}
893+
onClick={() => feedbackOutput(false, feedbackResult)}
894+
/>
895+
</EuiFlexItem>
896+
) : null}
897+
</EuiFlexGroup>
898+
842899
<EuiFlexItem grow={false}>
843900
<EuiButton
844901
fill={true}

public/plugin.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import {
4343
setDataSourceEnabled,
4444
setNavigationUI,
4545
setApplication,
46+
setUsageCollection,
4647
setAssistantClient,
4748
} from './services';
4849
import { AnomalyDetectionOpenSearchDashboardsPluginStart } from 'public';
@@ -194,10 +195,11 @@ export class AnomalyDetectionOpenSearchDashboardsPlugin
194195
plugins.uiActions.addTriggerAction(CONTEXT_MENU_TRIGGER, action);
195196
});
196197

198+
setUsageCollection(plugins.usageCollection);
197199
// Add suggest anomaly detector action to the uiActions in Discover
198-
if (plugins.assistantDashboards?.assistantTriggers?.AI_ASSISTANT_TRIGGER) {
200+
if (plugins.assistantDashboards?.assistantTriggers?.AI_ASSISTANT_QUERY_EDITOR_TRIGGER) {
199201
const suggestAnomalyDetectorAction = getSuggestAnomalyDetectorAction();
200-
plugins.uiActions.addTriggerAction(plugins.assistantDashboards.assistantTriggers.AI_ASSISTANT_TRIGGER, suggestAnomalyDetectorAction);
202+
plugins.uiActions.addTriggerAction(plugins.assistantDashboards.assistantTriggers.AI_ASSISTANT_QUERY_EDITOR_TRIGGER, suggestAnomalyDetectorAction);
201203
}
202204
// registers the expression function used to render anomalies on an Augmented Visualization
203205
plugins.expressions.registerFunction(overlayAnomaliesFunction);

public/services.ts

+4
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { createGetterSetter } from '../../../src/plugins/opensearch_dashboards_u
1616
import { UiActionsStart } from '../../../src/plugins/ui_actions/public';
1717
import { SavedAugmentVisLoader } from '../../../src/plugins/vis_augmenter/public';
1818
import { NavigationPublicPluginStart } from '../../../src/plugins/navigation/public';
19+
import { UsageCollectionSetup } from '../../../src/plugins/usage_collection/public/plugin';
1920
import { AssistantPublicPluginStart } from '../../../plugins/dashboards-assistant/public/';
2021

2122
export interface DataSourceEnabled {
@@ -46,6 +47,9 @@ export const [getUISettings, setUISettings] =
4647
export const [getQueryService, setQueryService] =
4748
createGetterSetter<DataPublicPluginStart['query']>('Query');
4849

50+
export const [getUsageCollection, setUsageCollection] =
51+
createGetterSetter<UsageCollectionSetup>('UsageCollection');
52+
4953
export const [getAssistantEnabled, setAssistantEnabled] =
5054
createGetterSetter<AssistantPublicPluginStart>('AssistantClient');
5155

public/utils/constants.ts

+8-1
Original file line numberDiff line numberDiff line change
@@ -117,4 +117,11 @@ export const DASHBOARD_PAGE_NAV_ID = `anomaly_detection_dashboard-dashboard`;
117117

118118
export const DETECTORS_PAGE_NAV_ID = `anomaly_detection_dashboard-detectors`;
119119

120-
export const USE_NEW_HOME_PAGE = 'home:useNewHomePage';
120+
export const USE_NEW_HOME_PAGE = 'home:useNewHomePage';
121+
122+
export enum SUGGEST_ANOMALY_DETECTOR_METRIC_TYPE {
123+
THUMBUP = 'thumbup',
124+
THUMBDOWN = 'thumbdown',
125+
GENERATED = 'generated',
126+
CREATED = 'created',
127+
}

0 commit comments

Comments
 (0)