Skip to content

Commit 26b12d1

Browse files
authored
Report metrics for suggest anomaly detector (opensearch-project#876)
* Add feedback button to the flyout of suggest anomaly detector Signed-off-by: gaobinlong <gbinlong@amazon.com> * Check feature flag before registering action to Discover page Signed-off-by: gaobinlong <gbinlong@amazon.com> * Fix url bug Signed-off-by: gaobinlong <gbinlong@amazon.com> * Remove unused dependency Signed-off-by: gaobinlong <gbinlong@amazon.com> * Fix e2e test failure Signed-off-by: gaobinlong <gbinlong@amazon.com> --------- Signed-off-by: gaobinlong <gbinlong@amazon.com>
1 parent ec02b63 commit 26b12d1

7 files changed

+251
-26
lines changed

.github/workflows/remote-integ-tests-workflow.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -155,14 +155,14 @@ jobs:
155155
working-directory: opensearch-dashboards-functional-test
156156

157157
- name: Capture failure screenshots
158-
uses: actions/upload-artifact@v1
158+
uses: actions/upload-artifact@v4
159159
if: failure()
160160
with:
161161
name: cypress-screenshots-${{ matrix.os }}
162162
path: opensearch-dashboards-functional-test/cypress/screenshots
163163

164164
- name: Capture failure test video
165-
uses: actions/upload-artifact@v1
165+
uses: actions/upload-artifact@v4
166166
if: failure()
167167
with:
168168
name: cypress-videos-${{ matrix.os }}

opensearch_dashboards.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
"optionalPlugins": [
99
"dataSource",
1010
"dataSourceManagement",
11-
"assistantDashboards"
11+
"assistantDashboards",
12+
"usageCollection"
1213
],
1314
"requiredPlugins": [
1415
"opensearchDashboardsUtils",

public/components/DiscoverAction/SuggestAnomalyDetector.test.tsx

+154-13
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: {
@@ -41,8 +41,10 @@ jest.mock('../../services', () => ({
4141
},
4242
}),
4343
getAssistantClient: jest.fn().mockReturnValue({
44-
executeAgentByName: jest.fn(),
45-
})
44+
agentConfigExists: jest.fn(),
45+
executeAgentByConfigName: jest.fn(),
46+
}),
47+
getUsageCollection: jest.fn(),
4648
}));
4749

4850
const renderWithRouter = () => ({
@@ -126,11 +128,13 @@ describe('GenerateAnomalyDetector spec', () => {
126128
timeFieldName: '@timestamp',
127129
},
128130
});
129-
131+
(getAssistantClient().agentConfigExists as jest.Mock).mockResolvedValueOnce({
132+
exists: true
133+
});
130134
});
131135

132136
it('renders with empty generated parameters', async () => {
133-
(getAssistantClient().executeAgentByName as jest.Mock).mockResolvedValueOnce({
137+
(getAssistantClient().executeAgentByConfigName as jest.Mock).mockResolvedValueOnce({
134138
body: {
135139
inference_results: [
136140
{
@@ -154,7 +158,7 @@ describe('GenerateAnomalyDetector spec', () => {
154158
});
155159

156160
it('renders with empty parameter', async () => {
157-
(getAssistantClient().executeAgentByName as jest.Mock).mockResolvedValueOnce({
161+
(getAssistantClient().executeAgentByConfigName as jest.Mock).mockResolvedValueOnce({
158162
body: {
159163
inference_results: [
160164
{
@@ -178,7 +182,7 @@ describe('GenerateAnomalyDetector spec', () => {
178182
});
179183

180184
it('renders with empty aggregation field or empty aggregation method', async () => {
181-
(getAssistantClient().executeAgentByName as jest.Mock).mockResolvedValueOnce({
185+
(getAssistantClient().executeAgentByConfigName as jest.Mock).mockResolvedValueOnce({
182186
body: {
183187
inference_results: [
184188
{
@@ -202,7 +206,7 @@ describe('GenerateAnomalyDetector spec', () => {
202206
});
203207

204208
it('renders with different number of aggregation methods and fields', async () => {
205-
(getAssistantClient().executeAgentByName as jest.Mock).mockResolvedValueOnce({
209+
(getAssistantClient().executeAgentByConfigName as jest.Mock).mockResolvedValueOnce({
206210
body: {
207211
inference_results: [
208212
{
@@ -226,7 +230,7 @@ describe('GenerateAnomalyDetector spec', () => {
226230
});
227231

228232
it('renders component completely', async () => {
229-
(getAssistantClient().executeAgentByName as jest.Mock).mockResolvedValueOnce({
233+
(getAssistantClient().executeAgentByConfigName as jest.Mock).mockResolvedValueOnce({
230234
body: {
231235
inference_results: [
232236
{
@@ -246,9 +250,143 @@ describe('GenerateAnomalyDetector spec', () => {
246250
expect(queryByText('Detector details')).not.toBeNull();
247251
expect(queryByText('Advanced configuration')).not.toBeNull();
248252
expect(queryByText('Model Features')).not.toBeNull();
253+
expect(queryByText('Was this helpful?')).not.toBeNull();
249254
});
250255
});
256+
});
257+
258+
describe('Test agent not configured', () => {
259+
beforeEach(() => {
260+
jest.clearAllMocks();
261+
const queryService = getQueryService();
262+
queryService.queryString.getQuery.mockReturnValue({
263+
dataset: {
264+
id: 'test-pattern',
265+
title: 'test-pattern',
266+
type: 'INDEX_PATTERN',
267+
timeFieldName: '@timestamp',
268+
},
269+
});
270+
});
271+
272+
it('renders with empty generated parameters', async () => {
273+
(getAssistantClient().agentConfigExists as jest.Mock).mockResolvedValueOnce({
274+
exists: false
275+
});
276+
277+
const { queryByText } = renderWithRouter();
278+
expect(queryByText('Suggested anomaly detector')).not.toBeNull();
251279

280+
await waitFor(() => {
281+
expect(getNotifications().toasts.addDanger).toHaveBeenCalledTimes(1);
282+
expect(getNotifications().toasts.addDanger).toHaveBeenCalledWith(
283+
'Generate parameters for creating anomaly detector failed, reason: Error: Agent for suggest anomaly detector not found, please configure an agent firstly!'
284+
);
285+
});
286+
});
287+
});
288+
289+
describe('Test feedback', () => {
290+
let reportUiStatsMock: any;
291+
292+
beforeEach(() => {
293+
const queryService = getQueryService();
294+
queryService.queryString.getQuery.mockReturnValue({
295+
dataset: {
296+
id: 'test-pattern',
297+
title: 'test-pattern',
298+
type: 'INDEX_PATTERN',
299+
timeFieldName: '@timestamp',
300+
},
301+
});
302+
303+
reportUiStatsMock = jest.fn();
304+
(getUsageCollection as jest.Mock).mockReturnValue({
305+
reportUiStats: reportUiStatsMock,
306+
METRIC_TYPE: {
307+
CLICK: 'click',
308+
},
309+
});
310+
311+
(getAssistantClient().agentConfigExists as jest.Mock).mockResolvedValueOnce({
312+
exists: true
313+
});
314+
});
315+
316+
afterEach(() => {
317+
jest.clearAllMocks();
318+
});
319+
320+
it('should call reportMetric with thumbup when thumbs up is clicked', async () => {
321+
(getAssistantClient().executeAgentByConfigName as jest.Mock).mockResolvedValueOnce({
322+
body: {
323+
inference_results: [
324+
{
325+
output: [
326+
{ result: "{\"index\":\"opensearch_dashboards_sample_data_logs\",\"categoryField\":\"ip\",\"aggregationField\":\"responseLatency,response\",\"aggregationMethod\":\"avg,sum\",\"dateFields\":\"utc_time,timestamp\"}" }
327+
]
328+
}
329+
]
330+
}
331+
});
332+
333+
const { queryByText, getByLabelText } = renderWithRouter();
334+
expect(queryByText('Suggested anomaly detector')).not.toBeNull();
335+
336+
await waitFor(() => {
337+
expect(queryByText('Create detector')).not.toBeNull();
338+
expect(queryByText('Was this helpful?')).not.toBeNull();
339+
});
340+
341+
userEvent.click(getByLabelText('feedback thumbs up'));
342+
expect(reportUiStatsMock).toHaveBeenCalled();
343+
expect(reportUiStatsMock).toHaveBeenCalledWith(
344+
'suggestAD',
345+
'click',
346+
expect.stringContaining('generated-')
347+
);
348+
expect(reportUiStatsMock).toHaveBeenCalledWith(
349+
'suggestAD',
350+
'click',
351+
expect.stringContaining('thumbup-')
352+
);
353+
});
354+
355+
356+
it('should call reportMetric with thumbdown when thumbs down is clicked', async () => {
357+
(getAssistantClient().executeAgentByConfigName as jest.Mock).mockResolvedValueOnce({
358+
body: {
359+
inference_results: [
360+
{
361+
output: [
362+
{ result: "{\"index\":\"opensearch_dashboards_sample_data_logs\",\"categoryField\":\"ip\",\"aggregationField\":\"responseLatency,response\",\"aggregationMethod\":\"avg,sum\",\"dateFields\":\"utc_time,timestamp\"}" }
363+
]
364+
}
365+
]
366+
}
367+
});
368+
369+
const { queryByText, getByLabelText } = renderWithRouter();
370+
expect(queryByText('Suggested anomaly detector')).not.toBeNull();
371+
372+
await waitFor(() => {
373+
expect(queryByText('Create detector')).not.toBeNull();
374+
expect(queryByText('Was this helpful?')).not.toBeNull();
375+
});
376+
377+
userEvent.click(getByLabelText('feedback thumbs down'));
378+
expect(reportUiStatsMock).toHaveBeenCalled();
379+
expect(reportUiStatsMock).toHaveBeenCalledWith(
380+
'suggestAD',
381+
'click',
382+
expect.stringContaining('generated-')
383+
);
384+
expect(reportUiStatsMock).toHaveBeenCalledWith(
385+
'suggestAD',
386+
'click',
387+
expect.stringContaining('thumbdown-')
388+
);
389+
});
252390
});
253391

254392
describe('Test API calls', () => {
@@ -263,6 +401,9 @@ describe('GenerateAnomalyDetector spec', () => {
263401
timeFieldName: '@timestamp',
264402
},
265403
});
404+
(getAssistantClient().agentConfigExists as jest.Mock).mockResolvedValueOnce({
405+
exists: true
406+
});
266407
});
267408

268409
it('All API calls execute successfully', async () => {
@@ -282,7 +423,7 @@ describe('GenerateAnomalyDetector spec', () => {
282423
});
283424
}
284425
});
285-
(getAssistantClient().executeAgentByName as jest.Mock).mockResolvedValueOnce({
426+
(getAssistantClient().executeAgentByConfigName as jest.Mock).mockResolvedValueOnce({
286427
body: {
287428
inference_results: [
288429
{
@@ -314,7 +455,7 @@ describe('GenerateAnomalyDetector spec', () => {
314455
});
315456

316457
it('Generate parameters failed', async () => {
317-
(getAssistantClient().executeAgentByName as jest.Mock).mockRejectedValueOnce('Generate parameters failed');
458+
(getAssistantClient().executeAgentByConfigName as jest.Mock).mockRejectedValueOnce('Generate parameters failed');
318459

319460
const { queryByText } = renderWithRouter();
320461
expect(queryByText('Suggested anomaly detector')).not.toBeNull();
@@ -341,7 +482,7 @@ describe('GenerateAnomalyDetector spec', () => {
341482
});
342483
}
343484
});
344-
(getAssistantClient().executeAgentByName as jest.Mock).mockResolvedValueOnce({
485+
(getAssistantClient().executeAgentByConfigName as jest.Mock).mockResolvedValueOnce({
345486
body: {
346487
inference_results: [
347488
{
@@ -412,7 +553,7 @@ describe('GenerateAnomalyDetector spec', () => {
412553
},
413554
});
414555

415-
(getAssistantClient().executeAgentByName as jest.Mock).mockResolvedValueOnce({
556+
(getAssistantClient().executeAgentByConfigName as jest.Mock).mockResolvedValueOnce({
416557
body: {
417558
inference_results: [
418559
{

0 commit comments

Comments
 (0)