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 +4 -4
- data/lib/kameleoon/configuration/data_file.rb +21 -3
- data/lib/kameleoon/configuration/feature_flag.rb +7 -5
- data/lib/kameleoon/configuration/me_group.rb +19 -0
- data/lib/kameleoon/data/cbscores.rb +58 -0
- data/lib/kameleoon/data/geolocation.rb +0 -1
- data/lib/kameleoon/data/kcs_heat.rb +3 -3
- data/lib/kameleoon/data/manager/visitor.rb +14 -1
- data/lib/kameleoon/kameleoon_client.rb +87 -23
- data/lib/kameleoon/kameleoon_client_config.rb +8 -4
- data/lib/kameleoon/managers/remote_data/remote_visitor_data.rb +28 -5
- data/lib/kameleoon/network/network_manager.rb +8 -4
- data/lib/kameleoon/network/url_provider.rb +45 -15
- data/lib/kameleoon/types/remote_visitor_data_filter.rb +5 -3
- data/lib/kameleoon/utils.rb +40 -11
- data/lib/kameleoon/version.rb +1 -1
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1831531ee6a40f0fbd32f110f9a9c9c1a33f71569c8b8de842bc01c06250d6d5
|
4
|
+
data.tar.gz: 6e6f8f9d0ee511c2a27fc63ff040e6155b90df66925989d1d08bd473f86c071b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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, :
|
13
|
-
:rule_info_by_exp_id, :variation_by_id, :custom_data_info,
|
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
|
21
|
-
"environment_enabled:#{@environment_enabled},
|
22
|
-
"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
|
@@ -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
|
-
|
946
|
-
|
947
|
-
|
948
|
-
|
949
|
-
|
950
|
-
|
951
|
-
|
952
|
-
|
953
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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,
|
1125
|
+
'-> (eval_exp: %s)', visitor_code, feature_flag, eval_exp
|
1062
1126
|
)
|
1063
|
-
|
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/
|
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
|
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
|
96
|
-
Logging::KameleoonLogger.log(log_level, "%s call '%s' failed: Received unexpected status code
|
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
|
-
|
18
|
-
|
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, :
|
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
|
-
|
29
|
-
automation_api_domain = DEFAULT_AUTOMATION_API_DOMAIN
|
34
|
+
network_domain = nil
|
30
35
|
)
|
31
36
|
@site_code = site_code
|
32
|
-
@data_api_domain =
|
33
|
-
@
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
data/lib/kameleoon/utils.rb
CHANGED
@@ -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
|
data/lib/kameleoon/version.rb
CHANGED
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.
|
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-
|
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
|