0
0
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:
Neil Kakkar 2022-02-09 15:32:54 +00:00 committed by GitHub
parent 31807bbc87
commit d5a48fbd06
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 211 additions and 11 deletions

View File

@ -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()

View File

@ -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

View File

@ -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;

View File

@ -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>

View File

@ -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[] => {