Skip to content

Commit c107292

Browse files
feat(dashboards): Track metrics for Seer dashboard create and edit flows (#112595)
- Adds `dashboards.seer.create.save` metric when a user saves a Seer-generated dashboard for the first time - Adds `dashboards.seer.edit.save` metric when a user saves AI-suggested changes on an existing dashboard - Adds `source` attribute (`create` | `edit`) to `dashboards.seer.validation` metric to distinguish between flows - Refactors `validateDashboardAndRecordMetrics` into shared `createFromSeerUtils` and threads `seerRunId` through the edit flow callbacks --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent f64843b commit c107292

File tree

6 files changed

+130
-65
lines changed

6 files changed

+130
-65
lines changed

static/app/views/dashboards/createFromSeer.tsx

Lines changed: 7 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,16 @@ import * as Sentry from '@sentry/react';
44
import {Alert} from '@sentry/scraps/alert';
55
import {Stack} from '@sentry/scraps/layout';
66

7-
import {validateDashboard} from 'sentry/actionCreators/dashboards';
87
import {addErrorMessage} from 'sentry/actionCreators/indicator';
98
import {ErrorBoundary} from 'sentry/components/errorBoundary';
109
import {t} from 'sentry/locale';
11-
import type {Organization} from 'sentry/types/organization';
1210
import {useLocation} from 'sentry/utils/useLocation';
1311
import {useOrganization} from 'sentry/utils/useOrganization';
1412
import {CreateFromSeerLoading} from 'sentry/views/dashboards/createFromSeerLoading';
1513
import {CreateFromSeerPrompt} from 'sentry/views/dashboards/createFromSeerPrompt';
1614

1715
import {WidgetErrorProvider} from './contexts/widgetErrorContext';
18-
import {statusIsTerminal} from './createFromSeerUtils';
16+
import {statusIsTerminal, validateDashboardAndRecordMetrics} from './createFromSeerUtils';
1917
import {DashboardChatPanel, type WidgetError} from './dashboardChatPanel';
2018
import {EMPTY_DASHBOARD} from './data';
2119
import {DashboardDetailWithInjectedProps as DashboardDetail} from './detail';
@@ -25,34 +23,6 @@ import {useSeerDashboardSession} from './useSeerDashboardSession';
2523

2624
const EMPTY_DASHBOARDS: never[] = [];
2725

28-
async function validateDashboardAndRecordMetrics(
29-
organization: Organization,
30-
newDashboard: DashboardDetails,
31-
seerRunId: number | null
32-
) {
33-
try {
34-
await validateDashboard(organization.slug, newDashboard);
35-
Sentry.metrics.count('dashboards.seer.validation', 1, {
36-
attributes: {
37-
status: 'success',
38-
organization_slug: organization.slug,
39-
...(seerRunId ? {seer_run_id: seerRunId} : {}),
40-
},
41-
});
42-
} catch (error) {
43-
Sentry.metrics.count('dashboards.seer.validation', 1, {
44-
attributes: {
45-
status: 'failure',
46-
organization_slug: organization.slug,
47-
...(seerRunId ? {seer_run_id: seerRunId} : {}),
48-
},
49-
});
50-
Sentry.captureException(error, {
51-
tags: {seer_run_id: seerRunId},
52-
});
53-
}
54-
}
55-
5626
export default function CreateFromSeer() {
5727
const organization = useOrganization();
5828
const location = useLocation();
@@ -89,7 +59,12 @@ export default function CreateFromSeer() {
8959
const handlePostCompletePollEnd = useCallback(() => {
9060
if (!hasValidatedRef.current && hasSeenNonTerminalRef.current) {
9161
hasValidatedRef.current = true;
92-
validateDashboardAndRecordMetrics(organization, dashboard, seerRunId);
62+
validateDashboardAndRecordMetrics({
63+
organization,
64+
dashboard,
65+
seerRunId,
66+
source: 'create',
67+
});
9368
}
9469
}, [organization, dashboard, seerRunId]);
9570

static/app/views/dashboards/createFromSeerUtils.tsx

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
import * as Sentry from '@sentry/react';
2+
3+
import {validateDashboard} from 'sentry/actionCreators/dashboards';
4+
import type {Organization} from 'sentry/types/organization';
15
import type {SeerExplorerResponse} from 'sentry/views/seerExplorer/hooks/useSeerExplorer';
26

37
import {
@@ -8,7 +12,7 @@ import {
812
getInitialColumnDepths,
913
} from './layoutUtils';
1014
import {DEFAULT_TABLE_LIMIT} from './types';
11-
import type {Widget, WidgetLayout} from './types';
15+
import type {DashboardDetails, Widget, WidgetLayout} from './types';
1216

1317
const DASHBOARD_ARTIFACT_KEY = 'dashboard';
1418

@@ -128,3 +132,39 @@ export function extractDashboardFromSession(
128132
export function statusIsTerminal(status?: string | null) {
129133
return status === 'completed' || status === 'error' || status === 'awaiting_user_input';
130134
}
135+
136+
export async function validateDashboardAndRecordMetrics({
137+
organization,
138+
dashboard,
139+
seerRunId,
140+
source,
141+
}: {
142+
dashboard: DashboardDetails;
143+
organization: Organization;
144+
seerRunId: number | null;
145+
source: 'create' | 'edit';
146+
}) {
147+
try {
148+
await validateDashboard(organization.slug, dashboard);
149+
Sentry.metrics.count('dashboards.seer.validation', 1, {
150+
attributes: {
151+
status: 'success',
152+
organization_slug: organization.slug,
153+
...(seerRunId ? {seer_run_id: seerRunId} : {}),
154+
source,
155+
},
156+
});
157+
} catch (error) {
158+
Sentry.metrics.count('dashboards.seer.validation', 1, {
159+
attributes: {
160+
status: 'failure',
161+
organization_slug: organization.slug,
162+
...(seerRunId ? {seer_run_id: seerRunId} : {}),
163+
source,
164+
},
165+
});
166+
Sentry.captureException(error, {
167+
tags: {seer_run_id: seerRunId},
168+
});
169+
}
170+
}

static/app/views/dashboards/dashboardChatPanel.tsx

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import {Container, Flex, Stack} from '@sentry/scraps/layout';
1111
import {IconChevron, IconSeer} from 'sentry/icons';
1212
import {t} from 'sentry/locale';
1313
import {MarkedText} from 'sentry/utils/marked/markedText';
14-
import {useLocation} from 'sentry/utils/useLocation';
1514
import {BlockComponent} from 'sentry/views/seerExplorer/blockComponents';
1615
import type {PendingUserInput} from 'sentry/views/seerExplorer/hooks/useSeerExplorer';
1716
import type {Block} from 'sentry/views/seerExplorer/types';
@@ -41,14 +40,10 @@ export function DashboardChatPanel({
4140
widgetErrors,
4241
}: DashboardChatPanelProps) {
4342
const theme = useTheme();
44-
const location = useLocation();
4543
const [inputValue, setInputValue] = useState('');
4644
const [isHistoryExpanded, setIsHistoryExpanded] = useState(true);
4745
const textAreaRef = useRef<HTMLTextAreaElement>(null);
4846
const chatContainerRef = useRef<HTMLDivElement>(null);
49-
const seerRunId = location.query?.seerRunId
50-
? Number(location.query.seerRunId)
51-
: undefined;
5247

5348
// Expand history automatically when updating triggered by user input
5449
useEffect(() => {
@@ -136,7 +131,6 @@ export function DashboardChatPanel({
136131
ref={chatContainerRef}
137132
blocks={blocks}
138133
pendingUserInput={pendingUserInput}
139-
seerRunId={seerRunId}
140134
isError={isError}
141135
widgetErrors={widgetErrors}
142136
/>
@@ -167,15 +161,13 @@ const ChatHistory = memo(function ChatHistoryInner({
167161
ref,
168162
blocks,
169163
pendingUserInput,
170-
seerRunId,
171164
isError,
172165
widgetErrors,
173166
}: {
174167
blocks: Block[];
175168
ref: React.Ref<HTMLDivElement>;
176169
isError?: boolean;
177170
pendingUserInput?: PendingUserInput | null;
178-
seerRunId?: number;
179171
widgetErrors?: WidgetError[];
180172
}) {
181173
return (
@@ -188,12 +180,7 @@ const ChatHistory = memo(function ChatHistoryInner({
188180
>
189181
<Stack>
190182
{blocks.map((block, index) => (
191-
<BlockComponent
192-
key={block.id}
193-
block={block}
194-
blockIndex={index}
195-
runId={seerRunId}
196-
/>
183+
<BlockComponent key={block.id} block={block} blockIndex={index} />
197184
))}
198185
{pendingUserInput && pendingUserInput.data.questions?.length > 0 && (
199186
<ChatMessageContainer padding="xl">

static/app/views/dashboards/dashboardEditSeerChat.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@ import {useSeerDashboardSession} from './useSeerDashboardSession';
88

99
interface DashboardEditSeerChatProps {
1010
dashboard: DashboardDetails;
11-
onDashboardUpdate: (dashboard: Pick<DashboardDetails, 'title' | 'widgets'>) => void;
11+
onDashboardUpdate: (
12+
dashboard: Pick<DashboardDetails, 'title' | 'widgets'>,
13+
seerRunId: number | null
14+
) => void;
1215
}
1316

1417
export function DashboardEditSeerChat({
@@ -22,8 +25,8 @@ export function DashboardEditSeerChat({
2225
organization.features.includes('dashboards-ai-generate');
2326

2427
const handleDashboardUpdate = useCallback(
25-
(data: {title: string; widgets: Widget[]}) => {
26-
onDashboardUpdate({title: data.title, widgets: data.widgets});
28+
(data: {title: string; widgets: Widget[]}, seerRunId: number | null) => {
29+
onDashboardUpdate({title: data.title, widgets: data.widgets}, seerRunId);
2730
},
2831
[onDashboardUpdate]
2932
);

static/app/views/dashboards/detail.tsx

Lines changed: 62 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ import {DiscoverQueryPageSource} from 'sentry/views/performance/utils';
8686

8787
import {PrebuiltDashboardOnboardingGate} from './components/prebuiltDashboardOnboardingGate';
8888
import {Controls} from './controls';
89+
import {validateDashboardAndRecordMetrics} from './createFromSeerUtils';
8990
import {Dashboard} from './dashboard';
9091
import {DashboardEditSeerChat} from './dashboardEditSeerChat';
9192
import {DEFAULT_STATS_PERIOD} from './data';
@@ -158,6 +159,8 @@ type State = {
158159
isSavingDashboardFilters: boolean;
159160
isWidgetBuilderOpen: boolean;
160161
modifiedDashboard: DashboardDetails | null;
162+
seerEditApplied: boolean;
163+
seerRunId: number | null;
161164
widgetLegendState: WidgetLegendSelectionState;
162165
widgetLimitReached: boolean;
163166
newlyAddedWidget?: Widget;
@@ -214,6 +217,8 @@ class DashboardDetail extends Component<Props, State> {
214217
openWidgetTemplates: undefined,
215218
newlyAddedWidget: undefined,
216219
isCommittingChanges: false,
220+
seerEditApplied: false,
221+
seerRunId: null,
217222
};
218223

219224
componentDidMount() {
@@ -856,6 +861,18 @@ class DashboardDetail extends Component<Props, State> {
856861
(newDashboard: DashboardDetails) => {
857862
addSuccessMessage(t('Dashboard created'));
858863
trackAnalytics('dashboards2.create.complete', {organization});
864+
const seerRunId = location.query?.seerRunId
865+
? Number(location.query.seerRunId)
866+
: null;
867+
if (seerRunId) {
868+
Sentry.metrics.count('dashboards.seer.create.save', 1, {
869+
attributes: {
870+
organization_slug: organization.slug,
871+
dashboard_id: newDashboard.id,
872+
seer_run_id: seerRunId,
873+
},
874+
});
875+
}
859876
this.setState(
860877
{
861878
dashboardState: DashboardState.VIEW,
@@ -899,6 +916,8 @@ class DashboardDetail extends Component<Props, State> {
899916
this.setState({
900917
dashboardState: DashboardState.VIEW,
901918
modifiedDashboard: null,
919+
seerEditApplied: false,
920+
seerRunId: null,
902921
});
903922
return;
904923
}
@@ -912,11 +931,22 @@ class DashboardDetail extends Component<Props, State> {
912931
}
913932
addSuccessMessage(t('Dashboard updated'));
914933
trackAnalytics('dashboards2.edit.complete', {organization});
934+
if (this.state.seerEditApplied && this.state.seerRunId) {
935+
Sentry.metrics.count('dashboards.seer.edit.save', 1, {
936+
attributes: {
937+
organization_slug: organization.slug,
938+
dashboard_id: newDashboard.id,
939+
seer_run_id: this.state.seerRunId,
940+
},
941+
});
942+
}
915943
this.setState(
916944
{
917945
dashboardState: DashboardState.VIEW,
918946
modifiedDashboard: null,
919947
isCommittingChanges: false,
948+
seerEditApplied: false,
949+
seerRunId: null,
920950
},
921951
() => {
922952
if (dashboard && newDashboard.id !== dashboard.id) {
@@ -941,6 +971,8 @@ class DashboardDetail extends Component<Props, State> {
941971
this.setState({
942972
dashboardState: DashboardState.VIEW,
943973
modifiedDashboard: null,
974+
seerEditApplied: false,
975+
seerRunId: null,
944976
});
945977
break;
946978
}
@@ -949,6 +981,8 @@ class DashboardDetail extends Component<Props, State> {
949981
this.setState({
950982
dashboardState: DashboardState.VIEW,
951983
modifiedDashboard: null,
984+
seerEditApplied: false,
985+
seerRunId: null,
952986
});
953987
break;
954988
}
@@ -961,21 +995,37 @@ class DashboardDetail extends Component<Props, State> {
961995
});
962996
};
963997

964-
handleSeerDashboardUpdate = ({
965-
title,
966-
widgets,
967-
}: Pick<DashboardDetails, 'title' | 'widgets'>) => {
968-
this.setState(state => {
969-
const dashboard = cloneDashboard(state.modifiedDashboard ?? this.props.dashboard);
970-
return {
971-
widgetLimitReached: widgets.length >= MAX_WIDGETS,
972-
modifiedDashboard: {
998+
handleSeerDashboardUpdate = (
999+
{title, widgets}: Pick<DashboardDetails, 'title' | 'widgets'>,
1000+
seerRunId: number | null
1001+
) => {
1002+
const {organization} = this.props;
1003+
this.setState(
1004+
state => {
1005+
const dashboard = cloneDashboard(state.modifiedDashboard ?? this.props.dashboard);
1006+
const updatedDashboard = {
9731007
...dashboard,
9741008
widgets,
9751009
...(title === undefined ? {} : {title}),
976-
},
977-
};
978-
});
1010+
};
1011+
return {
1012+
widgetLimitReached: widgets.length >= MAX_WIDGETS,
1013+
seerEditApplied: true,
1014+
seerRunId,
1015+
modifiedDashboard: updatedDashboard,
1016+
};
1017+
},
1018+
() => {
1019+
if (this.state.modifiedDashboard) {
1020+
validateDashboardAndRecordMetrics({
1021+
organization,
1022+
dashboard: this.state.modifiedDashboard,
1023+
seerRunId,
1024+
source: 'edit',
1025+
});
1026+
}
1027+
}
1028+
);
9791029
};
9801030

9811031
handleUpdateEditStateWidgets = (widgets: Widget[]) => {

static/app/views/dashboards/useSeerDashboardSession.tsx

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,10 @@ async function startDashboardEditSession(
3838
}
3939

4040
interface UseSeerDashboardSessionOptions {
41-
onDashboardUpdate: (data: {title: string; widgets: Widget[]}) => void;
41+
onDashboardUpdate: (
42+
data: {title: string; widgets: Widget[]},
43+
seerRunId: number | null
44+
) => void;
4245
dashboard?: Pick<DashboardDetails, 'title' | 'widgets'>;
4346
enabled?: boolean;
4447
onPostCompletePollEnd?: () => void;
@@ -127,10 +130,17 @@ export function useSeerDashboardSession({
127130
}
128131
const dashboardData = extractDashboardFromSession(session);
129132
if (dashboardData) {
130-
onDashboardUpdate(dashboardData);
133+
onDashboardUpdate(dashboardData, seerRunId);
131134
}
132135
}
133-
}, [isUpdating, sessionStatus, session, sessionUpdatedAt, onDashboardUpdate]);
136+
}, [
137+
isUpdating,
138+
sessionStatus,
139+
session,
140+
sessionUpdatedAt,
141+
onDashboardUpdate,
142+
seerRunId,
143+
]);
134144

135145
const sendFollowUpMessage = useCallback(
136146
async (message: string) => {

0 commit comments

Comments
 (0)