statsig 1.34.1 → 2.0.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: 3c2aff544b63f731dd32e2d2a498f83b91b66222f42d507f7ad483ce11b8cadf
4
- data.tar.gz: f7c1ec377a7ff7861696cb250817262bf999c8ba81904e6ed663a6e40fee9da0
3
+ metadata.gz: 27afba91b3e46ea8661d399ef0dc2696c6372bb214dbb993ea986e5b456edb7e
4
+ data.tar.gz: c06043637e4ce2ae3bbdef95ccc8a0713717b04e8149b46a0f70da502aa729db
5
5
  SHA512:
6
- metadata.gz: 1d42a4f31466ad9598bfa6e9fc59d768fe3821345ff2b1964385fef7a32a480819937994443145bb9b900c6a404611c69a80bc0399fd72c62b726a08a800dd05
7
- data.tar.gz: d6b3a2d232962779a8ab5de1d380bc89b28c64ee2ea5ca22c6d140a563cd096f9c5a965de712592d7c72007cf65c9bb853b86e0eaa86c78e49f4207395149759
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
@@ -14,7 +14,13 @@ module Statsig
14
14
  include_local_overrides: false
15
15
  )
16
16
  result = {}
17
+ target_app_id = evaluator.spec_store.get_app_id_for_sdk_key(client_sdk_key)
17
18
  entities.each do |name, spec|
19
+ config_target_apps = spec[:targetAppIDs]
20
+
21
+ unless target_app_id.nil? || (!config_target_apps.nil? && config_target_apps.include?(target_app_id))
22
+ next
23
+ end
18
24
  hashed_name, value = to_response(name, spec, evaluator, user, client_sdk_key, hash_algo, include_exposures, include_local_overrides)
19
25
  if !hashed_name.nil? && !value.nil?
20
26
  result[hashed_name] = value
@@ -25,25 +31,18 @@ module Statsig
25
31
  end
26
32
 
27
33
  def self.to_response(config_name, config_spec, evaluator, user, client_sdk_key, hash_algo, include_exposures, include_local_overrides)
28
-
29
- target_app_id = evaluator.spec_store.get_app_id_for_sdk_key(client_sdk_key)
30
- config_target_apps = config_spec.target_app_ids
31
-
32
- unless target_app_id.nil? || (!config_target_apps.nil? && config_target_apps.include?(target_app_id))
33
- return nil
34
- end
35
-
36
- category = config_spec.type
37
- entity_type = config_spec.entity
38
- 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
39
38
  return nil
40
39
  end
41
40
 
42
41
  if include_local_overrides
43
42
  case category
44
- when :feature_gate
43
+ when Const::TYPE_FEATURE_GATE
45
44
  local_override = evaluator.lookup_gate_override(config_name)
46
- when :dynamic_config
45
+ when Const::TYPE_DYNAMIC_CONFIG
47
46
  local_override = evaluator.lookup_config_override(config_name)
48
47
  end
49
48
  end
@@ -54,7 +53,7 @@ module Statsig
54
53
  disable_evaluation_details: true,
55
54
  disable_exposures: !include_exposures
56
55
  )
57
- evaluator.eval_spec(user, config_spec, eval_result)
56
+ evaluator.eval_spec(config_name_str, user, config_spec, eval_result)
58
57
  else
59
58
  eval_result = local_override
60
59
  end
@@ -67,10 +66,10 @@ module Statsig
67
66
  end
68
67
 
69
68
  case category
70
- when :feature_gate
69
+ when Const::TYPE_FEATURE_GATE
71
70
  result[:value] = eval_result.gate_value
72
- when :dynamic_config
73
- id_type = config_spec.id_type
71
+ when Const::TYPE_DYNAMIC_CONFIG
72
+ id_type = config_spec[:idType]
74
73
  result[:value] = eval_result.json_value
75
74
  result[:group] = eval_result.rule_id
76
75
  result[:is_device_based] = id_type.is_a?(String) && id_type.downcase == Statsig::Const::STABLEID
@@ -78,16 +77,16 @@ module Statsig
78
77
  return nil
79
78
  end
80
79
 
81
- if entity_type == :experiment
80
+ if entity_type == Const::TYPE_EXPERIMENT
82
81
  populate_experiment_fields(name, config_spec, eval_result, result, evaluator)
83
82
  end
84
83
 
85
- if entity_type == :layer
84
+ if entity_type == Const::TYPE_LAYER
86
85
  populate_layer_fields(config_spec, eval_result, result, evaluator, hash_algo, include_exposures)
87
86
  result.delete(:id_type) # not exposed for layer configs in /initialize
88
87
  end
89
88
 
90
- hashed_name = hash_name(config_name, hash_algo)
89
+ hashed_name = hash_name(config_name_str, hash_algo)
91
90
 
92
91
  result[:name] = hashed_name
93
92
  result[:rule_id] = eval_result.rule_id
@@ -101,14 +100,14 @@ module Statsig
101
100
 
102
101
  def self.populate_experiment_fields(config_name, config_spec, eval_result, result, evaluator)
103
102
  result[:is_user_in_experiment] = eval_result.is_experiment_group
104
- result[:is_experiment_active] = config_spec.is_active == true
103
+ result[:is_experiment_active] = config_spec[:isActive] == true
105
104
 
106
- if config_spec.has_shared_params != true
105
+ if config_spec[:hasSharedParams] != true
107
106
  return
108
107
  end
109
108
 
110
109
  result[:is_in_layer] = true
111
- result[:explicit_parameters] = config_spec.explicit_parameters || []
110
+ result[:explicit_parameters] = config_spec[:explicitParameters] || []
112
111
 
113
112
  layer_name = evaluator.spec_store.experiment_to_layer[config_name]
114
113
  if layer_name.nil? || evaluator.spec_store.layers[layer_name].nil?
@@ -121,15 +120,15 @@ module Statsig
121
120
 
122
121
  def self.populate_layer_fields(config_spec, eval_result, result, evaluator, hash_algo, include_exposures)
123
122
  delegate = eval_result.config_delegate
124
- result[:explicit_parameters] = config_spec.explicit_parameters || []
123
+ result[:explicit_parameters] = config_spec[:explicitParameters] || []
125
124
 
126
125
  if delegate.nil? == false && delegate.empty? == false
127
- delegate_spec = evaluator.spec_store.configs[delegate]
126
+ delegate_spec = evaluator.spec_store.configs[delegate.to_sym]
128
127
 
129
128
  result[:allocated_experiment_name] = hash_name(delegate, hash_algo)
130
129
  result[:is_user_in_experiment] = eval_result.is_experiment_group
131
- result[:is_experiment_active] = delegate_spec.is_active == true
132
- result[:explicit_parameters] = delegate_spec.explicit_parameters || []
130
+ result[:is_experiment_active] = delegate_spec[:isActive] == true
131
+ result[:explicit_parameters] = delegate_spec[:explicitParameters] || []
133
132
  end
134
133
 
135
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