mirror of
https://github.com/PostHog/posthog.git
synced 2024-11-28 09:16:49 +01:00
Add support for Groups in Experiments (#8463)
* add support for Experiment groups * update styling * incl groups in property selection * set participant type and respect FF group type on backend * remove aggregation select
This commit is contained in:
parent
31807bbc87
commit
d5a48fbd06
@ -99,6 +99,9 @@ class ExperimentSerializer(serializers.ModelSerializer):
|
||||
"multivariate": {"variants": variants or default_variants},
|
||||
}
|
||||
|
||||
if validated_data["filters"].get("aggregation_group_type_index"):
|
||||
filters["aggregation_group_type_index"] = validated_data["filters"]["aggregation_group_type_index"]
|
||||
|
||||
feature_flag_serializer = FeatureFlagSerializer(
|
||||
data={
|
||||
"key": feature_flag_key,
|
||||
@ -150,11 +153,17 @@ class ExperimentSerializer(serializers.ModelSerializer):
|
||||
):
|
||||
raise ValidationError("Can't update feature_flag_variants on Experiment")
|
||||
|
||||
feature_flag_properties = validated_data.get("filters", {}).get("properties", [])
|
||||
if feature_flag_properties:
|
||||
feature_flag_properties = validated_data.get("filters", {}).get("properties")
|
||||
if feature_flag_properties is not None:
|
||||
feature_flag.filters["groups"][0]["properties"] = feature_flag_properties
|
||||
feature_flag.save()
|
||||
|
||||
feature_flag_group_type_index = validated_data.get("filters", {}).get("aggregation_group_type_index")
|
||||
# Only update the group type index when filters are sent
|
||||
if validated_data.get("filters"):
|
||||
feature_flag.filters["aggregation_group_type_index"] = feature_flag_group_type_index
|
||||
feature_flag.save()
|
||||
|
||||
if instance.is_draft and has_start_date:
|
||||
feature_flag.active = True
|
||||
feature_flag.save()
|
||||
|
@ -2,11 +2,13 @@ import pytest
|
||||
from rest_framework import status
|
||||
|
||||
from ee.api.test.base import APILicensedTest
|
||||
from ee.clickhouse.models.group import create_group
|
||||
from ee.clickhouse.test.test_journeys import journeys_for
|
||||
from ee.clickhouse.util import ClickhouseTestMixin, snapshot_clickhouse_queries
|
||||
from posthog.constants import ExperimentSignificanceCode
|
||||
from posthog.models.experiment import Experiment
|
||||
from posthog.models.feature_flag import FeatureFlag
|
||||
from posthog.models.group_type_mapping import GroupTypeMapping
|
||||
|
||||
|
||||
class TestExperimentCRUD(APILicensedTest):
|
||||
@ -543,6 +545,101 @@ class TestExperimentCRUD(APILicensedTest):
|
||||
with self.assertRaises(Experiment.DoesNotExist):
|
||||
Experiment.objects.get(pk=id)
|
||||
|
||||
def test_creating_updating_experiment_with_group_aggregation(self):
|
||||
ff_key = "a-b-tests"
|
||||
response = self.client.post(
|
||||
f"/api/projects/{self.team.id}/experiments/",
|
||||
{
|
||||
"name": "Test Experiment",
|
||||
"description": "",
|
||||
"start_date": None,
|
||||
"end_date": None,
|
||||
"feature_flag_key": ff_key,
|
||||
"parameters": None,
|
||||
"filters": {
|
||||
"events": [{"order": 0, "id": "$pageview"}, {"order": 1, "id": "$pageleave"}],
|
||||
"properties": [
|
||||
{
|
||||
"key": "industry",
|
||||
"type": "group",
|
||||
"value": ["technology"],
|
||||
"operator": "exact",
|
||||
"group_type_index": 1,
|
||||
}
|
||||
],
|
||||
"aggregation_group_type_index": 1,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||
self.assertEqual(response.json()["name"], "Test Experiment")
|
||||
self.assertEqual(response.json()["feature_flag_key"], ff_key)
|
||||
|
||||
created_ff = FeatureFlag.objects.get(key=ff_key)
|
||||
|
||||
self.assertEqual(created_ff.key, ff_key)
|
||||
self.assertEqual(created_ff.filters["multivariate"]["variants"][0]["key"], "control")
|
||||
self.assertEqual(created_ff.filters["multivariate"]["variants"][1]["key"], "test")
|
||||
self.assertEqual(created_ff.filters["groups"][0]["properties"][0]["key"], "industry")
|
||||
self.assertEqual(created_ff.filters["aggregation_group_type_index"], 1)
|
||||
|
||||
id = response.json()["id"]
|
||||
|
||||
# Now update group type index
|
||||
response = self.client.patch(
|
||||
f"/api/projects/{self.team.id}/experiments/{id}",
|
||||
{
|
||||
"description": "Bazinga",
|
||||
"filters": {
|
||||
"events": [{"order": 0, "id": "$pageview"}, {"order": 1, "id": "$pageleave"}],
|
||||
"properties": [],
|
||||
"aggregation_group_type_index": 0,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
experiment = Experiment.objects.get(pk=id)
|
||||
self.assertEqual(experiment.description, "Bazinga")
|
||||
|
||||
created_ff = FeatureFlag.objects.get(key=ff_key)
|
||||
self.assertEqual(created_ff.key, ff_key)
|
||||
self.assertFalse(created_ff.active)
|
||||
self.assertEqual(created_ff.filters["multivariate"]["variants"][0]["key"], "control")
|
||||
self.assertEqual(created_ff.filters["multivariate"]["variants"][1]["key"], "test")
|
||||
self.assertEqual(created_ff.filters["groups"][0]["properties"], [])
|
||||
self.assertEqual(created_ff.filters["aggregation_group_type_index"], 0)
|
||||
|
||||
# Now remove group type index
|
||||
response = self.client.patch(
|
||||
f"/api/projects/{self.team.id}/experiments/{id}",
|
||||
{
|
||||
"description": "Bazinga",
|
||||
"filters": {
|
||||
"events": [{"order": 0, "id": "$pageview"}, {"order": 1, "id": "$pageleave"}],
|
||||
"properties": [
|
||||
{"key": "$geoip_country_name", "type": "person", "value": ["france"], "operator": "exact"}
|
||||
],
|
||||
# "aggregation_group_type_index": None, # removed key
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
experiment = Experiment.objects.get(pk=id)
|
||||
self.assertEqual(experiment.description, "Bazinga")
|
||||
|
||||
created_ff = FeatureFlag.objects.get(key=ff_key)
|
||||
self.assertEqual(created_ff.key, ff_key)
|
||||
self.assertFalse(created_ff.active)
|
||||
self.assertEqual(created_ff.filters["multivariate"]["variants"][0]["key"], "control")
|
||||
self.assertEqual(created_ff.filters["multivariate"]["variants"][1]["key"], "test")
|
||||
self.assertEqual(created_ff.filters["groups"][0]["properties"][0]["key"], "$geoip_country_name")
|
||||
self.assertEqual(created_ff.filters["aggregation_group_type_index"], None)
|
||||
|
||||
|
||||
class ClickhouseTestFunnelExperimentResults(ClickhouseTestMixin, APILicensedTest):
|
||||
@snapshot_clickhouse_queries
|
||||
|
@ -19,6 +19,15 @@
|
||||
}
|
||||
}
|
||||
|
||||
.person-selection {
|
||||
width: 100%;
|
||||
border-top: 1px solid $border;
|
||||
padding-top: $default_spacing;
|
||||
margin-top: $default_spacing;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.insights-graph-container {
|
||||
margin-bottom: $default_spacing;
|
||||
background-color: #fff;
|
||||
|
@ -90,6 +90,9 @@ export function Experiment_(): JSX.Element {
|
||||
getIndexForVariant,
|
||||
significanceBannerDetails,
|
||||
areTrendResultsConfusing,
|
||||
taxonomicGroupTypesForSelection,
|
||||
groupTypes,
|
||||
aggregationLabel,
|
||||
} = useValues(experimentLogic)
|
||||
const {
|
||||
setNewExperimentData,
|
||||
@ -336,7 +339,61 @@ export function Experiment_(): JSX.Element {
|
||||
</Col>
|
||||
</Col>
|
||||
)}
|
||||
<Form.Item label="Select participants" name="person-selection">
|
||||
<Row className="person-selection">
|
||||
<span>
|
||||
<b>Select Participants</b>
|
||||
</span>
|
||||
<span>
|
||||
<b>Participant Type</b>
|
||||
<Select
|
||||
value={
|
||||
newExperimentData?.filters?.aggregation_group_type_index !=
|
||||
undefined
|
||||
? newExperimentData.filters.aggregation_group_type_index
|
||||
: -1
|
||||
}
|
||||
onChange={(value) => {
|
||||
const groupTypeIndex = value !== -1 ? value : undefined
|
||||
if (
|
||||
groupTypeIndex !=
|
||||
newExperimentData?.filters?.aggregation_group_type_index
|
||||
) {
|
||||
setFilters({
|
||||
properties: [],
|
||||
aggregation_group_type_index: groupTypeIndex,
|
||||
})
|
||||
setNewExperimentData({
|
||||
filters: {
|
||||
aggregation_group_type_index: groupTypeIndex,
|
||||
// :TRICKY: We reset property filters after changing what you're aggregating by.
|
||||
properties: [],
|
||||
},
|
||||
})
|
||||
}
|
||||
}}
|
||||
style={{ marginLeft: 8 }}
|
||||
data-attr="participant-aggregation-filter"
|
||||
dropdownMatchSelectWidth={false}
|
||||
dropdownAlign={{
|
||||
// Align this dropdown by the right-hand-side of button
|
||||
points: ['tr', 'br'],
|
||||
}}
|
||||
>
|
||||
<Select.Option key={-1} value={-1}>
|
||||
Users
|
||||
</Select.Option>
|
||||
{groupTypes.map((groupType) => (
|
||||
<Select.Option
|
||||
key={groupType.group_type_index}
|
||||
value={groupType.group_type_index}
|
||||
>
|
||||
{capitalizeFirstLetter(
|
||||
aggregationLabel(groupType.group_type_index).plural
|
||||
)}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</span>
|
||||
<Col>
|
||||
<div className="text-muted">
|
||||
Select the entities who will participate in this experiment. If no
|
||||
@ -344,7 +401,6 @@ export function Experiment_(): JSX.Element {
|
||||
</div>
|
||||
<div style={{ flex: 3, marginRight: 5 }}>
|
||||
<PropertyFilters
|
||||
endpoint="person"
|
||||
pageKey={'EditFunnel-property'}
|
||||
propertyFilters={
|
||||
experimentInsightType === InsightType.FUNNELS
|
||||
@ -362,16 +418,13 @@ export function Experiment_(): JSX.Element {
|
||||
})
|
||||
}}
|
||||
style={{ margin: '1rem 0 0' }}
|
||||
taxonomicGroupTypes={[
|
||||
TaxonomicFilterGroupType.PersonProperties,
|
||||
TaxonomicFilterGroupType.CohortsWithAllUsers,
|
||||
]}
|
||||
taxonomicGroupTypes={taxonomicGroupTypesForSelection}
|
||||
popoverPlacement="top"
|
||||
taxonomicPopoverPlacement="auto"
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
</Form.Item>
|
||||
</Row>
|
||||
<Row className="metrics-selection">
|
||||
<Col style={{ paddingRight: 8 }}>
|
||||
<div className="mb-05">
|
||||
@ -431,6 +484,7 @@ export function Experiment_(): JSX.Element {
|
||||
hideMathSelector={true}
|
||||
hideDeleteBtn={filterSteps.length === 1}
|
||||
buttonCopy="Add funnel step"
|
||||
buttonType="link"
|
||||
showSeriesIndicator={!isStepsEmpty}
|
||||
seriesIndicatorType="numeric"
|
||||
fullWidth
|
||||
@ -880,6 +934,7 @@ export function ExperimentPreview({
|
||||
editingExistingExperiment,
|
||||
minimumDetectableChange,
|
||||
expectedRunningTime,
|
||||
aggregationLabel,
|
||||
} = useValues(experimentLogic)
|
||||
const { setNewExperimentData } = useActions(experimentLogic)
|
||||
const [currentVariant, setCurrentVariant] = useState('control')
|
||||
@ -1047,7 +1102,15 @@ export function ExperimentPreview({
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
'100% of users'
|
||||
<>
|
||||
100% of{' '}
|
||||
{experiment?.filters?.aggregation_group_type_index != undefined
|
||||
? capitalizeFirstLetter(
|
||||
aggregationLabel(experiment.filters.aggregation_group_type_index)
|
||||
.plural
|
||||
)
|
||||
: 'users'}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Col>
|
||||
|
@ -35,13 +35,22 @@ import { eventUsageLogic } from 'lib/utils/eventUsageLogic'
|
||||
import { userLogic } from 'scenes/userLogic'
|
||||
import { Tooltip } from 'lib/components/Tooltip'
|
||||
import { InfoCircleOutlined } from '@ant-design/icons'
|
||||
import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types'
|
||||
import { groupsModel } from '~/models/groupsModel'
|
||||
|
||||
const DEFAULT_DURATION = 14 // days
|
||||
|
||||
export const experimentLogic = kea<experimentLogicType>({
|
||||
path: ['scenes', 'experiment', 'experimentLogic'],
|
||||
connect: {
|
||||
values: [teamLogic, ['currentTeamId'], userLogic, ['hasAvailableFeature']],
|
||||
values: [
|
||||
teamLogic,
|
||||
['currentTeamId'],
|
||||
userLogic,
|
||||
['hasAvailableFeature'],
|
||||
groupsModel,
|
||||
['groupTypes', 'groupsTaxonomicTypes', 'aggregationLabel'],
|
||||
],
|
||||
actions: [experimentsLogic, ['updateExperiments', 'addToExperiments']],
|
||||
},
|
||||
actions: {
|
||||
@ -415,6 +424,19 @@ export const experimentLogic = kea<experimentLogicType>({
|
||||
)
|
||||
},
|
||||
],
|
||||
taxonomicGroupTypesForSelection: [
|
||||
(s) => [s.newExperimentData, s.groupsTaxonomicTypes],
|
||||
(newExperimentData, groupsTaxonomicTypes): TaxonomicFilterGroupType[] => {
|
||||
if (
|
||||
newExperimentData?.filters?.aggregation_group_type_index != null &&
|
||||
groupsTaxonomicTypes.length > 0
|
||||
) {
|
||||
return [groupsTaxonomicTypes[newExperimentData.filters.aggregation_group_type_index]]
|
||||
}
|
||||
|
||||
return [TaxonomicFilterGroupType.PersonProperties, TaxonomicFilterGroupType.Cohorts]
|
||||
},
|
||||
],
|
||||
parsedSecondaryMetrics: [
|
||||
(s) => [s.newExperimentData, s.experimentData],
|
||||
(newExperimentData: Partial<Experiment>, experimentData: Experiment): SecondaryExperimentMetric[] => {
|
||||
|
Loading…
Reference in New Issue
Block a user