kameleoon-client-ruby 3.8.0 → 3.10.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: a64d8edfee07c22561b166de49f9185d89a6fbdec9b49f6fd65860125bd54eab
4
- data.tar.gz: ee7ccf78d0d11dec198b930e77e496c39a220b87471d724015c78114b85368f8
3
+ metadata.gz: 1831531ee6a40f0fbd32f110f9a9c9c1a33f71569c8b8de842bc01c06250d6d5
4
+ data.tar.gz: 6e6f8f9d0ee511c2a27fc63ff040e6155b90df66925989d1d08bd473f86c071b
5
5
  SHA512:
6
- metadata.gz: ccfba38b35f50d2b1dda0ef2bf31406f74fd92663276f4297ae867bc7b88b8606b345a8a4234f24ef71c71fdc3555cc9852d7324c1a6a668ed8b6cbc98cfeedb
7
- data.tar.gz: 38543c67b7c8949ccba5c8e0f08eae86d2755a737bc25a5dc82e94dd92011c7678cb4b32647ffae0edb606913bff537455f2f42295fe9f421554726a57675808
6
+ metadata.gz: 6f575eb2b6e12302ffa5cdb067582d994ab890edcab12f174732d396adc4248fc96a9e27935c008db52f59a93b5e0c713530356acab47711727651d2a4d48e3a
7
+ data.tar.gz: '058e6a5284550663cf20223eca099728a9ca0985aacfcfa829e585070228a83b38b4facda77d38a7f3ae17ae8fad37cc7898e47e2519826364a46b3205b33452'
@@ -3,15 +3,16 @@
3
3
  require 'kameleoon/configuration/custom_data_info'
4
4
  require 'kameleoon/configuration/experiment'
5
5
  require 'kameleoon/configuration/feature_flag'
6
+ require 'kameleoon/configuration/me_group'
6
7
  require 'kameleoon/configuration/settings'
7
8
  require 'kameleoon/logging/kameleoon_logger'
8
9
 
9
10
  module Kameleoon
10
11
  module Configuration
11
12
  class DataFile
12
- attr_reader :settings, :feature_flags, :has_any_targeted_delivery_rule, :feature_flag_by_id, :rule_by_segment_id,
13
- :rule_info_by_exp_id, :variation_by_id, :custom_data_info, :experiment_ids_with_js_css_variable,
14
- :holdout
13
+ attr_reader :settings, :feature_flags, :me_groups, :has_any_targeted_delivery_rule, :feature_flag_by_id,
14
+ :rule_by_segment_id, :rule_info_by_exp_id, :variation_by_id, :custom_data_info,
15
+ :experiment_ids_with_js_css_variable, :holdout
15
16
 
16
17
  def to_s
17
18
  'DataFile{' \
@@ -51,6 +52,7 @@ module Kameleoon
51
52
  Logging::KameleoonLogger.debug('CALL: DataFile.init_default')
52
53
  @settings = Settings.new
53
54
  @feature_flags = {}
55
+ @me_groups = {}
54
56
  @has_any_targeted_delivery_rule = false
55
57
  @custom_data_info = Kameleoon::Configuration::CustomDataInfo.new(nil)
56
58
  Logging::KameleoonLogger.debug('RETURN: DataFile.init_default')
@@ -64,6 +66,7 @@ module Kameleoon
64
66
  ff = FeatureFlag.new(raw)
65
67
  @feature_flags[ff.feature_key] = ff
66
68
  end
69
+ @me_groups = make_me_groups(@feature_flags)
67
70
  @has_any_targeted_delivery_rule = any_targeted_delivery_rule?
68
71
  @custom_data_info = CustomDataInfo.new(configuration['customData'])
69
72
  @holdout = Experiment.from_json(configuration['holdout']) if configuration.include?('holdout')
@@ -101,6 +104,21 @@ module Kameleoon
101
104
  @experiment_ids_with_js_css_variable.freeze
102
105
  end
103
106
 
107
+ def make_me_groups(feature_flags)
108
+ me_group_lists = {}
109
+ feature_flags.each_value do |feature_flag|
110
+ next if feature_flag.me_group_name.nil?
111
+
112
+ me_group_list = me_group_lists[feature_flag.me_group_name]
113
+ if me_group_list
114
+ me_group_list.push(feature_flag)
115
+ else
116
+ me_group_lists[feature_flag.me_group_name] = [feature_flag]
117
+ end
118
+ end
119
+ me_group_lists.transform_values { |me_group_list| MEGroup.new(me_group_list) }
120
+ end
121
+
104
122
  def feature_flag_variable_js_css?(feature_flag)
105
123
  feature_flag.variations.first&.variables&.any? do |variable|
106
124
  %w[JS CSS].include?(variable.type)
@@ -8,7 +8,7 @@ module Kameleoon
8
8
  module Configuration
9
9
  # Class for manage all feature flags with rules
10
10
  class FeatureFlag
11
- attr_accessor :id, :feature_key, :variations, :default_variation_key, :environment_enabled, :rules
11
+ attr_accessor :id, :feature_key, :variations, :default_variation_key, :me_group_name, :environment_enabled, :rules
12
12
 
13
13
  def self.create_from_array(array)
14
14
  array&.map { |it| FeatureFlag.new(it) }
@@ -16,10 +16,11 @@ module Kameleoon
16
16
 
17
17
  def to_s
18
18
  'FeatureFlag{' \
19
- "id:#{@id}, " \
20
- "feature_key:#{@feature_key}, " \
21
- "environment_enabled:#{@environment_enabled}, " \
22
- "default_variation_key:#{@default_variation_key}, " \
19
+ "id:#{@id}," \
20
+ "feature_key:'#{@feature_key}'," \
21
+ "environment_enabled:#{@environment_enabled}," \
22
+ "default_variation_key:'#{@default_variation_key}'," \
23
+ "me_group_name:'#{@me_group_name}'," \
23
24
  "rules:#{@rules.size}" \
24
25
  '}'
25
26
  end
@@ -29,6 +30,7 @@ module Kameleoon
29
30
  @feature_key = hash['featureKey']
30
31
  @variations = Variation.create_from_array(hash['variations'])
31
32
  @default_variation_key = hash['defaultVariationKey']
33
+ @me_group_name = hash['mutuallyExclusiveGroup']
32
34
  @environment_enabled = hash['environmentEnabled']
33
35
  @rules = Rule.create_from_array(hash['rules'])
34
36
  end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kameleoon
4
+ module Configuration
5
+ class MEGroup
6
+ attr_reader :feature_flags
7
+
8
+ def initialize(feature_flags)
9
+ @feature_flags = feature_flags.sort { |a, b| a.id <=> b.id }
10
+ end
11
+
12
+ def get_feature_flag_by_hash(hash)
13
+ idx = (hash * @feature_flags.size).to_i
14
+ idx = @feature_flags.size - 1 if idx >= @feature_flags.size
15
+ @feature_flags[idx]
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kameleoon
4
+ class CBScores
5
+ # keys = experiment IDs / values = list of variation IDs ordered descending
6
+ # by score (there may be several variation ids with same score)
7
+ attr_reader :values
8
+
9
+ def initialize(cbs_map)
10
+ values = cbs_map.transform_values { |cbs_value| extract_var_ids(cbs_value) }
11
+ values.freeze
12
+ @values = values
13
+ end
14
+
15
+ def to_s
16
+ str_values = @values.transform_values { |vgs| vgs.map(&:to_s) }
17
+ "CBScores{values:#{str_values}}"
18
+ end
19
+
20
+ class ScoredVarId
21
+ attr_reader :variation_id, :score
22
+
23
+ def initialize(variation_id, score)
24
+ @variation_id = variation_id
25
+ @score = score
26
+ end
27
+ end
28
+
29
+ class VarGroup
30
+ attr_reader :ids
31
+
32
+ def initialize(ids)
33
+ ids.sort!
34
+ ids.freeze
35
+ @ids = ids
36
+ end
37
+
38
+ def to_s
39
+ "VarGroup{ids:#{@ids}}"
40
+ end
41
+ end
42
+
43
+ private
44
+
45
+ def extract_var_ids(scores)
46
+ grouped = {}
47
+ scores.each do |score|
48
+ glist = grouped[score.score]
49
+ if glist
50
+ glist.push(score.variation_id)
51
+ else
52
+ grouped[score.score] = [score.variation_id]
53
+ end
54
+ end
55
+ grouped.sort_by { |score, _| -score }.collect { |_, glist| VarGroup.new(glist) }
56
+ end
57
+ end
58
+ end
@@ -47,4 +47,3 @@ module Kameleoon
47
47
  end
48
48
  end
49
49
  end
50
-
@@ -8,9 +8,9 @@ module Kameleoon
8
8
  @values = values
9
9
  @values.freeze
10
10
  end
11
- end
12
11
 
13
- def to_s
14
- "KcsHeat{values:#{@values}}"
12
+ def to_s
13
+ "KcsHeat{values:#{@values}}"
14
+ end
15
15
  end
16
16
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'concurrent'
4
4
  require 'kameleoon/data/browser'
5
+ require 'kameleoon/data/cbscores'
5
6
  require 'kameleoon/data/conversion'
6
7
  require 'kameleoon/data/custom_data'
7
8
  require 'kameleoon/data/device'
@@ -106,6 +107,12 @@ module Kameleoon
106
107
  kcs_heat
107
108
  end
108
109
 
110
+ def cbscores
111
+ cbs = @data.cbscores
112
+ Logging::KameleoonLogger.debug('CALL/RETURN: Visitor.cbscores -> (cbs: %s)', cbs)
113
+ cbs
114
+ end
115
+
109
116
  def visitor_visits
110
117
  visitor_visits = @data.visitor_visits
111
118
  Logging::KameleoonLogger.debug('CALL/RETURN: Visitor.visitor_visits -> (visitor_visits: %s)', visitor_visits)
@@ -242,6 +249,8 @@ module Kameleoon
242
249
  @data.set_geolocation(data, overwrite)
243
250
  when KcsHeat
244
251
  @data.kcs_heat = data
252
+ when CBScores
253
+ @data.set_cbscores(data, overwrite)
245
254
  when VisitorVisits
246
255
  @data.visitor_visits = data
247
256
  when UniqueIdentifier
@@ -267,7 +276,7 @@ module Kameleoon
267
276
 
268
277
  class VisitorData
269
278
  attr_reader :mutex, :device, :browser, :geolocation, :operating_system
270
- attr_accessor :last_activity_time, :legal_consent, :user_agent, :cookie, :kcs_heat, :visitor_visits,
279
+ attr_accessor :last_activity_time, :legal_consent, :user_agent, :cookie, :kcs_heat, :cbscores, :visitor_visits,
271
280
  :mapping_identifier, :forced_variations, :simulated_variations
272
281
 
273
282
  def initialize
@@ -398,6 +407,10 @@ module Kameleoon
398
407
  @operating_system = operating_system if overwrite || @operating_system.nil?
399
408
  end
400
409
 
410
+ def set_cbscores(cbs, overwrite)
411
+ @cbscores = cbs if overwrite || @cbscores.nil?
412
+ end
413
+
401
414
  def add_forced_feature_variation(forced_variation)
402
415
  @simulated_variations ||= {}
403
416
  @simulated_variations[forced_variation.feature_key] = forced_variation
@@ -67,7 +67,7 @@ module Kameleoon
67
67
  config.environment,
68
68
  config.default_timeout_millisecond,
69
69
  Network::AccessTokenSourceFactory.new(config.client_id, config.client_secret),
70
- Network::UrlProvider.new(site_code)
70
+ Network::UrlProvider.new(site_code, config.network_domain)
71
71
  )
72
72
  @tracking_manager = Managers::Tracking::TrackingManager.new(
73
73
  @data_manager, @network_manager, @visitor_manager, config.tracking_interval_second, @scheduler
@@ -922,37 +922,62 @@ module Kameleoon
922
922
  forced_variation = visitor&.get_forced_feature_variation(feature_flag.feature_key)
923
923
  if forced_variation
924
924
  eval_exp = EvaluatedExperiment.from_forced_variation(forced_variation)
925
- elsif visitor_not_in_holdout?(visitor, visitor_code, track, save)
925
+ elsif visitor_not_in_holdout?(visitor, visitor_code, track, save) && \
926
+ ff_unrestricted_by_me_group?(visitor, visitor_code, feature_flag)
926
927
  eval_exp = _calculate_variation_key_for_feature(visitor_code, feature_flag)
927
928
  end
928
929
  save_variation(visitor_code, eval_exp, track: track) if save && !forced_variation&.simulated
929
930
  Logging::KameleoonLogger.debug(
930
931
  "RETURN: KameleoonClient.evaluate(visitor, visitor_code: '%s', feature_flag: %s, track: %s, save: %s)" \
931
- ' -> (eval_exp: %s)',
932
- visitor_code, feature_flag, track, save, eval_exp
932
+ ' -> (eval_exp: %s)', visitor_code, feature_flag, track, save, eval_exp
933
933
  )
934
934
  eval_exp
935
935
  end
936
936
 
937
+ def ff_unrestricted_by_me_group?(visitor, visitor_code, feature_flag)
938
+ return true if feature_flag.me_group_name.nil?
939
+
940
+ Logging::KameleoonLogger.debug(
941
+ "CALL: KameleoonClient.ff_unrestricted_by_me_group?(visitor, visitor_code: '%s', feature_flag: %s)",
942
+ visitor_code, feature_flag
943
+ )
944
+ unrestricted = true
945
+ me_group = @data_manager.data_file.me_groups[feature_flag.me_group_name]
946
+ if me_group
947
+ code_for_hash = get_code_for_hash(visitor, visitor_code)
948
+ me_group_hash = Utils::Hasher.obtain_hash_for_me_group(code_for_hash, feature_flag.me_group_name)
949
+ Logging::KameleoonLogger.debug(
950
+ "Calculated ME group hash %s for code: '%s', meGroup: '%s'",
951
+ me_group_hash, code_for_hash, feature_flag.me_group_name
952
+ )
953
+ unrestricted = me_group.get_feature_flag_by_hash(me_group_hash) == feature_flag
954
+ end
955
+ Logging::KameleoonLogger.debug(
956
+ "RETURN: KameleoonClient.ff_unrestricted_by_me_group?(visitor, visitor_code: '%s', feature_flag: %s)" \
957
+ ' -> (unrestricted: %s)', visitor_code, feature_flag, unrestricted
958
+ )
959
+ unrestricted
960
+ end
961
+
937
962
  def visitor_not_in_holdout?(visitor, visitor_code, track, save)
963
+ holdout = @data_manager.data_file.holdout
964
+ return true if holdout.nil?
965
+
938
966
  in_holdout_variation_key = 'in-holdout'
939
967
  Logging::KameleoonLogger.debug(
940
968
  "CALL: KameleoonClient.visitor_not_in_holdout?(visitor, visitor_code: '%s', track: %s, save: %s)",
941
969
  visitor_code, track, save
942
970
  )
943
- holdout = @data_manager.data_file.holdout
944
971
  is_not_in_holdout = true
945
- unless holdout.nil?
946
- code_for_hash = get_code_for_hash(visitor, visitor_code)
947
- variation_hash = Utils::Hasher.obtain(code_for_hash, holdout.id)
948
- Logging::KameleoonLogger.debug("Calculated holdout hash %s for code '%s'", variation_hash, code_for_hash)
949
- var_by_exp = holdout.get_variation(variation_hash)
950
- unless var_by_exp.nil?
951
- is_not_in_holdout = var_by_exp.variation_key != in_holdout_variation_key
952
- if save
953
- eval_exp = EvaluatedExperiment.new(var_by_exp, holdout, Configuration::RuleType::EXPERIMENTATION)
954
- save_variation(visitor_code, eval_exp, track: track)
955
- end
972
+ code_for_hash = get_code_for_hash(visitor, visitor_code)
973
+ variation_hash = Utils::Hasher.obtain(code_for_hash, holdout.id)
974
+ Logging::KameleoonLogger.debug("Calculated holdout hash %s for code '%s'", variation_hash, code_for_hash)
975
+ var_by_exp = holdout.get_variation(variation_hash)
976
+ unless var_by_exp.nil?
977
+ is_not_in_holdout = var_by_exp.variation_key != in_holdout_variation_key
978
+ if save
979
+ eval_exp = EvaluatedExperiment.new(var_by_exp, holdout, Configuration::RuleType::EXPERIMENTATION)
980
+ save_variation(visitor_code, eval_exp, track: track)
956
981
  end
957
982
  end
958
983
  Logging::KameleoonLogger.debug(
@@ -1007,6 +1032,42 @@ module Kameleoon
1007
1032
  visitor&.mapping_identifier || visitor_code
1008
1033
  end
1009
1034
 
1035
+ def evaluate_cbscores(visitor, visitor_code, rule)
1036
+ return nil if visitor&.cbscores.nil?
1037
+
1038
+ Logging::KameleoonLogger.debug(
1039
+ "CALL: KameleoonClient.evaluate_cbscores(visitor, visitor_code: '%s', rule: %s)", visitor_code, rule
1040
+ )
1041
+ eval_exp = nil
1042
+ var_id_group_by_scores = visitor.cbscores.values[rule.experiment.id]
1043
+ if var_id_group_by_scores
1044
+ var_by_exp_in_cbs = nil
1045
+ var_id_group_by_scores.each do |var_group|
1046
+ var_by_exp_in_cbs = rule.experiment.variations_by_exposition.filter do |var_by_exp|
1047
+ var_group.ids.include?(var_by_exp.variation_id)
1048
+ end
1049
+ break unless var_by_exp_in_cbs.empty?
1050
+ end
1051
+ if (var_by_exp_in_cbs&.size || 0).positive?
1052
+ size = var_by_exp_in_cbs.size
1053
+ if size > 1
1054
+ code_for_hash = get_code_for_hash(visitor, visitor_code)
1055
+ variation_hash = Utils::Hasher.obtain(code_for_hash, rule.experiment.id, rule.respool_time)
1056
+ Logging::KameleoonLogger.debug("Calculated CBS hash %s for code '%s'", variation_hash, code_for_hash)
1057
+ idx = [(variation_hash * size).to_i, size - 1].min
1058
+ else
1059
+ idx = 0
1060
+ end
1061
+ eval_exp = EvaluatedExperiment.from_var_by_exp_rule(var_by_exp_in_cbs[idx], rule)
1062
+ end
1063
+ end
1064
+ Logging::KameleoonLogger.debug(
1065
+ "RETURN: KameleoonClient.evaluate_cbscores(visitor, visitor_code: '%s', rule: %s) -> (eval_exp: %s)",
1066
+ visitor_code, rule, eval_exp
1067
+ )
1068
+ eval_exp
1069
+ end
1070
+
1010
1071
  ##
1011
1072
  # helper method for calculate variation key for feature flag
1012
1073
  def _calculate_variation_key_for_feature(visitor_code, feature_flag)
@@ -1017,12 +1078,12 @@ module Kameleoon
1017
1078
  visitor = @visitor_manager.get_visitor(visitor_code)
1018
1079
  code_for_hash = get_code_for_hash(visitor, visitor_code)
1019
1080
  # no rules -> return default_variation_key
1020
- selected = nil
1081
+ eval_exp = nil
1021
1082
  feature_flag.rules.each do |rule|
1022
1083
  forced_variation = visitor&.get_forced_experiment_variation(rule.experiment.id)
1023
1084
  if forced_variation&.force_targeting
1024
1085
  # Forcing experiment variation in force-targeting mode
1025
- selected = EvaluatedExperiment.from_var_by_exp_rule(forced_variation.var_by_exp, rule)
1086
+ eval_exp = EvaluatedExperiment.from_var_by_exp_rule(forced_variation.var_by_exp, rule)
1026
1087
  break
1027
1088
  end
1028
1089
  # check if visitor is targeted for rule, else next rule
@@ -1030,7 +1091,7 @@ module Kameleoon
1030
1091
 
1031
1092
  unless forced_variation.nil?
1032
1093
  # Forcing experiment variation in targeting-only mode
1033
- selected = EvaluatedExperiment.from_var_by_exp_rule(forced_variation.var_by_exp, rule)
1094
+ eval_exp = EvaluatedExperiment.from_var_by_exp_rule(forced_variation.var_by_exp, rule)
1034
1095
  break
1035
1096
  end
1036
1097
  # uses for rule exposition
@@ -1038,8 +1099,11 @@ module Kameleoon
1038
1099
  Logging::KameleoonLogger.debug("Calculated rule hash %s for code '%s'", hash_rule, code_for_hash)
1039
1100
  # check main expostion for rule with hashRule
1040
1101
  if hash_rule <= rule.exposition
1102
+ eval_exp = evaluate_cbscores(visitor, visitor_code, rule)
1103
+ break unless eval_exp.nil?
1104
+
1041
1105
  if rule.targeted_delivery_type?
1042
- selected = EvaluatedExperiment.from_var_by_exp_rule(rule.experiment.variations_by_exposition.first, rule)
1106
+ eval_exp = EvaluatedExperiment.from_var_by_exp_rule(rule.experiment.variations_by_exposition.first, rule)
1043
1107
  break
1044
1108
  end
1045
1109
  # uses for variation's expositions
@@ -1048,7 +1112,7 @@ module Kameleoon
1048
1112
  # get variation key with new hashVariation
1049
1113
  variation = rule.experiment.get_variation(hash_variation)
1050
1114
  unless variation.nil?
1051
- selected = EvaluatedExperiment.from_var_by_exp_rule(variation, rule)
1115
+ eval_exp = EvaluatedExperiment.from_var_by_exp_rule(variation, rule)
1052
1116
  break
1053
1117
  end
1054
1118
  # if visitor is targeted for targeted rule then break cycle -> return default
@@ -1058,9 +1122,9 @@ module Kameleoon
1058
1122
  end
1059
1123
  Logging::KameleoonLogger.debug(
1060
1124
  "RETURN: KameleoonClient._calculate_variation_key_for_feature(visitor_code: '%s', feature_flag: %s) " \
1061
- '-> (eval_exp: %s)', visitor_code, feature_flag, selected
1125
+ '-> (eval_exp: %s)', visitor_code, feature_flag, eval_exp
1062
1126
  )
1063
- selected
1127
+ eval_exp
1064
1128
  end
1065
1129
 
1066
1130
  def calculate_variation_key(eval_exp, default_value)
@@ -14,7 +14,7 @@ module Kameleoon
14
14
 
15
15
  attr_reader :client_id, :client_secret, :refresh_interval_second, :session_duration_second,
16
16
  :default_timeout_millisecond, :tracking_interval_second, :environment, :top_level_domain,
17
- :verbose_mode
17
+ :verbose_mode, :network_domain
18
18
 
19
19
  def to_s
20
20
  'KameleoonClientConfig{' \
@@ -25,7 +25,8 @@ module Kameleoon
25
25
  "environment:'#{@environment}'," \
26
26
  "default_timeout_millisecond:#{@default_timeout_millisecond}," \
27
27
  "top_level_domain:'#{@top_level_domain}'," \
28
- "verbose_mode:#{verbose_mode}" \
28
+ "verbose_mode:#{verbose_mode}," \
29
+ "network_domain:'#{@network_domain}'" \
29
30
  '}'
30
31
  end
31
32
 
@@ -39,7 +40,8 @@ module Kameleoon
39
40
  tracking_interval_millisecond: DEFAULT_TRACKING_INTERVAL_MILLISECONDS,
40
41
  environment: nil,
41
42
  top_level_domain: nil,
42
- verbose_mode: nil
43
+ verbose_mode: nil,
44
+ network_domain: nil
43
45
  )
44
46
  raise Exception::ConfigCredentialsInvalid, 'Client ID is not specified' if client_id&.empty? != false
45
47
  raise Exception::ConfigCredentialsInvalid, 'Client secret is not specified' if client_secret&.empty? != false
@@ -114,6 +116,7 @@ module Kameleoon
114
116
  )
115
117
  end
116
118
  @top_level_domain = Utils::Domain.validate_top_level_domain(top_level_domain)
119
+ @network_domain = Utils::Domain.validate_network_domain(network_domain)
117
120
  end
118
121
 
119
122
  def self.read_from_yaml(path)
@@ -131,7 +134,8 @@ module Kameleoon
131
134
  tracking_interval_millisecond: yaml['tracking_interval_millisecond'],
132
135
  environment: yaml['environment'],
133
136
  top_level_domain: yaml['top_level_domain'],
134
- verbose_mode: yaml['verbose_mode']
137
+ verbose_mode: yaml['verbose_mode'],
138
+ network_domain: yaml['network_domain']
135
139
  )
136
140
  end
137
141
  end
@@ -1,22 +1,23 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'kameleoon/data/manager/page_view_visit'
4
- require 'kameleoon/data/manager/assigned_variation'
5
- require 'kameleoon/data/visitor_visits'
6
3
  require 'kameleoon/data/browser'
7
- require 'kameleoon/data/device'
4
+ require 'kameleoon/data/cbscores'
8
5
  require 'kameleoon/data/conversion'
9
6
  require 'kameleoon/data/custom_data'
7
+ require 'kameleoon/data/device'
10
8
  require 'kameleoon/data/geolocation'
11
9
  require 'kameleoon/data/kcs_heat'
12
10
  require 'kameleoon/data/page_view'
11
+ require 'kameleoon/data/visitor_visits'
12
+ require 'kameleoon/data/manager/page_view_visit'
13
+ require 'kameleoon/data/manager/assigned_variation'
13
14
 
14
15
  module Kameleoon
15
16
  module Managers
16
17
  module RemoteData
17
18
  class RemoteVisitorData
18
19
  attr_reader :custom_data_dict, :page_view_visits, :conversions, :experiments, :device, :browser,
19
- :operating_system, :geolocation, :previous_visitor_visits, :kcs_heat, :visitor_code
20
+ :operating_system, :geolocation, :previous_visitor_visits, :kcs_heat, :cbs, :visitor_code
20
21
 
21
22
  def initialize(hash)
22
23
  current_visit = hash['currentVisit']
@@ -33,6 +34,7 @@ module Kameleoon
33
34
  @previous_visitor_visits = VisitorVisits.new(times_started)
34
35
  end
35
36
  @kcs_heat = parse_kcs_heat(hash['kcs'])
37
+ @cbs = parse_cbscores(hash['cbs'])
36
38
  end
37
39
 
38
40
  def collect_data_to_add
@@ -40,6 +42,7 @@ module Kameleoon
40
42
  data_to_add.concat(@custom_data_dict.values) unless @custom_data_dict.nil?
41
43
  data_to_add.push(@previous_visitor_visits) unless @previous_visitor_visits.nil?
42
44
  data_to_add.push(@kcs_heat) unless @kcs_heat.nil?
45
+ data_to_add.push(@cbs) unless @cbs.nil?
43
46
  data_to_add.concat(@page_view_visits.values) unless @page_view_visits.nil?
44
47
  data_to_add.concat(@experiments.values) unless @experiments.nil?
45
48
  data_to_add.concat(conversions_single_objects)
@@ -184,6 +187,26 @@ module Kameleoon
184
187
  end
185
188
  KcsHeat.new(value_map)
186
189
  end
190
+
191
+ def parse_cbscores(cbs)
192
+ return nil if cbs.nil?
193
+
194
+ cbs_map = {}
195
+ cbs.each do |str_exp_id, scored_var_entries|
196
+ next unless str_exp_id.is_a?(String) && scored_var_entries.is_a?(Hash)
197
+
198
+ entries = []
199
+ scored_var_entries.each do |str_var_id, score|
200
+ next unless str_var_id.is_a?(String) && (score.is_a?(Float) || score.is_a?(Integer))
201
+
202
+ var_id = str_var_id.to_i
203
+ entries.push(CBScores::ScoredVarId.new(var_id, score))
204
+ end
205
+ exp_id = str_exp_id.to_i
206
+ cbs_map[exp_id] = entries
207
+ end
208
+ CBScores.new(cbs_map)
209
+ end
187
210
  end
188
211
  end
189
212
  end
@@ -88,13 +88,13 @@ module Kameleoon
88
88
  if response.success?
89
89
  success = true
90
90
  elsif !response.error.nil?
91
- log_level = attempt < retry_limit ? Logging::LogLevel::WARNING : Logging::LogLevel::ERROR
91
+ log_level = get_log_level(attempt, retry_limit)
92
92
  Logging::KameleoonLogger.log(log_level, "%s call '%s' failed: Error occurred during request: %s",
93
93
  request.method, request.url, response.error)
94
94
  else
95
- log_level = attempt < retry_limit ? Logging::LogLevel::WARNING : Logging::LogLevel::ERROR
96
- Logging::KameleoonLogger.log(log_level, "%s call '%s' failed: Received unexpected status code '%s'",
97
- request.method, request.url, response.code)
95
+ log_level = get_log_level(attempt, retry_limit)
96
+ Logging::KameleoonLogger.log(log_level, "%s call '%s' failed: Received unexpected status code: %s, body: %s",
97
+ request.method, request.url, response.code, response.body)
98
98
  if response.code == 401 && request.access_token
99
99
  Logging::KameleoonLogger.warning("Unexpected rejection of access token '#{request.access_token}'")
100
100
  @access_token_source.discard_token(request.access_token)
@@ -113,6 +113,10 @@ module Kameleoon
113
113
  success ? response.body : false
114
114
  end
115
115
 
116
+ def get_log_level(attempt, attempt_count)
117
+ return log_level = attempt < attempt_count ? Logging::LogLevel::WARNING : Logging::LogLevel::ERROR
118
+ end
119
+
116
120
  def delay(period)
117
121
  sleep(period)
118
122
  end
@@ -14,27 +14,43 @@ module Kameleoon
14
14
  POST_DATA_PATH = '/map/maps'
15
15
  ACCESS_TOKEN_PATH = '/oauth/token'
16
16
 
17
- CONFIGURATION_API_URL_FORMAT = 'https://sdk-config.kameleoon.eu/%s'
18
- RT_CONFIGURATION_URL = 'https://events.kameleoon.com:8110/sse'
17
+ DEFAULT_EVENTS_DOMAIN = 'events.kameleoon.eu'
18
+ DEFAULT_CONFIGURATION_DOMAIN = 'sdk-config.kameleoon.eu'
19
+ DEFAULT_ACCESS_TOKEN_DOMAIN = 'api.kameleoon.com'
19
20
  DEFAULT_DATA_API_DOMAIN = 'data.kameleoon.io'
21
+
22
+ CONFIGURATION_API_URL_FORMAT = 'https://%s/%s'
23
+ DATA_API_URL_FORMAT = 'https://%s%s?%s'
24
+ RT_CONFIGURATION_URL_FORMAT = 'https://%s:8110/sse?%s'
25
+ ACCESS_TOKEN_URL_FORMAT = 'https://%s/oauth/token'
26
+
20
27
  TEST_DATA_API_DOMAIN = 'data.kameleoon.net'
21
- DEFAULT_AUTOMATION_API_DOMAIN = 'api.kameleoon.com'
22
28
  TEST_AUTOMATION_API_DOMAIN = 'api.kameleoon.net'
23
29
 
24
- attr_reader :site_code, :data_api_domain, :automation_api_domain
30
+ attr_reader :site_code, :data_api_domain, :events_domain, :configuration_domain, :access_token_domain
25
31
 
26
32
  def initialize(
27
33
  site_code,
28
- data_api_domain = DEFAULT_DATA_API_DOMAIN,
29
- automation_api_domain = DEFAULT_AUTOMATION_API_DOMAIN
34
+ network_domain = nil
30
35
  )
31
36
  @site_code = site_code
32
- @data_api_domain = data_api_domain
33
- @automation_api_domain = automation_api_domain
37
+ @data_api_domain = DEFAULT_DATA_API_DOMAIN
38
+ @events_domain = DEFAULT_EVENTS_DOMAIN
39
+ @configuration_domain = DEFAULT_CONFIGURATION_DOMAIN
40
+ @access_token_domain = DEFAULT_ACCESS_TOKEN_DOMAIN
41
+ @is_custom_domain = false
42
+ update_domains(network_domain)
34
43
  end
35
44
 
36
45
  def apply_data_api_domain(domain)
37
- @data_api_domain = domain if domain.is_a?(String)
46
+ return if domain.nil? || domain.empty?
47
+
48
+ if @is_custom_domain
49
+ sub_domain = domain.split('.').first
50
+ @data_api_domain = @data_api_domain.sub(/^[^.]+/, sub_domain)
51
+ else
52
+ @data_api_domain = domain
53
+ end
38
54
  end
39
55
 
40
56
  def make_tracking_url
@@ -44,7 +60,7 @@ module Kameleoon
44
60
  siteCode: @site_code,
45
61
  bodyUa: true
46
62
  }
47
- "https://#{@data_api_domain}#{TRACKING_PATH}?#{UriHelper.encode_query(params)}"
63
+ format(DATA_API_URL_FORMAT, @data_api_domain, TRACKING_PATH, UriHelper.encode_query(params))
48
64
  end
49
65
 
50
66
  def make_visitor_data_get_url(visitor_code, filter, is_unique_identifier = false)
@@ -60,7 +76,8 @@ module Kameleoon
60
76
  params[:experiment] = true if filter.experiments
61
77
  params[:page] = true if filter.page_views
62
78
  params[:staticData] = true if filter.device || filter.browser || filter.operating_system
63
- "https://#{@data_api_domain}#{VISITOR_DATA_PATH}?#{UriHelper.encode_query(params)}"
79
+ params[:cbs] = true if filter.cbs
80
+ format(DATA_API_URL_FORMAT, @data_api_domain, VISITOR_DATA_PATH, UriHelper.encode_query(params))
64
81
  end
65
82
 
66
83
  def make_api_data_get_request_url(key)
@@ -68,11 +85,11 @@ module Kameleoon
68
85
  siteCode: @site_code,
69
86
  key: key
70
87
  }
71
- "https://#{@data_api_domain}#{GET_DATA_PATH}?#{UriHelper.encode_query(params)}"
88
+ format(DATA_API_URL_FORMAT, @data_api_domain, GET_DATA_PATH, UriHelper.encode_query(params))
72
89
  end
73
90
 
74
91
  def make_configuration_url(environment = nil, timestamp = nil)
75
- url = format(CONFIGURATION_API_URL_FORMAT, @site_code)
92
+ url = format(CONFIGURATION_API_URL_FORMAT, @configuration_domain, @site_code)
76
93
  params = {}
77
94
  params[:environment] = environment unless environment.nil?
78
95
  params[:ts] = timestamp unless timestamp.nil?
@@ -82,11 +99,24 @@ module Kameleoon
82
99
 
83
100
  def make_real_time_url
84
101
  params = { siteCode: @site_code }
85
- "#{RT_CONFIGURATION_URL}?#{UriHelper.encode_query(params)}"
102
+ format(RT_CONFIGURATION_URL_FORMAT, @events_domain, UriHelper.encode_query(params))
86
103
  end
87
104
 
88
105
  def make_access_token_url
89
- "https://#{@automation_api_domain}#{ACCESS_TOKEN_PATH}"
106
+ format(ACCESS_TOKEN_URL_FORMAT, @access_token_domain)
107
+ end
108
+
109
+ private
110
+
111
+ def update_domains(network_domain)
112
+ return if network_domain.nil? || network_domain.empty?
113
+
114
+ @is_custom_domain = true
115
+
116
+ @data_api_domain = "data.#{network_domain}"
117
+ @events_domain = "events.#{network_domain}"
118
+ @configuration_domain = "sdk-config.#{network_domain}"
119
+ @access_token_domain = "api.#{network_domain}"
90
120
  end
91
121
  end
92
122
  end
@@ -5,7 +5,7 @@ module Kameleoon
5
5
  module Types
6
6
  class RemoteVisitorDataFilter
7
7
  attr_reader :previous_visit_amount, :current_visit, :custom_data, :page_views, :geolocation, :device, :browser,
8
- :operating_system, :conversions, :experiments, :kcs, :visitor_code
8
+ :operating_system, :conversions, :experiments, :kcs, :visitor_code, :cbs
9
9
 
10
10
  def to_s
11
11
  "RemoteVisitorDataFilter{previous_visit_amount:#{@previous_visit_amount}," \
@@ -19,13 +19,14 @@ module Kameleoon
19
19
  "conversions:#{@conversions}," \
20
20
  "experiments:#{@experiments}," \
21
21
  "kcs:#{@kcs}," \
22
- "visitor_code:#{@visitor_code}}"
22
+ "visitor_code:#{@visitor_code}}" \
23
+ "cbs:#{@cbs}}"
23
24
  end
24
25
 
25
26
  def initialize(
26
27
  previous_visit_amount: 1, current_visit: true, custom_data: true, page_views: false,
27
28
  geolocation: false, device: false, browser: false, operating_system: false, conversions: false,
28
- experiments: false, kcs: false, visitor_code: true
29
+ experiments: false, kcs: false, visitor_code: true, cbs: false
29
30
  )
30
31
  @previous_visit_amount = previous_visit_amount
31
32
  @current_visit = current_visit
@@ -39,6 +40,7 @@ module Kameleoon
39
40
  @experiments = experiments
40
41
  @kcs = kcs
41
42
  @visitor_code = visitor_code
43
+ @cbs = cbs
42
44
  end
43
45
  end
44
46
  end
@@ -38,6 +38,10 @@ module Kameleoon
38
38
  calculate("#{visitor_code}#{container_id}#{suffix}")
39
39
  end
40
40
 
41
+ def self.obtain_hash_for_me_group(visitor_code, me_group_name)
42
+ calculate("#{visitor_code}#{me_group_name}")
43
+ end
44
+
41
45
  def self.calculate(string_to_hash)
42
46
  parsed_value = Digest::SHA256.hexdigest(string_to_hash.encode('UTF-8')).to_i(16)
43
47
  max_value = BigDecimal('2')**BigDecimal('256')
@@ -73,17 +77,7 @@ module Kameleoon
73
77
  def self.validate_top_level_domain(top_level_domain)
74
78
  return nil if top_level_domain.nil? || top_level_domain.empty?
75
79
 
76
- top_level_domain = top_level_domain.downcase
77
-
78
- [HTTP, HTTPS].each do |protocol|
79
- next unless top_level_domain.start_with?(protocol)
80
-
81
- top_level_domain = top_level_domain[protocol.length..]
82
- Logging::KameleoonLogger.warning(
83
- "The top-level domain contains '%s'. Domain after protocol trimming: '%s'", protocol, top_level_domain
84
- )
85
- break
86
- end
80
+ top_level_domain = check_and_trim_protocol(top_level_domain.downcase)
87
81
 
88
82
  if !REGEX_DOMAIN.match?(top_level_domain) && top_level_domain != LOCALHOST
89
83
  Logging::KameleoonLogger.error(
@@ -96,6 +90,41 @@ module Kameleoon
96
90
 
97
91
  top_level_domain
98
92
  end
93
+
94
+ def self.validate_network_domain(network_domain)
95
+ return nil if network_domain.nil? || network_domain.empty?
96
+
97
+ network_domain = check_and_trim_protocol(network_domain.downcase)
98
+
99
+ # remove first and last dot
100
+ network_domain = network_domain.gsub(/^\.+|\.+$/, '')
101
+
102
+ if !REGEX_DOMAIN.match?(network_domain) && network_domain != LOCALHOST
103
+ Logging::KameleoonLogger.error(
104
+ "The network domain '%s' is invalid.",
105
+ network_domain
106
+ )
107
+ return nil
108
+ end
109
+
110
+ network_domain
111
+ end
112
+
113
+ private
114
+
115
+ def self.check_and_trim_protocol(domain)
116
+ [HTTP, HTTPS].each do |protocol|
117
+ next unless domain.start_with?(protocol)
118
+
119
+ domain = domain[protocol.length..]
120
+ Logging::KameleoonLogger.warning(
121
+ "The domain contains '%s'. Domain after protocol trimming: '%s'", protocol, domain
122
+ )
123
+ break
124
+ end
125
+
126
+ domain
127
+ end
99
128
  end
100
129
  end
101
130
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Kameleoon
4
- SDK_VERSION = '3.8.0'
4
+ SDK_VERSION = '3.10.0'
5
5
  SDK_NAME = 'RUBY'
6
6
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kameleoon-client-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.8.0
4
+ version: 3.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kameleoon
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-02-10 00:00:00.000000000 Z
11
+ date: 2025-03-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: em-http-request
@@ -80,12 +80,14 @@ files:
80
80
  - lib/kameleoon/configuration/data_file.rb
81
81
  - lib/kameleoon/configuration/experiment.rb
82
82
  - lib/kameleoon/configuration/feature_flag.rb
83
+ - lib/kameleoon/configuration/me_group.rb
83
84
  - lib/kameleoon/configuration/rule.rb
84
85
  - lib/kameleoon/configuration/settings.rb
85
86
  - lib/kameleoon/configuration/variable.rb
86
87
  - lib/kameleoon/configuration/variation.rb
87
88
  - lib/kameleoon/configuration/variation_exposition.rb
88
89
  - lib/kameleoon/data/browser.rb
90
+ - lib/kameleoon/data/cbscores.rb
89
91
  - lib/kameleoon/data/conversion.rb
90
92
  - lib/kameleoon/data/cookie.rb
91
93
  - lib/kameleoon/data/custom_data.rb