├── .github ├── CODEOWNERS ├── pull_request_template.md └── workflows │ ├── ci.yml │ └── update-license-year.yml ├── .gitignore ├── .rubocop.yml ├── .simplecov ├── CHANGES.txt ├── CONTRIBUTORS-GUIDE.md ├── Gemfile ├── LICENSE ├── README.md ├── Rakefile ├── ext └── murmurhash │ ├── 3_x64_128.c │ ├── 3_x86_32.c │ ├── MurmurHash3.java │ ├── extconf.rb │ ├── murmurhash.c │ └── murmurhash.h ├── gemfiles ├── faraday_after_0.13.gemfile └── faraday_before_0.13.gemfile ├── lib ├── murmurhash │ ├── base.rb │ ├── murmurhash.jar │ └── murmurhash_mri.rb ├── splitclient-rb.rb └── splitclient-rb │ ├── cache │ ├── adapters │ │ ├── cache_adapter.rb │ │ ├── memory_adapter.rb │ │ ├── memory_adapters │ │ │ ├── map_adapter.rb │ │ │ └── queue_adapter.rb │ │ └── redis_adapter.rb │ ├── fetchers │ │ ├── segment_fetcher.rb │ │ └── split_fetcher.rb │ ├── filter │ │ ├── bloom_filter.rb │ │ ├── filter_adapter.rb │ │ └── flag_set_filter.rb │ ├── hashers │ │ └── impression_hasher.rb │ ├── observers │ │ ├── impression_observer.rb │ │ └── noop_impression_observer.rb │ ├── repositories │ │ ├── events │ │ │ ├── memory_repository.rb │ │ │ └── redis_repository.rb │ │ ├── events_repository.rb │ │ ├── flag_sets │ │ │ ├── memory_repository.rb │ │ │ └── redis_repository.rb │ │ ├── impressions │ │ │ ├── memory_repository.rb │ │ │ └── redis_repository.rb │ │ ├── impressions_repository.rb │ │ ├── repository.rb │ │ ├── segments_repository.rb │ │ └── splits_repository.rb │ ├── routers │ │ └── impression_router.rb │ ├── senders │ │ ├── events_sender.rb │ │ ├── impressions_adapter │ │ │ ├── memory_sender.rb │ │ │ └── redis_sender.rb │ │ ├── impressions_count_sender.rb │ │ ├── impressions_formatter.rb │ │ ├── impressions_sender.rb │ │ ├── impressions_sender_adapter.rb │ │ └── localhost_repo_cleaner.rb │ └── stores │ │ ├── localhost_split_builder.rb │ │ ├── localhost_split_store.rb │ │ └── store_utils.rb │ ├── clients │ └── split_client.rb │ ├── constants.rb │ ├── engine │ ├── api │ │ ├── client.rb │ │ ├── events.rb │ │ ├── faraday_middleware │ │ │ └── gzip.rb │ │ ├── impressions.rb │ │ ├── segments.rb │ │ ├── splits.rb │ │ └── telemetry_api.rb │ ├── auth_api_client.rb │ ├── back_off.rb │ ├── common │ │ ├── impressions_counter.rb │ │ ├── impressions_manager.rb │ │ └── noop_impressions_counter.rb │ ├── evaluator │ │ └── splitter.rb │ ├── impressions │ │ ├── noop_unique_keys_tracker.rb │ │ └── unique_keys_tracker.rb │ ├── matchers │ │ ├── all_keys_matcher.rb │ │ ├── between_matcher.rb │ │ ├── between_semver_matcher.rb │ │ ├── combiners.rb │ │ ├── combining_matcher.rb │ │ ├── contains_all_matcher.rb │ │ ├── contains_any_matcher.rb │ │ ├── contains_matcher.rb │ │ ├── dependency_matcher.rb │ │ ├── ends_with_matcher.rb │ │ ├── equal_to_boolean_matcher.rb │ │ ├── equal_to_matcher.rb │ │ ├── equal_to_semver_matcher.rb │ │ ├── equal_to_set_matcher.rb │ │ ├── greater_than_or_equal_to_matcher.rb │ │ ├── greater_than_or_equal_to_semver_matcher.rb │ │ ├── in_list_semver_matcher.rb │ │ ├── less_than_or_equal_to_matcher.rb │ │ ├── less_than_or_equal_to_semver_matcher.rb │ │ ├── matcher.rb │ │ ├── matches_string_matcher.rb │ │ ├── negation_matcher.rb │ │ ├── part_of_set_matcher.rb │ │ ├── semver.rb │ │ ├── set_matcher.rb │ │ ├── starts_with_matcher.rb │ │ ├── user_defined_segment_matcher.rb │ │ └── whitelist_matcher.rb │ ├── metrics │ │ └── binary_search_latency_tracker.rb │ ├── models │ │ ├── label.rb │ │ ├── split.rb │ │ └── treatment.rb │ ├── parser │ │ ├── condition.rb │ │ ├── evaluator.rb │ │ └── partition.rb │ ├── push_manager.rb │ ├── status_manager.rb │ ├── sync_manager.rb │ └── synchronizer.rb │ ├── exceptions.rb │ ├── helpers │ ├── decryption_helper.rb │ ├── repository_helper.rb │ ├── thread_helper.rb │ └── util.rb │ ├── managers │ └── split_manager.rb │ ├── spec.rb │ ├── split_config.rb │ ├── split_factory.rb │ ├── split_factory_builder.rb │ ├── split_factory_registry.rb │ ├── split_logger.rb │ ├── sse │ ├── event_source │ │ ├── client.rb │ │ ├── event_parser.rb │ │ ├── event_types.rb │ │ └── stream_data.rb │ ├── notification_manager_keeper.rb │ ├── notification_processor.rb │ ├── sse_handler.rb │ └── workers │ │ ├── segments_worker.rb │ │ └── splits_worker.rb │ ├── telemetry │ ├── domain │ │ ├── constants.rb │ │ └── structs.rb │ ├── evaluation_consumer.rb │ ├── evaluation_producer.rb │ ├── init_consumer.rb │ ├── init_producer.rb │ ├── memory │ │ ├── memory_evaluation_consumer.rb │ │ ├── memory_evaluation_producer.rb │ │ ├── memory_init_consumer.rb │ │ ├── memory_init_producer.rb │ │ ├── memory_runtime_consumer.rb │ │ ├── memory_runtime_producer.rb │ │ └── memory_synchronizer.rb │ ├── redis │ │ ├── redis_evaluation_producer.rb │ │ ├── redis_init_producer.rb │ │ └── redis_synchronizer.rb │ ├── runtime_consumer.rb │ ├── runtime_producer.rb │ ├── storages │ │ └── memory.rb │ ├── sync_task.rb │ └── synchronizer.rb │ ├── utilitites.rb │ ├── validators.rb │ └── version.rb ├── sonar-project.properties ├── spec ├── allocations │ ├── cache │ │ ├── adapters │ │ │ └── memory │ │ │ │ └── map_adapter_spec.rb │ │ └── repositories │ │ │ └── impressions │ │ │ └── memory_repository_spec.rb │ └── splitclient-rb │ │ └── clients │ │ └── split_client_spec.rb ├── cache │ ├── adapters │ │ ├── cache_adapter_spec.rb │ │ ├── memory │ │ │ ├── map_adapter_spec.rb │ │ │ └── queue_adapter_spec.rb │ │ └── redis_adapter_spec.rb │ ├── fetchers │ │ ├── segment_fetch_spec.rb │ │ └── split_fetch_spec.rb │ ├── hashers │ │ └── impression_hasher_spec.rb │ ├── observers │ │ └── impression_observer_spec.rb │ ├── repositories │ │ ├── events_repository_spec.rb │ │ ├── flag_set_repository_spec.rb │ │ ├── impressions_repository_spec.rb │ │ ├── segments_repository_spec.rb │ │ └── splits_repository_spec.rb │ ├── routers │ │ └── impression_router_spec.rb │ ├── senders │ │ ├── events_sender_spec.rb │ │ ├── impressions_count_sender_spec.rb │ │ ├── impressions_formatter_spec.rb │ │ ├── impressions_sender_adapter_spec.rb │ │ ├── impressions_sender_spec.rb │ │ └── localhost_repo_cleaner_spec.rb │ └── stores │ │ └── localhost_split_store_spec.rb ├── engine │ ├── api │ │ ├── client_spec.rb │ │ ├── events_spec.rb │ │ ├── impressions_spec.rb │ │ ├── segments_spec.rb │ │ ├── splits_spec.rb │ │ └── telemetry_api_spec.rb │ ├── auth_api_client_spec.rb │ ├── common │ │ ├── impression_counter_spec.rb │ │ └── impression_manager_spec.rb │ ├── evaluator │ │ └── splitter_spec.rb │ ├── impressions │ │ ├── memory_unique_keys_tracker_spec.rb │ │ └── redis_unique_keys_tracker_spec.rb │ ├── matchers │ │ ├── all_keys_matcher_spec.rb │ │ ├── between_matcher_spec.rb │ │ ├── combining_matcher_spec.rb │ │ ├── contains_all_matcher_spec.rb │ │ ├── contains_any_matcher_spec.rb │ │ ├── contains_matcher_spec.rb │ │ ├── depencdency_matcher_spec.rb │ │ ├── ends_with_matcher_spec.rb │ │ ├── equal_to_boolean_matcher_spec.rb │ │ ├── equal_to_matcher_spec.rb │ │ ├── equal_to_set_matcher_spec.rb │ │ ├── greater_than_or_equal_to_matcher_spec.rb │ │ ├── less_than_or_equal_to_matcher_spec.rb │ │ ├── matcher_spec.rb │ │ ├── matches_between_semver_matcher_spec.rb │ │ ├── matches_equal_to_semver_matcher_spec.rb │ │ ├── matches_greater_than_or_equal_to_semver_matcher_spec.rb │ │ ├── matches_in_list_semver_matcher_spec.rb │ │ ├── matches_less_than_or_equal_to_semver_matcher_spec.rb │ │ ├── matches_string_matcher_spec.rb │ │ ├── negation_matcher_spec.rb │ │ ├── part_of_set_matcher_spec.rb │ │ ├── semver_matchers_integration_spec.rb │ │ ├── semver_spec.rb │ │ ├── starts_with_matcher_spec.rb │ │ ├── user_defined_segment_matcher_spec.rb │ │ └── whitelist_matcher_spec.rb │ ├── metrics │ │ └── binary_search_latency_tracker_spec.rb │ ├── parser │ │ └── evaluator_spec.rb │ ├── push_manager_spec.rb │ ├── status_manager_spec.rb │ ├── sync_manager_spec.rb │ └── synchronizer_spec.rb ├── engine_spec.rb ├── filter │ ├── bloom_filter_spec.rb │ ├── filter_adapter_spec.rb │ └── flag_set_filter_spec.rb ├── filter_imp_test.rb ├── http_server_mock.rb ├── integrations │ ├── dedupe_impression_spec.rb │ ├── in_memory_client_spec.rb │ ├── push_client_spec.rb │ └── redis_client_spec.rb ├── my_impression_listener.rb ├── redis_helper.rb ├── repository_helper.rb ├── spec_helper.rb ├── splitclient │ ├── engine_localhost_spec.rb │ ├── manager_localhost_spec.rb │ ├── split_client_spec.rb │ ├── split_config_spec.rb │ ├── split_factory_spec.rb │ ├── split_manager_spec.rb │ └── utilities_spec.rb ├── splitclient_rb_corner_cases_spec.rb ├── sse │ ├── event_source │ │ ├── back_off_spec.rb │ │ ├── client_spec.rb │ │ └── event_parser_spec.rb │ ├── notification_manager_keeper_spec.rb │ ├── sse_handler_spec.rb │ └── workers │ │ ├── segments_worker_spec.rb │ │ └── splits_worker_spec.rb ├── support │ └── matchers │ │ └── allocate_under_matcher.rb ├── telemetry │ ├── synchronizer_spec.rb │ ├── telemetry_evaluation_spec.rb │ ├── telemetry_init_spec.rb │ └── telemetry_runtime_spec.rb ├── test_data │ ├── hash │ │ ├── murmur3-64-128.csv │ │ ├── murmur3-sample-data-non-alpha-numeric-v2.csv │ │ ├── murmur3-sample-data-v2.csv │ │ └── sample-data.csv │ ├── integrations │ │ ├── auth_body_response.json │ │ ├── flag_sets.json │ │ ├── segment1.json │ │ ├── segment2.json │ │ ├── segment3.json │ │ ├── segment3_updated.json │ │ ├── splits.json │ │ ├── splits_push.json │ │ ├── splits_push2.json │ │ └── splits_push3.json │ ├── local_treatments │ │ ├── .split │ │ └── split.yaml │ ├── regexp │ │ └── data.txt │ ├── sample-data-non-alpha-numeric.csv │ ├── segments │ │ ├── combining_matcher_segments.json │ │ ├── engine_segments.json │ │ ├── engine_segments2.json │ │ ├── matchers_segments.json │ │ ├── segmentNoOneUses.json │ │ ├── segments.json │ │ └── segments2.json │ └── splits │ │ ├── between_matcher │ │ ├── datetime_matcher_splits.json │ │ ├── negate_number_matcher_splits.json │ │ ├── negative_number_matcher_splits.json │ │ └── number_matcher_splits.json │ │ ├── boolean_matcher │ │ └── splits.json │ │ ├── combining_matcher_splits.json │ │ ├── engine │ │ ├── all_keys_matcher.json │ │ ├── configurations.json │ │ ├── dependency_matcher.json │ │ ├── equal_to_set_matcher.json │ │ ├── flag_sets.json │ │ ├── impressions_test.json │ │ ├── killed.json │ │ ├── segment_deleted_matcher.json │ │ ├── segment_matcher.json │ │ ├── segment_matcher2.json │ │ └── whitelist_matcher.json │ │ ├── equal_to_matcher │ │ ├── date_splits.json │ │ ├── negative_splits.json │ │ ├── splits.json │ │ └── zero_splits.json │ │ ├── flag_sets.json │ │ ├── greater_than_or_equal_to_matcher │ │ ├── date_splits.json │ │ ├── negative_splits.json │ │ └── splits.json │ │ ├── imp-toggle.json │ │ ├── less_than_or_equal_to_matcher │ │ ├── date_splits.json │ │ ├── date_splits2.json │ │ ├── negative_splits.json │ │ └── splits.json │ │ ├── semver │ │ ├── between-semver.csv │ │ ├── equal-to-semver.csv │ │ ├── invalid-semantic-versions.csv │ │ └── valid-semantic-versions.csv │ │ ├── semver_matchers │ │ ├── semver_between.json │ │ ├── semver_equalto.json │ │ ├── semver_greater_or_equalto.json │ │ ├── semver_inlist.json │ │ └── semver_less_or_equalto.json │ │ ├── splits.json │ │ ├── splits2.json │ │ ├── splits3.json │ │ ├── splits4.json │ │ ├── splits_traffic_allocation.json │ │ ├── splits_traffic_allocation_one_percent.json │ │ └── whitelist_matcher_splits.json └── unique_keys_sender_adapter_test.rb ├── splitclient-rb.gemspec └── tasks ├── benchmark_get_treatment.rake └── irb.rake /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @splitio/sdk 2 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | # Ruby SDK 2 | 3 | ## What did you accomplish? 4 | 5 | 6 | ## How to test new changes? 7 | 8 | 9 | ## Extra Notes 10 | -------------------------------------------------------------------------------- /.github/workflows/update-license-year.yml: -------------------------------------------------------------------------------- 1 | name: Update License Year 2 | 3 | on: 4 | schedule: 5 | - cron: "0 3 1 1 *" # 03:00 AM on January 1 6 | 7 | permissions: 8 | contents: write 9 | pull-requests: write 10 | 11 | jobs: 12 | test: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v2 17 | with: 18 | fetch-depth: 0 19 | 20 | - name: Set Current year 21 | run: "echo CURRENT=$(date +%Y) >> $GITHUB_ENV" 22 | 23 | - name: Set Previous Year 24 | run: "echo PREVIOUS=$(($CURRENT-1)) >> $GITHUB_ENV" 25 | 26 | - name: Update LICENSE 27 | uses: jacobtomlinson/gha-find-replace@v2 28 | with: 29 | find: ${{ env.PREVIOUS }} 30 | replace: ${{ env.CURRENT }} 31 | include: "LICENSE" 32 | regex: false 33 | 34 | - name: Commit files 35 | run: | 36 | git config user.name 'github-actions[bot]' 37 | git config user.email 'github-actions[bot]@users.noreply.github.com' 38 | git commit -m "Updated License Year" -a 39 | 40 | - name: Create Pull Request 41 | uses: peter-evans/create-pull-request@v3 42 | with: 43 | token: ${{ secrets.GITHUB_TOKEN }} 44 | title: Update License Year 45 | branch: update-license 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | /.config 4 | /coverage/ 5 | /InstalledFiles 6 | /pkg/ 7 | /spec/reports/ 8 | /test/tmp/ 9 | /test/version_tmp/ 10 | /tmp/ 11 | 12 | ## Specific to RubyMotion: 13 | .dat* 14 | .repl_history 15 | build/ 16 | 17 | ## Documentation cache and generated files: 18 | /.yardoc/ 19 | /_yardoc/ 20 | /doc/ 21 | /rdoc/ 22 | 23 | ## Environment normalisation: 24 | /.bundle/ 25 | /vendor/bundle 26 | /lib/bundler/man/ 27 | 28 | # for a library or gem, you might want to ignore these files since the code is 29 | # intended to run in multiple environments; otherwise, check them in: 30 | Gemfile.lock 31 | .ruby-version 32 | .ruby-gemset 33 | 34 | # unless supporting rvm < 1.11.0 or doing something fancy, ignore this: 35 | .rvmrc 36 | 37 | # Ignore Byebug command history file. 38 | .byebug_history 39 | 40 | # Ignore built extensions 41 | lib/murmurhash/murmurhash.bundle 42 | lib/murmurhash/murmurhash.so 43 | 44 | ext/murmurhash/murmurhash.bundle 45 | ext/murmurhash/murmurhash.so 46 | 47 | # Ignore dump.rdb 48 | dump.rdb 49 | 50 | # Ignore Mac OS generated files 51 | .DS_Store 52 | 53 | # Ignore Appraisal gemfile.lock files 54 | /gemfiles/*.gemfile.lock 55 | 56 | release.sh 57 | 58 | .scannerwork 59 | -------------------------------------------------------------------------------- /.simplecov: -------------------------------------------------------------------------------- 1 | SimpleCov.start { add_filter %r{^/spec/} } 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | # Specify your gem's dependencies in splitclient-rb.gemspec 6 | gemspec 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright © 2025 Split Software, Inc. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'bundler/gem_tasks' 4 | require 'rspec/core/rake_task' 5 | require 'rubocop/rake_task' 6 | 7 | Dir['tasks/**/*.rake'].each { |rake| load rake } 8 | 9 | RSpec::Core::RakeTask.new(:spec) 10 | RuboCop::RakeTask.new(:rubocop) 11 | 12 | task spec: :compile 13 | case RUBY_PLATFORM 14 | when 'java' 15 | require 'rake/javaextensiontask' 16 | Rake::JavaExtensionTask.new 'murmurhash' do |ext| 17 | ext.lib_dir = 'lib/murmurhash' 18 | ext.target_version = '1.7' 19 | ext.source_version = '1.7' 20 | end 21 | else 22 | require 'rake/extensiontask' 23 | Rake::ExtensionTask.new 'murmurhash' do |ext| 24 | ext.lib_dir = 'lib/murmurhash' 25 | end 26 | end 27 | 28 | task default: %i[spec rubocop] 29 | -------------------------------------------------------------------------------- /ext/murmurhash/3_x86_32.c: -------------------------------------------------------------------------------- 1 | /* 2 | * MurmurHash3_x86_32 (C) Austin Appleby 3 | */ 4 | 5 | #include "murmurhash.h" 6 | 7 | uint32_t 8 | murmur_hash_process3_x86_32(const char * key, uint32_t len, uint32_t seed) 9 | { 10 | const uint8_t * data = (const uint8_t*)key; 11 | const int nblocks = len / 4; 12 | int i; 13 | 14 | uint32_t h1 = seed; 15 | 16 | const uint32_t c1 = 0xcc9e2d51; 17 | const uint32_t c2 = 0x1b873593; 18 | 19 | //---------- 20 | // body 21 | 22 | const uint32_t * blocks = (const uint32_t *)(data + nblocks*4); 23 | 24 | for(i = -nblocks; i; i++) 25 | { 26 | uint32_t k1 = getblock32(blocks,i); 27 | 28 | k1 *= c1; 29 | k1 = ROTL32(k1,15); 30 | k1 *= c2; 31 | 32 | h1 ^= k1; 33 | h1 = ROTL32(h1,13); 34 | h1 = h1*5+0xe6546b64; 35 | } 36 | 37 | //---------- 38 | // tail 39 | 40 | const uint8_t * tail = (const uint8_t*)(data + nblocks*4); 41 | 42 | uint32_t k1 = 0; 43 | 44 | switch(len & 3) 45 | { 46 | case 3: k1 ^= tail[2] << 16; 47 | case 2: k1 ^= tail[1] << 8; 48 | case 1: k1 ^= tail[0]; 49 | k1 *= c1; k1 = ROTL32(k1,15); k1 *= c2; h1 ^= k1; 50 | }; 51 | 52 | //---------- 53 | // finalization 54 | 55 | h1 ^= len; 56 | 57 | h1 = fmix32(h1); 58 | 59 | return h1; 60 | } 61 | 62 | VALUE 63 | murmur3_x86_32_finish(VALUE self) 64 | { 65 | uint8_t digest[4]; 66 | uint32_t h; 67 | 68 | h = _murmur_finish32(self, murmur_hash_process3_x86_32); 69 | assign_by_endian_32(digest, h); 70 | return rb_str_new((const char*) digest, 4); 71 | } 72 | 73 | VALUE 74 | murmur3_x86_32_s_digest(int argc, VALUE *argv, VALUE klass) 75 | { 76 | uint8_t digest[4]; 77 | uint32_t h; 78 | 79 | h = _murmur_s_digest32(argc, argv, klass, murmur_hash_process3_x86_32); 80 | assign_by_endian_32(digest, h); 81 | return rb_str_new((const char*) digest, 4); 82 | } 83 | 84 | VALUE 85 | murmur3_x86_32_s_rawdigest(int argc, VALUE *argv, VALUE klass) 86 | { 87 | return ULL2NUM(_murmur_s_digest32(argc, argv, klass, murmur_hash_process3_x86_32)); 88 | } -------------------------------------------------------------------------------- /ext/murmurhash/extconf.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'mkmf' 4 | 5 | create_makefile('murmurhash/murmurhash') 6 | -------------------------------------------------------------------------------- /gemfiles/faraday_after_0.13.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "faraday", "> 0.13" 6 | 7 | gemspec path: "../" 8 | -------------------------------------------------------------------------------- /gemfiles/faraday_before_0.13.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "faraday", ">= 0.9", "< 0.13" 6 | gem "net-http-persistent", "~> 3.0" 7 | 8 | gemspec path: "../" 9 | -------------------------------------------------------------------------------- /lib/murmurhash/base.rb: -------------------------------------------------------------------------------- 1 | module Digest 2 | ds = Struct.new(:digest_length, :seed_length) 3 | s1 = ds.new(4, 4) 4 | s2 = ds.new(8, 8) 5 | s3 = ds.new(16, 4) 6 | { 7 | '1' => s1, 8 | '2' => s1, 9 | '2A' => s1, 10 | '64A' => s2, 11 | '64B' => s2, 12 | 'Aligned2' => s1, 13 | 'Neutral2' => s1, 14 | '3_x86_32' => s1, 15 | '3_x86_128' => s3, 16 | '3_x64_128' => s3, 17 | }.each do |name, s| 18 | class_eval %Q{ 19 | class MurmurHashMRI#{name} < Digest::Class 20 | DEFAULT_SEED = "#{"\x00" * s.seed_length}".b 21 | 22 | def initialize 23 | @buffer = "" 24 | @seed = DEFAULT_SEED 25 | end 26 | 27 | def update(str) 28 | @buffer << str 29 | self 30 | end 31 | alias << update 32 | 33 | def reset 34 | @buffer.clear 35 | @seed = DEFAULT_SEED 36 | self 37 | end 38 | 39 | def seed 40 | @seed 41 | end 42 | 43 | def seed=(s) 44 | raise ArgumentError, "seed string should be #{s.seed_length} length" if #{s.seed_length} != s.length 45 | @seed = s 46 | end 47 | 48 | def digest_length 49 | #{s.digest_length} 50 | end 51 | 52 | def block_length 53 | 0 54 | end 55 | end 56 | } 57 | end 58 | end -------------------------------------------------------------------------------- /lib/murmurhash/murmurhash.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/splitio/ruby-client/f6360dfb5704a8ee6c1f70454f3fe1b5dd5185a6/lib/murmurhash/murmurhash.jar -------------------------------------------------------------------------------- /lib/murmurhash/murmurhash_mri.rb: -------------------------------------------------------------------------------- 1 | require 'digest' 2 | require 'murmurhash/base' 3 | require 'murmurhash/murmurhash' -------------------------------------------------------------------------------- /lib/splitclient-rb/cache/adapters/memory_adapter.rb: -------------------------------------------------------------------------------- 1 | require 'concurrent' 2 | 3 | module SplitIoClient 4 | module Cache 5 | module Adapters 6 | # Memory adapter can have different implementations, this class is used as a delegator to 7 | # this implementations 8 | class MemoryAdapter < SimpleDelegator 9 | end 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/splitclient-rb/cache/adapters/memory_adapters/queue_adapter.rb: -------------------------------------------------------------------------------- 1 | module SplitIoClient 2 | module Cache 3 | module Adapters 4 | module MemoryAdapters 5 | # Memory adapter implementation, which stores everything inside queue 6 | class QueueAdapter 7 | def initialize(max_size) 8 | @max_size = max_size 9 | @queue = Queue.new 10 | @current_size = Concurrent::AtomicFixnum.new(0) 11 | end 12 | 13 | def clear(_ = nil) 14 | @queue = Queue.new 15 | @current_size.value = 0 16 | end 17 | 18 | # Adds data to queue in non-blocking mode 19 | def add_to_queue(data) 20 | fail ThreadError if @current_size.value >= @max_size 21 | 22 | @queue.push(data) 23 | 24 | @current_size.increment 25 | end 26 | 27 | def clear 28 | get_batch(@current_size.value) 29 | end 30 | 31 | def get_batch(size) 32 | items = [] 33 | size.times do 34 | items << @queue.pop(true) 35 | @current_size.decrement 36 | end 37 | items 38 | rescue ThreadError 39 | items 40 | end 41 | 42 | def length 43 | @current_size.value 44 | end 45 | 46 | def empty? 47 | @queue.empty? 48 | end 49 | end 50 | end 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /lib/splitclient-rb/cache/filter/bloom_filter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'bitarray' 4 | 5 | module SplitIoClient 6 | module Cache 7 | module Filter 8 | class BloomFilter 9 | def initialize(capacity, false_positive_probability = 0.001) 10 | @capacity = capacity.round 11 | @m = best_m(capacity, false_positive_probability) 12 | reset_filter 13 | @k = best_k(capacity) 14 | end 15 | 16 | def add(string) 17 | return false if contains?(string) 18 | 19 | positions = hashes(string) 20 | positions.each { |position| @ba[position] = 1 } 21 | 22 | true 23 | end 24 | 25 | def contains?(string) 26 | !hashes(string).any? { |ea| @ba[ea] == 0 } 27 | end 28 | 29 | def clear 30 | @ba = nil 31 | reset_filter 32 | end 33 | 34 | private 35 | 36 | def reset_filter 37 | @ba = BitArray.new(@m.round) 38 | end 39 | 40 | # m is the required number of bits in the array 41 | def best_m(capacity, false_positive_probability) 42 | -(capacity * Math.log(false_positive_probability)) / (Math.log(2) ** 2) 43 | end 44 | 45 | # k is the number of hash functions that minimizes the probability of false positives 46 | def best_k(capacity) 47 | (Math.log(2) * (@ba.size / capacity)).round 48 | end 49 | 50 | def hashes(data) 51 | m = @ba.size 52 | h = Digest::MD5.hexdigest(data.to_s).to_i(16) 53 | x = h % m 54 | h /= m 55 | y = h % m 56 | h /= m 57 | z = h % m 58 | [x] + 1.upto(@k - 1).collect do |i| 59 | x = (x + y) % m 60 | y = (y + z) % m 61 | x 62 | end 63 | end 64 | end 65 | end 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /lib/splitclient-rb/cache/filter/filter_adapter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SplitIoClient 4 | module Cache 5 | module Filter 6 | class FilterAdapter 7 | def initialize(config, filter) 8 | @config = config 9 | @filter = filter 10 | end 11 | 12 | def add(feature_name, key) 13 | @filter.add("#{feature_name}#{key}") 14 | rescue StandardError => e 15 | @config.log_found_exception(__method__.to_s, e) 16 | end 17 | 18 | def contains?(feature_name, key) 19 | @filter.contains?("#{feature_name}#{key}") 20 | rescue StandardError => e 21 | @config.log_found_exception(__method__.to_s, e) 22 | end 23 | 24 | def clear 25 | @filter.clear 26 | rescue StandardError => e 27 | @config.log_found_exception(__method__.to_s, e) 28 | end 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/splitclient-rb/cache/filter/flag_set_filter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'set' 4 | 5 | module SplitIoClient 6 | module Cache 7 | module Filter 8 | class FlagSetsFilter 9 | def initialize(flag_sets = []) 10 | @flag_sets = Set.new(flag_sets) 11 | @should_filter = @flag_sets.any? 12 | end 13 | 14 | def should_filter? 15 | @should_filter 16 | end 17 | 18 | def flag_set_exist?(flag_set) 19 | return true unless @should_filter 20 | 21 | if not flag_set.is_a?(String) or flag_set.empty? 22 | return false 23 | end 24 | 25 | @flag_sets.intersection([flag_set]).any? 26 | end 27 | 28 | def intersect?(flag_sets) 29 | return true unless @should_filter 30 | 31 | if not flag_sets.is_a?(Array) or flag_sets.empty? 32 | return false 33 | end 34 | 35 | @flag_sets.intersection(Set.new(flag_sets)).any? 36 | end 37 | end 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/splitclient-rb/cache/hashers/impression_hasher.rb: -------------------------------------------------------------------------------- 1 | module SplitIoClient 2 | module Hashers 3 | class ImpressionHasher 4 | def initialize 5 | @murmur_hash_128_64 = case RUBY_PLATFORM 6 | when 'java' 7 | Proc.new { |key, seed| Java::MurmurHash3.hash128x64(key, seed) } 8 | else 9 | Proc.new { |key, seed| Digest::MurmurHashMRI3_x64_128.rawdigest(key, [seed].pack('L')) } 10 | end 11 | end 12 | 13 | def process(impression) 14 | impression_data = "#{unknown_if_null(impression[:k])}" 15 | impression_data << ":#{unknown_if_null(impression[:f])}" 16 | impression_data << ":#{unknown_if_null(impression[:t])}" 17 | impression_data << ":#{unknown_if_null(impression[:r])}" 18 | impression_data << ":#{zero_if_null(impression[:c])}" 19 | 20 | @murmur_hash_128_64.call(impression_data, 0)[0]; 21 | end 22 | 23 | private 24 | 25 | def unknown_if_null(value) 26 | value == nil ? "UNKNOWN" : value 27 | end 28 | 29 | def zero_if_null(value) 30 | value == nil ? 0 : value 31 | end 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/splitclient-rb/cache/observers/impression_observer.rb: -------------------------------------------------------------------------------- 1 | module SplitIoClient 2 | module Observers 3 | class ImpressionObserver 4 | LAST_SEEN_CACHE_SIZE = 500000 5 | 6 | def initialize 7 | @cache = LruRedux::TTL::ThreadSafeCache.new(LAST_SEEN_CACHE_SIZE) 8 | @impression_hasher = Hashers::ImpressionHasher.new 9 | end 10 | 11 | def test_and_set(impression) 12 | return if impression.nil? 13 | 14 | hash = @impression_hasher.process(impression) 15 | previous = @cache[hash] 16 | @cache[hash] = impression[:m] 17 | 18 | previous.nil? ? nil : [previous, impression[:m]].min 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/splitclient-rb/cache/observers/noop_impression_observer.rb: -------------------------------------------------------------------------------- 1 | module SplitIoClient 2 | module Observers 3 | class NoopImpressionObserver 4 | def test_and_set(impression) 5 | # no-op 6 | end 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/splitclient-rb/cache/repositories/events/memory_repository.rb: -------------------------------------------------------------------------------- 1 | module SplitIoClient 2 | module Cache 3 | module Repositories 4 | module Events 5 | class MemoryRepository < EventsRepository 6 | EVENTS_MAX_SIZE_BYTES = 5242880 7 | 8 | def initialize(config, telemetry_runtime_producer) 9 | @config = config 10 | @adapter = @config.events_adapter 11 | @size = 0 12 | @telemetry_runtime_producer = telemetry_runtime_producer 13 | end 14 | 15 | def add(key, traffic_type, event_type, time, value, properties, event_size) 16 | @adapter.add_to_queue(m: metadata, e: event(key, traffic_type, event_type, time, value, properties)) 17 | @size += event_size 18 | 19 | post_events if @size >= EVENTS_MAX_SIZE_BYTES || @adapter.length == @config.events_queue_size 20 | 21 | @telemetry_runtime_producer.record_events_stats(Telemetry::Domain::Constants::EVENTS_QUEUED, 1) 22 | rescue StandardError => e 23 | @config.log_found_exception(__method__.to_s, e) 24 | @telemetry_runtime_producer.record_events_stats(Telemetry::Domain::Constants::EVENTS_DROPPED, 1) 25 | end 26 | 27 | def clear 28 | @size = 0 29 | @adapter.clear 30 | end 31 | 32 | def empty? 33 | @adapter.empty? 34 | end 35 | 36 | def batch 37 | return [] if @config.events_queue_size.zero? 38 | 39 | @adapter.get_batch(@config.events_queue_size) 40 | end 41 | end 42 | end 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/splitclient-rb/cache/repositories/events/redis_repository.rb: -------------------------------------------------------------------------------- 1 | module SplitIoClient 2 | module Cache 3 | module Repositories 4 | module Events 5 | class RedisRepository < EventsRepository 6 | 7 | def initialize(config) 8 | @config = config 9 | @adapter = @config.events_adapter 10 | end 11 | 12 | def add(key, traffic_type, event_type, time, value, properties, size) 13 | @adapter.add_to_queue( 14 | namespace_key('.events'), 15 | { m: metadata, e: event(key, traffic_type, event_type, time, value, properties) }.to_json 16 | ) 17 | end 18 | 19 | def clear 20 | @adapter.get_from_queue(namespace_key('.events'), 0).map do |e| 21 | JSON.parse(e, symbolize_names: true) 22 | end 23 | rescue StandardError => e 24 | @config.logger.error("Exception while clearing events cache: #{e}") 25 | [] 26 | end 27 | 28 | def batch 29 | clear() 30 | end 31 | end 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/splitclient-rb/cache/repositories/events_repository.rb: -------------------------------------------------------------------------------- 1 | module SplitIoClient 2 | module Cache 3 | module Repositories 4 | # Repository which forwards events interface to the selected adapter 5 | class EventsRepository < Repository 6 | extend Forwardable 7 | def_delegators :@repository, :add, :clear, :batch 8 | 9 | def initialize(config, api_key, telemetry_runtime_producer) 10 | super(config) 11 | @repository = case @config.events_adapter.class.to_s 12 | when 'SplitIoClient::Cache::Adapters::MemoryAdapter' 13 | Repositories::Events::MemoryRepository.new(@config, telemetry_runtime_producer) 14 | when 'SplitIoClient::Cache::Adapters::RedisAdapter' 15 | Repositories::Events::RedisRepository.new(@config) 16 | end 17 | 18 | @api_key = api_key 19 | @telemetry_runtime_producer = telemetry_runtime_producer 20 | end 21 | 22 | def post_events 23 | events_api.post(self.clear) 24 | rescue StandardError => e 25 | @config.log_found_exception(__method__.to_s, e) 26 | end 27 | 28 | def empty? 29 | @repository.empty? 30 | end 31 | 32 | protected 33 | 34 | def metadata 35 | { 36 | s: "#{@config.language}-#{@config.version}", 37 | i: @config.machine_ip, 38 | n: @config.machine_name 39 | } 40 | end 41 | 42 | def event(key, traffic_type, event_type, time, value, properties) 43 | { 44 | key: key, 45 | trafficTypeName: traffic_type, 46 | eventTypeId: event_type, 47 | value: value, 48 | timestamp: time, 49 | properties: properties 50 | }.reject { |_, v| v.nil? } 51 | end 52 | 53 | private 54 | 55 | def events_api 56 | @events_api ||= SplitIoClient::Api::Events.new(@api_key, @config, @telemetry_runtime_producer) 57 | end 58 | end 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /lib/splitclient-rb/cache/repositories/flag_sets/memory_repository.rb: -------------------------------------------------------------------------------- 1 | require 'concurrent' 2 | 3 | module SplitIoClient 4 | module Cache 5 | module Repositories 6 | class MemoryFlagSetsRepository 7 | def initialize(flag_sets = []) 8 | @sets_feature_flag_map = {} 9 | flag_sets.each{ |flag_set| @sets_feature_flag_map[flag_set] = Set[] } 10 | end 11 | 12 | def flag_set_exist?(flag_set) 13 | @sets_feature_flag_map.key?(flag_set) 14 | end 15 | 16 | def get_flag_sets(flag_sets) 17 | to_return = Array.new 18 | flag_sets.each { |flag_set| to_return.concat(@sets_feature_flag_map[flag_set].to_a)} 19 | to_return.uniq 20 | end 21 | 22 | def add_flag_set(flag_set) 23 | @sets_feature_flag_map[flag_set] = Set[] if !flag_set_exist?(flag_set) 24 | end 25 | 26 | def remove_flag_set(flag_set) 27 | @sets_feature_flag_map.delete(flag_set) if flag_set_exist?(flag_set) 28 | end 29 | 30 | def add_feature_flag_to_flag_set(flag_set, feature_flag) 31 | @sets_feature_flag_map[flag_set].add(feature_flag) if flag_set_exist?(flag_set) 32 | end 33 | 34 | def remove_feature_flag_from_flag_set(flag_set, feature_flag) 35 | @sets_feature_flag_map[flag_set].delete(feature_flag) if flag_set_exist?(flag_set) 36 | end 37 | end 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/splitclient-rb/cache/repositories/flag_sets/redis_repository.rb: -------------------------------------------------------------------------------- 1 | require 'concurrent' 2 | 3 | module SplitIoClient 4 | module Cache 5 | module Repositories 6 | class RedisFlagSetsRepository < Repository 7 | 8 | def initialize(config) 9 | super(config) 10 | @adapter = SplitIoClient::Cache::Adapters::RedisAdapter.new(@config.redis_url) 11 | end 12 | 13 | def flag_set_exist?(flag_set) 14 | @adapter.exists?(namespace_key(".flagSet.#{flag_set}")) 15 | end 16 | 17 | def get_flag_sets(flag_sets) 18 | result = @adapter.redis.pipelined do |pipeline| 19 | flag_sets.each do |flag_set| 20 | pipeline.smembers(namespace_key(".flagSet.#{flag_set}")) 21 | end 22 | end 23 | to_return = Array.new 24 | result.each do |flag_set| 25 | flag_set.each { |feature_flag_name| to_return.push(feature_flag_name.to_s)} 26 | end 27 | to_return.uniq 28 | end 29 | 30 | def add_flag_set(flag_set) 31 | # not implemented 32 | end 33 | 34 | def remove_flag_set(flag_set) 35 | # not implemented 36 | end 37 | 38 | def add_feature_flag_to_flag_set(flag_set, feature_flag) 39 | # not implemented 40 | end 41 | 42 | def remove_feature_flag_from_flag_set(flag_set, feature_flag) 43 | # not implemented 44 | end 45 | 46 | end 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/splitclient-rb/cache/repositories/impressions/memory_repository.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SplitIoClient 4 | module Cache 5 | module Repositories 6 | module Impressions 7 | class MemoryRepository < ImpressionsRepository 8 | def initialize(config) 9 | @config = config 10 | @adapter = @config.impressions_adapter 11 | end 12 | 13 | def add_bulk(impressions) 14 | return 0 if impressions.nil? 15 | 16 | count = 0 17 | impressions.each do |impression| 18 | @adapter.add_to_queue(impression) 19 | count += 1 20 | end 21 | 22 | 0 23 | rescue ThreadError # queue is full 24 | if random_sampler.rand(1..1000) <= 2 # log only 0.2 % of the time 25 | @config.logger.warn("Dropping impressions. Current size is \ 26 | #{@config.impressions_queue_size}. " \ 27 | 'Consider increasing impressions_queue_size') 28 | end 29 | 30 | impressions.length - count 31 | end 32 | 33 | def batch 34 | return [] if @config.impressions_bulk_size.zero? 35 | 36 | @adapter.get_batch(@config.impressions_bulk_size) 37 | end 38 | 39 | def clear 40 | @adapter.clear 41 | end 42 | 43 | def empty? 44 | @adapter.empty? 45 | end 46 | 47 | private 48 | 49 | def random_sampler 50 | @random_sampler ||= Random.new 51 | end 52 | end 53 | end 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/splitclient-rb/cache/repositories/impressions/redis_repository.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SplitIoClient 4 | module Cache 5 | module Repositories 6 | module Impressions 7 | class RedisRepository < ImpressionsRepository 8 | EXPIRE_SECONDS = 3600 9 | 10 | def initialize(config) 11 | @config = config 12 | @adapter = @config.impressions_adapter 13 | end 14 | 15 | def add_bulk(impressions) 16 | impressions_json = impressions.map do |impression| 17 | impression.to_json 18 | end 19 | 20 | impressions_list_size = @adapter.add_to_queue(key, impressions_json) 21 | 22 | # Synchronizer might not be running 23 | @adapter.expire(key, EXPIRE_SECONDS) if impressions_json.size == impressions_list_size 24 | 0 25 | rescue StandardError => e 26 | @config.logger.error("Exception while add_bulk: #{e}") 27 | 0 28 | end 29 | 30 | def get_impressions(number_of_impressions = 0) 31 | @adapter.get_from_queue(key, number_of_impressions).map do |e| 32 | impression = JSON.parse(e, symbolize_names: true) 33 | impression[:i][:f] = impression[:i][:f].to_sym 34 | impression 35 | end 36 | rescue StandardError => e 37 | @config.logger.error("Exception while clearing impressions cache: #{e}") 38 | [] 39 | end 40 | 41 | def batch 42 | get_impressions(@config.impressions_bulk_size) 43 | end 44 | 45 | def clear 46 | get_impressions 47 | end 48 | 49 | def key 50 | @key ||= namespace_key('.impressions') 51 | end 52 | end 53 | end 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/splitclient-rb/cache/repositories/impressions_repository.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SplitIoClient 4 | module Cache 5 | module Repositories 6 | # Repository which forwards impressions interface to the selected adapter 7 | class ImpressionsRepository < Repository 8 | extend Forwardable 9 | def_delegators :@repository, :add_bulk, :batch, :clear, :empty? 10 | 11 | def initialize(config) 12 | super(config) 13 | @repository = case @config.impressions_adapter.class.to_s 14 | when 'SplitIoClient::Cache::Adapters::MemoryAdapter' 15 | Repositories::Impressions::MemoryRepository.new(@config) 16 | when 'SplitIoClient::Cache::Adapters::RedisAdapter' 17 | Repositories::Impressions::RedisRepository.new(@config) 18 | end 19 | end 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/splitclient-rb/cache/repositories/repository.rb: -------------------------------------------------------------------------------- 1 | module SplitIoClient 2 | module Cache 3 | class Repository 4 | 5 | def initialize(config) 6 | @config = config 7 | end 8 | 9 | def set_string(key, str) 10 | @adapter.set_string(namespace_key(key), str) 11 | end 12 | 13 | def string(key) 14 | @adapter.string(namespace_key(key)) 15 | end 16 | 17 | protected 18 | 19 | def namespace_key(key = '') 20 | "#{@config.redis_namespace}#{key}" 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/splitclient-rb/cache/routers/impression_router.rb: -------------------------------------------------------------------------------- 1 | module SplitIoClient 2 | class ImpressionRouter 3 | attr_reader :router_thread 4 | 5 | def initialize(config) 6 | @config = config 7 | @listener = @config.impression_listener 8 | 9 | return unless @listener 10 | 11 | @queue = Queue.new 12 | router_thread 13 | 14 | if defined?(PhusionPassenger) 15 | PhusionPassenger.on_event(:starting_worker_process) do |forked| 16 | router_thread if forked 17 | end 18 | end 19 | end 20 | 21 | def add_bulk(impressions) 22 | impressions.each do |impression| 23 | enqueue(impression) 24 | end 25 | end 26 | 27 | private 28 | 29 | def enqueue(impression) 30 | imp = { 31 | split_name: impression[:i][:f], 32 | matching_key: impression[:i][:k], 33 | bucketing_key: impression[:i][:b], 34 | time: impression[:i][:m], 35 | treatment: { 36 | label: impression[:i][:r], 37 | treatment: impression[:i][:t], 38 | change_number: impression[:i][:c] 39 | }, 40 | previous_time: impression[:i][:pt], 41 | attributes: impression[:attributes] 42 | } 43 | @queue.push(imp) if @listener 44 | rescue StandardError => e 45 | @config.log_found_exception(__method__.to_s, e) 46 | end 47 | 48 | def router_thread 49 | @config.threads[:impression_router] = Thread.new do 50 | loop do 51 | begin 52 | @listener.log(@queue.pop) 53 | rescue StandardError => e 54 | @config.log_found_exception(__method__.to_s, e) 55 | end 56 | end 57 | end 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /lib/splitclient-rb/cache/senders/events_sender.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SplitIoClient 4 | module Cache 5 | module Senders 6 | class EventsSender 7 | def initialize(events_repository, config) 8 | @events_repository = events_repository 9 | @config = config 10 | end 11 | 12 | def call 13 | events_thread 14 | end 15 | 16 | private 17 | 18 | def events_thread 19 | @config.threads[:events_sender] = Thread.new do 20 | begin 21 | @config.logger.info('Starting events service') 22 | 23 | loop do 24 | post_events 25 | 26 | sleep(SplitIoClient::Utilities.randomize_interval(@config.events_push_rate)) 27 | end 28 | rescue SplitIoClient::SDKShutdownException 29 | post_events 30 | 31 | @config.logger.info('Posting events due to shutdown') 32 | end 33 | end 34 | end 35 | 36 | def post_events 37 | @events_repository.post_events 38 | end 39 | end 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/splitclient-rb/cache/senders/impressions_count_sender.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SplitIoClient 4 | module Cache 5 | module Senders 6 | class ImpressionsCountSender 7 | def initialize(config, impression_counter, impressions_sender_adapter) 8 | @config = config 9 | @impression_counter = impression_counter 10 | @impressions_sender_adapter = impressions_sender_adapter 11 | end 12 | 13 | def call 14 | impressions_count_thread 15 | end 16 | 17 | private 18 | 19 | def impressions_count_thread 20 | @config.threads[:impressions_count_sender] = Thread.new do 21 | begin 22 | @config.logger.info('Starting impressions count service') 23 | loop do 24 | sleep(@config.counter_refresh_rate) 25 | post_impressions_count 26 | end 27 | rescue SplitIoClient::SDKShutdownException 28 | post_impressions_count 29 | 30 | @config.logger.info('Posting impressions count due to shutdown') 31 | end 32 | end 33 | end 34 | 35 | def post_impressions_count 36 | @impressions_sender_adapter.record_impressions_count(@impression_counter.pop_all) 37 | rescue StandardError => e 38 | @config.log_found_exception(__method__.to_s, e) 39 | end 40 | end 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/splitclient-rb/cache/senders/impressions_sender.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SplitIoClient 4 | module Cache 5 | module Senders 6 | class ImpressionsSender 7 | def initialize(impressions_repository, config, impressions_api) 8 | @impressions_repository = impressions_repository 9 | @config = config 10 | @impressions_api = impressions_api 11 | end 12 | 13 | def call 14 | impressions_thread 15 | end 16 | 17 | private 18 | 19 | def impressions_thread 20 | @config.threads[:impressions_sender] = Thread.new do 21 | begin 22 | @config.logger.info('Starting impressions service') 23 | 24 | loop do 25 | post_impressions(false) 26 | 27 | sleep(SplitIoClient::Utilities.randomize_interval(@config.impressions_refresh_rate)) 28 | end 29 | rescue SplitIoClient::SDKShutdownException 30 | post_impressions 31 | 32 | @config.logger.info('Posting impressions due to shutdown') 33 | end 34 | end 35 | end 36 | 37 | def post_impressions(fetch_all_impressions = true) 38 | formatted_impressions = ImpressionsFormatter.new(@impressions_repository) 39 | .call(fetch_all_impressions) 40 | 41 | impressions_api.post(formatted_impressions) 42 | rescue StandardError => e 43 | @config.log_found_exception(__method__.to_s, e) 44 | end 45 | 46 | def impressions_api 47 | @impressions_api 48 | end 49 | end 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/splitclient-rb/cache/senders/impressions_sender_adapter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SplitIoClient 4 | module Cache 5 | module Senders 6 | class ImpressionsSenderAdapter 7 | extend Forwardable 8 | def_delegators :@sender, :record_uniques_key, :record_impressions_count 9 | 10 | def initialize(config, telemetry_api, impressions_api) 11 | @sender = case config.telemetry_adapter.class.to_s 12 | when 'SplitIoClient::Cache::Adapters::RedisAdapter' 13 | Cache::Senders::RedisImpressionsSender.new(config) 14 | else 15 | Cache::Senders::MemoryImpressionsSender.new(config, telemetry_api, impressions_api) 16 | end 17 | end 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/splitclient-rb/cache/senders/localhost_repo_cleaner.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SplitIoClient 4 | module Cache 5 | module Senders 6 | class LocalhostRepoCleaner 7 | def initialize(impressions_repository, events_repository, config) 8 | @impressions_repository = impressions_repository 9 | @events_repository = events_repository 10 | @config = config 11 | end 12 | 13 | def call 14 | if ENV['SPLITCLIENT_ENV'] == 'test' 15 | clear_repositories 16 | else 17 | cleaner_thread 18 | 19 | if defined?(PhusionPassenger) 20 | PhusionPassenger.on_event(:starting_worker_process) do |forked| 21 | cleaner_thread if forked 22 | end 23 | end 24 | end 25 | end 26 | 27 | private 28 | 29 | def cleaner_thread 30 | @config.threads[:repo_cleaner] = Thread.new do 31 | @config.logger.info('Starting repositories cleanup service') 32 | loop do 33 | clear_repositories 34 | 35 | sleep(SplitIoClient::Utilities.randomize_interval(@config.features_refresh_rate)) 36 | end 37 | end 38 | end 39 | 40 | def clear_repositories 41 | @impressions_repository.clear 42 | @events_repository.clear 43 | end 44 | end 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/splitclient-rb/cache/stores/store_utils.rb: -------------------------------------------------------------------------------- 1 | module SplitIoClient 2 | module Cache 3 | module Stores 4 | class StoreUtils 5 | def self.random_interval(interval) 6 | random_factor = Random.new.rand(50..100) / 100.0 7 | 8 | interval * random_factor 9 | end 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/splitclient-rb/constants.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class SplitIoClient::Constants 4 | EXPIRATION_RATE = 600 5 | CONTROL_PRI = 'control_pri' 6 | CONTROL_SEC = 'control_sec' 7 | OCCUPANCY_CHANNEL_PREFIX = '[?occupancy=metrics.publishers]' 8 | FETCH_BACK_OFF_BASE_RETRIES = 1 9 | PUSH_CONNECTED = 'PUSH_CONNECTED' 10 | PUSH_RETRYABLE_ERROR = 'PUSH_RETRYABLE_ERROR' 11 | PUSH_NONRETRYABLE_ERROR = 'PUSH_NONRETRYABLE_ERROR' 12 | PUSH_SUBSYSTEM_DOWN = 'PUSH_SUBSYSTEM_DOWN' 13 | PUSH_SUBSYSTEM_READY = 'PUSH_SUBSYSTEM_READY' 14 | PUSH_SUBSYSTEM_OFF = 'PUSH_SUBSYSTEM_OFF' 15 | PUSH_FORCED_STOP = 'PUSH_FORCED_STOP' 16 | end 17 | -------------------------------------------------------------------------------- /lib/splitclient-rb/engine/api/faraday_middleware/gzip.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'faraday' 4 | 5 | module SplitIoClient 6 | module FaradayMiddleware 7 | class Gzip < Faraday::Middleware 8 | ACCEPT_ENCODING = 'Accept-Encoding' 9 | CONTENT_ENCODING = 'Content-Encoding' 10 | CONTENT_LENGTH = 'Content-Length' 11 | SUPPORTED_ENCODINGS = 'gzip,deflate' 12 | RUBY_ENCODING = '1.9'.respond_to?(:force_encoding) 13 | 14 | def call(env) 15 | env[:request_headers][ACCEPT_ENCODING] ||= SUPPORTED_ENCODINGS 16 | @app.call(env).on_complete do |response_env| 17 | case response_env[:response_headers][CONTENT_ENCODING] 18 | when 'gzip' 19 | reset_body(response_env, &method(:uncompress_gzip)) 20 | when 'deflate' 21 | reset_body(response_env, &method(:inflate)) 22 | end 23 | end 24 | end 25 | 26 | def reset_body(env) 27 | env[:body] = yield(env[:body]) 28 | env[:response_headers].delete(CONTENT_ENCODING) 29 | env[:response_headers][CONTENT_LENGTH] = env[:body].length 30 | end 31 | 32 | def uncompress_gzip(body) 33 | io = StringIO.new(body) 34 | gzip_reader = if RUBY_ENCODING 35 | Zlib::GzipReader.new(io, encoding: 'ASCII-8BIT') 36 | else 37 | Zlib::GzipReader.new(io) 38 | end 39 | gzip_reader.read 40 | end 41 | 42 | def inflate(body) 43 | # Inflate as a DEFLATE (RFC 1950+RFC 1951) stream 44 | Zlib::Inflate.inflate(body) 45 | rescue Zlib::DataError 46 | # Fall back to inflating as a "raw" deflate stream which 47 | # Microsoft servers return 48 | inflate = Zlib::Inflate.new(-Zlib::MAX_WBITS) 49 | begin 50 | inflate.inflate(body) 51 | ensure 52 | inflate.close 53 | end 54 | end 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /lib/splitclient-rb/engine/api/telemetry_api.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SplitIoClient 4 | module Api 5 | class TelemetryApi < Client 6 | def initialize(config, api_key, telemetry_runtime_producer) 7 | super(config) 8 | @api_key = api_key 9 | @telemetry_runtime_producer = telemetry_runtime_producer 10 | end 11 | 12 | def record_init(config_init) 13 | post_telemetry("#{@config.telemetry_service_url}/metrics/config", config_init, 'init') 14 | end 15 | 16 | def record_stats(stats) 17 | post_telemetry("#{@config.telemetry_service_url}/metrics/usage", stats, 'stats') 18 | end 19 | 20 | def record_unique_keys(uniques) 21 | return if uniques[:keys].empty? 22 | 23 | post_telemetry("#{@config.telemetry_service_url}/keys/ss", uniques, 'unique_keys') 24 | rescue StandardError => e 25 | @config.log_found_exception(__method__.to_s, e) 26 | end 27 | 28 | private 29 | 30 | def post_telemetry(url, obj, method) 31 | start = Time.now 32 | response = post_api(url, @api_key, obj) 33 | 34 | if response.success? 35 | @config.split_logger.log_if_debug("Telemetry post succeeded: record #{method}.") 36 | 37 | bucket = BinarySearchLatencyTracker.get_bucket((Time.now - start) * 1000.0) 38 | @telemetry_runtime_producer.record_sync_latency(Telemetry::Domain::Constants::TELEMETRY_SYNC, bucket) 39 | @telemetry_runtime_producer.record_successful_sync(Telemetry::Domain::Constants::TELEMETRY_SYNC, (Time.now.to_f * 1000.0).to_i) 40 | else 41 | @telemetry_runtime_producer.record_sync_error(Telemetry::Domain::Constants::TELEMETRY_SYNC, response.status) 42 | @config.logger.error("Unexpected status code while posting telemetry #{method}: #{response.status}.") 43 | end 44 | end 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/splitclient-rb/engine/back_off.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: false 2 | 3 | module SplitIoClient 4 | module Engine 5 | BACKOFF_MAX_ALLOWED = 1800 6 | class BackOff 7 | def initialize(back_off_base, attempt = 0, max_allowed = BACKOFF_MAX_ALLOWED) 8 | @attempt = attempt 9 | @back_off_base = back_off_base 10 | @max_allowed = max_allowed 11 | end 12 | 13 | def interval 14 | interval = 0 15 | interval = (@back_off_base * (2**@attempt)) if @attempt.positive? 16 | @attempt += 1 17 | 18 | interval >= @max_allowed ? @max_allowed : interval 19 | end 20 | 21 | def reset 22 | @attempt = 0 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/splitclient-rb/engine/common/impressions_counter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'concurrent' 4 | 5 | module SplitIoClient 6 | module Engine 7 | module Common 8 | TIME_INTERVAL_MS = 3600 * 1000 9 | 10 | class ImpressionCounter 11 | DEFAULT_AMOUNT = 1 12 | 13 | def initialize 14 | @cache = Concurrent::Hash.new 15 | end 16 | 17 | def inc(split_name, time_frame) 18 | key = make_key(split_name, time_frame) 19 | 20 | current_amount = @cache[key] 21 | @cache[key] = current_amount.nil? ? DEFAULT_AMOUNT : (current_amount + DEFAULT_AMOUNT) 22 | end 23 | 24 | def pop_all 25 | to_return = Concurrent::Hash.new 26 | 27 | @cache.each do |key, value| 28 | to_return[key] = value 29 | end 30 | @cache.clear 31 | 32 | to_return 33 | end 34 | 35 | def make_key(split_name, time_frame) 36 | "#{split_name}::#{ImpressionCounter.truncate_time_frame(time_frame)}" 37 | end 38 | 39 | def self.truncate_time_frame(timestamp_ms) 40 | timestamp_ms - (timestamp_ms % TIME_INTERVAL_MS) 41 | end 42 | end 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/splitclient-rb/engine/common/noop_impressions_counter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'concurrent' 4 | 5 | module SplitIoClient 6 | module Engine 7 | module Common 8 | class NoopImpressionCounter 9 | def inc(split_name, time_frame) 10 | # no-op 11 | end 12 | 13 | def pop_all 14 | # no-op 15 | end 16 | 17 | def make_key(split_name, time_frame) 18 | # no-op 19 | end 20 | 21 | def self.truncate_time_frame(timestamp_ms) 22 | # no-op 23 | end 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/splitclient-rb/engine/impressions/noop_unique_keys_tracker.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SplitIoClient 4 | module Engine 5 | module Impressions 6 | class NoopUniqueKeysTracker 7 | def call 8 | # no-op 9 | end 10 | 11 | def track(feature_name, key) 12 | # no-op 13 | end 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/splitclient-rb/engine/matchers/all_keys_matcher.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SplitIoClient 4 | # 5 | # class to implement the all keys matcher 6 | # 7 | class AllKeysMatcher < Matcher 8 | MATCHER_TYPE = 'ALL_KEYS' 9 | 10 | # 11 | # evaluates if the key matches the matcher 12 | # 13 | # @return [boolean] true for all instances 14 | def match?(_args) 15 | @logger.log_if_debug('[AllKeysMatcher] is always -> true') 16 | true 17 | end 18 | 19 | # 20 | # evaluates if the given object equals the matcher 21 | # 22 | # @param obj [object] object to be evaluated 23 | # 24 | # @return [boolean] true if obj equals the matcher 25 | def equals?(obj) 26 | if obj.instance_of?(AllKeysMatcher) 27 | true 28 | else 29 | super(obj) 30 | end 31 | end 32 | 33 | # 34 | # function to print string value for this matcher 35 | # 36 | # @return [string] string value of this matcher 37 | def to_s 38 | 'in segment all' 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/splitclient-rb/engine/matchers/between_matcher.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SplitIoClient 4 | class BetweenMatcher < Matcher 5 | MATCHER_TYPE = 'BETWEEN' 6 | 7 | attr_reader :attribute 8 | 9 | def initialize(attribute_hash, logger, validator) 10 | super(logger) 11 | @validator = validator 12 | @attribute = attribute_hash[:attribute] 13 | @data_type = attribute_hash[:data_type] 14 | @start_value = formatted_value(attribute_hash[:start_value], true) 15 | @end_value = formatted_value(attribute_hash[:end_value], true) 16 | end 17 | 18 | def match?(args) 19 | @logger.log_if_debug('[BetweenMatcher] evaluating value and attributes.') 20 | 21 | return false unless @validator.valid_matcher_arguments(args) 22 | 23 | value = formatted_value(args[:value] || args[:attributes][@attribute.to_sym]) 24 | @logger.log_if_debug("[BetweenMatcher] Value from parameters: #{value}.") 25 | return false unless value.is_a?(Integer) 26 | 27 | matches = (@start_value..@end_value).cover? value 28 | @logger.log_if_debug("[BetweenMatcher] is #{value} between #{@start_value} and #{@end_value} -> #{matches} .") 29 | matches 30 | end 31 | 32 | private 33 | 34 | def formatted_value(value, sdk_data = false) 35 | case @data_type 36 | when 'NUMBER' 37 | value 38 | when 'DATETIME' 39 | value /= 1000 if sdk_data 40 | 41 | SplitIoClient::Utilities.to_milis_zero_out_from_seconds(value) 42 | else 43 | @logger.error('Invalid data type') 44 | end 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/splitclient-rb/engine/matchers/between_semver_matcher.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SplitIoClient 4 | class BetweenSemverMatcher < Matcher 5 | MATCHER_TYPE = 'BETWEEN_SEMVER' 6 | 7 | attr_reader :attribute 8 | 9 | def initialize(attribute, start_value, end_value, logger, validator) 10 | super(logger) 11 | @validator = validator 12 | @attribute = attribute 13 | @semver_start = SplitIoClient::Semver.build(start_value, logger) 14 | @semver_end = SplitIoClient::Semver.build(end_value, logger) 15 | @logger = logger 16 | end 17 | 18 | def match?(args) 19 | return false unless verify_semver_arg?(args, 'BetweenSemverMatcher') 20 | 21 | value_to_match = SplitIoClient::Semver.build(args[:attributes][@attribute.to_sym], @logger) 22 | if value_to_match.nil? || @semver_start.nil? || @semver_end.nil? 23 | @logger.error('betweenStringMatcherData is required for BETWEEN_SEMVER matcher type') 24 | return false 25 | 26 | end 27 | matches = ([0, -1].include?(@semver_start.compare(value_to_match)) && 28 | [0, 1].include?(@semver_end.compare(value_to_match))) 29 | @logger.debug("[BetweenMatcher] #{value_to_match} matches -> #{matches}") 30 | matches 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/splitclient-rb/engine/matchers/combiners.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SplitIoClient 4 | # 5 | # class to represent combiner values 6 | # 7 | class Combiners 8 | # available combiners of the sdk 9 | AND = 'AND' 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/splitclient-rb/engine/matchers/contains_all_matcher.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SplitIoClient 4 | class ContainsAllMatcher < SetMatcher 5 | MATCHER_TYPE = 'CONTAINS_ALL' 6 | 7 | attr_reader :attribute 8 | 9 | def match?(args) 10 | if @remote_set.empty? 11 | @logger.log_if_debug('[ContainsAllMatcher] Remote Set Empty') 12 | return false 13 | end 14 | 15 | matches = @remote_set.subset? local_set(args[:attributes], @attribute) 16 | @logger.log_if_debug("[ContainsAllMatcher] Remote Set #{@remote_set} contains #{@attribute} -> #{matches}") 17 | matches 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/splitclient-rb/engine/matchers/contains_any_matcher.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SplitIoClient 4 | class ContainsAnyMatcher < SetMatcher 5 | MATCHER_TYPE = 'CONTAINS_ANY' 6 | 7 | attr_reader :attribute 8 | 9 | def match?(args) 10 | matches = local_set(args[:attributes], @attribute).intersect? @remote_set 11 | @logger.log_if_debug("[ContainsAnyMatcher] Remote Set #{@remote_set} contains any \ 12 | #{@attribute} or #{args[:attributes]}-> #{matches}") 13 | matches 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/splitclient-rb/engine/matchers/contains_matcher.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SplitIoClient 4 | class ContainsMatcher 5 | MATCHER_TYPE = 'CONTAINS_WITH' 6 | 7 | attr_reader :attribute 8 | 9 | def initialize(attribute, substr_list, logger, validator) 10 | @attribute = attribute 11 | @substr_list = substr_list 12 | @logger = logger 13 | @validator = validator 14 | end 15 | 16 | def match?(args) 17 | @logger.log_if_debug('[ContainsMatcher] evaluating value and attributes.') 18 | 19 | return false unless @validator.valid_matcher_arguments(args) 20 | 21 | value = get_value(args) 22 | 23 | @logger.log_if_debug("[ContainsMatcher] Value from parameters: #{value}.") 24 | return false if @substr_list.empty? 25 | 26 | matches = @substr_list.any? { |substr| value.to_s.include? substr } 27 | @logger.log_if_debug("[ContainsMatcher] #{@value} contains any of #{@substr_list} -> #{matches} .") 28 | matches 29 | end 30 | 31 | def string_type? 32 | true 33 | end 34 | 35 | private 36 | 37 | def get_value(args) 38 | args[:value] || args[:attributes].fetch(@attribute) do |a| 39 | args[:attributes][a.to_s] || args[:attributes][a.to_sym] 40 | end 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/splitclient-rb/engine/matchers/dependency_matcher.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SplitIoClient 4 | class DependencyMatcher 5 | MATCHER_TYPE = 'IN_SPLIT_TREATMENT' 6 | 7 | def initialize(feature_flag, treatments, logger) 8 | @feature_flag = feature_flag 9 | @treatments = treatments 10 | @logger = logger 11 | end 12 | 13 | def match?(args) 14 | keys = { matching_key: args[:matching_key], bucketing_key: args[:bucketing_key] } 15 | evaluate = args[:evaluator].evaluate_feature_flag(keys, @feature_flag, args[:attributes]) 16 | matches = @treatments.include?(evaluate[:treatment]) 17 | @logger.log_if_debug("[dependencyMatcher] Parent feature flag #{@feature_flag} evaluated to #{evaluate[:treatment]} \ 18 | with label #{evaluate[:label]}. #{@feature_flag} evaluated treatment is part of [#{@treatments}] ? #{matches}.") 19 | matches 20 | end 21 | 22 | def string_type? 23 | false 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/splitclient-rb/engine/matchers/ends_with_matcher.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SplitIoClient 4 | class EndsWithMatcher 5 | MATCHER_TYPE = 'ENDS_WITH' 6 | 7 | attr_reader :attribute 8 | 9 | def initialize(attribute, suffix_list, logger) 10 | @attribute = attribute 11 | @suffix_list = suffix_list 12 | @logger = logger 13 | end 14 | 15 | def match?(args) 16 | value = get_value(args) 17 | 18 | @logger.log_if_debug("[EndsWithMatcher] Value from parameters: #{value}.") 19 | 20 | if @suffix_list.empty? 21 | @logger.log_if_debug('[EndsWithMatcher] Sufix List empty.') 22 | return false 23 | end 24 | 25 | matches = @suffix_list.any? { |suffix| value.to_s.end_with? suffix } 26 | @logger.log_if_debug("[EndsWithMatcher] #{value} ends with any #{@suffix_list}") 27 | matches 28 | end 29 | 30 | def string_type? 31 | true 32 | end 33 | 34 | private 35 | 36 | def get_value(args) 37 | args[:value] || args[:attributes].fetch(@attribute) do |a| 38 | args[:attributes][a.to_s] || args[:attributes][a.to_sym] 39 | end 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/splitclient-rb/engine/matchers/equal_to_boolean_matcher.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SplitIoClient 4 | class EqualToBooleanMatcher 5 | MATCHER_TYPE = 'EQUAL_TO_BOOLEAN' 6 | 7 | attr_reader :attribute 8 | 9 | def initialize(attribute, boolean, logger) 10 | @attribute = attribute 11 | @boolean = boolean 12 | @logger = logger 13 | end 14 | 15 | def match?(args) 16 | value = get_value(args) 17 | value = false if value.to_s.casecmp('false').zero? 18 | value = true if value.to_s.casecmp('true').zero? 19 | 20 | matches = value == @boolean 21 | @logger.log_if_debug("[EqualToBooleanMatcher] #{value} equals to #{@boolean} -> #{matches}") 22 | matches 23 | end 24 | 25 | def string_type? 26 | false 27 | end 28 | 29 | private 30 | 31 | def get_value(args) 32 | args[:attributes].fetch(@attribute) do |a| 33 | args[:attributes][a.to_s] || args[:attributes][a.to_sym] 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/splitclient-rb/engine/matchers/equal_to_matcher.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SplitIoClient 4 | class EqualToMatcher < Matcher 5 | MATCHER_TYPE = 'EQUAL_TO' 6 | 7 | attr_reader :attribute 8 | 9 | def initialize(attribute_hash, logger, validator) 10 | super(logger) 11 | @validator = validator 12 | @attribute = attribute_hash[:attribute] 13 | @data_type = attribute_hash[:data_type] 14 | @value = formatted_value(attribute_hash[:value], true) 15 | end 16 | 17 | def match?(args) 18 | @logger.log_if_debug('[EqualsToMatcher] evaluating value and attributes.') 19 | 20 | return false unless @validator.valid_matcher_arguments(args) 21 | 22 | value = formatted_value(args[:value] || args[:attributes][@attribute.to_sym]) 23 | 24 | matches = value.is_a?(Integer) ? (value == @value) : false 25 | @logger.log_if_debug("[EqualsToMatcher] #{value} equals to #{@value} -> #{matches}") 26 | matches 27 | end 28 | 29 | private 30 | 31 | def formatted_value(value, sdk_data = false) 32 | case @data_type 33 | when 'NUMBER' 34 | value 35 | when 'DATETIME' 36 | value /= 1000 if sdk_data 37 | 38 | SplitIoClient::Utilities.to_milis_zero_out_from_hour value 39 | else 40 | @logger.error('Invalid data type') 41 | end 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/splitclient-rb/engine/matchers/equal_to_semver_matcher.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SplitIoClient 4 | class EqualToSemverMatcher < Matcher 5 | MATCHER_TYPE = 'EQUAL_TO_SEMVER' 6 | 7 | attr_reader :attribute 8 | 9 | def initialize(attribute, string_value, logger, validator) 10 | super(logger) 11 | @validator = validator 12 | @attribute = attribute 13 | @semver = SplitIoClient::Semver.build(string_value, logger) 14 | @logger = logger 15 | end 16 | 17 | def match?(args) 18 | return false unless verify_semver_arg?(args, 'EqualsToSemverMatcher') 19 | 20 | value_to_match = SplitIoClient::Semver.build(args[:attributes][@attribute.to_sym], @logger) 21 | return false unless check_semver_value_to_match(value_to_match, MATCHER_TYPE) 22 | 23 | matches = (@semver.version == value_to_match.version) 24 | @logger.debug("[EqualsToSemverMatcher] #{value_to_match} matches -> #{matches}") 25 | matches 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/splitclient-rb/engine/matchers/equal_to_set_matcher.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SplitIoClient 4 | class EqualToSetMatcher < SetMatcher 5 | MATCHER_TYPE = 'EQUAL_TO_SET' 6 | 7 | attr_reader :attribute 8 | 9 | def match?(args) 10 | set = local_set(args[:attributes], @attribute) 11 | matches = set == @remote_set 12 | @logger.log_if_debug("[EqualsToSetMatcher] #{set} equals to #{@remote_set} -> #{matches}") 13 | matches 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/splitclient-rb/engine/matchers/greater_than_or_equal_to_matcher.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SplitIoClient 4 | class GreaterThanOrEqualToMatcher < Matcher 5 | MATCHER_TYPE = 'GREATER_THAN_OR_EQUAL_TO' 6 | 7 | attr_reader :attribute 8 | 9 | def initialize(attribute_hash, logger, validator) 10 | super(logger) 11 | @validator = validator 12 | @attribute = attribute_hash[:attribute] 13 | @data_type = attribute_hash[:data_type] 14 | @value = formatted_value(attribute_hash[:value], true) 15 | end 16 | 17 | def match?(args) 18 | @logger.log_if_debug('[GreaterThanOrEqualToMatcher] evaluating value and attributes.') 19 | 20 | return false unless @validator.valid_matcher_arguments(args) 21 | 22 | value = formatted_value(args[:value] || args[:attributes][@attribute.to_sym]) 23 | 24 | matches = value.is_a?(Integer) ? (value >= @value) : false 25 | @logger.log_if_debug("[GreaterThanOrEqualToMatcher] #{value} greater than or equal to #{@value} -> #{matches}") 26 | matches 27 | end 28 | 29 | private 30 | 31 | def formatted_value(value, sdk_data = false) 32 | case @data_type 33 | when 'NUMBER' 34 | value 35 | when 'DATETIME' 36 | value /= 1000 if sdk_data # sdk returns already miliseconds, turning to seconds to do a correct zero_hour 37 | SplitIoClient::Utilities.to_milis_zero_out_from_seconds(value) 38 | else 39 | @logger.error('Invalid data type') 40 | end 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/splitclient-rb/engine/matchers/greater_than_or_equal_to_semver_matcher.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SplitIoClient 4 | class GreaterThanOrEqualToSemverMatcher < Matcher 5 | MATCHER_TYPE = 'GREATER_THAN_OR_EQUAL_TO_SEMVER' 6 | 7 | attr_reader :attribute 8 | 9 | def initialize(attribute, string_value, logger, validator) 10 | super(logger) 11 | @validator = validator 12 | @attribute = attribute 13 | @semver = SplitIoClient::Semver.build(string_value, logger) 14 | @logger = logger 15 | end 16 | 17 | def match?(args) 18 | return false unless verify_semver_arg?(args, 'GreaterThanOrEqualsToSemverMatcher') 19 | 20 | value_to_match = SplitIoClient::Semver.build(args[:attributes][@attribute.to_sym], @logger) 21 | return false unless check_semver_value_to_match(value_to_match, MATCHER_TYPE) 22 | 23 | matches = [0, 1].include?(value_to_match.compare(@semver)) 24 | @logger.debug("[GreaterThanOrEqualsToSemverMatcher] #{value_to_match} matches -> #{matches}") 25 | matches 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/splitclient-rb/engine/matchers/in_list_semver_matcher.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SplitIoClient 4 | class InListSemverMatcher < Matcher 5 | MATCHER_TYPE = 'IN_LIST_SEMVER' 6 | 7 | attr_reader :attribute 8 | 9 | def initialize(attribute, list_value, logger, validator) 10 | super(logger) 11 | @validator = validator 12 | @attribute = attribute 13 | @semver_list = [] 14 | 15 | list_value.map do |item| 16 | version = SplitIoClient::Semver.build(item, logger) 17 | @semver_list << version unless version.nil? 18 | end 19 | @logger = logger 20 | end 21 | 22 | def match?(args) 23 | return false if @semver_list.empty? || !verify_semver_arg?(args, 'InListSemverMatcher') 24 | 25 | value_to_match = SplitIoClient::Semver.build(args[:attributes][@attribute.to_sym], @logger) 26 | if value_to_match.nil? 27 | @logger.error('whitelistMatcherData is required for IN_LIST_SEMVER matcher type') 28 | return false 29 | 30 | end 31 | matches = (@semver_list.map { |item| item.version == value_to_match.version }).any? { |item| item == true } 32 | @logger.debug("[InListSemverMatcher] #{value_to_match} matches -> #{matches}") 33 | matches 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/splitclient-rb/engine/matchers/less_than_or_equal_to_matcher.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SplitIoClient 4 | class LessThanOrEqualToMatcher < Matcher 5 | MATCHER_TYPE = 'LESS_THAN_OR_EQUAL_TO' 6 | 7 | attr_reader :attribute 8 | 9 | def initialize(attribute_hash, logger, validator) 10 | super(logger) 11 | @validator = validator 12 | @attribute = attribute_hash[:attribute] 13 | @data_type = attribute_hash[:data_type] 14 | @value = formatted_value(attribute_hash[:value], true) 15 | end 16 | 17 | def match?(args) 18 | @logger.log_if_debug('[LessThanOrEqualToMatcher] evaluating value and attributes.') 19 | 20 | return false unless @validator.valid_matcher_arguments(args) 21 | 22 | value = formatted_value(args[:value] || args[:attributes][@attribute.to_sym]) 23 | 24 | matches = value.is_a?(Integer) ? (value <= @value) : false 25 | @logger.log_if_debug("[LessThanOrEqualToMatcher] #{value} less than or equal to #{@value} -> #{matches}") 26 | matches 27 | end 28 | 29 | private 30 | 31 | def formatted_value(value, sdk_data = false) 32 | case @data_type 33 | when 'NUMBER' 34 | value 35 | when 'DATETIME' 36 | value /= 1000 if sdk_data # sdk returns already miliseconds, turning to seconds to do a correct zero_hour 37 | SplitIoClient::Utilities.to_milis_zero_out_from_seconds(value) 38 | else 39 | @logger.error('Invalid data type') 40 | end 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/splitclient-rb/engine/matchers/less_than_or_equal_to_semver_matcher.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SplitIoClient 4 | class LessThanOrEqualToSemverMatcher < Matcher 5 | MATCHER_TYPE = 'LESS_THAN_OR_EQUAL_TO_SEMVER' 6 | 7 | attr_reader :attribute 8 | 9 | def initialize(attribute, string_value, logger, validator) 10 | super(logger) 11 | @validator = validator 12 | @attribute = attribute 13 | @semver = SplitIoClient::Semver.build(string_value, logger) 14 | @logger = logger 15 | end 16 | 17 | def match?(args) 18 | return false unless verify_semver_arg?(args, 'LessThanOrEqualsToSemverMatcher') 19 | 20 | value_to_match = SplitIoClient::Semver.build(args[:attributes][@attribute.to_sym], @logger) 21 | return false unless check_semver_value_to_match(value_to_match, MATCHER_TYPE) 22 | 23 | matches = [0, -1].include?(value_to_match.compare(@semver)) 24 | @logger.debug("[LessThanOrEqualsToSemverMatcher] #{value_to_match} matches -> #{matches}") 25 | matches 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/splitclient-rb/engine/matchers/matcher.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SplitIoClient 4 | # 5 | # class to implement the all keys matcher 6 | # 7 | class Matcher 8 | def initialize(logger) 9 | @logger = logger 10 | end 11 | 12 | # 13 | # evaluates if the given object equals the matcher 14 | # 15 | # @param obj [object] object to be evaluated 16 | # 17 | # @return [boolean] true if obj equals the matcher 18 | def equals?(obj) 19 | if obj.nil? 20 | false 21 | elsif !obj.instance_of?(self.class) 22 | false 23 | elsif equal?(obj) 24 | true 25 | else 26 | false 27 | end 28 | end 29 | 30 | def string_type? 31 | false 32 | end 33 | 34 | private 35 | 36 | def verify_semver_arg?(args, matcher_name) 37 | @logger.debug("[#{matcher_name}] evaluating value and attributes.") 38 | return false unless @validator.valid_matcher_arguments(args) 39 | 40 | true 41 | end 42 | 43 | def check_semver_value_to_match(value_to_match, matcher_spec_name) 44 | if value_to_match.nil? || @semver.nil? 45 | @logger.error("stringMatcherData is required for #{matcher_spec_name} matcher type") 46 | return false 47 | 48 | end 49 | true 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/splitclient-rb/engine/matchers/matches_string_matcher.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SplitIoClient 4 | class MatchesStringMatcher 5 | MATCHER_TYPE = 'MATCHES_STRING' 6 | 7 | attr_reader :attribute 8 | 9 | def initialize(attribute, regexp_string, logger) 10 | @attribute = attribute 11 | @regexp_string = @regexp_string.is_a?(Regexp) ? regexp_string : Regexp.new(regexp_string) 12 | @logger = logger 13 | end 14 | 15 | def match?(args) 16 | value = args[:value] || args[:attributes].fetch(@attribute) do |a| 17 | args[:attributes][a.to_s] || args[:attributes][a.to_sym] 18 | end 19 | 20 | matches = !(value =~ @regexp_string).nil? 21 | @logger.log_if_debug("[MatchesStringMatcher] #{value} matches #{@regexp_string} -> #{matches}") 22 | matches 23 | end 24 | 25 | def string_type? 26 | true 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/splitclient-rb/engine/matchers/negation_matcher.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SplitIoClient 4 | # 5 | # class to implement the negation of a matcher 6 | # 7 | class NegationMatcher < Matcher 8 | MATCHER_TYPE = 'NEGATION_MATCHER' 9 | 10 | def initialize(logger, matcher = nil) 11 | super(logger) 12 | @matcher = matcher 13 | end 14 | 15 | # 16 | # evaluates if the key matches the negation of the matcher 17 | # 18 | # @param key [string] key value to be matched 19 | # 20 | # @return [boolean] evaluation of the negation matcher 21 | def match?(args) 22 | matches = !@matcher.match?(args) 23 | @logger.log_if_debug("[NegationMatcherMatcher] Matcher #{@matcher} Arguments #{args} -> #{matches}") 24 | matches 25 | end 26 | 27 | def respond_to?(method) 28 | @matcher.respond_to? method 29 | end 30 | 31 | def attribute 32 | @matcher.attribute 33 | end 34 | 35 | def string_type? 36 | @matcher.string_type? 37 | end 38 | 39 | # 40 | # function to print string value for this matcher 41 | # 42 | # @return [string] string value of this matcher 43 | def to_s 44 | "not #{@matcher}" 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/splitclient-rb/engine/matchers/part_of_set_matcher.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SplitIoClient 4 | class PartOfSetMatcher < SetMatcher 5 | MATCHER_TYPE = 'PART_OF_SET' 6 | 7 | attr_reader :attribute 8 | 9 | def match?(args) 10 | @local_set = local_set(args[:attributes], @attribute) 11 | 12 | if @local_set.empty? 13 | @logger.log_if_debug('[PartOfSetMatcher] Local Set is empty.') 14 | return false 15 | end 16 | 17 | matches = @local_set.subset? @remote_set 18 | @logger.log_if_debug("[PartOfSetMatcher] Local Set #{@local_set} is a subset of #{@remote_set} -> #{matches}") 19 | matches 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/splitclient-rb/engine/matchers/set_matcher.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'set' 4 | 5 | module SplitIoClient 6 | class SetMatcher 7 | def string_type? 8 | false 9 | end 10 | 11 | protected 12 | 13 | def initialize(attribute, remote_array, logger) 14 | @attribute = attribute 15 | @remote_set = remote_array.to_set 16 | @logger = logger 17 | end 18 | 19 | def local_set(data, attribute) 20 | data = data.fetch(attribute) { |a| data[a.to_s] || data[a.to_sym] } 21 | # Allow user to pass individual elements as well 22 | local_array = data.is_a?(Array) ? data : [data] 23 | 24 | local_array.to_set 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/splitclient-rb/engine/matchers/starts_with_matcher.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SplitIoClient 4 | class StartsWithMatcher 5 | MATCHER_TYPE = 'STARTS_WITH' 6 | 7 | attr_reader :attribute 8 | 9 | def initialize(attribute, prefix_list, logger) 10 | @attribute = attribute 11 | @prefix_list = prefix_list 12 | @logger = logger 13 | end 14 | 15 | def match?(args) 16 | if @prefix_list.empty? 17 | @logger.log_if_debug('[StartsWithMatcher] Prefix List is empty.') 18 | return false 19 | end 20 | 21 | value = get_value(args) 22 | 23 | matches = @prefix_list.any? { |prefix| value.to_s.start_with? prefix } 24 | @logger.log_if_debug("[StartsWithMatcher] #{value} matches any of #{@prefix_list} -> #{matches}") 25 | matches 26 | end 27 | 28 | def string_type? 29 | true 30 | end 31 | 32 | private 33 | 34 | def get_value(args) 35 | args[:value] || args[:attributes].fetch(@attribute) do |a| 36 | args[:attributes][a.to_s] || args[:attributes][a.to_sym] 37 | end 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/splitclient-rb/engine/matchers/user_defined_segment_matcher.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SplitIoClient 4 | # 5 | # class to implement the user defined matcher 6 | # 7 | class UserDefinedSegmentMatcher < Matcher 8 | MATCHER_TYPE = 'IN_SEGMENT' 9 | 10 | def initialize(segments_repository, segment_name, logger) 11 | super(logger) 12 | @segments_repository = segments_repository 13 | @segment_name = segment_name 14 | end 15 | 16 | # 17 | # evaluates if the key matches the matcher 18 | # 19 | # @param key [string] key value to be matched 20 | # 21 | # @return [boolean] evaluation of the key against the segment 22 | def match?(args) 23 | matches = @segments_repository.in_segment?(@segment_name, args[:value] || args[:matching_key]) 24 | @logger.log_if_debug("[InSegmentMatcher] #{@segment_name} is in segment -> #{matches}") 25 | matches 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/splitclient-rb/engine/matchers/whitelist_matcher.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SplitIoClient 4 | # 5 | # class to implement the user defined matcher 6 | # 7 | class WhitelistMatcher < Matcher 8 | MATCHER_TYPE = 'WHITELIST_MATCHER' 9 | 10 | attr_reader :attribute 11 | 12 | def initialize(whitelist_data, logger, validator) 13 | super(logger) 14 | @validator = validator 15 | @whitelist = case whitelist_data 16 | when Array 17 | whitelist_data 18 | when Hash 19 | @matcher_type = 'ATTR_WHITELIST' 20 | @attribute = whitelist_data[:attribute] 21 | 22 | whitelist_data[:value] 23 | else 24 | [] 25 | end 26 | end 27 | 28 | def match?(args) 29 | return matches_user_whitelist(args) unless @matcher_type == 'ATTR_WHITELIST' 30 | 31 | @logger.log_if_debug('[WhitelistMatcher] evaluating value and attributes.') 32 | 33 | return false unless @validator.valid_matcher_arguments(args) 34 | 35 | matches_attr_whitelist(args) 36 | end 37 | 38 | def string_type? 39 | true 40 | end 41 | 42 | # 43 | # function to print string value for this matcher 44 | # 45 | # @return [string] string value of this matcher 46 | def to_s 47 | "in segment #{@whitelist}" 48 | end 49 | 50 | private 51 | 52 | def matches_user_whitelist(args) 53 | matches = @whitelist.include?(args[:value] || args[:matching_key]) 54 | @logger.log_if_debug("[WhitelistMatcher] #{@whitelist} include \ 55 | #{args[:value] || args[:matching_key]} -> #{matches}") 56 | matches 57 | end 58 | 59 | def matches_attr_whitelist(args) 60 | matches = @whitelist.include?(args[:value] || args[:attributes][@attribute.to_sym]) 61 | @logger.log_if_debug("[WhitelistMatcher] #{@whitelist} include \ 62 | #{args[:value] || args[:attributes][@attribute.to_sym]} -> #{matches}") 63 | matches 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /lib/splitclient-rb/engine/metrics/binary_search_latency_tracker.rb: -------------------------------------------------------------------------------- 1 | module SplitIoClient 2 | 3 | # 4 | # Tracks latencies pero bucket of time. 5 | # Each bucket represent a latency greater than the one before 6 | # and each number within each bucket is a number of calls in the range. 7 | # 8 | # (1) 1.00 9 | # (2) 1.50 10 | # (3) 2.25 11 | # (4) 3.38 12 | # (5) 5.06 13 | # (6) 7.59 14 | # (7) 11.39 15 | # (8) 17.09 16 | # (9) 25.63 17 | # (10) 38.44 18 | # (11) 57.67 19 | # (12) 86.50 20 | # (13) 129.75 21 | # (14) 194.62 22 | # (15) 291.93 23 | # (16) 437.89 24 | # (17) 656.84 25 | # (18) 985.26 26 | # (19) 1,477.89 27 | # (20) 2,216.84 28 | # (21) 3,325.26 29 | # (22) 4,987.89 30 | # (23) 7,481.83 31 | # 32 | # Created by fvitale on 2/17/16 based on java implementation by patricioe. 33 | # 34 | 35 | class BinarySearchLatencyTracker < NoMethodError 36 | 37 | BUCKETS = [ 1000, 1500, 2250, 3375, 5063, 38 | 7594, 11391, 17086, 25629, 38443, 39 | 57665, 86498, 129746, 194620, 291929, 40 | 437894, 656841, 985261, 1477892, 2216838, 41 | 3325257, 4987885, 7481828 ].freeze 42 | 43 | MAX_LATENCY = 7481828 44 | 45 | def self.get_bucket(latency) 46 | return find_bucket_index(latency * 1000) 47 | end 48 | 49 | private 50 | 51 | def self.find_bucket_index(micros) 52 | if (micros > MAX_LATENCY) then 53 | return BUCKETS.length - 1 54 | end 55 | 56 | if (micros < 1500) then 57 | return 0 58 | end 59 | 60 | index = BUCKETS.find_index(BUCKETS.bsearch {|x| x >= micros }) 61 | 62 | return index 63 | end 64 | 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /lib/splitclient-rb/engine/models/label.rb: -------------------------------------------------------------------------------- 1 | class SplitIoClient::Engine::Models::Label 2 | ARCHIVED = 'archived'.freeze 3 | NO_RULE_MATCHED = 'default rule'.freeze 4 | EXCEPTION = 'exception'.freeze 5 | KILLED = 'killed'.freeze 6 | NOT_IN_SPLIT = 'not in split'.freeze 7 | NOT_READY = 'not ready'.freeze 8 | NOT_FOUND = 'definition not found'.freeze 9 | end 10 | -------------------------------------------------------------------------------- /lib/splitclient-rb/engine/models/split.rb: -------------------------------------------------------------------------------- 1 | module SplitIoClient 2 | module Engine 3 | module Models 4 | class Split 5 | class << self 6 | def matchable?(data) 7 | data && data[:status] == 'ACTIVE' && data[:killed] == false 8 | end 9 | 10 | def archived?(data) 11 | data && data[:status] == 'ARCHIVED' 12 | end 13 | end 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/splitclient-rb/engine/models/treatment.rb: -------------------------------------------------------------------------------- 1 | class SplitIoClient::Engine::Models::Treatment 2 | CONTROL = 'control'.freeze 3 | end 4 | -------------------------------------------------------------------------------- /lib/splitclient-rb/engine/parser/partition.rb: -------------------------------------------------------------------------------- 1 | module SplitIoClient 2 | # 3 | # acts as dto for a partition structure 4 | # 5 | class Partition < NoMethodError 6 | 7 | # 8 | # definition of the condition 9 | # 10 | # @returns [object] condition values 11 | attr_accessor :data 12 | 13 | def initialize(partition) 14 | @data = partition 15 | end 16 | 17 | # 18 | # @return [object] the treatment value for this partition 19 | def treatment 20 | @data[:treatment] 21 | end 22 | 23 | # 24 | # @return [object] the size value for this partition 25 | def size 26 | @data[:size] 27 | end 28 | 29 | # 30 | # @return [boolean] true if the partition is empty false otherwise 31 | def is_empty? 32 | @data.empty? 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/splitclient-rb/engine/status_manager.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SplitIoClient 4 | module Engine 5 | class StatusManager 6 | def initialize(config) 7 | @config = config 8 | @sdk_ready = Concurrent::CountDownLatch.new(1) 9 | end 10 | 11 | def ready? 12 | return true if @config.consumer? 13 | 14 | @sdk_ready.wait(0) 15 | end 16 | 17 | def ready! 18 | return if ready? 19 | 20 | @sdk_ready.count_down 21 | @config.logger.info('SplitIO SDK is ready') 22 | end 23 | 24 | def wait_until_ready(seconds = nil) 25 | return if @config.consumer? 26 | 27 | timeout = seconds || @config.block_until_ready 28 | 29 | raise SDKBlockerTimeoutExpiredException, 'SDK start up timeout expired' unless @sdk_ready.wait(timeout) 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/splitclient-rb/exceptions.rb: -------------------------------------------------------------------------------- 1 | module SplitIoClient 2 | class SplitIoError < StandardError; end 3 | 4 | class SDKShutdownException < Exception; end 5 | 6 | class SDKBlockerTimeoutExpiredException < SplitIoError; end 7 | 8 | class SSEClientException < SplitIoError 9 | attr_reader :event 10 | 11 | def initialize(event) 12 | @event = event 13 | end 14 | end 15 | 16 | class ApiException < SplitIoError 17 | def initialize(msg, exception_code) 18 | @@exception_code = exception_code 19 | super(msg) 20 | end 21 | def exception_code 22 | @@exception_code 23 | end 24 | end 25 | 26 | end 27 | -------------------------------------------------------------------------------- /lib/splitclient-rb/helpers/decryption_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SplitIoClient 4 | NO_COMPRESSION = 0 5 | GZIP_COMPRESSION = 1 6 | ZLIB_COMPRESSION = 2 7 | 8 | module Helpers 9 | class DecryptionHelper 10 | def self.get_encoded_definition(compression, data) 11 | case compression 12 | when NO_COMPRESSION 13 | Base64.decode64(data) 14 | when GZIP_COMPRESSION 15 | gz = Zlib::GzipReader.new(StringIO.new(Base64.decode64(data))) 16 | gz.read 17 | when ZLIB_COMPRESSION 18 | Zlib::Inflate.inflate(Base64.decode64(data)) 19 | else 20 | raise StandardError, 'Compression flag value is incorrect' 21 | end 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/splitclient-rb/helpers/repository_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SplitIoClient 4 | module Helpers 5 | class RepositoryHelper 6 | def self.update_feature_flag_repository(feature_flag_repository, feature_flags, change_number, config) 7 | to_add = [] 8 | to_delete = [] 9 | feature_flags.each do |feature_flag| 10 | if Engine::Models::Split.archived?(feature_flag) || !feature_flag_repository.flag_set_filter.intersect?(feature_flag[:sets]) 11 | config.logger.debug("removing feature flag from store(#{feature_flag})") if config.debug_enabled 12 | to_delete.push(feature_flag) 13 | next 14 | end 15 | 16 | unless feature_flag.key?(:impressionsDisabled) 17 | feature_flag[:impressionsDisabled] = false 18 | if config.debug_enabled 19 | config.logger.debug("feature flag (#{feature_flag[:name]}) does not have impressionsDisabled field, setting it to false") 20 | end 21 | end 22 | 23 | config.logger.debug("storing feature flag (#{feature_flag[:name]})") if config.debug_enabled 24 | to_add.push(feature_flag) 25 | end 26 | feature_flag_repository.update(to_add, to_delete, change_number) 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/splitclient-rb/helpers/thread_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SplitIoClient 4 | module Helpers 5 | class ThreadHelper 6 | def self.stop(thread_sym, config) 7 | thread = config.threads[thread_sym] 8 | 9 | unless thread.nil? 10 | config.logger.debug("Stopping #{thread_sym} thread...") if config.debug_enabled 11 | Thread.kill(thread) 12 | end 13 | rescue StandardError => e 14 | config.logger.error(e.inspect) 15 | end 16 | 17 | def self.alive?(thread_sym, config) 18 | thread = config.threads[thread_sym] 19 | 20 | thread.nil? ? false : thread.alive? 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/splitclient-rb/helpers/util.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SplitIoClient 4 | module Helpers 5 | class Util 6 | def self.segment_names_by_feature_flag(feature_flag) 7 | feature_flag[:conditions].each_with_object(Set.new) do |condition, names| 8 | condition[:matcherGroup][:matchers].each do |matcher| 9 | next if matcher[:userDefinedSegmentMatcherData].nil? 10 | 11 | names << matcher[:userDefinedSegmentMatcherData][:segmentName] 12 | end 13 | end 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/splitclient-rb/spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SplitIoClient 4 | module Spec 5 | class FeatureFlags 6 | SPEC_VERSION = "1.1" 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/splitclient-rb/split_factory_builder.rb: -------------------------------------------------------------------------------- 1 | module SplitIoClient 2 | class SplitFactoryBuilder 3 | def self.build(api_key, config = {}) 4 | SplitFactory.new(api_key, config) 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/splitclient-rb/split_factory_registry.rb: -------------------------------------------------------------------------------- 1 | require 'logger' 2 | require 'socket' 3 | 4 | module SplitIoClient 5 | 6 | class << self 7 | attr_accessor :split_factory_registry 8 | end 9 | 10 | def self.load_factory_registry 11 | self.split_factory_registry ||= SplitFactoryRegistry.new 12 | end 13 | 14 | # 15 | # This class manages configuration options for the split client library. 16 | # If not custom configuration is required the default configuration values will be used 17 | # 18 | class SplitFactoryRegistry 19 | 20 | attr_accessor :api_keys_hash 21 | 22 | def initialize 23 | @api_keys_hash = Hash.new 24 | end 25 | 26 | def add(api_key) 27 | return unless api_key 28 | 29 | @api_keys_hash[api_key] = 0 unless @api_keys_hash[api_key] 30 | @api_keys_hash[api_key] += 1 31 | end 32 | 33 | def remove(api_key) 34 | return unless api_key 35 | 36 | @api_keys_hash[api_key] -= 1 if @api_keys_hash[api_key] 37 | @api_keys_hash.delete(api_key) if @api_keys_hash[api_key] == 0 38 | end 39 | 40 | def number_of_factories_for(api_key) 41 | return 0 unless api_key 42 | return 0 unless @api_keys_hash.key?(api_key) 43 | 44 | @api_keys_hash[api_key] 45 | end 46 | 47 | def other_factories 48 | return !@api_keys_hash.empty? 49 | end 50 | 51 | def active_factories 52 | @api_keys_hash.length 53 | end 54 | 55 | def redundant_active_factories 56 | to_return = 0 57 | 58 | @api_keys_hash.each { |key| to_return += (key[1]-1) } 59 | 60 | to_return 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /lib/splitclient-rb/split_logger.rb: -------------------------------------------------------------------------------- 1 | module SplitIoClient 2 | class SplitLogger 3 | def initialize(config) 4 | @config = config 5 | end 6 | 7 | def log_if_debug(message) 8 | @config.logger.debug(message) if @config.debug_enabled 9 | end 10 | 11 | def log_if_transport(message) 12 | @config.logger.debug(message) if @config.transport_debug_enabled 13 | end 14 | 15 | def error(message) 16 | @config.logger.error(message) 17 | end 18 | 19 | def debug(message) 20 | @config.logger.debug(message) if @config.debug_enabled 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/splitclient-rb/sse/event_source/event_parser.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: false 2 | 3 | module SplitIoClient 4 | module SSE 5 | module EventSource 6 | class EventParser 7 | BAD_REQUEST_CODE = 400 8 | 9 | def initialize(config) 10 | @config = config 11 | end 12 | 13 | def parse(raw_event) 14 | type = nil 15 | events = [] 16 | buffer = read_partial_data(raw_event) 17 | 18 | buffer.each do |d| 19 | splited_data = d.split(':') 20 | 21 | case splited_data[0] 22 | when 'event' 23 | type = splited_data[1].strip 24 | when 'data' 25 | data = parse_event_data(d, type) 26 | events << StreamData.new(type, data[:client_id], data[:data], data[:channel]) unless type.nil? || data[:data].nil? 27 | end 28 | end 29 | 30 | events 31 | rescue StandardError => e 32 | @config.logger.debug("Error during parsing a event: #{e.inspect}") 33 | [] 34 | end 35 | 36 | def first_event(raw_data) 37 | raw_data.split("\n")[0].split(' ')[1].to_i 38 | rescue StandardError => e 39 | @config.logger.debug("Error parsing first event: #{e.inspect}") 40 | BAD_REQUEST_CODE 41 | end 42 | 43 | private 44 | 45 | def parse_event_data(data, type) 46 | data_value = data.sub('data:', '') 47 | event_data = JSON.parse(data_value.strip) 48 | client_id = event_data['clientId']&.strip 49 | channel = event_data['channel']&.strip 50 | parsed_data = JSON.parse(event_data['data']) unless type == 'error' 51 | parsed_data = event_data if type == 'error' 52 | 53 | { client_id: client_id, channel: channel, data: parsed_data } 54 | end 55 | 56 | def read_partial_data(data) 57 | buffer = '' 58 | buffer << data 59 | buffer.chomp! 60 | buffer.split("\n") 61 | end 62 | end 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /lib/splitclient-rb/sse/event_source/event_types.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SplitIoClient 4 | module SSE 5 | module EventSource 6 | class EventTypes 7 | SPLIT_UPDATE = 'SPLIT_UPDATE' 8 | SPLIT_KILL = 'SPLIT_KILL' 9 | SEGMENT_UPDATE = 'SEGMENT_UPDATE' 10 | CONTROL = 'CONTROL' 11 | end 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/splitclient-rb/sse/event_source/stream_data.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: false 2 | 3 | module SplitIoClient 4 | module SSE 5 | module EventSource 6 | class StreamData 7 | attr_reader :event_type, :channel, :data, :client_id 8 | 9 | def initialize(event_type, client_id, data, channel) 10 | @event_type = event_type 11 | @client_id = client_id 12 | @data = data 13 | @channel = channel&.gsub(SplitIoClient::Constants::OCCUPANCY_CHANNEL_PREFIX, '') 14 | end 15 | 16 | def occupancy? 17 | @channel.include? 'control' 18 | end 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/splitclient-rb/sse/notification_processor.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SplitIoClient 4 | module SSE 5 | class NotificationProcessor 6 | def initialize(config, splits_worker, segments_worker) 7 | @config = config 8 | @splits_worker = splits_worker 9 | @segments_worker = segments_worker 10 | end 11 | 12 | def process(incoming_notification) 13 | case incoming_notification.data['type'] 14 | when SSE::EventSource::EventTypes::SPLIT_UPDATE 15 | process_split_update(incoming_notification) 16 | when SSE::EventSource::EventTypes::SPLIT_KILL 17 | process_split_kill(incoming_notification) 18 | when SSE::EventSource::EventTypes::SEGMENT_UPDATE 19 | process_segment_update(incoming_notification) 20 | else 21 | @config.logger.error("Incorrect event type: #{incoming_notification}") 22 | end 23 | end 24 | 25 | private 26 | 27 | def process_split_update(notification) 28 | @config.logger.debug("SPLIT UPDATE notification received: #{notification}") if @config.debug_enabled 29 | @splits_worker.add_to_queue(notification) 30 | end 31 | 32 | def process_split_kill(notification) 33 | @config.logger.debug("SPLIT KILL notification received: #{notification}") if @config.debug_enabled 34 | @splits_worker.add_to_queue(notification) 35 | end 36 | 37 | def process_segment_update(notification) 38 | @config.logger.debug("SEGMENT UPDATE notification received: #{notification}") if @config.debug_enabled 39 | change_number = notification.data['changeNumber'] 40 | segment_name = notification.data['segmentName'] 41 | 42 | @segments_worker.add_to_queue(change_number, segment_name) 43 | end 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/splitclient-rb/sse/sse_handler.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SplitIoClient 4 | module SSE 5 | class SSEHandler 6 | attr_reader :sse_client 7 | 8 | def initialize(config, 9 | splits_worker, 10 | segments_worker, 11 | sse_client) 12 | @config = config 13 | @splits_worker = splits_worker 14 | @segments_worker = segments_worker 15 | @sse_client = sse_client 16 | end 17 | 18 | def start(token_jwt, channels) 19 | @sse_client.start("#{@config.streaming_service_url}?channels=#{channels}&v=1.1&accessToken=#{token_jwt}") 20 | end 21 | 22 | def stop 23 | @sse_client.close(Constants::PUSH_FORCED_STOP) 24 | stop_workers 25 | rescue StandardError => e 26 | @config.logger.debug("SSEHandler stop error: #{e.inspect}") if @config.debug_enabled 27 | end 28 | 29 | def connected? 30 | @sse_client&.connected? || false 31 | end 32 | 33 | def start_workers 34 | @splits_worker.start 35 | @segments_worker.start 36 | end 37 | 38 | def stop_workers 39 | @splits_worker.stop 40 | @segments_worker.stop 41 | end 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/splitclient-rb/sse/workers/segments_worker.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SplitIoClient 4 | module SSE 5 | module Workers 6 | class SegmentsWorker 7 | def initialize(synchronizer, config, segments_repository) 8 | @synchronizer = synchronizer 9 | @config = config 10 | @segments_repository = segments_repository 11 | @queue = Queue.new 12 | @running = Concurrent::AtomicBoolean.new(false) 13 | end 14 | 15 | def add_to_queue(change_number, segment_name) 16 | item = { change_number: change_number, segment_name: segment_name } 17 | @config.logger.debug("SegmentsWorker add to queue #{item}") 18 | @queue.push(item) 19 | end 20 | 21 | def start 22 | if @running.value 23 | @config.logger.debug('segments worker already running.') 24 | return 25 | end 26 | 27 | @running.make_true 28 | perform_thread 29 | end 30 | 31 | def stop 32 | unless @running.value 33 | @config.logger.debug('segments worker not running.') 34 | return 35 | end 36 | 37 | @running.make_false 38 | SplitIoClient::Helpers::ThreadHelper.stop(:segment_update_worker, @config) 39 | end 40 | 41 | private 42 | 43 | def perform 44 | while (item = @queue.pop) 45 | segment_name = item[:segment_name] 46 | cn = item[:change_number] 47 | @config.logger.debug("SegmentsWorker change_number dequeue #{segment_name}, #{cn}") 48 | 49 | @synchronizer.fetch_segment(segment_name, cn) 50 | end 51 | end 52 | 53 | def perform_thread 54 | @config.threads[:segment_update_worker] = Thread.new do 55 | @config.logger.debug('Starting segments worker ...') if @config.debug_enabled 56 | perform 57 | end 58 | end 59 | end 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /lib/splitclient-rb/telemetry/domain/constants.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SplitIoClient 4 | module Telemetry 5 | module Domain 6 | class Constants 7 | BUR_TIMEOUT = 'bur_timeout' 8 | NON_READY_USAGES = 'non_ready_usages' 9 | 10 | IMPRESSIONS_DROPPED = 'impressions_dropped' 11 | IMPRESSIONS_DEDUPE = 'impressions_deduped' 12 | IMPRESSIONS_QUEUED = 'impressions_queued' 13 | 14 | EVENTS_DROPPED = 'events_dropped' 15 | EVENTS_QUEUED = 'events_queued' 16 | 17 | SPLIT_SYNC = 'split_sync' 18 | SEGMENT_SYNC = 'segment_sync' 19 | IMPRESSIONS_SYNC = 'impressions_sync' 20 | IMPRESSION_COUNT_SYNC = 'impression_count_sync' 21 | EVENT_SYNC = 'event_sync' 22 | TELEMETRY_SYNC = 'telemetry_sync' 23 | TOKEN_SYNC = 'token_sync' 24 | 25 | SSE_CONNECTION_ESTABLISHED = 0 26 | OCCUPANCY_PRI = 10 27 | OCCUPANCY_SEC = 20 28 | STREAMING_STATUS = 30 29 | CONNECTION_ERROR = 40 30 | TOKEN_REFRESH = 50 31 | ABLY_ERROR = 60 32 | SYNC_MODE = 70 33 | 34 | TREATMENT = 'treatment' 35 | TREATMENTS = 'treatments' 36 | TREATMENT_WITH_CONFIG = 'treatmentWithConfig' 37 | TREATMENTS_WITH_CONFIG = 'treatmentsWithConfig' 38 | TREATMENTS_BY_FLAG_SET = 'treatmentsByFlagSet' 39 | TREATMENTS_BY_FLAG_SETS = 'treatmentsByFlagSets' 40 | TREATMENTS_WITH_CONFIG_BY_FLAG_SET = 'treatmentWithConfigByFlagSet' 41 | TREATMENTS_WITH_CONFIG_BY_FLAG_SETS = 'treatmentsWithConfigByFlagSets' 42 | TRACK = 'track' 43 | 44 | SPLITS = 'splits' 45 | end 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/splitclient-rb/telemetry/evaluation_consumer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SplitIoClient 4 | module Telemetry 5 | class EvaluationConsumer 6 | extend Forwardable 7 | def_delegators :@evaluation, :pop_latencies, :pop_exceptions 8 | 9 | def initialize(config) 10 | @evaluation = SplitIoClient::Telemetry::MemoryEvaluationConsumer.new(config) 11 | end 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/splitclient-rb/telemetry/evaluation_producer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SplitIoClient 4 | module Telemetry 5 | class EvaluationProducer 6 | extend Forwardable 7 | def_delegators :@evaluation, 8 | :record_latency, 9 | :record_exception 10 | 11 | def initialize(config) 12 | @evaluation = case config.telemetry_adapter.class.to_s 13 | when 'SplitIoClient::Cache::Adapters::RedisAdapter' 14 | SplitIoClient::Telemetry::RedisEvaluationProducer.new(config) 15 | else 16 | SplitIoClient::Telemetry::MemoryEvaluationProducer.new(config) 17 | end 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/splitclient-rb/telemetry/init_consumer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SplitIoClient 4 | module Telemetry 5 | class InitConsumer 6 | extend Forwardable 7 | def_delegators :@init, :non_ready_usages, :bur_timeouts 8 | 9 | def initialize(config) 10 | @init = SplitIoClient::Telemetry::MemoryInitConsumer.new(config) 11 | end 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/splitclient-rb/telemetry/init_producer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SplitIoClient 4 | module Telemetry 5 | class InitProducer 6 | extend Forwardable 7 | def_delegators :@init, :record_config, :record_non_ready_usages, :record_bur_timeout 8 | 9 | def initialize(config) 10 | @init = case config.telemetry_adapter.class.to_s 11 | when 'SplitIoClient::Cache::Adapters::RedisAdapter' 12 | SplitIoClient::Telemetry::RedisInitProducer.new(config) 13 | else 14 | SplitIoClient::Telemetry::MemoryInitProducer.new(config) 15 | end 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/splitclient-rb/telemetry/memory/memory_evaluation_consumer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SplitIoClient 4 | module Telemetry 5 | class MemoryEvaluationConsumer 6 | def initialize(config) 7 | @config = config 8 | @adapter = config.telemetry_adapter 9 | end 10 | 11 | def pop_latencies 12 | to_return = @adapter.latencies.each_with_object({}) do |latency, memo| 13 | memo[latency[:method]] = latency[:latencies] 14 | end 15 | 16 | @adapter.init_latencies 17 | 18 | to_return 19 | end 20 | 21 | def pop_exceptions 22 | to_return = @adapter.exceptions.each_with_object({}) do |exception, memo| 23 | memo[exception[:method]] = exception[:exceptions].value 24 | end 25 | 26 | @adapter.init_exceptions 27 | 28 | to_return 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/splitclient-rb/telemetry/memory/memory_evaluation_producer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SplitIoClient 4 | module Telemetry 5 | class MemoryEvaluationProducer 6 | def initialize(config) 7 | @config = config 8 | @adapter = config.telemetry_adapter 9 | end 10 | 11 | def record_latency(method, bucket) 12 | @adapter.latencies.find { |l| l[:method] == method }[:latencies][bucket] += 1 13 | rescue StandardError => e 14 | @config.log_found_exception(__method__.to_s, e) 15 | end 16 | 17 | def record_exception(method) 18 | @adapter.exceptions.find { |l| l[:method] == method }[:exceptions].increment 19 | rescue StandardError => e 20 | @config.log_found_exception(__method__.to_s, e) 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/splitclient-rb/telemetry/memory/memory_init_consumer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SplitIoClient 4 | module Telemetry 5 | class MemoryInitConsumer 6 | DEFAULT_VALUE = 0 7 | 8 | def initialize(config) 9 | @config = config 10 | @adapter = config.telemetry_adapter 11 | end 12 | 13 | def non_ready_usages 14 | find_counts(Domain::Constants::NON_READY_USAGES) 15 | end 16 | 17 | def bur_timeouts 18 | find_counts(Domain::Constants::BUR_TIMEOUT) 19 | end 20 | 21 | private 22 | 23 | def find_counts(action) 24 | @adapter.factory_counters.find { |l| l[:action] == action }[:counts].value 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/splitclient-rb/telemetry/memory/memory_init_producer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SplitIoClient 4 | module Telemetry 5 | class MemoryInitProducer 6 | def initialize(config) 7 | @config = config 8 | @adapter = config.telemetry_adapter 9 | end 10 | 11 | def record_config 12 | # no op 13 | end 14 | 15 | def record_bur_timeout 16 | find_factory_counters(Domain::Constants::BUR_TIMEOUT)[:counts].increment 17 | rescue StandardError => e 18 | @config.log_found_exception(__method__.to_s, e) 19 | end 20 | 21 | def record_non_ready_usages 22 | find_factory_counters(Domain::Constants::NON_READY_USAGES)[:counts].increment 23 | rescue StandardError => e 24 | @config.log_found_exception(__method__.to_s, e) 25 | end 26 | 27 | private 28 | 29 | def find_factory_counters(action) 30 | @adapter.factory_counters.find { |l| l[:action] == action } 31 | end 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/splitclient-rb/telemetry/redis/redis_evaluation_producer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SplitIoClient 4 | module Telemetry 5 | class RedisEvaluationProducer 6 | def initialize(config) 7 | @config = config 8 | @adapter = config.telemetry_adapter 9 | 10 | @sdk_version = "#{@config.language}-#{@config.version}" 11 | @name = @config.machine_name 12 | @ip = @config.machine_ip 13 | end 14 | 15 | def record_latency(method, bucket) 16 | @adapter.hincrby(latency_key, "#{@sdk_version}/#{@name}/#{@ip}/#{method}/#{bucket}", 1) 17 | rescue StandardError => e 18 | @config.log_found_exception(__method__.to_s, e) 19 | end 20 | 21 | def record_exception(method) 22 | @adapter.hincrby(exception_key, "#{@sdk_version}/#{@name}/#{@ip}/#{method}", 1) 23 | rescue StandardError => e 24 | @config.log_found_exception(__method__.to_s, e) 25 | end 26 | 27 | private 28 | 29 | def latency_key 30 | "#{@config.redis_namespace}.telemetry.latencies" 31 | end 32 | 33 | def exception_key 34 | "#{@config.redis_namespace}.telemetry.exceptions" 35 | end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/splitclient-rb/telemetry/redis/redis_init_producer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SplitIoClient 4 | module Telemetry 5 | class RedisInitProducer 6 | def initialize(config) 7 | @config = config 8 | @adapter = config.telemetry_adapter 9 | end 10 | 11 | def record_config(config_data) 12 | return if config_data.nil? 13 | 14 | data = { t: { oM: config_data.om, st: config_data.st, aF: config_data.af, rF: config_data.rf, t: config_data.t } } 15 | field = "#{@config.language}-#{@config.version}/#{@config.machine_name}/#{@config.machine_ip}" 16 | 17 | @adapter.add_to_map(config_key, field, data.to_json) 18 | rescue StandardError => e 19 | @config.log_found_exception(__method__.to_s, e) 20 | end 21 | 22 | def record_bur_timeout 23 | # no-op 24 | end 25 | 26 | def record_non_ready_usages 27 | # no-op 28 | end 29 | 30 | private 31 | 32 | def config_key 33 | "#{@config.redis_namespace}.telemetry.init" 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/splitclient-rb/telemetry/redis/redis_synchronizer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SplitIoClient 4 | module Telemetry 5 | class RedisSynchronizer 6 | def initialize(config, 7 | telemetry_init_producer) 8 | @config = config 9 | @telemetry_init_producer = telemetry_init_producer 10 | end 11 | 12 | def synchronize_stats 13 | # No-op 14 | end 15 | 16 | def synchronize_config(active_factories = nil, redundant_active_factories = nil, tags = nil) 17 | active_factories ||= SplitIoClient.split_factory_registry.active_factories 18 | redundant_active_factories ||= SplitIoClient.split_factory_registry.redundant_active_factories 19 | 20 | init_config = ConfigInit.new(@config.mode, 'redis', active_factories, redundant_active_factories, tags) 21 | @telemetry_init_producer.record_config(init_config) 22 | rescue StandardError => e 23 | @config.log_found_exception(__method__.to_s, e) 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/splitclient-rb/telemetry/runtime_consumer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SplitIoClient 4 | module Telemetry 5 | class RuntimeConsumer 6 | extend Forwardable 7 | def_delegators :@runtime, 8 | :pop_tags, 9 | :impressions_stats, 10 | :events_stats, 11 | :last_synchronizations, 12 | :pop_http_errors, 13 | :pop_http_latencies, 14 | :pop_auth_rejections, 15 | :pop_token_refreshes, 16 | :pop_streaming_events, 17 | :session_length, 18 | :pop_updates_from_sse 19 | 20 | def initialize(config) 21 | @runtime = SplitIoClient::Telemetry::MemoryRuntimeConsumer.new(config) 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/splitclient-rb/telemetry/runtime_producer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SplitIoClient 4 | module Telemetry 5 | class RuntimeProducer 6 | extend Forwardable 7 | def_delegators :@runtime, 8 | :add_tag, 9 | :record_impressions_stats, 10 | :record_events_stats, 11 | :record_successful_sync, 12 | :record_sync_error, 13 | :record_sync_latency, 14 | :record_auth_rejections, 15 | :record_token_refreshes, 16 | :record_streaming_event, 17 | :record_session_length, 18 | :record_updates_from_sse 19 | 20 | def initialize(config) 21 | @runtime = SplitIoClient::Telemetry::MemoryRuntimeProducer.new(config) 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/splitclient-rb/telemetry/sync_task.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SplitIoClient 4 | module Telemetry 5 | class SyncTask 6 | def initialize(config, telemetry_synchronizer) 7 | @config = config 8 | @telemetry_synchronizer = telemetry_synchronizer 9 | end 10 | 11 | def call 12 | stats_thread 13 | end 14 | 15 | private 16 | 17 | def stats_thread 18 | @config.threads[:telemetry_stats_sender] = Thread.new { telemetry_sync_task } 19 | end 20 | 21 | def telemetry_sync_task 22 | @config.logger.info('Starting Telemetry Sync Task') 23 | 24 | loop do 25 | sleep(@config.telemetry_refresh_rate) 26 | 27 | @telemetry_synchronizer.synchronize_stats 28 | end 29 | rescue SplitIoClient::SDKShutdownException 30 | @telemetry_synchronizer.synchronize_stats 31 | 32 | @config.logger.info('Posting Telemetry due to shutdown') 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/splitclient-rb/telemetry/synchronizer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SplitIoClient 4 | module Telemetry 5 | class Synchronizer 6 | extend Forwardable 7 | def_delegators :@synchronizer, 8 | :synchronize_config, 9 | :synchronize_stats 10 | 11 | def initialize(config, 12 | telemtry_consumers, 13 | telemetry_init_producer, 14 | repositories, 15 | telemetry_api, 16 | flag_sets, 17 | flag_sets_invalid) 18 | @synchronizer = case config.telemetry_adapter.class.to_s 19 | when 'SplitIoClient::Cache::Adapters::RedisAdapter' 20 | SplitIoClient::Telemetry::RedisSynchronizer.new(config, 21 | telemetry_init_producer) 22 | else 23 | SplitIoClient::Telemetry::MemorySynchronizer.new(config, 24 | telemtry_consumers, 25 | repositories, 26 | telemetry_api, 27 | flag_sets, 28 | flag_sets_invalid) 29 | end 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/splitclient-rb/utilitites.rb: -------------------------------------------------------------------------------- 1 | module SplitIoClient 2 | module Utilities 3 | extend self 4 | 5 | # Convert String with Time info to its epoch FixNum previously setting to zero the seconds 6 | def to_epoch(value) 7 | parsed = Time.parse(value) 8 | zeroed = Time.new(parsed.year, parsed.month, parsed.day, parsed.hour, parsed.min, 0, 0) 9 | 10 | zeroed.to_i 11 | end 12 | 13 | def to_epoch_milis(value) 14 | to_epoch(value) * 1000 15 | end 16 | 17 | def to_milis_zero_out_from_seconds(value) 18 | parsed_value = Time.strptime(value.to_s, '%s').utc 19 | zeroed = Time.new(parsed_value.year, parsed_value.month, parsed_value.day, parsed_value.hour, parsed_value.min, 0, 0) 20 | 21 | zeroed.to_i * 1000 22 | rescue StandardError 23 | return :non_valid_date_info 24 | end 25 | 26 | def to_milis_zero_out_from_hour(value) 27 | parsed_value = Time.strptime(value.to_s, '%s').utc 28 | zeroed = Time.new(parsed_value.year, parsed_value.month, parsed_value.day, 0, 0, 0, 0) 29 | 30 | zeroed.to_i * 1000 31 | rescue StandardError 32 | return :non_valid_date_info 33 | end 34 | 35 | def randomize_interval(interval) 36 | random_factor = Random.new.rand(50..100) / 100.0 37 | 38 | interval * random_factor 39 | end 40 | 41 | def split_bulk_to_send(hash, divisions) 42 | count = 0 43 | 44 | hash.each_with_object([]) do |key_value, final| 45 | final[count % divisions] ||= {} 46 | final[count % divisions][key_value[0]] = key_value[1] 47 | count += 1 48 | end 49 | rescue StandardError 50 | [] 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /lib/splitclient-rb/version.rb: -------------------------------------------------------------------------------- 1 | module SplitIoClient 2 | VERSION = '8.5.0' 3 | end 4 | -------------------------------------------------------------------------------- /sonar-project.properties: -------------------------------------------------------------------------------- 1 | sonar.projectKey=ruby-client 2 | sonar.projectKey=ruby-client 3 | sonar.sources=lib 4 | sonar.tests=spec 5 | sonar.ruby.coverage.reportPaths=coverage/.resultset.sonarqube.json 6 | -------------------------------------------------------------------------------- /spec/allocations/cache/adapters/memory/map_adapter_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe SplitIoClient::Cache::Adapters::MemoryAdapters::MapAdapter do 6 | let(:mri_allocations) { 2 } 7 | let(:adapter) { described_class.new } 8 | let(:key) { 'foo' } 9 | let(:key2) { 'bar' } 10 | 11 | before(:each) do 12 | adapter 13 | end 14 | 15 | it 'initializes MapAdapter' do 16 | expect { adapter }.to allocate_max(mri_allocations + 1).objects 17 | end 18 | 19 | it 'initializes map' do 20 | expect { adapter.initialize_map(key) }.to allocate_max(mri_allocations + 1).objects 21 | end 22 | 23 | it 'adds to map' do 24 | adapter.initialize_map(key) 25 | 26 | expect { adapter.add_to_map(key, key, key) }.to allocate_max(mri_allocations).objects 27 | end 28 | 29 | it 'finds in map' do 30 | expect { adapter.find_in_map(key, key) }.to allocate_max(mri_allocations).objects 31 | end 32 | 33 | it 'deletes from map' do 34 | adapter.initialize_map(key) 35 | adapter.add_to_map(key, key, key) 36 | 37 | expect { adapter.delete_from_map(key, key) }.to allocate_max(mri_allocations).objects 38 | end 39 | 40 | it 'checks whether key is in map' do 41 | adapter.initialize_map(key) 42 | adapter.add_to_map(key, key, key) 43 | 44 | expect { adapter.in_map?(key, key) }.to allocate_max(mri_allocations).objects 45 | end 46 | 47 | it 'sets string' do 48 | expect { adapter.set_string(key, key) }.to allocate_max(mri_allocations).objects 49 | end 50 | 51 | it 'unions sets' do 52 | adapter.initialize_set(key) 53 | adapter.add_to_set(key, [key, key]) 54 | adapter.add_to_set(key2, [key2, key2]) 55 | 56 | expect { adapter.union_sets([key, key2]) }.to allocate_max(mri_allocations + 9).objects 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /spec/allocations/cache/repositories/impressions/memory_repository_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe SplitIoClient::Cache::Repositories::Impressions::MemoryRepository do 6 | let(:config) { SplitIoClient::SplitConfig.new(impressions_queue_size: 5) } 7 | let(:adapter) { SplitIoClient::Cache::Adapters::MemoryAdapter.new(MemoryAdapters::QueueAdapter.new(3)) } 8 | let(:repository) { described_class.new(adapter, config) } 9 | let(:key) { 'foo' } 10 | let(:data) { { foo: 'bar' }.freeze } 11 | 12 | xit 'adds impression' do 13 | repository 14 | key 15 | data 16 | 17 | expect { repository.add(key, data) }.to allocate_max(3).objects 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/cache/adapters/memory/queue_adapter_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe SplitIoClient::Cache::Adapters::MemoryAdapters::QueueAdapter do 6 | let(:queue_size) { 3 } 7 | let(:adapter) { described_class.new(queue_size) } 8 | 9 | before do 10 | queue_size.times { adapter.add_to_queue('foo') } 11 | end 12 | 13 | it 'throws exception if queue size is reached' do 14 | expect { adapter.add_to_queue('foo') }.to raise_error(ThreadError) 15 | end 16 | 17 | it 'sets correct current_size after clear' do 18 | adapter.clear 19 | 20 | expect(adapter.instance_variable_get(:@current_size).value).to eq(0) 21 | end 22 | 23 | it 'returns correct queue size' do 24 | expect(adapter.instance_variable_get(:@current_size).value).to eq(queue_size) 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /spec/cache/repositories/segments_repository_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe SplitIoClient::Cache::Repositories::SegmentsRepository do 6 | context 'memory adapter' do 7 | let(:repository) { described_class.new(@default_config) } 8 | let(:flag_sets_repository) {SplitIoClient::Cache::Repositories::MemoryFlagSetsRepository.new([]) } 9 | let(:flag_set_filter) {SplitIoClient::Cache::Filter::FlagSetsFilter.new([]) } 10 | let(:split_repository) { SplitIoClient::Cache::Repositories::SplitsRepository.new(@default_config, flag_sets_repository, flag_set_filter) } 11 | 12 | it 'removes keys' do 13 | repository.add_to_segment(name: 'foo', added: [1, 2, 3], removed: []) 14 | expect(repository.get_segment_keys('foo')).to eq([1, 2, 3]) 15 | 16 | repository.send(:remove_keys, 'foo', [1, 2, 3]) 17 | expect(repository.get_segment_keys('foo')).to eq([]) 18 | end 19 | 20 | it 'segment names and keys count' do 21 | repository.add_to_segment(name: 'foo-1', added: [1, 2, 3], removed: []) 22 | repository.add_to_segment(name: 'foo-2', added: [1, 2, 3, 4], removed: []) 23 | repository.add_to_segment(name: 'foo-3', added: [], removed: []) 24 | 25 | split_repository.set_segment_names(['foo-1', 'foo-2', 'foo-3']) 26 | 27 | expect(repository.segments_count).to be(3) 28 | expect(repository.segment_keys_count).to be(7) 29 | end 30 | end 31 | 32 | context 'redis adapter' do 33 | let(:repository) { described_class.new(SplitIoClient::SplitConfig.new(cache_adapter: :redis)) } 34 | 35 | it 'removes keys' do 36 | repository.add_to_segment(name: 'foo', added: [1, 2, 3], removed: []) 37 | expect(repository.get_segment_keys('foo')).to eq(%w[1 2 3]) 38 | 39 | repository.send(:remove_keys, 'foo', %w[1 2 3]) 40 | expect(repository.get_segment_keys('foo')).to eq([]) 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /spec/cache/stores/localhost_split_store_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe SplitIoClient::Cache::Stores::LocalhostSplitStore do 6 | let(:log) { StringIO.new } 7 | let(:config) { SplitIoClient::SplitConfig.new(logger: Logger.new(log)) } 8 | let(:flag_sets_repository) {SplitIoClient::Cache::Repositories::MemoryFlagSetsRepository.new([]) } 9 | let(:flag_set_filter) {SplitIoClient::Cache::Filter::FlagSetsFilter.new([]) } 10 | let(:splits_repository) { SplitIoClient::Cache::Repositories::SplitsRepository.new(config, flag_sets_repository, flag_set_filter) } 11 | 12 | let(:split_file) do 13 | ['local_feature local_treatment'] 14 | end 15 | 16 | before { allow(File).to receive(:open).and_return(split_file) } 17 | 18 | context '#initialize' do 19 | it 'logs warning when using old split file format' do 20 | described_class.new(splits_repository, config).call 21 | 22 | expect(log.string).to include 'Localhost mode: .split mocks ' \ 23 | 'will be deprecated soon in favor of YAML files, which provide more ' \ 24 | 'targeting power. Take a look in our documentation.' 25 | end 26 | end 27 | 28 | context '#store_splits' do 29 | let(:split_file2) do 30 | ['local_feature local_treatment', 'local_feature2 local_treatment2'] 31 | end 32 | 33 | let(:store) { described_class.new(splits_repository, config) } 34 | 35 | it 'fetch data in the cache' do 36 | store.send(:store_splits) 37 | 38 | expect(store.splits_repository.splits.size).to eq(1) 39 | end 40 | 41 | it 'refreshes splits' do 42 | store.send(:store_splits) 43 | 44 | expect(store.splits_repository.splits.size).to eq(1) 45 | expect(store.splits_repository.splits['local_feature2']).to be_nil 46 | 47 | allow(File).to receive(:open).and_return(split_file2) 48 | 49 | store.send(:store_splits) 50 | 51 | expect(store.splits_repository.splits.size).to eq(2) 52 | expect(store.splits_repository.splits['local_feature2']).not_to be_nil 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /spec/engine/api/client_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe SplitIoClient::Api::Client do 6 | describe '#get_api' do 7 | it 'makes GET request without error' do 8 | url = 'https://example.org?hello=world' 9 | api_key = 'abc-def-ghi' 10 | params = { hello: :world } 11 | 12 | stub_request(:get, url).to_return(status: 200) 13 | 14 | expect { described_class.new(@default_config).get_api(url, api_key, params) }.not_to raise_error 15 | end 16 | 17 | it 'makes POST request without error' do 18 | url = 'https://example.org' 19 | api_key = 'abc-def-ghi' 20 | data = { hello: :world } 21 | 22 | stub_request(:post, url).to_return(status: 200) 23 | 24 | expect { described_class.new(@default_config).post_api(url, api_key, data) }.not_to raise_error 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /spec/engine/api/telemetry_api_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe SplitIoClient::Api::TelemetryApi do 6 | let(:log) { StringIO.new } 7 | let(:config) { SplitIoClient::SplitConfig.new(logger: Logger.new(log), debug_enabled: true, transport_debug_enabled: true) } 8 | let(:telemetry_runtime_producer) { SplitIoClient::Telemetry::RuntimeProducer.new(config) } 9 | let(:telemetry_api) { described_class.new(config, 'api-key-test', telemetry_runtime_producer) } 10 | 11 | before do 12 | stub_request(:post, 'https://telemetry.split.io/api/v1/metrics/usage') 13 | .to_return(status: 200, body: 'ok') 14 | end 15 | 16 | it 'returns splits with segment names' do 17 | usage = SplitIoClient::Telemetry::Usage.new 18 | telemetry_api.record_stats(usage) 19 | 20 | expect(a_request(:post, 'https://telemetry.split.io/api/v1/metrics/usage')).to have_been_made 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /spec/engine/auth_api_client_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe SplitIoClient::Engine::AuthApiClient do 6 | subject { SplitIoClient::Engine::AuthApiClient } 7 | 8 | let(:body_response) do 9 | File.read(File.join(SplitIoClient.root, 'spec/test_data/integrations/auth_body_response.json')) 10 | end 11 | 12 | let(:api_key) { 'AuthApiClient-key' } 13 | let(:log) { StringIO.new } 14 | let(:config) { SplitIoClient::SplitConfig.new(logger: Logger.new(log)) } 15 | let(:telemetry_runtime_producer) { SplitIoClient::Telemetry::RuntimeProducer.new(config) } 16 | 17 | it 'authenticate success' do 18 | stub_request(:get, config.auth_service_url + "?s=1.1").to_return(status: 200, body: body_response) 19 | 20 | auth_api_client = subject.new(config, telemetry_runtime_producer) 21 | response = auth_api_client.authenticate(api_key) 22 | 23 | expect(response[:push_enabled]).to eq(true) 24 | expect(response[:channels]).to eq('xxxx_xxxx_segments%2Cxxxx_xxxx_splits%2C%5B%3Foccupancy%3Dmetrics.publishers%5Dcontrol_pri%2C%5B%3Foccupancy%3Dmetrics.publishers%5Dcontrol_sec') 25 | expect(response[:retry]).to eq(true) 26 | end 27 | 28 | it 'auth server return 500' do 29 | stub_request(:get, config.auth_service_url + "?s=1.1").to_return(status: 500) 30 | 31 | auth_api_client = subject.new(config, telemetry_runtime_producer) 32 | response = auth_api_client.authenticate(api_key) 33 | 34 | expect(response[:push_enabled]).to eq(false) 35 | expect(response[:retry]).to eq(true) 36 | end 37 | 38 | it 'auth server return 401' do 39 | stub_request(:get, config.auth_service_url + "?s=1.1").to_return(status: 401) 40 | 41 | auth_api_client = subject.new(config, telemetry_runtime_producer) 42 | response = auth_api_client.authenticate(api_key) 43 | 44 | expect(response[:push_enabled]).to eq(false) 45 | expect(response[:retry]).to eq(false) 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /spec/engine/evaluator/splitter_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe SplitIoClient::Splitter do 6 | RSpec.shared_examples 'Splitter' do |file, algorithm| 7 | it 'returns expected hash and bucket' do 8 | File.foreach(file) do |row| 9 | seed, key, hash, bucket = row.split(',') 10 | 11 | expect(described_class.new.count_hash(key, seed.to_i, algorithm)).to eq(hash.to_i) 12 | expect(described_class.new.bucket(hash.to_i)).to eq(bucket.to_i) 13 | end 14 | end 15 | end 16 | 17 | describe 'using mumur3 algorithm' do 18 | it_behaves_like( 19 | 'Splitter', 20 | File.expand_path(File.join(File.dirname(__FILE__), '../../test_data/hash/murmur3-sample-data-v2.csv')), 21 | false 22 | ) 23 | end 24 | 25 | describe 'using mumur3 algorithm with non-alphanumeric sample data' do 26 | it_behaves_like( 27 | 'Splitter', 28 | File.expand_path(File.join(File.dirname(__FILE__), 29 | '../../test_data/hash/murmur3-sample-data-non-alpha-numeric-v2.csv')), 30 | false 31 | ) 32 | end 33 | 34 | describe 'using legacy algorithm' do 35 | it_behaves_like( 36 | 'Splitter', 37 | File.expand_path(File.join(File.dirname(__FILE__), '../../test_data/hash/sample-data.csv')), 38 | true 39 | ) 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /spec/engine/matchers/all_keys_matcher_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe SplitIoClient::AllKeysMatcher do 6 | context '#to_s' do 7 | it 'it returns in segment all' do 8 | expect(described_class.new(@split_logger).to_s).to be 'in segment all' 9 | end 10 | end 11 | 12 | context '#string_type' do 13 | it 'is not string type matcher' do 14 | expect(described_class.new(@split_logger).string_type?).to be false 15 | end 16 | end 17 | 18 | context '#equals?' do 19 | let(:matcher) { described_class.new(@split_logger) } 20 | it 'is equal' do 21 | expect(matcher.equals?(matcher)).to be true 22 | end 23 | it 'is not equal because the object is nil' do 24 | expect(matcher.equals?(nil)).to be false 25 | end 26 | it 'is not equal because other type' do 27 | expect(matcher.equals?('string')).to be false 28 | end 29 | it 'is equal because is other instance but always return true' do 30 | expect(matcher.equals?(described_class.new(@split_logger))).to be true 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /spec/engine/matchers/combining_matcher_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe SplitIoClient::CombiningMatcher do 6 | subject do 7 | SplitIoClient::SplitFactory.new('test_api_key', {logger: Logger.new('/dev/null'), streaming_enabled: false, impressions_refresh_rate: 9999, impressions_mode: :none, features_refresh_rate: 9999, telemetry_refresh_rate: 99999}).client 8 | end 9 | 10 | let(:splits_json) do 11 | File.read(File.expand_path(File.join(File.dirname(__FILE__), 12 | '../../test_data/splits/combining_matcher_splits.json'))) 13 | end 14 | let(:segments_json) do 15 | File.read(File.expand_path(File.join(File.dirname(__FILE__), 16 | '../../test_data/segments/combining_matcher_segments.json'))) 17 | end 18 | 19 | before do 20 | stub_request(:get, 'https://sdk.split.io/api/segmentChanges/employees?since=-1') 21 | .to_return(status: 200, body: segments_json) 22 | stub_request(:get, /https:\/\/sdk\.split\.io\/api\/splitChanges\?s=1\.1&since/) 23 | .to_return(status: 200, body: splits_json) 24 | stub_request(:any, /https:\/\/telemetry.*/) 25 | .to_return(status: 200, body: 'ok') 26 | stub_request(:any, /https:\/\/events.*/) 27 | .to_return(status: 200, body: 'ok') 28 | sleep 1 29 | end 30 | 31 | describe 'anding' do 32 | it 'matches' do 33 | subject.block_until_ready 34 | 35 | expect(subject.get_treatment( 36 | 'user_for_testing_do_no_erase', 37 | 'PASSENGER_anding', 38 | 'join' => 1_461_283_200, 39 | 'custom_attribute' => 'usa' 40 | )).to eq('V-YZKS') 41 | sleep 1 42 | subject.destroy() 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /spec/engine/matchers/contains_all_matcher_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe SplitIoClient::ContainsAllMatcher do 6 | let(:local_array) { %w[a b c] } 7 | 8 | it 'matches' do 9 | expect(described_class.new('attr', %w[b], @split_logger).match?(attributes: { attr: local_array })) 10 | .to be(true) 11 | expect(described_class.new('attr', %w[b c], @split_logger).match?(attributes: { attr: local_array })) 12 | .to be(true) 13 | expect(described_class.new('attr', %w[a c], @split_logger).match?(attributes: { attr: local_array })) 14 | .to be(true) 15 | expect(described_class.new('attr', %w[a b c], @split_logger).match?(attributes: { attr: local_array })) 16 | .to be(true) 17 | end 18 | 19 | it 'does not match' do 20 | expect(described_class.new('attr', %w[a b c d], @split_logger).match?(attributes: { attr: local_array })) 21 | .to be(false) 22 | expect(described_class.new('attr', %w[a b d], @split_logger).match?(attributes: { attr: local_array })) 23 | .to be(false) 24 | expect(described_class.new('attr', %w[d], @split_logger).match?(attributes: { attr: local_array })) 25 | .to be(false) 26 | expect(described_class.new('attr', %w[], @split_logger).match?(attributes: { attr: local_array })) 27 | .to be(false) 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /spec/engine/matchers/contains_any_matcher_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe SplitIoClient::ContainsAnyMatcher do 6 | let(:remote_array) { %w[a b c] } 7 | 8 | it 'matches' do 9 | expect(described_class.new('attr', remote_array, @split_logger).match?(attributes: { attr: %w[a] })) 10 | .to be(true) 11 | expect(described_class.new('attr', remote_array, @split_logger).match?(attributes: { attr: %w[a d c] })) 12 | .to be(true) 13 | expect(described_class.new('attr', remote_array, @split_logger).match?(attributes: { attr: %w[c d e] })) 14 | .to be(true) 15 | end 16 | 17 | it 'does not match' do 18 | expect(described_class.new('attr', %w[], @split_logger).match?(attributes: { attr: %w[a b c] })) 19 | .to be(false) 20 | expect(described_class.new('attr', remote_array, @split_logger).match?(attributes: { attr: %w[] })) 21 | .to be(false) 22 | expect(described_class.new('attr', remote_array, @split_logger).match?(attributes: { attr: %w[d] })) 23 | .to be(false) 24 | expect(described_class.new('attr', remote_array, @split_logger).match?(attributes: { attr: %w[d e f] })) 25 | .to be(false) 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /spec/engine/matchers/depencdency_matcher_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe SplitIoClient::DependencyMatcher do 6 | let(:evaluator) { double } 7 | 8 | it 'matches' do 9 | allow(evaluator).to receive(:evaluate_feature_flag).with({ matching_key: 'foo', bucketing_key: 'bar' }, 'foo', nil) 10 | .and_return(treatment: 'yes') 11 | 12 | expect(described_class.new('foo', %w[on yes true], @split_logger) 13 | .match?(matching_key: 'foo', bucketing_key: 'bar', evaluator: evaluator)).to eq(true) 14 | end 15 | 16 | it 'does not match' do 17 | allow(evaluator).to receive(:evaluate_feature_flag).with({ matching_key: 'foo', bucketing_key: 'bar' }, 'foo', nil) 18 | .and_return(treatment: 'no') 19 | 20 | expect(described_class.new('foo', %w[on yes true], @split_logger) 21 | .match?(matching_key: 'foo', bucketing_key: 'bar', evaluator: evaluator)).to eq(false) 22 | end 23 | 24 | it 'is not string type matcher' do 25 | expect(described_class.new('foo', %w[on yes true], @split_logger).string_type?).to be false 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /spec/engine/matchers/equal_to_boolean_matcher_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe SplitIoClient::EqualToBooleanMatcher do 6 | it 'matches' do 7 | expect(described_class.new('value', true, @split_logger).match?(attributes: { value: true })).to eq(true) 8 | expect(described_class.new('value', true, @split_logger).match?(attributes: { value: 'true' })).to eq(true) 9 | expect(described_class.new('value', true, @split_logger).match?(attributes: { value: 'tRue' })).to eq(true) 10 | expect(described_class.new('value', false, @split_logger).match?(attributes: { value: false })).to eq(true) 11 | end 12 | 13 | it 'does not match' do 14 | expect(described_class.new('value', true, @split_logger).match?(attributes: { value: false })).to eq(false) 15 | expect(described_class.new('value', true, @split_logger).match?(attributes: { value: 'false' })).to eq(false) 16 | expect(described_class.new('value', true, @split_logger).match?(attributes: { value: 'something' })).to eq(false) 17 | expect(described_class.new('value', false, @split_logger).match?(attributes: { value: true })).to eq(false) 18 | expect(described_class.new('value', false, @split_logger).match?(attributes: { value: '' })).to eq(false) 19 | expect(described_class.new('value', false, @split_logger).match?(attributes: { value: {} })).to eq(false) 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec/engine/matchers/equal_to_set_matcher_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe SplitIoClient::EqualToSetMatcher do 6 | let(:remote_array) { %w[a b c] } 7 | 8 | it 'matches' do 9 | # Works both with symbol and string key 10 | expect(described_class.new('attr', remote_array, @split_logger).match?(attributes: { attr: %w[a b c] })) 11 | .to be(true) 12 | expect(described_class.new('attr', remote_array, @split_logger).match?(attributes: { 'attr' => %w[a b c] })) 13 | .to be(true) 14 | expect(described_class.new(:attr, remote_array, @split_logger).match?(attributes: { attr: %w[a b c] })) 15 | .to be(true) 16 | expect(described_class.new(:attr, remote_array, @split_logger).match?(attributes: { 'attr' => %w[a b c] })) 17 | .to be(true) 18 | end 19 | 20 | it 'does not match' do 21 | expect(described_class.new('attr', remote_array, @split_logger).match?(attributes: { attr: %w[a b c d] })) 22 | .to be(false) 23 | expect(described_class.new('attr', remote_array, @split_logger).match?(attributes: { attr: %w[d] })) 24 | .to be(false) 25 | expect(described_class.new('attr', remote_array, @split_logger).match?(attributes: { attr: %w[d e f] })) 26 | .to be(false) 27 | expect(described_class.new('attr', remote_array, @split_logger).match?(attributes: { attr: %w[] })) 28 | .to be(false) 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /spec/engine/matchers/matcher_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe SplitIoClient::Matcher do 6 | context '#equals?' do 7 | let(:matcher) { described_class.new(@split_logger) } 8 | it 'is equal' do 9 | expect(matcher.equals?(matcher)).to be true 10 | end 11 | it 'is not equal because the object is nil' do 12 | expect(described_class.new(@split_logger).equals?(nil)).to be false 13 | end 14 | it 'is not equal because other type' do 15 | expect(described_class.new(@split_logger).equals?('string')).to be false 16 | end 17 | it 'is not equal because is other instance' do 18 | expect(described_class.new(@split_logger).equals?(described_class.new(@split_logger))).to be false 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec/engine/matchers/matches_equal_to_semver_matcher_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe SplitIoClient::EqualToSemverMatcher do 6 | let(:raw) { { 7 | 'negate': false, 8 | 'matcherType': 'EQUAL_TO_SEMVER', 9 | 'stringMatcherData': "2.1.8" 10 | } } 11 | let(:config) { SplitIoClient::SplitConfig.new } 12 | 13 | it 'initilized params' do 14 | matcher = described_class.new("version", raw[:stringMatcherData], config.logger, config.split_validator) 15 | expect(matcher.attribute).to eq("version") 16 | semver = matcher.instance_variable_get(:@semver) 17 | expect(semver.instance_variable_get(:@version)).to eq("2.1.8") 18 | end 19 | 20 | it 'matches' do 21 | matcher = described_class.new("version", raw[:stringMatcherData], config.logger, config.split_validator) 22 | expect(matcher.match?(:attributes=>{"version": "2.1.8"})).to eq(true) 23 | end 24 | 25 | it 'does not match' do 26 | matcher = described_class.new("version", raw[:stringMatcherData], config.logger, config.split_validator) 27 | expect(matcher.match?(:attributes=>{"version": "2.1.8+rc"})).to eq(false) 28 | expect(matcher.match?(:attributes=>{"version": "2.1.5"})).to eq(false) 29 | expect(matcher.match?(:attributes=>{"version": "2.1.5-rc1"})).to eq(false) 30 | end 31 | 32 | it 'invalid attribute' do 33 | matcher = described_class.new("version", raw[:stringMatcherData], config.logger, config.split_validator) 34 | expect(matcher.match?(:attributes=>{"version": 2.1})).to eq(false) 35 | expect(matcher.match?(:attributes=>{"version": nil})).to eq(false) 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /spec/engine/matchers/matches_greater_than_or_equal_to_semver_matcher_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe SplitIoClient::GreaterThanOrEqualToSemverMatcher do 6 | let(:raw) { { 7 | 'negate': false, 8 | 'matcherType': 'GREATER_THAN_OR_EQUAL_TO_SEMVER', 9 | 'stringMatcherData': "2.1.8" 10 | } } 11 | let(:config) { SplitIoClient::SplitConfig.new } 12 | 13 | it 'initilized params' do 14 | matcher = described_class.new("version", raw[:stringMatcherData], config.logger, config.split_validator) 15 | expect(matcher.attribute).to eq("version") 16 | semver = matcher.instance_variable_get(:@semver) 17 | expect(semver.instance_variable_get(:@version)).to eq("2.1.8") 18 | end 19 | 20 | it 'matches' do 21 | matcher = described_class.new("version", raw[:stringMatcherData], config.logger, config.split_validator) 22 | expect(matcher.match?(:attributes=>{"version": "2.1.8+rc"})).to eq(true) 23 | expect(matcher.match?(:attributes=>{"version": "2.1.8"})).to eq(true) 24 | expect(matcher.match?(:attributes=>{"version": "2.1.11"})).to eq(true) 25 | expect(matcher.match?(:attributes=>{"version": "2.2.0"})).to eq(true) 26 | end 27 | 28 | it 'does not match' do 29 | matcher = described_class.new("version", raw[:stringMatcherData], config.logger, config.split_validator) 30 | expect(matcher.match?(:attributes=>{"version": "2.1.7"})).to eq(false) 31 | expect(matcher.match?(:attributes=>{"version": "2.0.22"})).to eq(false) 32 | end 33 | 34 | it 'invalid attribute' do 35 | matcher = described_class.new("version", raw[:stringMatcherData], config.logger, config.split_validator) 36 | expect(matcher.match?(:attributes=>{"version": 2.1})).to eq(false) 37 | expect(matcher.match?(:attributes=>{"version": nil})).to eq(false) 38 | end 39 | 40 | end 41 | -------------------------------------------------------------------------------- /spec/engine/matchers/matches_in_list_semver_matcher_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe SplitIoClient::InListSemverMatcher do 6 | let(:raw) { { 7 | 'negate': false, 8 | 'matcherType': 'INLIST_SEMVER', 9 | 'whitelistMatcherData': {"whitelist": ["2.1.8", "2.1.11"]} 10 | } } 11 | let(:config) { SplitIoClient::SplitConfig.new } 12 | 13 | it 'initilized params' do 14 | matcher = described_class.new("version", raw[:whitelistMatcherData][:whitelist], config.logger, config.split_validator) 15 | expect(matcher.attribute).to eq("version") 16 | semver_list = matcher.instance_variable_get(:@semver_list) 17 | expect(semver_list[0].instance_variable_get(:@version)).to eq("2.1.8") 18 | expect(semver_list[1].instance_variable_get(:@version)).to eq("2.1.11") 19 | end 20 | 21 | it 'matches' do 22 | matcher = described_class.new("version", raw[:whitelistMatcherData][:whitelist], config.logger, config.split_validator) 23 | expect(matcher.match?(:attributes=>{"version": "2.1.8"})).to eq(true) 24 | expect(matcher.match?(:attributes=>{"version": "2.1.11"})).to eq(true) 25 | end 26 | 27 | it 'does not match' do 28 | matcher = described_class.new("version", raw[:whitelistMatcherData][:whitelist], config.logger, config.split_validator) 29 | expect(matcher.match?(:attributes=>{"version": "2.1.8+rc"})).to eq(false) 30 | expect(matcher.match?(:attributes=>{"version": "2.1.7"})).to eq(false) 31 | expect(matcher.match?(:attributes=>{"version": "2.1.11-rc12"})).to eq(false) 32 | expect(matcher.match?(:attributes=>{"version": "2.1.8-rc1"})).to eq(false) 33 | end 34 | 35 | it 'invalid attribute' do 36 | matcher = described_class.new("version", raw[:whitelistMatcherData][:whitelist], config.logger, config.split_validator) 37 | expect(matcher.match?(:attributes=>{"version": 2.1})).to eq(false) 38 | expect(matcher.match?(:attributes=>{"version": nil})).to eq(false) 39 | end 40 | 41 | end 42 | -------------------------------------------------------------------------------- /spec/engine/matchers/matches_less_than_or_equal_to_semver_matcher_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe SplitIoClient::LessThanOrEqualToSemverMatcher do 6 | let(:raw) { { 7 | 'negate': false, 8 | 'matcherType': 'LESS_THAN_OR_EQUAL_TO_SEMVER', 9 | 'stringMatcherData': "2.1.8" 10 | } } 11 | let(:config) { SplitIoClient::SplitConfig.new({:logger => Logger.new('/dev/null')}) } 12 | 13 | it 'initilized params' do 14 | matcher = described_class.new("version", raw[:stringMatcherData], config.logger, config.split_validator) 15 | expect(matcher.attribute).to eq("version") 16 | semver = matcher.instance_variable_get(:@semver) 17 | expect(semver.instance_variable_get(:@version)).to eq("2.1.8") 18 | end 19 | 20 | it 'matches' do 21 | matcher = described_class.new("version", raw[:stringMatcherData], config.logger, config.split_validator) 22 | expect(matcher.match?(:attributes=>{"version": "2.1.8+rc"})).to eq(true) 23 | expect(matcher.match?(:attributes=>{"version": "2.1.8"})).to eq(true) 24 | expect(matcher.match?(:attributes=>{"version": "2.1.5"})).to eq(true) 25 | expect(matcher.match?(:attributes=>{"version": "2.1.5-rc1"})).to eq(true) 26 | end 27 | 28 | it 'does not match' do 29 | matcher = described_class.new("version", raw[:stringMatcherData], config.logger, config.split_validator) 30 | expect(matcher.match?(:attributes=>{"version": "2.1.10"})).to eq(false) 31 | expect(matcher.match?(:attributes=>{"version": "2.2.0-rc1"})).to eq(false) 32 | end 33 | 34 | it 'invalid attribute' do 35 | matcher = described_class.new("version", raw[:stringMatcherData], config.logger, config.split_validator) 36 | expect(matcher.match?(:attributes=>{"version": 2.1})).to eq(false) 37 | expect(matcher.match?(:attributes=>{"version": nil})).to eq(false) 38 | end 39 | 40 | end 41 | -------------------------------------------------------------------------------- /spec/engine/matchers/matches_string_matcher_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe SplitIoClient::MatchesStringMatcher do 6 | let(:regexp_file) { File.read(File.expand_path(File.join(File.dirname(__FILE__), '../../test_data/regexp/data.txt'))) } 7 | 8 | it 'matches' do 9 | expect(described_class.new('value', /fo./, @split_logger).match?(attributes: { value: 'foo' })).to eq(true) 10 | expect(described_class.new('value', 'foo', @split_logger).match?(attributes: { value: 'foo' })).to eq(true) 11 | 12 | expect(described_class.new('value', /fo./, @split_logger).match?(value: 'foo')).to eq(true) 13 | expect(described_class.new('value', 'foo', @split_logger).match?(value: 'foo')).to eq(true) 14 | end 15 | 16 | it 'does not match' do 17 | expect(described_class.new('value', /fo./, @split_logger).match?(attributes: { value: 'bar' })).to eq(false) 18 | expect(described_class.new('value', 'foo', @split_logger).match?(attributes: { value: 'bar' })).to eq(false) 19 | 20 | expect(described_class.new('value', /fo./, @split_logger).match?(value: 'bar')).to eq(false) 21 | expect(described_class.new('value', 'foo', @split_logger).match?(value: 'bar')).to eq(false) 22 | end 23 | 24 | it 'matches sample regexps from file' do 25 | regexp_file.split("\n").each do |str| 26 | regexp_str, test_str, result_str = str.split('#') 27 | 28 | expect( 29 | described_class.new('key', Regexp.new(regexp_str), @split_logger).match?(attributes: { key: test_str }) 30 | ).to eq(result_str == 'true') 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /spec/engine/matchers/part_of_set_matcher_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe SplitIoClient::PartOfSetMatcher do 6 | let(:remote_array) { %w[a b c] } 7 | 8 | it 'matches' do 9 | # Works both with symbol and string key 10 | expect(described_class.new('attr', remote_array, @split_logger).match?(attributes: { attr: %w[a b c] })) 11 | .to be(true) 12 | expect(described_class.new('attr', remote_array, @split_logger).match?(attributes: { 'attr' => %w[a b c] })) 13 | .to be(true) 14 | expect(described_class.new(:attr, remote_array, @split_logger).match?(attributes: { attr: %w[a b c] })) 15 | .to be(true) 16 | expect(described_class.new(:attr, remote_array, @split_logger).match?(attributes: { 'attr' => %w[a b c] })) 17 | .to be(true) 18 | 19 | expect(described_class.new('attr', remote_array, @split_logger).match?(attributes: { attr: %w[a b] })) 20 | .to be(true) 21 | expect(described_class.new('attr', remote_array, @split_logger).match?(attributes: { attr: %w[a] })) 22 | .to be(true) 23 | end 24 | 25 | it 'does not match' do 26 | expect(described_class.new('attr', remote_array, @split_logger).match?(attributes: { attr: %w[a b c d] })) 27 | .to be(false) 28 | expect(described_class.new('attr', remote_array, @split_logger).match?(attributes: { attr: %w[d] })) 29 | .to be(false) 30 | expect(described_class.new('attr', remote_array, @split_logger).match?(attributes: { attr: %w[d e f] })) 31 | .to be(false) 32 | expect(described_class.new('attr', remote_array, @split_logger).match?(attributes: { attr: %w[] })) 33 | .to be(false) 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /spec/engine/matchers/user_defined_segment_matcher_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe SplitIoClient::UserDefinedSegmentMatcher do 6 | context '#string_type' do 7 | it 'is not string type matcher' do 8 | expect(described_class.new(nil, nil, @split_logger).string_type?).to be false 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/engine/matchers/whitelist_matcher_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe SplitIoClient::WhitelistMatcher do 6 | subject do 7 | SplitIoClient::SplitFactory.new('test_api_key', {logger: Logger.new('/dev/null'), streaming_enabled: false, impressions_refresh_rate: 9999, impressions_mode: :none, features_refresh_rate: 9999, telemetry_refresh_rate: 99999}).client 8 | end 9 | 10 | let(:splits_json) do 11 | File.read(File.expand_path(File.join(File.dirname(__FILE__), 12 | '../../test_data/splits/whitelist_matcher_splits.json'))) 13 | end 14 | 15 | let(:user) { 'fake_user_id_1' } 16 | let(:feature) { 'test_feature' } 17 | let(:matching_attributes) { { list: 'pro' } } 18 | let(:non_matching_value_attributes) { { list: 'random' } } 19 | let(:missing_key_attributes) { {} } 20 | let(:nil_attributes) { nil } 21 | 22 | before do 23 | stub_request(:any, /https:\/\/telemetry.*/) 24 | .to_return(status: 200, body: 'ok') 25 | stub_request(:get, /https:\/\/sdk\.split\.io\/api\/splitChanges\?s=1\.1&since/) 26 | .to_return(status: 200, body: splits_json) 27 | stub_request(:any, /https:\/\/events.*/) 28 | .to_return(status: 200, body: "", headers: {}) 29 | end 30 | 31 | it 'validates the treatment is ON for correct attribute value' do 32 | subject.block_until_ready 33 | expect(subject.get_treatment(user, feature, matching_attributes)).to eq 'on' 34 | end 35 | 36 | it 'validates the treatment is the default treatment for incorrect attributes hash and nil' do 37 | subject.block_until_ready 38 | expect(subject.get_treatment(user, feature, non_matching_value_attributes)).to eq 'default' 39 | expect(subject.get_treatment(user, feature, missing_key_attributes)).to eq 'default' 40 | expect(subject.get_treatment(user, feature, nil_attributes)).to eq 'default' 41 | sleep 1 42 | subject.destroy() 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /spec/engine/metrics/binary_search_latency_tracker_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe SplitIoClient do 6 | it 'get_bucket' do 7 | result = SplitIoClient::BinarySearchLatencyTracker.get_bucket(1) 8 | expect(result).to be(0) 9 | 10 | result = SplitIoClient::BinarySearchLatencyTracker.get_bucket(1.5) 11 | expect(result).to be(1) 12 | 13 | result = SplitIoClient::BinarySearchLatencyTracker.get_bucket(2) 14 | expect(result).to be(2) 15 | 16 | result = SplitIoClient::BinarySearchLatencyTracker.get_bucket(70) 17 | expect(result).to be(11) 18 | 19 | result = SplitIoClient::BinarySearchLatencyTracker.get_bucket(8000) 20 | expect(result).to be(22) 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /spec/engine/status_manager_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe SplitIoClient::Engine::StatusManager do 6 | subject { SplitIoClient::Engine::StatusManager } 7 | 8 | let(:config) { SplitIoClient::SplitConfig.new(logger: Logger.new(StringIO.new)) } 9 | 10 | it 'check if sdk is ready - should return false' do 11 | status_manager = subject.new(config) 12 | 13 | expect(status_manager.ready?).to eq(false) 14 | end 15 | 16 | it 'check if sdk is ready - should return true' do 17 | status_manager = subject.new(config) 18 | 19 | expect(status_manager.ready?).to eq(false) 20 | 21 | status_manager.ready! 22 | expect(status_manager.ready?).to eq(true) 23 | end 24 | 25 | it 'wait until ready - should return false' do 26 | status_manager = subject.new(config) 27 | 28 | expect { status_manager.wait_until_ready(0.5) }.to raise_error(SplitIoClient::SplitIoError, 'SDK start up timeout expired') 29 | 30 | status_manager.ready! 31 | expect { status_manager.wait_until_ready(0) }.not_to raise_error 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /spec/filter/bloom_filter_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe SplitIoClient::Cache::Filter::BloomFilter do 6 | subject { SplitIoClient::Cache::Filter::BloomFilter } 7 | 8 | it 'validate add, contains and clear of Bloomfilter implementation' do 9 | bf = subject.new(1_000) 10 | 11 | expect(bf.add('feature-1::custom-key-1')).to eq(true) 12 | expect(bf.add('feature-1::custom-key-2')).to eq(true) 13 | expect(bf.add('feature-1::custom-key-3')).to eq(true) 14 | expect(bf.add('feature-1::custom-key-4')).to eq(true) 15 | 16 | expect(bf.contains?('feature-1::custom-key-1')).to eq(true) 17 | expect(bf.contains?('feature-1::custom-key-2')).to eq(true) 18 | expect(bf.contains?('feature-1::custom-key-3')).to eq(true) 19 | expect(bf.contains?('feature-1::custom-key-4')).to eq(true) 20 | expect(bf.contains?('feature-1::custom-key-5')).to eq(false) 21 | 22 | bf.clear 23 | 24 | expect(bf.contains?('feature-1::custom-key-1')).to eq(false) 25 | expect(bf.contains?('feature-1::custom-key-2')).to eq(false) 26 | expect(bf.contains?('feature-1::custom-key-3')).to eq(false) 27 | expect(bf.contains?('feature-1::custom-key-4')).to eq(false) 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /spec/filter/filter_adapter_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | require 'filter_imp_test' 5 | 6 | describe SplitIoClient::Cache::Filter::FilterAdapter do 7 | subject { SplitIoClient::Cache::Filter::FilterAdapter } 8 | 9 | let(:log) { StringIO.new } 10 | let(:config) { SplitIoClient::SplitConfig.new(logger: Logger.new(log)) } 11 | let(:filter) { FilterTest.new } 12 | 13 | it 'with custom filter' do 14 | adapter = subject.new(config, filter) 15 | 16 | adapter.add('feature-1', 'custom-key-1') 17 | adapter.add('feature-1', 'custom-key-2') 18 | adapter.add('feature-1', 'custom-key-3') 19 | adapter.add('feature-1', 'custom-key-4') 20 | 21 | expect(adapter.contains?('feature-1', 'custom-key-1')).to eq(true) 22 | expect(adapter.contains?('feature-1', 'custom-key-2')).to eq(true) 23 | expect(adapter.contains?('feature-1', 'custom-key-3')).to eq(true) 24 | expect(adapter.contains?('feature-1', 'custom-key-4')).to eq(true) 25 | expect(adapter.contains?('feature-1', 'custom-key-5')).to eq(false) 26 | 27 | adapter.clear 28 | 29 | expect(adapter.contains?('feature-1', 'custom-key-1')).to eq(false) 30 | expect(adapter.contains?('feature-1', 'custom-key-2')).to eq(false) 31 | expect(adapter.contains?('feature-1', 'custom-key-3')).to eq(false) 32 | expect(adapter.contains?('feature-1', 'custom-key-4')).to eq(false) 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /spec/filter/flag_set_filter_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require 'spec_helper' 3 | 4 | describe SplitIoClient::Cache::Filter::FlagSetsFilter do 5 | subject { SplitIoClient::Cache::Filter::FlagSetsFilter } 6 | 7 | it 'validate initialize, contains one or multiple sets' do 8 | fs = subject.new(['set_1', 'set_2']) 9 | 10 | expect(fs.flag_set_exist?('set_1')).to eq(true) 11 | expect(fs.flag_set_exist?('set_3')).to eq(false) 12 | expect(fs.flag_set_exist?(1)).to eq(false) 13 | 14 | expect(fs.intersect?(['set_3', 'set_1'])).to eq(true) 15 | expect(fs.intersect?(['set_2', 'set_1'])).to eq(true) 16 | expect(fs.intersect?(['set_3', 'set_4'])).to eq(false) 17 | expect(fs.intersect?('set_1')).to eq(false) 18 | 19 | fs2 = subject.new() 20 | expect(fs2.flag_set_exist?('set_1')).to eq(true) 21 | expect(fs2.intersect?(['set_2', 'set_1'])).to eq(true) 22 | 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /spec/filter_imp_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class FilterTest 4 | attr_reader :values 5 | def initialize 6 | @values = Set.new 7 | end 8 | 9 | def add(key) 10 | @values.add(key) 11 | end 12 | 13 | def contains?(key) 14 | @values.include?(key) 15 | end 16 | 17 | def clear 18 | @values.clear 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /spec/http_server_mock.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'webrick' 4 | require 'webrick/httpproxy' 5 | require 'webrick/https' 6 | 7 | class HTTPServerMock 8 | def initialize 9 | @port = 60_000 10 | begin 11 | @server = create_server(@port) 12 | rescue Errno::EADDRINUSE 13 | @port += 1 14 | retry 15 | end 16 | end 17 | 18 | def create_server(port) 19 | WEBrick::HTTPServer.new( 20 | BindAddress: '127.0.0.1', 21 | Port: port, 22 | Logger: WEBrick::Log.new('/dev/null'), 23 | AccessLog: [] 24 | ) 25 | end 26 | 27 | def start 28 | Thread.new { @server.start } 29 | end 30 | 31 | def stop 32 | @server.shutdown 33 | end 34 | 35 | def base_uri 36 | URI("http://127.0.0.1:#{@port}") 37 | end 38 | 39 | def setup_response(uri_path, &action) 40 | @server.mount_proc(uri_path, action) 41 | end 42 | end 43 | 44 | def mock_server(server = nil) 45 | server = HTTPServerMock.new if server.nil? 46 | begin 47 | server.start 48 | yield server 49 | ensure 50 | server.stop 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /spec/my_impression_listener.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class MyImpressionListener 4 | def initialize 5 | @queue = Queue.new 6 | end 7 | 8 | def log(impression) 9 | @queue.push(impression) 10 | end 11 | 12 | def size 13 | @queue.size 14 | end 15 | 16 | def queue 17 | items = [] 18 | size.times do 19 | items << @queue.pop(true) 20 | end 21 | 22 | items 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /spec/redis_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'redis' 4 | 5 | module RSpec 6 | module RedisHelper 7 | # When this module is included into the rspec config, 8 | # it will set up an around(:each) block to clear redis. 9 | def self.included(rspec) 10 | rspec.around(:each, redis: true) do |example| 11 | with_clean_redis do 12 | example.run 13 | end 14 | end 15 | end 16 | 17 | CONFIG = { url: ENV['REDIS_URL'] || 'redis://127.0.0.1:6379/0' }.freeze 18 | 19 | def redis 20 | @redis ||= ::Redis.connect(CONFIG) 21 | end 22 | 23 | def with_clean_redis 24 | redis.flushall # clean before run 25 | begin 26 | yield 27 | ensure 28 | redis.flushall # clean up after run 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'simplecov' 4 | require 'splitclient-rb' 5 | require 'concurrent' 6 | require 'redis_helper' 7 | require 'timecop' 8 | require 'pry' 9 | 10 | require 'webmock/rspec' 11 | require 'simplecov-json' 12 | SimpleCov.formatter = SimpleCov::Formatter::JSONFormatter 13 | WebMock.disable_net_connect! 14 | 15 | ENV['SPLITCLIENT_ENV'] ||= 'test' 16 | 17 | RSpec.configure do |config| 18 | config.filter_run focus: true 19 | config.run_all_when_everything_filtered = true 20 | config.include RSpec::RedisHelper, redis: true 21 | config.before(:all) do 22 | @default_config = SplitIoClient::SplitConfig.new 23 | @split_logger = @default_config.split_logger 24 | @split_validator = @default_config.split_validator 25 | end 26 | end 27 | 28 | Dir[File.dirname(__FILE__) + '/support/**/*.rb'].each { |f| require f } 29 | -------------------------------------------------------------------------------- /spec/splitclient/utilities_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe SplitIoClient::Utilities do 6 | describe 'utilities epoch convertions returns correct values' do 7 | let(:string_date) { '2007-11-03 13:18:05 UTC' } 8 | let(:zero_second_string_date) { '2007-11-03 13:18 UTC' } 9 | 10 | it 'validates to_epoch method converts string dates to epoc in seconds removing seconds' do 11 | converted_to_seconds = SplitIoClient::Utilities.to_epoch(string_date) 12 | expect(converted_to_seconds).to eq(Time.parse(zero_second_string_date).to_i) 13 | end 14 | 15 | it 'validates to_epoch_milis method converts string dates to epoc in milis removing seconds' do 16 | converted_to_milis = SplitIoClient::Utilities.to_epoch_milis(string_date) 17 | expect(converted_to_milis).to eq Time.parse(zero_second_string_date).to_i * 1000 18 | end 19 | end 20 | 21 | it 'split bulk of data - split equally' do 22 | hash = {} 23 | 24 | i = 1 25 | while i <= 6 26 | hash["mauro-#{i}"] = Set.new(['feature', 'feature-1']) 27 | i += 1 28 | end 29 | 30 | result = SplitIoClient::Utilities.split_bulk_to_send(hash, 3) 31 | 32 | expect(result.size).to eq 3 33 | expect(result[0].size).to eq 2 34 | expect(result[1].size).to eq 2 35 | expect(result[2].size).to eq 2 36 | end 37 | 38 | it 'split bulk of data - split in 4 bulks' do 39 | hash = {} 40 | 41 | i = 1 42 | while i <= 6 43 | hash["mauro-#{i}"] = 'feature-test' 44 | i += 1 45 | end 46 | 47 | result = SplitIoClient::Utilities.split_bulk_to_send(hash, 4) 48 | 49 | expect(result.size).to eq 4 50 | expect(result[0].size).to eq 2 51 | expect(result[1].size).to eq 2 52 | expect(result[2].size).to eq 1 53 | expect(result[3].size).to eq 1 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /spec/splitclient_rb_corner_cases_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe SplitIoClient do 6 | subject do 7 | SplitIoClient::SplitFactory.new('test_api_key', logger: Logger.new('/dev/null'), streaming_enabled: false).client 8 | end 9 | 10 | let(:splits_json) { File.read(File.expand_path(File.join(File.dirname(__FILE__), 'test_data/splits/splits.json'))) } 11 | let(:segments_json) do 12 | File.read(File.expand_path(File.join(File.dirname(__FILE__), 'test_data/segments/segmentNoOneUses.json'))) 13 | end 14 | 15 | let(:user) { 'fake_user_id_1' } 16 | let(:feature) { 'test_1_ruby' } 17 | let(:non_matching_value_attributes) { { list: 'random' } } 18 | let(:missing_key_attributes) { {} } 19 | let(:nil_attributes) { nil } 20 | let(:segment_res) { '{"name":"mauro_1","added":[],"removed":[],"since":-1,"till":-1 }' } 21 | 22 | before do 23 | stub_request(:post, 'https://events.split.io/api/testImpressions/bulk').to_return(status: 200, body: '') 24 | stub_request(:get, 'https://sdk.split.io/api/splitChanges?s=1.1&since=-1').to_return(status: 200, body: splits_json) 25 | stub_request(:get, 'https://sdk.split.io/api/segmentChanges/demo?since=-1').to_return(status: 200, body: segment_res) 26 | stub_request(:get, 'https://sdk.split.io/api/segmentChanges/employees?since=-1').to_return(status: 200, body: segment_res) 27 | stub_request(:get, 'https://sdk.split.io/api/splitChanges?s=1.1&since=1473413807667').to_return(status: 200, body: segment_res) 28 | stub_request(:post, 'https://telemetry.split.io/api/v1/metrics/config').to_return(status: 200, body: segment_res) 29 | stub_request(:post, 'https://events.split.io/api/testImpressions/count').to_return(status: 200, body: '') 30 | end 31 | 32 | it 'validates the feature is "default" for id when segment used does not exist' do 33 | subject.block_until_ready 34 | expect(subject.get_treatment(user, feature)).to eq 'default' 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /spec/support/matchers/allocate_under_matcher.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rspec/expectations' 4 | 5 | begin 6 | require 'allocation_stats' 7 | rescue LoadError 8 | puts 'Skipping AllocationStats.' 9 | end 10 | 11 | RSpec::Matchers.define :allocate_max do |expected| 12 | match do |actual| 13 | return skip('AllocationStats is not available: skipping.') unless defined?(AllocationStats) 14 | return skip if RUBY_PLATFORM == 'java' 15 | 16 | @trace = actual.is_a?(Proc) ? AllocationStats.new(burn: 3).trace(&actual) : actual 17 | @trace.new_allocations.size <= expected 18 | end 19 | 20 | def objects 21 | self 22 | end 23 | 24 | def supports_block_expectations? 25 | true 26 | end 27 | 28 | def output_trace_info(trace) 29 | trace.allocations(alias_paths: true).group_by(:sourcefile, :sourceline, :class).to_text 30 | end 31 | 32 | failure_message do |_actual| 33 | "expected max of #{expected} objects to be allocated; " \ 34 | "got #{@trace.new_allocations.size}:\n\n" << output_trace_info(@trace) 35 | end 36 | 37 | description do 38 | "allocates max of #{expected} objects" 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /spec/test_data/integrations/auth_body_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "pushEnabled": true, 3 | "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ4LWFibHktY2FwYWJpbGl0eSI6IntcInh4eHhfeHh4eF9zZWdtZW50c1wiOltcInN1YnNjcmliZVwiXSxcInh4eHhfeHh4eF9zcGxpdHNcIjpbXCJzdWJzY3JpYmVcIl0sXCJjb250cm9sX3ByaVwiOltcInN1YnNjcmliZVwiLFwiY2hhbm5lbC1tZXRhZGF0YTpwdWJsaXNoZXJzXCJdLFwiY29udHJvbF9zZWNcIjpbXCJzdWJzY3JpYmVcIixcImNoYW5uZWwtbWV0YWRhdGE6cHVibGlzaGVyc1wiXX0iLCJ4LWFibHktY2xpZW50SWQiOiJjbGllbnRJZCIsImV4cCI6MTU4OTgyMTc1NSwiaWF0IjoxNTg5ODE4MTU1fQ" 4 | } -------------------------------------------------------------------------------- /spec/test_data/integrations/flag_sets.json: -------------------------------------------------------------------------------- 1 | { 2 | "set_1": ["testing222"], 3 | "set_2": ["testing222", "testing"], 4 | "set_3": ["FACUNDO_TEST"] 5 | } 6 | -------------------------------------------------------------------------------- /spec/test_data/integrations/segment1.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "segment1", 3 | "added": [ 4 | "Test" 5 | ], 6 | "removed": [], 7 | "since": -1, 8 | "till": 1470947453877 9 | } -------------------------------------------------------------------------------- /spec/test_data/integrations/segment2.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "segment2", 3 | "added": [ 4 | "testo2222", 5 | "a_new_split_2" 6 | ], 7 | "removed": [], 8 | "since": -1, 9 | "till": 1470947453878 10 | } -------------------------------------------------------------------------------- /spec/test_data/integrations/segment3.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "segment3", 3 | "added": [ 4 | "testo2222", 5 | "test_string_without_attr", 6 | "Test_Save_1", 7 | "test_in_segment" 8 | ], 9 | "removed": [], 10 | "since": -1, 11 | "till": -1 12 | } -------------------------------------------------------------------------------- /spec/test_data/integrations/segment3_updated.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "segment3", 3 | "added": [ 4 | "testo2222", 5 | "test_string_without_attr", 6 | "Test_Save_1" 7 | ], 8 | "removed": ["test_in_segment"], 9 | "since": -1, 10 | "till": -1 11 | } -------------------------------------------------------------------------------- /spec/test_data/integrations/splits_push2.json: -------------------------------------------------------------------------------- 1 | { 2 | "splits": [], 3 | "since": 1585948850109, 4 | "till": 1585948850110 5 | } -------------------------------------------------------------------------------- /spec/test_data/local_treatments/.split: -------------------------------------------------------------------------------- 1 | foo on 2 | bar off 3 | -------------------------------------------------------------------------------- /spec/test_data/local_treatments/split.yaml: -------------------------------------------------------------------------------- 1 | - multiple_keys_feature: 2 | treatment: 'off' 3 | keys: 4 | config: {'desc': 'this applies only to OFF treatment'} 5 | 6 | - multiple_keys_feature: 7 | treatment: 'on' 8 | keys: ['john_doe', 'jane_doe'] 9 | config: {'desc': 'this applies only to ON and only for john_doe and jane_doe. The rest will receive OFF'} 10 | 11 | - single_key_feature: 12 | treatment: 'on' 13 | keys: 'john_doe' 14 | config: {'desc': 'this applies only to ON and only for john_doe. The rest will receive OFF'} 15 | 16 | - single_key_feature: 17 | treatment: 'off' 18 | keys: 19 | config: {'desc': 'this applies only to OFF treatment'} 20 | 21 | - no_keys_feature: 22 | treatment: 'off' 23 | keys: 24 | config: {'desc': 'this applies only to OFF treatment'} 25 | -------------------------------------------------------------------------------- /spec/test_data/segments/combining_matcher_segments.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "employees", 3 | "added": ["user_for_testing_do_no_erase"], 4 | "removed": [], 5 | "since": -1, 6 | "till": -1 7 | } 8 | -------------------------------------------------------------------------------- /spec/test_data/segments/engine_segments.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo", 3 | "added": ["fake_user_id_1", "fake_user_id_2", "222"], 4 | "removed": [], 5 | "since": -1, 6 | "till": -1 7 | } 8 | -------------------------------------------------------------------------------- /spec/test_data/segments/engine_segments2.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test_segment", 3 | "added": ["fake_user_id_3"], 4 | "removed": [], 5 | "since": -1, 6 | "till": 1452026405473 7 | } 8 | -------------------------------------------------------------------------------- /spec/test_data/segments/matchers_segments.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo", 3 | "added": ["fake_user_id_1", "fake_user_id_2"], 4 | "removed": [], 5 | "since": -1, 6 | "till": -1 7 | } 8 | -------------------------------------------------------------------------------- /spec/test_data/segments/segmentNoOneUses.json: -------------------------------------------------------------------------------- 1 | { 2 | "name":"no_one_uses", 3 | "added":[ 4 | "max", 5 | "dan" 6 | ], 7 | "removed":[], 8 | "since":-1, 9 | "till":1473863075059 10 | } 11 | -------------------------------------------------------------------------------- /spec/test_data/segments/segments.json: -------------------------------------------------------------------------------- 1 | { 2 | "name":"employees", 3 | "added":[ 4 | "max", 5 | "dan" 6 | ], 7 | "removed":[], 8 | "since":-1, 9 | "till":1473863075059 10 | } 11 | -------------------------------------------------------------------------------- /spec/test_data/segments/segments2.json: -------------------------------------------------------------------------------- 1 | { 2 | "name":"employees", 3 | "added":[], 4 | "removed":[], 5 | "since":1473863075059, 6 | "till":1473863075059 7 | } 8 | -------------------------------------------------------------------------------- /spec/test_data/splits/between_matcher/datetime_matcher_splits.json: -------------------------------------------------------------------------------- 1 | { 2 | "splits": [ 3 | { 4 | "orgId":"cee838c0-b3eb-11e5-855f-4eacec19f7bf", 5 | "environment":"cf2d09f0-b3eb-11e5-855f-4eacec19f7bf", 6 | "name":"test_feature", 7 | "trafficTypeId":"u", 8 | "trafficTypeName":"User", 9 | "seed":-195840228, 10 | "status":"ACTIVE", 11 | "killed":false, 12 | "defaultTreatment":"default", 13 | "conditions":[ 14 | { 15 | "matcherGroup":{ 16 | "combiner":"AND", 17 | "matchers":[ 18 | { 19 | "keySelector":{ 20 | "trafficType":"user", 21 | "attribute":"created" 22 | }, 23 | "matcherType":"BETWEEN", 24 | "negate":false, 25 | "userDefinedSegmentMatcherData":null, 26 | "whitelistMatcherData":null, 27 | "unaryNumericMatcherData":null, 28 | "betweenMatcherData":{ 29 | "dataType":"DATETIME", 30 | "start":1451687340000, 31 | "end":1459722588398 32 | } 33 | } 34 | ] 35 | }, 36 | "partitions":[ 37 | { 38 | "treatment":"on", 39 | "size":100 40 | }, 41 | { 42 | "treatment":"off", 43 | "size":0 44 | }, 45 | { 46 | "treatment":"default", 47 | "size":0 48 | } 49 | ] 50 | } 51 | ] 52 | } 53 | ] 54 | } 55 | -------------------------------------------------------------------------------- /spec/test_data/splits/between_matcher/negate_number_matcher_splits.json: -------------------------------------------------------------------------------- 1 | { 2 | "splits": [ 3 | { 4 | "orgId":"cee838c0-b3eb-11e5-855f-4eacec19f7bf", 5 | "environment":"cf2d09f0-b3eb-11e5-855f-4eacec19f7bf", 6 | "name":"test_feature", 7 | "trafficTypeId":"u", 8 | "trafficTypeName":"User", 9 | "seed":-195840228, 10 | "status":"ACTIVE", 11 | "killed":false, 12 | "defaultTreatment":"default", 13 | "conditions":[ 14 | { 15 | "matcherGroup":{ 16 | "combiner":"AND", 17 | "matchers":[ 18 | { 19 | "keySelector":{ 20 | "trafficType":"user", 21 | "attribute":"income" 22 | }, 23 | "matcherType":"BETWEEN", 24 | "negate":true, 25 | "userDefinedSegmentMatcherData":null, 26 | "whitelistMatcherData":null, 27 | "unaryNumericMatcherData":null, 28 | "betweenMatcherData":{ 29 | "dataType":"NUMBER", 30 | "start":100, 31 | "end":120 32 | } 33 | } 34 | ] 35 | }, 36 | "partitions":[ 37 | { 38 | "treatment":"on", 39 | "size":100 40 | }, 41 | { 42 | "treatment":"off", 43 | "size":0 44 | }, 45 | { 46 | "treatment":"default", 47 | "size":0 48 | } 49 | ] 50 | } 51 | ] 52 | } 53 | ] 54 | } 55 | -------------------------------------------------------------------------------- /spec/test_data/splits/between_matcher/negative_number_matcher_splits.json: -------------------------------------------------------------------------------- 1 | { 2 | "splits": [ 3 | { 4 | "orgId":"cee838c0-b3eb-11e5-855f-4eacec19f7bf", 5 | "environment":"cf2d09f0-b3eb-11e5-855f-4eacec19f7bf", 6 | "name":"test_feature", 7 | "trafficTypeId":"u", 8 | "trafficTypeName":"User", 9 | "seed":-195840228, 10 | "status":"ACTIVE", 11 | "killed":false, 12 | "defaultTreatment":"default", 13 | "conditions":[ 14 | { 15 | "matcherGroup":{ 16 | "combiner":"AND", 17 | "matchers":[ 18 | { 19 | "keySelector":{ 20 | "trafficType":"user", 21 | "attribute":"income" 22 | }, 23 | "matcherType":"BETWEEN", 24 | "negate":false, 25 | "userDefinedSegmentMatcherData":null, 26 | "whitelistMatcherData":null, 27 | "unaryNumericMatcherData":null, 28 | "betweenMatcherData":{ 29 | "dataType":"NUMBER", 30 | "start":-100, 31 | "end":120 32 | } 33 | } 34 | ] 35 | }, 36 | "partitions":[ 37 | { 38 | "treatment":"on", 39 | "size":100 40 | }, 41 | { 42 | "treatment":"off", 43 | "size":0 44 | }, 45 | { 46 | "treatment":"default", 47 | "size":0 48 | } 49 | ] 50 | } 51 | ] 52 | } 53 | ] 54 | } 55 | -------------------------------------------------------------------------------- /spec/test_data/splits/between_matcher/number_matcher_splits.json: -------------------------------------------------------------------------------- 1 | { 2 | "splits": [ 3 | { 4 | "orgId":"cee838c0-b3eb-11e5-855f-4eacec19f7bf", 5 | "environment":"cf2d09f0-b3eb-11e5-855f-4eacec19f7bf", 6 | "name":"test_feature", 7 | "trafficTypeId":"u", 8 | "trafficTypeName":"User", 9 | "seed":-195840228, 10 | "status":"ACTIVE", 11 | "killed":false, 12 | "defaultTreatment":"default", 13 | "conditions":[ 14 | { 15 | "matcherGroup":{ 16 | "combiner":"AND", 17 | "matchers":[ 18 | { 19 | "keySelector":{ 20 | "trafficType":"user", 21 | "attribute":"income" 22 | }, 23 | "matcherType":"BETWEEN", 24 | "negate":false, 25 | "userDefinedSegmentMatcherData":null, 26 | "whitelistMatcherData":null, 27 | "unaryNumericMatcherData":null, 28 | "betweenMatcherData":{ 29 | "dataType":"NUMBER", 30 | "start":100, 31 | "end":120 32 | } 33 | } 34 | ] 35 | }, 36 | "partitions":[ 37 | { 38 | "treatment":"on", 39 | "size":100 40 | }, 41 | { 42 | "treatment":"off", 43 | "size":0 44 | }, 45 | { 46 | "treatment":"default", 47 | "size":0 48 | } 49 | ] 50 | } 51 | ] 52 | } 53 | ] 54 | } 55 | -------------------------------------------------------------------------------- /spec/test_data/splits/boolean_matcher/splits.json: -------------------------------------------------------------------------------- 1 | { 2 | "splits": [ 3 | { 4 | "orgId":"cee838c0-b3eb-11e5-855f-4eacec19f7bf", 5 | "environment":"cf2d09f0-b3eb-11e5-855f-4eacec19f7bf", 6 | "name":"test_boolean", 7 | "trafficTypeId":"u", 8 | "trafficTypeName":"User", 9 | "seed":-195840228, 10 | "status":"ACTIVE", 11 | "killed":false, 12 | "defaultTreatment":"default", 13 | "conditions":[ 14 | { 15 | "matcherGroup":{ 16 | "combiner":"AND", 17 | "matchers":[ 18 | { 19 | "keySelector":{ 20 | "trafficType":"user", 21 | "attribute":"created" 22 | }, 23 | "matcherType":"EQUAL_TO_BOOLEAN", 24 | "negate":false, 25 | "userDefinedSegmentMatcherData":null, 26 | "whitelistMatcherData":null, 27 | "unaryNumericMatcherData":null, 28 | "booleanMatcherData": true 29 | } 30 | ] 31 | }, 32 | "partitions":[ 33 | { 34 | "treatment":"on", 35 | "size":100 36 | }, 37 | { 38 | "treatment":"off", 39 | "size":0 40 | }, 41 | { 42 | "treatment":"default", 43 | "size":0 44 | } 45 | ] 46 | } 47 | ] 48 | } 49 | ] 50 | } 51 | -------------------------------------------------------------------------------- /spec/test_data/splits/engine/all_keys_matcher.json: -------------------------------------------------------------------------------- 1 | { 2 | "splits": [ 3 | { 4 | "orgId":"cee838c0-b3eb-11e5-855f-4eacec19f7bf", 5 | "environment":"cf2d09f0-b3eb-11e5-855f-4eacec19f7bf", 6 | "name":"test_feature", 7 | "trafficTypeId":"u", 8 | "trafficTypeName":"User", 9 | "seed":-195840228, 10 | "status":"ACTIVE", 11 | "killed":false, 12 | "defaultTreatment":"off", 13 | "configurations":{ 14 | "on": "{\"size\":15,\"test\":20}" 15 | }, 16 | "conditions":[ 17 | { 18 | "matcherGroup":{ 19 | "combiner":"AND", 20 | "matchers":[ 21 | { 22 | "matcherType":"ALL_KEYS", 23 | "negate":false, 24 | "userDefinedSegmentMatcherData":null, 25 | "whitelistMatcherData":null 26 | } 27 | ] 28 | }, 29 | "partitions":[ 30 | { 31 | "treatment":"on", 32 | "size":100 33 | } 34 | ] 35 | } 36 | ], 37 | "sets": ["set_1"] 38 | } 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /spec/test_data/splits/engine/flag_sets.json: -------------------------------------------------------------------------------- 1 | { 2 | "set_1": ["test_feature"], 3 | "set_2": ["mauro_test"], 4 | "set_3": ["sample_feature", "beta_feature"], 5 | "set_4": ["test_whitelist"] 6 | } 7 | -------------------------------------------------------------------------------- /spec/test_data/splits/engine/killed.json: -------------------------------------------------------------------------------- 1 | { 2 | "splits": [ 3 | { 4 | "orgId":"cee838c0-b3eb-11e5-855f-4eacec19f7bf", 5 | "environment":"cf2d09f0-b3eb-11e5-855f-4eacec19f7bf", 6 | "name":"test_killed", 7 | "trafficTypeId":"u", 8 | "trafficTypeName":"User", 9 | "seed":-1245274114, 10 | "status":"ACTIVE", 11 | "killed":true, 12 | "defaultTreatment":"def_test", 13 | "conditions":[ 14 | { 15 | "matcherGroup":{ 16 | "combiner":"AND", 17 | "matchers":[ 18 | { 19 | "matcherType":"ALL_KEYS", 20 | "negate":false, 21 | "userDefinedSegmentMatcherData":null, 22 | "whitelistMatcherData":null 23 | } 24 | ] 25 | }, 26 | "partitions":[ 27 | { 28 | "treatment":"on", 29 | "size":100 30 | } 31 | ] 32 | } 33 | ] 34 | } 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /spec/test_data/splits/engine/segment_deleted_matcher.json: -------------------------------------------------------------------------------- 1 | { 2 | "splits": [ 3 | { 4 | "orgId":"cee838c0-b3eb-11e5-855f-4eacec19f7bf", 5 | "environment":"cf2d09f0-b3eb-11e5-855f-4eacec19f7bf", 6 | "name":"new_feature", 7 | "trafficTypeId":"u", 8 | "trafficTypeName":"User", 9 | "seed":-1177551240, 10 | "status":"ARCHIVED", 11 | "killed":false, 12 | "defaultTreatment":"def_test", 13 | "conditions":[ 14 | { 15 | "matcherGroup":{ 16 | "combiner":"AND", 17 | "matchers":[ 18 | { 19 | "matcherType":"IN_SEGMENT", 20 | "negate":false, 21 | "userDefinedSegmentMatcherData":{ 22 | "segmentName":"demo" 23 | }, 24 | "whitelistMatcherData":null 25 | } 26 | ] 27 | }, 28 | "partitions":[ 29 | { 30 | "treatment":"on", 31 | "size":100 32 | }, 33 | { 34 | "treatment":"control", 35 | "size":0 36 | } 37 | ] 38 | } 39 | ] 40 | } 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /spec/test_data/splits/engine/segment_matcher.json: -------------------------------------------------------------------------------- 1 | { 2 | "splits": [ 3 | { 4 | "orgId":"cee838c0-b3eb-11e5-855f-4eacec19f7bf", 5 | "environment":"cf2d09f0-b3eb-11e5-855f-4eacec19f7bf", 6 | "name":"new_feature", 7 | "trafficTypeId":"u", 8 | "trafficTypeName":"User", 9 | "seed":-1177551240, 10 | "status":"ACTIVE", 11 | "killed":false, 12 | "defaultTreatment":"def_test", 13 | "conditions":[ 14 | { 15 | "matcherGroup":{ 16 | "combiner":"AND", 17 | "matchers":[ 18 | { 19 | "matcherType":"IN_SEGMENT", 20 | "negate":false, 21 | "userDefinedSegmentMatcherData":{ 22 | "segmentName":"demo" 23 | }, 24 | "whitelistMatcherData":null 25 | } 26 | ] 27 | }, 28 | "partitions":[ 29 | { 30 | "treatment":"on", 31 | "size":100 32 | }, 33 | { 34 | "treatment":"control", 35 | "size":0 36 | } 37 | ], 38 | "label": "default label" 39 | } 40 | ] 41 | } 42 | ] 43 | } 44 | -------------------------------------------------------------------------------- /spec/test_data/splits/engine/whitelist_matcher.json: -------------------------------------------------------------------------------- 1 | { 2 | "splits": [ 3 | { 4 | "orgId":"cee838c0-b3eb-11e5-855f-4eacec19f7bf", 5 | "environment":"cf2d09f0-b3eb-11e5-855f-4eacec19f7bf", 6 | "name":"test_whitelist", 7 | "trafficTypeId":"u", 8 | "trafficTypeName":"User", 9 | "seed":-1245274114, 10 | "status":"ACTIVE", 11 | "killed":false, 12 | "defaultTreatment":"off", 13 | "conditions":[ 14 | { 15 | "matcherGroup":{ 16 | "combiner":"AND", 17 | "matchers":[ 18 | { 19 | "matcherType":"WHITELIST", 20 | "negate":false, 21 | "userDefinedSegmentMatcherData":null, 22 | "whitelistMatcherData":{ 23 | "whitelist":[ 24 | "fake_user_id_1", 25 | "fake_user_id_3" 26 | ] 27 | } 28 | } 29 | ] 30 | }, 31 | "partitions":[ 32 | { 33 | "treatment":"on", 34 | "size":100 35 | } 36 | ] 37 | } 38 | ] 39 | } 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /spec/test_data/splits/equal_to_matcher/date_splits.json: -------------------------------------------------------------------------------- 1 | { 2 | "splits": [ 3 | { 4 | "orgId":"cee838c0-b3eb-11e5-855f-4eacec19f7bf", 5 | "environment":"cf2d09f0-b3eb-11e5-855f-4eacec19f7bf", 6 | "name":"test_feature", 7 | "trafficTypeId":"u", 8 | "trafficTypeName":"User", 9 | "seed":-195840228, 10 | "status":"ACTIVE", 11 | "killed":false, 12 | "defaultTreatment":"default", 13 | "conditions":[ 14 | { 15 | "matcherGroup":{ 16 | "combiner":"AND", 17 | "matchers":[ 18 | { 19 | "keySelector":{ 20 | "trafficType":"user", 21 | "attribute":"created" 22 | }, 23 | "matcherType":"EQUAL_TO", 24 | "negate":false, 25 | "userDefinedSegmentMatcherData":null, 26 | "whitelistMatcherData":null, 27 | "unaryNumericMatcherData":{ 28 | "dataType":"DATETIME", 29 | "value":1459468800000 30 | }, 31 | "betweenMatcherData":null 32 | } 33 | ] 34 | }, 35 | "partitions":[ 36 | { 37 | "treatment":"on", 38 | "size":100 39 | }, 40 | { 41 | "treatment":"off", 42 | "size":0 43 | } 44 | ] 45 | } 46 | ], 47 | "sets": ["set_1"] 48 | } 49 | ] 50 | } 51 | -------------------------------------------------------------------------------- /spec/test_data/splits/equal_to_matcher/negative_splits.json: -------------------------------------------------------------------------------- 1 | { 2 | "splits": [ 3 | { 4 | "orgId":"cee838c0-b3eb-11e5-855f-4eacec19f7bf", 5 | "environment":"cf2d09f0-b3eb-11e5-855f-4eacec19f7bf", 6 | "name":"test_feature", 7 | "trafficTypeId":"u", 8 | "trafficTypeName":"User", 9 | "seed":-195840228, 10 | "status":"ACTIVE", 11 | "killed":false, 12 | "defaultTreatment":"default", 13 | "conditions":[ 14 | { 15 | "matcherGroup":{ 16 | "combiner":"AND", 17 | "matchers":[ 18 | { 19 | "keySelector":{ 20 | "trafficType":"user", 21 | "attribute":"age" 22 | }, 23 | "matcherType":"EQUAL_TO", 24 | "negate":false, 25 | "userDefinedSegmentMatcherData":null, 26 | "whitelistMatcherData":null, 27 | "unaryNumericMatcherData":{ 28 | "dataType":"NUMBER", 29 | "value":-1 30 | }, 31 | "betweenMatcherData":null 32 | } 33 | ] 34 | }, 35 | "partitions":[ 36 | { 37 | "treatment":"on", 38 | "size":100 39 | }, 40 | { 41 | "treatment":"off", 42 | "size":0 43 | } 44 | ] 45 | } 46 | ], 47 | "sets": ["set_1"] 48 | } 49 | ] 50 | } 51 | -------------------------------------------------------------------------------- /spec/test_data/splits/equal_to_matcher/splits.json: -------------------------------------------------------------------------------- 1 | { 2 | "splits": [ 3 | { 4 | "orgId":"cee838c0-b3eb-11e5-855f-4eacec19f7bf", 5 | "environment":"cf2d09f0-b3eb-11e5-855f-4eacec19f7bf", 6 | "name":"test_feature", 7 | "trafficTypeId":"u", 8 | "trafficTypeName":"User", 9 | "seed":-195840228, 10 | "status":"ACTIVE", 11 | "killed":false, 12 | "defaultTreatment":"default", 13 | "conditions":[ 14 | { 15 | "matcherGroup":{ 16 | "combiner":"AND", 17 | "matchers":[ 18 | { 19 | "keySelector":{ 20 | "trafficType":"user", 21 | "attribute":"age" 22 | }, 23 | "matcherType":"EQUAL_TO", 24 | "negate":false, 25 | "userDefinedSegmentMatcherData":null, 26 | "whitelistMatcherData":null, 27 | "unaryNumericMatcherData":{ 28 | "dataType":"NUMBER", 29 | "value":30 30 | }, 31 | "betweenMatcherData":null 32 | } 33 | ] 34 | }, 35 | "partitions":[ 36 | { 37 | "treatment":"on", 38 | "size":100 39 | }, 40 | { 41 | "treatment":"off", 42 | "size":0 43 | } 44 | ] 45 | } 46 | ], 47 | "sets": ["set_1"] 48 | } 49 | ] 50 | } 51 | -------------------------------------------------------------------------------- /spec/test_data/splits/equal_to_matcher/zero_splits.json: -------------------------------------------------------------------------------- 1 | { 2 | "splits": [ 3 | { 4 | "orgId":"cee838c0-b3eb-11e5-855f-4eacec19f7bf", 5 | "environment":"cf2d09f0-b3eb-11e5-855f-4eacec19f7bf", 6 | "name":"test_feature", 7 | "trafficTypeId":"u", 8 | "trafficTypeName":"User", 9 | "seed":-195840228, 10 | "status":"ACTIVE", 11 | "killed":false, 12 | "defaultTreatment":"default", 13 | "conditions":[ 14 | { 15 | "matcherGroup":{ 16 | "combiner":"AND", 17 | "matchers":[ 18 | { 19 | "keySelector":{ 20 | "trafficType":"user", 21 | "attribute":"age" 22 | }, 23 | "matcherType":"EQUAL_TO", 24 | "negate":false, 25 | "userDefinedSegmentMatcherData":null, 26 | "whitelistMatcherData":null, 27 | "unaryNumericMatcherData":{ 28 | "dataType":"NUMBER", 29 | "value":0 30 | }, 31 | "betweenMatcherData":null 32 | } 33 | ] 34 | }, 35 | "partitions":[ 36 | { 37 | "treatment":"on", 38 | "size":100 39 | }, 40 | { 41 | "treatment":"off", 42 | "size":0 43 | } 44 | ] 45 | } 46 | ], 47 | "sets": ["set_1"] 48 | } 49 | ] 50 | } 51 | -------------------------------------------------------------------------------- /spec/test_data/splits/flag_sets.json: -------------------------------------------------------------------------------- 1 | { 2 | "set_1": ["test_1_ruby", "sample_feature"], 3 | "set_2": ["sample_feature"] 4 | } 5 | -------------------------------------------------------------------------------- /spec/test_data/splits/greater_than_or_equal_to_matcher/date_splits.json: -------------------------------------------------------------------------------- 1 | { 2 | "splits": [ 3 | { 4 | "orgId":"cee838c0-b3eb-11e5-855f-4eacec19f7bf", 5 | "environment":"cf2d09f0-b3eb-11e5-855f-4eacec19f7bf", 6 | "name":"test_feature", 7 | "trafficTypeId":"u", 8 | "trafficTypeName":"User", 9 | "seed":-195840228, 10 | "status":"ACTIVE", 11 | "killed":false, 12 | "defaultTreatment":"default", 13 | "conditions":[ 14 | { 15 | "matcherGroup":{ 16 | "combiner":"AND", 17 | "matchers":[ 18 | { 19 | "keySelector":{ 20 | "trafficType":"user", 21 | "attribute":"created" 22 | }, 23 | "matcherType":"GREATER_THAN_OR_EQUAL_TO", 24 | "negate":false, 25 | "userDefinedSegmentMatcherData":null, 26 | "whitelistMatcherData":null, 27 | "unaryNumericMatcherData":{ 28 | "dataType":"DATETIME", 29 | "value":1459468800000 30 | }, 31 | "betweenMatcherData":null 32 | } 33 | ] 34 | }, 35 | "partitions":[ 36 | { 37 | "treatment":"on", 38 | "size":100 39 | }, 40 | { 41 | "treatment":"off", 42 | "size":0 43 | } 44 | ] 45 | } 46 | ] 47 | } 48 | ] 49 | } 50 | -------------------------------------------------------------------------------- /spec/test_data/splits/greater_than_or_equal_to_matcher/negative_splits.json: -------------------------------------------------------------------------------- 1 | { 2 | "splits": [ 3 | { 4 | "orgId":"cee838c0-b3eb-11e5-855f-4eacec19f7bf", 5 | "environment":"cf2d09f0-b3eb-11e5-855f-4eacec19f7bf", 6 | "name":"test_feature", 7 | "trafficTypeId":"u", 8 | "trafficTypeName":"User", 9 | "seed":-195840228, 10 | "status":"ACTIVE", 11 | "killed":false, 12 | "defaultTreatment":"default", 13 | "conditions":[ 14 | { 15 | "matcherGroup":{ 16 | "combiner":"AND", 17 | "matchers":[ 18 | { 19 | "keySelector":{ 20 | "trafficType":"user", 21 | "attribute":"age" 22 | }, 23 | "matcherType":"GREATER_THAN_OR_EQUAL_TO", 24 | "negate":false, 25 | "userDefinedSegmentMatcherData":null, 26 | "whitelistMatcherData":null, 27 | "unaryNumericMatcherData":{ 28 | "dataType":"NUMBER", 29 | "value":-30 30 | }, 31 | "betweenMatcherData":null 32 | } 33 | ] 34 | }, 35 | "partitions":[ 36 | { 37 | "treatment":"on", 38 | "size":100 39 | }, 40 | { 41 | "treatment":"off", 42 | "size":0 43 | } 44 | ] 45 | } 46 | ] 47 | } 48 | ] 49 | } 50 | -------------------------------------------------------------------------------- /spec/test_data/splits/greater_than_or_equal_to_matcher/splits.json: -------------------------------------------------------------------------------- 1 | { 2 | "splits": [ 3 | { 4 | "orgId":"cee838c0-b3eb-11e5-855f-4eacec19f7bf", 5 | "environment":"cf2d09f0-b3eb-11e5-855f-4eacec19f7bf", 6 | "name":"test_feature", 7 | "trafficTypeId":"u", 8 | "trafficTypeName":"User", 9 | "seed":-195840228, 10 | "status":"ACTIVE", 11 | "killed":false, 12 | "defaultTreatment":"default", 13 | "conditions":[ 14 | { 15 | "matcherGroup":{ 16 | "combiner":"AND", 17 | "matchers":[ 18 | { 19 | "keySelector":{ 20 | "trafficType":"user", 21 | "attribute":"age" 22 | }, 23 | "matcherType":"GREATER_THAN_OR_EQUAL_TO", 24 | "negate":false, 25 | "userDefinedSegmentMatcherData":null, 26 | "whitelistMatcherData":null, 27 | "unaryNumericMatcherData":{ 28 | "dataType":"NUMBER", 29 | "value":30 30 | }, 31 | "betweenMatcherData":null 32 | } 33 | ] 34 | }, 35 | "partitions":[ 36 | { 37 | "treatment":"on", 38 | "size":100 39 | }, 40 | { 41 | "treatment":"off", 42 | "size":0 43 | } 44 | ] 45 | } 46 | ] 47 | } 48 | ] 49 | } 50 | -------------------------------------------------------------------------------- /spec/test_data/splits/less_than_or_equal_to_matcher/date_splits.json: -------------------------------------------------------------------------------- 1 | { 2 | "splits": [ 3 | { 4 | "orgId":"cee838c0-b3eb-11e5-855f-4eacec19f7bf", 5 | "environment":"cf2d09f0-b3eb-11e5-855f-4eacec19f7bf", 6 | "name":"test_feature", 7 | "trafficTypeId":"u", 8 | "trafficTypeName":"User", 9 | "seed":-195840228, 10 | "status":"ACTIVE", 11 | "killed":false, 12 | "defaultTreatment":"default", 13 | "conditions":[ 14 | { 15 | "matcherGroup":{ 16 | "combiner":"AND", 17 | "matchers":[ 18 | { 19 | "keySelector":{ 20 | "trafficType":"user", 21 | "attribute":"created" 22 | }, 23 | "matcherType":"LESS_THAN_OR_EQUAL_TO", 24 | "negate":false, 25 | "userDefinedSegmentMatcherData":null, 26 | "whitelistMatcherData":null, 27 | "unaryNumericMatcherData":{ 28 | "dataType":"DATETIME", 29 | "value":1459468800000 30 | }, 31 | "betweenMatcherData":null 32 | } 33 | ] 34 | }, 35 | "partitions":[ 36 | { 37 | "treatment":"on", 38 | "size":100 39 | }, 40 | { 41 | "treatment":"off", 42 | "size":0 43 | } 44 | ] 45 | } 46 | ] 47 | } 48 | ] 49 | } 50 | -------------------------------------------------------------------------------- /spec/test_data/splits/less_than_or_equal_to_matcher/date_splits2.json: -------------------------------------------------------------------------------- 1 | { 2 | "splits": [ 3 | { 4 | "orgId":null, 5 | "environment":null, 6 | "trafficTypeId":null, 7 | "trafficTypeName":null, 8 | "name":"RUBY_isOnOrBeforeDateTimeWithAttributeValueThatDoesNotMatch", 9 | "seed":338948780, 10 | "status":"ACTIVE", 11 | "killed":false, 12 | "defaultTreatment":"V1", 13 | "conditions":[ 14 | { 15 | "matcherGroup":{ 16 | "combiner":"AND", 17 | "matchers":[ 18 | { 19 | "keySelector":{ 20 | "trafficType":"user", 21 | "attribute":"join" 22 | }, 23 | "matcherType":"LESS_THAN_OR_EQUAL_TO", 24 | "negate":false, 25 | "userDefinedSegmentMatcherData":null, 26 | "whitelistMatcherData":null, 27 | "unaryNumericMatcherData":{ 28 | "dataType":"DATETIME", 29 | "value":1466205600000 30 | }, 31 | "betweenMatcherData":null 32 | } 33 | ] 34 | }, 35 | "partitions":[ 36 | { 37 | "treatment":"V1", 38 | "size":0 39 | }, 40 | { 41 | "treatment":"V2", 42 | "size":100 43 | }, 44 | { 45 | "treatment":"V3", 46 | "size":0 47 | } 48 | ] 49 | } 50 | ] 51 | } 52 | ] 53 | } 54 | -------------------------------------------------------------------------------- /spec/test_data/splits/less_than_or_equal_to_matcher/negative_splits.json: -------------------------------------------------------------------------------- 1 | { 2 | "splits": [ 3 | { 4 | "orgId":"cee838c0-b3eb-11e5-855f-4eacec19f7bf", 5 | "environment":"cf2d09f0-b3eb-11e5-855f-4eacec19f7bf", 6 | "name":"test_feature", 7 | "trafficTypeId":"u", 8 | "trafficTypeName":"User", 9 | "seed":-195840228, 10 | "status":"ACTIVE", 11 | "killed":false, 12 | "defaultTreatment":"default", 13 | "conditions":[ 14 | { 15 | "matcherGroup":{ 16 | "combiner":"AND", 17 | "matchers":[ 18 | { 19 | "keySelector":{ 20 | "trafficType":"user", 21 | "attribute":"age" 22 | }, 23 | "matcherType":"LESS_THAN_OR_EQUAL_TO", 24 | "negate":false, 25 | "userDefinedSegmentMatcherData":null, 26 | "whitelistMatcherData":null, 27 | "unaryNumericMatcherData":{ 28 | "dataType":"NUMBER", 29 | "value":-30 30 | }, 31 | "betweenMatcherData":null 32 | } 33 | ] 34 | }, 35 | "partitions":[ 36 | { 37 | "treatment":"on", 38 | "size":100 39 | }, 40 | { 41 | "treatment":"off", 42 | "size":0 43 | } 44 | ] 45 | } 46 | ] 47 | } 48 | ] 49 | } 50 | -------------------------------------------------------------------------------- /spec/test_data/splits/less_than_or_equal_to_matcher/splits.json: -------------------------------------------------------------------------------- 1 | { 2 | "splits": [ 3 | { 4 | "orgId":"cee838c0-b3eb-11e5-855f-4eacec19f7bf", 5 | "environment":"cf2d09f0-b3eb-11e5-855f-4eacec19f7bf", 6 | "name":"test_feature", 7 | "trafficTypeId":"u", 8 | "trafficTypeName":"User", 9 | "seed":-195840228, 10 | "status":"ACTIVE", 11 | "killed":false, 12 | "defaultTreatment":"default", 13 | "conditions":[ 14 | { 15 | "matcherGroup":{ 16 | "combiner":"AND", 17 | "matchers":[ 18 | { 19 | "keySelector":{ 20 | "trafficType":"user", 21 | "attribute":"age" 22 | }, 23 | "matcherType":"LESS_THAN_OR_EQUAL_TO", 24 | "negate":false, 25 | "userDefinedSegmentMatcherData":null, 26 | "whitelistMatcherData":null, 27 | "unaryNumericMatcherData":{ 28 | "dataType":"NUMBER", 29 | "value":30 30 | }, 31 | "betweenMatcherData":null 32 | } 33 | ] 34 | }, 35 | "partitions":[ 36 | { 37 | "treatment":"on", 38 | "size":100 39 | }, 40 | { 41 | "treatment":"off", 42 | "size":0 43 | } 44 | ] 45 | } 46 | ] 47 | } 48 | ] 49 | } 50 | -------------------------------------------------------------------------------- /spec/test_data/splits/semver/between-semver.csv: -------------------------------------------------------------------------------- 1 | # version1, version2, version3, expected 2 | 1.1.1,2.2.2,3.3.3,true 3 | 1.1.1-rc.1,1.1.1-rc.2,1.1.1-rc.3,true 4 | 1.0.0-alpha,1.0.0-alpha.1,1.0.0-alpha.beta,true 5 | 1.0.0-alpha.1,1.0.0-alpha.beta,1.0.0-beta,true 6 | 1.0.0-alpha.beta,1.0.0-beta,1.0.0-beta.2,true 7 | 1.0.0-beta,1.0.0-beta.2,1.0.0-beta.11,true 8 | 1.0.0-beta.2,1.0.0-beta.11,1.0.0-rc.1,true 9 | 1.0.0-beta.11,1.0.0-rc.1,1.0.0,true 10 | 1.1.2,1.1.3,1.1.4,true 11 | 1.2.1,1.3.1,1.4.1,true 12 | 2.0.0,3.0.0,4.0.0,true 13 | 2.2.2,2.2.3-rc1,2.2.3,true 14 | 2.2.2,2.3.2-rc100,2.3.3,true 15 | 1.0.0-rc.1+build.1,1.2.3-beta,1.2.3-rc.1+build.123,true 16 | 3.3.3,3.3.3-alpha,3.3.4,false 17 | 2.2.2-rc.1,2.2.2+metadata,2.2.2-rc.10,false 18 | 1.1.1-rc.1,1.1.1-rc.3,1.1.1-rc.2,false -------------------------------------------------------------------------------- /spec/test_data/splits/semver/equal-to-semver.csv: -------------------------------------------------------------------------------- 1 | # version1, version2, equals 2 | 1.1.1,1.1.1,true 3 | 1.1.1,1.1.1+metadata,false 4 | 1.1.1,1.1.1-rc.1,false 5 | 88.88.88,88.88.88,true 6 | 1.2.3----RC-SNAPSHOT.12.9.1--.12,1.2.3----RC-SNAPSHOT.12.9.1--.12,true 7 | 10.2.3-DEV-SNAPSHOT,10.2.3-SNAPSHOT-123,false -------------------------------------------------------------------------------- /spec/test_data/splits/semver/invalid-semantic-versions.csv: -------------------------------------------------------------------------------- 1 | # invalid 2 | 1 3 | 1.2 4 | 1.alpha.2 5 | +invalid 6 | -invalid 7 | -invalid+invalid 8 | -invalid.01 9 | alpha 10 | alpha.beta 11 | alpha.beta.1 12 | alpha.1 13 | alpha+beta 14 | alpha_beta 15 | alpha. 16 | alpha.. 17 | beta 18 | -alpha. 19 | 1.2 20 | 1.2.3.DEV 21 | 1.2-SNAPSHOT 22 | 1.2.31.2.3----RC-SNAPSHOT.12.09.1--..12+788 23 | 1.2-RC-SNAPSHOT 24 | -1.0.3-gamma+b7718 25 | +justmeta 26 | #99999999999999999999999.999999999999999999.99999999999999999----RC-SNAPSHOT.12.09.1--------------------------------..12 -------------------------------------------------------------------------------- /spec/test_data/splits/semver/valid-semantic-versions.csv: -------------------------------------------------------------------------------- 1 | # higher, lower 2 | 1.1.2,1.1.1 3 | 1.0.0,1.0.0-rc.1 4 | 1.1.0-rc.1,1.0.0-beta.11 5 | 1.0.0-beta.11,1.0.0-beta.2 6 | 1.0.0-beta.2,1.0.0-beta 7 | 1.0.0-beta,1.0.0-alpha.beta 8 | 1.0.0-alpha.beta,1.0.0-alpha.1 9 | 1.0.0-alpha.1,1.0.0-alpha 10 | 2.2.2-rc.2+metadata-lalala,2.2.2-rc.1.2 11 | 1.2.3,0.0.4 12 | 1.1.2+meta,1.1.2-prerelease+meta 13 | 1.0.0-beta,1.0.0-alpha 14 | 1.0.0-alpha0.valid,1.0.0-alpha.0valid 15 | 1.0.0-rc.1+build.1,1.0.0-alpha-a.b-c-somethinglong+build.1-aef.1-its-okay 16 | 10.2.3-DEV-SNAPSHOT,1.2.3-SNAPSHOT-123 17 | 1.1.1-rc2,1.0.0-0A.is.legal 18 | 1.2.3----RC-SNAPSHOT.12.9.1--.12+788,1.2.3----R-S.12.9.1--.12+meta 19 | 1.2.3----RC-SNAPSHOT.12.9.1--.12.88,1.2.3----RC-SNAPSHOT.12.9.1--.12 20 | 9223372036854775807.9223372036854775807.9223372036854775807,9223372036854775807.9223372036854775807.9223372036854775806 21 | 1.1.1-alpha.beta.rc.build.java.pr.support.10,1.1.1-alpha.beta.rc.build.java.pr.support 22 | 1.1.2,1.1.1 23 | 1.2.1,1.1.1 24 | 2.1.1,1.1.1 25 | 1.1.1-rc.1,1.1.1-rc.0 -------------------------------------------------------------------------------- /spec/test_data/splits/splits_traffic_allocation_one_percent.json: -------------------------------------------------------------------------------- 1 | { 2 | "splits": [ 3 | { 4 | "name": "Traffic_Allocation_One_Percent", 5 | "algo": 1, 6 | "trafficAllocation": 1, 7 | "trafficAllocationSeed": -1, 8 | "killed": false, 9 | "status": "ACTIVE", 10 | "defaultTreatment": "default", 11 | "seed": -1222652054, 12 | "orgId": null, 13 | "environment": null, 14 | "trafficTypeId": null, 15 | "trafficTypeName": null, 16 | "conditions": [ 17 | { 18 | "conditionType": "ROLLOUT", 19 | "matcherGroup": { 20 | "combiner": "AND", 21 | "matchers": [ 22 | { 23 | "matcherType": "ALL_KEYS", 24 | "negate": false, 25 | "userDefinedSegmentMatcherData": null, 26 | "whitelistMatcherData": null 27 | } 28 | ] 29 | }, 30 | "partitions": [ 31 | { 32 | "treatment": "on", 33 | "size": 100 34 | } 35 | ], 36 | "label": "in segment all" 37 | } 38 | ] 39 | } 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /spec/test_data/splits/whitelist_matcher_splits.json: -------------------------------------------------------------------------------- 1 | { 2 | "splits": [ 3 | { 4 | "orgId":"cee838c0-b3eb-11e5-855f-4eacec19f7bf", 5 | "environment":"cf2d09f0-b3eb-11e5-855f-4eacec19f7bf", 6 | "name":"test_feature", 7 | "trafficTypeId":"u", 8 | "trafficTypeName":"User", 9 | "seed":-195840228, 10 | "status":"ACTIVE", 11 | "killed":false, 12 | "defaultTreatment":"default", 13 | "conditions":[ 14 | { 15 | "matcherGroup":{ 16 | "combiner":"AND", 17 | "matchers":[ 18 | { 19 | "keySelector":{ 20 | "trafficType":"user", 21 | "attribute":"list" 22 | }, 23 | "matcherType":"WHITELIST", 24 | "negate":false, 25 | "userDefinedSegmentMatcherData":null, 26 | "whitelistMatcherData":{ 27 | "whitelist":[ 28 | "pro", 29 | "premium", 30 | "standard" 31 | ] 32 | }, 33 | "unaryNumericMatcherData":null, 34 | "betweenMatcherData":null 35 | } 36 | ] 37 | }, 38 | "partitions":[ 39 | { 40 | "treatment":"on", 41 | "size":100 42 | }, 43 | { 44 | "treatment":"off", 45 | "size":0 46 | }, 47 | { 48 | "treatment":"default", 49 | "size":0 50 | } 51 | ] 52 | } 53 | ] 54 | } 55 | ] 56 | } 57 | -------------------------------------------------------------------------------- /spec/unique_keys_sender_adapter_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class MemoryUniqueKeysSenderTest 4 | attr_reader :bulks 5 | def initialize 6 | @bulks = [] 7 | end 8 | 9 | def record_uniques_key(bulk) 10 | @bulks << bulk 11 | end 12 | 13 | def record_impressions_count 14 | # TODO: implementation 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /tasks/benchmark_get_treatment.rake: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | desc 'Benchmark the get_treatment method call in 4 threads' 4 | 5 | # Usage: 6 | # rake concurrent_benchmark api_key=YOUR_API_KEY base_uri=YOUR_API_BASE_URI 7 | # [iterations=NUMBER_OF_ITERATIONS] [user_id=A_USER_ID] [feature_id=A_FEATURE_ID] 8 | task :concurrent_benchmark do 9 | require 'benchmark' 10 | require 'splitclient-rb' 11 | 12 | usage_message = 'Usage: rake concurrent_benchmark api_key=YOUR_API_KEY base_uri=YOUR_API_BASE_URI \ 13 | [iterations=NUMBER_OF_ITERATIONS] [user_id=A_USER_ID] [feature_id=A_FEATURE_ID]' 14 | 15 | if validate_params 16 | execute 17 | else 18 | p usage_message 19 | end 20 | end 21 | 22 | def validate_params 23 | !ENV['api_key'].nil? && !ENV['base_uri'].nil? 24 | end 25 | 26 | def split_client 27 | api_key = ENV['api_key'].nil? ? 'fake_api_key' : ENV['api_key'] 28 | base_uri = ENV['base_uri'].nil? ? 'fake/api/' : ENV['base_uri'] 29 | SplitIoClient::SplitFactory.new(api_key, base_uri: base_uri, logger: Logger.new('/dev/null').client) 30 | end 31 | 32 | def times_per_thread 33 | iterations = ENV['iterations'].nil? ? 1_000_000 : ENV['iterations'].to_i 34 | iterations / 4 35 | end 36 | 37 | def feature_id 38 | ENV['feature_id'].nil? ? 'sample_feature' : ENV['feature_id'] 39 | end 40 | 41 | def user_id 42 | ENV['user_id'].nil? ? 'fake_id_1' : ENV['user_id'] 43 | end 44 | 45 | def execute 46 | threads = [] 47 | puts Benchmark.measure do 48 | 4.times do |_i| 49 | threads << Thread.new do 50 | times_per_thread.times do 51 | split_client.get_treatment user_id, feature_id, attr: 123 52 | end 53 | end 54 | end 55 | threads.map(&:join) 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /tasks/irb.rake: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | desc 'Open an irb session preloaded with this library' 4 | task :irb do 5 | sh 'irb -rubygems -I lib -r splitclient-rb.rb' 6 | end 7 | --------------------------------------------------------------------------------