diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index e099d878557d4..865368fa16bf0 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -97,9 +97,9 @@ pageLoadAssetSize: interactiveSetup: 36524 intercepts: 21066 kibanaOverview: 6339 - kibanaReact: 21499 + kibanaReact: 21495 kibanaUsageCollection: 1736 - kibanaUtils: 54161 + kibanaUtils: 54084 kql: 15428 kubernetesSecurity: 6807 lens: 90000 @@ -154,7 +154,7 @@ pageLoadAssetSize: searchAssistant: 7079 searchGettingStarted: 6678 searchHomepage: 9005 - searchInferenceEndpoints: 10345 + searchInferenceEndpoints: 13404 searchNavigation: 8900 searchNotebooks: 18826 searchPlayground: 12122 diff --git a/x-pack/platform/plugins/shared/search_inference_endpoints/kibana.jsonc b/x-pack/platform/plugins/shared/search_inference_endpoints/kibana.jsonc index 140b77f5f501b..2a3479fc5e12e 100644 --- a/x-pack/platform/plugins/shared/search_inference_endpoints/kibana.jsonc +++ b/x-pack/platform/plugins/shared/search_inference_endpoints/kibana.jsonc @@ -25,6 +25,7 @@ "cloudConnect", "console", "serverless", + "usageCollection", ], "requiredBundles": [ "kibanaReact", diff --git a/x-pack/platform/plugins/shared/search_inference_endpoints/moon.yml b/x-pack/platform/plugins/shared/search_inference_endpoints/moon.yml index 175b768765d4c..76f784bfba392 100644 --- a/x-pack/platform/plugins/shared/search_inference_endpoints/moon.yml +++ b/x-pack/platform/plugins/shared/search_inference_endpoints/moon.yml @@ -54,6 +54,8 @@ dependsOn: - '@kbn/management-settings-ids' - '@kbn/scout' - '@kbn/search-api-panels' + - '@kbn/usage-collection-plugin' + - '@kbn/analytics' tags: - plugin - prod diff --git a/x-pack/platform/plugins/shared/search_inference_endpoints/public/analytics/constants.ts b/x-pack/platform/plugins/shared/search_inference_endpoints/public/analytics/constants.ts new file mode 100644 index 0000000000000..c089b17a56b51 --- /dev/null +++ b/x-pack/platform/plugins/shared/search_inference_endpoints/public/analytics/constants.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export enum EventType { + ENDPOINT_CREATED = 'endpoint_created', + ENDPOINT_EDITED = 'endpoint_edited', + DEFAULT_MODEL_CHANGED = 'default_model_changed', + FEATURE_SETTINGS_SAVED = 'feature_settings_saved', + FILTER_APPLIED = 'filter_applied', + GROUP_BY_CHANGED = 'group_by_changed', + EMPTY_STATE_VIEWED = 'empty_state_viewed', + FLYOUT_OPENED = 'flyout_opened', + FLYOUT_CLOSED = 'flyout_closed', + MODAL_OPENED = 'modal_opened', + MODAL_CLOSED = 'modal_closed', + EIS_MODEL_VIEWED = 'eis_model_viewed', + COPY_TO_FEATURE_TOGGLED = 'copy_to_feature_toggled', +} diff --git a/x-pack/platform/plugins/shared/search_inference_endpoints/public/application.tsx b/x-pack/platform/plugins/shared/search_inference_endpoints/public/application.tsx index f5bae783d9cac..b81798a7c2204 100644 --- a/x-pack/platform/plugins/shared/search_inference_endpoints/public/application.tsx +++ b/x-pack/platform/plugins/shared/search_inference_endpoints/public/application.tsx @@ -13,6 +13,7 @@ import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import { I18nProvider } from '@kbn/i18n-react'; import { Router } from '@kbn/shared-ux-router'; import type { AppPluginStartDependencies } from './types'; +import { UsageTrackerContextProvider } from './contexts/usage_tracker_context'; const renderMgmtApp = ( core: CoreStart, @@ -24,9 +25,11 @@ const renderMgmtApp = ( - - - + + + + + , diff --git a/x-pack/platform/plugins/shared/search_inference_endpoints/public/components/add_inference_endpoints/add_inference_flyout_wrapper.tsx b/x-pack/platform/plugins/shared/search_inference_endpoints/public/components/add_inference_endpoints/add_inference_flyout_wrapper.tsx index c671048107df5..39b94768e1a6a 100644 --- a/x-pack/platform/plugins/shared/search_inference_endpoints/public/components/add_inference_endpoints/add_inference_flyout_wrapper.tsx +++ b/x-pack/platform/plugins/shared/search_inference_endpoints/public/components/add_inference_endpoints/add_inference_flyout_wrapper.tsx @@ -10,6 +10,8 @@ import React, { useCallback } from 'react'; import InferenceFlyoutWrapper from '@kbn/inference-endpoint-ui-common'; import { ServiceProviderKeys } from '@kbn/inference-endpoint-ui-common'; import { useKibana } from '../../hooks/use_kibana'; +import { useUsageTracker } from '../../contexts/usage_tracker_context'; +import { EventType } from '../../analytics/constants'; const EXCLUDED_PROVIDERS = [ServiceProviderKeys.elasticsearch, ServiceProviderKeys.elastic]; @@ -29,10 +31,12 @@ export const AddInferenceFlyoutWrapper: React.FC serverless, }, } = useKibana(); + const usageTracker = useUsageTracker(); const onSubmitSuccess = useCallback(() => { + usageTracker.count(EventType.ENDPOINT_CREATED); reloadFn(); - }, [reloadFn]); + }, [reloadFn, usageTracker]); return ( { const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const usageTracker = useUsageTracker(); const handleValueChange = useCallback( (newOptions: EuiSelectableOption[]) => { const selectedOption = newOptions.find((option) => option.checked === 'on'); - onChange(parseGroupByValue(selectedOption?.key)); + const parsed = parseGroupByValue(selectedOption?.key); + usageTracker.count([EventType.GROUP_BY_CHANGED, `${EventType.GROUP_BY_CHANGED}_${parsed}`]); + onChange(parsed); setIsPopoverOpen(false); }, - [onChange] + [onChange, usageTracker] ); const { options, selectedOptionLabel } = useMemo(() => { let selectedOption = GROUP_BY_OPTIONS[0].label; diff --git a/x-pack/platform/plugins/shared/search_inference_endpoints/public/components/edit_inference_endpoints/edit_inference_flyout.tsx b/x-pack/platform/plugins/shared/search_inference_endpoints/public/components/edit_inference_endpoints/edit_inference_flyout.tsx index 9800c27250e08..d54169687a073 100644 --- a/x-pack/platform/plugins/shared/search_inference_endpoints/public/components/edit_inference_endpoints/edit_inference_flyout.tsx +++ b/x-pack/platform/plugins/shared/search_inference_endpoints/public/components/edit_inference_endpoints/edit_inference_flyout.tsx @@ -12,6 +12,8 @@ import { flattenObject } from '@kbn/object-utils'; import type { InferenceInferenceEndpointInfo } from '@elastic/elasticsearch/lib/api/types'; import { useKibana } from '../../hooks/use_kibana'; import { useQueryInferenceEndpoints } from '../../hooks/use_inference_endpoints'; +import { useUsageTracker } from '../../contexts/usage_tracker_context'; +import { EventType } from '../../analytics/constants'; interface EditInterfaceFlyoutProps { onFlyoutClose: () => void; @@ -29,9 +31,11 @@ export const EditInferenceFlyout: React.FC = ({ }, } = useKibana(); const { refetch } = useQueryInferenceEndpoints(); + const usageTracker = useUsageTracker(); const onEditSuccess = useCallback(() => { + usageTracker.count(EventType.ENDPOINT_EDITED); refetch(); - }, [refetch]); + }, [refetch, usageTracker]); const onFocusReturn = useCallback(() => { // Defer focus until after any closing animations complete requestAnimationFrame(() => { diff --git a/x-pack/platform/plugins/shared/search_inference_endpoints/public/components/external_inference_empty_prompt.tsx b/x-pack/platform/plugins/shared/search_inference_endpoints/public/components/external_inference_empty_prompt.tsx index 9b66afe828627..e79b9c5c0dea3 100644 --- a/x-pack/platform/plugins/shared/search_inference_endpoints/public/components/external_inference_empty_prompt.tsx +++ b/x-pack/platform/plugins/shared/search_inference_endpoints/public/components/external_inference_empty_prompt.tsx @@ -5,12 +5,14 @@ * 2.0. */ -import React from 'react'; +import React, { useEffect } from 'react'; import { EuiButton, EuiLink } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template'; import { docLinks } from '../../common/doc_links'; +import { useUsageTracker } from '../contexts/usage_tracker_context'; +import { EventType } from '../analytics/constants'; interface ExternalInferenceEmptyPromptProps { onFlyoutOpen: () => void; @@ -19,6 +21,10 @@ interface ExternalInferenceEmptyPromptProps { export const ExternalInferenceEmptyPrompt: React.FC = ({ onFlyoutOpen, }) => { + const usageTracker = useUsageTracker(); + useEffect(() => { + usageTracker.load(EventType.EMPTY_STATE_VIEWED); + }, [usageTracker]); return ( = ({ const euiThemeContext = useEuiTheme(); const [isPopoverOpen, setIsPopoverOpen] = useState(false); const toggleIsPopoverOpen = () => setIsPopoverOpen((prevValue) => !prevValue); + const usageTracker = useUsageTracker(); const options: MultiSelectFilterOption[] = _.uniqBy( rawOptions.map(({ key, label }) => ({ label, @@ -91,7 +94,11 @@ export const MultiSelectFilter: React.FC = ({ emptyMessage={i18n.translate('xpack.searchInferenceEndpoints.filter.emptyMessage', { defaultMessage: 'No options', })} - onChange={onChange} + onChange={(newOptions) => { + const filter = dataTestSubj ?? 'unknown'; + usageTracker.count([EventType.FILTER_APPLIED, `${EventType.FILTER_APPLIED}_${filter}`]); + onChange(newOptions); + }} singleSelection={false} renderOption={renderOption} > diff --git a/x-pack/platform/plugins/shared/search_inference_endpoints/public/components/inference_endpoints.tsx b/x-pack/platform/plugins/shared/search_inference_endpoints/public/components/inference_endpoints.tsx index 58d0692e533e4..416688e5e9afb 100644 --- a/x-pack/platform/plugins/shared/search_inference_endpoints/public/components/inference_endpoints.tsx +++ b/x-pack/platform/plugins/shared/search_inference_endpoints/public/components/inference_endpoints.tsx @@ -16,18 +16,23 @@ import { TabularPage } from './all_inference_endpoints/tabular_page'; import { ExternalInferenceHeader } from './external_inference_header'; import { AddInferenceFlyoutWrapper } from './add_inference_endpoints/add_inference_flyout_wrapper'; import { ExternalInferenceEmptyPrompt } from './external_inference_empty_prompt'; +import { useUsageTracker } from '../contexts/usage_tracker_context'; +import { EventType } from '../analytics/constants'; export const InferenceEndpoints: React.FC = () => { const { data, isLoading, refetch } = useQueryInferenceEndpoints(); const [isAddInferenceFlyoutOpen, setIsAddInferenceFlyoutOpen] = useState(false); + const usageTracker = useUsageTracker(); const onFlyoutOpen = useCallback(() => { + usageTracker.count([EventType.FLYOUT_OPENED, `${EventType.FLYOUT_OPENED}_add_inference`]); setIsAddInferenceFlyoutOpen(true); - }, []); + }, [usageTracker]); const onFlyoutClose = useCallback(() => { + usageTracker.count([EventType.FLYOUT_CLOSED, `${EventType.FLYOUT_CLOSED}_add_inference`]); setIsAddInferenceFlyoutOpen(false); - }, []); + }, [usageTracker]); const reload = useCallback(() => { refetch(); diff --git a/x-pack/platform/plugins/shared/search_inference_endpoints/public/components/model_detail_flyout/model_detail_flyout.tsx b/x-pack/platform/plugins/shared/search_inference_endpoints/public/components/model_detail_flyout/model_detail_flyout.tsx index 04d24685c4d84..ecfc9452f3ed4 100644 --- a/x-pack/platform/plugins/shared/search_inference_endpoints/public/components/model_detail_flyout/model_detail_flyout.tsx +++ b/x-pack/platform/plugins/shared/search_inference_endpoints/public/components/model_detail_flyout/model_detail_flyout.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useCallback, useMemo, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { EuiBadge, EuiButtonEmpty, @@ -35,6 +35,8 @@ import { import { getModelId } from '../../utils/get_model_id'; import { AddEndpointModal } from './add_endpoint_modal'; import { ModelEndpointRow } from './model_endpoint_row'; +import { useUsageTracker } from '../../contexts/usage_tracker_context'; +import { EventType } from '../../analytics/constants'; export interface ModelDetailFlyoutProps { modelId: string; @@ -56,6 +58,11 @@ export const ModelDetailFlyout: React.FC = ({ const flyoutTitleId = useGeneratedHtmlId(); const [isModalOpen, setIsModalOpen] = useState(false); const [editingEndpoint, setEditingEndpoint] = useState(); + const usageTracker = useUsageTracker(); + + useEffect(() => { + usageTracker.load([EventType.EIS_MODEL_VIEWED, `${EventType.EIS_MODEL_VIEWED}_${modelId}`]); + }, [usageTracker, modelId]); const { endpoints, displayName, modelAuthor } = useMemo(() => { const filtered = allEndpoints.filter((ep) => getModelId(ep) === modelId); @@ -89,19 +96,26 @@ export const ModelDetailFlyout: React.FC = ({ }, [endpoints]); const handleOpenAddModal = useCallback(() => { + usageTracker.count([EventType.MODAL_OPENED, `${EventType.MODAL_OPENED}_add_endpoint`]); setEditingEndpoint(undefined); setIsModalOpen(true); - }, []); + }, [usageTracker]); - const handleOpenEditModal = useCallback((endpoint: InferenceAPIConfigResponse) => { - setEditingEndpoint(endpoint); - setIsModalOpen(true); - }, []); + const handleOpenEditModal = useCallback( + (endpoint: InferenceAPIConfigResponse) => { + usageTracker.count([EventType.MODAL_OPENED, `${EventType.MODAL_OPENED}_edit_endpoint`]); + setEditingEndpoint(endpoint); + setIsModalOpen(true); + }, + [usageTracker] + ); const handleCloseModal = useCallback(() => { + const modalKind = editingEndpoint ? 'edit_endpoint' : 'add_endpoint'; + usageTracker.count([EventType.MODAL_CLOSED, `${EventType.MODAL_CLOSED}_${modalKind}`]); setIsModalOpen(false); setEditingEndpoint(undefined); - }, []); + }, [usageTracker, editingEndpoint]); const descriptionListItems = [ { diff --git a/x-pack/platform/plugins/shared/search_inference_endpoints/public/components/settings/copy_to_modal.tsx b/x-pack/platform/plugins/shared/search_inference_endpoints/public/components/settings/copy_to_modal.tsx index 140622e89bd0c..660f283b38059 100644 --- a/x-pack/platform/plugins/shared/search_inference_endpoints/public/components/settings/copy_to_modal.tsx +++ b/x-pack/platform/plugins/shared/search_inference_endpoints/public/components/settings/copy_to_modal.tsx @@ -24,6 +24,8 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { useRegisteredFeatures } from '../../hooks/use_registered_features'; import type { InferenceFeatureResponse as InferenceFeatureConfig } from '../../../common/types'; +import { useUsageTracker } from '../../contexts/usage_tracker_context'; +import { EventType } from '../../analytics/constants'; interface CopyToModalProps { sourceFeatureName: string; @@ -43,10 +45,15 @@ export const CopyToModal: React.FC = ({ const modalTitleId = useGeneratedHtmlId(); const { features: registeredFeatures } = useRegisteredFeatures(); const [idToSelectedMap, setIdToSelectedMap] = useState>({}); + const usageTracker = useUsageTracker(); - const handleToggle = useCallback((id: string) => { - setIdToSelectedMap((prev) => ({ ...prev, [id]: !prev[id] })); - }, []); + const handleToggle = useCallback( + (id: string) => { + usageTracker.count(EventType.COPY_TO_FEATURE_TOGGLED); + setIdToSelectedMap((prev) => ({ ...prev, [id]: !prev[id] })); + }, + [usageTracker] + ); const treeItems = useMemo(() => { const parentNameMap = new Map( diff --git a/x-pack/platform/plugins/shared/search_inference_endpoints/public/components/settings/default_model_section.tsx b/x-pack/platform/plugins/shared/search_inference_endpoints/public/components/settings/default_model_section.tsx index 0e18e973a2451..741c979284073 100644 --- a/x-pack/platform/plugins/shared/search_inference_endpoints/public/components/settings/default_model_section.tsx +++ b/x-pack/platform/plugins/shared/search_inference_endpoints/public/components/settings/default_model_section.tsx @@ -24,6 +24,8 @@ import { NO_DEFAULT_MODEL } from '../../../common/constants'; import { useConnectors } from '../../hooks/use_connectors'; import { useConnectorExists } from '../../hooks/use_connector_exists'; import type { UseDefaultModelSettingsReturn } from '../../hooks/use_default_model_settings'; +import { useUsageTracker } from '../../contexts/usage_tracker_context'; +import { EventType } from '../../analytics/constants'; interface Props { defaultModelSettings: UseDefaultModelSettingsReturn; @@ -89,6 +91,7 @@ export const DefaultModelSection: React.FC = ({ defaultModelSettings }) = const { exists: connectorExists, loading: connectorExistsLoading } = useConnectorExists( state.defaultModelId ); + const usageTracker = useUsageTracker(); const options = useMemo(() => getOptions(connectors), [connectors]); const selectedOptions = useMemo( @@ -124,6 +127,7 @@ export const DefaultModelSection: React.FC = ({ defaultModelSettings }) = const onChangeDefaultModel = (selected: EuiComboBoxOptionOption[]) => { const value = selected[0]?.value ?? NO_DEFAULT_MODEL; + usageTracker.count(EventType.DEFAULT_MODEL_CHANGED); setDefaultModelId(value); }; diff --git a/x-pack/platform/plugins/shared/search_inference_endpoints/public/components/settings/model_settings.tsx b/x-pack/platform/plugins/shared/search_inference_endpoints/public/components/settings/model_settings.tsx index e6be73a70d381..563f0c985c582 100644 --- a/x-pack/platform/plugins/shared/search_inference_endpoints/public/components/settings/model_settings.tsx +++ b/x-pack/platform/plugins/shared/search_inference_endpoints/public/components/settings/model_settings.tsx @@ -28,6 +28,8 @@ import { useModelSettingsForm } from './use_model_settings_form'; import { useDefaultModelSettings } from '../../hooks/use_default_model_settings'; import { useConnectors } from '../../hooks/use_connectors'; import { useKibana } from '../../hooks/use_kibana'; +import { useUsageTracker } from '../../contexts/usage_tracker_context'; +import { EventType } from '../../analytics/constants'; export const ModelSettings: React.FC = () => { const { @@ -78,6 +80,8 @@ export const ModelSettings: React.FC = () => { }; }, [isDirty, history]); + const usageTracker = useUsageTracker(); + const handleSave = useCallback(async () => { if (isFeatureDirty) { saveFeatures(); @@ -85,7 +89,8 @@ export const ModelSettings: React.FC = () => { if (defaultModelSettings.isDirty) { await defaultModelSettings.save(); } - }, [isFeatureDirty, saveFeatures, defaultModelSettings]); + usageTracker.count(EventType.FEATURE_SETTINGS_SAVED); + }, [isFeatureDirty, saveFeatures, defaultModelSettings, usageTracker]); const handleDiscardAndLeave = useCallback(() => { defaultModelSettings.reset(); diff --git a/x-pack/platform/plugins/shared/search_inference_endpoints/public/contexts/usage_tracker_context.tsx b/x-pack/platform/plugins/shared/search_inference_endpoints/public/contexts/usage_tracker_context.tsx new file mode 100644 index 0000000000000..39aac211159b0 --- /dev/null +++ b/x-pack/platform/plugins/shared/search_inference_endpoints/public/contexts/usage_tracker_context.tsx @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { createContext, useContext, useMemo } from 'react'; +import type { + UsageCollectionSetup, + UsageCollectionStart, +} from '@kbn/usage-collection-plugin/public'; + +import { createUsageTracker, createEmptyUsageTracker } from '../usage_tracker'; +import type { AppUsageTracker } from '../types'; + +const UsageTrackerContext = createContext(createEmptyUsageTracker()); + +export interface UsageTrackerContextProviderProps { + children: React.ReactNode | React.ReactNode[]; + usageCollection?: UsageCollectionSetup | UsageCollectionStart; +} + +export const UsageTrackerContextProvider = ({ + children, + usageCollection, +}: UsageTrackerContextProviderProps) => { + const usageTracker = useMemo(() => createUsageTracker(usageCollection), [usageCollection]); + return ( + {children} + ); +}; + +export const useUsageTracker = () => { + const ctx = useContext(UsageTrackerContext); + if (!ctx) { + throw new Error('UsageTrackerContext should be used inside of the UsageTrackerContextProvider'); + } + return ctx; +}; diff --git a/x-pack/platform/plugins/shared/search_inference_endpoints/public/elastic_inference_service_application.tsx b/x-pack/platform/plugins/shared/search_inference_endpoints/public/elastic_inference_service_application.tsx index 86460a05748f4..72deb3fc97de6 100644 --- a/x-pack/platform/plugins/shared/search_inference_endpoints/public/elastic_inference_service_application.tsx +++ b/x-pack/platform/plugins/shared/search_inference_endpoints/public/elastic_inference_service_application.tsx @@ -14,6 +14,7 @@ import { Router } from '@kbn/shared-ux-router'; import type { AppPluginStartDependencies } from './types'; import { ElasticInferenceService } from './components/elastic_inference_service'; import { InferenceEndpointsProvider } from './providers/inference_endpoints_provider'; +import { UsageTrackerContextProvider } from './contexts/usage_tracker_context'; export const renderElasticInferenceServiceApp = async ( core: CoreStart, @@ -25,9 +26,11 @@ export const renderElasticInferenceServiceApp = async ( - - - + + + + + diff --git a/x-pack/platform/plugins/shared/search_inference_endpoints/public/types.ts b/x-pack/platform/plugins/shared/search_inference_endpoints/public/types.ts index 9849b6b657de5..558d9f7883b22 100644 --- a/x-pack/platform/plugins/shared/search_inference_endpoints/public/types.ts +++ b/x-pack/platform/plugins/shared/search_inference_endpoints/public/types.ts @@ -16,6 +16,10 @@ import type { SharePluginSetup, SharePluginStart } from '@kbn/share-plugin/publi import type { ServerlessPluginSetup, ServerlessPluginStart } from '@kbn/serverless/public'; import type { LicensingPluginSetup, LicensingPluginStart } from '@kbn/licensing-plugin/public'; import type { CloudConnectedPluginStart } from '@kbn/cloud-connect-plugin/public'; +import type { + UsageCollectionSetup, + UsageCollectionStart, +} from '@kbn/usage-collection-plugin/public'; import type { ServiceProviderKeys } from '@kbn/inference-endpoint-ui-common'; import type { InferenceTaskType } from '@elastic/elasticsearch/lib/api/types'; @@ -34,6 +38,7 @@ export interface AppPluginStartDependencies { serverless?: ServerlessPluginStart; cloud?: CloudStart; cloudConnect?: CloudConnectedPluginStart; + usageCollection?: UsageCollectionStart; } export interface AppPluginSetupDependencies { @@ -45,6 +50,13 @@ export interface AppPluginSetupDependencies { management: ManagementSetup; share: SharePluginSetup; serverless?: ServerlessPluginSetup; + usageCollection?: UsageCollectionSetup; +} + +export interface AppUsageTracker { + click: (eventName: string | string[]) => void; + count: (eventName: string | string[]) => void; + load: (eventName: string | string[]) => void; } export type AppServicesContext = CoreStart & AppPluginStartDependencies; diff --git a/x-pack/platform/plugins/shared/search_inference_endpoints/public/usage_tracker.ts b/x-pack/platform/plugins/shared/search_inference_endpoints/public/usage_tracker.ts new file mode 100644 index 0000000000000..8eb462a7e3508 --- /dev/null +++ b/x-pack/platform/plugins/shared/search_inference_endpoints/public/usage_tracker.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { UiCounterMetricType } from '@kbn/analytics'; +import { METRIC_TYPE } from '@kbn/analytics'; +import type { + UsageCollectionSetup, + UsageCollectionStart, +} from '@kbn/usage-collection-plugin/public'; +import type { AppUsageTracker } from './types'; + +const APP_TRACKER_NAME = 'searchInferenceEndpoints'; + +export const createUsageTracker = ( + usageCollection?: UsageCollectionSetup | UsageCollectionStart +): AppUsageTracker => { + const track = (type: UiCounterMetricType, name: string | string[]) => + usageCollection?.reportUiCounter(APP_TRACKER_NAME, type, name); + + return { + click: (eventName: string | string[]) => { + track(METRIC_TYPE.CLICK, eventName); + }, + count: (eventName: string | string[]) => { + track(METRIC_TYPE.COUNT, eventName); + }, + load: (eventName: string | string[]) => { + track(METRIC_TYPE.LOADED, eventName); + }, + }; +}; + +export const createEmptyUsageTracker = (): AppUsageTracker => ({ + click: (_eventName: string | string[]) => {}, + count: (_eventName: string | string[]) => {}, + load: (_eventName: string | string[]) => {}, +}); diff --git a/x-pack/platform/plugins/shared/search_inference_endpoints/tsconfig.json b/x-pack/platform/plugins/shared/search_inference_endpoints/tsconfig.json index 11265f9c01b43..9c207a56ecae8 100644 --- a/x-pack/platform/plugins/shared/search_inference_endpoints/tsconfig.json +++ b/x-pack/platform/plugins/shared/search_inference_endpoints/tsconfig.json @@ -47,7 +47,9 @@ "@kbn/inference-plugin", "@kbn/management-settings-ids", "@kbn/scout", - "@kbn/search-api-panels" + "@kbn/search-api-panels", + "@kbn/usage-collection-plugin", + "@kbn/analytics" ], "exclude": [ "target/**/*" diff --git a/x-pack/solutions/search/plugins/search_getting_started/public/analytics/constants.ts b/x-pack/solutions/search/plugins/search_getting_started/public/analytics/constants.ts new file mode 100644 index 0000000000000..b69b167eac897 --- /dev/null +++ b/x-pack/solutions/search/plugins/search_getting_started/public/analytics/constants.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export enum AnalyticsEvents { + openedApp = 'opened_app', + agentBuilderOpened = 'agent_builder_opened', + claudeCliPromptCopied = 'claude_cli_prompt_copied', + codeExampleCopied = 'code_example_copied', + languageSelected = 'language_selected', +} diff --git a/x-pack/solutions/search/plugins/search_getting_started/public/components/agent_install/agent_install.tsx b/x-pack/solutions/search/plugins/search_getting_started/public/components/agent_install/agent_install.tsx index fad7410a02ba3..f9429c062b4c6 100644 --- a/x-pack/solutions/search/plugins/search_getting_started/public/components/agent_install/agent_install.tsx +++ b/x-pack/solutions/search/plugins/search_getting_started/public/components/agent_install/agent_install.tsx @@ -27,6 +27,8 @@ import { useKibana } from '../../hooks/use_kibana'; import { PromptModal } from './prompt_modal'; import { buildPrompt } from './util'; import { AgentBuilderPanelContainer } from './styles'; +import { useUsageTracker } from '../../contexts/usage_tracker_context'; +import { AnalyticsEvents } from '../../analytics/constants'; const AgentInstallPanel: React.FC<{ icon: string; @@ -62,6 +64,7 @@ export const AgentInstallSection = () => { const { services } = useKibana(); const [isPromptModalOpen, setIsPromptModalOpen] = useState(false); const [modalPrompt, setModalPrompt] = useState(''); + const usageTracker = useUsageTracker(); const closePromptModal = useCallback(() => setIsPromptModalOpen(false), []); @@ -73,13 +76,14 @@ export const AgentInstallSection = () => { }, []); const handleOpenInAgentBuilder = useCallback(() => { + usageTracker.click(AnalyticsEvents.agentBuilderOpened); services.agentBuilder?.openChat({ initialMessage: buildPrompt('agent-builder'), autoSendInitialMessage: true, newConversation: true, sessionTag: 'search-getting-started', }); - }, [services.agentBuilder]); + }, [services.agentBuilder, usageTracker]); return ( <> diff --git a/x-pack/solutions/search/plugins/search_getting_started/public/components/agent_install/prompt_modal.tsx b/x-pack/solutions/search/plugins/search_getting_started/public/components/agent_install/prompt_modal.tsx index 7ee57ac5baf71..fdaf5cc14f318 100644 --- a/x-pack/solutions/search/plugins/search_getting_started/public/components/agent_install/prompt_modal.tsx +++ b/x-pack/solutions/search/plugins/search_getting_started/public/components/agent_install/prompt_modal.tsx @@ -21,6 +21,8 @@ import { useGeneratedHtmlId, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { useUsageTracker } from '../../contexts/usage_tracker_context'; +import { AnalyticsEvents } from '../../analytics/constants'; interface PromptModalProps { prompt: string; @@ -29,6 +31,7 @@ interface PromptModalProps { export const PromptModal: React.FC = ({ prompt, onClose }) => { const modalTitleId = useGeneratedHtmlId({ prefix: 'promptModal' }); + const usageTracker = useUsageTracker(); return ( @@ -61,7 +64,14 @@ export const PromptModal: React.FC = ({ prompt, onClose }) => {(copy) => ( - + { + usageTracker.click(AnalyticsEvents.claudeCliPromptCopied); + copy(); + }} + fill + data-test-subj="promptModalCopyBtn" + > {i18n.translate('xpack.gettingStarted.promptModal.copy', { defaultMessage: 'Copy to clipboard', })} diff --git a/x-pack/solutions/search/plugins/search_getting_started/public/components/connect_code/install_command_code_box.tsx b/x-pack/solutions/search/plugins/search_getting_started/public/components/connect_code/install_command_code_box.tsx index 5af0f7c8fe61d..f6f698b7edcee 100644 --- a/x-pack/solutions/search/plugins/search_getting_started/public/components/connect_code/install_command_code_box.tsx +++ b/x-pack/solutions/search/plugins/search_getting_started/public/components/connect_code/install_command_code_box.tsx @@ -11,6 +11,8 @@ import { type AvailableLanguages, GettingStartedCodeExample, } from '@kbn/search-code-examples/src/getting-started-tutorials'; +import { useUsageTracker } from '../../contexts/usage_tracker_context'; +import { AnalyticsEvents } from '../../analytics/constants'; interface Props { selectedLanguage: AvailableLanguages; @@ -18,11 +20,22 @@ interface Props { export const InstallCommandCodeBox = ({ selectedLanguage }: Props) => { const installCommand = GettingStartedCodeExample[selectedLanguage].installCommandShell; + const usageTracker = useUsageTracker(); if (!installCommand) return null; return ( - + { + usageTracker.click([ + AnalyticsEvents.codeExampleCopied, + `${AnalyticsEvents.codeExampleCopied}_install_${selectedLanguage}`, + ]); + }} + > {installCommand} ); diff --git a/x-pack/solutions/search/plugins/search_getting_started/public/components/connect_code/language_selector.tsx b/x-pack/solutions/search/plugins/search_getting_started/public/components/connect_code/language_selector.tsx index e6428b448cd4e..6b61466a9d3ce 100644 --- a/x-pack/solutions/search/plugins/search_getting_started/public/components/connect_code/language_selector.tsx +++ b/x-pack/solutions/search/plugins/search_getting_started/public/components/connect_code/language_selector.tsx @@ -12,6 +12,8 @@ import { i18n } from '@kbn/i18n'; import type { AvailableLanguages } from '@kbn/search-code-examples/src/getting-started-tutorials'; import type { CodeLanguage } from '@kbn/search-code-examples/src/getting-started-tutorials/types'; import { useAssetBasePath } from '../../hooks/use_asset_base_path'; +import { useUsageTracker } from '../../contexts/usage_tracker_context'; +import { AnalyticsEvents } from '../../analytics/constants'; export interface LanguageSelectorProps { selectedLanguage: AvailableLanguages; @@ -27,6 +29,7 @@ export const LanguageSelector = ({ showLabel = false, }: LanguageSelectorProps) => { const assetBasePath = useAssetBasePath(); + const usageTracker = useUsageTracker(); const languageOptions = useMemo( () => options.map((lang) => ({ @@ -62,7 +65,13 @@ export const LanguageSelector = ({ })} options={languageOptions} valueOfSelected={selectedLanguage} - onChange={onSelectLanguage} + onChange={(value) => { + usageTracker.click([ + AnalyticsEvents.languageSelected, + `${AnalyticsEvents.languageSelected}_${value}`, + ]); + onSelectLanguage(value); + }} data-test-subj="codeExampleLanguageSelect" /> ); diff --git a/x-pack/solutions/search/plugins/search_getting_started/public/contexts/usage_tracker_context.tsx b/x-pack/solutions/search/plugins/search_getting_started/public/contexts/usage_tracker_context.tsx index 7c1e0a27daa6c..a66a0cc76698d 100644 --- a/x-pack/solutions/search/plugins/search_getting_started/public/contexts/usage_tracker_context.tsx +++ b/x-pack/solutions/search/plugins/search_getting_started/public/contexts/usage_tracker_context.tsx @@ -13,6 +13,7 @@ import type { import { createUsageTracker, createEmptyUsageTracker } from '../usage_tracker'; import type { AppUsageTracker } from '../types'; +import { AnalyticsEvents } from '../analytics/constants'; const UsageTrackerContext = createContext(createEmptyUsageTracker()); @@ -26,7 +27,7 @@ export function UsageTrackerContextProvider({ }: UsageTrackerContextProviderProps) { const usageTracker = useMemo(() => { const gettingStartedUsageTracker = createUsageTracker(usageCollection); - gettingStartedUsageTracker.load('opened_app'); + gettingStartedUsageTracker.load(AnalyticsEvents.openedApp); return gettingStartedUsageTracker; }, [usageCollection]); return ( diff --git a/x-pack/solutions/search/plugins/search_homepage/public/analytics/constants.ts b/x-pack/solutions/search/plugins/search_homepage/public/analytics/constants.ts new file mode 100644 index 0000000000000..7e3f10504eba1 --- /dev/null +++ b/x-pack/solutions/search/plugins/search_homepage/public/analytics/constants.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export enum AnalyticsEvents { + openedApp = 'opened_app', + metricFetchFailed = 'metric_fetch_failed', +} diff --git a/x-pack/solutions/search/plugins/search_homepage/public/components/search_homepage/basic_metric_badges.tsx b/x-pack/solutions/search/plugins/search_homepage/public/components/search_homepage/basic_metric_badges.tsx index a7479687c4455..b68f30c0ee202 100644 --- a/x-pack/solutions/search/plugins/search_homepage/public/components/search_homepage/basic_metric_badges.tsx +++ b/x-pack/solutions/search/plugins/search_homepage/public/components/search_homepage/basic_metric_badges.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React from 'react'; +import React, { useEffect } from 'react'; import { EuiBadge, @@ -21,6 +21,8 @@ import { useDashboardsStats } from '../../hooks/api/use_dashboards_stats'; import { useIndicesStats } from '../../hooks/api/use_indices_stats'; import { useStats } from '../../hooks/api/use_stats'; import { useAgentCount } from '../../hooks/api/use_agent_count'; +import { useUsageTracker } from '../../contexts/usage_tracker_context'; +import { AnalyticsEvents } from '../../analytics/constants'; const BASIC_METRIC_PANEL_TYPES = ['indices', 'storage', 'agentBuilder', 'discover'] as const; @@ -83,6 +85,40 @@ export const BasicMetricBadges = () => { isError: isErrorDashboards, } = useDashboardsStats(); const { tools, agents, isLoading: isLoadingAgents, isError: isErrorAgents } = useAgentCount(); + const usageTracker = useUsageTracker(); + + useEffect(() => { + if (isErrorStorageStats) { + usageTracker.count([ + AnalyticsEvents.metricFetchFailed, + `${AnalyticsEvents.metricFetchFailed}_storage`, + ]); + } + }, [isErrorStorageStats, usageTracker]); + useEffect(() => { + if (isErrorIndicesStats) { + usageTracker.count([ + AnalyticsEvents.metricFetchFailed, + `${AnalyticsEvents.metricFetchFailed}_indices`, + ]); + } + }, [isErrorIndicesStats, usageTracker]); + useEffect(() => { + if (isErrorDashboards) { + usageTracker.count([ + AnalyticsEvents.metricFetchFailed, + `${AnalyticsEvents.metricFetchFailed}_dashboards`, + ]); + } + }, [isErrorDashboards, usageTracker]); + useEffect(() => { + if (isErrorAgents) { + usageTracker.count([ + AnalyticsEvents.metricFetchFailed, + `${AnalyticsEvents.metricFetchFailed}_agents`, + ]); + } + }, [isErrorAgents, usageTracker]); const basicPanels: Array = [ { diff --git a/x-pack/solutions/search/plugins/search_homepage/public/contexts/usage_tracker_context.tsx b/x-pack/solutions/search/plugins/search_homepage/public/contexts/usage_tracker_context.tsx index a6a65892ebcdf..2ffe5277ef6f6 100644 --- a/x-pack/solutions/search/plugins/search_homepage/public/contexts/usage_tracker_context.tsx +++ b/x-pack/solutions/search/plugins/search_homepage/public/contexts/usage_tracker_context.tsx @@ -13,6 +13,7 @@ import type { import { createUsageTracker, createEmptyUsageTracker } from '../usage_tracker'; import type { AppUsageTracker } from '../types'; +import { AnalyticsEvents } from '../analytics/constants'; const UsageTrackerContext = createContext(createEmptyUsageTracker()); @@ -26,7 +27,7 @@ export function UsageTrackerContextProvider({ }: UsageTrackerContextProviderProps) { const usageTracker = useMemo(() => { const homePageUsageTracker = createUsageTracker(usageCollection); - homePageUsageTracker.load('opened_app'); + homePageUsageTracker.load(AnalyticsEvents.openedApp); return homePageUsageTracker; }, [usageCollection]); return (