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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 47b1ce0c330a250b307dd757fa3024fcd516e8cf264576e52fde101c412d2e76
4
- data.tar.gz: '08725cd78aab55aa87b70ebcf65a2a061b641cbab043a2df61552eff199cc534'
3
+ metadata.gz: 73269e57e35a55b2ff07748aa4cab6da17c37b42d64a170528cb5dd1955a9365
4
+ data.tar.gz: 831672593c8646bb05fae81c527a0e64016d3fbae42e722a6525988ce3257b73
5
5
  SHA512:
6
- metadata.gz: 983808c3419cfab8772b3dbe175070d42dc10a357f1cb657bc36aa56389e867027e4c3ad3286d2d78eb2c232e2448f7190658beafcb846d2e5f72deb3d747d04
7
- data.tar.gz: 92aab3899eb59d728170098b0b6ff62badffbcc890080c67eaf9657399f3de4f3c96d6241942ec1bf64b06bdc53e9dd588845a4c4cbe8b079759cbe51a71b0df
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
@@ -4,4 +4,6 @@
4
4
  source bin/helpers/_utils.sh
5
5
  set_source_and_root_dir
6
6
 
7
- bundle exec rspec
7
+ # Use RUBYLIB to avoid bundler dependency issues
8
+ # This allows tests to run even if bundler can't resolve all dev dependencies
9
+ RUBYLIB=lib rspec "$@"
@@ -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
- private
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
- return match_feature_flag_properties(flag, distinct_id, person_properties) if aggregation_group_type_index.nil?
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
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PostHog
4
- VERSION = '3.1.1'
4
+ VERSION = '3.1.2'
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.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-07 00:00:00.000000000 Z
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