From 53c3df893918e3e585a8c7b98cd66d526704d379 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 27 Sep 2024 11:12:23 +0000 Subject: [PATCH] T2viz UI metrics (#312) * Add metrics for text2viz Signed-off-by: Yulong Ruan * remove new line at the end of file Signed-off-by: Yulong Ruan * fix imported types Signed-off-by: Yulong Ruan * update changelogs Signed-off-by: Yulong Ruan * fix UI flashing Signed-off-by: Yulong Ruan * cleanup duplicate imports Signed-off-by: Yulong Ruan --------- Signed-off-by: Yulong Ruan (cherry picked from commit 519bb92d54a241f2258a49bedb9c80ad5a363368) Signed-off-by: github-actions[bot] # Conflicts: # CHANGELOG.md --- public/components/feedback_thumbs.test.tsx | 79 +++++++++++++++++++ public/components/feedback_thumbs.tsx | 59 ++++++++++++++ public/components/visualization/text2vega.ts | 4 +- public/components/visualization/text2viz.scss | 7 ++ public/components/visualization/text2viz.tsx | 47 ++++++++++- public/types.ts | 6 +- 6 files changed, 196 insertions(+), 6 deletions(-) create mode 100644 public/components/feedback_thumbs.test.tsx create mode 100644 public/components/feedback_thumbs.tsx diff --git a/public/components/feedback_thumbs.test.tsx b/public/components/feedback_thumbs.test.tsx new file mode 100644 index 00000000..c5055c5c --- /dev/null +++ b/public/components/feedback_thumbs.test.tsx @@ -0,0 +1,79 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import { METRIC_TYPE } from '@osd/analytics'; + +import { FeedbackThumbs } from './feedback_thumbs'; + +describe('', () => { + it('should report thumbs up metric', () => { + const usageCollectionMock = { + reportUiStats: jest.fn(), + METRIC_TYPE, + }; + + render(); + fireEvent.click(screen.getByLabelText('ThumbsUp')); + expect(usageCollectionMock.reportUiStats).toHaveBeenCalledWith( + 'test-app', + METRIC_TYPE.CLICK, + expect.stringMatching(/thumbs_up.*/) + ); + }); + + it('should report thumbs down metric', () => { + const usageCollectionMock = { + reportUiStats: jest.fn(), + METRIC_TYPE, + }; + + render(); + fireEvent.click(screen.getByLabelText('ThumbsDown')); + expect(usageCollectionMock.reportUiStats).toHaveBeenCalledWith( + 'test-app', + METRIC_TYPE.CLICK, + expect.stringMatching(/thumbs_down.*/) + ); + }); + + it('should only report metric only once', () => { + const usageCollectionMock = { + reportUiStats: jest.fn(), + METRIC_TYPE, + }; + + render(); + // click the button two times + fireEvent.click(screen.getByLabelText('ThumbsDown')); + fireEvent.click(screen.getByLabelText('ThumbsDown')); + expect(usageCollectionMock.reportUiStats).toHaveBeenCalledTimes(1); + }); + + it('should hide thumbs down button after thumbs up been clicked', () => { + const usageCollectionMock = { + reportUiStats: jest.fn(), + METRIC_TYPE, + }; + + render(); + + fireEvent.click(screen.getByLabelText('ThumbsUp')); + expect(screen.queryByLabelText('ThumbsDown')).toBeNull(); + }); + + it('should hide thumbs up button after thumbs down been clicked', () => { + const usageCollectionMock = { + reportUiStats: jest.fn(), + METRIC_TYPE, + }; + + render(); + + fireEvent.click(screen.getByLabelText('ThumbsDown')); + expect(screen.queryByLabelText('ThumbsUp')).toBeNull(); + }); +}); diff --git a/public/components/feedback_thumbs.tsx b/public/components/feedback_thumbs.tsx new file mode 100644 index 00000000..d7ee3343 --- /dev/null +++ b/public/components/feedback_thumbs.tsx @@ -0,0 +1,59 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import React, { useState } from 'react'; +import { v4 as uuidv4 } from 'uuid'; + +import { UsageCollectionStart } from '../../../../src/plugins/usage_collection/public'; + +interface Props { + appName: string; + usageCollection: UsageCollectionStart; + className?: string; +} + +export const FeedbackThumbs = ({ usageCollection, appName, className }: Props) => { + const [feedback, setFeedback] = useState<'thumbs_up' | 'thumbs_down' | undefined>(); + + const onFeedback = (eventName: 'thumbs_up' | 'thumbs_down') => { + // Only send metric if no current feedback set + if (!feedback) { + usageCollection.reportUiStats( + appName, + usageCollection.METRIC_TYPE.CLICK, + `${eventName}-${uuidv4()}` + ); + setFeedback(eventName); + } + }; + + return ( + + {(!feedback || feedback === 'thumbs_up') && ( + + onFeedback('thumbs_up')} + /> + + )} + {(!feedback || feedback === 'thumbs_down') && ( + + onFeedback('thumbs_down')} + /> + + )} + + ); +}; diff --git a/public/components/visualization/text2vega.ts b/public/components/visualization/text2vega.ts index aba0e388..ea8b0936 100644 --- a/public/components/visualization/text2vega.ts +++ b/public/components/visualization/text2vega.ts @@ -38,8 +38,8 @@ export class Text2Vega { this.result$ = this.input$ .pipe( filter((v) => v.prompt.length > 0), - debounceTime(200), - tap(() => this.status$.next('RUNNING')) + tap(() => this.status$.next('RUNNING')), + debounceTime(200) ) .pipe( switchMap((v) => diff --git a/public/components/visualization/text2viz.scss b/public/components/visualization/text2viz.scss index f2b0f84b..c917471e 100644 --- a/public/components/visualization/text2viz.scss +++ b/public/components/visualization/text2viz.scss @@ -16,4 +16,11 @@ padding-top: 15px; padding-left: 30px; } + + .feedback_thumbs { + position: absolute; + right: 16px; + top: 4px; + z-index: 9999; + } } diff --git a/public/components/visualization/text2viz.tsx b/public/components/visualization/text2viz.tsx index 99ee35e8..1efb507d 100644 --- a/public/components/visualization/text2viz.tsx +++ b/public/components/visualization/text2viz.tsx @@ -18,6 +18,7 @@ import { } from '@elastic/eui'; import React, { useEffect, useMemo, useRef, useState } from 'react'; import { i18n } from '@osd/i18n'; +import { v4 as uuidv4 } from 'uuid'; import { useCallback } from 'react'; import { useObservable } from 'react-use'; @@ -46,9 +47,10 @@ import { getIndexPatterns } from '../../services'; import { NLQ_VISUALIZATION_EMBEDDABLE_TYPE } from './embeddable/nlq_vis_embeddable'; import { NLQVisualizationInput } from './embeddable/types'; import { EditorPanel } from './editor_panel'; -import { VIS_NLQ_SAVED_OBJECT } from '../../../common/constants/vis_type_nlq'; +import { VIS_NLQ_APP_ID, VIS_NLQ_SAVED_OBJECT } from '../../../common/constants/vis_type_nlq'; import { HeaderVariant } from '../../../../../src/core/public'; import { TEXT2VEGA_INPUT_SIZE_LIMIT } from '../../../common/constants/llm'; +import { FeedbackThumbs } from '../feedback_thumbs'; export const Text2Viz = () => { const { savedObjectId } = useParams<{ savedObjectId?: string }>(); @@ -68,9 +70,23 @@ export const Text2Viz = () => { uiSettings, savedObjects, config, + usageCollection, }, } = useOpenSearchDashboards(); + /** + * Report metrics when the application is loaded + */ + useEffect(() => { + if (usageCollection) { + usageCollection.reportUiStats( + VIS_NLQ_APP_ID, + usageCollection.METRIC_TYPE.LOADED, + `app_loaded-${uuidv4()}` + ); + } + }, [usageCollection]); + const useUpdatedUX = uiSettings.get('home:useNewHomePage'); const [input, setInput] = useState(''); @@ -112,6 +128,15 @@ export const Text2Viz = () => { }); } else { setEditorInput(JSON.stringify(result, undefined, 4)); + + // Report metric when visualization generated successfully + if (usageCollection) { + usageCollection.reportUiStats( + VIS_NLQ_APP_ID, + usageCollection.METRIC_TYPE.LOADED, + `generated-${uuidv4()}` + ); + } } } }); @@ -119,7 +144,7 @@ export const Text2Viz = () => { return () => { subscription.unsubscribe(); }; - }, [http, notifications]); + }, [http, notifications, usageCollection]); /** * Loads the saved object from id when editing an existing visualization @@ -243,6 +268,15 @@ export const Text2Viz = () => { }), }); dialog.close(); + + // Report metric when a new visualization is saved. + if (usageCollection) { + usageCollection.reportUiStats( + VIS_NLQ_APP_ID, + usageCollection.METRIC_TYPE.LOADED, + `saved-${uuidv4()}` + ); + } } } catch (e) { notifications.toasts.addDanger({ @@ -270,7 +304,7 @@ export const Text2Viz = () => { /> ) ); - }, [notifications, vegaSpec, input, overlays, selectedSource, savedObjectId]); + }, [notifications, vegaSpec, input, overlays, selectedSource, savedObjectId, usageCollection]); const pageTitle = savedObjectId ? i18n.translate('dashboardAssistant.feature.text2viz.breadcrumbs.editVisualization', { @@ -412,6 +446,13 @@ export const Text2Viz = () => { paddingSize="none" scrollable={false} > + {usageCollection ? ( + + ) : null} diff --git a/public/types.ts b/public/types.ts index 317f78c2..6a95238f 100644 --- a/public/types.ts +++ b/public/types.ts @@ -20,8 +20,11 @@ import { AssistantClient } from './services/assistant_client'; import { UiActionsSetup, UiActionsStart } from '../../../src/plugins/ui_actions/public'; import { ExpressionsSetup, ExpressionsStart } from '../../../src/plugins/expressions/public'; import { SavedObjectsStart } from '../../../src/plugins/saved_objects/public'; +import { + UsageCollectionStart, + UsageCollectionSetup, +} from '../../../src/plugins/usage_collection/public'; -import { UsageCollectionSetup } from '../../../src/plugins/usage_collection/public'; import { ConfigSchema } from '../common/types/config'; export interface RenderProps { @@ -49,6 +52,7 @@ export interface AssistantPluginStartDependencies { uiActions: UiActionsStart; expressions: ExpressionsStart; savedObjects: SavedObjectsStart; + usageCollection?: UsageCollectionStart; } export interface AssistantPluginSetupDependencies {