statsig 1.25.2 → 1.30.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: 72119a11268473774b98457f89017155813cdaab4c19b8f07e1aad370ae6dd17
4
+ data.tar.gz: 69cb96c04c6b9c322bb239332b8c406a6b543bb42b0cab6da9e641d44fa15371
5
5
  SHA512:
6
- metadata.gz: 8f516ca6d221c9d789f1f358b550edeedf4203c01ead45374b97a3b81f4205d09f1c7c3e0214516cdcad314bacbdbde39a5df2053779679fe4df593c974c8300
7
- data.tar.gz: a90846ef3c050c7c0b8bddbabd19ed7bff6236a15ed941f55d1a305f4c9cb5ff35f595a3d7ea4ab1560d7f7bae11c669c29d17f8041da9125524669703dc5370
6
+ metadata.gz: d75c0a0d9c6529843c554d05b3ae49e6d863cd01da11f932e679bddddffab4287ef036af37c3cd8fff9310c10cd6758bc6e5435f78626245c5d1f315b91e09eb
7
+ data.tar.gz: b9886d0b78d1491393cfacdea4c1f4cdeae1d2a993a0f5a0875011be1083bb63e4a7721fb3a007869eaa7eaf69ad300438d8038c099e88277958d4daf6d226f7
@@ -1,4 +1,8 @@
1
1
  # typed: true
2
+
3
+ require_relative 'hash_utils'
4
+ require 'sorbet-runtime'
5
+
2
6
  $empty_eval_result = {
3
7
  :gate_value => false,
4
8
  :json_value => {},
@@ -9,10 +13,14 @@ $empty_eval_result = {
9
13
 
10
14
  module ClientInitializeHelpers
11
15
  class ResponseFormatter
12
- def initialize(evaluator, user)
16
+ extend T::Sig
17
+
18
+ def initialize(evaluator, user, hash, client_sdk_key)
13
19
  @evaluator = evaluator
14
20
  @user = user
15
21
  @specs = evaluator.spec_store.get_raw_specs
22
+ @hash = hash
23
+ @client_sdk_key = client_sdk_key
16
24
  end
17
25
 
18
26
  def get_responses(key)
@@ -23,7 +31,21 @@ module ClientInitializeHelpers
23
31
 
24
32
  private
25
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:')
38
+ end
39
+ end
40
+
26
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']
44
+
45
+ unless target_app_id.nil? || (!config_target_apps.nil? && config_target_apps.include?(target_app_id))
46
+ return nil
47
+ end
48
+
27
49
  eval_result = @evaluator.eval_spec(@user, config_spec)
28
50
  if eval_result.nil?
29
51
  return nil
@@ -33,9 +55,11 @@ module ClientInitializeHelpers
33
55
  :gate_value => eval_result.gate_value,
34
56
  :json_value => eval_result.json_value,
35
57
  :rule_id => eval_result.rule_id,
58
+ :group_name => eval_result.group_name,
59
+ :id_type => eval_result.id_type,
36
60
  :config_delegate => eval_result.config_delegate,
37
61
  :is_experiment_group => eval_result.is_experiment_group,
38
- :secondary_exposures => eval_result.secondary_exposures,
62
+ :secondary_exposures => filter_segments_from_secondary_exposures(eval_result.secondary_exposures),
39
63
  :undelegated_sec_exps => eval_result.undelegated_sec_exps
40
64
  }
41
65
 
@@ -52,10 +76,14 @@ module ClientInitializeHelpers
52
76
  end
53
77
 
54
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]
55
81
  when 'dynamic_config'
56
82
  id_type = config_spec['idType']
57
83
  result['value'] = safe_eval_result[:json_value]
58
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]
59
87
  result["is_device_based"] = id_type.is_a?(String) && id_type.downcase == 'stableid'
60
88
  else
61
89
  return nil
@@ -67,6 +95,7 @@ module ClientInitializeHelpers
67
95
 
68
96
  if entity_type == 'layer'
69
97
  populate_layer_fields(config_spec, safe_eval_result, result)
98
+ result.delete('id_type') # not exposed for layer configs in /initialize
70
99
  end
71
100
 
72
101
  hashed_name = hash_name(config_name)
@@ -75,7 +104,7 @@ module ClientInitializeHelpers
75
104
  "name" => hashed_name,
76
105
  "rule_id" => safe_eval_result[:rule_id],
77
106
  "secondary_exposures" => clean_exposures(safe_eval_result[:secondary_exposures])
78
- })]
107
+ }).compact]
79
108
  end
80
109
 
81
110
  def clean_exposures(exposures)
@@ -126,7 +155,14 @@ module ClientInitializeHelpers
126
155
  end
127
156
 
128
157
  def hash_name(name)
129
- Digest::SHA256.base64digest(name)
158
+ case @hash
159
+ when 'none'
160
+ return name
161
+ when 'sha256'
162
+ return Statsig::HashUtils.sha256(name)
163
+ when 'djb2'
164
+ return Statsig::HashUtils.djb2(name)
165
+ end
130
166
  end
131
167
  end
132
- end
168
+ end
data/lib/config_result.rb CHANGED
@@ -1,6 +1,11 @@
1
1
  # typed: true
2
+
3
+ require 'sorbet-runtime'
4
+
2
5
  module Statsig
3
6
  class ConfigResult
7
+ extend T::Sig
8
+
4
9
  attr_accessor :name
5
10
  attr_accessor :gate_value
6
11
  attr_accessor :json_value
@@ -13,6 +18,7 @@ module Statsig
13
18
  attr_accessor :evaluation_details
14
19
  attr_accessor :group_name
15
20
  attr_accessor :id_type
21
+ attr_accessor :target_app_ids
16
22
 
17
23
  def initialize(
18
24
  name,
@@ -20,12 +26,13 @@ module Statsig
20
26
  json_value = {},
21
27
  rule_id = '',
22
28
  secondary_exposures = [],
23
- config_delegate = '',
29
+ config_delegate = nil,
24
30
  explicit_parameters = [],
25
31
  is_experiment_group: false,
26
32
  evaluation_details: nil,
27
33
  group_name: nil,
28
- id_type: '')
34
+ id_type: '',
35
+ target_app_ids: nil)
29
36
  @name = name
30
37
  @gate_value = gate_value
31
38
  @json_value = json_value
@@ -38,6 +45,45 @@ 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
+ end
50
+
51
+ sig { params(config_name: String, user_persisted_values: UserPersistedValues).returns(T.nilable(ConfigResult)) }
52
+ def self.from_user_persisted_values(config_name, user_persisted_values)
53
+ sticky_values = user_persisted_values[config_name]
54
+ return nil if sticky_values.nil?
55
+
56
+ from_hash(config_name, sticky_values)
57
+ end
58
+
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
+ def to_hash
76
+ {
77
+ json_value: @json_value,
78
+ gate_value: @gate_value,
79
+ rule_id: @rule_id,
80
+ secondary_exposures: @secondary_exposures,
81
+ config_sync_time: @evaluation_details.config_sync_time,
82
+ init_time: @init_time,
83
+ group_name: @group_name,
84
+ id_type: @id_type,
85
+ target_app_ids: @target_app_ids
86
+ }
41
87
  end
42
88
  end
43
- end
89
+ end
data/lib/diagnostics.rb CHANGED
@@ -7,14 +7,18 @@ module Statsig
7
7
  extend T::Sig
8
8
 
9
9
  sig { returns(String) }
10
- attr_reader :context
10
+ attr_accessor :context
11
11
 
12
12
  sig { returns(T::Array[T::Hash[Symbol, T.untyped]]) }
13
13
  attr_reader :markers
14
14
 
15
+ sig { returns(T::Hash[String, Numeric]) }
16
+ attr_accessor :sample_rates
17
+
15
18
  def initialize(context)
16
19
  @context = context
17
20
  @markers = []
21
+ @sample_rates = {}
18
22
  end
19
23
 
20
24
  sig do
@@ -22,33 +26,37 @@ module Statsig
22
26
  key: String,
23
27
  action: String,
24
28
  step: T.any(String, NilClass),
25
- value: T.any(String, Integer, T::Boolean, NilClass),
26
- metadata: T.any(T::Hash[Symbol, T.untyped], NilClass)
29
+ tags: T::Hash[Symbol, T.untyped]
27
30
  ).void
28
31
  end
29
32
 
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
- })
33
+ def mark(key, action, step, tags)
34
+ marker = {
35
+ key: key,
36
+ action: action,
37
+ timestamp: (Time.now.to_f * 1000).to_i
38
+ }
39
+ if !step.nil?
40
+ marker[:step] = step
41
+ end
42
+ tags.each do |key, val|
43
+ unless val.nil?
44
+ marker[key] = val
45
+ end
46
+ end
47
+ @markers.push(marker)
39
48
  end
40
49
 
41
50
  sig do
42
51
  params(
43
52
  key: String,
44
53
  step: T.any(String, NilClass),
45
- value: T.any(String, Integer, T::Boolean, NilClass),
46
- metadata: T.any(T::Hash[Symbol, T.untyped], NilClass)
54
+ tags: T::Hash[Symbol, T.untyped]
47
55
  ).returns(Tracker)
48
56
  end
49
- def track(key, step = nil, value = nil, metadata = nil)
50
- tracker = Tracker.new(self, key, step, metadata)
51
- tracker.start(value)
57
+ def track(key, step = nil, tags = {})
58
+ tracker = Tracker.new(self, key, step, tags)
59
+ tracker.start(**tags)
52
60
  tracker
53
61
  end
54
62
 
@@ -61,10 +69,29 @@ module Statsig
61
69
  }
62
70
  end
63
71
 
72
+ def serialize_with_sampling
73
+ marker_keys = @markers.map { |e| e[:key] }
74
+ unique_marker_keys = marker_keys.uniq { |e| e }
75
+ sampled_marker_keys = unique_marker_keys.select do |key|
76
+ @sample_rates.key?(key) && !self.class.sample(@sample_rates[key])
77
+ end
78
+ final_markers = @markers.select do |marker|
79
+ !sampled_marker_keys.include?(marker[:key])
80
+ end
81
+ {
82
+ context: @context.clone,
83
+ markers: final_markers
84
+ }
85
+ end
86
+
64
87
  def clear_markers
65
88
  @markers.clear
66
89
  end
67
90
 
91
+ def self.sample(rate_over_ten_thousand)
92
+ rand * 10_000 < rate_over_ten_thousand
93
+ end
94
+
68
95
  class Context
69
96
  INITIALIZE = 'initialize'.freeze
70
97
  CONFIG_SYNC = 'config_sync'.freeze
@@ -81,22 +108,22 @@ module Statsig
81
108
  diagnostics: Diagnostics,
82
109
  key: String,
83
110
  step: T.any(String, NilClass),
84
- metadata: T.any(T::Hash[Symbol, T.untyped], NilClass)
111
+ tags: T::Hash[Symbol, T.untyped]
85
112
  ).void
86
113
  end
87
- def initialize(diagnostics, key, step, metadata)
114
+ def initialize(diagnostics, key, step, tags = {})
88
115
  @diagnostics = diagnostics
89
116
  @key = key
90
117
  @step = step
91
- @metadata = metadata
118
+ @tags = tags
92
119
  end
93
120
 
94
- def start(value = nil)
95
- @diagnostics.mark(@key, 'start', @step, value, @metadata)
121
+ def start(**tags)
122
+ @diagnostics.mark(@key, 'start', @step, tags.nil? ? {} : tags.merge(@tags))
96
123
  end
97
124
 
98
- def end(value = nil)
99
- @diagnostics.mark(@key, 'end', @step, value, @metadata)
125
+ def end(**tags)
126
+ @diagnostics.mark(@key, 'end', @step, tags.nil? ? {} : tags.merge(@tags))
100
127
  end
101
128
  end
102
129
  end
@@ -26,13 +26,26 @@ class DynamicConfig
26
26
  sig { returns(String) }
27
27
  attr_accessor :id_type
28
28
 
29
- sig { params(name: String, value: T::Hash[String, T.untyped], rule_id: String, group_name: T.nilable(String), id_type: String).void }
30
- def initialize(name, value = {}, rule_id = '', group_name = nil, id_type = '')
29
+ sig { returns(T.nilable(Statsig::EvaluationDetails)) }
30
+ attr_accessor :evaluation_details
31
+
32
+ sig do
33
+ params(
34
+ name: String,
35
+ value: T::Hash[String, T.untyped],
36
+ rule_id: String,
37
+ group_name: T.nilable(String),
38
+ id_type: String,
39
+ evaluation_details: T.nilable(Statsig::EvaluationDetails)
40
+ ).void
41
+ end
42
+ def initialize(name, value = {}, rule_id = '', group_name = nil, id_type = '', evaluation_details = nil)
31
43
  @name = name
32
44
  @value = value
33
45
  @rule_id = rule_id
34
46
  @group_name = group_name
35
47
  @id_type = id_type
48
+ @evaluation_details = evaluation_details
36
49
  end
37
50
 
38
51
  sig { params(index: String, default_value: T.untyped).returns(T.untyped) }
@@ -9,70 +9,58 @@ module Statsig
9
9
  class ErrorBoundary
10
10
  extend T::Sig
11
11
 
12
- sig { returns(T.any(StatsigLogger, NilClass)) }
13
- attr_accessor :logger
14
-
15
12
  sig { params(sdk_key: String).void }
16
13
  def initialize(sdk_key)
17
14
  @sdk_key = sdk_key
18
15
  @seen = Set.new
19
16
  end
20
17
 
21
- def sample_diagnostics
22
- rand(10_000).zero?
23
- end
24
-
25
18
  def capture(task:, recover: -> {}, caller: nil)
26
- if !caller.nil? && Diagnostics::API_CALL_KEYS.include?(caller) && sample_diagnostics
27
- diagnostics = Diagnostics.new('api_call')
28
- tracker = diagnostics.track(caller)
29
- end
30
19
  begin
31
20
  res = task.call
32
- tracker&.end(true)
33
- rescue StandardError => e
34
- tracker&.end(false)
35
- if e.is_a?(Statsig::UninitializedError) or e.is_a?(Statsig::ValueError)
21
+ rescue StandardError, SystemStackError => e
22
+ if e.is_a?(Statsig::UninitializedError) || e.is_a?(Statsig::ValueError)
36
23
  raise e
37
24
  end
25
+
38
26
  puts '[Statsig]: An unexpected exception occurred.'
39
- log_exception(e)
27
+ puts e.message
28
+ log_exception(e, tag: caller)
40
29
  res = recover.call
41
30
  end
42
- @logger&.log_diagnostics_event(diagnostics)
43
31
  return res
44
32
  end
45
33
 
46
34
  private
47
35
 
48
- def log_exception(exception)
49
- begin
50
- name = exception.class.name
51
- if @seen.include?(name)
52
- return
53
- end
54
-
55
- @seen << name
56
- meta = Statsig.get_statsig_metadata
57
- http = HTTP.headers(
58
- {
59
- 'STATSIG-API-KEY' => @sdk_key,
60
- 'STATSIG-SDK-TYPE' => meta['sdkType'],
61
- 'STATSIG-SDK-VERSION' => meta['sdkVersion'],
62
- 'Content-Type' => 'application/json; charset=UTF-8'
63
- }).accept(:json)
64
- body = {
65
- 'exception' => name,
66
- 'info' => {
67
- 'trace' => exception.backtrace.to_s,
68
- 'message' => exception.message
69
- }.to_s,
70
- 'statsigMetadata' => meta
71
- }
72
- http.post($endpoint, body: JSON.generate(body))
73
- rescue
36
+ def log_exception(exception, tag: nil)
37
+ name = exception.class.name
38
+ if @seen.include?(name)
74
39
  return
75
40
  end
41
+
42
+ @seen << name
43
+ meta = Statsig.get_statsig_metadata
44
+ http = HTTP.headers(
45
+ {
46
+ 'STATSIG-API-KEY' => @sdk_key,
47
+ 'STATSIG-SDK-TYPE' => meta['sdkType'],
48
+ 'STATSIG-SDK-VERSION' => meta['sdkVersion'],
49
+ 'STATSIG-SDK-LANGUAGE-VERSION' => meta['languageVersion'],
50
+ 'Content-Type' => 'application/json; charset=UTF-8'
51
+ }).accept(:json)
52
+ body = {
53
+ 'exception' => name,
54
+ 'info' => {
55
+ 'trace' => exception.backtrace.to_s,
56
+ 'message' => exception.message
57
+ }.to_s,
58
+ 'statsigMetadata' => meta,
59
+ 'tag' => tag
60
+ }
61
+ http.post($endpoint, body: JSON.generate(body))
62
+ rescue StandardError
63
+ return
76
64
  end
77
65
  end
78
- end
66
+ end
@@ -2,12 +2,13 @@
2
2
  module Statsig
3
3
 
4
4
  module EvaluationReason
5
- NETWORK = "Network"
6
- LOCAL_OVERRIDE = "LocalOverride"
7
- UNRECOGNIZED = "Unrecognized"
8
- UNINITIALIZED = "Uninitialized"
9
- BOOTSTRAP = "Bootstrap"
10
- DATA_ADAPTER = "DataAdapter"
5
+ NETWORK = 'Network'.freeze
6
+ LOCAL_OVERRIDE = 'LocalOverride'.freeze
7
+ UNRECOGNIZED = 'Unrecognized'.freeze
8
+ UNINITIALIZED = 'Uninitialized'.freeze
9
+ BOOTSTRAP = 'Bootstrap'.freeze
10
+ DATA_ADAPTER = 'DataAdapter'.freeze
11
+ PERSISTED = 'Persisted'.freeze
11
12
  end
12
13
 
13
14
  class EvaluationDetails
@@ -38,5 +39,9 @@ module Statsig
38
39
  def self.local_override(config_sync_time, init_time)
39
40
  EvaluationDetails.new(config_sync_time, init_time, EvaluationReason::LOCAL_OVERRIDE)
40
41
  end
42
+
43
+ def self.persisted(config_sync_time, init_time)
44
+ EvaluationDetails.new(config_sync_time, init_time, EvaluationReason::PERSISTED)
45
+ end
41
46
  end
42
- end
47
+ end