From aa4073036ab9650d968dce3216a797d50cb2aba9 Mon Sep 17 00:00:00 2001 From: Daniel Bachhuber Date: Tue, 14 Jan 2025 06:07:21 -0800 Subject: [PATCH 1/4] Create a dashboard in one click --- frontend/src/lib/utils/eventUsageLogic.ts | 11 +++ .../experiments/ExperimentView/components.tsx | 10 +++ .../scenes/experiments/experimentLogic.tsx | 85 ++++++++++++++++++- 3 files changed, 105 insertions(+), 1 deletion(-) diff --git a/frontend/src/lib/utils/eventUsageLogic.ts b/frontend/src/lib/utils/eventUsageLogic.ts index 01e7e2859cf6e..6d08067a70bdf 100644 --- a/frontend/src/lib/utils/eventUsageLogic.ts +++ b/frontend/src/lib/utils/eventUsageLogic.ts @@ -501,6 +501,10 @@ export const eventUsageLogic = kea([ experimentId, sharedMetric, }), + reportExperimentDashboardCreated: (experiment: Experiment, dashboardId: number) => ({ + experiment, + dashboardId, + }), // Definition Popover reportDataManagementDefinitionHovered: (type: TaxonomicFilterGroupType) => ({ type }), reportDataManagementDefinitionClickView: (type: TaxonomicFilterGroupType) => ({ type }), @@ -1118,6 +1122,13 @@ export const eventUsageLogic = kea([ shared_metric_kind: sharedMetric.query.kind, }) }, + reportExperimentDashboardCreated: ({ experiment, dashboardId }) => { + posthog.capture('experiment dashboard created', { + experiment_name: experiment.name, + experiment_id: experiment.id, + dashboard_id: dashboardId, + }) + }, reportPropertyGroupFilterAdded: () => { posthog.capture('property group filter added') }, diff --git a/frontend/src/scenes/experiments/ExperimentView/components.tsx b/frontend/src/scenes/experiments/ExperimentView/components.tsx index f14b4df8adab4..5cc281cfa4603 100644 --- a/frontend/src/scenes/experiments/ExperimentView/components.tsx +++ b/frontend/src/scenes/experiments/ExperimentView/components.tsx @@ -257,6 +257,7 @@ export function PageHeaderCustom(): JSX.Element { isSingleVariantShipped, featureFlags, hasGoalSet, + isCreatingExperimentDashboard, } = useValues(experimentLogic) const { launchExperiment, @@ -266,7 +267,9 @@ export function PageHeaderCustom(): JSX.Element { loadSecondaryMetricResults, createExposureCohort, openShipVariantModal, + createExperimentDashboard, } = useActions(experimentLogic) + const exposureCohortId = experiment?.exposure_cohort return ( @@ -302,6 +305,13 @@ export function PageHeaderCustom(): JSX.Element { > {exposureCohortId ? 'View' : 'Create'} exposure cohort + createExperimentDashboard()} + fullWidth + disabled={isCreatingExperimentDashboard} + > + Create dashboard + loadMetricResults(true)} fullWidth diff --git a/frontend/src/scenes/experiments/experimentLogic.tsx b/frontend/src/scenes/experiments/experimentLogic.tsx index d7c352d782e3c..17b6545a7b23e 100644 --- a/frontend/src/scenes/experiments/experimentLogic.tsx +++ b/frontend/src/scenes/experiments/experimentLogic.tsx @@ -1,4 +1,4 @@ -import { actions, connect, kea, key, listeners, path, props, reducers, selectors } from 'kea' +import { actions, connect, isBreakpoint, kea, key, listeners, path, props, reducers, selectors } from 'kea' import { forms } from 'kea-forms' import { loaders } from 'kea-loaders' import { router, urlToAction } from 'kea-router' @@ -9,6 +9,7 @@ import { lemonToast } from 'lib/lemon-ui/LemonToast/LemonToast' import { featureFlagLogic } from 'lib/logic/featureFlagLogic' import { hasFormErrors, toParams } from 'lib/utils' import { eventUsageLogic } from 'lib/utils/eventUsageLogic' +import { addProjectIdIfMissing } from 'lib/utils/router-utils' import { indexToVariantKeyFeatureFlagPayloads, variantKeyToIndexFeatureFlagPayloads, @@ -34,6 +35,8 @@ import { ExperimentFunnelsQuery, ExperimentSignificanceCode, ExperimentTrendsQuery, + InsightQueryNode, + InsightVizNode, NodeKind, } from '~/queries/schema/schema-general' import { @@ -42,6 +45,7 @@ import { ChartDisplayType, CohortType, CountPerActorMathType, + DashboardType, EntityTypes, Experiment, ExperimentResults, @@ -146,6 +150,7 @@ export const experimentLogic = kea([ 'reportExperimentReleaseConditionsViewed', 'reportExperimentHoldoutAssigned', 'reportExperimentSharedMetricAssigned', + 'reportExperimentDashboardCreated', ], teamLogic, ['addProductIntent'], @@ -271,6 +276,8 @@ export const experimentLogic = kea([ metadata, }), removeSharedMetricFromExperiment: (sharedMetricId: SharedMetric['id']) => ({ sharedMetricId }), + createExperimentDashboard: true, + setIsCreatingExperimentDashboard: (isCreating: boolean) => ({ isCreating }), }), reducers({ experiment: [ @@ -559,6 +566,12 @@ export const experimentLogic = kea([ closeSecondarySharedMetricModal: () => false, }, ], + isCreatingExperimentDashboard: [ + false, + { + setIsCreatingExperimentDashboard: (_, { isCreating }) => isCreating, + }, + ], }), listeners(({ values, actions }) => ({ createExperiment: async ({ draft }) => { @@ -923,6 +936,76 @@ export const experimentLogic = kea([ actions.closeSecondarySharedMetricModal() actions.loadExperiment() }, + createExperimentDashboard: async () => { + actions.setIsCreatingExperimentDashboard(true) + try { + // 1. Create the dashboard + // 2. Create secondary metric insights in reverse order + // 3. Create primary metric insights in reverse order + + const experimentUrl = + window.location.origin + addProjectIdIfMissing(urls.experiment(values.experimentId)) + const dashboard: DashboardType = await api.create( + `api/environments/${teamLogic.values.currentTeamId}/dashboards/`, + { + name: values.experiment.name + ' Dashboard', + description: `Dashboard for [${experimentUrl}](${experimentUrl})`, + } as Partial + ) + + // Reverse order because adding an insight to the dashboard + // places it at the beginning of the list + for (const type of ['secondary', 'primary']) { + const singleMetrics = + type === 'secondary' ? values.experiment.metrics_secondary : values.experiment.metrics + const sharedMetrics = values.experiment?.saved_metrics.filter( + (sharedMetric) => sharedMetric.metadata.type === type + ) + const metrics = [ + ...singleMetrics, + ...sharedMetrics.map((m) => ({ name: m.name, ...m.query })), + ].reverse() + if (metrics.length === 0) { + return + } + for (const query of metrics) { + const insightQuery: InsightVizNode = { + kind: NodeKind.InsightVizNode, + source: (query.kind === NodeKind.ExperimentTrendsQuery + ? query.count_query + : query.funnels_query) as InsightQueryNode, + } + await api.create(`api/projects/${teamLogic.values.currentTeamId}/insights`, { + name: query.name || undefined, + query: insightQuery, + dashboards: [dashboard.id], + }) + } + } + + actions.reportExperimentDashboardCreated(values.experiment, dashboard.id) + + const dashboardUrl = window.location.origin + addProjectIdIfMissing(urls.dashboard(dashboard.id)) + actions.updateExperiment({ + description: + (values.experiment.description + `\n\n` || '') + + `Dashboard: [${dashboardUrl}](${dashboardUrl})`, + }) + + lemonToast.success('Dashboard created successfully', { + button: { + label: 'View dashboard', + action: () => router.actions.push(`/dashboard/${dashboard.id}`), + }, + }) + } catch (error: any) { + if (!isBreakpoint(error)) { + const message = error.code && error.detail ? `${error.code}: ${error.detail}` : error + lemonToast.error(`Could not create dashboard: ${message}`) + } + } + actions.setIsCreatingExperimentDashboard(false) + }, })), loaders(({ actions, props, values }) => ({ experiment: { From c15698388bbdda618d07780b15dc7a548b535d75 Mon Sep 17 00:00:00 2001 From: Daniel Bachhuber Date: Wed, 15 Jan 2025 04:03:11 -0800 Subject: [PATCH 2/4] Tweak dashboard name Co-authored-by: Anders <754494+andehen@users.noreply.github.com> --- frontend/src/scenes/experiments/experimentLogic.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/scenes/experiments/experimentLogic.tsx b/frontend/src/scenes/experiments/experimentLogic.tsx index 17b6545a7b23e..1ccef50191330 100644 --- a/frontend/src/scenes/experiments/experimentLogic.tsx +++ b/frontend/src/scenes/experiments/experimentLogic.tsx @@ -948,7 +948,7 @@ export const experimentLogic = kea([ const dashboard: DashboardType = await api.create( `api/environments/${teamLogic.values.currentTeamId}/dashboards/`, { - name: values.experiment.name + ' Dashboard', + name: 'Experiment: ' + values.experiment.name, description: `Dashboard for [${experimentUrl}](${experimentUrl})`, } as Partial ) From 612942530dd452ae5bf48df4ee5a243bfe9e5d8d Mon Sep 17 00:00:00 2001 From: Daniel Bachhuber Date: Wed, 15 Jan 2025 04:43:50 -0800 Subject: [PATCH 3/4] Remove unnecessary conditional --- frontend/src/scenes/experiments/experimentLogic.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/frontend/src/scenes/experiments/experimentLogic.tsx b/frontend/src/scenes/experiments/experimentLogic.tsx index 1ccef50191330..f39a32f81436d 100644 --- a/frontend/src/scenes/experiments/experimentLogic.tsx +++ b/frontend/src/scenes/experiments/experimentLogic.tsx @@ -965,9 +965,6 @@ export const experimentLogic = kea([ ...singleMetrics, ...sharedMetrics.map((m) => ({ name: m.name, ...m.query })), ].reverse() - if (metrics.length === 0) { - return - } for (const query of metrics) { const insightQuery: InsightVizNode = { kind: NodeKind.InsightVizNode, From bb099f8a97354cc38ecd816e541725156c4ce2b8 Mon Sep 17 00:00:00 2001 From: Daniel Bachhuber Date: Wed, 15 Jan 2025 05:33:07 -0800 Subject: [PATCH 4/4] Prevent 'null' from being added to the description --- frontend/src/scenes/experiments/experimentLogic.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/scenes/experiments/experimentLogic.tsx b/frontend/src/scenes/experiments/experimentLogic.tsx index f39a32f81436d..d2a330af40745 100644 --- a/frontend/src/scenes/experiments/experimentLogic.tsx +++ b/frontend/src/scenes/experiments/experimentLogic.tsx @@ -985,7 +985,7 @@ export const experimentLogic = kea([ const dashboardUrl = window.location.origin + addProjectIdIfMissing(urls.dashboard(dashboard.id)) actions.updateExperiment({ description: - (values.experiment.description + `\n\n` || '') + + (values.experiment.description ? values.experiment.description + `\n\n` : '') + `Dashboard: [${dashboardUrl}](${dashboardUrl})`, })