Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/versioned-property-matching.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"posthog-go": patch
Comment thread
marandaneto marked this conversation as resolved.
---

Honor the definitions snapshot's `property_matching_version` during local flag evaluation: version 2 uses explicit boolean equality, while missing/1 and unknown versions retain legacy matching. Keep definitions and matching semantics together across reloads, cached 304 responses, group/cohort rules, and flag dependencies. Also preserve JSON-decoded dependency chains in cohort leaves, while requiring server evaluation for dependencies that need a different aggregation context. Same-group dependencies reuse the caller’s group key and properties for local evaluation.
23 changes: 13 additions & 10 deletions feature_flag_property_group.go
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
package posthog

func (poller *FeatureFlagsPoller) matchCohort(property FlagProperty, properties Properties, cohorts map[string]PropertyGroup, flagsByKey map[string]FeatureFlag, evaluationCache map[string]interface{}, distinctId string, deviceId *string) (bool, error) {
func (poller *FeatureFlagsPoller) matchCohort(property FlagProperty, properties Properties, cohorts map[string]PropertyGroup, flagsByKey map[string]FeatureFlag, evaluationCache map[string]interface{}, distinctId string, deviceId *string, aggregationGroupTypeIndex *uint8, snapshots ...*flagsState) (bool, error) {
state := poller.evaluationState(snapshots)
cohortId := valueToString(property.Value)
propertyGroup, ok := cohorts[cohortId]
if !ok {
return false, errCohortRequiresServerEval
}

return poller.matchPropertyGroup(propertyGroup, properties, cohorts, flagsByKey, evaluationCache, distinctId, deviceId)
return poller.matchPropertyGroup(propertyGroup, properties, cohorts, flagsByKey, evaluationCache, distinctId, deviceId, aggregationGroupTypeIndex, state)
}

func (poller *FeatureFlagsPoller) matchPropertyGroup(propertyGroup PropertyGroup, properties Properties, cohorts map[string]PropertyGroup, flagsByKey map[string]FeatureFlag, evaluationCache map[string]interface{}, distinctId string, deviceId *string) (bool, error) {
func (poller *FeatureFlagsPoller) matchPropertyGroup(propertyGroup PropertyGroup, properties Properties, cohorts map[string]PropertyGroup, flagsByKey map[string]FeatureFlag, evaluationCache map[string]interface{}, distinctId string, deviceId *string, aggregationGroupTypeIndex *uint8, snapshots ...*flagsState) (bool, error) {
state := poller.evaluationState(snapshots)
groupType := propertyGroup.Type

// Use pre-parsed values if available (built at load time), otherwise fall back to raw values
if len(propertyGroup.ParsedValues) > 0 {
return poller.matchParsedPropertyGroup(groupType, propertyGroup.ParsedValues, properties, cohorts, flagsByKey, evaluationCache, distinctId, deviceId)
return poller.matchParsedPropertyGroup(groupType, propertyGroup.ParsedValues, properties, cohorts, flagsByKey, evaluationCache, distinctId, deviceId, aggregationGroupTypeIndex, state)
}

if len(propertyGroup.Values) == 0 {
Expand All @@ -26,18 +28,19 @@ func (poller *FeatureFlagsPoller) matchPropertyGroup(propertyGroup PropertyGroup
// Raw values are a compatibility fallback. Convert them to the same typed
// representation used by production cohorts so evaluation has one code path.
parsedGroup := preParsePG(propertyGroup)
return poller.matchParsedPropertyGroup(groupType, parsedGroup.ParsedValues, properties, cohorts, flagsByKey, evaluationCache, distinctId, deviceId)
return poller.matchParsedPropertyGroup(groupType, parsedGroup.ParsedValues, properties, cohorts, flagsByKey, evaluationCache, distinctId, deviceId, aggregationGroupTypeIndex, state)
}

// matchParsedPropertyGroup evaluates pre-parsed property values without per-evaluation
// reconstruction from map[string]any. This is the fast path for cohort matching.
func (poller *FeatureFlagsPoller) matchParsedPropertyGroup(groupType string, parsedValues []parsedPropertyValue, properties Properties, cohorts map[string]PropertyGroup, flagsByKey map[string]FeatureFlag, evaluationCache map[string]interface{}, distinctId string, deviceId *string) (bool, error) {
func (poller *FeatureFlagsPoller) matchParsedPropertyGroup(groupType string, parsedValues []parsedPropertyValue, properties Properties, cohorts map[string]PropertyGroup, flagsByKey map[string]FeatureFlag, evaluationCache map[string]interface{}, distinctId string, deviceId *string, aggregationGroupTypeIndex *uint8, snapshots ...*flagsState) (bool, error) {
state := poller.evaluationState(snapshots)
errorMatchingLocally := false

for i := range parsedValues {
pv := &parsedValues[i]
if pv.IsGroup {
matches, err := poller.matchPropertyGroup(pv.Group, properties, cohorts, flagsByKey, evaluationCache, distinctId, deviceId)
matches, err := poller.matchPropertyGroup(pv.Group, properties, cohorts, flagsByKey, evaluationCache, distinctId, deviceId, aggregationGroupTypeIndex, state)
if err != nil {
if isServerEvalError(err) {
return false, err
Expand All @@ -62,11 +65,11 @@ func (poller *FeatureFlagsPoller) matchParsedPropertyGroup(groupType string, par
var err error
fp := &pv.Property
if fp.Type == "cohort" {
matches, err = poller.matchCohort(*fp, properties, cohorts, flagsByKey, evaluationCache, distinctId, deviceId)
matches, err = poller.matchCohort(*fp, properties, cohorts, flagsByKey, evaluationCache, distinctId, deviceId, aggregationGroupTypeIndex, state)
} else if fp.Type == "flag" {
matches, err = poller.evaluateFlagDependency(*fp, flagsByKey, evaluationCache, distinctId, deviceId, properties, cohorts)
matches, err = poller.evaluateFlagDependency(*fp, flagsByKey, evaluationCache, distinctId, deviceId, properties, cohorts, aggregationGroupTypeIndex, state)
} else {
matches, err = matchProperty(*fp, properties)
matches, err = matchProperty(*fp, properties, state.propertyMatchingVersion)
}

if err != nil {
Expand Down
4 changes: 2 additions & 2 deletions feature_flags_property_group_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,13 +72,13 @@ func TestMatchPropertyGroupRawAndParsedParity(t *testing.T) {
poller := &FeatureFlagsPoller{}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
rawResult, rawErr := poller.matchPropertyGroup(tt.group, tt.properties, nil, nil, nil, "distinct-id", nil)
rawResult, rawErr := poller.matchPropertyGroup(tt.group, tt.properties, nil, nil, nil, "distinct-id", nil, nil)
require.Equal(t, tt.want, rawResult)
require.Equal(t, tt.wantErr, rawErr)

parsedGroup := preParsePG(tt.group)
require.NotEmpty(t, parsedGroup.ParsedValues)
parsedResult, parsedErr := poller.matchPropertyGroup(parsedGroup, tt.properties, nil, nil, nil, "distinct-id", nil)
parsedResult, parsedErr := poller.matchPropertyGroup(parsedGroup, tt.properties, nil, nil, nil, "distinct-id", nil, nil)
require.Equal(t, rawResult, parsedResult)
require.Equal(t, rawErr, parsedErr)
})
Expand Down
Loading
Loading