Skip to content

Commit

Permalink
Allow toolbar to open an experiment pre-selected
Browse files Browse the repository at this point in the history
  • Loading branch information
Phanatic committed Oct 29, 2024
1 parent 483358d commit 6881ac4
Show file tree
Hide file tree
Showing 21 changed files with 215 additions and 69 deletions.
2 changes: 2 additions & 0 deletions ee/clickhouse/views/experiments.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@ class Meta:
"created_by",
"created_at",
"updated_at",
"type",
]
read_only_fields = [
"id",
Expand All @@ -193,6 +194,7 @@ class Meta:
"feature_flag",
"exposure_cohort",
"holdout",
"type",
]

def validate_parameters(self, value):
Expand Down
5 changes: 3 additions & 2 deletions frontend/src/layout/navigation-3000/sidebars/toolbar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,13 @@ export const toolbarSidebarLogic = kea<toolbarSidebarLogicType>([
path(['layout', 'navigation-3000', 'sidebars', 'toolbarSidebarLogic']),
connect(() => ({
values: [
authorizedUrlListLogic({ actionId: null, type: AuthorizedUrlListType.TOOLBAR_URLS }),
authorizedUrlListLogic({ actionId: null, experimentId: null, type: AuthorizedUrlListType.TOOLBAR_URLS }),
['urlsKeyed', 'suggestionsLoading', 'launchUrl'],
sceneLogic,
['activeScene', 'sceneParams'],
],
actions: [
authorizedUrlListLogic({ actionId: null, type: AuthorizedUrlListType.TOOLBAR_URLS }),
authorizedUrlListLogic({ actionId: null, experimentId: null, type: AuthorizedUrlListType.TOOLBAR_URLS }),
['addUrl', 'removeUrl', 'updateUrl'],
],
})),
Expand All @@ -50,6 +50,7 @@ export const toolbarSidebarLogic = kea<toolbarSidebarLogicType>([
onAdd: async (url) => {
await authorizedUrlListLogic({
actionId: null,
experimentId: null,
type: AuthorizedUrlListType.TOOLBAR_URLS,
}).asyncActions.addUrl(url)
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@ function EmptyState({
) : null
}

function AuthorizedUrlForm({ actionId, type }: AuthorizedUrlListProps): JSX.Element {
const logic = authorizedUrlListLogic({ actionId: actionId ?? null, type })
function AuthorizedUrlForm({ actionId, experimentId, type }: AuthorizedUrlListProps): JSX.Element {
const logic = authorizedUrlListLogic({ actionId: actionId ?? null, experimentId: experimentId ?? null, type })
const { isProposedUrlSubmitting } = useValues(logic)
const { cancelProposingUrl } = useActions(logic)
return (
Expand Down Expand Up @@ -78,15 +78,24 @@ function AuthorizedUrlForm({ actionId, type }: AuthorizedUrlListProps): JSX.Elem

export interface AuthorizedUrlListProps {
actionId?: number
experimentId?: number | 'new'
query?: string
type: AuthorizedUrlListType
}

export function AuthorizedUrlList({
actionId,
experimentId,
query,
type,
addText = 'Add',
}: AuthorizedUrlListProps & { addText?: string }): JSX.Element {
const logic = authorizedUrlListLogic({ actionId: actionId ?? null, type })
const logic = authorizedUrlListLogic({
experimentId: experimentId ?? null,
actionId: actionId ?? null,
type,
query,
})
const {
urlsKeyed,
suggestionsLoading,
Expand Down Expand Up @@ -162,7 +171,7 @@ export function AuthorizedUrlList({
type === AuthorizedUrlListType.TOOLBAR_URLS
? launchUrl(keyedURL.url)
: // other urls are simply opened directly
keyedURL.url
keyedURL.url + query
}
targetBlank
tooltip={
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ describe('the authorized urls list logic', () => {
logic = authorizedUrlListLogic({
type: AuthorizedUrlListType.TOOLBAR_URLS,
actionId: null,
experimentId: null,
})
logic.mount()
})
Expand Down Expand Up @@ -119,6 +120,7 @@ describe('the authorized urls list logic', () => {
logic = authorizedUrlListLogic({
type: AuthorizedUrlListType.RECORDING_DOMAINS,
actionId: null,
experimentId: null,
})
logic.mount()
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export interface ProposeNewUrlFormType {
export enum AuthorizedUrlListType {
TOOLBAR_URLS = 'TOOLBAR_URLS',
RECORDING_DOMAINS = 'RECORDING_DOMAINS',
WEB_EXPERIMENTS = 'WEB_EXPERIMENTS',
}

/**
Expand Down Expand Up @@ -90,19 +91,22 @@ export const validateProposedUrl = (
/** defaultIntent: whether to launch with empty intent (i.e. toolbar mode is default) */
export function appEditorUrl(
appUrl: string,
options?: { actionId?: number | null; userIntent?: ToolbarUserIntent }
options?: { actionId?: number | null; experimentId?: number | null | 'new'; userIntent?: ToolbarUserIntent }
): string {
// See https://github.com/PostHog/posthog-js/blob/f7119c/src/extensions/toolbar.ts#L52 for where these params
// are passed. `appUrl` is an extra `redirect_to_site` param.
const params: ToolbarParams & { appUrl: string } = {
userIntent: options?.userIntent ?? (options?.actionId ? 'edit-action' : 'add-action'),
userIntent:
options?.userIntent ??
(options?.actionId ? 'edit-action' : options?.experimentId ? 'edit-experiment' : 'add-action'),
// Make sure to pass the app url, otherwise the api_host will be used by
// the toolbar, which isn't correct when used behind a reverse proxy as
// we require e.g. SSO login to the app, which will not work when placed
// behind a proxy unless we register each domain with the OAuth2 client.
apiURL: apiHostOrigin(),
appUrl,
...(options?.actionId ? { actionId: options.actionId } : {}),
...(options?.experimentId ? { experimentId: options.experimentId } : {}),
}
return '/api/user/redirect_to_site/' + encodeParams(params, '?')
}
Expand Down Expand Up @@ -164,7 +168,9 @@ export interface KeyedAppUrl {

export interface AuthorizedUrlListLogicProps {
actionId: number | null
experimentId: number | null | 'new'
type: AuthorizedUrlListType
query: string | undefined
}
export const authorizedUrlListLogic = kea<authorizedUrlListLogicType>([
path((key) => ['lib', 'components', 'AuthorizedUrlList', 'authorizedUrlListLogic', key]),
Expand Down Expand Up @@ -372,11 +378,18 @@ export const authorizedUrlListLogic = kea<authorizedUrlListLogicType>([
},
],
launchUrl: [
(_, p) => [p.actionId],
(actionId) => (url: string) =>
appEditorUrl(url, {
(_, p) => [p.actionId, p.experimentId],
(actionId, experimentId) => (url: string) => {
if (experimentId) {
return appEditorUrl(url, {
experimentId,
})
}

return appEditorUrl(url, {
actionId,
}),
})
},
],
isAddUrlFormVisible: [(s) => [s.editUrlIndex], (editUrlIndex) => editUrlIndex === -1],
onlyAllowDomains: [(_, p) => [p.type], (type) => type === AuthorizedUrlListType.RECORDING_DOMAINS],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,13 @@ export const iframedToolbarBrowserLogic = kea<iframedToolbarBrowserLogicType>([

connect({
values: [
authorizedUrlListLogic({ actionId: null, type: AuthorizedUrlListType.TOOLBAR_URLS }),
authorizedUrlListLogic({ actionId: null, experimentId: null, type: AuthorizedUrlListType.TOOLBAR_URLS }),
['urlsKeyed', 'checkUrlIsAuthorized'],
teamLogic,
['currentTeam'],
],
actions: [
authorizedUrlListLogic({ actionId: null, type: AuthorizedUrlListType.TOOLBAR_URLS }),
authorizedUrlListLogic({ actionId: null, experimentId: null, type: AuthorizedUrlListType.TOOLBAR_URLS }),
['addUrl'],
teamLogic,
['updateCurrentTeamSuccess'],
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import '../Experiment.scss'

import { IconFlag } from '@posthog/icons'
import { LemonButton, LemonTable, LemonTableColumns } from '@posthog/lemon-ui'
import { LemonButton, LemonDialog, LemonTable, LemonTableColumns } from '@posthog/lemon-ui'
import { useActions, useValues } from 'kea'
import { AuthorizedUrlList } from 'lib/components/AuthorizedUrlList/AuthorizedUrlList'
import { AuthorizedUrlListType } from 'lib/components/AuthorizedUrlList/authorizedUrlListLogic'
import { IconOpenInApp } from 'lib/lemon-ui/icons'

import { sidePanelStateLogic } from '~/layout/navigation-3000/sidepanel/sidePanelStateLogic'
import { MultivariateFlagVariant, SidePanelTab } from '~/types'
Expand All @@ -16,9 +19,29 @@ export function DistributionTable(): JSX.Element {
const { reportExperimentReleaseConditionsViewed } = useActions(experimentLogic)
const { openSidePanel } = useActions(sidePanelStateLogic)

const onSelectElement = (variant: string): void => {
LemonDialog.open({
title: 'Select a domain',
description: 'Choose the domain on which to preview this experiment variant',
content: (
<>
<AuthorizedUrlList
query={'?__experiment_id=' + experiment?.id + '&__experiment_variant=' + variant}
experimentId={experiment?.id}
type={AuthorizedUrlListType.WEB_EXPERIMENTS}
/>
</>
),
primaryButton: {
children: 'Close',
type: 'secondary',
},
})
}
const className = experiment?.type === 'web' ? 'w-1/2.5' : 'w-1/3'
const columns: LemonTableColumns<MultivariateFlagVariant> = [
{
className: 'w-1/3',
className: className,
key: 'key',
title: 'Variant',
render: function Key(_, item): JSX.Element {
Expand All @@ -29,15 +52,15 @@ export function DistributionTable(): JSX.Element {
},
},
{
className: 'w-1/3',
className: className,
key: 'rollout_percentage',
title: 'Rollout',
render: function Key(_, item): JSX.Element {
return <div>{`${item.rollout_percentage}%`}</div>
},
},
{
className: 'w-1/3',
className: className,
key: 'variant_screenshot',
title: 'Screenshot',
render: function Key(_, item): JSX.Element {
Expand All @@ -50,6 +73,31 @@ export function DistributionTable(): JSX.Element {
},
]

if (experiment.type === 'web') {
columns.push({
className: className,
key: 'preview_web_experiment',
title: 'Preview',
render: function Key(_, item): JSX.Element {
return (
<div className="my-2">
<LemonButton
size="small"
type="secondary"
onClick={(e) => {
e.preventDefault()
onSelectElement(item.key)
}}
sideIcon={<IconOpenInApp />}
>
Preview variant
</LemonButton>
</div>
)
},
})
}

return (
<div>
<div className="flex">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@ import '../Experiment.scss'

import { LemonDivider } from '@posthog/lemon-ui'
import { useActions, useValues } from 'kea'
import { FEATURE_FLAGS } from 'lib/constants'
import { featureFlagLogic } from 'lib/logic/featureFlagLogic'
import { WebExperimentImplementationDetails } from 'scenes/experiments/WebExperimentImplementationDetails'

import { ExperimentImplementationDetails } from '../ExperimentImplementationDetails'
import { experimentLogic } from '../experimentLogic'
Expand All @@ -17,7 +16,6 @@ import {
import { DataCollection } from './DataCollection'
import { DistributionTable } from './DistributionTable'
import { ExperimentExposureModal, ExperimentGoalModal, Goal } from './Goal'
import { HoldoutSelector } from './HoldoutSelector'
import { Info } from './Info'
import { Overview } from './Overview'
import { ReleaseConditionsTable } from './ReleaseConditionsTable'
Expand All @@ -27,7 +25,6 @@ import { SecondaryMetricsTable } from './SecondaryMetricsTable'
export function ExperimentView(): JSX.Element {
const { experiment, experimentLoading, experimentResultsLoading, experimentId, experimentResults } =
useValues(experimentLogic)
const { featureFlags } = useValues(featureFlagLogic)

const { updateExperimentSecondaryMetrics } = useActions(experimentLogic)

Expand All @@ -51,7 +48,6 @@ export function ExperimentView(): JSX.Element {
<div className="xl:flex">
<div className="w-1/2 pr-2">
<Goal />
{featureFlags[FEATURE_FLAGS.EXPERIMENTS_HOLDOUTS] && <HoldoutSelector />}
</div>

<div className="w-1/2 xl:pl-2 mt-8 xl:mt-0">
Expand All @@ -65,14 +61,18 @@ export function ExperimentView(): JSX.Element {
<div className="xl:flex">
<div className="w-1/2 pr-2">
<Goal />
{featureFlags[FEATURE_FLAGS.EXPERIMENTS_HOLDOUTS] && <HoldoutSelector />}
</div>

<div className="w-1/2 xl:pl-2 mt-8 xl:mt-0">
<DataCollection />
</div>
</div>
<ExperimentImplementationDetails experiment={experiment} />
{experiment.type === 'web' ? (
<WebExperimentImplementationDetails experiment={experiment} />
) : (
<ExperimentImplementationDetails experiment={experiment} />
)}

{experiment.start_date && (
<div>
<ResultsHeader />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { AuthorizedUrlList } from 'lib/components/AuthorizedUrlList/AuthorizedUrlList'
import { AuthorizedUrlListType } from 'lib/components/AuthorizedUrlList/authorizedUrlListLogic'
import { IconOpenInApp } from 'lib/lemon-ui/icons'
import { LemonButton } from 'lib/lemon-ui/LemonButton'
import { LemonDialog } from 'lib/lemon-ui/LemonDialog'

import { Experiment } from '~/types'

interface WebExperimentImplementationDetails {
experiment: Partial<Experiment> | null
}
export function WebExperimentImplementationDetails({ experiment }: WebExperimentImplementationDetails): JSX.Element {
const onSelectElement = (): void => {
LemonDialog.open({
title: 'Select a domain',
description: experiment?.id
? 'Choose the domain on which to edit this experiment'
: 'Choose the domain on which to create this experiment',
content: (
<>
<AuthorizedUrlList experimentId={experiment?.id} type={AuthorizedUrlListType.TOOLBAR_URLS} />
</>
),
primaryButton: {
children: 'Close',
type: 'secondary',
},
})
}

return (
<>
<div>
<h2 className="font-semibold text-lg mb-2">Implementation</h2>
<div className="border px-6 rounded bg-bg-light">
<div className="font-semibold leading-tight text-base text-current m-4">
This Web experiment's implementation should be edited on your website.
</div>
<div className="m-4">
<LemonButton
size="small"
type="secondary"
onClick={onSelectElement}
sideIcon={<IconOpenInApp />}
>
Edit Web experiment on website
</LemonButton>
</div>
</div>
</div>
</>
)
}
2 changes: 1 addition & 1 deletion frontend/src/scenes/heatmaps/heatmapsBrowserLogic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export const heatmapsBrowserLogic = kea<heatmapsBrowserLogicType>([

connect({
values: [
authorizedUrlListLogic({ actionId: null, type: AuthorizedUrlListType.TOOLBAR_URLS }),
authorizedUrlListLogic({ actionId: null, experimentId: null, type: AuthorizedUrlListType.TOOLBAR_URLS }),
['urlsKeyed', 'checkUrlIsAuthorized'],
],
}),
Expand Down
Loading

0 comments on commit 6881ac4

Please sign in to comment.