Dashboard Library: Implement analytics tracking for Suggested Dashboards (#113417)
Implement analytics tracking for Suggested Dashboard * loaded - Tracks when library content becomes available * searchPerformed - Tracks search behavior (privacy-preserving, no query text) * itemClicked - Tracks dashboard selection * mappingFormShown - Tracks when datasource mapping form is displayed * mappingFormCompleted - Tracks successful mapping form completion --------- Co-authored-by: Juan Cabanas <juan.cabanas@grafana.com> Co-authored-by: nmarrs <nathanielmarrs@gmail.com>
This commit is contained in:
co-authored by
Juan Cabanas
nmarrs
parent
d35524915c
commit
6eabb9b2e4
@@ -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,
|
||||
|
||||
+14
-6
@@ -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()}`;
|
||||
|
||||
+47
-8
@@ -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<Record<string, UserSelectedDatasourceMappings>>(
|
||||
() => {
|
||||
// Initialize with existing unmapped inputs
|
||||
return unmappedInputs.reduce<Record<string, UserSelectedDatasourceMappings>>((acc, input) => {
|
||||
return unmappedDsInputs.reduce<Record<string, UserSelectedDatasourceMappings>>((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 (
|
||||
<Stack direction="column" gap={3} height="100%" justifyContent="space-between">
|
||||
@@ -116,14 +155,14 @@ export const CommunityDashboardMappingForm = ({
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{unmappedInputs.length > 0 && (
|
||||
{unmappedDsInputs.length > 0 && (
|
||||
<Stack direction="column" gap={2}>
|
||||
<Text element="h4" weight="medium">
|
||||
<Trans i18nKey="dashboard-library.community-mapping-form.datasources-title">
|
||||
Datasource Configuration
|
||||
</Trans>
|
||||
</Text>
|
||||
{unmappedInputs.map((input) => {
|
||||
{unmappedDsInputs.map((input) => {
|
||||
const selectedDatasource = userSelectedDsMappings[input.name]?.datasource;
|
||||
|
||||
return (
|
||||
|
||||
+38
-16
@@ -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,
|
||||
});
|
||||
};
|
||||
|
||||
+23
-13
@@ -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()}`;
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
};
|
||||
|
||||
+11
-2
@@ -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 && (
|
||||
<CommunityDashboardMappingForm
|
||||
unmappedInputs={mappingContext.unmappedInputs}
|
||||
unmappedDsInputs={mappingContext.unmappedDsInputs}
|
||||
constantInputs={mappingContext.constantInputs}
|
||||
existingMappings={mappingContext.existingMappings}
|
||||
onBack={handleBackToDashboards}
|
||||
onPreview={(allMappings) => {
|
||||
mappingContext.onInterpolateAndNavigate(allMappings);
|
||||
}}
|
||||
dashboardName={mappingContext.dashboardName}
|
||||
libraryItemId={String(mappingContext.dashboardJson.gnetId || '')}
|
||||
eventLocation={mappingContext.eventLocation}
|
||||
contentKind={mappingContext.contentKind}
|
||||
datasourceTypes={[datasourceInfo.type]}
|
||||
/>
|
||||
)}
|
||||
</TabContent>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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<string, unknown>) => {
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+74
-19
@@ -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<void> {
|
||||
// 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
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user