mixpanel-ruby 3.1.0 → 3.2.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/.github/dependabot.yml +14 -0
- data/.github/modules.json +18 -0
- data/.github/scripts/generate-changelog.sh +87 -0
- data/.github/workflows/pr-title-check.yml +51 -0
- data/.github/workflows/prepare-release.yml +189 -0
- data/.github/workflows/release-rubygems.yml +217 -0
- data/CHANGELOG.md +30 -0
- data/Readme.rdoc +65 -0
- data/lib/mixpanel-ruby/consumer.rb +54 -6
- data/lib/mixpanel-ruby/credentials.rb +26 -0
- data/lib/mixpanel-ruby/events.rb +69 -11
- data/lib/mixpanel-ruby/flags/flags_provider.rb +16 -2
- data/lib/mixpanel-ruby/flags/local_flags_provider.rb +85 -18
- data/lib/mixpanel-ruby/flags/remote_flags_provider.rb +4 -2
- data/lib/mixpanel-ruby/tracker.rb +39 -9
- data/lib/mixpanel-ruby/version.rb +1 -1
- data/lib/mixpanel-ruby.rb +1 -0
- data/openfeature-provider/CHANGELOG.md +5 -0
- data/openfeature-provider/README.md +2 -0
- data/openfeature-provider/lib/mixpanel/openfeature/version.rb +7 -0
- data/openfeature-provider/lib/mixpanel/openfeature.rb +1 -0
- data/openfeature-provider/mixpanel-ruby-openfeature.gemspec +3 -1
- data/spec/mixpanel-ruby/consumer_spec.rb +81 -0
- data/spec/mixpanel-ruby/credentials_security_spec.rb +108 -0
- data/spec/mixpanel-ruby/credentials_spec.rb +54 -0
- data/spec/mixpanel-ruby/events_spec.rb +152 -0
- data/spec/mixpanel-ruby/flags/local_flags_spec.rb +209 -2
- data/spec/mixpanel-ruby/flags/remote_flags_spec.rb +36 -0
- data/spec/mixpanel-ruby/tracker_spec.rb +18 -0
- metadata +14 -2
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
require 'json'
|
|
2
|
+
require 'timeout'
|
|
2
3
|
require 'mixpanel-ruby/flags/local_flags_provider'
|
|
3
4
|
require 'mixpanel-ruby/flags/types'
|
|
5
|
+
require 'mixpanel-ruby/credentials'
|
|
4
6
|
require 'webmock/rspec'
|
|
5
7
|
|
|
6
8
|
describe Mixpanel::Flags::LocalFlagsProvider do
|
|
@@ -16,7 +18,8 @@ describe Mixpanel::Flags::LocalFlagsProvider do
|
|
|
16
18
|
test_token,
|
|
17
19
|
config,
|
|
18
20
|
mock_tracker,
|
|
19
|
-
mock_error_handler
|
|
21
|
+
mock_error_handler,
|
|
22
|
+
nil # credentials
|
|
20
23
|
)
|
|
21
24
|
end
|
|
22
25
|
|
|
@@ -100,6 +103,34 @@ describe Mixpanel::Flags::LocalFlagsProvider do
|
|
|
100
103
|
}
|
|
101
104
|
end
|
|
102
105
|
|
|
106
|
+
describe '#initialize' do
|
|
107
|
+
it 'raises ArgumentError when polling_interval_in_seconds is not a positive number' do
|
|
108
|
+
[0, -1, 'sixty', :foo].each do |bad_value|
|
|
109
|
+
expect do
|
|
110
|
+
Mixpanel::Flags::LocalFlagsProvider.new(
|
|
111
|
+
test_token,
|
|
112
|
+
{ polling_interval_in_seconds: bad_value },
|
|
113
|
+
mock_tracker,
|
|
114
|
+
mock_error_handler
|
|
115
|
+
)
|
|
116
|
+
end.to raise_error(ArgumentError, /polling_interval_in_seconds/)
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
it 'falls back to the default polling_interval_in_seconds when caller passes nil' do
|
|
121
|
+
# nil must not silently override the default — CV#wait(mutex, nil) blocks
|
|
122
|
+
# indefinitely, which would silently disable polling.
|
|
123
|
+
expect do
|
|
124
|
+
Mixpanel::Flags::LocalFlagsProvider.new(
|
|
125
|
+
test_token,
|
|
126
|
+
{ polling_interval_in_seconds: nil },
|
|
127
|
+
mock_tracker,
|
|
128
|
+
mock_error_handler
|
|
129
|
+
)
|
|
130
|
+
end.not_to raise_error
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
103
134
|
describe '#get_variant_value' do
|
|
104
135
|
it 'returns fallback when no flag definitions' do
|
|
105
136
|
stub_flag_definitions([])
|
|
@@ -741,7 +772,8 @@ describe Mixpanel::Flags::LocalFlagsProvider do
|
|
|
741
772
|
test_token,
|
|
742
773
|
{ enable_polling: true, polling_interval_in_seconds: 0.1 },
|
|
743
774
|
mock_tracker,
|
|
744
|
-
mock_error_handler
|
|
775
|
+
mock_error_handler,
|
|
776
|
+
nil # credentials
|
|
745
777
|
)
|
|
746
778
|
|
|
747
779
|
begin
|
|
@@ -755,5 +787,180 @@ describe Mixpanel::Flags::LocalFlagsProvider do
|
|
|
755
787
|
polling_provider.stop_polling_for_definitions!
|
|
756
788
|
end
|
|
757
789
|
end
|
|
790
|
+
|
|
791
|
+
it 'shuts down promptly while the polling thread is waiting on the interval' do
|
|
792
|
+
flag = create_test_flag
|
|
793
|
+
stub_flag_definitions([flag])
|
|
794
|
+
|
|
795
|
+
polling_provider = Mixpanel::Flags::LocalFlagsProvider.new(
|
|
796
|
+
test_token,
|
|
797
|
+
# A long interval ensures the polling thread is parked on the wait when
|
|
798
|
+
# shutdown is requested — pre-fix, the join would have to ride out the
|
|
799
|
+
# full interval before the thread checked @stop_polling.
|
|
800
|
+
{ enable_polling: true, polling_interval_in_seconds: 30 },
|
|
801
|
+
mock_tracker,
|
|
802
|
+
mock_error_handler
|
|
803
|
+
)
|
|
804
|
+
|
|
805
|
+
polling_provider.start_polling_for_definitions!
|
|
806
|
+
begin
|
|
807
|
+
# Wait until the polling thread is parked on the condition variable
|
|
808
|
+
# (status 'sleep') so the spec deterministically exercises the CV-wakeup
|
|
809
|
+
# path.
|
|
810
|
+
polling_thread = polling_provider.instance_variable_get(:@polling_thread)
|
|
811
|
+
Timeout.timeout(2) { sleep 0.01 until polling_thread.status == 'sleep' }
|
|
812
|
+
|
|
813
|
+
t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
814
|
+
polling_provider.stop_polling_for_definitions!
|
|
815
|
+
elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0
|
|
816
|
+
expect(elapsed).to be < 1.0
|
|
817
|
+
ensure
|
|
818
|
+
polling_provider.stop_polling_for_definitions!
|
|
819
|
+
end
|
|
820
|
+
end
|
|
821
|
+
|
|
822
|
+
it 'shuts down promptly when stop races an in-flight fetch' do
|
|
823
|
+
flag = create_test_flag
|
|
824
|
+
fetch_count = 0
|
|
825
|
+
fetch_count_mutex = Mutex.new
|
|
826
|
+
second_fetch_started = Queue.new
|
|
827
|
+
second_fetch_release = Queue.new
|
|
828
|
+
|
|
829
|
+
stub_request(:get, endpoint_url_regex)
|
|
830
|
+
.to_return do |_request|
|
|
831
|
+
this_call = fetch_count_mutex.synchronize { fetch_count += 1 }
|
|
832
|
+
if this_call == 2
|
|
833
|
+
second_fetch_started << true
|
|
834
|
+
second_fetch_release.pop
|
|
835
|
+
end
|
|
836
|
+
{
|
|
837
|
+
status: 200,
|
|
838
|
+
body: { code: 200, flags: [flag] }.to_json,
|
|
839
|
+
headers: { 'Content-Type' => 'application/json' }
|
|
840
|
+
}
|
|
841
|
+
end
|
|
842
|
+
|
|
843
|
+
polling_provider = Mixpanel::Flags::LocalFlagsProvider.new(
|
|
844
|
+
test_token,
|
|
845
|
+
# 1 s interval — large enough that the lost-wakeup race (if reintroduced)
|
|
846
|
+
# would be detectable as ~1 s of extra teardown after the fetch finishes,
|
|
847
|
+
# but small enough that the second polling-thread fetch starts quickly.
|
|
848
|
+
{ enable_polling: true, polling_interval_in_seconds: 1.0 },
|
|
849
|
+
mock_tracker,
|
|
850
|
+
mock_error_handler
|
|
851
|
+
)
|
|
852
|
+
|
|
853
|
+
polling_provider.start_polling_for_definitions!
|
|
854
|
+
begin
|
|
855
|
+
# Bounded wait: if a regression keeps the polling thread from reaching
|
|
856
|
+
# the second fetch, fail fast rather than hanging the suite.
|
|
857
|
+
Timeout.timeout(5) { second_fetch_started.pop }
|
|
858
|
+
|
|
859
|
+
# Trigger shutdown while the polling thread is mid-fetch (NOT on the
|
|
860
|
+
# CV), so the broadcast goes to no waiter. Run it in a thread so we can
|
|
861
|
+
# release the fetch afterwards.
|
|
862
|
+
stopper = Thread.new { polling_provider.stop_polling_for_definitions! }
|
|
863
|
+
# Wait until the stopper has set @stop_polling and broadcast, so
|
|
864
|
+
# releasing the fetch deterministically lands in the lost-wakeup window
|
|
865
|
+
# this spec targets.
|
|
866
|
+
Timeout.timeout(2) { sleep 0.01 until polling_provider.instance_variable_get(:@stop_polling) }
|
|
867
|
+
|
|
868
|
+
t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
869
|
+
second_fetch_release << true
|
|
870
|
+
stopper.join
|
|
871
|
+
elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0
|
|
872
|
+
|
|
873
|
+
# With the predicate-inside-mutex check, the polling thread re-enters
|
|
874
|
+
# the mutex after fetch, sees @stop_polling = true, skips the wait, and
|
|
875
|
+
# breaks immediately. Without it, the thread would call wait(1.0 s),
|
|
876
|
+
# ride out the full interval, and only then break — elapsed would be
|
|
877
|
+
# ~1 s.
|
|
878
|
+
expect(elapsed).to be < 0.5
|
|
879
|
+
ensure
|
|
880
|
+
# Unblock any in-flight stub fetch in case we raised before releasing
|
|
881
|
+
# it normally — otherwise the polling thread stays blocked at
|
|
882
|
+
# second_fetch_release.pop and stop_polling_for_definitions!'s join
|
|
883
|
+
# would hang the suite.
|
|
884
|
+
second_fetch_release << true
|
|
885
|
+
polling_provider.stop_polling_for_definitions!
|
|
886
|
+
end
|
|
887
|
+
end
|
|
888
|
+
|
|
889
|
+
it 'stop arriving during the initial fetch wins; start does not spawn a thread' do
|
|
890
|
+
fetch_in_progress = Queue.new
|
|
891
|
+
fetch_release = Queue.new
|
|
892
|
+
|
|
893
|
+
stub_request(:get, endpoint_url_regex).to_return do |_req|
|
|
894
|
+
fetch_in_progress << true
|
|
895
|
+
fetch_release.pop
|
|
896
|
+
{
|
|
897
|
+
status: 200,
|
|
898
|
+
body: { code: 200, flags: [] }.to_json,
|
|
899
|
+
headers: { 'Content-Type' => 'application/json' }
|
|
900
|
+
}
|
|
901
|
+
end
|
|
902
|
+
|
|
903
|
+
polling_provider = Mixpanel::Flags::LocalFlagsProvider.new(
|
|
904
|
+
test_token,
|
|
905
|
+
{ enable_polling: true, polling_interval_in_seconds: 30 },
|
|
906
|
+
mock_tracker,
|
|
907
|
+
mock_error_handler
|
|
908
|
+
)
|
|
909
|
+
|
|
910
|
+
starter = Thread.new { polling_provider.start_polling_for_definitions! }
|
|
911
|
+
begin
|
|
912
|
+
# Wait until start is blocked inside the initial fetch.
|
|
913
|
+
Timeout.timeout(5) { fetch_in_progress.pop }
|
|
914
|
+
|
|
915
|
+
# Stop arrives while start is mid-fetch (start has already cleared
|
|
916
|
+
# @stop_polling, so this is the bad case where stop's set must "win"
|
|
917
|
+
# despite happening between start's clear and start's spawn-decision).
|
|
918
|
+
polling_provider.stop_polling_for_definitions!
|
|
919
|
+
|
|
920
|
+
# Let start finish its fetch and reach the lifecycle check.
|
|
921
|
+
fetch_release << true
|
|
922
|
+
starter.join
|
|
923
|
+
|
|
924
|
+
# No polling thread should have been spawned — stop's postcondition
|
|
925
|
+
# (polling is off when stop returns) must hold even when start was
|
|
926
|
+
# mid-fetch at the time of the stop call.
|
|
927
|
+
expect(polling_provider.instance_variable_get(:@polling_thread)).to be_nil
|
|
928
|
+
ensure
|
|
929
|
+
fetch_release << true
|
|
930
|
+
starter&.join
|
|
931
|
+
polling_provider.stop_polling_for_definitions!
|
|
932
|
+
end
|
|
933
|
+
end
|
|
934
|
+
end
|
|
935
|
+
|
|
936
|
+
describe 'service account credentials' do
|
|
937
|
+
it 'uses service account credentials for authentication' do
|
|
938
|
+
credentials = Mixpanel::ServiceAccountCredentials.new('test-user', 'test-secret', 'test-project')
|
|
939
|
+
flag = create_test_flag
|
|
940
|
+
|
|
941
|
+
stub_request(:get, endpoint_url_regex)
|
|
942
|
+
.with(
|
|
943
|
+
basic_auth: ['test-user', 'test-secret']
|
|
944
|
+
)
|
|
945
|
+
.to_return(
|
|
946
|
+
status: 200,
|
|
947
|
+
body: { code: 200, flags: [flag] }.to_json,
|
|
948
|
+
headers: { 'Content-Type' => 'application/json' }
|
|
949
|
+
)
|
|
950
|
+
|
|
951
|
+
credentials_provider = Mixpanel::Flags::LocalFlagsProvider.new(
|
|
952
|
+
test_token,
|
|
953
|
+
config,
|
|
954
|
+
mock_tracker,
|
|
955
|
+
mock_error_handler,
|
|
956
|
+
credentials
|
|
957
|
+
)
|
|
958
|
+
|
|
959
|
+
credentials_provider.start_polling_for_definitions!
|
|
960
|
+
result = credentials_provider.get_variant_value('test_flag', 'fallback', test_context, report_exposure: false)
|
|
961
|
+
|
|
962
|
+
expect(result).not_to eq('fallback')
|
|
963
|
+
credentials_provider.stop_polling_for_definitions!
|
|
964
|
+
end
|
|
758
965
|
end
|
|
759
966
|
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
require 'json'
|
|
2
2
|
require 'mixpanel-ruby/flags/remote_flags_provider'
|
|
3
3
|
require 'mixpanel-ruby/flags/types'
|
|
4
|
+
require 'mixpanel-ruby/credentials'
|
|
4
5
|
require 'webmock/rspec'
|
|
5
6
|
|
|
6
7
|
describe Mixpanel::Flags::RemoteFlagsProvider do
|
|
@@ -17,6 +18,7 @@ describe Mixpanel::Flags::RemoteFlagsProvider do
|
|
|
17
18
|
config,
|
|
18
19
|
mock_tracker,
|
|
19
20
|
mock_error_handler
|
|
21
|
+
# credentials defaults to nil
|
|
20
22
|
)
|
|
21
23
|
end
|
|
22
24
|
|
|
@@ -438,4 +440,38 @@ describe Mixpanel::Flags::RemoteFlagsProvider do
|
|
|
438
440
|
provider.send(:track_exposure_event, 'test_flag', variant, test_context)
|
|
439
441
|
end
|
|
440
442
|
end
|
|
443
|
+
|
|
444
|
+
describe 'service account credentials' do
|
|
445
|
+
it 'uses service account credentials for authentication' do
|
|
446
|
+
credentials = Mixpanel::ServiceAccountCredentials.new('test-user', 'test-secret', 'test-project')
|
|
447
|
+
|
|
448
|
+
response = create_success_response({
|
|
449
|
+
'test_flag' => {
|
|
450
|
+
'variant_key' => 'treatment',
|
|
451
|
+
'variant_value' => 'treatment'
|
|
452
|
+
}
|
|
453
|
+
})
|
|
454
|
+
|
|
455
|
+
stub_request(:get, endpoint_url_regex)
|
|
456
|
+
.with(
|
|
457
|
+
basic_auth: ['test-user', 'test-secret']
|
|
458
|
+
)
|
|
459
|
+
.to_return(
|
|
460
|
+
status: 200,
|
|
461
|
+
body: response.to_json,
|
|
462
|
+
headers: { 'Content-Type' => 'application/json' }
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
credentials_provider = Mixpanel::Flags::RemoteFlagsProvider.new(
|
|
466
|
+
test_token,
|
|
467
|
+
config,
|
|
468
|
+
mock_tracker,
|
|
469
|
+
mock_error_handler,
|
|
470
|
+
credentials
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
result = credentials_provider.get_variant_value('test_flag', 'fallback', test_context, report_exposure: false)
|
|
474
|
+
expect(result).to eq('treatment')
|
|
475
|
+
end
|
|
476
|
+
end
|
|
441
477
|
end
|
|
@@ -131,4 +131,22 @@ describe Mixpanel::Tracker do
|
|
|
131
131
|
expect(expect).to eq(found)
|
|
132
132
|
end
|
|
133
133
|
end
|
|
134
|
+
|
|
135
|
+
describe 'service account credentials' do
|
|
136
|
+
it 'should pass credentials to flags providers when passed directly' do
|
|
137
|
+
credentials = Mixpanel::ServiceAccountCredentials.new('user', 'secret', 'project123')
|
|
138
|
+
|
|
139
|
+
tracker = Mixpanel::Tracker.new(
|
|
140
|
+
'TEST TOKEN',
|
|
141
|
+
nil,
|
|
142
|
+
credentials: credentials,
|
|
143
|
+
local_flags_config: {},
|
|
144
|
+
remote_flags_config: {}
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
# Verify credentials were passed to providers by checking internal state
|
|
148
|
+
expect(tracker.local_flags.instance_variable_get(:@credentials)).to eq(credentials)
|
|
149
|
+
expect(tracker.remote_flags.instance_variable_get(:@credentials)).to eq(credentials)
|
|
150
|
+
end
|
|
151
|
+
end
|
|
134
152
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: mixpanel-ruby
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.
|
|
4
|
+
version: 3.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Mixpanel
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-07-10 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: mutex_m
|
|
@@ -170,9 +170,16 @@ executables: []
|
|
|
170
170
|
extensions: []
|
|
171
171
|
extra_rdoc_files: []
|
|
172
172
|
files:
|
|
173
|
+
- ".github/dependabot.yml"
|
|
174
|
+
- ".github/modules.json"
|
|
175
|
+
- ".github/scripts/generate-changelog.sh"
|
|
176
|
+
- ".github/workflows/pr-title-check.yml"
|
|
177
|
+
- ".github/workflows/prepare-release.yml"
|
|
178
|
+
- ".github/workflows/release-rubygems.yml"
|
|
173
179
|
- ".github/workflows/ruby.yml"
|
|
174
180
|
- ".gitignore"
|
|
175
181
|
- ".rspec"
|
|
182
|
+
- CHANGELOG.md
|
|
176
183
|
- Gemfile
|
|
177
184
|
- LICENSE
|
|
178
185
|
- Rakefile
|
|
@@ -184,6 +191,7 @@ files:
|
|
|
184
191
|
- demo/simple_messages.rb
|
|
185
192
|
- lib/mixpanel-ruby.rb
|
|
186
193
|
- lib/mixpanel-ruby/consumer.rb
|
|
194
|
+
- lib/mixpanel-ruby/credentials.rb
|
|
187
195
|
- lib/mixpanel-ruby/error.rb
|
|
188
196
|
- lib/mixpanel-ruby/events.rb
|
|
189
197
|
- lib/mixpanel-ruby/flags/flags_provider.rb
|
|
@@ -196,15 +204,19 @@ files:
|
|
|
196
204
|
- lib/mixpanel-ruby/tracker.rb
|
|
197
205
|
- lib/mixpanel-ruby/version.rb
|
|
198
206
|
- mixpanel-ruby.gemspec
|
|
207
|
+
- openfeature-provider/CHANGELOG.md
|
|
199
208
|
- openfeature-provider/Gemfile
|
|
200
209
|
- openfeature-provider/README.md
|
|
201
210
|
- openfeature-provider/RELEASE.md
|
|
202
211
|
- openfeature-provider/lib/mixpanel/openfeature.rb
|
|
203
212
|
- openfeature-provider/lib/mixpanel/openfeature/provider.rb
|
|
213
|
+
- openfeature-provider/lib/mixpanel/openfeature/version.rb
|
|
204
214
|
- openfeature-provider/mixpanel-ruby-openfeature.gemspec
|
|
205
215
|
- openfeature-provider/spec/mixpanel_openfeature_provider_spec.rb
|
|
206
216
|
- openfeature-provider/spec/spec_helper.rb
|
|
207
217
|
- spec/mixpanel-ruby/consumer_spec.rb
|
|
218
|
+
- spec/mixpanel-ruby/credentials_security_spec.rb
|
|
219
|
+
- spec/mixpanel-ruby/credentials_spec.rb
|
|
208
220
|
- spec/mixpanel-ruby/error_spec.rb
|
|
209
221
|
- spec/mixpanel-ruby/events_spec.rb
|
|
210
222
|
- spec/mixpanel-ruby/flags/local_flags_spec.rb
|