mixpanel-ruby 3.2.0 → 3.4.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 +4 -4
- data/CHANGELOG.md +16 -0
- data/Readme.rdoc +1 -1
- data/lib/mixpanel-ruby/flags/custom_operators.rb +207 -0
- data/lib/mixpanel-ruby/flags/flags_provider.rb +42 -5
- data/lib/mixpanel-ruby/flags/local_flags_provider.rb +36 -11
- data/lib/mixpanel-ruby/flags/remote_flags_provider.rb +18 -5
- data/lib/mixpanel-ruby/flags/types.rb +88 -9
- data/lib/mixpanel-ruby/version.rb +1 -1
- data/mixpanel-ruby.gemspec +1 -1
- data/openfeature-provider/CHANGELOG.md +10 -0
- data/openfeature-provider/README.md +29 -1
- data/openfeature-provider/lib/mixpanel/openfeature/provider.rb +30 -4
- data/openfeature-provider/lib/mixpanel/openfeature/version.rb +1 -1
- data/openfeature-provider/mixpanel-ruby-openfeature.gemspec +1 -1
- data/openfeature-provider/spec/mixpanel_openfeature_provider_spec.rb +74 -3
- data/spec/fixtures/datetime_compare_tests.json +100 -0
- data/spec/fixtures/semver_compare_tests.json +161 -0
- data/spec/mixpanel-ruby/flags/custom_operators_spec.rb +74 -0
- data/spec/mixpanel-ruby/flags/local_flags_spec.rb +283 -0
- data/spec/mixpanel-ruby/flags/remote_flags_spec.rb +68 -0
- data/spec/mixpanel-ruby/flags/types_spec.rb +55 -0
- metadata +9 -4
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# mixpanel-ruby-openfeature
|
|
2
2
|
|
|
3
|
-
#####
|
|
3
|
+
##### _July 28, 2026_ - [openfeature/v0.2.0](https://github.com/mixpanel/mixpanel-ruby/releases/tag/openfeature/v0.2.0)
|
|
4
4
|
|
|
5
5
|
[](https://rubygems.org/gems/mixpanel-ruby-openfeature)
|
|
6
6
|
[](https://openfeature.dev/)
|
|
@@ -201,6 +201,34 @@ When you are done using the provider, shut it down to stop any background pollin
|
|
|
201
201
|
provider.shutdown
|
|
202
202
|
```
|
|
203
203
|
|
|
204
|
+
## Async Exposure Tracking
|
|
205
|
+
|
|
206
|
+
By default, every flag evaluation tracks an exposure event inline — the `/track` HTTP round trip happens on the calling thread before `fetch_*_value` returns. For latency-sensitive code paths, pass an `:exposure_executor` so exposure tracking runs off-thread. The executor is duck-typed — anything that responds to `#post(&block)` works:
|
|
207
|
+
|
|
208
|
+
```ruby
|
|
209
|
+
# With concurrent-ruby (recommended)
|
|
210
|
+
require 'concurrent'
|
|
211
|
+
exposure_executor = Concurrent::FixedThreadPool.new(1)
|
|
212
|
+
|
|
213
|
+
provider = Mixpanel::OpenFeature::Provider.from_local(
|
|
214
|
+
'YOUR_PROJECT_TOKEN',
|
|
215
|
+
{ exposure_executor: exposure_executor },
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
# Without concurrent-ruby (minimal wrapper)
|
|
219
|
+
class ThreadPerCall
|
|
220
|
+
def post(&block) = Thread.new(&block)
|
|
221
|
+
end
|
|
222
|
+
provider = Mixpanel::OpenFeature::Provider.from_local(
|
|
223
|
+
'YOUR_PROJECT_TOKEN',
|
|
224
|
+
{ exposure_executor: ThreadPerCall.new },
|
|
225
|
+
)
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
Available on both local and remote flag configs. Defaults to `nil` (inline behavior); existing setups are unaffected.
|
|
229
|
+
|
|
230
|
+
> **Executor lifecycle:** `provider.shutdown` stops the flags provider's own resources (polling) but does NOT shut down a user-supplied executor. Own the executor's lifecycle explicitly — e.g. `at_exit { exposure_executor.shutdown; exposure_executor.wait_for_termination(5) }` for `Concurrent::FixedThreadPool`. Skipping this can leave background threads alive at process exit and delay shutdown.
|
|
231
|
+
|
|
204
232
|
## Error Handling
|
|
205
233
|
|
|
206
234
|
The provider uses OpenFeature's standard error codes to indicate issues during flag evaluation:
|
|
@@ -69,16 +69,41 @@ module Mixpanel
|
|
|
69
69
|
|
|
70
70
|
begin
|
|
71
71
|
result = @flags_provider.get_variant(flag_key, fallback, context, report_exposure: true)
|
|
72
|
-
rescue StandardError
|
|
73
|
-
return error_result(default_value, ::OpenFeature::SDK::Provider::ErrorCode::GENERAL)
|
|
72
|
+
rescue StandardError => e
|
|
73
|
+
return error_result(default_value, ::OpenFeature::SDK::Provider::ErrorCode::GENERAL, e.message)
|
|
74
74
|
end
|
|
75
75
|
|
|
76
|
-
|
|
76
|
+
# variant_source distinguishes local / remote / fallback. When fallback,
|
|
77
|
+
# fallback_reason carries the specific reason (PHP-aligned constants)
|
|
78
|
+
# so we can map each to the spec-correct OpenFeature response instead
|
|
79
|
+
# of collapsing every fallback to FLAG_NOT_FOUND. SDK-83: BACKEND_ERROR
|
|
80
|
+
# also carries the backend message, forwarded as error_message.
|
|
81
|
+
case result.fallback_reason&.kind
|
|
82
|
+
when :flag_not_found
|
|
77
83
|
return ::OpenFeature::SDK::Provider::ResolutionDetails.new(
|
|
78
84
|
value: default_value,
|
|
79
85
|
error_code: ::OpenFeature::SDK::Provider::ErrorCode::FLAG_NOT_FOUND,
|
|
80
86
|
reason: ::OpenFeature::SDK::Provider::Reason::DEFAULT
|
|
81
87
|
)
|
|
88
|
+
when :missing_context_key
|
|
89
|
+
return error_result(
|
|
90
|
+
default_value,
|
|
91
|
+
::OpenFeature::SDK::Provider::ErrorCode::TARGETING_KEY_MISSING,
|
|
92
|
+
result.fallback_reason.message
|
|
93
|
+
)
|
|
94
|
+
when :no_rollout_match
|
|
95
|
+
# Flag exists, user just didn't match any rollout — per the
|
|
96
|
+
# OpenFeature spec this is `reason: DEFAULT` with no error.
|
|
97
|
+
return ::OpenFeature::SDK::Provider::ResolutionDetails.new(
|
|
98
|
+
value: default_value,
|
|
99
|
+
reason: ::OpenFeature::SDK::Provider::Reason::DEFAULT
|
|
100
|
+
)
|
|
101
|
+
when :backend_error
|
|
102
|
+
return error_result(
|
|
103
|
+
default_value,
|
|
104
|
+
::OpenFeature::SDK::Provider::ErrorCode::GENERAL,
|
|
105
|
+
result.fallback_reason.message
|
|
106
|
+
)
|
|
82
107
|
end
|
|
83
108
|
|
|
84
109
|
value = result.variant_value
|
|
@@ -158,10 +183,11 @@ module Mixpanel
|
|
|
158
183
|
end
|
|
159
184
|
end
|
|
160
185
|
|
|
161
|
-
def error_result(default_value, error_code)
|
|
186
|
+
def error_result(default_value, error_code, error_message = nil)
|
|
162
187
|
::OpenFeature::SDK::Provider::ResolutionDetails.new(
|
|
163
188
|
value: default_value,
|
|
164
189
|
error_code: error_code,
|
|
190
|
+
error_message: error_message,
|
|
165
191
|
reason: ::OpenFeature::SDK::Provider::Reason::ERROR
|
|
166
192
|
)
|
|
167
193
|
end
|
|
@@ -17,7 +17,7 @@ Gem::Specification.new do |spec|
|
|
|
17
17
|
spec.require_paths = ['lib']
|
|
18
18
|
|
|
19
19
|
spec.add_runtime_dependency 'openfeature-sdk', '~> 0.5'
|
|
20
|
-
spec.add_runtime_dependency 'mixpanel-ruby', '~> 3.
|
|
20
|
+
spec.add_runtime_dependency 'mixpanel-ruby', '~> 3.3'
|
|
21
21
|
|
|
22
22
|
spec.add_development_dependency 'rspec', '~> 3.0'
|
|
23
23
|
spec.add_development_dependency 'simplecov'
|
|
@@ -75,15 +75,25 @@ RSpec.describe Mixpanel::OpenFeature::Provider do
|
|
|
75
75
|
def setup_flag(flag_key, value, variant_key: 'variant-key')
|
|
76
76
|
allow(mock_flags).to receive(:get_variant) do |key, fallback, _ctx, **_kwargs|
|
|
77
77
|
if key == flag_key
|
|
78
|
-
Mixpanel::Flags::SelectedVariant.new(
|
|
78
|
+
Mixpanel::Flags::SelectedVariant.new(
|
|
79
|
+
variant_key: variant_key,
|
|
80
|
+
variant_value: value,
|
|
81
|
+
variant_source: Mixpanel::Flags::VariantSource::LOCAL
|
|
82
|
+
)
|
|
79
83
|
else
|
|
80
|
-
fallback
|
|
84
|
+
fallback.as_fallback(Mixpanel::Flags::FallbackReason.flag_not_found)
|
|
81
85
|
end
|
|
82
86
|
end
|
|
83
87
|
end
|
|
84
88
|
|
|
89
|
+
def setup_fallback(reason)
|
|
90
|
+
allow(mock_flags).to receive(:get_variant) do |_key, fallback, _ctx, **_kwargs|
|
|
91
|
+
fallback.as_fallback(reason)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
85
95
|
def setup_flag_not_found
|
|
86
|
-
|
|
96
|
+
setup_fallback(Mixpanel::Flags::FallbackReason.flag_not_found)
|
|
87
97
|
end
|
|
88
98
|
|
|
89
99
|
# --- Metadata ---
|
|
@@ -305,6 +315,67 @@ RSpec.describe Mixpanel::OpenFeature::Provider do
|
|
|
305
315
|
end
|
|
306
316
|
end
|
|
307
317
|
|
|
318
|
+
# --- NO_ROLLOUT_MATCH (flag exists, no rollout matched) ---
|
|
319
|
+
|
|
320
|
+
describe 'no rollout match' do
|
|
321
|
+
before { setup_fallback(Mixpanel::Flags::FallbackReason.no_rollout_match) }
|
|
322
|
+
|
|
323
|
+
it 'returns DEFAULT reason without an error code' do
|
|
324
|
+
result = provider.fetch_boolean_value(flag_key: 'flag', default_value: true)
|
|
325
|
+
expect(result.value).to be true
|
|
326
|
+
expect(result.reason).to eq('DEFAULT')
|
|
327
|
+
expect(result.error_code).to be_nil
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
it 'works for strings' do
|
|
331
|
+
result = provider.fetch_string_value(flag_key: 'flag', default_value: 'default')
|
|
332
|
+
expect(result.value).to eq('default')
|
|
333
|
+
expect(result.reason).to eq('DEFAULT')
|
|
334
|
+
expect(result.error_code).to be_nil
|
|
335
|
+
end
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
# --- MISSING_CONTEXT_KEY ---
|
|
339
|
+
|
|
340
|
+
describe 'missing context key' do
|
|
341
|
+
before { setup_fallback(Mixpanel::Flags::FallbackReason.missing_context_key('distinct_id')) }
|
|
342
|
+
|
|
343
|
+
it 'returns TARGETING_KEY_MISSING for boolean' do
|
|
344
|
+
result = provider.fetch_boolean_value(flag_key: 'flag', default_value: false)
|
|
345
|
+
expect(result.value).to be false
|
|
346
|
+
expect(result.error_code).to eq('TARGETING_KEY_MISSING')
|
|
347
|
+
expect(result.reason).to eq('ERROR')
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
it 'returns TARGETING_KEY_MISSING for string and forwards the missing key as error_message' do
|
|
351
|
+
result = provider.fetch_string_value(flag_key: 'flag', default_value: 'default')
|
|
352
|
+
expect(result.value).to eq('default')
|
|
353
|
+
expect(result.error_code).to eq('TARGETING_KEY_MISSING')
|
|
354
|
+
expect(result.error_message).to eq('distinct_id')
|
|
355
|
+
expect(result.reason).to eq('ERROR')
|
|
356
|
+
end
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
# --- BACKEND_ERROR (SDK-83: forwards the backend's response message) ---
|
|
360
|
+
|
|
361
|
+
describe 'backend error' do
|
|
362
|
+
before do
|
|
363
|
+
setup_fallback(
|
|
364
|
+
Mixpanel::Flags::FallbackReason.backend_error(
|
|
365
|
+
'HTTP 400: distinct_id must be provided in evalContext as a string'
|
|
366
|
+
)
|
|
367
|
+
)
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
it 'maps to GENERAL and forwards the backend message as error_message' do
|
|
371
|
+
result = provider.fetch_string_value(flag_key: 'flag', default_value: 'default')
|
|
372
|
+
expect(result.value).to eq('default')
|
|
373
|
+
expect(result.error_code).to eq('GENERAL')
|
|
374
|
+
expect(result.reason).to eq('ERROR')
|
|
375
|
+
expect(result.error_message).to include('distinct_id must be provided')
|
|
376
|
+
end
|
|
377
|
+
end
|
|
378
|
+
|
|
308
379
|
# --- PROVIDER_NOT_READY ---
|
|
309
380
|
|
|
310
381
|
describe 'provider not ready' do
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
[
|
|
2
|
+
"A list of golden vectors for custom operators, to ensure logic parity across platforms",
|
|
3
|
+
|
|
4
|
+
"# Ordering and the six symbols",
|
|
5
|
+
["2026-07-15T00:00:00Z", "<", 1784160000000, true],
|
|
6
|
+
["2026-07-16T00:00:00Z", "<", 1784160000000, false],
|
|
7
|
+
["2026-07-16T00:00:00Z", "===", 1784160000000, true],
|
|
8
|
+
["2026-07-17T00:00:00Z", "!==", 1784160000000, true],
|
|
9
|
+
["2026-07-16T00:00:00Z", ">=", 1784160000000, true],
|
|
10
|
+
["2026-07-17T00:00:00Z", ">", 1784160000000, true],
|
|
11
|
+
["2026-07-15T00:00:00Z", ">", 1784160000000, false],
|
|
12
|
+
["2026-07-16T00:00:00Z", "<=", 1784160000000, true],
|
|
13
|
+
["2026-07-17T00:00:00Z", "<=", 1784160000000, false],
|
|
14
|
+
["2026-07-17T00:00:00Z", "===", 1784160000000, false],
|
|
15
|
+
["2026-07-16T00:00:00Z", "!==", 1784160000000, false],
|
|
16
|
+
["2026-07-15T00:00:00Z", ">=", 1784160000000, false],
|
|
17
|
+
|
|
18
|
+
"# Leap day",
|
|
19
|
+
["2024-02-29T00:00:00Z", "===", 1709164800000, true],
|
|
20
|
+
|
|
21
|
+
"# Time-zone offsets change the instant",
|
|
22
|
+
["2026-07-16T00:00:00+05:30", "===", 1784140200000, true],
|
|
23
|
+
["2026-07-16T02:00:00+02:00", "===", 1784160000000, true],
|
|
24
|
+
["2026-07-16T00:00:00+05:30", "<", 1784160000000, true],
|
|
25
|
+
["2026-07-16T00:00:00-08:00", "===", 1784188800000, true],
|
|
26
|
+
["2026-07-16T00:00:00-08:00", ">", 1784160000000, true],
|
|
27
|
+
["2026-07-16T00:00:00+00:00", "===", 1784160000000, true],
|
|
28
|
+
|
|
29
|
+
"# Sub-second precision is dropped",
|
|
30
|
+
["2026-07-16T00:00:00.5Z", "===", 1784160000000, true],
|
|
31
|
+
["2026-07-16T00:00:00.500Z", "===", 1784160000000, true],
|
|
32
|
+
["2026-07-16T00:00:00.123456Z", "===", 1784160000000, true],
|
|
33
|
+
["2026-07-16T00:00:00.999999999Z", "===", 1784160000000, true],
|
|
34
|
+
["2026-07-16T00:00:00.0Z", "===", 1784160000000, true],
|
|
35
|
+
["2026-07-16T00:00:00.500Z", ">=", 1784160000000, true],
|
|
36
|
+
["2026-07-16T23:59:59Z", "===", 1784246399000, true],
|
|
37
|
+
["2026-07-16T23:59:59Z", "<=", 1784246399000, true],
|
|
38
|
+
["2026-07-16T23:59:59.999Z", "===", 1784246399000, true],
|
|
39
|
+
["2026-07-16T23:59:59.999Z", "<=", 1784246399000, true],
|
|
40
|
+
|
|
41
|
+
"# Trimming and lowercasing",
|
|
42
|
+
["2026-07-16t00:00:00.500z", "===", 1784160000000, true],
|
|
43
|
+
["2026-07-16t02:00:00+02:00", "===", 1784160000000, true],
|
|
44
|
+
[" 2026-07-16T00:00:00Z ", "===", 1784160000000, true],
|
|
45
|
+
["2026-07-16t00:00:00z", "===", 1784160000000, true],
|
|
46
|
+
|
|
47
|
+
"# Wrong shapes, checked under both symbols",
|
|
48
|
+
["2026-7-16T00:00:00Z", "===", 1784160000000, false],
|
|
49
|
+
["2026-7-16T00:00:00Z", "!==", 1784160000000, false],
|
|
50
|
+
["2026-07-16 00:00:00Z", "===", 1784160000000, false],
|
|
51
|
+
["2026-07-16 00:00:00Z", "!==", 1784160000000, false],
|
|
52
|
+
["2026-07-16T00:00:00", "===", 1784160000000, false],
|
|
53
|
+
["2026-07-16T00:00:00", "!==", 1784160000000, false],
|
|
54
|
+
["2026-07-16T00:00:00.Z", "===", 1784160000000, false],
|
|
55
|
+
["2026-07-16T00:00:00.Z", "!==", 1784160000000, false],
|
|
56
|
+
["2026-07-16T00:00:00+0200", "===", 1784160000000, false],
|
|
57
|
+
["2026-07-16T00:00:00+0200", "!==", 1784160000000, false],
|
|
58
|
+
["2026-07-16T00:00:00+02", "===", 1784160000000, false],
|
|
59
|
+
["2026-07-16T00:00:00+02", "!==", 1784160000000, false],
|
|
60
|
+
["2026-07-16T00:00:00Zextra", "===", 1784160000000, false],
|
|
61
|
+
["2026-07-16T00:00:00Zextra", "!==", 1784160000000, false],
|
|
62
|
+
["2026-07-16", "===", 1784160000000, false],
|
|
63
|
+
["2026-07-16", "!==", 1784160000000, false],
|
|
64
|
+
["20260716T000000Z", "===", 1784160000000, false],
|
|
65
|
+
["20260716T000000Z", "!==", 1784160000000, false],
|
|
66
|
+
["2026-07-16T00:00:00z00:00", "===", 1784160000000, false],
|
|
67
|
+
["2026-07-16T00:00:00z00:00", "!==", 1784160000000, false],
|
|
68
|
+
["2026-07-16T00:00:00,5Z", "===", 1784160000000, false],
|
|
69
|
+
["2026-07-16T00:00:00,5Z", "!==", 1784160000000, false],
|
|
70
|
+
|
|
71
|
+
"# Missing or wrong-typed values",
|
|
72
|
+
[1784160000000, "===", 1784160000000, false],
|
|
73
|
+
["2026-07-16T00:00:00Z", "===", 1e19, false],
|
|
74
|
+
["2026-07-16T00:00:00Z", ">", 1e19, false],
|
|
75
|
+
["2026-07-16T00:00:00Z", "<", 1e19, false],
|
|
76
|
+
["2026-07-16", "===", 1784160000000, false],
|
|
77
|
+
["2026-07-16T00:00:00", "===", 1784160000000, false],
|
|
78
|
+
["yesterday", "===", 1784160000000, false],
|
|
79
|
+
[null, "===", 1784160000000, false],
|
|
80
|
+
|
|
81
|
+
"# Targets before 1970",
|
|
82
|
+
["1969-12-31T23:59:59Z", "===", -1000, true],
|
|
83
|
+
["1969-12-31T23:59:59Z", "!==", -1000, false],
|
|
84
|
+
["1969-12-31T23:59:59Z", ">=", -1000, true],
|
|
85
|
+
["1969-12-31T23:59:58Z", "<", -1000, true],
|
|
86
|
+
["1969-12-31T23:59:59Z", ">", -2000, true],
|
|
87
|
+
["1969-12-31T23:59:58.500Z", "===", -2000, true],
|
|
88
|
+
["1969-12-31T23:59:58.500Z", "!==", -1000, true],
|
|
89
|
+
|
|
90
|
+
"# Impossible dates and out-of-range fields",
|
|
91
|
+
["2026-02-30T00:00:00Z", "===", 1784160000000, false],
|
|
92
|
+
["2026-02-30T00:00:00Z", "!==", 1784160000000, false],
|
|
93
|
+
["2026-02-29T00:00:00Z", "!==", 1784160000000, false],
|
|
94
|
+
["2025-02-29T00:00:00Z", "!==", 1784160000000, false],
|
|
95
|
+
["2026-04-31T00:00:00Z", "!==", 1784160000000, false],
|
|
96
|
+
["2026-06-31T00:00:00Z", "!==", 1784160000000, false],
|
|
97
|
+
["2026-07-16T24:00:00Z", "!==", 1784160000000, false],
|
|
98
|
+
["2026-13-01T00:00:00Z", "!==", 1784160000000, false],
|
|
99
|
+
["2026-01-32T00:00:00Z", "!==", 1784160000000, false]
|
|
100
|
+
]
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
[
|
|
2
|
+
"A list of golden vectors for custom operators, to ensure logic parity across platforms",
|
|
3
|
+
|
|
4
|
+
"# Ordering and the six symbols",
|
|
5
|
+
["1.2.3", "===", "1.2.3", true],
|
|
6
|
+
["1.2.4", "===", "1.2.3", false],
|
|
7
|
+
["1.2.4", "!==", "1.2.3", true],
|
|
8
|
+
["1.2.2", "<", "1.2.3", true],
|
|
9
|
+
["1.2.3", "<", "1.2.3", false],
|
|
10
|
+
["1.2.3", "<=", "1.2.3", true],
|
|
11
|
+
["1.3.0", ">", "1.2.3", true],
|
|
12
|
+
["1.2.3", ">=", "1.2.3", true],
|
|
13
|
+
["1.10.0", ">", "1.9.0", true],
|
|
14
|
+
["10.0.0", ">", "9.0.0", true],
|
|
15
|
+
["1.0.10", ">", "1.0.9", true],
|
|
16
|
+
["2.0.0", ">", "1.9.9", true],
|
|
17
|
+
["1.0.0-alpha", "<", "1.0.0", true],
|
|
18
|
+
["v1.2.3", "===", "1.2.3", true],
|
|
19
|
+
["1.2.0", "===", "1.2", true],
|
|
20
|
+
[" 1.2.3 ", "===", "1.2.3", true],
|
|
21
|
+
["1.2.3", "!==", "1.2.3", false],
|
|
22
|
+
["1.2.4", "<=", "1.2.3", false],
|
|
23
|
+
["1.2.2", ">", "1.2.3", false],
|
|
24
|
+
["1.2.2", ">=", "1.2.3", false],
|
|
25
|
+
|
|
26
|
+
"# Pre-release ordering",
|
|
27
|
+
["1.0.0-alpha", "<", "1.0.0-beta", true],
|
|
28
|
+
["1.0.0-beta", "<", "1.0.0-rc1", true],
|
|
29
|
+
["1.0.0-rc1", "<", "1.0.0-rc2", true],
|
|
30
|
+
["1.0.0-alpha", "<", "1.0.0-alpha.1", true],
|
|
31
|
+
["1.0.0-alpha.1", "<", "1.0.0-alpha.beta", true],
|
|
32
|
+
["1.0.0-alpha", "<", "1.0.0-alpha.beta", true],
|
|
33
|
+
["1.0.0-beta.2", "<", "1.0.0-beta.11", true],
|
|
34
|
+
["1.0.0-a.1", "<", "1.0.0-b.1", true],
|
|
35
|
+
["1.0.0-a.1", "<", "1.0.0-a.2", true],
|
|
36
|
+
["1.0.0-rc1", "===", "1.0.0-rc1", true],
|
|
37
|
+
["1.0.0-rc1", ">", "1.0.0-rc.1", true],
|
|
38
|
+
["2.0.0-alpha", ">", "1.9.9", true],
|
|
39
|
+
|
|
40
|
+
"# A pre-release and a plain release, compared directly",
|
|
41
|
+
["1.0.0", ">", "1.0.0-alpha", true],
|
|
42
|
+
["1.0.0", ">=", "1.0.0-rc1", true],
|
|
43
|
+
["1.0.0", "!==", "1.0.0-alpha", true],
|
|
44
|
+
["1.0.0-alpha", "!==", "1.0.0", true],
|
|
45
|
+
["1.0.0-alpha", "<=", "1.0.0", true],
|
|
46
|
+
["1.0.0-alpha", ">", "0.9.9", true],
|
|
47
|
+
["1.0.0-rc1", "<", "1.0.1", true],
|
|
48
|
+
|
|
49
|
+
"# How pre-release identifiers compare, SemVer 2.0.0 item 11",
|
|
50
|
+
["1.0.0-2", "<", "1.0.0-10", true],
|
|
51
|
+
["1.0.0-1", "<", "1.0.0-alpha", true],
|
|
52
|
+
["1.0.0-alpha", "<", "1.0.0-alpha-1", true],
|
|
53
|
+
["1.0.0-beta.11", "<", "1.0.0-rc.1", true],
|
|
54
|
+
["1.0.0-rc.1", "<", "1.0.0", true],
|
|
55
|
+
["1.0.0-alpha.1.2.3", "<", "1.0.0-beta", true],
|
|
56
|
+
["1.0.0-beta", ">", "1.0.0-alpha.1", true],
|
|
57
|
+
|
|
58
|
+
"# Build metadata is ignored",
|
|
59
|
+
["1.0.0+build1", "===", "1.0.0+build2", true],
|
|
60
|
+
["1.0.0-alpha+build", "===", "1.0.0-alpha", true],
|
|
61
|
+
["1.2.3+build.1-2", "===", "1.2.3", true],
|
|
62
|
+
["1.0.0+build1", "!==", "1.0.0+build2", false],
|
|
63
|
+
["1.0.0+build1", "<", "1.0.0+build2", false],
|
|
64
|
+
["1.0.0+build1", ">", "1.0.0+build2", false],
|
|
65
|
+
["1.0.0+build1", "<=", "1.0.0+build2", true],
|
|
66
|
+
["1.0.0+build1", ">=", "1.0.0+build2", true],
|
|
67
|
+
["1.0.0+build9", "<", "1.0.1+build1", true],
|
|
68
|
+
["1.0.1+build1", ">", "1.0.0+build9", true],
|
|
69
|
+
|
|
70
|
+
"# Partial versions",
|
|
71
|
+
["1.2-alpha", "===", "1.2.0-alpha", true],
|
|
72
|
+
["1.2-alpha", "<", "1.3.1", true],
|
|
73
|
+
["1.2-alpha", "<", "1.2.0", true],
|
|
74
|
+
["1-rc1", "<", "1.0.0", true],
|
|
75
|
+
["1.2+build", "===", "1.2.0", true],
|
|
76
|
+
|
|
77
|
+
"# Zero versions",
|
|
78
|
+
["0.0.0", "===", "0.0.0", true],
|
|
79
|
+
["0.0.0", "<", "0.0.1", true],
|
|
80
|
+
["0", "===", "0.0.0", true],
|
|
81
|
+
|
|
82
|
+
"# A version ending in a bare hyphen is rejected",
|
|
83
|
+
["1.0.0-", "===", "1.0.0", false],
|
|
84
|
+
["1.0.0-", "!==", "1.0.0", false],
|
|
85
|
+
["1.2-", "===", "1.2.0", false],
|
|
86
|
+
["1.2-", "!==", "1.2.0", false],
|
|
87
|
+
|
|
88
|
+
"# Hyphens inside a pre-release are fine, since they are part of the pre-release identifier",
|
|
89
|
+
["1.0.0-alpha-", "<", "1.0.0", true],
|
|
90
|
+
|
|
91
|
+
"# Leading zeros are rejected",
|
|
92
|
+
["01.2.3", "===", "1.2.3", false],
|
|
93
|
+
["01.2.3", "!==", "1.2.3", false],
|
|
94
|
+
["1.02.3", "===", "1.2.3", false],
|
|
95
|
+
["1.02.3", "!==", "1.2.3", false],
|
|
96
|
+
["1.2.03", "===", "1.2.3", false],
|
|
97
|
+
["1.2.03", "!==", "1.2.3", false],
|
|
98
|
+
["01.02.03", "===", "1.2.3", false],
|
|
99
|
+
["01.02.03", "!==", "1.2.3", false],
|
|
100
|
+
|
|
101
|
+
"# Leading zeros in a numeric pre-release are rejected too, SemVer 2.0.0 item 9",
|
|
102
|
+
["1.2.3-01", "===", "1.2.3", false],
|
|
103
|
+
["1.2.3-01", "!==", "1.2.3", false],
|
|
104
|
+
["1.2.3-rc.01", "===", "1.2.3", false],
|
|
105
|
+
["1.2.3-rc.01", "!==", "1.2.3", false],
|
|
106
|
+
|
|
107
|
+
"# A lone zero is a legal pre-release identifier",
|
|
108
|
+
["1.2.3-0", "<", "1.2.3", true],
|
|
109
|
+
|
|
110
|
+
"# Digits inside a word are still valid",
|
|
111
|
+
["1.2.3-rc01", "<", "1.2.3", true],
|
|
112
|
+
|
|
113
|
+
"# A leading v is accepted, in either case",
|
|
114
|
+
["V1.2.3", "===", "1.2.3", true],
|
|
115
|
+
["v1.0.0-alpha", "<", "1.0.0", true],
|
|
116
|
+
["v1.2.4", "!==", "1.2.3", true],
|
|
117
|
+
["v1.2.3", "<=", "1.2.3", true],
|
|
118
|
+
["v1.2.4", ">", "1.2.3", true],
|
|
119
|
+
["v1.2.3", ">=", "1.2.3", true],
|
|
120
|
+
|
|
121
|
+
"# Missing or wrong-typed values",
|
|
122
|
+
["not-a-version", "===", "1.2.3", false],
|
|
123
|
+
[123, "===", "1.2.3", false],
|
|
124
|
+
[null, "===", "1.2.3", false],
|
|
125
|
+
|
|
126
|
+
"# Malformed versions, checked under both symbols",
|
|
127
|
+
["", "===", "1.2.3", false],
|
|
128
|
+
["", "!==", "1.2.3", false],
|
|
129
|
+
["v", "===", "1.2.3", false],
|
|
130
|
+
["v", "!==", "1.2.3", false],
|
|
131
|
+
["-1.2.3", "===", "1.2.3", false],
|
|
132
|
+
["-1.2.3", "!==", "1.2.3", false],
|
|
133
|
+
["1.", "===", "1.2.3", false],
|
|
134
|
+
["1.", "!==", "1.2.3", false],
|
|
135
|
+
["1.2.3.", "===", "1.2.3", false],
|
|
136
|
+
["1.2.3.", "!==", "1.2.3", false],
|
|
137
|
+
["1..2", "===", "1.2.3", false],
|
|
138
|
+
["1..2", "!==", "1.2.3", false],
|
|
139
|
+
["1.2.3.4", "===", "1.2.3", false],
|
|
140
|
+
["1.2.3.4", "!==", "1.2.3", false],
|
|
141
|
+
["^1.2.3", "===", "1.2.3", false],
|
|
142
|
+
["^1.2.3", "!==", "1.2.3", false],
|
|
143
|
+
["abc1.2.3", "===", "1.2.3", false],
|
|
144
|
+
["abc1.2.3", "!==", "1.2.3", false],
|
|
145
|
+
["1.2.3+", "===", "1.2.3", false],
|
|
146
|
+
["1.2.3+", "!==", "1.2.3", false],
|
|
147
|
+
["1.2.3-alpha..1", "===", "1.2.3", false],
|
|
148
|
+
["1.2.3-alpha..1", "!==", "1.2.3", false],
|
|
149
|
+
["1.2.3-.", "===", "1.2.3", false],
|
|
150
|
+
["1.2.3-.", "!==", "1.2.3", false],
|
|
151
|
+
["1.2.3-ALPHA_BETA", "===", "1.2.3", false],
|
|
152
|
+
["1.2.3-ALPHA_BETA", "!==", "1.2.3", false],
|
|
153
|
+
["vv1.2.3", "===", "1.2.3", false],
|
|
154
|
+
["vv1.2.3", "!==", "1.2.3", false],
|
|
155
|
+
|
|
156
|
+
"# Operand length cap",
|
|
157
|
+
["1.2.3-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "===", "1.2.3", false],
|
|
158
|
+
["1.2.3-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "!==", "1.2.3", false],
|
|
159
|
+
["1.2.3", "!==", "1.2.3-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", false],
|
|
160
|
+
["1.2.3-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "!==", "1.2.3", true]
|
|
161
|
+
]
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
require 'rspec'
|
|
3
|
+
require 'json_logic'
|
|
4
|
+
require 'mixpanel-ruby/flags/custom_operators'
|
|
5
|
+
|
|
6
|
+
# The golden vectors are the cross-SDK contract for the custom operators; the canonical copy and its
|
|
7
|
+
# README live in the analytics monorepo. Cases run through JsonLogic.apply so that operator
|
|
8
|
+
# registration is covered alongside the comparison itself.
|
|
9
|
+
#
|
|
10
|
+
# Defined at the top level so the case tables can be built while the describe blocks are collected.
|
|
11
|
+
FIXTURES = File.expand_path('../../fixtures', __dir__)
|
|
12
|
+
|
|
13
|
+
# The property key the vectors are evaluated against. It is plumbing the spec supplies, so any name
|
|
14
|
+
# works as long as the rule and the data agree on it.
|
|
15
|
+
VECTOR_KEY = 'value'.freeze
|
|
16
|
+
|
|
17
|
+
def rule_for(operator, symbol, target)
|
|
18
|
+
{ "#{operator}_compare" => [{ 'var' => VECTOR_KEY }, symbol, target] }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Build the event the rule reads from, omitting the key entirely for an unset property.
|
|
22
|
+
def data_for(subject)
|
|
23
|
+
subject.nil? ? {} : { VECTOR_KEY => subject }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Read a golden-vector file. String entries are headings, array entries are cases.
|
|
27
|
+
def load_vectors(operator)
|
|
28
|
+
entries = JSON.parse(File.read(File.join(FIXTURES, "#{operator}_compare_tests.json")))
|
|
29
|
+
|
|
30
|
+
section = ''
|
|
31
|
+
cases = []
|
|
32
|
+
entries.each_with_index do |entry, index|
|
|
33
|
+
if entry.is_a?(String)
|
|
34
|
+
section = entry
|
|
35
|
+
next
|
|
36
|
+
end
|
|
37
|
+
subject, symbol, target, want = entry
|
|
38
|
+
name = "#{index} #{section}: #{subject.to_json} #{symbol} #{target.to_json}"
|
|
39
|
+
cases << [name, rule_for(operator, symbol, target), data_for(subject), want]
|
|
40
|
+
end
|
|
41
|
+
cases
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
SEMVER_CASES = load_vectors('semver')
|
|
45
|
+
DATETIME_CASES = load_vectors('datetime')
|
|
46
|
+
|
|
47
|
+
describe Mixpanel::Flags::CustomOperators do
|
|
48
|
+
def apply(rule, data)
|
|
49
|
+
JsonLogic.apply(rule, data)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
describe 'semver_compare' do
|
|
53
|
+
SEMVER_CASES.each do |name, rule, data, want|
|
|
54
|
+
it name do
|
|
55
|
+
expect(apply(rule, data)).to eq(want)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
describe 'datetime_compare' do
|
|
61
|
+
DATETIME_CASES.each do |name, rule, data, want|
|
|
62
|
+
it name do
|
|
63
|
+
expect(apply(rule, data)).to eq(want)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# An unset property must produce an event with no key at all, rather than a key holding a nil.
|
|
69
|
+
# Both spellings fail closed, so the vectors alone cannot tell them apart.
|
|
70
|
+
it 'omits the property for an unset subject' do
|
|
71
|
+
expect(data_for(nil)).to eq({})
|
|
72
|
+
expect(data_for('1.2.3')).to eq({ VECTOR_KEY => '1.2.3' })
|
|
73
|
+
end
|
|
74
|
+
end
|