diff --git a/frontend/src/lib/components/IframedToolbarBrowser/IframedToolbarBrowser.tsx b/frontend/src/lib/components/IframedToolbarBrowser/IframedToolbarBrowser.tsx
index 3e34575b31c96..aa32b83e36ee7 100644
--- a/frontend/src/lib/components/IframedToolbarBrowser/IframedToolbarBrowser.tsx
+++ b/frontend/src/lib/components/IframedToolbarBrowser/IframedToolbarBrowser.tsx
@@ -35,10 +35,10 @@ export function IframedToolbarBrowser({
iframeRef,
userIntent,
}: {
- iframeRef?: React.MutableRefObject
+ iframeRef: React.MutableRefObject
userIntent: ToolbarUserIntent
}): JSX.Element | null {
- const logic = iframedToolbarBrowserLogic()
+ const logic = iframedToolbarBrowserLogic({ iframeRef, userIntent: userIntent })
const { browserUrl } = useValues(logic)
const { onIframeLoad, setIframeWidth } = useActions(logic)
diff --git a/frontend/src/lib/components/IframedToolbarBrowser/iframedToolbarBrowserLogic.ts b/frontend/src/lib/components/IframedToolbarBrowser/iframedToolbarBrowserLogic.ts
index 71a59fdde32a1..a2d74018d6aa8 100644
--- a/frontend/src/lib/components/IframedToolbarBrowser/iframedToolbarBrowserLogic.ts
+++ b/frontend/src/lib/components/IframedToolbarBrowser/iframedToolbarBrowserLogic.ts
@@ -10,11 +10,14 @@ import { LemonBannerProps } from 'lib/lemon-ui/LemonBanner'
import posthog from 'posthog-js'
import { RefObject } from 'react'
+import { ToolbarUserIntent } from '~/types'
+
import type { iframedToolbarBrowserLogicType } from './iframedToolbarBrowserLogicType'
export type IframedToolbarBrowserLogicProps = {
iframeRef: RefObject
clearBrowserUrlOnUnmount?: boolean
+ userIntent?: ToolbarUserIntent
}
export interface IFrameBanner {
@@ -50,9 +53,13 @@ export const iframedToolbarBrowserLogic = kea([
setIframeBanner: (banner: IFrameBanner | null) => ({ banner }),
startTrackingLoading: true,
stopTrackingLoading: true,
+ enableElementSelector: true,
+ disableElementSelector: true,
+ setNewActionName: (name: string | null) => ({ name }),
+ toolbarMessageReceived: (type: PostHogAppToolbarEvent, payload: Record) => ({ type, payload }),
}),
- reducers({
+ reducers(({ props }) => ({
// they're called common filters in the toolbar because they're shared between heatmaps and clickmaps
// the name is continued here since they're passed down into the embedded iframe
commonFilters: [
@@ -87,7 +94,7 @@ export const iframedToolbarBrowserLogic = kea([
],
browserUrl: [
null as string | null,
- { persist: true },
+ { persist: props.userIntent == 'heatmaps' },
{
setBrowserUrl: (_, { url }) => url,
},
@@ -107,7 +114,7 @@ export const iframedToolbarBrowserLogic = kea([
setIframeBanner: (_, { banner }) => banner,
},
],
- }),
+ })),
selectors({
isBrowserUrlAuthorized: [
@@ -138,7 +145,7 @@ export const iframedToolbarBrowserLogic = kea([
'*'
)
},
-
+ // heatmaps
patchHeatmapFilters: ({ filters }) => {
actions.sendToolbarMessage(PostHogAppToolbarEvent.PH_PATCH_HEATMAP_FILTERS, { filters })
},
@@ -159,6 +166,17 @@ export const iframedToolbarBrowserLogic = kea([
actions.sendToolbarMessage(PostHogAppToolbarEvent.PH_HEATMAPS_COMMON_FILTERS, { commonFilters: filters })
},
+ // actions
+ enableElementSelector: () => {
+ actions.sendToolbarMessage(PostHogAppToolbarEvent.PH_ELEMENT_SELECTOR, { enabled: true })
+ },
+ disableElementSelector: () => {
+ actions.sendToolbarMessage(PostHogAppToolbarEvent.PH_ELEMENT_SELECTOR, { enabled: false })
+ },
+ setNewActionName: ({ name }) => {
+ actions.sendToolbarMessage(PostHogAppToolbarEvent.PH_NEW_ACTION_NAME, { name })
+ },
+
onIframeLoad: () => {
// we get this callback whether the iframe loaded successfully or not
// and don't get a signal if the load was successful, so we have to check
@@ -171,13 +189,20 @@ export const iframedToolbarBrowserLogic = kea([
fixedPositionMode: values.heatmapFixedPositionMode,
commonFilters: values.commonFilters,
})
- actions.sendToolbarMessage(PostHogAppToolbarEvent.PH_HEATMAPS_CONFIG, {
- enabled: true,
- })
+ switch (props.userIntent) {
+ case 'heatmaps':
+ actions.sendToolbarMessage(PostHogAppToolbarEvent.PH_HEATMAPS_CONFIG, {
+ enabled: true,
+ })
+ break
+ }
}
const onIframeMessage = (e: MessageEvent): void => {
const type: PostHogAppToolbarEvent = e?.data?.type
+ const payload = e?.data?.payload
+
+ actions.toolbarMessageReceived(type, payload)
if (!type || !type.startsWith('ph-')) {
return
@@ -195,14 +220,17 @@ export const iframedToolbarBrowserLogic = kea([
case PostHogAppToolbarEvent.PH_TOOLBAR_INIT:
return init()
case PostHogAppToolbarEvent.PH_TOOLBAR_READY:
- posthog.capture('in-app heatmap frame loaded', {
- inapp_heatmap_page_url_visited: values.browserUrl,
- inapp_heatmap_filters: values.heatmapFilters,
- inapp_heatmap_color_palette: values.heatmapColorPalette,
- inapp_heatmap_fixed_position_mode: values.heatmapFixedPositionMode,
- })
- // reset loading tracking - if we're e.g. slow this will avoid a flash of warning message
- return actions.startTrackingLoading()
+ if (props.userIntent === 'heatmaps') {
+ posthog.capture('in-app heatmap frame loaded', {
+ inapp_heatmap_page_url_visited: values.browserUrl,
+ inapp_heatmap_filters: values.heatmapFilters,
+ inapp_heatmap_color_palette: values.heatmapColorPalette,
+ inapp_heatmap_fixed_position_mode: values.heatmapFixedPositionMode,
+ })
+ // reset loading tracking - if we're e.g. slow this will avoid a flash of warning message
+ return actions.startTrackingLoading()
+ }
+ return
case PostHogAppToolbarEvent.PH_TOOLBAR_HEATMAP_LOADING:
return actions.startTrackingLoading()
case PostHogAppToolbarEvent.PH_TOOLBAR_HEATMAP_LOADED:
@@ -223,6 +251,10 @@ export const iframedToolbarBrowserLogic = kea([
actions.stopTrackingLoading()
actions.setIframeBanner({ level: 'error', message: 'The heatmap failed to load.' })
return
+ case PostHogAppToolbarEvent.PH_NEW_ACTION_CREATED:
+ actions.setNewActionName(null)
+ actions.disableElementSelector()
+ return
default:
console.warn(`[PostHog Heatmaps] Received unknown child window message: ${type}`)
}
diff --git a/frontend/src/lib/components/IframedToolbarBrowser/utils.ts b/frontend/src/lib/components/IframedToolbarBrowser/utils.ts
index dbb47dd057d3c..87d9cae13cf9f 100644
--- a/frontend/src/lib/components/IframedToolbarBrowser/utils.ts
+++ b/frontend/src/lib/components/IframedToolbarBrowser/utils.ts
@@ -14,6 +14,9 @@ export enum PostHogAppToolbarEvent {
PH_TOOLBAR_HEATMAP_LOADING = 'ph-toolbar-heatmap-loading',
PH_TOOLBAR_HEATMAP_LOADED = 'ph-toolbar-heatmap-loaded',
PH_TOOLBAR_HEATMAP_FAILED = 'ph-toolbar-heatmap-failed',
+ PH_ELEMENT_SELECTOR = 'ph-element-selector',
+ PH_NEW_ACTION_NAME = 'ph-new-action-name',
+ PH_NEW_ACTION_CREATED = 'ph-new-action-created',
}
export const DEFAULT_HEATMAP_FILTERS: HeatmapFilters = {
diff --git a/frontend/src/scenes/dashboard/dashboardTemplateVariablesLogic.ts b/frontend/src/scenes/dashboard/dashboardTemplateVariablesLogic.ts
index da23d22466d25..e12513e025d38 100644
--- a/frontend/src/scenes/dashboard/dashboardTemplateVariablesLogic.ts
+++ b/frontend/src/scenes/dashboard/dashboardTemplateVariablesLogic.ts
@@ -1,7 +1,18 @@
-import { actions, kea, listeners, path, props, propsChanged, reducers, selectors } from 'kea'
+import { actions, connect, kea, listeners, path, props, propsChanged, reducers, selectors } from 'kea'
+import { iframedToolbarBrowserLogic } from 'lib/components/IframedToolbarBrowser/iframedToolbarBrowserLogic'
+import { PostHogAppToolbarEvent } from 'lib/components/IframedToolbarBrowser/utils'
import { isEmptyObject } from 'lib/utils'
-import { DashboardTemplateVariableType, FilterType, Optional } from '~/types'
+import {
+ ActionType,
+ BaseMathType,
+ DashboardTemplateVariableType,
+ EntityType,
+ EntityTypes,
+ FilterType,
+ Optional,
+ TemplateVariableStep,
+} from '~/types'
import type { dashboardTemplateVariablesLogicType } from './dashboardTemplateVariablesLogicType'
@@ -18,17 +29,22 @@ const FALLBACK_EVENT = {
export const dashboardTemplateVariablesLogic = kea([
path(['scenes', 'dashboard', 'DashboardTemplateVariablesLogic']),
props({ variables: [] } as DashboardTemplateVariablesLogicProps),
+ connect({
+ actions: [iframedToolbarBrowserLogic, ['toolbarMessageReceived', 'disableElementSelector']],
+ }),
actions({
setVariables: (variables: DashboardTemplateVariableType[]) => ({ variables }),
setVariable: (variableName: string, filterGroup: Optional) => ({
variable_name: variableName,
filterGroup,
}),
+ setVariableFromAction: (variableName: string, action: ActionType) => ({ variableName, action }),
setActiveVariableIndex: (index: number) => ({ index }),
incrementActiveVariableIndex: true,
possiblyIncrementActiveVariableIndex: true,
resetVariable: (variableId: string) => ({ variableId }),
goToNextUntouchedActiveVariableIndex: true,
+ setIsCurrentlySelectingElement: (isSelecting: boolean) => ({ isSelecting }),
}),
reducers({
variables: [
@@ -43,10 +59,23 @@ export const dashboardTemplateVariablesLogic = kea {
- // TODO: handle actions as well as events
+ // There is only one type with contents at a time
+ // So iterate through the types to find the first one with contents
+ const typeWithContents: EntityType = Object.keys(filterGroup).filter(
+ (group) => (filterGroup[group as EntityType] || [])?.length > 0
+ )?.[0] as EntityType
+
+ if (!typeWithContents) {
+ return state
+ }
+
return state.map((v: DashboardTemplateVariableType) => {
- if (v.name === variableName && filterGroup?.events?.length && filterGroup.events[0]) {
- return { ...v, default: filterGroup.events[0], touched: true }
+ if (
+ v.name === variableName &&
+ filterGroup?.[typeWithContents]?.length &&
+ filterGroup?.[typeWithContents]?.[0]
+ ) {
+ return { ...v, default: filterGroup[typeWithContents]?.[0] || {}, touched: true }
}
return { ...v }
})
@@ -68,6 +97,12 @@ export const dashboardTemplateVariablesLogic = kea state + 1,
},
],
+ isCurrentlySelectingElement: [
+ false as boolean,
+ {
+ setIsCurrentlySelectingElement: (_, { isSelecting }) => isSelecting,
+ },
+ ],
}),
selectors(() => ({
activeVariable: [
@@ -82,6 +117,12 @@ export const dashboardTemplateVariablesLogic = kea v.touched)
},
],
+ hasTouchedAnyVariable: [
+ (s) => [s.variables],
+ (variables: DashboardTemplateVariableType[]) => {
+ return variables.some((v) => v.touched)
+ },
+ ],
})),
listeners(({ actions, props, values }) => ({
possiblyIncrementActiveVariableIndex: () => {
@@ -103,6 +144,30 @@ export const dashboardTemplateVariablesLogic = kea {
+ const originalVariableName = variableName.replace(/\s-\s\d+/g, '')
+ const step: TemplateVariableStep = {
+ id: action.id.toString(),
+ math: BaseMathType.UniqueUsers,
+ name: action.name,
+ order: 0,
+ type: EntityTypes.ACTIONS,
+ selector: action.steps?.[0]?.selector,
+ href: action.steps?.[0]?.href,
+ url: action.steps?.[0]?.url,
+ }
+ const filterGroup: FilterType = {
+ actions: [step],
+ }
+ actions.setVariable(originalVariableName, filterGroup)
+ actions.setIsCurrentlySelectingElement(false)
+ },
+ toolbarMessageReceived: ({ type, payload }) => {
+ if (type === PostHogAppToolbarEvent.PH_NEW_ACTION_CREATED) {
+ actions.setVariableFromAction(payload.action.name, payload.action as ActionType)
+ actions.disableElementSelector()
+ }
+ },
})),
propsChanged(({ actions, props }, oldProps) => {
if (props.variables !== oldProps.variables) {
diff --git a/frontend/src/scenes/onboarding/productAnalyticsSteps/DashboardTemplateConfigureStep.tsx b/frontend/src/scenes/onboarding/productAnalyticsSteps/DashboardTemplateConfigureStep.tsx
index 2bed8e8a07d2f..476cef6de6275 100644
--- a/frontend/src/scenes/onboarding/productAnalyticsSteps/DashboardTemplateConfigureStep.tsx
+++ b/frontend/src/scenes/onboarding/productAnalyticsSteps/DashboardTemplateConfigureStep.tsx
@@ -8,7 +8,7 @@ import { useRef, useState } from 'react'
import { dashboardTemplateVariablesLogic } from 'scenes/dashboard/dashboardTemplateVariablesLogic'
import { newDashboardLogic } from 'scenes/dashboard/newDashboardLogic'
-import { OnboardingStepKey } from '../onboardingLogic'
+import { onboardingLogic, OnboardingStepKey } from '../onboardingLogic'
import { OnboardingStep } from '../OnboardingStep'
import { sdksLogic } from '../sdks/sdksLogic'
import { DashboardTemplateVariables } from './DashboardTemplateVariables'
@@ -30,7 +30,8 @@ export const OnboardingDashboardTemplateConfigureStep = ({
const theDashboardTemplateVariablesLogic = dashboardTemplateVariablesLogic({
variables: activeDashboardTemplate?.variables || [],
})
- const { variables, allVariablesAreTouched } = useValues(theDashboardTemplateVariablesLogic)
+ const { variables, allVariablesAreTouched, hasTouchedAnyVariable } = useValues(theDashboardTemplateVariablesLogic)
+ const { goToNextStep, setStepKey } = useActions(onboardingLogic)
const [isSubmitting, setIsSubmitting] = useState(false)
@@ -58,32 +59,50 @@ export const OnboardingDashboardTemplateConfigureStep = ({
Select where you want to track events from.
-
- Not seeing the site you want? Install posthog-js or the HTML snippet
- wherever you want to track events, then come back here.
-
{snippetHosts.length > 0 ? (
-
- {snippetHosts.map((host) => (
- {
- addUrl(host)
- setBrowserUrl(host)
- }}
- sideIcon={ }
- >
- {host}
-
- ))}
-
+ <>
+
+ Not seeing the site you want? Install posthog-js or the HTML
+ snippet wherever you want to track events, then come back here.
+
+
+ {snippetHosts.map((host) => (
+ {
+ addUrl(host)
+ setBrowserUrl(host)
+ }}
+ sideIcon={ }
+ >
+ {host}
+
+ ))}
+
+ >
) : (
-
- Hm, we're not finding any available hosts. Head back to the install
- step to install posthog-js in your frontend.
-
+ <>
+
+ Hm, it looks like you haven't ingested any events from a website
+ yet. To select actions from your site, head back to the{' '}
+ setStepKey(OnboardingStepKey.INSTALL)}>
+ install step
+ {' '}
+ to install posthog-js in your frontend.
+
+
+ You can still create a dashboard using custom event names,
+ though it's not quite as fun.
+
+ setStepKey(OnboardingStepKey.INSTALL)}
+ type="primary"
+ >
+ Install posthog-js
+
+ >
)}
@@ -116,24 +135,35 @@ export const OnboardingDashboardTemplateConfigureStep = ({
{' '}
(no need to send it now) .
-
- {
- if (activeDashboardTemplate) {
- setIsSubmitting(true)
- createDashboardFromTemplate(activeDashboardTemplate, variables, false)
- }
- }}
- loading={isLoading}
- fullWidth
- center
- className="mt-6"
- disabledReason={!allVariablesAreTouched && 'Please select an event for each variable'}
- >
- Create dashboard
-
+
+
+
+ {
+ if (activeDashboardTemplate) {
+ setIsSubmitting(true)
+ createDashboardFromTemplate(activeDashboardTemplate, variables, false)
+ }
+ }}
+ loading={isLoading}
+ fullWidth
+ center
+ className="grow"
+ disabledReason={
+ !allVariablesAreTouched && 'Please select an event for each variable'
+ }
+ >
+ Create dashboard
+
+
+
+ goToNextStep()} fullWidth center>
+ {hasTouchedAnyVariable ? 'Discard dashboard & skip' : 'Skip for now'}
+
+
+
>
diff --git a/frontend/src/scenes/onboarding/productAnalyticsSteps/DashboardTemplateVariables.tsx b/frontend/src/scenes/onboarding/productAnalyticsSteps/DashboardTemplateVariables.tsx
index e22f822eedcfe..9f87ff0aeb0f4 100644
--- a/frontend/src/scenes/onboarding/productAnalyticsSteps/DashboardTemplateVariables.tsx
+++ b/frontend/src/scenes/onboarding/productAnalyticsSteps/DashboardTemplateVariables.tsx
@@ -1,6 +1,7 @@
-import { IconCheckCircle, IconInfo, IconTrash } from '@posthog/icons'
-import { LemonBanner, LemonButton, LemonCollapse, LemonInput, LemonLabel } from '@posthog/lemon-ui'
+import { IconCheckCircle, IconInfo, IconTarget, IconTrash } from '@posthog/icons'
+import { LemonBanner, LemonButton, LemonCollapse, LemonInput, LemonLabel, Spinner } from '@posthog/lemon-ui'
import { useActions, useValues } from 'kea'
+import { iframedToolbarBrowserLogic } from 'lib/components/IframedToolbarBrowser/iframedToolbarBrowserLogic'
import { useEffect, useState } from 'react'
import { dashboardTemplateVariablesLogic } from 'scenes/dashboard/dashboardTemplateVariablesLogic'
import { newDashboardLogic } from 'scenes/dashboard/newDashboardLogic'
@@ -8,26 +9,37 @@ import { newDashboardLogic } from 'scenes/dashboard/newDashboardLogic'
import { DashboardTemplateVariableType } from '~/types'
function VariableSelector({
- variable,
+ variableName,
hasSelectedSite,
+ iframeRef,
}: {
- variable: DashboardTemplateVariableType
+ variableName: string
hasSelectedSite: boolean
+ iframeRef: React.RefObject
}): JSX.Element {
const { activeDashboardTemplate } = useValues(newDashboardLogic)
const theDashboardTemplateVariablesLogic = dashboardTemplateVariablesLogic({
variables: activeDashboardTemplate?.variables || [],
})
- const { setVariable, resetVariable, goToNextUntouchedActiveVariableIndex, incrementActiveVariableIndex } =
- useActions(theDashboardTemplateVariablesLogic)
- const { allVariablesAreTouched, variables, activeVariableIndex } = useValues(theDashboardTemplateVariablesLogic)
+ const {
+ setVariable,
+ resetVariable,
+ goToNextUntouchedActiveVariableIndex,
+ incrementActiveVariableIndex,
+ setIsCurrentlySelectingElement,
+ } = useActions(theDashboardTemplateVariablesLogic)
+ const { allVariablesAreTouched, variables, activeVariableIndex, isCurrentlySelectingElement } = useValues(
+ theDashboardTemplateVariablesLogic
+ )
const [customEventName, setCustomEventName] = useState(null)
const [showCustomEventField, setShowCustomEventField] = useState(false)
+ const { enableElementSelector, disableElementSelector, setNewActionName } = useActions(
+ iframedToolbarBrowserLogic({ iframeRef, clearBrowserUrlOnUnmount: true })
+ )
- const FALLBACK_EVENT = {
- id: '$other_event',
- math: 'dau',
- type: 'events',
+ const variable: DashboardTemplateVariableType | undefined = variables.find((v) => v.name === variableName)
+ if (!variable) {
+ return <>>
}
return (
@@ -37,12 +49,32 @@ function VariableSelector({
{variable.description}
+ {!showCustomEventField && activeVariableIndex == 0 && hasSelectedSite && !variable.touched && (
+
+
+ Tip: Navigate to the page you want before you start selecting.
+
+
+ )}
{variable.touched && !customEventName && (
-
{' '}
-
Selected
-
.md-invite-button
+
+ {' '}
+ Selected
+
+
+
+ CSS selector: {' '}
+ {variable.default.selector || 'not set'}
+
+
+ Element href: {variable.default.href || 'not set'}
+
+
+ Page URL: {variable.default.url || 'any url'}
+
+
{
+ disableElementSelector()
+ setNewActionName(null)
resetVariable(variable.id)
setCustomEventName(null)
setShowCustomEventField(false)
@@ -101,59 +135,100 @@ function VariableSelector({
)}
- {!hasSelectedSite ? (
- Please select a site to continue.
- ) : (
-
- {variable.touched ? (
- <>
- {!allVariablesAreTouched ||
- (allVariablesAreTouched && variables.length !== activeVariableIndex + 1) ? (
-
- !allVariablesAreTouched
- ? goToNextUntouchedActiveVariableIndex()
- : variables.length !== activeVariableIndex + 1
- ? incrementActiveVariableIndex()
- : null
- }
- >
- Continue
-
- ) : null}
- >
- ) : (
-
+
+
+ {variable.touched ? (
+ <>
+ {!allVariablesAreTouched ||
+ (allVariablesAreTouched && variables.length !== activeVariableIndex + 1) ? (
+
+ !allVariablesAreTouched
+ ? goToNextUntouchedActiveVariableIndex()
+ : variables.length !== activeVariableIndex + 1
+ ? incrementActiveVariableIndex()
+ : null
+ }
+ >
+ Continue
+
+ ) : null}
+ >
+ ) : (
+
+ {isCurrentlySelectingElement ? (
+ {
+ disableElementSelector()
+ setNewActionName(null)
+ setIsCurrentlySelectingElement(false)
+ }}
+ icon={ }
+ center
+ className="min-w-44"
+ >
+ Cancel selection
+
+ ) : (
{
setShowCustomEventField(false)
- setVariable(variable.name, { events: [FALLBACK_EVENT] })
+ enableElementSelector()
+ setNewActionName(variable.name)
+ setIsCurrentlySelectingElement(true)
}}
+ icon={ }
+ center
+ className="min-w-44"
+ disabledReason={!hasSelectedSite && 'Please select a site to continue'}
>
Select from site
- setShowCustomEventField(true)}>
- Use custom event
-
-
- )}
-
- )}
+ )}
+
{
+ disableElementSelector()
+ setNewActionName(null)
+ setShowCustomEventField(true)
+ setIsCurrentlySelectingElement(false)
+ }}
+ fullWidth
+ center
+ className="grow max-w-44"
+ >
+ Use custom event
+
+
+ )}
+
)
}
-export function DashboardTemplateVariables({ hasSelectedSite }: { hasSelectedSite: boolean }): JSX.Element {
+export function DashboardTemplateVariables({
+ hasSelectedSite,
+ iframeRef,
+}: {
+ hasSelectedSite: boolean
+ iframeRef: React.RefObject
+}): JSX.Element {
const { activeDashboardTemplate } = useValues(newDashboardLogic)
const theDashboardTemplateVariablesLogic = dashboardTemplateVariablesLogic({
variables: activeDashboardTemplate?.variables || [],
})
const { variables, activeVariableIndex } = useValues(theDashboardTemplateVariablesLogic)
- const { setVariables, setActiveVariableIndex } = useActions(theDashboardTemplateVariablesLogic)
+ const { setVariables, setActiveVariableIndex, setIsCurrentlySelectingElement } = useActions(
+ theDashboardTemplateVariablesLogic
+ )
+ const { setNewActionName, disableElementSelector } = useActions(
+ iframedToolbarBrowserLogic({ iframeRef, clearBrowserUrlOnUnmount: true })
+ )
// TODO: onboarding-dashboard-templates: this is a hack, I'm not sure why it's not set properly initially.
useEffect(() => {
@@ -172,10 +247,20 @@ export function DashboardTemplateVariables({ hasSelectedSite }: { hasSelectedSit
{v.touched && }
),
- content: ,
+ content: (
+
+ ),
className: 'p-4 bg-white',
onHeaderClick: () => {
setActiveVariableIndex(i)
+ disableElementSelector()
+ setNewActionName(null)
+ setIsCurrentlySelectingElement(false)
},
}))}
embedded
diff --git a/frontend/src/scenes/onboarding/sdks/sdksLogic.tsx b/frontend/src/scenes/onboarding/sdks/sdksLogic.tsx
index 0ab48594d2825..6d4a0d4641eb2 100644
--- a/frontend/src/scenes/onboarding/sdks/sdksLogic.tsx
+++ b/frontend/src/scenes/onboarding/sdks/sdksLogic.tsx
@@ -126,26 +126,25 @@ export const sdksLogic = kea([
loadSnippetEvents: async () => {
const query: HogQLQuery = {
kind: NodeKind.HogQLQuery,
- query: hogql`SELECT properties.$lib_version AS lib_version,
+ query: hogql`SELECT
max(timestamp) AS latest_timestamp,
- count(lib_version) AS count,
concat(
if(startsWith(properties.current_url, 'https://'), 'https://', 'http://'),
properties.$host
) AS full_host
FROM events
- WHERE timestamp >= now() - INTERVAL 3 DAY
+ WHERE timestamp >= now() - INTERVAL 5 DAY
AND timestamp <= now()
AND properties.$lib = 'web'
AND properties.$host is not null
- GROUP BY lib_version, full_host
+ GROUP BY full_host
ORDER BY latest_timestamp DESC
LIMIT 10`,
}
const res = await api.query(query)
const hasEvents = !!(res.results?.length ?? 0 > 0)
- const snippetHosts = res.results?.map((result) => result[3]).filter((val) => !!val) ?? []
+ const snippetHosts = res.results?.map((result) => result[1]).filter((val) => !!val) ?? []
if (hasEvents) {
actions.setSnippetHosts(snippetHosts)
}
diff --git a/frontend/src/toolbar/actions/ActionAttribute.tsx b/frontend/src/toolbar/actions/ActionAttribute.tsx
index c8cd2c68898b0..ca74a79363c07 100644
--- a/frontend/src/toolbar/actions/ActionAttribute.tsx
+++ b/frontend/src/toolbar/actions/ActionAttribute.tsx
@@ -1,6 +1,10 @@
-import { Link } from '@posthog/lemon-ui'
+import { LemonSwitch, Link } from '@posthog/lemon-ui'
+import { useActions, useValues } from 'kea'
import { IconBranch, IconClipboardEdit, IconLink, IconTextSize } from 'lib/lemon-ui/icons'
+import { actionsTabLogic } from './actionsTabLogic'
+import { ActionStepPropertyKey } from './ActionStep'
+
function SelectorString({ value }: { value: string }): JSX.Element {
const [last, ...rest] = value.split(' ').reverse()
return (
@@ -10,7 +14,16 @@ function SelectorString({ value }: { value: string }): JSX.Element {
)
}
-export function ActionAttribute({ attribute, value }: { attribute: string; value?: string }): JSX.Element {
+export function ActionAttribute({
+ attribute,
+ value,
+}: {
+ attribute: ActionStepPropertyKey
+ value?: string
+}): JSX.Element {
+ const { automaticCreationIncludedPropertyKeys, automaticActionCreationEnabled } = useValues(actionsTabLogic)
+ const { removeAutomaticCreationIncludedPropertyKey, addAutomaticCreationIncludedPropertyKey } =
+ useActions(actionsTabLogic)
const icon =
attribute === 'text' ? (
@@ -44,6 +57,17 @@ export function ActionAttribute({ attribute, value }: { attribute: string; value
return (
+ {automaticActionCreationEnabled && (
+
+ checked
+ ? addAutomaticCreationIncludedPropertyKey(attribute)
+ : removeAutomaticCreationIncludedPropertyKey(attribute)
+ }
+ />
+ )}
{icon}
{text}
diff --git a/frontend/src/toolbar/actions/ActionStep.tsx b/frontend/src/toolbar/actions/ActionStep.tsx
index f0d6272d01b8b..a2a34a43412a7 100644
--- a/frontend/src/toolbar/actions/ActionStep.tsx
+++ b/frontend/src/toolbar/actions/ActionStep.tsx
@@ -1,16 +1,26 @@
+import { useValues } from 'kea'
+
import { ActionAttribute } from '~/toolbar/actions/ActionAttribute'
import { ActionStepType } from '~/types'
+import { actionsTabLogic } from './actionsTabLogic'
+
interface ActionStepProps {
actionStep: ActionStepType
}
-type ActionStepPropertyKey = 'text' | 'name' | 'href' | 'selector'
+export type ActionStepPropertyKey = 'text' | 'name' | 'href' | 'selector' | 'url'
export function ActionStep({ actionStep }: ActionStepProps): JSX.Element {
+ const { automaticActionCreationEnabled } = useValues(actionsTabLogic)
+
+ const stepTypes = ['text', 'name', 'href', 'selector', automaticActionCreationEnabled ? 'url' : null].filter(
+ (key) => key
+ ) as ActionStepPropertyKey[]
+
return (
- {(['text', 'name', 'href', 'selector'] as ActionStepPropertyKey[]).map((attr) =>
+ {stepTypes.map((attr) =>
actionStep[attr] || attr === 'selector' ? (
) : null
diff --git a/frontend/src/toolbar/actions/actionsTabLogic.tsx b/frontend/src/toolbar/actions/actionsTabLogic.tsx
index 759d410d00f97..be2b0fa13f160 100644
--- a/frontend/src/toolbar/actions/actionsTabLogic.tsx
+++ b/frontend/src/toolbar/actions/actionsTabLogic.tsx
@@ -14,11 +14,25 @@ import { actionStepToActionStepFormItem, elementToActionStep, stepToDatabaseForm
import { ActionType, ElementType } from '~/types'
import type { actionsTabLogicType } from './actionsTabLogicType'
+import { ActionStepPropertyKey } from './ActionStep'
-function newAction(element: HTMLElement | null, dataAttributes: string[] = []): ActionDraftType {
+function newAction(
+ element: HTMLElement | null,
+ dataAttributes: string[] = [],
+ name: string | null,
+ includedPropertyKeys?: ActionStepPropertyKey[]
+): ActionDraftType {
return {
- name: '',
- steps: [element ? actionStepToActionStepFormItem(elementToActionStep(element, dataAttributes), true) : {}],
+ name: name || '',
+ steps: [
+ element
+ ? actionStepToActionStepFormItem(
+ elementToActionStep(element, dataAttributes),
+ true,
+ includedPropertyKeys
+ )
+ : {},
+ ],
pinned_at: null,
}
}
@@ -67,6 +81,11 @@ export const actionsTabLogic = kea
([
hideButtonActions: true,
setShowActionsTooltip: (showActionsTooltip: boolean) => ({ showActionsTooltip }),
setElementSelector: (selector: string, index: number) => ({ selector, index }),
+ setAutomaticActionCreationEnabled: (enabled: boolean, name?: string) => ({ enabled, name }),
+ actionCreatedSuccess: (action: ActionType) => ({ action }),
+ setautomaticCreationIncludedPropertyKeys: (keys: ActionStepPropertyKey[]) => ({ keys }),
+ removeAutomaticCreationIncludedPropertyKey: (key: ActionStepPropertyKey) => ({ key }),
+ addAutomaticCreationIncludedPropertyKey: (key: ActionStepPropertyKey) => ({ key }),
}),
connect(() => ({
@@ -142,6 +161,30 @@ export const actionsTabLogic = kea([
setShowActionsTooltip: (_, { showActionsTooltip }) => showActionsTooltip,
},
],
+ // we automatically create actions for people from analytics onboarding. This flag controls that experience.
+ automaticActionCreationEnabled: [
+ false as boolean,
+ {
+ setAutomaticActionCreationEnabled: (_, { enabled, name }) => (enabled && !!name) || false,
+ },
+ ],
+ newActionName: [
+ null as string | null,
+ {
+ setAutomaticActionCreationEnabled: (_, { enabled, name }) => (enabled && name ? name : null),
+ },
+ ],
+ automaticCreationIncludedPropertyKeys: [
+ [] as ActionStepPropertyKey[],
+ {
+ setAutomaticActionCreationEnabled: (_, { enabled }) =>
+ enabled ? ['text', 'href', 'name', 'selector', 'url'] : [],
+ setautomaticCreationIncludedPropertyKeys: (_, { keys }) => keys || [],
+ removeAutomaticCreationIncludedPropertyKey: (state, { key }) => state.filter((k) => k !== key),
+ addAutomaticCreationIncludedPropertyKey: (state, { key }) =>
+ !state.includes(key) ? [...state, key] : state,
+ },
+ ],
}),
forms(({ values, actions }) => ({
@@ -158,6 +201,19 @@ export const actionsTabLogic = kea([
const { apiURL, temporaryToken } = values
const { selectedActionId } = values
+ const findUniqueActionName = (baseName: string, index = 0): string => {
+ const proposedName = index === 0 ? baseName : `${baseName} - ${index}`
+ if (!values.allActions.find((action) => action.name === proposedName)) {
+ return proposedName
+ }
+ return findUniqueActionName(baseName, index + 1)
+ }
+
+ if (values.newActionName) {
+ // newActionName is programmatically set, but they may already have an existing action with that name. Append an index.
+ actionToSave.name = findUniqueActionName(values.newActionName)
+ }
+
let response: ActionType
if (selectedActionId && selectedActionId !== 'new') {
response = await api.update(
@@ -172,15 +228,19 @@ export const actionsTabLogic = kea([
}
breakpoint()
- actionsLogic.actions.updateAction({ action: response })
actions.selectAction(null)
+ actionsLogic.actions.updateAction({ action: response })
- lemonToast.success('Action saved', {
- button: {
- label: 'Open in PostHog',
- action: () => window.open(`${apiURL}${urls.action(response.id)}`, '_blank'),
- },
- })
+ if (!values.automaticActionCreationEnabled) {
+ lemonToast.success('Action saved', {
+ button: {
+ label: 'Open in PostHog',
+ action: () => window.open(`${apiURL}${urls.action(response.id)}`, '_blank'),
+ },
+ })
+ }
+
+ actions.actionCreatedSuccess(response)
},
// whether we show errors after touch (true) or submit (false)
@@ -211,22 +271,47 @@ export const actionsTabLogic = kea([
},
],
selectedAction: [
- (s) => [s.selectedActionId, s.newActionForElement, s.allActions, s.dataAttributes],
+ (s) => [
+ s.selectedActionId,
+ s.newActionForElement,
+ s.allActions,
+ s.dataAttributes,
+ s.newActionName,
+ s.automaticCreationIncludedPropertyKeys,
+ ],
(
selectedActionId,
newActionForElement,
allActions,
- dataAttributes
+ dataAttributes,
+ newActionName,
+ automaticCreationIncludedPropertyKeys
): ActionType | ActionDraftType | null => {
if (selectedActionId === 'new') {
- return newAction(newActionForElement, dataAttributes)
+ return newAction(
+ newActionForElement,
+ dataAttributes,
+ newActionName,
+ automaticCreationIncludedPropertyKeys
+ )
}
return allActions.find((a) => a.id === selectedActionId) || null
},
],
+ isReadyForAutomaticSubmit: [
+ (s) => [s.automaticActionCreationEnabled, s.selectedAction, s.actionForm],
+ (automaticActionCreationEnabled, selectedAction, actionForm): boolean => {
+ return (
+ (automaticActionCreationEnabled &&
+ selectedAction?.name &&
+ actionForm.steps?.[0]?.selector_selected) ||
+ false
+ )
+ },
+ ],
}),
- subscriptions(({ actions }) => ({
+ subscriptions(({ actions, values }) => ({
selectedAction: (selectedAction: ActionType | ActionDraftType | null) => {
if (!selectedAction) {
actions.setActionFormValues({ name: null, steps: [{}] })
@@ -237,6 +322,9 @@ export const actionsTabLogic = kea([
? selectedAction.steps.map((step) => actionStepToActionStepFormItem(step, false))
: [{}],
})
+ if (values.isReadyForAutomaticSubmit) {
+ actions.submitActionForm()
+ }
}
},
})),
diff --git a/frontend/src/toolbar/bar/toolbarLogic.ts b/frontend/src/toolbar/bar/toolbarLogic.ts
index 2176e5650cfee..dd5a8dd6a2f2b 100644
--- a/frontend/src/toolbar/bar/toolbarLogic.ts
+++ b/frontend/src/toolbar/bar/toolbarLogic.ts
@@ -31,7 +31,13 @@ export const toolbarLogic = kea([
connect(() => ({
actions: [
actionsTabLogic,
- ['showButtonActions', 'hideButtonActions', 'selectAction'],
+ [
+ 'showButtonActions',
+ 'hideButtonActions',
+ 'selectAction',
+ 'setAutomaticActionCreationEnabled',
+ 'actionCreatedSuccess',
+ ],
elementsLogic,
['enableInspect', 'disableInspect', 'createAction'],
heatmapLogic,
@@ -393,6 +399,10 @@ export const toolbarLogic = kea([
// if embedded we need to signal start and finish of heatmap loading to the parent
window.parent.postMessage({ type: PostHogAppToolbarEvent.PH_TOOLBAR_HEATMAP_FAILED }, '*')
},
+ actionCreatedSuccess: (action) => {
+ // if embedded, we need to tell the parent window that a new action was created
+ window.parent.postMessage({ type: PostHogAppToolbarEvent.PH_NEW_ACTION_CREATED, payload: action }, '*')
+ },
})),
afterMount(({ actions, values, cache }) => {
cache.clickListener = (e: MouseEvent): void => {
@@ -440,6 +450,17 @@ export const toolbarLogic = kea([
case PostHogAppToolbarEvent.PH_HEATMAPS_COMMON_FILTERS:
actions.setCommonFilters(e.data.payload.commonFilters)
return
+ case PostHogAppToolbarEvent.PH_ELEMENT_SELECTOR:
+ if (e.data.payload.enabled) {
+ actions.enableInspect()
+ } else {
+ actions.disableInspect()
+ actions.hideButtonActions()
+ }
+ return
+ case PostHogAppToolbarEvent.PH_NEW_ACTION_NAME:
+ actions.setAutomaticActionCreationEnabled(true, e.data.payload.name)
+ return
default:
console.warn(`[PostHog Toolbar] Received unknown parent window message: ${type}`)
}
diff --git a/frontend/src/toolbar/elements/ElementInfo.tsx b/frontend/src/toolbar/elements/ElementInfo.tsx
index 151367d229ad3..c4459838ed561 100644
--- a/frontend/src/toolbar/elements/ElementInfo.tsx
+++ b/frontend/src/toolbar/elements/ElementInfo.tsx
@@ -7,6 +7,8 @@ import { ActionStep } from '~/toolbar/actions/ActionStep'
import { elementsLogic } from '~/toolbar/elements/elementsLogic'
import { heatmapLogic } from '~/toolbar/elements/heatmapLogic'
+import { actionsTabLogic } from '../actions/actionsTabLogic'
+
function ElementStatistic({
prefix,
suffix,
@@ -34,6 +36,7 @@ export function ElementInfo(): JSX.Element | null {
const { activeMeta } = useValues(elementsLogic)
const { createAction } = useActions(elementsLogic)
+ const { automaticActionCreationEnabled } = useValues(actionsTabLogic)
if (!activeMeta) {
return null
@@ -79,16 +82,20 @@ export function ElementInfo(): JSX.Element | null {
) : null}
{/* eslint-disable-next-line react/forbid-dom-props */}
-
Actions ({activeMeta.actions.length})
+ {!automaticActionCreationEnabled && (
+ <>
+
Actions ({activeMeta.actions.length})
- {activeMeta.actions.length === 0 ? (
-
No actions include this element
- ) : (
-
a.action)} />
+ {activeMeta.actions.length === 0 ? (
+ No actions include this element
+ ) : (
+ a.action)} />
+ )}
+ >
)}
createAction(element)} icon={ }>
- Create a new action
+ {automaticActionCreationEnabled ? 'Select element' : 'Create a new action'}
>
diff --git a/frontend/src/toolbar/elements/elementsLogic.ts b/frontend/src/toolbar/elements/elementsLogic.ts
index eb019fdfe3edb..c7060ea5ba547 100644
--- a/frontend/src/toolbar/elements/elementsLogic.ts
+++ b/frontend/src/toolbar/elements/elementsLogic.ts
@@ -405,6 +405,7 @@ export const elementsLogic = kea([
},
createAction: ({ element }) => {
actions.selectElement(null)
+ // this just sets the action form
actions.newAction(element)
},
})),
diff --git a/frontend/src/toolbar/utils.ts b/frontend/src/toolbar/utils.ts
index ade5110122b76..65f8285cbc189 100644
--- a/frontend/src/toolbar/utils.ts
+++ b/frontend/src/toolbar/utils.ts
@@ -7,6 +7,8 @@ import { CSSProperties } from 'react'
import { ActionStepForm, ElementRect } from '~/toolbar/types'
import { ActionStepType } from '~/types'
+import { ActionStepPropertyKey } from './actions/ActionStep'
+
export const TOOLBAR_ID = '__POSTHOG_TOOLBAR__'
export const LOCALSTORAGE_KEY = '_postHogToolbarParams'
@@ -265,7 +267,11 @@ export function getBoxColors(color: 'blue' | 'red' | 'green', hover = false, opa
}
}
-export function actionStepToActionStepFormItem(step: ActionStepType, isNew = false): ActionStepForm {
+export function actionStepToActionStepFormItem(
+ step: ActionStepType,
+ isNew = false,
+ includedPropertyKeys?: ActionStepPropertyKey[]
+): ActionStepForm {
if (!step) {
return {}
}
@@ -281,24 +287,24 @@ export function actionStepToActionStepFormItem(step: ActionStepType, isNew = fal
...step,
href_selected: true,
selector_selected: hasSelector,
- text_selected: false,
- url_selected: false,
+ text_selected: includedPropertyKeys?.includes('text') || false,
+ url_selected: includedPropertyKeys?.includes('url') || false,
}
} else if (step.tag_name === 'button') {
return {
...step,
text_selected: true,
selector_selected: hasSelector,
- href_selected: false,
- url_selected: false,
+ href_selected: includedPropertyKeys?.includes('href') || false,
+ url_selected: includedPropertyKeys?.includes('url') || false,
}
}
return {
...step,
selector_selected: hasSelector,
- text_selected: false,
- url_selected: false,
- href_selected: false,
+ text_selected: includedPropertyKeys?.includes('text') || false,
+ url_selected: includedPropertyKeys?.includes('url') || false,
+ href_selected: includedPropertyKeys?.includes('href') || false,
}
}
diff --git a/frontend/src/types.ts b/frontend/src/types.ts
index 1000b66e54f06..c981ee4428725 100644
--- a/frontend/src/types.ts
+++ b/frontend/src/types.ts
@@ -574,6 +574,7 @@ export interface ActionStepType {
url?: string | null
/** @default StringMatching.Contains */
url_matching?: ActionStepStringMatching | null
+ name?: string | null
}
export interface ElementType {
@@ -1811,6 +1812,9 @@ export interface DashboardTemplateVariableType {
default: Record
required: boolean
touched?: boolean
+ selector?: string
+ href?: string
+ url?: string
}
export type DashboardLayoutSize = 'sm' | 'xs'
@@ -2142,6 +2146,17 @@ export interface FilterType {
aggregation_group_type_index?: integer // Groups aggregation
}
+export interface TemplateVariableStep {
+ id: string
+ math: BaseMathType
+ name: string | null
+ order: number
+ type: EntityTypes
+ selector?: string | null
+ href?: string | null
+ url?: string | null
+}
+
export interface PropertiesTimelineFilterType {
date_from?: string | null // DateMixin
date_to?: string | null // DateMixin