posthog-ruby 3.1.2 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/posthog/feature_flags.rb +138 -34
- data/lib/posthog/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 41c94649948ce4d629d17ccc7f5ed6649c7f9ed6e69ec491ca48bc638962cba4
|
4
|
+
data.tar.gz: 9a152790208aed756496e031faa0c2bf5a8dfba9bc8f74a1ac3510830132ac8a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b50f6538f3e285387f33a68a4c792c615f95d77826d333481637c7d3321a5490ce0be5f4bdf2ccb4ea914ae9ee5302dc488c9b3bf24f057a6c7d9c72799142d1
|
7
|
+
data.tar.gz: b34492ba3808627183010ca98d3e77356a873df98b5163854814e60f8225238db51cbafdb9786e9cf83a4295537c365e380094fe39d489bb518e979acd0578d4
|
@@ -186,7 +186,8 @@ module PostHog
|
|
186
186
|
|
187
187
|
if !flag_was_locally_evaluated && !only_evaluate_locally
|
188
188
|
begin
|
189
|
-
flags_data = get_all_flags_and_payloads(distinct_id, groups, person_properties, group_properties,
|
189
|
+
flags_data = get_all_flags_and_payloads(distinct_id, groups, person_properties, group_properties,
|
190
|
+
only_evaluate_locally, true)
|
190
191
|
if flags_data.key?(:featureFlags)
|
191
192
|
flags = stringify_keys(flags_data[:featureFlags] || {})
|
192
193
|
request_id = flags_data[:requestId]
|
@@ -526,8 +527,8 @@ module PostHog
|
|
526
527
|
|
527
528
|
properties.each do |prop|
|
528
529
|
PostHog::Utils.symbolize_keys!(prop)
|
529
|
-
|
530
|
-
|
530
|
+
|
531
|
+
matches = match_property(prop, property_values, cohort_properties)
|
531
532
|
|
532
533
|
negated = prop[:negation] || false
|
533
534
|
final_result = negated ? !matches : matches
|
@@ -549,37 +550,144 @@ module PostHog
|
|
549
550
|
group_type == 'AND'
|
550
551
|
end
|
551
552
|
|
552
|
-
|
553
|
-
|
553
|
+
# Evaluates a flag dependency property according to the dependency chain algorithm.
|
554
|
+
#
|
555
|
+
# @param property [Hash] Flag property with type="flag" and dependency_chain
|
556
|
+
# @param evaluation_cache [Hash] Cache for storing evaluation results
|
557
|
+
# @param distinct_id [String] The distinct ID being evaluated
|
558
|
+
# @param properties [Hash] Person properties for evaluation
|
559
|
+
# @param cohort_properties [Hash] Cohort properties for evaluation
|
560
|
+
# @return [Boolean] True if all dependencies in the chain evaluate to true, false otherwise
|
561
|
+
def evaluate_flag_dependency(property, evaluation_cache, distinct_id, properties, cohort_properties)
|
562
|
+
if property[:operator] != 'flag_evaluates_to'
|
563
|
+
# Should never happen, but just in case
|
564
|
+
raise InconclusiveMatchError, "Operator #{property[:operator]} not supported for flag dependencies"
|
565
|
+
end
|
566
|
+
|
567
|
+
if @feature_flags_by_key.nil? || evaluation_cache.nil?
|
568
|
+
# Cannot evaluate flag dependencies without required context
|
569
|
+
raise InconclusiveMatchError,
|
570
|
+
"Cannot evaluate flag dependency on '#{property[:key] || 'unknown'}' " \
|
571
|
+
'without feature flags loaded or evaluation_cache'
|
572
|
+
end
|
573
|
+
|
574
|
+
# Check if dependency_chain is present - it should always be provided for flag dependencies
|
575
|
+
unless property.key?(:dependency_chain)
|
576
|
+
# Missing dependency_chain indicates malformed server data
|
577
|
+
raise InconclusiveMatchError,
|
578
|
+
"Flag dependency property for '#{property[:key] || 'unknown'}' " \
|
579
|
+
"is missing required 'dependency_chain' field"
|
580
|
+
end
|
581
|
+
|
582
|
+
dependency_chain = property[:dependency_chain]
|
583
|
+
|
584
|
+
# Handle circular dependency (empty chain means circular)
|
585
|
+
if dependency_chain.empty?
|
586
|
+
PostHog::Logging.logger&.debug("Circular dependency detected for flag: #{property[:key]}")
|
587
|
+
raise InconclusiveMatchError,
|
588
|
+
"Circular dependency detected for flag '#{property[:key] || 'unknown'}'"
|
589
|
+
end
|
590
|
+
|
591
|
+
# Evaluate all dependencies in the chain order
|
592
|
+
dependency_chain.each do |dep_flag_key|
|
593
|
+
unless evaluation_cache.key?(dep_flag_key)
|
594
|
+
# Need to evaluate this dependency first
|
595
|
+
dep_flag = @feature_flags_by_key[dep_flag_key]
|
596
|
+
if dep_flag.nil?
|
597
|
+
# Missing flag dependency - cannot evaluate locally
|
598
|
+
evaluation_cache[dep_flag_key] = nil
|
599
|
+
raise InconclusiveMatchError,
|
600
|
+
"Cannot evaluate flag dependency '#{dep_flag_key}' - flag not found in local flags"
|
601
|
+
elsif !dep_flag[:active]
|
602
|
+
# Check if the flag is active (same check as in _compute_flag_locally)
|
603
|
+
evaluation_cache[dep_flag_key] = false
|
604
|
+
else
|
605
|
+
# Recursively evaluate the dependency using existing instance method
|
606
|
+
begin
|
607
|
+
dep_result = match_feature_flag_properties(
|
608
|
+
dep_flag,
|
609
|
+
distinct_id,
|
610
|
+
properties,
|
611
|
+
evaluation_cache,
|
612
|
+
cohort_properties
|
613
|
+
)
|
614
|
+
evaluation_cache[dep_flag_key] = dep_result
|
615
|
+
rescue InconclusiveMatchError => e
|
616
|
+
# If we can't evaluate a dependency, store nil and propagate the error
|
617
|
+
evaluation_cache[dep_flag_key] = nil
|
618
|
+
raise InconclusiveMatchError,
|
619
|
+
"Cannot evaluate flag dependency '#{dep_flag_key}': #{e.message}"
|
620
|
+
end
|
621
|
+
end
|
622
|
+
end
|
554
623
|
|
555
|
-
|
556
|
-
|
557
|
-
|
558
|
-
|
559
|
-
|
560
|
-
|
561
|
-
|
562
|
-
|
563
|
-
|
564
|
-
|
565
|
-
|
624
|
+
# Check the cached result
|
625
|
+
cached_result = evaluation_cache[dep_flag_key]
|
626
|
+
if cached_result.nil?
|
627
|
+
# Previously inconclusive - raise error again
|
628
|
+
raise InconclusiveMatchError,
|
629
|
+
"Flag dependency '#{dep_flag_key}' was previously inconclusive"
|
630
|
+
elsif !cached_result
|
631
|
+
# Definitive False result - dependency failed
|
632
|
+
return false
|
633
|
+
end
|
634
|
+
end
|
635
|
+
|
636
|
+
# Get the expected value of the immediate dependency and the actual value
|
637
|
+
expected_value = property[:value]
|
638
|
+
# The flag we want to evaluate is defined by :key which should ALSO be the last key in the dependency chain
|
639
|
+
actual_value = evaluation_cache[property[:key]]
|
640
|
+
|
641
|
+
self.class.matches_dependency_value(expected_value, actual_value)
|
642
|
+
end
|
643
|
+
|
644
|
+
def self.matches_dependency_value(expected_value, actual_value)
|
645
|
+
# Check if the actual flag value matches the expected dependency value.
|
646
|
+
#
|
647
|
+
# - String variant case: check for exact match or boolean true
|
648
|
+
# - Boolean case: must match expected boolean value
|
649
|
+
#
|
650
|
+
# @param expected_value [Object] The expected value from the property
|
651
|
+
# @param actual_value [Object] The actual value returned by the flag evaluation
|
652
|
+
# @return [Boolean] True if the values match according to flag dependency rules
|
653
|
+
|
654
|
+
# String variant case - check for exact match or boolean true
|
655
|
+
if actual_value.is_a?(String) && !actual_value.empty?
|
656
|
+
if expected_value.is_a?(TrueClass) || expected_value.is_a?(FalseClass)
|
657
|
+
# Any variant matches boolean true
|
658
|
+
return expected_value
|
659
|
+
elsif expected_value.is_a?(String)
|
660
|
+
# variants are case-sensitive, hence our comparison is too
|
661
|
+
return actual_value == expected_value
|
662
|
+
else
|
663
|
+
return false
|
664
|
+
end
|
665
|
+
|
666
|
+
# Boolean case - must match expected boolean value
|
667
|
+
elsif actual_value.is_a?(TrueClass) || actual_value.is_a?(FalseClass)
|
668
|
+
return actual_value == expected_value if expected_value.is_a?(TrueClass) || expected_value.is_a?(FalseClass)
|
566
669
|
end
|
670
|
+
|
671
|
+
# Default case
|
672
|
+
false
|
567
673
|
end
|
568
674
|
|
569
675
|
private_class_method :extract_value, :find_cohort_property, :nested_property_group?,
|
570
|
-
:match_nested_property_group, :match_regular_property_group
|
676
|
+
:match_nested_property_group, :match_regular_property_group
|
571
677
|
|
572
678
|
def _compute_flag_locally(flag, distinct_id, groups = {}, person_properties = {}, group_properties = {})
|
573
679
|
raise InconclusiveMatchError, 'Flag has experience continuity enabled' if flag[:ensure_experience_continuity]
|
574
680
|
|
575
681
|
return false unless flag[:active]
|
576
682
|
|
683
|
+
# Create evaluation cache for flag dependencies
|
684
|
+
evaluation_cache = {}
|
685
|
+
|
577
686
|
flag_filters = flag[:filters] || {}
|
578
687
|
|
579
688
|
aggregation_group_type_index = flag_filters[:aggregation_group_type_index]
|
580
689
|
if aggregation_group_type_index.nil?
|
581
|
-
return match_feature_flag_properties(flag, distinct_id, person_properties,
|
582
|
-
@cohorts)
|
690
|
+
return match_feature_flag_properties(flag, distinct_id, person_properties, evaluation_cache, @cohorts)
|
583
691
|
end
|
584
692
|
|
585
693
|
group_name = @group_type_mapping[aggregation_group_type_index.to_s.to_sym]
|
@@ -602,7 +710,8 @@ module PostHog
|
|
602
710
|
end
|
603
711
|
|
604
712
|
focused_group_properties = group_properties[group_name_symbol]
|
605
|
-
match_feature_flag_properties(flag, groups[group_name_symbol], focused_group_properties,
|
713
|
+
match_feature_flag_properties(flag, groups[group_name_symbol], focused_group_properties, evaluation_cache,
|
714
|
+
@cohorts)
|
606
715
|
end
|
607
716
|
|
608
717
|
def _compute_flag_payload_locally(key, match_value)
|
@@ -617,7 +726,7 @@ module PostHog
|
|
617
726
|
response
|
618
727
|
end
|
619
728
|
|
620
|
-
def match_feature_flag_properties(flag, distinct_id, properties, cohort_properties = {})
|
729
|
+
def match_feature_flag_properties(flag, distinct_id, properties, evaluation_cache, cohort_properties = {})
|
621
730
|
flag_filters = flag[:filters] || {}
|
622
731
|
|
623
732
|
flag_conditions = flag_filters[:groups] || []
|
@@ -633,7 +742,7 @@ module PostHog
|
|
633
742
|
# NOTE: This NEEDS to be `each` because `each_key` breaks
|
634
743
|
# This is not a hash, it's just an array with 2 entries
|
635
744
|
sorted_flag_conditions.each do |condition, _idx| # rubocop:disable Style/HashEachMethods
|
636
|
-
if
|
745
|
+
if condition_match(flag, distinct_id, condition, properties, evaluation_cache, cohort_properties)
|
637
746
|
variant_override = condition[:variant]
|
638
747
|
flag_multivariate = flag_filters[:multivariate] || {}
|
639
748
|
flag_variants = flag_multivariate[:variants] || []
|
@@ -659,26 +768,21 @@ module PostHog
|
|
659
768
|
false
|
660
769
|
end
|
661
770
|
|
662
|
-
|
663
|
-
def is_condition_match(flag, distinct_id, condition, properties, cohort_properties = {}) # rubocop:disable Naming/PredicateName
|
771
|
+
def condition_match(flag, distinct_id, condition, properties, evaluation_cache, cohort_properties = {})
|
664
772
|
rollout_percentage = condition[:rollout_percentage]
|
665
773
|
|
666
774
|
unless (condition[:properties] || []).empty?
|
667
|
-
|
668
|
-
# Skip flag dependencies as they are not supported in local evaluation
|
775
|
+
unless condition[:properties].all? do |prop|
|
669
776
|
if prop[:type] == 'flag'
|
670
|
-
|
671
|
-
|
672
|
-
|
673
|
-
)
|
674
|
-
next true
|
777
|
+
evaluate_flag_dependency(prop, evaluation_cache, distinct_id, properties, cohort_properties)
|
778
|
+
else
|
779
|
+
FeatureFlagsPoller.match_property(prop, properties, cohort_properties)
|
675
780
|
end
|
676
|
-
FeatureFlagsPoller.match_property(prop, properties, cohort_properties)
|
677
781
|
end
|
678
782
|
return false
|
679
|
-
elsif rollout_percentage.nil?
|
680
|
-
return true
|
681
783
|
end
|
784
|
+
|
785
|
+
return true if rollout_percentage.nil?
|
682
786
|
end
|
683
787
|
|
684
788
|
return false if !rollout_percentage.nil? && (_hash(flag[:key], distinct_id) > (rollout_percentage.to_f / 100))
|
data/lib/posthog/version.rb
CHANGED
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: posthog-ruby
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 3.
|
4
|
+
version: 3.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- ''
|
8
8
|
bindir: bin
|
9
9
|
cert_chain: []
|
10
|
-
date: 2025-08-
|
10
|
+
date: 2025-08-26 00:00:00.000000000 Z
|
11
11
|
dependencies:
|
12
12
|
- !ruby/object:Gem::Dependency
|
13
13
|
name: concurrent-ruby
|