posthog-ruby 3.1.1 → 3.1.2
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/bin/fmt +14 -0
- data/bin/test +3 -1
- data/lib/posthog/feature_flags.rb +139 -10
- data/lib/posthog/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 73269e57e35a55b2ff07748aa4cab6da17c37b42d64a170528cb5dd1955a9365
|
4
|
+
data.tar.gz: 831672593c8646bb05fae81c527a0e64016d3fbae42e722a6525988ce3257b73
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6805d61de4837441d2b4a333f76e093cac6668e2cbf9dba88fa1602f02ce7fedd28ab5cf433217bc1d40127bcf73e2f7ccebeaeb8086a98ec45a5c26b139b030
|
7
|
+
data.tar.gz: ec9ad7127fe2d0b6d78e1c6f9f28e4ccaf3c99362edc8f20e1b8f06afe56cbc84538bc9bcbc4acbce4d81a546e6046acddfb1cc0ac5ba5cba5f52b04446d467b
|
data/bin/fmt
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env bash
|
2
|
+
#/ Usage: bin/fmt [--check]
|
3
|
+
#/ Description: Format code using rubocop. Use --check to only check formatting without fixing.
|
4
|
+
source bin/helpers/_utils.sh
|
5
|
+
set_source_and_root_dir
|
6
|
+
|
7
|
+
# Check if --check flag is provided
|
8
|
+
if [[ "$1" == "--check" ]]; then
|
9
|
+
echo "Checking code formatting..."
|
10
|
+
rubocop
|
11
|
+
else
|
12
|
+
echo "Formatting code with rubocop..."
|
13
|
+
rubocop --autocorrect-all
|
14
|
+
fi
|
data/bin/test
CHANGED
@@ -30,6 +30,7 @@ module PostHog
|
|
30
30
|
@host = host
|
31
31
|
@feature_flags = Concurrent::Array.new
|
32
32
|
@group_type_mapping = Concurrent::Hash.new
|
33
|
+
@cohorts = Concurrent::Hash.new
|
33
34
|
@loaded_flags_successfully_once = Concurrent::AtomicBoolean.new
|
34
35
|
@feature_flags_by_key = nil
|
35
36
|
@feature_flag_request_timeout_seconds = feature_flag_request_timeout_seconds
|
@@ -367,13 +368,16 @@ module PostHog
|
|
367
368
|
parsed_dt
|
368
369
|
end
|
369
370
|
|
370
|
-
def self.match_property(property, property_values)
|
371
|
+
def self.match_property(property, property_values, cohort_properties = {})
|
371
372
|
# only looks for matches where key exists in property_values
|
372
373
|
# doesn't support operator is_not_set
|
373
374
|
|
374
375
|
PostHog::Utils.symbolize_keys! property
|
375
376
|
PostHog::Utils.symbolize_keys! property_values
|
376
377
|
|
378
|
+
# Handle cohort properties
|
379
|
+
return match_cohort(property, property_values, cohort_properties) if extract_value(property, :type) == 'cohort'
|
380
|
+
|
377
381
|
key = property[:key].to_sym
|
378
382
|
value = property[:value]
|
379
383
|
operator = property[:operator] || 'exact'
|
@@ -443,7 +447,127 @@ module PostHog
|
|
443
447
|
end
|
444
448
|
end
|
445
449
|
|
446
|
-
|
450
|
+
def self.match_cohort(property, property_values, cohort_properties)
|
451
|
+
# Cohort properties are in the form of property groups like this:
|
452
|
+
# {
|
453
|
+
# "cohort_id" => {
|
454
|
+
# "type" => "AND|OR",
|
455
|
+
# "values" => [{
|
456
|
+
# "key" => "property_name", "value" => "property_value"
|
457
|
+
# }]
|
458
|
+
# }
|
459
|
+
# }
|
460
|
+
cohort_id = extract_value(property, :value).to_s
|
461
|
+
property_group = find_cohort_property(cohort_properties, cohort_id)
|
462
|
+
|
463
|
+
raise InconclusiveMatchError, "can't match cohort without a given cohort property value" unless property_group
|
464
|
+
|
465
|
+
match_property_group(property_group, property_values, cohort_properties)
|
466
|
+
end
|
467
|
+
|
468
|
+
def self.match_property_group(property_group, property_values, cohort_properties)
|
469
|
+
return true if property_group.nil? || property_group.empty?
|
470
|
+
|
471
|
+
group_type = extract_value(property_group, :type)
|
472
|
+
properties = extract_value(property_group, :values)
|
473
|
+
|
474
|
+
return true if properties.nil? || properties.empty?
|
475
|
+
|
476
|
+
if nested_property_group?(properties)
|
477
|
+
match_nested_property_group(properties, group_type, property_values, cohort_properties)
|
478
|
+
else
|
479
|
+
match_regular_property_group(properties, group_type, property_values, cohort_properties)
|
480
|
+
end
|
481
|
+
end
|
482
|
+
|
483
|
+
def self.extract_value(hash, key)
|
484
|
+
return nil unless hash.is_a?(Hash)
|
485
|
+
|
486
|
+
hash[key] || hash[key.to_s]
|
487
|
+
end
|
488
|
+
|
489
|
+
def self.find_cohort_property(cohort_properties, cohort_id)
|
490
|
+
return nil unless cohort_properties.is_a?(Hash)
|
491
|
+
|
492
|
+
cohort_properties[cohort_id] || cohort_properties[cohort_id.to_sym]
|
493
|
+
end
|
494
|
+
|
495
|
+
def self.nested_property_group?(properties)
|
496
|
+
return false unless properties&.any?
|
497
|
+
|
498
|
+
first_property = properties[0]
|
499
|
+
return false unless first_property.is_a?(Hash)
|
500
|
+
|
501
|
+
first_property.key?(:values) || first_property.key?('values')
|
502
|
+
end
|
503
|
+
|
504
|
+
def self.match_nested_property_group(properties, group_type, property_values, cohort_properties)
|
505
|
+
case group_type
|
506
|
+
when 'AND'
|
507
|
+
properties.each do |property|
|
508
|
+
return false unless match_property_group(property, property_values, cohort_properties)
|
509
|
+
end
|
510
|
+
true
|
511
|
+
when 'OR'
|
512
|
+
properties.each do |property|
|
513
|
+
return true if match_property_group(property, property_values, cohort_properties)
|
514
|
+
end
|
515
|
+
false
|
516
|
+
else
|
517
|
+
raise InconclusiveMatchError, "Unknown property group type: #{group_type}"
|
518
|
+
end
|
519
|
+
end
|
520
|
+
|
521
|
+
def self.match_regular_property_group(properties, group_type, property_values, cohort_properties)
|
522
|
+
# Validate group type upfront
|
523
|
+
raise InconclusiveMatchError, "Unknown property group type: #{group_type}" unless %w[AND OR].include?(group_type)
|
524
|
+
|
525
|
+
error_matching_locally = false
|
526
|
+
|
527
|
+
properties.each do |prop|
|
528
|
+
PostHog::Utils.symbolize_keys!(prop)
|
529
|
+
matches = evaluate_property_match(prop, property_values, cohort_properties)
|
530
|
+
next if matches.nil? # Skip flag dependencies
|
531
|
+
|
532
|
+
negated = prop[:negation] || false
|
533
|
+
final_result = negated ? !matches : matches
|
534
|
+
|
535
|
+
# Short-circuit based on group type
|
536
|
+
if group_type == 'AND'
|
537
|
+
return false unless final_result
|
538
|
+
elsif final_result # group_type == 'OR'
|
539
|
+
return true
|
540
|
+
end
|
541
|
+
rescue InconclusiveMatchError => e
|
542
|
+
PostHog::Logging.logger&.debug("Failed to compute property #{prop} locally: #{e}")
|
543
|
+
error_matching_locally = true
|
544
|
+
end
|
545
|
+
|
546
|
+
raise InconclusiveMatchError, "can't match cohort without a given cohort property value" if error_matching_locally
|
547
|
+
|
548
|
+
# If we reach here, return default based on group type
|
549
|
+
group_type == 'AND'
|
550
|
+
end
|
551
|
+
|
552
|
+
def self.evaluate_property_match(prop, property_values, cohort_properties)
|
553
|
+
prop_type = extract_value(prop, :type)
|
554
|
+
|
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)
|
566
|
+
end
|
567
|
+
end
|
568
|
+
|
569
|
+
private_class_method :extract_value, :find_cohort_property, :nested_property_group?,
|
570
|
+
:match_nested_property_group, :match_regular_property_group, :evaluate_property_match
|
447
571
|
|
448
572
|
def _compute_flag_locally(flag, distinct_id, groups = {}, person_properties = {}, group_properties = {})
|
449
573
|
raise InconclusiveMatchError, 'Flag has experience continuity enabled' if flag[:ensure_experience_continuity]
|
@@ -453,7 +577,10 @@ module PostHog
|
|
453
577
|
flag_filters = flag[:filters] || {}
|
454
578
|
|
455
579
|
aggregation_group_type_index = flag_filters[:aggregation_group_type_index]
|
456
|
-
|
580
|
+
if aggregation_group_type_index.nil?
|
581
|
+
return match_feature_flag_properties(flag, distinct_id, person_properties,
|
582
|
+
@cohorts)
|
583
|
+
end
|
457
584
|
|
458
585
|
group_name = @group_type_mapping[aggregation_group_type_index.to_s.to_sym]
|
459
586
|
|
@@ -475,7 +602,7 @@ module PostHog
|
|
475
602
|
end
|
476
603
|
|
477
604
|
focused_group_properties = group_properties[group_name_symbol]
|
478
|
-
match_feature_flag_properties(flag, groups[group_name_symbol], focused_group_properties)
|
605
|
+
match_feature_flag_properties(flag, groups[group_name_symbol], focused_group_properties, @cohorts)
|
479
606
|
end
|
480
607
|
|
481
608
|
def _compute_flag_payload_locally(key, match_value)
|
@@ -490,7 +617,7 @@ module PostHog
|
|
490
617
|
response
|
491
618
|
end
|
492
619
|
|
493
|
-
def match_feature_flag_properties(flag, distinct_id, properties)
|
620
|
+
def match_feature_flag_properties(flag, distinct_id, properties, cohort_properties = {})
|
494
621
|
flag_filters = flag[:filters] || {}
|
495
622
|
|
496
623
|
flag_conditions = flag_filters[:groups] || []
|
@@ -506,7 +633,7 @@ module PostHog
|
|
506
633
|
# NOTE: This NEEDS to be `each` because `each_key` breaks
|
507
634
|
# This is not a hash, it's just an array with 2 entries
|
508
635
|
sorted_flag_conditions.each do |condition, _idx| # rubocop:disable Style/HashEachMethods
|
509
|
-
if is_condition_match(flag, distinct_id, condition, properties)
|
636
|
+
if is_condition_match(flag, distinct_id, condition, properties, cohort_properties)
|
510
637
|
variant_override = condition[:variant]
|
511
638
|
flag_multivariate = flag_filters[:multivariate] || {}
|
512
639
|
flag_variants = flag_multivariate[:variants] || []
|
@@ -533,7 +660,7 @@ module PostHog
|
|
533
660
|
end
|
534
661
|
|
535
662
|
# TODO: Rename to `condition_match?` in future version
|
536
|
-
def is_condition_match(flag, distinct_id, condition, properties) # rubocop:disable Naming/PredicateName
|
663
|
+
def is_condition_match(flag, distinct_id, condition, properties, cohort_properties = {}) # rubocop:disable Naming/PredicateName
|
537
664
|
rollout_percentage = condition[:rollout_percentage]
|
538
665
|
|
539
666
|
unless (condition[:properties] || []).empty?
|
@@ -546,7 +673,7 @@ module PostHog
|
|
546
673
|
)
|
547
674
|
next true
|
548
675
|
end
|
549
|
-
FeatureFlagsPoller.match_property(prop, properties)
|
676
|
+
FeatureFlagsPoller.match_property(prop, properties, cohort_properties)
|
550
677
|
end
|
551
678
|
return false
|
552
679
|
elsif rollout_percentage.nil?
|
@@ -607,6 +734,7 @@ module PostHog
|
|
607
734
|
@feature_flags = Concurrent::Array.new
|
608
735
|
@feature_flags_by_key = {}
|
609
736
|
@group_type_mapping = Concurrent::Hash.new
|
737
|
+
@cohorts = Concurrent::Hash.new
|
610
738
|
@loaded_flags_successfully_once.make_false
|
611
739
|
@quota_limited.make_true
|
612
740
|
return
|
@@ -619,8 +747,9 @@ module PostHog
|
|
619
747
|
@feature_flags_by_key[flag[:key]] = flag unless flag[:key].nil?
|
620
748
|
end
|
621
749
|
@group_type_mapping = res[:group_type_mapping] || {}
|
750
|
+
@cohorts = res[:cohorts] || {}
|
622
751
|
|
623
|
-
logger.debug "Loaded #{@feature_flags.length} feature flags"
|
752
|
+
logger.debug "Loaded #{@feature_flags.length} feature flags and #{@cohorts.length} cohorts"
|
624
753
|
@loaded_flags_successfully_once.make_true if @loaded_flags_successfully_once.false?
|
625
754
|
else
|
626
755
|
logger.debug "Failed to load feature flags: #{res}"
|
@@ -629,7 +758,7 @@ module PostHog
|
|
629
758
|
|
630
759
|
def _request_feature_flag_definitions
|
631
760
|
uri = URI("#{@host}/api/feature_flag/local_evaluation")
|
632
|
-
uri.query = URI.encode_www_form([['token', @project_api_key]])
|
761
|
+
uri.query = URI.encode_www_form([['token', @project_api_key], %w[send_cohorts true]])
|
633
762
|
req = Net::HTTP::Get.new(uri)
|
634
763
|
req['Authorization'] = "Bearer #{@personal_api_key}"
|
635
764
|
|
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.1.
|
4
|
+
version: 3.1.2
|
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-13 00:00:00.000000000 Z
|
11
11
|
dependencies:
|
12
12
|
- !ruby/object:Gem::Dependency
|
13
13
|
name: concurrent-ruby
|
@@ -30,6 +30,7 @@ executables:
|
|
30
30
|
extensions: []
|
31
31
|
extra_rdoc_files: []
|
32
32
|
files:
|
33
|
+
- bin/fmt
|
33
34
|
- bin/helpers/_utils.sh
|
34
35
|
- bin/posthog
|
35
36
|
- bin/test
|