diff --git a/public/app/features/dashboard-scene/utils/tracking.ts b/public/app/features/dashboard-scene/utils/tracking.ts index 1b625612274..0be4cdd05ed 100644 --- a/public/app/features/dashboard-scene/utils/tracking.ts +++ b/public/app/features/dashboard-scene/utils/tracking.ts @@ -1,5 +1,6 @@ import { store } from '@grafana/data'; import { config } from '@grafana/runtime'; +import { extractDatasourceTypesFromUrl } from 'app/features/dashboard/dashgrid/DashboardLibrary/utils/communityDashboardHelpers'; import { DashboardScene } from '../scene/DashboardScene'; import { EditableDashboardElementInfo } from '../scene/types/EditableDashboardElement'; @@ -55,16 +56,18 @@ export function trackDashboardSceneCreatedOrSaved( ) { // url values for dashboard library experiment const urlParams = new URLSearchParams(window.location.search); - const pluginId = urlParams.get('pluginId') || undefined; const sourceEntryPoint = urlParams.get('sourceEntryPoint') || undefined; - const libraryItemId = urlParams.get('libraryItemId') || undefined; + // For community dashboards, use gnetId as libraryItemId if libraryItemId is not present + const libraryItemId = urlParams.get('libraryItemId') || urlParams.get('gnetId') || undefined; const creationOrigin = urlParams.get('creationOrigin') || undefined; + // Extract datasourceTypes from URL params (supports both community and provisioned dashboards) + const datasourceTypes = extractDatasourceTypesFromUrl(); const dynamicDashboardsTrackingInformation = dashboard.getDynamicDashboardsTrackingInformation(); const dashboardLibraryProperties = config.featureToggles.dashboardLibrary ? { - datasourceTypes: [pluginId], + datasourceTypes, sourceEntryPoint, libraryItemId, creationOrigin, diff --git a/public/app/features/dashboard/dashgrid/DashboardLibrary/BasicProvisionedDashboardsEmptyPage.tsx b/public/app/features/dashboard/dashgrid/DashboardLibrary/BasicProvisionedDashboardsEmptyPage.tsx index de2145c2750..91ed79019fe 100644 --- a/public/app/features/dashboard/dashgrid/DashboardLibrary/BasicProvisionedDashboardsEmptyPage.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardLibrary/BasicProvisionedDashboardsEmptyPage.tsx @@ -11,7 +11,14 @@ import { PluginDashboard } from 'app/types/plugins'; import { DASHBOARD_LIBRARY_ROUTES } from '../types'; import { fetchProvisionedDashboards } from './api/dashboardLibraryApi'; -import { DashboardLibraryInteractions } from './interactions'; +import { + CONTENT_KINDS, + CREATION_ORIGINS, + DashboardLibraryInteractions, + DISCOVERY_METHODS, + EVENT_LOCATIONS, + SOURCE_ENTRY_POINTS, +} from './interactions'; import { getProvisionedDashboardImageUrl } from './utils/provisionedDashboardHelpers'; interface Props { @@ -42,12 +49,13 @@ export const BasicProvisionedDashboardsEmptyPage = ({ datasourceUid }: Props) => const onImportDashboardClick = async (dashboard: PluginDashboard) => { DashboardLibraryInteractions.itemClicked({ - contentKind: 'datasource_dashboard', + contentKind: CONTENT_KINDS.DATASOURCE_DASHBOARD, datasourceTypes: [dashboard.pluginId], libraryItemId: dashboard.uid, libraryItemTitle: dashboard.title, - sourceEntryPoint: 'datasource_page', - eventLocation: 'empty_dashboard', + sourceEntryPoint: SOURCE_ENTRY_POINTS.DATASOURCE_PAGE, + eventLocation: EVENT_LOCATIONS.EMPTY_DASHBOARD, + discoveryMethod: DISCOVERY_METHODS.BROWSE, }); const params = new URLSearchParams({ @@ -56,9 +64,9 @@ export const BasicProvisionedDashboardsEmptyPage = ({ datasourceUid }: Props) => pluginId: dashboard.pluginId, path: dashboard.path, // tracking event purpose values - sourceEntryPoint: 'datasource_page', + sourceEntryPoint: SOURCE_ENTRY_POINTS.DATASOURCE_PAGE, libraryItemId: dashboard.uid, - creationOrigin: 'dashboard_library_datasource_dashboard', + creationOrigin: CREATION_ORIGINS.DASHBOARD_LIBRARY_DATASOURCE_DASHBOARD, }); const templateUrl = `${DASHBOARD_LIBRARY_ROUTES.Template}?${params.toString()}`; diff --git a/public/app/features/dashboard/dashgrid/DashboardLibrary/CommunityDashboardMappingForm.tsx b/public/app/features/dashboard/dashgrid/DashboardLibrary/CommunityDashboardMappingForm.tsx index 27b0a352ec1..97c1e1d4de3 100644 --- a/public/app/features/dashboard/dashgrid/DashboardLibrary/CommunityDashboardMappingForm.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardLibrary/CommunityDashboardMappingForm.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { DataSourceInstanceSettings } from '@grafana/data'; import { Trans, t } from '@grafana/i18n'; @@ -7,14 +7,20 @@ import { Stack, Text, Button, Alert, Field, Input, Box } from '@grafana/ui'; import { DataSourcePicker } from 'app/features/datasources/components/picker/DataSourcePicker'; import { DashboardInput, DataSourceInput } from 'app/features/manage-dashboards/state/reducers'; +import { ContentKind, DashboardLibraryInteractions, EventLocation, SOURCE_ENTRY_POINTS } from './interactions'; import { InputMapping, mapConstantInputs, mapUserSelectedDatasources } from './utils/autoMapDatasources'; interface Props { - unmappedInputs: DataSourceInput[]; + unmappedDsInputs: DataSourceInput[]; constantInputs: DashboardInput[]; existingMappings: InputMapping[]; onBack: () => void; onPreview: (allMappings: InputMapping[]) => void; + dashboardName: string; + libraryItemId: string; + eventLocation: EventLocation; + contentKind: ContentKind; + datasourceTypes: string[]; } interface UserSelectedDatasourceMappings { @@ -24,16 +30,36 @@ interface UserSelectedDatasourceMappings { } export const CommunityDashboardMappingForm = ({ - unmappedInputs, + unmappedDsInputs, constantInputs, existingMappings, onBack, onPreview, + dashboardName, + libraryItemId, + eventLocation, + contentKind, + datasourceTypes, }: Props) => { + // Track mapping form shown on mount + useEffect(() => { + DashboardLibraryInteractions.mappingFormShown({ + contentKind, + datasourceTypes, + libraryItemId, + libraryItemTitle: dashboardName, + sourceEntryPoint: SOURCE_ENTRY_POINTS.DATASOURCE_PAGE, + eventLocation, + unmappedDsInputsCount: unmappedDsInputs.length, + constantInputsCount: constantInputs.length, + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + const [userSelectedDsMappings, setUserSelectedDsMappings] = useState>( () => { // Initialize with existing unmapped inputs - return unmappedInputs.reduce>((acc, input) => { + return unmappedDsInputs.reduce>((acc, input) => { const unmappedInput = { name: input.name, pluginId: input.pluginId, @@ -71,12 +97,25 @@ export const CommunityDashboardMappingForm = ({ }; const onPreviewClick = () => { + // Track mapping form completion + DashboardLibraryInteractions.mappingFormCompleted({ + contentKind, + datasourceTypes, + libraryItemId, + libraryItemTitle: dashboardName, + sourceEntryPoint: SOURCE_ENTRY_POINTS.DATASOURCE_PAGE, + eventLocation, + userMappedCount: unmappedDsInputs.length, + autoMappedCount: existingMappings.length, + }); + // Combine all mappings: // 1. Existing auto-mapped datasources // 2. User-selected datasources // 3. Constant values (user-edited or defaults) - const userSelectedDatasources = mapUserSelectedDatasources(unmappedInputs, userSelectedDsMappings); + const userSelectedDatasources = mapUserSelectedDatasources(unmappedDsInputs, userSelectedDsMappings); + const constantMappings = mapConstantInputs(constantInputs, constantValues); const allMappings = [...existingMappings, ...userSelectedDatasources, ...constantMappings]; @@ -85,7 +124,7 @@ export const CommunityDashboardMappingForm = ({ // Check if all unmapped datasource inputs have been mapped by user // Constants are optional (have default values) - const allDatasourcesMapped = unmappedInputs.every((input) => userSelectedDsMappings[input.name]?.datasource); + const allDatasourcesMapped = unmappedDsInputs.every((input) => userSelectedDsMappings[input.name]?.datasource); return ( @@ -116,14 +155,14 @@ export const CommunityDashboardMappingForm = ({ )} - {unmappedInputs.length > 0 && ( + {unmappedDsInputs.length > 0 && ( Datasource Configuration - {unmappedInputs.map((input) => { + {unmappedDsInputs.map((input) => { const selectedDatasource = userSelectedDsMappings[input.name]?.datasource; return ( diff --git a/public/app/features/dashboard/dashgrid/DashboardLibrary/CommunityDashboardSection.tsx b/public/app/features/dashboard/dashgrid/DashboardLibrary/CommunityDashboardSection.tsx index 791add79d32..2c70c354fbe 100644 --- a/public/app/features/dashboard/dashgrid/DashboardLibrary/CommunityDashboardSection.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardLibrary/CommunityDashboardSection.tsx @@ -1,5 +1,5 @@ import { css } from '@emotion/css'; -import { useEffect, useState, useRef } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { useSearchParams } from 'react-router-dom-v5-compat'; import { useAsync, useDebounce } from 'react-use'; @@ -11,7 +11,13 @@ import { Button, useStyles2, Stack, Grid, EmptyState, Alert, Pagination, FilterI import { DashboardCard } from './DashboardCard'; import { MappingContext } from './SuggestedDashboardsModal'; import { fetchCommunityDashboards } from './api/dashboardLibraryApi'; -import { DashboardLibraryInteractions } from './interactions'; +import { + CONTENT_KINDS, + DashboardLibraryInteractions, + DISCOVERY_METHODS, + EVENT_LOCATIONS, + SOURCE_ENTRY_POINTS, +} from './interactions'; import { GnetDashboard } from './types'; import { getThumbnailUrl, @@ -38,6 +44,7 @@ export const CommunityDashboardSection = ({ onShowMapping, datasourceType }: Pro const datasourceUid = searchParams.get('dashboardLibraryDatasourceUid'); const [currentPage, setCurrentPage] = useState(1); const [searchQuery, setSearchQuery] = useState(''); + const hasTrackedLoaded = useRef(false); const [debouncedSearchQuery, setDebouncedSearchQuery] = useState(''); useDebounce( @@ -81,6 +88,17 @@ export const CommunityDashboardSection = ({ onShowMapping, datasourceType }: Pro filter: debouncedSearchQuery.trim() || undefined, }); + // Track search if query is present + if (debouncedSearchQuery.trim()) { + DashboardLibraryInteractions.searchPerformed({ + datasourceTypes: [ds.type], + sourceEntryPoint: SOURCE_ENTRY_POINTS.DATASOURCE_PAGE, + eventLocation: EVENT_LOCATIONS.MODAL_COMMUNITY_TAB, + hasResults: apiResponse.dashboards.length > 0, + resultCount: apiResponse.dashboards.length, + }); + } + return { dashboards: apiResponse.dashboards, pages: apiResponse.pages, @@ -93,25 +111,18 @@ export const CommunityDashboardSection = ({ onShowMapping, datasourceType }: Pro }, [datasourceUid, currentPage, debouncedSearchQuery]); // Track analytics only once on first successful load - const hasTrackedRef = useRef(false); useEffect(() => { - if ( - !loading && - !hasTrackedRef.current && - currentPage === 1 && - response?.dashboards && - response.dashboards.length > 0 - ) { + if (!loading && !hasTrackedLoaded.current && response?.dashboards && response.dashboards.length > 0) { DashboardLibraryInteractions.loaded({ numberOfItems: response.dashboards.length, - contentKinds: ['community_dashboard'], + contentKinds: [CONTENT_KINDS.COMMUNITY_DASHBOARD], datasourceTypes: [response.datasourceType], - sourceEntryPoint: 'datasource_page', - eventLocation: 'suggested_dashboards_modal_community_tab', + sourceEntryPoint: SOURCE_ENTRY_POINTS.DATASOURCE_PAGE, + eventLocation: EVENT_LOCATIONS.MODAL_COMMUNITY_TAB, }); - hasTrackedRef.current = true; + hasTrackedLoaded.current = true; } - }, [loading, currentPage, response]); + }, [loading, response]); const styles = useStyles2(getStyles); @@ -126,11 +137,22 @@ export const CommunityDashboardSection = ({ onShowMapping, datasourceType }: Pro return; } + // Track item click + DashboardLibraryInteractions.itemClicked({ + contentKind: CONTENT_KINDS.COMMUNITY_DASHBOARD, + datasourceTypes: [response.datasourceType], + libraryItemId: String(dashboard.id), + libraryItemTitle: dashboard.name, + sourceEntryPoint: SOURCE_ENTRY_POINTS.DATASOURCE_PAGE, + eventLocation: EVENT_LOCATIONS.MODAL_COMMUNITY_TAB, + discoveryMethod: debouncedSearchQuery.trim() ? DISCOVERY_METHODS.SEARCH : DISCOVERY_METHODS.BROWSE, + }); + onUseCommunityDashboard({ dashboard, datasourceUid: datasourceUid || '', datasourceType: response.datasourceType, - eventLocation: 'suggested_dashboards_modal_community_tab', + eventLocation: EVENT_LOCATIONS.MODAL_COMMUNITY_TAB, onShowMapping, }); }; diff --git a/public/app/features/dashboard/dashgrid/DashboardLibrary/DashboardLibrarySection.tsx b/public/app/features/dashboard/dashgrid/DashboardLibrary/DashboardLibrarySection.tsx index 6c31bb7bc83..a672d3eb04f 100644 --- a/public/app/features/dashboard/dashgrid/DashboardLibrary/DashboardLibrarySection.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardLibrary/DashboardLibrarySection.tsx @@ -1,5 +1,5 @@ import { css } from '@emotion/css'; -import { useEffect, useMemo, useState, useRef } from 'react'; +import { useEffect, useMemo, useRef, useState } from 'react'; import { useSearchParams } from 'react-router-dom-v5-compat'; import { useAsync } from 'react-use'; @@ -13,7 +13,14 @@ import { DASHBOARD_LIBRARY_ROUTES } from '../types'; import { DashboardCard } from './DashboardCard'; import { fetchProvisionedDashboards } from './api/dashboardLibraryApi'; -import { DashboardLibraryInteractions } from './interactions'; +import { + CONTENT_KINDS, + CREATION_ORIGINS, + DashboardLibraryInteractions, + DISCOVERY_METHODS, + EVENT_LOCATIONS, + SOURCE_ENTRY_POINTS, +} from './interactions'; import { getProvisionedDashboardImageUrl } from './utils/provisionedDashboardHelpers'; // Constants for datasource-provided dashboards pagination @@ -24,6 +31,7 @@ export const DashboardLibrarySection = () => { const datasourceUid = searchParams.get('dashboardLibraryDatasourceUid'); const [currentPage, setCurrentPage] = useState(1); + const hasTrackedLoaded = useRef(false); // Get datasource info for empty state const datasourceType = useMemo(() => { @@ -49,17 +57,16 @@ export const DashboardLibrarySection = () => { }, [datasourceUid]); // Track analytics only once on first successful load - const hasTrackedRef = useRef(false); useEffect(() => { - if (!loading && !hasTrackedRef.current && templateDashboards && templateDashboards.length > 0) { + if (!loading && !hasTrackedLoaded.current && templateDashboards && templateDashboards.length > 0) { DashboardLibraryInteractions.loaded({ numberOfItems: templateDashboards.length, - contentKinds: ['datasource_dashboard'], + contentKinds: [CONTENT_KINDS.DATASOURCE_DASHBOARD], datasourceTypes: [datasourceType], - sourceEntryPoint: 'datasource_page', - eventLocation: 'suggested_dashboards_modal_provisioned_tab', + sourceEntryPoint: SOURCE_ENTRY_POINTS.DATASOURCE_PAGE, + eventLocation: EVENT_LOCATIONS.MODAL_PROVISIONED_TAB, }); - hasTrackedRef.current = true; + hasTrackedLoaded.current = true; } }, [loading, templateDashboards, datasourceType]); @@ -77,12 +84,13 @@ export const DashboardLibrarySection = () => { const onUseProvisionedDashboard = async (dashboard: PluginDashboard) => { DashboardLibraryInteractions.itemClicked({ - contentKind: 'datasource_dashboard', + contentKind: CONTENT_KINDS.DATASOURCE_DASHBOARD, datasourceTypes: [dashboard.pluginId], libraryItemId: dashboard.uid, libraryItemTitle: dashboard.title, - sourceEntryPoint: 'datasource_page', - eventLocation: 'suggested_dashboards_modal_provisioned_tab', + sourceEntryPoint: SOURCE_ENTRY_POINTS.DATASOURCE_PAGE, + eventLocation: EVENT_LOCATIONS.MODAL_PROVISIONED_TAB, + discoveryMethod: DISCOVERY_METHODS.BROWSE, }); const params = new URLSearchParams({ @@ -91,9 +99,11 @@ export const DashboardLibrarySection = () => { pluginId: dashboard.pluginId, path: dashboard.path, // tracking event purpose values - sourceEntryPoint: 'datasource_page', + sourceEntryPoint: SOURCE_ENTRY_POINTS.DATASOURCE_PAGE, libraryItemId: dashboard.uid, - creationOrigin: 'dashboard_library_datasource_dashboard', + creationOrigin: CREATION_ORIGINS.DASHBOARD_LIBRARY_DATASOURCE_DASHBOARD, + eventLocation: EVENT_LOCATIONS.MODAL_PROVISIONED_TAB, + contentKind: CONTENT_KINDS.DATASOURCE_DASHBOARD, }); const templateUrl = `${DASHBOARD_LIBRARY_ROUTES.Template}?${params.toString()}`; diff --git a/public/app/features/dashboard/dashgrid/DashboardLibrary/SuggestedDashboards.tsx b/public/app/features/dashboard/dashgrid/DashboardLibrary/SuggestedDashboards.tsx index ecd5e258cf7..8e0389525b0 100644 --- a/public/app/features/dashboard/dashgrid/DashboardLibrary/SuggestedDashboards.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardLibrary/SuggestedDashboards.tsx @@ -1,5 +1,5 @@ import { css } from '@emotion/css'; -import { useEffect, useMemo, useState, useRef } from 'react'; +import { useEffect, useMemo, useRef, useState } from 'react'; import { useSearchParams } from 'react-router-dom-v5-compat'; import { useAsync } from 'react-use'; @@ -12,7 +12,14 @@ import { PluginDashboard } from 'app/types/plugins'; import { DashboardCard } from './DashboardCard'; import { MappingContext, SuggestedDashboardsModal } from './SuggestedDashboardsModal'; import { fetchCommunityDashboards, fetchProvisionedDashboards } from './api/dashboardLibraryApi'; -import { DashboardLibraryInteractions } from './interactions'; +import { + CONTENT_KINDS, + CREATION_ORIGINS, + DashboardLibraryInteractions, + DISCOVERY_METHODS, + EVENT_LOCATIONS, + SOURCE_ENTRY_POINTS, +} from './interactions'; import { GnetDashboard } from './types'; import { getThumbnailUrl, @@ -44,6 +51,7 @@ const INCLUDE_LOGO = true; export const SuggestedDashboards = ({ datasourceUid }: Props) => { const styles = useStyles2(getStyles); + const hasTrackedLoaded = useRef(false); const [searchParams, setSearchParams] = useSearchParams(); const showLibraryModal = searchParams.get('dashboardLibraryModal') === 'open'; @@ -144,22 +152,24 @@ export const SuggestedDashboards = ({ datasourceUid }: Props) => { }, [result, loading]); // Track analytics only once on first successful load - const hasTrackedRef = useRef(false); useEffect(() => { - if (!loading && !hasTrackedRef.current && result && result.dashboards.length > 0) { - const contentKinds: Array<'datasource_dashboard' | 'community_dashboard'> = [ + if (!loading && !hasTrackedLoaded.current && result && result.dashboards.length > 0) { + const contentKinds = [ ...new Set( - result.dashboards.map((m) => (m.type === 'provisioned' ? 'datasource_dashboard' : 'community_dashboard')) + result.dashboards.map((m) => + m.type === 'provisioned' ? CONTENT_KINDS.DATASOURCE_DASHBOARD : CONTENT_KINDS.COMMUNITY_DASHBOARD + ) ), ]; + DashboardLibraryInteractions.loaded({ numberOfItems: result.dashboards.length, contentKinds, datasourceTypes: [datasourceType], - sourceEntryPoint: 'datasource_page', - eventLocation: 'empty_dashboard', + sourceEntryPoint: SOURCE_ENTRY_POINTS.DATASOURCE_PAGE, + eventLocation: EVENT_LOCATIONS.EMPTY_DASHBOARD, }); - hasTrackedRef.current = true; + hasTrackedLoaded.current = true; } }, [loading, result, datasourceType]); @@ -198,12 +208,13 @@ export const SuggestedDashboards = ({ datasourceUid }: Props) => { } DashboardLibraryInteractions.itemClicked({ - contentKind: 'datasource_dashboard', + contentKind: CONTENT_KINDS.DATASOURCE_DASHBOARD, datasourceTypes: [ds.type], libraryItemId: dashboard.uid, libraryItemTitle: dashboard.title, - sourceEntryPoint: 'datasource_page', - eventLocation: 'empty_dashboard', + sourceEntryPoint: SOURCE_ENTRY_POINTS.DATASOURCE_PAGE, + eventLocation: EVENT_LOCATIONS.EMPTY_DASHBOARD, + discoveryMethod: DISCOVERY_METHODS.BROWSE, }); // Navigate to template route (existing flow) @@ -212,9 +223,11 @@ export const SuggestedDashboards = ({ datasourceUid }: Props) => { title: dashboard.title || 'Template', pluginId: dashboard.pluginId, path: dashboard.path, - sourceEntryPoint: 'datasource_page', + sourceEntryPoint: SOURCE_ENTRY_POINTS.DATASOURCE_PAGE, libraryItemId: dashboard.uid, - creationOrigin: 'dashboard_library_datasource_dashboard', + creationOrigin: CREATION_ORIGINS.DASHBOARD_LIBRARY_DATASOURCE_DASHBOARD, + eventLocation: EVENT_LOCATIONS.EMPTY_DASHBOARD, + contentKind: CONTENT_KINDS.DATASOURCE_DASHBOARD, }); locationService.push(`/dashboard/template?${params.toString()}`); @@ -230,11 +243,22 @@ export const SuggestedDashboards = ({ datasourceUid }: Props) => { return; } + // Track item click + DashboardLibraryInteractions.itemClicked({ + contentKind: CONTENT_KINDS.COMMUNITY_DASHBOARD, + datasourceTypes: [ds.type], + libraryItemId: String(dashboard.id), + libraryItemTitle: dashboard.name, + sourceEntryPoint: SOURCE_ENTRY_POINTS.DATASOURCE_PAGE, + eventLocation: EVENT_LOCATIONS.EMPTY_DASHBOARD, + discoveryMethod: DISCOVERY_METHODS.BROWSE, + }); + onUseCommunityDashboard({ dashboard, datasourceUid, datasourceType: ds.type, - eventLocation: 'empty_dashboard', + eventLocation: EVENT_LOCATIONS.EMPTY_DASHBOARD, onShowMapping: onShowMapping, }); }; diff --git a/public/app/features/dashboard/dashgrid/DashboardLibrary/SuggestedDashboardsModal.tsx b/public/app/features/dashboard/dashgrid/DashboardLibrary/SuggestedDashboardsModal.tsx index e6b78d35e5f..5e1de6cf4dc 100644 --- a/public/app/features/dashboard/dashgrid/DashboardLibrary/SuggestedDashboardsModal.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardLibrary/SuggestedDashboardsModal.tsx @@ -12,6 +12,7 @@ import { DashboardJson } from 'app/features/manage-dashboards/types'; import { CommunityDashboardMappingForm } from './CommunityDashboardMappingForm'; import { CommunityDashboardSection } from './CommunityDashboardSection'; import { DashboardLibrarySection } from './DashboardLibrarySection'; +import { ContentKind, EventLocation } from './interactions'; import { InputMapping } from './utils/autoMapDatasources'; interface SuggestedDashboardsModalProps { @@ -26,10 +27,13 @@ type ModalView = 'datasource' | 'community' | 'mapping'; export interface MappingContext { dashboardName: string; dashboardJson: DashboardJson; - unmappedInputs: DataSourceInput[]; + unmappedDsInputs: DataSourceInput[]; constantInputs: DashboardInput[]; existingMappings: InputMapping[]; onInterpolateAndNavigate: (mappings: InputMapping[]) => void; + // Tracking context for analytics + eventLocation: EventLocation; + contentKind: ContentKind; } export const SuggestedDashboardsModal = ({ @@ -142,13 +146,18 @@ export const SuggestedDashboardsModal = ({ )} {activeView === 'mapping' && mappingContext && ( { mappingContext.onInterpolateAndNavigate(allMappings); }} + dashboardName={mappingContext.dashboardName} + libraryItemId={String(mappingContext.dashboardJson.gnetId || '')} + eventLocation={mappingContext.eventLocation} + contentKind={mappingContext.contentKind} + datasourceTypes={[datasourceInfo.type]} /> )} diff --git a/public/app/features/dashboard/dashgrid/DashboardLibrary/api/dashboardLibraryApi.ts b/public/app/features/dashboard/dashgrid/DashboardLibrary/api/dashboardLibraryApi.ts index d856fd996f0..b97568a8ab9 100644 --- a/public/app/features/dashboard/dashgrid/DashboardLibrary/api/dashboardLibraryApi.ts +++ b/public/app/features/dashboard/dashgrid/DashboardLibrary/api/dashboardLibraryApi.ts @@ -2,7 +2,7 @@ import { getBackendSrv } from '@grafana/runtime'; import { DashboardJson } from 'app/features/manage-dashboards/types'; import { PluginDashboard } from 'app/types/plugins'; -import { GnetDashboardsResponse } from '../types'; +import { GnetDashboardsResponse, Link } from '../types'; /** * Parameters for fetching community dashboards from Grafana.com @@ -18,11 +18,28 @@ export interface FetchCommunityDashboardsParams { filter?: string; } +/** + * Dependency item from Grafana.com dashboard API + */ +export interface GnetDashboardDependency { + pluginSlug: string; + pluginTypeCode: 'app' | 'panel' | 'datasource' | 'grafana'; + pluginName?: string; + pluginVersion?: string; + [key: string]: unknown; +} + /** * Response from the Gnet API when fetching a single dashboard */ export interface GnetDashboardResponse { json: DashboardJson; + dependencies?: { + items?: GnetDashboardDependency[]; + direction?: 'asc' | 'desc'; + orderBy?: string; + links?: Link[]; + }; [key: string]: unknown; } diff --git a/public/app/features/dashboard/dashgrid/DashboardLibrary/interactions.ts b/public/app/features/dashboard/dashgrid/DashboardLibrary/interactions.ts index 6b1a07c9e43..714e53145e9 100644 --- a/public/app/features/dashboard/dashgrid/DashboardLibrary/interactions.ts +++ b/public/app/features/dashboard/dashgrid/DashboardLibrary/interactions.ts @@ -2,18 +2,40 @@ import { reportInteraction } from '@grafana/runtime'; const SCHEMA_VERSION = 1; -type ContentKind = 'datasource_dashboard' | 'community_dashboard'; -// in future this could also include "template_dashboard" if/when items become templates -// | 'template_dashboard'; +// Constant values for tracking events +export const EVENT_LOCATIONS = { + EMPTY_DASHBOARD: 'empty_dashboard', + MODAL_PROVISIONED_TAB: 'suggested_dashboards_modal_provisioned_tab', + MODAL_COMMUNITY_TAB: 'suggested_dashboards_modal_community_tab', +} as const; -type SourceEntryPoint = 'datasource_page'; -// possible future flows onboarding, create-dashboard, empty states -// | 'create_dashboard' | 'empty_state'; +export const CONTENT_KINDS = { + DATASOURCE_DASHBOARD: 'datasource_dashboard', + COMMUNITY_DASHBOARD: 'community_dashboard', + // in future this could also include "TEMPLATE_DASHBOARD" if/when items become templates +} as const; -type EventLocation = - | 'empty_dashboard' - | 'suggested_dashboards_modal_provisioned_tab' - | 'suggested_dashboards_modal_community_tab'; +export const SOURCE_ENTRY_POINTS = { + DATASOURCE_PAGE: 'datasource_page', + // possible future flows: CREATE_DASHBOARD, EMPTY_STATE +} as const; + +export const DISCOVERY_METHODS = { + SEARCH: 'search', + BROWSE: 'browse', +} as const; + +export const CREATION_ORIGINS = { + DASHBOARD_LIBRARY_DATASOURCE_DASHBOARD: 'dashboard_library_datasource_dashboard', + DASHBOARD_LIBRARY_COMMUNITY_DASHBOARD: 'dashboard_library_community_dashboard', +} as const; + +// Derive types from constant maps for single source of truth +export type EventLocation = (typeof EVENT_LOCATIONS)[keyof typeof EVENT_LOCATIONS]; +export type ContentKind = (typeof CONTENT_KINDS)[keyof typeof CONTENT_KINDS]; +export type SourceEntryPoint = (typeof SOURCE_ENTRY_POINTS)[keyof typeof SOURCE_ENTRY_POINTS]; +export type DiscoveryMethod = (typeof DISCOVERY_METHODS)[keyof typeof DISCOVERY_METHODS]; +export type CreationOrigin = (typeof CREATION_ORIGINS)[keyof typeof CREATION_ORIGINS]; export const DashboardLibraryInteractions = { loaded: (properties: { @@ -25,6 +47,15 @@ export const DashboardLibraryInteractions = { }) => { reportDashboardLibraryInteraction('loaded', properties); }, + searchPerformed: (properties: { + datasourceTypes: string[]; + sourceEntryPoint: SourceEntryPoint; + eventLocation: EventLocation; + hasResults: boolean; + resultCount: number; + }) => { + reportDashboardLibraryInteraction('search_performed', properties); + }, itemClicked: (properties: { contentKind: ContentKind; datasourceTypes: string[]; @@ -32,9 +63,34 @@ export const DashboardLibraryInteractions = { libraryItemTitle: string; sourceEntryPoint: SourceEntryPoint; eventLocation: EventLocation; + discoveryMethod: DiscoveryMethod; }) => { reportDashboardLibraryInteraction('item_clicked', properties); }, + mappingFormShown: (properties: { + contentKind: ContentKind; + datasourceTypes: string[]; + libraryItemId: string; + libraryItemTitle: string; + sourceEntryPoint: SourceEntryPoint; + eventLocation: EventLocation; + unmappedDsInputsCount: number; + constantInputsCount: number; + }) => { + reportDashboardLibraryInteraction('mapping_form_shown', properties); + }, + mappingFormCompleted: (properties: { + contentKind: ContentKind; + datasourceTypes: string[]; + libraryItemId: string; + libraryItemTitle: string; + sourceEntryPoint: SourceEntryPoint; + eventLocation: EventLocation; + userMappedCount: number; + autoMappedCount: number; + }) => { + reportDashboardLibraryInteraction('mapping_form_completed', properties); + }, }; const reportDashboardLibraryInteraction = (name: string, properties?: Record) => { diff --git a/public/app/features/dashboard/dashgrid/DashboardLibrary/utils/autoMapDatasources.ts b/public/app/features/dashboard/dashgrid/DashboardLibrary/utils/autoMapDatasources.ts index 856c99daefc..20084231e61 100644 --- a/public/app/features/dashboard/dashgrid/DashboardLibrary/utils/autoMapDatasources.ts +++ b/public/app/features/dashboard/dashgrid/DashboardLibrary/utils/autoMapDatasources.ts @@ -20,7 +20,7 @@ export function isDataSourceInput(input: Input): input is Input & DataSourceInpu export interface AutoMapResult { allMapped: boolean; mappings: InputMapping[]; - unmappedInputs: DataSourceInput[]; + unmappedDsInputs: DataSourceInput[]; } /** @@ -35,7 +35,7 @@ export interface AutoMapResult { */ export function tryAutoMapDatasources(inputs: DataSourceInput[], currentDatasourceUid: string): AutoMapResult { const mappings: InputMapping[] = []; - const unmappedInputs: DataSourceInput[] = []; + const unmappedDsInputs: DataSourceInput[] = []; for (const input of inputs) { // Get all datasources compatible with this input's plugin type @@ -70,14 +70,14 @@ export function tryAutoMapDatasources(inputs: DataSourceInput[], currentDatasour value: selectedDs, }); } else { - unmappedInputs.push(input); + unmappedDsInputs.push(input); } } return { - allMapped: unmappedInputs.length === 0, + allMapped: unmappedDsInputs.length === 0, mappings, - unmappedInputs, + unmappedDsInputs, }; } diff --git a/public/app/features/dashboard/dashgrid/DashboardLibrary/utils/communityDashboardHelpers.ts b/public/app/features/dashboard/dashgrid/DashboardLibrary/utils/communityDashboardHelpers.ts index 83b7dd47d78..fed08c7ae73 100644 --- a/public/app/features/dashboard/dashgrid/DashboardLibrary/utils/communityDashboardHelpers.ts +++ b/public/app/features/dashboard/dashgrid/DashboardLibrary/utils/communityDashboardHelpers.ts @@ -3,12 +3,40 @@ import { DataSourceInput } from 'app/features/manage-dashboards/state/reducers'; import { DASHBOARD_LIBRARY_ROUTES } from '../../types'; import { MappingContext } from '../SuggestedDashboardsModal'; -import { fetchCommunityDashboard } from '../api/dashboardLibraryApi'; -import { DashboardLibraryInteractions } from '../interactions'; +import { fetchCommunityDashboard, GnetDashboardDependency } from '../api/dashboardLibraryApi'; +import { CONTENT_KINDS, ContentKind, CREATION_ORIGINS, EventLocation, SOURCE_ENTRY_POINTS } from '../interactions'; import { GnetDashboard, Link } from '../types'; import { InputMapping, tryAutoMapDatasources, parseConstantInputs, isDataSourceInput } from './autoMapDatasources'; +/** + * Extract datasource types from URL parameters for tracking purposes. + * Supports two formats: + * - datasourceTypes: JSON array of datasource types (for community dashboards) + * - pluginId: Single datasource type (legacy format for provisioned dashboards) + * + * @returns Array of datasource type strings, or undefined if not available + */ +export function extractDatasourceTypesFromUrl(): string[] | undefined { + const params = locationService.getSearchObject(); + const datasourceTypesParam = params.datasourceTypes; + const pluginIdParam = params.pluginId; + + if (datasourceTypesParam && typeof datasourceTypesParam === 'string') { + try { + return JSON.parse(datasourceTypesParam); + } catch { + // If parsing fails, return undefined + return undefined; + } + } else if (pluginIdParam && typeof pluginIdParam === 'string') { + // Fallback to legacy pluginId for provisioned dashboards + return [pluginIdParam]; + } + + return undefined; +} + /** * Extract thumbnail URL from dashboard screenshots */ @@ -86,17 +114,27 @@ export function navigateToTemplate( dashboardTitle: string, gnetId: number, datasourceUid: string, - mappings: InputMapping[] + mappings: InputMapping[], + eventLocation: EventLocation, + contentKind: ContentKind, + datasourceTypes?: string[] ): void { const searchParams = new URLSearchParams({ datasource: datasourceUid, title: dashboardTitle, gnetId: String(gnetId), - sourceEntryPoint: 'datasource_page', - creationOrigin: 'dashboard_library_community_dashboard', + sourceEntryPoint: SOURCE_ENTRY_POINTS.DATASOURCE_PAGE, + creationOrigin: CREATION_ORIGINS.DASHBOARD_LIBRARY_COMMUNITY_DASHBOARD, + contentKind, + eventLocation, mappings: JSON.stringify(mappings), }); + // Add datasource types for tracking if available + if (datasourceTypes && datasourceTypes.length > 0) { + searchParams.set('datasourceTypes', JSON.stringify(datasourceTypes)); + } + locationService.push({ pathname: DASHBOARD_LIBRARY_ROUTES.Template, search: searchParams.toString(), @@ -125,16 +163,8 @@ export async function onUseCommunityDashboard({ eventLocation, onShowMapping, }: UseCommunityDashboardParams): Promise { - // Track analytics - DashboardLibraryInteractions.itemClicked({ - contentKind: 'community_dashboard', - datasourceTypes: [datasourceType], - libraryItemId: String(dashboard.id), - libraryItemTitle: dashboard.name, - sourceEntryPoint: 'datasource_page', - eventLocation, - }); - + // Note: item_clicked tracking is done by the caller (CommunityDashboardSection or SuggestedDashboards) + // with the correct discoveryMethod before calling this function try { // Fetch full dashboard from Gcom, this is the JSON with __inputs const fullDashboard = await fetchCommunityDashboard(dashboard.id); @@ -143,6 +173,13 @@ export async function onUseCommunityDashboard({ // Parse datasource requirements from __inputs const dsInputs: DataSourceInput[] = dashboardJson.__inputs?.filter(isDataSourceInput) || []; + // Extract datasource types for tracking purposes from dependencies + const datasourceTypes = + fullDashboard.dependencies?.items + ?.filter((dep: GnetDashboardDependency) => dep.pluginTypeCode === 'datasource') + .map((dep: GnetDashboardDependency) => dep.pluginSlug) + .filter(Boolean) || []; + // Parse constant inputs - these always need user review const constantInputs = parseConstantInputs(dashboardJson.__inputs || []); @@ -151,22 +188,40 @@ export async function onUseCommunityDashboard({ // Decide whether to show mapping form or navigate directly // Show mapping form if: (a) there are unmapped datasources OR (b) there are constants - const needsMapping = mappingResult.unmappedInputs.length > 0 || constantInputs.length > 0; + const needsMapping = mappingResult.unmappedDsInputs.length > 0 || constantInputs.length > 0; if (!needsMapping) { // No mapping needed - all datasources auto-mapped, no constants - navigateToTemplate(dashboard.name, dashboard.id, datasourceUid, mappingResult.mappings); + navigateToTemplate( + dashboard.name, + dashboard.id, + datasourceUid, + mappingResult.mappings, + eventLocation, + CONTENT_KINDS.COMMUNITY_DASHBOARD, + datasourceTypes + ); } else { // Show mapping form for unmapped datasources and/or constants if (onShowMapping) { onShowMapping({ dashboardName: dashboard.name, dashboardJson, - unmappedInputs: mappingResult.unmappedInputs, + unmappedDsInputs: mappingResult.unmappedDsInputs, constantInputs, existingMappings: mappingResult.mappings, + eventLocation, + contentKind: CONTENT_KINDS.COMMUNITY_DASHBOARD, onInterpolateAndNavigate: (mappings) => - navigateToTemplate(dashboard.name, dashboard.id, datasourceUid, mappings), + navigateToTemplate( + dashboard.name, + dashboard.id, + datasourceUid, + mappings, + eventLocation, + CONTENT_KINDS.COMMUNITY_DASHBOARD, + datasourceTypes + ), }); } }