diff --git a/ee/clickhouse/queries/experiments/funnel_experiment_result.py b/ee/clickhouse/queries/experiments/funnel_experiment_result.py
index e311657cc52c7..f68816ed3b129 100644
--- a/ee/clickhouse/queries/experiments/funnel_experiment_result.py
+++ b/ee/clickhouse/queries/experiments/funnel_experiment_result.py
@@ -13,6 +13,7 @@
calculate_credible_intervals,
calculate_probabilities,
)
+from posthog.models.experiment import ExperimentHoldout
from posthog.models.feature_flag import FeatureFlag
from posthog.models.filters.filter import Filter
from posthog.models.team import Team
@@ -54,10 +55,13 @@ def __init__(
feature_flag: FeatureFlag,
experiment_start_date: datetime,
experiment_end_date: Optional[datetime] = None,
+ holdout: Optional[ExperimentHoldout] = None,
funnel_class: type[ClickhouseFunnel] = ClickhouseFunnel,
):
breakdown_key = f"$feature/{feature_flag.key}"
self.variants = [variant["key"] for variant in feature_flag.variants]
+ if holdout:
+ self.variants.append(f"holdout-{holdout.id}")
# our filters assume that the given time ranges are in the project timezone.
# while start and end date are in UTC.
diff --git a/ee/clickhouse/queries/experiments/trend_experiment_result.py b/ee/clickhouse/queries/experiments/trend_experiment_result.py
index 0971120f2366a..ac9508d21051c 100644
--- a/ee/clickhouse/queries/experiments/trend_experiment_result.py
+++ b/ee/clickhouse/queries/experiments/trend_experiment_result.py
@@ -22,6 +22,7 @@
calculate_credible_intervals,
calculate_probabilities,
)
+from posthog.models.experiment import ExperimentHoldout
from posthog.models.feature_flag import FeatureFlag
from posthog.models.filters.filter import Filter
from posthog.models.team import Team
@@ -81,9 +82,12 @@ def __init__(
experiment_end_date: Optional[datetime] = None,
trend_class: type[Trends] = Trends,
custom_exposure_filter: Optional[Filter] = None,
+ holdout: Optional[ExperimentHoldout] = None,
):
breakdown_key = f"$feature/{feature_flag.key}"
self.variants = [variant["key"] for variant in feature_flag.variants]
+ if holdout:
+ self.variants.append(f"holdout-{holdout.id}")
# our filters assume that the given time ranges are in the project timezone.
# while start and end date are in UTC.
diff --git a/ee/clickhouse/views/experiments.py b/ee/clickhouse/views/experiments.py
index d33f99bb41a3c..6df24dc012cea 100644
--- a/ee/clickhouse/views/experiments.py
+++ b/ee/clickhouse/views/experiments.py
@@ -51,6 +51,7 @@ def _calculate_experiment_results(experiment: Experiment, refresh: bool = False)
experiment.feature_flag,
experiment.start_date,
experiment.end_date,
+ holdout=experiment.holdout,
custom_exposure_filter=exposure_filter,
).get_results()
else:
@@ -60,6 +61,7 @@ def _calculate_experiment_results(experiment: Experiment, refresh: bool = False)
experiment.feature_flag,
experiment.start_date,
experiment.end_date,
+ holdout=experiment.holdout,
).get_results()
return _experiment_results_cached(
diff --git a/frontend/__snapshots__/scenes-app-experiments--complete-funnel-experiment--dark.png b/frontend/__snapshots__/scenes-app-experiments--complete-funnel-experiment--dark.png
index 5a042b0f3b12e..a0adec369a052 100644
Binary files a/frontend/__snapshots__/scenes-app-experiments--complete-funnel-experiment--dark.png and b/frontend/__snapshots__/scenes-app-experiments--complete-funnel-experiment--dark.png differ
diff --git a/frontend/__snapshots__/scenes-app-experiments--complete-funnel-experiment--light.png b/frontend/__snapshots__/scenes-app-experiments--complete-funnel-experiment--light.png
index e10f4cc6a6a75..bf1949df70469 100644
Binary files a/frontend/__snapshots__/scenes-app-experiments--complete-funnel-experiment--light.png and b/frontend/__snapshots__/scenes-app-experiments--complete-funnel-experiment--light.png differ
diff --git a/frontend/__snapshots__/scenes-app-experiments--running-trend-experiment--dark.png b/frontend/__snapshots__/scenes-app-experiments--running-trend-experiment--dark.png
index 32591170a12eb..a82b90c53b1d1 100644
Binary files a/frontend/__snapshots__/scenes-app-experiments--running-trend-experiment--dark.png and b/frontend/__snapshots__/scenes-app-experiments--running-trend-experiment--dark.png differ
diff --git a/frontend/__snapshots__/scenes-app-experiments--running-trend-experiment--light.png b/frontend/__snapshots__/scenes-app-experiments--running-trend-experiment--light.png
index 8e6538c1e24dd..425ee3b2416b4 100644
Binary files a/frontend/__snapshots__/scenes-app-experiments--running-trend-experiment--light.png and b/frontend/__snapshots__/scenes-app-experiments--running-trend-experiment--light.png differ
diff --git a/frontend/__snapshots__/scenes-app-experiments--running-trend-experiment-many-variants--dark.png b/frontend/__snapshots__/scenes-app-experiments--running-trend-experiment-many-variants--dark.png
index 784e386f8b67f..f529d6ff05a85 100644
Binary files a/frontend/__snapshots__/scenes-app-experiments--running-trend-experiment-many-variants--dark.png and b/frontend/__snapshots__/scenes-app-experiments--running-trend-experiment-many-variants--dark.png differ
diff --git a/frontend/__snapshots__/scenes-app-experiments--running-trend-experiment-many-variants--light.png b/frontend/__snapshots__/scenes-app-experiments--running-trend-experiment-many-variants--light.png
index 9c8668050ec83..d79998d829a77 100644
Binary files a/frontend/__snapshots__/scenes-app-experiments--running-trend-experiment-many-variants--light.png and b/frontend/__snapshots__/scenes-app-experiments--running-trend-experiment-many-variants--light.png differ
diff --git a/frontend/src/lib/constants.tsx b/frontend/src/lib/constants.tsx
index 3639d06e4b466..cc6a67fc504a2 100644
--- a/frontend/src/lib/constants.tsx
+++ b/frontend/src/lib/constants.tsx
@@ -220,6 +220,7 @@ export const FEATURE_FLAGS = {
LEGACY_ACTION_WEBHOOKS: 'legacy-action-webhooks', // owner: @mariusandra #team-cdp
SESSION_REPLAY_URL_TRIGGER: 'session-replay-url-trigger', // owner: @richard-better #team-replay
REPLAY_TEMPLATES: 'replay-templates', // owner: @raquelmsmith #team-replay
+ EXPERIMENTS_HOLDOUTS: 'experiments-holdouts', // owner: @jurajmajerik #team-experiments
MESSAGING: 'messaging', // owner @mariusandra #team-cdp
} as const
export type FeatureFlagKey = (typeof FEATURE_FLAGS)[keyof typeof FEATURE_FLAGS]
diff --git a/frontend/src/scenes/experiments/ExperimentForm.tsx b/frontend/src/scenes/experiments/ExperimentForm.tsx
index 484281178822c..bcae52d655911 100644
--- a/frontend/src/scenes/experiments/ExperimentForm.tsx
+++ b/frontend/src/scenes/experiments/ExperimentForm.tsx
@@ -5,11 +5,12 @@ import { LemonDivider, LemonInput, LemonTextArea, Tooltip } from '@posthog/lemon
import { BindLogic, useActions, useValues } from 'kea'
import { Form, Group } from 'kea-forms'
import { ExperimentVariantNumber } from 'lib/components/SeriesGlyph'
-import { MAX_EXPERIMENT_VARIANTS } from 'lib/constants'
+import { FEATURE_FLAGS, MAX_EXPERIMENT_VARIANTS } from 'lib/constants'
import { IconChevronLeft } from 'lib/lemon-ui/icons'
import { LemonButton } from 'lib/lemon-ui/LemonButton'
import { LemonField } from 'lib/lemon-ui/LemonField'
import { LemonRadio } from 'lib/lemon-ui/LemonRadio'
+import { LemonSelect } from 'lib/lemon-ui/LemonSelect'
import { capitalizeFirstLetter } from 'lib/utils'
import { useEffect } from 'react'
import { insightDataLogic } from 'scenes/insights/insightDataLogic'
@@ -23,7 +24,7 @@ import { experimentLogic } from './experimentLogic'
import { ExperimentInsightCreator } from './MetricSelector'
const StepInfo = (): JSX.Element => {
- const { experiment } = useValues(experimentLogic)
+ const { experiment, featureFlags } = useValues(experimentLogic)
const { addExperimentGroup, removeExperimentGroup, moveToNextFormStep } = useActions(experimentLogic)
return (
@@ -134,6 +135,14 @@ const StepInfo = (): JSX.Element => {
+ {featureFlags[FEATURE_FLAGS.EXPERIMENTS_HOLDOUTS] && (
+
+
Holdout group
+
Exclude a stable group of users from the experiment.
+
+
+
+ )}
{
)
}
+export const HoldoutSelector = (): JSX.Element => {
+ const { experiment, holdouts } = useValues(experimentLogic)
+ const { setExperiment } = useActions(experimentLogic)
+
+ const holdoutOptions = holdouts.map((holdout) => ({
+ value: holdout.id,
+ label: holdout.name,
+ }))
+ holdoutOptions.unshift({ value: null, label: 'No holdout' })
+
+ return (
+
+ {
+ setExperiment({
+ ...experiment,
+ holdout_id: value,
+ })
+ }}
+ data-attr="experiment-holdout-selector"
+ />
+
+ )
+}
+
export function ExperimentForm(): JSX.Element {
const { currentFormStep, props } = useValues(experimentLogic)
const { setCurrentFormStep } = useActions(experimentLogic)
diff --git a/frontend/src/scenes/experiments/ExperimentView/ExperimentView.tsx b/frontend/src/scenes/experiments/ExperimentView/ExperimentView.tsx
index 0430a50a0b41b..1c8bf5bd5e71a 100644
--- a/frontend/src/scenes/experiments/ExperimentView/ExperimentView.tsx
+++ b/frontend/src/scenes/experiments/ExperimentView/ExperimentView.tsx
@@ -2,6 +2,8 @@ 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 { ExperimentImplementationDetails } from '../ExperimentImplementationDetails'
import { experimentLogic } from '../experimentLogic'
@@ -15,6 +17,7 @@ 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'
@@ -24,6 +27,7 @@ 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)
@@ -47,6 +51,7 @@ export function ExperimentView(): JSX.Element {
+ {featureFlags[FEATURE_FLAGS.EXPERIMENTS_HOLDOUTS] && }
@@ -60,6 +65,7 @@ export function ExperimentView(): JSX.Element {
+ {featureFlags[FEATURE_FLAGS.EXPERIMENTS_HOLDOUTS] && }
diff --git a/frontend/src/scenes/experiments/ExperimentView/HoldoutSelector.tsx b/frontend/src/scenes/experiments/ExperimentView/HoldoutSelector.tsx
new file mode 100644
index 0000000000000..f8982ad3d3636
--- /dev/null
+++ b/frontend/src/scenes/experiments/ExperimentView/HoldoutSelector.tsx
@@ -0,0 +1,47 @@
+import { IconInfo } from '@posthog/icons'
+import { LemonSelect, Tooltip } from '@posthog/lemon-ui'
+import { useActions, useValues } from 'kea'
+
+import { experimentLogic } from '../experimentLogic'
+
+export function HoldoutSelector(): JSX.Element {
+ const { experiment, holdouts, isExperimentRunning } = useValues(experimentLogic)
+ const { setExperiment, updateExperiment } = useActions(experimentLogic)
+
+ const holdoutOptions = holdouts.map((holdout) => ({
+ value: holdout.id,
+ label: holdout.name,
+ }))
+ holdoutOptions.unshift({ value: null, label: 'No holdout' })
+
+ return (
+
+
+
Holdout group
+
+
+
+
+
+ {
+ setExperiment({
+ ...experiment,
+ holdout_id: value,
+ })
+ updateExperiment({ holdout_id: value })
+ }}
+ data-attr="experiment-holdout-selector"
+ />
+
+
+ )
+}
diff --git a/frontend/src/scenes/experiments/ExperimentView/components.tsx b/frontend/src/scenes/experiments/ExperimentView/components.tsx
index 71e6c7f35dd25..b4e092bf8f77f 100644
--- a/frontend/src/scenes/experiments/ExperimentView/components.tsx
+++ b/frontend/src/scenes/experiments/ExperimentView/components.tsx
@@ -50,10 +50,25 @@ export function VariantTag({
experimentId: number | 'new'
variantKey: string
}): JSX.Element {
- const { experimentResults, getIndexForVariant } = useValues(experimentLogic({ experimentId }))
+ const { experiment, experimentResults, getIndexForVariant } = useValues(experimentLogic({ experimentId }))
+
+ if (experiment.holdout && variantKey === `holdout-${experiment.holdout_id}`) {
+ return (
+
+
+ {experiment.holdout.name}
+
+ )
+ }
return (
-
+
- {tab === ExperimentsTabs.Archived ? (
-
+
+ {featureFlags[FEATURE_FLAGS.EXPERIMENTS_HOLDOUTS] && tab === ExperimentsTabs.Holdouts ? (
+
) : (
- router.actions.push(urls.experiment('new'))}
- isEmpty={shouldShowEmptyState}
- customHog={ExperimentsHog}
- />
- )}
- {!shouldShowEmptyState && (
<>
-
-
+ ) : (
+
router.actions.push(urls.experiment('new'))}
+ isEmpty={shouldShowEmptyState}
+ customHog={ExperimentsHog}
/>
-
-
- Status
-
-
{
- if (status) {
- setSearchStatus(status as ProgressStatus | 'all')
- }
+ )}
+ {!shouldShowEmptyState && (
+ <>
+
+
+
+
+ Status
+
+ {
+ if (status) {
+ setSearchStatus(status as ProgressStatus | 'all')
+ }
+ }}
+ options={
+ [
+ { label: 'All', value: 'all' },
+ { label: 'Draft', value: ProgressStatus.Draft },
+ { label: 'Running', value: ProgressStatus.Running },
+ { label: 'Complete', value: ProgressStatus.Complete },
+ ] as { label: string; value: string }[]
+ }
+ value={searchStatus ?? 'all'}
+ dropdownMatchSelectWidth={false}
+ dropdownMaxContentWidth
+ />
+
+ Created by
+
+ setUserFilter(user?.uuid ?? null)}
+ />
+
+
+
-
- Created by
-
- setUserFilter(user?.uuid ?? null)}
- />
-
-
-
+ >
+ )}
>
)}
diff --git a/frontend/src/scenes/experiments/Holdouts.tsx b/frontend/src/scenes/experiments/Holdouts.tsx
new file mode 100644
index 0000000000000..e6d7d6a13f328
--- /dev/null
+++ b/frontend/src/scenes/experiments/Holdouts.tsx
@@ -0,0 +1,229 @@
+import { IconPencil, IconTrash } from '@posthog/icons'
+import {
+ LemonBanner,
+ LemonButton,
+ LemonDialog,
+ LemonDivider,
+ LemonInput,
+ LemonLabel,
+ LemonModal,
+ LemonTable,
+ LemonTableColumns,
+} from '@posthog/lemon-ui'
+import { useActions, useValues } from 'kea'
+import { LemonSlider } from 'lib/lemon-ui/LemonSlider'
+import { useState } from 'react'
+
+import { Holdout, holdoutsLogic, NEW_HOLDOUT } from './holdoutsLogic'
+
+export function Holdouts(): JSX.Element {
+ const { holdouts, holdoutsLoading, holdout } = useValues(holdoutsLogic)
+ const { createHoldout, deleteHoldout, setHoldout, updateHoldout } = useActions(holdoutsLogic)
+
+ const [isHoldoutModalOpen, setIsHoldoutModalOpen] = useState(false)
+ const [editingHoldout, setEditingHoldout] = useState
(null)
+
+ const openEditModal = (holdout: Holdout): void => {
+ setEditingHoldout(holdout)
+ setHoldout(holdout)
+ setIsHoldoutModalOpen(true)
+ }
+
+ const openCreateModal = (): void => {
+ setEditingHoldout(null)
+ setHoldout({ ...NEW_HOLDOUT })
+ setIsHoldoutModalOpen(true)
+ }
+
+ const closeModal = (): void => {
+ setIsHoldoutModalOpen(false)
+ setEditingHoldout(null)
+ }
+
+ const getDisabledReason = (): string | undefined => {
+ if (!holdout.name) {
+ return 'Name is required'
+ }
+ if (holdout.filters?.[0]?.rollout_percentage === undefined) {
+ return 'Rollout percentage is required'
+ }
+ }
+
+ const columns = [
+ {
+ title: 'Name',
+ dataIndex: 'name',
+ key: 'name',
+ render: (name: string) => {name}
,
+ },
+ {
+ title: 'Description',
+ dataIndex: 'description',
+ key: 'description',
+ },
+ {
+ title: 'Rollout Percentage',
+ dataIndex: 'filters',
+ key: 'rollout',
+ render: (filters: Holdout['filters']) => {
+ const percentage = filters?.[0]?.rollout_percentage || 0
+ return {percentage} %
+ },
+ },
+ {
+ title: 'Actions',
+ key: 'actions',
+ render: (_: any, record: Holdout) => (
+
+
}
+ onClick={() => openEditModal(record)}
+ />
+
}
+ size="xsmall"
+ status="danger"
+ onClick={() => {
+ LemonDialog.open({
+ title: 'Delete this holdout?',
+ content: (
+
+ Are you sure you want to delete the holdout "{record.name}"? This action
+ cannot be undone.
+
+ ),
+ primaryButton: {
+ children: 'Delete',
+ type: 'primary',
+ status: 'danger',
+ onClick: () => deleteHoldout(record.id),
+ size: 'small',
+ },
+ secondaryButton: {
+ children: 'Cancel',
+ type: 'tertiary',
+ size: 'small',
+ },
+ })
+ }}
+ />
+
+ ),
+ },
+ ]
+
+ return (
+
+
+ Cancel
+ {
+ if (editingHoldout) {
+ updateHoldout(editingHoldout.id, holdout)
+ } else {
+ createHoldout()
+ }
+ closeModal()
+ }}
+ disabledReason={getDisabledReason()}
+ >
+ {editingHoldout ? 'Update' : 'Save'}
+
+ >
+ }
+ >
+
+
+ Name
+ setHoldout({ name })}
+ placeholder="e.g. 'Frontend holdout group 1'"
+ />
+
+
+ Description
+ setHoldout({ description })}
+ />
+
+
+
+
+
+
+ Specify the percentage population that should be included in this holdout group.
+ This is stable across experiments.
+
+
+
+
+ Roll out to{' '}
+
+ setHoldout({
+ filters: [{ properties: [], rollout_percentage }],
+ })
+ }
+ min={0}
+ max={100}
+ step={1}
+ className="ml-1.5 w-20"
+ />
+
+ setHoldout({
+ filters: [{ properties: [], rollout_percentage }],
+ })
+ }
+ min={0}
+ max={100}
+ step="any"
+ suffix={%}
+ />
+ of total users.
+
+
+
+
+
+
+
+
+ Holdouts are stable groups of users excluded from experiment variations.They act as a baseline,
+ helping you see how users behave without any changes applied. This lets you directly compare
+ their behavior to those exposed to the experiment variations. Once a holdout is configured, you
+ can apply it to an experiment during creation.
+
+
+
+
+
You have not created any holdouts yet.
+ }
+ loading={holdoutsLoading}
+ dataSource={holdouts}
+ columns={columns as LemonTableColumns}
+ />
+
+ New holdout
+
+
+ )
+}
diff --git a/frontend/src/scenes/experiments/experimentLogic.tsx b/frontend/src/scenes/experiments/experimentLogic.tsx
index 09b6597176fc8..19e33aca83831 100644
--- a/frontend/src/scenes/experiments/experimentLogic.tsx
+++ b/frontend/src/scenes/experiments/experimentLogic.tsx
@@ -54,6 +54,7 @@ import {
import { EXPERIMENT_EXPOSURE_INSIGHT_ID, EXPERIMENT_INSIGHT_ID } from './constants'
import type { experimentLogicType } from './experimentLogicType'
import { experimentsLogic } from './experimentsLogic'
+import { holdoutsLogic } from './holdoutsLogic'
import { getMinimumDetectableEffect, transformFiltersForWinningVariant } from './utils'
const NEW_EXPERIMENT: Experiment = {
@@ -71,6 +72,7 @@ const NEW_EXPERIMENT: Experiment = {
created_at: null,
created_by: null,
updated_at: null,
+ holdout_id: null,
}
export interface ExperimentLogicProps {
@@ -112,6 +114,8 @@ export const experimentLogic = kea
([
['insightDataLoading as goalInsightDataLoading'],
featureFlagLogic,
['featureFlags'],
+ holdoutsLogic,
+ ['holdouts'],
],
actions: [
experimentsLogic,
diff --git a/frontend/src/scenes/experiments/holdoutsLogic.tsx b/frontend/src/scenes/experiments/holdoutsLogic.tsx
new file mode 100644
index 0000000000000..3f70a30d61216
--- /dev/null
+++ b/frontend/src/scenes/experiments/holdoutsLogic.tsx
@@ -0,0 +1,95 @@
+import { actions, events, kea, listeners, path, reducers } from 'kea'
+import { loaders } from 'kea-loaders'
+import api from 'lib/api'
+import { lemonToast } from 'lib/lemon-ui/LemonToast/LemonToast'
+
+import { UserBasicType } from '~/types'
+
+import type { holdoutsLogicType } from './holdoutsLogicType'
+
+export interface Holdout {
+ id: number | null
+ name: string
+ description: string | null
+ filters: Record
+ created_by: UserBasicType | null
+ created_at: string | null
+ updated_at: string | null
+}
+
+export const NEW_HOLDOUT: Holdout = {
+ id: null,
+ name: '',
+ description: null,
+ filters: [
+ {
+ properties: [],
+ rollout_percentage: 10,
+ variant: 'holdout',
+ },
+ ],
+ created_by: null,
+ created_at: null,
+ updated_at: null,
+}
+
+export const holdoutsLogic = kea([
+ path(['scenes', 'experiments', 'holdoutsLogic']),
+ actions({
+ setHoldout: (holdout: Partial) => ({ holdout }),
+ createHoldout: true,
+ updateHoldout: (id: number | null, holdout: Partial) => ({ id, holdout }),
+ deleteHoldout: (id: number | null) => ({ id }),
+ loadHoldout: (id: number | null) => ({ id }),
+ }),
+ reducers({
+ holdout: [
+ NEW_HOLDOUT,
+ {
+ setHoldout: (state, { holdout }) => ({ ...state, ...holdout }),
+ },
+ ],
+ }),
+ loaders(({ values }) => ({
+ holdouts: [
+ [] as Holdout[],
+ {
+ loadHoldouts: async () => {
+ const response = await api.get(`api/projects/@current/experiment_holdouts/`)
+ return response.results as Holdout[]
+ },
+ createHoldout: async () => {
+ const response = await api.create(`api/projects/@current/experiment_holdouts/`, values.holdout)
+ return [...values.holdouts, response] as Holdout[]
+ },
+ updateHoldout: async ({ id, holdout }) => {
+ const response = await api.update(`api/projects/@current/experiment_holdouts/${id}/`, holdout)
+ return values.holdouts.map((h) => (h.id === id ? response : h)) as Holdout[]
+ },
+ deleteHoldout: async ({ id }) => {
+ await api.delete(`api/projects/@current/experiment_holdouts/${id}/`)
+ return values.holdouts.filter((h) => h.id !== id)
+ },
+ },
+ ],
+ })),
+ listeners(({ actions }) => ({
+ createHoldoutSuccess: () => {
+ lemonToast.success('Holdout created')
+ actions.loadHoldouts()
+ },
+ updateHoldoutSuccess: () => {
+ lemonToast.success('Holdout updated')
+ actions.loadHoldouts()
+ },
+ deleteHoldoutSuccess: () => {
+ lemonToast.success('Holdout deleted')
+ actions.loadHoldouts()
+ },
+ })),
+ events(({ actions }) => ({
+ afterMount: () => {
+ actions.loadHoldouts()
+ },
+ })),
+])
diff --git a/frontend/src/types.ts b/frontend/src/types.ts
index b01a6a080d5c9..7c0a8acedad00 100644
--- a/frontend/src/types.ts
+++ b/frontend/src/types.ts
@@ -25,6 +25,7 @@ import type { PostHog, SupportedWebVitalsMetrics } from 'posthog-js'
import { Layout } from 'react-grid-layout'
import { LogLevel } from 'rrweb'
import { BehavioralFilterKey, BehavioralFilterType } from 'scenes/cohorts/CohortFilters/types'
+import { Holdout } from 'scenes/experiments/holdoutsLogic'
import { AggregationAxisFormat } from 'scenes/insights/aggregationAxisFormat'
import { JSONContent } from 'scenes/notebooks/Notebook/utils'
import { Scene } from 'scenes/sceneTypes'
@@ -679,6 +680,7 @@ export enum ExperimentsTabs {
All = 'all',
Yours = 'yours',
Archived = 'archived',
+ Holdouts = 'holdouts',
}
export enum ActivityTab {
@@ -3256,6 +3258,8 @@ export interface Experiment {
created_at: string | null
created_by: UserBasicType | null
updated_at: string | null
+ holdout_id?: number | null
+ holdout?: Holdout
}
export interface FunnelExperimentVariant {