diff --git a/src/PostHog/Api/LocalEvaluationApiResult.cs b/src/PostHog/Api/LocalEvaluationApiResult.cs
index edb7225..1575ffe 100644
--- a/src/PostHog/Api/LocalEvaluationApiResult.cs
+++ b/src/PostHog/Api/LocalEvaluationApiResult.cs
@@ -199,6 +199,14 @@ internal record FeatureFlagGroup
[JsonPropertyName("rollout_percentage")]
public int? RolloutPercentage { get; init; } = 100;
+ ///
+ /// Optional per-condition aggregation override used by mixed-targeting flags.
+ /// When set, this condition targets the specified group type instead of the
+ /// flag-level aggregation. Null means person targeting under a mixed flag.
+ ///
+ [JsonPropertyName("aggregation_group_type_index")]
+ public int? AggregationGroupTypeIndex { get; init; }
+
///
/// Compares this instance to another for equality.
///
@@ -219,14 +227,15 @@ public virtual bool Equals(FeatureFlagGroup? other)
return ((Properties is null && other.Properties is null)
|| (Properties is not null && other.Properties is not null && Properties.SequenceEqual(other.Properties)))
&& Variant == other.Variant
- && RolloutPercentage == other.RolloutPercentage;
+ && RolloutPercentage == other.RolloutPercentage
+ && AggregationGroupTypeIndex == other.AggregationGroupTypeIndex;
}
///
/// Serves as the default hash function.
///
/// A hash code for the current object.
- public override int GetHashCode() => HashCode.Combine(Properties, Variant, RolloutPercentage);
+ public override int GetHashCode() => HashCode.Combine(Properties, Variant, RolloutPercentage, AggregationGroupTypeIndex);
}
///
diff --git a/src/PostHog/Features/LocalEvaluator.cs b/src/PostHog/Features/LocalEvaluator.cs
index 60564f6..508b247 100644
--- a/src/PostHog/Features/LocalEvaluator.cs
+++ b/src/PostHog/Features/LocalEvaluator.cs
@@ -262,6 +262,7 @@ StringOrValue MatchFeatureFlagProperties(
{
var filters = flag.Filters;
var flagConditions = filters?.Groups ?? [];
+ var flagAggregation = filters?.AggregationGroupTypeIndex;
var isInconclusive = false;
var flagVariants = filters?.Multivariate?.Variants ?? [];
@@ -269,9 +270,37 @@ StringOrValue MatchFeatureFlagProperties(
{
try
{
+ // Per-condition aggregation overrides only when the condition explicitly
+ // sets its own AggregationGroupTypeIndex (mixed targeting). When absent,
+ // fall back to the flag-level aggregation so existing pure person and
+ // pure group flags keep their original behavior.
+ var conditionAggregation = condition.AggregationGroupTypeIndex ?? flagAggregation;
+
+ var effectiveProperties = properties;
+ var effectiveBucketingId = distinctId;
+
+ // The condition explicitly sets its own aggregation, different from the
+ // flag level. Re-resolve properties and bucketing id from the condition's
+ // group so this condition evaluates against that group.
+ // conditionAggregation is non-null here: if it were null and flagAggregation
+ // were also null they'd be equal, and the only way conditionAggregation can
+ // be null at all (after the ?? above) is that case.
+ if (conditionAggregation != flagAggregation)
+ {
+ if (!_groupTypeMapping.TryGetValue(conditionAggregation!.Value, out var groupType)
+ || groups is null
+ || !groups.TryGetGroup(groupType, out var group))
+ {
+ // Skip this condition: group type unknown or not passed in.
+ continue;
+ }
+ effectiveProperties = group.Properties;
+ effectiveBucketingId = group.GroupKey;
+ }
+
// if any one condition resolves to True, we can short circuit and return
// the matching variant
- if (!IsConditionMatch(flag, distinctId, condition, properties, evaluationCache, groups))
+ if (!IsConditionMatch(flag, effectiveBucketingId, condition, effectiveProperties, evaluationCache, groups))
{
continue;
}
@@ -280,7 +309,7 @@ StringOrValue MatchFeatureFlagProperties(
var variant = variantOverride is not null
&& flagVariants.Select(v => v.Key).Contains(variantOverride)
? variantOverride
- : GetMatchingVariant(flag, distinctId);
+ : GetMatchingVariant(flag, effectiveBucketingId);
return variant is not null
? new StringOrValue(variant)
diff --git a/tests/UnitTests/Features/LocalEvaluatorTests.cs b/tests/UnitTests/Features/LocalEvaluatorTests.cs
index fe594cd..758efce 100644
--- a/tests/UnitTests/Features/LocalEvaluatorTests.cs
+++ b/tests/UnitTests/Features/LocalEvaluatorTests.cs
@@ -2,6 +2,7 @@
using System.Text.Json;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Time.Testing;
+using PostHog;
using PostHog.Api;
using PostHog.Features;
using PostHog.Json;
@@ -748,6 +749,204 @@ public void ThrowsInconclusiveMatchExceptionWhenUnknownOperator()
}
}
+public class TheMixedTargetingEvaluation
+{
+ static LocalEvaluationApiResult CreateMixedFlag()
+ {
+ return new LocalEvaluationApiResult
+ {
+ Flags = [
+ new LocalFeatureFlag
+ {
+ Id = 1,
+ TeamId = 1,
+ Name = "Mixed Flag",
+ Key = "mixed-flag",
+ Active = true,
+ Filters = new FeatureFlagFilters
+ {
+ AggregationGroupTypeIndex = null,
+ Groups = [
+ new FeatureFlagGroup
+ {
+ AggregationGroupTypeIndex = 0,
+ Properties = [
+ new PropertyFilter
+ {
+ Type = FilterType.Group,
+ Key = "plan",
+ Value = new PropertyFilterValue("enterprise"),
+ Operator = ComparisonOperator.Exact,
+ GroupTypeIndex = 0
+ }
+ ],
+ RolloutPercentage = 100
+ },
+ new FeatureFlagGroup
+ {
+ AggregationGroupTypeIndex = null,
+ Properties = [
+ new PropertyFilter
+ {
+ Type = FilterType.Person,
+ Key = "email",
+ Value = new PropertyFilterValue("test@example.com"),
+ Operator = ComparisonOperator.Exact
+ }
+ ],
+ RolloutPercentage = 100
+ }
+ ]
+ }
+ }
+ ],
+ GroupTypeMapping = new Dictionary { { "0", "company" } }
+ };
+ }
+
+ static LocalEvaluationApiResult CreateOnlyGroupFlag()
+ {
+ return new LocalEvaluationApiResult
+ {
+ Flags = [
+ new LocalFeatureFlag
+ {
+ Id = 2,
+ TeamId = 1,
+ Name = "Only Group Flag",
+ Key = "only-group-flag",
+ Active = true,
+ Filters = new FeatureFlagFilters
+ {
+ AggregationGroupTypeIndex = null,
+ Groups = [
+ new FeatureFlagGroup
+ {
+ AggregationGroupTypeIndex = 0,
+ Properties = [
+ new PropertyFilter
+ {
+ Type = FilterType.Group,
+ Key = "plan",
+ Value = new PropertyFilterValue("enterprise"),
+ Operator = ComparisonOperator.Exact,
+ GroupTypeIndex = 0
+ }
+ ],
+ RolloutPercentage = 100
+ }
+ ]
+ }
+ }
+ ],
+ GroupTypeMapping = new Dictionary { { "0", "company" } }
+ };
+ }
+
+ public static IEnumerable