statsig 1.34.2 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9feb2cdc643d463427564f33990fba62a3d4049f6361c054c47a37c4668206c4
4
- data.tar.gz: 17ab60cda06c3c157b0fa8a0d573abf93629a1b7092a5678789e85e044a056ed
3
+ metadata.gz: 27afba91b3e46ea8661d399ef0dc2696c6372bb214dbb993ea986e5b456edb7e
4
+ data.tar.gz: c06043637e4ce2ae3bbdef95ccc8a0713717b04e8149b46a0f70da502aa729db
5
5
  SHA512:
6
- metadata.gz: df765ca7dd72b88a8c34e2d8844f445a4ad878a0271a2d5f3d1a4410a8b2ce7369753df5acb1ab9cf0556082d9b9ec4618bed53650074083a60bc1f6ebf72757
7
- data.tar.gz: ffb48e41016e350a078787fe0392d4d7147e55c8067b0299756b7c26a036c859e648bcac83a56d2f3acc653ac870bfbe57910ef00882efb8bf93157fb905f8cd
6
+ metadata.gz: ac429df53886c94abbd1b801adb96a8bab92e6c8c73aa4f910a2a505b5a3bf65ba3d3b844cca6014f6dbac0b17ce05d7a967d5fde260f0bc6f3021e60c84e8af
7
+ data.tar.gz: 14d9c181b6093d56ca7bba00873ff1806cc063ac43608d9ff3cde9e2396423faf58fe50b4096ac4faf7806bcfaec4c946c325f9be20a83b4f311e521162958bb
data/lib/api_config.rb CHANGED
@@ -2,144 +2,3 @@ require 'constants'
2
2
 
3
3
  class UnsupportedConfigException < StandardError
4
4
  end
5
-
6
- module Statsig
7
- class APIConfig
8
- attr_accessor :name, :type, :is_active, :salt, :default_value, :enabled,
9
- :rules, :id_type, :entity, :explicit_parameters, :has_shared_params, :target_app_ids
10
-
11
- def self.from_json(json)
12
- new(
13
- name: json[:name],
14
- type: json[:type],
15
- is_active: json[:isActive],
16
- salt: json[:salt],
17
- default_value: json[:defaultValue] || {},
18
- enabled: json[:enabled],
19
- rules: json[:rules]&.map do |rule|
20
- APIRule.from_json(rule)
21
- end,
22
- id_type: json[:idType],
23
- entity: json[:entity],
24
- explicit_parameters: json[:explicitParameters],
25
- has_shared_params: json[:hasSharedParams],
26
- target_app_ids: json[:targetAppIDs]
27
- )
28
- end
29
-
30
- private
31
-
32
- def initialize(name:, type:, is_active:, salt:, default_value:, enabled:, rules:, id_type:, entity:,
33
- explicit_parameters: nil, has_shared_params: nil, target_app_ids: nil)
34
- @name = name
35
- @type = type.to_sym unless type.nil?
36
- @is_active = is_active
37
- @salt = salt
38
- @default_value = JSON.parse(JSON.generate(default_value))
39
- @enabled = enabled
40
- @rules = rules
41
- @id_type = id_type
42
- @entity = entity.to_sym unless entity.nil?
43
- @explicit_parameters = explicit_parameters
44
- @has_shared_params = has_shared_params
45
- @target_app_ids = target_app_ids
46
- end
47
- end
48
-
49
- class APIRule
50
-
51
- attr_accessor :name, :pass_percentage, :return_value, :id, :salt,
52
- :conditions, :id_type, :group_name, :config_delegate, :is_experiment_group
53
-
54
- def self.from_json(json)
55
- new(
56
- name: json[:name],
57
- pass_percentage: json[:passPercentage],
58
- return_value: json[:returnValue] || {},
59
- id: json[:id],
60
- salt: json[:salt],
61
- conditions: json[:conditions]&.map do |condition|
62
- APICondition.from_json(condition)
63
- end,
64
- id_type: json[:idType],
65
- group_name: json[:groupName],
66
- config_delegate: json[:configDelegate],
67
- is_experiment_group: json[:isExperimentGroup]
68
- )
69
- end
70
-
71
- private
72
-
73
- def initialize(name:, pass_percentage:, return_value:, id:, salt:, conditions:, id_type:,
74
- group_name: nil, config_delegate: nil, is_experiment_group: nil)
75
- @name = name
76
- @pass_percentage = pass_percentage.to_f
77
- @return_value = JSON.parse(JSON.generate(return_value))
78
- @id = id
79
- @salt = salt
80
- @conditions = conditions
81
- @id_type = id_type
82
- @group_name = group_name
83
- @config_delegate = config_delegate
84
- @is_experiment_group = is_experiment_group
85
- end
86
- end
87
-
88
- class APICondition
89
-
90
- attr_accessor :type, :target_value, :operator, :field, :additional_values, :id_type, :hash
91
- def self.from_json(json)
92
- hash = Statsig::HashUtils.md5(json.to_s).to_sym
93
- return Statsig::Memo.for_global(:api_condition_from_json, hash) do
94
- operator = json[:operator]
95
- unless operator.nil?
96
- operator = operator&.downcase&.to_sym
97
- unless Const::SUPPORTED_OPERATORS.include?(operator)
98
- raise UnsupportedConfigException
99
- end
100
- end
101
-
102
- type = json[:type]
103
- unless type.nil?
104
- type = type&.downcase&.to_sym
105
- unless Const::SUPPORTED_CONDITION_TYPES.include?(type)
106
- raise UnsupportedConfigException
107
- end
108
- end
109
-
110
- new(
111
- type: json[:type],
112
- target_value: json[:targetValue],
113
- operator: json[:operator],
114
- field: json[:field],
115
- additional_values: json[:additionalValues],
116
- id_type: json[:idType],
117
- hash: hash
118
- )
119
- end
120
- end
121
-
122
- private
123
-
124
- def initialize(type:, target_value:, operator:, field:, additional_values:, id_type:, hash:)
125
- @hash = hash
126
-
127
- @type = type.to_sym unless type.nil?
128
- if operator == "any_case_sensitive" || operator == "none_case_sensitive"
129
- if target_value.is_a?(Array)
130
- target_value = target_value.map { |item| [item.to_s, true] }.to_h
131
- end
132
- end
133
- if operator == "any" || operator == "none"
134
- if target_value.is_a?(Array)
135
- target_value = target_value.map { |item| [item.to_s.downcase, true] }.to_h
136
- end
137
- end
138
- @target_value = target_value
139
- @operator = operator.to_sym unless operator.nil?
140
- @field = field
141
- @additional_values = additional_values || {}
142
- @id_type = id_type
143
- end
144
- end
145
- end
@@ -16,7 +16,7 @@ module Statsig
16
16
  result = {}
17
17
  target_app_id = evaluator.spec_store.get_app_id_for_sdk_key(client_sdk_key)
18
18
  entities.each do |name, spec|
19
- config_target_apps = spec.target_app_ids
19
+ config_target_apps = spec[:targetAppIDs]
20
20
 
21
21
  unless target_app_id.nil? || (!config_target_apps.nil? && config_target_apps.include?(target_app_id))
22
22
  next
@@ -31,17 +31,18 @@ module Statsig
31
31
  end
32
32
 
33
33
  def self.to_response(config_name, config_spec, evaluator, user, client_sdk_key, hash_algo, include_exposures, include_local_overrides)
34
- category = config_spec.type
35
- entity_type = config_spec.entity
36
- if entity_type == :segment || entity_type == :holdout
34
+ config_name_str = config_name.to_s
35
+ category = config_spec[:type]
36
+ entity_type = config_spec[:entity]
37
+ if entity_type == Const::TYPE_SEGMENT || entity_type == Const::TYPE_HOLDOUT
37
38
  return nil
38
39
  end
39
40
 
40
41
  if include_local_overrides
41
42
  case category
42
- when :feature_gate
43
+ when Const::TYPE_FEATURE_GATE
43
44
  local_override = evaluator.lookup_gate_override(config_name)
44
- when :dynamic_config
45
+ when Const::TYPE_DYNAMIC_CONFIG
45
46
  local_override = evaluator.lookup_config_override(config_name)
46
47
  end
47
48
  end
@@ -52,7 +53,7 @@ module Statsig
52
53
  disable_evaluation_details: true,
53
54
  disable_exposures: !include_exposures
54
55
  )
55
- evaluator.eval_spec(user, config_spec, eval_result)
56
+ evaluator.eval_spec(config_name_str, user, config_spec, eval_result)
56
57
  else
57
58
  eval_result = local_override
58
59
  end
@@ -65,10 +66,10 @@ module Statsig
65
66
  end
66
67
 
67
68
  case category
68
- when :feature_gate
69
+ when Const::TYPE_FEATURE_GATE
69
70
  result[:value] = eval_result.gate_value
70
- when :dynamic_config
71
- id_type = config_spec.id_type
71
+ when Const::TYPE_DYNAMIC_CONFIG
72
+ id_type = config_spec[:idType]
72
73
  result[:value] = eval_result.json_value
73
74
  result[:group] = eval_result.rule_id
74
75
  result[:is_device_based] = id_type.is_a?(String) && id_type.downcase == Statsig::Const::STABLEID
@@ -76,16 +77,16 @@ module Statsig
76
77
  return nil
77
78
  end
78
79
 
79
- if entity_type == :experiment
80
+ if entity_type == Const::TYPE_EXPERIMENT
80
81
  populate_experiment_fields(name, config_spec, eval_result, result, evaluator)
81
82
  end
82
83
 
83
- if entity_type == :layer
84
+ if entity_type == Const::TYPE_LAYER
84
85
  populate_layer_fields(config_spec, eval_result, result, evaluator, hash_algo, include_exposures)
85
86
  result.delete(:id_type) # not exposed for layer configs in /initialize
86
87
  end
87
88
 
88
- hashed_name = hash_name(config_name, hash_algo)
89
+ hashed_name = hash_name(config_name_str, hash_algo)
89
90
 
90
91
  result[:name] = hashed_name
91
92
  result[:rule_id] = eval_result.rule_id
@@ -99,14 +100,14 @@ module Statsig
99
100
 
100
101
  def self.populate_experiment_fields(config_name, config_spec, eval_result, result, evaluator)
101
102
  result[:is_user_in_experiment] = eval_result.is_experiment_group
102
- result[:is_experiment_active] = config_spec.is_active == true
103
+ result[:is_experiment_active] = config_spec[:isActive] == true
103
104
 
104
- if config_spec.has_shared_params != true
105
+ if config_spec[:hasSharedParams] != true
105
106
  return
106
107
  end
107
108
 
108
109
  result[:is_in_layer] = true
109
- result[:explicit_parameters] = config_spec.explicit_parameters || []
110
+ result[:explicit_parameters] = config_spec[:explicitParameters] || []
110
111
 
111
112
  layer_name = evaluator.spec_store.experiment_to_layer[config_name]
112
113
  if layer_name.nil? || evaluator.spec_store.layers[layer_name].nil?
@@ -119,15 +120,15 @@ module Statsig
119
120
 
120
121
  def self.populate_layer_fields(config_spec, eval_result, result, evaluator, hash_algo, include_exposures)
121
122
  delegate = eval_result.config_delegate
122
- result[:explicit_parameters] = config_spec.explicit_parameters || []
123
+ result[:explicit_parameters] = config_spec[:explicitParameters] || []
123
124
 
124
125
  if delegate.nil? == false && delegate.empty? == false
125
- delegate_spec = evaluator.spec_store.configs[delegate]
126
+ delegate_spec = evaluator.spec_store.configs[delegate.to_sym]
126
127
 
127
128
  result[:allocated_experiment_name] = hash_name(delegate, hash_algo)
128
129
  result[:is_user_in_experiment] = eval_result.is_experiment_group
129
- result[:is_experiment_active] = delegate_spec.is_active == true
130
- result[:explicit_parameters] = delegate_spec.explicit_parameters || []
130
+ result[:is_experiment_active] = delegate_spec[:isActive] == true
131
+ result[:explicit_parameters] = delegate_spec[:explicitParameters] || []
131
132
  end
132
133
 
133
134
  if include_exposures
data/lib/constants.rb CHANGED
@@ -47,6 +47,12 @@ module Statsig
47
47
  USER_ID = 'user_id'.freeze
48
48
  USERAGENT = 'useragent'.freeze
49
49
  USERID = 'userid'.freeze
50
+ DICTIONARY = 'dictionary'.freeze
51
+ SEGMENT_PREFIX = 'segment:'.freeze
52
+
53
+ DYNAMIC_CONFIG_NAME = 'dynamic_config_name'.freeze
54
+ EXPERIMENT_NAME = 'experiment_name'.freeze
55
+ LAYER_NAME = 'layer_name'.freeze
50
56
 
51
57
  # Persisted Evaluations
52
58
  GATE_VALUE = 'gate_value'.freeze
@@ -58,5 +64,64 @@ module Statsig
58
64
  TARGET_APP_IDS = 'target_app_ids'.freeze
59
65
  CONFIG_SYNC_TIME = 'config_sync_time'.freeze
60
66
  INIT_TIME = 'init_time'.freeze
67
+
68
+ # Spec Types
69
+ TYPE_FEATURE_GATE = 'feature_gate'.freeze
70
+ TYPE_SEGMENT = 'segment'.freeze
71
+ TYPE_HOLDOUT = 'holdout'.freeze
72
+ TYPE_EXPERIMENT = 'experiment'.freeze
73
+ TYPE_LAYER = 'layer'.freeze
74
+ TYPE_DYNAMIC_CONFIG = 'dynamic_config'.freeze
75
+ TYPE_AUTOTUNE = 'autotune'.freeze
76
+
77
+ # API Conditions
78
+ CND_PUBLIC = 'public'.freeze
79
+ CND_IP_BASED = 'ip_based'.freeze
80
+ CND_UA_BASED = 'ua_based'.freeze
81
+ CND_USER_FIELD = 'user_field'.freeze
82
+ CND_PASS_GATE = 'pass_gate'.freeze
83
+ CND_FAIL_GATE = 'fail_gate'.freeze
84
+ CND_MULTI_PASS_GATE = 'multi_pass_gate'.freeze
85
+ CND_MULTI_FAIL_GATE = 'multi_fail_gate'.freeze
86
+ CND_CURRENT_TIME = 'current_time'.freeze
87
+ CND_ENVIRONMENT_FIELD = 'environment_field'.freeze
88
+ CND_USER_BUCKET = 'user_bucket'.freeze
89
+ CND_UNIT_ID = 'unit_id'.freeze
90
+
91
+ # API Operators
92
+ OP_GREATER_THAN = 'gt'.freeze
93
+ OP_GREATER_THAN_OR_EQUAL = 'gte'.freeze
94
+ OP_LESS_THAN = 'lt'.freeze
95
+ OP_LESS_THAN_OR_EQUAL = 'lte'.freeze
96
+ OP_ANY = 'any'.freeze
97
+ OP_NONE = 'none'.freeze
98
+ OP_ANY_CASE_SENSITIVE = 'any_case_sensitive'.freeze
99
+ OP_NONE_CASE_SENSITIVE = 'none_case_sensitive'.freeze
100
+ OP_EQUAL = 'eq'.freeze
101
+ OP_NOT_EQUAL = 'neq'.freeze
102
+
103
+ # API Operators (Version)
104
+ OP_VERSION_GREATER_THAN = 'version_gt'.freeze
105
+ OP_VERSION_GREATER_THAN_OR_EQUAL = 'version_gte'.freeze
106
+ OP_VERSION_LESS_THAN = 'version_lt'.freeze
107
+ OP_VERSION_LESS_THAN_OR_EQUAL = 'version_lte'.freeze
108
+ OP_VERSION_EQUAL = 'version_eq'.freeze
109
+ OP_VERSION_NOT_EQUAL = 'version_neq'.freeze
110
+
111
+ # API Operators (String)
112
+ OP_STR_STARTS_WITH_ANY = 'str_starts_with_any'.freeze
113
+ OP_STR_END_WITH_ANY = 'str_ends_with_any'.freeze
114
+ OP_STR_CONTAINS_ANY = 'str_contains_any'.freeze
115
+ OP_STR_CONTAINS_NONE = 'str_contains_none'.freeze
116
+ OP_STR_MATCHES = 'str_matches'.freeze
117
+
118
+ # API Operators (Time)
119
+ OP_BEFORE = 'before'.freeze
120
+ OP_AFTER = 'after'.freeze
121
+ OP_ON = 'on'.freeze
122
+
123
+ # API Operators (Segments)
124
+ OP_IN_SEGMENT_LIST = 'in_segment_list'.freeze
125
+ OP_NOT_IN_SEGMENT_LIST = 'not_in_segment_list'.freeze
61
126
  end
62
127
  end
@@ -33,8 +33,12 @@ class DynamicConfig
33
33
  # @param index The name of parameter being fetched
34
34
  # @param default_value The fallback value if the name cannot be found
35
35
  def get(index, default_value)
36
- return default_value if @value.nil? || !@value.key?(index)
37
- @value[index]
36
+ return default_value if @value.nil?
37
+
38
+ index_sym = index.to_sym
39
+ return default_value unless @value.key?(index_sym)
40
+
41
+ @value[index_sym]
38
42
  end
39
43
 
40
44
  ##
@@ -44,8 +48,12 @@ class DynamicConfig
44
48
  # @param index The name of parameter being fetched
45
49
  # @param default_value The fallback value if the name cannot be found
46
50
  def get_typed(index, default_value)
47
- return default_value if @value.nil? || !@value.key?(index)
48
- return default_value if @value[index].class != default_value.class and default_value.class != TrueClass and default_value.class != FalseClass
49
- @value[index]
51
+ return default_value if @value.nil?
52
+
53
+ index_sym = index.to_sym
54
+ return default_value unless @value.key?(index_sym)
55
+
56
+ return default_value if @value[index_sym].class != default_value.class and default_value.class != TrueClass and default_value.class != FalseClass
57
+ @value[index_sym]
50
58
  end
51
59
  end
@@ -25,15 +25,26 @@ module EvaluationHelpers
25
25
  end
26
26
 
27
27
  def self.equal_string_in_array(array, value, ignore_case)
28
- return false if array.nil?
28
+ if array.is_a?(Hash)
29
+ return array.has_key?(value.to_sym)
30
+ end
29
31
 
30
32
  str_value = value.to_s
31
33
  str_value_downcased = nil
32
34
 
33
- if ignore_case
34
- return array.has_key?(value.to_s.downcase)
35
- else
36
- return array.has_key?(value.to_s)
35
+ return false if array.nil?
36
+
37
+ return array.any? do |item|
38
+ next false if item.nil?
39
+ item_str = item.to_s
40
+
41
+ next false unless item_str.length == str_value.length
42
+
43
+ return true if item_str == str_value
44
+ next false unless ignore_case
45
+
46
+ str_value_downcased ||= str_value.downcase
47
+ item_str.downcase == str_value_downcased
37
48
  end
38
49
  end
39
50