Skip to content
47 changes: 47 additions & 0 deletions static/app/views/dashboards/components/genericOnboarding.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import emptyStateImg from 'sentry-images/spot/performance-waiting-for-span.svg';

import {LinkButton} from '@sentry/scraps/button';
import {Image} from '@sentry/scraps/image';
import {Flex} from '@sentry/scraps/layout';
import {Heading, Text} from '@sentry/scraps/text';

import Panel from 'sentry/components/panels/panel';
import {t} from 'sentry/locale';

interface OverviewOnboardingPanelProps {
heading: string;
}

export function GenericOnboarding({heading}: OverviewOnboardingPanelProps) {
return (
<Panel>
<Flex justify="center">
<Flex padding="xl" align="center" wrap="wrap-reverse" gap="3xl" maxWidth="1000px">
<Flex direction="column" gap="xl" flex="5" align="start">
<Heading as="h3" size="xl">
{heading}
</Heading>

<Text as="p" size="md">
{t(
'Send telemetry data to Sentry for this project to start using this dashboard. Set up your SDK to begin monitoring your application.'
)}
</Text>

<LinkButton
priority="primary"
external
href="https://docs.sentry.io/product/dashboards/"
>
{t('Read the Docs')}
</LinkButton>
</Flex>

<Flex flex="3" justify="center">
<Image src={emptyStateImg} alt="" width="100%" />
</Flex>
</Flex>
</Flex>
</Panel>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import usePageFilters from 'sentry/components/pageFilters/usePageFilters';
import {getSelectedProjectList} from 'sentry/utils/project/useSelectedProjectsHaveField';
import useOrganization from 'sentry/utils/useOrganization';
import useProjects from 'sentry/utils/useProjects';
import type {PrebuiltDashboardId} from 'sentry/views/dashboards/utils/prebuiltConfigs';
import {PREBUILT_DASHBOARDS} from 'sentry/views/dashboards/utils/prebuiltConfigs';
import * as ModuleLayout from 'sentry/views/insights/common/components/moduleLayout';
import {ModulesOnboardingPanel} from 'sentry/views/insights/common/components/modulesOnboarding';
import {useHasFirstSpan} from 'sentry/views/insights/common/queries/useHasFirstSpan';
import {useOnboardingProject} from 'sentry/views/insights/common/queries/useOnboardingProject';
import {Onboarding as AgentOnboarding} from 'sentry/views/insights/pages/agents/onboarding';
import {Onboarding as MCPOnboarding} from 'sentry/views/insights/pages/mcp/onboarding';
import {ModuleName} from 'sentry/views/insights/types';
import {LegacyOnboarding} from 'sentry/views/performance/onboarding';

import {GenericOnboarding} from './genericOnboarding';

interface PrebuiltDashboardOnboardingGateProps {
children: React.ReactNode;
prebuiltId?: PrebuiltDashboardId;
}

export function PrebuiltDashboardOnboardingGate({
prebuiltId,
children,
}: PrebuiltDashboardOnboardingGateProps) {
const organization = useOrganization();
const {projects} = useProjects();
const pageFilters = usePageFilters();

const onboarding = prebuiltId ? PREBUILT_DASHBOARDS[prebuiltId]?.onboarding : undefined;

const moduleName =
onboarding?.type === 'module' ? onboarding.moduleName : ModuleName.OTHER;

// First project that doesn't have any span data at all
const onboardingProject = useOnboardingProject();
const hasAnySpanData = !onboardingProject;

// Whether the selected projects have span of the required type
const hasFirstSpan = useHasFirstSpan(moduleName);

if (!onboarding) {
return children;
}

// If the dashboard uses module-specific onboarding, check whether
// module-specific data is available
if (onboarding.type === 'module') {
if (!hasAnySpanData) {
return (
<ModuleLayout.Full>
<LegacyOnboarding organization={organization} project={onboardingProject} />
</ModuleLayout.Full>
);
}

if (!hasFirstSpan) {
return (
<ModuleLayout.Full>
<ModulesOnboardingPanel moduleName={onboarding.moduleName} />
</ModuleLayout.Full>
);
}

return children;
}

const selectedProjects = getSelectedProjectList(
pageFilters.selection.projects,
projects
);

const hasData = onboarding.requiredProjectFlags.some(flag =>
selectedProjects.some(p => p[flag] === true)
);

if (hasData) {
return children;
}

if (onboarding.type === 'custom') {
if (onboarding.componentId === 'agent-monitoring') {
return <AgentOnboarding />;
}

return <MCPOnboarding />;
}

return <GenericOnboarding heading={onboarding.description} />;
}
67 changes: 38 additions & 29 deletions static/app/views/dashboards/detail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ import {MetricsDataSwitcher} from 'sentry/views/performance/landing/metricsDataS
import {MetricsDataSwitcherAlert} from 'sentry/views/performance/landing/metricsDataSwitcherAlert';
import {DiscoverQueryPageSource} from 'sentry/views/performance/utils';

import {PrebuiltDashboardOnboardingGate} from './components/prebuiltDashboardOnboardingGate';
import Controls from './controls';
import Dashboard from './dashboard';
import {DEFAULT_STATS_PERIOD} from './data';
Expand Down Expand Up @@ -997,17 +998,21 @@ class DashboardDetail extends Component<Props, State> {
location={location}
forceTransactions={metricsDataSide.forceTransactionsOnly}
>
<Dashboard
dashboard={modifiedDashboard ?? dashboard}
isEditingDashboard={this.isEditingDashboard}
widgetLimitReached={widgetLimitReached}
onUpdate={this.handleUpdateEditStateWidgets}
handleUpdateWidgetList={this.handleUpdateWidgetList}
handleAddCustomWidget={this.handleAddCustomWidget}
isEmbedded={this.isEmbedded}
isPreview={this.isPreview}
widgetLegendState={this.state.widgetLegendState}
/>
<PrebuiltDashboardOnboardingGate
prebuiltId={dashboard.prebuiltId}
>
<Dashboard
dashboard={modifiedDashboard ?? dashboard}
isEditingDashboard={this.isEditingDashboard}
widgetLimitReached={widgetLimitReached}
onUpdate={this.handleUpdateEditStateWidgets}
handleUpdateWidgetList={this.handleUpdateWidgetList}
handleAddCustomWidget={this.handleAddCustomWidget}
isEmbedded={this.isEmbedded}
isPreview={this.isPreview}
widgetLegendState={this.state.widgetLegendState}
/>
</PrebuiltDashboardOnboardingGate>
</MEPSettingProvider>
)}
</MetricsDataSwitcher>
Expand Down Expand Up @@ -1224,24 +1229,28 @@ class DashboardDetail extends Component<Props, State> {

<Fragment>
<WidgetQueryQueueProvider>
<Dashboard
dashboard={modifiedDashboard ?? dashboard}
isEditingDashboard={this.isEditingDashboard}
widgetLimitReached={widgetLimitReached}
onUpdate={this.handleUpdateEditStateWidgets}
handleUpdateWidgetList={this.handleUpdateWidgetList}
handleAddCustomWidget={this.handleAddCustomWidget}
onAddWidget={this.onAddWidget}
isEmbedded={this.isEmbedded}
isPreview={this.isPreview}
widgetLegendState={this.state.widgetLegendState}
onEditWidget={this.onEditWidget}
newlyAddedWidget={newlyAddedWidget}
onNewWidgetScrollComplete={
this.handleScrollToNewWidgetComplete
}
widgetInterval={this.props.widgetInterval}
/>
<PrebuiltDashboardOnboardingGate
prebuiltId={dashboard.prebuiltId}
>
<Dashboard
dashboard={modifiedDashboard ?? dashboard}
isEditingDashboard={this.isEditingDashboard}
widgetLimitReached={widgetLimitReached}
onUpdate={this.handleUpdateEditStateWidgets}
handleUpdateWidgetList={this.handleUpdateWidgetList}
handleAddCustomWidget={this.handleAddCustomWidget}
onAddWidget={this.onAddWidget}
isEmbedded={this.isEmbedded}
isPreview={this.isPreview}
widgetLegendState={this.state.widgetLegendState}
onEditWidget={this.onEditWidget}
newlyAddedWidget={newlyAddedWidget}
onNewWidgetScrollComplete={
this.handleScrollToNewWidgetComplete
}
widgetInterval={this.props.widgetInterval}
/>
</PrebuiltDashboardOnboardingGate>
</WidgetQueryQueueProvider>

<WidgetBuilderV2
Expand Down
33 changes: 32 additions & 1 deletion static/app/views/dashboards/utils/prebuiltConfigs.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type {Project} from 'sentry/types/project';
import {type DashboardDetails} from 'sentry/views/dashboards/types';
import {AI_AGENTS_MODELS_PREBUILT_CONFIG} from 'sentry/views/dashboards/utils/prebuiltConfigs/ai/aiAgentsModels';
import {AI_AGENTS_OVERVIEW_PREBUILT_CONFIG} from 'sentry/views/dashboards/utils/prebuiltConfigs/ai/aiAgentsOverview';
Expand Down Expand Up @@ -27,6 +28,7 @@ import {QUEUE_SUMMARY_PREBUILT_CONFIG} from 'sentry/views/dashboards/utils/prebu
import {SESSION_HEALTH_PREBUILT_CONFIG} from 'sentry/views/dashboards/utils/prebuiltConfigs/sessionHealth';
import {WEB_VITALS_SUMMARY_PREBUILT_CONFIG} from 'sentry/views/dashboards/utils/prebuiltConfigs/webVitals/pageSummary';
import {WEB_VITALS_PREBUILT_CONFIG} from 'sentry/views/dashboards/utils/prebuiltConfigs/webVitals/webVitals';
import type {ModulesWithOnboarding} from 'sentry/views/insights/common/components/modulesOnboarding';

export enum PrebuiltDashboardId {
FRONTEND_SESSION_HEALTH = 1,
Expand Down Expand Up @@ -59,7 +61,36 @@ export enum PrebuiltDashboardId {
BACKEND_CACHES = 28,
}

export type PrebuiltDashboard = Omit<DashboardDetails, 'id'>;
/** Boolean flags on Project that indicate whether telemetry data has been received. */
type ProjectTelemetryFlag = Extract<
keyof Project,
`hasInsights${string}` | 'hasSessions'
>;

export type OnboardingConfig =
| {
moduleName: ModulesWithOnboarding;
// Single-module onboarding: shows ModulesOnboardingPanel
type: 'module';
requiredProjectFlags?: ProjectTelemetryFlag[];
}
| {
componentId: 'agent-monitoring' | 'mcp';
requiredProjectFlags: ProjectTelemetryFlag[];
// Custom onboarding component (AI Agents, MCP)
type: 'custom';
}
| {
description: string;
requiredProjectFlags: ProjectTelemetryFlag[];
// Overview dashboard onboarding: shows a generic onboarding panel
// when NONE of the listed project flags are set
type: 'overview';
};
Comment on lines +71 to +89
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do wonder if having three types is necessary here? custom and overview are very similar, overview seems like it could just be a custom with componentId: 'overview'?

In fact i'm wondering if we need types at all here, although it would require some rejigging, it feels simpler imo if each prebuilt config just mapped to the id of their onboarding component. If we didn't care about duplication, we could even just make this a function that returns a onboarding component.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They do have slightly different behaviour because depending on the type of onboarding, it has to run different checks. Plus, the different onboarding components accept different kinds of props 😬


export type PrebuiltDashboard = Omit<DashboardDetails, 'id'> & {
onboarding?: OnboardingConfig;
};

// NOTE: These configs must be in sync with the prebuilt dashboards declared in
// the backend in the `PREBUILT_DASHBOARDS` constant.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,4 +159,9 @@ export const AI_AGENTS_MODELS_PREBUILT_CONFIG: PrebuiltDashboard = {
title: 'AI Agents Models',
filters: {},
widgets: [...FIRST_ROW_WIDGETS, MODELS_TABLE],
onboarding: {
type: 'custom',
componentId: 'agent-monitoring',
requiredProjectFlags: ['hasInsightsAgentMonitoring'],
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -215,4 +215,9 @@ export const AI_AGENTS_OVERVIEW_PREBUILT_CONFIG: PrebuiltDashboard = {
globalFilter: DEFAULT_GLOBAL_FILTERS,
},
widgets: [...FIRST_ROW_WIDGETS, ...SECOND_ROW_WIDGETS, AGENTS_TRACES_TABLE],
onboarding: {
type: 'custom',
componentId: 'agent-monitoring',
requiredProjectFlags: ['hasInsightsAgentMonitoring'],
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -96,4 +96,9 @@ export const AI_AGENTS_TOOLS_PREBUILT_CONFIG: PrebuiltDashboard = {
title: 'AI Agents Tools',
filters: {},
widgets: [...FIRST_ROW_WIDGETS, TOOLS_TABLE],
onboarding: {
type: 'custom',
componentId: 'agent-monitoring',
requiredProjectFlags: ['hasInsightsAgentMonitoring'],
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -214,4 +214,9 @@ export const MCP_OVERVIEW_PREBUILT_CONFIG: PrebuiltDashboard = {
title: 'MCP Overview',
filters: {},
widgets: [...FIRST_ROW_WIDGETS, ...SECOND_ROW_WIDGETS, OVERVIEW_TABLE],
onboarding: {
type: 'custom',
componentId: 'mcp',
requiredProjectFlags: ['hasInsightsMCP'],
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -125,4 +125,9 @@ export const MCP_PROMPTS_PREBUILT_CONFIG: PrebuiltDashboard = {
title: 'MCP Prompts',
filters: {},
widgets: [...FIRST_ROW_WIDGETS, PROMPTS_TABLE],
onboarding: {
type: 'custom',
componentId: 'mcp',
requiredProjectFlags: ['hasInsightsMCP'],
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -125,4 +125,9 @@ export const MCP_RESOURCES_PREBUILT_CONFIG: PrebuiltDashboard = {
title: 'MCP Resources',
filters: {},
widgets: [...FIRST_ROW_WIDGETS, RESOURCES_TABLE],
onboarding: {
type: 'custom',
componentId: 'mcp',
requiredProjectFlags: ['hasInsightsMCP'],
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -125,4 +125,9 @@ export const MCP_TOOLS_PREBUILT_CONFIG: PrebuiltDashboard = {
title: 'MCP Tools',
filters: {},
widgets: [...FIRST_ROW_WIDGETS, TOOLS_TABLE],
onboarding: {
type: 'custom',
componentId: 'mcp',
requiredProjectFlags: ['hasInsightsMCP'],
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -301,4 +301,9 @@ export const BACKEND_OVERVIEW_PREBUILT_CONFIG: PrebuiltDashboard = {
...BACKEND_OVERVIEW_SECOND_ROW_WIDGETS,
TRANSACTIONS_TABLE,
],
onboarding: {
type: 'overview',
requiredProjectFlags: ['hasInsightsDb', 'hasInsightsHttp'],
description: 'Get started with backend tracing',
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type {PrebuiltDashboard} from 'sentry/views/dashboards/utils/prebuiltConf
import {DASHBOARD_TITLE} from 'sentry/views/dashboards/utils/prebuiltConfigs/caches/settings';
import {TABLE_MIN_HEIGHT} from 'sentry/views/dashboards/utils/prebuiltConfigs/settings';
import {spaceWidgetsEquallyOnRow} from 'sentry/views/dashboards/utils/prebuiltConfigs/utils/spaceWidgetsEquallyOnRow';
import {SpanFields} from 'sentry/views/insights/types';
import {ModuleName, SpanFields} from 'sentry/views/insights/types';

const BASE_CONDITION = `${SpanFields.SPAN_OP}:[cache.get,cache.get_item]`;

Expand Down Expand Up @@ -112,4 +112,5 @@ export const CACHES_PREBUILT_CONFIG: PrebuiltDashboard = {
title: DASHBOARD_TITLE,
filters: {},
widgets: [...FIRST_ROW_WIDGETS, TRANSACTION_TABLE],
onboarding: {type: 'module', moduleName: ModuleName.CACHE},
};
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {DASHBOARD_TITLE} from 'sentry/views/dashboards/utils/prebuiltConfigs/fro
import {TABLE_MIN_HEIGHT} from 'sentry/views/dashboards/utils/prebuiltConfigs/settings';
import {spaceWidgetsEquallyOnRow} from 'sentry/views/dashboards/utils/prebuiltConfigs/utils/spaceWidgetsEquallyOnRow';
import {DEFAULT_RESOURCE_TYPES} from 'sentry/views/insights/browser/resources/settings';
import {SpanFields} from 'sentry/views/insights/types';
import {ModuleName, SpanFields} from 'sentry/views/insights/types';

const FILTER_QUERY = MutableSearch.fromQueryObject({
has: SpanFields.NORMALIZED_DESCRIPTION,
Expand Down Expand Up @@ -167,4 +167,5 @@ export const FRONTEND_ASSETS_PREBUILT_CONFIG: PrebuiltDashboard = {
},
title: DASHBOARD_TITLE,
widgets: [...FIRST_ROW_WIDGETS, ASSETS_TABLE],
onboarding: {type: 'module', moduleName: ModuleName.RESOURCE},
};
Loading
Loading