Skip to content

Commit af2dfbb

Browse files
committed
diagnostics sampling
1 parent d771dea commit af2dfbb

File tree

6 files changed

+102
-15
lines changed

6 files changed

+102
-15
lines changed

.github/workflows/build-and-test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ jobs:
1616
strategy:
1717
max-parallel: 1
1818
matrix:
19-
version: [2.5.0, 3.2.0]
19+
version: [3.2.0, 2.5.0]
2020
steps:
2121
- uses: actions/checkout@v2
2222
- name: Set up Ruby

lib/diagnostics.rb

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,18 @@ class Diagnostics
77
extend T::Sig
88

99
sig { returns(String) }
10-
attr_reader :context
10+
attr_accessor :context
1111

1212
sig { returns(T::Array[T::Hash[Symbol, T.untyped]]) }
1313
attr_reader :markers
1414

15+
sig { returns(T::Hash[String, Numeric]) }
16+
attr_accessor :sample_rates
17+
1518
def initialize(context)
1619
@context = context
1720
@markers = []
21+
@sample_rates = {}
1822
end
1923

2024
sig do
@@ -65,12 +69,27 @@ def serialize
6569
}
6670
end
6771

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+
6887
def clear_markers
6988
@markers.clear
7089
end
7190

72-
def self.sample(rate)
73-
rand(rate).zero?
91+
def self.sample(rate_over_ten_thousand)
92+
rand * 10_000 < rate_over_ten_thousand
7493
end
7594

7695
class Context

lib/spec_store.rb

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ def maybe_restart_background_threads
149149
end
150150

151151
def sync_config_specs
152-
@diagnostics = Diagnostics.new('config_sync')
152+
@diagnostics.context = 'config_sync'
153153
if @options.data_store&.should_be_used_for_querying_updates(Interfaces::IDataStore::CONFIG_SPECS_KEY)
154154
load_config_specs_from_storage_adapter
155155
else
@@ -159,7 +159,7 @@ def sync_config_specs
159159
end
160160

161161
def sync_id_lists
162-
@diagnostics = Diagnostics.new('config_sync')
162+
@diagnostics.context = 'config_sync'
163163
if @options.data_store&.should_be_used_for_querying_updates(Interfaces::IDataStore::ID_LISTS_KEY)
164164
get_id_lists_from_adapter
165165
else
@@ -280,6 +280,7 @@ def process_specs(specs_string, from_adapter: false)
280280
specs_json['feature_gates'].each { |gate| new_gates[gate['name']] = gate }
281281
specs_json['dynamic_configs'].each { |config| new_configs[config['name']] = config }
282282
specs_json['layer_configs'].each { |layer| new_layers[layer['name']] = layer }
283+
specs_json['diagnostics']&.each { |key, value| @diagnostics.sample_rates[key] = value }
283284

284285
if specs_json['layers'].is_a?(Hash)
285286
specs_json['layers'].each { |layer_name, experiments|
@@ -293,8 +294,6 @@ def process_specs(specs_string, from_adapter: false)
293294
@specs[:experiment_to_layer] = new_exp_to_layer
294295
@specs[:sdk_keys_to_app_ids] = specs_json['sdk_keys_to_app_ids'] || {}
295296

296-
specs_json['diagnostics']
297-
298297
unless from_adapter
299298
save_config_specs_to_storage_adapter(specs_string)
300299
end

lib/statsig_driver.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,7 @@ def maybe_restart_background_threads
243243

244244
def run_with_diagnostics(task:, caller:)
245245
diagnostics = nil
246-
if Statsig::Diagnostics::API_CALL_KEYS.include?(caller) && Statsig::Diagnostics.sample(10_000)
246+
if Statsig::Diagnostics::API_CALL_KEYS.include?(caller) && Statsig::Diagnostics.sample(1)
247247
diagnostics = Statsig::Diagnostics.new('api_call')
248248
tracker = diagnostics.track(caller)
249249
end

lib/statsig_logger.rb

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,11 +100,14 @@ def log_layer_exposure(user, layer, parameter_name, config_evaluation, context =
100100

101101
def log_diagnostics_event(diagnostics, user = nil)
102102
return if @options.disable_diagnostics_logging
103-
return if diagnostics.nil? || diagnostics.markers.empty?
103+
return if diagnostics.nil?
104104

105105
event = StatsigEvent.new($diagnostics_event)
106106
event.user = user
107-
event.metadata = diagnostics.serialize
107+
serialized = diagnostics.serialize_with_sampling
108+
return if serialized[:markers].empty?
109+
110+
event.metadata = serialized
108111
log_event(event)
109112
diagnostics.clear_markers
110113
end

test/test_diagnostics.rb

Lines changed: 70 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,10 @@ def setup
3232
@events.push(*JSON.parse(request.body)['events'])
3333
return ''
3434
})
35-
Spy.on(Statsig::Diagnostics, 'sample').and_return do
36-
true
37-
end
3835
end
3936

4037
def teardown
4138
super
42-
Spy.off(Statsig::Diagnostics, 'sample')
4339
WebMock.reset!
4440
WebMock.disable!
4541
end
@@ -190,12 +186,16 @@ def test_data_adapter_init
190186

191187
def test_api_call_diagnostics
192188
Statsig.initialize('secret-key')
189+
Spy.on(Statsig::Diagnostics, 'sample').and_return do
190+
true
191+
end
193192
user = StatsigUser.new(user_id: 'test-user')
194193
Statsig.check_gate_with_exposure_logging_disabled(user, 'non-existent-gate')
195194
Statsig.get_config_with_exposure_logging_disabled(user, 'non-existent-config')
196195
Statsig.get_experiment_with_exposure_logging_disabled(user, 'non-existent-experiment')
197196
Statsig.get_layer_with_exposure_logging_disabled(user, 'non-existent-layer')
198197
Statsig.shutdown
198+
Spy.off(Statsig::Diagnostics, 'sample')
199199

200200
keys = Statsig::Diagnostics::API_CALL_KEYS
201201

@@ -223,6 +223,72 @@ def test_disable_diagnostics_logging
223223
assert_equal(0, @events.length)
224224
end
225225

226+
def test_diagnostics_sampling
227+
json_file = File.read("#{__dir__}/data/download_config_specs.json")
228+
@mock_response = JSON.parse(json_file)
229+
@mock_response['diagnostics'] = {
230+
"download_config_specs": 5000,
231+
"get_id_list": 5000,
232+
"get_id_list_sources": 5000
233+
}
234+
stub_request(:post, 'https://statsigapi.net/v1/download_config_specs').to_return(
235+
status: 200,
236+
body: @mock_response.to_json,
237+
headers: { 'x-statsig-region' => 'az-westus-2' }
238+
)
239+
driver = StatsigDriver.new(
240+
'secret-key',
241+
StatsigOptions.new(
242+
disable_rulesets_sync: true,
243+
disable_idlists_sync: true,
244+
logging_interval_seconds: 9999
245+
)
246+
)
247+
logger = driver.instance_variable_get('@logger')
248+
logger.flush
249+
250+
assert_equal(1, @events.length)
251+
event = @events[0]
252+
assert_equal('statsig::diagnostics', event['eventName'])
253+
254+
metadata = event['metadata']
255+
assert_equal('initialize', metadata['context'])
256+
@events = []
257+
258+
10.times do
259+
driver.manually_sync_rulesets
260+
end
261+
logger.flush
262+
263+
assert(
264+
@events.length < 10 && @events.length.positive?,
265+
"Expected between 0 and 10 events, received #{@events.length}"
266+
)
267+
event = @events[0]
268+
assert_equal('statsig::diagnostics', event['eventName'])
269+
270+
metadata = event['metadata']
271+
assert_equal('config_sync', metadata['context'])
272+
@events = []
273+
274+
10.times do
275+
driver.manually_sync_idlists
276+
end
277+
logger.flush
278+
279+
assert(
280+
@events.length < 10 && @events.length.positive?,
281+
"Expected between 0 and 10 events, received #{@events.length}"
282+
)
283+
event = @events[0]
284+
assert_equal('statsig::diagnostics', event['eventName'])
285+
286+
metadata = event['metadata']
287+
assert_equal('config_sync', metadata['context'])
288+
289+
driver.shutdown
290+
end
291+
226292
private
227293

228294
def assert_marker_equal(marker, key, action, step = nil, tags = {})

0 commit comments

Comments
 (0)