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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 73269e57e35a55b2ff07748aa4cab6da17c37b42d64a170528cb5dd1955a9365
4
- data.tar.gz: 831672593c8646bb05fae81c527a0e64016d3fbae42e722a6525988ce3257b73
3
+ metadata.gz: 41c94649948ce4d629d17ccc7f5ed6649c7f9ed6e69ec491ca48bc638962cba4
4
+ data.tar.gz: 9a152790208aed756496e031faa0c2bf5a8dfba9bc8f74a1ac3510830132ac8a
5
5
  SHA512:
6
- metadata.gz: 6805d61de4837441d2b4a333f76e093cac6668e2cbf9dba88fa1602f02ce7fedd28ab5cf433217bc1d40127bcf73e2f7ccebeaeb8086a98ec45a5c26b139b030
7
- data.tar.gz: ec9ad7127fe2d0b6d78e1c6f9f28e4ccaf3c99362edc8f20e1b8f06afe56cbc84538bc9bcbc4acbce4d81a546e6046acddfb1cc0ac5ba5cba5f52b04446d467b
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, false, true)
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
- matches = evaluate_property_match(prop, property_values, cohort_properties)
530
- next if matches.nil? # Skip flag dependencies
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
- def self.evaluate_property_match(prop, property_values, cohort_properties)
553
- prop_type = extract_value(prop, :type)
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
- case prop_type
556
- when 'cohort'
557
- match_cohort(prop, property_values, cohort_properties)
558
- when 'flag'
559
- PostHog::Logging.logger&.warn(
560
- '[FEATURE FLAGS] Flag dependency filters are not supported in local evaluation. ' \
561
- "Skipping condition with dependency on flag '#{prop[:key]}'"
562
- )
563
- nil # Signal to skip this property
564
- else
565
- match_property(prop, property_values, cohort_properties)
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, :evaluate_property_match
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, @cohorts)
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 is_condition_match(flag, distinct_id, condition, properties, cohort_properties)
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
- # TODO: Rename to `condition_match?` in future version
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
- if !condition[:properties].all? do |prop|
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
- logger.warn(
671
- '[FEATURE FLAGS] Flag dependency filters are not supported in local evaluation. ' \
672
- "Skipping condition for flag '#{flag[:key]}' with dependency on flag '#{prop[:key]}'"
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))
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PostHog
4
- VERSION = '3.1.2'
4
+ VERSION = '3.2.0'
5
5
  end
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.1.2
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-13 00:00:00.000000000 Z
10
+ date: 2025-08-26 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: concurrent-ruby