diff --git a/cypress/e2e/insights-navigation.cy.ts b/cypress/e2e/insights-navigation.cy.ts index 1cb88f0c2b93f..eccb3f86cbf58 100644 --- a/cypress/e2e/insights-navigation.cy.ts +++ b/cypress/e2e/insights-navigation.cy.ts @@ -71,7 +71,6 @@ describe('Insights', () => { it('can open event explorer as an insight', () => { cy.clickNavMenu('activity') - cy.get('[data-attr="data-table-export-menu"]').click() cy.get('[data-attr="open-json-editor-button"]').click() cy.get('[data-attr="insight-json-tab"]').should('exist') }) diff --git a/frontend/__snapshots__/lemon-ui-lemon-calendar-lemon-calendar-select--with-time-toggle--dark.png b/frontend/__snapshots__/lemon-ui-lemon-calendar-lemon-calendar-select--with-time-toggle--dark.png index 966ad47ae71b0..818a792142337 100644 Binary files a/frontend/__snapshots__/lemon-ui-lemon-calendar-lemon-calendar-select--with-time-toggle--dark.png and b/frontend/__snapshots__/lemon-ui-lemon-calendar-lemon-calendar-select--with-time-toggle--dark.png differ diff --git a/frontend/__snapshots__/lemon-ui-lemon-calendar-lemon-calendar-select--with-time-toggle--light.png b/frontend/__snapshots__/lemon-ui-lemon-calendar-lemon-calendar-select--with-time-toggle--light.png index 9c4f4ff773fa9..3913efdf0b265 100644 Binary files a/frontend/__snapshots__/lemon-ui-lemon-calendar-lemon-calendar-select--with-time-toggle--light.png and b/frontend/__snapshots__/lemon-ui-lemon-calendar-lemon-calendar-select--with-time-toggle--light.png differ diff --git a/frontend/src/layout/navigation-3000/sidebars/featureFlags.tsx b/frontend/src/layout/navigation-3000/sidebars/featureFlags.tsx index 16acfa8f807e5..9f022d067003e 100644 --- a/frontend/src/layout/navigation-3000/sidebars/featureFlags.tsx +++ b/frontend/src/layout/navigation-3000/sidebars/featureFlags.tsx @@ -13,7 +13,8 @@ import { teamLogic } from 'scenes/teamLogic' import { urls } from 'scenes/urls' import { groupsModel } from '~/models/groupsModel' -import { FeatureFlagType } from '~/types' +import { InsightVizNode, NodeKind } from '~/queries/schema' +import { BaseMathType, FeatureFlagType } from '~/types' import { navigation3000Logic } from '../navigationLogic' import { ExtendedListItem, SidebarCategory } from '../types' @@ -56,6 +57,26 @@ export const featureFlagsSidebarLogic = kea([ if (!featureFlag.id) { throw new Error('Feature flag ID should never be missing in the sidebar') } + + const query: InsightVizNode = { + kind: NodeKind.InsightVizNode, + source: { + kind: NodeKind.TrendsQuery, + series: [ + { + event: '$pageview', + name: '$pageview', + kind: NodeKind.EventsNode, + math: BaseMathType.UniqueUsers, + }, + ], + breakdownFilter: { + breakdown: `$feature/${featureFlag.key}`, + breakdown_type: 'event', + }, + }, + } + return { key: featureFlag.id, name: featureFlag.key, @@ -115,13 +136,7 @@ export const featureFlagsSidebarLogic = kea([ }, { label: 'Try out in Insights', - to: urls.insightNew({ - events: [ - { id: '$pageview', name: '$pageview', type: 'events', math: 'dau' }, - ], - breakdown_type: 'event', - breakdown: `$feature/${featureFlag.key}`, - }), + to: urls.insightNew(undefined, undefined, query), 'data-attr': 'usage', }, ], diff --git a/frontend/src/lib/components/CommandPalette/commandPaletteLogic.tsx b/frontend/src/lib/components/CommandPalette/commandPaletteLogic.tsx index 6b94e11224320..9f1f2d5cfda9d 100644 --- a/frontend/src/lib/components/CommandPalette/commandPaletteLogic.tsx +++ b/frontend/src/lib/components/CommandPalette/commandPaletteLogic.tsx @@ -431,7 +431,7 @@ export const commandPaletteLogic = kea([ display: 'Create a new Trend insight', executor: () => { // TODO: Don't reset insight on change - push(urls.insightNew({ insight: InsightType.TRENDS })) + push(urls.insightNew(InsightType.TRENDS)) }, }, { @@ -439,7 +439,7 @@ export const commandPaletteLogic = kea([ display: 'Create a new Funnel insight', executor: () => { // TODO: Don't reset insight on change - push(urls.insightNew({ insight: InsightType.FUNNELS })) + push(urls.insightNew(InsightType.FUNNELS)) }, }, { @@ -447,7 +447,7 @@ export const commandPaletteLogic = kea([ display: 'Create a new Retention insight', executor: () => { // TODO: Don't reset insight on change - push(urls.insightNew({ insight: InsightType.RETENTION })) + push(urls.insightNew(InsightType.RETENTION)) }, }, { @@ -455,7 +455,7 @@ export const commandPaletteLogic = kea([ display: 'Create a new Paths insight', executor: () => { // TODO: Don't reset insight on change - push(urls.insightNew({ insight: InsightType.PATHS })) + push(urls.insightNew(InsightType.PATHS)) }, }, { @@ -463,7 +463,7 @@ export const commandPaletteLogic = kea([ display: 'Create a new Stickiness insight', executor: () => { // TODO: Don't reset insight on change - push(urls.insightNew({ insight: InsightType.STICKINESS })) + push(urls.insightNew(InsightType.STICKINESS)) }, }, { @@ -471,7 +471,7 @@ export const commandPaletteLogic = kea([ display: 'Create a new Lifecycle insight', executor: () => { // TODO: Don't reset insight on change - push(urls.insightNew({ insight: InsightType.LIFECYCLE })) + push(urls.insightNew(InsightType.LIFECYCLE)) }, }, { diff --git a/frontend/src/lib/utils/eventUsageLogic.ts b/frontend/src/lib/utils/eventUsageLogic.ts index 8aef510145114..30140759c4e2a 100644 --- a/frontend/src/lib/utils/eventUsageLogic.ts +++ b/frontend/src/lib/utils/eventUsageLogic.ts @@ -34,6 +34,7 @@ import { isFunnelsQuery, isInsightQueryNode, isInsightVizNode, + isNodeWithSource, } from '~/queries/utils' import { AccessLevel, @@ -235,6 +236,7 @@ function sanitizeFilterParams(filters: AnyPartialFilterType): Record { const payload: Record = { query_kind: query?.kind, + query_source_kind: isNodeWithSource(query) ? query.source.kind : undefined, } if (isInsightVizNode(query) || isInsightQueryNode(query)) { @@ -290,7 +292,7 @@ export const eventUsageLogic = kea([ params, }), // insights - reportInsightCreated: (insightType: InsightType | null) => ({ insightType }), + reportInsightCreated: (query: Node | null) => ({ query }), reportInsightSaved: (query: Node | null, isNewInsight: boolean) => ({ query, isNewInsight }), reportInsightViewed: ( insightModel: Partial, @@ -630,10 +632,14 @@ export const eventUsageLogic = kea([ } posthog.capture('person viewed', properties) }, - reportInsightCreated: async ({ insightType }, breakpoint) => { + reportInsightCreated: async ({ query }, breakpoint) => { // "insight created" essentially means that the user clicked "New insight" await breakpoint(500) // Debounce to avoid multiple quick "New insight" clicks being reported - posthog.capture('insight created', { insight: insightType }) + + posthog.capture('insight created', { + query_kind: query?.kind, + query_source_kind: isNodeWithSource(query) ? query.source.kind : undefined, + }) }, reportInsightSaved: async ({ query, isNewInsight }) => { // "insight saved" is a proxy for the new insight's results being valuable to the user diff --git a/frontend/src/queries/nodes/DataTable/BackToSource.tsx b/frontend/src/queries/nodes/DataTable/BackToSource.tsx index c56c1b94b158f..9b47d4fa0b358 100644 --- a/frontend/src/queries/nodes/DataTable/BackToSource.tsx +++ b/frontend/src/queries/nodes/DataTable/BackToSource.tsx @@ -30,9 +30,7 @@ export function BackToSource(): JSX.Element | null { - router.actions.push(urls.insightNew(undefined, undefined, JSON.stringify(backToSourceQuery))) - } + onClick={() => router.actions.push(urls.insightNew(undefined, undefined, backToSourceQuery))} > « Back to {backToSourceQuery.source.kind?.replace('Query', '') ?? 'Insight'} diff --git a/frontend/src/queries/nodes/DataTable/DataTableOpenEditor.tsx b/frontend/src/queries/nodes/DataTable/DataTableOpenEditor.tsx index 71eb46a0e7652..6b445ca6ed491 100644 --- a/frontend/src/queries/nodes/DataTable/DataTableOpenEditor.tsx +++ b/frontend/src/queries/nodes/DataTable/DataTableOpenEditor.tsx @@ -16,11 +16,19 @@ interface DataTableOpenEditorProps { export function DataTableOpenEditor({ query }: DataTableOpenEditorProps): JSX.Element | null { const { response } = useValues(dataTableLogic) + const tableInsightQuery: DataTableNode | null = response?.hogql + ? { + kind: NodeKind.DataTableNode, + full: true, + source: { kind: NodeKind.HogQLQuery, query: response.hogql }, + } + : null + return ( } - to={urls.insightNew(undefined, undefined, JSON.stringify(query))} + to={urls.insightNew(undefined, undefined, query)} sideAction={ response?.hogql ? { @@ -30,15 +38,7 @@ export function DataTableOpenEditor({ query }: DataTableOpenEditorProps): JSX.El items={[ { label: 'Open as direct SQL insight', - to: urls.insightNew( - undefined, - undefined, - JSON.stringify({ - kind: NodeKind.DataTableNode, - full: true, - source: { kind: NodeKind.HogQLQuery, query: response.hogql }, - }) - ), + to: urls.insightNew(undefined, undefined, tableInsightQuery!), 'data-attr': 'open-sql-editor-button', }, ]} diff --git a/frontend/src/queries/nodes/Node/EditHogQLButton.tsx b/frontend/src/queries/nodes/Node/EditHogQLButton.tsx index b079d09840141..e116b98bb8e99 100644 --- a/frontend/src/queries/nodes/Node/EditHogQLButton.tsx +++ b/frontend/src/queries/nodes/Node/EditHogQLButton.tsx @@ -2,26 +2,23 @@ import { IconQueryEditor } from 'lib/lemon-ui/icons' import { LemonButton, LemonButtonWithoutSideActionProps } from 'lib/lemon-ui/LemonButton' import { urls } from 'scenes/urls' -import { NodeKind } from '~/queries/schema' +import { DataTableNode, NodeKind } from '~/queries/schema' export interface EditHogQLButtonProps extends LemonButtonWithoutSideActionProps { hogql: string } export function EditHogQLButton({ hogql, ...props }: EditHogQLButtonProps): JSX.Element { + const query: DataTableNode = { + kind: NodeKind.DataTableNode, + full: true, + source: { kind: NodeKind.HogQLQuery, query: hogql }, + } return ( } tooltip="Edit SQL directly" {...props} diff --git a/frontend/src/queries/nodes/Node/OpenEditorButton.tsx b/frontend/src/queries/nodes/Node/OpenEditorButton.tsx index 43c50ad820946..7e29aa6188ddd 100644 --- a/frontend/src/queries/nodes/Node/OpenEditorButton.tsx +++ b/frontend/src/queries/nodes/Node/OpenEditorButton.tsx @@ -13,7 +13,7 @@ export function OpenEditorButton({ query, ...props }: OpenEditorButtonProps): JS } tooltip="Open as a new insight" {...props} diff --git a/frontend/src/scenes/data-management/actions/ActionsTable.tsx b/frontend/src/scenes/data-management/actions/ActionsTable.tsx index a0164fad09672..9bb24e85a09a6 100644 --- a/frontend/src/scenes/data-management/actions/ActionsTable.tsx +++ b/frontend/src/scenes/data-management/actions/ActionsTable.tsx @@ -1,7 +1,6 @@ import { IconCheckCircle } from '@posthog/icons' import { LemonInput, LemonSegmentedButton } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' -import { combineUrl } from 'kea-router' import api from 'lib/api' import { ObjectTags } from 'lib/components/ObjectTags/ObjectTags' import { ProductIntroduction } from 'lib/components/ProductIntroduction/ProductIntroduction' @@ -19,15 +18,8 @@ import { actionsLogic } from 'scenes/actions/actionsLogic' import { userLogic } from 'scenes/userLogic' import { actionsModel } from '~/models/actionsModel' -import { - ActionType, - AvailableFeature, - ChartDisplayType, - FilterLogicalOperator, - InsightType, - ProductKey, - ReplayTabs, -} from '~/types' +import { InsightVizNode, NodeKind } from '~/queries/schema' +import { ActionType, AvailableFeature, ChartDisplayType, FilterLogicalOperator, ProductKey, ReplayTabs } from '~/types' import { NewActionButton } from '../../actions/NewActionButton' import { teamLogic } from '../../teamLogic' @@ -44,6 +36,25 @@ export function ActionsTable(): JSX.Element { const { hasAvailableFeature } = useValues(userLogic) const { updateHasSeenProductIntroFor } = useActions(userLogic) + const tryInInsightsUrl = (action: ActionType): string => { + const query: InsightVizNode = { + kind: NodeKind.InsightVizNode, + source: { + kind: NodeKind.TrendsQuery, + series: [ + { + id: action.id, + name: action.name || undefined, + kind: NodeKind.ActionsNode, + }, + ], + interval: 'day', + trendsFilter: { display: ChartDisplayType.ActionsLineGraph }, + }, + } + return urls.insightNew(undefined, undefined, query) + } + const columns: LemonTableColumns = [ { title: 'Name', @@ -183,26 +194,7 @@ export function ActionsTable(): JSX.Element { > View recordings - + Try out in Insights diff --git a/frontend/src/scenes/data-management/database/DatabaseTables.tsx b/frontend/src/scenes/data-management/database/DatabaseTables.tsx index da69f2ff923b0..4304507754161 100644 --- a/frontend/src/scenes/data-management/database/DatabaseTables.tsx +++ b/frontend/src/scenes/data-management/database/DatabaseTables.tsx @@ -80,7 +80,7 @@ export function DatabaseTables({ const query = defaultQuery(table as string, Object.values(obj.fields)) return (
- + {table}
diff --git a/frontend/src/scenes/data-warehouse/external/forms/SyncProgressStep.tsx b/frontend/src/scenes/data-warehouse/external/forms/SyncProgressStep.tsx index e38cad0634274..03579c9a1cd44 100644 --- a/frontend/src/scenes/data-warehouse/external/forms/SyncProgressStep.tsx +++ b/frontend/src/scenes/data-warehouse/external/forms/SyncProgressStep.tsx @@ -74,7 +74,7 @@ export const SyncProgressStep = (): JSX.Element => { className="my-1" type="primary" onClick={() => cancelWizard()} - to={urls.insightNew(undefined, undefined, JSON.stringify(query))} + to={urls.insightNew(undefined, undefined, query)} > Query
diff --git a/frontend/src/scenes/experiments/ExperimentView/components.tsx b/frontend/src/scenes/experiments/ExperimentView/components.tsx index 0e808fb60a18a..73544888ab671 100644 --- a/frontend/src/scenes/experiments/ExperimentView/components.tsx +++ b/frontend/src/scenes/experiments/ExperimentView/components.tsx @@ -24,7 +24,7 @@ import { urls } from 'scenes/urls' import { filtersToQueryNode } from '~/queries/nodes/InsightQuery/utils/filtersToQueryNode' import { Query } from '~/queries/Query/Query' -import { NodeKind } from '~/queries/schema' +import { InsightVizNode, NodeKind } from '~/queries/schema' import { Experiment as ExperimentType, ExperimentResults, FilterType, InsightShortId, InsightType } from '~/types' import { experimentLogic } from '../experimentLogic' @@ -121,29 +121,27 @@ export function ExploreButton({ icon = }: { icon?: JSX.Element properties: [], } + const query: InsightVizNode = { + kind: NodeKind.InsightVizNode, + source: filtersToQueryNode( + transformResultFilters( + experimentResults?.filters + ? { ...experimentResults.filters, explicit_date: true } + : filtersFromExperiment + ) + ), + showTable: true, + showLastComputation: true, + showLastComputationRefresh: false, + } + return ( Explore results diff --git a/frontend/src/scenes/feature-flags/FeatureFlags.tsx b/frontend/src/scenes/feature-flags/FeatureFlags.tsx index 4824530c97dc7..bb935951ca995 100644 --- a/frontend/src/scenes/feature-flags/FeatureFlags.tsx +++ b/frontend/src/scenes/feature-flags/FeatureFlags.tsx @@ -25,10 +25,12 @@ import { urls } from 'scenes/urls' import { userLogic } from 'scenes/userLogic' import { groupsModel, Noun } from '~/models/groupsModel' +import { InsightVizNode, NodeKind } from '~/queries/schema' import { ActivityScope, AnyPropertyFilter, AvailableFeature, + BaseMathType, FeatureFlagFilters, FeatureFlagType, ProductKey, @@ -60,6 +62,28 @@ export function OverViewTab({ const { updateFeatureFlag, loadFeatureFlags, setSearchTerm, setFeatureFlagsFilters } = useActions(flagLogic) const { hasAvailableFeature } = useValues(userLogic) + const tryInInsightsUrl = (featureFlag: FeatureFlagType): string => { + const query: InsightVizNode = { + kind: NodeKind.InsightVizNode, + source: { + kind: NodeKind.TrendsQuery, + series: [ + { + event: '$pageview', + name: '$pageview', + kind: NodeKind.EventsNode, + math: BaseMathType.UniqueUsers, + }, + ], + breakdownFilter: { + breakdown_type: 'event', + breakdown: `$feature/${featureFlag.key}`, + }, + }, + } + return urls.insightNew(undefined, undefined, query) + } + const columns: LemonTableColumns = [ { title: 'Key', @@ -203,15 +227,7 @@ export function OverViewTab({ Edit
)} - + Try out in Insights diff --git a/frontend/src/scenes/feature-flags/RecentFeatureFlagInsightsCard.tsx b/frontend/src/scenes/feature-flags/RecentFeatureFlagInsightsCard.tsx index 225a69bdbf761..4cfaec0046a92 100644 --- a/frontend/src/scenes/feature-flags/RecentFeatureFlagInsightsCard.tsx +++ b/frontend/src/scenes/feature-flags/RecentFeatureFlagInsightsCard.tsx @@ -3,12 +3,26 @@ import { CompactList } from 'lib/components/CompactList/CompactList' import { InsightRow } from 'scenes/project-homepage/RecentInsights' import { urls } from 'scenes/urls' -import { InsightModel } from '~/types' +import { InsightVizNode, NodeKind } from '~/queries/schema' +import { BaseMathType, InsightModel } from '~/types' import { featureFlagLogic } from './featureFlagLogic' export function RecentFeatureFlagInsights(): JSX.Element { const { relatedInsights, relatedInsightsLoading, featureFlag } = useValues(featureFlagLogic) + const query: InsightVizNode = { + kind: NodeKind.InsightVizNode, + source: { + kind: NodeKind.TrendsQuery, + series: [ + { event: '$pageview', name: '$pageview', kind: NodeKind.EventsNode, math: BaseMathType.UniqueUsers }, + ], + breakdownFilter: { + breakdown_type: 'event', + breakdown: `$feature/${featureFlag.key}`, + }, + }, + } return ( } diff --git a/frontend/src/scenes/funnels/FunnelStepMore.tsx b/frontend/src/scenes/funnels/FunnelStepMore.tsx index e083a06aba36f..80d5b88b5935a 100644 --- a/frontend/src/scenes/funnels/FunnelStepMore.tsx +++ b/frontend/src/scenes/funnels/FunnelStepMore.tsx @@ -6,7 +6,8 @@ import { cleanFilters } from 'scenes/insights/utils/cleanFilters' import { urls } from 'scenes/urls' import { queryNodeToFilter } from '~/queries/nodes/InsightQuery/utils/queryNodeToFilter' -import { FunnelPathType, InsightType, PathType } from '~/types' +import { InsightVizNode, NodeKind } from '~/queries/schema' +import { FunnelPathType, PathType } from '~/types' import { funnelDataLogic } from './funnelDataLogic' @@ -28,6 +29,28 @@ export function FunnelStepMore({ stepIndex }: FunnelStepMoreProps): JSX.Element } const stepNumber = stepIndex + 1 + const getPathUrl = (funnelPathType: FunnelPathType, dropOff = false): string => { + const query: InsightVizNode = { + kind: NodeKind.InsightVizNode, + source: { + kind: NodeKind.PathsQuery, + funnelPathsFilter: { + funnelStep: dropOff ? stepNumber * -1 : stepNumber, + funnelSource: querySource!, + funnelPathType, + }, + pathsFilter: { + includeEventTypes: [PathType.PageView, PathType.CustomEvent], + }, + dateRange: { + date_from: filterProps.date_from, + }, + }, + } + + return urls.insightNew(undefined, undefined, query) + } + return ( {stepNumber > 1 && ( - + Show user paths leading to step )} {stepNumber > 1 && ( - + Show user paths between previous step and this step )} - + Show user paths after step {stepNumber > 1 && ( - + Show user paths after dropoff )} {stepNumber > 1 && ( - + Show user paths before dropoff )} diff --git a/frontend/src/scenes/insights/InsightNav/insightNavLogic.tsx b/frontend/src/scenes/insights/InsightNav/insightNavLogic.tsx index c4ac869a1ec34..a245432b3a3a8 100644 --- a/frontend/src/scenes/insights/InsightNav/insightNavLogic.tsx +++ b/frontend/src/scenes/insights/InsightNav/insightNavLogic.tsx @@ -1,14 +1,12 @@ import { actions, afterMount, connect, kea, key, listeners, path, props, reducers, selectors } from 'kea' -import { urlToAction } from 'kea-router' import { FEATURE_FLAGS } from 'lib/constants' import { LemonTag } from 'lib/lemon-ui/LemonTag/LemonTag' import { featureFlagLogic } from 'lib/logic/featureFlagLogic' import { identifierToHuman } from 'lib/utils' -import { insightDataLogic, queryFromKind } from 'scenes/insights/insightDataLogic' +import { getDefaultQuery, insightDataLogic } from 'scenes/insights/insightDataLogic' import { keyForInsightLogicProps } from 'scenes/insights/sharedUtils' import { filterTestAccountsDefaultsLogic } from 'scenes/settings/project/filterTestAccountDefaultsLogic' -import { examples, TotalEventsTable } from '~/queries/examples' import { insightMap } from '~/queries/nodes/InsightQuery/utils/queryNodeToFilter' import { ActionsNode, @@ -20,7 +18,6 @@ import { InsightVizNode, LifecycleFilter, LifecycleQuery, - NodeKind, PathsFilter, PathsQuery, RetentionFilter, @@ -47,9 +44,10 @@ import { isStickinessQuery, isTrendsQuery, } from '~/queries/utils' -import { BaseMathType, FilterType, InsightLogicProps, InsightType } from '~/types' +import { BaseMathType, InsightLogicProps, InsightType } from '~/types' import { MathAvailability } from '../filters/ActionFilter/ActionFilterRow/ActionFilterRow' +import { insightSceneLogic } from '../insightSceneLogic' import type { insightNavLogicType } from './insightNavLogicType' export interface Tab { @@ -116,7 +114,7 @@ export const insightNavLogic = kea([ filterTestAccountsDefaultsLogic, ['filterTestAccountsDefault'], ], - actions: [insightDataLogic(props), ['setQuery']], + actions: [insightDataLogic(props), ['setQuery'], insightSceneLogic, ['setOpenedWithQuery']], })), actions({ setActiveView: (view: InsightType) => ({ view }), @@ -224,41 +222,19 @@ export const insightNavLogic = kea([ }), listeners(({ values, actions }) => ({ setActiveView: ({ view }) => { - if ([InsightType.SQL, InsightType.JSON, InsightType.HOG].includes(view as InsightType)) { - // if the selected view is SQL or JSON then we must have the "allow queries" flag on, - // so no need to check it - if (view === InsightType.JSON) { - actions.setQuery(TotalEventsTable) - } else if (view === InsightType.SQL) { - actions.setQuery(examples.DataVisualization) - } else if (view === InsightType.HOG) { - actions.setQuery(examples.Hoggonacci) - } - } else { - let query: InsightVizNode - - if (view === InsightType.TRENDS) { - query = queryFromKind(NodeKind.TrendsQuery, values.filterTestAccountsDefault) - } else if (view === InsightType.FUNNELS) { - query = queryFromKind(NodeKind.FunnelsQuery, values.filterTestAccountsDefault) - } else if (view === InsightType.RETENTION) { - query = queryFromKind(NodeKind.RetentionQuery, values.filterTestAccountsDefault) - } else if (view === InsightType.PATHS) { - query = queryFromKind(NodeKind.PathsQuery, values.filterTestAccountsDefault) - } else if (view === InsightType.STICKINESS) { - query = queryFromKind(NodeKind.StickinessQuery, values.filterTestAccountsDefault) - } else if (view === InsightType.LIFECYCLE) { - query = queryFromKind(NodeKind.LifecycleQuery, values.filterTestAccountsDefault) - } else { - throw new Error('encountered unexpected type for view') - } + const query = getDefaultQuery(view, values.filterTestAccountsDefault) + if (isInsightVizNode(query)) { actions.setQuery({ ...query, source: values.queryPropertyCache ? mergeCachedProperties(query.source, values.queryPropertyCache) : query.source, } as InsightVizNode) + actions.setOpenedWithQuery(query) + } else { + actions.setQuery(query) + actions.setOpenedWithQuery(query) } }, setQuery: ({ query }) => { @@ -267,23 +243,6 @@ export const insightNavLogic = kea([ } }, })), - urlToAction(({ actions }) => ({ - '/insights/:shortId(/:mode)(/:subscriptionId)': ( - _, // url params - { dashboard, ...searchParams }, // search params - { filters: _filters } // hash params - ) => { - // capture any filters from the URL, either #filters={} or ?insight=X&bla=foo&bar=baz - const filters: Partial | null = - Object.keys(_filters || {}).length > 0 ? _filters : searchParams.insight ? searchParams : null - - if (!filters?.insight) { - return - } - - actions.setActiveView(filters?.insight) - }, - })), afterMount(({ values, actions }) => { if (values.query && isInsightVizNode(values.query)) { actions.updateQueryPropertyCache(cachePropertiesFromQuery(values.query.source, values.queryPropertyCache)) diff --git a/frontend/src/scenes/insights/InsightPageHeader.tsx b/frontend/src/scenes/insights/InsightPageHeader.tsx index 708152a4ede69..c5bd094f6c86f 100644 --- a/frontend/src/scenes/insights/InsightPageHeader.tsx +++ b/frontend/src/scenes/insights/InsightPageHeader.tsx @@ -210,18 +210,14 @@ export function InsightPageHeader({ insightLogicProps }: { insightLogicProps: In data-attr="edit-insight-sql" onClick={() => { router.actions.push( - urls.insightNew( - undefined, - undefined, - JSON.stringify({ - kind: NodeKind.DataTableNode, - source: { - kind: NodeKind.HogQLQuery, - query: hogQL, - }, - full: true, - } as DataTableNode) - ) + urls.insightNew(undefined, undefined, { + kind: NodeKind.DataTableNode, + source: { + kind: NodeKind.HogQLQuery, + query: hogQL, + }, + full: true, + } as DataTableNode) ) }} fullWidth diff --git a/frontend/src/scenes/insights/insightDataLogic.tsx b/frontend/src/scenes/insights/insightDataLogic.tsx index b1075ba7c321d..bca93a4cf03b4 100644 --- a/frontend/src/scenes/insights/insightDataLogic.tsx +++ b/frontend/src/scenes/insights/insightDataLogic.tsx @@ -14,7 +14,15 @@ import { filtersToQueryNode } from '~/queries/nodes/InsightQuery/utils/filtersTo import { queryNodeToFilter } from '~/queries/nodes/InsightQuery/utils/queryNodeToFilter' import { insightVizDataNodeKey } from '~/queries/nodes/InsightViz/InsightViz' import { queryExportContext } from '~/queries/query' -import { InsightNodeKind, InsightVizNode, Node, NodeKind } from '~/queries/schema' +import { + DataTableNode, + DataVisualizationNode, + HogQuery, + InsightNodeKind, + InsightVizNode, + Node, + NodeKind, +} from '~/queries/schema' import { isInsightVizNode } from '~/queries/utils' import { ExportContext, FilterType, InsightLogicProps, InsightType } from '~/types' @@ -30,6 +38,37 @@ export const queryFromFilters = (filters: Partial): InsightVizNode = source: filtersToQueryNode(filters), }) +export const getDefaultQuery = ( + insightType: InsightType, + filterTestAccountsDefault: boolean +): DataTableNode | DataVisualizationNode | HogQuery | InsightVizNode => { + if ([InsightType.SQL, InsightType.JSON, InsightType.HOG].includes(insightType)) { + if (insightType === InsightType.JSON) { + return examples.TotalEventsTable as DataTableNode + } else if (insightType === InsightType.SQL) { + return examples.DataVisualization as DataVisualizationNode + } else if (insightType === InsightType.HOG) { + return examples.Hoggonacci as HogQuery + } + } else { + if (insightType === InsightType.TRENDS) { + return queryFromKind(NodeKind.TrendsQuery, filterTestAccountsDefault) + } else if (insightType === InsightType.FUNNELS) { + return queryFromKind(NodeKind.FunnelsQuery, filterTestAccountsDefault) + } else if (insightType === InsightType.RETENTION) { + return queryFromKind(NodeKind.RetentionQuery, filterTestAccountsDefault) + } else if (insightType === InsightType.PATHS) { + return queryFromKind(NodeKind.PathsQuery, filterTestAccountsDefault) + } else if (insightType === InsightType.STICKINESS) { + return queryFromKind(NodeKind.StickinessQuery, filterTestAccountsDefault) + } else if (insightType === InsightType.LIFECYCLE) { + return queryFromKind(NodeKind.LifecycleQuery, filterTestAccountsDefault) + } + } + + throw new Error('encountered unexpected type for view') +} + export const queryFromKind = (kind: InsightNodeKind, filterTestAccountsDefault: boolean): InsightVizNode => ({ kind: NodeKind.InsightVizNode, source: { ...nodeKindToDefaultQuery[kind], ...(filterTestAccountsDefault ? { filterTestAccounts: true } : {}) }, diff --git a/frontend/src/scenes/insights/insightSceneLogic.test.ts b/frontend/src/scenes/insights/insightSceneLogic.test.ts index c2a45b334145d..8ba30590bbf75 100644 --- a/frontend/src/scenes/insights/insightSceneLogic.test.ts +++ b/frontend/src/scenes/insights/insightSceneLogic.test.ts @@ -6,7 +6,8 @@ import { insightSceneLogic } from 'scenes/insights/insightSceneLogic' import { urls } from 'scenes/urls' import { useMocks } from '~/mocks/jest' -import { InsightVizNode } from '~/queries/schema' +import { examples } from '~/queries/examples' +import { InsightVizNode, NodeKind } from '~/queries/schema' import { initKeaTests } from '~/test/init' import { InsightShortId, InsightType, ItemMode } from '~/types' @@ -43,8 +44,8 @@ describe('insightSceneLogic', () => { }) }) - it('redirects when opening /insight/new with filters', async () => { - router.actions.push(urls.insightNew({ insight: InsightType.FUNNELS })) + it('redirects when opening /insight/new with insight type in theurl', async () => { + router.actions.push(urls.insightNew(InsightType.FUNNELS)) await expectLogic(logic).toFinishAllListeners() await expectLogic(router) .delay(1) @@ -61,6 +62,29 @@ describe('insightSceneLogic', () => { ).toEqual('FunnelsQuery') }) + it('redirects when opening /insight/new with query in the url', async () => { + router.actions.push( + urls.insightNew(undefined, undefined, { + kind: NodeKind.InsightVizNode, + source: examples.InsightPathsQuery, + } as InsightVizNode) + ) + await expectLogic(logic).toFinishAllListeners() + await expectLogic(router) + .delay(1) + .toMatchValues({ + location: partial({ + pathname: addProjectIdIfMissing(urls.insightNew(), MOCK_TEAM_ID), + search: '', + hash: '', + }), + }) + + expect( + (logic.values.insightLogicRef?.logic.values.queryBasedInsight.query as InsightVizNode).source?.kind + ).toEqual('PathsQuery') + }) + it('persists edit mode in the url', async () => { const viewUrl = combineUrl(urls.insightView(Insight42)) const editUrl = combineUrl(urls.insightEdit(Insight42)) diff --git a/frontend/src/scenes/insights/insightSceneLogic.tsx b/frontend/src/scenes/insights/insightSceneLogic.tsx index ad5eff574651d..2e1220067564f 100644 --- a/frontend/src/scenes/insights/insightSceneLogic.tsx +++ b/frontend/src/scenes/insights/insightSceneLogic.tsx @@ -1,14 +1,14 @@ import { actions, BuiltLogic, connect, kea, listeners, path, reducers, selectors, sharedListeners } from 'kea' import { actionToUrl, beforeUnload, router, urlToAction } from 'kea-router' import { CombinedLocation } from 'kea-router/lib/utils' -import { lemonToast } from 'lib/lemon-ui/LemonToast/LemonToast' +import { objectsEqual } from 'lib/utils' import { eventUsageLogic, InsightEventSource } from 'lib/utils/eventUsageLogic' import { createEmptyInsight, insightLogic } from 'scenes/insights/insightLogic' import { insightLogicType } from 'scenes/insights/insightLogicType' -import { cleanFilters } from 'scenes/insights/utils/cleanFilters' import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' import { sceneLogic } from 'scenes/sceneLogic' import { Scene } from 'scenes/sceneTypes' +import { filterTestAccountsDefaultsLogic } from 'scenes/settings/project/filterTestAccountDefaultsLogic' import { teamLogic } from 'scenes/teamLogic' import { mathsLogic } from 'scenes/trends/mathsLogic' import { urls } from 'scenes/urls' @@ -16,18 +16,31 @@ import { urls } from 'scenes/urls' import { ActivityFilters } from '~/layout/navigation-3000/sidepanel/panels/activity/activityForSceneLogic' import { cohortsModel } from '~/models/cohortsModel' import { groupsModel } from '~/models/groupsModel' -import { ActivityScope, Breadcrumb, FilterType, InsightShortId, InsightType, ItemMode } from '~/types' +import { queryNodeToFilter } from '~/queries/nodes/InsightQuery/utils/queryNodeToFilter' +import { Node } from '~/queries/schema' +import { isInsightVizNode } from '~/queries/utils' +import { ActivityScope, Breadcrumb, InsightShortId, InsightType, ItemMode } from '~/types' -import { insightDataLogic } from './insightDataLogic' +import { getDefaultQuery, insightDataLogic } from './insightDataLogic' import { insightDataLogicType } from './insightDataLogicType' import type { insightSceneLogicType } from './insightSceneLogicType' import { summarizeInsight } from './summarizeInsight' +import { compareFilters } from './utils/compareFilters' export const insightSceneLogic = kea([ path(['scenes', 'insights', 'insightSceneLogic']), connect(() => ({ logic: [eventUsageLogic], - values: [teamLogic, ['currentTeam'], sceneLogic, ['activeScene'], preflightLogic, ['disableNavigationHooks']], + values: [ + teamLogic, + ['currentTeam'], + sceneLogic, + ['activeScene'], + preflightLogic, + ['disableNavigationHooks'], + filterTestAccountsDefaultsLogic, + ['filterTestAccountsDefault'], + ], })), actions({ setInsightId: (insightId: InsightShortId) => ({ insightId }), @@ -45,6 +58,7 @@ export const insightSceneLogic = kea([ logic, unmount, }), + setOpenedWithQuery: (query: Node | null) => ({ query }), }), reducers({ insightId: [ @@ -88,6 +102,7 @@ export const insightSceneLogic = kea([ setInsightDataLogicRef: (_, { logic, unmount }) => (logic && unmount ? { logic, unmount } : null), }, ], + openedWithQuery: [null as Node | null, { setOpenedWithQuery: (_, { query }) => query }], }), selectors(() => ({ legacyInsightSelector: [ @@ -190,7 +205,7 @@ export const insightSceneLogic = kea([ '/insights/:shortId(/:mode)(/:subscriptionId)': ( { shortId, mode, subscriptionId }, // url params { dashboard, ...searchParams }, // search params - { filters: _filters, q }, // hash params + { insight: insightType, q }, // hash params { method, initial }, // "location changed" event payload { searchParams: previousSearchParams } // previous location ) => { @@ -230,12 +245,15 @@ export const insightSceneLogic = kea([ actions.setSceneState(insightId, insightMode, subscriptionId) } - // capture any filters from the URL, either #filters={} or ?insight=X&bla=foo&bar=baz - const filters: Partial | null = - Object.keys(_filters || {}).length > 0 ? _filters : searchParams.insight ? searchParams : null + let query: Node | null = null + if (q) { + query = JSON.parse(q) + } else if (insightType && Object.values(InsightType).includes(insightType)) { + query = getDefaultQuery(insightType, values.filterTestAccountsDefault) + } - // Redirect to a simple URL if we had filters in the URL - if (filters || q) { + // Redirect to a simple URL if we had a query in the URL + if (q || insightType) { router.actions.replace( insightId === 'new' ? urls.insightNew(undefined, dashboard) @@ -246,15 +264,14 @@ export const insightSceneLogic = kea([ } // reset the insight's state if we have to - if (initial || method === 'PUSH' || filters || q) { + if (initial || method === 'PUSH' || query) { if (insightId === 'new') { const teamFilterTestAccounts = values.currentTeam?.test_account_filters_default_checked || false values.insightLogicRef?.logic.actions.setInsight( { ...createEmptyInsight('new', teamFilterTestAccounts), - ...(filters ? { filters: cleanFilters(filters || {}, teamFilterTestAccounts) } : {}), ...(dashboard ? { dashboards: [dashboard] } : {}), - ...(q ? { query: JSON.parse(q) } : {}), + ...(query ? { query } : {}), }, { fromPersistentApi: false, @@ -262,13 +279,10 @@ export const insightSceneLogic = kea([ } ) - eventUsageLogic.actions.reportInsightCreated(filters?.insight || InsightType.TRENDS) - } - } + actions.setOpenedWithQuery(query || null) - // show a warning toast if opened `/edit#filters={...}` - if (filters && insightMode === ItemMode.Edit && insightId !== 'new') { - lemonToast.info(`This insight has unsaved changes! Click "Save" to not lose them.`) + eventUsageLogic.actions.reportInsightCreated(query) + } } }, })), @@ -292,7 +306,7 @@ export const insightSceneLogic = kea([ setInsightMode: actionToUrl, } }), - beforeUnload(({ values }) => ({ + beforeUnload(({ values, props }) => ({ enabled: (newLocation?: CombinedLocation) => { // safeguard against running this check on other scenes if (values.activeScene !== Scene.Insight) { @@ -310,10 +324,44 @@ export const insightSceneLogic = kea([ return false } + let newInsightEdited = false + if (props.dashboardItemId === 'new') { + const startingQuery = + values.openedWithQuery || getDefaultQuery(InsightType.TRENDS, values.filterTestAccountsDefault) + const currentQuery = values.insightDataLogicRef?.logic.values.query + + if (isInsightVizNode(startingQuery) && isInsightVizNode(currentQuery)) { + // TODO: This shouldn't be necessary after we have removed the `cleanFilters` function. + // Currently this causes "default" properties to be set on the query. + const startingFilters = queryNodeToFilter(startingQuery.source) + const currentFilters = queryNodeToFilter(currentQuery.source) + + if ( + currentFilters.filter_test_accounts === false && + currentFilters.filter_test_accounts === values.filterTestAccountsDefault + ) { + delete currentFilters.filter_test_accounts + } + + newInsightEdited = !compareFilters( + startingFilters, + currentFilters, + values.filterTestAccountsDefault + ) + } else if (!isInsightVizNode(startingQuery) && !isInsightVizNode(currentQuery)) { + newInsightEdited = !objectsEqual(startingQuery, currentQuery) + } else { + newInsightEdited = true + } + } + + const insightMetadataEdited = !!values.insightLogicRef?.logic.values.insightChanged + const savedInsightEdited = + props.dashboardItemId !== 'new' && !!values.insightDataLogicRef?.logic.values.queryChanged + return ( values.insightMode === ItemMode.Edit && - (!!values.insightLogicRef?.logic.values.insightChanged || - !!values.insightDataLogicRef?.logic.values.queryChanged) + (insightMetadataEdited || savedInsightEdited || newInsightEdited) ) }, message: 'Leave insight?\nChanges you made will be discarded.', diff --git a/frontend/src/scenes/insights/utils.tsx b/frontend/src/scenes/insights/utils.tsx index f413e548c6805..c28c841647165 100644 --- a/frontend/src/scenes/insights/utils.tsx +++ b/frontend/src/scenes/insights/utils.tsx @@ -9,7 +9,15 @@ import { urls } from 'scenes/urls' import { propertyFilterTypeToPropertyDefinitionType } from '~/lib/components/PropertyFilters/utils' import { FormatPropertyValueForDisplayFunction } from '~/models/propertyDefinitionsModel' import { examples } from '~/queries/examples' -import { ActionsNode, BreakdownFilter, DataWarehouseNode, EventsNode, PathsFilter } from '~/queries/schema' +import { + ActionsNode, + BreakdownFilter, + DataWarehouseNode, + EventsNode, + InsightVizNode, + NodeKind, + PathsFilter, +} from '~/queries/schema' import { isDataWarehouseNode, isEventsNode } from '~/queries/utils' import { ActionFilter, @@ -24,7 +32,8 @@ import { InsightShortId, InsightType, PathType, - TrendsFilterType, + PropertyFilterType, + PropertyOperator, } from '~/types' import { insightLogic } from './insightLogic' @@ -355,15 +364,15 @@ export function getResponseBytes(apiResponse: Response): number { } export const insightTypeURL = { - TRENDS: urls.insightNew({ insight: InsightType.TRENDS }), - STICKINESS: urls.insightNew({ insight: InsightType.STICKINESS }), - LIFECYCLE: urls.insightNew({ insight: InsightType.LIFECYCLE }), - FUNNELS: urls.insightNew({ insight: InsightType.FUNNELS }), - RETENTION: urls.insightNew({ insight: InsightType.RETENTION }), - PATHS: urls.insightNew({ insight: InsightType.PATHS }), - JSON: urls.insightNew(undefined, undefined, JSON.stringify(examples.EventsTableFull)), - HOG: urls.insightNew(undefined, undefined, JSON.stringify(examples.Hoggonacci)), - SQL: urls.insightNew(undefined, undefined, JSON.stringify(examples.DataVisualization)), + TRENDS: urls.insightNew(InsightType.TRENDS), + STICKINESS: urls.insightNew(InsightType.STICKINESS), + LIFECYCLE: urls.insightNew(InsightType.LIFECYCLE), + FUNNELS: urls.insightNew(InsightType.FUNNELS), + RETENTION: urls.insightNew(InsightType.RETENTION), + PATHS: urls.insightNew(InsightType.PATHS), + JSON: urls.insightNew(undefined, undefined, examples.EventsTableFull), + HOG: urls.insightNew(undefined, undefined, examples.Hoggonacci), + SQL: urls.insightNew(undefined, undefined, examples.DataVisualization), } /** Combines a list of words, separating with the correct punctuation. For example: [a, b, c, d] -> "a, b, c, and d" */ @@ -379,46 +388,48 @@ export function concatWithPunctuation(phrases: string[]): string { } export function insightUrlForEvent(event: Pick): string | undefined { - let insightParams: Partial | undefined + let query: InsightVizNode | undefined if (event.event === '$pageview') { - insightParams = { - insight: InsightType.TRENDS, - interval: 'day', - display: ChartDisplayType.ActionsLineGraph, - actions: [], - events: [ - { - id: '$pageview', - name: '$pageview', - type: 'events', - order: 0, - properties: [ - { - key: '$current_url', - value: event.properties.$current_url, - type: 'event', - }, - ], - }, - ], + query = { + kind: NodeKind.InsightVizNode, + source: { + kind: NodeKind.TrendsQuery, + interval: 'day', + series: [ + { + event: '$pageview', + name: '$pageview', + kind: NodeKind.EventsNode, + properties: [ + { + key: '$current_url', + value: event.properties.$current_url, + type: PropertyFilterType.Event, + operator: PropertyOperator.Exact, + }, + ], + }, + ], + trendsFilter: { display: ChartDisplayType.ActionsLineGraph }, + }, } } else if (event.event !== '$autocapture') { - insightParams = { - insight: InsightType.TRENDS, - interval: 'day', - display: ChartDisplayType.ActionsLineGraph, - actions: [], - events: [ - { - id: event.event, - name: event.event, - type: 'events', - order: 0, - properties: [], - }, - ], + query = { + kind: NodeKind.InsightVizNode, + source: { + kind: NodeKind.TrendsQuery, + interval: 'day', + series: [ + { + event: event.event, + name: event.event, + kind: NodeKind.EventsNode, + }, + ], + trendsFilter: { display: ChartDisplayType.ActionsLineGraph }, + }, } } - return insightParams ? urls.insightNew(insightParams) : undefined + return query ? urls.insightNew(undefined, undefined, query) : undefined } diff --git a/frontend/src/scenes/insights/utils/compareFilters.ts b/frontend/src/scenes/insights/utils/compareFilters.ts index 93afde2296b6b..79d6562eb8a60 100644 --- a/frontend/src/scenes/insights/utils/compareFilters.ts +++ b/frontend/src/scenes/insights/utils/compareFilters.ts @@ -46,7 +46,7 @@ export const cleanFilter = ( export function compareFilters( a: Partial, b: Partial, - test_account_filters_default_checked: boolean | undefined + test_account_filters_default_checked?: boolean | undefined ): boolean { // this is not optimized for speed and does not work for many cases yet // e.g. falsy values are not treated the same as undefined values, unset filters are not handled, ordering of series isn't checked diff --git a/frontend/src/scenes/paths/pathsDataLogic.ts b/frontend/src/scenes/paths/pathsDataLogic.ts index 2c2f08223bf88..454ed411158ee 100644 --- a/frontend/src/scenes/paths/pathsDataLogic.ts +++ b/frontend/src/scenes/paths/pathsDataLogic.ts @@ -2,15 +2,17 @@ import { actions, connect, kea, key, listeners, path, props, selectors } from 'k import { router } from 'kea-router' import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' import { featureFlagLogic } from 'lib/logic/featureFlagLogic' +import { MathAvailability } from 'scenes/insights/filters/ActionFilter/ActionFilterRow/ActionFilterRow' import { insightVizDataLogic } from 'scenes/insights/insightVizDataLogic' import { keyForInsightLogicProps } from 'scenes/insights/sharedUtils' import { pathsTitle } from 'scenes/trends/persons-modal/persons-modal-utils' import { openPersonsModal, OpenPersonsModalProps } from 'scenes/trends/persons-modal/PersonsModal' import { urls } from 'scenes/urls' -import { InsightActorsQuery, NodeKind, PathsQuery } from '~/queries/schema' +import { actionsAndEventsToSeries } from '~/queries/nodes/InsightQuery/utils/filtersToQueryNode' +import { InsightActorsQuery, InsightVizNode, NodeKind, PathsQuery } from '~/queries/schema' import { isPathsQuery } from '~/queries/utils' -import { ActionFilter, InsightLogicProps, InsightType, PathType, PropertyFilterType, PropertyOperator } from '~/types' +import { ActionFilter, InsightLogicProps, PathType, PropertyFilterType, PropertyOperator } from '~/types' import type { pathsDataLogicType } from './pathsDataLogicType' import { PathNodeData } from './pathUtils' @@ -157,14 +159,19 @@ export const pathsDataLogic = kea([ } events.sort((a, b) => (a.order ?? 0) - (b.order ?? 0)) - if (events.length > 0) { - router.actions.push( - urls.insightNew({ - insight: InsightType.FUNNELS, - events: events.reverse(), + const query: InsightVizNode = { + kind: NodeKind.InsightVizNode, + source: { + kind: NodeKind.FunnelsQuery, + series: actionsAndEventsToSeries({ events: events.reverse() }, true, MathAvailability.None), + dateRange: { date_from: values.dateRange?.date_from, - }) - ) + }, + }, + } + + if (events.length > 0) { + router.actions.push(urls.insightNew(undefined, undefined, query)) } }, })), diff --git a/frontend/src/scenes/retention/retentionModalLogic.ts b/frontend/src/scenes/retention/retentionModalLogic.ts index e8e583298b178..7fbb821c7cef0 100644 --- a/frontend/src/scenes/retention/retentionModalLogic.ts +++ b/frontend/src/scenes/retention/retentionModalLogic.ts @@ -81,7 +81,7 @@ export const retentionModalLogic = kea([ ) { query.showPropertyFilter = false } - return urls.insightNew(undefined, undefined, JSON.stringify(query)) + return urls.insightNew(undefined, undefined, query) }, ], }), diff --git a/frontend/src/scenes/saved-insights/savedInsightsLogic.test.ts b/frontend/src/scenes/saved-insights/savedInsightsLogic.test.ts index 4cc1d84fb8cd8..6a61b7c05d675 100644 --- a/frontend/src/scenes/saved-insights/savedInsightsLogic.test.ts +++ b/frontend/src/scenes/saved-insights/savedInsightsLogic.test.ts @@ -1,10 +1,9 @@ -import { combineUrl, router } from 'kea-router' +import { router } from 'kea-router' import { expectLogic, partial } from 'kea-test-utils' import api from 'lib/api' import { MOCK_TEAM_ID } from 'lib/api.mock' import { DeleteDashboardForm, deleteDashboardLogic } from 'scenes/dashboard/deleteDashboardLogic' import { DuplicateDashboardForm, duplicateDashboardLogic } from 'scenes/dashboard/duplicateDashboardLogic' -import { cleanFilters } from 'scenes/insights/utils/cleanFilters' import { sceneLogic } from 'scenes/sceneLogic' import { Scene } from 'scenes/sceneTypes' import { urls } from 'scenes/urls' @@ -12,7 +11,7 @@ import { urls } from 'scenes/urls' import { useMocks } from '~/mocks/jest' import { dashboardsModel } from '~/models/dashboardsModel' import { initKeaTests } from '~/test/init' -import { InsightModel, InsightType, QueryBasedInsightModel } from '~/types' +import { InsightModel, QueryBasedInsightModel } from '~/types' import { INSIGHTS_PER_PAGE, InsightsResult, savedInsightsLogic } from './savedInsightsLogic' @@ -187,16 +186,6 @@ describe('savedInsightsLogic', () => { }) }) - describe('redirects old /insights urls to the real URL', () => { - it('new mode with ?insight= and no hash params', async () => { - router.actions.push(combineUrl('/insights', cleanFilters({ insight: InsightType.FUNNELS })).url) - await expectLogic(router).toMatchValues({ - location: partial({ pathname: urls.project(MOCK_TEAM_ID, urls.insightNew()) }), - hashParams: { filters: partial({ insight: InsightType.FUNNELS }) }, - }) - }) - }) - it('can duplicate and does not use derived name for name', async () => { const sourceInsight = createInsight(123, 'hello') as QueryBasedInsightModel sourceInsight.name = '' diff --git a/frontend/src/scenes/saved-insights/savedInsightsLogic.ts b/frontend/src/scenes/saved-insights/savedInsightsLogic.ts index a9b86424902fe..a2cdb3a1ef993 100644 --- a/frontend/src/scenes/saved-insights/savedInsightsLogic.ts +++ b/frontend/src/scenes/saved-insights/savedInsightsLogic.ts @@ -358,10 +358,6 @@ export const savedInsightsLogic = kea([ router.actions.push(urls.savedInsights()) } return - } else if (searchParams.insight) { - // old URL with `?insight=TRENDS` in query - router.actions.replace(urls.insightNew(searchParams)) - return } const currentFilters = cleanFilters(values.filters) diff --git a/frontend/src/scenes/settings/project/PathCleaningFiltersConfig.tsx b/frontend/src/scenes/settings/project/PathCleaningFiltersConfig.tsx index f1803e750b7ac..9ef7c81c18fca 100644 --- a/frontend/src/scenes/settings/project/PathCleaningFiltersConfig.tsx +++ b/frontend/src/scenes/settings/project/PathCleaningFiltersConfig.tsx @@ -24,8 +24,8 @@ export function PathCleaningFiltersConfig(): JSX.Element | null { return ( <>

- Make your Paths clearer by aliasing - one or multiple URLs.{' '} + Make your Paths clearer by aliasing one or multiple + URLs.{' '} Example: http://tenant-one.mydomain.com/accounts and{' '} http://tenant-two.mydomain.com/accounts can become a single /accounts{' '} diff --git a/frontend/src/scenes/surveys/SurveyView.tsx b/frontend/src/scenes/surveys/SurveyView.tsx index 8672ea7070b68..bb5f4e79280c1 100644 --- a/frontend/src/scenes/surveys/SurveyView.tsx +++ b/frontend/src/scenes/surveys/SurveyView.tsx @@ -13,19 +13,10 @@ import { LemonSkeleton } from 'lib/lemon-ui/LemonSkeleton' import { LemonTabs } from 'lib/lemon-ui/LemonTabs' import { capitalizeFirstLetter, pluralize } from 'lib/utils' import { useEffect, useState } from 'react' -import { urls } from 'scenes/urls' import { Query } from '~/queries/Query/Query' import { NodeKind } from '~/queries/schema' -import { - ActivityScope, - InsightType, - PropertyFilterType, - PropertyOperator, - Survey, - SurveyQuestionType, - SurveyType, -} from '~/types' +import { ActivityScope, PropertyFilterType, PropertyOperator, Survey, SurveyQuestionType, SurveyType } from '~/types' import { SURVEY_EVENT_NAME, SurveyQuestionLabel } from './constants' import { SurveyDisplaySummary } from './Survey' @@ -432,6 +423,7 @@ export function SurveyResult({ disableEventsTable }: { disableEventsTable?: bool surveyOpenTextResults, surveyOpenTextResultsReady, surveyNPSScore, + surveyAsInsightURL, } = useValues(surveyLogic) return ( @@ -510,22 +502,7 @@ export function SurveyResult({ disableEventsTable }: { disableEventsTable?: bool type="primary" data-attr="survey-results-explore" icon={} - to={urls.insightNew({ - insight: InsightType.TRENDS, - events: [ - { id: 'survey sent', name: 'survey sent', type: 'events' }, - { id: 'survey shown', name: 'survey shown', type: 'events' }, - { id: 'survey dismissed', name: 'survey dismissed', type: 'events' }, - ], - properties: [ - { - key: '$survey_id', - value: survey.id, - operator: PropertyOperator.Exact, - type: PropertyFilterType.Event, - }, - ], - })} + to={surveyAsInsightURL} > Explore results diff --git a/frontend/src/scenes/surveys/surveyLogic.tsx b/frontend/src/scenes/surveys/surveyLogic.tsx index 9659305b0580c..90df9b1bcf8bc 100644 --- a/frontend/src/scenes/surveys/surveyLogic.tsx +++ b/frontend/src/scenes/surveys/surveyLogic.tsx @@ -11,9 +11,10 @@ import { eventUsageLogic } from 'lib/utils/eventUsageLogic' import { Scene } from 'scenes/sceneTypes' import { urls } from 'scenes/urls' -import { DataTableNode, HogQLQuery, NodeKind } from '~/queries/schema' +import { DataTableNode, HogQLQuery, InsightVizNode, NodeKind } from '~/queries/schema' import { hogql } from '~/queries/utils' import { + BaseMathType, Breadcrumb, FeatureFlagFilters, MultipleSurveyQuestion, @@ -1115,6 +1116,47 @@ export const surveyLogic = kea([ (survey) => survey.questions.some((question) => question.branching && Object.keys(question.branching).length > 0), ], + surveyAsInsightURL: [ + (s) => [s.survey], + (survey) => { + const query: InsightVizNode = { + kind: NodeKind.InsightVizNode, + source: { + kind: NodeKind.TrendsQuery, + properties: [ + { + key: '$survey_id', + value: survey.id, + operator: PropertyOperator.Exact, + type: PropertyFilterType.Event, + }, + ], + series: [ + { + kind: NodeKind.EventsNode, + event: 'survey sent', + name: 'survey sent', + math: BaseMathType.TotalCount, + }, + { + kind: NodeKind.EventsNode, + event: 'survey shown', + name: 'survey shown', + math: BaseMathType.TotalCount, + }, + { + kind: NodeKind.EventsNode, + event: 'survey dismissed', + name: 'survey dismissed', + math: BaseMathType.TotalCount, + }, + ], + }, + } + + return urls.insightNew(undefined, undefined, query) + }, + ], }), forms(({ actions, props, values }) => ({ survey: { diff --git a/frontend/src/scenes/trends/persons-modal/personsModalLogic.ts b/frontend/src/scenes/trends/persons-modal/personsModalLogic.ts index 224464a866831..55514365774c9 100644 --- a/frontend/src/scenes/trends/persons-modal/personsModalLogic.ts +++ b/frontend/src/scenes/trends/persons-modal/personsModalLogic.ts @@ -361,7 +361,7 @@ export const personsModalLogic = kea([ source, full: true, } - return urls.insightNew(undefined, undefined, JSON.stringify(query)) + return urls.insightNew(undefined, undefined, query) }, ], }), diff --git a/frontend/src/scenes/urls.ts b/frontend/src/scenes/urls.ts index 42659a914800c..2bdaea9722e90 100644 --- a/frontend/src/scenes/urls.ts +++ b/frontend/src/scenes/urls.ts @@ -2,14 +2,14 @@ import { combineUrl } from 'kea-router' import { getCurrentTeamId } from 'lib/utils/getAppContext' import { ExportOptions } from '~/exporter/types' -import { HogQLFilters } from '~/queries/schema' +import { HogQLFilters, Node } from '~/queries/schema' import { ActionType, ActivityTab, AnnotationType, - AnyPartialFilterType, DashboardType, InsightShortId, + InsightType, PipelineNodeTab, PipelineStage, PipelineTab, @@ -71,13 +71,9 @@ export const urls = { `/events/${encodeURIComponent(id)}/${encodeURIComponent(timestamp)}`, ingestionWarnings: (): string => '/data-management/ingestion-warnings', insights: (): string => '/insights', - insightNew: ( - filters?: AnyPartialFilterType, - dashboardId?: DashboardType['id'] | null, - query?: string | Record - ): string => + insightNew: (type?: InsightType, dashboardId?: DashboardType['id'] | null, query?: Node): string => combineUrl('/insights/new', dashboardId ? { dashboard: dashboardId } : {}, { - ...(filters ? { filters } : {}), + ...(type ? { insight: type } : {}), ...(query ? { q: typeof query === 'string' ? query : JSON.stringify(query) } : {}), }).url, insightNewHogQL: (query: string, filters?: HogQLFilters): string => diff --git a/frontend/src/scenes/web-analytics/WebAnalyticsModal.tsx b/frontend/src/scenes/web-analytics/WebAnalyticsModal.tsx index 63bbf2d441f4f..19aa3535d690b 100644 --- a/frontend/src/scenes/web-analytics/WebAnalyticsModal.tsx +++ b/frontend/src/scenes/web-analytics/WebAnalyticsModal.tsx @@ -49,11 +49,7 @@ export const WebAnalyticsModal = (): JSX.Element | null => {

{modal.canOpenInsight ? ( } size="small" type="secondary" diff --git a/frontend/src/scenes/web-analytics/webAnalyticsLogic.tsx b/frontend/src/scenes/web-analytics/webAnalyticsLogic.tsx index 8b7f2309c3648..7349db8cb1051 100644 --- a/frontend/src/scenes/web-analytics/webAnalyticsLogic.tsx +++ b/frontend/src/scenes/web-analytics/webAnalyticsLogic.tsx @@ -533,7 +533,7 @@ export const webAnalyticsLogic = kea([ event: '$pageview', kind: NodeKind.EventsNode, math: BaseMathType.UniqueUsers, - name: '$pageview', + name: 'Pageview', custom_name: 'Unique visitors', }, ], @@ -927,6 +927,7 @@ export const webAnalyticsLogic = kea([ series: [ { event: '$pageview', + name: 'Pageview', kind: NodeKind.EventsNode, math: BaseMathType.UniqueUsers, }, @@ -1020,6 +1021,7 @@ export const webAnalyticsLogic = kea([ series: [ { event: '$pageview', + name: 'Pageview', kind: NodeKind.EventsNode, math: BaseMathType.UniqueUsers, }, @@ -1282,8 +1284,8 @@ export const webAnalyticsLogic = kea([ }, ], getNewInsightUrl: [ - (s) => [s.webAnalyticsFilters, s.dateFilter, s.tiles], - (webAnalyticsFilters: WebAnalyticsPropertyFilters, { dateTo, dateFrom }, tiles) => { + (s) => [s.tiles], + (tiles) => { return function getNewInsightUrl(tileId: TileId, tabId?: string): string | undefined { const formatQueryForNewInsight = (query: QuerySchema): QuerySchema => { if (query.kind === NodeKind.InsightVizNode) { @@ -1305,17 +1307,9 @@ export const webAnalyticsLogic = kea([ if (!tab) { throw new Error('Developer Error, tab not found') } - return urls.insightNew( - { properties: webAnalyticsFilters, date_from: dateFrom, date_to: dateTo }, - null, - formatQueryForNewInsight(tab.query) - ) + return urls.insightNew(undefined, undefined, formatQueryForNewInsight(tab.query)) } else if (tile.kind === 'query') { - return urls.insightNew( - { properties: webAnalyticsFilters, date_from: dateFrom, date_to: dateTo }, - null, - formatQueryForNewInsight(tile.query) - ) + return urls.insightNew(undefined, undefined, formatQueryForNewInsight(tile.query)) } else if (tile.kind === 'replay') { return urls.replay() }