quonfig 0.0.9 → 0.0.11
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 +4 -4
- data/CHANGELOG.md +43 -0
- data/README.md +4 -4
- data/lib/quonfig/evaluation_details.rb +60 -0
- data/lib/quonfig/options.rb +37 -16
- data/lib/quonfig/sse_config_client.rb +1 -1
- data/lib/quonfig/version.rb +5 -0
- data/lib/quonfig.rb +2 -1
- data/quonfig.gemspec +30 -163
- metadata +29 -182
- data/.claude/rules/constitution.md +0 -81
- data/.claude/rules/git-safety.md +0 -11
- data/.claude/rules/issue-tracking.md +0 -13
- data/.claude/rules/testing-workflow.md +0 -28
- data/.envrc.sample +0 -3
- data/.github/CODEOWNERS +0 -2
- data/.github/pull_request_template.md +0 -8
- data/.github/workflows/release.yml +0 -49
- data/.github/workflows/ruby.yml +0 -60
- data/.github/workflows/test.yaml +0 -40
- data/.rubocop.yml +0 -13
- data/.tool-versions +0 -1
- data/CLAUDE.md +0 -29
- data/CODEOWNERS +0 -1
- data/Gemfile +0 -26
- data/Gemfile.lock +0 -177
- data/Rakefile +0 -64
- data/VERSION +0 -1
- data/dev/allocation_stats +0 -60
- data/dev/benchmark +0 -40
- data/dev/console +0 -12
- data/dev/script_setup.rb +0 -18
- data/test/fixtures/datafile.json +0 -87
- data/test/integration/test_context_precedence.rb +0 -112
- data/test/integration/test_datadir_environment.rb +0 -54
- data/test/integration/test_dev_overrides.rb +0 -40
- data/test/integration/test_enabled.rb +0 -478
- data/test/integration/test_enabled_with_contexts.rb +0 -64
- data/test/integration/test_get.rb +0 -136
- data/test/integration/test_get_feature_flag.rb +0 -28
- data/test/integration/test_get_or_raise.rb +0 -60
- data/test/integration/test_get_weighted_values.rb +0 -34
- data/test/integration/test_helpers.rb +0 -667
- data/test/integration/test_helpers_test.rb +0 -73
- data/test/integration/test_post.rb +0 -44
- data/test/integration/test_telemetry.rb +0 -170
- data/test/support/common_helpers.rb +0 -106
- data/test/support/mock_base_client.rb +0 -27
- data/test/support/mock_config_loader.rb +0 -1
- data/test/test_bound_client.rb +0 -109
- data/test/test_caching_http_connection.rb +0 -218
- data/test/test_client.rb +0 -255
- data/test/test_client_network_mode.rb +0 -136
- data/test/test_client_telemetry.rb +0 -175
- data/test/test_config_loader.rb +0 -70
- data/test/test_context.rb +0 -139
- data/test/test_context_shape.rb +0 -37
- data/test/test_context_shape_aggregator.rb +0 -126
- data/test/test_datadir.rb +0 -203
- data/test/test_dev_context.rb +0 -163
- data/test/test_duration.rb +0 -37
- data/test/test_encryption.rb +0 -16
- data/test/test_evaluation_summaries_aggregator.rb +0 -180
- data/test/test_evaluator.rb +0 -285
- data/test/test_example_contexts_aggregator.rb +0 -119
- data/test/test_exponential_backoff.rb +0 -44
- data/test/test_fixed_size_hash.rb +0 -119
- data/test/test_helper.rb +0 -17
- data/test/test_http_connection.rb +0 -79
- data/test/test_internal_logger.rb +0 -34
- data/test/test_options.rb +0 -167
- data/test/test_rate_limit_cache.rb +0 -44
- data/test/test_reason.rb +0 -79
- data/test/test_rename.rb +0 -65
- data/test/test_resolver.rb +0 -291
- data/test/test_semantic_logger_filter.rb +0 -144
- data/test/test_semver.rb +0 -108
- data/test/test_should_log.rb +0 -186
- data/test/test_sse_config_client.rb +0 -297
- data/test/test_stdlib_formatter.rb +0 -195
- data/test/test_telemetry_reporter.rb +0 -209
- data/test/test_typed_getters.rb +0 -131
- data/test/test_types.rb +0 -141
- data/test/test_weighted_value_resolver.rb +0 -84
|
@@ -1,175 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'test_helper'
|
|
4
|
-
|
|
5
|
-
# Verifies that Quonfig::Client#get feeds every evaluation into the
|
|
6
|
-
# telemetry reporter's evaluation_summaries aggregator.
|
|
7
|
-
class TestClientTelemetry < Minitest::Test
|
|
8
|
-
CONFIG_KEY = 'my.flag'
|
|
9
|
-
|
|
10
|
-
class FakeHttpConnection
|
|
11
|
-
FakeResponse = Struct.new(:status)
|
|
12
|
-
attr_reader :posts
|
|
13
|
-
def initialize; @posts = []; end
|
|
14
|
-
def post(path, body); @posts << [path, body]; FakeResponse.new(200); end
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
# Plain ConfigResponse-shaped hash (matches Datadir.to_config_response).
|
|
18
|
-
def make_config(key:, value:, type: 'string', criteria: nil)
|
|
19
|
-
{
|
|
20
|
-
'id' => 'cid-abc',
|
|
21
|
-
'key' => key,
|
|
22
|
-
'type' => 'config',
|
|
23
|
-
'valueType' => type,
|
|
24
|
-
'sendToClientSdk' => false,
|
|
25
|
-
'default' => {
|
|
26
|
-
'rules' => [
|
|
27
|
-
{
|
|
28
|
-
'criteria' => criteria || [{ 'operator' => 'ALWAYS_TRUE' }],
|
|
29
|
-
'value' => { 'type' => type, 'value' => value }
|
|
30
|
-
}
|
|
31
|
-
]
|
|
32
|
-
},
|
|
33
|
-
'environment' => nil
|
|
34
|
-
}
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
def make_client_with_telemetry(store)
|
|
38
|
-
client = Quonfig::Client.new(Quonfig::Options.new, store: store)
|
|
39
|
-
|
|
40
|
-
# The store-injection path skips initialize_telemetry by design (it's
|
|
41
|
-
# for test/bootstrap mode), so we attach the reporter + aggregators
|
|
42
|
-
# here explicitly to exercise the record path.
|
|
43
|
-
summaries_agg = Quonfig::Telemetry::EvaluationSummariesAggregator.new(max_keys: 100)
|
|
44
|
-
shape_agg = Quonfig::Telemetry::ContextShapeAggregator.new(max_shapes: 100)
|
|
45
|
-
example_agg = Quonfig::Telemetry::ExampleContextsAggregator.new(max_contexts: 100)
|
|
46
|
-
|
|
47
|
-
options = Quonfig::Options.new(
|
|
48
|
-
sdk_key: 'qf_sk_dev_abc_deadbeef',
|
|
49
|
-
environment: 'development',
|
|
50
|
-
api_urls: ['https://primary.example.com'],
|
|
51
|
-
enable_sse: false,
|
|
52
|
-
enable_polling: false,
|
|
53
|
-
on_init_failure: Quonfig::Options::ON_INITIALIZATION_FAILURE::RETURN
|
|
54
|
-
).tap { |o| o.instance_variable_set(:@telemetry_destination, 'https://t.example.com') }
|
|
55
|
-
|
|
56
|
-
fake_conn = FakeHttpConnection.new
|
|
57
|
-
reporter = Quonfig::Telemetry::TelemetryReporter.new(
|
|
58
|
-
options: options,
|
|
59
|
-
instance_hash: client.instance_hash,
|
|
60
|
-
context_shape_aggregator: shape_agg,
|
|
61
|
-
example_contexts_aggregator: example_agg,
|
|
62
|
-
evaluation_summaries_aggregator: summaries_agg,
|
|
63
|
-
http_connection: fake_conn
|
|
64
|
-
)
|
|
65
|
-
client.instance_variable_set(:@telemetry_reporter, reporter)
|
|
66
|
-
|
|
67
|
-
[client, reporter, summaries_agg, fake_conn]
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
def test_get_pushes_evaluation_into_summaries_aggregator
|
|
71
|
-
store = Quonfig::ConfigStore.new
|
|
72
|
-
store.set(CONFIG_KEY, make_config(key: CONFIG_KEY, value: 'hello'))
|
|
73
|
-
|
|
74
|
-
client, _reporter, summaries_agg, _conn = make_client_with_telemetry(store)
|
|
75
|
-
client.get(CONFIG_KEY, Quonfig::NO_DEFAULT_PROVIDED, 'user' => { 'key' => 'u1' })
|
|
76
|
-
|
|
77
|
-
event = summaries_agg.drain_event
|
|
78
|
-
refute_nil event, 'expected an evaluation_summaries event after Client#get'
|
|
79
|
-
|
|
80
|
-
summary = event['summaries']['summaries'][0]
|
|
81
|
-
assert_equal CONFIG_KEY, summary['key']
|
|
82
|
-
assert_equal 'config', summary['type']
|
|
83
|
-
|
|
84
|
-
counter = summary['counters'][0]
|
|
85
|
-
assert_equal 'cid-abc', counter['configId']
|
|
86
|
-
assert_equal 0, counter['conditionalValueIndex']
|
|
87
|
-
assert_equal 1, counter['count']
|
|
88
|
-
assert_equal({ 'string' => 'hello' }, counter['selectedValue'])
|
|
89
|
-
# ALWAYS_TRUE on the only rule → STATIC (1)
|
|
90
|
-
assert_equal 1, counter['reason']
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
def test_get_with_targeting_rule_reports_targeting_match
|
|
94
|
-
store = Quonfig::ConfigStore.new
|
|
95
|
-
store.set(CONFIG_KEY, make_config(
|
|
96
|
-
key: CONFIG_KEY,
|
|
97
|
-
value: 'targeted',
|
|
98
|
-
criteria: [{
|
|
99
|
-
'operator' => 'PROP_IS_ONE_OF',
|
|
100
|
-
'propertyName' => 'user.tier',
|
|
101
|
-
'valueToMatch' => { 'type' => 'string_list', 'value' => ['pro'] }
|
|
102
|
-
}]
|
|
103
|
-
))
|
|
104
|
-
|
|
105
|
-
client, _reporter, summaries_agg, _conn = make_client_with_telemetry(store)
|
|
106
|
-
client.get(CONFIG_KEY, 'fallback', 'user' => { 'key' => 'u1', 'tier' => 'pro' })
|
|
107
|
-
|
|
108
|
-
counter = summaries_agg.drain_event['summaries']['summaries'][0]['counters'][0]
|
|
109
|
-
# Config has a non-ALWAYS_TRUE rule → TARGETING_MATCH (2)
|
|
110
|
-
assert_equal 2, counter['reason']
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
def test_repeated_get_increments_count_not_new_counters
|
|
114
|
-
store = Quonfig::ConfigStore.new
|
|
115
|
-
store.set(CONFIG_KEY, make_config(key: CONFIG_KEY, value: 'hello'))
|
|
116
|
-
|
|
117
|
-
client, _reporter, summaries_agg, _conn = make_client_with_telemetry(store)
|
|
118
|
-
3.times { client.get(CONFIG_KEY, Quonfig::NO_DEFAULT_PROVIDED, 'user' => { 'key' => 'u1' }) }
|
|
119
|
-
|
|
120
|
-
counters = summaries_agg.drain_event['summaries']['summaries'][0]['counters']
|
|
121
|
-
assert_equal 1, counters.size, 'same evaluation should dedupe into one counter'
|
|
122
|
-
assert_equal 3, counters[0]['count']
|
|
123
|
-
end
|
|
124
|
-
|
|
125
|
-
def test_missing_key_does_not_record_evaluation
|
|
126
|
-
store = Quonfig::ConfigStore.new
|
|
127
|
-
client, _reporter, summaries_agg, _conn = make_client_with_telemetry(store)
|
|
128
|
-
|
|
129
|
-
client.get('does.not.exist', 'fallback')
|
|
130
|
-
assert_nil summaries_agg.drain_event
|
|
131
|
-
end
|
|
132
|
-
|
|
133
|
-
# Regression: client used to hardcode weighted_value_index: nil, so split
|
|
134
|
-
# variants never reported as REASON_SPLIT in telemetry.
|
|
135
|
-
def test_weighted_value_records_split_reason_and_index
|
|
136
|
-
weighted_config = {
|
|
137
|
-
'id' => 'cid-weighted',
|
|
138
|
-
'key' => 'feature-flag.weighted',
|
|
139
|
-
'type' => 'feature_flag',
|
|
140
|
-
'valueType' => 'string',
|
|
141
|
-
'sendToClientSdk' => false,
|
|
142
|
-
'default' => {
|
|
143
|
-
'rules' => [
|
|
144
|
-
{
|
|
145
|
-
'criteria' => [{ 'operator' => 'ALWAYS_TRUE' }],
|
|
146
|
-
'value' => {
|
|
147
|
-
'type' => 'weighted_values',
|
|
148
|
-
'value' => {
|
|
149
|
-
'hashByPropertyName' => 'user.key',
|
|
150
|
-
'weightedValues' => [
|
|
151
|
-
{ 'value' => { 'type' => 'string', 'value' => 'control' }, 'weight' => 1 },
|
|
152
|
-
{ 'value' => { 'type' => 'string', 'value' => 'variant' }, 'weight' => 99 }
|
|
153
|
-
]
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
]
|
|
158
|
-
},
|
|
159
|
-
'environment' => nil
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
store = Quonfig::ConfigStore.new
|
|
163
|
-
store.set('feature-flag.weighted', weighted_config)
|
|
164
|
-
|
|
165
|
-
client, _reporter, summaries_agg, _conn = make_client_with_telemetry(store)
|
|
166
|
-
client.get('feature-flag.weighted', Quonfig::NO_DEFAULT_PROVIDED, 'user' => { 'key' => 'u1' })
|
|
167
|
-
|
|
168
|
-
counter = summaries_agg.drain_event['summaries']['summaries'][0]['counters'][0]
|
|
169
|
-
assert_equal Quonfig::EvalResult::REASON_SPLIT, counter['reason'],
|
|
170
|
-
'weighted variant evaluation must report REASON_SPLIT (3)'
|
|
171
|
-
refute_nil counter['weightedValueIndex'],
|
|
172
|
-
'weighted variant evaluation must report a weightedValueIndex'
|
|
173
|
-
assert_kind_of Integer, counter['weightedValueIndex']
|
|
174
|
-
end
|
|
175
|
-
end
|
data/test/test_config_loader.rb
DELETED
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'test_helper'
|
|
4
|
-
require 'ostruct'
|
|
5
|
-
require 'json'
|
|
6
|
-
|
|
7
|
-
class TestConfigLoader < Minitest::Test
|
|
8
|
-
def setup
|
|
9
|
-
super
|
|
10
|
-
options = Quonfig::Options.new(
|
|
11
|
-
sdk_key: '1-test-sdk-key',
|
|
12
|
-
api_urls: ['https://primary.example.test']
|
|
13
|
-
)
|
|
14
|
-
@loader = Quonfig::ConfigLoader.new(MockBaseClient.new(options))
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
def test_fetch_200_populates_configs_and_stores_etag_then_304_is_a_noop
|
|
18
|
-
body = JSON.generate(
|
|
19
|
-
'configs' => [
|
|
20
|
-
{ 'key' => 'my.flag', 'type' => 'config', 'valueType' => 'bool',
|
|
21
|
-
'default' => { 'rules' => [] } }
|
|
22
|
-
],
|
|
23
|
-
'meta' => { 'version' => 'v1', 'environment' => 'production' }
|
|
24
|
-
)
|
|
25
|
-
ok_response = Faraday::Response.new(
|
|
26
|
-
status: 200, body: body,
|
|
27
|
-
response_headers: { 'ETag' => 'W/"etag-one"' }
|
|
28
|
-
)
|
|
29
|
-
not_modified = Faraday::Response.new(
|
|
30
|
-
status: 304, body: '', response_headers: {}
|
|
31
|
-
)
|
|
32
|
-
|
|
33
|
-
observed_headers = []
|
|
34
|
-
|
|
35
|
-
http_conn = Minitest::Mock.new
|
|
36
|
-
http_conn.expect(:get, ok_response) do |path, headers|
|
|
37
|
-
observed_headers << headers.dup
|
|
38
|
-
path == '/api/v2/configs' && !headers.key?('If-None-Match')
|
|
39
|
-
end
|
|
40
|
-
http_conn.expect(:get, not_modified) do |path, headers|
|
|
41
|
-
observed_headers << headers.dup
|
|
42
|
-
path == '/api/v2/configs' && headers['If-None-Match'] == 'W/"etag-one"'
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
Quonfig::HttpConnection.stub :new, ->(_uri, _key) { http_conn } do
|
|
46
|
-
assert_equal :updated, @loader.fetch!
|
|
47
|
-
assert_equal :not_modified, @loader.fetch!
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
assert_equal 'W/"etag-one"', @loader.etag
|
|
51
|
-
calc = @loader.calc_config
|
|
52
|
-
assert calc.key?('my.flag'), "expected 'my.flag' to be loaded"
|
|
53
|
-
assert_equal 'W/"etag-one"', observed_headers[1]['If-None-Match']
|
|
54
|
-
http_conn.verify
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
def test_set_and_rm_preserved
|
|
58
|
-
config = OpenStruct.new(key: 'x', rows: [1])
|
|
59
|
-
@loader.set(config, :test)
|
|
60
|
-
assert @loader.calc_config.key?('x')
|
|
61
|
-
|
|
62
|
-
@loader.rm('x')
|
|
63
|
-
refute @loader.calc_config.key?('x')
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
def test_no_highwater_mark_attribute
|
|
67
|
-
refute @loader.respond_to?(:highwater_mark),
|
|
68
|
-
'highwater_mark should be removed from ConfigLoader'
|
|
69
|
-
end
|
|
70
|
-
end
|
data/test/test_context.rb
DELETED
|
@@ -1,139 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'test_helper'
|
|
4
|
-
|
|
5
|
-
class TestContext < Minitest::Test
|
|
6
|
-
EXAMPLE_PROPERTIES = {
|
|
7
|
-
user: { key: 'some-user-key', name: 'Ted' },
|
|
8
|
-
team: { key: 'abc', plan: 'pro' }
|
|
9
|
-
}.freeze
|
|
10
|
-
|
|
11
|
-
def test_initialize_with_empty_context
|
|
12
|
-
context = Quonfig::Context.new({})
|
|
13
|
-
assert_empty context.contexts
|
|
14
|
-
assert context.blank?
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
def test_initialize_with_hash
|
|
18
|
-
context = Quonfig::Context.new(test: { foo: 'bar' })
|
|
19
|
-
assert_equal 1, context.contexts.size
|
|
20
|
-
assert_equal 'bar', context.get('test.foo')
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def test_initialize_with_multiple_hashes
|
|
24
|
-
context = Quonfig::Context.new(test: { foo: 'bar' }, other: { foo: 'baz' })
|
|
25
|
-
assert_equal 2, context.contexts.size
|
|
26
|
-
assert_equal 'bar', context.get('test.foo')
|
|
27
|
-
assert_equal 'baz', context.get('other.foo')
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
def test_initialize_with_invalid_argument
|
|
31
|
-
assert_raises(ArgumentError) { Quonfig::Context.new([]) }
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def test_setting
|
|
35
|
-
context = Quonfig::Context.new({})
|
|
36
|
-
context.set('user', { key: 'value' })
|
|
37
|
-
context.set(:other, { key: 'different', something: 'other' })
|
|
38
|
-
|
|
39
|
-
assert_equal(
|
|
40
|
-
stringify(user: { key: 'value' }, other: { key: 'different', something: 'other' }),
|
|
41
|
-
context.to_h
|
|
42
|
-
)
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
def test_getting
|
|
46
|
-
context = Quonfig::Context.new(EXAMPLE_PROPERTIES)
|
|
47
|
-
assert_equal('some-user-key', context.get('user.key'))
|
|
48
|
-
assert_equal('pro', context.get('team.plan'))
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
def test_dot_notation_getting
|
|
52
|
-
context = Quonfig::Context.new('user' => { 'key' => 'value' })
|
|
53
|
-
assert_equal('value', context.get('user.key'))
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
def test_dot_notation_getting_with_symbols
|
|
57
|
-
context = Quonfig::Context.new(user: { key: 'value' })
|
|
58
|
-
assert_equal('value', context.get('user.key'))
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
def test_get_returns_nil_for_missing_property
|
|
62
|
-
context = Quonfig::Context.new(user: { key: 'value' })
|
|
63
|
-
assert_nil context.get('user.missing')
|
|
64
|
-
assert_nil context.get('absent.key')
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
def test_clear
|
|
68
|
-
context = Quonfig::Context.new(EXAMPLE_PROPERTIES)
|
|
69
|
-
context.clear
|
|
70
|
-
|
|
71
|
-
assert_empty context.to_h
|
|
72
|
-
assert context.blank?
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
def test_to_h_stringifies_keys
|
|
76
|
-
context = Quonfig::Context.new(EXAMPLE_PROPERTIES)
|
|
77
|
-
assert_equal stringify(EXAMPLE_PROPERTIES), context.to_h
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
def test_legacy_flat_hash_shorthand_promotes_to_blank_named_context
|
|
81
|
-
# Pre-named-contexts callers passed a flat Hash. The constructor still
|
|
82
|
-
# accepts that shape and stuffs it under the empty-string named context.
|
|
83
|
-
context = Quonfig::Context.new('foo' => 'bar')
|
|
84
|
-
assert_equal 'bar', context.get('.foo')
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
def test_grouped_key_combines_named_contexts_by_key
|
|
88
|
-
context = Quonfig::Context.new(
|
|
89
|
-
user: { key: 'u1' },
|
|
90
|
-
team: { key: 't1' }
|
|
91
|
-
)
|
|
92
|
-
|
|
93
|
-
# Mirrors sdk-node groupedKey: just the key/trackingId values, sorted
|
|
94
|
-
# and pipe-joined. Contexts without a key/trackingId contribute
|
|
95
|
-
# nothing — so the example aggregator drops anonymous contexts.
|
|
96
|
-
assert_equal 't1|u1', context.grouped_key
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
def test_named_context_lookup_returns_namedcontext
|
|
100
|
-
context = Quonfig::Context.new(user: { key: 'u1', name: 'Ted' })
|
|
101
|
-
user = context.context('user')
|
|
102
|
-
|
|
103
|
-
assert_kind_of Quonfig::Context::NamedContext, user
|
|
104
|
-
assert_equal 'user', user.name
|
|
105
|
-
assert_equal({ 'key' => 'u1', 'name' => 'Ted' }, user.to_h)
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
def test_named_context_lookup_for_missing_returns_empty_namedcontext
|
|
109
|
-
context = Quonfig::Context.new(user: { key: 'u1' })
|
|
110
|
-
missing = context.context('absent')
|
|
111
|
-
|
|
112
|
-
assert_kind_of Quonfig::Context::NamedContext, missing
|
|
113
|
-
assert_equal 'absent', missing.name
|
|
114
|
-
assert_empty missing.to_h
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
def test_comparable
|
|
118
|
-
a = Quonfig::Context.new(user: { key: 'u1' })
|
|
119
|
-
b = Quonfig::Context.new(user: { key: 'u1' })
|
|
120
|
-
c = Quonfig::Context.new(user: { key: 'u2' })
|
|
121
|
-
|
|
122
|
-
assert_equal a, b
|
|
123
|
-
refute_equal a, c
|
|
124
|
-
end
|
|
125
|
-
|
|
126
|
-
private
|
|
127
|
-
|
|
128
|
-
def stringify(hash)
|
|
129
|
-
hash.map { |k, v| [k.to_s, stringify_keys(v)] }.to_h
|
|
130
|
-
end
|
|
131
|
-
|
|
132
|
-
def stringify_keys(value)
|
|
133
|
-
if value.is_a?(Hash)
|
|
134
|
-
value.transform_keys(&:to_s)
|
|
135
|
-
else
|
|
136
|
-
value
|
|
137
|
-
end
|
|
138
|
-
end
|
|
139
|
-
end
|
data/test/test_context_shape.rb
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'test_helper'
|
|
4
|
-
|
|
5
|
-
class TestContextShape < Minitest::Test
|
|
6
|
-
class Email; end
|
|
7
|
-
|
|
8
|
-
def test_field_type_number
|
|
9
|
-
[
|
|
10
|
-
[1, 1],
|
|
11
|
-
[99_999_999_999_999_999_999_999_999_999_999_999_999_999_999, 1],
|
|
12
|
-
[-99_999_999_999_999_999_999_999_999_999_999_999_999_999_999, 1],
|
|
13
|
-
|
|
14
|
-
['a', 2],
|
|
15
|
-
['99999999999999999999999999999999999999999999', 2],
|
|
16
|
-
|
|
17
|
-
[1.0, 4],
|
|
18
|
-
[99_999_999_999_999_999_999_999_999_999_999_999_999_999_999.0, 4],
|
|
19
|
-
[-99_999_999_999_999_999_999_999_999_999_999_999_999_999_999.0, 4],
|
|
20
|
-
|
|
21
|
-
[true, 5],
|
|
22
|
-
[false, 5],
|
|
23
|
-
|
|
24
|
-
[[], 10],
|
|
25
|
-
[[1, 2, 3], 10],
|
|
26
|
-
[%w[a b c], 10],
|
|
27
|
-
|
|
28
|
-
# Unknown / custom types fall back to "string" (2).
|
|
29
|
-
[Email.new, 2]
|
|
30
|
-
].each do |value, expected|
|
|
31
|
-
actual = Quonfig::Telemetry::ContextShape.field_type_number(value)
|
|
32
|
-
|
|
33
|
-
refute_nil actual, "Expected a value for input: #{value.inspect}"
|
|
34
|
-
assert_equal expected, actual, "Expected #{expected} for #{value.inspect}"
|
|
35
|
-
end
|
|
36
|
-
end
|
|
37
|
-
end
|
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'test_helper'
|
|
4
|
-
|
|
5
|
-
class TestContextShapeAggregator < Minitest::Test
|
|
6
|
-
CONTEXT_1 = Quonfig::Context.new(
|
|
7
|
-
'user' => {
|
|
8
|
-
'name' => 'user-name',
|
|
9
|
-
'email' => 'user.email',
|
|
10
|
-
'age' => 42.5
|
|
11
|
-
},
|
|
12
|
-
'subscription' => {
|
|
13
|
-
'plan' => 'advanced',
|
|
14
|
-
'free' => false
|
|
15
|
-
}
|
|
16
|
-
).freeze
|
|
17
|
-
|
|
18
|
-
CONTEXT_2 = Quonfig::Context.new(
|
|
19
|
-
'user' => {
|
|
20
|
-
'name' => 'other-user-name',
|
|
21
|
-
'dob' => '2020-01-01'
|
|
22
|
-
},
|
|
23
|
-
'device' => {
|
|
24
|
-
'name' => 'device-name',
|
|
25
|
-
'os' => 'os-name',
|
|
26
|
-
'version' => 3
|
|
27
|
-
}
|
|
28
|
-
).freeze
|
|
29
|
-
|
|
30
|
-
CONTEXT_3 = Quonfig::Context.new(
|
|
31
|
-
'subscription' => {
|
|
32
|
-
'plan' => 'pro',
|
|
33
|
-
'trial' => true
|
|
34
|
-
}
|
|
35
|
-
).freeze
|
|
36
|
-
|
|
37
|
-
def test_push_respects_max_shapes
|
|
38
|
-
aggregator = Quonfig::Telemetry::ContextShapeAggregator.new(max_shapes: 9)
|
|
39
|
-
|
|
40
|
-
aggregator.push(CONTEXT_1)
|
|
41
|
-
aggregator.push(CONTEXT_2)
|
|
42
|
-
assert_equal 9, aggregator.data.size
|
|
43
|
-
|
|
44
|
-
# At the limit — further shapes get dropped.
|
|
45
|
-
aggregator.push(CONTEXT_3)
|
|
46
|
-
assert_equal 9, aggregator.data.size
|
|
47
|
-
|
|
48
|
-
tuples = aggregator.data.to_a
|
|
49
|
-
assert_includes tuples, ['user', 'name', 2]
|
|
50
|
-
assert_includes tuples, ['user', 'email', 2]
|
|
51
|
-
assert_includes tuples, ['user', 'age', 4]
|
|
52
|
-
assert_includes tuples, ['subscription', 'plan', 2]
|
|
53
|
-
assert_includes tuples, ['subscription', 'free', 5]
|
|
54
|
-
assert_includes tuples, ['device', 'name', 2]
|
|
55
|
-
assert_includes tuples, ['device', 'os', 2]
|
|
56
|
-
assert_includes tuples, ['device', 'version', 1]
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
def test_prepare_data_folds_tuples_and_clears
|
|
60
|
-
aggregator = Quonfig::Telemetry::ContextShapeAggregator.new(max_shapes: 1000)
|
|
61
|
-
|
|
62
|
-
aggregator.push(CONTEXT_1)
|
|
63
|
-
aggregator.push(CONTEXT_2)
|
|
64
|
-
aggregator.push(CONTEXT_3)
|
|
65
|
-
|
|
66
|
-
data = aggregator.prepare_data
|
|
67
|
-
|
|
68
|
-
assert_equal %w[user subscription device].sort, data.keys.sort
|
|
69
|
-
|
|
70
|
-
assert_equal(
|
|
71
|
-
{ 'name' => 2, 'email' => 2, 'dob' => 2, 'age' => 4 },
|
|
72
|
-
data['user']
|
|
73
|
-
)
|
|
74
|
-
|
|
75
|
-
assert_equal(
|
|
76
|
-
{ 'plan' => 2, 'trial' => 5, 'free' => 5 },
|
|
77
|
-
data['subscription']
|
|
78
|
-
)
|
|
79
|
-
|
|
80
|
-
assert_equal(
|
|
81
|
-
{ 'name' => 2, 'os' => 2, 'version' => 1 },
|
|
82
|
-
data['device']
|
|
83
|
-
)
|
|
84
|
-
|
|
85
|
-
assert_equal [], aggregator.data.to_a
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
def test_drain_event_emits_api_telemetry_wire_shape
|
|
89
|
-
aggregator = Quonfig::Telemetry::ContextShapeAggregator.new(max_shapes: 1000)
|
|
90
|
-
aggregator.push(Quonfig::Context.new('user' => { 'key' => 'abc', 'age' => 42 }))
|
|
91
|
-
|
|
92
|
-
event = aggregator.drain_event
|
|
93
|
-
|
|
94
|
-
refute_nil event
|
|
95
|
-
assert event.key?('contextShapes')
|
|
96
|
-
shapes = event['contextShapes']['shapes']
|
|
97
|
-
assert_equal 1, shapes.size
|
|
98
|
-
assert_equal 'user', shapes[0]['name']
|
|
99
|
-
assert_equal({ 'key' => 2, 'age' => 1 }, shapes[0]['fieldTypes'])
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
def test_drain_event_nil_when_empty
|
|
103
|
-
aggregator = Quonfig::Telemetry::ContextShapeAggregator.new(max_shapes: 1000)
|
|
104
|
-
assert_nil aggregator.drain_event
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
def test_push_dedupes_identical_shapes
|
|
108
|
-
aggregator = Quonfig::Telemetry::ContextShapeAggregator.new(max_shapes: 1000)
|
|
109
|
-
|
|
110
|
-
aggregator.push(Quonfig::Context.new('user' => { 'key' => 'a', 'age' => 1 }))
|
|
111
|
-
aggregator.push(Quonfig::Context.new('user' => { 'key' => 'b', 'age' => 2 }))
|
|
112
|
-
|
|
113
|
-
# Same (name, key, type) tuples should have been deduped by the Set.
|
|
114
|
-
assert_equal 2, aggregator.data.size
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
def test_accepts_plain_hash_context
|
|
118
|
-
aggregator = Quonfig::Telemetry::ContextShapeAggregator.new(max_shapes: 1000)
|
|
119
|
-
|
|
120
|
-
aggregator.push('user' => { 'key' => 'abc', 'is_admin' => true })
|
|
121
|
-
|
|
122
|
-
event = aggregator.drain_event
|
|
123
|
-
refute_nil event
|
|
124
|
-
assert_equal({ 'key' => 2, 'is_admin' => 5 }, event['contextShapes']['shapes'][0]['fieldTypes'])
|
|
125
|
-
end
|
|
126
|
-
end
|