statsig 1.25.2 → 1.33.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: c7f3c8522d7d8062daf248fd667fd0d8152a4194339fde799814a5da44812eda
4
- data.tar.gz: 1fd5e67d4a75ab3b5b6c92c780841252a4610e18bc13310f9b11dbb583aaf16a
3
+ metadata.gz: 56f4bfc568fa70750a5dc1376ffffacd1163344c63750492e0e0ff841fd73d18
4
+ data.tar.gz: 6e41d012096437a58a9ceea49c0aa5d71532d06d273b37593afcd7cb9bc07297
5
5
  SHA512:
6
- metadata.gz: 8f516ca6d221c9d789f1f358b550edeedf4203c01ead45374b97a3b81f4205d09f1c7c3e0214516cdcad314bacbdbde39a5df2053779679fe4df593c974c8300
7
- data.tar.gz: a90846ef3c050c7c0b8bddbabd19ed7bff6236a15ed941f55d1a305f4c9cb5ff35f595a3d7ea4ab1560d7f7bae11c669c29d17f8041da9125524669703dc5370
6
+ metadata.gz: 7060859db57bed3e98e372c1b3808f53802bb1c7e4e76f70b366e138c022d1aef522605457d8e69f8eb029394c8bb8c32e1aa1eb0933d4fcd8746f60452afac3
7
+ data.tar.gz: a0857b56731a9fab6d53748700a932fc3d6a3c7b065dd8ce54d03bf89274002f215e6ae39fa0b72d49c60cb4c7cc11307f7e4d3b1f965f7f4f5a721545712978
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,132 +1,160 @@
1
- # typed: true
2
- $empty_eval_result = {
3
- :gate_value => false,
4
- :json_value => {},
5
- :rule_id => "",
6
- :is_experiment_group => false,
7
- :secondary_exposures => []
8
- }
9
-
10
- module ClientInitializeHelpers
1
+ require_relative 'hash_utils'
2
+
3
+ require 'constants'
4
+
5
+ module Statsig
11
6
  class ResponseFormatter
12
- def initialize(evaluator, user)
13
- @evaluator = evaluator
14
- @user = user
15
- @specs = evaluator.spec_store.get_raw_specs
16
- end
7
+ def self.get_responses(
8
+ entities,
9
+ evaluator,
10
+ user,
11
+ client_sdk_key,
12
+ hash_algo,
13
+ include_exposures: true,
14
+ include_local_overrides: false
15
+ )
16
+ result = {}
17
+ entities.each do |name, spec|
18
+ hashed_name, value = to_response(name, spec, evaluator, user, client_sdk_key, hash_algo, include_exposures, include_local_overrides)
19
+ if !hashed_name.nil? && !value.nil?
20
+ result[hashed_name] = value
21
+ end
22
+ end
17
23
 
18
- def get_responses(key)
19
- @specs[key]
20
- .map { |name, spec| to_response(name, spec) }
21
- .delete_if { |v| v.nil? }.to_h
24
+ result
22
25
  end
23
26
 
24
- private
27
+ def self.to_response(config_name, config_spec, evaluator, user, client_sdk_key, hash_algo, include_exposures, include_local_overrides)
28
+ target_app_id = evaluator.spec_store.get_app_id_for_sdk_key(client_sdk_key)
29
+ config_target_apps = config_spec.target_app_ids
25
30
 
26
- def to_response(config_name, config_spec)
27
- eval_result = @evaluator.eval_spec(@user, config_spec)
28
- if eval_result.nil?
31
+ unless target_app_id.nil? || (!config_target_apps.nil? && config_target_apps.include?(target_app_id))
29
32
  return nil
30
33
  end
31
34
 
32
- safe_eval_result = eval_result == $fetch_from_server ? $empty_eval_result : {
33
- :gate_value => eval_result.gate_value,
34
- :json_value => eval_result.json_value,
35
- :rule_id => eval_result.rule_id,
36
- :config_delegate => eval_result.config_delegate,
37
- :is_experiment_group => eval_result.is_experiment_group,
38
- :secondary_exposures => eval_result.secondary_exposures,
39
- :undelegated_sec_exps => eval_result.undelegated_sec_exps
40
- }
35
+ category = config_spec.type
36
+ entity_type = config_spec.entity
37
+ if entity_type == :segment || entity_type == :holdout
38
+ return nil
39
+ end
41
40
 
42
- category = config_spec['type']
43
- entity_type = config_spec['entity']
41
+ if include_local_overrides
42
+ case category
43
+ when :feature_gate
44
+ local_override = evaluator.lookup_gate_override(config_name)
45
+ when :dynamic_config
46
+ local_override = evaluator.lookup_config_override(config_name)
47
+ end
48
+ end
44
49
 
45
- result = {}
50
+ if local_override.nil?
51
+ eval_result = ConfigResult.new(
52
+ name: config_name,
53
+ disable_evaluation_details: true,
54
+ disable_exposures: !include_exposures
55
+ )
56
+ evaluator.eval_spec(user, config_spec, eval_result)
57
+ else
58
+ eval_result = local_override
59
+ end
46
60
 
47
- case category
61
+ result = {}
48
62
 
49
- when 'feature_gate'
50
- if entity_type == 'segment' || entity_type == 'holdout'
51
- return nil
52
- end
63
+ result[:id_type] = eval_result.id_type
64
+ unless eval_result.group_name.nil?
65
+ result[:group_name] = eval_result.group_name
66
+ end
53
67
 
54
- result['value'] = safe_eval_result[:gate_value]
55
- when 'dynamic_config'
56
- id_type = config_spec['idType']
57
- result['value'] = safe_eval_result[:json_value]
58
- result["group"] = safe_eval_result[:rule_id]
59
- result["is_device_based"] = id_type.is_a?(String) && id_type.downcase == 'stableid'
68
+ case category
69
+ when :feature_gate
70
+ result[:value] = eval_result.gate_value
71
+ when :dynamic_config
72
+ id_type = config_spec.id_type
73
+ result[:value] = eval_result.json_value
74
+ result[:group] = eval_result.rule_id
75
+ result[:is_device_based] = id_type.is_a?(String) && id_type.downcase == Statsig::Const::STABLEID
60
76
  else
61
77
  return nil
62
78
  end
63
79
 
64
- if entity_type == 'experiment'
65
- populate_experiment_fields(config_name, config_spec, safe_eval_result, result)
80
+ if entity_type == :experiment
81
+ populate_experiment_fields(name, config_spec, eval_result, result, evaluator)
82
+ end
83
+
84
+ if entity_type == :layer
85
+ populate_layer_fields(config_spec, eval_result, result, evaluator, hash_algo, include_exposures)
86
+ result.delete(:id_type) # not exposed for layer configs in /initialize
66
87
  end
67
88
 
68
- if entity_type == 'layer'
69
- populate_layer_fields(config_spec, safe_eval_result, result)
89
+ hashed_name = hash_name(config_name, hash_algo)
90
+
91
+ result[:name] = hashed_name
92
+ result[:rule_id] = eval_result.rule_id
93
+
94
+ if include_exposures
95
+ result[:secondary_exposures] = clean_exposures(eval_result.secondary_exposures)
70
96
  end
71
97
 
72
- hashed_name = hash_name(config_name)
73
- [hashed_name, result.merge(
74
- {
75
- "name" => hashed_name,
76
- "rule_id" => safe_eval_result[:rule_id],
77
- "secondary_exposures" => clean_exposures(safe_eval_result[:secondary_exposures])
78
- })]
98
+ [hashed_name, result]
79
99
  end
80
100
 
81
- def clean_exposures(exposures)
101
+ def self.clean_exposures(exposures)
82
102
  seen = {}
83
103
  exposures.reject do |exposure|
84
- key = "#{exposure["gate"]}|#{exposure["gateValue"]}|#{exposure["ruleID"]}}"
104
+ key = "#{exposure[:gate]}|#{exposure[:gateValue]}|#{exposure[:ruleID]}}"
85
105
  should_reject = seen[key]
86
106
  seen[key] = true
87
107
  should_reject == true
88
108
  end
89
109
  end
90
110
 
91
- def populate_experiment_fields(config_name, config_spec, eval_result, result)
92
- result["is_user_in_experiment"] = eval_result[:is_experiment_group]
93
- result["is_experiment_active"] = config_spec['isActive'] == true
111
+ def self.populate_experiment_fields(config_name, config_spec, eval_result, result, evaluator)
112
+ result[:is_user_in_experiment] = eval_result.is_experiment_group
113
+ result[:is_experiment_active] = config_spec.is_active == true
94
114
 
95
- if config_spec['hasSharedParams'] != true
115
+ if config_spec.has_shared_params != true
96
116
  return
97
117
  end
98
118
 
99
- result["is_in_layer"] = true
100
- result["explicit_parameters"] = config_spec["explicitParameters"] || []
119
+ result[:is_in_layer] = true
120
+ result[:explicit_parameters] = config_spec.explicit_parameters || []
101
121
 
102
- layer_name = @specs[:experiment_to_layer][config_name]
103
- if layer_name.nil? || @specs[:layers][layer_name].nil?
122
+ layer_name = evaluator.spec_store.experiment_to_layer[config_name]
123
+ if layer_name.nil? || evaluator.spec_store.layers[layer_name].nil?
104
124
  return
105
125
  end
106
126
 
107
- layer = @specs[:layers][layer_name]
108
- result["value"] = layer["defaultValue"].merge(result["value"])
127
+ layer = evaluator.spec_store.layers[layer_name]
128
+ result[:value] = layer[:defaultValue].merge(result[:value])
109
129
  end
110
130
 
111
- def populate_layer_fields(config_spec, eval_result, result)
112
- delegate = eval_result[:config_delegate]
113
- result["explicit_parameters"] = config_spec["explicitParameters"] || []
131
+ def self.populate_layer_fields(config_spec, eval_result, result, evaluator, hash_algo, include_exposures)
132
+ delegate = eval_result.config_delegate
133
+ result[:explicit_parameters] = config_spec.explicit_parameters || []
114
134
 
115
135
  if delegate.nil? == false && delegate.empty? == false
116
- delegate_spec = @specs[:configs][delegate]
117
- delegate_result = @evaluator.eval_spec(@user, delegate_spec)
136
+ delegate_spec = evaluator.spec_store.configs[delegate]
118
137
 
119
- result["allocated_experiment_name"] = hash_name(delegate)
120
- result["is_user_in_experiment"] = delegate_result.is_experiment_group
121
- result["is_experiment_active"] = delegate_spec['isActive'] == true
122
- result["explicit_parameters"] = delegate_spec["explicitParameters"] || []
138
+ result[:allocated_experiment_name] = hash_name(delegate, hash_algo)
139
+ result[:is_user_in_experiment] = eval_result.is_experiment_group
140
+ result[:is_experiment_active] = delegate_spec.is_active == true
141
+ result[:explicit_parameters] = delegate_spec.explicit_parameters || []
123
142
  end
124
143
 
125
- result["undelegated_secondary_exposures"] = clean_exposures(eval_result[:undelegated_sec_exps] || [])
144
+ if include_exposures
145
+ result[:undelegated_secondary_exposures] = clean_exposures(eval_result.undelegated_sec_exps || [])
146
+ end
126
147
  end
127
148
 
128
- def hash_name(name)
129
- Digest::SHA256.base64digest(name)
149
+ def self.hash_name(name, hash_algo)
150
+ case hash_algo
151
+ when Statsig::Const::NONE
152
+ return name
153
+ when Statsig::Const::DJB2
154
+ return Statsig::HashUtils.djb2(name)
155
+ else
156
+ return Statsig::HashUtils.sha256(name)
157
+ end
130
158
  end
131
159
  end
132
- end
160
+ end
data/lib/config_result.rb CHANGED
@@ -1,6 +1,6 @@
1
- # typed: true
2
1
  module Statsig
3
2
  class ConfigResult
3
+
4
4
  attr_accessor :name
5
5
  attr_accessor :gate_value
6
6
  attr_accessor :json_value
@@ -13,24 +13,31 @@ module Statsig
13
13
  attr_accessor :evaluation_details
14
14
  attr_accessor :group_name
15
15
  attr_accessor :id_type
16
+ attr_accessor :target_app_ids
17
+ attr_accessor :disable_evaluation_details
18
+ attr_accessor :disable_exposures
16
19
 
17
20
  def initialize(
18
- name,
19
- gate_value = false,
20
- json_value = {},
21
- rule_id = '',
22
- secondary_exposures = [],
23
- config_delegate = '',
24
- 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,
25
28
  is_experiment_group: false,
26
29
  evaluation_details: nil,
27
30
  group_name: nil,
28
- id_type: '')
31
+ id_type: nil,
32
+ target_app_ids: nil,
33
+ disable_evaluation_details: false,
34
+ disable_exposures: false
35
+ )
29
36
  @name = name
30
37
  @gate_value = gate_value
31
38
  @json_value = json_value
32
39
  @rule_id = rule_id
33
- @secondary_exposures = secondary_exposures.is_a?(Array) ? secondary_exposures : []
40
+ @secondary_exposures = secondary_exposures
34
41
  @undelegated_sec_exps = @secondary_exposures
35
42
  @config_delegate = config_delegate
36
43
  @explicit_parameters = explicit_parameters
@@ -38,6 +45,30 @@ module Statsig
38
45
  @evaluation_details = evaluation_details
39
46
  @group_name = group_name
40
47
  @id_type = id_type
48
+ @target_app_ids = target_app_ids
49
+ @disable_evaluation_details = disable_evaluation_details
50
+ @disable_exposures = disable_exposures
51
+ end
52
+
53
+ def self.from_user_persisted_values(config_name, user_persisted_values)
54
+ sticky_values = user_persisted_values[config_name]
55
+ return nil if sticky_values.nil?
56
+
57
+ from_hash(config_name, sticky_values)
58
+ end
59
+
60
+ def to_hash
61
+ {
62
+ json_value: @json_value,
63
+ gate_value: @gate_value,
64
+ rule_id: @rule_id,
65
+ secondary_exposures: @secondary_exposures,
66
+ config_sync_time: @evaluation_details.config_sync_time,
67
+ init_time: @init_time,
68
+ group_name: @group_name,
69
+ id_type: @id_type,
70
+ target_app_ids: @target_app_ids
71
+ }
41
72
  end
42
73
  end
43
- end
74
+ end
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,103 +1,81 @@
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_reader :context
11
-
12
- sig { returns(T::Array[T::Hash[Symbol, T.untyped]]) }
13
3
  attr_reader :markers
14
4
 
15
- def initialize(context)
16
- @context = context
17
- @markers = []
18
- end
5
+ attr_accessor :sample_rates
19
6
 
20
- sig do
21
- params(
22
- key: String,
23
- action: String,
24
- step: T.any(String, NilClass),
25
- value: T.any(String, Integer, T::Boolean, NilClass),
26
- metadata: T.any(T::Hash[Symbol, T.untyped], NilClass)
27
- ).void
7
+ def initialize()
8
+ @markers = {:initialize => [], :api_call => [], :config_sync => []}
9
+ @sample_rates = {}
28
10
  end
29
11
 
30
- def mark(key, action, step = nil, value = nil, metadata = nil)
31
- @markers.push({
32
- key: key,
33
- step: step,
34
- action: action,
35
- value: value,
36
- metadata: metadata,
37
- timestamp: (Time.now.to_f * 1000).to_i
38
- })
12
+ def mark(key, action, step, tags, context)
13
+ marker = {
14
+ key: key,
15
+ action: action,
16
+ timestamp: (Time.now.to_f * 1000).to_i
17
+ }
18
+ if !step.nil?
19
+ marker[:step] = step
20
+ end
21
+ tags.each do |key, val|
22
+ unless val.nil?
23
+ marker[key] = val
24
+ end
25
+ end
26
+ if @markers[context].nil?
27
+ @markers[context] = []
28
+ end
29
+ @markers[context].push(marker)
39
30
  end
40
31
 
41
- sig do
42
- params(
43
- key: String,
44
- step: T.any(String, NilClass),
45
- value: T.any(String, Integer, T::Boolean, NilClass),
46
- metadata: T.any(T::Hash[Symbol, T.untyped], NilClass)
47
- ).returns(Tracker)
48
- end
49
- def track(key, step = nil, value = nil, metadata = nil)
50
- tracker = Tracker.new(self, key, step, metadata)
51
- tracker.start(value)
32
+ def track(context, key, step = nil, tags = {})
33
+ tracker = Tracker.new(self, context, key, step, tags)
34
+ tracker.start(**tags)
52
35
  tracker
53
36
  end
54
37
 
55
- sig { returns(T::Hash[Symbol, T.untyped]) }
56
-
57
- def serialize
38
+ def serialize_with_sampling(context)
39
+ marker_keys = @markers[context].map { |e| e[:key] }
40
+ unique_marker_keys = marker_keys.uniq { |e| e }
41
+ sampled_marker_keys = unique_marker_keys.select do |key|
42
+ @sample_rates.key?(key) && !self.class.sample(@sample_rates[key])
43
+ end
44
+ final_markers = @markers[context].select do |marker|
45
+ !sampled_marker_keys.include?(marker[:key])
46
+ end
58
47
  {
59
- context: @context.clone,
60
- markers: @markers.clone
48
+ context: context.clone,
49
+ markers: final_markers.clone
61
50
  }
62
51
  end
63
52
 
64
- def clear_markers
65
- @markers.clear
53
+ def clear_markers(context)
54
+ @markers[context].clear
66
55
  end
67
56
 
68
- class Context
69
- INITIALIZE = 'initialize'.freeze
70
- CONFIG_SYNC = 'config_sync'.freeze
71
- API_CALL = 'api_call'.freeze
57
+ def self.sample(rate_over_ten_thousand)
58
+ rand * 10_000 < rate_over_ten_thousand
72
59
  end
73
60
 
74
61
  API_CALL_KEYS = %w[check_gate get_config get_experiment get_layer].freeze
75
62
 
76
63
  class Tracker
77
- extend T::Sig
78
-
79
- sig do
80
- params(
81
- diagnostics: Diagnostics,
82
- key: String,
83
- step: T.any(String, NilClass),
84
- metadata: T.any(T::Hash[Symbol, T.untyped], NilClass)
85
- ).void
86
- end
87
- def initialize(diagnostics, key, step, metadata)
64
+ def initialize(diagnostics, context, key, step, tags = {})
88
65
  @diagnostics = diagnostics
66
+ @context = context
89
67
  @key = key
90
68
  @step = step
91
- @metadata = metadata
69
+ @tags = tags
92
70
  end
93
71
 
94
- def start(value = nil)
95
- @diagnostics.mark(@key, 'start', @step, value, @metadata)
72
+ def start(**tags)
73
+ @diagnostics.mark(@key, 'start', @step, tags.nil? ? {} : tags.merge(@tags), @context)
96
74
  end
97
75
 
98
- def end(value = nil)
99
- @diagnostics.mark(@key, 'end', @step, value, @metadata)
76
+ def end(**tags)
77
+ @diagnostics.mark(@key, 'end', @step, tags.nil? ? {} : tags.merge(@tags), @context)
100
78
  end
101
79
  end
102
80
  end
103
- end
81
+ end