diff --git a/CHANGELOG.md b/CHANGELOG.md
index c776b2c4..a0f28e0b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -17,6 +17,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
- fix: incorrect string escaping of vega schema([325](https://github.com/opensearch-project/dashboards-assistant/pull/325))
- feat: register the AI actions to query controls in discover([#327](https://github.com/opensearch-project/dashboards-assistant/pull/327))
- fix: t2viz ux improvements([#330](https://github.com/opensearch-project/dashboards-assistant/pull/330))
+- feat: report metrics for text to visualization([#312](https://github.com/opensearch-project/dashboards-assistant/pull/312))
### 📈 Features/Enhancements
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 {