statsig 1.30.0 → 1.32.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: 72119a11268473774b98457f89017155813cdaab4c19b8f07e1aad370ae6dd17
4
- data.tar.gz: 69cb96c04c6b9c322bb239332b8c406a6b543bb42b0cab6da9e641d44fa15371
3
+ metadata.gz: dd71ed81ad0f3658f5b6b480adba4a36f560357c67d6d9869b605e2d0e1e76c0
4
+ data.tar.gz: 59b7da08c4991da0e7b33dc08865612347442970932e71b8069bcd4518d50680
5
5
  SHA512:
6
- metadata.gz: d75c0a0d9c6529843c554d05b3ae49e6d863cd01da11f932e679bddddffab4287ef036af37c3cd8fff9310c10cd6758bc6e5435f78626245c5d1f315b91e09eb
7
- data.tar.gz: b9886d0b78d1491393cfacdea4c1f4cdeae1d2a993a0f5a0875011be1083bb63e4a7721fb3a007869eaa7eaf69ad300438d8038c099e88277958d4daf6d226f7
6
+ metadata.gz: 3775510747a856d9ae300ccad20a74de3cbe000bc3bca59c535c64cbab1bcb46a83069aa917299ee6dd7032f44206eecd39f3602c92f18c39a8ebbb57b45d9e9
7
+ data.tar.gz: e0a03db37029a260614a6eb8bf9edf1e2fe60f84d67fb9e027bf5fd72c3c4f969a7ab3145f55d0bd19f796b7d1733416547688e1076c69e066abd819c99b899c
data/lib/api_config.rb ADDED
@@ -0,0 +1,128 @@
1
+ require 'constants'
2
+
3
+ class UnsupportedConfigException < StandardError
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 initialize(name:, type:, is_active:, salt:, default_value:, enabled:, rules:, id_type:, entity:,
12
+ explicit_parameters: nil, has_shared_params: nil, target_app_ids: nil)
13
+ @name = name
14
+ @type = type.to_sym unless entity.nil?
15
+ @is_active = is_active
16
+ @salt = salt
17
+ @default_value = JSON.parse(JSON.generate(default_value))
18
+ @enabled = enabled
19
+ @rules = rules
20
+ @id_type = id_type
21
+ @entity = entity.to_sym unless entity.nil?
22
+ @explicit_parameters = explicit_parameters
23
+ @has_shared_params = has_shared_params
24
+ @target_app_ids = target_app_ids
25
+ end
26
+
27
+ def self.from_json(json)
28
+ new(
29
+ name: json[:name],
30
+ type: json[:type],
31
+ is_active: json[:isActive],
32
+ salt: json[:salt],
33
+ default_value: json[:defaultValue] || {},
34
+ enabled: json[:enabled],
35
+ rules: json[:rules]&.map do |rule|
36
+ APIRule.from_json(rule)
37
+ end,
38
+ id_type: json[:idType],
39
+ entity: json[:entity],
40
+ explicit_parameters: json[:explicitParameters],
41
+ has_shared_params: json[:hasSharedParams],
42
+ target_app_ids: json[:targetAppIDs]
43
+ )
44
+ end
45
+ end
46
+ end
47
+
48
+ module Statsig
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 initialize(name:, pass_percentage:, return_value:, id:, salt:, conditions:, id_type:,
55
+ group_name: nil, config_delegate: nil, is_experiment_group: nil)
56
+ @name = name
57
+ @pass_percentage = pass_percentage.to_f
58
+ @return_value = JSON.parse(JSON.generate(return_value))
59
+ @id = id
60
+ @salt = salt
61
+ @conditions = conditions
62
+ @id_type = id_type
63
+ @group_name = group_name
64
+ @config_delegate = config_delegate
65
+ @is_experiment_group = is_experiment_group
66
+ end
67
+
68
+ def self.from_json(json)
69
+ new(
70
+ name: json[:name],
71
+ pass_percentage: json[:passPercentage],
72
+ return_value: json[:returnValue] || {},
73
+ id: json[:id],
74
+ salt: json[:salt],
75
+ conditions: json[:conditions]&.map do |condition|
76
+ APICondition.from_json(condition)
77
+ end,
78
+ id_type: json[:idType],
79
+ group_name: json[:groupName],
80
+ config_delegate: json[:configDelegate],
81
+ is_experiment_group: json[:isExperimentGroup]
82
+ )
83
+ end
84
+ end
85
+ end
86
+
87
+ module Statsig
88
+ class APICondition
89
+
90
+ attr_accessor :type, :target_value, :operator, :field, :additional_values, :id_type
91
+
92
+ def initialize(type:, target_value:, operator:, field:, additional_values:, id_type:)
93
+ @type = type.to_sym unless type.nil?
94
+ @target_value = target_value
95
+ @operator = operator.to_sym unless operator.nil?
96
+ @field = field
97
+ @additional_values = additional_values || {}
98
+ @id_type = id_type
99
+ end
100
+
101
+ def self.from_json(json)
102
+ operator = json[:operator]
103
+ unless operator.nil?
104
+ operator = operator&.downcase&.to_sym
105
+ unless Const::SUPPORTED_OPERATORS.include?(operator)
106
+ raise UnsupportedConfigException
107
+ end
108
+ end
109
+
110
+ type = json[:type]
111
+ unless type.nil?
112
+ type = type&.downcase&.to_sym
113
+ unless Const::SUPPORTED_CONDITION_TYPES.include?(type)
114
+ raise UnsupportedConfigException
115
+ end
116
+ end
117
+
118
+ new(
119
+ type: json[:type],
120
+ target_value: json[:targetValue],
121
+ operator: json[:operator],
122
+ field: json[:field],
123
+ additional_values: json[:additionalValues],
124
+ id_type: json[:idType]
125
+ )
126
+ end
127
+ end
128
+ end
@@ -1,167 +1,138 @@
1
- # typed: true
2
-
3
1
  require_relative 'hash_utils'
4
- require 'sorbet-runtime'
5
2
 
6
- $empty_eval_result = {
7
- :gate_value => false,
8
- :json_value => {},
9
- :rule_id => "",
10
- :is_experiment_group => false,
11
- :secondary_exposures => []
12
- }
3
+ require 'constants'
13
4
 
14
- module ClientInitializeHelpers
5
+ module Statsig
15
6
  class ResponseFormatter
16
- extend T::Sig
17
-
18
- def initialize(evaluator, user, hash, client_sdk_key)
19
- @evaluator = evaluator
20
- @user = user
21
- @specs = evaluator.spec_store.get_raw_specs
22
- @hash = hash
23
- @client_sdk_key = client_sdk_key
24
- end
25
-
26
- def get_responses(key)
27
- @specs[key]
28
- .map { |name, spec| to_response(name, spec) }
29
- .delete_if { |v| v.nil? }.to_h
30
- end
31
-
32
- private
33
-
34
- sig { params(secondary_exposures: T::Array[T::Hash[String, String]]).returns(T::Array[T::Hash[String, String]]) }
35
- def filter_segments_from_secondary_exposures(secondary_exposures)
36
- secondary_exposures.reject do |exposure|
37
- exposure['gate'].to_s.start_with?('segment:')
7
+ def self.get_responses(entities, evaluator, user, client_sdk_key, hash_algo, include_exposures: true)
8
+ result = {}
9
+ entities.each do |name, spec|
10
+ hashed_name, value = to_response(name, spec, evaluator, user, client_sdk_key, hash_algo, include_exposures)
11
+ if !hashed_name.nil? && !value.nil?
12
+ result[hashed_name] = value
13
+ end
38
14
  end
15
+
16
+ result
39
17
  end
40
18
 
41
- def to_response(config_name, config_spec)
42
- target_app_id = @evaluator.spec_store.get_app_id_for_sdk_key(@client_sdk_key)
43
- config_target_apps = config_spec['targetAppIDs']
19
+ def self.to_response(config_name, config_spec, evaluator, user, client_sdk_key, hash_algo, include_exposures)
20
+ target_app_id = evaluator.spec_store.get_app_id_for_sdk_key(client_sdk_key)
21
+ config_target_apps = config_spec.target_app_ids
44
22
 
45
23
  unless target_app_id.nil? || (!config_target_apps.nil? && config_target_apps.include?(target_app_id))
46
24
  return nil
47
25
  end
48
26
 
49
- eval_result = @evaluator.eval_spec(@user, config_spec)
50
- if eval_result.nil?
27
+ category = config_spec.type
28
+ entity_type = config_spec.entity
29
+ if entity_type == :segment || entity_type == :holdout
51
30
  return nil
52
31
  end
53
32
 
54
- safe_eval_result = eval_result == $fetch_from_server ? $empty_eval_result : {
55
- :gate_value => eval_result.gate_value,
56
- :json_value => eval_result.json_value,
57
- :rule_id => eval_result.rule_id,
58
- :group_name => eval_result.group_name,
59
- :id_type => eval_result.id_type,
60
- :config_delegate => eval_result.config_delegate,
61
- :is_experiment_group => eval_result.is_experiment_group,
62
- :secondary_exposures => filter_segments_from_secondary_exposures(eval_result.secondary_exposures),
63
- :undelegated_sec_exps => eval_result.undelegated_sec_exps
64
- }
65
-
66
- category = config_spec['type']
67
- entity_type = config_spec['entity']
33
+ eval_result = ConfigResult.new(
34
+ name: config_name,
35
+ disable_evaluation_details: true,
36
+ disable_exposures: !include_exposures
37
+ )
38
+ evaluator.eval_spec(user, config_spec, eval_result)
68
39
 
69
40
  result = {}
70
41
 
71
- case category
72
-
73
- when 'feature_gate'
74
- if entity_type == 'segment' || entity_type == 'holdout'
75
- return nil
76
- end
42
+ result[:id_type] = eval_result.id_type
43
+ unless eval_result.group_name.nil?
44
+ result[:group_name] = eval_result.group_name
45
+ end
77
46
 
78
- result['value'] = safe_eval_result[:gate_value]
79
- result["group_name"] = safe_eval_result[:group_name]
80
- result["id_type"] = safe_eval_result[:id_type]
81
- when 'dynamic_config'
82
- id_type = config_spec['idType']
83
- result['value'] = safe_eval_result[:json_value]
84
- result["group"] = safe_eval_result[:rule_id]
85
- result["group_name"] = safe_eval_result[:group_name]
86
- result["id_type"] = safe_eval_result[:id_type]
87
- result["is_device_based"] = id_type.is_a?(String) && id_type.downcase == 'stableid'
47
+ case category
48
+ when :feature_gate
49
+ result[:value] = eval_result.gate_value
50
+ when :dynamic_config
51
+ id_type = config_spec.id_type
52
+ result[:value] = eval_result.json_value
53
+ result[:group] = eval_result.rule_id
54
+ result[:is_device_based] = id_type.is_a?(String) && id_type.downcase == Statsig::Const::STABLEID
88
55
  else
89
56
  return nil
90
57
  end
91
58
 
92
- if entity_type == 'experiment'
93
- populate_experiment_fields(config_name, config_spec, safe_eval_result, result)
59
+ if entity_type == :experiment
60
+ populate_experiment_fields(name, config_spec, eval_result, result, evaluator)
94
61
  end
95
62
 
96
- if entity_type == 'layer'
97
- populate_layer_fields(config_spec, safe_eval_result, result)
98
- result.delete('id_type') # not exposed for layer configs in /initialize
63
+ if entity_type == :layer
64
+ populate_layer_fields(config_spec, eval_result, result, evaluator, hash_algo, include_exposures)
65
+ result.delete(:id_type) # not exposed for layer configs in /initialize
99
66
  end
100
67
 
101
- hashed_name = hash_name(config_name)
102
- [hashed_name, result.merge(
103
- {
104
- "name" => hashed_name,
105
- "rule_id" => safe_eval_result[:rule_id],
106
- "secondary_exposures" => clean_exposures(safe_eval_result[:secondary_exposures])
107
- }).compact]
68
+ hashed_name = hash_name(config_name, hash_algo)
69
+
70
+ result[:name] = hashed_name
71
+ result[:rule_id] = eval_result.rule_id
72
+
73
+ if include_exposures
74
+ result[:secondary_exposures] = clean_exposures(eval_result.secondary_exposures)
75
+ end
76
+
77
+ [hashed_name, result]
108
78
  end
109
79
 
110
- def clean_exposures(exposures)
80
+ def self.clean_exposures(exposures)
111
81
  seen = {}
112
82
  exposures.reject do |exposure|
113
- key = "#{exposure["gate"]}|#{exposure["gateValue"]}|#{exposure["ruleID"]}}"
83
+ key = "#{exposure[:gate]}|#{exposure[:gateValue]}|#{exposure[:ruleID]}}"
114
84
  should_reject = seen[key]
115
85
  seen[key] = true
116
86
  should_reject == true
117
87
  end
118
88
  end
119
89
 
120
- def populate_experiment_fields(config_name, config_spec, eval_result, result)
121
- result["is_user_in_experiment"] = eval_result[:is_experiment_group]
122
- result["is_experiment_active"] = config_spec['isActive'] == true
90
+ def self.populate_experiment_fields(config_name, config_spec, eval_result, result, evaluator)
91
+ result[:is_user_in_experiment] = eval_result.is_experiment_group
92
+ result[:is_experiment_active] = config_spec.is_active == true
123
93
 
124
- if config_spec['hasSharedParams'] != true
94
+ if config_spec.has_shared_params != true
125
95
  return
126
96
  end
127
97
 
128
- result["is_in_layer"] = true
129
- result["explicit_parameters"] = config_spec["explicitParameters"] || []
98
+ result[:is_in_layer] = true
99
+ result[:explicit_parameters] = config_spec.explicit_parameters || []
130
100
 
131
- layer_name = @specs[:experiment_to_layer][config_name]
132
- if layer_name.nil? || @specs[:layers][layer_name].nil?
101
+ layer_name = evaluator.spec_store.experiment_to_layer[config_name]
102
+ if layer_name.nil? || evaluator.spec_store.layers[layer_name].nil?
133
103
  return
134
104
  end
135
105
 
136
- layer = @specs[:layers][layer_name]
137
- result["value"] = layer["defaultValue"].merge(result["value"])
106
+ layer = evaluator.spec_store.layers[layer_name]
107
+ result[:value] = layer[:defaultValue].merge(result[:value])
138
108
  end
139
109
 
140
- def populate_layer_fields(config_spec, eval_result, result)
141
- delegate = eval_result[:config_delegate]
142
- result["explicit_parameters"] = config_spec["explicitParameters"] || []
110
+ def self.populate_layer_fields(config_spec, eval_result, result, evaluator, hash_algo, include_exposures)
111
+ delegate = eval_result.config_delegate
112
+ result[:explicit_parameters] = config_spec.explicit_parameters || []
143
113
 
144
114
  if delegate.nil? == false && delegate.empty? == false
145
- delegate_spec = @specs[:configs][delegate]
146
- delegate_result = @evaluator.eval_spec(@user, delegate_spec)
115
+ delegate_spec = evaluator.spec_store.configs[delegate]
147
116
 
148
- result["allocated_experiment_name"] = hash_name(delegate)
149
- result["is_user_in_experiment"] = delegate_result.is_experiment_group
150
- result["is_experiment_active"] = delegate_spec['isActive'] == true
151
- result["explicit_parameters"] = delegate_spec["explicitParameters"] || []
117
+ result[:allocated_experiment_name] = hash_name(delegate, hash_algo)
118
+ result[:is_user_in_experiment] = eval_result.is_experiment_group
119
+ result[:is_experiment_active] = delegate_spec.is_active == true
120
+ result[:explicit_parameters] = delegate_spec.explicit_parameters || []
152
121
  end
153
122
 
154
- result["undelegated_secondary_exposures"] = clean_exposures(eval_result[:undelegated_sec_exps] || [])
123
+ if include_exposures
124
+ result[:undelegated_secondary_exposures] = clean_exposures(eval_result.undelegated_sec_exps || [])
125
+ end
155
126
  end
156
127
 
157
- def hash_name(name)
158
- case @hash
159
- when 'none'
128
+ def self.hash_name(name, hash_algo)
129
+ case hash_algo
130
+ when Statsig::Const::NONE
160
131
  return name
161
- when 'sha256'
162
- return Statsig::HashUtils.sha256(name)
163
- when 'djb2'
132
+ when Statsig::Const::DJB2
164
133
  return Statsig::HashUtils.djb2(name)
134
+ else
135
+ return Statsig::HashUtils.sha256(name)
165
136
  end
166
137
  end
167
138
  end
data/lib/config_result.rb CHANGED
@@ -1,10 +1,5 @@
1
- # typed: true
2
-
3
- require 'sorbet-runtime'
4
-
5
1
  module Statsig
6
2
  class ConfigResult
7
- extend T::Sig
8
3
 
9
4
  attr_accessor :name
10
5
  attr_accessor :gate_value
@@ -19,25 +14,30 @@ module Statsig
19
14
  attr_accessor :group_name
20
15
  attr_accessor :id_type
21
16
  attr_accessor :target_app_ids
17
+ attr_accessor :disable_evaluation_details
18
+ attr_accessor :disable_exposures
22
19
 
23
20
  def initialize(
24
- name,
25
- gate_value = false,
26
- json_value = {},
27
- rule_id = '',
28
- secondary_exposures = [],
29
- config_delegate = nil,
30
- explicit_parameters = [],
21
+ name:,
22
+ gate_value: false,
23
+ json_value: nil,
24
+ rule_id: nil,
25
+ secondary_exposures: [],
26
+ config_delegate: nil,
27
+ explicit_parameters: nil,
31
28
  is_experiment_group: false,
32
29
  evaluation_details: nil,
33
30
  group_name: nil,
34
- id_type: '',
35
- target_app_ids: nil)
31
+ id_type: nil,
32
+ target_app_ids: nil,
33
+ disable_evaluation_details: false,
34
+ disable_exposures: false
35
+ )
36
36
  @name = name
37
37
  @gate_value = gate_value
38
38
  @json_value = json_value
39
39
  @rule_id = rule_id
40
- @secondary_exposures = secondary_exposures.is_a?(Array) ? secondary_exposures : []
40
+ @secondary_exposures = secondary_exposures
41
41
  @undelegated_sec_exps = @secondary_exposures
42
42
  @config_delegate = config_delegate
43
43
  @explicit_parameters = explicit_parameters
@@ -46,9 +46,10 @@ module Statsig
46
46
  @group_name = group_name
47
47
  @id_type = id_type
48
48
  @target_app_ids = target_app_ids
49
+ @disable_evaluation_details = disable_evaluation_details
50
+ @disable_exposures = disable_exposures
49
51
  end
50
52
 
51
- sig { params(config_name: String, user_persisted_values: UserPersistedValues).returns(T.nilable(ConfigResult)) }
52
53
  def self.from_user_persisted_values(config_name, user_persisted_values)
53
54
  sticky_values = user_persisted_values[config_name]
54
55
  return nil if sticky_values.nil?
@@ -56,22 +57,6 @@ module Statsig
56
57
  from_hash(config_name, sticky_values)
57
58
  end
58
59
 
59
- sig { params(config_name: String, hash: Hash).returns(ConfigResult) }
60
- def self.from_hash(config_name, hash)
61
- new(
62
- config_name,
63
- hash['gate_value'],
64
- hash['json_value'],
65
- hash['rule_id'],
66
- hash['secondary_exposures'],
67
- evaluation_details: EvaluationDetails.persisted(hash['config_sync_time'], hash['init_time']),
68
- group_name: hash['group_name'],
69
- id_type: hash['id_type'],
70
- target_app_ids: hash['target_app_ids']
71
- )
72
- end
73
-
74
- sig { returns(Hash) }
75
60
  def to_hash
76
61
  {
77
62
  json_value: @json_value,
data/lib/constants.rb ADDED
@@ -0,0 +1,60 @@
1
+ module Statsig
2
+ module Const
3
+ EMPTY_STR = ''.freeze
4
+
5
+ SUPPORTED_CONDITION_TYPES = Set.new(%i[
6
+ public fail_gate pass_gate ip_based ua_based user_field
7
+ environment_field current_time user_bucket unit_id
8
+ ]).freeze
9
+
10
+ SUPPORTED_OPERATORS = Set.new(%i[
11
+ gt gte lt lte version_gt version_gte version_lt version_lte
12
+ version_eq version_neq any none any_case_sensitive none_case_sensitive
13
+ str_starts_with_any str_ends_with_any str_contains_any str_contains_none
14
+ str_matches eq neq before after on in_segment_list not_in_segment_list
15
+ ]).freeze
16
+
17
+ APP_VERSION = 'app_version'.freeze
18
+ APPVERSION = 'appversion'.freeze
19
+ BROWSER_NAME = 'browser_name'.freeze
20
+ BROWSER_VERSION = 'browser_version'.freeze
21
+ BROWSERNAME = 'browsername'.freeze
22
+ BROWSERVERSION = 'browserversion'.freeze
23
+ CML_SHA_256 = 'sha256'.freeze
24
+ CML_USER_ID = 'userID'.freeze
25
+ COUNTRY = 'country'.freeze
26
+ DEFAULT = 'default'.freeze
27
+ DISABLED = 'disabled'.freeze
28
+ DJB2 = 'djb2'.freeze
29
+ EMAIL = 'email'.freeze
30
+ FALSE = 'false'.freeze
31
+ IP = 'ip'.freeze
32
+ LAYER = :layer
33
+ LOCALE = 'locale'.freeze
34
+ NONE = 'none'.freeze
35
+ OS_NAME = 'os_name'.freeze
36
+ OS_VERSION = 'os_version'.freeze
37
+ OSNAME = 'osname'.freeze
38
+ OSVERSION = 'osversion'.freeze
39
+ OVERRIDE = 'override'.freeze
40
+ Q_RIGHT_CHEVRON = 'Q>'.freeze
41
+ STABLEID = 'stableid'.freeze
42
+ STATSIG_RUBY_SDK = 'statsig-ruby-sdk'.freeze
43
+ TRUE = 'true'.freeze
44
+ USER_AGENT = 'user_agent'.freeze
45
+ USER_ID = 'user_id'.freeze
46
+ USERAGENT = 'useragent'.freeze
47
+ USERID = 'userid'.freeze
48
+
49
+ # Persisted Evaluations
50
+ GATE_VALUE = 'gate_value'.freeze
51
+ JSON_VALUE = 'json_value'.freeze
52
+ RULE_ID = 'rule_id'.freeze
53
+ SECONDARY_EXPOSURES = 'secondary_exposures'.freeze
54
+ GROUP_NAME = 'group_name'.freeze
55
+ ID_TYPE = 'id_type'.freeze
56
+ TARGET_APP_IDS = 'target_app_ids'.freeze
57
+ CONFIG_SYNC_TIME = 'config_sync_time'.freeze
58
+ INIT_TIME = 'init_time'.freeze
59
+ end
60
+ end
data/lib/diagnostics.rb CHANGED
@@ -1,36 +1,15 @@
1
- # typed: true
2
-
3
- require 'sorbet-runtime'
4
-
5
1
  module Statsig
6
2
  class Diagnostics
7
- extend T::Sig
8
-
9
- sig { returns(String) }
10
- attr_accessor :context
11
-
12
- sig { returns(T::Array[T::Hash[Symbol, T.untyped]]) }
13
3
  attr_reader :markers
14
4
 
15
- sig { returns(T::Hash[String, Numeric]) }
16
5
  attr_accessor :sample_rates
17
6
 
18
- def initialize(context)
19
- @context = context
20
- @markers = []
7
+ def initialize()
8
+ @markers = {:initialize => [], :api_call => [], :config_sync => []}
21
9
  @sample_rates = {}
22
10
  end
23
11
 
24
- sig do
25
- params(
26
- key: String,
27
- action: String,
28
- step: T.any(String, NilClass),
29
- tags: T::Hash[Symbol, T.untyped]
30
- ).void
31
- end
32
-
33
- def mark(key, action, step, tags)
12
+ def mark(key, action, step, tags, context)
34
13
  marker = {
35
14
  key: key,
36
15
  action: action,
@@ -44,87 +23,59 @@ module Statsig
44
23
  marker[key] = val
45
24
  end
46
25
  end
47
- @markers.push(marker)
26
+ if @markers[context].nil?
27
+ @markers[context] = []
28
+ end
29
+ @markers[context].push(marker)
48
30
  end
49
31
 
50
- sig do
51
- params(
52
- key: String,
53
- step: T.any(String, NilClass),
54
- tags: T::Hash[Symbol, T.untyped]
55
- ).returns(Tracker)
56
- end
57
- def track(key, step = nil, tags = {})
58
- tracker = Tracker.new(self, key, step, tags)
32
+ def track(context, key, step = nil, tags = {})
33
+ tracker = Tracker.new(self, context, key, step, tags)
59
34
  tracker.start(**tags)
60
35
  tracker
61
36
  end
62
37
 
63
- sig { returns(T::Hash[Symbol, T.untyped]) }
64
-
65
- def serialize
66
- {
67
- context: @context.clone,
68
- markers: @markers.clone
69
- }
70
- end
71
-
72
- def serialize_with_sampling
73
- marker_keys = @markers.map { |e| e[:key] }
38
+ def serialize_with_sampling(context)
39
+ marker_keys = @markers[context].map { |e| e[:key] }
74
40
  unique_marker_keys = marker_keys.uniq { |e| e }
75
41
  sampled_marker_keys = unique_marker_keys.select do |key|
76
42
  @sample_rates.key?(key) && !self.class.sample(@sample_rates[key])
77
43
  end
78
- final_markers = @markers.select do |marker|
44
+ final_markers = @markers[context].select do |marker|
79
45
  !sampled_marker_keys.include?(marker[:key])
80
46
  end
81
47
  {
82
- context: @context.clone,
83
- markers: final_markers
48
+ context: context.clone,
49
+ markers: final_markers.clone
84
50
  }
85
51
  end
86
52
 
87
- def clear_markers
88
- @markers.clear
53
+ def clear_markers(context)
54
+ @markers[context].clear
89
55
  end
90
56
 
91
57
  def self.sample(rate_over_ten_thousand)
92
58
  rand * 10_000 < rate_over_ten_thousand
93
59
  end
94
60
 
95
- class Context
96
- INITIALIZE = 'initialize'.freeze
97
- CONFIG_SYNC = 'config_sync'.freeze
98
- API_CALL = 'api_call'.freeze
99
- end
100
-
101
61
  API_CALL_KEYS = %w[check_gate get_config get_experiment get_layer].freeze
102
62
 
103
63
  class Tracker
104
- extend T::Sig
105
-
106
- sig do
107
- params(
108
- diagnostics: Diagnostics,
109
- key: String,
110
- step: T.any(String, NilClass),
111
- tags: T::Hash[Symbol, T.untyped]
112
- ).void
113
- end
114
- def initialize(diagnostics, key, step, tags = {})
64
+ def initialize(diagnostics, context, key, step, tags = {})
115
65
  @diagnostics = diagnostics
66
+ @context = context
116
67
  @key = key
117
68
  @step = step
118
69
  @tags = tags
119
70
  end
120
71
 
121
72
  def start(**tags)
122
- @diagnostics.mark(@key, 'start', @step, tags.nil? ? {} : tags.merge(@tags))
73
+ @diagnostics.mark(@key, 'start', @step, tags.nil? ? {} : tags.merge(@tags), @context)
123
74
  end
124
75
 
125
76
  def end(**tags)
126
- @diagnostics.mark(@key, 'end', @step, tags.nil? ? {} : tags.merge(@tags))
77
+ @diagnostics.mark(@key, 'end', @step, tags.nil? ? {} : tags.merge(@tags), @context)
127
78
  end
128
79
  end
129
80
  end
130
- end
81
+ end